ThumbFan Keyboard 1.0.1がリリースされました。
今回のアプリでの変更点を見ながら、iOSキーボードやiOS8開発の技術についてまとめておきたいと思います。
カテゴリー: Objective-C
片手親指でキータイプができる英語キーボードThumbFan Keyboardをリリース。技術概要など
iOS8でエクステンションの機能が追加されて何か作ってみたかったのと、iPhone 6 Plusの大きさによる不便さを解決したいという理由で、片手親指キーボードをつくってみました。
Objective-Cで、サブクラスだけで使用出来るプロパティを作成する
Objective-Cでクラスを作る場合は、他のクラスからアクセスさせたくないプロパティはクラスエクステンションにして、自クラスだけでアクセス出来るようにします。こんな感じです。
モジュールはこんな感じです。
こうすることによって、notesというプロパティは、参照するクラスからは変更されたくないので、ヘッダーにはreadonly,クラスエクステンションでは、readwriteで定義することによって、クラス内で変更可能です。
booksというプロパティは、クラス内ではNSMutableArrayとして振る舞いたいけれども、対外的には変更してほしくないので、NSArrayとして返しています。booksと別のbooksInternalという内部用プロパティをNSMutableArrayで定義して、内部ではbooksInternalにアクセスすることによって、オブジェクトの追加が出来るようにしています。
このクラスを継承したクラスを作った場合は、対外的には同じくbooks, notesを触られたくないですが、継承したクラス内では触りたい訳です。
モジュールでbooks, notesを触ろうとすると当然ですが、エラーになります。
これを解決するために、クラスエクステンションを別ファイルに出してあげると解決出来ました。
このようにします。
クラスエクステンションの新しいファイルに、プロパティを記述
ベースクラスでは、クラスエクステンションをモジュールでImportします。
継承したクラスでもクラスエクステンションをモジュールでImportします。booksに関しては、内部的にmutableのプロパティ booksInteranlをクラスエクステンションで定義しているのでそちらを使用します。
これで、他のクラスからはprivateExtentionのヘッダーをインポートしないようにすれば定義が見えないので、コンパイル時点で正しい処理が行われていることを確認出来ます。
libextobjc の @ keypath でObjective-Cのプロパティ名を文字列化する
まえから、型チェックをした上でプロパティ名を文字列化したいと思っていました。
@interface MyClass
@property (nonatomic, strong) NSString * myPropertyName;
@end
@implementation MyClass
-(void) myFunc {
self.myPropertyName = @"abc";
NSString * property = NSStringFromSelector(@selector(myPropertyName));
// myPropertyName が出力される
NSLog(@"%@", property);
// abc が出力される
NSLog(@"%@",[self valueForKey:property]);
}
@end
これだと、SEL型から名前をえることが出来るのですが、myPropertyNameというプロパティが、MyClassのプロパティという保証がないので、困ることがあります。valueForKey:で使用した時に間違った方で使用してしまうこともあります。それを解決する方法をまえから考えていたのですが思い浮かばなかったものの、ReactiveCocoa(ReactiveCocoa/ReactiveCocoa )使っていたら、Libextobjc(jspahrsummers/libextobjc )にそれっぽいものがあったので調べてみました。libextobjc は、Objective-Cを便利に使う機能をまとめた関数群で、マクロなどを駆使して欲しい機能が実装されています。libextobjcの、”Compile-time checking of selectors”にはこのように説明されています。
Compile-time checking of selectors to ensure that an object declares a given selector, using EXTSelectorChecking.
セレクタの型チェックをコンパイル時に行うことが出来る、いい感じですね。これで上を書き換えるとこうなります。
@interface MyClass
@property (nonatomic, strong) NSString * myPropertyName;
@end
@implementation MyClass
-(void) myFunc {
self.myPropertyName = @"abc";
NSString * property = @keypath(self.myPropertyName);
// myPropertyName が出力される
NSLog(@"%@", property);
// abc が出力される
NSLog(@"%@",[self valueForKey:property]);
}
@end
分かりやすいですね!ここで、”self.myPropertyName”と書いているので、どのインスタンスの型のプロパティかがコンパイル時にチェック出来ます。RestKitやMantleなどのO/Rマッピング機能を使うために、文字列型でプロパティ名が欲しいことが多いのでとても役立ちます。
どうやっているのかを見て見たところこんな感じでした。
/**
* \@keypath allows compile-time verification of key paths. Given a real object
* receiver and key path:
*
* @code
NSString *UTF8StringPath = @keypath(str.lowercaseString.UTF8String);
// => @"lowercaseString.UTF8String"
NSString *versionPath = @keypath(NSObject, version);
// => @"version"
NSString *lowercaseStringPath = @keypath(NSString.new, lowercaseString);
// => @"lowercaseString"
* @endcode
*
* ... the macro returns an \c NSString containing all but the first path
* component or argument (e.g., @"lowercaseString.UTF8String", @"version").
*
* In addition to simply creating a key path, this macro ensures that the key
* path is valid at compile-time (causing a syntax error if not), and supports
* refactoring, such that changing the name of the property will also update any
* uses of \@keypath.
*/
#define keypath(...) \
metamacro_if_eq(1, metamacro_argcount(__VA_ARGS__))(keypath1(__VA_ARGS__))(keypath2(__VA_ARGS__))
#define keypath1(PATH) \
(((void)(NO && ((void)PATH, NO)), strchr(# PATH, '.') + 1))
#define keypath2(OBJ, PATH) \
(((void)(NO && ((void)OBJ.PATH, NO)), # PATH))
分かりにくいですが、keypath1のところだけを見てみると、
#define keypath1(PATH) \
(((void)(NO && ((void)PATH, NO)), strchr(# PATH, '.') + 1))
こんな感じで、左の、”(NO && ((void)PATH, NO))”でコンパイルチェックだけを通して、実際にはNO &&の後なので使用されないようにしています。
後半の右側で、パラメータを#defineマクロの機能で、文字列化して(# PATH)、その”.”の右側をとったC文字列を作成して、”myPropertyName”となるのですが、keypath( の前に@を付けているので、Objective-C文字列になるという訳ですね。
言葉で説明すると分かりにくいですが、使うと便利と思います。
#fukuObjC 福岡Swift&Obj-C&Xcode開発周り勉強会 – 0x02 で話してきました
Swift&Obj-C&Xcode開発周り勉強会 – 0x02 の実況ツイートまとめ #fukuObjC – Togetterまとめ
久しぶりにiOS開発の勉強会に出てみようということで、福岡では出たことなかったのですが参加してみました。
Swift&Obj-C&Xcode開発周り勉強会 – 0x02 on Zusaar
せっかくですので新しいものをやってみて登壇しようと思い、関心のあったReactiveCocoaの勉強してサンプルアプリを作って、iOSアプリを綺麗に作る方法についてまとめてみました。
ReactiveCocoaは面倒ですけど、ちゃんと作れば良いプログラムが出来るのは間違いないのでどんどん取り込んでいきたいと思います。SwiftでのFRPも調査していきたいですね。
福岡の勉強会初めてでましたが、他の発表も面白かったです。Swiftはこういう勉強会で軽く見ながら実践投入する段階で一気に勉強するのが良さそう。いかに公開してくださったスライドのリンクを置いておきます。
uounɹɐʇ (tarunon)さん
NSUserDefaults噺
くろねこまいける (kuronekomichael)さんの発表もそれぞれ良かった。スライドが上がったら追記します。
[Objective-C] libextobjc の @weakify と @strongify について
ちょっと遅れた話題ですが、libextobjcライブラリを使用して、weak変数を使う方法があることを知ったので調査してみました。
これまでの記述法と問題点
これまでわたしは、この記事ARC+Blocks+llvm4.0時代のコード記述作法 | Zero4Racer PRO Developer’s Blog で書いたルールにしたがって、ivar(クラス内の変数)を基本的に使わず、block内で使用する場合に、weak化して使用する方法を使ってきました。WEAKSELFMAKE;というマクロを作成して、selfのマクロを作成しています。
#define WEAKSELFMAKE __weak typeof(self) wself = self
使用する時は、
{
WEAKSELFMAKE;
SomeViewController *controller = [self.storyboard instantiateViewControllerWithIdentifier:NSStringFromClass([SomeViewController class])];
controller.block_completed = ^ (NSNumber * toReturn) {
wself.numberValue = toReturn.copy;
[wself redlowItems];
}
}
のような感じです。これで結構シンプルに書けてほぼ問題がないのですが、スコープ内のstrongの変数を使う場合は、weak化を自分で行う必要がありました。ちなみにStoryBoardIDをクラス名にしてNSStringFromClass([SomeViewController class])で書くのが最近のお気に入りです。
{
WEAKSELFMAKE;
SomeViewController *controller = [self.storyboard instantiateViewControllerWithIdentifier:NSStringFromClass([SomeViewController class])];
__weak typeof(controller) wcontroller = controller;
controller.block_completed = ^ (NSNumber * toReturn) {
wself.numberValue = toReturn.copy;
[wself redlowItems];
[wcontroller dismissViewControllerAnimated:YES completion:NULL];
}
}
@weakify, @strongifyで出来ること
jspahrsummers/libextobjc は、Objective-Cを便利にする拡張機能ライブラリです。cocoapodで、
pod 'libextobjc', '~> 0.4'
を追加すれば簡単に追加出来ます。上のサンプルを書き直すと、
{
SomeViewController *controller = [self.storyboard instantiateViewControllerWithIdentifier:NSStringFromClass([SomeViewController class])];
@weakify(self);
@weakify(controller);
controller.block_completed = ^ (NSNumber * toReturn) {
@strongify(self);
@strongify(controller);
self.numberValue = toReturn.copy;
[self redlowItems];
[controller dismissViewControllerAnimated:YES completion:NULL];
}
}
こんな感じになります。self, controller というオリジナルと同じ変数名を使える代わりに、使う前に、@strongify(self)をしてあげないといけないのがポイントです。
動作原理
Objective-Cのコードに慣れていたらこれが若干気持ち悪いコードに見えます。selfだと循環参照になってしまう気がしてしまうからです。reactive cocoa – Explanation of how weakify and strongify work in ReactiveCocoa – Stack Overflow この記事には、マクロを展開した後のコードが載せられていて分かりやすいです。
これを見ると、@weakifyマクロで self_weak_ というweak変数を作って、@strongifyマクロでself=self_weak_;と新しいself変数を作成していることが分かります。同じ変数名(self)だったら、ローカルスコープのselfの方が優先されるため、selfを使っても循環参照しないんですね。
あと、@weakifyという、予約語を作っているように見えますが、これも強引で、後ろに autorelease (このスクリーンキャプチャの場合は try {} @finally{} 付けることによって、展開後に@autorelease という形になるようにして、無理矢理@予約語のように見せてるんですね。マクロでこんな強引なことが出来るんですね。これの残念なのは、Xcode上での予約された@propertyのようなものと実際には違うので、コード上の色がおかしくなることです。
まとめ
実際これをプロジェクト規約に取り込むかですが、若干微妙なところですね。@strongifyを忘れるとselfを使っているところで循環参照してしまうのと、2行使ってしまうのでコードが煩雑になるので、現在のWEAKSELFMAKE;マクロの方が綺麗な気もします。それで、展開したマクロのself_weak_を使ってあげれば綺麗になると思いました。
{
SomeViewController *controller = [self.storyboard instantiateViewControllerWithIdentifier:NSStringFromClass([SomeViewController class])];
@weakify(self);
@weakify(controller);
controller.block_completed = ^ (NSNumber * toReturn) {
self_weak_.numberValue = toReturn.copy;
[self_weak_ redlowItems];
[controller_weak_ dismissViewControllerAnimated:YES completion:NULL];
}
}
これで、@strongifyを書き忘れることもないですし、selfを内部で使っていた時に色で見分けるのも簡単になります。しばらくこれで使ってみようと思います。
参考
cocoapods – libextobjc
libextobjc/extobjc at master · jspahrsummers/libextobjc
Safx: libextobjcの@strongifyと@weakifyについて
Objective-C – weakify/strongify マクロを使うと weak self パターンが簡単に書ける – Qiita
CocoaPodsのプロジェクトで、ヘッダーファイルが読め込めない時の対処方法
1つのプロジェクトに複数のターゲットを作成している時に起きる問題のようで引っかかったのでメモ。
cocoaPodsを導入して、podsで導入したフレームワークのヘッダーファイルが読めずに、
19:9: fatal error: ‘RestKit/RestKit.h’ file not found
のようなエラーが出ました。
“CocoaPodsのプロジェクトで、ヘッダーファイルが読め込めない時の対処方法” の続きを読む
TweetOverview2.0.3でのキーボードショートカット対応とiPad/キーボードに関する考察
TweetOverview 2.0.3がリリースされました。
iTunes の App Store で配信中の iPhone、iPod touch、iPad 用 TweetOverview ツイート俯瞰デスクサイドアプリ
今回のバージョンでは、キーボードショートカットに対応しています。
特にiPadでデスクサイドに置いた時に、リンクのウェブページを見ている時に矢印キーでページの上下が出来ればと思い、iOS 7 のキーボードショートカット機能を使用して作成しました。動作でもはこちら
現在対応しているショートカットは以下のものです。
- ウェブページ表示中の、↑↓矢印キーによるスクロール
- ウェブページ、詳細画面を、⌘+wキーで閉じる
- ツイート一覧画面を、← → キーでスクロールする
iPadのキーボードは、Logicoolのウルトラスリムキーボードミニを使っています。iPad miniのサイズでキーボードがうちやすいか心配だったのですが、このキーボードはよく考えられていて、caps-lockを削ったりして、aキーからlキーの幅を確保して、タイピングがしやすくなっています。MacBook風の色もいいですね。同じキーボードの白と黒は9,000円くらいなのに、この色だけセールで4700円ほどになっているみたいで、おすすめです。白とも黒ともマッチしないシルバーなので不人気なのかな?黒とは同じ色ではないですが、キーの色が黒なのでマッチしている気はします。このキーボードにしたのは、まず薄くて軽いことと、次にiPadをたてても使えることです。アプリによっては,たてた方が使いやすいアプリも多いので、このキーボードのように縦向きでも横向きでも使えるのはとても便利です。
技術的には、iOS7で追加されたキー取得のAPIを使用しました。@k_katsumi さんの、サンプルコードを参照しました。
kishikawakatsumi/KeyboardShortcuts
ViewControllerでキーを取得するのですが、childViewControllerを使用していると、最下層(親)のViewControllerにしかキーがいかないので、受け渡しの仕組みを作る必要がありました。また、キーをキャプチャすると、WebViewの入力時にそのキーが効かなくなるので(矢印など)、それもwebViewの入力中のステータスを確認して読み込みたいキーを変えていく必要があります。
APIとして、ボタンを押し続けているステータスや、ボタンのリピートなどがあればさらに良いと思うので、その辺は将来のiOSに期待です。
最後にiPadにおけるキーボードの考察ですが、やはりタッチによる操作中でも、キーボードによる操作は非常に便利であるということが分かりました。スクロール操作を行うために画面にタッチするよりも、キーボードのしたボタンに指を置いて一定量を読んだ後にボンポンキーを押していく方が楽です。また、⌘+wなどは使う人には染み付いているので、その自然な動作がキーボードで出来れば、タッチパネルをタッチするよりも使用感が高いです。もちろんiPadアプリの場合はタッチでの動作がメインになるのですが、キーボードを使用している層には、このようにキーボードショートカット対応してもらえるとうれしい機能がたくさんあると思いました。実際このアプリになれてしまうと、Safariや別のアプリでウェブを見ている時も、ついつい上下ボタンを押してしまうようになりました。
ちょっと癖のあるアプリですが、是非TweetOverviewを使って、追加してほしいキーボードショートカットなどがあったらレポートいただければと思います。
Text Kitを使用してUITextViewにリンクを埋め込んだりタブでカラムつけたりする方法
TweetOverview 2.0.2 がリリースされました。
iTunes の App Store で配信中の iPhone、iPod touch、iPad 用 TweetOverview ツイート俯瞰デスクサイドアプリ
TweetOverviewはiOS 7限定でアップデートしたのですが、iOS7限定にして、iOS7の最新機能を出来るだけ取り込むためにそうしました。その一つがTextKitです。「上を目指すプログラマーのためのiPhoneアプリ開発テクニック iOS 7編」を書きました[内容紹介あり] | Zero4Racer PRO Developer’s Blog こちらのリンクにもある、iOS7の解説本を書くためにいろいろテストをした機能を使って作成しています。
CFNetworkを使用したFTP接続のiOS6でのエラーに関して
ちょっと面倒なエラーが出たので備忘として記録しておきます。プロジェクトで使用している自作のFTPクラスでは、CFNetworkで作成したFTPストリームを、NSInputStreamに変換して、FTPの接続を行っています。メインキュー以外から呼ばれる前提で、非同期ではなく、同期的に呼ぶようにしています。
- (void) startGetSynchronous {
self.sema = dispatch_semaphore_create(0);
self.myFTPType = kFTPTypeGet;
CFReadStreamRef ftpStream;
NSInputStream * networkStream;
assert(networkStream == nil); // don't tap receive twice in a row!
ftpStream = CFReadStreamCreateWithFTPURL(NULL, (__bridge CFURLRef) self.ftpURL);
assert(ftpStream != NULL);
networkStream = (__bridge_transfer NSInputStream *) ftpStream;
[networkStream setProperty:(id)kCFBooleanTrue forKey:(id)kCFStreamPropertyFTPUsePassiveMode];
networkStream.delegate = self;
dispatch_async(dispatch_get_main_queue(), ^{
[networkStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
[networkStream open];
});
// Have to release ftpStream to balance out the create. self.networkStream
// has retained this for our persistent use.
self.error = nil;
self.dataReceived = [NSMutableData dataWithLength:0];
dispatch_semaphore_wait(self.sema, DISPATCH_TIME_FOREVER);
#if OS_OBJECT_USE_OBJC!=1
dispatch_release(self.sema);
#endif
}
このサンプルは、Appleのsampleに基づいて作っていました。SimpleFTPSample: Document Revision History SimpleFTPSampleでも、依然はこのように、InputStream自体は、retainせずに、RunLoopに追加する事によってオブジェクトが生存する形となっていました。
これで、iOS5でも問題無く動いていました。ただiOS6で基本的に動くのですが、ときどきエラーが起きてFTPが接続出来なくなったりしていました。エラーログを見ると、
Error: request (0x______) other than the current request(0x0) signalled it was complete on connection 0x_______
のようなエラーが出ていました。このメッセージで検索してみると、別のFTPマネージャーでも同じようなエラーが出ている模様。
Error: request (0x989dd00) other than the current request(0x0) signalled it was complete on connection 0x9b8c6e0 · Issue #5 · nkreipke/FTPManager
ここでは、ARC環境においてのメモリ管理が関係しているのではないか、そしてオブジェクトをstrongプロパティにすればよいのではないかという議論が行われていたので、僕の個人のクラスも、NSStreamオブジェクトをクラスのプロパティに変更して見たところ、エラーが発生しないようになっていました。
self.networkStreamInput = CFBridgingRelease(ftpStream);
[self.networkStreamInput setProperty:(id)kCFBooleanTrue forKey:(id)kCFStreamPropertyFTPUsePassiveMode];
self.networkStreamInput.delegate = self;
dispatch_async(dispatch_get_main_queue(), ^{
[self.networkStreamInput scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
[self.networkStreamInput open];
});
SimpleFTPSample自体も、2013/4/12に修正されています。その修正履歴には、SimpleFTPSample: Document Revision History
2013-04-12 Fixed a bug that caused an incompatibility with iOS 6 (r. 13171568), along with other minor changes including (r. 12478239).
となっていて、iOS 6との互換性問題のバグによって修正されたとありますね。最初からクラスのプロパティか、strongのインスタンス変数にしておけばよかったと思いましたが、サンプル通りに作っていたので、サンプルと同じ問題にぶつかっていました。ただ、CFNetworkによるFTPの動作がiOS5以前とiOS6で変更されたというのは事実のようですので、もし使っている方はご注意を。
ちなみに、参考にしたページには僕からもgitHubのissuesページに問題が解決した事を伝え、こちらのクラスもiOS6対応版に修正されたようです。まさに問題を共有して改善していくソーシャルコーディングですね。
Error: request (0x989dd00) other than the current request(0x0) signalled it was complete on connection 0x9b8c6e0 · Issue #5 · nkreipke/FTPManager