Использование делегатов в NSOperation

Я пытаюсь использовать CLLocationManager в NSOperation. В качестве части этого я требую возможность startUpdatingLocation, затем дождаться, пока CLLocation не будет получен до завершения операции.

В настоящее время я сделал следующее, однако метод делегата никогда не называется. Пожалуйста, кто-нибудь может сообщить, в чем проблема?

- (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;
}

Ответ 1

Его вызов для вызова метода делегата в той же очереди операций, что и главная. И очереди NSOperation по умолчанию серийны. Ваш цикл while просто вращается навсегда (потому что операция никогда не отменяется), и вызов вашего метода делегата сидит в очереди за ней, которую невозможно запустить.

Полностью избавиться от цикла while и завершить операцию. Затем, когда вызывается метод делегата, если он отменяет отказ, возвращаемый результат.

Ответ 2

Возвращаясь к обсуждению runloop, я обычно решаю это в моей реализации NSOperation:

// 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]];
}

Я отключаю isFinished, который я обновляю через сеттеры для isCancelled и isFinished. Вот пример isCancelled в качестве примера:

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

Тем не менее, я закрепил некоторые вопросы о том, почему это необходимо. Если вам не нужно ничего ударять, пока не будет найдено местоположение, почему бы не просто запустить диспетчер местоположений в основном потоке, дождаться соответствующего обратного вызова делегата, а затем запустить фоновую операцию?

Обновление: обновленное решение

Хотя первоначальный ответ в целом стоит, я полностью реализую решение, и он требует небольшого изменения того, как вы управляете циклом запуска. Тем не менее, весь код доступен на GitHub - https://github.com/nathanhjones/CLBackgroundOperation. Вот подробное объяснение подхода.

Тл; д-р

Изменить

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

к

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

Подробнее

Внутри вашего операционного интерфейса определите следующие три свойства. Мы укажем, что эти операции параллельны, поэтому мы будем управлять своим состоянием вручную. В решении на GitHub они являются частью NJBaseOperation.

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

В рамках своей реализации вы захотите сделать так readwrite следующим образом:

@interface NJBaseOperation ()

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

@end

Затем вы захотите синтезировать три свойства, которые вы указали выше, чтобы вы могли переопределить сеттеры и использовать их для управления своим состоянием операций. Здесь то, что я обычно использую, но иногда есть некоторые дополнительные утверждения, добавленные к методу setIsFinished:, в зависимости от моих потребностей.

- (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;
    }
}

Наконец, чтобы нам не нужно вручную отправлять уведомления KVO, мы реализуем следующий метод. Это работает, потому что наши свойства называются isExecuting, isFinished и isCancelled.

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

Теперь, когда фундамент операций позаботится о том, чтобы время выбивать материал местоположения. Вы захотите переопределить main, и внутри него запустите диспетчер местоположений и проинструктируйте текущий цикл цикла, чтобы он продолжал работать, пока вы не скажете об этом иначе. Это гарантирует, что ваш поток будет принимать вызовы делегата делегата. Здесь моя реализация:

- (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]];
    }
}

Вы должны получить обратный вызов делегата, после чего вы можете выполнить некоторую работу на основе местоположения, а затем завершить операцию. Здесь моя реализация, которая насчитывает до 10 000, а затем очищает.

- (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;
}

Источник в GitHub включает реализацию dealloc, которая просто регистрирует, что она вызывается, а также наблюдает за изменениями в operationCount моего NSOperationQueue и записывает счет - указывая, когда она возвращается к 0. Надежда что помогает. Дайте мне знать, если у вас есть вопросы.

Ответ 3

Я думаю, у вас есть два варианта.

  • Создайте отдельный поток с собственным циклом запуска для служб определения местоположения:

    #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
    
  • Кроме того, даже если вы используете операцию, вы можете просто запустить службы определения местоположения в основном потоке:

    #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
    

Я думаю, что я был бы склонен сделать второй подход (просто убедитесь, что я не делаю ничего слишком интенсивного в didUpdateLocations, или если бы я сделал это, сделайте это асинхронно), но оба эти подходы, похоже, работают.

Другой подход заключается в том, чтобы поддерживать цикл запуска до завершения операции:

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

Но это не работает в сочетании с CLLocationManager, так как runUntilDate не возвращает сразу (почти так, как будто CLLocationManager прикрепляет свой собственный источник к runloop, что предотвращает его выход), Думаю, вы могли бы изменить runUntilDate на что-то немного ближе, чем distantFuture (например, [NSDate dateWithTimeIntervalSinceNow:1.0]). Тем не менее, я думаю, что так же просто запустить эту службу запуска службы запуска в основной очереди, как и второе решение выше.

Сказав это, я не уверен, почему вы хотите вообще использовать диспетчер местоположений. Он уже асинхронный, поэтому я просто запустил диспетчер местоположений из основной очереди и назвал его днем.

Ответ 4

UIWebView с обратными вызовами метода UIWebViewDelegate в NSOperation

Сервер, на котором я хотел получить URL-адрес с сервера, который изменяет значения на основе выполнения JavaScript из разных браузеров. Таким образом, я ударил фиктивный UIWebView в NSOperation и использовал это, чтобы извлечь значение, которое я хотел в методе 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
{    
    NSString *episodeVideoURLString = [webView stringByEvaluatingJavaScriptFromString:@"document.getElementById('playerelement').getAttribute('data-media')"];
    NSURL *episodeVideoURL = [NSURL URLWithString:episodeVideoURLString];
    self.videoURL = episodeVideoURL;

    if ([self.delegate respondsToSelector:@selector(webViewOperationDidFinish:)]) {
        [self.delegate webViewOperationDidFinish:self];
    }

    [self completeOperation];
}

@end