75142913在线留言
CS193p2021学习笔记第十一课:Error Handling 错误处理与Persistence持久化_IOS开发_网络人

CS193p2021学习笔记第十一课:Error Handling 错误处理与Persistence持久化

Kwok 发表于:2021-08-30 09:30:07 点击:2 评论: 0

在我们使用持久化(将数据或者缓存保存到设备上面,以方便下次打开还可以继续使用这些数据)的时候会出现很多错误处理的问题,如网络超时、设备写入、读取权限、设备空间大小等问题都可能导致功能无法使用,所以我们为了避免让APP出现闪退的情况,就需要避开这些错误并友好的提醒用户。

一、抛出错误 Throwing errors

在前面的使用中我们就看到了很多用throws关键字标记的函数,这意味着他们是可能会抛出错误的,我上一课遇到时使用了"try?"忽略了错误,本课就要来学习怎么捕获到被throws抛出的错误。

1、自定义错误

我们先来看一下如果要在项目中使用自己定义的错误:

//一个使用throws关键字的(参加讲座)函数
func attendLecture() throws{
    if sleptIn{ //如果学生睡着了
        throw cs193pError.missedLecture//抛出一个错过课程的错误
    }
    askQuestions()//执行其它函数(上课并回答问题)
    ...
}
//定义错误需要符合 Error 协议,不需要对此协议做额外的事情
enum  cs193pError: Error{
   case lastHomework(daysLate: Int)//迟交作业(Int次)
   case missedLecture//错过课程
}

2、调用可抛出错误的函数

上面我们定义好了错误与将会抛出错误的函数。现在我们将调用 这个函数,和我们平时直接使用有一些不同,我们将使用try关键去尝试调用这个函数。我们有3种方式去调用:

a. 忽略错误: if let imageData = try? Data(contentsOf: url) { ... } 在上一课中我们已使用过了,如果有错误抛出imageData的值将会是nil,并不会对错误进行处理。只是过不了if这一关,后面的代码将不会被执行。

b.强制执行: 错误返回的是一个Optional类型,我们可以使用强制解包的方式让代码强制去执行 try! data.write(to: url) 如果强制解包失败,app将闪退,除非你觉得这行代码永远不会出现问题,否则使用这种方式需要格外小心,最好在注释里Mark一下方便以后查找。

c.使用 do {} catch 捕获错误:在其它编辑语言中最常见的捕获错误的方式,用法也差不多:

do {
   //执行将抛出错误的代码,do的作用在这里类似if error == nil 
} catch let error{
   //如果有错误抛出将执行此代码块,并将错误记录到let error里
}

 这是捕获处理错误的最基本几种框架,当然在处理大量可能会出现的错误里,我们需要多层catch let来捕获各种错误:

do {
   //执行代码 if(..){ 这里 }
   print("没有捕获到错误我才会执行哦~")
} catch cs193pError.lateHomework(let daysLate){
   //捕获抛出的错误是否与枚举里选项一致 case lateHomework
   //错误:迟交作业if ... else if( 这里 )
} catch let cs193pError where cs193pError is cs193pError{
   // cs193pError is cs193pError的意思是前一个变量名,后一个是类型名
   //这是swift内置的检查前一个变量是否属于后个类型的方式
   //某一个变量是否属于某种类型
   //定义(let) cs193pError 条件(where) (变量cs193pError) 是 (cs193pError类型)
   //这种方式也可以在if then里使用哦~
   //捕获所有 cs193pError 错误 if ... else if( 这里 )
} catch{
   //捕获到 cs193pError.missedLecture
   //捕获非 cs193pError 的其它错误 if ... else{ 这里 }
}
print(" 继续运行 ") //不管上面是否捕获到错误,此行代码将会正常运行

二、持久性 Persistence(9:53)

在IOS里让数据持久化是有很多种方法的,我们可以使用文件系统(FileManager)、CoreData(一个IOS内置的SQL 数据库)、CloudKit(将数据存在iCloud中)、自己的远程服务器(国内基本上都是这个)和 UserDefaults (Key Value的存储方式)等这几种常见的方式。

