SwiftUI學習100天(Day72 - 項目 14,第五部分)

原創(chuàng)鏈接:https://www.hackingwithswift.com/100/swiftui
以下內(nèi)容僅供學習參考:

今天是這個項目編碼的最后一天,我相信你期待著明天的挑戰(zhàn)和復習——它應該從這么長的教程中做出很好的改變。
不過,首先,我們需要涵蓋兩個棘手的主題,其中一個將真正挑戰(zhàn)你,因為我們將重構(gòu)我們的代碼以使用 MVVM 設計模式。正如你將看到的,這有助于在我們的項目中將邏輯與布局分開,但這也需要一些思考——尤其是因為你需要了解主要參與者的概念。
當你完成今天的工作時,你可能真的開始感覺到難度曲線在上升,因為我們的項目在增長,規(guī)模和復雜性都在增加。我想借此機會提醒你幾件事:
你不是一個人;?每個人都必須經(jīng)歷同樣的學習曲線。
這是一場馬拉松,而不是短跑;慢慢來,它會來的。
休息一下,稍后再討論一個話題是可以的;有新鮮的眼睛會有所幫助。
沒有奮斗就沒有學問;如果你正在努力學習一些東西,它最終會堅持得更好。
孔子有一句名言,你最好記住:“走得慢不重要,只要不停?!?/p>
今天你有兩個主題需要完成,其中你將學習如何安全地將數(shù)據(jù)寫入磁盤,以及如何啟用生物識別身份驗證。

