ํšŒ์ „๊ณผ ๊ด€๋ จ๋œ ์ฝ”๋“œ๋ฅผ ๋ณด๋‹ค๋ณด๋‹ˆ, compact, regular์™€ ๊ฐ™์€ ์šฉ์–ด๋“ค์ด ๋ณด์˜€๋‹ค. UITraitCollection์€ ๋ฌด์—‡์ผ๊นŒ?

Size Classes

๋จผ์ €, Apple์ด ๋‹ค์–‘ํ•œ ๋””๋ฐ”์ด์Šค๋“ค์˜ ์Šคํฌ๋ฆฐ ๋ชจ์–‘์„ ์–ด๋–ป๊ฒŒ ๋…ผ๋ฆฌ์ ์œผ๋กœ ๊ด€๋ฆฌํ•˜๋Š” ์ง€ ๋ถ€ํ„ฐ ์•Œ์•„์•ผ ํ•œ๋‹ค. ๊ฒฐ๊ณผ์ ์œผ๋กœ ๋งํ•˜๋ฉด, 2๊ฐœ์˜ Class๋ฅผ ๊ฐ€์ง€๊ณ  ๊ด€๋ฆฌํ•œ๋‹ค. (์—ฌ๊ธฐ์„œ Class๋Š” Programming์—์„œ์˜ Class๊ฐ€ ์•„๋‹ˆ๊ณ  ๋ถ„๋ฅ˜์˜ ์˜๋ฏธ์ด๋‹ค)

  • Regular
  • Compact

Regular๋Š” ์•„๋ฌด๋ž˜๋„ ์ผ๋ฐ˜์ ์ธ ์ƒํ™ฉ์„ ๋งํ•˜๋Š” ๊ฒƒ ๊ฐ™๊ณ , Compact๋Š” ์ข€ ๋นก๋นกํ•˜๋‹ค๋ผ๋Š” ๋Š๋‚Œ์„ ์ฃผ๋Š” ๊ฒƒ ๊ฐ™๋‹ค. ํ•ด๋‹น Size Class์˜ ๊ฒฝ์šฐ, HIG์— ๊ฐ€๋ฉด ์ฐพ์•„๋ณผ ์ˆ˜ ์žˆ๋‹ค. Code๋กœ๋Š” UIUserInterfaceSizeClass๋กœ ๊ตฌํ˜„๋˜์–ด ์žˆ๋‹ค.

UITraitCollection

horizontal, vertical size, display scale๊ณผ ๊ฐ™์€ iOS interface ํ™˜๊ฒฝ์„ ๋‹ด๊ณ  ์žˆ๋Š” Class

์ด์ „ ๊ธ€์—์„œ Dark mode๋ฅผ ๋ณด๋ฉด์„œ elevated level์— ๋Œ€ํ•ด ๋ฐฐ์› ๋‹ค. ๋ณด๋ฉด์„œ ๊ถ๊ธˆํ–ˆ๋˜ ์ ์€, ์–ด๋–ป๊ฒŒ device๊ฐ€ ์ด๋Ÿฌํ•œ ์ •๋ณด๋“ค์„ ์•Œ๊ณ  ํ™˜๊ฒฝ์— ๋งž๊ฒŒ ์ƒ‰์„ ๋ณ€ํ™”์‹œํ‚ค๋ƒ๋Š” ๊ฒƒ์ด์—ˆ๋‹ค. ๊ทธ ํ•ด๋‹ต์ด ๋ฐ”๋กœ UITraitCollection์ด๋‹ค. ์œ„์—์„œ ๋ณด์•˜๋˜ Size Classes ๋“ค๋„ ์ด UITraitCollection์—์„œ ๊ด€๋ฆฌํ•˜๋Š” ๊ฐ’์ค‘ ํ•˜๋‚˜์ด๋‹ค.

  • userInterfaceIdiom: device(iPhone, iPad, CarPlay)
  • userInterfaceStyle: appearance
  • userInterfaceLevel: VC์˜ level

