2016年8月24日水曜日

UIPageViewContollerについて

ページングするインタフェイスを作るためにUIPageViewContollerが用意されており、UIScrollViewで作りこむよりもページ切り替えの実装部分が不要なため簡潔に作れます。
しかし、TransitionStyleの初期値は"Page Curl"となっており、動作もこれに最適化されているようです。
"Scroll"も用意されていますが、この場合はちょっと思うようにいかない、カスタマイズが面倒、ということがあったのでまとめておきます。

1.UIPageViewContollerの部品構成


UIPageViewContoller
 ┗ _UIPageViewControllerContentView
  ┗ _UIQueuingScrollView
    ┗ UIPageControl

UIViewControllerのviewのサブビューにUIScrollViewとUIPageControlがある。(実際はその間にUIViewが配置されている。)

UIPageControlは、TransitionStyleが”Scroll”で、UIPageViewControllerDataSourceの次の二つのメソッドが実装されている時に追加される。
TransitionStyleの初期値は"Page Curl"なので注意。

- presentationCountForPageViewController:
- presentationIndexForPageViewController:

UIPageControlを使用しない場合はUIScrollViewは画面サイズと同じ。NavigationBarがある場合も画面サイズとなり、NavigationBarの下に隠れる部分ができる。

UIPageControlを使用する場合は画面下にUIPageControlのエリアが取られ、UIScrollViewはそれを除いたサイズになる。

2.最初のページの表示

UIPageViewContollerはスクロールに応じて前後の画面を要求するメソッドを呼び出すが、最初の画面はこれとは別に設定する必要がある。

- (void)viewDidLoad
{
    [super viewDidLoad];
    [self setViewControllers:@[someViewController]
                   direction:UIPageViewControllerNavigationDirectionForward
                    animated:NO
                  completion:nil];
}

3.ページ切替

UIPageViewContollerが表示する各ページにUIViewContollerを割り当てる。各ページには、そのviewが表示される。
各ページに異なるUIViewContollerのサブクラスを割り当てても良いが、例えば画像だけを入れ替えるような場合は同じ構成のUIViewContollerのサブクラスの複数インスタンスを割り当てたい。

AppleのPageContolサンプルで使われている方法を応用して、次のような実装をしてみた。

@interface MyPageViewController : UIPageViewController
                 <UIPageViewControllerDataSource>
//ViewControllerをキャッシュするArray
@property (nonatomic, strong) NSMutableArray *viewControllersCache;
@property (nonatomic) int numPages;
@end

@implementation MyPageViewController

- (void)viewDidLoad
{
    [super viewDidLoad];
    self.dataSource = self;
    //NSMutableArrayを作り、ページ数分のMyContentViewControllerオブジェクトをセットする。実際の画像セットはMyContentViewController表示時に行う。
    self.viewControllersCache = [[NSMutableArray alloc] init];
    //Photoライブラリの画像をセットするなど
    for (int n = 0; n < 3; n++) {
        self.numPages++;
//storyboardでUIViewContollerを追加し、classをMyContentViewController、Storuboard IDを"pageContent"に設定しておく。
        MyContentViewController *vc = [[self storyboard] instantiateViewControllerWithIdentifier:@"pageContent"];
        vc.pageNumber = n;
        [self.viewControllersCache addObject:vc];
    }
    [self setViewControllers:@[self.viewControllersCache.firstObject]
                   direction:UIPageViewControllerNavigationDirectionForward
                    animated:NO
                  completion:nil];
}

//今表示されているviewControllerの次のviewControllerを返す。
- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController
       viewControllerAfterViewController:(NLInfoContentViewController *)viewController
{
    if (viewController.pageNumber >= self,numPages - 1) return nil;
    return [self.viewControllersCache objectAtIndex:viewController.pageNumber + 1];
}

//今表示されているviewControllerの前のviewControllerを返す。
- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController
      viewControllerBeforeViewController:(NLInfoContentViewController *)viewController
{
    if (viewController.pageNumber <= 0) return nil;
    return [self.viewControllersCache objectAtIndex:viewController.pageNumber - 1];
}
@end

