Hatena::ブログ(Diary)

Over&Out その後 このページをアンテナに追加 RSSフィード Twitter

2012-12-30

若輩者ではありますが、iOSの技術書を書かせていただくことになりました。

ちょうど退職の意志を役員に伝えた1週間後ぐらい(出来すぎたタイミング!)に、とある出版社の方より、「書籍企画のご相談をさせていただきたく」とメールをいただきました。


Qiitaに書いた記事 を見てご連絡いただいたようです。


(後述しますが、)僕にはこれ以上ないほどありがたい話だったので、トントン拍子で話が進み、無事企画承認まで漕ぎ着けました。


内容

詳しくはまだ書けないのですが、方向性としては、このブログに書いてるような、iOS開発まわりのTipsというかレシピ集みたいな感じです。自分で調べて作ると時間がかかるとか、応用が効くとか、あまり他に情報がないといったTipsを100個ほど書きたいと思っています。


スケジュール的な話

もちろん本を書くのは初めてなので、常識的なスケジュール感がわからず、でもあんまり長期にわたるスケジュールを提示するとこの奇跡的にありがたいお話がなくなってしまうかもしれない・・・とびびって「1月中旬〜2月いっぱいまでで書き上げます」と、厳しいけどいけるような気がしなくもない、というギリギリのラインで提示しました。


普通はもっと長いらしいです。でももうそれで企画が通ってしまったのでやるしかありません。毎日6ページぐらい書かないと間に合わない計算です。


ブログに今まで書いてたことはベースにはなるものの、文章の品質や情報の精度の観点からそのままは使えないので、基本的に全部書き下ろす必要があります。なんだかこうして書けば書くほどヤバさが実感できてきたので、このあとすぐにでも執筆にとりかかろうと思います。


あれ、海外は?

先日の『面白法人カヤックを退職しました。』にちらっと書いたとおり、まずは海外に住み慣れる、ということから始めたいなぁと思ってまして、とりあえず行くだけ行ってあとは家(宿?)に籠って本を執筆しようと考えてます。


僕は今のところ英語で会話なんてできず、日本以外に住んだこともなく、コネや就労ビザもないので、こうして遠隔でやりやすい仕事でワンクッションおけるのはほんとありがたいです。


執筆し終えたら、ちゃんと英語を勉強しつつ海外就職活動する予定です。


iOSアプリ開発者のみなさまへ

正しく有益な情報の載っている本にしたいと思いますので、僕がTwitterやこのブログで間違ったこととか変なことを言ってたら、ぜひツッコミをいただけると嬉しいです!



2012-12-28

面白法人カヤックを退職しました。

今日は本年度の締め日であるとともに、僕のカヤックの最終出社日でした。


この件については、カヤックでやってきた仕事を振り返ったりとか、今後のこととかいろいろ書きたいことがあるのですが、今日はビールを多少嗜んでしまったので、とりいそぎ社内に流したメール(を一部編集したもの)をもってお知らせさせていただきます。


みなさま、おはようございます。

堤です。


チーム内では朝礼で挨拶をさせていただく機会があったのですが、

他のチームにもお世話になった方がたくさんいるので、

改めて全体に挨拶させていただきます。


12/31をもってカヤックを退職することになりました。


2010年の年始の「ぜんいん社長合宿」がカヤック初参加だったので、

ちょうど3年間お世話になったことになります。


別業界で働いていた頃、仕事が100%楽しめてなくて鬱々としていて、たまたま本屋で手に取った『カヤック会社案内』という本がカヤックとの出会いでした。


その本でカヤックの考え方に惚れ込み、ディレクターとして受けてみる実績も経験もないため落ち、それでもあきらめられなくて、webの勉強をして再挑戦。

2度目で「まぁ、いいんじゃない?」(byやなさん)と、何とか拾っていただくことができました。


なんとか憧れのカヤックに入れたものの、そこからがまた失敗続きで、サーバーサイドエンジニアとしてはまったく役に立てず(SQLのselect文から、あるいはsvn statusから教えてもらってました。。)、iPhoneやりたいと言って任せてもらった簡単なアプリの仕事でもcapがクライアントにあやまりにいくぐらいのミスをしたりと、ダメダメでした。


そんな中、カヤックに入って4ヶ月程経った頃、清さんから「アプリに専念してみたら?」と声をかけてもらったのが転機でした。