UITraitCollection์€ App ์‹คํ–‰์‹œ 1๊ฐœ์˜ ๊ฐ’๋งŒ ์กด์žฌํ•˜๋Š” ๊ฒƒ์ด ์•„๋‹ˆ๋‹ค. ๊ฐ๊ฐ์˜ view, viewController๋งˆ๋‹ค ์กด์žฌํ•œ๋‹ค. UITraitCollection ๊ฐ’์€ System์œผ๋กœ๋ถ€ํ„ฐ UIScreen์œผ๋กœ ์ „๋‹ฌ๋˜๊ณ , View ๊ณ„์ธต ๊ตฌ์กฐ๋ฅผ ๋”ฐ๋ผ ์ตœํ•˜์œ„์˜ View๊นŒ์ง€ ์ „๋‹ฌ๋œ๋‹ค.

๋งŒ์•ฝ ์ƒˆ๋กœ์šด View๊ฐ€ ์ƒ์„ฑ๋˜๋ฉด, ๋ถ€๋ชจ์˜ TraitCollection์„ view์— ๋ฐ€์–ด๋„ฃ์–ด์ค€๋‹ค.

Size

๊ฐ€์žฅ ์ž์ฃผ ์ ‘ํ•˜๊ฒŒ ๋  ๊ฒƒ์€, ํšŒ์ „์— ๋”ฐ๋ผ ์–ด๋–ป๊ฒŒ ์ฒ˜๋ฆฌ๋  ๊ฒƒ์ด๋ƒ๋ฅผ ๊ฒฐ์ •ํ•˜๋Š” ๊ฒƒ์ด๋‹ค. view.traitCollection.verticalSize์™€ ๊ฐ™์€ ํ˜•์‹์œผ๋กœ ํ˜„์žฌ device์˜ size class๋ฅผ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค. ์ถ”๊ฐ€์ ์œผ๋กœ iOS interface์˜ ๋ณ€ํ™”๊ฐ€ ์ผ์–ด๋‚œ ๊ฒฝ์šฐ ๋Œ€์‘ํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด, traitCollectionDidChange(_:)๋ฅผ overrideํ•˜์—ฌ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋‹ค.

๋งŒ์•ฝ interface ํ™˜๊ฒฝ์ด ๋ณ€ํ™”ํ•จ์— ๋”ฐ๋ผ ๋ฐœ์ƒํ•˜๋Š” animation์„ customizingํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด willTransition(to:with:) ๋ฉ”์„œ๋“œ๋ฅผ overrideํ•˜๋ฉด ๊ฐ€๋Šฅํ•˜๋‹ค.

Color

UITraitCollection๊ณผ Dynamic Color๋ฅผ ํ†ตํ•ด dark Mode๋กœ ๋ณ€๊ฒฝ๋˜์—ˆ์„ ์‹œ ์ž๋™์œผ๋กœ ๋ฐ˜์˜๋œ๋‹ค. ํ˜„์žฌ view์—์„œ ์–ด๋–ค Color๋ฅผ ์‚ฌ์šฉํ•˜๋Š”์ง€ ๋ณด๊ณ  ์‹ถ๋‹ค๋ฉด, resolvedColor(with:)๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋œ๋‹ค.

let dynamicColor = UIColor.systemBackground 
let traitCollection = view.traitCollection
let resolvedColor = dynamicColor.resolvedColor(with: traitCollection)

๋งŒ์•ฝ dynamic color๊ฐ€ ์•„๋‹Œ ๊ฒƒ์„ ๋ถˆ๋Ÿฌ์˜ฌ ๊ฒฝ์šฐ ์ผ๋ฐ˜์ ์œผ๋กœ ์‚ฌ์šฉํ•˜๋Š” Color๊ฐ€ ๋ฆฌํ„ด๋œ๋‹ค.

์ฝ”๋“œ๋กœ Dynamic Color๋ฅผ ๋งŒ๋“ค ์ˆ˜๋„ ์žˆ๋‹ค.

let dynamicColor = UIColor { (traitCollection: UITraitCollection) -> UIColor in 
    if traitCollection.userInterfaceStyle == .dark {
        return .black 
    } else {
        return .white 
    }
}

์ด๋ฏธ์ง€์˜ ๊ฒฝ์šฐ๋„ ์›๋ฆฌ๋Š” ๋น„์Šทํ•˜์—ฌ ์ƒ๋žตํ•œ๋‹ค.

UITraitCollection.current

