前提回忆

在上几个章节,咱们完结了想法笔记的根本页面的编程,并在上一章节中完结新建笔记的交互逻辑。

这几天和读者交流时收到反应,想法笔记项目哪怕只需2个页面交互,可是变量的双向绑定很是费事,只需运用到@Binding声明变量的视图,在所有用到该视图的页面都需求做双向绑定,这不高雅。

在本章中,根据上一章完结的新建笔记交互逻辑的根底上,咱们尝试运用MVVM形式修正代码和完善其他功用。本章内容较多,参数及办法调整可能会导致一堆临时性的BUG,请耐心学习和修正。

项目结构

首要是项目结构部分,现在咱们完结了Model模型类ToastView吐司视图ContentView主页NewNoteView新建笔记视图。如下图所示:

实战编程使用SwiftUI从0到1完成一款iOS笔记App(四)

按照项目编程的习气,咱们其实在正式编程前需求建立根底的页面结构,将模型类、视图、完结办法按照文件夹分隔。

在Xcode视图窗口右键,选择New Group,创立一个新的文件夹,如下图所示:

实战编程使用SwiftUI从0到1完成一款iOS笔记App(四)

咱们将新的文件夹命名为Model,并把Model.swift文件拖到里边,如下图所示:

实战编程使用SwiftUI从0到1完成一款iOS笔记App(四)

同理,咱们再创立一个View文件夹、Extension文件夹、ViewModel文件夹,并将ContentView.swiftNewNoteView.swift文件放入View文件夹中,将ToastView.swift放入Extension文件夹中,如下图所示:

实战编程使用SwiftUI从0到1完成一款iOS笔记App(四)

以上便是一个根底的项目的文件结构,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
}

实战编程使用SwiftUI从0到1完成一款iOS笔记App(四)

上述代码中,为了更好阐明MVVM开发形式中的Model,咱们更改NoteItem模型类名称为NoteModel便于了解。参数重命名的办法为选择参数点击右键,选择Refactor,选择Rename,修正为NoteModel

为了运用NoteModel模型类的序列化数据,NoteModel需求遵循Codable协议。

ViewModel

咱们在ViewModel文件夹下创立一个新的Swift文件,命名为ViewModel.swift。如下图所示:

实战编程使用SwiftUI从0到1完成一款iOS笔记App(四)

根底概念

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模型类数据,构建数组。

然后是想法笔记需求用到的参数writeTimetitlecontent,查找栏用到的参数searchText。当查找时,可能会因为关键字查找为空,导致查找列表变成“缺省图”形式,因而还需求声明一个参数isSearching,判别当时是否处于查找状态。

许多笔记App开发都会把新建页面和修改页面分隔写,包含网上下载下来的代码根本都是新增、修改两个页面,而两个页面运用相同的代码。亦或者是爽性就没有修改页面,只能新增、删去,不能修改,这都不够高雅。

因而,为了共用页面,咱们声明了3个Bool类型的参数isAddshowNewNoteViewshowEditNoteView

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)")
            }
        }
    }

实战编程使用SwiftUI从0到1完成一款iOS笔记App(四)

上述代码中,首要创立了一个办法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
            }
        }
    }

实战编程使用SwiftUI从0到1完成一款iOS笔记App(四)

因为列表中每一行的数据都有对应的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
        }
    }

实战编程使用SwiftUI从0到1完成一款iOS笔记App(四)

判别内容是否为空的办法是传入一个参数值,经过判别是否为空,从而回来一个Bool类型的值,后边咱们用来判别输入的标题和内容是否为空。

App运用

创立好ViewModel后,当咱们要运用它,需求在IdeaNoteApp项目页面声明好运用的ViewModel,如下代码所示:

import SwiftUI
@main
struct IdeaNoteApp: App {
    @StateObject  var viewModel: ViewModel = ViewModel()
    var body: some Scene {
        WindowGroup {
            ContentView()
                .environmentObject(viewModel)
        }
    }
}

实战编程使用SwiftUI从0到1完成一款iOS笔记App(四)

IdeaNoteApp页面是整个App启动时的页面,它相当于咱们的主函数,当时项目指向的主页是ContentView主页视图,因而在运用MVVM项目开发形式时,当咱们用运用到ViewModel时,就需求引证ViewModel到主函数中方可运用。

本章小结

祝贺你,准备工作已经就绪,随时能够开端下一步的内容。

当然,为了让大家更好地吸收学习内容,本章咱们就分享了如何完结Model、View Model部分,下一章节中,咱们将继续完结View视图的相关内容,咱们将在本来View视图的根底上进行内容调整,便利大家更好地了解SwiftUI的运作形式。

快来着手试试吧~

版权声明

本文为稀土技能社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!