ステータスバーの表示/非表示の挙動 iOS6

たまにレイアウトがくずれたり、ナビゲーションバーがステータスバーの下に潜り込んだりするため、 いろいろなパターンで動作確認してみる

- (void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];

//    [[UIApplication sharedApplication] setStatusBarHidden:YES withAnimation:UIStatusBarAnimationNone];
}
- (void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];

//    [[UIApplication sharedApplication] setStatusBarHidden:YES withAnimation:UIStatusBarAnimationNone];
}

- (void)viewWillDisappear:(BOOL)animated
{
    [super viewWillDisappear:animated];

//    [[UIApplication sharedApplication] setStatusBarHidden:NO withAnimation:UIStatusBarAnimationNone];

}

- (void)viewDidDisappear:(BOOL)animated
{
    [super viewDidDisappear:animated];    
//    [[UIApplication sharedApplication] setStatusBarHidden:NO withAnimation:UIStatusBarAnimationNone];

}
no viewWillAppear viewDidAppear viewWillDisappear viewDidDisappear 呼びもと モーダル
1 × ×
2 × ×
3 ×
4 ×

1

他のViewControllerに迷惑をかけないように、viewDidAppearと、viewWillDisappearで行う

×モーダルビューのレイアウトもstatus barの領域が残ったまま

×モーダルの場合、呼びもとのviewWillAppearではstatus barが隠れた状態のサイズとなる。

2

×モーダルビューはレイアウトがstatus barの領域が残ったまま

×呼び出しもとは全画面レイアウトした後にstatus barが表示されるためヘッダーが刺さる

3

○モーダルビュー側は問題なし

×モーダルの場合、呼びもとのviewWillAppearではstatus barが隠れた状態のサイズとなる。

4

○モーダルビュー側は問題なし

×呼び出しもとは全画面レイアウトした後にstatus barが表示されるためヘッダーが刺さる

考察

viewのライフサイクル中にやるのは良くないのかな。

viewWillAppearで自分の領域は全画面と宣言をし、離れる場合(閉じるボタン謳歌など)はそのタイミングで、 もとに戻してあげるのが正しいのかな。

ただし、もともと全画面状態から遷移だったり、次の画面が全画面かどうかわからないため、ポリシーの問題でもあるきがする。

今回は、モーダルビューだったので、閉じる用のdelegateを呼ぶ箇所で、setStatusBarHidden:NOをしていすると想定通りの挙動となった。

- (void)closeView
{
    [[UIApplication sharedApplication] setStatusBarHidden:NO withAnimation:UIStatusBarAnimationNone];
    [self.delegate viewControllerDidClosed:self];
}

- (IBAction)tappedCloseButton:(id)sender
{
    [self closeView];
}

WebDBプレスにはじめて記事をかきました

UIButtonのイメージとテキストを両方表示する

Xcode5+Jenkins+CocoaPodsのビルド

CocoaPods導入メモ

すでにあるプロジェクトでcocoapodsを導入するにあたり、ハマったところがあるのでメモ。

ターゲット名にハイフンが入るとダメ?

テスト用のターゲットを"sample-app-test"みたいな名前にしていると、Podfileにtargetを指定した場合、ハイフンで区切られてエラーとなる

target :sample-app-test, :exclusive => false do
    pod 'GHUnitIOS'
    pod 'OCMock'
end

エラー内容はこんな感じ

### Error

```
Pod::DSLError - Invalid `Podfile` file: undefined local variable or method `app' for Podfile:Pod::Podfile
 #
 #  from xxxxxxxxxxxxx/Podfile:9
 #  -------------------------------------------
 #
 >  target :sample-app-test, :exclusive => false do
 #      pod 'GHUnitIOS'
 #  -------------------------------------------
 #

テスト用ターゲットにも適用

基本的にメインのターゲットをコピペすればOK

