Swift, generics & protocols

Mikael Hansson forums at deadmengods.com
Tue Jan 17 22:19:26 CET 2017


Oh, I forgot the getData implementation which was way more nicer :-)

func getData<T:AppDataType>(completion: @escaping ([T]) -> ()) {
    let session = createURLSession()
    let dataType = T.dataType
    let url = ApplicationUtil.sharedInstance.urlForXmlTable(ofKind: dataType)
        
    AppParser<T>().parseFeed(url, withSession: session) { items in
        completion(items)
    }
}



> On 17 Jan 2017, at 22:11, Mikael Hansson <forums at deadmengods.com> wrote:
> 
> 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 <mailto: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 <mailto: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/1d11561b/attachment-0001.html>


More information about the music-bar mailing list