【Swift 3】関数内で非同期処理を行った後の値を返り値としたかった話【クロージャ】

どもども。Reoです。

もうずっと前から理解不能だったヤツをようやく理解できた気がするのでメモがてら書いていきます。

クロージャが使える方からすれば、ど定番の定石ってヤツなのかもしれないです。

 

今回は「とある関数内で非同期処理を行った後の値を返り値としたい場合」にどうすれば良いのかという話をしていきます。

 

例えば以下のコードを実行してみます。(Playground上で動かしています)

import UIKit

func getCat() -> String {

    var str = "初期値"
    
    //非同期処理を行う
    let dispatchQueue = DispatchQueue(label: "Dispatch Queue", attributes: [], target: nil)
    dispatchQueue.async {
        //適当にちょっと重たい処理
        for i in 0 ..< 1000{
            if(i == 999){
                str = "にゃんこ"
            }
        }
    }
    return str
}

//関数を使ってみる
print("getCat:\(getCat())")     //getCat:初期値

 

このgetCat()関数では、返り値に”にゃんこ”が欲しくて実行しますが、実際に返される値は”初期値”のままです。

 

この場合、非同期で行なっているdispatchQueue.async {  }ブロックの処理完了を待たずに返り値が返されてしまいます。

 

解決策は「クロージャを引数に使う!」という方法です。

 

結構前からこの問題に悩んでいて、何やらクロージャというものを使えばどうにかなるらしいけれど、そもそもクロージャってなんなの?って理解してませんでした。

クロージャの説明を調べて見てもなんかよくわかんないし、とりあえず以前までは返り値を扱うこと自体を諦めていました。そうしてその場しのぎコードが生まれていたんですが・・・。

最近その辺をちゃんと勉強しまして(前回の記事「「詳解Swift(第3版)」を読破したぞいっ」より)、クロージャってなんなのかようやくちょっと理解しました。

 

 

以下がクロージャを用い、先ほどの問題を解決したコードになります。

import UIKit

func getCat(_ after:@escaping (String) -> ()){
    var str = "初期値"
    
    //非同期処理を行う
    let dispatchQueue = DispatchQueue(label: "Dispatch Queue", attributes: [], target: nil)
    dispatchQueue.async {
        for i in 0 ..< 1000{
            if(i == 999){
                str = "にゃんこ"
            }
        }
        after(str)
    }
    print("先にこっちが呼ばれる\(str)")   //str = "初期値" のまま
}

//関数を使ってみる
getCat({ str in
    print("getCat:\(str)")         //getCat:にゃんこ
})

先ほどと同様に”にゃんこ”を得ることを目的とした関数になります。

 

これがまた分かりづらい〜〜〜んですよね。

まじでクロージャを理解する前(詳解Swiftを読む前)だと意味がわかりませんでした。

 

ざっくりとクロージャの説明

超ざっくりとしたクロージャの説明ですが、基本的には名前のない関数みたいなもんです。

引数と返り値を持つことができます。

例えば簡単なクロージャだと

let closure = { () -> () in
    print("クロージャ内")
}
closure() //これを実行して初めてクロージャ内のprintが行われる

() -> ()の部分は (引数) -> (返り値)を指定します。

上記の例に引数を設定してみると、

let closure = { (str:String) -> () in
    print("クロージャ内:\(str)")
}
closure("にゃーん")  //クロージャ内:にゃーん と表示
closure("わん")     //クロージャ内:わん と表示

返り値のみを設定すると、

let closure = { () -> (String) in
    let str = "もちもち"
    return str
}
print(closure())  //もちもち と表示

 

引数と返り値両方を設定すると

let closure = { (name:String) -> (String) in
    return "ペットの名前は" + name + "です。"
}
print(closure("もちこ"))    //ペットの名前はもちこです。 と表示

 

超簡単な例だと上記のような感じです。

その他にもクロージャには値をキャプチャしたり省略した書き方ができたりと、とにかく奥が深く説明すると長くなってしまうので、超簡単に説明させていただきました。

以下の記事も参考にすると良いかもしれないです〜

【Swift】クロージャの使い方。名前の無い関数を作る。(Swift 2.1、XCode 7.2、Android:無、iOS:有、興奮度:C)

 

 

非同期処理後の値を扱おう!

そして本題です。

 

先ほどの2つの関数を横並びで比較してみます。

まず第一の特徴として、関数の引数と返り値が異なります。

想定しているのはgetCat()を実行すると”にゃんこ”という文字列が得られるという挙動なので、非同期処理がなければ左側のgetCat()->String関数でおkということになります。

 

この左側の返り値の型をそのまま右側の引数のクロージャの引数にしています。

イメージ的には、getCat()を実行した返り値がクロージャの引数に返ってきたみたいな。

 

クロージャの型は (String) -> () です。Stringを引数に返り値はありません。

関数の中では、以下のように呼び出すことができます。

func getCat(_ after:@escaping (String) -> ()){
    after("何か文字列")
}

引数ラベルの名前は適当につけました。

 

気になるのはこの@escapingですよね。

これは、クロージャを関数の引数とする場合に必要となることがある修飾詞となります。

おもに「@escapingは関数の呼び出しが終了した後でもクロージャが使い続ける可能性がある場合に必要」となります。

まだ理解が浅いのでなんとも言えないんですが、コンパイラに怒られちゃうからつけてるっていうのが正直なところ。

 

