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,