廃墟

本ブログは更新を終了しました。 技術的な記事のみ、有用性を鑑みて残しておきます。

viewDidUnloadが使えなくなった今、ViewControllerでどうやってNSNotificationをobserveするか

表題の通りですが、ViewControllerでNSNotificationを受け取る事は非常に典型的なパターンです。
ここでは2通りの想定パターンについて簡単に紹介します。 (以下のコードはARCを利用している事を前提としています)

画面が表示されてないときはNotificationを受け取らなくて良い場合

- (void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didChangeHoge:) name:AKNHogeDidChangeNotification object:nil];
}

- (void)viewDidDisappear:(BOOL)animated
{
    [super viewDidDisappear:animated];
    [[NSNotificationCenter defaultCenter] removeObserver:self];
}

画面が表示されてないときにもNotificationを受け取りたい場合

こっちが本題です。

viewDidUnoadがあった頃は、viewDidUnloadとdeallocでやれば良かったのですが、そういうわけにはいかなくなりました。

また、アプリケーションの状態遷移によっては以下の様なケースがあるようで、これらに対処しなきゃなりません。

  • ビューが最初にロードされる時にviewDidLoadが呼ばれる
  • 他の画面に遷移
  • メモリ警告発生(didReceiveMemoryWarningが呼ばれる)
  • 元の画面に遷移
  • viewDidLoadがまた呼ばれる

参考記事でも言及されているように、何も考慮せずにviewDidLoadでaddObserverしてdeallocでremoveObserverするだけだとNotificationを二回受け取る事になってしまうので、都合が悪いです。

なので、didReceiveMemoryWarningでremoveObserverしても良いんですけど、今後のiOSアップデートでちょっとでもイベントループが変化すると破綻するような感じで、あんまりやりたくない方針。

また、init系メソッドでaddObserverするという手もありますが、ビューのないビューコントローラでNotificationを受け取ったらクラッシュしそうだし、それを防ぐ制御コードを書くのも何だか馬鹿らしい。

などと考えた結果、以下の様にしちゃえば良いという事に気づきました。

- (void)viewDidLoad
{
    [self addNotificationObserver];
}
- (void)dealloc
{
    [self removeNotificationObserver];
}

- (void)addNotificationObserver
{
    // 念のため先に削除する。
    [self removeNotificationObserver];
    
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didChangeHoge:) name:AKNHogeDidChangeNotification object:nil];
}
- (void)removeNotificationObserver
{
    [[NSNotificationCenter defaultCenter] removeObserver:self];
}

流石に、これが期待通りに動かなくなるようなイベントループの変更はこないでしょう。たぶん。。

大いに参考にした記事

もとい、元ネタです。 iOS5まではこの方法で良かったんだよね。*1

*1:僕が本格的にコード書き始めたのはiOS6からだから、詳しくは知らないんですけどね。

Mac OSX mountain Lion でレインボーカーソルが出る問題とその対処(要約:Sophosをアンインストールしよう)

僕のMacBook Air 11-inch, Mid 2013 (OSX 10.9.1)での話ですが、多くのバージョンで同じ様な問題が発生するのではないかと。

経緯とか

最近どうもネットを見るときもワンテンポ遅れる。それだけならまだしも、レインボーカーソルが出てOSが反応しなくなってしまう事が増えたなぁと思っていて、
原因を調べてみると、bluetoothからのスリープ解除を無効にしろとかPRAMリセットしろとか、いろんな情報がひっかかります。
僕はbluetoothからのスリープ解除は使ってるし、それ以外の方法もあまり気乗りがしないなーと思いつつ、この状況にいつ頃からなったんだっけ、と考えていました。

そういえば、先日Macのセキュリティが不安になって無料のアンチウイルスソフト、Sophos Anti Virus を入れたんでした。
インストール直後からDNSをフックしてるのかブラウジングは重かったし、ちょうどその頃からだなーと思って
「Sophos rainbow cursor」などと検索してみると、こんな記事がヒット。

はい。どう見てもSophosが原因ですね。

Sophos Anti Virus のアンインストール方法

ちなみに、アンインストール手段が分かりにくいのですが、以下の手段で削除できるはずです。 (人によっては "Macintosh HD"が違う名前になってる事もあります。)
ここでは現時点(2014年2月6日)で最新のSophosのバージョン9系を想定しています。

  1. 「ターミナル.app」を開く
  2. cd "/Volumes/Macintosh HD/Library/Sophos Anti-Virus" と入力してEnter
  3. sudo ./remove_v9.sh
  4. パスワードを聞かれるので入力してEnter
  5. 5秒ぐらい待つと削除処理が動くので、止まったのを確認してからターミナルを閉じる

