Przeglądaj źródła

Add Onboarding Completed animation before showing Main app view

Deniz Cengiz 1 rok temu
rodzic
commit
e5ee38e8f6

+ 16 - 4
Trio.xcodeproj/project.pbxproj

@@ -334,7 +334,7 @@
 		BA00D96F7B2FF169A06FB530 /* CGMSettingsStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C018D1680307A31C9ED7120 /* CGMSettingsStateModel.swift */; };
 		BD04ECCE2D29952A008C5FEB /* BolusProgressOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD04ECCD2D299522008C5FEB /* BolusProgressOverlay.swift */; };
 		BD0B2EF32C5998E600B3298F /* MealPresetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD0B2EF22C5998E600B3298F /* MealPresetView.swift */; };
-		BD10516D2DA986E1007C6D89 /* LogoAnimation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD10516C2DA986DC007C6D89 /* LogoAnimation.swift */; };
+		BD10516D2DA986E1007C6D89 /* PulsingLogoAnimation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD10516C2DA986DC007C6D89 /* PulsingLogoAnimation.swift */; };
 		BD1661312B82ADAB00256551 /* CustomProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD1661302B82ADAB00256551 /* CustomProgressView.swift */; };
 		BD249D862D42FBEC00412DEB /* GlucoseMetricsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD249D852D42FBE600412DEB /* GlucoseMetricsView.swift */; };
 		BD249D882D42FC0000412DEB /* BolusStatsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD249D872D42FBFB00412DEB /* BolusStatsView.swift */; };
@@ -600,6 +600,7 @@
 		DDAA29832D2D1D93006546A1 /* AdjustmentsRootView+Overrides.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDAA29822D2D1D7B006546A1 /* AdjustmentsRootView+Overrides.swift */; };
 		DDAA29852D2D1D9E006546A1 /* AdjustmentsRootView+TempTargets.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDAA29842D2D1D98006546A1 /* AdjustmentsRootView+TempTargets.swift */; };
 		DDB0E3712DB087B6004B826F /* PrivacyPolicyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB0E3702DB087B6004B826F /* PrivacyPolicyView.swift */; };
+		DDB0E3742DB1BAC1004B826F /* LogoBurstSplash.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB0E3732DB1BAC1004B826F /* LogoBurstSplash.swift */; };
 		DDB37CC52D05048F00D99BF4 /* ContactImageStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB37CC42D05048F00D99BF4 /* ContactImageStorage.swift */; };
 		DDB37CC72D05127500D99BF4 /* FontExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB37CC62D05127500D99BF4 /* FontExtensions.swift */; };
 		DDBD53FC2DAA903100F940A6 /* OverviewStepView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDBD53FB2DAA903100F940A6 /* OverviewStepView.swift */; };