1、File System 文件系统(12:20)

File System可以访问Unix文件系统,路径以(/)开头,IOS限制了跨APP访问,所以我们开发的APP只能访问系统给分配的文件区域(沙箱机制),所以我们只能从规划的沙箱目录里读取、写入自己APP的数据。

沙箱的目的是防止跨APP读取数据以保护其它APP的安全。

当用户卸载APP的时候,随之也会将当前沙箱里的内容全部删除(不会像windows留下一堆垃圾),沙箱里有很多的目录,有执行程序所在的位置,有Assets.xcassets文件中的JPEG资源,颜色等,但这个目录是只读的。另一个重要的目录是Documents目录,这是用户将其视为应用程序文档存放的地方,但并非所有应用程序都有文档(根据开发入口代码确定)。还有一些其它的目录,可以通过查看文档知道其用途。

2、访问沙箱里的文件

我们通过FileManager的URL访问沙箱里的文件。我们通过使用FileManager.default 这个共享的方式来访问沙箱里的文档。FileManager是线程安全的方式,在主队列中使用FileManager.default是最好的方式。 

let manager = FileManager.default //定义一个FileManager
let url = manager.urls(for: .documentDirectory, in: .userDomainMask).first//通过url访问

.documentDirectory (.applicationSupportDirectory或者.cachesDirectory)是上面所说的Documents目录(名字来源于文档查询),.userDomainMask在IOS里没有意义,是应用在MAC OS中的可以通过查询文档看到还有一些其它选项。可以看到urls是一个复数,所以我们得到的是整个目录下的所有文件的集合,当我们获取到了目录的url就可以创建、访问其中的文件 。

URL 将获取到下面2个方法来构建目录和文件:

func appendingPathComponent(String) -> URL //以路径方式附加到指定目录并返回URL func 
appendingPathExtension(String) -> URL //以扩展名的方式附加指定目录并返回URL

通过url关键字查找到相关的文件:

var isFileURL: Bool //是否为文件的URL判断状态
func resourceValues(for keys: [URLResourceKey]) throws -> [URLResourceKey:Any]?//通过URL资源的关键字查找文件

URLResourceKey 查找的关键字为:.creationDateKey(创建时间), .isDirectoryKey(是否为目录), .fileSizeKey (文件大小)

3、Data (18:18)

我们通过Data向文件系统或者文件系统中的文件写入实际性的内容(创建并写入内容 -> 保存文件到沙箱),Data也是一种基本的数据类型,和String、Int一样,Data是字节码(byte)的方式将数据读取或写入的。

a.获取数据

在上一节课里我们学习了通过拖拽图片到画布触发Data远程将图片下载,这不是Data的主要工作,其主业是从本地URL中获取数据,我们需要使用和上一课一样的方式:

Data init(contentsOf:URL, options:Data.ReadingOptions) throws //除了从互联网获取也可以通过本地的URL获取到数据
contentsOf参数是远程/本地的URL地址,而第二个参数options总是为[],Data函数会抛出错误,按照上面学习过的方式对错误进行处理。

b.写入数据

写入数据需要调用Data下面的write方法,我们只需要一个数据的Blob,在它上面调用 函数的 write(to url:URL)写进去。这只能适用于本地文件的URL。

func writ(to url: URL, options: Data.WritingOptions) throws -> Bool //向URL里写入数据,并返回结果,出错错误将被抛出

使用方式try? bolb.writ(to:URL) 类似这样就可以将blob里的数据写入到本地的URL路径里了。

FileManager还有一些其它实用的功能可调用,如fileExists(atPath: String) -> Bool 检测文件是否存在,移动、重命名等,如果需要详细了解文件操作了一些方法,需要查看官方文档。

4、Codable 可编码协议

编码的意义是让系统知道我们的内容里有哪些数据通过什么样的方式打包保存到文件系统里面去。在SwiftUI有一个名叫Codable的协议来处理这些事务,我们自己定义的数据结构只要符合了该协议就可以使其成为一个blob然后使用上面的writ方式归档打包保存到文件系统里。