Build Settings

  • Framework Search Paths
    • "$(SYSTEM_APPS_DIR)/Xcode.app/Contents/Developer/Library/Frameworks"
  • Header Search Paths
    • ${PODS_HEADERS_SEARCH_PATHS}
  • User-Defined (値は省略)
    • PODS_BUILD_HEADERS_SEARCH_PATHS
    • PODS_HEADERS_SEARCH_PATHS
    • PODS_PUBLIC_HEADERS_SEARCH_PATHS
    • PODS_ROOT

Build Pheases

run scriptの追加

  • Check Pods Manifest.lock
  • Copy Pods Resources

iOS xibを分けない国際化

xibファイルのLocalize

xibファイルに指定したUILabelやUIButtonなどのタイトルの文字を日本語/英語表示したい場合、 xibファイル自体を分けるやり方もあるが、レイアウト等全く同じなのに文言のためだけに2ファイル作らないといけない。

プログラムで指定

またUILabelやUIButtonをコードと紐付けてプログラムで指定方法もあるがそれだけのためにヘッダーファイルやviewDidLoadに追記するのもめんどくさい

「User Defined Runtime Attributes」+ Category

xibエディター上では指定できないパラメータ等をキー値コーディングにて設定できる。
その指定したパラメータを基にCategoryで拡張したメソッドにてNSLocalizeStringを行う。
#import "UILabel+Localize.h"

@implementation UILabel (Localize)

- (void)setLocalizeKey:(NSString*)key
{
    self.text = NSLocalizedString(key, key);
}

@end

xibエディター


あとはLocalizable.stringsを準備すればOK

CoreDataのマイグレーションテスト

CoreDataを使う場合、Model(テーブル定義みたいなもの)を変更すると、 マイグレーションが走る。
例えば新しく追加されたカラムについてデフォルト値をどうするかなど。

それをGUIで設定できる反面、各モデルバージョンからのマージ内容を毎回作成しないといけないので、 必ず設定し忘れがある。

マージポリシーなど個別に実装すればよいのだが、ひとまずテストを書いて、 設定漏れがないようにする。

sqlitenの作成

dataに各モデルバージョンのsqliteを置いておく

Data1.sqlite (Version1)
Data2.sqlite (Version2)

CoreDataManager

CoreDataを扱うクラス

@interface CoreDataManager : NSObject
{
    NSManagedObjectModel *managedObjectModel;       
    NSPersistentStoreCoordinator *persistentStoreCoordinator;
}

@property (nonatomic, strong, readonly) NSManagedObjectModel   *managedObjectModel;
@property (nonatomic, strong, readonly) NSManagedObjectContext *managedObjectContext;
@property (nonatomic, strong, readonly) NSPersistentStoreCoordinator *persistentStoreCoordinator;

- (NSString*)storeDirectory; // 保存用ディレクトリ

Test

  1. テスト用の保存ディレクトリ作成
  2. テスト用のSQLのコピー
  3. マイグレーションテスト実行
  4. テスト用保存ディレクトリの削除 ※GHUnit + OCmockを使っています

テスト用の保存ディレクトリ作成を行う

テスト実行前にディレクトリの作成を行う

- (NSString*)testStoreDirectory
{
    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
    NSString *basePath = ([paths count] > 0) ? [paths objectAtIndex:0] : nil;
    return [basePath stringByAppendingPathComponent:@"core_data"];
}

- (void)setUpClass {
    // Run at start of all tests in the class

    // Store用ディレクトリ作成

    NSFileManager* fileManager = [NSFileManager defaultManager];

    NSString* storeDirectory = [self testStoreDirectory];
    if (![fileManager fileExistsAtPath:storeDirectory]) {
        NSError*    error;
        BOOL ret = [fileManager createDirectoryAtPath:storeDirectory
           withIntermediateDirectories:YES attributes:nil error:&error];
        GHAssertTrue(ret, @"create save dir");
    }
}

テスト用SQLコピー

ファイル名をもらってコピーするmethodを作成

