$30 off During Our Annual Pro Sale. View Details »

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. 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