利用 SwiftUI 的 offset 位移 & 讓 stack 裡元件重疊

利用 offset 我們可以讓元件從它原本的位置偏移,比方以下 IG 頭像上的 # 即可利用 offset 實現。

ps: 對單字 offset 不熟的朋友,也可以參考以下 AI 的說明

https://chat.openai.com/share/0b6e6a46-9f25-4d70-90f2-acd18776c029

接下來就讓我們模仿 IG,試試 offset 的威力吧。以下以 ZStack 包含的 Image & Text 原本將置中重疊在一起。

struct ContentView: View {
var body: some View {
ZStack {
Image(.peter)
.resizable()
.scaledToFill()
.frame(width: 200, height: 200)
.clipShape(.circle)
Text("#")
.font(.system(size: 30))
.frame(width: 40, height: 40)
.background(.yellow)
.clipShape(.circle)
}
}
}

我們希望 # 的 Text 在右下角,所以我們在 ZStack 上添加參數 alignment,傳入 .bottomTrailing。

struct ContentView: View {
var body: some View {
ZStack(alignment: .bottomTrailing) {
Image(.peter)
.resizable()
.scaledToFill()
.frame(width: 200, height: 200)
.clipShape(.circle)
Text("#")
.font(.system(size: 30))
.frame(width: 40, height: 40)
.background(.yellow)
.clipShape(.circle)
}
}
}

利用 offset 移動元件

現在它已經很像 IG 的設計了,不過我們追求完美,所以這時再利用 offset 做偏移,x & y 傳入 -10,讓 # 向左和向上偏移 10 points。

struct ContentView: View {
var body: some View {
ZStack(alignment: .bottomTrailing) {
Image(.peter)
.resizable()
.scaledToFill()
.frame(width: 200, height: 200)
.clipShape(.circle)
Text("#")
.font(.system(size: 30))
.frame(width: 40, height: 40)
.background(.yellow)
.clipShape(.circle)
.offset(x: -10, y: -10)
}
}
}

參數 x & y 的預設值為 0,因此我們也可以只傳入 x 或 y,比方只傳入 x,讓它水平移動。

.offset(x: -10)

除了傳入 x & y,我們也可以傳入 CGSize,因此以下傳入 CGSize(width: -10, height: -10) 一樣會讓 # 向左和向上偏移 10 points。

.offset(CGSize(width: -10, height: -10))

要特別注意的,利用 offset 偏移後,元件的框框位置並沒有變,因此以下先呼叫 offset,再呼叫 background 後,它將在圖片的框框裡填滿背景顏色,造成圖片和背景色沒有對齊。

struct ContentView: View {
var body: some View {
Image(.peter)
.resizable()
.scaledToFill()
.frame(width: 200, height: 200)
.clipShape(.circle)
.offset(x: 50)
.background(.yellow)
}
}

因此我們應該先呼叫 background 設定背景,然後再呼叫 offset,如此即可連背景一起偏移。

struct ContentView: View {
var body: some View {
Image(.peter)
.resizable()
.scaledToFill()
.frame(width: 200, height: 200)
.clipShape(.circle)
.background(.yellow)
.offset(x: 50)
}
}

讓 stack 裡元件重疊

若想利用 offset 製造 HStack 或 VStack 裡元件部份重疊的效果,有時會發現結果不如預期,比方我們希望 HStack 裡水平排列的圖片部分重疊。

struct ContentView: View {
var body: some View {
HStack {
ForEach(0..<3) { index in
Image("peter\(index)")
.resizable()
.scaledToFill()
.frame(width: 100, height: 100)
.clipShape(.circle)
.offset(x: index > 0 ? -30 : 0)
}
}
}
}

結果卻發現第三張圖沒有跟第二張圖重疊,因為第二張圖和第三張圖都左移30,所以它們還是不會重疊。就像彼得潘要追左邊的 Wendy,彼得潘朝著 wendy 靠近 10 公分,但 wendy 也朝著左邊移動 10 公分,所以彼得潘還是無法追上 wendy。

此問題的修正方法是加上 padding,比方位移了 -30 的元件也補上 -30 的 padding,讓它的框框也跟著縮小。當第二張圖左移 30 後,第三張圖還是跟它保持原本的距離,而不是多了 30 points 的距離,如此第三張圖呼叫 offset 後即可追上第二張圖,跟第二張圖重疊。

struct ContentView: View {
var body: some View {
HStack {
ForEach(0..<3) { index in
Image("peter\(index)")
.resizable()
.scaledToFill()
.frame(width: 100, height: 100)
.clipShape(.circle)
.offset(x: index > 0 ? -30 : 0)
.padding(.trailing, index > 0 ? -30 : 0)
}
}
}
}

Button 的 offset 和點選問題

當 offset 作用在 button 時,有個特別的小陷阱。

struct ContentView: View {
var body: some View {
Button {
print("tap")
} label: {
Text("Imagination is the only weapon in the war against reality")
.padding()
.background {
Capsule()
.fill(.yellow)
}
}
.offset(y: 100)
}
}

若是將 offset 加在呈現 button 內容的 UI 元件,比方以上範例的 Text,將造成 button 顯示的位置移動了,但點選的位置不變,所以出現下圖的靈異現象,點選黃色區域沒反應,點選中間空白的地方才會點擊到 button。

因此若想用 offset 偏移 button 的位置記得要直接加在 button 上。

struct ContentView: View {
var body: some View {
Button {
print("tap")
} label: {
Text("Imagination is the only weapon in the war against reality")
.padding()
.background {
Capsule()
.fill(.yellow)
}
}
.offset(y: 100)
}
}

--

--

彼得潘的 iOS App Neverland
彼得潘的 Swift iOS App 開發問題解答集

彼得潘的iOS App程式設計入門,文組生的iOS App程式設計入門講師,彼得潘的 Swift 程式設計入門,App程式設計入門作者,http://apppeterpan.strikingly.com