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 の引数として受け取った nowProvider を self.nowProvider に代入しています。nowProvider は init の中で実行されるのではなく、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 が必要な直接の理由は「保存してあとで使うから」です。
