问题
In my SwiftUI view I have to trigger an action when a Toggle() changes its state. The toggle itself only takes a Binding. I therefore tried to trigger the action in the didSet of the @State variable. But the didSet never gets called.
Is there any (other) way to trigger an action? Or any way to observe the value change of a @State variable?
My code looks like this:
struct PWSDetailView : View {
@ObjectBinding var station: PWS
@State var isDisplayed: Bool = false {
didSet {
if isDisplayed != station.isDisplayed {
PWSStore.shared.toggleIsDisplayed(station)
}
}
}
var body: some View {
VStack {
ZStack(alignment: .leading) {
Rectangle()
.frame(width: UIScreen.main.bounds.width, height: 50)
.foregroundColor(Color.lokalZeroBlue)
Text(station.displayName)
.font(.title)
.foregroundColor(Color.white)
.padding(.leading)
}
MapView(latitude: station.latitude, longitude: station.longitude, span: 0.05)
.frame(height: UIScreen.main.bounds.height / 3)
.padding(.top, -8)
Form {
Toggle(isOn: $isDisplayed)
{ Text("Wetterstation anzeigen") }
}
Spacer()
}.colorScheme(.dark)
}
}
The desired behaviour would be that the action "PWSStore.shared.toggleIsDisplayed(station)" is triggered when the Toggle() changes its state.
回答1:
First, do you actually know that the extra KVO notifications for station.isDisplayed
are a problem? Are you experiencing performance problems? If not, then don't worry about it.
If you are experiencing performance problems and you've established that they're due to excessive station.isDisplayed
KVO notifications, then the next thing to try is eliminating unneeded KVO notifications. You do that by switching to manual KVO notifications.
Add this method to station
's class definition:
@objc class var automaticallyNotifiesObserversOfIsDisplayed: Bool { return false }
And use Swift's willSet
and didSet
observers to manually notify KVO observers, but only if the value is changing:
@objc dynamic var isDisplayed = false {
willSet {
if isDisplayed != newValue { willChangeValue(for: \.isDisplayed) }
}
didSet {
if isDisplayed != oldValue { didChangeValue(for: \.isDisplayed) }
}
}
回答2:
Here is a version without using tapGesture.
@State private var isDisplayed = false
Toggle("", isOn: $isDisplayed)
.onReceive([self.isDisplayed].publisher.first()) { (value) in
print("New value is: \(value)")
}
回答3:
Here is a more generic approach you can apply to any Binding
for almost all built in View
s like Pickers, Textfields, Toggle..
extension Binding {
func didSet(execute: @escaping (Value) -> Void) -> Binding {
return Binding(
get: {
return self.wrappedValue
},
set: {
self.wrappedValue = $0
execute($0)
}
)
}
}
And usage is simply;
@State var isOn: Bool = false
Toggle("Toggle Title", isOn: $isOn.didSet { (state) in
print(state)
})
回答4:
The cleanest approach in my opinion is to use a custom binding. With that you have full control when the toggle should actually switch
import SwiftUI
struct ToggleDemo: View {
@State private var isToggled = false
var body: some View {
let binding = Binding(
get: { self.isToggled },
set: {
potentialAsyncFunction($0)
}
)
func potentialAsyncFunction(_ newState: Bool) {
//something async
self.isToggled = newState
}
return Toggle("My state", isOn: binding)
}
}
回答5:
I think it's ok
struct ToggleModel {
var isWifiOpen: Bool = true {
willSet {
print("wifi status will change")
}
}
}
struct ToggleDemo: View {
@State var model = ToggleModel()
var body: some View {
Toggle(isOn: $model.isWifiOpen) {
HStack {
Image(systemName: "wifi")
Text("wifi")
}
}.accentColor(.pink)
.padding()
}
}
回答6:
I found a simpler solution, just use onTapGesture:D
Toggle(isOn: $stateChange) {
Text("...")
}
.onTapGesture {
// Any actions here.
}
回答7:
Based on @Legolas Wang's answer.
When you hide the original label from the toggle you can attach the tapGesture only to the toggle itself
HStack {
Text("...")
Spacer()
Toggle("", isOn: $stateChange)
.labelsHidden()
.onTapGesture {
// Any actions here.
}
}
回答8:
This is how I code:
Toggle("Title", isOn: $isDisplayed)
.onReceive([self.isDisplayed].publisher.first()) { (value) in
//Action code here
}
Updated code (Xcode 12, iOS14):
Toggle("Enabled", isOn: $isDisplayed.didSet { val in
//Action here
})
回答9:
class PWSStore : ObservableObject {
...
var station: PWS
@Published var isDisplayed = true {
willSet {
PWSStore.shared.toggleIsDisplayed(self.station)
}
}
}
struct PWSDetailView : View {
@ObservedObject var station = PWSStore.shared
...
var body: some View {
...
Toggle(isOn: $isDisplayed) { Text("Wetterstation anzeigen") }
...
}
}
Demo here https://youtu.be/N8pL7uTjEFM
回答10:
SwiftUI 2
If you're using SwiftUI 2 / iOS 14 you can use onChange
:
struct ContentView: View {
@State private var isDisplayed = false
var body: some View {
Toggle("", isOn: $isDisplayed)
.onChange(of: isDisplayed) { value in
// action...
print(value)
}
}
}
回答11:
You can try this(it's a workaround):
@State var isChecked: Bool = true
@State var index: Int = 0
Toggle(isOn: self.$isChecked) {
Text("This is a Switch")
if (self.isChecked) {
Text("\(self.toggleAction(state: "Checked", index: index))")
} else {
CustomAlertView()
Text("\(self.toggleAction(state: "Unchecked", index: index))")
}
}
And below it, create a function like this:
func toggleAction(state: String, index: Int) -> String {
print("The switch no. \(index) is \(state)")
return ""
}
回答12:
Here's my approach. I was facing the same issue, but instead decided to wrap UIKit's UISwitch into a new class conforming to UIViewRepresentable.
import SwiftUI
final class UIToggle: UIViewRepresentable {
@Binding var isOn: Bool
var changedAction: (Bool) -> Void
init(isOn: Binding<Bool>, changedAction: @escaping (Bool) -> Void) {
self._isOn = isOn
self.changedAction = changedAction
}
func makeUIView(context: Context) -> UISwitch {
let uiSwitch = UISwitch()
return uiSwitch
}
func updateUIView(_ uiView: UISwitch, context: Context) {
uiView.isOn = isOn
uiView.addTarget(self, action: #selector(switchHasChanged(_:)), for: .valueChanged)
}
@objc func switchHasChanged(_ sender: UISwitch) {
self.isOn = sender.isOn
changedAction(sender.isOn)
}
}
And then its used like this:
struct PWSDetailView : View {
@State var isDisplayed: Bool = false
@ObservedObject var station: PWS
...
var body: some View {
...
UIToggle(isOn: $isDisplayed) { isOn in
//Do something here with the bool if you want
//or use "_ in" instead, e.g.
if isOn != station.isDisplayed {
PWSStore.shared.toggleIsDisplayed(station)
}
}
...
}
}
回答13:
Just in case you don't want to use extra functions, mess the structure - use states and use it wherever you want. I know it's not a 100% answer for the event trigger, however, the state will be saved and used in the most simple way.
struct PWSDetailView : View {
@State private var isToggle1 = false
@State private var isToggle2 = false
var body: some View {
ZStack{
List {
Button(action: {
print("\(self.isToggle1)")
print("\(self.isToggle2)")
}){
Text("Settings")
.padding(10)
}
HStack {
Toggle(isOn: $isToggle1){
Text("Music")
}
}
HStack {
Toggle(isOn: $isToggle1){
Text("Music")
}
}
}
}
}
}
回答14:
Available for XCode 12
import SwiftUI
struct ToggleView: View {
@State var isActive: Bool = false
var body: some View {
Toggle(isOn: $isActive) { Text(isActive ? "Active" : "InActive") }
.padding()
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
}
}
来源:https://stackoverflow.com/questions/56996272/how-can-i-trigger-an-action-when-a-swiftui-toggle-is-toggled