ios - ¿Termina todas las solicitudes asíncronas antes de cargar datos?
facebook swift (6)
Me encontré con un problema en el que tengo varias solicitudes asíncronas que capturan imágenes e información de la API de Facebook y mi base de datos de Firebase. Quiero realizar todas mis solicitudes asíncronas, luego almacenar todos los datos que obtuve de la API de Facebook / base de datos de Firebase en un objeto completo que puedo cargar rápidamente. He configurado controladores de finalización para cada solicitud asincrónica, lo que pensé que obligaba al programa a "esperar" hasta que se complete la solicitud y luego que el programa continúe, pero eso no parece funcionar para mí. Debajo está mi intento:
func setupEvents(completion: (result: Bool, Event: Event) -> Void){
// Get a reference to Events
eventsReference = Firebase(url:"<DB Name>")
eventAttendeesRef = Firebase(url:"<DB Name>")
//Read the data at our posts reference
println("Event References: /(eventsReference)")
eventsReference.observeEventType(FEventType.ChildAdded, withBlock: { (snapshot) -> Void in
let eventName = snapshot.value["eventName"] as? String
let eventLocation = snapshot.value["eventLocation"] as? String
let eventCreator = snapshot.value["eventCreator"] as? String
var attendees: NSMutableDictionary = [:]
var attendeesImages = [UIImage]()
let attendee: NSMutableDictionary = [:]
let group = dispatch_group_create()
//Get attendees first
dispatch_group_enter(group)
self.getAttendees(snapshot.key as String, completion:{ (result, name, objectID) -> Void in
if(result == true){
println("Finished grabbing /(name!) /(objectID!)")
attendees.addEntriesFromDictionary(attendee as [NSObject : AnyObject])
}
else {
println("False")
}
dispatch_group_leave(group)
})
//Get attendees photos
dispatch_group_enter(group)
self.getAttendeesPictures(attendee, completion: { (result, image) -> Void in
if result == true {
println("Finished getting attendee photos. Now to store into Event object.")
attendeesImages.append(image!)
}
else{
println("false")
}
dispatch_group_leave(group)
})
dispatch_group_notify(group, dispatch_get_main_queue()) {
println("both requests done")
//Maintain array snapshot keys
self.eventIDs.append(snapshot.key)
if snapshot != nil {
let event = Event(eventName: eventName, eventLocation:eventLocation, eventPhoto:eventPhoto, fromDate:fromDate, fromTime:fromTime, toDate:toDate, toTime:toTime, attendees: attendees, attendeesImages:attendeesImages, attendeesImagesTest: attendeesImagesTest, privacy:privacy, eventCreator: eventCreator, eventCreatorID: eventCreatorID)
println("Event: /(event)")
completion(result: true, Event: event)
}
}
}) { (error) -> Void in
println(error.description)
}
}
Sé que tengo mis manejadores de finalización configurados correctamente como lo he probado en mi programa. Sin embargo, lo que quiero es que solo después de que se getAttendeesPictures
funciones getAttendees
y getAttendeesPictures
, quiero almacenar toda la información. Tomé la función snapshot
, getAttendees
y getAttendeesPictures
y las getAttendeesPictures
en un objeto de event
. ¿Alguna idea sobre cómo lograr esto? Intenté buscar en dispatch_groups
para ayudarme a manejar esto a través de este enlace: Comprobación de múltiples respuestas asíncronas de Alamofire y Swift pero mi programa parece ejecutar solo la función getAttendees
pero no la función getAttendeesPictures
. A continuación se encuentran también las funciones getAttendees
y getAttendeesPictures
:
func getAttendees(child: String, completion: (result: Bool, name: String?, objectID: String?) -> Void){
//Get event attendees of particular event
var attendeesReference = self.eventAttendeesRef.childByAppendingPath(child)
println("Loading event attendees")
//Get all event attendees
attendeesReference.observeEventType(FEventType.ChildAdded, withBlock: { (snapshot) -> Void in
let name = snapshot.value.objectForKey("name") as? String
let objectID = snapshot.value.objectForKey("objectID") as? String
println("Name: /(name) Object ID: /(objectID)")
completion(result: true, name: name, objectID: objectID)
}) { (error) -> Void in
println(error.description)
}
func getAttendeesPictures(attendees: NSMutableDictionary, completion: (result: Bool, image: UIImage?)-> Void){
println("Attendees Count: /(attendees.count)")
for (key, value) in attendees{
let url = NSURL(string: "https://graph.facebook.com//(key)/picture?type=large")
println("URL: /(url)")
let urlRequest = NSURLRequest(URL: url!)
//Asynchronous request to display image
NSURLConnection.sendAsynchronousRequest(urlRequest, queue: NSOperationQueue.mainQueue()) { (response:NSURLResponse!, data:NSData!, error:NSError!) -> Void in
if error != nil{
println("Error: /(error)")
}
// Display the image
let image = UIImage(data: data)
if(image != nil){
completion(result: true, image: image)
}
}
}
}
Esto está fuera de mi cabeza. La idea es leer y manejar nuevos datos asyc solo cuando se completen todos los bloques anidados.
Aprovechamos un ciclo while para manejar la espera de una señal para leer el siguiente conjunto de datos.
El bucle while externo continúa mientras done sea igual a falso. Y realmente no sucede nada, aparte de consumir ciclos de CPU mientras espera. El si dentro del bucle solo se activará (se establecerá en verdadero) cuando se hayan leído todos los asistentes.
Mientras tanto, dentro del ciclo trabajamos a través de bloques anidados, leyendo en el asistente y luego, cuando se completa, lee su imagen, y cuando eso termine lea los datos de la base de datos. Finalmente, una vez que tenemos todos los datos de los bloques previos, almacenamos los datos en un objeto que luego se agrega al diccionario. En ese momento, se determina si terminamos de leer a los asistentes y, en caso afirmativo, suelta la fianza por completo. Si no, leemos el próximo asistente.
(esto es conceptual)
done = false
readyToReadNextAttendee = true
while ( done == false )
{
if (readyToReadNextAttendee == true ) {
readyToReadNextAttendee = false
readAttendee
readPicture
readFirebase {
putDataIntoObject
addObjectToDictionary
if finishedReadingAttendees {
done = true
} else {
readyToReadNextAttendee = true
}
}
}
}
Si tiene la opción de leer en todos los asistentes primero, también puede iterar y ordenar, sin leer el siguiente índice hasta readyToReadNextAttendee = true
Hay algo mal con esto conceptualmente. Parece que desea esperar hasta que ambas funciones se completen antes de hacer otra cosa, pero lo que no ha explicado es que getAttendeesPictures
depende del resultado de getAttendees
. Eso significa que lo que realmente quiere hacer es ejecutar un bloque asíncrono, luego ejecutar un segundo bloque asíncrono con la salida del primero y luego ejecutar el bloque de finalización final cuando ambos terminen.
GCD no es particularmente adecuado para esto; es mejor utilizar NSOperationQueue con NSBlockOperations. Hay dos ventajas distintas a esto sobre GCD:
- NSOperation utiliza una sintaxis familiar orientada a objetos en comparación con las funciones de tipo C de GCD, por lo que es bastante fácil de escribir y comprender.
- Las operaciones en la cola pueden tener dependencias explícitas entre sí, por lo que puede dejar en claro que, por ejemplo, la operación B solo se ejecutará después de que se complete la operación A.
Hay una gran reseña de esto por NSHipster que recomiendo que vayas a leer. Se habla sobre todo en abstracto, pero lo que quiere hacer es usar NSBlockOperation para crear dos operaciones de bloques, una para ejecutar getAttendees
y otra para ejecutar getAttendeesPictures
, y luego hacer explícito que el segundo bloque depende del primero antes de agregarlos a ambos a una cola. Luego ambos se ejecutarán y puede usar un bloque de finalización en la segunda operación para hacer algo una vez que ambos se hayan completado.
Sin embargo, Dave Roberts tiene razón en su respuesta: un problema inmediato con el código es que no utiliza el resultado de la función getAttendees
para crear realmente asistentes. Quizás esta parte del código falta, pero por lo que puedo ver, el name
y el objectID
simplemente se imprimen. Si desea pasar algo útil a la función getAttendeesPictures
, deberá corregir esta parte primero.
Las dos solicitudes se están ejecutando al mismo tiempo, por lo que no hay asistentes para obtener imágenes de cuando se ejecuta la segunda solicitud, si el cierre de finalización de getAttendees
se va a llamar varias veces, entonces puede hacer algo como esto:
let group = dispatch_group_create()
for key in keys {
dispatch_group_enter(group)
self.getAttendee(key as String, completion:{ (result, attendee) in
if(result == true){
attendees.addEntriesFromDictionary(attendee)
self.getAttendeesPictures(attendee, completion: { (result, image) in
if result == true {
attendeesImages.append(image!)
}
dispatch_group_leave(group)
})
} else {
dispatch_group_leave(group)
}
})
}
dispatch_group_notify(group, dispatch_get_main_queue()) {}
Si el resultado de la primera solicitud es el conjunto completo de asistentes, ni siquiera necesita usar GCD, solo llame a getAttendeesPictures
dentro del cierre de finalización.
Este código no usa exactamente las mismas variables y métodos del código original, solo da la idea.
¡Espero eso ayude!
Para los usuarios que buscan respuesta a una pregunta en el título, utilicen dispatch_group
y GCD descritos aquí: es decir, insertar un grupo dentro del método de notificación de otro dispatch_group
es válido. Otra forma de ir a un nivel superior sería NSOperations
y dependencias que también darían un mayor control, como la cancelación de operaciones.
Esquema :
func doStuffonObjectsProcessAndComplete(arrayOfObjectsToProcess: Array) -> Void){
let firstGroup = dispatch_group_create()
for object in arrayOfObjectsToProcess {
dispatch_group_enter(firstGroup)
doStuffToObject(object, completion:{ (success) in
if(success){
// doing stuff success
}
else {
// doing stuff fail
}
// regardless, we leave the group letting GCD know we finished this bit of work
dispatch_group_leave(firstGroup)
})
}
// called once all code blocks entered into group have left
dispatch_group_notify(firstGroup, dispatch_get_main_queue()) {
let processGroup = dispatch_group_create()
for object in arrayOfObjectsToProcess {
dispatch_group_enter(processGroup)
processObject(object, completion:{ (success) in
if(success){
// processing stuff success
}
else {
// processing stuff fail
}
// regardless, we leave the group letting GCD know we finished this bit of work
dispatch_group_leave(processGroup)
})
}
dispatch_group_notify(processGroup, dispatch_get_main_queue()) {
print("All Done and Processed, so load data now")
}
}
}
El resto de esta respuesta es específico de esta base de código.
Parece que hay algunos problemas aquí: la función getAttendees
toma un evento child y devuelve un objectID
y un Name
que son ambos Strings? ¿No debería este método devolver un conjunto de asistentes? Si no, ¿cuál es el objectID
que se devuelve?
Una vez que se devuelve una matriz de asistentes, puede procesarlos en un grupo para obtener las imágenes.
El getAttendeesPictures
finalmente devuelve UIImages
de Facebook. Probablemente sea mejor guardarlos en el disco y pasar la path ref
. Mantener todas estas imágenes recuperadas es malo para la memoria y, dependiendo del tamaño y el número, puede ocasionar problemas rápidamente.
Algunos ejemplos:
func getAttendees(child: String, completion: (result: Bool, attendees: Array?) -> Void){
let newArrayOfAttendees = []()
// Get event attendees of particular event
// process attendees and package into an Array (or Dictionary)
// completion
completion(true, attendees: newArrayOfAttendees)
}
func getAttendeesPictures(attendees: Array, completion: (result: Bool, attendees: Array)-> Void){
println("Attendees Count: /(attendees.count)")
let picturesGroup = dispatch_group_create()
for attendee in attendees{
// for each attendee enter group
dispatch_group_enter(picturesGroup)
let key = attendee.objectID
let url = NSURL(string: "https://graph.facebook.com//(key)/picture?type=large")
let urlRequest = NSURLRequest(URL: url!)
//Asynchronous request to display image
NSURLConnection.sendAsynchronousRequest(urlRequest, queue: NSOperationQueue.mainQueue()) { (response:NSURLResponse!, data:NSData!, error:NSError!) -> Void in
if error != nil{
println("Error: /(error)")
}
// Display the image
let image = UIImage(data: data)
if(image != nil){
attendee.image = image
}
dispatch_group_leave(picturesGroup)
}
}
dispatch_group_notify(picturesGroup, dispatch_get_main_queue()) {
completion(true, attendees: attendees)
}
}
func setupEvents(completion: (result: Bool, Event: Event) -> Void){
// get event info and then for each event...
getAttendees(child:snapshot.key, completion: { (result, attendeesReturned) in
if result {
self.getAttendeesPictures(attendees: attendeesReturned, completion: { (result, attendees) in
// do something with completed array and attendees
}
}
else {
}
})
}
El código anterior es solo un esquema, pero con suerte lo señala en la dirección correcta.
Si bien definitivamente hay una solución con el uso de GCD y otras cosas, la sincronización en general es dolorosa y cuanto más se complica el código, más problemas comenzará a mostrar, pero creo que hay una solución única para eso: el marco de Bolts de Facebook (ambos para Android y iOS)
Uso del marco de pernos
Entonces, ¿qué tiene de mágico? Bueno, te permite crear "Tareas" y luego encadenarlas. El método en particular que le interesa es taskForCompletionOfAllTasks: que está hecho para procesamiento paralelo, justo lo que necesita. Escribí un pequeño ejemplo para usted que puede ajustar a sus necesidades:
func fetchAllInformation() -> BFTask {
// First, create all tasks (if you need more, than just create more, it is as easy as that
var task1 = BFTaskCompletionSource()
var task2 = BFTaskCompletionSource()
var tasks = [task1, task2]
// What you do, is you set result / error to tasks and the propagate in the chain upwards (it is either result, or error)
// You run task 1 in background
API.instance.fetchFirstDetailsInBackgroundWithBlock {
(object: AnyObject!, error: NSError!) -> Void in
// On error or on success, you assign result to task (whatever you want)
if error == nil {
task1.setResult(object)
} else {
task1.setError(error)
}
}
// You run task 2 in background
API.instance.fetchSecondDetailsInBackgroundWithBlock {
(object: AnyObject!, error: NSError!) -> Void in
// On error or on success, you assign result to task (whatever you want)
if error == nil {
task2.setResult(object)
} else {
task2.setError(error)
}
}
// Now you return new task, which will continue ONLY if all the tasks ended
return BFTask(forCompletionOfAllTasks: tasks)
}
Una vez que hayas hecho el método principal, puedes usar pernos que encadenan la magia:
func processFullObject() {
// Once you have main method done, you can use bolts chaining magic
self.fetchAllInformation().continueWithBlock { (task : BFTask!) -> AnyObject! in
// All the information fetched, do something with result and probably with information along the way
self.updateObject()
}
}
La documentación del marco de Bolts / README cubre básicamente todo lo que hay que saber al respecto y es bastante extensa, por lo que le sugiero que la revise: es muy fácil de usar una vez que obtiene los conceptos básicos. Yo personalmente lo uso para esto, y es una maravilla. Esperamos que esta respuesta le brinde una solución y enfoque diferentes, posiblemente uno más limpio.
Una idea que he utilizado es colocar una comprobación de declaración if dentro de la llamada de declaración de consulta y colocar la llamada de instrucción de consulta en un ciclo for (para que pueda recorrer todas sus consultas), por lo que la instrucción if debería comprobar si esto es Última llamada esperada, luego debe ejecutar una declaración de devolución o una declaración de deferred.resolve, el siguiente es un código de concepto.
var list=fooKeys //list of keys (requests) i want to fetch form firebase
var array=[] // This is the array that will hold the result of all requests
for(i=xyz;loop breaking condition; i++){
Ref = new Firebase("https://yourlink.firebaseio.com/foo/" + fooKeys[i]);
Ref.once("value", function (data) {
array.push(data.val());
if(loop breaking condition == true){
//This mean that we looped over all items
return array; //or deferred.resolve(array);
}
})
}
Poner este código en una función y llamarlo de forma asíncrona le dará la posibilidad de esperar por los resultados completos antes de proceder a hacer otras cosas.
Espero que (y los otros) encuentren esto beneficioso.