shimada-kの日記

ソフトウェア・エンジニアのブログです

UIScrollViewにおけるcontentOffsetの注意点

UINavigationControllerを使用している場合で、UIScrollViewをpagingで使用している時にコードからUILabelを置こうとした時に想定通りの座標で配置できなかったことがあったので解決手段をメモしておきます。

既知のトピックではありますが、ちゃんとまとまった情報に出会えなくてかなりハマったので書いておきます。

これは2014年iOSアドベントカレンダー5日目の記事として書かれました。

環境

コード

#import "ViewController.h"

@interface ViewController ()
@property (weak, nonatomic) IBOutlet UIScrollView *scrollView;

@end

#define NUM_LABEL 5

@implementation ViewController

- (void)viewDidLayoutSubviews {
    NSLog(@"viewDidLayoutSubviews");
    [super viewDidLayoutSubviews];
    
    //スクロールの総範囲の設定
    NSInteger kScrollObjWidth = [[UIScreen mainScreen] applicationFrame].size.width;
    [self.scrollView setContentSize:CGSizeMake(NUM_LABEL * kScrollObjWidth, self.scrollView.frame.size.height)];
    // 謎に下にずれることがあるのでy座標を0にする
    NSLog(@"contetOffset (%f, %f)", self.scrollView.contentOffset.x, self.scrollView.contentOffset.y);
    self.scrollView.contentOffset = CGPointMake(self.scrollView.contentOffset.x, 0);
}

- (void)viewDidAppear:(BOOL)animated
{
    NSLog(@"viewDidAppear");
    [super viewDidAppear:animated];
    //[self showObjects];
}

- (void)viewDidLoad {
    NSLog(@"viewDidLoad");
    [super viewDidLoad];
    self.scrollView.contentOffset = CGPointMake(self.scrollView.contentOffset.x, 0);
    // Do any additional setup after loading the view, typically from a nib.
    [self showObjects];
}

- (void)showObjects{
    NSInteger kScrollObjWidth = [[UIScreen mainScreen] applicationFrame].size.width;
    NSInteger i;
    
    for(i = 0; i < NUM_LABEL; i++){
        UILabel *label = [[UILabel alloc] init];
        
        label.frame = CGRectMake(kScrollObjWidth * i, 0, kScrollObjWidth, 30);
        
        label.textAlignment = NSTextAlignmentCenter;
        label.text = [NSString stringWithFormat:@"%ld", (long)i];
        
        label.backgroundColor = [UIColor greenColor];
        
        [self.scrollView addSubview:label];
    }
}

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

@end

このコード。上記のようにshowObjectsをviewDidLoadで呼び出すと下記のようになります。その場合、呼び出す前にcontentOffsetの初期化をやっても同様です。

オレンジ色のものがUIScrollViewで、緑色のものがUILabelです。

f:id:shimada-k:20141203013948p:plain

また、viewDidLayoutSubviewsでcontentOffsetを初期化しない場合でも同じ結果になります。コードからではなくInterface Builderでオブジェクトを置いている場合でも同様です。

ちなみに上記の状態でもマウスか指でスクロールして何かしらの動きを与えると座標が0の状態(想定通りの状態)に戻ります。

発生理由と対策

発生理由はNavigationControllerVilew配下にScrollViewを置いた場合、ContentsOffset.yのデフォルト値が-64になっているからです。

f:id:shimada-k:20141203012446p:plain

-64というのはステータスバーの20ptとナビゲーションバーの44ptの合計値で、NavigationController配下にあった時UIScrollViewのコンテンツが隠れてしまわないようにという配慮です。これはUIScrollViewの仕様です*1

ちなみにナビゲーションバーが存在しない状態だとcontentsOffsetの初期値は(0.000000, -20.000000)とはならずに(0.000000, 0.000000)となります。

解決するには

  • UIScollViewが配置され(てい)るUIViewControllerのautomaticallyAdjustsScrollViewInsetsをNOにする

もしくは

  • UIScrollViewのcontentsOffsetをviewDidLayoutSubviewsで初期化した後にshowObjectsを呼ぶ

必要があります。例えばcontentsOffsetをviewDidLayoutSubviewsを初期化してshowObjectsをviewDidAppearで呼び出すようにするとscrollViewの左上からちゃんとUILabelが表示されます。

f:id:shimada-k:20141203014005p:plain

この件はUIScrollViewに対してConstrainsを設定していても発生します。またタップすると(0, 0)に戻るという点と、ナビゲーションバーが存在しない状況では発生しないという点が邪魔をして、解決に時間がかかりました。