ラベル iOS の投稿を表示しています。 すべての投稿を表示
ラベル iOS の投稿を表示しています。 すべての投稿を表示

2014年11月15日土曜日

UITableViewでのインデックス表示時の注意

UITableView

インデックス表示に重なる部分の下に来る要素(ボタンなど)は反応しないので注意。

実際に表示が重ならなくても、表示領域的に。


UILabelのfontサイズについて

UILabelのfontサイズは.fontのpointSizeなどで設定できるが、実はこの値はiOSによって動的に変更される。

adjustsFontSizeToFitWidth=YESでminimumScaleFactorを設定していた場合、.text(attributedText)の表示幅によってfontのサイズも変更される。


それはまだ理解しやすいがもう1つわかりにくい変化タイミングがある。


attributedText内でフォントを設定した場合、その先頭にかけられたフォントサイズになる。
ひょっとしたら、フォントそのものも変更されるかもしれない。


// 全体にフォントを適用した文字列を作成
NSMutableAttributedString *astr = [[NSMutableAttributedString alloc]
initWithString:baseStr attributes:@{NSFontAttributeName:font}];


この場合、label.fontのサイズはここで指定したfontのそれになる。
問題はここから。このastrにさらに、先頭からlen文字に

NSInteger point=0; // 先頭
UIFont *zeroFont=[UIFont systemFontOfSize:0];
[astr addAttributes:@{NSFontAttributeName:zeroFont} range:NSMakeRange(point,len)];

とかしてフォント指定を重ねると、それがUILabel.fontのフォントサイズになる。
この場合フォントサイズは0なので、len文字は表示されない。
先頭ではなく文字列途中にかけた場合はフォントサイズにはならない。

このattributedTextの表示は正常に行われるが、後にそのUILabelにそのまま別の.textを代入したりするとフォントサイズ0なので全体が表示されないというバグが発生する。.fontでサイズを呼び出してもおかしい、ということになる。

そんなことするはずないと思われるかもしれないが、UITableViewCellで再利用する場合には有り得る話となる。


要注意。

2014年10月2日木曜日

UITableViewCellの再利用について

UITableViewでは、画面内の表示に必要な分だけ、登録したセル(UITableViewCell)がシステム内部に用意される。
あくまで表示分だけであって、全体ではない。スクロールで表示外に出たセルは新しく入ってくる表示分に再利用される

表示外に出たセル→→→+
----------                 |
表示内                     |
                          |再利用
                          |
                          |
----------                 |
表示内に入るセル←←←+|
(実際には表示街に出たセルがすぐ入ってくるセルになるわけではない。
逆スクロールを考慮してか、若干の猶予がある。)

再利用されるとき、セル内に配置されたラベルなどの内容は「そのまま」なので、新しい表示を作るには、
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
で設定する必要がある。

「そのまま」なのは.textだけでなく.hiddenや.frame、addSubviewしている場合はそれもなので、
それらを変更した場合には、元に戻す必要がある。

カスタムセルの場合、awakeFromNibはセルの新規作成時のみ呼び出されるので、
この中で、変化する要素の変化前の情報を保存しておき、
cellForRowAtIndexPath内でまずそれに戻し、その後、新しい表示を設定する。

全体の手順

UITableViewCellの子クラス

@interface NewCell()
{
    CGRect frame;
}

- (void)awakeFromNib
{
    printSelf();
    [super awakeFromNib];
    //変更される要素の属性保存
    //「例」
    frame=label.frame;
}

-(void)initialize
{
    printSelf();
    label.frame=frame;
    // 追加されたボタンを削除する
    for (UIView *vw in self.subviews) {
        if (vw.tag==TAG_ADD_BUTTON) {
            [vw removeFromSuperview];
        }
    }
}

要素を追加するときは、.tagにTAG_ADD_BUTTONを入れて、後で識別できるようにするといい。




どうやってもセルに不必要な表示が残るのでわかったことであった。

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の勉強にはなったのでいいけど。
一応ライブラリの作成先に連絡を入れるつもりだけど、果たして直してくれるかどうか。

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

2012年12月21日金曜日

iOSの色々なバグや仕様

X−BASIC for iOSの開発中に発見したiOSのバグや隠れ仕様をあげておく。

発生するバージョンを特定できてないものもあるが、今後の開発時に注意すれば良いということで。

・横画面において、キーボードwidthとheightが逆になってる
  (というか、縦画面時のサイズのまま返ってきている)
 0,0,1024,352のはずが、0,0,352,1024になってる。
 画面方向を認識して、読み替える必要がある。

