2

I want to remove repetitive code so I would like to create a simple MVP base view controller that will tie together a model, view and presenter types and automatically connect them e.g.:

class BaseMvpViewController<M: MvpModel, V: MvpView, P: MvpPresenter>: UIViewController {

Where my model and view are empty protocols:

protocol MvpModel {}
protocol MvpView: class {} // class is needed for weak property

and presenter looks like this:

protocol MvpPresenter {
    associatedtype View: MvpView
    weak var view: View? { get set }
    func onAttach(view: View)
    func onDetach(view: View)
}

This is my whole BaseMvpViewController:

class BaseMvpViewController<M: MvpModel, V, P: MvpPresenter>: UIViewController, MvpView {
    typealias View = V
    var model: M? = nil
    var presenter: P!

    required public init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }

    deinit {
        presenter.onDetach(view: self as! View)
    }

    override func viewDidLoad() {
        createPresenter()
        super.viewDidLoad()
        presenter.onAttach(view: self as! View)
    }

    func createPresenter() {
        guard presenter != nil else {
            preconditionFailure("Presenter was not created or it was not assigned into the `presenter` property!")
        }
    }
}

The problem is that the V must be without the protocol i.e. cannot be V: MvpView. Otherwise specific implementation of a VC must have a class/struct and not just a protocol for the MvpView. All my views are just protocols and my VCs will implement them e.g.

class MyViewController: BaseMvpViewController<MyModel, MyView, MyPresenter>, MyView

Now the compiler complains in the onAttach() and onDetach() methods that "argument type 'V' does not conform to expected type 'MvpView'"

So I tried an extension:

extension BaseMvpViewController where V: MvpView {
    override func viewDidLoad() {
        presenter.onAttach(view: self as! View)
    }
}

yet another compiler error: "cannot invoke 'onAttach' with an argument list of type '(view: V)'". There is another small compilation error "Members of constrained extensions cannot be declared @objc" where I override func viewDidLoad() in the extension. This can be fixed by my own method and calling that one from viewDidLoad in the custom class. Any idea how to achieve what I want?

This is a similar/same issue like Using some protocol as a concrete type conforming to another protocol is not supported but maybe something has been improved in the Swift world since then. Or did I really hit a hard limit in the current Swift's capabilities?

shelll
  • 3,234
  • 3
  • 33
  • 67
  • https://stackoverflow.com/questions/34534854/xcode-7-swift-2-impossible-to-instantiate-uiviewcontroller-subclass-of-generic-u – matt Aug 07 '17 at 12:17

1 Answers1

5

In have finally found a solution, the problem was in casting self as! View, it must be self as! P.View. And there cannot be a base protocol for view because protocols do not conform to themselves in Swift. Here is my complete code:

protocol MvpPresenter {
    associatedtype View
    var view: View? { get set }
    var isAttached: Bool { get }

    func onAttach(view: View)
    func onDetach(view: View)
}

/// Default implementation for the `isAttached()` method just checks if the `view` is non nil.
extension MvpPresenter {
    var isAttached: Bool { return view != nil }
}

class BaseMvpViewController<M, V, P: MvpPresenter>: UIViewController {
    typealias View = V
    var viewModel: M? = nil
    private(set) var presenter: P!

    //MARK: - Initializers

    required public init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }

    override public init(nibName: String?, bundle: Bundle?) {
        super.init(nibName: nibName, bundle: bundle)
    }

    deinit {
        presenter.onDetach(view: self as! P.View)
    }

    //MARK: - Lifecycle

    override func viewDidLoad() {
        super.viewDidLoad()
        presenter = createPresenter()
    }

    override func viewWillAppear(_ animated: Bool) {
        guard let view = self as? P.View else {
            preconditionFailure("MVP ViewController must implement the view protocol `\(View.self)`!")
        }

        super.viewWillAppear(animated)

        if (!presenter.isAttached) {
            presenter.onAttach(view: view)
        }
    }

    //MARK: - MVP

    /// Override and return a presenter in a subclass.
    func createPresenter() -> P {
        preconditionFailure("MVP method `createPresenter()` must be override in a subclass and do not call `super.createPresenter()`!")
    }
}

And a sample VC:

class MyGenericViewController: BaseMvpViewController<MyModel, MyView, MyPresenter>, MyView {
    ...
    override func createPresenter() -> MainPresenter {
        return MyPresenter()
    }
    ...
}

