SwiftUI學(xué)習(xí)100天(Day53 - 項目11,第一部分)

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

今天我們要開始另一個新項目,這就是事情真正開始變得嚴(yán)肅的地方,因為你將學(xué)習(xí)一項重要的新 Swift 技能、一項重要的新 SwiftUI 技能和一項重要的應(yīng)用程序開發(fā)技能,所有這些都會出現(xiàn)在我們構(gòu)建項目時很有用。
你將學(xué)習(xí)的應(yīng)用程序開發(fā)技能是 Apple 的框架之一:Core Data。它負(fù)責(zé)管理數(shù)據(jù)庫中的對象,包括讀取、寫入、過濾、排序等,它在 iOS、macOS 及更高版本的所有應(yīng)用程序開發(fā)中都非常重要。以前我們直接將數(shù)據(jù)寫入UserDefaults
,但這只是短期的事情,可以幫助你學(xué)習(xí) - 核心數(shù)據(jù)是真正的交易,被數(shù)十萬個應(yīng)用程序使用。
加拿大軟件開發(fā)人員 Rob Pike(Go 編程語言的創(chuàng)造者,開發(fā) Unix 的團(tuán)隊成員,UTF-8 的共同創(chuàng)造者,也是出版作者)寫了這篇關(guān)于數(shù)據(jù)的文章:
“數(shù)據(jù)占主導(dǎo)地位。如果你選擇了正確的數(shù)據(jù)結(jié)構(gòu)并很好地組織了事物,那么算法幾乎總是不言而喻的。數(shù)據(jù)結(jié)構(gòu),而不是算法,才是編程的核心?!?/p>
這通常被簡稱為“編寫使用智能對象的愚蠢代碼”,正如你將看到的,對象并沒有比在 Core Data 支持下變得更聰明!
今天你有四個主題要完成,你將在其中學(xué)習(xí)@Binding
、類型擦除、Core Data等。

書蟲:簡介
在這個項目中,我們將構(gòu)建一個應(yīng)用程序來跟蹤你讀過哪些書以及你對它們的看法,它會遵循與項目 10 類似的主題:讓我們掌握你已經(jīng)掌握的所有技能,然后添加一些額外的新技能,使他們都達(dá)到一個新的水平。
這次你將遇到 Core Data,它是 Apple 久經(jīng)考驗的數(shù)據(jù)庫處理框架。這個項目將作為 Core Data 的介紹,但我們很快就會進(jìn)入更多細(xì)節(jié)。
同時,我們還將構(gòu)建我們的第一個自定義用戶界面組件——一個星級評分小部件,用戶可以點擊它為每本書留下分?jǐn)?shù)。這將意味著向你介紹另一個屬性包裝器,稱為@Binding
- 相信我,這一切都有意義。
像往常一樣,我們將從演練開始這個項目所需的所有新技術(shù),因此請使用 App 模板創(chuàng)建一個名為 Bookworm 的新 iOS 應(yīng)用程序。
重要提示:我知道這很誘人,但請不要選中標(biāo)有“使用核心數(shù)據(jù)”的框。它會向你的項目添加一大堆無用的代碼,你只需刪除它即可繼續(xù)。



