SwiftUI 偵測內容改變的 onChange modifier

開發 iOS App 時,我們可以用 Swift property observer 的 willSet & didSet 設定變數內容改變時觸發某段程式,不過很可惜這樣的招術對 SwiftUI 的 @State & @Binding 變數無效。

例子1: 切換 toggle 播放音樂

比方以下例子,我們想要切換開關播放手心的薔薇。

import SwiftUI
import AVFoundation

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 {
Image("手心的薔薇")
.resizable()
.scaledToFit()
Toggle("播放手心的薔薇", isOn: $isPlay)
}
.padding()

}
}

因此我們應該在 isPlay 為 true 時播放音樂,isPlay 為 false 時停止播放。如果 isPlay 內容改變時可以觸發 didSet,我們可用以下的程式實現功能。

@State private var isPlay = false {
didSet {
if isPlay {
player.play()
} else {
player.pause()
}
}
}

但是天不從人願,@State 變數內容改變時,並不會觸發 property observer。

此問題也不是無解,我們可透過 modifier onChange 或 custom binding 解決。

onChange 可以偵測內容改變,在觀察的資料改變時觸發程式執行。

onChange 依據參數 action 的型別有以下兩種寫法。

  • 參數 action 的型別為 () -> Void
func onChange<V>(
of value: V,
initial: Bool = false,
_ action: @escaping () -> Void
) -> some View where V : Equatable
  • 參數 action 的型別為 (V, V) -> Void

V, V 分別對應資料的 oldValue & newValue。

func onChange<V>(
of value: V,
initial: Bool = false,
_ action: @escaping (V, V) -> Void
) -> some View where V : Equatable

以下我們改寫剛剛手心薔薇的程式說明。

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 {
Image(.手心的薔薇)
.resizable()
.scaledToFit()
Toggle("播放手心的薔薇", isOn: $isPlay)
}
.padding()
.onChange(of: isPlay) { oldValue, newValue in
if newValue {
player.play()
} else {
player.pause()
}
}
}
}

當 isPlay 改變時,onChange 參數 action 的程式將被觸發,執行以下的 closure 程式。oldValue 是 isPlay 之前的內容,newValue 是 isPlay 新的內容,因此判斷 newValue 可決定是否播放音樂。

{ oldValue, newValue in
if newValue {
player.play()
} else {
player.pause()
}
}

ps: iOS 17 之前版本的 onChange 請改用以下寫法。

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 {
Image(.手心的薔薇)
.resizable()
.scaledToFit()
Toggle("播放手心的薔薇", isOn: $isPlay)
}
.padding()
.onChange(of: isPlay, perform: { value in
if value {
player.play()
} else {
player.pause()
}
})
}
}

例子2: slider & date picker 的連動

struct ContentView: View {
@State private var date = Date()
@State private var year: Double = Double(Calendar.current.component(.year, from: Date()))
let startDate = DateComponents(calendar: .current, year: 2011).date!
let endDate = DateComponents(calendar: .current, year: 2100).date!

var body: some View {
VStack {
Text(year.formatted())
Slider(value: $year, in: 2011...2100, step: 1)
DatePicker(selection: $date, in: startDate...endDate, displayedComponents: .date) {
Text("")
}
.datePickerStyle(.graphical)
}
.onChange(of: year, { oldValue, newValue in
date = DateComponents(calendar: .current, year: Int(newValue)).date!
})
.onChange(of: date, { oldValue, newValue in
year = Double(Calendar.current.component(.year, from: newValue))
})
}
}

--

--

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

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