From 4d048ba5d63dcf7557f791ed19bae7dd01a25670 Mon Sep 17 00:00:00 2001 From: onevcat Date: Mon, 1 Apr 2019 14:09:31 +0900 Subject: [PATCH 1/3] Add tests for column data store and minor refactor --- LineSDK/LineSDK.xcodeproj/project.pbxproj | 12 ++ .../LineSDK/SharingUI/ColumnDataStore.swift | 12 +- ...hareTargetSearchResultViewController.swift | 6 +- .../ShareTargetSelectingTableCell.swift | 6 +- .../ShareTargetSelectingViewController.swift | 2 +- .../Sharing/ColumnDataStoreTests.swift | 112 ++++++++++++++++++ 6 files changed, 138 insertions(+), 12 deletions(-) create mode 100644 LineSDK/LineSDKTests/Sharing/ColumnDataStoreTests.swift diff --git a/LineSDK/LineSDK.xcodeproj/project.pbxproj b/LineSDK/LineSDK.xcodeproj/project.pbxproj index 0eb397be..2caa9642 100644 --- a/LineSDK/LineSDK.xcodeproj/project.pbxproj +++ b/LineSDK/LineSDK.xcodeproj/project.pbxproj @@ -204,6 +204,7 @@ 4BDD2C342133F32200DD563D /* LineSDKFlexSeparatorComponent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BDD2C332133F32200DD563D /* LineSDKFlexSeparatorComponent.swift */; }; 4BDD2C362133F32F00DD563D /* LineSDKSpacerComponent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BDD2C352133F32F00DD563D /* LineSDKSpacerComponent.swift */; }; 4BDD2C382133F34300DD563D /* LineSDKFlexBoxComponent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BDD2C372133F34300DD563D /* LineSDKFlexBoxComponent.swift */; }; + 4BE4E0FC2251CEE10071FC60 /* ColumnDataStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BE4E0FB2251CEE10071FC60 /* ColumnDataStoreTests.swift */; }; 4BEB491A212B94BC00BA946A /* FlexImageComponent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BEB4919212B94BC00BA946A /* FlexImageComponent.swift */; }; 4BEB491C212B992B00BA946A /* FlexImageComponentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BEB491B212B992B00BA946A /* FlexImageComponentTests.swift */; }; 4BEB491E212B9E5500BA946A /* FlexFillerComponent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BEB491D212B9E5500BA946A /* FlexFillerComponent.swift */; }; @@ -495,6 +496,7 @@ 4BDD2C332133F32200DD563D /* LineSDKFlexSeparatorComponent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LineSDKFlexSeparatorComponent.swift; sourceTree = ""; }; 4BDD2C352133F32F00DD563D /* LineSDKSpacerComponent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LineSDKSpacerComponent.swift; sourceTree = ""; }; 4BDD2C372133F34300DD563D /* LineSDKFlexBoxComponent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LineSDKFlexBoxComponent.swift; sourceTree = ""; }; + 4BE4E0FB2251CEE10071FC60 /* ColumnDataStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColumnDataStoreTests.swift; sourceTree = ""; }; 4BEB4919212B94BC00BA946A /* FlexImageComponent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlexImageComponent.swift; sourceTree = ""; }; 4BEB491B212B992B00BA946A /* FlexImageComponentTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlexImageComponentTests.swift; sourceTree = ""; }; 4BEB491D212B9E5500BA946A /* FlexFillerComponent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlexFillerComponent.swift; sourceTree = ""; }; @@ -824,6 +826,7 @@ 4B90588521006E5D004D717F /* LineSDKTests */ = { isa = PBXGroup; children = ( + 4BE4E0FA2251C9090071FC60 /* Sharing */, DB0AFF5F2247FB06002729AD /* UI */, 4B15EEF9211D5BE800866E6C /* Message */, 4B792FB421103CFD00EDDD1E /* API */, @@ -1117,6 +1120,14 @@ path = Component; sourceTree = ""; }; + 4BE4E0FA2251C9090071FC60 /* Sharing */ = { + isa = PBXGroup; + children = ( + 4BE4E0FB2251CEE10071FC60 /* ColumnDataStoreTests.swift */, + ); + path = Sharing; + sourceTree = ""; + }; 4BF45A872137B27F00CCD28E /* Crypto */ = { isa = PBXGroup; children = ( @@ -1742,6 +1753,7 @@ 4BEB491C212B992B00BA946A /* FlexImageComponentTests.swift in Sources */, 4B08F9C22119863500B140DF /* LineSDKErrorTests.swift in Sources */, 4B792FB121102D9200EDDD1E /* LoginProcessURLResponseTests.swift in Sources */, + 4BE4E0FC2251CEE10071FC60 /* ColumnDataStoreTests.swift in Sources */, 4B5EE2E6212BD53E0009DF2E /* FlexBubbleContainerTests.swift in Sources */, 4BF45A8B2137B2C500CCD28E /* RSAKeyTests.swift in Sources */, 4BFC09F5213CF5B200F4594D /* GetDiscoveryDocumentRequestTests.swift in Sources */, diff --git a/LineSDK/LineSDK/SharingUI/ColumnDataStore.swift b/LineSDK/LineSDK/SharingUI/ColumnDataStore.swift index 679c593a..08f18f6d 100644 --- a/LineSDK/LineSDK/SharingUI/ColumnDataStore.swift +++ b/LineSDK/LineSDK/SharingUI/ColumnDataStore.swift @@ -35,8 +35,8 @@ extension LineSDKNotificationKey { class ColumnDataStore { struct ColumnIndex: Equatable { - let row: Int let column: Int + let row: Int } struct AppendingIndexRange { @@ -105,7 +105,7 @@ class ColumnDataStore { ) } - let targetIndex = ColumnIndex(row: rowIndex, column: columnIndex) + let targetIndex = ColumnIndex(column: columnIndex, row: rowIndex) if let index = selected.firstIndex(of: targetIndex) { selected.remove(at: index) notifySelectingChange(selected: false, targetIndex: targetIndex) @@ -120,11 +120,15 @@ class ColumnDataStore { return true } - func indexes(atColumn column: Int, filtered: ((T) -> Bool)) -> [ColumnIndex] { + func indexes(atColumn column: Int, where filtered: ((T) -> Bool)) -> [ColumnIndex] { return data(atColumn: column) .enumerated() .filter { _, elem in filtered(elem) } - .map { ColumnIndex(row: $0.offset, column: column) } + .map { ColumnIndex(column: column, row: $0.offset) } + } + + func indexes(where filtered: ((T) -> Bool)) -> [[ColumnIndex]] { + return (0 ..< data.count).map { indexes(atColumn: $0, where: filtered) } } } diff --git a/LineSDK/LineSDK/SharingUI/ShareTargetSearchResultViewController.swift b/LineSDK/LineSDK/SharingUI/ShareTargetSearchResultViewController.swift index 5df116dc..6ddc4471 100644 --- a/LineSDK/LineSDK/SharingUI/ShareTargetSearchResultViewController.swift +++ b/LineSDK/LineSDK/SharingUI/ShareTargetSearchResultViewController.swift @@ -42,10 +42,8 @@ final class ShareTargetSearchResultViewController: UITableViewController, ShareT var searchText: String = "" { didSet { guard searchText != oldValue else { return } - filteredIndexes = MessageShareTargetType.allCases.map { - store.indexes(atColumn: $0.rawValue) { - $0.displayName.localizedCaseInsensitiveContains(searchText) - } + filteredIndexes = store.indexes { + $0.displayName.localizedCaseInsensitiveContains(searchText) } } } diff --git a/LineSDK/LineSDK/SharingUI/ShareTargetSelectingTableCell.swift b/LineSDK/LineSDK/SharingUI/ShareTargetSelectingTableCell.swift index 4fdc1961..109d050c 100644 --- a/LineSDK/LineSDK/SharingUI/ShareTargetSelectingTableCell.swift +++ b/LineSDK/LineSDK/SharingUI/ShareTargetSelectingTableCell.swift @@ -128,7 +128,7 @@ extension ShareTargetSelectingTableCell { func placeholderUserImage(for name: String) -> UIImage? { let value = name.count % 4 + 1 - return UIImage(named: "unknown_user_small_0\(value)", in: Bundle.frameworkBundle, compatibleWith: nil) + return UIImage(named: "unknown_user_small_0\(value)", in: .frameworkBundle, compatibleWith: nil) } func setShareTarget(_ target: ShareTarget, selected: Bool, highlightText: String? = nil) { @@ -144,8 +144,8 @@ extension ShareTargetSelectingTableCell { currentImageDownloadToken = token let selectedImage = selected ? - UIImage(named: "friend_check_on", in: Bundle.frameworkBundle, compatibleWith: nil) : - UIImage(named: "friend_check_off", in: Bundle.frameworkBundle, compatibleWith: nil) + UIImage(named: "friend_check_on", in: .frameworkBundle, compatibleWith: nil) : + UIImage(named: "friend_check_off", in: .frameworkBundle, compatibleWith: nil) tickImageView.image = selectedImage } } diff --git a/LineSDK/LineSDK/SharingUI/ShareTargetSelectingViewController.swift b/LineSDK/LineSDK/SharingUI/ShareTargetSelectingViewController.swift index 81ee4542..303a8dbc 100644 --- a/LineSDK/LineSDK/SharingUI/ShareTargetSelectingViewController.swift +++ b/LineSDK/LineSDK/SharingUI/ShareTargetSelectingViewController.swift @@ -147,7 +147,7 @@ extension ShareTargetSelectingViewController { withIdentifier: ShareTargetSelectingTableCell.reuseIdentifier, for: indexPath) as! ShareTargetSelectingTableCell - let dataIndex = ColumnIndex(row: indexPath.row, column: columnIndex) + let dataIndex = ColumnIndex(column: columnIndex, row: indexPath.row) let target = store.data(at: dataIndex) let selected = store.isSelected(at: dataIndex) diff --git a/LineSDK/LineSDKTests/Sharing/ColumnDataStoreTests.swift b/LineSDK/LineSDKTests/Sharing/ColumnDataStoreTests.swift new file mode 100644 index 00000000..471aeef3 --- /dev/null +++ b/LineSDK/LineSDKTests/Sharing/ColumnDataStoreTests.swift @@ -0,0 +1,112 @@ +// +// ColumnDataStoreTests.swift +// +// Copyright (c) 2016-present, LINE Corporation. All rights reserved. +// +// You are hereby granted a non-exclusive, worldwide, royalty-free license to use, +// copy and distribute this software in source code or binary form for use +// in connection with the web services and APIs provided by LINE Corporation. +// +// As with any software that integrates with the LINE Corporation platform, your use of this software +// is subject to the LINE Developers Agreement [http://terms2.line.me/LINE_Developers_Agreement]. +// This copyright notice shall be included in all copies or substantial portions of the software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +import XCTest +@testable import LineSDK + +class ColumnDataStoreTests: XCTestCase { + + var store: ColumnDataStore! + + override func setUp() { + super.setUp() + store = ColumnDataStore(columnCount: 3) + } + + override func tearDown() { + store = nil + super.tearDown() + } + + func testAppendData() { + store.append(data: [1,2,3], to: 0) + store.append(data: [4,5,6], to: 1) + store.append(data: [7,8,9], to: 2) + + XCTAssertEqual(store.data(atColumn: 0), [1, 2, 3]) + XCTAssertEqual(store.data(atColumn: 1), [4, 5, 6]) + XCTAssertEqual(store.data(atColumn: 2), [7, 8, 9]) + + store.append(data: [3,2,1], to: 0) + XCTAssertEqual(store.data(atColumn: 0), [1, 2, 3, 3, 2, 1]) + } + + func testGetData() { + store.append(data: [1,2,3], to: 0) + XCTAssertEqual(store.data(atColumn: 0), [1, 2, 3]) + XCTAssertEqual(store.data(atColumn: 1), []) + + XCTAssertEqual(store.data(atColumn: 0, row: 1), 2) + + let index = ColumnDataStore.ColumnIndex(column: 0, row: 1) + XCTAssertEqual(store.data(at: index), 2) + } + + func testSelectData() { + store.append(data: [1,2,3], to: 0) + XCTAssertTrue(store.selected.isEmpty) + + let performed = store.toggleSelect(atColumn: 0, row: 1) + XCTAssertTrue(performed) + + XCTAssertEqual(store.selected.count, 1) + XCTAssertTrue(store.isSelected(at: .init(column: 0, row: 1))) + + let deselectPerformed = store.toggleSelect(atColumn: 0, row: 1) + XCTAssertTrue(deselectPerformed) + XCTAssertEqual(store.selected.count, 0) + XCTAssertFalse(store.isSelected(at: .init(column: 0, row: 1))) + } + + func testMaximumSelection() { + store.maximumSelectedCount = 2 + store.append(data: [1,2,3], to: 0) + + // Select two elements. + XCTAssertTrue(store.toggleSelect(atColumn: 0, row: 0)) + XCTAssertTrue(store.toggleSelect(atColumn: 0, row: 1)) + + // `maximumSelectedCount` count reached. + XCTAssertFalse(store.toggleSelect(atColumn: 0, row: 2)) + + // Unselect one. + XCTAssertTrue(store.toggleSelect(atColumn: 0, row: 1)) + // Select the one failed again. + XCTAssertTrue(store.toggleSelect(atColumn: 0, row: 2)) + + // `maximumSelectedCount` count reached. + XCTAssertFalse(store.toggleSelect(atColumn: 0, row: 1)) + + XCTAssertTrue(store.isSelected(at: .init(column: 0, row: 0))) + XCTAssertFalse(store.isSelected(at: .init(column: 0, row: 1))) + XCTAssertTrue(store.isSelected(at: .init(column: 0, row: 2))) + } + + func testFilterValues() { + store.append(data: [1,2,3], to: 0) + store.append(data: [4,5,6], to: 1) + store.append(data: [7,8,9], to: 2) + + let result = store.indexes { $0 % 2 == 0 } + let values = result.map { indexes in indexes.map { store.data(at: $0) } } + XCTAssertEqual(values, [[2], [4, 6], [8]]) + } +} From aa70d8318d02530bc8f1b8556ae7c8082749b060 Mon Sep 17 00:00:00 2001 From: onevcat Date: Mon, 1 Apr 2019 14:20:13 +0900 Subject: [PATCH 2/3] Add tests for notification --- .../Sharing/ColumnDataStoreTests.swift | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/LineSDK/LineSDKTests/Sharing/ColumnDataStoreTests.swift b/LineSDK/LineSDKTests/Sharing/ColumnDataStoreTests.swift index 471aeef3..ddb6c342 100644 --- a/LineSDK/LineSDKTests/Sharing/ColumnDataStoreTests.swift +++ b/LineSDK/LineSDKTests/Sharing/ColumnDataStoreTests.swift @@ -25,6 +25,7 @@ import XCTest class ColumnDataStoreTests: XCTestCase { var store: ColumnDataStore! + var token: NotificationToken! override func setUp() { super.setUp() @@ -33,6 +34,7 @@ class ColumnDataStoreTests: XCTestCase { override func tearDown() { store = nil + token = nil super.tearDown() } @@ -109,4 +111,71 @@ class ColumnDataStoreTests: XCTestCase { let values = result.map { indexes in indexes.map { store.data(at: $0) } } XCTAssertEqual(values, [[2], [4, 6], [8]]) } + + func testAppendingDataNotification() { + let expect = expectation(description: "\(#file)_\(#line)") + + token = NotificationCenter.default.addObserver( + forName: .columnDataStoreDidAppendData, + object: nil, + queue: .main) + { + noti in + let range = noti.userInfo?[LineSDKNotificationKey.appendDataIndexRange] + as? ColumnDataStore.AppendingIndexRange + XCTAssertNotNil(range) + XCTAssertEqual(range!.column, 0) + XCTAssertEqual(range!.startIndex, 0) + XCTAssertEqual(range!.endIndex, 3) + expect.fulfill() + } + + store.append(data: [1,2,3], to: 0) + waitForExpectations(timeout: 1, handler: nil) + } + + func testSelectingDataNotification() { + let expect = expectation(description: "\(#file)_\(#line)") + + token = NotificationCenter.default.addObserver( + forName: .columnDataStoreDidSelect, + object: nil, + queue: .main) + { + noti in + let index = noti.userInfo?[LineSDKNotificationKey.selectingIndex] + as? ColumnDataStore.ColumnIndex + XCTAssertNotNil(index) + XCTAssertEqual(index!.column, 0) + XCTAssertEqual(index!.row, 2) + expect.fulfill() + } + + store.append(data: [1,2,3], to: 0) + _ = store.toggleSelect(atColumn: 0, row: 2) + waitForExpectations(timeout: 1, handler: nil) + } + + func testDeselectingDataNotification() { + let expect = expectation(description: "\(#file)_\(#line)") + + token = NotificationCenter.default.addObserver( + forName: .columnDataStoreDidDeselect, + object: nil, + queue: .main) + { + noti in + let index = noti.userInfo?[LineSDKNotificationKey.selectingIndex] + as? ColumnDataStore.ColumnIndex + XCTAssertNotNil(index) + XCTAssertEqual(index!.column, 0) + XCTAssertEqual(index!.row, 2) + expect.fulfill() + } + + store.append(data: [1,2,3], to: 0) + _ = store.toggleSelect(atColumn: 0, row: 2) + _ = store.toggleSelect(atColumn: 0, row: 2) + waitForExpectations(timeout: 1, handler: nil) + } } From 0d7becc9f99e063a3089ea290e73978d16f065f0 Mon Sep 17 00:00:00 2001 From: onevcat Date: Mon, 1 Apr 2019 14:44:50 +0900 Subject: [PATCH 3/3] Update sample app to handle share delegate --- .../SharingUI/ShareViewController.swift | 4 +- .../UI/SampleUIHomeViewController.swift | 43 ++++++++++++++++++- 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/LineSDK/LineSDK/SharingUI/ShareViewController.swift b/LineSDK/LineSDK/SharingUI/ShareViewController.swift index bbbda44f..b82587e3 100644 --- a/LineSDK/LineSDK/SharingUI/ShareViewController.swift +++ b/LineSDK/LineSDK/SharingUI/ShareViewController.swift @@ -25,7 +25,7 @@ public protocol ShareViewControllerDelegate: AnyObject { func shareViewController( _ controller: ShareViewController, didFailLoadingListType shareType: MessageShareTargetType, - withError: LineSDKError) + withError error: LineSDKError) func shareViewControllerDidCancelSharing(_ controller: ShareViewController) } @@ -33,7 +33,7 @@ extension ShareViewControllerDelegate { public func shareViewController( _ controller: ShareViewController, didFailLoadingListType shareType: MessageShareTargetType, - withError: LineSDKError) { } + withError error: LineSDKError) { } public func shareViewControllerDidCancelSharing(_ controller: ShareViewController) { } } diff --git a/LineSDKSample/LineSDKSample/UI/SampleUIHomeViewController.swift b/LineSDKSample/LineSDKSample/UI/SampleUIHomeViewController.swift index 08e189ec..f4d14658 100644 --- a/LineSDKSample/LineSDKSample/UI/SampleUIHomeViewController.swift +++ b/LineSDKSample/LineSDKSample/UI/SampleUIHomeViewController.swift @@ -30,12 +30,53 @@ class SampleUIHomeViewController: UITableViewController { override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { if indexPath.row == Cell.shareMessage.rawValue { - presentShareViewController() + + let canSendToFriends = ShareViewController.authorizationStatusForSendingMessage(to: .friends) + let canSendToGroups = ShareViewController.authorizationStatusForSendingMessage(to: .groups) + + switch (canSendToFriends, canSendToGroups) { + case (.authorized, .authorized): + presentShareViewController() + case (.lackOfPermissions(let p), _): fallthrough + case (_ , .lackOfPermissions(let p)): + UIAlertController.present( + in: self, + title: nil, + message: "Lack of permissions: \(p)", + actions: [.init(title: "OK", style: .cancel)] + ) + case (.lackOfToken, _): fallthrough + case (_, .lackOfToken): + UIAlertController.present( + in: self, + title: nil, + message: "Please login first.", + actions: [.init(title: "OK", style: .cancel)] + ) + } } } private func presentShareViewController() { let viewController = ShareViewController() + viewController.shareDelegate = self present(viewController, animated: true) } } + +extension SampleUIHomeViewController: ShareViewControllerDelegate { + func shareViewController( + _ controller: ShareViewController, + didFailLoadingListType shareType: MessageShareTargetType, + withError error: LineSDKError) + { + controller.dismiss(animated: true) { + UIAlertController.present(in: self, error: error) + } + } + + func shareViewControllerDidCancelSharing(_ controller: ShareViewController) { + UIAlertController.present( + in: self, title: nil, message: "User Cancelled", actions: [.init(title: "OK", style: .cancel)]) + } +}