๊ทธ๋Ÿผ UIView๋Š” ์–ด๋Š ์‹œ์ ์— ์ด ๊ฐ’์„ ์ฝ์–ด์„œ ์ฒ˜๋ฆฌํ•˜๋Š” ๊ฑธ๊นŒ? ์ผ๋‹จ ํ˜„์žฌ trait ๊ฐ’์„ ์ฝ์–ด์™€์•ผ ํ•  ๊ฒƒ์ด๋‹ค. ๊ทธ๋ž˜์„œ apple์€ iOS 13์— ํ˜„์žฌ์˜ traitCollection์„ ์•Œ๋ ค์ฃผ๋Š” static ๋ณ€์ˆ˜์ธ UITraitCollection.current๋ฅผ ์ถ”๊ฐ€ํ–ˆ๋‹ค.

class BackgroundView: UIView {
    override func draw(_ rect: CGRect) {
        // UIKit sets UITraitCollection.current to self.traitCollection
        UIColor.systemBackground.setFill()
        UIRectFill(rect) 
    }
}

UIView์˜ ๊ฒฝ์šฐ, draw method๊ฐ€ ํ˜ธ์ถœ๋˜๊ธฐ ์ง์ „์— UIKit์ด UITraitCollection.current๋ฅผ ์„ค์ •ํ•œ๋‹ค. ๊ทธ๋ ‡๊ธฐ ๋•Œ๋ฌธ์— ํ•˜์œ„ ๋ผ์ธ์ด ๋™์ž‘ํ•˜๋ฉด์„œ dark mode๋ฅผ ๋ฐ˜์˜ํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋œ๋‹ค.

์ด๋ ‡๊ฒŒ UITraitCollection.current๊ฐ€ ์—…๋ฐ์ดํŠธ ๋˜๋Š” ๊ฑด view๋งŒ์ด ์•„๋‹ˆ๋‹ค. ๋˜๋‹ค๋ฅธ ์„ค์ • ์‹œ์ ์€ layoutSubviews๊ฐ€ ํ˜ธ์ถœ๋˜๊ธฐ ์ „์ด๋‹ค. ๊ทธ๋ ‡๊ธฐ ๋•Œ๋ฌธ์—, ๋งŒ์•ฝ VC๋ฅผ ๊ธฐ์ค€์œผ๋กœ traitCollection ๊ธฐ๋ฐ˜ ๊ฐ’์„ ๋ณ€๊ฒฝํ•ด์•ผ ํ•œ๋‹ค๋ฉด, viewDidLoad๊ฐ€ ์•„๋‹Œ viewWillLayoutSubviews() ํ˜น์€ viewdidLayoutSubviews()์—์„œ ์ฒ˜๋ฆฌํ•ด์ฃผ์–ด์•ผ ํ•œ๋‹ค. UIPresentationController๋Š” ๋ญ”์ง€ ๋ชจ๋ฅด๊ฒ ๋Š”๋ฐ, ๋‹ค์Œ ๊ธ€์—์„œ ์•Œ์•„๋ณด์ž.

layoutSubviews()์—์„œ UI ๊ฐ’์„ ๋ณ€๊ฒฝํ•ด์ฃผ์—ˆ๋‹ค๋ฉด, setNeedsLayout์ด ํ˜ธ์ถœ๋˜๊ณ , ๋‹ค์Œ view update cycle์— ๋ฐ˜์˜๋˜์–ด dark modeํ™”๋ฉด์ด ๋ณด์ด๊ฒŒ ๋œ๋‹ค. VC๋ฅผ ๊ธฐ์ค€์œผ๋กœ ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์˜ˆ์‹œ๊ฐ€ ์žˆ๊ฒ ๋‹ค.

class ViewController: UIViewController {
    override func viewWillLayoutSubviews() {
        // Updated TraitCollection (UITraitCollection.current)
        super.viewWillLayoutSubviews()
        self.updateTitle()
    }
 
    private func updateTitle() {
        if #available(iOS 13.0, *) {
            guard traitCollection.userInterfaceStyle == .dark else {
                self.title = "Light Mode"
                return
            }
            self.title = "Dark Mode"
        } else {
            self.title = "Light Mode"
        }
    }
}

์ถ”๊ฐ€์ ์œผ๋กœ trait์ด ๋ณ€๊ฒฝ๋˜์—ˆ์„ ๋•Œ, ์•Œ๋ ค์ฃผ๋Š” callBack์ด ์žˆ๋‹ค. ๋ณ€๊ฒฝ ์‚ฌํ•ญ์ด ๋ฐœ์ƒํ•œ ์‹œ์ ์— ๋ฐ”๋กœ Color๋ฅผ ์ ์šฉํ•˜๊ฑฐ๋‚˜ ํ•  ๋•Œ ์šฉ์ดํ•˜๊ฒŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

