實戰教程·元宇宙來了,準備好你的電子名片了嗎?(八)

語言: CN / TW / HK

theme: smartblue highlight: a11y-light


前提回顧

在上幾個章節中,我們完成了Linkworld基本功能的搭建,也進一步瞭解了SwiftUI這一宣告式語法的程式設計方式的魅力之處。

在本章中,我們繼續學習本地化儲存相關方法,那麼讓我們開始吧。

外鏈跳轉:開啟應用外瀏覽器

在之前的章節中,我們學習過使用WebKit在應用中開啟Web網頁的方法,這裡再補充一個知識點— —如何喚起系統瀏覽器並開啟網站。

在SwiftUI中喚起外鏈的方法是使用Link方法,和NavigationLink方式類似,NavigationLink導航跳轉是在應用內跳轉頁面,而Link則是開啟iOS本地瀏覽器並訪問指定網站。

來到HomePageView頁面,我們給建立一個新的按鈕,如下程式碼所示:

// 開啟瀏覽器按鈕 func openWebBtn() -> some View { Image(systemName: "network") .font(.system(size: 17)) .foregroundColor(.blue) }

然後我們將按鈕加到頂部導航選單中,如下程式碼所示:

.navigationBarItems(leading: backBtn(),trailing: openWebBtn())

接下來我們來實現跳轉方法,在openWebBtn中使用Link方法,如下程式碼所示:

Link(destination: URL(string: "http://"+indexURL)!){ Image(systemName: "network") .font(.system(size: 17)) .foregroundColor(.blue) }

由於需要使用到系統瀏覽器做配合,因此需要“執行”模擬器裝置上測試效果。如下圖所示:

FileManager本地化儲存

接下來我們來學習本地化儲存,將請求回來的JSON檔案資料和本地建立的資料快取起來,在下一次開啟時還可以操作上一次的資料。

我們來到ViewModel檢視模型中,鍵入下面的程式碼:

// 獲取裝置上的文件目錄路徑 func documentsDirectory() -> URL { FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] }

FileManager是本地檔案儲存管理器,用於充當檔案儲存的中間橋樑。上述程式碼中我們建立了一個方法documentsDirectory允許開發者在可存取空間userDomainMask中使用沙盒documentDirectory,並返回一個URL路徑。如此,檔案儲存器FileManager在整個應用中都可以被使用。

然後我們通過FileManager本地檔案管理器訪問放置資料的資料夾,我們可以放在plist檔案中,如下程式碼所示:

// 獲取plist資料檔案的路徑 func dataFilePath() -> URL { documentsDirectory().appendingPathComponent("Linkworld.plist") }

上述程式碼中,我們操作的便是使用FileManager本地檔案管理器獲得Linkworld.plist檔案的路徑,便於我們操作plist檔案。

寫入本地資料

確定資料夾後,我們建立一個方法將資料寫入到本次儲存中,如下程式碼所示:

//寫入本地資料 func saveItems() { let encoder = PropertyListEncoder() do { let data = try encoder.encode(models) try data.write(to: dataFilePath(), options: Data.WritingOptions.atomic) } catch { print("錯誤資訊: (error.localizedDescription)") } }

上述程式碼中,我們使用編碼器PropertyListEncoder將物件例項與XML資料格式之間進行互相轉換,作用是使得原始資料能夠在系統中進行傳輸,傳輸的資料通過dataFilePath方法進行寫入儲存中,我們將整個寫入儲存的操作建立一個方法saveItems。

我們什麼時候會使用到將資料寫入儲存呢?是的,在每次資料發生變化時。因此我們可以在ViewModel檢視模型建立的方法中呼叫saveItems方法,如下圖所示:

寫入本次儲存儲存後,我們如果使用到網路請求的方法時,還需要將網路請求回來的資料也寫入到本次儲存中,以及在頁面載入時讀取本次儲存的內容。

載入本地資料

所以我們還需要建立一個讀取本地儲存資料的方法,將上一次存起來的資料在下一次開啟時加載出來,如下程式碼所示:

``` // 載入本地資料 func loadItems() { let path = dataFilePath()

// 如果沒有資料則跳過
if let data = try? Data(contentsOf: path) {
    let decoder = PropertyListDecoder()
    do {
        models = try decoder.decode([Model].self, from: data)
    } catch {
        print("錯誤提示: (error.localizedDescription)")
    }
}

} ```

上述程式碼中,我們建立了一個讀取本地資料的方法loadItems,在loadItems方法中,我們首先判斷資料路徑是否存在,如果存在則執行使用編碼器PropertyListEncoder傳輸資料,將符合Model資料模型的資料載入到models資料集中,如果失敗則輸出列印錯誤資訊。

初始化本地資料

完成後,我們需要在應用初始化時,讀取本次資料的方法,如下程式碼所示:

