iOS의 다크모드 - (2) 구현

저번 글에서는 다크 모드를 적용해야 하는 이유에 대하여 설명했다면, 이번에는 실제로 배포 중인 앱에 다크 모드를 적용하면서 사용했던 여러가지 기능들에 대한 이야기를 해 보려고 합니다.

Color Assets

Xcode 에서 제공하는 Asset 파일인 .xcassets 에는 생각보다 여러 종류의 데이터를 저장할 수 있습니다. 그 중 Color 데이터를 Asset 파일에 저장하면 다크 모드 대응을 조금 더 쉽게 진행할 수 있습니다.

Asset 파일을 열어 Attribute Inspector 를 확인해 보면 Appearances 항목이 보이는데 해당 항목에서 Any, Dark 또는 Any, Dark, Light 를 선택해 줄 경우 다크 모드에서 사용할 색상을 지정해 줄 수 있습니다.

여기서 지정한 색상 값은 Interface Builder 또는 Storyboard 에서 바로 사용이 가능하며, 코드 내에서도 불러와 사용할 수 있습니다. 코드에서 불러 올 때는 이미지를 불러올 때 처럼 UIColor.init(named:) 생성자를 사용해 색상을 불러올 수 있습니다.

다크 모드 색상 데이터 선언하기
https://s3-us-west-2.amazonaws.com/secure.notion-static.com/cdf5ea37-2ae4-430c-a5b4-5af6e57394d8/_2020-04-22__11.37.42.png
Asset 으로 선언한 색상 바로 사용하기

System Colors

iOS offers a range of system colors that automatically adapt to vibrancy and changes in accessibility settings like Increase Contrast and Reduce Transparency. The system colors look great individually and in combination, on both light and dark backgrounds, and in both light and dark modes.

Color - Visual Design - Human Interface Guidelines - Apple Developer

iOS 13에서는 시스템에서 미리 선언된 색상들이 있는데, 이 색상 들은 색상 관련 설정에 따라 자동으로 보기 좋은 색상으로 전환해 주는 기능을 가지고 있습니다. 그 중에 Dynamic System Color 들은 다크 모드 전환시 색이 반전되어 다크 모드 구현에 많은 도움이 됩니다.

문제는 이 시스템 색상 값들은 iOS 13에서 새로 추가된 내용이기 때문에 아래와 같이 UIColor 클래스의 Extension 을 이용하여 사용할 색상들을 정의 해 주는 것을 추천합니다. iOS 13 이하 버전에서 사용할 색상 들은 Asset 에서 불러 오거나 직접 만들어서 전달할 수 있습니다.

import UIKit

extension UIColor {
	static var accentRed: UIColor {
        if #available(iOS 13, *) { return .systemRed }
        return UIColor(named: "AccentRed")!
    }
    static var bgColor: UIColor {
        if #available(iOS 13, *) { return .systemBackground }
        return UIColor(red: 229/255, green: 234/255, blue: 235/255, alpha: 1.0)
    }
}

Template Image

앱을 작성할 때는 UI 구현을 위해 여러가지 이미지들을 사용하게 됩니다. 다크 모드에 맞는 이미지를 적용하기 위해서는 상황에 따라 다른 방법을 사용할 수 있습니다.

첫 번째, 이미지에 색상이 여러 개 사용 된 경우에는 Assets 내부에 다크 모드와 어울리는 이미지를 추가 등록해 주세요. Assets 내부에 이미지를 등록할 때 Appearance 값을 조정하게 되면 다크 모드에서 사용할 이미지를 추가 등록할 수 있습니다. 이렇게 이미지를 등록해 두면 기기에서 Appearance 값이 바뀌게 될 때 시스템에서 알아서 바꿔 주게 됩니다.

두 번째, 아이콘으로 사용될 단색 이미지의 경우에는 Template Image 기능을 사용하는 것이 좋습니다. Template Image 로 렌더링 된 이미지의 경우에는 View 에 지정된 Tint Color 가 이미지에 입혀져 보이게 되므로, 어떤 색상을 사용하든 하나의 이미지를 사용할 수 있게 됩니다. 이렇게 사용 할 경우 이미지 개수가 줄어들 게 되므로 번들 용량을 줄이는 데에 도움이 될 수 있습니다.

https://s3-us-west-2.amazonaws.com/secure.notion-static.com/2d406cd3-79f8-4b98-be5e-e3d5ce84ddd2/_2020-04-22__10.18.57.png
다크모드에서 사용할 이미지 등록
https://s3-us-west-2.amazonaws.com/secure.notion-static.com/a0663be0-b9b5-4b13-9120-1555668294d1/_2020-04-22__10.22.38.png
이미지 렌더링 타입을 템플릿 이미지로 설정

Handle Layer Color

View 에 윤곽선을 그리거나 그림자를 적용할 때 흔히 View 가 가지고 있는 Layer 에 색상을 적용하곤 합니다. 이 때는 UIColor 가 아닌 CGColor 객체를 Layer 의 속성으로 사용하게 되는데, CGColor 의 경우 디바이스의 Appearances 값이 바뀔 때 알아서 다크 모드에 맞는 색상으로 바뀌지 않기 때문에 직접 Layer 에 할당된 색상을 바꿔 줘야 하는 상황이 생기게 됩니다.

이럴 때는 UITraitEnvironment 프로토콜을 가지고 있는 UIView 또는 UIViewController 에서 아래와 같이 traitCollectionDidChange(_ previousTraitCollection:) 메소드를 Overriding 하여 Appearances 변경에 대응할 수 있습니다.

UITraitCollection 객체 내에는 객체 간에 다른 Color Appearance 를 가지고 있는지 판별할 수 있는 메소드가 존재합니다. 다만, 해당 메소드는 다크 모드 적용 이외에 다른 케이스도 같이 확인할 때가 있기 때문에 추가로 다크 모드 적용 여부에 대한 property 인 userInterfaceStyle 도 함께 체크 해 줘야 합니다.

private func updateTextFieldBorder() {
    emailFieldWrapperView.layer.borderWidth = 0.5
    emailFieldWrapperView.layer.borderColor = UIColor.fineLineColor.cgColor
}

override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
	super.traitCollectionDidChange(previousTraitCollection)

	guard #available(iOS 13, *) else { return }
    guard traitCollection.hasDifferentColorAppearance(comparedTo: previousTraitCollection) else { return }
    guard traitCollection.userInterfaceStyle != previousTraitCollection?.userInterfaceStyle else { return }
    
    updateTextFieldBorder()
}

참고 자료