diff --git a/Uplift.xcodeproj/project.pbxproj b/Uplift.xcodeproj/project.pbxproj index f59908b..2123451 100644 --- a/Uplift.xcodeproj/project.pbxproj +++ b/Uplift.xcodeproj/project.pbxproj @@ -105,6 +105,8 @@ 2EE5F3E22B12EDB6008E0299 /* Apollo in Frameworks */ = {isa = PBXBuildFile; productRef = 2EE5F3E12B12EDB6008E0299 /* Apollo */; }; 2EE5F3E42B12EDB6008E0299 /* ApolloAPI in Frameworks */ = {isa = PBXBuildFile; productRef = 2EE5F3E32B12EDB6008E0299 /* ApolloAPI */; }; 2EF1A2582B129EEB007A299F /* Network.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EF1A2572B129EEB007A299F /* Network.swift */; }; + 52A30D3A3053B7E3002AE86C /* IntroAnimationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52A30D393053B7DF002AE86C /* IntroAnimationViewModel.swift */; }; + 52A30D3C3053B9B8002AE86C /* SignInViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52A30D3B3053B9B4002AE86C /* SignInViewModel.swift */; }; 63001AD62CC9ACFF0082AFFA /* LoginViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63001AD52CC9ACFF0082AFFA /* LoginViewModel.swift */; }; 63001AD82CC9AD970082AFFA /* GoogleSignIn in Frameworks */ = {isa = PBXBuildFile; productRef = 63001AD72CC9AD970082AFFA /* GoogleSignIn */; }; 63001ADA2CC9AD980082AFFA /* GoogleSignInSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 63001AD92CC9AD970082AFFA /* GoogleSignInSwift */; }; @@ -160,6 +162,7 @@ C07CFEB42F6B5478005830D3 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C07CFEB32F6B546D005830D3 /* SettingsView.swift */; }; C0CB14922F709DA100E23E69 /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0CB14912F709D9D00E23E69 /* AboutView.swift */; }; C0CB14942F70A2C800E23E69 /* Member.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0CB14932F70A2C100E23E69 /* Member.swift */; }; + C0DED3032F70ACB600E43983 /* IntroAnimationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0DED3022F70ACAD00E43983 /* IntroAnimationView.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -262,6 +265,8 @@ 2EE0C7CA2B12D6EE000D3BF6 /* Gym.graphql */ = {isa = PBXFileReference; lastKnownFileType = text; path = Gym.graphql; sourceTree = ""; }; 2EE5F3C72B12E094008E0299 /* ApolloClientProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApolloClientProtocol.swift; sourceTree = ""; }; 2EF1A2572B129EEB007A299F /* Network.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Network.swift; sourceTree = ""; }; + 52A30D393053B7DF002AE86C /* IntroAnimationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntroAnimationViewModel.swift; sourceTree = ""; }; + 52A30D3B3053B9B4002AE86C /* SignInViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignInViewModel.swift; sourceTree = ""; }; 63001AD52CC9ACFF0082AFFA /* LoginViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoginViewModel.swift; sourceTree = ""; }; 6329A4452C964D0800D30A2F /* SignInView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignInView.swift; sourceTree = ""; }; 6329A4472CA0F89D00D30A2F /* CreateProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateProfileView.swift; sourceTree = ""; }; @@ -319,6 +324,7 @@ C07CFEB32F6B546D005830D3 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; C0CB14912F709D9D00E23E69 /* AboutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutView.swift; sourceTree = ""; }; C0CB14932F70A2C100E23E69 /* Member.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Member.swift; sourceTree = ""; }; + C0DED3022F70ACAD00E43983 /* IntroAnimationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntroAnimationView.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -437,6 +443,7 @@ 2E090ED02B1308C900BAE982 /* Views */ = { isa = PBXGroup; children = ( + C0DED3022F70ACAD00E43983 /* IntroAnimationView.swift */, 294CC21C2D7E34D000EF6487 /* WeeklyWorkoutTrackerView.swift */, 293032562D7A8F64002E5484 /* WorkoutProgressArc.swift */, 89950D892B992E8400DFB007 /* ClassesView.swift */, @@ -455,6 +462,8 @@ 2E090ED12B1308D500BAE982 /* ViewModels */ = { isa = PBXGroup; children = ( + 52A30D3B3053B9B4002AE86C /* SignInViewModel.swift */, + 52A30D393053B7DF002AE86C /* IntroAnimationViewModel.swift */, 293A20A12F467CED0012C74C /* SetGoalsViewModel.swift */, 135EEBA92EC7E5F70051AD5B /* WorkoutCheckInViewModel.swift */, 89C34AA02CA66E9000C579A5 /* CapacityRemindersViewModel.swift */, @@ -957,6 +966,7 @@ 893AD9172D595DC800C0817B /* MuscleGroupView.swift in Sources */, 89E4FAAA2CEFEC3C00A952B1 /* CapacitySemiCircleView.swift in Sources */, 89599A4E2BD4B39B00DA44DE /* FitnessClass.swift in Sources */, + 52A30D3A3053B7E3002AE86C /* IntroAnimationViewModel.swift in Sources */, 2E15F5122B3A3D0000414BEC /* TriangleShape.swift in Sources */, 293A20A22F467CF00012C74C /* SetGoalsViewModel.swift in Sources */, 8940D4EE2CE3E60200F4A4A4 /* Workout.swift in Sources */, @@ -967,6 +977,7 @@ 296A2DAE2D7A805300EF042F /* ProfileViewModel.swift in Sources */, 2E15F50C2B3A0B2100414BEC /* CapacityCircleView.swift in Sources */, 2E090EC52B12EF2600BAE982 /* Publishers.swift in Sources */, + 52A30D3C3053B9B8002AE86C /* SignInViewModel.swift in Sources */, C07CFEB42F6B5478005830D3 /* SettingsView.swift in Sources */, 135EEBAA2EC7E60B0051AD5B /* WorkoutCheckInViewModel.swift in Sources */, 2E39D8222B3B631200AD238B /* DividerLine.swift in Sources */, @@ -983,6 +994,7 @@ 2E39D82A2B3B9F3D00AD238B /* FacilityExpandedView.swift in Sources */, 2EB161C92BBC9C3100493B71 /* StretchyModifier.swift in Sources */, 6329A44A2CA0FE8A00D30A2F /* CheckBoxView.swift in Sources */, + C0DED3032F70ACB600E43983 /* IntroAnimationView.swift in Sources */, 2E39D82C2B3BB35000AD238B /* FacilityExpandedViewModel.swift in Sources */, 8928C2932F71DCE900FA18BB /* GoalSettingWarningModal.swift in Sources */, 8992674A2CC03E8D00E5741E /* Dropdown.swift in Sources */, diff --git a/Uplift.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Uplift.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 7ea63fd..596ddf8 100644 --- a/Uplift.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Uplift.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "24cad5d063271fbb982480784567afd93e78d473b3b449aaa03b5d8c9d3413ba", + "originHash" : "5b4741aa50c37b9bdaca9932fc3d8f79e13163bc7b1b107ec4b81f489e3fc6ca", "pins" : [ { "identity" : "abseil-cpp-binary", diff --git a/Uplift/Resources/Assets.xcassets/appdev_logo_white.imageset/Contents.json b/Uplift/Resources/Assets.xcassets/appdev_logo_white.imageset/Contents.json new file mode 100644 index 0000000..b8ddf9d --- /dev/null +++ b/Uplift/Resources/Assets.xcassets/appdev_logo_white.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "appdev_logo_white.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Frame 1575.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Frame 1575 (1).png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Uplift/Resources/Assets.xcassets/appdev_logo_white.imageset/Frame 1575 (1).png b/Uplift/Resources/Assets.xcassets/appdev_logo_white.imageset/Frame 1575 (1).png new file mode 100644 index 0000000..975458e Binary files /dev/null and b/Uplift/Resources/Assets.xcassets/appdev_logo_white.imageset/Frame 1575 (1).png differ diff --git a/Uplift/Resources/Assets.xcassets/appdev_logo_white.imageset/Frame 1575.png b/Uplift/Resources/Assets.xcassets/appdev_logo_white.imageset/Frame 1575.png new file mode 100644 index 0000000..f9c40ef Binary files /dev/null and b/Uplift/Resources/Assets.xcassets/appdev_logo_white.imageset/Frame 1575.png differ diff --git a/Uplift/Resources/Assets.xcassets/appdev_logo_white.imageset/appdev_logo_white.png b/Uplift/Resources/Assets.xcassets/appdev_logo_white.imageset/appdev_logo_white.png new file mode 100644 index 0000000..bbe3d04 Binary files /dev/null and b/Uplift/Resources/Assets.xcassets/appdev_logo_white.imageset/appdev_logo_white.png differ diff --git a/Uplift/Resources/Assets.xcassets/intro_background.imageset/Animation (1).png b/Uplift/Resources/Assets.xcassets/intro_background.imageset/Animation (1).png new file mode 100644 index 0000000..b07f050 Binary files /dev/null and b/Uplift/Resources/Assets.xcassets/intro_background.imageset/Animation (1).png differ diff --git a/Uplift/Resources/Assets.xcassets/intro_background.imageset/Animation.png b/Uplift/Resources/Assets.xcassets/intro_background.imageset/Animation.png new file mode 100644 index 0000000..4ad58f4 Binary files /dev/null and b/Uplift/Resources/Assets.xcassets/intro_background.imageset/Animation.png differ diff --git a/Uplift/Resources/Assets.xcassets/intro_background.imageset/Contents.json b/Uplift/Resources/Assets.xcassets/intro_background.imageset/Contents.json new file mode 100644 index 0000000..3e9de5e --- /dev/null +++ b/Uplift/Resources/Assets.xcassets/intro_background.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "intro_background.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Animation.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Animation (1).png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Uplift/Resources/Assets.xcassets/intro_background.imageset/intro_background.png b/Uplift/Resources/Assets.xcassets/intro_background.imageset/intro_background.png new file mode 100644 index 0000000..cdef1dd Binary files /dev/null and b/Uplift/Resources/Assets.xcassets/intro_background.imageset/intro_background.png differ diff --git a/Uplift/Resources/Assets.xcassets/mountain_back.imageset/Contents.json b/Uplift/Resources/Assets.xcassets/mountain_back.imageset/Contents.json index 140269b..2a36a79 100644 --- a/Uplift/Resources/Assets.xcassets/mountain_back.imageset/Contents.json +++ b/Uplift/Resources/Assets.xcassets/mountain_back.imageset/Contents.json @@ -6,12 +6,12 @@ "scale" : "1x" }, { - "filename" : "back 1.png", + "filename" : "back2x.png", "idiom" : "universal", "scale" : "2x" }, { - "filename" : "back 2.png", + "filename" : "back3x.png", "idiom" : "universal", "scale" : "3x" } diff --git a/Uplift/Resources/Assets.xcassets/mountain_back.imageset/back 1.png b/Uplift/Resources/Assets.xcassets/mountain_back.imageset/back (1) 1.png similarity index 100% rename from Uplift/Resources/Assets.xcassets/mountain_back.imageset/back 1.png rename to Uplift/Resources/Assets.xcassets/mountain_back.imageset/back (1) 1.png diff --git a/Uplift/Resources/Assets.xcassets/mountain_back.imageset/back 2.png b/Uplift/Resources/Assets.xcassets/mountain_back.imageset/back (1) 2.png similarity index 100% rename from Uplift/Resources/Assets.xcassets/mountain_back.imageset/back 2.png rename to Uplift/Resources/Assets.xcassets/mountain_back.imageset/back (1) 2.png diff --git a/Uplift/Resources/Assets.xcassets/mountain_back.imageset/back (1).png b/Uplift/Resources/Assets.xcassets/mountain_back.imageset/back (1).png new file mode 100644 index 0000000..e84fd8e Binary files /dev/null and b/Uplift/Resources/Assets.xcassets/mountain_back.imageset/back (1).png differ diff --git a/Uplift/Resources/Assets.xcassets/mountain_back.imageset/back2x.png b/Uplift/Resources/Assets.xcassets/mountain_back.imageset/back2x.png new file mode 100644 index 0000000..bea99e5 Binary files /dev/null and b/Uplift/Resources/Assets.xcassets/mountain_back.imageset/back2x.png differ diff --git a/Uplift/Resources/Assets.xcassets/mountain_back.imageset/back3x.png b/Uplift/Resources/Assets.xcassets/mountain_back.imageset/back3x.png new file mode 100644 index 0000000..bc89652 Binary files /dev/null and b/Uplift/Resources/Assets.xcassets/mountain_back.imageset/back3x.png differ diff --git a/Uplift/Resources/Assets.xcassets/mountain_front.imageset/1faede3a20c9bf4093a4b9d33c4c6d97ef6ca5a9 1.png b/Uplift/Resources/Assets.xcassets/mountain_front.imageset/1faede3a20c9bf4093a4b9d33c4c6d97ef6ca5a9 1.png deleted file mode 100644 index 118a385..0000000 Binary files a/Uplift/Resources/Assets.xcassets/mountain_front.imageset/1faede3a20c9bf4093a4b9d33c4c6d97ef6ca5a9 1.png and /dev/null differ diff --git a/Uplift/Resources/Assets.xcassets/mountain_front.imageset/1faede3a20c9bf4093a4b9d33c4c6d97ef6ca5a9 2.png b/Uplift/Resources/Assets.xcassets/mountain_front.imageset/1faede3a20c9bf4093a4b9d33c4c6d97ef6ca5a9 2.png deleted file mode 100644 index 118a385..0000000 Binary files a/Uplift/Resources/Assets.xcassets/mountain_front.imageset/1faede3a20c9bf4093a4b9d33c4c6d97ef6ca5a9 2.png and /dev/null differ diff --git a/Uplift/Resources/Assets.xcassets/mountain_front.imageset/1faede3a20c9bf4093a4b9d33c4c6d97ef6ca5a9.png b/Uplift/Resources/Assets.xcassets/mountain_front.imageset/1faede3a20c9bf4093a4b9d33c4c6d97ef6ca5a9.png deleted file mode 100644 index 118a385..0000000 Binary files a/Uplift/Resources/Assets.xcassets/mountain_front.imageset/1faede3a20c9bf4093a4b9d33c4c6d97ef6ca5a9.png and /dev/null differ diff --git a/Uplift/Resources/Assets.xcassets/mountain_front.imageset/Contents.json b/Uplift/Resources/Assets.xcassets/mountain_front.imageset/Contents.json index 8d4cff3..7efca35 100644 --- a/Uplift/Resources/Assets.xcassets/mountain_front.imageset/Contents.json +++ b/Uplift/Resources/Assets.xcassets/mountain_front.imageset/Contents.json @@ -1,17 +1,17 @@ { "images" : [ { - "filename" : "1faede3a20c9bf4093a4b9d33c4c6d97ef6ca5a9.png", + "filename" : "front 3.png", "idiom" : "universal", "scale" : "1x" }, { - "filename" : "1faede3a20c9bf4093a4b9d33c4c6d97ef6ca5a9 1.png", + "filename" : "front2x.png", "idiom" : "universal", "scale" : "2x" }, { - "filename" : "1faede3a20c9bf4093a4b9d33c4c6d97ef6ca5a9 2.png", + "filename" : "front3x.png", "idiom" : "universal", "scale" : "3x" } diff --git a/Uplift/Resources/Assets.xcassets/mountain_front.imageset/front 1.png b/Uplift/Resources/Assets.xcassets/mountain_front.imageset/front 1.png new file mode 100644 index 0000000..2309a17 Binary files /dev/null and b/Uplift/Resources/Assets.xcassets/mountain_front.imageset/front 1.png differ diff --git a/Uplift/Resources/Assets.xcassets/mountain_front.imageset/front 2.png b/Uplift/Resources/Assets.xcassets/mountain_front.imageset/front 2.png new file mode 100644 index 0000000..2309a17 Binary files /dev/null and b/Uplift/Resources/Assets.xcassets/mountain_front.imageset/front 2.png differ diff --git a/Uplift/Resources/Assets.xcassets/mountain_front.imageset/front 3.png b/Uplift/Resources/Assets.xcassets/mountain_front.imageset/front 3.png new file mode 100644 index 0000000..2309a17 Binary files /dev/null and b/Uplift/Resources/Assets.xcassets/mountain_front.imageset/front 3.png differ diff --git a/Uplift/Resources/Assets.xcassets/mountain_front.imageset/front.png b/Uplift/Resources/Assets.xcassets/mountain_front.imageset/front.png new file mode 100644 index 0000000..2309a17 Binary files /dev/null and b/Uplift/Resources/Assets.xcassets/mountain_front.imageset/front.png differ diff --git a/Uplift/Resources/Assets.xcassets/mountain_front.imageset/front2x.png b/Uplift/Resources/Assets.xcassets/mountain_front.imageset/front2x.png new file mode 100644 index 0000000..3d3d70d Binary files /dev/null and b/Uplift/Resources/Assets.xcassets/mountain_front.imageset/front2x.png differ diff --git a/Uplift/Resources/Assets.xcassets/mountain_front.imageset/front3x.png b/Uplift/Resources/Assets.xcassets/mountain_front.imageset/front3x.png new file mode 100644 index 0000000..8fc16e7 Binary files /dev/null and b/Uplift/Resources/Assets.xcassets/mountain_front.imageset/front3x.png differ diff --git a/Uplift/Utils/Constants.swift b/Uplift/Utils/Constants.swift index cd21dcb..938db0d 100644 --- a/Uplift/Utils/Constants.swift +++ b/Uplift/Utils/Constants.swift @@ -173,6 +173,11 @@ struct Constants { static let whistleSolid = Image("whistle_solid") static let settings = Image("settings") static let profileEmpty = Image("profile_empty") + static let introBackground = Image("intro_background") + static let mountainBack = Image("mountain_back") + static let mountainFront = Image("mountain_front") + static let appDevLogoWhite = Image("appdev_logo_white") + static let logoSunset = Image("logo_sunset") } /// Padding amounts used in Uplift. @@ -235,4 +240,59 @@ struct Constants { static let skippedLogin = "skippedLogin" } + /// Constants used in IntroAnimation + enum IntroAnimation { + static let entranceDuration: Double = 1.0 + static let entranceDelay: Double = 0.1 + + static let appDevLogoWidth: CGFloat = 164 + static let appDevLogoHeight: CGFloat = 24 + static let appDevLogoPadding: Double = 50.0 + + static let introDuration: Double = 1.5 + static let transitionDuration: Double = 0.35 + } + + /// Constants used in SignIn + enum SignIn { + static let backgroundTrailingPadding: CGFloat = 51 + + static let backgroundFadeDuration: Double = 1 + static let backgroundFadeDelay: Double = 1 + static let contentFadeDuration: Double = 1 + static let contentFadeDelay: Double = 2 + static let initialFadeDuration: Double = 0.3 + + static let cardSpacing: CGFloat = 12 + static let cardYOffset: CGFloat = 200 + static let firstCardDelay: Double = 2.5 + static let secondCardDelay: Double = 3 + static let thirdCardDelay: Double = 3.5 + static let cardsHorizontalPadding: CGFloat = 76 + + static let buttonHorizontalPadding: CGFloat = 46 + static let buttonVerticalPadding: CGFloat = 12 + static let buttonCornerRadius: CGFloat = 38 + + static let cardPadding: CGFloat = 12 + static let cardCornerRadius: CGFloat = 8 + + static let headerTopPadding: CGFloat = 212 + static let loginLabelTopPadding: CGFloat = 89 + static let cardsTopPadding: CGFloat = 24 + + static let spacerMinLength: CGFloat = 16 + + static let transitionedLogoWidth: CGFloat = 130 + static let transitionedLogoHeight: CGFloat = 115 + + static let enteredLogoWidth: CGFloat = 173.14737 + static let enteredLogoHeight: CGFloat = 152.79259 + + static let transitionedLogoTopPadding: CGFloat = 10 + static let transitionedLogoYOffset: CGFloat = 15 + static let enteredLogoYOffset: CGFloat = 160 + static let hiddenLogoYOffset: CGFloat = 900 + + } } diff --git a/Uplift/Utils/Custom/CustomLoadingView.swift b/Uplift/Utils/Custom/CustomLoadingView.swift index 8631ffc..d00509a 100644 --- a/Uplift/Utils/Custom/CustomLoadingView.swift +++ b/Uplift/Utils/Custom/CustomLoadingView.swift @@ -54,16 +54,14 @@ struct CustomLoadingView: View { private var spinningSpinner: some View { Constants.Images.spokes - .resizable() - .aspectRatio(contentMode: .fit) + .resizable().scaledToFit() .frame(width: 175, height: 175) .rotationEffect(.degrees(rotation)) } private var bobbingLogo: some View { Constants.Images.logoTransparent - .resizable() - .aspectRatio(contentMode: .fit) + .resizable().scaledToFit() .frame(width: 200, height: 176) .shadow(color: .orange.opacity(0.5), radius: 20) .offset(y: isAnimating ? -8 : 8) diff --git a/Uplift/ViewModels/IntroAnimationViewModel.swift b/Uplift/ViewModels/IntroAnimationViewModel.swift new file mode 100644 index 0000000..eee3748 --- /dev/null +++ b/Uplift/ViewModels/IntroAnimationViewModel.swift @@ -0,0 +1,58 @@ +// +// IntroAnimationViewModel.swift +// Uplift +// +// Created by Kaylee Ulep on 9/11/26. +// Copyright © 2026 Cornell AppDev. All rights reserved. +// + +import SwiftUI + +@MainActor +final class IntroAnimationViewModel: ObservableObject { + + // MARK: - Properties + + @Published var hasEntered = false + @Published var isFadingOut = false + + // MARK: - Helpers + + func appDevLogoYOffset(for height: CGFloat) -> CGFloat { + hasEntered ? 0 : height + } + + // MARK: - Animation + + func startAnimation( + for height: CGFloat, + onTransition: (() -> Void)?, + onFinished: (() -> Void)? + ) async { + guard height > 0 else { return } + + hasEntered = true + + try? await Task.sleep( + for: .seconds(Constants.IntroAnimation.introDuration) + ) + + guard !Task.isCancelled else { return } + + withAnimation( + .smooth(duration: Constants.IntroAnimation.transitionDuration) + ) { + isFadingOut = true + } + + onTransition?() + + try? await Task.sleep( + for: .seconds(Constants.IntroAnimation.transitionDuration) + ) + + guard !Task.isCancelled else { return } + + onFinished?() + } +} diff --git a/Uplift/ViewModels/SignInViewModel.swift b/Uplift/ViewModels/SignInViewModel.swift new file mode 100644 index 0000000..ea3bb88 --- /dev/null +++ b/Uplift/ViewModels/SignInViewModel.swift @@ -0,0 +1,96 @@ +// +// SignInViewModel.swift +// Uplift +// +// Created by Kaylee Ulep on 9/11/26. +// Copyright © 2026 Cornell AppDev. All rights reserved. +// + +import SwiftUI + +@MainActor +final class SignInAnimationViewModel: ObservableObject { + + // MARK: - Phase + + enum Phase { + case hidden + case entered + case transitioningToSignIn + case finished + } + + @Published var phase: Phase + + init(hasShownIntro: Bool) { + phase = hasShownIntro ? .finished : .hidden + } + + // MARK: - Computed Properties + + var showIntro: Bool { + phase != .finished + } + + var introLogoEntered: Bool { + switch phase { + case .entered, .transitioningToSignIn, .finished: + return true + case .hidden: + return false + } + } + + var isTransitioningToSignIn: Bool { + switch phase { + case .transitioningToSignIn, .finished: + return true + case .hidden, .entered: + return false + } + } + + var mainLogoSize: CGSize { + isTransitioningToSignIn + ? CGSize(width: Constants.SignIn.transitionedLogoWidth, height: Constants.SignIn.transitionedLogoHeight) + : CGSize(width: Constants.SignIn.enteredLogoWidth, height: Constants.SignIn.enteredLogoHeight) + } + + var mainLogoTopPadding: CGFloat { + isTransitioningToSignIn ? Constants.SignIn.transitionedLogoTopPadding : 0 + } + + var mainLogoYOffset: CGFloat { + if isTransitioningToSignIn { + return Constants.SignIn.transitionedLogoYOffset + } + + if introLogoEntered { + return Constants.SignIn.enteredLogoYOffset + } + + return Constants.SignIn.hiddenLogoYOffset + } + + // MARK: - Animation + + func startIntroLogoAnimation() async { + guard phase == .hidden else { return } + + await Task.yield() + + phase = .entered + } + + func transitionToSignIn() { + withAnimation( + .smooth(duration: Constants.IntroAnimation.transitionDuration) + ) { + phase = .transitioningToSignIn + } + } + + func finish() { + phase = .finished + } +} diff --git a/Uplift/Views/Classes/ClassDetailView.swift b/Uplift/Views/Classes/ClassDetailView.swift index a3f7fed..6d94ca7 100644 --- a/Uplift/Views/Classes/ClassDetailView.swift +++ b/Uplift/Views/Classes/ClassDetailView.swift @@ -88,8 +88,7 @@ struct ClassDetailView: View { .placeholder { Constants.Colors.black } - .resizable() - .aspectRatio(contentMode: .fill) + .resizable().scaledToFill() .stretchy(geometry) } .frame(height: 360) diff --git a/Uplift/Views/Home/GymDetailView.swift b/Uplift/Views/Home/GymDetailView.swift index 8de7e16..812112d 100644 --- a/Uplift/Views/Home/GymDetailView.swift +++ b/Uplift/Views/Home/GymDetailView.swift @@ -86,8 +86,7 @@ struct GymDetailView: View { .placeholder { Constants.Colors.gray01 } - .resizable() - .aspectRatio(contentMode: .fill) + .resizable().scaledToFill() .stretchy(geometry) } .frame(height: 330) diff --git a/Uplift/Views/IntroAnimationView.swift b/Uplift/Views/IntroAnimationView.swift new file mode 100644 index 0000000..dca9c14 --- /dev/null +++ b/Uplift/Views/IntroAnimationView.swift @@ -0,0 +1,110 @@ +import SwiftUI + +struct IntroAnimationView: View { + + // MARK: - Properties + + @StateObject private var viewModel = IntroAnimationViewModel() + + var onTransition: (() -> Void)? + var onFinished: (() -> Void)? + + // MARK: - UI + + var body: some View { + GeometryReader { geo in + ZStack { + backgroundImage + + ZStack(alignment: .bottom) { + Constants.Images.mountainBack + .resizable() + .scaledToFill() + .frame(maxWidth: .infinity) + .clipped() + .offset( + y: viewModel.hasEntered ? 0 : geo.size.height + ) + .animation( + .easeOut( + duration: Constants.IntroAnimation.entranceDuration + ), + value: viewModel.hasEntered + ) + + Constants.Images.mountainFront + .resizable() + .scaledToFill() + .frame(maxWidth: .infinity) + .clipped() + .offset( + y: viewModel.hasEntered ? 0 : geo.size.height + ) + .animation( + .easeOut( + duration: Constants.IntroAnimation.entranceDuration + ) + .delay(Constants.IntroAnimation.entranceDelay), + value: viewModel.hasEntered + ) + + appDevLogo(in: geo) + } + .opacity(viewModel.isFadingOut ? 0 : 1) + .animation( + .easeInOut( + duration: Constants.IntroAnimation.transitionDuration + ), + value: viewModel.isFadingOut + ) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .clipped() + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .ignoresSafeArea() + .task { + await viewModel.startAnimation( + for: geo.size.height, + onTransition: onTransition, + onFinished: onFinished + ) + } + } + } + + // MARK: - Views + + private var backgroundImage: some View { + Constants.Images.introBackground + .resizable() + .scaledToFill() + .frame(maxWidth: .infinity, maxHeight: .infinity) + .clipped() + .opacity(viewModel.hasEntered && !viewModel.isFadingOut ? 1 : 0) + .animation(.easeInOut(duration: Constants.IntroAnimation.transitionDuration), value: viewModel.isFadingOut) + } + + private func appDevLogo(in geo: GeometryProxy) -> some View { + Constants.Images.appDevLogoWhite + .resizable() + .scaledToFit() + .frame( + width: Constants.IntroAnimation.appDevLogoWidth, + height: Constants.IntroAnimation.appDevLogoHeight + ) + .opacity(viewModel.hasEntered ? 1 : 0) + .animation( + .easeOut( + duration: Constants.IntroAnimation.entranceDuration + ) + .delay(Constants.IntroAnimation.entranceDelay), + value: viewModel.hasEntered + ) + .padding(Constants.IntroAnimation.appDevLogoPadding) + } + +} + +#Preview { + IntroAnimationView(onFinished: {}) +} diff --git a/Uplift/Views/Onboarding/SignInView.swift b/Uplift/Views/Onboarding/SignInView.swift index 829bf35..35581e6 100644 --- a/Uplift/Views/Onboarding/SignInView.swift +++ b/Uplift/Views/Onboarding/SignInView.swift @@ -17,32 +17,70 @@ struct SignInView: View { @StateObject private var loginViewModel = LoginViewModel() @State private var animateElements: Bool = false + static var hasShownIntro = false + + @StateObject private var viewModel = SignInAnimationViewModel( + hasShownIntro: SignInView.hasShownIntro + ) + // MARK: - UI var body: some View { + ZStack { + signInContent + + if viewModel.showIntro { + IntroAnimationView( + onTransition: viewModel.transitionToSignIn, + onFinished: finishIntro + ) + } + + mainLogo + + } + .task { + await viewModel.startIntroLogoAnimation() + } + } + + private var signInContent: some View { ZStack(alignment: .top) { Constants.Images.backgroundEllipse .resizable() .scaledToFit() - .padding(.trailing, 51) + .padding(.trailing, Constants.SignIn.backgroundTrailingPadding) .ignoresSafeArea(edges: .top) .opacity(animateElements ? 1 : 0) - .animation(.easeIn(duration: 1).delay(1), value: animateElements) + .animation( + .easeIn(duration: Constants.SignIn.backgroundFadeDuration) + .delay(Constants.SignIn.backgroundFadeDelay), + value: animateElements + ) VStack { signInHeader loginButton - Spacer(minLength: 16) + Spacer(minLength: Constants.SignIn.spacerMinLength) skipButton } } .background(Color.white) .onAppear { - withAnimation(.easeIn(duration: 0.3)) { - animateElements = true + if viewModel.showIntro { + var transaction = Transaction() + transaction.disablesAnimations = true + + withTransaction(transaction) { + animateElements = true + } + } else { + withAnimation(.easeIn(duration: Constants.SignIn.initialFadeDuration)) { + animateElements = true + } } } } @@ -60,7 +98,11 @@ struct SignInView: View { .foregroundColor(Constants.Colors.gray04) } .opacity(animateElements ? 1 : 0) - .animation(.easeIn(duration: 1).delay(2), value: animateElements) + .animation( + .easeIn(duration: Constants.SignIn.contentFadeDuration) + .delay(Constants.SignIn.contentFadeDelay), + value: animateElements + ) } private var loginButton: some View { @@ -106,32 +148,48 @@ struct SignInView: View { Text("Log in") .font(Constants.Fonts.h2) .foregroundColor(Constants.Colors.black) - .padding(.horizontal, 46) - .padding(.vertical, 12) + .padding(.horizontal, Constants.SignIn.buttonHorizontalPadding) + .padding(.vertical, Constants.SignIn.buttonVerticalPadding) .background(Constants.Colors.yellow) - .cornerRadius(38) + .cornerRadius(Constants.SignIn.buttonCornerRadius) .upliftShadow(Constants.Shadows.smallLight) } .opacity(animateElements ? 1 : 0) - .animation(.easeIn(duration: 1).delay(2), value: animateElements) + .animation( + .easeIn(duration: Constants.SignIn.contentFadeDuration) + .delay(Constants.SignIn.contentFadeDelay), + value: animateElements + ) } private var cardsView: some View { - VStack(spacing: 12) { + VStack(spacing: Constants.SignIn.cardSpacing) { createGoalsView .opacity(animateElements ? 1 : 0) - .offset(y: animateElements ? 0 : 200) - .animation(.spring(duration: 1).delay(2.5), value: animateElements) + .offset(y: animateElements ? 0 : Constants.SignIn.cardYOffset) + .animation( + .spring(duration: Constants.SignIn.contentFadeDuration) + .delay(Constants.SignIn.firstCardDelay), + value: animateElements + ) trackGoalsView .opacity(animateElements ? 1 : 0) - .offset(y: animateElements ? 0 : 200) - .animation(.spring(duration: 1).delay(3), value: animateElements) + .offset(y: animateElements ? 0 : Constants.SignIn.cardYOffset) + .animation( + .spring(duration: Constants.SignIn.contentFadeDuration) + .delay(Constants.SignIn.secondCardDelay), + value: animateElements + ) workoutHistoryView .opacity(animateElements ? 1 : 0) - .offset(y: animateElements ? 0 : 200) - .animation(.spring(duration: 1).delay(3.5), value: animateElements) + .offset(y: animateElements ? 0 : Constants.SignIn.cardYOffset) + .animation( + .spring(duration: Constants.SignIn.contentFadeDuration) + .delay(Constants.SignIn.thirdCardDelay), + value: animateElements + ) } - .padding(.horizontal, 76) + .padding(.horizontal, Constants.SignIn.cardsHorizontalPadding) } private var createGoalsView: some View { @@ -144,9 +202,9 @@ struct SignInView: View { Spacer() } - .padding(12) + .padding(Constants.SignIn.cardPadding) .background(.white) - .cornerRadius(8) + .cornerRadius(Constants.SignIn.cardCornerRadius) .upliftShadow(Constants.Shadows.smallLight) } @@ -160,9 +218,9 @@ struct SignInView: View { Spacer() } - .padding(12) + .padding(Constants.SignIn.cardPadding) .background(.white) - .cornerRadius(8) + .cornerRadius(Constants.SignIn.cardCornerRadius) .upliftShadow(Constants.Shadows.smallLight) } @@ -176,42 +234,104 @@ struct SignInView: View { Spacer() } - .padding(12) + .padding(Constants.SignIn.cardPadding) .background(.white) - .cornerRadius(8) + .cornerRadius(Constants.SignIn.cardCornerRadius) .upliftShadow(Constants.Shadows.smallLight) } private var signInHeader: some View { VStack { - Constants.Images.logo - .resizable() - .frame(width: 130, height: 115) - .clipShape(RoundedRectangle(cornerRadius: 12)) - .padding(32) - .padding(.top, 60) - .offset(y: animateElements ? 0 : 200) - .animation(.smooth(duration: 2), value: animateElements) Text("Find what uplifts you.") .font(Constants.Fonts.h1) .foregroundStyle(Constants.Colors.black) - .padding(.top, 62) + .padding(.top, Constants.SignIn.headerTopPadding) .opacity(animateElements ? 1 : 0) - .animation(.easeIn(duration: 1).delay(2), value: animateElements) + .animation( + .easeIn(duration: Constants.SignIn.contentFadeDuration) + .delay(Constants.SignIn.contentFadeDelay), + value: animateElements + ) Text("Log in to:") .font(Constants.Fonts.h2) .foregroundStyle(Constants.Colors.black) - .padding(.top, 89) + .padding(.top, Constants.SignIn.loginLabelTopPadding) .opacity(animateElements ? 1 : 0) - .animation(.easeIn(duration: 1).delay(2), value: animateElements) + .animation( + .easeIn(duration: Constants.SignIn.contentFadeDuration) + .delay(Constants.SignIn.contentFadeDelay), + value: animateElements + ) cardsView - .padding(.top, 24) + .padding(.top, Constants.SignIn.cardsTopPadding) Spacer() } + .frame(maxWidth: .infinity) + } + + private var mainLogo: some View { + VStack { + ZStack { + Constants.Images.logoSunset + .resizable() + .scaledToFit() + .opacity( + viewModel.isTransitioningToSignIn + ? 0 + : 1 + ) + + Constants.Images.logo + .resizable() + .scaledToFit() + .opacity( + viewModel.isTransitioningToSignIn + ? 1 + : 0 + ) + } + .frame( + width: viewModel.mainLogoSize.width, + height: viewModel.mainLogoSize.height + ) + .animation( + .easeInOut( + duration: Constants.IntroAnimation.transitionDuration + ), + value: viewModel.isTransitioningToSignIn + ) + + Spacer() + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .padding( + .top, + viewModel.mainLogoTopPadding + ) + .offset( + y: viewModel.mainLogoYOffset + ) + .animation( + .easeOut( + duration: Constants.IntroAnimation.entranceDuration + ), + value: viewModel.introLogoEntered + ) + .animation( + .smooth( + duration: Constants.IntroAnimation.transitionDuration + ), + value: viewModel.isTransitioningToSignIn + ) + } + + private func finishIntro() { + SignInView.hasShownIntro = true + viewModel.finish() } }