Hatena::ブログ(Diary)

Kazzzの日記 このページをアンテナに追加 RSSフィード

2013-04-01

[][]コレクションリテラル記述方法に注意

#define COL_A @"columnA"
#define COL_B @"columnB"
#define COL_C @"columnC"
#define COL_D @"columnD"

NSArray* colums = @[COL_A,
                   COL_B,
                   COL_C
                   COL_D
                   ];

このようにNSArrayを定数で初期化した結果のダンプ

$0 = 0x0b49f510 <__NSArrayI 0xb49f510>(
columnA,
columnB,
columnC,
columnD
)

ではなく、

$0 = 0x0b49f510 <__NSArrayI 0xb49f510>(
columnA,
columnB,
columnCcolumnD
)

である

なんてことないCOL_CとCOL_Dに区切り文字がないだけなのだが、Objective-Cのヒアドキュメント(風)の文字列エラーを隠してしまい、序数2の配列に格納されるため、実行時バグとして表出する。

コンパイルエラーって大事だよねぇ。

2013-02-23

[][][] iOS6.0以降、GCDの'dispatch_release'はARC環境では使えない

以前GCD(Grand Central Dispatch)のエントリを書いた時に

dispatch_release(group)
グループは有限のリソースのため、使用が終わったら必ず解放する必要がある。(ARCの影響を受けないことに注意)

と書いたが、iOS 6.1のプロジェクトを新たに作成して以前のコードを取り込んだ所、以下のエラーコンパイルが通らない

'release' is unavailable: not available in automatic reference counting mode
ARC forbids explicit message send of 'release'

f:id:Kazzz:20130223121853p:image

どうやらiOS6.0以降扱いが変わったらしい。
今までエラーになっていなかったのはXcodeにて"iOS Deplyment Target"を5.xにしていたために、後方互換性が働いていたのだろう。

2013-01-11

[][]UITextFiledにInputFilter風のバリデーションメソッドを追加する

androidのEditTextクラスにはEditableインタフェースを経由して使うInputFilterインタフェースが用意されており、これを使って入力文字列のフィルタやバリデーションに使用することが出来た。

[android]EditableとInputFilter

iOSの場合、文字列自体を置き換えるようなフィルタを実装する方法は見つからなかったが※、同様にバリデーションを実行することは出来そうなので、表題の通りInputFilter"風"のメソッドを実装してみよう。

UITextFieldの拡張が1番それっぽいのだが、継承クラスを書くとそのベースクラスに依存してしまうのと、同様に拡張したクラスをInterfaceBuilderでも使わなければならない等面倒なことが多いので、ここはObjective-Cのカテゴリを使うことにする。

まずはヘッダからだ。

UITextField+InputFilter.h
#import <UIKit/UIKit.h>

@interface UITextField (InputFilter) <UITextFieldDelegate>
@property (strong, nonatomic) NSCharacterSet* filterCharSet;
@property (strong, nonatomic) NSRegularExpression* filterRegExp;
@property (        nonatomic) int maxLength;
/**
 * キャラクタセットによるバリデーションを実行します
 */
- (BOOL)validateForCharSet:(NSString*) targetString;
/**
 * 正規表現メタキャラクタによるバリデーションを実行します
 */
- (BOOL)validateForRegExp:(NSString*) targetString;
/**
 * 最大文字長によるバリデーションを実行します
 */
- (BOOL)validateForMaxLength:(NSString*) targetString;
@end

UITextField+InputFilter.hはをUITextFieldを拡張するカテゴリだ。
バリデーション方法は用途に応じて三種類用意すれば充分だろうか。

    • NSCharacterSetにマッチするか
    • NSRegularExpression(正規表現)にマッチするか
    • 文字列の最大超を超えていないか

これらのバリデーションをUITextFieldのデリゲートである-textField:shouldChangeCharactersInRange:replacementString:の実行時に行うようにする。

それぞれのバリデーション方法の基準になるプロパティを持つが、カテゴリはインスタンスプロパティを持てないので、おなじみのAssociatedObjectを使ってプロパティを実装することにする。

UITextField+InputFilter.m
#import "UITextField+InputFilter.h"
#import <objc/runtime.h>

#define TagKey_FilterCharSet "InputFilter_FilterCharSet"
#define TagKey_FilterRegExp "InputFilter_FilterRegExp"
#define TagKey_MaxLength "InputFilter_MaxLength"

@implementation UITextField (InputFilter)
@dynamic filterCharSet;
@dynamic filterRegExp;
@dynamic maxLength;

