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

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

今天開(kāi)始另一個(gè)新技術(shù)項(xiàng)目,我們將專(zhuān)注于繪圖。這是 SwiftUI 的一個(gè)領(lǐng)域,你可能認(rèn)為你不需要太多,但事實(shí)并非如此:SwiftUI 使高性能繪圖變得如此簡(jiǎn)單,每個(gè)人都可以訪(fǎng)問(wèn),你會(huì)找到可以發(fā)揮你的技能的地方在你構(gòu)建的幾乎每個(gè)應(yīng)用程序中。
繪畫(huà)的另一個(gè)好處——這將在本項(xiàng)目的第二部分和第三部分中變得更加明顯——有助于營(yíng)造一種嬉戲感。在接下來(lái)的幾天里,你會(huì)發(fā)現(xiàn)只需幾行代碼就可以創(chuàng)建漂亮的設(shè)計(jì),而我在準(zhǔn)備我的示例時(shí)浪費(fèi)了無(wú)數(shù)時(shí)間,只是玩玩和玩得開(kāi)心。
不要相信我的話(huà)——著名的荷蘭印象派畫(huà)家文森特梵高說(shuō),“我有時(shí)認(rèn)為沒(méi)有什么比繪畫(huà)更令人愉快的了?!?/p>
我想他是在做某事!
不管怎樣,我希望你能以開(kāi)放的心態(tài)對(duì)待接下來(lái)的幾天。也許你想跟隨編碼(我希望你這樣做!),或者你可能只是想坐下來(lái)看看有什么可能——無(wú)論哪種方式,我想你都會(huì)對(duì) SwiftUI 的繪圖的智能程度印象深刻!
今天你有四個(gè)主題要完成,你將在其中學(xué)習(xí)路徑、形狀、可插入形狀等。

繪圖:簡(jiǎn)介
在這個(gè)技術(shù)項(xiàng)目中,我們將仔細(xì)研究 SwiftUI 中的繪圖,包括創(chuàng)建自定義路徑和形狀、為你的更改設(shè)置動(dòng)畫(huà)、解決性能問(wèn)題等等——這是一個(gè)非常大的話(huà)題,值得密切關(guān)注。
在后臺(tái),SwiftUI 使用我們?cè)谄渌?Apple 框架上使用的相同繪圖系統(tǒng):Core Animation 和 Metal。大多數(shù)時(shí)候 Core Animation 負(fù)責(zé)我們的繪圖,無(wú)論是自定義路徑和形狀還是 UI 元素TextField
,但當(dāng)事情真的變得復(fù)雜時(shí),我們可以向下移動(dòng)到 Metal——Apple 的低級(jí)框架,它針對(duì)復(fù)雜的繪圖進(jìn)行了優(yōu)化。SwiftUI 的一個(gè)巧妙特性是這兩者幾乎可以互換:我們可以通過(guò)一個(gè)小的改變從 Core Animation 轉(zhuǎn)移到 Metal。
不管怎樣,我們有很多內(nèi)容要講,所以請(qǐng)創(chuàng)建一個(gè)名為 Drawing 的新 App 項(xiàng)目,讓我們深入了解……



