179 lines
5.9 KiB
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()
|
|
}
|