From e65397682c4d152abfa35f7b01dc49ccde4a9be4 Mon Sep 17 00:00:00 2001 From: Andreas Bauer Date: Sun, 10 Nov 2024 13:53:31 +0100 Subject: [PATCH] Ecosystem Upgrades and Reusability Improvements (#61) # Ecosystem Upgrades and Reusability Improvements ## :recycle: Current situation & Problem This PR upgrades to the latest version of Spezi packages. Further, we could remove some code, because some infrastructure was introduced in packages like SpeziDevices (BatteryIconView) and SpeziViews (SimpleTile and TileHeader view) ## :gear: Release Notes * Ecosystem upgrades. ## :books: Documentation -- ## :white_check_mark: Testing Existing tests. ### Code of Conduct & Contributing Guidelines By submitting creating this pull request, you agree to follow our [Code of Conduct](https://github.com/StanfordBDHG/.github/blob/main/CODE_OF_CONDUCT.md) and [Contributing Guidelines](https://github.com/StanfordBDHG/.github/blob/main/CONTRIBUTING.md): - [x] I agree to follow the [Code of Conduct](https://github.com/StanfordBDHG/.github/blob/main/CODE_OF_CONDUCT.md) and [Contributing Guidelines](https://github.com/StanfordBDHG/.github/blob/main/CONTRIBUTING.md). --- NAMS.xcodeproj/project.pbxproj | 225 ++++++++---------- .../xcshareddata/swiftpm/Package.resolved | 112 +++++---- .../xcshareddata/xcschemes/NAMS Muse.xcscheme | 2 +- .../xcshareddata/xcschemes/NAMS.xcscheme | 2 +- NAMS/Account/AccountSetupHeader.swift | 5 +- NAMS/Account/AccountSheet.swift | 52 ++-- NAMS/Account/InvestigatorCodeKey.swift | 53 ++--- NAMS/Devices/BatteryIcon.swift | 93 -------- NAMS/Devices/Biopot/BiopotDevice.swift | 23 +- NAMS/Devices/Biopot/BiopotService.swift | 14 +- .../Characteristics/AccelerometerSample.swift | 18 +- .../Biopot/Characteristics/BiopotSample.swift | 4 +- .../Characteristics/DataAcquisition.swift | 18 +- .../Biopot/Characteristics/DataControl.swift | 10 +- .../Characteristics/DeviceConfiguration.swift | 46 ++-- .../Characteristics/DeviceInformation.swift | 16 +- .../ImpedanceMeasurement.swift | 16 +- .../SamplingConfiguration.swift | 62 ++--- .../Views/BiopotDeviceDetailsView.swift | 1 + .../Biopot/Views/BiopotDeviceRow.swift | 14 +- NAMS/Devices/ConnectedDevice.swift | 6 +- NAMS/Devices/DeviceCoordinator.swift | 4 +- NAMS/Devices/EmptyScanningState.swift | 24 ++ NAMS/Devices/Mock/MockDevice.swift | 2 +- NAMS/Devices/Mock/MockDeviceManager.swift | 16 +- .../Mock/View+ScanNearbyMockDevices.swift | 21 ++ NAMS/Devices/Mock/Views/MockDeviceRow.swift | 14 +- NAMS/Devices/Muse/MuseDevice.swift | 2 +- NAMS/Devices/Muse/MuseDeviceManager.swift | 26 +- .../Muse/View+ScanNearbyMuseDevices.swift | 23 ++ .../Details/MuseBatteryDetailsSection.swift | 1 + NAMS/Devices/Muse/Views/MuseDeviceRow.swift | 14 +- NAMS/Devices/NAMSDevice.swift | 4 +- NAMS/Devices/NearbyDevicesView.swift | 8 +- NAMS/EEG/EEGRecordings.swift | 6 +- .../Chart/ChartMoreInformationView.swift | 11 +- NAMS/EEG/Views/EEGRecordingSessionView.swift | 11 +- NAMS/EEG/Views/EEGRecordingView.swift | 25 +- NAMS/EEG/Views/StartRecordingView.swift | 5 +- NAMS/Home.swift | 12 +- NAMS/NAMSApp.swift | 1 + NAMS/NAMSAppDelegate.swift | 33 ++- NAMS/NAMSStandard.swift | 28 +-- NAMS/Onboarding/AccountOnboarding.swift | 15 +- NAMS/Onboarding/OnboardingFlow.swift | 5 +- NAMS/Patients/Model/Patient.swift | 2 +- NAMS/Patients/Model/PatientListModel.swift | 20 +- NAMS/Patients/PatientRow.swift | 2 +- NAMS/Patients/Tasks/CompletedTask.swift | 2 +- NAMS/ScheduleView.swift | 5 +- NAMS/Tiles/CompletedTile.swift | 74 ------ NAMS/Tiles/MeasurementTile.swift | 55 +++-- NAMS/Tiles/ScreeningTile.swift | 61 ++++- NAMS/Tiles/ScreeningTileHeader.swift | 90 ------- NAMS/Tiles/SimpleTile.swift | 114 --------- NAMS/Tiles/TilesView.swift | 9 +- NAMS/Utils/Helper/Binding+Negate.swift | 20 -- NAMS/Utils/Helper/Bundle+Image.swift | 30 --- .../CodableArray+RawRepresentable.swift | 28 --- .../Helper/ProcessInfo+PreviewSimulator.swift | 16 -- NAMS/Utils/Questionnaire+Identifiable.swift | 6 +- NAMS/Utils/Testing/BiopotDevicePreview.swift | 30 --- NAMSTests/BiopotCodingTests.swift | 4 +- NAMSTests/BiopotDeviceTests.swift | 2 + NAMSUITests/BiopotTests.swift | 6 +- NAMSUITests/ContactsTests.swift | 2 + NAMSUITests/MockDeviceTests.swift | 16 +- NAMSUITests/PatientInformationTests.swift | 4 - fastlane/Fastfile | 2 +- 69 files changed, 662 insertions(+), 1041 deletions(-) delete mode 100644 NAMS/Devices/BatteryIcon.swift create mode 100644 NAMS/Devices/EmptyScanningState.swift create mode 100644 NAMS/Devices/Mock/View+ScanNearbyMockDevices.swift create mode 100644 NAMS/Devices/Muse/View+ScanNearbyMuseDevices.swift delete mode 100644 NAMS/Tiles/CompletedTile.swift delete mode 100644 NAMS/Tiles/ScreeningTileHeader.swift delete mode 100644 NAMS/Tiles/SimpleTile.swift delete mode 100644 NAMS/Utils/Helper/Binding+Negate.swift delete mode 100644 NAMS/Utils/Helper/Bundle+Image.swift delete mode 100644 NAMS/Utils/Helper/CodableArray+RawRepresentable.swift delete mode 100644 NAMS/Utils/Helper/ProcessInfo+PreviewSimulator.swift delete mode 100644 NAMS/Utils/Testing/BiopotDevicePreview.swift diff --git a/NAMS.xcodeproj/project.pbxproj b/NAMS.xcodeproj/project.pbxproj index ca9f08d..2c6f25b 100644 --- a/NAMS.xcodeproj/project.pbxproj +++ b/NAMS.xcodeproj/project.pbxproj @@ -13,11 +13,9 @@ 2DC1718A3F968CF02D7AF0EC /* PatientListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC17A06799FC1470D4DDC0D /* PatientListView.swift */; }; 2DC171B41D4A87E2C861C356 /* ConnectionState+Muse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC1775D695F23F9EB05697F /* ConnectionState+Muse.swift */; }; 2DC171EFD6619742C29254CE /* Fit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC177DBF00CB14CAC4C7E82 /* Fit.swift */; }; - 2DC17206C9867ACAC0915363 /* BiopotDevicePreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC177B4AFB1C891EDB8152B /* BiopotDevicePreview.swift */; }; 2DC17257A28A7E2232229658 /* PatientTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC170F68232B993528F84FE /* PatientTask.swift */; }; 2DC172753D306AE733D7FDC8 /* TileType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC177BFA6C401C2C87FCD5C /* TileType.swift */; }; 2DC1727A98890570E5A4B46D /* PatientTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC170F68232B993528F84FE /* PatientTask.swift */; }; - 2DC172E36CAF9AA8F191F723 /* MockDeviceRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC1794C7379ACE157EE11C4 /* MockDeviceRow.swift */; }; 2DC172F25F327CB4B718D589 /* Questionnaire+Identifiable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC174AABEF6E6015EFA8B4B /* Questionnaire+Identifiable.swift */; }; 2DC17337BA4FEF5664BC0D10 /* FinishedSetup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC173772F6FD3C05EBFCE52 /* FinishedSetup.swift */; }; 2DC17374D5266F13ADD5C002 /* MockDeviceManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC175A8D7EDF6EE1B55E859 /* MockDeviceManager.swift */; }; @@ -25,7 +23,6 @@ 2DC173E694F96E89EAB63FE0 /* IXNMuseConfiguration+Description.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC1785634B3460C4FB953C7 /* IXNMuseConfiguration+Description.swift */; }; 2DC173F479B53B9054330880 /* MuseDeviceRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC17FBF21126FAA3A35B601 /* MuseDeviceRow.swift */; }; 2DC174CC46386A3F7E20786B /* IXNMuseVersion+String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC17020D7BBBECE54A765C6 /* IXNMuseVersion+String.swift */; }; - 2DC174CCCA1DAC48C45CDAC4 /* BiopotDevicePreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC177B4AFB1C891EDB8152B /* BiopotDevicePreview.swift */; }; 2DC1751E71FB3A81CDAB38BB /* ConnectedDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC17AEF81E62461D4AA3E67 /* ConnectedDevice.swift */; }; 2DC1762E730B0472308EEFFC /* IXNMuseDataPacketType+Type.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC17CDCBE427101B2ACE5FB /* IXNMuseDataPacketType+Type.swift */; }; 2DC17686B3AEB09A8F60AB8E /* PatientListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC17A06799FC1470D4DDC0D /* PatientListView.swift */; }; @@ -52,7 +49,6 @@ 2DC17C61E1697767983916EF /* DeviceCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC172078D2802AD6068AC7E /* DeviceCoordinator.swift */; }; 2DC17CA2806C0BFD72984B3D /* MockDeviceManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC175A8D7EDF6EE1B55E859 /* MockDeviceManager.swift */; }; 2DC17CAFDF6974EAA57C5D34 /* MuseDeviceList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC179BED6FAAD175304A875 /* MuseDeviceList.swift */; }; - 2DC17CD906FF31BA6EA4CACD /* MockDeviceRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC1794C7379ACE157EE11C4 /* MockDeviceRow.swift */; }; 2DC17D19CFECCEF0A7206F35 /* ConnectionState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC179219FC061D5D0282BF2 /* ConnectionState.swift */; }; 2DC17E6D396A8A2B50E1D44F /* Questionnaire+Identifiable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC174AABEF6E6015EFA8B4B /* Questionnaire+Identifiable.swift */; }; 2DC17F50A02902B6757A435B /* MuseDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC17C8A120ECD3394366958 /* MuseDevice.swift */; }; @@ -74,9 +70,6 @@ 2FE5DC3A29EDD7CA004B9AB4 /* Welcome.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FE5DC3429EDD7CA004B9AB4 /* Welcome.swift */; }; 2FE5DC4029EDD7EE004B9AB4 /* FeatureFlags.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FE5DC3E29EDD7ED004B9AB4 /* FeatureFlags.swift */; }; 2FE5DC4129EDD7EE004B9AB4 /* StorageKeys.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FE5DC3F29EDD7EE004B9AB4 /* StorageKeys.swift */; }; - 2FE5DC4529EDD7F2004B9AB4 /* Binding+Negate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FE5DC4229EDD7F2004B9AB4 /* Binding+Negate.swift */; }; - 2FE5DC4629EDD7F2004B9AB4 /* Bundle+Image.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FE5DC4329EDD7F2004B9AB4 /* Bundle+Image.swift */; }; - 2FE5DC4729EDD7F2004B9AB4 /* CodableArray+RawRepresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FE5DC4429EDD7F2004B9AB4 /* CodableArray+RawRepresentable.swift */; }; 2FE5DC6429EDD883004B9AB4 /* SpeziAccount in Frameworks */ = {isa = PBXBuildFile; productRef = 2FE5DC6329EDD883004B9AB4 /* SpeziAccount */; }; 2FE5DC6729EDD894004B9AB4 /* SpeziContact in Frameworks */ = {isa = PBXBuildFile; productRef = 2FE5DC6629EDD894004B9AB4 /* SpeziContact */; }; 2FE5DC7529EDD8E6004B9AB4 /* SpeziFirebaseAccount in Frameworks */ = {isa = PBXBuildFile; productRef = 2FE5DC7429EDD8E6004B9AB4 /* SpeziFirebaseAccount */; }; @@ -105,21 +98,16 @@ A90830852BAA33E6003E0080 /* DownsampleConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = A90830832BAA33E6003E0080 /* DownsampleConfiguration.swift */; }; A91459762A4AF4E000A04641 /* QuestionnaireTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A91459752A4AF4E000A04641 /* QuestionnaireTests.swift */; }; A916ADD52AB60227006960DF /* NotificationPermissions.swift in Sources */ = {isa = PBXBuildFile; fileRef = A916ADD42AB60227006960DF /* NotificationPermissions.swift */; }; - A916ADD72AB62012006960DF /* ProcessInfo+PreviewSimulator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A916ADD62AB62012006960DF /* ProcessInfo+PreviewSimulator.swift */; }; A916ADD92AB6217D006960DF /* OnboardingFlow+PreviewSimulator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A916ADD82AB6217D006960DF /* OnboardingFlow+PreviewSimulator.swift */; }; A9216A742BA4C3A500B8A3F2 /* BiopotElectrodeLocations.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9216A732BA4C3A500B8A3F2 /* BiopotElectrodeLocations.swift */; }; A9216A752BA4C3A500B8A3F2 /* BiopotElectrodeLocations.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9216A732BA4C3A500B8A3F2 /* BiopotElectrodeLocations.swift */; }; A926D77F2AB7A552000C4C2F /* StorageKeys.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FE5DC3F29EDD7EE004B9AB4 /* StorageKeys.swift */; }; A926D7822AB7A552000C4C2F /* NotificationPermissions.swift in Sources */ = {isa = PBXBuildFile; fileRef = A916ADD42AB60227006960DF /* NotificationPermissions.swift */; }; A926D7872AB7A552000C4C2F /* Welcome.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FE5DC3429EDD7CA004B9AB4 /* Welcome.swift */; }; - A926D7882AB7A552000C4C2F /* Binding+Negate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FE5DC4229EDD7F2004B9AB4 /* Binding+Negate.swift */; }; - A926D78A2AB7A552000C4C2F /* ProcessInfo+PreviewSimulator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A916ADD62AB62012006960DF /* ProcessInfo+PreviewSimulator.swift */; }; A926D78B2AB7A552000C4C2F /* OnboardingFlow+PreviewSimulator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A916ADD82AB6217D006960DF /* OnboardingFlow+PreviewSimulator.swift */; }; A926D78C2AB7A552000C4C2F /* Home.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FC975A72978F11A00BA99FE /* Home.swift */; }; A926D78E2AB7A552000C4C2F /* OnboardingFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FE5DC3129EDD7CA004B9AB4 /* OnboardingFlow.swift */; }; - A926D7912AB7A552000C4C2F /* CodableArray+RawRepresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FE5DC4429EDD7F2004B9AB4 /* CodableArray+RawRepresentable.swift */; }; A926D7922AB7A552000C4C2F /* FeatureFlags.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FE5DC3E29EDD7ED004B9AB4 /* FeatureFlags.swift */; }; - A926D7932AB7A552000C4C2F /* Bundle+Image.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FE5DC4329EDD7F2004B9AB4 /* Bundle+Image.swift */; }; A926D7962AB7A552000C4C2F /* NAMSTestingSetup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F4E23822989D51F0013F3D9 /* NAMSTestingSetup.swift */; }; A926D79A2AB7A552000C4C2F /* NAMSAppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F5E32BC297E05EA003432F8 /* NAMSAppDelegate.swift */; }; A926D79C2AB7A552000C4C2F /* NAMSApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 653A2550283387FE005D4D48 /* NAMSApp.swift */; }; @@ -217,8 +205,14 @@ A94A42B72AE9EBE300A3F9E5 /* AccountSetupHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = A94A42AD2AE9EBE300A3F9E5 /* AccountSetupHeader.swift */; }; A94A42BA2AE9ED8300A3F9E5 /* AccountOnboarding.swift in Sources */ = {isa = PBXBuildFile; fileRef = A94A42B92AE9ED8200A3F9E5 /* AccountOnboarding.swift */; }; A94A42BB2AE9ED8300A3F9E5 /* AccountOnboarding.swift in Sources */ = {isa = PBXBuildFile; fileRef = A94A42B92AE9ED8200A3F9E5 /* AccountOnboarding.swift */; }; - A95EEAE32B6B54D3009B4CF8 /* BluetoothViews in Frameworks */ = {isa = PBXBuildFile; productRef = A95EEAE22B6B54D3009B4CF8 /* BluetoothViews */; }; - A95EEAE52B6B54DA009B4CF8 /* BluetoothViews in Frameworks */ = {isa = PBXBuildFile; productRef = A95EEAE42B6B54DA009B4CF8 /* BluetoothViews */; }; + A94AF85E2C9752B500351E0A /* EmptyScanningState.swift in Sources */ = {isa = PBXBuildFile; fileRef = A94AF85D2C9752B300351E0A /* EmptyScanningState.swift */; }; + A94AF85F2C9752B500351E0A /* EmptyScanningState.swift in Sources */ = {isa = PBXBuildFile; fileRef = A94AF85D2C9752B300351E0A /* EmptyScanningState.swift */; }; + A94AF8612C9752DF00351E0A /* View+ScanNearbyMockDevices.swift in Sources */ = {isa = PBXBuildFile; fileRef = A94AF8602C9752D600351E0A /* View+ScanNearbyMockDevices.swift */; }; + A94AF8622C9752DF00351E0A /* View+ScanNearbyMockDevices.swift in Sources */ = {isa = PBXBuildFile; fileRef = A94AF8602C9752D600351E0A /* View+ScanNearbyMockDevices.swift */; }; + A94AF8642C97540400351E0A /* View+ScanNearbyMuseDevices.swift in Sources */ = {isa = PBXBuildFile; fileRef = A94AF8632C9753FE00351E0A /* View+ScanNearbyMuseDevices.swift */; }; + A94AF8652C97540400351E0A /* View+ScanNearbyMuseDevices.swift in Sources */ = {isa = PBXBuildFile; fileRef = A94AF8632C9753FE00351E0A /* View+ScanNearbyMuseDevices.swift */; }; + A94AF86A2C9754C000351E0A /* MockDeviceRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = A94AF8682C9754C000351E0A /* MockDeviceRow.swift */; }; + A94AF86B2C9754C000351E0A /* MockDeviceRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = A94AF8682C9754C000351E0A /* MockDeviceRow.swift */; }; A95F4B312B990803006778E5 /* ByteCoding in Frameworks */ = {isa = PBXBuildFile; productRef = A95F4B302B990803006778E5 /* ByteCoding */; }; A95F4B332B990803006778E5 /* EDFFormat in Frameworks */ = {isa = PBXBuildFile; productRef = A95F4B322B990803006778E5 /* EDFFormat */; }; A95F4B352B990803006778E5 /* XCTByteCoding in Frameworks */ = {isa = PBXBuildFile; productRef = A95F4B342B990803006778E5 /* XCTByteCoding */; }; @@ -230,6 +224,10 @@ A974D6482B9C15540058C164 /* InvestigatorCodeKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = A974D6462B9C15540058C164 /* InvestigatorCodeKey.swift */; }; A9770E152B9A7D2500C01276 /* PatientInformation+Patient.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9770E142B9A7D2500C01276 /* PatientInformation+Patient.swift */; }; A9770E172B9A7D6E00C01276 /* Sex+Patient.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9770E162B9A7D6E00C01276 /* Sex+Patient.swift */; }; + A977F6752C932C780071A1D1 /* SpeziBluetoothServices in Frameworks */ = {isa = PBXBuildFile; productRef = A977F6742C932C780071A1D1 /* SpeziBluetoothServices */; }; + A977F6782C932D0E0071A1D1 /* SpeziDevicesUI in Frameworks */ = {isa = PBXBuildFile; productRef = A977F6772C932D0E0071A1D1 /* SpeziDevicesUI */; }; + A977F67A2C932D1E0071A1D1 /* SpeziBluetoothServices in Frameworks */ = {isa = PBXBuildFile; productRef = A977F6792C932D1E0071A1D1 /* SpeziBluetoothServices */; }; + A977F67C2C932D2A0071A1D1 /* SpeziDevicesUI in Frameworks */ = {isa = PBXBuildFile; productRef = A977F67B2C932D2A0071A1D1 /* SpeziDevicesUI */; }; A97E4F1F2B1EA0D600E25505 /* StartRecordingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A97E4F1E2B1EA0D600E25505 /* StartRecordingView.swift */; }; A97E4F202B1EA0D600E25505 /* StartRecordingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A97E4F1E2B1EA0D600E25505 /* StartRecordingView.swift */; }; A97E4F232B1EA21800E25505 /* EEGLocation+Biopot.swift in Sources */ = {isa = PBXBuildFile; fileRef = A97E4F222B1EA21800E25505 /* EEGLocation+Biopot.swift */; }; @@ -249,8 +247,6 @@ A988FEA62B03FB4A00022A61 /* SpeziBluetooth in Frameworks */ = {isa = PBXBuildFile; productRef = A988FEA52B03FB4A00022A61 /* SpeziBluetooth */; }; A988FEA82B03FB5A00022A61 /* SpeziBluetooth in Frameworks */ = {isa = PBXBuildFile; productRef = A988FEA72B03FB5A00022A61 /* SpeziBluetooth */; }; A988FEAA2B0414FD00022A61 /* BiopotDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = A988FEA92B0414FD00022A61 /* BiopotDevice.swift */; }; - A988FEAC2B043AED00022A61 /* BatteryIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = A988FEAB2B043AED00022A61 /* BatteryIcon.swift */; }; - A988FEAD2B043AED00022A61 /* BatteryIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = A988FEAB2B043AED00022A61 /* BatteryIcon.swift */; }; A988FEAF2B0452A900022A61 /* BiopotDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = A988FEA92B0414FD00022A61 /* BiopotDevice.swift */; }; A988FEB22B0452C400022A61 /* DeviceConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = A988FEB12B0452C400022A61 /* DeviceConfiguration.swift */; }; A988FEB32B0452C400022A61 /* DeviceConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = A988FEB12B0452C400022A61 /* DeviceConfiguration.swift */; }; @@ -285,8 +281,6 @@ A9BCB58D2AE84E6D00DA8588 /* CurrentPatientLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9BCB58B2AE84E6D00DA8588 /* CurrentPatientLabel.swift */; }; A9C82F922B608756004703E0 /* ImpedanceMeasurement.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9C82F912B608756004703E0 /* ImpedanceMeasurement.swift */; }; A9C82F932B60899B004703E0 /* ImpedanceMeasurement.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9C82F912B608756004703E0 /* ImpedanceMeasurement.swift */; }; - A9C82F952B6089C8004703E0 /* BluetoothServices in Frameworks */ = {isa = PBXBuildFile; productRef = A9C82F942B6089C8004703E0 /* BluetoothServices */; }; - A9C82F972B6089D2004703E0 /* BluetoothServices in Frameworks */ = {isa = PBXBuildFile; productRef = A9C82F962B6089D2004703E0 /* BluetoothServices */; }; A9C9B6B42ADE191100C8C46D /* MockDeviceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9C9B6B32ADE191100C8C46D /* MockDeviceTests.swift */; }; A9CE84512B1A9A14009CE3F4 /* NearbyDevicesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9CE84502B1A9A14009CE3F4 /* NearbyDevicesView.swift */; }; A9CE84522B1A9A14009CE3F4 /* NearbyDevicesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9CE84502B1A9A14009CE3F4 /* NearbyDevicesView.swift */; }; @@ -310,8 +304,6 @@ A9D4B8E72B68636A0054E27C /* MuseHeadbandFitView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9D4B8E52B6860AC0054E27C /* MuseHeadbandFitView.swift */; }; A9D4B8E92B6863D60054E27C /* FitLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9D4B8E82B6863D60054E27C /* FitLabel.swift */; }; A9D4B8EA2B6863D60054E27C /* FitLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9D4B8E82B6863D60054E27C /* FitLabel.swift */; }; - A9D4B8EC2B686D380054E27C /* ScreeningTileHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9D4B8EB2B686D380054E27C /* ScreeningTileHeader.swift */; }; - A9D4B8ED2B686D380054E27C /* ScreeningTileHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9D4B8EB2B686D380054E27C /* ScreeningTileHeader.swift */; }; A9D6BA822BACCB78002863DA /* RecordingState.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9D6BA812BACCB78002863DA /* RecordingState.swift */; }; A9D6BA832BACCB78002863DA /* RecordingState.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9D6BA812BACCB78002863DA /* RecordingState.swift */; }; A9D6BA852BACE051002863DA /* RecordingStateHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9D6BA842BACE051002863DA /* RecordingStateHeader.swift */; }; @@ -327,10 +319,6 @@ A9F051C42B9A8D90006D3FEF /* BiopotDeviceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9F916772B080967007CECF6 /* BiopotDeviceTests.swift */; }; A9F2ECC62AEB27B10057C7DD /* MeasurementTile.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9F2ECC52AEB27B10057C7DD /* MeasurementTile.swift */; }; A9F2ECC72AEB27B10057C7DD /* MeasurementTile.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9F2ECC52AEB27B10057C7DD /* MeasurementTile.swift */; }; - A9F2ECC92AEC2C300057C7DD /* CompletedTile.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9F2ECC82AEC2C300057C7DD /* CompletedTile.swift */; }; - A9F2ECCA2AEC2C300057C7DD /* CompletedTile.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9F2ECC82AEC2C300057C7DD /* CompletedTile.swift */; }; - A9F2ECCD2AEC58B00057C7DD /* SimpleTile.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9F2ECCC2AEC58B00057C7DD /* SimpleTile.swift */; }; - A9F2ECCE2AEC58B00057C7DD /* SimpleTile.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9F2ECCC2AEC58B00057C7DD /* SimpleTile.swift */; }; A9F2ECD02AEC5EF50057C7DD /* MeasurementTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9F2ECCF2AEC5EF50057C7DD /* MeasurementTask.swift */; }; A9F2ECD12AEC5EF50057C7DD /* MeasurementTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9F2ECCF2AEC5EF50057C7DD /* MeasurementTask.swift */; }; A9FCE8342AE9CA4F0008EA2B /* PatientInformationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9FCE8332AE9CA4F0008EA2B /* PatientInformationTests.swift */; }; @@ -367,12 +355,10 @@ 2DC1751EE233BB85004ECA90 /* IXNMusePreset+Description.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "IXNMusePreset+Description.swift"; sourceTree = ""; }; 2DC175A8D7EDF6EE1B55E859 /* MockDeviceManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockDeviceManager.swift; sourceTree = ""; }; 2DC1775D695F23F9EB05697F /* ConnectionState+Muse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ConnectionState+Muse.swift"; sourceTree = ""; }; - 2DC177B4AFB1C891EDB8152B /* BiopotDevicePreview.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BiopotDevicePreview.swift; sourceTree = ""; }; 2DC177BFA6C401C2C87FCD5C /* TileType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TileType.swift; sourceTree = ""; }; 2DC177DBF00CB14CAC4C7E82 /* Fit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Fit.swift; sourceTree = ""; }; 2DC1785634B3460C4FB953C7 /* IXNMuseConfiguration+Description.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "IXNMuseConfiguration+Description.swift"; sourceTree = ""; }; 2DC179219FC061D5D0282BF2 /* ConnectionState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConnectionState.swift; sourceTree = ""; }; - 2DC1794C7379ACE157EE11C4 /* MockDeviceRow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = MockDeviceRow.swift; path = Views/MockDeviceRow.swift; sourceTree = ""; }; 2DC179BED6FAAD175304A875 /* MuseDeviceList.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MuseDeviceList.swift; sourceTree = ""; }; 2DC17A06799FC1470D4DDC0D /* PatientListView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PatientListView.swift; sourceTree = ""; }; 2DC17AEF81E62461D4AA3E67 /* ConnectedDevice.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConnectedDevice.swift; sourceTree = ""; }; @@ -397,9 +383,6 @@ 2FE5DC3429EDD7CA004B9AB4 /* Welcome.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Welcome.swift; sourceTree = ""; }; 2FE5DC3E29EDD7ED004B9AB4 /* FeatureFlags.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeatureFlags.swift; sourceTree = ""; }; 2FE5DC3F29EDD7EE004B9AB4 /* StorageKeys.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StorageKeys.swift; sourceTree = ""; }; - 2FE5DC4229EDD7F2004B9AB4 /* Binding+Negate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Binding+Negate.swift"; sourceTree = ""; }; - 2FE5DC4329EDD7F2004B9AB4 /* Bundle+Image.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Bundle+Image.swift"; sourceTree = ""; }; - 2FE5DC4429EDD7F2004B9AB4 /* CodableArray+RawRepresentable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CodableArray+RawRepresentable.swift"; sourceTree = ""; }; 653A254D283387FE005D4D48 /* NAMS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = NAMS.app; sourceTree = BUILT_PRODUCTS_DIR; }; 653A2550283387FE005D4D48 /* NAMSApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NAMSApp.swift; sourceTree = ""; }; 653A255428338800005D4D48 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; @@ -415,7 +398,6 @@ A90830832BAA33E6003E0080 /* DownsampleConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownsampleConfiguration.swift; sourceTree = ""; }; A91459752A4AF4E000A04641 /* QuestionnaireTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuestionnaireTests.swift; sourceTree = ""; }; A916ADD42AB60227006960DF /* NotificationPermissions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationPermissions.swift; sourceTree = ""; }; - A916ADD62AB62012006960DF /* ProcessInfo+PreviewSimulator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProcessInfo+PreviewSimulator.swift"; sourceTree = ""; }; A916ADD82AB6217D006960DF /* OnboardingFlow+PreviewSimulator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OnboardingFlow+PreviewSimulator.swift"; sourceTree = ""; }; A9216A732BA4C3A500B8A3F2 /* BiopotElectrodeLocations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BiopotElectrodeLocations.swift; sourceTree = ""; }; A926D7C32AB7A552000C4C2F /* NAMS Muse.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "NAMS Muse.app"; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -455,6 +437,10 @@ A94A42AB2AE9EBE200A3F9E5 /* AccountButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountButton.swift; sourceTree = ""; }; A94A42AD2AE9EBE300A3F9E5 /* AccountSetupHeader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountSetupHeader.swift; sourceTree = ""; }; A94A42B92AE9ED8200A3F9E5 /* AccountOnboarding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountOnboarding.swift; sourceTree = ""; }; + A94AF85D2C9752B300351E0A /* EmptyScanningState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyScanningState.swift; sourceTree = ""; }; + A94AF8602C9752D600351E0A /* View+ScanNearbyMockDevices.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+ScanNearbyMockDevices.swift"; sourceTree = ""; }; + A94AF8632C9753FE00351E0A /* View+ScanNearbyMuseDevices.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+ScanNearbyMuseDevices.swift"; sourceTree = ""; }; + A94AF8682C9754C000351E0A /* MockDeviceRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockDeviceRow.swift; sourceTree = ""; }; A967061B2B1AA2E000C17BE5 /* EEGRecordingSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EEGRecordingSession.swift; sourceTree = ""; }; A974D6462B9C15540058C164 /* InvestigatorCodeKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvestigatorCodeKey.swift; sourceTree = ""; }; A9770E142B9A7D2500C01276 /* PatientInformation+Patient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PatientInformation+Patient.swift"; sourceTree = ""; }; @@ -468,7 +454,6 @@ A985DEC62BA4DB9300DDEBF8 /* BiopotDeviceRow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BiopotDeviceRow.swift; sourceTree = ""; }; A985DECC2BA4DBA900DDEBF8 /* BiopotCustomElectrodePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BiopotCustomElectrodePicker.swift; sourceTree = ""; }; A988FEA92B0414FD00022A61 /* BiopotDevice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BiopotDevice.swift; sourceTree = ""; }; - A988FEAB2B043AED00022A61 /* BatteryIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatteryIcon.swift; sourceTree = ""; }; A988FEB12B0452C400022A61 /* DeviceConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceConfiguration.swift; sourceTree = ""; }; A988FEB42B0453E100022A61 /* DeviceInformation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceInformation.swift; sourceTree = ""; }; A98905442BCEFD6800E36BA7 /* ChartMoreInformationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChartMoreInformationView.swift; sourceTree = ""; }; @@ -498,14 +483,11 @@ A9D4B8D42B685D800054E27C /* MuseBatteryProblemsHint.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MuseBatteryProblemsHint.swift; sourceTree = ""; }; A9D4B8E52B6860AC0054E27C /* MuseHeadbandFitView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MuseHeadbandFitView.swift; sourceTree = ""; }; A9D4B8E82B6863D60054E27C /* FitLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FitLabel.swift; sourceTree = ""; }; - A9D4B8EB2B686D380054E27C /* ScreeningTileHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreeningTileHeader.swift; sourceTree = ""; }; A9D6BA812BACCB78002863DA /* RecordingState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecordingState.swift; sourceTree = ""; }; A9D6BA842BACE051002863DA /* RecordingStateHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecordingStateHeader.swift; sourceTree = ""; }; A9D83F912B081A47000D0C78 /* BiopotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BiopotTests.swift; sourceTree = ""; }; A9DF79D92AE8986B00AB5983 /* SelectedPatientCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectedPatientCard.swift; sourceTree = ""; }; A9F2ECC52AEB27B10057C7DD /* MeasurementTile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeasurementTile.swift; sourceTree = ""; }; - A9F2ECC82AEC2C300057C7DD /* CompletedTile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompletedTile.swift; sourceTree = ""; }; - A9F2ECCC2AEC58B00057C7DD /* SimpleTile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleTile.swift; sourceTree = ""; }; A9F2ECCF2AEC5EF50057C7DD /* MeasurementTask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeasurementTask.swift; sourceTree = ""; }; A9F916772B080967007CECF6 /* BiopotDeviceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BiopotDeviceTests.swift; sourceTree = ""; }; A9FCE8332AE9CA4F0008EA2B /* PatientInformationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PatientInformationTests.swift; sourceTree = ""; }; @@ -519,22 +501,22 @@ A988FEBD2B05C7AE00022A61 /* SpeziPersonalInfo in Frameworks */, A95F4B332B990803006778E5 /* EDFFormat in Frameworks */, 2FE5DC6429EDD883004B9AB4 /* SpeziAccount in Frameworks */, - A95EEAE32B6B54D3009B4CF8 /* BluetoothViews in Frameworks */, A988FEA62B03FB4A00022A61 /* SpeziBluetooth in Frameworks */, 2FE5DC6729EDD894004B9AB4 /* SpeziContact in Frameworks */, A941711C2B9FB17A002E975A /* SpeziFirebaseAccountStorage in Frameworks */, 2FE5DC8429EDD934004B9AB4 /* SpeziQuestionnaire in Frameworks */, 2FE5DC8A29EDD972004B9AB4 /* SpeziLocalStorage in Frameworks */, + A977F6782C932D0E0071A1D1 /* SpeziDevicesUI in Frameworks */, 2FE5DC8C29EDD972004B9AB4 /* SpeziSecureStorage in Frameworks */, A92E34F02ADB9B7E00FE0B51 /* OrderedCollections in Frameworks */, 2FE5DC7529EDD8E6004B9AB4 /* SpeziFirebaseAccount in Frameworks */, 2F49B7762980407C00BCB272 /* Spezi in Frameworks */, - A9C82F952B6089C8004703E0 /* BluetoothServices in Frameworks */, 2FE5DC8F29EDD980004B9AB4 /* SpeziViews in Frameworks */, A95F4B312B990803006778E5 /* ByteCoding in Frameworks */, 2FE5DC8129EDD91D004B9AB4 /* SpeziOnboarding in Frameworks */, A9A48A8B2BA8CCE3002CB862 /* SpeziFirebaseStorage in Frameworks */, 2FE5DC7929EDD8E6004B9AB4 /* SpeziFirestore in Frameworks */, + A977F6752C932C780071A1D1 /* SpeziBluetoothServices in Frameworks */, 2FE5DC7729EDD8E6004B9AB4 /* SpeziFirebaseConfiguration in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -562,19 +544,19 @@ A95F4B372B99081D006778E5 /* ByteCoding in Frameworks */, A926D7A82AB7A552000C4C2F /* Muse.framework in Frameworks */, A926D7A92AB7A552000C4C2F /* SpeziAccount in Frameworks */, + A977F67A2C932D1E0071A1D1 /* SpeziBluetoothServices in Frameworks */, A926D7AA2AB7A552000C4C2F /* SpeziContact in Frameworks */, A926D7AB2AB7A552000C4C2F /* SpeziQuestionnaire in Frameworks */, A941711E2B9FB180002E975A /* SpeziFirebaseAccountStorage in Frameworks */, A926D7AC2AB7A552000C4C2F /* SpeziLocalStorage in Frameworks */, A926D7AD2AB7A552000C4C2F /* SpeziSecureStorage in Frameworks */, + A977F67C2C932D2A0071A1D1 /* SpeziDevicesUI in Frameworks */, A92E34F22ADB9B9000FE0B51 /* OrderedCollections in Frameworks */, A926D7AE2AB7A552000C4C2F /* SpeziFirebaseAccount in Frameworks */, - A95EEAE52B6B54DA009B4CF8 /* BluetoothViews in Frameworks */, A95F4B392B99081D006778E5 /* EDFFormat in Frameworks */, A988FEBF2B05C7B800022A61 /* SpeziPersonalInfo in Frameworks */, A926D7B02AB7A552000C4C2F /* Spezi in Frameworks */, A926D7B12AB7A552000C4C2F /* SpeziViews in Frameworks */, - A9C82F972B6089D2004703E0 /* BluetoothServices in Frameworks */, A988FEA82B03FB5A00022A61 /* SpeziBluetooth in Frameworks */, A926D7B42AB7A552000C4C2F /* SpeziOnboarding in Frameworks */, A9A48A8D2BA8CCEA002CB862 /* SpeziFirebaseStorage in Frameworks */, @@ -633,11 +615,7 @@ 2FE5DC3D29EDD7E4004B9AB4 /* Helper */ = { isa = PBXGroup; children = ( - 2FE5DC4229EDD7F2004B9AB4 /* Binding+Negate.swift */, - 2FE5DC4329EDD7F2004B9AB4 /* Bundle+Image.swift */, - 2FE5DC4429EDD7F2004B9AB4 /* CodableArray+RawRepresentable.swift */, A9BCB5852AE8347E00DA8588 /* CollectionReference+AsyncAwait.swift */, - A916ADD62AB62012006960DF /* ProcessInfo+PreviewSimulator.swift */, ); path = Helper; sourceTree = ""; @@ -766,10 +744,11 @@ A926D8342AB7C2CC000C4C2F /* Mock */ = { isa = PBXGroup; children = ( - 2DC175A8D7EDF6EE1B55E859 /* MockDeviceManager.swift */, + A94AF8692C9754C000351E0A /* Views */, 2DC17D172D8299600ED13D60 /* MockDevice.swift */, + 2DC175A8D7EDF6EE1B55E859 /* MockDeviceManager.swift */, 2DC17D48217697F558657E69 /* MockMeasurementGenerator.swift */, - 2DC1794C7379ACE157EE11C4 /* MockDeviceRow.swift */, + A94AF8602C9752D600351E0A /* View+ScanNearbyMockDevices.swift */, ); path = Mock; sourceTree = ""; @@ -801,13 +780,10 @@ A945340E2AEAF2860095AAD3 /* Tiles */ = { isa = PBXGroup; children = ( - A9F2ECC82AEC2C300057C7DD /* CompletedTile.swift */, A9F2ECC52AEB27B10057C7DD /* MeasurementTile.swift */, A94533F92AEADC8E0095AAD3 /* ScreeningTile.swift */, - A9F2ECCC2AEC58B00057C7DD /* SimpleTile.swift */, A945340B2AEAE6380095AAD3 /* TilesView.swift */, 2DC177BFA6C401C2C87FCD5C /* TileType.swift */, - A9D4B8EB2B686D380054E27C /* ScreeningTileHeader.swift */, ); path = Tiles; sourceTree = ""; @@ -831,6 +807,14 @@ path = Account; sourceTree = ""; }; + A94AF8692C9754C000351E0A /* Views */ = { + isa = PBXGroup; + children = ( + A94AF8682C9754C000351E0A /* MockDeviceRow.swift */, + ); + path = Views; + sourceTree = ""; + }; A967061A2B1AA28100C17BE5 /* Chart */ = { isa = PBXGroup; children = ( @@ -942,13 +926,14 @@ A99522402AA61D82009272F4 /* Muse */ = { isa = PBXGroup; children = ( + 2DC17C8A120ECD3394366958 /* MuseDevice.swift */, + A943DC232BA11C0C00039060 /* MuseDeviceInformation.swift */, + 2DC17DCD9F44F68CF55EC3BE /* MuseDeviceManager.swift */, + A94AF8632C9753FE00351E0A /* View+ScanNearbyMuseDevices.swift */, A926D7F32AB7B41B000C4C2F /* Extensions */, A926D7FB2AB7B41C000C4C2F /* Model */, A926D7F72AB7B41B000C4C2F /* Recording */, A9EB34D02B64DAB000FD62C3 /* Views */, - 2DC17C8A120ECD3394366958 /* MuseDevice.swift */, - 2DC17DCD9F44F68CF55EC3BE /* MuseDeviceManager.swift */, - A943DC232BA11C0C00039060 /* MuseDeviceInformation.swift */, ); path = Muse; sourceTree = ""; @@ -972,13 +957,13 @@ A926D8342AB7C2CC000C4C2F /* Mock */, A99522402AA61D82009272F4 /* Muse */, A93B82BF2B76E2E000C5DF3D /* AutoConnectConfigurationView.swift */, - A988FEAB2B043AED00022A61 /* BatteryIcon.swift */, A9CE84502B1A9A14009CE3F4 /* NearbyDevicesView.swift */, 2DC172078D2802AD6068AC7E /* DeviceCoordinator.swift */, 2DC17AEF81E62461D4AA3E67 /* ConnectedDevice.swift */, A93B82C22B76E72200C5DF3D /* AutoConnectConfiguration.swift */, A943DC292BA236A300039060 /* ConnectedDeviceDestination.swift */, A943DC3C2BA36B3400039060 /* NAMSDevice.swift */, + A94AF85D2C9752B300351E0A /* EmptyScanningState.swift */, ); path = Devices; sourceTree = ""; @@ -1034,7 +1019,6 @@ children = ( 2F4E23822989D51F0013F3D9 /* NAMSTestingSetup.swift */, 2FE5DC3E29EDD7ED004B9AB4 /* FeatureFlags.swift */, - 2DC177B4AFB1C891EDB8152B /* BiopotDevicePreview.swift */, A9A48A872BA8C13D002CB862 /* AutoStartRecordingView.swift */, ); path = Testing; @@ -1065,7 +1049,7 @@ buildRules = ( ); dependencies = ( - A96CD4142BCDA58100300B8C /* PBXTargetDependency */, + A977F67E2C933AA10071A1D1 /* PBXTargetDependency */, ); name = NAMS; packageProductDependencies = ( @@ -1083,12 +1067,12 @@ A92E34EF2ADB9B7E00FE0B51 /* OrderedCollections */, A988FEBC2B05C7AE00022A61 /* SpeziPersonalInfo */, A988FEA52B03FB4A00022A61 /* SpeziBluetooth */, - A9C82F942B6089C8004703E0 /* BluetoothServices */, - A95EEAE22B6B54D3009B4CF8 /* BluetoothViews */, A95F4B302B990803006778E5 /* ByteCoding */, A95F4B322B990803006778E5 /* EDFFormat */, A941711B2B9FB17A002E975A /* SpeziFirebaseAccountStorage */, A9A48A8A2BA8CCE3002CB862 /* SpeziFirebaseStorage */, + A977F6742C932C780071A1D1 /* SpeziBluetoothServices */, + A977F6772C932D0E0071A1D1 /* SpeziDevicesUI */, ); productName = NAMS; productReference = 653A254D283387FE005D4D48 /* NAMS.app */; @@ -1165,12 +1149,12 @@ A92E34F12ADB9B9000FE0B51 /* OrderedCollections */, A988FEBE2B05C7B800022A61 /* SpeziPersonalInfo */, A988FEA72B03FB5A00022A61 /* SpeziBluetooth */, - A9C82F962B6089D2004703E0 /* BluetoothServices */, - A95EEAE42B6B54DA009B4CF8 /* BluetoothViews */, A95F4B362B99081D006778E5 /* ByteCoding */, A95F4B382B99081D006778E5 /* EDFFormat */, A941711D2B9FB180002E975A /* SpeziFirebaseAccountStorage */, A9A48A8C2BA8CCEA002CB862 /* SpeziFirebaseStorage */, + A977F6792C932D1E0071A1D1 /* SpeziBluetoothServices */, + A977F67B2C932D2A0071A1D1 /* SpeziDevicesUI */, ); productName = NAMS; productReference = A926D7C32AB7A552000C4C2F /* NAMS Muse.app */; @@ -1184,7 +1168,7 @@ attributes = { BuildIndependentTargetsInParallel = 1; LastSwiftUpdateCheck = 1340; - LastUpgradeCheck = 1530; + LastUpgradeCheck = 1610; TargetAttributes = { 653A254C283387FE005D4D48 = { CreatedOnToolsVersion = 13.4; @@ -1224,6 +1208,7 @@ A988FEA42B03FB4A00022A61 /* XCRemoteSwiftPackageReference "SpeziBluetooth" */, A95F4B2F2B990803006778E5 /* XCRemoteSwiftPackageReference "SpeziFileFormats" */, A96CD4122BCDA57700300B8C /* XCRemoteSwiftPackageReference "SwiftLint" */, + A977F6762C932D0E0071A1D1 /* XCRemoteSwiftPackageReference "SpeziDevices" */, ); productRefGroup = 653A254E283387FE005D4D48 /* Products */; projectDirPath = ""; @@ -1307,8 +1292,6 @@ A9D4B8D92B685D800054E27C /* MuseInterventionRequiredHint.swift in Sources */, A943DC3D2BA36B3400039060 /* NAMSDevice.swift in Sources */, A94534092AEAE3490095AAD3 /* ScheduleView.swift in Sources */, - 2FE5DC4529EDD7F2004B9AB4 /* Binding+Negate.swift in Sources */, - A916ADD72AB62012006960DF /* ProcessInfo+PreviewSimulator.swift in Sources */, A94A42B62AE9EBE300A3F9E5 /* AccountSetupHeader.swift in Sources */, A926D82A2AB7B430000C4C2F /* EEGChart.swift in Sources */, A943DC242BA11C0C00039060 /* MuseDeviceInformation.swift in Sources */, @@ -1318,6 +1301,8 @@ A9DF79DA2AE8986B00AB5983 /* SelectedPatientCard.swift in Sources */, A98905452BCEFD6800E36BA7 /* ChartMoreInformationView.swift in Sources */, A926D8202AB7B430000C4C2F /* EEGChannelMark.swift in Sources */, + A94AF86A2C9754C000351E0A /* MockDeviceRow.swift in Sources */, + A94AF8622C9752DF00351E0A /* View+ScanNearbyMockDevices.swift in Sources */, A916ADD92AB6217D006960DF /* OnboardingFlow+PreviewSimulator.swift in Sources */, A989112D2A36687B00E66E3A /* PatientListSheet.swift in Sources */, 2FC975A82978F11A00BA99FE /* Home.swift in Sources */, @@ -1332,11 +1317,9 @@ 2FE5DC3729EDD7CA004B9AB4 /* OnboardingFlow.swift in Sources */, A9D4B8DC2B685D800054E27C /* MuseBatteryProblemsHint.swift in Sources */, A94A42BA2AE9ED8300A3F9E5 /* AccountOnboarding.swift in Sources */, - 2FE5DC4729EDD7F2004B9AB4 /* CodableArray+RawRepresentable.swift in Sources */, 2FE5DC4029EDD7EE004B9AB4 /* FeatureFlags.swift in Sources */, + A94AF8642C97540400351E0A /* View+ScanNearbyMuseDevices.swift in Sources */, A907DA362B1942B800FB69FB /* DataAcquisition.swift in Sources */, - A9D4B8EC2B686D380054E27C /* ScreeningTileHeader.swift in Sources */, - 2FE5DC4629EDD7F2004B9AB4 /* Bundle+Image.swift in Sources */, A988FEB52B0453E100022A61 /* DeviceInformation.swift in Sources */, A9F2ECD02AEC5EF50057C7DD /* MeasurementTask.swift in Sources */, A943DC432BA3B2B500039060 /* ChangeChartLayoutView.swift in Sources */, @@ -1353,7 +1336,6 @@ A98911322A36689D00E66E3A /* Patient.swift in Sources */, 2FE5DC2629EDD38A004B9AB4 /* Contacts.swift in Sources */, A94534102AEAF2AE0095AAD3 /* CompletedTask.swift in Sources */, - A988FEAC2B043AED00022A61 /* BatteryIcon.swift in Sources */, 2DC17337BA4FEF5664BC0D10 /* FinishedSetup.swift in Sources */, A943DC312BA26E0F00039060 /* TimedSample.swift in Sources */, A988FEAA2B0414FD00022A61 /* BiopotDevice.swift in Sources */, @@ -1379,12 +1361,12 @@ 2DC173E02BF55765A906AF4F /* IXNMuseDataPacketType+Type.swift in Sources */, A945340C2AEAE6380095AAD3 /* TilesView.swift in Sources */, A907DA392B195D4800FB69FB /* AccelerometerSample.swift in Sources */, - A9F2ECCD2AEC58B00057C7DD /* SimpleTile.swift in Sources */, 2DC179421EE83DA24520EABB /* HeadbandFit+Muse.swift in Sources */, A90830842BAA33E6003E0080 /* DownsampleConfiguration.swift in Sources */, A97E4F1F2B1EA0D600E25505 /* StartRecordingView.swift in Sources */, A9CE84512B1A9A14009CE3F4 /* NearbyDevicesView.swift in Sources */, A9D4B8E62B6860AC0054E27C /* MuseHeadbandFitView.swift in Sources */, + A94AF85E2C9752B500351E0A /* EmptyScanningState.swift in Sources */, 2DC17F8981E53AEED0CFD1BD /* MockDevice.swift in Sources */, A943DC3A2BA274B200039060 /* BufferedChannel.swift in Sources */, 2DC17C29AA0382E9F5F2AA4D /* MockMeasurementGenerator.swift in Sources */, @@ -1394,12 +1376,10 @@ A9BB13E62B9BCE85004EB9B9 /* PatientInformationHeader.swift in Sources */, A985DECA2BA4DB9300DDEBF8 /* BiopotDeviceRow.swift in Sources */, A9770E152B9A7D2500C01276 /* PatientInformation+Patient.swift in Sources */, - A9F2ECC92AEC2C300057C7DD /* CompletedTile.swift in Sources */, A9D4B8D52B685D800054E27C /* MuseDeviceDetailsView.swift in Sources */, 2DC1718A3F968CF02D7AF0EC /* PatientListView.swift in Sources */, 2DC172753D306AE733D7FDC8 /* TileType.swift in Sources */, 2DC17257A28A7E2232229658 /* PatientTask.swift in Sources */, - 2DC174CCCA1DAC48C45CDAC4 /* BiopotDevicePreview.swift in Sources */, A985DEBF2BA4D29800DDEBF8 /* PredefinedElectrodeLocation.swift in Sources */, 2DC1776253EA8999F231D6DA /* MuseDevice.swift in Sources */, A9D4B8D82B685D800054E27C /* MuseAboutDetailsSection.swift in Sources */, @@ -1408,7 +1388,6 @@ A985DEBB2BA4CC7F00DDEBF8 /* BiopotElectrodeLocationsEditView.swift in Sources */, A9216A742BA4C3A500B8A3F2 /* BiopotElectrodeLocations.swift in Sources */, 2DC17CAFDF6974EAA57C5D34 /* MuseDeviceList.swift in Sources */, - 2DC172E36CAF9AA8F191F723 /* MockDeviceRow.swift in Sources */, 2DC17100BA13C8B5325EBD94 /* EEGRecordings.swift in Sources */, 2DC17A3ADA039E3FD901D8CF /* HeadbandFit.swift in Sources */, 2DC17D19CFECCEF0A7206F35 /* ConnectionState.swift in Sources */, @@ -1469,16 +1448,16 @@ A943DC3E2BA36B3400039060 /* NAMSDevice.swift in Sources */, A945340A2AEAE3490095AAD3 /* ScheduleView.swift in Sources */, A926D7872AB7A552000C4C2F /* Welcome.swift in Sources */, - A926D7882AB7A552000C4C2F /* Binding+Negate.swift in Sources */, A94A42B72AE9EBE300A3F9E5 /* AccountSetupHeader.swift in Sources */, A9D4B8DD2B685DEB0054E27C /* MuseDeviceDetailsView.swift in Sources */, A943DC252BA11C0C00039060 /* MuseDeviceInformation.swift in Sources */, - A926D78A2AB7A552000C4C2F /* ProcessInfo+PreviewSimulator.swift in Sources */, A9F2ECC72AEB27B10057C7DD /* MeasurementTile.swift in Sources */, A943DC282BA22F5900039060 /* NAMSStandard.swift in Sources */, A9DF79E32AE8A82B00AB5983 /* SelectedPatientCard.swift in Sources */, A98905462BCEFD6800E36BA7 /* ChartMoreInformationView.swift in Sources */, A926D82B2AB7B430000C4C2F /* EEGChart.swift in Sources */, + A94AF86B2C9754C000351E0A /* MockDeviceRow.swift in Sources */, + A94AF8612C9752DF00351E0A /* View+ScanNearbyMockDevices.swift in Sources */, A926D8212AB7B430000C4C2F /* EEGChannelMark.swift in Sources */, A926D78B2AB7A552000C4C2F /* OnboardingFlow+PreviewSimulator.swift in Sources */, A926D78C2AB7A552000C4C2F /* Home.swift in Sources */, @@ -1494,8 +1473,7 @@ A9BCB5842AE830FB00DA8588 /* NewPatientModel.swift in Sources */, A9DF79DE2AE8A80D00AB5983 /* Patient.swift in Sources */, A907DA372B1942B800FB69FB /* DataAcquisition.swift in Sources */, - A926D7912AB7A552000C4C2F /* CodableArray+RawRepresentable.swift in Sources */, - A9D4B8ED2B686D380054E27C /* ScreeningTileHeader.swift in Sources */, + A94AF8652C97540400351E0A /* View+ScanNearbyMuseDevices.swift in Sources */, A926D7922AB7A552000C4C2F /* FeatureFlags.swift in Sources */, A988FEB62B0453E100022A61 /* DeviceInformation.swift in Sources */, A9C82F932B60899B004703E0 /* ImpedanceMeasurement.swift in Sources */, @@ -1504,7 +1482,6 @@ A941711A2B9FB0BB002E975A /* Sex+Patient.swift in Sources */, A9BCB5832AE8307800DA8588 /* SearchToken.swift in Sources */, A9D6BA832BACCB78002863DA /* RecordingState.swift in Sources */, - A926D7932AB7A552000C4C2F /* Bundle+Image.swift in Sources */, A943DC1F2BA0CA9500039060 /* BiopotService.swift in Sources */, A907DA342B193C9700FB69FB /* SamplingConfiguration.swift in Sources */, A9BCB58D2AE84E6D00DA8588 /* CurrentPatientLabel.swift in Sources */, @@ -1527,7 +1504,6 @@ A93B82C42B76E72200C5DF3D /* AutoConnectConfiguration.swift in Sources */, A926D8042AB7B41C000C4C2F /* BDFSample+Muse.swift in Sources */, A974D6482B9C15540058C164 /* InvestigatorCodeKey.swift in Sources */, - A988FEAD2B043AED00022A61 /* BatteryIcon.swift in Sources */, A9D4B8E02B685DF20054E27C /* MuseAboutDetailsSection.swift in Sources */, 2DC17B21929D86939F8EB566 /* ConnectionState+Muse.swift in Sources */, A9D4B8DE2B685DEE0054E27C /* MuseHeadbandFitSection.swift in Sources */, @@ -1544,8 +1520,8 @@ A945340D2AEAE6380095AAD3 /* TilesView.swift in Sources */, A90830852BAA33E6003E0080 /* DownsampleConfiguration.swift in Sources */, A907DA3A2B195D4800FB69FB /* AccelerometerSample.swift in Sources */, - A9F2ECCE2AEC58B00057C7DD /* SimpleTile.swift in Sources */, 2DC1762E730B0472308EEFFC /* IXNMuseDataPacketType+Type.swift in Sources */, + A94AF85F2C9752B500351E0A /* EmptyScanningState.swift in Sources */, A97E4F202B1EA0D600E25505 /* StartRecordingView.swift in Sources */, A943DC3B2BA274B200039060 /* BufferedChannel.swift in Sources */, A9D4B8E72B68636A0054E27C /* MuseHeadbandFitView.swift in Sources */, @@ -1557,12 +1533,10 @@ A9D4B8E12B685DF30054E27C /* MuseInterventionRequiredHint.swift in Sources */, 2DC176E7B29173393F43A357 /* MockDevice.swift in Sources */, 2DC179F4A6B69C07C1A440D2 /* MockMeasurementGenerator.swift in Sources */, - A9F2ECCA2AEC2C300057C7DD /* CompletedTile.swift in Sources */, 2DC17686B3AEB09A8F60AB8E /* PatientListView.swift in Sources */, 2DC17ADF934F839FC66BF7A0 /* TileType.swift in Sources */, 2DC1727A98890570E5A4B46D /* PatientTask.swift in Sources */, A985DEC02BA4D29800DDEBF8 /* PredefinedElectrodeLocation.swift in Sources */, - 2DC17206C9867ACAC0915363 /* BiopotDevicePreview.swift in Sources */, 2DC17F50A02902B6757A435B /* MuseDevice.swift in Sources */, A9D4B8E32B685DF80054E27C /* MuseHeadbandFitProblemsHint.swift in Sources */, 2DC17877F0B86FDEC613124B /* DeviceCoordinator.swift in Sources */, @@ -1570,7 +1544,6 @@ A9216A752BA4C3A500B8A3F2 /* BiopotElectrodeLocations.swift in Sources */, 2DC173F479B53B9054330880 /* MuseDeviceRow.swift in Sources */, 2DC17A843968AEFAB1B64C3B /* MuseDeviceList.swift in Sources */, - 2DC17CD906FF31BA6EA4CACD /* MockDeviceRow.swift in Sources */, 2DC17995C927FACA6C0146B3 /* EEGRecordings.swift in Sources */, 2DC1776FFC7360126770D201 /* HeadbandFit.swift in Sources */, A9D4B8EA2B6863D60054E27C /* FitLabel.swift in Sources */, @@ -1594,14 +1567,14 @@ target = 653A254C283387FE005D4D48 /* NAMS */; targetProxy = 653A256828338800005D4D48 /* PBXContainerItemProxy */; }; - A96CD4142BCDA58100300B8C /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - productRef = A96CD4132BCDA58100300B8C /* SwiftLintPlugin */; - }; A96CD4162BCDA58600300B8C /* PBXTargetDependency */ = { isa = PBXTargetDependency; productRef = A96CD4152BCDA58600300B8C /* SwiftLintPlugin */; }; + A977F67E2C933AA10071A1D1 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + productRef = A977F67D2C933AA10071A1D1 /* SwiftLintBuildToolPlugin */; + }; /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ @@ -1851,7 +1824,6 @@ 653A257528338800005D4D48 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; @@ -1870,7 +1842,6 @@ 653A257628338800005D4D48 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; @@ -1889,7 +1860,6 @@ 653A257828338800005D4D48 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = ""; @@ -1907,7 +1877,6 @@ 653A257928338800005D4D48 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = ""; @@ -2236,7 +2205,6 @@ A9EC7BFD2AE715F9003D572E /* Test */ = { isa = XCBuildConfiguration; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; @@ -2255,7 +2223,6 @@ A9EC7BFE2AE715F9003D572E /* Test */ = { isa = XCBuildConfiguration; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = ""; @@ -2328,10 +2295,10 @@ /* Begin XCRemoteSwiftPackageReference section */ 2F49B7742980407B00BCB272 /* XCRemoteSwiftPackageReference "Spezi" */ = { isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/StanfordSpezi/Spezi"; + repositoryURL = "https://github.com/StanfordSpezi/Spezi.git"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 1.0.0; + minimumVersion = 1.8.0; }; }; 2FE5DC6229EDD883004B9AB4 /* XCRemoteSwiftPackageReference "SpeziAccount" */ = { @@ -2339,7 +2306,7 @@ repositoryURL = "https://github.com/StanfordSpezi/SpeziAccount.git"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 1.2.1; + minimumVersion = 2.0.1; }; }; 2FE5DC6529EDD894004B9AB4 /* XCRemoteSwiftPackageReference "SpeziContact" */ = { @@ -2355,7 +2322,7 @@ repositoryURL = "https://github.com/StanfordSpezi/SpeziFirebase.git"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 1.0.0; + minimumVersion = 2.0.0; }; }; 2FE5DC7F29EDD91D004B9AB4 /* XCRemoteSwiftPackageReference "SpeziOnboarding" */ = { @@ -2387,7 +2354,7 @@ repositoryURL = "https://github.com/StanfordSpezi/SpeziViews.git"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 1.2.0; + minimumVersion = 1.7.0; }; }; 2FE5DC9029EDD9C3004B9AB4 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */ = { @@ -2395,15 +2362,15 @@ repositoryURL = "https://github.com/firebase/firebase-ios-sdk.git"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 10.17.0; + minimumVersion = 11.1.0; }; }; 2FE5DC9729EDD9D9004B9AB4 /* XCRemoteSwiftPackageReference "XCTestExtensions" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/StanfordBDHG/XCTestExtensions.git"; requirement = { - kind = upToNextMinorVersion; - minimumVersion = 0.4.10; + kind = upToNextMajorVersion; + minimumVersion = 1.0.0; }; }; A926D7662AB7A552000C4C2F /* XCRemoteSwiftPackageReference "Spezi" */ = { @@ -2475,23 +2442,31 @@ repositoryURL = "https://github.com/apple/swift-collections.git"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 1.0.5; + minimumVersion = 1.1.0; }; }; A95F4B2F2B990803006778E5 /* XCRemoteSwiftPackageReference "SpeziFileFormats" */ = { isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/StanfordSpezi/SpeziFileFormats"; + repositoryURL = "https://github.com/StanfordSpezi/SpeziFileFormats.git"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 1.2.0; + minimumVersion = 2.1.0; }; }; A96CD4122BCDA57700300B8C /* XCRemoteSwiftPackageReference "SwiftLint" */ = { isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/realm/SwiftLint"; + repositoryURL = "https://github.com/realm/SwiftLint.git"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 0.54.0; + minimumVersion = 0.57.0; + }; + }; + A977F6762C932D0E0071A1D1 /* XCRemoteSwiftPackageReference "SpeziDevices" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/StanfordSpezi/SpeziDevices.git"; + requirement = { + branch = "fix/accessibility-children"; + kind = branch; }; }; A988FEA42B03FB4A00022A61 /* XCRemoteSwiftPackageReference "SpeziBluetooth" */ = { @@ -2499,7 +2474,7 @@ repositoryURL = "https://github.com/StanfordSpezi/SpeziBluetooth"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 1.0.2; + minimumVersion = 3.1.0; }; }; /* End XCRemoteSwiftPackageReference section */ @@ -2640,16 +2615,6 @@ package = 2FE5DC7329EDD8E6004B9AB4 /* XCRemoteSwiftPackageReference "SpeziFirebase" */; productName = SpeziFirebaseAccountStorage; }; - A95EEAE22B6B54D3009B4CF8 /* BluetoothViews */ = { - isa = XCSwiftPackageProductDependency; - package = A988FEA42B03FB4A00022A61 /* XCRemoteSwiftPackageReference "SpeziBluetooth" */; - productName = BluetoothViews; - }; - A95EEAE42B6B54DA009B4CF8 /* BluetoothViews */ = { - isa = XCSwiftPackageProductDependency; - package = A988FEA42B03FB4A00022A61 /* XCRemoteSwiftPackageReference "SpeziBluetooth" */; - productName = BluetoothViews; - }; A95F4B302B990803006778E5 /* ByteCoding */ = { isa = XCSwiftPackageProductDependency; package = A95F4B2F2B990803006778E5 /* XCRemoteSwiftPackageReference "SpeziFileFormats" */; @@ -2675,15 +2640,35 @@ package = A95F4B2F2B990803006778E5 /* XCRemoteSwiftPackageReference "SpeziFileFormats" */; productName = EDFFormat; }; - A96CD4132BCDA58100300B8C /* SwiftLintPlugin */ = { + A96CD4152BCDA58600300B8C /* SwiftLintPlugin */ = { isa = XCSwiftPackageProductDependency; package = A96CD4122BCDA57700300B8C /* XCRemoteSwiftPackageReference "SwiftLint" */; productName = "plugin:SwiftLintPlugin"; }; - A96CD4152BCDA58600300B8C /* SwiftLintPlugin */ = { + A977F6742C932C780071A1D1 /* SpeziBluetoothServices */ = { + isa = XCSwiftPackageProductDependency; + package = A988FEA42B03FB4A00022A61 /* XCRemoteSwiftPackageReference "SpeziBluetooth" */; + productName = SpeziBluetoothServices; + }; + A977F6772C932D0E0071A1D1 /* SpeziDevicesUI */ = { + isa = XCSwiftPackageProductDependency; + package = A977F6762C932D0E0071A1D1 /* XCRemoteSwiftPackageReference "SpeziDevices" */; + productName = SpeziDevicesUI; + }; + A977F6792C932D1E0071A1D1 /* SpeziBluetoothServices */ = { + isa = XCSwiftPackageProductDependency; + package = A988FEA42B03FB4A00022A61 /* XCRemoteSwiftPackageReference "SpeziBluetooth" */; + productName = SpeziBluetoothServices; + }; + A977F67B2C932D2A0071A1D1 /* SpeziDevicesUI */ = { + isa = XCSwiftPackageProductDependency; + package = A977F6762C932D0E0071A1D1 /* XCRemoteSwiftPackageReference "SpeziDevices" */; + productName = SpeziDevicesUI; + }; + A977F67D2C933AA10071A1D1 /* SwiftLintBuildToolPlugin */ = { isa = XCSwiftPackageProductDependency; package = A96CD4122BCDA57700300B8C /* XCRemoteSwiftPackageReference "SwiftLint" */; - productName = "plugin:SwiftLintPlugin"; + productName = "plugin:SwiftLintBuildToolPlugin"; }; A988FEA52B03FB4A00022A61 /* SpeziBluetooth */ = { isa = XCSwiftPackageProductDependency; @@ -2715,16 +2700,6 @@ package = 2FE5DC7329EDD8E6004B9AB4 /* XCRemoteSwiftPackageReference "SpeziFirebase" */; productName = SpeziFirebaseStorage; }; - A9C82F942B6089C8004703E0 /* BluetoothServices */ = { - isa = XCSwiftPackageProductDependency; - package = A988FEA42B03FB4A00022A61 /* XCRemoteSwiftPackageReference "SpeziBluetooth" */; - productName = BluetoothServices; - }; - A9C82F962B6089D2004703E0 /* BluetoothServices */ = { - isa = XCSwiftPackageProductDependency; - package = A988FEA42B03FB4A00022A61 /* XCRemoteSwiftPackageReference "SpeziBluetooth" */; - productName = BluetoothServices; - }; /* End XCSwiftPackageProductDependency section */ }; rootObject = 653A2545283387FE005D4D48 /* Project object */; diff --git a/NAMS.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/NAMS.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index af385ae..0d7c672 100644 --- a/NAMS.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/NAMS.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,13 +1,13 @@ { - "originHash" : "5086b61a408696c4f726af4d46ae5275d0699ee624e8d619b38c592bbebc03f2", + "originHash" : "41c3afd956af575fe1bd0f7d491eaeb69bd3bcf5768b5320f90b1d955856af72", "pins" : [ { "identity" : "abseil-cpp-binary", "kind" : "remoteSourceControl", "location" : "https://github.com/google/abseil-cpp-binary.git", "state" : { - "revision" : "748c7837511d0e6a507737353af268484e1745e2", - "version" : "1.2024011601.1" + "revision" : "194a6706acbd25e4ef639bcaddea16e8758a3e27", + "version" : "1.2024011602.0" } }, { @@ -15,8 +15,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/google/app-check.git", "state" : { - "revision" : "c218c2054299b15ae577e818bbba16084d3eabe6", - "version" : "10.18.2" + "revision" : "21fe1af9be463a359aaf8d96789ef73fc3760d09", + "version" : "11.0.1" } }, { @@ -51,8 +51,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/firebase/firebase-ios-sdk.git", "state" : { - "revision" : "888f0b6026e2441a69e3ee2ad5293c7a92031e62", - "version" : "10.23.1" + "revision" : "1fc52ab0e172e7c5a961f975a76c2611f4f22852", + "version" : "11.2.0" } }, { @@ -60,8 +60,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/google/GoogleAppMeasurement.git", "state" : { - "revision" : "c7a5917ebe48d69f421aadf154ef3969c8b7f12d", - "version" : "10.23.1" + "revision" : "07a2f57d147d2bf368a0d2dcb5579ff082d9e44f", + "version" : "11.1.0" } }, { @@ -69,8 +69,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/google/GoogleDataTransport.git", "state" : { - "revision" : "a637d318ae7ae246b02d7305121275bc75ed5565", - "version" : "9.4.0" + "revision" : "617af071af9aa1d6a091d59a202910ac482128f9", + "version" : "10.1.0" } }, { @@ -78,8 +78,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/google/GoogleUtilities.git", "state" : { - "revision" : "26c898aed8bed13b8a63057ee26500abbbcb8d55", - "version" : "7.13.1" + "revision" : "53156c7ec267db846e6b64c9f4c4e31ba4cf75eb", + "version" : "8.0.2" } }, { @@ -87,8 +87,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/google/grpc-binary.git", "state" : { - "revision" : "e9fad491d0673bdda7063a0341fb6b47a30c5359", - "version" : "1.62.2" + "revision" : "f56d8fc3162de9a498377c7b6cea43431f4f5083", + "version" : "1.65.1" } }, { @@ -96,8 +96,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/google/gtm-session-fetcher.git", "state" : { - "revision" : "9534039303015a84837090d20fa21cae6e5eadb6", - "version" : "3.3.2" + "revision" : "a2ab612cb980066ee56d90d60d8462992c07f24b", + "version" : "3.5.0" } }, { @@ -159,8 +159,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/jpsim/SourceKitten.git", "state" : { - "revision" : "b6dc09ee51dfb0c66e042d2328c017483a1a5d56", - "version" : "0.34.1" + "revision" : "fd4df99170f5e9d7cf9aa8312aa8506e0e7a44e7", + "version" : "0.35.0" } }, { @@ -168,8 +168,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/StanfordSpezi/Spezi", "state" : { - "revision" : "c43e4fa3d3938a847de2b677091a34ddaea5bc76", - "version" : "1.2.3" + "revision" : "4513a697572e8e1faea1e0ee52e6fad4b8d3dd8d", + "version" : "1.8.0" } }, { @@ -177,8 +177,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/StanfordSpezi/SpeziAccount.git", "state" : { - "revision" : "cb9441e5fe9ca31a17be2507d03817a080e63e9d", - "version" : "1.2.2" + "revision" : "7ffb73de1c874c78029b620f74d6411bdf65f3fc", + "version" : "2.0.1" } }, { @@ -186,8 +186,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/StanfordSpezi/SpeziBluetooth", "state" : { - "revision" : "3178bc1184dc6636e8112694e09fdfca7cbcf7d7", - "version" : "1.0.2" + "revision" : "977cc675d8c2dca51eabd52dc0e9476702c678eb", + "version" : "3.1.0" } }, { @@ -199,13 +199,22 @@ "version" : "1.0.0" } }, + { + "identity" : "spezidevices", + "kind" : "remoteSourceControl", + "location" : "https://github.com/StanfordSpezi/SpeziDevices.git", + "state" : { + "branch" : "fix/accessibility-children", + "revision" : "6bd8e61f8153d0055f43fbe4260271b5bd7a2326" + } + }, { "identity" : "spezifileformats", "kind" : "remoteSourceControl", "location" : "https://github.com/StanfordSpezi/SpeziFileFormats", "state" : { - "revision" : "017ea997f4f6128b15e8b8d4aac979cb7a822e74", - "version" : "1.2.0" + "revision" : "3a7b6fc8843af63f7f4e8f8e0a4bf6ad5da9c446", + "version" : "2.1.2" } }, { @@ -213,8 +222,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/StanfordSpezi/SpeziFirebase.git", "state" : { - "revision" : "16c1c751c14b08ae593eacf9bc2752c2e070fe2f", - "version" : "1.1.0" + "revision" : "7c6829624884f6f1d700e0316b2580b39d3b0c5f", + "version" : "2.0.0" } }, { @@ -222,8 +231,17 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/StanfordSpezi/SpeziFoundation.git", "state" : { - "revision" : "01af5b91a54f30ddd121258e81aff2ddc2a99ff9", - "version" : "1.0.4" + "revision" : "5b4ad1b343154b52a68c33a6bfe02d9cb07cb9dc", + "version" : "2.0.0" + } + }, + { + "identity" : "spezinetworking", + "kind" : "remoteSourceControl", + "location" : "https://github.com/StanfordSpezi/SpeziNetworking", + "state" : { + "revision" : "26f1ed49e1916ae4fdc1bd033127a873a9e6dd0a", + "version" : "2.1.3" } }, { @@ -249,8 +267,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/StanfordSpezi/SpeziStorage.git", "state" : { - "revision" : "b958df9b31f24800388a7bfc28f457ce7b82556c", - "version" : "1.0.2" + "revision" : "60962375bbce7cc0599bc3da4ebda08231922371", + "version" : "1.2.0" } }, { @@ -258,8 +276,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/StanfordSpezi/SpeziViews.git", "state" : { - "revision" : "4d2a724d97c8f19ac7de7aa2c046b1cb3ef7b279", - "version" : "1.3.1" + "revision" : "f87514406bb57ae67d0040eec5454fff55104143", + "version" : "1.7.0" } }, { @@ -285,8 +303,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-collections.git", "state" : { - "revision" : "94cf62b3ba8d4bed62680a282d4c25f9c63c2efb", - "version" : "1.1.0" + "revision" : "9bf03ff58ce34478e66aaee630e491823326fd06", + "version" : "1.1.3" } }, { @@ -310,10 +328,10 @@ { "identity" : "swift-syntax", "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-syntax.git", + "location" : "https://github.com/swiftlang/swift-syntax.git", "state" : { - "revision" : "6ad4ea24b01559dde0773e3d091f1b9e36175036", - "version" : "509.0.2" + "revision" : "515f79b522918f83483068d99c68daeb5116342d", + "version" : "600.0.0-prerelease-2024-08-14" } }, { @@ -330,8 +348,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/realm/SwiftLint", "state" : { - "revision" : "f17a4f9dfb6a6afb0408426354e4180daaf49cee", - "version" : "0.54.0" + "revision" : "168fb98ed1f3e343d703ecceaf518b6cf565207b", + "version" : "0.57.0" } }, { @@ -357,8 +375,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/StanfordBDHG/XCTestExtensions.git", "state" : { - "revision" : "1fe9b8e76aeb7a132af37bfa0892160c9b662dcc", - "version" : "0.4.10" + "revision" : "5379d70249cae926927105bfb6686770f03ee5b9", + "version" : "1.1.0" } }, { @@ -366,8 +384,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/StanfordBDHG/XCTRuntimeAssertions", "state" : { - "revision" : "51da3403f128b120705571ce61e0fe190f8889e6", - "version" : "1.0.1" + "revision" : "f560ec8410af032dd485ca9386e8c2b5d3e1a1f8", + "version" : "1.1.3" } }, { @@ -381,4 +399,4 @@ } ], "version" : 3 -} \ No newline at end of file +} diff --git a/NAMS.xcodeproj/xcshareddata/xcschemes/NAMS Muse.xcscheme b/NAMS.xcodeproj/xcshareddata/xcschemes/NAMS Muse.xcscheme index 54c5565..0879fa9 100644 --- a/NAMS.xcodeproj/xcshareddata/xcschemes/NAMS Muse.xcscheme +++ b/NAMS.xcodeproj/xcshareddata/xcschemes/NAMS Muse.xcscheme @@ -1,6 +1,6 @@ = .empty("") -} - - -extension AccountValues { - var investigatorCode: String? { - storage[InvestigatorCodeKey.self] +private struct InvestigatorCodeEntry: DataEntryView { + @Binding var value: String + + var body: some View { + VerifiableTextField(AccountKeys.investigatorCode.name, text: $value) + .autocorrectionDisabled(true) + .textInputAutocapitalization(.never) + .validate(input: value, rules: .investigatorCodeMaxLength) } -} - -extension AccountKeys { - var investigatorCode: InvestigatorCodeKey.Type { - InvestigatorCodeKey.self + init(_ value: Binding) { + _value = value } } -extension InvestigatorCodeKey { - struct DataEntry: DataEntryView { - typealias Key = InvestigatorCodeKey - - @Binding var value: String +extension AccountDetails { + @AccountKey( + name: "Investigator Code", + category: .personalDetails, + as: String.self, + entryView: InvestigatorCodeEntry.self + ) + var investigatorCode: String? +} - init(_ value: Binding) { - _value = value - } - var body: some View { - VerifiableTextField(Key.name, text: $value) - .autocorrectionDisabled(true) - .textInputAutocapitalization(.never) - .validate(input: value, rules: .investigatorCodeMaxLength) - } - } +@KeyEntry(\.investigatorCode) +extension AccountKeys { } diff --git a/NAMS/Devices/BatteryIcon.swift b/NAMS/Devices/BatteryIcon.swift deleted file mode 100644 index f36c028..0000000 --- a/NAMS/Devices/BatteryIcon.swift +++ /dev/null @@ -1,93 +0,0 @@ -// -// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project -// -// SPDX-FileCopyrightText: 2023 Stanford University -// -// SPDX-License-Identifier: MIT -// - -import SwiftUI - -struct BatteryIcon: View { - private let percentage: Int - private let isCharging: Bool - - - var body: some View { - HStack { - Text(verbatim: "\(percentage) %") - batteryIcon // hides accessibility, only text will be shown - .foregroundStyle(.primary) - } - .accessibilityRepresentation { - if !isCharging { - Text(verbatim: "\(percentage) %") - } else { - Text(verbatim: "\(percentage) %, is charging") - } - } - } - - - @ViewBuilder var batteryIcon: some View { - Group { - if isCharging { - Image(systemName: "battery.100percent.bolt") - } else if percentage >= 90 { - Image(systemName: "battery.100") - } else if percentage >= 65 { - Image(systemName: "battery.75") - } else if percentage >= 40 { - Image(systemName: "battery.50") - } else if percentage >= 15 { - Image(systemName: "battery.25") - } else if percentage > 3 { - Image(systemName: "battery.25") - .symbolRenderingMode(.palette) - .foregroundStyle(.red, .primary) - } else { - Image(systemName: "battery.0") - .foregroundColor(.red) - } - } - .accessibilityHidden(true) - } - - - init(percentage: Int, isCharging: Bool) { - self.percentage = percentage - self.isCharging = isCharging - } - - init(percentage: Int) { - // isCharging=false is the same behavior as having no charging information - self.init(percentage: percentage, isCharging: false) - } -} - - -#if DEBUG -#Preview { - BatteryIcon(percentage: 100) -} - -#Preview { - BatteryIcon(percentage: 85, isCharging: true) -} - -#Preview { - BatteryIcon(percentage: 70) -} - -#Preview { - BatteryIcon(percentage: 50) -} - -#Preview { - BatteryIcon(percentage: 25) -} - -#Preview { - BatteryIcon(percentage: 10) -} -#endif diff --git a/NAMS/Devices/Biopot/BiopotDevice.swift b/NAMS/Devices/Biopot/BiopotDevice.swift index 906db1a..6222cbf 100644 --- a/NAMS/Devices/Biopot/BiopotDevice.swift +++ b/NAMS/Devices/Biopot/BiopotDevice.swift @@ -6,13 +6,13 @@ // SPDX-License-Identifier: MIT // -import BluetoothServices -import BluetoothViews import EDFFormat import OSLog import Spezi @_spi(TestingSupport) import SpeziBluetooth +import SpeziBluetoothServices +import SpeziDevices /// The BioPot 3 bluetooth device. @@ -21,7 +21,7 @@ import SpeziBluetooth /// * https://en.wikipedia.org/wiki/Bluetooth_Low_Energy#Software_model /// * https://www.bluetooth.com/blog/a-developers-guide-to-bluetooth /// * https://devzone.nordicsemi.com/guides/short-range-guides/b/bluetooth-low-energy/posts/ble-characteristics-a-beginners-tutorial -class BiopotDevice: BluetoothDevice, Identifiable, NAMSDevice { +final class BiopotDevice: BluetoothDevice, Identifiable, NAMSDevice, @unchecked Sendable { private let logger = Logger(subsystem: "edu.stanford.nams", category: "BiopotDevice") @DeviceState(\.id) @@ -30,6 +30,8 @@ class BiopotDevice: BluetoothDevice, Identifiable, NAMSDevice { var state @DeviceState(\.name) var name + @DeviceState(\.advertisementData) + var advertisementData @DeviceAction(\.connect) fileprivate var _connect @@ -82,13 +84,16 @@ class BiopotDevice: BluetoothDevice, Identifiable, NAMSDevice { } - required init() { - $state - .onChange(perform: handleChange) + required init() {} + + func configure() { + $state.onChange { @MainActor [weak self] state in + self?.handleChange(of: state) + } } - func connect() async { - await self._connect() + func connect() async throws { + try await self._connect() } func disconnect() async { @@ -131,7 +136,7 @@ extension BiopotDevice: Hashable { extension BiopotDevice: GenericBluetoothPeripheral { var label: String { - name ?? "unknown device" + advertisementData.localName ?? name ?? "unknown device" } var accessibilityLabel: String { diff --git a/NAMS/Devices/Biopot/BiopotService.swift b/NAMS/Devices/Biopot/BiopotService.swift index 151cf1e..83b7ad9 100644 --- a/NAMS/Devices/Biopot/BiopotService.swift +++ b/NAMS/Devices/Biopot/BiopotService.swift @@ -6,7 +6,8 @@ // SPDX-License-Identifier: MIT // -import class CoreBluetooth.CBUUID +@_spi(TestingSupport) +import ByteCoding import EDFFormat import OrderedCollections import OSLog @@ -22,7 +23,7 @@ class BiopotService: BluetoothService { /// The maximum amount of packets we cached when waiting for a dropped packet to be resent. private static let packetBufferMax = 3 - static let id = CBUUID(string: "FFF0") + static let id: BTUUID = "FFF0" private let logger = Logger(subsystem: "edu.stanford.nams", category: "BiopotService") @@ -64,9 +65,12 @@ class BiopotService: BluetoothService { /// The buffer is limited to `packetBufferMax`. @EEGProcessing private var packetBuffer: OrderedSet = [] - init() { - $dataAcquisition - .onChange(perform: handleDataAcquisition) + init() {} + + func configure() { + $dataAcquisition.onChange { [weak self] value in + self?.handleDataAcquisition(data: value) + } } @EEGProcessing diff --git a/NAMS/Devices/Biopot/Characteristics/AccelerometerSample.swift b/NAMS/Devices/Biopot/Characteristics/AccelerometerSample.swift index d20f4f9..fe03e79 100644 --- a/NAMS/Devices/Biopot/Characteristics/AccelerometerSample.swift +++ b/NAMS/Devices/Biopot/Characteristics/AccelerometerSample.swift @@ -23,15 +23,15 @@ struct AccelerometerSample { } -extension Point: ByteDecodable { - init?(from byteBuffer: inout ByteBuffer, preferredEndianness endianness: Endianness) { +extension Point: PrimitiveByteDecodable { + init?(from byteBuffer: inout ByteBuffer, endianness: Endianness) { guard byteBuffer.readableBytes >= 6 else { return nil } - guard let x = Int16(from: &byteBuffer, preferredEndianness: endianness), // swiftlint:disable:this identifier_name - let y = Int16(from: &byteBuffer, preferredEndianness: endianness), // swiftlint:disable:this identifier_name - let z = Int16(from: &byteBuffer, preferredEndianness: endianness) else { // swiftlint:disable:this identifier_name + guard let x = Int16(from: &byteBuffer, endianness: endianness), // swiftlint:disable:this identifier_name + let y = Int16(from: &byteBuffer, endianness: endianness), // swiftlint:disable:this identifier_name + let z = Int16(from: &byteBuffer, endianness: endianness) else { // swiftlint:disable:this identifier_name return nil } @@ -42,14 +42,14 @@ extension Point: ByteDecodable { } -extension AccelerometerSample: ByteDecodable { - init?(from byteBuffer: inout ByteBuffer, preferredEndianness endianness: Endianness) { +extension AccelerometerSample: PrimitiveByteDecodable { + init?(from byteBuffer: inout ByteBuffer, endianness: Endianness) { guard byteBuffer.readableBytes >= 12 else { return nil } - guard let point1 = Point(from: &byteBuffer, preferredEndianness: endianness), - let point2 = Point(from: &byteBuffer, preferredEndianness: endianness) else { + guard let point1 = Point(from: &byteBuffer, endianness: endianness), + let point2 = Point(from: &byteBuffer, endianness: endianness) else { return nil } diff --git a/NAMS/Devices/Biopot/Characteristics/BiopotSample.swift b/NAMS/Devices/Biopot/Characteristics/BiopotSample.swift index 3993041..90c60b5 100644 --- a/NAMS/Devices/Biopot/Characteristics/BiopotSample.swift +++ b/NAMS/Devices/Biopot/Characteristics/BiopotSample.swift @@ -17,7 +17,7 @@ struct BiopotSample: Hashable { // we always deal with 8 channel samples extension BiopotSample: ByteDecodable { - init?(from byteBuffer: inout ByteBuffer, preferredEndianness endianness: Endianness) { + init?(from byteBuffer: inout ByteBuffer) { guard byteBuffer.readableBytes >= 24 else { return nil } @@ -26,7 +26,7 @@ extension BiopotSample: ByteDecodable { channels.reserveCapacity(8) for _ in 0..<8 { - guard let channel = BDFSample(from: &byteBuffer, preferredEndianness: endianness) else { + guard let channel = BDFSample(from: &byteBuffer, endianness: .little) else { return nil } channels.append(channel) diff --git a/NAMS/Devices/Biopot/Characteristics/DataAcquisition.swift b/NAMS/Devices/Biopot/Characteristics/DataAcquisition.swift index 7b54895..e666262 100644 --- a/NAMS/Devices/Biopot/Characteristics/DataAcquisition.swift +++ b/NAMS/Devices/Biopot/Characteristics/DataAcquisition.swift @@ -60,12 +60,12 @@ extension DataAcquisition { extension DataAcquisition { // swiftlint:disable:next discouraged_optional_collection - fileprivate static func readSamples(from buffer: inout ByteBuffer, preferredEndianness endianness: Endianness, count: Int) -> [BiopotSample]? { + fileprivate static func readSamples(from buffer: inout ByteBuffer, count: Int) -> [BiopotSample]? { var samples: [BiopotSample] = [] samples.reserveCapacity(count) for _ in 0 ..< count { - guard let sample = BiopotSample(from: &buffer, preferredEndianness: endianness) else { + guard let sample = BiopotSample(from: &buffer) else { return nil } samples.append(sample) @@ -98,13 +98,13 @@ extension SomeDataAcquisition: DataAcquisition { extension DataAcquisition10: ByteDecodable { - init?(from byteBuffer: inout ByteBuffer, preferredEndianness endianness: Endianness) { + init?(from byteBuffer: inout ByteBuffer) { guard byteBuffer.readableBytes >= 244 else { // 244 bytes for type 10 return nil } - guard let totalSampleCount = UInt32(from: &byteBuffer, preferredEndianness: endianness), - let samples = Self.readSamples(from: &byteBuffer, preferredEndianness: endianness, count: 10) else { + guard let totalSampleCount = UInt32(from: &byteBuffer), + let samples = Self.readSamples(from: &byteBuffer, count: 10) else { return nil } @@ -115,14 +115,14 @@ extension DataAcquisition10: ByteDecodable { extension DataAcquisition11: ByteDecodable { - init?(from byteBuffer: inout ByteBuffer, preferredEndianness endianness: Endianness) { + init?(from byteBuffer: inout ByteBuffer) { guard byteBuffer.readableBytes >= 232 else { // 232 bytes for type 11 return nil } - guard let totalSampleCount = UInt32(from: &byteBuffer, preferredEndianness: endianness), - let samples = Self.readSamples(from: &byteBuffer, preferredEndianness: endianness, count: 9), - let accelerometerSample = AccelerometerSample(from: &byteBuffer, preferredEndianness: endianness) else { + guard let totalSampleCount = UInt32(from: &byteBuffer), + let samples = Self.readSamples(from: &byteBuffer, count: 9), + let accelerometerSample = AccelerometerSample(from: &byteBuffer, endianness: .little) else { return nil } diff --git a/NAMS/Devices/Biopot/Characteristics/DataControl.swift b/NAMS/Devices/Biopot/Characteristics/DataControl.swift index 78039e6..18c7a31 100644 --- a/NAMS/Devices/Biopot/Characteristics/DataControl.swift +++ b/NAMS/Devices/Biopot/Characteristics/DataControl.swift @@ -23,9 +23,9 @@ enum DataControl: UInt8 { } -extension DataControl: ByteCodable { - init?(from byteBuffer: inout ByteBuffer, preferredEndianness endianness: Endianness) { - guard let value = UInt8(from: &byteBuffer, preferredEndianness: endianness), +extension DataControl: PrimitiveByteCodable { + init?(from byteBuffer: inout ByteBuffer, endianness: Endianness) { + guard let value = UInt8(from: &byteBuffer, endianness: endianness), let dataControl = DataControl(rawValue: value) else { return nil } @@ -33,7 +33,7 @@ extension DataControl: ByteCodable { self = dataControl } - func encode(to byteBuffer: inout ByteBuffer, preferredEndianness endianness: Endianness) { - rawValue.encode(to: &byteBuffer, preferredEndianness: endianness) + func encode(to byteBuffer: inout ByteBuffer, endianness: Endianness) { + rawValue.encode(to: &byteBuffer, endianness: endianness) } } diff --git a/NAMS/Devices/Biopot/Characteristics/DeviceConfiguration.swift b/NAMS/Devices/Biopot/Characteristics/DeviceConfiguration.swift index c69daf6..46bb223 100644 --- a/NAMS/Devices/Biopot/Characteristics/DeviceConfiguration.swift +++ b/NAMS/Devices/Biopot/Characteristics/DeviceConfiguration.swift @@ -52,22 +52,22 @@ struct DeviceConfiguration { } -extension AccelerometerStatus: ByteCodable { - init?(from byteBuffer: inout ByteBuffer, preferredEndianness endianness: Endianness) { - guard let value = UInt8(from: &byteBuffer, preferredEndianness: endianness) else { +extension AccelerometerStatus: PrimitiveByteCodable { + init?(from byteBuffer: inout ByteBuffer, endianness: Endianness) { + guard let value = UInt8(from: &byteBuffer, endianness: endianness) else { return nil } self.init(rawValue: value) } - func encode(to byteBuffer: inout ByteBuffer, preferredEndianness endianness: Endianness) { - rawValue.encode(to: &byteBuffer, preferredEndianness: endianness) + func encode(to byteBuffer: inout ByteBuffer, endianness: Endianness) { + rawValue.encode(to: &byteBuffer, endianness: endianness) } } extension DeviceConfiguration: ByteCodable, Equatable { - init?(from byteBuffer: inout ByteBuffer, preferredEndianness endianness: Endianness) { + init?(from byteBuffer: inout ByteBuffer) { let endianness: Endianness = .big guard byteBuffer.readableBytes >= 16 else { @@ -76,14 +76,14 @@ extension DeviceConfiguration: ByteCodable, Equatable { byteBuffer.moveReaderIndex(to: 5) // reserved bytes - guard let channelCount = UInt8(from: &byteBuffer, preferredEndianness: endianness), - let accelerometerStatus = AccelerometerStatus(from: &byteBuffer, preferredEndianness: endianness), - let impedanceStatus = Bool(from: &byteBuffer, preferredEndianness: endianness), - let memoryStatus = Bool(from: &byteBuffer, preferredEndianness: endianness), - let samplesPerChannel = UInt8(from: &byteBuffer, preferredEndianness: endianness), - let dataSize = UInt8(from: &byteBuffer, preferredEndianness: endianness), - let syncEnabled = Bool(from: &byteBuffer, preferredEndianness: endianness), - let serialNumber = UInt32(from: &byteBuffer, preferredEndianness: endianness) else { + guard let channelCount = UInt8(from: &byteBuffer, endianness: endianness), + let accelerometerStatus = AccelerometerStatus(from: &byteBuffer, endianness: endianness), + let impedanceStatus = Bool(from: &byteBuffer, endianness: endianness), + let memoryStatus = Bool(from: &byteBuffer, endianness: endianness), + let samplesPerChannel = UInt8(from: &byteBuffer, endianness: endianness), + let dataSize = UInt8(from: &byteBuffer, endianness: endianness), + let syncEnabled = Bool(from: &byteBuffer, endianness: endianness), + let serialNumber = UInt32(from: &byteBuffer, endianness: endianness) else { return nil } @@ -99,20 +99,20 @@ extension DeviceConfiguration: ByteCodable, Equatable { } - func encode(to byteBuffer: inout ByteBuffer, preferredEndianness endianness: Endianness) { + func encode(to byteBuffer: inout ByteBuffer) { let endianness: Endianness = .big byteBuffer.reserveCapacity(minimumWritableBytes: 16) byteBuffer.writeRepeatingByte(0, count: 5) // reserved bytes, we just write zeros for now - channelCount.encode(to: &byteBuffer, preferredEndianness: endianness) - accelerometerStatus.encode(to: &byteBuffer, preferredEndianness: endianness) - impedanceStatus.encode(to: &byteBuffer, preferredEndianness: endianness) - memoryStatus.encode(to: &byteBuffer, preferredEndianness: endianness) - samplesPerChannel.encode(to: &byteBuffer, preferredEndianness: endianness) - dataSize.encode(to: &byteBuffer, preferredEndianness: endianness) - syncEnabled.encode(to: &byteBuffer, preferredEndianness: endianness) - serialNumber.encode(to: &byteBuffer, preferredEndianness: endianness) + channelCount.encode(to: &byteBuffer, endianness: endianness) + accelerometerStatus.encode(to: &byteBuffer, endianness: endianness) + impedanceStatus.encode(to: &byteBuffer, endianness: endianness) + memoryStatus.encode(to: &byteBuffer, endianness: endianness) + samplesPerChannel.encode(to: &byteBuffer, endianness: endianness) + dataSize.encode(to: &byteBuffer, endianness: endianness) + syncEnabled.encode(to: &byteBuffer, endianness: endianness) + serialNumber.encode(to: &byteBuffer, endianness: endianness) } } diff --git a/NAMS/Devices/Biopot/Characteristics/DeviceInformation.swift b/NAMS/Devices/Biopot/Characteristics/DeviceInformation.swift index d79753b..3bad851 100644 --- a/NAMS/Devices/Biopot/Characteristics/DeviceInformation.swift +++ b/NAMS/Devices/Biopot/Characteristics/DeviceInformation.swift @@ -23,15 +23,15 @@ struct DeviceInformation { extension DeviceInformation: ByteDecodable, Equatable { - init?(from byteBuffer: inout ByteBuffer, preferredEndianness endianness: Endianness) { + init?(from byteBuffer: inout ByteBuffer) { let endianness: Endianness = .big - guard let syncRatio = UInt64(from: &byteBuffer, preferredEndianness: endianness), - let syncMode = Bool(from: &byteBuffer, preferredEndianness: endianness), - let memoryWriteNumber = UInt16(from: &byteBuffer, preferredEndianness: endianness), - let memoryEraseMode = Bool(from: &byteBuffer, preferredEndianness: endianness), - let batteryLevel = UInt8(from: &byteBuffer, preferredEndianness: endianness), - let temperatureValue = UInt8(from: &byteBuffer, preferredEndianness: endianness), - let batteryCharging = Bool(from: &byteBuffer, preferredEndianness: endianness) else { + guard let syncRatio = UInt64(from: &byteBuffer, endianness: endianness), + let syncMode = Bool(from: &byteBuffer, endianness: endianness), + let memoryWriteNumber = UInt16(from: &byteBuffer, endianness: endianness), + let memoryEraseMode = Bool(from: &byteBuffer, endianness: endianness), + let batteryLevel = UInt8(from: &byteBuffer, endianness: endianness), + let temperatureValue = UInt8(from: &byteBuffer, endianness: endianness), + let batteryCharging = Bool(from: &byteBuffer, endianness: endianness) else { return nil } diff --git a/NAMS/Devices/Biopot/Characteristics/ImpedanceMeasurement.swift b/NAMS/Devices/Biopot/Characteristics/ImpedanceMeasurement.swift index e5a2845..a47009b 100644 --- a/NAMS/Devices/Biopot/Characteristics/ImpedanceMeasurement.swift +++ b/NAMS/Devices/Biopot/Characteristics/ImpedanceMeasurement.swift @@ -29,10 +29,10 @@ struct ImpedanceMeasurement { extension ImpedanceMeasurement: ByteCodable { - init?(from byteBuffer: inout ByteBuffer, preferredEndianness endianness: Endianness) { - guard let enabled = Bool(from: &byteBuffer, preferredEndianness: endianness), - let bioEnabled = Bool(from: &byteBuffer, preferredEndianness: endianness), - let interval = UInt8(from: &byteBuffer, preferredEndianness: endianness), + init?(from byteBuffer: inout ByteBuffer) { + guard let enabled = Bool(from: &byteBuffer, endianness: .big), + let bioEnabled = Bool(from: &byteBuffer, endianness: .big), + let interval = UInt8(from: &byteBuffer, endianness: .big), let values = byteBuffer.readBytes(length: byteBuffer.readableBytes) else { return nil } @@ -40,10 +40,10 @@ extension ImpedanceMeasurement: ByteCodable { self.init(enabled: enabled, bioImpedanceEnabled: bioEnabled, interval: interval, values: values) } - func encode(to byteBuffer: inout ByteBuffer, preferredEndianness endianness: Endianness) { - enabled.encode(to: &byteBuffer, preferredEndianness: endianness) - bioImpedanceEnabled.encode(to: &byteBuffer, preferredEndianness: endianness) - interval.encode(to: &byteBuffer, preferredEndianness: endianness) + func encode(to byteBuffer: inout ByteBuffer) { + enabled.encode(to: &byteBuffer, endianness: .big) + bioImpedanceEnabled.encode(to: &byteBuffer, endianness: .big) + interval.encode(to: &byteBuffer, endianness: .big) byteBuffer.writeBytes(values) } } diff --git a/NAMS/Devices/Biopot/Characteristics/SamplingConfiguration.swift b/NAMS/Devices/Biopot/Characteristics/SamplingConfiguration.swift index 76c73da..59a51d5 100644 --- a/NAMS/Devices/Biopot/Characteristics/SamplingConfiguration.swift +++ b/NAMS/Devices/Biopot/Characteristics/SamplingConfiguration.swift @@ -127,59 +127,59 @@ extension SamplingConfiguration { // swiftlint:enable identifier_name -extension SamplingConfiguration.LowPassFilter: ByteCodable { - init?(from byteBuffer: inout ByteBuffer, preferredEndianness endianness: Endianness) { - guard let value = UInt8(from: &byteBuffer, preferredEndianness: endianness) else { +extension SamplingConfiguration.LowPassFilter: PrimitiveByteCodable { + init?(from byteBuffer: inout ByteBuffer, endianness: Endianness) { + guard let value = UInt8(from: &byteBuffer, endianness: endianness) else { return nil } self.init(rawValue: value) } - func encode(to byteBuffer: inout ByteBuffer, preferredEndianness endianness: Endianness) { - rawValue.encode(to: &byteBuffer, preferredEndianness: endianness) + func encode(to byteBuffer: inout ByteBuffer, endianness: Endianness) { + rawValue.encode(to: &byteBuffer, endianness: endianness) } } -extension SamplingConfiguration.HighPassFilter: ByteCodable { - init?(from byteBuffer: inout ByteBuffer, preferredEndianness endianness: Endianness) { - guard let value = UInt8(from: &byteBuffer, preferredEndianness: endianness) else { +extension SamplingConfiguration.HighPassFilter: PrimitiveByteCodable { + init?(from byteBuffer: inout ByteBuffer, endianness: Endianness) { + guard let value = UInt8(from: &byteBuffer, endianness: endianness) else { return nil } self.init(rawValue: value) } - func encode(to byteBuffer: inout ByteBuffer, preferredEndianness endianness: Endianness) { - rawValue.encode(to: &byteBuffer, preferredEndianness: endianness) + func encode(to byteBuffer: inout ByteBuffer, endianness: Endianness) { + rawValue.encode(to: &byteBuffer, endianness: endianness) } } -extension SamplingConfiguration.SoftwareLowPassFilter: ByteCodable { - init?(from byteBuffer: inout ByteBuffer, preferredEndianness endianness: Endianness) { - guard let value = UInt8(from: &byteBuffer, preferredEndianness: endianness) else { +extension SamplingConfiguration.SoftwareLowPassFilter: PrimitiveByteCodable { + init?(from byteBuffer: inout ByteBuffer, endianness: Endianness) { + guard let value = UInt8(from: &byteBuffer, endianness: endianness) else { return nil } self.init(rawValue: value) } - func encode(to byteBuffer: inout ByteBuffer, preferredEndianness endianness: Endianness) { - rawValue.encode(to: &byteBuffer, preferredEndianness: endianness) + func encode(to byteBuffer: inout ByteBuffer, endianness: Endianness) { + rawValue.encode(to: &byteBuffer, endianness: endianness) } } extension SamplingConfiguration: ByteCodable { - init?(from byteBuffer: inout ByteBuffer, preferredEndianness endianness: Endianness) { + init?(from byteBuffer: inout ByteBuffer) { let endianness: Endianness = .big // we force big endianness for this type - guard let channelsBitMask = UInt32(from: &byteBuffer, preferredEndianness: endianness), - let lowPassFilter = LowPassFilter(from: &byteBuffer, preferredEndianness: endianness), - let highPassFilter = HighPassFilter(from: &byteBuffer, preferredEndianness: endianness), - let hardwareSamplingRate = UInt16(from: &byteBuffer, preferredEndianness: endianness), - let impedanceFrequency = UInt8(from: &byteBuffer, preferredEndianness: endianness), - let impedanceScale = UInt8(from: &byteBuffer, preferredEndianness: endianness), - let softwareLowPassFilter = SoftwareLowPassFilter(from: &byteBuffer, preferredEndianness: endianness) else { + guard let channelsBitMask = UInt32(from: &byteBuffer, endianness: endianness), + let lowPassFilter = LowPassFilter(from: &byteBuffer, endianness: endianness), + let highPassFilter = HighPassFilter(from: &byteBuffer, endianness: endianness), + let hardwareSamplingRate = UInt16(from: &byteBuffer, endianness: endianness), + let impedanceFrequency = UInt8(from: &byteBuffer, endianness: endianness), + let impedanceScale = UInt8(from: &byteBuffer, endianness: endianness), + let softwareLowPassFilter = SoftwareLowPassFilter(from: &byteBuffer, endianness: endianness) else { return nil } @@ -192,18 +192,18 @@ extension SamplingConfiguration: ByteCodable { self.softwareLowPassFilter = softwareLowPassFilter } - func encode(to byteBuffer: inout ByteBuffer, preferredEndianness endianness: Endianness) { + func encode(to byteBuffer: inout ByteBuffer) { byteBuffer.reserveCapacity(13) let endianness: Endianness = .big // we force big endianness for this type - channelsBitMask.encode(to: &byteBuffer, preferredEndianness: endianness) - lowPassFilter.encode(to: &byteBuffer, preferredEndianness: endianness) - highPassFilter.encode(to: &byteBuffer, preferredEndianness: endianness) - hardwareSamplingRate.encode(to: &byteBuffer, preferredEndianness: endianness) - impedanceFrequency.encode(to: &byteBuffer, preferredEndianness: endianness) - impedanceScale.encode(to: &byteBuffer, preferredEndianness: endianness) - softwareLowPassFilter.encode(to: &byteBuffer, preferredEndianness: endianness) + channelsBitMask.encode(to: &byteBuffer, endianness: endianness) + lowPassFilter.encode(to: &byteBuffer, endianness: endianness) + highPassFilter.encode(to: &byteBuffer, endianness: endianness) + hardwareSamplingRate.encode(to: &byteBuffer, endianness: endianness) + impedanceFrequency.encode(to: &byteBuffer, endianness: endianness) + impedanceScale.encode(to: &byteBuffer, endianness: endianness) + softwareLowPassFilter.encode(to: &byteBuffer, endianness: endianness) // for some reason Biopot wants 2 zero bytes at the end! byteBuffer.writeInteger(0, as: UInt16.self) diff --git a/NAMS/Devices/Biopot/Views/BiopotDeviceDetailsView.swift b/NAMS/Devices/Biopot/Views/BiopotDeviceDetailsView.swift index cd87985..decb77d 100644 --- a/NAMS/Devices/Biopot/Views/BiopotDeviceDetailsView.swift +++ b/NAMS/Devices/Biopot/Views/BiopotDeviceDetailsView.swift @@ -7,6 +7,7 @@ // import SpeziBluetooth +import SpeziDevicesUI import SpeziViews import SwiftUI diff --git a/NAMS/Devices/Biopot/Views/BiopotDeviceRow.swift b/NAMS/Devices/Biopot/Views/BiopotDeviceRow.swift index ae571bb..3caa745 100644 --- a/NAMS/Devices/Biopot/Views/BiopotDeviceRow.swift +++ b/NAMS/Devices/Biopot/Views/BiopotDeviceRow.swift @@ -6,8 +6,9 @@ // SPDX-License-Identifier: MIT // -import BluetoothViews import SpeziBluetooth +import SpeziDevicesUI +import SpeziViews import SwiftUI @@ -18,16 +19,25 @@ struct BiopotDeviceRow: View { private var deviceCoordinator @Binding private var path: NavigationPath + @State private var state: ViewState = .idle var body: some View { NearbyDeviceRow(peripheral: device) { Task { - await deviceCoordinator.tapDevice(.biopot(device)) + do { + try await deviceCoordinator.tapDevice(.biopot(device)) + } catch { + state = .error(AnyLocalizedError( + error: error, + defaultErrorDescription: "Failed to connect to device." + )) + } } } secondaryAction: { path.append(ConnectedDevice.biopot(device)) } + .viewStateAlert(state: $state) } init(device: BiopotDevice, path: Binding) { diff --git a/NAMS/Devices/ConnectedDevice.swift b/NAMS/Devices/ConnectedDevice.swift index be9ef3f..888746f 100644 --- a/NAMS/Devices/ConnectedDevice.swift +++ b/NAMS/Devices/ConnectedDevice.swift @@ -6,9 +6,9 @@ // SPDX-License-Identifier: MIT // -import BluetoothViews import EDFFormat import SpeziBluetooth +import SpeziDevicesUI enum ConnectedDevice { @@ -68,8 +68,8 @@ extension ConnectedDevice: NAMSDevice { } - func connect() async { - await underlyingDevice.connect() + func connect() async throws { + try await underlyingDevice.connect() } func disconnect() async { diff --git a/NAMS/Devices/DeviceCoordinator.swift b/NAMS/Devices/DeviceCoordinator.swift index 6355a92..860d851 100644 --- a/NAMS/Devices/DeviceCoordinator.swift +++ b/NAMS/Devices/DeviceCoordinator.swift @@ -77,7 +77,7 @@ class DeviceCoordinator: Module, EnvironmentAccessible, DefaultInitializable { /// Device is tapped by the user in the nearby devices view. @MainActor - func tapDevice(_ device: ConnectedDevice) async { + func tapDevice(_ device: ConnectedDevice) async throws { if let connectedDevice { logger.info("Disconnecting previously connected device \(connectedDevice.label)...") // either we tapped on the same device or on another one, in any case disconnect the currently connected device @@ -92,7 +92,7 @@ class DeviceCoordinator: Module, EnvironmentAccessible, DefaultInitializable { logger.info("Connecting to nearby device \(device.label)...") - await device.connect() + try await device.connect() self.associateDevice(device) } diff --git a/NAMS/Devices/EmptyScanningState.swift b/NAMS/Devices/EmptyScanningState.swift new file mode 100644 index 0000000..59ca966 --- /dev/null +++ b/NAMS/Devices/EmptyScanningState.swift @@ -0,0 +1,24 @@ +// +// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project +// +// SPDX-FileCopyrightText: 2024 Stanford University +// +// SPDX-License-Identifier: MIT +// + +import Foundation +@_spi(APISupport) +import SpeziBluetooth + + +struct EmptyScanningState: BluetoothScanningState { + init() {} + + func merging(with other: EmptyScanningState) -> EmptyScanningState { + EmptyScanningState() + } + + func updateOptions(minimumRSSI: Int?, advertisementStaleInterval: TimeInterval?) -> EmptyScanningState { + EmptyScanningState() + } +} diff --git a/NAMS/Devices/Mock/MockDevice.swift b/NAMS/Devices/Mock/MockDevice.swift index 1615ad5..0719705 100644 --- a/NAMS/Devices/Mock/MockDevice.swift +++ b/NAMS/Devices/Mock/MockDevice.swift @@ -6,11 +6,11 @@ // SPDX-License-Identifier: MIT // -import BluetoothViews import EDFFormat import Foundation import OSLog import SpeziBluetooth +import SpeziDevices @Observable diff --git a/NAMS/Devices/Mock/MockDeviceManager.swift b/NAMS/Devices/Mock/MockDeviceManager.swift index 86e8b19..e7e9748 100644 --- a/NAMS/Devices/Mock/MockDeviceManager.swift +++ b/NAMS/Devices/Mock/MockDeviceManager.swift @@ -7,12 +7,14 @@ // import Observation +@_spi(APISupport) import SpeziBluetooth @Observable -class MockDeviceManager { - static let defaultNearbyDevices: [MockDevice] = [ +@MainActor +final class MockDeviceManager { + static nonisolated let defaultNearbyDevices: [MockDevice] = [ MockDevice(name: "Mock Device 1"), MockDevice(name: "Mock Device 2") ] @@ -62,18 +64,18 @@ class MockDeviceManager { extension MockDeviceManager: BluetoothScanner { + typealias ScanningState = EmptyScanningState + var hasConnectedDevices: Bool { nearbyDevices.contains { device in device.state != .disconnected } } - func scanNearbyDevices(autoConnect: Bool) async { - precondition(!autoConnect, "AutoConnect is unsupported on \(Self.self)") + @MainActor + func scanNearbyDevices(_ state: EmptyScanningState) async { self.startScanning() } - func setAutoConnect(_ autoConnect: Bool) async { - precondition(!autoConnect, "AutoConnect is unsupported on \(Self.self)") - } + func updateScanningState(_ state: EmptyScanningState) async {} } diff --git a/NAMS/Devices/Mock/View+ScanNearbyMockDevices.swift b/NAMS/Devices/Mock/View+ScanNearbyMockDevices.swift new file mode 100644 index 0000000..7378e7d --- /dev/null +++ b/NAMS/Devices/Mock/View+ScanNearbyMockDevices.swift @@ -0,0 +1,21 @@ +// +// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project +// +// SPDX-FileCopyrightText: 2023 Stanford University +// +// SPDX-License-Identifier: MIT +// + +import SwiftUI +@_spi(APISupport) +import SpeziBluetooth + + +extension View { + func scanNearbyDevices( // swiftlint:disable:this function_default_parameter_at_end + enabled: Bool = true, + with mockManager: MockDeviceManager + ) -> some View { + scanNearbyDevices(enabled: enabled, scanner: mockManager, state: EmptyScanningState()) + } +} diff --git a/NAMS/Devices/Mock/Views/MockDeviceRow.swift b/NAMS/Devices/Mock/Views/MockDeviceRow.swift index 42c808a..e049f8d 100644 --- a/NAMS/Devices/Mock/Views/MockDeviceRow.swift +++ b/NAMS/Devices/Mock/Views/MockDeviceRow.swift @@ -6,7 +6,8 @@ // SPDX-License-Identifier: MIT // -import BluetoothViews +import SpeziDevicesUI +import SpeziViews import SwiftUI @@ -17,16 +18,25 @@ struct MockDeviceRow: View { private var deviceCoordinator @Binding private var path: NavigationPath + @State private var state: ViewState = .idle var body: some View { NearbyDeviceRow(peripheral: device) { Task { - await deviceCoordinator.tapDevice(.mock(device)) + do { + try await deviceCoordinator.tapDevice(.mock(device)) + } catch { + state = .error(AnyLocalizedError( + error: error, + defaultErrorDescription: "Failed to connect to device." + )) + } } } secondaryAction: { path.append(ConnectedDevice.mock(device)) } + .viewStateAlert(state: $state) } diff --git a/NAMS/Devices/Muse/MuseDevice.swift b/NAMS/Devices/Muse/MuseDevice.swift index a1a4f09..5f5fcba 100644 --- a/NAMS/Devices/Muse/MuseDevice.swift +++ b/NAMS/Devices/Muse/MuseDevice.swift @@ -6,11 +6,11 @@ // SPDX-License-Identifier: MIT // -import BluetoothViews import EDFFormat import Foundation import OSLog import SpeziBluetooth +import SpeziDevices #if MUSE diff --git a/NAMS/Devices/Muse/MuseDeviceManager.swift b/NAMS/Devices/Muse/MuseDeviceManager.swift index c611caf..b5815f5 100644 --- a/NAMS/Devices/Muse/MuseDeviceManager.swift +++ b/NAMS/Devices/Muse/MuseDeviceManager.swift @@ -8,17 +8,19 @@ import Observation import OSLog +@_spi(APISupport) import SpeziBluetooth import SwiftUI #if MUSE @Observable -class MuseDeviceManager { +@MainActor +final class MuseDeviceManager { private static let discoveryTimeout: Int64 = 10 - private let logger = Logger(subsystem: "edu.stanford.NAMS", category: "MuseDeviceManager") + private nonisolated let logger = Logger(subsystem: "edu.stanford.NAMS", category: "MuseDeviceManager") - private let museManager: IXNMuseManager + private nonisolated let museManager: IXNMuseManager @ObservationIgnored private var museListener: MuseListener? /// The list of nearby muse devices. @@ -35,7 +37,7 @@ class MuseDeviceManager { private var lastKnownBluetoothState: BluetoothState = .unknown - @MainActor var connectedMuse: MuseDevice? { + var connectedMuse: MuseDevice? { nearbyMuses.first { muse in muse.state == .connected } @@ -52,7 +54,6 @@ class MuseDeviceManager { self.museManager.removeFromList(after: Self.discoveryTimeout) // stale timeout if there isn't an updated advertisement } - @MainActor func startScanning() { logger.debug("Start scanning for nearby Muse devices...") if lastKnownBluetoothState == .poweredOff { @@ -64,7 +65,6 @@ class MuseDeviceManager { handleUpdatedDeviceList(museManager.getMuses()) } - @MainActor func stopScanning(state: BluetoothState) { logger.debug("Stopped scanning for nearby Muse devices!") self.lastKnownBluetoothState = state @@ -85,7 +85,6 @@ class MuseDeviceManager { } } - @MainActor func stopScanning() { // we are called from the modifier, so state must be powered on stopScanning(state: .poweredOn) @@ -110,7 +109,6 @@ class MuseDeviceManager { } - @MainActor private func handleUpdatedDeviceList(_ museList: [IXNMuse]) { MainActor.assertIsolated("Muse List was not updated on Main Actor!") var nearbyMuses = Set(museList) @@ -150,7 +148,6 @@ class MuseDeviceManager { checkHiddenTimerScheduled() } - @MainActor private func checkHiddenMuseDevices() { var hiddenDeviceUpdated = false @@ -167,7 +164,6 @@ class MuseDeviceManager { } } - @MainActor private func checkHiddenTimerScheduled() { if hiddenMuseDevices.isEmpty { hiddenDevicesTimer = nil @@ -198,14 +194,12 @@ extension MuseDeviceManager: BluetoothScanner { } } - func scanNearbyDevices(autoConnect: Bool) async { - precondition(!autoConnect, "AutoConnect is unsupported on \(Self.self)") - await self.startScanning() + @MainActor + func scanNearbyDevices(_ state: EmptyScanningState) async { + self.startScanning() } - func setAutoConnect(_ autoConnect: Bool) async { - precondition(!autoConnect, "AutoConnect is unsupported on \(Self.self)") - } + func updateScanningState(_ state: EmptyScanningState) async {} } diff --git a/NAMS/Devices/Muse/View+ScanNearbyMuseDevices.swift b/NAMS/Devices/Muse/View+ScanNearbyMuseDevices.swift new file mode 100644 index 0000000..3a03eca --- /dev/null +++ b/NAMS/Devices/Muse/View+ScanNearbyMuseDevices.swift @@ -0,0 +1,23 @@ +// +// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project +// +// SPDX-FileCopyrightText: 2023 Stanford University +// +// SPDX-License-Identifier: MIT +// + +import SwiftUI +@_spi(APISupport) +import SpeziBluetooth + + +#if MUSE +extension View { + func scanNearbyDevices( // swiftlint:disable:this function_default_parameter_at_end + enabled: Bool = true, + with museManager: MuseDeviceManager + ) -> some View { + scanNearbyDevices(enabled: enabled, scanner: museManager, state: EmptyScanningState()) + } +} +#endif diff --git a/NAMS/Devices/Muse/Views/Details/MuseBatteryDetailsSection.swift b/NAMS/Devices/Muse/Views/Details/MuseBatteryDetailsSection.swift index 75b8dba..f93948a 100644 --- a/NAMS/Devices/Muse/Views/Details/MuseBatteryDetailsSection.swift +++ b/NAMS/Devices/Muse/Views/Details/MuseBatteryDetailsSection.swift @@ -6,6 +6,7 @@ // SPDX-License-Identifier: MIT // +import SpeziDevicesUI import SpeziViews import SwiftUI diff --git a/NAMS/Devices/Muse/Views/MuseDeviceRow.swift b/NAMS/Devices/Muse/Views/MuseDeviceRow.swift index 09066c7..992f73c 100644 --- a/NAMS/Devices/Muse/Views/MuseDeviceRow.swift +++ b/NAMS/Devices/Muse/Views/MuseDeviceRow.swift @@ -6,7 +6,8 @@ // SPDX-License-Identifier: MIT // -import BluetoothViews +import SpeziDevicesUI +import SpeziViews import SwiftUI @@ -18,15 +19,24 @@ struct MuseDeviceRow: View { private var deviceCoordinator @Binding private var path: NavigationPath + @State private var state: ViewState = .idle var body: some View { NearbyDeviceRow(peripheral: device) { Task { - await deviceCoordinator.tapDevice(.muse(device)) + do { + try await deviceCoordinator.tapDevice(.muse(device)) + } catch { + state = .error(AnyLocalizedError( + error: error, + defaultErrorDescription: "Failed to connect to device." + )) + } } } secondaryAction: { path.append(ConnectedDevice.muse(device)) } + .viewStateAlert(state: $state) } diff --git a/NAMS/Devices/NAMSDevice.swift b/NAMS/Devices/NAMSDevice.swift index 5b34f23..7eec4fd 100644 --- a/NAMS/Devices/NAMSDevice.swift +++ b/NAMS/Devices/NAMSDevice.swift @@ -6,8 +6,8 @@ // SPDX-License-Identifier: MIT // -import BluetoothViews import EDFFormat +import SpeziDevices protocol NAMSDevice: GenericBluetoothPeripheral { @@ -18,7 +18,7 @@ protocol NAMSDevice: GenericBluetoothPeripheral { /// The duration of a single data record in seconds. var recordDuration: Int { get } - func connect() async + func connect() async throws func disconnect() async diff --git a/NAMS/Devices/NearbyDevicesView.swift b/NAMS/Devices/NearbyDevicesView.swift index 37bd9c3..a3b9258 100644 --- a/NAMS/Devices/NearbyDevicesView.swift +++ b/NAMS/Devices/NearbyDevicesView.swift @@ -6,9 +6,9 @@ // SPDX-License-Identifier: MIT // -import BluetoothViews import Spezi import SpeziBluetooth +import SpeziDevicesUI import SpeziViews import SwiftUI @@ -62,7 +62,7 @@ struct NearbyDevicesView: View { nearbyDevicesSection } else { Section { - BluetoothStateHint(bluetooth.state) + BluetoothUnavailableView(bluetooth.state) } } } @@ -105,7 +105,7 @@ struct NearbyDevicesView: View { } } - @ViewBuilder private var nearbyDevicesSection: some View { + @ViewBuilder @MainActor private var nearbyDevicesSection: some View { Section { #if MUSE MuseDeviceList(path: $navigationPath) @@ -126,7 +126,7 @@ struct NearbyDevicesView: View { } } } header: { - LoadingSectionHeaderView("Devices", loading: isScanning) + LoadingSectionHeader("Devices", loading: isScanning) } footer: { MuseTroublesConnectingHint() } diff --git a/NAMS/EEG/EEGRecordings.swift b/NAMS/EEG/EEGRecordings.swift index 22e3ee4..6aa554a 100644 --- a/NAMS/EEG/EEGRecordings.swift +++ b/NAMS/EEG/EEGRecordings.swift @@ -17,8 +17,10 @@ class EEGRecordings: Module, EnvironmentAccessible, DefaultInitializable { @StandardActor @ObservationIgnored private var standard: NAMSStandard - @Dependency @ObservationIgnored private var deviceCoordinator: DeviceCoordinator - @Dependency @ObservationIgnored private var patientList: PatientListModel + @Dependency(DeviceCoordinator.self) + @ObservationIgnored private var deviceCoordinator + @Dependency(PatientListModel.self) + @ObservationIgnored private var patientList required init() {} diff --git a/NAMS/EEG/Views/Chart/ChartMoreInformationView.swift b/NAMS/EEG/Views/Chart/ChartMoreInformationView.swift index d8d094e..2a794db 100644 --- a/NAMS/EEG/Views/Chart/ChartMoreInformationView.swift +++ b/NAMS/EEG/Views/Chart/ChartMoreInformationView.swift @@ -6,6 +6,7 @@ // SPDX-License-Identifier: MIT // +@_spi(TestingSupport) import SpeziAccount import SpeziViews import SwiftUI @@ -37,10 +38,10 @@ struct ChartMoreInformationView: View { #if DEBUG #Preview { - let detailsBuilder = AccountDetails.Builder() - .set(\.accountId, value: UUID().uuidString) - .set(\.userId, value: "lelandstanford@stanford.edu") - .set(\.name, value: PersonNameComponents(givenName: "Leland", familyName: "Stanford")) + var details = AccountDetails() + details.accountId = UUID().uuidString + details.userId = "lelandstanford@stanford.edu" + details.name = PersonNameComponents(givenName: "Leland", familyName: "Stanford") return NavigationStack { AutoStartRecordingView { session in @@ -51,7 +52,7 @@ struct ChartMoreInformationView: View { } } .previewWith(standard: NAMSStandard()) { - AccountConfiguration(building: detailsBuilder, active: MockUserIdPasswordAccountService()) + AccountConfiguration(service: InMemoryAccountService(), activeDetails: details) EEGRecordings() DeviceCoordinator(mock: .mock(MockDevice(name: "Mock Device 1", state: .connected))) PatientListModel(mock: Patient(id: UUID().uuidString, name: PersonNameComponents(givenName: "Leland", familyName: "Stanford"))) diff --git a/NAMS/EEG/Views/EEGRecordingSessionView.swift b/NAMS/EEG/Views/EEGRecordingSessionView.swift index e855f2b..329f8f5 100644 --- a/NAMS/EEG/Views/EEGRecordingSessionView.swift +++ b/NAMS/EEG/Views/EEGRecordingSessionView.swift @@ -6,6 +6,7 @@ // SPDX-License-Identifier: MIT // +@_spi(TestingSupport) import SpeziAccount import SpeziViews import SwiftUI @@ -155,10 +156,10 @@ extension View { #if DEBUG #Preview { - let detailsBuilder = AccountDetails.Builder() - .set(\.accountId, value: UUID().uuidString) - .set(\.userId, value: "lelandstanford@stanford.edu") - .set(\.name, value: PersonNameComponents(givenName: "Leland", familyName: "Stanford")) + var details = AccountDetails() + details.accountId = UUID().uuidString + details.userId = "lelandstanford@stanford.edu" + details.name = PersonNameComponents(givenName: "Leland", familyName: "Stanford") return NavigationStack { AutoStartRecordingView { session in @@ -169,7 +170,7 @@ extension View { } } .previewWith(standard: NAMSStandard()) { - AccountConfiguration(building: detailsBuilder, active: MockUserIdPasswordAccountService()) + AccountConfiguration(service: InMemoryAccountService(), activeDetails: details) EEGRecordings() DeviceCoordinator(mock: .mock(MockDevice(name: "Mock Device 1", state: .connected))) PatientListModel(mock: Patient(id: UUID().uuidString, name: PersonNameComponents(givenName: "Leland", familyName: "Stanford"))) diff --git a/NAMS/EEG/Views/EEGRecordingView.swift b/NAMS/EEG/Views/EEGRecordingView.swift index 3a498d6..4ea4b20 100644 --- a/NAMS/EEG/Views/EEGRecordingView.swift +++ b/NAMS/EEG/Views/EEGRecordingView.swift @@ -8,6 +8,7 @@ import Charts import Spezi +@_spi(TestingSupport) import SpeziAccount import SpeziBluetooth import SpeziOnboarding @@ -58,17 +59,17 @@ struct EEGRecordingView: View { #if DEBUG #Preview { - let detailsBuilder = AccountDetails.Builder() - .set(\.accountId, value: UUID().uuidString) - .set(\.userId, value: "lelandstanford@stanford.edu") - .set(\.name, value: PersonNameComponents(givenName: "Leland", familyName: "Stanford")) + var details = AccountDetails() + details.accountId = UUID().uuidString + details.userId = "lelandstanford@stanford.edu" + details.name = PersonNameComponents(givenName: "Leland", familyName: "Stanford") return NavigationStack { AutoStartRecordingView { _ in EEGRecordingView() } .previewWith(standard: NAMSStandard()) { - AccountConfiguration(building: detailsBuilder, active: MockUserIdPasswordAccountService()) + AccountConfiguration(service: InMemoryAccountService(), activeDetails: details) EEGRecordings() DeviceCoordinator(mock: .mock(MockDevice(name: "Mock Device 1", state: .connected))) PatientListModel(mock: Patient(id: UUID().uuidString, name: PersonNameComponents(givenName: "Leland", familyName: "Stanford"))) @@ -77,15 +78,15 @@ struct EEGRecordingView: View { } #Preview { - let detailsBuilder = AccountDetails.Builder() - .set(\.accountId, value: UUID().uuidString) - .set(\.userId, value: "lelandstanford@stanford.edu") - .set(\.name, value: PersonNameComponents(givenName: "Leland", familyName: "Stanford")) + var details = AccountDetails() + details.accountId = UUID().uuidString + details.userId = "lelandstanford@stanford.edu" + details.name = PersonNameComponents(givenName: "Leland", familyName: "Stanford") return NavigationStack { EEGRecordingView() .previewWith(standard: NAMSStandard()) { - AccountConfiguration(building: detailsBuilder, active: MockUserIdPasswordAccountService()) + AccountConfiguration(service: InMemoryAccountService(), activeDetails: details) EEGRecordings() DeviceCoordinator(mock: .mock(MockDevice(name: "Mock Device 1", state: .connected))) PatientListModel(mock: Patient(id: UUID().uuidString, name: PersonNameComponents(givenName: "Leland", familyName: "Stanford"))) @@ -97,9 +98,7 @@ struct EEGRecordingView: View { NavigationStack { EEGRecordingView() .previewWith(standard: NAMSStandard()) { - AccountConfiguration { - MockUserIdPasswordAccountService() - } + AccountConfiguration(service: InMemoryAccountService()) EEGRecordings() DeviceCoordinator() PatientListModel() diff --git a/NAMS/EEG/Views/StartRecordingView.swift b/NAMS/EEG/Views/StartRecordingView.swift index 744722c..d37d6af 100644 --- a/NAMS/EEG/Views/StartRecordingView.swift +++ b/NAMS/EEG/Views/StartRecordingView.swift @@ -6,6 +6,7 @@ // SPDX-License-Identifier: MIT // +@_spi(TestingSupport) import SpeziAccount import SpeziBluetooth import SpeziOnboarding @@ -68,9 +69,7 @@ struct StartRecordingView: View { #Preview { StartRecordingView(.constant(nil)) .previewWith(standard: NAMSStandard()) { - AccountConfiguration { - MockUserIdPasswordAccountService() - } + AccountConfiguration(service: InMemoryAccountService()) EEGRecordings() Bluetooth { Discover(BiopotDevice.self, by: .advertisedService(BiopotService.self)) diff --git a/NAMS/Home.swift b/NAMS/Home.swift index f62be1e..64e74e6 100644 --- a/NAMS/Home.swift +++ b/NAMS/Home.swift @@ -6,6 +6,7 @@ // SPDX-License-Identifier: MIT // +@_spi(TestingSupport) import SpeziAccount import SpeziBluetooth import SpeziViews @@ -125,7 +126,6 @@ struct HomeView: View { .accountRequired(!FeatureFlags.skipOnboarding && !FeatureFlags.injectDefaultPatient) { AccountSheet() } - .verifyRequiredAccountDetails() } @@ -141,16 +141,16 @@ struct HomeView: View { #if DEBUG #Preview { - let details = AccountDetails.Builder() - .set(\.accountId, value: UUID().uuidString) - .set(\.userId, value: "lelandstanford@stanford.edu") - .set(\.name, value: PersonNameComponents(givenName: "Leland", familyName: "Stanford")) + var details = AccountDetails() + details.accountId = UUID().uuidString + details.userId = "lelandstanford@stanford.edu" + details.name = PersonNameComponents(givenName: "Leland", familyName: "Stanford") return HomeView() .previewWith(standard: NAMSStandard()) { DeviceCoordinator() EEGRecordings() - AccountConfiguration(building: details, active: MockUserIdPasswordAccountService()) + AccountConfiguration(service: InMemoryAccountService(), activeDetails: details) Bluetooth { Discover(BiopotDevice.self, by: .advertisedService(BiopotService.self)) } diff --git a/NAMS/NAMSApp.swift b/NAMS/NAMSApp.swift index 45b9778..26ac643 100644 --- a/NAMS/NAMSApp.swift +++ b/NAMS/NAMSApp.swift @@ -7,6 +7,7 @@ // import Spezi +import SpeziViews import SwiftUI diff --git a/NAMS/NAMSAppDelegate.swift b/NAMS/NAMSAppDelegate.swift index 0fd4861..d59c02b 100644 --- a/NAMS/NAMSAppDelegate.swift +++ b/NAMS/NAMSAppDelegate.swift @@ -6,10 +6,12 @@ // SPDX-License-Identifier: MIT // +import FirebaseFirestore import Spezi import SpeziAccount import SpeziBluetooth import SpeziFirebaseAccount +import SpeziFirebaseAccountStorage import SpeziFirebaseStorage import SpeziFirestore import SwiftUI @@ -18,24 +20,21 @@ import SwiftUI class NAMSAppDelegate: SpeziAppDelegate { override var configuration: Configuration { Configuration(standard: NAMSStandard()) { - let methods: FirebaseAuthAuthenticationMethods = [.emailAndPassword, .signInWithApple] - - AccountConfiguration(configuration: [ - .requires(\.userId), - .requires(\.name), - .collects(\.investigatorCode) - ]) + AccountConfiguration( + service: FirebaseAccountService(providers: [.emailAndPassword, .signInWithApple], emulatorSettings: authEmulator), + storageProvider: FirestoreAccountStorage(storeIn: Firestore.firestore().collection("users")), + configuration: [ + .requires(\.userId), + .requires(\.name), + .collects(\.investigatorCode) + ] + ) Firestore(settings: firestoreSettings) if FeatureFlags.useFirebaseEmulator { - FirebaseAccountConfiguration( - authenticationMethods: methods, - emulatorSettings: (host: "localhost", port: 9099) - ) FirebaseStorageConfiguration(emulatorSettings: (host: "localhost", port: 9199)) } else { - FirebaseAccountConfiguration(authenticationMethods: methods) FirebaseStorageConfiguration() } @@ -47,7 +46,15 @@ class NAMSAppDelegate: SpeziAppDelegate { } } } - + + private var authEmulator: (host: String, port: Int)? { + if FeatureFlags.useFirebaseEmulator { + (host: "localhost", port: 9099) + } else { + nil + } + } + private var firestoreSettings: FirestoreSettings { let settings = FirestoreSettings() diff --git a/NAMS/NAMSStandard.swift b/NAMS/NAMSStandard.swift index d19fa6c..010bec1 100644 --- a/NAMS/NAMSStandard.swift +++ b/NAMS/NAMSStandard.swift @@ -14,13 +14,7 @@ import SpeziAccount import SpeziFirebaseAccountStorage -actor NAMSStandard: Standard, AccountStorageConstraint { - private static var userCollection: CollectionReference { - Firestore.firestore().collection("users") - } - - @Dependency private var storage = FirestoreAccountStorage(storeIn: NAMSStandard.userCollection) - +actor NAMSStandard: Standard { init() {} func uploadEEGRecording(file: URL, recordingId: UUID, patientId: String, format: FileFormat) async throws { @@ -33,24 +27,4 @@ actor NAMSStandard: Standard, AccountStorageConstraint { metadata.contentType = "application/octet-stream" _ = try await reference.putFileAsync(from: file, metadata: metadata) } - - func create(_ identifier: AdditionalRecordId, _ details: SignupDetails) async throws { - try await storage.create(identifier, details) - } - - func load(_ identifier: AdditionalRecordId, _ keys: [any AccountKey.Type]) async throws -> PartialAccountDetails { - try await storage.load(identifier, keys) - } - - func modify(_ identifier: AdditionalRecordId, _ modifications: AccountModifications) async throws { - try await storage.modify(identifier, modifications) - } - - func clear(_ identifier: AdditionalRecordId) async { - await storage.clear(identifier) - } - - func delete(_ identifier: AdditionalRecordId) async throws { - try await storage.delete(identifier) - } } diff --git a/NAMS/Onboarding/AccountOnboarding.swift b/NAMS/Onboarding/AccountOnboarding.swift index 78cadf4..a83dd1c 100644 --- a/NAMS/Onboarding/AccountOnboarding.swift +++ b/NAMS/Onboarding/AccountOnboarding.swift @@ -6,6 +6,7 @@ // SPDX-License-Identifier: MIT // +@_spi(TestingSupport) import SpeziAccount import SpeziOnboarding import SwiftUI @@ -49,20 +50,18 @@ struct AccountOnboarding: View { #Preview { stack .previewWith { - AccountConfiguration { - MockUserIdPasswordAccountService() - } + AccountConfiguration(service: InMemoryAccountService()) } } #Preview { - let details = AccountDetails.Builder() - .set(\.accountId, value: UUID().uuidString) - .set(\.userId, value: "lelandstanford@stanford.edu") - .set(\.name, value: PersonNameComponents(givenName: "Leland", familyName: "Stanford")) + var details = AccountDetails() + details.accountId = UUID().uuidString + details.userId = "lelandstanford@stanford.edu" + details.name = PersonNameComponents(givenName: "Leland", familyName: "Stanford") return stack .previewWith { - AccountConfiguration(building: details, active: MockUserIdPasswordAccountService()) + AccountConfiguration(service: InMemoryAccountService(), activeDetails: details) } } #endif diff --git a/NAMS/Onboarding/OnboardingFlow.swift b/NAMS/Onboarding/OnboardingFlow.swift index 94c8e88..af5708d 100644 --- a/NAMS/Onboarding/OnboardingFlow.swift +++ b/NAMS/Onboarding/OnboardingFlow.swift @@ -6,6 +6,7 @@ // SPDX-License-Identifier: MIT // +@_spi(TestingSupport) import SpeziAccount import SpeziFirebaseAccount import SpeziOnboarding @@ -42,6 +43,8 @@ struct OnboardingFlow: View { #if DEBUG #Preview { OnboardingFlow() - .environment(Account(MockUserIdPasswordAccountService())) + .previewWith { + AccountConfiguration(service: InMemoryAccountService()) + } } #endif diff --git a/NAMS/Patients/Model/Patient.swift b/NAMS/Patients/Model/Patient.swift index 3e4dcdd..892f68c 100644 --- a/NAMS/Patients/Model/Patient.swift +++ b/NAMS/Patients/Model/Patient.swift @@ -7,7 +7,7 @@ // import EDFFormat -import FirebaseFirestoreSwift +import FirebaseFirestore import Foundation diff --git a/NAMS/Patients/Model/PatientListModel.swift b/NAMS/Patients/Model/PatientListModel.swift index 70f2209..97890c4 100644 --- a/NAMS/Patients/Model/PatientListModel.swift +++ b/NAMS/Patients/Model/PatientListModel.swift @@ -7,7 +7,6 @@ // import FirebaseFirestore -import FirebaseFirestoreSwift import Observation import OrderedCollections import OSLog @@ -24,6 +23,9 @@ import SwiftUI class PatientListModel: Module, EnvironmentAccessible, DefaultInitializable { static let logger = Logger(subsystem: "edu.stanford.NAMS", category: "PatientListModel") + @Dependency(FirebaseAccountService.self) + @ObservationIgnored private var accountService: FirebaseAccountService? + var patientList: [Patient]? // swiftlint:disable:this discouraged_optional_collection @AppStorage(StorageKeys.selectedPatient) @@ -291,7 +293,7 @@ class PatientListModel: Module, EnvironmentAccessible, DefaultInitializable { return } - guard let service = account.registeredAccountServices.compactMap({ $0 as? any UserIdPasswordAccountService }).first else { + guard let accountService else { preconditionFailure("Failed to retrieve a user-id-password based account service for test account setup!") } @@ -300,15 +302,15 @@ class PatientListModel: Module, EnvironmentAccessible, DefaultInitializable { try await Task.sleep(for: .milliseconds(500)) do { - let details = SignupDetails.Builder() - .set(\.userId, value: email) - .set(\.name, value: PersonNameComponents(givenName: "Leland", familyName: "Stanford")) - .set(\.password, value: password) - .build() - try await service.signUp(signupDetails: details) + var details = AccountDetails() + details.userId = email + details.name = PersonNameComponents(givenName: "Leland", familyName: "Stanford") + details.password = password + + try await accountService.signUp(with: details) } catch { if "\(error)".contains("accountAlreadyInUse") { - try await service.login(userId: email, password: password) + try await accountService.login(userId: email, password: password) } else { throw error } diff --git a/NAMS/Patients/PatientRow.swift b/NAMS/Patients/PatientRow.swift index 67822b8..70013e2 100644 --- a/NAMS/Patients/PatientRow.swift +++ b/NAMS/Patients/PatientRow.swift @@ -6,7 +6,7 @@ // SPDX-License-Identifier: MIT // -import BluetoothViews +import SpeziDevicesUI import SpeziPersonalInfo import SpeziViews import SwiftUI diff --git a/NAMS/Patients/Tasks/CompletedTask.swift b/NAMS/Patients/Tasks/CompletedTask.swift index 767eaaa..ae67ac2 100644 --- a/NAMS/Patients/Tasks/CompletedTask.swift +++ b/NAMS/Patients/Tasks/CompletedTask.swift @@ -6,7 +6,7 @@ // SPDX-License-Identifier: MIT // -import FirebaseFirestoreSwift +import FirebaseFirestore import Foundation #if canImport(SpeziQuestionnaire) import SpeziQuestionnaire diff --git a/NAMS/ScheduleView.swift b/NAMS/ScheduleView.swift index 669b7e4..d319fdc 100644 --- a/NAMS/ScheduleView.swift +++ b/NAMS/ScheduleView.swift @@ -6,6 +6,7 @@ // SPDX-License-Identifier: MIT // +@_spi(TestingSupport) import SpeziAccount import SpeziBluetooth import SpeziViews @@ -94,9 +95,7 @@ struct ScheduleView: View { Bluetooth { Discover(BiopotDevice.self, by: .advertisedService(BiopotService.self)) } - AccountConfiguration { - MockUserIdPasswordAccountService() - } + AccountConfiguration(service: InMemoryAccountService()) } } #endif diff --git a/NAMS/Tiles/CompletedTile.swift b/NAMS/Tiles/CompletedTile.swift deleted file mode 100644 index 161be6c..0000000 --- a/NAMS/Tiles/CompletedTile.swift +++ /dev/null @@ -1,74 +0,0 @@ -// -// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project -// -// SPDX-FileCopyrightText: 2023 Stanford University -// -// SPDX-License-Identifier: MIT -// - -import SwiftUI - - -struct CompletedTile: View { - private let title: Title - private let description: Description - - var body: some View { - if Description.self == EmptyView.self { - SimpleTile { - header - } - } else { - SimpleTile { - header - } footer: { - description - .font(.callout) - } - } - } - - @ViewBuilder private var header: some View { - HStack { - Image(systemName: "checkmark.circle.fill") - .foregroundColor(.green) - .font(.custom("Completed Icon", size: 30, relativeTo: .title)) - .accessibilityHidden(true) - - VStack(alignment: .leading, spacing: 4) { - title - .font(.headline) - - Text("Completed", comment: "Completed Tile. Subtitle") - .font(.subheadline) - .foregroundColor(.secondary) - } - } - } - - init(@ViewBuilder title: () -> Title, @ViewBuilder description: () -> Description = { EmptyView() }) { - self.title = title() - self.description = description() - } -} - - -#if DEBUG -#Preview { - List { - CompletedTile { - Text(verbatim: "Test Task") - } description: { - Text(verbatim: "A nice description of a test task.") - } - } -} - -#Preview { - List { - CompletedTile { - Text(verbatim: "Test Task") - } - } -} -#endif diff --git a/NAMS/Tiles/MeasurementTile.swift b/NAMS/Tiles/MeasurementTile.swift index f22b432..d9eec9d 100644 --- a/NAMS/Tiles/MeasurementTile.swift +++ b/NAMS/Tiles/MeasurementTile.swift @@ -6,6 +6,7 @@ // SPDX-License-Identifier: MIT // +import SpeziViews import SwiftUI @@ -25,35 +26,45 @@ struct MeasurementTile: View { var body: some View { if completed { - CompletedTile { - Text(task.title) - } description: { + SimpleTile { + CompletedTileHeader { + Text(task.title) + } + } body: { Text(task.completedDescription) } } else { SimpleTile(alignment: .center) { - Image(systemName: "brain.fill") - .foregroundColor(.pink) - .font(.custom("EEG Icon", size: 50, relativeTo: .title)) - .accessibilityHidden(true) - Text(task.title) - .font(.title) - .fontWeight(.semibold) - .multilineTextAlignment(.center) // works better for larger text sizes - Text("\(task.expectedCompletionMinutes) min") - .foregroundColor(.secondary) - .font(.subheadline) - .multilineTextAlignment(.center) - .accessibilityLabel("takes \(task.expectedCompletionMinutes) min") - } footer: { + TileHeader(alignment: .center) { + Image(systemName: "brain.fill") + .foregroundColor(.pink) + .font(.custom("EEG Icon", size: 50, relativeTo: .title)) + .accessibilityHidden(true) + } title: { + Text(task.title) + .font(.title) + .fontWeight(.semibold) + .multilineTextAlignment(.center) // works better for larger text sizes + } subheadline: { + Text("\(task.expectedCompletionMinutes) min") + .foregroundColor(.secondary) + .font(.subheadline) + .multilineTextAlignment(.center) + .accessibilityLabel("takes \(task.expectedCompletionMinutes) min") + } + } body: { tileDescription - } action: { - presentingEEGRecording = true - } actionLabel: { - Text("Start \(task.tileType.localizedStringResource)") + } footer: { + Button { + presentingEEGRecording = true + } label: { + Text("Start \(task.tileType.localizedStringResource)") + .frame(maxWidth: .infinity, minHeight: 30) + } + .buttonStyle(.borderedProminent) + .disabled(task.requiresConnectedDevice && !deviceConnected) } .tint(.pink) - .disabled(task.requiresConnectedDevice && !deviceConnected) } } diff --git a/NAMS/Tiles/ScreeningTile.swift b/NAMS/Tiles/ScreeningTile.swift index 454ec3c..48bd800 100644 --- a/NAMS/Tiles/ScreeningTile.swift +++ b/NAMS/Tiles/ScreeningTile.swift @@ -8,6 +8,7 @@ #if canImport(SpeziQuestionnaire) import SpeziQuestionnaire +import SpeziViews import SwiftUI @@ -26,28 +27,66 @@ struct ScreeningTile: View { var body: some View { if completed { - CompletedTile { - Text(task.title) - .font(.headline) - } description: { + SimpleTile { + CompletedTileHeader { + Text(task.title) + } + } body: { Text(task.completedDescription) - .font(.callout) } } else { SimpleTile { - ScreeningTileHeader(task) - } footer: { + TileHeader { + Image(systemName: "list.bullet.clipboard") + .foregroundColor(.mint) + .font(.custom("Screening Task Icon", size: 30, relativeTo: .headline)) + .accessibilityHidden(true) + .dynamicTypeSize(...DynamicTypeSize.accessibility2) + } title: { + Text(task.title) + } subheadline: { + subheadline + } + } body: { Text(task.description) .font(.callout) - } action: { - presentingItem = task.questionnaire - } actionLabel: { - Text("Start \(task.tileType.localizedStringResource)") + } footer: { + Button { + presentingItem = task.questionnaire + } label: { + Text("Start \(task.tileType.localizedStringResource)") + .frame(maxWidth: .infinity, minHeight: 30) + } + .buttonStyle(.borderedProminent) } .tint(.mint) } } + @ViewBuilder private var subheadline: some View { + ViewThatFits(in: .horizontal) { + HStack { + Text(task.tileType.localizedStringResource) + Spacer() + expectedCompletion + } + .lineLimit(1) + .accessibilityElement(children: .combine) + + VStack { + Text(task.tileType.localizedStringResource) + expectedCompletion + } + .lineLimit(1) + .accessibilityElement(children: .combine) + } + } + + @ViewBuilder private var expectedCompletion: some View { + Text("\(task.expectedCompletionMinutes) min", comment: "Expected task completion in minutes.") + .accessibilityLabel("takes \(task.expectedCompletionMinutesSpoken) min") + } + init(task: ScreeningTask, presentingItem: Binding) { self.task = task diff --git a/NAMS/Tiles/ScreeningTileHeader.swift b/NAMS/Tiles/ScreeningTileHeader.swift deleted file mode 100644 index d4c4f59..0000000 --- a/NAMS/Tiles/ScreeningTileHeader.swift +++ /dev/null @@ -1,90 +0,0 @@ -// -// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project -// -// SPDX-FileCopyrightText: 2024 Stanford University -// -// SPDX-License-Identifier: MIT -// - -#if canImport(SpeziQuestionnaire) -import SpeziViews -import SwiftUI - - -struct ScreeningTileHeader: View { - private static let iconRealignSize: DynamicTypeSize = .accessibility3 - - private let task: ScreeningTask - - @Environment(\.dynamicTypeSize) - private var dynamicTypeSize - @Environment(\.horizontalSizeClass) - private var horizontalSizeClass // for iPad or landscape we want to stay horizontal - - @State private var subheadlineLayout: DynamicLayout? - - private var iconGloballyPlaced: Bool { - horizontalSizeClass == .regular || dynamicTypeSize < Self.iconRealignSize - } - - var body: some View { - HStack { - if iconGloballyPlaced { - clipboard - } - - VStack(alignment: .leading, spacing: 4) { - HStack { - if !iconGloballyPlaced { - clipboard - } - Text(task.title) - .font(.headline) - } - subheadline - } - } - } - - @ViewBuilder var clipboard: some View { - Image(systemName: "list.bullet.clipboard") - .foregroundColor(.mint) - .font(.custom("Screening Task Icon", size: 30, relativeTo: .headline)) - .accessibilityHidden(true) - .dynamicTypeSize(...DynamicTypeSize.accessibility2) - } - - @ViewBuilder var subheadline: some View { - DynamicHStack(realignAfter: .xxxLarge) { - Text(task.tileType.localizedStringResource) - - if subheadlineLayout == .horizontal { - Spacer() - } - - Text("\(task.expectedCompletionMinutes) min", comment: "Expected task completion in minutes.") - .accessibilityLabel("takes \(task.expectedCompletionMinutesSpoken) min") - } - .font(.subheadline) - .foregroundColor(.secondary) - .accessibilityElement(children: .combine) - .onPreferenceChange(DynamicLayout.self) { layout in - subheadlineLayout = layout - } - } - - - init(_ task: ScreeningTask) { - self.task = task - } -} - - -#if DEBUG -#Preview { - List { - ScreeningTileHeader(.mChatRF) - } -} -#endif -#endif diff --git a/NAMS/Tiles/SimpleTile.swift b/NAMS/Tiles/SimpleTile.swift deleted file mode 100644 index a971f65..0000000 --- a/NAMS/Tiles/SimpleTile.swift +++ /dev/null @@ -1,114 +0,0 @@ -// -// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project -// -// SPDX-FileCopyrightText: 2023 Stanford University -// -// SPDX-License-Identifier: MIT -// - -import SwiftUI - - -struct SimpleTile: View { - private struct Action { - let action: () -> Void - let label: ActionLabel - } - - private let alignment: HorizontalAlignment - private let header: Header - private let footer: Footer - private let action: Action? - - var body: some View { - VStack(alignment: alignment) { - tileLabel - - if let action { - Button(action: action.action) { - action.label - .frame(maxWidth: .infinity, minHeight: 30) - } - .buttonStyle(.borderedProminent) - .padding(.top, 8) - } - } - .containerShape(Rectangle()) - #if !TEST // it's easier to UI test for us without the accessibility representation - .accessibilityRepresentation { - if let action { - Button(action: action.action) { - tileLabel - } - } else { - tileLabel - .accessibilityElement(children: .combine) - } - } - #endif - } - - - @ViewBuilder var tileLabel: some View { - header - - if Footer.self != EmptyView.self || Action.self != EmptyView.self { - Divider() - .padding(.bottom, 4) - } - - footer - } - - private init( - alignment: HorizontalAlignment, - @ViewBuilder header: () -> Header, - @ViewBuilder footer: () -> Footer, - action: Action? - ) { - self.alignment = alignment - self.header = header() - self.footer = footer() - self.action = action - } - - - init( - alignment: HorizontalAlignment = .leading, - @ViewBuilder header: () -> Header, - @ViewBuilder footer: () -> Footer = { EmptyView() }, - action: @escaping () -> Void, - @ViewBuilder actionLabel: () -> ActionLabel - ) { - self.init(alignment: alignment, header: header, footer: footer, action: Action(action: action, label: actionLabel())) - } - - init( - alignment: HorizontalAlignment = .leading, - @ViewBuilder header: () -> Header, - @ViewBuilder footer: () -> Footer = { EmptyView() } - ) where ActionLabel == EmptyView { - self.init(alignment: alignment, header: header, footer: footer, action: nil) - } -} - - -#if DEBUG -#Preview { - List { - SimpleTile { - Text(verbatim: "Test Tile Header") - } footer: { - Text(verbatim: "The description of a tile") - } - } -} - -#Preview { - List { - SimpleTile { - Text(verbatim: "Test Tile Header only") - } - } -} -#endif diff --git a/NAMS/Tiles/TilesView.swift b/NAMS/Tiles/TilesView.swift index e228a4b..daa0935 100644 --- a/NAMS/Tiles/TilesView.swift +++ b/NAMS/Tiles/TilesView.swift @@ -7,6 +7,7 @@ // import Spezi +@_spi(TestingSupport) import SpeziAccount import SpeziBluetooth #if canImport(SpeziQuestionnaire) @@ -124,9 +125,7 @@ struct TilesView: View { return TilesView() .environment(patientList) .previewWith(standard: NAMSStandard()) { - AccountConfiguration { - MockUserIdPasswordAccountService() - } + AccountConfiguration(service: InMemoryAccountService()) EEGRecordings() DeviceCoordinator() } @@ -135,9 +134,7 @@ struct TilesView: View { #Preview { TilesView() .previewWith(standard: NAMSStandard()) { - AccountConfiguration { - MockUserIdPasswordAccountService() - } + AccountConfiguration(service: InMemoryAccountService()) PatientListModel() EEGRecordings() DeviceCoordinator() diff --git a/NAMS/Utils/Helper/Binding+Negate.swift b/NAMS/Utils/Helper/Binding+Negate.swift deleted file mode 100644 index 3d0767c..0000000 --- a/NAMS/Utils/Helper/Binding+Negate.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project -// -// SPDX-FileCopyrightText: 2023 Stanford University -// -// SPDX-License-Identifier: MIT -// - -import SwiftUI - - -extension Binding where Value == Bool { - /// Negates a `Binding`. - prefix static func ! (value: Binding) -> Binding { - Binding( - get: { !value.wrappedValue }, - set: { value.wrappedValue = !$0 } - ) - } -} diff --git a/NAMS/Utils/Helper/Bundle+Image.swift b/NAMS/Utils/Helper/Bundle+Image.swift deleted file mode 100644 index 6f60921..0000000 --- a/NAMS/Utils/Helper/Bundle+Image.swift +++ /dev/null @@ -1,30 +0,0 @@ -// -// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project -// -// SPDX-FileCopyrightText: 2023 Stanford University -// -// SPDX-License-Identifier: MIT -// - -import SwiftUI - - -extension Foundation.Bundle { - /// Loads an image from the `Bundle` instance. - /// - Parameters: - /// - name: The name of the image. - /// - fileExtension: The file extension of the image. - /// - Returns: Returns the `UIImage` loaded from the `Bundle` instance. - func image(withName name: String, fileExtension: String) -> UIImage { - guard let resourceURL = self.url(forResource: name, withExtension: fileExtension) else { - fatalError("Could not find the file \"\(name).\(fileExtension)\" in the bundle.") - } - - guard let resourceData = try? Data(contentsOf: resourceURL), - let image = UIImage(data: resourceData) else { - fatalError("Decode the image named \"\(name).\(fileExtension)\"") - } - - return image - } -} diff --git a/NAMS/Utils/Helper/CodableArray+RawRepresentable.swift b/NAMS/Utils/Helper/CodableArray+RawRepresentable.swift deleted file mode 100644 index c8eeeff..0000000 --- a/NAMS/Utils/Helper/CodableArray+RawRepresentable.swift +++ /dev/null @@ -1,28 +0,0 @@ -// -// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project -// -// SPDX-FileCopyrightText: 2023 Stanford University -// -// SPDX-License-Identifier: MIT -// - -import Foundation - - -extension Array: RawRepresentable where Element: Codable { - public var rawValue: String { - guard let data = try? JSONEncoder().encode(self), - let rawValue = String(data: data, encoding: .utf8) else { - return "[]" - } - return rawValue - } - - public init?(rawValue: String) { - guard let data = rawValue.data(using: .utf8), - let result = try? JSONDecoder().decode([Element].self, from: data) else { - return nil - } - self = result - } -} diff --git a/NAMS/Utils/Helper/ProcessInfo+PreviewSimulator.swift b/NAMS/Utils/Helper/ProcessInfo+PreviewSimulator.swift deleted file mode 100644 index 65b1cee..0000000 --- a/NAMS/Utils/Helper/ProcessInfo+PreviewSimulator.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project -// -// SPDX-FileCopyrightText: 2023 Stanford University -// -// SPDX-License-Identifier: MIT -// - -import Foundation - - -extension ProcessInfo { - var isPreviewSimulator: Bool { - environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1" - } -} diff --git a/NAMS/Utils/Questionnaire+Identifiable.swift b/NAMS/Utils/Questionnaire+Identifiable.swift index e7941a2..e7936cf 100644 --- a/NAMS/Utils/Questionnaire+Identifiable.swift +++ b/NAMS/Utils/Questionnaire+Identifiable.swift @@ -10,5 +10,9 @@ import SpeziQuestionnaire -extension Questionnaire: Identifiable {} +#if compiler(>=6) +extension Questionnaire: @retroactive Identifiable {} +#else +extension Questionnaire: Swift.Identifiable {} +#endif #endif diff --git a/NAMS/Utils/Testing/BiopotDevicePreview.swift b/NAMS/Utils/Testing/BiopotDevicePreview.swift deleted file mode 100644 index 59b5c1f..0000000 --- a/NAMS/Utils/Testing/BiopotDevicePreview.swift +++ /dev/null @@ -1,30 +0,0 @@ -// -// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project -// -// SPDX-FileCopyrightText: 2023 Stanford University -// -// SPDX-License-Identifier: MIT -// - -import Spezi -import SpeziBluetooth -import SwiftUI - - -private class PreviewDelegate: SpeziAppDelegate { - override var configuration: Configuration { - Configuration { - Bluetooth { - Discover(BiopotDevice.self, by: .advertisedService(BiopotService.self)) - } - } - } -} - - -extension View { - func biopotPreviewSetup2() -> some View { - self - .spezi(PreviewDelegate()) - } -} diff --git a/NAMSTests/BiopotCodingTests.swift b/NAMSTests/BiopotCodingTests.swift index c8d9c48..db0e145 100644 --- a/NAMSTests/BiopotCodingTests.swift +++ b/NAMSTests/BiopotCodingTests.swift @@ -6,9 +6,11 @@ // SPDX-License-Identifier: MIT // + +@_spi(TestingSupport) +import ByteCoding @testable import NAMS import NIOCore -@_spi(TestingSupport) import SpeziBluetooth import XCTByteCoding import XCTest diff --git a/NAMSTests/BiopotDeviceTests.swift b/NAMSTests/BiopotDeviceTests.swift index de3df65..b77a5ec 100644 --- a/NAMSTests/BiopotDeviceTests.swift +++ b/NAMSTests/BiopotDeviceTests.swift @@ -6,6 +6,8 @@ // SPDX-License-Identifier: MIT // +@_spi(TestingSupport) +import ByteCoding @testable import NAMS @_spi(TestingSupport) import SpeziBluetooth diff --git a/NAMSUITests/BiopotTests.swift b/NAMSUITests/BiopotTests.swift index 8f54fc0..f9c62b2 100644 --- a/NAMSUITests/BiopotTests.swift +++ b/NAMSUITests/BiopotTests.swift @@ -31,11 +31,11 @@ final class BiopotTests: XCTestCase { XCTAssertTrue(app.navigationBars.staticTexts["Nearby Devices"].waitForExistence(timeout: 2.0)) - XCTAssertTrue(app.buttons["SML BIO 0xAABBCCDD"].waitForExistence(timeout: 2.0)) - app.buttons["SML BIO 0xAABBCCDD"].tap() + XCTAssertTrue(app.staticTexts["SML BIO 0xAABBCCDD"].waitForExistence(timeout: 2.0)) + app.staticTexts["SML BIO 0xAABBCCDD"].tap() - XCTAssertTrue(app.buttons["SML BIO 0xAABBCCDD, Connected"].waitForExistence(timeout: 2.0)) + XCTAssertTrue(app.staticTexts["SML BIO 0xAABBCCDD, Connected"].waitForExistence(timeout: 2.0)) XCTAssertTrue(app.buttons["Device Details"].waitForExistence(timeout: 2.0)) app.buttons["Device Details"].tap() diff --git a/NAMSUITests/ContactsTests.swift b/NAMSUITests/ContactsTests.swift index 381439a..9d21af6 100644 --- a/NAMSUITests/ContactsTests.swift +++ b/NAMSUITests/ContactsTests.swift @@ -24,6 +24,8 @@ class ContactsTests: XCTestCase { func testContacts() throws { let app = XCUIApplication() + XCTAssertTrue(app.wait(for: .runningForeground, timeout: 5.0)) + XCTAssertTrue(app.tabBars["Tab Bar"].buttons["Contacts"].waitForExistence(timeout: 2)) app.tabBars["Tab Bar"].buttons["Contacts"].tap() diff --git a/NAMSUITests/MockDeviceTests.swift b/NAMSUITests/MockDeviceTests.swift index 116be95..d8fb44f 100644 --- a/NAMSUITests/MockDeviceTests.swift +++ b/NAMSUITests/MockDeviceTests.swift @@ -36,12 +36,12 @@ class MockDeviceTests: XCTestCase { // we don't check for the existence of the progress view as we probably cannot time that precisely - XCTAssertTrue(app.buttons["Mock Device 1"].waitForExistence(timeout: 5.0)) - app.buttons["Mock Device 1"].tap() + XCTAssertTrue(app.staticTexts["Mock Device 1"].waitForExistence(timeout: 5.0)) + app.staticTexts["Mock Device 1"].tap() - XCTAssertTrue(app.buttons["Mock Device 1, Connected"].waitForExistence(timeout: 5.0)) - XCTAssertTrue(app.buttons["Mock Device 2"].waitForExistence(timeout: 0.5)) // ensure not connected + XCTAssertTrue(app.staticTexts["Mock Device 1, Connected"].waitForExistence(timeout: 5.0)) + XCTAssertTrue(app.staticTexts["Mock Device 2"].waitForExistence(timeout: 0.5)) // ensure not connected XCTAssertTrue(app.buttons["Device Details"].waitForExistence(timeout: 2.0)) app.buttons["Device Details"].tap() @@ -82,7 +82,7 @@ class MockDeviceTests: XCTestCase { XCTAssertTrue(app.buttons["Disconnect"].waitForExistence(timeout: 0.5)) app.buttons["Disconnect"].tap() - XCTAssertTrue(app.buttons["Mock Device 1"].waitForExistence(timeout: 5.0)) // ensure not connected + XCTAssertTrue(app.staticTexts["Mock Device 1"].waitForExistence(timeout: 5.0)) // ensure not connected } func testSuccessfulEEGRecording() { @@ -186,9 +186,9 @@ extension XCUIApplication { XCTAssertTrue(navigationBars.staticTexts["Nearby Devices"].waitForExistence(timeout: 2.0)) - XCTAssertTrue(buttons["Mock Device 1"].waitForExistence(timeout: 5.0)) - buttons["Mock Device 1"].tap() - XCTAssertTrue(buttons["Mock Device 1, Connected"].waitForExistence(timeout: 5.0)) + XCTAssertTrue(staticTexts["Mock Device 1"].waitForExistence(timeout: 5.0)) + staticTexts["Mock Device 1"].tap() + XCTAssertTrue(staticTexts["Mock Device 1, Connected"].waitForExistence(timeout: 5.0)) XCTAssertTrue(navigationBars.buttons["Close"].waitForExistence(timeout: 0.5)) navigationBars.buttons["Close"].tap() diff --git a/NAMSUITests/PatientInformationTests.swift b/NAMSUITests/PatientInformationTests.swift index e4f9b25..4a4b6d9 100644 --- a/NAMSUITests/PatientInformationTests.swift +++ b/NAMSUITests/PatientInformationTests.swift @@ -112,7 +112,6 @@ final class PatientInformationTests: XCTestCase { XCTAssertTrue(app.textFields["enter last name"].waitForExistence(timeout: 0.5)) XCTAssertTrue(app.textFields["Patient Code"].waitForExistence(timeout: 0.5)) XCTAssertTrue(app.buttons["Sex, Not Disclosed"].waitForExistence(timeout: 0.5)) - XCTAssertTrue(app.textViews["add notes ..."].waitForExistence(timeout: 0.5)) try app.textFields["enter first name"].enter(value: "Jane") try app.textFields["enter last name"].enter(value: "Stanford") @@ -122,8 +121,6 @@ final class PatientInformationTests: XCTestCase { XCTAssertTrue(app.buttons["Female"].waitForExistence(timeout: 0.5)) app.buttons["Female"].tap() - try app.textViews["add notes ..."].enter(value: "My note ...", checkIfTextWasEnteredCorrectly: false, dismissKeyboard: false) - XCTAssertTrue(app.navigationBars.buttons["Done"].waitForExistence(timeout: 0.5)) app.navigationBars.buttons["Done"].tap() @@ -136,7 +133,6 @@ final class PatientInformationTests: XCTestCase { XCTAssertTrue(app.staticTexts["Jane Stanford"].waitForExistence(timeout: 0.5)) XCTAssertTrue(app.staticTexts["Sex, Female"].waitForExistence(timeout: 0.5)) XCTAssertTrue(app.staticTexts["Birthdate"].waitForExistence(timeout: 0.5)) - XCTAssertTrue(app.staticTexts["My note ..."].waitForExistence(timeout: 0.5)) XCTAssertTrue(app.buttons["Select Patient"].waitForExistence(timeout: 0.5)) app.buttons["Select Patient"].tap() diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 2006872..81229b7 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -21,7 +21,7 @@ platform :ios do scheme: "NAMS", derived_data_path: ".derivedData", code_coverage: true, - devices: ["iPhone 15 Pro"], + devices: ["iPhone 16 Pro"], disable_slide_to_type: false, concurrent_workers: 1, max_concurrent_simulators: 1,