1

I'm having trouble retrieving data from my Alamofire request asynchronously.

class BookGetter {

    static let instance = BookGetter()

    func getBook(bookId: String) -> Book {

        let rootUrl = "https://www.someusefulbookapi.com/bookid=?"
        let url = rootUrl + bookId
        var title = ""

        Alamofire.request(.GET, url).response { response in

            let jsonDict = JSON(data: response.2!)

            title = String(jsonDict["items"][0]["volumeInfo"]["title"])
        }
        let book = Book(title: title)
        print(book.title)
        return book
    }
}

The output of print(book.title) is "", and I understand this is because the print statement is running before the request returns.

How do I get the book instance to be returned only when it is instantiated with the data from the request?

ray john
  • 261
  • 2
  • 11
  • the print(book.title) should be inside the response block, as well as the line before that – washloops Oct 14 '15 at 15:53
  • You can't do it synchonously. You can only create and return book instance(with empty title) like you do it now, and then after request finising you can set book's title. – Sergei Stralenia Oct 14 '15 at 15:55
  • @SergeiStralenia I am using data hiding in my book model, so I can't change the value after it's been instantiated... – ray john Oct 14 '15 at 15:58
  • @washloops if I do that, the book object is out of scope and `return book` throws an error – ray john Oct 14 '15 at 16:00
  • @rayjohn, can you describe context, where you going to use this code? – Sergei Stralenia Oct 14 '15 at 16:06
  • I'm calling it in a ViewController when a user inputs a `bookId` into a text field @sergeistralenia. e.g. `let book = Librarian.instance.getBook(bookId)` – ray john Oct 14 '15 at 16:08

2 Answers2

10

The problem you have is that you are calling an asynchronous method and expecting to return the result synchronously. When your code is executed, the getBook function completes and returns before even the GET request has complete.

Basically, you have two options:

  1. Update your getBook method to be asynchronous and return the result with a completion block/callback
  2. Wait for the asynchronous call to complete, blocking the current thread (this is OK as long as it is not the main thread you are blocking), and return the result synchronously.

1. Update your method to be asynchronous

To do this, you must return the result on a block/callback function.

class BookGetter {

    static let instance = BookGetter()

    func getBook(bookId: String, complete: (book: Book?, error: NSError?) -> Void) {

        let rootUrl = "https://www.someusefulbookapi.com/bookid=?"
        let url = rootUrl + bookId
        var title = ""

        Alamofire.request(.GET, url).response { request, response, data, error in

            // TODO: You should check for network errors here
            // and notify the calling function and end-user properly.
            if error != nil {
                complete(book: nil, error: error as? NSError)
                return
            }

            let jsonDict = JSON(data: response.2!)

            title = String(jsonDict["items"][0]["volumeInfo"]["title"])
            let book = Book(title: title)
            print(book.title)
            complete(book: book, error: nil)
        }
    }
}

As mentioned in the above code, ideally you should handle errors in the callback response (including exceptions while parsing the JSON). Once handled, you can update the callback parameters to (book: Book?, error: NSError?) -> Void or similar, and check for book or error to be set on the caller function.

To call the function, you need to pass a block to handle the response:

BookGetter.instance.getBook("bookID") { (book, error) in
   if error != nil {
      // Show UIAlertView with error message (localizedDescription)
      return
   }
   // Update User Interface with the book details
}

2. Wait for the asynchronous call to complete

As mentioned above, this is a good idea only if you were running this code on a background thread. It is OK to block background threads, but it is never OK to block the main thread on a graphic application, as it will freeze the user interface. If you do not know what blocking means, please use the option #1.

class BookGetter {

    static let instance = BookGetter()

    func getBook(bookId: String) -> Book {

        let rootUrl = "https://www.someusefulbookapi.com/bookid=?"
        let url = rootUrl + bookId
        var title = ""

        let semaphore = dispatch_semaphore_create(0)    

        Alamofire.request(.GET, url).response { response in

            let jsonDict = JSON(data: response.2!)

            title = String(jsonDict["items"][0]["volumeInfo"]["title"])
            dispatch_semaphore_signal(semaphore)
        }

        //Wait for the request to complete
        while dispatch_semaphore_wait(semaphore, DISPATCH_TIME_NOW) != 0 {
            NSRunLoop.currentRunLoop().runMode(NSDefaultRunLoopMode, beforeDate: NSDate(timeIntervalSinceNow: 10))
        }

        let book = Book(title: title)
        print(book.title)
        return book
    }
}
Eneko Alonso
  • 18,884
  • 9
  • 62
  • 84
  • How would `Once handled, you can update the callback parameters to (book: Book?, error: NSError?) -> Void` be achieved? – ray john Oct 14 '15 at 17:30
  • Update the signature of your method from `func getBook(bookId: String, complete: (book: Book) -> Void) {` to `func getBook(bookId: String, complete: (book: Book?, error: NSError?) -> Void) {`. Then, check the Alamofire response for errors, as indicated in their documentation: https://github.com/Alamofire/Alamofire#response-handler – Eneko Alonso Oct 14 '15 at 17:32
  • i'm not following you. Is this supposed to be added to the code you provided? – ray john Oct 14 '15 at 17:34
  • I've updated the response to include error handling, though you might also want to check for exceptions while parsing the JSON, and other possible errors. – Eneko Alonso Oct 14 '15 at 17:49
  • Thanks, that seems to make sense. How would I call the function now, given that it wants an extra argument ? – ray john Oct 14 '15 at 17:57
  • I've updated the answer to display how you would call that method from your ViewController. – Eneko Alonso Oct 14 '15 at 18:02
  • Thanks so much. Could you point me to some documentation or articles that I can read that further explain this topic? – ray john Oct 14 '15 at 18:11
2

You can use closures and return a completionHandler with your book like in the following way:

func getBook(bookId: String, completionHandler: (book: Book?) -> ()) {

    let rootUrl = "https://www.someusefulbookapi.com/bookid=?"
    let url = rootUrl + bookId

    Alamofire.request(.GET, url).response { response in completionHandler(
       book: 
       {
           // In this block you create the Book object or returns nil in case of error
           if response == nil {
               return nil
           }

           let jsonDict = JSON(data: response.2!)
           let title = String(jsonDict["items"][0]["volumeInfo"]["title"])
           let book = Book(title: title)

           return book
       }())
    }
}

And then you can call it like in the following way:

getBook("idOfYourBook") { book in
     if let book = book {
        println(book.title)
     }
}

I hope this help you.

Victor Sigler
  • 23,243
  • 14
  • 88
  • 105