Difficulty: Beginner | Easy | Normal | Challenging
This article has been developed using Xcode 12.5, and Swift 5.4
- You need to be able to create a new Swift project, and a Single View Application
MVC: Model, View and Controller MVP: Model, View and Presenter
In my Previous article describing the MVP architecture, I created the Presenter in the View.
Now this is Ok, fine and whatever, but here we need to have the coordinator which is visible to the presenter. It is for this reason that we are creating the View from the presenter.
This is based on my flow coordinator article, and MVP article goes into some detail about the Dependency Factory that is used with the implementation described below.
There a a couple of goals in using MVP in any particular project
The Presenter updates the View and reacts to User Interactions with the View. This means, by definition, the View must call methods on the Presenter.
The View must therefore have a reference to the Presenter, at the same time as the Presenter is required to have a reference to the View.
This leaves an issue: which of the View and presenter should be created first (and which should create the other)?
In this implementation I've added to the DependencyFactory
, created the presenter and then set the presenter within the view controller. See the code right here:
let viewController = MenuViewController()
let presenter = MenuPresenter(coordinator: coordinator, view: viewController)
viewController.set(presenter: presenter)
return viewController
The idea of the dependency factory is based around my Flow Coordinators article, which might well make things seem a little clearer for the reader.
The most interesting part of this implementation, is the ProjectCoordinator which takes care of the navigation for the project away from the View (in this project the View Controller):
func start(_ navigationController: UINavigationController) {
let vc = factory.makeInitialViewController(coordinator: self)
self.navigationController = navigationController
navigationController.pushViewController(vc, animated: true)
}
func moveToDetail(withData data: String) {
let vc = factory.makeDetailViewController(coordinator: self, data: data)
navigationController?.pushViewController(vc, animated: true)
}
The DependencyFactory creates the View Controller and Presenters correctly to be used within the architecture implementation:
func makeDetailViewController(coordinator: ProjectCoordinator, data: String) -> DetailViewController {
let viewController = DetailViewController(data: data)
let presenter = DetailPresenter(view: viewController)
viewController.set(presenter: presenter)
return viewController
}
func makeInitialViewController(coordinator: ProjectCoordinator) -> MenuViewController {
let viewController = MenuViewController()
let presenter = MenuPresenter(coordinator: coordinator, view: viewController)
viewController.set(presenter: presenter)
return viewController
}
We set the view controller for the presenter in the initializer. Equally, use a function for the view controller to set the presenter - that is they both have references to each other.
We can take a look at the MenuViewController and MenuPresenter (the full code is in the repo), but the snippet here can be useful:
MenuViewController
class MenuViewController: UIViewController {
let tableView = UITableView()
var menuPresenter: MenuPresenterProtocol?
override func loadView() {
let view = UIView()
self.view = view
}
init() {
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = .white
let redView = UIView()
redView.backgroundColor = .red
redView.translatesAutoresizingMaskIntoConstraints = false
self.view.addSubview(redView)
tableView.translatesAutoresizingMaskIntoConstraints = false
self.view.addSubview(tableView)
let exampleButton = UIButton()
exampleButton.setTitle("test", for: .normal)
exampleButton.translatesAutoresizingMaskIntoConstraints = false
exampleButton.addTarget(menuPresenter, action: #selector(menuPresenter?.buttonPressed), for: .touchUpInside)
self.view.addSubview(exampleButton)
NSLayoutConstraint.activate([
redView.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor),
redView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
redView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
redView.heightAnchor.constraint(equalToConstant: 50),
tableView.topAnchor.constraint(equalTo: redView.bottomAnchor),
tableView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
tableView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
tableView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor),
exampleButton.centerXAnchor.constraint(equalTo: redView.centerXAnchor),
exampleButton.centerYAnchor.constraint(equalTo: redView.centerYAnchor),
exampleButton.heightAnchor.constraint(equalTo: redView.heightAnchor),
exampleButton.widthAnchor.constraint(equalTo: redView.widthAnchor)
])
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
tableView.delegate = self
tableView.dataSource = self
}
}
extension MenuViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
menuPresenter?.showDetail(data: menuPresenter?.data[indexPath.row] ?? "")
tableView.deselectRow(at: indexPath, animated: true)
}
}
extension MenuViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
menuPresenter?.data.count ?? 0
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
cell.textLabel?.text = menuPresenter?.data[indexPath.row]
return cell
}
}
extension MenuViewController {
func set(presenter: MenuPresenterProtocol) {
self.menuPresenter = presenter
}
}
MenuPresenter
@objc protocol MenuPresenterProtocol {
var data: [String] { get }
func buttonPressed()
func showDetail(data: String)
}
class MenuPresenter {
let data = ["a", "b", "c", "d"]
weak private var view: MenuViewController?
private var coordinator: ProjectCoordinator?
@objc func buttonPressed() {
print("Button Pressed")
}
init(coordinator: ProjectCoordinator, view: MenuViewController) {
self.coordinator = coordinator
self.view = view
}
func showDetail(data: String) {
coordinator?.moveToDetail(withData: data)
}
}
extension MenuPresenter: MenuPresenterProtocol { }
We set up the code in the SceneDelegate, this is done in the willConnectTo session
function, although of course we need to also include a UIWindow
property. In order to do so it is possible to replace the relevant function with the following:
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = (scene as? UIWindowScene) else { return }
window = UIWindow(windowScene: windowScene)
let rootNavigationController = UINavigationController()
let container = DependencyFactory(dependencies: .init())
let coordinator = container.makeInitialCoordinator()
coordinator.start(rootNavigationController)
window?.rootViewController = rootNavigationController
window?.makeKeyAndVisible()
}
There are quite a few files in this one!
We can take this file by file, and of course these files are in the accompanying repository!
The Project Coordinator conforms to two protocols, the AbstractCoordinator and the RootCoordinator,
Although child coordinators are not used in this particular project, it makes sense that this is included in this sample project:
AbstractCoordinator
protocol AbstractCoordinator {
func addChildCoordinator(_ coordinator: AbstractCoordinator)
func removeAllChildCoordinatorsWith<T>(type: T.Type)
func removeAllChildCoordinators()
}
RootCoordinator
protocol RootCoordinator: AnyObject {
func start(_ navigationController: UINavigationController)
}
ProjectCoordinator
class ProjectCoordinator: AbstractCoordinator, RootCoordinator {
private(set) var childCoordinators: [AbstractCoordinator] = []
// The reference is weak to prevent a retain cycle
weak var navigationController: UINavigationController?
private var factory: Factory
init(factory: Factory) {
self.factory = factory
}
func addChildCoordinator(_ coordinator: AbstractCoordinator) {
childCoordinators.append(coordinator)
}
func removeAllChildCoordinatorsWith<T>(type: T.Type) {
childCoordinators = childCoordinators.filter { $0 is T == false }
}
func removeAllChildCoordinators() {
childCoordinators.removeAll()
}
/// Start the coordinator, intiializing dependencies
/// - Parameter navigationController: The host UINavigationController
func start(_ navigationController: UINavigationController) {
let vc = factory.makeInitialViewController(coordinator: self)
self.navigationController = navigationController
navigationController.pushViewController(vc, animated: true)
}
func moveToDetail(withData data: String) {
let vc = factory.makeDetailViewController(coordinator: self, data: data)
navigationController?.pushViewController(vc, animated: true)
}
}
The DependencyFactory conforms to the Factory protocol:
protocol Factory {
func makeInitialViewController(coordinator: ProjectCoordinator) -> MenuViewController
func makeDetailViewController(coordinator: ProjectCoordinator, data: String) -> DetailViewController
}
class DependencyFactory: Factory {
func makeDetailViewController(coordinator: ProjectCoordinator, data: String) -> DetailViewController {
let viewController = DetailViewController(data: data)
let presenter = DetailPresenter(view: viewController)
viewController.set(presenter: presenter)
return viewController
}
func makeInitialViewController(coordinator: ProjectCoordinator) -> MenuViewController {
let viewController = MenuViewController()
let presenter = MenuPresenter(coordinator: coordinator, view: viewController)
viewController.set(presenter: presenter)
return viewController
}
struct Dependencies {
// this can be used for Network Managers, and similar
}
var dependencies: Dependencies
init(dependencies: Dependencies) {
self.dependencies = dependencies
}
func makeInitialCoordinator() -> ProjectCoordinator {
let coordinator = ProjectCoordinator(factory: self)
return coordinator
}
}
The view is represented by the View Controllers in this rather trivial example. Note that both of these View Controllers have a function to enable the presenter property to be set from outside the class. DetailViewController
class DetailViewController: UIViewController {
var detailPresenter: DetailPresenter?
var data: String
override func loadView() {
let view = UIView()
view.backgroundColor = .red
self.view = view
}
init(data: String) {
self.data = data
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
let label = UILabel()
label.textAlignment = .center
self.view.addSubview(label)
label.text = data
label.autoresizingMask = [.flexibleWidth, .flexibleHeight]
}
}
extension DetailViewController {
func set(presenter: DetailPresenter) {
self.detailPresenter = presenter
}
}
MenuViewController
class MenuViewController: UIViewController {
let tableView = UITableView()
var menuPresenter: MenuPresenter?
override func loadView() {
let view = UIView()
self.view = view
}
init() {
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = .white
let redView = UIView()
redView.backgroundColor = .red
redView.translatesAutoresizingMaskIntoConstraints = false
self.view.addSubview(redView)
tableView.translatesAutoresizingMaskIntoConstraints = false
self.view.addSubview(tableView)
let exampleButton = UIButton()
exampleButton.setTitle("test", for: .normal)
exampleButton.translatesAutoresizingMaskIntoConstraints = false
exampleButton.addTarget(menuPresenter, action: #selector(menuPresenter?.buttonPressed), for: .touchUpInside)
self.view.addSubview(exampleButton)
NSLayoutConstraint.activate([
redView.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor),
redView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
redView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
redView.heightAnchor.constraint(equalToConstant: 50),
tableView.topAnchor.constraint(equalTo: redView.bottomAnchor),
tableView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
tableView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
tableView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor),
exampleButton.centerXAnchor.constraint(equalTo: redView.centerXAnchor),
exampleButton.centerYAnchor.constraint(equalTo: redView.centerYAnchor),
exampleButton.heightAnchor.constraint(equalTo: redView.heightAnchor),
exampleButton.widthAnchor.constraint(equalTo: redView.widthAnchor)
])
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
tableView.delegate = self
tableView.dataSource = self
}
}
extension MenuViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
menuPresenter?.showDetail(data: menuPresenter?.data[indexPath.row] ?? "")
tableView.deselectRow(at: indexPath, animated: true)
}
}
extension MenuViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
menuPresenter?.data.count ?? 0
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
cell.textLabel?.text = menuPresenter?.data[indexPath.row]
return cell
}
}
extension MenuViewController {
func set(presenter: MenuPresenter) {
self.menuPresenter = presenter
}
}
There are two small presenters within this project, and they should contain the business logic for the views.
MenuPresenter
class MenuPresenter {
let data = ["a", "b", "c", "d"]
weak private var view: MenuViewController?
private var coordinator: ProjectCoordinator?
@objc func buttonPressed() {
print("Button Pressed")
}
init(coordinator: ProjectCoordinator, view: MenuViewController) {
self.coordinator = coordinator
self.view = view
}
func showDetail(data: String) {
coordinator?.moveToDetail(withData: data)
}
}
DetailPresenter
class DetailPresenter {
weak var view: DetailViewController?
init(view: DetailViewController) {
self.view = view
}
}
That about wraps this up! Yes, it is rather a combination of two previous articles, however I hope it has been of help to you particularly if you are considering using MVP in order to implement your App,
What do you think? Do let me know!
If you've any questions, comments or suggestions please hit me up on Twitter