忘れないでGCD(復習しよう)
このエントリーは、iOS Advent Calendar 2014 の 2日目です。 2日目なので、Swiftとかではない送りバンドな記事で行きます。
非同期処理が多く求められるモバイルアプリ開発の現場では、ReactiveCocoaやRxJava等のFrameworkが注目を浴びている。 しかし、意外と基本となるGCD(Grand Central Dispatch)のことを忘れがち。
FacebookとTwitterからタイムラインを取得しいい具合に表示する案件をやっていた時、非同期処理とNSNotificationを多様した難解な実装となっていた。 これもdispatch_groupやdispatch_barrier_asyncを使えば解決できるんだよね。 いい機会だし、復習してみよう。
GCDとは
Dispatch queueにBlocksとして実行したいタスクを渡し実行できる。このqueueには2種類ある。
- Serial dispatch queue:タスクを逐次的に実行
- Concurrent dispatch queue:他のタスクを待たずに実行
実際に使うときは、描画を行うメインスレッドであるMain dispatch queue(Serial dispatch queue)と、iOSがいい具合に判断しスレッドを作り実行してくれるGlobal dispatch queueのどちらかを選択して使う事になる。Global dispatch queueでは、優先度が選べるがこれはあくまで目安なので注意。
Serial dispatch queue
dispatch_queue_t queue = dispatch_queue_create("com.jsk.test", NULL); for (int i = 0; i < 5; i++) { dispatch_async(queue, ^{NSLog(@"%d", i); }); }
Output
0 1 2 3 4
Concurrent dispatch queue
dispatch_queue_t queue = dispatch_queue_create("com.jsk.test", NULL); for (int i = 0; i < 5; i++) { dispatch_async(queue, ^{NSLog(@"%d", i); }); }
Output(順番は実行時によって異なる)
4 0 2 1 3
Dispatch Groupでちょっと待つ
Dispatch Groupを使うと、queueに追加する処理をグループ化し、全ての処理が完了した事を受け取る事ができる。 これを使えば、非同期通信が複数走っている場合等の処理をまとめることができて便利。 使い方はとっても簡単。
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); dispatch_group_t group = dispatch_group_create(); for (int i = 0; i < 3; i++) { dispatch_group_async(group, queue, ^{ NSLog(@"%d", i); }); } dispatch_group_notify(group, queue, ^{ NSLog(@"Done"); });
Output
2 0 1 Done
また、dispatch_group_wait
を用いる事で処理をその箇所で止める事もできる。
Dispatch Barrier Async
上記Dispatch Groupと少し似ているが、Dispatch barrier asyncでは、Concurrent dispatch queueに追加された処理が実行完了されるまで待ち、Serial Dispatch Queueに新たなタスクを追加し、そのタスクが実行完了されるまで待つことが可能だ。 例えば以下のように使うことができる。
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); dispatch_async(queue, ^{NSLog(@"1");}); dispatch_async(queue, ^{NSLog(@"2");}); dispatch_async(queue, ^{NSLog(@"3");}); dispatch_barrier_async(queue, ^{NSLog(@"wait");}); dispatch_async(queue, ^{NSLog(@"4");}); dispatch_async(queue, ^{NSLog(@"5");});
dispatch_barrier_asyncメソッドを使うだけで、前の処理を待ってくれる。なんて便利なんだ。
まとめ
GCDを復習した。 dispatch_groupやdispatch_barrier_asyncを用いて他の非同期処理を待つ事ができる。 これでsemaphoreで無理やり処理していた箇所が書き換える事ができそうだ。
おまけ
Rebuild.fmで紹介されていたHBOのSilicon Valleyを見ている。あの近辺でのスタートアップがリアルに描かれていてかなり面白く、おすすめだ。
iOSの、画面遷移時のメモリリークが止まらなかった話
先日、画面遷移時にメモリが開放されず、徐々にメモリ利用率が上昇する現象に苦しまされた。
Instrumentsで調べてみても、リークは見られなかった。何が問題だったのか。それはdispatch_after
を用いたループするアニメーションだった。
dispatch_after
や、NSRunLoop
、NSTimer
等を用いてループ処理を実行していると、ownerとなるオブジェクトが解放されようとしても、これらのオブジェクトが強参照するために、解放されないようだ。
今回実装していた物
UIImageView
のカスタムクラスの上に、UIImage
が乗っており、animationImages
とNSTimer
によって、フェードイン・アウトするアニメーションの挙動を実装した。
参考:iphone fading of images
元々の実装
これだと、ループが回り続け、ownerのオブジェクトは永遠に開放されない
NSTimer *timer = [NSTimer timerWithTimeInterval:4.0 target:self selector:@selector(onTimer) userInfo:nil repeats:YES]; [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode]; [timer fire];
対策後
NSTimerをpropertyとして持ち、Viewが消える時に、invalidate
し、解放する必要がある。
@interface JSKSwipeViewController () @property (nonatomic) NSTimer *timer; @end @implementation JSKSwipeViewController - (void)startAnimation { _timer = [NSTimer timerWithTimeInterval:4.0 target:self selector:@selector(onTimer) userInfo:nil repeats:YES]; } - (void)viewDidDisappear:(BOOL)animated { [super viewDidDisappear:animated]; // stop animation and release [self.timer invalidate]; self.timer = nil; }
UINavigationControllerの戻るを消し、別のボタンで戻る
ViewDidLoadにて以下のメソッドを呼ぶ。 UINavigationController上の戻るボタンが消える。
[self.navigationItem setHidesBackButton:YES];
対象となるIBAction内で以下を呼ぶ。
[self.navigationController popViewControllerAnimated:YES];
結果、以下のように。
Today Extensionを実装してみた。
Today Extensionはウィジット
Today ExtensionはiOS8から導入されたウィジットを通知画面に設置する機能です。アプリの機能を拡張するExtensionの一つです。あまりに情報が少なくてハマったので、ブログに書いておきます。
Appleのドキュメントが一般公開されているので、詳しい情報は以下参照して下さい。
App Extension Programing Guide
この記事も参考にしました、 【iOS8】App Extension の実装方法 その1:ActionAdd Star
*以下はXcode6 beta3での検証結果です。画像はApp Extension Programing Guideから拝借してものです。
実装手順
- Today Extensionターゲットを作成
- Today ExtensionのViewの生成
- Info.plistを編集
- アプリ上でのコードの再利用:Framework化
- DBの共有:App Groupの作成
- データのアップデート処理
- Today Extensionからアプリを開く
- テストの実施
1. Today Extensionターゲットを作成
File > New > Target > Application Extension > Today Extensionを選択します。設定したExtensionの名前のディレクトリと、そのテストが生成されるます。生成される中身は以下のとおりです。
- info.plist
- .storyboardファイル
- ViewController.m, ViewController.h
プロジェクトファイルを確認すると、Extensionとそのテストのターゲットができていることが確認できます。
2. Today ExtensionのViewの生成
Today Extensionは通常のストーリーボードのViewと、view controllerで構成されています。Model層は本体アプリと共有されたもの、もしくはキャッシュされたフェッチ結果等がそれにあたります。
通常のViewと同様にストーリーボードで画面構成を生成します。通知センターは、TableView的にリストを表示しているので、ストーリーボード上にUITableViewを設置しました。いつもどおり、dataSourceとdelegateをストーリーボード上で設定。
このUITableViewをTodayExtensionで使う時に、背景色を正しく設定しないと通知センターっぽいUIにならないので注意が必要です。
ストーリーボード上で、UITableView及びUITableViewCellの背景色を透明にするか、以下のコードで明示的に透明にする必要があります。
必然的に黒背景になるので、その他のUIパーツ、UIButtonやUILabelは白っぽい文字でトンマナを合わせる必要があります。
3. Info.plistを編集
通知センターのタイトルを変更します。Bundle display nameにヘッダー部分に表示したい名称を入れます。 NSExtensionPointIdentifierをcom.company.appName.extensionのように変更。 その他は、通常のInfo.plistと同様に変更を加えていきます。
4. アプリ上でのコードの再利用
4-1. Framework化
App Extensionはアプリとは別のサンドボックスになっており、直接お互いのコードやDBにアクセスすることはできません。また結果的に別のアプリとして動くので、本体アプリは起動していないが、Extensionは動いている状態もありえます。そこで、コードを効率的にシェアする仕組みが必要です。そう、Frameworkです。Xcode6からはiOSアプリでもEmbeded frameworkを作ることができます。
File > New > Target > Framework & Library > Cocoa Touch FrameworkからFramework targetを生成します。Extensionにて使用したいファイルをBuild PhasesのCompile Sourcesに追加していきます。
この時、プロジェクトがMVCにしたがって設計されていると、依存関係が邪魔することなく必要なファイルだけをインポートできます。設計大事ですね。
4-2. CocoaPodsの利用(2014/08/02追記
Today ExtensionでCocoaPodsを利用する場合は、対象となるターゲットをPodfileに追記する必要があります。
Podfile Syntax Reference
target :test do pod 'OCMock', '~> 2.0.1' end
5. DBの共有:App Groupの作成
モデル部分の共有は、アプリ本体とExtensionどちらともアクセスできる領域にデータを保存する必要があります。今回はNSUserDefault
を使いました。
NSUserDefault
を使う時、通常はstandardUserDefaults
を使いますが、App Groupを定義し、共通領域に保存します。
- プロジェクトナビゲーター > 本体アプリ > Capabilities > App Groupsから設定をオンにします。
- 新しいコンテナとして、"group.com.companyName.myApp"のような命名をします。他の方法として、Apple Developer Center上でも同様に設定変更ができます。この場合、設定したprovisioning fileを再度ダウンロードし、Xcode上でプロジェクトに適応する必要があります。
- Today Extensionのターゲットでも同様のプロセスを実施します。
NSUserDefault
を読み込むときに、[[NSUserDefaults alloc] initWithSuiteName:@"group.com.companyName.myApp"]
のようにインスタンスを生成します。- 後は通常通り読み書きをするだけ。
この読み書きを実施するモデルを本体アプリで定義し、Embedded Frameworkに含めると共通化できて良さそうです。
6. データのアップデート処理
Today Extensionは、以下のメッソドを呼び、定期的にアップデートされます。その時、データ取得の成否をNCUpdateResult
として渡す必要があります。
7. Today Extensionからアプリを開く
URL schemeを使います。下の記事が参考になりました。
Custom URL Schemeの処理をシンプルに書く
8. テストの実施
Today Extensionは本体とは別のサンドボックスのため、実機やシミュレータを用いたテストでも何点かハマりました。
- ビルド時のターゲットをToday Extensionのものにしないと、ログが表示されない。
- 本体アプリターゲットでビルドしないと、必要なデータを保存できない。
- beta版だからか、シミュレータが不安定。よく落ちる。
- Today Extensionのストーリーボードに変更を加えた後、クリーン後ビルドしないと、UIが変更されない。
ユニットテストは通常と同様の書き方で問題なかったです。
まとめ
iOS8では簡単にウィジットが作れる。
2014/08/02追記
Facebook GroupでCocoaPods導入についてコメントを頂きましたので、追記しました。 https://www.facebook.com/groups/ios.dev.jp/permalink/768435093177874/
SwiftでUIBlurEffect実装してみた
SwiftでUIBlurEffectを実装してみた。
iOS7の登場と共に複数のライブラリが出現した。
iOS8では、動的にぼかしエフェクトを生成できるUIBlurEffectが追加されたため、今後はこれが主流になってくるだろう。
import UIKit class BlurEffectViewController: UIViewController { @IBOutlet var image: UIImageView override func viewDidLoad() { super.viewDidLoad() addBlurEffect() } func addBlurEffect() { var effect = UIBlurEffect(style: UIBlurEffectStyle.Light); var effectView = UIVisualEffectView(effect: effect); let rect = UIScreen.mainScreen().applicationFrame effectView.frame = CGRectMake(0, 0, rect.width, rect.height / 3) view.addSubview(effectView); } }
えー、マジBoxen!?Boxenが許されるのは2013年までだよね!
Brewfile+brew-caskでラクラクセットアップ
新しい開発環境を頂いたので、セットアップすることに。 毎回セットアップするのは、プログラマーの3大美徳 (怠惰・短気・傲慢)に反するので、自動化することに。
OSの再インストール
再起動時にcommand+R長押し
で、復元機能を呼び出す。 OS X Lion: Mac OS X を消去して再インストールする
AppStoreからダウンロード
事前にAppStoreからXcodeをダウンロードする必要があります。
ターミナルでの準備
ターミナルで以下をインストールします。
- Xcode command line tool
- Homebrew
- RVM
- CocoaPods
- NeoBundle
以下のシェルを任意のディレクトリでsh setup.sh
するだけでオッケー。
#!/bin/sh # Xcode command line tool xcode-select --install # Homebrew ruby -e "$(curl -fsSL https://raw.github.com/Homebrew/homebrew/go/install)" # Homebrew-cask option to make link in Application directory echo 'export HOMEBREW_CASK_OPTS="--appdir=/Applications --caskroom=/usr/local/Caskroom" ' >> ~/.bash_profile #RVM, Ruby, Rails \curl -sSL https://get.rvm.io | bash -s stable --rails # gem gem update rake gem install cocoapods #### vim git clone https://github.com/Shougo/neobundle.vim ~/.vim/bundle/neobundle.vim echo 'Go to Vim and Type NeoBundleInstall'
HomebrewとHomebrew-caskで必要なアプリケーションを一括インストール
任意のディレクトリに以下のBrewfile
を作り、brew bundle
をターミナルで実行。
ちなみに、brew-caskでインストールできるアプリケーションは下記から分かる。
https://github.com/phinze/homebrew-cask/tree/master/Casks
ここに含まれていないアプリケーションをインストールしたい場合は以下を参照。 http://blog.livedoor.jp/sonots/archives/35251881.html
update upgrade tap homebrew/versions tap phinze/homebrew-cask install brew-cask install zsh install autojump install git install tig install ansible install wget install curl install jq install vim install mysql install mongodb cask install dash cask install google-chrome cask install virtualbox cask install vagrant cask install kobito cask install alfred cask install dropbox cask install evernote cask install skitch cask install github cask install clipmenu cask install bettertouchtool cask install google-japanese-ime cask install the-unarchiver cask install sublime-text cask install skype cask install slack cask install licecap
まとめ
Brewfile+brew-caskで再セットアップが超効率アップ。