Jeffsuke is not a pen.

🏊‍♂️🚴‍♂️🏃‍♂️💻📱

忘れないでGCD(復習しよう)

このエントリーは、iOS Advent Calendar 2014 の 2日目です。 2日目なので、Swiftとかではない送りバンドな記事で行きます。

非同期処理が多く求められるモバイルアプリ開発の現場では、ReactiveCocoaやRxJava等のFrameworkが注目を浴びている。 しかし、意外と基本となるGCD(Grand Central Dispatch)のことを忘れがち。

FacebookTwitterからタイムラインを取得しいい具合に表示する案件をやっていた時、非同期処理と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を用いたループするアニメーションだった。

f:id:jeffsuke:20141002085323p:plain

dispatch_afterや、NSRunLoopNSTimer等を用いてループ処理を実行していると、ownerとなるオブジェクトが解放されようとしても、これらのオブジェクトが強参照するために、解放されないようだ。

今回実装していた物

UIImageViewのカスタムクラスの上に、UIImageが乗っており、animationImagesNSTimerによって、フェードイン・アウトするアニメーションの挙動を実装した。 参考:iphone fading of images

f:id:jeffsuke:20141002085957g:plain

元々の実装

これだと、ループが回り続け、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;
    
}

Today Extensionを実装してみた。

Today Extensionはウィジット

Today Extension from WWDC
Today ExtensionはiOS8から導入されたウィジットを通知画面に設置する機能です。アプリの機能を拡張するExtensionの一つです。あまりに情報が少なくてハマったので、ブログに書いておきます。 Appleのドキュメントが一般公開されているので、詳しい情報は以下参照して下さい。

App Extension Programing Guide

この記事も参考にしました、 【iOS8】App Extension の実装方法 その1:ActionAdd Star

*以下はXcode6 beta3での検証結果です。画像はApp Extension Programing Guideから拝借してものです。

実装手順

  1. Today Extensionターゲットを作成
  2. Today ExtensionのViewの生成
  3. Info.plistを編集
  4. アプリ上でのコードの再利用:Framework化
  5. DBの共有:App Groupの作成
  6. データのアップデート処理
  7. Today Extensionからアプリを開く
  8. テストの実施

1. Today Extensionターゲットを作成

File > New > Target > Application Extension > Today Extensionを選択します。設定したExtensionの名前のディレクトリと、そのテストが生成されるます。生成される中身は以下のとおりです。

  • info.plist
  • .storyboardファイル
  • ViewController.m, ViewController.h

プロジェクトファイルを確認すると、Extensionとそのテストのターゲットができていることが確認できます。

Set up on Xcode6

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にしたがって設計されていると、依存関係が邪魔することなく必要なファイルだけをインポートできます。設計大事ですね。

Embedded Framework

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を定義し、共通領域に保存します。

  1. プロジェクトナビゲーター > 本体アプリ > Capabilities > App Groupsから設定をオンにします。
  2. 新しいコンテナとして、"group.com.companyName.myApp"のような命名をします。他の方法として、Apple Developer Center上でも同様に設定変更ができます。この場合、設定したprovisioning fileを再度ダウンロードし、Xcode上でプロジェクトに適応する必要があります。
  3. Today Extensionのターゲットでも同様のプロセスを実施します。
  4. NSUserDefaultを読み込むときに、[[NSUserDefaults alloc] initWithSuiteName:@"group.com.companyName.myApp"]のようにインスタンスを生成します。
  5. 後は通常通り読み書きをするだけ。

この読み書きを実施するモデルを本体アプリで定義し、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が追加されたため、今後はこれが主流になってくるだろう。

f:id:jeffsuke:20140714093118p:plain

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でラクラクセットアップ

f:id:jeffsuke:20140501204050j:plain

新しい開発環境を頂いたので、セットアップすることに。 毎回セットアップするのは、プログラマーの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で再セットアップが超効率アップ。

参考文献

phinze/homebrew-cask

Boxen使ってて許されるのは2013年だけだった

MacBook Airを再インストールする際の覚え書き(アプリケーション)

brew-caskとシェルスクリプトでMacの環境構築

Xcodeにて、SchemeのデバイスがMy Mac 64-bitしか選択できなくなった時の対処法

Xcodeを使っていて、よくSchemeのデバイスがMy Mac 64-bitしか選択できなくなる。 アプリのBundleIDをXcode上で変更した時などに発生する。

また、iPhone用ビルドを実施するのは非常に簡単。

まず、Edit Schemeを立ち上げる すると、RunのExcutableがNoneになっている。 このNoneをアプリ名.appに変更する。

以上でiPhone用ビルドができるようになる。

f:id:jeffsuke:20140210210834j:plain