Building High Performance and Testable UI component

Building High Performance and Testable UI component

9bf923e39671cde83584e3e926296c13?s=128

Kishikawa Katsumi

September 16, 2017
Tweet

Transcript

  1. Building High Performance and Testable UI component iOSDC 2017 Kishikawa

    Kishikawa kk@realm.io
  2. Kishikawa Katsumi Realm Inc. kk@realm.io

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

    ϝϯςφϯε͠΍͍͢ʹςετ͠΍͍͢ • UIίϯϙʔωϯτΛςετ͢Δʹ͸ • ঢ়ଶΛϞσϧʹ෼཭͢Δ • Ϟσϧ͸ςετͰ͖Δʢ͠΍͍͢ʣ • ബ͍Ϗϡʔͱ෼ް͍Ϟσϧ kk@realm.io
  4. kk@realm.io https://github.com/kishikawakatsumi/SpreadsheetView

  5. kk@realm.io https://github.com/kishikawakatsumi/SpreadsheetView

  6. kk@realm.io

  7. Writing High Performance UI Component kk@realm.io

  8. Writing High Performance UI Component kk@realm.io • ܭଌ͢Δ • τϨʔυΦϑ

  9. ෛՙͷߴ͍ॲཧΛݮΒ͢ • UIΛ஗͘͢ΔཁҼΛ஌Δ • UIView͕ԿΛ͍ͯ͠Δͷ͔Λཧղ͢Δ • UITableView/UICollectionViewͷςΫχοΫ ΛֶͿ kk@realm.io

  10. UIView kk@realm.io • Ϗϡʔͷ਺͕૿͑Δ΄Ͳ஗͘ͳΔ • ੜ੒ͷίετ͸ൺֱతߴ͍

  11. kk@realm.io

  12. 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 } }
  13. kk@realm.io

  14. 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 }
  15. kk@realm.io

  16. kk@realm.io

  17. kk@realm.io let sum = numbers.reduce(0) { $0 + $1 }

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

    var sum: CGFloat = 0 for n in numbers { sum += n }
  19. τϨʔυΦϑΛ஌Δ kk@realm.io • ύϑΥʔϚϯενϡʔχϯάʹ͸ʢ΄ͱΜ Ͳͷ৔߹ʣτϨʔυΦϑ͕ଘࡏ͢Δ • ίʔυͷಡΈ΍͢͞΍ςετͷ͠΍͕͢͞ ٘ਜ਼ʹͳΔ͜ͱ͕ଟ͍ • ಘΔ΋ͷͱࣦ͏΋ͷͷόϥϯεͷݟۃΊ͕

    ࿹ͷݟͤͲ͜Ζ
  20. τϨʔυΦϑΛ஌Δ kk@realm.io • ύϑΥʔϚϯενϡʔχϯάʹ͸ʢ΄ͱΜ Ͳͷ৔߹ʣτϨʔυΦϑ͕ଘࡏ͢Δ • ίʔυͷಡΈ΍͢͞΍ςετͷ͠΍͕͢͞ ٘ਜ਼ʹͳΔ͜ͱ͕ଟ͍ • ಘΔ΋ͷͱࣦ͏΋ͷͷόϥϯεͷݟۃΊ͕

    ࿹ͷݟͤͲ͜Ζ
  21. ύϑΥʔϚϯεͱ ϝϯςφϏϦςΟʢอकੑʣͷཱ྆ kk@realm.io

  22. ϝϯςφϯε͠΍͍͢ UIίϯϙʔωϯτΛॻ͘ʹ͸ kk@realm.io

  23. ϝϯςφϯε͠΍͍͢ ςετ͠΍͍͢ UIίϯϙʔωϯτΛॻ͘ʹ͸ kk@realm.io

  24. UIίϯϙʔωϯτΛ ςετ͢Δ͜ͱ͸೉͍͠ kk@realm.io

  25. UIͷςετ͕ࠔ೉ͳཧ༝ kk@realm.io • ಺෦ঢ়ଶ͕ଟ͘ෳࡶ • ঢ়ଶΛมԽͤ͞ΔཁҼ͕ଟ͍ • ঢ়ଶ͕૬ޓ࡞༻Λٴ΅͢ • ਖ਼͍͠ڍಈ͕໌֬Ͱͳ͍

  26. Unit Testing vs UI Testing kk@realm.io • UIςετ͸ςετର৅ʹ௚઀ΞΫηεͰ͖ ͳ͍ •

    Ξαʔγϣϯ͕೉͍͠ • ੒ޭɾࣦഊͷج४͕໌֬Ͱͳ͍
  27. 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) }
  28. 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) } ಺෦ঢ়ଶͳ͠ ೖྗ ग़ྗ
  29. ؔ਺ ಺෦ঢ়ଶ ύϥϝʔλ ग़ྗ݁Ռ x, y add() Int

  30. None
  31. ಺෦ঢ়ଶ ύϥϝʔλ ؔ਺

  32. SpreadsheetView dataSource frame contentOffset ... λς/Ϥί NavigationController Touch ... iPhone/iPad

  33. ؔ਺ ಺෦ঢ়ଶ ύϥϝʔλ ग़ྗ݁Ռ

  34. ಺෦ঢ়ଶ ύϥϝʔλ SpreadsheetView dataSource frame contentOffset ... λς/Ϥί NavigationController Touch

    ... iPhone/iPad
  35. ςετ͠΍͍͢ߏ଄ͱ͸ kk@realm.io • σʔλͷྲྀΕΛ̍ํ޲ʹ͢Δ • ঢ়ଶΛϞσϧʹ෼཭͢Δ • ৼΔ෣͍ΛϞοΫʹஔ͖׵͑Δ

  36. σʔλͷྲྀΕΛ̍ํ޲ʹ͢Δ kk@realm.io ΞΫγϣϯ ϞσϧΛมߋ ϏϡʔΛߋ৽ λοϓ contentOffset selectedIndexPath εϫΠϓ layoutSubviews()

  37. ঢ়ଶΛϞσϧʹ෼཭͢Δ kk@realm.io ʁ

  38. ಺෦ঢ়ଶ ঢ়ଶΛϞσϧʹ෼཭͢Δ 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] }
  39. ύϥϝʔλ ঢ়ଶΛϞσϧʹ෼཭͢Δ kk@realm.io var contentOffset: CGPoint var frame: CGRect var

    orientation: UIDeviceOrientation ... ϝιου public override func layoutSubviews() { reloadDataIfNeeded() layoutCornerView() layoutRowHeaderView() layoutColumnHeaderView() layoutTableView() } ಺෦ঢ়ଶ
  40. 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 } ...
  41. 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)) } }
  42. 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) { ...
  43. kk@realm.io

  44. ಺෦ঢ়ଶ ύϥϝʔλ SpreadsheetView dataSource frame contentOffset ... λς/Ϥί NavigationController Touch

    ... iPhone/iPad
  45. ؔ਺ ಺෦ঢ়ଶ ύϥϝʔλ ग़ྗ݁Ռ x, y add() Int

  46. ঢ়ଶΛϞσϧʹ෼཭͢Δ kk@realm.io

  47. 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 { ... } } } }
  48. 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 { ... } ...
  49. 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 { ... } ...
  50. 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 { ... } ...
  51. 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 { ... } ...
  52. 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 }
  53. 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) }
  54. 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) }
  55. 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) }
  56. 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) }
  57. Tips kk@realm.io func resetContentSize(...) static func resetContentSize(...) ϓϩμΫγϣϯ ςετ

  58. Tips kk@realm.io init(spreadsheetView: SpreadsheetView, scrollView: ScrollView) init(spreadsheetViewConfiguration: SpreadsheetViewConfiguration, scrollViewConfiguration: ScrollViewConfiguration)

    ϓϩμΫγϣϯ ςετ
  59. ৼΔ෣͍ΛϞοΫʹஔ͖׵͑Δ kk@realm.io

  60. kk@realm.io let cell = dataSource.spreadsheetView(spreadsheetView, cellForItemAt: indexPath) ... scrollView.insertSubview(cell, at:

    0) ... scrollView.addSubview(border)
  61. kk@realm.io let cell = dataSource.spreadsheetView(spreadsheetView, cellForItemAt: indexPath) ... scrollView.insertSubview(cell, at:

    0) ... scrollView.addSubview(border)
  62. kk@realm.io protocol ViewLayouter { mutating func layout(cell: Cell) }

  63. kk@realm.io scrollView.insertSubview(cell, at: 0) layouter.layout(cell: cell)

  64. kk@realm.io scrollView.insertSubview(cell, at: 0) struct Layouter: ViewLayouter { let scrollView:

    ScrollView func layout(cell: Cell) { scrollView.insertSubview(cell, at: 0) } }
  65. 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)) } }
  66. 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, ...) ...
  67. 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)| ...
  68. Summary kk@realm.io • ςετ͠΍͍͢ίʔυ͸ྑ͍ίʔυ • ςετ͠΍͍͢ίʔυʹ͢Δʹ͸ • σʔλͷྲྀΕΛҰํ௨ߦʹ͢Δ • ঢ়ଶΛϞσϧʹ෼཭͢Δ

    • ৼΔ෣͍ΛϞοΫʹஔ͖׵͑Δ • ബ͍Ϗϡʔʢίϯτϩʔϥʣɺ෼ް͍Ϟσϧ
  69. Questions? Katsumi Kishikawa kk@realm.io www.realm.io @k_katsumi kk@realm.io