SwiftUI 开发之旅:CoreData 实操开发

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第2天,点击查看活动详情

coredata 是用于持久化存储数据的,可以把它的作用简单理解为类似于前端浏览器的 localStorage。但是当你把 APP 删除的时候,APP 对应的 coredata 数据也会被删除。

本文旨在快速讲清 coredata 的开发使用。

通常有借助 List 视图来讲解 coredata 的使用,这是一种常用的方式。但除此之外,也应当有更加通用的方式来使用 coredata,也就是下文将会讲解的增删改查内容。

学会了以下的内容,即使脱离了 List ,你也能单独实现其中的某一项功能:

  • 如何安装 coredata
  • 创建 coredata 实体和属性
  • coredata 如何增加数据
  • coredata 如何删除数据
  • coredata 如何查询数据
  • coredata 如何修改数据

话不多说,让我们开始吧。

下文将会使用 Xcode 14、SwiftUI 开发。

安装 coredata

新建的项目安装 coredata

  1. 创建一个新项目

  1. 选择 ios app

  1. 勾选 use Core Data

打开项目的 HelloCoreData.xcdatamodeld 文件,可以看到已经默认创建了一个名为 Item 的实体。

到这里,一个新项目安装 coredata 的部分就完成了。

现有项目安装 coredata

  1. 新建一个 Data Model 文件。

  1. 文件名一般和项目名称一样。

  1. 创建 Persistence.swift 文件

创建 Persistence,是为了让预览也能使用 coredata 数据;以下是一个官方模板,直接使用即可。

import CoreData





struct PersistenceController {
    static let shared = PersistenceController()



    static var preview: PersistenceController = {
        let result = PersistenceController(inMemory: true)
        let viewContext = result.container.viewContext
        // 给预览添加预设数据
        for _ in 0..<10 {
            let newItem = Item(context: viewContext)
            newItem.timestamp = Date()
        }
        do {
            try viewContext.save()
        } catch {
            // Replace this implementation with code to handle the error appropriately.
            // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
            let nsError = error as NSError
            fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
        }
        return result
    }()

    let container: NSPersistentContainer

    init(inMemory: Bool = false) {
        container = NSPersistentContainer(name: "HelloCoreData")
        if inMemory {
            container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
        }
        container.loadPersistentStores(completionHandler: { (storeDescription, error) in
            if let error = error as NSError? {
                // Replace this implementation with code to handle the error appropriately.
                // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.

                /*
                 Typical reasons for an error here include:
                 * The parent directory does not exist, cannot be created, or disallows writing.
                 * The persistent store is not accessible, due to permissions or data protection when the device is locked.
                 * The device is out of space.
                 * The store could not be migrated to the current model version.
                 Check the error message to determine what the actual problem was.
                 */
                fatalError("Unresolved error \(error), \(error.userInfo)")
            }
        })
        container.viewContext.automaticallyMergesChangesFromParent = true
    }
}

创建 coredata 实体和属性

在上文安装和配置好 coredata 后,接下来就可以创建实体和相应的属性了。

创建实体

在这里我们创建一个名为 User 的实体。

实体的名称一般采用首字符大写驼峰的命名方式。

添加了 3 个属性。

创建实体模型

创建完实体后,我们还需要创建一个实体对应的模型,Xcode 也提供了自动生成实体模型的功能,但这里我们采用手动创建实体的方式。

  1. 首先设置 User 实体的 Class 属性。

  1. 然后创建一个模型:Models/User.swift
import Foundation
import CoreData

final class User: NSManagedObject {
    @NSManaged var id: UUID
    // 用户名
    @NSManaged var name: String
    // 爱好
    @NSManaged var hobby: String
}

创建视图

接下来开始完成UI页面的编写。

创建一个名为 UserList.swift 文件。

我们会从 ContentView.swift 使用 NavigationLink 导航到 UserList.swift 页面。

先给预览配置 .environment 修饰符,这样预览后续才能正确显示 coredata 数据。

再新增一个 @Environment 属性,下面的增删改查功能都会用到 viewContext

只要你在视图中操作 coredata,基本都需要设置 @Environment 属性。

import SwiftUI








struct UserList: View {



    @Environment(\.managedObjectContext) private var viewContext






    var body: some View {
        Text("Hello, World!")
    }
}

struct UserList_Previews: PreviewProvider {
    static var previews: some View {
        UserList()
            .environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
    }

}

到这里,基本的准备工作和 UI 我们都已完成,接下就是实际操作了。

此时预览还不能正常显示,下面我们会修复这个问题。

coredata 查询数据

为了在 UserList 视图中显示用户列表数据,我们需要使用 @FetchRequest 来获取数据:

import SwiftUI








struct UserList: View {



    @Environment(\.managedObjectContext) private var viewContext



    


    @FetchRequest(


        entity: User.entity(),


        sortDescriptors: [NSSortDescriptor(keyPath: \User.id, ascending: false)],


        animation: .default)


        // 这就是我们获取到 coredata user 的数据
    private var userList: FetchedResults<User>
    
    var body: some View {
       Text("Hello, World!")
    }

}


