Swiftにおける「@escaping」について

朝の実(Asanomi)アイコン アプリ開発

Swiftのイニシャライザ(init())や関数の引数に @escaping が付いていることがあります。

init(nowProvider: @escaping () -> Date)

これは、関数や init に渡されたクロージャが、その場で使い切られず、呼び出しが終わったあとも保持される可能性があることを示す指定です。

この記事では、@escaping を「クロージャの寿命がどこまで続くか」という観点で整理します。

題材として、現在私が開発中の朝専用SNS「Asanomi」の PostViewModel で実際に使っている provider クロージャの形を取り上げます。

補足

ここでいう provider は、必要なタイミングで値を返す処理です。たとえば nowProvider なら、現在時刻を直接持つのではなく、現在時刻を取得する処理を渡しています。

近い言葉として callback もあります。callback は、何かが起きたときに呼んでもらう処理です。たとえば「下書きが変わったら親画面へ知らせる」「保存が終わったら画面を更新する」といった用途で使います。

クロージャは値として渡せる処理

まずは前提知識として、Swiftのクロージャについて説明します。
クロージャは以下の () -> Date = { Date() } の部分です。

let nowProvider: () -> Date = {
    Date() 
}
let now = nowProvider()

クロージャは、処理そのものを変数に代入するなど、として扱えます。

nowProvider が持っているのは Date そのものではなく、Date を返す処理です。型の () -> Date は「引数なしで Date を返す関数」を表します。

Javaで近いものを挙げるなら、Supplier<Date> のような役割です。

Supplier<Date> nowProvider = () -> new Date();
Date now = nowProvider.get();

ここだけを見ると、普通にメソッドを定義するのと大きく変わらないように見えます。

func now() -> Date {
    Date()
}

どちらも、呼び出せば Date を返す処理です。

違いは、クロージャにすると処理そのものをとして扱えることです。値として扱えるので、変数に入れたり、引数として渡したり、プロパティに保存したりできます。

Asanomi のように投稿時間の判定をテストしたい場合、ViewModel の中で Date() を直接呼ぶと、テスト実行時の現在時刻に引きずられます。一方で nowProvider として外から渡せるようにしておくと、テストでは「8:59を返す処理」「9:00を返す処理」を渡せます。

PostViewModel(
    repository: repositorySpy,
    nowProvider: { makeDate(hour: 8, minute: 59) }
)

つまり、クロージャを値として持つメリットは、処理をその場で書けることだけではありません。どの処理を使うかを外から差し替えられることにあります。

値を直接渡す代わりに「必要なタイミングで値を作る処理」を渡す設計にできます。現在時刻、ログイン中ユーザー、設定値、外部サービスへのアクセスなど、実行タイミングや環境に依存するものを差し替えたいときに便利です。

その場で使い切るクロージャには @escaping は不要

まず、@escaping が不要な形から見ます。

func printDate(_ provider: () -> Date) {
    print(provider())
}

この provider は、printDate の実行中に呼ばれて終わります。関数の外へ保存されるわけではなく、関数が戻ったあとに呼ばれることもありません。

このようなクロージャは、関数のスコープ内で完結しています。そのため @escaping は不要です。

不要な場面でも @escaping は付けられるのか

ここで気になるのは、関数や init の外で使わないクロージャに、あえて @escaping を付けられるのかという点です。

結論から言うと、クロージャ引数であれば付けること自体はできます。

func printDate(_ provider: @escaping () -> Date) {
    print(provider())
}

この例では、provider をプロパティに保存していません。非同期処理に渡してもいません。関数の中で呼んで終わりです。

それでも @escaping を付けたコードは、Swiftの構文としては成立します。

ただし、成立することと、付けるべきことは別です。

プロパティに保存するクロージャには @escaping が必要

一方で、受け取ったクロージャをプロパティへ保存する場合は @escaping が必要になります。

final class PostViewModel {
    private let nowProvider: () -> Date