在swift里很酷的是任何结构的变量都是Codable本身,swift会自动为你实现Codable。我们要做的就是在定义的struct上使用Codable即可。当我们结构体里嵌套了一个子结构体(上一个Memorize里的Card就是),而这些子结构也需要标记为Codable,只要是所有东西都是Codable系统就会自己完成blob的编码的工作,并可以存放到本地硬盘上。

如果有一个枚举数据,swift也会为枚举进行Codable的操作,但是如果枚举具有关联值,则不会这样做。需要我们手动完成编码工作。在后面的演示中我们会手动实现编码工作。

5、获取blob的方式

上面我们多次提到了blob,那么我们怎么将数据转为blob呢?这将有几种方式可以做了,最常见的就是将其编码为某种文件的格式:

let object:MyType = ...//我们定义的结构体需符合Codable协议
let jsonData:Data? = try? JSONEncoder().encode(object)//调用JSONEncoder下面的encode方式将object编码成Json格式

注意:json是互联网API传输中最常见的一种方式,在对其编解码过程中会抛出错误,我们正常情况应该使用do catch对其操作。

jsonData将是我们对象作为数据blob的JSON表示,因为可能会出现错误,所以这是一个Optional类型。

let jsonString = String(data: jsonData!, encoding: .utf8) //json是一种文本编码格式

我们可以调用 String 的data 将Data blob返回给jsonString查看其内容(将blob转为String)。在swift里所以数据都可以尝试转换为类型的新变量来进行。 JSON始终采用UTF8的编码来存储数据。我们拥有了josn数据的blob就可以调用上面说的write方法将数据写入到URL路径里。

try jsonData.write(to: url)//将抛出错误

我们还可以通过上面说的解码将json还原成我们的结构体数据,目前此方式应用于大小APP的远程数据交换。

if let myObject:MyType = try? JSONDecoder(),decode(MyType.self, from:jsonData!){ ... }//将json数据转换为MyType

6、编码过程中错误处理

这是通过Decoding时捕获的错误并对其进行处理。

do{
    let object = try JSONDecoder().decode(MyType,self, from: jsonData!)
} catch DecodingError.keyNotFound(let key, let context) {
    print("在JSON里不能找到 key (key) :(context.debugDescription)")
} catch DecodingError.valueNotFound(let type, let context) {
    print("未找到值")
} catch DecodingError.typeMismatch(let type, let context) {
    print("类型不匹配")
} catch DecodingError.dataCorrupted(let context) {
    print("数据损坏")
}

7、编码实例

上面对编解码进行了一系列的理论知道说明,这里我们将使用一个自己定义的MyType结构体进行编码操作演示:

struct MyType: Codable{
    var someDate:Date //日期类型
    var someString: String //字符串
    var other: SomeOtherType //其它符合Codable的数据,也可以是我们的子结构体
}

上面结构体里的Date与String都是系统内置的标准类型,都是符合Codable的,需要说明的是SomeOtherType是我们再次定义的结构体,所以也需要符合Codable才能进行编码操作。

上面的数据编码会将得到下面的内容:

{
    "someDate" : "2021-08-30T13:37:32:00Z",
    "someString" : "neter8.com 网络人",
    "other" : {
        "otherString" : "这里是子结构里的String",
        "otherData" : "2021-08-30T13:38:12:00Z"
    }
}

在json里的Key我们都是使用小写的字母,单词间使用下划线隔开,这与swift语法相违背,所以我们需要使用一个私有的枚举来控制jsonKey的显示。

//要修改json的key需要符合CodingKey协议
private enum CodingKeys : String, CodingKey {
    case someDate = "some_date" //编解码时会相互替换
    case other //其它要改的Key和上面一样设置即可
}

这里的枚举必须是String类型,并符合CodingKey协议,还要设置为私有。关于视频的手动编码方式可以查看视频第27分钟处左右。

三、UserDefaults 持久化(28:58)

