'NavigationView in iPad popover does not work properly in SwiftUI

I have the following code that displays a popover when a button is tapped:

struct ContentView: View {

    @State private var show = false

    var body: some View {

        Button("Open") {
            self.show.toggle()
        }.popover(isPresented: $show, content: {
//            NavigationView {
                ScrollView {
                    ForEach(0...10, id: \.self) {_ in
                        Text("Test popover ...")
                    }.padding()
                }
//            }
        })

    }
}

enter image description here

If I add a NavigationView in popover's content then I get this :

enter image description here

Any idea why this happens?

It works fine if I set a fixed frame for the content, but I do not wanna do that since I want the popover to resize according to it's content.



Solution 1:[1]

Probably on iPad they've got into chicken-egg problem with size detection, so just finalised with minimum.

Anyway, the solution would be to set .frame explicitly, either with predefined values (for iPad it is not so bad), or with dynamically calculated (eg. from outer frame via GeometryReader)

Here is an example. Tested with Xcode 12 / iPadOS 14

demo

struct TestPopover: View {

    @State private var show = false

    var body: some View {
        GeometryReader { gp in
            VStack {
                Button("Open") {
                    self.show.toggle()
                }.popover(isPresented: $show, content: {
                    NavigationView {
                        ScrollView {   // or List
                            ForEach(0...10, id: \.self) {_ in
                                Text("Test popover ...")
                            }.padding()
                        }
                        .navigationBarTitle("Test", displayMode: .inline)
                    }
                    .frame(width: gp.size.width / 3, height: gp.size.height / 3)
                })
            }.frame(maxWidth: .infinity, maxHeight: .infinity)
        }
    }
}

Variant 2: Partially calculated on outer size, partially on inner size.

demo2

struct TestPopover: View {

    @State private var show = false
    @State private var popoverWidth = CGFloat(100)

    var body: some View {
        GeometryReader { gp in
            VStack {
                Button("Open") {
                    self.show.toggle()
                }.popover(isPresented: $show, content: {
                    NavigationView {
                        ScrollView {   // or List
                            ForEach(0...10, id: \.self) {_ in
                                Text("Test popover ...").fixedSize()
                            }.padding()
                            .background(GeometryReader {
                                Color.clear
                                    .preference(key: ViewWidthKey.self, value: $0.frame(in: .local).size.width)
                            })
                            .onPreferenceChange(ViewWidthKey.self) {
                                self.popoverWidth = $0
                            }
                        }
                        .navigationBarTitle("Test", displayMode: .inline)
                    }
                    .frame(width: self.popoverWidth, height: gp.size.height / 3)
                })
            }.frame(maxWidth: .infinity, maxHeight: .infinity)
        }
    }
}

struct ViewWidthKey: PreferenceKey {
    typealias Value = CGFloat
    static var defaultValue = CGFloat.zero
    static func reduce(value: inout Value, nextValue: () -> Value) {
        value += nextValue()
    }
}

backup

Solution 2:[2]

Asperi's answer is great and thorough, but thought I'd add one for the lazier among us.

The tiny popover window is a bug introduced in iPadOS 13.4 (popovers appeared as you'd expect in 13.0.x - 13.3.x). I filed FB7640734 about it, which currently shows "less than 10" similar reports and is still open.

The easy workaround, which I use in a production app written in SwiftUI running on iOS, iPadOS, and Mac Catalyst is to add this after your NavigationView:

.frame(minWidth: 320, idealWidth: 400, maxWidth: nil, minHeight: 500, idealHeight: 700, maxHeight: nil, alignment: .top)

I.e. in the context of the OP's sample code:

struct ContentView: View {

    @State private var show = false

    var body: some View {

        Button("Open") {
            self.show.toggle()
        }.popover(isPresented: $show, content: {
            NavigationView {
                ScrollView {
                    ForEach(0...10, id: \.self) {_ in
                        Text("Test popover ...")
                    }.padding()
                }
            }.frame(minWidth: 320, idealWidth: 400, maxWidth: nil,
                    minHeight: 500, idealHeight: 700, maxHeight: nil,
                    alignment: .top)
        })

    }
}

This sets a decently-sized popover that will expand between 320-400 points wide and 500x700 points high, which in practice is a good size for a popover (any larger and you probably should be using something other than a popover).

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 ggruen