HealthKitアプリSwiftコードの紹介(6) – ヘルスデータのアップロード

Apple

HealthKitへのアクセス許可

HKHealthStoreクラスのrequestAuthorization(toShare:read:completion:)メソッドは、HealthKitフレームワークを使用してユーザーからアプリへのデータアクセス許可をリクエストするために使用されます。このメソッドは、データの共有(書き込み)と読み取りの種類を指定し、ユーザーに対してそれらのデータへのアクセス許可を求めます。具体的には、次のようなパラメータを取ります:

  • typesToShare: アプリがHealthKitに書き込むことを要求するデータの種類を指定するHKSampleTypeのセットです。例えば、身体活動データ、栄養データ、睡眠データなどが含まれます。
  • typesToRead: アプリがHealthKitから読み取ることを要求するデータの種類を指定するHKObjectTypeのセットです。例えば、心拍数、血圧、歩数などが含まれます。
  • completion: リクエストが完了した後に呼び出されるクロージャです。このクロージャは、次のパラメータを取ります:
    • success: ユーザーがすべての要求されたアクセス許可を与えた場合はtrue、それ以外の場合はfalseです。
    • error: リクエストが失敗した場合に返されるエラーオブジェクトです。

このメソッドを呼び出すと、HealthKitがユーザーにアクセス許可を求める標準的なシステムダイアログが表示されます。ユーザーがアクセス許可を与えると、指定された種類のデータにアクセスできるようになります。データアクセス許可が与えられない場合、successパラメータはfalseになり、適切なエラーオブジェクトがerrorパラメータに渡されます。

このメソッドはopenで修飾されており、サブクラスがオーバーライドできるようになっています。これにより、HKHealthStoreをサブクラス化してカスタム機能を追加したり、振る舞いを変更したりすることができます。

    func authorizeHealthKit(completion: @escaping () -> Void) {
        let healthKitTypesToRead = HealthKitStore.healthKitCharacteristicType + HealthKitStore.healthKitActivityType + HealthKitStore.healthKitEventType + HealthKitStore.healthKitCategoryType
                
        if !HKHealthStore.isHealthDataAvailable() {
            let error = NSError(domain: "", code: 2, userInfo: [NSLocalizedDescriptionKey:"HealthKit is not available in this Device"])
            return;
        }
        
        healthKitStore.requestAuthorization(toShare: nil, read: Set(healthKitTypesToRead)) { [weak self] result, error in
            if let error = error {
                print("Error = \(error)")
                return
            } else if result {
                self?.userDefault.setBool(value: true, key: Constaints.kHealkitAuthorization)
                completion()
            }
        }
    }

バックグラウンドでのアップロード

BackgroundTaskServiceクラス:startObservingDataChanges関数の定義

  • uploadDatas配列にデータが入っている場合に、アップロードを実行する
  • uploadDatas配列にデータを追加するのは、主にstoreDataToDatabase関数
  • getMostRecentHealthKitSample関数、getMostRecentHealthKitSample関数などでデータが取得できた場合、storeDataToDatabase関数を実行する
  • localDatabase.getLatestUpdatedTime()を開始日時、現在日時を終了日時とし、その間のデータを取得する
  • localDatabaseは、RealmをDBとして使用している
    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()
            }
        }
    }

    public func startObservingDataChanges(completion: @escaping () -> Void) {
        self.queue.async { [weak self] in
            // 省略
            self.group.notify(queue: self.queue) { [weak self] in
                guard let self else { return }
                self.updateCharacteristicIdentifiers()
                if self.uploadDatas.count != 0 {
                    let deviceUUID = self.userDefault.stringValue(key: Constaints.kDeviceUUID) ?? ""
                    self.dataSyncAPI.requestBGUploadData(data: self.uploadDatas, deviceUUID: deviceUUID, sendingIdentifiers: self.sendingIdentifier)
                }
                completion()
            }
        }
    }

    private func storeDataToDatabase<T>(type: String, models: [T]) where T: BaseUploadDataModel {
        if models.count != 0 {
            self.uploadDatas.append(UploadFileDataModel(type: type, body: models))
        }
    }

Realmは、モバイルアプリケーションや組み込みシステム向けのデータベースであり、特にiOSやAndroidアプリケーションの開発に広く使用されています。Realmは、軽量かつ高速なオブジェクト指向データベースであり、モバイルアプリケーションのデータの永続化や管理を容易にします。

