티스토리 뷰

🍏/iOS

[Dark Mode] Dynamic Color의 원리 / cgColor 대응

사용자 eungding 2020. 12. 2. 17:36
728x90
반응형

[1] Dynamic Color란

 

iOS 13부터 Dynamic Color가 지원되었습니다.

하나의 컬러이지만 라이트모드일때, 다크모드일때 각각 다른 컬러를 가지고 있고 상황에 맞게 적용됩니다.  

 

systemBackground 같은 시스템 컬러를 쓰거나 

Color Asset에서 원하는 커스텀 컬러를 지정해줄 수 있습니다.

 

 

아래의 코드를 실행시키고 라이트, 다크 모드로 변환시켜보면 잘되는 것을 볼 수 있습니다. 

class ViewController: UIViewController {
    
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = UIColor.systemBackground
    }
}

 

 

 

 

[2] Dynamic Color의 원리 (1)

 

그러면 어떻게 라이트 모드인지, 다크모드인지 판단해서 컬러를 저렇게 알아서 잘 바꿔줄까요?! 

 

(Implementing Dark Mode on iOS WWDC 영상의 19분부터 관련 내용이 나옵니다)

 

 

 

그것은 바로 traitCollection 때문입니다. 

viewController와 view는 traitCollection을 가지는데 

 

여기에는

- 실행하고 있는 기기가 아이폰, 아이패드, carplay 인지,

- appearance가 라이트모드/다크모드 인지,

- fullscreen인지 (base가 fullscreen말하는 거라고 하심)

 

관한 정보가 들어있다고 합니다. 

 

 

즉 Dynamic Color와 UITraitCollection이 협력해서 (콤비네이션)

color를 결정할 수 있는 것입니다. (final resolved color를 구하게 된다! 라고 표현하심)

 

 

 

[3] 직접 resolved Color 구하기

 

이렇게 final resolved color를 구하는 것은 자동으로 되지만

개발자가 직접 구할 수 도 있다고 합니다. 

 

 

시스템에서 하는 것처럼 

dynamicColor와 traitCollection을 준비하고

resolvedColor를 구하는 것입니다. 

 

이렇게 구한 resolvedColor는 더 이상 다이나믹컬러가 아니라고 합니다

즉 현재 traitCollection이 라이트모드라면 라이트모드 컬러, 다크모드라면 다크모드일때 컬러이지 

다이나믹 컬러가 아닙니다. 

 

 

테스트 해볼게요! 

class ViewController: UIViewController {
    
    override func viewDidLoad() {
        super.viewDidLoad()
        let dynamicColor = UIColor.systemBackground
        let traitCollection = self.traitCollection
        let resolvedColor = dynamicColor.resolvedColor(with: traitCollection)
        view.backgroundColor = resolvedColor
    }
}

 

라이트 모드일때로 resolved 컬러는 잘 구했지만

런타임때 Appearance를 바꿔도 변하지 않습니다. 당연히 다이나믹 컬러가 아니니까요-!

 

 

디바이스를 다크모드 설정으로 바꾸고 해봐도 위와 동일한 결과입니다. 

 

 

[4] 직접 Dyanmic Color 만들기

 

Color Asset에서 말고 

 

 

코드로도 다이나믹 컬러를 만들 수 있다고 합니다. 

 

 

 

[5] Dynamic Color 원리 (2)

 

위의 코드처럼 traitCollection을 넘겨주지 않았는데 어떻게 traitCollection을 알고 다이나믹 컬러가 자동으로 동작했을까요?!

이 질문과 함께 dynamic color의 원리에 대해 이어서 설명해주십니다.

 

 

traitCollection과 color를 직접 개발자가 조합해주지 않아도 ( [4] 직접 dynamic color 만들기 처럼)

UITraitCollection.current를 통해 다이나믹 컬러는 알아서 traitCollection 정보를 알 수 있기 때문입니다. 

 

 

 

UIKit은 아래 메소드가 불리기 전에 UITraitCollection.current를 set해준다고 하는데요 (??) 

 

 

정확히 무슨 말인지 모르겠지만 실험해볼게요-!

라이트모드, 다크 모드가 바뀔때마다 저 메소드가 불리고 

여기서  UITraitCollection.current.userInterfaceStyle을 검사해보면 바뀐 값으로 잘 줍니다.

class ViewController: UIViewController {
    
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = UIColor.systemBackground
    }
    
    override func viewWillLayoutSubviews() {
        super.viewWillLayoutSubviews()
        print(#function)
        print(UITraitCollection.current.userInterfaceStyle == .dark)
    }
    
    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        print(#function)
        print(UITraitCollection.current.userInterfaceStyle == .dark)
    }
}

 

 

그리고 traitCollection이 바뀌었을때를 알 수 있는 메소드도 제공하고 있어요

 

 

 저는 뷰컨트롤러에서 이렇게 테스트해봤어요!

    override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
        if let previousTraitCollection = previousTraitCollection {
            print(#function)
            print(previousTraitCollection.userInterfaceStyle == .dark)
            print(UITraitCollection.current.userInterfaceStyle == .dark)
        }
    }

 

다크 -> 라이트로 바꿨을때 

결과는 

traitCollectionDidChange(_:)
true
false

 

⚠️ 그리고 사진에 있는 저 메소드들 밖에서 UITraitCollection.current를 부르면 결과를 보장하지 않으니까

