13 的話
SwiftUI 專欄是一個付費專欄,只是我沒有設立付費牆,而是採用「誠實訂閱」制度。
我的目標是累積到 100 位 Patreon 支持者,目前進度來到 25%,感謝新舊朋友們的支持。
很高興專欄寫到第 5 篇了。今天來跟大家聊 Color 這個我很喜歡的主題,相信你會覺得有趣!
如果你是第一次讀這個專欄,建議先回到目錄,照順序閱讀。
首先來說 Color
的排版特性。
等一下 13,Color
不就是顏色嗎,哪來的排版特性?
我說過了呀,別把 UIKit 的思考習慣帶來 SwiftUI 嘛。色塊為什麼不能直接拿來排版呢?
🧱填滿畫面
我們可以把 Color
放進任何的 View Builder 裡,包括 View
的 body
。
顯示出來的效果,就是佔滿整個「畫面」,或者說,有多少就用多少。
用在有瀏海的 iPhone,系統會把 Safe Area 留白。如果想把顏色填滿,可以加個 .edgesIgnoringSafeArea(.all)
。
🥞多個 Color 自動等分
那如果我們把多個 Color
擺在一起呢?很簡單,它們會自動平均分配空間。
千萬別小看這個特性,很多需要等分的排版需求,用多個 Color
就可以解決。
舉例來說,當你需要自己做固定數量的 tab bar。
如果直接用 Button
下去排,因為文字不一樣,寬度顯得不平均,而且會縮在一起,而不是填滿全部的寬度。會變成這樣:
很多人會想說:「只要先知道全部的寬度,再拿來除以 tab 數量就好了。」但是,照著這個思路來做,很容易就會用上一期我說盡量別使用的 GeometryReader
。這裡就不做錯誤示範了。
其實遇到這種需求,用 Color
來排版真的很簡單。
你看這樣子,橫向寬度是否每個就是 1/3 了呢?
稍微解釋一下。首先我們在 HStack
裡放了 3 個 Color
,但是不需要真的有顏色,所以直接用透明的 .clear
。
接下來在每個 Color
加上 .overlay
這個 modifier,裡面放實際要按的 Button
。
.overlay
也是個好東西,其實我一直很想花大篇幅來介紹,但今天的主角不是他。目前記得拿它跟Color
一起服用就好了。
🔧修正 Button
點按範圍
上面這段程式碼看似排版正確了,但實際行為有點問題。是哪裡呢?原來是按下去的範圍仍然僅限於有文字的地方。因為 Button
的範圍僅限於 label
的內容。
讓我們對調一下內外順序,把 Color.clear.overlay
放到 Button
的 label
裡,就可以了。
🌄.foregroundColor 與 .background
有一次,同事在 review 我的 SwiftUI 程式碼時,問了非常好的問題:
「同樣是設定顏色,為什麼 .foregroundColor(color) 的名稱中有 color ,但是 .background(color) 卻沒有 color 呢?」
這是因為,這兩個 modifier 其實不是一對的,意義完全不同。
.foregroundColor 是用來幫文字、圖形上色的。我們傳入的
Color
是拿來當顏色使用的。.background 是用來幫
View
在它背後同樣範圍,放入另一個View
的。我們不一定要傳入Color
!
當然,如果傳入 Color
的話,它就會被當成 View
並且填滿「整個範圍」。這就回到我一開始說的 Color
排版特性。
每個 Color
都可以當成 View
來使用。
我的說明有簡化過,因為
background
有好幾種版本。這邊指的是傳入View
的 background<V>(),有興趣深入的話,請直接看連結的文件吧。
🌃.background 與 .overlay
再跟你說一件秘密吧。我們一開始不是教了 Color
搭配 .overlay
嗎?其實這個 .overlay
跟 .background
的排版行為完全一樣!
兩者都是在目前的 View
同樣範圍再放入另一個 View
。差別只是在於,加在 z 軸的上方還是下方而已。
上圖的範例中,兩個 Text
完全相同,尺寸也相同。Color
放在 .overlay
與 .background
的差別只在於跟 Text
是誰蓋掉誰。
結果我還是忍不住介紹了
.overlay
😂
🈳Color.clear
、 Spacer(),與 EmptyView()
這三個東西,乍看之下很像,實際上完全不同。
先說 EmptyView()
。當你需要指定或回傳一個 View
,但是又不希望看到它,就可以使用 EmptyView
,因為它不會參與排版。
雖然 Apple 官方文件說我們很少會用到,不過實際在寫程式的過程中,我還滿常拿來當作「暫時讓 code 可以 build 過的東西」。
這東西不佔視覺空間,所以我不稱它為「placeholder」,以免混淆概念了。
接下來比較 Color.clear
與 Spacer()
的不同。
我們已經知道 Color
會填滿範圍了,Spacer()
也是。但是 Spacer()
一般來說是用在 VStack
跟 HStack
,在其他 View
之間拉開距離用的。此時只會在單一軸向發生作用。
換言之,就是留白。
Spacer()
的優先度很低,別的 View
決定好長度或寬度以後,剩下的才留給 Spacer()
。相對的,Color
會吃掉一定的空間,甚至有可能會搶去其他 View
的。
上圖除了展示 Color
擠壓到 Text
的水平生存空間以外,它還往垂直方向長到滿。
所以,如果只是希望剩下的空間留白,應該用 Spacer()
,而不是 Color
。
通常也不會把
Color
跟Text
擺在同一個VStack
或HStack
裡啦。
🌈Color 與 UIColor 近似的顏色功能與特性
最後,讓我們很快帶過 Color
在顏色方面的功能與特性。很多都跟 UIColor 很像,所以比較簡單。
建立 Color
方面:
可以在 Asset Catalog 設定色碼與名字,並用 String 來 init
可以用 rgb、hue 與 saturation,或是 white 的值再搭配透明度來 init
可以從 UIColor、NSColor、CGColor 來 init,也可以反過來轉成 UIColor、NSColor、CGColor?
系統預設的彩色顏色,也跟 UIColor
沒什麼不同。有 red、orange、yellow 等等,可以直接從 Apple 的 Human Interface Guidelines 找到色碼表。
要注意的是,這些顏色都是動態的,會依照 Light / Dark Mode、Accessibility 的高對比設定等情況而有所不同。
這個特性可以在 Asset Color 設定(下圖右邊兩個箭頭)。
最基本的 black、white 當然也有,不過沒有提供 systemGray2 等灰階色。
UIKit element colors 也不在 SwiftUI Color
裡,只有 .primary 與 .secondary。畢竟 Color
還要支援非 iOS 的作業系統。真的有需要的話,自己轉就行了。
🎨Color 獨特的顏色功能與特性
.accentColor 是 SwiftUI
Color
特別的 property,可以說是整個 app 的主題色調吧。你可以在 Asset Catalog 設定。現在新增專案預設都會看到它(上圖左邊箭頭)。
值得一提的是,Mac app 要使用 accentColor 的話,使用者必須把系統偏好設定的強調顏色設定成「多色」才會發生作用。
可以用 .opacity() 直接幫
Color
修改透明度。iOS 16 新增了很有趣的功能 .gradient,可以直接從
Color
產生漸層的AnyGradient
。
Color
如何「保存」?
最後我想提一點,顏色的指定其實是滿複雜的事情。如果你想把 Color
存起來供未來使用(比如說把使用者選擇的顏色存在 UserDefaults
),Color
沒有提供直接讀取 rgb 與 alpha 值的功能,可能要先轉成 UIColor
再處理。
由於 Color
跟 UIColor
都有動態數值的功能,以及可能要考慮 color space,務必把這些細節搞清楚,以免存錯數值。
結論
Color
是View
,有排版特性,會填滿範圍多個
Color
擺在一起時,會平分空間搭配
.overlay
很好用.foregroundColor
與.background
不是一對。傳入Color
時,前者會拿來當顏色用,後者會拿來當View
使用.overlay
與.background
才是一對在
V/HStack
中,Color
會往兩軸填滿、Spacer()
只會在單軸做留白EmptyView()
不會參與排版顏色功能方面,許多都與
UIColor
相似獨特之處在於
.accentColor
、.opacity()
、.gradient
等如果要保存
Color
要特別小心
讀完這期 SwiftUI 專欄,有沒有覺得「好多東西其他 SwiftUI 教學都沒提過」呢?
喜歡本專欄的讀者,請到 Patreon 訂閱支持我🙏 我的目標是累積到 100 位支持者,目前 25%。也請按下愛心❤️、留言💬、回信✉️與我交流。這些回饋都會直接影響到我繼續寫作的動力與頻率喔。
希望今天的內容讓你可以少走一些彎路。我們下一期見!
13 你好。斗膽發表一點反駁意見。
用 Color 搭配 .overlay 來排版完全是對 Color 的誤用。
添加了不必要的 View 層級,也大大降低了代碼可讀性。
Color 看起来有平分空间的特性不是因为它是 Color,而是因为:
1. 它被设置成「佔用所有可用空間(ProposedViewSize)」的效果
2. HStack 对拥有该效果且 LayoutPriority 相等的所有 Child View 进行可用空间的平分
1的效果所有 Shape(Rectangle、Circle)都有,GeometryReader 也有。
講2是因為如果你給 Child View 添加不同的 LayoutPriority,這種效果就會失效。
最直接且正統的做法應該是给需要这种效果的 Child View 添加 .frame(maxWidth(or Height): .infinity),即告訴 SwiftUI「我要让這個 View 佔用尽可能大的可用空間」。
當然,說實話我不太相信13會不懂這個,但 SwiftUI 新手看到這篇文章很有可能就這麼開始誤用了所以以免萬一。