시스템이 모든 뷰컨트롤러와 뷰에 자동적으로 전파하는 데이터들
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
}
}
성능 고려사항
traitCollectionDidChange(_:)는 Deprecated되었다.
traitCollection에서 어떤 값이 뷰에서 필요한지 알 수 없기 때문에 불필요하게 자주 호출된다.
커스텀 trait을 정의하면 할 수록 스케일링 문제가 생기게 된다.
기존 버전을 위해서 남겨둬야 한다면 관심있는 trait변경에만 반응하도록 제대로 체크해야한다.
// Efficient implementation that only updates when necessary
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
if traitCollection.horizontalSizeClass != previousTraitCollection?.horizontalSizeClass {
updateViews(sizeClass: traitCollection.horizontalSizeClass)
}
}
func updateViews(sizeClass: UIUserInterfaceSizeClass) {
// Update views for the new size class...
}
그래서 새로운 trait registration API가 추가되었다.
실제로 사용하는 trait만 감지한다.
target-action 혹은 클로저 기반으로 콜백을 받을 수 있다.
메소드 오버라이드가 필요없다.
클로저 기반
// Register for horizontal size class changes on self
// weak 캡쳐도 필요없다.
registerForTraitChanges(
[UITraitHorizontalSizeClass.self]
) { (self: Self, previousTraitCollection: UITraitCollection) in
self.updateViews(sizeClass: self.traitCollection.horizontalSizeClass)
}
// Register for changes to multiple traits on another view
let anotherView: MyView
anotherView.registerForTraitChanges(
[UITraitHorizontalSizeClass.self, ContainedInSettingsTrait.self]
) { (view: MyView, previousTraitCollection: UITraitCollection) in
// Handle the trait change for this view...
}
target-action 기반
// Register for horizontal size class changes on self
registerForTraitChanges(
[UITraitHorizontalSizeClass.self],
action: #selector(UIView.setNeedsLayout)
) // target은 생략하면 registerForTraitChanges가 호출되는 Object
// action에 들어가는 메소드는 인자가 0~2개
// Register for changes to multiple traits on another view
let anotherView: MyView
anotherView.registerForTraitChanges(
[UITraitHorizontalSizeClass.self, ContainedInSettingsTrait.self],
target: self,
action: #selector(handleTraitChange(view:previousTraitCollection:))
)
@objc func handleTraitChange(view: MyView, previousTraitCollection: UITraitCollection) {
// Handle the trait change for this view...
}
시스템에서 제공하는 trait들 중 의미 있는 단위로 묶어서 몇가지를 제공
UITraitCollection.systemTraitsAffectingColorAppearance // dynamic color
UITraitCollection.systemTraitsAffectingImageLookup // Image(named:)
registerForTraitChanges(
UITraitCollection.systemTraitsAffectingColorAppearance,
action: #selector(handleColorAppearanceChange)
)
@objc func handleColorAppearanceChange() {
// Handle the color appearance trait changes...
}
등록은 라이프사이클에 맞춰서 자동적으로 해제되지만, 수동으로 해제도 가능하다.
// Store the returned registration token
let registration = registerForTraitChanges([UITraitHorizontalSizeClass.self], action: #selector(handleTraitChange))
// Later, use the stored registration token to manually unregister
unregisterForTraitChanges(registration)
best practice