ํ•˜์ง€๋งŒ.. ํ•ด๋‹น Method๊ฐ€ ํ˜ธ์ถœ๋œ ์‹œ์ ์—์„œ์˜ UITraitCollection.current๊ณผ ์ด method ์™ธ๋ถ€์—์„œ TraitCollection์˜ ๊ฐ’์€ ๋‹ค๋ฅผ ์ˆ˜ ์žˆ๋‹ค. ์ด๋Š” ๊ทธ๋Ÿด ์ˆ˜ ์žˆ๋Š” ๊ฒƒ์ด, traitCollection์˜ ๊ฐ’์„ ๊ฐ๊ฐ์˜ view๊ฐ€ ๊ฐ€์ง€๊ณ  ์žˆ๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค. ํ•˜์œ„ view๊นŒ์ง€ ์—…๋ฐ์ดํŠธ๋œ TraitCollection์ด ์ ์šฉ๋˜์ง€ ์•Š์€ ์‹œ์ ์— Callback์ด ํ˜ธ์ถœ๋  ์ˆ˜๋„ ์žˆ๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค. ๊ทธ๋ ‡๋‹ค๊ณ  ๊ฐ™์€ ์ฃผ์†Œ ๊ฐ’์— ์žˆ๋Š” ๋…€์„์˜ ๊ฐ’์„ ๋ฐ”๊พธ๋ฉด ๋™์‹œ์„ฑ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค. ์—ฌ๋Ÿฌ๋ชจ๋กœ ๊ณจ์น˜์•„ํ”ˆ ์ƒํ™ฉ์ด๋‹ค.

์ฆ‰, ์šฐ๋ฆฌ๊ฐ€ ํ•˜๊ณ  ์‹ถ์€ ๊ฒƒ์€ traitCollection์ด ๋ณ€๊ฒฝ๋œ ์‹œ์ ์˜ ๊ฐ€์žฅ ๋”ฐ๋ˆ๋”ฐ๋ˆํ•œ, ์ฆ‰ ๊ฐ€์žฅ ์‹ ์„ ํ•œ ๋…€์„์„ ๊ธฐ๋ฐ˜์œผ๋กœ Color๋ฅผ ๋ฐ”๊พธ๊ณ  ์‹ถ์€ ๊ฒƒ์ด๋‹ค. ์ด๋ฅผ ํ•˜๊ธฐ ์œ„ํ•ด์„œ Apple์€ ์„ธ๊ฐ€์ง€ ๋ฐฉ๋ฒ•์„ ์ œ์•ˆํ•œ๋‹ค.

let layer = CALayer()
let traitCollection = view.traitCollection
 
// Option 1 - resolvedColor๋ฅผ ํ†ตํ•ด traitCollection ๋ฐ˜์˜
let resolvedColor = UIColor.label.resolvedColor(with: traitCollection)
layer.borderColor = resolvedColor.cgColor
 
// Option 2 - performAsCurrent ํด๋กœ์ € ํ™œ์šฉ
traitCollection.performAsCurrent {
    layer.borderColor = UIColor.label.cgColor
}
 
// Option 3 - ์ง์ ‘ current TraitCollection ์—…๋ฐ์ดํŠธ
// ์ด ๊ฒฝ์šฐ UITraitCollection์€ ๋™์ž‘ํ•˜๋Š” Thread์—์„œ๋งŒ ์ ์šฉ๋˜์–ด ๋‹ค๋ฅธ Thread์— ์˜ํ–ฅ์„ ์ฃผ์ง€ ์•Š๋Š”๋‹ค.
// ์ด ๋ฐฉ์‹์€ performAsCurrent์˜ ๋‚ด๋ถ€ ๋™์ž‘๊ณผ ๋™์ผ
let savedTraitCollection = UITraitCollection.current
UITraitCollection.current = traitCollection
layer.borderColor = UIColor.label.cgColor
UITraitCollection.current = savedTraitCollection

traitCollectionDidChange(_:)์™€ ๊ฐ™์€ ๋ฉ”์„œ๋“œ๋Š” ๋‹น์—ฐํ•˜๊ฒŒ๋„ Color ๋ณ€ํ™”์— ๋”ฐ๋ผ์„œ๋งŒ ํ˜ธ์ถœ๋˜๋Š” ๊ฒƒ์ด ์•„๋‹ˆ๋‹ค. ๊ทธ๋ž˜์„œ userInterfaceStyle ๋ณ€๊ฒฝ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋Š” ์ถ”๊ฐ€์ ์ธ API๋„ ์ œ๊ณตํ•œ๋‹ค.

