So after the rather disappointing dead-end using the Pinterest API to download my boards and pinned images I decided to not waste the experience and as the code was relatively simple use this as a springboard to get into using Swift for HTTP requests and see how easy it would be parsing JSON.
I'd had a play around with making some basic GET requests previously and tried out NSURLConnection (
Shephertz has a good intro), then noticed the general advice that Apple has switched from using this in Mavericks to the newer NSURLSession API. There are some good online examples of this as well (see
Stackoverflow). The Playgrounds feature is yet again proving it's worth in prototyping that these new ideas can easily be explored and the various trial workings put into Playgrounds to reference, explore and come back to.
I did a bit of googling the other day around JSON processing in Swift and was surprised at the number of posts dealing with this and describing it in various shades of 'chewing glass'. There were a lot of 'simple-support' classes, a lot of really clever ideas overloading various operators and using the new Swift features for 'roll-your-own' operators. Given one of the ideas of Swift was for readability to aid understanding I was less than happy with these approaches as it looked like adding a whole new set of custom semantics. So, in my books, maybe a few more lines of code, but if this is easier to understand then all the better. Writing code is easy, it's the reading and understanding bit that takes time.
So, I plunged for the boring, long winded route to just see how easy it would be to walk through the JSON response for asking for the pins from a particular board. See my previous post to take a look at this description.
After navigating through the !s and ?s this turned out to not be too much trouble.
First of all let's get the HTTP request in the bag and take it onwards from there:
import Cocoa
// allow the asynchronous task to continue, set timeout in console
import XCPlayground
XCPSetExecutionShouldContinueIndefinitely(continueIndefinitely:true)
// this isn't my pinterest board, you'll need to change [hondrou] and [board]
let url = NSURL(string:"https://api.pinterest.com/v3/pidgets/boards/hondrou/board/pins/")
var session = NSURLSession.sharedSession()
var task:NSURLSessionDataTask = session.dataTaskWithURL(url)
{
(data, response, error) in
if let err = error
{
println("Error: \(err)")
}
else
{
// let's print out what we've got to check the look of the JSON response
println(NSString(data:data,encoding: NSUTF8StringEncoding))
}
// do all the JSON response processing here
}
task.resume()
Nice! This is pretty short and succinct. If you want to see the console output easily, just open up the Assistant Editor.
Things that are worth noting here are that the dataTaskWithURL function has the following prototype:
func dataTaskWithURL(_ url
: NSURL,
completionHandler completionHandler
: ((NSData!,
NSURLResponse!,
NSError!) -> Void)?) -> NSURLSessionDataTask
You'll notice that the second parameter of the function is a completionHandler. As I explored previously (and I'm very thankful to have got this now in my back-pocket) this can either be implemented as a more classic callback function or can be written as a Closure (as it is above). The way it is written above takes advantage of the Swift feature called
Trailing Closures.
This is a bit of Swift syntactic simplification that where the last parameter can be expressed as a closure it's possible to write it without putting the expression into the function parameter brackets. So, in the about the completion handler is the part that is written as a closure like this:
{
(data, response, error) in
// this is the closure code
}
As this dataTaskWithURL calls out to the completionHandler to execute this please do not forget to include the last
task.resume() and remember to keep the playground running indefinitely.
Now for the JSON processing, which looks like this:
var jsonError:NSError?
var json:NSDictionary = NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions.MutableContainers, error: &jsonError) as NSDictionary
if let err = jsonError
{
println("Error parsing json: \(err)")
}
let status:String? = json["status"] as? String
Wow! where did all the questions come from! Just roll with me and we'll tackle this in another post another time. This picks out the status part of the first bit of the JSON response. We can now take a look at parsing through the structure in luddite fashion for the moment.
What I want to do is walk through the structure, collect the description, link url, image url and id for the pin. First of all we can work through the structures like this:
let data:NSDictionary! = json["data"] as? NSDictionary!
let board:NSDictionary! = data["board"] as? NSDictionary!
let url:String? = board["image_thumbnail_url"] as? String
let pins:NSArray! = data["pins"] as? NSArray!
Now, I want to iterate through the pins, but before doing that it's useful as we'll be in an iteration loop that is a bit more difficult to follow in the playground it's useful to see what the structure of the first element of the array (e.g. the first pin) is like so we can explore that in the playground. This line does the trick:
let p = (data["pins"] as? NSArray)![0]
It also shows a little yellow warning triangle to tell you that the type is inferred to be AnyObject. We're ok with that as this is just exploratory.
So, using this I dabbled around a bit to make sure I could get the syntax right and then put the loop in. There is very nice post
here that gave me the neat syntax for describing this cleanly:
for (index,pin) in enumerate(pins)
{
// iteration
}
I really like this after being familiar with the C-sharp 'foreach' concept, this builds on the use of Tuples in Swift nicely (it's like the more clunky KeyValuePair iteration for Dictionary used in C-sharp).
Which I then finished off like this:
for (index,pin) in enumerate(pins)
{
let description:String? = pin["description"] as? String
let link:String? = pin["link"] as? String
let images:NSDictionary! = pin["images"] as? NSDictionary
let id:String? = pin["id"] as? String
let image237x:String = (images["237x"] as? NSDictionary)!["url"] as String
println("\(index): \(description!), \(id!) \(image237x)")
}
Nice. I can now list each of my pins, give a description, get the id and show the url to the image (although only the 237x sized image as I explained previously).
I'm quite pleased with this. It was pretty straightforward and easy to explore in a Playground and allowed me to tinker with some more Swift syntax and get a meaningful result. I expect I'll be building on this quite a bit.
Before I hit the sack.... last but not least..... I wanted to actually download those images.
This took a bit longer than I thought, just as I was fighting through getting the correct path to write to, in the end I just let it put the files in the easiest place. I think this might be a Playground security thing that I need to work through another way or just my lack of understanding Mac file-system issues at the moment. Anyway, here it is:
This goes into the loop behind the other functions
let imgURL: NSURL = NSURL(string: image237x)
// Download an NSData representation of the image at the URL
let imgData: NSData = NSData(contentsOfURL: imgURL)
let documentsPath = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)[0] as String
var error:NSError?
imgData.writeToFile("\(documentsPath)/\(id!).jpg", options: .AtomicWrite , error: &error)
I'm downloading each of the images into a file that is written as a jpg image using the pin-id as the file name. Now, just would have been good to get them at a useful resolution :-(
Update!
I had a
note from Reto Kuepfer on my previous post doing this with C-sharp and he found the answer to getting hold of the original resolution images.
Using the code above, what needs to be done is to swap out the 237x image url with 'originals' instead like this:
let image237x:String = (images["237x"] as? NSDictionary)!["url"] as String
let imageO = image237x.stringByReplacingOccurrencesOfString("237x", withString: "originals", options: NSStringCompareOptions.LiteralSearch, range: nil)
Then use imageO in the creation of NSURL.
In revisiting this code, in the intermediate time I have updated to the latest version of xCode, which brings a couple of little changes as they harmonise the API. The following adjustments need to be made:
let imgURL: NSURL! = NSURL(string: image237x)
and
let imgData: NSData! = NSData(contentsOfURL: imgURL)
and in the earlier code, the following change:
let p: AnyObject = (data["pins"] as? NSArray)![0]