@craft-native/ios 0.0.150 → 0.0.152
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -0
- package/dist/cli.js +6 -1
- package/dist/index.d.ts +9 -0
- package/dist/index.js +6 -1
- package/package.json +1 -1
- package/templates/CraftApp.swift +56 -1
- package/templates/CraftHeartRate.swift +699 -0
- package/templates/CraftNativeActions.swift +16 -0
- package/templates/CraftNativeScreen.swift +4 -263
- package/templates/CraftNativeServices.swift +6 -3
- package/templates/CraftWebAudio.swift +2269 -0
package/README.md
CHANGED
|
@@ -179,6 +179,7 @@ Edit `craft.config.json` in your iOS project:
|
|
|
179
179
|
"enableNFC": true,
|
|
180
180
|
"enableHealthKit": false,
|
|
181
181
|
"enableAppleMusic": false,
|
|
182
|
+
"enableHeartRate": false,
|
|
182
183
|
"enableScreenCapture": true,
|
|
183
184
|
"iosVersion": "15.0",
|
|
184
185
|
"teamId": ""
|
|
@@ -558,6 +559,35 @@ window.addEventListener('craftMusicChange', (event) => render(event.detail));
|
|
|
558
559
|
// CraftFakeMusicPlayer -bool YES`) for three made-up songs that play (counting as other audio) and skip.
|
|
559
560
|
// Simulator builds only; a device always talks to the Music app.
|
|
560
561
|
|
|
562
|
+
// Heart rate (enableHeartRate): a Bluetooth strap, or a watch broadcasting
|
|
563
|
+
// its heart rate, through the standard Heart Rate Service (0x180D) and its
|
|
564
|
+
// Heart Rate Measurement (0x2A37): bpm, sensor contact, energy and RR
|
|
565
|
+
// intervals. The app gets the `bluetooth-central` background mode, so
|
|
566
|
+
// readings keep coming during a workout with the screen locked. iOS asks
|
|
567
|
+
// for Bluetooth on the first scan or connect (or requestAuthorization()); word
|
|
568
|
+
// it with usageDescriptions.NSBluetoothAlwaysUsageDescription.
|
|
569
|
+
window.addEventListener('craftHeartRate', (event) => {
|
|
570
|
+
// { type: 'device', device: { id, name, rssi } } a scan found a strap
|
|
571
|
+
// { type: 'reading', bpm, rrIntervals, contact, energyExpended, timestamp, deviceId }
|
|
572
|
+
// { type: 'state', connection, bluetooth, device, lastDevice, bpm, ... }
|
|
573
|
+
});
|
|
574
|
+
await window.craft.heartRate.scan({ timeout: 15 }); // straps nearby, and one already connected
|
|
575
|
+
const strap = (await window.craft.heartRate.getDevices())[0]; // strongest signal first
|
|
576
|
+
await window.craft.heartRate.connect(strap.id); // remembered; rejects TIMEOUT after 15 s while still asking
|
|
577
|
+
await window.craft.heartRate.reconnect(); // next time: the remembered strap, no scan
|
|
578
|
+
// A strap that drops out is asked for again (connection 'reconnecting')
|
|
579
|
+
// until disconnect({ forget }) lets it go.
|
|
580
|
+
await window.craft.heartRate.startRecording(); // every reading kept natively, app closed or not
|
|
581
|
+
await window.craft.heartRate.pauseRecording(); // resumeRecording() / getRecordingState()
|
|
582
|
+
const { samples } = await window.craft.heartRate.stopRecording(); // [{ timestamp, bpm, rr? }]
|
|
583
|
+
// Native screens call the same service as the HeartRate module (capability
|
|
584
|
+
// `heartRate`), and hear the same craftHeartRate events. Or use `heartRate`
|
|
585
|
+
// from 'craft-native/mobile' (onReading, onDevice, onState); off a phone it
|
|
586
|
+
// answers { available: false }.
|
|
587
|
+
// The simulator has no Bluetooth: `simctl spawn <device> defaults write
|
|
588
|
+
// <bundle id> CraftFakeHeartRateMonitor -bool YES` stands in a strap ("Simulated
|
|
589
|
+
// HRM") whose readings are encoded and decoded as 0x2A37 payloads. Simulator builds only.
|
|
590
|
+
|
|
561
591
|
// Speech: short spoken cues that duck the person's music instead of stopping
|
|
562
592
|
// it, and play with the silent switch on. Always available (no config flag).
|
|
563
593
|
// Resolves when the utterance ends: true if spoken, false if stopped or
|
package/dist/cli.js
CHANGED
|
@@ -115,6 +115,7 @@ var DEFAULT_CONFIG = {
|
|
|
115
115
|
enableAR: false,
|
|
116
116
|
enableMLKit: false,
|
|
117
117
|
enableAppleMusic: false,
|
|
118
|
+
enableHeartRate: false,
|
|
118
119
|
iosVersion: "16.0",
|
|
119
120
|
watchosVersion: "9.0",
|
|
120
121
|
teamId: "",
|
|
@@ -171,7 +172,7 @@ function renderUsageDescriptions(config) {
|
|
|
171
172
|
[config.enableBackgroundLocation, "NSLocationAlwaysAndWhenInUseUsageDescription", `${config.appName} continues recording your route when the screen is locked or the app is in the background.`],
|
|
172
173
|
[config.enableContacts, "NSContactsUsageDescription", `${config.appName} accesses contacts only when you choose a contact feature.`],
|
|
173
174
|
[config.enableCalendar, "NSCalendarsUsageDescription", `${config.appName} accesses your calendar only when you choose a calendar feature.`],
|
|
174
|
-
[config.enableBluetooth, "NSBluetoothAlwaysUsageDescription", `${config.appName} uses Bluetooth to connect to nearby devices.`],
|
|
175
|
+
[config.enableBluetooth || config.enableHeartRate, "NSBluetoothAlwaysUsageDescription", config.enableHeartRate ? `${config.appName} connects to your heart-rate monitor over Bluetooth to show and record your heart rate during a workout.` : `${config.appName} uses Bluetooth to connect to nearby devices.`],
|
|
175
176
|
[config.enableMotionSensors, "NSMotionUsageDescription", `${config.appName} uses motion data for activity features.`],
|
|
176
177
|
[config.enableNFC, "NFCReaderUsageDescription", `${config.appName} reads NFC tags when you start a scan.`],
|
|
177
178
|
[config.enableLocalNetwork, "NSLocalNetworkUsageDescription", `${config.appName} connects to your own devices on this network.`],
|
|
@@ -254,6 +255,8 @@ function renderBackgroundModes(config) {
|
|
|
254
255
|
modes.add("processing");
|
|
255
256
|
if (config.enablePushNotifications)
|
|
256
257
|
modes.add("remote-notification");
|
|
258
|
+
if (config.enableHeartRate)
|
|
259
|
+
modes.add("bluetooth-central");
|
|
257
260
|
if (!modes.size)
|
|
258
261
|
return "";
|
|
259
262
|
return ` <key>UIBackgroundModes</key>
|
|
@@ -732,9 +735,11 @@ async function init(options) {
|
|
|
732
735
|
cpSync(join(TEMPLATES_DIR, "CraftNativeActions.swift"), join(output, "Sources", "CraftNativeActions.swift"));
|
|
733
736
|
cpSync(join(TEMPLATES_DIR, "CraftHealth.swift"), join(output, "Sources", "CraftHealth.swift"));
|
|
734
737
|
cpSync(join(TEMPLATES_DIR, "CraftMusic.swift"), join(output, "Sources", "CraftMusic.swift"));
|
|
738
|
+
cpSync(join(TEMPLATES_DIR, "CraftHeartRate.swift"), join(output, "Sources", "CraftHeartRate.swift"));
|
|
735
739
|
cpSync(join(TEMPLATES_DIR, "CraftNativeMutation.swift"), join(output, "Sources", "CraftNativeMutation.swift"));
|
|
736
740
|
cpSync(join(TEMPLATES_DIR, "CraftNativeUI.swift"), join(output, "Sources", "CraftNativeUI.swift"));
|
|
737
741
|
cpSync(join(TEMPLATES_DIR, "CraftNativeServices.swift"), join(output, "Sources", "CraftNativeServices.swift"));
|
|
742
|
+
cpSync(join(TEMPLATES_DIR, "CraftWebAudio.swift"), join(output, "Sources", "CraftWebAudio.swift"));
|
|
738
743
|
cpSync(join(TEMPLATES_DIR, "CraftHybrid.swift"), join(output, "Sources", "CraftHybrid.swift"));
|
|
739
744
|
const infoPlistTemplate = readFileSync(join(TEMPLATES_DIR, "Info.plist.template"), "utf-8");
|
|
740
745
|
const infoPlist = infoPlistTemplate.replace(/\{\{APP_NAME\}\}/g, xmlEscape(displayName)).replace(/\{\{BUNDLE_ID\}\}/g, finalBundleId).replace(/\{\{VERSION\}\}/g, config.version || "1.0.0").replace(/\{\{BUILD_NUMBER\}\}/g, config.buildNumber || "1").replace(/\{\{UI_STYLE\}\}/g, renderAppearance(config).interfaceStyle).replace(/\{\{STATUS_BAR_STYLE\}\}/g, renderAppearance(config).statusBarStyle).replace(/\{\{ORIENTATIONS\}\}/g, renderOrientations(config)).replace(/\{\{USAGE_DESCRIPTIONS\}\}/g, renderUsageDescriptions(config)).replace(/\{\{URL_TYPES\}\}/g, renderUrlTypes(config)).replace(/\{\{APP_BOUND_DOMAINS\}\}/g, renderAppBoundDomains(config)).replace(/\{\{BACKGROUND_MODES\}\}/g, renderBackgroundModes(config)).replace(/\{\{BACKGROUND_TASK_IDENTIFIERS\}\}/g, renderBackgroundTaskIdentifiers(config)).replace(/\{\{LAUNCH_SCREEN\}\}/g, renderLaunchScreen(config)).replace(/\{\{NON_EXEMPT_ENCRYPTION\}\}/g, config.usesNonExemptEncryption ? "true" : "false").replace(/\{\{LIVE_ACTIVITY_SUPPORT\}\}/g, config.enableLiveActivities ? ` <key>NSSupportsLiveActivities</key>
|
package/dist/index.d.ts
CHANGED
|
@@ -68,6 +68,15 @@ export interface CraftConfig {
|
|
|
68
68
|
* `craft.music.requestAuthorization()`, never at launch.
|
|
69
69
|
*/
|
|
70
70
|
enableAppleMusic?: boolean;
|
|
71
|
+
/**
|
|
72
|
+
* Read a Bluetooth heart-rate strap, or a watch broadcasting its heart rate
|
|
73
|
+
* (the standard Heart Rate Service), through `craft.heartRate`: scan,
|
|
74
|
+
* connect, live readings with RR intervals, and a recording kept natively.
|
|
75
|
+
* Adds the `bluetooth-central` background mode, so readings keep coming
|
|
76
|
+
* during a workout with the screen locked. iOS asks for Bluetooth the first
|
|
77
|
+
* time the page scans or connects.
|
|
78
|
+
*/
|
|
79
|
+
enableHeartRate?: boolean;
|
|
71
80
|
/**
|
|
72
81
|
* Purpose strings in the app's own words, by Info.plist key
|
|
73
82
|
* (`{ NSAppleMusicUsageDescription: 'Shows the song playing during a workout.' }`).
|
package/dist/index.js
CHANGED
|
@@ -170,6 +170,7 @@ var DEFAULT_CONFIG = {
|
|
|
170
170
|
enableAR: false,
|
|
171
171
|
enableMLKit: false,
|
|
172
172
|
enableAppleMusic: false,
|
|
173
|
+
enableHeartRate: false,
|
|
173
174
|
iosVersion: "16.0",
|
|
174
175
|
watchosVersion: "9.0",
|
|
175
176
|
teamId: "",
|
|
@@ -226,7 +227,7 @@ function renderUsageDescriptions(config) {
|
|
|
226
227
|
[config.enableBackgroundLocation, "NSLocationAlwaysAndWhenInUseUsageDescription", `${config.appName} continues recording your route when the screen is locked or the app is in the background.`],
|
|
227
228
|
[config.enableContacts, "NSContactsUsageDescription", `${config.appName} accesses contacts only when you choose a contact feature.`],
|
|
228
229
|
[config.enableCalendar, "NSCalendarsUsageDescription", `${config.appName} accesses your calendar only when you choose a calendar feature.`],
|
|
229
|
-
[config.enableBluetooth, "NSBluetoothAlwaysUsageDescription", `${config.appName} uses Bluetooth to connect to nearby devices.`],
|
|
230
|
+
[config.enableBluetooth || config.enableHeartRate, "NSBluetoothAlwaysUsageDescription", config.enableHeartRate ? `${config.appName} connects to your heart-rate monitor over Bluetooth to show and record your heart rate during a workout.` : `${config.appName} uses Bluetooth to connect to nearby devices.`],
|
|
230
231
|
[config.enableMotionSensors, "NSMotionUsageDescription", `${config.appName} uses motion data for activity features.`],
|
|
231
232
|
[config.enableNFC, "NFCReaderUsageDescription", `${config.appName} reads NFC tags when you start a scan.`],
|
|
232
233
|
[config.enableLocalNetwork, "NSLocalNetworkUsageDescription", `${config.appName} connects to your own devices on this network.`],
|
|
@@ -309,6 +310,8 @@ function renderBackgroundModes(config) {
|
|
|
309
310
|
modes.add("processing");
|
|
310
311
|
if (config.enablePushNotifications)
|
|
311
312
|
modes.add("remote-notification");
|
|
313
|
+
if (config.enableHeartRate)
|
|
314
|
+
modes.add("bluetooth-central");
|
|
312
315
|
if (!modes.size)
|
|
313
316
|
return "";
|
|
314
317
|
return ` <key>UIBackgroundModes</key>
|
|
@@ -787,9 +790,11 @@ async function init(options) {
|
|
|
787
790
|
cpSync(join(TEMPLATES_DIR, "CraftNativeActions.swift"), join(output, "Sources", "CraftNativeActions.swift"));
|
|
788
791
|
cpSync(join(TEMPLATES_DIR, "CraftHealth.swift"), join(output, "Sources", "CraftHealth.swift"));
|
|
789
792
|
cpSync(join(TEMPLATES_DIR, "CraftMusic.swift"), join(output, "Sources", "CraftMusic.swift"));
|
|
793
|
+
cpSync(join(TEMPLATES_DIR, "CraftHeartRate.swift"), join(output, "Sources", "CraftHeartRate.swift"));
|
|
790
794
|
cpSync(join(TEMPLATES_DIR, "CraftNativeMutation.swift"), join(output, "Sources", "CraftNativeMutation.swift"));
|
|
791
795
|
cpSync(join(TEMPLATES_DIR, "CraftNativeUI.swift"), join(output, "Sources", "CraftNativeUI.swift"));
|
|
792
796
|
cpSync(join(TEMPLATES_DIR, "CraftNativeServices.swift"), join(output, "Sources", "CraftNativeServices.swift"));
|
|
797
|
+
cpSync(join(TEMPLATES_DIR, "CraftWebAudio.swift"), join(output, "Sources", "CraftWebAudio.swift"));
|
|
793
798
|
cpSync(join(TEMPLATES_DIR, "CraftHybrid.swift"), join(output, "Sources", "CraftHybrid.swift"));
|
|
794
799
|
const infoPlistTemplate = readFileSync(join(TEMPLATES_DIR, "Info.plist.template"), "utf-8");
|
|
795
800
|
const infoPlist = infoPlistTemplate.replace(/\{\{APP_NAME\}\}/g, xmlEscape(displayName)).replace(/\{\{BUNDLE_ID\}\}/g, finalBundleId).replace(/\{\{VERSION\}\}/g, config.version || "1.0.0").replace(/\{\{BUILD_NUMBER\}\}/g, config.buildNumber || "1").replace(/\{\{UI_STYLE\}\}/g, renderAppearance(config).interfaceStyle).replace(/\{\{STATUS_BAR_STYLE\}\}/g, renderAppearance(config).statusBarStyle).replace(/\{\{ORIENTATIONS\}\}/g, renderOrientations(config)).replace(/\{\{USAGE_DESCRIPTIONS\}\}/g, renderUsageDescriptions(config)).replace(/\{\{URL_TYPES\}\}/g, renderUrlTypes(config)).replace(/\{\{APP_BOUND_DOMAINS\}\}/g, renderAppBoundDomains(config)).replace(/\{\{BACKGROUND_MODES\}\}/g, renderBackgroundModes(config)).replace(/\{\{BACKGROUND_TASK_IDENTIFIERS\}\}/g, renderBackgroundTaskIdentifiers(config)).replace(/\{\{LAUNCH_SCREEN\}\}/g, renderLaunchScreen(config)).replace(/\{\{NON_EXEMPT_ENCRYPTION\}\}/g, config.usesNonExemptEncryption ? "true" : "false").replace(/\{\{LIVE_ACTIVITY_SUPPORT\}\}/g, config.enableLiveActivities ? ` <key>NSSupportsLiveActivities</key>
|
package/package.json
CHANGED
package/templates/CraftApp.swift
CHANGED
|
@@ -1119,6 +1119,9 @@ struct CraftConfig: Codable {
|
|
|
1119
1119
|
var enableMLKit: Bool = false
|
|
1120
1120
|
/// The Music app's song and transport through `craft.music` (CraftMusic.swift).
|
|
1121
1121
|
var enableAppleMusic: Bool = false
|
|
1122
|
+
/// A Bluetooth heart-rate strap (or a watch broadcasting its heart rate)
|
|
1123
|
+
/// through `craft.heartRate` (CraftHeartRate.swift).
|
|
1124
|
+
var enableHeartRate: Bool = false
|
|
1122
1125
|
var devServerURL: String? = nil
|
|
1123
1126
|
var trustedOrigins: [String] = []
|
|
1124
1127
|
/// The longest the launch splash stays up when the page neither says it is
|
|
@@ -1641,6 +1644,16 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
1641
1644
|
if config.enableMotionSensors {
|
|
1642
1645
|
motionManager = CMMotionManager()
|
|
1643
1646
|
}
|
|
1647
|
+
if config.enableHeartRate {
|
|
1648
|
+
// The strap's readings and connection changes reach the page,
|
|
1649
|
+
// and through sendToWeb every native screen too. False once
|
|
1650
|
+
// this coordinator is gone: native screens still hear them.
|
|
1651
|
+
CraftHeartRate.shared.pageSink = { [weak self] detail in
|
|
1652
|
+
guard let self else { return false }
|
|
1653
|
+
self.sendToWeb("craftHeartRate", data: detail)
|
|
1654
|
+
return true
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1644
1657
|
if config.enableWatchApp {
|
|
1645
1658
|
setupWatchConnectivity()
|
|
1646
1659
|
}
|
|
@@ -2213,6 +2226,22 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
2213
2226
|
} else {
|
|
2214
2227
|
rejectCallback(callbackId, error: "Apple Music is disabled", code: "CAPABILITY_DISABLED")
|
|
2215
2228
|
}
|
|
2229
|
+
// MARK: - Heart rate (CraftHeartRate.swift)
|
|
2230
|
+
// Gated on enableHeartRate. One action for every method, as a
|
|
2231
|
+
// native screen's HeartRate module calls them; asking for
|
|
2232
|
+
// Bluetooth has its own, which waits on the person.
|
|
2233
|
+
case "heartRate", "heartRateRequestAuthorization":
|
|
2234
|
+
if !config.enableHeartRate {
|
|
2235
|
+
rejectCallback(callbackId, error: "Heart rate is disabled", code: "CAPABILITY_DISABLED")
|
|
2236
|
+
} else {
|
|
2237
|
+
let method = action == "heartRate" ? body["method"] as? String ?? "" : "requestAuthorization"
|
|
2238
|
+
CraftHeartRate.shared.perform(method: method, args: [body["options"] as? [String: Any] ?? [:]]) { [weak self] result in
|
|
2239
|
+
switch result {
|
|
2240
|
+
case .success(let value): self?.resolveCallback(callbackId, result: value)
|
|
2241
|
+
case .failure(let failure): self?.rejectCallback(callbackId, error: failure.message, code: failure.code)
|
|
2242
|
+
}
|
|
2243
|
+
}
|
|
2244
|
+
}
|
|
2216
2245
|
// MARK: - Orientation Lock
|
|
2217
2246
|
case "lockOrientation":
|
|
2218
2247
|
if config.enableOrientationLock {
|
|
@@ -3198,6 +3227,7 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
3198
3227
|
flashlight: true,
|
|
3199
3228
|
brightness: true,
|
|
3200
3229
|
music: \(config.enableAppleMusic),
|
|
3230
|
+
heartRate: \(config.enableHeartRate),
|
|
3201
3231
|
speech: true,
|
|
3202
3232
|
network: true,
|
|
3203
3233
|
deviceInfo: true,
|
|
@@ -4737,6 +4767,7 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
4737
4767
|
var personFacing = {
|
|
4738
4768
|
requestHealthAuthorization: true,
|
|
4739
4769
|
musicRequestAuthorization: true,
|
|
4770
|
+
heartRateRequestAuthorization: true,
|
|
4740
4771
|
requestPermission: true,
|
|
4741
4772
|
signInWithApple: true,
|
|
4742
4773
|
dialogAlert: true,
|
|
@@ -4951,6 +4982,30 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
4951
4982
|
},
|
|
4952
4983
|
observe: function(enabled) { return craft._invoke('musicObserve', {enabled: enabled !== false}); }
|
|
4953
4984
|
};
|
|
4985
|
+
// A Bluetooth heart-rate strap, or a watch broadcasting its
|
|
4986
|
+
// heart rate. Readings, devices found by scan() and connection
|
|
4987
|
+
// changes arrive as craftHeartRate events ({type: 'reading' |
|
|
4988
|
+
// 'device' | 'state'}); startRecording() keeps every reading
|
|
4989
|
+
// natively, screen locked or app closed, until stopRecording().
|
|
4990
|
+
(function() {
|
|
4991
|
+
function heartRate(method, options) { return craft._invoke('heartRate', {method: method, options: options || {}}); }
|
|
4992
|
+
craft.heartRate = {
|
|
4993
|
+
getState: function() { return heartRate('getState'); },
|
|
4994
|
+
requestAuthorization: function() { return craft._invoke('heartRateRequestAuthorization'); },
|
|
4995
|
+
scan: function(options) { return heartRate('scan', {timeout: options && typeof options.timeout === 'number' ? options.timeout : null}); },
|
|
4996
|
+
stopScan: function() { return heartRate('stopScan'); },
|
|
4997
|
+
getDevices: function() { return heartRate('getDevices'); },
|
|
4998
|
+
connect: function(id) { return heartRate('connect', {id: typeof id === 'string' ? id : id && id.id}); },
|
|
4999
|
+
reconnect: function() { return heartRate('reconnect'); },
|
|
5000
|
+
disconnect: function(options) { return heartRate('disconnect', {forget: !!(options && options.forget)}); },
|
|
5001
|
+
startRecording: function() { return heartRate('startRecording'); },
|
|
5002
|
+
pauseRecording: function() { return heartRate('pauseRecording'); },
|
|
5003
|
+
resumeRecording: function() { return heartRate('resumeRecording'); },
|
|
5004
|
+
stopRecording: function() { return heartRate('stopRecording'); },
|
|
5005
|
+
getRecordingState: function() { return heartRate('getRecordingState'); },
|
|
5006
|
+
readRecording: function(options) { return heartRate('readRecording', {since: options && typeof options.since === 'number' ? options.since : null}); }
|
|
5007
|
+
};
|
|
5008
|
+
})();
|
|
4954
5009
|
craft.liveActivity = {
|
|
4955
5010
|
start: function(options) { return craft._invoke('startLiveActivity', options || {}); },
|
|
4956
5011
|
update: function(idOrState, state) {
|
|
@@ -8450,7 +8505,7 @@ final class CraftZigRuntime: NSObject {
|
|
|
8450
8505
|
enum CraftWebServices {
|
|
8451
8506
|
private static var waiting: [String: (Result<Any, CraftNativeActionError>) -> Void] = [:]
|
|
8452
8507
|
/// Coordinator events a native screen hears too.
|
|
8453
|
-
static let forwardedEvents: Set<String> = ["craftLocationUpdate", "craftLocationError", "craftMusicChange"]
|
|
8508
|
+
static let forwardedEvents: Set<String> = ["craftLocationUpdate", "craftLocationError", "craftMusicChange", "craftHeartRate"]
|
|
8454
8509
|
static let event = Notification.Name("craftWebServicesEvent")
|
|
8455
8510
|
|
|
8456
8511
|
static var isAvailable: Bool { CraftSwiftShim.coordinator != nil }
|