IconCard 📦
以「SF Symbol 為前景,透明方紙為背景」的元件,適合用於當遮罩或反面遮罩。
Last updated
以「SF Symbol 為前景,透明方紙為背景」的元件,適合用於當遮罩或反面遮罩。
Last updated
如果遮罩圖與要剪的紙一樣大,用 SystemImage 📦 就可以。 如果遮罩圖較小、要剪的紙較大,用 IconCard 📦 。
下圖顯示 img
(IconCard) 從套用 .foregroundColor(.black)、.background(Color.white)、.compositingGroup()、.luminanceToAlpha(),一直到 paper
(紅色星點紙) 套用 .mask() 的整個過程。
這個過程後來被濃縮成一個 View extension:.inverseMask() 🌀
import SwiftUI
import Workaround // for `SystemImage`
/// # 📦 IconCard
/// - icon: SF Symbol 名稱。
/// - size: ⚠️ 注意:這是圖示大小,不是整張卡的大小‼️
/// ## Example
/// ````
/// IconCard(icon: "heart.fill", size: 32)
/// ````
public struct IconCard: View {
// properties
let iconSize: CGFloat // icon size
let icon : String // icon name
/// public init
public init(_ icon: String, size: CGFloat = 32) {
self.icon = icon
self.iconSize = size
}
// body
public var body: some View {
// 1. 選用 ZStack,讓物件可以在上下層間輕鬆堆疊。
// ⭐️ 先不限制 ZStack 的大小,它的大小由外部環境決定。
ZStack {
// 2. 先準備一張「透明紙」:這裡有兩個用意 ⭐️
// 1. 用 Rectangle 來定義此「元件的大小」(否則會以下面的 SystemImage 為準)。
// 2. 先指定 fg = .clear,將來如果整個元件再被設定 .foreground() 時,
// - Rectangle 的 fg 還是 .clear (不會被 overridden‼️)
// - SystemImage 的 fg 因為還沒被設定過,所以就可以被改寫。
Rectangle()
.foregroundColor(.clear) // ⭐️ 指定「透明色」!
// 3. 然後在上面畫圖示,並設定圖示大小。
// ⭐️ 注意:雖然沒有指定前景色,但在深色模式下,會用「白色」表示。
SystemImage(icon)
.frame(width: iconSize, height: iconSize)
} // container (ZStack)
} // body
}
import SwiftUI
import Workaround // for `SystemImage`
/// # 📦 IconCard
/// - icon: SF Symbol 名稱。
/// - size: ⚠️ 注意:這是圖示大小,不是整張卡的大小‼️
/// ## Example
/// ````
/// IconCard(icon: "heart.fill", size: 32)
/// ````
public struct IconCard: View {
// properties
let iconSize: CGFloat // icon size
let icon : String // icon name
/// public init
public init(_ icon: String, size: CGFloat = 32) {
self.icon = icon
self.iconSize = size
}
// body
public var body: some View {
// 1. 選用 ZStack,讓物件可以在上下層間輕鬆堆疊。
// ⭐️ 先不限制 ZStack 的大小,它的大小由外部環境決定。
ZStack {
// 2. 先準備一張「透明紙」:這裡有兩個用意 ⭐️
// 1. 用 Rectangle 來定義此「元件的大小」(否則會以下面的 SystemImage 為準)。
// 2. 先指定 fg = .clear,將來如果整個元件再被設定 .foreground() 時,
// - Rectangle 的 fg 還是 .clear (不會被 overridden‼️)
// - SystemImage 的 fg 因為還沒被設定過,所以就可以被改寫。
Rectangle()
.foregroundColor(.clear) // ⭐️ 指定「透明色」!
// 3. 然後在上面畫圖示,並設定圖示大小。
// ⭐️ 注意:雖然沒有指定前景色,但在深色模式下,會用「白色」表示。
SystemImage(icon)
.frame(width: iconSize, height: iconSize)
} // container (ZStack)
} // body
}
extension View {
@ViewBuilder
func label(_ text: String) -> some View {
VStack(alignment: .leading) {
self.border(Color.black)
Text(text).padding(.leading).padding(.bottom, 10)
}.border(Color.black)
}
}
import SwiftUI
import PlaygroundSupport
let shad = Color.black.opacity(0.8)
let clear = Image("transparent")
let paper = Image("red-stars").resizable().scaledToFit()
.frame(maxWidth: 280, maxHeight: 200)
struct ContentView: View {
// 0. 設定原圖、圖示大小、整個元件大小。
let mask: some View = IconCard("heart.fill", size: 100)
.frame(width: 150, height: 150)
// 1. 套用 .foregroundColor(),前景塗黑。
// 套用 .luminanceToAlph() 後,alpha = 0,變「透明」,等於「前景被挖掉」。
// ⭐️ 注意:此指令會套用到「所有圖層」,但不會 override 已經有 fgColor 的圖層。
var v1: some View { mask.foregroundColor(.black) }
// 2. 背景塗白
// 套用 .luminanceToAlph() 後,alpha = 1,所以「背景會保留下來」。
var v2: some View { v1.background(Color.white) } // opacity = 1 (show)
// 3. 合併所有圖層
var v3: some View { v2.compositingGroup() }
// 4. 再將亮度調為 alpha
var v4: some View { v3.luminanceToAlpha() }
var v5: some View {
ZStack {
// 紅色星點紙
paper
.mask(v4) // ⭐️ == paper.inverseMask(mask)
.shadow(color: shad, radius: 4, x: 4, y: 4)
// ⭐️ 畫原圖外框,但不顯示原圖。
mask
.hidden()
.border(Color.black)
} // ZStack
.background(Gradient.down(.yellow, .green))
}
var body: some View {
VStack(alignment: .leading) {
HStack {
mask.label("img")
v1 .label("fg = black")
v2 .label("bg = white")
}
HStack(alignment: .top) {
v4
.background(clear) // ⭐️ 底下鋪「透明斜格紙」
.clipped() // ⭐️ 裁掉超出 v4 的部分
.label("亮度轉 alpha")
v5.label("paper.inverseMask(img)")
}
} // container (HStack)
.padding()
.background(Color.gray)
} // body
}
PlaygroundPage.current.setLiveView(ContentView())