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

Building High Performance and Testable UI compo...

Building High Performance and Testable UI component

Kishikawa Katsumi

September 16, 2017
Tweet

More Decks by Kishikawa Katsumi

Other Decks in Programming

Transcript

  1. Agenda • ߴ଎ͳUIΛ࡞Δʹ͸ • ෛՙͷߴ͍ॲཧΛݮΒ͢ • ಡΈ΍͢͞ɺϝϯςφϯεͷ͠΍͢͞ͱͷτϨʔυΦϑ • ϝϯςφϯε͠΍͍͢ίʔυΛॻ͘ʹ͸ •

    ϝϯςφϯε͠΍͍͢ʹςετ͠΍͍͢ • UIίϯϙʔωϯτΛςετ͢Δʹ͸ • ঢ়ଶΛϞσϧʹ෼཭͢Δ • Ϟσϧ͸ςετͰ͖Δʢ͠΍͍͢ʣ • ബ͍Ϗϡʔͱ෼ް͍Ϟσϧ kk@realm.io
  2. kk@realm.io func layout() { for 0 in 0..<numberOfRows { for

    0 in 0..<numberOfColumns { let cellSize = CGSize(width: columnWidth, height: rowHeight) layoutCell(frame: CGRect(origin: cellOrigin, size: cellSize)) cellOrigin.x += columnWidth } cellOrigin.y += rowHeight } }
  3. kk@realm.io func layout() { let startRow = spreadsheetView.findIndex(in: scrollView.rowRecords, for:

    visibleRect.origin.y) cellOrigin.y = scrollView.rowRecords[startRow] for row in startRow..<rowCount { let stop = enumerateColumns(currentRow: row, currentRowIndex: rowIndex) if stop { break } cellOrigin.y += rowHeightCache[row] + intercellSpacing.height } } private func enumerateColumns(currentRow row: Int, currentRowIndex rowIndex: Int) -> Bool { let startColumn = spreadsheetView.findIndex(in: columnRecords, for: visibleRect.origin.x) cellOrigin.x = columnRecords[startColumn] while columnIndex < columnCount { let columnWidth = columnWidthCache[column] let rowHeight = rowHeightCache[row] guard cellOrigin.x + columnWidth > visibleRect.minX else { cellOrigin.x += columnWidth + intercellSpacing.width continue } guard cellOrigin.x <= visibleRect.maxX else { cellOrigin.x += columnWidth + intercellSpacing.width return false } guard cellOrigin.y + rowHeight > visibleRect.minY else { cellOrigin.x += columnWidth + intercellSpacing.width continue } guard cellOrigin.y <= visibleRect.maxY else { return true } let address = Address(row: row, column: column, rowIndex: rowIndex, columnIndex: columnIndex) visibleCellAddresses.insert(address) let cellSize = CGSize(width: columnWidth, height: rowHeight) layoutCell(address: address, frame: CGRect(origin: cellOrigin, size: cellSize)) cellOrigin.x += columnWidth } return false }
  4. kk@realm.io let sum = numbers.reduce(0) { $0 + $1 }

    var sum: CGFloat = 0 for n in numbers { sum += n }
  5. kk@realm.io let sum = numbers.reduce(0) { $0 + $1 }

    var sum: CGFloat = 0 for n in numbers { sum += n }
  6. kk@realm.io func add(x: Int, y: Int) -> Int { return

    x + y } func testAdd() { let a = 1 let b = 2 XCTAssertEqual(add(x: a, y: b), 3) }
  7. kk@realm.io func add(x: Int, y: Int) -> Int { return

    x + y } func testAdd() { let a = 1 let b = 2 XCTAssertEqual(add(x: a, y: b), 3) } ಺෦ঢ়ଶͳ͠ ೖྗ ग़ྗ
  8. ಺෦ঢ়ଶ ঢ়ଶΛϞσϧʹ෼཭͢Δ kk@realm.io public class SpreadsheetView: UIView { public var

    intercellSpacing = CGSize(width: 1, height: 1) public var gridStyle: GridStyle = .solid(width: 1, color: .lightGray) public protocol SpreadsheetViewDataSource: class { func numberOfColumns(...) -> Int func numberOfRows(...) -> Int func frozenColumns(...) -> Int func frozenRows(...) -> Int func spreadsheetView(_:widthForColumn:) -> CGFloat func spreadsheetView(_:heightForRow:) -> CGFloat func mergedCells(in:) -> [CellRange] }
  9. ύϥϝʔλ ঢ়ଶΛϞσϧʹ෼཭͢Δ kk@realm.io var contentOffset: CGPoint var frame: CGRect var

    orientation: UIDeviceOrientation ... ϝιου public override func layoutSubviews() { reloadDataIfNeeded() layoutCornerView() layoutRowHeaderView() layoutColumnHeaderView() layoutTableView() } ಺෦ঢ়ଶ
  10. kk@realm.io func testTableView() { let parameters = Parameters(numberOfColumns: 50, numberOfRows:

    60, frozenColumns: 1, frozenRows: 1) let viewController = SpreadsheetViewController() viewController.numberOfColumns = { _ in return parameters.numberOfColumns } viewController.numberOfRows = { _ in return parameters.numberOfRows } viewController.widthForColumn = { return parameters.columns[$1] } viewController.heightForRow = { return parameters.rows[$1] } viewController.frozenColumns = { _ in return parameters.frozenColumns } viewController.frozenRows = { _ in return parameters.frozenRows } ...
  11. kk@realm.io ... let window = UIWindow() window.backgroundColor = .white window.rootViewController

    = viewController window.makeKeyAndVisible() showViewController(viewController: viewController) waitRunLoop() XCTAssertEqual(spreadsheetView.visibleCells.count, numberOfVisibleColumns(...)) for (index, visibleCell) in spreadsheetView.visibleCells .sorted() .enumerated() { let column = index / numberOfVisibleRows(in: spreadsheetView, parameters: parameters) let row = index % numberOfVisibleRows(in: spreadsheetView, parameters: parameters) XCTAssertEqual(visibleCell.indexPath, IndexPath(row: row, column: column)) } }
  12. kk@realm.io ... let rect = cell.convert(cell.bounds, to: spreadsheetView) var actual

    = CGPoint.zero var expected = CGPoint.zero if scrollPosition.contains(.left) { actual.x = rect.origin.x if parameters.circularScrolling.options.direction.contains(CircularScrolling.Direction.horizontally) { if width < frozenWidth { expected.x = width + parameters.intercellSpacing.width } else { expected.x = frozenWidth + parameters.intercellSpacing.width } } else { if width < frozenWidth { expected.x = width + parameters.intercellSpacing.width } else if width <= parameters.columnWidth - spreadsheetView.frame.width + frozenWidth { expected.x = frozenWidth + parameters.intercellSpacing.width } else { expected.x = spreadsheetView.frame.width - (parameters.columnWidth - width) + parameters.intercellSpacing.width } } } if scrollPosition.contains(.centeredHorizontally) { ...
  13. kk@realm.io public class SpreadsheetView: UIView { public weak var dataSource:

    SpreadsheetViewDataSource? public var intercellSpacing = CGSize(width: 1, height: 1) public var gridStyle: GridStyle = .solid(width: 1, color: .lightGray) ... public override func layoutSubviews() { super.layoutSubviews() ... layoutCornerView() layoutRowHeaderView() layoutColumnHeaderView() layoutTableView() } func layout() { ... for rowIndex in (startRowIndex + startRow)..<rowCount { ... for rowIndex in (startRowIndex + startRow)..<rowCount { ... } } } }
  14. kk@realm.io final class LayoutEngine { private let spreadsheetView: SpreadsheetView private

    let intercellSpacing: CGSize private let defaultGridStyle: GridStyle ... private let visibleRect: CGRect ... private let numberOfColumns: Int private let numberOfRows: Int ... init(spreadsheetView: SpreadsheetView, scrollView: ScrollView) { self.spreadsheetView = spreadsheetView self.scrollView = scrollView intercellSpacing = spreadsheetView.intercellSpacing ... } func layout() { guard startColumn != columnCount && startRow != rowCount else { return } let startRowIndex = spreadsheetView.findIndex(in: scrollView.rowRecords, for: visibleRect.origin.y - insets.y) cellOrigin.y = insets.y + scrollView.rowRecords[startRowIndex] + intercellSpacing.height for rowIndex in (startRowIndex + startRow)..<rowCount { ... } ...
  15. kk@realm.io final class LayoutEngine { private let spreadsheetView: SpreadsheetView private

    let scrollView: ScrollView private let intercellSpacing: CGSize private let defaultGridStyle: GridStyle ... private let visibleRect: CGRect ... private let numberOfColumns: Int private let numberOfRows: Int ... init(spreadsheetView: SpreadsheetView, scrollView: ScrollView) { self.spreadsheetView = spreadsheetView self.scrollView = scrollView intercellSpacing = spreadsheetView.intercellSpacing ... } func layout() { guard startColumn != columnCount && startRow != rowCount else { return } let startRowIndex = spreadsheetView.findIndex(in: scrollView.rowRecords, for: visibleRect.origin.y - insets.y) cellOrigin.y = insets.y + scrollView.rowRecords[startRowIndex] + intercellSpacing.height for rowIndex in (startRowIndex + startRow)..<rowCount { ... } ...
  16. kk@realm.io final class LayoutEngine { private let spreadsheetView: SpreadsheetView private

    let scrollView: ScrollView private let intercellSpacing: CGSize private let defaultGridStyle: GridStyle ... private let visibleRect: CGRect ... private let numberOfColumns: Int private let numberOfRows: Int ... init(spreadsheetView: SpreadsheetView, scrollView: ScrollView) { self.spreadsheetView = spreadsheetView self.scrollView = scrollView intercellSpacing = spreadsheetView.intercellSpacing ... } func layout() { guard startColumn != columnCount && startRow != rowCount else { return } let startRowIndex = spreadsheetView.findIndex(in: scrollView.rowRecords, for: visibleRect.origin.y - insets.y) cellOrigin.y = insets.y + scrollView.rowRecords[startRowIndex] + intercellSpacing.height for rowIndex in (startRowIndex + startRow)..<rowCount { ... } ...
  17. kk@realm.io final class LayoutEngine { private let intercellSpacing: CGSize private

    let defaultGridStyle: GridStyle ... private let visibleRect: CGRect ... private let numberOfColumns: Int private let numberOfRows: Int ... init(spreadsheetView: SpreadsheetView, scrollView: ScrollView) { self.spreadsheetView = spreadsheetView self.scrollView = scrollView intercellSpacing = spreadsheetView.intercellSpacing ... } func layout() { guard startColumn != columnCount && startRow != rowCount else { return } let startRowIndex = spreadsheetView.findIndex(in: scrollView.rowRecords, for: visibleRect.origin.y - insets.y) cellOrigin.y = insets.y + scrollView.rowRecords[startRowIndex] + intercellSpacing.height for rowIndex in (startRowIndex + startRow)..<rowCount { ... } ...
  18. kk@realm.io struct SpreadsheetViewConfiguration { let intercellSpacing: CGSize let defaultGridStyle: GridStyle

    let circularScrollingOptions: CircularScrolling.Configuration.Options let circularScrollScalingFactor: (horizontal: Int, vertical: Int) let blankCellReuseIdentifier: String let highlightedIndexPaths: Set<IndexPath> let selectedIndexPaths: Set<IndexPath> } struct DataSourceSnapshot { let frozenColumns: Int let frozenRows: Int let columnWidthCache: [CGFloat] let rowHeightCache: [CGFloat] } init(spreadsheetViewConfiguration: SpreadsheetViewConfiguration, dataSourceSnapshot: DataSourceSnapshot, scrollViewConfiguration: ScrollViewConfiguration, scrollViewState: ScrollView.State) { self.spreadsheetViewConfiguration = spreadsheetViewConfiguration self.dataSourceSnapshot = dataSourceSnapshot self.scrollViewConfiguration = scrollViewConfiguration visibleRect = CGRect(origin: scrollViewState.contentOffset, size: scrollViewState.frame.size) cellOrigin = .zero }
  19. Tips kk@realm.io func resetContentSize(of scrollView: ScrollView) { scrollView.columnRecords.removeAll() scrollView.rowRecords.removeAll() let

    startColumn = scrollView.layoutAttributes.startColumn let columnCount = scrollView.layoutAttributes.columnCount var width: CGFloat = 0 for column in startColumn..<columnCount { scrollView.columnRecords.append(width) let index = column % numberOfColumns if !circularScrollingOptions.tableStyle.contains(.columnHeaderNotRepeated) || index >= startColumn { width += layoutProperties.columnWidthCache[index] + intercellSpacing.width } } ... scrollView.state.contentSize = CGSize(width: width + intercellSpacing.width, height: height + intercellSpacing.height) }
  20. Tips kk@realm.io func resetContentSize(of scrollView: ScrollView) { scrollView.columnRecords.removeAll() scrollView.rowRecords.removeAll() let

    startColumn = scrollView.layoutAttributes.startColumn let columnCount = scrollView.layoutAttributes.columnCount var width: CGFloat = 0 for column in startColumn..<columnCount { scrollView.columnRecords.append(width) let index = column % numberOfColumns if !circularScrollingOptions.tableStyle.contains(.columnHeaderNotRepeated) || index >= startColumn { width += layoutProperties.columnWidthCache[index] + intercellSpacing.width } } ... scrollView.state.contentSize = CGSize(width: width + intercellSpacing.width, height: height + intercellSpacing.height) }
  21. Tips kk@realm.io static func resetContentSize(of scrollView: ScrollView) { scrollView.columnRecords.removeAll() scrollView.rowRecords.removeAll()

    let startColumn = scrollView.layoutAttributes.startColumn let columnCount = scrollView.layoutAttributes.columnCount var width: CGFloat = 0 for column in startColumn..<columnCount { ... } ... scrollView.state.contentSize = CGSize(width: width + intercellSpacing.width, height: height + intercellSpacing.height) }
  22. Tips kk@realm.io func resetContentSize(scrollView: ScrollView) { SpreadsheetView.resetContentSize(scrollView: scrollView, spreadsheetViewConfiguration: spreadsheetViewConfiguration,

    layoutProperties: layoutProperties) } static func resetContentSize(scrollView: ScrollView, spreadsheetViewConfiguration: SpreadsheetViewConfiguration, layoutProperties: LayoutProperties) { let intercellSpacing = spreadsheetViewConfiguration.intercellSpacing let circularScrollingOptions = spreadsheetViewConfiguration.circularScrollingOptions ... var width: CGFloat = 0 for column in startColumn..<columnCount { ... } ... scrollView.state.contentSize = CGSize(width: width + intercellSpacing.width, height: height + intercellSpacing.height) }
  23. kk@realm.io scrollView.insertSubview(cell, at: 0) struct Layouter: ViewLayouter { let scrollView:

    ScrollView func layout(cell: Cell) { scrollView.insertSubview(cell, at: 0) } }
  24. kk@realm.io scrollView.insertSubview(cell, at: 0) struct DebugLayouter: ViewLayouter { var cells

    = [CellInfo]() mutating func layout(cell: Cell) { cells.append(CellInfo(frame: cell.frame, indexPath: cell.indexPath)) } }
  25. kk@realm.io func testLayout() { // State let spreadsheetViewConfiguration = SpreadsheetViewConfiguration(intercellSpacing:

    CGSize(width: 4, height defaultGridStyle: .none, circularScrollingOptions: CircularScrolli let dataSourceSnapshot = DataSourceSnapshot(frozenColumns: 0, frozenRows: 0) let dataSource = DataSource(spreadsheetViewConfiguration: spreadsheetViewConfiguration, dataSourceSnapsho // Parameters let contentOffset = CGPoint(x: 0, y: 0) let scrollViewState = ScrollView.State(frame: CGRect(x: 0, y: 0, width: 320, height: 567), contentOffset: // Test let layoutEngine = LayoutEngine(spreadsheetViewConfiguration: spreadsheetViewConfiguration, dataSourceSnapshot: dataSourceSnapshot, scrollViewConfiguration: scrollViewConfiguration, scrollViewState: scrollViewState) layoutEngine.layout() // Assert XCTAssertEqual(layoutEngine.layouter, ...) ...
  26. kk@realm.io Test Suite 'Selected tests' started at 2017-09-15 23:36:31.590 Test

    Suite 'SpreadsheetViewTests.xctest' started at 2017-09-15 23:36:31.591 Test Suite 'LayoutTests' started at 2017-09-15 23:36:31.591 Test Case '-[SpreadsheetViewTests.LayoutTests testLayout]' started. | R0C0 (4.0, 4.0, 60.0, 40.0)| R0C1 (68.0, 4.0, 60.0, 40.0)| R0C2 (132.0, 4.0, 60.0, 40.0)| | R1C0 (4.0, 48.0, 60.0, 40.0)| R1C1 (68.0, 48.0, 60.0, 40.0)| R1C2 (132.0, 48.0, 60.0, 40.0)| | R2C0 (4.0, 92.0, 60.0, 40.0)| R2C1 (68.0, 92.0, 60.0, 40.0)| R2C2 (132.0, 92.0, 60.0, 40.0)| | R3C0 (4.0, 136.0, 60.0, 40.0)| R3C1 (68.0, 136.0, 60.0, 40.0)| R3C2 (132.0, 136.0, 60.0, 40.0)| | R4C0 (4.0, 180.0, 60.0, 40.0)| R4C1 (68.0, 180.0, 60.0, 40.0)| R4C2 (132.0, 180.0, 60.0, 40.0)| | R5C0 (4.0, 224.0, 60.0, 40.0)| R5C1 (68.0, 224.0, 60.0, 40.0)| R5C2 (132.0, 224.0, 60.0, 40.0)| | R6C0 (4.0, 268.0, 60.0, 40.0)| R6C1 (68.0, 268.0, 60.0, 40.0)| R6C2 (132.0, 268.0, 60.0, 40.0)| | R7C0 (4.0, 312.0, 60.0, 40.0)| R7C1 (68.0, 312.0, 60.0, 40.0)| R7C2 (132.0, 312.0, 60.0, 40.0)| | R8C0 (4.0, 356.0, 60.0, 40.0)| R8C1 (68.0, 356.0, 60.0, 40.0)| R8C2 (132.0, 356.0, 60.0, 40.0)| | R9C0 (4.0, 400.0, 60.0, 40.0)| R9C1 (68.0, 400.0, 60.0, 40.0)| R9C2 (132.0, 400.0, 60.0, 40.0)| | R10C0 (4.0, 444.0, 60.0, 40.0)| R10C1 (68.0, 444.0, 60.0, 40.0)| R10C2 (132.0, 444.0, 60.0, 40.0)| | R11C0 (4.0, 488.0, 60.0, 40.0)| R11C1 (68.0, 488.0, 60.0, 40.0)| R11C2 (132.0, 488.0, 60.0, 40.0)| | R12C0 (4.0, 532.0, 60.0, 40.0)| R12C1 (68.0, 532.0, 60.0, 40.0)| R12C2 (132.0, 532.0, 60.0, 40.0)| ...