#pragma mark - UITextFieldDelegate デリゲート
#pragma mark -
- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string
{
    return [self validateForCharSet:string]
        && [self validateForRegExp:string]
        && [self validateForMaxLength:string] ;
}

#pragma mark - バリデーションメソッド
#pragma mark -
- (BOOL)validateForCharSet:(NSString*) targetString
{
    if ( self.filterCharSet )
    {
        return ![targetString rangeOfCharacterFromSet:self.filterCharSet].location == NSNotFound;
    }
    else
    {
        return YES;
    }
}
- (BOOL)validateForRegExp:(NSString*) targetString
{
    if ( self.filterRegExp )
    {
        return [self.filterRegExp numberOfMatchesInString:targetString options:0 range:NSMakeRange(0, [targetString length])] > 0;
    }
    else
    {
        return YES;
    }
}
- (BOOL)validateForMaxLength:(NSString*) targetString
{
    if ( self.maxLength != 0 )
    {
        return self.text.length +  targetString.length <= self.maxLength;
    }
    else
    {
        return YES;
    }
}

#pragma mark - dynamic property accessor
#pragma mark -
- (NSCharacterSet*)filterCharSet
{
    NSCharacterSet* charSet =  (NSCharacterSet*)(objc_getAssociatedObject(self, TagKey_FilterCharSet));
    return charSet;
}
-(void)setFilterCharSet:(NSCharacterSet *)filterCharSet
{
    objc_setAssociatedObject(self, TagKey_FilterCharSet, filterCharSet, OBJC_ASSOCIATION_RETAIN_NONATOMIC );    
}
- (NSRegularExpression*)filterRegExp
{
    NSRegularExpression* regExp =  (NSRegularExpression*)(objc_getAssociatedObject(self, TagKey_FilterRegExp));
    return regExp;
}
-(void)setFilterRegExp:(NSRegularExpression *)filterRegExp
{
    objc_setAssociatedObject(self, TagKey_FilterRegExp, filterRegExp, OBJC_ASSOCIATION_RETAIN_NONATOMIC );
}
- (int)maxLength
{
    NSNumber* length =  (NSNumber*)(objc_getAssociatedObject(self, TagKey_MaxLength));
    return [length intValue];
}
-(void)setMaxLength:(int)maxLength
{
    objc_setAssociatedObject(self, TagKey_MaxLength, [[NSNumber alloc] initWithInt:maxLength], OBJC_ASSOCIATION_RETAIN_NONATOMIC );
}
@end

カテゴリで実装したので使い方は簡単だ。上記のヘッダをインクルードして、UITextFiledの生成後にバリデーションに使うプロパティとバリデーションを実施するデリゲートを設定するコードを書けば良い。

デリゲートは自身に設定することもできるが、その場合は三種のバリデーション結果の論理積が戻り値になる。

使用列 (とあるViewControllerにて)
    //バリデーションのための正規表現と最大文字長設定
    NSError* error = nil;
    textField1.filterRegExp = [NSRegularExpression regularExpressionWithPattern:@"^[a-zA-Z0-9]+$" options:0 error:&error]; //英数字のみ許可
    textField1.delegate = self;
    textField1.maxLength = 10; //最大10桁#pragma mark - UITextFieldDelegate デリゲート
#pragma mark -
- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string
{
    return [textField validateForRegExp:string] && [textField validateForMaxLength:string];
}

この例ではバリデーションに成功しないと入力文字がキャンセルされるだけだが、Alertを表示する等の処理を書きたい場合はデリゲートメソッド中から呼び出せば良いだろう。

2012-11-13

[]ブロックをプロパティとして扱う

Androidの無名クラス、WindowsPhoneのラムダ(デリゲート)に慣れているプログラマはiOSに転向すると最初はそのような抽象がなくて絶望し、その後iOS4以降はBlocksが使えることに歓喜する。

Blockは基本的にはファンクションへのポインタ型で実装されており(Delphiを思い出すな)一時変数やパラメタとして使うのが普通だが、ならばプロパティとしても使えるだろうと以下のように定義する。

BBButtonProxy.h
@interface BBButtonProxy : NSObject
  @property (strong, nonatomic) NSString *label;
  @property (strong, nonatomic) void (^action)(); //※1
〜

〜
{
    //OKボタンが押下された場合の処理
    BBButtonProxy* yesItem = [[BBButtonProxy alloc] init];
    yesItem.label = @"yes";
    yesItem.action = ^
    {
        [self clearOrders];
        [self.tableView reloadData];
   };
}

定義自体に問題はなく、動作もはするが、ブロックの中でselfを直接又は間接的に参照した瞬間に「循環参照 (Self Capture)」に陥り、互いのメモリが解放されずに残ってしまう。また、プロパティactionの属性もstrongなので、BBButtonProxyはブロック自体を保持する。

