前提回顾
在上几个章节,我们完成了念头笔记的基本页面的编程,并在上一章节中完成新建笔记的交互逻辑。
这几天和读者沟通时收到反馈,念头笔记项目哪怕只有2个页面交互,但是变量的双向绑定很是麻烦,只要使用到@Binding
声明变量的视图,在所有用到该视图的页面都需要做双向绑定,这不优雅。
在本章中,基于上一章完成的新建笔记交互逻辑的基础上,我们尝试使用MVVM模式
修改代码和完善其他功能。本章内容较多,参数及方法调整可能会导致一堆临时性的BUG,请耐心学习和修改。
项目结构
首先是项目结构部分,目前我们完成了Model模型类
、ToastView吐司视图
、ContentView首页
、NewNoteView新建笔记视图
。如下图所示:
按照项目编程的习惯,我们其实在正式编程前需要建立基础的页面结构,将模型类、视图、实现方法按照文件夹分开。
在Xcode视图窗口右键,选择New Group
,创建一个新的文件夹,如下图所示:
我们将新的文件夹命名为Model
,并把Model.swift文件拖到里面,如下图所示:
同理,我们再创建一个View
文件夹、Extension
文件夹、ViewModel
文件夹,并将ContentView.swift
、NewNoteView.swift
文件放入View文件夹
中,将ToastView.swift
放入Extension文件夹
中,如下图所示:
以上便是一个基础的项目的文件结构,Model文件夹中放入需要使用的模型类,View文件夹中放入相应的页面,而ViewModel文件夹放入功能的实现逻辑和方法,这便是之后要使用MVVM
开发模式的做法。
至于其他的Swift文件,拓展的功能类可以创建名为Extension的文件夹,封装好的功能类可以放在Utils文件夹,公共类可以放在Constants文件夹......这些都是看项目和个人需要创建和使用。
Model
Model是我们的模型类,用于定义数据及其类型,由于我们需要用到MVVM开发模式,因此Model文件中只需要定义简单的参数就行了。如下代码所示:
import Foundation
import SwiftUI
struct NoteModel: Identifiable,Codable{
var id = UUID()
var writeTime: String
var title: String
var content: String
}
上述代码中,为了更好说明MVVM开发模式中的Model,我们更改NoteItem模型类名称为NoteModel
便于理解。参数重命名的方式为选择参数点击右键,选择Refactor
,选择Rename
,修改为NoteModel
。
为了使用NoteModel模型类的序列化数据,NoteModel需要遵循Codable
协议。
ViewModel
我们在ViewModel文件夹下创建一个新的Swift文件,命名为ViewModel.swift
。如下图所示:
基础概念
ViewModel是用来干什么的?
简单来说,Model是声明数据模型参数的,View是用来构建页面和基础交互的,ViewModel是用来实现基础功能的,包含念头笔记的增删改查,都是在ViewModel中实现,然后在View视图中调用,做到页面和数据分开。
而我们可以看到在ContentView首页视图和NewNoteView新建笔记视图中有很多参数是需要进行双向绑定的,如果不使用ViewModel的方式,那么页面之间都需要声明相同的参数,并做双向绑定。页面一多,那就和套娃一样,一直“回绑”。
我们创建一个ViewModel
类,并遵循ObservableObject
协议,如下代码所示:
import Combine
import Foundation
import SwiftUI
class ViewModel: ObservableObject {
}
上述代码中,我们引用Combine
框架,Combine为应用处理事件(增删改查)提供了一种声明性的方法。然后我们创建了一个ViewModel类,遵循ObservableObject协议,ObservableObject协议可以在视图外绑定自定义的对象,便于开发者使用。
参数声明
在ViewModel类中,我们声明需要用到的参数
,如下代码所示:
class ViewModel: ObservableObject {
//数据模型
@Published var noteModels = [NoteModel]()
//笔记参数
@Published var writeTime: String = ""
@Published var title: String = ""
@Published var content: String = ""
@Published var searchText = ""
//判断是否正在搜索
@Published var isSearching:Bool = false
//判断是否是新增
@Published var isAdd:Bool = true
//打开新建笔记弹窗
@Published var showNewNoteView:Bool = false
//打开编辑笔记弹窗
@Published var showEditNoteView:Bool = false
//打开删除确认弹窗
@Published var showActionSheet:Bool = false
//提示信息
@Published var showToast = false
@Published var showToastMessage: String = "提示信息"
}
上述代码中,noteModels
为引用NoteModel模型类数据,构建数组。
然后是念头笔记需要用到的参数writeTime
、title
、content
,搜索栏用到的参数searchText
。当搜索时,可能会由于关键字搜索为空,导致搜索列表变成“缺省图”模式,因此还需要声明一个参数isSearching
,判断当前是否处于搜索状态。
很多笔记App开发都会把新建页面和编辑页面分开写,包括网上下载下来的代码基本都是新增、编辑两个页面,而两个页面使用相同的代码。亦或者是干脆就没有编辑页面,只能新增、删除,不能编辑,这都不够优雅。
因此,为了共用页面,我们声明了3个Bool类型的参数isAdd
、showNewNoteView
、showEditNoteView
。
isAdd
用来 判断当前是新增操作还是编辑操作,showNewNoteView
用来绑定打开新增笔记弹窗的触发条件,showEditNoteView
用来绑定编辑弹窗的触发条件。
然后是删除操作,删除操作也需要声明参数触发,这里声明的参数名为showActionSheet
。
最后是Toast
提示部分,使用到的两个参数showToast
是否展示Toast,以及showToastMessage
提示信息内容,我们也在ViewModel里声明。
如此,我们便把所有页面用到的参数都抽离出来,后面就不需要在所有页面都声明一样的变量,且保持代码清晰。
功能方法
下面我们来创建一些念头笔记用到的方法,在之前的章节中我们实现了新建笔记的功能,但当我们每次重新打开APP时,它又会“恢复”到初始模式,在上一次操作的数据全部清空了。
这是因为我们只是完成了简单的操作而已,而没有实现其核心功能,即把数据存起来。但是我们没有数据库也没有云端,数据存在哪里呢?是的,放在本地,放到本地缓存起来。
在我们创建iOS
项目时,系统会创建一个plist
文件,作为缓存区,我们可以将数据暂时存储在这里。
加载数据
在ViewModel
类中,我们需要使用到的基本方法如下代码所示:
//初始化
init() {
loadItems()
saveItems()
}
// 获取设备上的文档目录路径
func documentsDirectory() -> URL {
FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
}
// 获取plist数据文件的路径
func dataFilePath() -> URL {
documentsDirectory().appendingPathComponent("IdeaNote.plist")
}
// 将数据写入本地存储
func saveItems() {
let encoder = PropertyListEncoder()
do {
let data = try encoder.encode(noteModels)
try data.write(to: dataFilePath(), options: Data.WritingOptions.atomic)
} catch {
print("Error writing items to file: (error.localizedDescription)")
}
}
// 从本地存储加载数据
func loadItems() {
let path = dataFilePath()
if let data = try? Data(contentsOf: path) {
let decoder = PropertyListDecoder()
do {
noteModels = try decoder.decode([NoteModel].self, from: data)
} catch {
print("Error reading items: (error.localizedDescription)")
}
}
}
上述代码中,首先创建了一个方法documentsDirectory
获取设备上的文档目录路径,再指定要获取的目录IdeaNote.plist
的方法dataFilePath
。
获得本地设备目录后,使用saveItems
方法将noteModels
数组中的数据写入到到本地存储中,当App打开的时候,使用loadItems
方法让List
列表从本地存储中加载数据遍历列表。
最后在我们App初始化加载的时候,调用loadItems
加载数据方法,并调用saveItems
方法将数据写入缓存中。
以上是加载本地数据的基本方法。
增删改查
紧接着,我们来实现念头笔记的增删改查的基本方法,如下代码所示:
// 创建笔记
func addItem(writeTime: String, title: String, content: String) {
let newItem = NoteModel(writeTime: writeTime, title: title, content: content)
noteModels.append(newItem)
saveItems()
}
// 获得数据项ID
func getItemById(itemId: UUID) -> NoteModel? {
return noteModels.first(where: { $0.id == itemId }) ?? nil
}
// 删除笔记
func deleteItem(itemId: UUID) {
noteModels.removeAll(where: { $0.id == itemId })
saveItems()
}
// 编辑笔记
func editItem(item: NoteModel) {
if let id = noteModels.firstIndex(where: { $0.id == item.id }) {
noteModels[id] = item
saveItems()
}
}
// 搜索笔记
func searchContet() {
let query = searchText.lowercased()
DispatchQueue.global(qos: .background).async {
let filter = self.noteModels.filter { $0.content.lowercased().contains(query) }
DispatchQueue.main.async {
self.noteModels = filter
}
}
}
由于列表中每一行的数据都有对应的ID,因此除了新增数据以外,删改查的基本功能都是通过数据的ID操作的,计算机都是先找到数据,再对数据进行操作。
新建笔记的方法addItem
,通过传入对应参数的值,然后将参数的值赋值给NoteModel模型类,再通过append
添加的方法添加到noteModels数组中,最后调用saveItems方法保存到本地。
删除笔记的方法deleteItem
,需要先获取到指定行数据的ID,这里抽离出了获得数据ID的方法getItemById,通过传入数据ID与NoteModel模型类的ID进行匹配就可以知道是哪一条数据,再调用removeAll
删除noteModels数组中指定ID的数据,最后调用saveItems方法保存操作。
编辑笔记的方法editItem
,也是传入符合NoteModel模型类的数据,找到它的ID,最后调用saveItems方法保存到本地。
搜索笔记的方法searchContet
,先定义用户搜索内容为searchText,再拿用户输入的内容和noteModels数组中的content内容数据做对比,如果符合,就返回符合的数据到noteModels数组中。
其他方法
除此之外,我们将原来在新建笔记页面使用到的获得当前时间的方法,包括判断输入内容是否为空的方法也纳入到ViewModel
里面,如下代码所示 :
// 获取当前系统时间
func getCurrentTime() -> String {
let dateformatter = DateFormatter()
dateformatter.dateFormat = "YYYY.MM.dd"
return dateformatter.string(from: Date())
}
// 判断文字是否为空
func isTextEmpty(text:String) -> Bool{
if text == "" {
return true
} else {
return false
}
}
判断内容是否为空的方法是传入一个参数值,通过判断是否为空,从而返回一个Bool类型的值,后面我们用来判断输入的标题和内容是否为空。
App使用
创建好ViewModel后,当我们要使用它,需要在IdeaNoteApp
项目页面声明好使用的ViewModel,如下代码所示:
import SwiftUI
@main
struct IdeaNoteApp: App {
@StateObject var viewModel: ViewModel = ViewModel()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(viewModel)
}
}
}
IdeaNoteApp
页面是整个App启动时的页面,它相当于我们的主函数,当前项目指向的首页是ContentView
首页视图,因此在使用MVVM
项目开发模式时,当我们用使用到ViewModel
时,就需要引用ViewModel到主函数中方可使用。
本章小结
恭喜你,准备工作已经就绪,随时可以开始下一步的内容。
当然,为了让大家更好地吸收学习内容,本章我们就分享了如何完成Model、View Model部分,下一章节中,我们将继续完成View视图的相关内容,我们将在原来View视图的基础上进行内容调整,方便大家更好地理解SwiftUI的运作模式。
快来动手试试吧~
版权声明
本文为稀土掘金技术社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!