먹고 기도하고 코딩하라

[iOS] 다크 모드에서 border, backgroundColor 색상 라이트모드로 고정하는 방법 본문

앱/Swift

[iOS] 다크 모드에서 border, backgroundColor 색상 라이트모드로 고정하는 방법

사과먹는사람 2024. 2. 8. 19:55
728x90
728x90

 

라이트/다크 모드 컬러가 다른 다이나믹 컬러를 사용할 때, 다크 모드에서도 뷰를 라이트모드 색상으로 표현하고 싶다면 보통은 뷰컨이나 뷰의 overrideUserIntefaceStyle을 .light로 고정해주면 된다.

if #available(iOS 13.0, *) {
    self.overrideUserInterfaceStyle = .light
}

하지만 완전한 해결이 되지는 않는다.

 

 

현상

특정 뷰의 borderColor, 그리고 backgroundColor 등을 동적으로 변경할 때, 색상이 다크 모드 색상을 그대로 따라가는 경우가 있다.

이런 경우 rgb 값을 명시적으로 넣어 라이트/다크모드에서의 색상을 모두 라이트로 통일하는 것을 한 가지 방법으로 사용할 수 있다.

하지만 추후에 다크 모드 지원을 염두에 두고 있다면, 따로 표시해두지 않고 rgb 고정값을 사용한 코드들은 나중에 다크 모드 지원을 위해 색상을 변경할 때 문제가 될 수 있다. 어느 부분에서 명시적으로 라이트모드를 사용했는지 검색이 어려울 수 있다.

 

 

원인

이 현상의 원인은 UIColor 내의 CGColor에 있다.

우리 프로젝트 내에서는 UIView의 borderColor 설정에 다음과 같은 extension 메소드를 넣어서 사용하고 있다.

// SomeViewController.swift
let someButton = UIButton()
someButton.borderColor = .wBlue100
// UIViewExtension.swift
public extension UIView {
    @IBInspectable var borderColor: UIColor? {
        get {
            return UIColor(cgColor: layer.borderColor!)
        }
        set {
            layer.borderColor = newValue?.cgColor
            setNeedsDisplay()
        }
    }
}

 

set할 때 이런 식으로 UIColor.cgColor를 레이어의 borderColor로 넣는 메소드다.

 

WWDC 2019 영상에 따르면, Dynamic Color는 UIViewController, UIView가 가진 UITraitCollection과 컬러셋의 조합으로 가능하다.

UITraitCollection이 가진 .userInterfaceStyle 프로퍼티 값으로 현재 기기의 라이트/다크 모드를 추적하는데, 다이나믹 컬러와 UITraitCollection의 정보를 통해(resolved) 최종적으로 이 모드에서 불러와야 할 컬러를 가져오게 된다.

 

UIKit의 UIColor보다 low-level인 CoreGraphics의 CGColor는 뷰의 CALayer 단에서 사용하는 컬러 객체다.

이 CGColor는 다이나믹 컬러를 지원하지 않기 때문에, 다른 뷰처럼 overrideUserInterfaceStyle이 아닌 UITraitCollection + 다이나믹 컬러로 resolved된 컬러를 반영한다.

그래서 overrideUserInterfaceStyle를 오버라이드하는 것 자체가 소용이 없고, CALayer의 borderColor 등 CGColor를 사용하는 특정 컬러 프로퍼티는 뷰가 처음 그려졌을 당시의 기기 모드와 상응하는 컬러에 그대로 굳어 있는 것이다.

 

 

해결법

이런 경우 라이트 모드의 UITraitCollection을 선언해준 후 컬러와 resolve해서 직접 cgColor를 구해주는 방법이 있다.

다크 모드 지원까지 일부 사용할 곳이 있을 것으로 판단해 UIColor Extension 내에 작성했다.

// UIColorExtension.swift
extension UIColor {
    @available(iOS 13.0, *)
    func toLightColor() -> CGColor? {
        return resolvedColor(with: UITraitCollection(userInterfaceStyle: .light)).cgColor
    }
}
extension UIColor {
    /* Resolve any color to its most fundamental form (a non-dynamic color) for a specific trait collection.
     */
    @available(iOS 13.0, *)
    open func resolvedColor(with traitCollection: UITraitCollection) -> UIColor
}

 

사용법

코드에서는 이렇게 사용할 수 있다.

나중에 다크 모드를 정식으로 지원할 때는 toLightColor()를 검색하면 명시적으로 라이트모드를 써둔 곳을 찾기 쉬울 것이다.

someView.borderColor = UIColor.wBlue100?.toLightColor()

이 현상은 borderColor 외에도 버튼 등의 backgroundColor가 동적으로 변경될 때도 나타난다.

우리 프로젝트 UIButton extension의 setBackgroundColor extension 메소드가 이 현상과 관련 있다.

extension UIButton {
    func setBackgroundColor(_ color: UIColor, for state: UIControl.State) {
        UIGraphicsBeginImageContext(CGSize(width: 1.0, height: 1.0))
        guard let context = UIGraphicsGetCurrentContext() else { return }
        context.setFillColor(color.cgColor)
        context.fill(CGRect(x: 0.0, y: 0.0, width: 1.0, height: 1.0))

        let backgroundImage = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()

        setBackgroundImage(backgroundImage, for: state)
    }
}

UIGraphicsGetCurrentContext()의 결과인 CGContext를 CGColor로 채우고 있다.

상술했듯, CGColor는 기기 모드를 따라가는 UITraitCollection에 의해 결정되기 때문에 뷰가 그려지던 당시의 모드 색상이 그대로 나오게 된다.

 

 

이럴 때는?

그래서 이렇게 내부적으로 CGColor를 사용할 때는 다음과 같이 하면 된다.

주의할 점은 이미 backgroundColor가 설정되어 있는 경우에는 새로운 컬러로 덮어씌우는 게 불가능하다.

그래서 미리 색상 변경 전 .clear로 투명하게 밀어버리고 layer 단의 backgroundColor에 직접 CGColor를 넣어주면 된다.

downloadButton.setBackgroundColor(.clear, for: .normal)
downloadButton.layer.backgroundColor = UIColor.wBlue500?.toLightColor()

 

 

 

References

728x90
반응형
Comments