ParseCareKit

CareKitImage


Documentation Tuturiol Xcode 13.3+ ci release Coverage License


使用风险自负。不保证符合 HIPAA 标准,我们不对您的任何数据处理不当负责

此框架是一个 API,用于使用 Parse-Swift 同步 CareKit 数据和 parse-server。 要了解更多关于如何使用 ParseCareKit 的信息,请查看 API 文档 以及 README 的其余部分。

对于后端,建议使用 parse-hipaa,它是一个开箱即用的符合 HIPAA 标准的 Parse/Postgres 或 Parse/Mongo 服务器,并带有 Parse Dashboard。 由于 parse-hipaa 是一个 parse-server,因此它可以用于 iOSAndroid 和基于 Web 的应用程序。 诸如 GraphQLRESTJS 等 API 也可以在 parse-hipaa 中启用,以便使用其他语言构建应用程序。 有关详细信息,请参阅 Parse SDK 文档。 parse-hipaa docker 镜像包含符合 HIPAA 标准的必要数据库审计和日志记录。

您还可以将 ParseCareKit 与任何 parse-server 设置一起使用。如果您决定使用自己的 parse-server,强烈建议将以下 CloudCode 添加到服务器的“cloud”文件夹中,以确保创建必要的类和字段,并确保推送实体的唯一性。此外,您应该按照说明设置额外的索引以优化查询。请注意,CareKit 数据极其敏感,您有责任确保您的 parse-server 符合 HIPAA 标准。

以下 CareKit 实体与 Parse 表/类同步

ParseCareKit 使属于同一用户的 iOS、watchOS、macOS 设备能够使用 ParseLiveQuery 进行反应式同步,而无需推送通知,前提是 LiveQuery 服务器已配置

带有 ParseCareKit 的 CareKit 示例应用

一个示例应用程序,CareKitSample-ParseCareKit,连接到前面提到的 parse-hipaa,并演示了如何使用 ParseCareKit 轻松地将 CareKit 数据同步到云端。

带有服务器连接信息的 ParseCareKit.plist

