- Published on
๐ WWDC23 - SwiftData
- Authors
- Name
- ์ด์ฐฝ์ค
WWDC23 - SwiftData
์ค๋์ SwiftData๋ฅผ ์์๋ณผ๊ฑฐ์์ Widget์๋ค ์จ๋จน์ด์ผ๋๊ฑฐ๋ ์ ๐
์ ๋ ์ผ๋จ CoreData๋ฅผ ์จ๋ณธ ์ ์ด ์์ต๋๋ค. ์๋ง ์กฐ๋ง๊ฐ ์ธ ๊ฒ ๊ฐ๊ธฐ๋ ํ๋ฐ.. ์ผ๋จ์ ์์ด์.. ์ ์์ผ๋๋ฉด์.. ์ฌ์ฉ ๋ด๋ ํ์ผ์ด ๋๋ฌด ์ฌ๊ธฐ์ ๊ธฐ ๋ถ์ฐ๋๊ฒ ๋๋ ๊ฒ ๊ฐ๋๋ผ๊ตฌ์.. ์ ์ฉ์ ํ์ผ ํ์๋ ์๊ณ ๋ง์ด์ฃ โฆ? ๊ทธ๋์ ๋์ Realm์ ์์ฃผ ์ผ์์ต๋๋ค.
๊ทธ๋ฐ๋ฐ.. SwiftData๊ฐ ์๋ก ๋์์ด์! iOS 17๋ถํฐ ์ง์ํด์ ์ค๋ฌด์์๋ 4~5๋ ๋ค์๋ ์ฐ๊ฒ ๋ ๊ฒ ๊ฐ์ง๋ง ๋์จ๊ฒ ์ด๋๊ฐ์ ๐ ์ผ๋จ Schema๋ฅผ ์ ์ํด์ฃผ๋ ๊ณผ์ ๋ถํฐ ๋๋ฌด ๊ฐ๋จํฉ๋๋ค.
์ถ๋ฐํด๋ณด์ฃ !
Using the Model Macro
SwiftData์ Schema ๋ ์ผ๋ฐ ํด๋์ค์ @Model
๋งคํฌ๋ก ๋ฅผ ๋ถ์ฌ์ฃผ๋ ๊ฒ์ผ๋ก ๋์
๋๋ค.
import SwiftData
@Model
class Trip {
var name: String
var destination: String
var endDate: Date
var startDate: Date
var bucketList: [BucketListItem]? = []
var livingAccommodation: LivingAccommodation?
}
๋๋ฌด ์ฝ๊ณ ๊ฐ๋จํ์ฃ ..?
@Model
๋งคํฌ๋ก๋ ํด๋์ค ๋ด๋ถ์ Stored Property ๋ค์ Persisted Property ๋ก ๋ฐ๊ฟ์ค๋๋ค.
์ฌ์ฉํ ์ ์๋ ํ์ ๋ค์ ์ด๋ ์ต๋๋ค.
- ๊ธฐ๋ณธ์ ์ธ Value Types (
Int
,String
, โฆ) struct
enum
Codable
- [ANY VALUE TYPE]
๋ค๋ฅธ @Model
ํด๋์ค๋ฅผ ์ฐธ์กฐํ๋ ๊ฒ์ผ๋ก ๊ด๊ณ ๊ตฌ์กฐ๋ฅผ ํ์ฑํ ์๋ ์์ต๋๋ค.
[BucketListItem]?
์ ๊ฐ์..
SwiftData ํด๋์ค์ ์ค์ ์ฌ์ฉ๋๋ ํด๋์ค๋ ๋์ผํ๊ฐ์?
๊ฐ์ธ์ ์ผ๋ก ๊ฐ์ฅ ๊ถ๊ธํ ์ ์ด์์ต๋๋ค.
public protocol Storable {
associatedtype RealmObject: RealmSwift.Object
associatedtype PropertyValue: PropertyValueType
/// ๋ฐ์ดํฐ ๋ชจ๋ธ์ ์๋ณ์๋ก ์ฌ์ฉ๋๋ ํ๋กํผํฐ
var identifier: Int { get }
init(realmObject: RealmObject)
func realmObject() -> RealmObject
}
Realm์ ์ฌ์ฉํ๋ค๋ณด๋ DB์์ ์ฌ์ฉํ๋ Data ๊ตฌ์กฐ์ฒด์ ์ค์ ๋ก ์ฌ์ฉ๋๋ ๋๋ฉ์ธ ํํ์ ๊ตฌ์กฐ์ฒด๋ ๋ค๋ฅธ ๊ตฌ์กฐ์ฒด๋ฅผ ์ฌ์ฉํ๊ณค ํ๊ฑฐ๋ ์.. (๋ณดํต ๋ฉํฐ ์ฐ๋ ๋ฉ์ ๊ณผ์ ์์ ๋ฌธ์ ๋ค์ด ๋ฐ์ํ์ต๋๋ค.)
ํ์ง๋ง SwiftData๋ ์ ํฌ๊ฐ ์๋ ์ฌ์ฉํ๋ ์ฝ๋์ ๋ณ ์ฐจ์ด๊ฐ ์๋๋ก ์ ํ์ด ๋ ธ๋ ฅํ๊ธฐ ๋๋ฌธ์ Model ํด๋์ค๋ฅผ ์ง์ ์ ์ผ๋ก UI ๋ ์ด์ด ๋ฑ์์ ์ฌ์ฉํ ์ ์๋ค ๊ณ ํฉ๋๋ค.
@Attribute
์ @Relationship
์ผ๋ก ํ๋กํผํฐ์ ์ถ๊ฐ์ ์ธ ์ ์ด๋ฅผ ํ ์ ์์ต๋๋ค.
@Attribute
@Attribute(.unique) var name: String
@Attribute(.unique)
๋ก PK๋ฅผ ์ค์ ํด์ค ์ ์์ต๋๋ค. PK๋ก ์ค์ ๋ ํ๋กํผํฐ์ ๊ฐ์ ๊ฐ์ ๊ฐ์ง ๋ชจ๋ธ์ด Create์ ๋์๋๋ insert()
๋ก ์ธํด ์ถ๊ฐ๋ ๋ Update์ ๋์๋๋ upsert()
๋ก ์ ํ๋๋ ํจ๊ณผ๋ฅผ ๊ฐ์ง๋๋ค.
@Attribute(originalName: "start_date") var startDate: Date
@Attribute(originalName)
์ผ๋ก๋ Migration์ด ๊ฐ๋ฅํฉ๋๋ค. ํ๋กํผํฐ์ ์ด๋ฆ์ ๋ฐ๊พธ๊ณ ์ถ์ง๋ง ๋ฐ์ดํฐ๋ ๊ทธ๋๋ก ๋จ๊ฒจ๋๊ณ ์ถ์ ๋ ์ฌ์ฉํ ์ ์์ต๋๋ค.
๊ทธ ์ธ์๋
- ์ธ๋ถ ๋์ฉ๋ ๋ฐ์ดํฐ ์ฌ์ฉ
- Transformable
- Spotlight
- Hash ์์
๋ฑ์ ์์ ์ด ๊ฐ๋ฅํ๋ค๊ณ ํ๋ค์.
@Relationship
๊ฐ ๋ชจ๋ธ๊ฐ Relationship์ด ์ค์ ๋์ด ์์ ๋, ์ญ์ ์ ์ฑ ์ ๊ธฐ๋ณธ๊ฐ์ Nullify ์ ๋๋ค.
์์๋ก ์ดํด๋ณด๋ฉด Trip
์ด ์ญ์ ๋์๋, BucketListItem
๊ณผ LivingAccommodation
์ Inverse Relationship์ผ๋ก ๋ฑ๋ก๋์ด ์๋ trip
์ด๋ผ๋ ํ๋กํผํฐ๊ฐ NULL์ด ๋๋ ๊ฒ์ด์ฃ .
ํ์ง๋ง .cascade
์ ์ฑ
์ผ๋ก Nullify ๋์ ํจ๊ป ์ญ์ ๋๋๋ก ํ ์๋ ์์ต๋๋ค.
@Relationship(.cascade) var bucketList: [BucketListItem]? = []
- originalName
- toMany Relationship์ผ ๋์ ๊ฐ์ ์ ํ
- Hash ์์
@Transient
@Transient var tripViews: Int = 0
@Transient
๋งคํฌ๋ก๋ฅผ ์ฌ์ฉํ๋ฉด ํด๋น ํ๋กํผํฐ๊ฐ SwiftData๋ก ์ฐ์ด๋ ๊ฒ์ ๋ง์ ์๋ ์์ต๋๋ค.
๋์ ํด๋น ํ๋กํผํฐ๋ ๊ธฐ๋ณธ๊ฐ ์ ๊ผญ ๊ฐ์ ธ์ผ๋ง ํฉ๋๋ค.
Evolving Schemas
์ฑ์ ๊ฐ๋ฐํ๊ฑฐ๋ ์์ฉํ๋ฅผ ํ ์ดํ์๋ DB์ ๊ตฌ์กฐ๋ ์ธ์ ๋ ์ง ๋ฐ๋ ์ ์์ต๋๋ค.
๋๋ฌธ์ Migration์ ํ์์ ์ธ ์๊ฑด์ด์ฃ .
SwiftData๋ VersionedSchema
์ SchemaMigrationPlan
์ ํตํด ์ด๋ฅผ ์ง์ํฉ๋๋ค.
enum SampleTripSchemaV1: VersionedSchema {
static var models: [any PersistentModel.Type] {
[Trip.self, BucketListItem.self, LivingAccommodation.self]
}
@Model
final class Trip {
var name: String
var destination: String
var start_date: Date
var end_date: Date
var bucketList: [BucketListItem]? = []
var livingAccommodation: LivingAccommodation?
}
enum SampleTripSchemaV2: VersionedSchema {
static var models: [any PersistentModel.Type] {
[Trip.self, BucketListItem.self, LivingAccommodation.self]
}
@Model
final class Trip {
@Attribute(.unique) var name: String
var destination: String
@Attribute(originalName: "srart_date") var startDate: Date
@Attribute(originalName: "end_date") var endDate: Date
var bucketList: [BucketListItem]? = []
var livingAccommodation: LivingAccommodation?
}
SchemaMigrationPlan
์ผ๋ก๋ ์ด๋ ๊ฒ ๋ฐ๊พผ ๋ฐ์ดํฐ๋ค์ ์ด๋ค ์์๋๋ก ๋ณ๊ฒฝํ ์ง ๋ฑ์ ์ธ๋ถ์ ์ธ ์ฌํญ๋ค์ ์ ํ ์ ์์ต๋๋ค.
enum SampleTripsMigrationPlan: SchemaMigrationPlan {
static var schemas: [any VersionedSchema.Type) {
[SampleTripsSchemaV1.self, SampletripsSchemaV2.self, SampleTripSchemaV3.self]
static var stages: [MigrationStage] {
[migrateV1toV2]
}
static let migrateV1toV2 = MigrationStage.custom(
fromVersion: SampleTripSchemaV1.self
toVersion: SampleTripSchema2.self ,
willMigrate: { context in
let trips = try? context.fetch(FetchDescriptor<SampleTripsSchemaV1.Trip>())
try? context.save()
}, didMigrate: nil
)
}
Working with your Data
Model Container
Schema ๋ ๋จ์ํ โ์ด๋ค ํํ๋ก ์ ์ฅ๋๋ ์ง์ ๋ํ ๊ฐ์ด๋๋ผ์ธโ ์ ๋์ ์ญํ ์ด๊ธฐ ๋๋ฌธ์ ์ค์ ๋ก ๋ฐ์ดํฐ๋ฅผ ์ ์ฅํ๋ ๋ฌด์ธ๊ฐ๋ ์์ด์ผ๊ฒ ์ฃ ?
๊ทธ๊ฒ์ด ๋ฐ๋ก Model Container ์ ๋๋ค.
Model Container๋ Schema๋ฅผ ์ฌ์ฉํ์ฌ ์ค์ ๋ก ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ฅผ ๊ตฌ์ถํฉ๋๋ค.
๊ทธ๋ฆฌ๊ณ ์ด๋ ๊ฒ ์ธ์คํด์คํ ๋ ๊ฐ ๋ฐ์ดํฐ๋ค(Persistence)์ Model Context ๋ฅผ ํตํด์ ์ ๊ทผ์ด ๊ฐ๋ฅํ์ฃ .
๊ทธ๋ฌ๋๊น Model Container ๋ Schema์ Persistence ์ฌ์ด์ ์ฐ๊ฒฐ๋ชฉ ์ญํ ์ ํ๋๊ฒ๋๋ค.
Model Container๋ ์ด๋ฐ ์์ ๋ค์ ํฉ๋๋ค.
- ๋ฒ์ ๊ด๋ฆฌ
- Migration
- Graph ๋ถ๋ฆฌ
Container ์ธ์คํด์ค๋ฅผ ์์ฑํ๋ ๊ฒ์ ๊ฐ๋จํฉ๋๋ค.
import SwiftData
let container = try ModelContainer(for: [Trip.self,
BucketListItem.self,
LiveAccommodation.self])
๊ทธ๋ฆฌ๊ณ ๋ ๋ณต์กํ ํํ์ DB๋ฅผ ๊ตฌ์ถํ ์ ์๋๋ก ์ถ๊ฐ์ ์ธ Configuration
๋ ์ ๊ณตํฉ๋๋ค.
import SwiftData
let container = try ModelContainer(for: [Trip.self, LivingAccommodation.self],
configuration: ModelConfiguration(url: URL("path")))
Model Configuration
let fullSchema = Schema([Trip.self,
BucketListItem.self,
LivingAccommodations.self,
Person.self,
Address.self])
let trips = ModelConfiguration(schema: Schema([Trip.self,
BucketListItem.self,
LivingAccommodations.self]),
url: URL(filePath: "/path/to/trip.store"),
cloudKitContainerIdentifier: "com.example.trips")
let people = ModelConfiguration(schema: Schema([Person.self,
Address.self]),
url: URL(filePath: "/path/to/people.store"),
cloudKitContainerIdentifier: "com.example.people")
let container = try ModelContainer(for: fullSchema, trips, people)
ModelConfiguration ์ ํตํด์๋
- Schema์ Persistence๋ฅผ ์ค๋ช ํ๊ฑฐ๋ (๋ฉ๋ชจ๋ฆฌ ํน์ ๋์คํฌ)
- ์ ์ฅ๋๋ ์์น๋ฅผ ์ง์ ํ๊ฑฐ๋
- Read-Only ๋ชจ๋๋ ์ง์ํ๊ณ
- CloudKit์ ์ฌ์ฉํ๋ ๊ฒฝ์ฐ identifier๋ฅผ ์ ๊ณตํฉ๋๋ค.
์ ์์์ ๊ฒฝ์ฐ trips
์ people
์ ๊ทธ๋ํ๋ฅผ ์ ์ฅ๋๋ ์์น์ CloudKit์ identifier๋ฅผ ๋ถ๋ฆฌํด์ ๋ค๋ฅด๊ฒ ์ค์ ํด์ฃผ๊ณ ์๋ค์.
์ดํ, ModelContainer ๋ฅผ ์์ฑํ ๋ ๋ง๋ค์ด๋ Schema์ Graph๋ค์ ํ๋๋ก ๋ฌถ์ด ์์ฑํ๊ณ ๊ด๋ฆฌํ ์ ์์ต๋๋ค.
Model Context
Model Context ๋ Model์ ์๊ธฐ๋ ๋ณํ๋ฅผ ๊ด์ฐฐํ๋ค๊ฐ ํธ๋ฆฌ๊ฑฐ๋๋ ํ๋๋ค์ ์ํํฉ๋๋ค.
1. ์ ๋ฐ์ดํธ๋ฅผ ์ถ์ ํ๊ณ , 2. ๋ฐ์ดํฐ๋ฅผ fetchํ๊ณ , 3. ๋ณ๊ฒฝ์ ์ ์ ์ฅํ๊ณ , 4. ๋ณ๊ฒฝ์ ์ ์ทจ์ํ๋ ๋ฑ์ ์์ ์ด ๊ฐ๋ฅํฉ๋๋ค.
import SwiftData
let context = container.mainContext
let context = ModelContext(container)
์ด๊ฒ์ด ์ด๋ป๊ฒ ๊ฐ๋ฅํ๋!? ํ๋ฉด, ModelContext๋ ์ผ์ข ์ Snapshot ์ฒ๋ผ ์๋ํฉ๋๋ค.
๋ณํ๊ฐ ์ผ์ด๋ ๋๋ง๋ค Model์ Snapshot์ ์ฐ์ด ๊ทธ ๋์ ์ํ๋ฅผ ์ ์ฅํด๋๋ ๊ฒ์ด์ฃ .
๋๋ถ์ ์ญ์ , ์์ ๊ณผ ๊ฐ์ ๋ณํ๊ฐ ์ด๋ฃจ์ด์ ธ๋ ์ด๋ค ์ธ์คํด์ค๊ฐ ์ญ์ ๋๊ณ , ์์ ๋์๋์ง์ ๋ํ ์ ๋ณด ๋ฅผ ๋ด๊ณ ์์ ์ ์์ต๋๋ค.
๊ทธ๋์ ์ฃผ์ํ ์ ์ ModelContext์ ๋ณ๊ฒฝ์ ๋ค์ context.save()
๋ฅผ ํตํด ์ ์ฅํ์ง ์์ผ๋ฉด ์ ์ฉ๋์ง ์์ต๋๋ค.
Undo with ModelContext
Model Container ์๋ ๊ธฐ๋ณธ์ ์ผ๋ก undoManager
๊ฐ ํฌํจ๋์ด ์์ต๋๋ค.
์ธ ์๊ฐ๋ฝ์ผ๋ก ์ค์์ดํ, ํ๋ค์ด์ ์ ๋ ฅ ์ทจ์ ๋ฑ ์์คํ ์ด ๊ธฐ๋ณธ์ ์ผ๋ก ์ ๊ณตํ๋ ์ ์ค์ฒ๋ค์ ์๋์ผ๋ก ์ง์ํ๋ค๋ ๊ทธ๋ฐ ์๊ธฐ์ ๋๋ค..
ModelContext AutoSave
.modelContainer(for: Trip.self, isAutosaveEnabled: false)
Undo๋ฟ๋ง ์๋๋ผ ์๋ ์ ์ฅ๋ ์ง์ํ๊ณ ์์ต๋๋ค.
์ฑ์ด ๋ฐฑ๊ทธ๋ผ์ด๋ ์ํ๋ก ๊ฐ๊ฑฐ๋ ์ข ๋ฃ๋ ๋, ํน์ ์ฑ์ด ์ฌ์ฉ๋จ์ ๋ฐ๋ผ ์๋์ ์ผ๋ก Context๋ฅผ ์ ์ฅํ๋ ๊ธฐ๋ฅ์ ๋๋ค.
Fetchํ ๊ฐ๋ค์ ๊ธฐ์กด์ ์๋ SortDescriptor
์ ์๋ก์ด Predicate
, FetchDescriptor
๋ฅผ ์ฌ์ฉํด ์ ๋ ฌํ๊ฑฐ๋ ํํฐํ ์ ์์ต๋๋ค.
Predicate
let tripPredicate = #Predicate<Trip> { $0.destination == "New York" }
let tripPredicate = #Predicate<Trip> {
$0.destination == "New York" &&
$0.name.contains("birthday")
}
#Predicate
๋งคํฌ๋ก๋ฅผ ์ฌ์ฉํด ์ํํ ์ ์์ผ๋ฉฐ ๊ธฐ์กด์ filter
์ ๋น์ทํ ์ญํ ์ ์ํํฉ๋๋ค.
๊ธฐ์กด์ filter
๋ .filter { }.filter { }
์ ๊ฐ์ด ์ค์ค์ด ์กฐ๊ฑด์ ์ถ๊ฐํด์ค์ผ ํ๋ ๋ฐ๋ฉด, ํ๋์ ํด๋ก์ ๋ด๋ถ์์ ๋ชจ๋ ์กฐ๊ฑด๋ค์ ๊ฒ์ฌ ํ ์ ์๋ ์ ์ด ํน์ง์
๋๋ค.
Fetch Descriptor
let descriptor = FetchDescriptor<Trip>(predicate: tripPredicate)
let trips = try context.fetch(descriptor)
FetchDescriptor
๋ ์์์ ์ดํด๋ณธ Predicate
๋ฅผ context
์ ์ ์ฉ์์ผ์ฃผ๋ ์ญํ ์ ํฉ๋๋ค.
let descriptor = FetchDescriptor<Trip>(
sortBy: SortDescriptor(\Trip.name),
predicate: tripPredicate
)
let trips = try context.fetch(descriptor)
์ด๋ ๊ฒ SortDescriptor
์ ์กฐํฉํด์ ์ฌ์ฉํ ์๋ ์์ต๋๋ค.
context.enumerate(descriptor
batchSize: 10000
allowEscapingMutations: true) { trip in
// operate on trip
}
์ด์ธ์๋ enumerate๋ผ๋ ํจ์๋ ์์ต๋๋ค.
batchSize๋ ํ ๋ฒ์ 5000๊ฐ์ ์ค๋ธ์ ํธ๋ค์ ์ฒ๋ฆฌํ๋๋ก ์ ํ๋์ด ์์ง๋ง, ์ด๋ฅผ 10000๊ฐ๋ก ๋๋ฆฌ๋ฉด ๋ฉ๋ชจ๋ฆฌ์ ํ ๋น๋์ ๋๋ฆฌ๊ณ , I/O ์๊ฐ์ ์ค์ผ ์ ์์ต๋๋ค. (๋ฉ๋ชจ๋ฆฌ ์ฐ์ฐ์ ๋์คํฌ ์ฒ๋ฆฌ๋ณด๋ค ์ฐ์ ์)
๋ฐ๋ฉด ์ฌ์ง, ์ํ์ ๊ฐ์ ๋ฐ์ดํฐ๋ค์ batchSize๋ฅผ ์ค์ด๋ ๊ฒ์ด ์ข์ต๋๋ค. ๋ฉ๋ชจ๋ฆฌ ์ฌ์ฉ๋์ ์ค์ด๋ฉด์ I/O ์ฒ๋ฆฌ๋์ ๋๋ฆด ์ ์๊ธฐ ๋๋ฌธ์ด์ฃ . (๋์คํฌ ์ฒ๋ฆฌ๋ฅผ ๋ฉ๋ชจ๋ฆฌ ์ฐ์ฐ๋ณด๋ค ์ฐ์ ์)
Modifying your Data
SwiftData๋ CRUD ์์ ๋ ๋๋ฌด ๊ฐ๋จํฉ๋๋ค!
var myTrip = Trip(name: "Birthday Trip", destination: "New York")
// Create
context.insert(myTrip)
// Delete
context.delete(myTrip)
try context.save()
References