2

I am creating a reusable bottom up panel where the view displayed inside the bottom up panel will be different. I also wanted this panel to be a view modifier. I have created view modifiers in past, but hasn't passed view as a view modifier content ever. When I try to pass the view, I am getting an error described below.

View modified code:

struct BottomPanel: ViewModifier {
    @Binding var isPresented: Bool
    let panelContent: Content

    init(isPresented: Binding<Bool>, @ViewBuilder panelContent: @escaping () -> Content) {
        self.panelContent = panelContent()
        self._isPresented = isPresented
    }

    func body(content: Content) -> some View {
        content.overlay(self.$isPresented.wrappedValue ? bottomPanelContent() : nil)
    }

    @ViewBuilder
    private func bottomPanelContent() -> some View {
        GeometryReader { geometry in
            VStack(spacing: 0) {
                self.panelContent
            }
            // some modifiers to change the color or height of the panel.
        }
    }
}

View extension:

extension View {
    func bottomPanel(isPresented: Binding<Bool>, @ViewBuilder panelContent: @escaping () -> BottomPanel.Content) -> some View {
        return modifier(BottomPanel(isPresented: isPresented, panelContent: panelContent)
    }
}

Content view and child view that I wish to open in bottom up panel:

struct ContentView: View {
    @State var showBottomPanel: Bool = false

    var body: some View {
        VStack {
            Button(action: { self.showBottomPanel = true}) {
                Text("Click me to open bottom panel")
            }
        }
        .bottomPanel(isPresented: $self.showBottomPanel, panelContent: { ChildView() })
    }
}

struct ChildView: View {
    var body: some View {
        VStack {
            Button("Click Me 1", action: {}).foregroundColor(.blue)
            Button("Click Me 2", action: {}).foregroundColor(.red)
        }
    }
}

Error: Cannot convert value of type 'ChildView' to closure result type 'BottomPanel.Content' (aka '_ViewModifier_Content<BottomPanel>').

What am I doing wrong? How do I pass the view to BottomPanel?

Note: I have removed a lot of code from bottom panel to keep the code post short, but let me know if it's needed and I can share.

Thanks for reading!

tech_human
  • 6,592
  • 16
  • 65
  • 107

2 Answers2

2

You wrote this:

    let panelContent: Content

The problem is that Content here is a special type defined by SwiftUI. It's a type-erased container kind of like AnyView, except that you cannot create your own values of this Content type.

You need to introduce your own generic type parameter for the panel content. You should also store the ViewBuilder callback that creates the panel content rather than computing it even when the panel isn't shown.

struct BottomPanel<PanelContent: View>: ViewModifier {
    @Binding var isPresented: Bool
    let panelContent: () -> PanelContent

    init(
        isPresented: Binding<Bool>,
        panelContent: @escaping () -> PanelContent
    ) {
        self.panelContent = panelContent
        self._isPresented = isPresented
    }

    func body(content: Content) -> some View {
        content.overlay(self.$isPresented.wrappedValue ? bottomPanelContent() : nil)
    }

    @ViewBuilder
    private func bottomPanelContent() -> some View {
        GeometryReader { geometry in
            VStack(spacing: 0) {
                panelContent()
            }
            // some modifiers to change the color or height of the panel.
        }
    }
}

Then you need to update your bottomPanel modifier to also be generic. In this case you can use the opaque parameter syntax instead of adding an explicit PanelContent generic parameter:

extension View {
    func bottomPanel(
        isPresented: Binding<Bool>,
        @ViewBuilder panelContent: @escaping () -> some View
    ) -> some View {
        return modifier(BottomPanel(isPresented: isPresented, panelContent: panelContent))
    }
}
rob mayoff
  • 375,296
  • 67
  • 796
  • 848
0

I was able to get the content to present by making a handful of changes. Without knowing how the content is meant to look, I cannot definitively say whether or not this actually presents the content as desired - but it presents the content nonetheless, circumventing the error that you were trying to bypass

import Foundation
import SwiftUI

struct ContentViewTest: View {
    @State var showBottomPanel: Bool = false
    
    var body: some View {
        VStack {
            Button(action: { self.showBottomPanel = true}) {
                Text("Click me to open bottom panel")
            }
        }
        .bottomPanel(isPresented: $showBottomPanel, panelContent: { AnyView(ChildView()) }())
    }
}

struct ChildView: View {
    var body: some View {
        VStack {
            Button("Click Me 1", action: {}).foregroundColor(.blue)
            Button("Click Me 2", action: {}).foregroundColor(.red)
        }
    }
}



extension View {
    func bottomPanel(isPresented: Binding<Bool>, panelContent: AnyView) -> some View {
        return modifier(BottomPanel(isPresented: isPresented, panelContent: AnyView(panelContent)))
    }
}


struct BottomPanel: ViewModifier {
    @Binding var isPresented: Bool
    let panelContent: AnyView
    
    init(isPresented: Binding<Bool>, panelContent: AnyView) {
        self.panelContent = AnyView(panelContent)
        self._isPresented = isPresented
    }
    
    func body(content: Content) -> some View {
        content.overlay(self.$isPresented.wrappedValue ? bottomPanelContent() : nil)
    }
    
    @ViewBuilder
    private func bottomPanelContent() -> some View {
        GeometryReader { geometry in
            VStack(spacing: 0) {
                self.panelContent
            }
            // some modifiers to change the color or height of the panel.
        }
    }
}
nickreps
  • 903
  • 8
  • 20
  • panelContent won't always be of type ChildView. I am trying to create BottomPanel as reusable component, so panelContent can be ChildView, it can be SummaryView or any other custom view. I just want to pass different views to bottom panel and that will be displayed. – tech_human Feb 25 '23 at 01:57
  • @tech_human Ah, I see. I've edited the answer to accept a view of any type, not just `ChildView`. Is the edit more along the lines of what you are looking for? – nickreps Feb 25 '23 at 02:31