奇怪的SwiftUI行为:使用@Environment(\。presentationMode)时,ViewModel类+ @Binding中断

时间:2020-02-09 02:43:03

标签: swiftui

我一直在寻找非常奇怪的SwiftUI错误,这些错误仅在非常特殊的情况下才会出现?。例如,我有一个显示为模型表的表格。此表单具有ViewModel,并显示UITextView(通过UIViewRepresentable@Binding-都在下面的代码中)。

一切正常,您可以运行下面的代码,您将看到所有双向绑定都按预期工作:在一个字段中键入,在另一个字段中更改,反之亦然。但是,一旦取消注释行@Environment(\.presentationMode) private var presentationMode,TextView中的双向绑定就会中断。您还将注意到ViewModel两次打印“ HERE”。

到底是怎么回事?我的猜测是,ContentView一旦显示出模态,presentationMode的值就会改变,然后重新呈现工作表(因此FormView)。那将解释重复的“ HERE”被记录。但是,为什么这会破坏双向文本绑定?

一种解决方法是不使用ViewModel,而直接在@State中拥有一个FormView属性。但这不是一个很好的解决方案,因为我在现实世界的表单中有很多逻辑,而我不想转移到表单视图。那么,有人有更好的解决方案吗?

import SwiftUI
import UIKit

struct TextView: UIViewRepresentable {
  @Binding var text: String

  func makeCoordinator() -> Coordinator {
    Coordinator(self)
  }

  func makeUIView(context: Context) -> UITextView {
    let uiTextView = UITextView()
    uiTextView.delegate = context.coordinator
    return uiTextView
  }

  func updateUIView(_ uiView: UITextView, context: Context) {
    uiView.text = self.text
  }

  class Coordinator : NSObject, UITextViewDelegate {
    var parent: TextView

    init(_ view: TextView) {
      self.parent = view
    }

    func textViewDidChange(_ textView: UITextView) {
      self.parent.text = textView.text
    }

    func textViewDidEndEditing(_ textView: UITextView) {
      self.parent.text = textView.text
    }
  }
}

struct ContentView: View {
  @State private var showForm = false
  //@Environment(\.presentationMode) private var presentationMode

  var body: some View {
    NavigationView {
      Text("Hello")
      .navigationBarItems(trailing: trailingNavigationBarItem)
    }
    .sheet(isPresented: $showForm) {
      FormView()
    }
  }

  private var trailingNavigationBarItem: some View {
    Button("Form") {
      self.showForm = true
    }
  }
}

struct FormView: View {
  @ObservedObject private var viewModel = ViewModel()

  var body: some View {
    NavigationView {
      Form {
        Section(header: Text(viewModel.text)) {
          TextView(text: $viewModel.text)
            .frame(height: 200)
        }

        Section(header: Text(viewModel.text)) {
          TextField("Text", text: $viewModel.text)
        }
      }
    }
  }
}

class ViewModel: ObservableObject {
  @Published var text = ""

  init() {
    print("HERE")
  }
}

1 个答案:

答案 0 :(得分:0)

我终于找到了一种解决方法:将ViewModel存储在ContentView上而不是FormView上,然后将其传递给FormView。

struct ContentView: View {
  @State private var showForm = false
  @Environment(\.presentationMode) private var presentationMode

  private let viewModel = ViewModel()

  var body: some View {
    NavigationView {
      Text("Hello")
      .navigationBarItems(trailing: trailingNavigationBarItem)
    }
    .sheet(isPresented: $showForm) {
      FormView(viewModel: self.viewModel)
    }
  }

  private var trailingNavigationBarItem: some View {
    Button("Form") {
      self.showForm = true
    }
  }
}

struct FormView: View {
  @ObservedObject var viewModel: ViewModel

  var body: some View {
    NavigationView {
      Form {
        Section(header: Text(viewModel.text)) {
          TextView(text: $viewModel.text)
            .frame(height: 200)
        }

        Section(header: Text(viewModel.text)) {
          TextField("Text", text: $viewModel.text)
        }
      }
    }
  }
}

class ViewModel: ObservableObject {
  @Published var text = ""

  init() {
    print("HERE")
  }
}

唯一的问题是,即使从未打开FormView,打开ContentView时也立即实例化了ViewModel。感觉有点浪费。尤其是当您的列表很大时,通过NavigationLink链接到一堆详细信息页面,即使您从未离开过List页面,现在这些页面都可以预先创建其Form-as-a-Form FormView的ViewModel。

可悲的是,我无法将ViewModel转换为结构,因为我实际上需要(异步)更改状态,然后最终遇到Escaping closure captures mutating 'self' parameter编译器错误。叹。是的,我坚持使用类。

问题的根源仍然是两次对FormView进行实例化(由于@Environment(\.presentationMode)),这也导致创建了两个ViewModel(我的解决方法是将一个副本基本传递给两个FormViews来解决)。但是,由于标准TextField确实按预期工作,因此中断@Binding仍然很奇怪。

SwiftUI仍然有很多奇怪的陷阱,我真的希望这很快变得更容易管理。如果有人能解释工作表,ObservableObject类(视图模型),@Environment(\.presentationMode)@Binding放在一起的行为,我真是无所适从。