iOS) 다크모드 대응

4 minute read

내용

  • 2가지 방법으로 다크 모드를 대응해보자
  • Color Assets, UIColor extension
  • 라이트 / 다크 모드를 제한해 보자

먼저 Xcode 와 시뮬레이터에서 라이트 / 다크 모드를 확인할 수 있는 방법을 알아보자.

  • 다음과 같이 Xcode 에서 Appearance 를 조절하면 런타임 중에도 확인 가능하다.

스크린샷 2021-10-11 오후 3 10 03

  • 시뮬레이터에서 [설정] → [개발자] → [Appearacne] 에서 토글로 모드를 바꿀 수 있다.

자 이제 대응하는 2가지 방법을 알아보자!

😎 1.Color Assets 으로 대응하기

  • color assets 를 위한 새로운 파일을 추가하고 싶다면 Asset Catalog 를 추가해서 Color Set 를 추가하거나

스크린샷 2021-10-11 오후 2 28 27

  • 기존의 Asset 에서 + 버튼을 눌러서 Color Set 을 추가해도 된다.

스크린샷 2021-10-11 오후 2 27 52

  • Appearance 에서 None 이 아닌 Any, Dark 를 선택해준다. (Any, Light, Dark 를 골라도 가능하다 대신 색을 3개를 지정해주어야 한다. 편의상 Any, Dark 를 선택해도 상관없다.)

스크린샷 2021-10-11 오후 2 32 08

  • 다음과 같이 다크모드일 경우 컬러도 등록해준다. backgroundColorAsset 로 이름을 지정해주었다.

스크린샷 2021-10-11 오후 2 33 57

  • 이제부터 스토리보드에서 사용가능하다! (스토리보드에서 Appearance 를 조절하면 그에 맞는 색으로 바뀐다.)

스크린샷 2021-10-11 오후 2 35 50

  • backgroundColorAsset 이 라이트모드에서는 하얀색, 다크모드에서는 검은색을 보여준다. (우리가 아는systemRed, systemBackground 등 System Color 는 모드에 따라 색이 서로 다르다)

스크린샷 2021-10-11 오후 2 36 03

  • System Background 로 배경색을 설정해두면 Appearance 를 변경해주면 다음과 같이 바뀐다. 또한 Appearance 를 선택해서 모드에 따른 뷰의 변화를 편리하게 볼 수 있다.

스크린샷 2021-10-11 오후 2 42 52

Color Assets programmatically 하게 사용하기

이렇게 추가한 Color Assets 은 Image Assets 와 동일하게 programmatically 하게 사용가능하다.

extension 으로 UIColor 상수를 만들어주었다.

extension UIColor {
    static let backgroundColorAsset = UIColor(named: "backgroundColorAsset")
}
// 사용
// view.backgroundColor = .backgroundColorAsset

그렇다면 이미지에 대해서도 다크모드 적용이 가능할까?

Appearances 를 Any, Dark 로 설정해서 이미지를 추가할 수 있다.

스크린샷 2021-10-11 오후 3 01 14

if let image = UIImage(named: "profileImage") {
            profileImage.image = image
}

😎 2.UIColor extension 으로 대응하기

먼저, UITraitEnvironment 에 대해서 알아보자.

iOS interface environment 을 앱에서 사용할 수 있도록 하는 메서드의 세트. 프로토콜이다.

iOS 인터페이스 환경에는 다음과 같은 trait 가 포함된다고 한다.

  • horizontal / vertical size class
  • display scale
  • user interface idiom
  • user interface style

우리는 여기서 user interface style 즉 라이트 / 다크 모드가 앱에서 필요하다. trait environment 에 접근하기 위해서는 UITraitCollection 클래스의 traitCollections 프로퍼티를 사용해야 한다.

출처

iOS ) UITraitEnvironment와 traitCollectionDidChange

extension UIColor {
    static var defaultLabelColor: UIColor {
        if #available(iOS 13, *) {
            return UIColor { (traitCollection: UITraitCollection) -> UIColor in
// ✅ UITraitCollection 의 userInterfaceStyle : 라이트인지 다크인지 알려준다.
                if traitCollection.userInterfaceStyle == .light {
                    return .black
                } else {
                    return .white
                }
            }
        } else {
            return .black
        }
    }
}
// 사용
// textLabel.textColor = .defaultLabelColor

위와 같이 커스텀하게 설정할 수 있다.

