iOS Architecture Patterns

Remove story board dependency

  1. Remove Main.storyboard file
  2. Remove storyboard reference from Info.plist → In Scene Configuration find Storyboard Name and delete it
  3. Go to build settings and remove UIKit MainStoryboard File Base Name field
  4. Create a window in Scene Delegate
func scene(_ scene: UIScene,
           willConnectTo session: UISceneSession,
           options connectionOptions: UIScene.ConnectionOptions) {
    
    guard let scene = (scene as? UIWindowScene) else { return }
    
    let window = UIWindow(windowScene: scene)
    window.rootViewController = ViewController()
    window.makeKeyAndVisible()
    self.window = window
}

Dependency injection

Explanation of the code under this link

public protocol InjectionKey {
    associatedtype Value
    static var currentValue: Self.Value { get set }
}
struct InjectedValues {
    private static var current = InjectedValues()
    
    static subscript<K>(key: K.Type) -> K.Value where K : InjectionKey {
        get { key.currentValue }
        set { key.currentValue = newValue }
    }
    
    static subscript<T>(_ keyPath: WritableKeyPath<InjectedValues, T>) -> T {
        get { current[keyPath: keyPath] }
        set { current[keyPath: keyPath] = newValue }
    }
}
@propertyWrapper
struct Injected<T> {
    private let keyPath: WritableKeyPath<InjectedValues, T>
    var wrappedValue: T {
        get { InjectedValues[keyPath] }
        set { InjectedValues[keyPath] = newValue }
    }
    
    init(_ keyPath: WritableKeyPath<InjectedValues, T>) {
        self.keyPath = keyPath
    }
}

Define dependency

private struct UsersRepositoryKey: InjectionKey {
    static var currentValue: AnyUsersRepository = UsersRepository()
}

extension InjectedValues {
    var usersRepository: AnyUsersRepository {
        get { Self[UsersRepositoryKey.self] }
        set { Self[UsersRepositoryKey.self] = newValue }
    }
}

protocol AnyUsersRepository {
    func getUsers(_ result: @escaping (Result<[User], Error>)->Void)
}

class UsersRepository: AnyUsersRepository {
    func getUsers(_ result: @escaping (Result<[User], Error>)->Void) {
        <#Implementation#>
    }
}
@Injected(\.usersRepository) var usersRepository: AnyUsersRepository
InjectedValues[\.usersRepository] = MockedUsersRepository()

Model-View-Controller

Clasic version

  • View and Model are linked together, so reusability is reduced.
  • Note: Views in iOS apps are quite often reusable.

{% svg ../svgs/classic-mvc.svg class="center-image" %}

Apple version

{% svg ../svgs/apple-mvc.svg class="center-image" %}

Model responsibilities:

  • Business logic
  • Accessing and manipulating data
  • Persistence
  • Communication/Networking
  • Parsing
  • Extensions and helper classes
  • Communication with models

Note: The Model must not communicate directly with the View. The Controller is the link between those

View responsibilities:

  • Animations, drawings (UIView, CoreAnimation, CoreGraphics)
  • Show data that controller sends
  • Might receive user input

Controller responsibilities:

  • Exchange data between View and Model
  • Receive user actions and interruptions or signals from the outside the app
  • Handles the view life cycle

Advantages

  • Simple and usually less code
  • Fast development for simple apps

Disadvantages

  • Controllers coupled views
  • Massive ViewControllers

Communication between components

Model-View-Presenter

In this design pattern View is implemented with classes UIView and UIViewController. The UIViewController has less responsibilities which are limited to:

  • Routing/Coordination
  • Navigation
  • Passing informations via a delegation pattern

View

class ExampleController: UIViewController {
    private let exampleView = ExampleView()
    override func loadView() {
        super.loadView()
        setup()
    }
    private func setup() {
        let presenter = ExamplePresenter(exampleView)
        exampleView.presenter = presenter
        exampleView.setupView()
        self.view = exampleView
    }
}

Presenter

protocol ExampleViewDelegate {
    func updateView()
}
class ExamplePresenter {
    private weak var exampleView: ExampleViewDelegate?
    init(_ exampleView: ExampleViewDelegate) {
        self.exampleView = exampleView
    }
}

Advantages

  • Easier to test business logic
  • Better separation of responsibilities

Disadvantages

  • Usually not a better choice for smaller projects
  • Presenters might become massive
  • Controllers still handle navigation. Possible solutions → extend the pattern with Router or Coordinator.

Common layers

  • Data access layer: CRUD operations facilitated with CoreData Realm etc.
  • Services: Classes that interacts with database entities, like retrieve data, transform them into objects.
  • Extensions/utils

Model-View-ViewModel

ViewModel has no references to the view.

{% svg ../svgs/mvvm.svg class="center-image" %}

Binding is done using: Combine Framework, RxSwift, Bond or KVO or using delegation pattern

  • Model does same things as in MVP and MVC.
  • View also is similar, but binds with ViewModel
  • ViewModel keeps updated state of the view, and process data for i

Advantages

  • Better reparation of responsibilities
  • Better testability, without needing to take into account the views

Disadvantages

  • Might be slower and introduce dependency on external libraries
  • Harder to learn and can become complex

Extension with Coordinator MVVM-C

Role of Coordinator is to manage navigation flow.

protocol Coordinator {
    var navigationController: UINavigationController { get set }
    func start()
}

