• Understanding traits
    • 시스템이 모든 뷰컨트롤러와 뷰에 자동적으로 전파하는 데이터들

    • UIKit은 자체적인 system-trait들을 많이 제공한다.

      userInterfaceStyle = .dark
      horizontalSizeClass = .compact
      preferredContentSizeCategory = .extraLarge
      
    • iOS 17부터는 커스텀trait을 선언할 수 있게 되었다.

      myAppTheme = .standard
      
    • TraitCollection

      • trait 들과 연관 데이터를 모아놓은 것
      • iOS 17 변경 사항
        • 클로져를 인자로 받는 생성자 추가

          • 변경 가능한 trait Container가 인자로 들어온다.
            • 새로운 프로토콜인 UIMutableTrait 타입으로 들어온다
          • 클로저 실행이 끝나면 생성자는 불변 Object인 UITraitCollection을 만들어준다.
          // 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 계층을 통해서 서로 연결되어 있다.

        스크린샷 2023-06-18 오후 3.28.55.png

      • 각 trait environment는 부모 environment로부터 trait 값을 이어받는다.

      • 언제나 가장 구체적인 trait environment를 사용해라

    • ex. ViewController와 View계층과 trait 계층

      스크린샷 2023-06-18 오후 3.31.52.png

      • iOS 17 이전
        • ViewController는 부모 뷰컨트롤러의 trait을 직접 상속받는다.
        • View는 자신이 속한 ViewController의 trait을 상속받는다.
        • ViewController가 없는 View는 superView의 trait을 상속받는다.
        • 이는 View계층을 통한 trait 상속이 ViewController가 중간에 끼면 이뤄지지 않는다는 것을 뜻한다
          • 그림에서 View of Parent의 trait은 View of Child에 상속되지 않는다.
      • iOS 17 이후
        • ViewController는 자신이 가진 View의 SuperView에서 traitCollection을 상속받는다.

          • 이전에는 부모 ViewController에서 가져왔었다.

          스크린샷 2023-06-18 오후 3.38.44.png

        • 때문에 ViewController의 traitCollection은 ViewController의 view가 view계층에 있어야만 제대로 업데이트가 된다.

          • 이는 viewWillAppear(_:) 에서는 trait이 최신값이 아니라는 것을 뜻한다.
            • viewWillAppear에서는 아직 view가 view계층에 없기 때문에
          • viewIsAppearing(_:)을 대신 써야 한다.
            • 이때는 viewcontroller와 view가 모두 최신 trait을 가진다.
            • view도 정확한 geometry 정보를 가진다.
            • iOS 13까지 back-deploy된다.
      • View의 traitCollection은 view계층에 있는 상태에서만 제대로 업데이트가 된다.
        • layout 직전에 업데이트 된다.
        • 즉, layoutSubviews()가 trait을 사용하기 최적인 시점이다.
        • layoutSubViews는 setNeedsLayout이 호출될 때마다 불리기 때문에, 중복작업을 하지 않도록 신경써야 한다.
  • Defining Custom traits
    • 언제 유용한가?

      • 여러 자식들에게 데이터를 전파할 때
      • 직접적으로 연결되지 않고 간접적으로 이어진 곳에 데이터를 전파할 때
      • 환경정보를 제공하고 싶을 때(포함된 viewController라던지)
      • 다만 이 데이터 전파는 공짜가 아니기 때문에, 직접 값을 전달할 수 있으면 그렇게 하는 게 성능 면에서 좋다.
    • 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

      • 값타입 사용하기
        • 가장 효율적인 데이터 타입
          • Bool
          • Int
          • Double
          • Int rawValue를 가지는 enum
      • Equatable을 통한 동등성 비교가 자주 일어나기 때문에 최대한 효율적으로 구현해야 한다.
      • Swift와 Objective-C 양쪽에서 쓰기 위해서는 양쪽에서 각각 구현해줘야 한다.
        • 다만 똑같이 구현했다면 데이터가 중복으로 존재하지는 않는다.
  • Applying overrides
    • trait 계층에서 데이터를 변경하는 방법

    • trait environment들에 추가된 traitOverrides 프로퍼티를 통해서 이뤄진다.

      • UIWindowScene
      • UIView(UIWindow 포함)
      • UIViewController
      • UIPresentationController
    • 부모의 override가 자식으로 전파되는 과정

      • 부모의 traitOverrides가 부모의 traitCollection에 영향을 준다.
      • 이 값이 자식에게 상속된다.
      • 자식의 traitOverrides가 자식의 traitOverrides에 영향을 준다.
    • traitOverrides는 옵셔널한 입력이고, traitCollection을 output으로 보면 된다.

      스크린샷 2023-06-18 오후 4.31.03.png

    • 다만 오버라이드한다고 즉시 view에 반영되지 않을 수도 있다.

      • view의 traitCollection은 레이아웃 직전에야 업데이트 되기 때문이다.
    • traitOverrides는 값의 존재 여부와 오버라이드를 없애는 기능도 제공한다.

      • traitOverrides는 입력 매커니즘이고, 값 참조는 traitCollection 프로퍼티를 통해서 해야한다.

        • override되어 있지 않은 값을 참조하려고 하면 exception이 발생한다.
        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
            }
        }
        
    • 성능 고려사항

  • Handling changes
  • SwiftUI Bridging