SwiftUI學(xué)習(xí)100天(Day57 - 項(xiàng)目 12,第一部分)

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

在這個(gè)技術(shù)項(xiàng)目中,我們將探索 Core Data 和 SwiftUI 如何協(xié)同工作來(lái)幫助我們構(gòu)建出色的應(yīng)用程序。我已經(jīng)向你介紹了項(xiàng)目 11 中的主題,但在這里我們將更詳細(xì)地介紹:自定義托管對(duì)象子類(lèi)、確保唯一性等。
美國(guó)企業(yè)家吉姆·羅恩 (Jim Rohn) 曾說(shuō)過(guò):“成功既不神奇也不神秘——成功是堅(jiān)持應(yīng)用基本原則的自然結(jié)果。”?Core Data 絕對(duì)是這些基礎(chǔ)知識(shí)之一——你當(dāng)然不會(huì)在每個(gè)項(xiàng)目中都使用它,但了解它的工作原理以及如何充分利用它將使你成為更好的應(yīng)用程序開(kāi)發(fā)人員。
今天你有五個(gè)主題需要完成,在這些主題中你將了解\.self
工作原理、如何僅在需要時(shí)保存托管對(duì)象上下文、如何確保對(duì)象是唯一的,等等。

Core Data:簡(jiǎn)介
這個(gè)技術(shù)項(xiàng)目將更詳細(xì)地探索Core Data,從一些基本技術(shù)的總結(jié)開(kāi)始,然后逐步解決一些更復(fù)雜的問(wèn)題。
當(dāng)你使用 Core Data 時(shí),請(qǐng)記住它已經(jīng)存在了很長(zhǎng)時(shí)間——它是在 Swift 出現(xiàn)之前設(shè)計(jì)的,更不用說(shuō) SwiftUI,所以偶爾你會(huì)遇到一些不能正常工作的部分正如我們希望的那樣,在 Swift 中也是如此。希望我們會(huì)在未來(lái)幾年看到這種情況的改善,但與此同時(shí)請(qǐng)耐心等待!
我們有很多 Core Data 需要探索,所以請(qǐng)創(chuàng)建一個(gè)我們可以嘗試的新項(xiàng)目。稱(chēng)它為“CoreDataProject”而不僅僅是“CoreData”,因?yàn)檫@會(huì)導(dǎo)致 Xcode 混淆。
確保你沒(méi)有選中“Use Core Data”復(fù)選框。相反,請(qǐng)從你的 Bookworm 項(xiàng)目中復(fù)制 DataController.swift,創(chuàng)建一個(gè)名為 CoreDataProject.xcdatamodeld 的新的空數(shù)據(jù)模型,然后更新 CoreDataProjectApp.swift 以創(chuàng)建一個(gè)DataController
實(shí)例并將其注入 SwiftUI 環(huán)境。這將使你離開(kāi)我們?cè)?Bookworm 項(xiàng)目的早期階段開(kāi)始的地方。
重要提示:當(dāng)你將DataController
從 Bookworm 復(fù)制到這個(gè)新項(xiàng)目時(shí),數(shù)據(jù)模型文件已經(jīng)改變——確保你更改NSPersistentContainer
初始化程序以引用新的數(shù)據(jù)模型文件而不是 Bookworm。
隨著你的進(jìn)步,有時(shí)你可能會(huì)發(fā)現(xiàn) Xcode 會(huì)忽略在 Core Data 編輯器中所做的更改,因此我喜歡在轉(zhuǎn)到另一個(gè)文件之前按 Cmd+S 來(lái)說(shuō)明這一點(diǎn)。如果失敗,請(qǐng)重新啟動(dòng) Xcode!
可以了,好了?我們走吧!
提示:有時(shí)你會(huì)看到標(biāo)題為“想要更進(jìn)一步?”的標(biāo)題。這包含一些額外的示例,可幫助你進(jìn)一步了解知識(shí),但除非你愿意,否則無(wú)需遵循此處 - 將其視為額外的學(xué)分。