使用@Binding 創(chuàng)建自定義組件
你已經(jīng)看到 SwiftUI 的@State
屬性包裝器如何讓我們使用本地值類型,以及如何@StateObject
讓我們使用可共享的引用類型。那么,還有第三個選項,稱為@Binding
,它允許我們將@State
一個視圖的屬性連接到一些底層模型數(shù)據(jù)。
想一想:當(dāng)我們創(chuàng)建一個撥動開關(guān)時,我們發(fā)送了某種可以更改的布爾屬性,如下所示:
因此,切換需要在用戶與其交互時更改我們的布爾值,但它如何記住它應(yīng)該更改的值?
這就是@Binding
:它讓我們在一個視圖中存儲一個可變值,該視圖實際上指向來自其他地方的一些其他值。在 Toggle
的情況下
,開關(guān)將其自身的本地綁定更改為布爾值,但實際上是在幕后操縱我們視圖中的@State
屬性。
這使得
@Binding
對于任何時候創(chuàng)建自定義用戶界面組件都非常重要。從本質(zhì)上講,UI 組件就像其他所有內(nèi)容一樣只是 SwiftUI 視圖,但@Binding
和
它們與眾不同:雖然它們可能具有本地@State
屬性,但它們也會公開@Binding的屬性,
讓它們直接與其他視圖交互。
為了演示這一點,我們將查看創(chuàng)建自定義按鈕所需的代碼,該按鈕在按下時保持按下狀態(tài)。我們的基本實現(xiàn)都是你以前見過的東西:一個帶有一些填充的按鈕、一個線性漸變的背景、一個Capsule
剪輯形狀等等——現(xiàn)在把它添加到 ContentView.swift 中:
唯一令人興奮的事情是我使用了兩種漸變顏色的屬性,因此它們可以通過創(chuàng)建按鈕的任何內(nèi)容進(jìn)行自定義。
我們現(xiàn)在可以創(chuàng)建這些按鈕之一作為我們主用戶界面的一部分,如下所示:
它在按鈕下方有一個文本視圖,因此我們可以跟蹤按鈕的狀態(tài)——嘗試運行你的代碼并查看它是如何工作的。
你會發(fā)現(xiàn)點擊按鈕確實會影響它的顯示方式,但我們的文本視圖并沒有反映出這種變化——它總是顯示“關(guān)閉”。很明顯,某些東西正在發(fā)生變化,因為按鈕的外觀在按下時發(fā)生了變化,但這種變化并沒有反映在ContentView
.
這里發(fā)生的事情是我們已經(jīng)定義了一種單向數(shù)據(jù)流:ContentView
有它的rememberMe
布爾值,它被用來創(chuàng)建一個PushButton
- 按鈕有一個由提供的初始值ContentView
。但是,一旦創(chuàng)建了按鈕,它就會接管該值的控制權(quán):它會在按鈕內(nèi)部將屬性isOn
在 true 或 false 之間切換,但不會將該更改傳回給ContentView
.
這是一個問題,因為我們現(xiàn)在有兩個事實來源:ContentView
存儲一個值,PushButton
另一個。幸運的是,這就是@Binding
派上用場的地方:它允許我們在使用PushButton
時和任何事物之間創(chuàng)建雙向連接,這樣當(dāng)一個值發(fā)生變化時,另一個值也會發(fā)生變化。
要切換到@Binding
我們只需要進(jìn)行兩個更改。首先,PushButton
將其isOn
屬性更改為:
其次,ContentView
中
改變我們創(chuàng)建按鈕的方式:
rememberMe
之前添加了一個美元符號
——我們傳遞的是綁定本身,而不是其中的布爾值。
現(xiàn)在再次運行代碼,你會發(fā)現(xiàn)一切都按預(yù)期工作:切換按鈕現(xiàn)在也可以正確更新文本視圖。
這就是 @Binding
的強大之處
:就按鈕而言,它只是切換一個布爾值——它不知道其他東西正在監(jiān)視該布爾值并根據(jù)變化采取行動。



使用 TextEditor 接受多行文本輸入
我們已經(jīng)多次使用?SwiftUI 的視圖?TextField
,當(dāng)用戶想要輸入短文本時,它非常有用。但是,對于較長的文本片段,你應(yīng)該改用TextEditor
視圖:它還希望提供到文本字符串的雙向綁定,但它還有一個額外的好處,即允許多行文本——這對接受來自用戶的更長的字符串。
主要是因為它在配置選項方面沒有什么特別之處,使用TextEditor
實際上比使用TextField
更容易
——你不能調(diào)整它的樣式或添加占位符文本,你只需將它綁定到一個字符串。但是,你確實需要小心確保它不會超出安全區(qū)域,否則打字會很棘手;將它嵌入 一個?NavigationView
、 一個Form
或相似的。
例如,我們可以通過結(jié)合 @AppStorage
來創(chuàng)建世界上最簡單的筆記應(yīng)用程序TextEditor
,如下所示
:
提示:?@AppStorage
并非旨在存儲安全信息,因此切勿將其用于任何私人用途。



