2014年8月28日木曜日

iPadでsizeToFitを使う場合の注意

UILabelを使っている処理で、末尾の1文字が欠けるという現象が出た。
よくよく調べると、
・文字列中に半角文字が1文字だけある
・sizeToFitを使っている
・iPad(32/64bit共)のみである
であった。

更に調査した結果、興味深いことがわかった。
sizeToFitした結果のサイズを調べると、iPadのみ1ピクセル分少ないのだ。

 CGRect frame=label.frame;
 NSLog(@"元frame=%@",NSStringFromCGRect(frame));
 [label sizeToFit];
 frame=label.frame;
 NSLog(@"後frame=%@",NSStringFromCGRect(frame));

この後frameの.size.width値がiPhoneでの結果に比べiPadは1小さい値が返ってきている。

さらに、UILabelでは、文字表示必要幅に対して1ピクセル分でも足りないと、末尾1文字がまるごと欠けるしまうようである。

iOS7のバグ。iOS6以前ではどうかは不明。

なので、対策としては

if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
  // iPadのみ
  CGRect frame=label.frame;
  frame.size.width=frame.size.width+1; // +1ピクセル
  label.frame=frame;
}

とすればよい。

2014年8月8日金曜日

NSString initWithStringの罠

オブジェクトで配布されているライブラリの中で落ちるバグに遭遇した。
落ちるときのログから判断するに、initWithString:nilしている部分があるらしい。
initWithStringはnilを渡してはいけない。

普通ならソースのないライブラリ内のバグなんてどうしようもないところだが、
Objective-Cには動的にメソッドを入れ替えるという荒業があるため、
なんとかなるかもしれない、と思った。
そう、NSString initWithString:を差し替えて、引数にnilが来たら、対応処理を入れればよい。
これをSwizzlingという。

処理の入れ替えは以下のように書く。

Method method1 = class_getInstanceMethod(元のクラス,@selector(元のメソッド名));
Method method2 = class_getInstanceMethod(入れ替えるクラス,@selector(そのメソッド名));
method_exchangeImplementations(method1, method2);

例えば、NSString initWithString:を差し替えるなら、NSStringのカテゴリを作成し、
(思い出し書きなので、完全に正しい保証なし。)

NSString+debug.h

#import <objc/runtime.h> // これが重要

@interface NSString (debug)
+(void)debug;
@end

NSString+debug.m

@implement NSString (debug)

-(NSString *)initWithString2:(NSString *)str
{
 ~デバッグ用コード~
  // 元の処理を呼び出す。
  // 再帰呼び出しのように見えるが、実はこれで元の処理が呼び出される。
  // [self initWithString:str]と書くと再帰呼び出しになってしまう
  return [self initWithString2:str];}

+(void)debug
{
 Method method1 = class_getInstanceMethod([self class],@selector(initWithString:));
 Method method2 = class_getInstanceMethod([self class],@selector(initWithString2:));
 method_exchangeImplementations(method1, method2);

}

@end

で、まずどこかで[NSString debug];と呼び出しておけば、後は入れ替わる・・・はずである。

が、結果から言えば、initWithStringは入れ替わらない。
試しに、stringWithStringを差し替えてみようと、

+(NSString *)stringWithString2:(NSString *)str
{
 ~
  // 元の処理を呼び出す。
  return [[self class] stringWithString2:str];}

+(void)debug
{
 Method method1 = class_getClassMethod([self class],@selector(stringWithString:));
 Method method2 = class_getClassMethod([self class],@selector(stringWithString2:));
 method_exchangeImplementations(method1, method2);
}

とすると、うまく入れ替わる。
initWithStringはインスタンスメソッドに対し、stringWithStringはクラスメソッドなので、使っている関数が違う。

差し替える前にデバッグ出力を追うと以下のように表示されていた。

initWithString:nilの時
Foundation             0x02d99f4a -[NSPlaceholderString initWithString:] + 99

stringWithString:nil
Foundation             0x02d9af4a -[NSPlaceholderString initWithString:] + 99
Foundation             0x02d9aec6 +[NSString stringWithString:] + 67


ここから察するに、NSString initWithString:は実はNSPlaceholderString initWithString:というメソッドに置き換えられていて実体が存在しない。stringWithString:は実体があって、その中でNSPlaceholderString initWithString:を呼び出している、ようである。

実体がないメッソドは置き換えられない。故に、NSString initWithStringは置き換わってくれない。

じゃあ、そのNSPlaceholderString initWithStringを置き換えればどうかと思うかもしれないが、困ったことにこいつは非公開クラスなので直接アクセスが出来ない=Swizzling出来ない。

ってなわけで、動的デバッグは出来なかったのであった。
Swizzlingの勉強にはなったのでいいけど。
一応ライブラリの作成先に連絡を入れるつもりだけど、果たして直してくれるかどうか。

→その後の調査の結果、ライブラリのバグではなく、ライブラリに必要なファイルが足りてなかったからと判明。しかし、それはそれで「ファイルがないときはエラーを出す」処理が抜けているという問題ありコードだと思うのだが。エラー処理がいかにちゃんと実装されているかが、ライブラリの完成度の尺度。