Comment exécuter des rappels asynchrones dans Playground
-
26-12-2019 - |
Question
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 Closures 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 le println
dans mon bloc d'achèvement ne sont jamais appelés...
La solution
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, utiliser d'autres méthodes d'attente comme les sémaphores de répartition), la manière "intégrée" que nous proposons dans les terrains de jeux pour attendre le travail asynchrone est de importer le XCPlayground
cadre et ensemble XCPlaygroundPage.currentPage.needsIndefiniteExecution = true
.Si cette propriété a été définie, une fois la source de votre terrain de jeu de niveau supérieur terminée, au lieu d'arrêter le terrain de jeu à cet endroit, 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'attente qui est 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 URLSession
au 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
Autres conseils
Cette API a encore changé dans Xcode 8 et a été déplacée vers le PlaygroundSupport
:
import PlaygroundSupport
PlaygroundPage.current.needsIndefiniteExecution = true
Ce changement a été mentionné dans Session 213 à la WWDC 2016.
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 actuelle :
import XCPlayground
XCPlaygroundPage.currentPage.needsIndefiniteExecution = true
…puis indiquez quand l’exécution est terminée 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()
La raison pour laquelle les rappels ne sont pas appelés est que RunLoop ne s'exécute pas en Playground (ni en mode REPL d'ailleurs).
Une façon un peu bizarre, mais efficace, de faire fonctionner les rappels consiste à utiliser un indicateur, puis à 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 de file d'attente asynchrone de tests unitaires qui appelle la file d'attente principale à la fin
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()
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()
Swift 3, Xcode 8, iOS 10
Remarques:
Dites au compilateur que le fichier du terrain de jeu nécessite une "exécution indéfinie"
Terminez manuellement l'exécution via un appel à PlaygroundSupport.current.completeExecution()
dans 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()
NSURLConnection.sendAsynchronousRequest(...)
NSRunLoop.currentRunLoop().run()