尝试将 .environment 与 Binding 结合使用时出现问题

1 binding environment-variables swiftui

在尝试在 SwiftUI 中实现从环境值获取绑定时,我遇到了困难。

我有一个小的 GlobalAlert 类,我创建一个 ObservableObject 并发布一个 Binding<Bool> ,旨在触发显示的警报。

然后,我为 Binding<Bool> 制作一些自定义的 EnvironmentKey 并扩展 EnvironmentValues 来保存它。

在我的应用程序中,我实例化一个 GlobalAlert 并将其设为 ObservedObject,并使用 .environment 修饰符将其注入到视图中。

在我的视图中,我选择了作为 Binding<Bool> 创建的环境值

视图中的按钮调用我在应用程序中创建的静态 GlobalAlert 上的方法(发布),该方法设置用于显示警报的状态 (isAlert)。

不显示警报。

在面包屑之后,我注意到我的发布方法确实设置了 isAlert 状态,但是当 .alert 开始测试它时,它已经被重置......至少我认为这就是正在发生的事情。我猜测 GlobalAlert 正在以某种方式重新实例化,但我不确定这将是什么,因为它是我的应用程序中的静态变量。

强制 Setter 将状态值设置为 true 会导致出现警报,这样我就知道我的管道正在工作。

这是代码...

import SwiftUI

enum alertTypes {
    case information
    case error
}

class GlobalAlert: ObservableObject{
    @Published var isAlert: Binding<Bool> = .constant(false)
    var alert: String = ""
    
    func publish(alert: String, type: alertTypes){
        print("gGlobalAlert publish")
        self.alert = alert
        print (isAlert)
        self.isAlert = .constant(true)
        print (isAlert)
    }
}

struct IsAlertKey: EnvironmentKey{
    static let defaultValue: Binding<Bool> = .constant(false)
}

extension EnvironmentValues {
    var isAlert: Binding<Bool> {
        get {
            print("Get")
            return self[IsAlertKey.self]
        }
        set {
            print("Set \(newValue)")
            self[IsAlertKey.self] = newValue
//            self[IsAlertKey.self] = .constant(true)
        }
    }
}

struct TestAlertApp: App {
    @ObservedObject static var gGlobalAlert = GlobalAlert()
    var body: some Scene {
        WindowGroup {
            ContentView()
                .environment( \.isAlert, TestAlertApp.gGlobalAlert.isAlert )
        }
    }
}

struct ContentView: View {
    @Environment(\.isAlert) var isAlert: Binding<Bool>
    var body: some View {
        VStack {
            Text("Hello, world!")
            Button("Test") {
                TestAlertApp.gGlobalAlert.publish(alert: "Alert triggered", type: .error)
            }
            .alert("\(TestAlertApp.gGlobalAlert.alert)", isPresented: isAlert){
                Button("OK", role: .cancel ) {
                    TestAlertApp.gGlobalAlert.isAlert = .constant(false)
                }
            }
        }.padding()
    }
}
Run Code Online (Sandbox Code Playgroud)

lor*_*sum 5

我将尝试对其进行分解,请注意,许多“原因”都是未知的,但这是自 SwiftUI 发布以来多年来我发现的逻辑。

\n

第1部分

\n

SwiftUI 整体上使用属性包装器来访问我们只能通过DynamicProperty.

\n

https://developer.apple.com/documentation/swiftui/dynamicproperty

\n

当此秘密存储确定(通过HashableIdentifiableEquatable)发生更改时,body它会重新加载并View在屏幕上更新。

\n

https://developer.apple.com/wwdc21/10022

\n

请注意,我说的是“属性包装器”(文档用语),虽然Binding<Bool>编译它不起作用,但它不是未记录的用途,您必须使用它@Binding才能正确进行重新加载。

\n

https://developer.apple.com/documentation/swiftui/binding

\n

我只见过苹果使用过一次,presentationMode但他们已经弃用了这个选项,选择了一个不使用Binding 解雇的版本。

\n

@Binding符合DynamicProperty

\n
\n

视图在重新计算 view\xe2\x80\x99s 主体之前为这些属性赋予值。

\n
\n

基于此,我说我的第一条评论Binding是“仅适用于 SwiftUI views/ DynamicPropertyconforming structs”。

\n

ObservableObject不满足这个标准,任何Binding在其中之一的使用都会有错误。有时它会表现得好像正在工作一样。

\n

第2部分

\n

有效的方法ObservableObject

\n
\n

ObservableObject 合成一个 objectWillChange 发布者,该发布者在其任何 @Published 属性更改之前发出更改的值。

\n
\n
\n

@ObservedObject 订阅一个可观察对象,并在可观察对象发生更改时使视图无效。

\n
\n

与不@ObservedObject使用或DynamicPropertyObservableObject@Published

\n

第三部分

\n

@ObservedObject static

\n
\n

SwiftUI 可能随时创建或重新创建视图,因此使用给定输入集初始化视图始终会产生相同的视图,这一点很重要。因此,在视图中创建观察对象是不安全的。相反,SwiftUI 为此提供了 StateObject 属性。

\n
\n

https://developer.apple.com/documentation/swiftui/managing-model-data-in-your-app

\n

这些是苹果的话,不是我的,只有他们可以告诉你原因,但我很确定这可以归结为秘密存储。

