ラベル サンプルあり の投稿を表示しています。 すべての投稿を表示
ラベル サンプルあり の投稿を表示しています。 すべての投稿を表示

UIAppearance で色や画像を変える

2011年10月31日月曜日 | Published in | 2 コメント

このエントリーをはてなブックマークに追加

UIAppearanceプロトコル


iOS5 から UIAppearance プロトコルが導入された。このプロトコルを使うと UINavigationBar や UISwitch のインスタンスの色を変更することができる。変更の仕方はこんな感じ。
[[UISwitch appearance] setOnTintColor:[UIColor greenColor]];
UIAppearance が有用なのは、上記の通りインスタンスではなくクラスに対して色設定ができること。つまりこれを使うと画面上で使われているすべての UISwitch の色を変えることができる。

例えば緑基調の画面があったとして、UIAppearance プロトコルを使うと別の色(例では赤)に変更することができる。
上記の例は1画面だけだが、アプリで使われるすべての画面で色が変わる。

上記例の色変更の箇所のコードはこんな感じ。
UIColor* color = nil;
    if (index == 0) {
        color = [UIColor colorWithRed:0 green:0.5 blue:0 alpha:1.0];
    } else {
        color = [UIColor colorWithRed:0.5 green:0 blue:0 alpha:1.0];
    }
    [[UINavigationBar appearance] setTintColor:color];
    [[UISegmentedControl appearance] setTintColor:color];
    [[UIProgressView appearance] setProgressTintColor:color];
    [[UISlider appearance] setMinimumTrackTintColor:color];
    [[UISwitch appearance] setOnTintColor:color];
    [[UIActivityIndicatorView appearance] setColor:color];
    [[UITabBar appearance] setTintColor:color];

UIAppearanceを使うとコントロール自体の基本色を実行中に切り替える事ができる。


所属ビューの限定


そのビューが所属している親ビューを条件に指定することができる。例えば UINavigationBar に配置されている UIBarButtonIem だけ色を変えたい場合は appearanceWhenContainedIn: を使ってこう書く。
[[UIBarButtonItem appearanceWhenContainedIn:
    [UINavigationBar class], nil] setTintColor:color];
親クラスは複数指定することができる。

appearance と appearanceWhenContainedIn: を比べた例。
[[UIBarButtonItem appearance] setTintColor:color];
この場合、UINaigationBar と UIToolBar 上の UIBarButtonItem の色が共に変更されている。

これを UINavigatorBar 限定にしてやるとこうなる。
[[UIBarButtonItem appearanceWhenContainedIn:
    [UINavigationBar class], nil] setTintColor:color];



サブビューへの適用


UINavigationBar や UIToolBar の色を変えるとそのサブビューである UIButtonBarItem の色も変更される。

[[UINavigationBar appearance] setTintColor:color];

ボタンを別の色に変えたい場合は明示的に指定してやる。
[[UINavigationBar appearance] setTintColor:color];
[[UIBarButtonItem appearance] setTintColor:blueColor];


UILabel


UILablel の文字色を変えると UITextField や UIButton の文字色も変わる。

これは UIButton 等が文字表示の為に UILabel をサブビューとして持っている為(例えば UIButton の場合、UIButtonLabel::UILabel を使っている)。UIButton の他にも UITextField が UISearchBar で使われていたりと他のコントロールを内包しているケースが多々ある。この為、UILabel や UItextField など基本的なパーツを使う場合は注意が必要。


ビュー階層内の優先順位


サブビューの設定が優先される。例えば UINavigationBar と UIBarButtonItem の両方で tintColorを設定した場合、UIBarButtonItem はそれ自身に設定した色のが優先される。
[[UINavigationBar appearance] setTintColor:color];
[[UIBarButtonItem appearance] setTintColor:blueColor];
呼び出し順は関係ないので逆にしてもこれは変わらない。
[[UIBarButtonItem appearance] setTintColor:blueColor];
[[UINavigationBar appearance] setTintColor:color];


画像の切替


UIAppearance による切替は色に限らず他の属性にも適用できる。例えば背景画像の切替をやって見る。
if (index == 0) {
        backImage = [UIImage imageNamed:@"back1.png"];
    } else {
        backImage = [UIImage imageNamed:@"back2.png"];
    }

    [[UIButton appearance] setBackgroundImage:backImage forState:UIControlStateNormal];

ボタンを画像で用意している場合や、ビューにテクスチャを貼っている場合など、あらかじめ複数種類の画像セットを用意しておけば簡単に切替ができる。


UIView


