HealthKitアプリSwiftコードの紹介(2) – バックグラウンドタスクサービスの実装

HealthKit

バックグラウンドタスクサービスの実装

このコードは、バックグラウンドで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メソッドでは、バックグラウンドタスクの登録を行います。handleBackgroundTaskhandleBackgroundRenewTaskメソッドでは、それぞれバックグラウンドタスクの処理を実装しています。

scheduleBackgroundTaskscheduleBackgroundRenewTaskメソッドでは、次回のバックグラウンドタスクの実行をスケジュールします。

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を使用することで、開発者は以下のようなことができます:

  1. 非同期処理の実行: タスクを非同期に実行して、メインスレッドをブロックせずにアプリケーションの応答性を向上させることができます。
  2. キューとタスクの管理: タスクをキューに追加し、GCDが適切なスレッドで実行をスケジュールします。
  3. ディスパッチグループ: 複数の非同期タスクをグループ化して、全てのタスクが完了するのを待つことができます。
  4. セマフォやディスパッチセマフォ: マルチスレッド環境での同期を実現するための手段を提供します。

GCDは、iOSやmacOSのアプリケーション開発において、並列処理や非同期処理を容易に行うための強力なツールとして広く活用されています。

self.queue.async とは?

    public func startObservingDataChanges(completion: @escaping () -> Void) {
        self.queue.async { [weak self] in

self.queue.asyncは、指定されたディスパッチキュー(DispatchQueue)で非同期にクロージャを実行するメソッドです。具体的には、バックグラウンドで非同期に処理を実行するために使用されます。

このメソッドは、次のような流れで動作します:

  1. asyncメソッドは、クロージャを指定されたディスパッチキューに追加します。
  2. システムはそのキューに追加されたクロージャを適切なタイミングで実行します。これにより、メインスレッドや他のキューがブロックされることなく、非同期で処理を実行できます。
  3. クロージャ内のコードが実行され、処理が完了した後、その結果は必要に応じて他の操作やコールバックに渡されます。

つまり、self.queue.asyncを使用することで、バックグラウンドでタスクを非同期に実行し、メインスレッドのブロックを回避しながら、アプリケーションの応答性を向上させることができます。

DispatchGroupとは?

    private var group = DispatchGroup()

DispatchGroupは、複数の非同期タスクが完了するのを待つための仕組みを提供する、Grand Central Dispatch(GCD)のクラスです。複数の非同期処理をグループ化し、すべての処理が完了するのを待つことができます。

具体的には、次のような場面で利用されます:

  1. 複数の非同期処理を並行して実行し、それらの処理がすべて完了した後に何らかの操作を行う場合。
  2. 複数の非同期処理が連鎖的に実行され、すべての処理が完了した後に次の手順に進む場合。

DispatchGroupを使用することで、非同期処理の完了を監視し、処理が全て完了した時点で通知を受け取ることができます。これにより、処理の同期や後続の処理の制御が容易になります。

DispatchGroupenter()メソッドとleave()メソッドは、複数の非同期処理のグループ化と管理を行うためのメソッドです。

  1. enter(): DispatchGroupに対して、新しいタスクの開始を通知します。つまり、グループ内で新しい非同期タスクが開始されたことを示します。
  2. leave(): DispatchGroupに対して、タスクの完了を通知します。つまり、グループ内の非同期タスクが完了したことを示します。

これらのメソッドは、通常、非同期処理の開始時点でenter()を呼び出し、処理が完了した時点でleave()を呼び出します。これにより、DispatchGroupはいつすべてのタスクが完了したかを把握し、その後の処理を適切に行うことができます。

具体的な使い方は以下の通りです:

let group = DispatchGroup()

// 非同期タスクの開始
group.enter()
asyncTask1 {
    // タスクが完了したことを通知
    group.leave()
}

// 非同期タスクの開始
group.enter()
asyncTask2 {
    // タスクが完了したことを通知
    group.leave()
}

// すべての非同期タスクが完了するのを待つ
group.notify(queue: .main) {
    print("すべてのタスクが完了しました")
}

この例では、2つの非同期タスクがasyncTask1asyncTask2であり、それぞれがenter()leave()DispatchGroupに登録されています。notify(queue:)メソッドは、すべてのタスクが完了した時に指定されたキューでクロージャを実行するために使用されます。

関連記事

カテゴリー

アーカイブ