'How do you share a data model between a UIKit view controller and a SwiftUI view that it presents?
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?
Solution 1:[1]
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)
}
}
}
}
Solution 2:[2]
When it comes to organizing the UI code, best practices mandate to have 3 parts:
- view (only visual structure, styling, animations)
- model (your data and business logic)
- a secret sauce that connects view and model
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:
- If this is too old-school for you, you can use Combine instead of the delegate to subscribe/publish a
didSubmit
event. - In this simple example the model is just a String. Feel free to use your custom model data type.
- There's no guarantee that MyFormViewModel object stays alive when the view is destroyed, so probably it is wise to keep a strong reference somewhere if you want it survive for longer.
$viewModel.name
syntax is a magic that creates aBinding<String>
instance referring to the mutablename
property of the MyFormViewModel.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | |
Solution 2 |