今のところおすすめはESET Cyver Securityかなぁ。

さて、削除も済んだところで、軽さに定評のある僕はESET Cyber Securityを導入しました。まだ体験版ですが、このまま何事もなければ購入すると思います。
Kasperskyとも迷ったんですが、ずいぶん昔に未知のウイルスをヒューリスティクスで検出してて印象よかったし、評判も良かったのでESETにしてみました。
別に他の製品でも良いと思いますけれど、どうしても有償ソフトの方が信頼出来ますね。以外と安いし。

Amazonでも買えるみたいです。


※ あまり無責任な事を言うとSophosの中の人からおこられるなーと思ったので、ESETに変えてから数日たってから記事を公開しています。 残念ながらSophosアンチウイルスが原因である事は明白でした。こういうソフトを作るのは大変ですね。

iPhone5S(arm64)でBlocksKitが使えるようになってた

libffiのバグが原因で、arm64向けにビルドしているとBlocksKitが使えませんでしたが、つい5日前ぐらいに直ったようです。

これは以前のBlocksKit1系ではなく、2系に対して行われています。
詳しく調べていないのですが、2系で廃止されたメソッドがある場合には対処できないかもしれません。 その場合は、速度を諦めてarmv7のみでビルドするなどで対処する方が良さそうです。

ただ、少し見た限り、2系はメソッドが洗練されていて、1系より良くなっているようでした。

現時点で対応するための Podfile の書き方

とりあえず以下のように書いておけばOKです。 そのうちstableとしてリリースされるでしょう。

pod 'BlocksKit', :git => 'https://github.com/pandamonia/BlocksKit.git', :commit => 'aa99a68674'

Issueに記述されている以下の指定方法だと、時折nextブランチにバグが含まれたりして、頻繁にバグを含んでしまいます。(実際ひっかかった…) まぁリリース前なのでしょうがないですね。

pod 'BlocksKit', :git => 'https://github.com/pandamonia/BlocksKit.git', :branch => 'next'


追記

こんな感じで紹介してもらったのですが、libffiを使わずに似たような機能を提供してくれるライブラリもあるみたいです。かなりしっかり作られているようなので、こちらの導入も検討してみたいですね。

iOSアプリ開発における正しいMVC

この記事は iPhone Advent Calendar 2013 - Adventar の12月24日分の記事になります。

今年もアドベントカレンダーが熱いわけですが、今年は僕は年中iOSアプリのコードを書いていたので、その中で手に入れた or 手に入れつつある様々な知見を皆さんに紹介していきたいと思います。

MVCって何だっけ? 〜UITableViewCellってあれで良いのか?〜

この記事を読んでいる方で、こんなコードを書いている人はいませんか?

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Cell" forIndexPath:indexPath];
    
    UIImageView *myImageView = (UIImageView *)[cell viewWithTag:10];
    
    myImageView.image = [UIImage imageNamed:self.imageName];
    
    return cell;
}

はい。少し前までの僕です。。
しかし、ここはあえて言わせて頂きたい。 糞コードだ! …と。

そもそもこれって、Viewの分離ができていないわけで、"M"と"VC"でしか分かれてないわけです。 それってPHPSymfony使って書いてるのと変わらない概念なわけで、そんな前時代的な(以下省略

正しいUITableViewCellの扱い方

こんな時、どうすればいいのかっていうと、最低限やりたいのが「Viewの分離」なわけです。Controllerのもつ役割とViewの持つ役割を分離しましょう。

具体的には、こんな感じ。

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *CellIdentifier = @"Cell";
    AKNGoodTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
    
    AKNEntry *entry = [self.fetchedResultsController objectAtIndexPath:indexPath];
    cell.titleLabel.text = entry.title;
    cell.bodyLabel.text  = entry.body;
    
    return cell;
}

// 途中省略

@interface AKNGoodTableViewCell : UITableViewCell

@property (nonatomic) IBOutlet UILabel *titleLabel;
@property (nonatomic) IBOutlet UILabel *bodyLabel;

@end

@implementation AKNGoodTableViewCell

@end

こんなふうにして、StoryBoardからアウトレットを引っ張ってあげましょう。 ちゃんとAutomatic の所に出てきますよ。

と、ここまで書いたのですが、コードで説明したほうが早いのでコードを書いていたら深夜になったので、一旦この記事を公開してしまいます。
後で説明を追記しようと思うので、良かったら左上、「読者になる」ボタンを押してやって下さい!

