2015年6月24日水曜日

Objective-CやiOSの隠れた仕様

X-BASIC for iOS v3.00の開発では、Objective-C、というかそのコンパイラの隠れた仕様に悩まされた部分があった。それを記録しておく。

(1)Objective-Cでの %ld の l判定はlong という宣言文字列と比較されているので実質ではない
何が言いたいのかというと、64ビットでlong != intで弾かれるのはいいとして、
32bitで実質 int == longでも「異なる」と判定されるということ。
X-BASICでは32ビット環境ではLong = longと定義していたのを、64ビット環境でLong = intと変更した。(LLong=long long;64bit)。32ビットのままでなければならない処理が山のようにあるからだ。ところが上記の仕様のため、従来%ldでいけたところをすべて%dにしなければいけなくなった。ところが%dにすると今度は32ビット環境下で警告を食らうハメになる。というわけで、結局都度(int)で型キャストするという手をとった。

(2)Objective-CのC型文字列の"~"はchar *であってsigned char *ではない。
これも警告で引っかかった。
確かにcharはコンパイルオプションでunsigned にもできるけど、でもそうでない設定の時はchar = signed charだろう?これはCの規約違反だと思うのだが。

 (3)定義済み定数__FUNCTION__ は char [n]と定義されるため、”%s”,__FUNCTION__では受けられるけど、func(char *mes)に対してfunc(__FUNCTION__)にすると警告が出る
 わけわからんかもしれないけど、やってみるとわかる。

 (4)NSUserDefaultで、setBoolしたものはintegerForKeyで読める
    [defaults setBool:NO        forKey:@"BOOL"];

    BOOL       ynb=[defaults    boolForKey:@"BOOL"];
    NSInteger yni=[defaults    integerForKey:@"BOOL”];
   どちらでも読めている。

まあ、普通はしないけど、バージョンアップで記録内容をBOOL範囲からNSInteger範囲にしなければならなかったので。

(5)array=[@"A\nB\n" componentsSeparatedByString:@"\x0a"]の結果は
[0]=@"A\n",[1]=@"B\n",[2]=@""となる。
最後に@""が入った要素ができるのが味噌。countで得られる値-1が行数である。

(6)Objective-C(というかそのライブラリ)の%sは日本語に対応できてない。
    NSString *mes=@"English日本語";
    char *cmes=[mes UTF8String];
    NSLog(@"元文字列=%@,char*=%s",mes,cmes);
    NSString *mes2=[NSString stringWithCString:cmes encoding:NSUTF8StringEncoding]; // もしくはmes2=@(cmes)だけでもOK
    NSLog(@"逆文字列=%@",mes2);

を実行すると、
元文字列=English日本語,char*=EnglishÊó•Êú¨Ë™û
逆文字列=English日本語
となる。要するに%sの表示だけがおかしい。NSLog()だけじゃなくてprintf()でも同じ。
日本語を含む文字列の表示には%sを使ってはいけないということ。
バグに近い仕様。

(7)NSStringのlength,substring*のメソッドはサロゲートペア文字を考慮していない
 サロゲートペア文字は常に2文字分の扱いをしないといけない
 サロゲートペアは見た目1文字だけど、内部では2文字の扱いになっているということ。
   length=2だし、substringのrangeもlength=2単位にしないといけない。
X-BASICではV3.10でNSStringをサロゲートペア対応にするためのカテゴリを作って対応した。


(8)NSLog(@"%@",文字列)の時、その文字列がサロゲートペアの最初の半分だった時、表示が全く出ない
これは(7)と絡むのだが、サロゲートペア文字は内部では2文字なので、その最初の1文字だけを持ってNSLog()で表示しようとすると、そのNSLog()すべてが表示されない。
NSLog(@"str=%@",[@"サロゲートペア文字" substringToIndex:1])とかすると、文字だけでなくstr=も含めて表示が出ない。
NSLog()入れてるはずなのに表示が出ないという時はこの可能性がある。

(9)これはObjective-Cではないけど、Zipアーカイブ内のファイル名は、それが作られた環境によって文字コードが異なる様子。
WindowsではSHIFT-JISで格納されている。ZipArchiveというライブラリではUTF8にしか対応していないため、Windows環境下で作られた日本語名ファイル含むZipを展開しようとすると、日本語名ファイルのみ抜けてしまう。
X-BASICではライブラリを修正して利用している。

(10)PNGファイルをiOSのリソースに入れると、ファイルが改変される。
ヘッダーの中にも情報が追加されてる。しかもその追加され方がPNGのフォーマット(規約)に合致していないので、たぶん、そのファイルを抜き出して画像ソフトに読ませても表示できない。
X-BASIC V3.10まででpngHeader()関数を内部にある画像に対して使うと正しい情報が得られないのはこのせい。

(11)iOSでPNGの透過を有効にするときは、256色にするか、インデックスカラーというものにしなければならない。普通にフルカラーで透過色を付けても透過しない。
インデックスカラーへの変換は、フリーのgimpかPhotoshopで出来る。

(12) NSTimerを実行させるにはNSRunLoopへ追加しなければならないが、これはメインスレッドで行わなければならない
NSTimer *tickTimer=[NSTimer timerWithTimeInterval:(NSTimeInterval)MML_1TICK_TIME
  target:self
selector:@selector(tickCountDown)
userInfo:nil
repeats:YES // 繰り返し
  ];

// 次がRunLoopへの追加だが、これはメインスレッドで実行しなければ有効にならない。

[[NSRunLoop currentRunLoop] addTimer:tickTimer forMode:NSDefaultRunLoopMode];

テストでは動いていたものが本番ではどうしても動かないので調べてみたらこれだった。
(X-BASIC'の言語処理は実はバックグラウンドで動いているのです。)
このメソッドはエラーを返さないのでわからなかった。

(13)複数の処理を並行動作させたいときはメインスレッドは使えない
メインスレッドは1つしかないから考えたらあたりまえのことなんだけど、

複数同時に走らせたい処理を
 [self performSelectorInBackground:@selector(fetch_main) withObject:nil];

[self performSelectorOnMainThread:@selector(fetch_main) withObject:nil waitUntilDone:NO];
に変更したらうまく動かなくなった(上記を複数回発行して複数本同時に走らせる)
バックグラウンドは同時に何本でも設定できる=並行動作するが、メインスレッドは1つしかないからである。メインスレッドでは、追加された順にキューに記録され、1つが終わると次のが走る。

iOS8とXcode6.3.2(一部7)のバグ

X-BASIC for iOS v3.00の開発に際して、iOS8のバグとかiOS7との挙動の違いをいくつか見つけたので記録しておく。Xcode6.3.2のバグも。

巷で情報がないもの。

(1)iOS8で、UITextViewでズームを繰り返すとシステムが反応しなくなることがある
iOS7.1では問題ない。なので、iOS8.1では頻発、8.2以上では頻度は減ったけどやはり出る。
UIScrollView上にUIImageViewを載せた場合は問題なさそう。
 回避策なし。


(2)iOS8.xで、UITextViewでズームした時、画面外に出た部分を表示できない
表示及びスクロール可能範囲が拡大前のそれと同じであり、またスクロールもできないし、表示も欠ける。
iOS7では問題ないが、iOS8は全てのバージョンで不可。他の設定が必要になったのかと思ったが、それらしいものはなかった。
 回避策なし。

UIScrollView上にUIImageViewを載せた場合は問題ない。
(1)(2)のせいで、X-BASICではテキストのズーム表示をiOS8上禁止した。iOS7では動く。


(3)UITextViewのscrollRangeToVisible:selectRangeで指定範囲までスクロールしようとした時、iOS8ではその設定のあとそれを発行するtextViewの内容を変更してもその場所に飛ぶが、iOS7では無効になる。

「例」
[txView  scrollRangeToVisible:selectRange];
txView.attributedText=〜
iOS8では修正後テキストの指定位置に移動するが、iOS7では移動しない。

先に修正すればOK。
txView.attributedText=〜
[txView  scrollRangeToVisible:selectRange];

 iOS7の動作もおかしいとはいえないが、なまじiOS8で動いてしまうだけにiOS7上での隠れバグになりそう。


(4)iOS8/iOS7とも、UITextView.contentOffsetに値を設定してもそこに飛ばないことがある。
 txView.contentOffset=offset
 としてもだめで、
 [txView setContentOffset:offset animated:YES];
とするといける。=offsetの後にsetNeedDisplayを発行しても、RunLoopに戻すようにしてもダメ。
animatedにすると時分割でoffsetを与え続けてくれるので動くのだと思う。
ということは、contentOffsetへの設定が無視されるタイミングがあるのだろう。

(5)contentSizeの値が正しくない
これはiOS7で散々騒がれて回避ロジックも編み出されたけど、iOS8でも治ってなかった。

ちなみに、UITextView内で行を追加した時、各行の表示位置はsizeWithAttributesで求められる文字列描画高さの累積に一致しない。このため、各行の表示位置を正確に知る方法がない(それがX-BASICでスクロール同期がうまくいかない理由)。


(6)Xcode6.3.2のバグ;IB上でのUndo
以下の手順で操作すると、おかしくなる。
1. IB上で何かUI要素を乗せて実行して動作を見る。
 IBActionでの接続もしてたらよりわかりやすい
2. その要素を削除して実行
3. Undoで要素を戻す
 IBActionでの接続も戻っている
これで元(1.の段階のもの)に戻るように思うが、実際には戻らない。UI要素が表示されない。表面上どこにも問題ないのに戻らないのでしばらく調査したが、どうもXcodeのバグ臭い。要素を一旦削除し、再度乗せたらうまく行った。
IB上で、実行を挟んだUndoは気をつけろ、ということ。

(7)Xcodeのバグ;iPhone6シミュレーターで日本語にならない
ここに情報があった
簡単になったのかそうでないのかわからないところ。でもシミュレーター上でも切り替えられるようにするのが筋。

(8)Xcodeのバグ;Breakポイントが違う場所に付く
 break文にBreakポイントをつけると、実行時にはbreakして飛んだ先に設定されてしまう。
 breakを通過したあとで止まるならいいが、breakを通らない時も止まってしまうので、デバッグがしにくくなる。
「例」
switch (n) {
 case 1:
   break; <- ここにBreakポイントを設定しても
}
<-ここで止まってしまう。n=1でなくても。

Xcode7でも治っていない。Xcodeでブレイクポイントはあまり使いものにならない状態。
バックグラウンドで動くタスクの場合だけかもしれないが。

(9)シミュレーター上で、Intervalが非常に短い間隔のNSTimerを発行すると正しい時間にならない。
1/10=0.1秒くらいなら大丈夫そうだけど、1/1000秒になると1秒以上(正確には計ってない)に1回くらいになってしまう。iPad2を選択しているとうまく動くのにiPad RetinaやAirを選んでいるとだめなので、それらのバグ。ちなみに、実機ではちゃんと動く。

(10)Xcode7で、ツールバーが表示されないことがある。2つ以上のプロジェクトを同時に開いた場合、2つ目以降にてこうなる模様。表示を選択しても表示されない。また、一旦閉じるとツールバーの表示状態がリセットされてしまう。極めて面倒。

UITextViewはiOS7で大問題を起こしたけど、iOS8でも別のバグを入れ込んでいる体たらく。アップルはもっと開発者の声を聞いて、大量の人員を導入してバグ取りすべき。iOS9なんて出している場合じゃない。

余談:
UITextViewで、画面上に表示されている範囲を知る方法が欲しいのだけど、実装してくれないかなぁ。