2016.03.09

NSOperationによるViewの管理


はじめに

こんにちは。次世代システム研究室のJ.L. です。

皆様はiOSアプリのビューをどのように管理していますか。例えば、あるイベント1、2があるとして、それぞれにアラートを表示するとします。そして、イベント1が発生し、それに対するアラートを表示している際に、イベント2が発生したとします。場合によってはすでに表示中のアラートを無視して、そのまま表示しても良いと思いますが、すでに表示中のアラートが閉じられて後、表示したい場合もあると思います。

上の例は少し極端な例かもしれませんが、ビューAから、ビューBに遷移する際に処理の流れで遷移を制御するのはストーリーボードでも良いですが、上の例のようにOSや不特定のタイミングで発生するイベントに対して条件に応じてビューを後で表示するするような制御はストーリーボードでは難しくなります。その際にはNSOperationを用いてViewの管理することで制御が容易になります。

 

NSOperationとNSOperationQueue

まず、NSOperationとは何かに関して少し説明しましょう。

NSOperationは任意のタスクを定義するクラスです。もちろんこれだけでは普通のクラスとあまり変わりがないものになりますが、NSOperationはタスクを効率良く管理するために様々なメソッドが事前に定義されています。それらのメソッドを利用し効率良くタスクたちを管理できるのがNSOperationQueueです。NSOperationQueueは名前の通りNSOperationを管理するQueueです。つまり、たくさんのタスクをQueueに入れておいて、一つもしくは複数のタスクを取り出し順次に実行することが可能です。もちろんQueueのタスクをサスペンドしたりキャンセルすることも可能です。そして、それぞれのタスクは全て別々のスレッドで実行されるので、マルチスレッドでタスクを管理することが簡単にできます。

 

NSOperation

NSOperationはそのまま利用するものではなく、NSOperationクラスを継承して利用することが想定しているクラスです。もし、簡単に使いたい場合はNSInvocationOperationとNSBlockOperationが用意されていますので、それらを利用してください。詳しくは以下のリンクからリファレンスを確認してください。
 

NSOperationのステータスプロパティ

1

NSOperationは以下のステータスを表すプロパティを持っています。
  • isReady
  • isExecuting
  • isCancelled
  • isFinished
これらはreadonly属性を持っていますが、必要に応じて継承する側でoverrideすることが可能です。ただし、これらのプロパティはKVOで監視対象になっているため、overrideする際にはKVO通知をするようにしてください。

 

startとmain

NSOperationを継承すると必ずoverrideしなければならないのが、以下のstartメソッドとmainメソッドです。どちらもNSOperationクラスがNSOperationQueueからタスクが実行される際に呼び出されます。ただし、以下のような違いがあります。
  • – start:メソッド終了後、「isFinished」ステータスが「YES」にならない。
  • – main:メソッド終了後、「isFinished」ステータスが「YES」になる。「start」メソッドがoverrideされてない場合のみ、Callbackされる。
mainメソッドは「isFinished」をフレームワークが管理するので、タスクの終了はmainメソッドの終了と一致します。startメソッドは「isFinished」にならないため、startメソッドが終了してもタスクは終了しません。(要するにNSOperationQueueに残っていてタスクを継続している意味になります。)startメソッドを終了するためには終了したいタイミングでプログラマが「isFinished」をYESにする必要があります。そして、ステータス変更後は必ずKVO通知を行い、ステータスが変更されてことをフレームワークに知らせる必要があります。

上の特徴からmainメソッドは一連の流れをタスク化する際に便利です。そして、startは不特定のイベントに応じてタスクを管理する際に便利なメソッドです。

※ 実際にはmainでもステータスをoverrideし、ステータスの管理を直接行えるのですが、そのような使用を想定されているメソッドではないですので、mainメソッドでステータスを直接管理することは控えましょう。

 

isReadyとdependencies

isReadyとdependenciesは両方ともタスクのstartやmainをフレームワークが呼び出していいかどうかを判断するためのメソッドになります。そして、isReadyがoverrideされている場合、dependenciesは無視されますので、使用する際にはどちらかのみ利用してください。

 

Viewを管理するViewOperationの作成

NSOperationに関して説明しましたので、本題のViewをどう管理するか説明します。NSOperationはタスクを定義するクラスです。そして、Viewを表示したり閉じたりすることもタスクですので、NSOperationを理解すればViewを管理するのもそれほど難しいことではないです。

ここではViewOperationというNSOperationを継承したクラスを作成します。