AknEp/AKNAdventar · GitHub

その他ありそうな疑問など

これじゃCellを複数のViewControllerで使いまわせないけどどうしたらいいの?

セルに対応したNibファイルを作っておいて、 registerNib:forCellReuseIdentifier: というメソッドを使うと解決します。
但し、UITableViewControllerで直接的に行うのではなく、対応するセルのクラス側にクラスメソッドを容易すると、よりエレガントになりそうです。 (ファイル名を再定義する必要がなくなるので。)

これって普通のViewController でも同じようなことをした方が良いの?

yesです。

特に、複雑な画面では、部分ごとにUIViewのサブクラスを作ってあげると、効果的でしょうね。

フリーランスでやってます。

いろいろ引き受けてます。興味があったら以下のメールアドレスまでどうぞ。 ご相談お待ちしております。

master(at-mark)silph.jp

UIView の setFrame: をオーバーライドするときに気をつけるべきこと

UITableViewCellの横幅を調整する — ひよっこ
この辺の記事を読んでいて思ったこと。

こういうミスって、オブジェクト指向に真摯に向き合ってないと、ついついやらかしがちだよなーと思ったので、なるべく分かりやすくメモしてみる。

大前提として、元記事の通り、こんな風にsetFrameがオーバーライドされているとします。

// UITableViewCell のサブクラスにて。 元記事からコピペ
- (void)setFrame:(CGRect)frame
{
        frame.origin.x += self.inset;
        frame.size.width -= 2 * self.inset;
        [super setFrame:frame];
}

さて、ここでセルが選択されたら少しだけ右に寄って、解除されたら左に戻るようにしてみましょう。

// id<UITableViewDelegate> にて。 UITableViewControllerとかで。

static const CGFloat selectedCellOffset = 10;

- (NSIndexPath *)tableView:(UITableView *)tableView willSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
    if ( ![[tableView indexPathForSelectedRow] isEqual:indexPath] ) {
        UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
        
        CGRect frame = cell.frame;
        frame.origin.x += selectedCellOffset;
        cell.frame = frame;
    }
    
    return indexPath;
}

- (void)tableView:(UITableView *)tableView didDeselectRowAtIndexPath:(NSIndexPath *)indexPath
{
    UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
    
    CGRect frame = cell.frame;
    frame.origin.x -= selectedCellOffset;
    cell.frame = frame;
}

実際にやってみなくても分かるようなことですが、
今回のようにframeで取得した値を利用してsetFrameが呼ばれるようなケースでは、先の例はバグを引き起こします。セルがどんどん小さくなっていく。 なんかまぬけで、実行してみるとちょっと笑える(?)

長く説明したけど、そもそも、ゲッタから受け取った値をセッタに代入できない時点で、何かおかしい訳ですよ。 Javaの入門書でも読んだら分かるようなことです。

ベターな解決策を提示してみます、こんな所でしょうか。

簡易版

// UITableViewCellのサブクラスに以下を書き加えましょう
- (void)setFrame:(CGRect)frame
{
        frame.origin.x += self.inset;
        frame.size.width -= 2 * self.inset;
        [super setFrame:frame];
}

ちょっと凝った版(あんまり変わらないような気も…)

@implementation AKNTableViewCell
{
    CGRect preservedFrame;
}

- (CGRect)frame
{
    return preservedFrame;
}

- (void)setFrame:(CGRect)frame
{
    self->preservedFrame = frame;
}

- (void)layoutSubviews
{
    CGRect insetFrame = [self insetFrame:self->preservedFrame];
    [super setFrame:insetFrame];
    [super layoutSubviews];
}

- (CGRect)insetFrame:(CGRect)frame
{
    frame.origin.x += inset;
    frame.origin.y += inset;
    frame.size.width -= 2 * inset;
    frame.size.height-= 2 * inset;
    
    return frame;
}

@end

「Rails勉強会@つくば#1」で発表してきました

@hurutoriya さんが主催してくれた Rails勉強会@つくば #1 に参加してきました。

iOSRailsを組み合わせて使う時の話を、LTとして、Rails開発者向けに行いました。

以下にスライドを置いておくので、良かったら見てください。

CocoaPodsでAdMobをインストールしようとすると"Unable to find a specification" と言われる

みなさん使ってますよね。CocoaPods。

AdMobの名前が変わってるみたいで、以下のように変えるといいようです。

- pod 'AdMob'
+ pod 'Google-Mobile-Ads-SDK'

アプリケーションコードはそのままでビルドが通りました。 基本的には同じもののようですね。