NSKeyedUnarchiverを使った正しい復元方法

NSKeyedArchiverで圧縮(シリアライズ)したコードは復元できない場合があるらしく、例外処理を挟んでいないとクラッシュするかもしれません。Appleのドキュメント「NSKeyedUnarchiver」のunarchiveObjectWithDataには注意書きがあります。

Discussion
This method raises an NSInvalidArchiveOperationException if data is not a valid archive.

と、言う事はアーカイブした後で復元できるかチェックしておけば良いとも考えたんですが、どうやら、iOSやアプリのアップデートにより、復元できない場合がある模様。アーカイブのチェックは、復元の時だけ例外処理を挟んでおくのが良さそうです。

NSData *archiveData = [[NSUserDefaults standardUserDefaults] objectForKey:@"ArchiveKey"];
if (archiveData != nil) {
    NSArray *array;
    @try {
        array = [NSKeyedUnarchiver unarchiveObjectWithData:archiveData];
    }
    @catch (NSException *exception) {
        [[NSUserDefaults standardUserDefaults] removeObjectForKey:@"ArchiveKey"];
        [[NSUserDefaults standardUserDefaults] synchronize];
    }
}

例えば、こんな感じでユーザーデフォルトにアーカイブDataを保存していて、例外をキャッチした時には、ゴミDataを取り除いておきます。

Appleのドキュメント「Archives and Serializations Programming Guide」のArchiveに例外についての詳しい説明があります。型が一致しない場合に例外が発生するようです。

Type Coercions
NSKeyedUnarchiver supports limited type coercion. A value encoded as any type of integer, be it a standard int or an explicit 32-bit or 64-bit integer, can be decoded using any of the integer decode methods. Likewise, a value encoded as a float or double can be decoded as either a float or a double value. When decoding a double value as a float, though, the decoded value loses precision. If an encoded value is too large to fit within the coerced decoded type, the decoding method throws an NSRangeException. Further, when trying to coerce a value to an incompatible type, such as decoding an int as a float, the decoding method throws an NSInvalidUnarchiveOperationException.

超簡単!WordPressの記事中に広告を差し込む

記事中の任意の場所に広告を掲載

functions.phpに広告用のショートタグを登録しておくと、タグを打った場所に広告を表示させられます。

function ads() {
    if (wp_is_mobile()) {
        return <<< EOM
            // 携帯用レスポンシブ広告コード
EOM;
    } else {
        return <<< EOM
            // パソコン用広告コード
EOM;
    }
}
add_shortcode('ads', 'ads');

記事中に[ads]と書けば、その場所に広告が表示されます。

記事中に自動挿入させる

functions.phpで記事表示フィルタに置換処理を追加します。記事を書く時に広告の事を意識せずに済むので大変便利です。上記のads関数を流用します。

function insert_ads_for_single($the_content) {
    if (is_single()) {
        $tag = '</p>';
        return preg_replace("|$tag|", $tag . ads(), $the_content, 3);
    }
    return $the_content;
}
add_filter('the_content', 'insert_ads_for_single');

段落のあとに広告を追加する処理を3回行います。見出し直後に広告を配置するのはGoogle的に良くないそうです。

UIViewControllerの経路を判別する

とあるViewControllerをモーダルでもナビゲーションでも使いたい時に、その経路を判別したいことがあります。例えば、再表示の時だけ再描画したいとか・・・。

表示する時

viewDidLoad、view~LayoutSubviewsでは残念ながら使えません。
view~Appearで判別できます。

- (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];
    if ([self isBeingPresented]) {
        // presentViewControllerされました。
    } else if ([self isMovingToParentViewController]) {
        // pushViewControllerされました。
    }
}

戻る・閉じる時

view~Disappearで判別できます。

- (void)viewWillDisappear:(BOOL)animated {
    [super viewWillDisappear:animated];
    if ([self isBeingDismissed]) {
        // dismissViewControllerされました。
    } else if ([self isMovingFromParentViewController]) {
        // popViewControllerされました。
    }
}

注意

isBeingがモーダル、isMovingがナビゲーション、となっています。
ただし、ナビゲーションコントローラ自体をpresentしてモーダル表示した場合は、[self.navigationController isBeing~];
にしないと、該当VCからでは判別できないので注意してください。

以上をふまえて判別メソッドをつくる

このVCが新たに表示される時かどうか

- (BOOL)isMovingToVisibleViewController {
    if (self.navigationController == nil) {
        return [self isBeingPresented];
    }
    return [self.navigationController isBeingPresented] || [self isMovingToParentViewController];
}

このVCが無くなる時かどうか

- (BOOL)isMovingFromVisibleViewController {
    if (self.navigationController == nil) {
        return [self isBeingDismissed];
    }
    return [self.navigationController isBeingDismissed] || [self isMovingFromParentViewController];
}

iOS 9.2

AVAudioEngineでiPodライブラリの曲を再生しよう

iOS8から導入されたAVAudioEngineを使えば、Core Audioよりも簡単!と、聞いていたので、iPodの曲を再生させてみました。追加できる音響効果はEQ、リバーブ、ピッチシフト、タイムストレッチなどのAudio Unit由来のものが使えるようです。今回は簡単なクラスAEAudioPlayerControllerを作ってリバーブをかけて再生してみます。

AEAudioPlayerController.h

#import <Foundation/Foundation.h>
#import <AVFoundation/AVFoundation.h>
#import <MediaPlayer/MediaPlayer.h>

@interface AEPlayerController : NSObject

+ (instancetype)sharedController;
- (void)play;

@end

AEAudioPlayerController.m

