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. 2.
  2. 3.
  3. 5.
  4. 6.
  5. 7.
  6. 12.
  7. 14.

    Data Model • which best supports what 
 the app

    needs to do • fits nicely into the UIKit 
 and iOS SDK in general
  8. 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…
  9. 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 }
  10. 19.
  11. 20.
  12. 21.
  13. 22.
  14. 23.
  15. 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 }
  16. 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 }
  17. 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 "" } }
  18. 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) }
  19. 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 }
  20. 31.

    execute(urlRequest, path: path, callback: callback) } func call(path: Path, callback:

    @escaping ServiceCallback) { let urlRequest = path.urlRequest
  21. 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
  22. 34.

    How to use URLSession • no shared state – each

    request is independent • each URLRequest is wrapped into NetworkOperation : AsyncOperation • which results in NetworkPayload
  23. 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 } }
  24. 36.

    struct NetworkPayload { let originalRequest: URLRequest var urlRequest: URLRequest init(urlRequest:

    URLRequest) { self.originalRequest = urlRequest self.urlRequest = urlRequest } ... }
  25. 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?
  26. 41.
  27. 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.
  28. 44.

    Benefits • Simplified maintenance • Simple mocking • Each layer

    is 100% independently testable • Easy to handle big differences in source data
  29. 45.

    Steps to implement • write API wrapper part first •

    write DataManager method and processing code • write middleware Manager method to call its corresponding DataManager
  30. 47.

    UI

  31. 48.

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

    never communicates directly with API wrappers or networking
  32. 49.
  33. 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)
  34. 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.
  35. 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
  36. 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
  37. 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.
  38. 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.
  39. 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! }
  40. 58.

    // Local data model var season: Season? { didSet {

    if !self.isViewLoaded { return } updateData() } } var promotedProducts: [Product] = [] { didSet { if !self.isViewLoaded { return } collectionView.reloadData() } }
  41. 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)
  42. 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
  43. 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.
  44. 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
  45. 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.
  46. 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? }
  47. 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 } } } }
  48. 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 }
  49. 70.
  50. 71.
  51. 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.
  52. 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?) }
  53. 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
  54. 77.

    class UIResponder : NSObject { open var next: UIResponder? {

    get } } public extension UIResponder { @objc public var coordinatingResponder: UIResponder? { return next } }
  55. 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) } }
  56. 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.
  57. 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) } } }
  58. 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 } }
  59. 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 } }
  60. 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
  61. 85.

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

    app) · Swift-Essentials · Swift-Network · RTSwiftCoreDataStack https://github.com/radianttap