iOS Architecture Patterns
Remove story board dependency
- Remove
Main.storyboardfile - Remove storyboard reference from
Info.plist→ In Scene Configuration findStoryboard Nameand delete it - Go to build settings and remove
UIKit MainStoryboard File Base Namefield - 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
ViewandModelare 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
ViewandModel - 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
- Delegation pattern
- Target-Action pattern
- Observer pattern with
NSNotificationCenter - Observer pattern with
KVO
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:
CRUDoperations facilitated withCoreDataRealmetc. - 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
Modeldoes same things as inMVPandMVC.Viewalso is similar, but binds withViewModelViewModelkeeps 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
UINavigationControllerand UIViewController, but it is contrary to other parts of the book, so I do not know - Similar to
Coordinatorform 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
}
}