exyte / PopupView

Toasts and popups library written with SwiftUI
MIT License
3.38k stars 258 forks source link

How to use popup in deeply nested view? #80

Closed mywristbands closed 1 year ago

mywristbands commented 1 year ago

In my app, I would like to display popups from views that are deeply nested in my app. When displaying built-in SwiftUI alerts, confirmation dialogs, sheets, etc., this is very easy to do. However, with this PopupView library, it is much more difficult. This is because, as you explain in your Note:

Please keep in mind that the popup calculates its position using the frame of the view you attach it to. So you'll likely want to attach it to the root view of your screen.

However, if I attach a popup to the root of my screen, but want to define the actual popup view from within a deeply nested view (so that I can more dynamically create the popup view for any situation), this is challenging to implement. To understand what I mean:

struct RootView: View {
  @State private var isPresented = false
  var body: some View {
    View1()
      .popup($isPresented) { /* Some view defined in View 7 */ } 
  }
}

struct View1: View {
  var body: some View { View2() }
}

struct View2: View {
  var body: some View { View3() }
}

... etc ...

struct View7: View {
  var body: some View {
    Text("This is a deeply nested subview")
    /* How would I define the view presented in the popup in RootView? */
  }
}

To resolve this problem, I had an idea to create an EnvironmentObject that any sub-view could use to change the popup presented, but it's only possible if I define the view parameter as type AnyView. Here's the EnvironmentObject I thought of:

class PopupRootModel: ObservableObject {
  @Published var params: Params<AnyView> = .init(view: { AnyView(EmptyView()) })
}

extension PopupRootModel {
  struct Params<PopupContent: View> {
    var isPresented: Bool = false
    var type: Popup<Int, PopupContent>.PopupType = .`default`
    var position: Popup<Int, PopupContent>.Position = .bottom
    var animation: Animation = Animation.easeOut(duration: 0.3)
    var autohideIn: Double? = nil
    var dragToDismiss: Bool = true
    var closeOnTap: Bool = true
    var closeOnTapOutside: Bool = false
    var backgroundColor: Color = Color.clear
    var dismissCallback: () -> Void = {}
    @ViewBuilder var view: () -> PopupContent
  }
}

And then I define the following view modifier to attach the environment object (PopupRootModel) to the root view:

extension View {
  func popupRoot() -> some View {
    PopupRoot(rootView: self)
  }
}

private struct PopupRoot<RootView: View>: View {
  let rootView: RootView

  @StateObject private var model = PopupRootModel()

  var body: some View {
    rootView
      .popup(
        isPresented: $model.params.isPresented,
        type: model.params.type,
        position: model.params.position,
        animation: model.params.animation,
        autohideIn: model.params.autohideIn,
        dragToDismiss: model.params.dragToDismiss,
        closeOnTap: model.params.closeOnTap,
        closeOnTapOutside: model.params.closeOnTapOutside,
        backgroundColor: model.params.backgroundColor,
        dismissCallback: model.params.dismissCallback,
        view: model.params.view
      )
      .environmentObject(model)
  }
}

Then, any sub-view could access the PopupRootModel, and edit the params field to set the popup to whatever it wants.

While I believe the above will work, I'd rather avoid using AnyView since it can have performance costs.

Another option would be to have a separate params property for every popup I need in my app. So I might have an errorParams, successParams, filterSheetParams, etc. However, this could get pretty unwieldy as I develop my app and may have dozens of different popup views, so it doesn't seem like an ideal option either.

How do you recommend going about presenting a popup from a deeply nested view?

f3dm76 commented 1 year ago

@mywristbands, sorry, I do not have any recommendations for your scenario. PopupView is just a tool, you can use it however you see fit. Or fork it and improve to your liking.

mywristbands commented 1 year ago

@f3dm76 That's ok, I just wanted to see if anyone had already figured this out before I implemented a solution myself. Thanks for letting me know.