struct UserList_Previews: PreviewProvider {
    static var previews: some View {
        UserList()
            .environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
    }
}

接着使用 ForEach 来显示数据:

struct UserList: View {
    @Environment(\.managedObjectContext) private var viewContext
    
    @FetchRequest(
        entity: User.entity(),
        sortDescriptors: [NSSortDescriptor(keyPath: \User.id, ascending: false)],
        animation: .default)
    private var userList: FetchedResults<User>
    
    var body: some View {
        VStack {
              if userList.isEmpty {
                Text("暂无用户数据")
            } else {
                ForEach(userList, id: \.self) { item in
                    HStack {
                        Text(item.name)
                        Text("爱好:\(item.hobby)")
                    }
                }
            }
        }.navigationTitle("用户列表")
    }
}

但现在预览还无法正常显示,我们需要在 Persistence.swift 文件中给预览添加一些数据用于显示:

  for index in 0..<10 {
      let newItem = Item(context: viewContext)
      newItem.timestamp = Date()

      // 添加的预览数据
      let userItem = User(context: viewContext)
      userItem.id = UUID()
      userItem.name = "用户 \(index)"
      userItem.hobby = "篮球"
  }

查询数据部分就完成了,是不是很简单。

不管你在哪一个视图中使用了 coredata 数据,要想让该视图正常预览,都需要在 Persistence.swift 添加相应的预览数据。当然,就算不添加预览数据,也不影响模拟器启动。

coredata 新增数据

在新增数据前,我们先简单添加一些UI控件。

下面的代码中将会省略预览的 UserList_Previews 代码。

自定义导航栏,添加一个返回按钮和新增按钮:

import SwiftUI








struct UserList: View {



    @Environment(\.managedObjectContext) private var viewContext



    


    @FetchRequest(


        entity: User.entity(),


        sortDescriptors: [NSSortDescriptor(keyPath: \User.id, ascending: false)],


        animation: .default)


    private var userList: FetchedResults<User>

    

    @State private var showAdd = false

    
    var body: some View {
        VStack {
            if userList.isEmpty {
                Text("暂无用户数据")
            } else {
                ForEach(userList, id: \.self) { item in
                    HStack {
                        Text(item.name)
                        Text("爱好:\(item.hobby)")
                    }
                }
            }
        }
        .padding()
        .navigationTitle("用户列表")
        .navigationBarBackButtonHidden(true)
        .navigationBarItems(leading: Button(action : {
        }){
            //按钮及其样式
            Image(systemName: "chevron.left")
        }, trailing:  Button(action : {
            self.showAdd = true
        }){
            Image(systemName: "plus")
        })
    }
}

创建表单和数据字段:

import SwiftUI








struct UserList: View {



    @Environment(\.managedObjectContext) private var viewContext



    


    @FetchRequest(


        entity: User.entity(),


        sortDescriptors: [NSSortDescriptor(keyPath: \User.id, ascending: false)],


        animation: .default)


    private var userList: FetchedResults<User>

    

    @State private var showAdd = false

    // 表单绑定数据
    @State private var name: String = ""
    @State private var hobby: String = ""

    func addUser() {
    }
    
    var body: some View {
        VStack {
            if showAdd {
                VStack {
                    TextField("用户名", text: $name)
                        .padding()
                        .border(Color.gray)
                    TextField("爱好", text: $hobby)
                        .padding()
                        .border(Color.gray)
                    Button(action: {
                        addUser()
                    }, label: {
                        Text("保存")
                            .padding(.horizontal, 2)
                            .padding(.vertical, 15)
                            .frame(maxWidth: .infinity)
                            .background(Color.blue)
                            .foregroundColor(.white).cornerRadius(24)
                    })
                }.padding()
            }
            
            if userList.isEmpty {
                Text("暂无用户数据")
            } else {
                ForEach(userList, id: \.self) { item in
                    HStack {
                        Text(item.name)
                        Text("爱好:\(item.hobby)")
                    }
                }
            }
        }
        .padding()
        .navigationTitle("用户列表")
        .navigationBarBackButtonHidden(true)
        .navigationBarItems(leading: Button(action : {
        }){
            //按钮及其样式
            Image(systemName: "chevron.left")
        }, trailing:  Button(action : {
            self.showAdd = true
        }){
            Image(systemName: "plus")
        })
    }
}

UI准备工作完成,现在新增数据时,我们需要通过 viewContext 获取到实体,然后给实体的属性赋值。

    func addUser() {

        withAnimation {

            if !name.isEmpty && !hobby.isEmpty {

                let newItem = User(context: viewContext)

                newItem.id = UUID()

                newItem.name = name

                newItem.hobby = hobby

                // ...
            }
        }
    }

最后,保存上下文。

调用 save() 保存数据很重要,如果不保存,即使数据新增成功,数据是没有真正存储到内存中的。

    func addUser() {

        withAnimation {

            if !name.isEmpty && !hobby.isEmpty {

                let newItem = User(context: viewContext)

                newItem.id = UUID()

                newItem.name = name

                newItem.hobby = hobby


                do {
                    try viewContext.save()
                    showAdd = false
                } catch {
                    let nsError = error as NSError
                     // fatalError() 使应用程序生成崩溃日志并终止。 尽管此功能在开发过程中可能很有用,不应在生产应用程序中使用此功能。
                    fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
                }
            }
        }
    }

