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 full-size slide

  2. UI·UX Data
    +

    View full-size slide

  3. And then you di

    View full-size slide

  4. And then you d

    View full-size slide

  5. And then you

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  8. Data Model
    • which best supports what 

    the app needs to do
    • fits nicely into the UIKit 

    and iOS SDK in general

    View full-size slide

  9. managed by DataManager

    View full-size slide

  10. 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 full-size slide

  11. API client wrappers

    View full-size slide

  12. 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 full-size slide

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

    View full-size slide

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

    }

    View full-size slide

  15. 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 full-size slide

  16. 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 full-size slide

  17. 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 full-size slide

  18. 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 full-size slide

  19. 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 full-size slide

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

    View full-size slide

  21. 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 full-size slide

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

    View full-size slide

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

    View full-size slide

  24. 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 full-size slide

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

    View full-size slide

  26. /// 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 full-size slide

  27. Business Logic

    View full-size slide

  28. Business Logic aka Middleware

    View full-size slide

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

    View full-size slide


  30. " # $ % & ' (

    View full-size slide

  31. • 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 full-size slide

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

    View full-size slide

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

    View full-size slide

  34. Re-learn the craft of
    layered architecture

    View full-size slide

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

    View full-size slide

  36. 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 full-size slide

  37. 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 full-size slide

  38. 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 full-size slide

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

    View full-size slide

  40. 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 full-size slide

  41. 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 full-size slide

  42. • 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 full-size slide

  43. 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 full-size slide

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

    View full-size slide

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

    View full-size slide

  46. AppDelegate!
    No.

    View full-size slide

  47. Coordinators

    View full-size slide

  48. 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 full-size slide

  49. 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 full-size slide

  50. 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 full-size slide

  51. 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 full-size slide

  52. 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 full-size slide

  53. 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 full-size slide

  54. // 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 full-size slide

  55. 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 full-size slide

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

    View full-size slide

  57. 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 full-size slide

  58. 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 full-size slide

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

    View full-size slide

  60. 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 full-size slide

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

    View full-size slide

  62. 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 full-size slide

  63. 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 full-size slide

  64. // 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 full-size slide

  65. 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 full-size slide

  66. 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 full-size slide

  67. 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 full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide