What I'm Looking For
I'm hoping someone can help me understand what's going on and why SwiftUI is behaving the way it is in this scenario.
What I was trying to do
I was originally trying to create a menu with a picker that has a list of calendars for the user to choose from. What was important to me was showing a colored circle to indicate the color of the calendar along with the title. At the time, I couldn't figure out how to get the icons colored in the picker menu. So, I thought a custom popover with a list would work. This is what led to me discovering this issue.
Setup
- I have a class that is managing creating or retrieving data for me asynchronously.
- I'm using that data to populate the list of the picker menu and/or popover list
- I have a
Stateproperty that holds onto the data in the view after being fetched - That data is fetched inside of a
.taskmodifier (also triedTaskinsideonAppear) - Lastly, I have a view that consists of a
VStackthat houses the button triggering the popover as well as a picker to act as the menu. Both are populated with the same data
Problem
When the picker is commented out, the popover always shows up empty, but it retains a frame. When the picker is uncommented, the data not only shows up in the picker, but it shows up in the popover as well. This is what where I'm really stumped. Why does the list in the popover only populate when the Picker I there as well?
Things I've Tried
- Putting the popover view into its own file
- Using
onAppearinstead of.task - Using a
VStackinstead of aListinside the popover - Making the results from the fetch sit inside the data manager class as an optional and then resetting any time the data fetch is completed
Code
Can easily copy and try for yourself
import SwiftUI
struct TestView: View {
let dataManager = DataManager()
@State private var items: [DataItem] = []
@State private var popoverIsPresented = false
var body: some View {
VStack {
Button {
popoverIsPresented = true
} label: {
Text("Choose Item")
}
.popover(isPresented: $popoverIsPresented, content: {
List {
ForEach(items) { item in
Text(item.title)
}
}
})
// List {
// ForEach(items) { item in
// Text(item.title)
// }
// }
}
.task { items = await dataManager.fetchData() }
}
}
#Preview {
TestView()
.frame(minWidth: 300, minHeight: 300)
}
struct DataItem: Identifiable {
let id = UUID()
let title: String
}
class DataManager {
func fetchData() async -> [DataItem] {
Array(repeating: .init(title: "This is an example item"), count: 10)
}
}
- Run it as is
- Then try uncommenting the Picker and you should then see the data appear in both views
Best Guess ♂️
- the popover doesn't get updated once the
Stateproperty changes (which would be a bug I think)
If I understand correctly, when you refer to a picker, you are actually referring to the
Listin your example, right?I was able to reproduce the issue by uncommenting the
List. However, there is then an error in the console:Let's fix this, to make sure it is not the cause of the problem. This means building the dummy array in a different way. For example:
When the
Listis commented out, thePopoverstays empty, as you described. My guess is that thebodyof the view is not being refreshed when the array is assigned to the state variable, because none of the visible content is dependent on the state variable. This may indeed be a bug, as you were suggesting.As a workaround, try making the
bodydependent on the state variable in some way. For example, you could show the count of items as a hiddenTextelement behind theButton:Regarding your original objectives:
The answer to SwiftUI Picker issue with Rectangle() instead of Text() shows how this can be done using a regular
Picker. See also Swift Picker Containing A View Not Showing The Color component.