如何結(jié)合 Core Data 和 SwiftUI
SwiftUI 和 Core Data 的推出時間幾乎正好相隔十年——iOS 13 的 SwiftUI 和 iPhoneOS 3 的 Core Data;很久以前它甚至不叫 iOS,因為 iPad 還沒有發(fā)布。盡管時間上相距甚遠(yuǎn),Apple 還是投入了大量工作來確保這兩項強大的技術(shù)能夠完美地協(xié)同工作,這意味著 Core Data 集成到 SwiftUI 中,就好像它始終以這種方式設(shè)計一樣。
首先,基礎(chǔ)知識:Core Data 是一個對象圖和持久性框架,這是一種奇特的說法,它讓我們定義對象和這些對象的屬性,然后讓我們從永久存儲中讀取和寫入它們。
從表面上看,這聽起來像是使用Codable
和
?UserDefaults
,但它比這更高級:Core Data 能夠?qū)ξ覀兊臄?shù)據(jù)進(jìn)行排序和過濾,并且可以處理更大的數(shù)據(jù)——實際上它可以存儲多少數(shù)據(jù)沒有限制。更好的是,Core Data 實現(xiàn)了各種更高級的功能,以便在你真正需要依靠它時使用:數(shù)據(jù)驗證、延遲加載數(shù)據(jù)、撤消和重做等等。
在這個項目中,我們將只使用少量的 Core Data 功能,但很快就會擴展——我只想讓你先體驗一下。
當(dāng)你創(chuàng)建你的 Xcode 項目時,我要求你不要選中 Use Core Data 框,因為盡管它去掉了一些無聊的設(shè)置代碼,但它也添加了一大堆額外的示例代碼,這些代碼毫無意義而且只是需要被刪除。
因此,你將學(xué)習(xí)如何手動設(shè)置 Core Data。它需要三個步驟,從我們定義要在我們的應(yīng)用程序中使用的數(shù)據(jù)開始。
之前我們這樣描述數(shù)據(jù):
然而,Core Data 并不是這樣工作的。你看,Core Data 需要提前知道我們所有的數(shù)據(jù)類型是什么樣的,它包含什么,以及它們?nèi)绾蜗嗷リP(guān)聯(lián)。
這一切都包含在一個名為數(shù)據(jù)模型的新文件類型中,它的文件擴展名為“xcdatamodeld”。現(xiàn)在讓我們創(chuàng)建一個:按 Cmd+N 創(chuàng)建一個新文件,從模板列表中選擇數(shù)據(jù)模型,然后將你的模型命名為 Bookworm.xcdatamodeld。
當(dāng)你按下創(chuàng)建時,Xcode 將在其數(shù)據(jù)模型編輯器中打開新文件。在這里,我們將我們的類型定義為“實體”,然后在其中創(chuàng)建屬性作為“屬性”——Core Data 負(fù)責(zé)將其轉(zhuǎn)換為可以在運行時使用的實際數(shù)據(jù)庫布局。
出于試用目的,請按添加實體按鈕創(chuàng)建一個新實體,然后雙擊其名稱將其重命名為“學(xué)生”。接下來,單擊 Attributes 表正下方的 + 按鈕添加兩個屬性:“id”作為 UUID 和“name”作為字符串。
這告訴了 Core Data 創(chuàng)建學(xué)生和保存他們需要知道的一切,所以我們可以繼續(xù)設(shè)置 Core Data 的第二步:編寫一些 Swift 代碼來加載該模型并準(zhǔn)備它供我們使用。
我們將把它分成幾個小部分來寫,這樣我就可以詳細(xì)解釋發(fā)生了什么。首先import Foundation
,創(chuàng)建一個名為 DataController.swift 的新 Swift 文件,并將其添加到其行上方:
我們將首先創(chuàng)建一個名為 DataController
的新類
,使其符合ObservableObject
以便我們可以將它與@StateObject
屬性包裝器一起使用——我們希望在我們的應(yīng)用程序啟動時創(chuàng)建其中一個,然后在我們的應(yīng)用程序運行期間保持它的活動狀態(tài)運行。
在這個類中,我們將添加一個類型的屬性NSPersistentContainer
,它是負(fù)責(zé)加載數(shù)據(jù)模型并讓我們訪問內(nèi)部數(shù)據(jù)的核心數(shù)據(jù)類型。從現(xiàn)代的角度來看,這聽起來很奇怪,但“NS”部分是“NeXTSTEP”的縮寫,這是 Apple 在 1997 年將史蒂夫·喬布斯帶回公司時獲得的一個巨大的操作系統(tǒng)——Core Data 有一些非常古老的操作系統(tǒng)基礎(chǔ)!
無論如何,首先將其添加到你的文件中:
這告訴 Core Data 我們要使用 Bookworm 數(shù)據(jù)模型。它實際上并沒有加載它——我們稍后會加載它——但它確實準(zhǔn)備了 Core Data 來加載它。數(shù)據(jù)模型不包含我們的實際數(shù)據(jù),只是像我們剛才定義的屬性和屬性的定義。
要實際加載數(shù)據(jù)模型,我們需要調(diào)用我們的容器loadPersistentStores()
,它告訴 Core Data 根據(jù) Bookworm.xcdatamodeld 中的數(shù)據(jù)模型訪問我們保存的數(shù)據(jù)。這樣不會把所有的數(shù)據(jù)同時加載到內(nèi)存中,因為那樣會很浪費,但至少Core Data可以看到我們所有的信息。
加載保存的數(shù)據(jù)完全有可能出錯,例如,數(shù)據(jù)可能已損壞。但老實說,如果它真的出錯了,你也無能為力——此時你能做的唯一有意義的事情就是向用戶顯示一條錯誤消息,并希望重新啟動應(yīng)用程序能解決問題。
不管怎樣,我們要為DataController
寫一個小的初始化程序來立即加載我們存儲的數(shù)據(jù)。如果出現(xiàn)問題——不太可能,但并非不可能——我們將向 Xcode 調(diào)試日志打印一條消息。
現(xiàn)在將此初始化程序添加到DataController
:
這就完成DataController
了,所以最后一步是創(chuàng)建一個實例DataController
并將其發(fā)送到 SwiftUI 的環(huán)境中。你已經(jīng)遇到過@Environment
要求 SwiftUI 關(guān)閉我們的視圖,但它還存儲其他有用的數(shù)據(jù),例如我們的時區(qū)、用戶界面外觀等。
這與 Core Data 相關(guān),因為大多數(shù)應(yīng)用程序一次只能使用一個 Core Data 存儲,因此我們不是每個視圖都試圖單獨創(chuàng)建自己的存儲,而是在我們的應(yīng)用啟動時創(chuàng)建一次,然后將其存儲在 SwiftUI 環(huán)境中,這樣我們應(yīng)用程序中的任何其他地方都可以使用它。
為此,打開 BookwormApp.swift,并將此屬性添加到結(jié)構(gòu)中:
這創(chuàng)建了我們的數(shù)據(jù)控制器,現(xiàn)在我們可以通過向該ContentView()
行添加一個新的修飾符將其放入 SwiftUI 的環(huán)境中:
提示:如果你使用 Xcode 的 SwiftUI 預(yù)覽,你還應(yīng)該將托管對象上下文注入到ContentView
.
你已經(jīng)遇到了數(shù)據(jù)模型,它存儲我們要使用的實體和屬性的定義,以及NSPersistentStoreContainer
處理將我們保存的實際數(shù)據(jù)加載到用戶設(shè)備的操作。好吧,你剛剛遇到了 Core Data 難題的第三部分:托管對象上下文。這些實際上是數(shù)據(jù)的“實時”版本——當(dāng)你加載對象并更改它們時,這些更改僅存在于內(nèi)存中,直到你專門將它們保存回持久存儲。
因此,視圖上下文的工作是讓我們使用內(nèi)存中的所有數(shù)據(jù),這比不斷地將數(shù)據(jù)讀寫到磁盤要快得多。當(dāng)我們準(zhǔn)備就緒時,如果我們希望下次運行應(yīng)用程序時它們存在,我們?nèi)匀恍枰獙⒏膶懭氤志么鎯?,但如果你不想要它們,你也可以選擇放棄更改。
至此,我們已經(jīng)創(chuàng)建了我們的 Core Data 模型,我們已經(jīng)加載了它,并且我們已經(jīng)準(zhǔn)備好與 SwiftUI 一起使用。仍然還有兩個重要的難題:讀取數(shù)據(jù)和寫入數(shù)據(jù)。
從 Core Data 中檢索信息是使用獲取請求完成的——我們描述我們想要什么,應(yīng)該如何排序,以及是否應(yīng)該使用任何過濾器,然后 Core Data 發(fā)回所有匹配的數(shù)據(jù)。我們需要確保此獲取請求隨時間保持最新,以便在創(chuàng)建或刪除學(xué)生時我們的 UI 保持同步。
SwiftUI 對此有一個解決方案,而且——你猜對了——它是另一個屬性包裝器。這次它被調(diào)用@FetchRequest
,它至少需要一個參數(shù)來描述我們希望如何對結(jié)果進(jìn)行排序。它有一個特定的格式,所以讓我們從為我們的學(xué)生添加一個獲取請求開始——現(xiàn)在請將這個屬性添加到ContentView
:
分解后,它創(chuàng)建了一個沒有排序的獲取請求,并將其放入一個名為students
的屬性中,這個屬性的類型是FetchedResults<Student>
。
從此以后,我們可以像使用常規(guī) Swift 數(shù)組一樣開始使用students
,但是你會看到一個問題。首先,一些將數(shù)組放入 一個 List的代碼
:
你發(fā)現(xiàn)問題了嗎?是的,student.name
是一個可選的——它可能有值也可能沒有。這是 Core Data 的一個領(lǐng)域,它會讓你非常惱火:它有可選數(shù)據(jù)的概念,但它與 Swift 的可選數(shù)據(jù)是完全不同的概念。如果我們對 Core Data 說“這東西不能是可選的”(你可以在模型編輯器中這樣做),它仍然會生成可選的 Swift 屬性,因為 Core Data 所關(guān)心的是屬性在保存時是否有值– 在其他時候它們可以為零。
如果你愿意,你可以運行代碼,但沒有什么意義——列表將是空的,因為我們還沒有添加任何數(shù)據(jù),所以我們的數(shù)據(jù)庫是空的。為了解決這個問題,我們將在我們的列表下方創(chuàng)建一個按鈕,每次點擊它時都會添加一個新的隨機學(xué)生,但首先我們需要一個新屬性來訪問我們之前創(chuàng)建的托管對象上下文。
讓我稍微備份一下,因為這很重要。當(dāng)我們定義“Student”實體時,實際發(fā)生的是 Core Data 為我們創(chuàng)建了一個繼承于它自己的類:NSManagedObject
.?我們在代碼中看不到這個類,因為它是在我們構(gòu)建項目時自動生成的,就像 Core ML 的模型一樣。這些對象之所以被稱為托管對象,是因為 Core Data 正在照管它們:它從持久容器中加載它們并將它們的更改也寫回。
我們所有的托管對象都位于一個托管對象上下文中,我們之前創(chuàng)建了其中之一。將它放入 SwiftUI 環(huán)境意味著它會自動用于@FetchRequest
屬性包裝器——它使用環(huán)境中可用的任何托管對象上下文。
無論如何,在添加和保存對象時,我們需要訪問 SwiftUI 環(huán)境中的托管對象上下文。這是@Environment
屬性包裝器的另一個用途——我們可以向它詢問當(dāng)前的托管對象上下文,并將它分配給一個屬性供我們使用。
因此,現(xiàn)在將此屬性添加到ContentView
:
有了它,下一步是添加一個按鈕,用于生成隨機學(xué)生并將他們保存在托管對象上下文中。為了幫助學(xué)生脫穎而出,我們將通過創(chuàng)建firstNames
和lastNames
數(shù)組來分配隨機名稱,然后使用randomElement()
來從每個數(shù)組中挑選一個。
首先在List
下面添加這個按鈕
:
注意:不可避免地會有人抱怨我強行解包那些對 randomElement()
的調(diào)用
,但我們實際上只是手動創(chuàng)建數(shù)組以具有值——它總是會成功。如果你非常討厭強制解包,也許可以用 nil 合并和默認(rèn)值替換它們。
現(xiàn)在是有趣的部分:我們將使用我們生成的類 Core Data 創(chuàng)建一個Student
對象。這需要附加到托管對象上下文,以便對象知道它應(yīng)該存儲在哪里。然后我們可以像通常為結(jié)構(gòu)體一樣為其賦值。
所以,現(xiàn)在將這三行添加到按鈕的動作閉包中:
最后,我們需要讓我們的托管對象上下文保存自己,這意味著它將把它的更改寫入持久存儲。這是一個拋出函數(shù)調(diào)用,因為理論上它可能會失敗。在實踐中,我們所做的一切都沒有任何失敗的機會,所以我們可以稱之為使用try?
——我們不關(guān)心捕獲錯誤。
因此,將最后一行添加到按鈕的操作中:
最后,你現(xiàn)在應(yīng)該能夠運行該應(yīng)用程序并嘗試一下——點擊添加按鈕幾次以生成一些隨機學(xué)生,你應(yīng)該會看到他們滑入我們列表的某個位置。更好的是,如果你重新啟動該應(yīng)用程序,你會發(fā)現(xiàn)你的學(xué)生仍然在那里,因為 Core Data 保存了他們。
現(xiàn)在,你可能認(rèn)為這是一個可怕的學(xué)習(xí),沒有太多結(jié)果,但你現(xiàn)在知道什么是持久存儲和托管對象上下文,什么是實體和屬性,什么是托管對象和獲取請求,并且你已經(jīng)也看到了如何保存更改。我們將在這個項目的稍后部分以及未來更多地關(guān)注 Core Data,但現(xiàn)在你已經(jīng)走得很遠(yuǎn)了。
這是該項目概述的最后一部分,但這次我不希望你完全重置呢的項目。是的,將 ContentView.swift 恢復(fù)到原來的狀態(tài),然后從 Bookworm.xcdatamodeld 中刪除 Student 實體,但請留下 BookwormApp.swift 和 DataController.swift——我們將在實際項目中使用它們!


