NEW: Learn to build amazing SwiftUI apps for macOS with my new book! >>

XML Parser

Forums > Swift

Hi @twostraws, could you please help me parse the XML constant string? I need to find every attribute of the tag Link and put it in my data model.

I've used your XMLParser class as coded below, but it can not even find the root element, which is DownloadMultiplo.

import Foundation

class XMLNode {
    let tag: String
    var data: String
    let attributes: [String: String]
    var childNodes: [XMLNode]

    init(tag: String, data: String, attributes: [String: String], childNodes: [XMLNode]) {
        self.tag = tag
        self.data = data
        self.attributes = attributes
        self.childNodes = childNodes
    }

    func getAttribute(_ name: String) -> String? {
        attributes[name]
    }

    func getElementsByTagName(_ name: String) -> [XMLNode] {
        var results = [XMLNode]()

        for node in childNodes {
            if node.tag == name {
                results.append(node)
            }

            results += node.getElementsByTagName(name)
        }

        return results
    }
}

class MicroDOM: NSObject, XMLParserDelegate {
    private let parser: XMLParser
    private var stack = [XMLNode]()
    private var tree: XMLNode?

    init(data: Data) {
        parser = XMLParser(data: data)
        super.init()
        parser.delegate = self
    }

    func parse() -> XMLNode? {
        parser.parse()

        guard parser.parserError == nil else {
            return nil
        }

        return tree
    }

    func parser(_ parser: XMLParser, didStartElement elementName: String, namespaceURI: String?, qualifiedName qName: String?, attributes attributeDict: [String: String] = [:]) {
        let node = XMLNode(tag: elementName, data: "", attributes: attributeDict, childNodes: [])
        stack.append(node)
    }

    func parser(_ parser: XMLParser, didEndElement elementName: String, namespaceURI: String?, qualifiedName qName: String?) {
        let lastElement = stack.removeLast()

        if let last = stack.last {
            last.childNodes += [lastElement]
        } else {
            tree = lastElement
        }
    }

    func parser(_ parser: XMLParser, foundCharacters string: String) {
        stack.last?.data = string
    }
}

let string = """
<DownloadMultiplo DataSolicitada="23/11/2021 00:00" TipoDocumento="IPE" DataConsulta="23/11/2021 11:56">
<Link url="http://siteempresas.bovespa.com.br/DWL/FormDetalheDownload.asp?site=C&prot=919084" Documento="IPE" ccvm="24830" DataRef="11/11/2021 10:30:59" FrmDtRef="dd/mm/aaaa hh:mm" Categoria="Reunião da Administração" Tipo="Conselho de Administração" Especie="Ata" Situacao="Cancelado"/>
<Link url="http://siteempresas.bovespa.com.br/DWL/FormDetalheDownload.asp?site=C&prot=919521" Documento="IPE" ccvm="23493" DataRef="22/11/2021 12:00:59" FrmDtRef="dd/mm/aaaa hh:mm" Categoria="Reunião da Administração" Tipo="Conselho de Administração" Especie="Ata" Situacao="Liberado"/>
<Link url="http://siteempresas.bovespa.com.br/DWL/FormDetalheDownload.asp?site=C&prot=919522" Documento="IPE" ccvm="23493" DataRef="22/11/2021 11:45:59" FrmDtRef="dd/mm/aaaa hh:mm" Categoria="Reunião da Administração" Tipo="Conselho Fiscal" Especie="Ata" Situacao="Liberado"/>
<Link url="http://siteempresas.bovespa.com.br/DWL/FormDetalheDownload.asp?site=C&prot=919523" Documento="IPE" ccvm="54461" DataRef="23/11/2021 23:59:59" FrmDtRef="dd/mm/aaaa" Categoria="Comunicado ao Mercado" Tipo="Outros Comunicados Não Considerados Fatos Relevantes" Especie=" " Situacao="Liberado"/>
<Link url="http://siteempresas.bovespa.com.br/DWL/FormDetalheDownload.asp?site=C&prot=919524" Documento="IPE" ccvm="51209" DataRef="23/11/2021 23:59:59" FrmDtRef="dd/mm/aaaa" Categoria="Comunicado ao Mercado" Tipo="Outros Comunicados Não Considerados Fatos Relevantes" Especie=" " Situacao="Liberado"/>
<Link url="http://siteempresas.bovespa.com.br/DWL/FormDetalheDownload.asp?site=C&prot=919525" Documento="IPE" ccvm="50563" DataRef="23/11/2021 23:59:59" FrmDtRef="dd/mm/aaaa" Categoria="Comunicado ao Mercado" Tipo="Outros Comunicados Não Considerados Fatos Relevantes" Especie=" " Situacao="Liberado"/>
<Link url="http://siteempresas.bovespa.com.br/DWL/FormDetalheDownload.asp?site=C&prot=919526" Documento="IPE" ccvm="52140" DataRef="23/11/2021 23:59:59" FrmDtRef="dd/mm/aaaa" Categoria="Comunicado ao Mercado" Tipo="Outros Comunicados Não Considerados Fatos Relevantes" Especie=" " Situacao="Liberado"/>
<Link url="http://siteempresas.bovespa.com.br/DWL/FormDetalheDownload.asp?site=C&prot=919527" Documento="IPE" ccvm="53961" DataRef="23/11/2021 23:59:59" FrmDtRef="dd/mm/aaaa" Categoria="Comunicado ao Mercado" Tipo="Outros Comunicados Não Considerados Fatos Relevantes" Especie=" " Situacao="Liberado"/>
<Link url="http://siteempresas.bovespa.com.br/DWL/FormDetalheDownload.asp?site=C&prot=919528" Documento="IPE" ccvm="52167" DataRef="23/11/2021 23:59:59" FrmDtRef="dd/mm/aaaa" Categoria="Comunicado ao Mercado" Tipo="Outros Comunicados Não Considerados Fatos Relevantes" Especie=" " Situacao="Liberado"/>
</DownloadMultiplo>
"""

