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

Posted

技术标签:

【中文标题】切换 swiftUI toggle() 时如何触发操作?【英文标题】:How can I trigger an action when a swiftUI toggle() is toggled? 【发布时间】: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' 的值会用旧值“更新”(--> 操作不必被触发)。我还没有找到区分这两种情况的方法。因此,我尝试直接在视图中触发操作。 【参考方案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)

【讨论】:

以上是关于切换 swiftUI toggle() 时如何触发操作?的主要内容,如果未能解决你的问题,请参考以下文章

如何在 SwiftUI Toggle 中使用自定义图像

UserDefaults 在 SwiftUI 中与 Toggle 绑定

SwiftUI:Toggle详解

已发布值上的 SwiftUI toggle() 函数停止使用 Swift 5.2 触发 didSet

如何在 SwiftUI 中触发状态更改/重绘切换更改

如何通过切换 Swiftui 传递函数