Xcode — SwiftUI 做到類同 UISegmentedControl 效果

春麗 S.T.E.M.
7 min readNov 26, 2024

--

目錄

⦿ 前情提要
⦿ SwiftUI
⦿ 顏色
⦿ 位置

前情提要

前一篇文章中,我們知道 SegmentedControl 並不是那麼容易客製化成我們要的效果,所以建構一個客製化的 View,來去處理切換 Segment 時顯示的切換,由於前篇使用了 UIKit,在這邊,我們也可以看看 SwiftUI 的效果如何。

這個頁籤的切換是放在 NavigationBar 上,做為推薦文章、熱門文章的切換,且左右切換像是滑動 ScrollView 來做呈現。

下面,我們來看看 SwiftUI 如何達成的。

繼續閱讀|回目錄

SwiftUI

從下圖,我們可以理解到如果這個 NavigationBar 的 View 是這樣排列,那麼最左邊、最右邊各是一個 Button,中間為兩個 Text,四者之間塞了三個 Spacer(),這個 Spacer() 就如同 UIKit 的 fixedSpace

let fixedSpace = UIBarButtonItem(barButtonSystemItem: .fixedSpace,
target: nil,
action: nil)

我們知道 ButtonTextTextButton 結構會是用 HStack 包起來的。

struct HomeNavigationBar: View {
var body: some View {
HStack(alignment: .top, spacing: 0) {
Button()
Spacer()
Text("推薦")
Spacer()
Text("推薦")
Spacer()
Button()
}
}
}

但是 Text 下方有一個切換條在跳動,所以應該是要把 TextSpacer()TextHStack 先包起來,再把 HStack切換條VStack 包起來,並且適時地更新切換條的位置。

除此之外,選中的 Text 顏色比較深,未選中的 Text 顏色比較淺,於是,看起來有兩個變數了。

由於 Text 並不是可以互動的按鈕,所以幫他加上 onTapGesture,這個 onTapGesture 最好可以去更改前面說的兩個變數,即切換條的位置(在 UIKit 中為 Constraint)會更新。

顏色

Text("推薦").opacity(Double(1 - offsetRatio * 0.5))
.onTapGesture {
withAnimation {
self.offsetRatio = 0
}
}

Text("熱門").opacity(Double(0.5 + offsetRatio * 0.5))
.onTapGesture {
withAnimation {
self.offsetRatio = 1
}
}

我們要做到的是將這個 offsetRatio 當作 Segment 的 Index,0 的時候為第一頁,1 的時候為第二頁。

也就是說,當我們加入 opacity 的時候,0 的時候第一頁為深色(opacity 為 1),第二頁為淺色(opacity 為 0.5);1 的時候第一頁為淺色(0.5),第二頁為深色(1)。

顏色確立了,下面來確認位置。

位置

在 VStack 中,包了一個圓角矩形,圓角半徑為 2,寬度為 Text 的一半,高度為 4,而重要的是它的 offset。

RoundedRectangle(cornerRadius: 2)
.foregroundColor(.blue)
.frame(width: 30, height: 4)
.offset(
x: screenWidth * 0.5 * (offsetRatio - 0.5) + kLabelWidth * (0.5 - offsetRatio)
)
// .frame(height: 6)

由於點擊 Text 為 01 的切換,這個 offset 也會依照 0 與 1 去更新。

不過我們先看看 VStack 的寬度。

VStack(spacing: 3) {
HStack {
Text("推薦")
Spacer()
Text("熱門")
}

RoundedRectangle(cornerRadius: 2)

}.frame(width: screenWidth * 0.5)

這個 VStack 如下,只占螢幕的一半。

這裡的計算要將橘色部份切成一半來看為 x/2,Text 的寬度為 y,圓角矩形的寬度為 y/2,但這邊必須要知道當 offset = 0 時,這個圓角矩形會在正中間,所以當 Index = 0 時,它要向左偏移;Index = 1 時,它要向右偏移

所以當 Index = 0 時,它必須從中間向左(負的)一半 VStack Width,但必須加回圓角矩形的 Width(因為是從左邊端點移動),即是 -x/2 + y/2

當 Index = 1 時,它必須從中間向右(正的)一半 VStack Width,但必須減掉圓角矩形的 Width(因為是從右邊端點移動),即是 x/2-y/2

所以提取 (offsetRatio - 0.5)(x - y) 就是偏移的公式了!得到如下:

VStack(spacing: 3) {
HStack {
Text("推薦")
Spacer()
Text("熱門")
}

RoundedRectangle(cornerRadius: 2)
.offset(
x: (offsetRatio - 0.5)(screenWidth * 0.5 - kLabelWidth)
)

}.frame(width: screenWidth * 0.5)

最後,就可以在 HomeView 中設定 NavigationBar,如下:

struct HomeView: View {
@State var offsetRatio: CGFloat = 0

var body: some View {
let screenWidth = UIScreen.main.bounds.width
let screenHeight = UIScreen.main.bounds.height

NavigationView {
HScrollViewController(pageWidth: screenWidth,
contentSize: CGSize(
width: screenWidth * 2,
height: screenHeight
),
offsetRatio: self.$offsetRatio) {
}
.navigationBarItems(leading: HomeNavigationBar(offsetRatio: $offsetRatio))

}
}

}

在這個 NavigationView 中,有一個客製化的 HScrollViewController 能夠呈現水平滑動的 View,這個 View width 為螢幕寬的兩倍,表示有兩頁的 View 可以呈現,即是推薦熱門頁的切換,然而他有一個 offsetRatio 的 @State 變數,並且它的 navigationBarItems 放入一個客製化的 HomeNavigationBar,同樣地,offsetRatio 是它的變數。

這表示 offsetRatio 的變化,會改動 scrollView 的呈現,反之亦然,這即是說如果 scrollView 滾動到第一頁,Index 為 0,滾動到第二頁,Index 為 1;或者是剛才的 Button 改動 Index 為 0 或 1,scrollView 同樣在第一頁與第二頁間切換。

是不是很有邏輯呢?

這次就分享到這,感謝您的閱讀。

繼續閱讀|回目錄

--

--

春麗 S.T.E.M.
春麗 S.T.E.M.

Written by 春麗 S.T.E.M.

Do not go gentle into that good night, Old age should burn and rave at close of day; Rage, rage, against the dying of the light.

No responses yet