更新於 2022 年 10 月 09 日
# 在 SwiftUI 中替 View 加上陰影
在 SwiftUI 中,我們可以輕鬆地替 View 加上陰影,只要加上 .shadow
調整器就可以動態產生陰影。iOS16 更是新增了方便的內陰影 API,產生有立體、層次感的畫面變得更容易。
外陰影
產生一個在右下方的陰影
Rectangle()
.foregroundColor(.teal)
.frame(width: 100, height: 100)
.shadow(radius: 3, x: 6, y: 6)
// 你也可以再加上顏色參數
整體陰影
有些時候你可能想替一個 Layout View 加上陰影,但卻發現連子畫面都被加上陰影了。
VStack(spacing: 30) {
Text("Hello")
Image(systemName: "fish")
}
.font(.largeTitle.bold())
.foregroundColor(.teal)
.padding(40)
.border(.teal, width: 3)
.shadow(radius: 3, x: 6, y: 6) // ?!
這個情況有兩個要注意的地方:
- VStack 本身並沒有具體的「佔位形狀」,現在畫面上出現方框陰影只是因為我們用了「border」(邊框)調整器而產生的邊框陰影。所以首先要先把 VStack 填滿,讓它有一個完整的形狀。
⬇️ 左邊是加上了半透明的背景的結果,跟右邊原本的陰影比較可以看到陰影形狀的差別。
- 陰影調整器會影響所有子畫面。就像太陽光照到地球不是只有地球本身產生陰影,而是地球上的萬物都產生陰影。如果我們不想要每一個 View 都產生陰影,我們可以使用 compositingGroup 這個調整器把相關畫面打包成單一個 View 之後再使用 shadow,就可以讓整個 Layout 只產生一個陰影。
VStack(spacing: 30) {
Text("Hello")
Image(systemName: "fish")
}
.font(.largeTitle.bold())
.foregroundColor(.teal)
.padding(40)
.border(.teal, width: 3)
.background(.white) // 1. 加上背景
.compositingGroup() // 2. 把這一行前面的畫面打包成單一個 View
.shadow(radius: 3, x: 6, y: 6)
// 3. 最後這個陰影就只會賦予打包出來的那一個 view 陰影了
// 💡 注意這三個調整器的先後順序很重要,要看懂為什麼是這個順序哦
內陰影
iOS16 新增了方便產生內陰影的方法,用法和外陰影有一點點不一樣。
外陰影你可以直接套用在任何 View 上面,但是內陰影只能套用在 ShapeStyle 的畫面上,畢竟內陰影需要某種空間才能成立。
💡 顏色、形狀、漸層都有遵循 ShapeStyle。
VStack(spacing: 30) {
Text("Hello")
Image(systemName: "fish")
}
.font(.largeTitle.bold())
.foregroundColor(.teal)
.padding(40)
.border(.teal, width: 3)
.background(
.white
.shadow(
.inner(color: .green, radius: 10))
// 在背景的白色色塊加上綠色內陰影
)
.compositingGroup()
.shadow(radius: 3, x: 6, y: 6)
如果是想在一般的 View 中使用,可以搭配 foregroundStyle
。
⬇️ 上面是單純的橘色,下面是加上內陰影的橘色。
VStack {
Text("Hello")
.foregroundStyle(.orange)
Text("Hello")
.foregroundStyle(.orange.shadow(.inner(radius: 3)))
} .font(.system(size: 50, weight: .black))
Neumorphic(擬物)風格
有一段時間很熱門的 Neumorphic 風格需要內陰影和外陰影搭配。用新的內陰影 API 寫起來短一點,但是很可惜顏色不能用漸層色,所以效果有限。
⬇️ 右邊是 isPressed 的情況。
🎨 範例的顏色色碼:
main - #ECF0F3
lightShadow - #FFFFFF
darkShadow - #D1D9E6
Text("Hello")
.foregroundColor(.teal)
.padding(.vertical, 25)
.padding(.horizontal, 50)
.scaleEffect(isPressed ? 0.98 : 1) // 按下時會稍微小一點
.animation(.easeOut, value: isPressed)
.background(
RoundedRectangle(cornerRadius: 8)
.foregroundStyle(
Color.main // 按下時變成內陰影
.shadow(isPressed ?
.inner(color: .darkShadow, radius: 4, x: 3, y: 3)
: .drop(color: .darkShadow, radius: 10, x: 10, y: 10))
.shadow(isPressed ?
.inner(color: .lightShadow, radius: 4, x: -3, y: -3)
: .drop(color: .lightShadow, radius: 10, x: -5, y: -5))
)
)
.onTapGesture { isPressed.toggle() }
.font(.system(size: 50, weight: .black))
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.main)
如果想用 Neumorphic 風格,內陰影還是用舊的 stroke + 漸層 mask 比較好看。
雖然現在這風格比較少用了,但還是寫來練習陰影 & mask 效果還是很有價值的。如果有興趣可以參考這個 repo。
# 陰影的效能
有時候你可能會看到 SwiftUI 警告你動態陰影的效能問題——
The layer is using dynamic shadows which are expensive to render.
如果實際使用上沒有問題就不用擔心。如果真的遇到卡卡的,你應該先考慮把 Layout 變成 Lazy 的,再考慮改變陰影的寫法。因為除非你是把陰影用來畫圖,否則需要一次 render 大量陰影的情況一定和你一次產生的 view 的數量有關。