当SwiftUI的toggle()被切换时,我该如何触发一个动作?

88

在我的SwiftUI视图中,当Toggle()改变状态时,我必须触发一个动作。Toggle本身只需要一个Binding。

因此,我尝试在@State变量的didSet中触发动作。但是didSet从未被调用。

是否有任何其他方法可以触发操作?或者任何观察@State变量值更改的方法?

我的代码如下:

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

期望的行为是当Toggle()改变状态时,触发动作"PWSStore.shared.toggleIsDisplayed(station)"。


由于我不知道您的应用程序背后发生的所有事情,因此这可能不是一个解决方案,但是由于“station”是一个“BindableObject”,您是否可以将Toggle(isOn: $isDisplayed)替换为Toggle(isOn: $station.isDisplayed),然后在您的“PWS”类中的“didSet”上更新“isDisplayed”以更新“PWSStore.shared”? - graycampbell
1
@graycampbell 理论上这是可行的(这也是我之前尝试过的)。不幸的是,我的 PWS 类(它是一个 Core Data 实体)的 didChangeValue(forKey:) 函数经常被调用。在某些情况下(比如按下切换按钮),'isDisplayed' 的值确实发生了变化(--> 应该触发操作)。在其他情况下,'isDisplayed' 的值会被旧值“更新”(--> 不需要触发操作)。我还没有找到区分这两种情况的方法。因此,我尝试直接在视图中触发操作。 - Brezentrager
20个回答

158

iOS 17+(测试版)

在 iOS 17 中,使用只有一个参数的 onChange 已经被弃用 - 相反,我们应该:

改用具有两个或零个参数的 onChange 操作闭包。

struct ContentView: View {
    @State private var isDisplayed = false
    
    var body: some View {
        Toggle("", isOn: $isDisplayed)
            .onChange(of: isDisplayed) {
                print("Action")
            }
            .onChange(of: isDisplayed) { oldValue, newValue in
                // action...
                print(oldValue, newValue)
            }
    }
}

我们还可以设置initial参数来指定视图初始显示时是否应运行该操作。
struct ContentView: View {
    @State private var isDisplayed = false
    
    var body: some View {
        Toggle("", isOn: $isDisplayed)
            .onChange(of: isDisplayed, initial: true) {
                print("Action")
            }
    }
}

iOS 14+

如果你使用的是iOS 14及更高版本,你可以使用onChange

struct ContentView: View {
    @State private var isDisplayed = false
    
    var body: some View {
        Toggle("", isOn: $isDisplayed)
            .onChange(of: isDisplayed) { value in
                // action...
                print(value)
            }
    }
}

7
这应该是最佳答案。 - user482594
4
嗨,当使用此方法时,如果切换值保存在数据库中,则会调用两次提取操作。一次是在 init() {} 中,另一次是在我们从视图模型更改 isDisplayed 布尔值时,onChange 再次被激活。是否有任何方法可以减轻这种情况? - mgh
1
它不起作用了.. 我已经尝试过了。 - Mehul Thakkar
@mgh 我把Toggle放在Button里面,并使用Button的动作来防止onChange问题。 - iljer

45

这是一个不使用tapGesture的版本。

@State private var isDisplayed = false
Toggle("", isOn: $isDisplayed)
   .onReceive([self.isDisplayed].publisher.first()) { (value) in
        print("New value is: \(value)")           
   }

1
这看起来很棒。不需要额外的绑定。 - Marc T.
15
很不错!你还可以使用.onReceive(Just(isDisplayed)) { value in ... } - Jacob
我想知道为什么你在方括号中放置了self.isDisplayed并附加了.publisher.first()。如果是ObservedObject而不是State,您将编写nameOfObject.$isDisplayed。至少在我的情况下似乎可以工作。 - a learner has no name
2
我相信这段代码会在任何情况下都会被触发,只要状态变量被改变了。 - biomiker
干净的解决方案,但遵循Jacob的建议会更好。谢谢! - Hamlet Minjares