上面学习了通过Codable协议对数据进行编码,它也可以与UserDefaults系统相结合使用。UserDefaults是一个轻量级的数据存储系统,类似CooKies、Redis的Key -> Value存储一样。这是一个早于swift语言诞生之前的古老API。这意味着使用它的时候会遇到swift中不存在的概念。所以我们需要仔细的阅读文档以了解它的使用方法。

1、属性列表 Property Lists(30:26)

你只能在UserDefaults中存储所谓的“属性列表”。属性列表不是协议、或者结构体、或者其它swift的数据结构。属性列表只是一系列可用类型的总称:里面有String,Int,Bool,浮点数(Flost、DouBle),Date,Data,Array 或者 Dictionary。

所以要存储到UserDefaults里的内容只能是上面列出来的swift类型的组合(具有一定的局限性,好处是可以快速使用保存一些临时的数据)。

看似可保存的数据类型是有限的,但里面有一个很大的漏洞,Data在上面我们学习过发现是一个Codable的东西,所以基本上我们转为Data就可以和其它存储没有区别了。

2、Any类型

前面说了UserDefaults中与swift不同的概念就是这个Any类型,Any的意思就是无类型的,但swift是强类型语言。所以拥有一个无类型的Any和swift不是同一个概念。

这里的Any的概念就是PHP里的Array,这个数组可以将上面属性列表里的所有类型混合存放在一个数组里。这将会在演示说使用Any存储并使用(as?)将数据转换回swift里的类型。

3、使用UserDefaultsd存储数据

首先我们需要初始化一个UserDefaults实例:

let defaults = UserDefaults.standard //定义一个UserDefaults实例

如果只需要在字典中存储一些东西可以直接使用:

defaults.set(object, forKey: "SomeKey")//object 必须是属性列表里的类型

也可以设置一个Double值:

defaults.setDouble(3.14, forKey: "pi")

4、使用UserDefaultsd读取数据

let i: Int = defaults.integer(forKey: "MyInteger") //从MyInteger里获取一个整数
let b: Data = defaults.data(forKey: "MyData") //从MyData里获取一个Data
let u: URL = defaults.url(forKey: "MyURL") //从MyURL里获取一个路径或者网址
let strings: [String] = defaults.stringArray(forKey: "MyStrings") //从myString里获取一个字符串数组

四、文档的持久化演示(35:48)

我们将通过上面的理论学习后在演示中对其编码,在演示中主要做2件事,保存我们的EmojiArt文档,然后还会在EmojiArt中实现另一个MVVM的ViewModel。所以EmojiArt将会是一款具有两个重要MVVM的应用程序。画布里的背影和Emoji的MVVM与底部的PaletteChooser调色板的MVVM,它将有多个调色板等。

1、文件持久化

首先我们需要定义一个函数来保存数据到本地的url里:

//将blob保存到本地url
private func save(to url: URL) {
    let thisfunction = "(String(describing: self)).(#function)" //使用哪个函数时调用出错(57:55)
    do {
        let data: Data = try emojiArt.json()//json在Model里实现
        print("(thisfunction) json = (String(data: data, encoding: .utf8) ?? "nil")") //打印被编码后的json
        try data.write(to: url)//尝试写入到本地路径
        print("(thisfunction) success!")//xx函数写入成功
    } catch let encodingError where encodingError is EncodingError {
        //EncodingError是一种编码时才出现的错误类型,当然还有DecodingError(59:59)
        print("(thisfunction) 无法将EmojiArt编码为JSON,因为: (encodingError.localizedDescription)")
    } catch {
        //非EncodingError错误的情况
        print("(thisfunction) 错误: (error)")
    }
}

2、去实现emojiArt.json()方法

我们通过调用Model里定义的json方法将数据转为json格式的Blob:

//返回JSON编码后的self数据并可能抛出错误
func json() throws -> Data {
    let encoder = JSONEncoder()//swift版本更新encode已不是静态的所以需要初始化一个实例再使用
    return try encoder.encode(self) //需要所有成员都必须实现Codable协议
}

