658 lines
16 KiB
Markdown
658 lines
16 KiB
Markdown
---
|
|
layout: default
|
|
title: iOS Architecture Patterns
|
|
categories: swift
|
|
---
|
|
### Remove story board dependency
|
|
|
|
1. Remove `Main.storyboard` file
|
|
1. Remove storyboard reference from `Info.plist` → In Scene Configuration find `Storyboard Name` and delete it
|
|
1. Go to build settings and remove `UIKit MainStoryboard File Base Name` field
|
|
1. Create a window in Scene Delegate
|
|
|
|
```swift
|
|
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
|
|
|
|
<!-- https://www.swiftbysundell.com/tips/testing-code-that-uses-static-apis/ -->
|
|
|
|
[Explanation of the code under this link](https://www.avanderlee.com/swift/dependency-injection/)
|
|
|
|
|
|
```swift
|
|
public protocol InjectionKey {
|
|
associatedtype Value
|
|
static var currentValue: Self.Value { get set }
|
|
}
|
|
```
|
|
|
|
```swift
|
|
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 }
|
|
}
|
|
}
|
|
```
|
|
|
|
```swift
|
|
@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
|
|
|
|
```swift
|
|
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#>
|
|
}
|
|
}
|
|
```
|
|
|
|
```swift
|
|
@Injected(\.usersRepository) var usersRepository: AnyUsersRepository
|
|
```
|
|
|
|
```swift
|
|
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.
|
|
|
|
<p>
|
|
{% svg ../svgs/classic-mvc.svg class="center-image" %}
|
|
</p>
|
|
|
|
#### Apple version
|
|
|
|
<p>
|
|
{% svg ../svgs/apple-mvc.svg class="center-image" %}
|
|
</p>
|
|
|
|
**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 `ViewController`s
|
|
|
|
#### Communication between components
|
|
|
|
- [Delegation](https://developer.apple.com/library/archive/documentation/General/Conceptual/Devpedia-CocoaApp/TargetAction.html) pattern
|
|
- [Target-Action](https://developer.apple.com/library/archive/documentation/General/Conceptual/Devpedia-CocoaApp/TargetAction.html) pattern
|
|
- [Observer](https://developer.apple.com/documentation/foundation/nsnotificationcenter) pattern with `NSNotificationCenter`
|
|
- [Observer](https://developer.apple.com/documentation/swift/using-key-value-observing-in-swift) 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
|
|
|
|
```swift
|
|
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
|
|
|
|
```swift
|
|
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.
|
|
|
|
<p>
|
|
{% svg ../svgs/mvvm.svg class="center-image" %}
|
|
</p>
|
|
|
|
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.
|
|
|
|
```swift
|
|
protocol Coordinator {
|
|
var navigationController: UINavigationController { get set }
|
|
func start()
|
|
}
|
|
```
|
|
|
|
and an example implementation
|
|
|
|
```swift
|
|
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.
|
|
|
|
```swift
|
|
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
|
|
|
|
<p>
|
|
{% svg ../svgs/viper-ownership.svg class="center-image" %}
|
|
</p>
|
|
|
|
|
|
#### 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
|
|
|
|
|
|
<!--
|
|
https://github.com/ochococo/Design-Patterns-In-Swift
|
|
|
|
https://nalexn.github.io/clean-architecture-swiftui/
|
|
https://medium.com/@vladislavshkodich/architectures-comparing-for-swiftui-6351f1fb3605
|
|
|
|
-->
|
|
|
|
**`Entity.swift`**
|
|
|
|
```swift
|
|
struct User: Codable {
|
|
let name: String
|
|
}
|
|
```
|
|
|
|
**`Interactor.swift`**
|
|
|
|
```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`**
|
|
|
|
```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`**
|
|
|
|
```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`**
|
|
|
|
```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
|
|
}
|
|
}
|
|
```
|
|
|
|
|
|
<!--
|
|
RIBs
|
|
https://github.com/uber/RIBs
|
|
https://medium.com/swlh/ios-architecture-exploring-ribs-3db765284fd8
|
|
https://github.com/uber/RIBs/wiki
|
|
-->
|
|
|
|
<!--
|
|
redux
|
|
https://medium.com/mackmobile/getting-started-with-redux-in-swift-54e00f323e2b
|
|
-->
|
|
|
|
<!--
|
|
|
|
# Factory
|
|
|
|
```swift
|
|
protocol ImageReader {
|
|
func getDecodeImage() -> DecodedImage
|
|
}
|
|
|
|
class DecodedImage {
|
|
private var image: String
|
|
|
|
init(image: String) {
|
|
self.image = image
|
|
}
|
|
|
|
var description: String {
|
|
"\(image): is decoded"
|
|
}
|
|
}
|
|
|
|
class GifReader: ImageReader {
|
|
private var decodedImage: DecodedImage
|
|
|
|
init(image: String) {
|
|
self.decodedImage = DecodedImage(image: image)
|
|
}
|
|
|
|
func getDecodeImage() -> DecodedImage {
|
|
decodedImage
|
|
}
|
|
}
|
|
|
|
class JpegReader: ImageReader {
|
|
private var decodedImage: DecodedImage
|
|
|
|
init(image: String) {
|
|
decodedImage = DecodedImage(image: image)
|
|
}
|
|
|
|
func getDecodeImage() -> DecodedImage {
|
|
decodedImage
|
|
}
|
|
}
|
|
|
|
func runFactoryExample() {
|
|
let reader: ImageReader
|
|
let format = "gif"
|
|
let image = "example image"
|
|
|
|
switch format {
|
|
case "gif":
|
|
reader = GifReader(image: image)
|
|
default:
|
|
reader = JpegReader(image: image)
|
|
}
|
|
|
|
let decodedImage = reader.getDecodeImage()
|
|
print(decodedImage.description)
|
|
}
|
|
```
|
|
|
|
|
|
```swift
|
|
|
|
protocol Observer<ValueType> {
|
|
associatedtype ValueType
|
|
func update(value: ValueType)
|
|
}
|
|
|
|
struct Subject<T> {
|
|
private var observers: [(T) -> Void] = []
|
|
|
|
mutating func attach<O: Observer>(observer: O) where O.ValueType == T {
|
|
observers.append { observer.update(value: $0) }
|
|
}
|
|
|
|
func notyfi(value: T) {
|
|
for observer in observers {
|
|
observer(value)
|
|
}
|
|
}
|
|
}
|
|
|
|
class ConcreteObserver: Observer {
|
|
func update(value: String) {
|
|
print("received: \(value)")
|
|
}
|
|
}
|
|
|
|
func runObserverExample() {
|
|
var subject = Subject<String>()
|
|
|
|
let observer1 = ConcreteObserver()
|
|
subject.attach(observer: observer1)
|
|
|
|
let observer2 = ConcreteObserver()
|
|
subject.attach(observer: observer2)
|
|
|
|
subject.notyfi(value: "some string")
|
|
}
|
|
|
|
// Version with more modern syntax
|
|
/*
|
|
protocol Observer<ValueType> {
|
|
associatedtype ValueType
|
|
func update(value: ValueType)
|
|
}
|
|
|
|
struct Subject<T> {
|
|
private var observers = Array<any Observer<T>>()
|
|
|
|
mutating func attach(observer: any Observer<T>) {
|
|
observers.append(observer)
|
|
}
|
|
|
|
func notify(value: T) {
|
|
for observer in observers {
|
|
observer.update(value: value)
|
|
}
|
|
}
|
|
}
|
|
*/
|
|
|
|
```
|
|
-->
|