mirror of
https://github.com/sudosylabs/vnidrop.git
synced 2026-08-05 10:29:58 +02:00
fix(apple): show receiver-approval modal on macOS release builds
The approval modal never appeared for a macOS sender: the receiver request reached the core and even fired its notification, but the modal stayed hidden. Root cause was observation, not presentation. `RootView` derived `approvals` and `messages` as `@ObservedObject` in `init` from a freshly built `AppGraph`. `init` runs on every view re-creation and each run makes a throwaway graph, so those observed objects were repointed to a dead `ApprovalCoordinator` that never receives core events — while the persisted `@StateObject graph` (and the models wired to it) kept the live one. Debug happened not to re-init the view, so it stayed on the live instance; release re-inits it, exposing the bug. Move the snackbar + approval modal into an `OverlayLayer` child view that takes the coordinator/messages as `@ObservedObject` and is constructed in `body` from the persisted `graph`, so the subscription is always against the live instances. While here: - Present the approval only after any open share/QR sheet has actually finished dismissing (macOS can't stack sheets), driven off the sheet's real `onDismiss` completion via a new `AdaptiveDrawer.onDismissed` hook and `SendModel.shareSheetsDismissed` — no wall-clock delay. - Move the list-level share-sheet state (`shareTargetId`) into `SendModel` so the approval flow can dismiss every share surface centrally. - Add a fallback: pending receiver rows in the Receivers panel now offer an Approve action (`SendModel.acceptReceiver`) alongside Refuse, for the case the modal didn't surface.
This commit is contained in:
@@ -9,15 +9,9 @@ struct RootView: View {
|
|||||||
@StateObject private var sendModel: SendModel
|
@StateObject private var sendModel: SendModel
|
||||||
@StateObject private var receiveModel: ReceiveModel
|
@StateObject private var receiveModel: ReceiveModel
|
||||||
@StateObject private var settingsModel: SettingsModel
|
@StateObject private var settingsModel: SettingsModel
|
||||||
@ObservedObject private var messages: UiMessageController
|
|
||||||
@ObservedObject private var approvals: ApprovalCoordinator
|
|
||||||
|
|
||||||
@Environment(\.scenePhase) private var scenePhase
|
@Environment(\.scenePhase) private var scenePhase
|
||||||
|
|
||||||
/// Drives the approval sheet; toggled from the pending-approval `onChange` so the
|
|
||||||
/// presentation can be deferred until the Share/QR sheet has dismissed on macOS.
|
|
||||||
@State private var showApproval = false
|
|
||||||
|
|
||||||
init(dependencies: AppDependencies) {
|
init(dependencies: AppDependencies) {
|
||||||
let graph = AppGraph(dependencies: dependencies)
|
let graph = AppGraph(dependencies: dependencies)
|
||||||
_graph = StateObject(wrappedValue: graph)
|
_graph = StateObject(wrappedValue: graph)
|
||||||
@@ -50,8 +44,6 @@ struct RootView: View {
|
|||||||
messages: graph.messages,
|
messages: graph.messages,
|
||||||
bugReports: NoopBugReportService()
|
bugReports: NoopBugReportService()
|
||||||
))
|
))
|
||||||
messages = graph.messages
|
|
||||||
approvals = graph.approvalCoordinator
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
@@ -60,12 +52,14 @@ struct RootView: View {
|
|||||||
let isDark = resolveDarkTheme(appModel.themeMode, systemDark: systemDark)
|
let isDark = resolveDarkTheme(appModel.themeMode, systemDark: systemDark)
|
||||||
ZStack {
|
ZStack {
|
||||||
navigation(windowClass: windowClass)
|
navigation(windowClass: windowClass)
|
||||||
SnackbarHost(controller: messages)
|
// Observe the coordinator/messages from the *persisted* `graph`
|
||||||
ApprovalModalHost(
|
// StateObject. Deriving them in `init` bound the view to a throwaway
|
||||||
isPresented: $showApproval,
|
// AppGraph rebuilt on every re-init, whose coordinator never receives
|
||||||
state: approvals.state,
|
// core events — so the approval modal never appeared.
|
||||||
onAccept: approvals.accept,
|
OverlayLayer(
|
||||||
onRefuse: approvals.refuse
|
approvals: graph.approvalCoordinator,
|
||||||
|
messages: graph.messages,
|
||||||
|
sendModel: sendModel
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.overlay {
|
.overlay {
|
||||||
@@ -103,27 +97,6 @@ struct RootView: View {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// A pending approval is a blocking modal. Close the sender's detail panel
|
|
||||||
// (e.g. the Share/QR sheet) first, then present the approval sheet — but on
|
|
||||||
// macOS a sheet presented while another is still dismissing is silently
|
|
||||||
// dropped, so defer the presentation until that dismissal finishes.
|
|
||||||
.onChange(of: approvals.state.current?.id) { _, id in
|
|
||||||
guard id != nil else { showApproval = false; return }
|
|
||||||
let wasShowingSheet = sendModel.state.detailPanel != nil
|
|
||||||
sendModel.closeDetailPanel()
|
|
||||||
#if os(macOS)
|
|
||||||
if wasShowingSheet {
|
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.45) {
|
|
||||||
if approvals.state.current != nil { showApproval = true }
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
showApproval = true
|
|
||||||
}
|
|
||||||
#else
|
|
||||||
_ = wasShowingSheet
|
|
||||||
showApproval = true
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
// macOS keeps `scenePhase == .active` even when the app loses focus, so
|
// macOS keeps `scenePhase == .active` even when the app loses focus, so
|
||||||
// drive foreground/background off NSApplication's active state instead —
|
// drive foreground/background off NSApplication's active state instead —
|
||||||
@@ -217,6 +190,70 @@ struct RootView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Hosts the snackbar and the approval modal, observing the coordinator and message
|
||||||
|
/// controller passed in from the persisted `AppGraph`. Kept as a child view so the
|
||||||
|
/// `@ObservedObject` subscriptions are established here (in `body`) against the live
|
||||||
|
/// instances, rather than in `RootView.init` against a throwaway graph.
|
||||||
|
private struct OverlayLayer: View {
|
||||||
|
@ObservedObject var approvals: ApprovalCoordinator
|
||||||
|
@ObservedObject var messages: UiMessageController
|
||||||
|
let sendModel: SendModel
|
||||||
|
|
||||||
|
/// Drives the approval sheet; toggled from the pending-approval `onChange` so the
|
||||||
|
/// presentation can be deferred until the Share/QR sheet has dismissed on macOS.
|
||||||
|
@State private var showApproval = false
|
||||||
|
|
||||||
|
/// macOS-only: an approval arrived while a share/QR sheet was still up. We close
|
||||||
|
/// that sheet and present the approval once its dismissal completes (see
|
||||||
|
/// `sendModel.shareSheetsDismissed`), since macOS drops a sheet shown mid-dismissal.
|
||||||
|
@State private var approvalAwaitingSheetDismiss = false
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
ZStack {
|
||||||
|
SnackbarHost(controller: messages)
|
||||||
|
ApprovalModalHost(
|
||||||
|
isPresented: $showApproval,
|
||||||
|
state: approvals.state,
|
||||||
|
onAccept: approvals.accept,
|
||||||
|
onRefuse: approvals.refuse
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// A pending approval is a blocking modal. Close any open share/QR sheet first
|
||||||
|
// (the detail-view panel *or* the list-level share sheet), then present the
|
||||||
|
// approval sheet: the approval is presented from the app root and neither
|
||||||
|
// platform reliably stacks it over a sheet owned by the Send screen.
|
||||||
|
.onChange(of: approvals.state.current?.id) { _, id in
|
||||||
|
guard id != nil else {
|
||||||
|
showApproval = false
|
||||||
|
approvalAwaitingSheetDismiss = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let wasShowingSheet = sendModel.state.detailPanel != nil
|
||||||
|
|| sendModel.state.shareTargetId != nil
|
||||||
|
sendModel.dismissShareSheets()
|
||||||
|
#if os(macOS)
|
||||||
|
// macOS silently drops a sheet presented while another is still dismissing,
|
||||||
|
// so wait for that sheet's real dismissal completion before presenting.
|
||||||
|
if wasShowingSheet {
|
||||||
|
approvalAwaitingSheetDismiss = true
|
||||||
|
} else {
|
||||||
|
showApproval = true
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
_ = wasShowingSheet
|
||||||
|
showApproval = true
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
#if os(macOS)
|
||||||
|
.onReceive(sendModel.shareSheetsDismissed) { _ in
|
||||||
|
guard approvalAwaitingSheetDismiss else { return }
|
||||||
|
approvalAwaitingSheetDismiss = false
|
||||||
|
if approvals.state.current != nil { showApproval = true }
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// A full-window cover with a centered spinner shown while the core is starting.
|
/// A full-window cover with a centered spinner shown while the core is starting.
|
||||||
private struct CoreStartingOverlay: View {
|
private struct CoreStartingOverlay: View {
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
|||||||
@@ -25,6 +25,11 @@ struct SendState: Equatable {
|
|||||||
var selectedTransferId: UInt64?
|
var selectedTransferId: UInt64?
|
||||||
var transferThumbnails: [UInt64: Data] = [:]
|
var transferThumbnails: [UInt64: Data] = [:]
|
||||||
var detailPanel: TransferDetailPanel?
|
var detailPanel: TransferDetailPanel?
|
||||||
|
/// Transfer whose share panel is presented inline from the list context menu
|
||||||
|
/// (distinct from `detailPanel == .share`, which shows it from the detail view).
|
||||||
|
/// Held in the model — not `SendScreen` @State — so the approval flow can dismiss
|
||||||
|
/// it centrally before presenting its modal.
|
||||||
|
var shareTargetId: UInt64?
|
||||||
var receiverHistory: [ReceiverRequestModel] = []
|
var receiverHistory: [ReceiverRequestModel] = []
|
||||||
var isLoadingReceivers = false
|
var isLoadingReceivers = false
|
||||||
var isDeleteConfirmationOpen = false
|
var isDeleteConfirmationOpen = false
|
||||||
@@ -56,6 +61,18 @@ final class SendModel: ObservableObject {
|
|||||||
private let messages: UiMessageController
|
private let messages: UiMessageController
|
||||||
private var cancellables = Set<AnyCancellable>()
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
|
/// Fires *after* a share/QR sheet (the detail-view panel or the list-level share
|
||||||
|
/// sheet) has finished animating out. The approval flow waits on this to present
|
||||||
|
/// its modal on macOS, where a sheet shown while another is still dismissing is
|
||||||
|
/// dropped — using the real completion instead of a guessed delay.
|
||||||
|
private let shareSheetsDismissedSubject = PassthroughSubject<Void, Never>()
|
||||||
|
var shareSheetsDismissed: AnyPublisher<Void, Never> {
|
||||||
|
shareSheetsDismissedSubject.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Invoked by a share sheet's `onDismiss` completion.
|
||||||
|
func shareSheetDidDismiss() { shareSheetsDismissedSubject.send(()) }
|
||||||
|
|
||||||
init(
|
init(
|
||||||
repository: CoreGateway,
|
repository: CoreGateway,
|
||||||
fileSystemService: FileSystemService,
|
fileSystemService: FileSystemService,
|
||||||
@@ -201,6 +218,17 @@ final class SendModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
func closeDetailPanel() { state.detailPanel = nil }
|
func closeDetailPanel() { state.detailPanel = nil }
|
||||||
|
|
||||||
|
func openShareTarget(_ transferId: UInt64) { state.shareTargetId = transferId }
|
||||||
|
func closeShareTarget() { state.shareTargetId = nil }
|
||||||
|
|
||||||
|
/// Dismisses every share/QR surface at once — the detail-view share panel and the
|
||||||
|
/// list-level share sheet. Used before presenting the receiver-approval modal, so
|
||||||
|
/// no competing sheet is left open (macOS drops a sheet shown over another).
|
||||||
|
func dismissShareSheets() {
|
||||||
|
state.detailPanel = nil
|
||||||
|
state.shareTargetId = nil
|
||||||
|
}
|
||||||
|
|
||||||
func requestDeleteTransfer() { state.isDeleteConfirmationOpen = true }
|
func requestDeleteTransfer() { state.isDeleteConfirmationOpen = true }
|
||||||
func dismissDeleteTransfer() { if !state.isDeleting { state.isDeleteConfirmationOpen = false } }
|
func dismissDeleteTransfer() { if !state.isDeleting { state.isDeleteConfirmationOpen = false } }
|
||||||
|
|
||||||
@@ -257,8 +285,19 @@ final class SendModel: ObservableObject {
|
|||||||
/// Uses the core's `respondReceiverRequest` (no backend change); applies to
|
/// Uses the core's `respondReceiverRequest` (no backend change); applies to
|
||||||
/// receivers that are still pending or accepted.
|
/// receivers that are still pending or accepted.
|
||||||
func cancelReceiver(requestId: String) {
|
func cancelReceiver(requestId: String) {
|
||||||
|
respondToReceiver(requestId: requestId, accepted: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Approves a single pending receiver by responding to its request positively.
|
||||||
|
/// A fallback for when the approval modal didn't surface — the pending receiver
|
||||||
|
/// can still be accepted from its row in the transfer's receivers panel.
|
||||||
|
func acceptReceiver(requestId: String) {
|
||||||
|
respondToReceiver(requestId: requestId, accepted: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func respondToReceiver(requestId: String, accepted: Bool) {
|
||||||
Task {
|
Task {
|
||||||
let result = await repository.respondReceiverRequest(requestId: requestId, accepted: false, reason: nil)
|
let result = await repository.respondReceiverRequest(requestId: requestId, accepted: accepted, reason: nil)
|
||||||
switch result {
|
switch result {
|
||||||
case .success:
|
case .success:
|
||||||
if let transferId = state.selectedTransferId { refreshReceivers(transferId) }
|
if let transferId = state.selectedTransferId { refreshReceivers(transferId) }
|
||||||
|
|||||||
@@ -7,14 +7,18 @@ struct SendScreen: View {
|
|||||||
@ObservedObject var model: SendModel
|
@ObservedObject var model: SendModel
|
||||||
let windowClass: WindowClass
|
let windowClass: WindowClass
|
||||||
|
|
||||||
/// Transfer whose share panel is presented inline from the list context menu.
|
|
||||||
@State private var shareTarget: Transfer?
|
|
||||||
/// Transfer pending an inline (list-level) delete confirmation.
|
/// Transfer pending an inline (list-level) delete confirmation.
|
||||||
@State private var deleteTarget: Transfer?
|
@State private var deleteTarget: Transfer?
|
||||||
|
|
||||||
private var outgoing: [Transfer] {
|
private var outgoing: [Transfer] {
|
||||||
model.coreState.transfers.filter { $0.direction == .send }
|
model.coreState.transfers.filter { $0.direction == .send }
|
||||||
}
|
}
|
||||||
|
/// The transfer whose list-level share sheet is open, resolved from the model's
|
||||||
|
/// `shareTargetId` (kept in the model so the approval flow can dismiss it).
|
||||||
|
private var shareTarget: Transfer? {
|
||||||
|
guard let id = model.state.shareTargetId else { return nil }
|
||||||
|
return outgoing.first { $0.transferId == id }
|
||||||
|
}
|
||||||
private var selectedTransfer: Transfer? {
|
private var selectedTransfer: Transfer? {
|
||||||
guard let id = model.state.selectedTransferId else { return nil }
|
guard let id = model.state.selectedTransferId else { return nil }
|
||||||
return outgoing.first { $0.transferId == id }
|
return outgoing.first { $0.transferId == id }
|
||||||
@@ -50,9 +54,10 @@ struct SendScreen: View {
|
|||||||
// composer drawer on the outer body, so the two don't clash). Opens the
|
// composer drawer on the outer body, so the two don't clash). Opens the
|
||||||
// share panel over the list without navigating into the transfer detail.
|
// share panel over the list without navigating into the transfer detail.
|
||||||
.adaptiveDrawer(
|
.adaptiveDrawer(
|
||||||
isPresented: Binding(get: { shareTarget != nil }, set: { if !$0 { shareTarget = nil } }),
|
isPresented: Binding(get: { shareTarget != nil }, set: { if !$0 { model.closeShareTarget() } }),
|
||||||
windowClass: windowClass,
|
windowClass: windowClass,
|
||||||
onDismiss: { shareTarget = nil }
|
onDismiss: model.closeShareTarget,
|
||||||
|
onDismissed: model.shareSheetDidDismiss
|
||||||
) {
|
) {
|
||||||
if let shareTarget {
|
if let shareTarget {
|
||||||
TransferSharePanel(model: model, transfer: shareTarget)
|
TransferSharePanel(model: model, transfer: shareTarget)
|
||||||
@@ -92,7 +97,8 @@ struct SendScreen: View {
|
|||||||
.adaptiveDrawer(
|
.adaptiveDrawer(
|
||||||
isPresented: Binding(get: { model.state.detailPanel != nil }, set: { _ in }),
|
isPresented: Binding(get: { model.state.detailPanel != nil }, set: { _ in }),
|
||||||
windowClass: windowClass,
|
windowClass: windowClass,
|
||||||
onDismiss: model.closeDetailPanel
|
onDismiss: model.closeDetailPanel,
|
||||||
|
onDismissed: model.shareSheetDidDismiss
|
||||||
) {
|
) {
|
||||||
if let panel = model.state.detailPanel {
|
if let panel = model.state.detailPanel {
|
||||||
DetailPanelContent(model: model, transfer: transfer, panel: panel)
|
DetailPanelContent(model: model, transfer: transfer, panel: panel)
|
||||||
@@ -127,7 +133,7 @@ struct SendScreen: View {
|
|||||||
.contextMenu {
|
.contextMenu {
|
||||||
if transfer.ticket != nil {
|
if transfer.ticket != nil {
|
||||||
Button {
|
Button {
|
||||||
shareTarget = transfer
|
model.openShareTarget(transfer.transferId)
|
||||||
} label: {
|
} label: {
|
||||||
Label(String(localized: L10n.Transfer.shareTitle), systemSymbol: .squareAndArrowUp)
|
Label(String(localized: L10n.Transfer.shareTitle), systemSymbol: .squareAndArrowUp)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -142,7 +142,8 @@ struct DetailPanelContent: View {
|
|||||||
loading: model.state.isLoadingReceivers,
|
loading: model.state.isLoadingReceivers,
|
||||||
events: model.coreState.events,
|
events: model.coreState.events,
|
||||||
transferTotalSize: transfer.totalSize,
|
transferTotalSize: transfer.totalSize,
|
||||||
onCancel: model.cancelReceiver
|
onCancel: model.cancelReceiver,
|
||||||
|
onAccept: model.acceptReceiver
|
||||||
)
|
)
|
||||||
case .share:
|
case .share:
|
||||||
TransferSharePanel(model: model, transfer: transfer)
|
TransferSharePanel(model: model, transfer: transfer)
|
||||||
@@ -193,6 +194,7 @@ struct ReceiverHistoryPanel: View {
|
|||||||
let events: [CoreEventModel]
|
let events: [CoreEventModel]
|
||||||
let transferTotalSize: UInt64
|
let transferTotalSize: UInt64
|
||||||
let onCancel: (String) -> Void
|
let onCancel: (String) -> Void
|
||||||
|
let onAccept: (String) -> Void
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
PanelContainer(title: String(localized: L10n.Transfer.receiversTitle)) {
|
PanelContainer(title: String(localized: L10n.Transfer.receiversTitle)) {
|
||||||
@@ -203,7 +205,12 @@ struct ReceiverHistoryPanel: View {
|
|||||||
} else {
|
} else {
|
||||||
ForEach(Array(receivers.enumerated()), id: \.element.id) { index, receiver in
|
ForEach(Array(receivers.enumerated()), id: \.element.id) { index, receiver in
|
||||||
if index > 0 { Divider().overlay(colors.borderDefault) }
|
if index > 0 { Divider().overlay(colors.borderDefault) }
|
||||||
ReceiverRow(receiver: receiver, sendProgress: sendProgress(for: receiver), onCancel: onCancel)
|
ReceiverRow(
|
||||||
|
receiver: receiver,
|
||||||
|
sendProgress: sendProgress(for: receiver),
|
||||||
|
onCancel: onCancel,
|
||||||
|
onAccept: onAccept
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -224,6 +231,7 @@ private struct ReceiverRow: View {
|
|||||||
let receiver: ReceiverRequestModel
|
let receiver: ReceiverRequestModel
|
||||||
let sendProgress: TransferProgress?
|
let sendProgress: TransferProgress?
|
||||||
let onCancel: (String) -> Void
|
let onCancel: (String) -> Void
|
||||||
|
let onAccept: (String) -> Void
|
||||||
|
|
||||||
/// Only pending requests can be cancelled per-receiver: the core rejects a
|
/// Only pending requests can be cancelled per-receiver: the core rejects a
|
||||||
/// negative response to an already-accepted request ("...not approved, or it
|
/// negative response to an already-accepted request ("...not approved, or it
|
||||||
@@ -257,14 +265,27 @@ private struct ReceiverRow: View {
|
|||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
if isCancelable {
|
if isCancelable {
|
||||||
Button(role: .destructive) {
|
VStack(alignment: .trailing, spacing: 8) {
|
||||||
onCancel(receiver.id)
|
Button(role: .destructive) {
|
||||||
} label: {
|
onCancel(receiver.id)
|
||||||
Text(String(localized: L10n.Button.refuse))
|
} label: {
|
||||||
.font(VniType.bodySmall)
|
Text(String(localized: L10n.Button.refuse))
|
||||||
|
.font(VniType.bodySmall)
|
||||||
|
}
|
||||||
|
.buttonStyle(.borderless)
|
||||||
|
.tint(.red)
|
||||||
|
// Fallback approve action, in case the approval modal didn't surface.
|
||||||
|
Button {
|
||||||
|
onAccept(receiver.id)
|
||||||
|
} label: {
|
||||||
|
Text(String(localized: L10n.Button.approve))
|
||||||
|
.font(VniType.bodySmall).fontWeight(.medium)
|
||||||
|
.foregroundStyle(.white)
|
||||||
|
.padding(.horizontal, 16).padding(.vertical, 7)
|
||||||
|
.background(Color.green, in: Capsule())
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
}
|
}
|
||||||
.buttonStyle(.borderless)
|
|
||||||
.tint(.red)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
|||||||
@@ -7,11 +7,16 @@ struct AdaptiveDrawer<DrawerContent: View>: ViewModifier {
|
|||||||
@Binding var isPresented: Bool
|
@Binding var isPresented: Bool
|
||||||
let windowClass: WindowClass
|
let windowClass: WindowClass
|
||||||
let onDismiss: () -> Void
|
let onDismiss: () -> Void
|
||||||
|
/// Fired after the sheet's dismissal animation completes (as opposed to
|
||||||
|
/// `onDismiss`, which requests the close). Lets callers serialize a follow-up
|
||||||
|
/// sheet against this one's actual teardown instead of guessing a delay.
|
||||||
|
let onDismissed: (() -> Void)?
|
||||||
@ViewBuilder let drawerContent: () -> DrawerContent
|
@ViewBuilder let drawerContent: () -> DrawerContent
|
||||||
|
|
||||||
func body(content: Content) -> some View {
|
func body(content: Content) -> some View {
|
||||||
content.sheet(
|
content.sheet(
|
||||||
isPresented: Binding(get: { isPresented }, set: { if !$0 { onDismiss() } })
|
isPresented: Binding(get: { isPresented }, set: { if !$0 { onDismiss() } }),
|
||||||
|
onDismiss: onDismissed
|
||||||
) {
|
) {
|
||||||
SheetChrome(onClose: onDismiss) { drawerContent() }
|
SheetChrome(onClose: onDismiss) { drawerContent() }
|
||||||
.modifier(PhoneDetents(enabled: windowClass == .phone))
|
.modifier(PhoneDetents(enabled: windowClass == .phone))
|
||||||
@@ -56,11 +61,12 @@ extension View {
|
|||||||
isPresented: Binding<Bool>,
|
isPresented: Binding<Bool>,
|
||||||
windowClass: WindowClass,
|
windowClass: WindowClass,
|
||||||
onDismiss: @escaping () -> Void,
|
onDismiss: @escaping () -> Void,
|
||||||
|
onDismissed: (() -> Void)? = nil,
|
||||||
@ViewBuilder content: @escaping () -> DrawerContent
|
@ViewBuilder content: @escaping () -> DrawerContent
|
||||||
) -> some View {
|
) -> some View {
|
||||||
modifier(AdaptiveDrawer(
|
modifier(AdaptiveDrawer(
|
||||||
isPresented: isPresented, windowClass: windowClass,
|
isPresented: isPresented, windowClass: windowClass,
|
||||||
onDismiss: onDismiss, drawerContent: content
|
onDismiss: onDismiss, onDismissed: onDismissed, drawerContent: content
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user