バックグラウンドタスクサービスの実装
このコードは、バックグラウンドでHealthKitからヘルスデータを読み出すためのサービスを実装しています。BackgroundTaskServiceProtocol
プロトコルを定義し、バックグラウンドタスクの登録や処理、データの取得などのメソッドを宣言しています。
バックグラウンドでHealthKitからヘルスデータを読み出すSwiftコードの紹介(1)と同じ以下のシーケンス図を用いて説明していきます。
BackgroundTaskService
クラスは、BackgroundTaskServiceProtocol
プロトコルを採用し、実装しています。このクラスは、ユーザーのデフォルト設定、Auth0サービス、データ同期API、ローカルデータベースなどの依存性を受け取ります。また、バックグラウンドで実行されるタスクの管理や、HealthKitからのデータ取得などを行います。
import BackgroundTasks
import UserNotifications
import HealthKit
protocol BackgroundTaskServiceProtocol {
func registerBackgroundTasks()
func scheduleBackgroundTask()
func scheduleBackgroundRenewTask()
}
class BackgroundTaskService: NSObject, BackgroundTaskServiceProtocol {
private var userDefault: UserDefaultUtilsProtocol
private var dataSyncAPI: DataSyncAPIServiceProtocol
private var auth0: Auth0ServiceProtocol
private var localDatabase: LocalDatabaseProtocol
private var group = DispatchGroup()
private var uploadDatas: [UploadFileDataModel] = []
private var sendingIdentifier: [String: Date] = [:]
private var activeQueries: [HKQuery] = []
private var blockIdentifier: [String: Bool] = [:]
private let queue: DispatchQueue = DispatchQueue(label: "bgTask")
private let healthKit: HKHealthStore = HKHealthStore()
init(userDefault: UserDefaultUtilsProtocol, auth0: Auth0ServiceProtocol, dataSyncAPI: DataSyncAPIServiceProtocol, localDatabase: LocalDatabaseProtocol) {
self.userDefault = userDefault
self.auth0 = auth0
self.dataSyncAPI = dataSyncAPI
self.localDatabase = localDatabase
}
func registerBackgroundTasks() {
BGTaskScheduler.shared.register(forTaskWithIdentifier: "bgTask", using: nil) { task in
self.handleBackgroundTask(task: task as! BGProcessingTask)
}
BGTaskScheduler.shared.register(forTaskWithIdentifier: "bgRenewTask", using: nil) { task in
self.handleBackgroundRenewTask(task: task as! BGProcessingTask)
}
}
func handleBackgroundRenewTask(task: BGProcessingTask) {
let queue: DispatchQueue = DispatchQueue(label: "bgRenewTask")
queue.async { [weak self] in
guard let self else { return }
self.auth0.renewBGAccessToken() {
task.setTaskCompleted(success: true)
self.scheduleBackgroundRenewTask()
}
}
task.expirationHandler = {
// Handle task expiration
queue.async { [weak self] in
guard let self else { return }
task.setTaskCompleted(success: false)
self.scheduleBackgroundRenewTask()
}
}
}
func handleBackgroundTask(task: BGProcessingTask) {
self.queue.async { [weak self] in
guard let self else { return }
self.startObservingDataChanges() {
self.writeStringToLocalFile(text: "Success get HealthKit data", fileName: "HealthKit\(Date())")
task.setTaskCompleted(success: true)
self.scheduleBackgroundTask()
}
}
task.expirationHandler = {
// Handle task expiration
self.queue.async { [weak self] in
guard let self else { return }
task.setTaskCompleted(success: false)
self.scheduleBackgroundTask()
}
}
}
func scheduleBackgroundRenewTask() {
// 省略
}
func scheduleBackgroundTask() {
// 省略
}
func updateCharacteristicIdentifiers() {
// 省略
}
public func startObservingDataChanges(completion: @escaping () -> Void) {
// 省略
}
public func getMostRecentHealthKitSample<T>(for sampleType: HKSampleType,
completion: @escaping ([T], Date, Error?) -> Void) where T: HKSample {
// 省略
}
private func queryECGSample(ecgSample: HKElectrocardiogram, ecgId: String) {
// 省略
}
private func updateEndTime(identifier: String, endDate: Date) {
// 省略
}
private func storeDataToDatabase<T>(model: T) where T: HealthKitModelProtocol {
// 省略
}
private func storeDataToDatabase<T>(type: String, models: [T]) where T: BaseUploadDataModel {
// 省略
}
func writeStringToLocalFile(text: String, fileName: String) {
// 省略
}
func getDocumentsDirectory() -> URL {
// 省略
}
private func calculateNextExecutionDate() -> Date {
// 省略
}
}
registerBackgroundTasks
メソッドでは、バックグラウンドタスクの登録を行います。handleBackgroundTask
とhandleBackgroundRenewTask
メソッドでは、それぞれバックグラウンドタスクの処理を実装しています。
scheduleBackgroundTask
とscheduleBackgroundRenewTask
メソッドでは、次回のバックグラウンドタスクの実行をスケジュールします。
startObservingDataChanges
メソッドでは、HealthKitからのデータ変更を監視し、データの取得や処理を行います。
getMostRecentHealthKitSample
メソッドでは、指定されたサンプルタイプの最新のデータを取得します。
また、queryECGSample
メソッドでは、心電図のデータ取得を行います。
その他にも、データの保存やファイルへの書き込み、次回の実行日時の計算など、様々な機能が実装されています。
BGTaskScheduler
へのバックグランドタスク登録
BGTaskScheduler
は、iOSのバックグラウンドタスクを管理するためのクラスであり、iOS 13以降で利用可能です。このコードでは、BackgroundTaskService
クラス内のregisterBackgroundTasks
メソッドでBGTaskScheduler
が利用されています。
func registerBackgroundTasks() {
BGTaskScheduler.shared.register(forTaskWithIdentifier: "bgTask", using: nil) { task in
self.handleBackgroundTask(task: task as! BGProcessingTask)
}
BGTaskScheduler.shared.register(forTaskWithIdentifier: "bgRenewTask", using: nil) { task in
self.handleBackgroundRenewTask(task: task as! BGProcessingTask)
}
}
BGTaskScheduler
はシングルトンではなく、このコードの中で明示的にインスタンス化されている部分はありません。ただし、BGTaskScheduler.shared
という静的プロパティを介してアクセスされています。これは、アプリ全体で唯一のBGTaskScheduler
のインスタンスを返す便利な方法です。
DispatchQueueとは?
private let queue: DispatchQueue = DispatchQueue(label: "bgTask")
DispatchQueue
は、Grand Central Dispatch(GCD)の一部であり、並列処理や非同期処理を管理するためのクラスです。DispatchQueue
を使用することで、タスクをキューに追加し、適切なスレッドで実行されるように制御することができます。
このコードでは、private let queue: DispatchQueue = DispatchQueue(label: "bgTask")
で、DispatchQueue
のインスタンスを生成しています。ラベルパラメータ "bgTask"
を指定していますが、これはキューのデバッグや識別のためのものであり、実際の処理には関与しません。このキューは、バックグラウンドで実行されるタスクを管理するために使用されます。
GCDとは?
GCD(Grand Central Dispatch)は、macOSやiOSなどのAppleのプラットフォームで使用されるマルチスレッドプログラミングのためのフレームワークです。GCDは、複数のタスクや処理を非同期で実行し、効率的にスレッドを管理するためのAPIを提供します。
GCDを使用することで、開発者は以下のようなことができます:
- 非同期処理の実行: タスクを非同期に実行して、メインスレッドをブロックせずにアプリケーションの応答性を向上させることができます。
- キューとタスクの管理: タスクをキューに追加し、GCDが適切なスレッドで実行をスケジュールします。
- ディスパッチグループ: 複数の非同期タスクをグループ化して、全てのタスクが完了するのを待つことができます。
- セマフォやディスパッチセマフォ: マルチスレッド環境での同期を実現するための手段を提供します。
GCDは、iOSやmacOSのアプリケーション開発において、並列処理や非同期処理を容易に行うための強力なツールとして広く活用されています。
self.queue.async とは?
public func startObservingDataChanges(completion: @escaping () -> Void) {
self.queue.async { [weak self] in
self.queue.async
は、指定されたディスパッチキュー(DispatchQueue)で非同期にクロージャを実行するメソッドです。具体的には、バックグラウンドで非同期に処理を実行するために使用されます。
このメソッドは、次のような流れで動作します:
async
メソッドは、クロージャを指定されたディスパッチキューに追加します。- システムはそのキューに追加されたクロージャを適切なタイミングで実行します。これにより、メインスレッドや他のキューがブロックされることなく、非同期で処理を実行できます。
- クロージャ内のコードが実行され、処理が完了した後、その結果は必要に応じて他の操作やコールバックに渡されます。
つまり、self.queue.async
を使用することで、バックグラウンドでタスクを非同期に実行し、メインスレッドのブロックを回避しながら、アプリケーションの応答性を向上させることができます。
DispatchGroupとは?
private var group = DispatchGroup()
DispatchGroup
は、複数の非同期タスクが完了するのを待つための仕組みを提供する、Grand Central Dispatch(GCD)のクラスです。複数の非同期処理をグループ化し、すべての処理が完了するのを待つことができます。
具体的には、次のような場面で利用されます:
- 複数の非同期処理を並行して実行し、それらの処理がすべて完了した後に何らかの操作を行う場合。
- 複数の非同期処理が連鎖的に実行され、すべての処理が完了した後に次の手順に進む場合。
DispatchGroup
を使用することで、非同期処理の完了を監視し、処理が全て完了した時点で通知を受け取ることができます。これにより、処理の同期や後続の処理の制御が容易になります。
DispatchGroup
のenter()
メソッドとleave()
メソッドは、複数の非同期処理のグループ化と管理を行うためのメソッドです。
enter()
:DispatchGroup
に対して、新しいタスクの開始を通知します。つまり、グループ内で新しい非同期タスクが開始されたことを示します。leave()
:DispatchGroup
に対して、タスクの完了を通知します。つまり、グループ内の非同期タスクが完了したことを示します。
これらのメソッドは、通常、非同期処理の開始時点でenter()
を呼び出し、処理が完了した時点でleave()
を呼び出します。これにより、DispatchGroup
はいつすべてのタスクが完了したかを把握し、その後の処理を適切に行うことができます。
具体的な使い方は以下の通りです:
let group = DispatchGroup()
// 非同期タスクの開始
group.enter()
asyncTask1 {
// タスクが完了したことを通知
group.leave()
}
// 非同期タスクの開始
group.enter()
asyncTask2 {
// タスクが完了したことを通知
group.leave()
}
// すべての非同期タスクが完了するのを待つ
group.notify(queue: .main) {
print("すべてのタスクが完了しました")
}
この例では、2つの非同期タスクがasyncTask1
とasyncTask2
であり、それぞれがenter()
とleave()
でDispatchGroup
に登録されています。notify(queue:)
メソッドは、すべてのタスクが完了した時に指定されたキューでクロージャを実行するために使用されます。