繁体   English   中英

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

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

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

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

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

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

iOS13+

这是一种更通用的方法,您可以将其应用于几乎所有内置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)
            }
        )
    }
}

用法很简单;

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

我认为最干净的方法是使用自定义绑定。 有了它,您就可以完全控制何时应该实际切换切换

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

我觉得没问题

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

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

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

这就是我编码的方式:

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

基于@Legolas Wang 的回答。

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

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

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

在这里演示https://youtu.be/N8pL7uTjEFM

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

首先,你真的知道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) }
    }
}

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

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

这是我编写的一个方便的扩展,用于在按下切换时触发回调。 与许多其他解决方案不同,这确实只会在切换切换时触发,而不是在初始化时触发,这对我的用例很重要。 这模仿了类似的 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) }))
    }
}

以防万一你不想使用额外的功能,弄乱结构 - 使用状态并在任何你想要的地方使用它。 我知道这不是事件触发器的 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")
                   }
                 }
        }
    }
}
}

这可能会切换它

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

低于 iOS 14:

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

用法:

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

func pinChanged(_ pin: Bool) {

}

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

在顶部添加一个透明的矩形,然后:

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

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