eric
eric

Reputation: 145

SwiftUI .popover frame size incorrect

In iOS 16.4 we can now use .presentationCompactAdaptation(.none) in our .popover to achieve a true popover on iOS (compact screen sizes).

SomeView()
    .popover(isPresented: $isPopoverOpen) {
        Text("Hello world!")
            .fixedSize(horizontal: false, vertical: true)
            .padding()
            .presentationCompactAdaptation(.none)
    }

This will give us something like:

enter image description here

Great, it works as expected!

The issue arises when the Text() in the popover spans multiple lines. For some reason, the popover height will only grow up to a certain height (~3 lines with non-dynamic .body font). Here is an illustration of the issue using some Lorem Ipsum text. Notice how the end gets clipped off because the popover height is too short:

enter image description here

How can I make the popover fit the Text() content? I can statically define the height but I would like the popover to perfectly fit the content.

Upvotes: 5

Views: 2822

Answers (3)

chopsalot
chopsalot

Reputation: 384

I had the same issue. Multiline text inside a popover wasn't being sized correctly. The above solution worked for me, so I turned Md. Ibrahim Hassan's answer into a SwiftUI extension

private struct PopoverMultilineHeightFix: ViewModifier {
    @State var textHeight: CGFloat = 0
 
    func body(content: Content) -> some View {
        content
           .fixedSize(horizontal: false, vertical: true)
            .overlay(
               GeometryReader { proxy in
                   Color
                       .clear
                       .preference(key: ContentLengthPreference.self,
                                   value: proxy.size.height)
               }
           )
           .onPreferenceChange(ContentLengthPreference.self) { value in
               DispatchQueue.main.async {
                  self.textHeight = value
               }
             }
           .frame(height: self.textHeight)
    }
}

private extension PopoverMultilineHeightFix {
    struct ContentLengthPreference: PreferenceKey {
       static var defaultValue: CGFloat { 0 }
       
       static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
          value = nextValue()
       }
    }
}

extension View {
    func popoverMultilineHeightFix() -> some View {
        modifier(PopoverMultilineHeightFix())
    }
}

Just call it like this:

Text("This is some really long text to test the line wrapping to make suire the popover handles it properly")
     .multilineTextAlignment(.leading)
     .popoverMultilineHeightFix()

Upvotes: 1

Yuji
Yuji

Reputation: 704

How do you like this?

You can negotiate size using Layout protocol.

import SwiftUI

struct PopoverContainer: Layout {
    func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
        guard subviews.count == 1 else {
            fatalError("You need to implement your layout!")
        }
        var p = proposal
        p.width = p.width ?? UIScreen.main.bounds.width
        p.height = p.height ?? UIScreen.main.bounds.height

        return subviews[0].sizeThatFits(p) // negotiates possible size
    }
    
    func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
        // entrusts default
    }
}

struct ContentView: View {
    @State private var isPopoverOpen = true
    var body: some View {
        Text("Hello, World!")
            .popover(isPresented: $isPopoverOpen) {
                PopoverContainer {
                    Text("""
                            Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
                            """
                    )
                    .fixedSize(horizontal: false, vertical: true)
                    .padding()
                }
                .presentationCompactAdaptation(.none)
                
            }
    }
}

Upvotes: 5

Md. Ibrahim Hassan
Md. Ibrahim Hassan

Reputation: 5477

You can get the height of the Text element using this approach.

For this, you get the height of the Text and set the height you received. This is how the code would look like

struct ContentLengthPreference: PreferenceKey {
    static var defaultValue: CGFloat { 0 }
    
    static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
        value = nextValue()
    }
}

struct ContentView: View {
    @State private var isPopoverOpen = true
    @State var textHeight: CGFloat = 0 // <-- this
    
    var body: some View {
        Text("Hello, World!")
            .popover(isPresented: $isPopoverOpen) {
                Text("""
                        Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
                        """
                )
                .overlay(
                    GeometryReader { proxy in
                        Color
                            .clear
                            .preference(key: ContentLengthPreference.self,
                                        value: proxy.size.height) // <-- this
                    }
                )
                .onPreferenceChange(ContentLengthPreference.self) { value in // <-- this
                    DispatchQueue.main.async {
                        print (value)
                        self.textHeight = value
                    }
                }
                .fixedSize(horizontal: false, vertical: true)
                .frame(height: textHeight)
                .padding()
                .presentationCompactAdaptation(.none)
                
            }
    }
}

Popover with Long Text Long text

Popover with Short Text Short text

Happy Coding!

Upvotes: 5

Related Questions