Slide 1

Slide 1 text

Building High Performance and Testable UI component iOSDC 2017 Kishikawa Kishikawa [email protected]

Slide 2

Slide 2 text

Kishikawa Katsumi Realm Inc. [email protected]

Slide 3

Slide 3 text

Agenda • ߴ଎ͳUIΛ࡞Δʹ͸ • ෛՙͷߴ͍ॲཧΛݮΒ͢ • ಡΈ΍͢͞ɺϝϯςφϯεͷ͠΍͢͞ͱͷτϨʔυΦϑ • ϝϯςφϯε͠΍͍͢ίʔυΛॻ͘ʹ͸ • ϝϯςφϯε͠΍͍͢ʹςετ͠΍͍͢ • UIίϯϙʔωϯτΛςετ͢Δʹ͸ • ঢ়ଶΛϞσϧʹ෼཭͢Δ • Ϟσϧ͸ςετͰ͖Δʢ͠΍͍͢ʣ • ബ͍Ϗϡʔͱ෼ް͍Ϟσϧ [email protected]

Slide 4

Slide 4 text

[email protected] https://github.com/kishikawakatsumi/SpreadsheetView

Slide 5

Slide 5 text

[email protected] https://github.com/kishikawakatsumi/SpreadsheetView

Slide 6

Slide 6 text

Slide 7

Slide 7 text

Writing High Performance UI Component [email protected]

Slide 8

Slide 8 text

Writing High Performance UI Component [email protected] • ܭଌ͢Δ • τϨʔυΦϑ

Slide 9

Slide 9 text

ෛՙͷߴ͍ॲཧΛݮΒ͢ • UIΛ஗͘͢ΔཁҼΛ஌Δ • UIView͕ԿΛ͍ͯ͠Δͷ͔Λཧղ͢Δ • UITableView/UICollectionViewͷςΫχοΫ ΛֶͿ [email protected]

Slide 10

Slide 10 text

UIView [email protected] • Ϗϡʔͷ਺͕૿͑Δ΄Ͳ஗͘ͳΔ • ੜ੒ͷίετ͸ൺֱతߴ͍

Slide 11

Slide 11 text

Slide 12

Slide 12 text

[email protected] func layout() { for 0 in 0..

Slide 13

Slide 13 text

Slide 14

Slide 14 text

[email protected] func layout() { let startRow = spreadsheetView.findIndex(in: scrollView.rowRecords, for: visibleRect.origin.y) cellOrigin.y = scrollView.rowRecords[startRow] for row in startRow.. 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 }

Slide 15

Slide 15 text

Slide 16

Slide 16 text

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

ύϑΥʔϚϯεͱ ϝϯςφϏϦςΟʢอकੑʣͷཱ྆ [email protected]

Slide 22

Slide 22 text

ϝϯςφϯε͠΍͍͢ UIίϯϙʔωϯτΛॻ͘ʹ͸ [email protected]

Slide 23

Slide 23 text

ϝϯςφϯε͠΍͍͢ ςετ͠΍͍͢ UIίϯϙʔωϯτΛॻ͘ʹ͸ [email protected]

Slide 24

Slide 24 text

UIίϯϙʔωϯτΛ ςετ͢Δ͜ͱ͸೉͍͠ [email protected]

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

Unit Testing vs UI Testing [email protected] • UIςετ͸ςετର৅ʹ௚઀ΞΫηεͰ͖ ͳ͍ • Ξαʔγϣϯ͕೉͍͠ • ੒ޭɾࣦഊͷج४͕໌֬Ͱͳ͍

Slide 27

Slide 27 text

[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) }

Slide 28

Slide 28 text

[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) } ಺෦ঢ়ଶͳ͠ ೖྗ ग़ྗ

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

No content

Slide 31

Slide 31 text

಺෦ঢ়ଶ ύϥϝʔλ ؔ਺

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

ςετ͠΍͍͢ߏ଄ͱ͸ [email protected] • σʔλͷྲྀΕΛ̍ํ޲ʹ͢Δ • ঢ়ଶΛϞσϧʹ෼཭͢Δ • ৼΔ෣͍ΛϞοΫʹஔ͖׵͑Δ

Slide 36

