Skip to content

Commit f940205

Browse files
Merge pull request #22 from SimformSolutionsPvtLtd/feature/reaction_animation
UNT-T30314 - Added new animation reaction view
2 parents be8b4b2 + e146d2f commit f940205

File tree

8 files changed

+464
-1
lines changed

8 files changed

+464
-1
lines changed

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,11 @@ SwiftUI animation library to bring your app to life. ✨
5858
- Download and drop **SSSwiftUIAnimations** folder in your project.
5959
- Congratulations!
6060

61+
### Reaction Animation View 🌊
62+
![ReactionAnimationView](https://github.com/SimformSolutionsPvtLtd/SS-iOS-Animations/blob/master/SSSwiftUIAnimations/GIFs/ReactionAnimationView.gif?raw=true)
63+
64+
[**Code Link**](https://github.com/SimformSolutionsPvtLtd/SS-iOS-Animations/tree/master/SSSwiftUIAnimations/Sources/ReactionAnimation) | Animation Name: ReactionAnimation
65+
6166
## Found these animations useful? :heart:
6267

6368
Support it by joining [stargazers] :star: for this repository.

SSSwiftUIAnimations.xcodeproj/project.pbxproj

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@
1414
4636F36E291E1BD600C8DB5B /* LeftArrow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4636F36D291E1BD600C8DB5B /* LeftArrow.swift */; };
1515
469963A5290FCE3600DC01AD /* SSLRArrowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 469963A4290FCE3600DC01AD /* SSLRArrowView.swift */; };
1616
8FE1727C2CAA8BF100F8AB45 /* ViewExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8FE1727A2CAA8BF100F8AB45 /* ViewExtension.swift */; };
17+
902200F62CDCC87E001DCC3A /* ReactionAnimationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 902200F52CDCC87E001DCC3A /* ReactionAnimationView.swift */; };
18+
902200F82CDCC951001DCC3A /* ReactionAnimationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 902200F72CDCC951001DCC3A /* ReactionAnimationViewModel.swift */; };
19+
902200FA2CDCCF23001DCC3A /* ExampleReactionAnimationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 902200F92CDCCF1C001DCC3A /* ExampleReactionAnimationView.swift */; };
20+
902201062CDE116E001DCC3A /* ReactionAnimationViewStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 902201052CDE1166001DCC3A /* ReactionAnimationViewStyle.swift */; };
21+
902201082CDE2293001DCC3A /* ReactionAnimationView.gif in Resources */ = {isa = PBXBuildFile; fileRef = 902201072CDE2293001DCC3A /* ReactionAnimationView.gif */; };
1722
B10677FE2BE8D0D400957B4E /* DownArrow.swift in Sources */ = {isa = PBXBuildFile; fileRef = B10677FD2BE8D0D400957B4E /* DownArrow.swift */; };
1823
B1098E7D2BD94ED900BC19DD /* WaveView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1098E7C2BD94ED900BC19DD /* WaveView.swift */; };
1924
B11B983A2BCE9C3F00D76016 /* CheckView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B11B98392BCE9C3F00D76016 /* CheckView.swift */; };
@@ -53,6 +58,11 @@
5358
469963A4290FCE3600DC01AD /* SSLRArrowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SSLRArrowView.swift; sourceTree = "<group>"; };
5459
8F9765232CE20D5000034CF7 /* Package.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Package.swift; sourceTree = "<group>"; };
5560
8FE1727A2CAA8BF100F8AB45 /* ViewExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewExtension.swift; sourceTree = "<group>"; };
61+
902200F52CDCC87E001DCC3A /* ReactionAnimationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionAnimationView.swift; sourceTree = "<group>"; };
62+
902200F72CDCC951001DCC3A /* ReactionAnimationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionAnimationViewModel.swift; sourceTree = "<group>"; };
63+
902200F92CDCCF1C001DCC3A /* ExampleReactionAnimationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleReactionAnimationView.swift; sourceTree = "<group>"; };
64+
902201052CDE1166001DCC3A /* ReactionAnimationViewStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionAnimationViewStyle.swift; sourceTree = "<group>"; };
65+
902201072CDE2293001DCC3A /* ReactionAnimationView.gif */ = {isa = PBXFileReference; lastKnownFileType = image.gif; path = ReactionAnimationView.gif; sourceTree = "<group>"; };
5666
B10677FD2BE8D0D400957B4E /* DownArrow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownArrow.swift; sourceTree = "<group>"; };
5767
B1098E7C2BD94ED900BC19DD /* WaveView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaveView.swift; sourceTree = "<group>"; };
5868
B11B98392BCE9C3F00D76016 /* CheckView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckView.swift; sourceTree = "<group>"; };
@@ -153,9 +163,20 @@
153163
path = Utils;
154164
sourceTree = "<group>";
155165
};
166+
902200F42CDCC860001DCC3A /* ReactionAnimation */ = {
167+
isa = PBXGroup;
168+
children = (
169+
902201052CDE1166001DCC3A /* ReactionAnimationViewStyle.swift */,
170+
902200F52CDCC87E001DCC3A /* ReactionAnimationView.swift */,
171+
902200F72CDCC951001DCC3A /* ReactionAnimationViewModel.swift */,
172+
);
173+
path = ReactionAnimation;
174+
sourceTree = "<group>";
175+
};
156176
B1343F702C06064B009ACE90 /* GIFs */ = {
157177
isa = PBXGroup;
158178
children = (
179+
902201072CDE2293001DCC3A /* ReactionAnimationView.gif */,
159180
B1343F712C060680009ACE90 /* ProgressView.gif */,
160181
B741A1432C4A6D610083399B /* WaterProgressView.gif */,
161182
B1343F732C060686009ACE90 /* LRArrowView.gif */,
@@ -180,6 +201,7 @@
180201
B153FD112BFB64BE00AEFE83 /* Examples */ = {
181202
isa = PBXGroup;
182203
children = (
204+
902200F92CDCCF1C001DCC3A /* ExampleReactionAnimationView.swift */,
183205
B19E0B652BF7498700E65974 /* ExampleProgressView.swift */,
184206
B153FD142BFB7A7900AEFE83 /* ExampleLRArrowView.swift */,
185207
B780A9252C3D806300342512 /* ExampleWaterProgressView.swift */,
@@ -191,6 +213,7 @@
191213
B1DE99D72C060E1A006995FB /* Sources */ = {
192214
isa = PBXGroup;
193215
children = (
216+
902200F42CDCC860001DCC3A /* ReactionAnimation */,
194217
8FE1727B2CAA8BF100F8AB45 /* Utils */,
195218
B780A9162C3D05CD00342512 /* WaterProgressAnimation */,
196219
B14AB36A2BC40286004B09C4 /* ProgressAnimation */,
@@ -295,6 +318,7 @@
295318
B1EA09DE2C11A6B70024BC28 /* Banner.png in Resources */,
296319
2BC2D8F728CF3A7000CAB302 /* Assets.xcassets in Resources */,
297320
B1343F742C060686009ACE90 /* LRArrowView.gif in Resources */,
321+
902201082CDE2293001DCC3A /* ReactionAnimationView.gif in Resources */,
298322
);
299323
runOnlyForDeploymentPostprocessing = 0;
300324
};
@@ -315,6 +339,7 @@
315339
B18792612AA5A0D2006F2CC9 /* CircularView.swift in Sources */,
316340
B153FD152BFB7A7900AEFE83 /* ExampleLRArrowView.swift in Sources */,
317341
2BC2D8F528CF3A6F00CAB302 /* ContentView.swift in Sources */,
342+
902200F62CDCC87E001DCC3A /* ReactionAnimationView.swift in Sources */,
318343
4636F36E291E1BD600C8DB5B /* LeftArrow.swift in Sources */,
319344
B15FD7992C04785700752CEA /* CustomToolBar.swift in Sources */,
320345
469963A5290FCE3600DC01AD /* SSLRArrowView.swift in Sources */,
@@ -324,14 +349,17 @@
324349
B153FD132BFB71F500AEFE83 /* FilledStrokeCircle.swift in Sources */,
325350
2BC2D8F328CF3A6F00CAB302 /* SSSwiftUIAnimationsApp.swift in Sources */,
326351
B741D9A62C46448200ABFCB4 /* WaterProgressTextView.swift in Sources */,
352+
902201062CDE116E001DCC3A /* ReactionAnimationViewStyle.swift in Sources */,
327353
B780A9242C3D7A7C00342512 /* WaterCircleOutlineView.swift in Sources */,
328354
B1DFCA532BF4FC7900F01505 /* ArrowView.swift in Sources */,
329355
B780A9182C3D063500342512 /* WaterProgressView.swift in Sources */,
330356
B11B983A2BCE9C3F00D76016 /* CheckView.swift in Sources */,
357+
902200F82CDCC951001DCC3A /* ReactionAnimationViewModel.swift in Sources */,
331358
B7ECD58D2C452D8100B6A703 /* BubbleView.swift in Sources */,
332359
8FE1727C2CAA8BF100F8AB45 /* ViewExtension.swift in Sources */,
333360
B780A9262C3D806300342512 /* ExampleWaterProgressView.swift in Sources */,
334361
B780A91A2C3D0BCB00342512 /* WaterProgressViewStyle.swift in Sources */,
362+
902200FA2CDCCF23001DCC3A /* ExampleReactionAnimationView.swift in Sources */,
335363
B1DFCA512BF4FA3D00F01505 /* ProgressCircle.swift in Sources */,
336364
B14AB36C2BC41B05004B09C4 /* ProgressView.swift in Sources */,
337365
);
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
//
2+
// ExampleReactionAnimationView.swift
3+
// SSSwiftUIAnimations
4+
//
5+
// Created by Faaiz Daglawala on 07/11/24.
6+
//
7+
8+
import SwiftUI
9+
10+
struct ExampleReactionAnimationView: View {
11+
12+
// MARK: - Variables
13+
14+
var body: some View {
15+
VStack {
16+
SSReactionAnimationView(
17+
style: SSReactionAnimationViewStyle(
18+
outerCircleColor: .clear,
19+
innerCircleColor: .blue,
20+
defautHeartColor: .gray,
21+
selectedHeartColor: .red
22+
)) { isSelected in }
23+
.frame(width: 100, height: 100)
24+
}
25+
.customToolbar(title: "ReactionAnimationView Example", fontSize: 17)
26+
}
27+
28+
}
29+
30+
#Preview {
31+
ExampleReactionAnimationView()
32+
}

SSSwiftUIAnimations/Examples/ExamplesList/ExampleListModel.swift

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,10 @@ class ExampleListModel: Identifiable {
2121
}
2222

2323
// data for example row list
24-
static let exampleList = [ExampleListModel(rowTitle: "ProgressView", destinationView: AnyView(ExampleProgressView())), ExampleListModel(rowTitle: "Arrow Left Right View", destinationView: AnyView(ExampleLRArrowView())), ExampleListModel(rowTitle: "Water Progress View", destinationView: AnyView(ExampleWaterProgressView()))]
24+
static let exampleList = [
25+
ExampleListModel(rowTitle: "ProgressView", destinationView: AnyView(ExampleProgressView())),
26+
ExampleListModel(rowTitle: "Arrow Left Right View", destinationView: AnyView(ExampleLRArrowView())),
27+
ExampleListModel(rowTitle: "Water Progress View", destinationView: AnyView(ExampleWaterProgressView())),
28+
ExampleListModel(rowTitle: "ReactionAnimaionView", destinationView: AnyView(ExampleReactionAnimationView()))
29+
]
2530
}
125 KB
Loading
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
//
2+
// ContentView.swift
3+
// ReactionAnimationSwiftUI
4+
//
5+
// Created by Faaiz Daglawala on 15/12/22.
6+
//
7+
8+
import SwiftUI
9+
10+
/// This is the Main View of ReactionAnimation. Add this to your View Hierarchy to start
11+
/// using animation in your screen. It will displays a heart animation with expanding circles and smaller animated circles surrounding it.
12+
/// Use case Example:
13+
/// ```swift
14+
/// SSReactionAnimationView(
15+
/// style: SSReactionAnimationViewStyle(
16+
/// outerCircleColor: .clear,
17+
/// innerCircleColor: .blue,
18+
/// defautHeartColor: .gray,
19+
/// selectedHeartColor: .red
20+
/// )
21+
/// )
22+
/// .frame(width: 100, height: 100)
23+
/// ```
24+
public struct SSReactionAnimationView: View {
25+
26+
// MARK: - Properties
27+
28+
/// The ViewModel that manages the animation states and properties.
29+
@ObservedObject var viewModel: SSReactionAnimationViewModel
30+
31+
/// A closure that is triggered when the animation is completed.
32+
/// The closure takes a single `Bool` parameter that indicates whether
33+
/// the animation concluded with the item in a selected state.
34+
var onAnimationCompleted: ((_ isSelected: Bool) -> ())?
35+
36+
// MARK: - Initializer
37+
38+
/// Initializes a new `SSReactionAnimationView` with a specified style and an optional
39+
/// completion handler for the animation.
40+
///
41+
/// - Parameters:
42+
/// - style: A `SSReactionAnimationViewStyle` object that defines the colors and other styling options for the animation.
43+
/// - onAnimationCompleted: An optional closure that is called when the animation completes,
44+
/// with a `Bool` indicating if the item is in a selected state.
45+
public init(style: SSReactionAnimationViewStyle, onAnimationCompleted: ((_ isSelected: Bool) -> ())? = nil) {
46+
self.viewModel = SSReactionAnimationViewModel(style: style)
47+
self.onAnimationCompleted = onAnimationCompleted
48+
}
49+
50+
public var body: some View {
51+
GeometryReader { geometryProxy in
52+
ZStack {
53+
// Background Circle
54+
Circle()
55+
.fill(viewModel.style.outerCircleColor)
56+
57+
// Heart Icon - shows either a default or selected version based on animation state.
58+
if viewModel.isSelected {
59+
// Display a selected-color heart after all animations are complete.
60+
Image(systemName: "heart.fill")
61+
.resizable()
62+
.scaledToFit()
63+
.frame(width: geometryProxy.size.width / 2, height: geometryProxy.size.height / 2)
64+
.foregroundColor(viewModel.style.selectedHeartColor)
65+
.opacity(1)
66+
.onTapGesture {
67+
// Reset small circles and animations when the selected heart is tapped.
68+
viewModel.resetBubbleProperties()
69+
viewModel.resetAnimation()
70+
}
71+
} else {
72+
// Default-colored heart that scales down when the animation starts.
73+
Image(systemName: "heart.fill")
74+
.resizable()
75+
.scaledToFit()
76+
.frame(width: geometryProxy.size.width / 2, height: geometryProxy.size.height / 2)
77+
.foregroundColor(viewModel.style.defautHeartColor)
78+
.opacity(viewModel.heartScaleAnimation ? 0 : 1)
79+
.scaleEffect(viewModel.heartScaleAnimation ? 0 : 1)
80+
.animation(.easeInOut(duration: 0.3), value: viewModel.heartScaleAnimation)
81+
.onTapGesture {
82+
// Start the animation when the default heart is tapped.
83+
viewModel.startAnimationSequence()
84+
}
85+
}
86+
87+
// Main expanding circles animation when the `circleAnimation` state is active.
88+
if viewModel.circleAnimation {
89+
MainAnimationCircles(viewModel: viewModel)
90+
}
91+
}
92+
.onChange(of: viewModel.isSelected) { _ in
93+
onAnimationCompleted?(viewModel.isSelected)
94+
}
95+
.onAppear {
96+
viewModel.setBubbleCount()
97+
// Set the geometry size in the ViewModel for positioning calculations.
98+
viewModel.setAnimationViewSize(size: geometryProxy.size)
99+
viewModel.calculateSizeProperties()
100+
}
101+
.frame(width: geometryProxy.size.width, height: geometryProxy.size.height)
102+
}
103+
}
104+
105+
/// A view that handles displaying the main animated circles around the central circle.
106+
private struct MainAnimationCircles: View {
107+
108+
// MARK: - Properties
109+
110+
/// Binding to the parent view's ViewModel to access animation properties.
111+
@ObservedObject var viewModel: SSReactionAnimationViewModel
112+
113+
// MARK: - Body
114+
115+
var body: some View {
116+
ZStack {
117+
// Seven Large Circles around the central circle, each positioned based on angle calculation.
118+
ForEach(0..<viewModel.bubbleCount, id: \.self) { index in
119+
Circle()
120+
.fill(viewModel.largeBubbleColors[index])
121+
.frame(width: viewModel.largeBubbleSize, height: viewModel.largeBubbleSize)
122+
.opacity(viewModel.bubblesOpacity)
123+
.offset(x: viewModel.largeBubbleRadius * cos(viewModel.angle(for: index)),
124+
y: viewModel.largeBubbleRadius * sin(viewModel.angle(for: index)))
125+
}
126+
127+
// Seven Small Circles around the central circle, offset by an additional angle spacing.
128+
ForEach(0..<viewModel.bubbleCount, id: \.self) { index in
129+
Circle()
130+
.fill(viewModel.smallBubbleColors[index])
131+
.frame(width: viewModel.smallBubbleSize, height: viewModel.smallBubbleSize)
132+
.opacity(viewModel.bubblesOpacity)
133+
.offset(x: viewModel.smallBubbleRadius * cos(viewModel.angle(for: index, withOffset: viewModel.angleSpacing)),
134+
y: viewModel.smallBubbleRadius * sin(viewModel.angle(for: index, withOffset: viewModel.angleSpacing)))
135+
}
136+
137+
// Expanding blue circle animation around the main heart.
138+
Circle()
139+
.strokeBorder(viewModel.style.innerCircleColor, lineWidth: viewModel.outlineWidth)
140+
.frame(width: viewModel.animationViewSize.width / 2, height: viewModel.animationViewSize.height / 2)
141+
.scaleEffect(viewModel.circleScale)
142+
}
143+
.onAppear {
144+
// Start the expansion animation when view appears.
145+
viewModel.triggerCircleExpansion()
146+
}
147+
}
148+
}
149+
}

0 commit comments

Comments
 (0)