SwiftUI 開發之旅:CoreData 實操開發

語言: CN / TW / HK

theme: smartblue

持續創作,加速成長!這是我參與「掘金日新計劃 · 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 資料;以下是一個官方模板,直接使用即可。

```swift 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

```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 屬性。

```swift 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 來獲取資料:

```swift 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 來顯示資料:

```swift 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 檔案中給預覽新增一些資料用於顯示:

```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 程式碼。

自定義導航欄,新增一個返回按鈕和新增按鈕:

```swift 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")
    })
}

} ```

建立表單和資料欄位:

```swift 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 獲取到實體,然後給實體的屬性賦值。

swift func addUser() { withAnimation { if !name.isEmpty && !hobby.isEmpty { let newItem = User(context: viewContext) newItem.id = UUID() newItem.name = name newItem.hobby = hobby // ... } } }

最後,儲存上下文。

呼叫 save() 儲存資料很重要,如果不儲存,即使資料新增成功,資料是沒有真正儲存到記憶體中的。

```swift 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的變數。

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

然後新增刪除按鈕和提示框,儲存被刪除資料的 id。

swift 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 函式,刪除後儲存即可。

```swift 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 函式:

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

在這裡我已經事先新增了3條資料,來看看完成的效果:

coredata 修改資料

修改時候會用到上一步建立的表單和資料。

再新增一個變數控制修改:

swift @State private var showUpdate = false

在文字旁邊新增一個修改按鈕,同時將當前要修改的資料物件的值賦值給表單繫結值,讓表單顯示對應資料。

swift 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 函式:

swift 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 函式:

```swift 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

👍點贊:如果有收穫和幫助,請點個贊支援一下!

🌟收藏:歡迎收藏文章,隨時檢視!

💬評論:歡迎評論交流學習,共同進步!