英文:
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<Bool> {
.init {
return on
} set: { newValue in
print("Side effect")
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()
}
}
}
}
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论