WWDC Quick Look 💓 By SwiftGGTeam
What's new in SwiftData

What's new in SwiftData

观看原视频

Highlight

SwiftData 新增 #Unique 宏支持复合唯一约束,新增 #Index 宏加速查询性能,并支持自定义数据存储实现。

核心内容

SwiftData 去年随 iOS 17 发布,旨在用 Swift 的现代语言特性(如宏)简化数据持久化。开发者只需用 @Model 装饰类,就能自动获得持久化能力,再通过 @Query 在 SwiftUI 视图中获取数据。

今年解决了一个实际问题:数据重复。当用户同步数据、批量导入或网络重试时,容易产生重复的模型实例。以前开发者需要手动去重,或依赖某个单一唯一标识符。但很多业务场景下,唯一性是多个字段的组合——比如旅行应用中,同名行程只要时间不同就应该允许存在,只有”名称+开始日期+结束日期”三者都相同时才算真正重复。

SwiftData 现在引入了 #Unique 宏,可以声明多个属性的复合唯一约束。当插入的模型与现有模型在这些唯一字段上冲突时,SwiftData 会自动执行 upsert(更新而非插入),从根本上避免重复。

另一个性能相关的问题是查询效率。当数据量增大时,对特定字段频繁排序和过滤会变慢。#Index 宏允许开发者声明数据库索引,类似书籍的目录,能显著加速这些操作。此外,SwiftData 现在支持完全自定义的数据存储,不再局限于 SQLite 底层,可以用 JSON、其他格式甚至完全自定义的持久化方案。

详细内容

复合唯一约束

#Unique 宏接受一组 KeyPath,声明这些属性组合必须唯一(03:08):

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?
}
  • #Unique<Trip> 声明 Trip 模型的唯一约束
  • \.name, \.startDate, \.endDate 三个属性组合必须唯一
  • 当插入的模型与现有模型在这三个字段上相同时,SwiftData 会更新现有模型而非插入新模型

这些唯一字段也代表了模型的身份。为了让 SwiftData 的 History API 在模型被删除后仍能识别这些身份,可以添加 @Attribute(.preserveValueOnDeletion)03:36):

@Model 
class Trip {
    #Unique<Trip>([\.name, \.startDate, \.endDate])
    
    @Attribute(.preserveValueOnDeletion)
    var name: String
    var destination: String

    @Attribute(.preserveValueOnDeletion)
    var startDate: Date

    @Attribute(.preserveValueOnDeletion)
    var endDate: Date
    
    var bucketList: [BucketListItem] = [BucketListItem]()
    var livingAccommodation: LivingAccommodation?
}
  • @Attribute(.preserveValueOnDeletion) 确保这些字段的值在模型删除后仍保留在历史记录中
  • History API 可以跟踪模型的插入、更新、删除,这些保留的值作为”墓碑”信息帮助处理变更

自定义数据存储

iOS 18 允许完全自定义数据存储(05:59):

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)
   }
}
  • JSONStoreConfiguration 替代默认的 ModelConfiguration,使用自定义数据存储
  • 自定义存储可以渐进式实现功能,快速起步
  • 仍可使用 @Model@Query 等 SwiftData API

预览数据支持

SwiftUI 预览现在可以用 PreviewModifier 创建示例数据(06:58):

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())
}
  • PreviewModifier 定义预览的共享上下文
  • isStoredInMemoryOnly: true 确保预览不写磁盘
  • Trip.makeSampleTrips 填充示例数据

使用时(08:15):

#Preview(traits: .sampleData) {
    ContentView()
}

对于依赖模型参数的视图,可以用 @Previewable 宏在预览中创建查询(08:50):

#Preview(traits: .sampleData) {
    @Previewable @Query var trips: [Trip]
    BucketListItemView(trip: trips.first)
}
  • @Previewable 允许在预览声明中创建 @Query
  • 返回的数组可直接传递给视图

复杂查询与表达式

#Predicate 可以构建复合过滤条件(09:55):

let predicate = #Predicate<Trip> {
    searchText.isEmpty ? true :
    $0.name.localizedStandardContains(searchText) ||
    $0.destination.localizedStandardContains(searchText)
}

iOS 18 新增 #Expression 宏,支持非布尔值的复杂计算(10:46):

let unplannedItemsExpression = #Expression<[BucketListItem], Int> { items in
    items.filter {
        !$0.isInPlan
    }.count
}

let today = Date.now
let tripsWithUnplannedItems = #Predicate<Trip>{ trip
    (trip.startDate ..< trip.endDate).contains(today) &&
    unplannedItemsExpression.evaluate(trip.bucketList) > 0
}
  • #Expression<[BucketListItem], Int> 定义一个输入为数组、输出为整数的表达式
  • 表达式计算数组中满足条件的元素数量
  • evaluate(trip.bucketList) 在谓词中调用表达式
  • 这使得谓词可以表达”数组中至少有一个元素满足条件”这类复杂逻辑

索引加速查询

#Index 宏为常用字段创建索引(12:41):

@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?
}
  • #Index<Trip> 为指定 KeyPath 创建索引
  • [\.name], [\.startDate], [\.endDate] 为单个字段创建索引
  • [\.name, \.startDate, \.endDate] 为三个字段组合创建复合索引
  • 索引用于加速这些字段上的排序和过滤操作
  • 对大数据集效果显著

核心启发

  • 为业务身份添加唯一约束:如果你有一个自然的主键(如用户 ID、订单号),用 #Unique 声明它。如果是复合键(如用户+时间),也一并声明。这样数据同步、批量导入时不会产生重复记录,代码也不用写去重逻辑。

  • 为高频查询字段添加索引:检查应用中的 @QueryFetchDescriptor,看哪些字段经常用于排序或过滤。这些字段应该用 #Index 标记。数据量小时差异不明显,但数据增长后索引能带来数倍性能提升。

  • 用 PreviewModifier 统一预览数据:不要在每个视图的 #Preview 里手动创建模型容器和示例数据。定义一个 PreviewModifier,让所有需要 SwiftData 数据的预览复用同一个数据源。这样预览更一致,修改示例数据时也只需要改一处。

关联 Session

评论

GitHub Issues · utterances