24/7 twenty-four seven

iOS/OS X application programing topics.

Objective-C や Xcode の新しい機能のほとんどは iOS4 以前でも動く

Objective-C Feature Availability Index に一覧表が載ってるのですが、ARCのWeak Reference 以外は iOS 4 以上、ほとんどはすべての iOS バージョンで動くので、古い環境を気にせずにジャンジャン使ってしまってOKです。

新しいリテラルや Subscripting は断然コードが書きやすく読みやすくなるのでこれはうれしいですね。


Objective-C Feature Availability Index

FeatureTools versionsOS X deploymentiOS deployment
Automatic Reference Counting (ARC)Xcode 4.2
(LLVM Compiler 3.0)
Requires modern runtime
Deploys back to OS X v10.7
Deploys back to iOS 5
Automatic Reference Counting without zeroing weak reference (“ARCLite”)Xcode 4.2
(LLVM Compiler 3.0)
Requires modern runtime
Deploys back to OS X v10.6
Deploys back to iOS 4
Default synthesis of @property instance variables and accessor methodsXcode 4.4 (LLVM Compiler 4.0)Requires modern runtime
Deploys back to OS X v10.6
Deploys back to iOS 4
Instance variables in class extensionsXcode 4.2
(LLVM Compiler 3.0)
Requires modern runtimeAll iOS releases
Instance variables in @implementation blockXcode 4.2
(LLVM Compiler 3.0)
Requires modern runtimeAll iOS releases
No forward method prototypes needed in @implementation blockXcode 4.3
(LLVM Compiler 3.1)
All releasesAll iOS releases
NSNumber, NSDictionary and NSArray literalsXcode 4.4
(LLVM Compiler 4.0)
All releasesAll iOS releases
@YES and @NO literalsXcode 4.4 and OS X 10.8 or later SDK
Xcode 4.5 and iOS 6 or later SDK
(LLVM Compiler 4.0)
All releasesAll iOS releases
NSDictionary and NSArray subscriptingXcode 4.4 and OS X 10.8 or later SDK
Xcode 4.5 and iOS 6 or later SDK
(LLVM Compiler 4.0)
Requires modern runtime
Deploys back to OS X v10.6
Deploys back to iOS 4

第1回iphone_dev_jp東京 iPhone/Mac Hackathon 〜みんなが幸せになるハッカソン〜 を開催しました

iphone_dev_jp東京 iPhone/Mac Hackathon : ATND
iPhone_dev_jpで、みんなが幸せになるハッカソンを開催します | fladdict


前回の勉強会で深津さんが「一発もののアプリじゃなくてきちんと使われるライブラリをドキュメント込みで作るハッカソンやったらいいんじゃない?」って話をしていたので、それはすばらしいと思ったのでやってみました。


ハッカソンってやったことなくて、勝手がわからずにかなりギリギリの告知になってしまったのですが、約30名の猛者が集まってくださいました。

とはいえ、私は1日でそんなに書けるものだろうかと不安だったので実はそれまでの1週間である程度メドを立てておこうとか思ってたのですが、意外と時間がなくてぶっつけになってしまいました。
でもなんとかなるもので、やっぱり集中して書いたほうがダンゼン効率がいいんだなあとか今さらながら思いました。


こういう周辺ライブラリは必要だなあと思っててもけっこう普段の時間には書こうと思ってもなかなか筆が進まなかったりするもので、こういう集まりは小規模でもいいので定期的にやっていこうというところを確認できた一日でした。
かなりメリットのあることがわかったので近いうちにまたやると思いますのでまたみなさん集まってくれたらうれしいです。

第3回iphone_dev_jp 東京iPhone/Mac勉強会を開催しました

第3回iphone_dev_jp 東京iPhone/Mac勉強会 : ATND

発表内容と資料のまとめ

http://mtl.recruit.co.jp/blog/2012/07/721_iphone_dev_jp_iphonemacmtl.html
会場を提供してくださったMTLフナミさんのすばらしいまとめです。
すべての資料と発表ごとに分けていただいたUstreamの動画を見ることができます。


↓ Slidrsで共有した資料
第3回 iphone_dev_jp 東京iPhone/Mac勉強会 - Slidrs

ネット配信の録画

↓ ニコニコ動画(プレミアム会員のひとは1週間はタイムシフト視聴できます)
7/21 iPhoneDev-jp 勉強会放送 - 2012/07/21 13:00開始 - ニコニコ生放送

↓ Youtube
http://www.youtube.com/playlist?list=PL286E9699885DAEA8&feature=mh_lolz

CoreData の 属性に現在時刻など固定値以外のデフォルト値を設定する

CoreData のオブジェクトに作成日や更新時刻が自動的に入ったらいいなと思うことってありますよね。固定の値ならばモデルエディタの Defaut の欄に設定しておけば初期値として自動的に設定されますが実行時の現在時刻のように変わる値はモデルエディタでは設定することができません。

このような値をデフォルト値として設定するには NSManagedObject の awakeFromInsert メソッドを使用します。

#import "RecentSearch+CoreData.h"

@implementation RecentSearch(CoreData)

- (void)awakeFromInsert {
    [super awakeFromInsert];
    self.createdAt = [NSDate date];
    self.updatedAt = self.createdAt;
}

↑ このように awakeFromInsert メソッドをオーバライドしておくと、このオブジェクトが新しく作成されたときに自動的に現在時刻が設定されます。


↓ ちなみに、これを書いている途中に発見したのですが、モデルエディタの Default のところに "now" や "today" と入力すると現在時刻が「固定値として」設定されます。


あくまでここで設定できる初期値は固定値なのでイマイチ何の役にたつのか分かりませんが、もしかしたら便利なのかもしれません。

CoreData の NSManagedObject のサブクラスを変更する場合はカテゴリを使うと便利

CoreDataのモデルクラスはXcodeのモデルエディタから自動生成しますが、生成されたクラスにメソッドを追加したりしたいことがあると思います。
そのとき、自動生成されたファイルを直接変更してしまうと、モデルに変更がありモデルクラスを再生成したときにその変更が上書きされてしまいます。

