【2023年版】数あるSwiftの非同期処理の実現方法をきちんと理解した上で使い分けよう
これは Qiita iOS Advent Calendar 2023 の13日目の記事です。
- 概観
- GCDを用いた非同期処理
- Operation、OperationQueueクラスを用いた非同期処理
- Threadクラスを用いた非同期処理
- [TBD] Combineを用いた非同期処理
- Swift Concurrencyを用いた非同期処理
- まとめ
概観
背景
この記事の目的一言で表すと、まさにタイトルの通り、この記事だけで『数あるSwiftの非同期処理の実現方法を、きちんと理解した上で使い分けられるようになる』ということになります。
モバイルアプリに限らず、開発者であれば誰でも触れ続けることが約束されている非同期処理。Swiftに関して言うと GCD、OperationQueue、Thread など、昔から様々な関連クラスがあった上に、iOS13から追加された Combine によってリアクティブな記述が可能になったり、Swift5.5から登場した Swift Concurrency によってさらに複雑さを増しました。
困った時のためのHow to記事は世の中に溢れていますが、「なぜそう書く?」というところまで突き詰められているものが少なかったり、昔からあるAPIとSwift Concurrencyを比較してくれているものが無かったりということに気が付きました。
ちょうど社内で勉強会をしたという背景もあり、今一度あらためて全てを洗ってみよう、そして同じようなことで困っているiOSエンジニアの皆さんの力になりたいというのがこの記事になっています。
例によってマサカリやご意見は大歓迎です。皆さんのお力も借りて、よりよい記事にしていければと思っております。
こんな人へ向けた記事
- Xcodeに怒られたからDispatchQueue.main.async {} を脳死で書き足した経験がある
- SwiftのコアライブラリにThreadクラスがあることをそもそも知らない
- Swift Concurrency にあまりついていけていない
- iOS開発で 非同期処理全般を理解せずに感覚で書いている
この記事で扱う非同期処理の実現方法
以下の5つについて扱います。
- GCDを用いた非同期処理
- Operation、OperationQueueクラスを用いた非同期処理
- Threadクラスを用いた非同期処理
- Combineを用いた非同期処理
- Swift Concurrency を用いた非同期処理
PromiseKitやRxSwiftなどの高名なサードパーティライブラリを用いた方法もありますが、2023年末現在ではトレンドとしてはすでにかなり下火のようなので、本記事では扱いません。
早速見ていきましょう。
GCDを用いた非同期処理
GCDとは
GCDとは、Grand Central Dispatch の略で、非同期処理を行うための低レベルAPI群です。iOS4.0、Mac OS X 10.6から導入された、C言語ベースのシステムレベルの技術であり、コアライブラリの libdispatch に実装されています。
特徴として、スレッド管理を全く意識せずに非同期処理を実現できる ということが挙げられます。タスクをキューに追加しておくと スレッドプール によって処理が随時実行されるという簡単な仕組みになっており、プログラマはスレッド自体は一切操作することなく、タスクをキューに追加するだけで非同期処理を実現できます。スレッドの管理はシステムが行い、CPUのコア数や負荷の状況などを考慮して自動で最適化を行ってくれます。
スレッドプールとは、非同期処理を実現するためデザインパターンの1つです。あらかじめ用意しておいたスレッドを使い回すことで、スレッドを頻繁に生成・破棄することによるオーバーヘッドを回避できます。
DispatchQueue
DispatchQueueとは、GCDにおけるキューの呼称です。よく見るあいつはGCDにおける機能群の一つだったんですね。
DispatchQueueへのタスク引き渡し
DispatchQueueには2種類のタスクの引き渡し方式があります。
並列(Concurrent)方式
- (CPUに余力があれば) 現在実行中の処理の終了を待たずに、次の処理を並列して実行してくれます
- 処理の順番は保証されませんが、全体的に早く処理されます
直列(Serial)方式
- 現在実行中の処理の終了を待ってから、次の処理を実行します
- 常に1つずつ順番通り処理されるので、順番が重要なタスクはこちらを用いるべきです
DispatchQueueの優先度
DispatchQueueには、キュー自体に実行優先度を指定できる仕組みがあります。
この用途に応じたタスクの実行優先度を分類して表したものを QoS (Quality as Service) と呼びます。
分類 | 概要 |
---|---|
userInteractive | ユーザからの入力に対して即座に反映させるべき処理に用いられる 例) アニメーションの実行など |
userInitiate | ユーザからの入力を受けて実行される一般的な処理に用いられる 例) ボタンがタップされた時の内部処理など |
default | userInitiate と utility の間の優先度で、QoSを指定しない場合に利用される ユーザによって明示的に指定されるべきではない |
utility | 視覚的な情報の更新を伴いながらも、即時の結果を要求しない処理に用いられる 例) プログレスバー付きのダウンロードなど |
background | バックグラウンドで行われて、数分から数時間かかっても問題ない処理に用いられる 例) データのバックアップなど |
DispatchQueueの分類
DispatchQueueには以下の3種類の分類があります。
分類 | 個数 | 生成方法 | タスク引渡し | 概要 |
---|---|---|---|---|
Main | 1つ | デフォでOSが用意 | 直列方式 | ・DispatchQueue.main で呼び出せる ・メインスレッドでタスクを実行する ・UIの更新はこのキューで行われなければならない |
Global | 5つ | デフォでOSが用意 | 並列方式 | ・DispatchQueue.global(qos: ) で呼び出せる ・QoSの数だけ存在し、QoSで優先順位を定義する |
Private | 自由 | ユーザが生成 | ユーザーが指定 | ・MainとGlobalだけだと不都合がある場合に利用する ・QoSやタスク引渡し方式を指定することが出来る |
脳死で書いていた DispatchQueue.main.async {} は 『OSが用意してくれている Main Dispatch Queue というキューに、直列方式でタスクを追加する』という実装だったということがここまででわかると思います。
これら3つの使い分けですが、特別な必要性がない場合には Main と Global のみを利用、本当にOSが用意しているものでは不十分だった際のみ Private Dispatch Queue を作成するという方針でよいと思います。 (ただ、10年弱iOSを書き続けていて、Private Dispatch Queueが必要だったケースはほぼほぼありませんでした。)
(Private Dispatch Queue)
上記の通り、使うことはほぼないと思いますが、簡単な使い方を記載しておきます。 DispatchQueue型のイニシャライザから生成するのですが、その際以下の引数を受けます。
- label: キューの名前を指定します
- 一般的に逆順DNS(Domain Name System)形式で命名します
- os: キューの優先度をQoSで指定します
- attributes: キューの様々なオプションを指定します
- .concurrent: キューが並列になります(デフォルトは直列)
- .initiallyInactive: タスクをキューの中で待機させ、手動で .activate() するまで実行しないようにします
let privateQueue = DispatchQueue( label: "com.my_company.my_app.upload_queue", qos: .default, attributes: [.concurrent])
GCDの使い方
DispatchQueueの代表的な操作のサンプルコードを記載します。
例として Global Dispatch Queue (QoSは userInteractive) を操作しています。
// Queueに同期的にタスクを追加する(sync) DispatchQueue.global(qos: .userInteractive).sync { // Task codes // 他の場所で同じQueueが使われていた場合、デッドロックが発生する可能性も } // Queueに非同期的にタスクを追加する(async) DispatchQueue.global(qos: .userInteractive).async { // Task codes } // 直列処理タスクを追加する(.barrier option) DispatchQueue.global(qos: .userInteractive).async(flags: .barrier) { // Task codes // .barrierオプションとともに追加されたタスクについては、1つずつ順番に処理を行う // 並列処理ディスパッチキューの中で例外的に直列処理したいタスク群が存在する場合に使用する } // タスクを遅延追加する(asyncAfter) DispatchQueue.global(qos: .userInteractive).asyncAfter(deadline: .now() + 10) { // Task codes // 設定時間経過後にタスクが追加される設定時間経過後にタスクが追加される。 } // タスクの一時停止と再開(suspend, resume) DispatchQueue.global(qos: .userInteractive).suspend() DispatchQueue.global(qos: .userInteractive).resume()
Operation、OperationQueueクラスを用いた非同期処理
Operation、OperationQueueクラスとは
Operation、OperationQueueは、コアライブラリである Foundation に実装されている、非同期処理を実現するためのクラスです。
Operation は、実行されるタスクとその関連情報をオブジェクト指向的手法でカプセル化したもの、OperationQueueは、Operationを優先度と準備状態に基づき実行するキューとなっております。
内部ではGCDを用いるため、GCDと同様にプログラマはスレッド自体は一切操作することなく、タスクをキューに追加するだけで非同期処理を実現できます。しかし、GCDでは Main Dispatch Queue と Global Dispatch Queue をOSが用意してくれていたのに対し、こちらは OperationQueue というキューを自前で生成しなければならないという違いがあります。
一方で、出来ることベースだけで考えるとGCDの上位互換になっているとも言えます。
以下で具体的な利用方法を解説し、最後にGCDと比較します。
Operation、OperationQueueクラスの使い方
タスクの定義とキューへの追加
先述の通り、Operation は、実行されるタスクとその関連情報をオブジェクト指向的手法でカプセル化したものです。
Operation のサブクラスとしてタスクを定義する形式にすることで、扱いやすいインタフェースを提供しています。
タスクの処理自体は、overrideした main() メソッドの中に記述します。
変数をプロパティとして管理することで、タスクの関連情報も簡潔に利用することができます。
以下に実装例を挙げます。
class MyOperation: Operation { // タスクの関連情報をプロパティとして保持する let myProperty: Int init(myProperty: Int) { self.myProperty = myProperty } // main()関数をoverrideすることでタスクの処理を記述する override func main() { print(myProperty) } }
そして、OperationQueueは、Operationを優先度と準備状態に基づいて実行するキューです。
GCDと違って、最初から用意されているキューは存在しないため、コンストラクタによって自前で生成して利用します。
同時に並列処理できるタスクの数 と QoS をプロパティとして指定することが出来ます。
インスタンスメソッドである addOperation(_:)、もしくは addOperations(_, waitUntilFinished:) によってタスクを追加することで、即座に処理が実行されます。
waitUntilFinished はBool型で、trueを与えることで、第一引数で与えた全てのタスクの実行が終わるまで、呼び出したスレッドをブロックするように出来ます。
let queue = OperationQueue() queue.maxConcurrentOperationCount = 2 // 同時に並列処理できるタスクの数を設定 queue.qualityOfService = .userInitiated // QoSを設定 queue.addOperation(MyOperation(myProperty: 0)) queue.addOperations([MyOperation(myProperty: 1), MyOperation(myProperty: 2)], waitUntilFinished: false)
タスク間の依存関係の設定
Operation、OperationQueueクラスのGCDに対する優位性の1つに、タスク間の依存関係を簡単に記述できる というものがあります。
ここでの依存関係とは「タスクAはタスクBが完了した後でないと実行しない」という関係を意味しています。
この記述には、Operationクラスの addDependency(: ) メソッドを用います。あるタスクに対して、それよりも先に実行されるべきタスクを引数に渡すことで、簡潔に依存関係を作成することが出来ます。
2つのタスクが、それぞれ別のスレッドで実行予定のタスクだったとしても依存関係はきちんと保たれます。
class OperationA: Operation { override func main() { print("OperationA is executed.") } } class OperationB: Operation { override func main() { print("OperationB is executed.") } } let queue = OperationQueue() queue.maxConcurrentOperationCount = 1 queue.qualityOfService = .userInitiated // [OperationA, OperationB] の順番で入ったOperationの配列を用意する var operations = [Operation]() operations.append(OperationA) operations.append(OperationB) // OperationB が完了するまで OperationA を実行しないように関連付ける operations[0].addDependency(operations[1]) // OperationQueueに [OperationA, OperationB] を追加 queue.addOperations(operations, waitUntilFinished: false) // 実行結果 // --> OperationB is executed. // --> OperationA is executed.
上記の例では、OperationQueueのmaxConcurrentOperationCount(同時に並列処理できるタスクの数)が 1 に設定されているので、依存関係を記述しなければ、本来はキューに追加された順に OperationA → OperationB の順番で実行されるはずです。ですが、「OperationAは必ずOperationBの後に実装する」という依存関係を定義したことで、OperationB → OperationA の順に実行されることが確約されるようになりました。
このように、Operation、OperationQueueクラスを用いることで、タスク間の依存関係を簡単に記述できます。
タスクのキャンセル
Operation、OperationQueueクラスのGCDに対するもう1つの優位性が、キューに追加したタスクを簡単にキャンセルできる というものがあります。
GCDは、タスクをクロージャで渡すという記述方法であるため、1度キューに積んだタスクを参照することがそもそも難しく、キューに追加後のタスクをキャンセルすることは非常に困難です。
一方で、Operationクラスには cancel() というメソッドが用意されており、これによって簡単にタスクをキャンセルすることができます。
class MyOperation: Operation { let myProperty: Int init(myProperty: Int) { self.myProperty = myProperty } override func main() { print("Operaion \(myProperty) is executed.") } } let queue = OperationQueue() queue.maxConcurrentOperationCount = 1 queue.qualityOfService = .userInitiated // プロパティが 0 ~ 5 のMyOperationの配列を用意して、キューにまとめて積む var operations = [Operation]() for i in 0..<6 { operations.append(MyOperation(myProperty: i)) } queue.addOperations(operations, waitUntilFinished: false) // プロパティが3のタスクをキャンセルする operations[3].cancel() // 実行結果 // --> Operation 0 is executed. // --> Operation 1 is executed. // --> Operation 2 is executed. // --> Operation 4 is executed. // --> Operation 5 is executed.
上記の例では、プロパティが3のタスクがキューに積まれた後にキャンセルされていることがわかると思います。
また、OperationQueue クラスの cancelAllOperations() メソッドを用いることで、キューに積まれた未実行のOperationを全てまとめてキャンセルすることも出来ます。
// キューに積まれた未実行のOperationをまとめてキャンセルする
queue.cancelAllOperations()
このように、Operation、OperationQueueクラスを用いることで、タスクのキャンセルも簡単に記述できます。
ただし、1点注意点があります。
Operationの処理の実行途中で cancel() が呼ばれた場合、そのOperationの処理は中断されず、最後まで実行されてしまいます。
もし、main() の中に書かれた処理を実行中にタスクがキャンセルされた場合のキャンセルケースを実装したい場合、 isCancelled というフラグを各時点で確認することで実装します。
class MyOperation: Operation { let myProperty: Int init(myProperty: Int) { self.myProperty = myProperty } override func main() { print("Operaion \(myProperty) is started.") // この時点でタスクがキャンセルされていないかを確認する if (isCanceled) { print("Operaion \(myProperty) is canceled.") return } print("Operaion \(myProperty) is executed.") } }
上記のように記述することで、もし main() に記述された処理が実行されている最中にタスクがキャンセルされた場合の処理を定義することが出来ます。
GCDとの比較
項目 | GCD | Operation、OperationQueueクラス |
---|---|---|
タスクの記述 | 比較的簡単 (クロージャを用いて簡潔に記述できる) | 比較的煩雑 (Operationクラスを用いた定義が必須) |
キューの生成 | 不要 (グローバルキューを利用できる) | 必要 (OperationQueueの生成が必須) |
タスクのキャンセル | 実装が困難 | cancel() で容易に実装可 |
タスク間の依存関係 | 実装が困難 | addDependency() で容易に実装可 |
まとめ | 簡単に記述できるが 複雑なタスクの操作や複数タスクの関連付けが出来ない |
記述に手間がかかるが 複雑なタスクや関連のある複数のタスクの操作に向いている |
余談:Operationクラスは単体でも実行できる
実は Operation は、start() メソッドを明示的に呼ぶことで、OperationQueueを併用しなくても単体で実行することが可能です。
ですが、使いにくい上に、OperationQueueと併用する方法に対する優位性もあまり感じされないので、使わない方がいいですという話を以下でします。なので読み飛ばしていただいて大丈夫です。
Operationクラスの公式リファレンスに以下のような記載があります。
If you do not want to use an operation queue, you can execute an operation yourself by calling its start() method directly from your code. Executing operations manually does put more of a burden on your code, because starting an operation that is not in the ready state triggers an exception. The isReady property reports on the operation’s readiness.
要約すると、Operation が実行可能な状態ではないときに実行すると例外が発生してしまうため、isReady というフラグを使ってOperationの準備状況を自前で確認する必要があり、コードの負担が増えてしまうと言われています。
また、Operationには以下の2種類の実装方法があります。
タイプ | 用途 | タスクの記述 |
---|---|---|
non-concurrent | 同期的なタスクの記述に用いる | main() をoverrideして書く |
concurrent | 非同期的なタスクの記述に用いる | start() をoverrideして書く |
Operaitonクラスのインスタンスは、main() の処理が完了すると、キューから削除されてしまいます。
そのため、concurrent(非同期的)なタスクを main() に記述すると、その完了を待たずにキューから削除されてしまうことになります。
そこでどうするかというと、Operation の処理が開始されるときに呼ばれる start() というメソッドをoverrideして非同期処理を記述し、Operationの実行状態を表す isExecuting と Operationの終了状態を表す isFinished という2つのフラグを手動で更新することで、Operationの状態を自前でハンドルするという実装方法になります。
詳しく知りたいという方は、Operation Queues - Concurrency Programming Guide をご覧ください
以上のように、OperationQueueを使っていたときはよしなにやってくれていたOperationの状態管理を、Operationを単体で利用しようとすると、ゴリゴリ自分で書かなければいけないことになります。
Operation クラスは、基本的には OperationQueue クラスとセットで利用することをおすすめします。
Threadクラスを用いた非同期処理
Threadクラスとは
Threadクラスは、コアライブラリである Foundation がスレッドそのものを実装したものです。このクラスを用いることで、プログラマが生のスレッドにアクセスすることが出来て、生成と制御を自身で行うことができます。
どうしても特定のタスク専用の新しいスレッドを作成したい場合や、GCDのスレッドプーリングの仕組みに依存しない独自の並列処理システムを組み上げたい時などに利用しましょう。そんな時が人生に何度あるのだろうかとは思ってしまいますが…
Threadクラスの使い方
Threadクラスの使い方は、Operationクラスとほぼ同じと言っても過言ではないです。Threadクラスの公式リファレンスでも以下のように言及されています。
The Thread class supports semantics similar to those of Operation for monitoring the runtime condition of a thread. You can use these semantics to cancel the execution of a thread or determine if the thread is still executing or has finished its task. Canceling a thread requires support from your thread code; see the description for cancel() for more information.
Thread | Apple Developer Documentation https://developer.apple.com/documentation/foundation/thread
Thread の作成
Thread を新しい生成する際は、Threadクラス のサブクラスとしてタスクを定義します。
行いたい処理自体は、overrideした main() メソッドの中に記述します。
// Threadクラスのサブクラスとして定義する class MyThread: Thread { override func main() { print("executed.") } } // 初期化した後に start() メソッドを呼ぶことで処理を開始する let thread = MyThread() thread.start()
Thread のキャンセル
タスクのキャンセルについても、Operationと同様に書くことが出来ます。
ただ、Operationクラスと同様に1点注意点があります。
処理の実行途中で cancel() が呼ばれた場合、その Thread の処理は中断されず、最後まで実行されてしまいます。
もし、main() の中に書かれた処理を実行中にタスクがキャンセルされた場合のキャンセルケースを実装したい場合、 isCancelled というフラグを各時点で確認することで実装します。
let thread = MyThread() thread.start() // タスクの処理をキャンセルする thread.cancel()
その他のThread操作
一部のThread 操作に関するメソッドはクラスメソッドとして定義されていて、実行することでカレントスレッドの操作ができます。自前のThreadクラスについてこれらの操作を行いたい時には、そのThreadの処理の中で呼ぶことで操作が可能になります。
class MyThread: Thread { override func main() { // forTimeIntervalで指定した秒数だけスレッドを停止する Thread.sleep(forTimeInterval: 10) // Date型の値で指定した日時までスレッドを停止する Thread.sleep(until: Calendar.current.date(byAdding: .minute, value: 1, to: Date())!) // スレッドの実行を途中で終了する Thread.exit() // 上記の exit() により、この処理が実行されることはない print("executed.") } }
ぶっちゃけいつ使うのか?
Thread クラスを利用してスレッドの管理を手動で行う必要があるケースは極めて稀、もっと言えばほぼ無いと言っていいと思います。
煩わしい上にバグの温床であるスレッド管理を全て肩代わりしてくれる GCDやOperation、OperationQueueがせっかく用意されているので、まずはそれらで代替することで何とか Thread管理を避けられないかを考えるべきだと思います。
乱暴にまとめてしまえば、なるべく使わなくていい方法を考えよう という結論になります。
[TBD] Combineを用いた非同期処理
時間が無くて書ききれておりません… また時間を見つけて埋めようと思います。
Swift Concurrencyを用いた非同期処理
Swift Concurrency とは
Swift 5.5からSwiftは並列処理を言語機能としてサポートするようになったのですが、そこで登場した非同期処理・並列処理をサポートする機能群を総称して Swift Concurrency と呼びます。
Swift Concurrency の概念は大きく以下の3つに大別できます。
- async / await
- 非同期処理を簡潔に記述するための構文
- Structured Concurrency
- 各並列処理間の関係性を整理するための概念
- Actor
- 安全な並列処理を記述するための新たな型の定義方法
今回はこのうち、非同期処理の実現に関係が深い、async/await と Structured Concurrency について、解説していきます。
async/await
async/await とは
async/await とは、非同期処理を簡潔に記述するための構文です。JavaScript を書く方には馴染みのある構文だと思いますが、Swift Concurrencyの流れでSwiftでも利用可能になりました。
これまでの非同期処理は、完了後の処理をクロージャで渡して、その引数で結果を受け取る書き方が一般的でした。非同期関数内でクロージャを手動で呼ぶためクロージャを呼び出し忘れたり、複数回呼んでしまうリスクがあります。
以下に、async/await 登場以前の実装方法で、「外部APIを叩いて画像を取得した上でサムネイルとして整形して、その結果を引数に completionHandler を呼ぶ」というよくある関数を実装したものを提示します。
非同期処理を連続で呼ぶときにクロージャを多用することとなりネストも深くなるため、コードが冗長かつ可読性が低くなってしまっていることがわかると思います。
// async/await 登場以前の書き方 func fetchThumbnail(for id: String, completion: @escaping (UIImage?, Error?) -> Void) { let request = thumbnailURLRequest(for: id) let task = URLSession.shared.dataTask(with: request) { data, response, error in if let error = error { completion(nil, error) } else if (response as? HTTPURLResponse)?.statusCode != 200 { completion(nil, FetchError.badID) } else { guard let image = UIImage(data: data!) else { completion(nil, FetchError.badImage) return } image.prepareThumbnail(of: CGSize(width: 40, height: 40)) { thumbnail in guard let thumbnail = thumbnail else { completion(nil, FetchError.badImage) return } completion(thumbnail, nil) } } } task.resume() }
一方、async/await は以下のような仕組みで機能します。
- async を関数につけると、その関数の中は“非同期なコンテキスト” と見なされるようになり他の async が付いた関数を呼べるようになる
- async が付いた関数を呼び出す際はawait というキーワードを付けて呼び出すことで結果を “待つ” ことが出来る
先ほどの関数を async/await で書き換えると以下のようになります。
// async/await を用いた書き方 func fetchThumbnail(for id: String) async throws -> UIImage { let request = thumbnailURLRequest(for: id) let (data, response) = try await URLSession.shared.data(for: request) guard (response as? HTTPURLResponse)?.statusCode == 200 else { throw FetchError.badID } let maybeImage = UIImage(data: data) guard let thumbnail = await maybeImage?.thumbnail else { throw FetchError.badImage } return thumbnail }
先ほどに比べて、コード量・ネストの深さ共に大幅に改善され、処理の流れが上から下へ追いやすくなったと思います。
このように、async/await を利用することで、非同期処理も同期処理と同じように記述することができるため、コードの行数やネストの深さが減り、可読性も改善されます。
Suspension Point
先述の通り、async関数をawaitを付けて呼び出すと処理の実行が一時的に中断(Suspend)され、非同期的に結果が得られてから続きが再開(Resume)されます。
そのため awaitキーワードのある部分を Suspension Point と呼びます。
スレッドで処理の実行が Suspendされている間、実はそのスレッドは解放されて他の処理の実行に回ることも出来るようになっています。
そして、処理がResumeされた時にそのスレッドが埋まっていた場合、処理の続きは他のスレッドで実行されることになります。
そのため、Suspension Point の前後の処理は別スレッドで行われることもあり得るということになるので注意しましょう。
async/await を使う際の注意
「async を関数につけると、その関数の中は“非同期なコンテキスト” と見なされるようになり他の async が付いた関数を呼べるようになる」と先述しましたが、裏を返せば「async関数は async関数からしか呼び出せない」ということになります。
つまり、コールスタックを遡っていくと必ずasync関数を呼び出す入口が必要になるということになるのです。
同期的な処理の中に、突然 async を登場させるために、Task という概念が必要になります。逆に全ての async 関数は Task に紐付けられ、 Task上で実行されます。async/await と Task はかなり密接に関わる概念なのですが、詳しくは次の Strucutured Concurrency の章で解説します。
Structured Concurrency
Structured Concurrency とは
Swift は “構造化プログラミング(Structured Programming)” という概念を採用しています。
具体的には、「原則として処理は上から下に流れ、他のプロセスにジャンプすることは出来ない」「変数は定義されたブロック内でのみ利用可能」という原則によって構成されています。
Structured Concurrencyとは、並列に実行されるそれぞれの処理の関係性についてもスコープで構造化することで並列処理を管理しやすいようにしようという考え方です。
Structured Concurrencyは、各並列処理を表す Task という概念によって実現されます。複数のタスクを “安全で効率的なときだけ” 並列で実行することで、Taskは非同期的な処理を並列に実行するためのコンテキストを提供します。
async let Task
構造化された Task を生成する最も簡単かつ一般的な方法が async let Task です。
asyncメソッドの返り値としてasync letで変数を定義すると、そのメソッドの完了を待たずに処理が進みます。そして、その変数を利用するところで変数にawaitをつけることで、初めてその処理が終わるまでプログラムが中断されます。
func fetchThumbnail(URL url) throws -> Data { // async let で thumbnail という変数を定義する // (downloadDataはasync関数) async let thumbnail = downloadData(from: url) // downloadDataの完了を待たずに進む // このawaitがSuspension Point になり、 downloadData()の完了を待つ return await thumbnail }
async let による変数定義では、変数の初期化処理を実行するための新しいTaskが、現在処理を実行中のTaskの子Task として生成され、実際の処理は 子Task の方で実行されます。
async let による変数定義の時点では Suspension Point とならず、変数に実際の値が代入されないままプレースホルダーが設定されただけの状態になります。その変数を参照する際に処理が完了している必要があるため、呼び元では await を指定し、その部分が Suspension Point となります。
注意すべきなのが、async let で宣言された定数はそのスコープを抜ける前に必ず await されなければならないという点です。async let のまま return するなど、スコープの外に持ち出すことはできません。仮にスコープ内で await しなかった場合、スコープから抜ける前に自動的に await されます。
また、async let を並べることで、並列的に処理を行うことも可能です。
func fetchThumbnails(URL largeUrl, URL smallUrl) throws -> (large: Data, small: Data) { async let largeThumbnail = downloadData(from: largeUrl) async let smallThumbnail = downloadData(from: smallUrl) // ここで2つのdownloadData()両方が完了するのを待つ let thumbnails = await (large: largeThumbnail, small: smallThumbnail) return thumbnails }
この場合、2つのasync let による変数定義によって、2つの子Taskが生成され、それらが並列に走っています。
Structured Concurrency では、Task間にこういった “構造” が保たれることで、タスクが管理しやすく、並列処理のライフサイクルが理解しやすい状態を保つことができるようになっています。
TaskGroup
async let Task でも複数のTaskの並列実行が可能でしたが、より柔軟に任意の数の処理を並列に実行するためには TaskGroup を用います。
TaskGroupを利用する際には、グローバル関数である withTaskGroup()、もしくはそのエラー対応版の withThrowingTaskGroup() を用います。結果の型を引数で指定することで、クロージャの引数としてタスクグループが受け取れるのでそこに対して addTask() で処理を追加していきます。
func fetchThumbnails(for ids: [String]) async throws -> [String: UIImage] { var thumbnails: [String: UIImage] = [:] // withThrowingTaskGroup() によってTaskGroupを生成 // (String, UIImage)型を結果の型として指定している try await withThrowingTaskGroup(of: (String, UIImage).self) { group in for id in ids { // 各idに紐づいたTaskを追加している group.addTask { return (id, try await fetchOneThumbnail(withID: id)) } } // 子タスクから結果を完了した順に逐一取得する for try await (id, thumbnail) in group { thumbnails[id] = thumbnail } } return thumbnails }
addTask()で追加された処理は、現在のTaskを親とする子Taskで実行されます。上の例の場合、addTask() によって追加された各処理は、fetchThumbnails() に紐づくTaskの子タスクとして実行されます。
もちろん親タスクがキャンセルされれば各子タスクもキャンセルされ、全ての子タスクが完了しなければ親タスクは完了しません。
また、TaskGroup に対して cancelAll() を呼び出すことで、 TaskGroup に含まれるTaskを全て明示的にキャンセルすることもできたり、タスクの優先度を TaskPriority 型で指定することができたりします。この辺りはかなりOperation、OperationQueueクラスの操作と類似していますね。
Unstructured Task、Detached Task
ここまで、構造化されたTaskによる並列処理の実現方法を見てきましたが、逆に構造化 “されていない” 並行性が必要なケースも存在します。例としては以下のような場合があります。
- 非同期的でないコンテキストから非同期的な処理を開始したい場合(Viewのライフサイクルなど)
- 子Task の処理が 親Task のライフサイクルを越えて実行され続けるような処理を行いたい場合
このような時には、Task.init を用いることで、構造化されていないTaskを初期化することが出来ます。このように生成されたTaskを Unstructured Task と呼びます。
func fetchThumbnail(URL url) async throws -> Data { let thumbnail = await downloadData(from: url) // サムネイルを保存するUnstructured Taskを生成 // (init() は省略できる ) Task { await saveThumbnail(thumbnail: Thumbnail) } return thumbnail }
上記の例では、構造化されていない、つまりfetchThumbnailのライフサイクルに縛られないUnstructured Taskを生成しており、このように書くことで、fetchThumbnail() は saveThumbnail() が終了するのを待たずに完了し、逆に saveThumbnail() は fetchThumbnail() が完了した後でも処理を続けることが出来ます。
このように Unstructured Taskは、親のライフサイクルに縛られないという性質を持っている一方で、実行の優先度などのもともとのコンテキストが持っていた情報をほとんど引き継ぎます。
そこで、Task.detached を用いることで、もともとのコンテキストが持っていた情報を全く引き継がないUnstructured Task を初期化することも出来ます。このようなTaskを Detached Task と呼びます。
func fetchThumbnail(URL url) async throws -> Data { let thumbnail = await downloadData(from: url) // サムネイルを保存するDetached Taskを生成 // fetchThumbnail()に紐づくTaskのコンテキストを全く引き継がない Task.detached(priority: .background) { await saveThumbnail(thumbnail: Thumbnail) } return thumbnail }
Detached Taskを利用する際には、「構造化されていない = そのライフサイクルをプログラマ自身が管理しなければいけない」ということに注意が必要です。
Taskの分類
ここまでで紹介した4種類のTaskについて、それぞれの特徴を比較する表を掲載します。どのTaskが使うのを適切なのかを調べる際の参考にしてみてください。
まとめ
結局どれを使うべきなのか?
ここまで以下の5つの非同期処理の実現方法について解説してきました。
- GCDを用いた非同期処理
- Operation、OperationQueueクラスを用いた非同期処理
- Threadクラスを用いた非同期処理
- Combineを用いた非同期処理
- Swift Concurrency を用いた非同期処理
最終的に気になるのは、「結局どれをいつ使うべきなの?」ということだと思います。
まずは特殊な Combine から見ていきたいと思います。
こちらはRxなどを書いてきた経験や知見があって、Reactive Programmingが手に馴染む方は候補に入れていただいてもいいと思います。ただし、文法が特殊で学習コストがかかる上に知識の横展開がしにくいこと、主にActorの登場によってSwift Concurrencyを無視してiOS開発を行うのが困難になってきていることを考えると、今から着手するというのはあまりおすすせしません。
次にThreadクラスについてですが、前述の通り、可能な限り使わなくていい方法を模索すべきだと思います。
煩わしい上にバグの温床であるスレッド管理を全て肩代わりしてくれる GCDやOperation、OperationQueueがせっかく用意されているので、まずはそれらで代替することで何とか Thread管理を避けられないかを考えるべきでしょう。
単純かつ独立した非同期処理を記述する場合は、GCDの利用で十分です。
非同期な処理の後でMainスレッドに制御を戻してViewを操作する場合など、既存のDispatchQueueのみの利用で数行で実現できます。そのためだけにOperaion、OperationQueueなどを使うのはtoo muchだと思います。
最後にOperaion、OperationQueueクラスとSwift Concurrencyの比較です。正直なことを言ってしまえば、出来ることはあまり変わらないので好みの問題だと思います。
「タスクの再利用性を重視して、タスクの処理のその関連情報をオブジェクト指向的にまとめて管理したい」のであれば Operaion、OperationQueueクラス。「タスクを構造的に管理することを重視して、複数のタスクが絡み合った処理も完結に記述したい」のであれば Swift Concurrency がおすすめです。
あくまで個人的な意見なので参考程度にしつつ、今後の実装に活かしていただければと思います。
おわりに
社内勉強会での内容をまとめ直しつつ、話したいことを肉付けしていくようにしていたら、とんでもないボリュームの記事になってしまいました。
冒頭でも申し上げましたが、マサカリやご意見は大歓迎です。この記事が少しでも皆さんの学びの助けになり、逆に僕にとっても更なる学びになることを願っています。
初期スタートアップにおける社内文化の在るべき形とは
はじめに
この記事は6人の小さなチームのマネジメントを3ヶ月齧っただけの僕が、それでも外部のメンターさん方に猛烈に相談に乗っていただいたりしながら 社内文化 について試行錯誤した道のりについて綴っています。拙文ですが温かく見守ってくださると幸いです。
背景
簡単に背景を共有させていただきたいと思います。
僕が3ヶ月間エンジニアのマネージャーを経験したこと
僕が所属している 株式会社Flatt のエンジニアチームは
12月まで | 1月以降 | |
---|---|---|
実働人数 | 3人 | 2倍の6人に |
社長の関わり方 | 手も動かしつつマネジメント | 現場を離れて社長業に専念 (マネジメント層が不在) |
というように 2017年 => 2018年 という年の変わり目を境に大きく環境が変わりました。
そこで純エンジニアポジションでは1番の古株だった僕がエンジニアのマネージャーに抜擢され、この3ヶ月間 6人のエンジニアチームのマネージャーとしてエンジニアの組織全体を見る経験 をしました。
社内文化に対する考え方の変遷
きっかけになったのはマネージャー就任
当初僕は「マネージャーになると言っても人々のタスクを管理してみんなが働きやすい環境を作ればいいのかな?」くらいの甘い認識でいました。
ところが始めてみると驚くことに、1日にしなければいけない意思決定の数が大小合わせて今までの10倍くらいになりました。
一介の平エンジニアとして働いていた時は、社長から降りてくる指示に従って、目の前の作業を最大限のパフォーマンスで完遂していれば評価されていました。
マネージャーになると『ユーザーのこと(一番大事!)』『リソース』『期日』『メンバーのモチベーション』などの多くのファクターに思いを馳せながら、テンポよくメンバー全員分の意思決定をしていかなければなりません。
しかしいかに小さなチームとはいえ、この意思決定が一過性のものであったり属人的であったりすると組織全体の脆弱性に繋がります。
そこで僕が意思決定の拠り所としたのが 『会社の文化やコアバリュー』でした(当然の流れだと思います)。
(※ 今後便宜のために『正確な』意思決定という言葉を使いますが、これの意図は打ち手として有効かどうかではなく、組織としての軸に沿っているという意味です)
社内文化は何のために存在するのか?
社内文化やそれに属するマインドに関する記事を見ていると「個人の成長のため」みたいなメンバー側から見た表面的な切り口で自己啓発のような文脈で語られているものが散見されます。
しかし、個人の成長 / 迅速で正確な意思決定 / コミュニケーションの円滑さ など社内文化の浸透による恩恵は様々ですが、それもひっくるめて社内文化の目的は必ず『長期的な組織の利益の最大化』に帰着します。
『長期的な組織の利益(どう定義するかは組織によります)の最大化』が全ての社内文化の上に存在しており、社内文化はその潤滑油にすぎない という事実を常に念頭に置いておかないと、社内文化はすぐに形骸化してしまいます。
社内文化の形骸化とは
端的に言うと 社内文化の言葉だけが一人歩きして、その本質への理解が疎らになり、結果パフォーマンスの低下に繋がる ことを表現しようとしています。
例えば『自責』という文化があった時に、長期的な組織の利益の最大化を考えると他の人に投げた方がいいトラブルシューティング(その問題について考察し、パフォーマンスを最適化するために他の人にアサインした時点で自責)を、『自責』という言葉の言葉尻だけ捉えて全部自分で抱えた場合、それは本来の目的に適っていないので形骸化していると言えると思います。
これが進行してしまう原因は大きく分けて2つあると思います。
組織の拡大に伴う形骸化
これは規模に応じて避けられなくなってくると思います。
初期のメンバーは特に、その組織に対して並々ならない思いがあります。皆が空気を吸うように「長期的な組織の利益の最大化」を念頭に置いて考え、能動的に行動します。
しかし、規模の拡大に伴って組織全体をマクロに捉える見方は薄れていき、だんだんと部署・チーム・個人のようなミクロな視点が蔓延していきます。
これは当然の動きであり、非難する意図は全くありません。全員が全員、前述のような長期的な組織の利益の最大化を第一に考えられる巨大組織の構成は正直不可能だと思います。
結果として生まれる個人がミクロな見方でバラバラな方向を向いている状態の下で、社内文化の形骸化を防ぐマネジメントはとても難易度が高いと思います。
現に大企業やメガベンチャーに就職した先輩方に企業の文化やコアバリューについてお話を聞くと、「言葉は覚えているが特に深い意味も考えていない人」、「なんとなく小馬鹿にしている人」や「そもそも存在自体を知らない人」がほとんどでした。
認識共有の不足からくる形骸化
前述の通り、組織がかなり大きくなってくると話は変わりますが、今後の組織の在り方を強く定義する存在である経営陣・初期のメンバーについては、この企業文化は高いレベルで正確に共有されていなければなりません。
このメンバーは今後、この文化やコアバリューを他のメンバーに伝えていくハブの役割を果たして行くことになるからです。
しかしメンバーが少ない中でも、ただでさえめまぐるしく状況が変化する初期のスタートアップの中では綿密なコミュニケーションの努力を怠っていると文化やコアバリューへの認識は容易に分岐していきます(体験談です)。
それぞれが大きな責任を持ち、かなりの量と質の意思決定を繰り返していると、大きなパラダイムシフトが頻繁に発生します。怖いのはそれが無意識化で進行するということです。
なので、社内の文化やコアバリューを擦り合わせて再定義する作業は、継続的にしていかないといけないのだと思います。
スタートアップ初期の社内文化の在り方
前述の通り、文化やコアバリューの形骸化は時間や規模に伴って大なれ小なれ必ず進行してしまいます。
しかし、堅牢な組織を保ち続けるために、これに抗う最大限の努力は継続的に行うべきだと考えています。
特に初期スタートアップに関しては以下の2つを心がけることが有効であると感じました。
最小人数で定義して共有には全体で取り組む
文化やコアバリューを定義する初期の段階で決めるための話です。
Fablic CEOの堀井さん が会社の納会にお話をしに来てくださった時に、
社内文化やコアバリューは社長の独断で定義していい、そしてそれをより多くのメンバーに(初期メンバーは特に)より深く浸透させることに対してのリソースは惜しまず割くべき(コアバリュー合宿など)
とおっしゃっていました(堀井さんはこの記事でも企業文化やコアバリューの大切さについて触れています)。
正直、この社内文化やコアバリューに関しては正解が存在しないと考えています。そんな抽象的な議論に多くの人数が関わると船頭多くして船山に上ってしまい、徒らに時間が過ぎていってしまいます。
それに加えて、お互いの主張が交錯して、最終的な成果物がそれぞれを折衷した何ともいえないものになってしまうというケースも往々にしてあります。
なので、定義は最小人数(できれば社長が1人で)して、他のメンバーはそれを理解するための施作・努力を継続的に行っていくというスタンスの方が効率よく軸を安定させられると思います。
(たまにこれらの定義を社外の人々に任せるケースを見ますが、とてもうーんという気持ちになります。)
共感レベルでの採用
(社内の文化やコアバリューの形骸化に)抗う最大限の努力は継続的に行うべき
と言及しましたが、やらなければいけないことが無限にあるスタートアップの中で、これにコストをかけすぎるのはとても痛手です。
なのでここの共有にコストがかかる人間は(初期は特に)採用の段階で弾くべきだと考えています。
事業の表面的なスピード感を重視するとスキルセットや経歴を最重要視しがちですが、僕は長期的な視点で見た時にこちらの方が圧倒的に重要だと思います。
これは僕が勝手に脳内でイメージしているオレオレ共感度グラフ的なものです。
社長の脳みそを完全再現している赤レベルでの共感は実質無理なので、初期のメンバーはできる限りピンクレベルであるべきであり、最低でも黄色レベルでないといけないと思います。
また組織が巨大化していった時に、全員の(特に責任を持っている人々の)共感レベルがどれくらいのレベルで保てるかというのが、堅牢な組織作りの大きな指標になると考えています。
社内文化に対する現在の僕なりの結論のまとめ
・文化やコアバリューの形骸化は時間や規模に伴って大なれ小なれ必ず進行する
・しかし、これは組織の脆弱化に繋がるため、抗うための最大限の努力は継続的に行うべき
・社内の文化やコアバリューを擦り合わせて再定義する作業は継続的にしていかないといけない
・初期スタートアップでは『最小人数での定義』と『共感レベルでの採用』が有効である
終わりに
感想
最後まで読んでいただきありがとうございました。正直この3ヶ月間においてはかなりの労力を組織への考察に割いたと思います。
こんなぺーぺーの悩みを真剣に聞き、相談に乗ってくださった業界の先輩の方々には感謝の気持ちでいっぱいです。
特に納会に来てくださった堀井さんの他に、組織についてたくさんのFBをくださった DeNAの千條さん、eurakaのkaneshinさん、Mercariのosamingoさんには感謝してもしきれません。
今後もエンジニアとしての力量を磨きつつ、考察を続けていこうと思います。
イルミネーションもいいけど僕の『正弦関数 in Swift』で作ったハート群もめっちゃ綺麗だから見て見て
この記事は Hakusan mafia Advent Calendar 2017 の24日目の記事です。どの辺がマフィアなのかはよくわかってません。。。
はじめに
クリスマスイブですね(記事出した時はそうなんです)。街には笑顔が溢れて、活気に満ちていることと思います。カップルで幸せな時間を共にする人、家族で温かい食卓を囲む人、孤独を噛みしめる人など、色々な過ごし方の人がいると思います。
中には クリスマスなのにSwiftでハートがセクシーに動くAnimationが作れなくて困ってる人 もいるでしょう。今回はそんな人たちのための記事です。
Swiftでは基本的なAnimationは数行で実装できるようになっている(本当に素敵)のですが、今回は標準では積まれていないゆるやかな往復運動を正弦関数を用いて実装したいと思います。
これから触れるAnimationは実際に僕の会社のサービス PinQul でも使われているものの一部で結構我ながら気に入ってます。
今回はこのハートの動きの部分が純Swiftでどうやって動かされているのかを見ていきたいと思います。
方針
このハートのAnimationは3つの小さなAnimationに分解できます。
- だんだんopacityが小さくなるAnimation
- y方向に一定速度で上昇していくAnimation
- x方向に緩やかに振動するAnimation
です。1と2に関してはCABasicAnimationというswift組み込みのAnimationで簡単に実装できるので、今回は3をメインに触れていきたいと思いますが、一応Animationの基礎についても触れていきます。
CAAnimationの基礎
僕の記事なんて読まなくても、素晴らしい記事がQiitaにたくさん落ちています。
その中でも今回必要なものだけを抜粋しました。
CABasicAnimation
SwiftでUIViewの属性の1つをAnimationで変化させたいと思った時、大概の場合は
- 何を変化させるか(keyPath)
- 初期値(fromValue)
- 終了値(toValue)
- 期間(duration)
を指定するだけで簡単に実装できます。これを可能にしているのが CABasicAnimation です。
例)
// // 透明からだんだんViewが現れるAnimation // heartViewは今回Animationを付けたいUIViewのインスタンス // let verticalAnimation = CABasicAnimation(keyPath: "position.y") verticalAnimation.fromValue = heartView.center.y verticalAnimation.toValue = heartView.center.y - 300 verticalAnimation.duration = 3.0 heartView.layer.add(verticalAnimation, forKey: nil)
※CABasicAnimationを用いてAnimationで変化させられる要素(keyPath)は以下を参照してください。
これでも十分綺麗ですが、まだちょっと物足りないですね。
CAAnimationGroup
実はCAAnimationはいくつでも組み合わせることができます。CAAnimationGroupというクラスを使っていきます。
CAAnimationGroupのインスタンスがもつ animations プロパティにCAAnimationの配列を渡すことで、それらのAnimationが1つに集められた1つのAnimationを作成することができます。
例)
let opacityAnimation = CABasicAnimation(keyPath: "opacity") opacityAnimation.fromValue = 1.0 opacityAnimation.toValue = 0.0 let verticalAnimation = CABasicAnimation(keyPath: "position.y") verticalAnimation.fromValue = heartView.center.y verticalAnimation.toValue = heartView.center.y - 300 let animationGroup = CAAnimationGroup() animationGroup.duration = 3.0 animationGroup.animations = [opacityAnimation, verticalAnimation] heartView.layer.add(animationGroup, forKey: nil)
透明度を追加したことで儚げな感じがプラスされましたね。人の心は移ろいやすいものです
もちろん配列に追加していけば3つでも4つでもAnimationを組み合わせることができます。
本題の正弦関数Animationについて
Swiftでの正弦関数
Swiftの標準ライブラリである Foundation は豊富に数学関数を含んでいます。
三角関数関連だけでも以下のラインナップです。完璧に揃ってます。
let x = CGFloat(1.0) //単位はラジアン // 通常のサイン・コサイン・タンジェント sin(x) cos(x) tan(x) // アークサイン・コサイン・タンジェント asin(x) acos(x) atan(x) // ハイパボリックサイン・コサイン・タンジェント sinh(x) cosh(x) tanh(x) // アークハイパボリックサイン・コサイン・タンジェント asinh(x) acosh(x) atanh(x)
今回はその中でも正弦関数(sin)をつかって滑らかなAnimationを実装していきます。
CAKeyframeAnimation
CAKeyframeAnimation は経過時間の配列とそれに対応する値の配列を与えることで、その途中経過をよしなに補完したAnimationを作成してくれます。
例) 公式Documentのサンプル
let colorAnimation = CAKeyframeAnimation(keyPath: "backgroundColor") colorAnimation.values = [UIColor.red.cgColor, UIColor.green.cgColor, UIColor.blue.cgColor] colorAnimation.keyTimes = [0, 0.5, 1] colorAnimation.duration = 2
値や経過時間の差分に変化をもたせることで、CABasicAnimationより自由度の高いAnimationを作成することができます。
今回は正弦関数を使って position.x の値を変則的に動かすことでゆるやかなカーブを描かせていきます。
ゆるやかな往復運動の実装
あらかじめAnimationの duration(時間の長さ) と frameCount(コマ数) を指定しておきます。
そして horizontalAnimation という名前で宣言しておきます。
// // heartViewは今回Animationを付けたいUIViewのインスタンス // let duration = 3.0 let frameCount = 30 let horizontalAnimation = CAKeyframeAnimation(keyPath: "position.x") horizontalAnimation.duration = duration
keyTimes(値を指定する先の経過時間)を設定する
今回 keyTimes に関しては均等に与えたいので duration をframeCountで均等に割った経過時間を指定します。
// // heartViewは今回Animationを付けたいUIViewのインスタンス // // let duration = 3.0 // let frameCount = 30 // let horizontalAnimation = CAKeyframeAnimation(keyPath: "position.x") // horizontalAnimation.duration = duration horizontalAnimation.keyTimes = (0...frameCount).map({ let dividedTime = Double($0) * duration / Double(frameCount) return NSNumber(value: Double(dividedTime)) })
values(keyTimesに対応する値の組み合わせ)を設定する
ここで正弦関数を用いて動きに遊びをもたせます
// // heartViewは今回Animationを付けたいUIViewのインスタンス // // let duration = 3.0 // let frameCount = 30 // let horizontalAnimation = CAKeyframeAnimation(keyPath: "position.x") // horizontalAnimation.duration = duration // horizontalAnimation.keyTimes = (0...frameCount).map({ // let dividedTime = Double($0) * duration / Double(frameCount) // return NSNumber(value: Double(dividedTime)) // }) horizontalAnimation.values = (0...frameCount).map({ let x = sin(CGFloat($0)) * 25 // 振幅: 25 return heartView.center.x + CGFloat(x) })
完成形
// // heartViewは今回Animationを付けたいUIViewのインスタンス // let duration = 3.0 let frameCount = 30 let horizontalAnimation = CAKeyframeAnimation(keyPath: "position.x") horizontalAnimation.duration = duration horizontalAnimation.keyTimes = (0...frameCount).map({ let dividedTime = Double($0) * duration / Double(frameCount) return NSNumber(value: Double(dividedTime)) }) horizontalAnimation.values = (0...frameCount).map({ let x = sin(CGFloat($0)) * 25 // 振幅: 25 return heartView.center.x + CGFloat(x) }) heartView.layer.add(horizontalAnimation, forKey: nil)
いい感じでゆらゆらしてます!
完成!
今までの2つの Animation と組み合わせて再生してみましょう。
let opacityAnimation = CABasicAnimation(keyPath: "opacity") opacityAnimation.fromValue = 1.0 opacityAnimation.toValue = 0.0 let verticalAnimation = CABasicAnimation(keyPath: "position.y") verticalAnimation.fromValue = heartView.center.y verticalAnimation.toValue = heartView.center.y - 300 let duration = 3.0 let frameCount = 30 let horizontalAnimation = CAKeyframeAnimation(keyPath: "position.x") horizontalAnimation.keyTimes = (0...frameCount).map({ let dividedTime = Double($0) * duration / Double(frameCount) return NSNumber(value: Double(dividedTime)) }) horizontalAnimation.values = (0...frameCount).map({ let x = sin(CGFloat($0)) * 25 // 振幅: 25 return heartView.center.x + CGFloat(x) }) let animationGroup = CAAnimationGroup() animationGroup.duration = duration animationGroup.animations = [opacityAnimation, verticalAnimation, horizontalAnimation] heartView.layer.add(animationGroup, forKey: nil)
素敵なAnimationが完成しました!!
まとめ
記事書いてて思ったけど "hyperbolic" ってカタカナで ハイパボリック って書いた瞬間急にダサいよね
Firebase Authentication の idToken をサーバーの認証に使い自サービスのUserと紐づけた話(iOS編)
これは iOS Advent Calendar 2017 の17日目の記事です。
はじめに
はじめまして、Flatt という会社でエンジニアをしている machio(まちお) と申します。iOSはまだ書き始めて半年もたたないひよっこですが、これくらいのプレッシャーでもないと勉強しないだろうということで今回エントリーしました。
現在 PinQul というライブショッピングのアプリを運営しています。作り始めた時はiOS初学だった僕ですが、最高のメンターさんと今をときめくFirebaseの力のおかげでこのアプリを2ヶ月で仕上げることができました。
タイトルの通りPinQulでは "Firebase Authentication の idToken をサーバーの認証に使い自サービスのUserと紐づけて使用" しています。
このケースに関する記事は僕の知る限りこの世にあまり存在しないので、Firebaseへの恩返しの意味もこめて、この認証のiOS側の実装の方法とそこで苦労したことについて書こうと思います(Firebaseのcalendarでやれ)。
Firebase is 何?
Firebase は俗にいう mBaaS(Mobile Backend as a Service)の1つです。2014年にGoogleに買収されて一躍有名になりました。詳細は以下に素晴らしいQiitaの記事を添付して割愛させていただきます。
正直 使い勝手が良すぎる & なんでもできてしまう のでもう本格的にサーバーレスで生きていけるなと思ってしまうほど素敵なサービスです(唯一欠点を挙げるとすれば日本語の公式リファレンスが古いのでサンプルコードがたまにアレです)。
Firebase Authenticationとは
公式のリファレンス によると
Firebase Authentication には、バックエンド サービス、使いやすい SDK、アプリでのユーザー認証に使用できる UI ライブラリが用意されています。Firebase Authentication では、パスワード、電話番号、一般的なフェデレーション ID プロバイダ(Google、Facebook、Twitter)などを使用した認証を行うことができます。
です。いろんなログイン方法が簡単に実装できるだけではなく、それらでログインしたUserをFirebaseのsdkを通すだけで一元に管理できるのでとても素敵です。
そしてその方法で認証したUserには idToken なるものが発行されます。これは他のFirebaseのサービスへのアクセスに使用するというのがスタンダードな使い方だと思うのですが、公式のsdkを使ってあれこれするだけで自サービスのサーバーサイドの認証にも使えます。
実際の実装
手順としては
- Firebaseを通してログイン完了
- クライアントでidTokenを取得
- それに該当するJWT(JSON Web Token)をリクエストにのせて送信
- サーバー側でそれを検証することで認証
という感じです。1の実装に関しては 公式リファレンスに詳しく書いてある & すでに世にたくさん記事があるので割愛したいと思います。
1さえ超えてしまえばclientでやらなければいけないことはとても単純です。idTokenはとても簡単に取得することができます。
import Firebase Auth.auth().currentUser?.getIDToken { idToken, error in if let error = error { // エラーハンドリング return } if let idToken = idToken { // idTokenが使える } }
そしてリクエストのheaderにtokenを配置します。PinQulではHTTPクライアントに @ishkawa さんの APIKit を使わせていただいているので、以下のような感じになります。
import APIKit struct HogeRequest: APIKit.Request { var idToken: String … var headerFields: [String: String] { return ["Authorization": "Bearer \(idToken)"] } … }
Auth.auth().currentUser?.getIDToken に渡すコールバックの中でhogehogeしたり、RxつかったりでRequestにidTokenをつけることができれば、あとは送るだけです。とんでもなく簡単です。
あとはこのidTokenを検証した後decodeしてuuidなどの情報を得ます。
単に実装するだけならばこれだけなのですが、PinQulでは一点めんどくさいところがあったので最後にそれに触れて終わりたいと思います。
ログインが必要な機能とそうでない機能がある。。。
これは少しめんどくさかったです。世の中のライブストリーミングのサービスのほとんどがライブを見るだけであればログインをスキップできるようになっています。PinQulも御多分に洩れずそういった実装にしているので認証(idToken)が必要な場合とそうでない場合があります。
なのでリクエストを飛ばす時にそのリクエストに認証が必要かを判断する必要があります。なのでその処理に特化したprotocolを用意して、各Requestにそれを継承させ、認証がいるかいらないかの判断をジェネリクスを使って行わせることにしました。
import APIKit protocol PinQulRequestProtocol: APIKit.Request { … var needLogin: Bool { get set } var idToken: String? { get set } … } struct HogeRequest: PinQulRequestProtocol { var needLogin var idToken: String? … var headerFields: [String: String] { if needLogin, let idToken = idToken { return ["Authorization": "Bearer \(idToken)"] } } … }
実際に認証が必要かを判断してリクエストを送る処理は APIKit の Session にextensionを貼って実装しています。
import APIKit extension Session { class func sendPinQulRequest<T: PinQulRequestProtocol>(pinQulRequest: T, handler: @escaping (Result<T.Response, SessionTaskError>) -> Void) { if pinQulRequest.needLogin { Auth.auth().currentUser?.getIDToken { idToken, error in if let error = error { // エラーハンドリング return } var pinQulRequest = pinQulRequest pinQulRequest.idToken = idToken self.send(pinQulRequest, handler: handler) } } else { self.send(pinQulRequest, handler: handler) } } }
これでiOS側の実装は完璧です!
あとはサーバー側でこれを検証して、正常に認証できたらdecodeしてデータを取り出しましょう。 公式のリファレンスに詳細に書いてあるので問題ないと思います。
ID トークンを検証する | Firebase Authentication
おわりに
Xcodeも今ではとても使いやすくなっていますし、Firebaseなんていうとても素敵なサービスが現れてアプリの開発のハードルがとても下がり、僕のような初学者でもゴリゴリアプリが作れるようになりました。
この記事で利益を被るのは本当にごくわずかな人だと思いますが、最後まで拙い文章をよんでいただきありがとうございます。まだまだ本当に力不足なのでマサカリ大歓迎です。バンバンお願いします!
PinQulは以下のURLからインストールできます。ぜひ手にとって使って見てください!
https://itunes.apple.com/jp/app/pinqul-%E3%83%94%E3%83%B3%E3%82%AF%E3%83%AB/id1275496090?mt=8
多人数でのiOS開発におけるStoryboardとの向き合い方(1画面1Storyboard)
※僕が試行錯誤したストーリーが前半を占めてるので興味がなければ読み飛ばしてください
Storyboardと付き合う
StoryboardやAutoLayoutの出現によって、iOSアプリケーション開発に置けるViewの実装のハードルは確実に低くなりました。弊社のプロダクトでも惜しみなくStoryboardを使用しています。
しかし多くの方がStoryboardとの向き合い方について頭を悩まされてきたと思います。
例として以下のCookPadさんの記事では
1つのStoryboardにViewControllerを複数配置して、Segueで画面遷移を実装(しかしViewは再利用したいのでxibファイルの形式で切り分ける)
という手法が取られています。
一瞬でこれだ!と思い、採用させていただきました。似たようなUIを何度も再利用していくアプリとの親和性は相当高い思います。
この設計で一生開発していくんだと思っていました。最初は、、、
Storyboardの闇に触れる
正直1人で開発を進めていくのであれば、1Storyboardに全て詰め込んでしまうのが一番シンプルで楽かなと思います。
しかし複数人開発になった瞬間にそういうわけにはいきません。 恐るべきことに異なるbranchで同じStoryboardを触るとほぼ確実にconflictが起きます。
しかもこのconflictが厄介で、conflictが発生した瞬間そのファイルはxcodeで開くことができなくなり、コードを見て愚直に1つ1つ問題を解消するしかなくなります。それも実力不足の僕の場合高確率で失敗するので、泣く泣く片方の変更を捨てることもしばしばです。
PRをだすたびに赤く染まるGithubの画面。「いっそのことStoryboardなんて使わなければよかった」、そんな考えが頭をよぎりました。
1画面1Storyboardという選択
そんなある日、某グルメサービスのiOSエンジニアさんとお話する機会があり、この悩みをぶつけたところ、「弊社は1画面につき1Storyboardでやってますね」という答えが帰って来ました。
確かに画面ごとにファイルを分けて画面ごとにタスクを分担すれば、まずconflictは起こりません。最初は「何を言ってるんだ、Segueを殺して何が楽しい」と憤ってた僕ですが、これが最高でした。
以下で詳しく説明します。
具体的な方法
別のStoryboardのViewControllerに移動するのは実は大して難しくありません。
let storyboard = UIStoryboard(name: "Hoge", bundle: nil) // storyboardのインスタンスを名前指定で取得 let nextVC = storyboard.instantiateInitialViewController() as! UIViewController // storyboard内で"is initial"に指定されているViewControllerを取得 self.present(ViewControllerVC, animated: true, completion: nil) // presentする
みたいに3行でいけます。基本的には
- 1ViewControllerにつき1Storyboardを用意してあげる
- 各Storyboardに適切な名前をつける
- 上記のコードで画面遷移!
これで大丈夫でしょう。ですがStoryboard経由でViewControllerを取得してる感が鬱陶しいので、弊社ではもう少し精錬して使用しています。
以下の3STEPです。
1. 以下のようなprotocolを定義します。
protocol StoryboardInstantiable { static var storyboardName: String { get } static var bundle: Bundle? { get } } extension StoryboardInstantiable where Self: UIViewController { static var bundle: Bundle? { return nil } static func instantiate() -> Self { let storyboard = UIStoryboard(name: storyboardName, bundle: bundle) return storyboard.instantiateInitialViewController() as! Self } }
2. これを各ViewControllerに継承させる
storyboardNameをそれぞれで定義しましょう。
class HogeViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() } // 諸々の処理 } extension HogeViewContoller: StoryboardInstantiable { static var storyboardName: String { return "Hoge" } }
3. 遷移のコードは2行に
let nextVC = HogeViewController.instantiate() // これだけでStoryboardに紐づいたHogeViewControllerを取得 self.present(ViewControllerVC, animated: true, completion: nil) // presentする
かなりシュッとしたと思います!ViewControllerベースで次の画面取得してる感が好きです。
終わりに
このやり方はあくまで一例であって、他にも色々な設計があり、それぞれが長所・短所を持っていると思います。その開発の環境に応じた最適なものを採用するのが一番ですが、多人数開発をするとき、この手法をチラッと思い出していただけると本望です。