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()
}
// }
})
}
}
If I add a NavigationView
in popover's content then I get this :
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.
You see, navigation views let us display new screens of content by sliding them in from the right edge. Each screen can have its own title, and it's the job of SwiftUI to make sure that title is shown in the navigation view at all times – you'll see the old title animate away, while the new title animates in.
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).
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
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.
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()
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With