Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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/

Aleksandar Vacić (Radiant Tap)

September 14, 2017
Tweet

More Decks by Aleksandar Vacić (Radiant Tap)

Other Decks in Technology

Transcript

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

    View Slide

  2. View Slide

  3. View Slide

  4. UI·UX Data
    +

    View Slide

  5. View Slide

  6. View Slide

  7. View Slide

  8. And then you di

    View Slide

  9. And then you d

    View Slide

  10. And then you

    View Slide

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

    View Slide

  12. View Slide

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

    View Slide

  14. Data Model
    • which best supports what 

    the app needs to do
    • fits nicely into the UIKit 

    and iOS SDK in general

    View Slide

  15. managed by DataManager

    View Slide

  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…

    View Slide

  17. API client wrappers

    View Slide

  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
    }

    View Slide

  19. View Slide

  20. View Slide

  21. View Slide

  22. View Slide

  23. View Slide

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

    View Slide

  25. enum Path {
    case promotions
    case seasons
    case products
    case details(styleCode: String)

    }

    View Slide

  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
    }

    View Slide

  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
    }

    View Slide

  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 ""
    }
    }

    View Slide

  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)
    }

    View Slide

  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
    }

    View Slide

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

    View Slide

  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

    View Slide

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

    View Slide

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

    View Slide

  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
    }
    }

    View Slide

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

    View Slide

  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?

    View Slide

  38. Business Logic

    View Slide

  39. Business Logic aka Middleware

    View Slide

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

    View Slide

  41. View Slide


  42. " # $ % & ' (

    View Slide

  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.

    View Slide

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

    View Slide

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

    View Slide

  46. Re-learn the craft of
    layered architecture

    View Slide

  47. UI

    View Slide

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

    View Slide

  49. View Slide

  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)

    View Slide

  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.

    View Slide

  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

    View Slide

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

    View Slide

  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

    View Slide

  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.

    View Slide

  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.

    View Slide

  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!
    }

    View Slide

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

    View Slide

  59. Where will the connection between
    the UI and middleware happen?

    View Slide

  60. AppDelegate!
    No.

    View Slide

  61. Coordinators

    View Slide

  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)

    View Slide

  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

    View Slide

  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.

    View Slide

  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

    View Slide

  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.

    View Slide

  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?
    }

    View Slide

  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
    }
    }
    }
    }

    View Slide

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

    View Slide

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

    View Slide

  71. View Slide

  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.

    View Slide

  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?)
    }

    View Slide

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

    View Slide

  75. UIResponder

    View Slide

  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

    View Slide

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

    View Slide

  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)
    }
    }

    View Slide

  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.

    View Slide

  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)
    }
    }
    }

    View Slide

  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
    }
    }

    View Slide

  82. 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
    }
    }

    View Slide

  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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide