diff --git a/ios/Ascently.xcodeproj/project.pbxproj b/ios/Ascently.xcodeproj/project.pbxproj
index fadb598..5169b71 100644
--- a/ios/Ascently.xcodeproj/project.pbxproj
+++ b/ios/Ascently.xcodeproj/project.pbxproj
@@ -465,7 +465,7 @@
CODE_SIGN_ENTITLEMENTS = Ascently/Ascently.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 36;
+ CURRENT_PROJECT_VERSION = 37;
DEVELOPMENT_TEAM = 4BC9Y2LL4B;
DRIVERKIT_DEPLOYMENT_TARGET = 24.6;
ENABLE_PREVIEWS = YES;
@@ -487,7 +487,7 @@
"@executable_path/Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 15.6;
- MARKETING_VERSION = 2.4.2;
+ MARKETING_VERSION = 2.5.0;
PRODUCT_BUNDLE_IDENTIFIER = com.atridad.Ascently;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@@ -513,7 +513,7 @@
CODE_SIGN_ENTITLEMENTS = Ascently/Ascently.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 36;
+ CURRENT_PROJECT_VERSION = 37;
DEVELOPMENT_TEAM = 4BC9Y2LL4B;
DRIVERKIT_DEPLOYMENT_TARGET = 24.6;
ENABLE_PREVIEWS = YES;
@@ -535,7 +535,7 @@
"@executable_path/Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 15.6;
- MARKETING_VERSION = 2.4.2;
+ MARKETING_VERSION = 2.5.0;
PRODUCT_BUNDLE_IDENTIFIER = com.atridad.Ascently;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@@ -602,7 +602,7 @@
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
CODE_SIGN_ENTITLEMENTS = SessionStatusLiveExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 36;
+ CURRENT_PROJECT_VERSION = 37;
DEVELOPMENT_TEAM = 4BC9Y2LL4B;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = SessionStatusLive/Info.plist;
@@ -613,7 +613,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
- MARKETING_VERSION = 2.4.2;
+ MARKETING_VERSION = 2.5.0;
PRODUCT_BUNDLE_IDENTIFIER = com.atridad.Ascently.SessionStatusLive;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
@@ -632,7 +632,7 @@
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
CODE_SIGN_ENTITLEMENTS = SessionStatusLiveExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 36;
+ CURRENT_PROJECT_VERSION = 37;
DEVELOPMENT_TEAM = 4BC9Y2LL4B;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = SessionStatusLive/Info.plist;
@@ -643,7 +643,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
- MARKETING_VERSION = 2.4.2;
+ MARKETING_VERSION = 2.5.0;
PRODUCT_BUNDLE_IDENTIFIER = com.atridad.Ascently.SessionStatusLive;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
diff --git a/ios/Ascently.xcodeproj/project.xcworkspace/xcuserdata/atridad.xcuserdatad/UserInterfaceState.xcuserstate b/ios/Ascently.xcodeproj/project.xcworkspace/xcuserdata/atridad.xcuserdatad/UserInterfaceState.xcuserstate
index e1c62aa..b8fa1db 100644
Binary files a/ios/Ascently.xcodeproj/project.xcworkspace/xcuserdata/atridad.xcuserdatad/UserInterfaceState.xcuserstate and b/ios/Ascently.xcodeproj/project.xcworkspace/xcuserdata/atridad.xcuserdatad/UserInterfaceState.xcuserstate differ
diff --git a/ios/Ascently/ContentView.swift b/ios/Ascently/ContentView.swift
index 52dd10a..9e178ef 100644
--- a/ios/Ascently/ContentView.swift
+++ b/ios/Ascently/ContentView.swift
@@ -44,6 +44,7 @@ struct ContentView: View {
.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
diff --git a/ios/Ascently/Info.plist b/ios/Ascently/Info.plist
index 9a5b5a9..f0d3170 100644
--- a/ios/Ascently/Info.plist
+++ b/ios/Ascently/Info.plist
@@ -15,5 +15,7 @@
This app needs access to save your climbing workouts to Apple Health.
NSHealthUpdateUsageDescription
This app needs access to save your climbing workouts to Apple Health.
+ NSAppleMusicUsageDescription
+ This app (optionally) needs access to your music library to play your selected playlist during climbing sessions.
diff --git a/ios/Ascently/Services/MusicService.swift b/ios/Ascently/Services/MusicService.swift
new file mode 100644
index 0000000..cc0fd7e
--- /dev/null
+++ b/ios/Ascently/Services/MusicService.swift
@@ -0,0 +1,197 @@
+import MusicKit
+import AVFoundation
+import SwiftUI
+import Combine
+
+@MainActor
+class MusicService: ObservableObject {
+ static let shared = MusicService()
+
+ @Published var isAuthorized = false
+ @Published var playlists: MusicItemCollection = []
+ @Published var selectedPlaylistId: String? {
+ didSet {
+ UserDefaults.standard.set(selectedPlaylistId, forKey: "ascently_selected_playlist_id")
+ }
+ }
+ @Published var isMusicEnabled: Bool {
+ didSet {
+ UserDefaults.standard.set(isMusicEnabled, forKey: "ascently_music_enabled")
+ if !isMusicEnabled {
+ // Genuinely unsure what I want to do with this but we should account for it at some point
+ }
+ }
+ }
+ @Published var isAutoPlayEnabled: Bool {
+ didSet {
+ UserDefaults.standard.set(isAutoPlayEnabled, forKey: "ascently_music_autoplay_enabled")
+ }
+ }
+ @Published var isAutoStopEnabled: Bool {
+ didSet {
+ UserDefaults.standard.set(isAutoStopEnabled, forKey: "ascently_music_autostop_enabled")
+ }
+ }
+ @Published var isPlaying = false
+
+ private var cancellables = Set()
+ private var hasStartedSessionPlayback = false
+ private var currentPlaylistTrackIds: Set = []
+
+ private init() {
+ self.selectedPlaylistId = UserDefaults.standard.string(forKey: "ascently_selected_playlist_id")
+ self.isMusicEnabled = UserDefaults.standard.bool(forKey: "ascently_music_enabled")
+ self.isAutoPlayEnabled = UserDefaults.standard.bool(forKey: "ascently_music_autoplay_enabled")
+ self.isAutoStopEnabled = UserDefaults.standard.bool(forKey: "ascently_music_autostop_enabled")
+
+ if isMusicEnabled {
+ Task {
+ await checkAuthorizationStatus()
+ }
+ }
+
+ setupObservers()
+ }
+
+ private func setupObservers() {
+ SystemMusicPlayer.shared.state.objectWillChange
+ .sink { [weak self] _ in
+ self?.updatePlaybackStatus()
+ }
+ .store(in: &cancellables)
+
+ SystemMusicPlayer.shared.queue.objectWillChange
+ .sink { [weak self] _ in
+ self?.checkQueueConsistency()
+ }
+ .store(in: &cancellables)
+ }
+
+ private func updatePlaybackStatus() {
+ Task { @MainActor [weak self] in
+ self?.isPlaying = SystemMusicPlayer.shared.state.playbackStatus == .playing
+ }
+ }
+
+ private func checkQueueConsistency() {
+ guard hasStartedSessionPlayback else { return }
+
+ Task { @MainActor [weak self] in
+ guard let self = self else { return }
+ if let currentEntry = SystemMusicPlayer.shared.queue.currentEntry,
+ let item = currentEntry.item {
+ if !self.currentPlaylistTrackIds.isEmpty && !self.currentPlaylistTrackIds.contains(item.id) {
+ self.hasStartedSessionPlayback = false
+ }
+ }
+ }
+ }
+
+ func toggleMusicEnabled(_ enabled: Bool) {
+ isMusicEnabled = enabled
+ if enabled {
+ Task {
+ await checkAuthorizationStatus()
+ }
+ }
+ }
+
+ func checkAuthorizationStatus() async {
+ let status = await MusicAuthorization.request()
+ self.isAuthorized = status == .authorized
+ if isAuthorized {
+ await fetchPlaylists()
+ }
+ }
+
+ func fetchPlaylists() async {
+ guard isAuthorized else { return }
+ do {
+ var request = MusicLibraryRequest()
+ request.sort(by: \.name, ascending: true)
+ let response = try await request.response()
+ self.playlists = response.items
+ } catch {
+ print("Error fetching playlists: \(error)")
+ }
+ }
+
+ func playSelectedPlaylistIfHeadphonesConnected() {
+ guard isMusicEnabled, isAutoPlayEnabled, let playlistId = selectedPlaylistId else { return }
+
+ if isHeadphonesConnected() {
+ playPlaylist(id: playlistId)
+ }
+ }
+
+ func resetSessionPlaybackState() {
+ hasStartedSessionPlayback = false
+ currentPlaylistTrackIds.removeAll()
+ }
+
+ func playPlaylist(id: String) {
+ print("Attempting to play playlist \(id)")
+ Task {
+ do {
+ if playlists.isEmpty {
+ await fetchPlaylists()
+ }
+
+ var targetPlaylist: Playlist?
+
+ if let playlist = playlists.first(where: { $0.id.rawValue == id }) {
+ targetPlaylist = playlist
+ } else {
+ var request = MusicLibraryRequest()
+ request.filter(matching: \.id, equalTo: MusicItemID(id))
+ let response = try await request.response()
+ targetPlaylist = response.items.first
+ }
+
+ if let playlist = targetPlaylist {
+ let detailedPlaylist = try await playlist.with([.tracks])
+ if let tracks = detailedPlaylist.tracks {
+ self.currentPlaylistTrackIds = Set(tracks.map { $0.id })
+ }
+
+ SystemMusicPlayer.shared.queue = [playlist]
+ try await SystemMusicPlayer.shared.play()
+ hasStartedSessionPlayback = true
+ }
+ } catch {
+ print("Error playing playlist: \(error)")
+ }
+ }
+ }
+
+ func stopPlaybackIfEnabled() {
+ guard isMusicEnabled, isAutoStopEnabled else { return }
+ SystemMusicPlayer.shared.stop()
+ }
+
+ func togglePlayback() {
+ Task {
+ if isPlaying {
+ SystemMusicPlayer.shared.pause()
+ } else {
+ if let playlistId = selectedPlaylistId, !hasStartedSessionPlayback {
+ playPlaylist(id: playlistId)
+ } else {
+ try? await SystemMusicPlayer.shared.play()
+ }
+ }
+ }
+ }
+
+ private func isHeadphonesConnected() -> Bool {
+ let route = AVAudioSession.sharedInstance().currentRoute
+ return route.outputs.contains { port in
+ port.portType == .headphones ||
+ port.portType == .bluetoothA2DP ||
+ port.portType == .bluetoothLE ||
+ port.portType == .bluetoothHFP ||
+ port.portType == .usbAudio ||
+ port.portType == .airPlay
+ }
+ }
+}
diff --git a/ios/Ascently/ViewModels/ClimbingDataManager.swift b/ios/Ascently/ViewModels/ClimbingDataManager.swift
index 246d123..4e16ec8 100644
--- a/ios/Ascently/ViewModels/ClimbingDataManager.swift
+++ b/ios/Ascently/ViewModels/ClimbingDataManager.swift
@@ -38,6 +38,7 @@ class ClimbingDataManager: ObservableObject {
let syncService = SyncService()
let healthKitService = HealthKitService.shared
+ let musicService = MusicService.shared
@Published var isSyncing = false
private enum Keys {
@@ -437,13 +438,15 @@ class ClimbingDataManager: ObservableObject {
saveSessions()
DataStateManager.shared.updateDataState()
- // MARK: - Start Live Activity for new session
if let gym = gym(withId: gymId) {
await LiveActivityManager.shared.startLiveActivity(
for: newSession,
gymName: gym.name)
}
+ musicService.resetSessionPlaybackState()
+ musicService.playSelectedPlaylistIfHeadphonesConnected()
+
if healthKitService.isEnabled {
do {
try await healthKitService.startWorkout(
@@ -488,8 +491,12 @@ class ClimbingDataManager: ObservableObject {
// Trigger auto-sync if enabled
syncService.triggerAutoSync(dataManager: self)
- // MARK: - End Live Activity after session ends
await LiveActivityManager.shared.endLiveActivity()
+
+ if UserDefaults.standard.bool(forKey: "isAutoStopMusicEnabled") {
+ musicService.stopPlaybackIfEnabled()
+ }
+ musicService.stopPlaybackIfEnabled()
if healthKitService.isEnabled {
do {
diff --git a/ios/Ascently/ViewModels/LiveActivityManager.swift b/ios/Ascently/ViewModels/LiveActivityManager.swift
index 3bce362..825256f 100644
--- a/ios/Ascently/ViewModels/LiveActivityManager.swift
+++ b/ios/Ascently/ViewModels/LiveActivityManager.swift
@@ -207,7 +207,7 @@ final class LiveActivityManager {
func startHealthChecks() {
stopHealthChecks() // Stop any existing timer
- AppLogger.debug("🩺 Starting Live Activity health checks", tag: Self.logTag)
+ AppLogger.debug("Starting Live Activity health checks", tag: Self.logTag)
healthCheckTimer = Timer.scheduledTimer(withTimeInterval: 30.0, repeats: true) {
[weak self] _ in
Task { @MainActor [weak self] in
@@ -233,7 +233,7 @@ final class LiveActivityManager {
// Only perform health check if it's been at least 25 seconds
guard timeSinceLastCheck >= 25 else { return }
- AppLogger.debug("🩺 Performing Live Activity health check", tag: Self.logTag)
+ AppLogger.debug("Performing Live Activity health check", tag: Self.logTag)
lastHealthCheck = now
let activities = Activity.activities
diff --git a/ios/Ascently/Views/CalendarView.swift b/ios/Ascently/Views/CalendarView.swift
index cd252e8..7660750 100644
--- a/ios/Ascently/Views/CalendarView.swift
+++ b/ios/Ascently/Views/CalendarView.swift
@@ -58,6 +58,7 @@ struct CalendarView: View {
let gym = dataManager.gym(withId: activeSession.gymId)
{
ActiveSessionBanner(session: activeSession, gym: gym)
+ .environmentObject(MusicService.shared)
.padding(.horizontal, 16)
.padding(.top, 8)
.padding(.bottom, 16)
diff --git a/ios/Ascently/Views/Detail/SessionDetailView.swift b/ios/Ascently/Views/Detail/SessionDetailView.swift
index 4f42d80..f35620e 100644
--- a/ios/Ascently/Views/Detail/SessionDetailView.swift
+++ b/ios/Ascently/Views/Detail/SessionDetailView.swift
@@ -1,10 +1,12 @@
import Combine
+import MusicKit
import SwiftUI
struct SessionDetailView: View {
let sessionId: UUID
@EnvironmentObject var dataManager: ClimbingDataManager
@EnvironmentObject var themeManager: ThemeManager
+ @EnvironmentObject var musicService: MusicService
@Environment(\.dismiss) private var dismiss
@State private var showingDeleteAlert = false
@State private var showingAddAttempt = false
@@ -41,13 +43,19 @@ struct SessionDetailView: View {
Section {
SessionHeaderCard(
session: session, gym: gym, stats: sessionStats)
- .listRowInsets(EdgeInsets())
+ .listRowInsets(EdgeInsets(top: 6, leading: 16, bottom: 6, trailing: 16))
.listRowBackground(Color.clear)
.listRowSeparator(.hidden)
- .padding(.bottom, 8)
+
+ if session.status == .active && musicService.isMusicEnabled && musicService.isAuthorized {
+ MusicControlCard()
+ .listRowInsets(EdgeInsets(top: 6, leading: 16, bottom: 6, trailing: 16))
+ .listRowBackground(Color.clear)
+ .listRowSeparator(.hidden)
+ }
SessionStatsCard(stats: sessionStats)
- .listRowInsets(EdgeInsets())
+ .listRowInsets(EdgeInsets(top: 6, leading: 16, bottom: 6, trailing: 16))
.listRowBackground(Color.clear)
.listRowSeparator(.hidden)
}
@@ -76,6 +84,7 @@ struct SessionDetailView: View {
)
.listRowBackground(Color.clear)
.listRowSeparator(.hidden)
+ .listRowInsets(EdgeInsets(top: 6, leading: 16, bottom: 6, trailing: 16))
} else {
ForEach(attemptsWithProblems.indices, id: \.self) { index in
let (attempt, problem) = attemptsWithProblems[index]
@@ -442,6 +451,53 @@ struct SessionStats {
let uniqueProblemsCompleted: Int
}
+struct MusicControlCard: View {
+ @EnvironmentObject var musicService: MusicService
+
+ var body: some View {
+ HStack {
+ Image(systemName: "music.note")
+ .font(.title2)
+ .foregroundColor(.pink)
+ .frame(width: 40, height: 40)
+ .background(Color.pink.opacity(0.1))
+ .clipShape(Circle())
+
+ VStack(alignment: .leading, spacing: 2) {
+ Text("Music")
+ .font(.headline)
+
+ if let playlistId = musicService.selectedPlaylistId,
+ let playlist = musicService.playlists.first(where: { $0.id.rawValue == playlistId }) {
+ Text(playlist.name)
+ .font(.caption)
+ .foregroundColor(.secondary)
+ } else {
+ Text("No playlist selected")
+ .font(.caption)
+ .foregroundColor(.secondary)
+ }
+ }
+
+ Spacer()
+
+ Button(action: {
+ musicService.togglePlayback()
+ }) {
+ Image(systemName: musicService.isPlaying ? "pause.circle.fill" : "play.circle.fill")
+ .font(.system(size: 32))
+ .foregroundColor(.pink)
+ }
+ }
+ .padding()
+ .background(
+ RoundedRectangle(cornerRadius: 16)
+ .fill(Color(.secondarySystemGroupedBackground))
+ .shadow(color: .black.opacity(0.05), radius: 5, x: 0, y: 2)
+ )
+ }
+}
+
#Preview {
NavigationView {
SessionDetailView(sessionId: UUID())
diff --git a/ios/Ascently/Views/SessionsView.swift b/ios/Ascently/Views/SessionsView.swift
index da39144..1b13aa2 100644
--- a/ios/Ascently/Views/SessionsView.swift
+++ b/ios/Ascently/Views/SessionsView.swift
@@ -122,6 +122,7 @@ struct SessionsList: View {
{
Section {
ActiveSessionBanner(session: activeSession, gym: gym)
+ .environmentObject(MusicService.shared)
.padding(.horizontal, 16)
.listRowInsets(EdgeInsets(top: 16, leading: 0, bottom: 24, trailing: 0))
.listRowBackground(Color.clear)
@@ -178,6 +179,17 @@ struct ActiveSessionBanner: View {
let gym: Gym
@EnvironmentObject var dataManager: ClimbingDataManager
@State private var navigateToDetail = false
+
+ // Access MusicService via DataManager if possible, or EnvironmentObject if injected
+ // Since DataManager holds MusicService, we can access it through there if we expose it or inject it.
+ // In SettingsView we saw .environmentObject(dataManager.musicService).
+ // We should probably inject it here too or access via dataManager if it's public.
+ // Let's check ClimbingDataManager again. It has `let musicService = MusicService.shared`.
+ // But it's not @Published so it won't trigger updates unless we observe the service itself.
+ // The best way is to use @EnvironmentObject var musicService: MusicService
+ // and ensure it's injected in the parent view.
+
+ @EnvironmentObject var musicService: MusicService
var body: some View {
HStack {
@@ -201,6 +213,19 @@ struct ActiveSessionBanner: View {
.foregroundColor(.secondary)
.monospacedDigit()
}
+
+ if musicService.isMusicEnabled && musicService.isAuthorized {
+ Button(action: {
+ musicService.togglePlayback()
+ }) {
+ HStack(spacing: 4) {
+ Image(systemName: musicService.isPlaying ? "pause.fill" : "play.fill")
+ .font(.caption)
+ }
+ .foregroundColor(.pink)
+ .padding(.top, 2)
+ }
+ }
}
.frame(maxWidth: .infinity, alignment: .leading)
.contentShape(Rectangle())
diff --git a/ios/Ascently/Views/SettingsView.swift b/ios/Ascently/Views/SettingsView.swift
index 52fb964..e2480fe 100644
--- a/ios/Ascently/Views/SettingsView.swift
+++ b/ios/Ascently/Views/SettingsView.swift
@@ -1,4 +1,5 @@
import HealthKit
+import MusicKit
import SwiftUI
import UniformTypeIdentifiers
@@ -21,6 +22,9 @@ struct SettingsView: View {
HealthKitSection()
.environmentObject(dataManager.healthKitService)
+ MusicSection()
+ .environmentObject(dataManager.musicService)
+
AppearanceSection()
DataManagementSection(
@@ -1099,6 +1103,53 @@ struct HealthKitSection: View {
}
}
+struct MusicSection: View {
+ @EnvironmentObject var musicService: MusicService
+
+ var body: some View {
+ Section {
+ Toggle(isOn: Binding(
+ get: { musicService.isMusicEnabled },
+ set: { musicService.toggleMusicEnabled($0) }
+ )) {
+ HStack {
+ Image(systemName: "music.note")
+ .foregroundColor(.pink)
+ Text("Apple Music Integration")
+ }
+ }
+
+ if musicService.isMusicEnabled {
+ if !musicService.isAuthorized {
+ Button("Connect Apple Music") {
+ Task {
+ await musicService.checkAuthorizationStatus()
+ }
+ }
+ } else {
+ Toggle("Auto-Play on Session Start", isOn: $musicService.isAutoPlayEnabled)
+ Toggle("Stop Music on Session End", isOn: $musicService.isAutoStopEnabled)
+
+ Picker("Playlist", selection: $musicService.selectedPlaylistId) {
+ Text("None").tag(nil as String?)
+ ForEach(musicService.playlists, id: \.id) { playlist in
+ Text(playlist.name).tag(playlist.id.rawValue as String?)
+ }
+ }
+
+ if musicService.isAutoPlayEnabled {
+ Text("Music will only auto-play if headphones are connected when you start a session.")
+ .font(.caption)
+ .foregroundColor(.secondary)
+ }
+ }
+ }
+ } header: {
+ Text("Music")
+ }
+ }
+}
+
#Preview {
SettingsView()
.environmentObject(ClimbingDataManager.preview)
diff --git a/ios/SessionStatusLive/SessionStatusLiveLiveActivity.swift b/ios/SessionStatusLive/SessionStatusLiveLiveActivity.swift
index 7fd2326..18aa5f8 100644
--- a/ios/SessionStatusLive/SessionStatusLiveLiveActivity.swift
+++ b/ios/SessionStatusLive/SessionStatusLiveLiveActivity.swift
@@ -42,6 +42,7 @@ struct SessionStatusLiveLiveActivity: Widget {
.foregroundColor(.secondary)
.lineLimit(1)
}
+ .padding(.leading, 8)
}
DynamicIslandExpandedRegion(.trailing) {
VStack(alignment: .trailing, spacing: 4) {
@@ -61,6 +62,7 @@ struct SessionStatusLiveLiveActivity: Widget {
.foregroundColor(.secondary)
}
}
+ .padding(.trailing, 8)
}
DynamicIslandExpandedRegion(.bottom) {
HStack {
@@ -72,6 +74,8 @@ struct SessionStatusLiveLiveActivity: Widget {
.font(.caption2)
.foregroundColor(.secondary)
}
+ .padding(.horizontal, 12)
+ .padding(.bottom, 4)
}
} compactLeading: {
Image(systemName: "figure.climbing")