在 SwiftUI 中理解通信模式“Preference Key”的问题

Posted

技术标签:

【中文标题】在 SwiftUI 中理解通信模式“Preference Key”的问题【英文标题】:Problems with understanding communication pattern "Preference Key" in SwiftUI 【发布时间】:2020-05-22 08:18:02 【问题描述】:

更新 - 我已经更新了整个问题,因为我觉得我对问题的描述很糟糕!


我刚刚开始玩SwiftUI,所以如果这个问题很愚蠢,请原谅。几天以来,我一直在尝试,阅读了许多不同的帖子,并尝试遵循 apple.com 上的官方文档,但不幸的是,我无法理解 Preference Keys 在 SwiftUI 中的工作原理。

基本上,我的测试应用程序由 3 个不同的组件构成:

ContentView
// contains a form and a text field in an HStack

    ∟ FormView
      // contains the SubFormViews in a TabView

          L SubFormView
            // contains the form fields

我想要实现的是从实际表单字段到主视图的信息流,然后更新文本字段。从我目前阅读的内容来看,PreferenceKey 似乎是解决这个问题的最佳选择。因此,我的块视图如下所示:

我的 SubFormView 看起来像这样:

import SwiftUI

// --------------------
//  The preference key
// --------------------
struct FormFieldKey: PreferenceKey 
    static var defaultValue = FormFieldData() // <-- I initialise the values in FormFieldData

    static func reduce(value: inout FormFieldData, nextValue: () -> FormFieldData) 
        value = nextValue()
    


// ---------------------
//  The form field data
// ---------------------
struct FormFieldData: Equatable 
    var firstName: String

    // Initialise the form fields
    init() 
        self.firstName = ""
    


// -------------------------
//  The actual subform view
// -------------------------
struct SubFormView: View 
    @State private var formFields = FormFieldData() // <-- I set the state vars for this view to FormFieldData

    var body: some View 
       Form 
        TextField("First Name", text: $formFields.firstName)
            .preference(key: FormFieldKey.self, value: self.formFields)
            // If I understood right, I set the Preference Key value here.
            // The value gets overridden by formFields.
        .padding()
    

那么,我的FormView是:

import SwiftUI

struct FormView: View 

    var body: some View 
        TabView()   // <-- I intend to put additional SubFormViews in different tabs

            SubFormView()
                .tabItem 
                    Text("Sub Form")
                
        
    

最后我的 ContentView 是:

import SwiftUI

struct ContentView: View 
    @State private var test: FormFieldData? // <-- I'm not sure why this has to be optional

    var body: some View 

        HStack 
            FormView()
                .onPreferenceChange(FormFieldKey.self)  self.test = $0  // <-- I literally have no clue what I'm supposed to do here!
            Text("\(self.test!.firstName)") // This is what I would like to achieve
                       .frame(maxWidth: .infinity, maxHeight: .infinity)
        
    

再次,如果这是一个愚蠢的问题,我很抱歉,但我真的被卡住了,根本无法理解这是如何工作的......

感谢您的帮助!

【问题讨论】:

注入 ObservableObject 作为视图模型在这种情况下会更方便。 @Asperi - 谢谢你的回答!我看了看,但第一个我无法真正让它与ObservableObject 一起工作,第二个我认为首选的方法是通过PreferenceKey (从孩子到远方的父母)。 from Child to distant Parent 对于子视图状态 - 是的,对于模型数据......我不会,但如你所愿 - 这是你的游戏。 )) 我对任何事情都持开放态度!我对ObservableObject 的问题是我不知道如何告诉Observable 它需要成为TextField 的值。我能找到的唯一属性是.preference...你能给我一个提示吗? 从远处看,您的问题看起来很像swiftui-lab.com/communicating-with-the-view-tree-part-3 正在做的事情,这是通过 PreferencesKeys 完成的 【参考方案1】:

由于似乎没有人可以帮助解决原始问题,因此我关注了@Asperi 的评论并设法通过ObservableObject 解决了问题。我仍然不确定这是否是正确的做法,但至少它对我有用。

