diff --git a/.github/workflows/ios-tests.yml b/.github/workflows/ios-tests.yml index 9e76ab8..4dc0bce 100644 --- a/.github/workflows/ios-tests.yml +++ b/.github/workflows/ios-tests.yml @@ -1,40 +1,46 @@ -name: iOS UI Tests +name: iOS CI for SPAR on: push: - branches: - - SPAR_iOS_App + branches: [ SPAR_iOS_App, main ] pull_request: + branches: [ SPAR_iOS_App, main ] jobs: - ios-tests: + build-and-test: runs-on: macos-14 - defaults: - run: - working-directory: SPAR - steps: - - name: Checkout project + - name: Checkout code uses: actions/checkout@v4 - name: Set up Xcode - run: sudo xcode-select -s /Applications/Xcode_15.0.app + run: sudo xcode-select -s /Applications/Xcode_15.3.app - - name: Install Swift Packages - run: xcodebuild -resolvePackageDependencies + - name: Clean Derived Data & Simulators + run: | + rm -rf ~/Library/Developer/Xcode/DerivedData + xcrun simctl shutdown all || true + xcrun simctl erase all || true - - name: Run UI Tests + - name: Run Tests with xcodebuild + env: + NSUnbufferedIO: YES run: | - mkdir -p TestResults - xcodebuild test -project SPAR.xcodeproj \ + xcodebuild \ + -project SPAR/SPAR.xcodeproj \ -scheme SPAR \ - -destination 'platform=iOS Simulator,name=iPhone 15' \ - -resultBundlePath TestResults/SPARTestResults.xcresult + -destination 'platform=iOS Simulator,name=iPhone 15,OS=17.0' \ + -derivedDataPath DerivedData \ + -enableCodeCoverage YES \ + clean test \ + | xcpretty --report html --report junit - name: Upload Test Results - if: always() # run even if tests fail + if: always() uses: actions/upload-artifact@v4 with: - name: SPAR-xcresult - path: SPAR/TestResults/SPARTestResults.xcresult + name: SPARTestResults + path: | + DerivedData/Logs/Test/*.xcresult + test_output/ diff --git a/README.md b/README.md index c7b5597..a18e77e 100644 --- a/README.md +++ b/README.md @@ -1 +1,158 @@ -# MSCS_Spring2025_Capping \ No newline at end of file +# πŸ–₯️ System Processes and Analysis Reporting (S.P.A.R) + +**S.P.A.R** is a cross-platform system monitoring application for Ubuntu Linux that provides real-time insights into CPU, memory, disk usage, and running processes. It supports both desktop and mobile platforms, enabling users to track system health anytime, anywhere. + +--- + +## πŸ“¦ Components + +### πŸ”Ή Desktop App (Electron + React) +- View real-time system metrics +- Responsive UI with Tailwind CSS +- Builds for `.exe`, `.deb`, `.tar.gz` + +### πŸ”Ή iOS App (SwiftUI) +- Monitors system metrics +- Supports accessibility: VoiceOver, Dynamic Type +- Face ID integration (Keychain) +- Interaction logging for future analytics + +### πŸ”Ή Backend Services +- RESTful API with **Spring Boot** +- Metrics collected using **Node.js** (`systeminformation`, `ps-list`) +- MySQL used for data storage +- API exposes CPU, memory, disk, battery, processes + +--- + +## 🧰 Tech Stack + +| Layer | Technology | +|--------------|---------------------------------| +| Desktop UI | Electron, React, Tailwind CSS | +| Mobile App | SwiftUI, Combine, Swift 5 | +| Backend API | Spring Boot (Java), Node.js | +| Data Storage | MySQL | +| Metrics Lib | `systeminformation`, `ps-list` | +| Tools | Xcode 15, IntelliJ, VS Code | + +--- + +## πŸš€ Getting Started + +### πŸ“ Clone Repo +```bash +git clone https://github.com/MSCS-Capping/MSCS_Spring2025_Capping.git +cd MSCS_Spring2025_Capping +``` + +### πŸ–₯️ Desktop App +```bash +cd desktop-app +npm install +npm start # Dev mode +npm run package # Build installer +``` + +### πŸ“± iOS App +1. cd SPAR_iOS +2. cd SPAR +1. Open `spar.xcodeproj` in Xcode 15 +2. Run on simulator or device +3. View process metrics and test accessibility + logging features + +### πŸ”§ Backend API +```bash +cd backend-api +./mvnw spring-boot:run +``` + +### 🟑 Node Metrics Collector +```bash +cd node-metrics +npm install +node collector.js +``` + +Ensure Spring Boot API is running before starting the Electron app. + +--- + +## πŸ”„ Actions & Deployment + +### βœ… GitHub Actions + +- iOS app uses GitHub Actions to ensure **all unit and UI tests pass before any merge** into `main` +- CI workflow runs on macOS runners with Xcode configured +- Prevents broken code from being merged into production + +```yaml +# .github/workflows/ios-tests.yml +name: iOS Tests +on: [pull_request] +jobs: + test: + runs-on: macos-latest + steps: + - uses: actions/checkout@v3 + - name: Set up Xcode + run: xcodebuild -project iOSApp.xcodeproj -scheme iOSApp -destination 'platform=iOS Simulator,name=iPhone 14' test +``` + +### πŸš€ Render Deployment + +- Spring Boot backend is deployed using **Render** +- MySQL database also hosted via Render's managed database services +- Auto-deploy enabled from `main` branch +- Environment variables (DB credentials, API keys) are stored securely in Render's dashboard + +--- + +## πŸ” Security & Testing + +- βœ… Unit & Integration Tests for backend and frontend +- β™Ώ Accessibility tested with VoiceOver and Dynamic Type +- πŸ”’ Keychain integration for login credentials (iOS) +- πŸ”„ MySQL data backups and error recovery mechanisms + +--- + +## 🌱 Future Enhancements + +- Export reports (CSV, PDF) +- Android version +- Push Notification +- Chatbot + +--- + +## πŸ‘₯ Contributors + +- **Abhijeet Cherungottil** + πŸ“§ [Abhijeet.Cherungottil1@marist.edu](mailto:Abhijeet.Cherungottil1@marist.edu) + +- **Sumanth Kumar Katapally** + πŸ“§ [SumanthKumar.Katapally1@marist.edu](mailto:SumanthKumar.Katapally1@marist.edu) + +- **Arjun Suresh (AJ)** + πŸ“§ [Arjun.Suresh1@marist.edu](mailto:Arjun.Suresh1@marist.edu) + +--- + +## πŸŽ₯ Demo Video + +πŸ“½οΈ Watch it here: Coming Soon +[]() + +--- + +## πŸ“š References + +- [Electron Documentation](https://www.electronjs.org/docs/latest) +- [Spring Boot](https://spring.io/projects/spring-boot) +- [System Information (NPM)](https://www.npmjs.com/package/systeminformation) +- [SwiftUI Accessibility Guide](https://developer.apple.com/documentation/swiftui/accessibility) +- [Tailwind CSS](https://tailwindcss.com/) +- [Render Deployment Docs](https://render.com/docs/deploy-a-java-spring-boot-app) + +--- diff --git a/SPAR_iOS/SPAR/SPAR.xcodeproj/project.pbxproj b/SPAR_iOS/SPAR/SPAR.xcodeproj/project.pbxproj new file mode 100644 index 0000000..e7dd99e --- /dev/null +++ b/SPAR_iOS/SPAR/SPAR.xcodeproj/project.pbxproj @@ -0,0 +1,798 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 56; + objects = { + +/* Begin PBXBuildFile section */ + 3D07B5382D85C3BB0085B32C /* ChartData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D07B5372D85C3BB0085B32C /* ChartData.swift */; }; + 3D07B53A2D85C3EC0085B32C /* ChartDataContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D07B5392D85C3EC0085B32C /* ChartDataContainer.swift */; }; + 3D07B53C2D85C4350085B32C /* HalfDonutChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D07B53B2D85C4350085B32C /* HalfDonutChart.swift */; }; + 3D07B53E2D8887F30085B32C /* ContentSizeCategory+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D07B53D2D8887F30085B32C /* ContentSizeCategory+Extension.swift */; }; + 3D07B5402D88881D0085B32C /* Onboarding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D07B53F2D88881D0085B32C /* Onboarding.swift */; }; + 3D505B4D2DB9C1BE00510486 /* DeviceOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D505B4C2DB9C1BE00510486 /* DeviceOptions.swift */; }; + 3D505B4F2DB9C6AD00510486 /* NavigationButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D505B4E2DB9C6AD00510486 /* NavigationButton.swift */; }; + 3D505B512DB9D76200510486 /* BatteryDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D505B502DB9D76200510486 /* BatteryDetailView.swift */; }; + 3D505B532DB9DAA500510486 /* MemoryUsageDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D505B522DB9DAA500510486 /* MemoryUsageDetailView.swift */; }; + 3D505B552DB9DD5500510486 /* ProcessDetailPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D505B542DB9DD5500510486 /* ProcessDetailPage.swift */; }; + 3D505B572DB9DF3F00510486 /* BatteryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D505B562DB9DF3F00510486 /* BatteryViewModel.swift */; }; + 3D505B592DB9E14700510486 /* MemoryUsageViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D505B582DB9E14700510486 /* MemoryUsageViewModel.swift */; }; + 3D505B5B2DB9E3F700510486 /* ProcessViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D505B5A2DB9E3F700510486 /* ProcessViewModel.swift */; }; + 3D505B5D2DBA657B00510486 /* AppSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D505B5C2DBA657B00510486 /* AppSettings.swift */; }; + 3D505B5F2DBA94B200510486 /* LoginModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D505B5E2DBA94B200510486 /* LoginModel.swift */; }; + 3D505B602DBA9EE900510486 /* LoginModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D505B5E2DBA94B200510486 /* LoginModel.swift */; }; + 3D505B612DBA9EF200510486 /* Onboarding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D07B53F2D88881D0085B32C /* Onboarding.swift */; }; + 3D505B632DBAAE2C00510486 /* DiskUsage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D505B622DBAAE2C00510486 /* DiskUsage.swift */; }; + 3D505B652DBAAE4D00510486 /* DiskUsageViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D505B642DBAAE4D00510486 /* DiskUsageViewModel.swift */; }; + 3D505B672DBAAFFE00510486 /* DiskUsageDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D505B662DBAAFFE00510486 /* DiskUsageDetailView.swift */; }; + 3D505B692DBAB31D00510486 /* CPUUsage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D505B682DBAB31D00510486 /* CPUUsage.swift */; }; + 3D505B6B2DBAC5EC00510486 /* CpuUsageViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D505B6A2DBAC5EC00510486 /* CpuUsageViewModel.swift */; }; + 3D505B6D2DBAC6EA00510486 /* CpuUsageDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D505B6C2DBAC6EA00510486 /* CpuUsageDetailView.swift */; }; + 3D505B6F2DBACDCA00510486 /* DiskIOModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D505B6E2DBACDCA00510486 /* DiskIOModel.swift */; }; + 3D505B712DBACDE500510486 /* DiskIOViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D505B702DBACDE500510486 /* DiskIOViewModel.swift */; }; + 3D505B732DBACE3800510486 /* DiskIODetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D505B722DBACE3800510486 /* DiskIODetailView.swift */; }; + 3D505B752DBACEBF00510486 /* String+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D505B742DBACEBF00510486 /* String+Extension.swift */; }; + 3D505B762DBACFB100510486 /* DiskUsage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D505B622DBAAE2C00510486 /* DiskUsage.swift */; }; + 3D505B772DBACFB500510486 /* CPUUsage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D505B682DBAB31D00510486 /* CPUUsage.swift */; }; + 3D505B792DBB170700510486 /* InfoRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D505B782DBB170700510486 /* InfoRow.swift */; }; + 3D5994F32DB2B79400E9215B /* LoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D5994F22DB2B79400E9215B /* LoginView.swift */; }; + 3D5994F52DB2BA1200E9215B /* BackgroundAnimationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D5994F42DB2BA1200E9215B /* BackgroundAnimationView.swift */; }; + 3D5994FA2DB307B400E9215B /* HomeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D5994F92DB307B400E9215B /* HomeViewModel.swift */; }; + 3D5994FC2DB30B0100E9215B /* LoginViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D5994FB2DB30B0100E9215B /* LoginViewModel.swift */; }; + 3D5994FF2DB3FFBE00E9215B /* NetworkService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D5994FE2DB3FFBE00E9215B /* NetworkService.swift */; }; + 3D5995012DB410BD00E9215B /* DeviceSpecification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D5995002DB410BD00E9215B /* DeviceSpecification.swift */; }; + 3D5995032DB410DB00E9215B /* ProcessStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D5995022DB410DB00E9215B /* ProcessStatus.swift */; }; + 3D5995052DB4110700E9215B /* BatteryInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D5995042DB4110700E9215B /* BatteryInfo.swift */; }; + 3D5995072DB4112100E9215B /* MemoryUsage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D5995062DB4112100E9215B /* MemoryUsage.swift */; }; + 3D5995092DB4114200E9215B /* NetworkManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D5995082DB4114200E9215B /* NetworkManager.swift */; }; + 3D59950B2DB4137E00E9215B /* MockNetworkService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D59950A2DB4137E00E9215B /* MockNetworkService.swift */; }; + 3D59950C2DB4138E00E9215B /* NetworkService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D5994FE2DB3FFBE00E9215B /* NetworkService.swift */; }; + 3D59950E2DB413C100E9215B /* MockData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D59950D2DB413C100E9215B /* MockData.swift */; }; + 3D5995102DB413E300E9215B /* NetworkManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D59950F2DB413E300E9215B /* NetworkManagerTests.swift */; }; + 3D5995112DB4162400E9215B /* NetworkManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D5995082DB4114200E9215B /* NetworkManager.swift */; }; + 3D5995122DB4167100E9215B /* DeviceSpecification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D5995002DB410BD00E9215B /* DeviceSpecification.swift */; }; + 3D5995132DB4167400E9215B /* ProcessStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D5995022DB410DB00E9215B /* ProcessStatus.swift */; }; + 3D5995142DB4167700E9215B /* BatteryInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D5995042DB4110700E9215B /* BatteryInfo.swift */; }; + 3D5995152DB4167A00E9215B /* MemoryUsage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D5995062DB4112100E9215B /* MemoryUsage.swift */; }; + 3D5995162DB41E0D00E9215B /* MockNetworkService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D59950A2DB4137E00E9215B /* MockNetworkService.swift */; }; + 3D5995172DB41E1500E9215B /* MockData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D59950D2DB413C100E9215B /* MockData.swift */; }; + 3D63FB0A2DC4F4F2006F53DE /* LoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D63FB092DC4F4F2006F53DE /* LoadingView.swift */; }; + 3D78BB2A2DB550E5000E5C2F /* LoginViewUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D78BB292DB550E5000E5C2F /* LoginViewUITests.swift */; }; + 3D8423A72DB14F86009CF847 /* SPARTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D8423A62DB14F86009CF847 /* SPARTests.swift */; }; + 3D8423B02DB15044009CF847 /* LaunchTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D8423AF2DB15044009CF847 /* LaunchTest.swift */; }; + 3D8423B12DB15061009CF847 /* Constant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DCE45882D68C19200FACBB8 /* Constant.swift */; }; + 3D8423B32DB15082009CF847 /* OnboardingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D8423B22DB15082009CF847 /* OnboardingTests.swift */; }; + 3D98C7F22D4FE04500462EF4 /* SPARApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D98C7F12D4FE04500462EF4 /* SPARApp.swift */; }; + 3D98C7F42D4FE04500462EF4 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D98C7F32D4FE04500462EF4 /* ContentView.swift */; }; + 3D98C7F62D4FE04700462EF4 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3D98C7F52D4FE04700462EF4 /* Assets.xcassets */; }; + 3D98C7F92D4FE04700462EF4 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3D98C7F82D4FE04700462EF4 /* Preview Assets.xcassets */; }; + 3DA1AA442DBDB01300678160 /* DiskIOModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D505B6E2DBACDCA00510486 /* DiskIOModel.swift */; }; + 3DA1AA452DBDB08700678160 /* AppSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D505B5C2DBA657B00510486 /* AppSettings.swift */; }; + 3DA1AA472DBF034200678160 /* KeychainHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DA1AA462DBF034200678160 /* KeychainHelper.swift */; }; + 3DCE45892D68C19200FACBB8 /* Constant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DCE45882D68C19200FACBB8 /* Constant.swift */; }; + 3DCE458B2D68C19F00FACBB8 /* SplashScreenView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DCE458A2D68C19F00FACBB8 /* SplashScreenView.swift */; }; + 3DCE458D2D68C1A900FACBB8 /* Logger+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DCE458C2D68C1A900FACBB8 /* Logger+Extension.swift */; }; + 3DCE458F2D68C1B200FACBB8 /* OnboardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DCE458E2D68C1B200FACBB8 /* OnboardingView.swift */; }; + 3DCE45912D68C7E700FACBB8 /* View+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DCE45902D68C7E700FACBB8 /* View+Extension.swift */; }; + 3DF9BA512D67762C00D0CC62 /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DF9BA502D67762C00D0CC62 /* HomeView.swift */; }; + 3DF9BA532D67767D00D0CC62 /* OnBoardingScreenView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DF9BA522D67767D00D0CC62 /* OnBoardingScreenView.swift */; }; + 3DF9BA592D678A9300D0CC62 /* (null) in Sources */ = {isa = PBXBuildFile; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 3D8423AA2DB14F86009CF847 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 3D98C7E62D4FE04500462EF4 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 3D98C7ED2D4FE04500462EF4; + remoteInfo = SPAR; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 3D07B5372D85C3BB0085B32C /* ChartData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChartData.swift; sourceTree = ""; }; + 3D07B5392D85C3EC0085B32C /* ChartDataContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChartDataContainer.swift; sourceTree = ""; }; + 3D07B53B2D85C4350085B32C /* HalfDonutChart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HalfDonutChart.swift; sourceTree = ""; }; + 3D07B53D2D8887F30085B32C /* ContentSizeCategory+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ContentSizeCategory+Extension.swift"; sourceTree = ""; }; + 3D07B53F2D88881D0085B32C /* Onboarding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Onboarding.swift; sourceTree = ""; }; + 3D505B4C2DB9C1BE00510486 /* DeviceOptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceOptions.swift; sourceTree = ""; }; + 3D505B4E2DB9C6AD00510486 /* NavigationButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationButton.swift; sourceTree = ""; }; + 3D505B502DB9D76200510486 /* BatteryDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatteryDetailView.swift; sourceTree = ""; }; + 3D505B522DB9DAA500510486 /* MemoryUsageDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemoryUsageDetailView.swift; sourceTree = ""; }; + 3D505B542DB9DD5500510486 /* ProcessDetailPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProcessDetailPage.swift; sourceTree = ""; }; + 3D505B562DB9DF3F00510486 /* BatteryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatteryViewModel.swift; sourceTree = ""; }; + 3D505B582DB9E14700510486 /* MemoryUsageViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemoryUsageViewModel.swift; sourceTree = ""; }; + 3D505B5A2DB9E3F700510486 /* ProcessViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProcessViewModel.swift; sourceTree = ""; }; + 3D505B5C2DBA657B00510486 /* AppSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSettings.swift; sourceTree = ""; }; + 3D505B5E2DBA94B200510486 /* LoginModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginModel.swift; sourceTree = ""; }; + 3D505B622DBAAE2C00510486 /* DiskUsage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiskUsage.swift; sourceTree = ""; }; + 3D505B642DBAAE4D00510486 /* DiskUsageViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiskUsageViewModel.swift; sourceTree = ""; }; + 3D505B662DBAAFFE00510486 /* DiskUsageDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiskUsageDetailView.swift; sourceTree = ""; }; + 3D505B682DBAB31D00510486 /* CPUUsage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CPUUsage.swift; sourceTree = ""; }; + 3D505B6A2DBAC5EC00510486 /* CpuUsageViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CpuUsageViewModel.swift; sourceTree = ""; }; + 3D505B6C2DBAC6EA00510486 /* CpuUsageDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CpuUsageDetailView.swift; sourceTree = ""; }; + 3D505B6E2DBACDCA00510486 /* DiskIOModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiskIOModel.swift; sourceTree = ""; }; + 3D505B702DBACDE500510486 /* DiskIOViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiskIOViewModel.swift; sourceTree = ""; }; + 3D505B722DBACE3800510486 /* DiskIODetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiskIODetailView.swift; sourceTree = ""; }; + 3D505B742DBACEBF00510486 /* String+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Extension.swift"; sourceTree = ""; }; + 3D505B782DBB170700510486 /* InfoRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoRow.swift; sourceTree = ""; }; + 3D5994F22DB2B79400E9215B /* LoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginView.swift; sourceTree = ""; }; + 3D5994F42DB2BA1200E9215B /* BackgroundAnimationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundAnimationView.swift; sourceTree = ""; }; + 3D5994F92DB307B400E9215B /* HomeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeViewModel.swift; sourceTree = ""; }; + 3D5994FB2DB30B0100E9215B /* LoginViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginViewModel.swift; sourceTree = ""; }; + 3D5994FE2DB3FFBE00E9215B /* NetworkService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkService.swift; sourceTree = ""; }; + 3D5995002DB410BD00E9215B /* DeviceSpecification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceSpecification.swift; sourceTree = ""; }; + 3D5995022DB410DB00E9215B /* ProcessStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProcessStatus.swift; sourceTree = ""; }; + 3D5995042DB4110700E9215B /* BatteryInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatteryInfo.swift; sourceTree = ""; }; + 3D5995062DB4112100E9215B /* MemoryUsage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemoryUsage.swift; sourceTree = ""; }; + 3D5995082DB4114200E9215B /* NetworkManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkManager.swift; sourceTree = ""; }; + 3D59950A2DB4137E00E9215B /* MockNetworkService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockNetworkService.swift; sourceTree = ""; }; + 3D59950D2DB413C100E9215B /* MockData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockData.swift; sourceTree = ""; }; + 3D59950F2DB413E300E9215B /* NetworkManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkManagerTests.swift; sourceTree = ""; }; + 3D63FB092DC4F4F2006F53DE /* LoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingView.swift; sourceTree = ""; }; + 3D78BB292DB550E5000E5C2F /* LoginViewUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginViewUITests.swift; sourceTree = ""; }; + 3D8423A42DB14F86009CF847 /* SPARTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SPARTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 3D8423A62DB14F86009CF847 /* SPARTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SPARTests.swift; sourceTree = ""; }; + 3D8423AF2DB15044009CF847 /* LaunchTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchTest.swift; sourceTree = ""; }; + 3D8423B22DB15082009CF847 /* OnboardingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingTests.swift; sourceTree = ""; }; + 3D98C7EE2D4FE04500462EF4 /* SPAR.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SPAR.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 3D98C7F12D4FE04500462EF4 /* SPARApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SPARApp.swift; sourceTree = ""; }; + 3D98C7F32D4FE04500462EF4 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; + 3D98C7F52D4FE04700462EF4 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 3D98C7F82D4FE04700462EF4 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; + 3DA1AA462DBF034200678160 /* KeychainHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainHelper.swift; sourceTree = ""; }; + 3DCE45882D68C19200FACBB8 /* Constant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Constant.swift; sourceTree = ""; }; + 3DCE458A2D68C19F00FACBB8 /* SplashScreenView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SplashScreenView.swift; sourceTree = ""; }; + 3DCE458C2D68C1A900FACBB8 /* Logger+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Logger+Extension.swift"; sourceTree = ""; }; + 3DCE458E2D68C1B200FACBB8 /* OnboardingView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OnboardingView.swift; sourceTree = ""; }; + 3DCE45902D68C7E700FACBB8 /* View+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+Extension.swift"; sourceTree = ""; }; + 3DF9BA502D67762C00D0CC62 /* HomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = ""; }; + 3DF9BA522D67767D00D0CC62 /* OnBoardingScreenView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnBoardingScreenView.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 3D8423A12DB14F86009CF847 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 3D98C7EB2D4FE04500462EF4 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 3D07B5312D851C1D0085B32C /* Extensions */ = { + isa = PBXGroup; + children = ( + 3D07B53D2D8887F30085B32C /* ContentSizeCategory+Extension.swift */, + 3DCE458C2D68C1A900FACBB8 /* Logger+Extension.swift */, + 3D505B742DBACEBF00510486 /* String+Extension.swift */, + 3DCE45902D68C7E700FACBB8 /* View+Extension.swift */, + ); + path = Extensions; + sourceTree = ""; + }; + 3D07B5322D851C490085B32C /* Onboarding */ = { + isa = PBXGroup; + children = ( + 3DF9BA522D67767D00D0CC62 /* OnBoardingScreenView.swift */, + 3DCE458E2D68C1B200FACBB8 /* OnboardingView.swift */, + ); + path = Onboarding; + sourceTree = ""; + }; + 3D07B5332D851C640085B32C /* Utilities */ = { + isa = PBXGroup; + children = ( + 3D505B5C2DBA657B00510486 /* AppSettings.swift */, + 3DCE45882D68C19200FACBB8 /* Constant.swift */, + 3D505B782DBB170700510486 /* InfoRow.swift */, + 3D63FB092DC4F4F2006F53DE /* LoadingView.swift */, + 3DA1AA462DBF034200678160 /* KeychainHelper.swift */, + 3D505B4E2DB9C6AD00510486 /* NavigationButton.swift */, + ); + path = Utilities; + sourceTree = ""; + }; + 3D07B5342D85C3740085B32C /* ChartUI */ = { + isa = PBXGroup; + children = ( + 3D07B53B2D85C4350085B32C /* HalfDonutChart.swift */, + ); + path = ChartUI; + sourceTree = ""; + }; + 3D07B5352D85C3860085B32C /* ViewModel */ = { + isa = PBXGroup; + children = ( + 3D505B562DB9DF3F00510486 /* BatteryViewModel.swift */, + 3D07B5392D85C3EC0085B32C /* ChartDataContainer.swift */, + 3D505B6A2DBAC5EC00510486 /* CpuUsageViewModel.swift */, + 3D505B702DBACDE500510486 /* DiskIOViewModel.swift */, + 3D505B642DBAAE4D00510486 /* DiskUsageViewModel.swift */, + 3D5994F92DB307B400E9215B /* HomeViewModel.swift */, + 3D5994FB2DB30B0100E9215B /* LoginViewModel.swift */, + 3D505B582DB9E14700510486 /* MemoryUsageViewModel.swift */, + 3D505B5A2DB9E3F700510486 /* ProcessViewModel.swift */, + ); + path = ViewModel; + sourceTree = ""; + }; + 3D07B5362D85C3910085B32C /* Data */ = { + isa = PBXGroup; + children = ( + 3D5995042DB4110700E9215B /* BatteryInfo.swift */, + 3D07B5372D85C3BB0085B32C /* ChartData.swift */, + 3D505B682DBAB31D00510486 /* CPUUsage.swift */, + 3D5995002DB410BD00E9215B /* DeviceSpecification.swift */, + 3D505B6E2DBACDCA00510486 /* DiskIOModel.swift */, + 3D505B622DBAAE2C00510486 /* DiskUsage.swift */, + 3D505B5E2DBA94B200510486 /* LoginModel.swift */, + 3D5995062DB4112100E9215B /* MemoryUsage.swift */, + 3D59950D2DB413C100E9215B /* MockData.swift */, + 3D07B53F2D88881D0085B32C /* Onboarding.swift */, + 3D5995022DB410DB00E9215B /* ProcessStatus.swift */, + ); + path = Data; + sourceTree = ""; + }; + 3D505B4B2DB9C19000510486 /* Device */ = { + isa = PBXGroup; + children = ( + 3D505B502DB9D76200510486 /* BatteryDetailView.swift */, + 3D505B6C2DBAC6EA00510486 /* CpuUsageDetailView.swift */, + 3D505B4C2DB9C1BE00510486 /* DeviceOptions.swift */, + 3D505B722DBACE3800510486 /* DiskIODetailView.swift */, + 3D505B662DBAAFFE00510486 /* DiskUsageDetailView.swift */, + 3D505B522DB9DAA500510486 /* MemoryUsageDetailView.swift */, + 3D505B542DB9DD5500510486 /* ProcessDetailPage.swift */, + ); + path = Device; + sourceTree = ""; + }; + 3D5994F12DB2ACB300E9215B /* Login */ = { + isa = PBXGroup; + children = ( + 3D5994F42DB2BA1200E9215B /* BackgroundAnimationView.swift */, + 3D5994F22DB2B79400E9215B /* LoginView.swift */, + ); + path = Login; + sourceTree = ""; + }; + 3D5994FD2DB3FF0700E9215B /* NetworkService */ = { + isa = PBXGroup; + children = ( + 3D59950A2DB4137E00E9215B /* MockNetworkService.swift */, + 3D5995082DB4114200E9215B /* NetworkManager.swift */, + 3D5994FE2DB3FFBE00E9215B /* NetworkService.swift */, + ); + path = NetworkService; + sourceTree = ""; + }; + 3D63FB062DC1B4FA006F53DE /* View */ = { + isa = PBXGroup; + children = ( + 3D07B5342D85C3740085B32C /* ChartUI */, + 3D505B4B2DB9C19000510486 /* Device */, + 3DF9BA502D67762C00D0CC62 /* HomeView.swift */, + 3D5994F12DB2ACB300E9215B /* Login */, + 3D07B5322D851C490085B32C /* Onboarding */, + 3DCE458A2D68C19F00FACBB8 /* SplashScreenView.swift */, + ); + path = View; + sourceTree = ""; + }; + 3D8423A52DB14F86009CF847 /* SPARTests */ = { + isa = PBXGroup; + children = ( + 3D8423AF2DB15044009CF847 /* LaunchTest.swift */, + 3D78BB292DB550E5000E5C2F /* LoginViewUITests.swift */, + 3D59950F2DB413E300E9215B /* NetworkManagerTests.swift */, + 3D8423B22DB15082009CF847 /* OnboardingTests.swift */, + 3D8423A62DB14F86009CF847 /* SPARTests.swift */, + ); + path = SPARTests; + sourceTree = ""; + }; + 3D98C7E52D4FE04500462EF4 = { + isa = PBXGroup; + children = ( + 3D98C7F02D4FE04500462EF4 /* SPAR */, + 3D8423A52DB14F86009CF847 /* SPARTests */, + 3D98C7EF2D4FE04500462EF4 /* Products */, + ); + sourceTree = ""; + }; + 3D98C7EF2D4FE04500462EF4 /* Products */ = { + isa = PBXGroup; + children = ( + 3D98C7EE2D4FE04500462EF4 /* SPAR.app */, + 3D8423A42DB14F86009CF847 /* SPARTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 3D98C7F02D4FE04500462EF4 /* SPAR */ = { + isa = PBXGroup; + children = ( + 3D98C7F52D4FE04700462EF4 /* Assets.xcassets */, + 3D98C7F32D4FE04500462EF4 /* ContentView.swift */, + 3D07B5362D85C3910085B32C /* Data */, + 3D07B5312D851C1D0085B32C /* Extensions */, + 3D5994FD2DB3FF0700E9215B /* NetworkService */, + 3D98C7F72D4FE04700462EF4 /* Preview Content */, + 3D98C7F12D4FE04500462EF4 /* SPARApp.swift */, + 3D07B5332D851C640085B32C /* Utilities */, + 3D63FB062DC1B4FA006F53DE /* View */, + 3D07B5352D85C3860085B32C /* ViewModel */, + ); + path = SPAR; + sourceTree = ""; + }; + 3D98C7F72D4FE04700462EF4 /* Preview Content */ = { + isa = PBXGroup; + children = ( + 3D98C7F82D4FE04700462EF4 /* Preview Assets.xcassets */, + ); + path = "Preview Content"; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 3D8423A32DB14F86009CF847 /* SPARTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 3D8423AC2DB14F86009CF847 /* Build configuration list for PBXNativeTarget "SPARTests" */; + buildPhases = ( + 3D8423A02DB14F86009CF847 /* Sources */, + 3D8423A12DB14F86009CF847 /* Frameworks */, + 3D8423A22DB14F86009CF847 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 3D8423AB2DB14F86009CF847 /* PBXTargetDependency */, + ); + name = SPARTests; + productName = SPARTests; + productReference = 3D8423A42DB14F86009CF847 /* SPARTests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; + 3D98C7ED2D4FE04500462EF4 /* SPAR */ = { + isa = PBXNativeTarget; + buildConfigurationList = 3D98C7FC2D4FE04700462EF4 /* Build configuration list for PBXNativeTarget "SPAR" */; + buildPhases = ( + 3D98C7EA2D4FE04500462EF4 /* Sources */, + 3D98C7EB2D4FE04500462EF4 /* Frameworks */, + 3D98C7EC2D4FE04500462EF4 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = SPAR; + productName = SPAR; + productReference = 3D98C7EE2D4FE04500462EF4 /* SPAR.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 3D98C7E62D4FE04500462EF4 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1540; + LastUpgradeCheck = 1540; + TargetAttributes = { + 3D8423A32DB14F86009CF847 = { + CreatedOnToolsVersion = 15.4; + TestTargetID = 3D98C7ED2D4FE04500462EF4; + }; + 3D98C7ED2D4FE04500462EF4 = { + CreatedOnToolsVersion = 15.4; + }; + }; + }; + buildConfigurationList = 3D98C7E92D4FE04500462EF4 /* Build configuration list for PBXProject "SPAR" */; + compatibilityVersion = "Xcode 14.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 3D98C7E52D4FE04500462EF4; + productRefGroup = 3D98C7EF2D4FE04500462EF4 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 3D98C7ED2D4FE04500462EF4 /* SPAR */, + 3D8423A32DB14F86009CF847 /* SPARTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 3D8423A22DB14F86009CF847 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 3D98C7EC2D4FE04500462EF4 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 3D98C7F92D4FE04700462EF4 /* Preview Assets.xcassets in Resources */, + 3D98C7F62D4FE04700462EF4 /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 3D8423A02DB14F86009CF847 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 3D5995122DB4167100E9215B /* DeviceSpecification.swift in Sources */, + 3D5995102DB413E300E9215B /* NetworkManagerTests.swift in Sources */, + 3D59950B2DB4137E00E9215B /* MockNetworkService.swift in Sources */, + 3D5995112DB4162400E9215B /* NetworkManager.swift in Sources */, + 3DA1AA442DBDB01300678160 /* DiskIOModel.swift in Sources */, + 3D5995152DB4167A00E9215B /* MemoryUsage.swift in Sources */, + 3D505B762DBACFB100510486 /* DiskUsage.swift in Sources */, + 3D5995132DB4167400E9215B /* ProcessStatus.swift in Sources */, + 3D8423A72DB14F86009CF847 /* SPARTests.swift in Sources */, + 3D8423B32DB15082009CF847 /* OnboardingTests.swift in Sources */, + 3D8423B02DB15044009CF847 /* LaunchTest.swift in Sources */, + 3D59950E2DB413C100E9215B /* MockData.swift in Sources */, + 3D505B602DBA9EE900510486 /* LoginModel.swift in Sources */, + 3D78BB2A2DB550E5000E5C2F /* LoginViewUITests.swift in Sources */, + 3D8423B12DB15061009CF847 /* Constant.swift in Sources */, + 3D505B772DBACFB500510486 /* CPUUsage.swift in Sources */, + 3D5995142DB4167700E9215B /* BatteryInfo.swift in Sources */, + 3D505B612DBA9EF200510486 /* Onboarding.swift in Sources */, + 3DA1AA452DBDB08700678160 /* AppSettings.swift in Sources */, + 3D59950C2DB4138E00E9215B /* NetworkService.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 3D98C7EA2D4FE04500462EF4 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 3D5994FA2DB307B400E9215B /* HomeViewModel.swift in Sources */, + 3DCE458B2D68C19F00FACBB8 /* SplashScreenView.swift in Sources */, + 3DCE458D2D68C1A900FACBB8 /* Logger+Extension.swift in Sources */, + 3DA1AA472DBF034200678160 /* KeychainHelper.swift in Sources */, + 3D505B4D2DB9C1BE00510486 /* DeviceOptions.swift in Sources */, + 3D505B5B2DB9E3F700510486 /* ProcessViewModel.swift in Sources */, + 3D505B632DBAAE2C00510486 /* DiskUsage.swift in Sources */, + 3DCE45912D68C7E700FACBB8 /* View+Extension.swift in Sources */, + 3D505B592DB9E14700510486 /* MemoryUsageViewModel.swift in Sources */, + 3D505B552DB9DD5500510486 /* ProcessDetailPage.swift in Sources */, + 3D5995172DB41E1500E9215B /* MockData.swift in Sources */, + 3DF9BA592D678A9300D0CC62 /* (null) in Sources */, + 3D5995162DB41E0D00E9215B /* MockNetworkService.swift in Sources */, + 3DCE458F2D68C1B200FACBB8 /* OnboardingView.swift in Sources */, + 3DF9BA512D67762C00D0CC62 /* HomeView.swift in Sources */, + 3D505B5D2DBA657B00510486 /* AppSettings.swift in Sources */, + 3D505B752DBACEBF00510486 /* String+Extension.swift in Sources */, + 3D5995072DB4112100E9215B /* MemoryUsage.swift in Sources */, + 3D505B712DBACDE500510486 /* DiskIOViewModel.swift in Sources */, + 3D505B6B2DBAC5EC00510486 /* CpuUsageViewModel.swift in Sources */, + 3D5994FC2DB30B0100E9215B /* LoginViewModel.swift in Sources */, + 3D505B6F2DBACDCA00510486 /* DiskIOModel.swift in Sources */, + 3D98C7F42D4FE04500462EF4 /* ContentView.swift in Sources */, + 3D98C7F22D4FE04500462EF4 /* SPARApp.swift in Sources */, + 3D505B5F2DBA94B200510486 /* LoginModel.swift in Sources */, + 3D5995052DB4110700E9215B /* BatteryInfo.swift in Sources */, + 3D5995012DB410BD00E9215B /* DeviceSpecification.swift in Sources */, + 3DF9BA532D67767D00D0CC62 /* OnBoardingScreenView.swift in Sources */, + 3D505B532DB9DAA500510486 /* MemoryUsageDetailView.swift in Sources */, + 3D505B6D2DBAC6EA00510486 /* CpuUsageDetailView.swift in Sources */, + 3D07B5382D85C3BB0085B32C /* ChartData.swift in Sources */, + 3D505B692DBAB31D00510486 /* CPUUsage.swift in Sources */, + 3D5995092DB4114200E9215B /* NetworkManager.swift in Sources */, + 3D5994FF2DB3FFBE00E9215B /* NetworkService.swift in Sources */, + 3D5995032DB410DB00E9215B /* ProcessStatus.swift in Sources */, + 3D505B732DBACE3800510486 /* DiskIODetailView.swift in Sources */, + 3D07B53E2D8887F30085B32C /* ContentSizeCategory+Extension.swift in Sources */, + 3D505B4F2DB9C6AD00510486 /* NavigationButton.swift in Sources */, + 3D505B512DB9D76200510486 /* BatteryDetailView.swift in Sources */, + 3D505B792DBB170700510486 /* InfoRow.swift in Sources */, + 3D07B53A2D85C3EC0085B32C /* ChartDataContainer.swift in Sources */, + 3D07B5402D88881D0085B32C /* Onboarding.swift in Sources */, + 3D5994F52DB2BA1200E9215B /* BackgroundAnimationView.swift in Sources */, + 3D63FB0A2DC4F4F2006F53DE /* LoadingView.swift in Sources */, + 3D505B572DB9DF3F00510486 /* BatteryViewModel.swift in Sources */, + 3DCE45892D68C19200FACBB8 /* Constant.swift in Sources */, + 3D5994F32DB2B79400E9215B /* LoginView.swift in Sources */, + 3D07B53C2D85C4350085B32C /* HalfDonutChart.swift in Sources */, + 3D505B672DBAAFFE00510486 /* DiskUsageDetailView.swift in Sources */, + 3D505B652DBAAE4D00510486 /* DiskUsageViewModel.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 3D8423AB2DB14F86009CF847 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 3D98C7ED2D4FE04500462EF4 /* SPAR */; + targetProxy = 3D8423AA2DB14F86009CF847 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 3D8423AD2DB14F86009CF847 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 2WRH48XVUZ; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.Abhijeet.SPARTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = SPAR; + }; + name = Debug; + }; + 3D8423AE2DB14F86009CF847 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 2WRH48XVUZ; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.Abhijeet.SPARTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = SPAR; + }; + name = Release; + }; + 3D98C7FA2D4FE04700462EF4 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 17.5; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 3D98C7FB2D4FE04700462EF4 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 17.5; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 3D98C7FD2D4FE04700462EF4 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"SPAR/Preview Content\""; + DEVELOPMENT_TEAM = 2WRH48XVUZ; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSFaceIDUsageDescription = "We use it to unlock the phone"; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; + INFOPLIST_KEY_UIUserInterfaceStyle = Light; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.Abhijeet.SPAR; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; + }; + name = Debug; + }; + 3D98C7FE2D4FE04700462EF4 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"SPAR/Preview Content\""; + DEVELOPMENT_TEAM = 2WRH48XVUZ; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSFaceIDUsageDescription = "We use it to unlock the phone"; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; + INFOPLIST_KEY_UIUserInterfaceStyle = Light; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.Abhijeet.SPAR; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 3D8423AC2DB14F86009CF847 /* Build configuration list for PBXNativeTarget "SPARTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 3D8423AD2DB14F86009CF847 /* Debug */, + 3D8423AE2DB14F86009CF847 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 3D98C7E92D4FE04500462EF4 /* Build configuration list for PBXProject "SPAR" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 3D98C7FA2D4FE04700462EF4 /* Debug */, + 3D98C7FB2D4FE04700462EF4 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 3D98C7FC2D4FE04700462EF4 /* Build configuration list for PBXNativeTarget "SPAR" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 3D98C7FD2D4FE04700462EF4 /* Debug */, + 3D98C7FE2D4FE04700462EF4 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 3D98C7E62D4FE04500462EF4 /* Project object */; +} diff --git a/SPAR_iOS/SPAR/SPAR.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/SPAR_iOS/SPAR/SPAR.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/SPAR_iOS/SPAR/SPAR.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/SPAR_iOS/SPAR/SPAR.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/SPAR_iOS/SPAR/SPAR.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/SPAR_iOS/SPAR/SPAR.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/SPAR_iOS/SPAR/SPAR.xcodeproj/xcshareddata/xcschemes/SPAR.xcscheme b/SPAR_iOS/SPAR/SPAR.xcodeproj/xcshareddata/xcschemes/SPAR.xcscheme new file mode 100644 index 0000000..bd2906f --- /dev/null +++ b/SPAR_iOS/SPAR/SPAR.xcodeproj/xcshareddata/xcschemes/SPAR.xcscheme @@ -0,0 +1,100 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SPAR_iOS/SPAR/SPAR.xcodeproj/xcshareddata/xcschemes/SPARTests.xcscheme b/SPAR_iOS/SPAR/SPAR.xcodeproj/xcshareddata/xcschemes/SPARTests.xcscheme new file mode 100644 index 0000000..b2bd369 --- /dev/null +++ b/SPAR_iOS/SPAR/SPAR.xcodeproj/xcshareddata/xcschemes/SPARTests.xcscheme @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/SPAR_iOS/SPAR/SPAR.xcodeproj/xcuserdata/abhijeetcherungottil.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist b/SPAR_iOS/SPAR/SPAR.xcodeproj/xcuserdata/abhijeetcherungottil.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist new file mode 100644 index 0000000..4005bfc --- /dev/null +++ b/SPAR_iOS/SPAR/SPAR.xcodeproj/xcuserdata/abhijeetcherungottil.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/SPAR_iOS/SPAR/SPAR.xcodeproj/xcuserdata/abhijeetcherungottil.xcuserdatad/xcschemes/xcschememanagement.plist b/SPAR_iOS/SPAR/SPAR.xcodeproj/xcuserdata/abhijeetcherungottil.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100644 index 0000000..36a9545 --- /dev/null +++ b/SPAR_iOS/SPAR/SPAR.xcodeproj/xcuserdata/abhijeetcherungottil.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,32 @@ + + + + + SchemeUserState + + SPAR.xcscheme_^#shared#^_ + + orderHint + 0 + + SPARTests.xcscheme_^#shared#^_ + + orderHint + 1 + + + SuppressBuildableAutocreation + + 3D8423792DB0871A009CF847 + + primary + + + 3D98C7ED2D4FE04500462EF4 + + primary + + + + + diff --git a/SPAR_iOS/SPAR/SPAR/Assets.xcassets/3D Modern analytics webpage mockup.imageset/7.png b/SPAR_iOS/SPAR/SPAR/Assets.xcassets/3D Modern analytics webpage mockup.imageset/7.png new file mode 100644 index 0000000..e9ac9d0 Binary files /dev/null and b/SPAR_iOS/SPAR/SPAR/Assets.xcassets/3D Modern analytics webpage mockup.imageset/7.png differ diff --git a/SPAR_iOS/SPAR/SPAR/Assets.xcassets/3D Modern analytics webpage mockup.imageset/Contents.json b/SPAR_iOS/SPAR/SPAR/Assets.xcassets/3D Modern analytics webpage mockup.imageset/Contents.json new file mode 100644 index 0000000..3f216bf --- /dev/null +++ b/SPAR_iOS/SPAR/SPAR/Assets.xcassets/3D Modern analytics webpage mockup.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "7.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SPAR_iOS/SPAR/SPAR/Assets.xcassets/3D business analytics laptop.imageset/9.png b/SPAR_iOS/SPAR/SPAR/Assets.xcassets/3D business analytics laptop.imageset/9.png new file mode 100644 index 0000000..e6aeb23 Binary files /dev/null and b/SPAR_iOS/SPAR/SPAR/Assets.xcassets/3D business analytics laptop.imageset/9.png differ diff --git a/SPAR_iOS/SPAR/SPAR/Assets.xcassets/3D business analytics laptop.imageset/Contents.json b/SPAR_iOS/SPAR/SPAR/Assets.xcassets/3D business analytics laptop.imageset/Contents.json new file mode 100644 index 0000000..74a7bb4 --- /dev/null +++ b/SPAR_iOS/SPAR/SPAR/Assets.xcassets/3D business analytics laptop.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "9.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SPAR_iOS/SPAR/SPAR/Assets.xcassets/3D colorful performance gauge.imageset/8.png b/SPAR_iOS/SPAR/SPAR/Assets.xcassets/3D colorful performance gauge.imageset/8.png new file mode 100644 index 0000000..8a1787e Binary files /dev/null and b/SPAR_iOS/SPAR/SPAR/Assets.xcassets/3D colorful performance gauge.imageset/8.png differ diff --git a/SPAR_iOS/SPAR/SPAR/Assets.xcassets/3D colorful performance gauge.imageset/Contents.json b/SPAR_iOS/SPAR/SPAR/Assets.xcassets/3D colorful performance gauge.imageset/Contents.json new file mode 100644 index 0000000..37d2707 --- /dev/null +++ b/SPAR_iOS/SPAR/SPAR/Assets.xcassets/3D colorful performance gauge.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "8.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SPAR_iOS/SPAR/SPAR/Assets.xcassets/3D mobile showing notification.imageset/10.png b/SPAR_iOS/SPAR/SPAR/Assets.xcassets/3D mobile showing notification.imageset/10.png new file mode 100644 index 0000000..efff374 Binary files /dev/null and b/SPAR_iOS/SPAR/SPAR/Assets.xcassets/3D mobile showing notification.imageset/10.png differ diff --git a/SPAR_iOS/SPAR/SPAR/Assets.xcassets/3D mobile showing notification.imageset/Contents.json b/SPAR_iOS/SPAR/SPAR/Assets.xcassets/3D mobile showing notification.imageset/Contents.json new file mode 100644 index 0000000..4934b21 --- /dev/null +++ b/SPAR_iOS/SPAR/SPAR/Assets.xcassets/3D mobile showing notification.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "10.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SPAR_iOS/SPAR/SPAR/Assets.xcassets/AccentColor.colorset/Contents.json b/SPAR_iOS/SPAR/SPAR/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/SPAR_iOS/SPAR/SPAR/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SPAR_iOS/SPAR/SPAR/Assets.xcassets/AppIcon.appiconset/3.png b/SPAR_iOS/SPAR/SPAR/Assets.xcassets/AppIcon.appiconset/3.png new file mode 100644 index 0000000..0119704 Binary files /dev/null and b/SPAR_iOS/SPAR/SPAR/Assets.xcassets/AppIcon.appiconset/3.png differ diff --git a/SPAR_iOS/SPAR/SPAR/Assets.xcassets/AppIcon.appiconset/Contents.json b/SPAR_iOS/SPAR/SPAR/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..4cfde57 --- /dev/null +++ b/SPAR_iOS/SPAR/SPAR/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,14 @@ +{ + "images" : [ + { + "filename" : "3.png", + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SPAR_iOS/SPAR/SPAR/Assets.xcassets/Contents.json b/SPAR_iOS/SPAR/SPAR/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/SPAR_iOS/SPAR/SPAR/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SPAR_iOS/SPAR/SPAR/Assets.xcassets/logo.imageset/3.png b/SPAR_iOS/SPAR/SPAR/Assets.xcassets/logo.imageset/3.png new file mode 100644 index 0000000..0119704 Binary files /dev/null and b/SPAR_iOS/SPAR/SPAR/Assets.xcassets/logo.imageset/3.png differ diff --git a/SPAR_iOS/SPAR/SPAR/Assets.xcassets/logo.imageset/Contents.json b/SPAR_iOS/SPAR/SPAR/Assets.xcassets/logo.imageset/Contents.json new file mode 100644 index 0000000..2237aa5 --- /dev/null +++ b/SPAR_iOS/SPAR/SPAR/Assets.xcassets/logo.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "3.png", + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "SP Ar-2.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SPAR_iOS/SPAR/SPAR/Assets.xcassets/logo.imageset/SP Ar-2.png b/SPAR_iOS/SPAR/SPAR/Assets.xcassets/logo.imageset/SP Ar-2.png new file mode 100644 index 0000000..e0b2f48 Binary files /dev/null and b/SPAR_iOS/SPAR/SPAR/Assets.xcassets/logo.imageset/SP Ar-2.png differ diff --git a/SPAR_iOS/SPAR/SPAR/ContentView.swift b/SPAR_iOS/SPAR/SPAR/ContentView.swift new file mode 100644 index 0000000..8beec4e --- /dev/null +++ b/SPAR_iOS/SPAR/SPAR/ContentView.swift @@ -0,0 +1,39 @@ +// +// ContentView.swift +// SPAR +// +// Created by Abhijeet Cherungottil on 2/2/25. +// + +import SwiftUI +import OSLog + +enum AppView: Hashable { + case splash + case onboarding + case home + case login +} + +struct ContentView: View { + @State private var currentView: AppView = .splash + + var body: some View { + NavigationStack { + switch currentView { + case .splash: + SplashScreenView(currentView: $currentView) + case .onboarding: + OnboardingView(currentView: $currentView) + case .home: + HomeView(currentView: $currentView) + case .login: + LoginView(currentView: $currentView) + } + } + } +} + +#Preview { + ContentView() +} diff --git a/SPAR_iOS/SPAR/SPAR/Data/BatteryInfo.swift b/SPAR_iOS/SPAR/SPAR/Data/BatteryInfo.swift new file mode 100644 index 0000000..920b38a --- /dev/null +++ b/SPAR_iOS/SPAR/SPAR/Data/BatteryInfo.swift @@ -0,0 +1,19 @@ +// +// BatteryInfo.swift +// SPAR +// +// Created by Abhijeet Cherungottil on 4/19/25. +// + +import Foundation + +struct BatteryInfo: Codable, Identifiable { + let id: Int + let userId: Int + let hasBattery: Bool + let batteryPercentage: Int + let deviceId:String + let powerConsumption: Double + let timestamp: String + let charging: Bool +} diff --git a/SPAR_iOS/SPAR/SPAR/Data/CPUUsage.swift b/SPAR_iOS/SPAR/SPAR/Data/CPUUsage.swift new file mode 100644 index 0000000..1ee45fd --- /dev/null +++ b/SPAR_iOS/SPAR/SPAR/Data/CPUUsage.swift @@ -0,0 +1,97 @@ +// +// CPUUsage.swift +// SPAR +// +// Created by Abhijeet Cherungottil on 4/24/25. +// + +import Foundation + +// MARK: - CpuCoreUsage model +struct CpuCoreUsage: Codable { + let core: Int + let usage: Double +} + +// Fixed CpuUsage struct with better JSON handling +struct CpuUsage: Codable { + let id: Int + let totalCpuLoad: Double + let userId: Int + let deviceId: String + let timestamp: String + let perCoreUsage: [CpuCoreUsage] + + // Custom encoding for when we need to convert back to the API format + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(id, forKey: .id) + try container.encode(totalCpuLoad, forKey: .totalCpuLoad) + try container.encode(userId, forKey: .userId) + try container.encode(deviceId, forKey: .deviceId) + try container.encode(timestamp, forKey: .timestamp) + + // Convert perCoreUsage to a JSON string + let perCoreData = try JSONEncoder().encode(perCoreUsage) + if let perCoreString = String(data: perCoreData, encoding: .utf8) { + try container.encode(perCoreString, forKey: .perCoreUsageJson) + } + } + + enum CodingKeys: String, CodingKey { + case id, totalCpuLoad, userId, deviceId, timestamp, perCoreUsageJson + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + id = try container.decode(Int.self, forKey: .id) + totalCpuLoad = try container.decode(Double.self, forKey: .totalCpuLoad) + userId = try container.decode(Int.self, forKey: .userId) + deviceId = try container.decode(String.self, forKey: .deviceId) + timestamp = try container.decode(String.self, forKey: .timestamp) + + // Try to decode the perCoreUsageJson string + let jsonString = try container.decode(String.self, forKey: .perCoreUsageJson) + + // First attempt: direct decoding + if let data = jsonString.data(using: .utf8) { + do { + perCoreUsage = try JSONDecoder().decode([CpuCoreUsage].self, from: data) + return + } catch { + print("First parse attempt failed: \(error)") + } + } + + // Second attempt: clean the string and try again + let cleanedJsonString = jsonString + .replacingOccurrences(of: "\\\"", with: "\"") + .replacingOccurrences(of: "\\\\", with: "\\") + + if let data = cleanedJsonString.data(using: .utf8) { + do { + perCoreUsage = try JSONDecoder().decode([CpuCoreUsage].self, from: data) + return + } catch { + print("Second parse attempt failed: \(error)") + } + } + + // If all else fails, provide default values + print("Using default values for perCoreUsage") + perCoreUsage = [] + } + + // Convenience initializer (not used in decoding) + init(id: Int, totalCpuLoad: Double, perCoreUsage: [CpuCoreUsage], userId: Int, deviceId: String, timestamp: String) { + self.id = id + self.totalCpuLoad = totalCpuLoad + self.perCoreUsage = perCoreUsage + self.userId = userId + self.deviceId = deviceId + self.timestamp = timestamp + } +} + diff --git a/SPAR_iOS/SPAR/SPAR/Data/ChartData.swift b/SPAR_iOS/SPAR/SPAR/Data/ChartData.swift new file mode 100644 index 0000000..b9d82a8 --- /dev/null +++ b/SPAR_iOS/SPAR/SPAR/Data/ChartData.swift @@ -0,0 +1,16 @@ +// +// ChartData.swift +// SPAR +// +// Created by Abhijeet Cherungottil on 3/15/25. +// + +import SwiftUI + +struct ChartData: Identifiable { + var id = UUID() + var color: Color + var type: String + var percent: CGFloat +} + diff --git a/SPAR_iOS/SPAR/SPAR/Data/DeviceSpecification.swift b/SPAR_iOS/SPAR/SPAR/Data/DeviceSpecification.swift new file mode 100644 index 0000000..cd6150e --- /dev/null +++ b/SPAR_iOS/SPAR/SPAR/Data/DeviceSpecification.swift @@ -0,0 +1,31 @@ +// +// DeviceSpecification.swift +// SPAR +// +// Created by Abhijeet Cherungottil on 4/19/25. +// + +import Foundation + +struct DeviceSpecification: Codable, Identifiable { + let id: Int + let deviceId: String + let deviceName: String + let manufacturer: String + let model: String + let processor: String + let cpuPhysicalCores: Int + let cpuLogicalCores: Int + let installedRam: Double + let graphics: String + let operatingSystem: String + let systemType: String + let registeredAt: String + + enum CodingKeys: String, CodingKey { + case id + case deviceId,deviceName, manufacturer, model, processor + case cpuPhysicalCores, cpuLogicalCores, installedRam + case graphics, operatingSystem, systemType, registeredAt + } +} diff --git a/SPAR_iOS/SPAR/SPAR/Data/DiskIOModel.swift b/SPAR_iOS/SPAR/SPAR/Data/DiskIOModel.swift new file mode 100644 index 0000000..a7e16d2 --- /dev/null +++ b/SPAR_iOS/SPAR/SPAR/Data/DiskIOModel.swift @@ -0,0 +1,17 @@ +// +// DiskIOModel.swift +// SPAR +// +// Created by Abhijeet Cherungottil on 4/24/25. +// + +import Foundation + +struct DiskIO: Codable, Identifiable { + let id: Int + let readSpeedMBps: Double + let writeSpeedMBps: Double + let userId: Int + let deviceId: String + let timestamp: String +} diff --git a/SPAR_iOS/SPAR/SPAR/Data/DiskUsage.swift b/SPAR_iOS/SPAR/SPAR/Data/DiskUsage.swift new file mode 100644 index 0000000..065277f --- /dev/null +++ b/SPAR_iOS/SPAR/SPAR/Data/DiskUsage.swift @@ -0,0 +1,18 @@ +// +// DiskUsage.swift +// SPAR +// +// Created by Abhijeet Cherungottil on 4/24/25. +// + +import Foundation +struct DiskUsage: Codable, Identifiable { + let id: Int + let filesystem: String + let sizeGB: Double + let usedGB: Double + let availableGB: Double + let userId: Int + let deviceId: String + let timestamp: String +} diff --git a/SPAR_iOS/SPAR/SPAR/Data/LoginModel.swift b/SPAR_iOS/SPAR/SPAR/Data/LoginModel.swift new file mode 100644 index 0000000..4a645ad --- /dev/null +++ b/SPAR_iOS/SPAR/SPAR/Data/LoginModel.swift @@ -0,0 +1,18 @@ +// +// LoginModel.swift +// SPAR +// +// Created by Abhijeet Cherungottil on 4/24/25. +// + +import Foundation + +struct LoginRequest: Encodable { + let username: String + let password: String +} + +struct LoginResponse: Decodable { + let token: String + let userId: Int +} diff --git a/SPAR_iOS/SPAR/SPAR/Data/MemoryUsage.swift b/SPAR_iOS/SPAR/SPAR/Data/MemoryUsage.swift new file mode 100644 index 0000000..1f8b4ba --- /dev/null +++ b/SPAR_iOS/SPAR/SPAR/Data/MemoryUsage.swift @@ -0,0 +1,19 @@ +// +// MemoryUsage.swift +// SPAR +// +// Created by Abhijeet Cherungottil on 4/19/25. +// + +import Foundation + +struct MemoryUsage: Codable, Identifiable { + let id: Int + let userId: Int + let deviceId:String + let totalMemory: Double + let usedMemory: Double + let availableMemory: Double + let timestamp: String +} + diff --git a/SPAR_iOS/SPAR/SPAR/Data/MockData.swift b/SPAR_iOS/SPAR/SPAR/Data/MockData.swift new file mode 100644 index 0000000..fe9de99 --- /dev/null +++ b/SPAR_iOS/SPAR/SPAR/Data/MockData.swift @@ -0,0 +1,134 @@ +// +// MockData.swift +// SPARTests +// +// Created by Abhijeet Cherungottil on 4/19/25. +// + +import Foundation + +struct MockData { + static let sampleDeviceData = """ + [ + { + "userId": 1, + "deviceId": "331330ac-5f82-43b0-9d39-84e1f7e7e358", + "deviceName": "MyComputer", + "manufacturer": "Dell", + "model": "Inspiron 15", + "processor": "Intel Core i7 2.8 GHz", + "cpuPhysicalCores": 4, + "cpuLogicalCores": 8, + "installedRam": 16.0, + "graphics": "NVIDIA GTX 1650", + "operatingSystem": "Windows 10 x64", + "systemType": "x64 operating system, x64-based processor", + "registeredAt": "2025-03-28T16:03:30.041384" + }, + { + "userId": 1, + "deviceId": "331330ac-5f82-43b0-9d39-84e1f7e7e358", + "deviceName": "Home", + "manufacturer": "Dell", + "model": "Inspiron 15", + "processor": "Intel Core i7 2.8 GHz", + "cpuPhysicalCores": 4, + "cpuLogicalCores": 8, + "installedRam": 32.0, + "graphics": "NVIDIA GTX 1650", + "operatingSystem": "Windows 10 x64", + "systemType": "x64 operating system, x64-based processor", + "registeredAt": "2025-04-13T15:28:39.97323" + } + ] + """.data(using: .utf8)! + + static let sampleProcessData = """ + [ + { + "userId": 1, + "id": 101, + "pid": 1234, + "name": "chrome.exe", + "cpuUsage": 12.5, + "memoryMB": 200.0, + "deviceId": "331330ac-5f82-43b0-9d39-84e1f7e7e358", + "timestamp": "2025-04-13T15:29:00.236114" + }, + { + "userId": 1, + "id": 102, + "pid": 5678, + "name": "node.exe", + "cpuUsage": 5.0, + "memoryMB": 150.0, + "deviceId": "331330ac-5f82-43b0-9d39-84e1f7e7e358", + "timestamp": "2025-04-13T15:29:00.236114" + } + ] + """.data(using: .utf8)! + + static let sampleBatteryData = """ + { + "userId": 1, + "id": 7, + "hasBattery": true, + "batteryPercentage": 85, + "powerConsumption": 5.0, + "timestamp": "2025-04-13T15:29:10.549936", + "deviceId": "331330ac-5f82-43b0-9d39-84e1f7e7e358", + "charging": false + } + """.data(using: .utf8)! + + static let sampleMemoryUsageData = """ + { + "userId": 1, + "id": 7, + "totalMemory": 16.0, + "usedMemory": 8.5, + "availableMemory": 7.5, + "deviceId": "331330ac-5f82-43b0-9d39-84e1f7e7e358", + "timestamp": "2025-04-13T15:28:49.261218" + } + """.data(using: .utf8)! + static let sampleDiskUsageData = """ + { + "id": 5, + "filesystem": "/dev/sda1", + "sizeGB": 512.0, + "usedGB": 200.0, + "availableGB": 312.0, + "userId": 1, + "deviceId": "331330ac-5f82-43b0-9d39-84e1f7e7e358", + "timestamp": "2025-04-22T15:57:10.390972" + } + """.data(using: .utf8)! + static let sampleCPUUsageData = """ + { + "id": 8, + "totalCpuLoad": 5.145907157059862, + "perCoreUsageJson": "[{\\"core\\":1,\\"usage\\":8.544345751027443},{\\"core\\":2,\\"usage\\":6.293496720055942},{\\"core\\":3,\\"usage\\":16.68324711500199},{\\"core\\":4,\\"usage\\":19.07660579449822},{\\"core\\":5,\\"usage\\":1.1985844017094018},{\\"core\\":6,\\"usage\\":1.0950489099589356},{\\"core\\":7,\\"usage\\":1.8733957798593288},{\\"core\\":8,\\"usage\\":2.554001268654225},{\\"core\\":9,\\"usage\\":0.8346409374687009},{\\"core\\":10,\\"usage\\":1.2519614062030515},{\\"core\\":11,\\"usage\\":1.0449704537108135},{\\"core\\":12,\\"usage\\":1.1484275889697537}]", + "userId": 1, + "deviceId": "47af4ef0-2c9f-4962-95f1-6b206ec305e6", + "timestamp": "2025-04-29T18:02:05.970927" + } + """.data(using: .utf8)! + static let sampleDiskIOUsageData = """ + {"id": 5, + "readSpeedMBps": 120.0, + "writeSpeedMBps": 80.0, + "userId": 1, + "deviceId": "331330ac-5f82-43b0-9d39-84e1f7e7e358", + "timestamp": "2025-04-22T15:57:10.377292" + } + """.data(using: .utf8)! + static let sampleLoginData = """ + { + "token": "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhbGljZSIsImlhdCI6MTc0NTM1MDY1NCwiZXhwIjo5MjIzNTQ2NTcxOTIwMn0.BypJMZiF7ooVbYXCioOQAljTyjmR9ET5aJTC9auiVxw", + "userId": 1 + } + """.data(using: .utf8)! +} + + diff --git a/SPAR_iOS/SPAR/SPAR/Data/Onboarding.swift b/SPAR_iOS/SPAR/SPAR/Data/Onboarding.swift new file mode 100644 index 0000000..0c40c2a --- /dev/null +++ b/SPAR_iOS/SPAR/SPAR/Data/Onboarding.swift @@ -0,0 +1,14 @@ +// +// Onboarding.swift +// SPAR +// +// Created by Abhijeet Cherungottil on 3/17/25. +// + +import Foundation + +struct Onboarding: Hashable { + let imageName: String + let title: String + let description: String +} diff --git a/SPAR_iOS/SPAR/SPAR/Data/ProcessStatus.swift b/SPAR_iOS/SPAR/SPAR/Data/ProcessStatus.swift new file mode 100644 index 0000000..596c827 --- /dev/null +++ b/SPAR_iOS/SPAR/SPAR/Data/ProcessStatus.swift @@ -0,0 +1,19 @@ +// +// ProcessStatus.swift +// SPAR +// +// Created by Abhijeet Cherungottil on 4/19/25. +// + +import Foundation + +struct ProcessStatus: Codable, Identifiable { + let id: Int + let pid: Int + let name: String + let cpuUsage: Double + let memoryMB: Double + let userId: Int + let deviceId: String + let timestamp: String +} diff --git a/SPAR_iOS/SPAR/SPAR/Extensions/ContentSizeCategory+Extension.swift b/SPAR_iOS/SPAR/SPAR/Extensions/ContentSizeCategory+Extension.swift new file mode 100644 index 0000000..8aeb6f1 --- /dev/null +++ b/SPAR_iOS/SPAR/SPAR/Extensions/ContentSizeCategory+Extension.swift @@ -0,0 +1,21 @@ +// +// ContentSizeCategory+Extension.swift +// SPAR +// +// Created by Abhijeet Cherungottil on 3/17/25. +// + +import SwiftUI + +extension ContentSizeCategory { + var customMinScaleFactor: CGFloat { + switch self { + case .extraSmall,.small, .medium: + return 1.0 + case .large, .extraLarge, .extraExtraLarge: + return 0.8 + default: + return 0.6 + } + } +} diff --git a/SPAR_iOS/SPAR/SPAR/Extensions/Logger+Extension.swift b/SPAR_iOS/SPAR/SPAR/Extensions/Logger+Extension.swift new file mode 100644 index 0000000..2a800ff --- /dev/null +++ b/SPAR_iOS/SPAR/SPAR/Extensions/Logger+Extension.swift @@ -0,0 +1,16 @@ +// +// Logger+Extension.swift +// SPAR +// +// Created by Abhijeet Cherungottil on 2/20/25. +// + +import Foundation +import OSLog + +extension Logger { + static let subsytem = Bundle.main.bundleIdentifier ?? "" + static let fileLocation = Logger(subsystem: subsytem, category: "FileLocation") + static let dataStore = Logger(subsystem: subsytem, category: "DataStore") + static let fileManager = Logger(subsystem: subsytem, category: "FileManager") +} diff --git a/SPAR_iOS/SPAR/SPAR/Extensions/String+Extension.swift b/SPAR_iOS/SPAR/SPAR/Extensions/String+Extension.swift new file mode 100644 index 0000000..81173a4 --- /dev/null +++ b/SPAR_iOS/SPAR/SPAR/Extensions/String+Extension.swift @@ -0,0 +1,27 @@ +// +// String+Extension.swift +// SPAR +// +// Created by Abhijeet Cherungottil on 4/24/25. +// + +import Foundation + +extension String { + func toFormattedDate() -> String { + // Define the input date format (ISO 8601) + let inputFormatter = DateFormatter() + inputFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSSSS" + + // Convert the string into a Date object + if let date = inputFormatter.date(from: self) { + // Define the desired output format + let outputFormatter = DateFormatter() + outputFormatter.dateStyle = .medium + outputFormatter.timeStyle = .short + // Return the formatted date string + return outputFormatter.string(from: date) + } + return "" + } +} diff --git a/SPAR_iOS/SPAR/SPAR/Extensions/View+Extension.swift b/SPAR_iOS/SPAR/SPAR/Extensions/View+Extension.swift new file mode 100644 index 0000000..7920709 --- /dev/null +++ b/SPAR_iOS/SPAR/SPAR/Extensions/View+Extension.swift @@ -0,0 +1,20 @@ +// +// View+Extension.swift +// SPAR +// +// Created by Abhijeet Cherungottil on 2/21/25. +// + +import SwiftUI +import OSLog + +extension View { + func logPageVisit() { + let logger = Logger.fileLocation + let structureName = String(describing: type(of: self)) // Get the structure name dynamically + logger.info("\(String(format: LoggerConstant.pageName, arguments: [structureName]))") + } + func accessibility(_ id: AccessibilityIdentifier) -> some View { + self.accessibilityIdentifier(id.rawValue) + } +} diff --git a/SPAR_iOS/SPAR/SPAR/NetworkService/MockNetworkService.swift b/SPAR_iOS/SPAR/SPAR/NetworkService/MockNetworkService.swift new file mode 100644 index 0000000..e4828b2 --- /dev/null +++ b/SPAR_iOS/SPAR/SPAR/NetworkService/MockNetworkService.swift @@ -0,0 +1,93 @@ +// +// MockNetworkService.swift +// SPARTests +// +// Created by Abhijeet Cherungottil on 4/19/25. +// + +import Foundation + + +// First, let's fix the MockNetworkService for CPU usage +final class MockNetworkService: NetworkServicing { + + enum Endpoint: String, CaseIterable { + case device = "device-specifications" + case process = "process-status" + case battery = "battery-info" + case memory = "ram-usage" + case disk = "disk-usage" + case diskIO = "disk-io" + case cpu = "cpu-usage" + case login = "auth/signin" + } + + func get(from url: URL, token: String?) async throws -> T { + guard let endpoint = Endpoint.allCases.first(where: { url.absoluteString.contains($0.rawValue) }) else { + throw URLError(.unsupportedURL) + } + + let mockData: Data + switch endpoint { + case .device: mockData = SimpleMockData.device + case .process: mockData = SimpleMockData.process + case .battery: mockData = SimpleMockData.battery + case .memory: mockData = SimpleMockData.memory + case .disk: mockData = SimpleMockData.disk + case .diskIO: mockData = SimpleMockData.diskIO + case .cpu: + let decoded = try JSONDecoder().decode(TempCPUUsage.self, from: SimpleMockData.cpu) + let parsedCore = try JSONDecoder().decode([CpuCoreUsage].self, from: decoded.perCoreUsageJson.data(using: .utf8)!) + let final = CpuUsage(id: decoded.id, + totalCpuLoad: decoded.totalCpuLoad, + perCoreUsage: parsedCore, + userId: decoded.userId, + deviceId: decoded.deviceId, + timestamp: decoded.registeredAt) + return final as! T + case .login: mockData = SimpleMockData.login + } + + return try JSONDecoder().decode(T.self, from: mockData) + } + + func post(to url: URL, body: U) async throws -> T { + return try JSONDecoder().decode(T.self, from: SimpleMockData.login) + } +} +// Helper extension to debug JSON data +extension Data { + var prettyPrintedJSONString: String? { + guard let object = try? JSONSerialization.jsonObject(with: self, options: []), + let data = try? JSONSerialization.data(withJSONObject: object, options: [.prettyPrinted]), + let prettyPrintedString = String(data: data, encoding: .utf8) else { return nil } + + return prettyPrintedString + } +} + +// Optionally, for debugging purposes +extension MockData { + static func printSampleCPUUsageData() { + print("Raw CPU Data:") + print(sampleCPUUsageData.prettyPrintedJSONString ?? "Could not pretty print") + } +} +struct TempCPUUsage: Codable { + var id: Int + var totalCpuLoad: Double + var perCoreUsageJson: String + var userId: Int + var deviceId: String + var registeredAt: String +} +struct SimpleMockData { + static let device = MockData.sampleDeviceData + static let process = MockData.sampleProcessData + static let battery = MockData.sampleBatteryData + static let memory = MockData.sampleMemoryUsageData + static let disk = MockData.sampleDiskUsageData + static let diskIO = MockData.sampleDiskIOUsageData + static let cpu = MockData.sampleCPUUsageData + static let login = MockData.sampleLoginData +} diff --git a/SPAR_iOS/SPAR/SPAR/NetworkService/NetworkManager.swift b/SPAR_iOS/SPAR/SPAR/NetworkService/NetworkManager.swift new file mode 100644 index 0000000..0e4374c --- /dev/null +++ b/SPAR_iOS/SPAR/SPAR/NetworkService/NetworkManager.swift @@ -0,0 +1,82 @@ +// +// NetworkManager.swift +// SPAR +// +// Created by Abhijeet Cherungottil on 4/19/25. +// + +import Foundation + +class NetworkManager { + private let networkService: NetworkServicing + private let baseURL = "https://mscs-spring2025-capping.onrender.com" + + init(networkService: NetworkServicing = NetworkService()) { + self.networkService = networkService + } + + private func makeURL(endpoint: String, userId: Int, deviceId: String) -> URL? { + URL(string: "\(baseURL)/api/metrics/\(endpoint)/\(userId)/\(deviceId)") + } + + func fetchDeviceSpecifications(for userId: Int) async throws -> [DeviceSpecification] { + + guard let url = URL(string: "https://mscs-spring2025-capping.onrender.com/api/users/\(userId)/getdevices"),let token = AppSettings.shared.authToken else { + throw URLError(.badURL) + } + print("userId",userId) + print("token",token) + return try await networkService.get(from: url,token: token) + } + func fetchCPUUsageInfo(for userId: Int, deviceId: String) async throws -> CpuUsage { + guard let url = makeURL(endpoint: "cpu-usage", userId: userId, deviceId: deviceId),let token = AppSettings.shared.authToken else { + throw URLError(.badURL) + } + + return try await networkService.get(from: url, token: token) + } + func fetchProcessStatus(for userId: Int, deviceId: String) async throws -> [ProcessStatus] { + guard let url = makeURL(endpoint: "process-status", userId: userId, deviceId: deviceId),let token = AppSettings.shared.authToken else { + throw URLError(.badURL) + } + return try await networkService.get(from: url, token: token) + } + + func fetchBatteryInfo(for userId: Int, deviceId: String) async throws -> BatteryInfo { + guard let url = makeURL(endpoint: "battery-info", userId: userId, deviceId: deviceId),let token = AppSettings.shared.authToken else { + throw URLError(.badURL) + } + return try await networkService.get(from: url, token: token) + } + + func fetchMemoryUsage(for userId: Int, deviceId: String) async throws -> MemoryUsage { + guard let url = makeURL(endpoint: "ram-usage", userId: userId, deviceId: deviceId),let token = AppSettings.shared.authToken else { + throw URLError(.badURL) + } + return try await networkService.get(from: url, token: token) + } + + func fetchDiskUsage(for userId: Int, deviceId: String) async throws -> DiskUsage { + guard let url = makeURL(endpoint: "disk-usage", userId: userId, deviceId: deviceId),let token = AppSettings.shared.authToken else { + throw URLError(.badURL) + } + return try await networkService.get(from: url, token: token) + } + + func fetchDiskIO(for userId: Int, deviceId: String) async throws -> DiskIO { + guard let url = makeURL(endpoint: "disk-io", userId: userId, deviceId: deviceId),let token = AppSettings.shared.authToken else { + throw URLError(.badURL) + } + return try await networkService.get(from: url, token: token) + } + + func login(username: String, password: String) async throws -> LoginResponse { + guard let url = URL(string: "https://mscs-spring2025-capping.onrender.com/api/auth/signin") else { + throw URLError(.badURL) + } + let loginRequest = LoginRequest(username: username, password: password) + return try await networkService.post(to: url, body: loginRequest) + } + +} + diff --git a/SPAR_iOS/SPAR/SPAR/NetworkService/NetworkService.swift b/SPAR_iOS/SPAR/SPAR/NetworkService/NetworkService.swift new file mode 100644 index 0000000..9268376 --- /dev/null +++ b/SPAR_iOS/SPAR/SPAR/NetworkService/NetworkService.swift @@ -0,0 +1,76 @@ +// +// NetworkService.swift +// SPAR +// +// Created by Abhijeet Cherungottil on 4/19/25. +// + +import Foundation +import Combine + +// MARK: - Protocol +protocol NetworkServicing { + func get(from url: URL, token: String?) async throws -> T + func post(to url: URL, body: U) async throws -> T +} + +// MARK: - Implementation +class NetworkService: NetworkServicing { + private let maxRetryCount = 3 + + // Generic GET + func get(from url: URL, token: String? = nil) async throws -> T { + var request = URLRequest(url: url) + request.httpMethod = "GET" + + if let token = token { + request.addValue("Bearer \(token)", forHTTPHeaderField: "Authorization") + request.addValue("application/json", forHTTPHeaderField: "Accept") + + } + print("request",request) + print("Request Headers:", request.allHTTPHeaderFields ?? [:]) + print("Request URL:", request.url?.absoluteString ?? "nil") + + return try await perform(request: request) + } + + // Generic POST + func post(to url: URL, body: U) async throws -> T { + var request = URLRequest(url: url) + request.httpMethod = "POST" + request.addValue("application/json", forHTTPHeaderField: "Content-Type") + request.httpBody = try JSONEncoder().encode(body) + + return try await perform(request: request) + } + + // MARK: - Private core method with Retry and Exponential Backoff + private func perform(request: URLRequest) async throws -> T { + var currentRetry = 0 + var delay: Double = 1.0 + + while currentRetry <= maxRetryCount { + do { + let (data, response) = try await URLSession.shared.data(for: request) + + guard let httpResponse = response as? HTTPURLResponse, + (200...299).contains(httpResponse.statusCode) else { + throw URLError(.badServerResponse) + } + + let decodedData = try JSONDecoder().decode(T.self, from: data) + return decodedData + } catch { + if currentRetry == maxRetryCount { + throw error + } + try await Task.sleep(nanoseconds: UInt64(delay * 1_000_000_000)) + delay *= 2 + currentRetry += 1 + } + } + + throw URLError(.cannotLoadFromNetwork) + } +} diff --git a/SPAR_iOS/SPAR/SPAR/Preview Content/Preview Assets.xcassets/Contents.json b/SPAR_iOS/SPAR/SPAR/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/SPAR_iOS/SPAR/SPAR/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SPAR_iOS/SPAR/SPAR/SPARApp.swift b/SPAR_iOS/SPAR/SPAR/SPARApp.swift new file mode 100644 index 0000000..86deea3 --- /dev/null +++ b/SPAR_iOS/SPAR/SPAR/SPARApp.swift @@ -0,0 +1,33 @@ +// +// SPARApp.swift +// SPAR +// +// Created by Abhijeet Cherungottil on 2/2/25. +// + +import SwiftUI +import OSLog + +@main +struct SPARApp: App { + init() { + // Check if we're running in UI Test mode + if ProcessInfo.processInfo.arguments.contains("-resetDefaults") { + let domain = Bundle.main.bundleIdentifier! + UserDefaults.standard.removePersistentDomain(forName: domain) + UserDefaults.standard.synchronize() + print("UserDefaults reset for UI Test") + } + } + + let logger = Logger.fileLocation + + var body: some Scene { + WindowGroup { + ContentView() + .onAppear{ + logger.info("\(URL.documentsDirectory.path())") + } + } + } +} diff --git a/SPAR_iOS/SPAR/SPAR/Utilities/AppSettings.swift b/SPAR_iOS/SPAR/SPAR/Utilities/AppSettings.swift new file mode 100644 index 0000000..0a3d3f6 --- /dev/null +++ b/SPAR_iOS/SPAR/SPAR/Utilities/AppSettings.swift @@ -0,0 +1,46 @@ +// +// AppSettings.swift +// SPAR +// +// Created by Abhijeet Cherungottil on 4/24/25. +// + +import Foundation + +final class AppSettings { + static let shared = AppSettings() + + private init() {} + + private let onboardingKey = "hasCompletedOnboarding" + private let tokenKey = "authToken" + private let userIdKey = "userId" + private let hasLoggedInOnceKey = "hasLoggedInOnce" + + func clearUserSession() { + self.authToken = nil + userId = nil + // clear any saved credentials or tokens + } + + var hasLoggedInOnce: Bool { + get { UserDefaults.standard.bool(forKey: hasLoggedInOnceKey) } + set { UserDefaults.standard.set(newValue, forKey: hasLoggedInOnceKey) } + } + + var hasCompletedOnboarding: Bool { + get { UserDefaults.standard.bool(forKey: onboardingKey) } + set { UserDefaults.standard.set(newValue, forKey: onboardingKey) } + } + + var authToken: String? { + get { UserDefaults.standard.string(forKey: "authToken") } + set { UserDefaults.standard.set(newValue, forKey: "authToken") } + } + + var userId: Int? { + get { UserDefaults.standard.integer(forKey: "userId") } + set { UserDefaults.standard.set(newValue, forKey: "userId") } + } + +} diff --git a/SPAR_iOS/SPAR/SPAR/Utilities/Constant.swift b/SPAR_iOS/SPAR/SPAR/Utilities/Constant.swift new file mode 100644 index 0000000..743db65 --- /dev/null +++ b/SPAR_iOS/SPAR/SPAR/Utilities/Constant.swift @@ -0,0 +1,137 @@ +// +// Constant.swift +// SPAR +// +// Created by Abhijeet Cherungottil on 2/20/25. +// + +import Foundation + +enum StringConstant { + static let appNameFullForm = "System Processes And Reports" + static let appName = "S.P.A.R" + static let welcomeTitle = "Welcome to \(appName)! Monitor Your System Health Anytime!" + static let welcomeDescription = "Keep an eye on your system’s performance with real-time metrics at your fingertips." + static let cpuTrackingTitle = "Track CPU & Disk Usage Effortlessly" + static let cpuTrackingDescription = "View real-time CPU and disk utilization metrics to optimize system performance." + static let alertsTitle = "Stay Notified with Critical Alerts!" + static let alertsDescription = "Get instant alerts when system performance is at risk." + static let desktopRequirementTitle = "Connect with Our Desktop App!" + static let desktopRequirementDescription = "To use this mobile app, you need to install our desktop companion app." + static let onboardingImages = "Onbording image of %@" + static let getstarted = "Get Started" + static let Dashboard = "Dashboard" + static let incorrectCredentials = "Invalid credentials or network error." + static let welcomeBack = "Welcome Back" + static let login = "LOGIN" + static let Username = "Username" + static let Password = "Password" + static let submit = "Submit" + static let searchIcon = "It's icon for searching device." + static let searchText = "Search devices..." + static let sdearchText = "Search devices..." + static let emptyscreenmsg = "You need to have at least one Desktop APP." + static let downloadApp = "Download SPAR Desktop" + + // device detail + static let deviceInfo = "Device Info" + static let deviceName = "Device Name" + static let manufacturer = "Manufacturer" + static let model = "Model" + static let processor = "Processor" + static let physicalCore = "Physical Cores" + static let logicalCores = "Logical Cores" + static let RAM = "RAM" + static let graphics = "Graphics" + static let OS = "OS" + static let systemType = "System Type" + static let registeredAt = "Time Stamp" + static let batteryInfo = "Battery Info" + static let cpu = "CPU" + static let memoryUsage = "Ram Usage" + static let diskUsage = "Disk Usage" + static let diskIO = "Disk IO" + static let processlist = "Running Processes" + static let details = "Details" + static let diskIOUssage = "Disk I/O Usage" + + // battery detail + static let batteryStatus = "Battery Status" + static let Charging = "Charging" + static let batYes = "Yes ⚑️" + static let batNo = "No❗️" + static let power = "Power Consumption" + + // memeory + static let totalMemeory = "Total Memory" + static let usedMemory = "Used Memory" + static let availableMemory = "Available Memory" + + // process + static let processMonitor = "Process Monitor" + static let metric = "Metric" + static let cpuUsage = "CPU Usage" + static let memory = "Memory" + static let process = "Process" + + // diskUsage + static let filesystem = "Filesystem" + static let size = "Total Size" + static let usedSpace = "Used Space" + static let availableSpace = "Available Space" + // cpu + static let totalCPULoad = "Total CPU Load" + static let allCore = "All Core Usage" + static let topFive = "Top 5 Core Usage" + + // diskio + static let RS = "Read Speed (MBps)" + static let WS = "Write Speed (MBps)" + static let diskIOChart = "Disk I/O Chart" + + + +} + +enum ImageConstant { + static let logo = "logo" + static let welcomeImage = "3D Modern analytics webpage mockup" + static let stopwatchImage = "3D colorful performance gauge" + static let laptopGraphImage = "3D business analytics laptop" + static let mobileNotificationImage = "3D mobile showing notification" + static let magnifyingGlass = "magnifyingglass" + static let xmarkCircleFill = "xmark.circle.fill" + static let chevronRight = "chevron.right" + static let eye = "eye" + static let eyeSlash = "eye.slash" + static let logout = "rectangle.portrait.and.arrow.right" + static let emptyScreenlogo = "desktopcomputer" + static let faceid = "faceid" + static let cpu = "cpu" + static let memorychip = "memorychip" + +} + +enum LoggerConstant { + static let pageName = "❕Page visited: %@" + static let getStartedTapped = "❕Get Started button was tapped" + static let LoginSubmitTapped = "❕Submit button was tapped" +} + +enum AccessibilityConstant { + static let signOut = "Sign Out" + static let metricPicker = "Metric Picker" + static let processtip1 = "Double tap to view more process details" + static let top5 = "Top five CPU cores by usage" + static let barChart = "Bar chart showing the five CPU cores with highest utilization" +} + +enum AccessibilityIdentifier: String { + case dashboardTitle + case onboardingWelcome + case onboardingCPU + case onboardingAlerts + case getStartedButton + case cpuUsageLabel + // Add more as needed +} diff --git a/SPAR_iOS/SPAR/SPAR/Utilities/InfoRow.swift b/SPAR_iOS/SPAR/SPAR/Utilities/InfoRow.swift new file mode 100644 index 0000000..e8f9364 --- /dev/null +++ b/SPAR_iOS/SPAR/SPAR/Utilities/InfoRow.swift @@ -0,0 +1,33 @@ +// +// InfoRow.swift +// SPAR +// +// Created by Abhijeet Cherungottil on 4/24/25. +// + +import SwiftUI + +struct InfoRow: View { + let label: String + let value: String + @Environment(\.sizeCategory) var sizeCategory + + + var body: some View { + HStack { + Text(label) + .fontWeight(.semibold) + .foregroundColor(.gray) + .minimumScaleFactor(sizeCategory.customMinScaleFactor) + Spacer() + Text(value) + .multilineTextAlignment(.trailing) + .foregroundColor(.black) + .minimumScaleFactor(sizeCategory.customMinScaleFactor) + } + } +} + +#Preview { + InfoRow(label: "User", value: "User123") +} diff --git a/SPAR_iOS/SPAR/SPAR/Utilities/KeychainHelper.swift b/SPAR_iOS/SPAR/SPAR/Utilities/KeychainHelper.swift new file mode 100644 index 0000000..62d05f3 --- /dev/null +++ b/SPAR_iOS/SPAR/SPAR/Utilities/KeychainHelper.swift @@ -0,0 +1,36 @@ +// +// KeychainHelper.swift +// SPAR +// +// Created by Abhijeet Cherungottil on 4/27/25. +// + +import Security +import Foundation + +class KeychainHelper { + static func save(_ data: Data, service: String, account: String) { + let query: [String: Any] = [ + kSecClass as String: kSecClassGenericPassword, + kSecAttrService as String: service, + kSecAttrAccount as String: account, + kSecValueData as String: data + ] + SecItemAdd(query as CFDictionary, nil) + } + + static func read(service: String, account: String) -> Data? { + let query: [String: Any] = [ + kSecClass as String: kSecClassGenericPassword, + kSecAttrService as String: service, + kSecAttrAccount as String: account, + kSecReturnData as String: true, + kSecMatchLimit as String: kSecMatchLimitOne + ] + + var result: AnyObject? + SecItemCopyMatching(query as CFDictionary, &result) + return result as? Data + } +} + diff --git a/SPAR_iOS/SPAR/SPAR/Utilities/LoadingView.swift b/SPAR_iOS/SPAR/SPAR/Utilities/LoadingView.swift new file mode 100644 index 0000000..7d3d3cd --- /dev/null +++ b/SPAR_iOS/SPAR/SPAR/Utilities/LoadingView.swift @@ -0,0 +1,38 @@ +// +// LoadingView.swift +// SPAR +// +// Created by Abhijeet Cherungottil on 5/2/25. +// + +import SwiftUI + +struct LoadingView: View { + let isLoading: Bool + let content: () -> Content + + var body: some View { + ZStack { + content() + .disabled(isLoading) + .blur(radius: isLoading ? 2 : 0) + + if isLoading { + VStack(spacing: 12) { + ProgressView() + .progressViewStyle(CircularProgressViewStyle()) + .scaleEffect(1.5) + Text("Loading...") + .font(.caption) + .foregroundColor(.gray) + } + .padding(20) + .background(Color(.systemBackground)) + .cornerRadius(12) + .shadow(radius: 10) + } + } + } +} + + diff --git a/SPAR_iOS/SPAR/SPAR/Utilities/NavigationButton.swift b/SPAR_iOS/SPAR/SPAR/Utilities/NavigationButton.swift new file mode 100644 index 0000000..76e0a97 --- /dev/null +++ b/SPAR_iOS/SPAR/SPAR/Utilities/NavigationButton.swift @@ -0,0 +1,52 @@ +// +// NavigationButton.swift +// SPAR +// +// Created by Abhijeet Cherungottil on 4/23/25. +// + +import SwiftUI + +struct NavigationButton: View { + let title: String + let destination: Destination + @State private var isPressed = false + + init(title: String, @ViewBuilder destination: () -> Destination) { + self.title = title + self.destination = destination() + } + + var body: some View { + NavigationLink(destination: destination) { + HStack { + Text(title) + .font(.headline) + .fontWeight(.bold) + .foregroundColor(.white) + Spacer() + Image(systemName: "chevron.right") + .foregroundColor(.white) + .accessibilityLabel("Next") // Added accessibility label for screen readers + } + .padding() + .background( + RoundedRectangle(cornerRadius: 16) + .fill(LinearGradient(colors: [Color.purple, Color.blue], startPoint: .topLeading, endPoint: .bottomTrailing)) + ) + .overlay( + RoundedRectangle(cornerRadius: 16) + .stroke(Color.white.opacity(0.25), lineWidth: 1) + ) + .shadow(color: Color.blue.opacity(0.5), radius: 10, x: 0, y: 5) + .scaleEffect(isPressed ? 0.97 : 1.0) + + .accessibilityElement(children: .ignore) // Ensures only the button's main label is read by screen readers + .accessibilityAddTraits(.isButton) // Marks this as a button for screen readers + + .focusable(true) // Make sure it is focusable for keyboard navigation + } + } +} + + diff --git a/SPAR_iOS/SPAR/SPAR/View/ChartUI/HalfDonutChart.swift b/SPAR_iOS/SPAR/SPAR/View/ChartUI/HalfDonutChart.swift new file mode 100644 index 0000000..1b500e2 --- /dev/null +++ b/SPAR_iOS/SPAR/SPAR/View/ChartUI/HalfDonutChart.swift @@ -0,0 +1,70 @@ +// +// HalfDonutChart.swift +// SPAR +// +// Created by Abhijeet Cherungottil on 3/15/25. +// + +import SwiftUI + +struct HalfDonutChart: View { + @Binding var chartDataObj: ChartData + @Environment(\.sizeCategory) var sizeCategory + + var body: some View { + VStack { + HStack { + Text(chartDataObj.type) + .font(.title) + .bold() + .minimumScaleFactor(sizeCategory.customMinScaleFactor) + Spacer() + } + + ZStack { + ArcShape(startAngle: .degrees(180), endAngle: .degrees(360)) + .stroke(chartDataObj.color.opacity(0.3), lineWidth: 45) + + ArcShape(startAngle: .degrees(180), endAngle: .degrees(180 + (Double(chartDataObj.percent / 100) * 180))) + .stroke(chartDataObj.color, lineWidth: 45) + .animation(.easeInOut(duration: 1.0), value: chartDataObj.percent) + + Text("\(Int(chartDataObj.percent))%") + .font(.title) + .bold() + .foregroundColor(.black) + .minimumScaleFactor(sizeCategory.customMinScaleFactor) + .offset(y: Double((250 / 3) * sizeCategory.customMinScaleFactor)) + .accessibilityHidden(true) // Hide visual label from VoiceOver + } + .accessibilityElement(children: .ignore) + .accessibilityLabel("\(chartDataObj.type) Usage") + .accessibilityValue("\(Int(chartDataObj.percent)) percent used") + .accessibilityHint("Semi-circular chart showing current \(chartDataObj.type.lowercased()) usage") + + Spacer() + } + .frame(width: 300, height: 250) + .padding() + .background(Color(red: 1, green: 0.961, blue: 0.882)) + } +} + + +struct ArcShape: Shape { + var startAngle: Angle + var endAngle: Angle + + func path(in rect: CGRect) -> Path { + var path = Path() + let radius = min(rect.width, rect.height) + let center = CGPoint(x: rect.midX, y: rect.maxY) // Align bottom-center + + path.addArc(center: center, radius: radius/1.9, startAngle: startAngle, endAngle: endAngle, clockwise: false) + return path + } +} + +#Preview { + HalfDonutChart(chartDataObj: .constant(ChartData(color: Color(#colorLiteral(red: 1, green: 0.493, blue: 0.474, alpha: 1)), type: "CPU", percent: 30))) +} diff --git a/SPAR_iOS/SPAR/SPAR/View/Device/BatteryDetailView.swift b/SPAR_iOS/SPAR/SPAR/View/Device/BatteryDetailView.swift new file mode 100644 index 0000000..8a3f2fa --- /dev/null +++ b/SPAR_iOS/SPAR/SPAR/View/Device/BatteryDetailView.swift @@ -0,0 +1,143 @@ +// +// BatteryDetailView.swift +// SPAR +// +// Created by Abhijeet Cherungottil on 4/23/25. +// + +import SwiftUI + +// MARK: - BatteryDetailView (Main View) +struct BatteryDetailView: View { + @StateObject private var viewModel: BatteryViewModel + let device: DeviceSpecification + @Environment(\.sizeCategory) var sizeCategory + + init(device: DeviceSpecification) { + _viewModel = StateObject(wrappedValue: BatteryViewModel(device: device)) + self.device = device + } + + var body: some View { + LoadingView(isLoading: viewModel.isLoading) { + ZStack { + // MARK: Background Gradient + LinearGradient(colors: [.blue.opacity(0.15), .purple.opacity(0.2)], startPoint: .top, endPoint: .bottom) + .ignoresSafeArea() + + VStack { + Spacer() + + // MARK: Card Content + BatteryCardView(viewModel: viewModel, device: device) + + Spacer() + } + .padding() + } + .onAppear { + self.logPageVisit() + } + } + } +} + +// MARK: - Battery Card View +struct BatteryCardView: View { + @ObservedObject var viewModel: BatteryViewModel + let device: DeviceSpecification + @Environment(\.sizeCategory) var sizeCategory + + var body: some View { + VStack(spacing: 30) { + // MARK: Title + Text(StringConstant.batteryStatus) + .font(.title) + .fontWeight(.bold) + .accessibilityAddTraits(.isHeader) + .minimumScaleFactor(sizeCategory.customMinScaleFactor) + + // MARK: Battery Visualization + BatteryGaugeView(percentage: viewModel.batteryInfo.batteryPercentage) + + // MARK: Info Rows + VStack(alignment: .leading, spacing: 15) { + InfoRow(label: StringConstant.deviceName, value: device.deviceName) + InfoRow(label: StringConstant.Charging, value: viewModel.batteryInfo.charging ? StringConstant.batYes : StringConstant.batNo) + InfoRow(label: StringConstant.power, value: String(format: "%.2f W", viewModel.batteryInfo.powerConsumption)) + InfoRow(label: StringConstant.registeredAt, value: viewModel.batteryInfo.timestamp.toFormattedDate()) + } + } + .padding() + .frame(maxWidth: 320) + .background(Color.white) + .cornerRadius(16) + .shadow(color: Color.black.opacity(0.1), radius: 10, x: 0, y: 5) + } +} + +// MARK: - Battery Gauge View +struct BatteryGaugeView: View { + let percentage: Int + @Environment(\.sizeCategory) var sizeCategory + + private var batteryFillWidth: CGFloat { + let maxWidth: CGFloat = 190 + return maxWidth * CGFloat(percentage) / 100 + } + + private var batteryColor: Color { + switch percentage { + case 0..<20: return .red + case 20..<50: return .orange + case 50..<80: return .yellow + default: return .green + } + } + + var body: some View { + ZStack(alignment: .leading) { + // Battery Shell + RoundedRectangle(cornerRadius: 12) + .stroke(Color.gray.opacity(0.3), lineWidth: 2) + .frame(width: 200, height: 80) + .background(Color.white.opacity(0.2)) + .clipShape(RoundedRectangle(cornerRadius: 12)) + + // Fill Bar + RoundedRectangle(cornerRadius: 10) + .fill(batteryColor) + .frame(width: batteryFillWidth, height: 70) + .animation(.easeInOut(duration: 0.5), value: percentage) + .padding(.leading, 5) + + // Percentage Text + Text("\(percentage)%") + .font(.title2) + .fontWeight(.bold) + .foregroundColor(.white) + .frame(width: 200, height: 80) + .background(Color.clear) + .minimumScaleFactor(sizeCategory.customMinScaleFactor) + } + } +} + +// MARK: - Preview +#Preview { + BatteryDetailView(device: DeviceSpecification( + id: 1, + deviceId: "ff", + deviceName: "MyComputer", + manufacturer: "Dell", + model: "Inspiron 15", + processor: "Intel Core i7 2.8 GHz", + cpuPhysicalCores: 4, + cpuLogicalCores: 8, + installedRam: 16.0, + graphics: "NVIDIA GTX 1650", + operatingSystem: "Windows 10 x64", + systemType: "x64-based processor", + registeredAt: "2025-03-28T16:03:30.041384" + )) +} diff --git a/SPAR_iOS/SPAR/SPAR/View/Device/CpuUsageDetailView.swift b/SPAR_iOS/SPAR/SPAR/View/Device/CpuUsageDetailView.swift new file mode 100644 index 0000000..7005006 --- /dev/null +++ b/SPAR_iOS/SPAR/SPAR/View/Device/CpuUsageDetailView.swift @@ -0,0 +1,142 @@ +// +// CpuUsageDetailView.swift +// SPAR +// +// Created by Abhijeet Cherungottil on 4/24/25. +// + +import SwiftUI +import Charts + +// MARK: - CpuUsageDetailView +struct CpuUsageDetailView: View { + @StateObject private var viewModel: CpuUsageViewModel + let device: DeviceSpecification + @Environment(\.sizeCategory) var sizeCategory + + // MARK: - Init + init(device: DeviceSpecification) { + _viewModel = StateObject(wrappedValue: CpuUsageViewModel(device: device)) + self.device = device + } + + // MARK: - Body + var body: some View { + LoadingView(isLoading: viewModel.isLoading) { + ZStack { + // MARK: Background Gradient + LinearGradient(colors: [.orange.opacity(0.2), .yellow.opacity(0.2)], + startPoint: .topLeading, + endPoint: .bottomTrailing) + .ignoresSafeArea() + + ScrollView { + VStack(spacing: 20) { + + // MARK: Header + Text(StringConstant.cpuUsage) + .font(.largeTitle) + .bold() + .accessibilityAddTraits(.isHeader) + .minimumScaleFactor(sizeCategory.customMinScaleFactor) + + Spacer(minLength: 20) + + // MARK: Donut Chart + if let chartData = viewModel.chartData { + HalfDonutChart(chartDataObj: .constant(chartData)) + .frame(height: 180) + .transition(.scale) + } + + Spacer(minLength: 40) + + // MARK: CPU Usage Information + if let usage = viewModel.cpuUsage { + VStack(alignment: .leading, spacing: 16) { + + // MARK: General Info + InfoRow(label: StringConstant.deviceName, value: device.deviceName) + InfoRow(label: StringConstant.totalCPULoad, value: String(format: "%.1f%%", usage.totalCpuLoad)) + InfoRow(label: StringConstant.registeredAt, value: usage.timestamp.toFormattedDate()) + + Divider().padding(.vertical, 8) + + // MARK: Per Core Usage Section (Consider splitting into its own view) + Text(StringConstant.allCore) + .font(.headline) + + ForEach(usage.perCoreUsage, id: \.core) { coreUsage in + InfoRow(label: "Core \(coreUsage.core)", value: String(format: "%.1f%%", coreUsage.usage)) + } + + Divider().padding(.vertical, 8) + + // MARK: Top 5 Core Usage Graph (Consider moving to a ChartSection view) + Text(StringConstant.topFive) + .font(.headline) + .minimumScaleFactor(sizeCategory.customMinScaleFactor) + + let topCores = usage.perCoreUsage + .sorted { $0.usage > $1.usage } + .prefix(5) + + Chart { + ForEach(topCores, id: \.core) { coreUsage in + BarMark( + x: .value("Core", "Core \(coreUsage.core)"), + y: .value("Usage", coreUsage.usage) + ) + .foregroundStyle(.blue.gradient) + .accessibilityLabel("Core \(coreUsage.core)") + .accessibilityValue("\(String(format: "%.1f", coreUsage.usage)) percent usage") + .accessibilityHidden(false) + } + } + .accessibilityElement(children: .contain) + .accessibilityLabel(AccessibilityConstant.top5) + .accessibilityHint(AccessibilityConstant.barChart) + .frame(height: 200) + .cornerRadius(10) + .padding(.top, 8) + + } + .padding() + .frame(maxWidth: 360) + .background(Color.white) + .cornerRadius(16) + .shadow(color: .black.opacity(0.1), radius: 8, x: 0, y: 5) + } + + Spacer() + } + .padding() + .animation(.easeInOut, value: viewModel.cpuUsage?.totalCpuLoad) + } + } + .onAppear { + // MARK: Log Page Visit + self.logPageVisit() + } + } + } +} + +// MARK: - Preview +#Preview { + CpuUsageDetailView(device: DeviceSpecification( + id: 1, + deviceId: "dd", + deviceName: "MyComputer", + manufacturer: "Dell", + model: "Inspiron 15", + processor: "Intel Core i7 2.8 GHz", + cpuPhysicalCores: 4, + cpuLogicalCores: 8, + installedRam: 16.0, + graphics: "NVIDIA GTX 1650", + operatingSystem: "Windows 10 x64", + systemType: "x64-based processor", + registeredAt: "2025-03-28T16:03:30.041384" + )) +} diff --git a/SPAR_iOS/SPAR/SPAR/View/Device/DeviceOptions.swift b/SPAR_iOS/SPAR/SPAR/View/Device/DeviceOptions.swift new file mode 100644 index 0000000..e24bcad --- /dev/null +++ b/SPAR_iOS/SPAR/SPAR/View/Device/DeviceOptions.swift @@ -0,0 +1,158 @@ +// +// DeviceOptions.swift +// SPAR +// +// Created by Abhijeet Cherungottil on 4/23/25. +// + +import SwiftUI + +// MARK: - Main Device Options View +struct DeviceOptions: View { + @Binding var currentView: AppView + let device : DeviceSpecification + @Environment(\.sizeCategory) var sizeCategory + + var body: some View { + ZStack { + // Background color + Color.white + + ScrollView { + VStack(spacing: 30) { + + // MARK: Device Info Section + DeviceInfoCard(device: device) + + // MARK: Navigation Buttons Section + DeviceNavigationSection(device: device) + } + .padding(.vertical) + } + } + .navigationTitle(StringConstant.details) + .navigationBarTitleDisplayMode(.inline) + .onAppear { + self.logPageVisit() + } + } +} + +// MARK: - Device Info Card View +struct DeviceInfoCard: View { + let device: DeviceSpecification + @Environment(\.sizeCategory) var sizeCategory + + var body: some View { + VStack(alignment: .leading, spacing: 15) { + Text(StringConstant.deviceInfo) + .font(.largeTitle) + .fontWeight(.heavy) + .foregroundColor(.white) + .padding(.bottom, 10) + .minimumScaleFactor(sizeCategory.customMinScaleFactor) + + Group { + DeviceInfoRow(label: StringConstant.deviceName, value: device.deviceName) + DeviceInfoRow(label: StringConstant.manufacturer, value: device.manufacturer) + DeviceInfoRow(label: StringConstant.model, value: device.model) + DeviceInfoRow(label: StringConstant.processor, value: device.processor) + DeviceInfoRow(label: StringConstant.physicalCore, value: "\(device.cpuPhysicalCores)") + DeviceInfoRow(label: StringConstant.logicalCores, value: "\(device.cpuLogicalCores)") + DeviceInfoRow(label: StringConstant.RAM, value: "\(device.installedRam) GB") + DeviceInfoRow(label: StringConstant.graphics, value: device.graphics) + DeviceInfoRow(label: StringConstant.OS, value: device.operatingSystem) + DeviceInfoRow(label: StringConstant.systemType, value: device.systemType) + DeviceInfoRow(label: StringConstant.registeredAt, value: device.registeredAt.toFormattedDate()) + } + } + .padding() + .background( + RoundedRectangle(cornerRadius: 20) + .fill(Color(red: 20/255, green: 20/255, blue: 20/255)) // Charcoal dark + .overlay( + RoundedRectangle(cornerRadius: 20) + .stroke( + LinearGradient(colors: [.green, .mint], + startPoint: .topLeading, + endPoint: .bottomTrailing), + lineWidth: 2 + ) + ) + ) + .padding(.horizontal) + .shadow(color: Color.green.opacity(0.4), radius: 10) + } +} + +// MARK: - Navigation Buttons Section +struct DeviceNavigationSection: View { + let device: DeviceSpecification + + var body: some View { + VStack(spacing: 20) { + NavigationButton(title: StringConstant.batteryInfo) { + BatteryDetailView(device: device) + } + NavigationButton(title: StringConstant.cpu) { + CpuUsageDetailView(device: device) + } + NavigationButton(title: StringConstant.memoryUsage) { + MemoryUsageDetailView(device: device) + } + NavigationButton(title: StringConstant.diskUsage) { + DiskUsageDetailView(device: device) + } + NavigationButton(title: StringConstant.diskIO) { + DiskIODetailView(device: device) + } + NavigationButton(title: StringConstant.processlist) { + ProcessDetailPage(device: device) + } + } + .padding(.horizontal) + } +} + +// MARK: - Device Info Row +struct DeviceInfoRow: View { + let label: String + let value: String + @Environment(\.sizeCategory) var sizeCategory + + var body: some View { + HStack { + Text(label + ":") + .foregroundColor(.white) + .fontWeight(.semibold) + .minimumScaleFactor(sizeCategory.customMinScaleFactor) + Spacer() + Text(value) + .multilineTextAlignment(.trailing) + .foregroundColor(.cyan) + .minimumScaleFactor(sizeCategory.customMinScaleFactor) + } + .font(.system(size: 16, design: .rounded)) + } +} + +// MARK: - Preview +#Preview { + NavigationStack { + DeviceOptions(currentView: .constant(.home), device: DeviceSpecification( + id: 1, + deviceId: "fff", + deviceName: "MyComputer", + manufacturer: "Dell", + model: "Inspiron 15", + processor: "Intel Core i7 2.8 GHz", + cpuPhysicalCores: 4, + cpuLogicalCores: 8, + installedRam: 16.0, + graphics: "NVIDIA GTX 1650", + operatingSystem: "Windows 10 x64", + systemType: "x64-based processor", + registeredAt: "2025-03-28T16:03:30.041384" + )) + } +} diff --git a/SPAR_iOS/SPAR/SPAR/View/Device/DiskIODetailView.swift b/SPAR_iOS/SPAR/SPAR/View/Device/DiskIODetailView.swift new file mode 100644 index 0000000..c477a7b --- /dev/null +++ b/SPAR_iOS/SPAR/SPAR/View/Device/DiskIODetailView.swift @@ -0,0 +1,135 @@ +// +// DiskIODetailView.swift +// SPAR +// +// Created by Abhijeet Cherungottil on 4/24/25. +// + +import SwiftUI +import Charts + +// MARK: - DiskIODetailView +struct DiskIODetailView: View { + @StateObject private var viewModel: DiskIOViewModel + let device: DeviceSpecification + @Environment(\.sizeCategory) var sizeCategory + + // MARK: - Init + init(device: DeviceSpecification) { + _viewModel = StateObject(wrappedValue: DiskIOViewModel(device: device)) + self.device = device + } + + // MARK: - Body + var body: some View { + LoadingView(isLoading: viewModel.isLoading) { + ZStack { + // MARK: Background Gradient + LinearGradient(colors: [.blue.opacity(0.2), .purple.opacity(0.2)], + startPoint: .topLeading, + endPoint: .bottomTrailing) + .ignoresSafeArea() + + ScrollView { + VStack(spacing: 20) { + // MARK: Header + Text(StringConstant.diskIOUssage) + .font(.largeTitle) + .bold() + .accessibilityAddTraits(.isHeader) + .minimumScaleFactor(sizeCategory.customMinScaleFactor) + + Spacer(minLength: 20) + + // MARK: Disk IO Information Section + if let diskIO = viewModel.diskIO { + DiskIOInfoSection(diskIO: diskIO, device: device) + } + + Spacer() + } + .padding() + } + } + .onAppear { + // MARK: Log Page Visit + self.logPageVisit() + } + } + } +} + +// MARK: - DiskIOInfoSection +struct DiskIOInfoSection: View { + let diskIO: DiskIO + let device: DeviceSpecification + + var body: some View { + VStack(alignment: .leading, spacing: 16) { + // MARK: Basic Info + InfoRow(label: StringConstant.deviceName, value: device.deviceName) // Update with device value + InfoRow(label: StringConstant.RS, value: String(format: "%.1f MBps", diskIO.readSpeedMBps)) + InfoRow(label: StringConstant.WS, value: String(format: "%.1f MBps", diskIO.writeSpeedMBps)) + InfoRow(label: StringConstant.registeredAt, value: diskIO.timestamp.toFormattedDate()) + + Divider().padding(.vertical, 8) + + // MARK: Disk IO Chart + Text(StringConstant.diskIOChart) + .font(.headline) + .minimumScaleFactor(0.75) + + DiskIOChart(diskIO: diskIO) + } + .padding() + .frame(maxWidth: 360) + .background(Color.white) + .cornerRadius(16) + .shadow(color: .black.opacity(0.1), radius: 8, x: 0, y: 5) + } +} + +// MARK: - DiskIOChart +struct DiskIOChart: View { + let diskIO: DiskIO + + var body: some View { + Chart { + // MARK: Read Speed Bar + BarMark( + x: .value("Read", "Read Speed"), + y: .value("Speed", diskIO.readSpeedMBps) + ) + .foregroundStyle(.green) + + // MARK: Write Speed Bar + BarMark( + x: .value("Write", "Write Speed"), + y: .value("Speed", diskIO.writeSpeedMBps) + ) + .foregroundStyle(.red) + } + .frame(height: 200) + .cornerRadius(10) + .padding(.top, 8) + } +} + +// MARK: - Preview +#Preview { + DiskIODetailView(device: DeviceSpecification( + id: 1, + deviceId: "ff", + deviceName: "MyComputer", + manufacturer: "Dell", + model: "Inspiron 15", + processor: "Intel Core i7 2.8 GHz", + cpuPhysicalCores: 4, + cpuLogicalCores: 8, + installedRam: 16.0, + graphics: "NVIDIA GTX 1650", + operatingSystem: "Windows 10 x64", + systemType: "x64-based processor", + registeredAt: "2025-03-28T16:03:30.041384" + )) +} diff --git a/SPAR_iOS/SPAR/SPAR/View/Device/DiskUsageDetailView.swift b/SPAR_iOS/SPAR/SPAR/View/Device/DiskUsageDetailView.swift new file mode 100644 index 0000000..80b3b8e --- /dev/null +++ b/SPAR_iOS/SPAR/SPAR/View/Device/DiskUsageDetailView.swift @@ -0,0 +1,94 @@ +// +// DiskUsageDetailView.swift +// SPAR +// +// Created by Abhijeet Cherungottil on 4/24/25. +// + +import SwiftUI + +// MARK: - DiskUsageDetailView +struct DiskUsageDetailView: View { + @StateObject private var viewModel: DiskUsageViewModel + let device: DeviceSpecification + @Environment(\.sizeCategory) var sizeCategory + + // MARK: - Init + init(device: DeviceSpecification) { + _viewModel = StateObject(wrappedValue: DiskUsageViewModel(device: device)) + self.device = device + } + + // MARK: - Body + var body: some View { + LoadingView(isLoading: viewModel.isLoading) { + ZStack { + // Background Gradient + LinearGradient(colors: [.purple.opacity(0.15), .blue.opacity(0.15)], startPoint: .top, endPoint: .bottom) + .ignoresSafeArea() + + ScrollView { + VStack(spacing: 30) { + // Header + Text(StringConstant.diskUsage) + .font(.largeTitle) + .bold() + .accessibilityAddTraits(.isHeader) + .minimumScaleFactor(sizeCategory.customMinScaleFactor) + + // Disk Usage Donut Chart + HalfDonutChart(chartDataObj: $viewModel.chartData) + + // Disk Info Display + if let diskInfo = viewModel.diskUsage { + VStack(alignment: .leading, spacing: 15) { + InfoRow(label: StringConstant.deviceName, value: device.deviceName) + InfoRow(label: StringConstant.filesystem, value: diskInfo.filesystem) + InfoRow(label: StringConstant.size, value: String(format: "%.2f GB", diskInfo.sizeGB)) + InfoRow(label: StringConstant.usedSpace, value: String(format: "%.2f GB", diskInfo.usedGB)) + InfoRow(label: StringConstant.availableSpace, value: String(format: "%.2f GB", diskInfo.availableGB)) + InfoRow(label: StringConstant.registeredAt, value: diskInfo.timestamp.toFormattedDate()) + } + .padding() + .frame(maxWidth: 320) + .background(Color.white) + .cornerRadius(12) + .shadow(color: Color.black.opacity(0.1), radius: 6, x: 0, y: 4) + } else { + // Error Message + Text(viewModel.errorMessage) + .foregroundColor(.red) + .font(.body) + } + + Spacer(minLength: 20) + } + .padding() + } + } + .onAppear { + self.logPageVisit() + } + } + } + +} + +// MARK: - Preview +#Preview { + DiskUsageDetailView(device: DeviceSpecification( + id: 1, + deviceId: "ff", + deviceName: "MyComputer", + manufacturer: "Dell", + model: "Inspiron 15", + processor: "Intel Core i7 2.8 GHz", + cpuPhysicalCores: 4, + cpuLogicalCores: 8, + installedRam: 16.0, + graphics: "NVIDIA GTX 1650", + operatingSystem: "Windows 10 x64", + systemType: "x64-based processor", + registeredAt: "2025-03-28T16:03:30.041384" + )) +} diff --git a/SPAR_iOS/SPAR/SPAR/View/Device/MemoryUsageDetailView.swift b/SPAR_iOS/SPAR/SPAR/View/Device/MemoryUsageDetailView.swift new file mode 100644 index 0000000..96570f8 --- /dev/null +++ b/SPAR_iOS/SPAR/SPAR/View/Device/MemoryUsageDetailView.swift @@ -0,0 +1,97 @@ +// +// MemoryUsageDetailView.swift +// SPAR +// +// Created by Abhijeet Cherungottil on 4/23/25. +// + +import SwiftUI + +// MARK: - MemoryUsageDetailView (Main View) +struct MemoryUsageDetailView: View { + @StateObject private var viewModel: MemoryUsageViewModel + let device: DeviceSpecification + @Environment(\.sizeCategory) var sizeCategory + + init(device: DeviceSpecification) { + _viewModel = StateObject(wrappedValue: MemoryUsageViewModel(device: device)) + self.device = device + } + + var body: some View { + LoadingView(isLoading: viewModel.isLoading) { + ZStack { + // MARK: Background + LinearGradient(colors: [.mint.opacity(0.2), .cyan.opacity(0.2)], + startPoint: .top, + endPoint: .bottom) + .ignoresSafeArea() + + VStack(spacing: 30) { + // MARK: Title + Text(StringConstant.memoryUsage) + .font(.largeTitle) + .bold() + .accessibilityAddTraits(.isHeader) + .minimumScaleFactor(sizeCategory.customMinScaleFactor) + + // MARK: Memory Usage Chart + HalfDonutChart(chartDataObj: $viewModel.chartData) + + // MARK: Info Section + MemoryInfoCard(device: device, viewModel: viewModel) + + Spacer() + } + .padding() + } + .onAppear { + self.logPageVisit() + } + } + } +} + +// MARK: - Memory Info Card View +struct MemoryInfoCard: View { + let device: DeviceSpecification + @ObservedObject var viewModel: MemoryUsageViewModel + + var body: some View { + VStack(alignment: .leading, spacing: 15) { + InfoRow(label: StringConstant.deviceName, value: device.deviceName) + InfoRow(label: StringConstant.totalMemeory, + value: String(format: "%.2f GB", viewModel.memoryInfo.totalMemory)) + InfoRow(label: StringConstant.usedMemory, + value: String(format: "%.2f GB", viewModel.memoryInfo.usedMemory)) + InfoRow(label: StringConstant.availableMemory, + value: String(format: "%.2f GB", viewModel.memoryInfo.availableMemory)) + InfoRow(label: StringConstant.registeredAt, + value: viewModel.memoryInfo.timestamp.toFormattedDate()) + } + .padding() + .frame(maxWidth: 320) + .background(Color.white) + .cornerRadius(12) + .shadow(color: Color.black.opacity(0.1), radius: 6, x: 0, y: 4) + } +} + +// MARK: - Preview +#Preview { + MemoryUsageDetailView(device: DeviceSpecification( + id: 1, + deviceId: "", + deviceName: "", + manufacturer: "", + model: "", + processor: "", + cpuPhysicalCores: 4, + cpuLogicalCores: 8, + installedRam: 16.0, + graphics: "NVIDIA GTX 1650", + operatingSystem: "Windows 10 x64", + systemType: "x64-based processor", + registeredAt: "2025-03-28T16:03:30.041384" + )) +} diff --git a/SPAR_iOS/SPAR/SPAR/View/Device/ProcessDetailPage.swift b/SPAR_iOS/SPAR/SPAR/View/Device/ProcessDetailPage.swift new file mode 100644 index 0000000..c3528df --- /dev/null +++ b/SPAR_iOS/SPAR/SPAR/View/Device/ProcessDetailPage.swift @@ -0,0 +1,129 @@ +// +// ProcessDetailPage.swift +// SPAR +// +// Created by Abhijeet Cherungottil on 4/23/25. +// + +import SwiftUI +import Charts + +struct ProcessDetailPage: View { + @ObservedObject private var viewModel: ProcessViewModel + let device: DeviceSpecification + @Environment(\.sizeCategory) var sizeCategory + + init(device: DeviceSpecification) { + let viewModel = ProcessViewModel(device: device) + self.viewModel = viewModel + self.device = device + } + + @State private var showCPU = true // Toggle between CPU and Memory + + var body: some View { + LoadingView(isLoading: viewModel.isLoading) { + ScrollView { + VStack(spacing: 20) { + Text(StringConstant.processMonitor) + .font(.largeTitle) + .bold() + .padding(.top) + .minimumScaleFactor(sizeCategory.customMinScaleFactor) + + Picker(StringConstant.metric, selection: $showCPU) { + Text(StringConstant.cpuUsage).tag(true) + .minimumScaleFactor(sizeCategory.customMinScaleFactor) + Text(StringConstant.memoryUsage).tag(false) + .minimumScaleFactor(sizeCategory.customMinScaleFactor) + } + .pickerStyle(.segmented) + .padding(.horizontal) + .accessibilityLabel(AccessibilityConstant.metricPicker) + + Chart { + ForEach(viewModel.processList) { process in + BarMark( + x: .value(StringConstant.process, "\(process.name) (\(process.pid))"), + y: .value(showCPU ? StringConstant.cpu : StringConstant.memory, showCPU ? process.cpuUsage : process.memoryMB) + ) + .foregroundStyle(showCPU ? Color.blue : Color.green) + .annotation(position: .top) { + Text(String(format: "%.1f", showCPU ? process.cpuUsage : process.memoryMB)) + .font(.caption) + .foregroundColor(.gray) + .accessibilityElement(children: .ignore) // Control VoiceOver reading + .accessibilityLabel("\(process.name), process id \(process.pid)") + .accessibilityValue(showCPU + ? String(format: "%.1f percent CPU usage", process.cpuUsage) + : String(format: "%.1f megabytes memory usage", process.memoryMB) + ) + .accessibilityHint(AccessibilityConstant.processtip1) + } + } + } + .frame(height: 300) + .padding(.horizontal) + + ForEach(viewModel.processList) { process in + VStack(alignment: .leading, spacing: 8) { + HStack { + Text("PID: \(process.pid)") + .font(.headline) + .minimumScaleFactor(sizeCategory.customMinScaleFactor) + Spacer() + Text(process.name) + .fontWeight(.semibold) + .minimumScaleFactor(sizeCategory.customMinScaleFactor) + } + + HStack { + Label(String(format: "%.1f%% CPU", process.cpuUsage), systemImage: ImageConstant.cpu) + Spacer() + Label(String(format: "%.1f MB", process.memoryMB), systemImage: ImageConstant.memorychip) + } + .font(.subheadline) + .foregroundColor(.gray) + + Text("Timestamp: \(process.timestamp.toFormattedDate())") + .font(.caption) + .foregroundColor(.secondary) + .minimumScaleFactor(sizeCategory.customMinScaleFactor) + } + .padding() + .background(.white) + .cornerRadius(12) + .shadow(color: .black.opacity(0.05), radius: 5, x: 0, y: 4) + .padding(.horizontal) + } + + Spacer(minLength: 40) + } + } + .onAppear { + self.logPageVisit() + } + .background(Color(.systemGroupedBackground).ignoresSafeArea()) + } + } + + +} + +#Preview { + ProcessDetailPage(device: DeviceSpecification( + id: 1, + deviceId: "preview", + deviceName: "Preview Device", + manufacturer: "Preview Inc.", + model: "Model X", + processor: "Apple M1", + cpuPhysicalCores: 4, + cpuLogicalCores: 8, + installedRam: 16.0, + graphics: "Integrated", + operatingSystem: "macOS", + systemType: "ARM64", + registeredAt: "2025-04-01T12:00:00Z" + )) +} diff --git a/SPAR_iOS/SPAR/SPAR/View/HomeView.swift b/SPAR_iOS/SPAR/SPAR/View/HomeView.swift new file mode 100644 index 0000000..3a3e878 --- /dev/null +++ b/SPAR_iOS/SPAR/SPAR/View/HomeView.swift @@ -0,0 +1,154 @@ +// +// HomeView.swift +// SPAR +// +// Created by Abhijeet Cherungottil on 2/20/25. +// + +import SwiftUI + +struct HomeView: View { + @Binding var currentView: AppView + @StateObject private var viewModel = HomeViewModel() + @Environment(\.sizeCategory) var sizeCategory + + var body: some View { + NavigationStack { + ZStack { + BackgroundAnimationView(animate: $viewModel.animate) + + VStack(spacing: 20) { + // SPAR title + HStack { + Text(StringConstant.appName) + .fontWeight(.heavy) + .font(.system(size: 48)) + .padding(.horizontal, 15) + .minimumScaleFactor(sizeCategory.customMinScaleFactor) + Spacer() + // πŸšͺ Sign Out Button + Button(action: { + viewModel.signOut(currentView: $currentView) + }) { + Image(systemName: ImageConstant.logout) + .font(.title2) + .foregroundColor(.red) + .padding(.trailing, 10) + .accessibilityLabel(AccessibilityConstant.signOut) + } + } + + // Custom Navigation Bar + HStack { + if viewModel.isSearching { + HStack { + Image(systemName: ImageConstant.magnifyingGlass) + .accessibilityLabel(StringConstant.searchIcon) + TextField(StringConstant.searchText, text: $viewModel.searchText) + .textFieldStyle(PlainTextFieldStyle()) + .autocorrectionDisabled(true) + .submitLabel(.search) + .minimumScaleFactor(sizeCategory.customMinScaleFactor) + + Button(action: { + viewModel.cancelSearching() + }) { + Image(systemName: ImageConstant.xmarkCircleFill) + .foregroundColor(.gray) + } + } + .padding(8) + .background(Color(.systemGray5)) + .cornerRadius(10) + .transition(.move(edge: .trailing).combined(with: .opacity)) + } else { + Text(StringConstant.Dashboard) + .fontWeight(.heavy) + .font(.system(size: 28)) + .padding(.horizontal, 5) + .minimumScaleFactor(sizeCategory.customMinScaleFactor) + + Spacer() + + Button(action: { + viewModel.startSearching() + }) { + Image(systemName: ImageConstant.magnifyingGlass) + .font(.title2) + .padding(.trailing, 10) + } + } + } + .padding(.horizontal) + + // Devices List + ScrollView { + LazyVStack(spacing: 15) { + ForEach(viewModel.filteredDevices, id: \.id) { device in + // Replaced NavigationLink with NavigationButton + NavigationButton(title: device.deviceName) { + DeviceOptions(currentView: $currentView, device: device) // Pass device to next page + } + .padding(.horizontal) + } + } + .padding(.top, 10) + } + + Spacer() + } + .padding(.top) + .navigationBarHidden(true) + .onAppear { + self.logPageVisit() + viewModel.getDeviceData() + } + + // Show download popup when no devices are available + if viewModel.showDownloadPopup { + VStack { + Spacer(minLength: 200) + + // Cool empty state + VStack { + Image(systemName: ImageConstant.emptyScreenlogo) + .font(.system(size: 70)) + .foregroundColor(.blue) + .scaleEffect(1.2) + .opacity(0.8) + .animation(.easeInOut(duration: 1).repeatForever(autoreverses: true), value: viewModel.showDownloadPopup) + + Text(StringConstant.emptyscreenmsg) + .font(.system(size: 28, weight: .bold)) + .foregroundColor(.primary) + .multilineTextAlignment(.center) + .padding(.top, 20) + .opacity(0.9) + .scaleEffect(viewModel.showDownloadPopup ? 1 : 1.05) + .animation(.easeInOut(duration: 0.6).repeatForever(autoreverses: true), value: viewModel.showDownloadPopup) + + Text(StringConstant.downloadApp) + .font(.system(size: 20, weight: .semibold)) + .foregroundColor(.blue) + .underline() + .onTapGesture { + // Action to download SPAR Desktop + // You can trigger URL to download or show info here + } + .padding(.top, 10) + + Spacer() + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .padding(40) + .transition(.opacity) + } + } + } + } + } +} + +#Preview { + HomeView(currentView: .constant(.home)) +} diff --git a/SPAR_iOS/SPAR/SPAR/View/Login/BackgroundAnimationView.swift b/SPAR_iOS/SPAR/SPAR/View/Login/BackgroundAnimationView.swift new file mode 100644 index 0000000..b265b8c --- /dev/null +++ b/SPAR_iOS/SPAR/SPAR/View/Login/BackgroundAnimationView.swift @@ -0,0 +1,53 @@ +// +// BackgroundAnimationView.swift +// SPAR +// +// Created by Abhijeet Cherungottil on 4/18/25. +// + +import SwiftUI + +struct BackgroundAnimationView: View { + @Binding var animate: Bool + + var body: some View { + ZStack { + Circle() + .fill(Color.blue.opacity(0.2)) + .frame(width: 100, height: 100) + .offset(x: animate ? -140 : -160, y: animate ? -290 : -310) + .animation(Animation.easeInOut(duration: 4).repeatForever(autoreverses: true), value: animate) + + RoundedRectangle(cornerRadius: 25) + .fill(Color.purple.opacity(0.2)) + .frame(width: 200, height: 100) + .rotationEffect(.degrees(animate ? 20 : 10)) + .offset(x: animate ? 110 : 130, y: animate ? -240 : -260) + .animation(Animation.easeInOut(duration: 5).repeatForever(autoreverses: true), value: animate) + + Circle() + .fill(Color.green.opacity(0.2)) + .frame(width: 150, height: 150) + .offset(x: animate ? 140 : 160, y: animate ? 290 : 310) + .animation(Animation.easeInOut(duration: 6).repeatForever(autoreverses: true), value: animate) + + RoundedRectangle(cornerRadius: 50) + .fill(Color.orange.opacity(0.15)) + .frame(width: 120, height: 120) + .rotationEffect(.degrees(animate ? 45 : 0)) + .offset(x: animate ? -100 : -120, y: animate ? 250 : 270) + .animation(Animation.easeInOut(duration: 7).repeatForever(autoreverses: true), value: animate) + + Circle() + .fill(Color.red.opacity(0.1)) + .frame(width: 80, height: 80) + .offset(x: animate ? 80 : 100, y: animate ? 100 : 120) + .animation(Animation.easeInOut(duration: 5).repeatForever(autoreverses: true), value: animate) + } + } +} + + +#Preview { + BackgroundAnimationView(animate: .constant(true)) +} diff --git a/SPAR_iOS/SPAR/SPAR/View/Login/LoginView.swift b/SPAR_iOS/SPAR/SPAR/View/Login/LoginView.swift new file mode 100644 index 0000000..b08679f --- /dev/null +++ b/SPAR_iOS/SPAR/SPAR/View/Login/LoginView.swift @@ -0,0 +1,217 @@ +// +// LoginView.swift +// SPAR +// +// Created by Abhijeet Cherungottil on 4/18/25. +// + +import Foundation +import SwiftUI + +// MARK: - LoginView +struct LoginView: View { + @Binding var currentView: AppView + @Environment(\.sizeCategory) var sizeCategory + + @StateObject private var viewModel = LoginViewModel() + private var coordinator: LoginCoordinator + + // MARK: - Init + init(currentView: Binding) { + self._currentView = currentView + self.coordinator = LoginCoordinator(currentView: currentView) + } + + // MARK: - Body + var body: some View { + ZStack { + // MARK: Background + Color.white + .ignoresSafeArea() + + // MARK: Background Animation + BackgroundAnimationView(animate: $viewModel.animate) + + // MARK: Login Form + VStack(spacing: 30) { + LoginHeader() + UsernameField(viewModel: viewModel) + PasswordField(viewModel: viewModel) + ErrorMessage(viewModel: viewModel) + SubmitButton(viewModel: viewModel) + FaceIDButton(viewModel: viewModel) + } + .padding(40) + } + .onAppear { + viewModel.delegate = coordinator + viewModel.animate = true + self.logPageVisit() + + // If already logged in once, automatically trigger FaceID + if AppSettings.shared.hasLoggedInOnce { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + viewModel.loginWithFaceID() + } + } + } + } + + // MARK: - LoginCoordinator + private class LoginCoordinator: LoginViewModelDelegate { + @Binding var currentView: AppView + + init(currentView: Binding) { + self._currentView = currentView + } + + func didLoginSuccessfully() { + currentView = .home + } + } +} + +// MARK: - Login Header +struct LoginHeader: View { + @Environment(\.sizeCategory) var sizeCategory + + var body: some View { + VStack(spacing: 5) { + Text(StringConstant.welcomeBack) + .font(.title3) + .foregroundColor(.gray) + .minimumScaleFactor(sizeCategory.customMinScaleFactor) + + Text(StringConstant.appName) + .font(.system(size: 48, weight: .heavy)) + .foregroundColor(.blue) + .minimumScaleFactor(sizeCategory.customMinScaleFactor) + + Text(StringConstant.login) + .font(.title3) + .foregroundColor(.gray) + .minimumScaleFactor(sizeCategory.customMinScaleFactor) + } + .padding(.bottom, 20) + } +} + +// MARK: - Username Field +struct UsernameField: View { + @ObservedObject var viewModel: LoginViewModel + @Environment(\.sizeCategory) var sizeCategory + + var body: some View { + TextField(StringConstant.Username, text: $viewModel.username) + .padding() + .background(Color.gray.opacity(0.1)) + .cornerRadius(15) + .foregroundColor(.black) + .font(.title2) + .minimumScaleFactor(sizeCategory.customMinScaleFactor) + .autocapitalization(.none) + .disableAutocorrection(true) + } +} + +// MARK: - Password Field +struct PasswordField: View { + @ObservedObject var viewModel: LoginViewModel + + var body: some View { + ZStack(alignment: .trailing) { + Group { + if viewModel.showPassword { + TextField(StringConstant.Password, text: $viewModel.password) + } else { + SecureField(StringConstant.Password, text: $viewModel.password) + } + } + .padding() + .background(Color.gray.opacity(0.1)) + .cornerRadius(15) + .foregroundColor(.black) + .font(.title2) + + Button(action: { + viewModel.logger.info("\(LoggerConstant.LoginSubmitTapped)") + viewModel.showPassword.toggle() + }) { + Image(systemName: viewModel.showPassword ? ImageConstant.eyeSlash : ImageConstant.eye) + .foregroundColor(.gray) + .padding() + } + } + } +} + +// MARK: - Error Message +struct ErrorMessage: View { + @ObservedObject var viewModel: LoginViewModel + + var body: some View { + if !viewModel.errorMessage.isEmpty { + Text(viewModel.errorMessage) + .foregroundColor(.red) + .font(.body) + .minimumScaleFactor(0.75) + .padding(.top, -20) + } + } +} + +// MARK: - Submit Button +struct SubmitButton: View { + @ObservedObject var viewModel: LoginViewModel + + var body: some View { + Button(action: { + viewModel.submit() + viewModel.logger.info("\(LoggerConstant.LoginSubmitTapped)") + AppSettings.shared.hasLoggedInOnce = true // Save after first login + }) { + Text(StringConstant.submit) + .frame(maxWidth: .infinity) + .padding() + .background(Color.blue) + .foregroundColor(.white) + .font(.title2.bold()) + .cornerRadius(15) + .minimumScaleFactor(0.75) + .shadow(radius: 10) + } + .accessibilityElement(children: .ignore) + .accessibilityAddTraits(.isButton) + } +} + +// MARK: - Face ID Button +struct FaceIDButton: View { + @ObservedObject var viewModel: LoginViewModel + + var body: some View { + if AppSettings.shared.hasLoggedInOnce { + Button(action: { + viewModel.loginWithFaceID() + }) { + Image(systemName: ImageConstant.faceid) + .resizable() + .scaledToFit() + .frame(width: 50, height: 50) + .padding() + .background(Color.blue.opacity(0.5)) + .foregroundColor(.white) + .clipShape(Circle()) + } + .padding(.top, 10) + .shadow(radius: 10) + } + } +} + +// MARK: - Preview +struct LoginView_Previews: PreviewProvider { + static var previews: some View { + LoginView(currentView: .constant(.login)) + } +} diff --git a/SPAR_iOS/SPAR/SPAR/View/Onboarding/OnBoardingScreenView.swift b/SPAR_iOS/SPAR/SPAR/View/Onboarding/OnBoardingScreenView.swift new file mode 100644 index 0000000..0357772 --- /dev/null +++ b/SPAR_iOS/SPAR/SPAR/View/Onboarding/OnBoardingScreenView.swift @@ -0,0 +1,63 @@ +// +// OnBoardingScreenView.swift +// SPAR +// +// Created by Abhijeet Cherungottil on 2/20/25. +// + +import SwiftUI +import OSLog + +struct OnBoardingScreenView: View { + let onboarding: Onboarding + var showButton: Bool = false + let logger = Logger.fileLocation + @Binding var currentView: AppView + @Environment(\.sizeCategory) var sizeCategory + + + var body: some View { + VStack(spacing: 20) { + Image(onboarding.imageName) + .resizable() + .scaledToFit() + .frame(height: 300) + .accessibilityLabel(String(format: StringConstant.onboardingImages, arguments: [onboarding.imageName])) + Text(onboarding.title) + .font(.largeTitle) + .fontWeight(.bold) + .multilineTextAlignment(.center) + .minimumScaleFactor(sizeCategory.customMinScaleFactor) + + Text(onboarding.description) + .font(.body) + .multilineTextAlignment(.center) + .padding(.horizontal, 40) + .minimumScaleFactor(sizeCategory.customMinScaleFactor) + + if showButton { + Button(action: { + // Change to home view when button is tapped + AppSettings.shared.hasCompletedOnboarding = true + currentView = .login + logger.info("\(LoggerConstant.getStartedTapped)") + }) { + Text(StringConstant.getstarted) + .font(.headline) + .foregroundColor(.white) + .padding() + .frame(maxWidth: .infinity) + .background(Color.blue) + .cornerRadius(10) + .padding(.horizontal, 40) + .minimumScaleFactor(sizeCategory.customMinScaleFactor) + } + } + } + } +} + +#Preview { + OnBoardingScreenView(onboarding: Onboarding(imageName: ImageConstant.welcomeImage, title: StringConstant.welcomeTitle, description: StringConstant.welcomeDescription), currentView: .constant(.onboarding)) +} + diff --git a/SPAR_iOS/SPAR/SPAR/View/Onboarding/OnboardingView.swift b/SPAR_iOS/SPAR/SPAR/View/Onboarding/OnboardingView.swift new file mode 100644 index 0000000..24a07c7 --- /dev/null +++ b/SPAR_iOS/SPAR/SPAR/View/Onboarding/OnboardingView.swift @@ -0,0 +1,30 @@ +// +// OnboardingView.swift +// SPAR +// +// Created by Abhijeet Cherungottil on 2/20/25. +// + +import SwiftUI +struct OnboardingView: View { + @Binding var currentView: AppView + + var body: some View { + TabView { + OnBoardingScreenView(onboarding: Onboarding(imageName: ImageConstant.welcomeImage, title: StringConstant.welcomeTitle, description: StringConstant.welcomeDescription), currentView: $currentView) + OnBoardingScreenView(onboarding: Onboarding(imageName: ImageConstant.stopwatchImage, title: StringConstant.cpuTrackingTitle, description: StringConstant.cpuTrackingDescription), currentView: $currentView) + OnBoardingScreenView(onboarding: Onboarding(imageName: ImageConstant.mobileNotificationImage, title: StringConstant.alertsTitle, description: StringConstant.alertsDescription), currentView: $currentView) + OnBoardingScreenView(onboarding: Onboarding(imageName: ImageConstant.laptopGraphImage, title: StringConstant.desktopRequirementTitle, description: StringConstant.desktopRequirementDescription), showButton: true, currentView: $currentView) + } + .tabViewStyle(.page(indexDisplayMode: .always)) + .indexViewStyle(.page(backgroundDisplayMode: .always)) + .onAppear{ + self.logPageVisit() + } + } +} + + +#Preview { + OnboardingView(currentView: .constant(.onboarding)) +} diff --git a/SPAR_iOS/SPAR/SPAR/View/SplashScreenView.swift b/SPAR_iOS/SPAR/SPAR/View/SplashScreenView.swift new file mode 100644 index 0000000..f1d1730 --- /dev/null +++ b/SPAR_iOS/SPAR/SPAR/View/SplashScreenView.swift @@ -0,0 +1,36 @@ +// +// SplashScreenView.swift +// SPAR +// +// Created by Abhijeet Cherungottil on 2/20/25. +// + +import SwiftUI + +struct SplashScreenView: View { + @Binding var currentView: AppView + + var body: some View { + VStack { + Image(ImageConstant.logo) + .resizable() + .scaledToFit() + .frame(height: 500) + } + .onAppear { + DispatchQueue.main.asyncAfter(deadline: .now() + 2) { + withAnimation { + if AppSettings.shared.hasCompletedOnboarding { + currentView = .login + } else { + currentView = .onboarding + } + } + } + } + } +} + +#Preview { + SplashScreenView(currentView: .constant(.splash)) +} diff --git a/SPAR_iOS/SPAR/SPAR/ViewModel/BatteryViewModel.swift b/SPAR_iOS/SPAR/SPAR/ViewModel/BatteryViewModel.swift new file mode 100644 index 0000000..e47e0bf --- /dev/null +++ b/SPAR_iOS/SPAR/SPAR/ViewModel/BatteryViewModel.swift @@ -0,0 +1,59 @@ +// +// BatteryViewModel.swift +// SPAR +// +// Created by Abhijeet Cherungottil on 4/23/25. +// + +import Foundation + +class BatteryViewModel: ObservableObject { + @Published var batteryInfo: BatteryInfo + private let networkManager = NetworkManager() + @Published var isLoading = false + + + init(device: DeviceSpecification) { + // 1. Set a placeholder batteryInfo first + self.batteryInfo = BatteryInfo( + id: 0, + userId: 1, + hasBattery: true, + batteryPercentage: 0, deviceId: "", + powerConsumption: 0, + timestamp: "", + charging: false + ) + + // 2. Then fetch actual data from API + fetchBatteryInfo(device: device) + } + + func fetchBatteryInfo(device: DeviceSpecification) { + Task { + guard let userId = AppSettings.shared.userId else { return } + + // Update isLoading on the main thread + await MainActor.run { + self.isLoading = true + } + + do { + let response = try await networkManager.fetchBatteryInfo(for: userId, deviceId: device.deviceId) + + // Publish on the main thread + await MainActor.run { + self.batteryInfo = response + self.isLoading = false + } + + } catch { + print("Failed to fetch battery info: \(error)") + await MainActor.run { + self.isLoading = false + } + } + } + } + +} diff --git a/SPAR_iOS/SPAR/SPAR/ViewModel/ChartDataContainer.swift b/SPAR_iOS/SPAR/SPAR/ViewModel/ChartDataContainer.swift new file mode 100644 index 0000000..622384a --- /dev/null +++ b/SPAR_iOS/SPAR/SPAR/ViewModel/ChartDataContainer.swift @@ -0,0 +1,16 @@ +// +// ChartDataContainer.swift +// SPAR +// +// Created by Abhijeet Cherungottil on 3/15/25. +// + +import SwiftUI + +class ChartDataContainer: ObservableObject { + @Published var cpuData: ChartData = ChartData(color: Color(#colorLiteral(red: 1, green: 0.493, blue: 0.474, alpha: 1)), type: "CPU", percent: 30) + @Published var diskData: ChartData = ChartData(color: Color(.green), type: "Disk", percent: 85) + @Published var MemooryData: ChartData = ChartData(color: Color(#colorLiteral(red: 0.2196078449, green: 0.007843137719, blue: 0.8549019694, alpha: 1)), type: "Memory", percent: 90) + + +} diff --git a/SPAR_iOS/SPAR/SPAR/ViewModel/CpuUsageViewModel.swift b/SPAR_iOS/SPAR/SPAR/ViewModel/CpuUsageViewModel.swift new file mode 100644 index 0000000..fa0df3b --- /dev/null +++ b/SPAR_iOS/SPAR/SPAR/ViewModel/CpuUsageViewModel.swift @@ -0,0 +1,88 @@ +// +// CpuUsageViewModel.swift +// SPAR +// +// Created by Abhijeet Cherungottil on 4/24/25. +// + +import Foundation +import SwiftUI +import OSLog + +class CpuUsageViewModel: ObservableObject { + @Published var cpuUsage: CpuUsage? + @Published var errorMessage: String = "" + @Published var chartData: ChartData? + @Published var isLoading: Bool = false + private let logger = Logger.fileLocation + private let networkManager = NetworkManager() + + init(device: DeviceSpecification) { + // Sample data + let sampleCoreData:[CpuCoreUsage] = [] // Empty array as JSON string + + let sampleUsage = CpuUsage( + id: 7, + totalCpuLoad: 0, + perCoreUsage: sampleCoreData, + userId: 1, + deviceId: "1", + timestamp: "1" + ) + + self.cpuUsage = sampleUsage + + self.chartData = ChartData( + color: .orange, + type: "CPU", + percent: CGFloat(sampleUsage.totalCpuLoad) + ) + + // Call fetchCPUInfo to load real data + fetchCPUInfo(device: device) + } + + func fetchCPUInfo(device: DeviceSpecification) { + print("Fetching CPU info for device: \(device.id)") + + Task { + await MainActor.run { + self.isLoading = true + } + + do { + guard let userId = AppSettings.shared.userId else { + print("User ID is nil. Aborting fetch.") + await MainActor.run { + self.isLoading = false + } + return + } + + print("User ID: \(userId)") + let response = try await networkManager.fetchCPUUsageInfo(for: userId, deviceId: device.deviceId) + print("response", response) + + await MainActor.run { + print("Received CPU usage data: \(response)") + self.cpuUsage = response + self.chartData = ChartData( + color: Color.purple, + type: "CPU", + percent: CGFloat(response.totalCpuLoad) + ) + self.isLoading = false + } + + } catch { + print("Failed to fetch CPU info: \(error)") + await MainActor.run { + self.errorMessage = "Failed to fetch CPU info: \(error.localizedDescription)" + self.isLoading = false + } + } + } + } + + +} diff --git a/SPAR_iOS/SPAR/SPAR/ViewModel/DiskIOViewModel.swift b/SPAR_iOS/SPAR/SPAR/ViewModel/DiskIOViewModel.swift new file mode 100644 index 0000000..8f23e73 --- /dev/null +++ b/SPAR_iOS/SPAR/SPAR/ViewModel/DiskIOViewModel.swift @@ -0,0 +1,65 @@ +// +// DiskIOViewModel.swift +// SPAR +// +// Created by Abhijeet Cherungottil on 4/24/25. +// + +import Foundation +import OSLog + +class DiskIOViewModel: ObservableObject { + @Published var diskIO: DiskIO? + @Published var errorMessage: String = "" + @Published var isLoading: Bool = false // βœ… Add isLoading + private let networkManager = NetworkManager() + + private let logger = Logger.fileLocation + + init(device: DeviceSpecification) { + // Sample static data for testing purposes + let sampleDiskIO = DiskIO( + id: 5, + readSpeedMBps: 0, + writeSpeedMBps: 0, + userId: 1, + deviceId: "1", + timestamp: "".toFormattedDate() + ) + self.diskIO = sampleDiskIO + fetchDiskIOInfo(device: device) + } + + // Fetch Disk I/O Data from the API + func fetchDiskIOInfo(device: DeviceSpecification) { + Task { + await MainActor.run { + self.isLoading = true // βœ… Start loading + } + + do { + guard let userId = AppSettings.shared.userId else { + await MainActor.run { + self.isLoading = false + self.errorMessage = "User ID not found" + } + return + } + + let response = try await networkManager.fetchDiskIO(for: userId, deviceId: device.deviceId) + + await MainActor.run { + self.diskIO = response + self.isLoading = false // βœ… Stop loading + } + + } catch { + print("Failed to fetch Disk IO info: \(error)") + await MainActor.run { + self.errorMessage = "Failed to load Disk IO info" + self.isLoading = false // βœ… Stop loading on error + } + } + } + } +} diff --git a/SPAR_iOS/SPAR/SPAR/ViewModel/DiskUsageViewModel.swift b/SPAR_iOS/SPAR/SPAR/ViewModel/DiskUsageViewModel.swift new file mode 100644 index 0000000..a83df24 --- /dev/null +++ b/SPAR_iOS/SPAR/SPAR/ViewModel/DiskUsageViewModel.swift @@ -0,0 +1,109 @@ +// +// DiskUsageViewModel.swift +// SPAR +// +// Created by Abhijeet Cherungottil on 4/24/25. +// + +import Foundation +import OSLog +import SwiftUI + +class DiskUsageViewModel: ObservableObject { + @Published var diskUsage: DiskUsage? + @Published var errorMessage: String = "" + @Published var chartData: ChartData = ChartData(color: .gray, type: "Disk", percent: 0) + @Published var isLoading: Bool = false + + private let networkManager = NetworkManager() + + + private let logger = Logger.fileLocation + + init(device: DeviceSpecification) { + let diskInfo = DiskUsage( + id: 1, + filesystem: "", + sizeGB: 1, + usedGB: 0, + availableGB: 0, + userId: 1, + deviceId: "1", + timestamp: "".toFormattedDate() + ) + + self.diskUsage = diskInfo + + let usedPercent = (diskInfo.usedGB / diskInfo.sizeGB) * 100 + self.chartData = ChartData( + color: .green, + type: "Disk", + percent: CGFloat(usedPercent) + ) + + fetchDiskUsageInfo(device: device) + } + + func fetchDiskUsageInfo(device: DeviceSpecification) { + Task { + await MainActor.run { + self.isLoading = true + } + + do { + guard let userId = AppSettings.shared.userId else { + await MainActor.run { + self.isLoading = false + } + return + } + + let response = try await networkManager.fetchDiskUsage(for: userId, deviceId: device.deviceId) + + await MainActor.run { + self.diskUsage = response + let usedPercent = (response.usedGB / response.sizeGB) * 100 + self.chartData = ChartData( + color: .green, + type: "Disk", + percent: CGFloat(usedPercent) + ) + self.isLoading = false + } + } catch { + await MainActor.run { + self.errorMessage = "Failed to load disk usage" + self.isLoading = false + } + logger.error("Failed to fetch Disk Usage info: \(error.localizedDescription)") + } + } + } + +} + + + + +// func fetchDiskUsage(userId: Int, deviceId: String) async { +// guard let url = URL(string: "\(Constants.baseURL)/api/metrics/disk-usage/\(userId)/\(deviceId)") else { +// self.errorMessage = "Invalid URL" +// return +// } +// +// do { +// let (data, _) = try await URLSession.shared.data(from: url) +// let decoder = JSONDecoder() +// let usage = try decoder.decode(DiskUsage.self, from: data) +// +// DispatchQueue.main.async { +// self.diskUsage = usage +// } +// } catch { +// DispatchQueue.main.async { +// self.logger.error("Failed to fetch disk usage: \(error.localizedDescription)") +// self.errorMessage = "Failed to load disk usage" +// } +// } +// } + diff --git a/SPAR_iOS/SPAR/SPAR/ViewModel/HomeViewModel.swift b/SPAR_iOS/SPAR/SPAR/ViewModel/HomeViewModel.swift new file mode 100644 index 0000000..dbf0108 --- /dev/null +++ b/SPAR_iOS/SPAR/SPAR/ViewModel/HomeViewModel.swift @@ -0,0 +1,89 @@ +// +// HomeViewModel.swift +// SPAR +// +// Created by Abhijeet Cherungottil on 4/18/25. +// + +import Foundation +import SwiftUI + +class HomeViewModel: ObservableObject { + @Published var isSearching = false + @Published var searchText = "" + @Published var animate: Bool = false + private let networkManager = NetworkManager() + @Published var devices: [DeviceSpecification] = [] + @Published var showDownloadPopup: Bool = false // Add this property to control the popup visibility + + init() { + getDeviceData() + } + + var filteredDevices: [DeviceSpecification] { + if searchText.isEmpty { + return devices + } else { + return devices.filter { + $0.deviceName.localizedCaseInsensitiveContains(searchText) + } + } + } + + func signOut(currentView: Binding) { + AppSettings.shared.clearUserSession() + DispatchQueue.main.async { + currentView.wrappedValue = .login // Redirect to login screen + } + } + + + func getDeviceData() { + Task { + do { + guard let id = AppSettings.shared.userId else {return } + let response = try await networkManager.fetchDeviceSpecifications(for: id) + print(response) + DispatchQueue.main.async { + self.devices = response + // Check if the response is empty, and if so, show the download popup + self.showDownloadPopup = response.isEmpty + } + + } catch { + print("⚠️ Decoding error: \(error)") + + } + } + } + + func startSearching() { + withAnimation { + isSearching = true + } + } + + func cancelSearching() { + withAnimation { + isSearching = false + searchText = "" + } + } +} + + +//DeviceSpecification( +// id: 1, +// userId: "User123", +// deviceName: "MyComputer", +// manufacturer: "Dell", +// model: "Inspiron 15", +// processor: "Intel Core i7 2.8 GHz", +// cpuPhysicalCores: 4, +// cpuLogicalCores: 8, +// installedRam: 16.0, +// graphics: "NVIDIA GTX 1650", +// operatingSystem: "Windows 10 x64", +// systemType: "x64-based processor", +// registeredAt: "2025-03-28T16:03:30.041384" +//) diff --git a/SPAR_iOS/SPAR/SPAR/ViewModel/LoginViewModel.swift b/SPAR_iOS/SPAR/SPAR/ViewModel/LoginViewModel.swift new file mode 100644 index 0000000..ee28e5d --- /dev/null +++ b/SPAR_iOS/SPAR/SPAR/ViewModel/LoginViewModel.swift @@ -0,0 +1,132 @@ +// +// LoginViewModel.swift +// SPAR +// +// Created by Abhijeet Cherungottil on 4/18/25. +// + +import Foundation +import SwiftUI +import OSLog +import LocalAuthentication + + +class LoginViewModel: ObservableObject { + @Published var username: String = "" + @Published var password: String = "" + @Published var animate: Bool = false + @Published var showPassword: Bool = false + @Published var errorMessage: String = "" + + let logger = Logger.fileLocation + private let networkManager = NetworkManager() + + weak var delegate: LoginViewModelDelegate? + + func loginWithFaceID() { + if ProcessInfo.processInfo.arguments.contains("UITestMode") { + print("Skipping Face ID for UI Tests") + return + } + authenticateWithFaceIDIfAvailable { success in + if success { + guard let usernameData = KeychainHelper.read(service: "SPAR", account: "username"), + let passwordData = KeychainHelper.read(service: "SPAR", account: "password"), + let savedUsername = String(data: usernameData, encoding: .utf8), + let savedPassword = String(data: passwordData, encoding: .utf8) else { + self.errorMessage = "No saved credentials found." + return + } + + Task { + do { + let response = try await self.networkManager.login(username: savedUsername, password: savedPassword) + + AppSettings.shared.authToken = response.token + AppSettings.shared.userId = response.userId + + DispatchQueue.main.async { + self.delegate?.didLoginSuccessfully() + } + } catch { + DispatchQueue.main.async { + self.errorMessage = StringConstant.incorrectCredentials + } + } + } + } else { + self.errorMessage = "Face ID Authentication Failed." + } + } + } + + + func submit() { + let isValidUsername = username.range(of: "^[a-zA-Z0-9]+$", options: .regularExpression) != nil + let disallowedCharacters = CharacterSet(charactersIn: "\"'`;/\\<>") + let isPasswordSafe = password.rangeOfCharacter(from: disallowedCharacters) == nil + + if !isValidUsername { + errorMessage = "Username can only contain letters and numbers." + return + } + + if !isPasswordSafe { + errorMessage = "Password contains unsafe characters like \", ', ;, or \\." + return + } + + + Task { + do { + let response = try await networkManager.login(username: username, password: password) + + AppSettings.shared.authToken = response.token + AppSettings.shared.userId = response.userId + // Save credentials securely + KeychainHelper.save(Data(username.utf8), service: "SPAR", account: "username") + KeychainHelper.save(Data(password.utf8), service: "SPAR", account: "password") + + + DispatchQueue.main.async { + self.delegate?.didLoginSuccessfully() + } + } catch { + DispatchQueue.main.async { + self.errorMessage = StringConstant.incorrectCredentials + } + } + } + } +} + +extension LoginViewModel { + func authenticateWithFaceIDIfAvailable(completion: @escaping (Bool) -> Void) { + #if targetEnvironment(simulator) + // Skip authentication on Simulator + print("Skipping Face ID authentication in Simulator.") + completion(true) // Treat as successful authentication + #else + let context = LAContext() + var error: NSError? + + if context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) { + context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: "Log in with Face ID") { success, authenticationError in + DispatchQueue.main.async { + completion(success) + } + } + } else { + DispatchQueue.main.async { + completion(false) + } + } + #endif + } + +} + + +protocol LoginViewModelDelegate: AnyObject { + func didLoginSuccessfully() +} diff --git a/SPAR_iOS/SPAR/SPAR/ViewModel/MemoryUsageViewModel.swift b/SPAR_iOS/SPAR/SPAR/ViewModel/MemoryUsageViewModel.swift new file mode 100644 index 0000000..49677e3 --- /dev/null +++ b/SPAR_iOS/SPAR/SPAR/ViewModel/MemoryUsageViewModel.swift @@ -0,0 +1,70 @@ +// +// MemoryUsageViewModel.swift +// SPAR +// +// Created by Abhijeet Cherungottil on 4/23/25. +// + +import SwiftUI + +class MemoryUsageViewModel: ObservableObject { + @Published var memoryInfo: MemoryUsage + @Published var chartData: ChartData + @Published var isLoading = false + + private let networkManager = NetworkManager() + + init(device: DeviceSpecification) { + // Default placeholder + let memoryInfo = MemoryUsage( + id: 1, + userId: 1, + deviceId: "", + totalMemory: 1, + usedMemory: 0.0, + availableMemory: 0.0, + timestamp: "".toFormattedDate() + ) + self.memoryInfo = memoryInfo + + let usedPercent = (memoryInfo.usedMemory / memoryInfo.totalMemory) * 100 + self.chartData = ChartData( + color: Color.purple, + type: "Memory", + percent: CGFloat(usedPercent) + ) + + fetchRamInfo(device: device) + } + + func fetchRamInfo(device: DeviceSpecification) { + Task { + guard let userId = AppSettings.shared.userId else { return } + + await MainActor.run { + self.isLoading = true + } + + do { + let response = try await networkManager.fetchMemoryUsage(for: userId, deviceId: device.deviceId) + + await MainActor.run { + self.memoryInfo = response + let usedPercent = (response.usedMemory / response.totalMemory) * 100 + self.chartData = ChartData( + color: Color.purple, + type: "Memory", + percent: CGFloat(usedPercent) + ) + self.isLoading = false + } + + } catch { + print("Failed to fetch memory info: \(error)") + await MainActor.run { + self.isLoading = false + } + } + } + } +} diff --git a/SPAR_iOS/SPAR/SPAR/ViewModel/ProcessViewModel.swift b/SPAR_iOS/SPAR/SPAR/ViewModel/ProcessViewModel.swift new file mode 100644 index 0000000..ccf109a --- /dev/null +++ b/SPAR_iOS/SPAR/SPAR/ViewModel/ProcessViewModel.swift @@ -0,0 +1,42 @@ +// +// ProcessViewModel.swift +// SPAR +// +// Created by Abhijeet Cherungottil on 4/23/25. +// + +import Foundation + +class ProcessViewModel: ObservableObject { + @Published var processList: [ProcessStatus] + private let networkManager = NetworkManager() + @Published var isLoading = false + + init(device: DeviceSpecification) { + processList = [] + fetchProcessInfo(device: device) + } + + func fetchProcessInfo(device: DeviceSpecification) { + Task { + do { + DispatchQueue.main.async { + self.isLoading = true + + } + defer { isLoading = false } + guard let userId = AppSettings.shared.userId else { return } + let response = try await networkManager.fetchProcessStatus(for: userId, deviceId: device.deviceId) + + DispatchQueue.main.async { + self.processList = response + self.isLoading = false + } + } catch { + print("Failed to fetch Process info: \(error)") + } + } + } + + +} diff --git a/SPAR_iOS/SPAR/SPARTests/LaunchTest.swift b/SPAR_iOS/SPAR/SPARTests/LaunchTest.swift new file mode 100644 index 0000000..9a091ed --- /dev/null +++ b/SPAR_iOS/SPAR/SPARTests/LaunchTest.swift @@ -0,0 +1,16 @@ +import XCTest + +final class LaunchTest: XCTestCase { + let app = XCUIApplication() + + override func setUp() { + continueAfterFailure = false + app.launchArguments += ["-UITestMode", "-resetDefaults"] + app.launch() + } + + func testSplashToOnboardingTransition() { + let onboardingTitle = app.staticTexts["\(StringConstant.welcomeTitle)"] + XCTAssertTrue(onboardingTitle.waitForExistence(timeout: 3)) + } +} diff --git a/SPAR_iOS/SPAR/SPARTests/LoginViewUITests.swift b/SPAR_iOS/SPAR/SPARTests/LoginViewUITests.swift new file mode 100644 index 0000000..5314600 --- /dev/null +++ b/SPAR_iOS/SPAR/SPARTests/LoginViewUITests.swift @@ -0,0 +1,92 @@ +// +// LoginViewUITests.swift +// SPARTests +// +// Created by Abhijeet Cherungottil on 4/20/25. +// + +import XCTest + +final class LoginViewUITests: XCTestCase { + + private var app: XCUIApplication! + + override func setUpWithError() throws { + continueAfterFailure = false + app = XCUIApplication() + app.launchArguments += ["-UITestMode", "-resetDefaults"] + app.launch() + } + + override func tearDownWithError() throws { + app = nil + } + + func testLoginWithWrongPassword() throws { + // Assuming you are starting at LoginView + while !app.buttons[StringConstant.getstarted].exists { + app.swipeLeft() + } + + let getStartedButton = app.buttons[StringConstant.getstarted] + getStartedButton.tap() + + let usernameField = app.textFields["Username"] + let passwordField = app.secureTextFields["Password"] + + XCTAssertTrue(usernameField.exists) + XCTAssertTrue(passwordField.exists) + + usernameField.tap() + usernameField.typeText("testuse\"r") + + passwordField.tap() + passwordField.typeText("wrongpassword") + + app.buttons["Submit"].tap() + + // Wait for the error message to appear + let errorMessage = app.staticTexts["Username can only contain letters and numbers."] + let exists = NSPredicate(format: "exists == 1") + + expectation(for: exists, evaluatedWith: errorMessage, handler: nil) + waitForExpectations(timeout: 5) // <-- YOU NEED THIS + XCTAssertTrue(errorMessage.exists) + } + + func testLoginWithCorrectPassword() throws { + // Assuming correct login changes view, maybe to HomeView or similar + while !app.buttons[StringConstant.getstarted].exists { + app.swipeLeft() + } + + + let getStartedButton = app.buttons[StringConstant.getstarted] + getStartedButton.tap() + + let usernameField = app.textFields["Username"] + let passwordField = app.secureTextFields["Password"] + + XCTAssertTrue(usernameField.exists) + XCTAssertTrue(passwordField.exists) + + usernameField.tap() + usernameField.typeText("user") + + passwordField.tap() + passwordField.typeText("Password") + + app.buttons["Submit"].tap() + + // After successful login, check for an element that appears ONLY after login + // Example: a "Welcome" label on HomeView + + let homeTitle = app.staticTexts["S.P.A.R"] + let exists = NSPredicate(format: "exists == 1") + + expectation(for: exists, evaluatedWith: homeTitle, handler: nil) + waitForExpectations(timeout: 5) + XCTAssertTrue(homeTitle.exists) + } +} + diff --git a/SPAR_iOS/SPAR/SPARTests/NetworkManagerTests.swift b/SPAR_iOS/SPAR/SPARTests/NetworkManagerTests.swift new file mode 100644 index 0000000..64f9fde --- /dev/null +++ b/SPAR_iOS/SPAR/SPARTests/NetworkManagerTests.swift @@ -0,0 +1,54 @@ +import XCTest +@testable import SPAR + +final class NetworkManagerTests: XCTestCase { + + var networkManager: NetworkManager! + + override func setUp() { + super.setUp() + networkManager = NetworkManager(networkService: MockNetworkService()) + AppSettings.shared.authToken = "dummyToken" // Set a dummy token + } + + override func tearDown() { + networkManager = nil + super.tearDown() + } + + + + func testFetchProcessStatus() async throws { + let processes = try await networkManager.fetchProcessStatus(for: 1, deviceId: "hh") + XCTAssertEqual(processes.count, 2) + XCTAssertEqual(processes.first?.name, "chrome.exe") + } + + func testFetchBatteryInfo() async throws { + let batteryInfo = try await networkManager.fetchBatteryInfo(for: 1, deviceId: "5") + XCTAssertTrue(batteryInfo.hasBattery) + XCTAssertEqual(batteryInfo.batteryPercentage, 85) + } + + func testFetchMemoryUsage() async throws { + let memoryUsage = try await networkManager.fetchMemoryUsage(for: 1, deviceId: "5") + XCTAssertEqual(memoryUsage.totalMemory, 16.0) + XCTAssertEqual(memoryUsage.usedMemory, 8.5) + } + + func testFetchDiskUsage() async throws { + let diskUsage = try await networkManager.fetchDiskUsage(for: 1, deviceId: "5") + XCTAssertEqual(diskUsage.sizeGB, 512.0) + } + + func testFetchDiskIO() async throws { + let diskIO = try await networkManager.fetchDiskIO(for: 1, deviceId: "5") + XCTAssertEqual(diskIO.readSpeedMBps, 120.0) + XCTAssertEqual(diskIO.writeSpeedMBps, 80.0) + } + + func testLogin() async throws { + let loginResponse = try await networkManager.login(username: "testUser", password: "testPassword") + XCTAssertNotNil(loginResponse.token) + } +} diff --git a/SPAR_iOS/SPAR/SPARTests/OnboardingTests.swift b/SPAR_iOS/SPAR/SPARTests/OnboardingTests.swift new file mode 100644 index 0000000..6669b32 --- /dev/null +++ b/SPAR_iOS/SPAR/SPARTests/OnboardingTests.swift @@ -0,0 +1,33 @@ +// +// OnboardingTests.swift +// SPARTests +// +// Created by Abhijeet Cherungottil on 4/17/25. +// + +import XCTest + +final class OnboardingTests: XCTestCase { + let app = XCUIApplication() + + override func setUp() { + continueAfterFailure = false + app.launchArguments += ["-UITestMode", "-resetDefaults"] + app.launch() + + } + + + func testGetStartedNavigatesToHome() { + while !app.buttons[StringConstant.getstarted].exists { + app.swipeLeft() + } + + + + let getStartedButton = app.buttons[StringConstant.getstarted] + getStartedButton.tap() + + XCTAssertTrue(app.staticTexts[StringConstant.welcomeBack].waitForExistence(timeout: 5)) + } +} diff --git a/SPAR_iOS/SPAR/SPARTests/SPARTests.swift b/SPAR_iOS/SPAR/SPARTests/SPARTests.swift new file mode 100644 index 0000000..cbefe08 --- /dev/null +++ b/SPAR_iOS/SPAR/SPARTests/SPARTests.swift @@ -0,0 +1,26 @@ +// +// SPARTests.swift +// SPARTests +// +// Created by Abhijeet Cherungottil on 4/17/25. +// + +import XCTest + +final class SPARTests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + + // In UI tests it is usually best to stop immediately when a failure occurs. + continueAfterFailure = false + + // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + +}