SwiftUI學(xué)習(xí)100天(Day90 - 項(xiàng)目 17,第五部分)

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

今天我們通過添加一些最終功能并修復(fù)大量錯(cuò)誤來(lái)結(jié)束我們的程序。是的,我們的程序有錯(cuò)誤,我將向你介紹其中的一些錯(cuò)誤,并向你展示如何修復(fù)它們。
當(dāng)你學(xué)習(xí)編程時(shí),發(fā)現(xiàn)代碼中的錯(cuò)誤可能會(huì)令人沮喪,因?yàn)楦杏X就像你搞砸了一樣。但正如傳奇的荷蘭計(jì)算機(jī)科學(xué)家 Edsger Dijkstra 曾經(jīng)說(shuō)過的那樣,“如果調(diào)試是消除錯(cuò)誤的過程,那么編程一定是將它們放入的過程?!?/p>
換句話說(shuō),當(dāng)你在開發(fā)軟件時(shí),修復(fù)錯(cuò)誤是理所當(dāng)然的,因?yàn)槲覀儾⒉煌昝?。你?duì)創(chuàng)建錯(cuò)誤、查找錯(cuò)誤和修復(fù)錯(cuò)誤越自在,你就會(huì)成為更好的開發(fā)人員。
今天你要完成三個(gè)主題,其中你將添加觸覺反饋,修復(fù)我們應(yīng)用程序中的許多錯(cuò)誤,然后添加一個(gè)新屏幕來(lái)編輯卡片。