UIAppearanceプロトコル適用の宣言は UIView で行われている。
UIKIT_CLASS_AVAILABLE(2_0) @interface UIView : UIResponder
  <NSCoding, UIAppearance, UIAppearanceContainer> {
  :
この為、UIView の appearance に対して設定を行うと画面に表示されるすべてのビューに対して変更が適用される。例えば
[[Uiview apperance] setbackgroundcolor:[UIColor whitecolor]];
とすると、こうなる。


実際のデザインよりはデバッグ目的での利用が多そう。


備考


色を設定した後の再描画方法がわからなかった。-[UIView setNeedsDisplay] を performSelector:@selector(setNeedsDisplay)object:delay: を使ってみたがうまくいかなかった。この理由によりサンプルでは下のタブバーの色だけが切り替わらない。※もしご存知の方がいたら是非教えて下さい。


サンプル


検証に使ったサンプルソースはこちら。
UIAppearanceSample at 2011-10-31 from xcatsan/iOS5-Sample - GitHub



- - - -

スキンの切替が容易に実現できるので今後重宝しそう。

CAGradientLayer を使ったグラデーション付きボタンの試作

2011年10月22日土曜日 | Published in | 4 コメント

このエントリーをはてなブックマークに追加

CAGradientLayer を使った描画がしたくて調べている。試しにボタンを作ってみた。


仕組み


UIControl をベースのクラスに使い、その上にレイヤーを重ねて作ってある。
基本の色は UIControl.backgroundColor で決める。

その上に CAGradientLayer をかぶせてグラデーションをかける。グラデーションは白色をアルファ値を変えて表現する。
self.gradientLayer = [CAGradientLayer layer];
    self.gradientLayer.frame = self.bounds;
    self.gradientLayer.locations = [NSArray arrayWithObjects:
                                    [NSNumber numberWithFloat:0.0],
                                    [NSNumber numberWithFloat:0.5],
                                    [NSNumber numberWithFloat:0.5],
                                    [NSNumber numberWithFloat:1.0],
                                    nil]
     self.gradientLayer.colors =
        [NSArray arrayWithObjects:
             (id)[UIColor colorWithWhite:1.0 alpha:0.7].CGColor,
             (id)[UIColor colorWithWhite:1.0 alpha:0.4].CGColor,
             (id)[UIColor colorWithWhite:1.0 alpha:0.3].CGColor,
             (id)[UIColor colorWithWhite:1.0 alpha:0.0].CGColor,
             nil];
    [self.layer addSublayer:self.gradientLayer];
こうするとベースの色(UIControl.backgroundColor)は単純に基本の色を指定するだけで良い。色を加工したり、あらかじめ色のグラーでションを用意する必要が無いので使い勝手がいい(と思う)。

その上にボタンの文字を CATextLayer に描く。
self.textLayer = [CATextLayer layer];
    self.textLayer.string = self.text;
    self.textLayer.font = CGFontCreateWithFontName((CFStringRef)[self _font].fontName);
    self.textLayer.fontSize = [self _font].pointSize;
    self.textLayer.truncationMode = kCATruncationEnd;
    self.textLayer.alignmentMode = kCAAlignmentCenter;
    self.textLayer.shadowColor = [UIColor blackColor].CGColor;
    self.textLayer.shadowRadius = 0.5;
    self.textLayer.shadowOffset =CGSizeMake(-0.5, -0.5);
    self.textLayer.shadowOpacity = 0.5;
    self.textLayer.foregroundColor = [UIColor whiteColor];
    [self.layer addSublayer:self.textLayer];

その上のレイヤーはボタンに立体感を出すためのハイライトライン。
これを描くために CALayer のサブクラス HighlightEdgeLayer を作り自前で線を描画している。
#define CORNER_RADIUS   5.0

@implementation HighlightEdgeLayer
- (void)drawInContext:(CGContextRef)context
{
    CGMutablePathRef path = CGPathCreateMutable();
    CGPoint p = CGPointMake(CORNER_RADIUS, CORNER_RADIUS);

    CGPathAddArc(path, NULL,
                 p.x, p.y,
                 CORNER_RADIUS,
                 2.0*M_PI/2.0,
                 3.0*M_PI/2.0,
                 false);

    p.x = self.bounds.size.width - CORNER_RADIUS;
    CGPathAddArc(path, NULL,
                 p.x, p.y,
                 CORNER_RADIUS,
                 3.0*M_PI/2.0,
                 4.0*M_PI/2.0,
                 false);

    CGContextAddPath(context, path);
    CGPathRelease(path);
    CGContextSetStrokeColorWithColor(context, [UIColor colorWithWhite:1.0 alpha:0.5].CGColor);
    CGContextSetLineWidth(context, 1.0);
    CGContextDrawPath(context, kCGPathStroke);

}
@end
一番上のレイヤーは UIControl に最初からついている CALayer。これで黒い境界線を引き、角を丸くしている。
self.layer.cornerRadius = CORNER_RADIUS;
    self.layer.masksToBounds = YES;
    self.layer.borderColor = [UIColor colorWithWhite:0.0 alpha:0.75].CGColor;
    self.layer.borderWidth = 1.0;

ボタンが押された時には -[UIControl setHighlighted:] が呼ばれるのでここでグラデーションの色を変えてやる。
- (void)setHighlighted:(BOOL)highlighted
{
    [super setHighlighted:highlighted];
    [self _setState:highlighted ? GradientButtonStateHighlighted : GradientButtonStateNormal];
}
サンプルでは3つの状態を定義しておいて、それぞれでグラデーションの色と文字色を変えている。
typedef enum {
    GradientButtonStateNormal = 0,
    GradientButtonStateHighlighted,
    GradientButtonStateDisabled
} GradientButtonState;

- (void)_setState:(GradientButtonState)state
{
    switch (state) {
        case GradientButtonStateNormal:
            self.gradientLayer.colors =
            [NSArray arrayWithObjects:
             (id)[UIColor colorWithWhite:1.0 alpha:0.7].CGColor,
             (id)[UIColor colorWithWhite:1.0 alpha:0.4].CGColor,
             (id)[UIColor colorWithWhite:1.0 alpha:0.3].CGColor,
             (id)[UIColor colorWithWhite:1.0 alpha:0.0].CGColor,
             nil];
            self.textLayer.foregroundColor = self.textColor.CGColor;
            break;
            
        case GradientButtonStateHighlighted:
            self.gradientLayer.colors =
            [NSArray arrayWithObjects:
             (id)[UIColor colorWithWhite:1.0 alpha:0.5].CGColor,
             (id)[UIColor colorWithWhite:1.0 alpha:0.2].CGColor,
             (id)[UIColor colorWithWhite:0.0 alpha:0.05].CGColor,
             (id)[UIColor colorWithWhite:0.0 alpha:0.1].CGColor,
             nil];
            self.textLayer.foregroundColor = self.textColor.CGColor;
            break;
            
        case GradientButtonStateDisabled:
            self.gradientLayer.colors =
            [NSArray arrayWithObjects:
             (id)[UIColor colorWithWhite:1.0 alpha:0.7].CGColor,
             (id)[UIColor colorWithWhite:1.0 alpha:0.4].CGColor,
             (id)[UIColor colorWithWhite:1.0 alpha:0.3].CGColor,
             (id)[UIColor colorWithWhite:1.0 alpha:0.0].CGColor,
             nil];
            self.textLayer.foregroundColor = [UIColor lightGrayColor].CGColor;
            break;
    }
}
押す前

押した後
ディゼーブル


サンプル


何色かのボタンを並べたサンプルを作ってみた。最後のボタンは Disable状態。
ベースの色は階調が見やすいように濃いめの色にした方がいい。これはグラデーションの表現を白で表現している為。標準で UIColor に用意されている greenColor や yellowColor は明るすぎて階調が出づらい。

Xib はこんな感じ。UIView を配置して大きさを調整した後、クラスに GradientButton を指定する。


利用上の注意点としては Xib 上で IBAction を指定すると Value Changed のアクションがデフォルトで選択される。これではボタン押下時のイベントが取れないので Touch up inside を使う。


ソースコード


GitHub からどうぞ。
GradientButton at 2011-10-22 from xcatsan/iOS-Sample-Code - GitHub


参考情報


テン*シー*シー - 【iPhoneアプリ開発ドリル】Aqua風ボタンを作る

CAGradientLayer の使い方はこのサイトがとても参考になった。UIControl をボタンとして実装するアイディアはここから拝借した。情報をどうも!

iPhoneアプリ開発、その(17) 文字だって回転|テン*シー*シー

CATextLayer の使い方もここ。

iphone - How can i convert UIFont to CGFontRef?I causes warning and not working - Stack Overflow

UIFont から CGFontRef を取る方法。
CGFontRef cgFont = CGFontCreateWithFontName((CFStringRef)uiFont.fontName);

サンプルコードのボタンのグラデーション(ハイライト)は iOS のポップアップメニューを参考にした。


なので通常のボタンと見た目がちょっと違う。


関連情報


CATextLayer Class Reference
CAGradientLayer Class Reference


TWTweetComposeViewController でツィート

2011年10月15日土曜日 | Published in | 7 コメント

このエントリーをはてなブックマークに追加

iOS 5 から Twitter投稿用の API が追加された。前から興味があったので試してみた。
TWTweetComposeViewController Class Reference

(2012/02/24追記あり) 

使い方


プロジェクトへフレームワークを追加しておく。

使いかたはこんな感じ。
#import <Twitter/TWTweetComposeViewController.h>

    TWTweetComposeViewController *viewController =
        [[TWTweetComposeViewController alloc] init];

    [viewController setInitialText:@"Initial String"];
    [viewController addImage:image];
    [viewController addURL:[NSURL URLWithString:@"http://www.apple.com/"]];

    viewController.completionHandler = ^(TWTweetComposeViewControllerResult res) {
        if (res == TWTweetComposeViewControllerResultDone) {
            NSLog(@"done");
               :
        } else if (res == TWTweetComposeViewControllerResultCancelled) {
            NSLog(@"cancel");
              :
        }
    };
    
    [self presentModalViewController:viewController animated:YES];
}
5分もかからない、コピペすれば数秒。iOS5以前の苦労がウソのような超簡単さ....。

