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()
}
}