Files
swift-composable-architectu…/Sources/ComposableArchitecture/SwiftUI/Deprecated/ActionSheet.swift
Stephen Celis 93fa026cbc Re-introduce legacy alert and action sheet APIs (#2379)
* Re-introduce legacy alert and action sheet APIs

With 1.0 we removed `View.alert(_ store:dismiss:)`, which was a
problematic API. It used different SwiftUI alert APIs depending on OS
version, which led to different runtime behavior for apps using it
depending on the OS and an unfixable bug:

https://github.com/pointfreeco/swift-composable-architecture/issues/1981

This was reason enough to remove the API, but it was also an API that
predated the Composable Architecture's navigation tools. This meant it
awkwardly took an explicit `dismiss:` action, and it was up to the
developer to remember to use this action to manually clear out state.
The Composable Architecture's navigation tools handle dismissal
automatically once integrated, but also only support iOS 15 alerts at
the moment.

This PR addresses the above: it introduces view modifiers for the old
iOS 13 style of alert (and `actionSheet`) providing some presentation
domain, and it will automatically dismiss these modals accordingly:

  - It adds `View.legacyAlert(store:)`. While we don't love
    `legacyAlert`, we haven't come up with a better option. We need a
    statically unique view modifier, and `alert(store:)` is already in
    use for iOS 15. We're open to suggestions here, though!

  - It adds `View.actionSheet(store:)`.

* wip
2023-08-18 10:34:27 -07:00

89 lines
2.9 KiB
Swift

import SwiftUI
extension View {
/// Displays an action sheet when then store's state becomes non-`nil`, and dismisses it when it
/// becomes `nil`.
///
/// - Parameters:
/// - store: A store that is focused on ``PresentationState`` and ``PresentationAction`` for an
/// alert.
/// - toDestinationState: A transformation to extract alert state from the presentation state.
/// - fromDestinationAction: A transformation to embed alert actions into the presentation
/// action.
@available(
iOS,
introduced: 13,
deprecated: 100000,
message: "use 'View.confirmationDialog(store:)' instead."
)
@available(macOS, unavailable)
@available(
tvOS,
introduced: 13,
deprecated: 100000,
message: "use 'View.confirmationDialog(store:)' instead."
)
@available(
watchOS,
introduced: 6,
deprecated: 100000,
message: "use 'View.confirmationDialog(store:)' instead."
)
public func actionSheet<ButtonAction>(
store: Store<
PresentationState<ConfirmationDialogState<ButtonAction>>, PresentationAction<ButtonAction>
>
) -> some View {
self.actionSheet(store: store, state: { $0 }, action: { $0 })
}
/// Displays an alert when then store's state becomes non-`nil`, and dismisses it when it becomes
/// `nil`.
///
/// - Parameters:
/// - store: A store that is focused on ``PresentationState`` and ``PresentationAction`` for an
/// alert.
/// - toDestinationState: A transformation to extract alert state from the presentation state.
/// - fromDestinationAction: A transformation to embed alert actions into the presentation
/// action.
@available(
iOS,
introduced: 13,
deprecated: 100000,
message: "use 'View.confirmationDialog(store:state:action:)' instead."
)
@available(macOS, unavailable)
@available(
tvOS,
introduced: 13,
deprecated: 100000,
message: "use 'View.confirmationDialog(store:state:action:)' instead."
)
@available(
watchOS,
introduced: 6,
deprecated: 100000,
message: "use 'View.confirmationDialog(store:state:action:)' instead."
)
public func actionSheet<State, Action, ButtonAction>(
store: Store<PresentationState<State>, PresentationAction<Action>>,
state toDestinationState: @escaping (_ state: State) -> ConfirmationDialogState<ButtonAction>?,
action fromDestinationAction: @escaping (_ alertAction: ButtonAction) -> Action
) -> some View {
self.presentation(
store: store, state: toDestinationState, action: fromDestinationAction
) { `self`, $item, _ in
let actionSheetState = store.state.value.wrappedValue.flatMap(toDestinationState)
self.actionSheet(item: $item) { _ in
ActionSheet(actionSheetState!) { action in
if let action = action {
store.send(.presented(fromDestinationAction(action)))
} else {
store.send(.dismiss)
}
}
}
}
}
}