被返回的self里所有嵌套的子结构、枚举都必须符合 Codable 协议,所以在需要 EmojiArtModel:Codable 、 Emoji:Identifiable,Hashable ,Codable,但enum Background: Equatable, Codable的编、解码是需要手动完成的:

3、enum Background 手动编码以符合 Codable 协议(42:39)

我们只要在enum Background: Equatable加上了Codable马上就会提示 有“关联值”的枚举不能自动完成Codable协议,所以我们点击fix自动增加了一个init和encode方法:

//初始化init实现Decoder以符合Codable协议
init(from decoder: Decoder) throws {
    //try 尝试将已编码的数据CodingKeys解码
    let container = try decoder.container(keyedBy: CodingKeys.self)
    //先假设被编码的数据是URL类型
    if let url = try? container.decode(URL.self, forKey: .url){
        self = .url(url) //当前枚举的值为.url
    } else if let imageData = try? container.decode(Data.self, forKey: .imageData){
        //再假设被编辑的数据是Data类型
        self = .imageData(imageData)
    }else{
        self = .blank //以上都不对的情况
    }
}
//实现Encoder以符合Codable协议
func encode(to encoder: Encoder) throws {
    var container = encoder.container(keyedBy: CodingKeys.self)
    switch self {
        case .url(let url):try container.encode(url, forKey: .url)
        case .imageData(let data):try container.encode(data, forKey: .imageData)
        case .blank: break
    }
}
//定义一个CodingKeys方便编码调用
private enum CodingKeys: String,CodingKey {
    case url = "theURL"
    case imageData
}

理解上面的代码是本课的重点内容,需要结合上面的理论知识。现在我们为文档增加了一个保存功能,现在我们需要再增加一个自动保存autosave()的功能来调用这个保存save()功能。

4、自动保存 autosave() (1:04:08)

当我们的数据emojiArt被修改后就立即调用这个自动保存的功能,所以我们需要在属性观察者didSet里增加一个aotusave()代码。下面我将将实际这个函数:

//自动保存
private func autosave() {
    //尝试获取 Autosave.url 路径(url有可能为nil)
    if let url = Autosave.url {
        save(to: url)//调用save函数
    }
}

这里的Autosave是一个结构体(1:05:30):

//自动保存时获取url路径
private struct Autosave {
    static let filename = "Autosaved.emojiart"//被保存的默认名称
    //当使用Autosave.url时获取到文档保存的URL路径(Optinal类型)
    static var url: URL? {
        //获取到沙箱里的document目录的url
        let documentDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first //这是一个跨平台的功能
        return documentDirectory?.appendingPathComponent(filename) //返回将文件名追回进入的URL(有可能是nil)
    }
    static let coalescingInterval = 5.0 //保存间隔时间
}

关于FileManager的使用可以查看上面的理论知识部分有说明。现在我们已可以自动保存文档了。下一步需要在程序启动初始化时从本地恢复文档的数据。

5、自动恢复数据

我们需要在Model里增加json: Data与URL的的初化init:

//初始化时传入 Data 格式 并可能抛出错误
init(json: Data) throws {
    //尝试将Data 解码成功的数据恢复给 self
    self = try JSONDecoder().decode(EmojiArtModel.self, from: json)
}
//初始化时传入本地URL路径(并可能抛出错误)
init(url: URL) throws {
    let data = try Data(contentsOf: url) //尝试从本地的 url 路径获取(这里将阻塞主线程时间极短)
    self = try EmojiArtModel(json: data)//然后再调用上面的init(json: Data)初始化 解码
}

这2个init看着有点绕圈子,实际我们通过传入一个本地的url给下面的init,尝试获取url路径里的数据,然后先将self赋值,成功后self将会被解码后的数据给替换掉。

现在我们只需要有ViewModel里去调用这个初始化即可:

init() {
    //尝试获取路径,并通过路径url初始化Model
    if let url = Autosave.url, let autosavedEmojiArt = try? EmojiArtModel(url: url) {
        emojiArt = autosavedEmojiArt //恢复初始化成功的数据
        fetchBackgroundImageDataIfNecessary()//抓取远程图片
    } else {
        emojiArt = EmojiArtModel() //原来的初始化
    }
}