使用 SwiftUI 創(chuàng)建自定義路徑
SwiftUI 為我們提供了一個(gè)專(zhuān)門(mén)用于繪制自定義形狀的類(lèi)型Path
。它的級(jí)別非常低,我的意思是你通常希望將它包裝在其他東西中以使其更有用,但由于它是其他工作的基礎(chǔ)構(gòu)建塊,我們將從那里開(kāi)始。
就像顏色、漸變和形狀一樣,路徑本身就是視圖。這意味著我們可以像使用文本視圖和圖像一樣使用它們,盡管你會(huì)看到它有點(diǎn)笨拙。
讓我們從一個(gè)簡(jiǎn)單的形狀開(kāi)始:畫(huà)一個(gè)三角形。有幾種創(chuàng)建路徑的方法,包括一種接受繪圖指令閉包的方法。這個(gè)閉包必須接受一個(gè)參數(shù),這是繪制的路徑。我意識(shí)到一開(kāi)始這可能有點(diǎn)費(fèi)腦筋,因?yàn)槲覀冋趧?chuàng)建一個(gè)路徑,并且在我們正在傳遞的路徑的初始化器內(nèi)部繪制路徑,但可以這樣想:SwiftUI給我們創(chuàng)建了一個(gè)空的路徑,然后讓我們有機(jī)會(huì)盡可能多地添加它。
Paths 有很多方法可以創(chuàng)建正方形、圓形、弧形和直線(xiàn)形狀。對(duì)于我們的三角形,我們需要移動(dòng)到一個(gè)起始位置,然后添加如下三行:
我們以前沒(méi)有用過(guò)CGPoint
,但我確實(shí)在項(xiàng)目 6 中偷偷對(duì)CGSize
做了一個(gè)快速參考
?!癈G”是 Core Graphics 的縮寫(xiě),它提供了一系列基本類(lèi)型,讓我們可以參考 X/Y 坐標(biāo) (?CGPoint
)、寬度和高度 (?CGSize
) 和矩形框 (?CGRect
)。
當(dāng)我們的三角形代碼運(yùn)行時(shí),你會(huì)看到一個(gè)大的黑色三角形。你看到它相對(duì)于屏幕的位置取決于你使用的是什么模擬器,這是這些原始路徑問(wèn)題的一部分:我們需要使用精確的坐標(biāo),所以如果你想單獨(dú)使用路徑,你要么需要接受GeometryReader
在所有設(shè)備上調(diào)整大小或使用類(lèi)似的東西
來(lái)相對(duì)于它們的容器縮放它們。
我們很快就會(huì)看到一個(gè)更好的選擇,但首先讓我們看看為路徑著色。一種選擇是使用fill()
修飾符,如下所示:
我們還可以使用stroke()
修飾符在路徑周?chē)L制而不是填充它:
不過(guò),這看起來(lái)不太正確——我們?nèi)切蔚牡捉呛芷炼液茕h利,但頂角壞了。發(fā)生這種情況是因?yàn)?SwiftUI 確保線(xiàn)條與前后的內(nèi)容整齊地連接起來(lái),而不僅僅是一系列單獨(dú)的線(xiàn)條,但我們的最后一行后面沒(méi)有任何內(nèi)容,因此無(wú)法建立連接。
解決此問(wèn)題的一種方法是要求 SwiftUI 關(guān)閉子路徑,這是我們?cè)诼窂街欣L制的形狀:
另一種方法是使用 SwiftUI 的專(zhuān)用StrokeStyle
結(jié)構(gòu),它使我們能夠控制每條線(xiàn)應(yīng)如何連接到它之后的線(xiàn)(線(xiàn)連接),以及當(dāng)它結(jié)束后沒(méi)有連接時(shí)應(yīng)如何繪制每條線(xiàn)(線(xiàn)帽)。這特別有用,因?yàn)?join 和 cap 的選項(xiàng)之一是.round
,它創(chuàng)建了柔和的圓形:
有了它,你就可以刪除對(duì) path.closeSubpath()
的調(diào)用
,因?yàn)樗辉傩枰恕?/p>
使用圓角解決了我們邊緣粗糙的問(wèn)題,但是并沒(méi)有解決固定坐標(biāo)的問(wèn)題。為此,我們需要從路徑繼續(xù)前進(jìn),看看更復(fù)雜的東西:形狀。



