iosswiftxmlxml-parsing

Parsing XML self closing tags in Swift


I have an XML file that has the following data:

<Book>
    <title>Some Random Book</title>
        <author>Someone Someone</author>
        <book_type>
            <FICT />
            <REF />
        </book_type>
</Book>

I'm trying to write a struct for it, but I'm not sure how to deal with the self-closing tags inside "book_type". Are those enums? Not all books will have a "book_type", and if they do, there could be more than one like in the example above.

Here's what I have, not sure if this is correct:

struct Book {
    var title: String?
    var author: String?
    var book_type: BookType?
}

enum BookType {
    case fiction
    case nonfiction     
    case reference
    case autobiography  
}

Thank you!


Solution

  • The problem you are having has nothing to do with parsing the XML. This is valid XML; parsing it is easy enough. The problem is that you need to come up with some way of representing the XML as an object. The way you do that is totally up to you; there are no magic rules. The thing to keep in mind is that not every XML topology is readily converted to a Swift object — and this is an example of such a topology.

    If you know for a fact that the four book types you have listed in your BookType enum are the only ones that will ever be encountered, then it seems to me that Book's book_type would most conveniently be a Set of BookType, since the order doesn't matter and a given type either is or is not in the list (and if there is no book_type then the set can simply be empty):

    struct Book {
        let title: String
        let author: String
        let book_type: Set<BookType>
    }
    
    enum BookType {
        case fiction
        case nonfiction     
        case reference
        case autobiography  
    }
    

    You'll notice that I didn't make anything Optional, since I assume that every book has a title and an author. But that is only an assumption; it is up to you to design these objects based on what you know to be the ways the XML can legally be structured.


    In case it is not clear to you how to get from the original XML to the posited Book object, here is a complete (but toy) example, where we create your example XML and parse it in the viewDidLoad of a view controller:

    import UIKit
    
    class ViewController: UIViewController {
    
        let parserDelegate = BookParserDelegate()
    
        override func viewDidLoad() {
            super.viewDidLoad()
            let xml = """
            <Book>
                <title>Some Random Book</title>
                <author>Someone Someone</author>
                <book_type>
                    <FICT />
                    <REF />
                </book_type>
            </Book>
            """
            let xmlData = xml.data(using: .utf8)!
            let parser = XMLParser(data: xmlData)
            parser.delegate = parserDelegate
            parser.parse()
            if let book = parserDelegate.theBook {
                print(book)
            }
        }
    }
    
    class BookParserDelegate: NSObject, XMLParserDelegate {
        struct Collector {
            var title = ""
            var author = ""
            var book_types = Set<BookType>()
        }
        var collector = Collector()
        var currentPath: WritableKeyPath<Collector, String>?
        var doingBookTypes = false
        var theBook: Book?
    
        func parser(
            _ parser: XMLParser,
            didStartElement elementName: String,
            namespaceURI: String?,
            qualifiedName qName: String?,
            attributes attributeDict: [String : String] = [:]
        ) {
            if elementName == "title" {
                currentPath = \.title
            } else if elementName == "author" {
                currentPath = \.author
            } else if elementName == "book_type" {
                doingBookTypes = true
            } else if doingBookTypes {
                if let type = BookType(rawValue: elementName) {
                    collector.book_types.insert(type)
                }
            }
        }
        func parser(
            _ parser: XMLParser,
            didEndElement elementName: String,
            namespaceURI: String?,
            qualifiedName qName: String?
        ) {
            currentPath = nil
            if elementName == "book_type" {
                doingBookTypes = false
            } else if elementName == "Book" {
                theBook = Book(
                    title: collector.title,
                    author: collector.author,
                    book_type: collector.book_types
                )
            }
        }
        func parser(
            _ parser: XMLParser,
            foundCharacters string: String
        ) {
            if let currentPath {
                collector[keyPath: currentPath] = string
            }
        }
    }
    
    struct Book {
        let title: String
        let author: String
        let book_type: Set<BookType>
    }
    
    enum BookType: String {
        case fiction = "FICT"
        case reference = "REF"
    }
    

    The output is

    Book(
        title: "Some Random Book", 
        author: "Someone Someone", 
        book_type: Set([.reference, .fiction])
    )
    

    which I believe is correct for the given XML. Note that I didn't use all four cases of your BookType because they don't occur in the XML example and you didn't tell me what they would look like if they did occur. Anyway, it's only a toy example; cleaning up the code and tweaking, etc., is left as an exercise for the reader.