使用 UINotificationFeedbackGenerator 讓 iPhone 振動(dòng)
iOS 帶有許多用于生成觸覺反饋的選項(xiàng),它們都可供我們?cè)?SwiftUI 中使用。在最簡(jiǎn)單的形式中,這就像創(chuàng)建UIFeedbackGenerator
其中一個(gè)子類的實(shí)例然后在
你準(zhǔn)備好時(shí)觸發(fā)它一樣簡(jiǎn)單,但是為了更精確地控制反饋,你應(yīng)該首先調(diào)用它的prepare()
方法讓 Taptic Engine 有機(jī)會(huì)預(yù)熱.
重要提示:預(yù)熱 Taptic Engine 有助于減少我們播放效果和實(shí)際發(fā)生之間的延遲,但它也會(huì)對(duì)電池產(chǎn)生影響,因此系統(tǒng)只會(huì)在你調(diào)用prepare()
后準(zhǔn)備一兩秒鐘
。
我們可以使用幾個(gè)不同的UIFeedbackGenerator
子類
,但我們?cè)谶@里要使用的是UINotificationFeedbackGenerator
,
因?yàn)樗峁┝嗽?iOS 中常見的成功和失敗觸覺。現(xiàn)在,我們可以向ContentView
添加一個(gè)
UINotificationFeedbackGenerator
的中心
實(shí)例,但這會(huì)導(dǎo)致一個(gè)問題:ContentView
在
每當(dāng)一張卡片被移除時(shí)都會(huì)收到通知,但在拖動(dòng)過程中不會(huì)收到通知,這意味著我們沒有機(jī)會(huì)預(yù)熱啟動(dòng) Taptic Engine。
因此,我們將為每個(gè)CardView
提供
自己的UINotificationFeedbackGenerator
實(shí)例,
以便他們可以根據(jù)需要準(zhǔn)備和播放它們。系統(tǒng)將負(fù)責(zé)確保觸覺都整齊排列,因此它們不會(huì)以某種方式混淆。
將此新屬性添加到CardView
:
現(xiàn)在找到CardView
拖動(dòng)手勢(shì)中的
,并將整個(gè)閉包更改為:removal?()
行
僅此一項(xiàng)就足以在我們的應(yīng)用程序中獲得觸覺,但始終存在觸覺延遲的風(fēng)險(xiǎn),因?yàn)?Taptic Engine 尚未準(zhǔn)備就緒。在這種情況下,觸覺仍會(huì)播放,但可能會(huì)延遲半秒——足以讓人感覺與我們的用戶界面有一點(diǎn)點(diǎn)脫節(jié)。
為了改善這一點(diǎn),我們需要在觸發(fā)觸覺之前稍微調(diào)用一下prepare()
。在激活之前立即調(diào)用prepare()是不夠的
:這樣做不會(huì)給Taptic?Engine足夠的預(yù)熱時(shí)間,因此你不會(huì)看到延遲有任何減少。相反,你應(yīng)該在知道可能需要觸覺時(shí)立即調(diào)用
prepare()
。
現(xiàn)在,你應(yīng)該了解兩個(gè)有用的實(shí)施細(xì)節(jié)。
首先,可以調(diào)用prepare()
然后永遠(yuǎn)不會(huì)觸發(fā)效果 - 系統(tǒng)將使 Taptic Engine 準(zhǔn)備好幾秒鐘,然后再次將其關(guān)閉。如果你反復(fù)調(diào)用prepare()
并且從未觸發(fā)它,系統(tǒng)可能會(huì)開始忽略你的prepare()
調(diào)用,直到至少發(fā)生一種效果。
其次,完全允許在觸發(fā)一次之前調(diào)用prepare()
多次——?prepare()
在 Taptic Engine 預(yù)熱時(shí)不會(huì)暫停你的應(yīng)用程序,并且在系統(tǒng)已經(jīng)準(zhǔn)備好時(shí)也不會(huì)產(chǎn)生任何實(shí)際性能成本。
將這兩個(gè)放在一起,我們將更新我們的拖動(dòng)手勢(shì),以便在手勢(shì)發(fā)生變化時(shí)調(diào)用prepare()
。這意味著在最終觸發(fā)觸覺之前它可能會(huì)被調(diào)用一百次,因?yàn)槊看斡脩粢苿?dòng)手指時(shí)它都會(huì)被觸發(fā)。
因此,將你的onChanged()
閉包修改為:
現(xiàn)在繼續(xù)嘗試該應(yīng)用程序,看看你的想法 - 根據(jù)你滑動(dòng)的方向,你應(yīng)該能夠感受到兩種截然不同的觸覺。
在我們結(jié)束觸覺之前,我希望你考慮一件事。多年前,百事公司向商場(chǎng)購(gòu)物者發(fā)起“百事可樂挑戰(zhàn)賽”:先喝一口可樂,再喝一口另一種,看看你更喜歡哪一種。結(jié)果發(fā)現(xiàn),與可口可樂相比,更多美國(guó)人更喜歡百事可樂,盡管可口可樂的市場(chǎng)份額要大得多。然而,有一個(gè)問題:人們似乎在測(cè)試中選擇了百事可樂,因?yàn)榘偈驴蓸返奈兜栏?,雖然它在小口量中效果很好,但在罐裝和瓶裝中效果不佳,而人們實(shí)際上更喜歡可口可樂。
我這么說(shuō)的原因是因?yàn)槲覀冊(cè)谖覀兊膽?yīng)用程序中添加了兩個(gè)觸覺通知,這些通知會(huì)經(jīng)常播放。當(dāng)你進(jìn)行小劑量測(cè)試時(shí),這些觸覺可能感覺很棒 – 你讓手機(jī)嗡嗡作響,這真的很令人愉快。但是,如果你是這個(gè)應(yīng)用程序的忠實(shí)用戶,那么我們的觸覺可能會(huì)遇到兩個(gè)問題:
用戶可能會(huì)覺得它們很煩人,因?yàn)樗鼈兠績(jī)傻饺刖蜁?huì)發(fā)生一次,具體取決于它們的速度。
更糟糕的是,用戶可能對(duì)它們變得不敏感——它們失去了所有有用性,無(wú)論是作為通知還是作為一點(diǎn)點(diǎn)喜悅的火花。
所以,既然你已經(jīng)親自嘗試過了,我希望你考慮一下應(yīng)該如何使用它們。如果這是我的應(yīng)用程序,我可能會(huì)保留失敗的觸覺,但我認(rèn)為成功的觸覺可能會(huì)消失——那個(gè)可能最常被觸發(fā),這意味著當(dāng)失敗的觸覺播放時(shí)感覺會(huì)更特別一些。



