實戰程式設計·使用SwiftUI從0到1完成一款iOS筆記App(四)
theme: smartblue
前提回顧
在上幾個章節,我們完成了念頭筆記的基本頁面的程式設計,並在上一章節中完成新建筆記的互動邏輯。
這幾天和讀者溝通時收到反饋,念頭筆記專案哪怕只有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天后未獲授權禁止轉載,侵權必究!
- 實戰教程·元宇宙來了,準備好你的電子名片了嗎?(一)
- 實戰教程·元宇宙來了,準備好你的電子名片了嗎?(八)
- 實戰教程·什麼年代了還在敲傳統木魚?(二)
- 實戰教程·元宇宙來了,準備好你的電子名片了嗎?(七)
- 實戰教程·元宇宙來了,準備好你的電子名片了嗎?(六)
- 實戰教程·元宇宙來了,準備好你的電子名片了嗎?(五)
- 實戰教程·元宇宙來了,準備好你的電子名片了嗎?(四)
- 實戰教程·元宇宙來了,準備好你的電子名片了嗎?(三)
- 實戰教程·元宇宙來了,準備好你的電子名片了嗎?(二)
- 實戰教程·什麼年代了還在敲傳統木魚?(一)
- 技術下午茶:產品經理是如何工作的?如何才算一份好的需求文件?如何設計一個簡單的列表,它應該具備哪些基本功能?
- 釋出&選擇釋出,使用SwiftUI搭建一個新建釋出彈窗(上)
- 釋出&選擇釋出,使用SwiftUI搭建一個新建釋出彈窗(下)
- 使用SwiftUI搭建一個風箏搖擺動畫,實現放風箏的夢想~
- SwiftUI100天:使用SwiftUI搭建一個計時器App
- 實戰程式設計·使用SwiftUI從0到1完成一款iOS筆記App(三)
- 初識MVVM·關於啟動頁、引導頁、登入頁的設計細節和互動邏輯
- 誰說程式設計師不懂浪漫,教你使用SwiftUI搭建一個電子相簿送給她吧~
- 實戰程式設計·刻在男人DNA裡的浪漫,空氣投籃(二)
- 實戰程式設計·使用SwiftUI從0到1完成一款iOS筆記App(四)