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

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

今天,我們繼續(xù)檢查我們項目的技術(shù),并且我們開始更多地涉足 SwiftUI 感覺不太好用的地方。今天你將看到 Core Image 如何與 SwiftUI 集成,答案是“不是很好”。我們還將開始研究 UIKit 如何與 SwiftUI 集成,同樣,答案也不是很好——我們需要投入大量工作才能將 UIKit 的圓釘擠入 SwiftUI 形孔中。
我想在這里看到更好的東西嗎?絕對 - 也許它會在 SwiftUI 的未來更新中出現(xiàn)。但有一句匿名的話我覺得用在這里很合適:“永遠(yuǎn)不要讓你想要的東西,讓你忘記你擁有的東西?!?/p>
是的,SwiftUI 與其他框架的集成現(xiàn)在有點不穩(wěn)定,但這并不意味著它應(yīng)該有損于 SwiftUI 為我們所做的其他偉大工作。
今天你只有兩個主題要完成,其中你將學(xué)習(xí)如何使用 Core Image 操作圖像,以及如何為 SwiftUI 包裝 UIKit 代碼。

將 Core Image 與 SwiftUI 集成
就像 Core Data 是 Apple 用于處理數(shù)據(jù)的內(nèi)置框架一樣,Core Image 是他們用于處理圖像的框架。這不是繪圖,或者至少在很大程度上它不是繪圖,而是關(guān)于更改現(xiàn)有圖像:應(yīng)用銳化、模糊、暈影、像素化等。如果你曾經(jīng)使用過 Apple 的 Photo Booth 應(yīng)用程序中可用的所有各種照片效果,那應(yīng)該會讓你很好地了解 Core Image 的用途!
然而,Core Image 并沒有很好地集成到 SwiftUI 中。事實上,我甚至不會說它很好地集成到 UIKit 中——Apple 做了一些工作來提供助手,但它仍然需要相當(dāng)多的思考。不過請堅持我的看法:一旦你了解了它的工作原理,結(jié)果就會非常出色,并且你會發(fā)現(xiàn)它在未來為你的應(yīng)用程序打開了一個完整的功能范圍。
首先,我們將輸入一些代碼來為我們提供一個基本圖像。我將以一種稍微奇怪的方式來構(gòu)建它,但是一旦我們混合在 Core Image 中它就會有意義:我們將創(chuàng)建Image
視圖作為一個可選@State
屬性,強制它與屏幕寬度相同,然后添加onAppear()
修飾符以實際加載圖像。
將示例圖像添加到你的資產(chǎn)目錄,然后將你的ContentView
結(jié)構(gòu)修改為:
首先,請注意 SwiftUI 處理可選視圖的流暢程度——它就是這么好用!但是,請注意我是如何將onAppear()
修改器附加到VStack
?的圖像周圍的,因為如果可選圖像是nil
那么它不會觸發(fā)該onAppear()
功能。
無論如何,當(dāng)該代碼運行時,它應(yīng)該會顯示你添加的示例圖像,并按比例整齊地適合屏幕。
現(xiàn)在是復(fù)雜的部分:實際上什么是Image
?如你所知,它是一個view,這意味著我們可以在 SwiftUI 視圖層次結(jié)構(gòu)中定位和調(diào)整它的大小。它還處理從我們的資產(chǎn)目錄和 SF Symbols 加載圖像,并且它也能夠從少數(shù)其他來源加載圖像。然而,最終它是要顯示的東西——我們不能將它的內(nèi)容寫入磁盤或以其他方式轉(zhuǎn)換它們,只能應(yīng)用一些簡單的 SwiftUI 過濾器。
如果我們想使用 Core Image,SwiftUI 的Image
視圖是一個很好的終點,但在其他地方使用它沒有用。也就是說,如果我們想要動態(tài)創(chuàng)建圖像、應(yīng)用 Core Image 濾鏡、將它們保存到用戶的照片庫等,那么 SwiftUI 的圖像就不能勝任這項工作。
Apple 為我們提供了其他三種圖像類型供我們使用,如果我們想使用 Core Image,我們需要巧妙地使用所有這三種圖像類型。它們聽起來可能很相似,但它們之間有一些微妙的區(qū)別,如果你想從 Core Image 中獲得任何有意義的東西,正確使用它們很重要。
除了 SwiftUI 的Image
視圖之外,其他三種圖像類型是:
UIImage
,它來自 UIKit。這是一種極其強大的圖像類型,能夠處理各種圖像類型,包括位圖(如 PNG)、矢量(如 SVG),甚至是構(gòu)成動畫的序列。UIImage
是 UIKit 的標(biāo)準(zhǔn)圖像類型,在這三個圖像類型中它最接近 SwiftUI 的Image
類型。CGImage
,來自 Core Graphics。這是一種更簡單的圖像類型,實際上只是一個二維像素陣列。CIImage
,來自 Core Image。這存儲了生成圖像所需的所有信息,但除非被要求,否則實際上不會將其轉(zhuǎn)換為像素。Apple 稱其CIImage
為“圖像配方”而不是實際圖像。
各種圖像類型之間存在一些互操作性:
我們可以從
CGImage
創(chuàng)建一個
UIImage
,并從UIImage
創(chuàng)建一個
。CGImage
我們可以從一個
UIImage
和一個CGImage
創(chuàng)建一個CIImage
,也可以從CIImage
創(chuàng)建一個
CGImage
。我們可以從一個
UIImage
和一個CGImage
創(chuàng)建一個 SwiftUI?Image
。
我知道,我知道:這很令人困惑,但希望你看到代碼后會感覺好些。重要的是這些圖像類型是純數(shù)據(jù)——我們不能將它們放入 SwiftUI 視圖層次結(jié)構(gòu)中,但我們可以自由地操作它們,然后在 SwiftUI Image
中呈現(xiàn)結(jié)果。
我們將進(jìn)行更改loadImage()
,以便UIImage
從我們的示例圖像創(chuàng)建一個,然后使用 Core Image 對其進(jìn)行操作。更具體地說,我們將從兩個任務(wù)開始:
我們需要將我們的示例圖像加載到一個
UIImage
中,它有一個初始化程序叫做UIImage(named:)
來從我們的資產(chǎn)目錄加載圖像。它返回一個可選的UIImage
,因為我們可能指定了一個不存在的圖像。我們會將其轉(zhuǎn)換為
CIImage
,這是 Core Image 想要使用的。
所以,首先用這個替換你當(dāng)前的loadImage()
實現(xiàn):
下一步將是創(chuàng)建一個 Core Image 上下文和一個 Core Image 過濾器。過濾器是完成以某種方式轉(zhuǎn)換圖像數(shù)據(jù)的實際工作的東西,例如模糊它、銳化它、調(diào)整顏色等等,上下文負(fù)責(zé)將處理過的數(shù)據(jù)轉(zhuǎn)換成CGImage
我們可以使用的數(shù)據(jù)。
這兩種數(shù)據(jù)類型都來自 Core Image,因此你需要添加兩個導(dǎo)入以使它們可供我們使用。所以請首先在 ContentView.swift 的頂部附近添加這些:
接下來我們將創(chuàng)建上下文和過濾器。對于這個例子,我們將使用棕褐色調(diào)濾鏡,它應(yīng)用棕色調(diào),使照片看起來像是很久以前拍的。
所以,// more code to come
用這個替換評論:
我們現(xiàn)在可以自定義我們的過濾器來改變它的工作方式。Sepia 是一個簡單的濾鏡,因此它只有兩個有趣的屬性:inputImage
是我們要更改的圖像,以及intensity
應(yīng)用棕褐色效果的強度,在 0(原始圖像)和 1(全棕褐色)范圍內(nèi)指定。
所以,在前兩行下面添加這兩行代碼:
這些都不是很難,但這里有一些變化:我們需要將過濾器的輸出轉(zhuǎn)換為Image
可以在視圖中顯示的 SwiftUI。這是我們需要同時依賴所有四種圖像類型的地方,因為最簡單的事情是:
從我們的過濾器中讀取輸出圖像,這將是一個
CIImage
.?這可能會失敗,所以它返回一個可選的。CGImage
要求我們的上下文從該輸出圖像創(chuàng)建一個。這也可能會失敗,所以它再次返回一個可選的。將其
CGImage
轉(zhuǎn)換為UIImage
.將其
UIImage
轉(zhuǎn)換為 SwiftUI?Image
。
你可以直接從一個CGImage
轉(zhuǎn)到 SwiftUI?Image
,但它需要額外的參數(shù),而且只會增加更多的復(fù)雜性!
這是 loadImage()
的最終代碼
:
如果你再次運行該應(yīng)用程序,你應(yīng)該會看到你的示例圖像現(xiàn)在應(yīng)用了棕褐色效果,這都要歸功于 Core Image。
現(xiàn)在,你可能會認(rèn)為僅僅為了獲得一個相當(dāng)簡單的結(jié)果就需要做大量的工作,但是既然你已經(jīng)掌握了 Core Image 的所有基礎(chǔ)知識,那么切換到不同的過濾器就相對容易了。
話雖這么說,Core Image 有點……好吧……讓我們說“創(chuàng)意”吧。它早在 iOS 5.0 中就被引入,到那時 Swift 已經(jīng)在 Apple 內(nèi)部開發(fā),但你真的不知道它——在最長的時間里,它的 API 是你能想象到的最不 Swifty 的東西,盡管 Apple 已經(jīng)慢慢有時你別無選擇,只能挖掘它的弱點。
首先,讓我們看看現(xiàn)代 API——我們可以用像這樣的像素濾鏡替換我們的棕褐色調(diào):
當(dāng)它運行時,你會看到我們的圖像看起來像素化了。比例為 100 應(yīng)該意味著像素跨度為 100 點,但因為我的圖像太大,像素相對較小。
現(xiàn)在讓我們試試這樣的水晶效果:
或者我們可以像這樣添加一個旋轉(zhuǎn)失真過濾器:
因此,我們可以僅使用現(xiàn)代 API 做很多事情。但是對于這個項目,我們將使用舊的 API 來設(shè)置值,例如radius
和scale
因為它允許我們動態(tài)設(shè)置值——我們可以從字面上詢問當(dāng)前過濾器它支持什么值,然后將它們發(fā)送進(jìn)去。
這是它的樣子:
有了它,你現(xiàn)在可以將旋轉(zhuǎn)失真更改為任何其他過濾器,并且代碼將繼續(xù)工作 - 每個調(diào)整值僅在支持時才發(fā)送。
請注意它是如何依賴于鍵的設(shè)置值的,這可能會讓你想起UserDefaults
的工作方式
。事實上,所有這些kCIInput
鍵都在幕后實現(xiàn)為字符串,因此它比你可能意識到的還要相似!
如果你要實施精確的 Core Image 調(diào)整,你絕對應(yīng)該使用使用精確屬性名稱和類型的較新 API,但在這個項目中,較舊的 API 很有用,因為它允許我們發(fā)送調(diào)整,而不管實際使用的是什么過濾器。



