SwiftUI:从UI(选取器等)设置ObservableObject中的Published值

9njqaruj  于 2023-02-03  发布在  Swift
关注(0)|答案(2)|浏览(194)

更新:

    • 这个问题已经解决了**(请参见下面的回答)。正确的方法是通过投影ObservableObject来获得Binding。例如,$options.refreshRate

TLDR版本:

如何获取SwiftUI Picker(或其他依赖于本地Binding的API)以立即更新我的ObservedObject/EnvironmentObject

场景:

以下是我在创建的每个SwiftUI应用程序中始终需要做的事情...

  • 我总是创建一些类来存储任何用户首选项(让我们称之为Options,我将其设置为ObservableObject
  • 任何需要使用的设置都标记为@Published
  • 任何使用它的视图都会将其作为@ObservedObject@EnvironmentObject引入,并订阅更改。

这一切都很好地工作。我总是面临的麻烦是如何从用户界面设置这一点。从用户界面,这是我通常在做什么(这应该听起来很正常):

  • 我有一些SwiftUI视图,如OptionsPanel,它驱动上面的Options类,并允许用户选择他们的选项。
  • 假设我们有一个由枚举定义的选项:
enum RefreshRate {
    case low, medium, high
}

当然,我会在SwiftUI中选择Picker来设置此参数...而Picker API要求我的选择参数为Binding这就是我发现问题的地方...

问题:

为了让Picker正常工作,我通常会使用一些本地Binding来实现这个目的。但是,最终,我并不关心这个本地值。我关心的是立即将这个新值广播到应用程序的其他部分。当我选择一个新的刷新率时,我希望 * 立即 * 知道更改的那一刻。ObservableObjectOptions类)对象很好地完成了这一点。我只是在更新一个本地的Binding,我需要弄清楚的是如何在每次Picker的状态改变时立即将其转换为ObservableObject
我有一个可行的解决方案......但我不喜欢它。下面是我的非理想解决方案:

非理想解决方案:

解决方案的第一部分实际上很好,但遇到了一个障碍...
在我的SwiftUI视图中,我可以使用另一个初始化器,而不是用@State设置Binding的最简单方法...

// Rather than this...
@ObservedObject var options: Options
@State var refreshRate: RefreshRate = .medium

// Do this...
@ObservedObject var options: Options
var refreshRate: Binding<RefreshRate>(
    get: { self.options.refreshRate },
    set: { self.options.refreshRate = $0 }
)

到目前为止,这是伟大的(理论上)!现在,我的本地Binding直接链接到ObservableObject。所有的变化Picker立即广播到整个应用程序。
但这实际上并不奏效。这就是我必须做一些非常混乱和不理想的事情来让它奏效的地方。
上面的代码产生以下错误:

Cannot use instance member 'options' within property initializer; property initializers run before 'self' is available

这是我的(坏的)变通方法。它起作用了,但是很糟糕...
Options类提供了一个shared示例作为静态属性,因此,在我的选项面板视图中,我执行以下操作:

@ObservedObject var options: Options = .shared // <-- This is still needed to tell SwiftUI to listen for updates
var refreshRate: Binding<RefreshRate>(
    get: { Options.shared.refreshRate },
    set: { Options.shared.refreshRate = $0 }
)

在实践中,这实际上在这种情况下是可行的。我并不真的需要有多个示例......只有一个。所以,只要我总是引用那个共享示例,一切都可以工作。但是感觉架构得不好。
那么... * 有人有更好的解决方案吗?* 这似乎是地球上每个应用程序都必须解决的问题,所以似乎一定有人有更好的方法。
(我知道有些人使用.onDisapear将本地状态同步到ObservedObject,但这也不理想。这不理想,因为我重视对应用程序的其余部分进行 * 立即 * 更新。)

owfi6suc

owfi6suc1#

好消息是你太努力了,太努力了。
ObservedObject属性 Package 器可以为你创建这个Binding,你只需要说$options.refreshRate
这里有一个测试操场供你尝试:

import SwiftUI

enum RefreshRate {
    case low, medium, high
}

class Options: ObservableObject {
    @Published var refreshRate = RefreshRate.medium
}

struct RefreshRateEditor: View {
    @ObservedObject var options: Options
    
    var body: some View {

                                       // vvvvvvvvvvvvvvvvvvvv

        Picker("Refresh Rate", selection: $options.refreshRate) {

                                       // ^^^^^^^^^^^^^^^^^^^^

            Text("Low").tag(RefreshRate.low)
            Text("Medium").tag(RefreshRate.medium)
            Text("High").tag(RefreshRate.high)
        }
        .pickerStyle(.segmented)
    }
}

struct ContentView: View {
    @StateObject var options = Options()
    
    var body: some View {
        VStack {
            RefreshRateEditor(options: options)
            
            Text("Refresh rate: \(options.refreshRate)" as String)
        }
        .padding()
    }
}

import PlaygroundSupport
PlaygroundPage.current.setLiveView(ContentView())

同样值得注意的是,如果你想创建一个自定义的Binding,你写的代码几乎可以工作,只要把它改为计算属性而不是存储属性即可:

var refreshRate: Binding<RefreshRate> {
    .init(
        get: { self.options.refreshRate },
        set: { self.options.refreshRate = $0 }
    )
}
lxkprmvk

lxkprmvk2#

如果我没理解错的话,您希望在SwiftUI中执行Set a Published value in an ObservableObject from the UI (Picker, etc.)
有很多方法可以做到这一点,我建议您使用ObservableObject类,并在视图中需要绑定的任何地方(如Picker直接使用它。
下面的示例代码显示了一种设置代码以执行此操作的方法:

import Foundation
import SwiftUI

// declare your ObservableObject class
class Options: ObservableObject {
    @Published var name = "Mickey"
}

struct ContentView: View {
    @StateObject var optionModel = Options()  // <-- initialise the model
    let selectionSet = ["Mickey", "Mouse", "Goofy", "Donald"]
    @State var showSheet = false

    var body: some View {
        VStack {
            Text(optionModel.name).foregroundColor(.red)
            Picker("names", selection: $optionModel.name) {  // <-- use the model directly as a $binding
                ForEach (selectionSet, id: \.self) { value in
                    Text(value).tag(value)
                }
            }
            Button("Show other view") { showSheet = true }
        }
        .sheet(isPresented: $showSheet) {
            SheetView(optionModel: optionModel) // <-- pass the model to other view, see also @EnvironmentObject
        }
    }
}

struct SheetView: View {
    @ObservedObject var optionModel: Options  // <-- receive the model
      
    var body: some View {
        VStack {
            Text(optionModel.name).foregroundColor(.green) // <-- show updated value
        }
    }
}

如果你真的希望有一个“无用的”中间局部变量,那么使用这个方法:

struct ContentView: View {
     @StateObject var optionModel = Options()  // <-- initialise the model
     let selectionSet = ["Mickey", "Mouse", "Goofy", "Donald"]
     @State var showSheet = false
     
     @State var localVar = ""  // <-- the local var
     
     var body: some View {
         VStack {
             Text(optionModel.name).foregroundColor(.red)
             Picker("names", selection: $localVar) {  // <-- using the localVar
                 ForEach (selectionSet, id: \.self) { value in
                     Text(value).tag(value)
                 }
             }
             .onChange(of: localVar) { newValue in
                 optionModel.name = newValue  // <-- update the model
             }
             Button("Show other view") { showSheet = true }
         }
         .sheet(isPresented: $showSheet) {
             SheetView(optionModel: optionModel) // <-- pass the model to other view, see also @EnvironmentObject
         }
     }
 }

相关问题