In SwiftUI on iOS, I have some data that I want to display in a popover. It has to be in a list because I want it to be able to utilize swipeActions. The problem is that for some reason, lists don't automatically size to fit in a popover, you have to manually resize them in order to get it to work. I've tried using GeometryReader
, fixedSize
, setting the frame
width and height to infinity, etc and nothing has worked. It only sizes to fit if it's a ForEach
without a list or in a scrollview, but then the swipeActions don't work. Is there a solution that exists so that I don't have to manually set the width and height in order for it to size correctly? Here is the code:
import SwiftUI
struct MainView: View {
@State private var popoverIsPresented: Bool = false
var body: some View {
Button {
popoverIsPresented = true
} label: {
Label("Plus", systemImage: "plus")
}
.popover(isPresented: $popoverIsPresented) {
ListView().presentationCompactAdaptation(.popover)
}
}
}
struct ListView: View {
@State private var names: [String] = ["User 1", "User 2", "User 3", "User 4"]
var body: some View {
List {
ForEach(names, id: \.self) { name in
Text(name).swipeActions(allowsFullSwipe: false) {
Button {
} label: {
Label("Delete", systemImage: "trash")
}
.tint(.red)
}
}
}
.scrollDisabled(true)
//.fixedSize()
//.frame(maxWidth: .infinity, maxHeight: .infinity)
.frame(width: 222, height: 333)
.listStyle(.plain)
}
}
One way to solve is to use .onGeometryChange
to measure the size of the list content and use this to set the size of the list.
The height of the list can be computed from the full height of one row and the number of rows.
The full height of a row can be measured by attaching the .onGeometryChange
modifier to the row background. Only the first row needs to be measured.
The width of the list should be determined by the widest row content. This means measuring the width of each row.
Since the width of the content does not include the list row insets, you will probably want to add some reserve width to allow space for the insets. Alternatively, you could set the list row insets to 0 and add padding to the content instead. However, this will affect the way the row separators are aligned.
Apply .fixedSize()
to the text, to prevent it from wrapping.
The popover is initially sized based on the ideal size of its content and the ideal size of a List
is normally very small. So it is important to set a sensible idealWidth
and idealHeight
on the list. These values actually represent a maximum limit on the width and height, once the measured sizes are applied.
struct ListView: View {
@State private var names: [String] = ["User 1", "User 2", "The quick brown fox", "jumps over the lazy dog"]
@State private var rowWidth: CGFloat?
@State private var rowHeight: CGFloat?
var body: some View {
List {
ForEach(Array(names.enumerated()), id: \.offset) { index, name in
Text(name).swipeActions(allowsFullSwipe: false) {
Button {
} label: {
Label("Delete", systemImage: "trash")
}
.tint(.red)
}
.fixedSize()
.onGeometryChange(for: CGFloat.self) { proxy in
proxy.size.width
} action: { width in
if width > rowWidth ?? 0 {
rowWidth = width
}
}
.listRowBackground(
Group {
if index == 0 {
Color.clear
.onGeometryChange(for: CGFloat.self) { proxy in
proxy.size.height
} action: { height in
rowHeight = height
}
}
}
)
}
}
.scrollDisabled(true)
.listStyle(.plain)
.frame(idealWidth: 400, idealHeight: 333)
.frame(maxWidth: listWidth, maxHeight: listHeight)
}
private var listWidth: CGFloat? {
if let rowWidth {
rowWidth + 40 // allow for list row insets
} else {
nil
}
}
private var listHeight: CGFloat? {
if let rowHeight {
rowHeight * CGFloat(names.count)
} else {
nil
}
}
}