・MFMailComposeViewControllerで送信するとメモリーリークが発生する
 下書き〜削除・保存すると発生しない
 宛先をメニューを選ぶだけでは発生しない   
  シミュレーター上だけかもしれない


・バックグラウンドでUIGraphicsGetImageFromCurrentImageContext()が取得できない
常にリターン値がNULLになる

・ナビゲーションバーのボタンのタッチ有効範囲はかなり広く、かつnaviが最優先されるため、それに隣接する領域ではタッチが検出できない。
ナビゲーションバーのすぐ下のあたりなど。
[lblBasicFile becomeFirstResponder];してもだめ

・[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillShow:) name:UIKeyboardWillShowNotification object:nil];が2回連続でかかることがある
キーボードを閉じてないにもかかわらず「開く」が連続で通知されるため、順番に依存する処理で異常が起こる。
iOS5.0のみ発生。iOS5.1では発生しない。
日本語環境だけかもしれないが、英語環境では未検証。



・デジタルコンパスはメインスレッドで初期化しないと取得できない。
locationManager=[[CLLocationManager alloc]init];
をメインスレッド内で実行しないと、デリゲートが発生しない。

・CGContextRefで宣言したメンバーにはKVOが設定できない
 設定しても発生しない。

・実行時に Unknown class  ??? in Interface Builder file.と出るときは、そのクラスの.mファイルのTarget Membershipにチェックを入れる
Xcode右上のViewの一番右から

・[s drawAtPoint:point withFont:[UIFont fontWithName:fontName size:pointSize]];
で、pointSize=0だと<Error>: CGAffineTransformInvert: singular matrix.というエラーが出る。
エラーと内容があってない。

 ・UIWebViewへloadHTMLした場合、webViewDidFinishLoad以降でないと操作してはいけない

・ステータスバーのタップで先頭にスクロール
 editor.scrollsToTop    =YES;
スクロールさせたいもののみYESにし、他はNOにしなければならない

・UIView.transform=CGAffineTransformMakeTranslation(0, -fontWy);
で 平行移動がかかるのはUIViewのframeそのものであり、内容が移動するわけではない。
描画範囲も変わらない。
したがって、スクロール処理には使えない。

 ・HTMLの表示され方いろいろ
 Chrome            <pre>内にフォント指定は無効
 UIWebViewとChrome     <pre>と<table>内の改行の扱いが異なる

 Chromeは<table>内は<pre>が無効になっている。
  改行には<br>が必要で、スペースは無視される。
 UIWebViewはSafariとも異なる
 そのため、HTML表示確認は必ずUIWebView上で行わなければならない。

・UIGraphicsGetImageFromCurrentImageContext()はそれが含まれるコンテキストの内容だけを出力するのではなく、現在有効になっているすべてのコンテキストの内容を合成して出力してしまう。
多分、呼び出し時点までにUIGraphicsBeginImageContextされている全てのコンテキストだと思われる。

・CGBitmapContextCreateImage()にCGImageRetain()を付けたら画面がおかしくなる=メモリーリークする

・複数のコンテキストを使う場合、その切替には
  UIGraphicsPushContext(context);
  UIGraphicsPopContext();
が重要。これがないとコンテキストの切替後にエラーが出る。
ドキュメントではメインスレッド内で呼び出せとあるが、サブスレッド内でも問題ない。

・アプリケーション内フォントを追加する場合
必ずルート直下のサブディレクトリに置く必要がある
サブディレクトリの中のサブディレクトリ内は不可
これは他のリソースでも同じ。

・AirPrintはメインスレッドからしか実行できない
ポップアップを伴うので仕方ない様子。UIを持つものは全てメインスレッドで実行するのが慣わし。


おまけ
・Xcodeのsnapshotは過去に消したファイルも戻してしまうので、事実上使いものにならない。
えらい目にあった。
全保存したいなら、自分でzip圧縮化フォルダごとコピーするのが吉。



2012年12月8日土曜日

iOSでのいろんな画面(2)

画面の続き。

充電機能はサポートしてない(日本語)

充電機能はサポートしてない(日本語);ロック画面時

2012年12月5日水曜日

iOSでのいろんな画面(1)


iOS上で見られる、いろいろな場面での画面を集めておこうかと。
もちろん、いつでも見られるものではなく、ちょっと特殊な状況での物。
取説で使うかも知れないので。

電池残り20%(英語)
充電機能なし(英語)
Bluetooth有効確認(英語)
Bluetooth有効確認(日本語)
電池がもう少しで切れる(日本語)

