Como executo callbacks assíncronos no Playground

117

Muitos métodos Cocoa e CocoaTouch têm callbacks de conclusão implementados como blocos em Objective-C e Closures em Swift. No entanto, ao tentar isso no Playground, a conclusão nunca é chamada. Por exemplo:

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

Posso ver a saída do console na linha do tempo do Playground, mas printlnno meu bloco de conclusão nunca são chamados ...

ikuramedia
fonte

Respostas:

186

Embora você possa executar um loop de execução manualmente (ou, para código assíncrono que não requer um loop de execução, use outros métodos de espera como semáforos de despacho), a maneira "embutida" que fornecemos em playgrounds para esperar pelo trabalho assíncrono é importar a XCPlaygroundestrutura e definir XCPlaygroundPage.currentPage.needsIndefiniteExecution = true. Se essa propriedade tiver sido definida, quando sua fonte de playground de nível superior terminar, em vez de parar o playground lá, continuaremos a girar o loop de execução principal, para que o código assíncrono tenha a chance de ser executado. Eventualmente encerraremos o playground após um tempo limite que é de 30 segundos, mas que pode ser configurado se você abrir o editor assistente e mostrar o assistente de cronograma; o tempo limite está no canto inferior direito.

Por exemplo, em Swift 3 (usando em URLSessionvez 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

Ou em 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
fonte
1
Para valer a pena, isso é abordado na WWDC 2014 §408: Swift Playgrounds, segunda metade
Chris Conover
3
Vale a pena notar que a partir do DP4, a XCPlaygroundestrutura agora está disponível para iOS Playgrounds também.
ikuramedia
4
Método atualizado:XCPlaygroundPage.currentPage.needsIndefiniteExecution = true
R Menke
23
Método atualizado: import PlaygroundSupportePlaygroundPage.current.needsIndefiniteExecution = true
SimplGy
48

Esta API mudou novamente no Xcode 8 e foi movida para PlaygroundSupport:

import PlaygroundSupport

PlaygroundPage.current.needsIndefiniteExecution = true

Essa mudança foi mencionada na Sessão 213 no WWDC 2016 .

BalestraPatrick
fonte
2
Não se esqueça de ligar PlaygroundPage.current.finishExecution().
Glenn
36

A partir do XCode 7.1, XCPSetExecutionShouldContinueIndefinitely()está obsoleto. A maneira correta de fazer isso agora é primeiro solicitar a execução indefinida como uma propriedade da página atual:

import XCPlayground

XCPlaygroundPage.currentPage.needsIndefiniteExecution = true

... então indique quando a execução terminar com:

XCPlaygroundPage.currentPage.finishExecution()

Por exemplo:

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
fonte
16

A razão pela qual os callbacks não são chamados é porque o RunLoop não está rodando no Playground (ou no modo REPL para esse assunto).

Uma maneira um tanto janky, mas eficaz, de fazer os callbacks operarem é com um sinalizador e, em seguida, iterando manualmente no 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 padrão tem sido frequentemente usado em testes de unidade que precisam testar retornos de chamada assíncronos, por exemplo: Padrão para fila assíncrona de teste de unidade que chama a fila principal na conclusão

ikuramedia
fonte
8

As novas APIs para XCode8, Swift3 e iOS 10 são,

// import the module
import PlaygroundSupport
// write this at the beginning
PlaygroundPage.current.needsIndefiniteExecution = true
// To finish execution
PlaygroundPage.current.finishExecution()
bradd123
fonte
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
fonte
3

Swift 3, xcode 8, iOS 10

Notas:

Diga ao compilador que o arquivo do playground requer "execução indefinida"

Encerre manualmente a execução por meio de uma chamada para PlaygroundSupport.current.completeExecution()dentro de seu manipulador de conclusão.

Você pode ter problemas com o diretório de cache e para resolver isso você precisará reinstanciar manualmente o singleton UICache.shared.

Exemplo:

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