Skip to content

Commit

Permalink
add insertCardAtIndex and appendCards methods
Browse files Browse the repository at this point in the history
  • Loading branch information
mac-gallagher committed Jul 7, 2020
1 parent 4efd183 commit 03e6294
Show file tree
Hide file tree
Showing 5 changed files with 212 additions and 12 deletions.
28 changes: 28 additions & 0 deletions Sources/Shuffle/SwipeCardStack/CardStackStateManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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 {
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 {
fatalError("Attempt to insert card at index \(index), but there are only \(remainingIndices.count + swipes.count)"
+ " 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()
Expand Down
45 changes: 37 additions & 8 deletions Sources/Shuffle/SwipeCardStack/SwipeCardStack.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand All @@ -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
}
Expand Down Expand Up @@ -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 }

Expand Down Expand Up @@ -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 }
Expand All @@ -248,6 +249,10 @@ open class SwipeCardStack: UIView, SwipeCardDelegate, UIGestureRecognizerDelegat
}
}

/// Shifts the
/// - Parameters:
/// - distance: <#distance description#>
/// - animated: <#animated description#>
public func shift(withDistance distance: Int = 1, animated: Bool) {
if !isEnabled || distance == 0 || visibleCards.count <= 1 { return }

Expand Down Expand Up @@ -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.
Expand All @@ -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 current 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 based 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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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?

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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") {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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())
}
}
Expand All @@ -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") {
Expand Down Expand Up @@ -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

0 comments on commit 03e6294

Please sign in to comment.