44

iOS13+

以下是一种更通用的方法,适用于几乎所有内置的View(如Pickers、Textfields、Toggle等)的任何Binding

extension Binding {
    func didSet(execute: @escaping (Value) -> Void) -> Binding {
        return Binding(
            get: { self.wrappedValue },
            set: {
                self.wrappedValue = $0
                execute($0)
            }
        )
    }
}

使用方法非常简单;

@State var isOn: Bool = false
Toggle("Title", isOn: $isOn.didSet { (state) in
   print(state)
})
@State private var isOn = false

var body: some View {
    Toggle("Title", isOn: $isOn)
        .onChange(of: isOn) { _isOn in
            /// use _isOn here..
        }
}

7
这是最干净的实现方式。对我来说,无论视图中的其他状态变量何时发生改变,onReceive都会被触发。使用这种解决方案,当附加的状态变量更改时才会运行操作。 - BitByteDog

16

在我看来,最干净的方法是使用自定义绑定。这样,您可以完全控制切换何时实际发生。

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

1
如果我已经有了ZStacks和VStacks,尝试将它们放在内部/外部,会出现许多错误。 - J A S K I E R
2
这是这个问题的正确解决方案。没有必要使用KVO通知来深入挖掘。 - Marc T.

12

我认为这没问题。

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

这是我最喜欢的答案,因为它不依赖于新的专用视图拥有的@State变量,因此它是一个真正的视图无状态实现,可以将切换值发布到视图模型中。 - Yizhar

6
.initBinding 的构造函数。
@State var isDisplayed: Bool

Toggle("some text", isOn: .init(
    get: { isDisplayed },
    set: {
        isDisplayed = $0
        print("changed")
    }
))

6

这是我的编码方式:

Toggle("Title", isOn: $isDisplayed)
.onReceive([self.isDisplayed].publisher.first()) { (value) in
    //Action code here
}

更新的代码(Xcode 12,iOS14):

Toggle("Enabled", isOn: $isDisplayed.didSet { val in
        //Action here        
})

这非常简洁明了。在我看来,这应该是正确的答案。 - Sébastien Stormacq
我无法让你的第二个版本工作,但第一个版本在我经过长时间搜索后确实解决了我的问题。第二个版本在我的编译中失败了。谢谢! - Manngo
谢谢!@Manngo 我刚刚测试了一下,它在我的Xcode12和iOS14上运行良好。你的Xcode版本是多少?有没有编译错误信息?我相信第二个更好 :) - z33
1
@z33 我正在使用 SCode 12,并且针对 MacOS 10.15 Catalina 进行开发。我没有直接收到错误信息。编译器花了很长时间才决定无法继续进行。 - Manngo
也同意这应该是答案。 - Zack Shapiro
.onReceive() 是唯一对我有效的东西。我仍然会收到一个 _CFRunLoopError_RunCalledWithInvalidMode 的消息,但至少闭包会执行。 - Jason Elwood

5
我找到了一个更简单的解决方案,只需使用onTapGesture:D
Toggle(isOn: $stateChange) {
  Text("...")
}
.onTapGesture {
  // Any actions here.
}

11
即使点击文本,它也会被触发。我认为这并不是一个好的解决方案。 - Jonas Deichelmann

4

基于 @Legolas Wang 的回答。

当你从开关(toggle)中隐藏原始标签,你可以将tapGesture只附加到开关本身。

HStack {
    Text("...")
    Spacer()
    Toggle("", isOn: $stateChange)
        .labelsHidden()
        .onTapGesture {
            // Any actions here.
        }
     }

最佳解决方案在这里!FYI - onTap 在 isOn 状态实际更改之前被调用,因此我还必须在 onTap 操作中添加0.1秒的延迟,以便 isOn 状态有时间在操作被调用之前切换。谢谢! - nicksarno

3
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") }
        ...
    }   
}

此处演示 https://youtu.be/N8pL7uTjEFM


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接