Sunday, January 15, 2017

Using RestKit to make a JSON post with Swift



While working on an iPad app using Swift 2, I came across the need to POST some JSON data in the body of a request to our server. Here's a simple example of how to accomplish that.

Note: This post assumes you already use RestKit in your project. Adding it to your build is covered by other articles on the web.

Before we jump into the code to do this, here's the JSON that we want to send:
{
  "dog_breed": "whippet",
  "pounds": 35,
  "height": 20,
  "age": 1
}

First, we'll make a class to hold the data we're POSTing:
import Foundation

class DogRequest: NSObject {
  var dog_breed: String? = nil
  var pounds: Int = 0
  var height: Int = 0
  var age: Int = 0
}

Now we make a mapping for the request data:
let dogRequestMapping: RKObjectMapping = RKObjectMapping(forClass:NSMutableDictionary.self)
dogRequestMapping.addAttributeMappingsFromArray(["dog_breed", "pounds", "height", "age"])
Note that the mapping will end up as a dictionary, so that's what we use for the forClass argument.

Now we create a descriptor for the request:
let dogDescriptor: RKRequestDescriptor = RKRequestDescriptor(mapping: dogRequestMapping,
                                                             objectClass: DogRequest.self,
                                                             rootKeyPath: nil,
                                                             method: RKRequestMethod.POST)
objectManager.addRequestDescriptor(dogDescriptor)

If your server expects a different base or root for your post data, you can specify that with a string passed to rootKeyPath.
For example, if we had rootKeyPath: "woof", the resulting JSON would look something like this:
{
  "woof": {
    "dog_breed": "whippet",
    "pounds": 35,
    "height": 20,
    "age": 1
  }
}

OK, so now we have the request side taken care of.  We take similar steps to get the result of our POST setup.  First, here's the result class:

import Foundation

class DogResponse: NSObject {
  var success: Bool = false
  var error: String? = nil
  var dog_breed: String? = nil
  var count: Int = 0
}


And here's a mapping for the response data:
let dogResponseMapping: RKObjectMapping = RKObjectMapping(forClass:DogResponse.self)
dogResponseMapping.addAttributeMappingsFromArray(["success", "dog_breed", "count", "error"])

And then the descriptor for the response:
let successCodes = RKStatusCodeIndexSetForClass(UInt(RKStatusCodeClassSuccessful))
let dogRespDesc: RKResponseDescriptor = RKResponseDescriptor(mapping: dogResponseMapping,
                                                             method: RKRequestMethod.POST, 
                                                             pathPattern: "dog/breed", 
                                                             keyPath: nil
                                                             statusCodes: successCodes)
objectManager.addResponseDescriptor(dogRespDesc)

Now, we'll use all this to actually send the request.

Let's create the data we want to POST:
let dog : DogRequest = DogRequest()
dog.dog_breed = "whippet"
dog.pounds = 35
dog.height = 20
dog.age = 1

Now, since we want to send the data as JSON (application/json) instead of the default (application/x-www-form-urlencoded), it's very important to tell RK what MIME type we want the data serialized into:
RKObjectManager.sharedManager().requestSerializationMIMEType = RKMIMETypeJSON

And finally, we make the actual POST and handle the response we get:
RKObjectManager.sharedManager().postObject(dog, path: "dog/breed", parameters: nil,
  success: {(operation: RKObjectRequestOperation!, mappingResult: RKMappingResult!) -> Void in
    let dogResponse : DogResponse = mappingResult.array().first as! DogResponse
    if dogResponse.success {
      // Yay!  We successfully posted the dog data
      print("There are now \(dogResponse.count) \(dogResponse.dog_breed) in the server!")
    } else {
      // Uh oh! We received an error response from the server
      print("Error in response: \(dogResponse.error)")
    }
  }, failure: {(operation: RKObjectRequestOperation!, error: NSError?) -> Void in
    // Oh no.  There was an error before it reached the server.
    print("ERROR': \(error)")
})

Pulling it all together, we get this:

File: DogRequest.swift
import Foundation

class DogRequest: NSObject {
  var dog_breed: String? = nil
  var pounds: Int = 0
  var height: Int = 0
  var age: Int = 0
}


File: DogResponse.swift
import Foundation

class DogResponse: NSObject {
  var success: Bool = false
  var error: String? = nil
  var dog_breed: String? = nil
  var count: Int = 0
}



File: ServerApi.swift
let baseURL: NSURL = NSURL(string: "https://dogs.example.com/public/v1/")!
let client: AFHTTPClient = AFHTTPClient(baseURL: baseURL)
let objectManager: RKObjectManager = RKObjectManager(HTTPClient: client)

//...

let dogRequestMapping: RKObjectMapping = RKObjectMapping(forClass:NSMutableDictionary.self)
dogRequestMapping.addAttributeMappingsFromArray(["dog_breed", "pounds", "height", "age"])

