Utilisation des delegates dans NSOperation

J'essaie de faire usage de CLLocationManager dans un NSOperation . Dans le cadre de cela, j'ai besoin de la possibilité de startUpdatingLocation puis attendre jusqu'à ce qu'une CLLocation soit reçue avant de terminer l'opération.

À l'heure actuelle, j'ai fait ce qui suit, mais la méthode des delegates ne semble jamais appelée. S'il vous plaît, quelqu'un peut-il nous dire quel est le problème?

 - (void)main { @autoreleasepool { if (self.isCancelled) return; // Record the fact we have not found the location yet shouldKeepLooking = YES; // Setup the location manager NSLog(@"Setting up location manager."); CLLocationManager *locationManager = [[CLLocationManager alloc] init]; locationManager.delegate = self; locationManager.desiredAccuracy = kCLLocationAccuracyBest; [locationManager startUpdatingLocation]; while (shouldKeepLooking) { if (self.isCancelled) return; // Do some other logic... } } } - (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations { // None of this ever seems to be called (despite updating the location) latestLocation = [locations lastObject]; [manager stopUpdatingLocation]; shouldKeepLooking = NO; } 

Il va appeler la méthode delegate dans la même queue que la fonction principale. Les files d'attente NSOperation sont en série par défaut. Votre boucle while est en train de tourner pour toujours (car l'opération n'est jamais annulée) et l'appel à votre méthode déléguée est assis dans la queue derrière elle ne peut jamais être exécuté.

Débarrassez-vous entièrement de la boucle while et laissez l'opération se terminer. Ensuite, lorsque la méthode delegate est appelée, si elle est annulée, ignorez le résultat en returnnant.

Pour revenir à la discussion runloop, voici comment je résous généralement cela dans mon implémentation NSOperation base:

 // create connection and keep the current runloop running until // the operation has finished. this allows this instance of the operation // to act as the connections delegate _connection = [[NSURLConnection alloc] initWithRequest:[self request] delegate:self]; while(!self.isFinished) { [[NSRunLoop currentRunLoop] runUntilDate:[NSDate distantFuture]]; } 

Je isFinished sur isFinished , que je garde à jour grâce à des setters pour isCancelled et isFinished . Voici le isCancelled comme exemple:

 - (void)setIsCancelled:(BOOL)isCancelled { _isCancelled = isCancelled; if (_isCancelled == YES) { self.isFinished = YES; } } 

Cela dit, j'appuie certaines des questions sur les raisons pour lesquelles cela est nécessaire. Si vous n'avez pas besoin de supprimer quelque chose jusqu'à ce qu'un location soit trouvé, pourquoi ne pas lancer votre gestionnaire de localization sur le thread principal, attendre le callback de délégué approprié, puis lancer l'opération en arrière-plan?

Mise à jour: solution mise à jour

Bien que la réponse initiale soit généralement la même, j'ai implémenté une solution complète et cela nécessite un léger changement dans la gestion de la boucle d'exécution. Cela dit, tout le code est disponible sur GitHub – https://github.com/nathanhjones/CLBackgroundOperation . Voici une explication détaillée de l'approche.

Tl; dr

Changement

 [[NSRunLoop currentRunLoop] runUntilDate:[NSDate distantFuture]]; 

à

 [[NSRunLoop currentRunLoop] runMode:NSRunLoopCommonModes beforeDate:[NSDate distantFuture]]; 

Détails

Dans votre interface d'opérations, définissez les trois propriétés suivantes. Nous indiquerons que ces opérations sont simultanées, donc nous gérerons leur état manuellement. Dans la solution sur GitHub, ils font partie de NJBaseOperation .

 @property(nonatomic,assign,readonly) BOOL isExecuting; @property(nonatomic,assign,readonly) BOOL isFinished; @property(nonatomic,assign,readonly) BOOL isCancelled; 

Dans votre implémentation des opérations, vous voudrez faire en sorte que readwrite fonctionne comme ceci:

 @interface NJBaseOperation () @property(nonatomic,assign,readwrite) BOOL isExecuting; @property(nonatomic,assign,readwrite) BOOL isFinished; @property(nonatomic,assign,readwrite) BOOL isCancelled; @end 

Ensuite, vous voudrez synthétiser les trois propriétés que vous avez définies ci-dessus afin de pouvoir replace les setters et les utiliser pour gérer votre état d'opérations. Voici ce que j'utilise généralement, mais parfois il y a des instructions supplémentaires ajoutées à la méthode setIsFinished: selon mes besoins.

 - (void)setIsExecuting:(BOOL)isExecuting { _isExecuting = isExecuting; if (_isExecuting == YES) { self.isFinished = NO; } } - (void)setIsFinished:(BOOL)isFinished { _isFinished = isFinished; if (_isFinished == YES) { self.isExecuting = NO; } } - (void)setIsCancelled:(BOOL)isCancelled { _isCancelled = isCancelled; if (_isCancelled == YES) { self.isFinished = YES; } } 

Enfin, juste pour ne pas avoir à envoyer manuellement les notifications KVO, nous allons implémenter la méthode suivante. Cela fonctionne car nos propriétés sont nommées isExecuting , isFinished et isCancelled .

 + (BOOL) automaticallyNotifiesObserversForKey:(NSSsortingng *)key { return YES; } 

Maintenant que la fondation des opérations est prise en charge il est time de supprimer les trucs de localization. Vous devez replace main et lancer votre gestionnaire de position à l'intérieur et requestr à la boucle d'exécution en cours de continuer à fonctionner jusqu'à ce que vous le disiez autrement. Cela garantit que votre thread est sur le sharepoint recevoir les callbacks de délégué d'location. Voici ma mise en œuvre:

 - (void)main { if (_locationManager == nil) { _locationManager = [[CLLocationManager alloc] init]; _locationManager.delegate = self; _locationManager.desiredAccuracy = kCLLocationAccuracyBest; [_locationManager startUpdatingLocation]; } while(!self.isFinished) { [[NSRunLoop currentRunLoop] runMode:NSRunLoopCommonModes beforeDate:[NSDate distantFuture]]; } } 

Vous devriez recevoir un callback de délégué à quel point vous pouvez effectuer un travail en fonction de l'location, puis terminer l'opération. Voici ma mise en œuvre qui count jusqu'à 10 000, puis nettoie.

 - (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations { NSLog(@"** Did Update Location: %@", [locations lastObject]); [_locationManager stopUpdatingLocation]; // do something here that takes some length of time to complete for (int i=0; i<10000; i++) { if ((i % 10) == 0) { NSLog(@"Loop %i", i); } } self.isFinished = YES; } 

La source sur GitHub inclut une implémentation de dealloc , qui se connecte simplement à son appel et observe également les changements apportés à l' operationCount de mon NSOperationQueue et enregistre le count – indiquant quand il revient à 0. Espérons que cela aide. Faites-moi savoir si vous avez des questions.

Je pense que vous avez deux options.

  1. Créez un thread séparé, avec sa propre boucle d'exécution, pour les services de localization:

     #import "LocationOperation.h" #import <CoreLocation/CoreLocation.h> @interface LocationOperation () <CLLocationManagerDelegate> @property (nonatomic, readwrite, getter = isFinished) BOOL finished; @property (nonatomic, readwrite, getter = isExecuting) BOOL executing; @property (nonatomic, strong) CLLocationManager *locationManager; @end @implementation LocationOperation @synthesize finished = _finished; @synthesize executing = _executing; - (id)init { self = [super init]; if (self) { _finished = NO; _executing = NO; } return self; } - (void)start { if ([self isCancelled]) { self.finished = YES; return; } self.executing = YES; [self performSelector:@selector(main) onThread:[[self class] locationManagerThread] withObject:nil waitUntilDone:NO modes:[[NSSet setWithObject:NSRunLoopCommonModes] allObjects]]; } - (void)main { [self startStandardUpdates]; } - (void)dealloc { NSLog(@"%s", __FUNCTION__); } #pragma mark - NSOperation methods - (BOOL)isConcurrent { return YES; } - (void)setExecuting:(BOOL)executing { if (executing != _executing) { [self willChangeValueForKey:@"isExecuting"]; _executing = executing; [self didChangeValueForKey:@"isExecuting"]; } } - (void)setFinished:(BOOL)finished { if (finished != _finished) { [self willChangeValueForKey:@"isFinished"]; _finished = finished; [self didChangeValueForKey:@"isFinished"]; } } - (void)completeOperation { self.executing = NO; self.finished = YES; } - (void)cancel { [self stopStandardUpdates]; [super cancel]; [self completeOperation]; } #pragma mark - Location Manager Thread + (void)locationManagerThreadEntryPoint:(id __unused)object { @autoreleasepool { [[NSThread currentThread] setName:@"location manager"]; NSRunLoop *runLoop = [NSRunLoop currentRunLoop]; [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode]; [runLoop run]; } } + (NSThread *)locationManagerThread { static NSThread *_locationManagerThread = nil; static dispatch_once_t oncePredicate; dispatch_once(&oncePredicate, ^{ _locationManagerThread = [[NSThread alloc] initWithTarget:self selector:@selector(locationManagerThreadEntryPoint:) object:nil]; [_locationManagerThread start]; }); return _locationManagerThread; } #pragma mark - Location Services - (void)startStandardUpdates { if (nil == self.locationManager) self.locationManager = [[CLLocationManager alloc] init]; self.locationManager.delegate = self; self.locationManager.desiredAccuracy = kCLLocationAccuracyBest; self.locationManager.distanceFilter = 500; [self.locationManager startUpdatingLocation]; } - (void)stopStandardUpdates { [self.locationManager stopUpdatingLocation]; self.locationManager = nil; } #pragma mark - CLLocationManagerDelegate - (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations { CLLocation* location = [locations lastObject]; // do whatever you want with the location // now, turn off location services if (location.horizontalAccuracy < 50) { [self stopStandardUpdates]; [self completeOperation]; } } @end 
  2. Alternativement, même si vous utilisez une opération, vous pouvez simplement exécuter les services de localization sur le thread principal:

     #import "LocationOperation.h" #import <CoreLocation/CoreLocation.h> @interface LocationOperation () <CLLocationManagerDelegate> @property (nonatomic, readwrite, getter = isFinished) BOOL finished; @property (nonatomic, readwrite, getter = isExecuting) BOOL executing; @property (nonatomic, strong) CLLocationManager *locationManager; @end @implementation LocationOperation @synthesize finished = _finished; @synthesize executing = _executing; - (id)init { self = [super init]; if (self) { _finished = NO; _executing = NO; } return self; } - (void)start { if ([self isCancelled]) { self.finished = YES; return; } self.executing = YES; [self startStandardUpdates]; } #pragma mark - NSOperation methods - (BOOL)isConcurrent { return YES; } - (void)setExecuting:(BOOL)executing { if (executing != _executing) { [self willChangeValueForKey:@"isExecuting"]; _executing = executing; [self didChangeValueForKey:@"isExecuting"]; } } - (void)setFinished:(BOOL)finished { if (finished != _finished) { [self willChangeValueForKey:@"isFinished"]; _finished = finished; [self didChangeValueForKey:@"isFinished"]; } } - (void)completeOperation { self.executing = NO; self.finished = YES; } - (void)cancel { [self stopStandardUpdates]; [super cancel]; [self completeOperation]; } #pragma mark - Location Services - (void)startStandardUpdates { [[NSOperationQueue mainQueue] addOperationWithBlock:^{ if (nil == self.locationManager) self.locationManager = [[CLLocationManager alloc] init]; self.locationManager.delegate = self; self.locationManager.desiredAccuracy = kCLLocationAccuracyBest; self.locationManager.distanceFilter = 500; [self.locationManager startUpdatingLocation]; }]; } - (void)stopStandardUpdates { [self.locationManager stopUpdatingLocation]; self.locationManager = nil; } #pragma mark - CLLocationManagerDelegate - (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations { CLLocation* location = [locations lastObject]; // do whatever you want with the location // now, turn off location services if (location.horizontalAccuracy < 50) { [self stopStandardUpdates]; [self completeOperation]; } } @end 

Je pense que je serais enclin à faire la deuxième approche (en veillant juste à ne rien faire de trop intensif dans didUpdateLocations , ou si je le faisais, assurez-vous de le faire de manière asynchronous), mais ces deux approches semblent fonctionner.

Une autre approche consiste à garder la boucle d'exécution en vie jusqu'à ce que l'opération soit terminée:

 while (![self isFinished]) { [[NSRunLoop currentRunLoop] runUntilDate:[NSDate distantFuture]]; } 

Mais cela ne semble pas fonctionner avec CLLocationManager , car runUntilDate ne returnne pas immédiatement (c'est presque comme si CLLocationManager attachait sa propre source au runloop, ce qui l'empêche de se fermer). Je suppose que vous pourriez changer le runUntilDate à quelque chose un peu plus proche que distantFuture (par exemple [NSDate dateWithTimeIntervalSinceNow:1.0] ). Pourtant, je pense qu'il est tout aussi facile d'exécuter ces services de démarrage de l'opération sur la queue principale, comme la deuxième solution ci-dessus.

Cela dit, je ne sais pas pourquoi vous voudriez utiliser le gestionnaire de localization dans une opération du tout. C'est déjà asynchronous, donc je voudrais simplement démarrer le gestionnaire de l'location de la queue principale et l'appeler un jour.

UIWebView avec les callbacks de la méthode UIWebViewDelegate dans un NSOperation

Un server que je voulais saisir une URL à partir d'un server qui change les valeurs basées sur l'exécution de JavaScript à partir de différents browsers. J'ai donc glissé un UIWebView factice dans un NSOperation et l'ai utilisé pour récupérer la valeur que je voulais dans la méthode UIWebViewDelegate.

 @interface WBSWebViewOperation () <UIWebViewDelegate> @property (assign, nonatomic) BOOL stopRunLoop; @property (assign, nonatomic, getter = isExecuting) BOOL executing; @property (assign, nonatomic, getter = isFinished) BOOL finished; @property (copy, nonatomic, readwrite) NSURL *videoURL; @property (strong, nonatomic) UIWebView *webView; @end @implementation WBSWebViewOperation - (id)init { self = [super init]; if (self) { _finished = NO; _executing = NO; } return self; } - (id)initWithURL:(NSURL *)episodeURL { self = [self init]; if (self != nil) { _episodeURL = episodeURL; } return self; } - (void)start { if (![self isCancelled]) { self.executing = YES; [self performSelector:@selector(main) onThread:[NSThread mainThread] withObject:nil waitUntilDone:NO modes:[[NSSet setWithObject:NSRunLoopCommonModes] allObjects]]; } else { self.finished = YES; } } - (void)main { if (self.episodeURL != nil) { NSURLRequest *request = [NSURLRequest requestWithURL:self.episodeURL]; UIWebView *webView = [[UIWebView alloc] init]; webView.delegate = self; [webView loadRequest:request]; self.webView = webView; } } #pragma mark - NSOperation methods - (BOOL)isConcurrent { return YES; } - (void)setExecuting:(BOOL)executing { [self willChangeValueForKey:@"isExecuting"]; _executing = executing; [self didChangeValueForKey:@"isExecuting"]; } - (void)setFinished:(BOOL)finished { [self willChangeValueForKey:@"isFinished"]; _finished = finished; [self didChangeValueForKey:@"isFinished"]; } - (void)completeOperation { self.executing = NO; self.finished = YES; } - (void)cancel { [self.webView stopLoading]; [super cancel]; [self completeOperation]; } #pragma mark - UIWebViewDelegate methods - (void)webViewDidFinishLoad:(UIWebView *)webView { NSSsortingng *episodeVideoURLSsortingng = [webView ssortingngByEvaluatingJavaScriptFromSsortingng:@"document.getElementById('playerelement').getAtsortingbute('data-media')"]; NSURL *episodeVideoURL = [NSURL URLWithSsortingng:episodeVideoURLSsortingng]; self.videoURL = episodeVideoURL; if ([self.delegate respondsToSelector:@selector(webViewOperationDidFinish:)]) { [self.delegate webViewOperationDidFinish:self]; } [self completeOperation]; } @end