presentModalViewController:viewController: で表示すると専用の投稿画面が立ち上がる。この画面はローカライズされていないようだ。
(2012/02/24追記) Storyboardもしくは呼び出し元のビューをローカライズすると、この画面もそれに合わせた言語になるとのこと。情報提供された方 Thanks!

また位置情報を追加することもできる。
Send を押すと投稿されて画面が閉じられる。この時、鳥のさえずり効果音も鳴る。

投稿もしくはキャンセルされると completionHandler が呼び出される。リファレンスによれば現在のスレッドで処理が実行されるようだ(別スレッドにはならない)。
Note that completion handlers are not called on any particular thread.

また UIPopoverController では使えないとのこと。
Warning: You cannot use an UIPopoverController object to present a TWTweetComposeViewController object.

利用可能かどうかを確認する為のクラスメソッドも用意されている。
// Returns if Twitter is accessible and at least one account has been setup.
+ (BOOL)canSendTweet;
iOS のTwitter設定でアカウントが登録されていない場合、NO が返る。

アカウント未登録状態で投稿画面を呼び出すとダイアログが表示される。
ここは日本語化されていないようだ。
Settings を押すと iOS の Twitter設定画面が開かれる。設定後にアプリを起動するとこのダイアログが消えた状態で復帰する。


サンプル


GitHub からどうぞ。
TweetSample at master from xcatsan/iOS5-Sample - GitHub

起動するとテキストビューが表示される。add image ボタンを押すと。カメラロールからイメージが追加できるようになっている。

投稿結果の例




備考


addImage: に複数画像を追加しても最初の一枚しか投稿されなかった。結果の BOOL値を確認したところ、2枚目以降は必ず NO が帰っていた。原因は不明。


参考情報


iOS5.0のTwitter APIを使って文字や画像をTweetする方法(TWTweetComposeViewController) - あらきんぐのiPhoneアプリ開発ブログ


Blocks を使ったアニメーションのひっかかりを解消する

2011年8月22日月曜日 | Published in | 0 コメント

このエントリーをはてなブックマークに追加

Cocoaの日々: [iOS] UITableView でプルダウンすると再読込するユーザインタフェースを実装

以前紹介した投稿でアニメーションにひっかかりを感じると書いた。
UITableView を下にドラッグした時に画面上部の矢印が下向き↓からクルッと回転して上向き↑になるアニメーションが起こる。これを iPhoneで実行してみるとカクっとひっかかるような感じがあった。コードはこんな感じ。
[UIView animateWithDuration:0.2
         animations:^{
             self.imageView.transform =
                CGAffineTransformMakeRotation(endAngle);
         }];
Blocks によるアニメーションを使っている。


beginAnimations


先日この件で Kyasu さんより情報提供があった。
kyasu says: 
2011年8月18日19:43

>プルダウンしてアニメーションが起きる時に若干のひっかかりを感じた。
この件ですが、Blocksを使わないでbeginAnimations,commitAnimationsを
使うと動作がブロックされないようです。
?...試してみよう。
[UIView beginAnimations:nil context:NULL];
    [UIView setAnimationDuration:0.2];
    self.imageView.transform =
    CGAffineTransformMakeRotation(endAngle);
    [UIView commitAnimations];
するとひっかかりが無くなってスムーズにアニメーションが動作するようになった。おお、これはいい。

ただ、その一方で beginAnimations と blocks アニメーションで違いがある?という疑問が湧いてきた。


UIViewAnimationOptionAllowUserInteraction


違いはあった。
UIViewAnimationOptionAllowUserInteraction

普通の表示だけのアニメーションだと違いは無いのだが今回のようにユーザが操作(ドラッグ)している最中にアニメーションを動作させる場合、blocks アニメーションのデフォルト動作では今回のようにひっかかりが発生する。この場合は blocksアニメーションのオプションに UIViewAnimationOptionAllowUserInteraction を指定する必要がある。こんな感じ。
[UIView animateWithDuration:0.2
                          delay:0.0
                        options:UIViewAnimationOptionCurveLinear |
                                UIViewAnimationOptionAllowUserInteraction
                     animations:^{
                         self.imageView.transform =
                         CGAffineTransformMakeRotation(endAngle);
                         
                     }
                     completion:NULL
     ];
beginAnimations を使ったアニメーションではデフォルトでこのオプションと同等の動作になっているということなのだろう。


ソースコード


改良版は GitHub からどうぞ。
CustomCellSample at 2011-08-29 from xcatsan/iOS-Sample-Code - GitHub


- - - -
この件はずっと気になっていたので解消されてうれしい。
Kyasu さん、情報提供ありがとうございました。


CALayer を使ってビューの内側に影を落とす

2011年8月8日月曜日 | Published in | 0 コメント

このエントリーをはてなブックマークに追加

ビューの上の縁に影を落としたい。こんな感じ。

簡単に出来る方法はないか。

CALayer


CALayer を使うと簡単にビューに影を落とすことができる。
CALayer* layer = self.imageView1.layer;
    layer.shadowOffset = CGSizeMake(2.5, 2.5);
    layer.shadowColor = [[UIColor blackColor] CGColor];
    layer.shadowOpacity = 0.5;


ただこの方法はビューの外側に影を落とせても、ビューの内部には影を落とせない。
どうするか。

CALayer のプロパティを眺めていていると shadowPath に気がついた。このプロパティには CGPathRef を渡すことができる。
@property CGPathRef shadowPath;
もしかしてこれを使って任意の場所や形で影が落とせないか。

試しにこんな矩形のパスを作って渡してみた。

CALayer* subLayer = imageView.layer;
    UIBezierPath* path = [UIBezierPath bezierPathWithRect:
            CGRectMake(-10.0, -10.0, subLayer.bounds.size.width+10.0, 10.0)];
    subLayer.shadowOffset = CGSizeMake(2.5, 2.5);
    subLayer.shadowColor = [[UIColor blackColor] CGColor];
    subLayer.shadowOpacity = 0.5;
    subLayer.shadowPath = [path CGPath];
すると影が落ちた(わかりやすいように画像を縮小してある)。

でも画像の下側だ。影はコンテンツの下に来るものだから当たり前といえば当たり前。コンテンツの上に影をかぶせるにはどうしたらいいか。

サブレイヤーを追加してそこへ影を落としてはどうか?やってみよう。
CALayer* subLayer = [CALayer layer];
    subLayer.frame = imageView.bounds;
    [imageView.layer addSublayer:subLayer];
    subLayer.masksToBounds = YES;
    UIBezierPath* path = [UIBezierPath bezierPathWithRect:
            CGRectMake(-10.0, -10.0, subLayer.bounds.size.width+10.0, 10.0)];
    subLayer.shadowOffset = CGSizeMake(2.5, 2.5);
    subLayer.shadowColor = [[UIColor blackColor] CGColor];
    subLayer.shadowOpacity = 0.5;
    subLayer.shadowPath = [path CGPath];

出た。いい感じだ。

どんなビューでも CALayer がサポートされているので例えば MKMapView でも簡単に影を落とせる。


バリエーション


上だけでなく左にも影を落としてみた。ついでに角も丸くした。


逆L字型の図形を左上に用意してその影を落とせばいい。