let dom = MicroDOM(data: Data(string.utf8))
let tree = dom.parse()
print(tree?.tag ?? "N/A")

It prints out N/A

1      

It's because your XML is not valid. Specifically, the URLs in your Link elements.

http://siteempresas.bovespa.com.br/DWL/FormDetalheDownload.asp?site=C&prot=919084

The parser chokes on &prot, which it tries to read as an XML entity because of the & but then there is no closing ; to end the entity.

You can see this if you add the following delegate method to the MicroDOM class:

func parser(_ parser: XMLParser, parseErrorOccurred parseError: Error) {
    let error = parseError as NSError
    print(error.userInfo["NSXMLParserErrorMessage"]!)
    print("line: \(error.userInfo["NSXMLParserErrorLineNumber"]!)")
    print("column: \(error.userInfo["NSXMLParserErrorColumn"]!)")
}

Prints:

EntityRef: expecting ';'

line: 2
column: 86

I'm honestly not sure how to solve this with the given XML. I tried replacing the raw & with an entity &amp;, which is what you are supposed to do, but the parser resolves &amp;prot to &prot and we're back where we started. Same with &amp;amp;prot. I'm not sure this is even allowed in XML attributes.

Do you control this XML? Ampersands are allowed within CDATA sections, so something like this works:

