skip to Main Content

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!

2

Answers


  1. 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.
            }
        }
    }
    
    Login or Signup to reply.
  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))
        }
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search