逆L字型図形は CGMutablePath を使って地道に描く。
- (void)_addDropShadowToView2:(UIView*)toView
{
    CALayer* subLayer = [CALayer layer];
    subLayer.frame = toView.bounds;
    [toView.layer addSublayer:subLayer];
    subLayer.masksToBounds = YES;

    CGSize size = subLayer.bounds.size;
    CGFloat x = -10.0;
    CGFloat y = -10.0;
    CGMutablePathRef pathRef = CGPathCreateMutable();
    CGPathMoveToPoint(pathRef, NULL, x, y);
    x += size.width + 10.0;
    CGPathAddLineToPoint(pathRef, NULL, x, y);
    y += 10.0;
    CGPathAddLineToPoint(pathRef, NULL, x, y);
    x -= size.width;
    CGPathAddLineToPoint(pathRef, NULL, x, y);
    y += size.height;
    CGPathAddLineToPoint(pathRef, NULL, x, y);
    x -= 5.0;   // (*)10
    CGPathAddLineToPoint(pathRef, NULL, x, y);
    y -= size.height;   // (*)size.height+10
    CGPathAddLineToPoint(pathRef, NULL, x, y);
    CGPathCloseSubpath(pathRef);
   
    subLayer.shadowOffset = CGSizeMake(2.5, 2.5);
    subLayer.shadowColor = [[UIColor blackColor] CGColor];
    subLayer.shadowOpacity = 0.5;
    subLayer.shadowPath = pathRef;
   
    CGPathRelease(pathRef);
   
}
実は逆L字型は少し歪んだ形をしている。(*)のついている2行はコメントに記載した値が正しいのだが、これを使うと下図の様にコンテンツ全体に薄い影がかかってしまった。

パスはきちんと閉じていると思うのだが、どうも思った形で影が落ちないようだ。上記値は試行錯誤で見つけた値。逆L字型が正確な形ではないが、見た目は意図通りの影が落ちているのでこれでよしとする。原因を知っている方(バグを見つけた方)がいたら是非教えて下さい。


ソースコード


GitHub からどうぞ。
LayerShadowSample at 2011-08-08 from xcatsan/iOS-Sample-Code - GitHub


参考情報


Fun shadow effects using custom CALayer shadowPaths | iOS/Web Developer's Life in Beta
shadowPath プロパティを使った様々な形の影の落とし方。参考になった。


Invitation to CoreAnimation - NIT-Universe
サブレイヤーを使うアイディアはここからヒントを得た。

関連情報


Cocoaの日々: Bezelボタンを作る[03]矩形の内側に影を落とす
以前紹介したビューの内側へ影を落とす方法。マスクを作ったりと結構面倒。今回のCALayerを使う方が簡単。

[iOS] データ保護(Data Protection)

2011年4月22日金曜日 | Published in | 3 コメント

このエントリーをはてなブックマークに追加

データ保護


iOS 4 以降、データ保護機能が提供されるようになった。特徴は次の通り。

  • データ保護機能を有効にするにはパスコードロックを有効にする必要がある
  • データ保護の適用はファイル単位となる
  • アプリケーションが明示的にファイルにデータ保護属性を付加することで有効になる
  • データ保護属性のついたファイルには、デバイスロック中は保護されていてアクセスができない
  • バックグラウンドで動作するアプリであっても、デバイスロック時にはデータ保護されたファイルへアクセスできない
  • データ保護属性のついたファイルは、デバイスロック中に iTunesなどのツールから持ち出すことができない
  • データ保護属性のついたファイルは暗号化される
  • 対象機種:iPhone 4, iPhone 3GS, iPod touch (3rd generation or later), and all iPad models
[参考情報] Limitations of Data Protection in iOS 4 | Anthony Vance

※上記は若干自信が無いところもあるので違っていたら是非教えて下さい。


データ保護機能を有効にするにはパスコードロックが有効になっている必要があり、有効の場合はその旨メッセージが表示される。

※iOS 3から 4 へアップデートした場合にはこのメッセージが出ない。この場合「復元」操作が必要。
iOS 4:データ保護について


データ保護 API


データ保護向けの APIがいくつか用意されている。

NSFileManager

ファイル属性に NSFileProtectionKey が追加された。値に NSFileProtectionComplete を指定するとデータ保護が有効になる。
NSDictionary* attributes =
    [NSDictionary dictionaryWithObject:NSFileProtectionComplete forKey:NSFileProtectionKey];
NSError* error = nil;
[fileManager setAttributes:attributes ofItemAtPath:filePath error:&error];
NSFilManager Reference - File Protection Values

なおファイルコピー時に NSFileProtectionComplete の設定はコピーされない。コピー先のファイルは NSFileProtectionNone となる。

NSData

ファイル書き出し時に NSDataWritingFileProtectionComplete オプションを指定するとデータ保護の適用を指定することができる。
NSError* error = nil;
[data writeToFile:filePath
 options:NSDataWritingFileProtectionComplete error:&error];
NSData Class Reference - NSDataWritingOptions

UIApplicationDelegate

データ保護が有効になる直前、無効になる直前に呼ばれるデリゲートメソッドが用意されている。
- (void)applicationProtectedDataDidBecomeAvailable:(UIApplication *)application
- (void)applicationProtectedDataWillBecomeUnavailable:(UIApplication *)application
UIApplicationDelegate Protocol Reference

また UIApplication には保護データへアクセス可能かどうかを知ることができるプロパティ protectedDataAvailable が用意されている。
UIApplication Class Reference


サンプル


(1)データ保護なし (2)データ保護あり(NSFileManagerで属性設定) (3)データ保護あり(NSDataで書き出し)の3種類のファイルを作成してデータの持ち出しと属性を調べてみた。コードはこんな感じ。

