SwiftUI學(xué)習(xí)100天(Day64 - 項目 13,第三部分)

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

信不信由你,在我們進(jìn)入實(shí)施階段之前,我們還有一天的時間來研究這個項目的技術(shù),我把最難的事情留到了最后。
今天,你將了解 SwiftUI 如何與 UIKit 協(xié)同工作的一些復(fù)雜情況。如果你之前曾經(jīng)使用過 UIKit,那么這些事情不會太費(fèi)力,但如果 UIKit 對你來說是新手,那么今天可能會像你第一次在 Swift 中遇到閉包時一樣受到傷害。對真的。
堅持下去!今天之后我們將開始將所有這些概念付諸實(shí)踐,這樣你就離真正有趣的部分很近了。從郵票中汲取靈感——正如作家喬什·比林斯 (Josh Billings) 曾經(jīng)打趣道,“它的用處在于能夠堅持一件事,直到它到達(dá)那里?!?/p>
今天你只有兩個主題要完成,你將在其中了解協(xié)調(diào)器、委托、NSObject
選擇@objc
器和其他在夜間出現(xiàn)的問題。

使用協(xié)調(diào)器管理 SwiftUI 視圖控制器
之前我們研究了如何使用UIViewControllerRepresentable
包裝 UIKit 視圖控制器,以便它可以在 SwiftUI 中使用,特別關(guān)注PHPickerViewController
.?然而,我們遇到了一個問題:雖然我們可以顯示圖像選擇器,但我們無法響應(yīng)用戶選擇圖像或按下取消。
SwiftUI 對此的解決方案稱為協(xié)調(diào)器,這對于來自 UIKit 背景的人來說有點(diǎn)混亂,因為我們有一個設(shè)計模式也稱為協(xié)調(diào)器,它執(zhí)行完全不同的角色。需要明確的是,SwiftUI 的協(xié)調(diào)器與許多開發(fā)人員在 UIKit 中使用的協(xié)調(diào)器模式完全不同,因此如果你以前使用過該模式,請將其從你的大腦中拋棄以避免混淆!
SwiftUI 的協(xié)調(diào)器旨在充當(dāng) UIKit 視圖控制器的委托。請記住,“委托”是響應(yīng)其他地方發(fā)生的事件的對象。例如,UIKit 允許我們將委托對象附加到其文本字段視圖,當(dāng)用戶鍵入任何內(nèi)容、按下回車鍵等時,該委托將收到通知。這意味著 UIKit 開發(fā)人員可以修改他們的文本字段的行為方式,而無需創(chuàng)建他們自己的自定義文本字段類型。
在 SwiftUI 中使用協(xié)調(diào)器需要你了解一些 UIKit 的工作方式,這并不奇怪,因為我們實(shí)際上是在集成 UIKit 的視圖控制器。因此,為了演示這一點(diǎn),我們將升級我們的ImagePicker
視圖,以便它可以在用戶選擇圖像或按下取消時進(jìn)行報告。
提醒一下,這是我們現(xiàn)在擁有的代碼:
我們將一步一步來,因為這里有很多東西要吸收——如果你需要一些時間來理解,不要難過,因為協(xié)調(diào)員在你第一次遇到他們時真的不容易。
首先,在結(jié)構(gòu)中添加這個嵌套類ImagePicker
:
是的,它需要是一個類,你稍后會看到。它不需要是一個嵌套類,盡管這是一個好主意,因為它巧妙地封裝了功能——如果沒有嵌套類,如果你有很多視圖控制器和協(xié)調(diào)器都混在一起,那將會很混亂。
即使該類位于UIViewControllerRepresentable
結(jié)構(gòu)內(nèi)部,SwiftUI 也不會自動將其用于視圖的協(xié)調(diào)器。相反,我們需要添加一個名為 makeCoordinator()
的新方法
,如果我們實(shí)現(xiàn)它,SwiftUI 會自動調(diào)用它。所有這一切需要做的就是創(chuàng)建和配置我們Coordinator
類的實(shí)例,然后將其返回。
現(xiàn)在我們的Coordinator
類沒有做任何特別的事情,所以我們可以通過將這個方法添加到ImagePicker
結(jié)構(gòu)中來發(fā)送一個:
到目前為止我們所做的是創(chuàng)建一個ImagePicker
結(jié)構(gòu)體,他知道如何創(chuàng)建
現(xiàn)在我們只是告訴PHPickerViewController
,ImagePicker
它應(yīng)該有一個協(xié)調(diào)器來處理來自那個PHPickerViewController
的通信
。
下一步是告訴PHPickerViewController
當(dāng)事情發(fā)生時它應(yīng)該告訴我們的協(xié)調(diào)員。這只需要 makeUIViewController()
中的一行代碼
,所以直接在該return picker
行之前添加:
該代碼無法編譯,但在我們修復(fù)它之前,我想花點(diǎn)時間深入了解剛剛發(fā)生的事情。
我們不稱呼makeCoordinator()
自己;SwiftUI 在創(chuàng)建 ImagePicker
的實(shí)例時自動調(diào)用它
。更好的是,SwiftUI 自動將它創(chuàng)建的協(xié)調(diào)器與我們的ImagePicker
結(jié)構(gòu)體相關(guān)聯(lián),這意味著當(dāng)它調(diào)用makeUIViewController()
時
,updateUIViewController()
會自動將該協(xié)調(diào)器對象傳遞給我們。
因此,我們剛剛編寫的代碼行告訴 Swift 使用剛剛創(chuàng)建的協(xié)調(diào)器作為PHPickerViewController
.?這意味著只要照片選擇器控制器內(nèi)部發(fā)生某些事情——即,當(dāng)用戶選擇圖像或按下取消——它就會將該操作報告給我們的協(xié)調(diào)器。
我們的代碼無法編譯的原因是 Swift 正在檢查我們的協(xié)調(diào)器類是否能夠充當(dāng) PHPickerViewController
的委托
,發(fā)現(xiàn)它不能,因此拒絕進(jìn)一步構(gòu)建我們的代碼。為了解決這個問題,我們需要修改我們的Coordinator
類:
對此:
這做了三件事:
它使類繼承自
NSObject
,這是 UIKit 中幾乎所有內(nèi)容的父類。NSObject
允許 Objective-C 在運(yùn)行時詢問對象它支持什么功能,這意味著照片選擇器可以說“嘿,用戶選擇了一張圖像,你想做什么?”它使類符合
PHPickerViewControllerDelegate
協(xié)議,這就是添加檢測用戶何時選擇圖像的功能。(NSObject
讓 Objective-C檢查功能;這個協(xié)議實(shí)際上提供了它。)它會阻止我們的代碼編譯,因為我們已經(jīng)說過該類符合
PHPickerViewControllerDelegate
但我們還沒有實(shí)現(xiàn)該協(xié)議所需的一個方法。
盡管如此,至少現(xiàn)在你可以明白為什么我們需要使用類?Coordinator
:我們需要繼承自NSObject
,
以便 Objective-C 可以查詢我們的協(xié)調(diào)器以查看它支持什么功能。
在這一點(diǎn)上,我們有一個ImagePicker
的結(jié)構(gòu)體,它包裝了一個PHPickerViewController
,并且我們已經(jīng)配置了圖像選擇器控制器,以便在發(fā)生有趣的事情時與我們的Coordinator
類交談。
最后一步是實(shí)現(xiàn)PHPickerViewControllerDelegate
協(xié)議的一個必需方法,當(dāng)用戶完成選擇時將調(diào)用該方法。這可能意味著我們有一張圖片,或者用戶按下了取消鍵,所以我們需要做出適當(dāng)?shù)捻憫?yīng)。
如果我們暫時將 UIKit 放在一邊并考慮純功能,我們想要的是ImagePicker
將該圖像報告給首先使用選擇器的任何人。我們在ContentView
的結(jié)構(gòu)體的工作表中展示ImagePicker
,我們希望無論選擇什么圖像都可以得到它,然后關(guān)閉工作表。
我們在這里需要的是 SwiftUI 的@Binding
屬性包裝器,它允許我們創(chuàng)建一個綁定ImagePicker
到任何創(chuàng)建它的對象。這意味著我們可以在我們的圖像選擇器中設(shè)置綁定值,并讓它實(shí)際更新存儲在其他地方的值——在ContentView
中,
例如。
因此,將此屬性添加到ImagePicker
:
現(xiàn)在,我們剛剛將該屬性添加到ImagePicker
,但我們需要在我們的Coordinator
類中訪問它,
因為這是在選擇圖像時將被通知的那個。
與其將數(shù)據(jù)向下傳遞一個級別,不如告訴協(xié)調(diào)器它的父級是什么,這樣它就可以直接修改那里的值。這意味著向類添加一個ImagePicker
屬性和關(guān)聯(lián)的初始化程序Coordinator
,如下所示:
現(xiàn)在我們可以修改makeCoordinator()
,以便它將ImagePicker
結(jié)構(gòu)傳遞給協(xié)調(diào)器,如下所示:
此時你的整個ImagePicker
結(jié)構(gòu)應(yīng)該是這樣的:
終于,我們準(zhǔn)備好實(shí)際讀取來自我們PHPickerViewController
的響應(yīng)
,這是通過實(shí)現(xiàn)一個具有非常特定名稱的方法來完成的。Swift 將在我們的Coordinator
類中查找此方法
,因為它是圖像選擇器的委托,因此請確保將其添加到此處。
方法名稱很長,需要完全正確才能讓 UIKit 找到它,但 Xcode 可以幫助我們完成自動完成。因此,單擊錯誤行上的紅色六邊形,然后單擊“修復(fù)”以添加此方法存根:
該方法接收我們關(guān)心的兩個對象:用戶與之交互的選擇器視圖控制器,以及用戶選擇的數(shù)組,因為可以讓用戶一次選擇多個圖像。
我們的工作是做三件事:
告訴選擇器解雇自己。
如果用戶沒有做出選擇則退出——如果他們點(diǎn)擊了取消。
否則,查看用戶的結(jié)果是否包含
UIImage
我們可以實(shí)際加載的內(nèi)容,如果是,則將其放入parent.image
屬性中。
因此,將“代碼”占位符替換為:
注意我們?nèi)绾涡枰愋娃D(zhuǎn)換UIImage
——那是因為我們提供的數(shù)據(jù)在理論上可以是任何東西。是的,我知道我們特別要求提供照片,但PHPickerViewControllerDelegate
對任何類型的媒體都調(diào)用相同的方法,這就是我們需要小心的原因。
在這一點(diǎn)上,我敢打賭你真的錯過了 SwiftUI 的美麗簡單,所以你會很高興知道我們終于完成了ImagePicker
結(jié)構(gòu)——它做了我們現(xiàn)在需要的一切。
所以,最后我們可以返回到 ContentView.swift。這是我們之前離開它的方式:
要將我們的ImagePicker
視圖集成到其中,我們需要首先添加另一個@State
可以傳遞到選擇器中的圖像屬性:
我們現(xiàn)在可以更改我們的sheet()
修改器以將該屬性傳遞給我們的圖像選擇器,因此它會在選擇圖像時更新:
接下來,我們需要一個可以在該屬性更改時調(diào)用的方法。請記住,我們不能在這里使用普通的屬性觀察器,因為 Swift 會忽略對綁定的更改,因此我們將編寫一個方法來檢查inputImage
是否
有值,如果有,則使用它為image
屬性分配一個新
.Image
視圖
將此方法添加到ContentView
現(xiàn)在:
現(xiàn)在我們可以使用onChange()
修飾符在選擇新圖像時調(diào)用
loadImage()
- 將其放在
sheet()
修飾符下方:
我們完成了!繼續(xù)運(yùn)行該應(yīng)用程序并嘗試一下——你應(yīng)該能夠點(diǎn)擊按鈕,瀏覽你的照片庫,然后選擇一張照片。當(dāng)發(fā)生這種情況時,照片選擇器應(yīng)該會消失,你選擇的圖像將顯示在下方。
我意識到此時你可能厭倦了 UIKit 和協(xié)調(diào)器,但在我們繼續(xù)之前我想總結(jié)一下完整的過程:
我們創(chuàng)建了一個符合
UIViewControllerRepresentable
的SwiftUI視圖
.我們給了它一個
makeUIViewController()
的方法
創(chuàng)建某種UIViewController
,
在我們的示例中是一個PHPickerViewController
.我們添加了一個嵌套
Coordinator
類作為 UIKit 視圖控制器和我們的 SwiftUI 視圖之間的橋梁。我們?yōu)樵搮f(xié)調(diào)器提供了一個
didFinishPicking
方法,該方法將在選擇圖像時由 iOS 觸發(fā)。最后,我們給了我們
ImagePicker
一個@Binding
屬性,以便它可以將更改發(fā)送回父視圖。
對于它的價值,在你使用過協(xié)調(diào)器一次之后,第二次和隨后的時間會更容易,但是如果你現(xiàn)在發(fā)現(xiàn)整個系統(tǒng)非常莫名其妙,我不會責(zé)怪你。
別太擔(dān)心——我們很快就會再次回到這個話題,所以你會有足夠的時間練習(xí)。這也意味著你不應(yīng)該刪除你的 ImagePicker.swift 文件,因為這是你將在這個項目和你創(chuàng)建的其他項目中使用的另一個有用的組件。



