From 54508d5ca67418509b7cf403d2889cf440cac678 Mon Sep 17 00:00:00 2001 From: Ricardo Colombo Oliveira Date: Thu, 28 Dec 2023 11:58:37 +0100 Subject: [PATCH] feat: update example app to v1.0.8 Using the Anyline Tire Tread SDK v3.0.0 --- Anyline Tire Demo.xcodeproj/project.pbxproj | 1076 +++++++++-------- .../xcshareddata/swiftpm/Package.resolved | 17 +- .../xcschemes/Anyline Tire Demo.xcscheme | 91 ++ .../qr_code_icon.imageset/Contents.json | 21 + .../qr_code_icon.imageset/QR Code Icon.png | Bin 0 -> 615 bytes .../DataStorage/UserDefaultsManager.swift | 17 + Anyline Tire Demo/Info.plist | 20 +- .../Components/ATDSideButton.swift | 13 +- .../Presentation/Components/ATDTopView.swift | 20 +- .../Controller/FeedbackViewController.swift | 6 +- .../Controller/LandingViewController.swift | 90 +- .../Controller/RecorderViewController.swift | 177 +++ .../LandingButtonActionsDelegate.swift | 2 + .../Landing/LandingTextViewDelegate.swift | 6 + .../Features/Landing/View/LandingView.swift | 113 +- .../Landing/View/TextLandingView.swift | 110 +- .../Landing/ViewModel/LandingViewModel.swift | 36 +- .../Controller/LoadingViewController.swift | 41 +- .../Loading/ViewModel/LoadingViewModel.swift | 66 +- .../QR/QRCodeReaderViewController.swift | 199 +++ .../ViewModel/ResultDetailsViewModel.swift | 12 +- .../Controller/ResultViewController.swift | 18 +- .../Result/View/MeasurementView.swift | 53 +- .../View/TireTreadMeasurementView.swift | 21 +- .../Scan/Controller/ScanViewController.swift | 371 +----- .../Controller/SettingsViewController.swift | 101 +- .../SettingsButtonActionsDelegate.swift | 8 + .../Settings/View/ButtonsSettingsView.swift | 5 +- .../Settings/View/CaptureSpeedView.swift | 84 ++ .../Settings/View/InfoSettingsView.swift | 7 +- .../Settings/View/LicenseSettingsView.swift | 85 +- .../Features/Settings/View/SettingsView.swift | 61 +- .../ViewModel/SettingsViewModel.swift | 19 +- ...wav => tiretread_sound_beep_too_close.wav} | Bin ...p.wav => tiretread_sound_beep_too_far.wav} | Bin .../Resources/Audio/tiretread_sound_start.mp3 | Bin 14880 -> 0 bytes .../Resources/Audio/tiretread_sound_start.wav | Bin 0 -> 71502 bytes .../Helpers/VolumeButtonObserver.swift | 40 +- .../Resources/System/SystemInfo.swift | 340 +++++- .../en.lproj/Localizable.strings | 37 +- README.md | 233 +--- 41 files changed, 2246 insertions(+), 1370 deletions(-) create mode 100644 Anyline Tire Demo.xcodeproj/xcshareddata/xcschemes/Anyline Tire Demo.xcscheme create mode 100644 Anyline Tire Demo/Assets.xcassets/qr_code_icon.imageset/Contents.json create mode 100644 Anyline Tire Demo/Assets.xcassets/qr_code_icon.imageset/QR Code Icon.png create mode 100644 Anyline Tire Demo/Presentation/Features/Landing/Controller/RecorderViewController.swift create mode 100644 Anyline Tire Demo/Presentation/Features/Landing/LandingTextViewDelegate.swift create mode 100644 Anyline Tire Demo/Presentation/Features/QR/QRCodeReaderViewController.swift create mode 100644 Anyline Tire Demo/Presentation/Features/Settings/View/CaptureSpeedView.swift rename Anyline Tire Demo/Resources/Audio/{tiretread_sound_low_beep.wav => tiretread_sound_beep_too_close.wav} (100%) rename Anyline Tire Demo/Resources/Audio/{tiretread_sound_high_beep.wav => tiretread_sound_beep_too_far.wav} (100%) delete mode 100644 Anyline Tire Demo/Resources/Audio/tiretread_sound_start.mp3 create mode 100644 Anyline Tire Demo/Resources/Audio/tiretread_sound_start.wav diff --git a/Anyline Tire Demo.xcodeproj/project.pbxproj b/Anyline Tire Demo.xcodeproj/project.pbxproj index fc978ef..725e8f0 100644 --- a/Anyline Tire Demo.xcodeproj/project.pbxproj +++ b/Anyline Tire Demo.xcodeproj/project.pbxproj @@ -3,597 +3,609 @@ archiveVersion = 1; classes = { }; - objectVersion = 56; + objectVersion = 51; objects = { /* Begin PBXBuildFile section */ - 802B90F6296D80EB0017919D /* Alamofire in Frameworks */ = {isa = PBXBuildFile; productRef = 802B90F5296D80EB0017919D /* Alamofire */; }; - A0BEEBA62A6FFF7800E27276 /* AnylineTireTreadSdk in Frameworks */ = {isa = PBXBuildFile; productRef = A0BEEBA52A6FFF7800E27276 /* AnylineTireTreadSdk */; }; - A0D1F2412A6181BD00A6146B /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D43E3FA4296573C400F1D384 /* SceneDelegate.swift */; }; - A0D1F2422A6181BD00A6146B /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D43E3FA2296573C400F1D384 /* AppDelegate.swift */; }; - A0D1F2432A6181D600A6146B /* UserDefaultsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4B9B46E299A46DE00C1CF81 /* UserDefaultsManager.swift */; }; - A0D1F2442A61821500A6146B /* LandingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D43E3FF02966E27900F1D384 /* LandingViewController.swift */; }; - A0D1F2452A61822200A6146B /* LandingViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 802B9112297033500017919D /* LandingViewModel.swift */; }; - A0D1F2462A61823500A6146B /* KeychainManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4084D6529911ADA00A6FB6F /* KeychainManager.swift */; }; - A0D1F2472A61824700A6146B /* ATDTopView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D44E4725299F88220077E2E2 /* ATDTopView.swift */; }; - A0D1F2482A61824E00A6146B /* LandingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80B979A0297193500033E3EE /* LandingView.swift */; }; - A0D1F2492A61825A00A6146B /* FontStruct.swift in Sources */ = {isa = PBXBuildFile; fileRef = D43E3FE329659A6F00F1D384 /* FontStruct.swift */; }; - A0D1F24A2A61826000A6146B /* ColorStruct.swift in Sources */ = {isa = PBXBuildFile; fileRef = D43E3FE129659A4500F1D384 /* ColorStruct.swift */; }; - A0D1F24B2A61826B00A6146B /* ATDSideButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D43E3FDA2965983B00F1D384 /* ATDSideButton.swift */; }; - A0D1F24C2A61827700A6146B /* TextLandingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4C316F029A54C1B00F836A1 /* TextLandingView.swift */; }; - A0D1F24D2A61829000A6146B /* LandingButtonActionsDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4438EF029704144002C3923 /* LandingButtonActionsDelegate.swift */; }; - A0D1F24E2A6182A400A6146B /* String+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = D43E40032966F1A600F1D384 /* String+Extension.swift */; }; - A0D1F24F2A6182B100A6146B /* SettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D43E40012966EE8A00F1D384 /* SettingsViewController.swift */; }; - A0D1F2502A6182B600A6146B /* ScanViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D43E3FFF2966EE7E00F1D384 /* ScanViewController.swift */; }; - A0D1F2512A6182C200A6146B /* UIViewController+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = D40E345D299A43F800DA93E3 /* UIViewController+Extension.swift */; }; - A0D1F2522A6182D500A6146B /* VolumeButtonObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = D400DD4529BA21F50021A291 /* VolumeButtonObserver.swift */; }; - A0D1F2532A6182E100A6146B /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4438EEC29703963002C3923 /* SettingsView.swift */; }; - A0D1F2542A6182E900A6146B /* ButtonsSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D42AB9F229714E46005F2093 /* ButtonsSettingsView.swift */; }; - A0D1F2552A6182EF00A6146B /* LicenseSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D42AB9F829716120005F2093 /* LicenseSettingsView.swift */; }; - A0D1F2562A6182F600A6146B /* ATDTitleLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D43E3FDE2965998800F1D384 /* ATDTitleLabel.swift */; }; - A0D1F2572A61830600A6146B /* ImperialSystemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D42AB9F629715E38005F2093 /* ImperialSystemView.swift */; }; - A0D1F2582A61830D00A6146B /* AccuracySpeedSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D42AB9FA297161D9005F2093 /* AccuracySpeedSettingsView.swift */; }; - A0D1F2592A61831400A6146B /* ATDTextLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D43E3FDC296598E700F1D384 /* ATDTextLabel.swift */; }; - A0D1F25A2A61831B00A6146B /* InfoSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D42AB9F429715B93005F2093 /* InfoSettingsView.swift */; }; - A0D1F25B2A61832200A6146B /* SystemInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D43EA3002982993300872088 /* SystemInfo.swift */; }; - A0D1F25C2A61832A00A6146B /* ATDTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = D42AB9FC297169B3005F2093 /* ATDTextField.swift */; }; - A0D1F25D2A61833900A6146B /* SettingsButtonActionsDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4438EEA297038B7002C3923 /* SettingsButtonActionsDelegate.swift */; }; - A0D1F25E2A61834300A6146B /* SettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4499019299CF5DF00BEEBD5 /* SettingsViewModel.swift */; }; - A0D1F25F2A61835C00A6146B /* ScanViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80C8EC8329893F2600873781 /* ScanViewModel.swift */; }; - A0D1F2602A61836400A6146B /* LoadingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4C316F729A60E7A00F836A1 /* LoadingViewController.swift */; }; - A0D1F2612A61836D00A6146B /* LoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4C316F529A60E6F00F836A1 /* LoadingView.swift */; }; - A0D1F2622A61838100A6146B /* AnimationLoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4C316FB29A610B900F836A1 /* AnimationLoadingView.swift */; }; - A0D1F2632A61839000A6146B /* LoadingButtonActionsDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4C316F929A60F0500F836A1 /* LoadingButtonActionsDelegate.swift */; }; - A0D1F2642A6183A900A6146B /* ErrorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D42ABA00297540B2005F2093 /* ErrorViewController.swift */; }; - A0D1F2652A6183B000A6146B /* LoadingViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4C316FE29A6279400F836A1 /* LoadingViewModel.swift */; }; - A0D1F2662A6183B800A6146B /* ErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D42ABA032975410E005F2093 /* ErrorView.swift */; }; - A0D1F2672A6183C000A6146B /* ErrorButtonActionsDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D42ABA072975470D005F2093 /* ErrorButtonActionsDelegate.swift */; }; - A0D1F2682A6183C800A6146B /* DescriptionErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D42ABA0929754DDD005F2093 /* DescriptionErrorView.swift */; }; - A0D1F2692A6183D000A6146B /* ResultViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D434E11F298138B7001E1FFB /* ResultViewController.swift */; }; - A0D1F26A2A6183D800A6146B /* ResultView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D434E121298138C7001E1FFB /* ResultView.swift */; }; - A0D1F26B2A6183DE00A6146B /* ResultButtonActionsDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D434E123298138E0001E1FFB /* ResultButtonActionsDelegate.swift */; }; - A0D1F26C2A6183E600A6146B /* ButtonsResultView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D434E12529813A63001E1FFB /* ButtonsResultView.swift */; }; - A0D1F26D2A6183EC00A6146B /* TireTreadMeasurementView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D43EA2FD2982874C00872088 /* TireTreadMeasurementView.swift */; }; - A0D1F26E2A6183F600A6146B /* ResultDetailsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D434E11529809341001E1FFB /* ResultDetailsViewController.swift */; }; - A0D1F26F2A6183FC00A6146B /* FeedbackViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D49F063A2976944B0031B752 /* FeedbackViewController.swift */; }; - A0D1F2702A61840400A6146B /* ResultDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D434E11A29809790001E1FFB /* ResultDetailsView.swift */; }; - A0D1F2712A61840A00A6146B /* ResultDetailsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4AAB8A32996D7F90045D527 /* ResultDetailsViewModel.swift */; }; - A0D1F2722A61841700A6146B /* MeasurementView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D434E12729813FDD001E1FFB /* MeasurementView.swift */; }; - A0D1F2732A61841F00A6146B /* PDFReaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D434E11329808E10001E1FFB /* PDFReaderView.swift */; }; - A0D1F2742A61842800A6146B /* FeedbackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4CCAC69297E959500CC78C6 /* FeedbackView.swift */; }; - A0D1F2752A61842C00A6146B /* FeedbackViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D421C097299BDD6400A9F5A6 /* FeedbackViewModel.swift */; }; - A0D1F2762A61843400A6146B /* ButtonsFeedbackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D49F063E2976997A0031B752 /* ButtonsFeedbackView.swift */; }; - A0D1F2772A61843900A6146B /* ATDSideTitleLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D42ABA0529754541005F2093 /* ATDSideTitleLabel.swift */; }; - A0D1F2782A61843E00A6146B /* TireDepthsFeedbackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4CCAC6B297E99A900CC78C6 /* TireDepthsFeedbackView.swift */; }; - A0D1F2792A61844800A6146B /* FeedbackButtonActionsDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4B5370529801C04009CAB53 /* FeedbackButtonActionsDelegate.swift */; }; - A0D1F27A2A61846500A6146B /* TireDepthFeedbackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4B53703298011B1009CAB53 /* TireDepthFeedbackView.swift */; }; - D410046029BB21F8007B476D /* tiretread_sound_high_beep.wav in Resources */ = {isa = PBXBuildFile; fileRef = D410045E29BB21F8007B476D /* tiretread_sound_high_beep.wav */; }; - D410046129BB21F8007B476D /* tiretread_sound_low_beep.wav in Resources */ = {isa = PBXBuildFile; fileRef = D410045F29BB21F8007B476D /* tiretread_sound_low_beep.wav */; }; - D43E3FAC296573C600F1D384 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D43E3FAB296573C600F1D384 /* Assets.xcassets */; }; - D43E3FAF296573C600F1D384 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D43E3FAD296573C600F1D384 /* LaunchScreen.storyboard */; }; - D43E3FE929659B8900F1D384 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = D43E3FEB29659B8900F1D384 /* Localizable.strings */; }; - D43E3FFB2966E86200F1D384 /* SnapKit in Frameworks */ = {isa = PBXBuildFile; productRef = D43E3FFA2966E86200F1D384 /* SnapKit */; }; - D43E3FFC2966EC1600F1D384 /* ProximaNova-Bold.otf in Resources */ = {isa = PBXBuildFile; fileRef = D43E3FEE2966D18700F1D384 /* ProximaNova-Bold.otf */; }; - D43E3FFD2966EC1800F1D384 /* ProximaNova-Light.otf in Resources */ = {isa = PBXBuildFile; fileRef = D43E3FEF2966D18700F1D384 /* ProximaNova-Light.otf */; }; - D43E3FFE2966EC1A00F1D384 /* ProximaNova-Regular.otf in Resources */ = {isa = PBXBuildFile; fileRef = D43E3FED2966D18700F1D384 /* ProximaNova-Regular.otf */; }; - D474EA2D29BB5A4000C1FABA /* tiretread_focuspoint_found.wav in Resources */ = {isa = PBXBuildFile; fileRef = D474EA2C29BB5A4000C1FABA /* tiretread_focuspoint_found.wav */; }; - D474EA2F29BB5D8F00C1FABA /* tiretread_sound_start.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = D474EA2E29BB5D8F00C1FABA /* tiretread_sound_start.mp3 */; }; - D474EA3129BB5D9D00C1FABA /* tiretread_sound_stop.wav in Resources */ = {isa = PBXBuildFile; fileRef = D474EA3029BB5D9D00C1FABA /* tiretread_sound_stop.wav */; }; - D4B53701298002C5009CAB53 /* KeychainSwift in Frameworks */ = {isa = PBXBuildFile; productRef = D4B53700298002C5009CAB53 /* KeychainSwift */; }; + 00D7D9629931D5F9C34F144F /* FeedbackViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9A8744B0A87FC65A8B46D20 /* FeedbackViewController.swift */; }; + 015D0AF4D1678D4F78A46880 /* SystemInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5F46F97960F579993C41507 /* SystemInfo.swift */; }; + 030CA17364D27E2C86B8D36F /* UserDefaultsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6FDFBEF38034A3F4671F98F /* UserDefaultsManager.swift */; }; + 030D8C7E8F9F826524BA31A7 /* SnapKit in Frameworks */ = {isa = PBXBuildFile; productRef = AE4098F783D33EDD00174621 /* SnapKit */; }; + 0A1A2396F1C55377451C6AF4 /* ResultViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBF323846DAEE03F621DBF05 /* ResultViewController.swift */; }; + 0FA8883A556B2FA320CA2DD2 /* AccuracySpeedSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94B3EE586C5AEFFD0946C9B5 /* AccuracySpeedSettingsView.swift */; }; + 1AC53E9E0FF2699C737155CF /* String+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67840B9E99FD8F9FC54844DB /* String+Extension.swift */; }; + 1D17BE143196523D2031D216 /* DescriptionErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 684EA574D2CD2834C4B90FE7 /* DescriptionErrorView.swift */; }; + 1DB1D383AF7C342F7E94BE9C /* AnimationLoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 039C5E9CF9FCB168405C7A63 /* AnimationLoadingView.swift */; }; + 29ED2FF6F2DD3987E89BFA6A /* tiretread_sound_stop.wav in Resources */ = {isa = PBXBuildFile; fileRef = 0279B6C33EC315BFB9202A88 /* tiretread_sound_stop.wav */; }; + 2E4E98C5AC372ECAECE7BCEA /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4F65BCB8B40B49FE3A81C57 /* SettingsView.swift */; }; + 35B3D619C9CB0D2485FB93C0 /* ResultDetailsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 592EBCA3FF9BC3EC5068119D /* ResultDetailsViewController.swift */; }; + 38DB3D561F4C43FB5A8B9727 /* ErrorButtonActionsDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FDD518668FD48C095561BE8 /* ErrorButtonActionsDelegate.swift */; }; + 3CAFFCCFEE3CA28949769BCE /* KeychainSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 5A8E509B4C8C4AC6C6113DBD /* KeychainSwift */; }; + 3CDCBDE8BEBC15FDA1FB6264 /* tiretread_focuspoint_found.wav in Resources */ = {isa = PBXBuildFile; fileRef = 9305D03F418897C0AD10B90F /* tiretread_focuspoint_found.wav */; }; + 40E037705F4EA729263B4531 /* ATDTitleLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2042E7AF3F98CE816AC74A87 /* ATDTitleLabel.swift */; }; + 4315EDFF1D1D482EB86C2AC6 /* ButtonsSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 203FEF3A731C7A34F6109D47 /* ButtonsSettingsView.swift */; }; + 454F272008A986B435532AE0 /* MeasurementView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E205BE38F7CF6F2E673D73B /* MeasurementView.swift */; }; + 4CEED1DF5615C73346CF62C8 /* AnylineTireTreadSdk in Frameworks */ = {isa = PBXBuildFile; productRef = 4FC06FD0D43DBAB59132C68A /* AnylineTireTreadSdk */; }; + 53E9BB7C9EFA688DF79A049A /* LoadingButtonActionsDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE123729BEA35B442A4649EC /* LoadingButtonActionsDelegate.swift */; }; + 59C58D5B136AB5E8E50E6CEF /* ATDSideTitleLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45AF252A096B6811DE5CBD11 /* ATDSideTitleLabel.swift */; }; + 5E7BB7B5DC99420C9F0C7503 /* ResultDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B64E5BEDEA3ABD3A59F22090 /* ResultDetailsView.swift */; }; + 60867B6448E0174B6C94DD34 /* PDFReaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B672D5814257084C202F656D /* PDFReaderView.swift */; }; + 61F8BC3D17A20B47266573CC /* SettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94A1F359FD235E25B6A374F9 /* SettingsViewModel.swift */; }; + 62089BD82CA50593B8FA437A /* FeedbackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD9A3DA93C8AE03554D6124E /* FeedbackView.swift */; }; + 630302657E9563016A6A475F /* tiretread_sound_beep_too_close.wav in Resources */ = {isa = PBXBuildFile; fileRef = 898D068837A772FDC17C4F80 /* tiretread_sound_beep_too_close.wav */; }; + 658795794DFEF5319801E7C5 /* ButtonsFeedbackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 628BA76297A2A7D92E46E45A /* ButtonsFeedbackView.swift */; }; + 66772AB4A13D6B2762CBAAD9 /* FeedbackViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7967A1D1C06BA2300E23BEA1 /* FeedbackViewModel.swift */; }; + 6A3DED6F32AF230CE411624C /* ATDSideButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1FCB5E52078C5C5E8B5569B /* ATDSideButton.swift */; }; + 6A88F4119F37EF8E80A6C418 /* LandingViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9341A53AAD476ED9898701C5 /* LandingViewModel.swift */; }; + 6E11D51C1B198681FBBBAF50 /* LoadingViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5010D7616B934B0CEA331D02 /* LoadingViewModel.swift */; }; + 7369D989A1B4FE06EB66969A /* tiretread_sound_start.wav in Resources */ = {isa = PBXBuildFile; fileRef = 356C7E9537E33B81DD3E9DF8 /* tiretread_sound_start.wav */; }; + 752146CA082BED4AE6FA6C33 /* ResultButtonActionsDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 635177CA7A2D6A49FB41537F /* ResultButtonActionsDelegate.swift */; }; + 75DED790D67B3F8F6810A375 /* LandingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53E863C814ADFA1AE90698DB /* LandingViewController.swift */; }; + 778185811BD1A1128E44BED5 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 4F41F1B3D9D343C30C7129C4 /* Assets.xcassets */; }; + 7FAF812248111EAE17B51F8A /* SettingsButtonActionsDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F23279E1B27D785630EA5A47 /* SettingsButtonActionsDelegate.swift */; }; + 7FDA3B9A76C6BF12C38735C4 /* ButtonsResultView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B31331DE50C6CDAD5AEA836F /* ButtonsResultView.swift */; }; + 8355FCC47FD62507211C2312 /* TireDepthFeedbackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5C8C38156D966C80C5CF40A /* TireDepthFeedbackView.swift */; }; + 84C49BAE86A8C61E2789AE51 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = BE053E1CF96479297B839F59 /* LaunchScreen.storyboard */; }; + 84C5DAAA08E0024CCBC9F586 /* ErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91A81A696C0FE10BADEDFFF0 /* ErrorView.swift */; }; + 87DB8F58F9BAA1992550BE5D /* ATDTopView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DBED146B22C6AF08693B5B3 /* ATDTopView.swift */; }; + 8A6F05DEA64AACAE756353C9 /* SettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79422290CE7EF2CD20C3D8EE /* SettingsViewController.swift */; }; + 8AF6D5CED47CF01761428943 /* ImperialSystemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63DA0E2038EF402A16AC3A68 /* ImperialSystemView.swift */; }; + 8BF4E284C06302D31E0935A4 /* LoadingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14A8CECEA3C792A45FFCB5FE /* LoadingViewController.swift */; }; + 916BB27AE45273720C857859 /* Alamofire in Frameworks */ = {isa = PBXBuildFile; productRef = 36DF23772E8928D0BFDB3967 /* Alamofire */; }; + 922F01E6511122B7C47A513A /* LandingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43FD477C87ED260D7F2D4B0B /* LandingView.swift */; }; + 927829229CB4F07B5590DDF8 /* TextLandingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9CB99471900809461B83691 /* TextLandingView.swift */; }; + 97EA42AE051CF8B91895393B /* TireDepthsFeedbackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD27ED133BF4D5F71179625C /* TireDepthsFeedbackView.swift */; }; + 9DC179F77F904D61DA624801 /* FontStruct.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A56CB61EFAE890DA36421FE /* FontStruct.swift */; }; + A1A43E97D4186089AD2FF75B /* KeychainManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C4839D8B8B3418C1F448C0C /* KeychainManager.swift */; }; + A5E50D4A5C0CC96AF74C8C8D /* LicenseSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1082146CA856FECC9AE1C392 /* LicenseSettingsView.swift */; }; + A657F5121313A04D7767CA25 /* ATDTextLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0D513A072F0FA35EEED2BE /* ATDTextLabel.swift */; }; + AB54A1FC59A94B0888996937 /* ResultDetailsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 743A1DCAF06FD21B53070DE9 /* ResultDetailsViewModel.swift */; }; + AC4D8CE67B84C89878A0FA2A /* RecorderViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F6D8CBBF0CEE70A674D10D60 /* RecorderViewController.swift */; }; + B2EBB1666A0CF9F779EF8931 /* TireTreadMeasurementView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 390C46701BB308E6F53EFB81 /* TireTreadMeasurementView.swift */; }; + B43E435CF5FD514D9D4D4245 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 3318E4FA5B27EDB760F76DFF /* Localizable.strings */; }; + C547C5D4DB92F152AC74F0F8 /* ProximaNova-Regular.otf in Resources */ = {isa = PBXBuildFile; fileRef = DB20B1D3E5DE027FAE19AD9F /* ProximaNova-Regular.otf */; }; + C6BF2CD1FD27D45201F44A83 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 515C57BE1A6CB1D9486937B3 /* AppDelegate.swift */; }; + CABBA4922CFF9C7D20F87DF6 /* CaptureSpeedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39FB6DAFECFCBA5FFF7D488D /* CaptureSpeedView.swift */; }; + CCA9F8DF9C1EDAD294141EA3 /* FeedbackButtonActionsDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0221683328A2A4F71635B509 /* FeedbackButtonActionsDelegate.swift */; }; + CE2BF07B703E436BCF4A75D0 /* ColorStruct.swift in Sources */ = {isa = PBXBuildFile; fileRef = E0E627967E61F1B098CD294E /* ColorStruct.swift */; }; + CFBC8035DE3560D7ABB142FD /* ProximaNova-Bold.otf in Resources */ = {isa = PBXBuildFile; fileRef = 8A35D4D3DBCB082A5C414DA4 /* ProximaNova-Bold.otf */; }; + D1C023881D18C8A35E33D669 /* ScanViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5BA79536B6EEBE089971CDA /* ScanViewController.swift */; }; + D5CBEE1F3E3661B52DBEF1CC /* VolumeButtonObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBBFD690B0F8E473AD1285E8 /* VolumeButtonObserver.swift */; }; + D5F2132E906282B4C27F6F83 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2193B4C3D407BACF67DB63B3 /* SceneDelegate.swift */; }; + D6919D00130DD2096F69F352 /* ResultView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44AFD25638D1C9513636C37 /* ResultView.swift */; }; + DA4553F0DFF86A25ACBF1197 /* LandingTextViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 846F00A3CE42550CD5D87E5F /* LandingTextViewDelegate.swift */; }; + DAB4121C20787CFBD4E8BC3B /* ATDTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E20CCB2348A1D5779AD369D /* ATDTextField.swift */; }; + DEC9D5EECB42273A5F89B447 /* LandingButtonActionsDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 696E6ED85B49AD9091DA35DE /* LandingButtonActionsDelegate.swift */; }; + E42046826C435BB3C8648356 /* InfoSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98E054DE5275CFBA5F059FFD /* InfoSettingsView.swift */; }; + E867FA59DFE1158EF820BA09 /* UIViewController+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE63BB8F9AD2406691FCCEAA /* UIViewController+Extension.swift */; }; + E9B33D39315858DA62E2C2BD /* ProximaNova-Light.otf in Resources */ = {isa = PBXBuildFile; fileRef = 7806B02B24D5CA9894554DDF /* ProximaNova-Light.otf */; }; + F526EDD2BB0625673BA4C11C /* LoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC8B10AB00E2C39D3A8E912B /* LoadingView.swift */; }; + F61AC54D164F3F1D35289943 /* QRCodeReaderViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 420A2185548C476939CD5037 /* QRCodeReaderViewController.swift */; }; + F81338C8435FEBA076801D69 /* ScanViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00CC8518403D55DB36CAFA0D /* ScanViewModel.swift */; }; + FA806DEF9791249E298CA64D /* tiretread_sound_beep_too_far.wav in Resources */ = {isa = PBXBuildFile; fileRef = B97D72540717494221B2D968 /* tiretread_sound_beep_too_far.wav */; }; + FE1F9B0BF324FAB9B95F0C81 /* ErrorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58DC8214D152A69BF6BE2BFA /* ErrorViewController.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ - 802B9112297033500017919D /* LandingViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LandingViewModel.swift; sourceTree = ""; }; - 80B979A0297193500033E3EE /* LandingView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LandingView.swift; sourceTree = ""; }; - 80C8EC8329893F2600873781 /* ScanViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScanViewModel.swift; sourceTree = ""; }; - D400DD4529BA21F50021A291 /* VolumeButtonObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VolumeButtonObserver.swift; sourceTree = ""; }; - D4084D6529911ADA00A6FB6F /* KeychainManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeychainManager.swift; sourceTree = ""; }; - D40E345D299A43F800DA93E3 /* UIViewController+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+Extension.swift"; sourceTree = ""; }; - D410045E29BB21F8007B476D /* tiretread_sound_high_beep.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; path = tiretread_sound_high_beep.wav; sourceTree = ""; }; - D410045F29BB21F8007B476D /* tiretread_sound_low_beep.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; path = tiretread_sound_low_beep.wav; sourceTree = ""; }; - D421C097299BDD6400A9F5A6 /* FeedbackViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbackViewModel.swift; sourceTree = ""; }; - D42AB9F229714E46005F2093 /* ButtonsSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonsSettingsView.swift; sourceTree = ""; }; - D42AB9F429715B93005F2093 /* InfoSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoSettingsView.swift; sourceTree = ""; }; - D42AB9F629715E38005F2093 /* ImperialSystemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImperialSystemView.swift; sourceTree = ""; }; - D42AB9F829716120005F2093 /* LicenseSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LicenseSettingsView.swift; sourceTree = ""; }; - D42AB9FA297161D9005F2093 /* AccuracySpeedSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccuracySpeedSettingsView.swift; sourceTree = ""; }; - D42AB9FC297169B3005F2093 /* ATDTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ATDTextField.swift; sourceTree = ""; }; - D42ABA00297540B2005F2093 /* ErrorViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorViewController.swift; sourceTree = ""; }; - D42ABA032975410E005F2093 /* ErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorView.swift; sourceTree = ""; }; - D42ABA0529754541005F2093 /* ATDSideTitleLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ATDSideTitleLabel.swift; sourceTree = ""; }; - D42ABA072975470D005F2093 /* ErrorButtonActionsDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorButtonActionsDelegate.swift; sourceTree = ""; }; - D42ABA0929754DDD005F2093 /* DescriptionErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DescriptionErrorView.swift; sourceTree = ""; }; - D434E11329808E10001E1FFB /* PDFReaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PDFReaderView.swift; sourceTree = ""; }; - D434E11529809341001E1FFB /* ResultDetailsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResultDetailsViewController.swift; sourceTree = ""; }; - D434E11A29809790001E1FFB /* ResultDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResultDetailsView.swift; sourceTree = ""; }; - D434E11F298138B7001E1FFB /* ResultViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResultViewController.swift; sourceTree = ""; }; - D434E121298138C7001E1FFB /* ResultView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResultView.swift; sourceTree = ""; }; - D434E123298138E0001E1FFB /* ResultButtonActionsDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResultButtonActionsDelegate.swift; sourceTree = ""; }; - D434E12529813A63001E1FFB /* ButtonsResultView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonsResultView.swift; sourceTree = ""; }; - D434E12729813FDD001E1FFB /* MeasurementView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeasurementView.swift; sourceTree = ""; }; - D43E3F9F296573C400F1D384 /* Anyline Tire Demo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Anyline Tire Demo.app"; sourceTree = BUILT_PRODUCTS_DIR; }; - D43E3FA2296573C400F1D384 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - D43E3FA4296573C400F1D384 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; - D43E3FAB296573C600F1D384 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - D43E3FAE296573C600F1D384 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; - D43E3FB0296573C600F1D384 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - D43E3FDA2965983B00F1D384 /* ATDSideButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ATDSideButton.swift; sourceTree = ""; }; - D43E3FDC296598E700F1D384 /* ATDTextLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ATDTextLabel.swift; sourceTree = ""; }; - D43E3FDE2965998800F1D384 /* ATDTitleLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ATDTitleLabel.swift; sourceTree = ""; }; - D43E3FE129659A4500F1D384 /* ColorStruct.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ColorStruct.swift; sourceTree = ""; }; - D43E3FE329659A6F00F1D384 /* FontStruct.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FontStruct.swift; sourceTree = ""; }; - D43E3FEA29659B8900F1D384 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; - D43E3FED2966D18700F1D384 /* ProximaNova-Regular.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "ProximaNova-Regular.otf"; sourceTree = ""; }; - D43E3FEE2966D18700F1D384 /* ProximaNova-Bold.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "ProximaNova-Bold.otf"; sourceTree = ""; }; - D43E3FEF2966D18700F1D384 /* ProximaNova-Light.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "ProximaNova-Light.otf"; sourceTree = ""; }; - D43E3FF02966E27900F1D384 /* LandingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LandingViewController.swift; sourceTree = ""; }; - D43E3FFF2966EE7E00F1D384 /* ScanViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScanViewController.swift; sourceTree = ""; }; - D43E40012966EE8A00F1D384 /* SettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewController.swift; sourceTree = ""; }; - D43E40032966F1A600F1D384 /* String+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Extension.swift"; sourceTree = ""; }; - D43EA2FD2982874C00872088 /* TireTreadMeasurementView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TireTreadMeasurementView.swift; sourceTree = ""; }; - D43EA3002982993300872088 /* SystemInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SystemInfo.swift; sourceTree = ""; }; - D4438EEA297038B7002C3923 /* SettingsButtonActionsDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsButtonActionsDelegate.swift; sourceTree = ""; }; - D4438EEC29703963002C3923 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; - D4438EF029704144002C3923 /* LandingButtonActionsDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LandingButtonActionsDelegate.swift; sourceTree = ""; }; - D4499019299CF5DF00BEEBD5 /* SettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewModel.swift; sourceTree = ""; }; - D44E4725299F88220077E2E2 /* ATDTopView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ATDTopView.swift; sourceTree = ""; }; - D474EA2C29BB5A4000C1FABA /* tiretread_focuspoint_found.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; path = tiretread_focuspoint_found.wav; sourceTree = ""; }; - D474EA2E29BB5D8F00C1FABA /* tiretread_sound_start.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = tiretread_sound_start.mp3; sourceTree = ""; }; - D474EA3029BB5D9D00C1FABA /* tiretread_sound_stop.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; path = tiretread_sound_stop.wav; sourceTree = ""; }; - D49F063A2976944B0031B752 /* FeedbackViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbackViewController.swift; sourceTree = ""; }; - D49F063E2976997A0031B752 /* ButtonsFeedbackView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonsFeedbackView.swift; sourceTree = ""; }; - D4AAB8A32996D7F90045D527 /* ResultDetailsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResultDetailsViewModel.swift; sourceTree = ""; }; - D4B53703298011B1009CAB53 /* TireDepthFeedbackView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TireDepthFeedbackView.swift; sourceTree = ""; }; - D4B5370529801C04009CAB53 /* FeedbackButtonActionsDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbackButtonActionsDelegate.swift; sourceTree = ""; }; - D4B9B46E299A46DE00C1CF81 /* UserDefaultsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefaultsManager.swift; sourceTree = ""; }; - D4C316F029A54C1B00F836A1 /* TextLandingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextLandingView.swift; sourceTree = ""; }; - D4C316F529A60E6F00F836A1 /* LoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingView.swift; sourceTree = ""; }; - D4C316F729A60E7A00F836A1 /* LoadingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingViewController.swift; sourceTree = ""; }; - D4C316F929A60F0500F836A1 /* LoadingButtonActionsDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingButtonActionsDelegate.swift; sourceTree = ""; }; - D4C316FB29A610B900F836A1 /* AnimationLoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnimationLoadingView.swift; sourceTree = ""; }; - D4C316FE29A6279400F836A1 /* LoadingViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingViewModel.swift; sourceTree = ""; }; - D4CCAC69297E959500CC78C6 /* FeedbackView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbackView.swift; sourceTree = ""; }; - D4CCAC6B297E99A900CC78C6 /* TireDepthsFeedbackView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TireDepthsFeedbackView.swift; sourceTree = ""; }; + 00CC8518403D55DB36CAFA0D /* ScanViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScanViewModel.swift; sourceTree = ""; }; + 0221683328A2A4F71635B509 /* FeedbackButtonActionsDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbackButtonActionsDelegate.swift; sourceTree = ""; }; + 0279B6C33EC315BFB9202A88 /* tiretread_sound_stop.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; path = tiretread_sound_stop.wav; sourceTree = ""; }; + 039C5E9CF9FCB168405C7A63 /* AnimationLoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnimationLoadingView.swift; sourceTree = ""; }; + 1082146CA856FECC9AE1C392 /* LicenseSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LicenseSettingsView.swift; sourceTree = ""; }; + 14A8CECEA3C792A45FFCB5FE /* LoadingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingViewController.swift; sourceTree = ""; }; + 1DBED146B22C6AF08693B5B3 /* ATDTopView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ATDTopView.swift; sourceTree = ""; }; + 203FEF3A731C7A34F6109D47 /* ButtonsSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonsSettingsView.swift; sourceTree = ""; }; + 2042E7AF3F98CE816AC74A87 /* ATDTitleLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ATDTitleLabel.swift; sourceTree = ""; }; + 2193B4C3D407BACF67DB63B3 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; + 2A56CB61EFAE890DA36421FE /* FontStruct.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FontStruct.swift; sourceTree = ""; }; + 3318E4FA5B27EDB760F76DFF /* Localizable.strings */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = Localizable.strings; path = en.lproj/Localizable.strings; sourceTree = ""; }; + 356C7E9537E33B81DD3E9DF8 /* tiretread_sound_start.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; path = tiretread_sound_start.wav; sourceTree = ""; }; + 390C46701BB308E6F53EFB81 /* TireTreadMeasurementView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TireTreadMeasurementView.swift; sourceTree = ""; }; + 39FB6DAFECFCBA5FFF7D488D /* CaptureSpeedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CaptureSpeedView.swift; sourceTree = ""; }; + 420A2185548C476939CD5037 /* QRCodeReaderViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeReaderViewController.swift; sourceTree = ""; }; + 43FD477C87ED260D7F2D4B0B /* LandingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LandingView.swift; sourceTree = ""; }; + 45AF252A096B6811DE5CBD11 /* ATDSideTitleLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ATDSideTitleLabel.swift; sourceTree = ""; }; + 4EB97AF36FE6ADF80D5595B4 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 4F41F1B3D9D343C30C7129C4 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 5010D7616B934B0CEA331D02 /* LoadingViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingViewModel.swift; sourceTree = ""; }; + 515C57BE1A6CB1D9486937B3 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 53E863C814ADFA1AE90698DB /* LandingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LandingViewController.swift; sourceTree = ""; }; + 58DC8214D152A69BF6BE2BFA /* ErrorViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorViewController.swift; sourceTree = ""; }; + 592EBCA3FF9BC3EC5068119D /* ResultDetailsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResultDetailsViewController.swift; sourceTree = ""; }; + 5C4839D8B8B3418C1F448C0C /* KeychainManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainManager.swift; sourceTree = ""; }; + 5FA6E900770A1B48D530C0B0 /* Anyline Tire Demo.app */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.application; path = "Anyline Tire Demo.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 5FDD518668FD48C095561BE8 /* ErrorButtonActionsDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorButtonActionsDelegate.swift; sourceTree = ""; }; + 628BA76297A2A7D92E46E45A /* ButtonsFeedbackView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonsFeedbackView.swift; sourceTree = ""; }; + 635177CA7A2D6A49FB41537F /* ResultButtonActionsDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResultButtonActionsDelegate.swift; sourceTree = ""; }; + 63B95ED79DF5781095E3CE27 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; + 63DA0E2038EF402A16AC3A68 /* ImperialSystemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImperialSystemView.swift; sourceTree = ""; }; + 67840B9E99FD8F9FC54844DB /* String+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Extension.swift"; sourceTree = ""; }; + 684EA574D2CD2834C4B90FE7 /* DescriptionErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DescriptionErrorView.swift; sourceTree = ""; }; + 696E6ED85B49AD9091DA35DE /* LandingButtonActionsDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LandingButtonActionsDelegate.swift; sourceTree = ""; }; + 6E20CCB2348A1D5779AD369D /* ATDTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ATDTextField.swift; sourceTree = ""; }; + 743A1DCAF06FD21B53070DE9 /* ResultDetailsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResultDetailsViewModel.swift; sourceTree = ""; }; + 7806B02B24D5CA9894554DDF /* ProximaNova-Light.otf */ = {isa = PBXFileReference; path = "ProximaNova-Light.otf"; sourceTree = ""; }; + 79422290CE7EF2CD20C3D8EE /* SettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewController.swift; sourceTree = ""; }; + 7967A1D1C06BA2300E23BEA1 /* FeedbackViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbackViewModel.swift; sourceTree = ""; }; + 846F00A3CE42550CD5D87E5F /* LandingTextViewDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LandingTextViewDelegate.swift; sourceTree = ""; }; + 898D068837A772FDC17C4F80 /* tiretread_sound_beep_too_close.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; path = tiretread_sound_beep_too_close.wav; sourceTree = ""; }; + 8A35D4D3DBCB082A5C414DA4 /* ProximaNova-Bold.otf */ = {isa = PBXFileReference; path = "ProximaNova-Bold.otf"; sourceTree = ""; }; + 91A81A696C0FE10BADEDFFF0 /* ErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorView.swift; sourceTree = ""; }; + 9305D03F418897C0AD10B90F /* tiretread_focuspoint_found.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; path = tiretread_focuspoint_found.wav; sourceTree = ""; }; + 9341A53AAD476ED9898701C5 /* LandingViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LandingViewModel.swift; sourceTree = ""; }; + 94A1F359FD235E25B6A374F9 /* SettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewModel.swift; sourceTree = ""; }; + 94B3EE586C5AEFFD0946C9B5 /* AccuracySpeedSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccuracySpeedSettingsView.swift; sourceTree = ""; }; + 98E054DE5275CFBA5F059FFD /* InfoSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoSettingsView.swift; sourceTree = ""; }; + 9E205BE38F7CF6F2E673D73B /* MeasurementView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeasurementView.swift; sourceTree = ""; }; + A5C8C38156D966C80C5CF40A /* TireDepthFeedbackView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TireDepthFeedbackView.swift; sourceTree = ""; }; + A9CB99471900809461B83691 /* TextLandingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextLandingView.swift; sourceTree = ""; }; + AA0D513A072F0FA35EEED2BE /* ATDTextLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ATDTextLabel.swift; sourceTree = ""; }; + B31331DE50C6CDAD5AEA836F /* ButtonsResultView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonsResultView.swift; sourceTree = ""; }; + B5F46F97960F579993C41507 /* SystemInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SystemInfo.swift; sourceTree = ""; }; + B64E5BEDEA3ABD3A59F22090 /* ResultDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResultDetailsView.swift; sourceTree = ""; }; + B672D5814257084C202F656D /* PDFReaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PDFReaderView.swift; sourceTree = ""; }; + B97D72540717494221B2D968 /* tiretread_sound_beep_too_far.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; path = tiretread_sound_beep_too_far.wav; sourceTree = ""; }; + C1FCB5E52078C5C5E8B5569B /* ATDSideButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ATDSideButton.swift; sourceTree = ""; }; + C44AFD25638D1C9513636C37 /* ResultView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResultView.swift; sourceTree = ""; }; + CBBFD690B0F8E473AD1285E8 /* VolumeButtonObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VolumeButtonObserver.swift; sourceTree = ""; }; + D5BA79536B6EEBE089971CDA /* ScanViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScanViewController.swift; sourceTree = ""; }; + D6FDFBEF38034A3F4671F98F /* UserDefaultsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefaultsManager.swift; sourceTree = ""; }; + DB20B1D3E5DE027FAE19AD9F /* ProximaNova-Regular.otf */ = {isa = PBXFileReference; path = "ProximaNova-Regular.otf"; sourceTree = ""; }; + DBF323846DAEE03F621DBF05 /* ResultViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResultViewController.swift; sourceTree = ""; }; + DD27ED133BF4D5F71179625C /* TireDepthsFeedbackView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TireDepthsFeedbackView.swift; sourceTree = ""; }; + DD9A3DA93C8AE03554D6124E /* FeedbackView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbackView.swift; sourceTree = ""; }; + DE123729BEA35B442A4649EC /* LoadingButtonActionsDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingButtonActionsDelegate.swift; sourceTree = ""; }; + E0E627967E61F1B098CD294E /* ColorStruct.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorStruct.swift; sourceTree = ""; }; + F23279E1B27D785630EA5A47 /* SettingsButtonActionsDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsButtonActionsDelegate.swift; sourceTree = ""; }; + F4F65BCB8B40B49FE3A81C57 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; + F6D8CBBF0CEE70A674D10D60 /* RecorderViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecorderViewController.swift; sourceTree = ""; }; + F9A8744B0A87FC65A8B46D20 /* FeedbackViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbackViewController.swift; sourceTree = ""; }; + FC8B10AB00E2C39D3A8E912B /* LoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingView.swift; sourceTree = ""; }; + FE63BB8F9AD2406691FCCEAA /* UIViewController+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+Extension.swift"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ - D43E3F9C296573C400F1D384 /* Frameworks */ = { + A7AA68BB7B6664B26C0758B7 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - D4B53701298002C5009CAB53 /* KeychainSwift in Frameworks */, - A0BEEBA62A6FFF7800E27276 /* AnylineTireTreadSdk in Frameworks */, - D43E3FFB2966E86200F1D384 /* SnapKit in Frameworks */, - 802B90F6296D80EB0017919D /* Alamofire in Frameworks */, + 4CEED1DF5615C73346CF62C8 /* AnylineTireTreadSdk in Frameworks */, + 916BB27AE45273720C857859 /* Alamofire in Frameworks */, + 3CAFFCCFEE3CA28949769BCE /* KeychainSwift in Frameworks */, + 030D8C7E8F9F826524BA31A7 /* SnapKit in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 802B9109296EAFF40017919D /* Presentation */ = { + 07797286DB83D938F3E42B90 /* Landing */ = { isa = PBXGroup; children = ( - D43E3FBD29657C4F00F1D384 /* Components */, - D43E3FBE29657C8C00F1D384 /* Features */, + 696E6ED85B49AD9091DA35DE /* LandingButtonActionsDelegate.swift */, + 846F00A3CE42550CD5D87E5F /* LandingTextViewDelegate.swift */, + 4F0317CEF684E6F6D8E09AB4 /* Controller */, + C0665B9A93EEE921E82262E9 /* View */, + 4D6401116713DEBBA0697755 /* ViewModel */, ); - path = Presentation; + path = Landing; sourceTree = ""; }; - 802B9114297059A70017919D /* ViewModel */ = { + 082EB2EB5944D20E39DE8532 /* Loading */ = { isa = PBXGroup; children = ( - 802B9112297033500017919D /* LandingViewModel.swift */, + DE123729BEA35B442A4649EC /* LoadingButtonActionsDelegate.swift */, + 38E7F8F5E0215730E7AE8127 /* Controller */, + 6739CDD1F9FF2CE43F822A4C /* View */, + 4ACD01D0B098B61A12C94F6B /* ViewModel */, ); - path = ViewModel; + path = Loading; sourceTree = ""; }; - 80B9799F297193500033E3EE /* View */ = { + 0B640698AABBBA4ECC8390C5 /* Resources */ = { isa = PBXGroup; children = ( - 80B979A0297193500033E3EE /* LandingView.swift */, - D4C316F029A54C1B00F836A1 /* TextLandingView.swift */, + 19003AC71D8C98073C1BB5CA /* Audio */, + 26065F191C2A166B67E54837 /* Extensions */, + 9040E2F1A88E371D7DDCB6A7 /* Helpers */, + 930DC432CB7ADC12BB1C40F9 /* System */, ); - path = View; + path = Resources; sourceTree = ""; }; - 80C8EC8229893F0B00873781 /* ViewModel */ = { + 125A9CAF259E1BB12AEF96B7 /* Settings */ = { isa = PBXGroup; children = ( - 80C8EC8329893F2600873781 /* ScanViewModel.swift */, + F23279E1B27D785630EA5A47 /* SettingsButtonActionsDelegate.swift */, + 223561F4EAF60F7650B21435 /* Controller */, + A02EF026C8E68AE22F23D8ED /* View */, + 43B3AB73302BC67D222ACA1C /* ViewModel */, ); - path = ViewModel; + path = Settings; sourceTree = ""; }; - A0BEEBA42A6FFF7800E27276 /* Frameworks */ = { + 19003AC71D8C98073C1BB5CA /* Audio */ = { isa = PBXGroup; children = ( + 9305D03F418897C0AD10B90F /* tiretread_focuspoint_found.wav */, + 898D068837A772FDC17C4F80 /* tiretread_sound_beep_too_close.wav */, + B97D72540717494221B2D968 /* tiretread_sound_beep_too_far.wav */, + 356C7E9537E33B81DD3E9DF8 /* tiretread_sound_start.wav */, + 0279B6C33EC315BFB9202A88 /* tiretread_sound_stop.wav */, ); - name = Frameworks; + path = Audio; sourceTree = ""; }; - D4084D6429911ADA00A6FB6F /* DataStorage */ = { + 223561F4EAF60F7650B21435 /* Controller */ = { isa = PBXGroup; children = ( - D4084D6529911ADA00A6FB6F /* KeychainManager.swift */, - D4B9B46E299A46DE00C1CF81 /* UserDefaultsManager.swift */, + 79422290CE7EF2CD20C3D8EE /* SettingsViewController.swift */, ); - path = DataStorage; + path = Controller; sourceTree = ""; }; - D410045D29BB21B2007B476D /* Audio */ = { + 22B452DAF0CCDD6BA9E99063 /* Fonts */ = { isa = PBXGroup; children = ( - D474EA2C29BB5A4000C1FABA /* tiretread_focuspoint_found.wav */, - D410045E29BB21F8007B476D /* tiretread_sound_high_beep.wav */, - D410045F29BB21F8007B476D /* tiretread_sound_low_beep.wav */, - D474EA2E29BB5D8F00C1FABA /* tiretread_sound_start.mp3 */, - D474EA3029BB5D9D00C1FABA /* tiretread_sound_stop.wav */, + 8A35D4D3DBCB082A5C414DA4 /* ProximaNova-Bold.otf */, + 7806B02B24D5CA9894554DDF /* ProximaNova-Light.otf */, + DB20B1D3E5DE027FAE19AD9F /* ProximaNova-Regular.otf */, ); - path = Audio; + path = Fonts; sourceTree = ""; }; - D421C096299BDD4E00A9F5A6 /* ViewModel */ = { + 26065F191C2A166B67E54837 /* Extensions */ = { isa = PBXGroup; children = ( - D421C097299BDD6400A9F5A6 /* FeedbackViewModel.swift */, + 67840B9E99FD8F9FC54844DB /* String+Extension.swift */, + FE63BB8F9AD2406691FCCEAA /* UIViewController+Extension.swift */, ); - path = ViewModel; + path = Extensions; sourceTree = ""; }; - D42AB9FE29754044005F2093 /* Error */ = { + 38E7F8F5E0215730E7AE8127 /* Controller */ = { isa = PBXGroup; children = ( - D42AB9FF2975409E005F2093 /* Controller */, - D42ABA072975470D005F2093 /* ErrorButtonActionsDelegate.swift */, + 14A8CECEA3C792A45FFCB5FE /* LoadingViewController.swift */, ); - path = Error; + path = Controller; sourceTree = ""; }; - D42AB9FF2975409E005F2093 /* Controller */ = { + 39269ECB8E0AAF521DD42C1C /* Controller */ = { isa = PBXGroup; children = ( - D42ABA00297540B2005F2093 /* ErrorViewController.swift */, - D42ABA02297540B6005F2093 /* View */, + D5BA79536B6EEBE089971CDA /* ScanViewController.swift */, ); path = Controller; sourceTree = ""; }; - D42ABA02297540B6005F2093 /* View */ = { + 42B88D7021F7770EB8516B60 /* Controller */ = { isa = PBXGroup; children = ( - D42ABA032975410E005F2093 /* ErrorView.swift */, - D42ABA0929754DDD005F2093 /* DescriptionErrorView.swift */, + 58DC8214D152A69BF6BE2BFA /* ErrorViewController.swift */, + 8301B8486339AEE6DE4D3B30 /* View */, + ); + path = Controller; + sourceTree = ""; + }; + 433DD292F275DAE422928890 /* View */ = { + isa = PBXGroup; + children = ( + B31331DE50C6CDAD5AEA836F /* ButtonsResultView.swift */, + 9E205BE38F7CF6F2E673D73B /* MeasurementView.swift */, + C44AFD25638D1C9513636C37 /* ResultView.swift */, + 390C46701BB308E6F53EFB81 /* TireTreadMeasurementView.swift */, ); path = View; sourceTree = ""; }; - D434E11729809348001E1FFB /* Result details */ = { + 43B3AB73302BC67D222ACA1C /* ViewModel */ = { isa = PBXGroup; children = ( - D434E1192980935E001E1FFB /* View */, - D4AAB8A22996D7E30045D527 /* ViewModel */, - D434E11829809359001E1FFB /* Controller */, + 94A1F359FD235E25B6A374F9 /* SettingsViewModel.swift */, ); - path = "Result details"; + path = ViewModel; sourceTree = ""; }; - D434E11829809359001E1FFB /* Controller */ = { + 443C59D5612CFED4001C8AD1 /* Anyline Tire Demo */ = { isa = PBXGroup; children = ( - D434E11529809341001E1FFB /* ResultDetailsViewController.swift */, + 515C57BE1A6CB1D9486937B3 /* AppDelegate.swift */, + 4F41F1B3D9D343C30C7129C4 /* Assets.xcassets */, + 63B95ED79DF5781095E3CE27 /* Info.plist */, + BE053E1CF96479297B839F59 /* LaunchScreen.storyboard */, + 3318E4FA5B27EDB760F76DFF /* Localizable.strings */, + 2193B4C3D407BACF67DB63B3 /* SceneDelegate.swift */, + B2A029EAA4AF722D05D155BE /* DataStorage */, + 22B452DAF0CCDD6BA9E99063 /* Fonts */, + 4E462EF51CB251ECFF0B094A /* Presentation */, + 0B640698AABBBA4ECC8390C5 /* Resources */, ); - path = Controller; + path = "Anyline Tire Demo"; sourceTree = ""; }; - D434E1192980935E001E1FFB /* View */ = { + 4ACD01D0B098B61A12C94F6B /* ViewModel */ = { isa = PBXGroup; children = ( - D434E11A29809790001E1FFB /* ResultDetailsView.swift */, + 5010D7616B934B0CEA331D02 /* LoadingViewModel.swift */, ); - path = View; + path = ViewModel; sourceTree = ""; }; - D434E11C2981388B001E1FFB /* Result */ = { + 4D6401116713DEBBA0697755 /* ViewModel */ = { isa = PBXGroup; children = ( - D434E11E298138A5001E1FFB /* Controller */, - D434E11D298138A0001E1FFB /* View */, - D434E123298138E0001E1FFB /* ResultButtonActionsDelegate.swift */, + 9341A53AAD476ED9898701C5 /* LandingViewModel.swift */, ); - path = Result; + path = ViewModel; sourceTree = ""; }; - D434E11D298138A0001E1FFB /* View */ = { + 4E462EF51CB251ECFF0B094A /* Presentation */ = { isa = PBXGroup; children = ( - D434E12529813A63001E1FFB /* ButtonsResultView.swift */, - D434E12729813FDD001E1FFB /* MeasurementView.swift */, - D434E121298138C7001E1FFB /* ResultView.swift */, - D43EA2FD2982874C00872088 /* TireTreadMeasurementView.swift */, + E0C51FE8D426F614EDA5597B /* Components */, + 8DAAD10E567CFE5AD8404D5E /* Features */, ); - path = View; + path = Presentation; sourceTree = ""; }; - D434E11E298138A5001E1FFB /* Controller */ = { + 4F0317CEF684E6F6D8E09AB4 /* Controller */ = { isa = PBXGroup; children = ( - D434E11F298138B7001E1FFB /* ResultViewController.swift */, + 53E863C814ADFA1AE90698DB /* LandingViewController.swift */, + F6D8CBBF0CEE70A674D10D60 /* RecorderViewController.swift */, ); path = Controller; sourceTree = ""; }; - D43E3F96296573C400F1D384 = { + 5940DD433D86ACF87D593153 /* Scan */ = { isa = PBXGroup; children = ( - D43E3FA1296573C400F1D384 /* Anyline Tire Demo */, - D43E3FA0296573C400F1D384 /* Products */, - A0BEEBA42A6FFF7800E27276 /* Frameworks */, + 39269ECB8E0AAF521DD42C1C /* Controller */, + EC96517ED54B69E4FCD95A03 /* ViewModel */, ); + path = Scan; sourceTree = ""; }; - D43E3FA0296573C400F1D384 /* Products */ = { + 5FC9F09156E795359BF8FC75 /* Result details */ = { isa = PBXGroup; children = ( - D43E3F9F296573C400F1D384 /* Anyline Tire Demo.app */, + E6A570D49D23B181862D5DC6 /* Controller */, + AA5761B4C1D318401AFC51B8 /* View */, + F7BADD19F44B4EC4EBDFDB31 /* ViewModel */, ); - name = Products; + path = "Result details"; sourceTree = ""; }; - D43E3FA1296573C400F1D384 /* Anyline Tire Demo */ = { + 6739CDD1F9FF2CE43F822A4C /* View */ = { isa = PBXGroup; children = ( - D4084D6429911ADA00A6FB6F /* DataStorage */, - 802B9109296EAFF40017919D /* Presentation */, - D43E3FB929657B9E00F1D384 /* Resources */, - D43E3FEC2966D17300F1D384 /* Fonts */, - D43E3FA2296573C400F1D384 /* AppDelegate.swift */, - D43E3FA4296573C400F1D384 /* SceneDelegate.swift */, - D43E3FAB296573C600F1D384 /* Assets.xcassets */, - D43E3FAD296573C600F1D384 /* LaunchScreen.storyboard */, - D43E3FB0296573C600F1D384 /* Info.plist */, - D43E3FEB29659B8900F1D384 /* Localizable.strings */, + 039C5E9CF9FCB168405C7A63 /* AnimationLoadingView.swift */, + FC8B10AB00E2C39D3A8E912B /* LoadingView.swift */, ); - path = "Anyline Tire Demo"; + path = View; sourceTree = ""; }; - D43E3FB829657B6B00F1D384 /* Controller */ = { + 7045F4FAFB44E34CF1F56CB0 /* ViewModel */ = { isa = PBXGroup; children = ( - D43E3FF02966E27900F1D384 /* LandingViewController.swift */, + 7967A1D1C06BA2300E23BEA1 /* FeedbackViewModel.swift */, ); - path = Controller; + path = ViewModel; sourceTree = ""; }; - D43E3FB929657B9E00F1D384 /* Resources */ = { + 7D071E38AC4FE9AAD02D7B33 /* Error */ = { isa = PBXGroup; children = ( - D410045D29BB21B2007B476D /* Audio */, - D43EA2FF2982991F00872088 /* System */, - D43E3FE029659A2300F1D384 /* Helpers */, - D43E3FBC29657C4600F1D384 /* Extensions */, + 5FDD518668FD48C095561BE8 /* ErrorButtonActionsDelegate.swift */, + 42B88D7021F7770EB8516B60 /* Controller */, ); - path = Resources; + path = Error; sourceTree = ""; }; - D43E3FBA29657BAB00F1D384 /* Landing */ = { + 8301B8486339AEE6DE4D3B30 /* View */ = { isa = PBXGroup; children = ( - 80B9799F297193500033E3EE /* View */, - 802B9114297059A70017919D /* ViewModel */, - D43E3FB829657B6B00F1D384 /* Controller */, - D4438EF029704144002C3923 /* LandingButtonActionsDelegate.swift */, + 684EA574D2CD2834C4B90FE7 /* DescriptionErrorView.swift */, + 91A81A696C0FE10BADEDFFF0 /* ErrorView.swift */, ); - path = Landing; + path = View; sourceTree = ""; }; - D43E3FBC29657C4600F1D384 /* Extensions */ = { + 8D7F71E3A783FE4F8C5BC78F /* Result */ = { isa = PBXGroup; children = ( - D43E40032966F1A600F1D384 /* String+Extension.swift */, - D40E345D299A43F800DA93E3 /* UIViewController+Extension.swift */, + 635177CA7A2D6A49FB41537F /* ResultButtonActionsDelegate.swift */, + FF266DC6636B9C10AEC09666 /* Controller */, + 433DD292F275DAE422928890 /* View */, ); - path = Extensions; + path = Result; sourceTree = ""; }; - D43E3FBD29657C4F00F1D384 /* Components */ = { + 8D913555918E6AE6F6BA8BAB /* Controller */ = { isa = PBXGroup; children = ( - D43E3FDA2965983B00F1D384 /* ATDSideButton.swift */, - D42ABA0529754541005F2093 /* ATDSideTitleLabel.swift */, - D42AB9FC297169B3005F2093 /* ATDTextField.swift */, - D43E3FDC296598E700F1D384 /* ATDTextLabel.swift */, - D43E3FDE2965998800F1D384 /* ATDTitleLabel.swift */, - D44E4725299F88220077E2E2 /* ATDTopView.swift */, - D434E11329808E10001E1FFB /* PDFReaderView.swift */, + F9A8744B0A87FC65A8B46D20 /* FeedbackViewController.swift */, ); - path = Components; + path = Controller; sourceTree = ""; }; - D43E3FBE29657C8C00F1D384 /* Features */ = { + 8DAAD10E567CFE5AD8404D5E /* Features */ = { isa = PBXGroup; children = ( - D43E3FBA29657BAB00F1D384 /* Landing */, - D43E3FC729657D1600F1D384 /* Settings */, - D49F0637297693D60031B752 /* Feedback */, - D43E3FD329657D1600F1D384 /* Scan */, - D4C316F229A60E2B00F836A1 /* Loading */, - D434E11C2981388B001E1FFB /* Result */, - D42AB9FE29754044005F2093 /* Error */, - D434E11729809348001E1FFB /* Result details */, + 7D071E38AC4FE9AAD02D7B33 /* Error */, + F1716B231CD02A1A83A12434 /* Feedback */, + 07797286DB83D938F3E42B90 /* Landing */, + 082EB2EB5944D20E39DE8532 /* Loading */, + D1A636076B4B96ECF9BDB1BE /* QR */, + 8D7F71E3A783FE4F8C5BC78F /* Result */, + 5FC9F09156E795359BF8FC75 /* Result details */, + 5940DD433D86ACF87D593153 /* Scan */, + 125A9CAF259E1BB12AEF96B7 /* Settings */, ); path = Features; sourceTree = ""; }; - D43E3FC729657D1600F1D384 /* Settings */ = { + 9040E2F1A88E371D7DDCB6A7 /* Helpers */ = { isa = PBXGroup; children = ( - D4438EEA297038B7002C3923 /* SettingsButtonActionsDelegate.swift */, - D43E3FCA29657D1600F1D384 /* View */, - D4499018299CF5CC00BEEBD5 /* ViewModel */, - D43E3FC829657D1600F1D384 /* Controller */, + E0E627967E61F1B098CD294E /* ColorStruct.swift */, + 2A56CB61EFAE890DA36421FE /* FontStruct.swift */, + CBBFD690B0F8E473AD1285E8 /* VolumeButtonObserver.swift */, ); - path = Settings; + path = Helpers; sourceTree = ""; }; - D43E3FC829657D1600F1D384 /* Controller */ = { + 930DC432CB7ADC12BB1C40F9 /* System */ = { isa = PBXGroup; children = ( - D43E40012966EE8A00F1D384 /* SettingsViewController.swift */, + B5F46F97960F579993C41507 /* SystemInfo.swift */, ); - path = Controller; + path = System; sourceTree = ""; }; - D43E3FCA29657D1600F1D384 /* View */ = { + 9AB0F6C55B1BEBC49AE6BFA0 /* Products */ = { isa = PBXGroup; children = ( - D42AB9FA297161D9005F2093 /* AccuracySpeedSettingsView.swift */, - D42AB9F229714E46005F2093 /* ButtonsSettingsView.swift */, - D42AB9F629715E38005F2093 /* ImperialSystemView.swift */, - D42AB9F429715B93005F2093 /* InfoSettingsView.swift */, - D42AB9F829716120005F2093 /* LicenseSettingsView.swift */, - D4438EEC29703963002C3923 /* SettingsView.swift */, + 5FA6E900770A1B48D530C0B0 /* Anyline Tire Demo.app */, ); - path = View; + name = Products; sourceTree = ""; }; - D43E3FD329657D1600F1D384 /* Scan */ = { + A02EF026C8E68AE22F23D8ED /* View */ = { isa = PBXGroup; children = ( - 80C8EC8229893F0B00873781 /* ViewModel */, - D43E3FD429657D1600F1D384 /* Controller */, + 94B3EE586C5AEFFD0946C9B5 /* AccuracySpeedSettingsView.swift */, + 203FEF3A731C7A34F6109D47 /* ButtonsSettingsView.swift */, + 39FB6DAFECFCBA5FFF7D488D /* CaptureSpeedView.swift */, + 63DA0E2038EF402A16AC3A68 /* ImperialSystemView.swift */, + 98E054DE5275CFBA5F059FFD /* InfoSettingsView.swift */, + 1082146CA856FECC9AE1C392 /* LicenseSettingsView.swift */, + F4F65BCB8B40B49FE3A81C57 /* SettingsView.swift */, ); - path = Scan; + path = View; sourceTree = ""; }; - D43E3FD429657D1600F1D384 /* Controller */ = { + AA5761B4C1D318401AFC51B8 /* View */ = { isa = PBXGroup; children = ( - D43E3FFF2966EE7E00F1D384 /* ScanViewController.swift */, + B64E5BEDEA3ABD3A59F22090 /* ResultDetailsView.swift */, ); - path = Controller; + path = View; sourceTree = ""; }; - D43E3FE029659A2300F1D384 /* Helpers */ = { + B2A029EAA4AF722D05D155BE /* DataStorage */ = { isa = PBXGroup; children = ( - D43E3FE329659A6F00F1D384 /* FontStruct.swift */, - D43E3FE129659A4500F1D384 /* ColorStruct.swift */, - D400DD4529BA21F50021A291 /* VolumeButtonObserver.swift */, + 5C4839D8B8B3418C1F448C0C /* KeychainManager.swift */, + D6FDFBEF38034A3F4671F98F /* UserDefaultsManager.swift */, ); - path = Helpers; + path = DataStorage; sourceTree = ""; }; - D43E3FEC2966D17300F1D384 /* Fonts */ = { + C0665B9A93EEE921E82262E9 /* View */ = { isa = PBXGroup; children = ( - D43E3FEE2966D18700F1D384 /* ProximaNova-Bold.otf */, - D43E3FEF2966D18700F1D384 /* ProximaNova-Light.otf */, - D43E3FED2966D18700F1D384 /* ProximaNova-Regular.otf */, + 43FD477C87ED260D7F2D4B0B /* LandingView.swift */, + A9CB99471900809461B83691 /* TextLandingView.swift */, ); - path = Fonts; + path = View; sourceTree = ""; }; - D43EA2FF2982991F00872088 /* System */ = { + C1784AF3D57C3616670266F6 = { isa = PBXGroup; children = ( - D43EA3002982993300872088 /* SystemInfo.swift */, + 443C59D5612CFED4001C8AD1 /* Anyline Tire Demo */, + 9AB0F6C55B1BEBC49AE6BFA0 /* Products */, ); - path = System; sourceTree = ""; }; - D4499018299CF5CC00BEEBD5 /* ViewModel */ = { + D1A636076B4B96ECF9BDB1BE /* QR */ = { isa = PBXGroup; children = ( - D4499019299CF5DF00BEEBD5 /* SettingsViewModel.swift */, + 420A2185548C476939CD5037 /* QRCodeReaderViewController.swift */, ); - path = ViewModel; + path = QR; sourceTree = ""; }; - D49F0637297693D60031B752 /* Feedback */ = { + E0C51FE8D426F614EDA5597B /* Components */ = { isa = PBXGroup; children = ( - D49F0639297693EC0031B752 /* View */, - D421C096299BDD4E00A9F5A6 /* ViewModel */, - D49F0638297693E60031B752 /* Controller */, - D4B5370529801C04009CAB53 /* FeedbackButtonActionsDelegate.swift */, + C1FCB5E52078C5C5E8B5569B /* ATDSideButton.swift */, + 45AF252A096B6811DE5CBD11 /* ATDSideTitleLabel.swift */, + 6E20CCB2348A1D5779AD369D /* ATDTextField.swift */, + AA0D513A072F0FA35EEED2BE /* ATDTextLabel.swift */, + 2042E7AF3F98CE816AC74A87 /* ATDTitleLabel.swift */, + 1DBED146B22C6AF08693B5B3 /* ATDTopView.swift */, + B672D5814257084C202F656D /* PDFReaderView.swift */, ); - path = Feedback; + path = Components; sourceTree = ""; }; - D49F0638297693E60031B752 /* Controller */ = { + E6A570D49D23B181862D5DC6 /* Controller */ = { isa = PBXGroup; children = ( - D49F063A2976944B0031B752 /* FeedbackViewController.swift */, + 592EBCA3FF9BC3EC5068119D /* ResultDetailsViewController.swift */, ); path = Controller; sourceTree = ""; }; - D49F0639297693EC0031B752 /* View */ = { + E82518A0B5C6D055E1240A4B /* View */ = { isa = PBXGroup; children = ( - D49F063E2976997A0031B752 /* ButtonsFeedbackView.swift */, - D4CCAC69297E959500CC78C6 /* FeedbackView.swift */, - D4B53703298011B1009CAB53 /* TireDepthFeedbackView.swift */, - D4CCAC6B297E99A900CC78C6 /* TireDepthsFeedbackView.swift */, + 628BA76297A2A7D92E46E45A /* ButtonsFeedbackView.swift */, + DD9A3DA93C8AE03554D6124E /* FeedbackView.swift */, + A5C8C38156D966C80C5CF40A /* TireDepthFeedbackView.swift */, + DD27ED133BF4D5F71179625C /* TireDepthsFeedbackView.swift */, ); path = View; sourceTree = ""; }; - D4AAB8A22996D7E30045D527 /* ViewModel */ = { + EC96517ED54B69E4FCD95A03 /* ViewModel */ = { isa = PBXGroup; children = ( - D4AAB8A32996D7F90045D527 /* ResultDetailsViewModel.swift */, + 00CC8518403D55DB36CAFA0D /* ScanViewModel.swift */, ); path = ViewModel; sourceTree = ""; }; - D4C316F229A60E2B00F836A1 /* Loading */ = { + F1716B231CD02A1A83A12434 /* Feedback */ = { isa = PBXGroup; children = ( - D4C316F329A60E4600F836A1 /* View */, - D4C316FD29A6278200F836A1 /* ViewModel */, - D4C316F429A60E4B00F836A1 /* Controller */, - D4C316F929A60F0500F836A1 /* LoadingButtonActionsDelegate.swift */, + 0221683328A2A4F71635B509 /* FeedbackButtonActionsDelegate.swift */, + 8D913555918E6AE6F6BA8BAB /* Controller */, + E82518A0B5C6D055E1240A4B /* View */, + 7045F4FAFB44E34CF1F56CB0 /* ViewModel */, ); - path = Loading; + path = Feedback; sourceTree = ""; }; - D4C316F329A60E4600F836A1 /* View */ = { + F7BADD19F44B4EC4EBDFDB31 /* ViewModel */ = { isa = PBXGroup; children = ( - D4C316F529A60E6F00F836A1 /* LoadingView.swift */, - D4C316FB29A610B900F836A1 /* AnimationLoadingView.swift */, + 743A1DCAF06FD21B53070DE9 /* ResultDetailsViewModel.swift */, ); - path = View; + path = ViewModel; sourceTree = ""; }; - D4C316F429A60E4B00F836A1 /* Controller */ = { + FF266DC6636B9C10AEC09666 /* Controller */ = { isa = PBXGroup; children = ( - D4C316F729A60E7A00F836A1 /* LoadingViewController.swift */, + DBF323846DAEE03F621DBF05 /* ResultViewController.swift */, ); path = Controller; sourceTree = ""; }; - D4C316FD29A6278200F836A1 /* ViewModel */ = { - isa = PBXGroup; - children = ( - D4C316FE29A6279400F836A1 /* LoadingViewModel.swift */, - ); - path = ViewModel; - sourceTree = ""; - }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ - D43E3F9E296573C400F1D384 /* Anyline Tire Demo */ = { + 4970B307CD8FFCB7C0762116 /* Anyline Tire Demo */ = { isa = PBXNativeTarget; - buildConfigurationList = D43E3FB3296573C600F1D384 /* Build configuration list for PBXNativeTarget "Anyline Tire Demo" */; + buildConfigurationList = DAAED4700B88574245FCE484 /* Build configuration list for PBXNativeTarget "Anyline Tire Demo" */; buildPhases = ( - D43E3F9B296573C400F1D384 /* Sources */, - D43E3F9C296573C400F1D384 /* Frameworks */, - D43E3F9D296573C400F1D384 /* Resources */, + 7A8B0CC15C0E90F484F91DD7 /* Sources */, + 6D8B5D5A9070181982C27C8B /* Resources */, + A7AA68BB7B6664B26C0758B7 /* Frameworks */, ); buildRules = ( ); @@ -601,170 +613,165 @@ ); name = "Anyline Tire Demo"; packageProductDependencies = ( - D43E3FFA2966E86200F1D384 /* SnapKit */, - 802B90F5296D80EB0017919D /* Alamofire */, - D4B53700298002C5009CAB53 /* KeychainSwift */, - A0BEEBA52A6FFF7800E27276 /* AnylineTireTreadSdk */, + 4FC06FD0D43DBAB59132C68A /* AnylineTireTreadSdk */, + 36DF23772E8928D0BFDB3967 /* Alamofire */, + 5A8E509B4C8C4AC6C6113DBD /* KeychainSwift */, + AE4098F783D33EDD00174621 /* SnapKit */, ); productName = "Anyline Tire Demo"; - productReference = D43E3F9F296573C400F1D384 /* Anyline Tire Demo.app */; + productReference = 5FA6E900770A1B48D530C0B0 /* Anyline Tire Demo.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ - D43E3F97296573C400F1D384 /* Project object */ = { + E5CDB6798FCC7934BA0D0DE6 /* Project object */ = { isa = PBXProject; attributes = { - BuildIndependentTargetsInParallel = 1; - LastSwiftUpdateCheck = 1420; - LastUpgradeCheck = 1420; + LastUpgradeCheck = 1200; TargetAttributes = { - D43E3F9E296573C400F1D384 = { - CreatedOnToolsVersion = 14.2; + 4970B307CD8FFCB7C0762116 = { + DevelopmentTeam = 35RHL53WRE; + ProvisioningStyle = Automatic; }; }; }; - buildConfigurationList = D43E3F9A296573C400F1D384 /* Build configuration list for PBXProject "Anyline Tire Demo" */; - compatibilityVersion = "Xcode 14.0"; + buildConfigurationList = 6AAA678835C7FA3FAECC4ABD /* Build configuration list for PBXProject "Anyline Tire Demo" */; + compatibilityVersion = "Xcode 11.0"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( - en, Base, + en, ); - mainGroup = D43E3F96296573C400F1D384; + mainGroup = C1784AF3D57C3616670266F6; packageReferences = ( - D43E3FF92966E86200F1D384 /* XCRemoteSwiftPackageReference "SnapKit" */, - 802B90F4296D80EB0017919D /* XCRemoteSwiftPackageReference "Alamofire" */, - D4B536FF298002C5009CAB53 /* XCRemoteSwiftPackageReference "keychain-swift" */, - A07622DF2A696B5400250387 /* XCRemoteSwiftPackageReference "anyline-tiretread-spm-module" */, + 8BDBD2C18B241087A97EA599 /* XCRemoteSwiftPackageReference "Alamofire" */, + 2E17D9FE7E357A222A39DA30 /* XCRemoteSwiftPackageReference "anyline-tiretread-spm-module" */, + 8308C77CEAF862D126235E79 /* XCRemoteSwiftPackageReference "keychain-swift" */, + 4B5AEA5F9FCA6152FDAB044B /* XCRemoteSwiftPackageReference "SnapKit" */, ); - productRefGroup = D43E3FA0296573C400F1D384 /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( - D43E3F9E296573C400F1D384 /* Anyline Tire Demo */, + 4970B307CD8FFCB7C0762116 /* Anyline Tire Demo */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ - D43E3F9D296573C400F1D384 /* Resources */ = { + 6D8B5D5A9070181982C27C8B /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - D410046129BB21F8007B476D /* tiretread_sound_low_beep.wav in Resources */, - D43E3FFD2966EC1800F1D384 /* ProximaNova-Light.otf in Resources */, - D474EA3129BB5D9D00C1FABA /* tiretread_sound_stop.wav in Resources */, - D474EA2F29BB5D8F00C1FABA /* tiretread_sound_start.mp3 in Resources */, - D43E3FAF296573C600F1D384 /* LaunchScreen.storyboard in Resources */, - D43E3FFC2966EC1600F1D384 /* ProximaNova-Bold.otf in Resources */, - D43E3FE929659B8900F1D384 /* Localizable.strings in Resources */, - D43E3FAC296573C600F1D384 /* Assets.xcassets in Resources */, - D410046029BB21F8007B476D /* tiretread_sound_high_beep.wav in Resources */, - D474EA2D29BB5A4000C1FABA /* tiretread_focuspoint_found.wav in Resources */, - D43E3FFE2966EC1A00F1D384 /* ProximaNova-Regular.otf in Resources */, + 778185811BD1A1128E44BED5 /* Assets.xcassets in Resources */, + 84C49BAE86A8C61E2789AE51 /* LaunchScreen.storyboard in Resources */, + B43E435CF5FD514D9D4D4245 /* Localizable.strings in Resources */, + CFBC8035DE3560D7ABB142FD /* ProximaNova-Bold.otf in Resources */, + E9B33D39315858DA62E2C2BD /* ProximaNova-Light.otf in Resources */, + C547C5D4DB92F152AC74F0F8 /* ProximaNova-Regular.otf in Resources */, + 3CDCBDE8BEBC15FDA1FB6264 /* tiretread_focuspoint_found.wav in Resources */, + 630302657E9563016A6A475F /* tiretread_sound_beep_too_close.wav in Resources */, + FA806DEF9791249E298CA64D /* tiretread_sound_beep_too_far.wav in Resources */, + 7369D989A1B4FE06EB66969A /* tiretread_sound_start.wav in Resources */, + 29ED2FF6F2DD3987E89BFA6A /* tiretread_sound_stop.wav in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ - D43E3F9B296573C400F1D384 /* Sources */ = { + 7A8B0CC15C0E90F484F91DD7 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - A0D1F24A2A61826000A6146B /* ColorStruct.swift in Sources */, - A0D1F25B2A61832200A6146B /* SystemInfo.swift in Sources */, - A0D1F2752A61842C00A6146B /* FeedbackViewModel.swift in Sources */, - A0D1F2692A6183D000A6146B /* ResultViewController.swift in Sources */, - A0D1F2722A61841700A6146B /* MeasurementView.swift in Sources */, - A0D1F2632A61839000A6146B /* LoadingButtonActionsDelegate.swift in Sources */, - A0D1F2562A6182F600A6146B /* ATDTitleLabel.swift in Sources */, - A0D1F26A2A6183D800A6146B /* ResultView.swift in Sources */, - A0D1F2572A61830600A6146B /* ImperialSystemView.swift in Sources */, - A0D1F2422A6181BD00A6146B /* AppDelegate.swift in Sources */, - A0D1F25E2A61834300A6146B /* SettingsViewModel.swift in Sources */, - A0D1F2782A61843E00A6146B /* TireDepthsFeedbackView.swift in Sources */, - A0D1F2592A61831400A6146B /* ATDTextLabel.swift in Sources */, - A0D1F27A2A61846500A6146B /* TireDepthFeedbackView.swift in Sources */, - A0D1F25A2A61831B00A6146B /* InfoSettingsView.swift in Sources */, - A0D1F25C2A61832A00A6146B /* ATDTextField.swift in Sources */, - A0D1F2462A61823500A6146B /* KeychainManager.swift in Sources */, - A0D1F2672A6183C000A6146B /* ErrorButtonActionsDelegate.swift in Sources */, - A0D1F26E2A6183F600A6146B /* ResultDetailsViewController.swift in Sources */, - A0D1F2732A61841F00A6146B /* PDFReaderView.swift in Sources */, - A0D1F24B2A61826B00A6146B /* ATDSideButton.swift in Sources */, - A0D1F2512A6182C200A6146B /* UIViewController+Extension.swift in Sources */, - A0D1F24D2A61829000A6146B /* LandingButtonActionsDelegate.swift in Sources */, - A0D1F2702A61840400A6146B /* ResultDetailsView.swift in Sources */, - A0D1F2452A61822200A6146B /* LandingViewModel.swift in Sources */, - A0D1F2772A61843900A6146B /* ATDSideTitleLabel.swift in Sources */, - A0D1F2612A61836D00A6146B /* LoadingView.swift in Sources */, - A0D1F2522A6182D500A6146B /* VolumeButtonObserver.swift in Sources */, - A0D1F2582A61830D00A6146B /* AccuracySpeedSettingsView.swift in Sources */, - A0D1F24C2A61827700A6146B /* TextLandingView.swift in Sources */, - A0D1F2502A6182B600A6146B /* ScanViewController.swift in Sources */, - A0D1F2412A6181BD00A6146B /* SceneDelegate.swift in Sources */, - A0D1F26B2A6183DE00A6146B /* ResultButtonActionsDelegate.swift in Sources */, - A0D1F2662A6183B800A6146B /* ErrorView.swift in Sources */, - A0D1F2602A61836400A6146B /* LoadingViewController.swift in Sources */, - A0D1F2792A61844800A6146B /* FeedbackButtonActionsDelegate.swift in Sources */, - A0D1F26F2A6183FC00A6146B /* FeedbackViewController.swift in Sources */, - A0D1F2552A6182EF00A6146B /* LicenseSettingsView.swift in Sources */, - A0D1F2542A6182E900A6146B /* ButtonsSettingsView.swift in Sources */, - A0D1F2762A61843400A6146B /* ButtonsFeedbackView.swift in Sources */, - A0D1F24F2A6182B100A6146B /* SettingsViewController.swift in Sources */, - A0D1F2642A6183A900A6146B /* ErrorViewController.swift in Sources */, - A0D1F26D2A6183EC00A6146B /* TireTreadMeasurementView.swift in Sources */, - A0D1F2742A61842800A6146B /* FeedbackView.swift in Sources */, - A0D1F26C2A6183E600A6146B /* ButtonsResultView.swift in Sources */, - A0D1F24E2A6182A400A6146B /* String+Extension.swift in Sources */, - A0D1F2482A61824E00A6146B /* LandingView.swift in Sources */, - A0D1F2682A6183C800A6146B /* DescriptionErrorView.swift in Sources */, - A0D1F2442A61821500A6146B /* LandingViewController.swift in Sources */, - A0D1F2432A6181D600A6146B /* UserDefaultsManager.swift in Sources */, - A0D1F25D2A61833900A6146B /* SettingsButtonActionsDelegate.swift in Sources */, - A0D1F25F2A61835C00A6146B /* ScanViewModel.swift in Sources */, - A0D1F2622A61838100A6146B /* AnimationLoadingView.swift in Sources */, - A0D1F2472A61824700A6146B /* ATDTopView.swift in Sources */, - A0D1F2712A61840A00A6146B /* ResultDetailsViewModel.swift in Sources */, - A0D1F2652A6183B000A6146B /* LoadingViewModel.swift in Sources */, - A0D1F2492A61825A00A6146B /* FontStruct.swift in Sources */, - A0D1F2532A6182E100A6146B /* SettingsView.swift in Sources */, + 6A3DED6F32AF230CE411624C /* ATDSideButton.swift in Sources */, + 59C58D5B136AB5E8E50E6CEF /* ATDSideTitleLabel.swift in Sources */, + DAB4121C20787CFBD4E8BC3B /* ATDTextField.swift in Sources */, + A657F5121313A04D7767CA25 /* ATDTextLabel.swift in Sources */, + 40E037705F4EA729263B4531 /* ATDTitleLabel.swift in Sources */, + 87DB8F58F9BAA1992550BE5D /* ATDTopView.swift in Sources */, + 0FA8883A556B2FA320CA2DD2 /* AccuracySpeedSettingsView.swift in Sources */, + 1DB1D383AF7C342F7E94BE9C /* AnimationLoadingView.swift in Sources */, + C6BF2CD1FD27D45201F44A83 /* AppDelegate.swift in Sources */, + 658795794DFEF5319801E7C5 /* ButtonsFeedbackView.swift in Sources */, + 7FDA3B9A76C6BF12C38735C4 /* ButtonsResultView.swift in Sources */, + 4315EDFF1D1D482EB86C2AC6 /* ButtonsSettingsView.swift in Sources */, + CABBA4922CFF9C7D20F87DF6 /* CaptureSpeedView.swift in Sources */, + CE2BF07B703E436BCF4A75D0 /* ColorStruct.swift in Sources */, + 1D17BE143196523D2031D216 /* DescriptionErrorView.swift in Sources */, + 38DB3D561F4C43FB5A8B9727 /* ErrorButtonActionsDelegate.swift in Sources */, + 84C5DAAA08E0024CCBC9F586 /* ErrorView.swift in Sources */, + FE1F9B0BF324FAB9B95F0C81 /* ErrorViewController.swift in Sources */, + CCA9F8DF9C1EDAD294141EA3 /* FeedbackButtonActionsDelegate.swift in Sources */, + 62089BD82CA50593B8FA437A /* FeedbackView.swift in Sources */, + 00D7D9629931D5F9C34F144F /* FeedbackViewController.swift in Sources */, + 66772AB4A13D6B2762CBAAD9 /* FeedbackViewModel.swift in Sources */, + 9DC179F77F904D61DA624801 /* FontStruct.swift in Sources */, + 8AF6D5CED47CF01761428943 /* ImperialSystemView.swift in Sources */, + E42046826C435BB3C8648356 /* InfoSettingsView.swift in Sources */, + A1A43E97D4186089AD2FF75B /* KeychainManager.swift in Sources */, + DEC9D5EECB42273A5F89B447 /* LandingButtonActionsDelegate.swift in Sources */, + DA4553F0DFF86A25ACBF1197 /* LandingTextViewDelegate.swift in Sources */, + 922F01E6511122B7C47A513A /* LandingView.swift in Sources */, + 75DED790D67B3F8F6810A375 /* LandingViewController.swift in Sources */, + 6A88F4119F37EF8E80A6C418 /* LandingViewModel.swift in Sources */, + A5E50D4A5C0CC96AF74C8C8D /* LicenseSettingsView.swift in Sources */, + 53E9BB7C9EFA688DF79A049A /* LoadingButtonActionsDelegate.swift in Sources */, + F526EDD2BB0625673BA4C11C /* LoadingView.swift in Sources */, + 8BF4E284C06302D31E0935A4 /* LoadingViewController.swift in Sources */, + 6E11D51C1B198681FBBBAF50 /* LoadingViewModel.swift in Sources */, + 454F272008A986B435532AE0 /* MeasurementView.swift in Sources */, + 60867B6448E0174B6C94DD34 /* PDFReaderView.swift in Sources */, + F61AC54D164F3F1D35289943 /* QRCodeReaderViewController.swift in Sources */, + AC4D8CE67B84C89878A0FA2A /* RecorderViewController.swift in Sources */, + 752146CA082BED4AE6FA6C33 /* ResultButtonActionsDelegate.swift in Sources */, + 5E7BB7B5DC99420C9F0C7503 /* ResultDetailsView.swift in Sources */, + 35B3D619C9CB0D2485FB93C0 /* ResultDetailsViewController.swift in Sources */, + AB54A1FC59A94B0888996937 /* ResultDetailsViewModel.swift in Sources */, + D6919D00130DD2096F69F352 /* ResultView.swift in Sources */, + 0A1A2396F1C55377451C6AF4 /* ResultViewController.swift in Sources */, + D1C023881D18C8A35E33D669 /* ScanViewController.swift in Sources */, + F81338C8435FEBA076801D69 /* ScanViewModel.swift in Sources */, + D5F2132E906282B4C27F6F83 /* SceneDelegate.swift in Sources */, + 7FAF812248111EAE17B51F8A /* SettingsButtonActionsDelegate.swift in Sources */, + 2E4E98C5AC372ECAECE7BCEA /* SettingsView.swift in Sources */, + 8A6F05DEA64AACAE756353C9 /* SettingsViewController.swift in Sources */, + 61F8BC3D17A20B47266573CC /* SettingsViewModel.swift in Sources */, + 1AC53E9E0FF2699C737155CF /* String+Extension.swift in Sources */, + 015D0AF4D1678D4F78A46880 /* SystemInfo.swift in Sources */, + 927829229CB4F07B5590DDF8 /* TextLandingView.swift in Sources */, + 8355FCC47FD62507211C2312 /* TireDepthFeedbackView.swift in Sources */, + 97EA42AE051CF8B91895393B /* TireDepthsFeedbackView.swift in Sources */, + B2EBB1666A0CF9F779EF8931 /* TireTreadMeasurementView.swift in Sources */, + E867FA59DFE1158EF820BA09 /* UIViewController+Extension.swift in Sources */, + 030CA17364D27E2C86B8D36F /* UserDefaultsManager.swift in Sources */, + D5CBEE1F3E3661B52DBEF1CC /* VolumeButtonObserver.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXVariantGroup section */ - D43E3FAD296573C600F1D384 /* LaunchScreen.storyboard */ = { + BE053E1CF96479297B839F59 /* LaunchScreen.storyboard */ = { isa = PBXVariantGroup; children = ( - D43E3FAE296573C600F1D384 /* Base */, + 4EB97AF36FE6ADF80D5595B4 /* Base */, ); name = LaunchScreen.storyboard; sourceTree = ""; }; - D43E3FEB29659B8900F1D384 /* Localizable.strings */ = { - isa = PBXVariantGroup; - children = ( - D43E3FEA29659B8900F1D384 /* en */, - ); - name = Localizable.strings; - sourceTree = ""; - }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ - D43E3FB1296573C600F1D384 /* Debug */ = { + 18C119D4D305BA2D99708F03 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_ENABLE_OBJC_WEAK = YES; @@ -799,8 +806,8 @@ GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", "$(inherited)", + "DEBUG=1", ); GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; @@ -808,23 +815,62 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.2; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; + PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 9BC34547D662BDC159D1D42F /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 35RHL53WRE; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = "Anyline Tire Demo/Info.plist"; + INFOPLIST_KEY_NSCameraUsageDescription = "Camera permission for capturing video"; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + INFOPLIST_KEY_UIRequiresFullScreen = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations = ( + UIInterfaceOrientationLandscapeLeft, + UIInterfaceOrientationLandscapeRight, + ); + IPHONEOS_DEPLOYMENT_TARGET = 16.4; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0.8; + PRODUCT_BUNDLE_IDENTIFIER = com.anyline.showcase.tiretread; + PROVISIONING_PROFILE_SPECIFIER = ""; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_EMIT_LOC_STRINGS = YES; + TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; }; - D43E3FB2296573C600F1D384 /* Release */ = { + D2397B9C3A9DEAAA948EC469 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_ENABLE_OBJC_WEAK = YES; @@ -862,17 +908,19 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.2; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; + PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; + SWIFT_VERSION = 5.0; VALIDATE_PRODUCT = YES; }; name = Release; }; - D43E3FB4296573C600F1D384 /* Debug */ = { + D9FEFD91F8F16C184F8617E8 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; @@ -881,63 +929,29 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = 35RHL53WRE; - FRAMEWORK_SEARCH_PATHS = "$(inherited)"; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = "Anyline Tire Demo/Info.plist"; INFOPLIST_KEY_NSCameraUsageDescription = "Camera permission for capturing video"; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; INFOPLIST_KEY_UIRequiresFullScreen = YES; - INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - IPHONEOS_DEPLOYMENT_TARGET = 15.2; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", + INFOPLIST_KEY_UISupportedInterfaceOrientations = ( + UIInterfaceOrientationLandscapeLeft, + UIInterfaceOrientationLandscapeRight, ); - MARKETING_VERSION = 1.0.1; - PRODUCT_BUNDLE_IDENTIFIER = com.anyline.showcase.tiretread; - PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = ""; - SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; - SUPPORTS_MACCATALYST = NO; - SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; - SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; - D43E3FB5296573C600F1D384 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CODE_SIGN_IDENTITY = "Apple Development"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = 35RHL53WRE; - FRAMEWORK_SEARCH_PATHS = "$(inherited)"; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = "Anyline Tire Demo/Info.plist"; - INFOPLIST_KEY_NSCameraUsageDescription = "Camera permission for capturing video"; - INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; - INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; - INFOPLIST_KEY_UIRequiresFullScreen = YES; - INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - IPHONEOS_DEPLOYMENT_TARGET = 15.2; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.1; + MARKETING_VERSION = 1.0.8; PRODUCT_BUNDLE_IDENTIFIER = com.anyline.showcase.tiretread; - PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; + SDKROOT = iphoneos; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; @@ -945,44 +959,36 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ - D43E3F9A296573C400F1D384 /* Build configuration list for PBXProject "Anyline Tire Demo" */ = { + 6AAA678835C7FA3FAECC4ABD /* Build configuration list for PBXProject "Anyline Tire Demo" */ = { isa = XCConfigurationList; buildConfigurations = ( - D43E3FB1296573C600F1D384 /* Debug */, - D43E3FB2296573C600F1D384 /* Release */, + 18C119D4D305BA2D99708F03 /* Debug */, + D2397B9C3A9DEAAA948EC469 /* Release */, ); defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; + defaultConfigurationName = Debug; }; - D43E3FB3296573C600F1D384 /* Build configuration list for PBXNativeTarget "Anyline Tire Demo" */ = { + DAAED4700B88574245FCE484 /* Build configuration list for PBXNativeTarget "Anyline Tire Demo" */ = { isa = XCConfigurationList; buildConfigurations = ( - D43E3FB4296573C600F1D384 /* Debug */, - D43E3FB5296573C600F1D384 /* Release */, + 9BC34547D662BDC159D1D42F /* Debug */, + D9FEFD91F8F16C184F8617E8 /* Release */, ); defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; + defaultConfigurationName = Debug; }; /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ - 802B90F4296D80EB0017919D /* XCRemoteSwiftPackageReference "Alamofire" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/Alamofire/Alamofire"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 5.0.0; - }; - }; - A07622DF2A696B5400250387 /* XCRemoteSwiftPackageReference "anyline-tiretread-spm-module" */ = { + 2E17D9FE7E357A222A39DA30 /* XCRemoteSwiftPackageReference "anyline-tiretread-spm-module" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/Anyline/anyline-tiretread-spm-module"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 2.0.0; + minimumVersion = 3.0.0; }; }; - D43E3FF92966E86200F1D384 /* XCRemoteSwiftPackageReference "SnapKit" */ = { + 4B5AEA5F9FCA6152FDAB044B /* XCRemoteSwiftPackageReference "SnapKit" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/SnapKit/SnapKit.git"; requirement = { @@ -990,7 +996,7 @@ kind = branch; }; }; - D4B536FF298002C5009CAB53 /* XCRemoteSwiftPackageReference "keychain-swift" */ = { + 8308C77CEAF862D126235E79 /* XCRemoteSwiftPackageReference "keychain-swift" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/evgenyneu/keychain-swift.git"; requirement = { @@ -998,30 +1004,38 @@ kind = branch; }; }; + 8BDBD2C18B241087A97EA599 /* XCRemoteSwiftPackageReference "Alamofire" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/Alamofire/Alamofire"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 5.0.0; + }; + }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ - 802B90F5296D80EB0017919D /* Alamofire */ = { + 36DF23772E8928D0BFDB3967 /* Alamofire */ = { isa = XCSwiftPackageProductDependency; - package = 802B90F4296D80EB0017919D /* XCRemoteSwiftPackageReference "Alamofire" */; + package = 8BDBD2C18B241087A97EA599 /* XCRemoteSwiftPackageReference "Alamofire" */; productName = Alamofire; }; - A0BEEBA52A6FFF7800E27276 /* AnylineTireTreadSdk */ = { + 4FC06FD0D43DBAB59132C68A /* AnylineTireTreadSdk */ = { isa = XCSwiftPackageProductDependency; - package = A07622DF2A696B5400250387 /* XCRemoteSwiftPackageReference "anyline-tiretread-spm-module" */; + package = 2E17D9FE7E357A222A39DA30 /* XCRemoteSwiftPackageReference "anyline-tiretread-spm-module" */; productName = AnylineTireTreadSdk; }; - D43E3FFA2966E86200F1D384 /* SnapKit */ = { + 5A8E509B4C8C4AC6C6113DBD /* KeychainSwift */ = { isa = XCSwiftPackageProductDependency; - package = D43E3FF92966E86200F1D384 /* XCRemoteSwiftPackageReference "SnapKit" */; - productName = SnapKit; + package = 8308C77CEAF862D126235E79 /* XCRemoteSwiftPackageReference "keychain-swift" */; + productName = KeychainSwift; }; - D4B53700298002C5009CAB53 /* KeychainSwift */ = { + AE4098F783D33EDD00174621 /* SnapKit */ = { isa = XCSwiftPackageProductDependency; - package = D4B536FF298002C5009CAB53 /* XCRemoteSwiftPackageReference "keychain-swift" */; - productName = KeychainSwift; + package = 4B5AEA5F9FCA6152FDAB044B /* XCRemoteSwiftPackageReference "SnapKit" */; + productName = SnapKit; }; /* End XCSwiftPackageProductDependency section */ }; - rootObject = D43E3F97296573C400F1D384 /* Project object */; + rootObject = E5CDB6798FCC7934BA0D0DE6 /* Project object */; } diff --git a/Anyline Tire Demo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Anyline Tire Demo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 231f9dc..b77f90e 100644 --- a/Anyline Tire Demo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Anyline Tire Demo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -5,17 +5,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/Alamofire/Alamofire", "state" : { - "revision" : "bc268c28fb170f494de9e9927c371b8342979ece", - "version" : "5.7.1" - } - }, - { - "identity" : "anyline-tiretread-spm-module", - "kind" : "remoteSourceControl", - "location" : "https://github.com/Anyline/anyline-tiretread-spm-module", - "state" : { - "revision" : "b805e64cc1cdc55c3d67d9450c457724741eee3d", - "version" : "2.1.0" + "revision" : "3dc6a42c7727c49bf26508e29b0a0b35f9c7e1ad", + "version" : "5.8.1" } }, { @@ -24,7 +15,7 @@ "location" : "https://github.com/evgenyneu/keychain-swift.git", "state" : { "branch" : "master", - "revision" : "c1fde55798b164cad44b5e23cfa2f0f1ebcd76af" + "revision" : "95219e66c94b6ba25e7d6ece7aecf1ea13134174" } }, { @@ -33,7 +24,7 @@ "location" : "https://github.com/SnapKit/SnapKit.git", "state" : { "branch" : "develop", - "revision" : "58320fe80522414bf3a7e24c88123581dc586752" + "revision" : "4d52cc3768ec5d345827b063dfa59e98a929965d" } } ], diff --git a/Anyline Tire Demo.xcodeproj/xcshareddata/xcschemes/Anyline Tire Demo.xcscheme b/Anyline Tire Demo.xcodeproj/xcshareddata/xcschemes/Anyline Tire Demo.xcscheme new file mode 100644 index 0000000..162eddd --- /dev/null +++ b/Anyline Tire Demo.xcodeproj/xcshareddata/xcschemes/Anyline Tire Demo.xcscheme @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Anyline Tire Demo/Assets.xcassets/qr_code_icon.imageset/Contents.json b/Anyline Tire Demo/Assets.xcassets/qr_code_icon.imageset/Contents.json new file mode 100644 index 0000000..204d3bc --- /dev/null +++ b/Anyline Tire Demo/Assets.xcassets/qr_code_icon.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "QR Code Icon.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Anyline Tire Demo/Assets.xcassets/qr_code_icon.imageset/QR Code Icon.png b/Anyline Tire Demo/Assets.xcassets/qr_code_icon.imageset/QR Code Icon.png new file mode 100644 index 0000000000000000000000000000000000000000..5ae8a9329c5f3db6e34878b45d5c3b8c80f754be GIT binary patch literal 615 zcmV-t0+{`YP)nW8Ei}tupl=A-1a*hq|RmFm1r?N|mENEniwjeR0^WL4aJ(#{SEKp4(9R`;4y^h(+ zMmB7Z$tVOas99dmJ$oj57>l*=6q+l{tZAb`rjJy`lVl&AQR{e0&T%cq!ei%4nL5CY zctaat$9FB`;uxm}4V?P}Y^Ju9VXzg1Vt$HjgUq8U^r0=08BL#8bR9?RQt5Tmg1a4P zL1X4y1rgaMU7mMc1iNQQLtP3*eWN7yjv@9T^+vT5=9T^TdDex_7Zo=Y+B{KkU1Rva zK9q&LH*(p21bfF&UCx#F=FY^yF;p~slbFiQps8gCcH?h~I<)gmsM$esHj%|dl{+YO z{$4)I>O^6m(Q&X0)%~ literal 0 HcmV?d00001 diff --git a/Anyline Tire Demo/DataStorage/UserDefaultsManager.swift b/Anyline Tire Demo/DataStorage/UserDefaultsManager.swift index 8ea07db..fbb6e16 100644 --- a/Anyline Tire Demo/DataStorage/UserDefaultsManager.swift +++ b/Anyline Tire Demo/DataStorage/UserDefaultsManager.swift @@ -1,4 +1,5 @@ import Foundation +import AnylineTireTreadSdk struct UserDefaultsManager { static var shared = UserDefaultsManager() @@ -39,4 +40,20 @@ struct UserDefaultsManager { UserDefaults.standard.set(newValue, forKey: "imperialSystem") } } + + var scanSpeed: ScanSpeed { + get { + switch Int32(UserDefaults.standard.integer(forKey: "scanSpeed")) { + case ScanSpeed.fast.ordinal: + return .fast + case ScanSpeed.slow.ordinal: + return .slow + default: + return .slow + } + } + set { + UserDefaults.standard.setValue(newValue.ordinal, forKey: "scanSpeed") + } + } } diff --git a/Anyline Tire Demo/Info.plist b/Anyline Tire Demo/Info.plist index d9a293d..7282393 100644 --- a/Anyline Tire Demo/Info.plist +++ b/Anyline Tire Demo/Info.plist @@ -2,13 +2,29 @@ + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 ITSAppUsesNonExemptEncryption UIAppFonts - ProximaNova-Bold.otf - ProximaNova-Light.otf ProximaNova-Regular.otf + ProximaNova-Light.otf + ProximaNova-Bold.otf UIApplicationSceneManifest diff --git a/Anyline Tire Demo/Presentation/Components/ATDSideButton.swift b/Anyline Tire Demo/Presentation/Components/ATDSideButton.swift index d654edd..4195993 100644 --- a/Anyline Tire Demo/Presentation/Components/ATDSideButton.swift +++ b/Anyline Tire Demo/Presentation/Components/ATDSideButton.swift @@ -1,19 +1,25 @@ import UIKit final class ATDSideButton: UIButton { - + // MARK: - Private Properties private let cornerRadius: CGFloat = 15 - + // MARK: - Init init(title: String) { super.init(frame: .zero) setup(title: title) } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + + override var isEnabled: Bool { + didSet { + backgroundColor = isEnabled ? ColorStruct.anylineBlue : ColorStruct.skyGrey + } + } } // MARK: - Private func @@ -21,6 +27,7 @@ private extension ATDSideButton { func setup(title: String) { setTitle(title, for: .normal) setTitleColor(ColorStruct.snowWhite, for: .normal) + setTitleColor(ColorStruct.skyGrey, for: .highlighted) backgroundColor = ColorStruct.anylineBlue contentHorizontalAlignment = .center layer.maskedCorners = [.layerMinXMinYCorner, .layerMinXMaxYCorner] diff --git a/Anyline Tire Demo/Presentation/Components/ATDTopView.swift b/Anyline Tire Demo/Presentation/Components/ATDTopView.swift index 8f0604d..e1be0da 100644 --- a/Anyline Tire Demo/Presentation/Components/ATDTopView.swift +++ b/Anyline Tire Demo/Presentation/Components/ATDTopView.swift @@ -1,16 +1,16 @@ import UIKit class ATDTopView: UIView { - + // MARK: - UI Properties private lazy var logoImageView: UIImageView = { let imageView = UIImageView() - imageView.image = UIImage(named: "anyline_logo_black") + imageView.image = UIImage(named: "anyline_Logo_Black") imageView.contentMode = .scaleAspectFit imageView.tintColor = .white return imageView }() - + lazy var appNameLabel: UILabel = { let label = UILabel() let boldText = "TIRE TREAD" @@ -25,7 +25,7 @@ class ATDTopView: UIView { label.translatesAutoresizingMaskIntoConstraints = false return label }() - + // MARK: - Private Properties // MARK: - Init @@ -45,27 +45,27 @@ class ATDTopView: UIView { // MARK: - Private functions private extension ATDTopView { - + // MARK: - Setup UI func configureView() { backgroundColor = ColorStruct.stoneGrey } - + func addSubviews() { self.addSubview(logoImageView) self.addSubview(appNameLabel) } - + func setupLayout() { - + self.logoImageView.snp.makeConstraints { make in make.height.equalTo(35) make.width.equalTo(150) make.leading.equalTo(25) make.top.equalTo(9) - make.bottom.equalTo(-9) + make.bottom.greaterThanOrEqualTo(-9) } - + self.appNameLabel.snp.makeConstraints { make in make.width.equalTo(300) make.trailing.equalTo(16) diff --git a/Anyline Tire Demo/Presentation/Features/Feedback/Controller/FeedbackViewController.swift b/Anyline Tire Demo/Presentation/Features/Feedback/Controller/FeedbackViewController.swift index 2b23d45..7d4512c 100644 --- a/Anyline Tire Demo/Presentation/Features/Feedback/Controller/FeedbackViewController.swift +++ b/Anyline Tire Demo/Presentation/Features/Feedback/Controller/FeedbackViewController.swift @@ -131,9 +131,9 @@ extension FeedbackViewController: FeedbackButtonActionsDelegate { let safeMiddleValue = Double(middleValue), let safeRightValue = Double(rightValue) { - let leftTreadResultRegion = TreadResultRegion.companion.doInitMm(isAvailable: true, confidence: 100, value: safeLeftValue) - let middleTreadResultRegion = TreadResultRegion.companion.doInitMm(isAvailable: true, confidence: 100, value: safeMiddleValue) - let rightTreadResultRegion = TreadResultRegion.companion.doInitMm(isAvailable: true, confidence: 100, value: safeRightValue) + let leftTreadResultRegion = TreadResultRegion.companion.doInitMm(isAvailable: true, value: safeLeftValue) + let middleTreadResultRegion = TreadResultRegion.companion.doInitMm(isAvailable: true, value: safeMiddleValue) + let rightTreadResultRegion = TreadResultRegion.companion.doInitMm(isAvailable: true, value: safeRightValue) let treadResultRegions = [leftTreadResultRegion, middleTreadResultRegion, rightTreadResultRegion] diff --git a/Anyline Tire Demo/Presentation/Features/Landing/Controller/LandingViewController.swift b/Anyline Tire Demo/Presentation/Features/Landing/Controller/LandingViewController.swift index 88a6a9a..c4ba7fe 100644 --- a/Anyline Tire Demo/Presentation/Features/Landing/Controller/LandingViewController.swift +++ b/Anyline Tire Demo/Presentation/Features/Landing/Controller/LandingViewController.swift @@ -1,15 +1,34 @@ import UIKit import SnapKit +import AnylineTireTreadSdk + +enum LandingViewDisplayMode { + case `default` + case intro +} class LandingViewController: UIViewController { + enum Constants { + static let userDefaultsFlag_seenIntroVideo = "userDefaultsFlag_seenIntroVideo" + } + + private var displayMode: LandingViewDisplayMode? { + didSet { + if let displayMode = displayMode { + landingView.setDisplayMode(displayMode) + landingView.startButton.isEnabled = true + } + } + } + // MARK: - UI properties private var topView: ATDTopView = { let view = ATDTopView() return view }() - private var customView: LandingView = { + private var landingView: LandingView = { let view = LandingView() return view }() @@ -29,7 +48,20 @@ class LandingViewController: UIViewController { configureView() addSubviews() setupLayout() - customView.delegate = self + landingView.delegate = self + + landingView.startButton.isEnabled = false + landingViewModel.tryInitializeSdk(context: self) { [weak self] success, errorMsg in + self?.landingView.startButton.isEnabled = true + if !success { + self?.showError(error: errorMsg ?? "Unknown error") + } + } + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + landingView.startButton.isEnabled = true } } @@ -41,18 +73,17 @@ private extension LandingViewController { func addSubviews() { self.view.addSubview(topView) - self.view.addSubview(customView) + self.view.addSubview(landingView) } func setupLayout() { - topView.snp.makeConstraints { make in make.height.equalTo(52) make.width.equalToSuperview() make.top.leading.trailing.equalToSuperview() } - customView.snp.makeConstraints({ make in + landingView.snp.makeConstraints({ make in make.top.equalTo(topView.snp.bottom) make.leading.trailing.bottom.equalToSuperview() }) @@ -62,26 +93,63 @@ private extension LandingViewController { // MARK: - LandingButtonActionsDelegate extension LandingViewController: LandingButtonActionsDelegate { func startButtonTapped() { - customView.startButton.isEnabled = false - landingViewModel.tryInitializeSdk(context: self) + if !UserDefaults.standard.bool(forKey: Constants.userDefaultsFlag_seenIntroVideo) { + displayMode = .intro + UserDefaults.standard.set(true, forKey: Constants.userDefaultsFlag_seenIntroVideo) + } else { + displayMode = .default + landingView.startButton.isEnabled = false + landingViewModel.tryInitializeSdk(context: self) { [weak self] success, errorMsg in + self?.landingView.startButton.isEnabled = true + guard let self = self, success else { + self?.showError(error: errorMsg ?? "Unknown error") + return + } + self.landingViewModel.requestPermissionsAndProceed(context: self) { success, errorMsg in + self.startScanningScreen() + } + } + } } func settingsButtonTapped() { let settingsVC = SettingsViewController() self.navigationController?.pushViewController(settingsVC, animated: true) } + + func cancelButtonTapped() { + displayMode = .default + } + + func tutorialButtonTapped() { + displayMode = .intro + } + + func startScanningScreen() { + landingView.startButton.isEnabled = true + let scanVC = ScanViewController() + scanVC.delegate = self + self.navigationController?.pushViewController(scanVC, animated: true) + } } // MARK: - LandingViewModelDelegate extension LandingViewController: LandingViewModelDelegate { func authenticationSuccessfully() { - customView.startButton.isEnabled = true - let scanVC = ScanViewController() - self.navigationController?.pushViewController(scanVC, animated: true) + startScanningScreen() } func showError(error: String) { - customView.startButton.isEnabled = true + landingView.startButton.isEnabled = true self.displayAlert(title: "error.title".localized(), message: error) } } + +extension LandingViewController: ScanViewControllerDelegate { + + func startRecorderFlow(uuid: String) { + self.navigationController?.popToViewController(self, animated: false) + let vc = RecorderViewController(uuid: uuid) + self.navigationController?.pushViewController(vc, animated: false) + } +} diff --git a/Anyline Tire Demo/Presentation/Features/Landing/Controller/RecorderViewController.swift b/Anyline Tire Demo/Presentation/Features/Landing/Controller/RecorderViewController.swift new file mode 100644 index 0000000..d78f618 --- /dev/null +++ b/Anyline Tire Demo/Presentation/Features/Landing/Controller/RecorderViewController.swift @@ -0,0 +1,177 @@ +import UIKit +import AnylineTireTreadSdk + +class RecorderViewController: UIViewController { + + var uuid: String + + init(uuid: String) { + self.uuid = uuid + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private var topView: ATDTopView = { + return ATDTopView() + }() + + private var textView: UITextView = { + let textView = UITextView() + + textView.backgroundColor = .white + textView.textColor = .black + textView.isScrollEnabled = false + textView.contentInset = .zero + textView.isEditable = false + textView.isSelectable = false + textView.translatesAutoresizingMaskIntoConstraints = false + + let paragraphStyle = NSMutableParagraphStyle() + paragraphStyle.lineSpacing = 10 + + let tutorialText = "recorder.loading.message".localized() + var attributedString = NSMutableAttributedString(string: tutorialText) + + let string = attributedString.string as NSString + let range = NSRange(location: 0, length: string.length) + + // Set the font + attributedString.addAttribute(.font, value: FontStruct.proximaNovaRegular24!, range: range) + attributedString.addAttribute(.foregroundColor, value: UIColor.black, range: range) + + // line height + attributedString.addAttribute(.paragraphStyle, value: paragraphStyle, range: range) + + textView.attributedText = attributedString + return textView + }() + + override func viewDidLoad() { + super.viewDidLoad() + + addSubviews() + setupLayout() + + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.7) { [weak self] in + guard let self = self else { return } + self.startQRCodeScanForTireIDFeedback(uuid: self.uuid) + } + } + + private func addSubviews() { + self.view.addSubview(topView) + self.view.addSubview(textView) + } + + private func setupLayout() { + view.backgroundColor = ColorStruct.snowWhite + + topView.snp.makeConstraints { make in + make.height.equalTo(52) + make.width.equalToSuperview() + make.top.leading.trailing.equalToSuperview() + } + + textView.snp.makeConstraints { make in + make.centerX.equalToSuperview() + make.centerY.equalToSuperview() + } + } + + private func startQRCodeScanForTireIDFeedback(uuid: String) { + + // scannerViewController?.view.isHidden = true + // show the QR Code + let message = "qrcodereader.guide_text.tire_id".localized() + QRCodeReaderViewController.showReader(over: self.navigationController!, + scanMode: .tireId, + msg: message, + animated: true) { [weak self] qrViewController, value in + guard let value = value else { + // User tapped Cancel + self?.navigationController?.popViewController(animated: false) + return + } + self?.showSendFeedbackConfirmation(over: qrViewController, uuid: uuid, tireId: value) + } + + textView.isHidden = true + } + + private func showSendFeedbackConfirmation(over qrViewController: QRCodeReaderViewController, uuid: String, tireId: String) { + + let message = String(format: "%@\n\n%@\n\n%@", + "recorder.msg.scanned_tire_id_1".localized(), + tireId, "recorder.msg.scanned_tire_id_2".localized()) + let alert = UIAlertController(title: "recorder.alert.title".localized(), + message: message, preferredStyle: .alert) + + // buttons shown in this order. + let buttonTitles = [ + "recorder.button.send", + "recorder.button.title.rescan", + "recorder.button.title.abort" + ] + + for buttonTitle in buttonTitles { + let title = buttonTitle.localized() + + alert.addAction(.init(title: title, style: .default) { [weak self] _ in + guard let self = self else { return } + switch buttonTitle { + case "recorder.button.title.rescan": + qrViewController.restart() + break + case "recorder.button.send": + self.sendTireIdFeedback(uuid: uuid, tireId: tireId, qrViewController: qrViewController) + break + case "recorder.button.title.abort": + qrViewController.dismiss(animated: false) + self.navigationController?.popToRootViewController(animated: true) + break + default: + break + } + }) + } + qrViewController.present(alert, animated: true) + } + + private func sendTireIdFeedback(uuid: String, tireId: String, qrViewController: QRCodeReaderViewController) { + var alertTitle = "recorder.alert.title".localized() + var alertMessage = "recorder.alert.message_success".localized() + var actions: [UIAlertAction] = [] + + AnylineTireTreadSdk.companion.sendTireIdFeedback(measurementUuid: uuid, tireId: tireId) { [weak self] in + let okButtonTitle = "recorder.button.title.ok".localized() + actions.append(.init(title: okButtonTitle, style: .default) { [weak self] _ in + qrViewController.dismiss(animated: false) + self?.navigationController?.popToRootViewController(animated: true) + }) + self?.presentAlert(over: qrViewController, title: alertTitle, message: alertMessage, actions: actions) + + } onFailure: { [weak self] error in + alertTitle = "recorder.alert.title".localized() + alertMessage = "recorder.alert.message_failure".localized() + actions.append(.init(title: "recorder.alert.btn_failure".localized(), style: .default) { [weak self] _ in + self?.sendTireIdFeedback(uuid: uuid, tireId: tireId, qrViewController: qrViewController) + }) + actions.append(.init(title: "recorder.button.title.abort".localized(), style: .default) { [weak self] _ in + qrViewController.dismiss(animated: false) + self?.navigationController?.popViewController(animated: true) + }) + self?.presentAlert(over: qrViewController, title: alertTitle, message: alertMessage, actions: actions) + } + } + + private func presentAlert(over qrViewController: QRCodeReaderViewController, title: String, message: String, actions: [UIAlertAction]) { + let alert = UIAlertController(title: title, message: message, preferredStyle: .alert) + for action in actions { + alert.addAction(action) + } + qrViewController.present(alert, animated: true) + } +} diff --git a/Anyline Tire Demo/Presentation/Features/Landing/LandingButtonActionsDelegate.swift b/Anyline Tire Demo/Presentation/Features/Landing/LandingButtonActionsDelegate.swift index 5cf1d32..1ff506e 100644 --- a/Anyline Tire Demo/Presentation/Features/Landing/LandingButtonActionsDelegate.swift +++ b/Anyline Tire Demo/Presentation/Features/Landing/LandingButtonActionsDelegate.swift @@ -3,4 +3,6 @@ import Foundation protocol LandingButtonActionsDelegate: AnyObject { func startButtonTapped() func settingsButtonTapped() + func cancelButtonTapped() + func tutorialButtonTapped() } diff --git a/Anyline Tire Demo/Presentation/Features/Landing/LandingTextViewDelegate.swift b/Anyline Tire Demo/Presentation/Features/Landing/LandingTextViewDelegate.swift new file mode 100644 index 0000000..6d0e657 --- /dev/null +++ b/Anyline Tire Demo/Presentation/Features/Landing/LandingTextViewDelegate.swift @@ -0,0 +1,6 @@ +import Foundation + +protocol LandingTextViewDelegate: AnyObject { + func startTapped() + func tutorialTapped() +} diff --git a/Anyline Tire Demo/Presentation/Features/Landing/View/LandingView.swift b/Anyline Tire Demo/Presentation/Features/Landing/View/LandingView.swift index 15dc48b..11ca77a 100644 --- a/Anyline Tire Demo/Presentation/Features/Landing/View/LandingView.swift +++ b/Anyline Tire Demo/Presentation/Features/Landing/View/LandingView.swift @@ -1,4 +1,5 @@ import UIKit +import WebKit class LandingView: UIView { @@ -15,8 +16,45 @@ class LandingView: UIView { return button }() + private lazy var cancelButton: ATDSideButton = { + let button = ATDSideButton(title: "landing.button.cancel".localized()) + button.addTarget(self, action: #selector(cancelButtonTapped), for: .touchUpInside) + return button + }() + + private lazy var tutorialButton: ATDSideButton = { + let button = ATDSideButton(title: "landing.button.tutorial".localized()) + button.addTarget(self, action: #selector(tutorialButtonTapped), for: .touchUpInside) + return button + }() + private lazy var textView: TextLandingView = { let view = TextLandingView() + view.delegate = self + return view + }() + + // you need the PRO account to embed using video rather than an iframe: https://stackoverflow.com/a/36042672 + private lazy var webView: WKWebView = { + let view = WKWebView() + let vimeoVideoID = 774805672 + + // can't get autoplay or playsinline to work at least on iOS / Safari + // the docs say that for autoplay to work on Safari, muted should be 1. But that + // doesn't appear to be the case still + let vimeoVideoURL = "https://player.vimeo.com/video/\(vimeoVideoID)?background=0&autoplay=1&muted=0&playsinline=1" + + let htmlCode = """ + + +""" + let htmlString = """ + +\(htmlCode) +""" + + view.loadHTMLString(htmlString, baseURL: nil) + view.scrollView.isScrollEnabled = false return view }() @@ -33,10 +71,34 @@ class LandingView: UIView { setupLayout() } + func setDisplayMode(_ displayMode: LandingViewDisplayMode) { + switch displayMode { + case .default: + startButton.isHidden = false + textView.isHidden = false + webView.isHidden = true + settingsButton.isHidden = false + cancelButton.isHidden = true + tutorialButton.isHidden = false + + case .intro: + startButton.isHidden = true + webView.isHidden = false + textView.isHidden = true + settingsButton.isHidden = true + cancelButton.isHidden = false + tutorialButton.isHidden = true + } + + let cancelBtnTitle = displayMode == .intro ? "landing.button.back".localized() : + "landing.button.cancel".localized() + + cancelButton.setTitle(cancelBtnTitle, for: .normal) + } + required init?(coder: NSCoder) { super.init(coder: coder) } - } // MARK: - Private functions @@ -49,11 +111,15 @@ private extension LandingView { func addSubviews() { self.addSubview(startButton) + self.addSubview(cancelButton) self.addSubview(settingsButton) + self.addSubview(tutorialButton) + self.addSubview(webView) self.addSubview(textView) } func setupLayout() { + startButton.snp.makeConstraints({ make in make.top.equalTo(safeAreaLayoutGuide.snp.top).offset(20) make.trailing.equalTo(0) @@ -61,6 +127,15 @@ private extension LandingView { make.height.equalTo(80) }) + // slightly below start (but above settings by at least as much) + tutorialButton.snp.makeConstraints({ make in + make.top.equalTo(startButton.snp.bottom).offset(10) + make.bottom.lessThanOrEqualTo(settingsButton.snp.top).offset(-10) + make.trailing.equalTo(0) + make.width.equalTo(150) + make.height.equalTo(80) + }) + settingsButton.snp.makeConstraints({ make in make.bottom.equalTo(safeAreaLayoutGuide.snp.bottom).offset(-20) make.trailing.equalTo(0) @@ -68,6 +143,22 @@ private extension LandingView { make.height.equalTo(80) }) + // occupies same space as settings + cancelButton.snp.makeConstraints({ make in + make.bottom.equalTo(safeAreaLayoutGuide.snp.bottom).offset(-20) + make.trailing.equalTo(0) + make.width.equalTo(150) + make.height.equalTo(80) + }) + + webView.snp.makeConstraints { make in + // make.top.equalTo(self.safeAreaLayoutGuide.snp.top) + // make.bottom.equalTo(self.safeAreaLayoutGuide.snp.bottom) + make.top.bottom.equalTo(self.safeAreaLayoutGuide).offset(25) + make.leading.equalTo(40) + make.trailing.equalTo(startButton.snp.leading).offset(-40) + } + textView.snp.makeConstraints { make in make.top.bottom.equalTo(0) make.leading.equalTo(40) @@ -85,4 +176,24 @@ private extension LandingView { func settingsButtonTapped() { delegate?.settingsButtonTapped() } + + @objc + func cancelButtonTapped() { + delegate?.cancelButtonTapped() + } + + @objc + func tutorialButtonTapped() { + delegate?.tutorialButtonTapped() + } +} + +extension LandingView: LandingTextViewDelegate { + func startTapped() { + delegate?.startButtonTapped() + } + + func tutorialTapped() { + delegate?.tutorialButtonTapped() + } } diff --git a/Anyline Tire Demo/Presentation/Features/Landing/View/TextLandingView.swift b/Anyline Tire Demo/Presentation/Features/Landing/View/TextLandingView.swift index bde090a..9f86391 100644 --- a/Anyline Tire Demo/Presentation/Features/Landing/View/TextLandingView.swift +++ b/Anyline Tire Demo/Presentation/Features/Landing/View/TextLandingView.swift @@ -1,7 +1,12 @@ import UIKit -class TextLandingView: UIView { - +class TextLandingView: UIView, UIGestureRecognizerDelegate { + + enum Constants { + static let documentationUrlFull = "https://tiretreaddocu.anyline.com" + static let documentationUrlShort = "tiretreaddocu.anyline.com" + } + // MARK: - UI properties private lazy var welcomeLabel: UILabel = { let label = UILabel() @@ -13,34 +18,63 @@ class TextLandingView: UIView { return label }() - private lazy var startLabel: UILabel = { - let label = UILabel() - let string = "To start scanning, press the START button" - let attributedString = NSMutableAttributedString(string: string) + private lazy var startTextView: UITextView = { + let textView = UITextView() + + textView.backgroundColor = .white + textView.textColor = .black + textView.isScrollEnabled = false + textView.contentInset = .zero + textView.isEditable = false + textView.isSelectable = false + + let paragraphStyle = NSMutableParagraphStyle() + paragraphStyle.lineSpacing = 10 - // Set the font to Helvetica - attributedString.addAttribute(.font, value: FontStruct.proximaNovaRegular24!, range: NSRange(location: 0, length: string.count)) - attributedString.addAttribute(.foregroundColor, value: UIColor.black, range: NSRange(location: 0, length: string.count)) + let tutorialText = "landing.text.tutorial".localized() + let startScanningText = "landing.text.start".localized() - // Set the color and font weight for the word "START" - let startRange = (string as NSString).range(of: "START") - attributedString.addAttribute(.foregroundColor, value: ColorStruct.anylineBlue, range: startRange) - attributedString.addAttribute(.font, value: FontStruct.proximaNovaBold24!, range: startRange) + var attributedString = NSMutableAttributedString(string: tutorialText) + attributedString.append(.init(string: "\n")) + attributedString.append(.init(.init(startScanningText))) + + let string = attributedString.string as NSString + let range = NSRange(location: 0, length: string.length) + + // Set the font + attributedString.addAttribute(.font, value: FontStruct.proximaNovaRegular24!, range: range) + attributedString.addAttribute(.foregroundColor, value: UIColor.black, range: range) + + // line height + attributedString.addAttribute(.paragraphStyle, value: paragraphStyle, range: range) + + // Bold the tutorial line + let tutorialRange = string.range(of: tutorialText) + attributedString.addAttribute(.font, value: FontStruct.proximaNovaBold24!, range: tutorialRange) + + // Set the attributes for the word "START" and "TUTORIAL" + var blueRange = string.range(of: "landing.button.start".localized()) + attributedString.addAttribute(.foregroundColor, value: ColorStruct.anylineBlue, range: blueRange) + attributedString.addAttribute(.font, value: FontStruct.proximaNovaBold24!, range: blueRange) + + blueRange = string.range(of: "landing.button.tutorial".localized()) + attributedString.addAttribute(.foregroundColor, value: ColorStruct.anylineBlue, range: blueRange) + attributedString.addAttribute(.font, value: FontStruct.proximaNovaBold24!, range: blueRange) // Create a label and set the attributed text - label.attributedText = attributedString - return label + textView.attributedText = attributedString + return textView }() private lazy var infoLabel: UILabel = { let label = UILabel() - let string = "For best scanning practices, please go to tiretreaddocu.anyline.com" + let string = "landing.text.info".localized() let attributedString = NSMutableAttributedString(string: string) attributedString.addAttribute(.font, value: FontStruct.proximaNovaRegular20!, range: NSRange(location: 0, length: string.count)) // Add an underline to the URL - let urlRange = (string as NSString).range(of: "tiretreaddocu.anyline.com") + let urlRange = (string as NSString).range(of: Constants.documentationUrlShort) attributedString.addAttribute(.underlineStyle, value: NSUnderlineStyle.single.rawValue, range: urlRange) // Add a tap gesture recognizer to handle clicks on the URL @@ -53,10 +87,10 @@ class TextLandingView: UIView { label.textColor = .black return label }() - - // MARK: - Private Properties - + + // MARK: - Public properties + var delegate: LandingTextViewDelegate? // MARK: - Init override init(frame: CGRect) { @@ -64,6 +98,11 @@ class TextLandingView: UIView { configureView() addSubviews() setupLayout() + + // add gesture recognizer for the text view + let tap = UITapGestureRecognizer(target: self, action: #selector(handleTextViewTap(_:))) + tap.delegate = self + startTextView.addGestureRecognizer(tap) } required init?(coder: NSCoder) { @@ -72,11 +111,34 @@ class TextLandingView: UIView { // MARK: - Actions @objc private func handleTap(_ gesture: UITapGestureRecognizer) { - if let url = URL(string: "https://tiretreaddocu.anyline.com") { + if let url = URL(string: Constants.documentationUrlFull) { UIApplication.shared.open(url, options: [:], completionHandler: nil) } } - + + @objc + func handleTextViewTap(_ sender: UITapGestureRecognizer) { + let myTextView = sender.view as! UITextView + let layoutManager = myTextView.layoutManager + + var location = sender.location(in: myTextView) + location.x -= myTextView.textContainerInset.left; + location.y -= myTextView.textContainerInset.top; + + let characterIndex = layoutManager.characterIndex(for: location, + in: myTextView.textContainer, + fractionOfDistanceBetweenInsertionPoints: nil) + + let string = myTextView.attributedText.string as NSString + let startRange = string.range(of: "landing.button.start".localized()) + let tutorialRange = string.range(of: "landing.button.tutorial".localized()) + + if characterIndex >= startRange.location && characterIndex < startRange.location + startRange.length { + delegate?.startTapped() + } else if characterIndex >= tutorialRange.location && characterIndex < tutorialRange.location + tutorialRange.length { + delegate?.tutorialTapped() + } + } } // MARK: - Private functions @@ -89,7 +151,7 @@ private extension TextLandingView { func addSubviews() { self.addSubview(welcomeLabel) - self.addSubview(startLabel) + self.addSubview(startTextView) self.addSubview(infoLabel) } @@ -98,7 +160,7 @@ private extension TextLandingView { make.leading.top.trailing.equalTo(20) }) - startLabel.snp.makeConstraints({ make in + startTextView.snp.makeConstraints({ make in make.top.equalTo(welcomeLabel.snp.bottom).offset(30) make.leading.trailing.equalTo(20) }) diff --git a/Anyline Tire Demo/Presentation/Features/Landing/ViewModel/LandingViewModel.swift b/Anyline Tire Demo/Presentation/Features/Landing/ViewModel/LandingViewModel.swift index 63d49af..f9d1e93 100644 --- a/Anyline Tire Demo/Presentation/Features/Landing/ViewModel/LandingViewModel.swift +++ b/Anyline Tire Demo/Presentation/Features/Landing/ViewModel/LandingViewModel.swift @@ -9,60 +9,62 @@ protocol LandingViewModelDelegate: AnyObject { } class LandingViewModel { - - // MARK: - Private Let's & Var's + + // MARK: - Private Properties private weak var landingViewModelDelegate: LandingViewModelDelegate? - // MARK: - Public Let's & Var's - // MARK: - Init init(delegate: LandingViewModelDelegate) { self.landingViewModelDelegate = delegate } + + var isInitialized: Bool { + AnylineTireTreadSdk.companion.isInitialized + } - func tryInitializeSdk(context: UIViewController) { + func tryInitializeSdk(context: UIViewController, completion: @escaping (Bool, String?) -> Void) { do { let keychainManager = KeychainManager() - guard let licenceID = keychainManager.getValue(forKey: KeychainKeys.licenseID) else { - landingViewModelDelegate?.showError(error: "error.license.missing_key".localized()) + guard let licenseString = keychainManager.getValue(forKey: KeychainKeys.licenseID) else { + completion(false, "error.license.missing_key".localized()) return } - try AnylineTireTreadSdk.companion.doInit(licenseKey: licenceID, context: context) + try AnylineTireTreadSdk.companion.doInit(licenseKey: licenseString, context: context) - if AnylineTireTreadSdk.companion.isInitialized { - requestPermissionsAndProceed(context: context) + if isInitialized { + completion(true, nil) } else { let errorMessage = "error.invalid_license".localized() - self.landingViewModelDelegate?.showError(error: errorMessage) + completion(false, errorMessage) } } catch { let errorMessage = "error.invalid_license".localized() + " (\(error.localizedDescription))" - self.landingViewModelDelegate?.showError(error: errorMessage) + completion(false, errorMessage) } } - func requestPermissionsAndProceed(context: UIViewController) { + func requestPermissionsAndProceed(context: UIViewController, completion: @escaping (Bool, String?) -> Void) { let cameraAuthorizationStatus = AVCaptureDevice.authorizationStatus(for: .video) switch cameraAuthorizationStatus { case .authorized: - self.landingViewModelDelegate?.authenticationSuccessfully() + completion(true, nil) case .notDetermined: AVCaptureDevice.requestAccess(for: .video) { granted in DispatchQueue.main.async { if granted { - self.landingViewModelDelegate?.authenticationSuccessfully() + completion(true, nil) } else { let errorMessage = "error.camera_permission".localized() - self.landingViewModelDelegate?.showError(error: errorMessage) + completion(false, errorMessage) } } } default: let errorMessage = "error.camera_permission".localized() - self.landingViewModelDelegate?.showError(error: errorMessage) + completion(false, errorMessage) } } } diff --git a/Anyline Tire Demo/Presentation/Features/Loading/Controller/LoadingViewController.swift b/Anyline Tire Demo/Presentation/Features/Loading/Controller/LoadingViewController.swift index 9796ce8..dbe3b23 100644 --- a/Anyline Tire Demo/Presentation/Features/Loading/Controller/LoadingViewController.swift +++ b/Anyline Tire Demo/Presentation/Features/Loading/Controller/LoadingViewController.swift @@ -6,38 +6,38 @@ protocol LoadingViewControllerDelegate: AnyObject { } class LoadingViewController: UIViewController { - + // MARK: - UI properties private var topView: ATDTopView = { let view = ATDTopView() return view }() - + private var customView: LoadingView = { let view = LoadingView() return view }() - + // MARK: - Private properties private lazy var loadingViewModel: LoadingViewModel = { return LoadingViewModel(delegate: self, uuid: uuid) }() - + // MARK: - Public Properties weak var delegate: LoadingViewControllerDelegate? var uuid: String - + // MARK: - Init init(uuid: String) { self.uuid = uuid self.customView.customView.measurementUUIDLabel.text = "Scan ID: \(self.uuid)" super.init(nibName: nil, bundle: nil) } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + // MARK: - Lifecycle override func viewDidLoad() { super.viewDidLoad() @@ -77,49 +77,52 @@ private extension LoadingViewController { } } } - + func addSubviews() { self.view.addSubview(topView) self.view.addSubview(customView) } - + func setupLayout() { self.topView.snp.makeConstraints { make in make.height.equalTo(52) make.width.equalToSuperview() make.top.leading.trailing.equalToSuperview() } - + self.customView.snp.makeConstraints({ make in make.top.equalTo(topView.snp.bottom) make.leading.trailing.bottom.equalToSuperview() }) } - + func setDelegates() { self.customView.delegate = self } - + func startProcessing() { self.loadingViewModel.startDataProcessing() } + + private func returnToHome(animated: Bool = true) { + navigationController?.popToRootViewController(animated: animated) + } } // MARK: - LoadingButtonActionsDelegate extension LoadingViewController: LoadingButtonActionsDelegate { func abortButtonTapped() { - navigationController?.popToRootViewController(animated: true) + returnToHome() } } // MARK: - LoadingViewModelDelegate extension LoadingViewController: LoadingViewModelDelegate { - - func displayDepthResultView(uuid: String, treadDepthResult: TreadDepthResultDTO) { + func displayDepthResultView(uuid: String, treadDepthResult: TreadDepthResult) { let vc = ResultViewController(uuid: uuid, measurementResult: treadDepthResult) self.navigationController?.pushViewController(vc, animated: true) } - + func displayError() { DispatchQueue.main.async { [weak self] in let vc = ErrorViewController() @@ -131,12 +134,10 @@ extension LoadingViewController: LoadingViewModelDelegate { // MARK: - ErrorViewControllerDelegate extension LoadingViewController: ErrorViewControllerDelegate { - func didAbort() { self.delegate?.resetScan() - DispatchQueue.main.async { - self.navigationController?.popToRootViewController(animated: true) + DispatchQueue.main.async { [weak self] in + self?.returnToHome() } } - } diff --git a/Anyline Tire Demo/Presentation/Features/Loading/ViewModel/LoadingViewModel.swift b/Anyline Tire Demo/Presentation/Features/Loading/ViewModel/LoadingViewModel.swift index cf17d6f..62b0daa 100644 --- a/Anyline Tire Demo/Presentation/Features/Loading/ViewModel/LoadingViewModel.swift +++ b/Anyline Tire Demo/Presentation/Features/Loading/ViewModel/LoadingViewModel.swift @@ -3,7 +3,7 @@ import AnylineTireTreadSdk protocol LoadingViewModelDelegate: AnyObject { func displayError() - func displayDepthResultView(uuid: String, treadDepthResult: TreadDepthResultDTO) + func displayDepthResultView(uuid: String, treadDepthResult: TreadDepthResult) } class LoadingViewModel { @@ -11,7 +11,7 @@ class LoadingViewModel { // MARK: - Private properties private weak var loadingViewModelDelegate: LoadingViewModelDelegate? private var processingAttempts = 0 - + // MARK: - Public properties var uuid: String @@ -29,55 +29,23 @@ class LoadingViewModel { } private func fetchTreadDepthResult() { - AnylineTireTreadSdk.companion.getTreadDepthReportResult( - measurementUuid: self.uuid, - onGetTreadDepthReportResultSucceed: { [weak self] response in - response.body { resultDTO, error in + do { + try AnylineTireTreadSdk.companion.getTreadDepthReportResult( + measurementUuid: self.uuid, + onGetTreadDepthReportResultSucceeded: { [weak self] treadDepthResult in guard let self = self else { return } - guard let status = resultDTO?.measurement.status else { - self.loadingViewModelDelegate?.displayError() - return + DispatchQueue.main.async { + self.loadingViewModelDelegate?.displayDepthResultView(uuid: self.uuid, treadDepthResult: treadDepthResult) } - - self.handleTreadDepthResult(treadDepthResult: resultDTO?.result, status: status) - } - }, - onGetTreadDepthReportResultFailed: { [weak self] response, exception in - self?.loadingViewModelDelegate?.displayError() - } - ) - } -} - -// MARK: - Private Actions -private extension LoadingViewModel { - - func isValidTreadDepthResult(treadDepthResult: TreadDepthResultDTO) -> Bool { - return treadDepthResult.global.confidence > 0 - } - - private func handleTreadDepthResult(treadDepthResult: TreadDepthResultDTO?, status: MeasurementStatusDTO) { - switch status { - case .completed: - guard let treadDepthResult = treadDepthResult, isValidTreadDepthResult(treadDepthResult: treadDepthResult) else { - DispatchQueue.main.async { - self.loadingViewModelDelegate?.displayError() - } - return - } - DispatchQueue.main.async { - self.loadingViewModelDelegate?.displayDepthResultView(uuid: self.uuid, treadDepthResult: treadDepthResult) - } - case .failed, .unknown: - DispatchQueue.main.async { - self.loadingViewModelDelegate?.displayError() - } - case .processing, .waitingforimages: - DispatchQueue.global().asyncAfter(deadline: .now() + 0.5) { [weak self] in - self?.fetchTreadDepthResult() - } - default: - break + }, + onGetTreadDepthReportResultFailed: { [weak self] measurementError in + print("Error code: " + (measurementError.errorCode ?? "not available")) + print("Error message: " + measurementError.errorMessage) + self?.loadingViewModelDelegate?.displayError() + }, timeoutSeconds: 60 + ) + } catch { + self.loadingViewModelDelegate?.displayError() } } } diff --git a/Anyline Tire Demo/Presentation/Features/QR/QRCodeReaderViewController.swift b/Anyline Tire Demo/Presentation/Features/QR/QRCodeReaderViewController.swift new file mode 100644 index 0000000..0a4b67a --- /dev/null +++ b/Anyline Tire Demo/Presentation/Features/QR/QRCodeReaderViewController.swift @@ -0,0 +1,199 @@ +import UIKit +import AVFoundation + +class QRCodeReaderViewController: UIViewController, AVCaptureMetadataOutputObjectsDelegate { + + enum ScanMode { + case `default` + case tireId + } + + private var scanMode: ScanMode = .default + + private var captureSession: AVCaptureSession! + + private var previewLayer: AVCaptureVideoPreviewLayer! + + private var completionBlock: ((QRCodeReaderViewController, String?) -> Void)? + + private var cancelButton: UIButton? + + private var instructionView: UIView? + + private var guideText: String? + + private var scannedString: String? { + didSet { + if let scannedString = scannedString { + completionBlock?(self, scannedString) + } + } + } + + private init() { + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + class func showReader(over presenter: UIViewController, + scanMode: ScanMode = .default, + msg: String? = nil, + animated: Bool = true, + completion: ((QRCodeReaderViewController, String?) -> Void)? = nil) { + let qrViewController = QRCodeReaderViewController() + qrViewController.completionBlock = completion + qrViewController.guideText = msg ?? "qrcodereader.guide_text.tire_id".localized() + qrViewController.scanMode = scanMode + qrViewController.modalPresentationStyle = .automatic + presenter.present(qrViewController, animated: animated) + } + + override func viewDidLoad() { + super.viewDidLoad() + setup() + } + + func restart() { + scannedString = nil + self.instructionView?.isHidden = false + self.cancelButton?.isHidden = false + self.previewLayer.isHidden = false + } + + private func addAccessoryViews() { + let cancelBtn = UIButton(type: .custom, + primaryAction: .init(title: "Cancel") { [weak self] _ in + guard let self = self else { return } + self.dismiss(animated: false) + self.completionBlock?(self, nil) + }) + view.addSubview(cancelBtn) + cancelBtn.translatesAutoresizingMaskIntoConstraints = false + cancelBtn.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -30).isActive = true + cancelBtn.topAnchor.constraint(equalTo: view.topAnchor, constant: 20).isActive = true + cancelBtn.titleLabel?.font = FontStruct.proximaNovaBold20 + cancelButton = cancelBtn + + let label = UILabel() + label.textColor = ColorStruct.snowWhite + label.font = UIFont(name: "ProximaNova-Bold", size: 16) + label.text = guideText + label.numberOfLines = 2 + label.translatesAutoresizingMaskIntoConstraints = false + + let holder = UIView() + holder.backgroundColor = ColorStruct.anylineBlue + holder.layer.cornerRadius = 15 + holder.layer.masksToBounds = true + holder.translatesAutoresizingMaskIntoConstraints = false + holder.addSubview(label) + view.addSubview(holder) + + self.instructionView = holder + + label.leadingAnchor.constraint(equalTo: holder.leadingAnchor, constant: 20).isActive = true + label.topAnchor.constraint(equalTo: holder.topAnchor, constant: 20.0).isActive = true + label.centerXAnchor.constraint(equalTo: holder.centerXAnchor).isActive = true + label.centerYAnchor.constraint(equalTo: holder.centerYAnchor).isActive = true + + holder.trailingAnchor.constraint(lessThanOrEqualTo: view.trailingAnchor, constant: -170.0).isActive = true + holder.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true + holder.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -40.0).isActive = true + holder.heightAnchor.constraint(greaterThanOrEqualToConstant: 40.0).isActive = true + + // fade them in + cancelBtn.alpha = 0 + holder.alpha = 0 + + UIView.animate(withDuration: 0.3, delay: 0.3, options: .curveEaseInOut, animations: { + cancelBtn.alpha = 1 + holder.alpha = 1 + }, completion: nil) + } + + func setup() { + captureSession = AVCaptureSession() + + guard let videoCaptureDevice = AVCaptureDevice.default(for: .video) else { + return + } + + do { + let videoInput = try AVCaptureDeviceInput(device: videoCaptureDevice) + captureSession.addInput(videoInput) + } catch { + print("Exception while setting up video input: \(error.localizedDescription)") + return + } + + let metadataOutput = AVCaptureMetadataOutput() + captureSession.addOutput(metadataOutput) + metadataOutput.setMetadataObjectsDelegate(self, queue: .main) + metadataOutput.metadataObjectTypes = [.qr] + + previewLayer = AVCaptureVideoPreviewLayer(session: captureSession) + + DispatchQueue.main.async { [weak self] in + guard let previewLayer = self?.previewLayer, let view = self?.view else { return } + previewLayer.frame = view.layer.bounds + view.layer.addSublayer(previewLayer) + previewLayer.videoGravity = .resizeAspectFill + + // Set the preview layer orientation based on the window scene orientation + let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene + let orientation = windowScene?.windows.first?.windowScene?.interfaceOrientation + var rotationAngle = CGFloat(0) + var videoOrientation: AVCaptureVideoOrientation = .landscapeRight + switch orientation { + case .landscapeLeft: + rotationAngle = 180 + videoOrientation = .landscapeLeft + default: + break + } + if #available(iOS 17.0, *) { + previewLayer.connection?.videoRotationAngle = rotationAngle + } else { + previewLayer.connection?.videoOrientation = videoOrientation + } + self?.addAccessoryViews() + } + + DispatchQueue.global(qos: .background).async { [weak self] in + self?.captureSession.startRunning() + } + } + + func metadataOutput(_ output: AVCaptureMetadataOutput, + didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) { + guard scannedString == nil else { return } + if let metadataObject = metadataObjects.first as? AVMetadataMachineReadableCodeObject, + let code = metadataObject.stringValue { + if scanMode == .tireId { + // only if we can extract tire_id from a URL string + if let tireId = tireIdFromScannedQRCode(code) { + self.scannedString = tireId + self.hideUIElements() + } + } else { + self.scannedString = code + self.hideUIElements() + } + } + } + + private func tireIdFromScannedQRCode(_ urlString: String) -> String? { + let queryItems = URLComponents(string: urlString)?.queryItems + let tireIdParam = queryItems?.filter { $0.name == "tire_id" }.first + return tireIdParam?.value + } + + fileprivate func hideUIElements() { + self.instructionView?.isHidden = true + self.cancelButton?.isHidden = true + self.previewLayer.isHidden = true + } +} diff --git a/Anyline Tire Demo/Presentation/Features/Result details/ViewModel/ResultDetailsViewModel.swift b/Anyline Tire Demo/Presentation/Features/Result details/ViewModel/ResultDetailsViewModel.swift index 9917718..504ab7b 100644 --- a/Anyline Tire Demo/Presentation/Features/Result details/ViewModel/ResultDetailsViewModel.swift +++ b/Anyline Tire Demo/Presentation/Features/Result details/ViewModel/ResultDetailsViewModel.swift @@ -33,14 +33,14 @@ class ResultDetailsViewModel { } try anylineSDK.doInit(licenseKey: licenceID, context: context) - let kotlinByteArray = anylineSDK.getTreadDepthReportPdf(measurementUuid: self.uuid) { _ in + anylineSDK.getTreadDepthReportPdf(measurementUuid: self.uuid) { pdfByteArray in print("PDF fetched from SDK.") - } - - // Convert Kotlin ByteArray to Swift Data - let data = kotlinByteArray.toNSData() + + // Convert ByteArray to Swift Data + let data = pdfByteArray.toNSData() - self.resultDetailsViewModelDelegate?.showPDF(pdfData: data) + self.resultDetailsViewModelDelegate?.showPDF(pdfData: data) + } } catch { resultDetailsViewModelDelegate?.showError(error: "error.description".localized()) } diff --git a/Anyline Tire Demo/Presentation/Features/Result/Controller/ResultViewController.swift b/Anyline Tire Demo/Presentation/Features/Result/Controller/ResultViewController.swift index 8fdfc89..c1de266 100644 --- a/Anyline Tire Demo/Presentation/Features/Result/Controller/ResultViewController.swift +++ b/Anyline Tire Demo/Presentation/Features/Result/Controller/ResultViewController.swift @@ -21,11 +21,11 @@ class ResultViewController: UIViewController { private var rightTireTreadValue: String? // MARK: - Public Properties - var measurementResult: TreadDepthResultDTO + var measurementResult: TreadDepthResult var uuid: String // MARK: - Init - init(uuid: String, measurementResult: TreadDepthResultDTO) { + init(uuid: String, measurementResult: TreadDepthResult) { self.uuid = uuid self.measurementResult = measurementResult @@ -101,21 +101,21 @@ private extension ResultViewController { let useImperial = UserDefaultsManager.shared.imperialSystem - topTireTreadValue = String(useImperial ? measurementResult.global.valueInch : measurementResult.global.valueMm) + topTireTreadValue = String(useImperial ? Double(measurementResult.global.valueInch32nds) : measurementResult.global.valueMm) - if measurementResult.regions[0].available { + if measurementResult.regions[0].isAvailable { leftTireTreadValue = String(useImperial ? - measurementResult.regions[0].valueInch : measurementResult.regions[0].valueMm) + Double(measurementResult.regions[0].valueInch32nds) : measurementResult.regions[0].valueMm) } - if measurementResult.regions[1].available { + if measurementResult.regions[1].isAvailable { middleTireTreadValue = String(useImperial ? - measurementResult.regions[1].valueInch : measurementResult.regions[1].valueMm) + Double(measurementResult.regions[1].valueInch32nds) : measurementResult.regions[1].valueMm) } - if measurementResult.regions[2].available { + if measurementResult.regions[2].isAvailable { rightTireTreadValue = String(useImperial ? - measurementResult.regions[2].valueInch : measurementResult.regions[2].valueMm) + Double(measurementResult.regions[2].valueInch32nds) : measurementResult.regions[2].valueMm) } } diff --git a/Anyline Tire Demo/Presentation/Features/Result/View/MeasurementView.swift b/Anyline Tire Demo/Presentation/Features/Result/View/MeasurementView.swift index a43a18c..e3e3695 100644 --- a/Anyline Tire Demo/Presentation/Features/Result/View/MeasurementView.swift +++ b/Anyline Tire Demo/Presentation/Features/Result/View/MeasurementView.swift @@ -3,10 +3,19 @@ import AnylineTireTreadSdk class MeasurementView: UIView { + enum Location { + case global + case regional + } + + var location: Location = .global + + private var isRegional: Bool { location == .regional } + // MARK: - UI properties private lazy var tireTreadValueLabel: ATDTextLabel = { let label = ATDTextLabel(text: measurementValue ?? "") - label.font = FontStruct.proximaNovaBold23 + label.font = isRegional ? FontStruct.proximaNovaBold16 : FontStruct.proximaNovaBold23 label.textColor = ColorStruct.snowWhite label.textAlignment = .center return label @@ -14,12 +23,12 @@ class MeasurementView: UIView { private lazy var tireTreadMeasurementLabel: ATDTextLabel = { let label = ATDTextLabel(text: unitStr) - label.font = FontStruct.proximaNovaBold23 + label.font = isRegional ? FontStruct.proximaNovaBold16 : FontStruct.proximaNovaBold23 label.textColor = ColorStruct.snowWhite label.textAlignment = .center return label }() - + private var unitStr: String { UserDefaultsManager.shared.imperialSystem ? "32\"" : "mm" } @@ -29,7 +38,7 @@ class MeasurementView: UIView { view.backgroundColor = .white return view }() - + private lazy var emptyMeasurementLine: UIView = { let view = UIView() view.backgroundColor = .white @@ -58,33 +67,35 @@ class MeasurementView: UIView { contentVStackView.isHidden = true return } + let metricValue = Double(measurementValue) ?? 0.0 tireTreadMeasurementLabel.text = unitStr + if UserDefaultsManager.shared.imperialSystem { - _ = DistanceUnitConvertKt.inchToFractionString(inch: Double(measurementValue) ?? 0.0, improper: true, denominator: 32) - let fractionTriple = try? DecimalFractionConvertKt.toFractionTriple(Double(measurementValue) ?? 0.0, improper: false, denominator: 32) - let numerator = fractionTriple?.second?.intValue ?? 0 - tireTreadValueLabel.text = "\(numerator)" - self.backgroundColor = getColorFromInchFraction(numerator) + if let doubleValue = Double(measurementValue) { + let intValue = Int(doubleValue) + tireTreadValueLabel.text = "\(intValue)" + self.backgroundColor = getColorFromInchFraction(intValue) + } } else { - let metricValue = Double(measurementValue) ?? 0.0 tireTreadValueLabel.text = String(format: "%.1f", metricValue) self.backgroundColor = getColorForMetricValue(metricValue) } } } - + // MARK: - Init - override init(frame: CGRect) { - super.init(frame: frame) - configureView() - addSubviews() - setupLayout() - } - required init?(coder: NSCoder) { super.init(coder: coder) } + init(location: MeasurementView.Location = .global) { + super.init(frame: .zero) + self.location = location + + configureView() + addSubviews() + setupLayout() + } } // MARK: - Private functions @@ -93,7 +104,7 @@ private extension MeasurementView { // MARK: - Setup UI func configureView() { backgroundColor = ColorStruct.leafGreen - layer.cornerRadius = 5 + layer.cornerRadius = isRegional ? 4 : 5 } func addSubviews() { @@ -123,7 +134,7 @@ private extension MeasurementView { make.width.equalTo(20) } } - + self.emptyMeasurementLine.snp.makeConstraints { make in make.height.equalTo(2) make.width.equalTo(16) @@ -151,6 +162,6 @@ private extension MeasurementView { return ColorStruct.leafGreen } } - + } diff --git a/Anyline Tire Demo/Presentation/Features/Result/View/TireTreadMeasurementView.swift b/Anyline Tire Demo/Presentation/Features/Result/View/TireTreadMeasurementView.swift index a0bd2db..9c3f539 100644 --- a/Anyline Tire Demo/Presentation/Features/Result/View/TireTreadMeasurementView.swift +++ b/Anyline Tire Demo/Presentation/Features/Result/View/TireTreadMeasurementView.swift @@ -16,17 +16,17 @@ class TireTreadMeasurementView: UIView { }() lazy var leftMeasurementView: MeasurementView = { - var view = MeasurementView() + var view = MeasurementView(location: .regional) return view }() lazy var middleMeasurementView: MeasurementView = { - var view = MeasurementView() + var view = MeasurementView(location: .regional) return view }() lazy var rightMeasurementView: MeasurementView = { - var view = MeasurementView() + var view = MeasurementView(location: .regional) return view }() @@ -98,6 +98,21 @@ private extension TireTreadMeasurementView { make.width.equalTo(450) make.height.equalTo(150) } + + self.leftMeasurementView.snp.makeConstraints { make in + make.width.equalTo(self.topMeasurementView).multipliedBy(0.8) + make.height.equalTo(self.topMeasurementView).multipliedBy(0.8) + } + + self.rightMeasurementView.snp.makeConstraints { make in + make.width.equalTo(self.topMeasurementView).multipliedBy(0.8) + make.height.equalTo(self.topMeasurementView).multipliedBy(0.8) + } + + self.middleMeasurementView.snp.makeConstraints { make in + make.width.equalTo(self.topMeasurementView).multipliedBy(0.8) + make.height.equalTo(self.topMeasurementView).multipliedBy(0.8) + } } } diff --git a/Anyline Tire Demo/Presentation/Features/Scan/Controller/ScanViewController.swift b/Anyline Tire Demo/Presentation/Features/Scan/Controller/ScanViewController.swift index 1aa659f..2e7d107 100644 --- a/Anyline Tire Demo/Presentation/Features/Scan/Controller/ScanViewController.swift +++ b/Anyline Tire Demo/Presentation/Features/Scan/Controller/ScanViewController.swift @@ -4,87 +4,66 @@ import CoreHaptics import MediaPlayer import AnylineTireTreadSdk +protocol ScanViewControllerDelegate { + func startRecorderFlow(uuid: String) +} + class ScanViewController: UIViewController, ScannerViewControllerHolder { - + // MARK: - Private Var's & Let's - private lazy var progressView: UIProgressView = { - let progressView = UIProgressView(progressViewStyle: .default) - progressView.trackTintColor = ColorStruct.skyGrey - progressView.progressTintColor = ColorStruct.anylineBlue - progressView.progress = 0.0 - progressView.isHidden = true - return progressView - }() - - private lazy var distanceLabel: UILabel = { - let label = UILabel() - label.text = "\("scan.distance.ok".localized()): 20" - label.textColor = .clear - label.numberOfLines = 1 - label.textAlignment = .center - label.font = FontStruct.proximaNovaBold23 - return label - }() - - private var scanTimer: Timer? - private var progress: Float = 0 - private let totalTime = 10.0 // Total time for scanning - private let interval = 0.1 // Time interval to update progressView private var volumeButtonObserver: VolumeButtonObserver? - - // Audio properties - var highBeepAudioPlayer: AVAudioPlayer? - var lowBeepAudioPlayer: AVAudioPlayer? - var focusAudioPlayer: AVAudioPlayer? - var startAudioPlayer: AVAudioPlayer? - var stopAudioPlayer: AVAudioPlayer? - var hapticPlayer: CHHapticPatternPlayer? - var engine: CHHapticEngine? - var beepingTimer: Timer? - var previousDistance = 0 - + + var delegate: ScanViewControllerDelegate? + var scannerViewController: UIViewController? var dismissViewController: (() -> Void)? - + // MARK: - Init init() { super.init(nibName: nil, bundle: nil) } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + // MARK: - Life cycle override func viewDidLoad() { super.viewDidLoad() setupTireTreadScanView() - setupSubviews() - setupAudioFiles() - setupHapticEngine() setupVolumeView() } - + override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) self.setupVolumeButtonObserver() } - + override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) - self.stopBeeping() - self.stopAudioPlayers() + self.delegate = nil self.resetVolumeButtonObserver() + self.scannerViewController = nil + } } // MARK: - Private Actions private extension ScanViewController { - + private func setupTireTreadScanView() { - let config = TireTreadScanViewConfig(measurementSystem: UserDefaultsManager.shared.imperialSystem ? .imperial : .metric, useDefaultUi: true, useDefaultHaptic: true) + let userDefaults = UserDefaultsManager.shared + let measurementSystem: MeasurementSystem = userDefaults.imperialSystem ? .imperial : .metric + + let config = TireTreadScanViewConfig.Builder() + .withMeasurementSystem(measurementSystem: measurementSystem) + // The API ".withScanSpeed()" is experimental, may impact scan performance and be removed with any major SDK release. + // You are advised to ignore this configuration on your implementation. + .withScanSpeed(scanSpeed: userDefaults.scanSpeed) + .build() + // creates a TireTreadScannerViewController. You can later refer to it here // as self.scannerViewController. TireTreadScanViewKt.TireTreadScanView(context: UIViewController(), config: config, callback: self) { [weak self] error in @@ -97,7 +76,7 @@ private extension ScanViewController { } addScanViewControllerAsChild() } - + private func addScanViewControllerAsChild() { guard let scannerViewController = scannerViewController else { displayError() @@ -108,54 +87,11 @@ private extension ScanViewController { scannerViewController.view.snp.makeConstraints { $0.edges.equalToSuperview() } scannerViewController.didMove(toParent: self) } - + func setupVolumeView() { let volumeView = MPVolumeView(frame: CGRect(x: -1000, y: -1000, width: 0, height: 0)) view.addSubview(volumeView) } - - func updateUI(status: DistanceStatus, distance: Int) { - let measurementUnit = UserDefaultsManager.shared.imperialSystem ? "in" : "cm" - let textAndColor = textAndColor(for: status) - - self.distanceLabel.text = "\(textAndColor.text): \(distance) \(measurementUnit)" - self.distanceLabel.textColor = textAndColor.color - } - - func textAndColor(for status: DistanceStatus) -> (text: String, color: UIColor) { - var text: String - var color: UIColor - - switch status { - case .close, .tooClose: - text = "scan.distance.increase".localized() - color = (status == .close) ? .yellow : .red - case .far, .tooFar: - text = "scan.distance.decrease".localized() - color = (status == .far) ? .yellow : .red - case .ok: - text = "scan.distance.ok".localized() - color = .green - default: - text = "scan.distance.ok".localized() - color = .green - } - - return (text, color) - } - - @objc func updateProgressView() { - // Update the progress - progress += Float(interval / totalTime) - progressView.progress = progress - - // Check if the scanning process is completed - if progress >= 1.0 { - // Stop the timer - scanTimer?.invalidate() - scanTimer = nil - } - } } // MARK: - AVAudioSession for Volume buttons @@ -170,7 +106,7 @@ private extension ScanViewController { func resetVolumeButtonObserver() { self.volumeButtonObserver = nil } - + private func handleVolumeButtonPressed() { if TireTreadScanner.companion.isInitialized { if TireTreadScanner.companion.instance.isScanning { @@ -183,265 +119,94 @@ private extension ScanViewController { } -// MARK: - Private setup methods -private extension ScanViewController { - func setupSubviews() { - setupBottomDistanceView() - setupProgressView() - } - - func setupProgressView() { - self.view.addSubview(progressView) - progressView.snp.makeConstraints { - $0.top.equalTo(20) - $0.leading.equalTo(200) - $0.trailing.equalTo(-200) - } - } - - func setupBottomDistanceView() { - self.view.addSubview(distanceLabel) - - distanceLabel.snp.makeConstraints { make in - make.leading.trailing.equalToSuperview() - make.bottom.equalTo(-50) - } - } -} - -// MARK: - AVAudioSession for Audio feedback -private extension ScanViewController { - - // Load audio files - func setupAudioFiles() { - - let focusBeepUrl = Bundle.main.url(forResource: "tiretread_focuspoint_found", withExtension: "wav")! - focusAudioPlayer = try? AVAudioPlayer(contentsOf: focusBeepUrl) - focusAudioPlayer?.prepareToPlay() - - let startBeepUrl = Bundle.main.url(forResource: "tiretread_sound_start", withExtension: "mp3")! - startAudioPlayer = try? AVAudioPlayer(contentsOf: startBeepUrl) - startAudioPlayer?.prepareToPlay() - - let stopBeepUrl = Bundle.main.url(forResource: "tiretread_sound_stop", withExtension: "wav")! - stopAudioPlayer = try? AVAudioPlayer(contentsOf: stopBeepUrl) - stopAudioPlayer?.prepareToPlay() - stopAudioPlayer?.delegate = self - - let highBeepUrl = Bundle.main.url(forResource: "tiretread_sound_high_beep", withExtension: "wav")! - highBeepAudioPlayer = try? AVAudioPlayer(contentsOf: highBeepUrl) - highBeepAudioPlayer?.prepareToPlay() - - let lowBeepUrl = Bundle.main.url(forResource: "tiretread_sound_low_beep", withExtension: "wav")! - lowBeepAudioPlayer = try? AVAudioPlayer(contentsOf: lowBeepUrl) - lowBeepAudioPlayer?.prepareToPlay() - - } - - func playStartSound() { - startAudioPlayer?.play() - } - - func playStopSound() { - stopAudioPlayer?.play() - } - - func playFocusSound() { - focusAudioPlayer?.play() - playHapticFeedback() - } - - // Function to start the beeping sound - func startBeeping(distanceInCm: Double, distanceStatus: DistanceStatus) { - // Cancel any existing timer - beepingTimer?.invalidate() - - // Calculate the interval for the timer based on the distance - var interval: Double - var playerToUse: AVAudioPlayer? - - switch distanceStatus { - case .close: - interval = 1000 - playerToUse = self.highBeepAudioPlayer - case .far: - interval = 1000 - playerToUse = self.lowBeepAudioPlayer - case .tooClose: - interval = 500 - playerToUse = self.highBeepAudioPlayer - case .tooFar: - interval = 500 - playerToUse = self.lowBeepAudioPlayer - default: return - } - - // another way of calculating interval (older method) -// let roundedDistance = Int(distanceInCm) -// if roundedDistance < 16 { -// interval = 500 * (distanceInCm / 16) -// playerToUse = self.highBeepAudioPlayer -// } else if roundedDistance > 22 { -// interval = max(500 + (distanceInCm - 22) * (0 - 500) / (40 - 22), 0) -// playerToUse = self.lowBeepAudioPlayer -// } else { -// // Distance is in the optimal range - no need to beep -// return -// } - - // Create a new timer - beepingTimer = Timer.scheduledTimer(withTimeInterval: interval / 1000, repeats: true) { _ in - DispatchQueue.global(qos: .userInitiated).async { - playerToUse?.play() - } - } - - // Ensure the timer is scheduled on the main thread - DispatchQueue.main.async { - self.beepingTimer?.fire() - } - } - - - func setupHapticEngine() { - do { - engine = try CHHapticEngine() - try engine?.start() - } catch let error { - print("Failed to create haptic engine: \(error.localizedDescription)") - } - } - - func playHapticFeedback() { - let sharpTap = CHHapticEvent(eventType: .hapticTransient, parameters: [CHHapticEventParameter(parameterID: .hapticIntensity, value: 1), CHHapticEventParameter(parameterID: .hapticSharpness, value: 1)], relativeTime: 0) - let vibration = CHHapticEvent(eventType: .hapticContinuous, parameters: [ - CHHapticEventParameter(parameterID: .hapticIntensity, value: 1), - CHHapticEventParameter(parameterID: .hapticSharpness, value: 1) - ], relativeTime: 0, duration: 0.1) - let pattern = try? CHHapticPattern(events: [sharpTap, vibration], parameters: []) - do { - hapticPlayer = try engine?.makePlayer(with: pattern!) - try hapticPlayer?.start(atTime: CHHapticTimeImmediate) - } catch { - // Handle the error - print("Failed to play haptic feedback: \(error.localizedDescription)") - } - } - - // Function to stop the beeping sound - func stopBeeping() { - // Cancel the timer - beepingTimer?.invalidate() - beepingTimer = nil - } - - func stopAudioPlayers() { - focusAudioPlayer?.stop() - startAudioPlayer?.stop() - highBeepAudioPlayer?.stop() - lowBeepAudioPlayer?.stop() - try? hapticPlayer?.stop(atTime: CHHapticTimeImmediate) - - focusAudioPlayer = nil - startAudioPlayer = nil - highBeepAudioPlayer = nil - lowBeepAudioPlayer = nil - beepingTimer = nil - hapticPlayer = nil - } -} - -// MARK: - AVAudioPlayerDelegate -extension ScanViewController: AVAudioPlayerDelegate { - - func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) { - stopAudioPlayer?.stop() - stopAudioPlayer = nil - } - -} - // MARK: - ScanViewModelDelegate extension ScanViewController: ScanViewModelDelegate { - + func displayError() { let vc = ErrorViewController() vc.delegate = self self.navigationController?.pushViewController(vc, animated: true) } - + func displayLoading(uuid: String) { - self.scanTimer?.invalidate() - self.playStopSound() DispatchQueue.main.async { let vc = LoadingViewController(uuid: uuid) self.navigationController?.pushViewController(vc, animated: true) } } - + } // MARK: - LoadingViewControllerDelegate extension ScanViewController: LoadingViewControllerDelegate { func resetScan() { - + } } // MARK: - ErrorViewControllerDelegate extension ScanViewController: ErrorViewControllerDelegate { func didAbort() { - + } } extension ScanViewController: TireTreadScanViewCallback { + + // We're using the SDK's defaultUi, so we only really need to implement the behavior + // for the "onScanAbort", "onUploadCompleted", "onUploadAborted", and "onUploadFailed" callbacks func onScanAbort(uuid: String?) { print("scan aborted for uuid: \(uuid ?? "")") self.navigationController?.popViewController(animated: true) } - + func onUploadAborted(uuid: String?) { print("upload aborted for uuid: \(uuid ?? "")") self.navigationController?.popViewController(animated: true) } - func onFocusFound(uuid: String?) { - self.playFocusSound() + func onUploadFailed(uuid: String?, exception: KotlinException) { + self.displayError() } func onUploadCompleted(uuid: String?) { - // Implement this method - self.playStopSound() - if let safeUuid = uuid { - self.displayLoading(uuid: safeUuid) + // On upload complete, we should check for Results. + + // the "shouldRequestTireIdFeedback" is only intended for feedback and + // does not need to be implemented + if let uuid = uuid { + var shouldRequestForFeedback: Bool = false + do { + shouldRequestForFeedback = try AnylineTireTreadSdk.companion.shouldRequestTireIdFeedback() + } catch { + print("caught exception: \(error.localizedDescription)") + } + if shouldRequestForFeedback { + delegate?.startRecorderFlow(uuid: uuid) + } else { + // Your application should directly fetch Results + self.displayLoading(uuid: uuid) + } } else { self.displayError() } } - func onImageUploaded(uuid: String?, uploaded: Int32, total: Int32) { - print("Native iOS: Image uploaded (\(uploaded)/\(total)) for uuid: \(uuid ?? "unknown")") - } - - func onUploadFailed(uuid: String?, exception: KotlinException) { - self.displayError() + func onFocusFound(uuid: String?) { } func onScanStart(uuid: String?) { - self.playStartSound() - self.progressView.isHidden = false - self.scanTimer = Timer.scheduledTimer(timeInterval: interval, target: self, selector: #selector(updateProgressView), userInfo: nil, repeats: true) } func onScanStop(uuid: String?) { - print("Native iOS: Scan stopped for uuid: \(uuid ?? "unknown")") - distanceLabel.isHidden = true - stopBeeping() - stopAudioPlayers() + print("Showcase iOS: Scan stopped for uuid: \(uuid ?? "unknown")") } + func onImageUploaded(uuid: String?, uploaded: Int32, total: Int32) { + print("Showcase iOS: Image uploaded (\(uploaded)/\(total)) for uuid: \(uuid ?? "unknown")") + } + /// Called when the distance has changed. /// /// - Parameters: @@ -453,12 +218,6 @@ extension ScanViewController: TireTreadScanViewCallback { /// /// Note: The distance values are provided in millimeters if the metric system is selected (`UserDefaultsManager.shared.imperialSystem = false`), and in inches if the imperial system is selected (`UserDefaultsManager.shared.imperialSystem = true`). func onDistanceChanged(uuid: String?, previousStatus: DistanceStatus, newStatus: DistanceStatus, previousDistance: Float, newDistance: Float) { - if Int(newDistance) != Int(previousDistance) { - let distanceInCentimeters = UserDefaultsManager.shared.imperialSystem ? (newDistance * 2.54) : (newDistance / 10.0) - startBeeping(distanceInCm: Double(distanceInCentimeters), distanceStatus: newStatus) - DispatchQueue.main.async { [weak self] in - self?.updateUI(status: newStatus, distance: Int(UserDefaultsManager.shared.imperialSystem ? newDistance : distanceInCentimeters)) - } - } + } } diff --git a/Anyline Tire Demo/Presentation/Features/Settings/Controller/SettingsViewController.swift b/Anyline Tire Demo/Presentation/Features/Settings/Controller/SettingsViewController.swift index a021d8d..f196d73 100644 --- a/Anyline Tire Demo/Presentation/Features/Settings/Controller/SettingsViewController.swift +++ b/Anyline Tire Demo/Presentation/Features/Settings/Controller/SettingsViewController.swift @@ -1,4 +1,5 @@ import UIKit +import AnylineTireTreadSdk class SettingsViewController: UIViewController { @@ -8,7 +9,7 @@ class SettingsViewController: UIViewController { return view }() - private var customView: SettingsView = { + private var settingsView: SettingsView = { let view = SettingsView() return view }() @@ -16,6 +17,8 @@ class SettingsViewController: UIViewController { // MARK: - Private Properties private var switchValueButton: Bool = UserDefaultsManager.shared.imageQualitySwitchValue private var imperialSystem: Bool = UserDefaultsManager.shared.imperialSystem + private var scanSpeed: ScanSpeed = UserDefaultsManager.shared.scanSpeed + private lazy var settingsViewModel: SettingsViewModel = { return SettingsViewModel(delegate: self) }() @@ -37,7 +40,7 @@ class SettingsViewController: UIViewController { configureView() addSubviews() setupLayout() - customView.delegate = self + settingsView.delegate = self } override func viewWillDisappear(_ animated: Bool) { @@ -70,7 +73,7 @@ class SettingsViewController: UIViewController { func saveLicenseID() { let keychainManager = KeychainManager() - let licenseIDText = customView.licenseView.licenseIdTextField.text + let licenseIDText = settingsView.licenseView.licenseIdTextField.text if let licenceID = keychainManager.getValue(forKey: KeychainKeys.licenseID) { if licenceID != licenseIDText { if let safeLicenseIDText = licenseIDText { @@ -99,7 +102,7 @@ private extension SettingsViewController { func addSubviews() { self.view.addSubview(topView) - self.view.addSubview(customView) + self.view.addSubview(settingsView) } func setupLayout() { @@ -110,7 +113,7 @@ private extension SettingsViewController { make.top.leading.trailing.equalToSuperview() } - customView.snp.makeConstraints { make in + settingsView.snp.makeConstraints { make in make.top.equalTo(topView.snp.bottom) make.leading.trailing.bottom.equalToSuperview() } @@ -120,13 +123,35 @@ private extension SettingsViewController { // MARK: - Private Actions private extension SettingsViewController { @objc func keyboardWillShow(notification: NSNotification) { - if let userInfo = notification.userInfo, - let keyboardFrame = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect { - - let keyboardHeight = keyboardFrame.size.height - 100 - + + // the field that caused the software keyboard to display + // TODO: maybe use the main responder + let field = self.settingsView.licenseView.licenseIdTextField + + guard let userInfo = notification.userInfo else { return } + + // Get the keyboard’s frame at the end of its animation. + guard let screen = notification.object as? UIScreen, + let keyboardFrameEnd = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { return } + + let fromCoordinateSpace = screen.coordinateSpace + + let toCoordinateSpace: UICoordinateSpace = view + + // Convert the keyboard's frame from the screen's coordinate space to your view's coordinate space. + let convertedKeyboardFrameEnd = fromCoordinateSpace.convert(keyboardFrameEnd, to: toCoordinateSpace) + + let yMargin = 10.0 // min distance we allow between the field's bottom and the keyboard + + let fieldBottomY = (field.convert(field.frame.origin, to: screen.coordinateSpace).y + + field.bounds.height + + yMargin) + + var shift = 0.0 + if convertedKeyboardFrameEnd.minY < fieldBottomY { + shift = convertedKeyboardFrameEnd.minY - fieldBottomY UIView.animate(withDuration: 0.3) { - self.view.frame.origin.y = -keyboardHeight + self.view.frame.origin.y = shift } } } @@ -144,6 +169,57 @@ private extension SettingsViewController { // MARK: - SettingsButtonActionsDelegate extension SettingsViewController: SettingsButtonActionsDelegate { + + func scanQRCodeTapped() { + QRCodeReaderViewController.showReader(over: self.navigationController!, + msg: "qrcodereader.guide_text.license_key".localized(), + animated: true) { [weak self] qrViewController, decoded in + guard let decoded = decoded else { + return + } + if let licenseKeyString = self?.returnValidLicenseKey(decoded) { + self?.settingsView.licenseView.licenseIdTextField.text = licenseKeyString + qrViewController.dismiss(animated: true) + } else { + let msg = "qrcodereader.message.invalid_license_key".localized() + self?.showAlertQRCode(over: qrViewController, msg: msg) + } + } + } + + func showAlertQRCode(over qrViewController: QRCodeReaderViewController, msg: String) { + let alert = UIAlertController(title: "Scan License Key", message: msg, preferredStyle: .alert) + alert.addAction(.init(title: "OK", style: .default, handler: { _ in + qrViewController.restart() + })) + qrViewController.present(alert, animated: true) + } + + fileprivate func returnValidLicenseKey(_ licenseKeyString: String) -> String? { + return licenseKeyString + } + + func scanSpeedDialogRequested(sender: UIButton, options: [ScanSpeed], completion: (ScanSpeed?) -> Void) { + let alert = UIAlertController(title: "Select Capture Speed", message: nil, preferredStyle: .actionSheet) + + for speed in options { + let title = speed.name + alert.addAction(.init(title: title, style: .default, handler: { [weak self] action in + print("preset tapped: \(title) for preset \(speed.name)") + self?.settingsView.captureSpeedView.scanSpeed = speed + UserDefaultsManager.shared.scanSpeed = speed + })) + } + alert.addAction(.init(title: "Cancel", style: .cancel)) + + let popoverController = alert.popoverPresentationController + popoverController?.sourceView = sender + popoverController?.sourceRect = sender.bounds + popoverController?.permittedArrowDirections = .up + + self.navigationController?.present(alert, animated: true) + } + func okButtonTapped() { saveImperialSystem() saveLicenseID() @@ -152,7 +228,8 @@ extension SettingsViewController: SettingsButtonActionsDelegate { } func testUploadButtonTapped() { - self.settingsViewModel.testSetup(context: self) + let licenseKey = settingsView.licenseView.licenseIdTextField.text ?? "" + self.settingsViewModel.testLicenseKey(licenseKey, context: self) } func cancelButtonTapped() { diff --git a/Anyline Tire Demo/Presentation/Features/Settings/SettingsButtonActionsDelegate.swift b/Anyline Tire Demo/Presentation/Features/Settings/SettingsButtonActionsDelegate.swift index ed66e10..2b7d6a1 100644 --- a/Anyline Tire Demo/Presentation/Features/Settings/SettingsButtonActionsDelegate.swift +++ b/Anyline Tire Demo/Presentation/Features/Settings/SettingsButtonActionsDelegate.swift @@ -1,4 +1,5 @@ import UIKit +import AnylineTireTreadSdk protocol SettingsButtonActionsDelegate: AnyObject { func okButtonTapped() @@ -6,4 +7,11 @@ protocol SettingsButtonActionsDelegate: AnyObject { func cancelButtonTapped() func imageTapped(tapGestureRecognizer: UITapGestureRecognizer) func switchChanged(mySwitch: UISwitch) + + func scanQRCodeTapped() + func scanSpeedDialogRequested(sender: UIButton, options: [ScanSpeed], completion: (ScanSpeed?) -> Void) +} + +protocol CaptureSpeedViewDelegate: AnyObject { + func buttonTapped(sender: UIButton) } diff --git a/Anyline Tire Demo/Presentation/Features/Settings/View/ButtonsSettingsView.swift b/Anyline Tire Demo/Presentation/Features/Settings/View/ButtonsSettingsView.swift index d7def81..20b98d1 100644 --- a/Anyline Tire Demo/Presentation/Features/Settings/View/ButtonsSettingsView.swift +++ b/Anyline Tire Demo/Presentation/Features/Settings/View/ButtonsSettingsView.swift @@ -69,14 +69,15 @@ private extension ButtonsSettingsView { } testUploadButton.snp.makeConstraints { make in - make.top.equalTo(okButton.snp.bottom).offset(30) + make.top.equalTo(okButton.snp.bottom).offset(10) + make.bottom.lessThanOrEqualTo(cancelButton.snp.top).offset(-10) make.trailing.equalTo(0) make.width.equalTo(150) make.height.equalTo(80) } cancelButton.snp.makeConstraints { make in - make.bottom.equalTo(safeAreaLayoutGuide.snp.bottom).offset(-10) + make.bottom.equalTo(safeAreaLayoutGuide.snp.bottom).offset(-20) make.trailing.equalTo(0) make.width.equalTo(150) make.height.equalTo(80) diff --git a/Anyline Tire Demo/Presentation/Features/Settings/View/CaptureSpeedView.swift b/Anyline Tire Demo/Presentation/Features/Settings/View/CaptureSpeedView.swift new file mode 100644 index 0000000..b01d0b5 --- /dev/null +++ b/Anyline Tire Demo/Presentation/Features/Settings/View/CaptureSpeedView.swift @@ -0,0 +1,84 @@ +import UIKit +import AnylineTireTreadSdk + +class CaptureSpeedView: UIView { + + var delegate: CaptureSpeedViewDelegate? + + var scanSpeed: ScanSpeed = UserDefaultsManager.shared.scanSpeed { + didSet { + let attrStr = attributedString(scanSpeed: scanSpeed) + buttonText.setAttributedTitle(attrStr, for: .normal) + } + } + + private func attributedString(scanSpeed: ScanSpeed) -> NSAttributedString { + let presetValue = scanSpeed.name + let fullText = "\(presetValue) (Tap to change)" + let attributedString = NSMutableAttributedString(string: fullText) + let range = (fullText as NSString).range(of: presetValue) + let boldAttributes: [NSAttributedString.Key: Any] = [ + .font: FontStruct.proximaNovaBold16! + ] + attributedString.addAttributes(boldAttributes, range: range) + return attributedString + } + + private lazy var headerLabel: ATDTextLabel = { + let label = ATDTextLabel(text: "settings.label.capture_speed".localized()) + return label + }() + + private lazy var buttonText: UIButton = { + let button = UIButton(type: .custom) + button.setTitleColor(ColorStruct.anylineBlue, for: .normal) + button.titleLabel?.font = FontStruct.proximaNovaRegular14 + button.addTarget(self, action: #selector(didPressButton), for: .touchUpInside) + return button + }() + + @objc func didPressButton(btn: UIButton) { + delegate?.buttonTapped(sender: btn) + } + + // MARK: - Init + override init(frame: CGRect) { + super.init(frame: frame) + backgroundColor = ColorStruct.snowWhite + configureView() + addSubviews() + setupLayout() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + } + +} + +private extension CaptureSpeedView { + + // MARK: - Setup UI + func configureView() { + let attrStr = attributedString(scanSpeed: scanSpeed) + buttonText.setAttributedTitle(attrStr, for: .normal) + } + + func addSubviews() { + self.addSubview(headerLabel) + self.addSubview(buttonText) + } + + func setupLayout() { + headerLabel.snp.makeConstraints { make in + make.top.bottom.equalToSuperview() + make.leading.equalToSuperview() + } + + buttonText.snp.makeConstraints { make in + make.top.bottom.equalToSuperview() + make.leading.equalTo(headerLabel.snp.trailing).offset(5) + make.trailing.lessThanOrEqualToSuperview() + } + } +} diff --git a/Anyline Tire Demo/Presentation/Features/Settings/View/InfoSettingsView.swift b/Anyline Tire Demo/Presentation/Features/Settings/View/InfoSettingsView.swift index 3b76ae9..a77958f 100644 --- a/Anyline Tire Demo/Presentation/Features/Settings/View/InfoSettingsView.swift +++ b/Anyline Tire Demo/Presentation/Features/Settings/View/InfoSettingsView.swift @@ -4,12 +4,13 @@ class InfoSettingsView: UIView { // MARK: - UI properties private lazy var appVersionLabel: ATDTextLabel = { - let label = ATDTextLabel(text: "settings.label.app_version".localized() + " \(SystemInfo.getAppVersion())") + let label = ATDTextLabel(text: "settings.label.app_sdk_version".localized() + " App: \(SystemInfo.getAppVersion()) - SDK: \(SystemInfo.getSDKVersion())") return label }() private lazy var deviceNameLabel: ATDTextLabel = { - let label = ATDTextLabel(text: "settings.label.device_name".localized() + " \(SystemInfo.getDeviceName())") + let deviceName = SystemInfo.getDeviceName() + let label = ATDTextLabel(text: "settings.label.device_name".localized() + " \(deviceName)") return label }() @@ -23,6 +24,7 @@ class InfoSettingsView: UIView { let stackView = UIStackView() stackView.axis = .vertical stackView.distribution = .fillEqually + stackView.spacing = 5 return stackView }() @@ -62,7 +64,6 @@ private extension InfoSettingsView { func setupLayout() { contentVStackView.snp.makeConstraints { make in make.edges.equalToSuperview() - make.height.equalTo(100) } } } diff --git a/Anyline Tire Demo/Presentation/Features/Settings/View/LicenseSettingsView.swift b/Anyline Tire Demo/Presentation/Features/Settings/View/LicenseSettingsView.swift index 528a563..1a20d50 100644 --- a/Anyline Tire Demo/Presentation/Features/Settings/View/LicenseSettingsView.swift +++ b/Anyline Tire Demo/Presentation/Features/Settings/View/LicenseSettingsView.swift @@ -1,22 +1,34 @@ import UIKit class LicenseSettingsView: UIView { - + + enum Constants { + static let emailAddress = "presales@anyline.com" + } + // MARK: - UI properties private lazy var licenseIdLabel: ATDTextLabel = { let label = ATDTextLabel(text: "settings.label.license_id".localized()) return label }() - + lazy var licenseIdTextField: ATDTextField = { let textField = ATDTextField(backgroundColor: ColorStruct.snowWhite) textField.placeholder = "settings.label.license_id".localized() if let licenceID = KeychainManager().getValue(forKey: KeychainKeys.licenseID) { textField.text = licenceID } + textField.returnKeyType = .done return textField }() - + + // button tap is handled by owner + lazy var scanQRCodeButton: UIButton = { + let button = UIButton() + button.setImage(.init(named: "qr_code_icon"), for: .normal) + return button + }() + private lazy var licenseHStackView: UIStackView = { let stackView = UIStackView() stackView.axis = .horizontal @@ -24,9 +36,9 @@ class LicenseSettingsView: UIView { stackView.spacing = 20 return stackView }() - + private lazy var supportLabel: ATDTextLabel = { - let emailAddress = "presales@anyline.com" + let emailAddress = Constants.emailAddress let label = ATDTextLabel(text: "") // Create a string with the email address @@ -38,6 +50,10 @@ class LicenseSettingsView: UIView { let url = URL(string: "mailto:\(emailAddress)")! attributedString.addAttribute(.link, value: url, range: range) + // NOTE: you cannot set a foreground color at the same range which you + // also set as a .link, unless you change the underlying type, currently + // label, to UITextView. + // Set the attributed string for the label label.attributedText = attributedString label.font = FontStruct.proximaNovaBold14 @@ -49,7 +65,7 @@ class LicenseSettingsView: UIView { label.addGestureRecognizer(tapGesture) return label }() - + private lazy var contentVStackView: UIStackView = { let stackView = UIStackView() stackView.axis = .vertical @@ -58,11 +74,11 @@ class LicenseSettingsView: UIView { stackView.spacing = 10 return stackView }() - + // MARK: - Private Properties - + // MARK: - Public properties - + // MARK: - Init override init(frame: CGRect) { super.init(frame: frame) @@ -71,72 +87,69 @@ class LicenseSettingsView: UIView { setupLayout() licenseIdTextField.delegate = self } - + required init?(coder: NSCoder) { super.init(coder: coder) } - + } // MARK: - Private functions private extension LicenseSettingsView { - + // MARK: - Setup UI func configureView() { backgroundColor = ColorStruct.snowWhite - + } - + func addSubviews() { self.addSubview(contentVStackView) self.contentVStackView.addArrangedSubview(licenseHStackView) self.licenseHStackView.addArrangedSubview(licenseIdLabel) self.licenseHStackView.addArrangedSubview(licenseIdTextField) + self.licenseHStackView.addArrangedSubview(scanQRCodeButton) self.contentVStackView.addArrangedSubview(supportLabel) } - + func setupLayout() { - + contentVStackView.snp.makeConstraints { make in make.edges.equalToSuperview() } - + + licenseIdLabel.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal) licenseIdLabel.snp.makeConstraints { make in make.centerY.equalTo(licenseIdTextField.snp.centerY) } - + licenseIdTextField.snp.makeConstraints { make in make.height.equalTo(40) - make.width.equalTo(450) + make.width.lessThanOrEqualTo(280) + make.trailing.equalTo(scanQRCodeButton.snp.leading).offset(-10) + } + + scanQRCodeButton.snp.makeConstraints { make in + make.trailing.equalTo(supportLabel) + make.centerY.equalTo(licenseIdTextField) } - } - + // MARK: - Actions @objc func handleLinkTap(_ gesture: UITapGestureRecognizer) { guard let label = gesture.view as? UILabel else { return } guard let attributedString = label.attributedText else { return } - - let location = gesture.location(in: label) - let textStorage = NSTextStorage(attributedString: attributedString) - let layoutManager = NSLayoutManager() - textStorage.addLayoutManager(layoutManager) - let textContainer = NSTextContainer(size: label.bounds.size) - textContainer.lineFragmentPadding = 0 - textContainer.maximumNumberOfLines = label.numberOfLines - textContainer.lineBreakMode = label.lineBreakMode - layoutManager.addTextContainer(textContainer) - - let characterIndex = layoutManager.characterIndex(for: location, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil) - if let url = attributedString.attribute(.link, at: characterIndex, effectiveRange: nil) as? URL { - UIApplication.shared.open(url) + let range = (attributedString.string as NSString).range(of: Constants.emailAddress) + if range.location != NSNotFound, + let emailURL = attributedString.attribute(.link, at: range.location, effectiveRange: nil) as? URL { + UIApplication.shared.open(emailURL, options: [:], completionHandler: nil) } } } // MARK: - UITextFieldDelegate extension LicenseSettingsView: UITextFieldDelegate { - + func textFieldShouldReturn(_ textField: UITextField) -> Bool { self.licenseIdTextField.resignFirstResponder() return true diff --git a/Anyline Tire Demo/Presentation/Features/Settings/View/SettingsView.swift b/Anyline Tire Demo/Presentation/Features/Settings/View/SettingsView.swift index 85484f9..be16a96 100644 --- a/Anyline Tire Demo/Presentation/Features/Settings/View/SettingsView.swift +++ b/Anyline Tire Demo/Presentation/Features/Settings/View/SettingsView.swift @@ -16,7 +16,12 @@ class SettingsView: UIView { stackView.spacing = spacing return stackView }() - + + private lazy var contentScrollView: UIScrollView = { + let scrollView = UIScrollView() + return scrollView + }() + private lazy var settingsLabel: ATDTitleLabel = { let label = ATDTitleLabel(textColor: ColorStruct.stoneGrey, text: "settings.label.settings".localized()) label.textAlignment = .center @@ -37,13 +42,23 @@ class SettingsView: UIView { let view = InfoSettingsView() return view }() - + + lazy var captureSpeedView: CaptureSpeedView = { + let view = CaptureSpeedView() + view.delegate = self + return view + }() + + @objc func didTapQRCode(sender: UIButton) { + delegate?.scanQRCodeTapped() + } + // MARK: - Private Properties - private let spacing: CGFloat = 20 + private let spacing: CGFloat = 15 // MARK: - Public properties weak var delegate: SettingsButtonActionsDelegate? - + // MARK: - Init override init(frame: CGRect) { super.init(frame: frame) @@ -56,7 +71,6 @@ class SettingsView: UIView { required init?(coder: NSCoder) { super.init(coder: coder) } - } // MARK: - Private functions @@ -65,30 +79,41 @@ private extension SettingsView { // MARK: - Setup UI func configureView() { backgroundColor = ColorStruct.snowWhite + licenseView.scanQRCodeButton.addTarget(self, + action: #selector(didTapQRCode), + for: .touchUpInside) } func addSubviews() { self.addSubview(buttonsView) self.addSubview(contentVStackView) - self.contentVStackView.addArrangedSubview(settingsLabel) - self.contentVStackView.addArrangedSubview(imperialSystemView) - self.contentVStackView.addArrangedSubview(licenseView) - self.contentVStackView.addArrangedSubview(infoView) + + contentVStackView.addArrangedSubview(settingsLabel) + contentVStackView.addArrangedSubview(imperialSystemView) + contentVStackView.addArrangedSubview(licenseView) + contentVStackView.addArrangedSubview(captureSpeedView) + contentVStackView.addArrangedSubview(infoView) } func setupLayout() { buttonsView.snp.makeConstraints { make in make.trailing.bottom.top.equalToSuperview() - make.width.equalTo(200) + make.width.equalTo(200).priority(.low) } - + contentVStackView.snp.makeConstraints { make in make.leading.equalTo(80) - make.trailing.equalTo(buttonsView.safeAreaLayoutGuide.snp.leading) + make.width.equalTo(500) make.top.equalTo(20) make.bottom.equalTo(0) } - + + captureSpeedView.snp.makeConstraints { make in + make.height.equalTo(35) + make.leading.equalToSuperview() + make.trailing.equalToSuperview() + } + settingsLabel.snp.makeConstraints { make in make.top.equalTo(self.safeAreaInsets.top) make.centerX.equalTo(self) @@ -124,3 +149,13 @@ extension SettingsView: ImperialSystemSettingsViewDelegate { delegate?.imageTapped(tapGestureRecognizer: tapGestureRecognizer) } } + +extension SettingsView: CaptureSpeedViewDelegate { + func buttonTapped(sender: UIButton) { + delegate?.scanSpeedDialogRequested(sender: sender, options: [.fast, .slow], completion: { scanSpeed in + if let scanSpeed = scanSpeed { + self.captureSpeedView.scanSpeed = scanSpeed + } + }) + } +} diff --git a/Anyline Tire Demo/Presentation/Features/Settings/ViewModel/SettingsViewModel.swift b/Anyline Tire Demo/Presentation/Features/Settings/ViewModel/SettingsViewModel.swift index 3f2414f..bd87bf5 100644 --- a/Anyline Tire Demo/Presentation/Features/Settings/ViewModel/SettingsViewModel.swift +++ b/Anyline Tire Demo/Presentation/Features/Settings/ViewModel/SettingsViewModel.swift @@ -20,23 +20,10 @@ class SettingsViewModel { self.settingsViewModelDelegate = delegate } - func testSetup(context: UIViewController) { + func testLicenseKey(_ licenseKey: String, context: UIViewController) { do { - let keychainManager = KeychainManager() - - guard let licenceID = keychainManager.getValue(forKey: KeychainKeys.licenseID) else { - settingsViewModelDelegate?.showError(error: "error.invalid_license".localized()) - return - } - - try AnylineTireTreadSdk.companion.doInit(licenseKey: licenceID, context: context) - - if AnylineTireTreadSdk.companion.isInitialized { - requestPermissionsAndProceed(context: context) - } else { - let errorMessage = "error.invalid_license".localized() - self.settingsViewModelDelegate?.showError(error: errorMessage) - } + try AnylineTireTreadSdk.companion.doInit(licenseKey: licenseKey, context: context) + requestPermissionsAndProceed(context: context) } catch { let errorMessage = "error.invalid_license".localized() + " (\(error.localizedDescription))" self.settingsViewModelDelegate?.showError(error: errorMessage) diff --git a/Anyline Tire Demo/Resources/Audio/tiretread_sound_low_beep.wav b/Anyline Tire Demo/Resources/Audio/tiretread_sound_beep_too_close.wav similarity index 100% rename from Anyline Tire Demo/Resources/Audio/tiretread_sound_low_beep.wav rename to Anyline Tire Demo/Resources/Audio/tiretread_sound_beep_too_close.wav diff --git a/Anyline Tire Demo/Resources/Audio/tiretread_sound_high_beep.wav b/Anyline Tire Demo/Resources/Audio/tiretread_sound_beep_too_far.wav similarity index 100% rename from Anyline Tire Demo/Resources/Audio/tiretread_sound_high_beep.wav rename to Anyline Tire Demo/Resources/Audio/tiretread_sound_beep_too_far.wav diff --git a/Anyline Tire Demo/Resources/Audio/tiretread_sound_start.mp3 b/Anyline Tire Demo/Resources/Audio/tiretread_sound_start.mp3 deleted file mode 100644 index 32337ba40f3edb932481f2c58e8833d440303695..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14880 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 diff --git a/Anyline Tire Demo/Resources/Audio/tiretread_sound_start.wav b/Anyline Tire Demo/Resources/Audio/tiretread_sound_start.wav new file mode 100644 index 0000000000000000000000000000000000000000..5033b626866e9a231c230d28fa7348f3928671ab GIT binary patch literal 71502 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 literal 0 HcmV?d00001 diff --git a/Anyline Tire Demo/Resources/Helpers/VolumeButtonObserver.swift b/Anyline Tire Demo/Resources/Helpers/VolumeButtonObserver.swift index d545417..86733ef 100644 --- a/Anyline Tire Demo/Resources/Helpers/VolumeButtonObserver.swift +++ b/Anyline Tire Demo/Resources/Helpers/VolumeButtonObserver.swift @@ -2,35 +2,25 @@ import AVFoundation class VolumeButtonObserver { - private let audioSession = AVAudioSession.sharedInstance() - private var observer: NSKeyValueObservation? - private var previousVolume: Float - var onVolumeButtonPressed: (() -> Void)? - + + deinit { + NotificationCenter.default.removeObserver(self) + } + init() { - do { - try audioSession.setActive(true) - previousVolume = audioSession.outputVolume - observer = audioSession.observe(\.outputVolume) { [weak self] _, _ in - self?.handleVolumeButtonPressed() - } - } catch { - print("Error configuring audio session: \(error)") - previousVolume = 0.0 - } + // This class' deinit has a call to deregister itself as the + // observer for this event. + NotificationCenter.default + .addObserver(self, + selector: #selector(volChanged(_:)), + name: NSNotification.Name(rawValue: "SystemVolumeDidChange"), + object: nil) } - - private func handleVolumeButtonPressed() { - let currentVolume = audioSession.outputVolume - if currentVolume < previousVolume { + + @objc func volChanged(_ notif: Notification) { + if notif.userInfo?["Reason"] as? String == "ExplicitVolumeChange" { onVolumeButtonPressed?() } - previousVolume = currentVolume - } - - deinit { - observer?.invalidate() - try? audioSession.setActive(false) } } diff --git a/Anyline Tire Demo/Resources/System/SystemInfo.swift b/Anyline Tire Demo/Resources/System/SystemInfo.swift index efceeae..5716387 100644 --- a/Anyline Tire Demo/Resources/System/SystemInfo.swift +++ b/Anyline Tire Demo/Resources/System/SystemInfo.swift @@ -1,3 +1,4 @@ +import AnylineTireTreadSdk import UIKit class SystemInfo { @@ -6,11 +7,346 @@ class SystemInfo { let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String return appVersion ?? "" } + + static func getSDKVersion() -> String { + return AnylineTireTreadSdk.companion.sdkVersion + } static func getDeviceName() -> String { - let deviceModel = UIDevice.current.model + let deviceModel = UIDevice().type.rawValue let deviceOSVersion = UIDevice.current.systemVersion - let versionString = "\(deviceModel) (\(deviceOSVersion))" + let versionString = "\(deviceModel) (iOS \(deviceOSVersion))" return versionString } } + +// Based on https://stackoverflow.com/a/46234519 +// This has to be updated with each new device release +public enum DeviceModel: String { + + // Simulator + case simulator = "simulator/sandbox", + + // iPod + iPod1 = "iPod 1", + iPod2 = "iPod 2", + iPod3 = "iPod 3", + iPod4 = "iPod 4", + iPod5 = "iPod 5", + iPod6 = "iPod 6", + iPod7 = "iPod 7", + + // iPad + iPad2 = "iPad 2", + iPad3 = "iPad 3", + iPad4 = "iPad 4", + iPadAir = "iPad Air ", + iPadAir2 = "iPad Air 2", + iPadAir3 = "iPad Air 3", + iPadAir4 = "iPad Air 4", + iPadAir5 = "iPad Air 5", + iPad5 = "iPad 5", //iPad 2017 + iPad6 = "iPad 6", //iPad 2018 + iPad7 = "iPad 7", //iPad 2019 + iPad8 = "iPad 8", //iPad 2020 + iPad9 = "iPad 9", //iPad 2021 + iPad10 = "iPad 10", //iPad 2022 + + // iPad Mini + iPadMini = "iPad Mini", + iPadMini2 = "iPad Mini 2", + iPadMini3 = "iPad Mini 3", + iPadMini4 = "iPad Mini 4", + iPadMini5 = "iPad Mini 5", + iPadMini6 = "iPad Mini 6", + + // iPad Pro + iPadPro9_7 = "iPad Pro 9.7\"", + iPadPro10_5 = "iPad Pro 10.5\"", + iPadPro11 = "iPad Pro 11\"", + iPadPro2_11 = "iPad Pro 11\" 2nd gen", + iPadPro3_11 = "iPad Pro 11\" 3rd gen", + iPadPro12_9 = "iPad Pro 12.9\"", + iPadPro2_12_9 = "iPad Pro 2 12.9\"", + iPadPro3_12_9 = "iPad Pro 3 12.9\"", + iPadPro4_12_9 = "iPad Pro 4 12.9\"", + iPadPro5_12_9 = "iPad Pro 5 12.9\"", + + // iPhone + iPhone4 = "iPhone 4", + iPhone4S = "iPhone 4S", + iPhone5 = "iPhone 5", + iPhone5S = "iPhone 5S", + iPhone5C = "iPhone 5C", + iPhone6 = "iPhone 6", + iPhone6Plus = "iPhone 6 Plus", + iPhone6S = "iPhone 6S", + iPhone6SPlus = "iPhone 6S Plus", + iPhoneSE = "iPhone SE", + iPhone7 = "iPhone 7", + iPhone7Plus = "iPhone 7 Plus", + iPhone8 = "iPhone 8", + iPhone8Plus = "iPhone 8 Plus", + iPhoneX = "iPhone X", + iPhoneXS = "iPhone XS", + iPhoneXSMax = "iPhone XS Max", + iPhoneXR = "iPhone XR", + iPhone11 = "iPhone 11", + iPhone11Pro = "iPhone 11 Pro", + iPhone11ProMax = "iPhone 11 Pro Max", + iPhoneSE2 = "iPhone SE 2nd gen", + iPhone12Mini = "iPhone 12 Mini", + iPhone12 = "iPhone 12", + iPhone12Pro = "iPhone 12 Pro", + iPhone12ProMax = "iPhone 12 Pro Max", + iPhone13Mini = "iPhone 13 Mini", + iPhone13 = "iPhone 13", + iPhone13Pro = "iPhone 13 Pro", + iPhone13ProMax = "iPhone 13 Pro Max", + iPhoneSE3 = "iPhone SE 3nd gen", + iPhone14 = "iPhone 14", + iPhone14Plus = "iPhone 14 Plus", + iPhone14Pro = "iPhone 14 Pro", + iPhone14ProMax = "iPhone 14 Pro Max", + iPhone15 = "iPhone 15", + iPhone15Plus = "iPhone 15 Plus", + iPhone15Pro = "iPhone 15 Pro", + iPhone15ProMax = "iPhone 15 Pro Max", + + // Apple Watch + AppleWatch1 = "Apple Watch 1gen", + AppleWatchS1 = "Apple Watch Series 1", + AppleWatchS2 = "Apple Watch Series 2", + AppleWatchS3 = "Apple Watch Series 3", + AppleWatchS4 = "Apple Watch Series 4", + AppleWatchS5 = "Apple Watch Series 5", + AppleWatchSE = "Apple Watch Special Edition", + AppleWatchS6 = "Apple Watch Series 6", + AppleWatchS7 = "Apple Watch Series 7", + + // Apple TV + AppleTV1 = "Apple TV 1gen", + AppleTV2 = "Apple TV 2gen", + AppleTV3 = "Apple TV 3gen", + AppleTV4 = "Apple TV 4gen", + AppleTV_4K = "Apple TV 4K", + AppleTV2_4K = "Apple TV 4K 2gen", + + unrecognized = "?unrecognized?" +} + +public extension UIDevice { + + var type: DeviceModel { + var systemInfo = utsname() + uname(&systemInfo) + let modelCode = withUnsafePointer(to: &systemInfo.machine) { + $0.withMemoryRebound(to: CChar.self, capacity: 1) { + ptr in String.init(validatingUTF8: ptr) + } + } + + let modelMap : [String: DeviceModel] = [ + + // Simulator + "i386" : .simulator, + "x86_64" : .simulator, + + // iPod + "iPod1,1" : .iPod1, + "iPod2,1" : .iPod2, + "iPod3,1" : .iPod3, + "iPod4,1" : .iPod4, + "iPod5,1" : .iPod5, + "iPod7,1" : .iPod6, + "iPod9,1" : .iPod7, + + // iPad + "iPad2,1" : .iPad2, + "iPad2,2" : .iPad2, + "iPad2,3" : .iPad2, + "iPad2,4" : .iPad2, + "iPad3,1" : .iPad3, + "iPad3,2" : .iPad3, + "iPad3,3" : .iPad3, + "iPad3,4" : .iPad4, + "iPad3,5" : .iPad4, + "iPad3,6" : .iPad4, + "iPad6,11" : .iPad5, //iPad 2017 + "iPad6,12" : .iPad5, + "iPad7,5" : .iPad6, //iPad 2018 + "iPad7,6" : .iPad6, + "iPad7,11" : .iPad7, //iPad 2019 + "iPad7,12" : .iPad7, + "iPad11,6" : .iPad8, //iPad 2020 + "iPad11,7" : .iPad8, + "iPad12,1" : .iPad9, //iPad 2021 + "iPad12,2" : .iPad9, + "iPad13,18" : .iPad10, + "iPad13,19" : .iPad10, + + // iPad Mini + "iPad2,5" : .iPadMini, + "iPad2,6" : .iPadMini, + "iPad2,7" : .iPadMini, + "iPad4,4" : .iPadMini2, + "iPad4,5" : .iPadMini2, + "iPad4,6" : .iPadMini2, + "iPad4,7" : .iPadMini3, + "iPad4,8" : .iPadMini3, + "iPad4,9" : .iPadMini3, + "iPad5,1" : .iPadMini4, + "iPad5,2" : .iPadMini4, + "iPad11,1" : .iPadMini5, + "iPad11,2" : .iPadMini5, + "iPad14,1" : .iPadMini6, + "iPad14,2" : .iPadMini6, + + // iPad Pro + "iPad6,3" : .iPadPro9_7, + "iPad6,4" : .iPadPro9_7, + "iPad7,3" : .iPadPro10_5, + "iPad7,4" : .iPadPro10_5, + "iPad6,7" : .iPadPro12_9, + "iPad6,8" : .iPadPro12_9, + "iPad7,1" : .iPadPro2_12_9, + "iPad7,2" : .iPadPro2_12_9, + "iPad8,1" : .iPadPro11, + "iPad8,2" : .iPadPro11, + "iPad8,3" : .iPadPro11, + "iPad8,4" : .iPadPro11, + "iPad8,9" : .iPadPro2_11, + "iPad8,10" : .iPadPro2_11, + "iPad13,4" : .iPadPro3_11, + "iPad13,5" : .iPadPro3_11, + "iPad13,6" : .iPadPro3_11, + "iPad13,7" : .iPadPro3_11, + "iPad8,5" : .iPadPro3_12_9, + "iPad8,6" : .iPadPro3_12_9, + "iPad8,7" : .iPadPro3_12_9, + "iPad8,8" : .iPadPro3_12_9, + "iPad8,11" : .iPadPro4_12_9, + "iPad8,12" : .iPadPro4_12_9, + "iPad13,8" : .iPadPro5_12_9, + "iPad13,9" : .iPadPro5_12_9, + "iPad13,10" : .iPadPro5_12_9, + "iPad13,11" : .iPadPro5_12_9, + + // iPad Air + "iPad4,1" : .iPadAir, + "iPad4,2" : .iPadAir, + "iPad4,3" : .iPadAir, + "iPad5,3" : .iPadAir2, + "iPad5,4" : .iPadAir2, + "iPad11,3" : .iPadAir3, + "iPad11,4" : .iPadAir3, + "iPad13,1" : .iPadAir4, + "iPad13,2" : .iPadAir4, + "iPad13,16" : .iPadAir5, + "iPad13,17" : .iPadAir5, + + // iPhone + "iPhone3,1" : .iPhone4, + "iPhone3,2" : .iPhone4, + "iPhone3,3" : .iPhone4, + "iPhone4,1" : .iPhone4S, + "iPhone5,1" : .iPhone5, + "iPhone5,2" : .iPhone5, + "iPhone5,3" : .iPhone5C, + "iPhone5,4" : .iPhone5C, + "iPhone6,1" : .iPhone5S, + "iPhone6,2" : .iPhone5S, + "iPhone7,1" : .iPhone6Plus, + "iPhone7,2" : .iPhone6, + "iPhone8,1" : .iPhone6S, + "iPhone8,2" : .iPhone6SPlus, + "iPhone8,4" : .iPhoneSE, + "iPhone9,1" : .iPhone7, + "iPhone9,3" : .iPhone7, + "iPhone9,2" : .iPhone7Plus, + "iPhone9,4" : .iPhone7Plus, + "iPhone10,1" : .iPhone8, + "iPhone10,4" : .iPhone8, + "iPhone10,2" : .iPhone8Plus, + "iPhone10,5" : .iPhone8Plus, + "iPhone10,3" : .iPhoneX, + "iPhone10,6" : .iPhoneX, + "iPhone11,2" : .iPhoneXS, + "iPhone11,4" : .iPhoneXSMax, + "iPhone11,6" : .iPhoneXSMax, + "iPhone11,8" : .iPhoneXR, + "iPhone12,1" : .iPhone11, + "iPhone12,3" : .iPhone11Pro, + "iPhone12,5" : .iPhone11ProMax, + "iPhone12,8" : .iPhoneSE2, + "iPhone13,1" : .iPhone12Mini, + "iPhone13,2" : .iPhone12, + "iPhone13,3" : .iPhone12Pro, + "iPhone13,4" : .iPhone12ProMax, + "iPhone14,4" : .iPhone13Mini, + "iPhone14,5" : .iPhone13, + "iPhone14,2" : .iPhone13Pro, + "iPhone14,3" : .iPhone13ProMax, + "iPhone14,6" : .iPhoneSE3, + "iPhone14,7" : .iPhone14, + "iPhone14,8" : .iPhone14Plus, + "iPhone15,2" : .iPhone14Pro, + "iPhone15,3" : .iPhone14ProMax, + "iPhone15,4" : .iPhone15, + "iPhone15,5" : .iPhone15Plus, + "iPhone16,1" : .iPhone15Pro, + "iPhone16,2" : .iPhone15ProMax, + + // Apple Watch + "Watch1,1" : .AppleWatch1, + "Watch1,2" : .AppleWatch1, + "Watch2,6" : .AppleWatchS1, + "Watch2,7" : .AppleWatchS1, + "Watch2,3" : .AppleWatchS2, + "Watch2,4" : .AppleWatchS2, + "Watch3,1" : .AppleWatchS3, + "Watch3,2" : .AppleWatchS3, + "Watch3,3" : .AppleWatchS3, + "Watch3,4" : .AppleWatchS3, + "Watch4,1" : .AppleWatchS4, + "Watch4,2" : .AppleWatchS4, + "Watch4,3" : .AppleWatchS4, + "Watch4,4" : .AppleWatchS4, + "Watch5,1" : .AppleWatchS5, + "Watch5,2" : .AppleWatchS5, + "Watch5,3" : .AppleWatchS5, + "Watch5,4" : .AppleWatchS5, + "Watch5,9" : .AppleWatchSE, + "Watch5,10" : .AppleWatchSE, + "Watch5,11" : .AppleWatchSE, + "Watch5,12" : .AppleWatchSE, + "Watch6,1" : .AppleWatchS6, + "Watch6,2" : .AppleWatchS6, + "Watch6,3" : .AppleWatchS6, + "Watch6,4" : .AppleWatchS6, + "Watch6,6" : .AppleWatchS7, + "Watch6,7" : .AppleWatchS7, + "Watch6,8" : .AppleWatchS7, + "Watch6,9" : .AppleWatchS7, + + // Apple TV + "AppleTV1,1" : .AppleTV1, + "AppleTV2,1" : .AppleTV2, + "AppleTV3,1" : .AppleTV3, + "AppleTV3,2" : .AppleTV3, + "AppleTV5,3" : .AppleTV4, + "AppleTV6,2" : .AppleTV_4K, + "AppleTV11,1" : .AppleTV2_4K + ] + + guard let map = modelCode, let model = modelMap[map] else { return DeviceModel.unrecognized } + if model == .simulator { + if let simModelCode = ProcessInfo().environment["SIMULATOR_MODEL_IDENTIFIER"] { + if let simModel = modelMap[simModelCode] { + return simModel + } + } + } + return model + } +} diff --git a/Anyline Tire Demo/en.lproj/Localizable.strings b/Anyline Tire Demo/en.lproj/Localizable.strings index 610c41d..aca8d86 100644 --- a/Anyline Tire Demo/en.lproj/Localizable.strings +++ b/Anyline Tire Demo/en.lproj/Localizable.strings @@ -1,7 +1,13 @@ // Landing View -"landing.button.start" = "START"; +"landing.button.start" = "OPEN SCANNER"; "landing.button.settings" = "SETTINGS"; +"landing.button.tutorial" = "TUTORIAL"; "landing.label.welcome" = "Welcome to the Anyline\nTire Tread Scanner!"; +"landing.button.cancel" = "CANCEL"; +"landing.button.back" = "BACK"; +"landing.text.start" = "To start scanning, press the OPEN SCANNER button."; +"landing.text.tutorial" = "Watch the TUTORIAL to learn how to scan."; +"landing.text.info" = "For best scanning practices, please go to tiretreaddocu.anyline.com"; // Settings "settings.button.ok" = "OK"; @@ -9,14 +15,19 @@ "settings.button.test_setup" = "TEST SETUP"; "settings.button.cancel" = "CANCEL"; "settings.label.settings" = "Settings"; + "settings.label.use_imperial_system" = "Use Imperial System:"; -"settings.label.license_id" = "License ID:"; +"settings.label.license_id" = "License Key:"; "settings.label.accuraccy_speed" = "Accuracy / Speed: High Speed"; "settings.label.high_accuracy" = "High Accuracy"; -"settings.label.app_version" = "App Version:"; +"settings.label.app_sdk_version" = "App and SDK Version:"; "settings.label.device_name" = "Device Name:"; "settings.label.upload" = "Upload kbps"; "settings.label.support" = "If you need a license please reach out to"; +"settings.label.capture_speed" = "Capture Speed:"; +"settings.label.capture_speed_fast" = "Fast"; +"settings.label.capture_speed_slow" = "Slow"; +"settings.label.capture_speed.tip" = "(Tap to change)"; "success.title" = "Success!"; "success.description" = "The SDK has been initialized successfully!"; @@ -36,11 +47,24 @@ "feedback.button.submit" = "Submit"; "feedback.button.cancel" = "Cancel"; +// Recorder +"recorder.loading.message" = "Processing..."; +"recorder.msg.scanned_tire_id_1" = "This is the scanned Tire ID:"; +"recorder.msg.scanned_tire_id_2" = "What would you like to do?"; +"recorder.alert.title" = "Data Collection"; +"recorder.alert.message_success" = "Your data recording has been successfully submitted."; +"recorder.alert.message_failure" = "Your data recording was not sent successfully. Retry?"; +"recorder.alert.btn_failure" = "Retry"; +"recorder.button.send" = "Submit"; +"recorder.button.title.rescan" = "Rescan Barcode"; +"recorder.button.title.abort" = "Abort"; +"recorder.button.title.ok" = "OK"; + // Error "error.title" = "Error"; "error.description" = "Something went wrong!, Please try again later"; "error.invalid_license" = "Your license key is invalid or the server is currently unavailable."; -"error.key.description" = "Unable to authenticate! Please check if you entered the correct License ID."; +"error.key.description" = "Unable to authenticate! Please check if you entered the correct license key."; "error.scan.description" = "The scan was not successful, please try again.\n\nMake sure you do not move the device too fast and keep the right distance."; "error.scan.dot.description" = "Unable to set a focus point. Please adjust the distance and the focus on the tire and try again."; "error.pdf.description" = "The PDF data is unavailable! Please try again later."; @@ -60,3 +84,8 @@ // Details "details.button.download" = "DOWNLOAD"; + +// QR Code Reader +"qrcodereader.guide_text.tire_id" = "Scan the QR code of the tire you have just scanned."; +"qrcodereader.guide_text.license_key" = "Scan the QR code of your license key."; +"qrcodereader.message.invalid_license_key" = "No valid license found."; diff --git a/README.md b/README.md index 301310a..563390a 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Anyline Tire Tread SDK Showcase +# Anyline TireTread Example for iOS This application runs on the **Anyline Tire Tread SDK** that helps users easily and accurately assess the health of their vehicle's tires. The application code is included @@ -7,241 +7,18 @@ to help developers build the SDK into their own iOS applications. For more information and additional guidance, please check the [Tire Tread SDK iOS Documentation](https://documentation.anyline.com/tiretreadsdk-component/latest/ios/overview.html). -## Requirements -- Xcode version: 14.3 or later -- iOS version: 15.0 or later -- Dependency manager: Swift Package Manager (Cocoapods also supported) - -## How to Run The Showcase App - -1. Clone the repository -2. Open the `Anyline Tire Demo.xcodeproj` -3. Install dependencies -4. Build and run the project on a simulator or a device - -## Integrate the Anyline Tire Tread SDK into a Native iOS App - -The following guide will walk you through the process of integrating `AnylineTireTreadSdk` into a native iOS application. - -### 1. Import the `AnylineTireTreadSdk` Framework - -#### With Swift Package Manager - -- In Xcode, go to `File -> Swift Packages -> Add Package Dependency` and enter the Swift Package repository of -`AnylineTireTreadSdk`. - -After cloning the `AnylineTireTreadSdk` repository, you may import the framework into your native iOS -application code: - -``` -import AnylineTireTreadSdk -``` - -#### With CocoaPods - -- Add the following line to your `Podfile`: - - ``` - pod 'AnylineTireTreadSdk', '~>2' - ``` - -- Run `pod install`. - -### 2. Licensing - -In order to use the `AnylineTireTreadSdk`, a valid license key is required. Please contact presales@anyline.com to -obtain a license key if needed. - -Before making any calls with `AnylineTireTreadSdk`, initialize it with a license key as follows: - -``` -func tryInitializeSdk(context: UIViewController) { - do { - let licenseKey = "" - - try AnylineTireTreadSdk.companion.doInit(licenseKey: licenseKey, context: context) - - // Continue setup after successful initialization - } catch { - // Handle initialization error - } -} -``` - -In the code above, replace `` with your actual license key string. This function attempts to -initialize `AnylineTireTreadSdk` with the license key provided. If the SDK fails to initialize, you should handle -this error appropriately. - -It is recommended to store the license key securely (such as in the system keychain). - -### 3. Check Camera Permissions - -Before starting the scan process, it is crucial to check and request camera permissions, as they are required -in order for the SDK to function correctly. - -First, in your application's Info.plist file, add an `NSCameraUsageDescription` string value which explains -to your users why the app would be requiring camera permission access from them. - -Then, right before requesting for a scan view from the SDK, call the following: - -``` -func requestPermissionsAndProceed(context: UIViewController) { - let cameraAuthorizationStatus = AVCaptureDevice.authorizationStatus(for: .video) - - switch cameraAuthorizationStatus { - case .authorized: - // Continue with setup - case .notDetermined: - AVCaptureDevice.requestAccess(for: .video) { granted in - DispatchQueue.main.async { - if granted { - // Continue with setup - } else { - // Handle denied access - } - } - } - default: - // Handle other cases - } -} -``` - -### 4. Implement the `ScannerViewControllerHolder` Interface - -In order to set up the scan view correctly, make your UIViewController subclass implement the -`ScannerViewControllerHolder` interface. - -This interface gives your view controller two properties: - -1. `scannerViewController`: This property will hold an instance of `TireTreadScannerViewController`, which -takes over the scanning process. Once it becomes available, you should add its main view into your -UIViewController's view hierarchy. -2. `dismissViewController`: This is a function property that will handle the event of the scan view being -dismissed. Assign it with a block that removes the scan view from your UIViewController's view hierarchy. - -Here is an example of how to implement the ScannerViewControllerHolder interface: -``` -class YourViewController: UIViewController, ScannerViewControllerHolder { - - // Implement the properties defined by the ScannerViewControllerHolder interface - var scannerViewController: UIViewController? - var dismissViewController: (() -> Void)? - - // The rest of your class implementation -} -``` - -Remember to replace `YourViewController` with the actual name of your view controller. - -### 5. Set up the Scan View Controller - -Once the SDK has been initialized and the necessary permissions have been granted, you can setup the -scan view controller. - -``` -private func setupTireTreadScanView() { - let config = TireTreadScanViewConfig(measurementSystem: .metric, useDefaultUi: true, useDefaultHaptic: false) - TireTreadScanViewKt.TireTreadScanView(context: self, config: config) { error in - // Handle initialization error - } -} - -private func addScanViewControllerAsChild() { - guard let scannerViewController = scannerViewController else { - // Handle error - return - } - addChild(scannerViewController) - view.addSubview(scannerViewController.view) - scannerViewController.didMove(toParent: self) -} -``` - -Once added, the Tire Tread scan view will be displayed, and the user will be guided by on-screen -prompts to scan a tire correctly and submit the results online for analysis. - -### 6. Implementing the `TireTreadScanViewCallback` - -Your UIViewController should also implement `TireTreadScanViewCallback` to handle scan events. -For example: - -``` -extension YourViewController: TireTreadScanViewCallback { - func onUploadCompleted(uuid: String?) { - // Handle upload completion - } - - // Implement the rest of the callback methods... -} -``` - -`onUploadCompleted` is called when the scanner has taken enough tire image frames and have -successfully uploaded them to the cloud for processing. A UUID string associated with the tire -scan session is provided in the callback in order for you to make the subsequent request for -the scan results. - -### 7. Fetch the Scan Results - -Once the scan has been successfully completed, use the scan's UUID to retrieve the measurement -values from `AnylineTireTreadSdk`: - -``` -private func fetchTreadDepthResult() { - AnylineTireTreadSdk.companion.getTreadDepthReportResult( - measurementUuid: uuid, - onGetTreadDepthReportResultSucceed: { [weak self] response in - response.body { resultDTO, error in - guard let self = self else { return } - guard let status = resultDTO?.measurement.status else { - // handle error - return - } - - // obtain the scan results - self.handleTreadDepthResult(treadDepthResult: resultDTO?.result, status: status) - } - }, - onGetTreadDepthReportResultFailed: { [weak self] response, exception in - // handle error - } - ) -} -``` - -We recommend implementing this call with a polling logic (e.g. running every 3 seconds or more), -until the final results become available. - - -## Dependencies - -List of third-party libraries and frameworks used in the project: -- Alamofire - Networking library -- SnapKit - Auto Layout DSL -- KeychainSwift - Keychain helper library - -## Project Structure - -- DataStorage: Contains the modules responsible for managing the app's local data storage -- Domain: Responsible for handling network requests and other domain-specific logic -- Presentation: Contains the UIKit components -- Resources: Contains the supplementary assets and helper classes -- Fonts - ---- - -## Get Help (Support) +## Get Help (Support) ## We don't actively monitor the GitHub Issues, please raise a support request using the [Anyline Helpdesk](https://support.anyline.com/). When raising a support request based on this GitHub Issue, please fill out and include the following information: - ``` -Support request concerning Anyline GitHub Repository: anyline-tiretread-showcase-app-ios +Support request concerning Anyline GitHub Repository: anyline-tiretread-ios-example ``` Thank you! -## License + +## License ## See LICENSE file. \ No newline at end of file