SwiftUI 从视图更新中发布环境更改

coc*_*oco 20 environment-variables swiftui

该应用程序有一个model存储用户当前对亮/暗模式的偏好,用户可以通过单击按钮来更改:

class DataModel: ObservableObject {
    @Published var mode: ColorScheme = .light
Run Code Online (Sandbox Code Playgroud)

ContentViewbody跟踪模型,并在模型更改时调整 colorScheme:

struct ContentView: View {
    @StateObject private var dataModel = DataModel()

var body: some View {
        NavigationStack(path: $path) { ...
        }
        .environmentObject(dataModel)
        .environment(\.colorScheme, dataModel.mode)
Run Code Online (Sandbox Code Playgroud)

从 Xcode 版本 14.0 beta 5 开始,这会产生一个紫色警告:Publishing changes from within view updates is not allowed, this will cause undefined behavior.还有其他方法可以做到这一点吗?或者是测试版中的一个小问题?谢谢!

mar*_*rkb 16

更新: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/


完全披露 - 我不完全确定为什么会发生这种情况,但这是我发现似乎有效的两个解决方案。

错误信息

动作错误

示例代码

// -- main view
@main
struct MyApp: App {
  @StateObject private var vm = ViewModel()
  var body: some Scene {
    WindowGroup {
      ViewOne()
       .environmentObject(vm)
    }
  }
}

// -- initial view
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()
    }
  }
}

// -- sheet view
struct SheetView: View {
  @EnvironmentObject private var vm: ViewModel
  var body: some View {
    Button {
      vm.isPresented.toggle()
    } label: {
      Text("Close sheet")
    }
  }
}

// -- view model
class ViewModel: ObservableObject {
  @Published var isPresented: Bool = false
}
Run Code Online (Sandbox Code Playgroud)

解决方案1

注意:根据我的测试和下面的示例,我仍然会出现错误。但如果我有一个更复杂/嵌套的应用程序,那么错误就会消失。

将 a 添加.buttonStyle()到执行初始切换的按钮。

因此,在aContentViewButton() {}添加.buttonStyle(.plain),它将消除紫色错误:

struct ViewOne: View {
  @EnvironmentObject private var vm: ViewModel
  var body: some View {
    Button {
      vm.isPresented.toggle()
    } label: {
      Text("Open sheet")
    }
    .buttonStyle(.plain) // <-- here
    .sheet(isPresented: $vm.isPresented) {
      SheetView()
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

^ 这可能更像是一种黑客而不是解决方案,因为它会从修改器输出一个新视图,这可能是导致它不在较大视图上输出错误的原因。

解决方案2

这要归功于 Alex Nagy(又名Rebeloper

正如亚历克斯所解释的:

.. 在 SwiftUI 3 和 SwiftUI 4 中,数据处理方式发生了变化。SwiftUI 如何处理,更具体地说是@Published变量..

因此,解决方案是让布尔触发器成为@State视图中的变量,而不是@PublishedViewModel 中的变量。但正如亚历克斯指出的那样,如果你的视图中有很多状态,或者无法深层链接等,它可能会让你的视图变得混乱。

仅 @State - 无 ViewModel

但是,由于这是 SwiftUI 4 希望这些操作的方式,因此我们按如下方式运行代码:

// -- main view
@main
struct MyApp: App {
  var body: some Scene {
    WindowGroup {
      ViewOne()
    }
  }
}

// -- initial view
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)
      // SheetView() <-- if using dismiss() in >= iOS 15
    }
  }
}

// -- sheet view
struct SheetView: View {
  // I'm showing a @Binding here for < iOS 15
  // but you can use the dismiss() option if you
  // target higher
  // @Environment(\.dismiss) private var dismiss
  @Binding var isPresented: Bool
  var body: some View {
    Button {
      isPresented.toggle()
      // dismiss()
    } label: {
      Text("Close sheet")
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

使用@Published@State

继续观看视频,如果您仍然需要使用该@Published变量,因为它可能与应用程序的其他区域相关联,您可以使用 a.onChange和 a.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
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

使用@State和@Published变量

但是,如果您必须为每个sheet或触发它,那么您的代码可能会变得非常混乱fullScreenCover

创建视图修改器

因此,为了让您更轻松地实现它,您可以创建一个 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
      }
  }
}
Run Code Online (Sandbox Code Playgroud)

查看扩展

并在视图上使用:

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)

//    .onReceive(vm.$isPresented) { newValue in
//      isPresented = newValue
//    }
//    .onChange(of: isPresented) { newValue in
//      vm.isPresented = newValue
//    }
  }
}
Run Code Online (Sandbox Code Playgroud)

^ 以此表示的任何内容都是我的假设,而不是真正的技术理解 - 我不是技术知识渊博的人:/