そこで、カテゴリを使って追加部分は別のファイルに分けておくと、モデルクラスを再生成しても後から追加した部分は上書きされずに残るのでそのまま使えます。


例えば下記のようなクラス (Event.h) があるとして、条件でフェッチするメソッドや、日付をフォーマットして返すメソッドを Event+CoreData.h/m や Event+Formatter.h/m として別ファイルに定義します。

// Event.h

#import <Foundation/Foundation.h>
#import <CoreData/CoreData.h>

@class Favorites, History, User;

@interface Event : NSManagedObject

@property (nonatomic, retain) NSNumber * accepted;
@property (nonatomic, retain) NSString * address;
@property (nonatomic, retain) NSString * catch;
@property (nonatomic, retain) NSDate * endedAt;
@property (nonatomic, retain) NSString * eventDescription;
@property (nonatomic, retain) NSNumber * eventID;
@property (nonatomic, retain) NSString * eventURL;
@property (nonatomic, retain) NSNumber * favorite;
〜(略)〜
// Event+CoreData.h

#import <Foundation/Foundation.h>
#import "Event.h"

@interface Event(CoreData)

+ (Event *)eventWithEventID:(NSNumber *)eventID;
+ (NSArray *)eventsWithStartDate:(NSDate *)startDate endDate:(NSDate *)endDate;

@end
// Event+CoreData.m

#import "Event+CoreData.h"
#import "EPCoreDataManager.h"

@implementation Event(CoreData)

+ (Event *)eventWithEventID:(NSNumber *)eventID {
    EPCoreDataManager *manager = [EPCoreDataManager sharedManager];
    NSManagedObjectContext *context = manager.managedObjectContext;
    
    NSPredicate *predicate = [self predicateWithEventID:eventID];
    
    NSFetchRequest *request = [[NSFetchRequest alloc] init];
    request.predicate = predicate;
    
    NSEntityDescription *entity = [NSEntityDescription entityForName:@"Event" inManagedObjectContext:context];
    request.entity = entity;
    
    NSArray *results = [context executeFetchRequest:request error:nil];
    
    Event *event = [results lastObject];
    if (!event) {
        event = [[Event alloc] initWithEntity:entity insertIntoManagedObjectContext:context];
        event.eventID = eventID;
    }
    
    return event;
}

+ (NSArray *)eventsWithStartDate:(NSDate *)startDate endDate:(NSDate *)endDate {
    EPCoreDataManager *manager = [EPCoreDataManager sharedManager];
    NSManagedObjectContext *context = manager.managedObjectContext;
    
    NSPredicate *predicate = [Event predicateWithStartDate:startDate endDate:endDate];
    
    NSFetchRequest *request = [[NSFetchRequest alloc] init];
    request.predicate = predicate;
    
    NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"startedAt" ascending:YES];
    request.sortDescriptors = [NSArray arrayWithObject:sortDescriptor];
    
    NSEntityDescription *entity = [NSEntityDescription entityForName:@"Event" inManagedObjectContext:context];
    request.entity = entity;
    
    NSArray *results = [context executeFetchRequest:request error:nil];
    
    return results;
}

@end
// Event+Formatter.h

#import <Foundation/Foundation.h>
#import "Event.h"

@interface Event(Formatter)

- (NSString *)formattedStartedAt;
- (NSString *)formattedEndedAt;
- (NSString *)formattedUpdatedAt;

@end
// Event+Formatter.m

#import "Event+Formatter.h"

@implementation Event(Formatter)

- (NSString *)formattedStartedAt {
    return [[self dateFormatter] stringFromDate:self.startedAt];
}

- (NSString *)formattedEndedAt {
    return [[self dateFormatter] stringFromDate:self.endedAt];
}

- (NSString *)formattedUpdatedAt {
    return [[self dateFormatter] stringFromDate:self.updatedAt];
}

- (NSDateFormatter *)dateFormatter {
    static NSDateFormatter *dateFormatter;
    static dispatch_once_t once;
    dispatch_once(&once, ^{
        dateFormatter = [[NSDateFormatter alloc] init];
        NSLocale *locale = [NSLocale currentLocale];
        NSArray *preferredLanguages = [NSLocale preferredLanguages];
        if ([preferredLanguages count] > 0) {
            locale = [[NSLocale alloc] initWithLocaleIdentifier:[preferredLanguages objectAtIndex:0]];
        }
        dateFormatter.locale = locale;
        dateFormatter.dateStyle = NSDateFormatterLongStyle;
        dateFormatter.timeStyle = NSDateFormatterShortStyle;
    });
    
    return dateFormatter;
}

@end

iphone_dev_jp 東京iPhone/Mac勉強会のあきすてさんの資料を共有しました



あきすてさんの依頼で私が代わりにSpeakerDeckで共有しました。
当日は話さなかった Unity Asset Server や Unity+Jenkins の話が載っていますのでぜひご覧ください。

iphone_dev_jp 東京iPhone/Mac勉強会 (@akisutesama) // Speaker Deck

iphone_dev_jp 東京iPhone/Mac勉強会を開催しました

iphone_dev_jp 東京iPhone/Mac勉強会 : ATND

ネット配信の録画(ニコニコ動画のプレミアム会員のひとは1週間はタイムシフト視聴できます)

4/14 iPhonedev-jp 勉強発表会 - 2012/04/14 13:00開始 - ニコニコ生放送


東京にいるのにもう長いことiOSの勉強会してない!名古屋とか大阪とか札幌のほうが活発にやってるなんておかしい!っていう感じのことをボードゲームの会のあとでごはんを食べているときに言ったら「じゃあ、やるかー」ということになって瀧内さん (@takiuchi) がサイバーエージェント・ベンチャーズのStartup Base Camp会議室というステキな場所をとってくれてその場で何人か発表してねってお願いしてなんだかんだで勉強会を主催することになりました。

そんそんさん(@sonson_twit)も書いているのですけど、僕もその場にいたメンバー+5〜10人くらいで考えていたので、あれよあれよというまに100人を超えて200人も参加希望があったときには正直どうしようかと思いました。