- (void)copySQLiteFile:(NSString*)filename
{
    NSString* filePath = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:filename];

    NSString* storePath = [[self testStoreDirectory] stringByAppendingPathComponent:@"Data.sqlite"];

    NSFileManager* fileManager = [NSFileManager defaultManager];

    if ([fileManager fileExistsAtPath:storePath]) {
        [fileManager removeItemAtPath:storePath error:NULL];
    }

    NSError* error;
    BOOL ret = [fileManager copyItemAtPath:filePath toPath:storePath error:&error];
    if (!ret)
        GHTestLog(@"error:%@", [error localizedDescription]);
    GHAssertTrue(ret, @"copy file");
}

マイグレーションテスト実行

OCMockをつかって保存用ディレクトリを書き換えておく

- (void)testDoMigration
    NSArray* tests
    = @[
        @"Data1.sqlite",
        @"Data2.sqlite",
        ];

    for (NSString* sqliteFile in tests) {
        GHTestLog(@"sqlite file :%@", sqliteFile);

        id mock = [OCMockObject partialMockForObject:[[CoreDataManager alloc] init]];
        [[[mock stub] andReturn:[self testStoreDirectory]] storeDirectory];

        [self copySQLiteFile:sqliteFile];

        GHAssertNoThrow([mock doMigration], @"no throw");

        // マイグレーション後のデータチェックとか
    }
}

テスト用保存ディレクトリの削除

- (void)tearDownClass {
    // Run at end of all tests in the class

    // Store用ディレクトリ削除

    NSFileManager* fileManager = [NSFileManager defaultManager];

    NSString* storeDirectory = [self testStoreDirectory];
    if ([fileManager fileExistsAtPath:storeDirectory]) {
        NSError*    error;
        BOOL ret = [fileManager removeItemAtPath:storeDirectory error:&error];
        GHAssertTrue(ret, @"remove save dir");
    }
}

OCMockの使い方

使い方

mock objectの生成

クラスオブジェクトから生成

空(メソッドがない)のmockオブジェクト生成
mockは定義していないメソッドを呼ぶと例外が発生する

id mock = [OCMockObject makeForClass:[NSString class]];
定義していないメソッドを読んでも例外が発生しない
id mock = [OCMockObject niceMockForClass:[NSString class]];
インスタンスオブジェクトから生成
id aMock = [OCMockObject partialMockForObject:[NSString stringWithString:@"foo"]];
プロトコルモック
id mock = [OCMockObject mockForProtocol:@protocol(NSLocking)];
observerモック
id mock = [OCMockObject observerMock]

stubの作成

id mock = [OCMockObject mockForClass:[NSString class]];
[[mock stub] lowercaseString];

呼ばれたことを検証する

id mock = [OCMockObject mockForClass:[NSString class]];
[[mock expect] lowercaseString];

// [mock lowercaseString];

[mock verify]; // exception throw
呼ばれた順庵も検証
[mock setExpectationOrderMatters:YES];

[[mock expect] lowercaseString];
[[mock expect] uppercaseString];

戻り値を指定

任意のオブジェクトを指定
[[[mock stub] andRetugn:@"ret"] lowercaseString];
プリミティブ型

OCMOCK_VALUE(1)とやるとエラーになる

int expectedValue = 1;
[[[mock stub] andRetugnValue:OCMOCK_VALUE(expectedValue)] lenght];

引数の指定

固定オブジェクト
[[mock stub] hasSuffix:@"foo"];
任意のオブジェクト
[[mock stub] hasSuffix:[OCMArg any]];
[[mock stub] hasSuffix:OCMOCK_ANY];
可変長引数
[[mock stub] hasSuffix:@"foo"];
参照の指定
[[mock expect] completePathIntoString:[OCMArg setTo:expectedName] caseSensitive:YES 
                         matchesIntoArray:[OCMArg setTo:expectedArray] filterTypes:OCMOCK_ANY];
Nilチェック
[[mock expect] someMethod:[OCMArg isNil]]
[[mock expect] someMethod:[OCMArg isNotNil]]
オブジェクトチェック
NSString* aValue = @"bar";
[[mock expect] someMethod:[OCMArg isNotEqual:aValue]];

