Files
Ascently/ios/Ascently/ContentView.swift

179 lines
5.9 KiB
Swift

import SwiftUI
struct ContentView: View {
@StateObject private var dataManager = ClimbingDataManager.shared
@State private var selectedTab = 0
@Environment(\.scenePhase) private var scenePhase
@State private var notificationObservers: [NSObjectProtocol] = []
var body: some View {
TabView(selection: $selectedTab) {
SessionsView()
.tabItem {
Image(systemName: "play.fill")
Text("Sessions")
}
.tag(0)
ProblemsView()
.tabItem {
Image(systemName: "star.fill")
Text("Problems")
}
.tag(1)
AnalyticsView()
.tabItem {
Image(systemName: "chart.bar.fill")
Text("Analytics")
}
.tag(2)
GymsView()
.tabItem {
Image(systemName: "location.fill")
Text("Gyms")
}
.tag(3)
SettingsView()
.tabItem {
Image(systemName: "gear")
Text("Settings")
}
.tag(4)
}
.environmentObject(dataManager)
.environmentObject(MusicService.shared)
.onChange(of: scenePhase) { oldPhase, newPhase in
if newPhase == .active {
// Add slight delay to ensure app is fully loaded
Task {
try? await Task.sleep(nanoseconds: 200_000_000) // 0.2 seconds
dataManager.onAppBecomeActive()
// Re-verify health integration when app becomes active
await dataManager.healthKitService.verifyAndRestoreIntegration()
}
} else if newPhase == .background {
dataManager.onAppEnterBackground()
}
}
.onAppear {
setupNotificationObservers()
// Trigger auto-sync on app start only
dataManager.syncService.triggerAutoSync(dataManager: dataManager)
// Verify and restore health integration if it was previously enabled
Task {
await dataManager.healthKitService.verifyAndRestoreIntegration()
}
}
.onDisappear {
removeNotificationObservers()
}
.overlay(alignment: .top) {
if let message = dataManager.successMessage {
SuccessMessageView(message: message)
.transition(.move(edge: .top).combined(with: .opacity))
.animation(.easeInOut, value: dataManager.successMessage)
}
if let error = dataManager.errorMessage {
ErrorMessageView(message: error)
.transition(.move(edge: .top).combined(with: .opacity))
.animation(.easeInOut, value: dataManager.errorMessage)
}
}
}
private func setupNotificationObservers() {
// Listen for when the app will enter foreground
let willEnterForegroundObserver = NotificationCenter.default.addObserver(
forName: UIApplication.willEnterForegroundNotification,
object: nil,
queue: .main
) { _ in
Task { @MainActor in
AppLogger.info(
"App will enter foreground - preparing Live Activity check", tag: "Lifecycle")
// Small delay to ensure app is fully active
try? await Task.sleep(nanoseconds: 800_000_000) // 0.8 seconds
dataManager.onAppBecomeActive()
// Re-verify health integration when returning from background
await dataManager.healthKitService.verifyAndRestoreIntegration()
}
}
// Listen for when the app becomes active
let didBecomeActiveObserver = NotificationCenter.default.addObserver(
forName: UIApplication.didBecomeActiveNotification,
object: nil,
queue: .main
) { _ in
Task { @MainActor in
AppLogger.info(
"App did become active - checking Live Activity status", tag: "Lifecycle")
try? await Task.sleep(nanoseconds: 300_000_000) // 0.3 seconds
dataManager.onAppBecomeActive()
await dataManager.healthKitService.verifyAndRestoreIntegration()
}
}
notificationObservers = [willEnterForegroundObserver, didBecomeActiveObserver]
}
private func removeNotificationObservers() {
for observer in notificationObservers {
NotificationCenter.default.removeObserver(observer)
}
notificationObservers.removeAll()
}
}
struct SuccessMessageView: View {
let message: String
var body: some View {
HStack {
Image(systemName: "checkmark.circle.fill")
.foregroundColor(.green)
Text(message)
.font(.subheadline)
.foregroundColor(.primary)
}
.padding()
.background(
RoundedRectangle(cornerRadius: 12)
.fill(.regularMaterial)
.shadow(radius: 4)
)
.padding(.horizontal)
.padding(.top, 8)
}
}
struct ErrorMessageView: View {
let message: String
var body: some View {
HStack {
Image(systemName: "exclamationmark.triangle.fill")
.foregroundColor(.red)
Text(message)
.font(.subheadline)
.foregroundColor(.primary)
}
.padding()
.background(
RoundedRectangle(cornerRadius: 12)
.fill(.regularMaterial)
.shadow(radius: 4)
)
.padding(.horizontal)
.padding(.top, 8)
}
}
#Preview {
ContentView()
}