Swift, generics & protocols

Mikael Hansson forums at deadmengods.com
Tue Jan 17 22:11:29 CET 2017


Yeah, Swift is really fun to work with and I enjoy doing apps more then web :-)

I was aiming for having a parser protocol that the base parser and the specific parsers implemented so the getData method could be ridden of the switch statement.

The problem is that, since I need the protocol to contain generic methods, I need to declare an associated type, like:

protocol ParserProtocol {
    associatedtype GenericType
    func parseFeed(_ xmlDocumentUrl: String, withSession session: URLSession, completionHandler: (([GenericType]) -> ())?)
}


Then I thought, ok I make the Message class serve it’s own parser, giving it a var for the ParserProtocol. All fine until I try to call myParser.parseFeed(…)
It turns out that when you make it generic you can only use it as a type constraint, you can’t use it as a type where you can access the protocol methods.

Another thing that’s a bit odd compared to the C# generics I’m used to is that you don’t specify the type, like getData<Message>(…), it’s only generic through inferring the type and it’s a bit picky when inferring :-)

Below the getData method is decided to be a getData<Message> because self.applicationUtil.messages is a Array<Message>. If I add another line in the closure it screws up the inferring, even though its just “let t = 0"

dataManager.getData() { items in
    self.applicationUtil.messages = items.sorted { $0.id < $1.id }
}


I solved it another way though where I was able to make a generic parser and not use typed versions of the generic base parser. Did it by putting some more methods in the AppDataType protocol that handles the type specific parts of the parsing:

protocol AppDataType {
    init()
    
    var id: Int { get set }
    var dataType: DataType { get }
    func toRecord() -> CKRecord
    var name: String { get }
    
    static var dataType: DataType { get }
    static func foundCharacters(string: String, elementData: String, currentElement: String, previousAttributeName: String, currentItem: Self)
}

// Default implementation to get each type’s name
extension AppDataType {
    var name: String {
        return self.dataType.name
    }
}

class Message: AppDataType {
    required init() {}
    
    static let dataType = DataType.message
    let dataType = DataType.message
    
    var id: Int = 0
    ...
    
    
    func toRecord() -> CKRecord {
        let recordID = CKRecordID(recordName: "\(name)_\(self.id)")
        let record = CKRecord(recordType: name, recordID: recordID)
        
        record["messageId"] = self.id as CKRecordValue?
        ...
        
        return record
    }
    
    class func foundCharacters(string: String, elementData: String, currentElement: String, previousAttributeName: String, currentItem: Message) {
        switch currentElement {
        case elementData where previousAttributeName == "k_ID": currentItem.id = Int(string) ?? 0
        case elementData where previousAttributeName == "message": currentItem.body += string
        case elementData where previousAttributeName == "day": currentItem.day += string
        case elementData where previousAttributeName == "imageID": currentItem.imageId += string
        case elementData where previousAttributeName == "orderinSubject": currentItem.index += string
        case elementData where previousAttributeName == "subject_ID": currentItem.subjectId += string
        case elementData where previousAttributeName == "subHeading": currentItem.subtitle += string
        case elementData where previousAttributeName == "heading": currentItem.title += string
        default: break
        }
    }
}


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

    // MARK: - Properties
    internal var dataTypeArray:[T] = []
    internal var previousAttributeName = ""
    internal var currentElement = ""
    
    internal var parserCompletionHandler:(([T]) -> Void)?
    internal var parserFoundCharacters: ((String, String, String, String, T) -> ())?
    
    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 AppParser<T: AppDataType>: Parser<T> {
    
    override init() {
        super.init()
        parserFoundCharacters = T.foundCharacters
    }
    
    let elementResultset = "resultset"
    let elementRecord = "record"
    let elementField = "field"
    let elementData = "data"
    
    let FieldAttribute = "name"
    
    func parser(_ parser: XMLParser, didStartElement elementName: String, namespaceURI: String?, qualifiedName qName: String?, attributes attributeDict: [String : String]) {
        currentElement = elementName
        
        switch elementName {
        case elementRecord: currentItem = T()
        case elementField: previousAttributeName = attributeDict[FieldAttribute]! as String
        default: break
        }
    }
    
    func parser(_ parser: XMLParser, foundCharacters string: String) {
        parserFoundCharacters?(string, elementData, currentElement, previousAttributeName, currentItem)
    }
    
    func parser(_ parser: XMLParser, didEndElement elementName: String, namespaceURI: String?, qualifiedName qName: String?) {
        if elementName == elementRecord {
            dataTypeArray += [currentItem]
        }
    }
}


/Micke


> On 17 Jan 2017, at 14:00, Jay Vaughan (ibisum) <ibisum at gmail.com> wrote:
> 
> 
>> How would you do this, is there another pattern that would be better?
>> /Micke
> 
> 
> I would do it just as you have done it - with protocols and sub-classes.  What is it about this solution that you don't like?
> 
> BTW - Interesting topic!  I haven't quite gotten up to speed on Swift yet (waiting for the 3.x dust to settle), so for me it was enlightening to see your implementation ..
> 
> ;
> --
> Jay Vaughan
> ibisum at gmail.com
> 
> 
> 
> 
> _______________________________________________
> music-bar mailing list
> music-bar at lists.music-bar.org
> http://lists.music-bar.org/cgi-bin/mailman/listinfo/music-bar

-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.music-bar.org/pipermail/music-bar/attachments/20170117/0402bf62/attachment-0001.html>


More information about the music-bar mailing list