なのでプロパティactionはコピー属性(copy)として、ブロック中で参照されるselfは__weakで修飾した別な変数に代入したのちに使用する。

@interface BBButtonProxy : NSObject
  @property (strong, nonatomic) NSString *label;
  @property (copy, nonatomic) void (^action)();※2

    //OKボタンが押下された場合の処理
    __weak HogeController* weakRef =  self;
    BBButtonProxy* yesItem = [[BBButtonProxy alloc] init];
    yesItem.label = @"yes";
    yesItem.action = ^
    {
        [weakRef clearOrders];
        [weakRef.tableView reloadData];
    };

※1:typedefを使って予め型を定義しておくこともできるが、このように書くことでブロック名がプロパティ名になりObjective-Cぽくて好きだ。

※2:このように書くのが一般的らしいのだが、copy属性はリファレンスによると「copyオブジェクトを挿入できるオブジェクト型のみ有効」とある。Blockを扱う場合、Block_copyが動作していると思うのだが、まだ検証していないので懐疑的だ。

2012-11-12

[][]UINavigationController配下のビューの閉じ方

UINavigationControllerの制御下(遷移下)のUIViewControllerで処理が終了したのでビューを閉じようとしたのだが、全く反応が無い。

FooViewController.m
[self dismissViewControllerAnimated:YES completion:nil]; //全く反応無し

このFooViewControllerはUINavigationControllerの制御下(遷移下)にあるため、通常のモーダルビューと同じ方法では閉じることができないのだ。
正しい方法は

[self.navigationController popViewControllerAnimated:YES];

となる。遷移を一つポップするんだな。納得 (ドキュメントみろよ俺)

また、このようにUINavigationControllerの制御下(遷移下)にある場合は遷移のルートとなるビューに直接戻ることもできる。

[self.navigationController popToRootViewControllerAnimated:YES];

iOSのUINavigationControllerは、上手く使うとiPhoneらしいタップ数の少ない操作性の良いアプリケーションを書くことができるが、コーディングの前に、ビュー同士の関係とその遷移をはっきりと頭に描いていないと使えないので中々取っつきづらい。 (私などついついモーダルビューを多用してしまう)

2012-11-10

[][]UINavigationController:navigationItemの戻るボタンのテキスト

は遷移元のビューのUINavigationController:navigationItem.titleをセットしないと表示されない。

以前に紹介したように、タイトルをカスタマイズするために独自のラベル(UILabel)をUINavigationController:navigationItem.titleViewにセットしていても同じなので注意が必要である

HogeNavigationController.m (viewDidLoadなど)
    //ナビゲーションバーのタイトルにカスタマイズしたラベルをセット
    UILabel* label = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 400, 44)];
    label.backgroundColor = [UIColor clearColor];
    label.font = [UIFont boldSystemFontOfSize:17.0];
    label.shadowColor = [UIColor colorWithWhite:0.0 alpha:0.5];
    label.textAlignment = UITextAlignmentCenter;
    label.textColor =[UIColor whiteColor];
    label.text = @"タイトル";

    [self.navigationItem setTitle:@"タイトル]; //この行が無いと遷移先のビューの戻るボタンが変わらない
    self.navigationItem.titleView = label;

2012-11-09

[]定数式が好き

Objective-Cの冗長さを知っているプログラマは最近※導入された定数記法と定数式が皆大好きだろう。

ちょっとしたことだが、メソッドの引数パラメタvalueが文字列かリストか分からない場合にそれをパースするには

- (void)parseValue:(id)value
{
    id<NSFastEnumeration> array = [value conformsToProtocol:@protocol(NSFastEnumeration)]
         ? value
         : @[value];
         
    for (NSString* s in array)
    {
       //文字列に対して処理
    }
}

便利だ。

あと、定数式はそれ自体ちゃんとしたオブジェクトとして扱われるので

for (int i = 0; i < 100; i++)
{
    NSString* stringValue = [@(i) stringValue];
}

不思議に見えるけど、@(i) は NSNumberオブジェクトを生成する定数式なのでこの書き方もOK。
NSStringクラスの書式付きコンストラクタを使っても同じことはできるんだけど、

[[NSString alloc] initWithFormat:@"%i", i];

このコンベンショナルな書き方は打鍵数が倍になる。
Objective-Cのメソッドはとにかく長いのだ。 Xcodeが無ければとうに心が折れているだろう。

※since Xcode 4.4 (+ LLVM 4.0) Modern Objective-Cだからもう最近でもないか。