주의하라고 합니다. ⚠️

 

그럴때는 직접 매니징해줘야한답니다!! 

 

[6] cgColor 다이나믹하게 설정하기

 

사실 UITraitCollection.current를 직접 부를일이 없는데 어떤 경우가 있을까요?!?!

바로 UIKit말고 low-level을 건드릴때입니다. 

예를 들어 cgColor를 설정해줄때가 있겠습니다. low-level 인 cgColor는 다이나믹 컬러가 될 수 없다고 합니다. 

그래서 개발자가 직접 resolved color를 구해서 넣어줘야합니다. 

 

UITraitCollection.current 값을 보장할 수 있는 세가지 메소드 (위의 사진 참고) 에서 해주면 되는데요 

 

traitCollectionDidChange에서 아래 처럼 해주는 것을 권장하고 있습니다!

 

 

하지만 traitCollectionDidChange는 초기화에서는 안불리고 change가 있을때 불리는 점을 기억해야합니다.

traits는 초기화될때 이미 예측되기때문에 (부모의 trait을 물려받는 다고 함.)

저 메소드가 안불린다고 합니다. 

 

그러니까 처음 뷰가 로드될때는 안불리고 라이트, 다크 모드로 바뀔때 불립니다.

 

 

그래서 여기서 해주는게 좋다고 말합니다. 

 

 

저는 traitCollectionDidChange 에서 해주고 싶었지만

그럼 viewDidLoad에서도 cgColor 설정해주는 코드(초기화 코드)를 또 작성해야하니까

일단 viewWillLayoutSubviews에서 테스트해보겠습니다.

 

우선 border 컬러를 이렇게 만들어주었어요.

 

 

그리고 목표는 라이트/다크 모드에 따라 border color를 바꿔주기 입니다. 

 

 

 

 

 

6.1  첫번째 방법

 

 

class ViewController: UIViewController {
    
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = UIColor.systemBackground
        view.layer.borderWidth = 15
    }
    
    override func viewWillLayoutSubviews() {
        super.viewWillLayoutSubviews()
        let dynamicColor = UIColor(named: "border")!
        let resolvedColor = dynamicColor.resolvedColor(with: self.traitCollection)
        view.layer.borderColor = resolvedColor.cgColor
    }
}

 

참고로 이거 세개 다 결과가 똑같아서 아무거나 해주면 됩니다. 

let resolvedColor = UIColor.systemBackground.resolvedColor(with: UITraitCollection.current)
let resolvedColor = UIColor.systemBackground.resolvedColor(with: self.view.traitCollection)
let resolvedColor = UIColor.systemBackground.resolvedColor(with: self.traitCollection)

 

 

이 방법의 단점은 cg color를 여러개 설정해줘야할때

resolvedColor를 다 구해줘야하니까 

불편하다는 점 입니다. 

 

 

6.2 두번째 방법

 

 

첫번째 방법 보다 더 쉬운 방법으로,

performAsCurrent 클로져 안에서 cgColor를 지정해주면 right value를 얻을 수 있다고 합니다.

 

 

class ViewController: UIViewController {
    
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = UIColor.systemBackground
        view.layer.borderWidth = 15
    }
    
    override func viewWillLayoutSubviews() {
        super.viewWillLayoutSubviews()
        self.traitCollection.performAsCurrent {
            view.layer.borderColor = UIColor(named: "border")!.cgColor
        }
    }
}

 

 

 

 

6.3 세번째 벙법

 

 

완전 안전하고 사이드 이펙트도 없는 방법이라고 합니다. 

 

 

class ViewController: UIViewController {
    
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = UIColor.systemBackground
        view.layer.borderWidth = 15
    }
    
    override func viewWillLayoutSubviews() {
        super.viewWillLayoutSubviews()
        let savedTraitCollection = UITraitCollection.current
        
        UITraitCollection.current = self.traitCollection
        view.layer.borderColor = UIColor(named: "border")!.cgColor
        
        UITraitCollection.current = savedTraitCollection
    }
}

 

흠...

근데 viewWilLayoutSubviews에서는 UITraitCollection.current가 보장된 값이라고 했고

UITraitCollection.current 구해보면 바뀐 appearacne로 잘나오는데 

왜 저렇게 해주는 지 모르겠네용,,, 

 

이렇게만 해도 잘 동작합니다. 

class ViewController: UIViewController {
    
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = UIColor.systemBackground
        view.layer.borderWidth = 15
    }
    
    override func viewWillLayoutSubviews() {
        super.viewWillLayoutSubviews()
        view.layer.borderColor = UIColor(named: "border")!.cgColor
    }
}

 

 

 

 

 

[ Reference ]

developer.apple.com/videos/play/wwdc2019/214/

 

Implementing Dark Mode on iOS - WWDC 2019 - Videos - Apple Developer

Hear from the UIKit engineering team about the principles and concepts that anchor Dark Mode on iOS. Get introduced to the principles of...

developer.apple.com

 

[ 더 읽어보기 ]

정리 잘되어있는 블로그!

 

hcn1519.github.io/articles/2020-03/ios_darkmode

 

iOS 다크모드 알아보기

iOS에서 다크모드를 적용하기 위해 알아야 하는 내용을 정리하였습니다.

hcn1519.github.io

 

 

728x90
반응형
댓글
댓글쓰기 폼