修復(fù)錯(cuò)誤
到目前為止,我們的 SwiftUI 應(yīng)用程序看起來(lái)不錯(cuò):我們有一堆可以拖動(dòng)來(lái)控制應(yīng)用程序的卡片,還有觸覺反饋和一些輔助功能支持。但與此同時(shí),它也充滿了阻礙它發(fā)展的故障——有些大,有些小,但都值得解決。
首先,可以在卡片不在頂部時(shí)四處拖動(dòng)卡片。這讓用戶感到困惑,因?yàn)樗麄兛梢宰ト∫粡埶麄儗?shí)際上看不到的卡片,所以這需要永遠(yuǎn)不可能。
為了解決這個(gè)問題,我們將使用allowsHitTesting()
這樣只有最后一張卡片——最上面的那張——可以被拖來(lái)拖去。找到在ContentView
中的
stacked()
修改器并直接在下面添加:
其次,我們的 UI 在與 VoiceOver 一起使用時(shí)有點(diǎn)亂。如果你在啟用了 VoiceOver 的真實(shí)設(shè)備上啟動(dòng)它,你會(huì)發(fā)現(xiàn)你可以點(diǎn)擊背景圖像來(lái)讀出“背景,圖像”,這是毫無(wú)意義的。然而,事情變得更糟了:向右輕掃一下,VoiceOver 就會(huì)在所有輔助元素之間移動(dòng)——它會(huì)讀出我們所有卡片中的文本,即使是那些不可見的卡片。
要解決背景圖像問題,我們應(yīng)該讓它使用裝飾圖像,這樣它就不會(huì)作為輔助功能布局的一部分被讀出。將背景圖片修改為:
要修復(fù)卡片,我們需要使用與我們一分鐘前添加的accessibilityHidden()
修改器具有相似條件的修改器allowsHitTesting()
。在這種情況下,索引小于頂部卡片的每張卡片都應(yīng)該從可訪問性系統(tǒng)中隱藏,因?yàn)樗鼘?duì)卡片沒有任何用處,因此將其直接添加到allowsHitTesting()
修飾符下方
:
我們的應(yīng)用程序存在第三個(gè)可訪問性問題,這是使用手勢(shì)控制事物的直接結(jié)果。是的,大多數(shù)時(shí)候使用手勢(shì)非常有趣,但如果你有特定的輔助功能需求,則可能很難使用它們。
在此應(yīng)用程序中,我們的手勢(shì)導(dǎo)致了多個(gè)問題:VoiceOver 用戶不清楚他們應(yīng)該如何控制該應(yīng)用程序:
我們不會(huì)說(shuō)卡片是可以點(diǎn)擊的按鈕。
當(dāng)答案被揭示時(shí),沒有聲音通知它是什么。
用戶無(wú)法通過向左或向右滑動(dòng)來(lái)瀏覽卡片。
解決這些問題只需要很少的工作,但回報(bào)是我們的應(yīng)用程序更容易為每個(gè)人所用。
首先,我們需要明確我們的卡片是可點(diǎn)擊的按鈕。這就像在.isButton
中添加
accessibilityAddTraits()
到CardView
的
。把這個(gè)放在它的ZStack
一樣簡(jiǎn)單opacity()
修飾符之后:
現(xiàn)在系統(tǒng)將顯示“誰(shuí)在神秘博士中扮演第 13 位醫(yī)生?按鈕”——向用戶提示卡片可以被點(diǎn)擊的重要提示。
其次,我們需要幫助系統(tǒng)讀取卡片的答案以及問題?,F(xiàn)在這是可能的,但前提是用戶在屏幕上四處滑動(dòng)——這遠(yuǎn)非顯而易見。因此,為了解決這個(gè)問題,我們將檢測(cè)用戶是否在他們的設(shè)備上啟用了輔助功能,如果啟用,則在顯示提示和顯示答案之間自動(dòng)切換。也就是說(shuō),我們不會(huì)將答案顯示在提示下方,而是將其關(guān)閉并只顯示答案,這將使 VoiceOver 立即讀出它。
SwiftUI 提供了一個(gè)特定的環(huán)境屬性來(lái)告訴我們 VoiceOver 何時(shí)運(yùn)行,稱為accessibilityVoiceOverEnabled
.?因此,將此新屬性添加到CardView
:
現(xiàn)在我們顯示提示和答案的代碼如下所示:
我們將對(duì)其進(jìn)行更改,以便將提示和答案顯示在單個(gè)文本視圖中,并由accessibilityEnabled
決定顯示哪種布局。將你的代碼修改為:
如果你使用 VoiceOver 嘗試一下,你會(huì)發(fā)現(xiàn)它的效果要好得多——只要雙擊名片,答案就會(huì)被讀出。
第三,我們需要讓用戶更容易將卡片標(biāo)記為正確或錯(cuò)誤,因?yàn)楝F(xiàn)在我們的圖像還不能切割它。它們不僅會(huì)阻止用戶使用點(diǎn)擊手勢(shì)與我們的應(yīng)用程序進(jìn)行交互,還會(huì)被讀作他們的 SF Symbols 名稱——“復(fù)選標(biāo)記、圓圈、圖像”——而不是任何有用的東西。
為了解決這個(gè)問題,我們需要用實(shí)際移除卡片的按鈕替換圖像。如果用戶是正確的或錯(cuò)誤的,我們實(shí)際上并沒有做任何不同的事情——我需要為你的挑戰(zhàn)留下一些東西!– 但我們至少可以從牌組中取出最上面的牌。同時(shí),我們將提供可訪問性標(biāo)簽和提示,以便用戶更好地了解按鈕的功能。
所以,用這個(gè)新代碼用這些圖像替換你當(dāng)前的HStack
:
因?yàn)榧词棺詈笠粡埧ㄆ驯灰瞥?,這些按鈕仍保留在屏幕上,所以我們需要在
removeCard(at:)
?的開頭添加一個(gè)
guard
檢查
以確保我們不會(huì)嘗試移除不存在的卡片。因此,將這一行新代碼放在該方法的開頭:
最后,我們可以在啟用differentiateWithoutColor
或啟用 VoiceOver 時(shí)使這些按鈕可見。這意味著將另一個(gè)accessibilityVoiceOverEnabled
屬性添加到ContentView
:
然后將if differentiateWithoutColor {
條件修改為:
通過這些可訪問性更改,我們的應(yīng)用程序?qū)γ總€(gè)人都更好地工作——干得好!
在我們完成之前,我想添加一個(gè)小的額外更改。現(xiàn)在,如果你稍微拖動(dòng)一個(gè)圖像然后放手,我們將它的偏移量設(shè)置回零,這會(huì)導(dǎo)致它跳回到屏幕的中心。如果我們將彈簧動(dòng)畫附加到我們的卡片上,它將滑入中心,我認(rèn)為這可以更清楚地向我們的用戶指示實(shí)際發(fā)生的事情。
要做到這一點(diǎn),請(qǐng)?jiān)?span id="5tt3ttt3t" class="color-pink-02">CardView
的
ZStack
末尾添加一個(gè)
animation()
修飾符
緊跟在onTapGesture()
后面:
好多了!
提示:如果仔細(xì)觀察,你可能會(huì)注意到,如果將卡片向右拖動(dòng)一點(diǎn)然后松開,卡片會(huì)閃爍紅色。稍后會(huì)詳細(xì)介紹!