또한 아래와 같이 system color 를 사용할 수도 있다. 하지만 iOS 13 부터 적용가능하기 때문에 이전 버전들의 경우도 대응해주어야 한다.

System color 로 대응하기

static var backgroundColor: UIColor {
        if #available(iOS 13.0, *) {
            return .systemBackground
        } else {
            return .white
        }
    }

다양한 시스템 컬러에 대해서 알아보려면 아래의 출처를 참고하자.

Color - Visual Design - iOS - Human Interface Guidelines - Apple Developer

⁉️문제

라이트 모드에서는 검은색 그림자가 다크 모드에서는 하얀색 그림자를 만들고 싶었다.

하지만 shadow 의 색이 대응이 되지 않았다..! (라이트에서 다크로 변경했기 때문에 초기에 설정된 그림자만 적용됨.)

⁉️ 해결

UIView 에 윤곽선 혹은 그림자를 적용할 때 CALayer 에 색상을 적용한다. 이 때는 UIColor 가 아닌 CGColor 객체를 shadowColor 의 객체로 사용하게 된다.

CGColor, NSTextAttachment 는 디바이스의 Appearances 값이 바뀔때 자동으로 바뀌지 않는다. 그래서 직접 바꿔 줘야 한다. 그림자의 색은 CGColor 와 관련있기에 다음과 같이 진행했다.

// traitCollection 이 변경될 때 호출되는 메서드
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
// 반드시 호출해야함. view 계층 구조에서 더 높은 인터페이스 요소가 먼저 레이아웃을 조정할 수 있도록 하기 위함.
        super.traitCollectionDidChange(previousTraitCollection)
        guard #available(iOS 13, *) else { return }

// ✅ 지정된 특성 컬렉션(previousTraitCollection)과 현재 특성 컬렉션 간의 변경이 색상 값에 영향을 미치는지 여부를 묻는다.
        guard traitCollection.hasDifferentColorAppearance(comparedTo: previousTraitCollection) else { return }
// ✅ 이전 traitCollection 의 userInterfaceStyle 와 비교.
        guard traitCollection.userInterfaceStyle != previousTraitCollection?.userInterfaceStyle else { return }
        qrcodeBackView.layer.shadowColor = UIColor.shadowColor.cgColor
}
// UI 를 초기화하는 과정에서도 
// qrcodeBackView.layer.shadowColor = UIColor.shadowColor.cgColor
// 추가해줘야 한다. 왜냐면 traitCollectionDidChange(_:) 메서드는 traitCollection 이 변경될 때 호출되기 때문이다.

이외에도 특정한 상황에서 특별한 동작을 부여하고 싶을 때 위의 메서드를 오버라이드 해서 사용한다고 한다.

✅ 로 표시된 주석 두개 중 아무거나 사용해도 괜찮다. 같은 의미다.

그림자가 잘 안보여서 굵기를 키웠다. 라이트모드에서는 검은색 그림자가 다크모드에서는 빨강 그림자가 나오게했다.

📌 라이트 / 다크 모드 제한시키기

여건에 따라서는 특정 모드로 제한해야하는 경우도 있다. 그렇다면 라이트/다크 모드로 제한하는 2가지 방법도 알아보자.

😎 1.info.plist 설정

라이트모드로만 제한해보자.

스크린샷 2021-10-11 오전 10 57 00

Appearance 혹은 UIUserInterfaceStyle key 에 고정시키고 싶은 모드 value 를 넣어주면 된다.

(라이트모드만 가능 → Light / 다크모드만 가능 → Dark)

😎 2.programmatically 하게 설정

  • AppDelegate, View Controller, UIView 에서 설정

AppDelegate 설정(Window 에서 설정)

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // ✅ iOS 13 부터 다크모드가 적용되므로 다음과 같은 조건문 성립.
        if #available(iOS 13.0, *) {
            // ✅ iOS 13 부터는 다크모드로만 제한.
            self.window?.overrideUserInterfaceStyle = UIUserInterfaceStyle.dark
        } else {
        // Fallback on earlier versions
        }
}

View Controller 에서 설정

override func viewDidLoad() {
    super.viewDidLoad()
    if #available(iOS 13.0, *) {
        overrideUserInterfaceStyle = .dark
    } else {
        // Fallback on earlier versions
    }
}

UIView 에서 설정

let view = UIView()
if #available(iOS 13.0, *) {
    view.overrideUserInterfaceStyle = .dark
} else {
    // Fallback on earlier versions
}

Categories:

Updated: