Published on

SwiftData in SwfitUI

Authors
  • Name
    Twitter

开篇

如果我们想在App中持久化,我们通常需要哪几步

1、定义你需要存储的数据的类型 2、建立一个数据库或数据文件 3、CRUD你的数据 4、在视图中使用这些数据

SwiftData的常规用法

1. 首先定义Schema,用@Model

一点说明,我在看WWDC2023中的关于SwiftData的课程时,他们多次提到了Schema这个单词,这其实是从数据库中引入的一个名词,它用于定义数据库中数据结构的元数据集合,描述了数据库中的表、字段、关键字、索引等信息。我们可以翻译成数据库模式或者数据库结构。

@Model 
class Wubi {
    let name: String
}

其实@Model也是一个宏,主要是将我们的类型修改成PersistentModel类型 不会侵入式修改类本身,而是让类遵循PersistentModel协议,来满足底层数据库Sqlite3的使用,有点类似于Coredata,说白了,SwiftData也是在这些基础的存储架构上进行的封装。

目前在Mac系统中,我找到了App创建的数据库文件路径

 /Users/yongyou/Library/Application\ Support/default.store 
 /Users/yongyou/Library/Application\ Support/default.store-shm 
 /Users/yongyou/Library/Application\ Support/default.store-wal 

我们可以通过数据库查看工具查看系统建立的数据表

   guard let appSupportDir = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).last else { return }

2. 拿到modelContainer,通过修改器来设置App的模型容器和模型上下文

extension Scene {
     public func modelContainer(_ container: ModelContainer) -> some Scene

}

Sets the model container and associated model context in this scene's environment. 设置此场景环境中的模型容器和关联模型上下文。

The environment's EnvironmentValues/modelContext property will be assigned a new context associated with this container. All implicit model context operations in this scene, such as Query properties, will use the environment's context.