// selector
[[mock expect] someMethod:[OCMArg checkWithSelector:aSelector onObject:anObject]];

// block
[[mock stub] hasSuffix:[OCMArg checkWithBlock:^(id value) { return [value isEqualToString:@"foo"]; }]];

処理内容の指定

例外
NSException *exception = [NSException exceptionWithName:@"TestException" reason:@"test" userInfo:nil];
[[[mock expect] andThrow:exception] lowercaseString];

niceMockForClassにおける例外処理登録

id mock = [OCMockObject niceMockForClass:[NSString class]];

[[mock reject] uppercaseString];
通知
NSNotification *notification = [NSNotification notificationWithName:TestNotification object:self];
[[[mock stub] andPost:notification] lowercaseString];
特定の処理を呼ぶ
// selectorを使うやり方
// 
// 自分のクラスに定義済み
// - (NSString *)valueForString:(NSString *)aString andMask:(NSStringCompareOptions)mask
///
[[[mock stub] andCall:@selector(valueForString:andMask:) onObject:self] commonPrefixWithString:@"FOO" options:NSCaseInsensitiveSearch];


// ブロックを使うやり方
void (^theBlock)(NSInvocation *) = ^(NSInvocation *invocation) {
    NSString *value;
    [invocation getArgument:&value atIndex:2];
    value = [NSString stringWithFormat:@"MOCK %@", value];
    [invocation setReturnValue:&value];
};

[[[mock stub] andDo:theBlock] stringByAppendingString:[OCMArg any]];
オリジナルの処理を実行

ただしpartialMockForObjectで生成した場合のみ

[[[mock expect] andForwardToRealObject] method2];

できないこと

クラスメソッドの追加

UIButtonにimageもtextも縦に表示させる

UIButtonにimageとtextを設定すると、横に並べて表示される。 縦に表示したい場合は一手間が必要(だと思う)

何も指定せずに表示してみる

何も指定せずにimageと適すを表示すると、imageとtextを横に並べてセンタリングされる

余白調整

縦に表示するためにそれぞれの余白を調整する

  • imageの余白指定 : imageEdgeInsets
  • textの余白指定 : titleEdgeInsets

※計算するまえに文字列を設定しても問題ない

実装

今回はUIButtonのカテゴリにレイアウト調整する関数を追加

@implementation UIButton (Layout)

- (void)verticalLayout
{
    float buttonHeight = CGRectGetHeight(self.frame);
    float imageHeight  = CGRectGetHeight(self.imageView.frame);
    float textHeight   = CGRectGetHeight(self.titleLabel.frame);

    float verticalMergin = (buttonHeight - imageHeight - textHeight) / 2.0f;

    float buttonWidht = CGRectGetWidth(self.frame);
    float imageWidht  = CGRectGetWidth(self.imageView.frame);
    float textWidht   = CGRectGetWidth(self.titleLabel.frame);

    float imageHorizonMergin = (buttonWidht - imageWidht) / 2.0f;

    self.contentEdgeInsets = UIEdgeInsetsZero;
    self.imageEdgeInsets
        = UIEdgeInsetsMake(verticalMergin,
                           imageHorizonMergin,
                           verticalMergin + textHeight,
                           imageHorizonMergin - textWidht);
    self.titleEdgeInsets
        = UIEdgeInsetsMake(verticalMergin + imageHeight,
                           -imageWidht,
                           verticalMergin,
                           0);
}

ハマったところ

  • Interface Builderで設定する場合は@2xをファイル名につけると画像が拡大されて表示される
  • 文字列は自動でセンタリングされるが、imageはセンタリングされない

ReactNativeでAndroid対応する話

前提 ReactNativeでiOS版のアプリをリリースしていて、Android版をリリースする話 トラブルシューティング Build.VERSION_CODES.Q が存在しないエラー compileSdkVersionを29以上にすると解決 メモリー足りないエラー Execu...