ディレクターとしては採用されず、サーバーサイドエンジニアとしては4ヶ月やっても成長が見られず、しかも当時のカヤックとしては年齢もだいぶ上の方だったので、自分にとっては最後のチャンスでした。


それからは、クライアントワークとブッコミでひたすらアプリをつくりまくり、ベトナムで玉田さん、はっしーさん、大塚さんとEncountMeをつくり、それをきっかけにかいちさんに声をかけてもらってソーシャルに移ってKOFをつくり、そこで一緒にやった嶋田さんと、ちょうどGoGoCargoを終えたマツケンさんとタップ忍者モンアツをつくり、その後阿部さんの秘蔵企画をつくり。。


最高に充実した3年間を送らせていただきました。生きている実感がはんぱなかったです。



そんなカヤック、辞めたいわけがないです。


ただ、ずっと以前から、カヤックに入るよりももっと前から「海外で働きたい」という憧れがあって、それは以前であればただの憧れでしかなかったのですが、iOSアプリをつくれるようになった今では

「今ならいける。というか今行かなかったら一生行けない気がする」

と思うようになり、それで退職を決意しました。


そんなこんなで、次の就職も、どの国に行くかもまだ決めてませんが、とにかく海外に飛び込んでこようと思います。

こんな話を、面談時にスッと理解して受け入れてくれた役員の御三方には本当に感謝です。



3年間、どうもありがとうございました!!!!!


長いですが、要約すると、みなさんお世話になりました、とにかく海外に行きたいです、という話です。


英語も全然話せないので、まずは行くだけ行って、日本の仕事をしつつ、住み慣れるところから始めたいと思っています。(まずはヨーロッパに行きたいと思っています。でもアメリカもいずれ行ってみたい)


遠隔でもいいから仕事あるぜ!とか、海外就職先紹介できるかもよ!とか、とりあえず飲もう!という方はご一報いただけるとうれしいです。今後ともどうぞよろしくお願いいたします!



2012-12-19

1行で iOS バージョン判定できる便利マクロ

下記のようにマクロを定義しておけば、

#define SYSTEM_VERSION_LESS_THAN(v) ([[[UIDevice currentDevice] systemVersion] \
compare:v options:NSNumericSearch] == NSOrderedAscending)

こんな感じで1行でバージョン判定できて便利です。

if (SYSTEM_VERSION_LESS_THAN(@"6.0"))
{
    // ios 5.x and below
}
else
{
    // ios 6.0 and above
}

普通の実装をマクロで定義しただけですが・・・



2012-12-17

Core Image の全エフェクトを試せるサンプルコードを公開しました

Core Image のフィルタ(画像にエフェクトをかけたり、色を調整したりするもの)を一通り試せるサンプルプロジェクトをgithubに上げました。


というか、これ、1年以上前にアップしてこちらの記事に書いたのですが、ほとんど認知されることがなかったので、改めて紹介させていただきます。


こんな感じでフィルターを試せます。

(フィルタのパラメータはランダムに生成されるので、かける度に変わる場合もあります)


f:id:shu223:20140131072927j:image:left:w240

f:id:shu223:20140131073048j:image:left:w240



プロジェクト一式、こちらからダウンロードできます。

https://github.com/shu223/FilterDemo


注意点として、選んでも何も起こらないフィルターがたくさんあります

詳細は後述しますが、こちら、Apple の PocketCoreImage というサンプルコードを数行いじっただけのものなので、あんまり多種多様なフィルタに対応したつくりになってはいないのです。。

そういうフィルタはリストから除いてもよかったのですが、「どんなフィルタがあるのか?」を一覧できる意味もあるので、残してあります。


PocketCoreImageからの改変点

CIFilterは、下記のように、NSString型の名前を引数に渡してインスタンスを生成します。

CIFilter *newFilter = [CIFilter filterWithName:name];

で、ここに渡せる名前をずらずらっと列挙するために、awakeFromNibで次のようなコードで全ビルトインフィルタの名前を取得しています。

NSArray *filterNames;
filterNames = [CIFilter filterNamesInCategory:kCICategoryBuiltIn];
_availableFilters = [NSArray arrayWithArray:filterNames];


それと、PocketCoreImageでは、フィルタをかける箇所で、次のようにKVCを利用して元画像をフィルタに渡しています。

[filter setValue:_filteredImage forKey:@"inputImage"];

