Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Swift: Using Alamofire and SwiftyJSON with a paged JSON API

Using Alamofire and SwiftyJSON to retrieve some JSON is trivial:

Given JSON such as

{
    "results": [
       {
         "id": "123",
         "name": "Bob"
       },
       {
          "id": "456",
          "name": "Sally"
       }
 }

This function will work:

func loadSomeJSONData() {
        Alamofire.request(.GET, "http://example.com/json/")
            .responseJSON { (_, _, data, _) in
                let json = JSON(data!)
                if let firstName = json["results"][0]["name"].string {
                    println("first name: \(firstName)") // firstName will equal "Bob"
                }
        }
    }

All well and good. My problem arises when I need to load JSON from a paged API, that is, when the data is collected from multiple calls to an API endpoint, where the JSON looks more like:

 {
    "currentPage": "1",
    "totalPages": "6"
    "results": [
       {
         "id": "123",
         "name": "Bob"
       },
       {
          "id": "456",
          "name": "Sally"
       }
     ]
 }

and then the next block would look like:

 {
    "currentPage": "2",
    "totalPages": "6"
    "results": [
       {
         "id": "789",
         "name": "Fred"
       },
       {
          "id": "012",
          "name": "Jane"
       }
     ]
 }

In this case, I can recursively call a function to gather all the "pages" but I'm not sure how to put all the JSON fragments together properly:

func loadSomeJSONDataFromPagedEndPoint(page : Int = 1) {
        Alamofire.request(.GET, "http://example.com/json/" + page)
            .responseJSON { (_, _, data, _) in
                let json = JSON(data!)
                if let totalPages = json["totalPages"].description.toInt() {
                    if let currentPage = json["currentPage"].description.toInt() {
                        let pageOfJSON = json["results"]

                        // add pageOfJSON to allJSON somehow??

                        if currentPage < totalPages {
                            self.loadSomeJSONDataFromPagedEndPoint(page: currentPage+1)
                        } else {
                            // done loading all JSON pages
                        }
                 }
 }

var allJSON
loadSomeJSONDataFromPagedEndPoint()

What I'd like to happen is to have the "results" portion of each JSON response eventually collected into a single array of objects (the { "id": "123", "name": "Bob"} objects)

Bonus question: I'm not sure why I need to do json["totalPages"].description.toInt() in order to get the value of totalPages, there must be a better way?

like image 495
gwint Avatar asked Dec 06 '22 22:12

gwint


1 Answers

You have several questions in here, so let's take them one at a time.

I can't tell from your post if you get valid JSON back for each page call or whether you need to put them altogether to complete the JSON. So let's walk through both cases.

Option 1 - Valid JSON from each Page

You're already very close, you just need to tweak your JSON parsing a bit and store the results. Here's what this could look like.

class PagedDownloader {
    var pagedResults = [AnyObject]()

    func loadSomeJSONDataFromPagedEndPoint(page: Int) {
        let request = Alamofire.request(.GET, "http://example.com/json/\(page)")
        request.responseJSON { [weak self] _, _, jsonData, _ in
            if let strongSelf = self {
                let json = JSON(jsonData!)

                let totalPages = json["totalPages"].stringValue.toInt()!
                let currentPage = json["currentPage"].stringValue.toInt()!

                let results = json["results"].arrayObject!
                strongSelf.pagedResults += results

                if currentPage < totalPages {
                    strongSelf.loadSomeJSONDataFromPagedEndPoint(currentPage + 1)
                } else {
                    strongSelf.parsePagedResults()
                }
            }
        }
    }

    func parsePagedResults() {
        let json = JSON(pagedResults)
        println(json)
    }
}

You seem to know your way around SwiftyJSON so I'll let you handle the parsePagedResults implementation.

Option 2 - Pages must be assembled to create valid JSON

Paging JSON

First off, you can't parse partial JSON, it just won't work. The NSJSONSerialization will fail. This means that you can't use the responseJSON serializer with paged JSON because data will always be nil and error will always be the json serialization error. Long story short, you need cache all your data until it's valid JSON, then you can parse.

Storing Paged JSON

If you're going to store it, this is what it could look like as a simple example without Alamofire getting in the mix.

class Pager {

    let page1 = "{\"currentPage\":\"1\",\"totalPages\":\"3\",\"results\":[{\"id\":\"123\",\"name\":\"Bob\"},"
    let page2 = "{\"id\":\"456\",\"name\":\"Sally\"},{\"id\":\"234\",\"name\":\"Christian\"},"
    let page3 = "{\"id\":\"567\",\"name\":\"Jerry\"},{\"id\":\"345\",\"name\":\"John\"}]}"

    let pages: [String]
    let jsonData: NSMutableData

    init() {
        self.pages = [page1, page2, page3]
        self.jsonData = NSMutableData()
    }

    func downloadPages() {
        for (index, page) in enumerate(pages) {
            jsonData.appendData(page.dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: false)!)
        }

        let json = JSON(data: jsonData)
        println(json)

        if let totalPages = json["totalPages"].string?.toInt() {
            println("Total Pages Value: \(totalPages)")
        }
    }
}

Your bonus question is answered at the end of that code chunk. You don't want to use description from SwiftyJSON, but instead the string optional cast and then optional chain into the toInt method.

Paging and Storing with Alamofire

Now that you have a simple example of how to write the JSON pages into data chunks, let's look at how that same approach could be used with the response serializer in Alamofire.

class Downloader {
    var jsonData = NSMutableData()
    var totalPagesDownloaded = 0
    let totalPagesToDownload = 6

    func loadSomeJSONDataFromPagedEndPoint() {
        for page in 1...self.totalPagesToDownload {
            let request = Alamofire.request(.GET, "http://example.com/json/\(page)")
            request.response { [weak self] _, _, data, _ in
                if let strongSelf = self {
                    strongSelf.jsonData.appendData(data as NSData)
                    ++strongSelf.totalPagesDownloaded

                    if strongSelf.totalPagesDownloaded == strongSelf.totalPagesToDownload {
                        strongSelf.parseJSONData()
                    }
                }
            }
        }
    }

    func parseJSONData() {
        let json = JSON(data: jsonData)
        println(json)
    }
}

Parsing the Resulting JSON with SwiftyJSON

Inside the parseJSONData function, just use all the awesome features of SwiftyJSON to parse out the values you need.

I'm pretty sure that covers all your possible use cases and questions. Hope that helps!

like image 190
cnoon Avatar answered Dec 21 '22 23:12

cnoon