새싹 과제에서 다크모드 대응을 추가로 해보던 중 겪은 이슈와 트러블 슈팅 내용입니다.
(나중에 회고 후 개념, 내용 다듬을 예정)
다크모드 대응하기(Dynamic color 적용)
다크모드를 위해 dynamic color로써 black/white 커스텀 컬러를 Assets에 추가함.

추가한 커스텀 컬러를 필요한 부분에 적용해보자.
과정
1. UIColor를 확장해서 보다 사용하기 쉽게 세팅
import UIKit
extension UIColor {
class var systemBlack: UIColor? {
return .init(named: "systemBlack")
}
class var systemWhite: UIColor? {
return .init(named: "systemWhite")
}
}
2. Interface Builder에서 dynamic color가 필요한 뷰에 적용


3. Interface Builder에서 적용할 수 없는 뷰들은 코드로 적용
TextField의 경우 좀 더 명확한 테두리를 주기 위해 layer.borderColor 속성에 해당 커스텀 컬러를 줬다.
func configureUI() {
searchTextField.layer.borderColor = UIColor.systemBlack?.cgColor
searchTextField.layer.borderWidth = 4.0
}
결과


대부분 의도한 대로 현재 기기의 모드에 따라 색상이 변했지만, TextField의 테두리만 변하지 않은 것을 볼 수 있었다.
textField.layer.borderColor에서 뭔가 문제가 있는 것 같다.
원인
찾아본 결과 CGColor는 Dynamic Color를 적용할 수 없었다.
다크모드에 필요한 Dynamic Color는 UITraitCollection과 엮여 필요한 색을 결정하는건데, UIKit보다 low-level인 CGColor는 UITraitCollection를 부를 수 없어서 dynamic color가 될 수 없는 것.
WWDC19에서 소개된 Implementing Dark Mode on iOS 와 다른 정리글들을 보면서 정확한 내용을 공부해보기로 했다.
다크모드 원리
UIViewController와 UIView는 traitCollection을 가짐.
이 traitCollection은 실행 디바이스의 종류, appearance(light/dark mode), fullscreen 여부 등에 관한 정보를 가지고 있음.


그림처럼 UITraitCollection.current을 통해 userInterfaceStyle의 값인 .dark를 확인하고, 그에 맞게 적절한 color를 결정하는 것.
UIKit은 특정 메소드를 호출할 때마다 Current Trait Collection을 설정하는데, 예를들면 UIView의 draw 메소드가 있음.


draw와 layout 관련 메소드들을 호출하면, 각 객체들은 UITraitCollection.current를 자신의 traitCollection 프로퍼티에 알려줌으로써 현재 traitCollection에 대해 알 수 있다.
또, traitCollection이 변경되면 이를 알 수 있는 메소드들도 있다(traitCollectionDidChange, tintColorDidChange).
이어서 wwdc에서는 dynamic color가 적용되지 않는 cgColor에 대해 대응하는 방법이 소개되었는데 그전에,
🔥 Big point to keep in mind
위의 메소드들을 제외하면, current trait collection을 보장할 수 없다. 즉 현재 시점의 기기가 어떤 모드인지를 확인할 수 없어 dynamic color를 적용할 수 없다.
해결방법
low-level인 CGColor는 dynamic color를 이해할 수 없음. Dynamic color는 UIKit 개념임.
따라서 우리가 직접, traitCollection에 접근해서 그에 맞는 색상을 결정해주면 됨. resolved color를 구하면 됨.
1. traitCollection을 사용해서 resolvedColor를 구하는 방법

- 단, 적용하고자 하는 dynamic color가 많을 경우 복잡해짐.
- 매번 이 방식으로 적용해야 함.
2. traitCollection의 performAsCurrent를 호출하고, 클로저 내부에서 cgColor를 지정하는 방법

- performAsCurrent의 실행부 클로저에서, dynamic color의 색상을 결정할 것임(resolved Color).
3. 직접 current Trait Collection을 설정하는 방법

- absolutely safe, lightweight, no side effects.
- 백그라운드 스레드도 안전함. 실행중인 특정 스레드에만 영향을 끼침.
해결방법 + Trait Collection을 사용하려면

Trait이 초기화되거나 업데이트 될 때마다 traitCollectionDidChange 메서드가 호출될 것 같지만, 처음 뷰가 로드될 때는 호출되지 않고, 모드에 변화가 발생할때만 호출됨!
따라서 traitCollectionDidChange 메서드에서 사용하는 것은 약간 모자람.

Trait Collections을 사용하려면 layout 관련 메소드에서 사용하는게 베스트다 베스트.
정리하자면 디바이스의 모드 변경을 감지하고, 그에 맞는 적절한 resolved color를 쓰고 싶다면?
✅ View, ViewController의 layout 관련 메서드에서 사용해라
물론 dynamic color(UIColor)는 알아서 변경하니까 상관없고, cgColor 쓸 때...!
해결코드
class NewlyCoinedWordViewController: UIViewController {
@IBOutlet var searchTextField: UITextField!
@IBOutlet var searchResultLabel: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
configureUI()
configureData()
searchTextField.delegate = self
}
// For Dynamic Color with CGColor
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
searchTextField.layer.borderColor = UIColor.systemBlack?.cgColor
}
@IBAction func didBackgroundTouched(_ sender: UITapGestureRecognizer) {
view.endEditing(true)
}
@IBAction func didSearchButtonTouched(_ sender: UIButton) {
view.endEditing(true)
searchNewlyCoinedWord(searchTextField.text!)
}
}
3번 방법으로 적용했었는데, 생각해보면 trait이 갱신되는 시점이 viewWillLayoutSubviews이므로, viewWillLayoutSubviews를 호출할 때마다 layer.borderColor에 dynamic color의 cgColor를 적용하게끔만 해도 원하는대로 적용되는 것을 확인할 수 있었다.
trait이 갱신될 때마다 그에 맞는 dynamic color의 resolvedColor를 cgColor로 변환해서 적용한 것!
테스트

- 처음 viewController가 로드되고 난 뒤, viewWillLayoutSubViews()가 호출됨
- 이후 디바이스의 모드를 변경할 때마다 traitCollectionDidChange(), viewWillLayoutSubviews()가 호출됨


결론
✅ traitCollection의 변경을 알 수 있는 layout 메서드에서 layer.borderColor를 지정하면, 모드에 맞는 resolvedColor가 매번 적절하게 지정된다
'트러블슈팅' 카테고리의 다른 글
UITextField secureTextEntry 설정시 키체인 영역 안나오게 하기 (0) | 2023.11.22 |
---|---|
CollectionView 페이징시 스크롤 멈춤현상 해결하기(feat. Activity Indicator, RefreshControl) (0) | 2023.09.24 |
Navigation Bar의 SearchBar에서 키보드 내리기(feat. resignFirstResponder) (0) | 2023.08.09 |