きっとその時点で僕がヘタレそうになっているということはお見通しだったのでしょう、しゃちさん(@shachi)をはじめ多くのひとが手伝ってくださったおかげでなんとか無事に開催することができました。
あらためて、ありがとうございました。

ただ、iOSの話っておもしろいことはひととおりやっちゃったよね、みたいな感覚があったのも確かで、たくさん来ていただいたものの期待に沿うような内容になるのかは正直半信半疑だったのですが、ふたを開けてみればそんな心配はまったく杞憂で、エクストリームiOSデベロップメントみたいな発表ばかり集まって非常に内容の濃い勉強会になったのでした。

本当にどの発表も楽しくて、それだけでこの会を開いてよかったなあと思いました。


また、今回はニコニコ生放送によるインターネット配信とフィードテイラーの大石さん(@oishi)のご好意によりSYNCNEL会議機能で資料を同期して閲覧する、ということが実現でき当日会場に来られなかったかたもリモートで参加することができ好評だったようです。

SYNCNELの会議機能を iPhone dev jp 勉強会で御利用頂きました! | 関西/大阪のiPhone・iPadアプリ開発 feedtailor Inc. 社長ブログ
ku-sukeのブログ
iPhonedev-jp 勉強発表会 – ニコニコ生放送を見たよ | Macで遊んでる
倍返しだ! : 『iphone_dev_jp iPhone勉強会を大阪で見る会』に行って来ました。





運営スタッフとしての反省は、事前に会場の下見に行っていればあと20人は参加人数の上限を増やすことができたかもしれないこと、ネットワークの準備と当日の仕切りをおざなりにしてしまったので最初の1〜2時間は安定してネット配信とスライドの同期が提供できなかったことが挙げられます。
非常にすばらしい発表をしてもらっていたのでキチッと準備していればちゃんとした形で最初からもっと多くのひとに聞いてもらうことができたかなあと思っています。


ということで不手際はありましたが、心からやってよかったと思いますし次回も近いうちに開催したいと思ってます。
なので発表をお願いしたら快く引き受けてくださいね!と誰ともなく言ってみる。


あとバッファとして用意していた私の発表はけっきょくしなかったので、近いうちに補足して記事にします。


http://shachi.hatenablog.com/entry/2012/04/14/220813
sonson.jp
ハイスピードXcodeコーディング – iphone_dev_jp東京勉強会 | DOTAPON Blog
MacでかんたんKinect - よぴったんこ
A-Liaison BLOG: iphone_dev_jp 東京iPhone/Mac勉強会でしゃべってきた & 反省
【参加してみた】iOSDevJp勉強会 | RE:START
SYNCNELの会議機能を iPhone dev jp 勉強会で御利用頂きました! | 関西/大阪のiPhone・iPadアプリ開発 feedtailor Inc. 社長ブログ
ku-sukeのブログ
iPhonedev-jp 勉強発表会 – ニコニコ生放送を見たよ | Macで遊んでる
倍返しだ! : 『iphone_dev_jp iPhone勉強会を大阪で見る会』に行って来ました。
iOS/MacOS 開発勉強会 - 情報 -> 知識 -> 経験 の足跡
【おもいっきり!iOS!】iphone_dev_jp 東京iPhone/Mac勉強会まとめ #idevjp - ぬんびりぶろぐ
☆iPhoneアプリ開発者のみなさまへ☆プロモコードでプロモーション!「PromoBook」#idevjp - ぬんびりぶろぐ
わたしを見つけて!アプリのApp Store内のSEO最適化 #idevjp - ぬんびりぶろぐ

当日のタイムテーブル

時間 発表者 タイトル
13:00 – 13:10 @takiuchiさん 会場についての諸注意・説明

(トイレの場所、無線LANの設定、自販機の使い方など)
13:10 – 13:35 @5mingame2さん C++とOpenGL ES 1.1の話
13:40 – 14:05 @kamiyanさん ibisPaint の OpenGL ES 2.0
14:10 – 14:35 @cocoponさん キーボードで完結!ハイスピードXcodeコーディング
14:40- 15:05 @nakamura001さん Unity恐くないよ!!
休憩 (10分)
15:15 – 15:40 @yopita_さん Mac でかんたん Kinect
15:45 – 16:10 @akisutesamaさん ・Unity Asset Server使ってみた

・Unity Asset Server+Jenkins

・Unity Cache Server

・Core Animation

・Blocks

・NSURLConnection
16:15 – 16:40 @novi_さん CoreText のはなし
16:45 – 17:10 @sonson_twitさん iCloud
休憩 (10分)
Lightening Talks
17:20 – @monsukeさん プロモコード配布サービス PromoBook について
@umekun123さん App Store内のSEO最適化について調べてみた
@k_katsumi WWDC Checkerの作りかた(Parse.com でらくらくサーバーサイド)

画像キャッシュとダウンローダで参考になる(と思う)オープンソースの実装


ダウンロードした画像をキャッシュするクラスの設計と実装について - 24/7 twenty-four seven

実は上の記事で紹介したクラスを書いた当時、それが必要だったアプリをリリースしたすぐ後くらいにほとんど同じ実装のライブラリを見つけまして。
自分の実装はそんなに間違ってなかったんだなーと安心しつつも、はじめからこれを使っとけばよかったとも思ったので紹介します。

rs/SDWebImage · GitHub


NSCacheは使ってないですが、その他は私が書いたものとよく似ていて、たぶんオーソドックスな実装なんじゃないかなと思います。


SDImageCache と SDWebImageDownloader がライブラリのコアとなるクラスで、これらとうまく連携するように UIImageView や UIButton の拡張や、サポートクラスがあるという構成になっています。

パッと見た感じ、けっこう構成ファイルが多いように見えますが、それぞれのクラスはかなり疎結合になっているのでキャッシュだけ、ダウンローダだけを利用することも簡単です。


前回の記事の、画像が無い場合にデフォルト画像を返す引数はキャッシュのクラスには無いですが、UIImageView のカテゴリで、 - (void)setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder; というメソッドがあるので、やっぱり無かったらプレースホルダとしてデフォルト画像を返せるようにする、というのはよくある要件なのだなあと思いました。