Realmの特徴は次のとおりです:

  1. オブジェクト指向データベース: Realmは、オブジェクト指向プログラミングの考え方に基づいています。つまり、データベースのエントリはクラスのインスタンスで表され、オブジェクト指向プログラミングの概念(継承、ポリモーフィズムなど)がそのまま適用されます。
  2. 高速なデータアクセス: Realmは非常に高速で効率的なデータアクセスを提供します。このため、大規模なデータセットにも適しています。
  3. クロスプラットフォーム: RealmはiOS、Android、React Native、Unityなど、さまざまなプラットフォームで利用できます。さらに、これらのプラットフォーム間でデータを同期することも可能です。
  4. 簡単なデータベース操作: Realmはシンプルで直感的なAPIを提供し、データベースの作成、更新、削除などの操作を簡単に行うことができます。
  5. リアルタイムデータ同期: Realmはリアルタイムでのデータ同期をサポートしており、複数のデバイス間でデータの同期を行うことができます。

これらの特徴により、Realmはモバイルアプリケーションのデータ管理において非常に便利なツールとなっています。

DataSyncAPIServiceクラス:requestBGUploadData関数の定義

    func requestBGUploadData(data: [UploadFileDataModel], deviceUUID: String, sendingIdentifiers: [String: Date]) {
        self.networkPublisher.uploadBG(UploadFileDataAPI(data: data, deviceUUID: deviceUUID, sendingIdentifiers: sendingIdentifiers))
    }

NetworkPublisherクラス:uploadBG関数の定義

    func uploadBG<T>(_ request: T) where T: BaseAPIProtocol {
        let config = URLSessionConfiguration.background(withIdentifier: "backgroundSession")
        let delegate = URLSessionDelegate()
        let session = URLSession(configuration: config, delegate: delegate, delegateQueue: nil)
        
        delegate.callback = { data, error in
            if let data = data {
                self.writeStringToLocalFile(text: String(data: data, encoding: .utf8) ?? "", fileName: "sent\(Date())")
                if let requestModel = request.requestModel as? RequestUploadFileDataModel {
                    for key in requestModel.sendingIdentifiers.keys {
                        self.localDatabase.updateLatestUpdatedTime(dataType: key, date: requestModel.sendingIdentifiers[key] ?? Date())
                    }
                    self.writeStringToLocalFile(text: "write to DB", fileName: "writedb\(Date())")
                }
                
            } else {
                print("\n Error \(error.debugDescription)")
                self.writeStringToLocalFile(text:"\n Error \(error.debugDescription)", fileName: "sentError\(Date())")
            }
        }
        
        print(String(format: "-------------------\n\n\n\n host: = %@ \n path: = %@ \n method = %@ \n params: = %@ \n time: %@ \n", self.networkConstants.baseURL, request.path, request.method.rawValue, request.params, DateUtil.dateToStr(date: Date(), format: "yyyy/MM/dd HH:mm:ss:sss")))
        
        let boundary = "Boundary-\(UUID().uuidString)"
        var newRequest = self.convertToURLRequest(request: request)
        
        let fileData = request.fileData ?? Data()
        let fileName = request.fileName
        let folderName = fileName.replacingOccurrences(of: ".json.gz", with: "")
        do {
            let tempDirectory = URL(fileURLWithPath: NSTemporaryDirectory())
            let folderURL = tempDirectory.appendingPathComponent(folderName, isDirectory: true)
            try FileManager.default.createDirectory(
                at: folderURL,
                withIntermediateDirectories: true,
                attributes: nil
            )
            let fileURL = folderURL.appendingPathExtension(boundary)
            
            guard let outputStream = OutputStream(url: fileURL, append: false) else {
                throw OutputStream.OutputStreamError.unableToCreateFile(fileURL)
            }
            
            outputStream.open()
            try outputStream.write("--\(boundary)\r\n")
            try outputStream.write("Content-Disposition: form-data; name=\"\(request.fileParam)\"; filename=\"\(fileName)\"\r\n")
            try outputStream.write("Content-Type: application/octet-stream\r\n\r\n")
            try outputStream.write(fileData)
            try outputStream.write("\r\n")
            try outputStream.write("--\(boundary)--\r\n")
            outputStream.close()

            newRequest.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")

            let task = session.uploadTask(with: newRequest, fromFile: fileURL)
            task.resume()
        } catch {
            
        }
    }

フォアグラウンドでのアップロード

HealthKitStoreクラス:startObservingDataChanges関数の定義

    public func startObservingDataChanges(queue: DispatchQueue?, isBackgroundTask: Bool, completion: @escaping () -> Void) {
            self.group.notify(queue: self.queue) { [weak self] in
                guard let self else { return }
                self.updateCharacteristicIdentifiers()
                self.dataSyncService.startSync(data: self.uploadDatas, sendingIdentifiers: self.sendingIdentifier, isBackgroundTask: isBackgroundTask)
                self.sendingIdentifier = [:]
                self.uploadDatas = []
                self.isHealkitLoadingData = false
                completion()
            }
    }