现在我们尝试启动模拟器测试一下,一切正常。

6、实现定时保存(1:18:30)

现在我们每次对画板修改都会调用自动保存,为了让效率更高我们将使用定时保存,并间隔5秒。

我们将原来emojiArt里didSet里的autosave()修改为scheduleAutosave(),然后我们来实现这个函数并赋予定时保存的功能:

private var autosaveTimer: Timer? //定义一个计时器
//通过计时器实现间隔自动保存
private func scheduleAutosave() {
    autosaveTimer?.invalidate() //停止计时器的再次触发,并请求将其从运行循环中删除。
    //Timer.scheduledTimer建一个计时器,并以默认模式在当前运行循环上调度它。
    autosaveTimer = Timer.scheduledTimer(withTimeInterval: Autosave.coalescingInterval, repeats: false) { _ in
        self.autosave()//调用自动保存        
    }
}

这样我们在5秒内不管有多少次更改都不会再次调用自动保存功能,超过5秒才会执行自动保存的工作。其工作机制是当我们对画板更改后5秒才会触发自动保存工作,假设我们一直5秒内修改,哪么自动保存 不会被调用 。

五、多个表情选择器(1:23:30)

现在我们只有一个默认的选择器,我们需要不同主题的选择器来满足画板的需要,所以我们将扩展表情选择器。这也是本程序的第二个MVVM,这与画板的MVVM完全分离。

1、新建一个属于Palette的ViewMode文件 PaletteStore.swift 并写入以下内容:

import SwiftUI
//定义 调色板Model
struct Palette: Identifiable, Codable {
    var name: String //调色板名称
    var emojis: String //调色板里有哪些表情(多个)
    let id: Int //符合 Identifiable 协议
    //初始化
    fileprivate init(name: String, emojis: String, id: Int) {
        self.name = name
        self.emojis = emojis
        self.id = id
    }
}
//定义调色板ViewModel
class PaletteStore: ObservableObject {
    let name: String //调色板名称(动物、万圣节、交通工具这样的主题名称)
    //我们的调色板有多个,所以使用数组
    @Published var palettes = [Palette]() {
        didSet {
            storeInUserDefaults()//更改后将自动保存到UserDefaults
        }
    }
    //返回userDefaultsKey
    private var userDefaultsKey: String {
        "PaletteStore:" + name //保存格式为PaletteStore:调色板名
    }
    //自动将编码后的数据保存到UserDefaults
    private func storeInUserDefaults() {
        UserDefaults.standard.set(try? JSONEncoder().encode(palettes), forKey: userDefaultsKey)
        //UserDefaults.standard.set(palettes.map { [$0.name,$0.emojis,String($0.id)] }, forKey: userDefaultsKey)//没有使用Codable就需要这样的方式去存,视频(1:34:39)
    }
    //从UserDefaults自动恢复数据
    private func restoreFromUserDefaults() {
        //使用Codable后代码变少更易读
        if let jsonData = UserDefaults.standard.data(forKey: userDefaultsKey),
           let decodedPalettes = try? JSONDecoder().decode(Array<Palette>.self, from: jsonData) {
            palettes = decodedPalettes
        }
        //未使用Codable所以下面的代码需要转化而代码量多,视频(1:37:16)
        //        if let palettesAsPropertyList = UserDefaults.standard.array(forKey: userDefaultsKey) as? [[String]] {
        //            for paletteAsArray in palettesAsPropertyList {
        //                if paletteAsArray.count == 3, let id = Int(paletteAsArray[2]), !palettes.contains(where: { $0.id == id }) {
        //                    let palette = Palette(name: paletteAsArray[0], emojis: paletteAsArray[1], id: id)
        //                    palettes.append(palette)
        //                }
        //            }
        //        }
    }
    //初始化
    init(named name: String) {
        self.name = name
        restoreFromUserDefaults() //自动恢复
        //如果没有调色板就导入默认的数据
        if palettes.isEmpty {
            insertPalette(named: "Vehicles", emojis: "🚙🚗🚘🚕🚖🏎🚚🛻🚛🚐🚓🚔🚑🚒🚀✈️🛫🛬🛩🚁🛸🚲🏍🛶⛵️🚤🛥🛳⛴🚢🚂🚝🚅🚆🚊🚉🚇🛺🚜")
            insertPalette(named: "Sports", emojis: "🏈⚾️🏀⚽️🎾🏐🥏🏓⛳️🥅🥌🏂⛷🎳")
            insertPalette(named: "Music", emojis: "🎼🎤🎹🪘🥁🎺🪗🪕🎻")
            insertPalette(named: "Animals", emojis: "🐥🐣🐂🐄🐎🐖🐏🐑🦙🐐🐓🐁🐀🐒🦆🦅🦉🦇🐢🐍🦎🦖🦕🐅🐆🦓🦍🦧🦣🐘🦛🦏🐪🐫🦒🦘🦬🐃🦙🐐🦌🐕🐩🦮🐈🦤🦢🦩🕊🦝🦨🦡🦫🦦🦥🐿🦔")
            insertPalette(named: "Animal Faces", emojis: "🐵🙈🙊🙉🐶🐱🐭🐹🐰🦊🐻🐼🐻‍❄️🐨🐯🦁🐮🐷🐸🐲")
            insertPalette(named: "Flora", emojis: "🌲🌴🌿☘️🍀🍁🍄🌾💐🌷🌹🥀🌺🌸🌼🌻")
            insertPalette(named: "Weather", emojis: "☀️🌤⛅️🌥☁️🌦🌧⛈🌩🌨❄️💨☔️💧💦🌊☂️🌫🌪")
            insertPalette(named: "COVID", emojis: "💉🦠😷🤧🤒")
            insertPalette(named: "Faces", emojis: "😀😃😄😁😆😅😂🤣🥲☺️😊😇🙂🙃😉😌😍🥰😘😗😙😚😋😛😝😜🤪🤨🧐🤓😎🥸🤩🥳😏😞😔😟😕🙁☹️😣😖😫😩🥺😢😭😤😠😡🤯😳🥶😥😓🤗🤔🤭🤫🤥😬🙄😯😧🥱😴🤮😷🤧🤒🤠")
        }
    }
    