この SDWebImageCarousel という Instagram を Macで閲覧できるアプリケーションでも使われています。

ダウンロードした画像をキャッシュするクラスの設計と実装について

iOS組み込みのキャッシュモジュールNSCacheについて発表しました - ninjinkun's diary


上のような感じでせっかく話を振ってもらったので私がいつも使ってるキャッシュの実装を公開してみます。
だいたい誰が書いてもこんな感じになると思っているのですが、こうしたら便利だよ、とか、それはおかしいとか突っ込んでもらえるとうれしいです。


インターフェースと実装はだいたい下記のようになります。

#import <Foundation/Foundation.h>

typedef void (^ImageCacheResultBlock)(UIImage *image, NSError *error);

@interface ImageCache : NSObject

+ (ImageCache *)sharedInstance;

- (UIImage *)imageWithURL:(NSString *)URL
                    block:(ImageResultBlock)block;
- (UIImage *)imageWithURL:(NSString *)URL 
             defaultImage:(UIImage *)defaultImage 
                    block:(ImageResultBlock)block;

- (void)clearMemoryCache;
- (void)deleteAllCacheFiles;

@end
〜(略)〜

- (UIImage *)imageWithURL:(NSString *)URL 
                    block:(ImageResultBlock)block {    
    return [self imageWithURL:URL defaultImage:nil block:block];
}

- (UIImage *)imageWithURL:(NSString *)URL 
             defaultImage:(UIImage *)defaultImage 
                    block:(ImageResultBlock)block {    
    if (!URL) {
        return defaultImage;
    }
    
    UIImage *cachedImage = [self cachedImageWithURL:URL];
    if (cachedImage) {
        return cachedImage;
    } else {
        cachedImage = defaultImage;
    }
    
    __block ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:[NSURL URLWithString:URL]];
    
    [request setCompletionBlock:^{
        NSData *data = [request responseData];
        UIImage *image = [UIImage imageWithData:data];
        if (image) {
            [self storeImage:image data:data URL:URL];
            block(image, nil);
        } else {
            block(nil, [NSError errorWithDomain:@"ImageCacheErrorDomain" code:0 userInfo:nil]);
        }
    }];
    [request setFailedBlock:^{
        block(nil, request.error);
    }];
    
    [networkQueue addOperation:request];
    
    return cachedImage;
}

設計の勘所

この例だと、キャッシュとダウンローダが一体になっていて、キャッシュに無かったら自動的にダウンロードしますが、場合に応じてダウンローダは別にすることもあります。

だいたい、以下の3パターンをプロジェクトに応じて使い分けています。


使い分けの目安としては、画像以外にもAPIアクセスがある場合(たいていはそうでしょうけど)は別にして、APIのアクセスを含むネットワークのアクセスを別のクラスにまとめてしまって、キャッシュクラスではネットワークアクセスは行わないようにするほうがスッキリすることが多いですね。


または、テーブルビューに表示する場合は、スクロール中にはダウンロードを実行したくないとか、画面に表示されている行の画像を先にダウンロードしたいとか、細かく挙動を制御したくなることがけっこうありますので、キャッシュからの取得とネットワークからの取得がコントロールできるように分けたほうがうまくいきます。


まあ、よっぽど小規模のアプリケーションでなければ、ダウンローダとキャッシュは分けたほうが融通が利いていいと思います。

2番目と3番目の違いは、キャッシュクラスに直接アクセスするかどうかです。キャッシュクラスに直接アクセスせずにダウンローダやAPIアクセスのクラスを通してのみ画像を取得するようにすると、コードがシンプルになるのですが、先に述べたようにダウンロードのタイミングを細かく制御する場合は3番目の設計にすることが多いです。


使い方は下のようになります。

UIImage *cachedImage = 
 [[ImageCache sharedInstance] imageWithURL:photoURL 
                                     block:^(UIImage *image, NSError *error)
 {
     cell.photo = image;
 }];

cell.photo = cachedImage;


たいていはこんなにシンプルにはならなくて実際は下のようになることが多いです。
キャッシュに無くてスクロール中やドラッグ中でない場合のみネットワークからダウンロードします。

UIImage *cachedImage = 
 [[ImageCache sharedInstance] cachedImageWithURL:photoURL];
if (!cachedImage) {
    if (!tableView.dragging && !tableView.decelerating) {
        [ImageDownloader downloaderWithURL:photoURL
                                  delegate:self 
                                  userInfo:[NSDictionary dictionaryWithObject:indexPath forKey:@"indexPath"]];
    }
}

cell.photo = cachedImage;

実装のTips

デフォルト画像をパラメータで指定できるようにしておく

キャッシュクラスあるいはAPIクラスの画像取得のメソッドには、デフォルト画像として無ければそれ自身をそのまま返すパラメータを用意しておくと便利です。
どう便利かというと、ユーザーアイコンなどで画像が未ダウンロード、あるいは設定されてないときにデフォルトアイコンを表示する、という要件はよくあると思いますが、それをシンプルに実装することができます。

UIImage *cachedIconImage = 
 [[ImageCache sharedInstance] imageWithURL:userIconURL 
                              defaultImage:[UIImage imageNamed:@"defaultIcon.png"]
                                     block:^(UIImage *image, NSError *error)
 {
     cell.userIcon = image;
 }];

cell.userIcon = cachedIconImage;
NSCacheを使う

キャッシュはディスクキャッシュをメインで使いつつ、メモリキャッシュと併用しますが、メモリキャッシュにはNSCacheを使うのが便利です。

NSCacheについては冒頭でも引用した下記の記事が詳しいです。
iOS組み込みのキャッシュモジュールNSCacheについて発表しました - ninjinkun's diary


簡単にいうとNSCacheはキャッシュ用に便利は機能が追加されたNSMutableDictionaryです。
格納できるオブジェクト数の上限や容量の上限を決めて超えたぶんは自動的に削除されるとか、自動削除のタイミングで処理を実行することができるなどです。


