My data model property is declared in my table view controller, and the SwiftUI view is modally presented. I'd like the presented Form
input to manipulate the data model. The resources I've found on data flow are just between SwiftUI views, and the resources I've found on UIKit integration are on embedding UIKit in SwiftUI rather than the other way around.
Furthermore, is there a good approach for a value type (in my case struct) data model, or would it be worth remodeling it as a class so that it's a reference type?
Let's analyse...
My data model property is declared in my table view controller and the SwiftUI view is modally presented.
So here is what you have now (probably simplified)
struct DataModel {
var value: String
}
class ViewController: UIViewController {
var dataModel: DataModel
// ... some other code
func showForm() {
let formView = FormView()
let controller = UIHostingController(rootView: formView)
self.present(controller, animating: true)
}
}
I'd like the presented Form input to manipulate the data model.
And here an update above with simple demo of passing value type data into SwiftUI view and get it back updated/modified/processed without any required refactoring of UIKit part.
The idea is simple - you pass current model into SwiftUI by value and return it back in completion callback updated and apply to local property (so if any observers are set they all work as expected)
Tested with Xcode 12 / iOS 14.
class ViewController: UIViewController {
var dataModel: DataModel
// ... some other code
func showForm() {
let formView = FormView(data: self.dataModel) { [weak self] newData in
self?.dismiss(animated: true) {
self?.dataModel = newData
}
}
let controller = UIHostingController(rootView: formView)
self.present(controller, animated: true)
}
}
struct FormView: View {
@State private var data: DataModel
private var completion: (DataModel) -> Void
init(data: DataModel, completion: @escaping (DataModel) -> Void) {
self._data = State(initialValue: data)
self.completion = completion
}
var body: some View {
Form {
TextField("", text: $data.value)
Button("Done") {
completion(data)
}
}
}
}
When it comes to organizing the UI code, best practices mandate to have 3 parts:
In UIKit we use MVP approach where a UIViewController subclass typically represents the secret sauce part.
In SwiftUI it is easier to use the MVVM approach due to the provided databinding facitilies. In MVVM the "ViewModel" is the secret sauce. It is a custom struct that holds the model data ready for your view to present, triggers view updates when the model data is updated, and forwards UI actions to do something with your model.
For example a form that edits a name could look like so:
struct MyForm: View {
let viewModel: MyFormViewModel
var body: some View {
Form {
TextField("Name", text: $viewModel.name)
Button("Submit", action: { self.viewModel.submit() })
}
}
}
class MyFormViewModel {
var name: String // implement a custom setter if needed
init(name: String) { this.name = name }
func submit() {
print("submitting: \(name)")
}
}
Having this, it is easy to forward the UI action to UIKit controller. One standard way is to use a delegate protocol:
protocol MyFormViewModelDelegate: class {
func didSubmit(viewModel: MyFormViewModel)
}
class MyFormViewModel {
weak var delegate: MyFormViewModelDelegate?
func submit() {
self.delegate?.didSubmit(viewModel: self)
}
...
Finally, your UIViewController can implement MyFormViewModelDelegate, create a MyFormViewModel instance, and subscribe to it by setting self
as a delegate
), and then pass the MyFormViewModel object to the MyForm view.
Improvements and other tips:
didSubmit
event.$viewModel.name
syntax is a magic that creates a Binding<String>
instance referring to the mutable name
property of the MyFormViewModel.If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With