まず、ViewのLifecycleをNSOperationのLifecycleと一緒に合わせて、NSOperationが実行されるとViewが表示されれ、NSOperationを終了するとViewも閉じるようにと設定しましょう。Viewの表示と終了は連続した処理の中ではなく大半の場合はユーザーによるイベントに応じてそのViewが閉じられることになるでしょう。そのため、NSOperationのmainメソッドより、終了を直接管理するstartメソッドを利用すべきです。

 
@implementation ViewOperation

- (void)start {
    if (self.isCancelled) {
        [self setFinished:YES];
        return;
    }

    // startメソッドをoverrideしているため、mainメソッドは直接呼ばれないです。
    dispatch_async(dispatch_get_main_queue(), ^{
        // Viewの表示はmain threadにて実行する必要があるため
        [self main];
    });
}

- (void)main {
    ViewController *viewController = ...... self.viewController = viewController;
    // baseViewControllerはViewOpeartionのinitメソッドもしくはカスタムメソッドにて事前に設定するようにしましょう。
    [baseViewController presentViewController:viewController animate:YES completion:nil];

    // ここでは直接ViewControllerを生成していますが、ストーリーボードのSegueを実装することもできます。
    // そして、ViewControllerを外部から生成して設定することも可能です。

    // startメソッドがoverrideされているため、isFinishedはNOのままmainメソッドが終了されます。
}

// closeメソッドはカスタムメソッドであり
- (void)close {
    if (![[NSThread currentThread] isMainThread]) {
        dispatch_async(dispatch_get_main_queue(), ^{
            [self close];
        });
        return;
    }
    if (self.viewController) {
        [self.viewController dismissViewControllerAnimated:YES completion:^{
            [self setFinished:YES];
            self.viewController = nil;
        }];
    }
}

- (void)setFinished:(BOOL)finished {
    [self willChangeValueForKey:@"isFinished"]; # isFinishedが変更されることを通知
    finished = YES;
    [self didChangeValueForKey:@"isFinished"]; # isFinishedが変更されたことを通知 
}

- (BOOL)isFinished {
    return finished;
}

@end
これでViewの表示と終了をViewOperationのLifeCycleと合わせました。このViewOperationをNSOperationQueueに追加してみましょう。
...
    // Viewを表示したいイベントが発生した時
    ViewOperation *operation = [[ViewOperation alloc] initWithBaseViewController:baseViewController];
    [self.operationQueue addOperation:operation];
...
こうすることでViewControllerが生成され表示されます。そして、ユーザーの操作に合わせてViewOperationのcloseメソッドを呼ぶとViewが閉じられます。

 

NSOperationQueueの制御

ViewOperationはViewControllerとLifecycleを合わせました。これからViewOperationを利用して複数のView同士の制御をしてみましょう。

 

maxConcurrentOperationCountを利用

maxConcurrentOperationCountはNSOperationQueueに追加されているOperationたちを同時に動かせる最大数を設定することができます。このプロパティを1にすることでViewOperationをいくら追加しても同時に表示するのは1つのみであり、表示中のViewを閉じると順次に次のViewが表示されるでしょう。

dependenciesを利用

もし、NSOperationQueueに複数のViewOperationを追加し、条件により表示するViewの順番が異なる場合はNSOperationのdependenciesを利用しましょう。dependenciesに設定されているNSOperationが終了されるまでNSOperationは実行されないです。ですので、条件に応じてdependenciesを制御することで実行順番を変えることは可能です。ただし、一つのNSOperationが複数のNSOperationのdependenciesに追加することはできないです。その場合は「NSInvalidArgumentException」が発生します。

isReadyを利用

もし、View Aが終了された際にView B, C, Dを表示したい場合だと上のdependenciesでは制御できないです。その際にはNSOperationのisReadyプロパティをoverrideしましょう。isReadyプロパティで「NO」を返すうちはNSOperationQueueから該当のNSOperationが実行される(startもしくはmainメソッドが呼び出される)ことはないです。そして、isReadyのステータスを変える場合は、KVO通知をし変わったことを親のクラスに通知することを忘れないようにしましょう。

 

まとめ

NSOperationを利用して、Viewを管理する方法を説明しました。UIAlertControllerの制御やOSの権限アラート及び通知に応じる各種アラートの制御をNSOperationで行ってみるのはいかがでしょうか。そして、NSOperationは名前通りに特定のタスクより普通のタスクも管理できるものであります。その他のタスクもNSOperationを利用して効率よく管理してみましょう。

 

次世代システム研究室では、アプリケーション開発や設計を行うアーキテクトを募集しています。アプリケーション開発者の方、次世代システム研究室にご興味を持って頂ける方がいらっしゃいましたら、ぜひ 募集職種一覧 からご応募をお願いします。

皆さんのご応募をお待ちしています。