私は数の上限だけを設定して、容量の制限は使いません。
サムネイル画像やアイコン画像とメイン画像で容量が全然違うよ、っていう場合はNSCacheのインスタンスを複数使ってそれぞれに上限を設定して使い分けます。

下記は、NSCacheを複数使い分ける例です。

cache = [[NSCache alloc] init];
cache.countLimit = 20;

thumbnailCache = [[NSCache alloc] init];
thumbnailCache.countLimit = 100;
ディスクキャッシュのファイル名はMD5ハッシュ値を使う

ダウンロードした画像データをディスクに保存するときのファイル名ですが、ダウンロード先のURLはスラッシュ"/"やコロン":"がパスの文字列としてジャマになったり、日本語が含まれていたりと面倒なのでURLからMD5ハッシュ値を計算して、それをファイル名に使います。

ついでに1つのディレクトリに大量のファイルを入れると速度低下が心配なので適当にバラけるようにハッシュ値の最初の2文字を使ってサブディレクトリに小分けします。


実装は次のようになります。

+ (NSString *)keyForURL:(NSString *)URL {
	if ([URL length] == 0) {
		return nil;
	}
	const char *cStr = [URL UTF8String];
	unsigned char result[16];
	CC_MD5(cStr, (CC_LONG)strlen(cStr), result);
	return [NSString stringWithFormat:@"%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X",
            result[0], result[1], result[2], result[3], result[4], result[5], result[6], result[7],result[8], result[9], result[10], result[11],result[12], result[13], result[14], result[15]]; 	
}

- (NSString *)pathForKey:(NSString *)key {
    NSString *path = [NSString stringWithFormat:@"%@/%@/%@", cacheDirectory, [key substringToIndex:2], key];
    return path;
}

- (void)storeImage:(UIImage *)image data:(NSData *)data URL:(NSString *)URL {
    NSString *key = [KDImageCache keyForURL:URL];
    [cache setObject:image forKey:key];
    
    [data writeToFile:[self pathForKey:key] atomically:NO];
}


以上です。
今回用いたサンプルの完全なコードを下に掲載しておきます。
ツッコミ歓迎です。

#import "ImageCache.h"
#import "ASIHTTPRequest.h"
#import <CommonCrypto/CommonHMAC.h>

@interface ImageCache() {
    NSFileManager *fileManager;
    NSString *cacheDirectory;
    
    NSCache *cache;
    
    NSOperationQueue *networkQueue;
}

@end

@implementation ImageCache

+ (ImageCache *)sharedInstance {
    static ImageCache *sharedInstance;
    static dispatch_once_t pred;
    dispatch_once(&pred, ^{
        sharedInstance = [[ImageCache alloc] init];
    });
    
    return sharedInstance;
}

- (id)init {
    self = [super init];
    if (self) {
        [[NSNotificationCenter defaultCenter] 
          addObserver:self 
             selector:@selector(didReceiveMemoryWarning:)
                 name:UIApplicationDidReceiveMemoryWarningNotification 
               object:nil];
        
        cache = [[NSCache alloc] init];
        cache.countLimit = 20;
        
        fileManager = [[NSFileManager alloc] init];
        
        NSArray *paths = 
         NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
        cacheDirectory = [[[paths lastObject] 
                            stringByAppendingPathComponent:@"Images"] retain];
        
        [self createDirectories];
        
        networkQueue = [[NSOperationQueue alloc] init];
        [networkQueue setMaxConcurrentOperationCount:1];
    }
    return self;
}

- (void)dealloc {
    [[NSNotificationCenter defaultCenter] removeObserver:self];
    [fileManager release];
    [cacheDirectory release];
    [cache release];
    [networkQueue release];
    [super dealloc];
}

- (void)didReceiveMemoryWarning:(NSNotification *)notif {
    [self clearMemoryCache];
}

- (void)createDirectories {
    BOOL isDirectory = NO;
    BOOL exists = [fileManager fileExistsAtPath:cacheDirectory 
                                    isDirectory:&isDirectory];
    if (!exists || !isDirectory) {
        [fileManager createDirectoryAtPath:cacheDirectory 
               withIntermediateDirectories:YES 
                                attributes:nil 
                                     error:nil];
    }
    for (int i = 0; i < 16; i++) {
        for (int j = 0; j < 16; j++) {
            NSString *subDir = 
             [NSString stringWithFormat:@"%@/%X%X", cacheDirectory, i, j];
            BOOL isDir = NO;
            BOOL existsSubDir = 
             [fileManager fileExistsAtPath:subDir isDirectory:&isDir];
            if (!existsSubDir || !isDir) {
                [fileManager createDirectoryAtPath:subDir 
                       withIntermediateDirectories:YES 
                                        attributes:nil 
                                             error:nil];
            }
        }
    }
}

#pragma mark -

+ (NSString *)keyForURL:(NSString *)URL {
	if ([URL length] == 0) {
		return nil;
	}
	const char *cStr = [URL UTF8String];
	unsigned char result[16];
	CC_MD5(cStr, (CC_LONG)strlen(cStr), result);
	return [NSString stringWithFormat:@"%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X",
            result[0], result[1], result[2], result[3], result[4], result[5], result[6], result[7],
            result[8], result[9], result[10], result[11],result[12], result[13], result[14], result[15]]; 	
}

- (NSString *)pathForKey:(NSString *)key {
    NSString *path = [NSString stringWithFormat:@"%@/%@/%@", cacheDirectory, [key substringToIndex:2], key];
    return path;
}

#pragma mark -

- (UIImage *)cachedImageWithURL:(NSString *)URL {
    NSString *key = [ImageCache keyForURL:URL];
    UIImage *cachedImage = [cache objectForKey:key];
    if (cachedImage) {
        return cachedImage;
    }
    
    cachedImage = [UIImage imageWithContentsOfFile:[self pathForKey:key]];
    if (cachedImage) {
        [cache setObject:cachedImage forKey:key];
    }
    
    return cachedImage;
}

#pragma mark -

- (void)storeImage:(UIImage *)image data:(NSData *)data URL:(NSString *)URL {
    NSString *key = [ImageCache keyForURL:URL];
    [cache setObject:image forKey:key];
    
    [data writeToFile:[self pathForKey:key] atomically:NO];
}