添加和刪除卡片
到目前為止,我們所做的一切都使用了一組固定的示例卡片,但當(dāng)然,只有當(dāng)用戶可以真正自定義他們看到的卡片列表時(shí),這個(gè)應(yīng)用程序才有用。這意味著添加一個(gè)列出所有現(xiàn)有卡片的新視圖,并允許用戶添加一個(gè)新的視圖,這是你以前見過的所有內(nèi)容。然而,這次有一個(gè)有趣的問題需要一些新的東西來(lái)修復(fù),所以值得解決這個(gè)問題。
首先我們需要一些狀態(tài)來(lái)控制我們的編輯屏幕是否可見。因此,將其添加到ContentView
:
接下來(lái)我們需要添加一個(gè)按鈕以在點(diǎn)擊時(shí)翻轉(zhuǎn)該布爾值,因此找到if differentiateWithoutColor || accessibilityEnabled
條件并將其放在它之前:
我們將設(shè)計(jì)一個(gè)新EditCards
視圖,來(lái)將Card
數(shù)組編碼和解碼
為UserDefaults
,但在我們這樣做之前,我希望你使Card
結(jié)構(gòu)符合Codable
如下所示:
現(xiàn)在創(chuàng)建一個(gè)名為“EditCards”的新 SwiftUI 視圖。這需要:
有自己的
Card
數(shù)組。包裹在
NavigationView
中
,這樣我們就可以添加一個(gè)完成按鈕來(lái)關(guān)閉視圖。有一個(gè)顯示所有現(xiàn)有卡片的列表。
添加滑動(dòng)以刪除這些卡片。
在列表頂部有一個(gè)部分,以便用戶可以添加新卡。
具有從
UserDefaults
.
我們之前已經(jīng)看過所有這些代碼,所以我不打算在這里再次解釋。我希望你能停下來(lái)欣賞這意味著你已經(jīng)走了多遠(yuǎn)!
用這個(gè)替換模板EditCards
結(jié)構(gòu):
這幾乎全部EditCards
完成,但在我們可以使用它之前,我們需要添加更多代碼到ContentView
,
以便它顯示按需顯示工作表并在關(guān)閉時(shí)調(diào)用resetCards()
。
我們之前使用過工作表,但我希望你向你展示一項(xiàng)額外的技術(shù):你可以將一個(gè)功能附加到你的工作表,該功能將在工作表關(guān)閉時(shí)自動(dòng)運(yùn)行。這對(duì)你需要從工作表傳回?cái)?shù)據(jù)的時(shí)間沒有幫助,但在這里我們只是要調(diào)用resetCards()
所以它是完美的。
在ContentView的最外層ZStack的末尾添加這個(gè)sheet()修飾符:
這行得通,但既然你在 SwiftUI 中獲得了更多經(jīng)驗(yàn),我想向你展示另一種獲得相同結(jié)果的方法。
當(dāng)我們使用修飾符sheet()
時(shí),
我們需要為 SwiftUI 提供一個(gè)它可以運(yùn)行的函數(shù),該函數(shù)返回要在工作表中顯示的視圖。對(duì)于上面的我們來(lái)說(shuō),這是一個(gè)帶有EditCards()
內(nèi)部的閉包——它創(chuàng)建并返回一個(gè)新視圖,這就是工作表想要的。
當(dāng)我們編寫EditCards()
時(shí)
,我們依賴于語(yǔ)法糖——我們將我們的視圖結(jié)構(gòu)視為一個(gè)函數(shù),因?yàn)?Swift 默默地將其視為對(duì)視圖初始化程序的調(diào)用。所以,實(shí)際上我們實(shí)際上是在寫EditCards.init()
,只是用一種更短的方式。
這一切都很重要,因?yàn)槲覀儗?shí)際上可以將初始化程序EditCards
直接傳遞給工作表,而不是創(chuàng)建一個(gè)調(diào)用初始化程序的閉包EditCards
,如下所示:
這意味著“當(dāng)你想讀取工作表的內(nèi)容時(shí),調(diào)用EditCards
初始化程序,它會(huì)把視圖發(fā)回給你使用。”
重要提示:這種方法之所以有效,是因?yàn)?strong>EditCards
有一個(gè)不接受任何參數(shù)的初始化程序。如果你需要傳入特定值,則需要改用基于閉包的方法。
不管怎樣,除了resetCards()
在工作表關(guān)閉時(shí)調(diào)用,我們還想在視圖首次出現(xiàn)時(shí)調(diào)用它,所以在前一個(gè)修飾符下面添加這個(gè)修飾符:
因此,當(dāng)視圖首次顯示時(shí)resetCards()
被調(diào)用,當(dāng)它EditCards
被關(guān)閉后顯示時(shí)resetCards()
也被調(diào)用。這意味著我們可以放棄我們的示例cards
數(shù)據(jù),而是將其設(shè)為一個(gè)在運(yùn)行時(shí)填充的空數(shù)組。
因此,將的cards
屬性更改ContentView
為:
最后,ContentView
,
我們需要讓cards
按需加載該屬性。這從我們剛剛添加的相同代碼開始EditCard
,所以現(xiàn)在把這個(gè)方法放到ContentView
:
現(xiàn)在我們可以添加對(duì)?resetCards()
中對(duì)
,以便loadData()
的調(diào)用cards
在應(yīng)用啟動(dòng)或用戶編輯卡片時(shí)用所有保存的卡片重新填充該屬性:
現(xiàn)在繼續(xù)運(yùn)行應(yīng)用程序。我們刪除了默認(rèn)示例,因此你需要按 + 圖標(biāo)添加一些你自己的示例。
完成最后的更改后,我們的應(yīng)用程序就完成了——干得好!