為什么 \.self 為 ForEach 工作?
之前我們研究了ForEach
可用于創(chuàng)建動(dòng)態(tài)視圖的各種方法,但它們都有一個(gè)共同點(diǎn):SwiftUI 需要知道如何唯一地標(biāo)識(shí)每個(gè)動(dòng)態(tài)視圖,以便它可以正確地動(dòng)畫(huà)更改。
如果對(duì)象符合Identifiable
協(xié)議,則 SwiftUI 將自動(dòng)使用其id
屬性進(jìn)行唯一化。如果我們不使用Identifiable
,那么我們可以使用我們知道是唯一的屬性的鍵路徑,例如一本書(shū)的 ISBN 號(hào)。但是如果我們不符合Identifiable
并且沒(méi)有唯一的鍵路徑,我們通??梢允褂?span id="s0sssss00s" class="color-pink-02">\.self
.
以前我們使用\.self
原始類(lèi)型,例如Int
和
String
,如下所示:
有了 Core Data,我們可以根據(jù)需要使用唯一的標(biāo)識(shí)符,但我們也可以使用\.self
并擁有一些運(yùn)行良好的東西。
當(dāng)我們用\.self
作標(biāo)識(shí)符時(shí),我們指的是“整個(gè)對(duì)象”,但實(shí)際上這并不意味著什么——結(jié)構(gòu)就是結(jié)構(gòu),因此除了其內(nèi)容之外,它沒(méi)有任何類(lèi)型的特定標(biāo)識(shí)信息。所以實(shí)際發(fā)生的是 Swift 計(jì)算結(jié)構(gòu)的散列值,這是一種以固定大小的值表示復(fù)雜數(shù)據(jù)的方式,然后使用該散列作為標(biāo)識(shí)符。
哈希值可以通過(guò)多種方式生成,但所有哈希生成函數(shù)的概念都是相同的:
無(wú)論輸入大小如何,輸出都應(yīng)該是相同的固定大小。
連續(xù)兩次為一個(gè)對(duì)象計(jì)算相同的散列應(yīng)該返回相同的值。
這兩個(gè)聽(tīng)起來(lái)很簡(jiǎn)單,但想一想:如果我們得到“Hello World”的哈希值和莎士比亞全集的哈希值,最終兩者的大小將相同。這意味著不可能將哈希值轉(zhuǎn)換回其原始值——我們無(wú)法將 40 個(gè)看似隨機(jī)的十六進(jìn)制字母和數(shù)字轉(zhuǎn)換為莎士比亞全集。
哈希通常用于數(shù)據(jù)驗(yàn)證之類(lèi)的事情。例如,如果你下載一個(gè) 8GB 的 zip 文件,你可以通過(guò)將該文件的本地哈希值與服務(wù)器的哈希值進(jìn)行比較來(lái)檢查它是否正確——如果它們匹配,則意味著該 zip 文件是相同的。哈希也與字典鍵和集合一起使用;這就是他們快速查找的方式。
所有這些都很重要,因?yàn)楫?dāng) Xcode 為我們的托管對(duì)象生成一個(gè)類(lèi)時(shí),它使該類(lèi)符合Hashable
,這是一個(gè)協(xié)議,這意味著 Swift 可以為其生成哈希值,這反過(guò)來(lái)意味著我們可以使用\.self
標(biāo)識(shí)符。這也是為什么String
和Int
可以和\.self
一起工作
:他們也符合Hashable
。
Hashable
有點(diǎn)像Codable
:如果我們想讓自定義類(lèi)型符合Hashable
,那么只要它包含的所有內(nèi)容也符合Hashable
,
那么我們不需要做任何工作。為了證明這一點(diǎn),我們可以創(chuàng)建一個(gè)符合Hashable
而非的自定義結(jié)構(gòu)Identifiable
,并使用\.self
它來(lái)識(shí)別它:
我們可以使Student
con符合Hashable
因?yàn)樗乃袑傩远家呀?jīng)符合Hashable
,所以 Swift 會(huì)計(jì)算每個(gè)屬性的哈希值,然后將它們組合成一個(gè)代表整個(gè)結(jié)構(gòu)的哈希值。當(dāng)然,如果我們最終有兩個(gè)同名的學(xué)生,我們就會(huì)遇到問(wèn)題,就像我們有一個(gè)包含兩個(gè)相同字符串的字符串?dāng)?shù)組一樣。
現(xiàn)在,你可能認(rèn)為這會(huì)導(dǎo)致一個(gè)問(wèn)題:如果我們創(chuàng)建兩個(gè)具有相同值的 Core Data 對(duì)象,它們將生成相同的散列,我們就會(huì)遇到動(dòng)畫(huà)問(wèn)題。然而,Core Data 在這里做了一些非常聰明的事情:它為我們創(chuàng)建的對(duì)象實(shí)際上有一系列超出我們?cè)跀?shù)據(jù)模型中定義的屬性的選擇,包括一個(gè)稱(chēng)為對(duì)象 ID 的對(duì)象 ID——該對(duì)象唯一的標(biāo)識(shí)符,無(wú)論它包含什么屬性。這些 ID 類(lèi)似于UUID
,盡管 Core Data 在我們創(chuàng)建對(duì)象時(shí)按順序生成它們。
所以,\.self
適用于任何符合 Hashable
的東西
,因?yàn)?Swift 將為對(duì)象生成哈希值并使用它來(lái)唯一標(biāo)識(shí)它。這也適用于 Core Data 的對(duì)象,因?yàn)樗鼈円呀?jīng)符合Hashable
.?所以,如果你想使用一個(gè)很棒的特定標(biāo)識(shí)符,但你不需要,因?yàn)?strong>\.self
也是一個(gè)選項(xiàng)。
警告:雖然連續(xù)兩次為一個(gè)對(duì)象計(jì)算相同的哈希值應(yīng)該返回相同的值,但在應(yīng)用程序的兩次運(yùn)行之間計(jì)算它——即計(jì)算哈希值、退出應(yīng)用程序、重新啟動(dòng),然后再次計(jì)算哈希值——可能會(huì)返回不同的值。