因为我是 SWIFTUI 的菜鸟,所以这显然不是一个完美的解决方案(例如数据模型),但是以防万一其他人遇到同样的问题,这是我的解决方案:

首先我创建了一个类 FormFields 作为ObservableObject

import Foundation

class FormFields: ObservableObject 
    @Published var formData = FormData()


// -----------------------
// FORM DATA
// -----------------------
// Combines all data of different form blocks and makes
// it accessible throughout the app.
struct FormData 
    var block1 = Block1Data()
    // you could add as many blocks as you wish



// -----------------------
// BLOCK 1 DATA
// -----------------------
// Data fields for block 1. (in this case it's all Strings)
struct Block1Data 
    var firstField: String
    var secondField: String
    var thirdField: String

    // initialise form fields with empty strings (this is not essential,
    // you could also do var firstField = ""
    init() 
        self.firstField = ""
        self.secondField = ""
        self.thirdField = ""
    

然后 SubformView 看起来像

import SwiftUI

struct SubFormView: View 

    // inject the environment variable "formFields"
    @EnvironmentObject var formFields: FormFields

    var body: some View 
       Form 
        TextField("First Field", text: $formFields.formData.block1.firstField)
        TextField("Second Field", text: $formFields.formData.block1.secondField)
        TextField("Third Field", text: $formFields.formData.block1.thirdField)
       
    


// if you want your preview working for this specific sub view, don't forget
// to add the environmentObject() to the view!
struct CompanyDataFormView_Previews: PreviewProvider 
    static var previews: some View 
        CompanyDataFormView().environmentObject(FormFields()) // <-- this here!
    


FormView 不会改变,因为没有什么需要更新

import SwiftUI

struct FormView: View 

    var body: some View 
        TabView() 

            SubFormView()
                .tabItem 
                    Text("Sub Form")
                
        
    

然后 ContentView 将是

import SwiftUI

struct ContentView: View 

    @EnvironmentObject var formFields: FormFields

    var body: some View 

        HStack 
            FormView()

            Text("Output any text from the form fields now! E.g. firstField = \(formFields.formData.block1.firstField)")

        
    


// again, if you wish to see the preview then you need to attach the environmentObject
struct ContentView_Previews: PreviewProvider 
    static var previews: some View 
        ContentView().environmentObject(FormFields())
    

为了完成这项工作,还需要完成最后一步!

您需要在AppDelegate 中设置环境变量,并将其附加到ContentView。这是我的 AppDelegate

import Cocoa
import SwiftUI

@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate 

    var window: NSWindow!

    // set the environment variable for the form fields
    var formFields = FormFields()

    func applicationDidFinishLaunching(_ aNotification: Notification) 
        // Create the SwiftUI view that provides the window contents.
        let contentView = ContentView()

        // Create the window and set the content view. 
        window = NSWindow(
            contentRect: NSRect(x: 0, y: 0, width: 480, height: 300),
            styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView],
            backing: .buffered, defer: false)
        window.center()
        window.setFrameAutosaveName("Main Window")
        window.contentView = NSHostingView(rootView: contentView.environmentObject(formFields)) // <-- attach it to ContentView
        window.makeKeyAndOrderFront(nil)
    

    func applicationWillTerminate(_ aNotification: Notification) 
        // Insert code here to tear down your application
    


我希望这可以在开发过程中为某人节省一点时间......

【讨论】:

以上是关于在 SwiftUI 中理解通信模式“Preference Key”的问题的主要内容,如果未能解决你的问题,请参考以下文章

如何允许对不在编辑模式下的 SwiftUI 列表中的行进行重新排序?

如何知道 UIKit 视图控制器中的 swiftUI 事件? ||如何提供 swiftUI 和 uikit 之间的通信

SwiftUI 和 MVVM - 模型和视图模型之间的通信

SwiftUI .fileImporter 在通过向下滑动关闭后无法再次显示

SwiftUI-MVVM

SwiftUI 中 ViewModel + View 之间的通信