Странное поведение SwiftUI: класс ViewModel + @Binding нарушается при использовании @Environment (\. PresentationMode) - PullRequest
0 голосов
/ 09 февраля 2020

Я продолжаю находить очень странные ошибки SwiftUI, которые появляются только при очень определенных c обстоятельствах ?. Например, у меня есть форма, которая отображается в виде листа модели. Эта форма имеет ViewModel и показывает UITextView (через UIViewRepresentable и @Binding - это все в коде ниже).

Все работает абсолютно нормально, вы можете запустить приведенный ниже код и вы Вы увидите, что все двусторонние привязки работают должным образом: введите в одно поле, а в другом - изменения, и наоборот. Однако, как только вы откомментируете строку @Environment(\.presentationMode) private var presentationMode, двустороннее связывание в TextView разрывается. Вы также заметите, что ViewModel печатает «ЗДЕСЬ» дважды.

Что, черт возьми, происходит? Я предполагаю, что, как только ContentView покажет модал, значение presentationMode изменится, что приведет к повторной визуализации листа (так, FormView). Это объяснило бы дубликат "ЗДЕСЬ", регистрирующийся. Но почему это нарушает двустороннюю привязку текста?

Один из способов - не использовать ViewModel, а просто иметь свойство @State непосредственно в FormView. Но это не очень хорошее решение, так как у меня есть куча логик c в моей реальной форме, которую я не хочу переходить к виду формы. Так, у кого-нибудь есть лучшее решение?

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 голосов
/ 09 февраля 2020

Я наконец нашел обходной путь: сохраните 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")
  }
}

Единственное, что ViewModel теперь создается правильно, когда ContentView открывается, даже если вы никогда не открываете FormView. Чувствует себя немного расточительно. Особенно, когда у вас есть большой список, с навигационными ссылками на кучу страниц с подробностями, которые теперь все создают свою ViewModel FormView, представленную в виде листа, заранее, даже если вы никогда не покидаете страницу списка.

К сожалению Я не могу превратить ViewModel в структуру, так как на самом деле мне нужно (асинхронно) изменять состояние, а затем в конечном итоге я сталкиваюсь с ошибкой Escaping closure captures mutating 'self' parameter компилятора. Вздох. Так что да, я застрял с использованием класса.

Проблема root по-прежнему заключается в том, что FormView создается дважды (из-за @Environment(\.presentationMode)), что также приводит к созданию двух ViewModel (что мой обходной путь решается путем передачи одной копии в оба FormViews в основном). Но все же странно, что это сломалось @Binding, так как стандартные TextFields работали, как и ожидалось.

В SwiftUI все еще есть много странных ошибок, подобных этой, я очень надеюсь, что этим скоро станет проще управлять. Если кто-нибудь может объяснить поведение листов, ObservableObject классов (viewmodels), @Environment(\.presentationMode) и @Binding вместе взятых, я все уши.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...