しまなみ海道全線走破の話はこちら。
そこにはアフリエイトが貼れないので、こちらに集約。
リュックは「ドイターフューチュラ28」
GPSは「GARMIN etrex20 中国語版」
液晶フィルタは一応これを上げておくけど、文字がにじむのでいまいちかもしれない。
おしりの痛み防止にはこれが良さそう。見つけただけで使ってはいないから、評価はお預け。
もう少し高級品はこちら。
2015年7月15日水曜日
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へ追加しなければならないが、これはメインスレッドで行わなければならない
(13)複数の処理を並行動作させたいときはメインスレッドは使えない
メインスレッドは1つしかないから考えたらあたりまえのことなんだけど、
複数同時に走らせたい処理を
[self performSelectorInBackground:@selector(fetch_main) withObject:nil];
を
[self performSelectorOnMainThread:@selector(fetch_main) withObject:nil waitUntilDone:NO];
に変更したらうまく動かなくなった(上記を複数回発行して複数本同時に走らせる)
バックグラウンドは同時に何本でも設定できる=並行動作するが、メインスレッドは1つしかないからである。メインスレッドでは、追加された順にキューに記録され、1つが終わると次のが走る。
(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で、画面上に表示されている範囲を知る方法が欲しいのだけど、実装してくれないかなぁ。
巷で情報がないもの。
(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でブレイクポイントはあまり使いものにならない状態。
バックグラウンドで動くタスクの場合だけかもしれないが。
1/10=0.1秒くらいなら大丈夫そうだけど、1/1000秒になると1秒以上(正確には計ってない)に1回くらいになってしまう。iPad2を選択しているとうまく動くのにiPad RetinaやAirを選んでいるとだめなので、それらのバグ。ちなみに、実機ではちゃんと動く。
(10)Xcode7で、ツールバーが表示されないことがある。2つ以上のプロジェクトを同時に開いた場合、2つ目以降にてこうなる模様。表示を選択しても表示されない。また、一旦閉じるとツールバーの表示状態がリセットされてしまう。極めて面倒。
UITextViewはiOS7で大問題を起こしたけど、iOS8でも別のバグを入れ込んでいる体たらく。アップルはもっと開発者の声を聞いて、大量の人員を導入してバグ取りすべき。iOS9なんて出している場合じゃない。
余談:
UITextViewで、画面上に表示されている範囲を知る方法が欲しいのだけど、実装してくれないかなぁ。
ラベル:
iOS7,
iOS8,
UITextView,
Xcode6,
バグ
2015年5月15日金曜日
Firefox/Chrome/IEでまともに見られないページに出会った時の対処方法
Windows7の再インストールした時、Firefox/Chrome/IEで発生していた各種問題を一気に解決した。それを記録にとどめておく。
・・・
1. Chromeでバンダイチャンネルが見られない
アドオンのAdblockが悪さをしていたと判明。
ホワイトリストでhttp://www.b-ch.comを通すことで解決。
2. Firefoxでバンダイチャンネルが見られない
自動的にhttpsで接続するアドオン(名前は失念)が悪さをしていた様子。外したらいけた。
ついでにこちらもAdblockを外すように設定。
3. FirefoxでGyaoが正常に見られない(テキストのみ表示になってしまう)
これもAdblockが原因。Gyaoを始め、Yahoo系のサイトはAdBlockを有効にしているとまともに表示されない事が多い(Yahoo知恵袋とか)。広告収入が主なサイトは、あえてAdblock対策してあるのかも。
4. FirefoxでSilverlightが正常に動かない
Yahooのルートラボとか。
これもAdBlockで対象ページを通せばOKだった。
とにかく動画系にはAdblockを入れているとおかしくなるサイトが多い。広告を外してくれるありがたいアドオンだが、副作用もあるので、表示がおかしい時はまず疑ったほうがいいかも。
5. NTTの速度測定ができない
http://www.flets-west.jp/speed/
5-1. 「必要なプラグインがインストールされてない」と出る場合
32bit-Javaのインストールが必要。
Javaのダウンロードサイトはアクセスしたブラウザのビット数でダウンロードできるJavaのビット数が自動的に切り替わってしまう。64bit Chrome使ってJavaダウンロードしたら64bit Javaが入ってた。
IE11やFirefoxは32ビットなので、それを使ってJavaをダウンロード&インストールすること。
5-2. 「エラーが発生する」
セキュリティーでブロックされてる。
コントロールパネル~Java(32ビット)~Javaコントロールパネルからセキュリティ~サイトリストの編集で追加として、速度計測サイトのアドレスを登録する。http://[~:~:~:~::~]/bench.jspで、~の部分はつなぐ先によって異なると思う。
登録すると、実行確認が出てくるので、「実行」を押すと実行される。
6. Silverlightを再インストールしようとするとWindowsが完全ハングアップする
これが一番厄介。Silverlightはアンインストール時にレジストリにゴミを残す。そしてそのごみがあると再インストール時にハングアップする。多分80%~95%までの間で。完全に固まってしまい、キーもマウスも何も効かないので強制電源OFFしかない。
解決法は、そのレジストリのゴミを取り除くこと。
Windows再起動後、regeditを起動、検索で「Silverlight」を探し、それを含むエントリを全て削除する。200個以上あるので地道にやるしかない(ひょっとしたら一部だけでいいかもしれないけど)。他のを削除しないように。
全部削除したらregeditを終了し(再起動は必要ない)、Silverlightを再インストールするとうまくいく。
7. ChromeでSilverlightが有効にならない
Chrome自体が外部プラグインを認めない方針になったことによるらしい。
以下の手順で有効にできる。
(1)URL欄に「chrome://flags/#enable-npapi」と入力
(2)出てくる設定一覧の中で、NPAPI を有効にする
(3)Chromeを再起動(先の画面の一番下にボタンが有る)
(4)URL欄に「chrome://plugins/」と入力
(5)Silverlightで「常に実行を許可する」をチェック
先のルートラボをChromeで閲覧する場合がこれが必須となる。
また、他の外部プラグインも同様に有効にできる。
・・・
1. Chromeでバンダイチャンネルが見られない
アドオンのAdblockが悪さをしていたと判明。
ホワイトリストでhttp://www.b-ch.comを通すことで解決。
2. Firefoxでバンダイチャンネルが見られない
自動的にhttpsで接続するアドオン(名前は失念)が悪さをしていた様子。外したらいけた。
ついでにこちらもAdblockを外すように設定。
3. FirefoxでGyaoが正常に見られない(テキストのみ表示になってしまう)
これもAdblockが原因。Gyaoを始め、Yahoo系のサイトはAdBlockを有効にしているとまともに表示されない事が多い(Yahoo知恵袋とか)。広告収入が主なサイトは、あえてAdblock対策してあるのかも。
4. FirefoxでSilverlightが正常に動かない
Yahooのルートラボとか。
これもAdBlockで対象ページを通せばOKだった。
とにかく動画系にはAdblockを入れているとおかしくなるサイトが多い。広告を外してくれるありがたいアドオンだが、副作用もあるので、表示がおかしい時はまず疑ったほうがいいかも。
5. NTTの速度測定ができない
http://www.flets-west.jp/speed/
5-1. 「必要なプラグインがインストールされてない」と出る場合
32bit-Javaのインストールが必要。
Javaのダウンロードサイトはアクセスしたブラウザのビット数でダウンロードできるJavaのビット数が自動的に切り替わってしまう。64bit Chrome使ってJavaダウンロードしたら64bit Javaが入ってた。
IE11やFirefoxは32ビットなので、それを使ってJavaをダウンロード&インストールすること。
5-2. 「エラーが発生する」
セキュリティーでブロックされてる。
コントロールパネル~Java(32ビット)~Javaコントロールパネルからセキュリティ~サイトリストの編集で追加として、速度計測サイトのアドレスを登録する。http://[~:~:~:~::~]/bench.jspで、~の部分はつなぐ先によって異なると思う。
登録すると、実行確認が出てくるので、「実行」を押すと実行される。
6. Silverlightを再インストールしようとするとWindowsが完全ハングアップする
これが一番厄介。Silverlightはアンインストール時にレジストリにゴミを残す。そしてそのごみがあると再インストール時にハングアップする。多分80%~95%までの間で。完全に固まってしまい、キーもマウスも何も効かないので強制電源OFFしかない。
解決法は、そのレジストリのゴミを取り除くこと。
Windows再起動後、regeditを起動、検索で「Silverlight」を探し、それを含むエントリを全て削除する。200個以上あるので地道にやるしかない(ひょっとしたら一部だけでいいかもしれないけど)。他のを削除しないように。
全部削除したらregeditを終了し(再起動は必要ない)、Silverlightを再インストールするとうまくいく。
7. ChromeでSilverlightが有効にならない
Chrome自体が外部プラグインを認めない方針になったことによるらしい。
以下の手順で有効にできる。
(1)URL欄に「chrome://flags/#enable-npapi」と入力
(2)出てくる設定一覧の中で、NPAPI を有効にする
(3)Chromeを再起動(先の画面の一番下にボタンが有る)
(4)URL欄に「chrome://plugins/」と入力
(5)Silverlightで「常に実行を許可する」をチェック
先のルートラボをChromeで閲覧する場合がこれが必須となる。
また、他の外部プラグインも同様に有効にできる。
ラベル:
Chrome,
Firefox,
IE,
Java,
Silverlight
2015年4月11日土曜日
GARMIN etreck20(台湾版)の使い方;随時追記
GARMIN社のGPS、etreck 20を購入した。日本語版もあるのだがかなり高い。
日本語版で下位機種のetreck10の値段で、海外版では上位機種であるこの20が買えてしまうほどだった。
しかも、調べてみると海外版を日本語化出来るそうなので、海外版を購入した。
~~
~~
これ、実際には台湾版(中国語版)である。Amazonの写真は赤の筐体が写っているが、台湾版は黄色である。この台湾版は英語版に比べ日本語化がしやすい。なので、「中国語がちょっとでも表示されるのは絶対嫌だ」というのがないなら、英語版を買うよりこの台湾版を入手した方がいい。
ちなみに、2015/4/10現在では円高も進んだこともあって、Amazon.comから英語版を購入しても、この台湾版と余り変わらない値段となってしまう。もっとも、この台湾版も日によって大幅に価格が変わることがあるようだが。
~~~準備~~~
届いたらまず、PCにGARMIN Expressと言うアプリを導入する。インターネットで検索して探す。
このアプリ自体は日本語化されている。 今回はWindows版を導入したが、Mac版も存在するようだ。
ファームのアップデートもこの上から行えるようであるが、台湾版は2015年4月11日現在V2.80が最新のようで、アップデートは出来ない。英語版にはV4.20まであるようだが、台湾版にはどうやっても導入出来なかった。
台湾版なので、付属取説は全く読めなかった。しかし、GARMIN Express上から英語の取説を開くことが出来る。英語の取説もわかりやすいとは思わないし、上位機種の30と共用なので20には存在しない機能についても書かれていたりする。なお、英語版向けの日本語簡易取説はここにある。機械翻訳らしく一部に間違いがあるけど。
実のところ、GARMIN Expressは余り使いようがない。ログの取り出しもこれでは出来ない。じゃあなんで導入するかといえば、念のため。
ちなみに、GARMIN Expressは、起動毎にスタートメニューのルートにGarminというフォルダを作る。きわめていらんお節介。
~~~日本語化~~~
台湾版の日本語化の手順は、
~~~メニューの日本語化~~~
これはとても簡単。
Setup~System~LanguageでJapaneseを選ぶだけ。
台湾版にはすでに日本語フォントと日本語メニューデータが入っているからこれだけで済む。
英語版だとフォントの導入から行わなければならないのでかなり面倒そう。
ただ、標準で入っている日本語メニューデータは、翻訳がおかしいと言うかわかりにくい部分があるので、私は独自に編集して使っている。まだ全ての表示を確認出来ていないので公開はしない(文字列長の問題があるかも知れないので)。
変更したい人は、本体を記録デバイスとして認識させて、\Garmin\Text\Japanese.gttを編集すればよい。UTF-8のテキストである。
なお、日本語を選んでも一部のメニューや文言は英語のままになる。どうもファームに直接書かれていて変更出来ない部分があるようだ。
~~~日本地図の導入~~~
この台湾版の最大の問題がこれで、標準では日本が全く表示されない。日本の座標を入れても海のど真ん中になってしまう。なので、日本で使うには日本地図の導入が必須である。
地図は市販のものもあるようだが、無料で手に入れることも出来る。その方法を紹介する。
なお、日本語地図の導入にはマイクロSDカードが必要である。一時的にではなく恒久的に。
~~
~~
日本語地図の導入には最低1GB必要だが、今時そんな小容量はないので問題ない。でも、このSDカードにはログデータも記録されるので、過去のログを本体内に長くとどめておきたいならそれ相応の容量が必要となる。
海外版etreck対応の日本地図はここ↓にある。時々更新されている。
OpenStreetMap for Garmin
これを台湾版GARMINで使う場合、ダウンロードすべきは英語版である。日本語版をダウンロードしてはいけない。英語版は英語(ローマ字)表記ではなく英語モデル用に文字コードUTF-8で作られているという意味だった(日本語版は日本モデル=etreck20J用にSHIFT-JISで作られていると思われる)。
誤解を招きやすい書き方をされているので要注意。
2016/02/17更新:
サイトが更新されデータが変わっている。ダウンロードすべきは日本~UTF8のもの。等高線の有り無しは好みで。
で、導入仕方。
SDカードスロットのカバーを上げた状態
SDカードを挿して蓋を閉めた状態
先にSDカードを入れて、USBで認識させて書き込めばいいと思うかもしれないが、USB経由のGARMIN内SDへの書き込みはかなり遅い(多分USB1.1だと思う)ので、特に大容量の地図データではしない方がよい。
これを導入すると、地図上の表記が漢字になるほか、検索も漢字で行える。ただし、OpenStreetMapの性格上、抜けや間違いがある。それに対して文句を言うのは筋違いである。気になるなら、自分で入力して貢献しよう。
日本語フォントを導入していない海外版でも使えるようにローマ字表記で表示する地図もある。ただ、台湾版は日本語フォントがあるのでわざわざこちらを導入する必要はない。ローマ字変換がおかしい部分もあるし。
~~~製品感想~~~
しばらく使った上での感想などをいくつか。
~~~ログの取り出し方法(日本語地図導入時)~~~
ログは、日本語地図導入時=SDカード挿入時はSDカード上に記録される。
なので、GARMINをUSBでつないで SDカード上のGarmin\TRK内のgpxファイルを取り出せばよい。GoogleEarthやカシミール3Dで表示させることができる。
(カシミールはひたすら重いので、編集でもしない限り使わないけど。)
~~~ショートカットの使い方~~~
ショートカットとは、 機能の中で良く使い物をbackボタンを押すだけで呼び出せる様にする機能である。ショートカットに登録した物は、backを押す度に切り替わって表示(いわゆるトグル表示)される。
しかしこいつには問題があって、ショートカットに登録された機能は普通のメニューからは表示されなくなる。
最初機能が表示されなくなって故障したかと思ったが、そういう仕様だった。
メニューからは常に全て表示させておいて欲しいものだ。
実際の所、ショートカットに登録したところで、メニューから選択するのとさほど操作手順は短縮されないと思うので、使い道は少ないと思う。
~~~液晶保護は必要か~~~
etrex20の液晶画面は傷はつきにくいと思う。自転車移動中にだいぶガチガチ当ててしまい、時には擦ってしまったかと思ったこともあったが無事だった。
傷よりむしろ手などの油の付着のほうが気になった。
ということで、一応液晶フィルムも購入してみた。屋外では反射して見にくかったので無反射タイプを選択。
でもこのフィルム、低反射はいいけど、字がボケる。かなり。
ということで、諸手を挙げてのおすすめは出来ない。
~~~ルート設定~~~
etrex20には、外部で作ったコースデータを入れて、それにそって案内するという機能もある。
案内と言ってもカーナビのように音声ガイドしてくれるわけではないが、人によっては重要かもしれない。
(私は基本軌跡をとれればいいので、重視してない)。
で、問題はそのルートデータの作成仕方。etrex20本体でも、過去の軌跡から設定できるのかもしれないが、いかんせん取説がないのでわからない。現在わかっているコースデータの作り方はYahooのルートラボを使う方法である。
細かい使い方はサイトのヘルプを見てもらうとして、このサイトで地図上にルートを描き、出力したものをetrex20のSDに入れて表示させる。
このサイトでは自分の作ったコースデータを公開したり、逆に他の人の公開データを見ることも出来る。
なおこのルートラボ、silverlightを使うのだが、silverlightはマイクロソフト自体が開発終了を宣言しているので、いつまで使えるのかはわからない。
・・・今後追加予定の項目・・・
~~~メニューのカスタマイズ~~~
~~~表示項目のカスタマイズ~~~
など
日本語版で下位機種のetreck10の値段で、海外版では上位機種であるこの20が買えてしまうほどだった。
しかも、調べてみると海外版を日本語化出来るそうなので、海外版を購入した。
~~
~~
これ、実際には台湾版(中国語版)である。Amazonの写真は赤の筐体が写っているが、台湾版は黄色である。この台湾版は英語版に比べ日本語化がしやすい。なので、「中国語がちょっとでも表示されるのは絶対嫌だ」というのがないなら、英語版を買うよりこの台湾版を入手した方がいい。
ちなみに、2015/4/10現在では円高も進んだこともあって、Amazon.comから英語版を購入しても、この台湾版と余り変わらない値段となってしまう。もっとも、この台湾版も日によって大幅に価格が変わることがあるようだが。
~~~準備~~~
届いたらまず、PCにGARMIN Expressと言うアプリを導入する。インターネットで検索して探す。
このアプリ自体は日本語化されている。 今回はWindows版を導入したが、Mac版も存在するようだ。
ファームのアップデートもこの上から行えるようであるが、台湾版は2015年4月11日現在V2.80が最新のようで、アップデートは出来ない。英語版にはV4.20まであるようだが、台湾版にはどうやっても導入出来なかった。
台湾版なので、付属取説は全く読めなかった。しかし、GARMIN Express上から英語の取説を開くことが出来る。英語の取説もわかりやすいとは思わないし、上位機種の30と共用なので20には存在しない機能についても書かれていたりする。なお、英語版向けの日本語簡易取説はここにある。機械翻訳らしく一部に間違いがあるけど。
実のところ、GARMIN Expressは余り使いようがない。ログの取り出しもこれでは出来ない。じゃあなんで導入するかといえば、念のため。
ちなみに、GARMIN Expressは、起動毎にスタートメニューのルートにGarminというフォルダを作る。きわめていらんお節介。
~~~日本語化~~~
台湾版の日本語化の手順は、
- メニューの日本語化
- 日本地図の導入
~~~メニューの日本語化~~~
これはとても簡単。
Setup~System~LanguageでJapaneseを選ぶだけ。
台湾版にはすでに日本語フォントと日本語メニューデータが入っているからこれだけで済む。
英語版だとフォントの導入から行わなければならないのでかなり面倒そう。
ただ、標準で入っている日本語メニューデータは、翻訳がおかしいと言うかわかりにくい部分があるので、私は独自に編集して使っている。まだ全ての表示を確認出来ていないので公開はしない(文字列長の問題があるかも知れないので)。
変更したい人は、本体を記録デバイスとして認識させて、\Garmin\Text\Japanese.gttを編集すればよい。UTF-8のテキストである。
なお、日本語を選んでも一部のメニューや文言は英語のままになる。どうもファームに直接書かれていて変更出来ない部分があるようだ。
~~~日本地図の導入~~~
この台湾版の最大の問題がこれで、標準では日本が全く表示されない。日本の座標を入れても海のど真ん中になってしまう。なので、日本で使うには日本地図の導入が必須である。
地図は市販のものもあるようだが、無料で手に入れることも出来る。その方法を紹介する。
なお、日本語地図の導入にはマイクロSDカードが必要である。一時的にではなく恒久的に。
~~
~~
日本語地図の導入には最低1GB必要だが、今時そんな小容量はないので問題ない。でも、このSDカードにはログデータも記録されるので、過去のログを本体内に長くとどめておきたいならそれ相応の容量が必要となる。
海外版etreck対応の日本地図はここ↓にある。時々更新されている。
OpenStreetMap for Garmin
2016/02/17更新:
サイトが更新されデータが変わっている。ダウンロードすべきは日本~UTF8のもの。等高線の有り無しは好みで。
で、導入仕方。
- 上記サイトから日本~英語版をダウンロードしてくる。等高線の有り無しは好みで。両者を同時に使うことは出来ないようである。Mega Limitedと言うサイトからダウンロードすることになるのだが、Google ChromeにAdBlockを導入しているとはじかれてしまいダウンロード出来ない。設定を変更して通るようにするか、他のブラウザを使うこと。
- ZIPを展開する。gmapsupp.imgというのが地図データである。
- マイクロSDに書き込む
まずGarminと言うフォルダを作り、その下に入れる。
\Garmin\gmapsupp.imgと言うファイルがあれば正解。 - GARMINの電池ぶたを開け、電池を外し、その下にあるSDスロットにカードを入れる。
写真は下参照(Google Bloggerの制約でここに写真が貼れないので)。
このスロットはカードを差し込むのではなく置くタイプなので、注意。 - 電池を入れ直して再起動。
- 設定>地図>地図選択でOSM MAPS Familyを有効に設定する
これをしないとせっかく導入した地図が表示されない。
同じファイル名でも内容が異なれば再度この設定が必要。 - 同、China BasemapおよびDigital Globeを無効にする
SDカードスロットのカバーを上げた状態
SDカードを挿して蓋を閉めた状態
先にSDカードを入れて、USBで認識させて書き込めばいいと思うかもしれないが、USB経由のGARMIN内SDへの書き込みはかなり遅い(多分USB1.1だと思う)ので、特に大容量の地図データではしない方がよい。
これを導入すると、地図上の表記が漢字になるほか、検索も漢字で行える。ただし、OpenStreetMapの性格上、抜けや間違いがある。それに対して文句を言うのは筋違いである。気になるなら、自分で入力して貢献しよう。
日本語フォントを導入していない海外版でも使えるようにローマ字表記で表示する地図もある。ただ、台湾版は日本語フォントがあるのでわざわざこちらを導入する必要はない。ローマ字変換がおかしい部分もあるし。
~~~製品感想~~~
しばらく使った上での感想などをいくつか。
- GPS衛星補足能力はかなり高い。また、一端見失っても再捕捉が早い。
→ではあるが、時々長時間再補足出来ないことがある。特に電源投入直後の捕捉は、高速移動中だとかなりかかる。新幹線の中で電源を切ったり、電池切れに気がついて交換した後に判明(全く別の日に両方やってしまった)。長時間捕捉失敗後も同様の傾向がある様子。
→低速移動中でもできないと判明。基本的に、初期のGPS補足は停止中にしなければならない(その割には電源ボタンが押しやすすぎ)。 - 精度は今まで使ったGPSの中でもトップクラス。さすが高いだけのことはある。平均的な誤差は2~3メートルという感じ。
ただ、GPSの原理上、低速移動時の誤差は大きくなってしまう。これは他のGPSも同様。 - 電池の保ちはかなり良いと思う。エネループ2本で21時間くらい保つ感じ(標準のログ記録間隔で)。でも、電池が減っても気がつきにくいので、毎日交換するのが吉かと。
- 重い。電池を抜いてもそう思う。
- 電源スイッチを間違って押してしまうこと多数回。 押し易す過ぎる。これは要改良点。スライドスイッチだったらよかったのに。うちではボタン上にホットメルトでぽっちを作って手触りでわかるようにしている。(電源以外も押されてしまいやすいけど、ほかの動作モードに入ってもログ記録は途切れないので致命的ではない。)
- 全体的に操作はわかりにくい。本体だけでは事実上出来ないこともある。それについてここで書いていこうかと。「トリップメーターどうやって使うんだ!?」
- 室内;木造でも
- 地下鉄、トンネル内;携帯電話が通じてもGPSは無理
- JR223系新快速車両;なぜか窓際でも補足できなかった(京都→大阪間で体験)。
- 新幹線700系の通路側
~~~ログの取り出し方法(日本語地図導入時)~~~
ログは、日本語地図導入時=SDカード挿入時はSDカード上に記録される。
なので、GARMINをUSBでつないで SDカード上のGarmin\TRK内のgpxファイルを取り出せばよい。GoogleEarthやカシミール3Dで表示させることができる。
(カシミールはひたすら重いので、編集でもしない限り使わないけど。)
~~~ショートカットの使い方~~~
ショートカットとは、 機能の中で良く使い物をbackボタンを押すだけで呼び出せる様にする機能である。ショートカットに登録した物は、backを押す度に切り替わって表示(いわゆるトグル表示)される。
しかしこいつには問題があって、ショートカットに登録された機能は普通のメニューからは表示されなくなる。
最初機能が表示されなくなって故障したかと思ったが、そういう仕様だった。
メニューからは常に全て表示させておいて欲しいものだ。
実際の所、ショートカットに登録したところで、メニューから選択するのとさほど操作手順は短縮されないと思うので、使い道は少ないと思う。
~~~液晶保護は必要か~~~
etrex20の液晶画面は傷はつきにくいと思う。自転車移動中にだいぶガチガチ当ててしまい、時には擦ってしまったかと思ったこともあったが無事だった。
傷よりむしろ手などの油の付着のほうが気になった。
ということで、一応液晶フィルムも購入してみた。屋外では反射して見にくかったので無反射タイプを選択。
でもこのフィルム、低反射はいいけど、字がボケる。かなり。
ということで、諸手を挙げてのおすすめは出来ない。
~~~ルート設定~~~
etrex20には、外部で作ったコースデータを入れて、それにそって案内するという機能もある。
案内と言ってもカーナビのように音声ガイドしてくれるわけではないが、人によっては重要かもしれない。
(私は基本軌跡をとれればいいので、重視してない)。
で、問題はそのルートデータの作成仕方。etrex20本体でも、過去の軌跡から設定できるのかもしれないが、いかんせん取説がないのでわからない。現在わかっているコースデータの作り方はYahooのルートラボを使う方法である。
細かい使い方はサイトのヘルプを見てもらうとして、このサイトで地図上にルートを描き、出力したものをetrex20のSDに入れて表示させる。
このサイトでは自分の作ったコースデータを公開したり、逆に他の人の公開データを見ることも出来る。
なおこのルートラボ、silverlightを使うのだが、silverlightはマイクロソフト自体が開発終了を宣言しているので、いつまで使えるのかはわからない。
・・・今後追加予定の項目・・・
~~~メニューのカスタマイズ~~~
~~~表示項目のカスタマイズ~~~
など
2014年11月15日土曜日
iOS7以降におけるUIButtonの挙動の問題について
iOS7以降では、UIButton.textを設定した時、UIButton.textLabel.frameが変更されるのは実際に表示されるタイミングになっている。すなわち、.text代入時にはtextLabelの表示幅や位置は取得できない。
通常それを知る必要はないが、UIButtonではtextLabelは基本的にセンタリングで表示される(titleEdgeInsetで補正は可能)ため、例えばボタンを表示上左寄せしたい場合にはtextLabel.frame.origin.x=0にしなければならない。ところが、.text代入直後は.textLabel.frameは{0,0,0,0}であり、この時点で代入しても無効になる。
ではどうするかというと、KVOで.textLabel.frameを監視して、その代入があったタイミングで補正する。
こんな感じ。
――――――――
UIButtonWithLabelAlignment.h
――――――――
#import <UIKit/UIKit.h>
// 左寄せと上寄せがORで設定できる
enum {
kUIButtonWithLabelAlignment_normal=0,
kUIButtonWithLabelAlignment_left =(1<<0),
kUIButtonWithLabelAlignment_up =(1<<1),
};
@interface UIButtonWithLabelAlignment : UIButton
@property (nonatomic) NSInteger align;
@end
――――――――
UIButtonWithLabelAlignment.m
――――――――
#import "UIButtonWithLabelAlignment.h"
#define OBSERVE_FRAME @"titleLabel.frame"
@interface UIButtonWithLabelAlignment()
{
BOOL setKVO;
}
@end
@implementation UIButtonWithLabelAlignment
-(instancetype)initWithFrame:(CGRect)frame
{
self=[super initWithFrame:frame];
if (self) {
[self addObserver:self forKeyPath:OBSERVE_FRAME options:
NSKeyValueObservingOptionNew context:NULL];
setKVO=YES;
}
return self;
}
// KVOによる変更通知受信
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object
change:(NSDictionary *)change context:(void *)context
{
// 1つしか登録してないのでkeyPathのチェックは省略
// 即解除;そうしないと以下でframeを操作するとまた発生してしまうから
[self removeObserver:self forKeyPath:OBSERVE_FRAME];
setKVO=NO;
// 寄せる
UIButton *btn=(UIButton *)object;
CGRect frame=btn.titleLabel.frame;
if (self.align&kUIButtonWithLabelAlignment_left) {
frame.origin.x=0;
}
if (self.align&kUIButtonWithLabelAlignment_up) {
frame.origin.y=0;
}
btn.titleLabel.frame=frame;
}
-(void)dealloc
{
if (setKVO) {
// 外れてないのが残ってたら外す
self.titleLabel.frame=CGRectZero; // 上の通知でKVOを解除する
// これでは解除処理が終了するまでにdeallocが終了してしまうためエラーが発生する
// [self removeObserver:self forKeyPath:OBSERVE_FRAME];
}
}
ついでに書いておくと、[UIButton sizeToFit]するとボタン幅が表示幅に合わされるので中央寄せ=左寄せで問題ないが、.textLabelにtruncateを設定していると、その表示幅はボタンの幅より必ず狭くなるのでセンタリングが起こる。これはtruncateが発生するラベルと発生しないラベルを並べた場合には、表示位置がずれて見えるということを意味する。なお、truncateが発生したボタンにsizeToFitをかけるとtruncateが外れてしまう(全体が表示できる幅のボタンになる)のでやってはいけない。
通常それを知る必要はないが、UIButtonではtextLabelは基本的にセンタリングで表示される(titleEdgeInsetで補正は可能)ため、例えばボタンを表示上左寄せしたい場合にはtextLabel.frame.origin.x=0にしなければならない。ところが、.text代入直後は.textLabel.frameは{0,0,0,0}であり、この時点で代入しても無効になる。
ではどうするかというと、KVOで.textLabel.frameを監視して、その代入があったタイミングで補正する。
こんな感じ。
――――――――
UIButtonWithLabelAlignment.h
――――――――
#import <UIKit/UIKit.h>
// 左寄せと上寄せがORで設定できる
enum {
kUIButtonWithLabelAlignment_normal=0,
kUIButtonWithLabelAlignment_left =(1<<0),
kUIButtonWithLabelAlignment_up =(1<<1),
};
@interface UIButtonWithLabelAlignment : UIButton
@property (nonatomic) NSInteger align;
@end
――――――――
UIButtonWithLabelAlignment.m
――――――――
#import "UIButtonWithLabelAlignment.h"
#define OBSERVE_FRAME @"titleLabel.frame"
@interface UIButtonWithLabelAlignment()
{
BOOL setKVO;
}
@end
@implementation UIButtonWithLabelAlignment
-(instancetype)initWithFrame:(CGRect)frame
{
self=[super initWithFrame:frame];
if (self) {
[self addObserver:self forKeyPath:OBSERVE_FRAME options:
NSKeyValueObservingOptionNew context:NULL];
setKVO=YES;
}
return self;
}
// KVOによる変更通知受信
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object
change:(NSDictionary *)change context:(void *)context
{
// 1つしか登録してないのでkeyPathのチェックは省略
// 即解除;そうしないと以下でframeを操作するとまた発生してしまうから
[self removeObserver:self forKeyPath:OBSERVE_FRAME];
setKVO=NO;
// 寄せる
UIButton *btn=(UIButton *)object;
CGRect frame=btn.titleLabel.frame;
if (self.align&kUIButtonWithLabelAlignment_left) {
frame.origin.x=0;
}
if (self.align&kUIButtonWithLabelAlignment_up) {
frame.origin.y=0;
}
btn.titleLabel.frame=frame;
}
-(void)dealloc
{
if (setKVO) {
// 外れてないのが残ってたら外す
self.titleLabel.frame=CGRectZero; // 上の通知でKVOを解除する
// これでは解除処理が終了するまでにdeallocが終了してしまうためエラーが発生する
// [self removeObserver:self forKeyPath:OBSERVE_FRAME];
}
}
ついでに書いておくと、[UIButton sizeToFit]するとボタン幅が表示幅に合わされるので中央寄せ=左寄せで問題ないが、.textLabelにtruncateを設定していると、その表示幅はボタンの幅より必ず狭くなるのでセンタリングが起こる。これはtruncateが発生するラベルと発生しないラベルを並べた場合には、表示位置がずれて見えるということを意味する。なお、truncateが発生したボタンにsizeToFitをかけるとtruncateが外れてしまう(全体が表示できる幅のボタンになる)のでやってはいけない。
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で再利用する場合には有り得る話となる。
要注意。
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月21日火曜日
Mac mini 2014の問題;BootCampは解決
Mac mini 2014を購入した。
壊れたMac mini 2011の代わりである。
で、いきなり色々と書いておかねばならないことがある。
(1)メモリーはユーザーで増設できない
もはや裏蓋すら開けることができない。
後で増設しようと思って8GBモデルにしたけど、こんな仕打ちとは。
(2)完全初期設定には有線マウスが必須
Bluetoothでも行けそうな画面が出てくるが、全く認識してくれなかったので有線マウスを使った。2回目以降ではBluetoothマウスが使える。なぜ?
(3)コネクタ間が狭くなっているので、接続できないことがある。特にHDMIとThunderboltの間が狭くなったため、干渉しやすい。HPの写真で見る限りmini2011と変わらないようだけど、実際にはぶつかってしまう。たぶん1ミリ以下だが狭くなったのだと思う。うちでは今まで使えてたHDMIケーブルがぶつかってしまい奥まで刺さらない。一応表示はできているけど。
(4)FireWire端子はない。
うちにはFireWire接続のHDDがあるためこの端子は必須で、Apple純正のThunderboltーFireWire変換ケーブルを買った。税込みで3000円ちょっと。これは予めわかっていたので買ってあった。このケーブル経由でのアクセスはMacでは全く問題ない。Windowsでは即認識されない。一旦再起動する必要がある(MacOS側で一度認識させるといいのかも)。
(5)初期設定は全て英語
全く困ったことに、初期設定の画面は全て英語である。それを翻訳しながらの作業になるので時間がかかった。TimeMachineからの戻しができるのでMacは移行が楽なのではあるけど。最初の選択画面で英語が選ばれていた模様。キーボードが効かなかったので適当に押していた時にそうなったか。キーボードを効かすにはスペースを押す。
(6)内蔵スピーカーからの音が割れる
物理的に共振している感じ。2011ではこんなことはなかった。
→起動音だけな感じ。
(7)BootCampでの注意
BootCampによるWindows7の導入時、USBに刺しておくメモリは、2014Yosemite上のBootCampで作ったWindowsのisoイメージ+αを入れたメモリだけにしなければならない。2011で作ったものは使えない。別のUSBメモリにドライバーを入れることもできるが、それはWindowsインストール中は刺していてはいけない。指していると、インストールの途中でエラーが発生しWindowsの導入に失敗する。
Windowsマシンでの導入失敗例を調べているうちに、「HDDを内蔵の他に接続していると失敗する」という症例を見つけ、まさかと思ってドライバの入ったUSBメモリを外したら行けた。しかし、Mac mini2011ではいけた気がしたのだが、それは起動できるUSBポートが1だけだという仕様だったからだろうか。いずれにせよ騒ぎ過ぎた。すまん。
なお、Bluetoothマウスを使っている場合、MacOS側とWindows側で別にペアリングを行う必要がある。その間は他方でマウスが使えないので有線マウスが必要となる。いろんな意味で、Macには有線マウスが必須。
(8)Thunderboltにディスプレイ以外の機器を繋いでいるとWindowsではスリープが使えなくなる
うちでは前述の通り変換ケーブルでFirewireのHDDを繋いでいるが、そうするとWindowsでスリープが使えない(休止は使える)。アップルの公式見解だから間違いない。変換ケーブルでDVIに繋いだモニタはたぶん大丈夫。それは2011でも繋いでたから。
問題は、Mac miniの電源ボタンが背面にあることで、休止にせよシャットダウンにせよ電源ボタンを押す必要があるから、今迄みたいにモニタの後ろに本体を置いておくと言うことが出来なくなった。
ということで、安いなりに手抜き、というか割り切った仕様の部分があるので要注意である。
・・・実際のところ性能はどうなのか・・・
で、mini2011と比較して2014はどうなのかについても軽く書いておく。
・静か
高負荷時でもファンの音が聞こえない。
筐体温度も低めな気がする。
・速くなったとは感じない
速度はCPUにせよGPUにせよ速くなったとは感じない。
エクスペリエンスインデックスではCPUが7.2、グラフィックが6.7と良い数値を出しているが、Windowsとしての動作は差を感じない。もっとも、2011では16GB積んでいたのが今回は8GBなので、その差を考えればやはり若干は速いのかもしれない。
という感じ。少なくとも、わざわざ2011から買い換える必要はないかと。
壊れたMac mini 2011の代わりである。
で、いきなり色々と書いておかねばならないことがある。
(1)メモリーはユーザーで増設できない
もはや裏蓋すら開けることができない。
後で増設しようと思って8GBモデルにしたけど、こんな仕打ちとは。
(2)完全初期設定には有線マウスが必須
Bluetoothでも行けそうな画面が出てくるが、全く認識してくれなかったので有線マウスを使った。2回目以降ではBluetoothマウスが使える。なぜ?
(3)コネクタ間が狭くなっているので、接続できないことがある。特にHDMIとThunderboltの間が狭くなったため、干渉しやすい。HPの写真で見る限りmini2011と変わらないようだけど、実際にはぶつかってしまう。たぶん1ミリ以下だが狭くなったのだと思う。うちでは今まで使えてたHDMIケーブルがぶつかってしまい奥まで刺さらない。一応表示はできているけど。
(4)FireWire端子はない。
うちにはFireWire接続のHDDがあるためこの端子は必須で、Apple純正のThunderboltーFireWire変換ケーブルを買った。税込みで3000円ちょっと。これは予めわかっていたので買ってあった。このケーブル経由でのアクセスはMacでは全く問題ない。Windowsでは即認識されない。一旦再起動する必要がある(MacOS側で一度認識させるといいのかも)。
(6)内蔵スピーカーからの音が割れる
物理的に共振している感じ。2011ではこんなことはなかった。
→起動音だけな感じ。
(7)BootCampでの注意
BootCampによるWindows7の導入時、USBに刺しておくメモリは、2014Yosemite上のBootCampで作ったWindowsのisoイメージ+αを入れたメモリだけにしなければならない。2011で作ったものは使えない。別のUSBメモリにドライバーを入れることもできるが、それはWindowsインストール中は刺していてはいけない。指していると、インストールの途中でエラーが発生しWindowsの導入に失敗する。
Windowsマシンでの導入失敗例を調べているうちに、「HDDを内蔵の他に接続していると失敗する」という症例を見つけ、まさかと思ってドライバの入ったUSBメモリを外したら行けた。しかし、Mac mini2011ではいけた気がしたのだが、それは起動できるUSBポートが1だけだという仕様だったからだろうか。いずれにせよ騒ぎ過ぎた。すまん。
なお、Bluetoothマウスを使っている場合、MacOS側とWindows側で別にペアリングを行う必要がある。その間は他方でマウスが使えないので有線マウスが必要となる。いろんな意味で、Macには有線マウスが必須。
(8)Thunderboltにディスプレイ以外の機器を繋いでいるとWindowsではスリープが使えなくなる
うちでは前述の通り変換ケーブルでFirewireのHDDを繋いでいるが、そうするとWindowsでスリープが使えない(休止は使える)。アップルの公式見解だから間違いない。変換ケーブルでDVIに繋いだモニタはたぶん大丈夫。それは2011でも繋いでたから。
問題は、Mac miniの電源ボタンが背面にあることで、休止にせよシャットダウンにせよ電源ボタンを押す必要があるから、今迄みたいにモニタの後ろに本体を置いておくと言うことが出来なくなった。
ということで、安いなりに手抜き、というか割り切った仕様の部分があるので要注意である。
・・・実際のところ性能はどうなのか・・・
で、mini2011と比較して2014はどうなのかについても軽く書いておく。
・静か
高負荷時でもファンの音が聞こえない。
筐体温度も低めな気がする。
・速くなったとは感じない
速度はCPUにせよGPUにせよ速くなったとは感じない。
エクスペリエンスインデックスではCPUが7.2、グラフィックが6.7と良い数値を出しているが、Windowsとしての動作は差を感じない。もっとも、2011では16GB積んでいたのが今回は8GBなので、その差を考えればやはり若干は速いのかもしれない。
という感じ。少なくとも、わざわざ2011から買い換える必要はないかと。
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を入れて、後で識別できるようにするといい。
どうやってもセルに不必要な表示が残るのでわかったことであった。
あくまで表示分だけであって、全体ではない。スクロールで表示外に出たセルは新しく入ってくる表示分に再利用される
表示外に出たセル→→→+
---------- |
表示内 |
|再利用
|
|
---------- |
表示内に入るセル←←←+|
(実際には表示街に出たセルがすぐ入ってくるセルになるわけではない。
逆スクロールを考慮してか、若干の猶予がある。)
再利用されるとき、セル内に配置されたラベルなどの内容は「そのまま」なので、新しい表示を作るには、
-(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年9月26日金曜日
Xcode6のバグ
ついでにXcode6のバグ(6.0.1〜6.1)。
(1)シミュレーターで時々一切のタッチを受け付けなくなる
動作してない気もする。
シミュレータの再起動で治る。6.1では発生していない。
(2)シミュレーターでキーボードが表示されない
beginFirstResponderしてもキーボードが表示されない。当然でキーボードが閉じるときの通知も来ないため、そういう処理をしていると異常になる。自作のものは仕事で作ったものも含めすべてアウトだった。
iOSシミュレーターからHardware→Keyboard→Connect Hardware Keyboardのチェックを外すと治る。Xcode5まではソフトウエアキーボードを出したままキーボードからの入力もできただけにものすごく面倒。
6.1でも同じ。
(3)UIButtonやUIImageViewに設定していた画像が外れてしまう
Images.xcassetを追加した時に発生するのかもしれない。
使えてた機能が使えなくなるので一番厄介。
(4)シミュレーターをiOS8にすると、UIWebView/WKWebViewがhttpに接続できない。
完全にiOS8シミュレーターのバグ。シミュレーター上のSafariでも同じ。
httpsにはつながる。
6.1ではSafariではhttpでも接続できるようになったが、UIWebView/WKWebViewは直ってない。
(5)IBでAutoresizeingの設定ができなくなることがある(矢印をクリックしても反応しない)。
一旦別の要素に切り替えて戻ってくると出来るようになる。
6.1でも治ってない。
(6)アプリ申請のためのVaridateをして、何らかの問題が発生したときのエラーメッセージが見当違いな物を出すことがある。
全く内容がわからないのでXcode5でやったらすぐわかった。メッセージが間違っているというか意味不明のが出てた。
(7)Commitのコメントを入力中に改行すると、カーソルが変な場所に飛んでしまう。
(8)シミュレーター内の設定でキーボードを選択しても反映されない
(設定してない=変更前のキーボードのままになる)
削除してても。
なんにしても、まだXcode5は外せない状況。アップルの製品はソフトもハードもバグや故障が多すぎ。もっと検証しろ、品質上げろ!
(1)シミュレーターで時々一切のタッチを受け付けなくなる
動作してない気もする。
シミュレータの再起動で治る。6.1では発生していない。
(2)シミュレーターでキーボードが表示されない
beginFirstResponderしてもキーボードが表示されない。当然でキーボードが閉じるときの通知も来ないため、そういう処理をしていると異常になる。自作のものは仕事で作ったものも含めすべてアウトだった。
iOSシミュレーターからHardware→Keyboard→Connect Hardware Keyboardのチェックを外すと治る。Xcode5まではソフトウエアキーボードを出したままキーボードからの入力もできただけにものすごく面倒。
6.1でも同じ。
(3)UIButtonやUIImageViewに設定していた画像が外れてしまう
Images.xcassetを追加した時に発生するのかもしれない。
使えてた機能が使えなくなるので一番厄介。
(4)シミュレーターをiOS8にすると、UIWebView/WKWebViewがhttpに接続できない。
完全にiOS8シミュレーターのバグ。シミュレーター上のSafariでも同じ。
httpsにはつながる。
6.1ではSafariではhttpでも接続できるようになったが、UIWebView/WKWebViewは直ってない。
(5)IBでAutoresizeingの設定ができなくなることがある(矢印をクリックしても反応しない)。
一旦別の要素に切り替えて戻ってくると出来るようになる。
6.1でも治ってない。
(6)アプリ申請のためのVaridateをして、何らかの問題が発生したときのエラーメッセージが見当違いな物を出すことがある。
全く内容がわからないのでXcode5でやったらすぐわかった。メッセージが間違っているというか意味不明のが出てた。
(7)Commitのコメントを入力中に改行すると、カーソルが変な場所に飛んでしまう。
(8)シミュレーター内の設定でキーボードを選択しても反映されない
(設定してない=変更前のキーボードのままになる)
削除してても。
なんにしても、まだXcode5は外せない状況。アップルの製品はソフトもハードもバグや故障が多すぎ。もっと検証しろ、品質上げろ!
iOS8のバグもしくは動作違い(随時更新)
iOS8で挙動がおかしくなった処理を書いておく。今のところ、多くは回避コードは見つかってない。
(1)UIWebViewでローカル=リソース内のHTMLファイルもしくはPDFを表示している中から外部へのリンクをクリックすると、処理から帰ってこない
ローカルからローカル、外部から外部は大丈夫みたい。
-(BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
{
return YES; // 常に内部で開く・・・リンクをクリックするとそこで処理が止まる
}
->Xcode6のバグへ移動
(2) MFMailComposeViewControllerでメイラーを表示すると、一瞬表示されるが、数秒で勝手にキャンセルがかかってしまう。
mail=[[MFMailComposeViewController alloc]init];
mail.mailComposeDelegate=self;
[mail setSubject:@"title"];
[mail setMessageBody:@"message" isHTML:NO];
[self presentViewController:mail animated:YES completion:nil];
以下のように書いているときは、閉じてしまう。 仮に閉じなかったとして、メイルは使える状態にはならない。
- (void)mailComposeController:(MFMailComposeViewController*)controller didFinishWithResult:(MFMailComposeResult)result error:(NSError*)error
{
[controller dismissViewControllerAnimated:YES completion:NULL];
controller=nil;
}
拙作ではぷろぐらま〜ず電卓がe-Mailが使えない状態になっている。
Zeroレコーダーは開発中に気がついたので、iOS8上ではe-Mailを使えないようにした。
シミュレータ上だけの問題みたい。iOS8.0.2の実機では、開く前に警告が出ているが送信はできる。
どちらもiOS7では全く問題ないのでiOS8のバグの可能性が高い。
(3)UIWebViewのデフォルト背景色が黒に変わった
これは多分設定でなんとかなるかと。やってないけど。
(4)iPadで、ActionSheetのデリゲートの中からAirPrintのポップオーバーが表示されない
iPhoneでは問題ない。
回避策は、UIActionSheetではなく、iOS8で追加されたUIAlertControllerを使うこと。
ActionSheetから表示しようとすると、Warning: Attempt to present <UINavigationController: 0x????????> on <表示しようとしているViewController: 0x????????> which is already presenting (null)
という警告が出て表示されない。全然わからなくて困っていたが、色々と表示条件を変えているうちにメッセージが変わり、上記のことがわかった。インターネットで調べても、stackoverflowを含めて見つからなかったので、世界初かも(^_^;)
UIAlertControllerはiOS8にはないので、バージョンを調べて処理を分ける必要がある。
また、iPhoneではUIActionSheetのままでも動くが、当然UIAlertControllerでも動く。
UIAlertControllerをiPadで使う場合は.popoverPresentationController.sourceViewと
.popoverPresentationController.batButtonItemまたは.sourceRectの設定が必須なので要注意。
メイラーの件はUIAlertControllerを使っても解決しない。
(5)WKWebViewでPDF内リンクが処理されない
PDF内にリンクがあった場合、UIWebViewではリンクが処理されるが、WKWebViewではタップが完全に無視される。回避策はない。リンク付きPDFを処理したいなら、現状WKWebViewは使えない。PDFの内部構造を解析してリンク情報を取り出し、別途ボタンを作って飛ばすとかは出来るんだけど、面倒。
(6)Tabbarでその他に入ったものから開いたViewControllerがTableViewを含む場合、
ステータスバーの部分をタップしても先頭までスクロールしない。
iOS7.1では発生、以前からそうなのかは不明。iOS8.1では修正されている。あっ、これはiOS8のバグじゃないね。
(7)NSAttributedStringでNSLinkAttributeNameを使った場合、下線が出ないことがある。出ない時はNSUnderlineStyleAttributeNameをつけても無視される。
出ることもある。truncateがあるときは表示されない。iOS7では問題ない。
->Xcode6のバグへ移動
(2) MFMailComposeViewControllerでメイラーを表示すると、一瞬表示されるが、数秒で勝手にキャンセルがかかってしまう。
mail=[[MFMailComposeViewController alloc]init];
mail.mailComposeDelegate=self;
[mail setSubject:@"title"];
[mail setMessageBody:@"message" isHTML:NO];
[self presentViewController:mail animated:YES completion:nil];
以下のように書いているときは、閉じてしまう。 仮に閉じなかったとして、メイルは使える状態にはならない。
- (void)mailComposeController:(MFMailComposeViewController*)controller didFinishWithResult:(MFMailComposeResult)result error:(NSError*)error
{
[controller dismissViewControllerAnimated:YES completion:NULL];
controller=nil;
}
シミュレータ上だけの問題みたい。iOS8.0.2の実機では、開く前に警告が出ているが送信はできる。
どちらもiOS7では全く問題ないのでiOS8のバグの可能性が高い。
(3)UIWebViewのデフォルト背景色が黒に変わった
これは多分設定でなんとかなるかと。やってないけど。
(4)iPadで、ActionSheetのデリゲートの中からAirPrintのポップオーバーが表示されない
iPhoneでは問題ない。
回避策は、UIActionSheetではなく、iOS8で追加されたUIAlertControllerを使うこと。
ActionSheetから表示しようとすると、Warning: Attempt to present <UINavigationController: 0x????????> on <表示しようとしているViewController: 0x????????> which is already presenting (null)
という警告が出て表示されない。全然わからなくて困っていたが、色々と表示条件を変えているうちにメッセージが変わり、上記のことがわかった。インターネットで調べても、stackoverflowを含めて見つからなかったので、世界初かも(^_^;)
UIAlertControllerはiOS8にはないので、バージョンを調べて処理を分ける必要がある。
また、iPhoneではUIActionSheetのままでも動くが、当然UIAlertControllerでも動く。
UIAlertControllerをiPadで使う場合は.popoverPresentationController.sourceViewと
.popoverPresentationController.batButtonItemまたは.sourceRectの設定が必須なので要注意。
メイラーの件はUIAlertControllerを使っても解決しない。
(5)WKWebViewでPDF内リンクが処理されない
PDF内にリンクがあった場合、UIWebViewではリンクが処理されるが、WKWebViewではタップが完全に無視される。回避策はない。リンク付きPDFを処理したいなら、現状WKWebViewは使えない。PDFの内部構造を解析してリンク情報を取り出し、別途ボタンを作って飛ばすとかは出来るんだけど、面倒。
(6)Tabbarでその他に入ったものから開いたViewControllerがTableViewを含む場合、
ステータスバーの部分をタップしても先頭までスクロールしない。
iOS7.1では発生、以前からそうなのかは不明。iOS8.1では修正されている。あっ、これはiOS8のバグじゃないね。
(7)NSAttributedStringでNSLinkAttributeNameを使った場合、下線が出ないことがある。出ない時はNSUnderlineStyleAttributeNameをつけても無視される。
出ることもある。truncateがあるときは表示されない。iOS7では問題ない。
2014年9月14日日曜日
iOS7でのdrawAtPoint/drawInRectについて
iOS6までに存在したNSStringの
- (CGSize)drawAtPoint:(CGPoint)point withFont:(UIFont *)font
- (CGSize)drawInRect:(CGRect)rect withFont:(UIFont *)font
はiOS7で非推奨になった。
代替メソッドとしては
- (void)drawAtPoint:(CGPoint)point withAttributes:(NSDictionary *)attrs
- (void)drawInRect:(CGRect)rect withAttributes:(NSDictionary *)attrs
が紹介されているが、実はこれらには描画色に関して重大な違いがある。
代替メソッドについて書かれてるサイトはあったけど描画色について書かれているところは見つけられなかったのでここに書いておく。
iOS6の
- (CGSize)drawAtPoint:(CGPoint)point withFont:(UIFont *)font
- (CGSize)drawInRect:(CGRect)rect withFont:(UIFont *)font
は[UIColor set]で設定された現在の描画色で描画されるが、iOS7の
- (void)drawAtPoint:(CGPoint)point withAttributes:(NSDictionary *)attrs
- (void)drawInRect:(CGRect)rect withAttributes:(NSDictionary *)attrs
はそれでは描画されない。常に黒になる。
ではどうやって描画色を指定するかといえば、attrsに入れる。すなわち、
NSDictionary *atrb=@{
NSFontAttributeName : font,
NSForegroundColorAttributeName:color
};
[self drawAtPoint:point withAttributes:atrb];
である。
本当なら[UIColor set]で設定された現在の描画色を得る方法があれば、それを読み取ってcolorに入れればいいが、その方法がないので、、描画色は他に変数に入れて管理しておく必要がある。
アップルはこういう重大な変更を平気でするから困る。変えるなら変えるでちゃんと告知して、正確な代替案を出してほしい。このことがわかるのに半日もかかってしまった。
X-BASIC for iOSの次バージョンは、この問題を超えて発表まであと少し。今回は機能追加なし。
- (CGSize)drawAtPoint:(CGPoint)point withFont:(UIFont *)font
- (CGSize)drawInRect:(CGRect)rect withFont:(UIFont *)font
はiOS7で非推奨になった。
代替メソッドとしては
- (void)drawAtPoint:(CGPoint)point withAttributes:(NSDictionary *)attrs
- (void)drawInRect:(CGRect)rect withAttributes:(NSDictionary *)attrs
が紹介されているが、実はこれらには描画色に関して重大な違いがある。
代替メソッドについて書かれてるサイトはあったけど描画色について書かれているところは見つけられなかったのでここに書いておく。
iOS6の
- (CGSize)drawAtPoint:(CGPoint)point withFont:(UIFont *)font
- (CGSize)drawInRect:(CGRect)rect withFont:(UIFont *)font
は[UIColor set]で設定された現在の描画色で描画されるが、iOS7の
- (void)drawAtPoint:(CGPoint)point withAttributes:(NSDictionary *)attrs
- (void)drawInRect:(CGRect)rect withAttributes:(NSDictionary *)attrs
はそれでは描画されない。常に黒になる。
ではどうやって描画色を指定するかといえば、attrsに入れる。すなわち、
NSDictionary *atrb=@{
NSFontAttributeName : font,
NSForegroundColorAttributeName:color
};
[self drawAtPoint:point withAttributes:atrb];
である。
本当なら[UIColor set]で設定された現在の描画色を得る方法があれば、それを読み取ってcolorに入れればいいが、その方法がないので、、描画色は他に変数に入れて管理しておく必要がある。
アップルはこういう重大な変更を平気でするから困る。変えるなら変えるでちゃんと告知して、正確な代替案を出してほしい。このことがわかるのに半日もかかってしまった。
X-BASIC for iOSの次バージョンは、この問題を超えて発表まであと少し。今回は機能追加なし。
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;
}
とすればよい。
よくよく調べると、
・文字列中に半角文字が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の勉強にはなったのでいいけど。
一応ライブラリの作成先に連絡を入れるつもりだけど、果たして直してくれるかどうか。
→その後の調査の結果、ライブラリのバグではなく、ライブラリに必要なファイルが足りてなかったからと判明。しかし、それはそれで「ファイルがないときはエラーを出す」処理が抜けているという問題ありコードだと思うのだが。エラー処理がいかにちゃんと実装されているかが、ライブラリの完成度の尺度。
落ちるときのログから判断するに、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の勉強にはなったのでいいけど。
一応ライブラリの作成先に連絡を入れるつもりだけど、果たして直してくれるかどうか。
→その後の調査の結果、ライブラリのバグではなく、ライブラリに必要なファイルが足りてなかったからと判明。しかし、それはそれで「ファイルがないときはエラーを出す」処理が抜けているという問題ありコードだと思うのだが。エラー処理がいかにちゃんと実装されているかが、ライブラリの完成度の尺度。
2014年7月15日火曜日
Xcode5、そのコンパイラーの64ビットにおけるBOOLの扱いについて
Xcode5上のObjective-Cで、32ビットの時は正常なのに、64ビットでだけおかしくなるというバグの解析依頼が来た。
64ビットでおかしくなるのは、主にビット幅による物が多いが、今回はそういうのではなさそうで、ぱっと考えられる原因がなくて難航した。
おかしくなる場所を絞り込んでいった結果、以下のような意外な原因だったので、報告する。
・・・
BOOLで宣言した変数に、YES/NO以外の整数値を入れた場合、その読み出し値が32ビットと64ビットで異なる。
「例」
BOOL flag=2;
本来このような記述そのもの間違いだが、プログラムの開発過程でYES/NOだけでは不十分になって他の値を代入するように変更することはまれにあり、そのときに変数の型を変更し忘れることも(私自身は経験ないが)、あるかもしれない。
このような代入があっても、コンパイラは標準ではエラーにしない(警告もださない)。Objective-CではBOOLはint(charかも)と同等だからである。なので、実行時も書き込みも読み出しも、見かけ上は正常である。
32ビット環境下では、flagを読み出すと2と読める。
ところが、64ビット環境下ではBOOL値に変換される、2は!=0なのでYESとなり1と読み出される。
(読み出し時に変換されているか、書き込み時にすでに変換されているかは不明である。)
すべての場合にそうなるかは未検証だが、少なくともそうなってしまうことがあるというのは確かである。
32ビットと64ビットでのコンパイラの挙動の違いである。
隠れたバグになるので、64ビット対応時には注意。
64ビットでおかしくなるのは、主にビット幅による物が多いが、今回はそういうのではなさそうで、ぱっと考えられる原因がなくて難航した。
おかしくなる場所を絞り込んでいった結果、以下のような意外な原因だったので、報告する。
・・・
BOOLで宣言した変数に、YES/NO以外の整数値を入れた場合、その読み出し値が32ビットと64ビットで異なる。
「例」
BOOL flag=2;
本来このような記述そのもの間違いだが、プログラムの開発過程でYES/NOだけでは不十分になって他の値を代入するように変更することはまれにあり、そのときに変数の型を変更し忘れることも(私自身は経験ないが)、あるかもしれない。
このような代入があっても、コンパイラは標準ではエラーにしない(警告もださない)。Objective-CではBOOLはint(charかも)と同等だからである。なので、実行時も書き込みも読み出しも、見かけ上は正常である。
32ビット環境下では、flagを読み出すと2と読める。
ところが、64ビット環境下ではBOOL値に変換される、2は!=0なのでYESとなり1と読み出される。
(読み出し時に変換されているか、書き込み時にすでに変換されているかは不明である。)
すべての場合にそうなるかは未検証だが、少なくともそうなってしまうことがあるというのは確かである。
32ビットと64ビットでのコンパイラの挙動の違いである。
隠れたバグになるので、64ビット対応時には注意。
2014年3月8日土曜日
クラスの内容をファイルに出力する方法(4)
最後にヘッダー。
で、こんな感じで使う。
// // saveClass // // (C) 2014 by AIG-Soft // under Apache License #import <Foundation/Foundation.h> BOOL saveClass( id object,NSString *basePath); BOOL deleteClass(id object,NSString *basePath); BOOL loadClass( id object,NSString *basePath); NSArray *propertyNames(id object); NSArray *memberNames(id object); NSDictionary *propertiesAttributes(id object); NSDictionary *memberAttributes(id object);
で、こんな感じで使う。
propertyNames(self); propertiesAttributes(self); memberNames(self); memberAttributes(self); saveClass( self, makeDocumentsPath()); loadClass( self, makeDocumentsPath()); deleteClass(self, makeDocumentsPath());ちなみにmakeDocumentsPath()はこんな関数。
NSString *makeDocumentsPath(void)
// ディレクトリ「Documents」のフルパスを得る
// iTunesからのデータやりとりもここ
{
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
// paths[0]を取り出す;そこにディレクトリ名が入っているらしい
NSString *documentsDirectory = (([paths count] > 0)? // 見つかったら
/* これ↑は単なるポインタ参照 */
paths[0] // 最初のもの
: NSTemporaryDirectory() ); // なければ一時ディレクトリ
return (documentsDirectory);
}
使って頂いて、デバッグや改良点などあれば教えていただければありがたいかと。2014年3月7日金曜日
クラスの内容をファイルに出力する方法(3)
さて、先の入出力関数の中では、NSArrayの中に別のクラスが存在する場合はそのまま処理できない。
それも入出力する場合は、そのクラスの中に少し処理を追加する必要がある。
追加するのはcoderという処理。これを入れるとクラスをNSDataにシリアライズして入出力出来るようになる。
以下のように実装する。
上記のメソッドを、出力したいクラスの@implementationの中に追加する。
なお、coderはカテゴリでは実装できない(無視される)。
また、coderを実装しただけでは[ary writeToFile]では出力されない様子である。
それも入出力する場合は、そのクラスの中に少し処理を追加する必要がある。
追加するのはcoderという処理。これを入れるとクラスをNSDataにシリアライズして入出力出来るようになる。
以下のように実装する。
こんなクラスの場合↓
@interface TestClass : NSObject
{
int testInt;
float testFloat;
NSArray *testArray;
}
こういうふうに実装する↓。
- (void)encodeWithCoder:(NSCoder *)aCoder
{
// NSLog(@"encodeWithCoder");
// メンバーのそれぞれをencode*:forKeyで変換する
// キー名は適当でいいけど、基本はメンバー名と同じにすればいいと思う
[aCoder encodeObject:testArray forKey:@"testArray"];
[aCoder encodeInt:testInt forKey:@"testInt"];
[aCoder encodeDouble:testFloat forKey:@"testFloat"];
}
- (id)initWithCoder:(NSCoder *)aDecoder
{
// NSLog(@"initWithCoder");
self = [super init];
if (self!=nil) {
// メンバーのそれぞれをdecode*ForKeyで逆変換する
// キー名はencodeで指定したもの
testArray = [aDecoder decodeObjectForKey: @"testArray"];
testInt = [aDecoder decodeIntegerForKey:@"testInt"];
testFloat = [aDecoder decodeDoubleForKey: @"testFloat"];
}
return self;
}
-(NSString *)description
// %@で表示するための文字列を返す処理
// これは必須ではない
{
return [NSString stringWithFormat:
@"< %@ : %p >"
@"testArray=%@ ,"
@"testInt=%u ,"
@"testFloat=%f ,"
,NSStringFromClass([self class]),self
,testArray
,testInt
,testFloat
];
// 再帰呼び出しになるので、"%@",selfは記述してはいけない
}
上記のメソッドを、出力したいクラスの@implementationの中に追加する。
なお、coderはカテゴリでは実装できない(無視される)。
また、coderを実装しただけでは[ary writeToFile]では出力されない様子である。
2014年3月6日木曜日
クラスの内容をファイルに出力する方法(2)
ということで、その本体のソース。
公開にあたって、一部書き換えたので動かなくなってたらごめんなさい。
「確認してから出せ」ッて言われそうだけど、時間がなくて。
公開にあたって、一部書き換えたので動かなくなってたらごめんなさい。
「確認してから出せ」ッて言われそうだけど、時間がなくて。
//
// saveClass.m : 全クラス内容を得る、保存する
//
// (C) 2014 by AIG-Soft
// under Apache License
/* この辺りを参照
クラスのメンバ名を文字列で指定する
http://program.station.ez-net.jp/special/handbook/objective-c/id/ivar.asp
オブジェクトが持つプロパティの型と名前のリストを取得する
http://d.hatena.ne.jp/shu223/20120226/1330231240
クラス名を取得する
http://lab.dolice.net/blog/2013/04/17/objc-ns-string-from-class/
シリアライズする
http://nagano.monalisa-au.org/archives/64
*/
#import "saveClass.h"
#import "objc/runtime.h"
#include <sys/stat.h> // mkdir()
#include <sys/types.h> // mkdir()のmode
#include <unistd.h> // rmdir()
#define MAX_CLASS_NAME (128) // クラス名はこれbytes以下にすること
//--------------------------------------------------------------
static BOOL saveClassSub(id object,Ivar *ivars,unsigned int cnt,NSString *basePath)
// 全メンバーの内容を保存する
// メモリ確保の関係でサブルーチンにする
// 未対応型クラスは実装を追加すること
{
NSLog(@"saveClassSub:%d",cnt);
for (int i = 0; i < cnt; i++) {
// 属性取得
const char *encode = ivar_getTypeEncoding(ivars[i]);
const char *name = ivar_getName(ivars[i]);
// パス名=basePath/クラス名/プロパティ
NSString *path=[NSString stringWithFormat:@"%@/%@/%s",basePath,NSStringFromClass([object class]),name];
NSLog(@"パス名=%@",path);
// 型別出力処理
FILE *fp;
// クラスの場合はwriteToFileを使うが、ファイルはとりあえずopenしておく。これにより、内容がnilだった場合も空ファイルができるようになる。
fp=fopen(nsStringTocString(path),"w");
if (fp==NULL) {
// ファイルが作成できない
NSLog(@"ファイルが作成できない");
return(NO);
}
switch (encode[0]) {
default:
NSLog(@"不明型:%s",encode);
break;
// 小文字はsigned,大文字はunsigned
case 'c':
case 'C': // char系(BOOL/char)
{
unsigned char result;
object_getInstanceVariable(object, name, (void**)&result);
fwrite(&result,sizeof(result),1,fp);
NSLog(@"char系:%d/%u",result,result);
}
break;
case 's':
case 'S': // short系
{
unsigned short result;
object_getInstanceVariable(object, name, (void**)&result);
fwrite(&result,sizeof(result),1,fp);
NSLog(@"short系:%d/%u",result,result);
}
break;
case 'i':
case 'I': // interger系
{
unsigned int result;
object_getInstanceVariable(object, name, (void**)&result);
fwrite(&result,sizeof(result),1,fp);
NSLog(@"interger系:%d/%u",result,result);
}
break;
case 'l':
case 'L': // long系 : iOSではintと同じはずだけど念のため
{
unsigned long result;
object_getInstanceVariable(object, name, (void**)&result);
fwrite(&result,sizeof(result),1,fp);
NSLog(@"long系:%ld/%lu",result,result);
}
break;
case 'q':
case 'Q': // long long
{
unsigned long long result;
#if 0
// 64bitでは以下の方法は正常動作しない
// http://stackoverflow.com/questions/1219081/object-getinstancevariable-works-for-float-int-bool-but-not-for-double
object_getInstanceVariable(object, name, (void**)&result);
#else
// なので、一旦メンバーの存在位置を取得して、そこから直接読み出す方法を使う
Ivar ivar = object_getInstanceVariable(object, name, NULL);
if (ivar) {
result= *(long long*)((char *)object + ivar_getOffset(ivar));
}
#endif
fwrite(&result,sizeof(result),1,fp);
NSLog(@"long long系:%lld/%llu",result,result);
}
break;
case 'f': // float
{
float result;
object_getInstanceVariable(object, name, (void**)&result);
fwrite(&result,sizeof(result),1,fp);
NSLog(@"float:%f",result);
}
break;
case 'd': // double
{
double result;
#if 0
// 64bitでは以下の方法は正常動作しない
object_getInstanceVariable(object, name, (void**)&result);
#else
// なので、一旦メンバーの存在位置を取得して、そこから直接読み出す方法を使う
Ivar ivar = object_getInstanceVariable(object, name, NULL);
if (ivar) {
result= *(double *)((char *)object + ivar_getOffset(ivar));
}
#endif
fwrite(&result,sizeof(result),1,fp);
NSLog(@"double=%.15f",result);
}
break;
// 汎用化を進めるときは以下に出力処理を追記すること
case '@': // クラス名
if (encode[1]=='"') {
// 以下に"クラス名"がある
// クラス名のみ切り出す
char className[MAX_CLASS_NAME+1]; // +1 for EOS
int j=0;
char c;
while ((c=encode[2+j])!='"') {
className[j]=c;
if (++j>=MAX_CLASS_NAME) {
break;
}
}
className[j]='\0';
NSLog(@"クラス=%s",className);
if (strcmp(className,"NSString")==0||strcmp(className,"NSMutableString")==0) {
// クラスはfopen()に頼らない出力をするので閉じてしまう
// これでとりあえずサイズ0のファイルができるので、オブジェクトの実体がnilの場合はそれが残る。
fclose(fp);
fp=NULL; // 閉じた印
//
NSString *str;
object_getInstanceVariable(object, name, (void**)&str);
NSLog(@"NSString=%@",str);
if (str!=nil) {
NSError *error=nil;
if (![str writeToFile:path atomically:YES encoding:NSUTF8StringEncoding error:&error]) return(NO);
}
break;
}
else
if (strcmp(className,"NSData") ==0||strcmp(className,"NSMutableData") ==0) {
// クラスはfopen()に頼らない出力をするので閉じてしまう
// これでとりあえずサイズ0のファイルができるので、オブジェクトの実体がnilの場合はそれが残る。
fclose(fp);
fp=NULL; // 閉じた印
//
NSData *data;
object_getInstanceVariable(object, name, (void**)&data);
// NSLog(@"NSData=%@",data);
if (data!=nil) {
if (![data writeToFile:path atomically:YES]) return(NO);
}
break;
}
else
if (strcmp(className,"NSArray") ==0||strcmp(className,"NSMutableArray") ==0) {
// クラスはfopen()に頼らない出力をするので閉じてしまう
// これでとりあえずサイズ0のファイルができるので、オブジェクトの実体がnilの場合はそれが残る。
fclose(fp);
fp=NULL; // 閉じた印
//
NSArray *ary;
object_getInstanceVariable(object, name, (void**)&ary);
NSLog(@"NSArray=%@",ary);
if (ary!=nil) {
#if 0
// aryの中にwriteToFileをサポートしてないクラスがあると失敗するので、無視するためエラーは取らない
// if (!
[ary writeToFile:path atomically:YES]
;
// ) return(NO);
#else
// aryの中に別のクラスが存在し、それも出力したい場合は、coderを実装した上、NSDataにシリアライズして出力する
// 注意
// 1.coderを実装しただけでは[ary writeToFile]では出力されない様子
// 2.coderはカテゴリでは実装できない(無視される)
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:ary];
// NSLog(@"data=%@",data);
[data writeToFile:path atomically:YES];
#endif
}
break;
}
else
if (strcmp(className,"NSDate") ==0) {
NSDate *date;
object_getInstanceVariable(object, name, (void**)&date);
NSLog(@"NSDate=%@",date);
if (date!=nil) {
NSTimeInterval dt=[date timeIntervalSince1970]; // 1970/1/1からの相対時間(数値)に変換する
fwrite(&dt,sizeof(dt),1,fp);
NSLog(@"dt=%f",dt);
}
break;
}
else {
NSLog(@"未サポートクラスは出力しない");
// NSSetはwriteToFileがない
}
} else {
// クラス名文字列がないときはid型
// 型が特定できないので出力対象としない
NSLog(@"id型は出力しない");
}
break;
}
if (fp!=NULL) fclose(fp);
// printf("型=%s/名前=%s\n",attributes,property_getName(properties[i]));
}
// 全出力終了
return(YES);
}
// basePath=makeDocumentsPath();
BOOL saveClass(id object,NSString *basePath)
// 指定オブジェクトの全メンバーの内容を保存する
// object : 対応オブジェクト
// basePath : 保存フォルダ(この下にクラス名フォルダが出来、その下にクラス名別ファイルが作成される)
{
NSLog(@"saveClass:%@",object);
// 全メンバ情報を得る
unsigned int cnt;
Ivar *ivar = class_copyIvarList([object class], &cnt);
//
// 先に保存ディレクトリを作成する : basePath/クラス名
NSString *dir=[NSString stringWithFormat:@"%@/%@",basePath,NSStringFromClass([object class])];
NSLog(@"保存ディレクトリ:%@",dir);
#if 1
mkdir(nsStringTocString(dir),S_IRUSR|S_IWUSR|S_IXUSR|S_IRGRP|S_IWGRP|S_IXGRP|S_IROTH|S_IXOTH|S_IXOTH);
#else
NSFileManager *fileManager = [NSFileManager defaultManager];
NSError *error=nil;
[fileManager createDirectoryAtPath:dir withIntermediateDirectories:YES attributes:nil error:&error];
#endif
// ここで失敗してディレクトリが出来なくても、あとのファイル書き出しでエラーが出るのでとりあえず無視する
// 既存の場合のエラーコードがわからないので。
//
BOOL ret=saveClassSub(object,ivar,cnt,basePath);
//
free(ivar);
NSLog(@"---------------");
return (ret);
}
//--------------------------------------------------------------
// saveとloadは必ず同じ内容のクラスで行うこと。
// 変わっている場合、動作は保証されない。
static BOOL loadClassSub(id object,Ivar *ivars,unsigned int cnt,NSString *basePath)
// 全メンバーの内容を復帰する
// メモリ確保の関係でサブルーチンにする
// 未対応型クラスは実装を追加すること
{
NSLog(@"loadClassSub:%d",cnt);
for (int i = 0; i < cnt; i++) {
// 属性取得
const char *encode = ivar_getTypeEncoding(ivars[i]);
const char *name = ivar_getName(ivars[i]);
// パス名=basePath/クラス名/メンバ名
NSString *path=[NSString stringWithFormat:@"%@/%@/%s",basePath,NSStringFromClass([object class]),name];
NSLog(@"パス名=%@",path);
// 型別読み込み処理
// 出力時に型までは保存してないので、同名の別クラスがあったりすると誤動作する
FILE *fp;
fp=fopen(nsStringTocString(path),"r");
if (fp==NULL) {
// ファイルがない
continue; // 無視するだけ
}
switch (encode[0]) {
default:
NSLog(@"不明型:%s",encode);
break;
// 小文字はsigned,大文字はunsigned
case 'c':
case 'C': // char系(BOOL/char)
{
unsigned char result;
if (fread(&result,sizeof(result),1,fp)<1) result=0;
object_setInstanceVariable(object, name, (void*)&result);
NSLog(@"char系:%d/%u",result,result);
}
break;
case 's':
case 'S': // short系
{
unsigned short result;
fread(&result,sizeof(result),1,fp);
object_setInstanceVariable(object, name, (void*)&result);
NSLog(@"short系:%d/%u",result,result);
}
break;
case 'i':
case 'I': // interger系
{
unsigned int result;
if (fread(&result,sizeof(result),1,fp)<1) result=0;
object_setInstanceVariable(object, name, (void*)&result);
NSLog(@"interger系:%d/%u",result,result);
}
break;
case 'l':
case 'L': // long系 : iOSではintと同じはずだけど念のため
{
unsigned long result;
if (fread(&result,sizeof(result),1,fp)<1) result=0;
object_setInstanceVariable(object, name, (void*)&result);
NSLog(@"long系:%ld/%lu",result,result);
}
break;
case 'q':
case 'Q': // long long
{
unsigned long long result;
if (fread(&result,sizeof(result),1,fp)<1) result=0;
#if 0
// 64bitでは以下の方法は正常動作しない
object_setInstanceVariable(object, name, (void*)&result);
#else
// なので、一旦メンバーの存在位置を取得して、そこから直接読み出す方法を使う
Ivar ivar = object_getInstanceVariable(object, name, NULL);
if (ivar) {
*(long long *)((char *)object + ivar_getOffset(ivar))=result;
}
#endif
NSLog(@"long long系:%lld/%llu",result,result);
}
break;
case 'f': // float
{
float result;
if (fread(&result,sizeof(result),1,fp)<1) result=0;
object_setInstanceVariable(object, name, (void*)&result);
NSLog(@"float:%f",result);
}
break;
case 'd': // double
{
double result=0;
if (fread(&result,sizeof(result),1,fp)<1) result=0;
#if 0
// 64bitでは以下の方法は正常動作しない
object_setInstanceVariable(object, name, (void*)&result);
#else
// なので、一旦メンバーの存在位置を取得して、そこから直接読み出す方法を使う
Ivar ivar = object_getInstanceVariable(object, name, NULL);
if (ivar) {
*(double *)((char *)object + ivar_getOffset(ivar))=result;
}
#endif
NSLog(@"double=%.15f",result);
// doubleの表示は%f。しかし、標準ではfloatと同じ精度(7桁)までしか表示しないので、double精度(15桁)表示させるには上記のように桁数を指定する必要がある。
// ただし、15桁は少数以下だけでなく整数位も含めた全体なので注意。要するにこの記述は必ずしも正しくはない。
}
break;
// 汎用化を進めるときは以下に読み込み処理を追記すること
case '@': // クラス名
if (encode[1]=='"') {
// 以下に"クラス名"がある
// クラス名のみ切り出す
char className[MAX_CLASS_NAME+1]; // +1 for EOS
int j=0;
char c;
while ((c=encode[2+j])!='"') {
className[j]=c;
if (++j>=MAX_CLASS_NAME) {
break;
}
}
className[j]='\0';
NSLog(@"クラス=%s",className);
if (strcmp(className,"NSString")==0||strcmp(className,"NSMutableString")==0) {
NSError *error=nil;
NSString *str=[NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:&error];
if (error==nil) {
str=nil;
}
object_setInstanceVariable(object, name, (void*)str);
NSLog(@"NSString=%@",str);
break;
}
else
if (strcmp(className,"NSData") ==0||strcmp(className,"NSMutableData") ==0) {
NSData *data=[NSData dataWithContentsOfFile:path];
object_setInstanceVariable(object, name, (void*)data);
NSLog(@"NSData=%@",data);
break;
}
else
if (strcmp(className,"NSArray") ==0||strcmp(className,"NSMutableArray") ==0) {
#if 1
NSData *data = [NSData dataWithContentsOfFile:path];
NSArray *ary = [NSKeyedUnarchiver unarchiveObjectWithData:data];
#else
NSArray *ary=[NSArray arrayWithContentsOfFile:path];
#endif
object_setInstanceVariable(object, name, (void*)ary);
NSLog(@"NSArray=%@",ary);
break;
}
else
if (strcmp(className,"NSDate") ==0) {
NSTimeInterval dt; // 1970/1/1からの相対時間(数値)で保存されている
if (fread(&dt,sizeof(dt),1,fp)<1) dt=0;
NSDate *date=[NSDate dateWithTimeIntervalSince1970:dt];
object_setInstanceVariable(object, name, (void*)date);
NSLog(@"dt=%f/date=%@",dt,date);
break;
}
else {
NSLog(@"未サポートクラスは読み込まない");
}
} else {
// クラス名文字列がないときはid型
// 型が特定できないので読み込み対象としない
NSLog(@"id型は読み込めない");
}
break;
}
fclose(fp);
// printf("型=%s/名前=%s\n",attributes,property_getName(properties[i]));
}
// 全読み込み終了
return(YES);
}
BOOL loadClass(id object,NSString *basePath)
// 指定オブジェクトの全メンバーの内容を復帰する
// object : 対応オブジェクト
// basePath : 保存フォルダ(この下にクラス名フォルダがあり、その下にクラス名別ファイルがあること)
{
NSLog(@"loadClass:%@",object);
// 全メンバ情報を得る
unsigned int cnt;
Ivar *ivar = class_copyIvarList([object class], &cnt);
//
BOOL ret=loadClassSub(object,ivar,cnt,basePath);
//
free(ivar);
NSLog(@"---------------");
return (ret);
}
//--------------------------------------------------------------
static void delete1(id object,NSString *basePath,const char *name)
// 1ファイル削除
{
NSString *path=[NSString stringWithFormat:@"%@/%@/%s",basePath,NSStringFromClass([object class]),name];
unlink(nsStringTocString(path));
}
static BOOL deleteClassSub(id object,Ivar *ivars,unsigned int cnt,NSString *basePath)
// 全メンバー名ファイルを削除する
// メモリ確保の関係でサブルーチンにする(deleteでは関係ないけど他と合わせている)
{
NSLog(@"deleteClassSub:%d",cnt);
for (int i = 0; i < cnt; i++) {
delete1(object,basePath,ivar_getName(ivars[i]));
}
// 全出力終了
return(YES);
}
BOOL deleteClass(id object,NSString *basePath)
// 指定オブジェクトの全メンバーの内容ファイルを削除する
// object : 対応オブジェクト
// basePath : 保存フォルダ(この下にクラス名フォルダがあり、その下にクラス名別ファイルがあること)
// クラス名ディレクトリも消す
{
NSLog(@"deleteClass:%@",object);
// 全メンバ情報を得る
unsigned int cnt;
Ivar *ivar = class_copyIvarList([object class], &cnt);
//
BOOL ret=deleteClassSub(object,ivar,cnt,basePath);
//
// 保存ディレクトリも削除する : basePath/クラス名
NSString *dir=[NSString stringWithFormat:@"%@/%@",basePath,NSStringFromClass([object class])];
rmdir(nsStringTocString(dir));
// unlink(nsStringTocString(dir)); // これではディレクトリは削除できない
NSLog(@"削除ディレクトリ:%@",dir);
//
free(ivar);
NSLog(@"---------------");
return (ret);
}
//--------------------------------------------------------------
#if 0
static const char * getPropertyType(objc_property_t property)
// 正しく取り出せないことがあるので封印(未解析)
{
const char *attributes = property_getAttributes(property);
printf("型=%s\n",attributes);
char buffer[1 + strlen(attributes)];
strcpy(buffer, attributes);
char *state = buffer, *attribute;
while ((attribute = strsep(&state, ",")) != NULL) {
if (attribute[0] == 'T' && attribute[1] != '@') {
return (const char *)[[NSData dataWithBytes:(attribute + 1) length:strlen(attribute) - 1] bytes];
}
else if (attribute[0] == 'T' && attribute[1] == '@' && strlen(attribute) == 2) {
return "id";
}
else if (attribute[0] == 'T' && attribute[1] == '@') {
return (const char *)[[NSData dataWithBytes:(attribute + 3) length:strlen(attribute) - 4] bytes];
}
}
return "";
}
#endif
NSArray *propertyNames(id object)
// オブジェクトの全プロパティ名を得る
// !=メンバ名なので注意
{
unsigned int cnt;
NSMutableArray *ary = [NSMutableArray array];
objc_property_t *properties = class_copyPropertyList([object class], &cnt);
for (int i = 0; i < cnt; i++) {
objc_property_t property = properties[i];
const char *name = property_getName(property);
if (name) {
[ary addObject:CstringToNSString(name)];
}
}
free(properties);
NSLog(@"プロパティ群=%@",ary);
return ary;
}
NSArray *memberNames(id object)
// オブジェクトの全メンバ名を得る
{
unsigned int cnt;
NSMutableArray *ary = [NSMutableArray array];
Ivar *ivar = class_copyIvarList([object class], &cnt);
for (int i = 0; i < cnt; i++) {
Ivar iv = ivar[i];
const char *name = ivar_getName(iv);
if (name) {
[ary addObject:CstringToNSString(name)];
}
}
free(ivar);
NSLog(@"メンバ名群=%@",ary);
return ary;
}
/*
property_getAttributes()の返してくる文字列
返ってくるのは@propertyされているもののみ
T 先頭
@"〜" クラス名 "〜"がないときはid
i signed int/NSInterger
I unsigned int
c signed char/BOOL
C unsigned char
q long long
Q unsigned long long
f float
d double
N nonatomic
C copy
& retain
R readonly
何もなしはassign,readwrite,atomic
Vの直後からメンバ名
*/
NSDictionary *propertiesAttributes(id object)
// 全プロパティ情報を得る
{
NSMutableDictionary *dic = [NSMutableDictionary dictionary];
unsigned int cnt;
objc_property_t *properties = class_copyPropertyList([object class], &cnt);
for (int i = 0; i < cnt; i++) {
const char *attributes = property_getAttributes(properties[i]);
const char *name = property_getName(properties[i]);
printf("型=%s/名前=%s\n",attributes,name);
[dic setObject:CstringToNSString(attributes) forKey:CstringToNSString(name)];
}
free(properties);
NSLog(@"プロパティ=%@",dic);
NSLog(@"-------------------------");
return dic;
}
NSDictionary *memberAttributes(id object)
// 全メンバー情報を得る
{
NSMutableDictionary *dic = [NSMutableDictionary dictionary];
unsigned int cnt;
Ivar *ivar = class_copyIvarList([object class], &cnt);
for (int i = 0; i < cnt; i++) {
const char *encode = ivar_getTypeEncoding(ivar[i]);
const char *name = ivar_getName(ivar[i]);
printf("型=%s/名前=%s\n",encode,name);
[dic setObject:CstringToNSString(encode) forKey:CstringToNSString(name)];
}
free(ivar);
NSLog(@"メンバー=%@",dic);
NSLog(@"-------------------------");
return dic;
}
//--------------------------------------------------------------
2014年3月5日水曜日
クラスの内容をファイルに出力する方法(1)
とあるクラスの内容をファイルに出力しようと思った。
NSArrayなどNS標準クラスならwriteToFile一発だが、自分で作ったクラスの場合はそうは行かない。
そこで、いろいろ調べて&考えてそのための関数を作った。
GitHubで公開すればいいのかもしれないが、それほど完成度は高く無いと思うので、ここで公開することにする。
まずは、どんな関数を作ったか、その一覧を公開する。
・・・
BOOL saveClass(id object,NSString *basePath)
// 指定オブジェクトの全メンバーの内容を保存する
// object : 対応オブジェクト
// basePath : 保存フォルダ(この下にクラス名フォルダが出来、その下にクラス名別ファイルが作成される)
// リターン値:YES=書き込めた、NO=書き込めなかった
BOOL loadClass(id object,NSString *basePath)
// 指定オブジェクトの全メンバーの内容を復帰する
// object : 対応オブジェクト
// basePath : 保存フォルダ(この下にクラス名フォルダがあり、その下にクラス名別ファイルがあること)
// リターン値:YES=読み込めた、NO=読み込めなかった
BOOL deleteClass(id object,NSString *basePath)
// 指定オブジェクトの全メンバーの内容ファイルを削除する
// object : 対応オブジェクト
// basePath : 保存フォルダ(この下にクラス名フォルダがあり、その下にクラス名別ファイルがあること)
// クラス名ディレクトリも消す
// リターン値:YES=削除できた、NO=削除できなかった
NSArray *propertyNames(id object)
// オブジェクトの全プロパティ名を得る
// !=メンバ名なので注意
NSArray *memberNames(id object)
// オブジェクトの全メンバ名を得る
NSDictionary *propertiesAttributes(id object)
// 全プロパティ情報を得る
NSDictionary *memberAttributes(id object)
// 全メンバー情報を得る
NSArrayなどNS標準クラスならwriteToFile一発だが、自分で作ったクラスの場合はそうは行かない。
そこで、いろいろ調べて&考えてそのための関数を作った。
GitHubで公開すればいいのかもしれないが、それほど完成度は高く無いと思うので、ここで公開することにする。
まずは、どんな関数を作ったか、その一覧を公開する。
・・・
BOOL saveClass(id object,NSString *basePath)
// 指定オブジェクトの全メンバーの内容を保存する
// object : 対応オブジェクト
// basePath : 保存フォルダ(この下にクラス名フォルダが出来、その下にクラス名別ファイルが作成される)
// リターン値:YES=書き込めた、NO=書き込めなかった
BOOL loadClass(id object,NSString *basePath)
// 指定オブジェクトの全メンバーの内容を復帰する
// object : 対応オブジェクト
// basePath : 保存フォルダ(この下にクラス名フォルダがあり、その下にクラス名別ファイルがあること)
// リターン値:YES=読み込めた、NO=読み込めなかった
BOOL deleteClass(id object,NSString *basePath)
// 指定オブジェクトの全メンバーの内容ファイルを削除する
// object : 対応オブジェクト
// basePath : 保存フォルダ(この下にクラス名フォルダがあり、その下にクラス名別ファイルがあること)
// クラス名ディレクトリも消す
// リターン値:YES=削除できた、NO=削除できなかった
NSArray *propertyNames(id object)
// オブジェクトの全プロパティ名を得る
// !=メンバ名なので注意
NSArray *memberNames(id object)
// オブジェクトの全メンバ名を得る
NSDictionary *propertiesAttributes(id object)
// 全プロパティ情報を得る
NSDictionary *memberAttributes(id object)
// 全メンバー情報を得る
・・・
なお予め断っておくと、
1. 標準NSクラスでも出力できないものがある
2. クラス名の最大長に制約がある
2. クラス名の最大長に制約がある
である。1は私が使わないと思うクラスまで処理を入れても大きくなるだけで無駄と判断したからである。2はプログラムを簡略化するための手抜き。定数1つ変更するだけで拡大できる。その辺りが「完成度が高くない」という理由であるが、その辺りは実際に使う人が改良すればいいのではないかと思う次第。
登録:
投稿 (Atom)

