iOS Release - 1.0.1

This commit is contained in:
2025-09-20 12:03:37 -06:00
parent 7c18b56674
commit 0235b5d506
6 changed files with 167 additions and 102 deletions

View File

@@ -390,7 +390,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 3; CURRENT_PROJECT_VERSION = 4;
DEVELOPMENT_TEAM = 4BC9Y2LL4B; DEVELOPMENT_TEAM = 4BC9Y2LL4B;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
@@ -410,7 +410,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.0.0; MARKETING_VERSION = 1.0.1;
PRODUCT_BUNDLE_IDENTIFIER = com.atridad.OpenClimb; PRODUCT_BUNDLE_IDENTIFIER = com.atridad.OpenClimb;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = YES; STRING_CATALOG_GENERATE_SYMBOLS = YES;
@@ -430,7 +430,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 3; CURRENT_PROJECT_VERSION = 4;
DEVELOPMENT_TEAM = 4BC9Y2LL4B; DEVELOPMENT_TEAM = 4BC9Y2LL4B;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
@@ -450,7 +450,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.0.0; MARKETING_VERSION = 1.0.1;
PRODUCT_BUNDLE_IDENTIFIER = com.atridad.OpenClimb; PRODUCT_BUNDLE_IDENTIFIER = com.atridad.OpenClimb;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = YES; STRING_CATALOG_GENERATE_SYMBOLS = YES;

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict/>
</plist>

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>BuildLocationStyle</key>
<string>UseAppPreferences</string>
<key>CompilationCachingSetting</key>
<string>Default</string>
<key>CustomBuildLocationType</key>
<string>RelativeToDerivedData</string>
<key>DerivedDataLocationStyle</key>
<string>Default</string>
<key>ShowSharedSchemesAutomaticallyEnabled</key>
<true/>
</dict>
</plist>

View File

@@ -87,16 +87,28 @@ final class LiveActivityManager {
/// Call this when a ClimbSession ends to end the Live Activity /// Call this when a ClimbSession ends to end the Live Activity
func endLiveActivity() async { func endLiveActivity() async {
guard let currentActivity else { // First end the tracked activity if it exists
print(" No current activity to end") if let currentActivity {
return 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<SessionActivityAttributes>.activities
await currentActivity.end(nil, dismissalPolicy: .immediate) if activities.isEmpty {
self.currentActivity = nil print(" No additional activities found")
print("✅ Live Activity ended successfully") } 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 /// Check if Live Activities are available and authorized

View File

@@ -1,4 +1,3 @@
import Combine import Combine
import SwiftUI import SwiftUI
@@ -8,19 +7,7 @@ struct SessionsView: View {
var body: some View { var body: some View {
NavigationView { NavigationView {
VStack(spacing: 0) { Group {
// 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
if dataManager.sessions.isEmpty && dataManager.activeSession == nil { if dataManager.sessions.isEmpty && dataManager.activeSession == nil {
EmptySessionsView() EmptySessionsView()
} else { } else {
@@ -28,6 +15,7 @@ struct SessionsView: View {
} }
} }
.navigationTitle("Sessions") .navigationTitle("Sessions")
.navigationBarTitleDisplayMode(.automatic)
.toolbar { .toolbar {
ToolbarItem(placement: .navigationBarTrailing) { ToolbarItem(placement: .navigationBarTrailing) {
if dataManager.gyms.isEmpty { if dataManager.gyms.isEmpty {
@@ -47,75 +35,7 @@ struct SessionsView: View {
AddEditSessionView() AddEditSessionView()
} }
} }
} .navigationViewStyle(.stack)
}
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)
}
} }
} }
@@ -130,18 +50,45 @@ struct SessionsList: View {
} }
var body: some View { var body: some View {
List(completedSessions) { session in List {
NavigationLink(destination: SessionDetailView(sessionId: session.id)) { // Active session banner section
SessionRow(session: session) 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) { // Completed sessions section
sessionToDelete = session if !completedSessions.isEmpty {
} label: { Section {
Label("Delete", systemImage: "trash") 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)) { .alert("Delete Session", isPresented: .constant(sessionToDelete != nil)) {
Button("Cancel", role: .cancel) { Button("Cancel", role: .cancel) {
sessionToDelete = nil 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 { struct SessionRow: View {
let session: ClimbSession let session: ClimbSession
@EnvironmentObject var dataManager: ClimbingDataManager @EnvironmentObject var dataManager: ClimbingDataManager