스트리밍은 대부분의 브라우저와
Developer 앱에서 사용할 수 있습니다.
-
SwiftData로 커스텀 데이터 저장소 만들기
영속성 백엔드에 명시적이고 선언적인 SwiftData 모델링 API의 역량을 통합해 보세요. 맞춤형 데이터 저장소를 빌드하는 방법과 앱에 영속성 기능을 단계적으로 추가하는 방법을 알아보세요. 이 세션을 최대한 활용하려면 먼저 WWDC23의 ‘SwiftData 알아보기' 및 'SwiftData로 스키마 모델링하기'를 시청하는 것이 좋습니다.
챕터
- 0:00 - Introduction
- 1:21 - Overview
- 4:50 - Meet DataStore
- 7:42 - Example store
리소스
관련 비디오
WWDC24
-
다운로드
안녕하세요 Luvena입니다 자체 지속성 백엔드와 SwiftData 사용 방법인 SwiftData의 맞춤형 DataStore를 설명하게 되어 기쁩니다 맞춤형 데이터 저장소는 원하는 문서, 파일 형식이나 지속성 백엔드를 사용할 수 있는 SwiftData의 새로운 기능입니다 또 기존의 모든 SwiftData 코드와도 잘 작동합니다
다음과 같이 SampleTrips 앱의 구현의 경우 이 비디오 뒤에서 구현할 예제 JSONStoreConfiguration으로 ModelConfiguration을 대체하면 저장소 유형을 변경할 수 있습니다 이 한 번의 대체만으로 이제 SampleTrips 앱에서 모델이나 뷰 코드를 변경할 필요 없이 ModelContainer가 다른 저장소 유형 사용 방법을 인식합니다
이 비디오는 SwiftData에서 저장소가 수행하는 역할과 ModelContext 및 ModelContainer와 상호 작용하는 방식을 소개합니다 또 새 DataStore 프로토콜로 빌드되는 방법을 살펴보겠습니다 마지막으로, 지속성을 위해 JSON 파일을 사용하는 예제를 통해 맞춤형 DataStore 구현의 필수 사항을 다뤄보겠습니다 상위 수준에서 저장소는 영구 모델을 지원할 때 필요한 모든 데이터를 가져오고 저장할 책임이 있습니다 SwiftData에서 맞춤형 데이터 저장소 작동 방식을 위해 제 앱 SampleTrips에 지속성 제공 방법을 살펴보겠습니다 이 앱은 SwiftUI, SwiftData의 강력한 시너지로 만들어졌습니다 일반적인 앱은 세 가지 중요한 부분으로 구성됩니다 SwiftUI는 사용자 인터페이스 ModelContext 모델에 데이터 표시 목록이나 레이블과 같은 뷰를 제공합니다 ModelContext는 ModelContainer 저장소로 데이터를 읽고 씁니다 이 비디오에서는 특히 SwiftData에서 저장소의 역할에 대해 중점을 두겠습니다 SampleTrips는 ModelContext로 뷰를 구동하고 여행을 표시합니다 ModelContext는 뷰의 각 여행에 대한 영구적 모델을 인스턴스화합니다 또한 이러한 각 여행에는 모델을 고유하게 식별하는 해당 영구 식별자가 있습니다 그리고 ModelContext는 제가 변경한 내용을 추적하여 필요할 때 저장소에 저장할 수 있도록 합니다 예를 들어, LA 여행을 취소하고 도쿄로 새 여행을 추가하기로 결정하면 이 변경은 ModelContext로 추적됩니다 새로운 도쿄 모델이 ModelContext에 삽입되면 임시 PersistentIdentifier에 의해 식별됩니다 ModelContext가 저장되면 저장소에 LA 여행을 삭제하고 새 도쿄 여행을 삽입하도록 지시합니다 그 다음 저장소는 ‘리매핑‘이라는 프로세스를 통해 도쿄 모델에 지속적 영구 식별자 Trip-5를 할당하고 이전 임시 식별자 Trip-t1에 매핑합니다
그런 다음 저장소는 도쿄 여행에 대한 업데이트된 영구 식별자로 ModelContext에 응답합니다
ModelContext가 상태 업데이트를 완료한 후 UI는 여행을 렌더링하는 뷰를 업데이트할 수 있습니다 변경 사항 지속은 ModelContext와 저장소가 함께 작동하여 SwiftData의 PersistentModel 지원 방법의 한 예에 불과합니다 이들은 가져오기 또는 저장과 같은 작업을 정의하는 일련의 요청과 응답을 사용하여 통신합니다 스토어의 역할은 모델 값이 유지되는 방식에 대한 구현을 제공하는 것입니다 이 커뮤니케이션에서는 DataStoreSnapshot이라는 전송 가능하고 코딩 가능한 모델 표현을 활용합니다
SampleTrips 앱에서 뷰는 영구적 모델을 사용하여 ModelContext와 통신합니다 하지만 ModelContext가 스토어와 통신해야 하는 경우 모델의 현재 상태를 저장하는 스냅샷을 생성합니다 스냅샷은 해당 시점의 모델 값에 대한 전송 가능하고 코딩 가능한 컨테이너입니다 영구적 모델과 마찬가지로 각각은 영구 식별자로 식별됩니다
저장소는 이 스냅샷을 사용하고 값을 저장 공간에 적용합니다 반대의 경우도 마찬가지입니다 저장소의 ModelContext가 데이터를 읽으면 저장소는 컨텍스트에서 요청하는 PersistentModel별 스냅샷 세트를 생성합니다
ModelContext는 뷰, 쿼리나 컨텍스트의 타 작업에 사용할 각 스냅샷에 PersistentModel을 만듭니다 저장소는 ModelContext가 모델 데이터를 저장 공간 형식으로 읽고 쓸 수 있게 해 SwiftData에서 중요한 역할을 합니다 새 DataStore 프로토콜과 작동 방식을 알려 드리겠습니다
저장소는 이를 설명하는 구성 ModelContext와 모델 값 전달 스냅샷, ModelContainer가 관리할 수 있는 저장소 구현이라는 세 가지 핵심 부분으로 구성됩니다 이러한 각 부분은 세 가지 서로 다른 프로토콜을 준수하는데 DataStoreConfiguration DataStoreSnapshot, DataStore이며 SwiftData 저장소는 ModelConfiguration, DefaultSnapshot DefaultStore 유형의 자체 구현을 제공합니다 DefaultStore는 마이그레이션 기록 추적, CloudKit 동기화 등 SwiftData의 모든 풍부한 기능을 지원합니다 또 성능, 확장성에 대한 플랫폼의 모범 사례를 캡슐화하여 영구적 모델을 위한 최선의 기본 선택이 됩니다
DataStore 프로토콜은 저장 가져오기, 캐싱을 포함해 저장소를 ModelContext에서 사용할 수 있게 SwiftData가 필요한 모든 기능을 정의합니다 추가 프로토콜은 저장소에 적용된 모든 변경 사항을 설명하는 새로운 History 프로토콜과 같은 선택적 데이터 저장소 기능을 정의합니다
ModelContext는 DataStore 프로토콜의 요청 및 응답을 사용하여 저장소와 통신합니다 예를 들어 저장소에서 데이터를 가져올 때 ModelContext는 저장소가 검색할 데이터를 설명하는 FetchDescriptor가 포함된 DataStoreFetchRequest를 저장소에 전송합니다
저장소에서 모델 값을 검색하면 각 모델에 대한 스냅샷을 생성하며 DataStoreFetchResult에서 반환합니다
그런 다음 ModelContext가 각 스냅샷의 PersistentModel을 생성합니다
ModelContext에서 모델을 변경하고 저장을 호출할 때도 비슷한 프로세스가 발생합니다 ModelContext는 수정된 모든 모델에 대한 스냅샷이 포함된 DataStoreSaveChangesRequest를 생성하고 요청을 저장소로 보냅니다
그런 다음 저장소는 스냅샷을 저장 공간에 적용하고 DataStoreSaveChangesResult를 생성해 ModelContext로 전송합니다 결과적으로 저장소는 Trip-t1과 같이 새로 삽입된 모델에 대한 리매핑된 식별자 맵을 제공합니다 이렇게 하면 ModelContext에게 삽입된 Trip의 영구 식별자를 Trip-5로 업데이트하게 합니다
마지막으로 ModelContext는 저장소의 저장 결과를 처리하고 상태를 업데이트하여 삽입된 Trip에 새 지속 영구식별자를 할당합니다
이제 DataStore의 역학에 대해 살펴보았으니 실제 구현하는 모습을 알아보려고 합니다 JSON 파일을 사용하여 SampleTrips 응용 프로그램에서 모델을 유지하는 저장소를 구현해 보겠습니다 시작하기 전에 두 가지에 대해 명확히 하고 싶습니다 이 저장소는 ‘아카이브 저장소‘로 읽거나 쓸 때 전체 파일이 로드됩니다 또한 Foundation에서 제공하는 JSON 코더를 사용하여 데이터를 파일에 스냅샷 배열로 저장할 것입니다
저장소를 생성하는 첫 번째 단계는 DataStoreConfiguration과 DataStore 프로토콜을 준수하는 구성 및 스토어 유형을 선언하는 것입니다
이 유형은 연관 유형을 사용해 서로를 참조합니다 구성에서 Store 유형을 JSONStore로 설정하고 저장소에서 Configuration을 JSONStoreConfiguration으로 설정했습니다
또 JSONStore는 ModelContext와 통신 시 사용하는 스냅샷의 유형을 선언합니다 여기서는 DefaultSnapshot을 사용합니다 모델 데이터의 인코딩이나 디코딩을 사용자화할 필요가 없기 때문입니다 이제 ModelContext에서 DataStore를 사용하는 데 필요한 두 메서드인 fetch와 save를 구현할 수 있습니다 ModelContext가 DataStoreFetchRequest를 전송하면 저장소에 있는 데이터를 로드하고 DataStoreFetchResult를 인스턴스화해야 합니다 DefaultSnapshot은 코딩 가능하니 JSONDecoder를 사용해 구성에서 제공한 파일 URL에서 저장에 대한 데이터를 로드할 수 있습니다 그런 다음 파일에서 스냅샷과 DataStoreFetchResult를 인스턴스화하여 반환합니다 현재 이 구현은 FetchDescriptor에 있는 predicate 또는 sort comparator를 처리하지 않습니다 Predicate나 sort comparator 변환은 복잡한 프로세스일 수 있는데 이 작업은 ModelContext로 대신 수행할 수 있습니다
이를 위해 요청에 predicate 또는 sort descriptor가 있으면 ‘preferInMemorySort‘ ‘preferInMemoryFilter‘ 오류에 throw를 사용하겠습니다 이때 이 방법은 매우 효과적인데 메모리에 로드할 수 있는 작은 데이터 세트이기 때문입니다 이제 쿼리 및 정렬을 지원할 수 있는 완전한 기능의 fetch 구현이 완성되었습니다 fetch가 구현되면 save를 구현하여 스냅샷을 JSON 파일에 쓸 수 있습니다 save를 구현할 때 삽입, 업데이트, 삭제라는 세 가지 유형의 변경을 고려하고 처리하려고 합니다 save 요청에서 수신되는 스냅샷의 처리를 시작하기 전에 먼저 파일의 현재 내용을 읽어와야 하는데 읽기로 정의한 별도 메서드로 처리합니다 모든 스냅샷을 영구 식별자에 의해 키가 지정된 사전으로 정리할 것이며 이는 마지막에 디스크에 기록될 새 JSON 파일의 작업 복사본이 될 것입니다
그런 다음 save 요청 내에서 삽입된 모델의 스냅샷을 처리합니다 삽입된 각 스냅샷에 식별자를 할당, 재매핑하는 작업이 포함됩니다 조금 더 자세히 살펴보겠습니다 모델이 저장소에 삽입될 때 각 모델에는 저장소와 연결 안 된 임시 식별자가 포함되어 있습니다 여기에 삽입된 각 스냅샷에 대해 새로운 지속적 영구 식별자를 생성합니다 이제 새 영구 식별자를 사용하는 스냅샷 사본을 만듭니다 이 새 영구 식별자는 나중에 저장 결과에서 ModelContext로 반환하기 위해 remappedIdentifiers 사전의 임시 식별자에 매핑됩니다 마지막으로, 삽입한 스냅샷을 파일에서 처음 로드한 스냅샷에 추가합니다
삽입된 스냅샷을 처리한 후 파일의 스냅샷을 save 요청에 있는 스냅샷으로 대체하여 업데이트를 처리합니다 마지막으로 파일에서 로드된 스냅샷에서 삭제된 스냅샷을 제거합니다 이제 파일에 다시 쓰고 싶은 완전한 업데이트된 데이터 세트가 snapshotsByIdentifier 사전에 있습니다
JSONEncoder를 사용하여 이 스냅샷의 작업 복사본을 단일 JSON 파일로 디스크에 다시 쓰겠습니다 마지막으로 save 결과가 포함된 DataStoreSaveChangesResult를 반환합니다 여기에는 업데이트할 컨텍스트에 대한 리매핑된 persistentIdentifier가 포함됩니다
이제 완전한 맞춤형 데이터 저장소가 있으므로 SampleTrips에 적용할 수 있습니다 앱의 정의에서 ModelConfiguration을 JSONStoreConfiguration으로 바꾸면 저장소 유형을 변경할 수 있습니다 With just this one replacement, the ModelContainer now knows to use a different store type, ModelContainer가 다른 저장소 유형을 사용하는 방법을 인식합니다
DataStore를 통해 SwiftData는 모든 저장 공간 형식 또는 지속성 백엔드에 데이터를 읽고 쓸 수 있습니다
이를 통해 필요한 모든 문서 데이터베이스나 클라우드 저장 공간에 SwiftUI PersistentModel의 힘을 사용할 수 있고 ModelContext는 필터링 및 정렬 기능을 제공하여 간단한 저장소 구현의 복잡성을 줄여 줍니다
SwiftData에 맞춤형 저장소를 적용하는 건 DataStoreConfiguration을 변경하는 것만큼 간단합니다 새 DataStore 프로토콜로 모든 지속성 백엔드에 대한 지원을 구현할 수 있습니다 이제 SwiftData는 새로운 가능성을 열었습니다 인덱싱 및 고유 제약 사항과 같은 다른 새로운 기능은 ‘SwiftData의 새로운 기능‘을 확인하세요 ‘SwiftData 기록으로 모델 변경 사항 추적하기‘에서 저장소의 내역을 살펴보는 방법에 대해 자세히 알아보세요
시청해 주셔서 감사합니다 어떤 작품이 나올지 기대되네요
-
-
8:15 - Implement a JSON store
// Implement a JSON store @available(swift 5.9) @available(macOS 15, iOS 18, tvOS 18, watchOS 11, visionOS 2, *) final class JSONStoreConfiguration: DataStoreConfiguration { typealias StoreType = JSONStore var name: String var schema: Schema? var fileURL: URL init(name: String, schema: Schema? = nil, fileURL: URL) { self.name = name self.schema = schema self.fileURL = fileURL } static func == (lhs: JSONStoreConfiguration, rhs: JSONStoreConfiguration) -> Bool { return lhs.name == rhs.name } func hash(into hasher: inout Hasher) { hasher.combine(name) } } @available(swift 5.9) @available(macOS 15, iOS 18, tvOS 18, watchOS 11, visionOS 2, *) final class JSONStore: DataStore { typealias Configuration = JSONStoreConfiguration typealias Snapshot = DefaultSnapshot var configuration: JSONStoreConfiguration var name: String var schema: Schema var identifier: String init(_ configuration: JSONStoreConfiguration, migrationPlan: (any SchemaMigrationPlan.Type)?) throws { self.configuration = configuration self.name = configuration.name self.schema = configuration.schema! self.identifier = configuration.fileURL.lastPathComponent } func save(_ request: DataStoreSaveChangesRequest<DefaultSnapshot>) throws -> DataStoreSaveChangesResult<DefaultSnapshot> { var remappedIdentifiers = [PersistentIdentifier: PersistentIdentifier]() var serializedTrips = try self.read() for snapshot in request.inserted { let permanentIdentifier = try PersistentIdentifier.identifier(for: identifier, entityName: snapshot.persistentIdentifier.entityName, primaryKey: UUID()) let permanentSnapshot = snapshot.copy(persistentIdentifier: permanentIdentifier) serializedTrips[permanentIdentifier] = permanentSnapshot remappedIdentifiers[snapshot.persistentIdentifier] = permanentIdentifier } for snapshot in request.updated { serializedTrips[snapshot.persistentIdentifier] = snapshot } for snapshot in request.deleted { serializedTrips[snapshot.persistentIdentifier] = nil } try self.write(serializedTrips) return DataStoreSaveChangesResult<DefaultSnapshot>(for: self.identifier, remappedPersistentIdentifiers: remappedIdentifiers, deletedIdentifiers: request.deleted.map({ $0.persistentIdentifier })) } func fetch<T>(_ request: DataStoreFetchRequest<T>) throws -> DataStoreFetchResult<T, DefaultSnapshot> where T : PersistentModel { if request.descriptor.predicate != nil { throw DataStoreError.preferInMemoryFilter } else if request.descriptor.sortBy.count > 0 { throw DataStoreError.preferInMemorySort } let objs = try self.read() let snapshots = objs.values.map({ $0 }) return DataStoreFetchResult(descriptor: request.descriptor, fetchedSnapshots: snapshots, relatedSnapshots: objs) } func read() throws -> [PersistentIdentifier: DefaultSnapshot] { if FileManager.default.fileExists(atPath: configuration.fileURL.path(percentEncoded: false)) { let decoder = JSONDecoder() decoder.dateDecodingStrategy = .iso8601 let trips = try decoder.decode([DefaultSnapshot].self, from: try Data(contentsOf: configuration.fileURL)) var result = [PersistentIdentifier: DefaultSnapshot]() trips.forEach { s in result[s.persistentIdentifier] = s } return result } else { return [:] } } func write(_ trips: [PersistentIdentifier: DefaultSnapshot]) throws { let encoder = JSONEncoder() encoder.dateEncodingStrategy = .iso8601 encoder.outputFormatting = [.prettyPrinted, .sortedKeys] let jsonData = try encoder.encode(trips.values.map({ $0 })) try jsonData.write(to: configuration.fileURL) } }
-
-
찾고 계신 콘텐츠가 있나요? 위에 주제를 입력하고 원하는 내용을 바로 검색해 보세요.
쿼리를 제출하는 중에 오류가 발생했습니다. 인터넷 연결을 확인하고 다시 시도해 주세요.