Marry G
Marry G

Reputation: 389

Implementing MVP using protocols

I'm trying to implement MVP using protocols, I have View controller that holds a reference to a presenter protocol. The presenter protocol contains a reference to the view and has associatedtype that represent the ResultType. The ResultType is different at each presenter. for example:

class PresenterA: PresenterProtocol {

    weak var view: ViewController!
    
    typealias ResultType = String
    var onDidPressCallback: ((ResultType) -> Void)?

}

It also can be

class PresenterB: PresenterProtocol {

    weak var view: ViewController!
    
    typealias ResultType = Apple
    var onDidPressCallback: ((ResultType) -> Void)?

}

The problem start's when Im holding a reference to the presenter from the ViewController.

class ViewController: UIViewController {
    var presenter: PresenterProtocol!
}

Of course It is not possible and I get this error:

Protocol 'PresenterProtocol' can only be used as a generic constraint because it has Self or associated type requirements

So I tried:

class ViewController<T: PresenterProtocol>: UIViewController {
    var presenter: T!
}

But now the PresenterProtocol has this issue:

Reference to generic type 'ViewController' requires arguments in <...>

What am I doing wrong? And how can I solve it?

Plus, let's say I can not support new versions so I cannot use opaque type (some keyword).

Upvotes: 1

Views: 475

Answers (1)

Rob
Rob

Reputation: 437897

In answer to your question, when you make the view controller generic, the error says that the “generic type 'ViewController' requires arguments in <...>”. So you can do precisely that:

class PresenterA: PresenterProtocol {
    weak var view: ViewController<PresenterA>!

    ...
}

You will probably end up wanting to make a required initializer so you can instantiate these presenters, e.g.:

protocol PresenterProtocol {
    associatedtype ResultType

    var onDidPressCallback: ((ResultType) -> Void)? { get set }

    init()
}

class PresenterA: PresenterProtocol {
    weak var view: ViewController<PresenterA>!

    var onDidPressCallback: ((String) -> Void)?

    required init() { ... }
}

class PresenterB: PresenterProtocol {
    weak var view: ViewController<PresenterB>!

    var onDidPressCallback: ((Apple) -> Void)?

    required init() { ... }
}

class ViewController<Presenter: PresenterProtocol>: UIViewController {
    var presenter = Presenter()

    override func viewDidLoad() {
        super.viewDidLoad()

        presenter.onDidPressCallback = { value in
            // show the value in the UI
        }

        ...
    }
}

Hopefully, that answers the question, but there are a few issues here:

  1. Presenters should be independent of UIKit. You do not want to have a reference to your view controller (or have any UIKit dependencies) in the presenter. Only the “view” (views, view controllers, etc.) should have UIKit dependencies. This separation of responsibilities is a core concept of MVP.

    There are a number of options to allow the presenter to inform the view of events (such as delegate protocols, async-await, etc.). But you have a closure variable, and that is a perfectly adequate way to proceed. The presenter does not need a reference to the view controller, but rather should simply call the appropriate closure. The view controller, obviously, will just set those closure variables so it can respond to the presenter events.

    But, as I said, there are a variety of different approaches to inform the “view” of events triggered by the presenter. But the presenter should not be reaching into the view controller, itself. No reference to the view controller is required or desired.

  2. By making the view controller a generic, it can no longer be instantiated via standard UIKit mechanisms (storyboards, NIBs, etc.). Unless you really want to lose the benefits of IB outlets, actions, etc., and do everything programmatically, you probably do not want to make it generic. You introduce significant development/maintenance costs for the sake of generics.

    Besides, a view controller for Apple and a view controller for String will probably have unique controls. Making it a generic might feel intuitively appealing at first, but in practice, it falls apart over time. The few times I went down this road, I ended up regretting it. As the UI evolves, you end up refining/specializing the UI for the individual types over time. The few times I tried permutations of this pattern, I found myself ripping it out later. It feels so intuitive, but it often becomes a hindrance later.

  3. Fortunately, when you remove the reference to the view controller from the presenter and do not make the view controller generic, much of this noise disappears:

    protocol PresenterProtocol {
        associatedtype ResultType
    
        var onDidPressCallback: ((ResultType) -> Void)? { get set }
    }
    
    class PresenterA: PresenterProtocol {
        var onDidPressCallback: ((String) -> Void)?
    
        // ...
    }
    
    class PresenterB: PresenterProtocol {
        var onDidPressCallback: ((Apple) -> Void)?
    
        // ...
    }
    
    class ViewControllerA: UIViewController {
        var presenter = PresenterA()
    
        override func viewDidLoad() {
            super.viewDidLoad()
    
            presenter.onDidPressCallback = { value in
                // show the data in the UI
            }
    
            // ...
        }
    }
    

    At this point, the protocol becomes more of a contract to ensure that all the presenters follow certain conventions for the shared functionality. But we no longer tie ourselves down, limiting our presenters to least-common-denominator functionality.

Upvotes: 1

Related Questions