Using Coadable with AnyView?

I'm attempting to decode XML into a SwiftUI hierarchy and I'd like to be permissive on what elements can contain what. So for example, I may have this type of markup:

<Nav>
  <Table>
    <Row>Foo</Row>
    <Row>Bar</Row>
  </Table>
</Nav>

And in this case Nav could contain any element types. I'm using the XMLCoder library which has Coadable support for XML however I am hitting some challenges with Swift's type system. For my Nav struct I have something like this:

struct Nav: View, Coadable {
  var content: [AnyView]

  var body: some View {
    NavigationView {
      content
   }
  }
}

Ignore the body for now. My understanding is that AnyView being weakly typed will cause problems with the decoder. Is there anyway to override the decoder to inform it to the types of the child elements based upon their element names?

Do Foo and Bar denote different view types here, or is this just content for the same view type? That is, would <Row> always render to something like a Text view with different content, or should view types be different based on the content of <Row> elements?

Yes I have views set up for Table and Row.

This is probably off-topic for the Swift forums, but I think something like this might be easier expressed and tested with an intermediate representation.

Rather than trying to directly go from XML -> SwiftUI Views, try going from XML -> Your custom view description format -> SwiftUI Views

It'll be much easier to specify the static mapping up front, since you'll just be able to switch over the cases of your enum inside your body.

2 Likes

What is inside the content?

Are you trying to pass in an Array of Views for example:

var content: [AnyView] = [
    TableHeader(),
    Row(),
    Row(),
    Row(),
    TableFooter()
]

And then hoping this gets rendered.

@alenm yes, I know that John Sundell has implemented something like this but I haven't had any succes getting a reply from him

  • Passing in a list of views has a hard limit. It's 10. You would need to work around this limit. There are workarounds to this, but something to keep in mind.
  • Here is a blog post by John Sundell on avoiding AnyView
  • @harlanhaskins point on converting your XML response into some struct before putting them into a View is a good point.
  • I think what you want is some @Viewbuilder functionality and the ability to store it in a property. This is coming into Swift 5.4 and here is a blog post with related content.

Is there anyway to override the decoder to inform it to the types of the child elements based upon their element names?

What about doing all this logic in a view model after it's been decoded. I'm posting this as a simple example. I might be missing something from your point of view.

// Step: 1 The XML Response Struct
struct Note: Codable, Equatable, DynamicNodeEncoding {
    
    let id: UInt
    let user: [String]?
    let categories: String
    let row: [String]
    
    enum CodingKeys: String, CodingKey {
            case id
            case user
            case row
            case categories = "category"
        }

    static func nodeEncoding(for key: CodingKey) -> XMLEncoder.NodeEncoding {
        switch key {
        case Note.CodingKeys.id: return .both
        default: return .element
        }
    }
}


// Step: 2 ViewModel that parse the XML and assigns it to a Publisher
class ViewModel: ObservableObject {
    
    @Published var note: Note
    // @Published var table: NavTable
    
    init() {
        let sourceXML = """
        <table id="123">
            <id>123</id>
            <user>Bob</user>
            <user>Jane</user>
            <category>Reminder</category>
            <row>I am row 1</row>
            <row>I am row 2</row>
            <row>I am row 3</row>
        </table>
        """
        let note = try! XMLDecoder().decode(Note.self, from: Data(sourceXML.utf8))
        self.note = note
        // You can further start creating a custom structs here
        // ...
        // self.table = NavTable(name: "Testing", rows: note.row.map({RowItem(item: $0)}))
    }
}


// Step: 3 The many ways to render a row
struct ContentView: View {
    
    @StateObject var viewModel: ViewModel = ViewModel()
    
    var body: some View {
        NavigationView {
            VStack {
                
                // Example 1 - Using ForEach
                ForEach(viewModel.note.row, id: \.self) { item in
                    Text(item)
                }
                
                Divider()
                
                // Example 2 - Similar to Example 1 just in a function
                rows(items: viewModel.note.row)
                
                Divider()
                
                // Example 3 - Using a Generic view with a closure
                // This generic view could contain many other views
                MyCustomNavList(viewModel.note.row) { item in
                    TableRow(text: item)
                }
                
            }
            .navigationTitle("Navigation Title")
        }
    }
    
    func rows(items: [String]) -> some View {
        ForEach(items, id: \.self) { item in
            Text(item)
        }
    }
    
}


struct TableRow: View {
    let text: String
    var body: some View {
        VStack {
            Text(text)
        }
    }
}


struct MyCustomNavList<Element, RowContent>: View where Element: Hashable, RowContent: View {
    
     private let data: [Element]
     private let rowContent: (Element) -> RowContent
    
    init(_ data: [Element], @ViewBuilder rowContent: @escaping (Element) -> RowContent) {
        self.data = data
        self.rowContent = rowContent
    }
    
    var body: some View {
            ForEach(data, id:\.self) { item in
                rowContent(item)
            }
    }

}

I see, so in this case I wouldn't need to worry about the type system if all elements decoded to a general Element struct and then that tree could be used to generate the View tree?

Element is a generic placeholder with type constraint.

struct MyCustomNavList<Element, RowContent>: View where Element: Hashable, RowContent: View {
    
     private let data: [Element]
     private let rowContent: (Element) -> RowContent

    init(_ data: [Element], @ViewBuilder rowContent: @escaping (Element) -> RowContent) {
        self.data = data
        self.rowContent = rowContent
    }
    var body: some View {
            ForEach(data, id:\.self) { item in
                rowContent(item)
            }
    }
}

The way to read this is...
MyCustomNavList is a View where the Element conforms to the Hashable protocol and the RowContent conforms to a View.

I'm passing in viewModel.note.row which is an array of [String]. The String type already conforms to Hashable and my TableRow is a View.

 MyCustomNavList(viewModel.note.row) { item in
     TableRow(text: item)
 }

You always need to think about the type system
Here is good WWDC video to watch on embracing type inference.

It's hard to provide further help without seeing code examples. Stackoverflow could help you out further with implementation details.