diff --git a/Shuffle-iOS.podspec b/Shuffle-iOS.podspec index 5e268fe3..ac7f745a 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.3.2" +s.version = "0.3.3" 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.3.2" } +s.source = { :git => "https://github.com/mac-gallagher/Shuffle.git", :tag => "v0.3.3" } 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 71625a88..eabba7a9 100644 --- a/Sources/Shuffle/SwipeCardStack/CardStackStateManager.swift +++ b/Sources/Shuffle/SwipeCardStack/CardStackStateManager.swift @@ -24,7 +24,10 @@ import Foundation -typealias Swipe = (index: Int, direction: SwipeDirection) +struct Swipe: Equatable { + var index: Int + var direction: SwipeDirection +} protocol CardStackStateManagable { var remainingIndices: [Int] { get } @@ -33,6 +36,7 @@ protocol CardStackStateManagable { func insert(_ index: Int, at position: Int) func delete(_ index: Int) + func delete(indexAtPosition position: Int) func swipe(_ direction: SwipeDirection) func undoSwipe() -> Swipe? @@ -67,7 +71,7 @@ class CardStackStateManager: CardStackStateManagable { // Increment all stored indices greater than or equal to index by 1 remainingIndices = remainingIndices.map { $0 >= index ? $0 + 1 : $0 } - swipes = swipes.map { $0.index >= index ? Swipe($0.index + 1, $0.direction) : $0 } + swipes = swipes.map { $0.index >= index ? Swipe(index: $0.index + 1, direction: $0.direction) : $0 } remainingIndices.insert(index, at: position) } @@ -85,13 +89,23 @@ class CardStackStateManager: CardStackStateManagable { // Decrement all stored indices greater than or equal to index by 1 remainingIndices = remainingIndices.map { $0 >= index ? $0 - 1 : $0 } - swipes = swipes.map { $0.index >= index ? Swipe($0.index - 1, $0.direction) : $0 } + swipes = swipes.map { $0.index >= index ? Swipe(index: $0.index - 1, direction: $0.direction) : $0 } + } + + func delete(indexAtPosition position: Int) { + precondition(position >= 0, "Attempt to delete card at position \(position)") + //swiftlint:disable:next line_length + precondition(position < remainingIndices.count, "Attempt to delete card at position \(position), but there are only \(remainingIndices.count) cards remaining in the stack before the update") + + // Decrement all stored indices greater than or equal to index by 1 + let index = remainingIndices.remove(at: position) + remainingIndices = remainingIndices.map { $0 >= index ? $0 - 1 : $0 } } func swipe(_ direction: SwipeDirection) { if remainingIndices.isEmpty { return } let firstIndex = remainingIndices.removeFirst() - let swipe = Swipe(direction: direction, index: firstIndex) + let swipe = Swipe(index: firstIndex, direction: direction) swipes.append(swipe) } diff --git a/Sources/Shuffle/SwipeCardStack/SwipeCardStack.swift b/Sources/Shuffle/SwipeCardStack/SwipeCardStack.swift index 8e5f2df0..68490b1b 100644 --- a/Sources/Shuffle/SwipeCardStack/SwipeCardStack.swift +++ b/Sources/Shuffle/SwipeCardStack/SwipeCardStack.swift @@ -24,11 +24,14 @@ import UIKit -/// A typealias for a `SwipeCard` and it's corresponding index in the card stack's `dataSource`. -typealias Card = (index: Int, card: SwipeCard) - open class SwipeCardStack: UIView, SwipeCardDelegate, UIGestureRecognizerDelegate { + /// A internal structure for a `SwipeCard` and it's corresponding index in the card stack's `dataSource`. + struct Card { + var index: Int + var card: SwipeCard + } + open var animationOptions: CardStackAnimatableOptions = CardStackAnimationOptions.default /// Return `false` if you wish to ignore all horizontal gestures on the card stack. @@ -201,7 +204,7 @@ open class SwipeCardStack: UIView, SwipeCardDelegate, UIGestureRecognizerDelegat if (stateManager.remainingIndices.count - visibleCards.count) > 0 { let bottomCardIndex = stateManager.remainingIndices[visibleCards.count] if let card = loadCard(at: bottomCardIndex) { - insertCard(Card(bottomCardIndex, card), at: visibleCards.count) + insertCard(Card(index: bottomCardIndex, card: card), at: visibleCards.count) } } @@ -290,6 +293,34 @@ open class SwipeCardStack: UIView, SwipeCardDelegate, UIGestureRecognizerDelegat return nil } + func reloadVisibleCards() { + visibleCards.forEach { $0.card.removeFromSuperview() } + visibleCards.removeAll() + + let numberOfCards = min(stateManager.remainingIndices.count, numberOfVisibleCards) + for position in 0.. SwipeCard? { + let card = dataSource?.cardStack(self, cardForIndexAt: index) + card?.delegate = self + card?.panGestureRecognizer.delegate = self + return card + } + + // MARK: - State Management + /// Returns the current position of the card at the specified index. /// /// A returned value of `0` indicates that the card is the topmost card in the stack. @@ -309,8 +340,7 @@ open class SwipeCardStack: UIView, SwipeCardDelegate, UIGestureRecognizerDelegat /// 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`. + /// - position: The position of the new card in the card stack. public func insertCard(atIndex index: Int, position: Int) { guard let dataSource = dataSource else { return } @@ -351,8 +381,8 @@ open class SwipeCardStack: UIView, SwipeCardDelegate, UIGestureRecognizerDelegat reloadVisibleCards() } - /// Deletes the card at the specified index. Removes swipes - /// - Parameter index: The index of the card in the data source + /// Deletes the card at the specified index. + /// - Parameter index: The index of the card in the data source. public func deleteCard(atIndex index: Int) { guard let dataSource = dataSource else { return } @@ -371,30 +401,24 @@ open class SwipeCardStack: UIView, SwipeCardDelegate, UIGestureRecognizerDelegat reloadVisibleCards() } - func reloadVisibleCards() { - visibleCards.forEach { $0.card.removeFromSuperview() } - visibleCards.removeAll() + /// Deletes the card at the specified position in the card stack. + /// - Parameter position: The position of the card to delete in the card stack. + public func deleteCard(atPosition position: Int) { + guard let dataSource = dataSource else { return } - let numberOfCards = min(stateManager.remainingIndices.count, numberOfVisibleCards) - for position in 0.. SwipeCard? { - let card = dataSource?.cardStack(self, cardForIndexAt: index) - card?.delegate = self - card?.panGestureRecognizer.delegate = self - return card + if newNumberOfCards != oldNumberOfCards - 1 { + let errorString = StringUtils.createInvalidUpdateErrorString(newCount: newNumberOfCards, + oldCount: oldNumberOfCards, + deletedCount: 1) + fatalError(errorString) + } + + reloadVisibleCards() } // MARK: - Notifications diff --git a/Tests/ShuffleTests/SwipeCardStack/Mocks/MockCardStackStateManager.swift b/Tests/ShuffleTests/SwipeCardStack/Mocks/MockCardStackStateManager.swift index e1fad49a..3f0f505e 100644 --- a/Tests/ShuffleTests/SwipeCardStack/Mocks/MockCardStackStateManager.swift +++ b/Tests/ShuffleTests/SwipeCardStack/Mocks/MockCardStackStateManager.swift @@ -41,12 +41,20 @@ class MockCardStackStateManager: CardStackStateManagable { insertPositions.append(position) } - var deleteCalled: Bool = false - var deleteIndex: Int? + var deleteAtIndexCalled: Bool = false + var deleteAtIndexIndex: Int? func delete(_ index: Int) { - deleteCalled = true - deleteIndex = index + deleteAtIndexCalled = true + deleteAtIndexIndex = index + } + + var deleteAtPositionCalled: Bool = false + var deleteAtPositionPosition: Int? + + func delete(indexAtPosition position: Int) { + deleteAtPositionCalled = true + deleteAtPositionPosition = position } var swipeCalled = false diff --git a/Tests/ShuffleTests/SwipeCardStack/Specs/CardStackStateManagerSpec.swift b/Tests/ShuffleTests/SwipeCardStack/Specs/CardStackStateManagerSpec.swift index ed6d8228..5ff485b3 100644 --- a/Tests/ShuffleTests/SwipeCardStack/Specs/CardStackStateManagerSpec.swift +++ b/Tests/ShuffleTests/SwipeCardStack/Specs/CardStackStateManagerSpec.swift @@ -53,58 +53,53 @@ 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] - } + context("and index is less than zero") { + let index: Int = -1 it("should throw a fatal error") { - expect(subject.insert(0, at: position)).to(throwAssertion()) + expect(subject.insert(index, at: 0)).to(throwAssertion()) } } - context("and position is greater than the number of remaining indices") { - let position: Int = 4 + context("and index is greater than totalIndexCount") { + let index: Int = 5 beforeEach { - subject.remainingIndices = [1, 2, 3] + subject.swipes = [Swipe(index: 0, direction: .left), + Swipe(index: 1, direction: .left)] + subject.remainingIndices = [2, 3] } it("should throw a fatal error") { - expect(subject.insert(0, at: position)).to(throwAssertion()) + expect(subject.insert(index, at: 0)).to(throwAssertion()) } } - context("and index is less than zero") { - let index: Int = -1 - - beforeEach { - subject.remainingIndices = [1, 2, 3] - } + context("and position is less than zero") { + let position: Int = -1 it("should throw a fatal error") { - expect(subject.insert(index, at: 0)).to(throwAssertion()) + expect(subject.insert(0, at: position)).to(throwAssertion()) } } - context("and index is greater than the number of remaining indices + swiped count") { - let index: Int = 5 + context("and position is greater than the number of remaining indices") { + let position: Int = 4 beforeEach { - subject.swipes = [Swipe(0, .left), Swipe(1, .left)] - subject.remainingIndices = [2, 3] + subject.remainingIndices = [1, 2, 3] } it("should throw a fatal error") { - expect(subject.insert(index, at: 0)).to(throwAssertion()) + expect(subject.insert(0, at: position)).to(throwAssertion()) } } - context("and position is at least zero and at most the number of remaining indices") { + context("and position and index are within the expected ranges") { let oldRemainingIndices: [Int] = [3, 2, 5, 6, 0] - let oldSwipes = [Swipe(1, .left), Swipe(4, .left), Swipe(7, .left)] + let oldSwipes = [Swipe(index: 1, direction: .left), + Swipe(index: 4, direction: .left), + Swipe(index: 7, direction: .left)] let index: Int = 4 let position: Int = 2 @@ -114,15 +109,14 @@ class CardStackStateManagerSpec: QuickSpec { subject.insert(index, at: position) } - it("should insert the index at the correct position in remainingIndices") { - expect(subject.remainingIndices[position]) == index - } + it("should correctly update the swipes and remaining indices") { + let expectedSwipes = [Swipe(index: 1, direction: .left), + Swipe(index: 5, direction: .left), + Swipe(index: 8, direction: .left)] + expect(subject.swipes) == expectedSwipes - it("should increment all stored indices greater than or equal to 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 + let expectedRemainingIndices = [3, 2, 4, 6, 7, 0] + expect(subject.remainingIndices) == expectedRemainingIndices } } } @@ -138,10 +132,11 @@ class CardStackStateManagerSpec: QuickSpec { } } - context("and index is greater than the number of remaining indices - 1") { - let index: Int = 3 + context("and index is greater than the totalIndexCount - 1") { + let index: Int = 4 beforeEach { + subject.swipes = [Swipe(index: 0, direction: .left)] subject.remainingIndices = [1, 2, 3] } @@ -150,31 +145,31 @@ class CardStackStateManagerSpec: QuickSpec { } } - context("and index is at least zero and at most the number of remaining indices - 1") { + context("and index is in the expect range") { let oldRemainingIndices: [Int] = [3, 2, 5, 6, 0] - let oldSwipes = [Swipe(1, .left), Swipe(4, .left), Swipe(7, .left)] + let oldSwipes = [Swipe(index: 1, direction: .left), + Swipe(index: 4, direction: .left), + Swipe(index: 7, direction: .left)] beforeEach { subject.remainingIndices = oldRemainingIndices subject.swipes = oldSwipes } - context("and the index has already been swiped") { + context("and the index has been swiped") { let index: Int = 4 beforeEach { subject.delete(index) } - it("should remove any swipes with the index") { - expect(subject.swipes.contains { $0.index == index }) == false - expect(subject.swipes.count) == oldSwipes.count - 1 - } + it("should correctly update the swipes and remaining indices") { + let expectedSwipes = [Swipe(index: 1, direction: .left), + Swipe(index: 6, direction: .left)] + expect(subject.swipes) == expectedSwipes - it("should decrement all stored indices greater than or equal to index by one") { - expect(subject.remainingIndices[2]) == oldRemainingIndices[2] - 1 - expect(subject.remainingIndices[3]) == oldRemainingIndices[3] - 1 - expect(subject.swipes[1].index) == oldSwipes[2].index - 1 + let expectedRemainingIndices = [3, 2, 4, 5, 0] + expect(subject.remainingIndices) == expectedRemainingIndices } } @@ -185,22 +180,61 @@ class CardStackStateManagerSpec: QuickSpec { subject.delete(index) } - it("should remove the index from remainingIndiecs") { - expect(subject.remainingIndices.count) == oldRemainingIndices.count - 1 - } + it("should correctly update the swipes and remaining indices") { + let expectedSwipes = [Swipe(index: 1, direction: .left), + Swipe(index: 3, direction: .left), + Swipe(index: 6, direction: .left)] + expect(subject.swipes) == expectedSwipes - it("should decrement all stored indices greater than or equal to index by one") { - expect(subject.remainingIndices[0]) == oldRemainingIndices[0] - 1 - expect(subject.remainingIndices[1]) == oldRemainingIndices[2] - 1 - expect(subject.remainingIndices[2]) == oldRemainingIndices[3] - 1 - expect(subject.swipes[1].index) == oldSwipes[1].index - 1 - expect(subject.swipes[2].index) == oldSwipes[2].index - 1 + let expectedRemainingIndices = [2, 4, 5, 0] + expect(subject.remainingIndices) == expectedRemainingIndices } } } } - // MARK: - Delete At Position + // MARK: - Delete Index At Position + + describe("When calling delete:indexAtPosition") { + context("and position is less than zero") { + let position: Int = -1 + + it("should throw a fatal error") { + expect(subject.delete(indexAtPosition: position)).to(throwAssertion()) + } + } + + context("and position is greater than the number of remaining indices - 1") { + let position: Int = 3 + + beforeEach { + subject.remainingIndices = [1, 2, 3] + } + + it("should throw a fatal error") { + expect(subject.delete(indexAtPosition: position)).to(throwAssertion()) + } + } + + context("and index is within the expect range") { + let oldRemainingIndices: [Int] = [3, 2, 5, 6, 0] + let position: Int = 2 + + beforeEach { + subject.remainingIndices = oldRemainingIndices + subject.delete(indexAtPosition: position) + } + + it("should remove the index at the position from remainingIndiecs") { + expect(subject.remainingIndices.count) == oldRemainingIndices.count - 1 + } + + it("should correctly update the remaining indices") { + let expectedRemainingIndices = [3, 2, 5, 0] + expect(subject.remainingIndices) == expectedRemainingIndices + } + } + } // MARK: - Swipe @@ -256,7 +290,7 @@ class CardStackStateManagerSpec: QuickSpec { } context("and there is at least one swipe") { - let swipe = Swipe(5, .left) + let swipe = Swipe(index: 5, direction: .left) beforeEach { subject.swipes = [swipe] @@ -299,7 +333,8 @@ class CardStackStateManagerSpec: QuickSpec { beforeEach { subject.remainingIndices = [2, 3, 4] - subject.swipes = [Swipe(0, .left), Swipe(1, .up)] + subject.swipes = [Swipe(index: 0, direction: .left), + Swipe(index: 1, direction: .up)] subject.reset(withNumberOfCards: numberOfCards) } diff --git a/Tests/ShuffleTests/SwipeCardStack/Specs/SwipeCardStackSpec_Base.swift b/Tests/ShuffleTests/SwipeCardStack/Specs/SwipeCardStackSpec_Base.swift index 4712859b..7677f433 100644 --- a/Tests/ShuffleTests/SwipeCardStack/Specs/SwipeCardStackSpec_Base.swift +++ b/Tests/ShuffleTests/SwipeCardStack/Specs/SwipeCardStackSpec_Base.swift @@ -30,6 +30,8 @@ import UIKit // swiftlint:disable closure_body_length function_body_length implicitly_unwrapped_optional class SwipeCardStackSpec_Base: QuickSpec { + typealias Card = SwipeCardStack.Card + override func spec() { var mockLayoutProvider: MockCardStackLayoutProvider! var mockStateManager: MockCardStackStateManager! @@ -137,7 +139,7 @@ class SwipeCardStackSpec_Base: QuickSpec { let index: Int = 2 beforeEach { - subject.visibleCards = [Card(index, SwipeCard())] + subject.visibleCards = [Card(index: index, card: SwipeCard())] } it("should return the first index from visibleCards") { @@ -163,9 +165,9 @@ class SwipeCardStackSpec_Base: QuickSpec { let firstCard = SwipeCard() beforeEach { - subject.visibleCards = [Card(0, firstCard), - Card(1, SwipeCard()), - Card(2, SwipeCard())] + subject.visibleCards = [Card(index: 0, card: firstCard), + Card(index: 1, card: SwipeCard()), + Card(index: 2, card: SwipeCard())] } it("should return the first visible card") { @@ -180,9 +182,9 @@ class SwipeCardStackSpec_Base: QuickSpec { let visibleCards = [SwipeCard(), SwipeCard(), SwipeCard()] beforeEach { - subject.visibleCards = [Card(0, visibleCards[0]), - Card(1, visibleCards[1]), - Card(2, visibleCards[2])] + subject.visibleCards = [Card(index: 0, card: visibleCards[0]), + Card(index: 1, card: visibleCards[1]), + Card(index: 2, card: visibleCards[2])] } it("should return the visible cards expect the first card") { @@ -250,9 +252,9 @@ class SwipeCardStackSpec_Base: QuickSpec { beforeEach { mockLayoutProvider.testCardContainerFrame = cardContainerFrame - subject.visibleCards = [Card(0, visibleCards[0]), - Card(1, visibleCards[1]), - Card(2, visibleCards[2])] + subject.visibleCards = [Card(index: 0, card: visibleCards[0]), + Card(index: 1, card: visibleCards[1]), + Card(index: 2, card: visibleCards[2])] subject.layoutSubviews() } diff --git a/Tests/ShuffleTests/SwipeCardStack/Specs/SwipeCardStackSpec_MainMethods.swift b/Tests/ShuffleTests/SwipeCardStack/Specs/SwipeCardStackSpec_MainMethods.swift index 59b52734..adbe4449 100644 --- a/Tests/ShuffleTests/SwipeCardStack/Specs/SwipeCardStackSpec_MainMethods.swift +++ b/Tests/ShuffleTests/SwipeCardStack/Specs/SwipeCardStackSpec_MainMethods.swift @@ -30,6 +30,8 @@ import UIKit // swiftlint:disable type_body_length closure_body_length implicitly_unwrapped_optional class SwipeCardStackSpec_MainMethods: QuickSpec { + typealias Card = SwipeCardStack.Card + // swiftlint:disable:next function_body_length override func spec() { var mockAnimator: MockCardStackAnimator! @@ -100,7 +102,7 @@ class SwipeCardStackSpec_MainMethods: QuickSpec { describe("When calling swipeAction") { let direction: SwipeDirection = .left - let topCard = Card(0, SwipeCard()) + let topCard = Card(index: 0, card: SwipeCard()) let forced = false let animated = false @@ -136,7 +138,7 @@ class SwipeCardStackSpec_MainMethods: QuickSpec { beforeEach { mockStateManager.remainingIndices = remainingIndices - subject.visibleCards = [topCard, Card(1, SwipeCard())] + subject.visibleCards = [topCard, Card(index: 1, card: SwipeCard())] subject.testLoadCard = testLoadCard subject.swipeAction(topCard: topCard.card, direction: direction, @@ -165,7 +167,9 @@ class SwipeCardStackSpec_MainMethods: QuickSpec { context("and there are no more cards to load") { beforeEach { mockStateManager.remainingIndices = [1, 2] - subject.visibleCards = [topCard, Card(1, SwipeCard()), Card(2, SwipeCard())] + subject.visibleCards = [topCard, + Card(index: 1, card: SwipeCard()), + Card(index: 2, card: SwipeCard())] subject.swipeAction(topCard: topCard.card, direction: direction, forced: forced, @@ -269,7 +273,8 @@ class SwipeCardStackSpec_MainMethods: QuickSpec { let previousSwipeDirection: SwipeDirection = .left beforeEach { - mockStateManager.undoSwipeSwipe = Swipe(previousSwipeIndex, previousSwipeDirection) + mockStateManager.undoSwipeSwipe = Swipe(index: previousSwipeIndex, + direction: previousSwipeDirection) subject.undoLastSwipe(animated: animated) } @@ -338,7 +343,7 @@ class SwipeCardStackSpec_MainMethods: QuickSpec { context("and the distance is greater than zero") { context("and there are less than two visible cards") { beforeEach { - subject.visibleCards = [Card(0, SwipeCard())] + subject.visibleCards = [Card(index: 0, card: SwipeCard())] subject.shift(withDistance: 1, animated: false) } @@ -354,7 +359,8 @@ class SwipeCardStackSpec_MainMethods: QuickSpec { beforeEach { mockStateManager.remainingIndices = [1, 2, 3] - subject.visibleCards = [Card(0, SwipeCard()), Card(0, SwipeCard())] + subject.visibleCards = [Card(index: 0, card: SwipeCard()), + Card(index: 0, card: SwipeCard())] subject.shift(withDistance: distance, animated: animated) } @@ -472,7 +478,7 @@ class SwipeCardStackSpec_MainMethods: QuickSpec { describe("When calling insertCard") { let numberOfCards: Int = 5 - let card = Card(0, SwipeCard()) + let card = Card(index: 0, card: SwipeCard()) let position: Int = 3 var cardContainer: UIView? @@ -480,7 +486,7 @@ class SwipeCardStackSpec_MainMethods: QuickSpec { beforeEach { cardContainer = subject.subviews.first for position in 0..