시스템이 모든 뷰컨트롤러와 뷰에 자동적으로 전파하는 데이터들
UIKit은 자체적인 system-trait들을 많이 제공한다.
userInterfaceStyle = .dark
horizontalSizeClass = .compact
preferredContentSizeCategory = .extraLarge
iOS 17부터는 커스텀trait을 선언할 수 있게 되었다.
myAppTheme = .standard
TraitCollection
클로져를 인자로 받는 생성자 추가
// Build a new trait collection instance from scratch
let myTraits = UITraitCollection { mutableTraits in
mutableTraits.userInterfaceIdiom = .phone
mutableTraits.horizontalSizeClass = .regular
}
기존 traitCollection의 값을 기반으로 새로 만들어주는 메소드 추가
// Get a new instance by modifying traits of an existing one
let otherTraits = myTraits.modifyingTraits { mutableTraits in
mutableTraits.horizontalSizeClass = .compact
mutableTraits.userInterfaceStyle = .dark
}
Trait environment
windowScene, UIWindow, Presentation, UIViewController, UIView
각 environment는 각자의 traitCollection을 가지고, 이 값은 서로 다를 수 있다.
trait environment는 trait 계층을 통해서 서로 연결되어 있다.
각 trait environment는 부모 environment로부터 trait 값을 이어받는다.
언제나 가장 구체적인 trait environment를 사용해라
ex. ViewController와 View계층과 trait 계층
ViewController는 자신이 가진 View의 SuperView에서 traitCollection을 상속받는다.
때문에 ViewController의 traitCollection은 ViewController의 view가 view계층에 있어야만 제대로 업데이트가 된다.
언제 유용한가?
ex. 이 뷰가 특정 뷰컨트롤러 안에 있는지 알려주는 trait
SwiftUI의 EnvironmentKey 구현과 비슷
struct ContainedInSettingsTrait: UITraitDefinition {
static let defaultValue = false
}
let traitCollection = UITraitCollection { mutableTraits in
mutableTraits[ContainedInSettingsTrait.self] = true
}
let value = traitCollection[ContainedInSettingsTrait.self]
프로퍼티로 쓰게 만들기
extension UITraitCollection {
var isContainedInSettings: Bool { self[ContainedInSettingsTrait.self] }
}
extension UIMutableTraits {
var isContainedInSettings: Bool {
get { self[ContainedInSettingsTrait.self] }
set { self[ContainedInSettingsTrait.self] = newValue }
}
}
let traitCollection = UITraitCollection { mutableTraits in
mutableTraits.isContainedInSettings = true
}
let value = traitCollection.isContainedInSettings
ex. 커스텀 테마
enum MyAppTheme: Int {
case standard, pastel, bold, monochrome
}
struct MyAppThemeTrait: UITraitDefinition {
static let defaultValue = MyAppTheme.standard
static let affectsColorAppearance = true // 이 trait이 바뀌면 앱의 뷰가 다시 그려져야 함을 나타낸다. 기본 값은 false
static let name = "Theme" // 디버거 등에서 출력할 때 사용하는 값. 원래는 타입 이름을 그대로 씀.
static let identifier = "com.myapp.theme" // encoding 등에서 사용. 기본값 있음
}
extension UITraitCollection {
var myAppTheme: MyAppTheme { self[MyAppThemeTrait.self] }
}
extension UIMutableTraits {
var myAppTheme: MyAppTheme {
get { self[MyAppThemeTrait.self] }
set { self[MyAppThemeTrait.self] = newValue }
}
}
// traitCollection 사용하기
let customBackgroundColor = UIColor { traitCollection in
switch traitCollection.myAppTheme {
case .standard: return UIColor(named: "StandardBackground")!
case .pastel: return UIColor(named: "PastelBackground")!
case .bold: return UIColor(named: "BoldBackground")!
case .monochrome: return UIColor(named: "MonochromeBackground")!
}
}
let view = UIView()
view.backgroundColor = customBackgroundColor
best practice
trait 계층에서 데이터를 변경하는 방법
trait environment들에 추가된 traitOverrides 프로퍼티를 통해서 이뤄진다.
부모의 override가 자식으로 전파되는 과정
traitOverrides는 옵셔널한 입력이고, traitCollection을 output으로 보면 된다.
다만 오버라이드한다고 즉시 view에 반영되지 않을 수도 있다.
traitOverrides는 값의 존재 여부와 오버라이드를 없애는 기능도 제공한다.
traitOverrides는 입력 매커니즘이고, 값 참조는 traitCollection 프로퍼티를 통해서 해야한다.
func toggleThemeOverride(_ overrideTheme: MyAppTheme) {
if view.traitOverrides.contains(MyAppThemeTrait.self) {
// There's an existing theme override; remove it
view.traitOverrides.remove(MyAppThemeTrait.self)
} else {
// There's no existing theme override; apply one
view.traitOverrides.myAppTheme = overrideTheme
}
}
성능 고려사항