Slide 36 text

σʔλͷྲྀΕΛ̍ํ޲ʹ͢Δ [email protected] ΞΫγϣϯ ϞσϧΛมߋ ϏϡʔΛߋ৽ λοϓ contentOffset selectedIndexPath εϫΠϓ layoutSubviews()

Slide 37

Slide 37 text

ঢ়ଶΛϞσϧʹ෼཭͢Δ [email protected] ʁ

Slide 38

Slide 38 text

಺෦ঢ়ଶ ঢ়ଶΛϞσϧʹ෼཭͢Δ [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] }

Slide 39

Slide 39 text

ύϥϝʔλ ঢ়ଶΛϞσϧʹ෼཭͢Δ [email protected] var contentOffset: CGPoint var frame: CGRect var orientation: UIDeviceOrientation ... ϝιου public override func layoutSubviews() { reloadDataIfNeeded() layoutCornerView() layoutRowHeaderView() layoutColumnHeaderView() layoutTableView() } ಺෦ঢ়ଶ

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

[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)) } }

Slide 42

Slide 42 text

[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) { ...

Slide 43

Slide 43 text

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

ঢ়ଶΛϞσϧʹ෼཭͢Δ [email protected]

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

[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 let selectedIndexPaths: Set } 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 }

Slide 53

Slide 53 text

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..= startColumn { width += layoutProperties.columnWidthCache[index] + intercellSpacing.width } } ... scrollView.state.contentSize = CGSize(width: width + intercellSpacing.width, height: height + intercellSpacing.height) }

Slide 54

Slide 54 text

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..= startColumn { width += layoutProperties.columnWidthCache[index] + intercellSpacing.width } } ... scrollView.state.contentSize = CGSize(width: width + intercellSpacing.width, height: height + intercellSpacing.height) }

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

Tips [email protected] func resetContentSize(...) static func resetContentSize(...) ϓϩμΫγϣϯ ςετ

Slide 58

Slide 58 text

Tips [email protected] init(spreadsheetView: SpreadsheetView, scrollView: ScrollView) init(spreadsheetViewConfiguration: SpreadsheetViewConfiguration, scrollViewConfiguration: ScrollViewConfiguration) ϓϩμΫγϣϯ ςετ

Slide 59

Slide 59 text

ৼΔ෣͍ΛϞοΫʹஔ͖׵͑Δ [email protected]

Slide 60

Slide 60 text

[email protected] let cell = dataSource.spreadsheetView(spreadsheetView, cellForItemAt: indexPath) ... scrollView.insertSubview(cell, at: 0) ... scrollView.addSubview(border)

Slide 61

Slide 61 text

[email protected] let cell = dataSource.spreadsheetView(spreadsheetView, cellForItemAt: indexPath) ... scrollView.insertSubview(cell, at: 0) ... scrollView.addSubview(border)

Slide 62

Slide 62 text

[email protected] protocol ViewLayouter { mutating func layout(cell: Cell) }

Slide 63

Slide 63 text

[email protected] scrollView.insertSubview(cell, at: 0) layouter.layout(cell: cell)

Slide 64

Slide 64 text

[email protected] scrollView.insertSubview(cell, at: 0) struct Layouter: ViewLayouter { let scrollView: ScrollView func layout(cell: Cell) { scrollView.insertSubview(cell, at: 0) } }

Slide 65

Slide 65 text

[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)) } }

Slide 66

Slide 66 text

[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, ...) ...

Slide 67

Slide 67 text

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

Slide 68

Slide 68 text

Summary [email protected] • ςετ͠΍͍͢ίʔυ͸ྑ͍ίʔυ • ςετ͠΍͍͢ίʔυʹ͢Δʹ͸ • σʔλͷྲྀΕΛҰํ௨ߦʹ͢Δ • ঢ়ଶΛϞσϧʹ෼཭͢Δ • ৼΔ෣͍ΛϞοΫʹஔ͖׵͑Δ • ബ͍Ϗϡʔʢίϯτϩʔϥʣɺ෼ް͍Ϟσϧ

Slide 69

Slide 69 text

Questions? Katsumi Kishikawa [email protected] www.realm.io @k_katsumi [email protected]