环境的 EnvironmentValues/modelContext 属性将被分配给与此容器关联的新上下文。 属性分配一个与此容器关联的新上下文。此场景中的所有隐式 模型上下文操作(如 Query 属性,都将使用环境的上下文。

@main
struct WubiMacApp: App {
    let modelContainer: ModelContainer
    init() {
        do {
            modelContainer = try ModelContainer(for: Wubi.self)
        } catch {
            fatalError("Could not initialize ModelContainer")
        }
    }
    var body: some Scene {
        WindowGroup {
            WubiMacContentView()
        }
        .modelContainer(modelContainer)
    }
}

其中的ModelContainer的作用是修改App的Schema和模型存储配置(Model storage configuration) 它的定义如下:

@available(swift 5.9)
@available(macOS 14, iOS 17, tvOS 17, watchOS 10, *)
public class ModelContainer : Equatable, @unchecked Sendable {

    /// An object that maps model classes to data in the model store, and helps with the migration of that data
    /// between releases.
    final public let schema: Schema

    ///An interface for describing the evolution of a schema and how to migrate between specific versions.
    final public let migrationPlan: (SchemaMigrationPlan.Type)?

    /// A type that describes the configuration of an app's schema or specific group of models.
    public var configurations: Set<ModelConfiguration>

    /// An object that enables you to fetch, insert, and delete models, and save any changes to disk.
    @MainActor public var mainContext: ModelContext { get }

    /// Returns a Boolean value indicating whether two values are equal.
    ///
    /// Equality is the inverse of inequality. For any values `a` and `b`,
    /// `a == b` implies that `a != b` is `false`.
    ///
    /// - Parameters:
    ///   - lhs: A value to compare.
    ///   - rhs: Another value to compare.
    public static func == (lhs: ModelContainer, rhs: ModelContainer) -> Bool

    public convenience init(for forTypes: PersistentModel.Type..., migrationPlan: (SchemaMigrationPlan.Type)? = nil, configurations: ModelConfiguration...) throws

    public convenience init(for givenSchema: Schema, migrationPlan: (SchemaMigrationPlan.Type)? = nil, configurations: ModelConfiguration...) throws

    public init(for givenSchema: Schema, migrationPlan: (SchemaMigrationPlan.Type)? = nil, configurations: [ModelConfiguration]) throws

    public func deleteAllData()
}

schema: Schema我们可以理解成数据库中的Schema,同时也提到了能帮助迁移数据在不同的版本中 migrationPlan: (SchemaMigrationPlan.Type)? 描述架构的演变,以及如何在特定的版本中迁移数据 configurations: Set<ModelConfiguration>描述应用程序模式或特定模型组配置的类型。 在初始化方法中,我们可以重点关注这两个方法

public convenience init(for forTypes: PersistentModel.Type..., migrationPlan: (SchemaMigrationPlan.Type)? = nil, configurations: ModelConfiguration...) throws

public convenience init(for givenSchema: Schema, migrationPlan: (SchemaMigrationPlan.Type)? = nil, configurations: ModelConfiguration...) throws

他们提供的第一个参数 forTypes: PersistentModel.Type...其实就是通过@Model宏来定义的类类型 当然,我们也可以自定义一个Schema来告诉编译器,App的Schema是怎样的 两者都是一样的效果

3. 通过modelContext去修改数据

@Environment(\.modelContext) var modelContext

modelContext.insert(wubi)

@Environment(.modelContext)是一个属性包装器,它的初始化方法如下

 @inlinable public init(_ keyPath: KeyPath<EnvironmentValues, Value>)

通过传入一个keyPath路径,来得到一个modelContext

// Available when SwiftUI is imported with SwiftData
extension EnvironmentValues {

    /// The SwiftData model context that will be used for queries and other
    /// model operations within this environment.
    @available(iOS 17.0, macOS 14.0, tvOS 17.0, watchOS 10.0, *)
    public var modelContext: ModelContext
}

其中的ModelContext定义如下

/// An object that enables you to fetch, insert, and delete models, and save any changes to disk.
@available(swift 5.9)
@available(macOS 14, iOS 17, tvOS 17, watchOS 10, *)
public class ModelContext : Equatable {

    public var undoManager: UndoManager?

    public var insertedModelsArray: [PersistentModel] { get }

    public var changedModelsArray: [PersistentModel] { get }

    public var deletedModelsArray: [PersistentModel] { get }

    public var container: ModelContainer { get }

    public var autosaveEnabled: Bool

    public init(_ container: ModelContainer)

    /// Returns a Boolean value indicating whether two values are equal.
    ///
    /// Equality is the inverse of inequality. For any values `a` and `b`,
    /// `a == b` implies that `a != b` is `false`.
    ///
    /// - Parameters:
    ///   - lhs: A value to compare.
    ///   - rhs: Another value to compare.
    public static func == (lhs: ModelContext, rhs: ModelContext) -> Bool

    public var hasChanges: Bool { get }

    public func model(for persistentModelID: PersistentIdentifier) -> PersistentModel

    public func registeredModel<T>(for persistentModelID: PersistentIdentifier) -> T? where T : PersistentModel

    public func insert<T>(_ model: T) where T : PersistentModel

    public func rollback()

    public func processPendingChanges()

    public func delete<T>(model: T.Type, where predicate: Predicate<T>? = nil, includeSubclasses: Bool = true) throws where T : PersistentModel

    public func delete<T>(_ model: T) where T : PersistentModel

    public func transaction(block: () throws -> Void) throws

    public func save() throws

    public func enumerate<T>(_ fetch: FetchDescriptor<T>, batchSize: Int = 5000, allowEscapingMutations: Bool = false, block: (_ model: T) throws -> Void) throws where T : PersistentModel

    public func fetch<T>(_ descriptor: FetchDescriptor<T>) throws -> [T] where T : PersistentModel

    public func fetchCount<T>(_ descriptor: FetchDescriptor<T>) throws -> Int where T : PersistentModel

    public func fetch<T>(_ descriptor: FetchDescriptor<T>, batchSize: Int) throws -> FetchResultsCollection<T> where T : PersistentModel

    public func fetchIdentifiers<T>(_ descriptor: FetchDescriptor<T>) throws -> [PersistentIdentifier] where T : PersistentModel

    public func fetchIdentifiers<T>(_ descriptor: FetchDescriptor<T>, batchSize: Int) throws -> FetchResultsCollection<PersistentIdentifier> where T : PersistentModel

    public static let willSave: Notification.Name

    public static let didSave: Notification.Name

    /// Describes the data in the user info dictionary of a notification sent by a model context.
    public enum NotificationKey : String {

        /// A token that indicates which generation of the model store SwiftData is using.
        case queryGeneration

        /// A set of values identifying the context's invalidated models.
        case invalidatedAllIdentifiers

        /// A set of values identifying the context's inserted models.
        case insertedIdentifiers

        /// A set of values identifying the context's updated models.
        case updatedIdentifiers

        /// A set of values identifying the context's deleted models.
        case deletedIdentifiers

        /// Creates a new instance with the specified raw value.
        ///
        /// If there is no value of the type that corresponds with the specified raw
        /// value, this initializer returns `nil`. For example:
        ///
        ///     enum PaperSize: String {
        ///         case A4, A5, Letter, Legal
        ///     }
        ///
        ///     print(PaperSize(rawValue: "Legal"))
        ///     // Prints "Optional("PaperSize.Legal")"
        ///
        ///     print(PaperSize(rawValue: "Tabloid"))
        ///     // Prints "nil"
        ///
        /// - Parameter rawValue: The raw value to use for the new instance.
        public init?(rawValue: String)

        /// The raw type that can be used to represent all values of the conforming
        /// type.
        ///
        /// Every distinct value of the conforming type has a corresponding unique
        /// value of the `RawValue` type, but there may be values of the `RawValue`
        /// type that don't have a corresponding value of the conforming type.
        public typealias RawValue = String

        /// The corresponding value of the raw type.
        ///
        /// A new instance initialized with `rawValue` will be equivalent to this
        /// instance. For example:
        ///
        ///     enum PaperSize: String {
        ///         case A4, A5, Letter, Legal
        ///     }
        ///
        ///     let selectedSize = PaperSize.Letter
        ///     print(selectedSize.rawValue)
        ///     // Prints "Letter"
        ///
        ///     print(selectedSize == PaperSize(rawValue: selectedSize.rawValue)!)
        ///     // Prints "true"
        public var rawValue: String { get }
    }
}

提供了对PersistentModel的CURD操作

4. 通过@Query去使用数据

@Query是一个宏,我们可以用Xcode来展开代码,不过我在Xcode15.1中,展开代码遇到了问题,展开后有编译错误,可能要等Xcode来修复这个问题

回到正题,说一下@Query的用法

struct SearchHistoryView: View {
    @Query var historys: [Wubi]
    @Binding var selectionIndex: String

    var body: some View {
        WubiListView(selectionIndex: $selectionIndex, wubis: historys)
    }
}

我的这个用法中,我有疑问的地方是,我通过@Model定义的Wubi这个类,但是我@Query所使用的是[Wubi]这个数据,好像记得SwiftData会自动关联,这个问题还有待进一步查证。

我在实际项目中遇到的问题是

我有一个@Model的Class,但是会生成两个和两个不同的数据,如何分别去生成和使用

目前的猜测是不能,只有在同一个Model中去过滤不同的数据

Swift Data中的一些编程思想

体验下来,SwiftData真的丝滑!!!

1.SwiftData是结合了两个技术的产物,CoreData的持久化技术和Swift的现代特性concurrency。 2.用最少的代码和无外部依赖性的方式为应用程序快速添加持久性。 3.通过宏来快速、高效和安全的编写代码,从而为你的App定义整个模型层。 4.除了在本地创建内容,SwiftData还有其它的用途。通过网络请求获取数据的App可以使用它来实现轻量级缓存机制,并提供有限的离线功能。 5.在设计上是非侵入性的,它是对App现有模型的补充。其实现的原理是通过两个协议来实现PersistentModel和Observable PersistentModel主要是用来实现inline shechma,Observable主要是用来实现变化跟踪 6.SchemaMigrationPlan只有在自动迁移不了的情况下才需要出场。

Todo

https://www.swiftyplace.com/blog/swiftdata-stack-understanding-containers https://www.hackingwithswift.com/books/ios-swiftui/relationships-with-swiftdata-swiftui-and-query https://developer.apple.com/documentation/swiftdata