\n

static@ObservedObject实际上是解决最大缺陷(无法管理生命周期)的解决方法。

\n

这就是苹果为我们提供StateObjectiOS 14 的原因。

\n
\n

使用状态对象作为存储在视图层次结构中的引用类型的单一事实来源。通过将 @StateObject 属性应用于属性声明并提供符合 ObservableObject 协议的初始值,在 App、场景或视图中创建状态对象。将状态对象声明为私有,以防止从成员初始化器设置它们,这可能与 SwiftUI 提供的存储管理冲突:

\n
\n

https://developer.apple.com/documentation/swiftui/stateobject

\n

正如我的一般规则,我说StateObjectis 用于初始化和ObservedObject/EnvironmentObject用于传递。

\n

第 4 部分

\n

混合environmentObservedObject

\n

您已经通过使用破解了连接Binding<Bool>,但由于上述所有原因,它不起作用。要将内部的东西放入ObservableObject环境中,您应该使用.environmentObject(_:)注入并@EnvironmentObject访问您需要的地方。

\n

第五部分

\n

constant& Binding/第二条评论

\n

Binding根据定义,它是双向连接,其价值来自真理的来源。

\n

在你的代码@StateObject

\n
@Published var isAlert: Bool = false \n
Run Code Online (Sandbox Code Playgroud)\n

应该是你的真相来源。现在苹果...

\n
\n

Constant(_:)\n创建具有不可变值的绑定。

\n
\n
\n

使用此方法创建与无法更改的值的绑定。当使用 PreviewProvider 查看视图如何表示不同值时,这非常有用。

\n
\n

在 a 中ViewBinding不改变的与let属性相同,因为“它不会改变”,所以如果你找到一个理由在s.constant之外使用Preview它,那就是浪费资源,因为拥有 a 更有效let

\n

我还认为使用.constant预览之外会产生误导。它给人的印象是,特定的属性正在使用事实来源,并且对于应用程序的增长来说,如果您有大量没有真正与家长交谈的死胡同,那么这可能是一场噩梦。<- 意见

\n

第 6 部分

\n

注意 Apple 如何使用值类型来处理“状态更改”

\n

https://developer.apple.com/documentation/swiftui/managing-user-interface-state

\n

总长DR

\n
    \n
  1. 为什么您链接的示例有效,而您的示例无效?只是猜测,但是...
  2. \n
\n

在示例中,您显示的 OP 是从 到DynamicProperty StateDynamicProperty EnvironmentValues因此它与您正在做的事情不相等。两者都是DynamicProperty直接与秘密存储连接的值。

\n

ObservableObject您的设置也需要 s 才能工作的所有机制,Binding无法“发布”和“发出”,因此DynamicProperty ObservedObject可能会使View.

\n

我们不要忘记,这Binding是双向连接,而不是事实来源,它需要父级才能正常工作。

\n

“修复”您的代码

\n

代替

\n
@Published var isAlert: Binding<Bool> = .constant(false)\n
Run Code Online (Sandbox Code Playgroud)\n

\n
@Published var isAlert: Bool = false\n
Run Code Online (Sandbox Code Playgroud)\n

Binding不符合HashableIdentifiable或者Equatable无法知道何时发布。) <<\xe2\x80\x94 为什么我认为它会默默地失败。

\n

然后更换

\n
@ObservedObject static var gGlobalAlert = GlobalAlert()\n
Run Code Online (Sandbox Code Playgroud)\n

\n
@StateObject private var gGlobalAlert = GlobalAlert()\n
Run Code Online (Sandbox Code Playgroud)\n

static不在View访问秘密存储的范围内并且@ObservedObject不安全。)<<\xe2\x80\x94-为什么静态不健全。

\n

然后更换

\n
.environment( \\.isAlert, TestAlertApp.gGlobalAlert.isAlert )\n
Run Code Online (Sandbox Code Playgroud)\n

\n
.environmentObject(gGlobalAlert)\n
Run Code Online (Sandbox Code Playgroud)\n

(您现在正在传递对象本身。)

\n

然后全部替换

\n
@Environment(\\.isAlert) var isAlert: Binding<Bool>\n
Run Code Online (Sandbox Code Playgroud)\n

\n
@EnvironmentObject var gGlobalAlert: GlobalAlert\n
Run Code Online (Sandbox Code Playgroud)\n

然后你可以使用

\n
$gGlobalAlert.isAlert\n
Run Code Online (Sandbox Code Playgroud)\n

当你需要一个Binding

\n

但我个人会使用NotificationCenter这个,你本质上是用一个低效的版本重新创建它。

\n

我可以再做一个长篇大论的解释来解释为什么。但在简短的DynamicProperty更新和ObservedObject无效中,直接使用的包装器DynamicProperty总是会更高效。

\n

附...

\n

如果你使用的话我会更多地考虑这个

\n
@StateObject private var gGlobalAlert = GlobalAlert()\n
Run Code Online (Sandbox Code Playgroud)\n

\n
.environment( \\.isAlert, $gGlobalAlert.isAlert )\n
Run Code Online (Sandbox Code Playgroud)\n

并保持您当前的EnvironmentValues设置“可能/应该”工作,但我还没有测试过这一点,我也认为这会非常低效,让我们不要忘记“无效”并且我们已经拥有了NotificationCenter

\n