// definitions
    NSString* basePath = [NSSearchPathForDirectoriesInDomains(
        NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
    NSFileManager* fileManager = [NSFileManager defaultManager];

    NSString* src1 = [[NSBundle mainBundle] pathForResource:@"sample" ofType:@"jpg"];
    NSString* src2 = [[NSBundle mainBundle] pathForResource:@"sample_encrypted" ofType:@"jpg"];

    NSString* dst1 = [basePath stringByAppendingPathComponent:@"sample.jpg"];
    NSString* dst2 = [basePath stringByAppendingPathComponent:@"sample_encrypted.jpg"];
    NSString* dst3 = [basePath stringByAppendingPathComponent:@"sample_encrypted2.jpg"];
    
    NSLog(@"dst1: %@", dst1);
    NSLog(@"dst2: %@", dst2);
    NSLog(@"dst3: %@", dst3);
    
    NSError* error = nil;
    
    // copy files
    if (![fileManager copyItemAtPath:src1 toPath:dst1 error:&error]) {
        NSLog(@"%@", error);        
    }
    if (![fileManager copyItemAtPath:src2 toPath:dst2 error:&error]) {
        NSLog(@"%@", error);        
    }

    // set attributes
    NSDictionary* attributes = [NSDictionary dictionaryWithObject:NSFileProtectionComplete
                                                           forKey:NSFileProtectionKey];
    if (![fileManager setAttributes:attributes
                       ofItemAtPath:dst2
                              error:&error]) {
        NSLog(@"%@", error);
    }
    
    // create dst3
    NSData* data = [NSData dataWithContentsOfFile:src1];
    if (![data writeToFile:dst3
                   options:NSDataWritingFileProtectionComplete
                     error:&error]) {
        NSLog(@"%@", error);
    }
あらかじめ JPEG画像を用意しておき、これを Documentディレクトリへコピーしたり(1)(2)、書きだしたり(3)している。


iTunes でアクセス


このアプリを実行した後、iTunes へ繋ぎデータを持ち出しを試みてみた。

予想通りデータ保護のかかっている(2)(3)のファイルは持ち出し時にエラーが出た。

データ保護はパスコードロックがかかっている時のみ有効。


試しにそれぞれのファイル属性を出してみた。
2011-04-22 10:26:20.894 FileProtectionSample[2272:707] dst1: {
    NSFileCreationDate = "2011-04-20 07:18:08 +0000";
    NSFileExtensionHidden = 0;
    NSFileGroupOwnerAccountID = 501;
    NSFileGroupOwnerAccountName = mobile;
    NSFileModificationDate = "2011-04-20 07:18:08 +0000";
    NSFileOwnerAccountID = 501;
    NSFileOwnerAccountName = mobile;
    NSFilePosixPermissions = 420;
    NSFileProtectionKey = NSFileProtectionNone;
    NSFileReferenceCount = 1;
    NSFileSize = 3896;
    NSFileSystemFileNumber = 77317;
    NSFileSystemNumber = 234881027;
    NSFileType = NSFileTypeRegular;
}
2011-04-22 10:26:20.912 FileProtectionSample[2272:707] dst2: {
    NSFileCreationDate = "2011-04-20 07:18:08 +0000";
    NSFileExtensionHidden = 0;
    NSFileGroupOwnerAccountID = 501;
    NSFileGroupOwnerAccountName = mobile;
    NSFileModificationDate = "2011-04-20 07:18:08 +0000";
    NSFileOwnerAccountID = 501;
    NSFileOwnerAccountName = mobile;
    NSFilePosixPermissions = 420;
    NSFileProtectionKey = NSFileProtectionComplete;
    NSFileReferenceCount = 1;
    NSFileSize = 3896;
    NSFileSystemFileNumber = 77318;
    NSFileSystemNumber = 234881027;
    NSFileType = NSFileTypeRegular;
}
2011-04-22 10:26:20.929 FileProtectionSample[2272:707] dst3: {
    NSFileCreationDate = "2011-04-20 07:53:00 +0000";
    NSFileExtensionHidden = 0;
    NSFileGroupOwnerAccountID = 501;
    NSFileGroupOwnerAccountName = mobile;
    NSFileModificationDate = "2011-04-22 01:26:20 +0000";
    NSFileOwnerAccountID = 501;
    NSFileOwnerAccountName = mobile;
    NSFilePosixPermissions = 420;
    NSFileProtectionKey = NSFileProtectionComplete;
    NSFileReferenceCount = 1;
    NSFileSize = 3896;
    NSFileSystemFileNumber = 13804;
    NSFileSystemNumber = 234881027;
    NSFileType = NSFileTypeRegular;
}
NSFileProtectionKey が設定されているのがわかる。またファイルサイズに違いは無い。APIを使ったデータ保護設定の有無は単純に属性設定の違いだけのようだ。

なおシミュレータでは "NSFileProtectionKey" の値が含まれない。確認できるのは実機だけのようだ。


Xcode でアクセス


Xcode のオーガナイザでアプリのフォルダ一式をダウンロードしてみる。
"Download"ボタンでPCへ保存しようとすると途中でエラーが出る。


ダウンロード


サンプルのソースコードは GitHub からどうぞ。
FileProtectionSample at 2011-04-21 from xcatsan/iOS-Sample-Code - GitHub


参考情報



「11-3 データ保護」が詳しく参考になった。


Core Data and Enterprise iPhone Applications – Protecting Your Data << Nick Harris
今回のデータ保護機能を使って CoreData のデータ(SQLite)を暗号化する方法が紹介されている。


Limitations of Data Protection in iOS 4 | Anthony Vance
データ保護についての特徴と制約がよくまとめられている。データ保護関連の情報は少ないので貴重な解説。


iOS 4: Data protection, hardware encryption and other insight
データ保護、ハードウェア暗号化の話題など。

Working with Protected Files
データ保護機能の使い方。setAttributes: を使う場合はデータを書きだす前に NSFileProtectionComplete を設定することが推奨されている。


iTunesを使ったファイル共有機能を使う方法 - 強火で進め
iTunes で iOSデバイス内のフォルダへアクセスする方法について。Info.plist で "Application supports iTunes file sharing" を有効にすると iTunes の Appタブでアプリケーションの Documents フォルダへアクセスすることができるようになる。今回のサンプルはこれが有効になっている。


iOS 4:データ保護について
データ保護を有効にする方法。iOS 3からのバージョンアップの場合はデバイスの復元が必要。

[iOS] バックグラウンド実行見本(Task Completion)

2011年4月8日金曜日 | Published in | 2 コメント

このエントリーをはてなブックマークに追加

(2011-12-01 追記あり)UIApplicationDelegateの呼び出しが iOS5 から変わった件。

Task Completion を使った iOS4 でのバックグラウンド実行サンプルを作ってみた。

サンプル


実行するとキューにたまった 30個のデータが順番に処理されてテーブルから消えていく。

処理は GCD を使い別スレッドで実行される。右上の[+]ボタンを押すとキューへデータが追加されていく。途中でホームボタンを押してアプリを切り替えても Task Completion によって処理は停止すること無く実行され続ける。わかりやすいようにアプリのアイコンバッヂに残タスク数を表示してみた。
バッジの数字は時間と共にカウントダウンされていくので処理が行われていることが確認できる。


Task Completion とは?


Task Completion は、iOS4 から導入されたマルチタスキングの機能の一つで、これを利用すると最大10分間を上限にバックグラウンドで処理を実行できる。利用するには -[UIApplication beginBackgroundTaskWithExpirationHandler:] を使う。このメソッドは iOS に対してバックグラウンド時の処理続行を依頼するものなので通常はアプリケーションがバックグラウンド状態になるタイミング(applicationDidEnterBackground:)などで呼び出す。公式リファレンスでは次のようなコードが紹介されている。
iOS Application Programming Guide: Executing Code in the Background より

- (void)applicationDidEnterBackground:(UIApplication *)application
{
    UIApplication*    app = [UIApplication sharedApplication];
 
    bgTask = [app beginBackgroundTaskWithExpirationHandler:^{
        [app endBackgroundTask:bgTask];
        bgTask = UIBackgroundTaskInvalid;
    }];
 
    // Start the long-running task and return immediately.
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
 
        // Do the work associated with the task.
 
        [app endBackgroundTask:bgTask];
        bgTask = UIBackgroundTaskInvalid;
    });
}
beginBackgroundTaskWithExiprationHandler: を呼び出すと、それ以降別のアプリを使用している間も処理を続行させることができる。このメソッドの引数 blocks には、処理が10分過ぎても終わらない時に実行する処理を書いておく。endBackgroundTask: はバックグラウンド処理が終わった時に呼び出す。この引数は beginBackgroundTaskWithExiprationHandler: の戻り値を渡す。


ソースコード解説


サンプルでの Task Completion の利用方法を見ていく。今回は簡易的なキュー(FIFOバッファ)を用意して、そこに入っているデータを単純に取り出すだけの処理を行うスレッドを実行させた。キューの定義はこんな感じ。
@interface Queue : NSObject {

}
@property (retain) NSMutableArray* queue;

