iOS

[iOS] 다크모드 지원: CGColor에 Dynamic Color 적용하기

burgerkinghero 2022. 11. 19. 15:08

 

새 회사에서 간단한 스펙들부터 처리하면서 천천히 팀에 녹아들고 있다. 그러다보니 주로 비즈니스 로직과 별도의 UI 관련 이슈들 위주로 보고 있는데, 그 중에 상당히 골아팠던 이슈가 있어서 기록해보려한다!

 

1. 다크모드 지원하기

다크모드를 지원하는 앱을 개발할 때 light, dark 모드에 각각 대응하는 컬러셋을 만들어주는 것이 일반적이다.

이렇게!

이렇게 해두면 light <-> dark 모드 간 전환 시 색상이 바뀌어야하는 뷰들에 대해 매번 분기를 칠 필요 없이 위 색상으로 한번만 assign 해주면 된다. 코드로 예를 들어보면 아래와 같이 작성할 수 있을 것이다.

self.dynamicColorTestView.backgroundColor = UIColor(named: "DynamicColorSet")

아주 간단하게 개발할 수 있도록 애플이 야무지게 도와줬다. 이렇게 모드에 따라 색상이 알아서 변하도록 지원되는 걸 Dynamic Color라고 한다.

 

하지만 Dynamic Color가 적용될 수 있는 범위는 UIKit까지다. 더 로우레벨(CALayer 등)로 내려가면 색상이 바뀌지 않는다.

이번에 내가 처리해야했던 이슈도 CGColor에 Dynamic Color가 적용되지 않는 것이 원인이었다.

 

2. 테스트

아래 영상을 보자.

아래 뷰의 borderColor 변화가 없다.

 

이 영상에서 상단에 있는 뷰는 backgroudColor에, 하단에 있는 뷰는 borderColor에 위에서 에셋에 추가한 Dynamic Color를 적용했다. 차이점이 있다면 backgroundColor에는 UIColor가, borderColor에는 CGColor가 대입된다.

 

라이트모드에서는 빨간색, 다크모드에서는 파란색으로 나와야 정상이다. 테스트 결과는 보다시피 CGColor가 Dynamic Color를 이해하지 못해 적용되지 않는 것을 알 수 있다.

 

3. UITraitCollection

WWDC 영상 Implementing Dark Mode on iOS의 19분대부터 약 6분정도 Dynamic Color, UITraitCollection에 대해서 설명하고 있다. 이 내용의 핵심은 위에서 언급했듯 1. 로우레벨은 Dynamic Color를 이해하지 못하며, 2. 이런 경우에는 UITraitCollection을 활용하라는 것이다. 그렇다면 UITraitCollection이 뭘까?

 

UITraitCollection은 iOS 인터페이스 환경에 대한 정보를 담고 있는 클래스로 모든 뷰컨트롤러와 뷰가 들고있는 값이다. 이 UITraitCollection이 가진 정보 중에 userInterfaceStyle이라는 현재 라이트모드인지 다크모드인지에 대한 정보가 들어있다.

 

실제로 Dynamic Color가 작동할 때 이 userInterfaceStyle이 관여한다. 이 값을 바라보고 어떤 색상을 보여줄지 시스템이 자동으로 판단하는 것이다.

 

그렇다면 이런 Dynamic Color 메커니즘을 이해하지 못하는 CGColor에 대해서는 어떻게 라이트/다크모드 전환을 대응해야할까? resolvedColor(with:) 메소드를 사용한다. 이 메소드는 "Returns the version of the current color that results from the specified traits."라고 정의되어있다. 즉, 특정 trait 정보에 대응되는 색상을 반환한다. 아래와 같이 사용할 수 있다.

self.dynamicColorTestView.layer.borderColor = UIColor(named: "DynamicColorSet")?.resolvedColor(with: self.traitCollection).cgColor

UIKit 레벨까지는 이렇게 부가적인 처리를 해주지 않아도 말 그대로 Dynamic하게 시스템에서 처리해주었지만 CGcolor는 이런 별도의 과정이 필요하다.

 

4. 아직 한 발 남았다..!

놀랍게도 viewDidLoad에서 resolvedColor(with:) 메소드를 통해 색상을 적절히 assign 해줘도 위의 영상처럼 그대로다. 이 이유는 traitCollection의 정보가 보장되지 않기 때문이다.

 

UITraitCollection에는 UITraitCollection.current 값이 있는데 이름 그대로 현재 상태를 나타낸다. 주의할 점은 이 값이 정확하게 설정되는 것이 항상 보장되지 않는다는 것이다.

 

WWDC 영상에 따르면 아래 메소드들 중 회색 글씨로 표기된 메소드들이 호출될 때 UITraitCollection.current 값이 정해진다. (초록색을 표기된 메소드들은 각각의 객체에서 trait가 바뀌는 시점을 잡을 수 있는 메소드)

즉, 정확한 현재 traitCollection 값을 resolvedColor(with:) 메소드에 전달하여 올바른 색상을 표기하기 위해서는 layoutSubviews를 override해서 resolve하는 코드를 넣어주어야 한다는 것이다.

 

override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()
    self.dynamicColorTestView.layer.borderColor = UIColor(named: "DynamicColorSet")?.resolvedColor(with: self.traitCollection).cgColor
}

 

5. 최종 결과

성공!!