本日は、ひと通り見終えた感じの個人的にお気に入りな技術ブログ「その Swift コード、こう書き換えてみないか」の余興的に続く事項の最後のところ、Delegate
や NotificationCenter
を Swift Concurrency で扱うまわりを見ていく回にしますね。よろしくお願いします。
—————————————————————————————————————————————————— 熊谷さんのやさしい Swift 勉強会 #280
00:00 開始 00:54 今回は Delegate パターンを Swift Concurrency に置き換える話 01:11 ところで、クロージャーパターンという言葉はある? 02:46 JavaScript でのクロージャーの書き方 03:58 Swift でクロージャを書いてみる 07:03 それで、クロージャーパターンとは? 09:07 デザインパターンってなんだろう 12:51 Delegate パターンを Concurrency に書き換える 13:50 CheckedContinuation は、スレッドセーフ 15:53 Swift Concurrency は非同期処理の手法のひとつ 17:21 呼び戻す先を async 関数に書き換えてみる 21:45 CheckedContinuation は、構造体 22:59 CheckedContinuation 破棄の必要性は? 26:30 同じ async メソッドが複数呼び出される可能性 ——————————————————————————————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #280
では始めていきますね。今日も引き続き、田中亮香さんのSwiftコードを改良する提案についてのブログを見終えました。このブログのテーマとして、UIキット周りのお話もあるのですが、これは別の方が書いたもので、後で見る予定です。
前回はエリゲートパターンやクロージャーパターンをSwiftコンカレンシーで扱うという話の中で、クロージャーパターンを同期関数に変える部分について見ましたね。振り返りますが、クロージャーパターンという言葉はあまり馴染みがなかったので調べてみました。
クロージャーデザインパターンは、JavaScriptの分野で紹介されているようです。具体的には、GroovyのコードをJavaScriptに書き換えた記事がありました。JavaScript 2015で導入されたconst
とアロー関数を使ったタイル(スタイル)に取り組んだ内容です。要はクロージャーですね。const
は違いますが、JavaScriptでlet
と同じようなものです。
クロージャーパターンの記事に出てくるコードですが、パラメータを受け取る関数が書かれており、JavaScriptでは動的型付けの言語なので、具体的にどんな型が来るのかは明示していません。アロー関数を使ってクロージャーのボディを書くことで、戻り値も動的に決めることができます。
具体的な例をJavaScriptのコードとして示しますと、
const exampleFunction = (param) => {
// クロージャーの処理
};
という感じになります。
これをSwiftに置き換える場合、例えば、
let exampleFunction: (Int) -> Void = { param in
// クロージャーの処理
}
といった形で書けます。これにより、クロージャーを変数に代入し、その変数からクロージャーを実行することができます。
要するに、クロージャーデザインパターンは、関数型の引数を受け取り、それを実行するというものです。これを整理して、Swiftでも用います。
ブログの内容に戻りますと、リストやサンプルコードが書かれていて、それをそのまま書き換えて使う形になります。具体的には、クロージャーを受け取って実行する典型的なパターンです。
このクロージャーデザインパターンを利用することで、より柔軟でカスタマイズ可能なプログラムを作成できるようになります。他のデザインパターン、例えばインデペンデントイテレーター(Iterator)パターンなども、同様の発想でクロージャーを使うことがあります。
クロージャーパターンを理解することで、Swiftの書き方や応用例が広がるはずですので、引き続き勉強を進めましょう。 ダイナミカルコンディショナルエクセキューションプロジェクトを何回か行っている中で、テンプレートメソッドパターンについて話しました。具体的には、クロージャーを受け取って処理するものです。これはテンプレートメソッドパターンと類似しており、マップなどの関数がその例に当たるかと思います。
アンドロイドチームの勉強会でもデザインパターンについて話していますが、個人的にはデザインパターンにそれほど強い関心を持たずにプログラムしてきたため、よくわからない部分もあります。多くのパターンが出てきても、同じことをしているように見えることが多いです。それに対して違和感はないのですが、デザインパターンの区別をしっかりつけてプログラムを組むのが重要だと感じる場面もあります。
例えば、アダプターパターンやファクトリーメソッド、テンプレートメソッドなどの話題が出ても、一見同じように見えることがあります。ただし、クロージャーの使い方やカスタマイズの方法について考えると、これらのパターンが具体的にどのように異なるのかを理解することで、より理論に基づいた設計ができるようになります。
クロージャパターンについては、2015年の時点で既に研究されて広まっていました。今では常識となっていますね。2015年というと、Swiftは既に登場していた時期になります。私はObjective-Cの時代からiOSをやっていたので、Objective-C 2.0のプロパティやブロックス(Blocks)が登場した時期のことも思い出されます。
さて、前回の話ではデリゲートパターンとエーシングな感想(非同期処理に関するアプローチ)について触れました。今回も基本的な流れは同じですが、デリゲートパターンをブリッジのような方法で非同期処理に結びつける話です。完全にブリッジではないかもしれませんが、そのように考えるとわかりやすいです。
エーシングの感想では、CheckedContinuation
を使って非同期処理の状態を管理します。処理が終了した後にリジュームすることで、中断された非同期処理を再開します。このプロセスをデリゲートに任せる前に、CheckedContinuation
を呼び、デリゲートからの応答が返ってきた時に処理を再開します。
デリゲートパターンと非同期処理(コンカレンシー)の共存は今後も続いていくでしょう。全ての処理が非同期で行えるわけではなく、コールバックなども依然として必要な場面があります。そのため、CheckedContinuation
のようなツールを使って共存していくことが求められます。 デリゲートパターンもよく使われます。デリゲートパターンは普通に使われるもので、オブジェクト指向プラスアルファのレベルなら全然普通の仕組みです。イベントのハンドリングに関する話ですね。これについても特に新しいことではないですが、使われる頻度は減っていくかもしれません。
これを非同期関数にする方法を見ていきます。例えば Pet
クラスというモデルがあって、このクラスは NSObject
を継承しています。ただし、NSObject
を継承する必要があるかどうかは状況によります。このクラスには「残高」を取得するメソッドがあります。残高を取得する際にエラーが発生することもあります。
このメソッド内で withCheckedThrowingContinuation
を呼び出します。この関数は非同期関数の中でよく出てくるものです。エラーハンドリングをするかどうかを選択できます。例えば、次のように書きます。
withCheckedThrowingContinuation { continuation in
// デリゲートを発火させるコード
}
このとき、「デリゲートを発火させる」というのは、デリゲートのメソッドをコールバックして処理を続けることを指します。そのとき、continuation
をデリゲートメソッド内で使う必要があるため、Pet
クラス内に activeContinuation
という形で保存しておくことができます。この activeContinuation
はオプショナル型にし、非同期処理中かどうかを判断できるようにします。また、外部から触れないように private
にしておきます。
例えば、NFCタブリーダーセッションの処理を考えてみると、以下のようなコードになります。
initiateNFCTagReaderSession { [weak self] balance, error in
guard let self = self else { return }
if let error = error {
self.activeContinuation?.resume(throwing: error)
} else if let balance = balance {
self.activeContinuation?.resume(returning: balance)
}
self.activeContinuation = nil
}
このようにして、セッションを作成し、デリゲートからのコールバックを待っている間、インスタンスを保持しておきます。デリゲートメソッドの中で continuation
を使い、例えばエラーが返ってきたときには次のようにします。
delegateMethod { result in
switch result {
case .success(let balance):
self.activeContinuation?.resume(returning: balance)
case .failure(let error):
self.activeContinuation?.resume(throwing: error)
}
self.activeContinuation = nil
self.session = nil
}
セッションや continuation
を適切にクリーンアップすることが重要です。特に、continuation
を解放しないとメモリリークの原因となります。
また、デリゲートと completionHandler
を比較してみると、デリゲートの場合、値を取得したもののそれを元の処理に渡すのを忘れてしまってもコンパイルエラーにはならず、気づきにくいことがあります。そのため、async/await
を使った非同期処理は、より安全で分かりやすいコードを書くのに役立ちます。
これで Pet
クラスの readBalance
メソッドを非同期メソッドとして呼び出すことができるようになります。以上が今回の説明です。 自分はデリゲートメソッドの綴りを間違えていて、そもそも呼ばれないという間違いのほうが多い気がします。デリゲートで答えをもらったら大体使うじゃないですか。使い忘れるっていうのもなかなかないかなという気がします。もちろん、コンパイルエラーにはならないので、逆にコンパイラがそれを使っていないと言えないんですよね。そういう面でも今回の練習はよくできています。戻り値を返したら、それを普通使いますよね。使わなかったらエラーを出せるもんね。確かに、その心配が今回の練習で少なくなると思います。
アプリが用意したフレームワークの多くはObjective-Cとの互換性を持ちつつ、非同期処理(エーシンク)に対応していると思いますが、自分で非同期に対応する必要があるパターンもあります。破棄を忘れずに、保持したコンティネーションの初期化を忘れないようにしましょう。保護されているから外から勝手に扱われることはないのですが、エーシンクメソッドが同時に呼ばれたときにデータレースが発生することがあるかもしれません。リードバランスというメソッドが呼ばれている最中に、もう一回リードバランスが呼ばれるのを防ぐのにはアクターを使います。同時に呼ばれるとセッションがどうなっているかによりますが、結構やばいです。
具体的な例で試してみるとしましょう。例えば、以下のようなクラスを試してみます。
class Sample {
var value: Int = 0
func doSomething() async {
value += 1
await Task.sleep(for: .seconds(1))
print("value: \\(value)")
}
}
これで、doSomething
メソッドを非同期で実行し、1秒待ってから値をプリントするようにします。
次にタスクを作成して実行してみましょう。
let object = Sample()
Task {
await object.doSomething()
}
Task {
await object.doSomething()
}
このようにすると、タスクが2回実行される間にvalue
が2回変更されますが、それがどうなるかを見てみましょう。要は、リードバランスを同時に呼ぶときに応答が入る前にもう一回呼ばれる場合、コンティネーションが変わってしまう可能性があるため注意が必要です。
コンティネーションとセッションはちゃんと後片付けをしたほうが良さそうですね。その辺りはスレッドを統一するなどの工夫が必要かもしれません。
今日はここまでにして終わりにしましょう。次回はノーティフィケーションセンターについて見ていくことにしましょう。それでは、お疲れさまでした。ありがとうございました。