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

Building High Performance and Testable UI component

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ίϯϙʔωϯτΛςετ͢Δʹ͸ • ঢ়ଶΛϞσϧʹ෼཭͢Δ • Ϟσϧ͸ςετͰ͖Δʢ͠΍͍͢ʣ • ബ͍Ϗϡʔͱ෼ް͍Ϟσϧ [email protected]
  2. [email protected] 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. [email protected] 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. [email protected] let sum = numbers.reduce(0) { $0 + $1 }

    var sum: CGFloat = 0 for n in numbers { sum += n }
  5. [email protected] let sum = numbers.reduce(0) { $0 + $1 }

    var sum: CGFloat = 0 for n in numbers { sum += n }
  6. [email protected] 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. [email protected] 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. ಺෦ঢ়ଶ ঢ়ଶΛϞσϧʹ෼཭͢Δ [email protected] 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. ύϥϝʔλ ঢ়ଶΛϞσϧʹ෼཭͢Δ [email protected] var contentOffset: CGPoint var frame: CGRect var

    orientation: UIDeviceOrientation ... ϝιου public override func layoutSubviews() { reloadDataIfNeeded() layoutCornerView() layoutRowHeaderView() layoutColumnHeaderView() layoutTableView() } ಺෦ঢ়ଶ
  10. [email protected] 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. [email protected] ... 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. [email protected] ... 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. [email protected] 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. [email protected] 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. [email protected] 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. [email protected] 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. [email protected] 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. [email protected] 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 [email protected] 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 [email protected] 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 [email protected] 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 [email protected] 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. [email protected] scrollView.insertSubview(cell, at: 0) struct Layouter: ViewLayouter { let scrollView:

    ScrollView func layout(cell: Cell) { scrollView.insertSubview(cell, at: 0) } }
  24. [email protected] 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. [email protected] 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. [email protected] 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)| ...