電池が切れる直前(日本語のようだけど実は英語)
 Bluetooth接続確認

2012年12月2日日曜日

iOSバグ写真集

iOSにはいろいろとバグがいる。
クラスライブラリの中にいて、プログラマーに要らぬ苦労をかける物もいるが、
中には見て「ちょっと楽しくなる」というか、「見つけてやったり!」と愉快になる
物もある。

そういう物の発生現場写真をいくつか紹介。

最初のは、「高温注意」のメッセージ。そもそもこれが出た状況自体が理解できない。
室温20度以下で、筐体を触っても熱い部分などなかった。充電はしていたので、
電池周辺の極狭い範囲で熱を発生していたのだろうか。充電器を外し、電源も完全に切ってしばらく放置したら直った。

「温度検知機能があるのだなあ」と思ったのだけど、それよりおかしいのは警告メッセージ。
文字が多重に表示されていて読めない。 正しくは下のようになる。



次はiCoundのバックアップメッセージの異常。
なぜか2重に表示された。しかもOK押しても動かないハングアップ状態。
このときは、リセット(HOME押しながら電源ボタン長押し)することで解消した。


なんか他にもあったような気がするけど、画面が発掘できたのはこの2つだけだった。
これからもバグハンターしていきたい(^_^;)

2012年11月30日金曜日

CGContextSetLineWidth()とCGContextSetLineDash()の関係

X-BASIC for iOS開発の過程で、CGContextSetLineWidth()とCGContextSetLineDash()の関係がどうにもわからないので調べてみた。線幅を1より大きくした時のDashの描かれ方が意図したとおりにならない。

dash=10,20の設定でlineWidth1〜10まで変化させた時、下図のようになった。


width=1の時は設定通りだが、widthが増えるとそうではなくなる。
widthは縦幅だけでなくON部分のみ横幅まで増やしてしまう。
およそではあるが、widthに+1付きon区間が2増え、OFF区間が2減る。

最初iOSのバグかと思ったほど。
意図してこの2つを使いこなせる人間はいるのだろうか?

インターネットで色々調べてもLineWidth=1の時の描き方の例はあってもそれ以上の時は見つけられなかったので、ここに。

ちなみに、X-BASIC for iOSでは意図したとおりに描かれるようパラメーターを自動調整している。

2012年10月20日土曜日

iOSウオッチドッグの存在

今まで全く気にしてなかったが、どうもiOS中にもウォッチドッグタイマーが存在するようである。

ウォッチドッグが何であるか、詳しくは調べてもらうとして、超簡単に書けば、
暴走防止のためのタイマーである。 組み込み機器ではウォッチドッグにひかかったら、
暴走や(周辺デバイス)無反応とみなしリセットをかけるのが普通である。
(正確には、プログラム的に一定時間以内にウォッチドッグタイマーをリセットしないと、ハードウエア的にリセットがかかるようになっている。)

某所の情報によると、iOSでもWebView周りには
  • Watchdog: 20秒(アプリケーション起動時など)
  • DNS: 30秒
  • TCP Connection: 75秒
  • NSURLConnection: 60秒
というウォッチドッグがいるらしい。
"0x8badfood"という例外コードを返してくるとか。"ate bad food"ということらしい。
ateはeatの過去形。発音が8=eightと同じである。

しかし、X-BASIC for iOS開発時の調査の結果、カーネルそのものに関わるタイムアウトも存在ようである。

以下のループをシミュレーター上で実行すると、約256秒でプログラムが落ちる。
while (1) {
#if 0
    NSDate *dt=[[NSDate alloc ]initWithTimeIntervalSinceNow:0.0];
    [[NSRunLoop currentRunLoop]runUntilDate:dt];
    [dt release];
#endif
 }
これで落ちるときはEXC_???(11)(code=0,subcode=0x0)となる。

#ifの中は有効無効どちらにしても変化なし。
また、この中にどれだけ長いCの処理を書いていたとしても、落とされてしまう。
 iOS5.1/6.0とも。

しかもこれ、Xcodeにつないでいる実機上やシミュレーター上でもLeakでトレースしていると発生しない。単独の実機ではHOMEに戻ってしまうし、シミュレーター上で普通に実行している場合は、上記エラーというかトラップを発生して止まる。

2013/11/10追記
iOS7シミュレーターでは発生しない様な気がする。Xcode5で改善された?

・・・

