Skip to content

Commit 169d194

Browse files
committed
Add a deck shuffle animation
1 parent 1070972 commit 169d194

20 files changed

+211
-107
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<Bucket
3+
uuid = "9A6DE458-2DE5-4B9D-8388-0FFCB601AD10"
4+
type = "1"
5+
version = "2.0">
6+
</Bucket>

Demo/Shared/Buttons/RoundButton.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
// Demo
44
//
55
// Created by Daniel Saidi on 2022-11-29.
6-
// Copyright © 2022 Daniel Saidi. All rights reserved.
6+
// Copyright © 2022-2023 Daniel Saidi. All rights reserved.
77
//
88

99
import SwiftUI

Demo/Shared/ContentView.swift

+9-86
Original file line numberDiff line numberDiff line change
@@ -20,17 +20,8 @@ struct ContentView: View {
2020
@State
2121
var selectedHobby: Hobby?
2222

23-
@State
24-
var isShuffling = false
25-
26-
@State
27-
var shuffleDegrees = Self.getShuffleDegrees()
28-
29-
@State
30-
var shuffleOffsetsY = Self.getShuffleOffsets()
31-
32-
@State
33-
var shuffleOffsetsX = Self.getShuffleOffsets()
23+
@StateObject
24+
var animation = DeckShuffleAnimation()
3425

3526
var body: some View {
3627
NavigationView {
@@ -81,85 +72,17 @@ private extension ContentView {
8172
RoundButton(
8273
text: "Shuffle",
8374
image: "shuffle",
84-
action: shuffle
75+
action: { animation.shuffle($deck) }
8576
)
8677
}
8778

8879
func card(for hobby: Hobby) -> some View {
89-
let data = shuffleData(for: hobby)
90-
return HobbyCard(item: hobby)
91-
.rotationEffect(data.0)
92-
.offset(x: data.1, y: data.2)
93-
}
94-
}
95-
96-
private extension ContentView {
97-
98-
func shuffle() {
99-
withAnimation {
100-
isShuffling = true
101-
performAfterDelay(shuffleSecond)
102-
}
103-
}
104-
105-
func shuffleSecond() {
106-
withAnimation {
107-
isShuffling = false
108-
performAfterDelay(shuffleThird)
109-
}
110-
}
111-
112-
func shuffleThird() {
113-
withAnimation {
114-
randomizeShuffleData()
115-
isShuffling = true
116-
performAfterDelay(shuffleDeck)
117-
}
118-
}
119-
120-
func shuffleDeck() {
121-
deck.shuffle()
122-
performAfterDelay(endShuffle)
123-
}
124-
125-
func endShuffle() {
126-
withAnimation {
127-
isShuffling = false
128-
randomizeShuffleData()
129-
}
130-
}
131-
132-
func randomizeShuffleData() {
133-
shuffleDegrees = Self.getShuffleDegrees()
134-
shuffleOffsetsX = Self.getShuffleOffsets()
135-
shuffleOffsetsY = Self.getShuffleOffsets()
136-
}
137-
138-
func performAfterDelay(_ action: @escaping () -> Void) {
139-
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2, execute: action)
140-
}
141-
142-
func shuffleData(for hobby: Hobby) -> (Angle, Double, Double) {
143-
guard
144-
isShuffling,
145-
let index = deck.items.firstIndex(of: hobby)
146-
else { return (.zero, 0, 0) }
147-
let degrees = Angle.degrees(shuffleDegrees[index])
148-
let offsetX = shuffleOffsetsX[index]
149-
let offsetY = shuffleOffsetsY[index]
150-
return (degrees, offsetX, offsetY)
151-
}
152-
153-
static func getShuffleDegrees() -> [Double] {
154-
(0...100).map { _ in
155-
Double.random(in: -8...8)
156-
}
157-
}
158-
159-
static func getShuffleOffsets() -> [Double] {
160-
(0...100).map { _ in
161-
Double.random(in: -8...8)
162-
}
80+
HobbyCard(item: hobby)
81+
.withShuffleAnimation(
82+
animation,
83+
for: hobby,
84+
in: deck
85+
)
16386
}
16487
}
16588

Demo/Shared/Hobbies/Hobby.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
// DeckKitDemo
44
//
55
// Created by Daniel Saidi on 2020-09-22.
6-
// Copyright © 2020 Daniel Saidi. All rights reserved.
6+
// Copyright © 2020-2023 Daniel Saidi. All rights reserved.
77
//
88

99
import DeckKit

Demo/Shared/Hobbies/HobbyCard.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
// DeckKitDemo
44
//
55
// Created by Daniel Saidi on 2020-09-22.
6-
// Copyright © 2020 Daniel Saidi. All rights reserved.
6+
// Copyright © 2020-2023 Daniel Saidi. All rights reserved.
77
//
88

99
import SwiftUI

Demo/Shared/Hobbies/HobbyCardContent.swift

+2-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
// DeckKitDemo
44
//
55
// Created by Daniel Saidi on 2020-09-22.
6-
// Copyright © 2020 Daniel Saidi. All rights reserved.
6+
// Copyright © 2020-2023 Daniel Saidi. All rights reserved.
77
//
88

99
import SwiftUI
@@ -98,6 +98,7 @@ private extension HobbyCardContent {
9898
var footnote: some View {
9999
Text(inSheet ? "Swipe down to close" : "Swipe left for a new hobby, swipe right to select this one.")
100100
.font(.footnote)
101+
.fixedSize(horizontal: false, vertical: true)
101102
}
102103
}
103104

RELEASE_NOTES.md

+3-2
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,9 @@ Until then, deprecated features may be removed in the next minor version.
1010

1111
### 💡 New features
1212

13-
`Deck` has a new `shuffle` function.
14-
`DeckView` has a new convenience initializer.
13+
* `Deck` has a new `shuffle` function.
14+
* `DeckShuffleAnimation` is a new animation.
15+
* `DeckView` has a new convenience initializer.
1516

1617

1718

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
//
2+
// DeckShuffleAnimation.swift
3+
// DeckKit
4+
//
5+
// Created by Daniel Saidi on 2023-06-13.
6+
// Copyright © 2020-2023 Daniel Saidi. All rights reserved.
7+
//
8+
9+
import SwiftUI
10+
11+
/**
12+
This animation can be used to animate deck shuffling.
13+
14+
To use this animation, first create a `@StateObject` in the
15+
view that should use the animation, then bind the animation
16+
to your deck item views using the `withDeckShuffleAnimation`
17+
view modifier, then call `shuffle` shuffle the deck with an
18+
animation.
19+
*/
20+
public class DeckShuffleAnimation: ObservableObject {
21+
22+
/**
23+
Create a deck shuffle animation.
24+
25+
- Parameters:
26+
- maxDegrees: The max rotation to apply to the cards, by default `6`.
27+
- maxOffsetX: The max x offset to apply to the cards, by default `6`.
28+
- maxOffsetY: The max y offset to apply to the cards, by default `6`.
29+
*/
30+
public init(
31+
maxDegrees: Double = 6,
32+
maxOffsetX: Double = 6,
33+
maxOffsetY: Double = 6
34+
) {
35+
self.maxDegrees = maxDegrees
36+
self.maxOffsetX = maxOffsetX
37+
self.maxOffsetY = maxOffsetY
38+
}
39+
40+
/// The max rotation to apply to the cards.
41+
public let maxDegrees: Double
42+
43+
/// The max x offset to apply to the cards.
44+
public let maxOffsetX: Double
45+
46+
/// The max y offset to apply to the cards.
47+
public let maxOffsetY: Double
48+
49+
50+
/// Whether or not the animation is currently shuffling.
51+
@Published
52+
public private(set) var isShuffling = false
53+
54+
@Published
55+
fileprivate var animationTrigger = false
56+
57+
58+
/// This data type defines shuffle rotation and offsets.
59+
public typealias ShuffleData = (Angle, x: Double, y: Double)
60+
61+
private var shuffleData: [ShuffleData] = []
62+
}
63+
64+
public extension View {
65+
66+
/**
67+
Apply a shuffle animation to a deck item view.
68+
*/
69+
func withShuffleAnimation<Item>(
70+
_ animation: DeckShuffleAnimation,
71+
for item: Item,
72+
in deck: Deck<Item>
73+
) -> some View {
74+
let data = animation.shuffleData(for: item, in: deck)
75+
return self.rotationEffect(data?.0 ?? .zero)
76+
.offset(x: data?.1 ?? 0, y: data?.2 ?? 0)
77+
.animation(.default, value: animation.animationTrigger)
78+
}
79+
}
80+
81+
public extension DeckShuffleAnimation {
82+
83+
/**
84+
Shuffle the provided deck with a shuffle animation.
85+
86+
- Parameters:
87+
- deck: The deck to shuffle.
88+
- times: The number of times to shuffle the deck, by default `5`.
89+
*/
90+
func shuffle<Item>(
91+
_ deck: Binding<Deck<Item>>,
92+
times: Int = 5
93+
) {
94+
if animationTrigger { return }
95+
randomizeShuffleData(for: deck)
96+
shuffle(deck, times: times, time: 1)
97+
}
98+
99+
/**
100+
Get the current shuffle data for a certain deck item.
101+
*/
102+
func shuffleData<Item>(
103+
for item: Item,
104+
in deck: Deck<Item>
105+
) -> ShuffleData? {
106+
guard
107+
shuffleData.count == deck.items.count,
108+
let index = deck.items.firstIndex(of: item)
109+
else { return nil }
110+
return shuffleData[index]
111+
}
112+
}
113+
114+
private extension DeckShuffleAnimation {
115+
116+
func performAfterDelay(_ action: @escaping () -> Void) {
117+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2, execute: action)
118+
}
119+
120+
func randomizeShuffleData<Item>(for deck: Binding<Deck<Item>>) {
121+
shuffleData = (0..<deck.wrappedValue.items.count).map { _ in
122+
(
123+
Angle.degrees(Double.random(in: -maxDegrees...maxDegrees)),
124+
Double.random(in: -maxOffsetX...maxOffsetX),
125+
Double.random(in: -maxOffsetY...maxOffsetY)
126+
)
127+
}
128+
}
129+
130+
func shuffle<Item>(
131+
_ deck: Binding<Deck<Item>>,
132+
times: Int,
133+
time: Int
134+
) {
135+
animationTrigger.toggle()
136+
performAfterDelay {
137+
if time < times {
138+
self.randomizeShuffleData(for: deck)
139+
self.shuffle(deck, times: times, time: time + 1)
140+
} else {
141+
self.easeOutShuffleState(for: deck)
142+
}
143+
}
144+
}
145+
146+
func easeOutShuffleState<Item>(for deck: Binding<Deck<Item>>) {
147+
shuffleData = shuffleData.map { ($0.0/2, $0.1/2, $0.2/2) }
148+
animationTrigger.toggle()
149+
performAfterDelay {
150+
self.resetShuffleState(for: deck)
151+
}
152+
}
153+
154+
func resetShuffleState<Item>(for deck: Binding<Deck<Item>>) {
155+
animationTrigger.toggle()
156+
shuffleData = []
157+
performAfterDelay {
158+
deck.wrappedValue.shuffle()
159+
self.animationTrigger = false
160+
}
161+
}
162+
}

