NavigationView in iPad popover does not work properly in SwiftUI
Asked Answered
B

2

14

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.

Birk answered 15/4, 2020 at 11:52 Comment(1)
Did you find a workaround, or is there a bug report with Apple?Leda
B
8

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

Baxley answered 4/8, 2020 at 18:44 Comment(2)
This solves the problem for width, but actually I could set a fixed size for that, what's more important is the popover height, let's say I have a List in the popover, I need the user to be able to see as much as possible from the list without scrolling, so the popover should adapt to the List's content size, do you have any idea how to get this value ?Birk
Height of ScrollView/List can be infinite (if there are hundreds of rows), so out of screen. You can calculate height same as shown for width and decide if to limit it (if it is out of screen height) or assign as-calculated. See SwiftUI: Make ScrollView scrollable only if it exceeds the height of the screen for example.Baxley
A
9

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).

Accused answered 7/10, 2020 at 22:57 Comment(1)
iOS15 and still the same issue with small popoverAdenoidectomy
B
8

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

Baxley answered 4/8, 2020 at 18:44 Comment(2)
This solves the problem for width, but actually I could set a fixed size for that, what's more important is the popover height, let's say I have a List in the popover, I need the user to be able to see as much as possible from the list without scrolling, so the popover should adapt to the List's content size, do you have any idea how to get this value ?Birk
Height of ScrollView/List can be infinite (if there are hundreds of rows), so out of screen. You can calculate height same as shown for width and decide if to limit it (if it is out of screen height) or assign as-calculated. See SwiftUI: Make ScrollView scrollable only if it exceeds the height of the screen for example.Baxley

© 2022 - 2024 — McMap. All rights reserved.