Slide 1

Slide 1 text

Warning: The Slides Contain Larger Code Examples & Handwri;ng Download Slides Here: bit.ly/pg-ios-refactor iOS Rewrite | @benjaminencz | PlanGrid, April 2016 1

Slide 2

Slide 2 text

iOS Refactor Rewrite Goals - Concepts and Tools - Usage iOS Rewrite | @benjaminencz | PlanGrid, April 2016 2

Slide 3

Slide 3 text

Goals Move Fast & Don't Break Too Many Things - Comprehensible Code Base - Less Code - Test Coverage - Best Prac9ces for Faster Onboarding Performance - Provide Fast Defaults (Infrastructure) - Make Bad Choices Hard iOS Rewrite | @benjaminencz | PlanGrid, April 2016 3

Slide 4

Slide 4 text

Pa#erns in Code Shared Infrastructure iOS Rewrite | @benjaminencz | PlanGrid, April 2016 4

Slide 5

Slide 5 text

iOS Rewrite | @benjaminencz | PlanGrid, April 2016 5

Slide 6

Slide 6 text

iOS Rewrite | @benjaminencz | PlanGrid, April 2016 6

Slide 7

Slide 7 text

Part 1: UI & UI State Part 2: Persistence Part 3: Sync iOS Rewrite | @benjaminencz | PlanGrid, April 2016 7

Slide 8

Slide 8 text

Part 1: UI & UI State Flux + Reac+ve View Layer iOS Rewrite | @benjaminencz | PlanGrid, April 2016 8

Slide 9

Slide 9 text

iOS Rewrite | @benjaminencz | PlanGrid, April 2016 9

Slide 10

Slide 10 text

iOS Rewrite | @benjaminencz | PlanGrid, April 2016 10

Slide 11

Slide 11 text

iOS Rewrite | @benjaminencz | PlanGrid, April 2016 11

Slide 12

Slide 12 text

UI Layer in Detail Store iOS Rewrite | @benjaminencz | PlanGrid, April 2016 12

Slide 13

Slide 13 text

Store • Most new code for a feature lives in here • Defines State for a scene • Updates State when Actions come in • Exposes Signal for UI layer to observe state • Communicates with ObjectHaven to read & write data iOS Rewrite | @benjaminencz | PlanGrid, April 2016 13

Slide 14

Slide 14 text