- (void)clearMemoryCache {
    [cache removeAllObjects];
}

- (void)deleteAllCacheFiles {
    [cache removeAllObjects];
    
    if ([fileManager fileExistsAtPath:cacheDirectory]) {
        if ([fileManager removeItemAtPath:cacheDirectory error:nil]) {
            [self createDirectories];
        }
    }
    
    BOOL isDirectory = NO;
    BOOL exists = [fileManager fileExistsAtPath:cacheDirectory isDirectory:&isDirectory];
    if (!exists || !isDirectory) {
        [fileManager createDirectoryAtPath:cacheDirectory 
               withIntermediateDirectories:YES 
                                attributes:nil 
                                     error:nil];
    }
}

#pragma mark -

- (UIImage *)imageWithURL:(NSString *)URL 
                    block:(ImageResultBlock)block {    
    return [self imageWithURL:URL defaultImage:nil block:block];
}

- (UIImage *)imageWithURL:(NSString *)URL 
             defaultImage:(UIImage *)defaultImage 
                    block:(ImageResultBlock)block {    
    if (!URL) {
        return defaultImage;
    }
    
    UIImage *cachedImage = [self cachedImageWithURL:URL];
    if (cachedImage) {
        return cachedImage;
    }
    
    __block ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:[NSURL URLWithString:URL]];
    
    [request setCompletionBlock:^{
        NSData *data = [request responseData];
        UIImage *image = [UIImage imageWithData:data];
        if (image) {
            [self storeImage:image data:data URL:URL];
            block(image, nil);
        } else {
            block(nil, [NSError errorWithDomain:@"ImageCacheErrorDomain" code:0 userInfo:nil]);
        }
    }];
    [request setFailedBlock:^{
        block(nil, request.error);
    }];
    
    [networkQueue addOperation:request];
    
    return defaultImage;
}

@end

iOS 5.1 ではスクロールビューをスクロールするたびに少しずつメモリリークが起こる。

iOS 5.1 では UIScrollViewにバグがあるようで慣性スクロールが終わるタイミングで少しずつメモリがリークしてしまうようです。
UIScrollView を継承している UITableView や UITextView も同じ不具合が発生します。
内部に UIScrollView を保持している UIWebView も同様です。


Xcode 4.3.1 の Single View Application に下記のコードを追加しただけのアプリケーションと Instruments の Leaks を使用して検証しました。

- (void)viewDidLoad
{
    [super viewDidLoad];

    UIScrollView *scrollView = [[UIScrollView alloc] initWithFrame:CGRectMake(0.0f, 0.0f, 320.0f, 460.0f)];
    scrollView.contentSize = CGSizeMake(320.0f, 920.0f);
    [self.view addSubview:scrollView];
    [scrollView release];
}

iOS 5.1 ではシミュレータでも iPhone でも iPad でもメモリリークが発生しました。
5.0.1 や 4.3.5 ではシミュレータ、デバイスとも再現しませんでした。


アップルの Developer Forums にもこの問題についての書き込みがありました。

Developer Forums: Elements App Memory Leak


アプリケーション側で対処できることは無さそうなので私たちにできることはバグレポートを送ってできる限り早く修正してもらうことですね。

Apple Bug Reporter

音声を使ってハンズフリーでアプリケーションを操作するサンプル

iOS 5.1 からプログラムからも利用できるようになった音声入力を使ってアプリケーションを操作するサンプルを公開しました。

kishikawakatsumi/VoiceNavigation · GitHub




このサンプルでは「〜を検索」と話すとその言葉で Google 検索を実行します。
「戻る」「進む」と話すことでブラウザの「戻る/進む」を実行できます。


一定の間隔で自動的に音声入力の開始/終了を繰り返すことで常に入力を受け取れるようにしています。
開始/終了を任意のタイミングで実行することもできるので何らかの合図(カメラや振動)によって開始するようにしてもいいかもしれません。


認識精度が優れているのでパターンマッチを増やすだけでもいろいろなことができておもしろいのではないでしょうか。
ぜひいろいろ試してみてください。


※読めばわかると思いますが、音声入力の開始と終了に非公開の API を使用していますので、残念ながらそのまま AppStore に申請することはできません。
※iPad で使ったらダンゼン便利、と思ってユニバーサルアプリとして作ってますが iPad の UI はまだ書いてません。
※なぜかバックグラウンドから戻ってきたあと動作が怪しくなります。

iOS 5.1 の音声入力を使ってアプリケーションを操作してみる

iOS 5.1 から日本語の音声入力がサポートされました。さらに API にも UIDictationPhrase など音声認識ができそうなクラスが追加されています。
これはプログラムから音声認識を使うことができるのか?そうすると音声を使ってハンズフリーでアプリケーションの操作ができるかと思いましたが調べてみるとそんなにうまい話ではありませんでした。


まず iOS 5.1 で追加された関係のありそうな API を見てみましょう。

UITextInput.h
  • Added UIDictationPhrase
  • Added UIDictationPhrase.alternativeInterpretations
  • Added UIDictationPhrase.text
  • Added -[UITextInput dictationRecognitionFailed]
  • Added -[UITextInput dictationRecordingDidEnd]
  • Added -[UITextInput insertDictationResult:]


UIDictationPhrase といかにも便利そうな名前のクラスですが、リファレンスをよく読んでみると text プロパティと alternativeInterpretations プロパティ以外はメソッドもなく、単に UITextInput protocol の insertDictationResult: の引数に結果を詰めるだけのクラスということがわかります。

さらに読み進めていくと追加された API はすべて UITextInput protocol のメソッドであり要するに音声入力はあくまでもテキスト入力の一環で、 UITextInput protocol を通して入力を受け取ることしかできないということがわかります。

つまり手順としては

  1. UITextInput protocol を実装して
  2. ファーストレスポンダになりキーボードを出して
  3. キーボードの音声入力ボタンで録音を開始して
  4. 完了ボタンで録音した音声をサーバに送り
  5. 結果を UITextInput protocol のメソッドを通して受け取る

