Skip to content

Commit 94bbd30

Browse files
add insertCardAtIndex and appendCards methods (#89)
1 parent 4efd183 commit 94bbd30

File tree

6 files changed

+214
-14
lines changed

6 files changed

+214
-14
lines changed

Shuffle-iOS.podspec

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
Pod::Spec.new do |s|
22

33
s.name = "Shuffle-iOS"
4-
s.version = "0.2.7"
4+
s.version = "0.3.0"
55
s.platform = :ios, "9.0"
66
s.summary = "A multi-directional card swiping library inspired by Tinder"
77

@@ -13,7 +13,7 @@ s.homepage = "https://github.com/mac-gallagher/Shuffle"
1313
s.documentation_url = "https://github.com/mac-gallagher/Shuffle/tree/master/README.md"
1414
s.license = { :type => 'MIT', :file => 'LICENSE' }
1515
s.author = { "Mac Gallagher" => "[email protected]" }
16-
s.source = { :git => "https://github.com/mac-gallagher/Shuffle.git", :tag => "v0.2.7" }
16+
s.source = { :git => "https://github.com/mac-gallagher/Shuffle.git", :tag => "v0.3.0" }
1717

1818
s.swift_version = "5.0"
1919
s.source_files = "Sources/**/*.{h,swift}"

Sources/Shuffle/SwipeCardStack/CardStackStateManager.swift

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ protocol CardStackStateManagable {
3030
var remainingIndices: [Int] { get }
3131
var swipes: [Swipe] { get }
3232

33+
func insert(_ index: Int, at position: Int)
34+
3335
func swipe(_ direction: SwipeDirection)
3436
func undoSwipe() -> Swipe?
3537
func shift(withDistance distance: Int)
@@ -49,6 +51,32 @@ class CardStackStateManager: CardStackStateManagable {
4951
/// An array containing the swipe history of the card stack.
5052
var swipes: [Swipe] = []
5153

54+
func insert(_ index: Int, at position: Int) {
55+
if position < 0 {
56+
fatalError("Attempt to insert card at position \(position)")
57+
}
58+
59+
if position > remainingIndices.count {
60+
//swiftlint:disable:next line_length
61+
fatalError("Attempt to insert card at position \(position), but there are only \(remainingIndices.count + 1) cards remaining in the stack after the update")
62+
}
63+
64+
if index < 0 {
65+
fatalError("Attempt to insert card at data source index \(index)")
66+
}
67+
68+
if index > remainingIndices.count + swipes.count {
69+
//swiftlint:disable:next line_length
70+
fatalError("Attempt to insert card at index \(index), but there are only \(remainingIndices.count + swipes.count + 1) cards after the update")
71+
}
72+
73+
// Increment all stored indices in the range [0, index] by 1
74+
remainingIndices = remainingIndices.map { $0 >= index ? $0 + 1 : $0 }
75+
swipes = swipes.map { $0.index >= index ? Swipe($0.index + 1, $0.direction) : $0 }
76+
77+
remainingIndices.insert(index, at: position)
78+
}
79+
5280
func swipe(_ direction: SwipeDirection) {
5381
if remainingIndices.isEmpty { return }
5482
let firstIndex = remainingIndices.removeFirst()

Sources/Shuffle/SwipeCardStack/SwipeCardStack.swift

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,12 @@ open class SwipeCardStack: UIView, SwipeCardDelegate, UIGestureRecognizerDelegat
3333

3434
/// Return `false` if you wish to ignore all horizontal gestures on the card stack.
3535
///
36-
/// You may wish to modify this property if your card stack is embedded in a `UIScrollView`.
36+
/// You may wish to modify this property if your card stack is embedded in a `UIScrollView`, for example.
3737
open var shouldRecognizeHorizontalDrag: Bool = true
3838

3939
/// Return `false` if you wish to ignore all vertical gestures on the card stack.
4040
///
41-
/// You may wish to modify this property if your card stack is embedded in a `UIScrollView`.
41+
/// You may wish to modify this property if your card stack is embedded in a `UIScrollView`, for example.
4242
open var shouldRecognizeVerticalDrag: Bool = true
4343

4444
public weak var delegate: SwipeCardStackDelegate?
@@ -55,6 +55,7 @@ open class SwipeCardStack: UIView, SwipeCardDelegate, UIGestureRecognizerDelegat
5555
}
5656
}
5757

58+
/// The data source index corresponding to the topmost card in the stack.
5859
public var topCardIndex: Int? {
5960
return visibleCards.first?.index
6061
}
@@ -167,12 +168,10 @@ open class SwipeCardStack: UIView, SwipeCardDelegate, UIGestureRecognizerDelegat
167168

168169
// MARK: - Main Methods
169170

170-
/// Calling this method triggers a swipe on the card stack.
171+
/// Triggers a swipe on the card stack in the specified direction.
171172
/// - Parameters:
172173
/// - direction: The direction to swipe the top card.
173-
/// - animated: A boolean indicating whether the reverse swipe should be animated. Setting this
174-
/// to `false` will immediately
175-
/// position the card at end state of the animation when the method is called.
174+
/// - animated: A boolean indicating whether the swipe action should be animated.
176175
public func swipe(_ direction: SwipeDirection, animated: Bool) {
177176
if !isEnabled { return }
178177

@@ -225,6 +224,8 @@ open class SwipeCardStack: UIView, SwipeCardDelegate, UIGestureRecognizerDelegat
225224
}
226225
}
227226

227+
/// Returns the most recently swiped card to the top of the card stack.
228+
/// - Parameter animated: A boolean indicating whether the undo action should be animated.
228229
public func undoLastSwipe(animated: Bool) {
229230
if !isEnabled { return }
230231
guard let previousSwipe = stateManager.undoSwipe() else { return }
@@ -248,6 +249,10 @@ open class SwipeCardStack: UIView, SwipeCardDelegate, UIGestureRecognizerDelegat
248249
}
249250
}
250251