and an example implementation

class ExampleCoordinator: Coordinator {
    var navigationController: UINavigationController
    init(navigationController: UINavigationController) {
        self.navigationController = UINavigationController
    }

    func start() {
        let viewModel = ExampleViewModel(someService: SomeService(),
                                         coordinator: self)
        navigationController.pushViewController(ExampleController(viewModel),
                                                animated: true)
    }

    func showList(_ list: ExampleListModel) {
        let listCoordinator = ListCoordinator(navigationController: navigationController
                                              list: list)
        listCoordinator.start()
    }
}

In the book I am reading the author created an ExampleCoordinatorProtocol with a func showList(_ list: ExampleListModel) where the ExampleCoordinator implemented it. I think it does not make any sense, however if we might want to inject the coordinator then we might want to relay on an abstraction.

func scene(_ scene: UIScene,
           willConnectTo session: UISceneSession,
           options connectionOptions: UIScene.ConnectionOptions) {
    
    guard let scene = (scene as? UIWindowScene) else { return }
    
    let window = UIWindow(windowScene: scene)
    let navigationController = UINavigationController()
    exampleCoordinator = ExampleCoordinator(navigationController: navigationController)
    exampleCoordinator?.start()
    window.rootViewController = navigationController
    window.makeKeyAndVisible()
    self.window = window
}

VIPER

{% svg ../svgs/viper-ownership.svg class="center-image" %}

View

It includes UIViewController

  • Made only to preserve elements like Buttons Labels
  • It sends informations to presenters, and receive messages what to show and knows how

Interactor

  • Receives informations form databases, servers etc.
  • The book says that the Interactor receive actions from presenter, and returns the result via Delegation Pattern.
  • The interactor never sends entities to the Presenter

Presenter

  • Is in the centre and serves as a link
  • Process events from the view and requests data from the Interctor. It receives that as primitives, never Entities.
  • it handles navigation to the other screens using the Router

Entity

  • Simple models usually data structures
  • They can only be used by the Interactor

Router

  • Creates screens
  • Handles navigation, but itself does not know where to go to.
  • The book says it is the owner of the UINavigationController and UIViewController, but it is contrary to other parts of the book, so I do not know
  • Similar to Coordinator form MVVM-C

Entity.swift

struct User: Codable {
    let name: String
}

Interactor.swift

enum FetchError: Error {
    case failed
}

protocol AnyInteractor {
    var presenter: AnyPresenter? { get set }
    
    func getUsers()
}

class UserInteractor: AnyInteractor {
    @Injected(\.usersRepository) var usersRepository: AnyUsersRepository
    
    var presenter: AnyPresenter?
    
    func getUsers() {
        usersRepository.getUsers { [weak self] in self?.presenter?.interactorDidFetchUsers(with: $0) }
    }
}

Presenter.swift

protocol AnyPresenter {
    var router: AnyRouter? { get set }
    var interactor: AnyInteractor? { get set }
    var view: AnyView? { get set }
    
    func interactorDidFetchUsers(with result: Result<[User], Error>)
}

class UserPresenter: AnyPresenter {
    func interactorDidFetchUsers(with result: Result<[User], Error>) {
        switch result {
        case let .success(users):
            view?.update(with: users)
        case let .failure(error):
            view?.update(with: error.localizedDescription)
        }
    }
    
    var router: AnyRouter?
    
    var interactor: AnyInteractor? {
        didSet {
            interactor?.getUsers()
        }
    }
    
    var view: AnyView?
}

Router.swift

typealias EntryPoint = AnyView & UIViewController

protocol AnyRouter {
    var entry: EntryPoint? { get }
    static func start() -> AnyRouter
}

class UserRouter: AnyRouter {
    var entry: EntryPoint?
    
    
    static func start() -> AnyRouter {
        let router = UserRouter()
        
        var view: AnyView = UserViewController()
        var presenter: AnyPresenter = UserPresenter()
        var interactor: AnyInteractor = UserInteractor()
        
        view.presenter = presenter
        
        interactor.presenter = presenter
        
        presenter.router = router
        presenter.view = view
        presenter.interactor = interactor
        
        router.entry = view as? EntryPoint
        
        return router
    }
}



//There are a few retain cycles with view, presenter, router and interactor. One option you can do is to make those protocols conforms to AnyObject, and mark these references as "weak":
//1. router's ref to presenter
//2. router's ref to view
//3. presenter's ref to view
//4. interactor's ref to presenter

View.swift

protocol AnyView {
    var presenter: AnyPresenter? { get set }
    
    func update(with users: [User])
    func update(with error: String)
}

class UserViewController: UIViewController, AnyView {
    
    
    var presenter: AnyPresenter?
    
    private let tableView: UITableView = {
        let tableView = UITableView()
        tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
        tableView.isHidden = true
        return tableView
    }()
    
    var users = [User]()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        view.addSubview(tableView)
        tableView.delegate = self
        tableView.dataSource = self
    }
    
    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        tableView.frame = view.bounds
    }
    
    func update(with users: [User]) {
        DispatchQueue.main.async {
            self.users = users
            self.tableView.reloadData()
            self.tableView.isHidden = false
        }
    }
    
    func update(with error: String) {
        
    }
}


extension UserViewController: UITableViewDelegate, UITableViewDataSource {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        users.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
        cell.textLabel?.text = users[indexPath.row].name
        return cell
    }
}