스트리밍은 대부분의 브라우저와
Developer 앱에서 사용할 수 있습니다.
-
UIKit 특성 시스템 활용하기
UIKit의 특성 시스템에 대한 강력한 개선 사항을 살펴보세요. 사용자 지정 특성을 정의하여 UITraitCollection에 자체 데이터를 추가하고, 특성 오버라이드 API를 사용하여 뷰 컨트롤러 및 뷰로 전파되는 데이터를 수정하고, API를 채택하여 유연성과 성능을 개선하는 방법을 알려드립니다. 또한 앱에서 UIKit 및 SwiftUI 컴포넌트의 데이터에 원활하게 액세스하기 위해 UIKit 특성을 SwiftUI 환경 키와 연결하는 방법도 살펴봅니다.
챕터
- 0:57 - Understanding traits
- 7:44 - Defining custom traits
- 14:31 - Applying overrides
- 19:48 - Handling changes
- 25:24 - SwiftUI bridging
리소스
관련 비디오
WWDC23
-
다운로드
♪ ♪
'UIKit 특성 시스템 활용하기'에 오신 걸 환영합니다 저는 타일러 폭스입니다 UI 프레임워크 엔지니어죠 여러분께 몇 가지 놀라운 새 UIKit 기능을 말씀드리게 되어 기쁩니다 이 기능들은 iOS 17에서 활용할 수 있죠 먼저 UIKit의 특성 시스템에 대한 기본 사항을 살펴보겠습니다 그런 다음 새로운 기능을 설명해 드리겠습니다 새로운 기능에는 사용자 지정 특성을 정의하여 UITraitCollection에 고유한 데이터를 추가하는 기능과 앱 계층 구조에서 특성 오버라이드를 쉽게 하는 방법 특성 변경을 보다 유연하게 처리하는 방법 등이 있습니다 마지막으로, UIKit 특성과 SwiftUI 환경 키를 연결하여 앱에서 UIKit와 SwiftUI 컴포넌트 사이에 데이터를 매끄럽게 전달하는 방법을 설명해 드리겠습니다 이제 몇 가지 기본 사항을 검토해 보죠 특성은 독립적인 데이터 조각으로 시스템이 자동으로 앱의 모든 뷰 컨트롤러와 뷰에 전파하는 것입니다 UIKit는 많은 내장 시스템 특성을 제공합니다 사용자 인터페이스 스타일과 가로 크기 클래스 선호하는 콘텐츠 크기 범주 같은 것들이죠 iOS 17에서는 여러분만의 사용자 지정 특성도 정의할 수 있습니다 이를 통해 강력하고 새로운 방법으로 앱의 뷰 컨트롤러와 뷰에 데이터를 제공할 수 있죠 사용자 지정 특성에 대해서는 잠시 후에 자세히 설명하겠습니다 UIKit에서 특성으로 작업할 때는 주로 특성 컬렉션을 사용합니다 특성 컬렉션에는 특성과 관련 값이 포함돼 있죠 iOS 17의 몇 가지 새로운 API로 특성 컬렉션으로 작업하는 것이 더 수월해졌습니다 먼저 클로저를 받는 새로운 이니셜라이저가 있습니다 이 클로저 내부에는 가변 특성 컨테이너가 있어 값을 설정할 수 있습니다 이 가변 컨테이너는 새로운 프로토콜 UIMutableTraits를 따릅니다 클로저 내부에서 userInterfaceIdiom을 .phone으로 설정하고 horizontalSizeClass를 .regular로 설정합니다 클로저 실행이 완료되면 이니셜라이저는 불변 UITraitCollection 인스턴스를 반환합니다 여기에는 클로저 내부에서 설정한 모든 특성값이 들어 있죠 새로운 modifyingTraits 메서드도 있어서 새 인스턴스를 생성할 수 있죠 클로저 내부의 본래 특성 컬렉션 값을 수정하는 방법으로요 여기서는 horizontalSizeClass를 .compact로 바꿨고 userInterfaceStyle에 .dark를 넣었습니다 userInterfaceIdiom은 바꾸지 않았기 때문에 본래 특성 컬렉션의 .phone과 동일하게 유지됩니다 이렇게 직접 특성 컬렉션을 만들 수도 있지만 대부분의 경우 특성 환경에서 특성 컬렉션을 가져옵니다 앱의 특성 환경은 윈도우 씬 윈도우, 프레젠테이션 컨트롤러 뷰 컨트롤러 및 뷰입니다 이러한 특성 환경에는 모두 고유한 특성 컬렉션이 있으며 각 특성 컬렉션에는 다른 값이 포함될 수 있습니다 특성 환경은 특성 계층 구조에 연결되어 있으며 특성 계층 구조란 앱에서 특성이 흐르는 방식입니다 이것은 특성 계층 구조의 트리 구조 예시입니다 각 윈도우 씬에서 개별 뷰 컨트롤러와 뷰까지 나와 있는데요 각 특성 환경은 부모 환경으로부터 특성값을 상속받죠 가능한 한 항상 가장 구체적인 특성 환경의 특성 컬렉션을 사용하세요 특성이 뷰 컨트롤러 및 뷰를 통해 흐르는 방식을 자세히 살펴볼게요
다음은 부모 뷰 컨트롤러인데 자식 뷰 컨트롤러를 포함하고 있죠 점선은 뷰 컨트롤러 계층 구조를 나타냅니다 부모 컨트롤러는 뷰를 소유하고 있으며 이 둘을 연결하는 실선은 관계를 나타냅니다 부모 뷰에는 하나의 서브뷰가 있으며 뷰를 가로지르는 점선은 뷰 계층 구조를 나타냅니다 마지막으로, 자식 뷰 컨트롤러의 뷰는 중간 뷰의 서브뷰입니다 먼저 iOS 17 이전에는 뷰 컨트롤러와 뷰를 통해 특성이 어떻게 흘러왔는지 설명하겠습니다 뷰 컨트롤러는 부모 뷰 컨트롤러로부터 직접 특성을 상속받았습니다 그리고 뷰 컨트롤러 소유의 뷰는 뷰 컨트롤러에서 직접 특성을 상속받았죠 마지막으로 뷰 컨트롤러가 없는 뷰는 슈퍼뷰로부터 직접 특성을 상속받았습니다 이 동작은 뷰 계층 구조의 특성 흐름이 중지된다는 뜻입니다 뷰 컨트롤러가 소유한 각 뷰에서요 예를 들어 부모 컨트롤러 뷰의 특성값은 직접 서브뷰에서만 상속됩니다 자식 컨트롤러의 뷰는 그 값을 받지 못합니다 해당 뷰 아래에 있는 뷰 계층 구조의 서브뷰임에도요 이 동작은 의외일 수 있습니다 iOS 17에서는 이 문제를 제거했습니다 뷰 컨트롤러와 뷰의 특성 계층 구조를 통합해서요 이제 뷰 컨트롤러는 뷰의 슈퍼뷰에서 특성 컬렉션을 상속하죠 부모 뷰 컨트롤러에서 직접 상속하지 않고요 이로써 특성의 흐름은 간단한 선형이 됐습니다 뷰 컨트롤러와 뷰를 통해서요 여전히 뷰 컨트롤러는 부모 뷰 컨트롤러에서 특성을 상속하지만 그 사이의 뷰를 통해 간접으로 이뤄질 뿐이라는 점에 유의하세요 뷰 컨트롤러는 뷰 계층 구조에서 특성을 상속하므로 뷰 컨트롤러가 업데이트된 특성을 받으려면 뷰 컨트롤러의 뷰가 계층 구조에 있어야 합니다 따라서 계층 구조에 추가되기 전에 뷰 컨트롤러의 특성 컬렉션에 액세스하면 뷰 컨트롤러는 해당 특성에 대한 최신 값을 갖지 못합니다 영향을 받는 코드가 가장 많이 발견되는 곳은 viewWillAppear로 뷰가 계층 구조에 추가되기 전에 항상 호출됩니다 이제 viewIsAppearing이라는 새 콜백을 대신 사용할 수 있죠 viewIsAppearing은 viewWillAppear에서 이름을 땄는데 계층 구조에 뷰가 추가되고 뷰 컨트롤러와 뷰가 최신 특성 컬렉션을 갖게 되면 viewIsAppearing은 드롭인 대체 메서드로서 현재 viewWillAppear를 사용하는 거의 모든 경우를 대체하죠 가장 좋은 점은 이 새로운 메서드가 iOS 13까지 역배포된다는 겁니다 'UIKit의 새 기능'을 보시면 새로운 콜백과 이것이 뷰 컨트롤러 수명 주기에 어떻게 적용되는지 더 자세히 알 수 있습니다 iOS 17은 또한 뷰 특성 업데이트의 일관성과 성능을 개선합니다 뷰는 계층 구조에 있을 때만 특성 컬렉션을 업데이트하며 계층 구조에 들어가면 각 뷰는 레이아웃을 수행하기 직전에만 특성 컬렉션을 업데이트합니다 가장 좋은 방법은 레이아웃 중에 특성을 사용하는 것입니다 뷰의 경우 traitCollection을 사용하되 layoutSubviews 메서드 내부에서 하는 것을 뜻하죠 layoutSubviews는 다시 실행된다는 걸 잊지 마세요 뷰에서 setNeedsLayout이 호출될 때마다요 여러 번 호출된다면 구현에서 중복 작업을 피해야 합니다 사용자 지정 특성은 iOS 17의 강력한 새 기능으로 뷰 컨트롤러와 뷰에 데이터를 제공하는 완전히 새로운 방법을 열어줍니다 앱에서 데이터로 작업할 때 다음 사항을 고려하세요 새 사용자지정 특성을정의할시기를 결정하는 데 도움이 될 겁니다 여러 자식 뷰로 데이터를 전파해야 한다면 특성은 훌륭한 선택이죠 예를 들어 하나의 뷰 컨트롤러에서 여러 개의 자식 뷰 컨트롤러로 또는 하나의 슈퍼뷰에서 모든 서브뷰로 전파해야 한다면요 또한 특성으로 다른 컴포넌트에 데이터를 보낼 수도 있죠 여러 계층 깊이에 중첩돼 있을 수 있는 직접 연결되지 않은 곳에요 특성은 계층 구조를 통해 상속되므로 뷰와 뷰 컨트롤러에 환경에 대한 콘텍스트를 제공할 수 있습니다 포함하는 뷰 컨트롤러에 대한 정보를 제공한다든지요 특성 시스템은 강력하지만 데이터를 전파하는 데 사용하면 무료가 아닙니다 최상의 성능을 위해 가치를 추가할 때 특성을 사용하되 데이터를 직접 쉽게 전달할 수 있다면 특성을 쓰지 마세요 이제 첫 번째 사용자 지정 특성을 정의할 준비가 되었습니다 제 앱에 설정 화면이 있고 설정 뷰 컨트롤러의 뷰 포함 여부를 나타내는 특성을 구현하고 싶다고 해 보죠 몇 줄의 코드만으로 사용자 지정 특성을 정의할 수 있습니다
먼저 새 구조체를 선언하고 UITraitDefinition 프로토콜에 맞는 필수 정적 프로퍼티인 defaultValue를 구현합니다 이것은 값이 설정되지 않은 경우 특성의 기본값이며 각 특성 정의에는 연관된 값 유형이 있습니다 defaultValue에서 유추되는 것들이죠 이 경우에는 기본값을 false로 지정했기 때문에 이 특성의 값 유형은 Bool로 유추됩니다 이전에 SwiftUI에서 사용자 지정 환경 키를 정의해 봤다면 매우 친숙하게 느껴질 것입니다 특성을 정의하고 나면 새로운 API와 함께 사용할 수 있죠 바로 UITraitCollection과 UIMutableTraits입니다 특성은 값을 가져오고 설정하는 데 사용하는 키라고 생각하면 됩니다 새로운 UITraitCollection 이니셜라이저 내에서 특성에 대한 값을 설정할 수 있죠 UIMutableTraits의 아래 첨자 연산자를 사용해서요 그런 다음 UITraitCollection의 아래 첨자 연산자를 사용해 특성에 대한 값을 다시 읽을 수 있습니다 두 개의 간단한 확장을 추가하면 이 특성에 액세스할 수 있습니다 모든 시스템 특성과 마찬가지로 표준프로퍼티 구문을 사용하면되죠 여기서는 읽기 전용 프로퍼티를 선언했습니다 불변 UITraitCollection 클래스의 확장 안에요 그런 다음 UIMutableTraits 프로토콜의 확장 안에도 읽기 및 쓰기 프로퍼티를 선언했습니다 아주 간단한 확장을 추가했기 때문에 표준 프로퍼티 구문을 사용해서 어디서든 제 특성에 액세스할 수 있죠 사용자 지정 특성을 정의할 때는 항상 이러한 확장을 작성하세요 사용자 지정 특성에 대한 또 다른 아이디어가 있습니다 제 앱에서 사용자 지정 색상 테마 지원을 구축한다고 가정해 보죠 MyAppTheme이라는 열거형이 있는데요 제 앱에서 지원하는 네 가지 색상 테마를 나타냅니다 가장 먼저 할 일은 새로운 구조체를 선언하는 거죠 UITraitDefinition 프로토콜에 맞도록요 표준 테마를 이 특성의 기본값으로 사용하고 이 새로운 테마 특성을 제 앱의 사용자 지정 동적 색상에 사용할 계획이므로 이 특성이 색 실현에 영향을 미치고 특성이 바뀌면 시스템이 자동으로 뷰를 다시 그리도록 지정합니다 색 실현에 영향을 미치는 특성은 훨씬 더 비싸므로 드물게 변경되는 특성에 대해서만 아껴서 사용하세요 특성에는 이름도 있으며 이것은 디버거에서 특성을 출력하는 등의 작업에 사용됩니다 기본적으로 특성 유형 자신의 이름을 사용하지만 '테마'와 같이 더 짧은 이름을 지정할 수 있습니다 마지막으로 식별자 문자열을 제공할 수 있으며 식별자를 통해 특성을 인코딩 같은 추가 기능에 사용할 수 있습니다 역방향 DNS 포맷을 사용하여 각 특성의 식별자가 앱에서 전 세계적으로 고유한지 확인하죠
이 특성을 설정하고 가져오는 데 일반 프로퍼티 구문을 사용하고 싶어서 UITraitCollection 및 UIMutableTraits를 확장하여 프로퍼티를 선언하겠습니다 이전 예제에서 한 것처럼요 이것으로 사용자 지정 테마 특성 구현에 필요한 모든것을마쳤습니다 이제 이 새로운 특성을 사용할 수 있습니다 예를 들어사용자 지정 동적 색상을 정의하는 방법은 다음과 같습니다 테마에 따라 색이 달라지죠 동적 공급자 이니셜라이저를 사용하여 새 UIColor를 만들고 클로저 내부에서는 전달받은 특성 컬렉션의 테마를 사용하여 반환할 색상을 결정합니다 이제 이 사용자 지정 배경색을 뷰에 설정할 수 있습니다 이 특성을 정의할 때 색 실현에 영향을 준다고 했으므로 이 사용자 지정 배경색을 사용하는 모든 뷰는 테마가 변경되면 자동으로 업데이트됩니다 특성을 정의할 때 가장 중요한 것은 특성값의 관련 데이터 유형입니다 가장 좋은 특성은 단순한 구조체와 열거형을 포함한 값 유형을 기반으로 구축된 것입니다 Swift의 클래스를 기반으로 하는 특성은 피하세요 특성에 가장 효율적인 데이터 유형은 Bool, Int, Double 또는 Int 원시값을 사용하는 열거형입니다 열거형은 특성에 가장 유용한 데이터 유형으로 손꼽히며 열거형의 원시 데이터 유형으로 Int를 명시적으로 지정하면 최고로 효율적입니다 특성값으로 사용하는 모든 사용자 지정 구조체 데이터 유형은 Equatable 프로토콜의 효율적인 구현이 있어야 합니다 시스템에서 특성값을 자주 비교하여 특성이 변경된 시점을 결정하므로 등가 함수가 가능한 한 빨라야 합니다
Objective-C를 사용하는 앱의 경우 새로운 특성 시스템 기능을 해당 앱에서도 사용할 수 있습니다 사용자 정의 특성에 대한 API는 Swift와 Objective-C에서 다르지만 Swift에서 하나의 사용자 지정 특성을 정의하고 양쪽에서 동일한 기본 데이터를 가리키도록 할 수 있습니다 자세한 내용과 특별한 고려 사항은 문서를 참조하세요 사용자 지정 특성을 정의했다면 다음 단계는 앱의 일부 데이터를 앱의 특성 계층 구조에서 채우는 거죠
특성 오버라이드는 특성 계층 구조 내에서 데이터를 수정하는 데 사용하는 메커니즘입니다 iOS 17에서는 특성 오버라이드 적용이 그 어느 때보다 쉽죠 새로운 traitOverrides 프로퍼티가 각 특성 환경 클래스에 있습니다 윈도우 씬, 윈도우, 뷰 뷰 컨트롤러 및 프레젠테이션 컨트롤러 등에요 특성 계층 구조 그림으로 돌아가서 특성 오버라이드는 이 트리의 모든 위치에서 특성값을 변경합니다 계층 구조의 특성 환경 중 하나에 특성 오버라이드가 적용되면 해당 객체 및 모든 자손의 특성 컬렉션 내의 해당 특성값이 변경됩니다 특성 계층 구조에서 부모 및 자식 특성 환경을 가져오면 특성 오버라이드는 두 환경에 다음과 같은 영향을 줍니다 부모에 적용된 특성 오버라이드는 부모의 특성 컬렉션에 영향을 주죠 그런 다음 부모의 특성 컬렉션 값이 자식에게 상속됩니다 마지막으로 자식의 특성 오버라이드가 상속된 값에 적용돼 자신의 특성 컬렉션이 생성됩니다 특성 오버라이드는 선택적 입력으로 특성 컬렉션은 출력으로 생각하세요 오버라이드가 없는 특성은 모두 부모로부터 상속받게 됩니다 특성 오버라이드를 사용하는 예제를 살펴보겠습니다 앱의 특정 부분에 대한 색상 테마를 변경하는 예제죠 오른쪽에는 앱의 특성 계층 구조 그림이 있습니다 처음에는 테마 특성값을 채우려고 오버라이드를 적용하지 않아서 이러한 모든 특성 컬렉션에는 기본값인 표준 테마가 있습니다 이 계층 구조의 루트인 윈도우 씬에 특성 오버라이드를 적용하는 것부터 시작해 보죠 traitOverrides 프로퍼티는 UIMutableTraits 프로토콜을 활용해서 특성값을 쉽게 설정할 수 있습니다 사용자 지정 특성에 대한 오버라이드값을 표준 프로퍼티 구문으로 설정할 수 있습니다 앞서 설명한 UIMutableTraits의 확장을 써서 윈도우 씬의 특성 오버라이드에서 테마를 파스텔로 설정하면 해당 윈도우 씬 내의 모든 윈도우 뷰 컨트롤러, 뷰가 특성 컬렉션에서 파스텔값을 상속받게 됩니다 따라서 계층 구조의 루트 한 곳에 테마를 설정함으로써 기본값을 변경하였으며 이것은 계층 구조 내의 모든 것에 전파됩니다 예를 들어 해당 윈도우 씬 내에서 모든 뷰 컨트롤러의 특성 컬렉션으로부터 테마를 읽고 파스텔로 돌아갈 수 있습니다 그런 다음 traitOverrides 프로퍼티를 사용해 계층 구조의 더 아래쪽 뷰에서 해당 뷰 및 그 아래쪽 모든 뷰의 테마를 수정할 수 있죠 여기서는 이 뷰에 대해 단색 테마 특성 오버라이드를 설정하고 있습니다 따라서 이 단색값은 서브뷰에서 상속받은 값이죠 이 계층 구조의 상위에서 파스텔값을 재정의해서요 특성 오버라이드 변경 사항이 특성 컬렉션에 즉시 반영되지 않을 수 있습니다 예를 들어 뷰는 레이아웃 직전에 특성 컬렉션을 업데이트해서 뷰의 특성 오버라이드 수정 사항은 특성 컬렉션에 반영되지 않습니다 layoutSubviews를 실행하기 직전까지요 또한 traitOverrides 프로퍼티를 쓰면 오버라이드 적용 여부를 확인하고 오버라이드를 모두 삭제할 수 있죠 다음 예제는 오버라이드를 전환하는 예입니다 traitOverrides.contains 메서드를 사용하여 오버라이드를 확인하고 traitOverrides.remove 메서드로 오버라이드를 완전히 제거합니다 메서드가 호출될 때마다 기존 오버라이드를 제거하거나 아직 없는 경우에는 새 테마 오버라이드를 적용하겠죠 특성 오버라이드는 값을 설정하는 입력 메커니즘으로 traitCollection 프로퍼티를 써서 특성값을 읽어야 합니다 오버라이드가 설정되지 않은 상태에서 traitOverrides를 읽으면 예외가 발생합니다
특성 오버라이드를 사용할 때 염두에 두어야 할 몇 가지 성능 고려 사항은 다음과 같습니다 첫째, 각 특성 오버라이드는 비용이 적게 드니 필요한 곳에만 특성 오버라이드를 설정하고 사용하지 않는 특성 오버라이드는 설정하지 마세요 그리고 특성 오버라이드가 변경될 때마다 시스템은 특성 컬렉션을 업데이트해야 합니다 계층 구조에 있는 모든 자손에 대해서요 따라서 특성 오버라이드를 변경하는 횟수를 최소화해야죠 마지막으로, 계층 구조의 루트 즉 윈도우 씬이나 윈도우에 적용된 특성 오버라이드는 그 아래에 모두 적용됩니다 이것은 아주 유용합니다 이 밖에도 윈도우 씬이나 윈도우에 특성 오버라이드를 적용한 훌륭한 사용 예는 많습니다 그러나 특성이 계층 구조 아래쪽에 있어 몇 개의 뷰에만 영향을 미치는 경우 해당 뷰의 가장 가까운 조상에 특성 오버라이드를 적용합니다 슈퍼뷰나 뷰 컨트롤러에 말이죠 그렇게 하면 전체 계층 구조에 특성을 전파하는 비용을 지불하지 않아도 됩니다 계층 구조의 일부만 해당 데이터를 사용한다면요 이제 특성을 정의하고 계층 구조에서 그에 대한 데이터를 채우는 방법을 알았으므로 값이 변경되는 경우를 처리해야 합니다
traitCollectionDidChange는 iOS 17에서는 더 이상 사용되지 않습니다 traitCollectionDidChange를 구현할 때 시스템은 사용자가 실제로 어떤 특성에관심이 있는지 알지못하므로 특성이 값을 변경할 때마다 해당 메서드를 호출해야 하지만 대부분의 클래스는 소수의 특성만 사용하고 다른 특성의 변경은 신경 쓰지 않습니다 그래서 사용자 지정 특성을 더 많이 추가해도 traitCollectionDidChange가 커지지 않습니다 대신 새로운 특성 등록 API가 있습니다 더 유연하고 성능이 개선됐죠 특정 특성에 대한 변경 사항을 등록해서 시스템은 사용자가 어떤 특성에 의존하는지 정확히 알 수 있죠 새로운 API를 사용하면 콜백을 수신할 수 있습니다 타깃 및 동작 패턴이나 클로저를 사용해서요 더 이상 하위 클래스에서 메서드를 재정의할 필요가 없어서 이제 어디서나 특성 변경 사항을 쉽게 관찰할 수 있습니다 먼저 traitCollectionDidChange의 기존 구현을 업데이트하는 방법부터 설명하겠습니다 이것이 제 기존 구현입니다 updateViews를 호출하기 전에 horizontalSizeClass 특성 변경을 어떻게 확인하는지 유의하세요 해당 메서드는 이 하나의 특성에만 의존하거든요 이전 iOS 버전에 앱을 배포하고 있으므로 traitCollectionDidChange를 계속 사용하고 싶다면 구현할 때 관심을 두고 있는 특정 특성이 변경되었는지 확인해야 합니다 이제 이 구현을 iOS 17의 새로운 특성 등록 메서드로 대체하겠습니다 먼저 클로저 기반 메서드로 시작하죠 registerForTraitChanges를 호출해서 등록할 특성 어레이를 전달합니다 모든 시스템 특성에 대한 새로운 UITrait 심볼이 있습니다 horizontalSizeClass처럼요 그런 다음 특정 특성이 변경될 때 호출되는 클로저를 전달합니다 이 클로저는 다른 특성의 변경에는 호출되지 않으므로 여기서 이전 및 신규 특성값을 비교할 필요가 없습니다 특성이 변경된 객체는 클로저의 첫 번째 매개변수로 전달됩니다 이 매개변수를 사용하면 해당 객체에 대한 약한 참조를 캡처할 필요가 없습니다 self에 특성 변경을 등록할 때는 항상 'self:Self'를 작성하세요 다른 특성 환경에 대한 특성 변경을 관찰할 수도 있죠 여기서는 두 가지 특성에 대한 변경 사항을 등록합니다 horizontalSizeClass와 앞서 정의한 사용자 지정 특성 ContainedInSettings이죠 클로저는 이 다른 뷰에서 이러한 특성 중 하나가 변경되면 실행되죠 등록하려는 뷰의 유형을 클로저의 첫 번째 매개변수로 작성합니다
다음은 새로운 타깃 및 동작 기반 메서드의 예입니다 registerForTraitChanges를 호출하고 등록할 특성의 어레이와 변경 시 호출할 타깃 및 동작 메서드를 전달합니다 타깃 매개변수는 선택 사항이며 생략하면 타깃은 registerForTraitChanges가 호출되는 동일한 객체가 됩니다 이 경우 그 대상은 self입니다 클로저 메서드와 마찬가지로 다른 특성 환경에서도 변경 사항을 등록할 수 있는데요 여기서는 다른 뷰에서 특성 변경 사항을 등록하지만 handleTraitChange라는 이름의 메서드 호출을 self에 설정합니다 타깃 및 동작을 사용하여 특성 변경 사항을 등록하는 경우 액션 메서드에는 매개변수 0-2개가 있을 수 있죠 첫 번째 매개변수는 항상 특성이 변경되는 객체입니다 이 매개변수를 사용하여 새 traitCollection을 가져옵니다 두 번째 매개변수는 항상 이전 특성 컬렉션입니다 변경 전 해당 객체의 특성이죠 개별 특성을 등록하는 것 외에도 시스템 특성의 새로운 의미 집합을 사용하여 등록할 수도 있습니다 예를 들어 systemTraitsAffecting ColorAppearance가 있는데 시스템 동적 색상에 영향을 미치는 모든 시스템 특성을 반환합니다 systemTraitsAffecting ImageLookup도 있습니다 이것은 이미지를 로드할 때 UIImage(named:)를 사용하는 시스템 특성 하위 집합을 반환하죠 이런 집합 중 어느 것이든 registerForTraitChanges로 직접 전달하여 사용자 지정 무효화를 수행합니다
등록은 자동으로 정리됩니다 새 메서드를 사용하여 특성 변경을 등록하면요 고급 사용 예의 경우 수동으로 등록을 취소할 수 있습니다 각 등록 메서드에서 반환되는 토큰을 사용해서요 하지만 이러한 경우는 매우 드물기 때문에 보통 registerForTraitChanges를 호출할 때 반환값을 무시하면 됩니다 새로운 특성 등록 API를 사용할 때 명심해야 할 두 가지가 있습니다 첫째, 실제로 의존하는 특성에 대해서만 등록하여 관련 없는 특성이 값을 변경할 때 작업을 수행하지 않도록 하세요 마지막으로, 특성 변경에 대한 응답으로 무효화를 시도해 보세요 즉시 업데이트하지 않고요 예를 들어, 뷰 하위 클래스의 layoutSubviews 메서드 내 특성을 사용한다면 setNeedsLayout을 호출하여 특성 변경을 무효화하죠 이렇게 하면 뷰가 layoutSubview를 수신하도록 예약되지만 업데이트가 즉시 수행되지는 않습니다 이제 UIKit의 특성 시스템으로 자체 데이터를 전파할 수 있으므로 데이터를 매끄럽게 전달할 수 있는 완전히 새로운 방법이 열리죠 앱의 UIKit와 SwiftUI 컴포넌트 사이에요 UIKit의 사용자 지정 특성은 SwiftUI의 사용자 지정 환경 키와 매우 유사합니다 이를 연결하여 UIKit와 SwiftUI 모두에서 동일한 데이터에 액세스할 수 있습니다 UIKit 내부에 SwiftUI 컴포넌트를 포함하든 SwiftUI 내부에 UIKit 컴포넌트를 포함하든 연결된 데이터는 둘 사이에서 매끄럽게 전달되며 동일한 기본 데이터를 읽고 쓸 수 있습니다 UIKit 코드의 특성 API와 SwiftUI 코드의 환경 API를 사용해서요 정말 너무 쉽습니다 제가 앱의 UIKit 코드에 대해 정의한 새로운 색상 테마 특성을 가져와 SwiftUI의 해당 환경 키에 연결하는 작업 말이죠
UIKit의 사용자 지정 특성과 SwiftUI의 사용자 지정 환경 키가 동일한 데이터를 나타낸다고 가정하면 이를 연결하려면 적합성을 추가하기만 하면 됩니다 UITraitBridgedEnvironmentKey 프로토콜에요 이를 위해 메서드를 하나 구현했습니다 UIKit에서 특성을 읽고 값을 SwiftUI로 반환하는 거죠 또한 SwiftUI 환경값을 UIKit 특성에 기록하는 메서드도 하나 더 구현했습니다 이제 UIKit 특성과 SwiftUI 환경 키 모두 통합 스토리지에 액세스하므로 이제 동일한 데이터를 읽거나 쓸 수 있습니다 두 프레임워크를 사용하여 작성된 컴포넌트에서요 다음은 연결된 특성과 환경 키를 사용하는 방법의 예인데요 앱의 루트에서 테마 특성에 대한 특성 오버라이드를 UIKit 윈도우 씬에 적용합니다 이것은 단색 테마 값을 전파합니다 해당 윈도우 씬에 포함된 모든 항목에요 해당 윈도우 씬의 윈도우 깊숙한 곳에 UIKit 컬렉션 뷰가 있습니다 이 컬렉션 뷰에는 UIHostingConfiguration을 사용하여 구성된 셀이 있어 각 셀에 SwiftUI 뷰를 표시합니다 SwiftUI CellView 내부에 'theme'이라는 프로퍼티가 있는데 환경 프로퍼티 래퍼를 사용하여 SwiftUI 환경으로부터 값을 읽습니다 환경의 값은 UIKit의 연결된 특성에 대해 동일한 값을 지닙니다 마지막으로 테마 프로퍼티를 사용하여 이 SwiftUI 뷰 내부의 텍스트 색상을 제어합니다 SwiftUI가 데이터 종속성을 자동으로 추적하기 때문에 UIKit 윈도우 씬에서 테마 특성 오버라이드가 다른 값으로 변경되면 SwiftUI 셀 뷰가 새 테마를 반영하도록 자동으로 업데이트됩니다 연결은 다른 방향에서도 작동합니다 여기 SwiftUI 뷰가 있는데 앱의 설정을 표시하고 있죠 environment 수정자를 사용하여 표준 테마를 설정합니다 표준 테마는 설정 컨트롤러의 모든 항목에 적용될 겁니다 이것은 개념적으로 UIKit에서 특성 오버라이드를 적용하는 것과 동일합니다 그런 다음 UIViewController Representable에 포함된 UIKit 기반 설정 뷰 컨트롤러에서 연결된 특성으로부터 테마값을 읽고 이를 사용하여 이 뷰 컨트롤러에 표시되는 제목을 업데이트합니다 연결된 UIKit 특성과 SwiftUI 환경 키를 써서 너무나 쉽고 매끄럽게 데이터에 액세스하죠 이제 강력한 새 기능을 배웠으니 앱에서 특성 시스템을 활용할 수 있는 위치를 찾아보세요 특성 시스템은 여러분만의 사용자 지정 특성을 정의하여 데이터를 자동으로 전파할 수 있죠 다음으로, 새로운 특성 오버라이드 프로퍼티를 채택하여 특성 계층 구조에서 데이터를 쉽게 수정하세요 보다 유연한 특성 등록 API를 사용하여 우리가 사용하는 정확한 특성에 엄밀한 종속성을 생성합니다 마지막으로, 사용자 지정 UIKit 특성을 사용자 지정 SwiftUI 환경 키와 연결해 앱의 UIKit와 SwiftUI 컴포넌트 간에 데이터가 매끄럽게 흐르도록 합니다 이제 특성의 힘을 발휘하는 것은 여러분의 몫입니다 시청해 주셔서 감사합니다 ♪ ♪
-
-
1:51 - Working with trait collections
// Build a new trait collection instance from scratch let myTraits = UITraitCollection { mutableTraits in mutableTraits.userInterfaceIdiom = .phone mutableTraits.horizontalSizeClass = .regular } // Get a new instance by modifying traits of an existing one let otherTraits = myTraits.modifyingTraits { mutableTraits in mutableTraits.horizontalSizeClass = .compact mutableTraits.userInterfaceStyle = .dark }
-
9:06 - Implementing a simple custom trait
struct ContainedInSettingsTrait: UITraitDefinition { static let defaultValue = false } let traitCollection = UITraitCollection { mutableTraits in mutableTraits[ContainedInSettingsTrait.self] = true } let value = traitCollection[ContainedInSettingsTrait.self] // true
-
10:23 - Implementing a simple custom trait with a property
struct ContainedInSettingsTrait: UITraitDefinition { static let defaultValue = false } 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 // true
-
11:00 - Implementing a custom theme trait
enum MyAppTheme: Int { case standard, pastel, bold, monochrome } struct MyAppThemeTrait: UITraitDefinition { static let defaultValue = MyAppTheme.standard static let affectsColorAppearance = true static let name = "Theme" static let identifier = "com.myapp.theme" } extension UITraitCollection { var myAppTheme: MyAppTheme { self[MyAppThemeTrait.self] } } extension UIMutableTraits { var myAppTheme: MyAppTheme { get { self[MyAppThemeTrait.self] } set { self[MyAppThemeTrait.self] = newValue } } }
-
12:33 - Using a custom theme trait
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
-
18:05 - Managing trait overrides
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 } }
-
21:00 - Trait change handling on older iOS versions
// 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... }
-
21:28 - Registering for trait changes using a closure
// Register for horizontal size class changes on self 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... }
-
22:48 - Registering for trait changes using a target-action
// Register for horizontal size class changes on self registerForTraitChanges( [UITraitHorizontalSizeClass.self], action: #selector(UIView.setNeedsLayout) ) // 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... }
-
24:20 - Registering for changes to system traits affecting color appearance
registerForTraitChanges( UITraitCollection.systemTraitsAffectingColorAppearance, action: #selector(handleColorAppearanceChange) ) @objc func handleColorAppearanceChange() { // Handle the color appearance trait changes... }
-
24:37 - Manually unregistering for 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) @objc func handleTraitChange() { // Handle the trait change... }
-
26:19 - Implementing a bridged UIKit trait and SwiftUI environment key
enum MyAppTheme: Int { case standard, pastel, bold, monochrome } // Custom UIKit trait struct MyAppThemeTrait: UITraitDefinition { static let defaultValue = MyAppTheme.standard static let affectsColorAppearance = true } extension UITraitCollection { var myAppTheme: MyAppTheme { self[MyAppThemeTrait.self] } } extension UIMutableTraits { var myAppTheme: MyAppTheme { get { self[MyAppThemeTrait.self] } set { self[MyAppThemeTrait.self] = newValue } } } // Custom SwiftUI environment key struct MyAppThemeKey: EnvironmentKey { static let defaultValue = MyAppTheme.standard } extension EnvironmentValues { var myAppTheme: MyAppTheme { get { self[MyAppThemeKey.self] } set { self[MyAppThemeKey.self] = newValue } } } // Bridge SwiftUI environment key with UIKit trait extension MyAppThemeKey: UITraitBridgedEnvironmentKey { static func read(from traitCollection: UITraitCollection) -> MyAppTheme { traitCollection.myAppTheme } static func write(to mutableTraits: inout UIMutableTraits, value: MyAppTheme) { mutableTraits.myAppTheme = value } }
-
27:01 - Setting a UIKit trait and reading the bridged environment value from SwiftUI
// UIKit trait override applied to the window scene let windowScene: UIWindowScene windowScene.traitOverrides.myAppTheme = .monochrome // Cell in a UICollectionView configured to display a SwiftUI view let cell: UICollectionViewCell cell.contentConfiguration = UIHostingConfiguration { CellView() } // SwiftUI view displayed in the cell, which reads the bridged value from the environment struct CellView: View { @Environment(\.myAppTheme) var theme: MyAppTheme var body: some View { Text("Settings") .foregroundStyle(theme == .monochrome ? .gray : .blue) } }
-
28:16 - Setting a SwiftUI environment value and reading the bridged trait from UIKit
// SwiftUI environment value applied to a UIViewControllerRepresentable struct SettingsView: View { var body: some View { SettingsControllerRepresentable() .environment(\.myAppTheme, .standard) } } final class SettingsControllerRepresentable: UIViewControllerRepresentable { func makeUIViewController(context: Context) -> SettingsViewController { SettingsViewController() } func updateUIViewController(_ uiViewController: SettingsViewController, context: Context) { // Update the view controller... } } // UIKit view controller contained in the SettingsControllerRepresentable class SettingsViewController: UIViewController { override func viewWillLayoutSubviews() { super.viewWillLayoutSubviews() title = settingsTitle(for: traitCollection.myAppTheme) } func settingsTitle(for theme: MyAppTheme) -> String { switch theme { case .standard: return "Standard" case .pastel: return "Pastel" case .bold: return "Bold" case .monochrome: return "Monochrome" } } }
-
-
찾고 계신 콘텐츠가 있나요? 위에 주제를 입력하고 원하는 내용을 바로 검색해 보세요.
쿼리를 제출하는 중에 오류가 발생했습니다. 인터넷 연결을 확인하고 다시 시도해 주세요.