演示效果:

到这里,新增数据部分完成。

tips: 在模拟器输入中文:在设置-通用-语言与地区中添加简体中文的语言,在设置-通用-键盘添加简体中文输入法后,在模拟器中按 Command + K,调起软键盘,点击软键盘下面那个小地球仪,切换成中文输入,就能在模拟器中输入中文了。

coredata 删除数据

先来添加一个删除提示框,首先创建一个控制显示提示框的变量和用于存储被删除数据id的变量。

  @State private var isDelete = false
  @State private var deleteId: UUID = UUID()
  // ...

然后添加删除按钮和提示框,保存被删除数据的 id。

  if userList.isEmpty {
      Text("暂无用户数据")
  } else {
      ForEach(userList, id: \.self) { item in
          HStack {
              Text(item.name)
              Text("爱好:\(item.hobby)")
              Button(action: {
                  isDelete = true
                  // 点击删除时保存要删除的数据的 id
                  self.currentUserId = item.id
              }, label: {
                  Text("删除")
              })
              .alert("提示", isPresented: $isDelete) {
                  Button(role: .cancel) {
                      isDelete = false
                  } label: {
                      Text("取消")
                  }
                  Button(role: .destructive) {
                  } label: {
                      Text("删除")
                  }
              } message: {
                  Text("确定删除吗?")
              }
          }
      }
  }

添加删除函数,该函数根据传递的 id 参数找到需要被删除的数据,然后传递给 viewContext.delete 函数,删除后保存即可。

  func deleteUser(id: UUID) {
      if let record = userList.first(where: { $0.id == id }) {
          withAnimation {
              viewContext.delete(record)



              do {
                  try viewContext.save()
                  isDelete = false
              } catch {
                  isDelete = false
                  let nsError = error as NSError
                  fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
              }
          }

      }
  }

调用 deleteUser 函数:

  Button(role: .destructive) {
      deleteUser(id: currentUserId)
  } label: {
      Text("删除")
  }

在这里我已经事先新增了3条数据,来看看完成的效果:

coredata 修改数据

修改时候会用到上一步创建的表单和数据。

再新增一个变量控制修改:

@State private var showUpdate = false

在文字旁边新增一个修改按钮,同时将当前要修改的数据对象的值赋值给表单绑定值,让表单显示对应数据。

Text(item.name)
Text("爱好:\(item.hobby)")
Button(action: {
    showUpdate = true
    // 赋值
    self.name = item.name
    self.hobby = item.hobby
    self.currentUserId = item.id
}, label: {
    Text("修改")
})

当 showUpdate 为 true 时显示表单,以及切换为调用 updateUser 函数:

if showAdd || showUpdate {
  VStack {
      TextField("用户名", text: $name)
          .padding()
          .border(Color.gray)
      TextField("爱好", text: $hobby)
          .padding()
          .border(Color.gray)
      Button(action: {
          if showAdd {
              addUser()
          } else if showUpdate {
              updateUser(id: currentUserId)
          }

      }, label: {
          Text("保存")
              .padding(.horizontal, 2)
              .padding(.vertical, 15)
              .frame(maxWidth: .infinity)
              .background(Color.blue)
              .foregroundColor(.white).cornerRadius(24)
      })
  }.padding()
}

编写 updateUser 函数:

func updateUser(id: UUID) {
    if let record = userList.first(where: { $0.id == id }) {
        withAnimation {
            record.name = self.name
            record.hobby = self.hobby

            do {
                try viewContext.save()
                showUpdate = false
            } catch {
                showUpdate = false
                let nsError = error as NSError
                fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
            }
        }
    }
}

来看最后效果:

在上面的几个操作中,省略了一些数据清理工作,比如新增后的数据置空,表单的隐藏等。

总结

本文我们完成了 coredata 的增删改查等基本操作,相信经过学习你已经基本掌握了这些内容,coredata 还有很多使用方式,比如更细粒度的查询,分页等操作,有机会我们再讲吧。

这是 SwiftUI 开发之旅专栏的文章,是 swiftui 开发学习的经验总结及实用技巧分享,欢迎关注该专栏,会坚持输出。同时欢迎关注我的个人公众号 @JSHub:提供最新的开发信息速报,优质的技术干货推荐。或是查看我的个人博客:Devcursor

?点赞:如果有收获和帮助,请点个赞支持一下!

?收藏:欢迎收藏文章,随时查看!

?评论:欢迎评论交流学习,共同进步!

© 版权声明
THE END
喜欢就支持一下吧
点赞0

Warning: mysqli_query(): (HY000/3): Error writing file '/tmp/MY4OtrSt' (Errcode: 28 - No space left on device) in /www/wwwroot/583.cn/wp-includes/class-wpdb.php on line 2345
admin的头像-五八三
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

图形验证码
取消
昵称代码图片