SwiftUI 中的路徑與形狀
SwiftUI 支持使用兩種略有不同的類(lèi)型進(jìn)行自定義繪圖:路徑和形狀。路徑是一系列的繪圖指令,例如“從這里開(kāi)始,到這里畫(huà)一條線(xiàn),然后在那里畫(huà)一個(gè)圓”,都是使用絕對(duì)坐標(biāo)。相比之下,形狀不知道將在哪里使用或?qū)⑹褂枚啻?,而是要求將其自身繪制在給定的矩形內(nèi)。
有用的是,形狀是使用路徑構(gòu)建的,所以一旦你理解了路徑,形狀就很容易了。此外,就像路徑、顏色和漸變一樣,形狀是視圖,這意味著我們可以將它們與文本視圖、圖像等一起使用。
SwiftUI 實(shí)現(xiàn)為具有單一必需方法的Shape
協(xié)議:給定以下矩形,你想繪制什么路徑?這仍然會(huì)像直接使用原始路徑一樣創(chuàng)建和返回路徑,但是因?yàn)槲覀円呀?jīng)掌握了形狀將被使用的大小,所以我們確切地知道要繪制我們的路徑有多大——我們不再需要依賴(lài)固定坐標(biāo)。
例如,之前我們使用Path
創(chuàng)建了一個(gè)三角形
,但我們可以將其包裹在一個(gè)形狀中以確保它自動(dòng)占據(jù)所有可用空間,如下所示:
使這項(xiàng)工作變得容易得多CGRect
,它提供了有用的屬性,例如minX
(矩形中的最小 X 值)、maxX
(矩形中的最大 X 值)和midX
(
minX
和
maxX
之間的中點(diǎn))。
然后我們可以創(chuàng)建一個(gè)精確大小的紅色三角形,如下所示:
StrokeStyle
形狀還支持用于創(chuàng)建更高級(jí)筆畫(huà)的相同參數(shù):
理解
Path
和Shape
之間區(qū)別的關(guān)鍵
是可重用性:路徑旨在完成一件特定的事情,而形狀具有繪圖空間的靈活性,并且還可以接受參數(shù)以讓我們進(jìn)一步自定義它們。
為了演示這一點(diǎn),我們可以創(chuàng)建一個(gè)Arc
接受三個(gè)參數(shù)的形狀:開(kāi)始角度、結(jié)束角度以及是否順時(shí)針繪制圓弧。這可能看起來(lái)很簡(jiǎn)單,特別是因?yàn)?span id="s0sssss00s" class="color-pink-02">Path
它有一個(gè)addArc()
方法,但正如你將看到的那樣,它有幾個(gè)有趣的雜項(xiàng)。
讓我們從最簡(jiǎn)單的弧形開(kāi)始:
我們現(xiàn)在可以像這樣創(chuàng)建一個(gè)圓弧:
如果你查看弧線(xiàn)的預(yù)覽,很可能它看起來(lái)與你預(yù)期的完全不同。我們要求順時(shí)針旋轉(zhuǎn)從 0 度到 110 度的弧,但我們似乎得到了逆時(shí)針旋轉(zhuǎn)的 90 度到 200 度的弧。
這里發(fā)生的事情有兩個(gè)方面:
在 SwiftUI 看來(lái) 0 度不是筆直向上,而是筆直向右。
形狀從左下角而不是左上角測(cè)量它們的坐標(biāo),這意味著 SwiftUI 從一個(gè)角度到另一個(gè)角度是相反的。在我看來(lái),這是非常陌生的。
我們可以使用一種新方法來(lái)解決這兩個(gè)問(wèn)題,該path(in:)
方法從開(kāi)始角和結(jié)束角減去 90 度,并翻轉(zhuǎn)方向,以便 SwiftUI 的行為符合自然預(yù)期的方式:
運(yùn)行該代碼,看看你的想法——對(duì)我來(lái)說(shuō),它產(chǎn)生了一種更自然的工作方式,并巧妙地隔離了 SwiftUI 的繪圖行為。