DataSyncServiceクラス:getAndUploadData関数の定義

    func startSync(data: [UploadFileDataModel], sendingIdentifiers: [String: Date], isBackgroundTask: Bool) {
        self.sendingIdentifiers = sendingIdentifiers
        self.currentData = data
        self.getAndUploadData()
        self.syncStatusPublisher.send()
    }

//省略

extension DataSyncService {
    func getAndUploadData() {
        if self.isReadyToSync && self.currentData.count != 0 {
            self.dataSyncAPI.requestUploadData(data: self.currentData, deviceUUID: self.deviceUUID, sendingIdentifiers: self.sendingIdentifiers)
        }
    }
}

DataSyncAPIServiceクラス:requestUploadData関数の定義

    func requestUploadData(data: [UploadFileDataModel], deviceUUID: String, sendingIdentifiers: [String: Date]) {
        Task {
            await self.networkPublisher.upload(UploadFileDataAPI(data: data, deviceUUID: deviceUUID, sendingIdentifiers: sendingIdentifiers))
                .sink(receiveCompletion: { [weak self] completion in
                self?.handleError(completion: completion)
            }, receiveValue: { [weak self] _ in
                self?.uploadDataPublisher.send()
            }).store(in: &tasks)
        }
    }

NetworkPublisherクラス:upload関数の定義

    func upload<T, V>(_ request: T) async -> Future<V, ResponseBaseModel> where T: BaseAPIProtocol, V: ResponseBaseModel, T.ResponseModel == V {
        return Future() { promise in
            let config = URLSessionConfiguration.background(withIdentifier: "backgroundSession")
            let delegate = URLSessionDelegate()
            let session = URLSession(configuration: config, delegate: delegate, delegateQueue: nil)
            
            delegate.callback = { [weak self] data, error in
                if let data = data {
                    print("\n response: \(String(data: data, encoding: .utf8) ?? "")")
                    let response = V.init()
                    response.mapJSONString(data: data)
                    self?.writeStringToLocalFile(text: String(data: data, encoding: .utf8) ?? "", fileName: "sent\(Date())")
                    if let requestModel = request.requestModel as? RequestUploadFileDataModel {
                        for key in requestModel.sendingIdentifiers.keys {
                            self?.localDatabase.updateLatestUpdatedTime(dataType: key, date: requestModel.sendingIdentifiers[key] ?? Date())
                        }
                    }
                    promise(.success(response))
                } else {
                    print("\n Error \(error.debugDescription)")
                    promise(.failure(ResponseBaseModel()))
                }
            }
            
            print(String(format: "-------------------\n\n\n\n host: = %@ \n path: = %@ \n method = %@ \n params: = %@ \n time: %@ \n", self.networkConstants.baseURL, request.path, request.method.rawValue, request.params, DateUtil.dateToStr(date: Date(), format: "yyyy/MM/dd HH:mm:ss:sss")))
            
            let boundary = "Boundary-\(UUID().uuidString)"
            var newRequest = self.convertToURLRequest(request: request)
            let fileData = request.fileData ?? Data()
            let fileName = request.fileName
            let folderName = fileName.replacingOccurrences(of: ".json.gz", with: "")
            do {
                let tempDirectory = URL(fileURLWithPath: NSTemporaryDirectory())
                let folderURL = tempDirectory.appendingPathComponent(folderName, isDirectory: true)
                try FileManager.default.createDirectory(
                    at: folderURL,
                    withIntermediateDirectories: true,
                    attributes: nil
                )
                let fileURL = folderURL.appendingPathExtension(boundary)
                
                guard let outputStream = OutputStream(url: fileURL, append: false) else {
                    throw OutputStream.OutputStreamError.unableToCreateFile(fileURL)
                }
                
                outputStream.open()
                try outputStream.write("--\(boundary)\r\n")
                try outputStream.write("Content-Disposition: form-data; name=\"\(request.fileParam)\"; filename=\"\(fileName)\"\r\n")
                try outputStream.write("Content-Type: application/octet-stream\r\n\r\n")
                try outputStream.write(fileData)
                try outputStream.write("\r\n")
                try outputStream.write("--\(boundary)--\r\n")
                outputStream.close()

                newRequest.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")

                let task = session.uploadTask(with: newRequest, fromFile: fileURL)
                task.resume()
                self.writeStringToLocalFile(text: "  task.resume()", fileName: "taskresume\(Date())")
            } catch {
                
            }
        }
    }

関連記事

カテゴリー

アーカイブ