- (void)putObject:(id)object;
- (id)getObject;
- (void)removeObject;
- (NSUInteger)count;
- (NSArray*)list;
@end
次にアプリケーション初期化処理。
- (BOOL)application:(UIApplication *)application
 didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    // [1] creating sample data and put into the queue
    Queue* queue = [[Queue alloc] init];
    for (int i=0; i < 60; i++) {
        NSString* str = [NSString stringWithFormat:@"DATA-%02d", i];
        [queue putObject:str];
    }
    self.rootViewController.queue = queue;
    [queue release];

    // [2] init window    
    self.window.rootViewController = self.navigationController;
    [self.window makeKeyAndVisible];
    
    // [3] start thread
    dispatch_queue_t gcd_queue =
       dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_async(gcd_queue, ^{
        
        UIApplication* app = [UIApplication sharedApplication];
        app.applicationIconBadgeNumber = [queue count];
        for(;;) {
            if ([queue count] > 0) {
                id object = [queue getObject];
                NSLog(@"processing: %@", object);
                [NSThread sleepForTimeInterval:1.0];    // dummy wait
                NSLog(@"done: %@", object);
                [queue removeObject];
                app.applicationIconBadgeNumber = [queue count];
                
                dispatch_async(dispatch_get_main_queue(), ^{
                    [self.rootViewController.tableView reloadData];
                });
                
                if ([queue count] == 0 && backgroundTaskIdentifer != UIBackgroundTaskInvalid) {
                    dispatch_async(dispatch_get_main_queue(), ^{
                        NSLog(@"finished!");
                        if (backgroundTaskIdentifer != UIBackgroundTaskInvalid) {
                            [app endBackgroundTask:backgroundTaskIdentifer];
                            backgroundTaskIdentifer = UIBackgroundTaskInvalid;
                        }
                    });
                }
            } else {
                [NSThread sleepForTimeInterval:1.0];
            }
        }
        
    });
    
    return YES;
}
前半ではキューへデータを入れたり[1]、ウィンドウの初期化[2]を行っている。後半の [3]で GCDを使いスレッドを一つ作成し、そこで定期的にキュー内のデータを処理させている。このスレッド内ではキューから1づつデータを取り出し処理(単純に1秒スリープ)を繰り返し行う。すべての処理が終わったときに Task Completion が有効( != UIBackgroundTaskInvalid)なら endBackgroundTask: を読んでバックグラウンド処理を終了させている。キューが空になった時には1秒間スリープし、その後キューにデータがあれば処理を実行し、無ければ再びスリープする動作を繰り返す。

Task Completion を有効にする処理は applicationWillResignActive: に書いている。
- (void)applicationWillResignActive:(UIApplication *)application
{
    NSLog(@"%s", __PRETTY_FUNCTION__);
    UIApplication* app = [UIApplication sharedApplication];
    
    NSAssert(backgroundTaskIdentifer == UIBackgroundTaskInvalid, nil);
    
    backgroundTaskIdentifer = [app beginBackgroundTaskWithExpirationHandler:^{
        
        NSLog(@"expired!");
        dispatch_async(dispatch_get_main_queue(), ^{
            if (backgroundTaskIdentifer != UIBackgroundTaskInvalid) {
                [app endBackgroundTask:backgroundTaskIdentifer];
                backgroundTaskIdentifer = UIBackgroundTaskInvalid;
            }
        });
    }];
    
}
なお Task Completion を有効にした後に別アプリへ切り替え、さらにその後再びこのアプリへ戻ってきた時にまだ Task Completion が有効だった場合には endBackgroundTask: を呼び出してやる必要がある。これを applicationDidBecomeActive: に書いておく。
- (void)applicationDidBecomeActive:(UIApplication *)application
{
    NSLog(@"%s", __PRETTY_FUNCTION__);
    
    UIApplication* app = [UIApplication sharedApplication];
    dispatch_async(dispatch_get_main_queue(), ^{
        if (backgroundTaskIdentifer != UIBackgroundTaskInvalid) {
            [app endBackgroundTask:backgroundTaskIdentifer];
            backgroundTaskIdentifer = UIBackgroundTaskInvalid;
        }
    });
}

Task Completion の制御を
applicationDidEnterBackground:
applicationWillEnterForeground:
の組ではなく
applicationWillResignActive:
applicationDidBecomeActive:
の組を使うのは、スリープボタンを押した時は前者の組み合わせは呼ばれない為。後者であればスリープ => 復帰の時に呼び出されるので今回の目的に適している。

(2011-12-01 追記)iOS5からスリープボタンを押した時でも前者の組み合わせが呼び出されるとのこと。(@hkato193 さん Thanksです)

参考:Cocoaの日々: UIApplicationDelegate のマルチタスキング関連メソッド調査

ちなみに Task Completion を使うとスリープ中もバックグラウンド処理は走り続ける(上限10分)。


ダウンロード


サンプルのソースは GitHub からどうぞ。

BackgroundQueueSample/BackgroundQueueSample at 2011-04-08b from xcatsan/iOS-Sample-Code - GitHub


参考情報



Task Completion の情報は @hkato193さんが書いている第2章の「マルチタスキング」が詳しい。入手可能な日本語の情報ではこれが一番いい。
同様に GCD や Blocks の解説もこの本の第5章「マルチスレッド」(@splhackさん執筆)が詳しくておすすめ。

iOS Application Programming Guide: Executing Code in the Background
Apple提供情報。

[iOS] ディレクトリ配下の総ファイルサイズを計算する 〜 BSDのftsを使う

2011年3月7日月曜日 | Published in | 0 コメント

このエントリーをはてなブックマークに追加

ファイルキャッシュライブラリを作っていてキャッシュディレクトリ配下の総ファイルサイズを計算する必要が出てきた。今回はファイルサイズ計算にBSD由来のライブラリ fts を使ってみた。


fts


Apple のリファレンスを眺めているとディレクトリ走査でパフォーマンスを考慮するなら fts を使うのがいいと出ていた。
File-System Performance Guidelines: Iterating Directory Contents
上記内の Traversing Directories in BSD に記述あり。

調べてみたところ fts というのは BSD系OSで使われているディレクトリ走査用のライブラリのようだ。
fts_open - Linuxの手で行なうページ

(特徴)
・サブディレクトリを含むファイルとディレクトリの一覧を取得できる
・パス名の他、stats構造体を取得できる
・ソートが可能(比較関数を渡すことができる)

サブディレクトリも再帰的にリストアップできるので従来からある readdir系のライブラリよりも使い勝手が良い。また fts ではパス名だけでなく stats構造体も取得できるのでファイルサイズを計算する場合にわざわざ stats関数を呼び出す必要がない。

以下、1ファイル/ディレクトリ毎に取得できる構造体 FTSENT の内容:
typedef struct _ftsent {
             u_short fts_info;               /* flags for FTSENT structure */
             char *fts_accpath;              /* access path */
             char *fts_path;                 /* root path */
             u_short fts_pathlen;            /* strlen(fts_path) */
             char *fts_name;                 /* file name */
             u_short fts_namelen;            /* strlen(fts_name) */
             short fts_level;                /* depth (-1 to N) */
             int fts_errno;                  /* file errno */
             long fts_number;                /* local numeric value */
             void *fts_pointer;              /* local address value */
             struct ftsent *fts_parent;      /* parent directory */
             struct ftsent *fts_link;        /* next file structure */
             struct ftsent *fts_cycle;       /* cycle structure */
             struct stat *fts_statp;         /* stat(2) information */
     } FTSENT;
