版本記錄
版本號 | 時間 |
---|---|
V1.0 | 2020.05.30 星期六 |
前言
數據的持久化存儲是移動端不可避免的一個問題,很多時候的業務邏輯都需要我們進行本地化存儲解決和完成,我們可以采用很多持久化存儲方案,比如說
plist
文件(屬性列表)、preference
(偏好設置)、NSKeyedArchiver
(歸檔)、SQLite 3
、CoreData
,這里基本上我們都用過。這幾種方案各有優缺點,其中,CoreData是蘋果極力推薦我們使用的一種方式,我已經將它分離出去一個專題進行說明講解。這個專題主要就是針對另外幾種數據持久化存儲方案而設立。
1. 數據持久化方案解析(一) —— 一個簡單的基于SQLite持久化方案示例(一)
2. 數據持久化方案解析(二) —— 一個簡單的基于SQLite持久化方案示例(二)
3. 數據持久化方案解析(三) —— 基于NSCoding的持久化存儲(一)
4. 數據持久化方案解析(四) —— 基于NSCoding的持久化存儲(二)
5. 數據持久化方案解析(五) —— 基于Realm的持久化存儲(一)
6. 數據持久化方案解析(六) —— 基于Realm的持久化存儲(二)
7. 數據持久化方案解析(七) —— 基于Realm的持久化存儲(三)
8. 數據持久化方案解析(八) —— UIDocument的數據存儲(一)
9. 數據持久化方案解析(九) —— UIDocument的數據存儲(二)
10. 數據持久化方案解析(十) —— UIDocument的數據存儲(三)
開始
首先看下主要內容:
在本教程中,您將學習使用
@State,@Environment
和@FetchRequest
屬性包裝器將數據持久保存在應用程序中。內容來自翻譯。
接著看下寫作環境:
Swift 5, iOS 13, Xcode 11
下面就是正文了
想象一下,記下Notes
中的一些重要內容,卻發現下次打開應用程序時數據消失了!幸運的是,持久化在iOS上非常出色。多虧了Core Data
,所有筆記,照片和其他數據都是安全的。
需要跨應用程序啟動存儲數據時,可以使用多種不同的技術。Core Data
是iOS上的首選解決方案。 Apple的Core Data
框架具有出色的性能和廣泛的功能,可管理應用程序的整個模型層,并處理對設備存儲磁盤的持久性。
在本教程中,您將重構應用程序以增加持久化,并防止在應用程序重啟時丟失數據的噩夢。在此過程中,您將學習:
- 在項目中設置
Core Data
。 - 使用
SwiftUI
的數據流訪問Core Data
框架中所需的內容。 - 使用
Core Data
定義和創建新的模型對象。 - 使用
Fetch Requests
從磁盤檢索對象。
因此,下面一起來了解有關Core Data
功能及其工作原理的更多信息!
打開起始項目,并build
歡迎使用FaveFlicks
,您自己喜歡的電影的個人收藏。 這是一個簡單的應用程序,可讓您在列表中添加或刪除電影。 但是,它有一個明顯的問題。
是的,您猜對了:該應用程序不會保留數據! 這意味著,如果您將一些電影添加到列表中,然后重新啟動應用程序,則您精心添加的電影將消失。
1. Testing FaveFlick’s Persistence
要從列表中刪除電影,請向左滑動并點按Delete
。
接下來,點擊右上角的加號按鈕以添加您的收藏夾之一。
你將會看到Add Movie
頁面
每個Movie
對象僅存在于內存中。 它們沒有存儲在磁盤上,因此關閉應用程序會刪除您的更改并恢復到我喜歡的電影的列表。
注意:如果您嘗試第二次打開
add movie
頁面,則什么也不會發生。 這是SwiftUI
中的一個已知Apple
bug
。 解決方法是,您需要以某種方式更新UI以添加更多電影。 您可以下拉列表以更新UI,然后添加更多電影。
強制關閉應用程序以測試其持久化。 將應用置于前臺,進入快速應用切換器fast app switcher
。 為此,請從屏幕底部輕輕向上拖動。 如果您的設備有一個,請雙擊Home
按鈕以啟用快速應用程序切換器。
現在,選擇FaveFlicks
并向上滑動以關閉該應用程序。 在home
屏幕上,點擊FaveFlicks
再次將其打開。
請注意,您所做的更改已消失,并且默認影片已恢復。
現在該修復此問題。首先設置Core Data
。
Setting Up Core Data
在開始設置持久性之前,您應該了解Core Data
的活動部分,也稱為Core Data stack
。Core Data stack
包括:
- 定義模型對象的
managed object model
,(也稱為實體(entities)
)及其與其他實體的關系。將其視為您的數據庫架構(database schema)
。在FaveFlicks
中,您將將Movie
實體定義為FaveFlicks.xcdatamodeld
中managed object model
的一部分。您將使用NSManagedObjectModel
類在代碼中訪問您的managed object model
。 -
NSPersistentStoreCoordinator
,用于管理實際的數據庫(actual database)
。 -
NSManagedObjectContext
,它是一個內存暫存器,可讓您創建,編輯,刪除或檢索實體。通常,在與Core Data
進行交互時,您將使用managed object context
。
有了這些,就可以開始了!
1. Adding the Core Data stack
盡管設置整個Core Data stack
似乎很艱巨,但要感謝NSPersistentContainer
,這很容易。它可以為您創建一切。打開SceneDelegate.swift
并在import SwiftUI
之后添加以下內容:
import CoreData
Core Data
存在于其自己的框架中,因此您必須導入它才能使用它。
現在,在SceneDelegate
的末尾添加以下內容:
// 1
lazy var persistentContainer: NSPersistentContainer = {
// 2
let container = NSPersistentContainer(name: "FaveFlicks")
// 3
container.loadPersistentStores { _, error in
// 4
if let error = error as NSError? {
// You should add your own error handling code here.
fatalError("Unresolved error \(error), \(error.userInfo)")
}
}
return container
}()
下面就是要做的內容:
- 1) 將一個名為
persistentContainer
的懶加載屬性添加到您的SceneDelegate
。首次引用該屬性時,它將創建一個NSPersistentContainer
。 - 2) 創建一個名為
FaveFlicks
的容器。如果您在Project navigator
中查看應用程序的文件列表,則會看到一個名為FaveFlicks.xcdatamodeld
的文件。該文件是您稍后將在其中設計Core Data model schema
的位置。該文件的名稱必須與容器的名稱匹配。 - 3) 指示容器加載
persistent store
,這將簡單地設置Core Data stack
。 - 4) 如果發生錯誤,則會記錄錯誤并終止該應用程序。在真實的應用程序中,您應該通過顯示一個對話框指示該應用程序處于怪異狀態并需要重新安裝來處理此問題。此處的任何錯誤都應該很少發生,并且是由于開發人員的錯誤造成的,因此,在將您的應用提交到
App Store
之前,請務必先發現錯誤。
就這些。這就是設置Core Data stack
所需的全部。不是很難,對吧?
您還需要一種將任何數據保存到磁盤的方法,因為Core Data
不會自動處理該數據。仍在SceneDelegate.swift
中,在類末尾添加以下方法:
func saveContext() {
// 1
let context = persistentContainer.viewContext
// 2
if context.hasChanges {
do {
// 3
try context.save()
} catch {
// 4
// The context couldn't be saved.
// You should add your own error handling here.
let nserror = error as NSError
fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
}
}
}
這將創建一個名為saveContext()
的方法,該方法將執行以下操作:
- 1) 獲取持久性容器
(persistent container)
的viewContext
。 這是一個特殊的managed object context
,僅在主線程上使用。 您將用它來保存所有未保存的數據。 - 2) 僅當有更改要保存時才保存。
- 3) 保存上下文。 此調用可能會引發錯誤,因此包含在
try / catch
中。 - 4) 發生錯誤時,系統會記錄該錯誤并將該應用終止。 就像以前的方法一樣,此處的任何錯誤都應僅在開發期間發生,但應以適當的方式在您的應用程序中進行處理,以防萬一。
既然您已經設置了Core Data stack
,并且可以保存更改,現在是時候將其連接到應用程序的其余部分了。
現在,在scene(_:willConnectTo:options :)
中,將let contentView = MovieList()
替換為以下內容:
let context = persistentContainer.viewContext
let contentView = MovieList().environment(\.managedObjectContext, context)
這只是獲取與您先前使用的相同的viewContext
并將其設置為MovieList SwiftUI
視圖上的環境變量。 該視圖稍后將使用此視圖從Core Data
存儲中添加和刪除電影。
現在,將以下方法添加到SceneDelegate
的末尾:
func sceneDidEnterBackground(_ scene: UIScene) {
saveContext()
}
這指示應用程序在后臺運行時調用您先前添加的save
方法。 這是將數據保存到磁盤的好時機。 稍后,您將看到如何更頻繁地保存。
構建并運行以檢查該應用程序是否仍然有效。
Creating the Data Model
現在該是該應用程序主要部分上的工作了。 在Xcode
中,打開FaveFlicks.xcdatamodel
。 現在它是空的,但是您將在下面聲明Movie
實體(entity)
。 在這里定義數據模型的schema
。 您將添加相關的實體(可以創建的對象類型),并定義關系(relationships)
以指示實體的連接方式。
單擊Add Entity
。
Xcode
在data model
中創建一個新實體,默認情況下名為Entity
。 雙擊名稱并將其更改為Movie
接下來,單擊Attributes
下的+
圖標以添加新屬性。 將其命名為title
并將類型設置為String
。
最后,再添加兩個屬性:一個名為String
的genre
,另一個為Date
類型的releaseDate
。 完成后,Movie
實體的屬性將與以下各項匹配:
1. Relationships and Fetched Properties
盡管FaveFlicks
僅具有一個Movie
實體,但是在具有較大數據模型的應用程序中,您可能會遇到關系和獲取的屬性。 關系(relationship)
與任何數據庫中的關系相同:它使您可以定義兩個實體之間的關系。
但是,Fetched properties
是更高級的Core Data
主題。 您可以將其視為類似于弱單向關系的計算屬性。 例如,如果FaveFlicks
具有Cinema
實體,則它可能具有currentShowingMovies
的Fetched properties
,該屬性將獲取電影院中當前的Movies
。
Removing the Old Movie Struct
打開Movie.swift
。 在本教程開始時,Movie
結構是模型對象(model object)
。 Core Data
創建了自己的Movie
類,因此您需要刪除Movie.swift
。 通過在“項目”導航器中右鍵單擊Movie.swift
并選擇Delete
來刪除它。 在出現的對話框中,單擊Move to Trash
。
Build
應用。 您會看到幾個需要修復的錯誤,因為您剛剛刪除了Movie
。
注意:您需要保持準確,并在本節中刪除舊的
Movie
結構的大量代碼,因此請密切注意!
首先,打開MovieList.swift
。 您會找到存儲在簡單movies
數組中的movies
列表。 在MovieList
的頂部,將聲明movies
數組的行更改為空數組,如下所示:
@State var movies: [Movie] = []
@State
屬性包裝器是SwiftUI
數據流的重要組成部分。 聲明此本地屬性的類擁有它。 如果有任何更改movies
的值,則擁有它的視圖將觸發UI的更新。
現在,刪除makeMovieDefaults()
,因為它已不再使用。
在addMovie(title:genre:releaseDate :)
中,將創建movies
并將其添加到movies
數組。 刪除其內容并將其保留為空白方法。 您將在后面的部分中使用它來創建Movie
實體的新實例。
最后,刪除deleteMovie(at :)
的內容。 您稍后將用刪除Core Data
實體的代碼替換它。
Using the New Movie Entity
現在,您已經在數據模型(data model)
中創建了Movie
實體,Xcode
將自動生成它自己的Movie
類,您將使用它來代替。 數據模型(data model)
中的所有實體都是NSManagedObject
的子類。 這是一個managed object
,因為Core Data
主要通過使用Managed Object Context
來為您處理生命周期和持久性。
舊的Movie
結構沒有使用可選屬性。 但是,所有NSManagedObject
子類都為其屬性使用可選屬性。 這意味著您需要對使用Movie
的文件進行一些更改。
1. Using an Entity’s Attributes in a View
現在,您將學習在視圖中使用實體的屬性(attributes)
。 打開MovieRow.swift
。 然后,將body
屬性替換為:
var body: some View {
VStack(alignment: .leading) {
// 1
movie.title.map(Text.init)
.font(.title)
HStack {
// 2
movie.genre.map(Text.init)
.font(.caption)
Spacer()
// 3
movie.releaseDate.map { Text(Self.releaseFormatter.string(from: $0)) }
.font(.caption)
}
}
}
視圖的結構完全相同,但是您會注意到所有movie
attributes
都已映射到Views
。
Core Data entity
上的所有屬性(attributes)
都是可選的。 也就是說,title
屬性的類型為String?
,referenceDate
的類型為Date?
等等。 因此,現在您需要一種獲取可選值的方法。
在ViewBuilder
中,例如MovieRows
的body
屬性,您無法添加控制流語句(如if let
)。 每行應為View
或nil
。
如果attributes
為non-nil
,則上面標記為1
、2
和3
的行是Text
視圖。 否則,它為nil
。 這是在SwiftUI代碼中處理可選內容的便捷方法。
最后,構建并運行。 您刪除了舊的Movie
結構,并將其替換為Core Data
實體。 作為獎勵,您現在擁有空視圖,而不是電影列表。
如果您制作電影,則什么也不會發生。 接下來,您將解決此問題。
Using Environment to Access Managed Object Context
接下來,您將學習如何從managed object context
訪問對象。 返回MovieList.swift
,在movies
聲明下添加以下行:
@Environment(\.managedObjectContext) var managedObjectContext
還記得您之前在MovieList
上設置了managedObjectContext
環境變量嗎? 好吧,現在您聲明它已經存在,因此可以訪問它。
@Environment
是SwiftUI
數據流的另一個重要部分,可讓您訪問全局屬性。 當您要將環境對象傳遞給視圖時,可以在創建對象時將其傳遞給視圖。
現在,將以下方法添加到MovieList.swift
中:
func saveContext() {
do {
try managedObjectContext.save()
} catch {
print("Error saving managed object context: \(error)")
}
}
創建,更新或刪除實體時,需要在managed object context
(內存暫存器)中進行。 要將更改實際寫入磁盤,必須保存上下文。 此方法將新的或更新的對象保存到持久性存儲中。
接下來,找到addMovie(title:genre:releaseDate :)
。 從刪除舊的Movie
以來,該方法仍然是空白的,因此將其替換為以下方法以創建新的Movie
實體:
func addMovie(title: String, genre: String, releaseDate: Date) {
// 1
let newMovie = Movie(context: managedObjectContext)
// 2
newMovie.title = title
newMovie.genre = genre
newMovie.releaseDate = releaseDate
// 3
saveContext()
}
在這里,您:
- 1) 在
managed object context
中創建一個新的Movie
。 - 2) 設置將
Movie
的所有屬性作為參數傳遞到addMovie(title:genre:releaseDate :)
中。 - 3) 保存
managed object context
。
Build
并運行和創建新電影。 您會注意到一個空白列表。
那是因為您正在創建電影,但沒有檢索它們以顯示在列表中。 在下一節中,您將對其進行修復,最后您將再次在該應用程序中觀到movies
。
Fetching Objects
現在,您將學習如何顯示自己制作的電影。 您需要使用FetchRequest
從持久性存儲中獲取它們。
在MovieList
的頂部,刪除聲明movies
數組的行。 用以下FetchRequest
替換它:
// 1
@FetchRequest(
// 2
entity: Movie.entity(),
// 3
sortDescriptors: [
NSSortDescriptor(keyPath: \Movie.title, ascending: true)
]
// 4
) var movies: FetchedResults<Movie>
當您需要從Core Data
檢索實體時,可以創建FetchRequest
。在這里,您:
- 1) 使用
@FetchRequest
屬性包裝器(property wrapper)
聲明該屬性,該包裝器可讓您直接在SwiftUI
視圖中使用結果。 - 2) 在屬性包裝器內,指定要獲取
Core Data
的實體。這將獲取Movie
實體的實例。 - 3) 添加一個排序描述符
(sort descriptors)
數組,以確定結果的順序。例如,您可以按流派(genre)
對Movie
進行排序,然后對具有相同流派的電影按標題進行排序。但是在這里,您只需按標題排序。 - 4) 最后,在屬性包裝器之后,聲明類型為
FetchedResults
的movies
屬性。
1. Predicates
這將獲取Core Data
存儲的所有Movie
。但是,如果您需要過濾對象或僅檢索一個特定實體怎么辦?您還可以使用謂詞(predicate)
配置fetched request
以限制結果,例如僅獲取特定年份的電影或匹配特定流派的電影。為此,您可以在@FetchRequest
屬性包裝的末尾添加謂詞參數,如下所示:
predicate: NSPredicate(format: "genre contains 'Action'")
您的提取請求應該會提取所有電影,因此現在無需添加它。 但是,如果您想嘗試一下,那就一定要做!
Testing the Results
Build
并運行。 您會看到電影列表。 恭喜你!
好吧,這只是使您回到起點。 要測試電影是否已存儲到磁盤,請添加一些電影,然后按Xcode中的stop
以終止該應用程序。 然后構建并再次運行。 您所有的電影仍將在那里!
Deleting Objects
接下來,您將學習刪除對象。 如果向左滑動并嘗試刪除電影,則什么也不會發生。 要解決此問題,請將deleteMovie(at :)
替換為:
func deleteMovie(at offsets: IndexSet) {
// 1
offsets.forEach { index in
// 2
let movie = self.movies[index]
// 3
self.managedObjectContext.delete(movie)
}
// 4
saveContext()
}
這是正在做的事情:
- 1) 滑動以刪除列表中的對象時,
SwiftUI
List
會為您提供刪除的IndexSet
。 使用forEach
遍歷IndexSet
。 - 2) 獲取當前
index
的電影。 - 3) 從
managed object context
中刪除影片。 - 4) 保存上下文以將更改持久保存到磁盤。
構建并運行。 然后,刪除電影。
大功告成!
在本教程中,您已經了解了很多數據流,但是如果您想了解更多信息,請觀看WWDC 2019的數據流通過SwiftUIData Flow Through SwiftUI視頻。
后記
本篇主要講述了基于Core Data 和 SwiftUI的數據存儲示例,感興趣的給個贊或者關注~~~