このクロージャを非同期処理の最後に呼び出しています。

引数に本来返り値としたかった値を指定します。

    dispatchQueue.async {
        for i in 0 ..< 1000{
            if(i == 999){
                str = "にゃんこ"
            }
        }
        after(str)      //str = "にゃんこ" が引数
    }

関数内に、非同期処理のブロックの後に何か処理があれば、そちらも並列して実行されています。

 

あとはこの関数の呼び出し方です。

getCat({ str in
    print("getCat:\(str)")         //getCat:にゃんこ
})

もし、関数の引数ラベルがあった場合は

func getCat(after:@escaping (String) -> ()){ ... }

getCat(after: { str in
    print("getCat:\(str)")         //getCat:にゃんこ
})

といった書き方になります。

 

ちょっと省略した書き方になっていますが、丁寧に書くとこんな感じ

getCat(after: { (str:String) -> () in
    print("getCat:\(str)")
})

 

getCatの引数にクロージャを指定します。さらにクロージャを分けて書くと分かりやすいかもしれない。

let closure = { (str:String) -> () in
    print("getCat:\(str)")
}
getCat(after:closure)

4通りの書き方を書きましたが、どれでもおkです。

 

この書き方をしてようやく@escapingの「関数の呼び出しが終了した後でもクロージャが使い続ける可能性がある場合」というのを理解したのでちょっとだけ脱線。

let closure = { (str:String) -> () in
    print("getCat:\(str)")
}
getCat(after:closure)

//ここで別にclosureだけを実行することもできる(関数と関係ないところでクロージャを使うことができる)
closure("おもち")       //getCat:おもち と表示

 

クロージャは呼び出されるまで実行されないので、引数に設定した時点では評価されません。

なのでまずは普通に関数内部を上から順に実行します。

getCat({ str in   print(“getCat:\(str)”) })と書くと正直簡潔だけど慣れるまで本当わかりづらい・・・。

関数内部でクロージャを呼び出す部分に到着してようやくクロージャが呼び出されます。

 

これをうまく利用して、想定した返り値をゲットするというわけです。

説明がむずい!!!

 

ちなみにこのクロージャが呼ばれるタイミングは結構差があります。

流れをざっくり書いてみましたが、なんか余計わかりづらい気もする・・・

 

 

下のログを見ていただくとわかるんですが、getCat()の関数の処理が終わって、さらにその後に続くfor-in文の途中でようやく非同期処理が完了して、クロージャ内のログが表示されています。

何度か実行しなおしてみると、呼ばれるタイミングが違うときがあるんですね。

for-in文までたどり着く前にクロージャが呼ばれることもありますし、for-in文の途中で呼ばれることもあります。

 

色々頑張って図を作ったわりに、ログと流れの番号がグダグダだったりするのは大目に見ていただきたい・・・・。

 

なんだか空回った説明になってしまった気もしますが、この辺で。

GCD周りにはわりと毎回苦しめられるんですが、クロージャをうまく使いこなすことでどうにか慣れていきたいです。

 

脱初心者を目指していますが、至らぬ点もいっぱいあると思いますので、何か間違っているよーとかこうしたほうがいいよーとかあればぜひ教えていただければ嬉しいです。

 

 

少しはスキルアップしたと思いたいところだけれど、どこかで循環参照してるよ!とかにはまだなかなか気づかないし怖いよー

返り値はないって書いてるけど実際はVoidが返ってるよとか言われたらもうゴメンナサイって感じだよー

 

実際はこれAlamofireを扱う時に使いました。気づかないうちにクロージャに入ってて気づかないうちに非同期処理になってたなんてつい最近まで知らなかったよ><

2018/04/14追記
Swift4でも動作チェック済みです。

クロージャ本当に便利。死ぬほど便利。
コメントは認証制です。詳しくは下記の注意をお読みください。お気軽にコメントお願いします!

Write a Comment

コメント時の注意

「Twitter」「Facebook」「Google+」「WordPress」のいずれかのアカウントをお持ちの方は各アカウントと連携することでコメントできます。 コメントしたことはSNSに流れませんので、アカウントをお持ちの方はこちらの方法でコメントを投稿して下さると嬉しいです。 アカウントをお持ちでない方はメールアドレスで投稿することができます。 初回コメント時は承認後に表示されます。

Related Memo...

記事を書くほどでもないけれどメモっておきたいこと

テスト投稿。

例えばiphone7 の画面サイズ

750 × 1334
半分375 × 667

iOS

UINavigationController + UIScrollView の組み合わせで使っている時に謎の余白ができる時

UINavigationController + UIScrollView の組み合わせで使っていて、UIScrollView 上に AutoLayout で上下左右0で View を設置しているのに、30px程度上にずれてしまうとき。

`navigationController.navigationBar.isTranslucent = false` にすると直るかもしれない。

ScrollView上のコンテンツとNavigationBarの重なっているところが透過していたら多分これで直せるはず。

通常のターゲットではちゃんと動いているのに、iOSSnapshotTestCase を用いたテストでだけこの対応が必要なのよくわからないけれど。。。

iOS

UITableView.RowAnimation の .none はアニメーションするよ

UITableView.RowAnimation の .none はアニメーションがnoneなわけじゃなく、デフォルトの設定を使うよという意味らしい。

The inserted or deleted rows use the default animations.

なのでアニメーションしちゃう。今更の気づき。

 

iOS
more