Comment exécuter des rappels asynchrones dans Playground

117

De nombreuses méthodes Cocoa et CocoaTouch ont des rappels d'achèvement implémentés sous forme de blocs dans Objective-C et de fermetures dans Swift. Cependant, lorsque vous les essayez dans Playground, la complétion n'est jamais appelée. Par exemple:

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

Je peux voir la sortie de la console dans ma timeline Playground, mais les printlndans mon bloc de complétion ne sont jamais appelés ...

Ikuramedia
la source

Réponses:

187

Bien que vous puissiez exécuter une boucle d'exécution manuellement (ou, pour le code asynchrone qui ne nécessite pas de boucle d'exécution, utilisez d'autres méthodes d'attente telles que l'envoi de sémaphores), la façon «intégrée» que nous fournissons dans les aires de jeux pour attendre le travail asynchrone est de importer le XCPlaygroundcadre et définir XCPlaygroundPage.currentPage.needsIndefiniteExecution = true. Si cette propriété a été définie, lorsque votre source de terrain de jeu de niveau supérieur se termine, au lieu d'arrêter le terrain de jeu, nous continuerons à faire tourner la boucle d'exécution principale, afin que le code asynchrone ait une chance de s'exécuter. Nous finirons par terminer le terrain de jeu après un délai d'expiration par défaut de 30 secondes, mais qui peut être configuré si vous ouvrez l'éditeur d'assistant et affichez l'assistant de chronologie; le délai d'attente est en bas à droite.

Par exemple, dans Swift 3 (en utilisant URLSessionau lieu 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 dans 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
la source
1
Pour tout ce que ça vaut, cela est couvert dans WWDC 2014 §408: Swift Playgrounds, deuxième mi
Chris Conover
3
Il convient de noter qu'à partir de DP4, le XCPlaygroundcadre est désormais également disponible pour iOS Playgrounds.
ikuramedia
4
Méthode mise à jour:XCPlaygroundPage.currentPage.needsIndefiniteExecution = true
R Menke
23
Méthode mise à jour: import PlaygroundSupportetPlaygroundPage.current.needsIndefiniteExecution = true
SimplGy
48

Cette API a de nouveau changé dans Xcode 8 et elle a été déplacée vers le PlaygroundSupport:

import PlaygroundSupport

PlaygroundPage.current.needsIndefiniteExecution = true

Ce changement a été mentionné lors de la session 213 de la WWDC 2016 .

BalestraPatrick
la source
2
N'oubliez pas d'appeler PlaygroundPage.current.finishExecution().
Glenn
36

Depuis XCode 7.1, XCPSetExecutionShouldContinueIndefinitely()est obsolète. La bonne façon de procéder maintenant est de demander d'abord une exécution indéfinie en tant que propriété de la page en cours:

import XCPlayground

XCPlaygroundPage.currentPage.needsIndefiniteExecution = true

… Puis indiquez la fin de l'exécution avec:

XCPlaygroundPage.currentPage.finishExecution()

Par exemple:

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
la source
16

La raison pour laquelle les rappels ne sont pas appelés est que le RunLoop ne fonctionne pas dans Playground (ou en mode REPL d'ailleurs).

Un moyen quelque peu capricieux, mais efficace, de faire fonctionner les rappels est d'utiliser un indicateur, puis d'itérer manuellement sur la boucle d'exécution:

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

Ce modèle a souvent été utilisé dans les tests unitaires qui doivent tester les rappels asynchrones, par exemple: Modèle pour la file d'attente asynchrone de test unitaire qui appelle la file d'attente principale à la fin

Ikuramedia
la source
8

Les nouvelles API comme pour XCode8, Swift3 et iOS 10 sont,

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

Swift 3, xcode 8, iOS 10

Remarques:

Dites au compilateur que le fichier de terrain de jeu nécessite une "exécution indéfinie"

Terminez manuellement l'exécution via un appel à l' PlaygroundSupport.current.completeExecution()intérieur de votre gestionnaire d'achèvement.

Vous pouvez rencontrer des problèmes avec le répertoire de cache et pour résoudre ce problème, vous devrez ré-instancier manuellement le singleton UICache.shared.

Exemple:

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