Swift, generics & protocols

Mikael Hansson forums at deadmengods.com
Sun Jan 15 12:58:10 CET 2017


Apparently this area is bit clunky at the moment, might be mitigated in Swift 4.

This is what I’m trying to do:

I’m going to fetch some tables from an XML data source, parse the XML into different types of objects and then upload it to iCloud through CloudKit.

In order to reduce code duplication I made all objects conform to a protocol and then I created a generic base class parser as most of the parsing code is common for each object. As there are some specifics for each object I create subclasses to the parser to handle those bits.

I’ve tried a million different solutions. Making a protocol for the parser didn’t work as protocols and generics doesn’t mix that well atm, if you specify type in the protocol (by using associatedtype) you can only use the protocol as a type constraint, i.e you can’t, as I need to, both cast to the type and access methods in the protocol.

There’s some relevant code below that works but it’s a bit clunky in the DataManager at the bottom. How would you do this, is there another pattern that would be better?

/Micke

/* AppDataTypes */
protocol AppDataType {
    static var dataType: DataType { get }
    var id: Int { get set }
    func toRecord() -> CKRecord
    init()
}

class Message: AppDataType {
    required init() {}
    
    static let dataType = DataType.message
    
    var id: Int = 0
    var day: String = ""
    var title: String = ""
    var subtitle: String = ""
    var body: String = ""
    var subjectId: String = ""
    var orderInSubject: String = ""
    var imageId: String = ""
    
    
    func toRecord() -> CKRecord {
        let recordID = CKRecordID(recordName: "Message_\(self.id)")
        let record = CKRecord(recordType: "Message", recordID: recordID)
        
        record["messageId"] = self.id as CKRecordValue?
        record["day"] = self.day as CKRecordValue?
        record["title"] = self.title as CKRecordValue?
        record["subtitle"] = self.subtitle as CKRecordValue?
        record["body"] = self.body as CKRecordValue?
        record["subjectId"] = self.subjectId as CKRecordValue?
        record["orderInSubject"] = self.orderInSubject as CKRecordValue?
        record["imageId"] = self.imageId as CKRecordValue?
        
        return record
    }
}

class Subject: AppDataType {
    required init() {}
    
    static let dataType = DataType.subject

    var id: Int = 0
    // subject specific properties
    
    func toRecord() -> CKRecord {
        let recordID = CKRecordID(recordName: "Message_\(self.id)")
        let record = CKRecord(recordType: "Message", recordID: recordID)
        
        //...
        
        return record
    }
}


/* XML Parser */
class Parser<T: AppDataType>: NSObject, XMLParserDelegate {
    // Intended as an abstract class, don't use as concrete

    // MARK: - Properties
    internal var dataTypeArray:[T] = []
    internal var previousAttributeName = ""
    internal var currentElement = ""
    internal var parserCompletionHandler:(([T]) -> Void)?
    
    internal var currentItem = T()
    
    // MARK: - Parsing the document
    func parserDidStartDocument(_ parser: XMLParser) {
        dataTypeArray = []
    }
    
    func parseFeed(_ xmlDocumentUrl: String, withSession session: URLSession, completionHandler: (([T]) -> ())?) {
        self.parserCompletionHandler = completionHandler
        let request = URLRequest(url: URL(string: xmlDocumentUrl)!)
        let task = session.dataTask(with: request, completionHandler: { (data, response, error) -> Void in
            guard let data = data else {
                if let error = error {
                    print(error)
                }
                return
            }
            // Parse XML data
            let parser = XMLParser(data: data)
            parser.delegate = self
            parser.parse()
        })
        task.resume()
    }
    
    func parserDidEndDocument(_ parser: XMLParser) {
        parserCompletionHandler?(dataTypeArray)
    }
    
    
    // MARK: - Error handling
    func parser(_ parser: XMLParser, parseErrorOccurred parseError: Error) {
        print(parseError.localizedDescription)
    }
}


class MessageParser<T: Message>: Parser<T> {

    // MARK: - Message specific properties
    let elementResultset = "resultset"
    let elementRecord = "record"
    let elementField = "field"
    let elementData = "data"
    
    let FieldAttribute = "name"

    let nameAttributeID = "k_ID"
    let nameAttributeDay = "day"
    let nameAttributeTitle = "heading"
    let nameAttributeSubtitle = "subHeading"
    let nameAttributeBody = "message"
    let nameAttributeSubjectID = "subject_ID"
    let nameAttributeOrderInSubject = "orderinSubject"
    let nameAttributeImageID = "imageID"
    
    // MARK: - Message specific parser methods
    func parser(_ parser: XMLParser, didStartElement elementName: String, namespaceURI: String?, qualifiedName qName: String?, attributes attributeDict: [String : String]) {
        didStartElement(elementName, attributes: attributeDict)
    }
    
    func parser(_ parser: XMLParser, foundCharacters string: String) {
        parserFoundCharacters(string: string)
    }
    
    func parser(_ parser: XMLParser, didEndElement elementName: String, namespaceURI: String?, qualifiedName qName: String?) {
        didEndElement(elementName: elementName)
    }
    
    
    private func didStartElement(_ elementName: String, attributes attributeDict: [String:String]) {
        currentElement = elementName
        
        switch elementName {
        case elementRecord: currentItem = T()
        case elementField: previousAttributeName = attributeDict[FieldAttribute]! as String
        default: break
        }
    }
    
    private func parserFoundCharacters(string: String) {
        switch currentElement {
        case elementData where previousAttributeName == nameAttributeID: currentItem.id = Int(string) ?? 0
        case elementData where previousAttributeName == nameAttributeBody: currentItem.body += string
        case elementData where previousAttributeName == nameAttributeDay: currentItem.day += string
        case elementData where previousAttributeName == nameAttributeImageID: currentItem.imageId += string
        case elementData where previousAttributeName == nameAttributeOrderInSubject: currentItem.orderInSubject += string
        case elementData where previousAttributeName == nameAttributeSubjectID: currentItem.subjectId += string
        case elementData where previousAttributeName == nameAttributeSubtitle: currentItem.subtitle += string
        case elementData where previousAttributeName == nameAttributeTitle: currentItem.title += string
        default: break
        }
    }
    
    private func didEndElement(elementName: String) {
        if elementName == elementRecord {
            dataTypeArray += [currentItem]
            print(currentItem.subtitle)
        }
    }
}


/* DataManager */
class DataManager {
    
    func getData<T:AppDataType>() -> [T] {
        
        var dataTypeArray = [T]()

        let session = createURLSession()
        let dataType = T.dataType
        let url = ApplicationUtil.sharedInstance.urlForXmlTable(ofKind: dataType)

        switch T.self {
        case _ where T.self is Message.Type:
            MessageParser<Message>().parseFeed(url, withSession: session) { (items) in
                for item in items {
                    if let message = item as? T {
                        dataTypeArray.append(message)
                    }
                }
            }
        //case _ where T.self is Subject.Type:
        //    SubjectParser<Subject>().parseFeed(url, withSession: session) { (items) in
        //        for item in items {
        //            if let message = item as? T {
        //                dataTypeArray.append(message)
        //            }
        //        }
        //    }
        default: break
        }
        
        
        return dataTypeArray
    }
}
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.music-bar.org/pipermail/music-bar/attachments/20170115/dd0aad9e/attachment-0001.html>


More information about the music-bar mailing list