創(chuàng)建 NSManagedObject 子類(lèi)
當(dāng)我們創(chuàng)建一個(gè)新的 Core Data 實(shí)體時(shí),Xcode 會(huì)在我們構(gòu)建代碼時(shí)自動(dòng)為我們生成一個(gè)托管對(duì)象類(lèi)。然后我們可以在 SwiftUI 中使用@FetchRequest
來(lái)在我們的用戶(hù)界面中顯示數(shù)據(jù),但正如你所見(jiàn),這非常痛苦:有很多可選值要解包,因此你需要散布 nil 合并,以使你的代碼正常工作。
有兩種解決方案:一種快速而簡(jiǎn)單但有時(shí)會(huì)出現(xiàn)問(wèn)題的解決方案,或者一種稍微慢一些但從長(zhǎng)遠(yuǎn)來(lái)看效果更好的解決方案。
首先,讓我們創(chuàng)建一個(gè)要使用的實(shí)體:打開(kāi)你的數(shù)據(jù)模型并創(chuàng)建一個(gè)名為 Movie 的實(shí)體,其屬性為“title”(字符串)、“director”(字符串)和“year”(整數(shù) 16)。在你離開(kāi)數(shù)據(jù)模型編輯器之前,我希望你轉(zhuǎn)到“查看”菜單并選擇“檢查器”>“數(shù)據(jù)模型”,這會(huì)在 Xcode 右側(cè)彈出一個(gè)窗格,其中包含有關(guān)你現(xiàn)在選擇的任何內(nèi)容的更多信息。
當(dāng)你選擇 Movie 時(shí),你會(huì)看到該實(shí)體的各種數(shù)據(jù)模型選項(xiàng),但我特別希望你查看一個(gè):“Codegen”。這控制了 Xcode 在我們構(gòu)建項(xiàng)目時(shí)如何將實(shí)體生成為托管對(duì)象類(lèi),默認(rèn)情況下它將是類(lèi)定義。我想將其更改為手動(dòng)/無(wú),這使我們可以完全控制課程的制作方式。
現(xiàn)在 Xcode 不再生成一個(gè)Movie
類(lèi)供我們?cè)诖a中使用,除非我們用一些真正的 Swift 代碼實(shí)際創(chuàng)建類(lèi),否則我們不能在代碼中使用它。為此,轉(zhuǎn)到“編輯器”菜單并選擇“創(chuàng)建 NSManagedObject 子類(lèi)”,確保選擇“CoreDataProject”,然后按“下一步”,然后確保選擇“電影”并再次按“下一步”。系統(tǒng)會(huì)詢(xún)問(wèn)你 Xcode 應(yīng)將其代碼保存在何處,因此請(qǐng)確保選擇左側(cè)帶有黃色文件夾圖標(biāo)的“CoreDataProject”,并同時(shí)選擇 CoreDataProject 文件夾。準(zhǔn)備就緒后,按創(chuàng)建以完成該過(guò)程。
我們剛剛所做的是要求 Xcode 將其生成的代碼轉(zhuǎn)換為我們可以看到和更改的實(shí)際 Swift 文件,但請(qǐng)記住,如果你更改 Xcode 為我們生成的文件然后重新生成這些文件,你的更改將會(huì)丟失。
Xcode 會(huì)為我們生成兩個(gè)文件,但我們只關(guān)心其中一個(gè):Movie+CoreDataProperties.swift。在里面你會(huì)看到這三行代碼:
在那一小段代碼中,你可以看到三件事:
這就是我們的可選問(wèn)題的來(lái)源。
year
不是可選的,這意味著 Core Data 將為我們假設(shè)一個(gè)默認(rèn)值。它用于
@NSManaged
所有三個(gè)屬性。
@NSManaged
不是屬性包裝器——這比 SwiftUI 中的屬性包裝器要老得多。相反,這揭示了一些 Core Data 的內(nèi)部工作方式:而不是那些實(shí)際作為屬性存在于類(lèi)中的值,它們實(shí)際上只是用來(lái)從 Core Data 用來(lái)存儲(chǔ)其信息的字典中讀取和寫(xiě)入。當(dāng)我們讀取或?qū)懭?span id="s0sssss00s" class="color-pink-02">@NSManaged
屬性的值時(shí)
,Core Data 會(huì)捕獲并在內(nèi)部處理它——它遠(yuǎn)非簡(jiǎn)單的 Swift 字符串。
現(xiàn)在,你可能會(huì)查看該代碼并認(rèn)為“我不想要那里的可選值”,并將其更改為:
你知道嗎?那絕對(duì)有效。你可以使用Movie
與以前相同的代碼創(chuàng)建對(duì)象,使用獲取請(qǐng)求來(lái)查詢(xún)它們,保存它們的托管對(duì)象上下文等等,所有這些都沒(méi)有問(wèn)題。
但是,你可能會(huì)注意到一些奇怪的事情:即使我們的屬性不再是可選的,仍然可以在Movie
不提供這些值的情況下創(chuàng)建類(lèi)的實(shí)例。這應(yīng)該是不可能的:這些屬性不是可選的,這意味著它們必須始終具有值,但我們可以在沒(méi)有值的情況下創(chuàng)建它們。
這里發(fā)生的是一點(diǎn)@NSManaged
魔法泄漏——記住,這些不是真實(shí)的屬性,因此@NSManaged
讓我們做一些不應(yīng)該工作的事情。事實(shí)上,它確實(shí)有效,而且對(duì)于小型 Core Data 項(xiàng)目和/或?qū)W習(xí)者,我認(rèn)為刪除可選性是一個(gè)好主意。然而,還有一個(gè)更深層次的問(wèn)題:Core Data 是惰性的。
還記得 Swift 的lazy
關(guān)鍵字嗎,它如何讓我們延遲工作直到我們真正需要它?Core Data 做了很多相同的事情,除了數(shù)據(jù):有時(shí)它看起來(lái)像一些數(shù)據(jù)已經(jīng)加載,但實(shí)際上并沒(méi)有加載,因?yàn)?Core Data 試圖最小化它的內(nèi)存影響。Core Data 在“斷層線(xiàn)”的意義上稱(chēng)這些斷層——存在某物的地方和某物只是占位符的地方之間的一條線(xiàn)。
我們不需要做任何特殊的工作來(lái)處理這些故障,因?yàn)橐坏┪覀儑L試讀取它們,Core Data 就會(huì)透明地獲取真實(shí)數(shù)據(jù)并將其發(fā)回——這是?@NSManaged
的另一個(gè)好處
。然而,當(dāng)我們開(kāi)始研究 Core Data 的屬性類(lèi)型時(shí),我們冒著暴露其特殊弱點(diǎn)的風(fēng)險(xiǎn)。這件事特別不能按照 Swift 預(yù)期的方式工作,如果我們?cè)噲D繞過(guò)它,那么我們幾乎會(huì)引發(fā)問(wèn)題——我們已經(jīng)說(shuō)過(guò)絕對(duì)不會(huì)為 nil 的值可能會(huì)在任何時(shí)候突然變?yōu)?nil。
相反,你可能需要考慮添加計(jì)算屬性以幫助我們安全地訪問(wèn)可選值,同時(shí)也讓我們將你的 nil 合并代碼全部存儲(chǔ)在一個(gè)地方。例如,將其添加為屬性Movie
可確保我們始終有一個(gè)有效的標(biāo)題字符串可供使用:
這樣你的代碼的其余部分就不必?fù)?dān)心 Core Data 的可選性,如果你想更改默認(rèn)值,你可以在一個(gè)文件中完成。