ここで、inputImageというアクセサを持たないフィルタもあるので、

if (![filter respondsToSelector:NSSelectorFromString(@"inputImage")]) {
    continue;
}

とチェックを入れて例外を回避しています。

(詳細はこちらの記事をご参照ください)


その他、コードの解説

もともと PocketCoreImage にあった部分なのですが、多種多様なパラメータを持つ不特定多数のフィルタに対してランダム値を設定する部分はこうなっているよ、というのを紹介させていただきます。


まず、CIFilterのattributesメソッドでパラメータ一覧を取得します。

(このあとここでは設定しないパラメータを取り除く処理が入りますが割愛)

NSDictionary *filterAttributes = [filter attributes];

NSNumber型のパラメータだけを取り出し、

for (NSString *key in editableAttributes) {
    
    NSDictionary *attributeDictionary = [editableAttributes objectForKey:key];

    if ([[attributeDictionary objectForKey:kCIAttributeClass] isEqualToString:@"NSNumber"]) {

        // 後述    
    }   
}

BOOL/Decimal/Integerそれぞれの型に応じて、ランダム値を設定しています。

(下記は、上記コードのif文の中身)

        if ([attributeDictionary objectForKey:kCIAttributeType] == kCIAttributeTypeBoolean)
        {
            NSInteger randomValue = (rand() % 2); 
            
            NSLog(@"Setting %i for key %@ of type BOOL", randomValue, key);
            [filter setValue:[NSNumber numberWithInteger:randomValue] forKey:key];
        }
        else if([attributeDictionary objectForKey:kCIAttributeType] == kCIAttributeTypeScalar ||
                [attributeDictionary objectForKey:kCIAttributeType] == kCIAttributeTypeDistance ||
                [attributeDictionary objectForKey:kCIAttributeType] == kCIAttributeTypeAngle)
        {
            // Get the min and max values
            float maximumValue = [[attributeDictionary valueForKey:kCIAttributeSliderMax] floatValue];
            float minimumValue = [[attributeDictionary valueForKey:kCIAttributeSliderMin] floatValue];
            
            float randomValue = randFloat(minimumValue, maximumValue);

            NSLog(@"Setting %f for key %@ of type Decimal", randomValue, key);
            [filter setValue:[NSNumber numberWithFloat:randomValue] forKey:key];
        }
        else
        {
            // Get the min and max values
            NSInteger maximumValue = [[attributeDictionary valueForKey:kCIAttributeMax] integerValue];
            NSInteger minimumValue = [[attributeDictionary valueForKey:kCIAttributeMin] integerValue];
            
            NSInteger randomValue = (rand() % (maximumValue - minimumValue)) + minimumValue;
            
            NSLog(@"Setting %i for key %@ of type Integer", randomValue, key);
            [filter setValue:[NSNumber numberWithInteger:randomValue] forKey:key];
        }

Core Image の参考書籍

iOS Core Frameworksテクニカルガイド

iOS Core Frameworksテクニカルガイド
Shawn Welch
インプレスジャパン
売り上げランキング: 242494


2012年9月と比較的最近出版されたものですが、原著の出版は2011年9月。したがって iOS5 時代のものです。『Core Image』の章があるのですが、広く浅く、という感じです。


iOS5プログラミングブック

iOS5プログラミングブック
加藤 寛人 吉田 悠一 藤川 宏之 西方 夏子 関川 雄介 高丘 知央
インプレスジャパン
売り上げランキング: 88968


Core Imageの章あり。この本は全体的に説明が詳しく、あまり他の書籍やブログにないところまで踏み込んだ情報も多いので、個人的にはこちらの方がおすすめです。


おまけ:CIFilterのフィルタ名一覧(iOS 6 版)

前回記事(2011年11月)時点では 48 個だったものが、なんと 94 個に増えていました。


CIAdditionCompositing

CIAffineClamp

CIAffineTile

CIAffineTransform

CIBarsSwipeTransition

CIBlendWithMask

CIBloom

CIBumpDistortion

CIBumpDistortionLinear

CICheckerboardGenerator

CICircleSplashDistortion

CICircularScreen

CIColorBlendMode

CIColorBurnBlendMode

CIColorControls

CIColorCube

CIColorDodgeBlendMode

CIColorInvert

CIColorMap

CIColorMatrix

CIColorMonochrome

CIColorPosterize

CIConstantColorGenerator