override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
    super.traitCollectionDidChange(previousTraitCollection)
    if traitCollection.hasDifferentColorAppearance(comparedTo: previousTraitCollection) {
        // Resolve dynamic colors again
    }
}

TraitCollection์„ ํ™œ์šฉํ•œ Style ๊ฐ•์ œ ์„ค์ •

๊ฐ๊ฐ์˜ view์— traitCollection์„ ๊ฐ€์ง€๊ณ  ์žˆ๋‹ค๋ฉด, ๋ถ€๋ถ„์ ์œผ๋กœ ์›ํ•˜๋Š” mode๋ฅผ ์ ์šฉํ•˜๋Š” ๊ฒƒ๋„ ๊ฐ€๋Šฅํ•  ๊ฒƒ์ด๋‹ค.

class UIViewController {
    var overrideUserInterfaceStyle: UIUserInterfaceStyle
}
 
class UIView {
    var overrideUserInterfaceStyle: UIUserInterfaceStyle
}

overrideUserInterfaceStyle ์ด๋ผ๋Š” property๊ฐ€ iOS13๋ถ€ํ„ฐ ์ƒˆ๋กœ ์ƒ๊ฒผ๋Š”๋ฐ, ์ด ๊ฐ’์— ๋Œ€ํ•ด .light, .dark์™€ ๊ฐ™์ด ์ง€์ •ํ•˜๋ฉด ํ•˜์œ„ Subview๊นŒ์ง€ ์Šคํƒ€์ผ์ด overriding๋œ๋‹ค.

ํ˜น์€ ์ „์ฒด ์•ฑ์— ๋Œ€ํ•ด dark mode๋ฅผ ๊ฐ•์ œํ•˜๊ณ  ์‹ถ์€ ๊ฒฝ์šฐ, Info.plist์— UIUserInterfaceStyle ๊ฐ’์„ .light, .dark์™€ ๊ฐ™์ด ์„ค์ •ํ•˜๋ฉด ๋œ๋‹ค.

TraitCollection Debug

Debug๋ฅผ ์œ„ํ•œ option์ด ์ถ”๊ฐ€๋˜์—ˆ๋‹ค.

๋งˆ๋ฌด๋ฆฌ

  • TraitCollection์€ ๋‹ค์–‘ํ•œ trait(device, style, size)๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ๋Š” ๊ฐ์ฒด์ด๋‹ค.
  • ์ดˆ๊ธฐํ™”์‹œ parent view๋กœ๋ถ€ํ„ฐ view์— ํ• ๋‹น๋œ๋‹ค.
  • UIScreen์œผ๋กœ ๋ถ€ํ„ฐ view ๊ณ„์ธต์„ ๋”ฐ๋ผ ์—…๋ฐ์ดํŠธ ๋œ๋‹ค.
  • ์‹œ์ ๋ฌธ์ œ ๋•Œ๋ฌธ์— traitCollectionDidChange(_:)์—์„œ์˜ ๊ฐ’๊ณผ view๊ฐ€ ๊ฐ€์ง€๊ณ  ์žˆ๋Š” ๊ฐ’์ด ๋‹ค๋ฅผ ์ˆ˜ ์žˆ๋‹ค.
  • size์™ธ์— ์ƒ‰๋งŒ ๋ฐ˜์˜ํ•˜๊ธฐ ์œ„ํ•œ method(traitCollection.hasDifferentColorAppearance(comparedTo:))๊ฐ€ ์žˆ๋‹ค.
  • layout์ด ๋ณ€ํ™”๋˜๋Š” ์‹œ์ (layoutSubview())๊ฐ€ trait์„ ์‚ฌ์šฉํ•˜๊ธฐ ๊ฐ€์žฅ ์ข‹์€ ์‹œ์ ์ด๋‹ค.

Dark mode์™€ TraitCollection์ด ์—ฎ์ด๋Š” ๋ฐ”๋žŒ์— ๊ตฌ๋ถ„ํ•˜์—ฌ ์ •๋ฆฌํ•˜๊ธฐ๊ฐ€ ์‰ฝ์ง€ ์•Š์•˜๋‹ค. ๋!

Reference