diff --git a/Shuffle-iOS.podspec b/Shuffle-iOS.podspec index 8351ac2f..82c7b975 100644 --- a/Shuffle-iOS.podspec +++ b/Shuffle-iOS.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |s| s.name = "Shuffle-iOS" -s.version = "0.2.7" +s.version = "0.3.0" s.platform = :ios, "9.0" s.summary = "A multi-directional card swiping library inspired by Tinder" @@ -13,7 +13,7 @@ s.homepage = "https://github.com/mac-gallagher/Shuffle" s.documentation_url = "https://github.com/mac-gallagher/Shuffle/tree/master/README.md" s.license = { :type => 'MIT', :file => 'LICENSE' } s.author = { "Mac Gallagher" => "jmgallagher36@gmail.com" } -s.source = { :git => "https://github.com/mac-gallagher/Shuffle.git", :tag => "v0.2.7" } +s.source = { :git => "https://github.com/mac-gallagher/Shuffle.git", :tag => "v0.3.0" } s.swift_version = "5.0" s.source_files = "Sources/**/*.{h,swift}" diff --git a/Sources/Shuffle/SwipeCardStack/CardStackStateManager.swift b/Sources/Shuffle/SwipeCardStack/CardStackStateManager.swift index 35a503bd..41927626 100644 --- a/Sources/Shuffle/SwipeCardStack/CardStackStateManager.swift +++ b/Sources/Shuffle/SwipeCardStack/CardStackStateManager.swift @@ -30,6 +30,8 @@ protocol CardStackStateManagable { var remainingIndices: [Int] { get } var swipes: [Swipe] { get } + func insert(_ index: Int, at position: Int) + func swipe(_ direction: SwipeDirection) func undoSwipe() -> Swipe? func shift(withDistance distance: Int) @@ -49,6 +51,32 @@ class CardStackStateManager: CardStackStateManagable { /// An array containing the swipe history of the card stack. var swipes: [Swipe] = [] + func insert(_ index: Int, at position: Int) { + if position < 0 { + fatalError("Attempt to insert card at position \(position)") + } + + if position > remainingIndices.count { + //swiftlint:disable:next line_length + fatalError("Attempt to insert card at position \(position), but there are only \(remainingIndices.count + 1) cards remaining in the stack after the update") + } + + if index < 0 { + fatalError("Attempt to insert card at data source index \(index)") + } + + if index > remainingIndices.count + swipes.count { + //swiftlint:disable:next line_length + fatalError("Attempt to insert card at index \(index), but there are only \(remainingIndices.count + swipes.count + 1) cards after the update") + } + + // Increment all stored indices in the range [0, index] by 1 + remainingIndices = remainingIndices.map { $0 >= index ? $0 + 1 : $0 } + swipes = swipes.map { $0.index >= index ? Swipe($0.index + 1, $0.direction) : $0 } + + remainingIndices.insert(index, at: position) + } + func swipe(_ direction: SwipeDirection) { if remainingIndices.isEmpty { return } let firstIndex = remainingIndices.removeFirst() diff --git a/Sources/Shuffle/SwipeCardStack/SwipeCardStack.swift b/Sources/Shuffle/SwipeCardStack/SwipeCardStack.swift index b8010154..5ae0bfc8 100644 --- a/Sources/Shuffle/SwipeCardStack/SwipeCardStack.swift +++ b/Sources/Shuffle/SwipeCardStack/SwipeCardStack.swift @@ -33,12 +33,12 @@ open class SwipeCardStack: UIView, SwipeCardDelegate, UIGestureRecognizerDelegat /// Return `false` if you wish to ignore all horizontal gestures on the card stack. /// - /// You may wish to modify this property if your card stack is embedded in a `UIScrollView`. + /// You may wish to modify this property if your card stack is embedded in a `UIScrollView`, for example. open var shouldRecognizeHorizontalDrag: Bool = true /// Return `false` if you wish to ignore all vertical gestures on the card stack. /// - /// You may wish to modify this property if your card stack is embedded in a `UIScrollView`. + /// You may wish to modify this property if your card stack is embedded in a `UIScrollView`, for example. open var shouldRecognizeVerticalDrag: Bool = true public weak var delegate: SwipeCardStackDelegate? @@ -55,6 +55,7 @@ open class SwipeCardStack: UIView, SwipeCardDelegate, UIGestureRecognizerDelegat } } + /// The data source index corresponding to the topmost card in the stack. public var topCardIndex: Int? { return visibleCards.first?.index } @@ -167,12 +168,10 @@ open class SwipeCardStack: UIView, SwipeCardDelegate, UIGestureRecognizerDelegat // MARK: - Main Methods - /// Calling this method triggers a swipe on the card stack. + /// Triggers a swipe on the card stack in the specified direction. /// - Parameters: /// - direction: The direction to swipe the top card. - /// - animated: A boolean indicating whether the reverse swipe should be animated. Setting this - /// to `false` will immediately - /// position the card at end state of the animation when the method is called. + /// - animated: A boolean indicating whether the swipe action should be animated. public func swipe(_ direction: SwipeDirection, animated: Bool) { if !isEnabled { return } @@ -225,6 +224,8 @@ open class SwipeCardStack: UIView, SwipeCardDelegate, UIGestureRecognizerDelegat } } + /// Returns the most recently swiped card to the top of the card stack. + /// - Parameter animated: A boolean indicating whether the undo action should be animated. public func undoLastSwipe(animated: Bool) { if !isEnabled { return } guard let previousSwipe = stateManager.undoSwipe() else { return } @@ -248,6 +249,10 @@ open class SwipeCardStack: UIView, SwipeCardDelegate, UIGestureRecognizerDelegat } } + /// Shifts the remaining cards in the card stack by the specified distance. Any swiped cards are ignored. + /// - Parameters: + /// - distance: The distance to shift the remaining cards by. + /// - animated: A boolean indicating whether the undo action should be animated. public func shift(withDistance distance: Int = 1, animated: Bool) { if !isEnabled || distance == 0 || visibleCards.count <= 1 { return } @@ -275,7 +280,6 @@ open class SwipeCardStack: UIView, SwipeCardDelegate, UIGestureRecognizerDelegat } /// Returns the `SwipeCard` at the specified index. - /// /// - Parameter index: The index of the card in the data source. /// - Returns: The `SwipeCard` at the specified index, or `nil` if the card is not visible or the index is /// out of range. @@ -292,10 +296,35 @@ open class SwipeCardStack: UIView, SwipeCardDelegate, UIGestureRecognizerDelegat /// - Parameter index: The index of the card in the data source. /// - Returns: The current position of the card at the specified index, or `nil` if the index if out of range or the /// card has been swiped. - public func position(forCardAtIndex index: Int) -> Int? { + public func positionforCard(at index: Int) -> Int? { return stateManager.remainingIndices.firstIndex(of: index) } + /// Returns the remaining number of cards in the card stack. + /// - Returns: The number of cards in the card stack which have not yet been swiped. + public func numberOfRemainingCards() -> Int { + return stateManager.remainingIndices.count + } + + /// Inserts a new card with the given index at the specified position. + /// - Parameters: + /// - index: The index of the card in the data source. + /// - position: The position of the new card in the card stack. This position should be determined on + /// the returned value of `numberOfRemainingCards`. + public func insertCard(atIndex index: Int, position: Int) { + stateManager.insert(index, at: position) + reloadVisibleCards() + } + + /// Appends a collection of new cards with the specifed indices to the bottom of the card stack. + /// - Parameter indices: The indices of the cards in the data source. + public func appendCards(atIndices indices: [Int]) { + for index in indices { + stateManager.insert(index, at: numberOfRemainingCards()) + } + reloadVisibleCards() + } + func reloadVisibleCards() { visibleCards.forEach { $0.card.removeFromSuperview() } visibleCards.removeAll() diff --git a/Tests/ShuffleTests/SwipeCardStack/Mocks/MockCardStackStateManager.swift b/Tests/ShuffleTests/SwipeCardStack/Mocks/MockCardStackStateManager.swift index 0dd26968..2dadd8b1 100644 --- a/Tests/ShuffleTests/SwipeCardStack/Mocks/MockCardStackStateManager.swift +++ b/Tests/ShuffleTests/SwipeCardStack/Mocks/MockCardStackStateManager.swift @@ -31,6 +31,16 @@ class MockCardStackStateManager: CardStackStateManagable { var swipes: [Swipe] = [] + var insertCalled: Bool = false + var insertIndices: [Int] = [] + var insertPositions: [Int] = [] + + func insert(_ index: Int, at position: Int) { + insertCalled = true + insertIndices.append(index) + insertPositions.append(position) + } + var swipeCalled = false var swipeDirection: SwipeDirection? diff --git a/Tests/ShuffleTests/SwipeCardStack/Specs/CardStackStateManagerSpec.swift b/Tests/ShuffleTests/SwipeCardStack/Specs/CardStackStateManagerSpec.swift index d83c47f3..95e062ec 100644 --- a/Tests/ShuffleTests/SwipeCardStack/Specs/CardStackStateManagerSpec.swift +++ b/Tests/ShuffleTests/SwipeCardStack/Specs/CardStackStateManagerSpec.swift @@ -50,6 +50,83 @@ class CardStackStateManagerSpec: QuickSpec { } } + // MARK: - Insert + + describe("When calling insert") { + context("and position is less than zero") { + let position: Int = -1 + + beforeEach { + subject.remainingIndices = [1, 2, 3] + } + + it("should throw a fatal error") { + expect(subject.insert(4, at: position)).to(throwAssertion()) + } + } + + context("and position is greater than the number of remaining indices") { + let position: Int = 4 + + beforeEach { + subject.remainingIndices = [1, 2, 3] + } + + it("should throw a fatal error") { + expect(subject.insert(4, at: position)).to(throwAssertion()) + } + } + + context("and index is less than zero") { + let index: Int = -1 + + beforeEach { + subject.remainingIndices = [1, 2, 3] + } + + it("should throw a fatal error") { + expect(subject.insert(index, at: 0)).to(throwAssertion()) + } + } + + context("and index is greater than the number of remaining indices + swiped count") { + let index: Int = 5 + + beforeEach { + subject.swipes = [Swipe(0, .left), Swipe(1, .left)] + subject.remainingIndices = [2, 3] + } + + it("should throw a fatal error") { + expect(subject.insert(index, at: 0)).to(throwAssertion()) + } + } + + context("and position is at least zero and at most the number of remaining indices") { + let oldRemainingIndices: [Int] = [3, 2, 5, 6, 0] + let oldSwipes = [Swipe(1, .left), Swipe(4, .left), Swipe(7, .left)] + let index: Int = 4 + let position: Int = 2 + + beforeEach { + subject.remainingIndices = oldRemainingIndices + subject.swipes = oldSwipes + subject.insert(index, at: position) + } + + it("should insert the index at the correct position in remainingIndices") { + expect(subject.remainingIndices[position]) == index + } + + it("should increment all stored indices greater than index by one") { + expect(subject.remainingIndices[3]) == oldRemainingIndices[2] + 1 + expect(subject.remainingIndices[4]) == oldRemainingIndices[3] + 1 + expect(subject.swipes[1].index) == oldSwipes[1].index + 1 + expect(subject.swipes[2].index) == oldSwipes[2].index + 1 + } + } + } + // MARK: - Swipe describe("When calling swipe") { diff --git a/Tests/ShuffleTests/SwipeCardStack/Specs/SwipeCardStackSpec_MainMethods.swift b/Tests/ShuffleTests/SwipeCardStack/Specs/SwipeCardStackSpec_MainMethods.swift index 840e4611..529702ef 100644 --- a/Tests/ShuffleTests/SwipeCardStack/Specs/SwipeCardStackSpec_MainMethods.swift +++ b/Tests/ShuffleTests/SwipeCardStack/Specs/SwipeCardStackSpec_MainMethods.swift @@ -27,7 +27,7 @@ import Quick @testable import Shuffle import UIKit -// swiftlint:disable type_body_length closure_body_length implicitly_unwrapped_optional +// swiftlint:disable file_length type_body_length closure_body_length implicitly_unwrapped_optional class SwipeCardStackSpec_MainMethods: QuickSpec { // swiftlint:disable:next function_body_length @@ -471,7 +471,7 @@ class SwipeCardStackSpec_MainMethods: QuickSpec { let index: Int = 2 it("should return nil") { - let actualPosition = subject.position(forCardAtIndex: index) + let actualPosition = subject.positionforCard(at: index) expect(actualPosition).to(beNil()) } } @@ -480,12 +480,68 @@ class SwipeCardStackSpec_MainMethods: QuickSpec { let index: Int = 4 it("should return the first index of the specified position in the state manager's remaining indices") { - let actualPosition = subject.position(forCardAtIndex: index) + let actualPosition = subject.positionforCard(at: index) expect(actualPosition) == 1 } } } + // MARK: Number of Remaining Cards + + describe("When calling numberOfRemainingCards") { + beforeEach { + mockStateManager.remainingIndices = [3, 4, 5, 4] + } + + it("should return the number of elements in the state manager's remainingIndices") { + expect(subject.numberOfRemainingCards()) == mockStateManager.remainingIndices.count + } + } + + // MARK: Insert Card At Index + + describe("When calling insertCardAtIndex") { + let index: Int = 1 + let position: Int = 2 + + beforeEach { + subject.insertCard(atIndex: index, position: position) + } + + it("should call the stateManager's insert method with the correct parameters") { + expect(mockStateManager.insertCalled) == true + expect(mockStateManager.insertIndices) == [index] + expect(mockStateManager.insertPositions) == [position] + } + + it("should call reloadVisibleCards") { + expect(subject.reloadVisibleCardsCalled) == true + } + } + + // MARK: Append Cards + + describe("When calling appendCards") { + let indices: [Int] = [1, 2, 3] + let remainingIndices: [Int] = [0, 1, 2, 3, 4] + + beforeEach { + mockStateManager.remainingIndices = remainingIndices + subject.appendCards(atIndices: indices) + } + + it("should call the stateManager's insert method with the correct parameters") { + expect(mockStateManager.insertCalled) == true + expect(mockStateManager.insertIndices) == indices + expect(mockStateManager.insertPositions) == Array(repeating: remainingIndices.count, + count: indices.count) + } + + it("should call reloadVisibleCards") { + expect(subject.reloadVisibleCardsCalled) == true + } + } + // MARK: Reload Visible Cards describe("When calling reloadVisibleCards") { @@ -593,4 +649,4 @@ class SwipeCardStackSpec_MainMethods: QuickSpec { } } } -// swiftlint:enable type_body_length closure_body_length implicitly_unwrapped_optional +// swiftlint:enable file_length type_body_length closure_body_length implicitly_unwrapped_optional