CICopyMachineTransition

CICrop

CIDarkenBlendMode

CIDifferenceBlendMode

CIDisintegrateWithMaskTransition

CIDissolveTransition

CIDotScreen

CIEightfoldReflectedTile

CIExclusionBlendMode

CIExposureAdjust

CIFalseColor

CIFlashTransition

CIFourfoldReflectedTile

CIFourfoldRotatedTile

CIFourfoldTranslatedTile

CIGammaAdjust

CIGaussianBlur

CIGaussianGradient

CIGlideReflectedTile

CIGloom

CIHardLightBlendMode

CIHatchedScreen

CIHighlightShadowAdjust

CIHoleDistortion

CIHueAdjust

CIHueBlendMode

CILanczosScaleTransform

CILightenBlendMode

CILightTunnel

CILinearGradient

CILineScreen

CILuminosityBlendMode

CIMaskToAlpha

CIMaximumComponent

CIMaximumCompositing

CIMinimumComponent

CIMinimumCompositing

CIModTransition

CIMultiplyBlendMode

CIMultiplyCompositing

CIOverlayBlendMode

CIPinchDistortion

CIPixellate

CIRadialGradient

CIRandomGenerator

CISaturationBlendMode

CIScreenBlendMode

CISepiaTone

CISharpenLuminance

CISixfoldReflectedTile

CISixfoldRotatedTile

CISmoothLinearGradient

CISoftLightBlendMode

CISourceAtopCompositing

CISourceInCompositing

CISourceOutCompositing

CISourceOverCompositing

CIStarShineGenerator

CIStraightenFilter

CIStripesGenerator

CISwipeTransition

CITemperatureAndTint

CIToneCurve

CITriangleKaleidoscope

CITwelvefoldReflectedTile

CITwirlDistortion

CIUnsharpMask

CIVibrance

CIVignette

CIVortexDistortion

CIWhitePointAdjust



2012-12-07

Method Swizzling をうまく使っている実用例

Method Swizzlingは、既存のメソッドの実装を、自前の実装に差し替えるための手法です。


・・・ということを知ってはいても、どういうときに使うと便利なのかイマイチわかってなかったので、Method Swizzlingをうまく使った実用例を2つほど探してきました。


実用例その1:既存ソースコードに手を入れずに機能追加

xib ファイルのローカライズを IB 上でできるようにする AutoNibL10n

通常、xibで作成したUIをローカライズする場合、

  • xibファイルを言語ごとに用意する
  • アウトレットを定義してプログラム側からローカライズした文言をセットする

といった面倒な作業が必要でしたが、 AutoNibL10n を使用すると、xibファイルを IB から直接多言語対応できるようになります。


たとえば、RootViewController.xibというファイルがあり、その中のUILabelオブジェクトを多言語化したい場合、直接 IB から UILabel のtextプロパティに "mytext" を指定すれば、Localizable.strings に"mytext"というキーで定義された各言語の文字列が挿入されるようになります。


AutoNibL10n における Method Swizzling

この AutoNibL10n、使用するために新たにコードを書く必要は一切なく、ヘッダをインポートする必要すらありません。


どうやっているかというと、Method Swizzling を利用して、load メソッドがコールされるタイミングで(=メモリにアプリケーションがロードされるタイミングで) NSObject の awakeFromNib メソッドを自前の localizeNibObject メソッドに入れ替えることで実現しています。

+(void)load {
    Method localizeNibObject = class_getInstanceMethod([NSObject class], @selector(localizeNibObject));
    Method awakeFromNib = class_getInstanceMethod([NSObject class], @selector(awakeFromNib));
    method_exchangeImplementations(awakeFromNib, localizeNibObject);
}

awakeFromNib の差し替え後のメソッド localizeNibObject の実装はこんな感じです。

-(void)localizeNibObject {
    LocalizeIfClass(UIBarButtonItem);
    else LocalizeIfClass(UIBarItem);
    else LocalizeIfClass(UIButton);
    else LocalizeIfClass(UILabel);
    else LocalizeIfClass(UINavigationItem);
    else LocalizeIfClass(UISearchBar);
    else LocalizeIfClass(UISegmentedControl);
    else LocalizeIfClass(UITextField);
    else LocalizeIfClass(UITextView);
    else LocalizeIfClass(UIViewController);

    if (self.isAccessibilityElement == YES)
    {
        self.accessibilityLabel = localizedString(self.accessibilityLabel);
        self.accessibilityHint = localizedString(self.accessibilityHint);
    }

    [self localizeNibObject]; // actually calls awakeFromNib as we did some method swizzling
}