//UIImageViewを表示するViewController。
@interface MyContentViewController : UIViewController
//MyPageViewContrllerでインスタンス作成時にページ番号をセット
@property (nonatomic) int pageNumber;
//InterfaceBuilderでMyContentViewController内にUIImageを追加、コネクトする。
@property (nonatomic, weak) IBOutlet UIImageView * imageView;
@end

@implementation MyContentViewController
//viewWillAppearでUIImageを作る。
- (void)viewDidLayoutSubviews
{
    [super viewDidLayoutSubviews];
    if (self.imageView.image == nil)  {
        [self setImage];
    }
}

- (void)setImage
{
        NSString *imgName = [NSString stringWithFormat:@"img%d", self.pageNumber];
self.imageView.image = [UIImage imageNamed:imgName];
    //UIImageがself.imageViewのframeにリサイズされるので、必要に応じてself.imageViewのframeを変更する。
    //最初のページのサイズ調整を行うのはviewDidLayoutSubviewsのタイミングが良い。
}

@end

4.背景色設定

各ページの内容を表示するUIViewControllerのviewが1ページ分のエリアを覆うので、この背景色が優先される。これはInterfaceBuilderで設定可能。デフォルトはwhiteColor。

UIViewControllerの背景色をclearColorにした時はUIPageViewControllerのviewの背景色が有効になる。これはデフォルトでは未設定(透明)なので、見た目は黒になる。
この間にUIScrollViewがあるが、この背景色も未設定(透明)。

UIPageViewControllerのviewの属性はInterfaceBuilderでは設定できないので、コーディングで行う。

- (void)viewDidLoad
{
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor greenColor];
}

5.UIPageControlの表示

TransitionStyleを"Scroll"に設定し、次の二つのメソッドを実装する。

//実際のページ数(以上)の数を返す。
- (NSInteger)presentationCountForPageViewController:(UIPageViewController *)pageViewController
{
    return self.numPages;
}

//初期表示ページに対応するindexを返す。
- (NSInteger)presentationIndexForPageViewController:(UIPageViewController *)pageViewController
{
    return 0;
}

UIPageControlは画面下に表示され、各ページの表示領域はこれを除いた部分になる。
UIPageControlの背景色はclearColorで、その下のUIPageViewControllerの背景色(透明=)が表示される。

6.UIPageControlの属性変更(位置変更以外)

UIPageViewControllerが表示するUIPageControlの属性はInterfaceBuilderでは変更できない。
コーディングで変更する場合は次の要領で行う。

- (void)viewDidLoad
{
    [super viewDidLoad];
    [UIPageControl appearance].pageIndicatorTintColor = [UIColor grayColor];
    [UIPageControl appearance].currentPageIndicatorTintColor = [UIColor greenColor];
    [UIPageControl appearance].backgroundColor = [UIColor blueColor];
}

viewDidLoad以外にviewWillAppear, viewWillLayoutSubviewsなどでも有効だが、viewDidLayoutSubviewsでは遅すぎる。

7.UIScrollView、UIPageControlのframe変更

UIPageControlを表示するとUIScrollViewのサイズが小さくなる。フルサイズ表示したい場合はリサイズが必要になる。
いささか無理やりだが、UIPageViewControllerのサブビュー階層をたどってUIScrollViewオブジェクトを探し、そのframeを変更することでリサイズすることができる。
UIPageControlの位置変更も同じ要領で可能。
リサイズが有効なのはviewDidLayoutSubviewsのタイミング。それ以前にframeを設定しても、サブビューのレイアウト時にUIPageControlが再計算してしまう。

- (void)viewDidLayoutSubviews
{
    [super viewDidLayoutSubviews];

    UIScrollView *sv = [self findViewOfClass:[UIScrollView class] fromView:self.view];
    sv.frame = self.view.frame;
    
    UIPageControl *pc = [self findViewOfClass:[UIPageControl class] fromView:self.view];
    pc.frame = CGRectMake(0, 100, pc.frame.size.width, pc.frame.size.height);
}

