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:
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?
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.
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.
@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?