【Swift】一度だけ処理を実行したい!を叶えるclosure

Sunday, September 25, 2016

インスタンス生成してから一度だけ処理を行いたい! viewWillAppear: で1回目だけ処理をしたい!
なんてときに、 Bool のフラグ変数持ってチェックしたり、 dispatch_once 使ったり色々な方法があるのですが、そもそもSwift3からは dispatch_once使えない ということもあるし、フラグ変数持つのも嫌だったので、じゃあ closure で解決してみようということで挑戦してみました。

その前に

https://swift.org/migration-guide/Dispatch の項に、dispatch_onceがなくなった経緯と、もし使いたい場合の方法が書いてあります。

  let myGlobal = { … global contains initialization in a call to a closure … }()
  _ = myGlobal  // using myGlobal will invoke the initialization code only the first time it is used.

たしかにこれでも良いかもしれないですが、 _ = myGlobal ってしているのが気持ち悪い…。
ということでもう少し何かいい案がないかと探していると、 先人の教え 的なものが見つかりました。
これを基に、より扱いやすい形でこのアプローチ自体を 関数化 することに。

関数を用意する

まず以下のような関数 execute_once を用意します。(グローバル領域に定義で大丈夫です。)

public typealias OnceClosure = (() -> Void)?
public func execute_once(_ execute: () -> Void) -> OnceClosure {
    execute()
    return { return nil }()
}

引数で受け取った closure を実行し、 (() -> Void)? を返します。
ただ、closureの即時実行にて return nil をしているので、実質的には nil を返しています。
ちなみに、この関数単体では、 一度だけ実行する を実現できません。なので、 lazy var と組み合わせて使います。

  • やや不可解な return { return nil }()

単純にreturn nilとしてしまうと、(() -> Void)?としてうまく返せずコンパイルエラーがでてしまうので、一度即時実行するclosureからreturn nilしてあげることで、(() -> Void)?型として nil を返すように包んであげています。

lazy var で一度だけ実行したい処理を定義する

この関数が用意できたら、以下のように class 内に lazy var で一度だけ実行したい処理を書いてあげます。

class ViewController: UIViewController {
    private var dataSource = DataSource()
    lazy var onceReloader: OnceClosure = execute_once {
        print("execute once")
        self.dataSource.reload()
    }
}

この例では、 viewWillAppear: で一度だけ(ViewController生成後初回表示時)リロード処理をしたい!というのを想定しています。
closure内に処理を書くだけですね。

使ってみる

実際に使ってみます。

class ViewController: UIViewController {
    private var dataSource = DataSource()
    lazy var onceReloader: OnceClosure = execute_once {
        print("execute once")
        self.dataSource.reload()
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        onceReloader?()   
    }
}

呼び出す時は、lazy varで宣言した変数を、 変数名?() として書いてあげるだけです。
ここで、 execute_once 関数の返す型を (() -> Void)? としたのが活きてきます。
一番最初に出した、migration_guideで出されていた案と比べると、

_ = myGlobal

onceReloader?()

となります。後者の方が関数っぽく見えるので、ただの変数と間違えないで済むんじゃないかと思います。
また、?が付くように Optional にしたのは、なんとなく一度だけ実行するような雰囲気が出せると思い、参考にした記事をそのまま踏襲しました。

せっかくなので

フレームワーク化して使えるようにしてみました。よかったら使ってください…!

参考

先人の教え がなかったら厳しかったです。ありがとうございます。
こちらで述べられていたものをちょっと良くした程度といえばそこまでですが、納得できる形まで持っていけたのでよかったかなと思います。
参考にした記事のものと、こちらの記事のものと見比べてもらえればと思います。


techSwiftSwift3.0

もようがえ

[Swift]関数の返り値を使わない時は