티스토리 뷰

반응형

 

 

안녕하세요 :) Zedd입니다.

기록용 글입니다.

 

# 문제

1. 다크모드 작업 중

2. 이 프로젝트는 light Mode만 지원하는 상태.

== 디바이스가 Dark Mode여도 내 앱은 여전히 Light Mode로 나온다.

다크모드를 opt-out하는 방법은 여러가지가 있는데, 

 

https://stackoverflow.com/a/56546554

 

AppDelegate에서 이런식으로 지정하고 있다.

3. 디바이스/시뮬레이터를 다크모드로 지정한다 < 핵중요!!!!!!!!!!!!!!!

4. Color Asset을 만든다.

5. 버튼에 borderColor를 줘야함

self.myButton.layer.borderWidth = 0.5
self.myButton.layer.borderColor = UIColor(named: "borderColor")?.cgColor

평범한 코드.

 

근데 여기서 문제가 생긴다.

진짜 이건 왜 그러는지 모르겠는데..

평범한 스토리보드에 UIButton과 UITextField가 있다.

override func viewDidLoad() {
    super.viewDidLoad()
    
    self.myButton.layer.borderColor = UIColor(named: "borderColor")?.cgColor
    self.myButton.layer.borderWidth = 0.5
    
    self.textField.addTarget(self, action: #selector(textFieldDidChange), for: .editingChanged)
}

viewDidLoad에서 myButton에 borderColor와 borderWidth를 줬다.

그리고 UITextField가 수정될 때 마다 textFieldDidChange를 호출해준다.

textFieldDidChange는 다음과 같다. 

@objc
func textFieldDidChange() {
    self.myButton.layer.borderColor = UIColor(named: "borderColor")?.cgColor
    self.myButton.layer.borderWidth = 0.5
}

그렇다. viewDidLoad에서 했던것처럼 똑같이!!!! borderColor와 borderWidth를 줬다.

 

자..그럼 생각해보자.

1. 현재 내 프로젝트의 AppDelegate에는 (SceneDelegate가 없는 프로젝트.)

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    if #available(iOS 13.0, *) {
        self.window?.overrideUserInterfaceStyle = .light
    } else {
        // Fallback on earlier versions
    }
    return true
}

이렇게 코드가 들어가있고,

 

2. 나는 14.0 시뮬레이터에서 돌릴 것이기 때문에 조건문을 통과하게 된다. 즉,

self.window?.overrideUserInterfaceStyle = .light

이게 불린다는거다. 

내 디바이스/시뮬레이터가 Dark Mode여도 내 앱은 Light Mode로 나온다는거다. 

 

3.

UIColor(named: "borderColor")

borderColor는 

이렇게 생겼다.

Light Mode일땐 Any Appearance꺼가 불릴거기 때문에 연한 회색이 나온다.

Dark Mode일때는 검정색 같은 색깔이다.

내 프로젝트에서는 뭐다? 

self.window?.overrideUserInterfaceStyle = .light

==> 연한 회색이 나올것. 

 

3. 14.0 시뮬레이터에서 빌드를 한다. (이 시뮬레이터는 Dark Mode이다)

 

4. viewDidLoad가 불린다.

