From 4dfbab19ec9fe53cafc2adeae9ba03202f7ffc40 Mon Sep 17 00:00:00 2001 From: Ladislas de Toldi Date: Thu, 7 Dec 2023 00:45:19 +0100 Subject: [PATCH] :rotating_light: (swiftformat): --enable organizeDeclarations --- .swiftformat | 2 + .../Sources/Models/Commands.swift | 33 +-- Apps/BLEKitExample/Sources/Models/Robot.swift | 16 +- .../BotConnectComponents/BotFaceView.swift | 8 +- .../View/BotConnectComponents/BotStore.swift | 4 + .../Sources/View/RobotListView.swift | 4 + .../Sources/DesignSystem.swift | 25 +- .../GEKNewSystem/GEKNewSystemView.swift | 4 + .../Sources/SwitchBoard.swift | 4 + Apps/LekaApp/Sources/Data/AvatarsData.swift | 2 + .../Sources/Data/DiscoveryCompany.swift | 6 +- Apps/LekaApp/Sources/Data/TilesContent.swift | 2 + Apps/LekaApp/Sources/HomeView.swift | 4 + .../Sources/Models/ActivityModel.swift | 78 +++--- .../LekaApp/Sources/Models/CompanyModel.swift | 3 + .../Sources/Models/CurriculumModel.swift | 48 ++-- .../LekaApp/Sources/Models/SidebarModel.swift | 2 + .../Models/YAMLDecoder/YAMLDecoder.swift | 8 +- .../Sources/Stores/ActivityViewModel.swift | 39 +-- .../Sources/Stores/CurriculumViewModel.swift | 8 +- .../Sources/Stores/NavigationViewModel.swift | 33 +-- Apps/LekaApp/Sources/Styles/Styles.swift | 2 + .../Activities/ActivityListView.swift | 5 + .../Components/ActivityListCell.swift | 4 + .../SelectedActivityInstructionsView.swift | 4 + .../Activities_shared/GoButton.swift | 44 ++-- .../Activities_shared/InstructionsView.swift | 4 + .../MarkdownRepresentable.swift | 73 ++++-- .../Commands/CommandListView.swift | 12 +- .../ActivityListCell_Curriculums.swift | 4 + .../Components/CurriculumPillShapedView.swift | 4 + .../Curriculums/CurriculumDetailsView.swift | 14 +- .../Curriculums/CurriculumListView.swift | 8 +- .../CurrentGameInstructionView.swift | 56 +++-- .../Views/Game/Components/LottieView.swift | 34 ++- .../Views/Game/Components/PlayZone.swift | 80 +++--- .../Game/Components/ProgressBarView.swift | 24 +- .../LekaApp/Sources/Views/Game/GameView.swift | 234 +++++++++--------- .../InformationTiles/InfoTile.swift | 13 +- .../GlobalComponentViews/LekaTextField.swift | 4 + .../SignUpPath/SignupFinalStep.swift | 8 +- .../SignUpPath/SignupStep1.swift | 10 +- .../SignUpPath/SignupStep2.swift | 10 +- .../SignUpPath/SignupStep3.swift | 10 +- .../WelcomeViews/LoginView.swift | 71 +++--- .../WelcomeViews/SignupView.swift | 69 +++--- .../WelcomeViews/WelcomeView.swift | 4 + .../Views/Profiles/ProfileEditorView.swift | 4 + .../Pickers/AvatarPicker_Teachers.swift | 8 +- .../Pickers/AvatarPicker_Users.swift | 8 +- .../Components/AvatarPickerStore.swift | 4 + .../Components/JobPickerTrigger.swift | 4 + .../CreateTeacherProfileView.swift | 68 ++--- .../CreateUserProfileView.swift | 16 +- .../JobPicker/Components/JobPickerStore.swift | 19 +- .../JobPicker/JobPicker.swift | 11 +- .../ProfileSets/ProfileSet_Teachers.swift | 64 ++--- .../ProfileSets/ProfileSet_Users.swift | 18 +- .../TeacherSet_AvatarCell.swift | 4 + .../UserSet_AvatarCell.swift | 4 + .../Components/SettingsSection_Profiles.swift | 4 + .../ProfilesButton/Components/TickPic.swift | 20 +- .../Components/UserSidebarAvatarCell.swift | 4 + .../GoToProfileEditorButton.swift | 4 + .../Components/RobotConnectionIndicator.swift | 10 +- .../RobotButton/GoToRobotConnectButton.swift | 4 + .../Components/Header/SidebarHeaderView.swift | 4 + .../Sources/Views/Sidebar/SidebarView.swift | 10 +- .../Sources/Libs/FirmwareManager.swift | 36 +-- .../Template/UpdateProcessTemplate.swift | 34 ++- .../UpdateProcessController.swift | 40 +-- .../Version/UpdateProcessV100.swift | 136 ++++++---- .../Version/UpdateProcessV130.swift | 144 +++++++---- Apps/LekaUpdater/Sources/Localization.swift | 14 +- .../View/InformationView/ChangelogView.swift | 12 +- .../InformationView/InformationView.swift | 12 +- .../RobotCannotBeUpdateIllustration.swift | 48 ++-- .../RobotInformationView.swift | 6 +- .../RobotNeedsUpdateIllustration.swift | 48 ++-- .../RobotUpToDateIllustration.swift | 48 ++-- .../Sources/View/UpdateStatusDemoView.swift | 24 +- .../View/UpdatingViews/SendingFileView.swift | 6 +- .../ViewModel/InformationViewModel.swift | 20 +- .../ViewModel/RequirementsViewModel.swift | 16 +- .../ViewModel/RobotInformationViewModel.swift | 18 +- .../ViewModel/UpdateStatusViewModel.swift | 48 ++-- Examples/Module/Sources/HelloView.swift | 10 +- .../AccountKitExample/Sources/MainView.swift | 4 + .../ViewModels/OrganisationViewModel.swift | 42 ++-- .../Sources/Views/HomeView.swift | 7 +- .../Sources/Views/LoginView.swift | 9 +- .../Sources/Views/SignupView.swift | 7 +- .../ViewModels/RobotListViewModel.swift | 46 ++-- .../Sources/Views/ConnectButton.swift | 4 + .../Sources/Views/ContentView.swift | 13 +- .../Sources/Views/RobotDiscoveryView.swift | 16 +- .../Sources/Views/SendDataButton.swift | 4 + Modules/BLEKit/Sources/BLEManager.swift | 38 +-- Modules/BLEKit/Sources/BLESpecs.swift | 32 +-- .../Models/AdvertisingServiceData.swift | 10 +- .../Models/CharacteristicModelNotifying.swift | 16 +- .../Models/CharacteristicModelReadOnly.swift | 14 +- .../Models/CharacteristicModelWriteOnly.swift | 14 +- .../Sources/Models/RobotAdvertisingData.swift | 16 +- .../Sources/Models/RobotDiscoveryModel.swift | 24 +- .../Sources/Models/RobotPeripheral.swift | 26 +- .../Sources/Activity/Activity.swift | 32 ++- .../Activity/ActivitySequenceManager.swift | 14 +- Modules/ContentKit/Sources/ContentKit.swift | 10 +- .../Exercice+AudioRecordingPlayer.swift | 16 +- .../Exercise/Exercice+HideAndSeek.swift | 40 +-- .../Exercise/Exercice+MusicalInstrument.swift | 18 +- .../Sources/Exercise/Exercise+Action.swift | 104 ++++---- .../Exercise+DragAndDropIntoZones.swift | 2 + .../Exercise+DragAndDropToAssociate.swift | 40 +-- .../Exercise/Exercise+TouchToSelect.swift | 40 +-- .../Sources/Exercise/Exercise.swift | 32 ++- .../Sources/Utils/AudioRecording.swift | 26 +- Modules/DesignKit/Sources/ContentView.swift | 10 +- Modules/DesignKit/Sources/LottieView.swift | 38 +-- .../Modifiers/AlertWhenNoUserSelected.swift | 14 +- .../Modifiers/AlertWhenRobotIsNeeded.swift | 14 +- .../Staging/Melody/MelodyGameplay.swift | 29 ++- .../Staging/Melody/MelodySongModel.swift | 12 +- .../Sources/Staging/Melody/MelodyView.swift | 21 +- .../Staging/Melody/MelodyViewModel.swift | 21 +- .../Sources/Staging/Pairing/PairingView.swift | 12 +- .../DraggableImageAnswerNode.swift | 10 +- ...DragAndDropIntoZonesView+0_BaseScene.swift | 48 ++-- .../DragAndDropIntoZonesView+ViewModel.swift | 18 +- .../IntoZones/DragAndDropIntoZonesView.swift | 18 +- ...agAndDropToAssociateView+0_BaseScene.swift | 32 ++- ...DragAndDropToAssociateView+ViewModel.swift | 18 +- .../DragAndDropToAssociateView.swift | 28 ++- ...anceFreeze+LauncherView+ButtonStyles.swift | 8 +- .../DanceFreeze/DanceFreeze+MainView.swift | 41 +-- .../DanceFreeze+MainViewViewModel.swift | 50 ++-- .../DanceFreeze/DanceFreeze+PlayerView.swift | 12 +- .../DanceFreeze+RobotManager.swift | 30 ++- .../DanceFreeze+SongSelectorView.swift | 20 +- .../HideAndSeekView+ButtonLabel.swift | 8 +- .../HideAndSeekView+HiddenView.swift | 1 + .../HideAndSeek/HideAndSeekView+Player.swift | 30 ++- .../HideAndSeek/HideAndSeekView.swift | 25 +- ...ntView+XylophoneView+TileButtonStyle.swift | 4 + .../MusicalInstrumentView+XylophoneView.swift | 14 +- .../Instrument/MusicalInstrumentView.swift | 10 +- .../RemoteArrowView+ArrowButton.swift | 2 + .../RemoteArrow/RemoteArrowView.swift | 1 + .../Joystick/JoystickView.swift | 6 +- .../Joystick/JoystickViewViewModel.swift | 22 +- ...edZoneSelectorView+BeltSectionButton.swift | 10 +- .../LedZoneSelectorView+BeltSectionIcon.swift | 8 +- .../LedZoneSelectorView+EarButton.swift | 16 +- .../LedZoneSelectorView+ModeButton.swift | 4 + .../LedZoneSelector/LedZoneSelectorView.swift | 4 + .../RemoteStandard+MainView.swift | 6 +- .../Exercises/Touch/ChoiceColorView.swift | 20 +- .../Exercises/Touch/ChoiceImageView.swift | 20 +- ...tenThenTouchToSelectView+1_OneChoice.swift | 8 +- ...tenThenTouchToSelectView+2_TwoChoice.swift | 10 +- ...nThenTouchToSelectView+3_ThreeChoice.swift | 12 +- ...enThenTouchToSelectView+4_FourChoice.swift | 12 +- ...enThenTouchToSelectView+5_FiveChoice.swift | 12 +- ...tenThenTouchToSelectView+6_SixChoice.swift | 12 +- .../ListenThenTouchToSelectView.swift | 30 ++- .../ObserveThenTouchToSelect.swift | 36 +-- ...rveThenTouchToSelectView+1_OneChoice.swift | 8 +- ...rveThenTouchToSelectView+2_TwoChoice.swift | 10 +- ...eThenTouchToSelectView+3_ThreeChoice.swift | 12 +- ...veThenTouchToSelectView+4_FourChoice.swift | 12 +- ...veThenTouchToSelectView+5_FiveChoice.swift | 12 +- ...rveThenTouchToSelectView+6_SixChoice.swift | 12 +- ...botThenTouchToSelectView+1_OneChoice.swift | 8 +- ...botThenTouchToSelectView+2_TwoChoice.swift | 10 +- ...tThenTouchToSelectView+3_ThreeChoice.swift | 12 +- ...otThenTouchToSelectView+4_FourChoice.swift | 12 +- ...otThenTouchToSelectView+5_FiveChoice.swift | 12 +- ...botThenTouchToSelectView+6_SixChoice.swift | 12 +- .../RobotThenTouchToSelectView.swift | 38 +-- .../TouchToSelectChoiceView.swift | 14 +- .../TouchToSelectView+1_OneChoice.swift | 8 +- .../TouchToSelectView+2_TwoChoices.swift | 10 +- .../TouchToSelectView+3_ThreeChoices.swift | 10 +- .../TouchToSelectView+4_FourChoices.swift | 12 +- .../TouchToSelectView+5_FiveChoices.swift | 12 +- .../TouchToSelectView+6_SixChoices.swift | 12 +- .../TouchToSelect/TouchToSelectView.swift | 28 ++- .../TouchToSelectViewViewModel.swift | 18 +- .../_NewSystem/Utils/AudioPlayer.swift | 24 +- .../_NewSystem/Utils/MIDIInstrument.swift | 2 + .../Sources/_NewSystem/Utils/MIDIPlayer.swift | 50 ++-- .../Sources/_NewSystem/Utils/MIDISample.swift | 10 +- .../Sources/_NewSystem/Utils/MIDIScale.swift | 2 + .../Views/Activity/ActivityProgressBar.swift | 8 +- .../Views/Activity/ActivityView.swift | 48 ++-- .../Activity/ActivityViewViewModel.swift | 48 ++-- .../Exercise/ExerciseInstructionsButton.swift | 15 +- .../LogKitExample/Sources/NewModule.swift | 8 +- Modules/LogKit/Sources/LogKit.swift | 8 +- Modules/LogKit/Sources/LogKitLogHandler.swift | 71 +++--- .../RobotKitExample/Sources/MainView.swift | 6 +- .../ViewModels/RobotControlViewModel.swift | 16 +- .../Sources/Views/RobotControlView.swift | 12 +- Modules/RobotKit/Sources/MagicCards.swift | 14 +- Modules/RobotKit/Sources/Robot+Colors.swift | 48 ++-- Modules/RobotKit/Sources/Robot+Lights.swift | 75 ++++-- Modules/RobotKit/Sources/Robot+Motion.swift | 30 ++- .../RobotKit/Sources/Robot+Reinforcers.swift | 2 + Modules/RobotKit/Sources/Robot.swift | 38 +-- .../Sources/UI/Buttons/ButtonBordered.swift | 14 +- .../Sources/UI/Buttons/ButtonFilled.swift | 14 +- .../UI/Buttons/ButtonStyle+Extension.swift | 22 +- .../RobotKit/Sources/UI/Buttons/Buttons.swift | 14 +- .../UI/ViewModels/BatteryViewModel.swift | 10 +- .../ConnectedRobotInformationViewModel.swift | 24 +- .../ViewModels/RobotConnectionViewModel.swift | 40 +-- .../ViewModels/RobotDiscoveryViewModel.swift | 28 ++- .../UI/Views/RobotConnectionView.swift | 76 +++--- .../Sources/UI/Views/RobotDiscoveryView.swift | 18 +- .../LocalHelper.swift | 6 +- .../Project+Templates+Module.swift | 8 +- 222 files changed, 3017 insertions(+), 1799 deletions(-) diff --git a/.swiftformat b/.swiftformat index 34eb4c2a0b..3fc2191e76 100644 --- a/.swiftformat +++ b/.swiftformat @@ -86,6 +86,8 @@ --enable opaqueGenericParameters +--enable organizeDeclarations + # Disabled rules --disable blankLinesBetweenImports diff --git a/Apps/BLEKitExample/Sources/Models/Commands.swift b/Apps/BLEKitExample/Sources/Models/Commands.swift index d9588f51ec..7d905e4f4e 100644 --- a/Apps/BLEKitExample/Sources/Models/Commands.swift +++ b/Apps/BLEKitExample/Sources/Models/Commands.swift @@ -7,12 +7,6 @@ import Foundation // MARK: - LKCommand enum LKCommand { - static let startByte: UInt8 = 0x2A - static let startByteLength: UInt8 = 0x04 - static let ledFull: UInt8 = 0x13 - static let motor: UInt8 = 0x20 - static let motivator: UInt8 = 0x50 - // MARK: - LKCommand Led Full enum LedFull { @@ -45,6 +39,12 @@ enum LKCommand { static let spinBlink: UInt8 = 0x54 static let blinkGreen: UInt8 = 0x55 } + + static let startByte: UInt8 = 0x2A + static let startByteLength: UInt8 = 0x04 + static let ledFull: UInt8 = 0x13 + static let motor: UInt8 = 0x20 + static let motivator: UInt8 = 0x50 } func checksum8(_ values: [UInt8]) -> UInt8 { @@ -60,9 +60,22 @@ func checksum8(_ values: [UInt8]) -> UInt8 { // MARK: - CommandKit class CommandKit { + // MARK: Lifecycle + + // MARK: - Initialisation + + init() { + for _ in 0...LKCommand.startByteLength - 1 { + self.startSequence.append(LKCommand.startByte) + } + } + + // MARK: Internal + // MARK: - Singleton static let sharedSingleton = CommandKit() + let command = LKCommand.self // MARK: - Variables @@ -73,14 +86,6 @@ class CommandKit { var numberOfCommands: Int = 0 var isEncapsulated: Bool = false - // MARK: - Initialisation - - init() { - for _ in 0...LKCommand.startByteLength - 1 { - self.startSequence.append(LKCommand.startByte) - } - } - // MARK: - Led Functions func addAllLeds(of earOrBelt: UInt8, rgbColor red: UInt8, _ green: UInt8, _ blue: UInt8) { diff --git a/Apps/BLEKitExample/Sources/Models/Robot.swift b/Apps/BLEKitExample/Sources/Models/Robot.swift index 928fb17347..5eb1c0afbd 100644 --- a/Apps/BLEKitExample/Sources/Models/Robot.swift +++ b/Apps/BLEKitExample/Sources/Models/Robot.swift @@ -6,12 +6,7 @@ import BLEKit import Foundation class Robot: ObservableObject { - var robotPeripheral: RobotPeripheral? { - didSet { - updateDeviceInformation() - subscribeToDeviceUpdates() - } - } + // MARK: Internal @Published var manufacturer: String = "" @Published var modelNumber: String = "" @@ -26,6 +21,13 @@ class Robot: ObservableObject { let commands = CommandKit() + var robotPeripheral: RobotPeripheral? { + didSet { + updateDeviceInformation() + subscribeToDeviceUpdates() + } + } + func updateDeviceInformation() { guard let robotPeripheral = self.robotPeripheral else { return } @@ -51,6 +53,8 @@ class Robot: ObservableObject { robotPeripheral.sendCommand(data) } + // MARK: Private + private func registerReadOnlyCharacteristicClosures() { for char in kDefaultReadOnlyCharacteristics { var newChar = char diff --git a/Apps/BLEKitExample/Sources/View/BotConnectComponents/BotFaceView.swift b/Apps/BLEKitExample/Sources/View/BotConnectComponents/BotFaceView.swift index 78a7aa82cc..dd352b4a51 100644 --- a/Apps/BLEKitExample/Sources/View/BotConnectComponents/BotFaceView.swift +++ b/Apps/BLEKitExample/Sources/View/BotConnectComponents/BotFaceView.swift @@ -7,6 +7,8 @@ import SwiftUI // MARK: - BotFaceView struct BotFaceView: View { + // MARK: Internal + @Binding var isSelected: Bool @Binding var isConnected: Bool @Binding var name: String @@ -14,8 +16,6 @@ struct BotFaceView: View { @Binding var isCharging: Bool @Binding var osVersion: String - @State private var rotation: CGFloat = 0.0 - var body: some View { VStack { Image("robot_connexion_bluetooth") @@ -50,6 +50,10 @@ struct BotFaceView: View { } .animation(.default, value: isConnected) } + + // MARK: Private + + @State private var rotation: CGFloat = 0.0 } // MARK: - BotFaceView_Previews diff --git a/Apps/BLEKitExample/Sources/View/BotConnectComponents/BotStore.swift b/Apps/BLEKitExample/Sources/View/BotConnectComponents/BotStore.swift index 85a0be8d7e..a772648fe5 100644 --- a/Apps/BLEKitExample/Sources/View/BotConnectComponents/BotStore.swift +++ b/Apps/BLEKitExample/Sources/View/BotConnectComponents/BotStore.swift @@ -16,6 +16,8 @@ struct NoFeedback_ButtonStyle: ButtonStyle { // MARK: - BotStore struct BotStore: View { + // MARK: Internal + @EnvironmentObject var bleManager: BLEManager @EnvironmentObject var robot: Robot @ObservedObject var botVM: BotViewModel @@ -57,6 +59,8 @@ struct BotStore: View { .frame(height: 500) } + // MARK: Private + private var availableBots: some View { ForEach(0.. Bool { - lhs.instruction == rhs.instruction + // MARK: Lifecycle + + init( + instruction: LocalizedContent = LocalizedContent(), + correctAnswer: String = "", + images: [String] = [], + sound: [String]? = [] + ) { + self.instruction = instruction + self.correctAnswer = correctAnswer + self.images = images + self.sound = sound } + // MARK: Internal + enum CodingKeys: String, CodingKey { case instruction, images, sound case correctAnswer = "correct_answer" @@ -87,15 +107,7 @@ struct Step: Codable, Equatable { var images: [String] var sound: [String]? - init( - instruction: LocalizedContent = LocalizedContent(), - correctAnswer: String = "", - images: [String] = [], - sound: [String]? = [] - ) { - self.instruction = instruction - self.correctAnswer = correctAnswer - self.images = images - self.sound = sound + static func == (lhs: Step, rhs: Step) -> Bool { + lhs.instruction == rhs.instruction } } diff --git a/Apps/LekaApp/Sources/Models/CompanyModel.swift b/Apps/LekaApp/Sources/Models/CompanyModel.swift index c0cb2bf900..7f20ce9ed6 100644 --- a/Apps/LekaApp/Sources/Models/CompanyModel.swift +++ b/Apps/LekaApp/Sources/Models/CompanyModel.swift @@ -63,6 +63,9 @@ enum Professions: String, Identifiable, CaseIterable { case educSpe, eje, monit, monitAt, teach, ASC, psychoMot, ergo, ortho, kine, pedopsy, med, psy, infir, soign, AESH, AES, AVDH, auxVieScol, pueri, auxPueri, ludo + + // MARK: Internal + // swiftlint:enable identifier_name var id: Self { self } diff --git a/Apps/LekaApp/Sources/Models/CurriculumModel.swift b/Apps/LekaApp/Sources/Models/CurriculumModel.swift index 0a8c216b0b..b149e66906 100644 --- a/Apps/LekaApp/Sources/Models/CurriculumModel.swift +++ b/Apps/LekaApp/Sources/Models/CurriculumModel.swift @@ -7,13 +7,7 @@ import Foundation // MARK: - CurriculumList struct CurriculumList: Codable { - enum CodingKeys: String, CodingKey { - case curriculums - case sectionTitle = "section_title" - } - - var sectionTitle: LocalizedContent - var curriculums: [String] + // MARK: Lifecycle init( sectionTitle: LocalizedContent = LocalizedContent(), @@ -22,24 +16,22 @@ struct CurriculumList: Codable { self.sectionTitle = sectionTitle self.curriculums = curriculums } -} -// MARK: - Curriculum + // MARK: Internal -struct Curriculum: Codable, Identifiable { enum CodingKeys: String, CodingKey { - case title, subtitle, activities - case id = "uuid" - case fullTitle = "full_title" - case quantity = "number_of_activities" + case curriculums + case sectionTitle = "section_title" } - var id: String - var title: LocalizedContent - var subtitle: LocalizedContent - var fullTitle: LocalizedContent - var quantity: Int - var activities: [String] + var sectionTitle: LocalizedContent + var curriculums: [String] +} + +// MARK: - Curriculum + +struct Curriculum: Codable, Identifiable { + // MARK: Lifecycle init( id: String = "", @@ -56,4 +48,20 @@ struct Curriculum: Codable, Identifiable { self.quantity = quantity self.activities = activities } + + // MARK: Internal + + enum CodingKeys: String, CodingKey { + case title, subtitle, activities + case id = "uuid" + case fullTitle = "full_title" + case quantity = "number_of_activities" + } + + var id: String + var title: LocalizedContent + var subtitle: LocalizedContent + var fullTitle: LocalizedContent + var quantity: Int + var activities: [String] } diff --git a/Apps/LekaApp/Sources/Models/SidebarModel.swift b/Apps/LekaApp/Sources/Models/SidebarModel.swift index 7e6f39a58b..cbcec791e2 100644 --- a/Apps/LekaApp/Sources/Models/SidebarModel.swift +++ b/Apps/LekaApp/Sources/Models/SidebarModel.swift @@ -10,6 +10,8 @@ import SwiftUI enum SidebarDestinations: Int, Identifiable, CaseIterable, Hashable { case curriculums, activities, commands + // MARK: Internal + var id: Self { self } } diff --git a/Apps/LekaApp/Sources/Models/YAMLDecoder/YAMLDecoder.swift b/Apps/LekaApp/Sources/Models/YAMLDecoder/YAMLDecoder.swift index 1ba0960b80..59b7494b97 100644 --- a/Apps/LekaApp/Sources/Models/YAMLDecoder/YAMLDecoder.swift +++ b/Apps/LekaApp/Sources/Models/YAMLDecoder/YAMLDecoder.swift @@ -30,6 +30,8 @@ extension YamlFileDecodable { enum CustomError: Error, CustomStringConvertible { case failedToGetFilePath + // MARK: Internal + var description: String { switch self { case .failedToGetFilePath: return "Unable to get the path to the Yaml file!" @@ -40,7 +42,7 @@ enum CustomError: Error, CustomStringConvertible { // MARK: - YamlFiles struct YamlFiles: RawRepresentable, Hashable { - var rawValue: String + // MARK: Lifecycle init?(rawValue: String) { self.rawValue = rawValue @@ -49,6 +51,10 @@ struct YamlFiles: RawRepresentable, Hashable { init(_ rawValue: String) { self.rawValue = rawValue } + + // MARK: Internal + + var rawValue: String } // MARK: - CurriculumCategories diff --git a/Apps/LekaApp/Sources/Stores/ActivityViewModel.swift b/Apps/LekaApp/Sources/Stores/ActivityViewModel.swift index 3d4c3bed1e..2ddf4b0397 100644 --- a/Apps/LekaApp/Sources/Stores/ActivityViewModel.swift +++ b/Apps/LekaApp/Sources/Stores/ActivityViewModel.swift @@ -9,25 +9,6 @@ import Yams // @MainActor class ActivityViewModel: NSObject, ObservableObject, YamlFileDecodable { - func getActivity(_ title: String) -> Activity { - do { - return try self.decodeYamlFile(withName: title, toType: Activity.self) - } catch { - print("Activities: Failed to decode Yaml file with error:", error) - return Activity() - } - } - - // Temporary Instructions Source - func getInstructions() -> String { - do { - return try self.decodeYamlFile(withName: "instructions", toType: Instructions.self).instructions.localized() - } catch { - print("Instructions: Failed to decode Yaml file with error:", error) - return Instructions().instructions.localized() - } - } - // MARK: - Current Activity's properties @Published var currentActivity = Activity() @@ -67,6 +48,26 @@ class ActivityViewModel: NSObject, ObservableObject, YamlFileDecodable { // Here because GameMetrics is @EnvironmentObject @Published var isSpeaking: Bool = false @Published var synth = AVSpeechSynthesizer() + + func getActivity(_ title: String) -> Activity { + do { + return try self.decodeYamlFile(withName: title, toType: Activity.self) + } catch { + print("Activities: Failed to decode Yaml file with error:", error) + return Activity() + } + } + + // Temporary Instructions Source + func getInstructions() -> String { + do { + return try self.decodeYamlFile(withName: "instructions", toType: Instructions.self).instructions.localized() + } catch { + print("Instructions: Failed to decode Yaml file with error:", error) + return Instructions().instructions.localized() + } + } + func speak(sentence: String) { synth.delegate = self let utterance = AVSpeechUtterance(string: sentence) diff --git a/Apps/LekaApp/Sources/Stores/CurriculumViewModel.swift b/Apps/LekaApp/Sources/Stores/CurriculumViewModel.swift index 83187a668d..7db35a6021 100644 --- a/Apps/LekaApp/Sources/Stores/CurriculumViewModel.swift +++ b/Apps/LekaApp/Sources/Stores/CurriculumViewModel.swift @@ -19,6 +19,11 @@ class CurriculumViewModel: ObservableObject, YamlFileDecodable { @Published var selectedCurriculumRank: String = "" @Published var selectedCurriculumIcon: String = "" @Published var selectedCurriculumDescription: String = "" + + // MARK: - ActivityList -> will not stay here - list activities files from the Bundle instead + + @Published var activityFilesCompleteList: [String] = [] // will not stay like that - list files instead + @Published var selectedCurriculum: Int? = 0 { didSet { currentCurriculum = availableCurriculums[selectedCurriculum ?? 0] @@ -82,9 +87,6 @@ class CurriculumViewModel: ObservableObject, YamlFileDecodable { } } - // MARK: - ActivityList -> will not stay here - list activities files from the Bundle instead - - @Published var activityFilesCompleteList: [String] = [] // will not stay like that - list files instead func getCompleteActivityList() { for curriculum in availableCurriculums { activityFilesCompleteList.append(contentsOf: curriculum.activities) diff --git a/Apps/LekaApp/Sources/Stores/NavigationViewModel.swift b/Apps/LekaApp/Sources/Stores/NavigationViewModel.swift index a57227d41b..02b13a5151 100644 --- a/Apps/LekaApp/Sources/Stores/NavigationViewModel.swift +++ b/Apps/LekaApp/Sources/Stores/NavigationViewModel.swift @@ -6,12 +6,6 @@ import Foundation import SwiftUI class NavigationViewModel: ObservableObject { - // sidebar utils - @Published var sidebarVisibility = NavigationSplitViewVisibility.all - @Published var showSettings: Bool = false - @Published var showProfileEditor: Bool = false - @Published var showRobotPicker: Bool = false - // Educative Content section data static let educContentSectionLabels: [SectionLabel] = [ SectionLabel( @@ -30,6 +24,13 @@ class NavigationViewModel: ObservableObject { label: "Commandes" ), ] + + // sidebar utils + @Published var sidebarVisibility = NavigationSplitViewVisibility.all + @Published var showSettings: Bool = false + @Published var showProfileEditor: Bool = false + @Published var showRobotPicker: Bool = false + @Published var educContentList = ListModel( title: "Contenu Éducatif", sections: educContentSectionLabels @@ -37,6 +38,16 @@ class NavigationViewModel: ObservableObject { // Overall Navigation from the sidebar @Published var currentView: SidebarDestinations = .curriculums + // Navigation within FullScreenCover to GameView() + @Published var pathsFromHome = NavigationPath() + @Published var showActivitiesFullScreenCover: Bool = false + @Published var pathToGame = NavigationPath() + + // Info Tiles triggers + @Published var showInfoCurriculums: Bool = true + @Published var showInfoActivities: Bool = true + @Published var showInfoCommands: Bool = true + // Returned Views & NavigationTitles @ViewBuilder var allSidebarDestinationViews: some View { switch currentView { @@ -54,16 +65,6 @@ class NavigationViewModel: ObservableObject { } } - // Navigation within FullScreenCover to GameView() - @Published var pathsFromHome = NavigationPath() - @Published var showActivitiesFullScreenCover: Bool = false - @Published var pathToGame = NavigationPath() - - // Info Tiles triggers - @Published var showInfoCurriculums: Bool = true - @Published var showInfoActivities: Bool = true - @Published var showInfoCommands: Bool = true - func contextualInfo() -> TileData { switch currentView { case .curriculums: return .curriculums diff --git a/Apps/LekaApp/Sources/Styles/Styles.swift b/Apps/LekaApp/Sources/Styles/Styles.swift index 55ce9da86e..0051e9dc8c 100644 --- a/Apps/LekaApp/Sources/Styles/Styles.swift +++ b/Apps/LekaApp/Sources/Styles/Styles.swift @@ -61,6 +61,7 @@ struct CircledIcon_NoFeedback_ButtonStyle: ButtonStyle { struct Connect_ButtonStyle: ButtonStyle { @EnvironmentObject var metrics: UIMetrics var reversed: Bool = false + func makeBody(configuration: Self.Configuration) -> some View { configuration.label .font(metrics.bold15) @@ -81,6 +82,7 @@ struct JobPickerToggleStyle: ToggleStyle { var onImage = "checkmark.circle" var offImage = "circle" var action: () -> Void + func makeBody(configuration: Configuration) -> some View { HStack(spacing: 10) { Image(systemName: configuration.isOn ? onImage : offImage) diff --git a/Apps/LekaApp/Sources/Views/EducationalContent/Activities/ActivityListView.swift b/Apps/LekaApp/Sources/Views/EducationalContent/Activities/ActivityListView.swift index 09b9ad67ca..4964cd8359 100644 --- a/Apps/LekaApp/Sources/Views/EducationalContent/Activities/ActivityListView.swift +++ b/Apps/LekaApp/Sources/Views/EducationalContent/Activities/ActivityListView.swift @@ -6,6 +6,8 @@ import DesignKit import SwiftUI struct ActivityListView: View { + // MARK: Internal + @EnvironmentObject var curriculumVM: CurriculumViewModel @EnvironmentObject var activityVM: ActivityViewModel @EnvironmentObject var navigationVM: NavigationViewModel @@ -15,6 +17,7 @@ struct ActivityListView: View { // Data modeled for Search Feature @State var searchQuery = "" + var searchResults: [String] { guard searchQuery.isEmpty else { return curriculumVM.activityFilesCompleteList.filter { @@ -48,6 +51,8 @@ struct ActivityListView: View { ) } + // MARK: Private + private var completeActivityList: some View { ScrollViewReader { proxy in List(searchResults.enumerated().map({ $0 }), id: \.element) { index, item in diff --git a/Apps/LekaApp/Sources/Views/EducationalContent/Activities/Components/ActivityListCell.swift b/Apps/LekaApp/Sources/Views/EducationalContent/Activities/Components/ActivityListCell.swift index d183da0ad2..8c9b2cdbae 100644 --- a/Apps/LekaApp/Sources/Views/EducationalContent/Activities/Components/ActivityListCell.swift +++ b/Apps/LekaApp/Sources/Views/EducationalContent/Activities/Components/ActivityListCell.swift @@ -6,6 +6,8 @@ import DesignKit import SwiftUI struct ActivityListCell: View { + // MARK: Internal + @EnvironmentObject var metrics: UIMetrics let activity: Activity @@ -26,6 +28,8 @@ struct ActivityListCell: View { .padding(.vertical, 4) } + // MARK: Private + private var iconView: some View { Image(icon) .activityIconImageModifier(diameter: iconDiameter) diff --git a/Apps/LekaApp/Sources/Views/EducationalContent/Activities/Components/SelectedActivityInstructionsView.swift b/Apps/LekaApp/Sources/Views/EducationalContent/Activities/Components/SelectedActivityInstructionsView.swift index f322145a2a..fd2ca0c5a1 100644 --- a/Apps/LekaApp/Sources/Views/EducationalContent/Activities/Components/SelectedActivityInstructionsView.swift +++ b/Apps/LekaApp/Sources/Views/EducationalContent/Activities/Components/SelectedActivityInstructionsView.swift @@ -6,6 +6,8 @@ import DesignKit import SwiftUI struct SelectedActivityInstructionsView: View { + // MARK: Internal + @EnvironmentObject var activityVM: ActivityViewModel @EnvironmentObject var metrics: UIMetrics @@ -29,6 +31,8 @@ struct SelectedActivityInstructionsView: View { .preferredColorScheme(.light) } + // MARK: Private + private var activityDetailHeader: some View { HStack { Spacer() diff --git a/Apps/LekaApp/Sources/Views/EducationalContent/Activities_shared/GoButton.swift b/Apps/LekaApp/Sources/Views/EducationalContent/Activities_shared/GoButton.swift index 520477b2ce..f404ff033d 100644 --- a/Apps/LekaApp/Sources/Views/EducationalContent/Activities_shared/GoButton.swift +++ b/Apps/LekaApp/Sources/Views/EducationalContent/Activities_shared/GoButton.swift @@ -6,6 +6,8 @@ import DesignKit import SwiftUI struct GoButton: View { + // MARK: Internal + @EnvironmentObject var company: CompanyViewModel @EnvironmentObject var activityVM: ActivityViewModel @EnvironmentObject var navigationVM: NavigationViewModel @@ -29,26 +31,7 @@ struct GoButton: View { } } - private func goButtonAction() { - activityVM.setupGame(with: activityVM.currentActivity) - guard robotVM.robotIsConnected || robotVM.userChoseToPlayWithoutRobot else { - navigationVM.pathToGame = NavigationPath([PathsToGame.robot]) - navigationVM.showActivitiesFullScreenCover = true - return - } - guard settings.companyIsConnected else { - navigationVM.pathToGame = NavigationPath([PathsToGame.game]) - navigationVM.showActivitiesFullScreenCover = true - return - } - guard company.selectionSetIsCorrect() else { - navigationVM.pathToGame = NavigationPath([PathsToGame.user]) - navigationVM.showActivitiesFullScreenCover = true - return - } - navigationVM.pathToGame = NavigationPath([PathsToGame.game]) - navigationVM.showActivitiesFullScreenCover = true - } + // MARK: Private private var goButtonLabel: some View { ZStack { @@ -70,4 +53,25 @@ struct GoButton: View { .frame(width: 127, height: 127) .compositingGroup() } + + private func goButtonAction() { + activityVM.setupGame(with: activityVM.currentActivity) + guard robotVM.robotIsConnected || robotVM.userChoseToPlayWithoutRobot else { + navigationVM.pathToGame = NavigationPath([PathsToGame.robot]) + navigationVM.showActivitiesFullScreenCover = true + return + } + guard settings.companyIsConnected else { + navigationVM.pathToGame = NavigationPath([PathsToGame.game]) + navigationVM.showActivitiesFullScreenCover = true + return + } + guard company.selectionSetIsCorrect() else { + navigationVM.pathToGame = NavigationPath([PathsToGame.user]) + navigationVM.showActivitiesFullScreenCover = true + return + } + navigationVM.pathToGame = NavigationPath([PathsToGame.game]) + navigationVM.showActivitiesFullScreenCover = true + } } diff --git a/Apps/LekaApp/Sources/Views/EducationalContent/Activities_shared/InstructionsView.swift b/Apps/LekaApp/Sources/Views/EducationalContent/Activities_shared/InstructionsView.swift index 6437c7c747..4464c27e1a 100644 --- a/Apps/LekaApp/Sources/Views/EducationalContent/Activities_shared/InstructionsView.swift +++ b/Apps/LekaApp/Sources/Views/EducationalContent/Activities_shared/InstructionsView.swift @@ -10,6 +10,8 @@ import SwiftUI // MARK: - InstructionsView struct InstructionsView: View { + // MARK: Internal + @EnvironmentObject var activityVM: ActivityViewModel @EnvironmentObject var metrics: UIMetrics @@ -23,6 +25,8 @@ struct InstructionsView: View { } } + // MARK: Private + @ViewBuilder private var instructionsMarkdownView: some View { // Text(activityVM.getInstructions()) diff --git a/Apps/LekaApp/Sources/Views/EducationalContent/Activities_shared/MarkdownRepresentable.swift b/Apps/LekaApp/Sources/Views/EducationalContent/Activities_shared/MarkdownRepresentable.swift index b1e76a6adb..a9577cdc0d 100644 --- a/Apps/LekaApp/Sources/Views/EducationalContent/Activities_shared/MarkdownRepresentable.swift +++ b/Apps/LekaApp/Sources/Views/EducationalContent/Activities_shared/MarkdownRepresentable.swift @@ -17,24 +17,56 @@ import SwiftUI // import Down class MarkdownObservable: ObservableObject { - @Published public var textView = UITextView() - public let text: String + // MARK: Lifecycle init(text: String) { self.text = text } + + // MARK: Public + + @Published public var textView = UITextView() + public let text: String } // MARK: - MarkdownRepresentable struct MarkdownRepresentable: UIViewRepresentable { - @Binding var dynamicHeight: CGFloat - @EnvironmentObject var markdownObject: MarkdownObservable + // MARK: Lifecycle init(height: Binding) { self._dynamicHeight = height } + // MARK: Internal + + class Coordinator: NSObject { + // MARK: Lifecycle + + init(text: UITextView) { + textView = text + } + + // func textAttachmentDidLoadImage(textAttachment: AsyncImageLoad, displaySizeChanged: Bool) + // { + // if displaySizeChanged + // { + // textView.layoutManager.setNeedsLayout(forAttachment: textAttachment) + // } + // + // // always re-display, the image might have changed + // textView.layoutManager.setNeedsDisplay(forAttachment: textAttachment) + // } + + // MARK: Public + + // }, AsyncImageLoadDelegate { + public var textView: UITextView + } + + @Binding var dynamicHeight: CGFloat + @EnvironmentObject var markdownObject: MarkdownObservable + func makeCoordinator() -> Coordinator { Coordinator(text: markdownObject.textView) } @@ -79,40 +111,20 @@ struct MarkdownRepresentable: UIViewRepresentable { .height } } - - class Coordinator: NSObject { // }, AsyncImageLoadDelegate { - public var textView: UITextView - - init(text: UITextView) { - textView = text - } - - // func textAttachmentDidLoadImage(textAttachment: AsyncImageLoad, displaySizeChanged: Bool) - // { - // if displaySizeChanged - // { - // textView.layoutManager.setNeedsLayout(forAttachment: textAttachment) - // } - // - // // always re-display, the image might have changed - // textView.layoutManager.setNeedsDisplay(forAttachment: textAttachment) - // } - } } // MARK: - DownAttributedString struct DownAttributedString: View { - @ObservedObject private var markdownObject: MarkdownObservable - private var markdownString: String - - @State private var height: CGFloat = .zero + // MARK: Lifecycle init(text: String) { self.markdownString = text self.markdownObject = MarkdownObservable(text: text) } + // MARK: Internal + var body: some View { VStack(alignment: .leading) { ScrollView { @@ -123,4 +135,11 @@ struct DownAttributedString: View { } .navigationBarTitleDisplayMode(.inline) } + + // MARK: Private + + @ObservedObject private var markdownObject: MarkdownObservable + private var markdownString: String + + @State private var height: CGFloat = .zero } diff --git a/Apps/LekaApp/Sources/Views/EducationalContent/Commands/CommandListView.swift b/Apps/LekaApp/Sources/Views/EducationalContent/Commands/CommandListView.swift index 3eabb69100..567e6c457c 100644 --- a/Apps/LekaApp/Sources/Views/EducationalContent/Commands/CommandListView.swift +++ b/Apps/LekaApp/Sources/Views/EducationalContent/Commands/CommandListView.swift @@ -8,13 +8,11 @@ import SwiftUI // MARK: - CommandListView struct CommandListView: View { + // MARK: Internal + @EnvironmentObject var settings: SettingsViewModel @EnvironmentObject var navigationVM: NavigationViewModel - private let images: [String] = [ - "standard-remote", "colored-arrows", "color-remote copy", "big-joystick", "hand-remote", - ] - var body: some View { ZStack { DesignKitAsset.Colors.lekaLightBlue.swiftUIColor.ignoresSafeArea() @@ -42,6 +40,12 @@ struct CommandListView: View { .animation(.easeOut(duration: 0.4), value: navigationVM.showInfo()) .onAppear { navigationVM.sidebarVisibility = .all } } + + // MARK: Private + + private let images: [String] = [ + "standard-remote", "colored-arrows", "color-remote copy", "big-joystick", "hand-remote", + ] } // MARK: - CommandListView_Previews diff --git a/Apps/LekaApp/Sources/Views/EducationalContent/Curriculums/Components/ActivityListCell_Curriculums.swift b/Apps/LekaApp/Sources/Views/EducationalContent/Curriculums/Components/ActivityListCell_Curriculums.swift index 2e471800de..4c5de00f05 100644 --- a/Apps/LekaApp/Sources/Views/EducationalContent/Curriculums/Components/ActivityListCell_Curriculums.swift +++ b/Apps/LekaApp/Sources/Views/EducationalContent/Curriculums/Components/ActivityListCell_Curriculums.swift @@ -6,6 +6,8 @@ import DesignKit import SwiftUI struct ActivityListCell_Curriculums: View { + // MARK: Internal + @EnvironmentObject var metrics: UIMetrics let activity: Activity @@ -26,6 +28,8 @@ struct ActivityListCell_Curriculums: View { .padding(.vertical, 4) } + // MARK: Private + private var iconView: some View { Image(icon) .activityIconImageModifier(diameter: iconDiameter) diff --git a/Apps/LekaApp/Sources/Views/EducationalContent/Curriculums/Components/CurriculumPillShapedView.swift b/Apps/LekaApp/Sources/Views/EducationalContent/Curriculums/Components/CurriculumPillShapedView.swift index 685074ee75..26d3a84427 100644 --- a/Apps/LekaApp/Sources/Views/EducationalContent/Curriculums/Components/CurriculumPillShapedView.swift +++ b/Apps/LekaApp/Sources/Views/EducationalContent/Curriculums/Components/CurriculumPillShapedView.swift @@ -6,6 +6,8 @@ import DesignKit import SwiftUI struct CurriculumPillShapedView: View { + // MARK: Internal + @EnvironmentObject var metrics: UIMetrics var curriculum: Curriculum @@ -23,6 +25,8 @@ struct CurriculumPillShapedView: View { .compositingGroup() } + // MARK: Private + private var topContent: some View { Text(curriculum.fullTitle.localized()) .multilineTextAlignment(.center) diff --git a/Apps/LekaApp/Sources/Views/EducationalContent/Curriculums/CurriculumDetailsView.swift b/Apps/LekaApp/Sources/Views/EducationalContent/Curriculums/CurriculumDetailsView.swift index 526dbf6f4e..23e24a4800 100644 --- a/Apps/LekaApp/Sources/Views/EducationalContent/Curriculums/CurriculumDetailsView.swift +++ b/Apps/LekaApp/Sources/Views/EducationalContent/Curriculums/CurriculumDetailsView.swift @@ -8,20 +8,19 @@ import SwiftUI // MARK: - CurriculumDetailsView struct CurriculumDetailsView: View { + // MARK: Internal + @EnvironmentObject var curriculumVM: CurriculumViewModel @EnvironmentObject var activityVM: ActivityViewModel @EnvironmentObject var navigationVM: NavigationViewModel @EnvironmentObject var metrics: UIMetrics - private func goButtonIsDisabled() -> Bool { - !curriculumVM.currentCurriculum.activities.map({ UUID(uuidString: activityVM.getActivity($0).id) }) - .contains(curriculumVM.currentCurriculumSelectedActivityID) - } - var body: some View { curriculumDetailContent } + // MARK: Private + private var curriculumDetailContent: some View { ZStack(alignment: .top) { // NavigationBar color @@ -127,6 +126,11 @@ struct CurriculumDetailsView: View { } } } + + private func goButtonIsDisabled() -> Bool { + !curriculumVM.currentCurriculum.activities.map({ UUID(uuidString: activityVM.getActivity($0).id) }) + .contains(curriculumVM.currentCurriculumSelectedActivityID) + } } // MARK: - ContextualActivitiesDetailsView_Previews diff --git a/Apps/LekaApp/Sources/Views/EducationalContent/Curriculums/CurriculumListView.swift b/Apps/LekaApp/Sources/Views/EducationalContent/Curriculums/CurriculumListView.swift index b94d228dd2..9fd5983f45 100644 --- a/Apps/LekaApp/Sources/Views/EducationalContent/Curriculums/CurriculumListView.swift +++ b/Apps/LekaApp/Sources/Views/EducationalContent/Curriculums/CurriculumListView.swift @@ -8,12 +8,12 @@ import SwiftUI // MARK: - CurriculumListView struct CurriculumListView: View { + // MARK: Internal + @EnvironmentObject var navigationVM: NavigationViewModel @EnvironmentObject var curriculumVM: CurriculumViewModel @EnvironmentObject var metrics: UIMetrics - private let columns = Array(repeating: GridItem(), count: 3) - var body: some View { ZStack { DesignKitAsset.Colors.lekaLightBlue.swiftUIColor.ignoresSafeArea() @@ -47,6 +47,10 @@ struct CurriculumListView: View { ) } + // MARK: Private + + private let columns = Array(repeating: GridItem(), count: 3) + @ViewBuilder private func allCurriculums(category: CurriculumCategories) -> some View { let list: [Curriculum] = curriculumVM.getCurriculumsFrom(category: category) diff --git a/Apps/LekaApp/Sources/Views/Game/Components/CurrentGameInstructionView.swift b/Apps/LekaApp/Sources/Views/Game/Components/CurrentGameInstructionView.swift index 4756c60f21..2b0ab49ea1 100644 --- a/Apps/LekaApp/Sources/Views/Game/Components/CurrentGameInstructionView.swift +++ b/Apps/LekaApp/Sources/Views/Game/Components/CurrentGameInstructionView.swift @@ -8,10 +8,40 @@ import SwiftUI // MARK: - CurrentGameInstructionView struct CurrentGameInstructionView: View { + // MARK: Internal + @EnvironmentObject var activityVM: ActivityViewModel @ObservedObject var gameMetrics: GameMetrics @Environment(\.dismiss) var dismiss + var body: some View { + NavigationStack { + ZStack(alignment: .top) { + // Header color + DesignKitAsset.Colors.lekaDarkBlue.swiftUIColor.ignoresSafeArea() + + // Background Color + DesignKitAsset.Colors.lekaLightGray.swiftUIColor.padding(.top, 70) + + VStack(spacing: 0) { + activityDetailHeader + InstructionsView() + } + } + .navigationBarTitleDisplayMode(.inline) + .toolbarBackground(.hidden, for: .navigationBar) + .interactiveDismissDisabled() + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + resumeButton + } + } + } + .preferredColorScheme(.light) + } + + // MARK: Private + private var activityDetailHeader: some View { HStack { Spacer() @@ -38,32 +68,6 @@ struct CurrentGameInstructionView: View { .foregroundColor(.white) }) } - - var body: some View { - NavigationStack { - ZStack(alignment: .top) { - // Header color - DesignKitAsset.Colors.lekaDarkBlue.swiftUIColor.ignoresSafeArea() - - // Background Color - DesignKitAsset.Colors.lekaLightGray.swiftUIColor.padding(.top, 70) - - VStack(spacing: 0) { - activityDetailHeader - InstructionsView() - } - } - .navigationBarTitleDisplayMode(.inline) - .toolbarBackground(.hidden, for: .navigationBar) - .interactiveDismissDisabled() - .toolbar { - ToolbarItem(placement: .navigationBarTrailing) { - resumeButton - } - } - } - .preferredColorScheme(.light) - } } // MARK: - CurrentGameInstructionView_Previews diff --git a/Apps/LekaApp/Sources/Views/Game/Components/LottieView.swift b/Apps/LekaApp/Sources/Views/Game/Components/LottieView.swift index efb2a3452a..197a21a1d9 100644 --- a/Apps/LekaApp/Sources/Views/Game/Components/LottieView.swift +++ b/Apps/LekaApp/Sources/Views/Game/Components/LottieView.swift @@ -6,17 +6,7 @@ import Lottie import SwiftUI struct LottieView: UIViewRepresentable { - typealias UIViewType = UIView - - func makeCoordinator() -> Coordinator { - Coordinator(self) - } - - var name: String! - var speed: CGFloat - var reverse: Bool - var action: () -> Void - @Binding var play: Bool + // MARK: Lifecycle public init( name: String, @@ -32,15 +22,33 @@ struct LottieView: UIViewRepresentable { self._play = play } - var animationView = LottieAnimationView() + // MARK: Internal + + typealias UIViewType = UIView class Coordinator: NSObject { - var parent: LottieView + // MARK: Lifecycle init(_ animationView: LottieView) { self.parent = animationView super.init() } + + // MARK: Internal + + var parent: LottieView + } + + var name: String! + var speed: CGFloat + var reverse: Bool + var action: () -> Void + @Binding var play: Bool + + var animationView = LottieAnimationView() + + func makeCoordinator() -> Coordinator { + Coordinator(self) } func makeUIView(context: UIViewRepresentableContext) -> UIView { diff --git a/Apps/LekaApp/Sources/Views/Game/Components/PlayZone.swift b/Apps/LekaApp/Sources/Views/Game/Components/PlayZone.swift index c01a3cf3d8..79260e54c0 100644 --- a/Apps/LekaApp/Sources/Views/Game/Components/PlayZone.swift +++ b/Apps/LekaApp/Sources/Views/Game/Components/PlayZone.swift @@ -8,47 +8,11 @@ import SwiftUI // MARK: - PlayZone struct PlayZone: View { + // MARK: Internal + @ObservedObject var gameMetrics: GameMetrics @EnvironmentObject var activityVM: ActivityViewModel - // Grid configuration - private var columns: [GridItem] { - let number = activityVM.currentActivity.numberOfImages - if number % 3 == 0 { - guard activityVM.currentActivityType != "touch_to_select" else { - return [ - GridItem(.fixed(gameMetrics.playGridBtnCellSize), alignment: .center), - GridItem(.fixed(gameMetrics.playGridBtnCellSize), alignment: .center), - GridItem(.fixed(gameMetrics.playGridBtnCellSize), alignment: .center), - ] - } - return [ - GridItem(.fixed(gameMetrics.playGridBtnCellSize), alignment: .center), - GridItem(.fixed(gameMetrics.playGridBtnCellSize), alignment: .center), - ] - } else if number == 2 { - return [ - GridItem(.fixed(gameMetrics.playGridBtnCellSize), alignment: .center), - GridItem(.fixed(gameMetrics.playGridBtnCellSize), alignment: .center), - ] - } else if number == 4 { - guard activityVM.currentActivityType != "touch_to_select" else { - return [ - GridItem(.fixed(gameMetrics.playGridBtnCellSize), alignment: .center), - GridItem(.fixed(gameMetrics.playGridBtnCellSize), alignment: .center), - GridItem(.fixed(gameMetrics.playGridBtnCellSize), alignment: .center), - GridItem(.fixed(gameMetrics.playGridBtnCellSize), alignment: .center), - ] - } - return [ - GridItem(.fixed(gameMetrics.playGridBtnCellSize), alignment: .center), - GridItem(.fixed(gameMetrics.playGridBtnCellSize), alignment: .center), - ] - } else { - return [GridItem(.fixed(gameMetrics.playGridBtnCellSize), alignment: .center)] - } - } - var body: some View { HStack { Spacer() @@ -107,6 +71,46 @@ struct PlayZone: View { } ) } + + // MARK: Private + + // Grid configuration + private var columns: [GridItem] { + let number = activityVM.currentActivity.numberOfImages + if number % 3 == 0 { + guard activityVM.currentActivityType != "touch_to_select" else { + return [ + GridItem(.fixed(gameMetrics.playGridBtnCellSize), alignment: .center), + GridItem(.fixed(gameMetrics.playGridBtnCellSize), alignment: .center), + GridItem(.fixed(gameMetrics.playGridBtnCellSize), alignment: .center), + ] + } + return [ + GridItem(.fixed(gameMetrics.playGridBtnCellSize), alignment: .center), + GridItem(.fixed(gameMetrics.playGridBtnCellSize), alignment: .center), + ] + } else if number == 2 { + return [ + GridItem(.fixed(gameMetrics.playGridBtnCellSize), alignment: .center), + GridItem(.fixed(gameMetrics.playGridBtnCellSize), alignment: .center), + ] + } else if number == 4 { + guard activityVM.currentActivityType != "touch_to_select" else { + return [ + GridItem(.fixed(gameMetrics.playGridBtnCellSize), alignment: .center), + GridItem(.fixed(gameMetrics.playGridBtnCellSize), alignment: .center), + GridItem(.fixed(gameMetrics.playGridBtnCellSize), alignment: .center), + GridItem(.fixed(gameMetrics.playGridBtnCellSize), alignment: .center), + ] + } + return [ + GridItem(.fixed(gameMetrics.playGridBtnCellSize), alignment: .center), + GridItem(.fixed(gameMetrics.playGridBtnCellSize), alignment: .center), + ] + } else { + return [GridItem(.fixed(gameMetrics.playGridBtnCellSize), alignment: .center)] + } + } } // MARK: - PlayZone_Previews diff --git a/Apps/LekaApp/Sources/Views/Game/Components/ProgressBarView.swift b/Apps/LekaApp/Sources/Views/Game/Components/ProgressBarView.swift index 94d752894d..3f9f4b962b 100644 --- a/Apps/LekaApp/Sources/Views/Game/Components/ProgressBarView.swift +++ b/Apps/LekaApp/Sources/Views/Game/Components/ProgressBarView.swift @@ -11,18 +11,6 @@ struct ProgressBarView: View { @EnvironmentObject var activityVM: ActivityViewModel @ObservedObject var gameMetrics: GameMetrics - @ViewBuilder - func stepMarker(_ color: Color) -> some View { - Circle() - .fill( - color, - strokeBorder: .white, - lineWidth: gameMetrics.stepMarkerBorderWidth - ) - .background(Circle().fill(.white)) - .padding(gameMetrics.stepMarkerPadding) - } - var body: some View { Capsule() .fill(DesignKitAsset.Colors.progressBar.swiftUIColor) @@ -39,6 +27,18 @@ struct ProgressBarView: View { } ) } + + @ViewBuilder + func stepMarker(_ color: Color) -> some View { + Circle() + .fill( + color, + strokeBorder: .white, + lineWidth: gameMetrics.stepMarkerBorderWidth + ) + .background(Circle().fill(.white)) + .padding(gameMetrics.stepMarkerPadding) + } } // MARK: - ProgressBarView_Previews diff --git a/Apps/LekaApp/Sources/Views/Game/GameView.swift b/Apps/LekaApp/Sources/Views/Game/GameView.swift index 5140fea243..3c61cc5b6c 100644 --- a/Apps/LekaApp/Sources/Views/Game/GameView.swift +++ b/Apps/LekaApp/Sources/Views/Game/GameView.swift @@ -7,6 +7,8 @@ import Lottie import SwiftUI struct GameView: View { + // MARK: Internal + @StateObject var gameMetrics = GameMetrics() @EnvironmentObject var navigationVM: NavigationViewModel @EnvironmentObject var robotVM: RobotViewModel @@ -14,31 +16,95 @@ struct GameView: View { @EnvironmentObject var settings: SettingsViewModel @Environment(\.dismiss) var dismiss - @State private var textOffset: CGFloat = -100 - @State private var textOpacity: Double = 0 - @State private var offsetGameOverBtn: CGFloat = 0 - @State private var offsetReplayBtn: CGFloat = 0 - @State private var initialBtnOffsets: [CGFloat] = [] - - @State private var showInstructionModal: Bool = false - - private var infoButton: some View { - Button { - showInstructionModal.toggle() - } label: { - Image(systemName: "info.circle") - .foregroundColor(DesignKitAsset.Colors.lekaDarkBlue.swiftUIColor) - } - .opacity(showInstructionModal ? 0 : 1) - } + var body: some View { + DesignKitAsset.Colors.lekaLightBlue.swiftUIColor + .edgesIgnoringSafeArea(.all) + .navigationBarTitleDisplayMode(.inline) + .navigationBarBackButtonHidden() + .tint(DesignKitAsset.Colors.lekaDarkBlue.swiftUIColor) + .overlay( + VStack(spacing: 0) { + VStack(spacing: gameMetrics.headerSpacing) { + ProgressBarView(gameMetrics: gameMetrics) + InstructionButton(gameMetrics: gameMetrics) + } + .frame(maxHeight: gameMetrics.headerTotalHeight) + .padding(.top, gameMetrics.headerPadding) - private var successScreen: some View { - LottieView( - name: "motivator", speed: 0.5, - action: { activityVM.hideMotivator() }, - play: $activityVM.showMotivator - ) - .scaleEffect(gameMetrics.motivatorScale, anchor: .center) + VStack { + Spacer() + PlayZone(gameMetrics: gameMetrics) + .layoutPriority(1) + Spacer() + } + } + ) + .toolbar { + ToolbarItem(placement: .navigationBarLeading) { + Button( + action: { + activityVM.resetActivity() + navigationVM.showActivitiesFullScreenCover = false + robotVM.userChoseToPlayWithoutRobot = false + // show alert to inform about losing the progress?? + }, + label: { + Image(systemName: "chevron.left") + .padding(.horizontal) + } + ) + .disabled(activityVM.tapIsDisabled) + } + ToolbarItem(placement: .principal) { + HStack(spacing: 4) { + Text(activityVM.currentActivityTitle) + if settings.companyIsConnected, settings.exploratoryModeIsOn { + Image(systemName: "binoculars.fill") + } + } + .font(gameMetrics.semi17) + .foregroundColor(DesignKitAsset.Colors.lekaDarkBlue.swiftUIColor) + } + ToolbarItem(placement: .navigationBarTrailing) { + infoButton + } + } + .sheet(isPresented: $showInstructionModal) { + CurrentGameInstructionView(gameMetrics: gameMetrics) + } + .overlay( + ZStack { + if activityVM.showMotivator { + successScreen + } else if activityVM.showEndAnimation { + cheerScreen + } else { + // Not an EmptyView() to avoid breaking the opacity animation + Rectangle() + .fill(Color.clear) + } + } + .background(content: { + Group { + if activityVM.showBlurryBG { + Rectangle() + .fill(.regularMaterial) + .transition(.opacity) + } + } + .animation(.default, value: activityVM.showBlurryBG) + }) + .edgesIgnoringSafeArea(.all) + ) + .background( + GeometryReader { reader in + Color.clear + .onAppear { + offsetGameOverBtn = -reader.frame(in: .local).width / 2 + offsetReplayBtn = reader.frame(in: .local).width / 2 + initialBtnOffsets = [offsetGameOverBtn, offsetReplayBtn] + } + }) } @ViewBuilder @@ -109,6 +175,35 @@ struct GameView: View { } } + // MARK: Private + + @State private var textOffset: CGFloat = -100 + @State private var textOpacity: Double = 0 + @State private var offsetGameOverBtn: CGFloat = 0 + @State private var offsetReplayBtn: CGFloat = 0 + @State private var initialBtnOffsets: [CGFloat] = [] + + @State private var showInstructionModal: Bool = false + + private var infoButton: some View { + Button { + showInstructionModal.toggle() + } label: { + Image(systemName: "info.circle") + .foregroundColor(DesignKitAsset.Colors.lekaDarkBlue.swiftUIColor) + } + .opacity(showInstructionModal ? 0 : 1) + } + + private var successScreen: some View { + LottieView( + name: "motivator", speed: 0.5, + action: { activityVM.hideMotivator() }, + play: $activityVM.showMotivator + ) + .scaleEffect(gameMetrics.motivatorScale, anchor: .center) + } + private var cheerScreen: some View { ZStack { LottieView( @@ -164,95 +259,4 @@ struct GameView: View { .padding(.vertical, gameMetrics.endAnimBtnPadding) } } - - var body: some View { - DesignKitAsset.Colors.lekaLightBlue.swiftUIColor - .edgesIgnoringSafeArea(.all) - .navigationBarTitleDisplayMode(.inline) - .navigationBarBackButtonHidden() - .tint(DesignKitAsset.Colors.lekaDarkBlue.swiftUIColor) - .overlay( - VStack(spacing: 0) { - VStack(spacing: gameMetrics.headerSpacing) { - ProgressBarView(gameMetrics: gameMetrics) - InstructionButton(gameMetrics: gameMetrics) - } - .frame(maxHeight: gameMetrics.headerTotalHeight) - .padding(.top, gameMetrics.headerPadding) - - VStack { - Spacer() - PlayZone(gameMetrics: gameMetrics) - .layoutPriority(1) - Spacer() - } - } - ) - .toolbar { - ToolbarItem(placement: .navigationBarLeading) { - Button( - action: { - activityVM.resetActivity() - navigationVM.showActivitiesFullScreenCover = false - robotVM.userChoseToPlayWithoutRobot = false - // show alert to inform about losing the progress?? - }, - label: { - Image(systemName: "chevron.left") - .padding(.horizontal) - } - ) - .disabled(activityVM.tapIsDisabled) - } - ToolbarItem(placement: .principal) { - HStack(spacing: 4) { - Text(activityVM.currentActivityTitle) - if settings.companyIsConnected, settings.exploratoryModeIsOn { - Image(systemName: "binoculars.fill") - } - } - .font(gameMetrics.semi17) - .foregroundColor(DesignKitAsset.Colors.lekaDarkBlue.swiftUIColor) - } - ToolbarItem(placement: .navigationBarTrailing) { - infoButton - } - } - .sheet(isPresented: $showInstructionModal) { - CurrentGameInstructionView(gameMetrics: gameMetrics) - } - .overlay( - ZStack { - if activityVM.showMotivator { - successScreen - } else if activityVM.showEndAnimation { - cheerScreen - } else { - // Not an EmptyView() to avoid breaking the opacity animation - Rectangle() - .fill(Color.clear) - } - } - .background(content: { - Group { - if activityVM.showBlurryBG { - Rectangle() - .fill(.regularMaterial) - .transition(.opacity) - } - } - .animation(.default, value: activityVM.showBlurryBG) - }) - .edgesIgnoringSafeArea(.all) - ) - .background( - GeometryReader { reader in - Color.clear - .onAppear { - offsetGameOverBtn = -reader.frame(in: .local).width / 2 - offsetReplayBtn = reader.frame(in: .local).width / 2 - initialBtnOffsets = [offsetGameOverBtn, offsetReplayBtn] - } - }) - } } diff --git a/Apps/LekaApp/Sources/Views/GlobalComponentViews/InformationTiles/InfoTile.swift b/Apps/LekaApp/Sources/Views/GlobalComponentViews/InformationTiles/InfoTile.swift index 95d5b8d309..d77544fd3b 100644 --- a/Apps/LekaApp/Sources/Views/GlobalComponentViews/InformationTiles/InfoTile.swift +++ b/Apps/LekaApp/Sources/Views/GlobalComponentViews/InformationTiles/InfoTile.swift @@ -8,16 +8,14 @@ import SwiftUI // MARK: - InfoTile struct InfoTile: View { + // MARK: Internal + @EnvironmentObject var settings: SettingsViewModel @EnvironmentObject var navigationVM: NavigationViewModel @EnvironmentObject var viewRouter: ViewRouter @EnvironmentObject var metrics: UIMetrics let data: TileData - private var headerColor: Color { - data == .discovery - ? DesignKitAsset.Colors.lekaOrange.swiftUIColor : DesignKitAsset.Colors.lekaDarkBlue.swiftUIColor - } var body: some View { VStack(spacing: 0) { @@ -30,6 +28,13 @@ struct InfoTile: View { .clipShape(RoundedRectangle(cornerRadius: metrics.tilesRadius, style: .continuous)) } + // MARK: Private + + private var headerColor: Color { + data == .discovery + ? DesignKitAsset.Colors.lekaOrange.swiftUIColor : DesignKitAsset.Colors.lekaDarkBlue.swiftUIColor + } + private var tileHeader: some View { ZStack { Text(data.content.title!) diff --git a/Apps/LekaApp/Sources/Views/GlobalComponentViews/LekaTextField.swift b/Apps/LekaApp/Sources/Views/GlobalComponentViews/LekaTextField.swift index 53ab7805e5..0626fa37b5 100644 --- a/Apps/LekaApp/Sources/Views/GlobalComponentViews/LekaTextField.swift +++ b/Apps/LekaApp/Sources/Views/GlobalComponentViews/LekaTextField.swift @@ -8,6 +8,8 @@ import SwiftUI // MARK: - LekaTextField struct LekaTextField: View { + // MARK: Internal + @EnvironmentObject var metrics: UIMetrics var label: String @@ -28,6 +30,8 @@ struct LekaTextField: View { } } + // MARK: Private + @ViewBuilder private var entryField: some View { TextField("", text: $entry) { isEditingNow in diff --git a/Apps/LekaApp/Sources/Views/Identification/SignUpPath/SignupFinalStep.swift b/Apps/LekaApp/Sources/Views/Identification/SignUpPath/SignupFinalStep.swift index 8291b73631..1799301ad8 100644 --- a/Apps/LekaApp/Sources/Views/Identification/SignUpPath/SignupFinalStep.swift +++ b/Apps/LekaApp/Sources/Views/Identification/SignUpPath/SignupFinalStep.swift @@ -6,11 +6,11 @@ import DesignKit import SwiftUI struct SignupFinalStep: View { + // MARK: Internal + @EnvironmentObject var metrics: UIMetrics @EnvironmentObject var viewRouter: ViewRouter - private let data: TileData = .signupFinalStep - var body: some View { ZStack { DesignKitAsset.Colors.lekaLightBlue.swiftUIColor.ignoresSafeArea() @@ -24,6 +24,10 @@ struct SignupFinalStep: View { } } + // MARK: Private + + private let data: TileData = .signupFinalStep + private var tile: some View { HStack(alignment: .center, spacing: 0) { VStack(spacing: 0) { diff --git a/Apps/LekaApp/Sources/Views/Identification/SignUpPath/SignupStep1.swift b/Apps/LekaApp/Sources/Views/Identification/SignUpPath/SignupStep1.swift index 767b0f298d..1ce77ba74c 100644 --- a/Apps/LekaApp/Sources/Views/Identification/SignUpPath/SignupStep1.swift +++ b/Apps/LekaApp/Sources/Views/Identification/SignUpPath/SignupStep1.swift @@ -6,10 +6,9 @@ import DesignKit import SwiftUI struct SignupStep1: View { - @EnvironmentObject var metrics: UIMetrics + // MARK: Internal - private let data: TileData = .signupBravo - @State private var navigateToSignup2: Bool = false + @EnvironmentObject var metrics: UIMetrics var body: some View { ZStack { @@ -27,6 +26,11 @@ struct SignupStep1: View { } } + // MARK: Private + + private let data: TileData = .signupBravo + @State private var navigateToSignup2: Bool = false + private var tile: some View { HStack(alignment: .center, spacing: 0) { VStack(spacing: 0) { diff --git a/Apps/LekaApp/Sources/Views/Identification/SignUpPath/SignupStep2.swift b/Apps/LekaApp/Sources/Views/Identification/SignUpPath/SignupStep2.swift index 0b23414b4c..abc3ee4320 100644 --- a/Apps/LekaApp/Sources/Views/Identification/SignUpPath/SignupStep2.swift +++ b/Apps/LekaApp/Sources/Views/Identification/SignUpPath/SignupStep2.swift @@ -6,10 +6,9 @@ import DesignKit import SwiftUI struct SignupStep2: View { - @EnvironmentObject var metrics: UIMetrics + // MARK: Internal - private let data: TileData = .signupStep1 - @State private var navigateToTeacherCreation: Bool = false + @EnvironmentObject var metrics: UIMetrics var body: some View { ZStack { @@ -27,6 +26,11 @@ struct SignupStep2: View { } } + // MARK: Private + + private let data: TileData = .signupStep1 + @State private var navigateToTeacherCreation: Bool = false + private var tile: some View { HStack(alignment: .center, spacing: 0) { VStack(spacing: 0) { diff --git a/Apps/LekaApp/Sources/Views/Identification/SignUpPath/SignupStep3.swift b/Apps/LekaApp/Sources/Views/Identification/SignUpPath/SignupStep3.swift index db77c2570b..1d3430c63d 100644 --- a/Apps/LekaApp/Sources/Views/Identification/SignUpPath/SignupStep3.swift +++ b/Apps/LekaApp/Sources/Views/Identification/SignUpPath/SignupStep3.swift @@ -6,12 +6,11 @@ import DesignKit import SwiftUI struct SignupStep3: View { + // MARK: Internal + @EnvironmentObject var company: CompanyViewModel @EnvironmentObject var metrics: UIMetrics - private let data: TileData = .signupStep2 - @State private var navigateToUserCreation: Bool = false - var body: some View { ZStack { DesignKitAsset.Colors.lekaLightBlue.swiftUIColor.ignoresSafeArea() @@ -28,6 +27,11 @@ struct SignupStep3: View { } } + // MARK: Private + + private let data: TileData = .signupStep2 + @State private var navigateToUserCreation: Bool = false + private var tile: some View { HStack(alignment: .center, spacing: 0) { VStack(spacing: 0) { diff --git a/Apps/LekaApp/Sources/Views/Identification/WelcomeViews/LoginView.swift b/Apps/LekaApp/Sources/Views/Identification/WelcomeViews/LoginView.swift index f24a48ac60..70dcfafd7f 100644 --- a/Apps/LekaApp/Sources/Views/Identification/WelcomeViews/LoginView.swift +++ b/Apps/LekaApp/Sources/Views/Identification/WelcomeViews/LoginView.swift @@ -8,44 +8,13 @@ import SwiftUI // MARK: - LoginView struct LoginView: View { + // MARK: Internal + @EnvironmentObject var company: CompanyViewModel @EnvironmentObject var settings: SettingsViewModel @EnvironmentObject var metrics: UIMetrics @FocusState var focusedField: FormField? - @State private var mail: String = "" - @State private var password: String = "" - @State private var isEditing = false - - @State private var navigateToTeacherSelector: Bool = false - - // Make sure you have set up Associated Domains for your app and AutoFill Passwords - // is enabled in Settings in order to get the strong password proposals etc... - // the same applies for both login/signup - // re-enable autofill modifiers in LekaTextField when OK (textContentType) - - func connectIsDisabled() -> Bool { - !mail.isValidEmail() || mail.isEmpty || password.isEmpty - } - - // TESTS ***************************************************************** - @State private var credentialsAreCorrect: Bool = true - - private func submitForm() { - if mail == LekaCompany().lekaCompany.mail { - if password != LekaCompany().lekaCompany.password { - credentialsAreCorrect = false - } else { - credentialsAreCorrect = true - settings.companyIsConnected = true - company.currentCompany = LekaCompany().lekaCompany - settings.companyIsLoggingIn = true - navigateToTeacherSelector.toggle() - } - } else { - credentialsAreCorrect = false - } - } // TESTS ***************************************************************** @@ -73,6 +42,26 @@ struct LoginView: View { } } + // Make sure you have set up Associated Domains for your app and AutoFill Passwords + // is enabled in Settings in order to get the strong password proposals etc... + // the same applies for both login/signup + // re-enable autofill modifiers in LekaTextField when OK (textContentType) + + func connectIsDisabled() -> Bool { + !mail.isValidEmail() || mail.isEmpty || password.isEmpty + } + + // MARK: Private + + @State private var mail: String = "" + @State private var password: String = "" + @State private var isEditing = false + + @State private var navigateToTeacherSelector: Bool = false + + // TESTS ***************************************************************** + @State private var credentialsAreCorrect: Bool = true + private var title: some View { Text("Se connecter") .textCase(.uppercase) @@ -162,6 +151,22 @@ struct LoginView: View { } } } + + private func submitForm() { + if mail == LekaCompany().lekaCompany.mail { + if password != LekaCompany().lekaCompany.password { + credentialsAreCorrect = false + } else { + credentialsAreCorrect = true + settings.companyIsConnected = true + company.currentCompany = LekaCompany().lekaCompany + settings.companyIsLoggingIn = true + navigateToTeacherSelector.toggle() + } + } else { + credentialsAreCorrect = false + } + } } // MARK: - LoginView_Previews diff --git a/Apps/LekaApp/Sources/Views/Identification/WelcomeViews/SignupView.swift b/Apps/LekaApp/Sources/Views/Identification/WelcomeViews/SignupView.swift index cc2dac61fd..744298b00d 100644 --- a/Apps/LekaApp/Sources/Views/Identification/WelcomeViews/SignupView.swift +++ b/Apps/LekaApp/Sources/Views/Identification/WelcomeViews/SignupView.swift @@ -8,43 +8,13 @@ import SwiftUI // MARK: - SignupView struct SignupView: View { + // MARK: Internal + @EnvironmentObject var company: CompanyViewModel @EnvironmentObject var settings: SettingsViewModel @EnvironmentObject var metrics: UIMetrics @FocusState var focusedField: FormField? - @State private var isEditing = false - @State private var mail: String = "" - @State private var password: String = "" - @State private var confirm: String = "" - @State private var accountAlreadyExists: Bool = false - @State private var navigateToSignup1: Bool = false - - func connectIsDisabled() -> Bool { - !mail.isValidEmail() || !passwordsMatch() || mail.isEmpty || password.isEmpty || confirm.isEmpty - || accountAlreadyExists - } - - func passwordsMatch() -> Bool { - password == confirm - } - - func checkAccountAvailability() { - accountAlreadyExists = (mail == "test@leka.io") - } - - private func submitForm() { - checkAccountAvailability() - if accountAlreadyExists { - password = "" - confirm = "" - } else { - company.currentCompany.mail = mail - company.currentCompany.password = password - settings.companyIsConnected = true - navigateToSignup1.toggle() - } - } var body: some View { ZStack(alignment: .center) { @@ -68,6 +38,28 @@ struct SignupView: View { } } + func connectIsDisabled() -> Bool { + !mail.isValidEmail() || !passwordsMatch() || mail.isEmpty || password.isEmpty || confirm.isEmpty + || accountAlreadyExists + } + + func passwordsMatch() -> Bool { + password == confirm + } + + func checkAccountAvailability() { + accountAlreadyExists = (mail == "test@leka.io") + } + + // MARK: Private + + @State private var isEditing = false + @State private var mail: String = "" + @State private var password: String = "" + @State private var confirm: String = "" + @State private var accountAlreadyExists: Bool = false + @State private var navigateToSignup1: Bool = false + @ViewBuilder private var mailTextField: some View { var mailTitle: String { @@ -176,6 +168,19 @@ struct SignupView: View { .buttonStyle(.borderedProminent) .tint(DesignKitAsset.Colors.lekaDarkBlue.swiftUIColor) } + + private func submitForm() { + checkAccountAvailability() + if accountAlreadyExists { + password = "" + confirm = "" + } else { + company.currentCompany.mail = mail + company.currentCompany.password = password + settings.companyIsConnected = true + navigateToSignup1.toggle() + } + } } // MARK: - SignupView_Previews diff --git a/Apps/LekaApp/Sources/Views/Identification/WelcomeViews/WelcomeView.swift b/Apps/LekaApp/Sources/Views/Identification/WelcomeViews/WelcomeView.swift index 92d2db050a..54f57b39ec 100644 --- a/Apps/LekaApp/Sources/Views/Identification/WelcomeViews/WelcomeView.swift +++ b/Apps/LekaApp/Sources/Views/Identification/WelcomeViews/WelcomeView.swift @@ -8,6 +8,8 @@ import SwiftUI // MARK: - WelcomeView struct WelcomeView: View { + // MARK: Internal + @EnvironmentObject var company: CompanyViewModel @EnvironmentObject var viewRouter: ViewRouter @EnvironmentObject var metrics: UIMetrics @@ -41,6 +43,8 @@ struct WelcomeView: View { } } + // MARK: Private + private var logoLeka: some View { Image( DesignKitAsset.Assets.lekaLogo.name, diff --git a/Apps/LekaApp/Sources/Views/Profiles/ProfileEditorView.swift b/Apps/LekaApp/Sources/Views/Profiles/ProfileEditorView.swift index fddb16a94d..443fc4336e 100644 --- a/Apps/LekaApp/Sources/Views/Profiles/ProfileEditorView.swift +++ b/Apps/LekaApp/Sources/Views/Profiles/ProfileEditorView.swift @@ -8,6 +8,8 @@ import SwiftUI // MARK: - ProfileEditorView struct ProfileEditorView: View { + // MARK: Internal + @EnvironmentObject var company: CompanyViewModel @EnvironmentObject var settings: SettingsViewModel @EnvironmentObject var metrics: UIMetrics @@ -40,6 +42,8 @@ struct ProfileEditorView: View { } } + // MARK: Private + // Toolbar private var navigationTitle: some View { HStack(spacing: 4) { diff --git a/Apps/LekaApp/Sources/Views/Profiles/ProfilesComponents/AvatarPicker/Pickers/AvatarPicker_Teachers.swift b/Apps/LekaApp/Sources/Views/Profiles/ProfilesComponents/AvatarPicker/Pickers/AvatarPicker_Teachers.swift index 68b076b8c6..e58ea72642 100644 --- a/Apps/LekaApp/Sources/Views/Profiles/ProfilesComponents/AvatarPicker/Pickers/AvatarPicker_Teachers.swift +++ b/Apps/LekaApp/Sources/Views/Profiles/ProfilesComponents/AvatarPicker/Pickers/AvatarPicker_Teachers.swift @@ -5,12 +5,12 @@ import SwiftUI struct AvatarPicker_Teachers: View { + // MARK: Internal + @EnvironmentObject var company: CompanyViewModel @EnvironmentObject var metrics: UIMetrics @EnvironmentObject var navigationVM: NavigationViewModel - @State private var selected: String = "" - var body: some View { ZStack { Color.white.edgesIgnoringSafeArea(.top) @@ -37,4 +37,8 @@ struct AvatarPicker_Teachers: View { .toolbarBackground(navigationVM.showProfileEditor ? .visible : .automatic, for: .navigationBar) .preferredColorScheme(.light) } + + // MARK: Private + + @State private var selected: String = "" } diff --git a/Apps/LekaApp/Sources/Views/Profiles/ProfilesComponents/AvatarPicker/Pickers/AvatarPicker_Users.swift b/Apps/LekaApp/Sources/Views/Profiles/ProfilesComponents/AvatarPicker/Pickers/AvatarPicker_Users.swift index 6d8537e279..8e4c5a9517 100644 --- a/Apps/LekaApp/Sources/Views/Profiles/ProfilesComponents/AvatarPicker/Pickers/AvatarPicker_Users.swift +++ b/Apps/LekaApp/Sources/Views/Profiles/ProfilesComponents/AvatarPicker/Pickers/AvatarPicker_Users.swift @@ -5,12 +5,12 @@ import SwiftUI struct AvatarPicker_Users: View { + // MARK: Internal + @EnvironmentObject var company: CompanyViewModel @EnvironmentObject var metrics: UIMetrics @EnvironmentObject var navigationVM: NavigationViewModel - @State private var selected: String = "" - var body: some View { ZStack { Color.white.edgesIgnoringSafeArea(.top) @@ -37,4 +37,8 @@ struct AvatarPicker_Users: View { .toolbarBackground(navigationVM.showProfileEditor ? .visible : .automatic, for: .navigationBar) .preferredColorScheme(.light) } + + // MARK: Private + + @State private var selected: String = "" } diff --git a/Apps/LekaApp/Sources/Views/Profiles/ProfilesComponents/AvatarPicker/Pickers/Components/AvatarPickerStore.swift b/Apps/LekaApp/Sources/Views/Profiles/ProfilesComponents/AvatarPicker/Pickers/Components/AvatarPickerStore.swift index 2df76cd28d..f829f38a64 100644 --- a/Apps/LekaApp/Sources/Views/Profiles/ProfilesComponents/AvatarPicker/Pickers/Components/AvatarPickerStore.swift +++ b/Apps/LekaApp/Sources/Views/Profiles/ProfilesComponents/AvatarPicker/Pickers/Components/AvatarPickerStore.swift @@ -6,6 +6,8 @@ import DesignKit import SwiftUI struct AvatarPickerStore: View { + // MARK: Internal + @EnvironmentObject var metrics: UIMetrics @Binding var selected: String @@ -19,6 +21,8 @@ struct AvatarPickerStore: View { } } + // MARK: Private + private func makeAvatarCategoryRow(category: AvatarCategory) -> some View { VStack(alignment: .leading, spacing: 10) { Text(category.category) diff --git a/Apps/LekaApp/Sources/Views/Profiles/ProfilesComponents/CreateProfiles/Components/JobPickerTrigger.swift b/Apps/LekaApp/Sources/Views/Profiles/ProfilesComponents/CreateProfiles/Components/JobPickerTrigger.swift index f1ac6b694c..ab982f291c 100644 --- a/Apps/LekaApp/Sources/Views/Profiles/ProfilesComponents/CreateProfiles/Components/JobPickerTrigger.swift +++ b/Apps/LekaApp/Sources/Views/Profiles/ProfilesComponents/CreateProfiles/Components/JobPickerTrigger.swift @@ -6,6 +6,8 @@ import DesignKit import SwiftUI struct JobPickerTrigger: View { + // MARK: Internal + @EnvironmentObject var company: CompanyViewModel @EnvironmentObject var metrics: UIMetrics @@ -33,6 +35,8 @@ struct JobPickerTrigger: View { .frame(width: 435) } + // MARK: Private + private var buttonLabel: some View { HStack(spacing: 0) { Text("Sélectionnez") diff --git a/Apps/LekaApp/Sources/Views/Profiles/ProfilesComponents/CreateProfiles/CreateTeacherProfileView.swift b/Apps/LekaApp/Sources/Views/Profiles/ProfilesComponents/CreateProfiles/CreateTeacherProfileView.swift index 0069c0d223..8c6237e0ce 100644 --- a/Apps/LekaApp/Sources/Views/Profiles/ProfilesComponents/CreateProfiles/CreateTeacherProfileView.swift +++ b/Apps/LekaApp/Sources/Views/Profiles/ProfilesComponents/CreateProfiles/CreateTeacherProfileView.swift @@ -8,6 +8,8 @@ import SwiftUI // MARK: - CreateTeacherProfileView struct CreateTeacherProfileView: View { + // MARK: Internal + @EnvironmentObject var company: CompanyViewModel @EnvironmentObject var settings: SettingsViewModel @EnvironmentObject var viewRouter: ViewRouter @@ -15,13 +17,6 @@ struct CreateTeacherProfileView: View { @EnvironmentObject var navigationVM: NavigationViewModel @Environment(\.dismiss) var dismiss - @FocusState private var focusedField: FormField? - @State private var isEditing = false - @State private var showDeleteConfirmation: Bool = false - @State private var navigateToSignup3: Bool = false - @State private var navigateToJobPicker: Bool = false - @State private var navigateToAvatarPicker: Bool = false - var body: some View { ZStack { Color.white.edgesIgnoringSafeArea(.top) @@ -69,6 +64,40 @@ struct CreateTeacherProfileView: View { .preferredColorScheme(.light) } + @ViewBuilder + var validateButton: some View { + if viewRouter.currentPage == .welcome { + EmptyView() + } else { + Button( + action: { + company.saveProfileChanges(.teacher) + if settings.companyIsLoggingIn { + company.assignCurrentProfiles() + viewRouter.currentPage = .home + settings.companyIsLoggingIn = false + } else { + dismiss() + } + hideKeyboard() + }, + label: { + validateButtonLabel + } + ) + .disabled(company.bufferTeacher.name.isEmpty) + } + } + + // MARK: Private + + @FocusState private var focusedField: FormField? + @State private var isEditing = false + @State private var showDeleteConfirmation: Bool = false + @State private var navigateToSignup3: Bool = false + @State private var navigateToJobPicker: Bool = false + @State private var navigateToAvatarPicker: Bool = false + private var nameField: some View { LekaTextField( label: "Nom d'accompagnant", entry: $company.bufferTeacher.name, isEditing: $isEditing, type: .name, @@ -128,31 +157,6 @@ struct CreateTeacherProfileView: View { .foregroundColor(DesignKitAsset.Colors.lekaDarkBlue.swiftUIColor) } - @ViewBuilder - var validateButton: some View { - if viewRouter.currentPage == .welcome { - EmptyView() - } else { - Button( - action: { - company.saveProfileChanges(.teacher) - if settings.companyIsLoggingIn { - company.assignCurrentProfiles() - viewRouter.currentPage = .home - settings.companyIsLoggingIn = false - } else { - dismiss() - } - hideKeyboard() - }, - label: { - validateButtonLabel - } - ) - .disabled(company.bufferTeacher.name.isEmpty) - } - } - private var validateButtonLabel: some View { HStack(spacing: 4) { Image(systemName: "checkmark.circle") diff --git a/Apps/LekaApp/Sources/Views/Profiles/ProfilesComponents/CreateProfiles/CreateUserProfileView.swift b/Apps/LekaApp/Sources/Views/Profiles/ProfilesComponents/CreateProfiles/CreateUserProfileView.swift index ed5858377c..39d4243103 100644 --- a/Apps/LekaApp/Sources/Views/Profiles/ProfilesComponents/CreateProfiles/CreateUserProfileView.swift +++ b/Apps/LekaApp/Sources/Views/Profiles/ProfilesComponents/CreateProfiles/CreateUserProfileView.swift @@ -8,18 +8,14 @@ import SwiftUI // MARK: - CreateUserProfileView struct CreateUserProfileView: View { + // MARK: Internal + @EnvironmentObject var company: CompanyViewModel @EnvironmentObject var viewRouter: ViewRouter @EnvironmentObject var metrics: UIMetrics @EnvironmentObject var navigationVM: NavigationViewModel @Environment(\.dismiss) var dismiss - @FocusState private var focusedField: FormField? - @State private var isEditing = false - @State private var showDeleteConfirmation: Bool = false - @State private var navigateToSignupFinalStep: Bool = false - @State private var navigateToAvatarPicker: Bool = false - var body: some View { ZStack { Color.white.edgesIgnoringSafeArea(.top) @@ -62,6 +58,14 @@ struct CreateUserProfileView: View { .preferredColorScheme(.light) } + // MARK: Private + + @FocusState private var focusedField: FormField? + @State private var isEditing = false + @State private var showDeleteConfirmation: Bool = false + @State private var navigateToSignupFinalStep: Bool = false + @State private var navigateToAvatarPicker: Bool = false + private var nameField: some View { LekaTextField( label: "Nom d'utilisateur", entry: $company.bufferUser.name, isEditing: $isEditing, type: .name, diff --git a/Apps/LekaApp/Sources/Views/Profiles/ProfilesComponents/JobPicker/Components/JobPickerStore.swift b/Apps/LekaApp/Sources/Views/Profiles/ProfilesComponents/JobPicker/Components/JobPickerStore.swift index e94739714a..13b7da632e 100644 --- a/Apps/LekaApp/Sources/Views/Profiles/ProfilesComponents/JobPicker/Components/JobPickerStore.swift +++ b/Apps/LekaApp/Sources/Views/Profiles/ProfilesComponents/JobPicker/Components/JobPickerStore.swift @@ -5,14 +5,9 @@ import SwiftUI struct JobPickerStore: View { + // MARK: Internal + @Binding var selectedJobs: [String] - private func jobSelection(profession: String) { - if selectedJobs.contains(profession) { - selectedJobs.removeAll(where: { profession == $0 }) - } else { - selectedJobs.append(profession) - } - } var body: some View { ScrollView(showsIndicators: false) { @@ -33,4 +28,14 @@ struct JobPickerStore: View { } } } + + // MARK: Private + + private func jobSelection(profession: String) { + if selectedJobs.contains(profession) { + selectedJobs.removeAll(where: { profession == $0 }) + } else { + selectedJobs.append(profession) + } + } } diff --git a/Apps/LekaApp/Sources/Views/Profiles/ProfilesComponents/JobPicker/JobPicker.swift b/Apps/LekaApp/Sources/Views/Profiles/ProfilesComponents/JobPicker/JobPicker.swift index 3aacd56600..aa7e22fa4a 100644 --- a/Apps/LekaApp/Sources/Views/Profiles/ProfilesComponents/JobPicker/JobPicker.swift +++ b/Apps/LekaApp/Sources/Views/Profiles/ProfilesComponents/JobPicker/JobPicker.swift @@ -8,6 +8,8 @@ import SwiftUI // MARK: - JobPicker struct JobPicker: View { + // MARK: Internal + @EnvironmentObject var company: CompanyViewModel @EnvironmentObject var metrics: UIMetrics @EnvironmentObject var viewRouter: ViewRouter @@ -15,9 +17,6 @@ struct JobPicker: View { @Environment(\.dismiss) var dismiss @FocusState var focusedField: FormField? - @State private var otherJobText: String = "" - @State private var isEditing = false - @State private var selectedJobs: [String] = [] var body: some View { ZStack { @@ -41,6 +40,12 @@ struct JobPicker: View { .toolbarBackground(navigationVM.showProfileEditor ? .visible : .automatic, for: .navigationBar) } + // MARK: Private + + @State private var otherJobText: String = "" + @State private var isEditing = false + @State private var selectedJobs: [String] = [] + private var customJobTextField: some View { VStack(spacing: 0) { Divider() diff --git a/Apps/LekaApp/Sources/Views/Profiles/ProfilesComponents/ProfileSets/ProfileSet_Teachers.swift b/Apps/LekaApp/Sources/Views/Profiles/ProfilesComponents/ProfileSets/ProfileSet_Teachers.swift index 4b698f8802..8a353561ea 100644 --- a/Apps/LekaApp/Sources/Views/Profiles/ProfilesComponents/ProfileSets/ProfileSet_Teachers.swift +++ b/Apps/LekaApp/Sources/Views/Profiles/ProfilesComponents/ProfileSets/ProfileSet_Teachers.swift @@ -6,20 +6,14 @@ import DesignKit import SwiftUI struct ProfileSet_Teachers: View { + // MARK: Internal + @EnvironmentObject var company: CompanyViewModel @EnvironmentObject var settings: SettingsViewModel @EnvironmentObject var viewRouter: ViewRouter @EnvironmentObject var metrics: UIMetrics @EnvironmentObject var navigationVM: NavigationViewModel - @State private var showEditProfileTeacher: Bool = false - @State private var navigateToTeacherCreation: Bool = false - - // check if less than 7 profiles to display in order to adapt Layout (HStack vs. Scrollable Grid) - private func sixMax() -> Bool { - company.currentCompany.teachers.count < 7 - } - var body: some View { VStack(spacing: 0) { header @@ -56,6 +50,11 @@ struct ProfileSet_Teachers: View { } } + // MARK: Private + + @State private var showEditProfileTeacher: Bool = false + @State private var navigateToTeacherCreation: Bool = false + private var editButton: some View { Button { if settings.companyIsConnected { @@ -70,28 +69,6 @@ struct ProfileSet_Teachers: View { .buttonStyle(CircledIcon_NoFeedback_ButtonStyle(font: metrics.bold16)) } - @ViewBuilder - private func addButton() -> some View { - Button { - if viewRouter.currentPage == .welcome { - // Existing company is connected, we're in the selector here - company.resetBufferProfile(.teacher) - navigateToTeacherCreation.toggle() - } else { - if settings.companyIsConnected { - company.editingProfile = false - company.resetBufferProfile(.teacher) - showEditProfileTeacher.toggle() - } else { - settings.showConnectInvite.toggle() - } - } - } label: { - Image(systemName: "plus") - } - .buttonStyle(CircledIcon_NoFeedback_ButtonStyle(font: metrics.bold16)) - } - private var header: some View { HStack(spacing: 20) { if !navigationVM.showProfileEditor { @@ -143,4 +120,31 @@ struct ProfileSet_Teachers: View { ._safeAreaInsets(EdgeInsets(top: 20, leading: 20, bottom: 20, trailing: 20)) } } + + // check if less than 7 profiles to display in order to adapt Layout (HStack vs. Scrollable Grid) + private func sixMax() -> Bool { + company.currentCompany.teachers.count < 7 + } + + @ViewBuilder + private func addButton() -> some View { + Button { + if viewRouter.currentPage == .welcome { + // Existing company is connected, we're in the selector here + company.resetBufferProfile(.teacher) + navigateToTeacherCreation.toggle() + } else { + if settings.companyIsConnected { + company.editingProfile = false + company.resetBufferProfile(.teacher) + showEditProfileTeacher.toggle() + } else { + settings.showConnectInvite.toggle() + } + } + } label: { + Image(systemName: "plus") + } + .buttonStyle(CircledIcon_NoFeedback_ButtonStyle(font: metrics.bold16)) + } } diff --git a/Apps/LekaApp/Sources/Views/Profiles/ProfilesComponents/ProfileSets/ProfileSet_Users.swift b/Apps/LekaApp/Sources/Views/Profiles/ProfilesComponents/ProfileSets/ProfileSet_Users.swift index 55a581eea4..fe652e2f14 100644 --- a/Apps/LekaApp/Sources/Views/Profiles/ProfilesComponents/ProfileSets/ProfileSet_Users.swift +++ b/Apps/LekaApp/Sources/Views/Profiles/ProfilesComponents/ProfileSets/ProfileSet_Users.swift @@ -6,19 +6,14 @@ import DesignKit import SwiftUI struct ProfileSet_Users: View { + // MARK: Internal + @EnvironmentObject var company: CompanyViewModel @EnvironmentObject var settings: SettingsViewModel @EnvironmentObject var metrics: UIMetrics @EnvironmentObject var navigationVM: NavigationViewModel @Environment(\.dismiss) var dismiss - @State private var showEditProfileUser: Bool = false - - // check if less than 7 profiles to display in order to adapt Layout (HStack vs. Scrollable Grid) - private func sixMax() -> Bool { - company.currentCompany.users.count < 7 - } - var body: some View { VStack(spacing: 0) { header @@ -52,6 +47,10 @@ struct ProfileSet_Users: View { } } + // MARK: Private + + @State private var showEditProfileUser: Bool = false + private var editButton: some View { Button { if settings.companyIsConnected { @@ -139,4 +138,9 @@ struct ProfileSet_Users: View { ._safeAreaInsets(EdgeInsets(top: 20, leading: 20, bottom: 20, trailing: 20)) } } + + // check if less than 7 profiles to display in order to adapt Layout (HStack vs. Scrollable Grid) + private func sixMax() -> Bool { + company.currentCompany.users.count < 7 + } } diff --git a/Apps/LekaApp/Sources/Views/Profiles/ProfilesComponents/ProfileSets/TeacherSetComponents/TeacherSet_AvatarCell.swift b/Apps/LekaApp/Sources/Views/Profiles/ProfilesComponents/ProfileSets/TeacherSetComponents/TeacherSet_AvatarCell.swift index 2678d630e9..292b7ed411 100644 --- a/Apps/LekaApp/Sources/Views/Profiles/ProfilesComponents/ProfileSets/TeacherSetComponents/TeacherSet_AvatarCell.swift +++ b/Apps/LekaApp/Sources/Views/Profiles/ProfilesComponents/ProfileSets/TeacherSetComponents/TeacherSet_AvatarCell.swift @@ -6,6 +6,8 @@ import DesignKit import SwiftUI struct TeacherSet_AvatarCell: View { + // MARK: Internal + @EnvironmentObject var company: CompanyViewModel @EnvironmentObject var settings: SettingsViewModel @EnvironmentObject var viewRouter: ViewRouter @@ -71,6 +73,8 @@ struct TeacherSet_AvatarCell: View { .buttonStyle(NoFeedback_ButtonStyle()) } + // MARK: Private + private func selectionIndicator(id: UUID) -> some View { // TODO(@ladislas): review logic in the future let lineWidth: CGFloat = { diff --git a/Apps/LekaApp/Sources/Views/Profiles/ProfilesComponents/ProfileSets/UserSetComponents/UserSet_AvatarCell.swift b/Apps/LekaApp/Sources/Views/Profiles/ProfilesComponents/ProfileSets/UserSetComponents/UserSet_AvatarCell.swift index 39cbb4fda4..736993453b 100644 --- a/Apps/LekaApp/Sources/Views/Profiles/ProfilesComponents/ProfileSets/UserSetComponents/UserSet_AvatarCell.swift +++ b/Apps/LekaApp/Sources/Views/Profiles/ProfilesComponents/ProfileSets/UserSetComponents/UserSet_AvatarCell.swift @@ -6,6 +6,8 @@ import DesignKit import SwiftUI struct UserSet_AvatarCell: View { + // MARK: Internal + @EnvironmentObject var company: CompanyViewModel @EnvironmentObject var settings: SettingsViewModel @EnvironmentObject var metrics: UIMetrics @@ -87,6 +89,8 @@ struct UserSet_AvatarCell: View { .buttonStyle(NoFeedback_ButtonStyle()) } + // MARK: Private + @ViewBuilder private func selectionIndicator(id: UUID) -> some View { // TODO(@ladislas): review logic in the future diff --git a/Apps/LekaApp/Sources/Views/Settings/Components/SettingsSection_Profiles.swift b/Apps/LekaApp/Sources/Views/Settings/Components/SettingsSection_Profiles.swift index 9a4eff2e60..7afccd82a3 100644 --- a/Apps/LekaApp/Sources/Views/Settings/Components/SettingsSection_Profiles.swift +++ b/Apps/LekaApp/Sources/Views/Settings/Components/SettingsSection_Profiles.swift @@ -6,6 +6,8 @@ import DesignKit import SwiftUI struct SettingsSection_Profiles: View { + // MARK: Internal + @EnvironmentObject var company: CompanyViewModel @EnvironmentObject var metrics: UIMetrics @@ -24,6 +26,8 @@ struct SettingsSection_Profiles: View { } } + // MARK: Private + private func avatar(_ name: String) -> some View { Image(name, bundle: Bundle(for: DesignKitResources.self)) .resizable() diff --git a/Apps/LekaApp/Sources/Views/Sidebar/Components/Header/ProfilesButton/Components/TickPic.swift b/Apps/LekaApp/Sources/Views/Sidebar/Components/Header/ProfilesButton/Components/TickPic.swift index 7aaf0646de..4fe67bcb51 100644 --- a/Apps/LekaApp/Sources/Views/Sidebar/Components/Header/ProfilesButton/Components/TickPic.swift +++ b/Apps/LekaApp/Sources/Views/Sidebar/Components/Header/ProfilesButton/Components/TickPic.swift @@ -9,16 +9,6 @@ struct TickPic: View { @EnvironmentObject var company: CompanyViewModel @EnvironmentObject var settings: SettingsViewModel - func imageFromContext() -> Image { - guard settings.exploratoryModeIsOn else { - guard company.profileIsAssigned(.user) || !settings.companyIsConnected else { - return DesignKitAsset.Images.cross.swiftUIImage - } - return DesignKitAsset.Images.tick.swiftUIImage - } - return Image(systemName: "binoculars.fill") - } - var body: some View { HStack(alignment: .top) { imageFromContext() @@ -40,4 +30,14 @@ struct TickPic: View { .offset(y: settings.exploratoryModeIsOn ? 4 : -4) } } + + func imageFromContext() -> Image { + guard settings.exploratoryModeIsOn else { + guard company.profileIsAssigned(.user) || !settings.companyIsConnected else { + return DesignKitAsset.Images.cross.swiftUIImage + } + return DesignKitAsset.Images.tick.swiftUIImage + } + return Image(systemName: "binoculars.fill") + } } diff --git a/Apps/LekaApp/Sources/Views/Sidebar/Components/Header/ProfilesButton/Components/UserSidebarAvatarCell.swift b/Apps/LekaApp/Sources/Views/Sidebar/Components/Header/ProfilesButton/Components/UserSidebarAvatarCell.swift index a67d197e2b..00f8176980 100644 --- a/Apps/LekaApp/Sources/Views/Sidebar/Components/Header/ProfilesButton/Components/UserSidebarAvatarCell.swift +++ b/Apps/LekaApp/Sources/Views/Sidebar/Components/Header/ProfilesButton/Components/UserSidebarAvatarCell.swift @@ -5,6 +5,8 @@ import SwiftUI struct UserSidebarAvatarCell: View { + // MARK: Internal + @EnvironmentObject var company: CompanyViewModel @EnvironmentObject var settings: SettingsViewModel @EnvironmentObject var metrics: UIMetrics @@ -29,6 +31,8 @@ struct UserSidebarAvatarCell: View { } } + // MARK: Private + @ViewBuilder private var avatarAccessoryView: some View { if !settings.companyIsConnected || (!company.profileIsAssigned(.user) && !settings.exploratoryModeIsOn) { Image(systemName: "exclamationmark.circle.fill") diff --git a/Apps/LekaApp/Sources/Views/Sidebar/Components/Header/ProfilesButton/GoToProfileEditorButton.swift b/Apps/LekaApp/Sources/Views/Sidebar/Components/Header/ProfilesButton/GoToProfileEditorButton.swift index e8839d436c..875fed7e4a 100644 --- a/Apps/LekaApp/Sources/Views/Sidebar/Components/Header/ProfilesButton/GoToProfileEditorButton.swift +++ b/Apps/LekaApp/Sources/Views/Sidebar/Components/Header/ProfilesButton/GoToProfileEditorButton.swift @@ -8,6 +8,8 @@ import SwiftUI // MARK: - GoToProfileEditorButton struct GoToProfileEditorButton: View { + // MARK: Internal + @EnvironmentObject var settings: SettingsViewModel @EnvironmentObject var metrics: UIMetrics @EnvironmentObject var navigationVM: NavigationViewModel @@ -39,6 +41,8 @@ struct GoToProfileEditorButton: View { .animation(.default, value: settings.exploratoryModeIsOn) } + // MARK: Private + private var exploratoryModeLabel: some View { Text("Mode exploratoire") .font(metrics.reg17) diff --git a/Apps/LekaApp/Sources/Views/Sidebar/Components/Header/RobotButton/Components/RobotConnectionIndicator.swift b/Apps/LekaApp/Sources/Views/Sidebar/Components/Header/RobotButton/Components/RobotConnectionIndicator.swift index 3dd0b96d2e..06d78eb4a5 100644 --- a/Apps/LekaApp/Sources/Views/Sidebar/Components/Header/RobotButton/Components/RobotConnectionIndicator.swift +++ b/Apps/LekaApp/Sources/Views/Sidebar/Components/Header/RobotButton/Components/RobotConnectionIndicator.swift @@ -7,13 +7,12 @@ import RobotKit import SwiftUI struct RobotConnectionIndicator: View { + // MARK: Internal + @EnvironmentObject var metrics: UIMetrics @StateObject var robotViewModel: ConnectedRobotInformationViewModel = ConnectedRobotInformationViewModel() - @State private var isAnimated: Bool = false - @State private var diameter: CGFloat = 0 - var body: some View { ZStack { Circle() @@ -65,6 +64,11 @@ struct RobotConnectionIndicator: View { } } + // MARK: Private + + @State private var isAnimated: Bool = false + @State private var diameter: CGFloat = 0 + @ViewBuilder private var badgeView: some View { if !robotViewModel.isConnected { Image(systemName: "exclamationmark.circle.fill") diff --git a/Apps/LekaApp/Sources/Views/Sidebar/Components/Header/RobotButton/GoToRobotConnectButton.swift b/Apps/LekaApp/Sources/Views/Sidebar/Components/Header/RobotButton/GoToRobotConnectButton.swift index 91aa7b908f..18552944cb 100644 --- a/Apps/LekaApp/Sources/Views/Sidebar/Components/Header/RobotButton/GoToRobotConnectButton.swift +++ b/Apps/LekaApp/Sources/Views/Sidebar/Components/Header/RobotButton/GoToRobotConnectButton.swift @@ -7,6 +7,8 @@ import RobotKit import SwiftUI struct GoToRobotConnectButton: View { + // MARK: Internal + @EnvironmentObject var metrics: UIMetrics @EnvironmentObject var navigationVM: NavigationViewModel @@ -30,6 +32,8 @@ struct GoToRobotConnectButton: View { .contentShape(Rectangle()) } + // MARK: Private + @ViewBuilder private var buttonContent: some View { if robotViewModel.isConnected { diff --git a/Apps/LekaApp/Sources/Views/Sidebar/Components/Header/SidebarHeaderView.swift b/Apps/LekaApp/Sources/Views/Sidebar/Components/Header/SidebarHeaderView.swift index 5b1c8d4006..b56beeb9c8 100644 --- a/Apps/LekaApp/Sources/Views/Sidebar/Components/Header/SidebarHeaderView.swift +++ b/Apps/LekaApp/Sources/Views/Sidebar/Components/Header/SidebarHeaderView.swift @@ -6,6 +6,8 @@ import DesignKit import SwiftUI struct SidebarHeaderView: View { + // MARK: Internal + @EnvironmentObject var metrics: UIMetrics var body: some View { @@ -17,6 +19,8 @@ struct SidebarHeaderView: View { .frame(minHeight: 350, idealHeight: 350, maxHeight: 371) } + // MARK: Private + private var logoLeka: some View { DesignKitAsset.Assets.lekaLogo.swiftUIImage .resizable() diff --git a/Apps/LekaApp/Sources/Views/Sidebar/SidebarView.swift b/Apps/LekaApp/Sources/Views/Sidebar/SidebarView.swift index 19b308fc9f..f58a749669 100644 --- a/Apps/LekaApp/Sources/Views/Sidebar/SidebarView.swift +++ b/Apps/LekaApp/Sources/Views/Sidebar/SidebarView.swift @@ -6,13 +6,12 @@ import DesignKit import SwiftUI struct SidebarView: View { + // MARK: Internal + @EnvironmentObject var navigationVM: NavigationViewModel @EnvironmentObject var settings: SettingsViewModel @EnvironmentObject var metrics: UIMetrics - @State private var appVersion: String? = "" - @State private var buildNumber: String? = "" - var body: some View { ScrollView { VStack { @@ -35,6 +34,11 @@ struct SidebarView: View { } } + // MARK: Private + + @State private var appVersion: String? = "" + @State private var buildNumber: String? = "" + @ViewBuilder private var settingsButton: some View { if settings.companyIsConnected { diff --git a/Apps/LekaUpdater/Sources/Libs/FirmwareManager.swift b/Apps/LekaUpdater/Sources/Libs/FirmwareManager.swift index 49762fbf0b..74c33ddd91 100644 --- a/Apps/LekaUpdater/Sources/Libs/FirmwareManager.swift +++ b/Apps/LekaUpdater/Sources/Libs/FirmwareManager.swift @@ -16,8 +16,9 @@ enum RobotUpdateStatus { // MARK: - FirmwareManager class FirmwareManager: ObservableObject { - // swiftlint:disable:next force_cast - let currentVersion = Version(Bundle.main.object(forInfoDictionaryKey: "LEKA_OS_VERSION") as! String)! + // MARK: Public + + @Published public var data = Data() public var major: UInt8 { UInt8(currentVersion.major) @@ -31,24 +32,10 @@ class FirmwareManager: ObservableObject { UInt16(currentVersion.patch) } - @Published public var data = Data() - public var sha256: String { SHA256.hash(data: data).compactMap { String(format: "%02x", $0) }.joined() } - func compareWith(version: Version?) -> RobotUpdateStatus { - guard let version = version else { - return .needsUpdate - } - - if version >= currentVersion { - return .upToDate - } - - return .needsUpdate - } - public func load() -> Bool { guard let fileURL = Bundle.main.url(forResource: "LekaOS-\(currentVersion)", withExtension: "bin") else { return false @@ -61,4 +48,21 @@ class FirmwareManager: ObservableObject { return false } } + + // MARK: Internal + + // swiftlint:disable:next force_cast + let currentVersion = Version(Bundle.main.object(forInfoDictionaryKey: "LEKA_OS_VERSION") as! String)! + + func compareWith(version: Version?) -> RobotUpdateStatus { + guard let version = version else { + return .needsUpdate + } + + if version >= currentVersion { + return .upToDate + } + + return .needsUpdate + } } diff --git a/Apps/LekaUpdater/Sources/Libs/UpdateProcess/Template/UpdateProcessTemplate.swift b/Apps/LekaUpdater/Sources/Libs/UpdateProcess/Template/UpdateProcessTemplate.swift index 95b2a7d791..f3caaf169c 100644 --- a/Apps/LekaUpdater/Sources/Libs/UpdateProcess/Template/UpdateProcessTemplate.swift +++ b/Apps/LekaUpdater/Sources/Libs/UpdateProcess/Template/UpdateProcessTemplate.swift @@ -6,6 +6,21 @@ import Combine import Foundation class UpdateProcessTemplate: UpdateProcessProtocol { + // MARK: Lifecycle + + init() { + subscribeToStateUpdates() + } + + // MARK: Public + + // MARK: - Public variables + + public var currentStage = CurrentValueSubject(.initial) + public var sendingFileProgression = CurrentValueSubject(0.0) + + // MARK: Internal + // MARK: - Internal states, events, errors enum UpdateState { @@ -23,20 +38,17 @@ class UpdateProcessTemplate: UpdateProcessProtocol { case notAvailable } + func startProcess() { + currentInternalState.send(completion: .failure(.notAvailable)) + } + + // MARK: Private + // MARK: - Private variables private var cancellables: Set = [] private var currentInternalState = CurrentValueSubject(.initial) - // MARK: - Public variables - - public var currentStage = CurrentValueSubject(.initial) - public var sendingFileProgression = CurrentValueSubject(0.0) - - init() { - subscribeToStateUpdates() - } - private func subscribeToStateUpdates() { self.currentInternalState .receive(on: DispatchQueue.main) @@ -56,8 +68,4 @@ class UpdateProcessTemplate: UpdateProcessProtocol { private func convertReceivedValue(state: UpdateState) { self.currentStage.send(.initial) // only available state } - - func startProcess() { - currentInternalState.send(completion: .failure(.notAvailable)) - } } diff --git a/Apps/LekaUpdater/Sources/Libs/UpdateProcess/UpdateProcessController.swift b/Apps/LekaUpdater/Sources/Libs/UpdateProcess/UpdateProcessController.swift index 9684e77313..9ab4d3ca8d 100644 --- a/Apps/LekaUpdater/Sources/Libs/UpdateProcess/UpdateProcessController.swift +++ b/Apps/LekaUpdater/Sources/Libs/UpdateProcess/UpdateProcessController.swift @@ -32,22 +32,7 @@ enum UpdateProcessError: Error { // MARK: - UpdateProcessController class UpdateProcessController { - // MARK: - Private variables - - private var currentUpdateProcess: any UpdateProcessProtocol - - // MARK: - Public variables - - public static let availableVersions = [ - Version(1, 0, 0), - Version(1, 1, 0), - Version(1, 2, 0), - Version(1, 3, 0), - Version(1, 4, 0), - ] - - public var currentStage = CurrentValueSubject(.initial) - public var sendingFileProgression = CurrentValueSubject(0.0) + // MARK: Lifecycle init() { let currentRobotVersion = Robot.shared.osVersion.value @@ -65,7 +50,30 @@ class UpdateProcessController { self.sendingFileProgression = self.currentUpdateProcess.sendingFileProgression } + // MARK: Public + + // MARK: - Public variables + + public static let availableVersions = [ + Version(1, 0, 0), + Version(1, 1, 0), + Version(1, 2, 0), + Version(1, 3, 0), + Version(1, 4, 0), + ] + + public var currentStage = CurrentValueSubject(.initial) + public var sendingFileProgression = CurrentValueSubject(0.0) + + // MARK: Internal + func startUpdate() { currentUpdateProcess.startProcess() } + + // MARK: Private + + // MARK: - Private variables + + private var currentUpdateProcess: any UpdateProcessProtocol } diff --git a/Apps/LekaUpdater/Sources/Libs/UpdateProcess/Version/UpdateProcessV100.swift b/Apps/LekaUpdater/Sources/Libs/UpdateProcess/Version/UpdateProcessV100.swift index 1545e6dbb5..9350ec648b 100644 --- a/Apps/LekaUpdater/Sources/Libs/UpdateProcess/Version/UpdateProcessV100.swift +++ b/Apps/LekaUpdater/Sources/Libs/UpdateProcess/Version/UpdateProcessV100.swift @@ -81,6 +81,8 @@ private class StateLoadingUpdateFile: GKState, StateEventProcessor { // MARK: - StateSettingDestinationPath private class StateSettingDestinationPath: GKState, StateEventProcessor { + // MARK: Internal + override func isValidNextState(_ stateClass: AnyClass) -> Bool { stateClass is StateSendingFile.Type || stateClass is StateErrorRobotUnexpectedDisconnection.Type } @@ -100,6 +102,8 @@ private class StateSettingDestinationPath: GKState, StateEventProcessor { } } + // MARK: Private + private func setDestinationPath() { let osVersion = globalFirmwareManager.currentVersion @@ -122,31 +126,7 @@ private class StateSettingDestinationPath: GKState, StateEventProcessor { // MARK: - StateSendingFile private class StateSendingFile: GKState, StateEventProcessor { - private var cancellables: Set = [] - - private let maximumPacketSize: Int = 61 - - private var currentPacket: Int = 0 - private var expectedCompletePackets: Int - private var expectedRemainingBytes: Int - private var expectedPackets: Int { - expectedRemainingBytes == 0 ? expectedCompletePackets : expectedCompletePackets + 1 - } - - private var _progression: Float { - Float(currentPacket) / Float(expectedPackets) - } - - private lazy var characteristic: CharacteristicModelWriteOnly = CharacteristicModelWriteOnly( - characteristicUUID: BLESpecs.FileExchange.Characteristics.fileReceptionBuffer, - serviceUUID: BLESpecs.FileExchange.service, - onWrite: { - self.currentPacket += 1 - self.tryToSendNextPacket() - } - ) - - public var progression = CurrentValueSubject(0.0) + // MARK: Lifecycle override init() { let dataSize = globalFirmwareManager.data.count @@ -161,17 +141,11 @@ private class StateSendingFile: GKState, StateEventProcessor { self.subscribeToFirmwareDataUpdates() } - private func subscribeToFirmwareDataUpdates() { - globalFirmwareManager.$data - .receive(on: DispatchQueue.main) - .sink { data in - let dataSize = data.count + // MARK: Public - self.expectedCompletePackets = Int(floor(Double(dataSize / self.maximumPacketSize))) - self.expectedRemainingBytes = Int(dataSize % self.maximumPacketSize) - } - .store(in: &cancellables) - } + public var progression = CurrentValueSubject(0.0) + + // MARK: Internal override func isValidNextState(_ stateClass: AnyClass) -> Bool { stateClass is StateApplyingUpdate.Type || stateClass is StateErrorRobotUnexpectedDisconnection.Type @@ -196,6 +170,44 @@ private class StateSendingFile: GKState, StateEventProcessor { } } + // MARK: Private + + private var cancellables: Set = [] + + private let maximumPacketSize: Int = 61 + + private var currentPacket: Int = 0 + private var expectedCompletePackets: Int + private var expectedRemainingBytes: Int + private lazy var characteristic: CharacteristicModelWriteOnly = CharacteristicModelWriteOnly( + characteristicUUID: BLESpecs.FileExchange.Characteristics.fileReceptionBuffer, + serviceUUID: BLESpecs.FileExchange.service, + onWrite: { + self.currentPacket += 1 + self.tryToSendNextPacket() + } + ) + + private var expectedPackets: Int { + expectedRemainingBytes == 0 ? expectedCompletePackets : expectedCompletePackets + 1 + } + + private var _progression: Float { + Float(currentPacket) / Float(expectedPackets) + } + + private func subscribeToFirmwareDataUpdates() { + globalFirmwareManager.$data + .receive(on: DispatchQueue.main) + .sink { data in + let dataSize = data.count + + self.expectedCompletePackets = Int(floor(Double(dataSize / self.maximumPacketSize))) + self.expectedRemainingBytes = Int(dataSize % self.maximumPacketSize) + } + .store(in: &cancellables) + } + private func sendFile() { tryToSendNextPacket() } @@ -239,7 +251,7 @@ private class StateSendingFile: GKState, StateEventProcessor { // MARK: - StateApplyingUpdate private class StateApplyingUpdate: GKState, StateEventProcessor { - private var cancellables: Set = [] + // MARK: Internal override func isValidNextState(_ stateClass: AnyClass) -> Bool { stateClass is StateWaitingForRobotToReboot.Type @@ -263,6 +275,10 @@ private class StateApplyingUpdate: GKState, StateEventProcessor { } } + // MARK: Private + + private var cancellables: Set = [] + private func setMajorMinorRevision() { let majorData = Data([globalFirmwareManager.major]) @@ -307,20 +323,19 @@ private class StateApplyingUpdate: GKState, StateEventProcessor { // MARK: - StateWaitingForRobotToReboot private class StateWaitingForRobotToReboot: GKState, StateEventProcessor { - private var cancellables: Set = [] + // MARK: Lifecycle - private var expectedRobot: RobotPeripheral? - private var isRobotUpToDate: Bool = false + init(expectedRobot: RobotPeripheral?) { + self.expectedRobot = expectedRobot + } + + // MARK: Internal override func isValidNextState(_ stateClass: AnyClass) -> Bool { stateClass is StateFinal.Type || stateClass is StateErrorRobotNotUpToDate.Type || stateClass is StateErrorRobotUnexpectedDisconnection.Type } - init(expectedRobot: RobotPeripheral?) { - self.expectedRobot = expectedRobot - } - override func didEnter(from previousState: GKState?) { registerScanForRobot() } @@ -344,6 +359,13 @@ private class StateWaitingForRobotToReboot: GKState, StateEventProcessor { } } + // MARK: Private + + private var cancellables: Set = [] + + private var expectedRobot: RobotPeripheral? + private var isRobotUpToDate: Bool = false + private func registerScanForRobot() { BLEManager.shared.scanForRobots() .receive(on: DispatchQueue.main) @@ -390,17 +412,7 @@ private class StateErrorRobotUnexpectedDisconnection: GKState, StateError {} // MARK: - UpdateProcessV100 class UpdateProcessV100: UpdateProcessProtocol { - // MARK: - Private variables - - private var stateMachine: GKStateMachine? - private var stateSendingFile = StateSendingFile() - - private var cancellables: Set = [] - - // MARK: - Public variables - - public var currentStage = CurrentValueSubject(.initial) - public var sendingFileProgression = CurrentValueSubject(0.0) + // MARK: Lifecycle init() { self.stateMachine = GKStateMachine(states: [ @@ -425,10 +437,26 @@ class UpdateProcessV100: UpdateProcessProtocol { self.sendingFileProgression = self.stateSendingFile.progression } + // MARK: Public + + // MARK: - Public variables + + public var currentStage = CurrentValueSubject(.initial) + public var sendingFileProgression = CurrentValueSubject(0.0) + public func startProcess() { process(event: .startUpdateRequested) } + // MARK: Private + + // MARK: - Private variables + + private var stateMachine: GKStateMachine? + private var stateSendingFile = StateSendingFile() + + private var cancellables: Set = [] + private func process(event: UpdateEvent) { guard let state = stateMachine?.currentState as? any StateEventProcessor else { return diff --git a/Apps/LekaUpdater/Sources/Libs/UpdateProcess/Version/UpdateProcessV130.swift b/Apps/LekaUpdater/Sources/Libs/UpdateProcess/Version/UpdateProcessV130.swift index aaa8f8daec..a895f32ee6 100644 --- a/Apps/LekaUpdater/Sources/Libs/UpdateProcess/Version/UpdateProcessV130.swift +++ b/Apps/LekaUpdater/Sources/Libs/UpdateProcess/Version/UpdateProcessV130.swift @@ -84,6 +84,8 @@ private class StateLoadingUpdateFile: GKState, StateEventProcessor { // MARK: - StateSettingFileExchangeState private class StateSettingFileExchangeState: GKState, StateEventProcessor { + // MARK: Internal + override func isValidNextState(_ stateClass: AnyClass) -> Bool { stateClass is StateSettingDestinationPath.Type || stateClass is StateErrorRobotUnexpectedDisconnection.Type @@ -104,6 +106,8 @@ private class StateSettingFileExchangeState: GKState, StateEventProcessor { } } + // MARK: Private + private func setFileExchangeState() { let data = Data([1]) @@ -122,6 +126,8 @@ private class StateSettingFileExchangeState: GKState, StateEventProcessor { // MARK: - StateSettingDestinationPath private class StateSettingDestinationPath: GKState, StateEventProcessor { + // MARK: Internal + override func isValidNextState(_ stateClass: AnyClass) -> Bool { stateClass is StateClearingFile.Type || stateClass is StateErrorRobotUnexpectedDisconnection.Type } @@ -141,6 +147,8 @@ private class StateSettingDestinationPath: GKState, StateEventProcessor { } } + // MARK: Private + private func setDestinationPath() { let osVersion = globalFirmwareManager.currentVersion @@ -163,6 +171,8 @@ private class StateSettingDestinationPath: GKState, StateEventProcessor { // MARK: - StateClearingFile private class StateClearingFile: GKState, StateEventProcessor { + // MARK: Internal + override func isValidNextState(_ stateClass: AnyClass) -> Bool { stateClass is StateSendingFile.Type || stateClass is StateErrorRobotUnexpectedDisconnection.Type } @@ -182,6 +192,8 @@ private class StateClearingFile: GKState, StateEventProcessor { } } + // MARK: Private + private func setClearPath() { let data = Data([1]) @@ -200,31 +212,7 @@ private class StateClearingFile: GKState, StateEventProcessor { // MARK: - StateSendingFile private class StateSendingFile: GKState, StateEventProcessor { - private var cancellables: Set = [] - - private let maximumPacketSize: Int = 61 - - private var currentPacket: Int = 0 - private var expectedCompletePackets: Int - private var expectedRemainingBytes: Int - private var expectedPackets: Int { - expectedRemainingBytes == 0 ? expectedCompletePackets : expectedCompletePackets + 1 - } - - private var _progression: Float { - Float(currentPacket) / Float(expectedPackets) - } - - private lazy var characteristic: CharacteristicModelWriteOnly? = CharacteristicModelWriteOnly( - characteristicUUID: BLESpecs.FileExchange.Characteristics.fileReceptionBuffer, - serviceUUID: BLESpecs.FileExchange.service, - onWrite: { - self.currentPacket += 1 - DispatchQueue.main.asyncAfter(deadline: .now() + 0.04, execute: self.tryToSendNextPacket) - } - ) - - public var progression = CurrentValueSubject(0.0) + // MARK: Lifecycle override init() { let dataSize = globalFirmwareManager.data.count @@ -239,17 +227,11 @@ private class StateSendingFile: GKState, StateEventProcessor { self.subscribeToFirmwareDataUpdates() } - private func subscribeToFirmwareDataUpdates() { - globalFirmwareManager.$data - .receive(on: DispatchQueue.main) - .sink { data in - let dataSize = data.count + // MARK: Public - self.expectedCompletePackets = Int(floor(Double(dataSize / self.maximumPacketSize))) - self.expectedRemainingBytes = Int(dataSize % self.maximumPacketSize) - } - .store(in: &cancellables) - } + public var progression = CurrentValueSubject(0.0) + + // MARK: Internal override func isValidNextState(_ stateClass: AnyClass) -> Bool { stateClass is StateApplyingUpdate.Type || stateClass is StateErrorRobotUnexpectedDisconnection.Type @@ -275,6 +257,44 @@ private class StateSendingFile: GKState, StateEventProcessor { } } + // MARK: Private + + private var cancellables: Set = [] + + private let maximumPacketSize: Int = 61 + + private var currentPacket: Int = 0 + private var expectedCompletePackets: Int + private var expectedRemainingBytes: Int + private lazy var characteristic: CharacteristicModelWriteOnly? = CharacteristicModelWriteOnly( + characteristicUUID: BLESpecs.FileExchange.Characteristics.fileReceptionBuffer, + serviceUUID: BLESpecs.FileExchange.service, + onWrite: { + self.currentPacket += 1 + DispatchQueue.main.asyncAfter(deadline: .now() + 0.04, execute: self.tryToSendNextPacket) + } + ) + + private var expectedPackets: Int { + expectedRemainingBytes == 0 ? expectedCompletePackets : expectedCompletePackets + 1 + } + + private var _progression: Float { + Float(currentPacket) / Float(expectedPackets) + } + + private func subscribeToFirmwareDataUpdates() { + globalFirmwareManager.$data + .receive(on: DispatchQueue.main) + .sink { data in + let dataSize = data.count + + self.expectedCompletePackets = Int(floor(Double(dataSize / self.maximumPacketSize))) + self.expectedRemainingBytes = Int(dataSize % self.maximumPacketSize) + } + .store(in: &cancellables) + } + private func sendFile() { tryToSendNextPacket() } @@ -305,7 +325,7 @@ private class StateSendingFile: GKState, StateEventProcessor { // MARK: - StateApplyingUpdate private class StateApplyingUpdate: GKState, StateEventProcessor { - private var cancellables: Set = [] + // MARK: Internal override func isValidNextState(_ stateClass: AnyClass) -> Bool { stateClass is StateWaitingForRobotToReboot.Type @@ -329,6 +349,10 @@ private class StateApplyingUpdate: GKState, StateEventProcessor { } } + // MARK: Private + + private var cancellables: Set = [] + private func setMajorMinorRevision() { let majorData = Data([globalFirmwareManager.major]) @@ -377,20 +401,19 @@ private class StateApplyingUpdate: GKState, StateEventProcessor { // MARK: - StateWaitingForRobotToReboot private class StateWaitingForRobotToReboot: GKState, StateEventProcessor { - private var cancellables: Set = [] + // MARK: Lifecycle - private var expectedRobot: RobotPeripheral? - private var isRobotUpToDate: Bool = false + init(expectedRobot: RobotPeripheral?) { + self.expectedRobot = expectedRobot + } + + // MARK: Internal override func isValidNextState(_ stateClass: AnyClass) -> Bool { stateClass is StateFinal.Type || stateClass is StateErrorRobotNotUpToDate.Type || stateClass is StateErrorRobotUnexpectedDisconnection.Type } - init(expectedRobot: RobotPeripheral?) { - self.expectedRobot = expectedRobot - } - override func didEnter(from previousState: GKState?) { registerScanForRobot() } @@ -414,6 +437,13 @@ private class StateWaitingForRobotToReboot: GKState, StateEventProcessor { } } + // MARK: Private + + private var cancellables: Set = [] + + private var expectedRobot: RobotPeripheral? + private var isRobotUpToDate: Bool = false + private func registerScanForRobot() { BLEManager.shared.scanForRobots() .receive(on: DispatchQueue.main) @@ -460,17 +490,7 @@ private class StateErrorRobotUnexpectedDisconnection: GKState, StateError {} // MARK: - UpdateProcessV130 class UpdateProcessV130: UpdateProcessProtocol { - // MARK: - Private variables - - private var stateMachine: GKStateMachine? - private var stateSendingFile = StateSendingFile() - - private var cancellables: Set = [] - - // MARK: - Public variables - - public var currentStage = CurrentValueSubject(.initial) - public var sendingFileProgression = CurrentValueSubject(0.0) + // MARK: Lifecycle init() { self.stateMachine = GKStateMachine(states: [ @@ -497,10 +517,26 @@ class UpdateProcessV130: UpdateProcessProtocol { self.sendingFileProgression = self.stateSendingFile.progression } + // MARK: Public + + // MARK: - Public variables + + public var currentStage = CurrentValueSubject(.initial) + public var sendingFileProgression = CurrentValueSubject(0.0) + public func startProcess() { process(event: .startUpdateRequested) } + // MARK: Private + + // MARK: - Private variables + + private var stateMachine: GKStateMachine? + private var stateSendingFile = StateSendingFile() + + private var cancellables: Set = [] + private func process(event: UpdateEvent) { guard let state = stateMachine?.currentState as? any StateEventProcessor else { return diff --git a/Apps/LekaUpdater/Sources/Localization.swift b/Apps/LekaUpdater/Sources/Localization.swift index 033956eec1..ad83700ba8 100644 --- a/Apps/LekaUpdater/Sources/Localization.swift +++ b/Apps/LekaUpdater/Sources/Localization.swift @@ -80,13 +80,6 @@ extension l10n { } enum update { - static let stepNumber = LocalizedStringInterpolation("update.step_number", value: "Step %@", comment: "Update step number") - - static let errorTitle = LocalizedString("update.error", value: "An error has occurred", comment: "Update error") - static let errorDescription = LocalizedString("update.error_description", value: "Unknown error", comment: "Update error description") - static let errorCallToAction = LocalizedString("update.error_call_to_action", value: "Contact technical support", comment: "Update error call to action") - static let errorBackButtonTitle = LocalizedString("update.error_back_button", value: "Return to robot connection page", comment: "Update error back button") - enum requirements { static let instructionsText = LocalizedString( "update.requirements.instructions_text", value: "To start the update, make sure that:", @@ -144,6 +137,13 @@ extension l10n { static let robotUpdatedSuccessfully = LocalizedString("update.finished.robot_updated_successfully", value: "Congrats! 🥳\nYour robot is now up-to-date 🎉", comment: "Robot updated successfully") } + + static let stepNumber = LocalizedStringInterpolation("update.step_number", value: "Step %@", comment: "Update step number") + + static let errorTitle = LocalizedString("update.error", value: "An error has occurred", comment: "Update error") + static let errorDescription = LocalizedString("update.error_description", value: "Unknown error", comment: "Update error description") + static let errorCallToAction = LocalizedString("update.error_call_to_action", value: "Contact technical support", comment: "Update error call to action") + static let errorBackButtonTitle = LocalizedString("update.error_back_button", value: "Return to robot connection page", comment: "Update error back button") } } diff --git a/Apps/LekaUpdater/Sources/View/InformationView/ChangelogView.swift b/Apps/LekaUpdater/Sources/View/InformationView/ChangelogView.swift index 77b01e5297..2975d6497d 100644 --- a/Apps/LekaUpdater/Sources/View/InformationView/ChangelogView.swift +++ b/Apps/LekaUpdater/Sources/View/InformationView/ChangelogView.swift @@ -9,6 +9,14 @@ import SwiftUI // MARK: - ChangelogView struct ChangelogView: View { + // MARK: Internal + + var body: some View { + Text(changelog) + } + + // MARK: Private + private var changelog: LocalizedStringKey { // swiftlint:disable:next force_cast let osVersion = Bundle.main.object(forInfoDictionaryKey: "LEKA_OS_VERSION") as! String @@ -28,10 +36,6 @@ struct ChangelogView: View { return "\(l10n.information.changelogNotFoundText)" } } - - var body: some View { - Text(changelog) - } } // MARK: - ChangelogView_Previews diff --git a/Apps/LekaUpdater/Sources/View/InformationView/InformationView.swift b/Apps/LekaUpdater/Sources/View/InformationView/InformationView.swift index 5ee00a2333..3f8af375cc 100644 --- a/Apps/LekaUpdater/Sources/View/InformationView/InformationView.swift +++ b/Apps/LekaUpdater/Sources/View/InformationView/InformationView.swift @@ -11,14 +11,12 @@ import Version // MARK: - InformationView struct InformationView: View { + // MARK: Internal + @StateObject var viewModel = InformationViewModel() @Binding var isConnectionViewPresented: Bool @Binding var isUpdateStatusViewPresented: Bool - private var isViewVisible: Bool { - !self.isConnectionViewPresented && !self.isUpdateStatusViewPresented - } - var body: some View { NavigationStack { VStack { @@ -123,6 +121,12 @@ struct InformationView: View { } } } + + // MARK: Private + + private var isViewVisible: Bool { + !self.isConnectionViewPresented && !self.isUpdateStatusViewPresented + } } // MARK: - InformationView_Previews diff --git a/Apps/LekaUpdater/Sources/View/InformationView/RobotCannotBeUpdateIllustration.swift b/Apps/LekaUpdater/Sources/View/InformationView/RobotCannotBeUpdateIllustration.swift index 4bfd90e22a..ff70aec7bf 100644 --- a/Apps/LekaUpdater/Sources/View/InformationView/RobotCannotBeUpdateIllustration.swift +++ b/Apps/LekaUpdater/Sources/View/InformationView/RobotCannotBeUpdateIllustration.swift @@ -8,31 +8,17 @@ import SwiftUI // MARK: - RobotCannotBeUpdatedIllustration struct RobotCannotBeUpdatedIllustration: View { - public var illustrationSize: CGFloat = 300 - - private var circleSize: CGFloat { - illustrationSize * 250 / 300 - } + // MARK: Lifecycle - private var circleLineWidth: CGFloat { - illustrationSize / 60 - } - - private var dashSpacer: CGFloat { - illustrationSize / 18 + init(size: CGFloat = 300) { + self.illustrationSize = size } - private var imageSize: CGFloat { - illustrationSize * 180 / 300 - } + // MARK: Public - private var checkmarkSize: CGFloat { - illustrationSize * 56 / 300 - } + public var illustrationSize: CGFloat = 300 - init(size: CGFloat = 300) { - self.illustrationSize = size - } + // MARK: Internal var body: some View { ZStack { @@ -67,6 +53,28 @@ struct RobotCannotBeUpdatedIllustration: View { } .frame(width: circleSize, height: illustrationSize) } + + // MARK: Private + + private var circleSize: CGFloat { + illustrationSize * 250 / 300 + } + + private var circleLineWidth: CGFloat { + illustrationSize / 60 + } + + private var dashSpacer: CGFloat { + illustrationSize / 18 + } + + private var imageSize: CGFloat { + illustrationSize * 180 / 300 + } + + private var checkmarkSize: CGFloat { + illustrationSize * 56 / 300 + } } // MARK: - RobotCannotBeUpdatedIllustration_Previews diff --git a/Apps/LekaUpdater/Sources/View/InformationView/RobotInformationView.swift b/Apps/LekaUpdater/Sources/View/InformationView/RobotInformationView.swift index 42d44567eb..2d63ff3296 100644 --- a/Apps/LekaUpdater/Sources/View/InformationView/RobotInformationView.swift +++ b/Apps/LekaUpdater/Sources/View/InformationView/RobotInformationView.swift @@ -11,7 +11,7 @@ import Version // MARK: - RobotInformationView struct RobotInformationView: View { - @StateObject private var viewModel = RobotInformationViewModel() + // MARK: Internal var body: some View { VStack(alignment: .leading) { @@ -25,6 +25,10 @@ struct RobotInformationView: View { } .padding() } + + // MARK: Private + + @StateObject private var viewModel = RobotInformationViewModel() } // MARK: - RobotInformationView_Previews diff --git a/Apps/LekaUpdater/Sources/View/InformationView/RobotNeedsUpdateIllustration.swift b/Apps/LekaUpdater/Sources/View/InformationView/RobotNeedsUpdateIllustration.swift index a3d8c7c02b..e9152697df 100644 --- a/Apps/LekaUpdater/Sources/View/InformationView/RobotNeedsUpdateIllustration.swift +++ b/Apps/LekaUpdater/Sources/View/InformationView/RobotNeedsUpdateIllustration.swift @@ -8,31 +8,17 @@ import SwiftUI // MARK: - RobotNeedsUpdateIllustration struct RobotNeedsUpdateIllustration: View { - public var illustrationSize: CGFloat = 300 - - private var circleSize: CGFloat { - illustrationSize * 250 / 300 - } + // MARK: Lifecycle - private var circleLineWidth: CGFloat { - illustrationSize / 60 - } - - private var dashSpacer: CGFloat { - illustrationSize / 18 + init(size: CGFloat = 300) { + self.illustrationSize = size } - private var imageSize: CGFloat { - illustrationSize * 180 / 300 - } + // MARK: Public - private var checkmarkSize: CGFloat { - illustrationSize * 56 / 300 - } + public var illustrationSize: CGFloat = 300 - init(size: CGFloat = 300) { - self.illustrationSize = size - } + // MARK: Internal var body: some View { ZStack { @@ -67,6 +53,28 @@ struct RobotNeedsUpdateIllustration: View { } .frame(width: circleSize, height: illustrationSize) } + + // MARK: Private + + private var circleSize: CGFloat { + illustrationSize * 250 / 300 + } + + private var circleLineWidth: CGFloat { + illustrationSize / 60 + } + + private var dashSpacer: CGFloat { + illustrationSize / 18 + } + + private var imageSize: CGFloat { + illustrationSize * 180 / 300 + } + + private var checkmarkSize: CGFloat { + illustrationSize * 56 / 300 + } } // MARK: - RobotNeedsUpdateIllustration_Previews diff --git a/Apps/LekaUpdater/Sources/View/InformationView/RobotUpToDateIllustration.swift b/Apps/LekaUpdater/Sources/View/InformationView/RobotUpToDateIllustration.swift index 698ec9066c..ea480f34b5 100644 --- a/Apps/LekaUpdater/Sources/View/InformationView/RobotUpToDateIllustration.swift +++ b/Apps/LekaUpdater/Sources/View/InformationView/RobotUpToDateIllustration.swift @@ -8,31 +8,17 @@ import SwiftUI // MARK: - RobotUpToDateIllustration struct RobotUpToDateIllustration: View { - public var illustrationSize: CGFloat = 300 - - private var circleSize: CGFloat { - illustrationSize * 250 / 300 - } + // MARK: Lifecycle - private var circleLineWidth: CGFloat { - illustrationSize / 60 - } - - private var dashSpacer: CGFloat { - illustrationSize / 18 + init(size: CGFloat = 300) { + self.illustrationSize = size } - private var imageSize: CGFloat { - illustrationSize * 180 / 300 - } + // MARK: Public - private var checkmarkSize: CGFloat { - illustrationSize * 56 / 300 - } + public var illustrationSize: CGFloat = 300 - init(size: CGFloat = 300) { - self.illustrationSize = size - } + // MARK: Internal var body: some View { ZStack { @@ -66,6 +52,28 @@ struct RobotUpToDateIllustration: View { } .frame(width: circleSize, height: illustrationSize) } + + // MARK: Private + + private var circleSize: CGFloat { + illustrationSize * 250 / 300 + } + + private var circleLineWidth: CGFloat { + illustrationSize / 60 + } + + private var dashSpacer: CGFloat { + illustrationSize / 18 + } + + private var imageSize: CGFloat { + illustrationSize * 180 / 300 + } + + private var checkmarkSize: CGFloat { + illustrationSize * 56 / 300 + } } // MARK: - RobotUpToDateIllustration_Previews diff --git a/Apps/LekaUpdater/Sources/View/UpdateStatusDemoView.swift b/Apps/LekaUpdater/Sources/View/UpdateStatusDemoView.swift index 4db2e98983..f0752b133d 100644 --- a/Apps/LekaUpdater/Sources/View/UpdateStatusDemoView.swift +++ b/Apps/LekaUpdater/Sources/View/UpdateStatusDemoView.swift @@ -8,12 +8,7 @@ import SwiftUI // MARK: - UpdateStatusDemoViewModel class UpdateStatusDemoViewModel: ObservableObject { - private var updateProcessController: UpdateProcessController - - @Published public var state = "" - @Published public var error: String = "" - - private var cancellables: Set = [] + // MARK: Lifecycle init() { self.updateProcessController = UpdateProcessController() @@ -21,10 +16,21 @@ class UpdateStatusDemoViewModel: ObservableObject { subscribeToStateUpdates() } + // MARK: Public + + @Published public var state = "" + @Published public var error: String = "" + public func startUpdate() { updateProcessController.startUpdate() } + // MARK: Private + + private var updateProcessController: UpdateProcessController + + private var cancellables: Set = [] + private func subscribeToStateUpdates() { self.updateProcessController.currentStage .receive(on: DispatchQueue.main) @@ -63,7 +69,7 @@ class UpdateStatusDemoViewModel: ObservableObject { // MARK: - UpdateStatusDemoView struct UpdateStatusDemoView: View { - @StateObject private var viewModel = UpdateStatusDemoViewModel() + // MARK: Internal var body: some View { VStack { @@ -94,6 +100,10 @@ struct UpdateStatusDemoView: View { Spacer() } } + + // MARK: Private + + @StateObject private var viewModel = UpdateStatusDemoViewModel() } // MARK: - UpdateStatusDemoView_Previews diff --git a/Apps/LekaUpdater/Sources/View/UpdatingViews/SendingFileView.swift b/Apps/LekaUpdater/Sources/View/UpdatingViews/SendingFileView.swift index 3cee593562..9ab2eae872 100644 --- a/Apps/LekaUpdater/Sources/View/UpdatingViews/SendingFileView.swift +++ b/Apps/LekaUpdater/Sources/View/UpdatingViews/SendingFileView.swift @@ -47,7 +47,7 @@ struct SendingFileContentView: View { // MARK: - SendingFileView_Previews struct SendingFileView_Previews: PreviewProvider { - @State private static var progress: Float = 0.66 + // MARK: Internal static var previews: some View { VStack { @@ -70,4 +70,8 @@ struct SendingFileView_Previews: PreviewProvider { .frame(maxWidth: .infinity, maxHeight: 250) } } + + // MARK: Private + + @State private static var progress: Float = 0.66 } diff --git a/Apps/LekaUpdater/Sources/ViewModel/InformationViewModel.swift b/Apps/LekaUpdater/Sources/ViewModel/InformationViewModel.swift index 139b72bf97..698e4b42b3 100644 --- a/Apps/LekaUpdater/Sources/ViewModel/InformationViewModel.swift +++ b/Apps/LekaUpdater/Sources/ViewModel/InformationViewModel.swift @@ -8,22 +8,30 @@ import RobotKit import Version class InformationViewModel: ObservableObject { - private var cancellables: Set = [] - - @Published var showRobotCannotBeUpdated: Bool = false - @Published var showRobotNeedsUpdate: Bool = true - @Published var robotName: String = "n/a" - @Published var robotOSVersion: String = "" + // MARK: Lifecycle init() { self.subscribeToRobotNameUpdates() self.subscribeToRobotOsVersionUpdates() } + // MARK: Public + public func onViewReappear() { self.robotName = Robot.shared.name.value } + // MARK: Internal + + @Published var showRobotCannotBeUpdated: Bool = false + @Published var showRobotNeedsUpdate: Bool = true + @Published var robotName: String = "n/a" + @Published var robotOSVersion: String = "" + + // MARK: Private + + private var cancellables: Set = [] + private func subscribeToRobotNameUpdates() { Robot.shared.name .receive(on: DispatchQueue.main) diff --git a/Apps/LekaUpdater/Sources/ViewModel/RequirementsViewModel.swift b/Apps/LekaUpdater/Sources/ViewModel/RequirementsViewModel.swift index 751a74aaf3..1a9707e73a 100644 --- a/Apps/LekaUpdater/Sources/ViewModel/RequirementsViewModel.swift +++ b/Apps/LekaUpdater/Sources/ViewModel/RequirementsViewModel.swift @@ -9,7 +9,14 @@ import RobotKit import SwiftUI class RequirementsViewModel: ObservableObject { - private var cancellables: Set = [] + // MARK: Lifecycle + + init() { + subscribeToRobotBatteryUpdates() + subscribeToRobotIsChargingUpdates() + } + + // MARK: Internal let requirementsInstructionsText = l10n.update.requirements.instructionsText @@ -25,10 +32,9 @@ class RequirementsViewModel: ObservableObject { @Published var robotIsReadyToUpdate = false @Published var robotIsNotReadyToUpdate = true - init() { - subscribeToRobotBatteryUpdates() - subscribeToRobotIsChargingUpdates() - } + // MARK: Private + + private var cancellables: Set = [] private func subscribeToRobotBatteryUpdates() { Robot.shared.battery diff --git a/Apps/LekaUpdater/Sources/ViewModel/RobotInformationViewModel.swift b/Apps/LekaUpdater/Sources/ViewModel/RobotInformationViewModel.swift index 20be7a1990..3480788922 100644 --- a/Apps/LekaUpdater/Sources/ViewModel/RobotInformationViewModel.swift +++ b/Apps/LekaUpdater/Sources/ViewModel/RobotInformationViewModel.swift @@ -8,12 +8,7 @@ import LocalizationKit import RobotKit class RobotInformationViewModel: ObservableObject { - private var cancellables: Set = [] - - @Published var robotSerialNumber = "n/a" - @Published var robotBattery = "n/a" - @Published var robotOsVersion = "n/a" - @Published var robotIsCharging = "n/a" + // MARK: Lifecycle init() { subscribeToRobotSerialNumberUpdates() @@ -22,6 +17,17 @@ class RobotInformationViewModel: ObservableObject { subscribeToRobotChargingStatusUpdates() } + // MARK: Internal + + @Published var robotSerialNumber = "n/a" + @Published var robotBattery = "n/a" + @Published var robotOsVersion = "n/a" + @Published var robotIsCharging = "n/a" + + // MARK: Private + + private var cancellables: Set = [] + private func subscribeToRobotSerialNumberUpdates() { Robot.shared.serialNumber .receive(on: DispatchQueue.main) diff --git a/Apps/LekaUpdater/Sources/ViewModel/UpdateStatusViewModel.swift b/Apps/LekaUpdater/Sources/ViewModel/UpdateStatusViewModel.swift index be561ef72c..9236eaba26 100644 --- a/Apps/LekaUpdater/Sources/ViewModel/UpdateStatusViewModel.swift +++ b/Apps/LekaUpdater/Sources/ViewModel/UpdateStatusViewModel.swift @@ -9,18 +9,15 @@ import RobotKit import SwiftUI class UpdateStatusViewModel: ObservableObject { - enum UpdateStatus { - case sendingFile - case rebootingRobot - case updateFinished - case error - } + // MARK: Lifecycle - // MARK: - Private variables - - private var updateProcessController = UpdateProcessController() + init() { + subscribeToStateUpdates() + subscribeToSendingFileProgressionUpdates() + subscribeToRobotIsChargingUpdates() + } - private var cancellables: Set = [] + // MARK: Public // MARK: - Public variables @@ -44,12 +41,29 @@ class UpdateStatusViewModel: ObservableObject { } } - init() { - subscribeToStateUpdates() - subscribeToSendingFileProgressionUpdates() - subscribeToRobotIsChargingUpdates() + public func startUpdate() { + UIApplication.shared.isIdleTimerDisabled = true + + updateProcessController.startUpdate() } + // MARK: Internal + + enum UpdateStatus { + case sendingFile + case rebootingRobot + case updateFinished + case error + } + + // MARK: Private + + // MARK: - Private variables + + private var updateProcessController = UpdateProcessController() + + private var cancellables: Set = [] + private func subscribeToStateUpdates() { self.updateProcessController.currentStage .receive(on: DispatchQueue.main) @@ -123,12 +137,6 @@ class UpdateStatusViewModel: ObservableObject { .store(in: &cancellables) } - public func startUpdate() { - UIApplication.shared.isIdleTimerDisabled = true - - updateProcessController.startUpdate() - } - private func onUpdateEnded() { UIApplication.shared.isIdleTimerDisabled = false } diff --git a/Examples/Module/Sources/HelloView.swift b/Examples/Module/Sources/HelloView.swift index 5244dfb7b2..dee8bd60a9 100644 --- a/Examples/Module/Sources/HelloView.swift +++ b/Examples/Module/Sources/HelloView.swift @@ -7,14 +7,15 @@ import SwiftUI // MARK: - HelloView public struct HelloView: View { - var color: Color - var name: String + // MARK: Lifecycle public init(color: Color, name: String) { self.color = color self.name = name } + // MARK: Public + public var body: some View { ZStack { color @@ -27,6 +28,11 @@ public struct HelloView: View { .multilineTextAlignment(.center) } } + + // MARK: Internal + + var color: Color + var name: String } // MARK: - ContentView_Previews diff --git a/Modules/AccountKit/Examples/AccountKitExample/Sources/MainView.swift b/Modules/AccountKit/Examples/AccountKitExample/Sources/MainView.swift index 7d920f115a..c5ada55902 100644 --- a/Modules/AccountKit/Examples/AccountKitExample/Sources/MainView.swift +++ b/Modules/AccountKit/Examples/AccountKitExample/Sources/MainView.swift @@ -5,6 +5,8 @@ import SwiftUI struct MainView: View { + // MARK: Internal + @EnvironmentObject var authenticationState: OrganisationAuthState var body: some View { @@ -30,6 +32,8 @@ struct MainView: View { }) } + // MARK: Private + private var navigation: some View { NavigationStack { VStack(spacing: 10) { diff --git a/Modules/AccountKit/Examples/AccountKitExample/Sources/ViewModels/OrganisationViewModel.swift b/Modules/AccountKit/Examples/AccountKitExample/Sources/ViewModels/OrganisationViewModel.swift index 5cc3a264d9..4e3a941471 100644 --- a/Modules/AccountKit/Examples/AccountKitExample/Sources/ViewModels/OrganisationViewModel.swift +++ b/Modules/AccountKit/Examples/AccountKitExample/Sources/ViewModels/OrganisationViewModel.swift @@ -12,27 +12,6 @@ struct OrganisationViewModel { // var users: [User] = [] var confirmPassword: String = "" - // MARK: - Validation Checks - - func isEmailValid() -> Bool { - let mailTest = NSPredicate( - format: "SELF MATCHES %@", - "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}") - return mailTest.evaluate(with: mail) - } - - func isPasswordValid(_ password: String) -> Bool { - // 8 chars min, contain a cap letter and a number at least - let passwordTest = NSPredicate( - format: "SELF MATCHES %@", - "^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9]).{8,}$") - return passwordTest.evaluate(with: password) - } - - func passwordsMatch() -> Bool { - confirmPassword == password - } - var signUpIsComplete: Bool { if !isEmailValid() || !isPasswordValid(password) || !passwordsMatch() { return false @@ -60,4 +39,25 @@ struct OrganisationViewModel { var invalidConfirmPasswordText: String { "Password fields do not match." } + + // MARK: - Validation Checks + + func isEmailValid() -> Bool { + let mailTest = NSPredicate( + format: "SELF MATCHES %@", + "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}") + return mailTest.evaluate(with: mail) + } + + func isPasswordValid(_ password: String) -> Bool { + // 8 chars min, contain a cap letter and a number at least + let passwordTest = NSPredicate( + format: "SELF MATCHES %@", + "^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9]).{8,}$") + return passwordTest.evaluate(with: password) + } + + func passwordsMatch() -> Bool { + confirmPassword == password + } } diff --git a/Modules/AccountKit/Examples/AccountKitExample/Sources/Views/HomeView.swift b/Modules/AccountKit/Examples/AccountKitExample/Sources/Views/HomeView.swift index e17324c4fa..9f243951d5 100644 --- a/Modules/AccountKit/Examples/AccountKitExample/Sources/Views/HomeView.swift +++ b/Modules/AccountKit/Examples/AccountKitExample/Sources/Views/HomeView.swift @@ -5,8 +5,9 @@ import SwiftUI struct HomeView: View { + // MARK: Internal + @EnvironmentObject var authenticationState: OrganisationAuthState - @State private var goBackToContentView: Bool = false var body: some View { switch authenticationState.organisationIsAuthenticated { @@ -19,6 +20,10 @@ struct HomeView: View { } } + // MARK: Private + + @State private var goBackToContentView: Bool = false + private var content: some View { VStack(spacing: 10) { Text("Organisation is Logged In!") diff --git a/Modules/AccountKit/Examples/AccountKitExample/Sources/Views/LoginView.swift b/Modules/AccountKit/Examples/AccountKitExample/Sources/Views/LoginView.swift index dde1cf1199..3ce45027d7 100644 --- a/Modules/AccountKit/Examples/AccountKitExample/Sources/Views/LoginView.swift +++ b/Modules/AccountKit/Examples/AccountKitExample/Sources/Views/LoginView.swift @@ -6,9 +6,9 @@ import AuthenticationServices import SwiftUI struct LoginView: View { + // MARK: Internal + @EnvironmentObject var authenticationState: OrganisationAuthState - @State private var organisation = OrganisationViewModel() - @State private var showSheet: Bool = false var body: some View { VStack(spacing: 10) { @@ -27,6 +27,11 @@ struct LoginView: View { .sheet(isPresented: $showSheet) { ForgotPasswordView() } } + // MARK: Private + + @State private var organisation = OrganisationViewModel() + @State private var showSheet: Bool = false + private var emailField: some View { VStack(alignment: .leading, spacing: 10) { TextField("email", text: $organisation.mail) diff --git a/Modules/AccountKit/Examples/AccountKitExample/Sources/Views/SignupView.swift b/Modules/AccountKit/Examples/AccountKitExample/Sources/Views/SignupView.swift index 9beb08a3d3..f89ac8250c 100644 --- a/Modules/AccountKit/Examples/AccountKitExample/Sources/Views/SignupView.swift +++ b/Modules/AccountKit/Examples/AccountKitExample/Sources/Views/SignupView.swift @@ -6,8 +6,9 @@ import AuthenticationServices import SwiftUI struct SignupView: View { + // MARK: Internal + @EnvironmentObject var authenticationState: OrganisationAuthState - @State private var organisation = OrganisationViewModel() var body: some View { VStack(spacing: 10) { @@ -26,6 +27,10 @@ struct SignupView: View { .animation(.default, value: organisation.passwordsMatch()) } + // MARK: Private + + @State private var organisation = OrganisationViewModel() + private var emailField: some View { VStack(alignment: .leading, spacing: 10) { TextField("email", text: $organisation.mail) diff --git a/Modules/BLEKit/Examples/BLEKitExample/Sources/ViewModels/RobotListViewModel.swift b/Modules/BLEKit/Examples/BLEKitExample/Sources/ViewModels/RobotListViewModel.swift index a134ca4161..455b81468d 100644 --- a/Modules/BLEKit/Examples/BLEKitExample/Sources/ViewModels/RobotListViewModel.swift +++ b/Modules/BLEKit/Examples/BLEKitExample/Sources/ViewModels/RobotListViewModel.swift @@ -8,20 +8,7 @@ import SwiftUI @MainActor public class RobotListViewModel: ObservableObject { - // MARK: - Private variables - - internal let bleManager: BLEManager - internal var cancellables: Set = [] - internal var scanForRobotsTask: AnyCancellable? - - // MARK: - Published variables - - @Published var selectedRobotDiscovery: RobotDiscoveryModel? - // TODO(@ladislas): are they both needed? - @Published var connectedRobotDiscovery: RobotDiscoveryModel? - @Published var connectedRobotPeripheral: RobotPeripheral? - @Published var robotDiscoveries: [RobotDiscoveryModel] = [] - @Published var isScanning: Bool = false + // MARK: Lifecycle // MARK: - Public functions @@ -30,6 +17,15 @@ public class RobotListViewModel: ObservableObject { subscribeToScanningStatus() } + // MARK: - Private functions + + internal init(availableRobots: [RobotDiscoveryModel]) { + self.bleManager = BLEManager.live() + self.robotDiscoveries = availableRobots + } + + // MARK: Public + public func scanForPeripherals() { if !bleManager.isScanning.value { print("Start scanning") @@ -84,12 +80,24 @@ public class RobotListViewModel: ObservableObject { } } - // MARK: - Private functions + // MARK: Internal - internal init(availableRobots: [RobotDiscoveryModel]) { - self.bleManager = BLEManager.live() - self.robotDiscoveries = availableRobots - } + // MARK: - Private variables + + internal let bleManager: BLEManager + internal var cancellables: Set = [] + internal var scanForRobotsTask: AnyCancellable? + + // MARK: - Published variables + + @Published var selectedRobotDiscovery: RobotDiscoveryModel? + // TODO(@ladislas): are they both needed? + @Published var connectedRobotDiscovery: RobotDiscoveryModel? + @Published var connectedRobotPeripheral: RobotPeripheral? + @Published var robotDiscoveries: [RobotDiscoveryModel] = [] + @Published var isScanning: Bool = false + + // MARK: Private private func subscribeToScanningStatus() { bleManager.isScanning diff --git a/Modules/BLEKit/Examples/BLEKitExample/Sources/Views/ConnectButton.swift b/Modules/BLEKit/Examples/BLEKitExample/Sources/Views/ConnectButton.swift index 091bf83799..c2cfcdced1 100644 --- a/Modules/BLEKit/Examples/BLEKitExample/Sources/Views/ConnectButton.swift +++ b/Modules/BLEKit/Examples/BLEKitExample/Sources/Views/ConnectButton.swift @@ -9,6 +9,8 @@ import SwiftUI // MARK: - ConnectButton struct ConnectButton: View { + // MARK: Internal + // MARK: - Environment variables @EnvironmentObject var robotListViewModel: RobotListViewModel @@ -37,6 +39,8 @@ struct ConnectButton: View { robotListViewModel.connectedRobotPeripheral == nil && robotListViewModel.selectedRobotDiscovery == nil) } + // MARK: Private + // MARK: - Private views private var connectedView: some View { diff --git a/Modules/BLEKit/Examples/BLEKitExample/Sources/Views/ContentView.swift b/Modules/BLEKit/Examples/BLEKitExample/Sources/Views/ContentView.swift index 218c36d4a2..9867008319 100644 --- a/Modules/BLEKit/Examples/BLEKitExample/Sources/Views/ContentView.swift +++ b/Modules/BLEKit/Examples/BLEKitExample/Sources/Views/ContentView.swift @@ -8,9 +8,7 @@ import SwiftUI // MARK: - ContentView struct ContentView: View { - // MARK: - Environment variables - - @StateObject private var robotListViewModel: RobotListViewModel + // MARK: Lifecycle // MARK: - Public functions @@ -20,6 +18,8 @@ struct ContentView: View { self._robotListViewModel = StateObject(wrappedValue: RobotListViewModel(bleManager: bleManager)) } + // MARK: Internal + // MARK: - Views var body: some View { @@ -55,12 +55,19 @@ struct ContentView: View { .navigationTitle("BLEKit Example App") .environmentObject(robotListViewModel) } + + // MARK: Private + + // MARK: - Environment variables + + @StateObject private var robotListViewModel: RobotListViewModel } // MARK: - ContentView_Previews struct ContentView_Previews: PreviewProvider { static let bleManager = BLEManager.live() + static var previews: some View { ContentView(bleManager: bleManager) } diff --git a/Modules/BLEKit/Examples/BLEKitExample/Sources/Views/RobotDiscoveryView.swift b/Modules/BLEKit/Examples/BLEKitExample/Sources/Views/RobotDiscoveryView.swift index d88123c275..7b8346387b 100644 --- a/Modules/BLEKit/Examples/BLEKitExample/Sources/Views/RobotDiscoveryView.swift +++ b/Modules/BLEKit/Examples/BLEKitExample/Sources/Views/RobotDiscoveryView.swift @@ -6,16 +6,14 @@ import BLEKit import SwiftUI struct RobotDiscoveryView: View { - private var discovery: RobotDiscoveryModel - - // MARK: - Environment variables - - @EnvironmentObject private var robotListViewModel: RobotListViewModel + // MARK: Lifecycle public init(discovery: RobotDiscoveryModel) { self.discovery = discovery } + // MARK: Internal + var body: some View { HStack(spacing: 20) { Image( @@ -68,6 +66,14 @@ struct RobotDiscoveryView: View { } } } + + // MARK: Private + + private var discovery: RobotDiscoveryModel + + // MARK: - Environment variables + + @EnvironmentObject private var robotListViewModel: RobotListViewModel } // TODO(@ladislas): create protocol and mock RobotDiscovery diff --git a/Modules/BLEKit/Examples/BLEKitExample/Sources/Views/SendDataButton.swift b/Modules/BLEKit/Examples/BLEKitExample/Sources/Views/SendDataButton.swift index 670f80c2a5..d97e1de551 100644 --- a/Modules/BLEKit/Examples/BLEKitExample/Sources/Views/SendDataButton.swift +++ b/Modules/BLEKit/Examples/BLEKitExample/Sources/Views/SendDataButton.swift @@ -8,6 +8,8 @@ import SwiftUI // MARK: - SendDataButton struct SendDataButton: View { + // MARK: Internal + // MARK: - Environment properties @EnvironmentObject var robotListViewModel: RobotListViewModel @@ -24,6 +26,8 @@ struct SendDataButton: View { .disabled(self.robotListViewModel.connectedRobotPeripheral == nil) } + // MARK: Private + // MARK: - Private views private var labelView: some View { diff --git a/Modules/BLEKit/Sources/BLEManager.swift b/Modules/BLEKit/Sources/BLEManager.swift index f75ad0405c..2741b88081 100644 --- a/Modules/BLEKit/Sources/BLEManager.swift +++ b/Modules/BLEKit/Sources/BLEManager.swift @@ -5,6 +5,19 @@ import CombineCoreBluetooth public class BLEManager { + // MARK: Lifecycle + + // MARK: - Public functions + + public init(centralManager: CentralManager) { + self.centralManager = centralManager + + self.subscribeToDidDisconnect() + self.subscribeToDidConnect() + } + + // MARK: Public + public static var shared: BLEManager = BLEManager( centralManager: .live( ManagerCreationOptions(showPowerAlert: true, restoreIdentifier: "io.leka.module.BLEKit.Manager.live"))) @@ -19,22 +32,6 @@ public class BLEManager { connectedRobotPeripheral != nil ? true : false } - // MARK: - Private variables - - private var centralManager: CentralManager - private var connectedRobotPeripheral: RobotPeripheral? - - private var cancellables: Set = [] - - // MARK: - Public functions - - public init(centralManager: CentralManager) { - self.centralManager = centralManager - - self.subscribeToDidDisconnect() - self.subscribeToDidConnect() - } - public static func live() -> BLEManager { BLEManager(centralManager: CentralManager.live()) } @@ -101,6 +98,15 @@ public class BLEManager { centralManager.cancelPeripheralConnection(connectedPeripheral) } + // MARK: Private + + // MARK: - Private variables + + private var centralManager: CentralManager + private var connectedRobotPeripheral: RobotPeripheral? + + private var cancellables: Set = [] + // MARK: - Private functions private func subscribeToDidConnect() { diff --git a/Modules/BLEKit/Sources/BLESpecs.swift b/Modules/BLEKit/Sources/BLESpecs.swift index b9963582a0..0a78dca1cf 100644 --- a/Modules/BLEKit/Sources/BLESpecs.swift +++ b/Modules/BLEKit/Sources/BLESpecs.swift @@ -11,55 +11,53 @@ public enum BLESpecs { } public enum DeviceInformation { - public static let service = CBUUID(string: "0x180A") - public enum Characteristics { public static let manufacturer = CBUUID(string: "0x2A29") public static let modelNumber = CBUUID(string: "0x2A24") public static let serialNumber = CBUUID(string: "0x2A25") public static let osVersion = CBUUID(string: "0x2A26") } + + public static let service = CBUUID(string: "0x180A") } public enum Battery { - public static let service = CBUUID(string: "0x180F") - public enum Characteristics { public static let level = CBUUID(string: "0x2A19") } + + public static let service = CBUUID(string: "0x180F") } public enum Monitoring { - public static let service = CBUUID(string: "0x7779") - public enum Characteristics { public static let chargingStatus = CBUUID(string: "0x6783") public static let screensaverEnable = CBUUID(string: "0x8369") public static let softReboot = CBUUID(string: "0x8382") public static let hardReboot = CBUUID(string: "0x7282") } + + public static let service = CBUUID(string: "0x7779") } public enum Config { - public static let service = CBUUID(string: "0x6770") - public enum Characteristics { public static let robotName = CBUUID(string: "8278") } + + public static let service = CBUUID(string: "0x6770") } public enum MagicCard { - public static let service = CBUUID(data: Data("Magic Card".utf8 + Data([0, 0, 0, 0, 0, 0]))) - public enum Characteristics { public static let id = CBUUID(data: Data("ID".utf8) + Data([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0])) public static let language = CBUUID(data: Data("Language".utf8) + Data([0, 0, 0, 0, 0, 0, 0, 0])) } + + public static let service = CBUUID(data: Data("Magic Card".utf8 + Data([0, 0, 0, 0, 0, 0]))) } public enum FileExchange { - public static let service = CBUUID(string: "0x8270") - public enum Characteristics { public static let setState = CBUUID(string: "0x8383") public static let filePath = CBUUID(string: "0x7080") @@ -67,11 +65,11 @@ public enum BLESpecs { public static let fileReceptionBuffer = CBUUID(string: "0x8283") public static let fileSHA256 = CBUUID(string: "0x7083") } + + public static let service = CBUUID(string: "0x8270") } public enum FirmwareUpdate { - public static let service = CBUUID(string: "0x7085") - public enum Characteristics { public static let requestUpdate = CBUUID(string: "0x8285") public static let requestFactoryReset = CBUUID(string: "0x8270") @@ -79,13 +77,15 @@ public enum BLESpecs { public static let versionMinor = CBUUID(string: "0x7773") public static let versionRevision = CBUUID(string: "0x8269") } + + public static let service = CBUUID(string: "0x7085") } public enum Commands { - public static let service = CBUUID(string: "0xDFB0") - public enum Characteristics { public static let tx = CBUUID(string: "0xDFB1") } + + public static let service = CBUUID(string: "0xDFB0") } } diff --git a/Modules/BLEKit/Sources/Models/AdvertisingServiceData.swift b/Modules/BLEKit/Sources/Models/AdvertisingServiceData.swift index 7811e3a348..672086c957 100644 --- a/Modules/BLEKit/Sources/Models/AdvertisingServiceData.swift +++ b/Modules/BLEKit/Sources/Models/AdvertisingServiceData.swift @@ -7,15 +7,19 @@ import Foundation // MARK: - AdvertisingServiceData internal struct AdvertisingServiceData { - let battery: Int - let isCharging: Bool - let osVersion: String? + // MARK: Lifecycle init(data: Data) { self.battery = getBattery(data: data) self.isCharging = getChargingState(data: data) self.osVersion = getOsVersion(data: data) } + + // MARK: Internal + + let battery: Int + let isCharging: Bool + let osVersion: String? } // MARK: - AdvertisingServiceDataIndex diff --git a/Modules/BLEKit/Sources/Models/CharacteristicModelNotifying.swift b/Modules/BLEKit/Sources/Models/CharacteristicModelNotifying.swift index b2178935dc..782067de2c 100644 --- a/Modules/BLEKit/Sources/Models/CharacteristicModelNotifying.swift +++ b/Modules/BLEKit/Sources/Models/CharacteristicModelNotifying.swift @@ -7,12 +7,7 @@ import CombineCoreBluetooth // MARK: - CharacteristicModelNotifying public struct CharacteristicModelNotifying { - public typealias Callback = ((_ data: Data?) -> Void) - - public let characteristicUUID: CBUUID - public let serviceUUID: CBUUID - public let cbCharacteristic: CBCharacteristic? - public let onNotification: Callback? + // MARK: Lifecycle public init( characteristicUUID: CBUUID, serviceUUID: CBUUID, cbCharacteristic: CBCharacteristic? = nil, @@ -23,6 +18,15 @@ public struct CharacteristicModelNotifying { self.cbCharacteristic = cbCharacteristic self.onNotification = onNotification } + + // MARK: Public + + public typealias Callback = ((_ data: Data?) -> Void) + + public let characteristicUUID: CBUUID + public let serviceUUID: CBUUID + public let cbCharacteristic: CBCharacteristic? + public let onNotification: Callback? } // MARK: Hashable diff --git a/Modules/BLEKit/Sources/Models/CharacteristicModelReadOnly.swift b/Modules/BLEKit/Sources/Models/CharacteristicModelReadOnly.swift index 5bdb58681f..f57b31017f 100644 --- a/Modules/BLEKit/Sources/Models/CharacteristicModelReadOnly.swift +++ b/Modules/BLEKit/Sources/Models/CharacteristicModelReadOnly.swift @@ -7,17 +7,21 @@ import CombineCoreBluetooth // MARK: - CharacteristicModelReadOnly public struct CharacteristicModelReadOnly { - public typealias Callback = ((_ data: Data?) -> Void) - - public let characteristicUUID: CBUUID - public let serviceUUID: CBUUID - public let onRead: Callback? + // MARK: Lifecycle public init(characteristicUUID: CBUUID, serviceUUID: CBUUID, onRead: Callback? = nil) { self.characteristicUUID = characteristicUUID self.serviceUUID = serviceUUID self.onRead = onRead } + + // MARK: Public + + public typealias Callback = ((_ data: Data?) -> Void) + + public let characteristicUUID: CBUUID + public let serviceUUID: CBUUID + public let onRead: Callback? } // MARK: Hashable diff --git a/Modules/BLEKit/Sources/Models/CharacteristicModelWriteOnly.swift b/Modules/BLEKit/Sources/Models/CharacteristicModelWriteOnly.swift index a2abc63e68..880754d924 100644 --- a/Modules/BLEKit/Sources/Models/CharacteristicModelWriteOnly.swift +++ b/Modules/BLEKit/Sources/Models/CharacteristicModelWriteOnly.swift @@ -5,15 +5,19 @@ import CombineCoreBluetooth public struct CharacteristicModelWriteOnly { - public typealias Callback = (() -> Void) - - public let characteristicUUID: CBUUID - public let serviceUUID: CBUUID - public let onWrite: Callback? + // MARK: Lifecycle public init(characteristicUUID: CBUUID, serviceUUID: CBUUID, onWrite: Callback? = nil) { self.characteristicUUID = characteristicUUID self.serviceUUID = serviceUUID self.onWrite = onWrite } + + // MARK: Public + + public typealias Callback = (() -> Void) + + public let characteristicUUID: CBUUID + public let serviceUUID: CBUUID + public let onWrite: Callback? } diff --git a/Modules/BLEKit/Sources/Models/RobotAdvertisingData.swift b/Modules/BLEKit/Sources/Models/RobotAdvertisingData.swift index 4dc4ca7d35..6a5b6e57a5 100644 --- a/Modules/BLEKit/Sources/Models/RobotAdvertisingData.swift +++ b/Modules/BLEKit/Sources/Models/RobotAdvertisingData.swift @@ -5,12 +5,7 @@ import CombineCoreBluetooth public struct RobotAdvertisingData { - // MARK: - Public variables - - public let name: String - public let battery: Int - public let isCharging: Bool - public let osVersion: String? + // MARK: Lifecycle // MARK: - Public functions @@ -32,4 +27,13 @@ public struct RobotAdvertisingData { self.init(name: advertisementData.localName, serviceData: robotServiceData) } + + // MARK: Public + + // MARK: - Public variables + + public let name: String + public let battery: Int + public let isCharging: Bool + public let osVersion: String? } diff --git a/Modules/BLEKit/Sources/Models/RobotDiscoveryModel.swift b/Modules/BLEKit/Sources/Models/RobotDiscoveryModel.swift index 6b12b9fe41..89e320b2c1 100644 --- a/Modules/BLEKit/Sources/Models/RobotDiscoveryModel.swift +++ b/Modules/BLEKit/Sources/Models/RobotDiscoveryModel.swift @@ -7,16 +7,7 @@ import CombineCoreBluetooth // MARK: - RobotDiscoveryModel public struct RobotDiscoveryModel: Identifiable { - // MARK: - Public variables - - public let robotPeripheral: RobotPeripheral! - public let rssi: Double? - - public let id: UUID - public let name: String - public let isCharging: Bool - public let battery: Int - public let osVersion: String + // MARK: Lifecycle // MARK: - Public functions @@ -39,6 +30,19 @@ public struct RobotDiscoveryModel: Identifiable { self.battery = advertisingData.battery self.osVersion = computeVersion(version: advertisingData.osVersion, name: advertisingData.name) } + + // MARK: Public + + // MARK: - Public variables + + public let robotPeripheral: RobotPeripheral! + public let rssi: Double? + + public let id: UUID + public let name: String + public let isCharging: Bool + public let battery: Int + public let osVersion: String } // MARK: Equatable diff --git a/Modules/BLEKit/Sources/Models/RobotPeripheral.swift b/Modules/BLEKit/Sources/Models/RobotPeripheral.swift index fdef3b0c49..0e585d545b 100644 --- a/Modules/BLEKit/Sources/Models/RobotPeripheral.swift +++ b/Modules/BLEKit/Sources/Models/RobotPeripheral.swift @@ -5,16 +5,7 @@ import CombineCoreBluetooth public class RobotPeripheral: Equatable { - // MARK: - Public variables - - // TODO(@ladislas): should they be published? maybe, need to investigate - public var peripheral: Peripheral - public var notifyingCharacteristics: Set = [] - public var readOnlyCharacteristics: Set = [] - - // MARK: - Private variables - - private var cancellables: Set = [] + // MARK: Lifecycle // MARK: - Public functions @@ -22,6 +13,15 @@ public class RobotPeripheral: Equatable { self.peripheral = peripheral } + // MARK: Public + + // MARK: - Public variables + + // TODO(@ladislas): should they be published? maybe, need to investigate + public var peripheral: Peripheral + public var notifyingCharacteristics: Set = [] + public var readOnlyCharacteristics: Set = [] + public static func == (lhs: RobotPeripheral, rhs: RobotPeripheral) -> Bool { lhs.peripheral.id == rhs.peripheral.id } @@ -123,6 +123,12 @@ public class RobotPeripheral: Equatable { .store(in: &cancellables) } + // MARK: Private + + // MARK: - Private variables + + private var cancellables: Set = [] + // MARK: - Private functions private func listenForUpdates(on characteristic: CharacteristicModelNotifying) { diff --git a/Modules/ContentKit/Sources/Activity/Activity.swift b/Modules/ContentKit/Sources/Activity/Activity.swift index 2604e22586..ff3ac1d939 100644 --- a/Modules/ContentKit/Sources/Activity/Activity.swift +++ b/Modules/ContentKit/Sources/Activity/Activity.swift @@ -6,19 +6,7 @@ import Foundation import Yams public struct Activity: Codable, Identifiable { - public let id: String - public let name: String - public let description: String - public let image: String - public let shuffleExercises: Bool - public let shuffleSequences: Bool - public var sequence: [Exercise.Sequence] - - private enum CodingKeys: String, CodingKey { - case id, name, description, image, sequence - case shuffleExercises = "shuffle_exercises" - case shuffleSequences = "shuffle_sequences" - } + // MARK: Lifecycle public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) @@ -32,4 +20,22 @@ public struct Activity: Codable, Identifiable { self.shuffleExercises = try container.decodeIfPresent(Bool.self, forKey: .shuffleExercises) ?? false self.shuffleSequences = try container.decodeIfPresent(Bool.self, forKey: .shuffleSequences) ?? false } + + // MARK: Public + + public let id: String + public let name: String + public let description: String + public let image: String + public let shuffleExercises: Bool + public let shuffleSequences: Bool + public var sequence: [Exercise.Sequence] + + // MARK: Private + + private enum CodingKeys: String, CodingKey { + case id, name, description, image, sequence + case shuffleExercises = "shuffle_exercises" + case shuffleSequences = "shuffle_sequences" + } } diff --git a/Modules/ContentKit/Sources/Activity/ActivitySequenceManager.swift b/Modules/ContentKit/Sources/Activity/ActivitySequenceManager.swift index 5c4e217815..cfc1616569 100644 --- a/Modules/ContentKit/Sources/Activity/ActivitySequenceManager.swift +++ b/Modules/ContentKit/Sources/Activity/ActivitySequenceManager.swift @@ -3,10 +3,7 @@ // SPDX-License-Identifier: Apache-2.0 public class ActivitySequenceManager { - private let activity: Activity - - public var currentSequenceIndex: Int = 0 - public var currentExerciseIndexInSequence: Int = 0 + // MARK: Lifecycle public init(activity: Activity) { var localActivity = activity @@ -24,6 +21,11 @@ public class ActivitySequenceManager { self.activity = localActivity } + // MARK: Public + + public var currentSequenceIndex: Int = 0 + public var currentExerciseIndexInSequence: Int = 0 + public var totalSequences: Int { activity.sequence.count } @@ -62,4 +64,8 @@ public class ActivitySequenceManager { currentExerciseIndexInSequence = activity.sequence[currentSequenceIndex].exercises.count - 1 } } + + // MARK: Private + + private let activity: Activity } diff --git a/Modules/ContentKit/Sources/ContentKit.swift b/Modules/ContentKit/Sources/ContentKit.swift index 6f2b6fb94f..dc1a7ee8f7 100644 --- a/Modules/ContentKit/Sources/ContentKit.swift +++ b/Modules/ContentKit/Sources/ContentKit.swift @@ -11,14 +11,18 @@ let log = LogKit.createLoggerFor(module: "ContentKit") // MARK: - ContentKit public class ContentKit { - public var shared: ContentKit { - ContentKit() - } + // MARK: Lifecycle private init() { // nothing to do } + // MARK: Public + + public var shared: ContentKit { + ContentKit() + } + // TODO(@ladislas): maybe return optional activity instead of fatalError public static func decodeActivity(_ filename: String) -> Activity { do { diff --git a/Modules/ContentKit/Sources/Exercise/Exercice+AudioRecordingPlayer.swift b/Modules/ContentKit/Sources/Exercise/Exercice+AudioRecordingPlayer.swift index 3e99a4be87..3a35450d3b 100644 --- a/Modules/ContentKit/Sources/Exercise/Exercice+AudioRecordingPlayer.swift +++ b/Modules/ContentKit/Sources/Exercise/Exercice+AudioRecordingPlayer.swift @@ -4,11 +4,7 @@ public enum AudioRecordingPlayer { public struct Payload: Codable { - public let songs: [AudioRecording] - - enum CodingKeys: String, CodingKey { - case songs - } + // MARK: Lifecycle public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) @@ -16,5 +12,15 @@ public enum AudioRecordingPlayer { [AudioRecording.Song].self, forKey: .songs) self.songs = audioRecordingSongs.map { AudioRecording($0) } } + + // MARK: Public + + public let songs: [AudioRecording] + + // MARK: Internal + + enum CodingKeys: String, CodingKey { + case songs + } } } diff --git a/Modules/ContentKit/Sources/Exercise/Exercice+HideAndSeek.swift b/Modules/ContentKit/Sources/Exercise/Exercice+HideAndSeek.swift index a0acea4046..06958487b4 100644 --- a/Modules/ContentKit/Sources/Exercise/Exercice+HideAndSeek.swift +++ b/Modules/ContentKit/Sources/Exercise/Exercice+HideAndSeek.swift @@ -4,38 +4,50 @@ public enum HideAndSeek { public struct Payload: Codable { + // MARK: Lifecycle + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.instructions = try container.decode(Instructions.self, forKey: .instructions) + } + + // MARK: Public + public struct Instructions: Codable { + // MARK: Lifecycle + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + self.textMainInstructions = try container.decode(String.self, forKey: .textMainInstructions) + self.textSubInstructions = try container.decode(String.self, forKey: .textSubInstructions) + self.textButtonOk = try container.decode(String.self, forKey: .textButtonOk) + self.textButtonRobotFound = try container.decode(String.self, forKey: .textButtonRobotFound) + } + + // MARK: Public + public let textMainInstructions: String public let textSubInstructions: String public let textButtonOk: String public let textButtonRobotFound: String + // MARK: Internal + enum CodingKeys: String, CodingKey { case textMainInstructions = "text_main_instructions" case textSubInstructions = "text_sub_instructions" case textButtonOk = "text_button_ok" case textButtonRobotFound = "text_button_robot_found" } - - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - - self.textMainInstructions = try container.decode(String.self, forKey: .textMainInstructions) - self.textSubInstructions = try container.decode(String.self, forKey: .textSubInstructions) - self.textButtonOk = try container.decode(String.self, forKey: .textButtonOk) - self.textButtonRobotFound = try container.decode(String.self, forKey: .textButtonRobotFound) - } } public let instructions: Instructions + // MARK: Internal + enum CodingKeys: String, CodingKey { case instructions } - - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - self.instructions = try container.decode(Instructions.self, forKey: .instructions) - } } } diff --git a/Modules/ContentKit/Sources/Exercise/Exercice+MusicalInstrument.swift b/Modules/ContentKit/Sources/Exercise/Exercice+MusicalInstrument.swift index 3845fc9cc4..f8cb2e5446 100644 --- a/Modules/ContentKit/Sources/Exercise/Exercice+MusicalInstrument.swift +++ b/Modules/ContentKit/Sources/Exercise/Exercice+MusicalInstrument.swift @@ -4,12 +4,7 @@ public enum MusicalInstrument { public struct Payload: Codable { - public let instrument: String - public let scale: String - - enum CodingKeys: String, CodingKey { - case instrument, scale - } + // MARK: Lifecycle public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) @@ -17,5 +12,16 @@ public enum MusicalInstrument { self.instrument = try container.decode(String.self, forKey: .instrument) self.scale = try container.decode(String.self, forKey: .scale) } + + // MARK: Public + + public let instrument: String + public let scale: String + + // MARK: Internal + + enum CodingKeys: String, CodingKey { + case instrument, scale + } } } diff --git a/Modules/ContentKit/Sources/Exercise/Exercise+Action.swift b/Modules/ContentKit/Sources/Exercise/Exercise+Action.swift index 20986b5e46..16c6f1f851 100644 --- a/Modules/ContentKit/Sources/Exercise/Exercise+Action.swift +++ b/Modules/ContentKit/Sources/Exercise/Exercise+Action.swift @@ -11,6 +11,56 @@ public extension Exercise { case ipad(type: ActionType) case robot(type: ActionType) + // MARK: Lifecycle + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let type = try container.decode(String.self, forKey: .type) + let valueContainer = try container.nestedContainer(keyedBy: CodingKeys.self, forKey: .value) + switch type { + case "ipad": + let valueType = try valueContainer.decode(ValueType.self, forKey: .type) + switch valueType { + case .color: + let color = try valueContainer.decode(String.self, forKey: .value) + self = .ipad(type: .color(color)) + case .image: + let image = try valueContainer.decode(String.self, forKey: .value) + self = .ipad(type: .image(image)) + case .audio: + let audio = try valueContainer.decode(String.self, forKey: .value) + self = .ipad(type: .audio(audio)) + case .speech: + let speech = try valueContainer.decode(String.self, forKey: .value) + self = .ipad(type: .speech(speech)) + } + case "robot": + let valueType = try valueContainer.decode(ValueType.self, forKey: .type) + switch valueType { + case .image: + let image = try valueContainer.decode(String.self, forKey: .value) + self = .robot(type: .image(image)) + case .color: + let color = try valueContainer.decode(String.self, forKey: .value) + self = .robot(type: .color(color)) + default: + throw DecodingError.dataCorruptedError( + forKey: .type, + in: valueContainer, + debugDescription: "Unexpected type for RobotMedia") + } + default: + throw DecodingError.dataCorruptedError( + forKey: .type, + in: container, + debugDescription: + "Cannot decode ExercisePayload. Available keys: \(container.allKeys.map { $0.stringValue })" + ) + } + } + + // MARK: Public + public enum ActionType: Codable { case color(String) case image(String) @@ -18,11 +68,6 @@ public extension Exercise { case speech(String) } - private enum CodingKeys: String, CodingKey { - case type - case value - } - public enum ValueType: String, Codable { case color case image @@ -71,50 +116,11 @@ public extension Exercise { } } - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - let type = try container.decode(String.self, forKey: .type) - let valueContainer = try container.nestedContainer(keyedBy: CodingKeys.self, forKey: .value) - switch type { - case "ipad": - let valueType = try valueContainer.decode(ValueType.self, forKey: .type) - switch valueType { - case .color: - let color = try valueContainer.decode(String.self, forKey: .value) - self = .ipad(type: .color(color)) - case .image: - let image = try valueContainer.decode(String.self, forKey: .value) - self = .ipad(type: .image(image)) - case .audio: - let audio = try valueContainer.decode(String.self, forKey: .value) - self = .ipad(type: .audio(audio)) - case .speech: - let speech = try valueContainer.decode(String.self, forKey: .value) - self = .ipad(type: .speech(speech)) - } - case "robot": - let valueType = try valueContainer.decode(ValueType.self, forKey: .type) - switch valueType { - case .image: - let image = try valueContainer.decode(String.self, forKey: .value) - self = .robot(type: .image(image)) - case .color: - let color = try valueContainer.decode(String.self, forKey: .value) - self = .robot(type: .color(color)) - default: - throw DecodingError.dataCorruptedError( - forKey: .type, - in: valueContainer, - debugDescription: "Unexpected type for RobotMedia") - } - default: - throw DecodingError.dataCorruptedError( - forKey: .type, - in: container, - debugDescription: - "Cannot decode ExercisePayload. Available keys: \(container.allKeys.map { $0.stringValue })" - ) - } + // MARK: Private + + private enum CodingKeys: String, CodingKey { + case type + case value } } } diff --git a/Modules/ContentKit/Sources/Exercise/Exercise+DragAndDropIntoZones.swift b/Modules/ContentKit/Sources/Exercise/Exercise+DragAndDropIntoZones.swift index 3c1789b578..fa69559818 100644 --- a/Modules/ContentKit/Sources/Exercise/Exercise+DragAndDropIntoZones.swift +++ b/Modules/ContentKit/Sources/Exercise/Exercise+DragAndDropIntoZones.swift @@ -9,6 +9,8 @@ public enum DragAndDropIntoZones { case zoneA case zoneB + // MARK: Public + public struct Details: Codable { public let value: String public let type: Exercise.UIElementType diff --git a/Modules/ContentKit/Sources/Exercise/Exercise+DragAndDropToAssociate.swift b/Modules/ContentKit/Sources/Exercise/Exercise+DragAndDropToAssociate.swift index 0d147e7872..6e845b5ff5 100644 --- a/Modules/ContentKit/Sources/Exercise/Exercise+DragAndDropToAssociate.swift +++ b/Modules/ContentKit/Sources/Exercise/Exercise+DragAndDropToAssociate.swift @@ -12,13 +12,7 @@ public enum DragAndDropToAssociate { } public struct Choice: Codable { - public let value: String - public let type: Exercise.UIElementType - public let category: Category? - - private enum CodingKeys: String, CodingKey { - case value, type, category - } + // MARK: Lifecycle public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) @@ -32,16 +26,22 @@ public enum DragAndDropToAssociate { self.type = type self.category = category } - } - public struct Payload: Codable { - public let choices: [Choice] - public let shuffleChoices: Bool + // MARK: Public - enum CodingKeys: String, CodingKey { - case choices - case shuffleChoices = "shuffle_choices" + public let value: String + public let type: Exercise.UIElementType + public let category: Category? + + // MARK: Private + + private enum CodingKeys: String, CodingKey { + case value, type, category } + } + + public struct Payload: Codable { + // MARK: Lifecycle public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) @@ -49,6 +49,18 @@ public enum DragAndDropToAssociate { self.choices = try container.decode([Choice].self, forKey: .choices) self.shuffleChoices = try container.decodeIfPresent(Bool.self, forKey: .shuffleChoices) ?? false } + + // MARK: Public + + public let choices: [Choice] + public let shuffleChoices: Bool + + // MARK: Internal + + enum CodingKeys: String, CodingKey { + case choices + case shuffleChoices = "shuffle_choices" + } } } diff --git a/Modules/ContentKit/Sources/Exercise/Exercise+TouchToSelect.swift b/Modules/ContentKit/Sources/Exercise/Exercise+TouchToSelect.swift index ad09f26998..d676f6f58f 100644 --- a/Modules/ContentKit/Sources/Exercise/Exercise+TouchToSelect.swift +++ b/Modules/ContentKit/Sources/Exercise/Exercise+TouchToSelect.swift @@ -6,13 +6,7 @@ public enum TouchToSelect { public struct Choice: Codable { - public let value: String - public let type: Exercise.UIElementType - public let isRightAnswer: Bool - - private enum CodingKeys: String, CodingKey { - case value, type, isRightAnswer - } + // MARK: Lifecycle public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) @@ -26,16 +20,22 @@ public enum TouchToSelect { self.type = type self.isRightAnswer = isRightAnswer } - } - public struct Payload: Codable { - public let choices: [Choice] - public let shuffleChoices: Bool + // MARK: Public - enum CodingKeys: String, CodingKey { - case choices - case shuffleChoices = "shuffle_choices" + public let value: String + public let type: Exercise.UIElementType + public let isRightAnswer: Bool + + // MARK: Private + + private enum CodingKeys: String, CodingKey { + case value, type, isRightAnswer } + } + + public struct Payload: Codable { + // MARK: Lifecycle public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) @@ -43,6 +43,18 @@ public enum TouchToSelect { self.shuffleChoices = try container.decodeIfPresent(Bool.self, forKey: .shuffleChoices) ?? false } + + // MARK: Public + + public let choices: [Choice] + public let shuffleChoices: Bool + + // MARK: Internal + + enum CodingKeys: String, CodingKey { + case choices + case shuffleChoices = "shuffle_choices" + } } } diff --git a/Modules/ContentKit/Sources/Exercise/Exercise.swift b/Modules/ContentKit/Sources/Exercise/Exercise.swift index b8b7ba939c..bdc1a6a34c 100644 --- a/Modules/ContentKit/Sources/Exercise/Exercise.swift +++ b/Modules/ContentKit/Sources/Exercise/Exercise.swift @@ -5,19 +5,7 @@ import Foundation public struct Exercise: Codable { - public let instructions: String - public let interface: Interface - public let gameplay: Gameplay? - public let action: Action? - public let payload: ExercisePayloadProtocol? - - enum CodingKeys: String, CodingKey { - case instructions, interface, gameplay, action, payload - } - - public func encode(to encoder: Encoder) throws { - fatalError("💥 Not implemented yet") - } + // MARK: Lifecycle public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) @@ -57,4 +45,22 @@ public struct Exercise: Codable { forKey: .payload, in: container, debugDescription: "Invalid combination of interface or gameplay") } } + + // MARK: Public + + public let instructions: String + public let interface: Interface + public let gameplay: Gameplay? + public let action: Action? + public let payload: ExercisePayloadProtocol? + + public func encode(to encoder: Encoder) throws { + fatalError("💥 Not implemented yet") + } + + // MARK: Internal + + enum CodingKeys: String, CodingKey { + case instructions, interface, gameplay, action, payload + } } diff --git a/Modules/ContentKit/Sources/Utils/AudioRecording.swift b/Modules/ContentKit/Sources/Utils/AudioRecording.swift index 511295b0fc..926272e7b1 100644 --- a/Modules/ContentKit/Sources/Utils/AudioRecording.swift +++ b/Modules/ContentKit/Sources/Utils/AudioRecording.swift @@ -5,6 +5,20 @@ import Foundation public struct AudioRecording: Codable, Hashable, Equatable { + // MARK: Lifecycle + + public init(name: String, file: String) { + self.name = name + self.file = file + } + + public init(_ song: Song) { + self.name = song.details.name + self.file = song.details.file + } + + // MARK: Public + public enum Song: String, Codable { case earlyBird case emptyPage @@ -14,6 +28,8 @@ public struct AudioRecording: Codable, Hashable, Equatable { case inTheGame case littleByLittle + // MARK: Internal + var details: (name: String, file: String) { switch self { case .earlyBird: @@ -36,14 +52,4 @@ public struct AudioRecording: Codable, Hashable, Equatable { public let name: String public let file: String - - public init(name: String, file: String) { - self.name = name - self.file = file - } - - public init(_ song: Song) { - self.name = song.details.name - self.file = song.details.file - } } diff --git a/Modules/DesignKit/Sources/ContentView.swift b/Modules/DesignKit/Sources/ContentView.swift index 9a7cd7d901..23508ef476 100644 --- a/Modules/DesignKit/Sources/ContentView.swift +++ b/Modules/DesignKit/Sources/ContentView.swift @@ -7,14 +7,15 @@ import SwiftUI // MARK: - Hello public struct Hello: View { - let name: String - let color: Color + // MARK: Lifecycle public init(_ name: String, in color: Color) { self.name = name self.color = color } + // MARK: Public + public var body: some View { VStack { Text("Hello, \(name)!") @@ -27,6 +28,11 @@ public struct Hello: View { .frame(width: 200.0) } } + + // MARK: Internal + + let name: String + let color: Color } // MARK: - ContentView_Previews diff --git a/Modules/DesignKit/Sources/LottieView.swift b/Modules/DesignKit/Sources/LottieView.swift index da6afc328b..24161ef1a0 100644 --- a/Modules/DesignKit/Sources/LottieView.swift +++ b/Modules/DesignKit/Sources/LottieView.swift @@ -6,18 +6,7 @@ import Lottie import SwiftUI public struct LottieView: UIViewRepresentable { - public typealias UIViewType = UIView - - public func makeCoordinator() -> Coordinator { - Coordinator(self) - } - - var name: String! - var speed: CGFloat - var reverse: Bool - var loopMode: LottieLoopMode - var action: () -> Void - @Binding var play: Bool + // MARK: Lifecycle public init( name: String, @@ -37,15 +26,25 @@ public struct LottieView: UIViewRepresentable { self._play = play } - var animationView = LottieAnimationView() + // MARK: Public + + public typealias UIViewType = UIView public class Coordinator: NSObject { - var parent: LottieView + // MARK: Lifecycle init(_ animationView: LottieView) { self.parent = animationView super.init() } + + // MARK: Internal + + var parent: LottieView + } + + public func makeCoordinator() -> Coordinator { + Coordinator(self) } public func makeUIView(context: UIViewRepresentableContext) -> UIView { @@ -94,4 +93,15 @@ public struct LottieView: UIViewRepresentable { } } } + + // MARK: Internal + + var name: String! + var speed: CGFloat + var reverse: Bool + var loopMode: LottieLoopMode + var action: () -> Void + @Binding var play: Bool + + var animationView = LottieAnimationView() } diff --git a/Modules/DesignKit/Sources/Modifiers/AlertWhenNoUserSelected.swift b/Modules/DesignKit/Sources/Modifiers/AlertWhenNoUserSelected.swift index 4c61d2d6b5..5d37f490d6 100644 --- a/Modules/DesignKit/Sources/Modifiers/AlertWhenNoUserSelected.swift +++ b/Modules/DesignKit/Sources/Modifiers/AlertWhenNoUserSelected.swift @@ -7,7 +7,13 @@ import SwiftUI // MARK: - AlertWhenNoUserSelected struct AlertWhenNoUserSelected: ViewModifier { - @State private var showAlert: Bool = false + // MARK: Lifecycle + + public init() { + // nothing to do + } + + // MARK: Internal func body(content: Content) -> some View { content @@ -21,9 +27,9 @@ struct AlertWhenNoUserSelected: ViewModifier { } } - public init() { - // nothing to do - } + // MARK: Private + + @State private var showAlert: Bool = false private var alertContent: some View { Group { diff --git a/Modules/DesignKit/Sources/Modifiers/AlertWhenRobotIsNeeded.swift b/Modules/DesignKit/Sources/Modifiers/AlertWhenRobotIsNeeded.swift index 83b4f114b6..d48ef8186d 100644 --- a/Modules/DesignKit/Sources/Modifiers/AlertWhenRobotIsNeeded.swift +++ b/Modules/DesignKit/Sources/Modifiers/AlertWhenRobotIsNeeded.swift @@ -7,7 +7,13 @@ import SwiftUI // MARK: - AlertWhenRobotIsNeeded struct AlertWhenRobotIsNeeded: ViewModifier { - @State private var showAlert: Bool = false + // MARK: Lifecycle + + public init() { + // nothing to do + } + + // MARK: Internal func body(content: Content) -> some View { content @@ -21,9 +27,9 @@ struct AlertWhenRobotIsNeeded: ViewModifier { } } - public init() { - // nothing to do - } + // MARK: Private + + @State private var showAlert: Bool = false private var alertContent: some View { Group { diff --git a/Modules/GameEngineKit/Sources/Staging/Melody/MelodyGameplay.swift b/Modules/GameEngineKit/Sources/Staging/Melody/MelodyGameplay.swift index 6d926a9673..975aeb7c7c 100644 --- a/Modules/GameEngineKit/Sources/Staging/Melody/MelodyGameplay.swift +++ b/Modules/GameEngineKit/Sources/Staging/Melody/MelodyGameplay.swift @@ -8,16 +8,7 @@ import ContentKit import SwiftUI public class MelodyGameplay: ObservableObject { - @Published public var progress: CGFloat = 0.0 - @Published var state: ExerciseState = .idle - - private let xyloPlayer = MIDIPlayer(instrument: .xylophone) - - private let song: MelodySongModel - - private var midiNotes: [MIDINoteData] = [] - private var currentNoteNumber: MIDINoteNumber = 0 - private var currentNoteIndex: Int = 0 + // MARK: Lifecycle public init(song: MelodySongModel) { self.song = song @@ -30,6 +21,14 @@ public class MelodyGameplay: ObservableObject { print(getColorFromMIDINote()) } + // MARK: Public + + @Published public var progress: CGFloat = 0.0 + + // MARK: Internal + + @Published var state: ExerciseState = .idle + func process(tile: XylophoneTile) { guard currentNoteIndex < midiNotes.count else { return } @@ -70,4 +69,14 @@ public class MelodyGameplay: ObservableObject { }) return kListOfTiles[index!].color.screen } + + // MARK: Private + + private let xyloPlayer = MIDIPlayer(instrument: .xylophone) + + private let song: MelodySongModel + + private var midiNotes: [MIDINoteData] = [] + private var currentNoteNumber: MIDINoteNumber = 0 + private var currentNoteIndex: Int = 0 } diff --git a/Modules/GameEngineKit/Sources/Staging/Melody/MelodySongModel.swift b/Modules/GameEngineKit/Sources/Staging/Melody/MelodySongModel.swift index 8887184b63..db53e03745 100644 --- a/Modules/GameEngineKit/Sources/Staging/Melody/MelodySongModel.swift +++ b/Modules/GameEngineKit/Sources/Staging/Melody/MelodySongModel.swift @@ -7,10 +7,7 @@ import SwiftUI // MARK: - MelodySongModel public struct MelodySongModel { - let midiFile: URL - let title: String - let tempo: Double - let octaveGap: UInt8 + // MARK: Lifecycle public init(midiFile: URL, title: String, tempo: Double, octaveGap: UInt8) { self.midiFile = midiFile @@ -18,6 +15,13 @@ public struct MelodySongModel { self.tempo = tempo self.octaveGap = octaveGap } + + // MARK: Internal + + let midiFile: URL + let title: String + let tempo: Double + let octaveGap: UInt8 } public let kListOfMelodySongsAvailable: [MelodySongModel] = [ diff --git a/Modules/GameEngineKit/Sources/Staging/Melody/MelodyView.swift b/Modules/GameEngineKit/Sources/Staging/Melody/MelodyView.swift index 0904d23f4d..2ca7569e63 100644 --- a/Modules/GameEngineKit/Sources/Staging/Melody/MelodyView.swift +++ b/Modules/GameEngineKit/Sources/Staging/Melody/MelodyView.swift @@ -9,7 +9,12 @@ import SwiftUI // MARK: - XylophoneTile public struct XylophoneTile: Identifiable { + // MARK: Public + public var id: Int + + // MARK: Internal + var noteNumber: MIDINoteNumber var color: Robot.Color } @@ -27,15 +32,14 @@ public let kListOfTiles: [XylophoneTile] = [ // MARK: - MelodyView public struct MelodyView: View { - @ObservedObject private var viewModel: MelodyViewModel - - let defaultTilesSpacing: CGFloat = 16 - let tileNumber = 7 + // MARK: Lifecycle public init(gameplay: MelodyGameplay) { self.viewModel = MelodyViewModel(gameplay: gameplay) } + // MARK: Public + public var body: some View { VStack(spacing: 50) { ContinuousProgressBar(progress: viewModel.progress) @@ -58,6 +62,15 @@ public struct MelodyView: View { } } } + + // MARK: Internal + + let defaultTilesSpacing: CGFloat = 16 + let tileNumber = 7 + + // MARK: Private + + @ObservedObject private var viewModel: MelodyViewModel } // MARK: - MelodyView_Previews diff --git a/Modules/GameEngineKit/Sources/Staging/Melody/MelodyViewModel.swift b/Modules/GameEngineKit/Sources/Staging/Melody/MelodyViewModel.swift index 68adf1bbbe..8909230534 100644 --- a/Modules/GameEngineKit/Sources/Staging/Melody/MelodyViewModel.swift +++ b/Modules/GameEngineKit/Sources/Staging/Melody/MelodyViewModel.swift @@ -6,12 +6,7 @@ import Combine import SwiftUI public class MelodyViewModel: Identifiable, ObservableObject { - public var gameplay: MelodyGameplay - - @Published public var progress: CGFloat - @Published var state: ExerciseState - - private var cancellables: Set = [] + // MARK: Lifecycle public init(gameplay: MelodyGameplay) { self.gameplay = gameplay @@ -36,7 +31,21 @@ public class MelodyViewModel: Identifiable, ObservableObject { .store(in: &cancellables) } + // MARK: Public + + public var gameplay: MelodyGameplay + + @Published public var progress: CGFloat + public func onTileTapped(tile: XylophoneTile) { gameplay.process(tile: tile) } + + // MARK: Internal + + @Published var state: ExerciseState + + // MARK: Private + + private var cancellables: Set = [] } diff --git a/Modules/GameEngineKit/Sources/Staging/Pairing/PairingView.swift b/Modules/GameEngineKit/Sources/Staging/Pairing/PairingView.swift index db88d3f53e..b54bde5c1b 100644 --- a/Modules/GameEngineKit/Sources/Staging/Pairing/PairingView.swift +++ b/Modules/GameEngineKit/Sources/Staging/Pairing/PairingView.swift @@ -10,6 +10,8 @@ import SwiftUI private enum Action { case play, pause, stop + // MARK: Public + public func icon(_ stopButtonDisabled: Bool) -> some View { switch self { case .play: @@ -46,13 +48,14 @@ private enum Action { // MARK: - PairingView public struct PairingView: View { - @State private var stopButtonDisabled: Bool = true - @State private var playButtonVisible: Bool = true + // MARK: Lifecycle public init() { // Nothing to do } + // MARK: Public + public var body: some View { VStack { Text( @@ -82,6 +85,11 @@ public struct PairingView: View { } } + // MARK: Private + + @State private var stopButtonDisabled: Bool = true + @State private var playButtonVisible: Bool = true + private func actionButton(_ action: Action) -> some View { VStack { Button { diff --git a/Modules/GameEngineKit/Sources/_NewSystem/Exercises/DragAndDrop/DraggableImageAnswerNode.swift b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/DragAndDrop/DraggableImageAnswerNode.swift index 14079ea4a1..3711cff513 100644 --- a/Modules/GameEngineKit/Sources/_NewSystem/Exercises/DragAndDrop/DraggableImageAnswerNode.swift +++ b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/DragAndDrop/DraggableImageAnswerNode.swift @@ -6,9 +6,7 @@ import ContentKit import SpriteKit class DraggableImageAnswerNode: SKSpriteNode { - var id: String - var isDraggable: Bool = true - var defaultPosition: CGPoint? + // MARK: Lifecycle init(choice: GameplayAssociateCategoriesChoiceModel, scale: CGFloat = 1, position: CGPoint) { self.id = choice.id @@ -46,4 +44,10 @@ class DraggableImageAnswerNode: SKSpriteNode { required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } + + // MARK: Internal + + var id: String + var isDraggable: Bool = true + var defaultPosition: CGPoint? } diff --git a/Modules/GameEngineKit/Sources/_NewSystem/Exercises/DragAndDrop/IntoZones/DragAndDropIntoZonesView+0_BaseScene.swift b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/DragAndDrop/IntoZones/DragAndDropIntoZonesView+0_BaseScene.swift index e1b681f86c..e591c908cb 100644 --- a/Modules/GameEngineKit/Sources/_NewSystem/Exercises/DragAndDrop/IntoZones/DragAndDropIntoZonesView+0_BaseScene.swift +++ b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/DragAndDrop/IntoZones/DragAndDropIntoZonesView+0_BaseScene.swift @@ -15,19 +15,7 @@ extension DragAndDropIntoZonesView { } class BaseScene: SKScene { - var viewModel: ViewModel - var dropZoneA: DropZoneNode - var dropZoneB: DropZoneNode? - - private var hints: Bool - private var biggerSide: CGFloat = 130 - private var selectedNodes: [UITouch: DraggableImageAnswerNode] = [:] - private var answerNodes: [DraggableImageAnswerNode] = [] - private var playedNode: DraggableImageAnswerNode? - private var spacer: CGFloat = .zero - private var defaultPosition = CGPoint.zero - private var expectedItemsNodes: [String: [SKSpriteNode]] = [:] - private var cancellables: Set = [] + // MARK: Lifecycle init( viewModel: ViewModel, hints: Bool, dropZoneA: DragAndDropIntoZones.DropZone.Details, @@ -51,6 +39,12 @@ extension DragAndDropIntoZonesView { fatalError("init(coder:) has not been implemented") } + // MARK: Internal + + var viewModel: ViewModel + var dropZoneA: DropZoneNode + var dropZoneB: DropZoneNode? + func reset() { self.backgroundColor = .clear self.removeAllChildren() @@ -203,14 +197,6 @@ extension DragAndDropIntoZonesView { selectedNodes = [:] } - private func disableWrongAnswer(_ node: DraggableImageAnswerNode) { - let gameplayChoiceModel = viewModel.choices.first(where: { $0.id == node.id })! - if gameplayChoiceModel.choice.dropZone == nil { - node.colorBlendFactor = 0.4 - node.isDraggable = false - } - } - override func didMove(to view: SKView) { self.reset() } @@ -269,5 +255,25 @@ extension DragAndDropIntoZonesView { wrongAnswerBehavior(playedNode!) } } + + // MARK: Private + + private var hints: Bool + private var biggerSide: CGFloat = 130 + private var selectedNodes: [UITouch: DraggableImageAnswerNode] = [:] + private var answerNodes: [DraggableImageAnswerNode] = [] + private var playedNode: DraggableImageAnswerNode? + private var spacer: CGFloat = .zero + private var defaultPosition = CGPoint.zero + private var expectedItemsNodes: [String: [SKSpriteNode]] = [:] + private var cancellables: Set = [] + + private func disableWrongAnswer(_ node: DraggableImageAnswerNode) { + let gameplayChoiceModel = viewModel.choices.first(where: { $0.id == node.id })! + if gameplayChoiceModel.choice.dropZone == nil { + node.colorBlendFactor = 0.4 + node.isDraggable = false + } + } } } diff --git a/Modules/GameEngineKit/Sources/_NewSystem/Exercises/DragAndDrop/IntoZones/DragAndDropIntoZonesView+ViewModel.swift b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/DragAndDrop/IntoZones/DragAndDropIntoZonesView+ViewModel.swift index c73352a48e..cae55d99d4 100644 --- a/Modules/GameEngineKit/Sources/_NewSystem/Exercises/DragAndDrop/IntoZones/DragAndDropIntoZonesView+ViewModel.swift +++ b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/DragAndDrop/IntoZones/DragAndDropIntoZonesView+ViewModel.swift @@ -8,11 +8,7 @@ import SwiftUI extension DragAndDropIntoZonesView { class ViewModel: ObservableObject { - @Published var choices: [GameplayDragAndDropIntoZonesChoiceModel] = [] - @ObservedObject var exercicesSharedData: ExerciseSharedData - - private let gameplay: GameplayFindTheRightAnswers - private var cancellables: Set = [] + // MARK: Lifecycle init(choices: [DragAndDropIntoZones.Choice], shared: ExerciseSharedData? = nil) { let gameplayChoiceModel = choices.map { GameplayDragAndDropIntoZonesChoiceModel(choice: $0) } @@ -25,12 +21,24 @@ extension DragAndDropIntoZonesView { subscribeToGameplayStateUpdates() } + // MARK: Public + public func onChoiceTapped( choice: GameplayDragAndDropIntoZonesChoiceModel, dropZone: DragAndDropIntoZones.DropZone ) { gameplay.process(choice, dropZone) } + // MARK: Internal + + @Published var choices: [GameplayDragAndDropIntoZonesChoiceModel] = [] + @ObservedObject var exercicesSharedData: ExerciseSharedData + + // MARK: Private + + private let gameplay: GameplayFindTheRightAnswers + private var cancellables: Set = [] + private func subscribeToGameplayDragAndDropChoicesUpdates() { gameplay.choices .receive(on: DispatchQueue.main) diff --git a/Modules/GameEngineKit/Sources/_NewSystem/Exercises/DragAndDrop/IntoZones/DragAndDropIntoZonesView.swift b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/DragAndDrop/IntoZones/DragAndDropIntoZonesView.swift index 50a343e49b..2d40064f3d 100644 --- a/Modules/GameEngineKit/Sources/_NewSystem/Exercises/DragAndDrop/IntoZones/DragAndDropIntoZonesView.swift +++ b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/DragAndDrop/IntoZones/DragAndDropIntoZonesView.swift @@ -8,10 +8,8 @@ import SpriteKit import SwiftUI public struct DragAndDropIntoZonesView: View { - @StateObject private var viewModel: ViewModel - @State private var scene: SKScene = SKScene() - let dropZoneA: DragAndDropIntoZones.DropZone.Details - let dropZoneB: DragAndDropIntoZones.DropZone.Details? + // MARK: Lifecycle + // TODO(@HPezz): Add hints variable // let hints: Bool @@ -37,6 +35,8 @@ public struct DragAndDropIntoZonesView: View { self.dropZoneB = payload.dropZoneB } + // MARK: Public + public var body: some View { GeometryReader { proxy in SpriteView( @@ -57,6 +57,16 @@ public struct DragAndDropIntoZonesView: View { .edgesIgnoringSafeArea(.horizontal) } + // MARK: Internal + + let dropZoneA: DragAndDropIntoZones.DropZone.Details + let dropZoneB: DragAndDropIntoZones.DropZone.Details? + + // MARK: Private + + @StateObject private var viewModel: ViewModel + @State private var scene: SKScene = SKScene() + private func makeScene(size: CGSize) -> SKScene { guard let finalScene = scene as? DragAndDropIntoZonesView.BaseScene else { return SKScene() diff --git a/Modules/GameEngineKit/Sources/_NewSystem/Exercises/DragAndDrop/ToAssociate/DragAndDropToAssociateView+0_BaseScene.swift b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/DragAndDrop/ToAssociate/DragAndDropToAssociateView+0_BaseScene.swift index 0fe871ce2f..9bfa4b126a 100644 --- a/Modules/GameEngineKit/Sources/_NewSystem/Exercises/DragAndDrop/ToAssociate/DragAndDropToAssociateView+0_BaseScene.swift +++ b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/DragAndDrop/ToAssociate/DragAndDropToAssociateView+0_BaseScene.swift @@ -9,19 +9,7 @@ import SwiftUI extension DragAndDropToAssociateView { class BaseScene: SKScene { - var viewModel: ViewModel - var spacer = CGFloat.zero - var defaultPosition = CGPoint.zero - var initialNodeX: CGFloat = .zero - var verticalSpacing: CGFloat = .zero - - private var biggerSide: CGFloat = 150 - private var playedNode: DraggableImageAnswerNode? - private var playedDestination: DraggableImageAnswerNode? - private var dropDestinations: [DraggableImageAnswerNode] = [] - private var selectedNodes: [UITouch: DraggableImageAnswerNode] = [:] - private var expectedItemsNodes: [String: [SKSpriteNode]] = [:] - private var cancellables: Set = [] + // MARK: Lifecycle init(viewModel: ViewModel) { self.viewModel = viewModel @@ -35,6 +23,14 @@ extension DragAndDropToAssociateView { fatalError("init(coder:) has not been implemented") } + // MARK: Internal + + var viewModel: ViewModel + var spacer = CGFloat.zero + var defaultPosition = CGPoint.zero + var initialNodeX: CGFloat = .zero + var verticalSpacing: CGFloat = .zero + func reset() { self.backgroundColor = .clear self.removeAllChildren() @@ -218,5 +214,15 @@ extension DragAndDropToAssociateView { viewModel.onChoiceTapped(choice: choice, destination: destination) } } + + // MARK: Private + + private var biggerSide: CGFloat = 150 + private var playedNode: DraggableImageAnswerNode? + private var playedDestination: DraggableImageAnswerNode? + private var dropDestinations: [DraggableImageAnswerNode] = [] + private var selectedNodes: [UITouch: DraggableImageAnswerNode] = [:] + private var expectedItemsNodes: [String: [SKSpriteNode]] = [:] + private var cancellables: Set = [] } } diff --git a/Modules/GameEngineKit/Sources/_NewSystem/Exercises/DragAndDrop/ToAssociate/DragAndDropToAssociateView+ViewModel.swift b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/DragAndDrop/ToAssociate/DragAndDropToAssociateView+ViewModel.swift index 59472c8ea9..badd56b5aa 100644 --- a/Modules/GameEngineKit/Sources/_NewSystem/Exercises/DragAndDrop/ToAssociate/DragAndDropToAssociateView+ViewModel.swift +++ b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/DragAndDrop/ToAssociate/DragAndDropToAssociateView+ViewModel.swift @@ -8,11 +8,7 @@ import SwiftUI extension DragAndDropToAssociateView { class ViewModel: ObservableObject { - @Published var choices: [GameplayAssociateCategoriesChoiceModel] = [] - @ObservedObject var exercicesSharedData: ExerciseSharedData - - private let gameplay: GameplayAssociateCategories - private var cancellables: Set = [] + // MARK: Lifecycle init( choices: [DragAndDropToAssociate.Choice], @@ -28,12 +24,24 @@ extension DragAndDropToAssociateView { subscribeToGameplayStateUpdates() } + // MARK: Public + public func onChoiceTapped( choice: GameplayAssociateCategoriesChoiceModel, destination: GameplayAssociateCategoriesChoiceModel ) { gameplay.process(choice, destination) } + // MARK: Internal + + @Published var choices: [GameplayAssociateCategoriesChoiceModel] = [] + @ObservedObject var exercicesSharedData: ExerciseSharedData + + // MARK: Private + + private let gameplay: GameplayAssociateCategories + private var cancellables: Set = [] + private func subscribeToGameplayAssociateCategoriesChoicesUpdates() { gameplay.choices .receive(on: DispatchQueue.main) diff --git a/Modules/GameEngineKit/Sources/_NewSystem/Exercises/DragAndDrop/ToAssociate/DragAndDropToAssociateView.swift b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/DragAndDrop/ToAssociate/DragAndDropToAssociateView.swift index 72e8137994..6977a30551 100644 --- a/Modules/GameEngineKit/Sources/_NewSystem/Exercises/DragAndDrop/ToAssociate/DragAndDropToAssociateView.swift +++ b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/DragAndDrop/ToAssociate/DragAndDropToAssociateView.swift @@ -9,16 +9,7 @@ import SpriteKit import SwiftUI public struct DragAndDropToAssociateView: View { - enum Interface: Int { - case twoChoices = 2 - case threeChoices - case fourChoices - case fiveChoices - case sixChoices - } - - @StateObject private var viewModel: ViewModel - @State private var scene: SKScene = SKScene() + // MARK: Lifecycle public init(choices: [DragAndDropToAssociate.Choice], shuffle: Bool = false) { self._viewModel = StateObject( @@ -40,6 +31,8 @@ public struct DragAndDropToAssociateView: View { ) } + // MARK: Public + public var body: some View { GeometryReader { proxy in SpriteView( @@ -67,6 +60,21 @@ public struct DragAndDropToAssociateView: View { .edgesIgnoringSafeArea(.horizontal) } + // MARK: Internal + + enum Interface: Int { + case twoChoices = 2 + case threeChoices + case fourChoices + case fiveChoices + case sixChoices + } + + // MARK: Private + + @StateObject private var viewModel: ViewModel + @State private var scene: SKScene = SKScene() + private func makeScene(size: CGSize) -> SKScene { guard let finalScene = scene as? BaseScene else { return SKScene() diff --git a/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Specialized/DanceFreeze/DanceFreeze+LauncherView+ButtonStyles.swift b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Specialized/DanceFreeze/DanceFreeze+LauncherView+ButtonStyles.swift index a4e02c9470..280effc3c0 100644 --- a/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Specialized/DanceFreeze/DanceFreeze+LauncherView+ButtonStyles.swift +++ b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Specialized/DanceFreeze/DanceFreeze+LauncherView+ButtonStyles.swift @@ -30,14 +30,18 @@ extension DanceFreeze.LauncherView { } struct StageModeButtonStyle: View { - let text: String - let color: Color + // MARK: Lifecycle init(_ text: String, color: Color) { self.text = text self.color = color } + // MARK: Internal + + let text: String + let color: Color + var body: some View { Text(text) .font(.headline) diff --git a/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Specialized/DanceFreeze/DanceFreeze+MainView.swift b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Specialized/DanceFreeze/DanceFreeze+MainView.swift index c5aec3bdd2..6902cb0f3d 100644 --- a/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Specialized/DanceFreeze/DanceFreeze+MainView.swift +++ b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Specialized/DanceFreeze/DanceFreeze+MainView.swift @@ -7,22 +7,10 @@ import DesignKit import SwiftUI enum DanceFreeze { - enum Stage { - case waitingForSelection - case automaticMode - case manualMode - } - - enum Motion { - case rotation - case movement - } + // MARK: Public public struct MainView: View { - @State private var mode = Stage.waitingForSelection - @State private var motion: Motion = .rotation - @StateObject private var viewModel: MainViewViewModel - let songs: [AudioRecording] + // MARK: Lifecycle public init(songs: [AudioRecording]) { self.songs = songs @@ -40,6 +28,8 @@ enum DanceFreeze { songs: payload.songs, shared: data)) } + // MARK: Public + public var body: some View { NavigationStack { switch mode { @@ -63,6 +53,29 @@ enum DanceFreeze { } .navigationViewStyle(StackNavigationViewStyle()) } + + // MARK: Internal + + let songs: [AudioRecording] + + // MARK: Private + + @State private var mode = Stage.waitingForSelection + @State private var motion: Motion = .rotation + @StateObject private var viewModel: MainViewViewModel + } + + // MARK: Internal + + enum Stage { + case waitingForSelection + case automaticMode + case manualMode + } + + enum Motion { + case rotation + case movement } } diff --git a/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Specialized/DanceFreeze/DanceFreeze+MainViewViewModel.swift b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Specialized/DanceFreeze/DanceFreeze+MainViewViewModel.swift index 51bce31ca8..447001c9b0 100644 --- a/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Specialized/DanceFreeze/DanceFreeze+MainViewViewModel.swift +++ b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Specialized/DanceFreeze/DanceFreeze+MainViewViewModel.swift @@ -9,15 +9,7 @@ import SwiftUI extension DanceFreeze { class MainViewViewModel: ObservableObject { - @ObservedObject var exercicesSharedData: ExerciseSharedData - @Published public var progress: CGFloat = 0.0 - @Published public var isDancing: Bool = false - - var robotManager: RobotManager - var audioPlayer: AudioPlayer - let songs: [AudioRecording] - var motionMode: Motion = .rotation - var cancellables: Set = [] + // MARK: Lifecycle init(songs: [AudioRecording], shuffle: Bool = false, shared: ExerciseSharedData? = nil) { self.songs = songs @@ -30,18 +22,10 @@ extension DanceFreeze { subscribeToAudioPlayerProgress() } - private func subscribeToAudioPlayerProgress() { - self.audioPlayer.$progress - .receive(on: DispatchQueue.main) - .sink { [weak self] in - guard let self = self else { return } - self.progress = $0 - if self.progress == 1 { - completeDanceFreeze() - } - } - .store(in: &cancellables) - } + // MARK: Public + + @Published public var progress: CGFloat = 0.0 + @Published public var isDancing: Bool = false public func onDanceFreezeToggle() { if progress == 1.0 { @@ -67,11 +51,35 @@ extension DanceFreeze { motionMode = motion } + // MARK: Internal + + @ObservedObject var exercicesSharedData: ExerciseSharedData + var robotManager: RobotManager + var audioPlayer: AudioPlayer + let songs: [AudioRecording] + var motionMode: Motion = .rotation + var cancellables: Set = [] + func completeDanceFreeze() { robotManager.stopRobot() exercicesSharedData.state = .completed } + // MARK: Private + + private func subscribeToAudioPlayerProgress() { + self.audioPlayer.$progress + .receive(on: DispatchQueue.main) + .sink { [weak self] in + guard let self = self else { return } + self.progress = $0 + if self.progress == 1 { + completeDanceFreeze() + } + } + .store(in: &cancellables) + } + private func robotDance() { switch motionMode { case .rotation: diff --git a/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Specialized/DanceFreeze/DanceFreeze+PlayerView.swift b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Specialized/DanceFreeze/DanceFreeze+PlayerView.swift index 8edc12eabd..7f66dc6f01 100644 --- a/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Specialized/DanceFreeze/DanceFreeze+PlayerView.swift +++ b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Specialized/DanceFreeze/DanceFreeze+PlayerView.swift @@ -7,10 +7,7 @@ import SwiftUI extension DanceFreeze { struct PlayerView: View { - @ObservedObject var viewModel: MainViewViewModel - - let isAuto: Bool - let motion: Motion + // MARK: Lifecycle public init(viewModel: MainViewViewModel, isAuto: Bool, motion: Motion) { self.viewModel = viewModel @@ -18,6 +15,13 @@ extension DanceFreeze { self.motion = motion } + // MARK: Internal + + @ObservedObject var viewModel: MainViewViewModel + + let isAuto: Bool + let motion: Motion + var body: some View { VStack { ContinuousProgressBar(progress: viewModel.progress) diff --git a/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Specialized/DanceFreeze/DanceFreeze+RobotManager.swift b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Specialized/DanceFreeze/DanceFreeze+RobotManager.swift index 48480af86a..0ea14d98bd 100644 --- a/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Specialized/DanceFreeze/DanceFreeze+RobotManager.swift +++ b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Specialized/DanceFreeze/DanceFreeze+RobotManager.swift @@ -9,6 +9,8 @@ import SwiftUI extension DanceFreeze { class RobotManager { + // MARK: Internal + let robot = Robot.shared var lastMove = 0 @@ -29,19 +31,6 @@ extension DanceFreeze { robot.shine(randomLight) } - private func getRandomColor() -> Robot.Color { - let colors: [Robot.Color] = [.red, .blue, .green, .yellow, .lightBlue, .purple, .orange, .pink] - return colors.randomElement()! - } - - private func getRandomLight(color: Robot.Color) -> Robot.Lights { - let lights: [Robot.Lights] = [ - .earLeft(in: color), .earRight(in: color), .quarterBackLeft(in: color), .quarterBackRight(in: color), - .quarterFrontLeft(in: color), .quarterFrontRight(in: color), - ] - return lights.randomElement()! - } - func rotationDance() -> CGFloat { let motions: [(duration: CGFloat, motion: Robot.Motion)] = [ (3, .spin(.clockwise, speed: 1)), @@ -73,5 +62,20 @@ extension DanceFreeze { return action.duration } + + // MARK: Private + + private func getRandomColor() -> Robot.Color { + let colors: [Robot.Color] = [.red, .blue, .green, .yellow, .lightBlue, .purple, .orange, .pink] + return colors.randomElement()! + } + + private func getRandomLight(color: Robot.Color) -> Robot.Lights { + let lights: [Robot.Lights] = [ + .earLeft(in: color), .earRight(in: color), .quarterBackLeft(in: color), .quarterBackRight(in: color), + .quarterFrontLeft(in: color), .quarterFrontRight(in: color), + ] + return lights.randomElement()! + } } } diff --git a/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Specialized/DanceFreeze/DanceFreeze+SongSelectorView.swift b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Specialized/DanceFreeze/DanceFreeze+SongSelectorView.swift index e04f289fe7..4f534f6c01 100644 --- a/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Specialized/DanceFreeze/DanceFreeze+SongSelectorView.swift +++ b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Specialized/DanceFreeze/DanceFreeze+SongSelectorView.swift @@ -8,19 +8,20 @@ import SwiftUI extension DanceFreeze { struct SongSelectorView: View { - @ObservedObject private var viewModel: MainViewViewModel - @State private var selectedAudioRecording: AudioRecording - - let columns = [ - GridItem(.flexible()), - GridItem(.flexible()), - ] + // MARK: Lifecycle init(viewModel: MainViewViewModel) { self.viewModel = viewModel self.selectedAudioRecording = viewModel.songs.first! } + // MARK: Internal + + let columns = [ + GridItem(.flexible()), + GridItem(.flexible()), + ] + var body: some View { VStack { HStack { @@ -66,6 +67,11 @@ extension DanceFreeze { viewModel.setAudioRecording(audioRecording: viewModel.songs.first!) } } + + // MARK: Private + + @ObservedObject private var viewModel: MainViewViewModel + @State private var selectedAudioRecording: AudioRecording } } diff --git a/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Specialized/HideAndSeek/HideAndSeekView+ButtonLabel.swift b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Specialized/HideAndSeek/HideAndSeekView+ButtonLabel.swift index ce8d08abb7..9df4e3200f 100644 --- a/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Specialized/HideAndSeek/HideAndSeekView+ButtonLabel.swift +++ b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Specialized/HideAndSeek/HideAndSeekView+ButtonLabel.swift @@ -6,14 +6,18 @@ import SwiftUI extension HideAndSeekView { struct ButtonLabel: View { - let text: String - let color: Color + // MARK: Lifecycle init(_ text: String, color: Color) { self.text = text self.color = color } + // MARK: Internal + + let text: String + let color: Color + var body: some View { Text(text) .font(.title2) diff --git a/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Specialized/HideAndSeek/HideAndSeekView+HiddenView.swift b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Specialized/HideAndSeek/HideAndSeekView+HiddenView.swift index 11bb702a15..37be15244e 100644 --- a/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Specialized/HideAndSeek/HideAndSeekView+HiddenView.swift +++ b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Specialized/HideAndSeek/HideAndSeekView+HiddenView.swift @@ -8,6 +8,7 @@ import SwiftUI extension HideAndSeekView { struct HiddenView: View { let textSubInstructions: String + var body: some View { VStack { Text(textSubInstructions) diff --git a/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Specialized/HideAndSeek/HideAndSeekView+Player.swift b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Specialized/HideAndSeek/HideAndSeekView+Player.swift index e764c944c6..784c64e160 100644 --- a/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Specialized/HideAndSeek/HideAndSeekView+Player.swift +++ b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Specialized/HideAndSeek/HideAndSeekView+Player.swift @@ -8,10 +8,28 @@ import SwiftUI // swiftlint:disable nesting extension HideAndSeekView { struct Player: View { + // MARK: Lifecycle + + init( + stage: Binding, textSubInstructions: String, textButtonRobotFound: String, + shared: ExerciseSharedData? = nil + ) { + self._stage = stage + self.textSubInstructions = textSubInstructions + self.textButtonRobotFound = textButtonRobotFound + + self.exercicesSharedData = shared ?? ExerciseSharedData() + self.exercicesSharedData.state = .playing + } + + // MARK: Internal + enum Stimulation: String, CaseIterable { case light case motion + // MARK: Public + public func icon() -> Image { switch self { case .light: @@ -28,18 +46,6 @@ extension HideAndSeekView { let textButtonRobotFound: String let robotManager = RobotManager() - init( - stage: Binding, textSubInstructions: String, textButtonRobotFound: String, - shared: ExerciseSharedData? = nil - ) { - self._stage = stage - self.textSubInstructions = textSubInstructions - self.textButtonRobotFound = textButtonRobotFound - - self.exercicesSharedData = shared ?? ExerciseSharedData() - self.exercicesSharedData.state = .playing - } - var body: some View { ZStack { HiddenView(textSubInstructions: textSubInstructions) diff --git a/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Specialized/HideAndSeek/HideAndSeekView.swift b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Specialized/HideAndSeek/HideAndSeekView.swift index dda02d7ca3..94b11885fa 100644 --- a/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Specialized/HideAndSeek/HideAndSeekView.swift +++ b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Specialized/HideAndSeek/HideAndSeekView.swift @@ -6,14 +6,7 @@ import ContentKit import SwiftUI struct HideAndSeekView: View { - enum HideAndSeekStage { - case toHide - case hidden - } - - @State private var stage: HideAndSeekStage - let instructions: HideAndSeek.Payload.Instructions - let shared: ExerciseSharedData? + // MARK: Lifecycle public init(instructions: HideAndSeek.Payload.Instructions) { self.stage = .toHide @@ -31,6 +24,8 @@ struct HideAndSeekView: View { self.shared = data } + // MARK: Public + public var body: some View { switch stage { case .toHide: @@ -43,4 +38,18 @@ struct HideAndSeekView: View { textButtonRobotFound: instructions.textButtonRobotFound, shared: shared) } } + + // MARK: Internal + + enum HideAndSeekStage { + case toHide + case hidden + } + + let instructions: HideAndSeek.Payload.Instructions + let shared: ExerciseSharedData? + + // MARK: Private + + @State private var stage: HideAndSeekStage } diff --git a/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Specialized/Instrument/MusicalInstrumentView+XylophoneView+TileButtonStyle.swift b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Specialized/Instrument/MusicalInstrumentView+XylophoneView+TileButtonStyle.swift index 55f52be3bb..7ae3140f1c 100644 --- a/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Specialized/Instrument/MusicalInstrumentView+XylophoneView+TileButtonStyle.swift +++ b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Specialized/Instrument/MusicalInstrumentView+XylophoneView+TileButtonStyle.swift @@ -9,6 +9,8 @@ import SwiftUI extension MusicalInstrumentView.XylophoneView { struct TileButtonStyle: ButtonStyle { + // MARK: Internal + let xyloAttachColor = Color(red: 0.87, green: 0.65, blue: 0.54) let defaultMaxTileHeight: Int = 500 let defaultTileHeightGap: Int = 250 @@ -61,6 +63,8 @@ extension MusicalInstrumentView.XylophoneView { ) } + // MARK: Private + private func setSizeFromIndex() -> CGFloat { let sizeDiff = defaultTileHeightGap / tileNumber let tileHeight = defaultMaxTileHeight - index * sizeDiff diff --git a/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Specialized/Instrument/MusicalInstrumentView+XylophoneView.swift b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Specialized/Instrument/MusicalInstrumentView+XylophoneView.swift index b2ce76a9c3..5010cf73d0 100644 --- a/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Specialized/Instrument/MusicalInstrumentView+XylophoneView.swift +++ b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Specialized/Instrument/MusicalInstrumentView+XylophoneView.swift @@ -8,11 +8,7 @@ import SwiftUI extension MusicalInstrumentView { struct XylophoneView: View { - @ObservedObject var xyloPlayer: MIDIPlayer - let tilesSpacing: CGFloat - let tileNumber: Int - let tileColors: [Robot.Color] = [.pink, .red, .orange, .yellow, .green, .lightBlue, .blue, .purple] - let scale: MIDIScale + // MARK: Lifecycle init(midiPlayer: MIDIPlayer, scale: MIDIScale) { self.xyloPlayer = midiPlayer @@ -21,6 +17,14 @@ extension MusicalInstrumentView { self.tilesSpacing = scale.self == .majorPentatonic ? 40 : 20 } + // MARK: Internal + + @ObservedObject var xyloPlayer: MIDIPlayer + let tilesSpacing: CGFloat + let tileNumber: Int + let tileColors: [Robot.Color] = [.pink, .red, .orange, .yellow, .green, .lightBlue, .blue, .purple] + let scale: MIDIScale + var body: some View { HStack(spacing: tilesSpacing) { ForEach(0.. = [] + // MARK: Lifecycle init(dragDiameter: CGFloat) { self.dragDiameter = dragDiameter @@ -31,4 +24,17 @@ class JoystickViewViewModel: ObservableObject { }) .store(in: &cancellables) } + + // MARK: Internal + + var joystickMonitor = JoystickMonitor() + + let dragDiameter: CGFloat + let shape: JoystickShape = .circle + + // MARK: Private + + @Published private var position: CGPoint = CGPoint(x: 0.0, y: 0.0) + + private var cancellables: Set = [] } diff --git a/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Specialized/RemoteStandard/LedZoneSelector/LedZoneSelectorView+BeltSectionButton.swift b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Specialized/RemoteStandard/LedZoneSelector/LedZoneSelectorView+BeltSectionButton.swift index a31d2cf883..fc30fbd3b6 100644 --- a/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Specialized/RemoteStandard/LedZoneSelector/LedZoneSelectorView+BeltSectionButton.swift +++ b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Specialized/RemoteStandard/LedZoneSelector/LedZoneSelectorView+BeltSectionButton.swift @@ -32,12 +32,11 @@ public extension Robot.Lights { extension LedZoneSelectorView { struct BeltSectionButton: View { + // MARK: Internal + var section: Robot.Lights let robot = Robot.shared - @State private var buttonPressed = false - @State private var backgroundLineWidth = 0 - var body: some View { LedZoneShape(section: section) .stroke(section.color.screen, style: StrokeStyle(lineWidth: 10, lineCap: .round)) @@ -65,6 +64,11 @@ extension LedZoneSelectorView { ) .animation(Animation.easeInOut(duration: 0.2), value: backgroundLineWidth) } + + // MARK: Private + + @State private var buttonPressed = false + @State private var backgroundLineWidth = 0 } } diff --git a/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Specialized/RemoteStandard/LedZoneSelector/LedZoneSelectorView+BeltSectionIcon.swift b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Specialized/RemoteStandard/LedZoneSelector/LedZoneSelectorView+BeltSectionIcon.swift index 128fbde492..ba3ece8b3e 100644 --- a/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Specialized/RemoteStandard/LedZoneSelector/LedZoneSelectorView+BeltSectionIcon.swift +++ b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Specialized/RemoteStandard/LedZoneSelector/LedZoneSelectorView+BeltSectionIcon.swift @@ -7,15 +7,19 @@ import SwiftUI extension LedZoneSelectorView { struct BeltSectionIcon: View { - var section: Robot.Lights + // MARK: Internal - @State private var backgroundLineWidth = 0 + var section: Robot.Lights var body: some View { LedZoneShape(section: section) .stroke(.black, style: StrokeStyle(lineWidth: 3, lineCap: .round)) .frame(width: 60, height: 60) } + + // MARK: Private + + @State private var backgroundLineWidth = 0 } } diff --git a/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Specialized/RemoteStandard/LedZoneSelector/LedZoneSelectorView+EarButton.swift b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Specialized/RemoteStandard/LedZoneSelector/LedZoneSelectorView+EarButton.swift index e91fc338e2..61763b5c46 100644 --- a/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Specialized/RemoteStandard/LedZoneSelector/LedZoneSelectorView+EarButton.swift +++ b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Specialized/RemoteStandard/LedZoneSelector/LedZoneSelectorView+EarButton.swift @@ -7,16 +7,17 @@ import SwiftUI extension LedZoneSelectorView { struct EarButton: View { - let selectedEar: Robot.Lights - let robot = Robot.shared - - @State private var buttonPressed = false - @State private var backgroundDimension = 0 + // MARK: Lifecycle init(selectedEar: Robot.Lights) { self.selectedEar = selectedEar } + // MARK: Internal + + let selectedEar: Robot.Lights + let robot = Robot.shared + var body: some View { Circle() .foregroundColor(selectedEar.color.screen) @@ -37,6 +38,11 @@ extension LedZoneSelectorView { ) .animation(.easeInOut(duration: 0.2), value: backgroundDimension) } + + // MARK: Private + + @State private var buttonPressed = false + @State private var backgroundDimension = 0 } } diff --git a/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Specialized/RemoteStandard/LedZoneSelector/LedZoneSelectorView+ModeButton.swift b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Specialized/RemoteStandard/LedZoneSelector/LedZoneSelectorView+ModeButton.swift index 49e38b60ba..6c874c3ed3 100644 --- a/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Specialized/RemoteStandard/LedZoneSelector/LedZoneSelectorView+ModeButton.swift +++ b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Specialized/RemoteStandard/LedZoneSelector/LedZoneSelectorView+ModeButton.swift @@ -7,6 +7,8 @@ import SwiftUI extension LedZoneSelectorView { struct ModeButton: View { + // MARK: Internal + var mode: RemoteStandard.DisplayMode @Binding var displayMode: RemoteStandard.DisplayMode @@ -27,6 +29,8 @@ extension LedZoneSelectorView { .background(ModeFeedback(backgroundDimension: displayMode == mode ? 80 : 0)) } + // MARK: Private + private var earsSectionIcons: some View { HStack { switch mode { diff --git a/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Specialized/RemoteStandard/LedZoneSelector/LedZoneSelectorView.swift b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Specialized/RemoteStandard/LedZoneSelector/LedZoneSelectorView.swift index c8bdf6f783..21867a8910 100644 --- a/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Specialized/RemoteStandard/LedZoneSelector/LedZoneSelectorView.swift +++ b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Specialized/RemoteStandard/LedZoneSelector/LedZoneSelectorView.swift @@ -5,6 +5,8 @@ import SwiftUI struct LedZoneSelectorView: View { + // MARK: Internal + let displayMode: RemoteStandard.DisplayMode var body: some View { @@ -28,6 +30,8 @@ struct LedZoneSelectorView: View { } } + // MARK: Private + private var earsSectionButtons: some View { HStack(spacing: 50) { switch displayMode { diff --git a/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Specialized/RemoteStandard/RemoteStandard+MainView.swift b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Specialized/RemoteStandard/RemoteStandard+MainView.swift index 563d9cd5f3..58f3387531 100644 --- a/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Specialized/RemoteStandard/RemoteStandard+MainView.swift +++ b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Specialized/RemoteStandard/RemoteStandard+MainView.swift @@ -46,7 +46,7 @@ enum RemoteStandard { } struct MainView: View { - @State private var displayMode = DisplayMode.fullBelt + // MARK: Internal var body: some View { HStack(spacing: 400) { @@ -67,6 +67,10 @@ enum RemoteStandard { } } } + + // MARK: Private + + @State private var displayMode = DisplayMode.fullBelt } } diff --git a/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/ChoiceColorView.swift b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/ChoiceColorView.swift index 2ecd0e158c..65d308635e 100644 --- a/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/ChoiceColorView.swift +++ b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/ChoiceColorView.swift @@ -8,13 +8,7 @@ import RobotKit import SwiftUI struct ChoiceColorView: View { - private let color: Robot.Color - private let size: CGFloat - private let state: GameplayChoiceState - private let kOverLayScaleFactor: CGFloat = 1.08 - - @State private var animationPercent: CGFloat = .zero - @State private var overlayOpacity: CGFloat = .zero + // MARK: Lifecycle init(color: String, size: CGFloat, state: GameplayChoiceState = .idle) { self.color = Robot.Color(from: color) @@ -22,6 +16,8 @@ struct ChoiceColorView: View { self.state = state } + // MARK: Internal + // TODO(@ladislas): handle case of color white, add colored border? var circle: some View { color.screen @@ -74,6 +70,16 @@ struct ChoiceColorView: View { } } } + + // MARK: Private + + private let color: Robot.Color + private let size: CGFloat + private let state: GameplayChoiceState + private let kOverLayScaleFactor: CGFloat = 1.08 + + @State private var animationPercent: CGFloat = .zero + @State private var overlayOpacity: CGFloat = .zero } #Preview { diff --git a/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/ChoiceImageView.swift b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/ChoiceImageView.swift index 50fe4f0ff8..083344edbd 100644 --- a/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/ChoiceImageView.swift +++ b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/ChoiceImageView.swift @@ -8,13 +8,7 @@ import RobotKit import SwiftUI struct ChoiceImageView: View { - private let image: String - private let size: CGFloat - private let state: GameplayChoiceState - private let kOverLayScaleFactor: CGFloat = 1.08 - - @State private var animationPercent: CGFloat = .zero - @State private var overlayOpacity: CGFloat = .zero + // MARK: Lifecycle init(image: String, size: CGFloat, state: GameplayChoiceState = .idle) { self.image = image @@ -22,6 +16,8 @@ struct ChoiceImageView: View { self.state = state } + // MARK: Internal + @ViewBuilder var circle: some View { if let uiImage = UIImage(named: image) { @@ -89,6 +85,16 @@ struct ChoiceImageView: View { } } } + + // MARK: Private + + private let image: String + private let size: CGFloat + private let state: GameplayChoiceState + private let kOverLayScaleFactor: CGFloat = 1.08 + + @State private var animationPercent: CGFloat = .zero + @State private var overlayOpacity: CGFloat = .zero } #Preview { diff --git a/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/ListenThenTouchToSelect/ListenThenTouchToSelectView+1_OneChoice.swift b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/ListenThenTouchToSelect/ListenThenTouchToSelectView+1_OneChoice.swift index f203ed701b..8c207f47a9 100644 --- a/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/ListenThenTouchToSelect/ListenThenTouchToSelectView+1_OneChoice.swift +++ b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/ListenThenTouchToSelect/ListenThenTouchToSelectView+1_OneChoice.swift @@ -7,11 +7,11 @@ import SwiftUI extension ListenThenTouchToSelectView { struct OneChoiceView: View { + // MARK: Internal + @ObservedObject var viewModel: TouchToSelectViewViewModel let isTappable: Bool - private let kAnswerSize: CGFloat = 300 - var body: some View { let choice = viewModel.choices[0] TouchToSelectChoiceView(choice: choice, size: kAnswerSize, isTappable: isTappable) @@ -19,6 +19,10 @@ extension ListenThenTouchToSelectView { viewModel.onChoiceTapped(choice: choice) } } + + // MARK: Private + + private let kAnswerSize: CGFloat = 300 } } diff --git a/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/ListenThenTouchToSelect/ListenThenTouchToSelectView+2_TwoChoice.swift b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/ListenThenTouchToSelect/ListenThenTouchToSelectView+2_TwoChoice.swift index ad5bc7df85..cb9aa49d53 100644 --- a/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/ListenThenTouchToSelect/ListenThenTouchToSelectView+2_TwoChoice.swift +++ b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/ListenThenTouchToSelect/ListenThenTouchToSelectView+2_TwoChoice.swift @@ -7,12 +7,11 @@ import SwiftUI extension ListenThenTouchToSelectView { struct TwoChoicesView: View { + // MARK: Internal + @ObservedObject var viewModel: TouchToSelectViewViewModel let isTappable: Bool - private let kHorizontalSpacing: CGFloat = 100 - private let kAnswerSize: CGFloat = 300 - var body: some View { HStack(spacing: kHorizontalSpacing) { ForEach(viewModel.choices) { choice in @@ -23,6 +22,11 @@ extension ListenThenTouchToSelectView { } } } + + // MARK: Private + + private let kHorizontalSpacing: CGFloat = 100 + private let kAnswerSize: CGFloat = 300 } } diff --git a/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/ListenThenTouchToSelect/ListenThenTouchToSelectView+3_ThreeChoice.swift b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/ListenThenTouchToSelect/ListenThenTouchToSelectView+3_ThreeChoice.swift index fd88aaf931..07e2b77224 100644 --- a/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/ListenThenTouchToSelect/ListenThenTouchToSelectView+3_ThreeChoice.swift +++ b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/ListenThenTouchToSelect/ListenThenTouchToSelectView+3_ThreeChoice.swift @@ -7,13 +7,11 @@ import SwiftUI extension ListenThenTouchToSelectView { struct ThreeChoicesView: View { + // MARK: Internal + @ObservedObject var viewModel: TouchToSelectViewViewModel let isTappable: Bool - private let kHorizontalSpacing: CGFloat = 200 - private let kVerticalSpacing: CGFloat = 40 - private let kAnswerSize: CGFloat = 240 - var body: some View { VStack(spacing: kVerticalSpacing) { HStack(spacing: kHorizontalSpacing) { @@ -31,6 +29,12 @@ extension ListenThenTouchToSelectView { } } } + + // MARK: Private + + private let kHorizontalSpacing: CGFloat = 200 + private let kVerticalSpacing: CGFloat = 40 + private let kAnswerSize: CGFloat = 240 } } diff --git a/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/ListenThenTouchToSelect/ListenThenTouchToSelectView+4_FourChoice.swift b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/ListenThenTouchToSelect/ListenThenTouchToSelectView+4_FourChoice.swift index 373016c407..46aa372a88 100644 --- a/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/ListenThenTouchToSelect/ListenThenTouchToSelectView+4_FourChoice.swift +++ b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/ListenThenTouchToSelect/ListenThenTouchToSelectView+4_FourChoice.swift @@ -7,13 +7,11 @@ import SwiftUI extension ListenThenTouchToSelectView { struct FourChoicesView: View { + // MARK: Internal + @ObservedObject var viewModel: TouchToSelectViewViewModel let isTappable: Bool - private let kHorizontalSpacing: CGFloat = 200 - private let kVerticalSpacing: CGFloat = 40 - private let kAnswerSize: CGFloat = 240 - var body: some View { VStack(spacing: kVerticalSpacing) { HStack(spacing: kHorizontalSpacing) { @@ -35,6 +33,12 @@ extension ListenThenTouchToSelectView { } } } + + // MARK: Private + + private let kHorizontalSpacing: CGFloat = 200 + private let kVerticalSpacing: CGFloat = 40 + private let kAnswerSize: CGFloat = 240 } } diff --git a/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/ListenThenTouchToSelect/ListenThenTouchToSelectView+5_FiveChoice.swift b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/ListenThenTouchToSelect/ListenThenTouchToSelectView+5_FiveChoice.swift index 78c30ca7da..b60be0c6d6 100644 --- a/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/ListenThenTouchToSelect/ListenThenTouchToSelectView+5_FiveChoice.swift +++ b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/ListenThenTouchToSelect/ListenThenTouchToSelectView+5_FiveChoice.swift @@ -7,13 +7,11 @@ import SwiftUI extension ListenThenTouchToSelectView { struct FiveChoicesView: View { + // MARK: Internal + @ObservedObject var viewModel: TouchToSelectViewViewModel let isTappable: Bool - private let kHorizontalSpacing: CGFloat = 60 - private let kVerticalSpacing: CGFloat = 40 - private let kAnswerSize: CGFloat = 200 - var body: some View { VStack(spacing: kVerticalSpacing) { HStack(spacing: kHorizontalSpacing) { @@ -35,6 +33,12 @@ extension ListenThenTouchToSelectView { } } } + + // MARK: Private + + private let kHorizontalSpacing: CGFloat = 60 + private let kVerticalSpacing: CGFloat = 40 + private let kAnswerSize: CGFloat = 200 } } diff --git a/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/ListenThenTouchToSelect/ListenThenTouchToSelectView+6_SixChoice.swift b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/ListenThenTouchToSelect/ListenThenTouchToSelectView+6_SixChoice.swift index da2bea4dc9..f9e1c6cb64 100644 --- a/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/ListenThenTouchToSelect/ListenThenTouchToSelectView+6_SixChoice.swift +++ b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/ListenThenTouchToSelect/ListenThenTouchToSelectView+6_SixChoice.swift @@ -7,13 +7,11 @@ import SwiftUI extension ListenThenTouchToSelectView { struct SixChoicesView: View { + // MARK: Internal + @ObservedObject var viewModel: TouchToSelectViewViewModel let isTappable: Bool - private let kHorizontalSpacing: CGFloat = 60 - private let kVerticalSpacing: CGFloat = 40 - private let kAnswerSize: CGFloat = 200 - var body: some View { VStack(spacing: kVerticalSpacing) { HStack(spacing: kHorizontalSpacing) { @@ -35,6 +33,12 @@ extension ListenThenTouchToSelectView { } } } + + // MARK: Private + + private let kHorizontalSpacing: CGFloat = 60 + private let kVerticalSpacing: CGFloat = 40 + private let kAnswerSize: CGFloat = 200 } } diff --git a/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/ListenThenTouchToSelect/ListenThenTouchToSelectView.swift b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/ListenThenTouchToSelect/ListenThenTouchToSelectView.swift index 03f17cea15..b9d7f2943c 100644 --- a/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/ListenThenTouchToSelect/ListenThenTouchToSelectView.swift +++ b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/ListenThenTouchToSelect/ListenThenTouchToSelectView.swift @@ -7,17 +7,7 @@ import ContentKit import SwiftUI public struct ListenThenTouchToSelectView: View { - enum Interface: Int { - case oneChoice = 1 - case twoChoices - case threeChoices - case fourChoices - case fiveChoices - case sixChoices - } - - @StateObject private var viewModel: TouchToSelectViewViewModel - @StateObject private var audioPlayer: AudioPlayer + // MARK: Lifecycle public init(choices: [TouchToSelect.Choice], audioRecording: AudioRecording) { self._viewModel = StateObject(wrappedValue: TouchToSelectViewViewModel(choices: choices)) @@ -40,6 +30,8 @@ public struct ListenThenTouchToSelectView: View { self._audioPlayer = StateObject(wrappedValue: AudioPlayer(audioRecording: audioRecording)) } + // MARK: Public + public var body: some View { let interface = Interface(rawValue: viewModel.choices.count) @@ -104,4 +96,20 @@ public struct ListenThenTouchToSelectView: View { Spacer() } } + + // MARK: Internal + + enum Interface: Int { + case oneChoice = 1 + case twoChoices + case threeChoices + case fourChoices + case fiveChoices + case sixChoices + } + + // MARK: Private + + @StateObject private var viewModel: TouchToSelectViewViewModel + @StateObject private var audioPlayer: AudioPlayer } diff --git a/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/ObserveThenTouchToSelect/ObserveThenTouchToSelect.swift b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/ObserveThenTouchToSelect/ObserveThenTouchToSelect.swift index 8506167c2f..5657389cb3 100644 --- a/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/ObserveThenTouchToSelect/ObserveThenTouchToSelect.swift +++ b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/ObserveThenTouchToSelect/ObserveThenTouchToSelect.swift @@ -7,20 +7,7 @@ import ContentKit import SwiftUI public struct ObserveThenTouchToSelectView: View { - enum Interface: Int { - case oneChoice = 1 - case twoChoices - case threeChoices - case fourChoices - case fiveChoices - case sixChoices - } - - @StateObject private var viewModel: TouchToSelectViewViewModel - - @State private var imageWasTapped = false - - private let image: String + // MARK: Lifecycle public init(choices: [TouchToSelect.Choice], image: String) { self._viewModel = StateObject(wrappedValue: TouchToSelectViewViewModel(choices: choices)) @@ -42,6 +29,8 @@ public struct ObserveThenTouchToSelectView: View { self.image = name } + // MARK: Public + public var body: some View { let interface = Interface(rawValue: viewModel.choices.count) @@ -101,4 +90,23 @@ public struct ObserveThenTouchToSelectView: View { Spacer() } } + + // MARK: Internal + + enum Interface: Int { + case oneChoice = 1 + case twoChoices + case threeChoices + case fourChoices + case fiveChoices + case sixChoices + } + + // MARK: Private + + @StateObject private var viewModel: TouchToSelectViewViewModel + + @State private var imageWasTapped = false + + private let image: String } diff --git a/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/ObserveThenTouchToSelect/ObserveThenTouchToSelectView+1_OneChoice.swift b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/ObserveThenTouchToSelect/ObserveThenTouchToSelectView+1_OneChoice.swift index 0346912ad0..b3e6dcbc3c 100644 --- a/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/ObserveThenTouchToSelect/ObserveThenTouchToSelectView+1_OneChoice.swift +++ b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/ObserveThenTouchToSelect/ObserveThenTouchToSelectView+1_OneChoice.swift @@ -7,11 +7,11 @@ import SwiftUI extension ObserveThenTouchToSelectView { struct OneChoiceView: View { + // MARK: Internal + @ObservedObject var viewModel: TouchToSelectViewViewModel let isTappable: Bool - private let kAnswerSize: CGFloat = 180 - var body: some View { let choice = viewModel.choices[0] TouchToSelectChoiceView(choice: choice, size: kAnswerSize, isTappable: isTappable) @@ -19,6 +19,10 @@ extension ObserveThenTouchToSelectView { viewModel.onChoiceTapped(choice: choice) } } + + // MARK: Private + + private let kAnswerSize: CGFloat = 180 } } diff --git a/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/ObserveThenTouchToSelect/ObserveThenTouchToSelectView+2_TwoChoice.swift b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/ObserveThenTouchToSelect/ObserveThenTouchToSelectView+2_TwoChoice.swift index 36de53c1dd..a09da051a5 100644 --- a/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/ObserveThenTouchToSelect/ObserveThenTouchToSelectView+2_TwoChoice.swift +++ b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/ObserveThenTouchToSelect/ObserveThenTouchToSelectView+2_TwoChoice.swift @@ -7,12 +7,11 @@ import SwiftUI extension ObserveThenTouchToSelectView { struct TwoChoicesView: View { + // MARK: Internal + @ObservedObject var viewModel: TouchToSelectViewViewModel let isTappable: Bool - private let kHorizontalSpacing: CGFloat = 60 - private let kAnswerSize: CGFloat = 180 - var body: some View { HStack(spacing: kHorizontalSpacing) { ForEach(viewModel.choices) { choice in @@ -23,6 +22,11 @@ extension ObserveThenTouchToSelectView { } } } + + // MARK: Private + + private let kHorizontalSpacing: CGFloat = 60 + private let kAnswerSize: CGFloat = 180 } } diff --git a/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/ObserveThenTouchToSelect/ObserveThenTouchToSelectView+3_ThreeChoice.swift b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/ObserveThenTouchToSelect/ObserveThenTouchToSelectView+3_ThreeChoice.swift index 14081c6270..6723aa5596 100644 --- a/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/ObserveThenTouchToSelect/ObserveThenTouchToSelectView+3_ThreeChoice.swift +++ b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/ObserveThenTouchToSelect/ObserveThenTouchToSelectView+3_ThreeChoice.swift @@ -7,13 +7,11 @@ import SwiftUI extension ObserveThenTouchToSelectView { struct ThreeChoicesView: View { + // MARK: Internal + @ObservedObject var viewModel: TouchToSelectViewViewModel let isTappable: Bool - private let kHorizontalSpacing: CGFloat = 60 - private let kVerticalSpacing: CGFloat = 40 - private let kAnswerSize: CGFloat = 180 - var body: some View { VStack(spacing: kVerticalSpacing) { HStack(spacing: kHorizontalSpacing) { @@ -31,6 +29,12 @@ extension ObserveThenTouchToSelectView { } } } + + // MARK: Private + + private let kHorizontalSpacing: CGFloat = 60 + private let kVerticalSpacing: CGFloat = 40 + private let kAnswerSize: CGFloat = 180 } } diff --git a/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/ObserveThenTouchToSelect/ObserveThenTouchToSelectView+4_FourChoice.swift b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/ObserveThenTouchToSelect/ObserveThenTouchToSelectView+4_FourChoice.swift index c64ae26139..7b7aecbbdd 100644 --- a/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/ObserveThenTouchToSelect/ObserveThenTouchToSelectView+4_FourChoice.swift +++ b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/ObserveThenTouchToSelect/ObserveThenTouchToSelectView+4_FourChoice.swift @@ -7,13 +7,11 @@ import SwiftUI extension ObserveThenTouchToSelectView { struct FourChoicesView: View { + // MARK: Internal + @ObservedObject var viewModel: TouchToSelectViewViewModel let isTappable: Bool - private let kHorizontalSpacing: CGFloat = 60 - private let kVerticalSpacing: CGFloat = 40 - private let kAnswerSize: CGFloat = 180 - var body: some View { VStack(spacing: kVerticalSpacing) { HStack(spacing: kHorizontalSpacing) { @@ -35,6 +33,12 @@ extension ObserveThenTouchToSelectView { } } } + + // MARK: Private + + private let kHorizontalSpacing: CGFloat = 60 + private let kVerticalSpacing: CGFloat = 40 + private let kAnswerSize: CGFloat = 180 } } diff --git a/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/ObserveThenTouchToSelect/ObserveThenTouchToSelectView+5_FiveChoice.swift b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/ObserveThenTouchToSelect/ObserveThenTouchToSelectView+5_FiveChoice.swift index 189b113845..8a0e14b8b7 100644 --- a/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/ObserveThenTouchToSelect/ObserveThenTouchToSelectView+5_FiveChoice.swift +++ b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/ObserveThenTouchToSelect/ObserveThenTouchToSelectView+5_FiveChoice.swift @@ -7,13 +7,11 @@ import SwiftUI extension ObserveThenTouchToSelectView { struct FiveChoicesView: View { + // MARK: Internal + @ObservedObject var viewModel: TouchToSelectViewViewModel let isTappable: Bool - private let kHorizontalSpacing: CGFloat = 40 - private let kVerticalSpacing: CGFloat = 40 - private let kAnswerSize: CGFloat = 140 - var body: some View { VStack(spacing: kVerticalSpacing) { HStack(spacing: kHorizontalSpacing) { @@ -35,6 +33,12 @@ extension ObserveThenTouchToSelectView { } } } + + // MARK: Private + + private let kHorizontalSpacing: CGFloat = 40 + private let kVerticalSpacing: CGFloat = 40 + private let kAnswerSize: CGFloat = 140 } } diff --git a/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/ObserveThenTouchToSelect/ObserveThenTouchToSelectView+6_SixChoice.swift b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/ObserveThenTouchToSelect/ObserveThenTouchToSelectView+6_SixChoice.swift index 742be46f42..cc027f045b 100644 --- a/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/ObserveThenTouchToSelect/ObserveThenTouchToSelectView+6_SixChoice.swift +++ b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/ObserveThenTouchToSelect/ObserveThenTouchToSelectView+6_SixChoice.swift @@ -7,13 +7,11 @@ import SwiftUI extension ObserveThenTouchToSelectView { struct SixChoicesView: View { + // MARK: Internal + @ObservedObject var viewModel: TouchToSelectViewViewModel let isTappable: Bool - private let kHorizontalSpacing: CGFloat = 40 - private let kVerticalSpacing: CGFloat = 40 - private let kAnswerSize: CGFloat = 140 - var body: some View { VStack(spacing: kVerticalSpacing) { HStack(spacing: kHorizontalSpacing) { @@ -35,6 +33,12 @@ extension ObserveThenTouchToSelectView { } } } + + // MARK: Private + + private let kHorizontalSpacing: CGFloat = 40 + private let kVerticalSpacing: CGFloat = 40 + private let kAnswerSize: CGFloat = 140 } } diff --git a/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/RobotThenTouchToSelect/RobotThenTouchToSelectView+1_OneChoice.swift b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/RobotThenTouchToSelect/RobotThenTouchToSelectView+1_OneChoice.swift index 77b9ab5457..23b58adc4e 100644 --- a/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/RobotThenTouchToSelect/RobotThenTouchToSelectView+1_OneChoice.swift +++ b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/RobotThenTouchToSelect/RobotThenTouchToSelectView+1_OneChoice.swift @@ -7,11 +7,11 @@ import SwiftUI extension RobotThenTouchToSelectView { struct OneChoiceView: View { + // MARK: Internal + @ObservedObject var viewModel: TouchToSelectViewViewModel let isTappable: Bool - private let kAnswerSize: CGFloat = 300 - var body: some View { let choice = viewModel.choices[0] TouchToSelectChoiceView(choice: choice, size: kAnswerSize, isTappable: isTappable) @@ -19,6 +19,10 @@ extension RobotThenTouchToSelectView { viewModel.onChoiceTapped(choice: choice) } } + + // MARK: Private + + private let kAnswerSize: CGFloat = 300 } } diff --git a/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/RobotThenTouchToSelect/RobotThenTouchToSelectView+2_TwoChoice.swift b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/RobotThenTouchToSelect/RobotThenTouchToSelectView+2_TwoChoice.swift index dc4440d5e1..a66d6240ce 100644 --- a/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/RobotThenTouchToSelect/RobotThenTouchToSelectView+2_TwoChoice.swift +++ b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/RobotThenTouchToSelect/RobotThenTouchToSelectView+2_TwoChoice.swift @@ -7,12 +7,11 @@ import SwiftUI extension RobotThenTouchToSelectView { struct TwoChoicesView: View { + // MARK: Internal + @ObservedObject var viewModel: TouchToSelectViewViewModel let isTappable: Bool - private let kHorizontalSpacing: CGFloat = 100 - private let kAnswerSize: CGFloat = 300 - var body: some View { HStack(spacing: kHorizontalSpacing) { ForEach(viewModel.choices) { choice in @@ -23,6 +22,11 @@ extension RobotThenTouchToSelectView { } } } + + // MARK: Private + + private let kHorizontalSpacing: CGFloat = 100 + private let kAnswerSize: CGFloat = 300 } } diff --git a/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/RobotThenTouchToSelect/RobotThenTouchToSelectView+3_ThreeChoice.swift b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/RobotThenTouchToSelect/RobotThenTouchToSelectView+3_ThreeChoice.swift index 09cb53254f..9f253a1bcd 100644 --- a/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/RobotThenTouchToSelect/RobotThenTouchToSelectView+3_ThreeChoice.swift +++ b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/RobotThenTouchToSelect/RobotThenTouchToSelectView+3_ThreeChoice.swift @@ -7,13 +7,11 @@ import SwiftUI extension RobotThenTouchToSelectView { struct ThreeChoicesView: View { + // MARK: Internal + @ObservedObject var viewModel: TouchToSelectViewViewModel let isTappable: Bool - private let kHorizontalSpacing: CGFloat = 200 - private let kVerticalSpacing: CGFloat = 40 - private let kAnswerSize: CGFloat = 240 - var body: some View { VStack(spacing: kVerticalSpacing) { HStack(spacing: kHorizontalSpacing) { @@ -31,6 +29,12 @@ extension RobotThenTouchToSelectView { } } } + + // MARK: Private + + private let kHorizontalSpacing: CGFloat = 200 + private let kVerticalSpacing: CGFloat = 40 + private let kAnswerSize: CGFloat = 240 } } diff --git a/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/RobotThenTouchToSelect/RobotThenTouchToSelectView+4_FourChoice.swift b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/RobotThenTouchToSelect/RobotThenTouchToSelectView+4_FourChoice.swift index 1de59bd9a6..4536d04469 100644 --- a/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/RobotThenTouchToSelect/RobotThenTouchToSelectView+4_FourChoice.swift +++ b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/RobotThenTouchToSelect/RobotThenTouchToSelectView+4_FourChoice.swift @@ -7,13 +7,11 @@ import SwiftUI extension RobotThenTouchToSelectView { struct FourChoicesView: View { + // MARK: Internal + @ObservedObject var viewModel: TouchToSelectViewViewModel let isTappable: Bool - private let kHorizontalSpacing: CGFloat = 200 - private let kVerticalSpacing: CGFloat = 40 - private let kAnswerSize: CGFloat = 240 - var body: some View { VStack(spacing: kVerticalSpacing) { HStack(spacing: kHorizontalSpacing) { @@ -35,6 +33,12 @@ extension RobotThenTouchToSelectView { } } } + + // MARK: Private + + private let kHorizontalSpacing: CGFloat = 200 + private let kVerticalSpacing: CGFloat = 40 + private let kAnswerSize: CGFloat = 240 } } diff --git a/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/RobotThenTouchToSelect/RobotThenTouchToSelectView+5_FiveChoice.swift b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/RobotThenTouchToSelect/RobotThenTouchToSelectView+5_FiveChoice.swift index 9de26f325f..e80ae5f0fd 100644 --- a/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/RobotThenTouchToSelect/RobotThenTouchToSelectView+5_FiveChoice.swift +++ b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/RobotThenTouchToSelect/RobotThenTouchToSelectView+5_FiveChoice.swift @@ -7,13 +7,11 @@ import SwiftUI extension RobotThenTouchToSelectView { struct FiveChoicesView: View { + // MARK: Internal + @ObservedObject var viewModel: TouchToSelectViewViewModel let isTappable: Bool - private let kHorizontalSpacing: CGFloat = 60 - private let kVerticalSpacing: CGFloat = 40 - private let kAnswerSize: CGFloat = 200 - var body: some View { VStack(spacing: kVerticalSpacing) { HStack(spacing: kHorizontalSpacing) { @@ -35,6 +33,12 @@ extension RobotThenTouchToSelectView { } } } + + // MARK: Private + + private let kHorizontalSpacing: CGFloat = 60 + private let kVerticalSpacing: CGFloat = 40 + private let kAnswerSize: CGFloat = 200 } } diff --git a/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/RobotThenTouchToSelect/RobotThenTouchToSelectView+6_SixChoice.swift b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/RobotThenTouchToSelect/RobotThenTouchToSelectView+6_SixChoice.swift index dddfc6a29d..a03a61fb35 100644 --- a/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/RobotThenTouchToSelect/RobotThenTouchToSelectView+6_SixChoice.swift +++ b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/RobotThenTouchToSelect/RobotThenTouchToSelectView+6_SixChoice.swift @@ -7,13 +7,11 @@ import SwiftUI extension RobotThenTouchToSelectView { struct SixChoicesView: View { + // MARK: Internal + @ObservedObject var viewModel: TouchToSelectViewViewModel let isTappable: Bool - private let kHorizontalSpacing: CGFloat = 60 - private let kVerticalSpacing: CGFloat = 40 - private let kAnswerSize: CGFloat = 200 - var body: some View { VStack(spacing: kVerticalSpacing) { HStack(spacing: kHorizontalSpacing) { @@ -35,6 +33,12 @@ extension RobotThenTouchToSelectView { } } } + + // MARK: Private + + private let kHorizontalSpacing: CGFloat = 60 + private let kVerticalSpacing: CGFloat = 40 + private let kAnswerSize: CGFloat = 200 } } diff --git a/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/RobotThenTouchToSelect/RobotThenTouchToSelectView.swift b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/RobotThenTouchToSelect/RobotThenTouchToSelectView.swift index b3fa698e97..cbe44182b6 100644 --- a/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/RobotThenTouchToSelect/RobotThenTouchToSelectView.swift +++ b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/RobotThenTouchToSelect/RobotThenTouchToSelectView.swift @@ -9,21 +9,7 @@ import RobotKit import SwiftUI public struct RobotThenTouchToSelectView: View { - enum Interface: Int { - case oneChoice = 1 - case twoChoices - case threeChoices - case fourChoices - case fiveChoices - case sixChoices - } - - @StateObject private var viewModel: TouchToSelectViewViewModel - @State private var didSendCommandToRobot = false - - private let actionType: Exercise.Action.ActionType - - let robot = Robot.shared + // MARK: Lifecycle public init(choices: [TouchToSelect.Choice]) { self._viewModel = StateObject(wrappedValue: TouchToSelectViewViewModel(choices: choices)) @@ -48,6 +34,8 @@ public struct RobotThenTouchToSelectView: View { self.robot.blacken(.all) } + // MARK: Public + public var body: some View { let interface = Interface(rawValue: viewModel.choices.count) @@ -138,4 +126,24 @@ public struct RobotThenTouchToSelectView: View { Spacer() } } + + // MARK: Internal + + enum Interface: Int { + case oneChoice = 1 + case twoChoices + case threeChoices + case fourChoices + case fiveChoices + case sixChoices + } + + let robot = Robot.shared + + // MARK: Private + + @StateObject private var viewModel: TouchToSelectViewViewModel + @State private var didSendCommandToRobot = false + + private let actionType: Exercise.Action.ActionType } diff --git a/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/TouchToSelect/TouchToSelectChoiceView.swift b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/TouchToSelect/TouchToSelectChoiceView.swift index 06502dc589..a3cd4c4e63 100644 --- a/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/TouchToSelect/TouchToSelectChoiceView.swift +++ b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/TouchToSelect/TouchToSelectChoiceView.swift @@ -6,11 +6,7 @@ import ContentKit import SwiftUI struct TouchToSelectChoiceView: View { - let choice: TouchToSelect.Choice - let state: GameplayChoiceState - - let size: CGFloat - var isTappable = true + // MARK: Lifecycle private init(choice: TouchToSelect.Choice, state: GameplayChoiceState, size: CGFloat, isTappable: Bool = true) { self.choice = choice @@ -23,6 +19,14 @@ struct TouchToSelectChoiceView: View { self.init(choice: choice.choice, state: choice.state, size: size, isTappable: isTappable) } + // MARK: Internal + + let choice: TouchToSelect.Choice + let state: GameplayChoiceState + + let size: CGFloat + var isTappable = true + var body: some View { // TODO(@ladislas): Add text Group { diff --git a/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/TouchToSelect/TouchToSelectView+1_OneChoice.swift b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/TouchToSelect/TouchToSelectView+1_OneChoice.swift index 50d708ce4f..85bae00949 100644 --- a/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/TouchToSelect/TouchToSelectView+1_OneChoice.swift +++ b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/TouchToSelect/TouchToSelectView+1_OneChoice.swift @@ -7,9 +7,9 @@ import SwiftUI extension TouchToSelectView { struct OneChoiceView: View { - @ObservedObject var viewModel: TouchToSelectViewViewModel + // MARK: Internal - private let kAnswerSize: CGFloat = 300 + @ObservedObject var viewModel: TouchToSelectViewViewModel var body: some View { let choice = viewModel.choices[0] @@ -18,6 +18,10 @@ extension TouchToSelectView { viewModel.onChoiceTapped(choice: choice) } } + + // MARK: Private + + private let kAnswerSize: CGFloat = 300 } } diff --git a/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/TouchToSelect/TouchToSelectView+2_TwoChoices.swift b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/TouchToSelect/TouchToSelectView+2_TwoChoices.swift index 2de34c1c14..182b456763 100644 --- a/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/TouchToSelect/TouchToSelectView+2_TwoChoices.swift +++ b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/TouchToSelect/TouchToSelectView+2_TwoChoices.swift @@ -7,10 +7,9 @@ import SwiftUI extension TouchToSelectView { struct TwoChoicesView: View { - @ObservedObject var viewModel: TouchToSelectViewViewModel + // MARK: Internal - private let kHorizontalSpacing: CGFloat = 150 - private let kAnswerSize: CGFloat = 300 + @ObservedObject var viewModel: TouchToSelectViewViewModel var body: some View { HStack(spacing: kHorizontalSpacing) { @@ -22,6 +21,11 @@ extension TouchToSelectView { } } } + + // MARK: Private + + private let kHorizontalSpacing: CGFloat = 150 + private let kAnswerSize: CGFloat = 300 } } diff --git a/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/TouchToSelect/TouchToSelectView+3_ThreeChoices.swift b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/TouchToSelect/TouchToSelectView+3_ThreeChoices.swift index 3fc4cacf8d..4b64cd3c66 100644 --- a/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/TouchToSelect/TouchToSelectView+3_ThreeChoices.swift +++ b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/TouchToSelect/TouchToSelectView+3_ThreeChoices.swift @@ -7,10 +7,9 @@ import SwiftUI extension TouchToSelectView { struct ThreeChoicesView: View { - @ObservedObject var viewModel: TouchToSelectViewViewModel + // MARK: Internal - private let kHorizontalSpacing: CGFloat = 80 - private let kAnswerSize: CGFloat = 280 + @ObservedObject var viewModel: TouchToSelectViewViewModel var body: some View { HStack(spacing: kHorizontalSpacing) { @@ -22,6 +21,11 @@ extension TouchToSelectView { } } } + + // MARK: Private + + private let kHorizontalSpacing: CGFloat = 80 + private let kAnswerSize: CGFloat = 280 } } diff --git a/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/TouchToSelect/TouchToSelectView+4_FourChoices.swift b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/TouchToSelect/TouchToSelectView+4_FourChoices.swift index 78194553c8..de55783d64 100644 --- a/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/TouchToSelect/TouchToSelectView+4_FourChoices.swift +++ b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/TouchToSelect/TouchToSelectView+4_FourChoices.swift @@ -7,11 +7,9 @@ import SwiftUI extension TouchToSelectView { struct FourChoicesView: View { - @ObservedObject var viewModel: TouchToSelectViewViewModel + // MARK: Internal - private let kHorizontalSpacing: CGFloat = 200 - private let kVerticalSpacing: CGFloat = 40 - private let kAnswerSize: CGFloat = 240 + @ObservedObject var viewModel: TouchToSelectViewViewModel var body: some View { VStack(spacing: kVerticalSpacing) { @@ -34,6 +32,12 @@ extension TouchToSelectView { } } } + + // MARK: Private + + private let kHorizontalSpacing: CGFloat = 200 + private let kVerticalSpacing: CGFloat = 40 + private let kAnswerSize: CGFloat = 240 } } diff --git a/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/TouchToSelect/TouchToSelectView+5_FiveChoices.swift b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/TouchToSelect/TouchToSelectView+5_FiveChoices.swift index b68861fa5b..efc4699c34 100644 --- a/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/TouchToSelect/TouchToSelectView+5_FiveChoices.swift +++ b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/TouchToSelect/TouchToSelectView+5_FiveChoices.swift @@ -7,11 +7,9 @@ import SwiftUI extension TouchToSelectView { struct FiveChoicesView: View { - @ObservedObject var viewModel: TouchToSelectViewViewModel + // MARK: Internal - private let kHorizontalSpacing: CGFloat = 60 - private let kVerticalSpacing: CGFloat = 40 - private let kAnswerSize: CGFloat = 240 + @ObservedObject var viewModel: TouchToSelectViewViewModel var body: some View { VStack(spacing: kVerticalSpacing) { @@ -34,6 +32,12 @@ extension TouchToSelectView { } } } + + // MARK: Private + + private let kHorizontalSpacing: CGFloat = 60 + private let kVerticalSpacing: CGFloat = 40 + private let kAnswerSize: CGFloat = 240 } } diff --git a/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/TouchToSelect/TouchToSelectView+6_SixChoices.swift b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/TouchToSelect/TouchToSelectView+6_SixChoices.swift index 7be4fd3eb4..44af876432 100644 --- a/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/TouchToSelect/TouchToSelectView+6_SixChoices.swift +++ b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/TouchToSelect/TouchToSelectView+6_SixChoices.swift @@ -7,11 +7,9 @@ import SwiftUI extension TouchToSelectView { struct SixChoicesView: View { - @ObservedObject var viewModel: TouchToSelectViewViewModel + // MARK: Internal - private let kHorizontalSpacing: CGFloat = 60 - private let kVerticalSpacing: CGFloat = 40 - private let kAnswerSize: CGFloat = 240 + @ObservedObject var viewModel: TouchToSelectViewViewModel var body: some View { VStack(spacing: kVerticalSpacing) { @@ -34,6 +32,12 @@ extension TouchToSelectView { } } } + + // MARK: Private + + private let kHorizontalSpacing: CGFloat = 60 + private let kVerticalSpacing: CGFloat = 40 + private let kAnswerSize: CGFloat = 240 } } diff --git a/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/TouchToSelect/TouchToSelectView.swift b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/TouchToSelect/TouchToSelectView.swift index 1bddfc6937..b2827147e2 100644 --- a/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/TouchToSelect/TouchToSelectView.swift +++ b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/TouchToSelect/TouchToSelectView.swift @@ -7,16 +7,7 @@ import ContentKit import SwiftUI public struct TouchToSelectView: View { - enum Interface: Int { - case oneChoice = 1 - case twoChoices - case threeChoices - case fourChoices - case fiveChoices - case sixChoices - } - - @StateObject private var viewModel: TouchToSelectViewViewModel + // MARK: Lifecycle public init(choices: [TouchToSelect.Choice], shuffle: Bool = false) { self._viewModel = StateObject(wrappedValue: TouchToSelectViewViewModel(choices: choices, shuffle: shuffle)) @@ -32,6 +23,8 @@ public struct TouchToSelectView: View { choices: payload.choices, shuffle: payload.shuffleChoices, shared: data)) } + // MARK: Public + public var body: some View { let interface = Interface(rawValue: viewModel.choices.count) @@ -58,4 +51,19 @@ public struct TouchToSelectView: View { Text("❌ Interface not available for \(viewModel.choices.count) choices") } } + + // MARK: Internal + + enum Interface: Int { + case oneChoice = 1 + case twoChoices + case threeChoices + case fourChoices + case fiveChoices + case sixChoices + } + + // MARK: Private + + @StateObject private var viewModel: TouchToSelectViewViewModel } diff --git a/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/TouchToSelect/TouchToSelectViewViewModel.swift b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/TouchToSelect/TouchToSelectViewViewModel.swift index 36b410bcac..277b13b106 100644 --- a/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/TouchToSelect/TouchToSelectViewViewModel.swift +++ b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Touch/TouchToSelect/TouchToSelectViewViewModel.swift @@ -7,11 +7,7 @@ import ContentKit import SwiftUI class TouchToSelectViewViewModel: ObservableObject { - @Published var choices: [GameplayTouchToSelectChoiceModel] = [] - @ObservedObject var exercicesSharedData: ExerciseSharedData - - private let gameplay: GameplayFindTheRightAnswers - private var cancellables: Set = [] + // MARK: Lifecycle init(choices: [TouchToSelect.Choice], shuffle: Bool = false, shared: ExerciseSharedData? = nil) { self.gameplay = GameplayFindTheRightAnswers( @@ -22,10 +18,22 @@ class TouchToSelectViewViewModel: ObservableObject { subscribeToGameplayStateUpdates() } + // MARK: Public + public func onChoiceTapped(choice: GameplayTouchToSelectChoiceModel) { gameplay.process(choice) } + // MARK: Internal + + @Published var choices: [GameplayTouchToSelectChoiceModel] = [] + @ObservedObject var exercicesSharedData: ExerciseSharedData + + // MARK: Private + + private let gameplay: GameplayFindTheRightAnswers + private var cancellables: Set = [] + private func subscribeToGameplaySelectionChoicesUpdates() { gameplay.choices .receive(on: DispatchQueue.main) diff --git a/Modules/GameEngineKit/Sources/_NewSystem/Utils/AudioPlayer.swift b/Modules/GameEngineKit/Sources/_NewSystem/Utils/AudioPlayer.swift index 479728db12..12d9c01661 100644 --- a/Modules/GameEngineKit/Sources/_NewSystem/Utils/AudioPlayer.swift +++ b/Modules/GameEngineKit/Sources/_NewSystem/Utils/AudioPlayer.swift @@ -10,12 +10,7 @@ import Foundation // MARK: - AudioPlayer public class AudioPlayer: NSObject, ObservableObject { - @Published var progress: CGFloat = 0.0 - @Published var didFinishPlaying = false - - private var player: AVAudioPlayer! - - private var cancellables: Set = [] + // MARK: Lifecycle public init(audioRecording: AudioRecording) { super.init() @@ -23,6 +18,15 @@ public class AudioPlayer: NSObject, ObservableObject { didFinishPlaying = false } + // MARK: Internal + + @Published var progress: CGFloat = 0.0 + @Published var didFinishPlaying = false + + var isPlaying: Bool { + self.player.isPlaying + } + func setAudioPlayer(audioRecording: AudioRecording) { progress = 0.0 didFinishPlaying = false @@ -68,9 +72,11 @@ public class AudioPlayer: NSObject, ObservableObject { didFinishPlaying = true } - var isPlaying: Bool { - self.player.isPlaying - } + // MARK: Private + + private var player: AVAudioPlayer! + + private var cancellables: Set = [] } // MARK: AVAudioPlayerDelegate diff --git a/Modules/GameEngineKit/Sources/_NewSystem/Utils/MIDIInstrument.swift b/Modules/GameEngineKit/Sources/_NewSystem/Utils/MIDIInstrument.swift index db4f8f5489..c4e699a1db 100644 --- a/Modules/GameEngineKit/Sources/_NewSystem/Utils/MIDIInstrument.swift +++ b/Modules/GameEngineKit/Sources/_NewSystem/Utils/MIDIInstrument.swift @@ -9,6 +9,8 @@ import AudioKit enum MIDIInstrument: String { case xylophone + // MARK: Internal + var samples: [MIDISample] { switch self { case .xylophone: diff --git a/Modules/GameEngineKit/Sources/_NewSystem/Utils/MIDIPlayer.swift b/Modules/GameEngineKit/Sources/_NewSystem/Utils/MIDIPlayer.swift index 95b77fbc46..3a2b86c54d 100644 --- a/Modules/GameEngineKit/Sources/_NewSystem/Utils/MIDIPlayer.swift +++ b/Modules/GameEngineKit/Sources/_NewSystem/Utils/MIDIPlayer.swift @@ -8,10 +8,7 @@ import ContentKit import SwiftUI class MIDIPlayer: ObservableObject { - private let engine = AudioEngine() - private let sampler = MIDISampler() - private let sequencer = AppleSequencer() - private let instrument = MIDICallbackInstrument() + // MARK: Lifecycle init(instrument: MIDIInstrument) { engine.output = sampler @@ -20,24 +17,7 @@ class MIDIPlayer: ObservableObject { startAudioEngine() } - private func startAudioEngine() { - do { - try engine.start() - } catch { - print("Could not start AudioKit") - } - } - - private func loadInstrument(samples: [MIDISample]) { - do { - let files = samples.compactMap { - $0.audioFile - } - try sampler.loadAudioFiles(files) - } catch { - print("Could not load file") - } - } + // MARK: Internal func loadMIDIFile(fileURL: URL, tempo: Double) { sequencer.loadMIDIFile(fromURL: fileURL) @@ -66,4 +46,30 @@ class MIDIPlayer: ObservableObject { // TODO(@hugo): BUG - length is not correct, it returns 32 seconds but the track is ~12 secondes long sequencer.tracks[1].length } + + // MARK: Private + + private let engine = AudioEngine() + private let sampler = MIDISampler() + private let sequencer = AppleSequencer() + private let instrument = MIDICallbackInstrument() + + private func startAudioEngine() { + do { + try engine.start() + } catch { + print("Could not start AudioKit") + } + } + + private func loadInstrument(samples: [MIDISample]) { + do { + let files = samples.compactMap { + $0.audioFile + } + try sampler.loadAudioFiles(files) + } catch { + print("Could not load file") + } + } } diff --git a/Modules/GameEngineKit/Sources/_NewSystem/Utils/MIDISample.swift b/Modules/GameEngineKit/Sources/_NewSystem/Utils/MIDISample.swift index 22e26f71ea..de6eafcbe0 100644 --- a/Modules/GameEngineKit/Sources/_NewSystem/Utils/MIDISample.swift +++ b/Modules/GameEngineKit/Sources/_NewSystem/Utils/MIDISample.swift @@ -6,9 +6,7 @@ import AVFAudio import SwiftUI struct MIDISample { - var fileName: String - var midiNote: Int - var audioFile: AVAudioFile? + // MARK: Lifecycle init(file: String, note: Int) { fileName = file @@ -21,4 +19,10 @@ struct MIDISample { fatalError("Could not load: \(fileName)") } } + + // MARK: Internal + + var fileName: String + var midiNote: Int + var audioFile: AVAudioFile? } diff --git a/Modules/GameEngineKit/Sources/_NewSystem/Utils/MIDIScale.swift b/Modules/GameEngineKit/Sources/_NewSystem/Utils/MIDIScale.swift index fa5455d230..5204f2ac2f 100644 --- a/Modules/GameEngineKit/Sources/_NewSystem/Utils/MIDIScale.swift +++ b/Modules/GameEngineKit/Sources/_NewSystem/Utils/MIDIScale.swift @@ -8,6 +8,8 @@ enum MIDIScale: String { case majorPentatonic case majorHeptatonic + // MARK: Internal + var notes: [MIDINoteNumber] { switch self { case .majorPentatonic: diff --git a/Modules/GameEngineKit/Sources/_NewSystem/Views/Activity/ActivityProgressBar.swift b/Modules/GameEngineKit/Sources/_NewSystem/Views/Activity/ActivityProgressBar.swift index c21ab4cc4f..18d2d8c927 100644 --- a/Modules/GameEngineKit/Sources/_NewSystem/Views/Activity/ActivityProgressBar.swift +++ b/Modules/GameEngineKit/Sources/_NewSystem/Views/Activity/ActivityProgressBar.swift @@ -6,11 +6,11 @@ import DesignKit import SwiftUI struct ActivityProgressBar: View { + // MARK: Internal + @ObservedObject var viewModel: ActivityViewViewModel let height: CGFloat = 30 - @State private var currentColor: Color = .white - var body: some View { HStack(spacing: 0) { Spacer() @@ -62,4 +62,8 @@ struct ActivityProgressBar: View { Spacer() } } + + // MARK: Private + + @State private var currentColor: Color = .white } diff --git a/Modules/GameEngineKit/Sources/_NewSystem/Views/Activity/ActivityView.swift b/Modules/GameEngineKit/Sources/_NewSystem/Views/Activity/ActivityView.swift index 95217a2911..279ca42803 100644 --- a/Modules/GameEngineKit/Sources/_NewSystem/Views/Activity/ActivityView.swift +++ b/Modules/GameEngineKit/Sources/_NewSystem/Views/Activity/ActivityView.swift @@ -7,14 +7,14 @@ import RobotKit import SwiftUI public struct ActivityView: View { - @Environment(\.dismiss) var dismiss - - @ObservedObject var viewModel: ActivityViewViewModel + // MARK: Lifecycle public init(viewModel: ActivityViewViewModel) { self.viewModel = viewModel } + // MARK: Public + public var body: some View { NavigationStack { ZStack(alignment: .bottomTrailing) { @@ -73,6 +73,31 @@ public struct ActivityView: View { } } + // MARK: Internal + + @Environment(\.dismiss) var dismiss + + @ObservedObject var viewModel: ActivityViewViewModel + + // MARK: Private + + @ViewBuilder + private var continueButton: some View { + let state = viewModel.currentExerciseSharedData.state + + if state != .completed { + EmptyView() + } else { + Button("Continuer") { + viewModel.isLastExercise ? dismiss() : viewModel.moveToNextExercise() + } + .buttonStyle(.borderedProminent) + .tint(.green) + .padding() + .transition(.asymmetric(insertion: .opacity.animation(.snappy.delay(2)), removal: .identity)) + } + } + @ViewBuilder private func currentExerciseInterface() -> some View { switch viewModel.currentExerciseInterface { @@ -137,23 +162,6 @@ public struct ActivityView: View { ) } } - - @ViewBuilder - private var continueButton: some View { - let state = viewModel.currentExerciseSharedData.state - - if state != .completed { - EmptyView() - } else { - Button("Continuer") { - viewModel.isLastExercise ? dismiss() : viewModel.moveToNextExercise() - } - .buttonStyle(.borderedProminent) - .tint(.green) - .padding() - .transition(.asymmetric(insertion: .opacity.animation(.snappy.delay(2)), removal: .identity)) - } - } } #Preview { diff --git a/Modules/GameEngineKit/Sources/_NewSystem/Views/Activity/ActivityViewViewModel.swift b/Modules/GameEngineKit/Sources/_NewSystem/Views/Activity/ActivityViewViewModel.swift index e6dccf6f8a..69372916ae 100644 --- a/Modules/GameEngineKit/Sources/_NewSystem/Views/Activity/ActivityViewViewModel.swift +++ b/Modules/GameEngineKit/Sources/_NewSystem/Views/Activity/ActivityViewViewModel.swift @@ -7,21 +7,7 @@ import ContentKit import SwiftUI public class ActivityViewViewModel: ObservableObject { - private let sequenceManager: ActivitySequenceManager - - private var cancellables: Set = [] - - @Published var currentActivity: Activity - - @Published var totalSequences: Int - @Published var currentSequenceIndex: Int - - @Published var totalExercisesInCurrentSequence: Int - @Published var currentExerciseIndexInSequence: Int - - @Published var currentExercise: Exercise - @Published var currentExerciseInterface: Exercise.Interface - @Published var currentExerciseSharedData: ExerciseSharedData + // MARK: Lifecycle public init(activity: Activity) { self.sequenceManager = ActivitySequenceManager(activity: activity) @@ -41,6 +27,28 @@ public class ActivityViewViewModel: ObservableObject { subscribeToCurrentExerciseSharedDataUpdates() } + // MARK: Internal + + @Published var currentActivity: Activity + + @Published var totalSequences: Int + @Published var currentSequenceIndex: Int + + @Published var totalExercisesInCurrentSequence: Int + @Published var currentExerciseIndexInSequence: Int + + @Published var currentExercise: Exercise + @Published var currentExerciseInterface: Exercise.Interface + @Published var currentExerciseSharedData: ExerciseSharedData + + var isFirstExercise: Bool { + sequenceManager.isFirstExercise + } + + var isLastExercise: Bool { + sequenceManager.isLastExercise + } + func moveToNextExercise() { sequenceManager.moveToNextExercise() updateValues() @@ -51,13 +59,11 @@ public class ActivityViewViewModel: ObservableObject { updateValues() } - var isFirstExercise: Bool { - sequenceManager.isFirstExercise - } + // MARK: Private - var isLastExercise: Bool { - sequenceManager.isLastExercise - } + private let sequenceManager: ActivitySequenceManager + + private var cancellables: Set = [] private func updateValues() { currentExercise = sequenceManager.currentExercise diff --git a/Modules/GameEngineKit/Sources/_NewSystem/Views/Exercise/ExerciseInstructionsButton.swift b/Modules/GameEngineKit/Sources/_NewSystem/Views/Exercise/ExerciseInstructionsButton.swift index ff6e63bf3d..99a473c5a6 100644 --- a/Modules/GameEngineKit/Sources/_NewSystem/Views/Exercise/ExerciseInstructionsButton.swift +++ b/Modules/GameEngineKit/Sources/_NewSystem/Views/Exercise/ExerciseInstructionsButton.swift @@ -10,9 +10,8 @@ import SwiftUI // TODO(@ladislas): refactor speech synth into own class class SpeakerViewModel: NSObject, ObservableObject, AVSpeechSynthesizerDelegate { - @Published var isSpeaking = false + // MARK: Lifecycle - private var synthesizer = AVSpeechSynthesizer() override init() { super.init() synthesizer.delegate = self @@ -22,6 +21,10 @@ class SpeakerViewModel: NSObject, ObservableObject, AVSpeechSynthesizerDelegate synthesizer.delegate = nil } + // MARK: Internal + + @Published var isSpeaking = false + func speak(sentence: String) { let utterance = AVSpeechUtterance(string: sentence) utterance.rate = 0.40 @@ -39,6 +42,10 @@ class SpeakerViewModel: NSObject, ObservableObject, AVSpeechSynthesizerDelegate internal func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didFinish utterance: AVSpeechUtterance) { self.isSpeaking = false } + + // MARK: Private + + private var synthesizer = AVSpeechSynthesizer() } // MARK: - ExerciseInstructionsButton @@ -58,6 +65,8 @@ struct ExerciseInstructionsButton: View { // MARK: - StepInstructions_ButtonStyle struct StepInstructions_ButtonStyle: ButtonStyle { + // MARK: Internal + @Binding var isSpeaking: Bool func makeBody(configuration: Self.Configuration) -> some View { @@ -90,6 +99,8 @@ struct StepInstructions_ButtonStyle: ButtonStyle { .animation(.easeOut(duration: 0.2), value: isSpeaking) } + // MARK: Private + private var backgroundGradient: some View { ZStack { Color.white diff --git a/Modules/LogKit/Examples/LogKitExample/Sources/NewModule.swift b/Modules/LogKit/Examples/LogKitExample/Sources/NewModule.swift index 0274711e8c..f2e58bfa4e 100644 --- a/Modules/LogKit/Examples/LogKitExample/Sources/NewModule.swift +++ b/Modules/LogKit/Examples/LogKitExample/Sources/NewModule.swift @@ -6,13 +6,19 @@ import Foundation import LogKit public struct NewModule { - private let log = LogKit.createLoggerFor(module: "NewModule") + // MARK: Lifecycle public init() { log.info("new module has been initialized") } + // MARK: Public + public func doSomething() { log.info("doing something") } + + // MARK: Private + + private let log = LogKit.createLoggerFor(module: "NewModule") } diff --git a/Modules/LogKit/Sources/LogKit.swift b/Modules/LogKit/Sources/LogKit.swift index 625ccd06d3..8eca7ccd4e 100644 --- a/Modules/LogKit/Sources/LogKit.swift +++ b/Modules/LogKit/Sources/LogKit.swift @@ -5,12 +5,14 @@ import Logging public struct LogKit { - private static var hasBeenInitialized: Bool = false + // MARK: Lifecycle private init() { // nothing to do } + // MARK: Public + public static func createLoggerFor(module: String) -> Logger { createLogger(label: "mod:\(module)") } @@ -19,6 +21,10 @@ public struct LogKit { createLogger(label: "app:\(app)") } + // MARK: Private + + private static var hasBeenInitialized: Bool = false + private static func createLogger(label: String) -> Logger { if !hasBeenInitialized { LoggingSystem.bootstrap(LogKitLogHandler.standardOutput) diff --git a/Modules/LogKit/Sources/LogKitLogHandler.swift b/Modules/LogKit/Sources/LogKitLogHandler.swift index 0e1774ca9f..d1ade226db 100644 --- a/Modules/LogKit/Sources/LogKitLogHandler.swift +++ b/Modules/LogKit/Sources/LogKitLogHandler.swift @@ -32,6 +32,14 @@ internal typealias CFilePointer = UnsafeMutablePointer // MARK: - StdioOutputStream internal struct StdioOutputStream: TextOutputStream { + internal enum FlushMode { + case undefined + case always + } + + internal static let stderr = StdioOutputStream(file: systemStderr, flushMode: .always) + internal static let stdout = StdioOutputStream(file: systemStdout, flushMode: .always) + internal let file: CFilePointer internal let flushMode: FlushMode @@ -58,42 +66,44 @@ internal struct StdioOutputStream: TextOutputStream { contiguousString.makeContiguousUTF8() return contiguousString.utf8 } - - internal static let stderr = StdioOutputStream(file: systemStderr, flushMode: .always) - internal static let stdout = StdioOutputStream(file: systemStdout, flushMode: .always) - - internal enum FlushMode { - case undefined - case always - } } // MARK: - LogKitLogHandler public struct LogKitLogHandler: LogHandler { - internal typealias _SendableTextOutputStream = TextOutputStream & Sendable + // MARK: Lifecycle - /// Factory that makes a `LogKitLogHandler` to directs its output to `stdout` - public static func standardOutput(label: String) -> LogKitLogHandler { - LogKitLogHandler( - label: label, stream: StdioOutputStream.stdout, metadataProvider: LoggingSystem.metadataProvider) + // internal for testing only + internal init(label: String, stream: _SendableTextOutputStream) { + self.init(label: label, stream: stream, metadataProvider: LoggingSystem.metadataProvider) } - /// Factory that makes a `LogKitLogHandler` that directs its output to `stdout` - public static func standardOutput(label: String, metadataProvider: Logger.MetadataProvider?) -> LogKitLogHandler { - LogKitLogHandler(label: label, stream: StdioOutputStream.stdout, metadataProvider: metadataProvider) + // internal for testing only + internal init(label: String, stream: _SendableTextOutputStream, metadataProvider: Logger.MetadataProvider?) { + self.label = label + self.stream = stream + self.metadataProvider = metadataProvider } - private let stream: _SendableTextOutputStream - private let label: String + // MARK: Public public var logLevel: Logger.Level = .trace public var metadataProvider: Logger.MetadataProvider? - private var prettyMetadata: String? public var metadata = Logger.Metadata() + /// Factory that makes a `LogKitLogHandler` to directs its output to `stdout` + public static func standardOutput(label: String) -> LogKitLogHandler { + LogKitLogHandler( + label: label, stream: StdioOutputStream.stdout, metadataProvider: LoggingSystem.metadataProvider) + } + + /// Factory that makes a `LogKitLogHandler` that directs its output to `stdout` + public static func standardOutput(label: String, metadataProvider: Logger.MetadataProvider?) -> LogKitLogHandler { + LogKitLogHandler(label: label, stream: StdioOutputStream.stdout, metadataProvider: metadataProvider) + } + public subscript(metadataKey metadataKey: String) -> Logger.Metadata.Value? { get { self.metadata[metadataKey] @@ -103,18 +113,6 @@ public struct LogKitLogHandler: LogHandler { } } - // internal for testing only - internal init(label: String, stream: _SendableTextOutputStream) { - self.init(label: label, stream: stream, metadataProvider: LoggingSystem.metadataProvider) - } - - // internal for testing only - internal init(label: String, stream: _SendableTextOutputStream, metadataProvider: Logger.MetadataProvider?) { - self.label = label - self.stream = stream - self.metadataProvider = metadataProvider - } - public func log( level: Logger.Level, message: Logger.Message, @@ -131,6 +129,17 @@ public struct LogKitLogHandler: LogHandler { ) } + // MARK: Internal + + internal typealias _SendableTextOutputStream = TextOutputStream & Sendable + + // MARK: Private + + private let stream: _SendableTextOutputStream + private let label: String + + private var prettyMetadata: String? + private func prettyLevel(_ level: Logger.Level) -> String { switch level { case .trace: diff --git a/Modules/RobotKit/Examples/RobotKitExample/Sources/MainView.swift b/Modules/RobotKit/Examples/RobotKitExample/Sources/MainView.swift index 59ce7c700e..853ab8dbfb 100644 --- a/Modules/RobotKit/Examples/RobotKitExample/Sources/MainView.swift +++ b/Modules/RobotKit/Examples/RobotKitExample/Sources/MainView.swift @@ -6,7 +6,7 @@ import RobotKit import SwiftUI struct MainView: View { - @State private var presentRobotConnection: Bool = false + // MARK: Internal var body: some View { NavigationStack { @@ -34,6 +34,10 @@ struct MainView: View { .navigationTitle("RobotKit Explorer") } } + + // MARK: Private + + @State private var presentRobotConnection: Bool = false } #Preview { diff --git a/Modules/RobotKit/Examples/RobotKitExample/Sources/ViewModels/RobotControlViewModel.swift b/Modules/RobotKit/Examples/RobotKitExample/Sources/ViewModels/RobotControlViewModel.swift index 122591b6af..cda0d26952 100644 --- a/Modules/RobotKit/Examples/RobotKitExample/Sources/ViewModels/RobotControlViewModel.swift +++ b/Modules/RobotKit/Examples/RobotKitExample/Sources/ViewModels/RobotControlViewModel.swift @@ -7,11 +7,7 @@ import RobotKit import SwiftUI class RobotControlViewModel: ObservableObject { - @Published var magicCard: MagicCard = .none - @Published var magicCardImage: Image = Image(systemName: "photo") - - private let robot: Robot - private var cancellables: Set = [] + // MARK: Lifecycle init(robot: Robot) { self.robot = robot @@ -34,4 +30,14 @@ class RobotControlViewModel: ObservableObject { } .store(in: &cancellables) } + + // MARK: Internal + + @Published var magicCard: MagicCard = .none + @Published var magicCardImage: Image = Image(systemName: "photo") + + // MARK: Private + + private let robot: Robot + private var cancellables: Set = [] } diff --git a/Modules/RobotKit/Examples/RobotKitExample/Sources/Views/RobotControlView.swift b/Modules/RobotKit/Examples/RobotKitExample/Sources/Views/RobotControlView.swift index f4bbbb021f..2821c44567 100644 --- a/Modules/RobotKit/Examples/RobotKitExample/Sources/Views/RobotControlView.swift +++ b/Modules/RobotKit/Examples/RobotKitExample/Sources/Views/RobotControlView.swift @@ -7,14 +7,16 @@ import RobotKit import SwiftUI struct RobotControlView: View { - @StateObject var viewModel: RobotControlViewModel - - private let robot = Robot.shared + // MARK: Lifecycle init(viewModel: RobotControlViewModel) { self._viewModel = StateObject(wrappedValue: viewModel) } + // MARK: Internal + + @StateObject var viewModel: RobotControlViewModel + var body: some View { VStack { VStack(alignment: .leading, spacing: 30) { @@ -121,6 +123,10 @@ struct RobotControlView: View { } .buttonStyle(.robotControlPlainButtonStyle(foreground: .white, background: .red)) } + + // MARK: Private + + private let robot = Robot.shared } #Preview { diff --git a/Modules/RobotKit/Sources/MagicCards.swift b/Modules/RobotKit/Sources/MagicCards.swift index 365c45c034..c46f79b3d8 100644 --- a/Modules/RobotKit/Sources/MagicCards.swift +++ b/Modules/RobotKit/Sources/MagicCards.swift @@ -9,6 +9,15 @@ import Foundation // swiftlint:disable identifier_name public struct MagicCard: Equatable { + // MARK: Lifecycle + + public init(language: Language = .none, id: UInt16) { + self.language = language + self.id = id + } + + // MARK: Public + public enum Language: UInt8 { case none = 0 case fr_FR = 1 @@ -18,11 +27,6 @@ public struct MagicCard: Equatable { public let language: Language public let id: UInt16 - public init(language: Language = .none, id: UInt16) { - self.language = language - self.id = id - } - public static func == (lhs: MagicCard, rhs: MagicCard) -> Bool { lhs.id == rhs.id } diff --git a/Modules/RobotKit/Sources/Robot+Colors.swift b/Modules/RobotKit/Sources/Robot+Colors.swift index b2d6ffb675..c0ae39db75 100644 --- a/Modules/RobotKit/Sources/Robot+Colors.swift +++ b/Modules/RobotKit/Sources/Robot+Colors.swift @@ -10,8 +10,7 @@ import SwiftUI public extension Robot { struct Color { - private let robotRGB: [UInt8] - private let screenRGB: [UInt8] + // MARK: Lifecycle public init(robot rRGB: UInt8..., screen sRGB: UInt8...) { guard rRGB.count == 3, sRGB.count == 3 else { fatalError() } @@ -20,6 +19,29 @@ public extension Robot { self.screenRGB = sRGB } + public init(from value: String) { + guard let color = ColorString(rawValue: value)?.color else { + fatalError("Invalid color string \(value)") + } + self = color + } + + // MARK: Public + + public var robot: [UInt8] { + robotRGB + } + + public var screen: SwiftUI.Color { + SwiftUI.Color( + red: Double(screenRGB[0]) / 255.0, + green: Double(screenRGB[1]) / 255.0, + blue: Double(screenRGB[2]) / 255.0 + ) + } + + // MARK: Private + private enum ColorString: String { case black case white @@ -32,6 +54,8 @@ public extension Robot { case pink case yellow + // MARK: Public + public var color: Robot.Color { switch self { case .black: @@ -58,24 +82,8 @@ public extension Robot { } } - public init(from value: String) { - guard let color = ColorString(rawValue: value)?.color else { - fatalError("Invalid color string \(value)") - } - self = color - } - - public var robot: [UInt8] { - robotRGB - } - - public var screen: SwiftUI.Color { - SwiftUI.Color( - red: Double(screenRGB[0]) / 255.0, - green: Double(screenRGB[1]) / 255.0, - blue: Double(screenRGB[2]) / 255.0 - ) - } + private let robotRGB: [UInt8] + private let screenRGB: [UInt8] } } diff --git a/Modules/RobotKit/Sources/Robot+Lights.swift b/Modules/RobotKit/Sources/Robot+Lights.swift index 9888d0b649..00ebb1b960 100644 --- a/Modules/RobotKit/Sources/Robot+Lights.swift +++ b/Modules/RobotKit/Sources/Robot+Lights.swift @@ -6,28 +6,62 @@ public extension Robot { enum Lights { + case all(in: Color) + + case full(_ position: Full.Position, in: Color) + + case halfLeft(in: Color) + case halfRight(in: Color) + case quarterFrontLeft(in: Color) + case quarterFrontRight(in: Color) + case quarterBackLeft(in: Color) + case quarterBackRight(in: Color) + + case earLeft(in: Color) + case earRight(in: Color) + + case spot(Spot.Position, ids: [UInt8], in: Color) + case range(start: UInt8, end: UInt8, in: Color) + + // MARK: Public + public enum Spot { - static let id: UInt8 = 0x10 + // MARK: Public + public enum Position: UInt8 { case ears = 0x11 case belt = 0x12 } + + // MARK: Internal + + static let id: UInt8 = 0x10 } public enum Full { - static let id: UInt8 = 0x13 + // MARK: Public + public enum Position: UInt8 { case ears = 0x14 case belt = 0x15 } + + // MARK: Internal + + static let id: UInt8 = 0x13 } public enum Range { - static let id: UInt8 = 0x16 + // MARK: Public + public enum Position: UInt8 { case ears = 0x17 case belt = 0x18 } + + // MARK: Internal + + static let id: UInt8 = 0x16 } public enum Blacken { @@ -47,23 +81,6 @@ public extension Robot { case range(start: UInt8, end: UInt8) } - case all(in: Color) - - case full(_ position: Full.Position, in: Color) - - case halfLeft(in: Color) - case halfRight(in: Color) - case quarterFrontLeft(in: Color) - case quarterFrontRight(in: Color) - case quarterBackLeft(in: Color) - case quarterBackRight(in: Color) - - case earLeft(in: Color) - case earRight(in: Color) - - case spot(Spot.Position, ids: [UInt8], in: Color) - case range(start: UInt8, end: UInt8, in: Color) - public var color: Robot.Color { switch self { case let .all(color), @@ -82,13 +99,7 @@ public extension Robot { } } - static func spot(on position: Spot.Position, ids: UInt8..., in color: Color) -> Self { - .spot(position, ids: ids, in: color) - } - - static func range(startID: UInt8, endID: UInt8, in color: Color) -> Self { - .range(start: startID, end: endID, in: color) - } + // MARK: Internal var cmd: [[UInt8]] { var output: [[UInt8]] = [[]] @@ -161,6 +172,16 @@ public extension Robot { return output } + static func spot(on position: Spot.Position, ids: UInt8..., in color: Color) -> Self { + .spot(position, ids: ids, in: color) + } + + static func range(startID: UInt8, endID: UInt8, in color: Color) -> Self { + .range(start: startID, end: endID, in: color) + } + + // MARK: Private + private func shineSpot(_ id: UInt8, on position: Spot.Position, in color: Color) -> [UInt8] { var payload: [UInt8] = [] diff --git a/Modules/RobotKit/Sources/Robot+Motion.swift b/Modules/RobotKit/Sources/Robot+Motion.swift index 20e3e8d8da..4bc4c47ad0 100644 --- a/Modules/RobotKit/Sources/Robot+Motion.swift +++ b/Modules/RobotKit/Sources/Robot+Motion.swift @@ -12,18 +12,6 @@ extension Float { public extension Robot { enum Motion { - static let id: UInt8 = 0x20 - - enum Motor: UInt8 { - case left = 0x21 - case right = 0x22 - } - - public enum Rotation: UInt8 { - case clockwise = 0x00 - case counterclockwise = 0x01 - } - case stop case free(left: Float, right: Float) @@ -35,6 +23,22 @@ public extension Robot { case backwardRight(speed: Float) case spin(Rotation, speed: Float) + // MARK: Public + + public enum Rotation: UInt8 { + case clockwise = 0x00 + case counterclockwise = 0x01 + } + + // MARK: Internal + + enum Motor: UInt8 { + case left = 0x21 + case right = 0x22 + } + + static let id: UInt8 = 0x20 + var cmd: [[UInt8]] { var output: [[UInt8]] = [[]] @@ -115,6 +119,8 @@ public extension Robot { return output } + // MARK: Private + private func setMotor(_ motor: Motor, speed: Float, rotation: Rotation) -> [UInt8] { let speed: UInt8 = UInt8(abs(speed) * 255) diff --git a/Modules/RobotKit/Sources/Robot+Reinforcers.swift b/Modules/RobotKit/Sources/Robot+Reinforcers.swift index 05e503a9c2..29d3a7f61b 100644 --- a/Modules/RobotKit/Sources/Robot+Reinforcers.swift +++ b/Modules/RobotKit/Sources/Robot+Reinforcers.swift @@ -10,6 +10,8 @@ public extension Robot { case spinBlinkBlueViolet = 0x54 case spinBlinkGreenOff = 0x55 + // MARK: Internal + static let id: UInt8 = 0x50 var cmd: [UInt8] { diff --git a/Modules/RobotKit/Sources/Robot.swift b/Modules/RobotKit/Sources/Robot.swift index 45623cedf3..017689d2e0 100644 --- a/Modules/RobotKit/Sources/Robot.swift +++ b/Modules/RobotKit/Sources/Robot.swift @@ -13,8 +13,26 @@ let log = LogKit.createLoggerFor(module: "RobotKit") // MARK: - Robot public class Robot { + // MARK: Lifecycle + + private init() { + subscribeToBLEConnectionUpdates() + } + + // MARK: Public + public static var shared: Robot = Robot() + // MARK: - Information + + public var isConnected: CurrentValueSubject = CurrentValueSubject(false) + + public var name: CurrentValueSubject = CurrentValueSubject("(robot not connected)") + public var osVersion: CurrentValueSubject = CurrentValueSubject(nil) + public var serialNumber: CurrentValueSubject = CurrentValueSubject("(n/a)") + public var isCharging: CurrentValueSubject = CurrentValueSubject(false) + public var battery: CurrentValueSubject = CurrentValueSubject(0) + // MARK: - Internal properties public var connectedPeripheral: RobotPeripheral? { @@ -31,22 +49,6 @@ public class Robot { } } - var cancellables: Set = [] - - private init() { - subscribeToBLEConnectionUpdates() - } - - // MARK: - Information - - public var isConnected: CurrentValueSubject = CurrentValueSubject(false) - - public var name: CurrentValueSubject = CurrentValueSubject("(robot not connected)") - public var osVersion: CurrentValueSubject = CurrentValueSubject(nil) - public var serialNumber: CurrentValueSubject = CurrentValueSubject("(n/a)") - public var isCharging: CurrentValueSubject = CurrentValueSubject(false) - public var battery: CurrentValueSubject = CurrentValueSubject(0) - // MARK: - General public func stop() { @@ -65,4 +67,8 @@ public class Robot { Just(MagicCard.dice_roll) .eraseToAnyPublisher() } + + // MARK: Internal + + var cancellables: Set = [] } diff --git a/Modules/RobotKit/Sources/UI/Buttons/ButtonBordered.swift b/Modules/RobotKit/Sources/UI/Buttons/ButtonBordered.swift index fd71384c2a..d9f006be20 100644 --- a/Modules/RobotKit/Sources/UI/Buttons/ButtonBordered.swift +++ b/Modules/RobotKit/Sources/UI/Buttons/ButtonBordered.swift @@ -5,10 +5,7 @@ import SwiftUI struct ButtonBordered: View { - private let foreground: Color? - private let border: Color? - private let label: Label - private let action: () -> Void + // MARK: Lifecycle init(tint: Color? = nil, @ViewBuilder label: () -> Label, action: @escaping () -> Void) { self.foreground = tint @@ -24,6 +21,8 @@ struct ButtonBordered: View { self.label = label() } + // MARK: Internal + var body: some View { Button( action: { @@ -35,6 +34,13 @@ struct ButtonBordered: View { ) .buttonStyle(.robotControlBorderedButtonStyle(foreground: foreground, border: border)) } + + // MARK: Private + + private let foreground: Color? + private let border: Color? + private let label: Label + private let action: () -> Void } #Preview { diff --git a/Modules/RobotKit/Sources/UI/Buttons/ButtonFilled.swift b/Modules/RobotKit/Sources/UI/Buttons/ButtonFilled.swift index eeb55cf45d..87caecc32e 100644 --- a/Modules/RobotKit/Sources/UI/Buttons/ButtonFilled.swift +++ b/Modules/RobotKit/Sources/UI/Buttons/ButtonFilled.swift @@ -5,10 +5,7 @@ import SwiftUI struct ButtonFilled: View { - private let foreground: Color - private let background: Color - private let label: Label - private let action: () -> Void + // MARK: Lifecycle init(foreground: Color, background: Color, @ViewBuilder label: () -> Label, action: @escaping () -> Void) { self.foreground = foreground @@ -24,6 +21,8 @@ struct ButtonFilled: View { self.label = label() } + // MARK: Internal + var body: some View { Button( action: { @@ -35,6 +34,13 @@ struct ButtonFilled: View { ) .buttonStyle(.robotControlPlainButtonStyle(foreground: foreground, background: background)) } + + // MARK: Private + + private let foreground: Color + private let background: Color + private let label: Label + private let action: () -> Void } #Preview { diff --git a/Modules/RobotKit/Sources/UI/Buttons/ButtonStyle+Extension.swift b/Modules/RobotKit/Sources/UI/Buttons/ButtonStyle+Extension.swift index 499cf4d2f8..864c7c06dc 100644 --- a/Modules/RobotKit/Sources/UI/Buttons/ButtonStyle+Extension.swift +++ b/Modules/RobotKit/Sources/UI/Buttons/ButtonStyle+Extension.swift @@ -29,14 +29,15 @@ public extension ButtonStyle where Self == RobotControlBorderedButtonStyle { // public struct RobotControlPlainButtonStyle: ButtonStyle { - private let foreground: Color - private let background: Color + // MARK: Lifecycle init(foreground: Color?, background: Color?) { self.foreground = foreground ?? .black self.background = background ?? .white } + // MARK: Public + public func makeBody(configuration: Configuration) -> some View { HStack(spacing: 5) { configuration.label @@ -48,14 +49,17 @@ public struct RobotControlPlainButtonStyle: ButtonStyle { .clipShape(RoundedRectangle(cornerRadius: 10)) .opacity(configuration.isPressed ? 0.8 : 1) } + + // MARK: Private + + private let foreground: Color + private let background: Color } // MARK: - RobotControlBorderedButtonStyle public struct RobotControlBorderedButtonStyle: ButtonStyle { - private let foreground: Color - private let border: Color - private let background: Color + // MARK: Lifecycle init(foreground: Color?, border: Color?, background: Color? = nil) { self.foreground = foreground ?? .accentColor @@ -63,6 +67,8 @@ public struct RobotControlBorderedButtonStyle: ButtonStyle { self.background = background ?? .clear } + // MARK: Public + public func makeBody(configuration: Configuration) -> some View { HStack(spacing: 5) { configuration.label @@ -78,4 +84,10 @@ public struct RobotControlBorderedButtonStyle: ButtonStyle { ) .opacity(configuration.isPressed ? 0.8 : 1) } + + // MARK: Private + + private let foreground: Color + private let border: Color + private let background: Color } diff --git a/Modules/RobotKit/Sources/UI/Buttons/Buttons.swift b/Modules/RobotKit/Sources/UI/Buttons/Buttons.swift index fbbb58c965..a401a070d6 100644 --- a/Modules/RobotKit/Sources/UI/Buttons/Buttons.swift +++ b/Modules/RobotKit/Sources/UI/Buttons/Buttons.swift @@ -5,10 +5,7 @@ import SwiftUI public struct RobotControlActionButton: View { - private let title: String - private let image: String - private let tint: Color - private let action: () -> Void + // MARK: Lifecycle public init(title: String, image: String, tint: Color, action: @escaping () -> Void) { self.title = title @@ -17,6 +14,8 @@ public struct RobotControlActionButton: View { self.action = action } + // MARK: Public + public var body: some View { Button { action() @@ -26,6 +25,13 @@ public struct RobotControlActionButton: View { } .buttonStyle(.robotControlBorderedButtonStyle(foreground: tint, border: tint)) } + + // MARK: Private + + private let title: String + private let image: String + private let tint: Color + private let action: () -> Void } #Preview { diff --git a/Modules/RobotKit/Sources/UI/ViewModels/BatteryViewModel.swift b/Modules/RobotKit/Sources/UI/ViewModels/BatteryViewModel.swift index fc5ac9d298..4cf3a30c0c 100644 --- a/Modules/RobotKit/Sources/UI/ViewModels/BatteryViewModel.swift +++ b/Modules/RobotKit/Sources/UI/ViewModels/BatteryViewModel.swift @@ -6,9 +6,7 @@ import BLEKit import SwiftUI public struct BatteryViewModel: Equatable { - public let level: Int - public let name: String - public let color: Color + // MARK: Lifecycle public init(level: Int) { self.level = level @@ -36,4 +34,10 @@ public struct BatteryViewModel: Equatable { self.color = .gray } } + + // MARK: Public + + public let level: Int + public let name: String + public let color: Color } diff --git a/Modules/RobotKit/Sources/UI/ViewModels/ConnectedRobotInformationViewModel.swift b/Modules/RobotKit/Sources/UI/ViewModels/ConnectedRobotInformationViewModel.swift index ab4d06818b..358c1f0244 100644 --- a/Modules/RobotKit/Sources/UI/ViewModels/ConnectedRobotInformationViewModel.swift +++ b/Modules/RobotKit/Sources/UI/ViewModels/ConnectedRobotInformationViewModel.swift @@ -6,12 +6,14 @@ import Combine import SwiftUI public class ConnectedRobotInformationViewModel: ObservableObject { - @Published public var isConnected: Bool = false { - didSet { - self.isNotConnected = !isConnected - } + // MARK: Lifecycle + + public init() { + getRobotInformation() } + // MARK: Public + @Published public var isNotConnected: Bool = true @Published public var name: String = "(n/a)" @@ -21,13 +23,19 @@ public class ConnectedRobotInformationViewModel: ObservableObject { @Published public var battery: Int = 0 @Published public var isCharging: Bool = false + @Published public var isConnected: Bool = false { + didSet { + self.isNotConnected = !isConnected + } + } + + // MARK: Internal + let robot = Robot.shared - private var cancellables: Set = [] + // MARK: Private - public init() { - getRobotInformation() - } + private var cancellables: Set = [] private func getRobotInformation() { robot.isConnected diff --git a/Modules/RobotKit/Sources/UI/ViewModels/RobotConnectionViewModel.swift b/Modules/RobotKit/Sources/UI/ViewModels/RobotConnectionViewModel.swift index 18c466e99f..a59e6ce00a 100644 --- a/Modules/RobotKit/Sources/UI/ViewModels/RobotConnectionViewModel.swift +++ b/Modules/RobotKit/Sources/UI/ViewModels/RobotConnectionViewModel.swift @@ -7,27 +7,14 @@ import Combine import Foundation public class RobotConnectionViewModel: ObservableObject { - @Published var robotDiscoveries: [RobotDiscoveryModel] = [] - @Published var selectedDiscovery: RobotDiscoveryModel? - - @Published var connectedDiscovery: RobotDiscoveryModel? { - didSet { - connected = connectedDiscovery != nil - } - } - - @Published var connected: Bool = false - - private let robot = Robot.shared - private let bleManager = BLEManager.shared - - private var cancellables: Set = [] - private var scanCancellable: AnyCancellable? + // MARK: Lifecycle public init() { self.connected = bleManager.isConnected } + // MARK: Public + public func select(discovery: RobotDiscoveryModel) { if selectedDiscovery == discovery { selectedDiscovery = nil @@ -80,4 +67,25 @@ public class RobotConnectionViewModel: ObservableObject { bleManager.disconnect() connectedDiscovery = nil } + + // MARK: Internal + + @Published var robotDiscoveries: [RobotDiscoveryModel] = [] + @Published var selectedDiscovery: RobotDiscoveryModel? + + @Published var connected: Bool = false + + @Published var connectedDiscovery: RobotDiscoveryModel? { + didSet { + connected = connectedDiscovery != nil + } + } + + // MARK: Private + + private let robot = Robot.shared + private let bleManager = BLEManager.shared + + private var cancellables: Set = [] + private var scanCancellable: AnyCancellable? } diff --git a/Modules/RobotKit/Sources/UI/ViewModels/RobotDiscoveryViewModel.swift b/Modules/RobotKit/Sources/UI/ViewModels/RobotDiscoveryViewModel.swift index 0c39107f34..4f4685eeaf 100644 --- a/Modules/RobotKit/Sources/UI/ViewModels/RobotDiscoveryViewModel.swift +++ b/Modules/RobotKit/Sources/UI/ViewModels/RobotDiscoveryViewModel.swift @@ -8,18 +8,7 @@ import SwiftUI // MARK: - RobotDiscoveryViewModel public struct RobotDiscoveryViewModel: Identifiable { - public enum Status: CaseIterable { - case connected - case unselected - case selected - } - - public let id: UUID - public let name: String - public let status: Status - public let isCharging: Bool - public let osVersion: String - public let battery: BatteryViewModel + // MARK: Lifecycle init( name: String, battery: Int, isCharging: Bool, osVersion: String, status: Status = .unselected @@ -40,6 +29,21 @@ public struct RobotDiscoveryViewModel: Identifiable { self.osVersion = "LekaOS \(discovery.osVersion)" self.battery = BatteryViewModel(level: discovery.battery) } + + // MARK: Public + + public enum Status: CaseIterable { + case connected + case unselected + case selected + } + + public let id: UUID + public let name: String + public let status: Status + public let isCharging: Bool + public let osVersion: String + public let battery: BatteryViewModel } // MARK: Equatable diff --git a/Modules/RobotKit/Sources/UI/Views/RobotConnectionView.swift b/Modules/RobotKit/Sources/UI/Views/RobotConnectionView.swift index f47d734580..0419aa353a 100644 --- a/Modules/RobotKit/Sources/UI/Views/RobotConnectionView.swift +++ b/Modules/RobotKit/Sources/UI/Views/RobotConnectionView.swift @@ -8,19 +8,13 @@ import DesignKit import SwiftUI public struct RobotConnectionView: View { - @StateObject var viewModel: RobotConnectionViewModel - - @Environment(\.dismiss) var dismiss + // MARK: Lifecycle public init(viewModel: RobotConnectionViewModel = RobotConnectionViewModel()) { self._viewModel = StateObject(wrappedValue: viewModel) } - private let columns: [GridItem] = [ - GridItem(), - GridItem(), - GridItem(), - ] + // MARK: Public public var body: some View { NavigationStack { @@ -69,6 +63,29 @@ public struct RobotConnectionView: View { } } + // MARK: Internal + + struct BackgroundView: View { + var body: some View { + DesignKitAsset.Images.interfaceCloud.swiftUIImage + .resizable() + .aspectRatio(contentMode: .fill) + .ignoresSafeArea(.all) + } + } + + @StateObject var viewModel: RobotConnectionViewModel + + @Environment(\.dismiss) var dismiss + + // MARK: Private + + private let columns: [GridItem] = [ + GridItem(), + GridItem(), + GridItem(), + ] + private var searchingView: some View { // TODO(@ladislas): review "no robot found" interface // TODO(@ladislas): handle no robots found after xx seconds + add refresh button @@ -94,26 +111,6 @@ public struct RobotConnectionView: View { } } - @ViewBuilder - private func robotDiscoveryCellView(for discovery: RobotDiscoveryModel) -> some View { - if discovery == viewModel.connectedDiscovery { - RobotDiscoveryView( - discovery: RobotDiscoveryViewModel( - discovery: discovery, status: .connected) - ) - } else if discovery == viewModel.selectedDiscovery { - RobotDiscoveryView( - discovery: RobotDiscoveryViewModel( - discovery: discovery, status: .selected) - ) - } else { - RobotDiscoveryView( - discovery: RobotDiscoveryViewModel( - discovery: discovery, status: .unselected) - ) - } - } - private var searchButton: some View { ButtonBordered(tint: .blue) { HStack { @@ -183,12 +180,23 @@ public struct RobotConnectionView: View { } } - struct BackgroundView: View { - var body: some View { - DesignKitAsset.Images.interfaceCloud.swiftUIImage - .resizable() - .aspectRatio(contentMode: .fill) - .ignoresSafeArea(.all) + @ViewBuilder + private func robotDiscoveryCellView(for discovery: RobotDiscoveryModel) -> some View { + if discovery == viewModel.connectedDiscovery { + RobotDiscoveryView( + discovery: RobotDiscoveryViewModel( + discovery: discovery, status: .connected) + ) + } else if discovery == viewModel.selectedDiscovery { + RobotDiscoveryView( + discovery: RobotDiscoveryViewModel( + discovery: discovery, status: .selected) + ) + } else { + RobotDiscoveryView( + discovery: RobotDiscoveryViewModel( + discovery: discovery, status: .unselected) + ) } } } diff --git a/Modules/RobotKit/Sources/UI/Views/RobotDiscoveryView.swift b/Modules/RobotKit/Sources/UI/Views/RobotDiscoveryView.swift index b18302be70..903f8b0b47 100644 --- a/Modules/RobotKit/Sources/UI/Views/RobotDiscoveryView.swift +++ b/Modules/RobotKit/Sources/UI/Views/RobotDiscoveryView.swift @@ -6,12 +6,7 @@ import DesignKit import SwiftUI struct RobotDiscoveryView: View { - // MARK: - Private variables - - @State private var rotation: CGFloat = 0.0 - @State private var inset: CGFloat = 0.0 - - private var discovery: RobotDiscoveryViewModel + // MARK: Lifecycle // MARK: - Public functions @@ -19,6 +14,8 @@ struct RobotDiscoveryView: View { self.discovery = discovery } + // MARK: Internal + // MARK: - Views var body: some View { @@ -34,6 +31,15 @@ struct RobotDiscoveryView: View { .padding(.horizontal, 20) } + // MARK: Private + + // MARK: - Private variables + + @State private var rotation: CGFloat = 0.0 + @State private var inset: CGFloat = 0.0 + + private var discovery: RobotDiscoveryViewModel + // MARK: - Private views private var robotFace: some View { diff --git a/Plugins/ios-monorepo/ProjectDescriptionHelpers/LocalHelper.swift b/Plugins/ios-monorepo/ProjectDescriptionHelpers/LocalHelper.swift index 517e6e3cac..35758a88b5 100644 --- a/Plugins/ios-monorepo/ProjectDescriptionHelpers/LocalHelper.swift +++ b/Plugins/ios-monorepo/ProjectDescriptionHelpers/LocalHelper.swift @@ -5,9 +5,13 @@ import Foundation public struct LocalHelper { - let name: String + // MARK: Lifecycle public init(name: String) { self.name = name } + + // MARK: Internal + + let name: String } diff --git a/Tuist/ProjectDescriptionHelpers/Project+Templates+Module.swift b/Tuist/ProjectDescriptionHelpers/Project+Templates+Module.swift index d8fe074ec4..db24216091 100644 --- a/Tuist/ProjectDescriptionHelpers/Project+Templates+Module.swift +++ b/Tuist/ProjectDescriptionHelpers/Project+Templates+Module.swift @@ -7,13 +7,17 @@ import ProjectDescription // MARK: - ModuleExample public struct ModuleExample { - public let name: String - public let infoPlist: [String: InfoPlist.Value] + // MARK: Lifecycle public init(name: String, infoPlist: [String: InfoPlist.Value] = [:]) { self.name = name self.infoPlist = infoPlist } + + // MARK: Public + + public let name: String + public let infoPlist: [String: InfoPlist.Value] } public extension Project {