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