diff --git a/ios/OpenClimb.xcodeproj/project.pbxproj b/ios/OpenClimb.xcodeproj/project.pbxproj index 4698134..fefa0e6 100644 --- a/ios/OpenClimb.xcodeproj/project.pbxproj +++ b/ios/OpenClimb.xcodeproj/project.pbxproj @@ -396,7 +396,7 @@ CODE_SIGN_ENTITLEMENTS = OpenClimb/OpenClimb.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 12; + CURRENT_PROJECT_VERSION = 13; DEVELOPMENT_TEAM = 4BC9Y2LL4B; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; @@ -416,7 +416,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.2.1; + MARKETING_VERSION = 1.2.2; PRODUCT_BUNDLE_IDENTIFIER = com.atridad.OpenClimb; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -439,7 +439,7 @@ CODE_SIGN_ENTITLEMENTS = OpenClimb/OpenClimb.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 12; + CURRENT_PROJECT_VERSION = 13; DEVELOPMENT_TEAM = 4BC9Y2LL4B; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; @@ -459,7 +459,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.2.1; + MARKETING_VERSION = 1.2.2; PRODUCT_BUNDLE_IDENTIFIER = com.atridad.OpenClimb; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -481,7 +481,7 @@ ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; CODE_SIGN_ENTITLEMENTS = SessionStatusLiveExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 12; + CURRENT_PROJECT_VERSION = 13; DEVELOPMENT_TEAM = 4BC9Y2LL4B; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = SessionStatusLive/Info.plist; @@ -492,7 +492,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.2.1; + MARKETING_VERSION = 1.2.2; PRODUCT_BUNDLE_IDENTIFIER = com.atridad.OpenClimb.SessionStatusLive; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; @@ -511,7 +511,7 @@ ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; CODE_SIGN_ENTITLEMENTS = SessionStatusLiveExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 12; + CURRENT_PROJECT_VERSION = 13; DEVELOPMENT_TEAM = 4BC9Y2LL4B; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = SessionStatusLive/Info.plist; @@ -522,7 +522,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.2.1; + MARKETING_VERSION = 1.2.2; PRODUCT_BUNDLE_IDENTIFIER = com.atridad.OpenClimb.SessionStatusLive; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; diff --git a/ios/OpenClimb.xcodeproj/project.xcworkspace/xcuserdata/atridad.xcuserdatad/UserInterfaceState.xcuserstate b/ios/OpenClimb.xcodeproj/project.xcworkspace/xcuserdata/atridad.xcuserdatad/UserInterfaceState.xcuserstate index 328c1d7..7c52966 100644 Binary files a/ios/OpenClimb.xcodeproj/project.xcworkspace/xcuserdata/atridad.xcuserdatad/UserInterfaceState.xcuserstate and b/ios/OpenClimb.xcodeproj/project.xcworkspace/xcuserdata/atridad.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/ios/OpenClimb/Utils/IconTestView.swift b/ios/OpenClimb/Utils/IconTestView.swift index 0332043..a2dd554 100644 --- a/ios/OpenClimb/Utils/IconTestView.swift +++ b/ios/OpenClimb/Utils/IconTestView.swift @@ -1,4 +1,3 @@ - import Combine import SwiftUI @@ -11,7 +10,7 @@ import SwiftUI @State private var testResults: [String] = [] var body: some View { - NavigationView { + NavigationStack { List { StatusSection() @@ -364,7 +363,7 @@ import SwiftUI @Environment(\.colorScheme) private var colorScheme var body: some View { - NavigationView { + NavigationStack { VStack(spacing: 30) { Text("Icon Appearance Comparison") .font(.title2) diff --git a/ios/OpenClimb/Views/AddEdit/AddAttemptView.swift b/ios/OpenClimb/Views/AddEdit/AddAttemptView.swift index 3c3afae..36d7a0b 100644 --- a/ios/OpenClimb/Views/AddEdit/AddAttemptView.swift +++ b/ios/OpenClimb/Views/AddEdit/AddAttemptView.swift @@ -42,7 +42,7 @@ struct AddAttemptView: View { } var body: some View { - NavigationView { + NavigationStack { Form { if !showingCreateProblem { ProblemSelectionSection() @@ -597,7 +597,7 @@ struct ProblemExpandedView: View { @State private var selectedImageIndex = 0 var body: some View { - NavigationView { + NavigationStack { ScrollView { VStack(alignment: .leading, spacing: 16) { // Images @@ -735,7 +735,7 @@ struct EditAttemptView: View { } var body: some View { - NavigationView { + NavigationStack { Form { if !showingCreateProblem { ProblemSelectionSection() diff --git a/ios/OpenClimb/Views/AddEdit/AddEditGymView.swift b/ios/OpenClimb/Views/AddEdit/AddEditGymView.swift index d0f5f69..37b7d41 100644 --- a/ios/OpenClimb/Views/AddEdit/AddEditGymView.swift +++ b/ios/OpenClimb/Views/AddEdit/AddEditGymView.swift @@ -1,4 +1,3 @@ - import SwiftUI struct AddEditGymView: View { @@ -34,7 +33,7 @@ struct AddEditGymView: View { } var body: some View { - NavigationView { + NavigationStack { Form { BasicInfoSection() ClimbTypesSection() diff --git a/ios/OpenClimb/Views/AddEdit/AddEditProblemView.swift b/ios/OpenClimb/Views/AddEdit/AddEditProblemView.swift index 78e91ec..c075edc 100644 --- a/ios/OpenClimb/Views/AddEdit/AddEditProblemView.swift +++ b/ios/OpenClimb/Views/AddEdit/AddEditProblemView.swift @@ -55,7 +55,7 @@ struct AddEditProblemView: View { } var body: some View { - NavigationView { + NavigationStack { Form { GymSelectionSection() BasicInfoSection() diff --git a/ios/OpenClimb/Views/AddEdit/AddEditSessionView.swift b/ios/OpenClimb/Views/AddEdit/AddEditSessionView.swift index 724b482..ba0df52 100644 --- a/ios/OpenClimb/Views/AddEdit/AddEditSessionView.swift +++ b/ios/OpenClimb/Views/AddEdit/AddEditSessionView.swift @@ -1,4 +1,3 @@ - import SwiftUI struct AddEditSessionView: View { @@ -21,7 +20,7 @@ struct AddEditSessionView: View { } var body: some View { - NavigationView { + NavigationStack { Form { GymSelectionSection() SessionDetailsSection() diff --git a/ios/OpenClimb/Views/AnalyticsView.swift b/ios/OpenClimb/Views/AnalyticsView.swift index fa1aac1..f1f7e1d 100644 --- a/ios/OpenClimb/Views/AnalyticsView.swift +++ b/ios/OpenClimb/Views/AnalyticsView.swift @@ -4,7 +4,7 @@ struct AnalyticsView: View { @EnvironmentObject var dataManager: ClimbingDataManager var body: some View { - NavigationView { + NavigationStack { ScrollView { LazyVStack(spacing: 20) { OverallStatsSection() diff --git a/ios/OpenClimb/Views/Detail/ProblemDetailView.swift b/ios/OpenClimb/Views/Detail/ProblemDetailView.swift index fe38fa1..a909cbb 100644 --- a/ios/OpenClimb/Views/Detail/ProblemDetailView.swift +++ b/ios/OpenClimb/Views/Detail/ProblemDetailView.swift @@ -420,7 +420,7 @@ struct ImageViewerView: View { } var body: some View { - NavigationView { + NavigationStack { TabView(selection: $currentIndex) { ForEach(imagePaths.indices, id: \.self) { index in ProblemDetailImageFullView(imagePath: imagePaths[index]) diff --git a/ios/OpenClimb/Views/Detail/SessionDetailView.swift b/ios/OpenClimb/Views/Detail/SessionDetailView.swift index 84a8e4d..383a97f 100644 --- a/ios/OpenClimb/Views/Detail/SessionDetailView.swift +++ b/ios/OpenClimb/Views/Detail/SessionDetailView.swift @@ -9,24 +9,11 @@ struct SessionDetailView: View { @State private var showingAddAttempt = false @State private var editingAttempt: Attempt? @State private var attemptToDelete: Attempt? - @State private var currentTime = Date() private var session: ClimbSession? { dataManager.session(withId: sessionId) } - private func startTimer() { - // Update every 5 seconds instead of 1 second for better performance - timer = Timer.scheduledTimer(withTimeInterval: 5.0, repeats: true) { _ in - currentTime = Date() - } - } - - private func stopTimer() { - timer?.invalidate() - timer = nil - } - private var gym: Gym? { guard let session = session else { return nil } return dataManager.gym(withId: session.gymId) @@ -47,14 +34,12 @@ struct SessionDetailView: View { calculateSessionStats() } - @State private var timer: Timer? - var body: some View { ScrollView { LazyVStack(spacing: 20) { if let session = session, let gym = gym { SessionHeaderCard( - session: session, gym: gym, stats: sessionStats, currentTime: currentTime) + session: session, gym: gym, stats: sessionStats) SessionStatsCard(stats: sessionStats) @@ -69,12 +54,7 @@ struct SessionDetailView: View { } .padding() } - .onAppear { - startTimer() - } - .onDisappear { - stopTimer() - } + .navigationTitle("Session Details") .navigationBarTitleDisplayMode(.inline) .toolbar { @@ -182,7 +162,6 @@ struct SessionHeaderCard: View { let session: ClimbSession let gym: Gym let stats: SessionStats - let currentTime: Date var body: some View { VStack(alignment: .leading, spacing: 16) { @@ -197,9 +176,13 @@ struct SessionHeaderCard: View { if session.status == .active { if let startTime = session.startTime { - Text("Duration: \(formatDuration(from: startTime, to: currentTime))") + Text("Duration: ") .font(.subheadline) .foregroundColor(.secondary) + + Text(timerInterval: startTime...Date.distantFuture, countsDown: false) + .font(.subheadline) + .foregroundColor(.secondary) + .monospacedDigit() } } else if let duration = session.duration { Text("Duration: \(duration) minutes") @@ -246,20 +229,6 @@ struct SessionHeaderCard: View { return formatter.string(from: date) } - private func formatDuration(from start: Date, to end: Date) -> String { - let interval = end.timeIntervalSince(start) - let hours = Int(interval) / 3600 - let minutes = Int(interval) % 3600 / 60 - let seconds = Int(interval) % 60 - - if hours > 0 { - return String(format: "%dh %dm %ds", hours, minutes, seconds) - } else if minutes > 0 { - return String(format: "%dm %ds", minutes, seconds) - } else { - return String(format: "%ds", seconds) - } - } } struct SessionStatsCard: View { diff --git a/ios/OpenClimb/Views/GymsView.swift b/ios/OpenClimb/Views/GymsView.swift index 3937ae5..3c4d72e 100644 --- a/ios/OpenClimb/Views/GymsView.swift +++ b/ios/OpenClimb/Views/GymsView.swift @@ -5,7 +5,7 @@ struct GymsView: View { @State private var showingAddGym = false var body: some View { - NavigationView { + NavigationStack { VStack { if dataManager.gyms.isEmpty { EmptyGymsView() diff --git a/ios/OpenClimb/Views/LiveActivityDebugView.swift b/ios/OpenClimb/Views/LiveActivityDebugView.swift index 5d75989..bd08921 100644 --- a/ios/OpenClimb/Views/LiveActivityDebugView.swift +++ b/ios/OpenClimb/Views/LiveActivityDebugView.swift @@ -9,7 +9,7 @@ struct LiveActivityDebugView: View { @State private var isTestRunning = false var body: some View { - NavigationView { + NavigationStack { VStack(alignment: .leading, spacing: 20) { // Header diff --git a/ios/OpenClimb/Views/ProblemsView.swift b/ios/OpenClimb/Views/ProblemsView.swift index a4458f4..59b41a6 100644 --- a/ios/OpenClimb/Views/ProblemsView.swift +++ b/ios/OpenClimb/Views/ProblemsView.swift @@ -6,6 +6,8 @@ struct ProblemsView: View { @State private var selectedClimbType: ClimbType? @State private var selectedGym: Gym? @State private var searchText = "" + @State private var showingSearch = false + @FocusState private var isSearchFocused: Bool private var filteredProblems: [Problem] { var filtered = dataManager.problems @@ -38,29 +40,67 @@ struct ProblemsView: View { } var body: some View { - NavigationView { - VStack(spacing: 0) { - if !dataManager.problems.isEmpty { - FilterSection( - selectedClimbType: $selectedClimbType, - selectedGym: $selectedGym, - filteredProblems: filteredProblems - ) - .padding() - .background(.regularMaterial) - } + NavigationStack { + Group { + VStack(spacing: 0) { + if showingSearch { + HStack(spacing: 8) { + Image(systemName: "magnifyingglass") + .foregroundColor(.secondary) + .font(.system(size: 16, weight: .medium)) - if filteredProblems.isEmpty { - EmptyProblemsView( - isEmpty: dataManager.problems.isEmpty, - isFiltered: !dataManager.problems.isEmpty - ) - } else { - ProblemsList(problems: filteredProblems) + TextField("Search problems...", text: $searchText) + .textFieldStyle(.plain) + .font(.system(size: 16)) + .focused($isSearchFocused) + .submitLabel(.search) + } + .padding(.horizontal, 12) + .padding(.vertical, 10) + .background { + if #available(iOS 18.0, *) { + RoundedRectangle(cornerRadius: 12) + .fill(.regularMaterial) + .overlay { + RoundedRectangle(cornerRadius: 12) + .stroke(.quaternary, lineWidth: 0.5) + } + } else { + RoundedRectangle(cornerRadius: 10) + .fill(Color(.systemGray6)) + .overlay { + RoundedRectangle(cornerRadius: 10) + .stroke(Color(.systemGray4), lineWidth: 0.5) + } + } + } + .padding(.horizontal) + .padding(.top, 8) + .animation(.easeInOut(duration: 0.3), value: showingSearch) + } + + if !dataManager.problems.isEmpty && !showingSearch { + FilterSection( + selectedClimbType: $selectedClimbType, + selectedGym: $selectedGym, + filteredProblems: filteredProblems + ) + .padding() + .background(.regularMaterial) + } + + if filteredProblems.isEmpty { + EmptyProblemsView( + isEmpty: dataManager.problems.isEmpty, + isFiltered: !dataManager.problems.isEmpty + ) + } else { + ProblemsList(problems: filteredProblems) + } } } .navigationTitle("Problems") - .searchable(text: $searchText, prompt: "Search problems...") + .navigationBarTitleDisplayMode(.automatic) .toolbar { ToolbarItemGroup(placement: .navigationBarTrailing) { if dataManager.isSyncing { @@ -81,6 +121,22 @@ struct ProblemsView: View { ) } + Button(action: { + withAnimation(.easeInOut(duration: 0.3)) { + showingSearch.toggle() + if showingSearch { + isSearchFocused = true + } else { + searchText = "" + isSearchFocused = false + } + } + }) { + Image(systemName: showingSearch ? "xmark.circle.fill" : "magnifyingglass") + .font(.system(size: 16, weight: .medium)) + .foregroundColor(showingSearch ? .secondary : .blue) + } + if !dataManager.gyms.isEmpty { Button("Add") { showingAddProblem = true diff --git a/ios/OpenClimb/Views/SessionsView.swift b/ios/OpenClimb/Views/SessionsView.swift index 898d458..3852036 100644 --- a/ios/OpenClimb/Views/SessionsView.swift +++ b/ios/OpenClimb/Views/SessionsView.swift @@ -6,7 +6,7 @@ struct SessionsView: View { @State private var showingAddSession = false var body: some View { - NavigationView { + NavigationStack { Group { if dataManager.sessions.isEmpty && dataManager.activeSession == nil { EmptySessionsView() @@ -53,7 +53,6 @@ struct SessionsView: View { AddEditSessionView() } } - .navigationViewStyle(.stack) } } @@ -129,11 +128,8 @@ struct ActiveSessionBanner: View { let session: ClimbSession let gym: Gym @EnvironmentObject var dataManager: ClimbingDataManager - @State private var currentTime = Date() @State private var navigateToDetail = false - @State private var timer: Timer? - var body: some View { HStack { VStack(alignment: .leading, spacing: 4) { @@ -151,9 +147,10 @@ struct ActiveSessionBanner: View { .foregroundColor(.secondary) if let startTime = session.startTime { - Text(formatDuration(from: startTime, to: currentTime)) + Text(timerInterval: startTime...Date.distantFuture, countsDown: false) .font(.caption) .foregroundColor(.secondary) + .monospacedDigit() } } .frame(maxWidth: .infinity, alignment: .leading) @@ -180,42 +177,12 @@ struct ActiveSessionBanner: View { .fill(.green.opacity(0.1)) .stroke(.green.opacity(0.3), lineWidth: 1) ) - .onAppear { - startTimer() - } - .onDisappear { - stopTimer() - } + .navigationDestination(isPresented: $navigateToDetail) { SessionDetailView(sessionId: session.id) } } - private func formatDuration(from start: Date, to end: Date) -> String { - let interval = end.timeIntervalSince(start) - let hours = Int(interval) / 3600 - let minutes = Int(interval) % 3600 / 60 - let seconds = Int(interval) % 60 - - if hours > 0 { - return String(format: "%dh %dm %ds", hours, minutes, seconds) - } else if minutes > 0 { - return String(format: "%dm %ds", minutes, seconds) - } else { - return String(format: "%ds", seconds) - } - } - - private func startTimer() { - timer = Timer.scheduledTimer(withTimeInterval: 5.0, repeats: true) { _ in - currentTime = Date() - } - } - - private func stopTimer() { - timer?.invalidate() - timer = nil - } } struct SessionRow: View { diff --git a/ios/OpenClimb/Views/SettingsView.swift b/ios/OpenClimb/Views/SettingsView.swift index cd55dd4..becdc92 100644 --- a/ios/OpenClimb/Views/SettingsView.swift +++ b/ios/OpenClimb/Views/SettingsView.swift @@ -11,49 +11,52 @@ struct SettingsView: View { @State private var activeSheet: SheetType? var body: some View { - List { - SyncSection() - .environmentObject(dataManager.syncService) + NavigationStack { + List { + SyncSection() + .environmentObject(dataManager.syncService) - DataManagementSection( - activeSheet: $activeSheet - ) + DataManagementSection( + activeSheet: $activeSheet + ) - AppInfoSection() - } - .navigationTitle("Settings") - .toolbar { - ToolbarItem(placement: .navigationBarTrailing) { - if dataManager.isSyncing { - HStack(spacing: 2) { - ProgressView() - .progressViewStyle(CircularProgressViewStyle(tint: .blue)) - .scaleEffect(0.6) + AppInfoSection() + } + .navigationTitle("Settings") + .navigationBarTitleDisplayMode(.automatic) + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + if dataManager.isSyncing { + HStack(spacing: 2) { + ProgressView() + .progressViewStyle(CircularProgressViewStyle(tint: .blue)) + .scaleEffect(0.6) + } + .padding(.horizontal, 6) + .padding(.vertical, 3) + .background( + Circle() + .fill(.regularMaterial) + ) + .transition(.scale.combined(with: .opacity)) + .animation( + .easeInOut(duration: 0.2), value: dataManager.isSyncing + ) } - .padding(.horizontal, 6) - .padding(.vertical, 3) - .background( - Circle() - .fill(.regularMaterial) - ) - .transition(.scale.combined(with: .opacity)) - .animation( - .easeInOut(duration: 0.2), value: dataManager.isSyncing - ) } } - } - .sheet( - item: Binding( - get: { activeSheet }, - set: { activeSheet = $0 } - ) - ) { sheetType in - switch sheetType { - case .export(let data): - ExportDataView(data: data) - case .importData: - ImportDataView() + .sheet( + item: Binding( + get: { activeSheet }, + set: { activeSheet = $0 } + ) + ) { sheetType in + switch sheetType { + case .export(let data): + ExportDataView(data: data) + case .importData: + ImportDataView() + } } } } @@ -191,7 +194,7 @@ struct ExportDataView: View { @State private var isCreatingFile = true var body: some View { - NavigationView { + NavigationStack { VStack(spacing: 30) { if isCreatingFile { // Loading state - more prominent @@ -498,7 +501,7 @@ struct SyncSettingsView: View { @State private var testResultMessage = "" var body: some View { - NavigationView { + NavigationStack { Form { Section { TextField("Server URL", text: $serverURL) @@ -691,7 +694,7 @@ struct ImportDataView: View { @State private var showingDocumentPicker = false var body: some View { - NavigationView { + NavigationStack { VStack(spacing: 20) { Image(systemName: "square.and.arrow.down") .font(.system(size: 60))