@@ -1126,7 +1127,7 @@
 		BA49538D56989D8DA6FCF538 /* TargetsEditorDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TargetsEditorDataFlow.swift; sourceTree = "<group>"; };
 		BD04ECCD2D299522008C5FEB /* BolusProgressOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusProgressOverlay.swift; sourceTree = "<group>"; };
 		BD0B2EF22C5998E600B3298F /* MealPresetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MealPresetView.swift; sourceTree = "<group>"; };
-		BD10516C2DA986DC007C6D89 /* LogoAnimation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogoAnimation.swift; sourceTree = "<group>"; };
+		BD10516C2DA986DC007C6D89 /* PulsingLogoAnimation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PulsingLogoAnimation.swift; sourceTree = "<group>"; };
 		BD1661302B82ADAB00256551 /* CustomProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomProgressView.swift; sourceTree = "<group>"; };
 		BD1CF8B72C1A4A8400CB930A /* ConfigOverride.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = ConfigOverride.xcconfig; sourceTree = "<group>"; };
 		BD249D852D42FBE600412DEB /* GlucoseMetricsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseMetricsView.swift; sourceTree = "<group>"; };
@@ -1394,6 +1395,7 @@
 		DDAA29822D2D1D7B006546A1 /* AdjustmentsRootView+Overrides.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AdjustmentsRootView+Overrides.swift"; sourceTree = "<group>"; };
 		DDAA29842D2D1D98006546A1 /* AdjustmentsRootView+TempTargets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AdjustmentsRootView+TempTargets.swift"; sourceTree = "<group>"; };
 		DDB0E3702DB087B6004B826F /* PrivacyPolicyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyPolicyView.swift; sourceTree = "<group>"; };
+		DDB0E3732DB1BAC1004B826F /* LogoBurstSplash.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogoBurstSplash.swift; sourceTree = "<group>"; };
 		DDB37CC22D05044D00D99BF4 /* ContactTrickEntryStored+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ContactTrickEntryStored+CoreDataClass.swift"; sourceTree = "<group>"; };
 		DDB37CC32D05044D00D99BF4 /* ContactTrickEntryStored+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ContactTrickEntryStored+CoreDataProperties.swift"; sourceTree = "<group>"; };
 		DDB37CC42D05048F00D99BF4 /* ContactImageStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactImageStorage.swift; sourceTree = "<group>"; };
@@ -2765,6 +2767,7 @@
 		BD47FD152D88AAD80043966B /* View */ = {
 			isa = PBXGroup;
 			children = (
+				DDB0E3722DB1BABB004B826F /* Animations */,
 				DD4A00202DAEEEC400AB7387 /* OnboardingView+AlgorithmUtil.swift */,
 				DD3F1F882D9E078300DCE7B3 /* TherapySettingEditorView.swift */,
 				DDC38E0F2D9B376900ADCB46 /* OnboardingView+Util.swift */,
@@ -2779,7 +2782,6 @@
 			children = (
 				DD4A00222DAEF5CD00AB7387 /* AlgorithmSettings */,
 				DDBD53FB2DAA903100F940A6 /* OverviewStepView.swift */,
-				BD10516C2DA986DC007C6D89 /* LogoAnimation.swift */,
 				DDF691362DA30332008BF16C /* StartupGuideStepView.swift */,
 				DDF6905B2DA0AFC5008BF16C /* WelcomeStepView.swift */,
 				DDF6902B2DA028D3008BF16C /* DiagnosticsStepView.swift */,
@@ -3327,6 +3329,15 @@
 			path = AppVersionChecker;
 			sourceTree = "<group>";
 		};
+		DDB0E3722DB1BABB004B826F /* Animations */ = {
+			isa = PBXGroup;
+			children = (
+				BD10516C2DA986DC007C6D89 /* PulsingLogoAnimation.swift */,
+				DDB0E3732DB1BAC1004B826F /* LogoBurstSplash.swift */,
+			);
+			path = Animations;
+			sourceTree = "<group>";
+		};
 		DDC9B9962CFD2332003E7721 /* Nightscout */ = {
 			isa = PBXGroup;
 			children = (
@@ -4042,6 +4053,7 @@
 				38E44539274E411700EC9A94 /* Disk+UIImage.swift in Sources */,
 				3BAD36CC2D7D420E00CC298D /* CoreDataInitializationCoordinator.swift in Sources */,
 				DD4A00242DAEF5E400AB7387 /* AlgorithmSubstepView.swift in Sources */,
+				DDB0E3742DB1BAC1004B826F /* LogoBurstSplash.swift in Sources */,
 				388E595C25AD948C0019842D /* TrioApp.swift in Sources */,
 				38FEF3FC2737E53800574A46 /* MainStateModel.swift in Sources */,
 				DD1745352C55AE7E00211FAC /* TargetBehavoirRootView.swift in Sources */,
@@ -4472,7 +4484,7 @@
 				BDC530FF2D0F6BE300088832 /* ContactImageManager.swift in Sources */,
 				BDC531122D1060FA00088832 /* ContactImageDetailView.swift in Sources */,
 				DDE179552C910127003CDDB7 /* LoopStatRecord+CoreDataProperties.swift in Sources */,
-				BD10516D2DA986E1007C6D89 /* LogoAnimation.swift in Sources */,
+				BD10516D2DA986E1007C6D89 /* PulsingLogoAnimation.swift in Sources */,
 				DDE179562C910127003CDDB7 /* BolusStored+CoreDataClass.swift in Sources */,
 				DDE179572C910127003CDDB7 /* BolusStored+CoreDataProperties.swift in Sources */,
 				BD4D738D2D15A4080052227B /* TDDStored+CoreDataClass.swift in Sources */,

+ 47 - 28
Trio/Sources/Application/TrioApp.swift

@@ -35,7 +35,7 @@ extension Notification.Name {
     @State private var appState = AppState()
     @State private var showLoadingView = true
     @State private var showLoadingError = false
-    @State private var showOnboardingView = false
+    @State private var showOnboardingCompletedSplash = false
 
     // Dependencies Assembler
     // contain all dependencies Assemblies
@@ -103,7 +103,7 @@ extension Notification.Name {
             object: nil,
             queue: .main
         ) { [self] _ in
-            showOnboardingView = false
+            showOnboardingCompletedSplash = true
         }
 
         let submodulesInfo = BuildDetails.shared.submodules.map { key, value in
@@ -171,40 +171,59 @@ extension Notification.Name {
 
     var body: some Scene {
         WindowGroup {
-            if self.showLoadingView {
-                Main.LoadingView(showError: $showLoadingError, retry: retryCoreDataInitialization)
-                    .onAppear {
-                        if self.initState.complete {
+            ZStack {
+                if self.showLoadingView {
+                    Main.LoadingView(showError: $showLoadingError, retry: retryCoreDataInitialization)
+                        .onAppear {
+                            if self.initState.complete {
+                                Task { @MainActor in
+                                    try? await Task.sleep(for: .seconds(1.8))
+                                    self.showLoadingView = false
+                                }
+                            }
+                            if self.initState.error {
+                                self.showLoadingError = true
+                            }
+                        }
+                        .onReceive(Foundation.NotificationCenter.default.publisher(for: .initializationCompleted)) { _ in
                             Task { @MainActor in
                                 try? await Task.sleep(for: .seconds(1.8))
                                 self.showLoadingView = false
                             }
                         }
-                        if self.initState.error {
+                        .onReceive(Foundation.NotificationCenter.default.publisher(for: .initializationError)) { _ in
                             self.showLoadingError = true
                         }
+                } else if showOnboardingCompletedSplash {
+                    LogoBurstSplash(isActive: $showOnboardingCompletedSplash) {
+                        Main.RootView(resolver: resolver)
+                            .preferredColorScheme(colorScheme(for: colorSchemePreference))
+                            .environment(
+                                \.managedObjectContext,
+                                coreDataStack.persistentContainer.viewContext
+                            )
+                            .environment(appState)
+                            .environmentObject(Icons())
+                            .onOpenURL(perform: handleURL)
                     }
-                    .onReceive(Foundation.NotificationCenter.default.publisher(for: .initializationCompleted)) { _ in
-                        Task { @MainActor in
-                            try? await Task.sleep(for: .seconds(1.8))
-                            self.showLoadingView = false
-                        }
-                    }
-                    .onReceive(Foundation.NotificationCenter.default.publisher(for: .initializationError)) { _ in
-                        self.showLoadingError = true
-                    }
-            } else if onboardingManager.shouldShowOnboarding {
-                // Show onboarding if needed
-                Onboarding.RootView(resolver: resolver, onboardingManager: onboardingManager)
-                    .preferredColorScheme(colorScheme(for: .dark) ?? nil)
-                    .transition(.opacity)
-            } else {
-                Main.RootView(resolver: resolver)
-                    .preferredColorScheme(colorScheme(for: colorSchemePreference) ?? nil)
-                    .environment(\.managedObjectContext, coreDataStack.persistentContainer.viewContext)
-                    .environment(appState)
-                    .environmentObject(Icons())
-                    .onOpenURL(perform: handleURL)
+                } else if onboardingManager.shouldShowOnboarding {
+                    // Show onboarding if needed
+                    Onboarding.RootView(resolver: resolver, onboardingManager: onboardingManager)
+                        .preferredColorScheme(colorScheme(for: .dark) ?? nil)
+                        .transition(.opacity)
+                } else {
+                    Main.RootView(resolver: resolver)
+                        .preferredColorScheme(colorScheme(for: colorSchemePreference) ?? nil)
+                        .environment(\.managedObjectContext, coreDataStack.persistentContainer.viewContext)
+                        .environment(appState)
+                        .environmentObject(Icons())
+                        .onOpenURL(perform: handleURL)
+                }
+            }
+            .onReceive(Foundation.NotificationCenter.default.publisher(for: .onboardingCompleted)) { _ in
+                Task { @MainActor in
+                    self.showOnboardingCompletedSplash = true
+                }
             }
         }
         .onChange(of: scenePhase) { _, newScenePhase in

+ 3 - 0
Trio/Sources/Localizations/Main/Localizable.xcstrings

@@ -145898,6 +145898,9 @@
         }
       }
     },
+    "One last thing, before you begin..." : {
+
+    },
     "Only adjust these settings if you’re an advanced or returning user who knows what they’re doing." : {
 
     },

+ 156 - 0
Trio/Sources/Modules/Onboarding/View/Animations/LogoBurstSplash.swift

@@ -0,0 +1,156 @@
+import SwiftUI
+
+struct LogoBurstSplash<Content: View>: View {
+    @Binding var isActive: Bool
+    private let content: Content
+
+    @State private var logoScale: CGFloat = 0.5
+    @State private var logoOpacity: Double = 0
+    @State private var logoRotation: Double = 0
+    @State private var isPulsing = false
+
+    @State private var exploded = false
+    @State private var shapes: [BurstShape] = []
+    @State private var shapesOpacity: Double = 1
+
+    @State private var viewOpacity: Double = 1.0
+    @State private var splashScale: CGFloat = 1.0
+
+    init(isActive: Binding<Bool>, @ViewBuilder content: () -> Content) {
+        _isActive = isActive
+        self.content = content()
+    }
+
+    var body: some View {
+        GeometryReader { geo in
+            ZStack {
+                content
+                    .opacity(isActive ? 0 : 1)
+
+                if isActive {
+                    LinearGradient(
+                        gradient: Gradient(colors: [Color.bgDarkBlue, Color.bgDarkerDarkBlue]),
+                        startPoint: .top, endPoint: .bottom
+                    )
+                    .ignoresSafeArea()
+
+                    ZStack {
+                        // shards
+                        ForEach(shapes) { shape in
+                            Circle()
+                                .fill(shape.color)
+                                .frame(width: 6, height: 6)
+                                .position(x: geo.size.width / 2, y: geo.size.height / 2)
+                                .offset(
+                                    x: exploded ? shape.xOffset : 0,
+                                    y: exploded ? shape.yOffset : 0
+                                )
+                                .opacity(exploded ? shapesOpacity : 0)
+                                .animation(.easeOut(duration: 0.8), value: exploded)
+                                .animation(.easeIn(duration: 0.5), value: shapesOpacity)
+                        }
+
+                        // logo
+                        Image("trioCircledNoBackground")
+                            .resizable()
+                            .scaledToFit()
+                            .frame(width: 100, height: 100)
+                            .scaleEffect(isPulsing ? 1.1 : logoScale)
+                            .opacity(logoOpacity)
+                            .rotationEffect(.degrees(logoRotation))
+                            .animation(.easeInOut(duration: 1.0), value: logoScale)
+                            .animation(.easeInOut(duration: 1.0), value: logoOpacity)
+                            .animation(.linear(duration: 2.0), value: logoRotation)
+                            .animation(
+                                .easeInOut(duration: 0.8).repeatForever(autoreverses: true),
+                                value: isPulsing
+                            )
+                    }
+                    .scaleEffect(splashScale)
+                    .opacity(viewOpacity)
+                    .onAppear {
+                        shapes = BurstShape.createBurst(count: 250, in: geo.frame(in: .local))
+
+                        withAnimation {
+                            isPulsing = true
+                            logoOpacity = 1
+                            logoScale = 1
+                            logoRotation = 360
+                        }
+
+                        DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
+                            isPulsing = false
+                        }
+
+                        DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
+                            exploded = true
+                        }
+
+                        DispatchQueue.main.asyncAfter(deadline: .now() + 2.5) {
+                            withAnimation {
+                                logoOpacity = 0
+                                shapesOpacity = 0
+                            }
+                        }
+
+                        DispatchQueue.main.asyncAfter(deadline: .now() + 2.8) {
+                            withAnimation(.easeIn(duration: 0.6)) {
+                                viewOpacity = 0
+                                splashScale = 0.1
+                            }
+                        }
+
+                        // 5) Hide splash at 3.0s
+                        DispatchQueue.main.asyncAfter(deadline: .now() + 3.1) {
+                            isActive = false
+                        }
+                    }
+                }
+            }
+        }
+    }
+}
+
+private struct BurstShape: Identifiable {
+    let id = UUID()
+    let angle: Double
+    let distance: CGFloat
+    let color: Color
+
+    var xOffset: CGFloat { cos(angle) * distance }
+    var yOffset: CGFloat { sin(angle) * distance }
+
+    static func createBurst(count: Int, in rect: CGRect) -> [BurstShape] {
+        let gradientColors: [Color] = [
+            Color(red: 0.7216, green: 0.3412, blue: 1),
+            Color(red: 0.6235, green: 0.4235, blue: 0.9804),
+            Color(red: 0.4863, green: 0.5451, blue: 0.9529),
+            Color(red: 0.3412, green: 0.6667, blue: 0.9255),
+            Color(red: 0.2627, green: 0.7333, blue: 0.9137)
+        ]
+        return (0 ..< count).map { i in
+            let angle = Double.random(in: 0 ..< 360) * .pi / 180
+            let distance = CGFloat.random(
+                in: min(rect.width, rect.height) * 0.3 ... max(rect.width, rect.height) * 0.6
+            )
+            let color = gradientColors[i % gradientColors.count]
+            return BurstShape(angle: angle, distance: distance, color: color)
+        }
+    }
+}
+
+// MARK: Preview
+
+struct LogoBurstSplash_Previews: PreviewProvider {
+    static var previews: some View {
+        LogoBurstSplash(isActive: .constant(true)) {
+            ZStack {
+                Color.white.ignoresSafeArea()
+                Text("Main Content")
+                    .font(.largeTitle)
+                    .foregroundColor(.gray)
+            }
+        }
+        .previewDevice("iPhone 14 Pro")
+    }
+}

+ 1 - 1
Trio/Sources/Modules/Onboarding/View/OnboardingSteps/LogoAnimation.swift

@@ -1,5 +1,5 @@
 //
-//  LogoAnimation.swift
+//  PulsingLogoAnimation.swift
 //  Trio
 //
 //  Created by Marvin Polscheit on 11.04.25.

+ 10 - 8
Trio/Sources/Modules/Onboarding/View/OnboardingSteps/StartupGuideStepView.swift

@@ -48,14 +48,16 @@ struct StartupGuideStepView: View {
                 BulletPoint(String(localized: "We recommend reviewing them carefully — Trio will guide you step-by-step."))
             }.padding(.horizontal)
 
-            HStack {
-                Text("You can pause at any time. Just be aware: if you ")
-                    + Text("force quit").bold()
-                    + Text(" the app before finishing onboarding, ")
-                    + Text("your progress will not be saved.").bold()
-            }
-            .multilineTextAlignment(.leading)
-            .padding(.horizontal)
+            VStack(alignment: .leading, spacing: 10) {
+                Text("One last thing, before you begin...").bold()
+                HStack {
+                    Text("You can pause at any time. Just be aware: if you ")
+                        + Text("force quit").bold()
+                        + Text(" the app before finishing onboarding, ")
+                        + Text("your progress will not be saved.").bold()
+                }
+            }.multilineTextAlignment(.leading)
+                .padding(.horizontal)
 
             Divider()