ParseCareKit 带有一个辅助方法 PCKUtility.configureParse(),它可以帮助应用程序轻松连接到您的 parse-server。 为了利用该辅助方法,请将 ParseCareKit.plist 文件复制到 Xcode 项目中的“Supporting Files”文件夹中。 务必将 ApplicationIDServer 更改为服务器的正确值。 只需在 AppDelegate.swift 中的 didFinishLaunchingWithOptions 中添加以下内容即可

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {

//Pulls from ParseCareKit.plist to connect to server
PCKUtility.configureParse() 

//If you need certificate pinning:
PCKUtility.configureParse { (challenge, completionHandler) in
    //Return how you want to handle the challenge. See docs for more information.
    completionHandler(.performDefaultHandling, nil)
}

哪个版本的 ParseCareKit 适合您的需求?

请注意,建议使用向量时钟 (ParseRemote) 而不是挂钟时间 (ParseSynchronizedStoreManager),因为后者可能会遇到更多同步问题。如果您选择走挂钟路线,我建议您的应用程序适合每个用户一台设备,以减少潜在的同步问题。您可以通过查看 向量时钟来了解有关向量时钟工作原理的更多信息。

安装 ParseCareKit

Swift 包管理器 (SPM)

可以通过 SPM 安装 ParseCareKit。打开现有项目或创建一个新的 Xcode 项目,然后导航到 File > Swift Packages > Add Package Dependency。输入 url https://github.com/netreconlab/ParseCareKit 并点击 Next。选择 main 分支,然后在下一个屏幕上,选中该包。

注意:ParseCareKit 包括来自 CareKit 的 main 分支的 CareKitStore(它是一个依赖项),因此无需将 CareKitStore 添加到您的应用程序。如果您需要 CareKit 的其余部分,您只需要通过 SPM 添加 CareKitCareKitUI。任何时候需要 ParseCareKit,只需在文件顶部添加 import ParseCareKit 即可。

通过 cocoapods 安装

要通过 cocoapods 安装,请转到 Parse-Objc SDK 分支以获取自述文件。main 分支与 cocoapods 不兼容,因为 CareKit 框架与 Cocoapods 不兼容。

作为框架安装

如果您的项目中已经通过 SPM 或复制添加了 CareKit,您需要将其删除,因为 ParseCareKit 带有兼容版本的 CareKit,并且 CareKit 出现两次的冲突会导致您的应用程序崩溃

设置 Parse Server Remote

有关如何设置 parse-server 的详细信息,请按照 此处的说明进行操作,或查看其详细的指南。请注意,默认情况下,在计算机本地、docker、AWS、Google Cloud 上的标准部署不符合 HIPAA 标准。

使用 ACL 保护云中的患者数据

ParseCareKit 将在保存到 Parse Server 的每个对象上设置默认 ACL,只有创建数据的用户才能进行读/写访问。如果您希望默认情况下具有不同的访问级别,则应在初始化 ParseCareKit 时传递您喜欢的默认 ACL。例如,要使创建的所有数据只能由创建它的用户读取和写入,请执行以下操作

//Set default ACL for all Parse Classes
var newACL = ParseACL()
newACL.publicRead = false
newACL.publicWrite = false        
newACL.setReadAccess(user: user, value: true)
newACL.setWriteAccess(user: user, value: true)
do {
    let parse = try ParseRemote(uuid: UUID(uuidString: "3B5FD9DA-C278-4582-90DC-101C08E7FC98")!,
                                auto: false,
                                subscribeToRemoteUpdates: false,
                                defaultACL: newACL)
} catch {
    print(error.localizedDescription)
}

当授予 CareTeam 或其他实体访问权限时,在决定适当的 ACL 或 Role 时应格外小心。请随时阅读有关 Parse 中 ACLRole 访问的更多信息。 有关详细信息,您的设置应类似于 此处的代码。

同步您的数据

假设您已经熟悉 CareKit(请查看他们的文档以了解详细信息)。 使用 ParseCareKit 非常简单,特别是如果您使用开箱即用的 OCKStore。 如果您使用自定义 OCKStore,您将需要子类化并编写一些额外的代码来将您的护理存储与 parse-server 同步。

使用向量时钟,也就是 CareKit 的 KnowledgeVector (ParseRemote)

ParseCareKit 通过利用 OCKRemoteSynchronizableOCKStore 保持同步。我建议将其作为一个单例,因为它可以处理来自护理存储的所有同步。下面是一个例子

/*Use Clock and OCKRemoteSynchronizable to keep data synced. 
This works with 1 or many devices per patient.*/
let uuid = UUID(uuidString: "3B5FD9DA-C278-4582-90DC-101C08E7FC98")!
let remoteStoreManager = ParseRemote(uuid: uuid, auto: true)
let dataStore = OCKStore(name: "myDataStore", type: .onDisk, remote: remoteStoreManager)
remoteStoreManager.delegate = self //Conform to this protocol if you are writing custom CloudCode in Parse and want to push syncs
remoteStoreManager.parseRemoteDelegate = self //Conform to this protocol to resolve conflicts

传递给 ParseRemoteuuid 用于时钟。 一种允许高度灵活性的可能解决方案是每个用户类型的每个用户都有 1 个这样的对象。 这允许您拥有一个可以是“医生”和“患者”的 ParseUser。 您应该为此特定 ParseUser 的 DoctorPCKPatient 类型生成不同的 uuid。 您可以将所有类型保存到 ParseUser

let userTypeUUIDDictionary = [
"doctor": UUID().uuidString,
"patient": UUID().uuidString
]

//Store the possible uuids for each type
guard var user = User.current else {
    return
}
user.userTypes = userTypeUUIDDictionary //Note that you need to save the UUID in string form to Parse
user.loggedInType = "doctor" 
user.save()

//Start synch with the correct knowlege vector for the particular type of user
let lastLoggedInType = User.current?.loggedInType
let userTypeUUIDString = User.current?.userTypes[lastLoggedInType] as! String
let userTypeUUID = UUID(uuidString: userTypeUUID)!

//Start synching 
let remoteStoreManager = ParseRemote(uuid: userTypeUUID, auto: true)
let dataStore = OCKStore(name: "myDataStore", type: .onDisk, remote: remoteStoreManager)
remoteStoreManager.delegate = self //Conform to this protocol if you are writing custom CloudCode in Parse and want to push syncs
remoteStoreManager.parseRemoteDelegate = self //Conform to this protocol to resolve conflicts

注册为委托,以防 ParseCareKit 需要您的应用程序来更新 CareKit 实体。 ParseCareKit 无权访问您的 CareKitStor,因此如果 ParseCareKit 检测到问题并且需要进行本地更新,您的应用程序将必须进行必要的更新。 注册委托还允许您处理同步冲突。 下面是一个例子

extension AppDelegate: OCKRemoteSynchronizationDelegate, ParseRemoteDelegate {
    func didRequestSynchronization(_ remote: OCKRemoteSynchronizable) {
        print("Implement so ParseCareKit can tell your OCKStore to sync to the cloud")
        //example
        store.synchronize { error in
            print(error?.localizedDescription ?? "Successful sync with remote!")
        }
    }
    
    func remote(_ remote: OCKRemoteSynchronizable, didUpdateProgress progress: Double) {
        print("Completed: \(progress)")
    }

    func chooseConflictResolutionPolicy(_ conflict: OCKMergeConflictDescription, completion: @escaping (OCKMergeConflictResolutionPolicy) -> Void) {
        let conflictPolicy = OCKMergeConflictResolutionPolicy.keepDevice
        completion(conflictPolicy)
    }

    func successfullyPushedDataToCloud(){        
             print("Notified when data is succefully pushed to the cloud")        
         }
    }

自定义与 CareKit 同步的 Parse 实体

有时您需要通过添加与标准 CareKit 实体字段不同的字段来自定义实体。如果您要添加的字段可以转换为字符串,建议您利用 CareKit 实体的 userInfo: [String:String] 字段。为此,您只需使用 extension 扩展您想要自定义的实体。例如,下面展示了如何向 OCKPatient<->Patient 添加字段。

extension Patient: Person {
    var primaryCondition String? {
        guard let userInfo = userInfo else {
            return nil
        }
        return userInfo["CustomPatientUserInfoPrimaryConditionKey"]
    }

    mutating func setPrimaryCondition(_ primaryCondition: String?) {
        var mutableUserInfo = currentUserInfo()

        if let primaryCondition = primaryCondition {
            mutableUserInfo[PatientUserInfoKey.primaryCondition.rawValue] = primaryCondition
        }else{
            mutableUserInfo.removeValue(forKey: PatientUserInfoKey.primaryCondition.rawValue)
        }
        userInfo = mutableUserInfo
    }
}

如果您想创建自己的类型并用它们替换具体的 CareKit 类型,您应该从 ParseCareKit 复制/粘贴相应的代码,例如 PCKPatientPCKContactPCKTask 等。然后,您需要在初始化 ParseRemoteSynchronizingManager 时传递您的自定义结构。方法如下所示。

let updatedConcreteClasses: [PCKStoreClass: PCKSynchronizable] = [
    .patient: CancerPatient()
]

remoteStoreManager = ParseRemote(uuid: uuid, auto: true, replacePCKStoreClasses: updatedConcreteClasses)
dataStore = OCKStore(name: storeName, type: .onDisk(), remote: remoteStoreManager)
remoteStoreManager.delegate = self
remoteStoreManager.parseRemoteDelegate = self

当然,您可以通过实现您自己的 copyCareKit 和 converToCareKit 方法并跳过调用父类方法来进一步自定义。

您还可以将“自定义” Parse 类映射到具体的 OCKStore 类。当您希望在同一个应用程序中同时拥有 DoctorPCKPatient,但希望将它们都在本地映射到 iOS 设备上的 OCKPatient 表时,这非常有用。ParseCareKit 使这变得简单。遵循与上面创建 CancerPatient 相同的过程,但将 kPCKCustomClassKey 键添加到 userInfo,并将 Doctor.className() 作为值。 见下文。

struct Doctor: Patient {
    public var type:String?
    
    func new(from careKitEntity: OCKEntity)->PCKSynchronizable? {
        
        switch careKitEntity {
        case .patient(let entity):
            return Doctor(careKitEntity: entity)
        default:
            os_log("new(with:) The wrong type (%{private}@) of entity was passed", log: .carePlan, type: .error, careKitEntity.entityType.debugDescription)
            completion(nil)
        }
    }
    
    //Add a convienience initializer to to ensure that that the doctor class is always created correctly
    init(careKitEntity: OCKAnyPatient {
        self.init()
        self.new(from: careKitEntity)
        self.userInfo = [kPCKCustomClassKey: self.className]
    }
    
    copyCareKit(_ patientAny: OCKAnyPatient)->Patient? {
        
        guard let doctor = patientAny as? OCKPatient else{
            return nil
        }
        
        super.new(from: doctor, clone: clone)
        self.type = cancerPatient.userInfo?["CustomDoctorUserInfoTypeKey"]
        return seld
    }
    
    func convertToCareKit() -> OCKPatient? {
        guard var partiallyConvertedDoctor = super.convertToCareKit() else{return nil}
        
        var userInfo: [String:String]!
        if partiallyConvertedDoctor.userInfo == nil{
            userInfo = [String:String]()
        }else{
            userInfo = partiallyConvertedDoctor.userInfo!
        }
        if let type = self.type{
            userInfo["CustomDoctorUserInfoTypeKey"] = type
        }
        
        partiallyConvertedDoctor?.userInfo = userInfo
        return partiallyConvertedPatient
    }
}

您永远不应直接将对 ParseCareKit 类的更改保存到 Parse,因为它可能导致您的数据不同步。 而是使用每个类的 convertToCareKit 方法,并使用 CareStore 的 addupdate 方法。 例如,建议在创建要在 CareKit 和 ParseCareKit 之间同步的新项目时使用以下流程。

//Create doctor using CareKit
let newCareKitDoctor = OCKPatient(id: "drJohnson", givenName: "Jane", familyName: "Johnson")

//Initialize new Parse doctor with the CareKit one
_ = Doctor(careKitEntity: newCareKitDoctor){
   doctor in
   
   //Make sure the Doctor was created as Parse doctor
   guard let newParseDoctor = doctor as? Doctor else{
       return
   }
   
   //Make any edits you need to the new doctor
   newParseDoctor.type = "Cancer" //This was a custom value added in the Doctor class 
   newParseDoctor.sex = "Female" //This default from OCKPatient, Doctor has all defaults of it's CareKit counterpart
   
   
   guard let updatedCareKitDoctor = newParseDoctor.convertToCareKit() else {
       completion(nil,nil)
       return
   }
   
   store.addPatient(updatedCareKitDoctor, callbackQueue: .main){
       result in
       
       switch result{
       
       case .success(let doctor):
           print("Successfully add the doctor to the CareStore \(updatedCareKitDoctor)")
           print("CareKit and ParseCareKit will automatically handle syncing this data to the Parse Server")
       case .failure(let error):
           print("Error, couldn't save doctor. \(error)")
       }
   }
}

像 CareKit 中的 OCKQuery 一样查询 Parse

ParseCareKit 提供了一些辅助方法,可以以类似于在 CareKit 中查询的方式查询 Parse。这一点很重要,因为 PCKPatientPCKCarePlanPCKContactPCKTask 是版本化的实体,而 PCKOutcome 是已标记删除的实体。使用常规查询查询上述每个类将返回“版本化”实体的所有版本,或者已标记删除和未标记删除的 PCKOutcome。有关 CareKit 中版本控制如何工作的描述可以在 这里找到。

//To query the most recent version
let query = Patient.query(for: Date())

query.find(callbackQueue: .main){ results in
            
    switch results {

    case .success(let patients):
        print(patients)
    case .failure(let error):
        print(error)
    }
}

对于 PCKOutcome

//To query all current outcomes
let query = Outcome.queryNotDeleted()
query.find(callbackQueue: .main){ results in

    switch results {

    case .success(let outcomes):
        print(outcomes)
    case .failure(let error):
        print(error)
    }
}

自定义 OCKStores

如果您有自定义的 store,并且已经创建了自己的实体,您只需遵循 PCKVersionable (OCKPatient, OCKCarePlan, OCKTask, OCKContact, OCKOutcome) 协议。此外,您还需要遵循 PCKSynchronizable 协议。您可以浏览 ParseCareKit 实体,例如 PCKCarePlan (PCKVersionable) 作为构建您自己的实体的参考。