1
\$\begingroup\$

I write Swift and architect apps mostly based on VIPER pattern. The thing I always wonder is, is it a proper to store some temporal values in Interactor? Let me show some examples I often write, excluding boilerplates like 'inits' nor 'injections':

ex. 1: Object Validation (View -> Presenter -> Interactor -> DataStore)

// Entity
struct Person: PersonEntity {
    let name: String?
    let age: Int?
}

// Interactor IO
class PersonAddDefaultInteractor: PersonAddInteractor {

    let dataSource: PersonAddDataStore
    weak var output: PersonAddInteractorOutput?

    var model: PersonEntity = Person(name: nil, age: nil) // <- IS THIS ACCEPTABLE APPROACH?

    func updateName(_ name: String) {
        model.name = name 
    }

    func updateAge(_ age: Int) {
        model.age = age 
    }

    func validate() {
        output?.validated(success: model.name != nil && model.age != nil)
    }

    func save() {
        dataSource.save(model)
    }
}

// Presenter
class PersonAddDefaultPresenter: PersonAddPresenter {

    weak var view: PersonAddView?
    let interactor: PersonAddInteractor
    let router: PersonAddWireframe

    func updateName(_ name: String) {
        interactor.updateName(name)
        interactor.validate()
    }

    func updateAge(_ age: Int) {
        interactor.updateAge(age)
        interactor.validate()
    }

    func save() {
        interactor.save()
    }
}

extension PersonAddDefaultPresenter: PersonAddInteractorOutput {

    func validated(success: Bool) {
        view?.setSaveButtonEnabled(success)

        if !success {
            router.showAlertController()
        }
    }
}

// View
class PersonAddViewController: UIViewController {

    var presenter: PersonPresenter?
    let saveButton: UIButton = UIButton()

    @objc func updateName(_ sender: UITextField) {
        guard let name: String = sender.text else { return }
        presenter?.updateName(name)
    }

    @objc func updateAge(_ sender: UISlider) {
        presenter?.updateAge(Int(sender.value))
    }

    @objc func save(_ sender: Any) {
        presenter?.save()
    }
}

extension PersonAddViewController: PersonView {

    func setSaveButtonEnabled(_ isEnabled: Bool) {
        saveButton.isEnabled = isEnabled
    }
}

The point here is, naturally you cannot obtain values that UITextField and UISlider has at the same time. (or must I make a func to do that?). Thus, I tend to - well, actually all the time - make temporal values/states/flags variables on Interactor, then do validate things using those stored values.


ex. 2: Fetching DataSource asynchronously without closures (DataSource -> Interactor -> Presenter -> View -> UICollectionView)

// Entity
struct Person: PersonEntity {
    let name: String?
    let age: Int?
}

// DataSource IO
class PersonDefaultDataSource: PersonDataSource {

    weak var output: PersonDataSourceOutput?

    func fetch() {
        output?.fetched([/* some data */])
    }
}

// Interactor IO
class PersonDefaultInteractor: PersonInteractor {

    let dataSource: PersonDataStore
    weak var output: PersonInteractorOutput?

    var models: [PersonEntity] = [] // <- IS THIS ACCEPTABLE APPROACH?

    func fetch() {
        dataSource.fetch()
    }

    func numberOfItems() -> {
        return models.count
    }

    func fetchItem(at indexPath: IndexPath) -> PersonEntity {
        return models[indexPath.row]
    }
}

class PersonDefaultInteractor: PersonDataSourceOutput {

    func fetched(_ models: [PersonEntity]) {
        DispatchQueue.main.async {
            self.models = models
            self.output?.fetchedItems()
        }
    }
}

// Presenter
class PersonDefaultPresenter: PersonPresenter {

    weak var view: PersonView?
    let interactor: PersonInteractor
    let router: PersonWireframe

    func onViewDidLoad() {
        interactor.fetch()
    }

    func numberOfItems() -> {
        return interactor.numberOfItems()
    }

    func fetchItem(at indexPath: IndexPath) -> PersonEntity {
        return interactor.fetchItem(at: indexPath)
    }
}

extension PersonDefaultPresenter: PersonInteractorOutput {

    func fetchedItems() {
        view?.reloadCollectionView()
    }
}

// View
class PersonAddViewController: UIViewController {

    var presenter: PersonPresenter?
    let collectionView = UICollectionView()

    override func viewDidLoad {
        super.viewDidLoad()

        presenter?.onViewDidLoad()
    }
}

extension PersonAddViewController: UICollectionViewDataSource {

    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection: Int) {
        return presenter?.numberOfItems() ?? 0
    }

    func collectionView(_ collectionView: UICollectionView, cellForItemAt: IndexPath) {
        let cell = collectionView.dequeue...etc
        let item = presenter?.fetchItem(at: indexPath)

        cell.nameLabel.text = item.name
        cell.ageLabel.text = item.age

        return cell
    }
}

extension PersonAddViewController: PersonView {

    func reloadCollectionView() {
        collectionView.reloadData()
    }
}

So, in this case, because I do want to avoid using dozens of closures:

  1. I fetch some data from DataSource asynchronously and store them in Interactor
  2. Then notify presenter that interactor obtained needed models
  3. Presenter notify view, that datasource is ready and do UICollectionView.reloadData() for the first time

Do you have an idea what I am describing what I am doing now? Simply saying, when needed values to do a task are from individual controls, the solution only I can think of is to store each values on somewhere and keep those values latest. Or, would function like below - collecting values from all individual controls all the time when a value is needed - fit more in VIPER?

var nameTextField: UITextField!
var descriptionTextView: UITextView!

func collectCurrentValues() {
    presenter?.updateName(nameTextField.text!)
    presenter?.updateDescription(descriptionTextView.text!)
}
\$\endgroup\$
1
  • \$\begingroup\$ should this has been posted at stackoverflow...? \$\endgroup\$ – provdr Jun 17 '17 at 5:04

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.