前提回顾
在前几个章节中,我们已经完成了Linkworld的基本功能,但要达到最小MVP产品还有些距离。完成的功能目前只能在模拟器中使用,缺少本地化存储、网络请求和编辑、删除等功能,这尚未达到最小MVP的要求。
MVP是最小的可用产品,其核心在于最小和可用。“最小”代表着该产品可以反复迭代且迭代周期短,“可用”代表该产品是可以满足用户预期的可以交付的产品,而不仅仅是demo。
因此我们继续深入完善Linkworld这款产品,实现最小MVP产品。
MVVM架构模式:Model-View-ViewModel
通常在SwiftUi开发过程中,开发者们会使用MVVM开发架构模式进行项目搭建。MVVM全称为Model-View-ViewModel,即模型-视图-视图模型。在该架构模式中,我们建立数据模型将数据进行抽象和分离,然后单独创建视图,最后使用ViewModel进行双向绑定链接数据和视图。
在本项目中,我们创建的Model文件便是Model模型部分,而ContentView、NewView、HomePageView是View视图部分,而我们实现的单独抽离ViewModel模型视图部分内容,因此在视图与视图之间,视图与模型之间都建立了多次的变量声明和双向绑定,示例:indexURL。
下面我们来创建ViewModel视图模型并在该项目中使用它,创建一个新的Swift文件,命名为ViewModel,如下图所示:
为了便于文件管理,我们可以创建对应的文件夹用于存放不同内容的文件,使得文件目录更加清晰明了,如下图所示:
ViewModel模型视图文件作为链接数据模型和视图之前的“联络官”,需要给视图传递来自于Model的数据,因此我们可以创建一个结构体,遵循ObservableObject协议,ObservableObject协议可以在数据更新时通知视图更新内容。
然后在ViewModel模型视图内,我们使用建立发布器来创建来自Model的数组结构,如下代码所示:
import Foundation
import SwiftUI
class ViewModel:ObservableObject {
@Published var models = [Model]()
}
URLSession实例:创建网络请求
创建好ViewModel模型视图后,我们来学习使用URLSession创建一个网络请求,在学习之前我们先来了解一个JSON文件的相关知识。
JSON是一种文件格式,和TXT文本一样,JSON使用带有固定格式的纯文本创建数据集。我们在搭建NewView添加身份卡视图时曾实现了新增身份卡的方法,即赋值给符合Model格式的变量,然后将Model格式的数据添加到个人主页的List中。
JSON文件则是包含了所有符合Model格式的数据的数组集合,我们可以创建好一个JSON文件及其内容,并通过网络请求将JSON文件请求到本地并解析加载到List中。
我们先来看看JSON文件的格式,如下图所示:
[ { "platformIcon": "icon_juejin", "title": "掘金签约作者", "platformName": "稀土掘金技术社区", "indexURL": "juejin.cn/user/3897092103223517" }, { "platformIcon": "icon_aliyun", "title": "专家博主", "platformName": "阿里云社区", "indexURL": "developer.aliyun.com/profile/expert/376pj7xeqgqjy" }]
上述JSON文件是通过一个在线JSON文件生成器(如npoint.io、fastmock.site等等)生成的准备代码,其中由“[]”包裹的是数组,而“{}”包裹的是数组中的对象,我们创建了2个对象,且数据模型格式都符合Model数据模型定义的参数。
通过第三方网站可以生成一个API接口的链接地址,我们声明一个常量参数存储它,如下代码所示:
let JsonURL = "https://api.npoint.io/c51968c99a2a087dac5e"
当然,我们也可以使用测试工具测试创建的JSON文件地址返回的结果是否正确,可以使用postman或者apifox测试软件进行测试,避免在项目开发时请求数据失败而导致开发卡点的问题,如下图所示:
上图中,使用的测试工具是apifox。使用方法也很简单,创建一个接口并将刚刚生成的地输入进去,并设置好对应的Query参数,点击运行,便可查看该接口返回状态及结果。熟练使用接口测试工具,可以帮助我们节省开发时间。
回归正题,我们在ViewModel视图模型中使用URLSession实例创建一个网络请求的方法,如下代码所示:
//网络请求
func getData() {
let session = URLSession(configuration: .default)
session.dataTask(with: URL(string: JsonURL)!) { data, _, _ in
guard let jsonData = data else { return }
do {
let data = try JSONDecoder().decode([Model].self, from: jsonData)
self.models = data
} catch {
print(error)
}
}
.resume()
}
上述代码中,我们创建了一个网络请求方法getData,在方法里,我们使用URLSession创建网络请求,声明了常量session来使用URLSession API,然后加载的目标地址为声明的JsonURL,并执行下载数据,当下载成功后,将按照Model数据模型格式的数组加载到models数组里,若加载失败则打印错误信息。
然后我们可以在初始化时调用getData获得数据方法来加载数据,如下代码所示:
init(){
getData()
}
由于使用JSONDecoder编码器,因此Model数据模型还需要设置遵循可编码性 Codable 协议,才能避免报错。又因为id在JSON文件中不存在,因此需要告诉编译器使用某些CodingKeys,不包括id。如下代码所示:
enum CodingKeys : String, CodingKey {
case platformIcon, title, platformName, indexURL
}
上述代码中,我们使用枚举类CodingKeys,用来指定Model数据模型中应该接收哪些数据,用于排除请求JSON时缺少ID的问题。另外CodingKey也可以作为关键字替换的方法,当实际开发过程中后端与前端定义的字段名称不一样时,也可是使用该方法进行字段对照映射,这点后面有时间会讲到。
ViewModel模型视图创建完成后,我们来到ContentView视图,要使用ViewModel模型视图链接Model和View,我们需要使用新增一个状态变量,如下代码所示:
@StateObject var viewModel = ViewModel()
声明状态变量viewModel作用是初始化ViewModel并交由SwiftUI进行管理存储,这样我们就可以在View视图中使用ViewModel模式视图中的所有方法和变量。
紧接着,我们将原来在List使用的用@State声明的models数组替换成viewModel中的数组models,如下图所示:
如此,通过URLSession API,我们从网络请求了JSON文件数据,并把它解析到了LIst中并展示出来了。如下图所示:
方法优化:优化“添加身份卡”方法
接下来我们来优化添加身份卡的方法,在ViewModel模型视图中,我们使用@Published声明了数据模型数组models,并在ContentView页面中替换了使用@State声明的models数组。
因此我们可以将所有网络请求、新增、编辑、删除的方法都放在ViewModel模型视图里,然后在所有页面建立链接,新增方法如下代码所示:
// 创建身份卡
func addCard(newItem: Model) {
models.append(newItem)
}
上述代码中,我们创建了一个方法addCard,通过传入符合Model数据模型格式的常量newItem中,然后将newItem添加到数组models中,完成添加数据的动作。
接下来回到 NewView中,我们引用ViewModel视图模型,并替换原来使用@Binding双向绑定声明的数组models,如下代码所示:
var viewModel:ViewModel
这时候可能会报错,没有关系,这是因为原来@Binding双向绑定声明的数组models建立的关系被破坏掉了,而为什么不像ContentView那样使用@StateObject var viewModel = ViewModel()声明,这是因为在添加页面,添加的数据要加载到ContentView的List列表中,进行数据之间的传递。
添加身份卡的方法需要替换原有的models,换成viewModel视图模型中的addCard方法,如下代码所示:
self.viewModel.addCard(newItem: newItem)
并且NewView页面预览时,需要赋予变量viewModel值,如下代码所示:
NewView(viewModel: ViewModel())
上述代码中,我们使用ViewModel视图模型代替纯Model数据模型的数组,并将原来的添加身份卡方法替换为ViewModel创建好的addCard方法,为了预览当前NewView页面,还需要给NewView声明的viewModel变量赋予默认值ViewModel()。
在NewView页面调整后,由于之前我们还在引用NewView视图的相关页面进行了绑定,这时候要回到ContentView视图中,解除之前的绑定,如下图所示:
NewView(viewModel: viewModel)
如此,我们便实现了使用Model-View-ViewModel架构模式调整原有项目代码,并实现引用ViewModel视图模型中的方法来创建视图,做到将页面和数据分隔开,使得代码进一步精炼。
方法新增:增加“删除身份卡”的方法
调整完“添加身份卡”方法后,我们来补充“删除身份卡”方法。删除身份卡的方法和添加身份卡方法一样,我们可以在ViewModel视图模型中创建,如下代码所示:
// 删除身份卡片
func deleteItem(itemId: UUID) {
models.removeAll(where: {$0.id == itemId})
}
上述代码中,我们创建了一个删除身份卡片的方法deleteItem,传入身份卡片数据的UUID,作为索引找到对应的身份卡片,然后调用removeAll方法,删除models数组中对应UUID对传入的UUID的数据,实现删除数据的操作。
回到ContentView页面,我们创建一个简单的删除身份卡的交互,使用contextMenu上下文菜单按钮搭建删除交互,如下代码所示:
// 卡片视图
CardView(platformIcon: item.platformIcon, title: item.title, platformName: item.platformName, indexURL: item.indexURL)
// 长按唤起删除
.contextMenu {
Button(action: {
self.viewModel.deleteItem(itemId: item.id)
}, label: {
Text("删除")
})
}
上述代码中,我们给CardView卡片视图添加了一个上下文菜单的交互操作,当点击CardView卡片时,唤起上下文菜单,点击菜单按钮调用viewModel视图模型的deleteItem删除方法,指定删除的卡片id为点击卡片的UUID。
我们长按点击卡片预览下效果,如下图所示:
上面的方法是使用contextMenu上下文菜单实现身份卡片的方法,比较简单。
项目预览
完成后,我们在模拟器中预览下效果,如下图所示:
项目小结
在本章中,我们学习了Model-View-ViewModel模型-视图-模型视图架构模式进行项目的开发,将使用URLSession API实现网络请求,将JSON数据文件从云端请求到本地渲染,其中对于JSON文件缺少UUID的处理至关重要。
功能方法方面,我们在ViewModel视图模型中创建了addCard添加身份卡方法替换了原有在View视图中实现的操作逻辑,进一步地,我们创建了deleteItem删除身份卡方法,通过识别当前卡片的UUID,指定卡片进行删除操作。
自此,Linkworld项目的功能越来越完善了,但还不够好。接下来的章节,我们介绍另一种删除数据项的交互,以及编辑页面、排序等功能的实现,请保持期待吧~
版权声明
本文为稀土掘金技术社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!