NSManagedObjectContext 的條件保存
我們一直在使用NSManagedObjectContext
的
方法save()
將所有未保存的更改清除到永久存儲(chǔ)中,但我們還沒(méi)有做的是檢查是否確實(shí)需要保存任何更改。這通常是可以的,因?yàn)?span id="s0sssss00s" class="color-pink-02">save()
只有在我們專(zhuān)門(mén)進(jìn)行更改(例如插入或刪除數(shù)據(jù))之后才發(fā)出調(diào)用是很常見(jiàn)的。
但是,將你的保存集中在一起以便一次保存所有內(nèi)容也很常見(jiàn),這對(duì)性能的影響較低。事實(shí)上,Apple 特別聲明我們應(yīng)該始終在調(diào)用save()
之前檢查未提交的更改
,以避免讓 Core Data 執(zhí)行不需要的工作。
幸運(yùn)的是,我們可以通過(guò)兩種方式檢查變化。首先,每個(gè)托管對(duì)象都有一個(gè)hasChanges
屬性,當(dāng)對(duì)象有未保存的更改時(shí)該屬性為真。并且,整個(gè)上下文還包含一個(gè)hasChanges
屬性,用于檢查上下文擁有的任何對(duì)象是否發(fā)生了變化。
因此,與其save()
直接調(diào)用,不如先將其包裝在支票中,如下所示:
這是一個(gè)很小的改變,但它很重要——做不需要的工作是沒(méi)有意義的,無(wú)論多么小,特別是因?yàn)槿绻阕隽俗銐蚨嗟男」ぷ?,你就?huì)意識(shí)到你已經(jīng)積累了一些大工作。