@property (nonatomic) AVAudioEngine *engine;
@property (nonatomic) AVAudioPlayerNode *playerNode;
@property (nonatomic) AVAudioUnitReverb *reverbNode;
@property (nonatomic) AVAudioFile *audioFile;
@property (nonatomic) MPMediaItem *nowPlayingItem;

AVAudioPlayerNodeとはプレイヤーの様なものですが、AVAudioPlayerとは全くの他人
さらにリバーブがAVAudioUnitReverbとなっていてNodeが付いていない

+ (instancetype)sharedController {
    static id sharedController;
    static dispatch_once_t queue = 0;
    dispatch_once(&queue, ^ {
        sharedController = [self new];
    });
    return sharedController;
}
- (instancetype)init {
    self = [super init];
    if (self) {
        self.engine = [AVAudioEngine new];
        self.playerNode = [AVAudioPlayerNode new];
        self.reverbNode = [AVAudioUnitReverb new];
        self.reverbNode.wetDryMix = 40.0f;
        // attach to engine
        [self.engine attachNode:self.playerNode];
        [self.engine attachNode:self.reverbNode];
        // player -> reverb -> mixer
        [self.engine connect:self.playerNode to:self.reverbNode format:self.audioFile.processingFormat];
        [self.engine connect:self.reverbNode to:self.engine.mainMixerNode format:self.audioFile.processingFormat];
        // 適当に再生できる曲をセット
        MPMediaQuery *query = [MPMediaQuery songsQuery];
        [query addFilterPredicate:[MPMediaPropertyPredicate predicateWithValue:@NO forProperty:MPMediaItemPropertyIsCloudItem]];
        self.nowPlayingItem = query.items[0];
    }
    return self;
}
- (void)play {
    NSURL *url = [self.nowPlayingItem valueForProperty:MPMediaItemPropertyAssetURL];
    self.audioFile = [[AVAudioFile alloc] initForReading:url error:nil];
    [self.engine startAndReturnError:nil];
    [self loadBuffer];
    [self.playerNode play];
}
- (void)loadBuffer {
    uint32_t capacity = 32768;
    AVAudioPCMBuffer *buffer = [[AVAudioPCMBuffer alloc] initWithPCMFormat:self.audioFile.processingFormat frameCapacity:capacity];
    if (buffer != nil) {
        [self.audioFile readIntoBuffer:buffer error:nil];
        [self scheduleBuffer:buffer];
    }
}
- (void)scheduleBuffer:(AVAudioPCMBuffer *)buffer {
    [self.playerNode scheduleBuffer:buffer atTime:nil options:AVAudioPlayerNodeBufferInterrupts completionHandler:^{
        [self loadBuffer];
    }];
}

適当な場所で[[AEAudioPlayerController sharedController] play];すると再生開始。loadBufferすると繰り返しloadBufferが呼ばれて少しづつバッファが溜まる仕組みです。iPodの曲はアプリにバンドルされたファイルを再生するのと違って、こうしておかないと再生されないみたい。これなら、ストリーミングにも使えそうでいいかも。ただ、プレイヤーとして完成させるには、まだまだ機能不足。それでも再生だけならCore Audioより楽かも。

EQも挟むならこんな感じになります。

// player -> eq -> reverb -> mixer
[self.engine connect:self.playerNode to:self.eqNode format:self.audioFile.processingFormat];
[self.engine connect:self.eqNode to:self.reverbNode format:self.audioFile.processingFormat];
[self.engine connect:self.reverbNode to:self.engine.mainMixerNode format:self.audioFile.processingFormat];

iOS8.1

Xcode6でプロジェクトにpchファイルを追加する

Xcode6からpchファイルが新規プロジェクトには含まれなくなりましたので、追加する方法をご紹介。

まず、「New File…」で「Prefix.pch」を追加します。
ファイル名は自由に決められますが、ここではPrefixで。

2015-03-30 14.24.15

2015-03-30 14.24.28

2015-03-30 14.24.40

ファイルを追加しただけでは機能しないので、ファイルの場所を設定しておきます。
「Build Settings > Apple LLVM 6.0 – Language > Prefix Header」の行に「$(PRODUCT_NAME)/Prefix.pch」を設定します。

2015-03-30 14.27.32

2015-03-30 14.27.37

iOSアプリ開発に便利なマクロ

Prefix.pchに定義しておくと便利なマクロです。
ファイルが見当たらない場合は次の記事も合わせてどうぞ。

短縮NSLog

#define LOG(format, ...) (NSLog(@"%s(L%d) %@", __PRETTY_FUNCTION__, __LINE__, [NSString stringWithFormat:format, ##__VA_ARGS__]))

NSLogと同じ使い方で「メソッド名」「行数」も追加して出力します。

設定言語判定

#define SYSTEM_LANGUAGE ([NSLocale preferredLanguages][0])
if ([SYSTEM_LANGUAGE isEqualToString:@"ja"]) {
    // 日本語で使用中
}

iOSバージョン判定

#define SYSTEM_VERSION_COMPARE(comparator, version) ([[UIDevice currentDevice].systemVersion compare:version options:NSNumericSearch] comparator NSOrderedSame)

比較演算子で分かりやすく分岐できます。

if (SYSTEM_VERSION_COMPARE(>=, @"4.2.10")) {
    // iOS4.2.10以上
}

iPhone or iPad デバイス判定

#define IS_IPAD ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad)
#define IS_IPHONE ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPhone)

条件の左辺はマクロが定義されているので、UI_USER_INTERFACE_IDIOM()に書き換えても同じです。

[UIDevice currentDevice].userInterfaceIdiom

の部分は新しい書き方だと

[UIScreen mainScreen].traitCollection.userInterfaceIdiom