Explicit State as Data public struct TeamManagementState { /// Alphabetically sorted list of collaborators and admins on the project public var users: [TeamMember] public var selectedUsers: [TeamMember] public var selectionEnabled: Bool = false public var multiselectEnabled: Bool = false public var invitationsEnabled: Bool = false } iOS Rewrite | @benjaminencz | PlanGrid, April 2016 14

Slide 15

Slide 15 text

Ac#ons as Data struct TeamManagementActions { ///Action that asks the store to filter users based on provided search term struct SearchTeamMembers: ProjectSpecificAction { let projectUid: String let searchTerm: String let cursorPosition: Int } /// Action that asks the store to invite a team member struct InviteTeamMember: ProjectSpecificAction { let projectUid: String let userEmail: String } } iOS Rewrite | @benjaminencz | PlanGrid, April 2016 15

Slide 16

Slide 16 text

Store Changes State in Response to Ac2ons func _handleActions(action: AnyAction) { switch action { // ... case let inviteAction as TeamManagementActions.InviteTeamMember: self._inviteTeamMember(inviteAction.userEmail) case let selectAction as TeamManagementActions.SelectTeamMember: self._selectTeamMember(selectAction.userEmail) // ... } } private func _inviteTeamMember(email: String) { let userExists = self._state.value.users.contains { $0.email == email } if !userExists { let newMember = TeamMember(email:email) self._state.value.insertUser(newMember) dispatch_background_high({ self._digest.userProvider.addCollaborator(email) }) } } iOS Rewrite | @benjaminencz | PlanGrid, April 2016 16

Slide 17

Slide 17 text

Signal on Store Makes State Observable var state: SignalProducer iOS Rewrite | @benjaminencz | PlanGrid, April 2016 17

Slide 18

Slide 18 text

View Layer Observes Signal and Updates Itself // The edit button is enabled when the state allows purging self.navigationItem.rightBarButtonItem!.racEnabled <~ self.store.state .map { $0.isPurgable } iOS Rewrite | @benjaminencz | PlanGrid, April 2016 18

Slide 19

Slide 19 text

Detour: Why Reac/veCocoa? iOS Rewrite | @benjaminencz | PlanGrid, April 2016 19

Slide 20

Slide 20 text

Ideal World (aka React): func render(state: State) -> UI iOS Rewrite | @benjaminencz | PlanGrid, April 2016 20

Slide 21

Slide 21 text

Real World: • UIKit cannot be updated based on state • All state updates need to be incremental • This is the root cause of much horrible UI code; all state lives in UIViewController, all state is implicitly part of the UI components itself • ReactiveCocoa is our bridge from the ideal world to the real world iOS Rewrite | @benjaminencz | PlanGrid, April 2016 21

Slide 22

Slide 22 text

Building Reac-ve Bridges: // The edit button is enabled when the state allows purging self.navigationItem.rightBarButtonItem!.racEnabled <~ self.store.state .map { $0.isPurgable } iOS Rewrite | @benjaminencz | PlanGrid, April 2016 22

Slide 23

Slide 23 text

Building Reac-ve Bridges: // When the download/purge button gets enabled/disabled, reload the cell // so that the table view changes its appearance correctly self.projectStorageTableViewDataSource.tableViewModel.producer .ignoreNil() .skip(1) .map { ($0[NSIndexPath(forRow: 0, inSection: 2)] as? CenteredLabelCellViewModel)?.selectionStyle } .ignoreNil() .skipRepeats() .startWithNext { [weak self] _ in guard let `self` = self else { return } self.tableView.reloadRowsAtIndexPaths( [NSIndexPath(forRow: 0, inSection: 2)], withRowAnimation: .None ) } iOS Rewrite | @benjaminencz | PlanGrid, April 2016 23

Slide 24

Slide 24 text

Store Summary • Store defines State for a scene • Store updates State when Actions come in • Store exposes Signal for UI layer to observe state • Store communicates with ObjectHaven to read & write data iOS Rewrite | @benjaminencz | PlanGrid, April 2016 24

Slide 25

Slide 25 text

iOS Rewrite | @benjaminencz | PlanGrid, April 2016 25

Slide 26

Slide 26 text

UI Layer in Detail ViewProvider iOS Rewrite | @benjaminencz | PlanGrid, April 2016 26

Slide 27

Slide 27 text

ViewProvider • Take State from store • Returns view models • Our React-like layer • Pure Func:on: func tableViewModelForState(state: ProjectStorageState) -> FluxTableViewModel iOS Rewrite | @benjaminencz | PlanGrid, April 2016 27

Slide 28

Slide 28 text

ViewProvider: View Code as Data /// Generates `FluxTableViewModel` that represent the current `ProjectStorageState` func tableViewModelForState(state: ProjectStorageState) -> FluxTableViewModel { return FluxTableViewModel(sectionModels: [ FluxTableViewModel.SectionModel(headerTitle: "project.sheets".translate(), headerHeight: 50, cellViewModels: [ self._cellViewModelForCategoryDownloadState( state.currentSet, viewState: state.viewState, initialized: state.downloadStateInitialized ), self._cellViewModelForCategoryDownloadState( state.pastSet, viewState: state.viewState, initialized: state.downloadStateInitialized ) ]), FluxTableViewModel.SectionModel(headerTitle: nil, headerHeight: 20, cellViewModels: [ state.viewState == .EditMode ? self._purgeCellViewModelForState(state) : self._downloadCellViewModelForState(state) ]) ]) iOS Rewrite | @benjaminencz | PlanGrid, April 2016 28

Slide 29

Slide 29 text

UI Layer in Detail ViewModel to UIKit Transla3on iOS Rewrite | @benjaminencz | PlanGrid, April 2016 29

Slide 30

Slide 30 text

ViewModel to UIKit Transla3on • Takes view model and turns it into UIKit components / proper9es • Simpler bindings are implemented manually; complex bindings are done via components (e.g. FluxTableViewModel) // The edit button is enabled when the state allows purging self.navigationItem.rightBarButtonItem!.racEnabled <~ self.viewModel .editingEnabled iOS Rewrite | @benjaminencz | PlanGrid, April 2016 30

Slide 31

Slide 31 text

Part 2: Persistence ObjectHaven iOS Rewrite | @benjaminencz | PlanGrid, April 2016 31

Slide 32

Slide 32 text

ObjectHaven • We iden(fied that our persistence code is highly redundant • Wanted to share persistence code among all types • Provide hooks for customiza(on • Now types only need to adopt ObjectHavenElement protocol iOS Rewrite | @benjaminencz | PlanGrid, April 2016 32

Slide 33

Slide 33 text

iOS Rewrite | @benjaminencz | PlanGrid, April 2016 33

Slide 34

Slide 34 text

Making a Type Persistable public struct ReportTemplatesTable: SqlTable { public static let name = "report_templates" public static let primaryKey = PrimaryKey(Columns.uid) public struct Columns { public static let uid = Column("uid") public static let reportType = Column("report_type") public static let filename = Column("filename") public static let size = Column("size") public static let urlFull = OptionalColumn("url_full") public static let urlThumbnail = OptionalColumn("url_thumbnail") } } iOS Rewrite | @benjaminencz | PlanGrid, April 2016 34

Slide 35

Slide 35 text

Making a Type Persistable public final class PGReportTemplateModel: NSObject, ProjectSpecificModel, UIDIdentifiable { public convenience init?(resultSet: FMResultSet) { guard let project = resultSet.project else { return nil } let columns = ReportTemplatesTable.Columns.self let file = PGReportFileJsonModel( uid: columns.uid.value(resultSet), filename: columns.filename.value(resultSet), size: columns.size.value(resultSet) ) public static var table: SqlTable.Type { return ReportTemplatesTable.self } public var columnRepresentation: [(SqlColumn, SqlTypeConvertible?)] { let columns = ReportTemplatesTable.Columns.self return [ (columns.uid, self.uid), (columns.filename, self.file.filename), (columns.size, self.file.size), (columns.urlFull, self.file.urlFull), (columns.urlThumbnail, self.file.urlThumbnail), (columns.reportType, self.reportTypeUid) ] } public var primaryKeyValue: String { return self.file.uid } } } iOS Rewrite | @benjaminencz | PlanGrid, April 2016 35

Slide 36

Slide 36 text

Making a Type Persistable • A#er adoping protocols all basic persistence features are available to that type: • Saving, dele;ng, querying, reference coun;ng, asset management, change tracking, etc. • Bonus: Free Test Coverage through our Generic Test Suite describe("Test Object Havens") { testHaven(FieldReportTemplateTestCase.self) testHaven(FieldReportTypeTestCase.self) testHaven(FieldReportFileTestCase.self) testHaven(FieldReportTestCase.self) } iOS Rewrite | @benjaminencz | PlanGrid, April 2016 36

Slide 37

Slide 37 text

Usage let reportType = PGReportTypeFullModel() try projectContext.reportTypeHaven.saveLocalChanges(reportType) iOS Rewrite | @benjaminencz | PlanGrid, April 2016 37

Slide 38

Slide 38 text

Part 3: Sync iOS Rewrite | @benjaminencz | PlanGrid, April 2016 38

Slide 39

Slide 39 text

iOS Rewrite | @benjaminencz | PlanGrid, April 2016 39

Slide 40

Slide 40 text

Sync • Same idea as ObjectHaven: Code is provided as infrastructure • Adop6ng types just configure that code iOS Rewrite | @benjaminencz | PlanGrid, April 2016 40

Slide 41

Slide 41 text

Sync in Detail: Download Sync iOS Rewrite | @benjaminencz | PlanGrid, April 2016 41

Slide 42

Slide 42 text

Download Sync: DownloadSyncable public protocol DownloadSyncable { /// Uniquely identifies saved `SyncRequestState` in the database. static var requestStateKey: String { get } /// Defines API endpoint from which to sync this entity static func apiEndpoint() -> NSURLComponents /// Creates the first in a potential chain of paged requests given the previously saved parameters. /// - note: For requests like `ProjectDigest` that don't use paging parameters will be `nil`. static func startingRequest(state: SyncRequestState?) -> SyncRequest? /// Process the server's response to a request. /// - returns: The list of changes and optionally a request if there is another paginated request. /// The parameters on the returned request will be saved as the "previous parameters". static func processServerResponse(response: SyncResponse, request: SyncRequest, state: SyncRequestState?) -> (changes: [ModelChange], newState: SyncRequestState?, nextRequest: SyncRequest?) } iOS Rewrite | @benjaminencz | PlanGrid, April 2016 42

Slide 43

Slide 43 text

Download Sync: DownloadSyncable Types that adopt the protocol get the following for free: • Network Requests for described endpoints • Pagina8on • Persistence of request state in DB (for next sync) • Persistence of downloaded data in ObjectHaven or other PersistenceProvider iOS Rewrite | @benjaminencz | PlanGrid, April 2016 43

Slide 44

Slide 44 text

Download Sync Example: Conten4ul API extension AnnouncementEntry: DownloadSyncable { public static var requestStateKey: String { return "ContentfulContentRequestKey" } public static func apiEndpoint() -> NSURLComponents { return NSURLComponents(string: "spaces/\(Announcement.space.uid.value)/sync")! } public static func startingRequest(state: SyncRequestState?) -> SyncRequest? { let urlComponents = self.apiEndpoint() if let syncedState = state { urlComponents.queryItems = [NSURLQueryItem(name: "sync_token", value: syncedState.payload["sync_token"].string)] } else { let params = [ "initial": "true", ] urlComponents.queryItems = params.map { NSURLQueryItem(name: $0, value: $1) } } return SyncRequest(relativeUrl:urlComponents, headers:[:]) } // ... } iOS Rewrite | @benjaminencz | PlanGrid, April 2016 44

Slide 45

Slide 45 text

Download Sync Example: Conten4ul API Kicking synchroniza/on off: public static func syncAnnouncements(db: GlobalDatabase = _defaultDb) { let persistenceProvider = AnnouncementsSyncPersistenceProvider( globalDB: db ) let syncManager = SyncManager( persistenceProvider: persistenceProvider, HTTPAuthenticationHeader: ["Authorization": "Bearer \(ContentfulManager._authToken)"], baseURL: ContentfulManager._contentfulBaseURL ) syncManager.sync([[AnnouncementEntry.self]]) { result in if result == .Failure { DDLogError("contentful sync failed!") } } } iOS Rewrite | @benjaminencz | PlanGrid, April 2016 45

Slide 46

Slide 46 text

Sync in Detail: Upload Sync iOS Rewrite | @benjaminencz | PlanGrid, April 2016 46

Slide 47

Slide 47 text

Sync in Detail: Upload Sync • Wanted to conceptualize the modifica3on of local data to reduce redundant code, reduce room for error and enable new sync features, e.g. rollback of changes iOS Rewrite | @benjaminencz | PlanGrid, April 2016 47

Slide 48

Slide 48 text

Sync in Detail: Upload Sync Opera3ons • Objects can't be mutated directly anymore instead changes are modelled as MutationOperation • MutationOperation describes a change and provides relevant data for push request • CreationOperation and DeletionOperation are created implicitly by ObjectHaven iOS Rewrite | @benjaminencz | PlanGrid, April 2016 48

Slide 49

Slide 49 text

Upload Sync: Muta0on Opera0ons let exampleModel = PGExampleObjectHavenModel() exampleModel = exampleModel.mutate(PGExampleObjectHavenModelChangeType("New Test Type")) objectHaven.saveLocalChanges(exampleModel) iOS Rewrite | @benjaminencz | PlanGrid, April 2016 49

Slide 50

Slide 50 text

iOS Rewrite | @benjaminencz | PlanGrid, April 2016 50

Slide 51

Slide 51 text

Sync in Detail: Rollbacks iOS Rewrite | @benjaminencz | PlanGrid, April 2016 51

Slide 52

Slide 52 text

iOS Rewrite | @benjaminencz | PlanGrid, April 2016 52

Slide 53

Slide 53 text

iOS Rewrite | @benjaminencz | PlanGrid, April 2016 53

Slide 54

Slide 54 text

iOS Rewrite | @benjaminencz | PlanGrid, April 2016 54

Slide 55

Slide 55 text

Sync in Detail: Rollbacks • Will provide customiza3on hooks; but by default rollbacks will be implemented by common infrastructure • Note: v1 will only do complete rollback of an object (a@er first failed opera3on). v2 will support demonstrated par3al rollback. iOS Rewrite | @benjaminencz | PlanGrid, April 2016 55

Slide 56

Slide 56 text

Part 4: Tests as Documenta1on iOS Rewrite | @benjaminencz | PlanGrid, April 2016 56

Slide 57

Slide 57 text

describe("Storing the initial server version of an entity") { context("When the ObjectHaven saves an entity via `saveFromServer`") { beforeEach { OperationDeserializerRegistry.operationTypes.append(PGExampleObjectHavenModelChangeType.self) haven = ObjectHaven() try! haven.database.context.executeUpdate(PGExampleObjectHavenModel.createTable()) operationDAO = SyncOperationDAO(database: haven.database) exampleModel = PGExampleObjectHavenModel() try! haven.saveFromServer(exampleModel) } it("stores a JSON blob that represents the object in the database") { let serverVersion = try haven.latestServerVersion(exampleModel.uid) expect(serverVersion).to(equal(exampleModel)) } context("when a deletion operation is performed") { beforeEach { try! haven.removeLocally(exampleModel) } it("removes the object from the local db") { let object = haven[exampleModel.uid] expect(object).to(beNil()) } } } iOS Rewrite | @benjaminencz | PlanGrid, April 2016 57

Slide 58

Slide 58 text

iOS Rewrite | @benjaminencz | PlanGrid, April 2016 58

Slide 59

Slide 59 text

Fin iOS Rewrite | @benjaminencz | PlanGrid, April 2016 59