init() { loadItems() }

完成後,我們來到ContentView檢視,建立一條資料並重新整理模擬器預覽,無論我們離開此頁面還是推出Xcode,資料都會被儲存在本地中,在下一次開啟時就會看得到上一次建立的資料。

CoreData資料持久化框架

接下來我們再學習一種本地資料持久化的方法,也是目前使用最多的資料持久化方法,即使用CoreData資料持久化框架。

首先我們要建立一個Data Model資料模型檔案放置在Model資料夾中,命名為CoreData,如下圖所示:

建立資料模型

然後選中CoreData資料模型,點選下面工具欄的Add Entity建立一個實體,命名為Model。並且在Model實體中定義好專案需要的屬性,如下圖所示:

由於Model實體我們重新定義了,那麼要保證Module模組要選擇CurrrentProductModule當前產品的模型,Codegen程式碼基因要選擇Manual/None,不然我們在專案中引用模型的時候可能會找不到我們定義的Model實體,如下圖所示:

建立持久儲存區檔案

模型準備完成後,下一步我們需要建立一個持久儲存區的檔案,用於儲存資料到Model中,我們在Model資料夾中建立一個新的Swift檔案,命名為Persistence,並鍵入下面的程式碼:

``` import CoreData

struct Persistence { // 一個單例供我們的整個應用程式使用 static let shared = Persistence()

// 儲存核心資料
let container: NSPersistentContainer

// 用於載入 Core Data 的初始化程式,可以選擇使用記憶體中的儲存區。
init(inMemory: Bool = false) {
    container = NSPersistentContainer(name: "CoreData")

    if inMemory {
        container.persistentStoreDescriptions.first?.url = URL(fileURLWithPath: "/dev/null")
    }

    container.loadPersistentStores { description, error in
        if let error = error {
            fatalError("Error: (error.localizedDescription)")
        }
    }
}

} ```

上述程式碼中,我們首先引用了CoreData資料持久化框架,然後建立了一個結構體PersistenceController並定義一個常量shared用於初始化。如果是專案建立之初勾選了使用CoreData,則系統會預設建立需要的檔案,上述的內容就當作固定的模版使用吧。

訪問資料庫容器

然後宣告一個新的變數container指向NSPersistentContainer資料庫容器,宣告容器後再給容器進行初始化操作。首先檢查記憶體中是否存在資料庫CoreData,如果存在則在需要時進行載入,如果載入失敗則輸出錯誤資訊。

接下來我們需要在專案中訪問資料庫容器,開啟LinkworldApp檔案,建立新的變數persistenceController賦值PersistenceController.shared,並使用環境修改器將資料庫資料傳遞給子檢視,如下程式碼所示:

``` import SwiftUI import CoreData

@main struct LinkworldApp: App {

let persistenceController = Persistence.shared

var body: some Scene {
    WindowGroup {
        ContentView()
            .environment(.managedObjectContext, persistenceController.container.viewContext)
    }
}

} ```

專案屬性匹配

緊接著,我們還要改造下Model資料模型中的檔案,使其宣告的屬性和資料庫中的屬性一一匹配,如下程式碼所示:

``` import CoreData import Foundation import SwiftUI

public class Model: NSManagedObject,Identifiable { @NSManaged public var id: UUID @NSManaged public var platformIcon: String @NSManaged public var title: String @NSManaged public var platformName: String @NSManaged public var indexURL: String

enum CodingKeys: String, CodingKey {
    case platformIcon, title, platformName, indexURL
}

} ```

上述程式碼中,由於CoreData模型類繼承自NSManagedObject協議,每個屬性都使用@NSManaged進行宣告。

引用資料庫

定義好資料模型後,我們來到ContentView檢視,我們使用@FetchRequest屬性包裝器從資料庫載入資料,並且註釋原來的@StateObject宣告的viewModel,如下程式碼所示:

``` @FetchRequest(entity: Model.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \Model.title, ascending: false)])

var models: FetchedResults ```

緊接著我們替換掉原來viewModel模式檢視資料遍歷的List列表,如下圖所示:

這時可能出現很多報錯,這是因為我們替換了原來的viewModel模型檢視中的models資料集,因此在很多使用models資料集進行資料傳輸的地方都會找不到物件而報錯。

這沒關係,我們一點點修復它。

修改NewView新建檢視

我們先來到NewView新建身份卡檢視,我們需要宣告一個環境變數來管理物件上下文,而且還需要註釋原來的viewModel檢視模型,如下程式碼所示:

@Environment(.managedObjectContext) var context

新增身份卡的方法我們也需要重新設計,並且註釋之前的程式碼,如下程式碼所示:

``` // 賦值 let newItem = Model(context: context) newItem.id = UUID() newItem.platformIcon = platformIcon newItem.title = title newItem.platformName = platformName newItem.indexURL = indexURL

// 儲存 do { try context.save() } catch { print(error) } ```

