简体   繁体   English

切换 swiftUI toggle() 时如何触发操作?

[英]How can I trigger an action when a swiftUI toggle() is toggled?

In my SwiftUI view I have to trigger an action when a Toggle() changes its state.在我的 SwiftUI 视图中,当 Toggle() 更改其 state 时,我必须触发一个动作。 The toggle itself only takes a Binding.切换本身只需要一个绑定。 I therefore tried to trigger the action in the didSet of the @State variable.因此,我尝试在 @State 变量的 didSet 中触发操作。 But the didSet never gets called.但是 didSet 永远不会被调用。

Is there any (other) way to trigger an action?是否有任何(其他)方式来触发动作? Or any way to observe the value change of a @State variable?或者有什么方法可以观察@State 变量的值变化?

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.期望的行为是当 Toggle() 更改其 state 时触发操作“PWSStore.shared.toggleIsDisplayed(station)”。

SwiftUI 2 SwiftUI 2

If you're using SwiftUI 2 / iOS 14 you can use onChange :如果您使用的是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)
            }
    }
}

Here is a version without using tapGesture.这是一个没有使用 tapGesture 的版本。

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

iOS13+ iOS13+

Here is a more generic approach you can apply to any Binding for almost all built in View s like Pickers, Textfields, Toggle..这是一种更通用的方法,您可以将其应用于几乎所有内置View的任何Binding ,例如 Pickers、Textfields、Toggle..

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

And usage is simply;用法很简单;

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

iOS14+ iOS14+

@State private var isOn = false

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

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

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

I found a simpler solution, just use onTapGesture:D我找到了一个更简单的解决方案,只需使用 onTapGesture:D

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

This is how I code:这就是我编码的方式:

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

Updated code (Xcode 12, iOS14):更新代码(Xcode 12、iOS14):

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

Based on @Legolas Wang's answer.基于@Legolas Wang 的回答。

When you hide the original label from the toggle you can attach the tapGesture only to the toggle itself当您从切换隐藏原始标签时,您可以将 tapGesture 仅附加到切换本身

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

The .init is the constructor of Binding .initBinding的构造函数

@State var isDisplayed: Bool

Toggle("some text", isOn: .init(
    get: { isDisplayed },
    set: {
        isDisplayed = $0
        print("changed")
    }
))
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在这里演示https://youtu.be/N8pL7uTjEFM

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.我遇到了同样的问题,但决定将 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)
    }
}

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

First, do you actually know that the extra KVO notifications for station.isDisplayed are a problem?首先,你真的知道station.isDisplayed的额外 KVO 通知是个问题吗? 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.如果您遇到性能问题并且您已经确定它们是由于过多的station.isDisplayed KVO 通知,那么接下来要尝试的是消除不需要的 KVO 通知。 You do that by switching to manual KVO notifications.您可以通过切换到手动 KVO 通知来做到这一点。

Add this method to station 's class definition:将此方法添加到station的类定义中:

@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:并使用 Swift 的willSetdidSet观察者手动通知 KVO 观察者,但前提是值发生变化:

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

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

Here is a handy extension I wrote to fire a callback whenever the toggle is pressed.这是我编写的一个方便的扩展,用于在按下切换时触发回调。 Unlike a lot of the other solutions this truly only will fire when the toggle is switched and not on init which for my use case was important.与许多其他解决方案不同,这确实只会在切换切换时触发,而不是在初始化时触发,这对我的用例很重要。 This mimics similar SwiftUI initializers such as TextField for onCommit.这模仿了类似的 SwiftUI 初始化程序,例如用于 onCommit 的 TextField。

USAGE:用法:

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

EXTENSIONS:扩展:

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

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.我知道这不是事件触发器的 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")
                   }
                 }
        }
    }
}
}

This could toggle it maybe这可能会切换它

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

lower than iOS 14:低于 iOS 14:

extension for Binding with Equatable check with Equatable check进行绑定的扩展

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

Usage:用法:

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

func pinChanged(_ pin: Bool) {

}

Available for Xcode 13.4适用于 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)")
        }
    }
}

Add a transparent Rectangle on top, then:在顶部添加一个透明的矩形,然后:

ZStack{
            
            Toggle(isOn: self.$isSelected, label: {})
            Rectangle().fill(Color.white.opacity(0.1))
            
        }
        .contentShape(Rectangle())
        .onTapGesture(perform: {
            
            self.isSelected.toggle()
            
        })

Available for XCode 12适用于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))
    }
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM