はてな touch 1.2.7 がアップルの審査を通過しました。
iOS 6, iPhone 5をサポートしました。
変更点
- iOS 6, iPhone 5をサポートしました。
はてな touch 1.2.7 がアップルの審査を通過しました。
iOS 6, iPhone 5をサポートしました。
Objective-C Feature Availability Index に一覧表が載ってるのですが、ARCのWeak Reference 以外は iOS 4 以上、ほとんどはすべての iOS バージョンで動くので、古い環境を気にせずにジャンジャン使ってしまってOKです。
新しいリテラルや Subscripting は断然コードが書きやすく読みやすくなるのでこれはうれしいですね。
Objective-C Feature Availability Index
Feature | Tools versions | OS X deployment | iOS 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 methods | Xcode 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 extensions | Xcode 4.2 (LLVM Compiler 3.0) | Requires modern runtime | All iOS releases |
Instance variables in @implementation block | Xcode 4.2 (LLVM Compiler 3.0) | Requires modern runtime | All iOS releases |
No forward method prototypes needed in @implementation block | Xcode 4.3 (LLVM Compiler 3.1) | All releases | All iOS releases |
NSNumber, NSDictionary and NSArray literals | Xcode 4.4 (LLVM Compiler 4.0) | All releases | All iOS releases |
@YES and @NO literals | Xcode 4.4 and OS X 10.8 or later SDK Xcode 4.5 and iOS 6 or later SDK (LLVM Compiler 4.0) | All releases | All iOS releases |
NSDictionary and NSArray subscripting | Xcode 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 |
ShareKit/Classes/ShareKit/Sharers/Services/Hatena at hatena · kishikawakatsumi/ShareKit · GitHub
ShareKit といういろいろな外部サービスとの連携機能を提供するライブラリがあるのですが、それのプラグイン (ShareKit では Sharer と呼びます) として「はてなブックマーク」とリンクを共有するものを書きました。
オリジナルを fork して hatena ブランチにコミットしています。利用するには clone して hatena ブランチに切り替えます。
本家にPull Request を送ったので、もしかしたらマージされるかもしれません。
(既存の Sharer に日本のサービスはなかったのでイマイチ勝手がわかりませんでした。)
↓ 発端は fladdict さんとの下記のやりとりです。
アプリではてぶをサポートしようとしたら、APIがキモイしメンドクサイので断念した。ライブラリないかな。なければあきらめよう。
2012-08-26 23:12:48 via web
@fladdict はてブするほう?なんやったら書きますよ。はてな応援。
2012-08-26 23:29:18 via Echofon to @fladdict
@k_katsumi おお、ありがとうございます! とりあえず、昨日おみせしたやつTestFlightしますね
2012-08-27 00:01:18 via web to @k_katsumi
@fladdict どうもっす。仕様とあと適当に締切を設定してくれたら間に合うように書きます。
2012-08-27 00:06:16 via Echofon to @fladdict
@k_katsumi うぃ、仕様書というか、ShareKitがわりとイイ感じなのでこれにはてなを追加しようかなぁと思ってたところなんです。URL
2012-08-27 00:08:27 via web to @k_katsumi
@fladdict なるほど。じゃあShareKitを見て合わせてみます。なんやったらPullReqしよ。
2012-08-27 00:10:24 via Echofon to @fladdict
@akisutesama @k_katsumi sharekitはステキライブラリですよ。もっと流行るべき
2012-08-27 01:13:56 via web to @akisutesama
はてなブックマークへの投稿は昔書いたことがあったので、認証を OAuth にしたらあとはカンタンそうだなと思って安請け合いしました。いやまあ、特に難しいことはなかったんですけど。
@fladdict ShareKitってこのリポジトリで合ってます?最後の更新が2年以上前っぽいですけど。 URL
2012-08-31 16:45:26 via Echofon to @fladdict
@fladdict はてなブックマークのShareKit拡張書いたよー URL hatenaブランチをチェックアウトしてください。
2012-09-04 04:26:28 via YoruFukurou to @fladdict
@k_katsumi ありがとうございまっす!!! さっそく組み込むです!
2012-09-04 11:52:08 via web to @k_katsumi
@fladdict OAuthだけでいいっすよね。ただiOS6でサンブルプロジェクトで動かすとたまにクラッシュすることがあった気がします。まだ直せてない。あとOAMutableURLRequestを拡張してるのは付属のに2重エンコードのバグがあったからです。
2012-09-04 11:53:28 via Echofon to @fladdict
@k_katsumi おお、2重エンコードなんてしてたのですねぇ。そのfix本家におつたえしたら喜ばれそう。
2012-09-04 11:55:36 via web to @k_katsumi
ShareKitのOAuthConsumerの2重エンコードこれね。 URL これだとKeyとValueをURLエンコードしたあとさらにエンコードするから「extraOAuthParametersがある場合に」シグネチャがおかしくなる。
@k_katsumi おぉ。なるほです!
2012-09-04 12:06:08 via web to @k_katsumi
↑ ちょっとだけハマったのは、付属している OAuth ライブラリ (OAuthConsumer) が不具合のあるリビジョンのもので、シグネチャを組み立てるときに追加の OAuth パラメータの部分を2重に URL エンコードしていました。
そのため、はてなの OAuth は必ずコールバック URL を oauth_callback パラメータで指定する必要があるのですが、シグネチャが invalid になって OAuth が成功しないということがありました。
↓ こちらの不具合は別の Pull Request でパッチを送ったので現在は修正されています。
Fix double URI encode for extra OAuth parameters. by kishikawakatsumi · Pull Request #515 · ShareKit/ShareKit · GitHub
@k_katsumi うーん、「認証要求に問題が発生しました」って怒られる。Consumer = APIキー Secret = 秘密鍵ですよね。Callbackはアプリ登録時のものにしてます。plistじゃなくてクラス内にキー直書きでいいんですよね。
2012-09-04 12:37:28 via web to @k_katsumi
@fladdict 僕の環境だとSHKHatena.mの82行目をwrite_publicだけにしたら通りました。scopeパラメータは完全にはてなの仕様なので詳しい人に聞かないとわからないっすねえ。
2012-09-04 13:34:33 via Echofon to @fladdict
@k_katsumi はてなAPI、不正トークンエラーとかもバリバリでて、なんかAPIがすごい不安定ですよねぇ。社内的にAPI全然興味ないのかしらとか思う。
2012-09-04 14:45:40 via web to @k_katsumi
@fladdict 何回かの割合でトークンが不正って言われちゃいますよね。。。あんまり安定しないようだとID, PASSWORDでWSSE認証するほうも用意しますわ。
2012-09-04 14:47:22 via Echofon to @fladdict
@k_katsumi ありがとうございますー。とりあえず埋め込んだものをもうちょっとブラッシュしてTestFlightを発射できるようにしますね!
2012-09-04 14:50:45 via web to @k_katsumi
↑ そんなこんなでとりあえず書いてみた割りにはちゃんと使えてるようでよかったです。
OAuth で許可するスコープに write_private を書いたら失敗する点とか気になるところはあるけど。。。
はてなブックマークのShareKit拡張もこのままPullRequest送ろうかしら。さっきのが取り込まれたらファイルが1組減るんだけど。 URL
@k_katsumi いいね!
2012-09-16 14:21:06 via web to @k_katsumi
@fladdict 一点サービス名をローカライズ(Hatena Bookmark => はてなブックマーク)しようかどうか考えていて、他にサービス名をローカライズしてるところがない(必要がない)からstringsファイルに追記していいものかどうか悩んでいます。
2012-09-16 14:26:57 via Echofon to @fladdict
@k_katsumi なくていいんじゃないでしょうか。日本語サービスは日本人以外みんなが困るのでw
2012-09-16 14:27:55 via web to @k_katsumi
@fladdict そですね。僕もそんな気がします。
2012-09-16 14:28:55 via Echofon to @fladdict
↑ Pull Request しました。↓
Add Hatena Bookmark Sharer. by kishikawakatsumi · Pull Request #521 · ShareKit/ShareKit · GitHub
iphone_dev_jp東京 iPhone/Mac Hackathon : ATND
iPhone_dev_jpで、みんなが幸せになるハッカソンを開催します | fladdict
前回の勉強会で深津さんが「一発もののアプリじゃなくてきちんと使われるライブラリをドキュメント込みで作るハッカソンやったらいいんじゃない?」って話をしていたので、それはすばらしいと思ったのでやってみました。
ハッカソンってやったことなくて、勝手がわからずにかなりギリギリの告知になってしまったのですが、約30名の猛者が集まってくださいました。
とはいえ、私は1日でそんなに書けるものだろうかと不安だったので実はそれまでの1週間である程度メドを立てておこうとか思ってたのですが、意外と時間がなくてぶっつけになってしまいました。
でもなんとかなるもので、やっぱり集中して書いたほうがダンゼン効率がいいんだなあとか今さらながら思いました。
こういう周辺ライブラリは必要だなあと思っててもけっこう普段の時間には書こうと思ってもなかなか筆が進まなかったりするもので、こういう集まりは小規模でもいいので定期的にやっていこうというところを確認できた一日でした。
かなりメリットのあることがわかったので近いうちにまたやると思いますのでまたみなさん集まってくれたらうれしいです。
fladdict/AAMCommandKit · GitHub
tek-nishi/game05 · GitHub
inamiy/YISplashScreen · GitHub
ShunsukeAraki/SARRangeSlider · GitHub
corosukeK/HMGLTransitionSegue · GitHub
https://github.com/akisute/NLTHTTPStubServer
https://github.com/yanamura3/NSDateHelper
https://github.com/kasajei/GPUImageKit
kasajei/UIKitHelper · GitHub
第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 のオブジェクトに作成日や更新時刻が自動的に入ったらいいなと思うことってありますよね。固定の値ならばモデルエディタの 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" と入力すると現在時刻が「固定値として」設定されます。
あくまでここで設定できる初期値は固定値なのでイマイチ何の役にたつのか分かりませんが、もしかしたら便利なのかもしれません。
kishikawakatsumi/WWDCChecker-Mac · GitHub
kishikawakatsumi/WWDCChecker-iPhone · GitHub
WWDC 2012 がようやく発表されました。
チケットは2時間ほどで売り切れてしまいましたが、なんとか買うことができました。
これまでの傾向から激しい争奪戦になることは分かっていたので WWDC のサイト (WWDC - Apple Developer) を監視して、更新があったら手元の iPhone にプッシュ通知で知らせてくれるアプリケーションを作りました。
今回、無事に役目を果たすことができたので少々の解説をしつつ、来年のためにソースコードを公開します。
↓ ぞくぞくと寄せられる喜びの声
WWDCきたー。
@k_katsumi きしかわさんのあぷりのおかげで気づく事が出来ました
2012-04-25 21:37:58 via YoruFukurou to @k_katsumi
WWDCきたー。
@k_katsumi ありがとうー
2012-04-25 21:40:13 via Tweetlogix to @k_katsumi
帰りの電車だったから、ヤバかった。ゲームしてて家に着くまで気づかないとこやった。
@k_katsumi ぼくもiPhoneで今急いで買ったっす。
2012-04-25 21:43:56 via Echofon to @k_katsumi
2012-04-25 21:44:08 via Echofon to @Seasons
@nagisawks @k_katsumi あざっす!岸川さん様々ですよ。
2012-04-25 21:44:28 via Echofon to @nagisawks
@k_katsumi ありがと!ありがと!
構成は、通知を受けるための iPhone アプリケーションと、常時起動していてサイトを監視し変更があったら通知する Mac アプリケーション の2つのソフトウェアからなります。
↓ iPhone アプリはこんな感じです。
といってもこの画像ではわかりにくいですが、WWDC のサイトに自動的に接続するようにしてあるブラウザです。
別にプッシュ通知を受けるために必要なだけなので特に機能は必要ないのですが、プッシュ通知から起動したあと、すぐに購入することができるようにこうしてみました。
プッシュ通知には Parse.com を使いました。
1,000,000 通知/月までは無料で使えるので今回のような Ad Hoc で限られた人にだけ配布するような用途にはピッタリです。
また Parse は iOS 用には専用の SDK が提供されているので決まったコードを少し書くだけでプッシュ通知が受けられるようになります。
↓ これだけのコードで通知を受けられるようになります。キモはプッシュ通知なのでインストールして通知が有効になってさえいれば、アプリケーションは起動しててもしてなくてもどうでもいいので、これだけでほぼ完成といえます。
@implementation WWDCAppDelegate @synthesize window; @synthesize viewController; - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { #if !(TARGET_IPHONE_SIMULATOR) TESTFLIGHT_TAKEOFF; #endif [application registerForRemoteNotificationTypes:UIRemoteNotificationTypeSound | UIRemoteNotificationTypeAlert]; [Parse setApplicationId:@"SCRdFC3GKlr9WD5ElC9IGpBZewLxDhrl2zSQOKlu" clientKey:@"5cRNScoCLbMlnPbgwznv40TxNRJKrVQGmXWwbCH5"]; self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; self.viewController = [[WWDCViewController alloc] initWithNibName:@"WWDCViewController" bundle:nil]; window.rootViewController = viewController; [window makeKeyAndVisible]; return YES; } - (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)newDeviceToken { [PFPush storeDeviceToken:newDeviceToken]; [PFPush subscribeToChannelInBackground:@""]; } - (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error { if ([error code] == 3010) { NSLog(@"%@", @"Push notifications don't work in the simulator!"); } NSString *message = error.localizedDescription; NSBundle *bundle = [NSBundle mainBundle]; NSDictionary *infoDictionary = [bundle localizedInfoDictionary]; NSString *appName = [[infoDictionary count] ? infoDictionary : [bundle infoDictionary] objectForKey:@"CFBundleName"]; UIAlertView *alert = [[UIAlertView alloc] initWithTitle:appName message:message delegate:nil cancelButtonTitle:nil otherButtonTitles:@"OK", nil]; [alert show]; } - (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo { [PFPush handlePush:userInfo]; } @end
Mac アプリケーションはいろいろ試しましたが、結局 WebView を使って一定の間隔でサイトにアクセスしてタイトルに変更がないかを比較する、という方式で監視することにしました。
HTTP リクエストを使ってダウンロードしたデータを比較したり、もっと厳密にやろうかと思ったのですがいろいろあってこの方法に落ち着きました。
これを家でつけっぱなしにしている Mac にインストールして準備完了です。
↓ Mac アプリのコードの抜粋です。Mac の WebView は自由にデータにアクセスできるから楽チンですね。Parse は SDK によるアクセスの他に REST API も用意されていて、プッシュ通知も REST API を使って iOS 以外から送信することができます。
- (void)onTimer:(NSTimer *)t { if (!timer) { self.timer = [NSTimer timerWithTimeInterval:120.0 target:self selector:@selector(onTimer:) userInfo:nil repeats:YES]; [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes]; } NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"https://developer.apple.com/wwdc/"]]; [request setHTTPShouldHandleCookies:NO]; [request setCachePolicy:NSURLRequestReloadIgnoringLocalCacheData]; [[webView mainFrame] loadRequest:request]; } - (void)webView:(WebView *)sender didReceiveTitle:(NSString *)title forFrame:(WebFrame *)frame { if ([title isEqualToString:@"Apple Worldwide Developers Conference 2011"]) { [self log:@"WWDC Checked... no changes\n"]; } else { [self notify]; } } - (void)notify { [self log:@"WWDC 2012 may have been announced!\n"]; NSString *JSON = @"{\"channel\":\"\", \"data\":{\"alert\":\"WWDC 2012 may have been announced!\", \"sound\":\"notify.wav\"}}"; NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"https://api.parse.com/1/push"]]; [request setHTTPMethod:@"POST"]; [request addValue:@"ICFdOE4GOlu8WN5KlC0ILpASiqIxShrl6zWKQKlu" forHTTPHeaderField:@"X-Parse-Application-Id"]; [request addValue:@"ork5uoWbcoYxV8wXz34WLhNfKzmyAGEKTOG4JDKF" forHTTPHeaderField:@"X-Parse-REST-API-Key"]; [request addValue:@"application/json" forHTTPHeaderField:@"Content-Type"]; [request setHTTPBody:[JSON dataUsingEncoding:NSUTF8StringEncoding]]; [NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) { if (error) { [self log:error.localizedDescription]; } }]; }
Mac アプリのほうも、最初はウインドウなど何もなかったのですが、ちゃんと動いているのか分からないと心配で監視アプリの死活を監視するためのアプリが必要になって……とキリがないのでウインドウにログ出力をするようにしました。
これで変更があったり、アプリが動かなくなっていることが一目でわかるようになりました。
↓ 以下は発表があった当時の様子です。2分間隔なので誤差はありますがこの記録によると 21:34:32 に発表があったことになります。
売り切れたのがだいたい 23 時半ですので今年は2時間もたなかったということになりますね。
というわけで来年はもはや発表前に売り切れるんじゃないかという話もある WWDC ですが、少しでも有利に争奪戦に参加するためにぜひ有効に活用してみてください。
(上記のコード、および GitHub のコードは Parse.com や TestFlight のトークンを書き換えてあります。試される際は自分のアカウントのトークンに直してご利用ください。)
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
@k_katsumi あ、それとは別にお渡ししたPDFの資料をどっかで後悔していただけたら当日できなかったUnityの話も見られていいかなと
@akisutesama 勉強会のときのPDF、SlideShareで共有するね?
2012-04-16 22:50:43 via Echofon to @akisutesama
@k_katsumi おなしゃす!!
@akisutesama 容量が大きすぎてSlideShareは上限にかかったのでSpeakerDeckで共有しました。| iphone_dev_jp 東京iPhone/Mac勉強会 (@akisutesama) URL
2012-04-18 02:12:51 via Echofon to @akisutesama
あきすてさんの依頼で私が代わりにSpeakerDeckで共有しました。
当日は話さなかった Unity Asset Server や Unity+Jenkins の話が載っていますのでぜひご覧ください。
iphone_dev_jp 東京iPhone/Mac勉強会 (@akisutesama) // Speaker Deck
iphone_dev_jp 東京iPhone/Mac勉強会 : ATND
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
実は上の記事で紹介したクラスを書いた当時、それが必要だったアプリをリリースしたすぐ後くらいにほとんど同じ実装のライブラリを見つけまして。
自分の実装はそんなに間違ってなかったんだなーと安心しつつも、はじめからこれを使っとけばよかったとも思ったので紹介します。
NSCacheは使ってないですが、その他は私が書いたものとよく似ていて、たぶんオーソドックスな実装なんじゃないかなと思います。
SDImageCache と SDWebImageDownloader がライブラリのコアとなるクラスで、これらとうまく連携するように UIImageView や UIButton の拡張や、サポートクラスがあるという構成になっています。
パッと見た感じ、けっこう構成ファイルが多いように見えますが、それぞれのクラスはかなり疎結合になっているのでキャッシュだけ、ダウンローダだけを利用することも簡単です。
前回の記事の、画像が無い場合にデフォルト画像を返す引数はキャッシュのクラスには無いですが、UIImageView のカテゴリで、 - (void)setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder; というメソッドがあるので、やっぱり無かったらプレースホルダとしてデフォルト画像を返せるようにする、というのはよくある要件なのだなあと思いました。
この SDWebImage は Carousel という Instagram を Macで閲覧できるアプリケーションでも使われています。
iOS組み込みのキャッシュモジュールNSCacheについて発表しました - ninjinkun's diary
@k_katsumi キャッシュを分ける方のはわかりやすくて良いですね。後から読む人の参考になりそうなので、URL と URL の発言、ブログに引用させていただいても良いでしょうか。
2012-03-26 16:42:44 via web to @k_katsumi
@ninjinkun はい。ぜひぜひー。せっかくなので便乗して僕がいつも使ってる画像キャッシュのコードを共有したりしてみます。
2012-03-26 16:45:05 via YoruFukurou to @ninjinkun
@k_katsumi お、それは楽しみです!この手のものはみんな独自に作ってる感じだと思うので、参考にさせていただきたいですー。
2012-03-26 16:48:23 via web to @k_katsumi
@ninjinkun ですよね〜。僕もなんとなくもやっと合ってるよなあ。。。?と思ってたので今夜ちょちょいっとやります。
2012-03-26 16:50:56 via Echofon to @ninjinkun
上のような感じでせっかく話を振ってもらったので私がいつも使ってるキャッシュの実装を公開してみます。
だいたい誰が書いてもこんな感じになると思っているのですが、こうしたら便利だよ、とか、それはおかしいとか突っ込んでもらえるとうれしいです。
インターフェースと実装はだいたい下記のようになります。
#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;
キャッシュクラスあるいは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については冒頭でも引用した下記の記事が詳しいです。
iOS組み込みのキャッシュモジュールNSCacheについて発表しました - ninjinkun's diary
簡単にいうとNSCacheはキャッシュ用に便利は機能が追加されたNSMutableDictionaryです。
格納できるオブジェクト数の上限や容量の上限を決めて超えたぶんは自動的に削除されるとか、自動削除のタイミングで処理を実行することができるなどです。
私は数の上限だけを設定して、容量の制限は使いません。
サムネイル画像やアイコン画像とメイン画像で容量が全然違うよ、っていう場合はNSCacheのインスタンスを複数使ってそれぞれに上限を設定して使い分けます。
下記は、NSCacheを複数使い分ける例です。
cache = [[NSCache alloc] init]; cache.countLimit = 20; thumbnailCache = [[NSCache alloc] init]; thumbnailCache.countLimit = 100;
ダウンロードした画像データをディスクに保存するときのファイル名ですが、ダウンロード先の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 では 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
アプリケーション側で対処できることは無さそうなので私たちにできることはバグレポートを送ってできる限り早く修正してもらうことですね。
iOS 5.1 からプログラムからも利用できるようになった音声入力を使ってアプリケーションを操作するサンプルを公開しました。
kishikawakatsumi/VoiceNavigation · GitHub
このサンプルでは「〜を検索」と話すとその言葉で Google 検索を実行します。
「戻る」「進む」と話すことでブラウザの「戻る/進む」を実行できます。
一定の間隔で自動的に音声入力の開始/終了を繰り返すことで常に入力を受け取れるようにしています。
開始/終了を任意のタイミングで実行することもできるので何らかの合図(カメラや振動)によって開始するようにしてもいいかもしれません。
認識精度が優れているのでパターンマッチを増やすだけでもいろいろなことができておもしろいのではないでしょうか。
ぜひいろいろ試してみてください。
※読めばわかると思いますが、音声入力の開始と終了に非公開の API を使用していますので、残念ながらそのまま AppStore に申請することはできません。
※iPad で使ったらダンゼン便利、と思ってユニバーサルアプリとして作ってますが iPad の UI はまだ書いてません。
※なぜかバックグラウンドから戻ってきたあと動作が怪しくなります。
iOS 5.1 から日本語の音声入力がサポートされました。さらに API にも UIDictationPhrase など音声認識ができそうなクラスが追加されています。
これはプログラムから音声認識を使うことができるのか?そうすると音声を使ってハンズフリーでアプリケーションの操作ができるかと思いましたが調べてみるとそんなにうまい話ではありませんでした。
まず iOS 5.1 で追加された関係のありそうな API を見てみましょう。
UIDictationPhrase といかにも便利そうな名前のクラスですが、リファレンスをよく読んでみると text プロパティと alternativeInterpretations プロパティ以外はメソッドもなく、単に UITextInput protocol の insertDictationResult: の引数に結果を詰めるだけのクラスということがわかります。
さらに読み進めていくと追加された API はすべて UITextInput protocol のメソッドであり要するに音声入力はあくまでもテキスト入力の一環で、 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 で公開しています。
ぜひ試して改良してみてください。