The book with the Challenge: Parse the XML Courses Feed. I get trouble here, who can help me?
Here’s what I came up with. It returns all the courses, not just the next offering of each title.
func stringFromXMLNode(context: NSXMLNode, xpath: String) throws -> String? {
let nodes = try context.nodesForXPath(xpath)
if let node = nodes.first,
let string = node.stringValue {
return string
}
return nil
}
func coursesFromXMLData(data: NSData) throws -> [Course] {
let dateFormatter = NSDateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd hh:mm:ss z"
var courses: [Course] = []
let doc = try NSXMLDocument(data: data, options:0)
let courseNodes = try doc.nodesForXPath("//class")
for courseNode in courseNodes {
if let courseTitle = try stringFromXMLNode(courseNode, xpath: "offering"),
let urlString = try stringFromXMLNode(courseNode, xpath: "offering/@href"),
let courseURL = NSURL(string: urlString),
let dateString = try stringFromXMLNode(courseNode, xpath: "begin"),
let courseStartDate = dateFormatter.dateFromString(dateString) {
courses.append(Course(title: courseTitle, url: courseURL, nextStartDate: courseStartDate))
}
}
return courses
}
Mine is very similar to Jim’s, but I had to add a do-catch statement for all the try expressions. It handles all errors that I can see at this point, except one. This excerpt from resultFromXMLData(data:):
let courseNodes = try doc.nodes(forXPath: "//class")
I can change “//class” to “//clss” to try to make it throw an error and it never parses the data and doesn’t throw an error. In the documentation for nodes(forXPath:) it says “You call this method in a try expression and handle any errors in the catch clauses of a do statement…” which I think I’m doing.
The main do statement in resultFromXMLData(data:) doesn’t seem to do anything except cancel the compile warning messages from Xcode about errors not being dealt with. I had to add the guard statements to catch the individual errors from parsing the nodes. Otherwise, I could change one of the strings (like “offering”) to gibberish, and nothing would happen, just like with the “//class” error above. It feels like I’m doing something wrong here, I just haven’t figured out what it is yet.
Spoiler alert: The following code has my solutions to all the challenges. If you want to figure them out on your own, don’t look ahead.
MainWindowController.swift:
import Cocoa
class MainWindowController: NSWindowController {
@IBOutlet var tableView: NSTableView!
@IBOutlet var arrayController: NSArrayController!
@IBOutlet var progressSpinner: NSProgressIndicator!
let fetcher = ScheduleFetcher()
@objc dynamic var courses: [Course] = []
override var windowNibName: NSNib.Name? {
return NSNib.Name(rawValue: "MainWindowController")
}
override func windowDidLoad() {
super.windowDidLoad()
tableView.target = self
tableView.doubleAction = #selector(openClass(sender:))
progressSpinner.isHidden = false
progressSpinner.startAnimation(self)
fetcher.fetchCoursesUsing { (result) in
switch result {
case .Success(let courses):
print("Got courses: \(courses)")
self.courses = courses
self.progressSpinner.isHidden = true
self.progressSpinner.stopAnimation(self)
case .Failure(let error):
self.progressSpinner.isHidden = true
self.progressSpinner.stopAnimation(self)
print("Got error: \(error)")
NSAlert(error: error).runModal()
self.courses = []
}
}
}
@objc func openClass(sender: AnyObject) {
if let course = arrayController.selectedObjects.first as? Course {
NSWorkspace.shared.open(course.url as URL)
}
}
}
ScheduleFetcher.swift:
import Foundation
class ScheduleFetcher {
enum FetchCoursesResult {
case Success([Course])
case Failure(Error)
}
var isXML: Bool = false
let session: URLSession
let urlStringJSON = "http://bookapi.bignerdranch.com/courses.json"
let urlStringXML = "http:/bookapi.bignerdranch.com/courses.xml"
init() {
let config = URLSessionConfiguration.default
session = URLSession(configuration: config)
}
//------------------------------------------------------------------------
func isXML(url: URL) -> Bool {
if url.pathExtension == "xml" {
isXML = true
}
else if url.pathExtension == "json" {
isXML = false
}
else {
print("Unexpected extension.")
// ... some sort of error handling here
}
return isXML
}
//------------------------------------------------------------------------
func fetchCoursesUsing(completionHandler: @escaping(FetchCoursesResult) -> (Void)) {
let url = URL(string: urlStringXML)!
let request = URLRequest(url: url as URL)
isXML = isXML(url: url)
let task = session.dataTask(with: request, completionHandler: {
(data, response, error) -> Void in
var result: FetchCoursesResult
if data == nil {
result = .Failure(error!)
}
else if let response = response as? HTTPURLResponse {
print("\(String(describing: data?.count)) bytes, HTTP \(response.statusCode).")
if response.statusCode == 200 {
if self.isXML(url: url) {
result = self.resultFromXMLData(data: data!)
} else {
result = self.resultFromJSONData(data: data!)
}
}
else {
let error = self.errorWithCode(code: 1,
localizedDescription: "Bad status code \(response.statusCode).")
result = .Failure(error)
}
}
else {
let error = self.errorWithCode(code: 1,
localizedDescription: "Unexpected response object.")
result = .Failure(error)
}
OperationQueue.main.addOperation({
completionHandler(result)
})
})
task.resume()
}
//------------------------------------------------------------------------
func errorWithCode(code: Int, localizedDescription: String) -> Error {
return NSError(domain: "ScheduleFetcher",
code: code,
userInfo: [NSLocalizedDescriptionKey: localizedDescription])
}
//------------------------------------------------------------------------
func courseFromDictionary(courseDict: NSDictionary) -> Course? {
if let title = courseDict["title"] as? String,
let urlString = courseDict["url"] as? String,
let upcomingArray = courseDict["upcoming"] as? [NSDictionary],
let nextUpcomingDict = upcomingArray.first,
let nextStartDateString = nextUpcomingDict["start_date"] as? String
{
let url = NSURL(string: urlString)!
let dateFormatter = DateFormatter() // book says to use var here, causes warning
dateFormatter.dateFormat = "yyyy-MM-dd"
let nextStartDate = dateFormatter.date(from: nextStartDateString)!
return Course(title: title,
url: url,
nextStartDate: nextStartDate)
}
return nil
}
//-------------------------------------------------------------------------------------------------------
func resultFromJSONData(data: Data) -> FetchCoursesResult {
var topLevelDict = Dictionary<String, Any>()
do {
topLevelDict = (try JSONSerialization.jsonObject(with: data, options: []) as? [String : Any])!
} catch {
return .Failure(error)
}
let courseDicts = topLevelDict["courses"] as! [NSDictionary]
var courses: [Course] = []
for courseDict in courseDicts {
guard let course = courseFromDictionary(courseDict: courseDict) else {
return .Failure(errorWithCode(code: 2,
localizedDescription: "resultFromJSONData: Unexpected data structure"))
}
courses.append(course)
}
return .Success(courses)
}
//-----------------------------------------------------------------------------------------------------
func stringFromXMLNode(context: XMLNode, xpath: String) throws -> String? {
let nodes = try context.nodes(forXPath: xpath)
if let node = nodes.first, let string = node.stringValue {
return string
}
return nil
}
//-----------------------------------------------------------------------------------------------------
func resultFromXMLData(data: Data) -> FetchCoursesResult {
var courses: [Course] = []
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd hh:mm:ss z"
do {
let doc = try XMLDocument(data: data, options: [])
let courseNodes = try doc.nodes(forXPath: "//class")
for courseNode in courseNodes {
guard let courseTitle = try stringFromXMLNode(context: courseNode, xpath: "offering"),
let urlString = try stringFromXMLNode(context: courseNode, xpath: "offering/@href"),
let courseURL = URL(string: urlString),
let dateString = try stringFromXMLNode(context: courseNode, xpath: "begin"),
let courseStartDate = dateFormatter.date(from: dateString) else {
return .Failure(errorWithCode(code: 2,
localizedDescription: "resultFromXMLData: Unexpected data structure"))
}
courses.append(Course(title: courseTitle,
url: courseURL as NSURL,
nextStartDate: courseStartDate))
}
} catch {
return .Failure(error)
}
return .Success(courses)
}
//--------------------------------------------------------------------------------------------------------
}
Course.swift:
import Foundation
class Course: NSObject {
@objc dynamic let title:String
@objc dynamic let url: NSURL
@objc dynamic let nextStartDate: Date
init(title: String, url: NSURL, nextStartDate: Date) {
self.title = title
self.url = url
self.nextStartDate = nextStartDate
super.init()
}
}
Any tips on how to make this better, and how to fix/handle the aforementioned error issue would be appreciated. Thanks!
let courseNodes = try doc.nodes(forXPath: “//class”) seems to always want to return an empty array if you mess with the forXPath: parameter. So, I just included this, after that statement: if courseNodes.isEmpty { return .Failure(error) } Perhaps not the best, but a fix for now. Thanks for sharing your code. It was very helpful. Cheers.