I'm trying to debug an issue where, after the update of a @Published
value on an @ObservedObject
in a LazyVStack
, the cells don't appear to redraw.
Imagine I have this app below, that draws 18 cells, each representing a different country on a continent (apologize for the ugly colors):
I have a an array of Country
objects that I render as cells onscreen. Each Country
conforms to Identifiable
, and uses a monotonically increasing number to implement the Identifiable
protocol.
My question is this: imagine this @ObservedObject
, or, in this demo code below, @State
, changes somehow. What is returned is a list of identical size, where every Model object returns a monotonically increasing ID
. How does the LazyVStack
know that it should redraw? I wrote the code below and I feel that it should not work, yet somehow it does. When I click on the "Asia" button, or I click on the "Africa" button, it renders the way I expect and immediately updates the cells -- yet I feel that clicking on the opposite button should have no effects, because it will be loading a collection of the same size, with the same IDs, and so SwiftUI should effectively re-use the old cells.
That scenario I am expecting is the bug that I believe, but cannot yet prove, my actual app is experiencing. I am attempting to write demo code to prove this theory.
Here is the complete demo code that I believe should reproduce this issue (but somehow is working fine).
import SwiftUI
public struct Continent {
public let name: String
public let countries: [Country]
public init(name: String, countries: [Country]) {
self.name = name
self.countries = countries
}
}
extension Continent: Hashable {
public func hash(into hasher: inout Hasher) {
hasher.combine(countries)
}
}
public struct Country: Identifiable, Equatable {
public var id: Int {
return index
}
public let regionCode: String
public let index: Int
public init(regionCode: String, index: Int = 0) {
self.regionCode = regionCode
self.index = index
}
public var countryName: String? {
Locale.current.localizedString(forRegionCode: regionCode)
}
}
extension Country: Hashable {
public func hash(into hasher: inout Hasher) {
hasher.combine(index)
}
}
struct ContentView: View {
@State var continents: [Continent] = continentListAfrica
@ViewBuilder private func Grid(stack: Continent, columns: Int) -> some View {
let chunks = stride(from: 0, to: stack.countries.count, by: columns).map {
Array(stack.countries[$0..<min($0 + columns, stack.countries.count)])
}
Text(stack.name)
ForEach(chunks, id: \.self) { chunk in
GridRow(chunk: chunk, stack: stack, columns: columns)
}
}
@ViewBuilder private func GridRow(chunk: [Country], stack: Continent, columns: Int) -> some View {
let emptyElements = columns - chunk.count
HStack(spacing: 5) {
ForEach(chunk) { country in
Cell(title: country.countryName ?? "Unknown")
}
if emptyElements > 0 {
ForEach(0..<emptyElements, id: \.self) { _ in
Cell(title: "Blank")
}
}
}
}
@ViewBuilder private func CountryListView(continentList: [Continent]) -> some View {
LazyVStack(spacing: 0) {
ForEach(continentList, id: \.self) { stack in
Grid(stack: stack, columns: 3)
}
}
}
var body: some View {
VStack {
ScrollView {
CountryListView(continentList: continents)
}
HStack {
Button {
self.continents = ContentView.continentListAsia
} label: {
Text("Asia")
}
Button {
self.continents = ContentView.continentListAfrica
} label: {
Text("Africa")
}
}
}
.padding()
}
private static let continentListAfrica: [Continent] = [
Continent(name: "Africa", countries: [
Country(regionCode: "dz", index: 1),
Country(regionCode: "bi", index: 2),
Country(regionCode: "cm", index: 3),
Country(regionCode: "td", index: 4),
Country(regionCode: "km", index: 5),
Country(regionCode: "eg", index: 6),
Country(regionCode: "er", index: 7),
Country(regionCode: "sz", index: 8),
Country(regionCode: "et", index: 9),
Country(regionCode: "gm", index: 10),
Country(regionCode: "gh", index: 11),
Country(regionCode: "gn", index: 12),
Country(regionCode: "ke", index: 13),
Country(regionCode: "lr", index: 14),
Country(regionCode: "ly", index: 15),
Country(regionCode: "mw", index: 16),
Country(regionCode: "mr", index: 17),
Country(regionCode: "mu", index: 18)
])
]
private static let continentListAsia: [Continent] = [
Continent(name: "Asia", countries: [
Country(regionCode: "af", index: 1),
Country(regionCode: "bd", index: 2),
Country(regionCode: "bt", index: 3),
Country(regionCode: "bn", index: 4),
Country(regionCode: "ps", index: 5),
Country(regionCode: "id", index: 6),
Country(regionCode: "ir", index: 7),
Country(regionCode: "iq", index: 8),
Country(regionCode: "kw", index: 9),
Country(regionCode: "lb", index: 10),
Country(regionCode: "my", index: 11),
Country(regionCode: "mv", index: 12),
Country(regionCode: "mm", index: 13),
Country(regionCode: "om", index: 14),
Country(regionCode: "pk", index: 15),
Country(regionCode: "qa", index: 16),
Country(regionCode: "sa", index: 17),
Country(regionCode: "sg", index: 18)
])
]
}
struct Cell: View {
private let title: String
public init(title: String) {
self.title = title
}
public var body: some View {
ZStack {
Text(title.prefix(10))
.background(Color.purple)
}.frame(width: 125, height: 125)
.background(Color.green)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}