如何將圖像保存到用戶的照片庫
在我們完成這個項目的技術(shù)之前,我們需要解決最后一個 UIKit 的樂趣:一旦我們處理了用戶的圖像,我們就會返回UIImage
,但是我們需要一種方法來將處理過的圖像保存到用戶的照片庫。這使用了一個名為 UIImageWriteToSavedPhotosAlbum
()的 UIKit 函數(shù)
,其最簡單的形式使用起來很簡單,但為了使其有用,你需要重新研究 UIKit。至少它會讓你真正體會到 SwiftUI 有多好!
在我們編寫任何代碼之前,我們需要做一些新的事情:我們需要為我們的項目添加一個配置選項。我們構(gòu)建的每個項目都有一大堆這樣的東西,描述我們支持的界面方向、我們應(yīng)用程序的版本號和其他固定數(shù)據(jù)。這不是代碼:這些選項必須全部提前聲明,在一個單獨(dú)的文件中,這樣系統(tǒng)就可以讀取它們而無需運(yùn)行我們的應(yīng)用程序。
這些選項都位于 Xcode 中的特定位置,除非你知道自己在做什么,否則很難找到:
在 Project Navigator 中,選擇樹中的頂部項目。它將包含你的項目名稱 Instafilter。
你會看到 Instafilter 列在 PROJECT 和 TARGETS 下。請在目標(biāo)下選擇它。
現(xiàn)在你會在頂部看到一堆選項卡,包括“常規(guī)”、“簽名和功能”等 - 從那里選擇“信息”。
你可以在此處為你的項目添加一系列配置選項,但現(xiàn)在我們需要一個特定選項。你看,寫入照片庫是一項受保護(hù)的操作,這意味著我們不能在沒有用戶明確許可的情況下執(zhí)行此操作。iOS 將負(fù)責(zé)請求許可并檢查響應(yīng),但我們需要提供一個簡短的字符串來向用戶解釋我們?yōu)槭裁匆紫葘懭雸D像。
要添加你的權(quán)限字符串,請右鍵單擊任何現(xiàn)有選項,然后選擇添加行。你會看到一個可供選擇的選項下拉列表——我希望你向下滾動并選擇“隱私 - 照片庫添加使用說明”。對于右側(cè)的值,請輸入文本“我們要保存過濾后的照片”。
完成后,我們現(xiàn)在可以使用該UIImageWriteToSavedPhotosAlbum()
方法寫出圖片。我們之前的工作中已經(jīng)有了這個loadImage()
方法:
我們可以修改它,讓它立即保存加載的圖像,有效地創(chuàng)建一個副本。將此行添加到方法的末尾:
就是這樣 - 每次你導(dǎo)入圖像時,我們的應(yīng)用程序都會將其保存回照片庫。第一次嘗試時,iOS 會自動提示用戶寫照片的權(quán)限,并顯示我們添加到配置選項中的字符串。
現(xiàn)在,你可能會看著它并認(rèn)為“這很簡單!”?你是對的。但之所以容易,是因為我們做了盡可能少的工作:我們將要保存的圖像作為第一個參數(shù)提供給UIImageWriteToSavedPhotosAlbum()
?,然后nil
作為其他三個參數(shù)提供。
這些nil
參數(shù)很重要,或者至少前兩個很重要:它們告訴 Swift 在保存完成時應(yīng)該調(diào)用什么方法,這反過來會告訴我們保存操作是成功還是失敗。如果你不關(guān)心那個,那你就完了——三個值都是nil
是可以的。但請記?。河脩艨梢跃芙^訪問他們的照片庫,因此如果你沒有發(fā)現(xiàn)保存錯誤,他們會想知道為什么你的應(yīng)用無法正常運(yùn)行。
UIKit 需要兩個參數(shù)來知道要調(diào)用哪個函數(shù)的原因是因為這段代碼很舊——比 Swift 老得多,事實(shí)上,它甚至比 Objective-C 的閉包等價物還要早。因此,它使用了一種完全不同的函數(shù)引用方式:在引用第一個nil
時,
我們應(yīng)該指向一個對象,在引用第二個nil
時,
我們應(yīng)該指向要調(diào)用的方法的名稱。
如果這聽起來很糟糕,恐怕你只知道一半的故事。你看,這兩個參數(shù)都有其自身的復(fù)雜性:
我們提供的對象必須是類,而且必須繼承自
NSObject
。這意味著我們無法指向 SwiftUI 視圖結(jié)構(gòu)。該方法作為方法名稱提供,而不是實(shí)際方法。Objective-C 使用此方法名稱在運(yùn)行時查找實(shí)際代碼,然后可以運(yùn)行這些代碼。該方法需要有一個特定的簽名(參數(shù)列表),否則我們的代碼將無法工作。
但是等等:還有更多!出于性能原因,Swift 不喜歡以 Objective-C 可以讀取的方式生成代碼——整個“在運(yùn)行時查找方法”的東西非常簡潔,但也非常慢。因此,在編寫方法名稱時,我們需要做兩件事:
使用一個名為
#selector
的特殊編譯器指令標(biāo)記該方法
,它要求 Swift 確保方法名稱存在于我們所說的位置。添加一個調(diào)用
@objc
到方法的屬性,它告訴 Swift 生成可以被 Objective-C 讀取的代碼。
你知道的,在我轉(zhuǎn)向 SwiftUI 之前我寫了十多年的 UIKit 代碼,并且已經(jīng)寫下所有這些解釋使得這個舊的 API 看起來像是反人類罪??杀氖牵褪沁@樣,我們堅持下去。
在向你展示代碼之前,我想提一下第四個參數(shù)。所以,第一個是要保存的圖像,第二個是應(yīng)該通知保存結(jié)果的對象,第三個是應(yīng)該運(yùn)行的對象上的方法,然后是第四個。我們不會在這里使用它,但你需要知道它的作用:我們可以在這里提供任何類型的數(shù)據(jù),當(dāng)我們的完成方法被調(diào)用時,它會傳回給我們。
這就是 UIKit 所說的“上下文”,它可以幫助你將一個圖像保存操作與另一個圖像保存操作區(qū)分開來。你可以在這里提供任何你想要的東西,所以 UIKit 使用你能想象到的最不干涉的類型:一塊原始的內(nèi)存塊,Swift 對此不做任何保證。這在 Swift 中有其自己的特殊類型名稱:UnsafeRawPointer
。老實(shí)說,如果不是因為我們不得不在這里使用它,我甚至不會告訴你它的存在,因為在你的應(yīng)用程序開發(fā)生涯的這個階段,它并不是真正有用的。
無論如何,這已經(jīng)綽綽有余了。在你決定放棄這個項目并直接進(jìn)入下一個項目之前,讓我們把它結(jié)束并完成。
正如我所說,要將圖像寫入照片庫并讀取響應(yīng),我們需要某種繼承自NSObject
.?在里面,我們需要一個帶有精確簽名的方法,并用 @objc
標(biāo)記
,然后我們可以從 UIImageWriteToSavedPhotosAlbum()
調(diào)用它
。
將所有這些放在一起,請創(chuàng)建一個名為 ImageSaver.swift 的新 Swift 文件,將其基礎(chǔ)導(dǎo)入更改為 SwiftUI,然后為其提供以下代碼:
有了它,我們現(xiàn)在可以從 ContentView
中使用它
,如下所示:
如果你現(xiàn)在運(yùn)行代碼,你應(yīng)該看到“保存完成!”?選擇圖像后會立即輸出消息,但你還會看到系統(tǒng)提示你授予寫入照片庫的權(quán)限。
是的,考慮到它需要多少解釋,這是非常少的代碼,但從好的方面來說,完成了這個項目的概述,所以在很長一段時間(很長,很長?。┳詈笪覀兛梢赃M(jìn)入實(shí)際的實(shí)現(xiàn)。
請繼續(xù)并將你的項目恢復(fù)到默認(rèn)狀態(tài),這樣我們就有了一個干凈的工作狀態(tài),但我希望你保留ImagePicker
和ImageSaver
- 這兩個項目稍后都會用到,它們在其他項目中也很有用你將來可能創(chuàng)建的項目。


