更新:2022-09-28
Xcode 14.1 Beta 3(终于)修复了“不允许在视图更新中发布更改,这将导致未定义的行为”错误。
详情请见:https://www.donnywals.com/xcode-14-publishing-changes-from-within-view-updates-is-not-allowed-this-will-cause-undefined-behavior/
完全透明地说——我不确定为什么会发生这种情况,但这是我找到的两个似乎可行的解决方案。
![错误信息](https://istack.dev59.com/vX0dK.webp)
![错误示例](https://istack.dev59.com/S18BG.gif)
代码示例
@main
struct MyApp: App {
@StateObject private var vm = ViewModel()
var body: some Scene {
WindowGroup {
ViewOne()
.environmentObject(vm)
}
}
}
struct ViewOne: View {
@EnvironmentObject private var vm: ViewModel
var body: some View {
Button {
vm.isPresented.toggle()
} label: {
Text("Open sheet")
}
.sheet(isPresented: $vm.isPresented) {
SheetView()
}
}
}
struct SheetView: View {
@EnvironmentObject private var vm: ViewModel
var body: some View {
Button {
vm.isPresented.toggle()
} label: {
Text("Close sheet")
}
}
}
class ViewModel: ObservableObject {
@Published var isPresented: Bool = false
}
解决方案 1
注意: 经过我的测试和下面的例子,我仍然会看到错误。但如果我的应用更复杂/嵌套,则错误会消失。
在执行初始切换的按钮上添加.buttonStyle()
。
因此,在ContentView
中的Button() {}
中加入.buttonStyle(.plain)
,它将删除紫色错误:
struct ViewOne: View {
@EnvironmentObject private var vm: ViewModel
var body: some View {
Button {
vm.isPresented.toggle()
} label: {
Text("Open sheet")
}
.buttonStyle(.plain)
.sheet(isPresented: $vm.isPresented) {
SheetView()
}
}
}
^这可能更像是一种hack而不是解决方案,因为它将从修改器输出一个新视图,这可能是导致它在较大的视图上不输出错误的原因。
解决方案2
这个解决方案来自于Alex Nagy(又名Rebeloper)。
正如Alex所解释的:
..随着SwiftUI 3和SwiftUI 4数据处理方式有所改变。SwiftUI的处理方式,更具体地说是@Published
变量的处理方式...
因此,解决方案是将布尔触发器作为视图中的@State
变量,而不是作为ViewModel中的@Published
变量。但正如Alex所指出的,这可能会使您的视图变得混乱,如果您在其中有很多状态,或者无法进行深度链接等。
![@State only - no ViewModel](https://istack.dev59.com/eOG97.gif)
然而,由于这是SwiftUI 4希望它们操作的方式,我们按如下方式运行代码:
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ViewOne()
}
}
}
struct ViewOne: View {
@State private var isPresented = false
var body: some View {
Button {
isPresented.toggle()
} label: {
Text("Open sheet")
}
.sheet(isPresented: $isPresented) {
SheetView(isPresented: $isPresented)
}
}
}
struct SheetView: View {
@Binding var isPresented: Bool
var body: some View {
Button {
isPresented.toggle()
} label: {
Text("Close sheet")
}
}
}
使用 @Published
和 @State
如果你需要继续使用 @Published
变量,因为它可能与您的应用程序的其他区域相关联,您可以通过 .onChange
和 .onReceive
来链接这两个变量:
struct ViewOne: View {
@EnvironmentObject private var vm: ViewModel
@State private var isPresented = false
var body: some View {
Button {
vm.isPresented.toggle()
} label: {
Text("Open sheet")
}
.sheet(isPresented: $isPresented) {
SheetView(isPresented: $isPresented)
}
.onReceive(vm.$isPresented) { newValue in
isPresented = newValue
}
.onChange(of: isPresented) { newValue in
vm.isPresented = newValue
}
}
}
![使用 @State 和 @Published 变量](https://istack.dev59.com/IeDZW.gif)
但是如果您需要为每个 sheet
或 fullScreenCover
触发它,这样的代码将变得非常混乱。
创建一个 ViewModifier
因此,为了让您更容易实现它,您可以创建一个 ViewModifier,Alex 已经展示了它也可以工作:
extension View {
func sync(_ published: Binding<Bool>, with binding: Binding<Bool>) -> some View {
self
.onChange(of: published.wrappedValue) { newValue in
binding.wrappedValue = newValue
}
.onChange(of: binding.wrappedValue) { newValue in
published.wrappedValue = newValue
}
}
}
![查看扩展名](https://istack.dev59.com/HbSk4.gif)
在查看中使用:
struct ViewOne: View {
@EnvironmentObject private var vm: ViewModel
@State private var isPresented = false
var body: some View {
Button {
vm.isPresented.toggle()
} label: {
Text("Open sheet")
}
.sheet(isPresented: $isPresented) {
SheetView(isPresented: $isPresented)
}
.sync($vm.isPresented, with: $isPresented)
}
}
^ 任何带有此标识的内容均为我的假设,而非实际技术理解 - 我不具备专业技术知识 :/
...which the user can change by clicking on a button:...
, show us the code of the Button and how youchange
thedataModel
. You can usually resolve this type of issue, usingDispatchQueue.main.async {....}
- workingdog support Ukraine