diff --git a/ios/OpenClimb.xcodeproj/project.pbxproj b/ios/OpenClimb.xcodeproj/project.pbxproj
index 530d3a3..cf0d7f6 100644
--- a/ios/OpenClimb.xcodeproj/project.pbxproj
+++ b/ios/OpenClimb.xcodeproj/project.pbxproj
@@ -390,7 +390,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 3;
+ CURRENT_PROJECT_VERSION = 4;
DEVELOPMENT_TEAM = 4BC9Y2LL4B;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
@@ -410,7 +410,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
- MARKETING_VERSION = 1.0.0;
+ MARKETING_VERSION = 1.0.1;
PRODUCT_BUNDLE_IDENTIFIER = com.atridad.OpenClimb;
PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = YES;
@@ -430,7 +430,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 3;
+ CURRENT_PROJECT_VERSION = 4;
DEVELOPMENT_TEAM = 4BC9Y2LL4B;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
@@ -450,7 +450,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
- MARKETING_VERSION = 1.0.0;
+ MARKETING_VERSION = 1.0.1;
PRODUCT_BUNDLE_IDENTIFIER = com.atridad.OpenClimb;
PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = YES;
diff --git a/ios/OpenClimb.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/ios/OpenClimb.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
new file mode 100644
index 0000000..0c67376
--- /dev/null
+++ b/ios/OpenClimb.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
@@ -0,0 +1,5 @@
+
+
+
+
+
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 7292e37..e829cb1 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.xcodeproj/project.xcworkspace/xcuserdata/atridad.xcuserdatad/WorkspaceSettings.xcsettings b/ios/OpenClimb.xcodeproj/project.xcworkspace/xcuserdata/atridad.xcuserdatad/WorkspaceSettings.xcsettings
new file mode 100644
index 0000000..723a561
--- /dev/null
+++ b/ios/OpenClimb.xcodeproj/project.xcworkspace/xcuserdata/atridad.xcuserdatad/WorkspaceSettings.xcsettings
@@ -0,0 +1,16 @@
+
+
+
+
+ BuildLocationStyle
+ UseAppPreferences
+ CompilationCachingSetting
+ Default
+ CustomBuildLocationType
+ RelativeToDerivedData
+ DerivedDataLocationStyle
+ Default
+ ShowSharedSchemesAutomaticallyEnabled
+
+
+
diff --git a/ios/OpenClimb/ViewModels/LiveActivityManager.swift b/ios/OpenClimb/ViewModels/LiveActivityManager.swift
index efa406e..2598e42 100644
--- a/ios/OpenClimb/ViewModels/LiveActivityManager.swift
+++ b/ios/OpenClimb/ViewModels/LiveActivityManager.swift
@@ -87,16 +87,28 @@ final class LiveActivityManager {
/// Call this when a ClimbSession ends to end the Live Activity
func endLiveActivity() async {
- guard let currentActivity else {
- print("âšī¸ No current activity to end")
- return
+ // First end the tracked activity if it exists
+ if let currentActivity {
+ print("đ´ Ending tracked Live Activity: \(currentActivity.id)")
+ await currentActivity.end(nil, dismissalPolicy: .immediate)
+ self.currentActivity = nil
+ print("â
Tracked Live Activity ended successfully")
}
- print("đ´ Ending Live Activity: \(currentActivity.id)")
+ // Force end ALL active activities of our type to ensure cleanup
+ print("đ Checking for any remaining active activities...")
+ let activities = Activity.activities
- await currentActivity.end(nil, dismissalPolicy: .immediate)
- self.currentActivity = nil
- print("â
Live Activity ended successfully")
+ if activities.isEmpty {
+ print("âšī¸ No additional activities found")
+ } else {
+ print("đ´ Found \(activities.count) additional active activities, ending them...")
+ for activity in activities {
+ print("đ´ Force ending activity: \(activity.id)")
+ await activity.end(nil, dismissalPolicy: .immediate)
+ }
+ print("â
All Live Activities ended successfully")
+ }
}
/// Check if Live Activities are available and authorized
diff --git a/ios/OpenClimb/Views/SessionsView.swift b/ios/OpenClimb/Views/SessionsView.swift
index 3e7f600..8fa486e 100644
--- a/ios/OpenClimb/Views/SessionsView.swift
+++ b/ios/OpenClimb/Views/SessionsView.swift
@@ -1,4 +1,3 @@
-
import Combine
import SwiftUI
@@ -8,19 +7,7 @@ struct SessionsView: View {
var body: some View {
NavigationView {
- VStack(spacing: 0) {
- // Active session banner
- if let activeSession = dataManager.activeSession,
- let gym = dataManager.gym(withId: activeSession.gymId)
- {
- VStack(spacing: 8) {
- ActiveSessionBanner(session: activeSession, gym: gym)
- .padding(.horizontal)
- }
- .padding(.top, 8)
- }
-
- // Sessions list
+ Group {
if dataManager.sessions.isEmpty && dataManager.activeSession == nil {
EmptySessionsView()
} else {
@@ -28,6 +15,7 @@ struct SessionsView: View {
}
}
.navigationTitle("Sessions")
+ .navigationBarTitleDisplayMode(.automatic)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
if dataManager.gyms.isEmpty {
@@ -47,75 +35,7 @@ struct SessionsView: View {
AddEditSessionView()
}
}
- }
-}
-
-struct ActiveSessionBanner: View {
- let session: ClimbSession
- let gym: Gym
- @EnvironmentObject var dataManager: ClimbingDataManager
- @State private var currentTime = Date()
-
- private let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
-
- var body: some View {
- NavigationLink(destination: SessionDetailView(sessionId: session.id)) {
- HStack {
- VStack(alignment: .leading, spacing: 4) {
- HStack {
- Image(systemName: "play.fill")
- .foregroundColor(.green)
- .font(.caption)
- Text("Active Session")
- .font(.headline)
- .fontWeight(.bold)
- }
-
- Text(gym.name)
- .font(.subheadline)
- .foregroundColor(.secondary)
-
- if let startTime = session.startTime {
- Text(formatDuration(from: startTime, to: currentTime))
- .font(.caption)
- .foregroundColor(.secondary)
- }
- }
-
- Spacer()
-
- Button("End") {
- dataManager.endSession(session.id)
- }
- .buttonStyle(.borderedProminent)
- .controlSize(.small)
- }
- .padding()
- .background(
- RoundedRectangle(cornerRadius: 12)
- .fill(.green.opacity(0.1))
- .stroke(.green.opacity(0.3), lineWidth: 1)
- )
- }
- .buttonStyle(.plain)
- .onReceive(timer) { _ in
- currentTime = 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)
- }
+ .navigationViewStyle(.stack)
}
}
@@ -130,18 +50,45 @@ struct SessionsList: View {
}
var body: some View {
- List(completedSessions) { session in
- NavigationLink(destination: SessionDetailView(sessionId: session.id)) {
- SessionRow(session: session)
+ List {
+ // Active session banner section
+ if let activeSession = dataManager.activeSession,
+ let gym = dataManager.gym(withId: activeSession.gymId)
+ {
+ Section {
+ ActiveSessionBanner(session: activeSession, gym: gym)
+ .padding(.horizontal, 16)
+ .listRowInsets(EdgeInsets(top: 16, leading: 0, bottom: 24, trailing: 0))
+ .listRowBackground(Color.clear)
+ .listRowSeparator(.hidden)
+ }
}
- .swipeActions(edge: .trailing, allowsFullSwipe: false) {
- Button(role: .destructive) {
- sessionToDelete = session
- } label: {
- Label("Delete", systemImage: "trash")
+
+ // Completed sessions section
+ if !completedSessions.isEmpty {
+ Section {
+ ForEach(completedSessions) { session in
+ NavigationLink(destination: SessionDetailView(sessionId: session.id)) {
+ SessionRow(session: session)
+ }
+ .swipeActions(edge: .trailing, allowsFullSwipe: false) {
+ Button(role: .destructive) {
+ sessionToDelete = session
+ } label: {
+ Label("Delete", systemImage: "trash")
+ }
+ }
+ }
+ } header: {
+ if dataManager.activeSession != nil {
+ Text("Previous Sessions")
+ .font(.headline)
+ .fontWeight(.semibold)
+ }
}
}
}
+ .listStyle(.insetGrouped)
.alert("Delete Session", isPresented: .constant(sessionToDelete != nil)) {
Button("Cancel", role: .cancel) {
sessionToDelete = nil
@@ -160,6 +107,91 @@ struct SessionsList: View {
}
}
+struct ActiveSessionBanner: View {
+ let session: ClimbSession
+ let gym: Gym
+ @EnvironmentObject var dataManager: ClimbingDataManager
+ @State private var currentTime = Date()
+ @State private var navigateToDetail = false
+
+ private let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
+
+ var body: some View {
+ HStack {
+ VStack(alignment: .leading, spacing: 4) {
+ HStack {
+ Image(systemName: "play.fill")
+ .foregroundColor(.green)
+ .font(.caption)
+ Text("Active Session")
+ .font(.headline)
+ .fontWeight(.bold)
+ }
+
+ Text(gym.name)
+ .font(.subheadline)
+ .foregroundColor(.secondary)
+
+ if let startTime = session.startTime {
+ Text(formatDuration(from: startTime, to: currentTime))
+ .font(.caption)
+ .foregroundColor(.secondary)
+ }
+ }
+ .contentShape(Rectangle())
+ .onTapGesture {
+ navigateToDetail = true
+ }
+
+ Spacer()
+
+ Button(action: {
+ dataManager.endSession(session.id)
+ }) {
+ Image(systemName: "stop.fill")
+ .font(.system(size: 16, weight: .bold))
+ .foregroundColor(.white)
+ .frame(width: 32, height: 32)
+ .background(Color.red)
+ .clipShape(Circle())
+ }
+ }
+ .padding()
+ .background(
+ RoundedRectangle(cornerRadius: 12)
+ .fill(.green.opacity(0.1))
+ .stroke(.green.opacity(0.3), lineWidth: 1)
+ )
+ .onReceive(timer) { _ in
+ currentTime = Date()
+ }
+ .background(
+ NavigationLink(
+ destination: SessionDetailView(sessionId: session.id),
+ isActive: $navigateToDetail
+ ) {
+ EmptyView()
+ }
+ .hidden()
+ )
+ }
+
+ 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 SessionRow: View {
let session: ClimbSession
@EnvironmentObject var dataManager: ClimbingDataManager