使用 InsettableShape 添加 strokeBorder() 支持
如果你創(chuàng)建一個(gè)沒(méi)有特定大小的形狀,它會(huì)自動(dòng)擴(kuò)展以占據(jù)所有可用空間。例如,這將創(chuàng)建一個(gè)填滿(mǎn)我們視圖的圓圈,并給它一個(gè) 40 磅的藍(lán)色邊框:
仔細(xì)觀察邊框的左右邊緣——你是否注意到它們是如何被切斷的?
你在這里看到的是 SwiftUI 在形狀周?chē)L制邊框的方式的副作用。如果你遞給某人一個(gè)圓的鉛筆輪廓,并要求他們用粗筆在圓上畫(huà)畫(huà),他們會(huì)準(zhǔn)確地描繪出圓的線(xiàn)——大約一半的筆在線(xiàn)內(nèi),一半在線(xiàn)外。這就是 SwiftUI 為我們所做的,但是當(dāng)我們的形狀到達(dá)屏幕邊緣時(shí),這意味著邊框的外部部分最終超出了我們的屏幕邊緣。
現(xiàn)在嘗試使用這個(gè)圓圈:
現(xiàn)在我們把stroke()
改成strokeBorder()
,得到
了一個(gè)更好的結(jié)果:我們所有的邊界都是可見(jiàn)的,因?yàn)?Swift 撫摸著圓圈的內(nèi)部而不是在線(xiàn)的中心。
之前我們構(gòu)建了一個(gè)這樣的Arc
形狀:
就像Circle
,它會(huì)自動(dòng)占用所有可用空間。但是,這種代碼將不起作用:
如果你打開(kāi) Xcode 的錯(cuò)誤消息,你會(huì)看到它說(shuō)“Value of type 'Arc' has no member 'strokeBorder’(類(lèi)型 'Arc' 的值沒(méi)有成員 'strokeBorder')”——也就是說(shuō),strokeBorder()
修飾符不存在于Arc
.
SwiftUI的Circle
和我們的Arc
之間有一個(gè)很小但很重要的區(qū)別
:兩者都符合Shape
協(xié)議,但Circle
?也符合協(xié)議:InsettableShape
.?這是一種可以插入(向內(nèi)縮?。┮欢恳援a(chǎn)生另一種形狀的形狀。它生成的內(nèi)嵌形狀可以是任何其他類(lèi)型的不可設(shè)置形狀,但實(shí)際上它應(yīng)該是相同的形狀,只是較小的矩形。
為了讓Arc
符合InsettableShape
。
我們需要向它添加一個(gè)額外的方法:inset(by:)
。這將被賦予插入量(筆劃線(xiàn)寬的一半),并且應(yīng)該返回一種新的可插入形狀——在我們的例子中,這意味著我們應(yīng)該創(chuàng)建一個(gè)插入弧。問(wèn)題是,我們不知道弧的實(shí)際大小,因?yàn)?span id="s0sssss00s" class="color-pink-02">path(in:)
還沒(méi)有被調(diào)用。
解決方案其實(shí)很簡(jiǎn)單:如果我們給Arc
形狀一個(gè)insetAmount
默認(rèn)為 0 的新屬性,我們可以在inset(by:)
調(diào)用時(shí)添加它。需要時(shí),我們可以調(diào)用多次inset(by:)
,例如,如果我們想手動(dòng)調(diào)用一次,然后使用strokeBorder()
.
首先,將這個(gè)新屬性添加到Arc
:
現(xiàn)在給它這個(gè)inset(by:)
方法:
重要提示:這是我們需要使用CGFloat
的極少數(shù)地方之一
,這是一種古老的Double
形式
,有點(diǎn)奇怪,它已經(jīng)進(jìn)入了 SwiftUI。它也被用于許多其他地方,但大多數(shù)情況下 Swift 允許我們使用Double
替代它
!
傳入的amount
參數(shù)應(yīng)應(yīng)用于所有邊,在弧的情況下,這意味著我們應(yīng)該使用它來(lái)減小繪制半徑。因此,將addArc()
內(nèi)部調(diào)用更改path(in:)
為:
有了這個(gè)改變,我們現(xiàn)在可以像這樣Arc
符合InsettableShape
:
注意:?InsettableShape
實(shí)際上是建立在 Shape
之上的
,因此無(wú)需在其中添加兩者。


