-
-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathPromptedAction.swift
More file actions
132 lines (114 loc) · 4.43 KB
/
Copy pathPromptedAction.swift
File metadata and controls
132 lines (114 loc) · 4.43 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
//
// This source file is part of the My Heart Counts iOS application based on the Stanford Spezi Template Application project
//
// SPDX-FileCopyrightText: 2025 Stanford University
//
// SPDX-License-Identifier: MIT
//
import Foundation
import MyHeartCountsShared
import SFSafeSymbols
@_spi(APISupport)
import Spezi
import SpeziHealthKit
import SpeziSensorKit
extension HomeTab {
@Observable
@MainActor
final class PromptedAction: nonisolated Identifiable, Sendable {
typealias Handler = @Sendable @MainActor (Spezi) async throws -> Void
struct ID: Hashable, Codable, Sendable {
private let value: String
init(_ value: String) {
self.value = value
}
init(from decoder: any Decoder) throws {
let container = try decoder.singleValueContainer()
self.value = try container.decode(String.self)
}
func encode(to encoder: any Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(self.value)
}
}
enum Condition {
/// A condition that evaluates to true, if the number of days that have passed since the study enrollment falls within the specified range.
case daysSinceEnrollment(ClosedRange<Int>)
/// A condition that uses a custom closure.
case custom(@MainActor @Sendable (Spezi) -> Bool)
}
struct Content: Hashable {
let symbol: SFSymbol
let title: LocalizedStringResource
let message: LocalizedStringResource
}
nonisolated let id: ID
/// The action's condition.
///
/// The action will only be prompted to the user if all condutions evaluate to true.
let conditions: [Condition]
let content: Content
private let handler: Handler
// intended for observing when the action was performed, and the UI needs to be updated.
@MainActor private(set) var lastResult: Result<Void, any Error>?
nonisolated init(id: ID, conditions: [Condition], content: Content, handler: @escaping Handler) {
self.id = id
self.conditions = conditions
self.content = content
self.handler = handler
}
func callAsFunction(_ spezi: Spezi) async throws {
lastResult = await Result {
try await handler(spezi)
}
try lastResult!.get() // swiftlint:disable:this force_unwrapping
}
}
}
extension HomeTab.PromptedAction.ID {
static let sensorKit = Self("edu.stanford.MyHeartCounts.HomeTabAction.EnableSensorKit")
static let clinicalRecords = Self("edu.stanford.MyHeartCounts.HomeTabAction.EnableClinicalRecords")
}
extension HomeTab.PromptedAction {
static let allActions: [HomeTab.PromptedAction] = [.enableSensorKit]
static let enableSensorKit = HomeTab.PromptedAction(
id: .sensorKit,
conditions: [
.daysSinceEnrollment(0...21),
.custom { _ in
SensorKit.isAvailable
&& !FeatureFlags.isTakingDemoScreenshots
&& SensorKit.mhcSensors.contains { $0.authorizationStatus == .notDetermined }
}
],
content: .init(
symbol: .waveformPathEcgRectangle,
title: "Enable SensorKit",
message: "ENABLE_SENSORKIT_SUBTITLE"
)
) { spezi in
guard let sensorKit = spezi.module(SensorKit.self) else {
return
}
let result = try await sensorKit.requestAccess(to: SensorKit.mhcSensors)
for sensor in result.authorized {
try? await sensor.startRecording()
}
}
static let enableClinicalRecords = HomeTab.PromptedAction(
id: .clinicalRecords,
conditions: [
.daysSinceEnrollment(0...21),
.custom { spezi in
spezi.module(ClinicalRecordPermissions.self)?.authorizationState == .cancelled
}
],
content: .init(
symbol: HealthRecords.symbol,
title: "Enable Clinical Records",
message: "HEALTH_RECORDS_NUDGE_SUBTITLE"
)
) { spezi in
try await spezi.module(ClinicalRecordPermissions.self)?.askForAuthorization(askAgainIfCancelledPreviously: true)
}
}