当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个回答

2

这是我的方法。我也遇到了同样的问题,但我决定将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的做法,但这可能是品味的问题。我更喜欢在View声明内部使用闭包。 - paescebu
非常好。避免了任何时间问题,使视图更加“清晰”,并保留了所有的UISwitch功能。 - user4860907
谢谢 @Tall Dane!但是我觉得现在我会选择使用SwiftUI 2中提供的onChanged修饰符 :)。 - paescebu

1

适用于 Xcode 13.4 版本。

import SwiftUI

struct ToggleBootCamp: View {
    @State var isOn: Bool = true
    @State var status: String = "ON"
    
    var body: some View {
        NavigationView {
            VStack {
                Toggle("Switch", isOn: $isOn)
                    .onChange(of: isOn, perform: {
                        _isOn in
                        // Your code here...
                        status = _isOn ? "ON" : "OFF"
                    })
                Spacer()
            }.padding()
            .navigationTitle("Toggle switch is: \(status)")
        }
    }
}

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

谢谢,Rob!你的第一行代码已经完成了任务。@objc class var automaticallyNotifiesObserversOfIsDisplayed: Bool { return false } 我不完全理解背后的机制(苹果文档也没有帮助太多),但似乎这行代码只是静默了一些通知。当创建 PWS 类的实例或设置 isDisplayed 的值(但未更改)时,不会发送任何通知。但是当 SwiftUI 视图实际更改 isDisplayed 的值时,仍然会有通知。对于我的应用程序,这正是我需要的行为。 - Brezentrager

1
你可以尝试这个方法(它是一个解决办法):

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

1
这是我编写的一个方便的扩展,可以在切换按钮被按下时触发回调。与其他许多解决方案不同的是,这个扩展只会在切换按钮切换时触发,而不会在初始化时触发,对于我的使用情况来说非常重要。这类似于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) }))
    }
}

1
我使用了`did set`来监控切换值的变化,
并且
使用了通用子视图。

enter image description here

import SwiftUI
    
    
    
    class ViewModel : ObservableObject{
        
        @Published var isDarkMode = false{
            
            didSet{
                
                print("isDarkMode \(isDarkMode)")
            }
        }
        
        @Published var isShareMode = false{
            
            didSet{
                
                print("isShareMode \(isShareMode)")
            }
        }
        
        @Published var isRubberMode = false{
            
            didSet{
                
                print("isRubberMode \(isRubberMode)")
            }
        }
        
        
    }
    
    
    
    struct ToggleDemo: View {
        
        @StateObject var vm = ViewModel()
        
        
        var body: some View {
            
            VStack{
                
                VStack{
    
                    ToggleSubView(mode: $vm.isDarkMode, pictureName: "moon.circle.fill")
                  ToggleSubView(mode: $vm.isShareMode, pictureName: "square.and.arrow.up.on.square")
                   ToggleSubView(mode: $vm.isRubberMode, pictureName: "eraser.fill")
                }
            
            
                Spacer()
                
            
            }
            
            .background(vm.isDarkMode ? Color.black : Color.white)
        }
    }
    
    struct ToggleDemo_Previews: PreviewProvider {
        static var previews: some View {
            ToggleDemo()
        }
    }
    
    
    struct ToggleSubView : View{
        
        @Binding var mode : Bool
        
        var pictureName : String
        
        var body : some View{
            
            
            Toggle(isOn: $mode) {
                Image(systemName: pictureName)
            }
            .padding(.horizontal)
            .frame(height:44)
            .background(Color(.systemGroupedBackground))
            .cornerRadius(10)
        
            .padding()
            
        }
    }

0
在顶部添加一个透明的矩形,然后:
ZStack{
            
            Toggle(isOn: self.$isSelected, label: {})
            Rectangle().fill(Color.white.opacity(0.1))
            
        }
        .contentShape(Rectangle())
        .onTapGesture(perform: {
            
            self.isSelected.toggle()
            
        })

0

低于iOS 14:

使用Equatable检查的绑定扩展

public extension Binding {
    func onChange(_ handler: @escaping (Value) -> Void) -> Binding<Value> where Value: Equatable {
        Binding(
            get: { self.wrappedValue },
            set: { newValue in
                if self.wrappedValue != newValue { // equal check
                    self.wrappedValue = newValue
                    handler(newValue)
                }
            }
        )
    }
}

使用方法:

Toggle(isOn: $pin.onChange(pinChanged(_:))) {
    Text("Equatable Value")
}

func pinChanged(_ pin: Bool) {

}

0

如果您不想使用额外的函数,破坏结构 - 使用状态并在任何地方使用它。我知道这不是事件触发器的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")
                   }
                 }
        }
    }
}
}

-1

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

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