在 SwiftUI 視圖中包裝 UIViewController
SwiftUI 是一個非常棒的構(gòu)建應(yīng)用程序的框架,但現(xiàn)在它還遠(yuǎn)未完成——有很多事情它做不到,所以如果你想添加更多高級功能,你需要學(xué)會與 UIKit 對話。有時這是為了集成你為 UIKit 編寫的現(xiàn)有代碼(例如,如果你在一家擁有現(xiàn)有 UIKit 應(yīng)用程序的公司工作),但有時這是因為 UIKit 和 Apple 的其他框架為我們提供了我們想要展示的有用代碼在 SwiftUI 布局中。
在這個項目中,我們將要求用戶從他們的照片庫中導(dǎo)入圖片。Apple 的 API 帶有用于執(zhí)行此操作的專用代碼,但尚未移植到 SwiftUI,因此我們需要自己編寫該橋接器。相反,它內(nèi)置于一個名為 PhotosUI 的單獨框架中,該框架旨在與 UIKit 一起使用,因此需要我們了解 UIKit 的工作方式。
在我們編寫代碼之前,你需要了解三件事,它們都有點像 UIKit 101,但如果你只使用過 SwiftUI,它們對你來說將是新的:
UIKit 有一個名為
UIView
的類
,它是布局中所有視圖的父類。因此,標(biāo)簽、按鈕、文本字段、滑塊等等——這些都是視圖。UIKit 有一個名為
UIViewController
的類
,它旨在保存所有代碼,使視圖栩栩如生。就像UIView
,UIViewController
有很多做不同種類工作的子類。UIKit 使用一種稱為委托的設(shè)計模式來決定工作發(fā)生的位置。因此,在決定如何響應(yīng)文本字段更改時,我們將創(chuàng)建一個具有我們功能的自定義類,并將其作為我們文本字段的委托。
所有這些都很重要,因為要求用戶從他們的庫中選擇照片使用了一個名為 PHPickerViewController
的視圖控制器
和委托協(xié)議PHPickerViewControllerDelegate
。SwiftUI 不能直接使用這兩個,所以我們需要對它們進(jìn)行包裝。
我們將從簡單的方式開始。包裝 UIKit 視圖控制器需要我們創(chuàng)建一個符合UIViewControllerRepresentable
協(xié)議的結(jié)構(gòu)。
因此,按 Cmd+N 創(chuàng)建一個新文件,選擇 Swift File,并將其命名為 ImagePicker.swift。添加import PhotosUI
和import SwiftUI
到新文件的頂部,然后給它這個代碼:
該協(xié)議建立在View
之上
,這意味著我們定義的結(jié)構(gòu)可以在 SwiftUI 視圖層次結(jié)構(gòu)中使用,但是我們不提供body
屬性,因為視圖的主體是視圖控制器本身——它只顯示 UIKit 發(fā)回的內(nèi)容。
符合UIViewControllerRepresentable
?要求我們用兩種方法填充該結(jié)構(gòu):一種稱為makeUIViewController()
,負(fù)責(zé)創(chuàng)建初始視圖控制器,另一種稱為updateUIViewController()
,旨在讓我們在某些 SwiftUI 狀態(tài)更改時更新視圖控制器。
這些方法具有非常精確的簽名,因此我將向你展示一個簡潔的快捷方式。這些方法很長的原因是因為 SwiftUI 需要知道我們的結(jié)構(gòu)正在包裝什么類型的視圖控制器,所以如果我們直接告訴 Swift 該類型 Xcode 將幫助我們完成剩下的工作。
現(xiàn)在將此代碼添加到結(jié)構(gòu)中:
這些代碼不足以正確編譯,但是當(dāng) Xcode 顯示錯誤“Type ImagePicker does not conform to protocol UIViewControllerRepresentable”時,請單擊錯誤左側(cè)的紅色和白色圓圈并選擇“修復(fù)”。這將使 Xcode 編寫我們實際需要的兩個方法,事實上,這些方法實際上足以讓 Swift 確定視圖控制器類型,因此你可以刪除該typealias
行。
你應(yīng)該有這樣的結(jié)構(gòu):
我們不會使用updateUIViewController()
,所以你可以從那里刪除“代碼”行,這樣該方法就是空的。
但是,該makeUIViewController()
方法很重要,因此請將其現(xiàn)有的“代碼”行替換為:
這會創(chuàng)建一個新的照片選擇器配置,要求它僅向我們提供圖像,然后使用它來創(chuàng)建并返回一個PHPickerViewController
執(zhí)行選擇圖像的實際工作的。
我們很快會添加更多代碼,但這實際上是我們包裝基本視圖控制器所需的全部。
我們的ImagePicker
結(jié)構(gòu)是一個有效的 SwiftUI 視圖,這意味著我們現(xiàn)在可以像任何其他 SwiftUI 視圖一樣在工作表中顯示它。這個特殊的結(jié)構(gòu)是為了顯示圖像而設(shè)計的,所以我們需要一個可選的Image
視圖來保存選定的圖像,以及一些確定工作表是否顯示的狀態(tài)。
用這個替換你當(dāng)前的ContentView
結(jié)構(gòu):
繼續(xù)并在模擬器或你的真實設(shè)備上運行它。當(dāng)你點擊按鈕時,默認(rèn)的 iOS 圖像選擇器應(yīng)該向上滑動,讓你瀏覽所有照片。
但是,選擇圖像時不會發(fā)生任何事情,取消按鈕也不會執(zhí)行任何操作。你看,即使我們已經(jīng)創(chuàng)建并展示了一個有效的?PHPickerViewController
,我們實際上并沒有告訴它如何響應(yīng)用戶交互。
要做到這一點需要一個全新的概念:協(xié)調(diào)員。


