SwiftUI Toggle如何区分通过UI操作更改的值与通过编程更改的值

huangapple go评论110阅读模式
英文:

SwiftUI Toggle how to distinguish changing value by UI action vs changing programatically

问题

如果我有一个切换按钮,它通过外部异步加载和用户输入更新其状态,我该如何区分这两者?例如,执行特殊操作以响应用户操作。

Group {
    Toggle(isOn: $on) {
        EmptyView()
    }
}
.onChange(of: on) { newValue in
    // 判断 "on" 是否是由用户操作还是在 onAppear 异步更新时改变的?
}
.onAppear {
    // 在这里进行异步更新 on
}

PS:这主要用于 macOS,因为在 macOS 上,Toggle 上的 tapGesture 不起作用。

英文:

If i have a toggle, which updates its state from external async load but also by user intput, how can i differentiate those two? eg. to perform a special action on user action

    Group {
        Toggle(isOn: $on) {
            EmptyView()
        }
    }
    .onChange(of: on) { newValue in
        was "on" changed by user or onAppear async update?
    }
    .onAppear {
        async update on
    }

PS: this is mostly for macOS, and there the tapGesture on Toggle doesn't work

答案1

得分: 4

SwiftUI没有提供一种内置的方法来区分状态更改是由用户交互触发还是由程序性更改(例如异步更新)触发的。但是,您可以通过创建一个单独的变量来充当程序性更改的标志来使用一个变通方法。

在这里是一个代码示例:

@State private var on = false
@State private var isProgrammaticChange = false

Group {
    Toggle(isOn: $on) {
        EmptyView()
    }
}
.onChange(of: on) { newValue in
    if isProgrammaticChange {
        // 更改是通过程序性方式进行的。
        // 什么也不做或根据需要处理。
        isProgrammaticChange = false // 重置标志
    } else {
        // 更改是由用户进行的。
        // 在这里执行特殊操作。
    }
}
.onAppear {
    DispatchQueue.main.asyncAfter(deadline: .now() + 1) { // 模拟异步加载
        isProgrammaticChange = true
        on.toggle()
    }
}

在这个代码中,isProgrammaticChange 是一个标志,用于标记何时以程序性方式进行更改。在我们以程序性方式更改 on 状态之前,我们将 isProgrammaticChange 设置为 true。在 .onChange(of: on) 修饰符中,我们可以检查 isProgrammaticChange 标志,以查看更改是通过程序性方式还是由用户进行的。

请注意,DispatchQueue.main.asyncAfter(deadline: .now() + 1) 用于模拟 .onAppear 修饰符中的异步更新。您应该将其替换为您自己的异步代码,并确保在更改 on 之前立即将 isProgrammaticChange 设置为 true

最后,请记住在处理程序性更改后将 isProgrammaticChange 标志重置为 false,以确保后续由用户触发的更改能够正确处理。

如果您想要使用 Combine 框架的另一种更干净的解决方案,请参考下面的"Other Solution (Combine)" 部分。

英文:

SwiftUI doesn't provide a built-in way to distinguish whether a state change was triggered by a user interaction or a programmatic change, such as an asynchronous update. However, you can use a workaround by creating a separate variable that acts as a flag for programmatic changes.

Here is a code example:

@State private var on = false
@State private var isProgrammaticChange = false

Group {
    Toggle(isOn: $on) {
        EmptyView()
    }
}
.onChange(of: on) { newValue in
    if isProgrammaticChange {
        // The change was made programmatically.
        // Do nothing or handle as needed.
        isProgrammaticChange = false // Reset the flag
    } else {
        // The change was made by the user.
        // Perform the special action here.
    }
}
.onAppear {
    DispatchQueue.main.asyncAfter(deadline: .now() + 1) { // Simulate async load
        isProgrammaticChange = true
        on.toggle()
    }
}