この中でコールされている localizedString メソッドで、NSBundle の localizedStringForKey:value:table: メソッドを用いて Localizable.strings に定義されたローカライズ文字列を取得しています。

return [[NSBundle mainBundle] localizedStringForKey:aString value:nil table:nil];

実用例その2:iOSバージョンの違いを吸収する

UIRefreshControl を iOS5 でも使えるようにする ISRefreshControl

ISRefreshControl は、iOS6で導入された UIRefreshControl を iOS5 でも使えるようにするライブラリです。


UIRefreshControl と同じ API なので、導入する側は UIRefreshControl と同じように実装するだけ

self.refreshControl = (id)[[ISRefreshControl alloc] init];
[self.refreshControl addTarget:self
                        action:@selector(refresh)
              forControlEvents:UIControlEventValueChanged];

これだけで、iOS6 で動作させると UIKit の UIRefreshControl が呼ばれ、iOS5 で動作させると ISRefreshControl (UIRefreshControl と同じ見た目、挙動)が呼ばれます


ISRefreshControl における Method Swizzling

UITableViewController+RefreshControl というカテゴリが用意されていて、これの load メソッド内で、次のように Method Swizzling が行われています。

+ (void)load
{
    @autoreleasepool {
        if ([[[UIDevice currentDevice] systemVersion] hasPrefix:@"5"]) {
            Swizzle([self class], @selector(refreshControl), @selector(iOS5_refreshControl));
            Swizzle([self class], @selector(setRefreshControl:), @selector(iOS5_setRefreshControl:));
        }
    }
}

(※Swizzle()は同ファイルに定義されているMethod Swizzlingのラッパー関数)

iOSのバージョンが "5" だった場合に、UITableViewController の refreshControl というプロパティの setter と getter を差し替える実装になっていることがわかります。


iOS5のときに差し替える実装は、

- (ISRefreshControl *)iOS5_refreshControl
{
    return objc_getAssociatedObject(self, @"iOS5RefreshControl");
}

- (void)iOS5_setRefreshControl:(ISRefreshControl *)refreshControl
{
    objc_setAssociatedObject(self, @"iOS5RefreshControl", refreshControl, OBJC_ASSOCIATION_RETAIN);
}

となっていて、Associated Objectの仕組みを利用して iOS5 で UITableViewController の refreshControl というプロパティにアクセスできるようにしています。

で、KVOでrefreshControlプロパティを見張り、

[self addObserver:self
       forKeyPath:@"refreshControl"
          options:options
          context:NULL];

refreshControlプロパティに値(UIViewサブクラスのオブジェクト)がセットされたときに、所定の位置にそのビューを貼付ける、という実装になっています。

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    if (object == self && [keyPath isEqualToString:@"refreshControl"]) {
        UIView *oldView = [change objectForKey:@"old"];
        UIView *newView = [change objectForKey:@"new"];
    
        if ([oldView isKindOfClass:[UIView class]]) {
            [oldView removeFromSuperview];
        }
        if ([newView isKindOfClass:[UIView class]]) {
            newView.frame = CGRectMake(0, -50, self.view.frame.size.width, 50);
            newView.autoresizingMask = UIViewAutoresizingFlexibleWidth;
            [newView setNeedsLayout];
            [self.view addSubview:newView];
        }
        return;
    }
    // 後略
}

このように、Method Swizzling を用いることで、導入する側は iOS のバージョン判定も、このライブラリ独自のAPIを使う必要もなく、UIRefreshControlと同じように実装するだけ、というカンタン導入を実現しています。


Method Swizzlingの参考書籍

日本語の本だと、『iOS SDK Hacks』に解説と事例が掲載されています。


iOS SDK Hacks ―プロが教えるiPhoneアプリ開発テクニック
吉田 悠一 高山 征大 UICoderz
オライリージャパン
売り上げランキング: 226658


まとめ

Method Swizzlingをうまく使っている実用例を2つ紹介しました。

これ以外にも、単体テストで使える OCMock というライブラリも、Method Swizzling を利用して既存オブジェクトのメソッドを差し替えてテストできるようにしていたりと、Github上にはたくさんの実用例が転がっているようなので、また追記したいと思います。



