前回の続き

サンプル: https://github.com/stack3/STPopoverControllerSample

前回の最後でUIPopoverController#dismissPopoverAnimated:の呼び出し、 UIPopoverControllerの解放周りの処理、それらを随所で書く必要があると大変なことを説明しました。

今回は、できるだけ一箇所に同じ処理をまとめて、かつ、メモリリークを避ける方法を説明したいと思います。まずUIPopoverControllerについての大前提。それは

「同時に2つ以上Popoverが表示されることはない」

ということです。

つまりUIPopoverControllerのオブジェクトは1箇所で管理すれば良いのです。Popoverを開くUIViewControllerごとに管理する必要はないはずです。

サンプルのPopover by AppContextを開いてください。Popoverボタンを押すとPopoverが表示されます。

f:id:eimei23:20130324151653p:plain

見た目は前回と同じですが実装が異なります。

STAppContextというクラスを用意して、このクラスでUIPopoverControllerオブジェクトを管理しています。

STAppContext.m

@implementation STAppContext {
    __strong UIPopoverController *_popoverController;
}

STAppContextはシングルトンで、アプリケーション内で1オブジェクトしか生成しません。

+ (STAppContext *)sharedAppContext
{
    static STAppContext *singleton = nil;
    if (singleton == nil) {
        singleton = [[STAppContext alloc] init];
    }
    return singleton;
}

Popoverを表示・非表示するメソッドを用意しています。

- (void)presentPopoverWithContentViewController:(UIViewController *)contentViewController fromRect:(CGRect)fromRect inView:(UIView *)inView permittedArrowDirections:(UIPopoverArrowDirection)permittedArrowDirections animated:(BOOL)animated
{
    if (_popoverController) {
        [_popoverController dismissPopoverAnimated:NO];
    }
    
    _popoverController = [[UIPopoverController alloc] initWithContentViewController:contentViewController];
    _popoverController.delegate = self;
    [_popoverController presentPopoverFromRect:fromRect inView:inView permittedArrowDirections:permittedArrowDirections animated:animated];
}

- (void)dismissPopoverAnimated:(BOOL)animated
{
    if (_popoverController) {
        [_popoverController dismissPopoverAnimated:YES];
        _popoverController = nil;
        
        NSLog(@"AppContext released popoverController.");
    }
}

dismissPopoverAnimated:でUIPopoverController#dismissPopoverAnimated:を呼び出し後、_popoverControllerをnilにしていることに注目。

UIPopoverControllerDelegateも実装します。上記のpresentPopoverWith***メソッドでUIPopoverController#delegateをself(STAppContext)にしていることに注目。

#pragma mark - UIPopoverControllerDelegate

- (void)popoverControllerDidDismissPopover:(UIPopoverController *)popoverController
{
    _popoverController = nil;
    
    NSLog(@"AppContext released popoverController.");
}

Popoverの外側をタップした時に呼ばれる、上記Delegateメソッドでも_popoverControllerをnilにして解放します。

あとはアプリケーション内で、

  • Popoverを表示するときは、STAppContext#presentPopoverWith***
  • Popoverを非表示のときはSTAppContext#dismissPopoverAnimated:

を使うことを徹底すれば良いわけです。同じ実装を何度も書かずにすみ、さらにメモリリークの問題も起きません。

以下はPopoverボタンをタップした時の実装です。

STPopoverByAppContextViewController.m

- (void)didTapPopoverButton
{
    STPopoverByAppContextContentViewController *contentViewController = [[STPopoverByAppContextContentViewController alloc] init];
    [[STAppContext sharedAppContext] presentPopoverWithContentViewController:contentViewController fromRect:_popoverButton.frame inView:self.view permittedArrowDirections:UIPopoverArrowDirectionAny animated:YES];
}

閉じるボタン

中身のUIViewControllerからUIPopoverControllerを参照できないので、UIPopoverController#dismissPopoverAnimated:を直接呼び出せない問題。前回の記事その1では、中身のUIViewControllerが独自にDelegateを宣言して、それを介してPopoverを開いたUIViewControllerにdismissPopoverAnimated:を呼ばせるようにしていました。

しかし今回の仕組みであれば、そのような面倒はありません。Popoverを閉じたいタイミングでSTAppContext#dismissPopoverAnimated:を呼び出せば良いからです。

今回、閉じるボタンは以下のように実装しています。

STPopoverByAppContextContentViewController.m

- (void)didTapCloseButton
{
    [[STAppContext sharedAppContext] dismissPopoverAnimated:YES];
}

他にもっと良いやり方があるかもしれませんが、とりあえず自分はこの方法で実装するようにしています。