Search code examples
swiftgoogle-app-engine

How to return a value from a void closure in Swift?


I have a function that queries a certain user in order to access an array of that user. I returns the user and I can access their array. However, the call is asynchronous and what is being returned is nil. The function over all has a completion handler, however, inside there is a query call and by default that query returns Void.

    func _getAllMatches(completionHandler: ((UIBackgroundFetchResult) -> Void)!) -> Int{
    var toReturn = [GTLUserUser]()
    let query = GTLQueryUser.queryForUserList()
    query.userBucket = "messages-20-messagestabletes-1465782960"
    service.executeQuery(query, completionHandler: {(ticket, response, error) -> Void in
        if error != nil{
            self._showErrorDialog(error)
            return
        }
        else{
            let userCollection = response as! GTLUserCollection
            if let newUsers = userCollection.items() as? [GTLUserUser]{
                toReturn = newUsers
                completionHandler(UIBackgroundFetchResult.NewData)

            }
        }
    })

return toReturn[0].likedArray.count
}

How do I wait for this query to return and assign to "toReturn" so that it will actually return something instead of returning nothing.


Solution

  • Since it's an asynchronous method, you cannot return the value, but instead follow the completion handler pattern, including the data returned as a parameter:

    func performAllMatchesQueryWithCompletionHandler(completionHandler: (UIBackgroundFetchResult, [GTLUserUser]?, ErrorType?) -> ()) {
        let query = GTLQueryUser.queryForUserList()
        query.userBucket = "messages-20-messagestabletes-1465782960"
        service.executeQuery(query) { ticket, response, error in
            guard error == nil else {
                completionHandler(.failed, nil, error)
                return
            }
    
            if let userCollection = response as? GTLUserCollection, let newUsers = userCollection.items() as? [GTLUserUser] {
                completionHandler(.newData, newUsers, nil)
            } else {
                completionHandler(.noData, nil, nil)
            }
        }
    }
    

    I infer from your use of UIBackgroundFetchResult, that you're doing a background fetch. If so, your performFetchWithCompletionHandler might look like so:

    func application(application: UIApplication, performFetchWithCompletionHandler completionHandler: (UIBackgroundFetchResult) -> Void) {
        performAllMatchesQueryWithCompletionHandler { fetchResult, users, error in
            switch fetchResult {
            case .failed:
                // do whatever you want if there was an error
    
            case .noData:
                // do whatever you want when there is no data
    
            case .newData:
                // do whatever you want with `users`, presumably updating your model or what have you
            }
    
            completionHandler(fetchResult)
        }
    }
    

    You could then call this method from viewDidLoad or wherever appropriate for your app.

    Note, I removed the leading underscore on the method names, as that's not a common convention in Swift, but call your methods whatever you want. And I renamed _getAllMatches to performAllMatchesQueryWithCompletionHandler, as it makes it more clear that you're performing an asynchronous query.


    In comments, you say that you're not doing background fetch, but rather are populating a table. So you might do something like:

    func retrieveDataForTableView(tableView: UITableView) {
        performAllMatchesQueryWithCompletionHandler { fetchResult, users, error in
            switch fetchResult {
            case .failed:
                // do whatever you want if there was an error
    
            case .noData:
                // do whatever you want when there is no data
    
            case .newData:
                // do whatever you want with `users`, presumably updating your model or what have you
    
                // once you've updated your model, you can reload the table:
    
                tableView.reloadData()
            }
        }
    }
    

    Note, I've assumed that this completion handler is running on the main thread. If not, you'd want to dispatch_async(dispatch_get_main_queue()) { ... } the code that updates the model and calls reloadData.

    Personally, I wouldn't be inclined to use UIBackgroundFetchResult for my performAllMatchesQueryWithCompletionHandler if I wasn't really doing background fetch. I'd probably use my own enumeration for that, to avoid any confusion regarding the intent of this code.