2012-12-03

AVFoundation 使用時のカメラ起動を高速化する

AVFoundation を用いて静止画とか動画とかを撮影する場合に、カメラの起動時間を速くする方法です。


計測してみると、どうも AVCaptureSession の startRunning に一番時間がかかってるので、

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(queue, ^{
    
    [self.captureSession startRunning];

    dispatch_async(dispatch_get_main_queue(), ^{
        
        // メインスレッドでの処理(UIまわり)
    });
});

こうすることで体感起動時間を速くできました。

(要は startRunning をバックグラウンドで処理するだけ)


ただ、AVCaptureSession の startRunning は非同期で行われていてそれ自体は速くはなっていないので、プレビュー用の AVCaptureVideoPreviewLayer を使い回している場合は、カメラ画面に遷移したあと startRunning が完了するまでの間、プレビューレイヤに前回撮影時の画像が描画されたままだったりします。その場合は、

self.previewLayer.hidden = YES;

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(queue, ^{

    [self.captureSession startRunning];

    dispatch_async(dispatch_get_main_queue(), ^{
        
        self.previewLayer.hidden = NO;
        
        // メインスレッドでの処理(UIまわり)
    });
});

こんな感じで出したり隠したりして対処しました。


僕の実装ケースにしか当てはまらないのかもしれませんが・・



2012-12-02

シャッター音の鳴らないカメラアプリの実装方法

「カメラ機能をアプリにつけたいけどシャッター音を鳴らしたくない」とか、「カメラ起動時のアニメーションが嫌だ」とか、カメラ機能をもっと自由にカスタマイズしたい場合は、UIImagePickerController を使うのではなく AVFoundation フレームワークを使う必要があります。


先日、UIImagePickerController を使わないカメラアプリの実装方法という記事を書いたのですが、そこでは UIImagePickerController を使わず、AVFoundation を用いて静止画を撮影するところまで書きました。


今回は、シャッター音を鳴らさない、いわゆる「静音カメラ」「マナーカメラ」「無音カメラ」と呼ばれている機能を実装します。


静止画撮影はデフォルトの挙動としてシャッター音がなってしまうようになっていてそこは変えられないので、動画モードで撮影を開始し、必要なフレームを静止画として取り込むことで「シャッター音の鳴らないカメラ」を実現します。


参考書籍及びサンプルコード

今回のサンプルコードは、gumroad よりダウンロードしていただけます。


http://gum.co/pzBMA


※すいません、無料ではなく、85円です。

(ほんとは50円ぐらいにしたかったけど、gumroadの制限によりこれ以下には設定できませんでした)


【注意点】

  • iOS 6.0 で動作確認しております。
  • コードのみの販売です。サポート等はご容赦ください

また、上記サンプルは、次の書籍を参考に実装しました。

iOS4プログラミングブック
畑 圭輔 加藤 寛人 坂本 一樹 藤川 宏之 高橋 啓治郎 沖田 知彦 柳澤 昇
インプレスジャパン
売り上げランキング: 32117


こちらの第3章『マルチメディア』の章とサンプルコードが参考になるかと思います。(本記事の数倍参考になります。おすすめです。)


準備

AVFoundation.framework, AssetsLibrary.framework に加えて、以下のフレームワークをプロジェクトに追加します。

  • CoreMedia.framework
  • CoreVideo.framework

ヘッダでの宣言

変数

撮影を制御するのに使用するフラグと、ビットマップ保存領域用のポインタを定義します。

BOOL isRequireTakePhoto;
BOOL isProcessingTakePhoto;
void *bitmap;

プロパティ
@property (nonatomic, retain) UIImage *imageBuffer;

プロトコルへの準拠を宣言
<AVCaptureVideoDataOutputSampleBufferDelegate>

実装

イメージバッファ初期化処理