override func viewDidLoad() {
    super.viewDidLoad()
    
    self.myButton.layer.borderColor = UIColor(named: "borderColor")?.cgColor
    self.myButton.layer.borderWidth = 0.5
    
    self.textField.addTarget(self, action: #selector(textFieldDidChange), for: .editingChanged)
}

5. myButton에 borderColor가 들어간다. 위에서 말했듯이 연한 회색이 들어간다. 

왜냐? 내 앱은 Light Mode로 지정해놨기 때문이다. 

 

 

오 잘 나오는 것을 볼 수 있다!

 

6. 

self.textField.addTarget(self, action: #selector(textFieldDidChange), for: .editingChanged)
@objc
func textFieldDidChange() {
    self.myButton.layer.borderColor = UIColor(named: "borderColor")?.cgColor
    self.myButton.layer.borderWidth = 0.5
}

viewDidLoad에 수정될 때 마다 textFieldDidChange가 불린다. 그렇다면 TextField를 수정해보자.

 

7.

 

 

import UIKit

class ViewController: UIViewController {

    @IBOutlet weak var textField: UITextField!
    @IBOutlet weak var myButton: UIButton!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        self.myButton.layer.borderColor = UIColor(named: "borderColor")?.cgColor
        self.myButton.layer.borderWidth = 0.5
        self.textField.addTarget(self, action: #selector(textFieldDidChange), for: .editingChanged)
    }
    
    @objc
    func textFieldDidChange() {
        self.myButton.layer.borderColor = UIColor(named: "borderColor")?.cgColor
        self.myButton.layer.borderWidth = 0.5
    }
}

하나도 다를게 없는 코드이다. 근데 왜 갑자기 DarkMode의 color가 나오냐 이말이다.

border가 문제인가? 그럼 backgroundColor를 줘보자.

class ViewController: UIViewController {

    @IBOutlet weak var textField: UITextField!
    @IBOutlet weak var myButton: UIButton!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        self.myButton.layer.borderColor = UIColor(named: "borderColor")?.cgColor
        self.myButton.layer.borderWidth = 0.5
        self.myButton.backgroundColor = UIColor(named: "borderColor") ✅✅
        self.textField.addTarget(self, action: #selector(textFieldDidChange), for: .editingChanged)
    }
    
    @objc
    func textFieldDidChange() {
        self.myButton.layer.borderColor = UIColor(named: "borderColor")?.cgColor
        self.myButton.layer.borderWidth = 0.5
        self.myButton.backgroundColor = UIColor(named: "borderColor") ✅✅
    }
}

 ✅✅에 있는 라인을 추가했다. myButton의 background에 내 Color를 넣어보는거다.

 

BackgroundColor는 변하지 않고 잘 나온다.

그렇다면 CGColor가 아주 강력한 범인이다.

처음에는 SceneDelegate를 사용하지 않고 AppDelegate에서 지정해서 그런가..? 싶었다.

하지만 SceneDelegate window의 overrideUserInterfaceStyle를 light를 해도 여전히 같은 현상.

 

위에서

https://stackoverflow.com/a/56546554

이런식으로 지정하고 있다고 언급했다.

그럼 이런식으로 지정하지 말고, 

<key>UIUserInterfaceStyle</key>
<string>Light</string>

info.plist에 지정을 해보자.

 

 

잘 나온다!!!

 

그렇다면 AppDelegate에서 window의 overrideUserInterfaceStyle에 지정해서 그런거냐?

라는 물음에는 궁금증이 세가지가 생긴다.

1. 처음 viewDidLoad에서 지정한 border는 왜 lightMode색깔로 잘 나온거지?

2. Button의 BackgroundColor는 왜 문제없이 잘 먹여진거지?

3. AppDelegate / SceneDelegate에서 지정한거랑 info.plist에서 지정한거랑 뭐가 다르지? 

왜 info.plist에서 지정하면 잘되지? 

 

 

 

# 해결

이와 관련하여 찾아보니, WWDC세션(Implementing Dark Mode on iOS)이 있었다. 

위와같은 메소드 외에는 현재 trait collection에 특정 값이 있다고 보장할 수 없다고 한다. 

이러한 메소드 외부에서 dynamic color를 resolve해야하는 경우, 개발자가 직접 관리해줘야한다.

개발자가 직접 관리해줘야 하는 예시는 다음과 같다.

CALayer및 CGColor와 같은 Lower-level class들은 dynamic color를 이해하지 못한다고 한다. dynamic color는 UIKit 개념이라고..

(Lower-level classes, like CA Layer and CG Color, don't understand dynamic colors. It's a UIKit concept.)

이렇게 할 경우, CGColor는 dynamic color가 아니라고 한다.

그래서 UIKit에서 CGColor를 호출하려면 이걸 resolve해야한다.

 

# 첫번째 방법.

view의 traitCollection을 가져온뒤, UIColor의 resolvedColor를 호출한다. 

resolvedColor는 지정된 traitCollection의 color를 반환하다고 보면 된다.

 

# 두번째 방법.

지금 소개할 방법이 애플이 더 쉽다고 말하는 방법이다.

traitCollection에서 performAsCurrent를 호출하는 방법이다.

클로저 내부에서 색상을 확인하므로 올바른 값을 얻을 수 있다고 한다.

이것은 약간 협박(?intimidating) 처럼 보이지만, 절대적으로 안전하며 / 가볍고 / 사이드 이펙트도 없다고 한다. 

(This looks a little intimidating but it's absolutely safe.)This looks a little intimidating but it's absolutely safe. It's lightweight. There are no side effects.)

심지어 백그라운드 스레드로부터도 안전하다고 한다.

실행중인 특정 스레드에만 영향을 미치므로 기본 스레드에는 영향을 주지 않는다고...

자세한 사항은 WWDC세션(Implementing Dark Mode on iOS)를 참고.

그럼 위 방법으로 해결해보자.

@objc
func textFieldDidChange() {
    self.traitCollection.performAsCurrent {
        self.myButton.layer.borderColor = UIColor(named: "borderColor")?.cgColor
        self.myButton.layer.borderWidth = 0.5
    }
}

애플이 추천하는 두번째 방법으로 했다. 

오른쪽이 performAsCurrent로 감싼 버전이다. light Mode color로 잘 동작하는 것을 볼 수 있다. 

위에서 말한 질문

Q : 처음 viewDidLoad에서 지정한 border는 왜 lightMode색깔로 잘 나온거지?

A : 이 질문에 대한 대답은 확신할 수가 없는데..

현재 trait collection에 특정 값이 있다고 보장할 수 없는게..무조건 보장이 안된다! 이건 아니지 않을까? 

viewDidLoad에서는 window의 interface style을 잘 가져온 것 같은데, update하는 과정에서 현재 모드에 대한 dynamic color를 가져오는데 실패하는것 같다. 

 

Q : Button의 BackgroundColor는 왜 문제없이 잘 먹여진거지?

A : dynamic color가 UIKit컨셉이고, backgroundColor는 UIColor를 받으니..문제가 없었을 것이다.

 

Q : AppDelegate / SceneDelegate에서 지정한거랑 info.plist에서 지정한거랑 뭐가 다르지? 

왜 info.plist에서 지정하면 잘되지? 

A : 모르겠음......info.plist에서 지정하면 앱 전체를 그 모드로 딱 지정!!! 해버리는거라 window의 하위로 붙는 viewController의 interface style override과정이랑 뭔가 다를 것 같긴 한데..정확한 이유는 모르겠다. 

사실 info.plist에서 지정했다고 무조건 된다!!라고 하기도 좀 찜찜하다.

CALayer, CGColor는 traitCollection을 보장을 못하는건 똑같지 않나?

 

글이 길어졌는데..그냥 CGColor나 CALayer를 사용하는 곳이면, performAsCurrent로 감싸주기로 하자..

 

저 WWDC세션도 봤을텐데...이 사실을 너무 늦게 안 것 같다. 

반응형