使用約束確保核心數(shù)據(jù)對(duì)象是唯一的
默認(rèn)情況下,Core Data 會(huì)添加任何你想要的對(duì)象,但這很快就會(huì)變得混亂,特別是當(dāng)你知道兩個(gè)或更多對(duì)象同時(shí)沒(méi)有意義時(shí)。例如,如果你使用電子郵件地址存儲(chǔ)聯(lián)系人的詳細(xì)信息,那么將兩個(gè)或三個(gè)不同的聯(lián)系人附加到同一個(gè)電子郵件地址就沒(méi)有意義。
為了幫助解決這個(gè)問(wèn)題,Core Data 給了我們約束:我們可以讓一個(gè)屬性受到約束,這樣它就必須始終是唯一的。然后我們可以繼續(xù)創(chuàng)建任意數(shù)量的對(duì)象,無(wú)論是否唯一,但只要我們要求 Core Data 保存這些對(duì)象,它就會(huì)解決重復(fù)項(xiàng),以便只寫(xiě)入一個(gè)數(shù)據(jù)。更好的是,如果已經(jīng)寫(xiě)入了一些與我們的約束沖突的數(shù)據(jù),我們可以選擇它應(yīng)該如何處理合并數(shù)據(jù)。
要嘗試這一點(diǎn),請(qǐng)創(chuàng)建一個(gè)名為 Wizard 的新實(shí)體,其中包含一個(gè)名為“name”的字符串屬性?,F(xiàn)在選擇 Wizard 實(shí)體,在數(shù)據(jù)模型檢查器中查找 Constraints,然后按下正下方的 + 按鈕。你應(yīng)該看到“comma,separated,properties”出現(xiàn),為我們提供了一個(gè)工作示例。選擇它并按 enter 重命名它,并給它文本“name”代替——這使我們的 name 屬性獨(dú)一無(wú)二。請(qǐng)記住按 Cmd+S 保存你的更改!
現(xiàn)在轉(zhuǎn)到 ContentView.swift 并為其提供以下代碼:
你可以看到它有一個(gè)用于顯示向?qū)У牧斜?,一個(gè)用于添加向?qū)У陌粹o和一個(gè)用于保存的第二個(gè)按鈕。當(dāng)你運(yùn)行該應(yīng)用程序時(shí),你會(huì)發(fā)現(xiàn)你可以多次按添加以查看“哈利波特”滑入表格,但是當(dāng)你按“保存”時(shí)我們會(huì)收到一個(gè)錯(cuò)誤 – Core Data 已檢測(cè)到碰撞并拒絕保存更改。
如果你希望Core Data 寫(xiě)入更改,你需要打開(kāi) DataController.swift 并調(diào)整 loadPersistentStores() 完成處理程序以指定在這種情況下應(yīng)如何合并數(shù)據(jù):
這要求 Core Data 根據(jù)屬性合并重復(fù)對(duì)象——它會(huì)嘗試使用新版本的屬性智能地覆蓋其數(shù)據(jù)庫(kù)中的版本。如果你再次運(yùn)行代碼,你會(huì)看到一些非常棒的東西:你可以按 Add 多次,但是當(dāng)你按 Save 時(shí),它會(huì)全部折疊成一行,因?yàn)?Core Data 刪除了重復(fù)項(xiàng)。


