Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add insertCard and appendCards methods #89

Merged
merged 1 commit into from
Jul 7, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Shuffle-iOS.podspec
Original file line number Diff line number Diff line change
@@ -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"

Expand All @@ -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" => "[email protected]" }
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}"
Expand Down
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 {
//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()
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 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 }

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 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()
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