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

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

今天我們將推進(jìn)更高級(jí)的Core Data技術(shù)——在功能和實(shí)用性方面真正讓應(yīng)用程序脫穎而出的技術(shù)。其中一些將需要一些時(shí)間來學(xué)習(xí),特別是因?yàn)楫?dāng)我們更多地深入到 Core Data 中時(shí),你將開始更多地看到它的 Objective-C 軟肋。
堅(jiān)持下去!正如 Maya Angelou 所說,“所有偉大的成就都需要時(shí)間”——理解 Core Data 在這里為我們所做的一切需要一些工作,但它會(huì)得到回報(bào),我相信你會(huì)喜歡在你的應(yīng)用程序。
今天你要完成三個(gè)主題,在這些主題中你將了解NSPredicate
、動(dòng)態(tài)更改提取請(qǐng)求、創(chuàng)建關(guān)系等。
有一次你會(huì)看到我說你已經(jīng)達(dá)到了一個(gè)很好的點(diǎn),可以繼續(xù)學(xué)習(xí)下一個(gè)教程,但如果你繼續(xù)超越那個(gè)點(diǎn),我們將探索一些更高級(jí)的主題。需要明確的是,額外的工作是可選的:如果你時(shí)間緊迫,或者只是想打下基礎(chǔ),請(qǐng)不要這樣做。

