Skip to content

Commit

Permalink
add deleteCards methods (#109)
Browse files Browse the repository at this point in the history
  • Loading branch information
mac-gallagher authored Aug 1, 2020
1 parent 2be4f20 commit 9502494
Show file tree
Hide file tree
Showing 11 changed files with 258 additions and 80 deletions.
22 changes: 12 additions & 10 deletions Documentation/AdvancedUsage.md
Original file line number Diff line number Diff line change
@@ -1,21 +1,26 @@
# Advanced Usage

* [Adding New Cards](#adding-new-cards)
* [Animations](#animations)
* [Inserting and Deleting Cards](#inserting-and-deleting-cards)
* [Swipe Recognition](#swipe-recognition)

## Adding New Cards
## Animations
// TODO

## Inserting and Deleting Cards

If you're using an external API to retrieve your `SwipeCard` data models, chances are you'll need to update the card stack occasionally as new models come in. As of version [3.0.1](https://github.com/mac-gallagher/Shuffle/releases/tag/v0.3.1), Shuffle includes the following methods on `SwipeCardStack` to make this possible:
If you're using an external API to retrieve your `SwipeCard` data models, chances are you'll need to update the card stack occasionally as new models come in. As of version [0.4.0](https://github.com/mac-gallagher/Shuffle/releases/tag/v0.4.0), Shuffle includes the following methods on `SwipeCardStack`:

```swift
func insertCard(atIndex index: Int, position: Int)
func appendCards(atIndices indices: [Int]) // Index refers to the index of the card in the data source
```
```swift
func appendCards(atIndices indices: [Int])
func deleteCards(atIndices indices: [Int])
func deleteCards(atPositions positions: [Int]) // Position refers to the position of the card in the stack
```

With these methods, we can give the iullusion of an "infinite" card stack. Let's look at an example.
Using the insert methods in particular, we can give the illusion of an "infinite" card stack. Let's look at an example.

### External API Example
Suppose we have a utility which fetches raw data models and decodes them into an array of `CardModels`:
Expand All @@ -42,7 +47,7 @@ class ViewController: UIViewController: SwipeCardStackDataSource, SwipeCardStack
cardStack.dataSource = self
cardStack.delegate = self

// layout cardStack on view
// Layout cardStack on view

addCards()
}
Expand Down Expand Up @@ -93,7 +98,7 @@ NetworkUtility.fetchNewCardModels { [weak self] newModels in
guard let strongSelf = self else { return }

DispatchQueue.main.async {
// insert in reverse order so that newModels.first is the model for the topmost card
// Insert in reverse order so that newModels.first is the model for the topmost card
for model in newModels.reversed() {
strongSelf.cardModels.insert(model, at: 0)
strongSelf.cardStack.insertCard(atIndex: 0, position: 0)
Expand All @@ -119,8 +124,5 @@ The `insertCard` method has an additional `position` parameter which represents

Since the number of remaining cards is subject to change, be sure to calculate the `position` based on the value returned from the `numberOfRemainingCards:` method on `SwipeCardStack`.

## Animations
// TODO

## Swipe Recognition
// TODO
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,8 +172,8 @@ func setOverlays(_ overlays: [SwipeDirection: UIView])
## Advanced Usage
For more advanced usage, including

* [Adding New Cards](Documentation/AdvancedUsage.md#adding-new-cards)
* [Animations](Documentation/AdvancedUsage.md#animations)
* [Inserting and Deleting Cards](Documentation/AdvancedUsage.md#inserting-and-deleting-cards)
* [Swipe Recognition](Documentation/AdvancedUsage.md#swipe-recognition)

visit the document [here](Documentation/AdvancedUsage.md).
Expand Down
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.3.3"
s.version = "0.4.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.3.3" }
s.source = { :git => "https://github.com/mac-gallagher/Shuffle.git", :tag => "v0.4.0" }

s.swift_version = "5.0"
s.source_files = "Sources/**/*.{h,swift}"
Expand Down
4 changes: 4 additions & 0 deletions Shuffle.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
881FD28C231B78FB003ACA43 /* CardStackAnimationOptionsSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 881FD28B231B78FB003ACA43 /* CardStackAnimationOptionsSpec.swift */; };
904ADE5B4A83DD023287EB73 /* Pods_ShuffleExample.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 91AB74E2E12D2E4C970B6743 /* Pods_ShuffleExample.framework */; };
AD0542C02271397900B42353 /* Shuffle.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AD72AC5D2270E5E20083E735 /* Shuffle.framework */; };
AD0DF2C424D1FC9800B7DD2E /* TestableCardStackStateManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD0DF2C324D1FC9800B7DD2E /* TestableCardStackStateManager.swift */; };
AD10982922879C42008CB197 /* CardAnimationOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD10982322879C42008CB197 /* CardAnimationOptions.swift */; };
AD10982B22879C42008CB197 /* CardAnimator.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD10982522879C42008CB197 /* CardAnimator.swift */; };
AD10982D22879C42008CB197 /* SwipeCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD10982822879C42008CB197 /* SwipeCard.swift */; };
Expand Down Expand Up @@ -133,6 +134,7 @@
91AB74E2E12D2E4C970B6743 /* Pods_ShuffleExample.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_ShuffleExample.framework; sourceTree = BUILT_PRODUCTS_DIR; };
AD0542BB2271397900B42353 /* ShuffleTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ShuffleTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
AD0542BF2271397900B42353 /* ShuffleTests.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = ShuffleTests.plist; sourceTree = "<group>"; };
AD0DF2C324D1FC9800B7DD2E /* TestableCardStackStateManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestableCardStackStateManager.swift; sourceTree = "<group>"; };
AD10982322879C42008CB197 /* CardAnimationOptions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CardAnimationOptions.swift; sourceTree = "<group>"; };
AD10982522879C42008CB197 /* CardAnimator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CardAnimator.swift; sourceTree = "<group>"; };
AD10982822879C42008CB197 /* SwipeCard.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwipeCard.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -518,6 +520,7 @@
ADEE86E022ADED7F00AAE7A1 /* Testables */ = {
isa = PBXGroup;
children = (
AD0DF2C324D1FC9800B7DD2E /* TestableCardStackStateManager.swift */,
ADEE86E122ADED8A00AAE7A1 /* TestableSwipeCardStack.swift */,
);
path = Testables;
Expand Down Expand Up @@ -803,6 +806,7 @@
AD9F44BC2468D04600725A8D /* SwipeCardStackSpec_MainMethods.swift in Sources */,
AD52EF8B22E532B90063AE5D /* TestableNotificationCenter.swift in Sources */,
AD1098382287CAD4008CB197 /* TestableSwipeCard.swift in Sources */,
AD0DF2C424D1FC9800B7DD2E /* TestableCardStackStateManager.swift in Sources */,
AD52EF8F22E56AB70063AE5D /* TestableCardTransformProvider.swift in Sources */,
AD10983D2287CAE4008CB197 /* MockSwipeCardDelegate.swift in Sources */,
ADEE86DA22ADEC9700AAE7A1 /* MockSwipeCardStackDelegate.swift in Sources */,
Expand Down
12 changes: 12 additions & 0 deletions Sources/Shuffle/Internal/Array+Extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,15 @@ extension Array {
self = Array(self[index ..< endIndex] + self[startIndex ..< index])
}
}

extension Array where Element: Hashable {

func removingDuplicates() -> [Element] {
var dict = [Element: Bool]()
return filter { dict.updateValue(true, forKey: $0) == nil }
}

mutating func removeDuplicates() {
self = self.removingDuplicates()
}
}
38 changes: 38 additions & 0 deletions Sources/Shuffle/SwipeCardStack/CardStackStateManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,11 @@ protocol CardStackStateManagable {
var totalIndexCount: Int { get }

func insert(_ index: Int, at position: Int)

func delete(_ index: Int)
func delete(_ indices: [Int])
func delete(indexAtPosition position: Int)
func delete(indicesAtPositions positions: [Int])

func swipe(_ direction: SwipeDirection)
func undoSwipe() -> Swipe?
Expand All @@ -61,6 +64,8 @@ class CardStackStateManager: CardStackStateManagable {
return remainingIndices.count + swipes.count
}

// MARK: - Insertion

func insert(_ index: Int, at position: Int) {
precondition(index >= 0, "Attempt to insert card at index \(index)")
//swiftlint:disable:next line_length
Expand All @@ -76,6 +81,8 @@ class CardStackStateManager: CardStackStateManagable {
remainingIndices.insert(index, at: position)
}

// MARK: - Deletion

func delete(_ index: Int) {
precondition(index >= 0, "Attempt to delete card at index \(index)")
//swiftlint:disable:next line_length
Expand All @@ -92,6 +99,20 @@ class CardStackStateManager: CardStackStateManagable {
swipes = swipes.map { $0.index >= index ? Swipe(index: $0.index - 1, direction: $0.direction) : $0 }
}

func delete(_ indices: [Int]) {
var remainingIndices = indices.removingDuplicates()

while !remainingIndices.isEmpty {
let index = remainingIndices[0]
delete(index)

remainingIndices.remove(at: 0)

// Decrement all remaining indices greater than or equal to index by 1
remainingIndices = remainingIndices.map { $0 >= index ? $0 - 1 : $0 }
}
}

func delete(indexAtPosition position: Int) {
precondition(position >= 0, "Attempt to delete card at position \(position)")
//swiftlint:disable:next line_length
Expand All @@ -100,8 +121,25 @@ class CardStackStateManager: CardStackStateManagable {
// 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 }
swipes = swipes.map { $0.index >= index ? Swipe(index: $0.index - 1, direction: $0.direction) : $0 }
}

func delete(indicesAtPositions positions: [Int]) {
var remainingPositions = positions.removingDuplicates()

while !remainingPositions.isEmpty {
let position = remainingPositions[0]
delete(indexAtPosition: position)

remainingPositions.remove(at: 0)

// Decrement all remaining positions greater than or equal to position by 1
remainingPositions = remainingPositions.map { $0 >= position ? $0 - 1 : $0 }
}
}

// MARK: - Main Methods

func swipe(_ direction: SwipeDirection) {
if remainingIndices.isEmpty { return }
let firstIndex = remainingIndices.removeFirst()
Expand Down
43 changes: 28 additions & 15 deletions Sources/Shuffle/SwipeCardStack/SwipeCardStack.swift
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ open class SwipeCardStack: UIView, SwipeCardDelegate, UIGestureRecognizerDelegat
stateManager.swipe(direction)
visibleCards.remove(at: 0)

// insert new card if needed
// Insert new card if needed
if (stateManager.remainingIndices.count - visibleCards.count) > 0 {
let bottomCardIndex = stateManager.remainingIndices[visibleCards.count]
if let card = loadCard(at: bottomCardIndex) {
Expand Down Expand Up @@ -337,7 +337,15 @@ open class SwipeCardStack: UIView, SwipeCardDelegate, UIGestureRecognizerDelegat
return stateManager.remainingIndices.count
}

/// Returns the indices of the swiped cards in the order they were swiped.
/// - Returns: The indices of the swiped cards in the data source.
public func swipedCards() -> [Int] {
return stateManager.swipes.map { $0.index }
}

/// Inserts a new card with the given index at the specified position.
///
/// Calling this method will not clear the swipe history nor trigger a reload of the data source.
/// - Parameters:
/// - index: The index of the card in the data source.
/// - position: The position of the new card in the card stack.
Expand All @@ -360,6 +368,8 @@ open class SwipeCardStack: UIView, SwipeCardDelegate, UIGestureRecognizerDelegat
}

/// Appends a collection of new cards with the specifed indices to the bottom of the card stack.
///
/// Calling this method will not clear the swipe history nor trigger a reload of the data source.
/// - Parameter indices: The indices of the cards in the data source.
public func appendCards(atIndices indices: [Int]) {
guard let dataSource = dataSource else { return }
Expand All @@ -381,43 +391,46 @@ open class SwipeCardStack: UIView, SwipeCardDelegate, UIGestureRecognizerDelegat
reloadVisibleCards()
}

/// Deletes the card at the specified index.
/// - Parameter index: The index of the card in the data source.
public func deleteCard(atIndex index: Int) {
/// Deletes the cards at the specified indices. If an index corresponds to a card that has been swiped,
/// it is removed from the swipe history.
///
/// Calling this method will not clear the swipe history nor trigger a reload of the data source.
/// - Parameter indices: The indices of the cards in the data source to delete.
public func deleteCards(atIndices indices: [Int]) {
guard let dataSource = dataSource else { return }

let oldNumberOfCards = stateManager.totalIndexCount
let newNumberOfCards = dataSource.numberOfCards(in: self)

stateManager.delete(index)

if newNumberOfCards != oldNumberOfCards - 1 {
if newNumberOfCards != oldNumberOfCards - indices.count {
let errorString = StringUtils.createInvalidUpdateErrorString(newCount: newNumberOfCards,
oldCount: oldNumberOfCards,
deletedCount: 1)
deletedCount: indices.count)
fatalError(errorString)
}

stateManager.delete(indices)
reloadVisibleCards()
}

/// 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) {
/// Deletes the cards at the specified positions in the card stack.
///
/// Calling this method will not clear the swipe history nor trigger a reload of the data source.
/// - Parameter positions: The positions of the cards to delete in the card stack.
public func deleteCards(atPositions positions: [Int]) {
guard let dataSource = dataSource else { return }

let oldNumberOfCards = stateManager.totalIndexCount
let newNumberOfCards = dataSource.numberOfCards(in: self)

stateManager.delete(indexAtPosition: position)

if newNumberOfCards != oldNumberOfCards - 1 {
if newNumberOfCards != oldNumberOfCards - positions.count {
let errorString = StringUtils.createInvalidUpdateErrorString(newCount: newNumberOfCards,
oldCount: oldNumberOfCards,
deletedCount: 1)
deletedCount: positions.count)
fatalError(errorString)
}

stateManager.delete(indicesAtPositions: positions)
reloadVisibleCards()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,20 +41,24 @@ class MockCardStackStateManager: CardStackStateManagable {
insertPositions.append(position)
}

var deleteAtIndexCalled: Bool = false
var deleteAtIndexIndex: Int?
func delete(_ index: Int) {}

func delete(_ index: Int) {
deleteAtIndexCalled = true
deleteAtIndexIndex = index
var deleteIndicesCalled: Bool = false
var deleteIndicesIndices: [Int] = []

func delete(_ indices: [Int]) {
deleteIndicesCalled = true
deleteIndicesIndices = indices
}

var deleteAtPositionCalled: Bool = false
var deleteAtPositionPosition: Int?
func delete(indexAtPosition position: Int) {}

var deleteIndicesAtPositionCalled: Bool = false
var deleteIndicesAtPositionPositions: [Int] = []

func delete(indexAtPosition position: Int) {
deleteAtPositionCalled = true
deleteAtPositionPosition = position
func delete(indicesAtPositions positions: [Int]) {
deleteIndicesAtPositionCalled = true
deleteIndicesAtPositionPositions = positions
}

var swipeCalled = false
Expand Down
Loading

0 comments on commit 9502494

Please sign in to comment.