SwiftUI 開發之旅:CoreData 實操開發
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
- 建立一個新專案
- 選擇 ios app
- 勾選 use Core Data
開啟專案的 HelloCoreData.xcdatamodeld 檔案,可以看到已經預設建立了一個名為 Item 的實體。
到這裡,一個新專案安裝 coredata 的部分就完成了。
現有專案安裝 coredata
- 新建一個 Data Model 檔案。
- 檔名一般和專案名稱一樣。
- 建立 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 也提供了自動生成實體模型的功能,但這裡我們採用手動建立實體的方式。
- 首先設定 User 實體的 Class 屬性。
- 然後建立一個模型: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。
👍點贊:如果有收穫和幫助,請點個贊支援一下!
🌟收藏:歡迎收藏文章,隨時檢視!
💬評論:歡迎評論交流學習,共同進步!