數據持久化方案解析(十一) —— 基于Core Data 和 SwiftUI的數據存儲示例(一)

版本記錄

版本號 時間
V1.0 2020.05.30 星期六

前言

數據的持久化存儲是移動端不可避免的一個問題,很多時候的業務邏輯都需要我們進行本地化存儲解決和完成,我們可以采用很多持久化存儲方案,比如說plist文件(屬性列表)、preference(偏好設置)、NSKeyedArchiver(歸檔)、SQLite 3CoreData,這里基本上我們都用過。這幾種方案各有優缺點,其中,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 stackCore Data stack包括:

  • 定義模型對象的managed object model,(也稱為實體(entities))及其與其他實體的關系。將其視為您的數據庫架構(database schema)。在FaveFlicks中,您將將Movie實體定義為FaveFlicks.xcdatamodeldmanaged 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

Xcodedata model中創建一個新實體,默認情況下名為Entity。 雙擊名稱并將其更改為Movie

接下來,單擊Attributes下的+圖標以添加新屬性。 將其命名為title并將類型設置為String

最后,再添加兩個屬性:一個名為Stringgenre,另一個為Date類型的releaseDate。 完成后,Movie實體的屬性將與以下各項匹配:

1. Relationships and Fetched Properties

盡管FaveFlicks僅具有一個Movie實體,但是在具有較大數據模型的應用程序中,您可能會遇到關系和獲取的屬性。 關系(relationship)與任何數據庫中的關系相同:它使您可以定義兩個實體之間的關系。

但是,Fetched properties是更高級的Core Data主題。 您可以將其視為類似于弱單向關系的計算屬性。 例如,如果FaveFlicks具有Cinema實體,則它可能具有currentShowingMoviesFetched 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中,例如MovieRowsbody屬性,您無法添加控制流語句(如if let)。 每行應為Viewnil

如果attributesnon-nil,則上面標記為123的行是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環境變量嗎? 好吧,現在您聲明它已經存在,因此可以訪問它。

@EnvironmentSwiftUI數據流的另一個重要部分,可讓您訪問全局屬性。 當您要將環境對象傳遞給視圖時,可以在創建對象時將其傳遞給視圖。

現在,將以下方法添加到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) 最后,在屬性包裝器之后,聲明類型為FetchedResultsmovies屬性。

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的數據存儲示例,感興趣的給個贊或者關注~~~

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。