ということになります。


キーボードは自動で表示するとしても、入力の開始と終了でキーボードのボタンを手で押す必要があるというのは今回の目的においてはあまり便利ではありません。

そこでどうにか自動的に入力を開始/終了することができないか調べてみたところ、UIDictationController というクラスにそれっぽいメソッドがありました。

@interface UIDictationController
〜(略)〜
+ (id)sharedInstance;
+ (id)activeInstance;
- (void)stopDictation;
- (void)cancelDictation;
- (void)startDictation;
- (void)startRecordingLimitTimer;
- (void)cancelRecordingLimitTimer;
〜(略)〜
@end

実験してみると、startDictation, stopDictation, cancelDictation メソッドでそれぞれ、開始・終了・キャンセルを実行できることがわかりましたので今回はこれを利用することにしました。
公開されていないクラスのため、このままでは AppStore に申請することはできませんが、今回は実験ということで。


ということでだいたい実現できそうな気になってきたので以下のように実装してみました。
まず、なにはなくとも UITextInput protocol を実装します。 UITextInput protocol は UIKeyInput protocol を継承しているのでそちらのメソッドも実装します。
カスタムのテキストビューを作るためにテキスト入力システムとやりとりするものなので実装しなければいけないメソッドがかなりたくさんありますが、今回はテキスト入力をまじめにハンドリングする必要はないので、ほとんど空の実装でオーケーです。

@interface VNTextInputView : UIView<UITextInput, UIGestureRecognizerDelegate>

@end

#pragma mark UIKeyInput methods

- (void)deleteBackward {
    
}

〜(略)〜

#pragma mark UITextInput methods

- (NSString *)textInRange:(UITextRange *)range {
    return nil;
}

〜(略)〜

#pragma mark -

- (void)insertDictationResult:(NSArray *)dictationResult {
    [[NSNotificationCenter defaultCenter] postNotificationName:VNDictationRecognitionSucceededNotification
                                                        object:self 
                                                      userInfo:[NSDictionary dictionaryWithObject:dictationResult forKey:VNDictationResultKey]];
}

- (void)dictationRecordingDidEnd {
    [[NSNotificationCenter defaultCenter] postNotificationName:VNDictationRecordingDidEndNotification object:self];
}

- (void)dictationRecognitionFailed {
    [[NSNotificationCenter defaultCenter] postNotificationName:VNDictationRecognitionFailedNotification object:self];
}

@end


iOS 5.1 で追加された3つのメソッドで音声入力をハンドリングします。
音声入力ボタンを押す、あるいは startDictation メソッドで開始したあと、完了ボタン、あるいは stopDictation メソッドが呼ばれたタイミングで dictationRecordingDidEnd が呼ばれます。
そして自動的にサーバに録音された音声データが送られます。
サーバの処理が完了した段階で、何らかのテキストが認識された場合は insertDictationResult: が、音声認識ができなかった場合は dictationRecognitionFailed のいずれかが呼ばれます。

結果は文字列として UIDictationPhrase の text プロパティに格納されており、1つの音声データに対して複数の候補がある場合は alternativeInterpretations プロパティに配列で格納されて受け取れます。UIDictationPhrase はある程度のまとまりごとに配列になっています。


これで任意のタイミングで音声入力を利用できるようになりました。
認識の精度はかなり高いのでパターンマッチを工夫するだけでも実用になりそうです。
使い道としてはハンズフリーでもアプリケーションの操作や、別のプログラムを呼び出したり、家電のリモコンとして使ってもおもしろそうです。


ここまでを簡単な形に整理して GitHub で公開しています。
ぜひ試して改良してみてください。

kishikawakatsumi/VoiceNavigation · GitHub

Titanium mobile の KeyChain モジュール TiKeyChainStore を書きました。【添削希望】

UICKeyChainStore を応用して Titanium mobile の KeyChain モジュール TiKeyChainStore を書きました。

kishikawakatsumi/TiKeyChainStore · GitHub


下記のように使います。

var store = tikeychainstore.createKeychainStore({
  service: 'com.kishikawakatsumi.ti' // optional
});

store.setKeyChainItem ({
  key: 'userame',
  value: 'kishikawakatsumi@mac.com'
});

store.setKeyChainItem ({
  key: 'password',
  value: 'password1234'
});

store.synchronize;

Ti.API.info(store.description); // debug print


Titanium のモジュールは初めて書いたので、詳しい方に添削してもらえるとうれしいです。

KeyChain のデータを操作するラッパークラス UICKeyChainStore を書きました。

アップルのサンプルコード GenericKeychain に含まれる KeyChain のラッパー KeychainItemWrapper.m の実装が微妙だったので書きました。

kishikawakatsumi/UICKeyChainStore · GitHub


KeychainItemWrapper クラスには下記で報告されている問題や、
A-Liaison BLOG: KeychainItemWrapper を改造して、複数の Keychain Item に同時にアクセス出来るようにしてみた


下記の箇所でメモリーリークする問題があったり、使い勝手もよくないので、そのまま使うのはおすすめしません。

