Kenzot
Kenzot

Reputation: 1

How to force .popover not to create vertical system padding for Text

I am facing a problem: I need to create a popover with an error message for a text field. I am using .popover in the below method:

.popover(
    isPresented: $isValid,
    attachmentAnchor: .point(.center),
    arrowEdge: .bottom
) {
    Text("Some error text")
        .presentationCompactAdaptation(.popover)
}

However, I am getting parasitic vertical padding at the top and bottom.

Example 1

But if I use some kind of representation with exact dimensions, there are no additional insets like with Text.

.popover(
    isPresented: $isValid,
    attachmentAnchor: .point(.center),
    arrowEdge: .bottom
) {
    Rectangle().fill(.yellow)
        .frame(width: 100, height: 100)
        .presentationCompactAdaptation(.popover)
}

Example 2

I observe the problem only with Text.

I found many solutions on stackoverflow involving bulk extensions with custom popover implementations.

But is there a simpler solution? I don't want to add large custom implementations to the project purely because of the two insets.

I've tried a lot of options (for example, I've tried using ScrollView for Text) trying to get rid of the vertical padding and make the popover wrap the Text to fit the content, but I haven't been able to.

If anyone knows what the issue is, please help me out.

P.S. target: iOS 16

Upvotes: 0

Views: 389

Answers (1)

MatBuompy
MatBuompy

Reputation: 2093

You could go for a custom implementation like this, it's not that large actually. I've just built it myself a few seconds ago:

extension View {
    
    @ViewBuilder
    func customPopOver(isPresented: Bool, message: String, popoverOffset: CGFloat = -30, indicatorOffset: CGFloat = 14) -> some View {
        self
            .overlay {
                RoundedRectangle(cornerRadius: 10, style: .continuous)
                    .fill(.background)
                    .overlay(alignment: .bottom) {
                        IndicatorShape()
                            .rotation(.degrees(180))
                            .frame(width: 20, height: 15)
                            .foregroundStyle(.background)
                            //.shadow(radius: 2)
                            .offset(y: indicatorOffset)
                    }
                    .overlay {
                        Text(message)
                            .presentationCompactAdaptation(.popover)
                    }
                    .shadow(radius: 10)
                    .offset(y: popoverOffset)
                    .frame(minWidth: 160, maxWidth: .infinity)
                    .opacity(isPresented ? 1 : 0)
                    .scaleEffect(isPresented ? 1 : 0)
                    .animation(.spring(duration: 0.3), value: isPresented)
                
            }
    }
}

struct IndicatorShape: Shape {
    
    func path(in rect: CGRect) -> Path {
        return Path { path in
            let width = rect.width
            let height = rect.height
            
            path.move(to: CGPoint(x: width / 2, y: 0))
            path.addLine(to: CGPoint(x: 0, y: height))
            path.addLine(to: CGPoint(x: width, y: height))
        }
    }
}

You can add more flexibilty to the extension adding more parameters like colors, witdth, etc. You only need the IndicatorShape if you want the little triangle at the bottom of the popover. Its use is pretty straightforward:

struct ContentView: View {
    
    @State var isValid = false
    
    var body: some View {
        VStack {
            Button("Show popover") {
                isValid.toggle()
            }
            .customPopOver(isPresented: isValid, message: "Some text")
        }
    }
}

I hope to have helped you. Let me know your thought about this!

Upvotes: 0

Related Questions