├── Scripts
├── validate.sh
├── lint.sh
├── validate_demo.sh
├── install_swiftlint.sh
├── build.sh
└── test.sh
├── CardStackExample
├── CardStackExample.gif
├── CardStackExample
│ ├── Assets.xcassets
│ │ ├── Contents.json
│ │ └── AppIcon.appiconset
│ │ │ └── Contents.json
│ ├── Preview Content
│ │ └── Preview Assets.xcassets
│ │ │ └── Contents.json
│ ├── Models
│ │ └── DataExample.swift
│ ├── Views
│ │ ├── CardExampleView.swift
│ │ └── StackExampleView.swift
│ ├── AppDelegate.swift
│ ├── Base.lproj
│ │ └── LaunchScreen.storyboard
│ ├── Info.plist
│ └── SceneDelegate.swift
└── CardStackExample.xcodeproj
│ ├── project.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ └── swiftpm
│ │ └── Package.resolved
│ └── project.pbxproj
├── .swiftpm
└── xcode
│ ├── package.xcworkspace
│ └── contents.xcworkspacedata
│ └── xcshareddata
│ └── xcschemes
│ └── CardStack.xcscheme
├── Tests
├── LinuxMain.swift
└── CardStackTests
│ ├── MockData
│ └── MockCardData.swift
│ ├── NavigationHelperTests.swift
│ ├── CardViewHelperTests+getSmallestItemSize.swift
│ ├── CardViewHelperTests+getCardSize.swift
│ └── CardViewHelperTests.swift
├── Sources
└── CardStack
│ ├── Enums
│ ├── StackStyle.swift
│ └── HorizontalDirection.swift
│ ├── Protocols
│ ├── CardView.swift
│ └── CardData.swift
│ ├── Extensions
│ ├── CGFloat.swift
│ └── DragGesture.swift
│ ├── Models
│ ├── DragInformation.swift
│ └── StackConfiguration.swift
│ ├── Views
│ ├── CardStackView.swift
│ ├── CardStackMainView.swift
│ └── CardContentView.swift
│ ├── Helpers
│ ├── NavigationHelper.swift
│ └── CardViewHelper.swift
│ └── CardNavigation
│ └── CardNavigation.swift
├── .swiftlint.yml
├── .github
└── workflows
│ └── manual.yml
├── .travis.yml
├── LICENSE
├── Package.swift
├── .gitignore
└── README.md
/Scripts/validate.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | ./temp/swiftlint lint --strict
4 |
--------------------------------------------------------------------------------
/Scripts/lint.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | if which swiftlint >/dev/null; then
4 | swiftlint --config ../.swiftlint.yml
5 | fi
6 |
--------------------------------------------------------------------------------
/CardStackExample/CardStackExample.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nhoogendoorn/CardStack/HEAD/CardStackExample/CardStackExample.gif
--------------------------------------------------------------------------------
/CardStackExample/CardStackExample/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/CardStackExample/CardStackExample/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/Scripts/validate_demo.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | cd CardStackExample
4 |
5 | xcodebuild -project CardStackExample.xcodeproj -scheme CardStackExample -destination "OS=13.0,name=iPhone 11" | xcpretty
6 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/CardStackExample/CardStackExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Tests/LinuxMain.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 |
3 | import CardStackTests
4 |
5 | var tests = [XCTestCaseEntry]()
6 | //tests += CardStackTests.allTests()
7 | fatalError("Running tests like this is unsupported. Run the tests again by using `swift test --enable-test-discovery`")
8 | XCTMain(tests)
9 |
--------------------------------------------------------------------------------
/Sources/CardStack/Enums/StackStyle.swift:
--------------------------------------------------------------------------------
1 | //
2 | // StackStyle.swift
3 | // CardStack
4 | //
5 | // Created by Niels Hoogendoorn on 15/03/2020.
6 | // Copyright © 2020 Nihoo. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | enum StackStyle {
12 | case sameHeight, decreasingHeight
13 | }
14 |
--------------------------------------------------------------------------------
/Scripts/install_swiftlint.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | # -L to enable redirects
4 | echo "Installing SwiftLint by downloading a pre-compiled binary"
5 | curl -L 'https://github.com/realm/SwiftLint/releases/download/0.37.0/portable_swiftlint.zip' -o swiftlint.zip
6 | mkdir temp
7 | unzip swiftlint.zip -d temp
8 | rm -f swiftlint.zip
9 |
--------------------------------------------------------------------------------
/Sources/CardStack/Protocols/CardView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Card.swift
3 | // CardStack
4 | //
5 | // Created by Niels Hoogendoorn on 14/03/2020.
6 | // Copyright © 2020 Nihoo. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | public protocol CardView: View {
12 | init(data: Data)
13 | }
14 |
--------------------------------------------------------------------------------
/Sources/CardStack/Protocols/CardData.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CardData.swift
3 | // CardStack
4 | //
5 | // Created by Niels Hoogendoorn on 14/03/2020.
6 | // Copyright © 2020 Nihoo. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | public protocol CardData: Identifiable {
12 | var id: String { get }
13 | }
14 |
--------------------------------------------------------------------------------
/Sources/CardStack/Enums/HorizontalDirection.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HorizontalDirection.swift
3 | // CardStack
4 | //
5 | // Created by Niels Hoogendoorn on 08/02/2020.
6 | // Copyright © 2020 Nihoo. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | enum HorizontalDirection {
12 | case forward, backward, none
13 | }
14 |
--------------------------------------------------------------------------------
/CardStackExample/CardStackExample/Models/DataExample.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DataExample.swift
3 | // CardStackExample
4 | //
5 | // Created by Niels Hoogendoorn on 15/03/2020.
6 | // Copyright © 2020 Nihoo. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 | import CardStack
11 |
12 | struct DataExample: CardData {
13 | var id: String
14 | var color: Color
15 | }
16 |
--------------------------------------------------------------------------------
/Sources/CardStack/Extensions/CGFloat.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CGFloat.swift
3 | // CardStack
4 | //
5 | // Created by Niels Hoogendoorn on 08/02/2020.
6 | // Copyright © 2020 Nihoo. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | extension CGFloat {
12 | static let screenWidth: CGFloat = UIScreen.main.bounds.width
13 | static let cardDistance: CGFloat = 15
14 | }
15 |
--------------------------------------------------------------------------------
/Sources/CardStack/Models/DragInformation.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DragInformation.swift
3 | // CardStack
4 | //
5 | // Created by Niels Hoogendoorn on 08/02/2020.
6 | // Copyright © 2020 Nihoo. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | struct DragInformation {
12 | let currentIndex: Int
13 | let items: [Data]
14 | let item: Data
15 | let dragValue: CGPoint
16 | }
17 |
--------------------------------------------------------------------------------
/CardStackExample/CardStackExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "object": {
3 | "pins": [
4 | {
5 | "package": "CardStack",
6 | "repositoryURL": "https://github.com/nhoogendoorn/CardStack",
7 | "state": {
8 | "branch": null,
9 | "revision": "15055eb52796546111e89c8acf7c6e7a4fcf22a4",
10 | "version": "0.1.3"
11 | }
12 | }
13 | ]
14 | },
15 | "version": 1
16 | }
17 |
--------------------------------------------------------------------------------
/Scripts/build.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | scheme="CardStack"
4 |
5 | while getopts "d:" opt; do
6 | case $opt in
7 | d) destinations+=("$OPTARG");;
8 | #...
9 | esac
10 | done
11 | shift $((OPTIND -1))
12 |
13 | echo "destinations = ${destinations[@]}"
14 |
15 | set -o pipefail
16 | xcodebuild -version
17 |
18 | for dest in "${destinations[@]}"; do
19 | echo "Building for destination: $dest"
20 | xcodebuild build -scheme $scheme -destination "$dest" | xcpretty;
21 | done
22 |
--------------------------------------------------------------------------------
/.swiftlint.yml:
--------------------------------------------------------------------------------
1 | disabled_rules:
2 | - trailing_whitespace
3 | - identifier_name
4 | - xctfail_message
5 | - unneeded_break_in_switch
6 | - multiple_closures_with_trailing_closure
7 | - force_try
8 | identifier_name:
9 | excluded:
10 | - id
11 |
12 | line_length: 200
13 |
14 | function_body_length:
15 | warning: 45
16 | error: 100
17 |
18 | function_parameter_count:
19 | warning: 5
20 |
21 | cyclomatic_complexity:
22 | ignores_case_statements: true
23 |
24 | type_name:
25 | allowed_symbols:
26 | - "_"
27 |
--------------------------------------------------------------------------------
/.github/workflows/manual.yml:
--------------------------------------------------------------------------------
1 | # This is a basic workflow that is manually triggered
2 |
3 | name: Release
4 |
5 | # Controls when the action will run. Workflow runs when manually triggered using the UI
6 | # or API.
7 | on:
8 | release:
9 | types: [created]
10 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel
11 | jobs:
12 | # This workflow contains a single job called "greet"
13 | greet:
14 | # The type of runner that the job will run on
15 | runs-on: ubuntu-latest
16 |
17 | # Steps represent a sequence of tasks that will be executed as part of the job
18 | steps:
19 | # Runs a single command using the runners shell
20 | - name: Send greeting
21 | run: echo "Hello ${{ inputs.name }}"
22 |
--------------------------------------------------------------------------------
/Tests/CardStackTests/MockData/MockCardData.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MockCardData.swift
3 | // TestTests
4 | //
5 | // Created by Niels Hoogendoorn on 13/03/2020.
6 | // Copyright © 2020 Nihoo. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | @testable import CardStack
11 |
12 | struct MockCardInformation: CardData {
13 | var id: String = UUID().uuidString
14 | }
15 |
16 | class MockCardData {
17 | static let items: [MockCardInformation] = [MockCardInformation(),
18 | MockCardInformation(),
19 | MockCardInformation(),
20 | MockCardInformation(),
21 | MockCardInformation()]
22 | }
23 |
--------------------------------------------------------------------------------
/Scripts/test.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | scheme="CardStack"
4 |
5 | while getopts "s:d:" opt; do
6 | case $opt in
7 | s) scheme=${OPTARG};;
8 | d) destinations+=("$OPTARG");;
9 | #...
10 | esac
11 | done
12 | shift $((OPTIND -1))
13 |
14 | echo "scheme = ${scheme}"
15 | echo "destinations = ${destinations[@]}"
16 |
17 |
18 | set -o pipefail
19 | xcodebuild -version
20 |
21 |
22 | xcodebuild build-for-testing -scheme "$scheme" -destination "${destinations[0]}" | xcpretty;
23 |
24 | for destination in "${destinations[@]}"; do
25 | echo "\nRunning tests for destination: $destination"
26 |
27 | # passing multiple destinations to `test` command results in Travis hanging
28 | xcodebuild test-without-building -scheme "$scheme" -destination "$destination" | xcpretty;
29 | done
30 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: swift
2 | osx_image: xcode11.4
3 |
4 | git:
5 | depth: 1
6 |
7 | stages:
8 | - name: test
9 |
10 | branches:
11 | only:
12 | - master
13 |
14 | jobs:
15 | include:
16 | - stage: test
17 | name: Run Unit Tests (iOS, Xcode 11.3)
18 | osx_image: xcode11.4
19 | script: Scripts/test.sh -d "OS=13.3,name=iPhone 11"
20 |
21 | - stage: test
22 | name: Run Validations (SwiftLint)
23 | osx_image: xcode11.4
24 | install: Scripts/install_swiftlint.sh
25 | script: Scripts/validate.sh
26 |
27 | - stage: test
28 | name: Build Demo Project
29 | osx_image: xcode11.4
30 | script: Scripts/validate_demo.sh
31 |
32 | - stage: test
33 | name: Swift Build (Swift Package Manager)
34 | osx_image: xcode11.4
35 | script: Scripts/build.sh
--------------------------------------------------------------------------------
/Sources/CardStack/Views/CardStackView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CardStackView.swift
3 | // CardStack
4 | //
5 | // Created by Niels Hoogendoorn on 08/02/2020.
6 | // Copyright © 2020 Nihoo. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | public struct CardStackView: View {
12 | var configuration: StackConfiguration?
13 | let items: [Data]
14 |
15 | public init(configuration: StackConfiguration? = nil, items: [Data]) {
16 | self.configuration = configuration
17 | self.items = items
18 | }
19 |
20 | public var body: some View {
21 | CardStackMainView(configuration: configuration)
22 | .animation(.default)
23 | .frame(maxWidth: .infinity, maxHeight: .infinity)
24 | .environmentObject(CardNavigation(items: items,
25 | startIndex: configuration?.startIndex ?? 0))
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/CardStackExample/CardStackExample/Views/CardExampleView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CardExampleView.swift
3 | // CardStackExample
4 | //
5 | // Created by Niels Hoogendoorn on 15/03/2020.
6 | // Copyright © 2020 Nihoo. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 | import CardStack
11 |
12 | struct CardExampleView: CardView {
13 | var data: DataExample?
14 |
15 | init(data: Data) where Data: CardData {
16 | if let infoData = data as? DataExample {
17 | self.data = infoData
18 | }
19 | }
20 |
21 | var body: some View {
22 | data?.color
23 | .frame(width: 200, height: 200)
24 | .shadow(color: Color.black.opacity(0.1), radius: 3, x: 0, y: 0)
25 | .cornerRadius(8)
26 | }
27 | }
28 |
29 | struct CardExampleView_Previews: PreviewProvider {
30 | static var previews: some View {
31 | CardExampleView(data: DataExample(id: UUID().uuidString, color: .red))
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/CardStackExample/CardStackExample/Views/StackExampleView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // StackExampleView.swift
3 | // CardStackExample
4 | //
5 | // Created by Niels Hoogendoorn on 15/03/2020.
6 | // Copyright © 2020 Nihoo. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 | import CardStack
11 |
12 | struct StackExampleView: View {
13 | let items: [DataExample] = [DataExample(id: UUID().uuidString, color: .red),
14 | DataExample(id: UUID().uuidString, color: .blue),
15 | DataExample(id: UUID().uuidString, color: .yellow),
16 | DataExample(id: UUID().uuidString, color: .green),
17 | DataExample(id: UUID().uuidString, color: .orange)
18 | ]
19 |
20 | let configuration = StackConfiguration()
21 |
22 | var body: some View {
23 | CardStackView(configuration: configuration, items: items)
24 | }
25 | }
26 |
27 | struct StackExampleView_Previews: PreviewProvider {
28 | static var previews: some View {
29 | StackExampleView()
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Niels Hoogendoorn
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Sources/CardStack/Helpers/NavigationHelper.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NavigationHelper.swift
3 | // CardStack
4 | //
5 | // Created by Niels Hoogendoorn on 08/02/2020.
6 | // Copyright © 2020 Nihoo. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | enum NavigationHelper {
12 | static func allowedToMove(direction: HorizontalDirection, items: [Any], currentIndex: Int) -> Bool {
13 | guard listIsNotEmpty(items) else { return false }
14 | switch direction {
15 | case .forward:
16 | return canMoveForward(items: items, currentIndex: currentIndex)
17 | case .backward:
18 | return canMoveBackward(currentIndex: currentIndex)
19 | case .none:
20 | return false
21 | }
22 | }
23 |
24 | static func canMoveForward(items: [Any], currentIndex: Int) -> Bool {
25 | currentIndex + 1 < items.count
26 | }
27 |
28 | static func canMoveBackward(currentIndex: Int) -> Bool {
29 | currentIndex - 1 >= .zero
30 | }
31 |
32 | static func listIsNotEmpty(_ list: [Any]) -> Bool {
33 | list.indices.count > .zero
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/Sources/CardStack/Extensions/DragGesture.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DragGesture.swift
3 | // CardStack
4 | //
5 | // Created by Niels Hoogendoorn on 08/02/2020.
6 | // Copyright © 2020 Nihoo. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | extension DragGesture.Value {
12 | func getSwipeDirection(minimumDistance: CGFloat = .zero) -> HorizontalDirection {
13 | if isSwipingForward(minimumDistance: minimumDistance) {
14 | return .forward
15 | } else if isSwipingBackward(minimumDistance: minimumDistance) {
16 | return .backward
17 | } else {
18 | return .none
19 | }
20 | }
21 |
22 | func isSwipingForward(minimumDistance: CGFloat = .zero) -> Bool {
23 | let distance = self.startLocation.x - self.location.x
24 | return distance > minimumDistance
25 | }
26 |
27 | func isSwipingBackward(minimumDistance: CGFloat = .zero) -> Bool {
28 | let distance = self.location.x - self.startLocation.x
29 | return distance > minimumDistance
30 | }
31 |
32 | func horizontalDistance() -> CGFloat {
33 | return self.startLocation.x - self.location.x
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.1
2 | // The swift-tools-version declares the minimum version of Swift required to build this package.
3 |
4 | import PackageDescription
5 |
6 | let package = Package(
7 | name: "CardStack",
8 | platforms: [
9 | .iOS(.v13)
10 | ],
11 | products: [
12 | // Products define the executables and libraries produced by a package, and make them visible to other packages.
13 | .library(
14 | name: "CardStack",
15 | targets: ["CardStack"])
16 | ],
17 | dependencies: [
18 | // Dependencies declare other packages that this package depends on.
19 | // .package(url: /* package url */, from: "1.0.0"),
20 | ],
21 | targets: [
22 | // Targets are the basic building blocks of a package. A target can define a module or a test suite.
23 | // Targets can depend on other targets in this package, and on products in packages which this package depends on.
24 | .target(
25 | name: "CardStack",
26 | dependencies: []),
27 | .testTarget(
28 | name: "CardStackTests",
29 | dependencies: ["CardStack"])
30 | ]
31 | )
32 |
--------------------------------------------------------------------------------
/Sources/CardStack/CardNavigation/CardNavigation.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CardNavigation.swift
3 | // CardStack
4 | //
5 | // Created by Niels Hoogendoorn on 08/02/2020.
6 | // Copyright © 2020 Nihoo. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | open class CardNavigation: ObservableObject {
12 | @Published var currentIndex: Int = .zero
13 | @Published var items: [Data] = []
14 |
15 | var reversedList: [Data] {
16 | items.reversed()
17 | }
18 |
19 | init(items: [Data], startIndex: Int) {
20 | guard items.indices.contains(startIndex) else {
21 | assertionFailure("Start index out of bounds")
22 | return
23 | }
24 | self.items = items
25 | self.currentIndex = startIndex
26 | }
27 |
28 | func move(_ direction: HorizontalDirection) {
29 | let allowedToMove = NavigationHelper.allowedToMove(direction: direction,
30 | items: items,
31 | currentIndex: currentIndex)
32 | guard allowedToMove else { return }
33 | switch direction {
34 | case .forward:
35 | currentIndex += 1
36 | case .backward:
37 | currentIndex -= 1
38 | case .none:
39 | break
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/Sources/CardStack/Models/StackConfiguration.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SwipableStackConfiguration.swift
3 | // CardStack
4 | //
5 | // Created by Niels Hoogendoorn on 08/02/2020.
6 | // Copyright © 2020 Nihoo. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | open class StackConfiguration {
12 | public init(startIndex: Int = 0,
13 | minimumSwipingDistance: CGFloat = 0,
14 | numberOfCardsShown: Int = 3) {
15 | self.startIndex = startIndex
16 | self.minimumSwipingDistance = minimumSwipingDistance
17 | self.numberOfCardsShown = numberOfCardsShown
18 | }
19 |
20 | /// Sets the index the card list should start with. Setting it to 3, would show the card for index 3 for example when the View loads. Default value is `0`..
21 | var startIndex: Int = 0
22 |
23 | /// The minimum swiping distance before the dragging starts.
24 | var minimumSwipingDistance: CGFloat = 0
25 |
26 | /// The number of cards shown in the View at the same time.
27 | var numberOfCardsShown: Int = 3
28 |
29 | /// Set the style for stacking. Default is decreasing height, where every next card is 30 points smaller. The other option is .sameHeight.
30 | // This last option is not yet implemented.
31 | // var stackStyle: StackStyle = .decreasingHeight
32 |
33 | /// Access the default configuration
34 | static var shared = StackConfiguration()
35 | }
36 |
--------------------------------------------------------------------------------
/Tests/CardStackTests/NavigationHelperTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NavigationHelperTests.swift
3 | // TestTests
4 | //
5 | // Created by Niels Hoogendoorn on 12/03/2020.
6 | // Copyright © 2020 Nihoo. All rights reserved.
7 | //
8 |
9 | import XCTest
10 | @testable import CardStack
11 |
12 | class NavigationHelperTests: XCTestCase {
13 |
14 | func test_canMoveForward_currentIndexIsLowerThanTotalItems_returnTrue() {
15 | let items: [Int] = [1, 2, 3, 4]
16 | let currentIndex = 1
17 | XCTAssertTrue(NavigationHelper.canMoveForward(items: items, currentIndex: currentIndex))
18 | }
19 |
20 | func test_canMoveForward_currentIndexIsEqualToLastItemIndex_returnFalse() {
21 | let items: [Int] = [1, 2, 3, 4]
22 | let currentIndex = items.count - 1
23 | XCTAssertFalse(NavigationHelper.canMoveForward(items: items, currentIndex: currentIndex))
24 | }
25 |
26 | func test_canMoveBackward_currentIndexIsGreaterThanZero_returnTrue() {
27 | let currentIndex = 1
28 | XCTAssertTrue(NavigationHelper.canMoveBackward(currentIndex: currentIndex))
29 | }
30 |
31 | func test_canMoveBackward_currentIndexIsZero_returnFalse() {
32 | let currentIndex = 0
33 | XCTAssertFalse(NavigationHelper.canMoveBackward(currentIndex: currentIndex))
34 | }
35 |
36 | func test_allowedToMove_itemsIsEmpty_returnFalse() {
37 | XCTAssertFalse(NavigationHelper.allowedToMove(direction: .forward, items: [], currentIndex: 0))
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/CardStackExample/CardStackExample/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // CardStackExample
4 | //
5 | // Created by Niels Hoogendoorn on 15/03/2020.
6 | // Copyright © 2020 Nihoo. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | @UIApplicationMain
12 | class AppDelegate: UIResponder, UIApplicationDelegate {
13 |
14 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
15 | // Override point for customization after application launch.
16 | return true
17 | }
18 |
19 | // MARK: UISceneSession Lifecycle
20 |
21 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
22 | // Called when a new scene session is being created.
23 | // Use this method to select a configuration to create the new scene with.
24 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
25 | }
26 |
27 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) {
28 | // Called when the user discards a scene session.
29 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
30 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return.
31 | }
32 |
33 | }
34 |
--------------------------------------------------------------------------------
/CardStackExample/CardStackExample/Base.lproj/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/Tests/CardStackTests/CardViewHelperTests+getSmallestItemSize.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CardViewHelperTests+getSmallestItemSize.swift
3 | //
4 | //
5 | // Created by Niels Hoogendoorn on 13/04/2020.
6 | //
7 |
8 | import XCTest
9 | @testable import CardStack
10 |
11 | extension CardViewHelperTests {
12 | func test_getSmallestItemSize_cardSizeIs200ItemsShownIsMin1_return0() {
13 | let sut = CardViewHelper.getSmallestItemSize(cardSize: cardSize, itemsShown: -1)
14 | XCTAssertEqual(sut, .zero)
15 | }
16 |
17 | func test_getSmallestItemSize_cardSizeIs200ItemsShownIs0_return0() {
18 | let sut = CardViewHelper.getSmallestItemSize(cardSize: cardSize, itemsShown: 0)
19 | XCTAssertEqual(sut, .zero)
20 | }
21 |
22 | func test_getSmallestItemSize_cardSizeIs200ItemsShownIs1_return200MinCardDistancex2() {
23 | let sut = CardViewHelper.getSmallestItemSize(cardSize: cardSize, itemsShown: 1)
24 | let expectedResult = CGSize(width: cardSize.width - (.cardDistance * 2),
25 | height: cardSize.height - (.cardDistance * 2))
26 | XCTAssertEqual(sut, expectedResult)
27 | }
28 |
29 | func test_getSmallestItemSize_cardSizeIs200ItemsShownIs2_return200MinCardDistancex4() {
30 | let sut = CardViewHelper.getSmallestItemSize(cardSize: cardSize, itemsShown: 2)
31 | let expectedResult = CGSize(width: cardSize.width - (.cardDistance * 4),
32 | height: cardSize.height - (.cardDistance * 4))
33 | XCTAssertEqual(sut, expectedResult)
34 | }
35 | func test_getSmallestItemSize_cardSizeIs0ItemsShownIs0_return0() {
36 | let sut = CardViewHelper.getSmallestItemSize(cardSize: .zero, itemsShown: .zero)
37 | XCTAssertEqual(sut, .zero)
38 | }
39 | func test_getSmallestItemSize_cardSizeIs0ItemsShownIs1_return0() {
40 | let sut = CardViewHelper.getSmallestItemSize(cardSize: .zero, itemsShown: 1)
41 | XCTAssertEqual(sut, .zero)
42 | }
43 |
44 | }
45 |
--------------------------------------------------------------------------------
/CardStackExample/CardStackExample/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "iphone",
5 | "size" : "20x20",
6 | "scale" : "2x"
7 | },
8 | {
9 | "idiom" : "iphone",
10 | "size" : "20x20",
11 | "scale" : "3x"
12 | },
13 | {
14 | "idiom" : "iphone",
15 | "size" : "29x29",
16 | "scale" : "2x"
17 | },
18 | {
19 | "idiom" : "iphone",
20 | "size" : "29x29",
21 | "scale" : "3x"
22 | },
23 | {
24 | "idiom" : "iphone",
25 | "size" : "40x40",
26 | "scale" : "2x"
27 | },
28 | {
29 | "idiom" : "iphone",
30 | "size" : "40x40",
31 | "scale" : "3x"
32 | },
33 | {
34 | "idiom" : "iphone",
35 | "size" : "60x60",
36 | "scale" : "2x"
37 | },
38 | {
39 | "idiom" : "iphone",
40 | "size" : "60x60",
41 | "scale" : "3x"
42 | },
43 | {
44 | "idiom" : "ipad",
45 | "size" : "20x20",
46 | "scale" : "1x"
47 | },
48 | {
49 | "idiom" : "ipad",
50 | "size" : "20x20",
51 | "scale" : "2x"
52 | },
53 | {
54 | "idiom" : "ipad",
55 | "size" : "29x29",
56 | "scale" : "1x"
57 | },
58 | {
59 | "idiom" : "ipad",
60 | "size" : "29x29",
61 | "scale" : "2x"
62 | },
63 | {
64 | "idiom" : "ipad",
65 | "size" : "40x40",
66 | "scale" : "1x"
67 | },
68 | {
69 | "idiom" : "ipad",
70 | "size" : "40x40",
71 | "scale" : "2x"
72 | },
73 | {
74 | "idiom" : "ipad",
75 | "size" : "76x76",
76 | "scale" : "1x"
77 | },
78 | {
79 | "idiom" : "ipad",
80 | "size" : "76x76",
81 | "scale" : "2x"
82 | },
83 | {
84 | "idiom" : "ipad",
85 | "size" : "83.5x83.5",
86 | "scale" : "2x"
87 | },
88 | {
89 | "idiom" : "ios-marketing",
90 | "size" : "1024x1024",
91 | "scale" : "1x"
92 | }
93 | ],
94 | "info" : {
95 | "version" : 1,
96 | "author" : "xcode"
97 | }
98 | }
--------------------------------------------------------------------------------
/CardStackExample/CardStackExample/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 | LSRequiresIPhoneOS
22 |
23 | UIApplicationSceneManifest
24 |
25 | UIApplicationSupportsMultipleScenes
26 |
27 | UISceneConfigurations
28 |
29 | UIWindowSceneSessionRoleApplication
30 |
31 |
32 | UISceneConfigurationName
33 | Default Configuration
34 | UISceneDelegateClassName
35 | $(PRODUCT_MODULE_NAME).SceneDelegate
36 |
37 |
38 |
39 |
40 | UILaunchStoryboardName
41 | LaunchScreen
42 | UIRequiredDeviceCapabilities
43 |
44 | armv7
45 |
46 | UISupportedInterfaceOrientations
47 |
48 | UIInterfaceOrientationPortrait
49 | UIInterfaceOrientationLandscapeLeft
50 | UIInterfaceOrientationLandscapeRight
51 |
52 | UISupportedInterfaceOrientations~ipad
53 |
54 | UIInterfaceOrientationPortrait
55 | UIInterfaceOrientationPortraitUpsideDown
56 | UIInterfaceOrientationLandscapeLeft
57 | UIInterfaceOrientationLandscapeRight
58 |
59 |
60 |
61 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Xcode
2 | #
3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
4 |
5 | ## User settings
6 | xcuserdata/
7 |
8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
9 | *.xcscmblueprint
10 | *.xccheckout
11 |
12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
13 | build/
14 | DerivedData/
15 | *.moved-aside
16 | *.pbxuser
17 | !default.pbxuser
18 | *.mode1v3
19 | !default.mode1v3
20 | *.mode2v3
21 | !default.mode2v3
22 | *.perspectivev3
23 | !default.perspectivev3
24 |
25 | ## Obj-C/Swift specific
26 | *.hmap
27 |
28 | ## App packaging
29 | *.ipa
30 | *.dSYM.zip
31 | *.dSYM
32 |
33 | ## Playgrounds
34 | timeline.xctimeline
35 | playground.xcworkspace
36 |
37 | # Swift Package Manager
38 | #
39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
40 | # Packages/
41 | # Package.pins
42 | # Package.resolved
43 | # *.xcodeproj
44 | #
45 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
46 | # hence it is not needed unless you have added a package configuration file to your project
47 | # .swiftpm
48 |
49 | .build/
50 |
51 | # CocoaPods
52 | #
53 | # We recommend against adding the Pods directory to your .gitignore. However
54 | # you should judge for yourself, the pros and cons are mentioned at:
55 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
56 | #
57 | # Pods/
58 | #
59 | # Add this line if you want to avoid checking in source code from the Xcode workspace
60 | # *.xcworkspace
61 |
62 | # Carthage
63 | #
64 | # Add this line if you want to avoid checking in source code from Carthage dependencies.
65 | # Carthage/Checkouts
66 |
67 | Carthage/Build/
68 |
69 | # Accio dependency management
70 | Dependencies/
71 | .accio/
72 |
73 | # fastlane
74 | #
75 | # It is recommended to not store the screenshots in the git repo.
76 | # Instead, use fastlane to re-generate the screenshots whenever they are needed.
77 | # For more information about the recommended setup visit:
78 | # https://docs.fastlane.tools/best-practices/source-control/#source-control
79 |
80 | fastlane/report.xml
81 | fastlane/Preview.html
82 | fastlane/screenshots/**/*.png
83 | fastlane/test_output
84 |
85 | # Code Injection
86 | #
87 | # After new code Injection tools there's a generated folder /iOSInjectionProject
88 | # https://github.com/johnno1962/injectionforxcode
89 |
90 | iOSInjectionProject/
91 |
--------------------------------------------------------------------------------
/Sources/CardStack/Views/CardStackMainView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CardStackMainView.swift
3 | // CardStack
4 | //
5 | // Created by Niels Hoogendoorn on 08/02/2020.
6 | // Copyright © 2020 Nihoo. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | struct CardStackMainView: View {
12 | @EnvironmentObject var navigation: CardNavigation
13 | @State var dragValue: CGPoint = .zero
14 | var configuration: StackConfiguration?
15 |
16 | init(configuration: StackConfiguration? = nil) {
17 | self.configuration = configuration
18 | }
19 |
20 | var body: some View {
21 | ZStack(alignment: .trailing) {
22 | // Reverse the item list so the first item in the list
23 | // will show in front.
24 | ForEach(navigation.reversedList, id: \.id) { item in
25 | CardContentView(info: item,
26 | configuration: self.configuration ?? StackConfiguration.shared,
27 | dragValue: self.$dragValue)
28 | }
29 | }.frame(maxWidth: .infinity, maxHeight: .infinity)
30 | // We need to adjust the offset for the current index after flipping
31 | // the cards otherwise it will stay in the same position. We want to
32 | // have every list item on the place of the first index in the list.
33 | .offset(x: getOffsetForStackedView(), y: .zero)
34 | .gesture(DragGesture()
35 | .onChanged({ (value: DragGesture.Value) in
36 | self.handleHorizontalDragging(value)
37 | })
38 | .onEnded({ (value: DragGesture.Value) in
39 | self.handleDraggingEnd(value)
40 | })
41 | )
42 | }
43 |
44 | fileprivate func getOffsetForStackedView() -> CGFloat {
45 | return CardViewHelper.getListOffset(currentIndex: navigation.currentIndex)
46 | }
47 |
48 | fileprivate func handleHorizontalDragging(_ value: DragGesture.Value) {
49 | self.dragValue = CGPoint(x: value.horizontalDistance(), y: .zero)
50 | }
51 |
52 | fileprivate func handleDraggingEnd(_ value: DragGesture.Value) {
53 | self.handleSwipeDirection(value)
54 |
55 | // Reset dragvalue after moving so that the card is at its base position.
56 | self.dragValue = .zero
57 | }
58 |
59 | fileprivate func handleSwipeDirection(_ value: DragGesture.Value) {
60 | let minDistance = configuration?.minimumSwipingDistance ?? StackConfiguration.shared.minimumSwipingDistance
61 |
62 | let swipeDirection = value.getSwipeDirection(minimumDistance: minDistance)
63 | switch swipeDirection {
64 | case .forward:
65 | self.navigation.move(.forward)
66 | case .backward:
67 | self.navigation.move(.backward)
68 | case .none:
69 | break
70 | }
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/CardStackExample/CardStackExample/SceneDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SceneDelegate.swift
3 | // CardStackExample
4 | //
5 | // Created by Niels Hoogendoorn on 15/03/2020.
6 | // Copyright © 2020 Nihoo. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import SwiftUI
11 |
12 | class SceneDelegate: UIResponder, UIWindowSceneDelegate {
13 |
14 | var window: UIWindow?
15 |
16 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
17 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
18 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
19 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
20 |
21 | // Create the SwiftUI view that provides the window contents.
22 | let contentView = StackExampleView()
23 |
24 | // Use a UIHostingController as window root view controller.
25 | if let windowScene = scene as? UIWindowScene {
26 | let window = UIWindow(windowScene: windowScene)
27 | window.rootViewController = UIHostingController(rootView: contentView)
28 | self.window = window
29 | window.makeKeyAndVisible()
30 | }
31 | }
32 |
33 | func sceneDidDisconnect(_ scene: UIScene) {
34 | // Called as the scene is being released by the system.
35 | // This occurs shortly after the scene enters the background, or when its session is discarded.
36 | // Release any resources associated with this scene that can be re-created the next time the scene connects.
37 | // The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead).
38 | }
39 |
40 | func sceneDidBecomeActive(_ scene: UIScene) {
41 | // Called when the scene has moved from an inactive state to an active state.
42 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive.
43 | }
44 |
45 | func sceneWillResignActive(_ scene: UIScene) {
46 | // Called when the scene will move from an active state to an inactive state.
47 | // This may occur due to temporary interruptions (ex. an incoming phone call).
48 | }
49 |
50 | func sceneWillEnterForeground(_ scene: UIScene) {
51 | // Called as the scene transitions from the background to the foreground.
52 | // Use this method to undo the changes made on entering the background.
53 | }
54 |
55 | func sceneDidEnterBackground(_ scene: UIScene) {
56 | // Called as the scene transitions from the foreground to the background.
57 | // Use this method to save data, release shared resources, and store enough scene-specific state information
58 | // to restore the scene back to its current state.
59 | }
60 |
61 | }
62 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/xcshareddata/xcschemes/CardStack.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
29 |
35 |
36 |
37 |
38 |
39 |
44 |
45 |
47 |
53 |
54 |
55 |
56 |
57 |
67 |
68 |
74 |
75 |
81 |
82 |
83 |
84 |
86 |
87 |
90 |
91 |
92 |
--------------------------------------------------------------------------------
/Sources/CardStack/Views/CardContentView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CardContentView.swift
3 | // CardStack
4 | //
5 | // Created by Niels Hoogendoorn on 08/02/2020.
6 | // Copyright © 2020 Nihoo. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | struct CardContentView: View {
12 | @EnvironmentObject var navigation: CardNavigation
13 | let info: Data
14 | let configuration: StackConfiguration
15 |
16 | @State var didAppear: Bool = false
17 | @State var isActive: Bool = false
18 | @State var originalItemSize: CGSize = .zero
19 |
20 | var itemSize: CGSize {
21 | getItemSize(geoSize: originalItemSize)
22 | }
23 | @Binding var dragValue: CGPoint
24 |
25 | init(info: Data, configuration: StackConfiguration, dragValue: Binding) {
26 | self.info = info
27 | self.configuration = configuration
28 | self._dragValue = dragValue
29 | }
30 |
31 | var body: some View {
32 | ZStack {
33 | Content(data: info)
34 | .background(GeometryReader { geo in
35 | Color.clear
36 | .onAppear {
37 | self.originalItemSize = geo.size
38 | }
39 | }.frame(maxWidth: .infinity, maxHeight: .infinity))
40 |
41 | // Add animation to show correct scrolling speed.
42 | .scaleEffect(getScaleSize())
43 | .animation(.easeInOut(duration: didAppear ? 0.3 : .zero))
44 |
45 | // Base offset should always be set first, otherwise the
46 | // dragging offset will give an incorrect value.
47 | .offset(getCardBaseOffset())
48 | .offset(getCardDraggingOffset())
49 | .onAppear {
50 | self.didAppear = true
51 | }
52 | .onTapGesture {
53 | self.isActive.toggle()
54 | }
55 | }
56 | .frame(width: itemSize.width, height: itemSize.height)
57 | }
58 |
59 | func getScaleSize() -> CGSize {
60 | CardViewHelper.getCardScaleSize(cardsShown: configuration.numberOfCardsShown,
61 | currentIndex: navigation.currentIndex,
62 | items: navigation.items,
63 | item: info)
64 | }
65 |
66 | func getItemSize(geoSize: CGSize) -> CGSize {
67 | let size = CardViewHelper.getCardSize(cardSize: geoSize,
68 | cardsShown: configuration.numberOfCardsShown,
69 | currentIndex: navigation.currentIndex,
70 | items: navigation.items,
71 | item: info)
72 | return size
73 | }
74 |
75 | func getCardBaseOffset() -> CGSize {
76 | CardViewHelper.getCardBaseOffset(cardSize: itemSize,
77 | cardsShown: configuration.numberOfCardsShown,
78 | currentIndex: navigation.currentIndex,
79 | items: navigation.items,
80 | currentItem: info)
81 | }
82 |
83 | func getCardDraggingOffset() -> CGSize {
84 | let dragInfo = DragInformation(currentIndex: navigation.currentIndex,
85 | items: navigation.items,
86 | item: info,
87 | dragValue: dragValue)
88 | return CardViewHelper.getCardDraggingOffset(dragInfo: dragInfo)
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # CardStack
2 |
3 | 
4 | [](https://swift.org/package-manager)
5 | 
6 | [](https://github.com/nhoogendoorn/CardStack/releases)
7 | [](https://github.com/nhoogendoorn/CardStack/blob/master/LICENSE)
8 |
9 | > A SwiftUI package that lets you implement swipable cards in your project.
10 |
11 | 
12 |
13 | ## Requirements
14 |
15 | - iOS 13.0+
16 | - Xcode 11.0
17 |
18 | # Installation
19 |
20 | The preferred way of installing CardStack is via the [Swift Package Manager](https://swift.org/package-manager/).
21 |
22 | 1. In Xcode, open your project and navigate to **File** → **Swift Packages** → **Add Package Dependency...**
23 | 2. Paste the repository URL (`https://github.com/nhoogendoorn/CardStack`) and click **Next**.
24 | 3. For **Rules**, select **Version** (with `Up to Next Major`).
25 | 4. Click **Finish**.
26 |
27 | ## Usage example
28 |
29 | > For a full example see the example project.
30 |
31 | Initialize a CardStack view by passing a CardView and CardData. CardView is a protocol that inherits from View and CardData is a protocol that inherits from Identifiable.
32 |
33 | ### CardData
34 |
35 | The CardData object just needs to have an id, but can for the rest contain any type of data.
36 |
37 | ```swift
38 | import SwiftUI
39 | import CardStack
40 |
41 | struct DataExample: CardData {
42 | var id: String
43 | var color: Color
44 | }
45 | ```
46 |
47 | ### CardView
48 |
49 | The Card View will have access to the data you define as CardData.
50 |
51 | ```swift
52 | import SwiftUI
53 | import CardStack
54 |
55 | struct CardExampleView: Card {
56 | var data: DataExample?
57 |
58 | init(data: CardData) where CardData: CardData {
59 | if let infoData = data as? DataExample {
60 | self.data = infoData
61 | }
62 | }
63 |
64 | var body: some View {
65 | data?.color
66 | .frame(width: 200, height: 200)
67 | .shadow(color: Color.black.opacity(0.1), radius: 3, x: 0, y: 0)
68 | .cornerRadius(8)
69 | }
70 | }
71 | ```
72 |
73 | ### CardStackView
74 |
75 | The CardStack is the main container for all the cards.
76 |
77 | ```swift
78 | import SwiftUI
79 | import CardStack
80 |
81 | struct StackExampleView: View {
82 | let items: [DataExample] = [DataExample(id: UUID().uuidString, color: .red),
83 | DataExample(id: UUID().uuidString, color: .blue),
84 | DataExample(id: UUID().uuidString, color: .yellow),
85 | DataExample(id: UUID().uuidString, color: .green),
86 | DataExample(id: UUID().uuidString, color: .orange)
87 | ]
88 |
89 | let configuration = StackConfiguration()
90 |
91 | var body: some View {
92 | CardStack(configuration: nil, items: items)
93 | }
94 | }
95 | ```
96 |
97 | ### Configuration
98 |
99 | In the configuration file you can set the following parameters:
100 |
101 | ```
102 | /// Sets the index the card list should start with. Setting it to 3, would show the card for index 3 for example when the View loads. Default value is `0`..
103 | var startIndex: Int = 0
104 |
105 | /// The minimum swiping distance before the dragging starts.
106 | var minimumSwipingDistance: CGFloat = 0
107 |
108 | /// The number of cards shown in the View at the same time.
109 | var numberOfCardsShown: Int = 3
110 |
111 | /// Access the default configuration
112 | static var shared = StackConfiguration()
113 | ```
114 |
115 | ## Author
116 |
117 | Niels Hoogendoorn
118 |
119 | ## License
120 |
121 | CardStack is available under the MIT license. See the LICENSE file for more info.
122 |
--------------------------------------------------------------------------------
/Sources/CardStack/Helpers/CardViewHelper.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CardViewHelper.swift
3 | // CardStack
4 | //
5 | // Created by Niels Hoogendoorn on 08/02/2020.
6 | // Copyright © 2020 Nihoo. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | enum CardViewHelper {
12 | static func getSmallestItemSize(cardSize: CGSize, itemsShown: Int) -> CGSize {
13 | guard cardSize != .zero, itemsShown > .zero else { return .zero }
14 | let width: CGFloat = cardSize.width + (CGFloat(itemsShown) * -.cardDistance * 2)
15 | let height: CGFloat = cardSize.height + (CGFloat(itemsShown) * -.cardDistance * 2)
16 | return CGSize(width: width, height: height)
17 | }
18 |
19 | static func getCardSize(
20 | cardSize: CGSize,
21 | cardsShown: Int,
22 | currentIndex: Int,
23 | items: [Data],
24 | item: Data) -> CGSize {
25 | let index = items.firstIndex(where: { $0.id == item.id })
26 | guard let foundIndex = index else { return .zero }
27 | let smallestItemSize = getSmallestItemSize(cardSize: cardSize,
28 | itemsShown: cardsShown - 1)
29 |
30 | if shouldShowOffscreen(currentIndex: currentIndex, itemIndex: foundIndex) {
31 | return smallestItemSize
32 | }
33 |
34 | switch foundIndex {
35 | case getTheVisibleIndexRange(currentIndex: currentIndex, cardsShown: cardsShown):
36 |
37 | let indexDistance: CGFloat = CGFloat(foundIndex - currentIndex)
38 | let itemOffset = (indexDistance * -.cardDistance) * 2
39 | let width = cardSize.width + itemOffset
40 | let height = cardSize.height + itemOffset
41 | return CGSize(width: width, height: height)
42 | default:
43 | return smallestItemSize
44 | }
45 | }
46 |
47 | static func getCardBaseOffset(
48 | cardSize: CGSize,
49 | cardsShown: Int,
50 | currentIndex: Int,
51 | items: [Data],
52 | currentItem: Data) -> CGSize {
53 | guard
54 | let index = items.firstIndex(where: {
55 | $0.id == currentItem.id })
56 | else { return .zero }
57 |
58 | if shouldShowOffscreen(currentIndex: currentIndex, itemIndex: index) {
59 | return CGSize(width: -(.screenWidth + cardSize.width), height: .zero)
60 | }
61 |
62 | switch index {
63 | case getTheVisibleIndexRange(currentIndex: currentIndex, cardsShown: cardsShown):
64 | let offset = CGFloat(index) * .cardDistance
65 | return CGSize(width: offset, height: .zero)
66 | default:
67 | let lastShownIndex = getLastShownIndexFrom(currentIndex: currentIndex,
68 | cardsShown: cardsShown)
69 | let offset = CGFloat(lastShownIndex) * .cardDistance
70 | return CGSize(width: offset, height: .zero)
71 | }
72 | }
73 |
74 | static func shouldShowOffscreen(currentIndex: Int, itemIndex: Int) -> Bool {
75 | // If the index is lower than the current index it should move to the
76 | // left side of the screen so that it is invisible.
77 | itemIndex < currentIndex
78 | }
79 |
80 | static func getTheVisibleIndexRange(currentIndex: Int, cardsShown: Int) -> ClosedRange {
81 | let lastShownIndex = getLastShownIndexFrom(currentIndex: currentIndex, cardsShown: cardsShown) - 1
82 | return currentIndex...lastShownIndex
83 | }
84 |
85 | static func getLastShownIndexFrom(currentIndex: Int, cardsShown: Int) -> Int {
86 | currentIndex + cardsShown
87 | }
88 |
89 | static func getCardDraggingOffset(dragInfo: DragInformation) -> CGSize {
90 | guard
91 | dragInfo.items.indices.contains(dragInfo.currentIndex),
92 | dragInfo.items[dragInfo.currentIndex].id == dragInfo.item.id
93 | else { return .zero }
94 | return CGSize(width: -dragInfo.dragValue.x, height: 0)
95 | }
96 |
97 | static func getListOffset(currentIndex: Int) -> CGFloat {
98 | return CGFloat(currentIndex) * -.cardDistance
99 | }
100 |
101 | static func getCardScaleSize(
102 | cardsShown: Int,
103 | currentIndex: Int,
104 | items: [Data],
105 | item: Data) -> CGSize {
106 |
107 | let index = items.firstIndex(where: { $0.id == item.id })
108 | guard let foundIndex = index else { return .zero }
109 |
110 | switch foundIndex {
111 | case getTheVisibleIndexRange(currentIndex: currentIndex, cardsShown: cardsShown):
112 | let indexDistance: CGFloat = CGFloat(foundIndex - currentIndex)
113 | let factor: CGFloat = (.cardDistance / 100) * indexDistance
114 | return CGSize(width: 1 - factor, height: 1 - factor)
115 | default:
116 | let smallestFactor: CGFloat = (.cardDistance / 100) * CGFloat(cardsShown)
117 | return CGSize(width: 1 - smallestFactor, height: 1 - smallestFactor)
118 | }
119 | }
120 |
121 | }
122 |
--------------------------------------------------------------------------------
/Tests/CardStackTests/CardViewHelperTests+getCardSize.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CardViewHelperTests+getCardSize.swift
3 | //
4 | //
5 | // Created by Niels Hoogendoorn on 13/04/2020.
6 | //
7 |
8 | import XCTest
9 | @testable import CardStack
10 |
11 | extension CardViewHelperTests {
12 | func test_getCardSize_size200cardsShown3currentIndex0_itemIdx0_return200() {
13 | let sut = CardViewHelper.getCardSize(cardSize: cardSize,
14 | cardsShown: 3,
15 | currentIndex: 0,
16 | items: MockCardData.items,
17 | item: MockCardData.items[0])
18 | XCTAssertEqual(sut, cardSize)
19 | }
20 |
21 | func test_getCardSize_size200cardsShown3currentIndex1_itemIdx0_return200MinCardDistancex4() {
22 | let sut = CardViewHelper.getCardSize(cardSize: cardSize,
23 | cardsShown: 3,
24 | currentIndex: 1,
25 | items: MockCardData.items,
26 | item: MockCardData.items[0])
27 | let expectedResult = CGSize(width: cardSize.width - (.cardDistance * 4),
28 | height: cardSize.height - (.cardDistance * 4))
29 | XCTAssertEqual(sut, expectedResult)
30 | }
31 |
32 | func test_getCardSize_size200cardsShown3currentIndex1_itemIdx1_return200() {
33 | let sut = CardViewHelper.getCardSize(cardSize: cardSize,
34 | cardsShown: 3,
35 | currentIndex: 1,
36 | items: MockCardData.items,
37 | item: MockCardData.items[1])
38 | XCTAssertEqual(sut, cardSize)
39 | }
40 |
41 | func test_getCardSize_size200cardsShown3currentIndex1_itemIdx2_return200MinCardDistancex2() {
42 | let sut = CardViewHelper.getCardSize(cardSize: cardSize,
43 | cardsShown: 3,
44 | currentIndex: 1,
45 | items: MockCardData.items,
46 | item: MockCardData.items[2])
47 | let expectedResult = CGSize(width: 200 - (.cardDistance * 2),
48 | height: 200 - (.cardDistance * 2))
49 | XCTAssertEqual(sut, expectedResult)
50 | }
51 |
52 | func test_getCardSize_size200cardsShown3currentIndex1_itemIdx3_return200MinCardDistancex4() {
53 | let sut = CardViewHelper.getCardSize(cardSize: cardSize,
54 | cardsShown: 3,
55 | currentIndex: 1,
56 | items: MockCardData.items,
57 | item: MockCardData.items[3])
58 | let expectedResult = CGSize(width: 200 - (.cardDistance * 4),
59 | height: 200 - (.cardDistance * 4))
60 | XCTAssertEqual(sut, expectedResult)
61 | }
62 |
63 | func test_getCardSize_size200cardsShown3currentIndex2_itemIdx0_return200MinCardDistancex4() {
64 | let sut = CardViewHelper.getCardSize(cardSize: cardSize,
65 | cardsShown: 3,
66 | currentIndex: 2,
67 | items: MockCardData.items,
68 | item: MockCardData.items[0])
69 | let expectedResult = CGSize(width: 200 - (.cardDistance * 4),
70 | height: 200 - (.cardDistance * 4))
71 | XCTAssertEqual(sut, expectedResult)
72 | }
73 |
74 | func test_getCardSize_size200cardsShown3currentIndex2_itemIdx2_return200() {
75 | let sut = CardViewHelper.getCardSize(cardSize: cardSize,
76 | cardsShown: 3,
77 | currentIndex: 2,
78 | items: MockCardData.items,
79 | item: MockCardData.items[2])
80 | XCTAssertEqual(sut, cardSize)
81 | }
82 |
83 | func test_getCardSize_size200cardsShown3currentIndex2_itemIdx3_return200MinCardDistancex2() {
84 | let sut = CardViewHelper.getCardSize(cardSize: cardSize,
85 | cardsShown: 3,
86 | currentIndex: 2,
87 | items: MockCardData.items,
88 | item: MockCardData.items[3])
89 | let expectedResult = CGSize(width: 200 - (.cardDistance * 2),
90 | height: 200 - (.cardDistance * 2))
91 | XCTAssertEqual(sut, expectedResult)
92 | }
93 |
94 | func test_getCardSize_size200cardsShown2currentIndex2_itemIdx2_return200() {
95 | let sut = CardViewHelper.getCardSize(cardSize: cardSize,
96 | cardsShown: 2,
97 | currentIndex: 2,
98 | items: MockCardData.items,
99 | item: MockCardData.items[2])
100 | XCTAssertEqual(sut, cardSize)
101 | }
102 |
103 | func test_getCardSize_size200cardsShown2currentIndex0_itemIdx1_return200MinCardDistancex2() {
104 | let sut = CardViewHelper.getCardSize(cardSize: cardSize,
105 | cardsShown: 2,
106 | currentIndex: 0,
107 | items: MockCardData.items,
108 | item: MockCardData.items[1])
109 | let expectedResult = CGSize(width: 200 - (.cardDistance * 2),
110 | height: 200 - (.cardDistance * 2))
111 | XCTAssertEqual(sut, expectedResult)
112 | }
113 |
114 | func test_getCardSize_size200cardsShown2currentIndex0_itemIdx2_return200MinCardDistancex2() {
115 | let sut = CardViewHelper.getCardSize(cardSize: cardSize,
116 | cardsShown: 2,
117 | currentIndex: 0,
118 | items: MockCardData.items,
119 | item: MockCardData.items[2])
120 | let expectedResult = CGSize(width: 200 - (.cardDistance * 2),
121 | height: 200 - (.cardDistance * 2))
122 | XCTAssertEqual(sut, expectedResult)
123 | }
124 |
125 | }
126 |
--------------------------------------------------------------------------------
/Tests/CardStackTests/CardViewHelperTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CardViewHelperTests.swift
3 | // TestTests
4 | //
5 | // Created by Niels Hoogendoorn on 13/03/2020.
6 | // Copyright © 2020 Nihoo. All rights reserved.
7 | //
8 |
9 | import XCTest
10 | @testable import CardStack
11 |
12 | class CardViewHelperTests: XCTestCase {
13 | let cardSize = CGSize(width: 200, height: 200)
14 |
15 | func test_getCardBaseOffset_size200cardsShown3currentIndex0_itemIdx0_return0X0() {
16 | let sut = CardViewHelper.getCardBaseOffset(cardSize: cardSize,
17 | cardsShown: 3,
18 | currentIndex: 0,
19 | items: MockCardData.items,
20 | currentItem: MockCardData.items[0])
21 | XCTAssertEqual(sut, .zero)
22 | }
23 |
24 | func test_getCardBaseOffset_size200cardsShown3currentIndex1_itemIdx0_returnMinScreenwidthPlus200X0() {
25 | let sut = CardViewHelper.getCardBaseOffset(cardSize: cardSize,
26 | cardsShown: 3,
27 | currentIndex: 1,
28 | items: MockCardData.items,
29 | currentItem: MockCardData.items[0])
30 | let expectedResult = CGSize(width: -(.screenWidth + cardSize.width),
31 | height: .zero)
32 | XCTAssertEqual(sut, expectedResult)
33 | }
34 |
35 | func test_getCardBaseOffset_size200cardsShown3currentIndex2_itemIdx1_returnMinScreenwidthPlus200X0() {
36 | let sut = CardViewHelper.getCardBaseOffset(cardSize: cardSize,
37 | cardsShown: 3,
38 | currentIndex: 2,
39 | items: MockCardData.items,
40 | currentItem: MockCardData.items[1])
41 | let expectedResult = CGSize(width: -(.screenWidth + cardSize.width),
42 | height: .zero)
43 | XCTAssertEqual(sut, expectedResult)
44 | }
45 |
46 | func test_getCardBaseOffset_size200cardsShown3currentIndex0_itemIdx1_returnCardDistancex0() {
47 | let sut = CardViewHelper.getCardBaseOffset(cardSize: cardSize,
48 | cardsShown: 3,
49 | currentIndex: 0,
50 | items: MockCardData.items,
51 | currentItem: MockCardData.items[1])
52 | let expectedResult = CGSize(width: .cardDistance,
53 | height: .zero)
54 | XCTAssertEqual(sut, expectedResult)
55 | }
56 | func test_getCardBaseOffset_size200cardsShown3currentIndex0_itemIdx2_returnCardDistancex2X0() {
57 | let sut = CardViewHelper.getCardBaseOffset(cardSize: cardSize,
58 | cardsShown: 3,
59 | currentIndex: 0,
60 | items: MockCardData.items,
61 | currentItem: MockCardData.items[2])
62 | let expectedResult = CGSize(width: .cardDistance * 2,
63 | height: .zero)
64 | XCTAssertEqual(sut, expectedResult)
65 | }
66 |
67 | func test_shouldShowOffscreen_curIndexIs0ItemIndexIs1_returnFalse() {
68 | let sut = CardViewHelper.shouldShowOffscreen(currentIndex: 0, itemIndex: 1)
69 | XCTAssertFalse(sut)
70 | }
71 |
72 | func test_shouldShowOffscreen_curIndexIs1ItemIndexIs1_returnFalse() {
73 | let sut = CardViewHelper.shouldShowOffscreen(currentIndex: 1, itemIndex: 1)
74 | XCTAssertFalse(sut)
75 | }
76 |
77 | func test_shouldShowOffscreen_curIndexIs1ItemIndexIs0_returnTrue() {
78 | let sut = CardViewHelper.shouldShowOffscreen(currentIndex: 1, itemIndex: 0)
79 | XCTAssertTrue(sut)
80 | }
81 |
82 | func test_getTheVisibleIndexRange_curIndexIs0cardsShownIs3_return0Range2() {
83 | let sut = CardViewHelper.getTheVisibleIndexRange(currentIndex: 0, cardsShown: 3)
84 | let expectedResult = 0...2
85 | XCTAssertEqual(sut, expectedResult)
86 | }
87 |
88 | func test_getTheVisibleIndexRange_curIndexIs1cardsShownIs3_return1Range3() {
89 | let sut = CardViewHelper.getTheVisibleIndexRange(currentIndex: 1, cardsShown: 3)
90 | let expectedResult = 1...3
91 | XCTAssertEqual(sut, expectedResult)
92 | }
93 |
94 | func test_getLastShownIndexFrom_curIndexIs0cardsShownIs3_return3() {
95 | let sut = CardViewHelper.getLastShownIndexFrom(currentIndex: 0, cardsShown: 3)
96 | XCTAssertEqual(sut, 3)
97 | }
98 |
99 | func test_getLastShownIndexFrom_curIndexIs1cardsShownIs3_return4() {
100 | let sut = CardViewHelper.getLastShownIndexFrom(currentIndex: 1, cardsShown: 3)
101 | XCTAssertEqual(sut, 4)
102 | }
103 |
104 | func test_getCardDraggingOffset_cardsIdxsDoesNotContainCurrentIndex_return0X0() {
105 | let dragInfo = DragInformation(currentIndex: MockCardData.items.count,
106 | items: MockCardData.items,
107 | item: MockCardData.items[0],
108 | dragValue: .zero)
109 | let sut = CardViewHelper.getCardDraggingOffset(dragInfo: dragInfo)
110 | XCTAssertEqual(sut, .zero)
111 | }
112 |
113 | func test_getCardDraggingOffset_itemIsNotCardForCurIdx_return0X0() {
114 | let dragInfo = DragInformation(currentIndex: 1,
115 | items: MockCardData.items,
116 | item: MockCardData.items[0],
117 | dragValue: .zero)
118 | let sut = CardViewHelper.getCardDraggingOffset(dragInfo: dragInfo)
119 | XCTAssertEqual(sut, .zero)
120 | }
121 |
122 | func test_getCardDraggingOffset_itemIsCardForCurIdxDragValXIsMin20_returnMin20X0() {
123 | let dragInfo = DragInformation(currentIndex: 0,
124 | items: MockCardData.items,
125 | item: MockCardData.items[0],
126 | dragValue: CGPoint(x: 20, y: 0))
127 | let sut = CardViewHelper.getCardDraggingOffset(dragInfo: dragInfo)
128 | XCTAssertEqual(sut, CGSize(width: -20, height: 0))
129 | }
130 |
131 | func test_getListOffset_curIdxIs0_return0xMinCardOffsetInStack() {
132 | let sut = CardViewHelper.getListOffset(currentIndex: 0)
133 | XCTAssertEqual(sut, 0 * -.cardDistance)
134 | }
135 |
136 | func test_getListOffset_curIdxIs1_return1xMinCardOffsetInStack() {
137 | let sut = CardViewHelper.getListOffset(currentIndex: 1)
138 | XCTAssertEqual(sut, 1 * -.cardDistance)
139 | }
140 |
141 | func test_getListOffset_curIdxIs2_return2xMinCardOffsetInStack() {
142 | let sut = CardViewHelper.getListOffset(currentIndex: 2)
143 | XCTAssertEqual(sut, 2 * -.cardDistance)
144 | }
145 |
146 | func test_getCardScaleSize_itemIdxIs0CurIdxIs0CardsShownIs3_return1() {
147 | let sut = CardViewHelper.getCardScaleSize(cardsShown: 3,
148 | currentIndex: 0,
149 | items: MockCardData.items,
150 | item: MockCardData.items[0])
151 | XCTAssertEqual(sut, CGSize(width: 1, height: 1))
152 | }
153 |
154 | func test_getCardScaleSize_itemIdxIs1CurIdxIs0CardsShownIs3_return1MinCardDistanceDiv100() {
155 | let sut = CardViewHelper.getCardScaleSize(cardsShown: 3,
156 | currentIndex: 0,
157 | items: MockCardData.items,
158 | item: MockCardData.items[1])
159 | XCTAssertEqual(sut, CGSize(width: 1 - CGFloat.cardDistance / 100,
160 | height: 1 - CGFloat.cardDistance / 100))
161 | }
162 | func test_getCardScaleSize_itemIdxIs2CurIdxIs0CardsShownIs3_return1MinCardDistanceDiv100xCardsShownMinOne() {
163 | let currentIndex = 0
164 | let itemIndex = 2
165 | let itemDistance = itemIndex - currentIndex
166 | let sut = CardViewHelper.getCardScaleSize(cardsShown: 3,
167 | currentIndex: currentIndex,
168 | items: MockCardData.items,
169 | item: MockCardData.items[itemIndex])
170 | let factor = (CGFloat.cardDistance / 100) * CGFloat(itemDistance)
171 | let expectedResult = CGSize(width: 1 - factor,
172 | height: 1 - factor)
173 | XCTAssertEqual(sut, expectedResult)
174 | }
175 |
176 | func test_getCardScaleSize_itemIdxIs3CurIdxIs0CardsShownIs3_return1MinCardDistanceDiv100xCardsShownMinOne() {
177 | let currentIndex = 0
178 | let itemIndex = 3
179 | let itemDistance = itemIndex - currentIndex
180 | let sut = CardViewHelper.getCardScaleSize(cardsShown: 3,
181 | currentIndex: currentIndex,
182 | items: MockCardData.items,
183 | item: MockCardData.items[itemIndex])
184 | let factor = (CGFloat.cardDistance / 100) * CGFloat(itemDistance)
185 | let expectedResult = CGSize(width: 1 - factor,
186 | height: 1 - factor)
187 | XCTAssertEqual(sut, expectedResult)
188 | }
189 |
190 | func test_getCardScaleSize_itemIdxIs2CurIdxIs0CardsShownIs2_return1MinCardDistanceDiv100xCardsShownMinOne() {
191 | let currentIndex = 0
192 | let itemIndex = 2
193 | let itemDistance = itemIndex - currentIndex
194 | let sut = CardViewHelper.getCardScaleSize(cardsShown: 2,
195 | currentIndex: currentIndex,
196 | items: MockCardData.items,
197 | item: MockCardData.items[itemIndex])
198 | let factor = (CGFloat.cardDistance / 100) * CGFloat(itemDistance)
199 | let expectedResult = CGSize(width: 1 - factor,
200 | height: 1 - factor)
201 | XCTAssertEqual(sut, expectedResult)
202 | XCTAssertEqual(sut, expectedResult)
203 | }
204 |
205 | func test_getCardScaleSize_itemIdxIs2CurIdxIs1CardsShownIs2_return1MinCardDistanceDiv100() {
206 | let cardsShown = 2
207 | let sut = CardViewHelper.getCardScaleSize(cardsShown: cardsShown,
208 | currentIndex: 1,
209 | items: MockCardData.items,
210 | item: MockCardData.items[2])
211 | let expectedResult = CGSize(width: 1 - CGFloat.cardDistance / 100,
212 | height: 1 - CGFloat.cardDistance / 100)
213 | XCTAssertEqual(sut, expectedResult)
214 | }
215 |
216 | func test_getCardScaleSize_itemIdxIs2CurIdxIs2CardsShownIs2_return1() {
217 | let cardsShown = 2
218 | let sut = CardViewHelper.getCardScaleSize(cardsShown: cardsShown,
219 | currentIndex: 2,
220 | items: MockCardData.items,
221 | item: MockCardData.items[2])
222 | let expectedResult = CGSize(width: 1, height: 1)
223 | XCTAssertEqual(sut, expectedResult)
224 | }
225 |
226 | }
227 |
--------------------------------------------------------------------------------
/CardStackExample/CardStackExample.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 52;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | A6A97FBE24339C5100FE5A2F /* CardStack in Frameworks */ = {isa = PBXBuildFile; productRef = A6A97FBD24339C5100FE5A2F /* CardStack */; };
11 | A6F9714C241E639900374CF8 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6F9714B241E639900374CF8 /* AppDelegate.swift */; };
12 | A6F9714E241E639900374CF8 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6F9714D241E639900374CF8 /* SceneDelegate.swift */; };
13 | A6F97152241E639E00374CF8 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A6F97151241E639E00374CF8 /* Assets.xcassets */; };
14 | A6F97155241E639E00374CF8 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A6F97154241E639E00374CF8 /* Preview Assets.xcassets */; };
15 | A6F97158241E639E00374CF8 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = A6F97156241E639E00374CF8 /* LaunchScreen.storyboard */; };
16 | A6F97165241E65B000374CF8 /* DataExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6F97164241E65B000374CF8 /* DataExample.swift */; };
17 | A6F97167241E65D000374CF8 /* CardExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6F97166241E65D000374CF8 /* CardExampleView.swift */; };
18 | A6F97169241E65E200374CF8 /* StackExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6F97168241E65E200374CF8 /* StackExampleView.swift */; };
19 | /* End PBXBuildFile section */
20 |
21 | /* Begin PBXFileReference section */
22 | A6F97148241E639900374CF8 /* CardStackExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = CardStackExample.app; sourceTree = BUILT_PRODUCTS_DIR; };
23 | A6F9714B241E639900374CF8 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
24 | A6F9714D241E639900374CF8 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; };
25 | A6F97151241E639E00374CF8 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
26 | A6F97154241E639E00374CF8 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; };
27 | A6F97157241E639E00374CF8 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; };
28 | A6F97159241E639E00374CF8 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
29 | A6F97164241E65B000374CF8 /* DataExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataExample.swift; sourceTree = ""; };
30 | A6F97166241E65D000374CF8 /* CardExampleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardExampleView.swift; sourceTree = ""; };
31 | A6F97168241E65E200374CF8 /* StackExampleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StackExampleView.swift; sourceTree = ""; };
32 | /* End PBXFileReference section */
33 |
34 | /* Begin PBXFrameworksBuildPhase section */
35 | A6F97145241E639900374CF8 /* Frameworks */ = {
36 | isa = PBXFrameworksBuildPhase;
37 | buildActionMask = 2147483647;
38 | files = (
39 | A6A97FBE24339C5100FE5A2F /* CardStack in Frameworks */,
40 | );
41 | runOnlyForDeploymentPostprocessing = 0;
42 | };
43 | /* End PBXFrameworksBuildPhase section */
44 |
45 | /* Begin PBXGroup section */
46 | A6F9713F241E639900374CF8 = {
47 | isa = PBXGroup;
48 | children = (
49 | A6F9714A241E639900374CF8 /* CardStackExample */,
50 | A6F97149241E639900374CF8 /* Products */,
51 | );
52 | sourceTree = "";
53 | };
54 | A6F97149241E639900374CF8 /* Products */ = {
55 | isa = PBXGroup;
56 | children = (
57 | A6F97148241E639900374CF8 /* CardStackExample.app */,
58 | );
59 | name = Products;
60 | sourceTree = "";
61 | };
62 | A6F9714A241E639900374CF8 /* CardStackExample */ = {
63 | isa = PBXGroup;
64 | children = (
65 | A6F97163241E659300374CF8 /* Views */,
66 | A6F97162241E658D00374CF8 /* Models */,
67 | A6F9714B241E639900374CF8 /* AppDelegate.swift */,
68 | A6F9714D241E639900374CF8 /* SceneDelegate.swift */,
69 | A6F97151241E639E00374CF8 /* Assets.xcassets */,
70 | A6F97156241E639E00374CF8 /* LaunchScreen.storyboard */,
71 | A6F97159241E639E00374CF8 /* Info.plist */,
72 | A6F97153241E639E00374CF8 /* Preview Content */,
73 | );
74 | path = CardStackExample;
75 | sourceTree = "";
76 | };
77 | A6F97153241E639E00374CF8 /* Preview Content */ = {
78 | isa = PBXGroup;
79 | children = (
80 | A6F97154241E639E00374CF8 /* Preview Assets.xcassets */,
81 | );
82 | path = "Preview Content";
83 | sourceTree = "";
84 | };
85 | A6F97162241E658D00374CF8 /* Models */ = {
86 | isa = PBXGroup;
87 | children = (
88 | A6F97164241E65B000374CF8 /* DataExample.swift */,
89 | );
90 | path = Models;
91 | sourceTree = "";
92 | };
93 | A6F97163241E659300374CF8 /* Views */ = {
94 | isa = PBXGroup;
95 | children = (
96 | A6F97166241E65D000374CF8 /* CardExampleView.swift */,
97 | A6F97168241E65E200374CF8 /* StackExampleView.swift */,
98 | );
99 | path = Views;
100 | sourceTree = "";
101 | };
102 | /* End PBXGroup section */
103 |
104 | /* Begin PBXNativeTarget section */
105 | A6F97147241E639900374CF8 /* CardStackExample */ = {
106 | isa = PBXNativeTarget;
107 | buildConfigurationList = A6F9715C241E639E00374CF8 /* Build configuration list for PBXNativeTarget "CardStackExample" */;
108 | buildPhases = (
109 | A6F97144241E639900374CF8 /* Sources */,
110 | A6F97145241E639900374CF8 /* Frameworks */,
111 | A6F97146241E639900374CF8 /* Resources */,
112 | A6A97FBF2433F44900FE5A2F /* Lint */,
113 | );
114 | buildRules = (
115 | );
116 | dependencies = (
117 | );
118 | name = CardStackExample;
119 | packageProductDependencies = (
120 | A6A97FBD24339C5100FE5A2F /* CardStack */,
121 | );
122 | productName = CardStackExample;
123 | productReference = A6F97148241E639900374CF8 /* CardStackExample.app */;
124 | productType = "com.apple.product-type.application";
125 | };
126 | /* End PBXNativeTarget section */
127 |
128 | /* Begin PBXProject section */
129 | A6F97140241E639900374CF8 /* Project object */ = {
130 | isa = PBXProject;
131 | attributes = {
132 | LastSwiftUpdateCheck = 1130;
133 | LastUpgradeCheck = 1130;
134 | ORGANIZATIONNAME = Nihoo;
135 | TargetAttributes = {
136 | A6F97147241E639900374CF8 = {
137 | CreatedOnToolsVersion = 11.3.1;
138 | };
139 | };
140 | };
141 | buildConfigurationList = A6F97143241E639900374CF8 /* Build configuration list for PBXProject "CardStackExample" */;
142 | compatibilityVersion = "Xcode 9.3";
143 | developmentRegion = en;
144 | hasScannedForEncodings = 0;
145 | knownRegions = (
146 | en,
147 | Base,
148 | );
149 | mainGroup = A6F9713F241E639900374CF8;
150 | packageReferences = (
151 | A6A97FBC24339C5100FE5A2F /* XCRemoteSwiftPackageReference "CardStack" */,
152 | );
153 | productRefGroup = A6F97149241E639900374CF8 /* Products */;
154 | projectDirPath = "";
155 | projectRoot = "";
156 | targets = (
157 | A6F97147241E639900374CF8 /* CardStackExample */,
158 | );
159 | };
160 | /* End PBXProject section */
161 |
162 | /* Begin PBXResourcesBuildPhase section */
163 | A6F97146241E639900374CF8 /* Resources */ = {
164 | isa = PBXResourcesBuildPhase;
165 | buildActionMask = 2147483647;
166 | files = (
167 | A6F97158241E639E00374CF8 /* LaunchScreen.storyboard in Resources */,
168 | A6F97155241E639E00374CF8 /* Preview Assets.xcassets in Resources */,
169 | A6F97152241E639E00374CF8 /* Assets.xcassets in Resources */,
170 | );
171 | runOnlyForDeploymentPostprocessing = 0;
172 | };
173 | /* End PBXResourcesBuildPhase section */
174 |
175 | /* Begin PBXShellScriptBuildPhase section */
176 | A6A97FBF2433F44900FE5A2F /* Lint */ = {
177 | isa = PBXShellScriptBuildPhase;
178 | buildActionMask = 2147483647;
179 | files = (
180 | );
181 | inputFileListPaths = (
182 | );
183 | inputPaths = (
184 | );
185 | name = Lint;
186 | outputFileListPaths = (
187 | );
188 | outputPaths = (
189 | );
190 | runOnlyForDeploymentPostprocessing = 0;
191 | shellPath = /bin/sh;
192 | shellScript = "# Type a script or drag a script file from your workScripts/lint.sh\n../Scripts/lint.sh\n";
193 | };
194 | /* End PBXShellScriptBuildPhase section */
195 |
196 | /* Begin PBXSourcesBuildPhase section */
197 | A6F97144241E639900374CF8 /* Sources */ = {
198 | isa = PBXSourcesBuildPhase;
199 | buildActionMask = 2147483647;
200 | files = (
201 | A6F9714C241E639900374CF8 /* AppDelegate.swift in Sources */,
202 | A6F9714E241E639900374CF8 /* SceneDelegate.swift in Sources */,
203 | A6F97165241E65B000374CF8 /* DataExample.swift in Sources */,
204 | A6F97167241E65D000374CF8 /* CardExampleView.swift in Sources */,
205 | A6F97169241E65E200374CF8 /* StackExampleView.swift in Sources */,
206 | );
207 | runOnlyForDeploymentPostprocessing = 0;
208 | };
209 | /* End PBXSourcesBuildPhase section */
210 |
211 | /* Begin PBXVariantGroup section */
212 | A6F97156241E639E00374CF8 /* LaunchScreen.storyboard */ = {
213 | isa = PBXVariantGroup;
214 | children = (
215 | A6F97157241E639E00374CF8 /* Base */,
216 | );
217 | name = LaunchScreen.storyboard;
218 | sourceTree = "";
219 | };
220 | /* End PBXVariantGroup section */
221 |
222 | /* Begin XCBuildConfiguration section */
223 | A6F9715A241E639E00374CF8 /* Debug */ = {
224 | isa = XCBuildConfiguration;
225 | buildSettings = {
226 | ALWAYS_SEARCH_USER_PATHS = NO;
227 | CLANG_ANALYZER_NONNULL = YES;
228 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
229 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
230 | CLANG_CXX_LIBRARY = "libc++";
231 | CLANG_ENABLE_MODULES = YES;
232 | CLANG_ENABLE_OBJC_ARC = YES;
233 | CLANG_ENABLE_OBJC_WEAK = YES;
234 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
235 | CLANG_WARN_BOOL_CONVERSION = YES;
236 | CLANG_WARN_COMMA = YES;
237 | CLANG_WARN_CONSTANT_CONVERSION = YES;
238 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
239 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
240 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
241 | CLANG_WARN_EMPTY_BODY = YES;
242 | CLANG_WARN_ENUM_CONVERSION = YES;
243 | CLANG_WARN_INFINITE_RECURSION = YES;
244 | CLANG_WARN_INT_CONVERSION = YES;
245 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
246 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
247 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
248 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
249 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
250 | CLANG_WARN_STRICT_PROTOTYPES = YES;
251 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
252 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
253 | CLANG_WARN_UNREACHABLE_CODE = YES;
254 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
255 | COPY_PHASE_STRIP = NO;
256 | DEBUG_INFORMATION_FORMAT = dwarf;
257 | ENABLE_STRICT_OBJC_MSGSEND = YES;
258 | ENABLE_TESTABILITY = YES;
259 | GCC_C_LANGUAGE_STANDARD = gnu11;
260 | GCC_DYNAMIC_NO_PIC = NO;
261 | GCC_NO_COMMON_BLOCKS = YES;
262 | GCC_OPTIMIZATION_LEVEL = 0;
263 | GCC_PREPROCESSOR_DEFINITIONS = (
264 | "DEBUG=1",
265 | "$(inherited)",
266 | );
267 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
268 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
269 | GCC_WARN_UNDECLARED_SELECTOR = YES;
270 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
271 | GCC_WARN_UNUSED_FUNCTION = YES;
272 | GCC_WARN_UNUSED_VARIABLE = YES;
273 | IPHONEOS_DEPLOYMENT_TARGET = 13.2;
274 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
275 | MTL_FAST_MATH = YES;
276 | ONLY_ACTIVE_ARCH = YES;
277 | SDKROOT = iphoneos;
278 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
279 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
280 | };
281 | name = Debug;
282 | };
283 | A6F9715B241E639E00374CF8 /* Release */ = {
284 | isa = XCBuildConfiguration;
285 | buildSettings = {
286 | ALWAYS_SEARCH_USER_PATHS = NO;
287 | CLANG_ANALYZER_NONNULL = YES;
288 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
289 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
290 | CLANG_CXX_LIBRARY = "libc++";
291 | CLANG_ENABLE_MODULES = YES;
292 | CLANG_ENABLE_OBJC_ARC = YES;
293 | CLANG_ENABLE_OBJC_WEAK = YES;
294 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
295 | CLANG_WARN_BOOL_CONVERSION = YES;
296 | CLANG_WARN_COMMA = YES;
297 | CLANG_WARN_CONSTANT_CONVERSION = YES;
298 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
299 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
300 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
301 | CLANG_WARN_EMPTY_BODY = YES;
302 | CLANG_WARN_ENUM_CONVERSION = YES;
303 | CLANG_WARN_INFINITE_RECURSION = YES;
304 | CLANG_WARN_INT_CONVERSION = YES;
305 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
306 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
307 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
308 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
309 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
310 | CLANG_WARN_STRICT_PROTOTYPES = YES;
311 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
312 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
313 | CLANG_WARN_UNREACHABLE_CODE = YES;
314 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
315 | COPY_PHASE_STRIP = NO;
316 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
317 | ENABLE_NS_ASSERTIONS = NO;
318 | ENABLE_STRICT_OBJC_MSGSEND = YES;
319 | GCC_C_LANGUAGE_STANDARD = gnu11;
320 | GCC_NO_COMMON_BLOCKS = YES;
321 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
322 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
323 | GCC_WARN_UNDECLARED_SELECTOR = YES;
324 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
325 | GCC_WARN_UNUSED_FUNCTION = YES;
326 | GCC_WARN_UNUSED_VARIABLE = YES;
327 | IPHONEOS_DEPLOYMENT_TARGET = 13.2;
328 | MTL_ENABLE_DEBUG_INFO = NO;
329 | MTL_FAST_MATH = YES;
330 | SDKROOT = iphoneos;
331 | SWIFT_COMPILATION_MODE = wholemodule;
332 | SWIFT_OPTIMIZATION_LEVEL = "-O";
333 | VALIDATE_PRODUCT = YES;
334 | };
335 | name = Release;
336 | };
337 | A6F9715D241E639E00374CF8 /* Debug */ = {
338 | isa = XCBuildConfiguration;
339 | buildSettings = {
340 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
341 | CODE_SIGN_STYLE = Automatic;
342 | DEVELOPMENT_ASSET_PATHS = "\"CardStackExample/Preview Content\"";
343 | DEVELOPMENT_TEAM = 2G429TK35T;
344 | ENABLE_PREVIEWS = YES;
345 | INFOPLIST_FILE = CardStackExample/Info.plist;
346 | LD_RUNPATH_SEARCH_PATHS = (
347 | "$(inherited)",
348 | "@executable_path/Frameworks",
349 | );
350 | PRODUCT_BUNDLE_IDENTIFIER = nihoo.CardStackExample;
351 | PRODUCT_NAME = "$(TARGET_NAME)";
352 | SWIFT_VERSION = 5.0;
353 | TARGETED_DEVICE_FAMILY = "1,2";
354 | };
355 | name = Debug;
356 | };
357 | A6F9715E241E639E00374CF8 /* Release */ = {
358 | isa = XCBuildConfiguration;
359 | buildSettings = {
360 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
361 | CODE_SIGN_STYLE = Automatic;
362 | DEVELOPMENT_ASSET_PATHS = "\"CardStackExample/Preview Content\"";
363 | DEVELOPMENT_TEAM = 2G429TK35T;
364 | ENABLE_PREVIEWS = YES;
365 | INFOPLIST_FILE = CardStackExample/Info.plist;
366 | LD_RUNPATH_SEARCH_PATHS = (
367 | "$(inherited)",
368 | "@executable_path/Frameworks",
369 | );
370 | PRODUCT_BUNDLE_IDENTIFIER = nihoo.CardStackExample;
371 | PRODUCT_NAME = "$(TARGET_NAME)";
372 | SWIFT_VERSION = 5.0;
373 | TARGETED_DEVICE_FAMILY = "1,2";
374 | };
375 | name = Release;
376 | };
377 | /* End XCBuildConfiguration section */
378 |
379 | /* Begin XCConfigurationList section */
380 | A6F97143241E639900374CF8 /* Build configuration list for PBXProject "CardStackExample" */ = {
381 | isa = XCConfigurationList;
382 | buildConfigurations = (
383 | A6F9715A241E639E00374CF8 /* Debug */,
384 | A6F9715B241E639E00374CF8 /* Release */,
385 | );
386 | defaultConfigurationIsVisible = 0;
387 | defaultConfigurationName = Release;
388 | };
389 | A6F9715C241E639E00374CF8 /* Build configuration list for PBXNativeTarget "CardStackExample" */ = {
390 | isa = XCConfigurationList;
391 | buildConfigurations = (
392 | A6F9715D241E639E00374CF8 /* Debug */,
393 | A6F9715E241E639E00374CF8 /* Release */,
394 | );
395 | defaultConfigurationIsVisible = 0;
396 | defaultConfigurationName = Release;
397 | };
398 | /* End XCConfigurationList section */
399 |
400 | /* Begin XCRemoteSwiftPackageReference section */
401 | A6A97FBC24339C5100FE5A2F /* XCRemoteSwiftPackageReference "CardStack" */ = {
402 | isa = XCRemoteSwiftPackageReference;
403 | repositoryURL = "https://github.com/nhoogendoorn/CardStack";
404 | requirement = {
405 | kind = upToNextMajorVersion;
406 | minimumVersion = 0.1.0;
407 | };
408 | };
409 | /* End XCRemoteSwiftPackageReference section */
410 |
411 | /* Begin XCSwiftPackageProductDependency section */
412 | A6A97FBD24339C5100FE5A2F /* CardStack */ = {
413 | isa = XCSwiftPackageProductDependency;
414 | package = A6A97FBC24339C5100FE5A2F /* XCRemoteSwiftPackageReference "CardStack" */;
415 | productName = CardStack;
416 | };
417 | /* End XCSwiftPackageProductDependency section */
418 | };
419 | rootObject = A6F97140241E639900374CF8 /* Project object */;
420 | }
421 |
--------------------------------------------------------------------------------