viewWillAppear,viewDidAppearで一度だけ処理をする


2016/02/02 修正・加筆

## 一度だけ処理をしたい
タイミングによっては、 *viewDidLoad*より、 *viewWillAppear*, *viewDidAppear*で行ったほうが良い処理もあったりしますが、
*viewDidLoad*とは違い、後者の2つのメソッドは、他の画面に遷移して戻ってきた時にも発火してしまうので、一度だけ処理を行いたいものも何度も呼ばれてしまう可能性があります。
(例えば、 *viewWillAppear*のタイミングで、はじめて遷移した一度だけサーバーから何かを通信して取得したい場合や、
はじめて遷移した一度だけ、アニメーションを行う場合。アニメーションはiOS7以降、 *viewDidLoad*だと正常に動かない可能性があるため。)

シチュエーションはこんな感じです。

----
ある画面の遷移で、
`A → B → C`
と遷移するときに、画面Bにおいて、
`A → B` *(push)*
と遷移するときは*viewWillAppear*で**ある処理(X)**をしたくて、
`B ← C ` *(pop)*
と画面を戻るときは、*viewWillAppear*が呼ばれるけど、**処理X**は**したくない**。
再度、Aまで戻って、
`A → B` *(push)*
と遷移するときは*viewWillAppear*で**処理X**をしたい、

----

ある **ViewController**にはじめて遷移した且つ、  *viewWillAppear*, *viewDidAppear*で一度だけ処理を行いたい場合、以下のようにするのも1つの手段ですが、毎回それのための変数を用意するのは **正直だるい**です。

```swift:これを毎回書くのはだるい...
class SomeViewController: UIViewController {
    private var firstAppear: Bool = false
    
    override func viewWillAppear(animated: Bool) {
        super.viewWillAppear(animated)
        if !firstAppear {
            // do something...
            firstAppear = true
        }
        // ...
    }
}
```

なので、その実装を隠蔽して使い回しができるように、実装してみました。

[FirstAppearing](https://github.com/sgr-ksmt/FirstAppearing)
(Carthage/CocoaPods対応してあります。)

## 実装

実装は[こちら](https://github.com/sgr-ksmt/FirstAppearing/blob/master/FirstAppearing/FirstAppearing.swift)をご覧ください。

protocolとextension、Associated Objectを使っている程度の軽量ソースとなってます。
一応UIViewController(とその継承クラス)のみ使用できるようにprotocolに制約をかけています。

## 使用方法

以下のように、UIViewControllerに、 *FirstAppearing*を適応すれば、
<b>*viewWillAppearOnce*, *viewDidAppearOnce*</b>が使用できるようになります。
そのViewControllerが破棄されるまでの間、呼びだされても一度しか *closure*内に書いた処理を実行しません。
また、 **@noescape**属性付きなので、 **[unowned self]**みたいなのは不要です。

また、 *fromFunction*の引数にデフォルトで **\_\_FUNCTION\_\_**を仕込むことで、呼び出し元のメソッド名が取得できるので、
これを利用して、 **viewWillAppear/viewDidAppear**以外のメソッドで呼び出すのを抑制しています。

```swift:FirstAppearingを適応
class SomeViewController: UIViewController, FirstAppearing {

    override func viewDidLoad() {
        super.viewDidLoad()
        viewWillAppearOnce() {
            print("Wahaha!!") // not working!!
        }
    }
    
    override func viewWillAppear(animated: Bool) {
        super.viewWillAppear(animated)
        viewWillAppearOnce() {
            // do something...
            print("viewWillAppear Once") // call once!
        }
    }

}

```

(これだけだとサンプルとしては不足しているのですが、)
SomeViewControllerから別のViewControllerに遷移(modalでもpushでも)して、
戻ってきた時に、再度 *viewWillAppear*が呼ばれた時は、 *viewWillAppearOnce*内の処理は行われません。

## 追記: dispatch_onceを使う場合

コメントより頂きました。
boolのフラグを立てるよりは楽になるかと思います。
これあればいいじゃんってなりそうですが、お好みでどうぞ。

```swift
class SomeViewController: UIViewController {
    private var onceTokenViewWillAppear: dispatch_once_t = 0
    
    override func viewWillAppear(animated: Bool) {
        super.viewWillAppear(animated)
        
        dispatch_once(&onceTokenViewWillAppear) {
            print(self, "viewWillAppear")
        }
    }
    
    private var onceTokenViewDidAppear: dispatch_once_t = 0
    override func viewDidAppear(animated: Bool) {
        super.viewDidAppear(animated)
        
        dispatch_once(&onceTokenViewDidAppear) {
            print(self, "viewDidAppear")
        }
    }
}
```