※ man fts(3) より転載



実装例


こんな感じ。
#include <sys/types.h>
#include <sys/stat.h>
#include <fts.h>

- (IBAction)fts
{
    int size = 0;
    FTS* fts;
    FTSENT *entry;
    char* paths[] = {
        [[self path] cStringUsingEncoding:NSUTF8StringEncoding], NULL
    };
    fts = fts_open(paths, 0, NULL);
    while ((entry = fts_read(fts))) {
        if (entry->fts_info & FTS_DP || entry->fts_level == 0) {
            // ignore post-order
            continue;
        }
        if (entry->fts_info & FTS_F) {
            size += entry->fts_statp->st_size;
        }
    }
    fts_close(fts);
}
再帰コードが要らないのでスッキリして簡単。fts_openで渡すパスは配列で渡す必要がある。また通常だと取得リスト内にディレクトリが2回現れる(pre-order と post-order)ので重複させたくない場合は上記コードのように2回目に出現するタイミング(post-order)を無視するようにしている(entry->fts_info & FTS_DP)。また fts_open の引数で渡したディレクトリ自身がリストに含まれないようにトップレベルのディレクトリ(etnry->fts_level==0 のケース)も無視している。ファイルサイズは FTSENT構造体の持つ fts_statp 経由で簡単に取得できる。

fts_open の第3引数にはソート用の比較関数を渡すことができる。
FTS *fts_open(char * const *path_argv, int options,
                     int (*compar)(const FTSENT **, const FTSENT **));
例えば名前順に一覧を取得したい場合は
fts = fts_open(paths, 0, cmpare);
として compare関数を用意してやる。
int cmpare(const FTSENT **a, const FTSENT **b)
{
    return (strcasecmp((*a)->fts_name, (*b)->fts_name));
}


他の方法との比較


ファイルサイズ計算の方法は fts 以外には readdir と stats の組み合わせの他、NSFileManager を使う方法がある。これらの方法と fts を使う場合の計3つの方法についてかかる時間の比較をやってみた。

条件


下記のテストデータ(ディレクトリ、ファイル)を作成し、rootディレクトリ配下のファイルサイズを計算するのにかかった時間を実機で計測した。
条件:ディレクトリ数 10x10x10、ファイル数 10x10x10、1ファイルのサイズ 1024バイト
実機: iPhone 3GS / iOS 4.2.1
(root)
  |--00
  |   |--00
  |   |   |--00
  |   |   |   |--file-00
  |   |   |   |--file-01
  |   |   |   :
  |   |   |   |--file-09
  |   |   |--01
  |   |   :
  |   |
  |   |--01
  :   :

ソース


readdir を使った場合のソース。
int countStdlib;
int sizeStdlib;
void countdir(const char* path) {
    DIR* dir = opendir(path);
    struct dirent* ent;
    struct stat buf;
    
    char newPath[4096];
    if (dir) {
        for(;;) {
            if ((ent = readdir(dir)) == NULL) {
                break;
            }
            strcpy(newPath, path);
            strcat(newPath, "/");
            strcat(newPath, ent->d_name);

            if (ent->d_type == DT_DIR) {
                if (strcmp(ent->d_name, ".") && strcmp(ent->d_name, "..")) {
                    countdir(newPath);
                }
            } else {
                // file
                stat(newPath, &buf);
                sizeStdlib += buf.st_size;
                countStdlib++;
            }
        }
    }
    closedir(dir);
}

- (IBAction)stdlib
{
    countStdlib = 0;
    sizeStdlib = 0;
    countdir([[self path] cStringUsingEncoding:NSUTF8StringEncoding]);
}

NSFIleManager を使った場合のソース。
- (IBAction)cocoa
{
    int count = 0;
    int size = 0;
    NSFileManager* fileManager = [NSFileManager defaultManager];
    NSError* error = nil;
    NSString* path = [self path];

    for (NSString* filename in [fileManager enumeratorAtPath:[self path]]) { 
        NSDictionary* attributes = [fileManager
            attributesOfItemAtPath:[path stringByAppendingPathComponent:filename]
                                    error:&error];
        if ([[attributes objectForKey:NSFileType] isEqualToString:NSFileTypeRegular]) {
            count++;
            size += [[attributes objectForKey:NSFileSize] intValue];
        }
    }
}

結果


fts            6.6秒
readdir        8.5秒
NSFileManager 40.6秒
fts が一番速いが readdirとの差はそれほど大きくない。readdir の遅れ 2秒近くは関数の再帰処理と stats関数で呼び出し回数が増えている分のオーバヘッドと思われる。NSFileManager はオブジェクトの生成によるオーバヘッドが大きく予想通り他の2つと比べてかなり遅い。

ファイルサイズの取得処理を除いて単純な一覧取得だけに絞って実行してみると次のようになる。
fts            1.3秒 (FTS_NOSTAT オプション)
readdir        1.1秒
NSFileManager 11.0秒
全体的にかなり時間が短縮できることから stats実行の処理が比較的重いことが分かる。

考察


fts は readdir とほぼ同じパフォーマンスを持ちながら、readdirよりも簡潔にコードを記述できる。また標準で stats情報の取得やソート処理が行えるなど高機能である。fts が使える処理系(iOS含む)であればこちらを利用する方が何かと便利だろう。NSFileManager は他の2つに比べると速度性能でかなり劣る。ただ cocoaで記述できるメリットもあるので対象データ量が少ない場合であればこれを使うのも悪くはない。

なお性能が同程度となるともう一つ気になるのはメモリのフットプリント。Instrumentsを使いシミュレータ上でのメモリの変化を調べてみたところ次のようになった。

(1) fts
770KB→934KB (+164KB)

(2) readdir
762KB→934KB(+172KB)

(3) NSFileManager
1MB → 27MB(+26MB)

fts と readdir はほとんど変わらず、両方共にメモリの利用量は多く無い。一方 NSFileManager の場合は NSString の生成コストが大きくかなりのメモリを消費している。

おまけ:fts ソート時の性能


先のケースではソートを行っていなかった。パス名でのソート処理を入れた時の時間を計測してみた。
:
fts = fts_open(paths, 0, cmpare);
  :
}
int cmpare(const FTSENT **a, const FTSENT **b)
{
    return (-strcasecmp((*a)->fts_name, (*b)->fts_name));  // パス名降順
}

結果は 6.6秒とソート未指定時と変わらない。念のためパス内容を NSLog で出して確認してみたがちゃんとパス名降順で一覧が取得できてる。これは速いな。



サンプル


GitHub からどうぞ。
ftsSample at 2011-03-07 from xcatsan/iOS-Sample-Code - GitHub

実行するとボタンの載った画面が現れる。
"setup directories" をタップするとテスト用のディレクトリ/ファイルが生成される。これは数十秒かかるがインストール後の最初の一回だけの実行で良い。

 BSD fts .... fts によるファイルサイズ計算
 stdlib ... readdir を使ったファイルサイズ計算
 Cocoa ... NSFileManager を使ったファイルサイズ計算


参考情報


fts_open

ftsでファイル階層を取得する。: Xo式 実験室(labo.xo-ox.net)

FreeBSDのlsを読む fts(3)でlsを作ってみよう~。 - ボクノス

Recursive read directory - verzeichnis rekursiv auslesen - dirent - GIDForums

