diff --git a/SSSwiftUIAnimations/SSSwiftUIAnimations.xcodeproj/project.pbxproj b/SSSwiftUIAnimations/SSSwiftUIAnimations.xcodeproj/project.pbxproj index 28688ed..3c0ab01 100644 --- a/SSSwiftUIAnimations/SSSwiftUIAnimations.xcodeproj/project.pbxproj +++ b/SSSwiftUIAnimations/SSSwiftUIAnimations.xcodeproj/project.pbxproj @@ -11,6 +11,7 @@ 2BC2D8F528CF3A6F00CAB302 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BC2D8F428CF3A6F00CAB302 /* ContentView.swift */; }; 2BC2D8F728CF3A7000CAB302 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2BC2D8F628CF3A7000CAB302 /* Assets.xcassets */; }; 2BC2D8FA28CF3A7000CAB302 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2BC2D8F928CF3A7000CAB302 /* Preview Assets.xcassets */; }; + 7DD1EBF22935D384008021DF /* sendButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7DD1EBF12935D384008021DF /* sendButtonView.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -19,6 +20,7 @@ 2BC2D8F428CF3A6F00CAB302 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 2BC2D8F628CF3A7000CAB302 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 2BC2D8F928CF3A7000CAB302 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; + 7DD1EBF12935D384008021DF /* sendButtonView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = sendButtonView.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -53,6 +55,7 @@ children = ( 2BC2D8F228CF3A6F00CAB302 /* SSSwiftUIAnimationsApp.swift */, 2BC2D8F428CF3A6F00CAB302 /* ContentView.swift */, + 7DD1EBF02935D35A008021DF /* SendButton */, 2BC2D8F628CF3A7000CAB302 /* Assets.xcassets */, 2BC2D8F828CF3A7000CAB302 /* Preview Content */, ); @@ -67,6 +70,14 @@ path = "Preview Content"; sourceTree = ""; }; + 7DD1EBF02935D35A008021DF /* SendButton */ = { + isa = PBXGroup; + children = ( + 7DD1EBF12935D384008021DF /* sendButtonView.swift */, + ); + path = SendButton; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -138,6 +149,7 @@ buildActionMask = 2147483647; files = ( 2BC2D8F528CF3A6F00CAB302 /* ContentView.swift in Sources */, + 7DD1EBF22935D384008021DF /* sendButtonView.swift in Sources */, 2BC2D8F328CF3A6F00CAB302 /* SSSwiftUIAnimationsApp.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/SSSwiftUIAnimations/SSSwiftUIAnimations/Assets.xcassets/arrow.imageset/Contents.json b/SSSwiftUIAnimations/SSSwiftUIAnimations/Assets.xcassets/arrow.imageset/Contents.json new file mode 100644 index 0000000..9c3c97a --- /dev/null +++ b/SSSwiftUIAnimations/SSSwiftUIAnimations/Assets.xcassets/arrow.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "arrow 2.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/SSSwiftUIAnimations/SSSwiftUIAnimations/Assets.xcassets/arrow.imageset/arrow 2.pdf b/SSSwiftUIAnimations/SSSwiftUIAnimations/Assets.xcassets/arrow.imageset/arrow 2.pdf new file mode 100644 index 0000000..9cb7761 Binary files /dev/null and b/SSSwiftUIAnimations/SSSwiftUIAnimations/Assets.xcassets/arrow.imageset/arrow 2.pdf differ diff --git a/SSSwiftUIAnimations/SSSwiftUIAnimations/Assets.xcassets/checkmark.imageset/Contents.json b/SSSwiftUIAnimations/SSSwiftUIAnimations/Assets.xcassets/checkmark.imageset/Contents.json new file mode 100644 index 0000000..6a984ff --- /dev/null +++ b/SSSwiftUIAnimations/SSSwiftUIAnimations/Assets.xcassets/checkmark.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "checkmark 1.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/SSSwiftUIAnimations/SSSwiftUIAnimations/Assets.xcassets/checkmark.imageset/checkmark 1.pdf b/SSSwiftUIAnimations/SSSwiftUIAnimations/Assets.xcassets/checkmark.imageset/checkmark 1.pdf new file mode 100644 index 0000000..7f33b8b Binary files /dev/null and b/SSSwiftUIAnimations/SSSwiftUIAnimations/Assets.xcassets/checkmark.imageset/checkmark 1.pdf differ diff --git a/SSSwiftUIAnimations/SSSwiftUIAnimations/SendButton/sendButtonView.swift b/SSSwiftUIAnimations/SSSwiftUIAnimations/SendButton/sendButtonView.swift new file mode 100644 index 0000000..4a1bd1b --- /dev/null +++ b/SSSwiftUIAnimations/SSSwiftUIAnimations/SendButton/sendButtonView.swift @@ -0,0 +1,212 @@ +// +// sendButton.swift +// SSSwiftUIAnimation +// +// Created by Darshan Gujarati on 11/11/22. +// + +import SwiftUI + +enum SendState { + case origin + case arrowOut + case changeArrowColor + case arrowAtP1 + case arrowAtP2 + case arrowAtP3 + case arrowToTick + case sentOut + case viewCenter + + var animation: Animation { + switch self { + case .arrowOut: return Animation.easeIn(duration: 1.0) + case .arrowAtP1, .arrowAtP3: + return Animation.easeIn(duration: 0.1) + case .arrowToTick: + return Animation.easeIn(duration: 0.0) + default: return Animation.easeIn + } + } + var text: String { + switch self { + case .origin: return "Send" + default: return "" + } + } + var imageRotation: Double { + switch self { + case .arrowAtP1, .changeArrowColor: return -80 + case .arrowAtP2: return 100 + case .arrowAtP3: return 230 + default: return 0 + } + } + var imageName: String { + switch self { + case .origin, .arrowOut, .changeArrowColor, .arrowAtP1, .arrowAtP2, .arrowAtP3: + return "arrow" + case .arrowToTick, .sentOut, .viewCenter: + return "checkmark" + } + } + var imageColor: Color { + switch self { + case .changeArrowColor, .arrowOut, .arrowAtP2, .arrowAtP3: return Color.cyan + default: return Color.white + } + } + var offY: CGFloat { + switch self { + case . arrowOut, .arrowAtP2, .arrowAtP3, .arrowToTick, .viewCenter: return -5 + default: return 0 + } + } +} + +struct sendButtonView: View { + let duration: Double = 3 + var geo: GeometryProxy + var p1: CGPoint { + return CGPoint(x: geo.size.width / 4, y: geo.size.height / 2) + } + var p2: CGPoint { + return CGPoint(x: geo.size.width + 20, y: -geo.size.height) + } + var p3: CGPoint { + return CGPoint(x: geo.size.width / 2, y: geo.size.height / 2) + } + var curvePoint1: CGPoint { + return CGPoint(x: geo.size.width, y: -geo.size.height * 2) + } + var curvePoint2: CGPoint { + return CGPoint(x: geo.size.width, y: geo.size.height * 3) + } + var sizeArrowOut: CGSize { + return CGSize(width: geo.size.height + 10, height: geo.size.height + 10) + } + var sizeSentOut: CGSize { + return CGSize(width: geo.size.height, height: geo.size.height) + } + var path : Path { + var result = Path() + result.move(to: p1) + result.addCurve(to: p2, control1: curvePoint1, control2: curvePoint1) + result.move(to: p2) + result.addCurve(to: p3, control1: curvePoint2, control2: curvePoint2) + return result + } + + @State var frameSize = CGSize(width: 120, height: 60) + @State var isMovingForward = false + @State var animationState: SendState = .origin + + var tMax : CGFloat { isMovingForward ? 1 : 0 } + var opac : Double { isMovingForward ? 1 : 0 } + + var body: some View { + VStack { + ZStack { + Text("Sent!") + .font(.system(size: 20, weight: .bold, design: .rounded)) + .frame(width: animationState == .sentOut ? geo.size.width / 1.1 : 0, height: geo.size.height / 1.5, alignment: .center) + .background(.cyan) + .foregroundColor(.white) + .cornerRadius(12) + .shadow(radius: 3) + .offset(x: animationState == .sentOut ? geo.size.width / 1.71 : 0) + + ZStack { + Text(animationState.text) + .frame(alignment: .center) + .foregroundColor(.white) + .font(.system(size: 20, weight: .bold, design: .rounded)) + .offset(x: 15) + } + .frame(width: frameSize.width, height: frameSize.height) + .background(Color.cyan) + .cornerRadius(animationState == .origin ? geo.size.height / 5 : frameSize.height / 2) + .offset(y: animationState.offY) + .animation(animationState.animation, value: animationState) + + Image(animationState.imageName) + .rotationEffect(Angle(degrees: animationState.imageRotation), anchor: .center) + .foregroundColor(animationState.imageColor) + .modifier(Moving(time: tMax, path: path, start: p1)) + } + } + .shadow(radius: 10) + .offset(x: (animationState == .sentOut || animationState == .viewCenter) ? -geo.size.width / 2.5 : 0) + .onAppear { + frameSize = geo.size + } + .onTapGesture { + Timer.scheduledTimer(withTimeInterval: 0.0, repeats: false) { (Timer) in + self.animationState = .arrowAtP1 + } + Timer.scheduledTimer(withTimeInterval: 0.09, repeats: false) { (Timer) in + self.animationState = .changeArrowColor + } + Timer.scheduledTimer(withTimeInterval: 0.1, repeats: false) { (Timer) in + frameSize = sizeArrowOut + self.animationState = .arrowOut + isMovingForward = true + + DispatchQueue.main.asyncAfter(deadline: .now() + duration + 0.5) { + isMovingForward = false + } + } + Timer.scheduledTimer(withTimeInterval: 0.4, repeats: false) { (Timer) in + self.animationState = .arrowAtP2 + } + Timer.scheduledTimer(withTimeInterval: 0.9, repeats: false) { (Timer) in + self.animationState = .arrowAtP3 + } + Timer.scheduledTimer(withTimeInterval: 1.1, repeats: false) { (Timer) in + self.animationState = .arrowToTick + } + + Timer.scheduledTimer(withTimeInterval: 1.9, repeats: false) { (Timer) in + self.animationState = .viewCenter + } + Timer.scheduledTimer(withTimeInterval: 2.0, repeats: false) { (Timer) in + frameSize = sizeSentOut + self.animationState = .sentOut + } + Timer.scheduledTimer(withTimeInterval: 3, repeats: false) { (Timer) in + frameSize = geo.size + self.animationState = .origin + } + } + .animation(animationState.animation, value: animationState) + } +} + +struct Moving: AnimatableModifier { + var time : CGFloat + let path : Path + let start: CGPoint + + var animatableData: CGFloat { + get { time } + set { time = newValue } + } + + func body(content: Content) -> some View { + content + .position( + path.trimmedPath(from: 0, to: time).currentPoint ?? start + ) + } +} + +struct sendButton_Previews: PreviewProvider { + static var previews: some View { + VStack { + GeometryReader { geo in + sendButtonView(geo: geo) + } + .frame(width: 120, height: 60, alignment: .center) + } + } +}