2014年11月15日土曜日

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

UITableView

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

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


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が外れてしまう(全体が表示できる幅のボタンになる)のでやってはいけない。

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月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から買い換える必要はないかと。

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年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は外せない状況。アップルの製品はソフトもハードもバグや故障が多すぎ。もっと検証しろ、品質上げろ!

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では問題ない。

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の次バージョンは、この問題を超えて発表まであと少し。今回は機能追加なし。

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

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

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ビット対応時には注意。

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にシリアライズして入出力出来るようになる。

以下のように実装する。


こんなクラスの場合↓
@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)
// 全メンバー情報を得る

・・・

なお予め断っておくと、

1. 標準NSクラスでも出力できないものがある
2. クラス名の最大長に制約がある

である。1は私が使わないと思うクラスまで処理を入れても大きくなるだけで無駄と判断したからである。2はプログラムを簡略化するための手抜き。定数1つ変更するだけで拡大できる。その辺りが「完成度が高くない」という理由であるが、その辺りは実際に使う人が改良すればいいのではないかと思う次第。

2014年2月18日火曜日

Xcode5で既存のプロジェクトにUnittestを追加する

今までUnitTestというものは使ったことがなかった、というかどうやって使うのかわからなかったのだけど、とあるところで使っている人のソースを見せてもらって、「こりゃ、うまく使えば便利かも」と思った。

が、Xcode5でProjectを新規に作るなら勝手にテスト用プロジェクトも入るのでいいのだが、既存のプロジェクトにそれを追加するとなると、方法がわからなかった。
検索してもXcode4の時のはあっても5のはなかったり、間違ってたりで使えなかった。

ということで、一念発起して探しだした結果を公開。

(1)プロジェクトを開く。プロジェクトを選択状態にすると、情報が表示されるけど、その一番左上に小さな再生ボタンみたいなのを押す。

(2)表示が展開されてPROJECTとTARGETSが表示されるので、TARGETSの下のAdd Targetsをクリックする。

(3)するとターゲット追加ダイアログが表示されるので、Cocoa Touch Unit Testing Bundleを選択し、Nextを押す。

(4)必要情報を記入する。基本的には入力されているので、Organization NameとCompany Identiferのみ入力すればいいはず。すべて入力しないとFinishは押せない。

(5)するとこのようにテスト用プロジェクトが追加されるので、あとは*Tests.mにテスト用コードを記述していくだけ。


で、利用する上での注意。

・UnitTestはシミュレーター上でしか走らない
・XCT*()はマクロで、内部でselfを使うため関数内では使えない
XCTAssertEqual()では型まで正確に比較されてしまうようである。

  したがって、intunsigned longの比較では、数値が同じでも不一致判定される。
・何故かプロジェクト内ファイルのリンクが通らないことがあるので、その時はテストの中に同じファイルをコピーする必要がある(プロジェクトの下に別のプロジェクトがいるような階層構造の時)。ファイル更新時にはコピーすることを忘れないこと。多分Xcode5のバグ。

2014年2月7日金曜日

printf()でdoubleを表示するにはどうするか

printf()でdouble値を表示する時の話。

表示指定はfloat/double共に"%f"である。
doubleはfloatの倍精度だから"%lf"・・・ではない。

でもこれには1つ落とし穴があって、doubleを%fで表示すると精度いっぱいの表示がなされない。%fはfloatの精度7桁までしか表示しないようになっているからだ。

なので、doubleの精度である15桁を表示させるには
    printf("%.15f",d)
のように、小数点以下の表示桁数を指定する必要がある。

ただし、15桁は少数以下だけでなく整数位も含めた全体なので注意。
要するにこの記述も必ずしも正しくはない。
また、これで表示させると累積演算誤差になる部分(小数部は2進数で表しきれないことによる問題)まで表示される。

全体で15桁という指定をするにうまい方法はないかは、X-BASIC for iOSのusingを実装するときに散々考えた。基本方法は以下のとおり。
(1)double値を整数部と小数部に分離する
 modf()関数を使う
(2)整数部をsprintf("%d")で文字列化して、その桁数を数える
    ただし、整数部が0の時は整数部桁数=0とする
(3)小数部の桁数を持つフォーマット文字列を作成する
  sprintf(format,"%%.%df",15-整数部桁数)
(4)それで表示する
 printf("%d",整数部)
 printf(format,小数部)

