🪶

Swift @escaping

2024/07/24に公開

What is @escaping

Escaping Closures

クロージャが関数の引数として渡され、関数が返った後に呼び出されるとき、クロージャは関数をエスケープすると言われます。クロージャを引数のひとつとする関数を宣言するとき、クロージャがエスケープできることを示すために、引数の型の前に@escapingと書くことができます。

クロージャがエスケープされる1つの方法は、関数の外部で定義された変数に格納されることです。例えば、非同期処理を開始する関数の多くは、完了ハンドラとしてクロージャ引数を取ります。関数は処理を開始すると戻りますが、処理が完了するまでクロージャは呼び出されません。例えば

var completionHandlers: [() -> Void] = []
func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) {
    completionHandlers.append(completionHandler)
}

someFunctionWithEscapingClosure(_:)関数はクロージャを引数にとり、関数の外で宣言された配列に追加します。この関数のパラメータを@escapingでマークしなければ、コンパイル時にエラーが発生する。

selfを参照するエスケーピング・クロージャは、selfがクラスのインスタンスを参照している場合、特別な配慮が必要です。selfをエスケーピング・クロージャーに取り込むと、誤って強い参照サイクルを作りやすくなります。参照サイクルについては、自動参照カウントを参照してください。

通常、クロージャは変数をクロージャ本体で使うことで暗黙的に変数をキャプチャしますが、この場合は明示的に行う必要があります。selfをキャプチャしたい場合は、selfを使うときに明示的に書くか、クロージャのキャプチャリストにselfを含める。selfを明示的に書くことで、自分の意図を伝えることができ、参照サイクルがないことを確認することができます。例えば、以下のコードでは、someFunctionWithEscapingClosure(:)に渡されたクロージャは明示的にselfを参照している。対照的に、someFunctionWithNonescapingClosure(:)に渡されたクロージャはノンエスケープ・クロージャであり、暗黙的にselfを参照することができる。

func someFunctionWithNonescapingClosure(closure: () -> Void) {
    closure()
}


class SomeClass {
    var x = 10
    func doSomething() {
        someFunctionWithEscapingClosure { self.x = 100 }
        someFunctionWithNonescapingClosure { x = 200 }
    }
}


let instance = SomeClass()
instance.doSomething()
print(instance.x)
// Prints "200"


completionHandlers.first?()
print(instance.x)
// Prints "100"

SwiftUIでどう使うのか?

ボタンのコンポーネントを作ったときに、処理を書く部分のクロージャーのプロパティを定義する部分で使いました。

import SwiftUI

struct CustomButton: View {
    var iconName: String
    var title: String
    var action: () -> Void
    var backgroundColor: Color
    var foregroundColor: Color
    var width: CGFloat
    var height: CGFloat
    
    init(iconName: String, 
         title: String, 
         backgroundColor: Color = .white, 
         foregroundColor: Color = .black, 
         width: CGFloat = 300, 
         height: CGFloat = 52, 
         action: @escaping () -> Void) {
        self.iconName = iconName
        self.title = title
        self.action = action
        self.backgroundColor = backgroundColor
        self.foregroundColor = foregroundColor
        self.width = width
        self.height = height
    }

    var body: some View {
        Button(action: action) {
            HStack {
                Image(systemName: iconName)
                    .frame(width: 24, alignment: .leading)
                Text(title)
                    .frame(maxWidth: .infinity, alignment: .leading)
                Spacer()
            }
            .padding(.leading, 10)
            .foregroundColor(foregroundColor)
            .font(.system(size: 16, weight: .semibold))
            .frame(width: width, height: height, alignment: .leading)
            .background(backgroundColor)
            .cornerRadius(15)
        }
    }
}

使用例

Bookマークボタンを作るのに使いました。左にアイコン、右にタイトルがあります。{}の中に、処理を書くことができます。

CustomButton(
    iconName: "bookmark.fill",
    title: "お気に入り",
    backgroundColor: .blue,
    foregroundColor: .white,
    width: 157,
    height: 64
) {
    print("お気に入りに追加されました")
}

まとめ

Flutterで似たようなものをボタンのコンポーネントで作ったことがありました。そのときは、VoidCallbackというデータ型を使っていましたね。voidだから戻り値がない。コンポーネントの時だと、何も処理書かないので、使うまでは、エスケープ「逃亡・脱出」してくださいとのことみたいです。
使うまでは、何もないってことですね。

Discussion