<Link Documento="IPE" ccvm="24830" DataRef="11/11/2021 10:30:59" FrmDtRef="dd/mm/aaaa hh:mm" Categoria="Reunião da Administração" Tipo="Conselho de Administração" Especie="Ata" Situacao="Cancelado">
<![CDATA[http://siteempresas.bovespa.com.br/DWL/FormDetalheDownload.asp?site=C&prot=919084]]>
</Link>

I reformatted your XML and altered the root-level code to show some results and it looked good:

let string = """
    <DownloadMultiplo DataSolicitada="23/11/2021 00:00" TipoDocumento="IPE" DataConsulta="23/11/2021 11:56">
    <Link Documento="IPE" ccvm="24830" DataRef="11/11/2021 10:30:59" FrmDtRef="dd/mm/aaaa hh:mm" Categoria="Reunião da Administração" Tipo="Conselho de Administração" Especie="Ata" Situacao="Cancelado"><![CDATA[http://siteempresas.bovespa.com.br/DWL/FormDetalheDownload.asp?site=C&prot=919084]]></Link>
    <Link Documento="IPE" ccvm="23493" DataRef="22/11/2021 12:00:59" FrmDtRef="dd/mm/aaaa hh:mm" Categoria="Reunião da Administração" Tipo="Conselho de Administração" Especie="Ata" Situacao="Liberado"><![CDATA[http://siteempresas.bovespa.com.br/DWL/FormDetalheDownload.asp?site=C&prot=919521]]></Link>
    <Link Documento="IPE" ccvm="23493" DataRef="22/11/2021 11:45:59" FrmDtRef="dd/mm/aaaa hh:mm" Categoria="Reunião da Administração" Tipo="Conselho Fiscal" Especie="Ata" Situacao="Liberado"><![CDATA[http://siteempresas.bovespa.com.br/DWL/FormDetalheDownload.asp?site=C&prot=919522]]></Link>
    <Link Documento="IPE" ccvm="54461" DataRef="23/11/2021 23:59:59" FrmDtRef="dd/mm/aaaa" Categoria="Comunicado ao Mercado" Tipo="Outros Comunicados Não Considerados Fatos Relevantes" Especie=" " Situacao="Liberado"><![CDATA[http://siteempresas.bovespa.com.br/DWL/FormDetalheDownload.asp?site=C&prot=919523]]></Link>
    <Link Documento="IPE" ccvm="51209" DataRef="23/11/2021 23:59:59" FrmDtRef="dd/mm/aaaa" Categoria="Comunicado ao Mercado" Tipo="Outros Comunicados Não Considerados Fatos Relevantes" Especie=" " Situacao="Liberado"><![CDATA[http://siteempresas.bovespa.com.br/DWL/FormDetalheDownload.asp?site=C&prot=919524]]></Link>
    <Link Documento="IPE" ccvm="50563" DataRef="23/11/2021 23:59:59" FrmDtRef="dd/mm/aaaa" Categoria="Comunicado ao Mercado" Tipo="Outros Comunicados Não Considerados Fatos Relevantes" Especie=" " Situacao="Liberado"><![CDATA[http://siteempresas.bovespa.com.br/DWL/FormDetalheDownload.asp?site=C&prot=919525]]></Link>
    <Link Documento="IPE" ccvm="52140" DataRef="23/11/2021 23:59:59" FrmDtRef="dd/mm/aaaa" Categoria="Comunicado ao Mercado" Tipo="Outros Comunicados Não Considerados Fatos Relevantes" Especie=" " Situacao="Liberado"><![CDATA[http://siteempresas.bovespa.com.br/DWL/FormDetalheDownload.asp?site=C&prot=919526]]></Link>
    <Link Documento="IPE" ccvm="53961" DataRef="23/11/2021 23:59:59" FrmDtRef="dd/mm/aaaa" Categoria="Comunicado ao Mercado" Tipo="Outros Comunicados Não Considerados Fatos Relevantes" Especie=" " Situacao="Liberado"><![CDATA[http://siteempresas.bovespa.com.br/DWL/FormDetalheDownload.asp?site=C&prot=919527]]></Link>
    <Link Documento="IPE" ccvm="52167" DataRef="23/11/2021 23:59:59" FrmDtRef="dd/mm/aaaa" Categoria="Comunicado ao Mercado" Tipo="Outros Comunicados Não Considerados Fatos Relevantes" Especie=" " Situacao="Liberado"><![CDATA[http://siteempresas.bovespa.com.br/DWL/FormDetalheDownload.asp?site=C&prot=919528]]></Link>
</DownloadMultiplo>
"""

let dom = MicroDOM(data: Data(string.utf8))
if let tree = dom.parse() {
    tree.childNodes.forEach {
        print($0.data)
    }
}

Printed:

http://siteempresas.bovespa.com.br/DWL/FormDetalheDownload.asp?site=C&prot=919084
http://siteempresas.bovespa.com.br/DWL/FormDetalheDownload.asp?site=C&prot=919521
http://siteempresas.bovespa.com.br/DWL/FormDetalheDownload.asp?site=C&prot=919522
http://siteempresas.bovespa.com.br/DWL/FormDetalheDownload.asp?site=C&prot=919523
http://siteempresas.bovespa.com.br/DWL/FormDetalheDownload.asp?site=C&prot=919524
http://siteempresas.bovespa.com.br/DWL/FormDetalheDownload.asp?site=C&prot=919525
http://siteempresas.bovespa.com.br/DWL/FormDetalheDownload.asp?site=C&prot=919526
http://siteempresas.bovespa.com.br/DWL/FormDetalheDownload.asp?site=C&prot=919527
http://siteempresas.bovespa.com.br/DWL/FormDetalheDownload.asp?site=C&prot=919528

1      

Actually I do not control the XML format, it comes from a CVM (the Brazilian version of the SEC) API.

It is strange because the Python XML parser works just fine with the original format.

I'll try to parse it with Python, convert the data to a JSON format and then access it with Swift with my own API.

Thanks a lot for your help!

1      

Hacking with Swift is sponsored by RevenueCat

SPONSORED Spend less time managing in-app purchase infrastructure so you can focus on building your app. RevenueCat gives everything you need to easily implement, manage, and analyze in-app purchases and subscriptions without managing servers or writing backend code.

Get Started

Sponsor Hacking with Swift and reach the world's largest Swift community!

Python's parser must be more lenient than Swift's.

I tried several online XML syntax checkers and they all choked at the same spot for the same reason.

1      

Hi @roosterboy, as a follow up in this topic.

I can now parse correctlly the XML just adding the modifier - .replacingOccurrences(of: "&prot", with: "&prot") - to my string.

The problem is that it returns a dictionary and my Documento Model is:

struct Documento: Identifiable, Codable {
    let id = UUID()
    let url: String
    let documento: String
    let ccvm: String
    let dataRef: String
    let frmDtRef: String
    let categoria: String
    let tipo: String
    let especie: String
    let situacao: String

    enum CodingKeys: String, CodingKey {
        case url
        case documento = "Documento"
        case ccvm
        case dataRef = "DataRef"
        case frmDtRef = "FrmDtRef"
        case categoria = "Categoria"
        case tipo = "Tipo"
        case especie = "Especie"
        case situacao = "Situacao"
    }
}

So I had to encode the dictionary to a JSON format and then decode it back to my data type.

class DocumentoViewModel: ObservableObject {
    @Published var documentos: [Documento] = []

    init() {
        fetch(
            """
            <DownloadMultiplo DataSolicitada="16/12/2021 00:00" TipoDocumento="IPE" DataConsulta="16/12/2021 17:00">
            <Link url="http://siteempresas.bovespa.com.br/DWL/FormDetalheDownload.asp?site=C&prot=926177" Documento="IPE" ccvm="22144" DataRef="15/12/2021 23:59:59" FrmDtRef="dd/mm/aaaa" Categoria="Comunicado ao Mercado" Tipo="Outros Comunicados Não Considerados Fatos Relevantes" Especie=" " Situacao="Cancelado"/>
            <Link url="http://siteempresas.bovespa.com.br/DWL/FormDetalheDownload.asp?site=C&prot=926363" Documento="IPE" ccvm="50091" DataRef="16/12/2021 23:59:59" FrmDtRef="dd/mm/aaaa" Categoria="Comunicado ao Mercado" Tipo="Outros Comunicados Não Considerados Fatos Relevantes" Especie=" " Situacao="Liberado"/>
            <Link url="http://siteempresas.bovespa.com.br/DWL/FormDetalheDownload.asp?site=C&prot=926364" Documento="IPE" ccvm="53902" DataRef="16/12/2021 23:59:59" FrmDtRef="dd/mm/aaaa" Categoria="Comunicado ao Mercado" Tipo="Outros Comunicados Não Considerados Fatos Relevantes" Especie=" " Situacao="Liberado"/>
            <Link url="http://siteempresas.bovespa.com.br/DWL/FormDetalheDownload.asp?site=C&prot=926365" Documento="IPE" ccvm="56154" DataRef="16/12/2021 23:59:59" FrmDtRef="dd/mm/aaaa" Categoria="Comunicado ao Mercado" Tipo="Outros Comunicados Não Considerados Fatos Relevantes" Especie=" " Situacao="Liberado"/>
            <Link url="http://siteempresas.bovespa.com.br/DWL/FormDetalheDownload.asp?site=C&prot=926366" Documento="IPE" ccvm="55573" DataRef="16/12/2021 23:59:59" FrmDtRef="dd/mm/aaaa" Categoria="Comunicado ao Mercado" Tipo="Outros Comunicados Não Considerados Fatos Relevantes" Especie=" " Situacao="Liberado"/>
            <Link url="http://siteempresas.bovespa.com.br/DWL/FormDetalheDownload.asp?site=C&prot=926367" Documento="IPE" ccvm="55611" DataRef="16/12/2021 23:59:59" FrmDtRef="dd/mm/aaaa" Categoria="Comunicado ao Mercado" Tipo="Outros Comunicados Não Considerados Fatos Relevantes" Especie=" " Situacao="Liberado"/>
            <Link url="http://siteempresas.bovespa.com.br/DWL/FormDetalheDownload.asp?site=C&prot=926368" Documento="IPE" ccvm="54003" DataRef="16/12/2021 23:59:59" FrmDtRef="dd/mm/aaaa" Categoria="Comunicado ao Mercado" Tipo="Outros Comunicados Não Considerados Fatos Relevantes" Especie=" " Situacao="Liberado"/>
            <Link url="http://siteempresas.bovespa.com.br/DWL/FormDetalheDownload.asp?site=C&prot=926369" Documento="IPE" ccvm="53139" DataRef="16/12/2021 23:59:59" FrmDtRef="dd/mm/aaaa" Categoria="Comunicado ao Mercado" Tipo="Outros Comunicados Não Considerados Fatos Relevantes" Especie=" " Situacao="Liberado"/>
            <Link url="http://siteempresas.bovespa.com.br/DWL/FormDetalheDownload.asp?site=C&prot=926370" Documento="IPE" ccvm="54810" DataRef="16/12/2021 23:59:59" FrmDtRef="dd/mm/aaaa" Categoria="Comunicado ao Mercado" Tipo="Outros Comunicados Não Considerados Fatos Relevantes" Especie=" " Situacao="Liberado"/>
            <Link url="http://siteempresas.bovespa.com.br/DWL/FormDetalheDownload.asp?site=C&prot=926371" Documento="IPE" ccvm="53155" DataRef="16/12/2021 23:59:59" FrmDtRef="dd/mm/aaaa" Categoria="Comunicado ao Mercado" Tipo="Outros Comunicados Não Considerados Fatos Relevantes" Especie=" " Situacao="Liberado"/>
            </DownloadMultiplo>
            """.replacingOccurrences(of: "&prot", with: "&amp;prot")
        )
    }

    func fetch(_ string: String) {
        let dom = MicroDOM(data: Data(string.utf8))
        let tree = dom.parse()

        if let links = tree?.getElementsByTagName("Link") {
            for link in links {
                let dictionary = link.attributes

                do {
                    let encodedDictionary = try JSONEncoder().encode(dictionary)
                    let decoder = JSONDecoder()
                    print(try decoder.decode(Documento.self, from: encodedDictionary))
                } catch {
                    print("Error: ", error)
                }

            }
        }
    }
}

It works well, printing the correct Documento type:

Documento(id: 5E1582ED-C71C-4EEC-A8A9-05C50E23A247, url: "http://siteempresas.bovespa.com.br/DWL/FormDetalheDownload.asp?site=C&prot=926177", documento: "IPE", ccvm: "22144", dataRef: "15/12/2021 23:59:59", frmDtRef: "dd/mm/aaaa", categoria: "Comunicado ao Mercado", tipo: "Outros Comunicados Não Considerados Fatos Relevantes", especie: " ", situacao: "Cancelado") Documento(id: CD008921-C9A7-4822-B132-83ED7EF4ADFA, url: "http://siteempresas.bovespa.com.br/DWL/FormDetalheDownload.asp?site=C&prot=926363", documento: "IPE", ccvm: "50091", dataRef: "16/12/2021 23:59:59", frmDtRef: "dd/mm/aaaa", categoria: "Comunicado ao Mercado", tipo: "Outros Comunicados Não Considerados Fatos Relevantes", especie: " ", situacao: "Liberado") Documento(id: 4C0796DE-4BEC-43D5-90B5-C3D2D77B93C2, url: "http://siteempresas.bovespa.com.br/DWL/FormDetalheDownload.asp?site=C&prot=926364", documento: "IPE", ccvm: "53902", dataRef: "16/12/2021 23:59:59", frmDtRef: "dd/mm/aaaa", categoria: "Comunicado ao Mercado", tipo: "Outros Comunicados Não Considerados Fatos Relevantes", especie: " ", situacao: "Liberado") Documento(id: 042FE16F-ADAF-44F9-B716-348FBC040B09, url: "http://siteempresas.bovespa.com.br/DWL/FormDetalheDownload.asp?site=C&prot=926365", documento: "IPE", ccvm: "56154", dataRef: "16/12/2021 23:59:59", frmDtRef: "dd/mm/aaaa", categoria: "Comunicado ao Mercado", tipo: "Outros Comunicados Não Considerados Fatos Relevantes", especie: " ", situacao: "Liberado") Documento(id: 6082A043-87FF-4180-8D40-CD4DC45ACC9F, url: "http://siteempresas.bovespa.com.br/DWL/FormDetalheDownload.asp?site=C&prot=926366", documento: "IPE", ccvm: "55573", dataRef: "16/12/2021 23:59:59", frmDtRef: "dd/mm/aaaa", categoria: "Comunicado ao Mercado", tipo: "Outros Comunicados Não Considerados Fatos Relevantes", especie: " ", situacao: "Liberado") Documento(id: FC850B0F-236B-418C-82D8-0A565051E805, url: "http://siteempresas.bovespa.com.br/DWL/FormDetalheDownload.asp?site=C&prot=926367", documento: "IPE", ccvm: "55611", dataRef: "16/12/2021 23:59:59", frmDtRef: "dd/mm/aaaa", categoria: "Comunicado ao Mercado", tipo: "Outros Comunicados Não Considerados Fatos Relevantes", especie: " ", situacao: "Liberado") Documento(id: 85872D19-CA2C-45C9-8A80-E19C23CA582A, url: "http://siteempresas.bovespa.com.br/DWL/FormDetalheDownload.asp?site=C&prot=926368", documento: "IPE", ccvm: "54003", dataRef: "16/12/2021 23:59:59", frmDtRef: "dd/mm/aaaa", categoria: "Comunicado ao Mercado", tipo: "Outros Comunicados Não Considerados Fatos Relevantes", especie: " ", situacao: "Liberado") Documento(id: C590FF8A-A81A-4416-A53B-AE333AF37899, url: "http://siteempresas.bovespa.com.br/DWL/FormDetalheDownload.asp?site=C&prot=926369", documento: "IPE", ccvm: "53139", dataRef: "16/12/2021 23:59:59", frmDtRef: "dd/mm/aaaa", categoria: "Comunicado ao Mercado", tipo: "Outros Comunicados Não Considerados Fatos Relevantes", especie: " ", situacao: "Liberado") Documento(id: C8971E3D-6DB8-4C96-B04B-91E4D6E2B5AD, url: "http://siteempresas.bovespa.com.br/DWL/FormDetalheDownload.asp?site=C&prot=926370", documento: "IPE", ccvm: "54810", dataRef: "16/12/2021 23:59:59", frmDtRef: "dd/mm/aaaa", categoria: "Comunicado ao Mercado", tipo: "Outros Comunicados Não Considerados Fatos Relevantes", especie: " ", situacao: "Liberado") Documento(id: EF5C4E5A-C4E9-4A79-B71A-485B889A86E0, url: "http://siteempresas.bovespa.com.br/DWL/FormDetalheDownload.asp?site=C&prot=926371", documento: "IPE", ccvm: "53155", dataRef: "16/12/2021 23:59:59", frmDtRef: "dd/mm/aaaa", categoria: "Comunicado ao Mercado", tipo: "Outros Comunicados Não Considerados Fatos Relevantes", especie: " ", situacao: "Liberado")

The new problem is: when I try to return the result of the fetch method I don't know exactly what is going wrong:

func fetch(_ string: String) -> [Documento] {
        let dom = MicroDOM(data: Data(string.utf8))
        let tree = dom.parse()

        if let links = tree?.getElementsByTagName("Link") {
            for link in links {
                let dictionary = link.attributes

                do {
                    let encodedDictionary = try JSONEncoder().encode(dictionary)
                    let decoder = JSONDecoder()
                    return try decoder.decode([Documento].self, from: encodedDictionary)
                } catch {
                    print("Error: ", error)
                }
            }
        }
    }

I receive this fail message: Missing return in a function expected to return '[Documento]'

Thanks for the help!

1      

You have a return statement in the event that everything goes right, but don't return anything if/when something goes wrong. The compiler requires you to return something (or crash with a fatalError) for every path through the function.

You need to put something like return [] at the end of your function.

1      

@roosterboy,

I have solved most of my problems parsing the XML document. But there is a new one related to SwiftUI.

My document model is:

import Foundation
import XMLCoder

struct Documento: Identifiable, Codable {
    var id = UUID()
    let url: String
    let documento: String
    let ccvm: String
    let dataRef: Date
    let frmDtRef: String
    let categoria: String
    let tipo: String
    let especie: String
    let situacao: String
    let assuntos: Assuntos

    enum CodingKeys: String, CodingKey {
        case url
        case documento = "Documento"
        case ccvm
        case dataRef = "DataRef"
        case frmDtRef = "FrmDtRef"
        case categoria = "Categoria"
        case tipo = "Tipo"
        case especie = "Especie"
        case situacao = "Situacao"
        case assuntos = "Assuntos"
    }

    struct Assuntos: Codable, DynamicNodeEncoding {
        let assunto: [String]?

        enum CodingKeys: String, CodingKey {
            case assunto = "Assunto"
        }

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

The xml document is: (as an example)

<DownloadMultiplo DataSolicitada="22/12/2021 00:00" TipoDocumento="IPE" DataConsulta="26/12/2021 22:44"> <Link url="http://siteempresas.bovespa.com.br/DWL/FormDetalheDownload.asp?site=C&prot=925719" Documento="IPE" ccvm="20516" DataRef="13/12/2021 23:59:59" FrmDtRef="dd/mm/aaaa" Categoria="Documentos de Oferta de Distribuição Pública" Tipo="Prospecto de Distribuição Pública" Especie="Prospecto Preliminar" Situacao="Cancelado"> <Assuntos Quantidade="1"> <Assunto> Prospecto preliminar da oferta pública de debêntures incentivadas </Assunto> </Assuntos> </Link> <Link url="http://siteempresas.bovespa.com.br/DWL/FormDetalheDownload.asp?site=C&prot=925720" Documento="IPE" ccvm="20516" DataRef="13/12/2021 23:59:59" FrmDtRef="dd/mm/aaaa" Categoria="Documentos de Oferta de Distribuição Pública" Tipo="Aviso ao Mercado" Especie=" " Situacao="Cancelado"> <Assuntos Quantidade="1"> <Assunto> Prospecto preliminar da oferta pública de debêntures incentivadas </Assunto> </Assuntos> </Link> <Link url="http://siteempresas.bovespa.com.br/DWL/FormDetalheDownload.asp?site=C&prot=926544" Documento="IPE" ccvm="26492" DataRef="31/12/2021 23:59:59" FrmDtRef="mm/aaaa" Categoria="Valores Mobiliários negociados e detidos (art. 11 da Instr. CVM nº 358)" Tipo="Posição Consolidada" Especie=" " Situacao="Cancelado"> <Assuntos Quantidade="0"/> </Link> <Link url="http://siteempresas.bovespa.com.br/DWL/FormDetalheDownload.asp?site=C&prot=926834" Documento="IPE" ccvm="26492" DataRef="31/12/2021 23:59:59" FrmDtRef="mm/aaaa" Categoria="Valores Mobiliários negociados e detidos (art. 11 da Instr. CVM nº 358)" Tipo="Posição Consolidada" Especie=" " Situacao="Cancelado"> <Assuntos Quantidade="0"/> </Link> <Link url="http://siteempresas.bovespa.com.br/DWL/FormDetalheDownload.asp?site=C&prot=927115" Documento="IPE" ccvm="20010" DataRef="04/12/2021 08:00:59" FrmDtRef="dd/mm/aaaa hh:mm" Categoria="Reunião da Administração" Tipo="Conselho de Administração" Especie="Ata" Situacao="Cancelado"> <Assuntos Quantidade="1"> <Assunto>Aprovação da Aquisição da Echoenergia</Assunto> </Assuntos> </Link> <Link url="http://siteempresas.bovespa.com.br/DWL/FormDetalheDownload.asp?site=C&prot=928060" Documento="IPE" ccvm="53503" DataRef="22/12/2021 23:59:59" FrmDtRef="dd/mm/aaaa" Categoria="Comunicado ao Mercado" Tipo="Outros Comunicados Não Considerados Fatos Relevantes" Especie=" " Situacao="Liberado"> <Assuntos Quantidade="1"> <Assunto>4</Assunto> </Assuntos> </Link> <Link url="http://siteempresas.bovespa.com.br/DWL/FormDetalheDownload.asp?site=C&prot=928061" Documento="IPE" ccvm="51403" DataRef="22/12/2021 23:59:59" FrmDtRef="dd/mm/aaaa" Categoria="Comunicado ao Mercado" Tipo="Outros Comunicados Não Considerados Fatos Relevantes" Especie=" " Situacao="Liberado"> <Assuntos Quantidade="1"> <Assunto>4</Assunto> </Assuntos> </Link> <Link url="http://siteempresas.bovespa.com.br/DWL/FormDetalheDownload.asp?site=C&prot=928062" Documento="IPE" ccvm="14311" DataRef="16/12/2021 16:30:59" FrmDtRef="dd/mm/aaaa hh:mm" Categoria="Reunião da Administração" Tipo="Conselho de Administração" Especie="Ata" Situacao="Liberado"> <Assuntos Quantidade="1"> <Assunto> Aprovada a participação da Copel no Leilão de Transmissão ANEEL 002/2021 </Assunto> </Assuntos> </Link> <Link url="http://siteempresas.bovespa.com.br/DWL/FormDetalheDownload.asp?site=C&prot=928063" Documento="IPE" ccvm="25640" DataRef="22/12/2021 23:59:59" FrmDtRef="dd/mm/aaaa" Categoria="Fato Relevante" Tipo=" " Especie=" " Situacao="Liberado"> <Assuntos Quantidade="1"> <Assunto>Acordo de Investimento e Suprimento de Energia</Assunto> </Assuntos> </Link> <Link url="http://siteempresas.bovespa.com.br/DWL/FormDetalheDownload.asp?site=C&prot=928064" Documento="IPE" ccvm="11592" DataRef="22/12/2021 23:59:59" FrmDtRef="dd/mm/aaaa" Categoria="Fato Relevante" Tipo=" " Especie=" " Situacao="Liberado"> <Assuntos Quantidade="1"> <Assunto>Acordo de Investimentos Unipar e AES</Assunto> </Assuntos> </Link> <Link url="http://siteempresas.bovespa.com.br/DWL/FormDetalheDownload.asp?site=C&prot=928065" Documento="IPE" ccvm="24740" DataRef="21/12/2021 09:30:59" FrmDtRef="dd/mm/aaaa hh:mm" Categoria="Reunião da Administração" Tipo="Conselho de Administração" Especie="Sumário das Decisões" Situacao="Liberado"> <Assuntos Quantidade="3"> <Assunto> Aprovada a antecipação de parcela de Juros sobre o Capital Próprio - JCP, em substituição aos dividendos do exercício de 2021, ao acionista com posição em 31.12.2021, de acordo com a Lei Federal nº 9. </Assunto> <Assunto> Aprovada a revisão do Plano de Negócios da Copel Geração e Transmissão S.A. para o período 2022-2026 </Assunto> <Assunto> recebido reporte das atividades do Subcomitê de Sustentabilidade da GeT e determinou que reportes do subcomitê sejam periodicamente trazidos ao Colegiado </Assunto> </Assuntos> </Link> </DownloadMultiplo>

My view model is:

import Foundation

@MainActor
class DocumentoListViewModel: ObservableObject {
    @Published var documentos: [Documento] = []
    @Published var isLoading = false
    @Published var showAlert = false
    @Published var errorMessage: String?

    func fetchDocumentos() async {
        let apiService = APIService(urlString: "http://seguro.bmfbovespa.com.br/rad/download/SolicitaDownload.asp")
        isLoading.toggle()

        defer {
            isLoading.toggle()
        }

        do {
            documentos = try await apiService.getData()
        } catch {
            showAlert = true
            errorMessage = error.localizedDescription + "\nPlease contact the developer."
        }
    }
}

The decoded data is:

[EvolveApp.Documento(id: 874F4915-C3BA-4507-B49E-87E2CD34E266, url: "http://siteempresas.bovespa.com.br/DWL/FormDetalheDownload.asp?site=C&prot=925719", documento: "IPE", ccvm: "20516", dataRef: 2021-12-14 02:59:59 +0000, frmDtRef: "dd/mm/aaaa", categoria: "Documentos de Oferta de Distribuição Pública", tipo: "Prospecto de Distribuição Pública", especie: "Prospecto Preliminar", situacao: "Cancelado", assuntos: EvolveApp.Documento.Assuntos(assunto: Optional(["Prospecto preliminar da oferta pública de debêntures incentivadas"]))), EvolveApp.Documento(id: 8AFB67B7-C922-4018-BE3F-78DC687C7ECF, url: "http://siteempresas.bovespa.com.br/DWL/FormDetalheDownload.asp?site=C&prot=925720", documento: "IPE", ccvm: "20516", dataRef: 2021-12-14 02:59:59 +0000, frmDtRef: "dd/mm/aaaa", categoria: "Documentos de Oferta de Distribuição Pública", tipo: "Aviso ao Mercado", especie: " ", situacao: "Cancelado", assuntos: EvolveApp.Documento.Assuntos(assunto: Optional(["Prospecto preliminar da oferta pública de debêntures incentivadas"]))), EvolveApp.Documento(id: 604D5CCB-D5F4-4CB4-B2A8-AB6B176A4DAA, url: "http://siteempresas.bovespa.com.br/DWL/FormDetalheDownload.asp?site=C&prot=926544", documento: "IPE", ccvm: "26492", dataRef: 2022-01-01 02:59:59 +0000, frmDtRef: "mm/aaaa", categoria: "Valores Mobiliários negociados e detidos (art. 11 da Instr. CVM nº 358)", tipo: "Posição Consolidada", especie: " ", situacao: "Cancelado", assuntos: EvolveApp.Documento.Assuntos(assunto: nil)), EvolveApp.Documento(id: C717723F-749C-4A20-A3C0-A433F45CDA10, url: "http://siteempresas.bovespa.com.br/DWL/FormDetalheDownload.asp?site=C&prot=926834", documento: "IPE", ccvm: "26492", dataRef: 2022-01-01 02:59:59 +0000, frmDtRef: "mm/aaaa", categoria: "Valores Mobiliários negociados e detidos (art. 11 da Instr. CVM nº 358)", tipo: "Posição Consolidada", especie: " ", situacao: "Cancelado", assuntos: EvolveApp.Documento.Assuntos(assunto: nil)), EvolveApp.Documento(id: 12687ADA-474A-442B-B2D9-53FD6504A90C, url: "http://siteempresas.bovespa.com.br/DWL/FormDetalheDownload.asp?site=C&prot=927115", documento: "IPE", ccvm: "20010", dataRef: 2021-12-04 11:00:59 +0000, frmDtRef: "dd/mm/aaaa hh:mm", categoria: "Reunião da Administração", tipo: "Conselho de Administração", especie: "Ata", situacao: "Cancelado", assuntos: EvolveApp.Documento.Assuntos(assunto: Optional(["Aprovação da Aquisição da Echoenergia"]))), EvolveApp.Documento(id: 5C1189E7-B55A-4655-B1FA-FCC6AF8B3A16, url: "http://siteempresas.bovespa.com.br/DWL/FormDetalheDownload.asp?site=C&prot=928060", documento: "IPE", ccvm: "53503", dataRef: 2021-12-23 02:59:59 +0000, frmDtRef: "dd/mm/aaaa", categoria: "Comunicado ao Mercado", tipo: "Outros Comunicados Não Considerados Fatos Relevantes", especie: " ", situacao: "Liberado", assuntos: EvolveApp.Documento.Assuntos(assunto: Optional(["4"]))), EvolveApp.Documento(id: ECA702C1-2E5B-40A4-8836-AEAFE65EE5E5, url: "http://siteempresas.bovespa.com.br/DWL/FormDetalheDownload.asp?site=C&prot=928061", documento: "IPE", ccvm: "51403", dataRef: 2021-12-23 02:59:59 +0000, frmDtRef: "dd/mm/aaaa", categoria: "Comunicado ao Mercado", tipo: "Outros Comunicados Não Considerados Fatos Relevantes", especie: " ", situacao: "Liberado", assuntos: EvolveApp.Documento.Assuntos(assunto: Optional(["4"]))), EvolveApp.Documento(id: C6C362C6-77FF-4263-BC81-928F36E431B8, url: "http://siteempresas.bovespa.com.br/DWL/FormDetalheDownload.asp?site=C&prot=928062", documento: "IPE", ccvm: "14311", dataRef: 2021-12-16 19:30:59 +0000, frmDtRef: "dd/mm/aaaa hh:mm", categoria: "Reunião da Administração", tipo: "Conselho de Administração", especie: "Ata", situacao: "Liberado", assuntos: EvolveApp.Documento.Assuntos(assunto: Optional(["Aprovada a participação da Copel no Leilão de Transmissão ANEEL 002/2021"]))), EvolveApp.Documento(id: 58FBCEF2-E67F-417E-8197-CCF972AD4340, url: "http://siteempresas.bovespa.com.br/DWL/FormDetalheDownload.asp?site=C&prot=928063", documento: "IPE", ccvm: "25640", dataRef: 2021-12-23 02:59:59 +0000, frmDtRef: "dd/mm/aaaa", categoria: "Fato Relevante", tipo: " ", especie: " ", situacao: "Liberado", assuntos: EvolveApp.Documento.Assuntos(assunto: Optional(["Acordo de Investimento e Suprimento de Energia"]))), EvolveApp.Documento(id: 5199FEC7-D55C-40C0-9B13-3BFEB94C7F48, url: "http://siteempresas.bovespa.com.br/DWL/FormDetalheDownload.asp?site=C&prot=928064", documento: "IPE", ccvm: "11592", dataRef: 2021-12-23 02:59:59 +0000, frmDtRef: "dd/mm/aaaa", categoria: "Fato Relevante", tipo: " ", especie: " ", situacao: "Liberado", assuntos: EvolveApp.Documento.Assuntos(assunto: Optional(["Acordo de Investimentos Unipar e AES"]))), EvolveApp.Documento(id: 1905FF9A-9D91-430E-AEC9-3F3FC8035567, url: "http://siteempresas.bovespa.com.br/DWL/FormDetalheDownload.asp?site=C&prot=928065", documento: "IPE", ccvm: "24740", dataRef: 2021-12-21 12:30:59 +0000, frmDtRef: "dd/mm/aaaa hh:mm", categoria: "Reunião da Administração", tipo: "Conselho de Administração", especie: "Sumário das Decisões", situacao: "Liberado", assuntos: EvolveApp.Documento.Assuntos(assunto: Optional(["Aprovada a antecipação de parcela de Juros sobre o Capital Próprio - JCP, em substituição aos dividendos do exercício de 2021, ao acionista com posição em 31.12.2021, de acordo com a Lei Federal nº 9.", "Aprovada a revisão do Plano de Negócios da Copel Geração e Transmissão S.A. para o período 2022-2026", "recebido reporte das atividades do Subcomitê de Sustentabilidade da GeT e determinou que reportes do subcomitê sejam periodicamente trazidos ao Colegiado"])))

and my view is:

import SwiftUI

struct ContentView: View {
    @StateObject var vm = DocumentoListViewModel()

    var dateFormatter: DateFormatter {
        let formatter = DateFormatter()
        formatter.timeStyle = .none
        formatter.dateStyle = .medium
        return formatter
    }

    var body: some View {
        NavigationView {
            List {
                ForEach(vm.documentos) { item in
                    NavigationLink(destination: PDFKitView(url: URL(string: item.url)!),
                                   label: {
                        VStack(alignment: .leading) {
                            Text(item.ccvm)
                                .font(.largeTitle)
                                .bold()
                            Text(item.categoria.uppercased())
                                .font(.subheadline)
                                .lineLimit(1)
                            Text(dateFormatter.string(from: item.dataRef))
                                .font(.caption)
                        }
                        .padding()
                    })
                }
            }
            .overlay(content: {
                if vm.isLoading {
                    ProgressView()
                }
            })
            .alert("Application Error", isPresented: $vm.showAlert, actions: {
                Button("OK") {}
            }, message: {
                if let errorMessage = vm.errorMessage {
                    Text(errorMessage)
                }
            })
            .navigationBarTitle("Documentos CVM")
            .listStyle(.plain)
        }
        .task {
            await vm.fetchDocumentos()
        }
    }
}

The problem is: I can not display the text coming from the Assuntos (which means Subjects) from the documents.

When i try:

Text(item.assuntos), Xcode tells me that this property is defined on Documento, and may not be available in this context.

I'd like to loop over all the assuntos (subjects) and present them below item.categoria.

Do you have any idea where I am making a mistake?

Thanks a lot!

1      

If you hover your mouse over item.assuntos and hold down the option key and click, it will tell you what TYPE item.assuntos is.

It looks like item.assuntos is a struct of type Assuntos. This struct holds an optional array of strings defined as:

    struct Assuntos: Codable, DynamicNodeEncoding {
        let assunto: [String]?   // this is an optional array of strings.

I think you are failing on the line:

Text(item.assuntos)  // assuntos is not a string. It's a struct that holds an array of optional strings.

Maybe try:

Text(item.assuntos.assunto[0]  ?? "no assunto here" ) // nil coelescing. If assunto is nil, provide string alternative.  

But this is just debugging. You'll need to provide extra logic if there are several items in the assunto array.

1      

Text(item.assuntos)

Documento.assuntos is not a String or anythinng else that Text accepts as a parameter. Since assuntos contains an Optional array of Strings, you would need to loop through it to display them all. Something like this:

//since Documento.Assuntos.assunto is [String]?
if let assuntos = item.assuntos.assunto {
    ForEach(assuntos, id: \.self) { a in
        Text(a) //or whatever
    }
}

Also, just a bit of advice regarding your dateFormatter property...

You currently have it in a computed property, which means that every time you call it a new DateFormatter object will be created. That is an expensive object to create so you want to minimize the number of times you do so. And since you always use the same settings on the DateFormatter, you don't nneed it to be dynamic and can set the properties when it's created and then forget about them. So if you instead initialize dateFormatter with a closure:

private let dateFormatter: DateFormatter = {
    let formatter = DateFormatter()
    formatter.timeStyle = .none
    formatter.dateStyle = .medium
    return formatter
}()

then the DateFormatter object will only be created once and then reused over and over. You can also make it a let instead of a var since now it won't be recomputed every time you access it. (And I threw in the private since it shouldn't be accessible from outside the View struct.

1      

It worked, thank you!

1      

@roosterboy, what is the best way in Swift (and iOS development) to join two or more datasets?

I mean, in my XML dataset above , I have this ccvm field, which is basically an ID of a publicly traded company listed in the brazilian stock exchange. I have another dataset that have a primary key that corresponds to the ccvm field, with all the information about the company, like names, sectors, ticker symbols, etc.

So basically I have a one-to-many relationship, with one company reporting many documents.

I'm not sure if a use Core Data to model this relationship, store these datasets on it, or if there is another solution for this task.

Tks!

1      

Hacking with Swift is sponsored by RevenueCat

SPONSORED Spend less time managing in-app purchase infrastructure so you can focus on building your app. RevenueCat gives everything you need to easily implement, manage, and analyze in-app purchases and subscriptions without managing servers or writing backend code.

Get Started

Sponsor Hacking with Swift and reach the world's largest Swift community!

Reply to this topic…

You need to create an account or log in to reply.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.