    // MARK: - Intent 用户意图
    //通过索引 找到 调色板
    func palette(at index: Int) -> Palette {
        let safeIndex = min(max(index, 0), palettes.count - 1)
        return palettes[safeIndex]
    }
    
    //移除一个调色板
    @discardableResult //可废弃的结果
    func removePalette(at index: Int) -> Int {
        if palettes.count > 1, palettes.indices.contains(index) {
            palettes.remove(at: index)
        }
        return index % palettes.count
    }
    //插入一个调色板
    func insertPalette(named name: String, emojis: String? = nil, at index: Int = 0) {
        let unique = (palettes.max(by: { $0.id < $1.id })?.id ?? 0) + 1
        let palette = Palette(name: name, emojis: emojis ?? "", id: unique)
        let safeIndex = min(max(index, 0), palettes.count)
        palettes.insert(palette, at: safeIndex)
    }
}

2、为程序增加ViewModel:

在上面我们增加了一个Model + ViewModel组成的文件,像第九课一样,需要在程序入口将ViewModel初始化并传入View里。

打开EmojiArtApp.swift 在里面增加:

private let paletteStore = PaletteStore(named: "Default")//palette的ViewModel与View的接口

六、课后总结

本课的理论部分比较长,主要讲的是持久化的几种方式与使用方法,然后在我们的演示中将持久化、错误处理都用上了。

除非注明,网络人的文章均为原创,转载请以链接形式标明本文地址:http://www.neter8.com/ios/156.html
标签:错误处理持久化cs193pKwok最后编辑于:2021-09-02 10:02:12
0
感谢打赏!

《CS193p2021学习笔记第十一课:Error Handling 错误处理与Persistence持久化》的网友评论(0)

本站推荐阅读

热门点击文章