Sources/DeckKit/Deck.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
// DeckKit
44
//
55
// Created by Daniel Saidi on 2020-08-31.
6-
// Copyright © 2020 Daniel Saidi. All rights reserved.
6+
// Copyright © 2020-2023 Daniel Saidi. All rights reserved.
77
//
88

99
import Foundation

Sources/DeckKit/DeckContext.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
// DeckKit
44
//
55
// Created by Daniel Saidi on 2020-08-31.
6-
// Copyright © 2020 Daniel Saidi. All rights reserved.
6+
// Copyright © 2020-2023 Daniel Saidi. All rights reserved.
77
//
88

99
import Foundation

Sources/DeckKit/DeckItem.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
// DeckKit
44
//
55
// Created by Daniel Saidi on 2020-08-31.
6-
// Copyright © 2020 Daniel Saidi. All rights reserved.
6+
// Copyright © 2020-2023 Daniel Saidi. All rights reserved.
77
//
88

99
import Foundation

Sources/DeckKit/DeckKit.docc/DeckKit.md

+4
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,10 @@ DeckKit is available under the MIT license.
5959
- ``DeckView``
6060
- ``DeckViewConfiguration``
6161

62+
### Animations
63+
64+
- ``DeckShuffleAnimation``
65+
6266
### Favorites
6367

6468
- ``Favoritable``

Sources/DeckKit/DeckView.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
// DeckKit
44
//
55
// Created by Daniel Saidi on 2020-08-31.
6-
// Copyright © 2020 Daniel Saidi. All rights reserved.
6+
// Copyright © 2020-2023 Daniel Saidi. All rights reserved.
77
//
88

99
#if os(iOS) || os(macOS)

Sources/DeckKit/DeckViewConfiguration.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
// DeckKit
44
//
55
// Created by Daniel Saidi on 2022-11-28.
6-
// Copyright © 2022 Daniel Saidi. All rights reserved.
6+
// Copyright © 2022-2023 Daniel Saidi. All rights reserved.
77
//
88

99
import Foundation

0 commit comments

Comments
 (0)