This VC will automatically have a viewModel property of type MyModel (could be anything e.g. struct, class, enum, etc), property presenter of type MyPresenter and this presenter will be automatically attached between viewDidLoad and viewWillAppear. One method must be overridden, the createPresenter() where you must create and return a presenter. This is called before the custom VC's viewDidLoad method. Presenter is detached in the deinit.

The last problem is that generic view controllers cannot be used in interface builder (IB), because IB talks to code via Objective-C runtime and that does not know true generics, thus does not see our generic VC. The app crashes when instantiating a generic VC from a storyboard/xib. There is a trick though that fixes this. Just load the generic VC manually into the Objective-C runtime before any instantiation from storyboard/xib. Good is in AppDelegate's init method:

init() {
    ...
    MyGenericViewController.load()
    ...
}

EDIT 1: I have found the loading of generic VC into Objective-C runtime in this SO answer https://stackoverflow.com/a/43896830/671580

EDIT 2: Sample presenter class. The mandatory things is the typealias, the weak var view: View? and the onAttach & onDetach methods. Minimum implementation of the attach/detach methods is also provided.

class SamplePresenter: MvpPresenter {
    // These two are needed!
    typealias View = SampleView
    weak var view: View?

    private let object: SomeObject
    private let dao: SomeDao

    //MARK: - Initializers

    /// Sample init method which accepts some parameters.
    init(someObject id: String, someDao dao: SomeDao) {
        guard let object = dao.getObject(id: id) else {
            preconditionFailure("Object does not exist!")
        }

        self.object = object
        self.dao = dao
    }

    //MARK: - MVP. Both the onAttach and onDetach must assign the self.view property!

    func onAttach(view: View) {
        self.view = view
    }

    func onDetach(view: View) {
        self.view = nil
    }

    //MARK: - Public interface

    /// Sample public method that can be called from the view (e.g. a ViewController)
    /// that will load some data and tell the view to display them.
    func loadData() {
        guard let view = view else {
            return
        }

        let items = dao.getItem(forObject: object)
        view.showItems(items)
    }

    //MARK: - Private
}
shelll
  • 3,234
  • 3
  • 33
  • 67
  • Can you share the implementation of Presenter? – mehdok Jan 01 '18 at 06:48
  • 1
    @mehdok I have added a sample presenter class. – shelll Jan 01 '18 at 10:01
  • Your view is like this ?? `protocol SampleView: MvpView {}` – mehdok Jan 01 '18 at 12:01
  • 1
    @mehdok there is no base protocol for views, because of Swift's limitations in generic protocols. So the views are just protocols. – shelll Jan 01 '18 at 12:44
  • 0_o What is the purpose of receiving a `View` (`func onDetach(view: View)`)? It does not make sense to me. – eMdOS Jan 11 '18 at 15:57
  • @eMdOS it is a lifecycle method meaning that the view is detached i.e. not show on the screen. there you can stop sensors (accelerometer, gps, etc.), regular refresh of data, pause/clean-up open gl, etc. the detach should be probably called in `viewWillDisappear ` instead of `deinit` though. there could also be an `onDestroyView` method which can be called in the `deinit`. thus we could detach when pushing a screen over the current one. – shelll Jan 12 '18 at 07:16
  • @shelll That is not what I meant. ```swift func onDetach(view: View) { self.view = nil } ``` What does not make sense to me is, why you are receiving a `View` if you are not using it?. `func onDettach() { self.view = .none }` should be enough. – eMdOS Jan 12 '18 at 15:24
  • @eMdOS this is a basic example where there is nothing in the `onDetach`, except for the mandatory `self.view = nil`. the detach is a good place for e.g. pausing media playback and clean-up. but yes, the view in the parameter is the one that is currently in the `self.view` property... – shelll Jan 12 '18 at 16:26
  • @shelll how to do this base class if controller `TableViewController` not a `UIViewController ` – a.masri Mar 16 '19 at 11:59
  • @a.masri you need an "MVP" implementation per base `UIViewController` class (or any UIView, if you need that). They will look the same except they will inherit from different VC e.g. `class BaseMvpNavigationController: UINavigationController` or `class BaseMvpTableViewController: UITableViewController`. Implementations are the same. – shelll Mar 18 '19 at 08:51