Architecting Alive Apps

48
#Swift3Arch Architecting Alive Apps Jorge D. Ortiz Fuentes @jdortiz

Transcript of Architecting Alive Apps

Page 1: Architecting Alive Apps

#Swift3Arch

Architecting Alive Apps

Jorge D. Ortiz Fuentes @jdortiz

Page 2: Architecting Alive Apps

#Swift3CA

A Canonical Examples Production

Page 3: Architecting Alive Apps

#Swift3CA

Agenda

★Advanced Architecture Background

★Application for Frameworks

★Real World Example

★Recommendations

★Recap

Page 4: Architecting Alive Apps

Advanced Architecture

Page 5: Architecting Alive Apps

The Classics

Page 6: Architecting Alive Apps

#Swift3Arch

Clean Architecture: iOS

App Delegate

View (VC) Presenter Interactor Entity Gateway

Connector

Page 7: Architecting Alive Apps

#Swift3Arch

Clean Architecture Layers

UI

DB

Preseters

Gateways

Use cases

Entities

Depen

denc

ies

Page 8: Architecting Alive Apps

#Swift3CA

Viewclass AddProgrammerViewController: UITableViewController, UITextFieldDelegate { var presenter: AddProgrammerPresenter! var connector: AddProgrammerConnector! @IBOutlet weak var nameTextField: UITextField! override func viewDidLoad() { super.viewDidLoad() presenter.viewReady() }

@IBAction func cancel(_ sender: Any) { presenter.cancel() } @IBAction func save(_ sender: Any) { presenter.save() } }

extension AddProgrammerViewController: AddProgrammerView { func display(title: String) { self.title = title } func enableSaveButton(_ enable: Bool) { saveButton.isEnabled = enable } }

Page 9: Architecting Alive Apps

#Swift3CA