將 MVVM 引入你的 SwiftUI 項目
到目前為止,我已經(jīng)向你介紹了 Swift 和 SwiftUI 的一系列概念,并且我還提供了一些關于如何更好地組織代碼的技巧。好吧,在這里我想進一步探討后一部分:我們將研究通常稱為軟件架構(gòu)的東西,或者更宏大的名稱架構(gòu)設計模式——實際上它只是一種構(gòu)建代碼的特殊方式。
我們要查看的模式稱為 MVVM,它是 Model View View-Model 的首字母縮寫詞。這是一個非常糟糕的名字,并且徹底混淆了人們,但恐怕我們現(xiàn)在還是堅持使用它。什么是 MVVM 沒有單一的定義,你會發(fā)現(xiàn)網(wǎng)上有各種各樣的人在爭論它,但這沒關系——在這里我們將保持簡單,并使用 MVVM 作為獲取我們程序的一些方式我們的視圖結(jié)構(gòu)中的狀態(tài)和邏輯。實際上,我們將邏輯與布局分開。
我們將邊走邊探索這個定義,但現(xiàn)在讓我們從大事開始:創(chuàng)建一個名為 ContentView-ViewModel.swift 的新 Swift 文件,然后為它額外導入MapKit 。我們將使用它來創(chuàng)建一個新類來管理我們的數(shù)據(jù),并代表ContentView
結(jié)構(gòu)對其進行操作,這樣我們的視圖就不會真正關心底層數(shù)據(jù)系統(tǒng)的工作方式。
我們將從三件微不足道的事情開始,然后從那里開始。首先,創(chuàng)建一個符合ObservableObject
協(xié)議的新類,這樣我們就可以將更改報告回正在監(jiān)視的任何 SwiftUI 視圖:
其次,我希望你將該新類放在上的擴展中ContentView
,如下所示:
現(xiàn)在我們說這不僅僅是任何視圖模型,它是ContentView
.?稍后,你將負責添加第二個視圖模型來處理EditView
,因此你可以嘗試查看概念如何映射到其他地方。
我希望你做的最后一個小改動是向@MainActor
整個類添加一個新屬性 ,如下所示:
主要參與者負責運行所有用戶界面更新,并將該屬性添加到類意味著我們希望它的所有代碼——任何時候它運行任何東西,除非我們特別要求——在那個主要參與者上運行。這很重要,因為它負責進行 UI 更新,而這些更新必須發(fā)生在主要參與者身上。在實踐中這并不是那么容易,但我們稍后會談到這一點。
現(xiàn)在,我們以前使用過ObservableObject
類,但沒有@MainActor
--他們
怎么會起作用?好吧,無論何時我們使用@StateObject
或@ObservedObject?
Swift 都在幕后默默地為我們推斷屬性@MainActor
——它知道這兩者都意味著 SwiftUI 視圖依賴于外部對象來觸發(fā)其 UI 更新,因此它將確保所有工作自動發(fā)生在沒有我們要求的主要演員。
但是,這并不能提供 100% 的安全性。是的,當從 SwiftUI 視圖使用時,Swift 會推斷出這一點,但是如果你從其他地方訪問你的類 - 例如,從另一個類?然后代碼可以在任何地方運行,這是不安全的。因此,通過在@MainActor
此處添加屬性,我們采用了一種“腰帶和大括號”方法:我們告訴 Swift 此類的每個部分都應該在主要參與者上運行,因此更新 UI 是安全的,無論它在哪里使用。
現(xiàn)在我們已經(jīng)有了我們的類,我們可以從我們的視圖中選擇哪些狀態(tài)應該被移動到視圖模型中。有些人會告訴你移動所有這些,其他人會更有選擇性,這沒關系——再說一次,MVVM 沒有單一的樣子,所以我將為你提供工具和知識來自己試驗。
讓我們從簡單的事情開始:將所有三個@State
屬性移到它的ContentView
視圖模型中,@State private
只是切換@Published
——它們不能再私有了,因為它們明確需要共享ContentView
:
ContentView
現(xiàn)在我們可以用一個屬性替換所有這些屬性:
這當然會破壞很多代碼,但修復很容易——只需viewModel
在不同的地方添加即可。所以,$mapRegion
變成$viewModel.mapRegion
,locations
變成viewModel.locations
,等等。
一旦你在任何需要它的地方添加了它,你的代碼就會再次編譯,但你可能想知道這有什么幫助——我們不是只是將代碼從一個地方移動到另一個地方嗎?嗯,是的,但是有一個重要的區(qū)別會隨著你的技能的增長而變得更加清晰:將所有這些功能都放在一個單獨的類中使得為你的代碼編寫測試變得更加容易。
視圖在處理數(shù)據(jù)表示時效果最好,這意味著數(shù)據(jù)操作是將代碼移入視圖模型的理想選擇。考慮到這一點,如果你仔細查看你的ContentView
代碼,你可能會注意到我們的視圖在兩個地方做了比它應該做的更多的工作:添加一個新位置和更新一個現(xiàn)有位置,這兩者都在我們視圖的內(nèi)部數(shù)據(jù)中產(chǎn)生模型。
從視圖模型的屬性中讀取數(shù)據(jù)通常沒問題,但寫入數(shù)據(jù)則不然,因為本練習的重點是將邏輯與布局分開。如果我們限制編寫視圖模型數(shù)據(jù),你可以立即找到這兩個地方——將locations
視圖模型中的屬性修改為:
現(xiàn)在我們已經(jīng)說過讀取位置很好,但只有類本身可以寫入位置。Xcode 會立即指出我們需要從視圖中獲取代碼的兩個位置:添加新位置和更新現(xiàn)有位置。
因此,我們可以從向視圖模型添加一個新方法來處理添加新位置開始:
然后可以從中的 + 按鈕使用它ContentView
:
第二個有問題的地方是更新一個位置,所以我希望你把整個if let index
檢查剪切到剪貼板,然后將它粘貼到視圖模型中的一個新方法中,添加一個檢查,我們有一個選定的地方可以使用:
確保并viewModel
從那里刪除兩個引用——它們不再需要了。
現(xiàn)在EditView
工作表ContentView
可以將其數(shù)據(jù)傳遞到視圖模型:
此時視圖模型已經(jīng)接管了所有方面ContentView
,這很棒:視圖在那里呈現(xiàn)數(shù)據(jù),視圖模型在那里管理數(shù)據(jù)。分裂并不總是那么干凈,盡管你可能會在網(wǎng)上其他地方聽到這種說法,這也沒關系——一旦你進入更高級的項目,你會發(fā)現(xiàn)“一刀切”的方法通常不適合任何人,所以我們盡我們所能。
無論如何,在這種情況下,既然我們已經(jīng)設置好視圖模型,我們可以升級它以支持數(shù)據(jù)的加載和保存。這將在文檔目錄中查找特定文件,然后使用JSONEncoder
或JSONDecoder
轉(zhuǎn)換它以備使用。
之前我向你展示了如何使用可重用功能找到我們應用程序的文檔目錄,但在這里我們將把它打包為一個擴展,FileManager
以便在任何項目中更容易訪問。
創(chuàng)建一個名為 FileManager-DocumentsDirectory.swift 的新 Swift 文件,然后為其提供以下代碼:
現(xiàn)在我們可以在我們的文檔目錄中的任何地方創(chuàng)建一個文件的 URL,但是我不想在加載和保存文件時都這樣做,因為這意味著如果我們改變了我們的保存位置,我們需要記住更新這兩個地方.
因此,更好的想法是向我們的視圖模型添加一個新屬性來存儲我們要保存到的位置:
有了它,我們就可以創(chuàng)建一個新的初始化器和一個新的save()
方法來確保我們的數(shù)據(jù)被自動保存。首先將其添加到視圖模型:
至于保存,之前我向你展示了如何將一個字符串寫入磁盤,但是這個Data
版本更好,因為它讓我們只用一行代碼就可以做一些非常神奇的事情:我們可以要求 iOS 確保文件是加密寫入的,所以只有在用戶解鎖他們的設備后才能讀取它。這是請求原子寫入的補充——iOS 幾乎為我們完成了所有工作。
現(xiàn)在將此方法添加到視圖模型:
是的,要確保文件以強加密方式存儲,只需添加.completeFileProtection
數(shù)據(jù)寫入選項即可。
使用這種方法,我們可以在任意數(shù)量的文件中寫入任意數(shù)量的數(shù)據(jù)——它比UserDefaults
靈活得多,還允許我們根據(jù)需要加載和保存數(shù)據(jù),而不是像UserDefaults
那樣在應用程序啟動時立即加載和保存數(shù)據(jù)。
在我們完成這一步之前,我們需要對我們的視圖模型進行一些小的更改,以便使用我們剛剛編寫的代碼。
首先,locations
數(shù)組不再需要初始化為空數(shù)組,因為這是由初始化程序處理的。將其更改為:
其次,我們需要save()
在添加新位置或更新現(xiàn)有位置后調(diào)用該方法,因此請將其添加save()
到這兩個方法的末尾。
繼續(xù)并立即運行該應用程序,你應該會發(fā)現(xiàn)你可以自由添加項目,然后重新啟動該應用程序以查看它們恢復原樣。
這總共花費了相當多的代碼,但最終結(jié)果是我們已經(jīng)很好地完成了加載和保存:
所有邏輯都在視圖之外處理,因此稍后當你學習編寫測試時,你會發(fā)現(xiàn)使用視圖模型要容易得多。
當我們寫入數(shù)據(jù)時,我們會讓 iOS 對其進行加密,以便在用戶解鎖設備之前無法讀取或?qū)懭胛募?/p>
加載和保存過程幾乎是透明的——我們添加了一個修改器并更改了另一個,僅此而已。
當然,我們的應用程序還不是真正安全:我們已經(jīng)確保我們的數(shù)據(jù)文件使用加密保存,因此只有在設備解鎖后才能讀取它,但沒有什么可以阻止其他人之后讀取數(shù)據(jù)。