バッファ確保処理をカメラデバイス初期化コード(参考:http://d.hatena.ne.jp/shu223/20121002/1354424588title=前回記事])の前あたりに書いておきます。

size_t width = 640;
size_t height = 480;
bitmap = malloc(width * height * 4);
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
CGDataProviderRef dataProviderRef = CGDataProviderCreateWithData(NULL, bitmap, width * height * 4, NULL);
CGImageRef cgImage = CGImageCreate(width, height, 8, 32, width * 4, colorSpace, kCGBitmapByteOrder32Little | kCGImageAlphaPremultipliedFirst, dataProviderRef, NULL, 0, kCGRenderingIntentDefault);
self.imageBuffer = [UIImage imageWithCGImage:cgImage];
CGColorSpaceRelease(colorSpace);
CGDataProviderRelease(dataProviderRef);

出力関連の初期化処理

前回は静止画の撮影だったので出力先として AVCaptureStillImageOutput を用いましたが、今回は動画の撮影なので、AVCaptureVideoDataOutput を使います。

//self.imageOutput = [[AVCaptureStillImageOutput alloc] init];
AVCaptureVideoDataOutput *videoOutput = [[AVCaptureVideoDataOutput alloc] init];
[captureSession addOutput:videoOutput];

デリゲートの設定

下記のようにデリゲートを指定することで、AVCaptureVideoDataOutputSampleBufferDelegate プロトコルの captureOutput:didOutputSampleBuffer:fromConnection: が毎フレーム呼ばれるようになります。

videoOutput.alwaysDiscardsLateVideoFrames = YES;
dispatch_queue_t queue = dispatch_queue_create("com.overout223.myQueue", NULL);
[videoOutput setSampleBufferDelegate:self
                               queue:queue];
dispatch_release(queue);

シャッターボタンのアクションを実装

ここでは「写真撮るよフラグ」を立てるだけです。

- (IBAction)pressShutter {

	if (!isProcessingTakePhoto) {

        isRequireTakePhoto = YES;
    }
}

プロトコルの実装

カメラが動画のフレームを取得する度に(つまり撮影時の毎フレーム)、AVCaptureVideoDataOutputSampleBufferDelegate プロトコルの captureOutput:didOutputSampleBuffer:fromConnection: メソッドが呼ばれるので、そこで必要に応じて画像の保存処理を行います。


ユーザーがシャッターボタンを押すと isProcessingTakePhoto フラグが YES になっているので、そのときだけカメラロールへの保存処理を行うようにします。

- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer
       fromConnection:(AVCaptureConnection *)connection {

    if (isRequireTakePhoto) {

        isRequireTakePhoto = NO;
        isProcessingTakePhoto = YES;

        CVPixelBufferRef pixbuff = CMSampleBufferGetImageBuffer(sampleBuffer);

        if(CVPixelBufferLockBaseAddress(pixbuff, 0) == kCVReturnSuccess){

            memcpy(bitmap, CVPixelBufferGetBaseAddress(pixbuff), 640 * 480 * 4);

            CMAttachmentMode attachmentMode;
            CFDictionaryRef metadataDictionary = CMGetAttachment(sampleBuffer, CFSTR("MetadataDictionary"), &attachmentMode);

            // フォトアルバムに保存
            ALAssetsLibrary* library = [[ALAssetsLibrary alloc] init];

            [library writeImageToSavedPhotosAlbum:self.imageBuffer.CGImage
                                         metadata:(NSDictionary *)CFBridgingRelease(metadataDictionary)
                                  completionBlock:^(NSURL *assetURL, NSError *error) {
                                      
                                      NSLog(@"URL:%@", assetURL);
                                      NSLog(@"error:%@", error);
                                      isProcessingTakePhoto = NO;
                                  }];

            CVPixelBufferUnlockBaseAddress(pixbuff, 0);
		}
	}
}

できあがり

f:id:shu223:20121202175000p:image:w160


プレビュー表示と「シャッター音の鳴らない」撮影ボタンだけからなる、シンプルな無音カメラアプリのできあがりです。


まとめ

AVFoundation フレームワークを用いて、「シャッター音の鳴らないカメラ」を実装する方法についてご紹介しました。


サンプルコードを見ると、一見長くて難しいですが、「画像の保存」などおなじみの処理、各種初期化等の手順がややこしくて覚えにくい処理等をスニペット化してしまえば、30分といわず10分でもできてしまうかもしれません。


AVFoundationはまだまだまだ探求の余地があるので、また勉強しつつ記事にしたいと思います。



2009 | 08 |
2011 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 10 | 11 | 12 |
2012 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 10 | 11 | 12 |
2013 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 10 | 11 | 12 |
2014 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 10 | 11 | 12 |
2015 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 10 | 11 | 12 |
2016 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 11 | 12 |
2017 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 10 | 11 | 12 |
2018 | 02 |