Manpage of READDIR

dirent

[iOS] 複数アプリケーション間でのデータ共有 〜 Keychain Services を使った第三の方法

2011年2月7日月曜日 | Published in | 1 コメント

このエントリーをはてなブックマークに追加

※タイトルはあまり深い意味は無い。なんとなく「第三の〜」の響きが良かったので。。

前回紹介した Keychain Services を使えば制限付きながら iOS 上の複数のアプリケーションでデータ共有ができることがわかったのでそれを解説する。

[前回] Cocoaの日々: [iOS] Keychain Services とは


仕組み


Keychain Services に格納されるアイテム(パスワードなど)のアクセス制御は Keychain Access Group(グループ)を元に行われる。アイテムにはこのグループ属性があり、同じグループに所属しているアプリケーションからのみアクセスが許可される。

アプリケーションは複数のグループに所属することができるので、データの共有を目的したグループを用意しておき、複数のアプリケーションでこのグループに所属すれば、このグループに所属するアイテムへそれら複数のアプリケーションがアクセスすることができる。

Keychain Services のアイテムはパスワード、秘密鍵、証明書を格納するようになっているが、CFData(NSData)型 であればパスワードである必要はない。格納したい値を CFData(NSData)へ変換すれば Keychain Services へ格納することができるので任意の値を共有することができる。

Keychain Access Group の詳細は前回の「3. アクセス制御」を参照のこと。


サンプル


プロジェクトを2つ(KeyChainApp-1と KeyChainApp-2)用意し、それぞれのアプリケーションから同じアイテムへアクセスできるかを検証してみた。


KeyChainApp-1


Entitlements.plist

application-identifier: GFDZH8PXXX.com.yourcompany.KeyChainApp-1

アイテム登録コード
- (IBAction)addNewItem
{
 NSData* passwordData = [self.password.text dataUsingEncoding:NSUTF8StringEncoding];

 NSMutableDictionary* attributes = [NSMutableDictionary dictionary];
 [attributes setObject:(id)kSecClassGenericPassword forKey:(id)kSecClass];
 [attributes setObject:(id)self.account.text forKey:(id)kSecAttrAccount];
 [attributes setObject:passwordData forKey:(id)kSecValueData];
 [attributes setObject:@"GFDZH8PXXX.share" forKey:(id)kSecAttrAccessGroup];

 OSStatus err = SecItemAdd((CFDictionaryRef)attributes, NULL);
 if (err == noErr) {
  NSLog(@"SecItemAdd: noErr");
 } else {
  NSLog(@"SecItemAdd: error(%d)", err);
 }
}
登録するアイテムの Keychain Access Group (kSecAttrAccessGroup)に "GFDZH8PXXX.share" を指定している。
※サンプルコードを自分の環境でビルドする場合は "GFDZH8PXX" の箇所を自分のプロビジョニングファイルの app-identifier に書き換えること。

KeyChainApp-2


Entitlements.plist

application-identifier: GFDZH8PXXX.com.yourcompany.KeyChainApp-2

アクセス可能なアイテムをすべてデバッグコンソールへ表示。
- (IBAction)dumpItems
{
 NSMutableDictionary* query = [NSMutableDictionary dictionary];
 
 [query setObject:(id)kSecClassGenericPassword forKey:(id)kSecClass];
 [query setObject:(id)kCFBooleanTrue forKey:(id)kSecReturnAttributes];
 [query setObject:(id)kCFBooleanTrue forKey:(id)kSecReturnData];
 [query setObject:(id)kSecMatchLimitAll forKey:(id)kSecMatchLimit];
 
 CFArrayRef result = nil;
 OSStatus err = SecItemCopyMatching((CFDictionaryRef)query,(CFTypeRef*)&result);
 
 if (err == noErr) {
  NSLog(@"SecItemCopyMatching: noErr");
  NSLog(@"%@", result);
 } else if(err = errSecItemNotFound) {
  NSLog(@"SecItemCopyMatching: errSecItemNotFound");
 } else {
  NSLog(@"SecItemCopyMatching: error(%d)", err);
 }
}
パスワードを更新。
- (IBAction)updateItem
{
 NSMutableDictionary* attributes = nil;
 NSMutableDictionary* query = [NSMutableDictionary dictionary];
 NSData* passwordData = [self.password.text dataUsingEncoding:NSUTF8StringEncoding];
 
 [query setObject:(id)kSecClassGenericPassword forKey:(id)kSecClass];
 [query setObject:(id)self.account.text forKey:(id)kSecAttrAccount];
 
 OSStatus err = SecItemCopyMatching((CFDictionaryRef)query, NULL);
 
 if (err == noErr) {
  // update item
  NSLog(@"SecItemCopyMatching: noErr");
  
  attributes = [NSMutableDictionary dictionary];
  [attributes setObject:passwordData forKey:(id)kSecValueData];
  
  err = SecItemUpdate((CFDictionaryRef)query, (CFDictionaryRef)attributes);
  if (err == noErr) {
   NSLog(@"SecItemUpdate: noErr");
  } else {
   NSLog(@"SecItemUpdate: error(%d)", err);
  }
  
 } else if (err = errSecItemNotFound) {
  // add new item
  NSLog(@"SecItemCopyMatching: errSecItemNotFound");
  
 } else {
  NSLog(@"SecItemCopyMatching: error(%d)", err);
 }
 
}


結果


まず KeyChainApp-1 を立ち上げてアカウント/パスワードを登録(Add new item)する。
Dump items で登録内容を確認しておく。
{
        acct = hashiguchi;
        agrp = "GFDZH8PCUM.share";
        pdmn = ak;
        svce = "";
        "v_Data" = <70617373 3030>;
    }
続いて KeyChainApp-2 を立ち上げ、登録アイテムを表示する(Dump items)。

すると
{
        acct = hashiguchi;
        agrp = "GFDZH8PCUM.share";
        pdmn = ak;
        svce = "";
        "v_Data" = <70617373 3030>;
    }
出た。KeyChainApp-1 で登録したデータをまったく別のアプリ KeyChainApp-2 で読み出すことができた。

更新はどうだろうか。KeyChainApp-1 で登録したこのデータ(パスワード)を KeyChainApp-2 で書き換えてみる。
すると
{
        acct = hashiguchi;
        agrp = "GFDZH8PCUM.share";
        pdmn = ak;
        svce = "";
        "v_Data" = <70617373 3131>;
    }
書き換わった。


ソースコード


GitHub からどうぞ。
KeyChainApp-1 at 2011-02-07 from xcatsan/iOS-Sample-Code - GitHub
KeyChainApp-2 at 2011-02-07 from xcatsan/iOS-Sample-Code - GitHub


制限


複数アプリケーション間でのデータ共有方法として使えることがわかった Keychain Services だが重大な制限もある
同じプロビジョニングファイルから作成されたアプリケーション間でしか
Keychain Services を介したデータ共有は行えない
これは Keychain Access Group の指定方法の制約による。詳細は前回の「3. アクセス制御」を参照のこと。この為、残念ながら他社の作成したアプリとデータ交換を自由に行えるわけではない。


補足


シミュレータの場合、Entitlements.plist は無視されるので今回のように kSecAttrAccessGroup を設定すると登録はエラーとなる(-25243)。シミュレータの場合、前回説明したように Keychain Access Group は常に "test"(固定)となる。

人気の投稿(過去 30日間)