無限ループらしきところを警告しているつもりなのかもしれないが、定期的にcurrentRunLoopに戻しても発生するから、迷惑な事この上ない。
警告ならメッセージで出すべきであって、プログラムを落とすのはやりすぎ。

普通ウォッチドッグタイマーにはそれをリセットする機構があるが、iOSにはない。
ゆえに、iOSは組み込み機器的には使えない。
(iPadやiPod touchが安いから、筐体に組み入れて組み込み機器にしたらいいじゃん、という意見を見たことがあるが、iOSはこの件を含め、組み込み機器向けじゃない。組み込み機器なめんなよってな感じ。)

    ・・・

さらなる調査で、RunLoop内では、如何様にしてもループを回し続けるのはご法度と判明。必ず落ちる。
バッググラウンド内なら回し続けても大丈夫。でもバックグラウンドからからは表示させることができない。というか、表示や入力に関わるすべてのUI要素が使えなくなる。UIWebViewに至ってはレポートまで出力して落ちる。
だが、バックグラウンドからRunLoopに通信する方法が見つからない。
(KVOは同一スレッド内で実行されるのでダメ。 Notificationもおそらく同様。)
これさえあれば何とかならんでもないのだが。

KVOの通知でフラグを立て、先のループ内で表示するという手を考えた。
while (1) {
    NSDate *dt=[[NSDate alloc ]initWithTimeIntervalSinceNow:0.1]; // 0.0はだめ
    [[NSRunLoop currentRunLoop]runUntilDate:dt]
    [dt release];
    if (flag) {
       // 表示
    }
 }
実際にこれで表示が出るには出る。が、秒1回くらいならずっとOKなのだが、秒30回くらいの速度でしばらく回していると表示が出なくなる。どうも、NSRunLoopの行から帰ってこないようだ。RunLoopに入りっぱなしになるという感じ。バグじゃろうこれは。

→アップルのドキュメントの中にRunLoopにスレッドを接続する方法を発見。
何とかUIViewの表示だけは出来るようになった。が、問題はUIViewだけじゃなくUITextFiled等も含む全UI要素に関わるので対応が非常に大変。
→実機で落ちなけりゃそれでOKで無視することにした。


2012年7月20日金曜日

UTF16をUTF8に変換する処理

今度はUTF16をUTF8に変換する処理。

実を言うと、UTF16はあまり使い道がない。と思う。
->とか書いてたけど、実はiOSの中の文字コードはUTF16だと判明。
characterAtIndexで返ってくるコードはUTF16である。
UTF8だと思って処理を書いてたらうまく動かないので調べたらそうだった。
(後日アップルのドキュメントを読み返したら、UTF16だとちゃんと書いてあった。
でも、あんなところに1行だけでは気が付く人少ないのでは?)
ただし、ソースコード(その中に記述する文字列)そのものはUTF8でなくてはならない。
ここは少しややこしいところ。

SHIFT-JISは、組み込み機器ではプログラムもデータも小さくなるので非常に使いやすい。
表示幅とバイト数が一致するのもいい点である。
UTF8はASCIIコードとの互換性が高く、ASCII用のプログラムを移植するに都合がいいことが、X-BASIC for iOSの開発でよくわかった。

SHIFT-JISとの変換に関してはUTF16の方が行いやすい。

ulong Utf16ToUtf8(ulong n)
// http://ja.wikipedia.org/wiki/UTF-8 参照
// Unicodeは文字コードの総称で、UTF-16やUTF-8はその表現方法の種類、である。
{
 if (n<=0x7f) return(n); // ASCII互換
 if (n<=0x7ff) {
  // 8421 8421   8421 8421
  // 110x_xxxx | 10xx_xxxx
  return  w16(             (0xc0|(n>>6))   , (0x80|(n&0x3f)) );
 }
 if (n<=0xffff) {
  // 8421 8421   8421 8421   8421 8421
  // 1110_xxxx | 10xx_xxxx | 10xx_xxxx
  return w24(       (0xe0|((n>>12)&0x0f)) , (0x80|((n>>6)&0x3f)) , (0x80|(n&0x3f)) );
  
 }
 // n<=0x1fffff
 // 8421 8421   8421 8421   8421 8421   8421 8421
 // 1111_0xxx | 10xx_xxxx | 10xx xxxx | 10xx xxxx
 return w32( (0xf0|((n>>18)&0x07)) , (0x80|((n>>12)&0x3f)) , (0x80|((n>>6)&0x3f)) , (0x80|(n&0x3f)) );
 
 // 定義上はこれ以上もあるが、実在しないので省略
}