스트리밍은 대부분의 브라우저와
Developer 앱에서 사용할 수 있습니다.
-
SwiftData의 새로운 기능
SwiftData는 표현적이며 선언적인 API로 앱의 영속성을 강화할 수 있도록 도와줍니다. 복합적인 고유성 관련 제약 사항, #Index를 통한 더욱 빠른 쿼리, Xcode 미리보기의 쿼리 및 풍부한 서술 표현 등 SwiftData의 개선 사항을 알아보세요. 이러한 기능을 사용하여 더욱 정교한 모델을 표현하고 앱 내 성능을 향상하는 방법을 함께 모색해 봅니다. SwiftData에서 맞춤형 데이터 저장소를 만들거나 History API를 사용하는 방법을 알아보려면 ‘SwiftData로 자체 데이터 저장소 만들기' 또는 ‘SwiftData 기록으로 모델 변경 사항 추적하기'를 시청하세요.
챕터
- 0:00 - Introduction
- 0:57 - Adopt SwiftData
- 2:11 - Customize the schema
- 2:43 - #Unique macro
- 3:37 - History API
- 4:29 - Tailor a model container
- 5:39 - Custom data stores
- 6:41 - Xcode previews
- 9:20 - Customize queries
- 10:18 - #Expression macro
- 11:56 - #Index macro
리소스
관련 비디오
WWDC24
WWDC23
-
다운로드
안녕하세요, 저는 SwiftData 팀의 엔지니어인 Rishi Verma입니다 오늘은 SwiftData의 새 기능을 소개해 드리고자 합니다
iOS 17에는 모든 Apple 플랫폼에서 Swift로 앱의 데이터를 모델링하고 영속화할 수 있는 프레임워크인 SwiftData가 도입되었습니다 매크로와 같은 최신 Swift 언어 기능을 사용하는 SwiftData에서는 코드를 빠르고, 효율적으로 안전하게 작성할 수 있죠 이 동영상에서는 SwiftData 프레임워크에 대해 간략히 다시 소개해 드린 후 새로운 스키마 매크로를 사용하여 중복 모델을 피하는 방법을 알려드리겠습니다 그다음 모델 컨테이너를 설정 및 구성하는 새로운 방법을 다룬 후
마지막으로, 복합 필터를 통해 쿼리를 최적화하고 새 매크로로 성능을 개선하는 방법을 설명해 드리겠습니다
우선 SwiftData에 대해 간단히 설명해 드리겠습니다 SwiftData는 앱의 모델 레이어를 쉽게 구축하고 앱이 출시될 때마다 이를 영속화할 수 있는 프레임워크죠
이 프레임워크는 영속성뿐만 아니라 스키마 모델링 및 이전, 그래프 관리 CloudKit과의 동기화 등 다양한 기능을 제공합니다 앱에서 SwiftData를 채택하는 것이 얼마나 쉬운지 보여드리기 위해 저와 팀원들이 개발하고 있는 앱인 Trips를 보여드리겠습니다 Trips는 SwiftUI로 작성된 앱으로 휴가와 관련된 여러 아이디어를 기록하는 데 사용할 수 있죠
이 앱의 모델에 대해 SwiftData를 사용하려면 프레임워크를 가져온 다음 @Model 매크로로 각 모델을 꾸미면 됩니다 SwiftData는 매우 유용합니다
그리고 앱의 정의에서 WindowGroup의 modelContainer 한정자는 전체 보기 계층에 Trip 모델에 대한 정보를 알려줍니다
이렇게 하면 보기에서 정적 데이터를 제거하고 대신 @Query를 사용하여 보기를 채울 수 있습니다 모델 컨테이너에서 Trip 모델을 가져오고 Trip 배열을 반환하죠 완료했어요, 이제 앱에서 제가 만든 모든 여행을 영속화하고 SwiftUI 보기에 완벽하게 맞죠 첫 단계에서는 @Model 매크로를 추가했습니다 이는 스키마 맞춤화의 시작에 불과합니다
강력한 @Model 매크로는 영속화 경험을 빠르게 시작합니다 영속화할 수 있는 모든 클래스를 매크로로 꾸미기만 하면 Trip 클래스와 관련 모델의 저장된 속성이 영속화됩니다
또한 한 단계 더 나아가 @Attributes 및 @Relationships의 매크로로 스키마를 맞춤화하고 저장된 속성을 @Transient로 표시하여 해당 데이터의 영속화를 막을 수 있습니다
그리고 올해에는 영구 모델에 복합 제약 조건을 구성할 수 있는 새 스키마 매크로가 추가되었죠
새로운 #Unique 매크로를 사용하여 모델 데이터에서 항상 고유하게 유지되어야 하는 모델 속성의 조합을 SwiftData에 알릴 수 있습니다 두 모델 인스턴스가 동일한 고유 값을 공유하는 경우 SwiftData는 기존 모델과 충돌 시 Upsert를 수행합니다
예를 들어, Trips 앱에서 #Unique 매크로를 사용하여 모든 여행의 name, startDate 및 endDate가 고유하도록 설정할 수 있습니다 이렇게 하면 앱에서 시작일 또는 종료일이 다른 경우에만 여러 여행이 같은 이름을 공유하도록 허용할 수 있죠 이렇게 하면 데이터 중복을 쉽게 방지할 수 있습니다 SwiftData는 어떤 모델이 실제로 중복된 것인지 파악하기 위한 정보를 더 많이 보유하고 있으며 대신 데이터에 대한 업데이트를 수행하기 때문이죠
#Unique 속성이 @Model이 중복되지 않도록 하기 때문에 이 모델의 정체성을 나타내기도 합니다 또한 @Attribute 매크로를 통해 preserveValueOnDeletion으로 속성을 꾸밀 수 있습니다 이렇게 하면 이러한 고유성 값을 SwiftData의 History API에서도 사용할 수 있게 됩니다
SwiftData의 기록을 통해 앱에서 시간이 지남에 따라 어떤 모델이 삽입, 업데이트 또는 삭제되었는지 파악할 수 있습니다 모델이 삭제되면 보존하도록 표시된 값은 기록 정보에 tombstone 값으로 유지되어 앱이 변경 사항을 처리하는 데 필요한 정보를 제공합니다 또한 이를 지원하도록 구축된 맞춤 데이터 저장소에서도 작동하죠 더 자세히 알아보려면 ‘SwiftData 기록으로 모델 변경 사항 추적하기’를 확인하세요 모델 컨테이너를 조정하면 앱에서 데이터 위치와 앱 전체에서 데이터가 사용되는 방식을 세부적으로 조정할 수 있죠
modelContainer 한정자로 SwiftData를 간편하게 시작할 수 있습니다 영속화할 모델 유형을 제공하기만 하면 SwiftData가 컨테이너를 설정합니다 또한 modelContainer 한정자로 컨테이너의 다른 속성을 맞춤화할 수 있죠
예를 들어, 데이터를 디스크가 아닌 메모리에 보관하거나 자동 저장을 활성화 또는 비활성화할 수 있으며 실행 취소 지원을 켜거나 끌 수 있습니다
디스크에서 저장되는 위치를 변경하는 것과 같이 modelContainer를 더욱더 맞춤화하려면 나만의 modelContainer 인스턴스를 별도로 빌드할 수 있죠 Trips 앱에서 보여드리겠습니다 modelContainer 한정자로 컨테이너를 구성하는 대신 container 속성을 사용하여 직접 만들어 보겠습니다
속성의 클로저에서 모델에 대한 구성을 만들고 스키마를 전달합니다 여기에서 디스크의 데이터에 대한 URL도 맞춤화하겠습니다 그런 다음 이 구성을 ModelContainer 이니셜라이저에 전달한 후 반환할게요
iOS 18에서는 SwiftData로 modelContainer를 더 맞춤화할 수 있죠 완전히 맞춤화된 데이터 저장소로 가능합니다 기본 데이터 저장소는 SwiftData의 모든 기능을 지원하는 강력한 영속성 백엔드를 제공합니다 하지만 이제 자체 구현을 사용하여 컨테이너 전체의 데이터를 영속화시키는 나만의 데이터 저장소를 만들 수 있습니다
예를 들어, Trips 앱에서 저는 JSON 파일로 만든 맞춤화 문서 형식을 구현했죠 이를 앱에서 사용하려면 현재 모델 구성을 맞춤화된 데이터 저장소의 모델 구성과 교체하면 됩니다 이 예시에서는 JSONStoreConfiguration이죠
맞춤화된 데이터 저장소를 사용하면 영속화해야 할 데이터 형식과 관계없이 @Model 및 @Query 매크로 등 주로 사용하는 SwiftData API를 활용할 수 있죠 또한 데이터 저장소가 기능을 점진적으로 채택할 수 있으므로 빠르게 시작할 수 있습니다 더 자세히 알아보려면 ‘SwiftData로 자체 데이터 저장소 만들기’를 확인하세요
또한 Xcode 미리보기에서 사용할 맞춤화된 컨테이너를 만들 수 있죠 미리보기는 SwiftUI로 앱을 개발할 때 매우 유용하며 SwiftData에서도 잘 작동합니다
Trips 앱의 모든 보기에 대해 멋진 미리보기를 만들고자 합니다 미리보기 특성을 사용하여 시작해 보겠습니다
이를 위해 PreviewModifier를 따르는 SampleData라는 새 구조체를 만듭니다 이 구조체에는 다음 두 가지 함수를 작성해야 합니다 하나는 미리보기에 대한 공유 컨텍스트를 설정하기 위한 함수고 다른 하나는 공유 컨텍스트를 보기에 적용하기 위한 함수입니다 Trips 미리보기에 대해 ModelContainer를 sampleData의 공유 컨텍스트로 제공하겠습니다 미리보기는 디스크에 아무것도 저장할 필요가 없으므로 데이터를 메모리에만 저장하는 ModelConfiguration을 만들고 ModelContainer를 설정하겠습니다
그런 다음 앞서 만든 메소드를 호출하여 다양한 샘플 여행을 생성하고 이를 모델 컨테이너에 저장합니다 여행의 이름과 날짜는 모두 고유하므로 제가 직접 코드에서 중복을 제거하지 않아도 돼요, SwiftData가 대신 제거하죠
마지막으로 컨테이너를 반환하겠습니다 다음으로, 이 sampleData가 사용되는 보기에 이 modelContainer를 추가하는 메소드를 구현해야 합니다 이를 위해서는 modelContainer 한정자로 컨테이너를 적용하기만 하면 됩니다
마지막으로, 이 sampleData를 간단하게 접근하기 위해 PreviewTrait에 대한 확장을 추가하겠습니다 이렇게 하면 sampleData()라는 새 정적 속성이 생성됩니다 이 SampleData() 구조를 한정자로 적용하죠
이제 SwiftUI 보기에 대한 미리보기를 선언하면 .sampleData와 traits 매개변수를 함께 사용할 수 있습니다 이렇게 하면 인메모리 모델 컨테이너가 생성되고 sampleData가 로드되고 미리보기가 수정되어 SwiftUI 보기에서 사용할 수 있죠
훌륭한 샘플 데이터를 사용할 수 있으면 SwiftData 쿼리를 사용하여 앱의 모든 보기에서 쉽게 작업할 수 있죠 그러나 앱 보기 중 일부는 쿼리를 포함하지 않을 수 있습니다 전달되는 모델에 의존하기 때문이죠 이를 위해 @Previewable 매크로로 멋진 미리보기를 만들 수 있습니다
Trips에서 예를 보여드릴게요 BucketListItemView는 하나의 trip을 매개변수로 사용합니다 BucketListItemView에 sampleData가 있는 모델 컨테이너가 있지만 아직 데이터를 쿼리하지 않았습니다
이제 @Previewable 매크로를 사용하여 미리보기 선언에서 바로 쿼리를 만들 수 있죠 이렇게 하면 sampleData를 사용하여 미리보기를 만들기 위해 BucketListItemView에 전달할 수 있는 trip의 배열이 제공됩니다
마지막으로 SwiftData에 대해 풍부하고 최적화된 쿼리를 만들어 보죠 쿼리는 쉽게 정렬 및 필터링할 수 있는 Model 배열로 SwiftUI 뷰를 구동하며 ModelContainer의 변경 사항에 자동으로 반응합니다 #Predicate는 필터링을 촉진하고 데이터 쿼리 중에 평가될 수 있죠 대용량 인메모리 데이터 세트 대신에요 Trip에 필터를 적용하는 몇 가지 방법을 알아보겠습니다 Trips 앱에 검색 창을 추가하면 searchText를 사용하여 쿼리를 필터링하기 위한 predicate, 심지어는 fetch를 만들 수 있죠
predicate는 쉽게 빌드할 수 있습니다 사용자가 제공한 searchText가 Trip 이름에 있는지 확인하면 되죠 그러나 텍스트는 Trip 이름 외 항목에도 적용될 수 있습니다
그러므로 복합 predicate를 빌드하여 Trip의 destination 속성도 검사하겠습니다 복합 predicate를 만드는 데 필요한 것은 이것이 전부였으나 predicate가 더 많은 기능을 수행하도록 할 수 있습니다
이제 iOS 18에서는 Foundation의 새로운 #Expression 매크로로 복잡한 predicate를 쉽게 만들 수 있습니다
Expression은 참 또는 거짓을 생성하지 않고 임의의 유형을 허용하는 참조 값을 허용합니다
Expression은 모델의 속성을 사용하여 복잡한 평가를 표현하는 데 사용할 수 있으며 predicate 내에서 구성하여 쿼리 결과를 더욱 맞춤화할 수 있습니다
Trips 앱에서 아직 볼 곳이 남아있는 진행 중인 여행을 가져오는 쿼리를 만들고 싶습니다 이러한 여행은 isInPlan 속성이 아직 false인 trip의 BucketListItems로 모델링됩니다 predicate를 빌드하여 시작하겠습니다
predicate에서 여행이 계속 진행 중임을 지정해야 하므로 현재 날짜가 시작 날짜와 종료 날짜 사이에 속해야겠네요
한편 trip의 BucketListItems 중 하나 이상의 isInPlan 속성이 false로 설정되어야 한다고 지정해야 합니다 Predicate만으로는 이를 표현할 수 없죠 계획되지 않은 BucketListItems의 수를 집계하는 속성이 없기 때문입니다 이를 위해 이 로직을 predicate에 구축하는 표현식을 만들겠습니다
이 표현식은 제가 아직 계획하지 않은 BucketListItems의 수를 집계할 것입니다 이 표현식은 BucketListItems의 배열을 가져온 다음 필터 요건을 충족하는 항목의 수를 반환합니다
이제 이 표현식을 제공된 trip의 bucketList 항목과 함께 predicate의 일부로 평가할 수 있죠 그러면 predicate에서 표현식의 결과가 0보다 큰지 확인할 수 있습니다 표현식을 사용하면 predicate 매크로를 훨씬 더 강력하고 표현력이 풍부한 쿼리 작성 도구로 활용하여 앱에 필요한 데이터를 효율적으로 가져올 수 있습니다 하지만 쿼리의 성능을 개선하는 또 다른 방법이 있는데 바로 새로운 스키마 매크로인 #Index를 사용하는 것입니다 새로운 #Index 매크로는 모델에 단일 또는 복합 인덱스를 생성하는 기능을 추가합니다 인덱스는 책의 목차처럼 SwiftData가 생성하여 컨테이너에 저장하는 추가 메타데이터를 나타냅니다 이 메타데이터를 사용하면 지정된 주요 경로에 대한 쿼리를 빠르고 효율적으로 수행할 수 있습니다
이러한 이점을 활용하려면 SwiftData가 인덱스를 생성해야 하는 속성을 선언해야 합니다 쿼리를 정렬 및 필터링할 때 가장 자주 발생하는 속성을 고려해 보세요
Trips 앱에서는 name, startDate, endDate를 사용하는 필터 및 정렬에 의해 trip이 자주 쿼리됩니다 이러한 쿼리를 더욱 빠르게 수행하려면 #Index 매크로를 추가한 다음 name, startDate 및 endDate의 주요 경로와 이 3가지의 복합 인덱스를 지정하면 됩니다 광범위한 휴가 아이디어와 같이 대규모 데이터 세트의 경우 필터링 및 정렬이 상당히 빨라집니다 SwiftUI에서 predicate 매크로를 사용하면 쿼리를 쉽게 활용할 수 있으며 표현식을 사용하면 더욱 강력해집니다 이제 #Index 매크로로 앱에서 쿼리의 성능을 더욱 향상시킬 수 있습니다
강력한 SwiftData 기능으로 앱의 모델 레이어를 빌드하세요 스키마에 #Unique 제약 조건을 추가하면 모델이 중복되는 일을 쉽게 막을 수 있습니다 그리고 새로운 #Index 매크로를 추가하여 쿼리 속도를 높이세요
새로운 History API로 앱의 모델 변경 사항을 추적하세요 그리고 맞춤화된 데이터 저장소로 나만의 문서 형식 또는 영속성 백엔드로 SwiftData의 기능을 활용할 수 있습니다
이 세션을 들어주셔서 감사합니다 여러분이 개발할 놀라운 결과물을 기대하겠습니다
-
-
1:32 - SampleTrips models decorated with @Model
// Trip Models decorated with @Model import Foundation import SwiftData @Model class Trip { var name: String var destination: String var startDate: Date var endDate: Date var bucketList: [BucketListItem] = [BucketListItem]() var livingAccommodation: LivingAccommodation? } @Model class BucketListItem {...} @Model class LivingAccommodation {...}
-
1:43 - SampleTrips using modelContainer scene modifier
// Trip App using modelContainer Scene modifier import SwiftUI import SwiftData @main struct TripsApp: App { var body: some Scene { WindowGroup { ContentView } .modelContainer(for: Trip.self) } }
-
1:53 - SampleTrips using @Query
// Trip App using @Query import SwiftUI import SwiftData struct ContentView: View { @Query var trips: [Trip] var body: some View { NavigationSplitView { List(selection: $selection) { ForEach(trips) { trip in TripListItem(trip: trip) } } } } }
-
2:16 - SampleTrips models decorated with @Model
// Trip Models decorated with @Model import Foundation import SwiftData @Model class Trip { var name: String var destination: String var startDate: Date var endDate: Date var bucketList: [BucketListItem] = [BucketListItem]() var livingAccommodation: LivingAccommodation? } @Model class BucketListItem {...} @Model class LivingAccommodation {...}
-
3:08 - Add unique constraints to avoid duplication
// Add unique constraints to avoid duplication import SwiftData @Model class Trip { #Unique<Trip>([\.name, \.startDate, \.endDate]) var name: String var destination: String var startDate: Date var endDate: Date var bucketList: [BucketListItem] = [BucketListItem]() var livingAccommodation: LivingAccommodation? }
-
3:36 - Add .preserveValueOnDeletion to capture unique columns
// Add .preserveValueOnDeletion to capture unique columns import SwiftData @Model class Trip { #Unique<Trip>([\.name, \.startDate, \.endDate]) @Attribute(.preserveValueOnDeletion) var name: String var destination: String @Attribute(.preserveValueOnDeletion) var startDate: Date @Attribute(.preserveValueOnDeletion) var endDate: Date var bucketList: [BucketListItem] = [BucketListItem]() var livingAccommodation: LivingAccommodation? }
-
4:35 - SampleTrips using modelContainer scene modifier
// Trip App using modelContainer Scene modifier import SwiftUI import SwiftData @main struct TripsApp: App { var body: some Scene { WindowGroup { ContentView() } .modelContainer(for: Trip.self) } }
-
4:52 - Customize a model container in the app
// Customize a model container in the app import SwiftUI import SwiftData @main struct TripsApp: App { var body: some Scene { WindowGroup { ContentView() } .modelContainer(for: Trip.self, inMemory: true, isAutosaveEnabled: true, isUndoEnabled: true) } }
-
5:13 - Add a model container to the app
// Add a model container to the app import SwiftUI import SwiftData @main struct TripsApp: App { var container: ModelContainer = { do { let configuration = ModelConfiguration(schema: Schema([Trip.self]), url: fileURL) return try ModelContainer(for: Trip.self, configurations: configuration) } catch { ... } }() var body: some Scene { WindowGroup { ContentView() } .modelContainer(container) } }
-
5:59 - Use your own custom data store
// Use your own custom data store import SwiftUI import SwiftData @main struct TripsApp: App { var container: ModelContainer = { do { let configuration = JSONStoreConfiguration(schema: Schema([Trip.self]), url: jsonFileURL) return try ModelContainer(for: Trip.self, configurations: configuration) } catch { ... } }() var body: some Scene { WindowGroup { ContentView() } .modelContainer(container) } }
-
6:58 - Make preview data using traits
// Make preview data using traits struct SampleData: PreviewModifier { static func makeSharedContext() throws -> ModelContainer { let config = ModelConfiguration(isStoredInMemoryOnly: true) let container = try ModelContainer(for: Trip.self, configurations: config) Trip.makeSampleTrips(in: container) return container } func body(content: Content, context: ModelContainer) -> some View { content.modelContainer(context) } } extension PreviewTrait where T == Preview.ViewTraits { @MainActor static var sampleData: Self = .modifier(SampleData()) }
-
8:15 - Use sample data in a preview
// Use sample data in a preview import SwiftUI import SwiftData struct ContentView: View { @Query var trips: [Trip] var body: some View { ... } } #Preview(traits: .sampleData) { ContentView() }
-
8:50 - Create a preview query using @Previewable
// Create a preview query using @Previewable import SwiftUI import SwiftData #Preview(traits: .sampleData) { @Previewable @Query var trips: [Trip] BucketListItemView(trip: trips.first) }
-
9:55 - Create a predicate to find a Trip based on search text
// Create a Predicate to find a Trip based on Search Text let predicate = #Predicate<Trip> { searchText.isEmpty ? true : $0.name.localizedStandardContains(searchText) }
-
10:06 - Create a Compound Predicate to find a Trip based on Search Text
// Create a Compound Predicate to find a Trip based on Search Text let predicate = #Predicate<Trip> { searchText.isEmpty ? true : $0.name.localizedStandardContains(searchText) || $0.destination.localizedStandardContains(searchText) }
-
10:46 - Build a predicate to find Trips with BucketListItems that are not in the plan
// Build a predicate to find Trips with BucketListItems that are not in the plan let unplannedItemsExpression = #Expression<[BucketListItem], Int> { items in items.filter { !$0.isInPlan }.count } let today = Date.now let tripsWithUnplannedItems = #Predicate<Trip>{ trip // The current date falls within the trip (trip.startDate ..< trip.endDate).contains(today) && // The trip has at least one BucketListItem // where 'isInPlan' is false unplannedItemsExpression.evaluate(trip.bucketList) > 0 }
-
12:41 - Add Index for commonly used KeyPaths or combination of KeyPaths
// Add Index for commonly used KeyPaths or combination of KeyPaths import SwiftData @Model class Trip { #Unique<Trip>([\.name, \.startDate, \.endDate #Index<Trip>([\.name], [\.startDate], [\.endDate], [\.name, \.startDate, \.endDate]) var name: String var destination: String var startDate: Date var endDate: Date var bucketList: [BucketListItem] = [BucketListItem var livingAccommodation: LivingAccommodation }
-
-
찾고 계신 콘텐츠가 있나요? 위에 주제를 입력하고 원하는 내용을 바로 검색해 보세요.
쿼리를 제출하는 중에 오류가 발생했습니다. 인터넷 연결을 확인하고 다시 시도해 주세요.