Использование делегатов в 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