#Swift3Arch
Architecting Alive Apps
Jorge D. Ortiz Fuentes @jdortiz
#Swift3CA
A Canonical Examples Production
#Swift3CA
Agenda
★Advanced Architecture Background
★Application for Frameworks
★Real World Example
★Recommendations
★Recap
Advanced Architecture
The Classics
#Swift3Arch
Clean Architecture: iOS
App Delegate
View (VC) Presenter Interactor Entity Gateway
Connector
#Swift3Arch
Clean Architecture Layers
UI
DB
Preseters
Gateways
Use cases
Entities
Depen
denc
ies
#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 } }
#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) } }
#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() } } }
#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) } }
#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 } }
#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) } }
And What If?
#Swift3CA
Interacting with Frameworks
Accounts
AddressBook
HomeKit
EventKit
HealthKit
Core Audio
Core LocationHomeKit
EventKit
The Secret Sauce
#Swift3CA
Injecting Dependencies
ViewPresenter
UseCaseFactory
Entity Gateway
Connector
Been There, Done That
Dependency Inversion Principle
High Low Abstract
Low
#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() } } } }
#Swift3CA
Injecting Dependencies 2
ViewPresenter
UseCaseFactory
Entity Gateway
Connector
Framework
Real World Example
A Prototype
#Swift3CA
MeetinatorEventKit
HomeKit
Magic
#Swift3CA
Yes, but…
#Swift3CA
MQTTServer
Suscribe “/rooms/meeting1/colorlight”
Device
Raspberry Pi
Device
Device
#Swift3CA
MQTTServer
Publish “/rooms/meeting1/colorlight”
{ “firetime”: …}
Device
Raspberry Pi
Device
Device
iPhone
#Swift3CA
Another ApproachEventKit
CocoaMQTT MQTT (JSON)
Mosquitto
Right Abstraction?
#Swift3CA
Any* Abstraction is Better than No
Abstraction
Hints for Good Abstractions
#Swift3CA
No References to Framework
#Swift3CA
Use Your Own Data Types
#Swift3CA
Events
struct MeetingEvent { let id: String var name: String var startDate: Date var endDate: Date var hasLights: Bool }
#Swift3CA
Use Your Own Communication
(Delegates/Observer/Rx…)
#Swift3CA
Use Only What You Need
#Swift3CA
Move Implementation Details Into
Abstracted Type
Details
#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
#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 }
#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 } }
#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)") } } } }
#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 !// … } } }
#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 } }
Recap
#Swift3CA
Recap
★ IoT is cool!
★Advanced architectures can also be applied to apps with frameworks
★Use abstractions
★YES, I mean it: Use abstractions
Thank You!
@jdortiz #Swift3CA