Slide 1

Slide 1 text

Highly maintainable App Architecture Aleksandar Vacić Radiant Tap Belgrade, Serbia @radiantav at NSSpain 2017

Slide 2

Slide 2 text

No content

Slide 3

Slide 3 text

No content

Slide 4

Slide 4 text

UI·UX Data +

Slide 5

Slide 5 text

No content

Slide 6

Slide 6 text

No content

Slide 7

Slide 7 text

No content

Slide 8

Slide 8 text

And then you di

Slide 9

Slide 9 text

And then you d

Slide 10

Slide 10 text

And then you

Slide 11

Slide 11 text

And then you experience the joy of enterprise company’s production data And then you

Slide 12

Slide 12 text

No content

Slide 13

Slide 13 text

When spec-ing out your app… Start with good, clean… Ignore the source data!

Slide 14

Slide 14 text

Data Model • which best supports what 
 the app needs to do • fits nicely into the UIKit 
 and iOS SDK in general

Slide 15

Slide 15 text

managed by DataManager

Slide 16

Slide 16 text

DataManager • converts raw data into your Data Model types (and vice-versa) • handles data persistence • does not care where the raw data is coming from Usually it’s from various APIs, so you need…

Slide 17

Slide 17 text

API client wrappers

Slide 18

Slide 18 text

final class IvkoService { static let shared = IvkoService() private init() { urlSessionConfiguration = { let c = URLSessionConfiguration.default … return c }() queue = { let oq = OperationQueue() oq.qualityOfService = .userInitiated return oq }() } fileprivate var urlSessionConfiguration: URLSessionConfiguration fileprivate var queue: OperationQueue }

Slide 19

Slide 19 text

No content

Slide 20

Slide 20 text

No content

Slide 21

Slide 21 text

No content

Slide 22

Slide 22 text

No content

Slide 23

Slide 23 text

No content

Slide 24

Slide 24 text

Swift has perfect tool to model these API endpoints: enum with associated values

Slide 25

Slide 25 text

enum Path { case promotions case seasons case products case details(styleCode: String) … }

Slide 26

Slide 26 text

fileprivate var method: HTTPMethod { return .GET } private var headers: [String: String] { var h: [String: String] = [:] switch self { default: h["Accept"] = "application/json" } return h }

Slide 27

Slide 27 text

private var fullURL: URL { var url = IvkoService.baseURL switch self { case .promotions: url.appendPathComponent("slides.json") case .seasons: url.appendPathComponent("seasons.json") case .products: url.appendPathComponent("products.json") case .details: url.appendPathComponent("details") } return url }

Slide 28

Slide 28 text

private var params: [String: String] { var p : [String: String] = [:] switch self { case .details(let styleCode): p["style"] = styleCode default: break } return p } private var encodedParams: String { switch self { case .details: return queryEncoded(params: params) default: return "" } }

Slide 29

Slide 29 text

private func queryEncoded(params: [String: String]) -> String { if params.count == 0 { return "" } var arr = [String]() for (key, value) in params { let s = "\(key)=\(value)" arr.append(s) } return arr.joined(separator: "&") } private func jsonEncoded(params: JSON) -> Data? { return try? JSONSerialization.data(withJSONObject: params) }

Slide 30

Slide 30 text

fileprivate var urlRequest: URLRequest { guard var components = URLComponents(url: fullURL, resolvingAgainstBaseURL: false) else { fatalError("Invalid URL") } switch method { case .GET: components.query = queryEncoded(params: params) default: break } guard let url = components.url else { fatalError("Invalid URL") } var r = URLRequest(url: url) r.httpMethod = method.rawValue r.allHTTPHeaderFields = headers switch method { case .POST: r.httpBody = jsonEncoded(params: params) break default: break } return r }

Slide 31

Slide 31 text