使用 NSPredicate 過濾 @FetchRequest
當(dāng)我們使用 SwiftUI 的@FetchRequest
屬性包裝器時(shí),我們可以提供一個(gè)排序描述符數(shù)組來控制結(jié)果的排序,但我們也可以提供一個(gè)NSPredicate
來控制應(yīng)該顯示哪些結(jié)果。謂詞是簡(jiǎn)單的測(cè)試,測(cè)試將應(yīng)用于我們Core Data實(shí)體中的每個(gè)對(duì)象——只有通過測(cè)試的對(duì)象才會(huì)包含在結(jié)果數(shù)組中。
NSPredicate
的語法
不是你可以輕易猜到的,但實(shí)際上你只需要幾種類型的謂詞,所以它并不像你想象的那么糟糕。
要嘗試一些謂詞,請(qǐng)創(chuàng)建一個(gè)名為 Ship 的新實(shí)體,它具有兩個(gè)字符串屬性:“name”和“universe”。
現(xiàn)在將 ContentView.swift 修改為:
我們現(xiàn)在可以按下按鈕將一些樣本數(shù)據(jù)注入 Core Data,但現(xiàn)在我們沒有謂詞。為了解決這個(gè)問題,我們需要將nil謂詞值替換為
可以應(yīng)用于我們的對(duì)象的某種測(cè)試。
例如,我們可以像這樣請(qǐng)求星球大戰(zhàn)中的飛船:
如果你的數(shù)據(jù)包含引號(hào),這會(huì)變得很復(fù)雜,因此使用特殊語法更常見:`%@' 表示“在此處插入一些數(shù)據(jù)”,并允許我們將該數(shù)據(jù)作為參數(shù)提供給謂詞而不是內(nèi)聯(lián)。
所以,我們可以這樣寫:
除了==
,我們還可以使用諸如<
和>
之類的比較
來過濾我們的對(duì)象。例如,這將返回 Defiant、Enterprise 和 Executor:
%@
在幕后做了很多工作,特別是在將原生 Swift 類型轉(zhuǎn)換為 Core Data 等價(jià)物時(shí)。例如,我們可以使用IN
謂詞來檢查 universe 是否是數(shù)組中的三個(gè)選項(xiàng)之一,如下所示:
我們還可以使用謂詞來檢查字符串的一部分,使用諸如BEGINS
WITH和CONTAINS
之類的運(yùn)算符。例如,這將返回所有以大寫字母 E 開頭的船只:
該謂詞區(qū)分大小寫;如果你想忽略大小寫,則需要將其修改為:
CONTAINS[c]
工作方式類似,除了不是以你的子字符串開頭,它可以在屬性內(nèi)的任何位置。
最后,你可以翻轉(zhuǎn)謂詞使用NOT
, 以獲得它們常規(guī)行為的反轉(zhuǎn)。例如,這會(huì)找到所有不以 E 開頭的船只:
如果你需要更復(fù)雜的謂詞,將它們加入使用AND
以根據(jù)需要建立盡可能多的精度,或者添加一個(gè) Core Data 導(dǎo)入并查看NSCompoundPredicate
- 它可以讓你從幾個(gè)較小的謂詞中構(gòu)建一個(gè)謂詞。



使用 SwiftUI 動(dòng)態(tài)過濾@FetchRequest
我被問到最多的 SwiftUI 問題之一是:如何動(dòng)態(tài)更改核心數(shù)據(jù)@FetchRequest
以使用不同的謂詞或排序順序?出現(xiàn)這個(gè)問題是因?yàn)楂@取請(qǐng)求是作為一個(gè)屬性創(chuàng)建的,所以如果你試圖讓它們引用另一個(gè)屬性,Swift 將拒絕。
這里有一個(gè)簡(jiǎn)單的解決方案,回想起來通常很明顯,因?yàn)樗瞧渌磺械墓ぷ鞣绞剑何覀儜?yīng)該將我們想要的功能分割成一個(gè)單獨(dú)的視圖,然后將值注入其中。
我想用一些真實(shí)的代碼來演示這一點(diǎn),所以我把最簡(jiǎn)單的例子放在一起:它向 Core Data 添加了三位歌手,然后使用兩個(gè)按鈕來顯示姓氏以 A 或 S 結(jié)尾的歌手。
首先創(chuàng)建一個(gè)名為 Singer 的新核心數(shù)據(jù)實(shí)體,并為其提供兩個(gè)字符串屬性:“firstName”和“l(fā)astName”。使用數(shù)據(jù)模型檢查器將其 Codegen 更改為 Manual/None,然后轉(zhuǎn)到 Editor 菜單并選擇 Create NSManagedObject Subclass 這樣我們就可以獲得一個(gè)Singer
我們可以自定義的類。
Xcode 為我們生成文件后,打開 Singer+CoreDataProperties.swift 并添加這兩個(gè)屬性,使該類更易于與 SwiftUI 一起使用:
好的,現(xiàn)在進(jìn)入真正的工作。
第一步是設(shè)計(jì)一個(gè)視圖來承載我們的信息。就像我說的,這也將有兩個(gè)按鈕,讓我們改變視圖的過濾方式,我們將有一個(gè)額外的按鈕來插入一些測(cè)試數(shù)據(jù),這樣你就可以看到它是如何工作的。
首先,向你的ContentView
結(jié)構(gòu)體添加兩個(gè)屬性,以便我們有一個(gè)可以將對(duì)象保存到的托管對(duì)象上下文,以及一些我們可以用作過濾器的狀態(tài):
對(duì)于視圖的主體,我們將使用VStack
帶有三個(gè)按鈕的 ,以及我們希望List
顯示匹配歌手的位置的注釋:
到目前為止,很容易?,F(xiàn)在是有趣的部分:我們需要用真實(shí)的東西替換那個(gè)評(píng)論// list of matching singers
。這不會(huì)被使用@FetchRequest
,因?yàn)槲覀兿M軌蛟诔跏蓟绦蛑袆?chuàng)建自定義獲取請(qǐng)求,但我們將使用的代碼幾乎是相同的。
創(chuàng)建一個(gè)名為“FilteredList”的新 SwiftUI 視圖,并為其賦予以下屬性:
這將存儲(chǔ)我們的獲取請(qǐng)求,以便我們可以在body
中循環(huán)遍歷它
.?然而,我們不在這里創(chuàng)建獲取請(qǐng)求,因?yàn)槲覀內(nèi)匀徊恢牢覀円阉魇裁础O喾?,我們將?chuàng)建一個(gè)自定義初始化器,它接受一個(gè)過濾器字符串并使用它來設(shè)置fetchRequest
屬性。
現(xiàn)在添加這個(gè)初始化器:
這將使用當(dāng)前托管對(duì)象上下文運(yùn)行獲取請(qǐng)求。因?yàn)檫@個(gè)視圖將在內(nèi)部使用ContentView
,所以我們甚至不需要將托管對(duì)象上下文注入到環(huán)境中——它會(huì)從ContentView
中繼承上下文
。
你是否注意到開頭有一個(gè)下劃線_fetchRequest
?那是故意的。你看,我們不是在獲取請(qǐng)求中寫入獲取的結(jié)果對(duì)象,而是編寫一個(gè)全新的獲取請(qǐng)求。
要理解這一點(diǎn),請(qǐng)考慮@State
屬性包裝器。在這個(gè)場(chǎng)景的背后,它被實(shí)現(xiàn)為一個(gè)名為 State
的結(jié)構(gòu)
,它包含我們放入其中的任何值——例如,一個(gè)整數(shù)。如果我們有一個(gè)@State
屬性叫做score
并將值 10 賦給它,我們的意思是將 10 放入State
屬性包裝器內(nèi)的整數(shù)中。然而,我們也可以給它賦值_score
——如果需要的話,我們可以在里面寫一個(gè)全新的State
結(jié)構(gòu)。
因此,通過分配給_fetchRequest
,我們并不是要說“這里有一大堆我們希望你使用的新結(jié)果”,而是告訴 Swift 我們想要更改整個(gè)獲取請(qǐng)求本身。
剩下的就是寫視圖的主體,所以給視圖這個(gè)主體:
至于FilteredList的預(yù)覽結(jié)構(gòu)
,你可以安全地將其刪除。
現(xiàn)在視圖已經(jīng)完成,我們可以返回ContentView
并用一些將我們的過濾器傳遞到的實(shí)際代碼替換注釋FilteredList
:
現(xiàn)在運(yùn)行程序試一試:首先點(diǎn)擊“添加示例”按鈕創(chuàng)建三個(gè)歌手對(duì)象,然后點(diǎn)擊“顯示 A”或“顯示 S”以在姓氏字母之間切換。你應(yīng)該看到我們List
使用不同的數(shù)據(jù)動(dòng)態(tài)更新,具體取決于你按下的按鈕。
因此,需要一些新知識(shí)才能完成這項(xiàng)工作,但實(shí)際上并沒有那么難——只要你像 SwiftUI 一樣思考,解決方案就在那里。
提示:你可能會(huì)查看我們的代碼并認(rèn)為每次重新創(chuàng)建視圖時(shí)——即每次容器視圖中的任何狀態(tài)更改時(shí)——我們也在重新創(chuàng)建獲取請(qǐng)求,這反過來意味著在沒有其他情況時(shí)從數(shù)據(jù)庫中讀取已經(jīng)改變。
這可能看起來非常浪費(fèi),如果它真的發(fā)生了,那將是非常浪費(fèi)。幸運(yùn)的是,Core Data 不會(huì)做這樣的事情:它只會(huì)在過濾器字符串更改時(shí)重新運(yùn)行數(shù)據(jù)庫查詢,即使重新創(chuàng)建視圖也是如此。
想更進(jìn)一步?
為了獲得更大的靈活性,我們可以改進(jìn)我們的FilteredList
視圖,使其適用于任何類型的實(shí)體,并且可以在任何字段上進(jìn)行過濾。為了使其正常工作,我們需要進(jìn)行一些更改:
我們將使用泛型,而不是專門引用
Singer
該類,但有一個(gè)約束,即傳入的任何內(nèi)容都必須是一個(gè)NSManagedObject
.我們需要接受第二個(gè)參數(shù)來決定我們要過濾的鍵名,因?yàn)槲覀兛赡苷谑褂脹]有
lastName
屬性的實(shí)體。因?yàn)槲覀兪孪炔恢烂總€(gè)實(shí)體將包含什么,所以我們將讓我們的包含視圖來決定。因此,我們不只是使用歌手名字的文本視圖,而是要求一個(gè)可以運(yùn)行的閉包來配置他們想要的視圖。
里面有兩個(gè)復(fù)雜的部分。第一個(gè)是決定每個(gè)列表行的內(nèi)容的閉包,因?yàn)樗枰褂脙蓚€(gè)重要的語法。我們?cè)谠缙陉P(guān)于視圖和修飾符的技術(shù)項(xiàng)目即將結(jié)束時(shí)查看了這些內(nèi)容,但如果你錯(cuò)過了它們:
如果需要,
@ViewBuilder
讓我們的包含視圖(無論使用列表的是什么)發(fā)送多個(gè)視圖。@escaping
表示閉包將被存儲(chǔ)起來并在以后使用,這意味著 Swift 需要照顧好它的內(nèi)存。
第二個(gè)復(fù)雜的部分是我們?nèi)绾巫屓萜饕晥D自定義搜索鍵。以前我們是這樣控制過濾值的:
因此,你可能會(huì)進(jìn)行有根據(jù)的猜測(cè)并編寫如下代碼:
但是,那是行不通的。你看,當(dāng)我們編寫%@?
Core Data 時(shí),會(huì)自動(dòng)為我們插入引號(hào),以便謂詞正確讀取。這很有用,因?yàn)槿绻覀兊淖址?hào),它會(huì)自動(dòng)確保它們不會(huì)與它添加的引號(hào)沖突。
這意味著當(dāng)我們使用%@
屬性名稱時(shí),我們可能會(huì)得到這樣的謂詞:
這是不正確的:屬性名稱不應(yīng)包含在引號(hào)中。
為了解決這個(gè)問題,NSPredicate
有一個(gè)特殊的符號(hào)可以用來替換屬性名稱:%K
,代表“鍵”。這將插入我們提供的值,但不會(huì)在它們周圍添加引號(hào)。正確的謂詞是這樣的:
所以,為 CoreData 添加一個(gè)導(dǎo)入,以便我們可以引用NSManagedObject
,然后用這個(gè)替換你當(dāng)前的FilteredList
結(jié)構(gòu):
我們現(xiàn)在可以通過ContentView
像這樣升級(jí)來使用新的過濾列表:
請(qǐng)注意我是如何專門用作(singer: Singer)
閉包參數(shù)的——這是必需的,以便 Swift 了解FilteredList
其使用方式。請(qǐng)記住,我們說過它可以是任何類型的NSManagedObject
,但為了讓 Swift 準(zhǔn)確知道它是什么類型的托管對(duì)象,我們需要明確說明。
不管怎樣,有了這個(gè)改變,我們現(xiàn)在可以將我們的列表與任何類型的過濾器鍵和任何類型的實(shí)體一起使用——它更有用了!



與 Core Data、SwiftUI 和 @FetchRequest 的一對(duì)多關(guān)系
Core Data 允許我們使用關(guān)系將實(shí)體鏈接在一起,當(dāng)我們使用@FetchRequest
?
Core Data 時(shí),會(huì)將所有數(shù)據(jù)發(fā)送回給我們使用。然而,這是 Core Data 略顯陳舊的一個(gè)領(lǐng)域:為了讓關(guān)系運(yùn)作良好,我們需要?jiǎng)?chuàng)建一個(gè)自定義NSManagedObject
子類,提供對(duì) SwiftUI 更友好的包裝器。
為了證明這一點(diǎn),我們將構(gòu)建兩個(gè) Core Data 實(shí)體:一個(gè)用于跟蹤糖果棒,另一個(gè)用于跟蹤這些棒棒糖的來源國。
關(guān)系有四種形式:
一對(duì)一關(guān)系意味著一個(gè)實(shí)體中的一個(gè)對(duì)象恰好鏈接到另一個(gè)實(shí)體中的一個(gè)對(duì)象。在我們的例子中,這意味著每種糖果都有一個(gè)原產(chǎn)國,每個(gè)國家只能生產(chǎn)一種糖果。
一對(duì)多關(guān)系意味著實(shí)體中的一個(gè)對(duì)象鏈接到另一個(gè)實(shí)體中的許多對(duì)象。在我們的例子中,這意味著一種糖果可以同時(shí)在許多國家推出,但每個(gè)國家仍然只能生產(chǎn)一種糖果。
多對(duì)一關(guān)系意味著一個(gè)實(shí)體中的許多對(duì)象鏈接到另一個(gè)實(shí)體中的一個(gè)對(duì)象。在我們的示例中,這意味著每種糖果都有一個(gè)原產(chǎn)國,并且每個(gè)國家可以生產(chǎn)多種糖果。
多對(duì)多關(guān)系意味著一個(gè)實(shí)體中的許多對(duì)象鏈接到另一個(gè)實(shí)體中的許多對(duì)象。在我們的示例中,這意味著許多國家同時(shí)引入了一種糖果,并且每個(gè)國家都可以生產(chǎn)多種糖果。
所有這些都在不同的時(shí)間使用,但在我們的糖果示例中,多對(duì)一關(guān)系最有意義——每種糖果都是在一個(gè)國家發(fā)明的,但每個(gè)國家可以發(fā)明多種糖果。
因此,打開你的數(shù)據(jù)模型并添加兩個(gè)實(shí)體:Candy,帶有名為“name”的字符串屬性,以及 Country,帶有名為“fullName”和“shortName”的字符串屬性。盡管某些類型的糖果具有相同的名稱(如美國和英國的“Smarties”),但國家/地區(qū)絕對(duì)是獨(dú)一無二的,因此請(qǐng)為“shortName”添加一個(gè)約束條件。
提示:如果你忘記了如何添加約束,請(qǐng)不要擔(dān)心:選擇 Country 實(shí)體,轉(zhuǎn)到 View 菜單并選擇 Inspectors > Data Model,單擊 Constraints 下的 + 按鈕,然后將示例重命名為“shortName”。
在完成此數(shù)據(jù)模型之前,我們需要告訴 Core Data Candy 和 Country 之間存在一對(duì)多關(guān)系:
選擇國家/地區(qū)后,在關(guān)系表下按 +。調(diào)用關(guān)系“candy”,將其目的地更改為 Candy,然后在數(shù)據(jù)模型檢查器中將 Type 更改為 To Many。
現(xiàn)在選擇 Candy,并在那里添加另一個(gè)關(guān)系。將關(guān)系稱為“來源”,將其目的地更改為“國家/地區(qū)”,然后將其倒數(shù)設(shè)置為“糖果”,以便 Core Data 理解鏈接是雙向的。
這就完成了我們的實(shí)體,下一步是查看 Xcode 為我們生成的代碼。請(qǐng)記住按 Cmd+S 強(qiáng)制 Xcode 保存你的更改。
選擇 Candy 和 Country 并將它們的 Codegen 設(shè)置為 Manual/None,然后轉(zhuǎn)到 Editor 菜單并選擇 Create NSManagedObject Subclass 為我們的兩個(gè)實(shí)體創(chuàng)建代碼——記住將它們保存在 CoreDataProject 組和文件夾中。
當(dāng)我們選擇兩個(gè)實(shí)體時(shí),Xcode 將為我們生成四個(gè) Swift 文件。Candy+CoreDataProperties.swift 幾乎完全符合你的期望,盡管請(qǐng)注意origin
現(xiàn)在是一個(gè)Country
.?Country+CoreDataProperties.swift比較復(fù)雜,因?yàn)閄code也生成了一些方法供我們使用。
之前我們研究了如何使用子類清理 Core Data 的可選值NSManagedObject
,但這里有一個(gè)額外的復(fù)雜性:Country
類有一個(gè)candy
屬性是NSSet
.?這是舊的 Objective-C 數(shù)據(jù)類型,相當(dāng)于 Swift 的Set
,但我們不能將它與 SwiftUI 一起使用ForEach
。
為了解決這個(gè)問題,我們需要修改 Xcode 為我們生成的文件,添加使 SwiftUI 運(yùn)行良好的便利包裝器。對(duì)于這個(gè)Candy
類來說,這就像包裝name
屬性一樣簡(jiǎn)單,以便它始終返回一個(gè)字符串:
對(duì)于這個(gè)Country
類,我們可以在shortName
和
fullName
周圍創(chuàng)建相同的字符串包裝器,如下所示:
然而,當(dāng)涉及到candy
時(shí),事情就復(fù)雜多了
。這是一個(gè)NSSet
,它可以包含任何東西,因?yàn)?Core Data 沒有將它限制為Candy
.
所以,為了讓這個(gè)東西變成對(duì) SwiftUI 有用的形式,我們需要:
將它從一個(gè)
NSSet
轉(zhuǎn)換
為Set<Candy>
——我們知道其內(nèi)容類型的 Swift 原生類型。將其
Set<Candy>
轉(zhuǎn)換為數(shù)組,以便ForEach
可以從那里讀取單個(gè)值。對(duì)該數(shù)組進(jìn)行排序,使糖果條以合理的順序排列。
Swift 實(shí)際上讓我們同時(shí)執(zhí)行步驟 2 和 3,因?yàn)閷?duì)集合進(jìn)行排序會(huì)自動(dòng)返回一個(gè)數(shù)組。然而,對(duì)數(shù)組進(jìn)行排序比你想象的要難:這是一個(gè)自定義類型的數(shù)組,所以我們不能只使用sorted()
并讓 Swift 弄清楚它。相反,我們需要提供一個(gè)閉包來接受兩個(gè)糖果塊,如果第一塊糖果應(yīng)該排在第二塊之前,則返回 true。
因此,現(xiàn)在請(qǐng)將此計(jì)算屬性添加到Country
:
這就完成了我們的Core Data類,所以現(xiàn)在我們可以編寫一些 SwiftUI 代碼來完成所有這些工作。
打開 ContentView.swift 并賦予它以下兩個(gè)屬性:
請(qǐng)注意我們不需要在我們的獲取請(qǐng)求中指定任何關(guān)于關(guān)系的信息——Core Data 理解實(shí)體是鏈接的,所以它會(huì)根據(jù)需要獲取它們。
至于視圖的主體,我們將使用一個(gè)List
包含兩個(gè)ForEach
視圖的 :一個(gè)為每個(gè)國家/地區(qū)創(chuàng)建一個(gè)部分,另一個(gè)為每個(gè)國家/地區(qū)創(chuàng)建糖果。這個(gè)List
將依次進(jìn)入 VStack
因此我們可以在下面添加一個(gè)按鈕來生成一些示例數(shù)據(jù):
請(qǐng)務(wù)必運(yùn)行該代碼,因?yàn)樗\(yùn)行良好——當(dāng)點(diǎn)擊“添加”按鈕時(shí),我們所有的糖果條都會(huì)自動(dòng)分類。更好的是,因?yàn)槲覀冊(cè)?strong>NSManagedObject
子類中完成了所有繁重的工作,生成的 SwiftUI 代碼實(shí)際上非常簡(jiǎn)單——它不知道幕后有一個(gè)NSSet
,因此更容易理解。
提示:如果你在按添加后沒有看到你的糖果條被分類到不同的部分,請(qǐng)確保你沒有從類DataController
中刪除mergePolicy
更改。提醒一下,它應(yīng)該設(shè)置為NSMergePolicy.mergeByPropertyObjectTrump
.


