커스텀 데이터 저장소는 임의의 문서, 파일 포맷 혹은 영속성 백엔드를 추가할 수 있는 새로운 기능
ModelConfiguration을 바꾸기만 하면 된다.
import SwiftUI
import SwiftData
struct TripsApp: App {
var container: ModelContainer = {
do {
let configuration = JSONStoreConfiguration(schema: Schema([Trip.self]), url: fileURL)
return try ModelContainer(for: Trip.self, configuration: configuration)
} catch { ... }
} ()
}
Overview
저장소: 영속성 모델을 지원하기 위해 필요한 모든 데이터를 가져오고 저장하는 책임을 가지고 있는 개체
modelContext와 저장소의 상호작용 예시
삭제와 추가 모두 처리하는데, 이때 추가되는 것에는 임시 ID를 발급한다.
저장소는 추가된 모델에 영구적인 ID를 발급하고 임시 ID를 여기에 매핑한다(remapping)
저장소는 modelContext에 업데이트된 ID를 응답으로 주고 Context는 이를 받아서 자신을 업데이트한다.
이제 뷰를 업데이트 한다.
변경 뿐 아니라 데이터 가져오기와 저장등의 요청 셋을 처리한다.
저장소는 모델이 실제로 저장되는 구현을 제공한다.
상호작용은 DataStoreSnapshot이라는 sendable하고 codable한 표현을 사용한다.
이렇든 저장소는 ModelContext가 어떠한 포맷이던 읽을 수 있게 만들어주는 중요한 역할을 한다.
Meet DataStore
기본 구현체: DefaultStore
ModelContext가 사용하기 위해서 필요한 기능들을 정의한다.
추가 프로토콜로 추가 기능 구현 가능
ex. fetch(DataStoreFetchRequest, DataStoreFetchResult)
ex. save(DataStoreSaveChangeRequest, DataStoreSaveChangeResult)
Example store
타입 정의
class JSONStoreConfiguration: DataStoreConfiguration {
}
class JSONStore: DataStore {
}
연관 타입 정의
class JSONStoreConfiguration: DataStoreConfiguration {
typealias Store = JSONStore
}
class JSONStore: DataStore {
typealias Configuration = JSONStoreConfiguration
typealias Snapshot = DefaultSnapshot
}
fetch 구현
class JSONStore: DataStore {
func fetch<T>(_ request: DataStoreFetchRequest<T>) throws -> DataStoreFetchRequest<T, DefaultSnapshot> where T: PersistentModel {
if request.descriptor.predicate != nil {
throw DataStoreError.preferInMemoryFilter
} else if request.desriptor.sortBy.count > 0 {
throw DataSourceError.preferInMemorySort
}
let decoder = JSONDecoder()
let data = try Data(contentsOf: configuration.fileURL)
let trips = try decoder.decode([DefaultSnapshot].self, from: data)
return DataSourceFetchResult(descriptor: request.descriptor,
fetchedSnapshots: trips)
}
}
save 구현
class JSONStore: DataStore {
func save(_ request: DataStoreSaveChangesRequest<DefaultSnapshot>) throws -> DataStoreSaveChangesResult<DefaultSnapshot> {
/// 별도로 정의한 메소드를 통해서 데이터를 읽은 다음에 스냅샷을 분류한다.
var snapshotsByIdentifier = [PersistentIdentifier: DefaultSnapshot]()
try self.read().forEach { snapshotsByIdentifier[$0.persistentIdentifier] = $0 }
/// 추가 요청을 처리한다. 여기서 remapping도 해준다.
var remappedIdentifiers = [PersistentIdentifier: PersistentIdentifier]()
for snapshot in request.inserted {
let entityName = snapshot.persistentIdentifier.entityName
let permanentIdentifier = try PersistentIdentifier.identifier(for: identifier,
entityName: entityName,
primaryKey: UUID())
let snapshotCopy = snapshot.copy(persistentIdentifier: permanentIdentifier)
remappedIdentifiers[snapshot.persistentIdentifier] = permanentIdentifier
snapshotsByIdentifier[permanentIdentifier] = snapshotCopy
}
// 업데이트 처리
for snapshot in request.updated {
snapshotsByIdentifier[snapshot.persistentIdentifier] = snapshot
}
// 삭제 처리
for snapshot in request.deleted {
snapshotsByIdentifier[snapshot.persistentIdentifier] = nil
}
// 저장
let snapshots = snapshotsByIdentifier.values.map { $0 }
let encoder = JSONEncoder()
let jsonData = try encoder.encode(snapshots)
try jsonData.write(to: configuration.fileURL)
return DataStoreSaveChangesResults(for: self.identifier,
remappedIdentifiers: remappedIdentifiers)
}
Configuration 구현
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)
}
}
JSONStore 전체 구현
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)
}
}