How do I run Asynchronous callbacks in Playground

AsynchronousCallbackClosuresSwiftSwift Playground

Asynchronous Problem Overview


Many Cocoa and CocoaTouch methods have completion callbacks implemented as blocks in Objective-C and Closures in Swift. However, when trying these out in Playground, the completion is never called. For example:

// Playground - noun: a place where people can play

import Cocoa
import XCPlayground

let url = NSURL(string: "http://stackoverflow.com")
let request = NSURLRequest(URL: url)

NSURLConnection.sendAsynchronousRequest(request, queue:NSOperationQueue.currentQueue() {
response, maybeData, error in

    // This block never gets called?
    if let data = maybeData {
	    let contents = NSString(data:data, encoding:NSUTF8StringEncoding)
	    println(contents)
    } else {
	    println(error.localizedDescription)
    }
}

I can see the console output in my Playground timeline, but the println in my completion block are never called...

Asynchronous Solutions


Solution 1 - Asynchronous

While you can run a run loop manually (or, for asynchronous code that doesn't require a run loop, use other waiting methods like dispatch semaphores), the "built-in" way we provide in playgrounds to wait for asynchronous work is to import the XCPlayground framework and set XCPlaygroundPage.currentPage.needsIndefiniteExecution = true. If this property has been set, when your top level playground source finishes, instead of stopping the playground there we will continue to spin the main run loop, so asynchronous code has a chance to run. We will eventually terminate the playground after a timeout which defaults to 30 seconds, but which can be configured if you open the assistant editor and show the timeline assistant; the timeout is in the lower-right.

For example, in Swift 3 (using URLSession instead of NSURLConnection):

import UIKit
import PlaygroundSupport

let url = URL(string: "http://stackoverflow.com")!

URLSession.shared.dataTask(with: url) { data, response, error in
    guard let data = data, error == nil else {
        print(error ?? "Unknown error")
        return
    }

    let contents = String(data: data, encoding: .utf8)
    print(contents!)
}.resume()

PlaygroundPage.current.needsIndefiniteExecution = true

Or in Swift 2:

import UIKit
import XCPlayground

let url = NSURL(string: "http://stackoverflow.com")
let request = NSURLRequest(URL: url!)

NSURLConnection.sendAsynchronousRequest(request, queue: NSOperationQueue.currentQueue()) { response, maybeData, error in
    if let data = maybeData {
        let contents = NSString(data:data, encoding:NSUTF8StringEncoding)
        println(contents)
    } else {
        println(error.localizedDescription)
    }
}

XCPlaygroundPage.currentPage.needsIndefiniteExecution = true

Solution 2 - Asynchronous

This API changed again in Xcode 8 and it was moved to the PlaygroundSupport:

import PlaygroundSupport

PlaygroundPage.current.needsIndefiniteExecution = true

This change was mentioned in Session 213 at WWDC 2016.

Solution 3 - Asynchronous

As of XCode 7.1, XCPSetExecutionShouldContinueIndefinitely() is deprecated. The correct way to do this now is to first request indefinite execution as a property of the current page:

import XCPlayground

XCPlaygroundPage.currentPage.needsIndefiniteExecution = true

…then indicate when execution has finished with:

XCPlaygroundPage.currentPage.finishExecution()

For example:

import Foundation
import XCPlayground

XCPlaygroundPage.currentPage.needsIndefiniteExecution = true

NSURLSession.sharedSession().dataTaskWithURL(NSURL(string: "http://stackoverflow.com")!) {
    result in
    print("Got result: \(result)")
    XCPlaygroundPage.currentPage.finishExecution()
}.resume()

Solution 4 - Asynchronous

The reason the callbacks are not called is because the RunLoop isn't running in Playground (or in REPL mode for that matter).

A somewhat janky, but effective, way to make the callbacks operate is with a flag and then manually iterating on the runloop:

// Playground - noun: a place where people can play

import Cocoa
import XCPlayground

let url = NSURL(string: "http://stackoverflow.com")
let request = NSURLRequest(URL: url)

var waiting = true

NSURLConnection.sendAsynchronousRequest(request, queue:NSOperationQueue.currentQueue() {
response, maybeData, error in
    waiting = false
    if let data = maybeData {
	    let contents = NSString(data:data, encoding:NSUTF8StringEncoding)
	    println(contents)
    } else {
	    println(error.localizedDescription)
    }
}

while(waiting) {
    NSRunLoop.currentRunLoop().runMode(NSDefaultRunLoopMode, beforeDate: NSDate())
    usleep(10)
}

This pattern has often been used in Unit Tests which need to test async callbacks, for example: https://stackoverflow.com/questions/7817605/pattern-for-unit-testing-async-queue-that-calls-main-queue-on-completion

Solution 5 - Asynchronous

The new APIs as for XCode8, Swift3 and iOS 10 are,

// import the module
import PlaygroundSupport
// write this at the beginning
PlaygroundPage.current.needsIndefiniteExecution = true
// To finish execution
PlaygroundPage.current.finishExecution()

Solution 6 - Asynchronous

Swift 4, Xcode 9.0

import Foundation
import PlaygroundSupport

PlaygroundPage.current.needsIndefiniteExecution = true

let url = URL(string: "https://jsonplaceholder.typicode.com/posts/1")!

let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
    guard error == nil else {
        print(error?.localizedDescription ?? "")
        return
    }
    
    if let data = data, let contents = String(data: data, encoding: String.Encoding.utf8) {
        print(contents)
    }
}
task.resume()

Solution 7 - Asynchronous

Swift 3, xcode 8, iOS 10

Notes:

Tell the compiler that the playground file requires "indefinite execution"

Manually terminate execution via a call to PlaygroundSupport.current.completeExecution() within your completion handler.

You may run into problems with the cache directory and to resolve this you will need to manually re-instantiate the UICache.shared singleton.

Example:

import UIKit
import Foundation
import PlaygroundSupport

// resolve path errors
URLCache.shared = URLCache(memoryCapacity: 0, diskCapacity: 0, diskPath: nil)

// identify that the current page requires "indefinite execution"
PlaygroundPage.current.needsIndefiniteExecution = true

// encapsulate execution completion
func completeExecution() {
    PlaygroundPage.current.finishExecution()
}

let url = URL(string: "http://i.imgur.com/aWkpX3W.png")

let task = URLSession.shared.dataTask(with: url!) { (data, response, error) in
    var image = UIImage(data: data!)

    // complete execution
    completeExecution()
}

task.resume()

Solution 8 - Asynchronous

NSURLConnection.sendAsynchronousRequest(...)    
NSRunLoop.currentRunLoop().run()

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionikuramediaView Question on Stackoverflow
Solution 1 - AsynchronousRick BallardView Answer on Stackoverflow
Solution 2 - AsynchronousBalestraPatrickView Answer on Stackoverflow
Solution 3 - AsynchronousPaul CantrellView Answer on Stackoverflow
Solution 4 - AsynchronousikuramediaView Answer on Stackoverflow
Solution 5 - Asynchronousbradd123View Answer on Stackoverflow
Solution 6 - Asynchronousp-sunView Answer on Stackoverflow
Solution 7 - AsynchronousLloyd BriggsView Answer on Stackoverflow
Solution 8 - AsynchronousTony PanView Answer on Stackoverflow