- (void)resetKeychainItem
{
	OSStatus junk = noErr;
    if (!keychainItemData) 
    {
        self.keychainItemData = [[NSMutableDictionary alloc] init]; // <= メモリーリークする!


というわけで、あたらしく書いて見ました。

使い方

  1. リンクするフレームワークに Security.framework を追加します。
  2. UICKeyChainStore.h と UICKeyChainStore.m をプロジェクトに追加します。

クラスメソッドを使って値を操作する

便利メソッドとしてクラスメソッドを用意してあります。キーと値を指定するだけで簡単に使えます。
サービス名を指定しない場合は自動的に Bundle ID がサービス名になります。


キーと値を指定して値を追加・更新する。

[UICKeyChainStore setString:@"kishikawakatsumi" forKey:@"username"];
[UICKeyChainStore setString:@"password1234" forKey:@"password"];

//=> ["username" = "kishikawakatsumi", "password" = "password1234"]

サービス名を明示的に指定することもできます。

[UICKeyChainStore setString:@"kishikawakatsumi" forKey:@"username" service:@"com.kishikawakatsumi"];
[UICKeyChainStore setString:@"password1234" forKey:@"password" service:@"com.kishikawakatsumi"];


値をキーチェーンから削除するにはキーを指定します。

[UICKeyChainStore removeItemForKey:@"username"];
[UICKeyChainStore removeItemForKey:@"password"];

サービス名を指定していた場合は、サービス名も含めて指定します。

[UICKeyChainStore removeItemForKey:@"username" service:@"com.kishikawakatsumi"];
[UICKeyChainStore removeItemForKey:@"password" service:@"com.kishikawakatsumi"];

KeyChainStore オブジェクトを使って値を操作する

複数の項目を追加・更新する場合は UICKeyChainStore のインスタンスを作成するほうが便利です。
NSUserDefaults のような使い勝手になります。


デフォルトのサービス名 (Bundle ID) の KeyChainStore を作成した場合。

UICKeyChainStore *store = [UICKeyChainStore keyChain];

[store setString:@"kishikawakatsumi@mac.com" forKey:@"username"];
[store setString:@"password1234" forKey:@"password"];

[store synchronize]; // Write to keychain.


サービス名を指定して KeyChainStore を作成した場合。。

UICKeyChainStore *store = [UICKeyChainStore keyChainStoreWithService:@"com.kishikawakatsumi"];

[store removeItemForKey:@"username"];
[store removeItemForKey:@"password"];

[store synchronize]; // Write to keychain.

UINavigationBar に複数の UIBarButtonItem を配置するには

UINavigationBar は基本的に左右 (leftBarButtnItem, rightarButtonItem) に一つずつしかボタンを配置することができません。
しかし、ちょっと工夫をするとその制限を突破することができます。
(まあ物理的なスペースの関係でせいぜい2つか3つがやっとなのですけどね)

方法その1. UIToolbar を UIBarButtonItem として配置し、その中に UIBarButtonItem を並べる


もっとも見た目がキレイに仕上がる方法です。

UINavigationBar の leftBarButtnItem と rightarButtonItem は UIBarButtonItem のインスタンスをそれぞれ1つずつしか代入できませんが、UIToolbar を UIBarButtonItem として作成することで、その UIToolbar に複数のボタンを配置することができるようになります。

UIBarButtonItem *sendButton = [[UIBarButtonItem alloc] initWithTitle:NSLocalizedString(@"Send", nil) style:UIBarButtonItemStyleBordered target:self action:@selector(send:)];

UIBarButtonItem *cameraButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemCamera target:self action:@selector(cancel:)];
cameraButton.style = UIBarButtonItemStyleBordered;

UIBarButtonItem *space = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace target:nil action:nil];

UIToolbar *toolbar = [[MyToolbar alloc] initWithFrame:CGRectMake(0.0f, 0.0f, 110.0f, 44.0f)];
toolbar.backgroundColor = [UIColor clearColor];
toolbar.autoresizingMask = UIViewAutoresizingFlexibleHeight;
UIBarButtonItem *toolbarBarButtonItem = [[UIBarButtonItem alloc] initWithCustomView:toolbar];
[toolbar release];

toolbar.items = [NSArray arrayWithObjects:space, cameraButton, sendButton, nil];
[space release];
[cameraButton release];
[sendButton release];

self.navigationItem.rightBarButtonItem = toolbarBarButtonItem;
[toolbarBarButtonItem release];


ただ、標準の UIToolbar の外観は UINavigationBar の外観と微妙な差異があるので、このままでは境界が目立ってしまいます。


そこで UIToolbar のサブクラスを作成し、空の drawRect: メソッドでオーバーライドし、標準の描画を無効にします。

@interface MyToolbar : UIToolbar
@end

@implementation MyToolbar

- (void)drawRect:(CGRect)rect {
    
}

@end


すると、このような状態になります。


真っ黒になってしまったので、背景色を透明に設定します。
drawRect: メソッドの処理は backgroundColor と無関係というのがミソですね。

toolbar.backgroundColor = [UIColor clearColor];


キレイになりました。


もし、横方向の画面をサポートする場合、ナビゲーションバーは横方向では 30 ピクセルの幅に変わりますので、ツールバーの autoresizingMask プロパティに UIViewAutoresizingFlexibleHeight を設定しておきましょう。

toolbar.autoresizingMask = UIViewAutoresizingFlexibleHeight;



UIViewAutoresizingFlexibleHeight を指定しない場合は下のようになります。すこしボタンの大きさがアンバランスですね。


色をつけると違いが分かりやすくなります。


tintColor を設定すれば色も変えられます。

方法その2. UISegmentedControl をボタンのように使用する

標準のメールアプリでも採用されている方法です。
メールアプリの「前へ」「次へ」のように同じような動作をするボタンを並べる場合は良い方法だと思います。


titleView プロパティに乗せると3つのボタンを並べても余裕があります。


UISegmentedControl をボタンとして使用するには、momentary プロパティを YES に、アクションは UIControlEventValueChanged を設定します。

segmentedControl = [[UISegmentedControl alloc] initWithItems:items];
segmentedControl.segmentedControlStyle = UISegmentedControlStyleBar;
segmentedControl.momentary = YES;
segmentedControl.frame = CGRectMake(segmentedControl.frame.origin.x, segmentedControl.frame.origin.y, segmentedControl.frame.size.width + 16.0f * [items count], segmentedControl.frame.size.height);
[segmentedControl addTarget:self action:@selector(segmentedControlAction:) forControlEvents:UIControlEventValueChanged];
[self.navigationItem setTitleView:segmentedControl];
[segmentedControl release];

方法その3. UIToolbar を上部のナビゲーションバーの位置に配置する

簡単ですがオススメしません。
というのも UIToolbar は画面の下部に配置するようにデザインされているため、上部に配置するとどうしても外観に違和感がでてしまうからです。
(iOS 4.x までの iPhone の場合。iPad のツールバーは上下どちらに配置してもいいようにデザインされている。)


ナビゲーションバーの位置に配置したツールバー。
ステータスバーとの境界が不自然ですね。