├── 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 | ![Swift 5.1](https://img.shields.io/badge/Swift-5.1-orange.svg?style=for-the-badge) 4 | [![SPM](https://img.shields.io/badge/spm-compatible-brightgreen.svg?style=for-the-badge)](https://swift.org/package-manager) 5 | ![Platforms](https://img.shields.io/badge/Platforms-iOS-blue.svg?style=for-the-badge) 6 | [![Git Version](https://img.shields.io/github/v/release/nhoogendoorn/CardStack.svg?style=for-the-badge)](https://github.com/nhoogendoorn/CardStack/releases) 7 | [![license](https://img.shields.io/github/license/nhoogendoorn/CardStack.svg?style=for-the-badge)](https://github.com/nhoogendoorn/CardStack/blob/master/LICENSE) 8 | 9 | > A SwiftUI package that lets you implement swipable cards in your project. 10 | 11 | ![CardStackExample](CardStackExample/CardStackExample.gif) 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 | --------------------------------------------------------------------------------