SwiftData는 WWDC2023에서 공개된 새로운 데이터 관리 프레임워크이다.
Core Data와 비슷한 개념으로, Swift를 사용해 더 쉽게 데이터를 관리할 수 있고, 무엇보다 SwiftUI에 최적화되어있다.
SwiftData는 iOS 17 이상, macOS 14 이상, watchOS 10 이상에서 사용이 가능하다.
당장 SwiftData의 에러 메시지 또한 Core Data로 레이블되어 있는 것만 보아도, SwiftData는 사실 Core Data의 많은 부분을 공유하고 있다.
하지만 SwiftData는 Swift의 현대적인 concurrency, macros 등과 같은 기능들로 쉽고 빠르게, 적은 코드로 목적을 달성할 수 있다.
SwiftData를 사용하기 위해서는 Xcode 15 버전 이상을 사용해야 한다.
만약 Xcode 15 버전 이상을 사용중이라면, 새 프로젝트 생성 창의 Storage 선택 항목에 SwiftData를 발견할 수 있다.
SwiftData를 기본으로 생성하면, Xcode가 친절하게 예제용 Item
모델과 이를 테스트할 수 있는 View를 그려준다.
이 대로 생성해서 사용해도 좋지만, 나는 none으로 생성하여 처음부터 작성해보려 한다.
Core Data는 Data Model 파일에 모델을 작성했던 것과는 달리, SwiftData는 Swift의 새 기능, macro를 활용해 모델을 정의한다.
SwiftData의 모델을 선언하려면 @Model
매크로를 사용하면 된다.
간단한 메모 앱을 만들기 위해 Memo
모델을 만들어 보자.
import Foundation
import SwiftData
@Model
class Memo {
var content: String
var timestamp: Date
init(content: String, timestamp: Date) {
self.content = content
self.timestamp = timestamp
}
}
Memo
모델은 메모를 작성할 수 있는 memo 프로퍼티와 작성 시간을 저장하는 timestamp 프로퍼티로 구성했다.
View Model을 만드는 것 처럼 클래스를 작성하면 굉장히 간단하게 스키마를 만들 수 있다.
@Attribute
매크로를 사용하여 모델의 프로퍼티에 속성을 부여하는 것도 가능하다.
macro Attribute(
_ options: Schema.Attribute.Option...,
originalName: String? = nil,
hashModifier: String? = nil
)
@Attribute
매크로의 구조이다.
이 @Attribute
매크로의 옵션에 사용할 수 있는 것에는 다음과 같은 것들이 있다.
예를 들어, 만약 위에서 작성한 Memo
모델이 완전히 유일한 값인 id
를 갖게 한다고 하면, 다음과 같이 코드를 작성할 수 있다.
import Foundation
import SwiftData
@Model
class Memo {
@Attirbute(.unique) var id = UUID()
var content: String
var timestamp: Date
init(content: String, timestamp: Date) {
self.content = content
self.timestamp = timestamp
}
}
각 Model
들은 서로에 대해 Relationship을 설정할 수 있다.
Relationship은 @Relationship
매크로를 통해 설정할 수 있는데, 예시를 위해 Memo
에 관계를 설정할 Tag
를 만들어 둘의 관계를 설정해보도록 하겠다.
우선 Tag
모델을 하나 만들어준다.
import Foundation
import SwiftData
@Model
final class Tag {
var tag: String
init(tag: String) {
self.tag = tag
}
}
Tag
모델은 Memo
에 태그를 붙일 수 있도록 tag: String
프로퍼티를 하나 추가해주었다.
이 Tag
를 Memo
에 Relationship으로 설정하려면, Memo
에 Tag
프로퍼티를 Optional로 선언하고, @Relationship
매크로를 붙여주면 된다.
import Foundation
import SwiftData
@Model
final class Memo {
@Attribute(.unique) var id = UUID()
var content: String
var timestamp: Date
// ↓ 이 부분
@Relationship var tag: Tag?
init(content: String, timestamp: Date) {
self.content = content
self.timestamp = timestamp
}
}
그런데 보통 메모 앱들은 한 메모에 대해 여러개의 태그를 갖는 경우가 많기 때문에, 우리도 한 Memo
가 여러개의 Tag
를 갖도록 하면 좋을 것 같다.
만약 1:n 관계를 설정하려면, 단순히 프로퍼티를 배열로 선언해주면 된다.
import Foundation
import SwiftData
@Model
final class Memo {
@Attribute(.unique) var id = UUID()
var content: String
var timestamp: Date
// ↓ 이 부분
@Relationship var tags: [Tag]?
init(content: String, timestamp: Date) {
self.content = content
self.timestamp = timestamp
}
}
이렇게 관계를 설정해 주었을 때, Memo
에 관계된 Tag
는 다음과 같이 접근할 수 있다.
memo.tags // Optional<[Tag]>
Memo
에서 Tag
를 참조하는 경우도 있지만, Tag
에서 Memo
를 참조하는 경우도 있지 않을까?
다행히 SwiftData의 @Relationship
매크로는 이를 위해 inverse 옵션을 제공한다.
inverse 옵션을 설정하려면, 우선 inverse를 설정할 모델에 다음과 같이 inverse할 모델을 프로퍼티로 설정해준다.
우리의 경우엔 Tag
모델에 Memo
를 프로퍼티로 설정해주면 된다.
import Foundation
import SwiftData
@Model
final class Tag {
var tag: String
var memo: Memo? // 이 부분
init(tag: String) {
self.tag = tag
}
}
다음으로, @Relationship
매크로에 inverse 옵션을 해당 프로퍼티의 Keypath로 넣어주면 inverse 옵션 설정이 완료된다.
import Foundation
import SwiftData
@Model
final class Memo {
@Attribute(.unique) var id = UUID()
var content: String
var timestamp: Date
// ↓ 이 부분
@Relationship(inverse: \Tag.memo) var tags: [Tag]?
init(content: String, timestamp: Date) {
self.content = content
self.timestamp = timestamp
}
}
이제 만든 모델을 스키마로 사용하기 위해 ModelContainer로 설정해주어야 한다.
SwiftData는 ModelContainer를 쉽고 편하게 설정하기 위한 .modelContainer
modifier를 제공하는데, 이를 사용해 ModelContainer를 설정할 수 있다.
ModelContainer는 일반적으로 앱의 엔트리포인트 App
구조체 안에 작성해준다.
import SwiftUI
import SwiftData
@main
struct SwiftDataTestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
여기에 프로퍼티로 ModelContainer를 만들어준 뒤, ContentView
에 .modelContainer
modifier로 등록해주면 된다.
import SwiftUI
import SwiftData
@main
struct SwiftDataTestApp: App {
var modelContainer: ModelContainer = {
let schema = Schema([Memo.self, Tag.self])
let modelConfiguration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: false)
do {
return try ModelContainer(for: schema, configurations: [modelConfiguration])
} catch {
fatalError("Could not create ModelContainer: \(error)")
}
}()
var body: some Scene {
WindowGroup {
ContentView()
.modelContainer(modelContainer)
}
}
}
전체 코드는 이렇게 된다.
modelContainer
부분을 조금 더 들여다보자.
var modelContainer: ModelContainer = {
let schema = Schema([Memo.self, Tag.self])
let modelConfiguration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: false)
do {
return try ModelContainer(for: schema, configurations: [modelConfiguration])
} catch {
fatalError("Could not create ModelContainer: \(error)")
}
}()
ModelContainer는 주어진 스키마에서 데이터베이스를 생성하고, 디스크의 읽기 및 쓰기와 iCloud 동기화를 담당한다.
ModelContainer를 생성하려면 우선 사용할 모델들을 스키마로 만들어준다.
let schema = Schema([Memo.self, Tag.self])
다음으로 ModelConfiguration을 생성해 모델 관리 규칙을 설정해준다.
let modelConfiguration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: false)
ModelConfiguration의 옵션으로는 여러 가지가 있는데, 프리뷰 등에서 데이터를 메모리 상에서만 관리할지 여부를 결정하는 inStoredInMemoryOnly, CloudKit을 사용할 때 데이터베이스를 설정하는 cloudKitbataBase 등이 있다.
여담으로, cloudKitDatabase를.none
으로 설정하더라도 SwiftData 버그로 인해 프로젝트에 CloudKit을 활성화하면 자동으로 iCloud에 동기화를 시도한다.
How to stop SwiftData syncing with CloudKit
let config = ModelConfiguration(cloudKitDatabase: .none)
혹시 앱이 SwiftData를 사용하는데, iCloud Database에 맞게 모델을 최적화하지 않았다면, iCloud capability를 활성화하지 않는 것을 추천한다..
마지막으로 이렇게 만든 Schema와 ModelConfiguration을 사용해 ModelContainer를 만들어준다.
do {
return try ModelContainer(for: schema, configurations: [modelConfiguration])
} catch {
fatalError("Could not create ModelContainer: \(error)")
}
이렇게 만든 modelContainer
를 ContentView
에 연결하면 View에서 Environment로 ModelContext를 받아와 사용할 수 있다.
var body: some Scene {
WindowGroup {
ContentView()
.modelContainer(modelContainer) // <--- 이 부분
}
}
import SwiftUI
struct ContentView: View {
@Environment(\.modelContext) var modelContext // <--- 이 부분
var body: some View {
Text("Hello, world!")
}
}
#Preview {
ContentView()
}
SwiftData는 SwiftUI에서 저장된 데이터를 쉽게 쿼리할 수 있도록 @Query
매크로를 제공한다.
@Query var memos: [Memo]
또한 Predicate과 SortDescriptor을 사용해 데이터를 필터링하거나 보여줄 순서를 변경할 수도 있다.
@Query(
filter: #Predicate<Memo>({ $0.content.contains("swift") }),
sort: [SortDescriptor(\.timestamp, order: .forward)]
)
var memos: [Memo]
만약 코드상으로 데이터를 읽어야 한다면, FetchDescriptor를 사용해 데이터를 읽어올 수도 있다.
let predicate = #Predicate<Memo> { $0.content.contains("swift") }
let sort = [SortDescriptor<Memo>(\.timestamp, order: .forward)]
let desciptor = FetchDescriptor<Memo>(predicate: predicate, sortBy: sort)
let memos = try modelContext.fetch(desciptor)
데이터의 삽입 또한 modelContext.insert(_ model:)
을 사용해 쉽게 할 수 있다.
먼저 삽입할 데이터를 생성해주고, modelContext
에 insert()
해주면 끝이다.
let memo = Memo(content: "Memo Content", timestamp: Date())
modelContext.insert(memo)
앞서 Memo
모델과 관계를 가지는 Tag
모델을 만들었는데, Memo
에 Tag
모델을 넣을 때에는 우선 Tag
를 생성해주고, 이를 Memo
에 넣어주면 된다.
var memo = Memo(content: "Memo Content", timestamp: Date())
memo.tags = [Tag(tag: "test")]
modelContext.insert(memo)
데이터를 수정해야 할 때에는 별 다른 작업 없이 Model을 직접 수정해준다.
var memo = Memo(content: "Memo Content", timestamp: Date())
modelContext.insert(memo)
...
memo.content = "Edited Content" // 데이터 수정
클래스에 @Model
매크로를 사용하게 되면 해당 클래스는 자동으로 Observable 프로토콜을 따르게 된다.
그래서 모델 클래스는 변경 사항을 자동으로 추적하고, 이를 SwiftUI View에 자동으로 업데이트 해준다고 한다.
데이터를 삭제할 때에는 ModelContext
에 삭제할 모델에 대하여 .delete(_ model:)
함수를 사용해주면 된다.
var memo = Memo(content: "Memo Content", timestamp: Date())
modelContext.insert(memo)
...
modelContext.delete(memo)
많은 도움이 되었어요 감사합니다~ ^-^