Search code examples
iosipadswiftui

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

  • 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()
        }
    }