Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 756
Austin Conlon Avatar asked Sep 08 '20 11:09

Austin Conlon


2 Answers

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)
            }
        }
    }
}
like image 163
Asperi Avatar answered Oct 18 '22 18:10

Asperi


When it comes to organizing the UI code, best practices mandate to have 3 parts:

  1. view (only visual structure, styling, animations)
  2. model (your data and business logic)
  3. 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:

  1. If this is too old-school for you, you can use Combine instead of the delegate to subscribe/publish a didSubmit event.
  2. In this simple example the model is just a String. Feel free to use your custom model data type.
  3. 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.
  4. $viewModel.name syntax is a magic that creates a Binding<String> instance referring to the mutable name property of the MyFormViewModel.
like image 6
battlmonstr Avatar answered Oct 18 '22 18:10

battlmonstr