BETA

後輩に送るなんちゃって非同期(block)解説

投稿日:2019-10-24
最終更新:2019-10-24

前置き: ある日のslackにて

(iOSアプリでdismissとpresent連続処理する時に思うように行かない、という発言を受けて)

自分:
突然だけど思いついたので非同期処理クイズ
次のコードの処理はどの順番に処理されますか?

処理1  
dismiss(animated: true, completion: { [weak self] in  
    処理2  
})  
処理3  

後輩:
1→2→3!!!


というわけで後輩に捧げます。
間違ってること言ってたらごめんな(先回り謝罪)

そもそもこれは何をやっているのか

iOSの UIViewControllerdismiss のそもそもの定義はこちらです。
(公式リファレンス)より

func dismiss(animated flag: Bool, completion: (() -> Void)? = nil)  

問題の completion、これは (() -> Void)?) という型の block です。一般的にはコールバックと呼ぶと通じる人が多いかもしれません。

コールバック(英: Callback)とは、プログラミングにおいて、他のコードの引数として渡されるサブルーチンである。(wiki冒頭より引用)

はい、これで理解できるならつまづく人いないわってやつですね。

まずそもそも completion: (() -> Void)? = nil) は、

「`completion` という引数に `(() -> Void)? ` というblockを渡してください、  
指定しない場合は `nil` です。」  

という意味です。
block関数と読み替えても問題ありません。
Intの引数に整数を渡すのと同じように、
(() -> Void)?の引数には関数を渡す必要があります。

() -> Void の部分が関数の詳細を示しています。左側が引数、右側が戻り値です。
この場合は、左側が()なので引数なし、右側がVoid なので戻り値もなし。つまり、

func action()  

こういう処理を要求しています。
Int には整数しか渡せないように、()->Voidには「引数戻り値なしの関数」しか渡せません。そして、後ろの?はオプショナルなので、(() -> Void)?は「引数戻り値なしの関数かnil」を要求している、ということになります。

処理1  
dismiss(animated: true, completion: { [weak self] in  
    処理2  
})  
処理3  

この{ [weak self] in 処理2}は、名前をつけていない関数であり、それをSwiftではblockと呼びます。(この{}などで名前のつけない関数を渡す、という考え方自体は他の言語でも一般的です。)
ですから、これは以下のように書き換えることもできます。

func action() {  
    処理2  
}  

func start() {  
    処理1  
    dismiss(animated: true, completion:action)  
    処理3  
}  

そして、どう渡したにせよ、completionとして渡した関数は、dismissの中で呼び出すことができます。

// 実際のコードを確認することはできないのでイメージでお送りしております  
func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {  
    // なんかきっとここで画面を閉じる処理  

    completion?()  
}  

このcompletion?()のタイミングで、blockや渡した関数の中身の処理が動作します。

で、結局処理の順番はどうなるのか?

AppleのAPIで説明すると謎が多いので、同じ型の関数を自前で実装してみて詳細を説明します。

import Foundation  

protocol CallbackSample {  
    func action(flag: Bool, completion: (() -> Void)?)  
}  

print("processing1")  
actionClass.action(flag: true) {  
    print("processing2")  
}  
print("processing3")  

dismissと同じような関数を定義したprotocolを作ってみました。
ちなみにこれだけだとactionClassを作ってないのでエラーです。
さて、CallbackSampleはどんな動きをするのか?
答えは「どう実装しているかによる」です。 というわけで以下の二つの実装を比べてみてください。

class CallbackSampleImpl1: CallbackSample {  
    func action(flag: Bool, completion: (() -> Void)?) {  
        completion?()  
    }  
}  

class CallbackSampleImpl2: CallbackSample {  
    var function: (() -> Void)?  

    func action(flag: Bool, completion: (() -> Void)?) {  
        function = completion  
        Timer.scheduledTimer(  
            timeInterval: 1.0,  
            target: self,  
            selector: #selector(delayedAction),  
            userInfo: nil,  
            repeats: false  
        )  
    }  

    @objc func delayedAction() {  
        function?()  
    }  
}  

どちらもCallbackSample を適用しています。(なお引数flagは今のところ使ってないです)
しかし中身は全く違っていて、CallbackSampleImpl1は即座に渡されたblockを呼び出しています。
一方、CallbackSampleImpl2は一度変数に保持し、タイマーを使って1秒後にblockを呼び出します。

繰り返しますが、どちらもメソッドの定義はfunc action(flag: Bool, completion: (() -> Void)?)で同じです。
しかし、どちらのクラスを利用するかで、当然ログの出方は変わってきます。

let actionClass: CallbackSample = CallbackSampleImpl1()  
print("processing1")  
actionClass.action(flag: true) {  
    print("processing2")  
}  
print("processing3")  

// ログはこうなる  
// processing1  
// processing2  
// processing3  

--

let actionClass: CallbackSample = CallbackSampleImpl2()  
print("processing1")  
actionClass.action(flag: true) {  
    print("processing2")  
}  
print("processing3")  

// ログはこうなる  
// processing1  
// processing3  
// processing2  

ですから、前置きで出したクイズ、次のコードの処理はどの順番に処理されますか?に対する答えは、
1->2->3 か 1->3->2 で、どちらかなのかは実装次第となります。

基本的に、block処理が出てきたら上記のように、この中の処理はいつ呼ばれるかわからないと考えましょう。
その上で、いつ呼ばれても問題ないようにするのが基本です。
ただし、AppleのAPIなのでリファレンス等にどのようなタイミングで呼び出されるのかが明記されているような場合は、それを前提に実装してもいいと考えています。
例えば今回のdismissはリファレンスを読むと、Parameterscompletionの部分に、

The block to execute after the view controller is dismissed.

とありますから、必ず画面遷移が終わったあとに来る、という前提で処理を書いても問題ないです。

後輩「2を処理してから dismiss であってますか!!!」

合ってません!ここまで読んだ今なら「なぜか」がわかるのではないでしょうか!
そう、渡された関数がいつ呼び出されるか次第なのです。
blockがすぐ呼び出されたら処理2の方が早いし、処理が終わったあとにblockが呼び出されるならdismissしてから処理2となります。

技術ブログをはじめよう Qrunch(クランチ)は、プログラマの技術アプトプットに特化したブログサービスです
駆け出しエンジニアからエキスパートまで全ての方々のアウトプットを歓迎しております!
or 外部アカウントで 登録 / ログイン する
クランチについてもっと詳しく

この記事が掲載されているブログ

日々のつまづきや失敗、発見を記録して生きたいのでござるよ。

よく一緒に読まれる記事

0件のコメント

ブログ開設 or ログイン してコメントを送ってみよう
目次をみる
技術ブログをはじめよう Qrunch(クランチ)は、プログラマの技術アプトプットに特化したブログサービスです
or 外部アカウントではじめる
10秒で技術ブログが作れます!