¿Cómo ejecuto devoluciones de llamada asincrónicas en Playground?

117

Muchos métodos Cocoa y CocoaTouch tienen devoluciones de llamada de finalización implementadas como bloques en Objective-C y Closures en Swift. Sin embargo, al probarlos en Playground, nunca se llama a la finalización. Por ejemplo:

// 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)
    }
}

Puedo ver la salida de la consola en mi línea de tiempo de Playground, pero printlnnunca se llaman en mi bloque de finalización ...

ikuramedia
fuente

Respuestas:

186

Si bien puede ejecutar un ciclo de ejecución manualmente (o, para el código asincrónico que no requiere un ciclo de ejecución, use otros métodos de espera como semáforos de envío), la forma "incorporada" que proporcionamos en los patios de recreo para esperar el trabajo asincrónico es importar el XCPlaygroundmarco y configurarXCPlaygroundPage.currentPage.needsIndefiniteExecution = true . Si se ha establecido esta propiedad, cuando finalice la fuente del patio de recreo de nivel superior, en lugar de detener el patio de recreo allí, continuaremos girando el ciclo de ejecución principal, por lo que el código asincrónico tiene la oportunidad de ejecutarse. Eventualmente terminaremos el patio de recreo después de un tiempo de espera predeterminado de 30 segundos, pero que se puede configurar si abre el editor asistente y muestra el asistente de línea de tiempo; el tiempo de espera está en la parte inferior derecha.

Por ejemplo, en Swift 3 (usando URLSession lugar de 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

O en 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
Rick Ballard
fuente
1
Por todo lo que vale, esto está cubierto en WWDC 2014 §408: Swift Playgrounds, segunda mitad
Chris Conover
3
Vale la pena señalar que desde DP4, el XCPlaygroundmarco ahora también está disponible para iOS Playgrounds.
ikuramedia
4
Método actualizado:XCPlaygroundPage.currentPage.needsIndefiniteExecution = true
R Menke
23
Método actualizado: import PlaygroundSupportyPlaygroundPage.current.needsIndefiniteExecution = true
SimplGy
48

Esta API cambió nuevamente en Xcode 8 y se movió a PlaygroundSupport:

import PlaygroundSupport

PlaygroundPage.current.needsIndefiniteExecution = true

Este cambio se mencionó en la Sesión 213 en WWDC 2016 .

BalestraPatrick
fuente
2
No olvide llamar PlaygroundPage.current.finishExecution().
Glenn
36

A partir de XCode 7.1, XCPSetExecutionShouldContinueIndefinitely()está obsoleto. La forma correcta de hacer esto ahora es solicitar primero la ejecución indefinida como una propiedad de la página actual:

import XCPlayground

XCPlaygroundPage.currentPage.needsIndefiniteExecution = true

… Luego indique cuando la ejecución ha terminado con:

XCPlaygroundPage.currentPage.finishExecution()

Por ejemplo:

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()
Paul Cantrell
fuente
16

La razón por la que no se llaman las devoluciones de llamada es porque RunLoop no se está ejecutando en Playground (o en modo REPL para el caso).

Una forma algo burda, pero efectiva, de hacer que las devoluciones de llamada operen es con una bandera y luego iterando manualmente en el 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)
}

Este patrón se ha utilizado a menudo en pruebas unitarias que necesitan probar devoluciones de llamada asíncronas, por ejemplo: Patrón para la cola asíncrona de prueba unitaria que llama a la cola principal al finalizar

ikuramedia
fuente
8

Las nuevas API como para XCode8, Swift3 e iOS 10 son,

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

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()
p-sol
fuente
3

Swift 3, xcode 8, iOS 10

Notas:

Dígale al compilador que el archivo de la zona de juegos requiere "ejecución indefinida"

Finalice manualmente la ejecución mediante una llamada a PlaygroundSupport.current.completeExecution()su controlador de finalización.

Puede tener problemas con el directorio de caché y, para resolverlo, deberá volver a crear una instancia manual del singleton UICache.shared.

Ejemplo:

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()
Lloyd Briggs
fuente
-3
NSURLConnection.sendAsynchronousRequest(...)    
NSRunLoop.currentRunLoop().run()
Tony Pan
fuente