diff --git a/apps/iOS/App/Info.plist b/apps/iOS/App/Info.plist
index 08465ae..d488f6e 100644
--- a/apps/iOS/App/Info.plist
+++ b/apps/iOS/App/Info.plist
@@ -8,6 +8,17 @@
system bar no longer applies. -->
UIDesignRequiresCompatibility
+
+
+ UIUserInterfaceStyle
+ Light
+
CFBundleDevelopmentRegion
$(DEVELOPMENT_LANGUAGE)
CFBundleExecutable
diff --git a/apps/iOS/Views/AuthorityRecommendationView.swift b/apps/iOS/Views/AuthorityRecommendationView.swift
index 518a302..faa0728 100644
--- a/apps/iOS/Views/AuthorityRecommendationView.swift
+++ b/apps/iOS/Views/AuthorityRecommendationView.swift
@@ -151,41 +151,37 @@ struct AuthorityRecommendationsView: View {
showingAddSheet = false
}
}
- .confirmationDialog(
+ // Alerts rather than confirmationDialogs: on iPad a dialog is presented
+ // as a popover, and on iPadOS 26 (with the app's
+ // UIDesignRequiresCompatibility opt-out) that popover renders with no
+ // readable content. Alerts are unaffected.
+ .alert(
"Delete Authority Reference",
isPresented: $showingDeleteConfirmation,
- titleVisibility: .visible
- ) {
+ presenting: deleteTarget
+ ) { authority in
Button("Delete", role: .destructive) {
- if let authority = deleteTarget {
- Task { await deleteAuthority(authority) }
- }
+ Task { await deleteAuthority(authority) }
}
Button("Cancel", role: .cancel) {
deleteTarget = nil
}
- } message: {
- if let authority = deleteTarget {
- Text("Remove this \(authority.sourceDisplayName) reference from the recording?")
- }
+ } message: { authority in
+ Text("Remove this \(authority.sourceDisplayName) reference from the recording?")
}
- .confirmationDialog(
+ .alert(
"Link to This Recording",
isPresented: $showingLinkConfirmation,
- titleVisibility: .visible
- ) {
+ presenting: linkTarget
+ ) { authority in
Button("Link to \(albumTitle)") {
- if let authority = linkTarget {
- Task { await linkAuthority(authority) }
- }
+ Task { await linkAuthority(authority) }
}
Button("Cancel", role: .cancel) {
linkTarget = nil
}
- } message: {
- if let authority = linkTarget {
- Text("Link \"\(authority.artistName ?? "Unknown") - \(authority.albumTitle ?? "Unknown")\" to this recording?")
- }
+ } message: { authority in
+ Text("Link \"\(authority.artistName ?? "Unknown") - \(authority.albumTitle ?? "Unknown")\" to this recording?")
}
.overlay {
if linkingInProgress {
diff --git a/apps/iOS/Views/MusicBrainzSearchSheet.swift b/apps/iOS/Views/MusicBrainzSearchSheet.swift
index 1bf153e..0ac599f 100644
--- a/apps/iOS/Views/MusicBrainzSearchSheet.swift
+++ b/apps/iOS/Views/MusicBrainzSearchSheet.swift
@@ -12,6 +12,7 @@ struct MusicBrainzSearchSheet: View {
let onSongImported: () -> Void
@Environment(\.dismiss) private var dismiss
+ @Environment(\.openURL) private var openURL
@EnvironmentObject var authManager: AuthenticationManager
@StateObject private var musicBrainzService = MusicBrainzService()
@@ -73,31 +74,39 @@ struct MusicBrainzSearchSheet: View {
} message: {
Text(resultMessage)
}
- .confirmationDialog(
- "Request Song",
- isPresented: .constant(selectedWork != nil && !isSubmitting),
- titleVisibility: .visible
- ) {
- if let work = selectedWork {
- Button("Request \"\(work.title)\"") {
- Task {
- await requestSong(work)
- }
- }
-
- if let url = URL(string: work.musicbrainzUrl) {
- Link("View on MusicBrainz", destination: url)
- }
-
- Button("Cancel", role: .cancel) {
- selectedWork = nil
- }
+ }
+ // An alert rather than a confirmationDialog: on iPad a dialog is presented
+ // as a popover, and on iPadOS 26 (with the app's
+ // UIDesignRequiresCompatibility opt-out) that popover renders with no
+ // readable content. Alerts are unaffected, but SwiftUI only honours one
+ // alert per view, so this one hangs off the NavigationStack while the
+ // result alert above stays on its content. Alerts also only take buttons,
+ // so the MusicBrainz link opens through the environment.
+ .alert(
+ "Request Song",
+ isPresented: Binding(
+ get: { selectedWork != nil && !isSubmitting },
+ set: { if !$0 { selectedWork = nil } }
+ ),
+ presenting: selectedWork
+ ) { work in
+ Button("Request \"\(work.title)\"") {
+ Task {
+ await requestSong(work)
}
- } message: {
- if let work = selectedWork {
- Text("Request that \"\(work.title)\" by \(work.composerDisplay) be added? We'll review it and add the song if it's a good fit.")
+ }
+
+ if let url = URL(string: work.musicbrainzUrl) {
+ Button("View on MusicBrainz") {
+ openURL(url)
}
}
+
+ Button("Cancel", role: .cancel) {
+ selectedWork = nil
+ }
+ } message: { work in
+ Text("Request that \"\(work.title)\" by \(work.composerDisplay) be added? We'll review it and add the song if it's a good fit.")
}
.presentationDetents([.medium, .large])
.presentationDragIndicator(.visible)
diff --git a/apps/iOS/Views/RecordingContributionEditView.swift b/apps/iOS/Views/RecordingContributionEditView.swift
index c28254f..84fb1a9 100644
--- a/apps/iOS/Views/RecordingContributionEditView.swift
+++ b/apps/iOS/Views/RecordingContributionEditView.swift
@@ -149,11 +149,11 @@ struct RecordingContributionEditView: View {
} message: {
Text(errorMessage)
}
- .confirmationDialog(
- "Delete Contribution",
- isPresented: $showDeleteConfirmation,
- titleVisibility: .visible
- ) {
+ // An alert rather than a confirmationDialog: on iPad a dialog is
+ // presented as a popover, and on iPadOS 26 (with the app's
+ // UIDesignRequiresCompatibility opt-out) that popover renders with no
+ // readable content. Alerts are unaffected.
+ .alert("Delete Contribution", isPresented: $showDeleteConfirmation) {
Button("Delete", role: .destructive) {
deleteContribution()
}
diff --git a/apps/iOS/Views/SongDetailView.swift b/apps/iOS/Views/SongDetailView.swift
index 0593d3f..a095eee 100644
--- a/apps/iOS/Views/SongDetailView.swift
+++ b/apps/iOS/Views/SongDetailView.swift
@@ -31,9 +31,6 @@ struct SongDetailView: View {
@State private var alertMessage = ""
@State private var isAddingToRepertoire = false
- // Song refresh management
- @State private var showRefreshConfirmation = false
-
// NEW: Toast notification
@State private var toast: ToastItem?
@@ -140,9 +137,29 @@ struct SongDetailView: View {
.font(ApproachNoteTheme.largeTitle(weight: .regular))
.foregroundColor(ApproachNoteTheme.textSecondary)
)
- .onLongPressGesture {
- if canQueueForRefresh {
- showRefreshConfirmation = true
+ // Long-press the title to re-research the song. This used to be a
+ // confirmationDialog, but on iPad that presents as a popover, and
+ // on iPadOS 26 (with the app's UIDesignRequiresCompatibility
+ // opt-out) the popover renders as an empty panel over the tab bar
+ // with no readable content. A context menu draws its own chrome,
+ // anchors to the title, and is the natural match for a long press.
+ .contextMenu {
+ Section("Refresh Song Data") {
+ if canQueueForRefresh {
+ Button {
+ refreshSongData(forceRefresh: false)
+ } label: {
+ Label("Quick Refresh", systemImage: "arrow.clockwise")
+ }
+ Button {
+ refreshSongData(forceRefresh: true)
+ } label: {
+ Label("Full Refresh", systemImage: "arrow.triangle.2.circlepath")
+ }
+ Text("Quick uses cached data. Full re-fetches everything.")
+ } else {
+ Text("This song is already queued for research.")
+ }
}
}
@@ -346,21 +363,6 @@ struct SongDetailView: View {
} message: {
Text(alertMessage)
}
- .confirmationDialog(
- "Refresh Song Data",
- isPresented: $showRefreshConfirmation,
- titleVisibility: .visible
- ) {
- Button("Quick Refresh") {
- refreshSongData(forceRefresh: false)
- }
- Button("Full Refresh") {
- refreshSongData(forceRefresh: true)
- }
- Button("Cancel", role: .cancel) { }
- } message: {
- Text("Quick refresh uses cached data for faster results. Full refresh re-fetches everything from external sources.")
- }
.toast($toast)
.onDisappear {
viewModel.stopResearchStatusPolling()
diff --git a/apps/iOS/Views/YouTubeImportView.swift b/apps/iOS/Views/YouTubeImportView.swift
index 8978031..46423fb 100644
--- a/apps/iOS/Views/YouTubeImportView.swift
+++ b/apps/iOS/Views/YouTubeImportView.swift
@@ -94,11 +94,11 @@ struct YouTubeImportView: View {
)
}
}
- .confirmationDialog(
- "Link to Recording?",
- isPresented: $showingRecordingChoice,
- titleVisibility: .visible
- ) {
+ // An alert rather than a confirmationDialog: on iPad a dialog is presented
+ // as a popover, and on iPadOS 26 (with the app's
+ // UIDesignRequiresCompatibility opt-out) that popover renders with no
+ // readable content. Alerts are unaffected.
+ .alert("Link to Recording?", isPresented: $showingRecordingChoice) {
Button("Pick a Recording") {
showingRecordingPicker = true
}