252+
/// Shifts the remaining cards in the card stack by the specified distance. Any swiped cards are ignored.
253+
/// - Parameters:
254+
/// - distance: The distance to shift the remaining cards by.
255+
/// - animated: A boolean indicating whether the undo action should be animated.
251256
public func shift(withDistance distance: Int = 1, animated: Bool) {
252257
if !isEnabled || distance == 0 || visibleCards.count <= 1 { return }
253258

@@ -275,7 +280,6 @@ open class SwipeCardStack: UIView, SwipeCardDelegate, UIGestureRecognizerDelegat
275280
}
276281

277282
/// Returns the `SwipeCard` at the specified index.
278-
///
279283
/// - Parameter index: The index of the card in the data source.
280284
/// - Returns: The `SwipeCard` at the specified index, or `nil` if the card is not visible or the index is
281285
/// out of range.
@@ -292,10 +296,35 @@ open class SwipeCardStack: UIView, SwipeCardDelegate, UIGestureRecognizerDelegat
292296
/// - Parameter index: The index of the card in the data source.
293297
/// - Returns: The current position of the card at the specified index, or `nil` if the index if out of range or the
294298
/// card has been swiped.
295-
public func position(forCardAtIndex index: Int) -> Int? {
299+
public func positionforCard(at index: Int) -> Int? {
296300
return stateManager.remainingIndices.firstIndex(of: index)
297301
}
298302

303+
/// Returns the remaining number of cards in the card stack.
304+
/// - Returns: The number of cards in the card stack which have not yet been swiped.
305+
public func numberOfRemainingCards() -> Int {
306+
return stateManager.remainingIndices.count
307+
}
308+
309+
/// Inserts a new card with the given index at the specified position.
310+
/// - Parameters:
311+
/// - index: The index of the card in the data source.
312+
/// - position: The position of the new card in the card stack. This position should be determined on
313+
/// the returned value of `numberOfRemainingCards`.
314+
public func insertCard(atIndex index: Int, position: Int) {
315+
stateManager.insert(index, at: position)
316+
reloadVisibleCards()
317+
}
318+
319+
/// Appends a collection of new cards with the specifed indices to the bottom of the card stack.
320+
/// - Parameter indices: The indices of the cards in the data source.
321+
public func appendCards(atIndices indices: [Int]) {
322+
for index in indices {
323+
stateManager.insert(index, at: numberOfRemainingCards())
324+
}
325+
reloadVisibleCards()
326+
}
327+
299328
func reloadVisibleCards() {
300329
visibleCards.forEach { $0.card.removeFromSuperview() }
301330
visibleCards.removeAll()

Tests/ShuffleTests/SwipeCardStack/Mocks/MockCardStackStateManager.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,16 @@ class MockCardStackStateManager: CardStackStateManagable {
3131

3232
var swipes: [Swipe] = []
3333

34+
var insertCalled: Bool = false
35+
var insertIndices: [Int] = []
36+
var insertPositions: [Int] = []
37+
38+
func insert(_ index: Int, at position: Int) {
39+
insertCalled = true
40+
insertIndices.append(index)
41+
insertPositions.append(position)
42+
}
43+
3444
var swipeCalled = false
3545
var swipeDirection: SwipeDirection?
3646

Tests/ShuffleTests/SwipeCardStack/Specs/CardStackStateManagerSpec.swift

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,83 @@ class CardStackStateManagerSpec: QuickSpec {
5050
}
5151
}
5252

53+
// MARK: - Insert
54+
55+
describe("When calling insert") {
56+
context("and position is less than zero") {
57+
let position: Int = -1
58+
59+
beforeEach {
60+
subject.remainingIndices = [1, 2, 3]
61+
}
62+
63+
it("should throw a fatal error") {
64+
expect(subject.insert(4, at: position)).to(throwAssertion())
65+
}
66+
}
67+
68+
context("and position is greater than the number of remaining indices") {
69+
let position: Int = 4
70+
71+
beforeEach {
72+
subject.remainingIndices = [1, 2, 3]
73+
}
74+
75+
it("should throw a fatal error") {
76+
expect(subject.insert(4, at: position)).to(throwAssertion())
77+
}
78+
}
79+
80+
context("and index is less than zero") {
81+
let index: Int = -1
82+
83+
beforeEach {
84+
subject.remainingIndices = [1, 2, 3]
85+
}
86+
87+
it("should throw a fatal error") {
88+
expect(subject.insert(index, at: 0)).to(throwAssertion())
89+
}
90+
}
91+
92+
context("and index is greater than the number of remaining indices + swiped count") {
93+
let index: Int = 5
94+
95+
beforeEach {
96+
subject.swipes = [Swipe(0, .left), Swipe(1, .left)]
97+
subject.remainingIndices = [2, 3]
98+
}
99+
100+
it("should throw a fatal error") {
101+
expect(subject.insert(index, at: 0)).to(throwAssertion())
102+
}
103+
}
104+
105+
context("and position is at least zero and at most the number of remaining indices") {
106+
let oldRemainingIndices: [Int] = [3, 2, 5, 6, 0]
107+
let oldSwipes = [Swipe(1, .left), Swipe(4, .left), Swipe(7, .left)]
108+
let index: Int = 4
109+
let position: Int = 2
110+
111+
beforeEach {
112+
subject.remainingIndices = oldRemainingIndices
113+
subject.swipes = oldSwipes
114+
subject.insert(index, at: position)
115+
}
116+
117+
it("should insert the index at the correct position in remainingIndices") {
118+
expect(subject.remainingIndices[position]) == index
119+
}
120+
121+
it("should increment all stored indices greater than index by one") {
122+
expect(subject.remainingIndices[3]) == oldRemainingIndices[2] + 1
123+
expect(subject.remainingIndices[4]) == oldRemainingIndices[3] + 1
124+
expect(subject.swipes[1].index) == oldSwipes[1].index + 1
125+
expect(subject.swipes[2].index) == oldSwipes[2].index + 1
126+
}
127+
}
128+
}
129+
53130
// MARK: - Swipe
54131

55132
describe("When calling swipe") {

Tests/ShuffleTests/SwipeCardStack/Specs/SwipeCardStackSpec_MainMethods.swift

Lines changed: 60 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import Quick
2727
@testable import Shuffle
2828
import UIKit
2929

30-
// swiftlint:disable type_body_length closure_body_length implicitly_unwrapped_optional
30+
// swiftlint:disable file_length type_body_length closure_body_length implicitly_unwrapped_optional
3131
class SwipeCardStackSpec_MainMethods: QuickSpec {
3232

3333
// swiftlint:disable:next function_body_length
@@ -471,7 +471,7 @@ class SwipeCardStackSpec_MainMethods: QuickSpec {
471471
let index: Int = 2
472472

473473
it("should return nil") {
474-
let actualPosition = subject.position(forCardAtIndex: index)
474+
let actualPosition = subject.positionforCard(at: index)
475475
expect(actualPosition).to(beNil())
476476
}
477477
}
@@ -480,12 +480,68 @@ class SwipeCardStackSpec_MainMethods: QuickSpec {
480480
let index: Int = 4
481481

482482
it("should return the first index of the specified position in the state manager's remaining indices") {
483-
let actualPosition = subject.position(forCardAtIndex: index)
483+
let actualPosition = subject.positionforCard(at: index)
484484
expect(actualPosition) == 1
485485
}
486486
}
487487
}
488488

489+
// MARK: Number of Remaining Cards
490+
491+
describe("When calling numberOfRemainingCards") {
492+
beforeEach {
493+
mockStateManager.remainingIndices = [3, 4, 5, 4]
494+
}
495+
496+
it("should return the number of elements in the state manager's remainingIndices") {
497+
expect(subject.numberOfRemainingCards()) == mockStateManager.remainingIndices.count
498+
}
499+
}
500+
501+
// MARK: Insert Card At Index
502+
503+
describe("When calling insertCardAtIndex") {
504+
let index: Int = 1
505+
let position: Int = 2
506+
507+
beforeEach {
508+
subject.insertCard(atIndex: index, position: position)
509+
}
510+
511+
it("should call the stateManager's insert method with the correct parameters") {
512+
expect(mockStateManager.insertCalled) == true
513+
expect(mockStateManager.insertIndices) == [index]
514+
expect(mockStateManager.insertPositions) == [position]
515+
}
516+
517+
it("should call reloadVisibleCards") {
518+
expect(subject.reloadVisibleCardsCalled) == true
519+
}
520+
}
521+
522+
// MARK: Append Cards
523+
524+
describe("When calling appendCards") {
525+
let indices: [Int] = [1, 2, 3]
526+
let remainingIndices: [Int] = [0, 1, 2, 3, 4]
527+
528+
beforeEach {
529+
mockStateManager.remainingIndices = remainingIndices
530+
subject.appendCards(atIndices: indices)
531+
}
532+
533+
it("should call the stateManager's insert method with the correct parameters") {
534+
expect(mockStateManager.insertCalled) == true
535+
expect(mockStateManager.insertIndices) == indices
536+
expect(mockStateManager.insertPositions) == Array(repeating: remainingIndices.count,
537+
count: indices.count)
538+
}
539+
540+
it("should call reloadVisibleCards") {
541+
expect(subject.reloadVisibleCardsCalled) == true
542+
}
543+
}
544+
489545
// MARK: Reload Visible Cards
490546

491547
describe("When calling reloadVisibleCards") {
@@ -593,4 +649,4 @@ class SwipeCardStackSpec_MainMethods: QuickSpec {
593649
}
594650
}
595651
}
596-
// swiftlint:enable type_body_length closure_body_length implicitly_unwrapped_optional
652+
// swiftlint:enable file_length type_body_length closure_body_length implicitly_unwrapped_optional

0 commit comments

Comments
 (0)