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 新手看到這篇文章很有可能就這麼開始誤用了所以以免萬一。