mirror of
https://github.com/yattee/yattee.git
synced 2026-10-23 13:12:53 +00:00
Ask link action confirmations in an alert on tvOS
A tvOS sheet fills most of the screen, far too much for a two-button question. The shared sheet host now presents link action confirmations as an alert there, answering cancel when it goes away without a choice.
This commit is contained in:
@@ -130,11 +130,14 @@ private struct AppSheetHost: ViewModifier {
|
||||
func body(content: Content) -> some View {
|
||||
content
|
||||
.environment(\.presentAppSheet, PresentAppSheetAction { sheet = $0 })
|
||||
.sheet(item: $sheet) { sheet in
|
||||
.sheet(item: presentedSheet) { sheet in
|
||||
sheetContent(for: sheet)
|
||||
.sheetToastOverlay(scope: sheetToastLevel.scope, depth: sheetToastLevel.depth)
|
||||
.environment(\.sheetToastLevel, sheetToastLevel)
|
||||
}
|
||||
#if os(tvOS)
|
||||
.linkActionConfirmationAlert(pendingConfirmation) { sheet = nil }
|
||||
#endif
|
||||
// Declared before the request observer so a deep link that both
|
||||
// dismisses and presents ends with its own sheet up.
|
||||
.onChange(of: dismissGeneration) { _, _ in
|
||||
@@ -145,6 +148,26 @@ private struct AppSheetHost: ViewModifier {
|
||||
}
|
||||
}
|
||||
|
||||
/// The sheet to present as a sheet. On tvOS a link action confirmation
|
||||
/// is a compact alert instead: a tvOS sheet fills most of the screen.
|
||||
private var presentedSheet: Binding<AppSheet?> {
|
||||
#if os(tvOS)
|
||||
Binding {
|
||||
if case .linkActionConfirmation = sheet { return nil }
|
||||
return sheet
|
||||
} set: { sheet = $0 }
|
||||
#else
|
||||
$sheet
|
||||
#endif
|
||||
}
|
||||
|
||||
#if os(tvOS)
|
||||
private var pendingConfirmation: LinkActionConfirmation? {
|
||||
if case .linkActionConfirmation(let confirmation) = sheet { return confirmation }
|
||||
return nil
|
||||
}
|
||||
#endif
|
||||
|
||||
private var dismissGeneration: Int? {
|
||||
dismissesOnExternalNavigation ? appEnvironment?.navigationCoordinator.dismissPresentationsGeneration : nil
|
||||
}
|
||||
|
||||
@@ -44,6 +44,12 @@ final class LinkActionConfirmation {
|
||||
}
|
||||
}
|
||||
|
||||
extension LinkActionConfirmation: Equatable {
|
||||
nonisolated static func == (lhs: LinkActionConfirmation, rhs: LinkActionConfirmation) -> Bool {
|
||||
lhs === rhs
|
||||
}
|
||||
}
|
||||
|
||||
struct LinkActionConfirmationSheet: View {
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
@@ -57,29 +63,6 @@ struct LinkActionConfirmationSheet: View {
|
||||
}
|
||||
}
|
||||
|
||||
#if os(tvOS)
|
||||
private var content: some View {
|
||||
NavigationStack {
|
||||
List {
|
||||
Section {
|
||||
header
|
||||
.padding(.vertical, 8)
|
||||
}
|
||||
Section {
|
||||
Button { respond(true) } label: {
|
||||
Label(confirmation.confirmTitle, systemImage: confirmation.systemImage)
|
||||
}
|
||||
Button(role: .cancel) { respond(false) } label: {
|
||||
Label(String(localized: "common.cancel"), systemImage: "xmark")
|
||||
}
|
||||
}
|
||||
}
|
||||
.scrollClipDisabled()
|
||||
.padding(.horizontal, 40)
|
||||
.padding(.vertical, 24)
|
||||
}
|
||||
}
|
||||
#else
|
||||
private var content: some View {
|
||||
VStack(spacing: 0) {
|
||||
header
|
||||
@@ -112,10 +95,11 @@ struct LinkActionConfirmationSheet: View {
|
||||
#if os(macOS)
|
||||
.frame(width: 380)
|
||||
#endif
|
||||
#if !os(tvOS)
|
||||
.presentationDetents([.height(210)])
|
||||
.presentationDragIndicator(.visible)
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
private var header: some View {
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
@@ -141,3 +125,35 @@ struct LinkActionConfirmationSheet: View {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
#if os(tvOS)
|
||||
extension View {
|
||||
/// Presents a link action confirmation as an alert. tvOS sheets fill most
|
||||
/// of the screen, too much for a two-button question.
|
||||
func linkActionConfirmationAlert(
|
||||
_ confirmation: LinkActionConfirmation?,
|
||||
onDismiss: @escaping () -> Void
|
||||
) -> some View {
|
||||
alert(
|
||||
confirmation?.title ?? "",
|
||||
isPresented: Binding {
|
||||
confirmation != nil
|
||||
} set: { isPresented in
|
||||
if !isPresented { onDismiss() }
|
||||
},
|
||||
presenting: confirmation
|
||||
) { confirmation in
|
||||
Button(confirmation.confirmTitle) { confirmation.answer(true) }
|
||||
Button(String(localized: "common.cancel"), role: .cancel) { confirmation.answer(false) }
|
||||
} message: { confirmation in
|
||||
Text([confirmation.subject, confirmation.detail].compactMap { $0 }.joined(separator: "\n"))
|
||||
}
|
||||
.onChange(of: confirmation) { previous, _ in
|
||||
// Gone without an answer (Menu button, external navigation, another
|
||||
// sheet): treat as cancel. Deferred so a button's answer comes first.
|
||||
guard let previous else { return }
|
||||
Task { @MainActor in previous.answer(false) }
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
Reference in New Issue
Block a user