    init(nowProvider: @escaping () -> Date) {
        self.nowProvider = nowProvider
    }

    func save() {
        let createdAt = nowProvider()
        // 投稿保存処理で createdAt を使う
    }
}

この例では、init の引数として受け取った nowProviderself.nowProvider に代入しています。nowProviderinit の中で実行されるのではなく、ViewModel のプロパティとして保持され、あとから save() の中で呼ばれます。

つまり、このクロージャの寿命は init の実行範囲を超えています。

Swiftでは、このように関数や init の外へ持ち出されるクロージャを escaping closure として扱います。そのため、引数側に @escaping を付けます。

init(nowProvider: @escaping () -> Date)

@escaping は「このクロージャをあとで使う可能性があります」という、呼び出し側と実装側の両方に向けた明示です。

Asanomi の PostViewModel での使い方

Asanomi の PostViewModel では、投稿保存時に必要な値を provider として受け取る形にしていました。

init(
    repository: PostSaveRepository,
    nowProvider: @escaping () -> Date,
    authorIdProvider: @escaping () -> String,
    authorProfileProvider: @escaping () -> AuthorProfile,
    clientTimeZoneProvider: @escaping () -> TimeZone
) {
    self.repository = repository
    self.nowProvider = nowProvider
    self.authorIdProvider = authorIdProvider
    self.authorProfileProvider = authorProfileProvider
    self.clientTimeZoneProvider = clientTimeZoneProvider
}

ここで渡しているのは、現在時刻、投稿者ID、投稿者プロフィール、タイムゾーンといった値そのものではありません。それらを必要なタイミングで取得するための処理です。

ViewModel は、これらの provider を init の中で一度だけ呼ぶわけではありません。プロパティとして保持し、投稿保存処理の中で呼びます。したがって、それぞれの provider は init の外でも生きる必要があります。

このため @escaping が必要になります。

重要なのは、@escaping が必要な理由を「テストで差し替えるため」と混同しないことです。テストで固定値を返す provider に差し替えられるのは、この設計の利点です。しかし @escaping が必要になる直接の理由は、受け取ったクロージャをプロパティへ保存して init 後に使うからです。

DIとの関係

provider を外から渡す形は、DIの考え方と相性が良いです。

ViewModel の中で直接 Date()TimeZone.current を呼ぶと、そのViewModelは実行時の環境に結びつきます。実装としては短くなりますが、テスト時に時刻やタイムゾーンを固定しづらくなります。

PostViewModel(
    repository: repository,
    nowProvider: { Date() },
    authorIdProvider: { currentUser.id },
    authorProfileProvider: { currentProfile },
    clientTimeZoneProvider: { .current }
)

このように外から provider を渡すと、本番では現在の値を返し、テストでは固定値を返すようにできます。

let fixedDate = Date(timeIntervalSince1970: 0)
let viewModel = PostViewModel(
    repository: repositorySpy,
    nowProvider: { fixedDate },
    authorIdProvider: { "test-user" },
    authorProfileProvider: { testProfile },
    clientTimeZoneProvider: { TimeZone(secondsFromGMT: 0)! }
)

ただし、provider を受け取ってすぐ実行するのではなく、ViewModel のプロパティに保存してあとで使うなら、その provider は escaping closure になります。

まとめ

@escaping は、受け取ったクロージャを関数や init の外でも使うことを示す指定です。中で呼んで終わるなら不要で、プロパティに保存したり、非同期処理のあとで呼んだりするなら必要になります。

@escaping が必要な直接の理由は「保存してあとで使うから」です。

sho shimizu

SwiftUI を用いた iOS アプリの個人開発を行っています。
これまでに2本のアプリを App Store にリリースしました。

現在は、朝専用SNS「朝の実(Asanomi)」を開発中です。
アプリを作る中で学んだことや、設計・実装で考えたことを、
あとから振り返れるように技術記事としてまとめています。

sho shimizuをフォローする
アプリ開発技術解説朝の実未分類
シェアする
sho shimizuをフォローする