macOS SwiftUI Table with more than 10 columns?

Viewed 1119

I've tried to add the Group {} trick to get more than 10 elements in a Table view, but the fails to compile just like when there is more than 10 elements without the group.

    var body: some View {
      Table(viewModel.tableArrayOfStructs, sortOrder: $sortOrder) {
        Group {
          TableColumn("One", value: \.One).width(min: 35, ideal: 35, max:   60)
          TableColumn("Two", value: \.Two).width(30)
          TableColumn("Three", value: \.Three).width(50)
          TableColumn("Four", value: \.Four).width(min: 150, ideal: 200, max: nil)
          TableColumn("Five", value: \.Five).width(50)
          TableColumn("Six", value: \.Six).width(min: 50, ideal: 55, max: nil)
          TableColumn("Seven", value: \.Seven).width(88)
          TableColumn("Eight", value: \.Eight).width(88)
          TableColumn("Nine", value: \.Nine).width(20)
          TableColumn("Ten", value: \.Ten).width(50)
       }
       TableColumn("Eleven", value: \.Eleven).width(50)

    }

If I add the eleventh+ column also into a new group I get the same issue. The compiler reports:

The compiler is unable to type-check this expression in reasonable time; try breaking up the expression into distinct sub-expressions

Is there any means to have a table with more than 10 columns short of dropping down to NSViewRepresentable?

3 Answers

Apple Developer Technical Support was able to help. They recommended being explicit with the value: type information in the TableColumn to give the compiler some help. Without explicit type info with or without the group the compiler simply gives up. I also found that once you add one group, you need to add all TableColumns to be within groups (which have 10 or less TableColums per group)

var body: some View {
  Table(viewModel.tableArrayOfStructs, sortOrder: $sortOrder) {
    Group {
      TableColumn("One", value: \ViewModel.TableArrayOfStructs.One).width(min: 35, ideal: 35, max:   60)
      TableColumn("Two", value: \ViewModel.TableArrayOfStructs.Two).width(30)
      TableColumn("Three", value: \ViewModel.TableArrayOfStructs.Three).width(50)
      TableColumn("Four", value: \ViewModel.TableArrayOfStructs.Four).width(min: 150, ideal: 200, max: nil)
      TableColumn("Five", value: \ViewModel.TableArrayOfStructs.Five).width(50)
      TableColumn("Six", value: \ViewModel.TableArrayOfStructs.Six).width(min: 50, ideal: 55, max: nil)
      TableColumn("Seven", value: \ViewModel.TableArrayOfStructs.Seven).width(88)
      TableColumn("Eight", value: \ViewModel.TableArrayOfStructs.Eight).width(88)
      TableColumn("Nine", value: \ViewModel.TableArrayOfStructs.Nine).width(20)
      TableColumn("Ten", value: \ViewModel.TableArrayOfStructs.Ten).width(50)
   }
   Group {
     TableColumn("Eleven", value: \ViewModel.TableArrayOfStructs.Eleven).width(50)
   }

}

Thank you very much for the tip of using Group. I was not sure it was going to work on TableColumn the same way as with other View but it works. In my case, it was not compulsory to enclose all table columns within group.

I also receive the tip from Apple Developer Technical Support to be explicit about the value type: \ViewModel.property instead of .property.

We can also use computed property to generate a TableColumn like this:

var firstNameColumn: TableColumn<ViewModel, KeyPathComparator<ViewModel>, Text, Text> {
    TableColumn("Firstname", value: \ViewModel.firstname)
}

The first Text refer to the \ViewModel.firstname that generates automatically a text while the second Text is for the label "FirstName"

In some case, when the type cannot be compare, you can replace KeyPathComparator by Never.

For instance for a date:

var dateColumn: TableColumn<ViewModel, Never, DateView, Text> {
    TableColumn("Date") {doc in
        DateView(date: doc.date)
    }
}

Where DateView is a custom View.

Hope that helps others

Regards

Vincent

An example as requested in the comment (untested).

var body: some View {
  Table(viewModel.tableArrayOfStructs, sortOrder: $sortOrder) {
    Group {
      self.foo //since foo has the @VB annotation, these will just be “pasted” in here. 
    }
    self.foobar //not a @VB, so it returns a specific view
    self.bar(count: …)
 }

@ViewBuilder //not always necessary but usually very handy, depending on what you return
var foo: some View {
      TableColumn("One", value: \.One).width(min: 35, ideal: 35, max:   60)
      TableColumn("Two", value: \.Two).width(30)
      TableColumn("Three", value: \.Three).width(50)
      TableColumn("Four", value: \.Four).width(min: 150, ideal: 200, max: nil)
      TableColumn("Five", value: \.Five).width(50)
      TableColumn("Six", value: \.Six).width(min: 50, ideal: 55, max: nil)
      TableColumn("Seven", value: \.Seven).width(88)
      TableColumn("Eight", value: \.Eight).width(88)
      TableColumn("Nine", value: \.Nine).width(20)
      TableColumn("Ten", value: \.Ten).width(50)
   }

var foobar: some View {
  Group {
          TableColumn("One", value: \.One).width(min: 35, ideal: 35, max:   60)
          TableColumn("Two", value: \.Two).width(30)
          TableColumn("Three", value: \.Three).width(50)
  }
}

func bar(count: Int) -> some View
{
   //as with foo, return your sub view
   //annotate with @ViewBuilder if necessary
}

Point is, you don’t have to construct your entire view in the view’s body. Depending on programming style, code organisation,semantics, … ; split up your code into other child view structs or (private) vars and funcs within a view struct.

Related