SwiftUI 的 custom binding

開發 SwiftUI App 時,很多 UI 元件都搭配了 Binding 型別的參數,因此具有元件修改時自動更新資料,資料修改時自動更新元件的神奇效果,比方以下跟變數 isPlay 綁在一起的 Toggle 元件。

struct ContentView: View {
@State private var isPlay = false

var body: some View {
Toggle("播放", isOn: $isPlay)
}
}

然而有時我們希望元件改變時能額外做一些事,不只是更新變數的內容。這要如何實現呢 ?

在 iOS 14 有比較簡單的方法,透過偵測內容改變的 onChange modifier。

舊版的話則可自己產生 Custom Binding, 在 Binding 裡撰寫想做的事情。接下來我們將以兩個常見的例子說明,利用 toggle 控制音樂的播放和讓 slider & date picker 連動。

利用 toggle 控制音樂的播放

以播放周興哲的我很快樂為例,我們希望點選 toggle 控制音樂的播放,當 Toggle 裡加 $ 傳入 Bindign 時將無法控制音樂的播放。

struct ContentView: View {
@State private var isPlay = false
let player = AVPlayer(url: URL(string: "https://audio-ssl.itunes.apple.com/itunes-assets/Music1/v4/ab/3e/54/ab3e546a-ceb8-0d53-5169-9f1d6d55586c/mzaf_4788478901280424198.plus.aac.p.m4a")!)

var body: some View {
VStack {
Toggle("播放", isOn: $isPlay)
}
.padding()
}
}

為了點選 toggle 控制音樂播放,生成 Toggle 時,我們在 isOn 參數傳入自己產生的 Binding,選擇 (get: ()-> _, set: (_) -> Void)

在 Binding 的 get & set 參數撰寫以下程式。Toggle 將依據 get 的回傳結果決定顯示的狀態,true 時顯示打開,false 時顯示關閉。我們希望變數 isPlay 等於 true 時顯示打開,isPlay 等於 false 時顯示關閉,因此在 get 裡回傳 isPlay。使用者點選 toggle 時將執行參數 set 的 closure,$0 代表開關顯示的狀態。我們希望開關顯示的狀態和變數 isPlay 一致,因此我們將 $0 存到 isPlay。

var body: some View {
VStack {
Toggle("播放", isOn: Binding(get: {
isPlay
}, set: {
isPlay = $0
}))
}
.padding()
}

改成以上寫法後,想要打開開關時播放音樂,關閉開關時暫停音樂就簡單多了。我們可在參數 set 的 closure 裡依據 isPlay 呼叫 player 的 play 或 pause。

var body: some View {
VStack {
Toggle("播放", isOn: Binding(get: {
isPlay
}, set: {
isPlay = $0
if isPlay {
player.play()
} else {
player.pause()
}
}))
}
.padding()
}

slider & date picker 的連動

我們想用 slider & date picker 選擇 2011 ~ 2020 的年份,想分別做到很簡單,但想要滑動 slider 時更新 date picker,滑動 date picker 時更新 slider 卻不容易。當我們生成 Slider & DatePicker 時傳入 $ 產生的 Binding,滑動 Slider & DatePicker 時只會更新它們綁定的變數。

struct ContentView: View {
@State private var date = Date()
@State private var year: Double = Double(Calendar.current.component(.year, from: Date()))

var body: some View {

var components = DateComponents()
components.calendar = Calendar.current
components.year = 2011
let startDate = components.date!
components.year = 2020
let endDate = components.date!
return VStack {
Text("\(year, specifier: "%g")")
Slider(value: $year, in: 2011...2020, step: 1)
DatePicker(selection: $date, in: startDate...endDate, displayedComponents: .date) {
Text("")
}
}
.padding()
}
}

改用 custom binding,我們可在滑動 slider 時更新 date picker 綁定的 date,滑動 date picker 時更新 slider 綁定的 year。

struct ContentView: View {
@State private var date = Date()
@State private var year: Double = Double(Calendar.current.component(.year, from: Date()))

var body: some View {

var components = DateComponents()
components.calendar = Calendar.current
components.year = 2011
let startDate = components.date!
components.year = 2020
let endDate = components.date!
return VStack {
Text("\(year, specifier: "%g")")
Slider(value: Binding(get: {
year
}, set: {
year = $0
var components = DateComponents()
components.calendar = Calendar.current
components.year = Int(year)
date = components.date!
}), in: 2011...2020, step: 1)
DatePicker(selection: Binding(get: {
date
}, set: {
date = $0
year = Double(Calendar.current.component(.year, from: date))
}), in: startDate...endDate, displayedComponents: .date) {
Text("")
}
}
.padding()
}
}

參考連結

https://www.hackingwithswift.com/guide/ios-swiftui/2/2/key-points

好聽的周興哲我很快樂綱琴演奏

--

--

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

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