実行中のNSOperationに考慮しつつ、NSOperationQueueのキャンセルを実行する


`NSOperationQueue`の`cancelAllOperation`を呼び出せば、全ての処理を止めることが可能ですが、キューに追加した`NSOperation`の処理によっては、すぐにキャンセルできない場合があります。

例えば、下のようなケースの場合などです。

- ダウンロードしたデータをパースした後、Core Dataのデータインポート処理。すぐにキャンセルすると、データの不整合が発生するため、実行中の処理は終わるまで待たせる。

それを実現するには、下のように`NSOperationQueue`のサブクラスを作成し、実行中の処理が終わるまで待ち、終わったら完了のブロックを実行するメソッドを作ればよいでしょう。

```objectivec
// DataParseOperationQueue.h
#import <Foundation/Foundation.h>

@interface DataParseOperationQueue : NSOperationQueue

// XXX: 全ての実行中の`NSOperation`が終わるまで待ち、完了したらcompletionHandler blocksを実行する
- (void)cancelAllOperationsWithCompletionHandler:(void(^)(void))completionHandler;

@end

// DataParseOperationQueue.m
#import "DataParseOperationQueue.h"

static NSString *const SynchronizedQueueLabelPrefix = @"com.rh7.synchronized.DataParseOperationQueue";

@interface ParseOperationQueue ()

// XXX: 同時アクセスを防止するためのシリアルキュー(@synchronizedでも可)
@property (nonatomic) dispatch_queue_t synchronizedQueue;

@end

@implementation ParseOperationQueue

- (instancetype)init {
    self = [super init];
    if (self) {
        const char *synchronized_queue_label = [[SynchronizedQueueLabelPrefix stringByAppendingPathExtension:[[NSUUID UUID] UUIDString]] UTF8String];
        _synchronizedQueue = dispatch_queue_create(synchronized_queue_label, DISPATCH_QUEUE_SERIAL);
    }
    return self;
}

- (void)cancelAllOperationsWithCompletionHandler:(void(^)(void))completionHandler {
    dispatch_sync(self.synchronizedQueue, ^{
		// XXX: 実行待ちの`NSOperation`を実行されないように停止する
        self.suspended = YES;
		// XXX: 実行待ちの`NSOperation`は全てキャンセルする
        NSArray *pendingOperations = [self.operations filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"%K == %@", @"isExecuting", @NO]];
        [pendingOperations enumerateObjectsUsingBlock:^(NSOperation *op, NSUInteger idx, BOOL *stop) {
            [op cancel];
        }];

		// XXX: 実行中の`NSOperation`が全て完了してから、`completionHandler`を一番最後に実行するため、同時実行並列数を1にする
        NSUInteger currentMaxConcurrentCount = self.maxConcurrentOperationCount;
        self.maxConcurrentOperationCount = 1;
      	// XXX: 再開
        self.suspended = NO;
		// XXX: このOperationは必ず最後に実行される
        NSBlockOperation *waitForCompletion = [NSBlockOperation blockOperationWithBlock:^{
            if (completionHandler) {
                completionHandler();	// 呼び出し元から渡されたblocksを実行
            }
			// XXX: 設定を元に戻す
            self.maxConcurrentOperationCount = currentMaxConcurrentCount;
        }];
        [self addOperation:waitForCompletion];
    });
}

@end
```

このOperationQueueを使って、処理を実行するManagerクラスを定義した場合、キャンセル処理下記のようになります。

```objectivec
#import "DataParseOperationQueue.h"

@interface AccountManger ()

@property (nonatomic, readonly) DataParseOperationQueue *operationQueue;

@end

@implementation AccountManager

- (void)cancelAllOperationsWithCompletionHandler:(void(^)(void))completionHandler {
    dispatch_sync(self.synchronizedQueue, ^{
        DataParseOperationQueue *operationQueue = self.operationQueue;
        self.operationQueue = nil;      // XXX: 新しい処理を追加されないように、 一旦他から参照させないようにする
        // XXX: Pendingのままにして、実行していないオペレーションをすべてキャンセルする
        [operationQueue cancelAllOperationsWithCompletionHandler:^{
            // XXX: 元に戻す
            self.operationQueue = operationQueue;
            if (completionHandler) {
                completionHandler();
            }
        }];
    });
}

@end
```

さらに、ログアウト時に`NSOperationQueue`に追加されている処理が空っぽになるまで待つようにする場合は、下記のような感じになるでしょう。`PromiseKit`は、複数の処理を同時に実行して待つことが可能だから使ってます。

```objectivec
- (PMKPromise *)waitAllOperations {
    PMKPromise *accountManagerCancelAllOperations = [PMKPromise new:^(PMKPromiseFulfiller fulfill, PMKPromiseRejecter reject) {
        [[AccountManager defaultManager] cancelAllOperationsWithCompletionHandler:^{ fulfill(nil); }];
    }];
    PMKPromise *apiResourceManagerCancelAllOperations = [PMKPromise new:^(PMKPromiseFulfiller fulfill, PMKPromiseRejecter reject) {
        [[APIResourceManager defaultManager] cancelAllOperationsWithCompletionHandler:^{ fulfill(nil); }];
    }];
    return [PMKPromise when:@[accountManagerCancelAllOperations, organizationCancelAllOperations]];
}

- (void)logout {
	[APISessionManager sharedManager] resetSession];
	[self waitAllOperations].finally(^{
		// XXX: すべてのOperationQueueのキャンセルが完了したら、ログアウト処理を実行する。
		[self closeSessionWithNotification];
	});
}
```

## 参考

- [PromiseKit](http://promisekit.org/)