Skip to content

Instantly share code, notes, and snippets.

@simme
Created April 4, 2023 08:44
Show Gist options
  • Save simme/cff23d521653f380310c04fc785364c5 to your computer and use it in GitHub Desktop.
Save simme/cff23d521653f380310c04fc785364c5 to your computer and use it in GitHub Desktop.
UIKit helpers for presenting alerts and confirmation dialogs
/*
Example usage:
confirmationDialog(
store: store.scope(state: \.destination, action: UserRecipesListReducer.Action.destination),
state: /UserRecipesListReducer.Destination.State.confirmation,
action: UserRecipesListReducer.Destination.Action.confirmation
).store(in: &subscriptions)
alert(
store: store.scope(state: \.destination, action: UserRecipesListReducer.Action.destination),
state: /UserRecipesListReducer.Destination.State.alert,
action: UserRecipesListReducer.Destination.Action.alert
).store(in: &subscriptions)
*/
extension StoreViewController {
func alert<DestinationState, DestinationAction, Action>(
store: Store<DestinationState?, PresentationAction<DestinationAction>>,
state toAlertState: @escaping (DestinationState) -> AlertState<Action>?,
action fromAlertAction: @escaping (Action) -> DestinationAction
) -> any Cancellable {
self.alert(
store: store.scope(
state: { $0.flatMap(toAlertState) },
action: {
switch $0 {
case .dismiss: return .dismiss
case let .presented(action): return .presented(fromAlertAction(action))
}
}
)
)
}
func alert<Action>(
store: Store<AlertState<Action>?, PresentationAction<Action>>
) -> any Cancellable {
let viewStore = ViewStore(store, observe: { $0 }, removeDuplicates: { ($0 != nil) == ($1 != nil) })
return viewStore.publisher.sink { alertState in
if let alertState {
let alertController = UIAlertController(state: alertState) { action in
if let action {
viewStore.send(.presented(action))
}
}
self.present(alertController, animated: true)
} else {
self.dismiss(animated: true)
}
}
}
func confirmationDialog<DestinationState, DestinationAction, Action>(
store: Store<DestinationState?, PresentationAction<DestinationAction>>,
state toConfirmationDialogState: @escaping (DestinationState) -> ConfirmationDialogState<Action>?,
action fromConfirmationAction: @escaping (Action) -> DestinationAction
) -> any Cancellable {
self.confirmationDialog(
store: store.scope(
state: { $0.flatMap(toConfirmationDialogState) },
action: {
switch $0 {
case .dismiss: return .dismiss
case let .presented(action): return .presented(fromConfirmationAction(action))
}
}
)
)
}
func confirmationDialog<Action>(
store: Store<ConfirmationDialogState<Action>?, PresentationAction<Action>>
) -> any Cancellable {
let viewStore = ViewStore(store, observe: { $0 }, removeDuplicates: { ($0 != nil) == ($1 != nil) })
return viewStore.publisher.sink { confirmationState in
if let confirmationState {
let alertController = UIAlertController(state: confirmationState) { action in
if let action {
viewStore.send(.presented(action))
}
}
self.present(alertController, animated: true)
} else {
self.dismiss(animated: true)
}
}
}
}
@simme
Copy link
Author

simme commented Apr 5, 2023

How does the child state get nilled when the user backs up with the back button?

@acosmicflamingo
Copy link

@simme that's a really good point...I created my own navigation controller and I had to override the popViewController like this:

public final class MyNavigationController: UIViewController {
  // ...
  override public func popViewController(animated: Bool) -> UIViewController? {
    closeSearchControllersIfNeeded()
    configureTransitionAnimationIfNeeded(animated: animated)
    let poppedViewController = super.popViewController(animated: animated)
    if let navigationSupportedController = poppedViewController as?
        UIViewController & TCANavigationSupport,
       animationPhase == .popFromTap
    {
      // Where you'd call popViewController from StoreViewController
      navigationSupportedController.popViewController()
    }
    return poppedViewController
  }

Then in your StoreViewController you'd do something like this:

public final class StoreViewController: UIViewController {
  // ...
  public func popViewController() {
    viewStore.send(.requestToPopViewController)
  }
}

Finally, in your reducer you nil out state:

public struct MyReducer: Reducer {
  // ..
  case .requestToPopViewController:
  defer { state.destination = nil }
  return .none
}

An absolute boilerplate party, which I wish I didn't have to attend but I want to support iOS 15 (which means I can't override navigationItem.backButtonAction), and I don't like how I lose the original back button design when creating my own back button. It's an ugly workaround :(

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment