LoginSignup
6
8

More than 3 years have passed since last update.

【Swift】[weak self]付のクロージャに親スコープの変数を渡したい!!!

Last updated at Posted at 2020-01-22

小ネタです。

UI更新処理ってメインスレッドでやらないといけないので、
DispatchQueue.main.async {[weak self] in …… }の中に書くじゃないですか?
このときに、関数内のスコープを持った変数をクロージャに渡したい、ということがありました。

class ViewController {
    func hoge() {
        var word = "これを渡したい"
        DispatchQueue.main.async {[weak self] in
            //どうしたらいい?
        }
    }
}

正解

先に正解を書くと、

class ViewController {
    func hoge() {
        var word = "これを渡したい"
        DispatchQueue.main.async {[weak self, word] in
            print(word) //->これを渡したい
        }
    }
}

で渡せました。
(この例だとself使ってませんが、実際のコードではselfにもアクセスする必要がありました)

というかそもそも、クロージャ内からwordにアクセス可能だったので、これでもいけます。

class ViewController {
    func hoge() {
        var word = "これを渡したい"
        DispatchQueue.main.async {[weak self] in
            print(word) //->これを渡したい
        }
    }
}

下の書き方がいいと思います。

ダメな例

以下、NG集。

DispatchQueue.main.async {[weak self] word in
DispatchQueue.main.async {[weak self], word in
DispatchQueue.main.async {[weak self](word) in
DispatchQueue.main.async {[weak self](w: String = word) in
DispatchQueue.main.async {[weak self](word = self.word) in

よくわかる解説

とりあえず動けばいいだけなら、ここまで読んでいただければOK。
ちょっとモヤった(循環参照的なやつ大丈夫なのか?とか)ので、NGな例がなぜNGなのか調べました。
DispatchQueueクラスのasyncメソッドの仕様と、Swiftのキャプチャリスト(Capture List)の知識が必要です。

asyncメソッドの仕様

DispatchQueue.main.async {[weak self](word) in

これを書くと、

Contextual closure type '@convention(block) () -> Void' expects 0 arguments, but 1 was used in closure body

こんな感じで怒られると思います。
エラ〜メッセージがちょっと難しくて、最初わかんなかったんですが、考えてみたら当然で、
async()の引数として要求しているクロージャは() -> Void型のクロージャで、引数はありません。

async()の定義
public func async(group: DispatchGroup? = nil, qos: DispatchQoS = .unspecified, flags: DispatchWorkItemFlags = [], execute work: @escaping @convention(block) () -> Void)

定義見ると引数ずらずらありますが、group/qos/flagsはデフォルト引数がそれぞれ指定されているので、
メインスレッド呼びたいだけの時は省略することが多いです。
最後のクロージャもトレイリングクロージャで引数名書かないので、普段使ってる書き方と定義が直感的に紐づかないですね。

キャプチャリストとは

クロージャは安易に使うと安易に循環参照を生みます。
それで[weak self]とかつけてるわけなんですが、実はこの[]がキャプチャリストです。
(僕はさっきはじめて知りました)

Resolving Strong Reference Cycles for Closures
(日本語版)2.16.5. クロージャによる強い参照の循環 | 自動参照カウント | Swift

クロージャの実行スコープは、親クラスとは別になるので、クロージャ内部で使う定数やら変数やらをキャプチャしてやる必要があります。

というかそもそもクロージャは呼び出されたスコープ内はキャプチャしてる

「クロージャに親スコープの変数渡したい時は、キャプチャリスト内に書いてね!」という結論にしようと思って記事を書いていたんですが、
調べてみたら、そもそもクロージャは呼び出されたスコープ内はキャプチャしてることが判明しました。

Capturing Values
2.7.3. 値のキャプチャ | クロージャ | Swift

まさかそのまま書けば使えるなんて思わず、色々な書き方を試してしまいました。。。😫

6
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
8