实战教程·元宇宙来了,准备好你的电子名片了吗?(八)

语言: 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天后未获授权禁止转载,侵权必究!

「其他文章」