上述程式碼中,我們聲明瞭一個常量newItem來獲得Model資料庫實體的資料型別,然後給實體的引數賦值,最後呼叫save方法儲存資料。

修改EditView編輯檢視

NewView新建身份卡檢視基本完成了,我們再來到EditView編輯檢視,同理,我們宣告一個環境變數來管理物件上下文,如下程式碼所示:

@Environment(.managedObjectContext) var context @FetchRequest(entity: Model.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \Model.id, ascending: false)]) var models: FetchedResults<Model>

對於編輯儲存操作,我們也需要註釋原有的程式碼,新增新的編輯更新方法,並修改EditView_Previews繫結關係,如下程式碼所示:

``` if let editItem = models.first(where: { $0.id == model.id }) {

editItem.platformIcon = model.platformIcon
editItem.title = model.title
editItem.platformName = model.platformName
editItem.indexURL = model.indexURL

do {
    try context.save()
    self.presentationMode.wrappedValue.dismiss()
} catch {
    let nsError = error as NSError
    fatalError("Unresolved error (nsError), (nsError.userInfo)")
}

} ```

上述程式碼中,由於我們需要編輯更新資料,因此首先要獲得當前編輯資料的ID,我們通過判斷當前ID與model(傳輸過來)中的資料做匹配,匹配成功後我們進行重新賦值,最後依舊呼叫save方法儲存資料。

完成NewView新建頁面和EditView編輯頁面後,我們回到ContentView檢視,首先是CardView檢視,我們註釋原有的viewModel檢視模型相關內容,並且宣告一個全域性變數用於關聯資料,如下程式碼所示:

@Environment(.managedObjectContext) var context var model: Model

然後修改開啟編輯介面檢視的繫結關係,並且先刪除原先刪除身份卡的方法,如下程式碼所示:

// 開啟編輯彈窗 .sheet(isPresented: self.$showEditView, onDismiss: { self.showEditView = false }) { EditView(model: model) }

完成後,我們再來到ContentView的body檢視,修改NewView繫結關係和CardView繫結關係,如下程式碼所示:

// 卡片檢視 CardView(platformIcon: item.platformIcon, title: item.title, platformName: item.platformName, indexURL: item.indexURL,model: item)

上述程式碼做的事情比較繞,簡單解釋就是我們在NewView檢視和NewView檢視重新聲明瞭相關屬性或者變數,我們就需要在使用到這些檢視或者跳轉到這些檢視的地方做資料的繫結,便於資料在頁面之間傳遞。

模擬器效果預覽

完成之後,我們發現操作了下模擬器,新增身份卡後還是沒有資料,這是因為Contentview_preview結構體中注入託管物件上下文,我們給Contentview_preview結構體注入資料,我們先建立示例資料,如下程式碼所示:

``` // SwiftUI預覽的測試配置 static var preview: Persistence = { let controller = Persistence(inMemory: true)

// 示例資料
let newItem = Model(context: controller.container.viewContext)
newItem.id = UUID()
newItem.platformIcon = "icon_juejin"
newItem.title = "簽約作者"
newItem.platformName = "掘金技術社群"
newItem.indexURL = "juejin.cn/user/3897092103223517"

return controller

}() ```

然後加資料集加到Contentview_preview結構體中,如下程式碼所示:

ContentView().environment(.managedObjectContext, PersistenceController.preview.container.viewContext)

修改刪除方法

最後我們再回到CardView檢視,修改下刪除的方法,依舊需要先引入資料庫,如下程式碼所示:

``` @FetchRequest(entity: Model.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \Model.id, ascending: false)])

var models: FetchedResults ```

然後在呼叫刪除方法的地方鍵入下面的程式碼,如下程式碼所示:

``` if let deleteItem = models.first(where: { $0.id == model.id }) { context.delete(deleteItem)

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

} ```

上述程式碼中,我們和在EditView編輯頁面做的事情一樣,通過判斷當前操作的ID是models資料集中的那一項,然後呼叫delete方法刪除資料,最後呼叫save方法儲存當前操作。

專案小結

本章介紹了兩種資料持久化的方法,筆者比較推薦第二種使用CoreData框架進行資料持久化的方法,因為後期可以和iCloud進行通訊實現雲端儲存的功能,後面的章節會找機會講講這個。

以及在本章中我們將一些新增、編輯、刪除的方法都放在了檢視中,沒有好好利用MVVM結構模式,也是為了讓大家先熟悉CoreData框架的使用。我們也可以在後面自己想想如何將一些方法抽離出來,搭建ViewModel檢視模型部分,這就當作作業吧~

版權宣告

本文為稀土掘金技術社群首發簽約文章,14天內禁止轉載,14天后未獲授權禁止轉載,侵權必究!

「其他文章」