Improve concurrency model for iOS
This commit is contained in:
@@ -123,11 +123,13 @@ struct AddEditSessionView: View {
|
||||
if isEditing, let session = existingSession {
|
||||
let updatedSession = session.updated(notes: notes.isEmpty ? nil : notes)
|
||||
dataManager.updateSession(updatedSession)
|
||||
dismiss()
|
||||
} else {
|
||||
dataManager.startSession(gymId: gym.id, notes: notes.isEmpty ? nil : notes)
|
||||
Task {
|
||||
await dataManager.startSession(gymId: gym.id, notes: notes.isEmpty ? nil : notes)
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -138,8 +138,10 @@ struct SessionDetailView: View {
|
||||
if let session = session {
|
||||
if session.status == .active {
|
||||
Button("End Session") {
|
||||
dataManager.endSession(session.id)
|
||||
dismiss()
|
||||
Task {
|
||||
await dataManager.endSession(session.id)
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
.foregroundColor(.orange)
|
||||
} else {
|
||||
|
||||
@@ -220,7 +220,7 @@ struct LiveActivityDebugView: View {
|
||||
appendDebugOutput("Live Activity start request sent")
|
||||
|
||||
// Wait and update
|
||||
try? await Task.sleep(nanoseconds: 3_000_000_000) // 3 seconds
|
||||
try? await Task.sleep(for: .seconds(3))
|
||||
appendDebugOutput("Updating Live Activity with test data...")
|
||||
await LiveActivityManager.shared.updateLiveActivity(
|
||||
elapsed: 180,
|
||||
@@ -229,7 +229,7 @@ struct LiveActivityDebugView: View {
|
||||
)
|
||||
|
||||
// Another update
|
||||
try? await Task.sleep(nanoseconds: 3_000_000_000) // 3 seconds
|
||||
try? await Task.sleep(for: .seconds(3))
|
||||
appendDebugOutput("Second update...")
|
||||
await LiveActivityManager.shared.updateLiveActivity(
|
||||
elapsed: 360,
|
||||
@@ -238,7 +238,7 @@ struct LiveActivityDebugView: View {
|
||||
)
|
||||
|
||||
// End after delay
|
||||
try? await Task.sleep(nanoseconds: 5_000_000_000) // 5 seconds
|
||||
try? await Task.sleep(for: .seconds(5))
|
||||
appendDebugOutput("Ending Live Activity...")
|
||||
await LiveActivityManager.shared.endLiveActivity()
|
||||
|
||||
@@ -253,8 +253,10 @@ struct LiveActivityDebugView: View {
|
||||
}
|
||||
|
||||
appendDebugOutput("Ending current session: \(activeSession.id)")
|
||||
dataManager.endSession(activeSession.id)
|
||||
appendDebugOutput("Session ended")
|
||||
Task {
|
||||
await dataManager.endSession(activeSession.id)
|
||||
appendDebugOutput("Session ended")
|
||||
}
|
||||
}
|
||||
|
||||
private func forceLiveActivityUpdate() {
|
||||
|
||||
@@ -19,12 +19,8 @@ struct ProblemsView: View {
|
||||
@State private var animationKey = 0
|
||||
|
||||
private func updateFilteredProblems() {
|
||||
Task(priority: .userInitiated) {
|
||||
let result = await computeFilteredProblems()
|
||||
// Switch back to the main thread to update the UI
|
||||
await MainActor.run {
|
||||
cachedFilteredProblems = result
|
||||
}
|
||||
Task {
|
||||
cachedFilteredProblems = await computeFilteredProblems()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -80,7 +80,9 @@ struct SessionsView: View {
|
||||
} else if dataManager.activeSession == nil {
|
||||
Button("Start Session") {
|
||||
if dataManager.gyms.count == 1 {
|
||||
dataManager.startSession(gymId: dataManager.gyms.first!.id)
|
||||
Task {
|
||||
await dataManager.startSession(gymId: dataManager.gyms.first!.id)
|
||||
}
|
||||
} else {
|
||||
showingAddSession = true
|
||||
}
|
||||
@@ -228,7 +230,9 @@ struct ActiveSessionBanner: View {
|
||||
}
|
||||
|
||||
Button(action: {
|
||||
dataManager.endSession(session.id)
|
||||
Task {
|
||||
await dataManager.endSession(session.id)
|
||||
}
|
||||
}) {
|
||||
Image(systemName: "stop.fill")
|
||||
.font(.system(size: 16, weight: .bold))
|
||||
@@ -327,7 +331,9 @@ struct EmptySessionsView: View {
|
||||
if !dataManager.gyms.isEmpty {
|
||||
Button("Start Session") {
|
||||
if dataManager.gyms.count == 1 {
|
||||
dataManager.startSession(gymId: dataManager.gyms.first!.id)
|
||||
Task {
|
||||
await dataManager.startSession(gymId: dataManager.gyms.first!.id)
|
||||
}
|
||||
} else {
|
||||
showingAddSession = true
|
||||
}
|
||||
|
||||
@@ -84,11 +84,11 @@ extension SheetType: Identifiable {
|
||||
|
||||
struct AppearanceSection: View {
|
||||
@EnvironmentObject var themeManager: ThemeManager
|
||||
|
||||
|
||||
let columns = [
|
||||
GridItem(.adaptive(minimum: 44))
|
||||
]
|
||||
|
||||
|
||||
var body: some View {
|
||||
Section("Appearance") {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
@@ -96,7 +96,7 @@ struct AppearanceSection: View {
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.textCase(.uppercase)
|
||||
|
||||
|
||||
LazyVGrid(columns: columns, spacing: 12) {
|
||||
ForEach(ThemeManager.presetColors, id: \.self) { color in
|
||||
Circle()
|
||||
@@ -123,7 +123,7 @@ struct AppearanceSection: View {
|
||||
}
|
||||
.padding(.vertical, 8)
|
||||
}
|
||||
|
||||
|
||||
if !isSelected(.blue) {
|
||||
Button("Reset to Default") {
|
||||
withAnimation {
|
||||
@@ -134,17 +134,17 @@ struct AppearanceSection: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func isSelected(_ color: Color) -> Bool {
|
||||
// Compare using UIColor to handle different Color initializers
|
||||
let selectedUIColor = UIColor(themeManager.accentColor)
|
||||
let targetUIColor = UIColor(color)
|
||||
|
||||
|
||||
// Simple equality check might fail for some system colors, so we check components if needed
|
||||
// But usually UIColor equality is robust enough for system colors
|
||||
return selectedUIColor == targetUIColor
|
||||
}
|
||||
|
||||
|
||||
private func colorDescription(for color: Color) -> String {
|
||||
switch color {
|
||||
case .blue: return "Blue"
|
||||
@@ -474,8 +474,8 @@ struct ExportDataView: View {
|
||||
}
|
||||
|
||||
private func createTempFile() {
|
||||
let logTag = Self.logTag // Capture before entering background queue
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
let logTag = Self.logTag
|
||||
Task.detached(priority: .userInitiated) {
|
||||
do {
|
||||
let formatter = ISO8601DateFormatter()
|
||||
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
|
||||
@@ -489,28 +489,23 @@ struct ExportDataView: View {
|
||||
for: .documentDirectory, in: .userDomainMask
|
||||
).first
|
||||
else {
|
||||
Task { @MainActor in
|
||||
await MainActor.run {
|
||||
AppLogger.error("Could not access Documents directory", tag: logTag)
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
self.isCreatingFile = false
|
||||
}
|
||||
return
|
||||
}
|
||||
let fileURL = documentsURL.appendingPathComponent(filename)
|
||||
|
||||
// Write the ZIP data to the file
|
||||
try data.write(to: fileURL)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
await MainActor.run {
|
||||
self.tempFileURL = fileURL
|
||||
self.isCreatingFile = false
|
||||
}
|
||||
} catch {
|
||||
Task { @MainActor in
|
||||
await MainActor.run {
|
||||
AppLogger.error("Failed to create export file: \(error)", tag: logTag)
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
self.isCreatingFile = false
|
||||
}
|
||||
}
|
||||
@@ -809,12 +804,12 @@ struct SyncSettingsView: View {
|
||||
|
||||
syncService.serverURL = newURL
|
||||
syncService.authToken = newToken
|
||||
|
||||
|
||||
// Ensure provider type is set to server
|
||||
if syncService.providerType != .server {
|
||||
syncService.providerType = .server
|
||||
}
|
||||
|
||||
|
||||
dismiss()
|
||||
}
|
||||
.fontWeight(.semibold)
|
||||
@@ -1116,7 +1111,7 @@ struct HealthKitSection: View {
|
||||
|
||||
struct MusicSection: View {
|
||||
@EnvironmentObject var musicService: MusicService
|
||||
|
||||
|
||||
var body: some View {
|
||||
Section {
|
||||
Toggle(isOn: Binding(
|
||||
@@ -1129,7 +1124,7 @@ struct MusicSection: View {
|
||||
Text("Apple Music Integration")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if musicService.isMusicEnabled {
|
||||
if !musicService.isAuthorized {
|
||||
Button("Connect Apple Music") {
|
||||
@@ -1140,14 +1135,14 @@ struct MusicSection: View {
|
||||
} 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)
|
||||
|
||||
Reference in New Issue
Block a user