SwiftData 복습
앱의 모델 레이어를 쉽게 만들고 앱 실행 간에 영속적으로 저장하게 해주는 프레임워크
영속성 뿐 아니라 모델링, 스키마 마이그레이션, 그래프 관리, CloudKit을 통한 동기화 등의 다양한 기능을 제공한다.
기본 사용법
모델에 @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 {...}
모델 컨테이너를 뷰에서 지정
// Trip App using modelContainer Scene modifier
import SwiftUI
import SwiftData
@main
struct TripsApp: App {
var body: some Scene {
WindowGroup {
ContentView
}
.modelContainer(for: Trip.self)
}
}
데이터를 컨테이너에서 쿼리
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)
}
}
}
}
}
Cusomize the Schema
기존에 스키마 커스텀을 위해서 사용하던 매크로들이 있었다.
올해 추가된 매크로
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?
}
preserveValueOnDeletion
Tailor a container
modelContainer modifier를 그냥 쓰면 SwiftUI가 컨테이너를 자동으로 설정해준다.
@main
struct TripsApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer(for: Trip.self)
}
}
몇가지 커스텀도 가능하다.
// 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)
}
}
이 상으로 커스텀하기 위해서는 직접 Container 인스턴스를 만들어야 한다.
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)
}
}
아예 저장소를 커스텀하게 만들어 쓸 수도 있다.
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)
}
}
ex.프리뷰를 위한 인메모리 저장소
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())
}
import SwiftUI
import SwiftData
struct ContentView: View {
@Query
var trips: [Trip]
var body: some View {
...
}
}
#Preview(traits: .sampleData) {
ContentView()
}
프리뷰에서 바로 쿼리해서 데이터 꺼내기
import SwiftUI
import SwiftData
#Preview(traits: .sampleData) {
@Previewable @Query var trips: [Trip]
BucketListItemView(trip: trips.first)
}
Optimize queries
쿼리는 모델의 배열을 쉽게 정렬 및 필터링하고 뷰를 동작시킬 수 있게 해준다.
#Predicate를 통해서 데이터 쿼리 중에 필터링을 평가하여 메모리에 전체 데이터를 안올리고도 동작한다.
let predicate = #Predicate<Trip> {
searchText.isEmpty ? true : $0.name.localizedStandardContains(searchText)
}
17.4부터는 복합적인 판단식을 작성할 수 있다.
let predicate = #Predicate<Trip> {
searchText.isEmpty ? true :
$0.name.localizedStandardContains(searchText) ||
$0.destination.localizedStandardContains(searchText)
}
iOS18부터는 Expression 매크로를 통해서 복잡한 판단식을 쉽게 작성할 수 있다.
// 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
}
#Index 매크로
모델의 단일 혹은 복합 인덱스를 만든다.
책의 목차처럼 Index는 SwiftData가 만드는 추가 데이터를 의미하고 컨테이너에 저장된다.
지정된 keyPath에 대한 쿼리를 빠르고 효율적으로 만들어준다.
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
}