Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Speaker Deck
PRO
Sign in
Sign up for free
Building High Performance and Testable UI component
Kishikawa Katsumi
September 16, 2017
Programming
7
36k
Building High Performance and Testable UI component
Kishikawa Katsumi
September 16, 2017
Tweet
Share
More Decks by Kishikawa Katsumi
See All by Kishikawa Katsumi
家のいろいろな数値を計測する
kishikawakatsumi
2
1k
GitHub Actionsでテストの結果をわかりやすく表示する
kishikawakatsumi
1
130
GitHub Actionsでテストの結果をわかりやすく表示する
kishikawakatsumi
1
670
Network ExtensionでiOSデバイス上で動くパケットキャプチャを作る
kishikawakatsumi
7
4.1k
Xcode Cloud at a glance
kishikawakatsumi
2
700
StoreKit Testingについて
kishikawakatsumi
1
780
Build Swift Web Playground
kishikawakatsumi
1
120
SourceKit-LSPを使ってWebブラウザでSwiftの入力補完を実現する
kishikawakatsumi
1
2.2k
400種類のアプリを毎日ビルドする自動化の技術
kishikawakatsumi
15
7.5k
Other Decks in Programming
See All in Programming
Learning DDD輪読会#4 / Learning DDD Book Club #4
suzushin54
1
150
Cloud-Conference-Day-Spring Cloud + Spring Webflux: como desenvolver seu primeiro microsserviço reativo em Java?
kamilahsantos
1
140
Kotlin KSP - Intro
taehwandev
1
490
インフラエンジニアの多様性と評価、またはキャリアへのつなげ方 / Careers as infrastructure engineers
katsuhisa91
0
540
スモールチームがAmazon Cognitoでコスパよく作るサービス間連携認証
tacke_jp
2
750
Get Ready for Jakarta EE 10
ivargrimstad
0
2.7k
デュアルトラックアジャイル× Agile Testingから 見えてきたQAのミライ
atamaplus
0
420
microCMS × Shopifyで、ECサイトがリニューアル後急成長した話
microcms
0
470
LegalForceの契約データを脅かすリスクの排除と 開発速度の向上をどうやって両立したか
aibou
0
290
【Qiita Night】新卒エンジニアによるSwift6与太予想
eiji127
0
180
Quartoを使ってみませんか / quarto_get_started
s_uryu
2
320
未経験QAの私が、よきQA(Question Asker) になっていく物語
atamaplus
0
330
Featured
See All Featured
Atom: Resistance is Futile
akmur
255
20k
The Most Common Mistakes in Cover Letters
jrick
PRO
4
24k
Gamification - CAS2011
davidbonilla
75
3.9k
Fantastic passwords and where to find them - at NoRuKo
philnash
25
1.5k
Code Reviewing Like a Champion
maltzj
506
37k
Helping Users Find Their Own Way: Creating Modern Search Experiences
danielanewman
7
1k
Side Projects
sachag
449
37k
Navigating Team Friction
lara
175
11k
jQuery: Nuts, Bolts and Bling
dougneiner
56
6.4k
Robots, Beer and Maslow
schacon
152
7.1k
Easily Structure & Communicate Ideas using Wireframe
afnizarnur
181
15k
The Pragmatic Product Professional
lauravandoore
19
2.9k
Transcript
Building High Performance and Testable UI component iOSDC 2017 Kishikawa
Kishikawa kk@realm.io
Kishikawa Katsumi Realm Inc. kk@realm.io
Agenda • ߴͳUIΛ࡞Δʹ • ෛՙͷߴ͍ॲཧΛݮΒ͢ • ಡΈ͢͞ɺϝϯςφϯεͷ͢͠͞ͱͷτϨʔυΦϑ • ϝϯςφϯε͍͢͠ίʔυΛॻ͘ʹ •
ϝϯςφϯε͍͢͠ʹςετ͍͢͠ • UIίϯϙʔωϯτΛςετ͢Δʹ • ঢ়ଶΛϞσϧʹ͢Δ • ϞσϧςετͰ͖Δʢ͍͢͠ʣ • ബ͍Ϗϡʔͱް͍Ϟσϧ kk@realm.io
kk@realm.io https://github.com/kishikawakatsumi/SpreadsheetView
kk@realm.io https://github.com/kishikawakatsumi/SpreadsheetView
kk@realm.io
Writing High Performance UI Component kk@realm.io
Writing High Performance UI Component kk@realm.io • ܭଌ͢Δ • τϨʔυΦϑ
ෛՙͷߴ͍ॲཧΛݮΒ͢ • UIΛ͘͢ΔཁҼΛΔ • UIView͕ԿΛ͍ͯ͠Δͷ͔Λཧղ͢Δ • UITableView/UICollectionViewͷςΫχοΫ ΛֶͿ kk@realm.io
UIView kk@realm.io • Ϗϡʔͷ͕૿͑Δ΄Ͳ͘ͳΔ • ੜͷίετൺֱతߴ͍
kk@realm.io
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 } }
kk@realm.io
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 }
kk@realm.io
kk@realm.io
kk@realm.io let sum = numbers.reduce(0) { $0 + $1 }
var sum: CGFloat = 0 for n in numbers { sum += n }
kk@realm.io let sum = numbers.reduce(0) { $0 + $1 }
var sum: CGFloat = 0 for n in numbers { sum += n }
τϨʔυΦϑΛΔ kk@realm.io • ύϑΥʔϚϯενϡʔχϯάʹʢ΄ͱΜ Ͳͷ߹ʣτϨʔυΦϑ͕ଘࡏ͢Δ • ίʔυͷಡΈ͢͞ςετͷ͕͢͠͞ ٘ਜ਼ʹͳΔ͜ͱ͕ଟ͍ • ಘΔͷͱࣦ͏ͷͷόϥϯεͷݟۃΊ͕
ͷݟͤͲ͜Ζ
τϨʔυΦϑΛΔ kk@realm.io • ύϑΥʔϚϯενϡʔχϯάʹʢ΄ͱΜ Ͳͷ߹ʣτϨʔυΦϑ͕ଘࡏ͢Δ • ίʔυͷಡΈ͢͞ςετͷ͕͢͠͞ ٘ਜ਼ʹͳΔ͜ͱ͕ଟ͍ • ಘΔͷͱࣦ͏ͷͷόϥϯεͷݟۃΊ͕
ͷݟͤͲ͜Ζ
ύϑΥʔϚϯεͱ ϝϯςφϏϦςΟʢอकੑʣͷཱ྆ kk@realm.io
ϝϯςφϯε͍͢͠ UIίϯϙʔωϯτΛॻ͘ʹ kk@realm.io
ϝϯςφϯε͍͢͠ ςετ͍͢͠ UIίϯϙʔωϯτΛॻ͘ʹ kk@realm.io
UIίϯϙʔωϯτΛ ςετ͢Δ͜ͱ͍͠ kk@realm.io
UIͷςετ͕ࠔͳཧ༝ kk@realm.io • ෦ঢ়ଶ͕ଟ͘ෳࡶ • ঢ়ଶΛมԽͤ͞ΔཁҼ͕ଟ͍ • ঢ়ଶ͕૬ޓ࡞༻Λٴ΅͢ • ਖ਼͍͠ڍಈ͕໌֬Ͱͳ͍
Unit Testing vs UI Testing kk@realm.io • UIςετςετରʹΞΫηεͰ͖ ͳ͍ •
Ξαʔγϣϯ͕͍͠ • ޭɾࣦഊͷج४͕໌֬Ͱͳ͍
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) }
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) } ෦ঢ়ଶͳ͠ ೖྗ ग़ྗ
ؔ ෦ঢ়ଶ ύϥϝʔλ ग़ྗ݁Ռ x, y add() Int
None
෦ঢ়ଶ ύϥϝʔλ ؔ
SpreadsheetView dataSource frame contentOffset ... λς/Ϥί NavigationController Touch ... iPhone/iPad
ؔ ෦ঢ়ଶ ύϥϝʔλ ग़ྗ݁Ռ
෦ঢ়ଶ ύϥϝʔλ SpreadsheetView dataSource frame contentOffset ... λς/Ϥί NavigationController Touch
... iPhone/iPad
ςετ͍͢͠ߏͱ kk@realm.io • σʔλͷྲྀΕΛ̍ํʹ͢Δ • ঢ়ଶΛϞσϧʹ͢Δ • ৼΔ͍ΛϞοΫʹஔ͖͑Δ
σʔλͷྲྀΕΛ̍ํʹ͢Δ kk@realm.io ΞΫγϣϯ ϞσϧΛมߋ ϏϡʔΛߋ৽ λοϓ contentOffset selectedIndexPath εϫΠϓ layoutSubviews()
ঢ়ଶΛϞσϧʹ͢Δ kk@realm.io ʁ
෦ঢ়ଶ ঢ়ଶΛϞσϧʹ͢Δ 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] }
ύϥϝʔλ ঢ়ଶΛϞσϧʹ͢Δ kk@realm.io var contentOffset: CGPoint var frame: CGRect var
orientation: UIDeviceOrientation ... ϝιου public override func layoutSubviews() { reloadDataIfNeeded() layoutCornerView() layoutRowHeaderView() layoutColumnHeaderView() layoutTableView() } ෦ঢ়ଶ
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 } ...
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)) } }
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) { ...
kk@realm.io
෦ঢ়ଶ ύϥϝʔλ SpreadsheetView dataSource frame contentOffset ... λς/Ϥί NavigationController Touch
... iPhone/iPad
ؔ ෦ঢ়ଶ ύϥϝʔλ ग़ྗ݁Ռ x, y add() Int
ঢ়ଶΛϞσϧʹ͢Δ kk@realm.io
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 { ... } } } }
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 { ... } ...
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 { ... } ...
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 { ... } ...
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 { ... } ...
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 }
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) }
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) }
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) }
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) }
Tips kk@realm.io func resetContentSize(...) static func resetContentSize(...) ϓϩμΫγϣϯ ςετ
Tips kk@realm.io init(spreadsheetView: SpreadsheetView, scrollView: ScrollView) init(spreadsheetViewConfiguration: SpreadsheetViewConfiguration, scrollViewConfiguration: ScrollViewConfiguration)
ϓϩμΫγϣϯ ςετ
ৼΔ͍ΛϞοΫʹஔ͖͑Δ kk@realm.io
kk@realm.io let cell = dataSource.spreadsheetView(spreadsheetView, cellForItemAt: indexPath) ... scrollView.insertSubview(cell, at:
0) ... scrollView.addSubview(border)
kk@realm.io let cell = dataSource.spreadsheetView(spreadsheetView, cellForItemAt: indexPath) ... scrollView.insertSubview(cell, at:
0) ... scrollView.addSubview(border)
kk@realm.io protocol ViewLayouter { mutating func layout(cell: Cell) }
kk@realm.io scrollView.insertSubview(cell, at: 0) layouter.layout(cell: cell)
kk@realm.io scrollView.insertSubview(cell, at: 0) struct Layouter: ViewLayouter { let scrollView:
ScrollView func layout(cell: Cell) { scrollView.insertSubview(cell, at: 0) } }
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)) } }
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, ...) ...
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)| ...
Summary kk@realm.io • ςετ͍͢͠ίʔυྑ͍ίʔυ • ςετ͍͢͠ίʔυʹ͢Δʹ • σʔλͷྲྀΕΛҰํ௨ߦʹ͢Δ • ঢ়ଶΛϞσϧʹ͢Δ
• ৼΔ͍ΛϞοΫʹஔ͖͑Δ • ബ͍Ϗϡʔʢίϯτϩʔϥʣɺް͍Ϟσϧ
Questions? Katsumi Kishikawa kk@realm.io www.realm.io @k_katsumi kk@realm.io