Presenterclass AddProgrammerPresenter { private let useCaseFactory: UseCaseFactory weak var view: AddProgrammerView! private var programmer = ProgrammerRequest() { didSet { updateView() } }

init(useCaseFactory: UseCaseFactory) { self.useCaseFactory = useCaseFactory } func viewReady() { configureView() } private func updateView() { showTitle() !// … configureSaveAbility() } private func configureView() { setUpName() !// … updateView() } private func showTitle() { view.display(title: programmer.name) } }

Page 10: Architecting Alive Apps

#Swift3CA

Interactorclass AddProgrammerUseCase { fileprivate let entityGateway: EntityGateway fileprivate let request: ProgrammerRequest fileprivate let completion: AddProgrammerCompletion init(entityGateway: EntityGateway, request: ProgrammerRequest, completion: @escaping AddProgrammerCompletion) { self.entityGateway = entityGateway self.request = request self.completion = completion } }

extension AddProgrammerUseCase: UseCase { func execute() { let programmer = Programmer(…) entityGateway.create(programmer: programmer) { self.completion() } } }

Page 11: Architecting Alive Apps

#Swift3CA

Entity Gateway

class InMemoryRepo { var fetchNotifier: FetchProgrammersCompletion? fileprivate var programmers = […] }

extension InMemoryRepo: EntityGateway { func create(programmer: Programmer, completion: @escaping CreateProgrammerCompletion) { programmers.append(programmer) completion() self.fetchNotifier?(programmers) } }

Page 12: Architecting Alive Apps

#Swift3CA

Connector

class AddProgrammerConnector { let entityGateway: EntityGateway init(entityGateway: EntityGateway) { self.entityGateway = entityGateway } func assembleModule(view: AddProgrammerViewController) { let useCaseFactory = UseCaseFactory(entityGateway: entityGateway) let presenter = AddProgrammerPresenter(useCaseFactory: useCaseFactory) view.presenter = presenter view.connector = self presenter.view = view } }

Page 13: Architecting Alive Apps

#Swift3CA

Factory

class UseCaseFactory { let entityGateway: EntityGateway init(entityGateway: EntityGateway) { self.entityGateway = entityGateway } func addProgrammerUseCase(request: ProgrammerRequest, completion: @escaping AddProgrammerCompletion) !-> UseCase { return AddProgrammerUseCase(entityGateway: entityGateway, request: request, completion: completion) } }

Page 14: Architecting Alive Apps

And What If?

Page 15: Architecting Alive Apps

#Swift3CA

Interacting with Frameworks

Accounts

AddressBook

HomeKit

EventKit

HealthKit

Core Audio

Core LocationHomeKit

EventKit

Page 16: Architecting Alive Apps

The Secret Sauce

Page 17: Architecting Alive Apps

#Swift3CA

Injecting Dependencies

ViewPresenter

UseCaseFactory

Entity Gateway

Connector

Page 18: Architecting Alive Apps

Been There, Done That

Page 19: Architecting Alive Apps

Dependency Inversion Principle

High Low Abstract

Low

Page 20: Architecting Alive Apps

#Swift3CA

CloudKit

★This is data source using CloudKit

★CloudKit is an implementation detail

★NO leaky abstractions

class CloudKitRepo { let database: CKDatabase let programmerRecord = "Programmer" init() { let container = CKContainer.default() database = container.database(with: .private) }

fileprivate func recordFrom(programmer: Programmer) !-> CKRecord { let programmerID = CKRecordID(recordName: programmer.id) let record = CKRecord(recordType: programmerRecord, recordID: programmerID) updateRecordProperties(record: record, programmer: programmer) return record } }

extension CloudKitRepo: EntityGatewayProtocol { func create(programmer: Programmer, completion:@escaping () !-> Void) { let record = recordFrom(programmer: programmer) database.save(record) { record, error in guard error !== nil else { NSLog("Save error: \(error!?.localizedDescription)") return } DispatchQueue.main.sync { completion() } } } }

Page 21: Architecting Alive Apps

#Swift3CA

Injecting Dependencies 2

ViewPresenter

UseCaseFactory

Entity Gateway

Connector

Framework

Page 22: Architecting Alive Apps

Real World Example

Page 23: Architecting Alive Apps

A Prototype

Page 24: Architecting Alive Apps

#Swift3CA

MeetinatorEventKit

HomeKit

Magic

Page 25: Architecting Alive Apps

#Swift3CA

Yes, but…

Page 26: Architecting Alive Apps

#Swift3CA

MQTTServer

Suscribe “/rooms/meeting1/colorlight”

Device

Raspberry Pi

Device

Device

Page 27: Architecting Alive Apps

#Swift3CA

MQTTServer

Publish “/rooms/meeting1/colorlight”

{ “firetime”: …}

Device

Raspberry Pi

Device

Device

iPhone

Page 28: Architecting Alive Apps

#Swift3CA

Another ApproachEventKit

CocoaMQTT MQTT (JSON)

Mosquitto

Page 29: Architecting Alive Apps

Right Abstraction?

Page 30: Architecting Alive Apps

#Swift3CA

Any* Abstraction is Better than No

Abstraction

Page 31: Architecting Alive Apps

Hints for Good Abstractions

Page 32: Architecting Alive Apps

#Swift3CA

No References to Framework

Page 33: Architecting Alive Apps

#Swift3CA

Use Your Own Data Types

Page 34: Architecting Alive Apps

#Swift3CA

Events

struct MeetingEvent { let id: String var name: String var startDate: Date var endDate: Date var hasLights: Bool }

Page 35: Architecting Alive Apps

#Swift3CA

Use Your Own Communication

(Delegates/Observer/Rx…)

Page 36: Architecting Alive Apps

#Swift3CA

Use Only What You Need

Page 37: Architecting Alive Apps

#Swift3CA

Move Implementation Details Into

Abstracted Type

Page 38: Architecting Alive Apps

Details

Page 39: Architecting Alive Apps

#Swift3CA

From Date to JSON

★MQTT messages contained JSON

★ firetime is a JSON format date

let formatter = ISO8601DateFormatter() let command: [ String: String ] = [ "firetime": formatter.string(from: fireDate), "type": type.mqttActionType(), ] let jsonData = try! JSONSerialization.data(withJSONObject: command, options: JSONSerialization.WritingOptions()) as Data

Page 40: Architecting Alive Apps

#Swift3CA

Picky with Dates

★HomeKit HMTimeTrigger only accepts times with seconds = 0

private func fixFireDate(_ fireDate: Date) !-> Date { let calendar = Calendar.current let fixedFireDate = calendar.nextDate(after: fireDate, matching: DateComponents(second: 0), matchingPolicy: .nextTime)!

return fixedFireDate }

Page 41: Architecting Alive Apps

#Swift3CA

Browse HomeKit★HomeKit offers several

abstractions in a hierarchy

• Homes

• Rooms

• Accessories

• Services

• Triggers

★Extract what you need

class HomeKitColorLight: NSObject, LightController { var delegate: LightControllerDelegate? fileprivate let homeManager: HMHomeManager fileprivate var primaryHome: HMHome?

func homeManagerDidUpdateHomes(_ manager: HMHomeManager) { primaryHome = homeManager.primaryHome delegate!?.lightControllerReady(self) } private func searchFirstColorLight() !-> HMService? { let lightbulbs = primaryHome!?.servicesWithTypes([HMServiceTypeLightbulb]) let colorLightbulb = lightbulbs!?.first { (service) in let characteristics = service.characteristics.filter { (characteristic) in return characteristic.characteristicType !== HMCharacteristicTypeHue } return characteristics.count > 0 } return colorLightbulb } }

Page 42: Architecting Alive Apps

#Swift3CA

Look for Events

★EventKit allows multiple calendars

★Avoid that complexity

fileprivate func fetchMeetingCalendar() { guard status !== .ready else { return } let calendars = eventStore.calendars(for: .event) let calendar = calendars.filter { $0.title !== meetingCalendarTitle } .first if let calendar = calendar { meetingCalendar = calendar } else { meetingCalendar = EKCalendar(for: .event, eventStore: eventStore) if let meetingCalendar = meetingCalendar { meetingCalendar.title = meetingCalendarTitle meetingCalendar.source = eventStore.defaultCalendarForNewEvents.source do { try eventStore.saveCalendar(meetingCalendar, commit: true) } catch let error as NSError { NSLog("Error: \(error)") } } } }

Page 43: Architecting Alive Apps

#Swift3CA

Authorization

★EventKit requires Authorization to access the data

class EventKitEventProvider { enum Status { case ready, accessDenied, unknown } let eventStore: EKEventStore var status = Status.unknown init() { eventStore = EKEventStore() checkAccessToEvents() fetchMeetingCalendar() } private func checkAccessToEvents() { switch EKEventStore.authorizationStatus(for: .event) { case .authorized: status = .ready case .notDetermined: !// First time access requestAccessToEvents() case .denied, .restricted: status = .accessDenied } } private func requestAccessToEvents() { eventStore.requestAccess(to: .event) { (granted: Bool, error: Error?) in !// … } } }

Page 44: Architecting Alive Apps

#Swift3CA

extension LightControllerAction { func homeKitColor() !-> UIColor { let color: UIColor switch(self) { case .start: color = UIColor.green case .warn: color = UIColor.orange case .end: color = UIColor.red case .off: color = UIColor.black } return color } func sceneName() !-> String { let name: String switch(self) { case .start: name = "Go green" case .warn: name = "Go orange" case .end: name = "Go red" case .off: name = "Go off" } return name } }

Extend like a Boss

extension LightControllerAction { func mqttActionType() !-> String { let action: String switch self { case .start: action = "start" case .warn: action = "warn" case .end: action = "end" case .off: action = "off" } return action } }

Page 45: Architecting Alive Apps

Recap

Page 46: Architecting Alive Apps

#Swift3CA

Recap

★ IoT is cool!

★Advanced architectures can also be applied to apps with frameworks

★Use abstractions

★YES, I mean it: Use abstractions

Page 47: Architecting Alive Apps

Thank You!

Page 48: Architecting Alive Apps

@jdortiz #Swift3CA