What I'd like to do is create a grid of items, where there is either 1 item, or 2 items, per row depending on wether a cell is selected. There is an edge-case where a cell can be alone on a row, yet not selected, and it should only take up the space for which it would had it shared the space with two grid items. The only time a cell should take up the full width of the grid should be when it's selected.
GridRow
that has two, however tapping does not appear to update my state.LazyVGrid(columns: [GridItem(.adaptive(minimum: 150)), GridItem(.flexible()) ]) {
ForEach(locationService.locationGroups, id: \.self) { group in
if let item = group.first(where: { $0.selected }) {
GridRow {
TrailDetailCardView(location: item.location)
.onTapGesture {
locationService.setSelected(for: item, to: !item.selected)
}
}
} else {
GridRow {
ForEach(group, id: \.self) { rowItem in
TrailDetailCardView(location: rowItem.location)
.onTapGesture {
locationService.setSelected(for: rowItem, to: !rowItem.selected)
}
}
}
}
}
}
@Published var locationGroups: [[LocationToggleable]]
however LocationToggleable
is not Observable
, it only contains a selected
property that I put in there to make the object properly Hashable
for other use cases. I think this object, or my usage, is why the .onTapGesture{...}
doesn't seem to update state in my LazyVGrid
.static private func getListGroups(filteredLocations: [LocationToggleable]) -> [[LocationToggleable]] {
var groupedKeys: [[LocationToggleable]] = []
var currentRow: [LocationToggleable] = []
for item in filteredLocations.sorted(by: { $0.location.locationName < $1.location.locationName }) {
if item.selected {
if !currentRow.isEmpty {
groupedKeys.append(currentRow)
currentRow = []
}
groupedKeys.append([item])
} else {
if currentRow.count < 2 {
currentRow.append(item)
} else {
groupedKeys.append(currentRow) // Append the current row to groupedKeys
currentRow = [item]// Start a new row with the current item
}
}
}
if !currentRow.isEmpty {
groupedKeys.append(currentRow)
}
return groupedKeys
}
@Published var locationGroups: [[LocationToggleable]]
and also then update the view.Grid {
ForEach(locationService.locationGroups, id: \.self) { rowItems in
// Check if this row contains a selected item
if rowItems.count == 1 && rowItems[0].selected {
let selectedItem = rowItems[0]
TrailDetailCardView(location: selectedItem.location)
.frame(maxWidth: .infinity) // To make the card take the full width
.matchedGeometryEffect(id: "location-id-\(selectedItem.id)", in: namespace)
.id(selectedItem.id)
.onTapGesture {
locationService.setSelected(for: selectedItem, to: !selectedItem.selected)
}
} else {
GridRow {
ForEach(rowItems, id: \.self) { item in
TrailDetailCardView(location: item.location)
.matchedGeometryEffect(id: "location-id-\(item.id)", in: namespace)
.id(item.id)
.onTapGesture {
locationService.setSelected(for: item, to: !item.selected)
}
if rowItems.count < 2 && !item.selected && locationService.locationGroups.count < 3 {
TrailDetailCardView(location: item.location)
.disabled(true)
.opacity(0.0)
}
}
}
}
}
}
.lineLimit(1)
.minimumScaleFactor(0.75)
.padding()
The solution was to create two columns, and treat the .selected
one as a Section
. In particular treating it as a Section.header
instead of just a section body. In the future, if I want to have extra sections, I can simply define that and keep my other logic for each section outside the body. This doesn't resolve the @State
issues, but it does allow for LazyLoading which improves performance significantly as well as meets the above requirements.
struct LocationListView: View {
@Namespace var namespace: Namespace.ID
@ObservedObject var locationService: LocationService
let gridItems = [
GridItem(.flexible(minimum: UIScreen.main.bounds.width / 2 - 50)),
GridItem(.flexible(minimum: UIScreen.main.bounds.width / 2 - 50))
]
@State var showingFirst = false
var body: some View {
ScrollViewReader { proxy in
ScrollView {
VStack {
LazyVGrid(columns: gridItems) {
ForEach(locationService.toggleableLocations, id: \.self) { item in
if item.selected {
Section(content: {}, header: {
TrailDetailCardView(location: item.location)
.frame(maxWidth: .infinity)
.matchedGeometryEffect(id: "location-id-\(item.id)", in: namespace)
.id(item.id)
.onTapGesture {
proxy.scrollTo(item.id)
locationService.setSelected(for: item, to: !item.selected)
}
})
} else {
TrailDetailCardView(location: item.location)
.matchedGeometryEffect(id: "location-id-\(item.id)", in: namespace)
.id(item.id)
.onTapGesture {
proxy.scrollTo(item.id)
locationService.setSelected(for: item, to: !item.selected)
}
}
}
}
}
.lineLimit(1)
.minimumScaleFactor(0.75)
.padding()
}
}
}
}