In this code, isProgrammaticChange is a flag to mark when a change is made programmatically. Before we change the on state programmatically, we set isProgrammaticChange to true. In the .onChange(of: on) modifier, we can check the isProgrammaticChange flag to see whether the change was made programmatically or by the user.

Note that DispatchQueue.main.asyncAfter(deadline: .now() + 1) is used to simulate an asynchronous update in the .onAppear modifier. You would replace this with your own asynchronous code, making sure to set isProgrammaticChange to true right before making the change to on.

Finally, remember to reset the isProgrammaticChange flag back to false after handling the programmatic change to ensure that subsequent user-triggered changes are handled correctly.

Other Solution (Combine)

Or you can use another solution using Combine that is cleaner than using a flag. You could create a PassthroughSubject that emits events whenever the toggle is switched programmatically.

import Combine
import SwiftUI

class ViewModel: ObservableObject {
    @Published var isOn: Bool = false
    let programmaticallyToggled = PassthroughSubject<Void, Never>()
}

struct ContentView: View {
    @StateObject private var viewModel = ViewModel()

    var body: some View {
        Group {
            Toggle(isOn: $viewModel.isOn) {
                EmptyView()
            }
        }
        .onChange(of: viewModel.isOn) { newValue in
            if !viewModel.programmaticallyToggled.hasAnyObservers {
                // Change was made by user, do your special action here.
            }
        }
        .onAppear {
            DispatchQueue.main.asyncAfter(deadline: .now() + 1) { // Simulate async load
                viewModel.programmaticallyToggled.send()
                viewModel.isOn.toggle()
            }
        }
        .onReceive(viewModel.programmaticallyToggled) { _ in
            // Do nothing, but this is necessary to ensure that hasAnyObservers returns true when programmatic changes occur.
        }
    }
}

In this example, we create a ViewModel class that contains the isOn state as well as a PassthroughSubject named programmaticallyToggled. In the onChange modifier of the Toggle, we check whether the programmaticallyToggled subject has any subscribers. If it doesn't, then the change was made by the user.

The onReceive modifier is necessary because a PassthroughSubject will only have subscribers while it is in the process of sending an event. Adding an onReceive modifier to the view ensures that hasAnyObservers will return true whenever programmaticallyToggled sends an event.

This method avoids the need for a separate flag variable, but it does require understanding of Combine, which is Apple's reactive programming framework. If you're not familiar with Combine, the first solution with the flag variable might be easier to understand.

答案2

得分: 1

如果您希望在使用用户操作时产生副作用,可以使用自定义包装器 Binding

struct ContentView: View {
    @State private var on: Bool = false
    
    var userManagedOn: Binding<Bool> {
        .init {
            return on
        } set: { newValue in
            print("副作用")
            on = newValue
        }
    }
    
    var body: some View {
        VStack {
            Group {
                Toggle(isOn: userManagedOn) {
                    EmptyView()
                }
            }
        }
        .padding()
        .onAppear {
            Task { @MainActor in
                try? await Task.sleep(nanoseconds: NSEC_PER_SEC)
                on.toggle()
            }
        }
    }
}
英文:

If you want a side effect for use the user actions, you can use a custom wrapper Binding:

struct ContentView: View {
    @State private var on: Bool = false
    
    var userManagedOn: Binding&lt;Bool&gt; {
        .init {
            return on
        } set: { newValue in
            print(&quot;Side effect&quot;)
            on = newValue
        }
    }
    
    var body: some View {
        VStack {
            Group {
                Toggle(isOn: userManagedOn) {
                    EmptyView()
                }
            }
        }
        .padding()
        .onAppear {
            Task { @MainActor in
                try? await Task.sleep(nanoseconds: NSEC_PER_SEC)
                on.toggle()
            }
        }
    }
}

huangapple
  • 本文由 发表于 2023年6月2日 02:21:26
  • 转载请务必保留本文链接:https://go.coder-hub.com/76384685.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定