f:id:eimei23:20130324113849p:plain

UIPopoverControllerはiPadアプリではよく使われるものですが、結構あつかいにくいものだと思います。考えてプログラミングしないとメモリリークにつながります。まずは簡単なサンプルから説明して、問題点とその解決方法を説明していきたいと思います。

Popoverを表示する

以下のサンプルをダウンロードして下さい。

https://github.com/stack3/STPopoverControllerSample

起動してPopover Sampleを選択。Popoverボタンを押すとPopoverが表示されます。

f:id:eimei23:20130324114100p:plain

Popoverの外側、もしくはPopoverのCloseボタンを押すとPopoverが閉じます。

プログラムを見てみましょう。STPopoverSampleViewController.mのdidTapPopoverButtonでPopoverボタンを押した時のイベントを処理します。

STPopoverSampleViewController.m

- (void)didTapPopoverButton
{
    STPopoverSampleContentViewController *contentViewController = [[STPopoverSampleContentViewController alloc] init];
    contentViewController.delegate = self;
    
    _popoverController = [[UIPopoverController alloc] initWithContentViewController:contentViewController];
    _popoverController.delegate = self;
    [_popoverController presentPopoverFromRect:_popoverButton.frame inView:self.view permittedArrowDirections:UIPopoverArrowDirectionAny animated:YES];
}

処理の流れは以下のとおり

  • STPopoverSampleContentViewControllerがPopoverの中身に表示されるUIViewController
  • UIPopoverController#initWithContentViewControllerにそれを渡します
  • UIPopoverController#presentPopoverFromRect:inView:permittedArrowDirections:animatedでPopoverを表示します

注意すべきなのはUIPopoverControllerのオブジェクトをメンバ変数で保持する必要があることです。ローカル変数ですとメソッドのスコープを抜けたらオブジェクトが解放されてしまいPopoverが表示されません。

このオブジェクトを保持しないといけないところがメモリリークに繋がる原因になるのですが・・・

Popoverのサイズについて

Popoverのサイズは中身となるUIViewController(UIPopoverController#contentViewController)のcontentSizeInPopoverプロパティで決まります。今回の場合は、STPopoverSampleContentViewControllerのinitメソッドで設定しています。

self.contentSizeForViewInPopover = CGSizeMake(320, 280);

ここで設定した値がPopoverの中身のサイズになります。Popoverの枠を含めたサイズではありません。

ちなみにUIPopoverControllerにもPopoverの中身のサイズを指定するプロパティ(popoverContentSize)があります。しかし、こちらは基本的には使わないほうが良さそうです。UINavigationControllerが中身になるとき適切なサイズにならないことがあります。また中身となるUIViewController自身がcontentSizeInPopoverによって適切なサイズを決定する方が自然なように思います。

UIPopoverControllerオブジェクトの解放

Popoverが非表示になった時、UIPopoverControllerを解放しないといけません。このサンプルの場合は、STPopoverSampleViewControllerのメンバ変数_popoverControllerをnilにする必要があります。

先ほど説明したようにPopoverが非表示になるタイミングは2通りあります。

  • Popoverの外側をタップした場合
  • PopoverのCloseボタンをタップした場合

Popoverの外側をタップして非表示になる場合

この場合は、UIPopoverControllerDelegateによって判断出来ます。よってPopoverを表示するViewControllerで、このDelegateを実装する必要があります。

STPopoverSampleViewController.h

@interface STPopoverSampleViewController : UIViewController<UIPopoverControllerDelegate, STPopoverContentViewControllerDelegate>

非表示になるタイミングでUIPopoverController#popoverControllerDidDismissPopover:が呼ばれるので、ここで_popoverControllerをnilにして解放します。

STPopoverSampleViewController.m

#pragma mark - UIPopoverControllerDelegate

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

PopoverのCloseボタンをタップして非表示にする場合

この場合は、プログラムでPopoverを非表示にする必要があります。これはUIPopoverController#dismissPopoverAnimated:を呼ぶことで実現できます。やっかいなのが、Popoverの中身であるUIViewControllerからUIPopoverControllerを参照する仕組みがないことです。よってdismissPopoverAnimatedを呼び出すためには工夫が必要です。

  • 中身となるUIViewControllerが独自にDelegateを宣言
  • Closeボタンが押されたらDelegateメソッドで通知する
  • Popoverを開いたUIViewControllerは、上記Delegateメソッドを実装
  • その中で、メンバー変数として保持しているUIPopoverControllerのdismissPopoverAnimatedを呼ぶ。

今回はこの方法で実装したものを説明します。先に言っておくと、良い方法だとは思いませんが、問題を示すためにまず説明します。

STPopoverSampleContentViewController.h

@interface STPopoverSampleContentViewController : UIViewController

@property (weak, nonatomic) id delegate;

@end

@protocol STPopoverContentViewControllerDelegate 

- (void)popoverContentViewControllerDidTapCloseButton:(STPopoverSampleContentViewController *)sender;

@end

Closeボタンを押した時にDelegateメソッドを呼びます。

STPopoverSampleContentViewController.m

- (void)didTapCloseButton
{
    [_delegate popoverContentViewControllerDidTapCloseButton:self];
}

次にSTPopoverSampleViewControllerで、このDelegateメソッドを実装。dismissPopoverAnimatedを呼び出し、_popoverControllerをnilにします。ここでnilにしても非表示のアニメは行われます。

STPopoverSampleViewController.m

#pragma mark - STPopoverContentViewControllerDelegate

- (void)popoverContentViewControllerDidTapCloseButton:(id)sender
{
    if (_popoverController) {
        [_popoverController dismissPopoverAnimated:YES];
        _popoverController = nil;
        
        NSLog(@"PopoverSampleViewController released popoverController.");
    }
}

これで然るべきタイミングでPopoverが非表示され、UIPopoverControllerオブジェクトも解放されます。しかし、さまざまな箇所でPopoverを表示したい時、随所でこの処理を書かなければいけないとなると大変です。

次回は、その辺の解決方法について説明したいと思います。

その2へつづく