1

I'm trying to create table views based on both List and ScrollView, with a shared base view. My current attempt uses a builder-based approach, but requires closures with AnyView type erasure.

My goal is to avoid using AnyView type erasure, to improve performance, scalability, and design.

My attempts to re-design with generic type parameters have so far failed.

The simplified (and working) code example below displays only a single String column, but captures the basic challenge. (In actuality, these are multi-column tables with various data types, specialized formatters, modifiers, etc.)

import SwiftUI

struct ContentView: View {
    let data = ["a", "b", "c", "d"]
    var body: some View {
        HStack {
            ListTable(title: "My List", data: data)
            ScrollTable(title: "My Scroll", data: data)
        }
    }
}

struct ListTable<T: Hashable>: View {
    var title: String, data: [T]
    var body: some View {
        BaseTable() { header, row in
            VStack {
                header(title)
                List(data, id: \.self) { row($0) }
            }
        }
    }
}

struct ScrollTable<T: Hashable>: View {
    var title: String, data: [T]
    var body: some View {
        BaseTable() { header, row in
            VStack {
                header(title)
                ScrollView { ForEach(data, id: \.self) { row($0) } }
            }
        }
    }
}

struct BaseTable<Content: View, T: Hashable>: View {
    typealias HBuilder = (String) -> AnyView
    typealias RBuilder = (T) -> AnyView
    var builder: (@escaping HBuilder, @escaping RBuilder) -> Content
    var body: some View {
        // NOTE this is where I'd like to avoid type-erasure to AnyView
        builder({ AnyView( Text("Header \($0)") )},
                { AnyView( Text("Row \(String(describing: $0))") ) })
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View { ContentView() }
}

Two related questions that didn't quite provide a solution:

  • 65810765 - How to create a struct parameter that is a closure with input, returning some View instead of AnyView in SwiftUI?
  • 64655195 - How to avoid casting SwiftUI views to AnyView when passing them in as a parameter to a function?
reedes
  • 21
  • 2
  • If you know the result of the `BaseTable` builder, why not just explicitly define the types? `HBuilder = (String) -> Text` then return `Text`? – jnpdx Feb 09 '22 at 05:29
  • Look at `SwiftUI.LazyVGrid` interface design - it actually fits table concept. – Asperi Feb 09 '22 at 05:43
  • jnpdx: that works for the header in the example, but ideally the builders support other types through a generic parameter, such as Toggle, Picker, TextField, or other type. – reedes Feb 09 '22 at 15:50
  • asperi: in my implementation I make extensive use of LazyVGrid in the header/row builders, but I stripped them from the single-column example for clarity. However, I'll experiment with using them as the basis for the explicit type. – reedes Feb 09 '22 at 16:07
  • UPDATE: my first attempt to use LazyVGrid as the explicit view type failed, as it requires a view type parameter. (AnyView works here too, but doesn't solve the problem.) – reedes Feb 09 '22 at 16:32

1 Answers1

1

I have a working answer to my own question.

Creating a 'wrapper' view for the row, which will serve the explicit type used in the row builder closure:

// new 'wrapper' view, which can contain Text, LazyVGrid, etc.
struct RowView<T: Hashable>: View {
    var value: T
    var body: some View {
        Text("Row \(String(describing: value))")
    }
}

// update to original BaseTable
struct BaseTable<Content: View, T: Hashable>: View {
    typealias HBuilder = (String) -> Text
    typealias RBuilder = (T) -> RowView<T>
    var builder: (@escaping HBuilder, @escaping RBuilder) -> Content
    var body: some View {
        builder({ Text("Header \($0)") },
                { RowView<T>(value: $0) })
    }
}

Thanks to the commenters!

reedes
  • 21
  • 2