SwiftUI dynamic number of columns with LazyVGrid

As a septuagenarian my memory is prone to leaks and I therefore rely on this forum and Stack Overflow for discovering (rediscovering?) solutions to problems. I like to give back when I can, so here goes.....

I'm currently doing a project with a variable number of BLE sensors at varying locations and want to display a neat table (grid) of Observations and Locations (in pure SwiftUI) like this:

Temperature: Locn1_value, Locn2_value , ..... Locnx_value, obsTime
Humidity: Locn1_value, Locn2_value ..... Locnxvalue, obsTime
(optionally more sensors)

The SwiftUI View is:
struct SensorObservationsView: View {
    let sensorServer = SensorServer.shared
    @State var latestObservations = [ObservationSummary]()
    @State var obsColumns = Array(repeating: GridItem(.flexible(),spacing: 20), count: 4)

    var body: some View {
        VStack{
            ForEach(latestObservations,id: \.id) { latestObs in
                HStack{
                    LazyVGrid(columns: obsColumns, content: {
                        Text(latestObs.id) .foregroundColor(latestObs.colour)
                        ForEach(latestObs.summaryRows, id:\.id) { row in
                            Text(row.strVal) .foregroundColor(latestObs.colour)
                        }
                        Text(latestObs.summaryRows.last!.strTime) .foregroundColor(latestObs.colour)
                    })
                }
            }
        }
        .onReceive(sensorServer.observationsUpdated, perform: { observationSummaries in
            if observationSummaries.isEmpty { return }
            latestObservations = observationSummaries
            let columns = observationSummaries.last!.summaryRows.count
            var newColumns = [GridItem]()
            #if os(tvOS)
            newColumns.append(GridItem(.fixed(230.0), spacing: 10.0, alignment: .leading))
            #else
            newColumns.append(GridItem(.fixed(130.0), spacing: 10.0, alignment: .leading))
            #endif
            for
in (0..<columns) {
                #if os(tvOS)
                newColumns.append(GridItem(.fixed(170.0), spacing: 10.0, alignment: .trailing))
                #else
                newColumns.append(GridItem(.fixed(70.0), spacing: 10.0, alignment: .trailing))
                #endif
            }
            #if os(tvOS)
            newColumns.append(GridItem(.fixed(190.0), spacing: 10.0, alignment: .trailing))
            #else
            newColumns.append(GridItem(.fixed(90.0), spacing: 10.0, alignment: .trailing))
            #endif
            obsColumns = newColumns
            })
    }
}

SensorServer collects all required characteristics for all active sensors every few minutes, then publishes the set via SensorServer.observationsUpdated. The View's .onReceive then creates an appropriate array of GridItems based on the number of columns in latestObservations (sadly, I named these as "summaryRows" - because the raw observations are in rows). "latestObs.id" is the observation type e.g. "temperature". The observation time for all is the same and taken from the timestamp of the last item of the summary rows(columns). I also adjust the layout depending on the target platform.

PS: SensorServer ensures that there's the same number of location columns, using a default content of "n/a" if there's no valid data from the BLE sensor. The solution is dynamic in that I can add/remove locations (sensors) and not have to recode the View. Sensor data are pre-formatted to strings before sending to the view.

I hope this helps someone, somewhere, sometime. Cheers, Michaela
  • Yes, it helped another ancient coder. I'm working on Swift access to MySQL databases to maintain homemade bookkeeping programs. I wanted to display the results of a query, which can of course have a varying number of columns with varying keys. I tried for a couple of days to figure out the Table/TableColumn SwiftUI View, and then found your post. Something like this is working great:

           ForEach(aDBModel.dbRowsAsDicts, id: .id) { thisDict in         HStack {           LazyVGrid(columns: obsColumns, content: {             ForEach(aDBModel.identifiableFieldNames, id:.id) { aColName in               Text(thisDict.theDict[aColName.theString] ?? "")             }           })         }       }

    Thanks!

    XCode sure beats keypunching, waiting for your deck of cards to run, and then looking up a bunch of numeric error codes!

  • It's great to hear from another coder from the card-punch era: there's a few of us around on here, but not many. I'm currently rewriting my budgeting/expense management app, from Swift/SQLite to SwiftUI & CoreData-CloudKit to allow multi-platform syncing. Thanks for the feedback. Regards, Michaela

Add a Comment