將我們的 UI 鎖定在 Face ID 后面
為了完成我們的應用程序,我們將進行最后一項重要更改:我們將要求用戶使用 Touch ID 或 Face ID 對自己進行身份驗證,以便查看他們在應用程序上標記的所有位置。畢竟,這是他們的私人數(shù)據(jù),我們應該尊重這一點,當然,這給了我一個機會,讓你在實際環(huán)境中使用一項重要技能!
首先,我們需要在我們的視圖模型中添加一些新狀態(tài)來跟蹤應用程序是否已解鎖。因此,首先添加這個新屬性:
其次,我們需要在我們的項目配置選項中添加 Face ID 權(quán)限請求密鑰,向用戶解釋我們?yōu)槭裁匆褂?Face ID。如果你還沒有添加它,現(xiàn)在轉(zhuǎn)到你的目標選項,選擇信息選項卡,然后右鍵單擊任何現(xiàn)有行并在其中添加“隱私 - 面部識別碼使用說明”鍵。你可以輸入你喜歡的內(nèi)容,但“請驗證你自己以解鎖你的位置”似乎是一個不錯的選擇。
第三,我們需要添加import LocalAuthentication
到你的視圖模型文件的頂部,以便我們可以訪問 Apple 的身份驗證框架。
現(xiàn)在是困難的部分。如果你還記得,由于其 Objective-C 根源,用于生物識別身份驗證的代碼有點令人不快,因此最好讓它遠離 SwiftUI 的整潔。因此,我們將編寫一個專用authenticate()
方法來處理所有生物識別工作:
創(chuàng)建一個
LAContext
這樣我們就有了可以檢查和執(zhí)行生物認證的東西。詢問當前設備是否能夠進行生物認證。
如果是,則啟動請求并提供一個閉包以在它完成時運行。
請求完成后,檢查結(jié)果。
如果成功,我們將設置
isUnlocked
為 true 以便我們可以正常運行我們的應用程序。
現(xiàn)在將此方法添加到你的視圖模型中:
請記住,我們代碼中的字符串用于 Touch ID,而 Info.plist 中的字符串用于 Face ID。
現(xiàn)在我們需要做一個實際上非常小的調(diào)整,但如果你正在閱讀本文而不是觀看視頻,則可能很難形象化。ZStack
中的所有內(nèi)容都
需要縮進一層,并將其放在它前面:
在結(jié)束之前添加ZStack
:
所以,它應該是這個樣子:
所以現(xiàn)在我們需要做的就是用觸發(fā)authenticate()
方法的實際按鈕填寫評論// button here
。你可以設計任何你想要的,但這樣的東西應該足夠了:
你現(xiàn)在可以繼續(xù)并再次運行該應用程序,因為我們的代碼幾乎完成了。如果這是你第一次在模擬器中使用 Face ID,你需要轉(zhuǎn)到“功能”菜單并選擇“Face ID”>“已注冊”,但是一旦你重新啟動應用程序,你就可以使用“功能”>“Face ID”>“匹配面部”進行身份驗證。
但是,當它運行時,你可能會注意到一個問題:應用程序似乎工作正常,但 Xcode 可能會在其調(diào)試輸出中顯示一條警告消息。更重要的是,它還會顯示一個紫色警告,這是 Xcode 標記運行時問題的問題——當我們的代碼做了一些它確實不應該做的事情時。
在這種情況下,它應該指向我們視圖模型中的這一行:
旁邊應該寫著“不允許從后臺線程發(fā)布更改”,這意味著“你正在嘗試更改 UI,但你不是從主要參與者那里做的,這會導致問題?!?/p>
現(xiàn)在,這可能會令人困惑,因為我們之前專門將@MainActor
屬性添加到我們的整個類,我說這意味著該類的所有代碼都將在主要參與者上運行,因此對于 UI 更新是安全的。但是,我在那里添加了一個重要的附帶條件:“除非我們特別要求?!?/p>
在這種情況下,我們確實提出了其他要求,但可能并不明顯:當我們要求 Face ID 完成用戶身份驗證工作時,這發(fā)生在我們的程序之外——不是我們在進行實際的面部檢查,而是 Apple。當該過程完成時,Apple 將調(diào)用我們的完成閉包來說明它是否成功,但不管我們的@MainActor
屬性如何,它都不會在主要參與者上調(diào)用。
這里的解決方案是確保我們更改主要角色的屬性isUnlocked
。這可以通過啟動一個新任務,然后在那里調(diào)用await MainActor.run()
來完成,如下所示:
這實際上意味著“啟動一個新的后臺任務,然后立即使用該后臺任務對主要參與者的一些工作進行排隊?!?/p>
這行得通,但我們可以做得更好:我們可以告訴 Swift 我們的任務代碼需要直接在主要參與者上運行,方法是給閉包本身一個@MainActor
屬性。因此,新任務不會跳轉(zhuǎn)到后臺任務然后返回到主要參與者,而是會立即開始在主要參與者上運行:
這樣我們的代碼就完成了,這是另一個完整的應用程序——干得好!