特定のクラス(またはそのサブクラス)のインスタンスを探すメソッド

- (id)findViewOfClass:(Class)class fromView:(UIView *)view
{
    if ([view isKindOfClass:class]) return view;
    for(UIView *subview in view.subviews) {
        if ([subview isKindOfClass:class]) {
            return subview;
        }
        id v = [self findViewOfClass:class fromView:subview];
        if (v) return v;
    }
    return nil;
}

8.UIPageViewContollerの縦または横固定

UIPageViewContollerDelegateに次のメソッドがあるが、これを実装しただけでは縦・横固定にならない。

- pageViewControllerPreferredInterfaceOrientationForPresentation:
- pageViewControllerSupportedInterfaceOrientations:

UIPageViewContollerを表示するUINavigationControlに次のようなメソッドを追加する。

- (UIInterfaceOrientationMask)supportedInterfaceOrientations
{
    return UIInterfaceOrientationMaskPortrait;
}

UINavigationControlがいくつかのViewControllerを扱う場合は、次のようにするとViewController別に縦・横の扱いを変えることができる。

- (UIInterfaceOrientationMask)supportedInterfaceOrientations
{
    return [self.visibleViewController supportedInterfaceOrientations];
}

2016年8月11日木曜日

UITableViewのヘッダに大文字+小文字混じりの英字を表示する方法

UITableViewのヘッダにテキストを設定する場合は
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
で文字列を返します。

このままだと、英字部分が全て大文字に変換されます。
iOSがIOSとなるなど、変換して欲しくない、という場合がありえます。

この場合、この後で呼ばれる
- (void)tableView:(UITableView *)tableView willDisplayHeaderView:(UIView *)view forSection:(NSInteger)section
で文字列を再設定すると、大文字+小文字混じりの表示も可能になります。

ヘッダ部分の表示位置はtitleForHeaderInSectionが返す文字列から計算されるので、willDisplayHeaderViewで再設定する場合も、ここで適切な文字列を返しておく必要があります。

PHPhotoLibrayのrequestAuthorizationでプロンプトが表示されない

PHPhotoLibrayの

+ (void)requestAuthorization:(void (^)(PHAuthorizationStatus status))handler
でアクセス許可を要求するプロンプトが表示されない。

どうやらiOS9から発生しているようです。

Info.plistにCFBundleDisplayNameを設定すれば表示されるようになると書かれているページが見つかりますが、これでもダメ。

正式ドキュメントではないようでが、一旦ユーザが設定した後はプロンプトを表示しないとのこと。

別のことで、1日一回だけチェックするので、テスト時に時計を進めろ、というものがあったので、試しに時計を進めてみたらプロンプトが表示されるようになった。

デバッグ中の場合の手順

  1. XCodeでのデバッグを終了させる。
  2. 設定で、対象アプリの写真へのアクセスをオフにする。
  3. 対象アプリをアンインストール(削除)する。
    時計を進めるだけではダメ。再検証していないが、日付を変えた後でアンインストールしてもうまくいかなった。
  4. 設定 > 一般 > 日付と時刻 で日付を1日進める。
    自動設定をオフにす。
    一旦設定すると設定した日時が記録されるようで、何度もテストする場合はその度にさらに1日進める必要がある。
  5. XCodeでデバッグを開始する。


requestAuthorization実行で写真へのアクセスを要求するプロンプトが表示される。

このプロンプトは非同期で実行され、プロンプトを閉じたことがわからないので、アプリ側で何らかの再実行手段を講じる。
もしかしたら、何らかの通知機能があるかもしれない。

プロンプトでOKをタップした場合は正常処理を進めることができるが、キャンセルがタップされた場合は写真へのアクセスが禁止されたままの状態となる。この状態で再度requestAuthorizationを実行しても(おそらく24時間経過するまでは)プロンプトは再度表示されない。そのため、設定で写真へのアクセスを許可するように、ユーザに知らせる必要がある。

また、iOS10からinfo.plistにNSPhotoLibraryUsageDescriptionを設定しておかないとエラーが発生するので、これも設定しておく。