【问题标题】:How can I trigger an action when a swiftUI toggle() is toggled?切换 swiftUI toggle() 时如何触发操作?
【发布时间】:2019-11-21 14:09:55
【问题描述】:

在我的 SwiftUI 视图中,我必须在 Toggle() 更改其状态时触发一个动作。切换本身只需要一个绑定。 因此,我尝试在 @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)”。

【问题讨论】:

  • 因为我不知道你的应用程序幕后发生的一切,这可能不是一个解决方案,但由于stationBindableObject,你不能只替换@987654324 @ 和 Toggle(isOn: $station.isDisplayed) 然后更新 didSet 中的 PWSStore.shared isDisplayed PWS 类?
  • @graycampbell 理论上可行(这是我之前尝试过的)。不幸的是,我的 PWS 类(它是一个核心日期实体)的 didChangeValue(forKey:) 函数经常被调用。在某些情况下(例如按下切换按钮),“isDisplayed”的值确实发生了变化(--> 应该触发该操作)。在其他情况下,'isDisplayed' 的值会用旧值“更新”(--> 操作不必被触发)。我还没有找到区分这两种情况的方法。因此,我尝试直接在视图中触发操作。

标签: swift toggle swiftui


【解决方案1】:

首先,你真的知道station.isDisplayed 的额外KVO 通知是个问题吗?您是否遇到性能问题?如果没有,请不要担心。

如果您遇到性能问题并且您已经确定它们是由于过多的station.isDisplayed KVO 通知引起的,那么接下来要尝试的是消除不需要的 KVO 通知。您可以通过切换到手动 KVO 通知来做到这一点。

将此方法添加到station的类定义中:

@objc class var automaticallyNotifiesObserversOfIsDisplayed: Bool { return false }

并使用 Swift 的 willSetdidSet 观察者手动通知 KVO 观察者,但前提是值发生变化:

@objc dynamic var isDisplayed = false {
    willSet {
        if isDisplayed != newValue { willChangeValue(for: \.isDisplayed) }
    }
    didSet {
        if isDisplayed != oldValue { didChangeValue(for: \.isDisplayed) }
    }
}

【讨论】:

  • 谢谢,罗伯!您的第一行代码已经完成了这项工作。 @objc class var automaticallyNotifiesObserversOfIsDisplayed: Bool { return false } 我不完全理解后台的机制(Apple 文档并没有太大帮助),但似乎这条线只会使一些通知静音。创建 PWS 类的实例或设置 isDisplayed 的值(但未更改)时,不会发送通知。但是当一个 SwiftUI 视图实际上改变了isDisplayed 的值时,仍然有一个通知。对于我的应用,这正是我需要的行为。
【解决方案2】:

你可以试试这个(这是一种解决方法):

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

在它下面,创建一个这样的函数:

func toggleAction(state: String, index: Int) -> String {
    print("The switch no. \(index) is \(state)")
    return ""
}

