Appearance
SwiftData
CRUD
modelContainer 存储数据的默认 sqlite 文件路径
默认路径在 URL.applicationSupportDirectory.path(percentEncoded: false)
swift
import SwiftUI
import SwiftData
@main
struct SwiftUI_BookApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer(for: Book.self)
}
init() {
print(URL.applicationSupportDirectory.path(percentEncoded: false))
}
}输出:
/Users/luca/Library/Developer/CoreSimulator/Devices/B5AFEF8F-FA98-4FCD-8C43-10B51B3561B0/data/Containers/Data/Application/437D58C9-6F43-4E1E-A2C6-8BDFEDE4B064/Library/Application Support/- 实现:1.新增 2.查询 3.删除 4.更新
modelContainer
自定义 SQLite 数据库路径
在构造器里自定义 modelContainer,这里只是改了下 SQLite 的文件名
也可以使用 ModelConfiguration 的 url 参数来调整文件的存储路径
swift
import SwiftUI
import SwiftData
@main
struct SwiftUI_BookApp: App {
let modelContainer: ModelContainer
var body: some Scene {
WindowGroup {
BookListView()
}
.modelContainer(modelContainer)
}
init() {
let schema = Schema([Book.self])
let config = ModelConfiguration("MyBooks", schema: schema)
do {
modelContainer = try ModelContainer(for: schema, configurations: config)
} catch {
fatalError("Could not configure the container")
}
}
}配置 Preview 的 modelContainer
swift
import Foundation
import SwiftData
struct Preview {
let container: ModelContainer
init(_ models: any PersistentModel.Type...) {
let config = ModelConfiguration(isStoredInMemoryOnly: true)
let schema = Schema(models)
do {
container = try ModelContainer(for: schema, configurations: config)
} catch {
fatalError("Could not create preview container")
}
}
func addExamples(_ examples: [any PersistentModel]) {
Task { @MainActor in
examples.forEach { example in
container.mainContext.insert(example)
}
}
}
}Dynamic Sorts and Filters
排序和过滤的核心逻辑
swift
@Query(sort: \Book.status) private var books: [Book]
init(sortOrder: SortOrder, filterString: String) {
let sort = switch sortOrder {
case .status:
[SortDescriptor(\Book.status), SortDescriptor(\Book.title)]
case .title:
[SortDescriptor(\Book.title)]
case .author:
[SortDescriptor(\Book.author)]
}
let filter = #Predicate<Book> { book in
book.title.localizedStandardContains(filterString)
|| book.author.localizedStandardContains(filterString)
|| filterString.isEmpty
}
// 当你使用 @Query 属性包装器时,Swift 编译器会自动生成两个属性:
// _books - 访问 Query 包装器本身,可以重新初始化
// books - 只能访问查询结果数组,无法改变查询配置
_books = Query(filter: filter, sort: sort)
}Migration
- 新增字段: Must be optional or have default value
- 修改字段名称
swift
@Attribute(originalName: "summary")
var avatar: Data- 删除字段
其它 Migration
swift
// 图片、音频、视频等媒体文件
@Attribute(.externalStorage)
var avatar: Data
// 在 iCloud 同步时需要进行端到端加密
@Attribute(.allowsCloudEncryption)
var sin: String
@Attribute(.unique)
var title: String
@Attribute(.deleteRules: .cascade)
var quotes: [Quotes]?拓展:MVVM 改造
- @Observable 替代 Combine
- @Bindable:接收 + 延续绑定能力
- let:接收 + 只读(不需要绑定)
- @Bindable 和 @Binding 的区别
@Observable 什么时候需要 @MainActor?
- 有后台任务,需要更新 UI
swift
@Observable
class DataViewModel {
var data: [Item] = []
var isLoading = false
func fetchData() async {
isLoading = true
// 后台网络请求
let result = await networkService.fetch()
// ⚠️ 这里需要确保在主线程更新 UI
await MainActor.run {
self.data = result
self.isLoading = false
}
}
}或者更优雅的方式:
swift
@Observable
class DataViewModel {
var data: [Item] = []
var isLoading = false
@MainActor // ← 只标记这个函数
func updateUI(with result: [Item]) {
self.data = result
self.isLoading = false
}
func fetchData() async {
isLoading = true
let result = await networkService.fetch()
await updateUI(with: result)
}
}- 整个 ViewModel 都涉及复杂异步操作
swift
@MainActor // ← 整个类都在主线程
@Observable
class ComplexViewModel {
var data: [Item] = []
var status: String = ""
func fetchData() async {
// 复杂的异步操作
data = await service.fetch()
status = "完成" // 自动在主线程
}
func processData() async {
// 更多异步操作
data = await processor.process(data)
}
}actor 关键词
- 请求在访问 actor 的状态时是会加锁的,这样其它请求就无法访问这个状态了,从而保证线程安全
- Actor 保证 "永远不会有两个线程同时访问状态",但不保证 "await 前后状态不变"
- 片段级别的线程安全,而不是方法级别的线程安全
One to Many Relationships
- 一对多关系:Book -> Quote
swift
@Model
class Quote {
@Relationship(deleteRule: .cascade)
var book: Book?
}
@Model
class Book {
var quotes: [Quote]? // 需要设置为 ?,否则后续集成 CloudKit 会有问题
}- 删除级联:
@Relationship(deleteRule: .cascade)
Many to Many Relationships
- 多对多关系:Book <-> Genre
swift
@Model
class Genre {
var books: [Book]?
}
@Model
class Book {
@Relationship(inverse: \Genre.books)
var genres: [Genre]?
}- 做了啥?
- 创建 GenresView
- 创建 NewGenreView 并通过 sheet 集成到 GenresView 里
- 在 EditBookView 通过 sheet 集成到 GenresView 里
- 创建 GenresStackView
- 使用 ViewThatFits 和 ScrollView 解决 Genre 移除