execute(urlRequest, path: path, callback: callback) } func call(path: Path, callback: @escaping ServiceCallback) { let urlRequest = path.urlRequest

Slide 32

Slide 32 text

execute(urlRequest, path: path, callback: callback) } func call(path: Path, callback: @escaping ServiceCallback) { let urlRequest = path.urlRequest • apply OAuth • manage session cookies • download / upload details
 etc

Slide 33

Slide 33 text

Networking is easy, completely solved problem on iOS: URLSession & friends

Slide 34

Slide 34 text

How to use URLSession • no shared state – each request is independent • each URLRequest is wrapped into NetworkOperation : AsyncOperation • which results in NetworkPayload

Slide 35

Slide 35 text

final class NetworkOperation: AsyncOperation { init(urlRequest: URLRequest, urlSessionConfiguration: URLSessionConfiguration = URLSessionConfiguration.default, callback: @escaping (NetworkPayload) -> Void) { self.payload = NetworkPayload(urlRequest: urlRequest) self.callback = callback self.urlSessionConfiguration = urlSessionConfiguration super.init() self.qualityOfService = .utility } }

Slide 36

Slide 36 text

struct NetworkPayload { let originalRequest: URLRequest var urlRequest: URLRequest init(urlRequest: URLRequest) { self.originalRequest = urlRequest self.urlRequest = urlRequest } ... }

Slide 37

Slide 37 text

/// Any error that URLSession may populate /// (timeouts, no connection etc) var error: NetworkError? /// Received HTTP response. /// Process status code and headers var response: HTTPURLResponse? /// Received stream of bytes var data: Data?

Slide 38

Slide 38 text

Business Logic

Slide 39

Slide 39 text

Business Logic aka Middleware

Slide 40

Slide 40 text

Middleware managers • dependent on DataManager • provide business meaning to data instances

Slide 41

Slide 41 text

No content

Slide 42

Slide 42 text

" # $ % & ' (

Slide 43

Slide 43 text

• Middleware (Business Logic) • Data Wrangling (Management) • Data Sourcing • Networking (if needed) This layered approach is amazingly flexible Each of these layers have a strictly defined role and as such they are very suitable to automated testing, easy to debug and mock etc.

Slide 44

Slide 44 text

Benefits • Simplified maintenance • Simple mocking • Each layer is 100% independently testable • Easy to handle big differences in source data

Slide 45

Slide 45 text

Steps to implement • write API wrapper part first • write DataManager method and processing code • write middleware Manager method to call its corresponding DataManager

Slide 46

Slide 46 text

Re-learn the craft of layered architecture

Slide 47

Slide 47 text

UI

Slide 48

Slide 48 text

UIrules of engagement 1. should communicate only with middleware 2. never communicates directly with API wrappers or networking

Slide 49

Slide 49 text

No content

Slide 50

Slide 50 text

To complete the purchase, ideally I should write something like: Simple, readable, easy to explain. let product = promotedProducts[indexPath.item] cartAddProduct(product) ... cartBuyNow(product, sender: self)

Slide 51

Slide 51 text

This is not crazy talk, UIKit already works like this: Side-note: if you are still writing self.navigationController.pushViewController(vc) — please stop (unless you still need to support iOS 7) Call show(vc, sender: self) from any darn-deep level in your UIVC hierarchy and it automagically slides from the side or is presented.

Slide 52

Slide 52 text

Poor PromoContainerCell – it acts like both delegate and delegator for an action (cartBuyNow) that it cares nothing about. PromoCell().delegate = PromoContainerCell() ... PromoContainerCell().delegate = HomeController() Solution: Delegate pattern

Slide 53

Slide 53 text

Why would any of these views / controllers even know that CartManager exists? *

Slide 54

Slide 54 text

UIViewController role • should not know nor care about the hierarchy it’s presented in (is it inside the nav, split, popover, whatever) • It must be able to receive data it needs or ask for it, without knowing who exactly is delivering the data • It should be able to send info about events that happened in it — button taps, row/cell taps, swipes, any kind of interaction — to its semantic parent

Slide 55

Slide 55 text

Dependency Injection = (stored) properties If the Controller is able to feed its View(s) using only its local Model then you have properly implemented MVC.

Slide 56

Slide 56 text

• Embrace MVC. It’s great pattern, easy to explain, easy to use. • Use MVC properly. Use it for one UI entity only, to coordinate between its local data model and the view. • Think thrice before reaching out to anything else.

Slide 57

Slide 57 text

final class HomeController: UIViewController, StoryboardLoadable { // UI Outlets @IBOutlet fileprivate weak var collectionView: UICollectionView! @IBOutlet fileprivate weak var notificationContainer: UIView! @IBOutlet fileprivate weak var cartBarItem: UIBarButtonItem! }

Slide 58

Slide 58 text

// Local data model var season: Season? { didSet { if !self.isViewLoaded { return } updateData() } } var promotedProducts: [Product] = [] { didSet { if !self.isViewLoaded { return } collectionView.reloadData() } }

Slide 59

Slide 59 text

Where will the connection between the UI and middleware happen?

Slide 60

Slide 60 text

AppDelegate! No.

Slide 61

Slide 61 text

Coordinators

Slide 62

Slide 62 text

Coordinators • completely solve the data flow in the UI layer. • don’t hold nor contain any sort of app’s data • their primary concern is to shuffle data from middleware into front-end UI
 (and vice-versa)

Slide 63

Slide 63 text

Coordinator role • Create instances of the VCs • Show or hide VCs • Configure VCs (set DI properties) • Receive data requests from VC • Route requests to middleware • Route results back to VC

Slide 64

Slide 64 text

How to use them • Create Application Coordinator, strongly referenced from AppDelegate • It’s the only Coordinator that is always in memory • It creates and holds instances of all the middleware Managers, like API wrapper, DataManager, CartManager etc.

Slide 65

Slide 65 text

Coordinators = big-picture sections of your app Any (non-App) Coordinator can have children and can be a child of other Coordinators. App → Catalog → Payment App → Account → Payment App → Catalog → Cart → Payment App → Payment

Slide 66

Slide 66 text

Each Coordinator then employs Dependency Injection to receive a special struct called AppDependency. This is simply a collection of references to middleware Manager instances.

Slide 67

Slide 67 text

struct AppDependency { var apiManager: IvkoService? var dataManager: DataManager? var assetManager: AssetManager? var accountManager: AccountManager? var cartManager: CartManager? var catalogManager: CatalogManager? var keychainProvider: Keychain? var persistanceProvider: RTCoreDataStack? var moc: NSManagedObjectContext? }

Slide 68

Slide 68 text

// Protocol you need to apply to all the Coordinators, // so the new `dependencies` value is propagated down protocol NeedsDependency: class { var dependencies: AppDependency? { get set } } extension NeedsDependency where Self: Coordinating { func updateChildCoordinatorDependencies() { self.childCoordinators.forEach { (_, coordinator) in if let c = coordinator as? NeedsDependency { c.dependencies = dependencies } } } }

Slide 69

Slide 69 text

open class Coordinator: Coordinating { open let rootViewController: T public init(rootViewController: T?) { guard let rvc = rootViewController else { fatalError("Must supply UIViewController") } self.rootViewController = rvc }

Slide 70

Slide 70 text

All that solves just the input part, from middleware to UI. What about the other way..?

Slide 71

Slide 71 text

No content

Slide 72

Slide 72 text

Looking for solution to any architectural issue, it’s good practice to first look at your framework of choice. Never fight the SDK. Be friendly to it.

Slide 73

Slide 73 text

Lo and behold, UIKit already has very similar functionality; I mentioned it some minutes ago: Not directly usable though, but it does lead us to solution… class UIViewController { func show(_ viewController: UIViewController, sender: Any?) }

Slide 74

Slide 74 text

What’s the common ancestor for both UIView and UIViewController?

Slide 75

Slide 75 text

UIResponder

Slide 76

Slide 76 text

UIResponder 1. class Coordinator: UIResponder {…} 2. find a replacement for parent that spans both UIView and UIViewController 3. make sure that replacement works with Coordinator too

Slide 77

Slide 77 text

class UIResponder : NSObject { open var next: UIResponder? { get } } public extension UIResponder { @objc public var coordinatingResponder: UIResponder? { return next } }

Slide 78

Slide 78 text

extension UIResponder { func cartBuyNow(_ product: Product, sender: Any?) { coordinatingResponder?.cartBuyNow(product, sender: sender) } func cartAdd(product: Product, color: ColorBox, sender: Any?, completion: @escaping (Bool, Int) -> Void) { coordinatingResponder?.cartAdd(product: product, color: color, sender: sender, completion: completion) } }

Slide 79

Slide 79 text

Key: these methods can be used to request stuff by going up the chain and can receive stuff back through the callback’s chain.

Slide 80

Slide 80 text

// Inject parentCoordinator property // into all UIViewControllers extension UIViewController { private struct AssociatedKeys { static var ParentCoordinator = "ParentCoordinator" } public weak var parentCoordinator: Coordinating? { get { return objc_getAssociatedObject(self, &AssociatedKeys.ParentCoordinator) as? Coordinating } set { objc_setAssociatedObject(self, &AssociatedKeys.ParentCoordinator, newValue, .OBJC_ASSOCIATION_ASSIGN) } } }

Slide 81

Slide 81 text

extension UIViewController { override open var coordinatingResponder: UIResponder? { guard let parentCoordinator = self.parentCoordinator else { guard let parentController = self.parent else { return view.superview } return parentController as UIResponder } return parentCoordinator as? UIResponder } }

Slide 82

Slide 82 text

open class Coordinator: UIResponder, Coordinating { open let rootViewController: T public init(rootViewController: T?) { guard let rvc = rootViewController else { fatalError("Must supply UIViewController”) } self.rootViewController = rvc super.init() rvc.parentCoordinator = self } open weak var parent: Coordinating? open override var coordinatingResponder: UIResponder? { return parent as? UIResponder } }

Slide 83

Slide 83 text

Greatest benefit: each layer and component has clear input and output. Which allows you to mind-map entire app before even starting to code. LAYERS architecture

Slide 84

Slide 84 text

Coordinator UI (- , sherlock this, pretty please)

Slide 85

Slide 85 text

Everything mentioned here is MIT licensed. · Coordinator (with example app) · Swift-Essentials · Swift-Network · RTSwiftCoreDataStack https://github.com/radianttap

Slide 86

Slide 86 text

Thank you. Aleksandar Vacić RadiantTap.com aplus.rs https://github.com/radianttap https://speakerdeck.com/radianttap