【讨论】:

    【解决方案3】:

    我觉得还可以

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

    【讨论】:

      【解决方案4】:
      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

      【讨论】:

        【解决方案5】:

        我找到了一个更简单的解决方案,只需使用 onTapGesture:D

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

        【讨论】:

        • 它也会触发,即使在文本被点击时也是如此。我认为这不是一个好的解决方案。
        【解决方案6】:

        这是我的方法。我遇到了同样的问题,但决定将 UIKit 的 UISwitch 包装到一个符合 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)
            }
        }
        

        然后它是这样使用的:

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

        【讨论】:

        • 对于@Philipp Serflings 方法:附加 TapGestureRecognizer 对我来说不是一个选项,因为当您执行“滑动”来切换切换时它不会触发。而且我不希望失去 UISwitch 的功能。并且使用绑定作为代理就可以了,但我不觉得这是一种 SwiftUI 的方式,但这可能是一个品味问题。我更喜欢视图声明本身中的闭包
        • 非常好。避免任何时间问题,并提供“干净”的视图并维护所有 UISwitch 功能。
        • 谢谢@Tall Dane!但我想现在我会使用 SwiftUI 2 附带的 onChanged 修饰符:)。
        【解决方案7】:

        基于@Legolas Wang 的回答。

        当您从开关中隐藏原始标签时,您可以仅将 tapGesture 附加到开关本身

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

        【讨论】:

        • 这里最好的解决方案!仅供参考 - 在 isOn 状态实际更改之前调用 onTap,因此我还必须为 onTap 操作添加 0.1 秒的延迟,以便在调用操作之前,isOn 状态有时间切换。谢谢!
        【解决方案8】:

        在我看来,最简洁的方法是使用自定义绑定。 有了它,您就可以完全控制切换开关的实际切换时间

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

        【讨论】:

        • 如果我已经有一个 ZStacks 和 VStacks 会出现很多错误...试图放在里面/外面 - 只有错误
        • 这是解决这个问题的正确方法。没有理由去挖掘 KVO 通知。
        【解决方案9】:

        这是一个没有使用 tapGesture 的版本。

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

        【讨论】:

        • 这看起来很棒。不需要额外的绑定。
        • 这很好!你也可以.onReceive(Just(isDisplayed)) { value in … }
        • 我想知道你为什么将 self.isDisplayed 放在方括号中并附加 .publisher.first()。如果是 ObservedObject 而不是 State,您将改写 nameOfObject.$isDisplayed。至少这对我来说似乎有效。
        • 我相信无论出于何种原因更改状态变量时都会触发此代码?
        【解决方案10】:

        以防万一您不想使用额外的功能,弄乱结构 - 使用状态并在任何您想要的地方使用它。我知道这不是事件触发器的 100% 答案,但是,状态将被保存并以最简单的方式使用。

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

        【讨论】:

          【解决方案11】:

          适用于 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))
              }
          }
          

          【讨论】:

            【解决方案12】:

            这就是我的编码方式:

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

            【讨论】:

            • 这个超级干净简洁。恕我直言,这应该是正确答案
            • 我无法让你的第二个工作,但经过大量搜索后,第一个版本确实解决了我的问题。第二个版本不会为我编译。谢谢
            • 谢谢! @Manngo 我刚才测试了它。它适用于我的 Xcode12 和 iOS14。你的 Xcode 版本是多少?有没有编译错误信息?我相信第二个更好:)
            • @z33 我使用的是 SCode 12,但目标是 MacOS 10.15 Catalina。我没有直接收到错误消息。编译器需要很长时间才能决定它不能继续。
            • 也同意这应该是答案
            【解决方案13】:

            iOS13+

            这是一种更通用的方法,您可以将其应用于几乎所有内置的 Views,例如 Pickers、Textfields、Toggle..

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

            iOS14+

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

            【讨论】:

            • 这是最干净的实现。对我来说,当视图中的任何其他状态变量发生变化时,就会触发 onReceive。使用此解决方案,该操作仅在附加状态变量更改时运行。
            【解决方案14】:

            SwiftUI 2

            如果您使用的是 SwiftUI 2 / 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)
                        }
                }
            }
            

            【讨论】:

            • 这应该是最佳答案。
            • 您好,使用此方法时,如果toggle值保存在数据库中,会调用两次fetch操作。一旦在 init() {} 处,当我们从视图模型更改 isDisplayed 布尔值时,onChange 再次被激活。有什么办法可以缓解吗?
            【解决方案15】:

            这是我编写的一个方便的扩展,用于在按下切换按钮时触发回调。与许多其他解决方案不同,这确实只会在切换切换时触发,而不是在初始化时触发,这对我的用例很重要。这模仿了类似的 SwiftUI 初始化程序,例如用于 onCommit 的 TextField。

            用法:

            Toggle("My Toggle", isOn: $isOn, onToggled: { value in
                print(value)
            })
            

            扩展:

            extension Binding {
                func didSet(execute: @escaping (Value) -> Void) -> Binding {
                    Binding(
                        get: { self.wrappedValue },
                        set: {
                            self.wrappedValue = $0
                            execute($0)
                        }
                    )
                }
            }
            
            extension Toggle where Label == Text {
            
                /// Creates a toggle that generates its label from a localized string key.
                ///
                /// This initializer creates a ``Text`` view on your behalf, and treats the
                /// localized key similar to ``Text/init(_:tableName:bundle:comment:)``. See
                /// `Text` for more information about localizing strings.
                ///
                /// To initialize a toggle with a string variable, use
                /// ``Toggle/init(_:isOn:)-2qurm`` instead.
                ///
                /// - Parameters:
                ///   - titleKey: The key for the toggle's localized title, that describes
                ///     the purpose of the toggle.
                ///   - isOn: A binding to a property that indicates whether the toggle is
                ///    on or off.
                ///   - onToggled: A closure that is called whenver the toggle is switched.
                ///    Will not be called on init.
                public init(_ titleKey: LocalizedStringKey, isOn: Binding<Bool>, onToggled: @escaping (Bool) -> Void) {
                    self.init(titleKey, isOn: isOn.didSet(execute: { value in onToggled(value) }))
                }
            
                /// Creates a toggle that generates its label from a string.
                ///
                /// This initializer creates a ``Text`` view on your behalf, and treats the
                /// title similar to ``Text/init(_:)-9d1g4``. See `Text` for more
                /// information about localizing strings.
                ///
                /// To initialize a toggle with a localized string key, use
                /// ``Toggle/init(_:isOn:)-8qx3l`` instead.
                ///
                /// - Parameters:
                ///   - title: A string that describes the purpose of the toggle.
                ///   - isOn: A binding to a property that indicates whether the toggle is
                ///    on or off.
                ///   - onToggled: A closure that is called whenver the toggle is switched.
                ///    Will not be called on init.
                public init<S>(_ title: S, isOn: Binding<Bool>, onToggled: @escaping (Bool) -> Void) where S: StringProtocol {
                    self.init(title, isOn: isOn.didSet(execute: { value in onToggled(value) }))
                }
            }
            

            【讨论】:

              【解决方案16】:

              .initBinding

              的构造函数
              @State var isDisplayed: Bool
              
              Toggle("some text", isOn: .init(
                  get: { isDisplayed },
                  set: {
                      isDisplayed = $0
                      print("changed")
                  }
              ))
              

              【讨论】:

                【解决方案17】:

                这可能会切换它

                @Published private(set) var data: [Book] = []
                
                func isBookmarked(article: Book) {
                    guard let index = data.firstIndex(where: { $0.id == book.id }) else {
                        return
                    }
                    if(book.bookmarked != nil) {
                        data[index].bookmarked?.toggle()
                        print("Bookmark added!")
                    }
                    else {
                        data[index].bookmarked = true
                        print("Bookmark added!")
                    }
                }
                
                func deleteBookmark(offset: IndexSet) {
                    data.remove(atOffsets: offset)
                }
                

                【讨论】:

                  猜你喜欢
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  • 2021-10-24
                  • 1970-01-01
                  • 1970-01-01
                  相关资源
                  最近更新 更多