Pro Yearly is on sale from $80 to $50! »

Highly maintainable App Architecture

Highly maintainable App Architecture

Talk presented at NSSpain 2017, about LAYERS architecture.
Video: https://vimeo.com/235115845

References multiple projects on my GitHub account:

https://github.com/radianttap/Coordinator

https://github.com/radianttap/Swift-Essentials

https://github.com/radianttap/Swift-Network

Read also blog posts at aplus.rs:
http://aplus.rs/tags/coordinator/

392063da3ef29084374ea1d7ea03d336?s=128

Aleksandar Vacić (Radiant Tap)

September 14, 2017
Tweet

Transcript

  1. Highly maintainable App Architecture Aleksandar Vacić Radiant Tap Belgrade, Serbia

    @radiantav at NSSpain 2017
  2. None
  3. None
  4. UI·UX Data +

  5. None
  6. None
  7. None
  8. And then you di

  9. And then you d

  10. And then you

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

    data And then you
  12. None
  13. When spec-ing out your app… Start with good, clean… Ignore

    the source data!
  14. Data Model • which best supports what 
 the app

    needs to do • fits nicely into the UIKit 
 and iOS SDK in general
  15. managed by DataManager

  16. 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…
  17. API client wrappers

  18. 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 }
  19. None
  20. None
  21. None
  22. None
  23. None
  24. Swift has perfect tool to model these API endpoints: enum

    with associated values
  25. enum Path { case promotions case seasons case products case

    details(styleCode: String) … }
  26. fileprivate var method: HTTPMethod { return .GET } private var

    headers: [String: String] { var h: [String: String] = [:] switch self { default: h["Accept"] = "application/json" } return h }
  27. 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 }
  28. 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 "" } }
  29. 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) }
  30. 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 }
  31. execute(urlRequest, path: path, callback: callback) } func call(path: Path, callback:

    @escaping ServiceCallback) { let urlRequest = path.urlRequest
  32. 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
  33. Networking is easy, completely solved problem on iOS: URLSession &

    friends
  34. How to use URLSession • no shared state – each

    request is independent • each URLRequest is wrapped into NetworkOperation : AsyncOperation • which results in NetworkPayload
  35. 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 } }
  36. struct NetworkPayload { let originalRequest: URLRequest var urlRequest: URLRequest init(urlRequest:

    URLRequest) { self.originalRequest = urlRequest self.urlRequest = urlRequest } ... }
  37. /// 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?
  38. Business Logic

  39. Business Logic aka Middleware

  40. Middleware managers • dependent on DataManager • provide business meaning

    to data instances
  41. None
  42. " # $ % & ' (

  43. • 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.
  44. Benefits • Simplified maintenance • Simple mocking • Each layer

    is 100% independently testable • Easy to handle big differences in source data
  45. Steps to implement • write API wrapper part first •

    write DataManager method and processing code • write middleware Manager method to call its corresponding DataManager
  46. Re-learn the craft of layered architecture

  47. UI

  48. UIrules of engagement 1. should communicate only with middleware 2.

    never communicates directly with API wrappers or networking
  49. None
  50. 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)
  51. 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.
  52. 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
  53. Why would any of these views / controllers even know

    that CartManager exists? *
  54. 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
  55. 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.
  56. • 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.
  57. 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! }
  58. // Local data model var season: Season? { didSet {

    if !self.isViewLoaded { return } updateData() } } var promotedProducts: [Product] = [] { didSet { if !self.isViewLoaded { return } collectionView.reloadData() } }
  59. Where will the connection between the UI and middleware happen?

  60. AppDelegate! No.

  61. Coordinators

  62. 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)
  63. 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
  64. 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.
  65. 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
  66. Each Coordinator then employs Dependency Injection to receive a special

    struct called AppDependency. This is simply a collection of references to middleware Manager instances.
  67. 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? }
  68. // 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 } } } }
  69. open class Coordinator<T: UIViewController>: Coordinating { open let rootViewController: T

    public init(rootViewController: T?) { guard let rvc = rootViewController else { fatalError("Must supply UIViewController") } self.rootViewController = rvc }
  70. All that solves just the input part, from middleware to

    UI. What about the other way..?
  71. None
  72. 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.
  73. 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?) }
  74. What’s the common ancestor for both UIView and UIViewController?

  75. UIResponder

  76. 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
  77. class UIResponder : NSObject { open var next: UIResponder? {

    get } } public extension UIResponder { @objc public var coordinatingResponder: UIResponder? { return next } }
  78. 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) } }
  79. Key: these methods can be used to request stuff by

    going up the chain and can receive stuff back through the callback’s chain.
  80. // 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) } } }
  81. 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 } }
  82. open class Coordinator<T: UIViewController>: 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 } }
  83. 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
  84. Coordinator UI (- , sherlock this, pretty please)

  85. Everything mentioned here is MIT licensed. · Coordinator (with example

    app) · Swift-Essentials · Swift-Network · RTSwiftCoreDataStack https://github.com/radianttap
  86. Thank you. Aleksandar Vacić RadiantTap.com aplus.rs https://github.com/radianttap https://speakerdeck.com/radianttap