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

PlanGrid iOS Refactor Presentation

PlanGrid iOS Refactor Presentation

Internal Presentation discussing iOS Refactor project.

De23af005c790b22f8ce4d201e6ca027?s=128

Benjamin Encz

April 06, 2016
Tweet

Transcript

  1. Warning: The Slides Contain Larger Code Examples & Handwri;ng Download

    Slides Here: bit.ly/pg-ios-refactor iOS Rewrite | @benjaminencz | PlanGrid, April 2016 1
  2. iOS Refactor Rewrite Goals - Concepts and Tools - Usage

    iOS Rewrite | @benjaminencz | PlanGrid, April 2016 2
  3. 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
  4. Pa#erns in Code Shared Infrastructure iOS Rewrite | @benjaminencz |

    PlanGrid, April 2016 4
  5. iOS Rewrite | @benjaminencz | PlanGrid, April 2016 5

  6. iOS Rewrite | @benjaminencz | PlanGrid, April 2016 6

  7. Part 1: UI & UI State Part 2: Persistence Part

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

    Layer iOS Rewrite | @benjaminencz | PlanGrid, April 2016 8
  9. iOS Rewrite | @benjaminencz | PlanGrid, April 2016 9

  10. iOS Rewrite | @benjaminencz | PlanGrid, April 2016 10

  11. iOS Rewrite | @benjaminencz | PlanGrid, April 2016 11

  12. UI Layer in Detail Store iOS Rewrite | @benjaminencz |

    PlanGrid, April 2016 12
  13. 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
  14. 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
  15. 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
  16. 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
  17. Signal on Store Makes State Observable var state: SignalProducer<TeamManagementState, NoError>

    iOS Rewrite | @benjaminencz | PlanGrid, April 2016 17
  18. 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
  19. Detour: Why Reac/veCocoa? iOS Rewrite | @benjaminencz | PlanGrid, April

    2016 19
  20. Ideal World (aka React): func render(state: State) -> UI iOS

    Rewrite | @benjaminencz | PlanGrid, April 2016 20
  21. 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
  22. 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
  23. 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
  24. 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
  25. iOS Rewrite | @benjaminencz | PlanGrid, April 2016 25

  26. UI Layer in Detail ViewProvider iOS Rewrite | @benjaminencz |

    PlanGrid, April 2016 26
  27. 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
  28. 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
  29. UI Layer in Detail ViewModel to UIKit Transla3on iOS Rewrite

    | @benjaminencz | PlanGrid, April 2016 29
  30. 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
  31. Part 2: Persistence ObjectHaven<T> iOS Rewrite | @benjaminencz | PlanGrid,

    April 2016 31
  32. 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
  33. iOS Rewrite | @benjaminencz | PlanGrid, April 2016 33

  34. 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<String>("uid") public static let reportType = Column<String>("report_type") public static let filename = Column<String>("filename") public static let size = Column<Int>("size") public static let urlFull = OptionalColumn<NSURL>("url_full") public static let urlThumbnail = OptionalColumn<NSURL>("url_thumbnail") } } iOS Rewrite | @benjaminencz | PlanGrid, April 2016 34
  35. 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
  36. 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
  37. Usage let reportType = PGReportTypeFullModel() try projectContext.reportTypeHaven.saveLocalChanges(reportType) iOS Rewrite |

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

    2016 38
  39. iOS Rewrite | @benjaminencz | PlanGrid, April 2016 39

  40. Sync • Same idea as ObjectHaven: Code is provided as

    infrastructure • Adop6ng types just configure that code iOS Rewrite | @benjaminencz | PlanGrid, April 2016 40
  41. Sync in Detail: Download Sync iOS Rewrite | @benjaminencz |

    PlanGrid, April 2016 41
  42. 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
  43. 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
  44. 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
  45. 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
  46. Sync in Detail: Upload Sync iOS Rewrite | @benjaminencz |

    PlanGrid, April 2016 46
  47. 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
  48. 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
  49. Upload Sync: Muta0on Opera0ons let exampleModel = PGExampleObjectHavenModel() exampleModel =

    exampleModel.mutate(PGExampleObjectHavenModelChangeType("New Test Type")) objectHaven.saveLocalChanges(exampleModel) iOS Rewrite | @benjaminencz | PlanGrid, April 2016 49
  50. iOS Rewrite | @benjaminencz | PlanGrid, April 2016 50

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

    April 2016 51
  52. iOS Rewrite | @benjaminencz | PlanGrid, April 2016 52

  53. iOS Rewrite | @benjaminencz | PlanGrid, April 2016 53

  54. iOS Rewrite | @benjaminencz | PlanGrid, April 2016 54

  55. 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
  56. Part 4: Tests as Documenta1on iOS Rewrite | @benjaminencz |

    PlanGrid, April 2016 56
  57. 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
  58. iOS Rewrite | @benjaminencz | PlanGrid, April 2016 58

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