let dogDescriptor: RKRequestDescriptor = RKRequestDescriptor(mapping: dogRequestMapping,
                                                             objectClass: DogRequest.self,
                                                             rootKeyPath: nil,
                                                             method: RKRequestMethod.POST)
objectManager.addRequestDescriptor(dogDescriptor)

let dogResponseMapping: RKObjectMapping = RKObjectMapping(forClass:DogResponse.self)
dogResponseMapping.addAttributeMappingsFromArray(["success", "dog_breed", "count", "error"])

let successCodes = RKStatusCodeIndexSetForClass(UInt(RKStatusCodeClassSuccessful))
let dogRespDesc: RKResponseDescriptor = RKResponseDescriptor(mapping: dogResponseMapping,
                                                             method: RKRequestMethod.POST, 
                                                             pathPattern: "dog/breed", 
                                                             keyPath: nil
                                                             statusCodes: successCodes)
objectManager.addResponseDescriptor(dogRespDesc)

// ...

let dog : DogRequest = DogRequest()
dog.dog_breed = "whippet"
dog.pounds = 35
dog.height = 20
dog.age = 1

RKObjectManager.sharedManager().postObject(dog, path: "dog/breed", parameters: nil,
  success: {(operation: RKObjectRequestOperation!, mappingResult: RKMappingResult!) -> Void in
    let dogResponse : DogResponse = mappingResult.array().first as! DogResponse
    if dogResponse.success {
      // Yay!  We successfully posted the dog data
      print("There are now \(dogResponse.count) \(dogResponse.dog_breed) in the server!")
    } else {
      // Uh oh! We received an error response from the server
      print("Error in response: \(dogResponse.error)")
    }
  }, failure: {(operation: RKObjectRequestOperation!, error: NSError?) -> Void in
    // Oh no.  There was an error before it reached the server.
    print("ERROR': \(error)")
})

Saturday, December 31, 2016

Scroll to the top of a UICollectionView when the content changes

I was recently working on an iPad app (using Swift 2) which has a UICollectionView of news articles.  These articles are categorized into channels, which allows the user to choose specific types of news to read.  In this situation we really wanted to scroll to the top of the articles when the user chooses a new channel, otherwise they might miss the articles above where they had scrolled on the previous channel without even realizing it.


So to solve this, I first tried adding code to scroll to the top right after I retrieve the content and call UICollectionView.reloadData().  Unfortunately this didn't turn out as I had hoped.
Since reloadData runs asynchronously, the jump to the top will be attempted before the new content is loaded into the collection. This causes a crash the first time it's called, since there will be no items in the view yet and therefore you can't scroll to the top item.
Ok, so we should probably be doing the scroll in UICollectionViewDelegate's collectionViewDidFinishLoading(UICollectionView) function, right?  Well, unfortunately, that doesn't actually exist.



So, we need to detect when the content has finished loading, and only then scroll to the top.

Here's how I made it work:


1. I created an ("old school" Objective-C style) observer in my controller's viewDidLoad to see that the content had loaded.

...

var observerAdded : Bool = false
private var newsContext = 0

override func viewDidLoad() {
  super.viewDidLoad()

  ...

  if(!self.observerAdded) {
    let opt = NSKeyValueObservingOptions([.New, .Old])
    self.collectionView.addObserver(self,
                                    forKeyPath: "contentSize",
                                    options: opt,
                                    context: &self.newsContext)
    self.observerAdded = true
  }
}

Notes:
  • The reason for observerAdded is that I don't want to add it more than once and I need to know that it should be removed.
  • The reason for myContext is that I need to know this observer callback is for me and not something else iOS or other code is doing.

2. Then I made sure to unregister the observer during cleanup.

deinit {
    if(self.observerAdded) {
        collectionView.removeObserver(self,
                                      forKeyPath: "contentSize",
                                      context: &self.newsContext)
        self.observerAdded = false
    }
}


3. Finally I added the observer callback and scroll to the top item.

override func observeValueForKeyPath(keyPath: String?,
                                     ofObject object: AnyObject?,
                                     change: [String : AnyObject]?,
                                     context: UnsafeMutablePointer<void>) {
    if(context == &self.newsContext) {
        let old = change![NSKeyValueChangeOldKey]?.CGSizeValue()
        let new = change![NSKeyValueChangeNewKey]?.CGSizeValue()
        if(old != new && new?.height > 100) {
            dispatch_async(dispatch_get_main_queue(), {
                self.collectionView.scrollToItemAtIndexPath(NSIndexPath(forItem: 0, inSection: 0),
                                                            atScrollPosition: .Top,
                                                            animated: true)
            })
        }
    } else {
        super.observeValueForKeyPath(keyPath, ofObject: object, change: change, context: context)
    }
}