である。符号も考慮しておかなければならない。

しかし、素直にdoubleを桁数分表示する指定子があればいいと思うのだが、何故無いのだろう?誰も必要としないのだろうか?

2014年1月29日水曜日

iOSシミュレーターのUITextView/UItextFiledの入力バグ

シミュレーター上でのUITextView/UITextFiledではMacのキーボードから日本語を含め入力できるが、たまに出来なくなることがある。

シミュレーターで
 ハードウエア〜デバイス
で機器を切り替えると治ったが、ようやくもっと根本的に原因がわかった。

Commandキーが入力状態でロックされている。
なので、入力できなくなった状態でAを押すとCommand-Aとみなされ、全選択になる。
治すには、Commandキーを押せばよい。

入力中にCommand+<-/->で画面回転させると、発生する。
それ以外にも、MissionControlでのキー操作でも発生するので、
MacOS側でCommandを使うキー操作をすると軒並みダメになるみたい。

私はシステム環境設定のキーボードではControlとCommandを入れ替えているが、
それが影響している可能性はある。
いずれにせよシミュレーターのバグである。

追記:
Commandキーを押しても治らないこともある様子。その場合はデバイス切り替えをするか、シミュレーターを再起動するしかないかもしれない。

2014年1月22日水曜日

Block構文の罠

先日、ブロック構文で引っかかったので覚え書き。

たとえば、 イカのようなブロック構文を引数に保つメソッドを実装したとする。

- (void)blockTest:(id)arg
                       onSuccess:(void(^)(void))successBlock
                       onFailure:(void(^)(NSInteger errCode, NSError* error))failureBlock

この処理の中で通信とか遅い処理を行う。成功すればonSuccessが実行され、失敗すればonFailureが実行される、と言う作りである。

この時、

-(void)method
{
    [〜 blockTest:@"arg"
        onSuccess:^(void) {
            // 成功時処理
        }
        onFailure:^(NSInteger errCode, NSError *error) {
            // エラー処理
        }
    ];
    // 素通り
}
   
という処理はどう走るか。

実は、まず素通りし、methodからも抜けてしまう。
しかるのち、blockTestが実際に終了した時に、その結果に応じてonSuccessまたはonFailureのいずれか「だけ」が走る。
その時には素通りの部分は実行されない。

すなわち、(見かけ上)一旦抜けたメソッド内に、後から、非同期にまた戻ってくるということになる。
これは通常のCのプログラムでは考えられない動作なので理解が難しい。
(一旦抜けた関数の、さらにその中の一部の処理だけに戻ってくるというのはありえない。)

このブロック文はデリゲートの代わりだからこういうことになる。
なまじブロックでメソッド内に記述するからややこしくなるわけである。

ブロックは便利な記述方法ではあるが、プログラムの動きで言えば、記述の流れと処理の流れが一致せずわかりにくくなるので、
注意が必要である。

2014年1月15日水曜日

iOS7のUITextViewのバグ(その2)

iOS7のUITextViewには前にも書いたとおり、非常にたくさんのバグが存在するが、
またバグを発見してしまった。これは表に出てこないのでちょっとわかりにくいバグ。

 (1)入力状態にある間中、メモリ利用量が増加し続ける
64バイトずつメモリが確保され続けている。
だいたいではあるが13KB/分くらいの増加量。
調べると、libdispatch.dylibが_dispatch_continueation_alloc_from_heapを発行し続けているらしいが、
詳細は不明。

終了すると一括開放されるのでリークとはならない。



(2)メモリリークもある模様
しかし、別のところでメモリリークがある。内部で呼び出されていると思われる、NSUndoManagerというものが、メモリを開放しないで終了している。
1回あたりは少量だが、メモリリークが検出されること自体余り良い気分ではない。

なにはともあれ、iOS7のUITextViewはバグが多すぎて困る。一から作り直したりするからこういうことになるのだ。 従来版も残しながら新版をリリースし、以降を推奨しながら、バグが枯れた頃に旧版を廃止するのが普通ではないかと思うのだが、アップルには世間の常識は通用しないからなぁ。


おまけ
(3)UIDatePickerを回し続けると急激にメモリ利用量が増加する
ただし、止めて一定時間立つと開放される様子。
メモリ残り容量が少ない時にUIDatePickerを 動かすとメモリ不足で落ちたりするかもしれない。