├── MotionTracking
├── Assets.xcassets
│ ├── Contents.json
│ ├── AppIcon.appiconset
│ │ ├── Logo-motion-traking-20.png
│ │ ├── Logo-motion-traking-29.png
│ │ ├── Logo-motion-traking-40.png
│ │ ├── Logo-motion-traking-76.png
│ │ ├── Logo-motion-traking-1024.png
│ │ ├── Logo-motion-traking-20@2x.png
│ │ ├── Logo-motion-traking-20@3x.png
│ │ ├── Logo-motion-traking-29@2x.png
│ │ ├── Logo-motion-traking-29@3x.png
│ │ ├── Logo-motion-traking-40@2x.png
│ │ ├── Logo-motion-traking-40@3x.png
│ │ ├── Logo-motion-traking-60@2x.png
│ │ ├── Logo-motion-traking-60@3x.png
│ │ ├── Logo-motion-traking-76@2x.png
│ │ ├── Logo-motion-traking-83.5@2x.png
│ │ └── Contents.json
│ └── AccentColor.colorset
│ │ └── Contents.json
├── FilesList
│ ├── View
│ │ └── FilesListTableViewCell.swift
│ ├── FilesListViewModel.swift
│ └── FilesListViewController.swift
├── Info.plist
├── ExportFile
│ ├── Data
│ │ └── FieldsMapper.swift
│ ├── ExportFileViewModel.swift
│ └── ExportFileViewController.swift
├── Common
│ ├── Base
│ │ ├── BaseViewModel.swift
│ │ ├── BaseTableViewController.swift
│ │ └── BaseViewController.swift
│ ├── Extention
│ │ └── Date+Extention.swift
│ └── Manager
│ │ ├── FileTrackingManager.swift
│ │ └── CSVFileManager.swift
├── AppDelegate.swift
├── SceneDelegate.swift
└── Base.lproj
│ └── LaunchScreen.storyboard
├── Screenshots
├── screenshot-iphone-1.png
├── screenshot-iphone-2.png
├── screenshot-applewatch-1.png
├── screenshot-applewatch-2.png
├── screenshot-applewatch-3.png
└── screenshot-applewatch-4.png
├── MotionTracking WatchKit App
├── Assets.xcassets
│ ├── Contents.json
│ ├── AppIcon.appiconset
│ │ ├── Logo-motion-traking-24@2x.png
│ │ ├── Logo-motion-traking-29@2x.png
│ │ ├── Logo-motion-traking-29@3x.png
│ │ ├── Logo-motion-traking-33@2x.png
│ │ ├── Logo-motion-traking-40@2x.png
│ │ ├── Logo-motion-traking-44@2x.png
│ │ ├── Logo-motion-traking-46@2x.png
│ │ ├── Logo-motion-traking-50@2x.png
│ │ ├── Logo-motion-traking-51@2x.png
│ │ ├── Logo-motion-traking-86@2x.png
│ │ ├── Logo-motion-traking-98@2x.png
│ │ ├── Logo-motion-traking-1024@1x.png
│ │ ├── Logo-motion-traking-108@2x.png
│ │ ├── Logo-motion-traking-117@2x.png
│ │ ├── Logo-motion-traking-27-5@2x.png
│ │ └── Contents.json
│ └── AccentColor.colorset
│ │ └── Contents.json
└── Base.lproj
│ └── Interface.storyboard
├── MotionTracking WatchKit Extension
├── Assets.xcassets
│ ├── Contents.json
│ └── Complication.complicationset
│ │ ├── Graphic Bezel.imageset
│ │ └── Contents.json
│ │ ├── Graphic Circular.imageset
│ │ └── Contents.json
│ │ ├── Graphic Corner.imageset
│ │ └── Contents.json
│ │ ├── Graphic Large Rectangular.imageset
│ │ └── Contents.json
│ │ ├── Circular.imageset
│ │ └── Contents.json
│ │ ├── Extra Large.imageset
│ │ └── Contents.json
│ │ ├── Modular.imageset
│ │ └── Contents.json
│ │ ├── Utilitarian.imageset
│ │ └── Contents.json
│ │ ├── Graphic Extra Large.imageset
│ │ └── Contents.json
│ │ └── Contents.json
├── Ressources
│ └── spinner
│ │ ├── spinner1.png
│ │ ├── spinner2.png
│ │ ├── spinner3.png
│ │ ├── spinner4.png
│ │ ├── spinner5.png
│ │ ├── spinner6.png
│ │ ├── spinner7.png
│ │ ├── spinner8.png
│ │ ├── spinner1@2x.png
│ │ ├── spinner1@3x.png
│ │ ├── spinner2@2x.png
│ │ ├── spinner2@3x.png
│ │ ├── spinner3@2x.png
│ │ ├── spinner3@3x.png
│ │ ├── spinner4@2x.png
│ │ ├── spinner4@3x.png
│ │ ├── spinner5@2x.png
│ │ ├── spinner5@3x.png
│ │ ├── spinner6@2x.png
│ │ ├── spinner6@3x.png
│ │ ├── spinner7@2x.png
│ │ ├── spinner7@3x.png
│ │ ├── spinner8@2x.png
│ │ └── spinner8@3x.png
├── MotionTracking WatchKit Extension.entitlements
├── MotionTracking WatchKit ExtensionRelease.entitlements
├── Common
│ ├── Utils
│ │ ├── ErrorApp.swift
│ │ ├── Constants.swift
│ │ ├── CurrentSession.swift
│ │ └── Parameters.swift
│ ├── Extention
│ │ ├── WKInterfaceSwitch+Extention.swift
│ │ ├── WKInterfaceLabel+Extention.swift
│ │ ├── WKInterfaceSlider+Extention.swift
│ │ ├── WKInterfacePicker+Extention.swift
│ │ ├── InterfaceImage+Extention.swift
│ │ └── WKInterfaceTimer+Extention.swift
│ ├── Base
│ │ ├── BaseViewModel.swift
│ │ └── BaseInterfaceController.swift
│ └── Manager
│ │ ├── ConnectivityManager.swift
│ │ └── MotionManager.swift
├── Tracking
│ ├── ReadyViewModel.swift
│ ├── ResultViewModel.swift
│ ├── ReadyInterfaceController.swift
│ ├── ResultInterfaceController.swift
│ ├── TrackingViewModel.swift
│ └── TrackingInterfaceController.swift
├── Info.plist
├── PushNotificationPayload.apns
├── NotificationController.swift
├── Settings
│ ├── SettingsViewModel.swift
│ └── SettingsInterfaceController.swift
├── Home
│ ├── HomeViewModel.swift
│ └── HomeInterfaceController.swift
├── ComplicationController.swift
└── ExtensionDelegate.swift
├── MotionTracking.xcodeproj
└── project.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ └── IDEWorkspaceChecks.plist
├── MotionTracking-WatchKit-App-Info.plist
├── MotionTracking.xcworkspace
├── contents.xcworkspacedata
└── xcshareddata
│ └── IDEWorkspaceChecks.plist
├── Podfile.lock
├── Podfile
├── MotionTrackingUITests
├── MotionTrackingUITestsLaunchTests.swift
└── MotionTrackingUITests.swift
├── MotionTracking WatchKit AppUITests
├── MotionTracking_WatchKit_AppUITestsLaunchTests.swift
└── MotionTracking_WatchKit_AppUITests.swift
├── LICENSE
├── MotionTrackingTests
├── MotionTrackingTests.swift
└── ExportFileViewModelTests.swift
├── MotionTracking WatchKit AppTests
└── MotionTracking_WatchKit_AppTests.swift
├── README.md
└── .gitignore
/MotionTracking/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Screenshots/screenshot-iphone-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/k-angama/MotionTracking/HEAD/Screenshots/screenshot-iphone-1.png
--------------------------------------------------------------------------------
/Screenshots/screenshot-iphone-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/k-angama/MotionTracking/HEAD/Screenshots/screenshot-iphone-2.png
--------------------------------------------------------------------------------
/Screenshots/screenshot-applewatch-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/k-angama/MotionTracking/HEAD/Screenshots/screenshot-applewatch-1.png
--------------------------------------------------------------------------------
/Screenshots/screenshot-applewatch-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/k-angama/MotionTracking/HEAD/Screenshots/screenshot-applewatch-2.png
--------------------------------------------------------------------------------
/Screenshots/screenshot-applewatch-3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/k-angama/MotionTracking/HEAD/Screenshots/screenshot-applewatch-3.png
--------------------------------------------------------------------------------
/Screenshots/screenshot-applewatch-4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/k-angama/MotionTracking/HEAD/Screenshots/screenshot-applewatch-4.png
--------------------------------------------------------------------------------
/MotionTracking WatchKit App/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/MotionTracking WatchKit Extension/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/MotionTracking WatchKit Extension/Ressources/spinner/spinner1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/k-angama/MotionTracking/HEAD/MotionTracking WatchKit Extension/Ressources/spinner/spinner1.png
--------------------------------------------------------------------------------
/MotionTracking WatchKit Extension/Ressources/spinner/spinner2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/k-angama/MotionTracking/HEAD/MotionTracking WatchKit Extension/Ressources/spinner/spinner2.png
--------------------------------------------------------------------------------
/MotionTracking WatchKit Extension/Ressources/spinner/spinner3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/k-angama/MotionTracking/HEAD/MotionTracking WatchKit Extension/Ressources/spinner/spinner3.png
--------------------------------------------------------------------------------
/MotionTracking WatchKit Extension/Ressources/spinner/spinner4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/k-angama/MotionTracking/HEAD/MotionTracking WatchKit Extension/Ressources/spinner/spinner4.png
--------------------------------------------------------------------------------
/MotionTracking WatchKit Extension/Ressources/spinner/spinner5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/k-angama/MotionTracking/HEAD/MotionTracking WatchKit Extension/Ressources/spinner/spinner5.png
--------------------------------------------------------------------------------
/MotionTracking WatchKit Extension/Ressources/spinner/spinner6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/k-angama/MotionTracking/HEAD/MotionTracking WatchKit Extension/Ressources/spinner/spinner6.png
--------------------------------------------------------------------------------
/MotionTracking WatchKit Extension/Ressources/spinner/spinner7.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/k-angama/MotionTracking/HEAD/MotionTracking WatchKit Extension/Ressources/spinner/spinner7.png
--------------------------------------------------------------------------------
/MotionTracking WatchKit Extension/Ressources/spinner/spinner8.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/k-angama/MotionTracking/HEAD/MotionTracking WatchKit Extension/Ressources/spinner/spinner8.png
--------------------------------------------------------------------------------
/MotionTracking WatchKit Extension/Ressources/spinner/spinner1@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/k-angama/MotionTracking/HEAD/MotionTracking WatchKit Extension/Ressources/spinner/spinner1@2x.png
--------------------------------------------------------------------------------
/MotionTracking WatchKit Extension/Ressources/spinner/spinner1@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/k-angama/MotionTracking/HEAD/MotionTracking WatchKit Extension/Ressources/spinner/spinner1@3x.png
--------------------------------------------------------------------------------
/MotionTracking WatchKit Extension/Ressources/spinner/spinner2@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/k-angama/MotionTracking/HEAD/MotionTracking WatchKit Extension/Ressources/spinner/spinner2@2x.png
--------------------------------------------------------------------------------
/MotionTracking WatchKit Extension/Ressources/spinner/spinner2@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/k-angama/MotionTracking/HEAD/MotionTracking WatchKit Extension/Ressources/spinner/spinner2@3x.png
--------------------------------------------------------------------------------
/MotionTracking WatchKit Extension/Ressources/spinner/spinner3@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/k-angama/MotionTracking/HEAD/MotionTracking WatchKit Extension/Ressources/spinner/spinner3@2x.png
--------------------------------------------------------------------------------
/MotionTracking WatchKit Extension/Ressources/spinner/spinner3@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/k-angama/MotionTracking/HEAD/MotionTracking WatchKit Extension/Ressources/spinner/spinner3@3x.png
--------------------------------------------------------------------------------
/MotionTracking WatchKit Extension/Ressources/spinner/spinner4@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/k-angama/MotionTracking/HEAD/MotionTracking WatchKit Extension/Ressources/spinner/spinner4@2x.png
--------------------------------------------------------------------------------
/MotionTracking WatchKit Extension/Ressources/spinner/spinner4@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/k-angama/MotionTracking/HEAD/MotionTracking WatchKit Extension/Ressources/spinner/spinner4@3x.png
--------------------------------------------------------------------------------
/MotionTracking WatchKit Extension/Ressources/spinner/spinner5@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/k-angama/MotionTracking/HEAD/MotionTracking WatchKit Extension/Ressources/spinner/spinner5@2x.png
--------------------------------------------------------------------------------
/MotionTracking WatchKit Extension/Ressources/spinner/spinner5@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/k-angama/MotionTracking/HEAD/MotionTracking WatchKit Extension/Ressources/spinner/spinner5@3x.png
--------------------------------------------------------------------------------
/MotionTracking WatchKit Extension/Ressources/spinner/spinner6@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/k-angama/MotionTracking/HEAD/MotionTracking WatchKit Extension/Ressources/spinner/spinner6@2x.png
--------------------------------------------------------------------------------
/MotionTracking WatchKit Extension/Ressources/spinner/spinner6@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/k-angama/MotionTracking/HEAD/MotionTracking WatchKit Extension/Ressources/spinner/spinner6@3x.png
--------------------------------------------------------------------------------
/MotionTracking WatchKit Extension/Ressources/spinner/spinner7@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/k-angama/MotionTracking/HEAD/MotionTracking WatchKit Extension/Ressources/spinner/spinner7@2x.png
--------------------------------------------------------------------------------
/MotionTracking WatchKit Extension/Ressources/spinner/spinner7@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/k-angama/MotionTracking/HEAD/MotionTracking WatchKit Extension/Ressources/spinner/spinner7@3x.png
--------------------------------------------------------------------------------
/MotionTracking WatchKit Extension/Ressources/spinner/spinner8@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/k-angama/MotionTracking/HEAD/MotionTracking WatchKit Extension/Ressources/spinner/spinner8@2x.png
--------------------------------------------------------------------------------
/MotionTracking WatchKit Extension/Ressources/spinner/spinner8@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/k-angama/MotionTracking/HEAD/MotionTracking WatchKit Extension/Ressources/spinner/spinner8@3x.png
--------------------------------------------------------------------------------
/MotionTracking/Assets.xcassets/AppIcon.appiconset/Logo-motion-traking-20.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/k-angama/MotionTracking/HEAD/MotionTracking/Assets.xcassets/AppIcon.appiconset/Logo-motion-traking-20.png
--------------------------------------------------------------------------------
/MotionTracking/Assets.xcassets/AppIcon.appiconset/Logo-motion-traking-29.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/k-angama/MotionTracking/HEAD/MotionTracking/Assets.xcassets/AppIcon.appiconset/Logo-motion-traking-29.png
--------------------------------------------------------------------------------
/MotionTracking/Assets.xcassets/AppIcon.appiconset/Logo-motion-traking-40.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/k-angama/MotionTracking/HEAD/MotionTracking/Assets.xcassets/AppIcon.appiconset/Logo-motion-traking-40.png
--------------------------------------------------------------------------------
/MotionTracking/Assets.xcassets/AppIcon.appiconset/Logo-motion-traking-76.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/k-angama/MotionTracking/HEAD/MotionTracking/Assets.xcassets/AppIcon.appiconset/Logo-motion-traking-76.png
--------------------------------------------------------------------------------
/MotionTracking/Assets.xcassets/AppIcon.appiconset/Logo-motion-traking-1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/k-angama/MotionTracking/HEAD/MotionTracking/Assets.xcassets/AppIcon.appiconset/Logo-motion-traking-1024.png
--------------------------------------------------------------------------------
/MotionTracking/Assets.xcassets/AppIcon.appiconset/Logo-motion-traking-20@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/k-angama/MotionTracking/HEAD/MotionTracking/Assets.xcassets/AppIcon.appiconset/Logo-motion-traking-20@2x.png
--------------------------------------------------------------------------------
/MotionTracking/Assets.xcassets/AppIcon.appiconset/Logo-motion-traking-20@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/k-angama/MotionTracking/HEAD/MotionTracking/Assets.xcassets/AppIcon.appiconset/Logo-motion-traking-20@3x.png
--------------------------------------------------------------------------------
/MotionTracking/Assets.xcassets/AppIcon.appiconset/Logo-motion-traking-29@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/k-angama/MotionTracking/HEAD/MotionTracking/Assets.xcassets/AppIcon.appiconset/Logo-motion-traking-29@2x.png
--------------------------------------------------------------------------------
/MotionTracking/Assets.xcassets/AppIcon.appiconset/Logo-motion-traking-29@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/k-angama/MotionTracking/HEAD/MotionTracking/Assets.xcassets/AppIcon.appiconset/Logo-motion-traking-29@3x.png
--------------------------------------------------------------------------------
/MotionTracking/Assets.xcassets/AppIcon.appiconset/Logo-motion-traking-40@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/k-angama/MotionTracking/HEAD/MotionTracking/Assets.xcassets/AppIcon.appiconset/Logo-motion-traking-40@2x.png
--------------------------------------------------------------------------------
/MotionTracking/Assets.xcassets/AppIcon.appiconset/Logo-motion-traking-40@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/k-angama/MotionTracking/HEAD/MotionTracking/Assets.xcassets/AppIcon.appiconset/Logo-motion-traking-40@3x.png
--------------------------------------------------------------------------------
/MotionTracking/Assets.xcassets/AppIcon.appiconset/Logo-motion-traking-60@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/k-angama/MotionTracking/HEAD/MotionTracking/Assets.xcassets/AppIcon.appiconset/Logo-motion-traking-60@2x.png
--------------------------------------------------------------------------------
/MotionTracking/Assets.xcassets/AppIcon.appiconset/Logo-motion-traking-60@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/k-angama/MotionTracking/HEAD/MotionTracking/Assets.xcassets/AppIcon.appiconset/Logo-motion-traking-60@3x.png
--------------------------------------------------------------------------------
/MotionTracking/Assets.xcassets/AppIcon.appiconset/Logo-motion-traking-76@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/k-angama/MotionTracking/HEAD/MotionTracking/Assets.xcassets/AppIcon.appiconset/Logo-motion-traking-76@2x.png
--------------------------------------------------------------------------------
/MotionTracking/Assets.xcassets/AppIcon.appiconset/Logo-motion-traking-83.5@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/k-angama/MotionTracking/HEAD/MotionTracking/Assets.xcassets/AppIcon.appiconset/Logo-motion-traking-83.5@2x.png
--------------------------------------------------------------------------------
/MotionTracking.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/MotionTracking WatchKit App/Assets.xcassets/AppIcon.appiconset/Logo-motion-traking-24@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/k-angama/MotionTracking/HEAD/MotionTracking WatchKit App/Assets.xcassets/AppIcon.appiconset/Logo-motion-traking-24@2x.png
--------------------------------------------------------------------------------
/MotionTracking WatchKit App/Assets.xcassets/AppIcon.appiconset/Logo-motion-traking-29@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/k-angama/MotionTracking/HEAD/MotionTracking WatchKit App/Assets.xcassets/AppIcon.appiconset/Logo-motion-traking-29@2x.png
--------------------------------------------------------------------------------
/MotionTracking WatchKit App/Assets.xcassets/AppIcon.appiconset/Logo-motion-traking-29@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/k-angama/MotionTracking/HEAD/MotionTracking WatchKit App/Assets.xcassets/AppIcon.appiconset/Logo-motion-traking-29@3x.png
--------------------------------------------------------------------------------
/MotionTracking WatchKit App/Assets.xcassets/AppIcon.appiconset/Logo-motion-traking-33@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/k-angama/MotionTracking/HEAD/MotionTracking WatchKit App/Assets.xcassets/AppIcon.appiconset/Logo-motion-traking-33@2x.png
--------------------------------------------------------------------------------
/MotionTracking WatchKit App/Assets.xcassets/AppIcon.appiconset/Logo-motion-traking-40@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/k-angama/MotionTracking/HEAD/MotionTracking WatchKit App/Assets.xcassets/AppIcon.appiconset/Logo-motion-traking-40@2x.png
--------------------------------------------------------------------------------
/MotionTracking WatchKit App/Assets.xcassets/AppIcon.appiconset/Logo-motion-traking-44@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/k-angama/MotionTracking/HEAD/MotionTracking WatchKit App/Assets.xcassets/AppIcon.appiconset/Logo-motion-traking-44@2x.png
--------------------------------------------------------------------------------
/MotionTracking WatchKit App/Assets.xcassets/AppIcon.appiconset/Logo-motion-traking-46@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/k-angama/MotionTracking/HEAD/MotionTracking WatchKit App/Assets.xcassets/AppIcon.appiconset/Logo-motion-traking-46@2x.png
--------------------------------------------------------------------------------
/MotionTracking WatchKit App/Assets.xcassets/AppIcon.appiconset/Logo-motion-traking-50@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/k-angama/MotionTracking/HEAD/MotionTracking WatchKit App/Assets.xcassets/AppIcon.appiconset/Logo-motion-traking-50@2x.png
--------------------------------------------------------------------------------
/MotionTracking WatchKit App/Assets.xcassets/AppIcon.appiconset/Logo-motion-traking-51@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/k-angama/MotionTracking/HEAD/MotionTracking WatchKit App/Assets.xcassets/AppIcon.appiconset/Logo-motion-traking-51@2x.png
--------------------------------------------------------------------------------
/MotionTracking WatchKit App/Assets.xcassets/AppIcon.appiconset/Logo-motion-traking-86@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/k-angama/MotionTracking/HEAD/MotionTracking WatchKit App/Assets.xcassets/AppIcon.appiconset/Logo-motion-traking-86@2x.png
--------------------------------------------------------------------------------
/MotionTracking WatchKit App/Assets.xcassets/AppIcon.appiconset/Logo-motion-traking-98@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/k-angama/MotionTracking/HEAD/MotionTracking WatchKit App/Assets.xcassets/AppIcon.appiconset/Logo-motion-traking-98@2x.png
--------------------------------------------------------------------------------
/MotionTracking-WatchKit-App-Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/MotionTracking WatchKit App/Assets.xcassets/AppIcon.appiconset/Logo-motion-traking-1024@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/k-angama/MotionTracking/HEAD/MotionTracking WatchKit App/Assets.xcassets/AppIcon.appiconset/Logo-motion-traking-1024@1x.png
--------------------------------------------------------------------------------
/MotionTracking WatchKit App/Assets.xcassets/AppIcon.appiconset/Logo-motion-traking-108@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/k-angama/MotionTracking/HEAD/MotionTracking WatchKit App/Assets.xcassets/AppIcon.appiconset/Logo-motion-traking-108@2x.png
--------------------------------------------------------------------------------
/MotionTracking WatchKit App/Assets.xcassets/AppIcon.appiconset/Logo-motion-traking-117@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/k-angama/MotionTracking/HEAD/MotionTracking WatchKit App/Assets.xcassets/AppIcon.appiconset/Logo-motion-traking-117@2x.png
--------------------------------------------------------------------------------
/MotionTracking WatchKit App/Assets.xcassets/AppIcon.appiconset/Logo-motion-traking-27-5@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/k-angama/MotionTracking/HEAD/MotionTracking WatchKit App/Assets.xcassets/AppIcon.appiconset/Logo-motion-traking-27-5@2x.png
--------------------------------------------------------------------------------
/MotionTracking WatchKit Extension/MotionTracking WatchKit Extension.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/MotionTracking WatchKit Extension/MotionTracking WatchKit ExtensionRelease.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/MotionTracking.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/MotionTracking.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/MotionTracking.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/MotionTracking/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "platform" : "universal",
6 | "reference" : "systemRedColor"
7 | },
8 | "idiom" : "universal"
9 | }
10 | ],
11 | "info" : {
12 | "author" : "xcode",
13 | "version" : 1
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/MotionTracking WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Bezel.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "watch",
5 | "scale" : "2x"
6 | },
7 | {
8 | "idiom" : "watch",
9 | "scale" : "2x",
10 | "screen-width" : ">183"
11 | }
12 | ],
13 | "info" : {
14 | "author" : "xcode",
15 | "version" : 1
16 | },
17 | "properties" : {
18 | "auto-scaling" : "auto"
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/MotionTracking WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Circular.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "watch",
5 | "scale" : "2x"
6 | },
7 | {
8 | "idiom" : "watch",
9 | "scale" : "2x",
10 | "screen-width" : ">183"
11 | }
12 | ],
13 | "info" : {
14 | "author" : "xcode",
15 | "version" : 1
16 | },
17 | "properties" : {
18 | "auto-scaling" : "auto"
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/MotionTracking WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Corner.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "watch",
5 | "scale" : "2x"
6 | },
7 | {
8 | "idiom" : "watch",
9 | "scale" : "2x",
10 | "screen-width" : ">183"
11 | }
12 | ],
13 | "info" : {
14 | "author" : "xcode",
15 | "version" : 1
16 | },
17 | "properties" : {
18 | "auto-scaling" : "auto"
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/MotionTracking WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Large Rectangular.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "watch",
5 | "scale" : "2x"
6 | },
7 | {
8 | "idiom" : "watch",
9 | "scale" : "2x",
10 | "screen-width" : ">183"
11 | }
12 | ],
13 | "info" : {
14 | "author" : "xcode",
15 | "version" : 1
16 | },
17 | "properties" : {
18 | "auto-scaling" : "auto"
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/MotionTracking WatchKit App/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "display-p3",
6 | "components" : {
7 | "alpha" : "1.000",
8 | "blue" : "0.314",
9 | "green" : "0.274",
10 | "red" : "0.700"
11 | }
12 | },
13 | "idiom" : "universal"
14 | }
15 | ],
16 | "info" : {
17 | "author" : "xcode",
18 | "version" : 1
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/MotionTracking WatchKit Extension/Common/Utils/ErrorApp.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ErrorApp.swift
3 | // MotionTracking WatchKit Extension
4 | //
5 | // Created by Karim Angama on 01/02/2022.
6 | //
7 |
8 | import Foundation
9 |
10 | struct ErrorApp: Error {
11 |
12 | let title: String?
13 | let description: String?
14 |
15 | init(title: String? = nil, description: String? = nil) {
16 | self.title = title
17 | self.description = description
18 | }
19 |
20 | }
21 |
--------------------------------------------------------------------------------
/Podfile.lock:
--------------------------------------------------------------------------------
1 | PODS:
2 | - SSZipArchive (2.5.3)
3 | - SwiftCSVExport (2.6.0)
4 |
5 | DEPENDENCIES:
6 | - SSZipArchive (= 2.5.3)
7 | - SwiftCSVExport (= 2.6.0)
8 |
9 | SPEC REPOS:
10 | trunk:
11 | - SSZipArchive
12 | - SwiftCSVExport
13 |
14 | SPEC CHECKSUMS:
15 | SSZipArchive: d3ea9f5e15234229a8d7e164c2789a82b7c19440
16 | SwiftCSVExport: d9db3fb80484115028792f8d539445e3fb2c0ecb
17 |
18 | PODFILE CHECKSUM: 73cf4d3697669326df9a63004ccf13146d3c1eb0
19 |
20 | COCOAPODS: 1.12.1
21 |
--------------------------------------------------------------------------------
/MotionTracking WatchKit Extension/Common/Utils/Constants.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Constants.swift
3 | // MotionTracking WatchKit Extension
4 | //
5 | // Created by Karim Angama on 01/02/2022.
6 | //
7 |
8 | import Foundation
9 |
10 | struct Constants {
11 |
12 | // MARK: Configuration
13 |
14 | struct Configuration {
15 |
16 | static let initialMotionInterval: Double = 50
17 | static let initialTimer: Double = 5.0
18 | static let timeRange = Array(1...60)
19 | }
20 |
21 | }
22 |
--------------------------------------------------------------------------------
/MotionTracking WatchKit Extension/Tracking/ReadyViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ReadyViewModel.swift
3 | // MotionTracking WatchKit Extension
4 | //
5 | // Created by Karim Angama on 02/02/2022.
6 | //
7 |
8 | import Foundation
9 |
10 | class ReadyViewModel: BaseViewModel {
11 |
12 | var motionManager: MotionManager?
13 |
14 | override func context(object: Any?) {
15 | motionManager = object as? MotionManager
16 |
17 | // Start the location for start tracking in background
18 | motionManager?.startLocation()
19 | }
20 |
21 | }
22 |
--------------------------------------------------------------------------------
/MotionTracking WatchKit Extension/Assets.xcassets/Complication.complicationset/Circular.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "watch",
5 | "scale" : "2x"
6 | },
7 | {
8 | "idiom" : "watch",
9 | "scale" : "2x",
10 | "screen-width" : "<=145"
11 | },
12 | {
13 | "idiom" : "watch",
14 | "scale" : "2x",
15 | "screen-width" : ">183"
16 | }
17 | ],
18 | "info" : {
19 | "author" : "xcode",
20 | "version" : 1
21 | },
22 | "properties" : {
23 | "auto-scaling" : "auto"
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/MotionTracking WatchKit Extension/Assets.xcassets/Complication.complicationset/Extra Large.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "watch",
5 | "scale" : "2x"
6 | },
7 | {
8 | "idiom" : "watch",
9 | "scale" : "2x",
10 | "screen-width" : "<=145"
11 | },
12 | {
13 | "idiom" : "watch",
14 | "scale" : "2x",
15 | "screen-width" : ">183"
16 | }
17 | ],
18 | "info" : {
19 | "author" : "xcode",
20 | "version" : 1
21 | },
22 | "properties" : {
23 | "auto-scaling" : "auto"
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/MotionTracking WatchKit Extension/Assets.xcassets/Complication.complicationset/Modular.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "watch",
5 | "scale" : "2x"
6 | },
7 | {
8 | "idiom" : "watch",
9 | "scale" : "2x",
10 | "screen-width" : "<=145"
11 | },
12 | {
13 | "idiom" : "watch",
14 | "scale" : "2x",
15 | "screen-width" : ">183"
16 | }
17 | ],
18 | "info" : {
19 | "author" : "xcode",
20 | "version" : 1
21 | },
22 | "properties" : {
23 | "auto-scaling" : "auto"
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/MotionTracking WatchKit Extension/Assets.xcassets/Complication.complicationset/Utilitarian.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "watch",
5 | "scale" : "2x"
6 | },
7 | {
8 | "idiom" : "watch",
9 | "scale" : "2x",
10 | "screen-width" : "<=145"
11 | },
12 | {
13 | "idiom" : "watch",
14 | "scale" : "2x",
15 | "screen-width" : ">183"
16 | }
17 | ],
18 | "info" : {
19 | "author" : "xcode",
20 | "version" : 1
21 | },
22 | "properties" : {
23 | "auto-scaling" : "auto"
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/MotionTracking WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Extra Large.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "watch",
5 | "scale" : "2x"
6 | },
7 | {
8 | "idiom" : "watch",
9 | "scale" : "2x",
10 | "screen-width" : "<=145"
11 | },
12 | {
13 | "idiom" : "watch",
14 | "scale" : "2x",
15 | "screen-width" : ">183"
16 | }
17 | ],
18 | "info" : {
19 | "author" : "xcode",
20 | "version" : 1
21 | },
22 | "properties" : {
23 | "auto-scaling" : "auto"
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/MotionTracking WatchKit Extension/Common/Utils/CurrentSession.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CurrentSession.swift
3 | // MotionTracking WatchKit Extension
4 | //
5 | // Created by Karim Angama on 02/02/2022.
6 | //
7 |
8 | import Foundation
9 |
10 | class CurrentSession {
11 |
12 | private(set) static var numberFileSending = 0
13 | private(set) static var numberFileError = 0
14 |
15 | static func addNumberFileSending() -> Int {
16 | numberFileSending = numberFileSending + 1
17 | return numberFileSending
18 | }
19 |
20 | static func addNumberFileError() -> Int {
21 | numberFileError = numberFileError + 1
22 | return numberFileError
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/MotionTracking WatchKit Extension/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | NSExtension
6 |
7 | NSExtensionAttributes
8 |
9 | WKAppBundleIdentifier
10 | com.kangama.MotionTracking.watchkitapp
11 |
12 | NSExtensionPointIdentifier
13 | com.apple.watchkit
14 |
15 | UIBackgroundModes
16 |
17 | location
18 |
19 | WKBackgroundModes
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/MotionTracking WatchKit Extension/Common/Extention/WKInterfaceSwitch+Extention.swift:
--------------------------------------------------------------------------------
1 | //
2 | // WKInterfaceSwitch+Extention.swift
3 | // MotionTracking WatchKit Extension
4 | //
5 | // Created by Karim Angama on 02/02/2022.
6 | //
7 |
8 | import Foundation
9 | import WatchKit
10 |
11 | extension WKInterfaceSwitch {
12 |
13 | private static var _on = [String:Bool]()
14 | var on: Bool {
15 | get {
16 | let tmpAddress = String(format: "%p", unsafeBitCast(self, to: Int.self))
17 | return WKInterfaceSwitch._on[tmpAddress] ?? false
18 | }
19 | set {
20 | setOn(newValue)
21 | let tmpAddress = String(format: "%p", unsafeBitCast(self, to: Int.self))
22 | WKInterfaceSwitch._on[tmpAddress] = newValue
23 | }
24 | }
25 |
26 | }
27 |
--------------------------------------------------------------------------------
/MotionTracking WatchKit Extension/Common/Extention/WKInterfaceLabel+Extention.swift:
--------------------------------------------------------------------------------
1 | //
2 | // WKInterfaceLabel+Extention.swift
3 | // MotionTracking WatchKit Extension
4 | //
5 | // Created by Karim Angama on 01/02/2022.
6 | //
7 |
8 | import Foundation
9 | import WatchKit
10 |
11 | extension WKInterfaceLabel {
12 |
13 | private static var _text = [String:String]()
14 | var text: String {
15 | get {
16 | let tmpAddress = String(format: "%p", unsafeBitCast(self, to: Int.self))
17 | return WKInterfaceLabel._text[tmpAddress] ?? ""
18 | }
19 | set {
20 | setText(newValue)
21 | let tmpAddress = String(format: "%p", unsafeBitCast(self, to: Int.self))
22 | WKInterfaceLabel._text[tmpAddress] = newValue
23 | }
24 | }
25 |
26 | }
27 |
--------------------------------------------------------------------------------
/MotionTracking WatchKit Extension/Common/Extention/WKInterfaceSlider+Extention.swift:
--------------------------------------------------------------------------------
1 | //
2 | // WKInterfaceSlider+Extention.swift
3 | // MotionTracking WatchKit Extension
4 | //
5 | // Created by Karim Angama on 02/02/2022.
6 | //
7 |
8 | import Foundation
9 | import WatchKit
10 |
11 | extension WKInterfaceSlider {
12 |
13 | private static var _text = [String:Float]()
14 | var value: Float {
15 | get {
16 | let tmpAddress = String(format: "%p", unsafeBitCast(self, to: Int.self))
17 | return WKInterfaceSlider._text[tmpAddress] ?? 0.0
18 | }
19 | set {
20 | setValue(newValue)
21 | let tmpAddress = String(format: "%p", unsafeBitCast(self, to: Int.self))
22 | WKInterfaceSlider._text[tmpAddress] = newValue
23 | }
24 | }
25 |
26 | }
27 |
--------------------------------------------------------------------------------
/MotionTracking WatchKit Extension/Common/Extention/WKInterfacePicker+Extention.swift:
--------------------------------------------------------------------------------
1 | //
2 | // WKInterfacePicker+Extention.swift
3 | // MotionTracking WatchKit Extension
4 | //
5 | // Created by Karim Angama on 02/02/2022.
6 | //
7 |
8 | import Foundation
9 | import WatchKit
10 |
11 | extension WKInterfacePicker {
12 |
13 | private static var _selectedItemIndex = [String:Int]()
14 | var selectedItemIndex: Int {
15 | get {
16 | let tmpAddress = String(format: "%p", unsafeBitCast(self, to: Int.self))
17 | return WKInterfacePicker._selectedItemIndex[tmpAddress] ?? 0
18 | }
19 | set {
20 | setSelectedItemIndex(newValue)
21 | let tmpAddress = String(format: "%p", unsafeBitCast(self, to: Int.self))
22 | WKInterfacePicker._selectedItemIndex[tmpAddress] = newValue
23 | }
24 | }
25 |
26 | }
27 |
--------------------------------------------------------------------------------
/MotionTracking/FilesList/View/FilesListTableViewCell.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FilesListTableViewCell.swift
3 | // MotionTracking
4 | //
5 | // Created by Karim Angama on 04/02/2022.
6 | //
7 |
8 | import Foundation
9 | import UIKit
10 |
11 | class FilesListTableViewCell:UITableViewCell {
12 |
13 | func configuration(entity: FileTrackingEntity) {
14 | var content = self.defaultContentConfiguration()
15 | content.text = entity.date.mediumDate
16 | content.secondaryText = entity.information
17 | content.image = imageCell(entity.isLocation)
18 | self.contentConfiguration = content
19 | }
20 |
21 | private func imageCell(_ isLocation: Bool) -> UIImage? {
22 | isLocation
23 | ?
24 | UIImage(systemName: "location.square", withConfiguration: nil)
25 | :
26 | UIImage(systemName: "doc", withConfiguration: nil)
27 | }
28 |
29 | }
30 |
--------------------------------------------------------------------------------
/MotionTracking/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | UIApplicationSceneManifest
6 |
7 | UIApplicationSupportsMultipleScenes
8 |
9 | UISceneConfigurations
10 |
11 | UIWindowSceneSessionRoleApplication
12 |
13 |
14 | UISceneConfigurationName
15 | Default Configuration
16 | UISceneDelegateClassName
17 | $(PRODUCT_MODULE_NAME).SceneDelegate
18 | UISceneStoryboardFile
19 | Main
20 |
21 |
22 |
23 |
24 | UIBackgroundModes
25 |
26 | processing
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/Podfile:
--------------------------------------------------------------------------------
1 | # Uncomment the next line to define a global platform for your project
2 | platform :ios, '15.5'
3 |
4 | target 'MotionTracking' do
5 | # Comment the next line if you don't want to use dynamic frameworks
6 | use_frameworks!
7 |
8 | # Pods for MotionTracking
9 | pod 'SwiftCSVExport' , '= 2.6.0'
10 | pod 'SSZipArchive', '2.5.3'
11 |
12 | target 'MotionTrackingTests' do
13 | inherit! :search_paths
14 | # Pods for testing
15 | end
16 |
17 | target 'MotionTrackingUITests' do
18 | # Pods for testing
19 | end
20 |
21 | end
22 |
23 | target 'MotionTracking WatchKit App' do
24 | # Comment the next line if you don't want to use dynamic frameworks
25 | use_frameworks!
26 |
27 | # Pods for MotionTracking WatchKit App
28 |
29 | target 'MotionTracking WatchKit AppTests' do
30 | inherit! :search_paths
31 | # Pods for testing
32 | end
33 |
34 | target 'MotionTracking WatchKit AppUITests' do
35 | # Pods for testing
36 | end
37 |
38 | end
39 |
--------------------------------------------------------------------------------
/MotionTracking WatchKit Extension/PushNotificationPayload.apns:
--------------------------------------------------------------------------------
1 | {
2 | "aps": {
3 | "alert": {
4 | "body": "Test message",
5 | "title": "Optional title",
6 | "subtitle": "Optional subtitle"
7 | },
8 | "category": "myCategory",
9 | "thread-id": "5280"
10 | },
11 |
12 | "WatchKit Simulator Actions": [
13 | {
14 | "title": "First Button",
15 | "identifier": "firstButtonAction"
16 | }
17 | ],
18 |
19 | "customKey": "Use this file to define a testing payload for your notifications. The aps dictionary specifies the category, alert text and title. The WatchKit Simulator Actions array can provide info for one or more action buttons in addition to the standard Dismiss button. Any other top level keys are custom payload. If you have multiple such JSON files in your project, you'll be able to select them when choosing to debug the notification interface of your Watch App."
20 | }
21 |
--------------------------------------------------------------------------------
/MotionTrackingUITests/MotionTrackingUITestsLaunchTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MotionTrackingUITestsLaunchTests.swift
3 | // MotionTrackingUITests
4 | //
5 | // Created by Karim Angama on 19/01/2022.
6 | //
7 |
8 | import XCTest
9 |
10 | class MotionTrackingUITestsLaunchTests: XCTestCase {
11 |
12 | override class var runsForEachTargetApplicationUIConfiguration: Bool {
13 | true
14 | }
15 |
16 | override func setUpWithError() throws {
17 | continueAfterFailure = false
18 | }
19 |
20 | func testLaunch() throws {
21 | let app = XCUIApplication()
22 | app.launch()
23 |
24 | // Insert steps here to perform after app launch but before taking a screenshot,
25 | // such as logging into a test account or navigating somewhere in the app
26 |
27 | let attachment = XCTAttachment(screenshot: app.screenshot())
28 | attachment.name = "Launch Screen"
29 | attachment.lifetime = .keepAlways
30 | add(attachment)
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/MotionTracking WatchKit AppUITests/MotionTracking_WatchKit_AppUITestsLaunchTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MotionTracking_WatchKit_AppUITestsLaunchTests.swift
3 | // MotionTracking WatchKit AppUITests
4 | //
5 | // Created by Karim Angama on 19/01/2022.
6 | //
7 |
8 | import XCTest
9 |
10 | class MotionTracking_WatchKit_AppUITestsLaunchTests: XCTestCase {
11 |
12 | override class var runsForEachTargetApplicationUIConfiguration: Bool {
13 | true
14 | }
15 |
16 | override func setUpWithError() throws {
17 | continueAfterFailure = false
18 | }
19 |
20 | func testLaunch() throws {
21 | let app = XCUIApplication()
22 | app.launch()
23 |
24 | // Insert steps here to perform after app launch but before taking a screenshot,
25 | // such as logging into a test account or navigating somewhere in the app
26 |
27 | let attachment = XCTAttachment(screenshot: app.screenshot())
28 | attachment.name = "Launch Screen"
29 | attachment.lifetime = .keepAlways
30 | add(attachment)
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/MotionTracking/ExportFile/Data/FieldsMapper.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FieldsMapper.swift
3 | // MotionTracking
4 | //
5 | // Created by Karim Angama on 26/02/2022.
6 | //
7 |
8 | import Foundation
9 |
10 | struct FieldsMapper {
11 |
12 | static func mapper(_ motion: (Bool, Bool, Bool, Bool, Bool), _ location: (Bool, Bool)) -> CSVFileFields {
13 | CSVFileFields(
14 | timestamp: motion.0,
15 | gravityX: motion.1,
16 | gravityY: motion.1,
17 | gravityZ: motion.1,
18 | rotationRateX: motion.2,
19 | rotationRateY: motion.2,
20 | rotationRateZ: motion.2,
21 | userAccelerationX: motion.3,
22 | userAccelerationY: motion.3,
23 | userAccelerationZ: motion.3,
24 | attitudeRoll: motion.4,
25 | attitudePitch: motion.4,
26 | attitudeYaw: motion.4,
27 | locationLatitude: location.0,
28 | locationLongitude: location.0,
29 | locationAltitude: location.1
30 | )
31 | }
32 |
33 | }
34 |
--------------------------------------------------------------------------------
/MotionTracking WatchKit Extension/NotificationController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NotificationController.swift
3 | // MotionTracking WatchKit Extension
4 | //
5 | // Created by Karim Angama on 19/01/2022.
6 | //
7 |
8 | import WatchKit
9 | import Foundation
10 | import UserNotifications
11 |
12 | class NotificationController: WKUserNotificationInterfaceController {
13 |
14 | override init() {
15 | // Initialize variables here.
16 | super.init()
17 |
18 | // Configure interface objects here.
19 | }
20 |
21 | override func willActivate() {
22 | // This method is called when watch view controller is about to be visible to user
23 | }
24 |
25 | override func didDeactivate() {
26 | // This method is called when watch view controller is no longer visible
27 | }
28 |
29 | override func didReceive(_ notification: UNNotification) {
30 | // This method is called when a notification needs to be presented.
31 | // Implement it if you use a dynamic notification interface.
32 | // Populate your dynamic notification interface as quickly as possible.
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2024 Karim Angama
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in
11 | all copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | THE SOFTWARE.
20 |
--------------------------------------------------------------------------------
/MotionTracking WatchKit Extension/Common/Base/BaseViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BaseViewModel.swift
3 | // MotionTracking WatchKit Extension
4 | //
5 | // Created by Karim Angama on 31/01/2022.
6 | //
7 |
8 | import Foundation
9 | import Combine
10 |
11 | protocol ViewModelProtocol {
12 | func exeUseCase()
13 | func observers()
14 | func context(object: Any?)
15 | }
16 |
17 | class BaseViewModel: ViewModelProtocol {
18 |
19 | /// For memory management and subscriptions cancellations
20 | var cancellable = Set()
21 |
22 | /// Pass context object
23 | private(set) var context: Any?
24 |
25 |
26 | required init() {}
27 |
28 | /**
29 | * Setup here
30 | */
31 | func setup() {}
32 |
33 | /**
34 | * Add use case observable in the function
35 | */
36 | func exeUseCase() {}
37 |
38 | /**
39 | * Add Observers
40 | */
41 | func observers() {}
42 |
43 | /**
44 | * context from controller that did push or modal presentation.
45 | *
46 | * @param context object passed
47 | */
48 | func context(object: Any?) {}
49 |
50 | }
51 |
--------------------------------------------------------------------------------
/MotionTracking/Common/Base/BaseViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BaseViewModel.swift
3 | // MotionTracking
4 | //
5 | // Created by Karim Angama on 03/02/2022.
6 | //
7 |
8 | import Foundation
9 | import Combine
10 |
11 | protocol ViewModel {
12 | func setup()
13 | func exeUseCase()
14 | func observers()
15 | }
16 |
17 | protocol Router {
18 | associatedtype T
19 | }
20 |
21 | class BaseRouter {
22 |
23 | }
24 |
25 | class BaseViewModel: ViewModel {
26 |
27 | /// For memory management and subscriptions cancellations
28 | var cancellable = Set()
29 |
30 | /// Error observable
31 | private(set) var error = PassthroughSubject()
32 |
33 |
34 | required init() {}
35 |
36 | /**
37 | * Setup here
38 | */
39 | func setup() {}
40 |
41 | /**
42 | * Add use case observable in the function
43 | */
44 | func exeUseCase() {}
45 |
46 | /**
47 | * Add Observers in the view model
48 | */
49 | func observers() {}
50 |
51 | /**
52 | * Manage code error
53 | */
54 | func error(_ error: Error) {
55 | self.error.send(error)
56 | }
57 |
58 | }
59 |
--------------------------------------------------------------------------------
/MotionTracking WatchKit Extension/Common/Extention/InterfaceImage+Extention.swift:
--------------------------------------------------------------------------------
1 | //
2 | // InterfaceImage+Extention.swift
3 | // MotionTracking WatchKit Extension
4 | //
5 | // Created by Karim Angama on 31/01/2022.
6 | //
7 |
8 | import Foundation
9 | import WatchKit
10 |
11 | extension WKInterfaceImage {
12 |
13 | private static var _isLoaderIndicator = [String:Bool]()
14 | var isLoaderIndicator: Bool {
15 | get {
16 | let tmpAddress = String(format: "%p", unsafeBitCast(self, to: Int.self))
17 | return WKInterfaceImage._isLoaderIndicator[tmpAddress] ?? false
18 | }
19 | set {
20 | if newValue {
21 | startLoaderIndicator()
22 | }else{
23 | stopLoaderIndicator()
24 | }
25 | let tmpAddress = String(format: "%p", unsafeBitCast(self, to: Int.self))
26 | WKInterfaceImage._isLoaderIndicator[tmpAddress] = newValue
27 | }
28 | }
29 |
30 | func startLoaderIndicator() {
31 | setImageNamed("spinner")
32 | setHidden(false)
33 | startAnimatingWithImages(in: NSMakeRange(1, 8), duration: 0.5, repeatCount: 0)
34 | }
35 |
36 | func stopLoaderIndicator() {
37 | setHidden(true)
38 | stopAnimating()
39 | }
40 |
41 | }
42 |
--------------------------------------------------------------------------------
/MotionTracking/Common/Extention/Date+Extention.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Date+Extention.swift
3 | // MotionTracking
4 | //
5 | // Created by Karim Angama on 03/02/2022.
6 | //
7 |
8 | import Foundation
9 |
10 | extension Date {
11 |
12 | enum DateFormat: String {
13 | case iso8601 = "yyyy-MM-dd'T'HH:mm:ss"
14 | }
15 |
16 | static let null: Date = {
17 | return Date(timeIntervalSince1970: 0)
18 | }()
19 |
20 | var nowDateIso8601: String {
21 | let format = DateFormatter()
22 | format.dateFormat = DateFormat.iso8601.rawValue
23 | return format.string(from: self)
24 | }
25 |
26 | var mediumDate: String {
27 | let formatter = DateFormatter()
28 | formatter.dateStyle = .medium
29 | formatter.timeStyle = .medium
30 | return formatter.string(from: self)
31 | }
32 |
33 | static func string(date: String) -> Date {
34 | let format = DateFormatter()
35 | format.dateFormat = DateFormat.iso8601.rawValue
36 | return format.date(from: date) ?? Date.null
37 | }
38 |
39 | }
40 |
41 | extension Double {
42 |
43 | var time: String {
44 | let format = DateComponentsFormatter()
45 | format.allowedUnits = [.hour, .minute, .second]
46 | format.unitsStyle = .brief
47 | return format.string(from: TimeInterval(self)) ?? "-"
48 | }
49 |
50 | }
51 |
--------------------------------------------------------------------------------
/MotionTrackingTests/MotionTrackingTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MotionTrackingTests.swift
3 | // MotionTrackingTests
4 | //
5 | // Created by Karim Angama on 19/01/2022.
6 | //
7 |
8 | import XCTest
9 | @testable import MotionTracking
10 |
11 | class MotionTrackingTests: XCTestCase {
12 |
13 | override func setUpWithError() throws {
14 | // Put setup code here. This method is called before the invocation of each test method in the class.
15 | }
16 |
17 | override func tearDownWithError() throws {
18 | // Put teardown code here. This method is called after the invocation of each test method in the class.
19 | }
20 |
21 | func testExample() throws {
22 | // This is an example of a functional test case.
23 | // Use XCTAssert and related functions to verify your tests produce the correct results.
24 | // Any test you write for XCTest can be annotated as throws and async.
25 | // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error.
26 | // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards.
27 | }
28 |
29 | func testPerformanceExample() throws {
30 | // This is an example of a performance test case.
31 | self.measure {
32 | // Put the code you want to measure the time of here.
33 | }
34 | }
35 |
36 | }
37 |
--------------------------------------------------------------------------------
/MotionTracking WatchKit Extension/Assets.xcassets/Complication.complicationset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "assets" : [
3 | {
4 | "filename" : "Circular.imageset",
5 | "idiom" : "watch",
6 | "role" : "circular"
7 | },
8 | {
9 | "filename" : "Extra Large.imageset",
10 | "idiom" : "watch",
11 | "role" : "extra-large"
12 | },
13 | {
14 | "filename" : "Graphic Bezel.imageset",
15 | "idiom" : "watch",
16 | "role" : "graphic-bezel"
17 | },
18 | {
19 | "filename" : "Graphic Circular.imageset",
20 | "idiom" : "watch",
21 | "role" : "graphic-circular"
22 | },
23 | {
24 | "filename" : "Graphic Corner.imageset",
25 | "idiom" : "watch",
26 | "role" : "graphic-corner"
27 | },
28 | {
29 | "filename" : "Graphic Extra Large.imageset",
30 | "idiom" : "watch",
31 | "role" : "graphic-extra-large"
32 | },
33 | {
34 | "filename" : "Graphic Large Rectangular.imageset",
35 | "idiom" : "watch",
36 | "role" : "graphic-large-rectangular"
37 | },
38 | {
39 | "filename" : "Modular.imageset",
40 | "idiom" : "watch",
41 | "role" : "modular"
42 | },
43 | {
44 | "filename" : "Utilitarian.imageset",
45 | "idiom" : "watch",
46 | "role" : "utilitarian"
47 | }
48 | ],
49 | "info" : {
50 | "author" : "xcode",
51 | "version" : 1
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/MotionTracking WatchKit Extension/Common/Extention/WKInterfaceTimer+Extention.swift:
--------------------------------------------------------------------------------
1 | //
2 | // WKInterfaceTimer+Extention.swift
3 | // MotionTracking WatchKit Extension
4 | //
5 | // Created by Karim Angama on 31/01/2022.
6 | //
7 |
8 | import Foundation
9 | import WatchKit
10 |
11 | extension WKInterfaceTimer {
12 |
13 | private static var _isStart = [String:Bool]()
14 | var isStart: Bool {
15 | get {
16 | let tmpAddress = String(format: "%p", unsafeBitCast(self, to: Int.self))
17 | return WKInterfaceTimer._isStart[tmpAddress] ?? false
18 | }
19 | set {
20 | if newValue {
21 | start()
22 | }else{
23 | stop()
24 | }
25 | let tmpAddress = String(format: "%p", unsafeBitCast(self, to: Int.self))
26 | WKInterfaceTimer._isStart[tmpAddress] = newValue
27 | }
28 | }
29 |
30 | private static var _date = [String:Date]()
31 | var date: Date {
32 | get {
33 | let tmpAddress = String(format: "%p", unsafeBitCast(self, to: Int.self))
34 | return WKInterfaceTimer._date[tmpAddress] ?? Date()
35 | }
36 | set {
37 | setDate(newValue)
38 | let tmpAddress = String(format: "%p", unsafeBitCast(self, to: Int.self))
39 | WKInterfaceTimer._date[tmpAddress] = newValue
40 | }
41 | }
42 |
43 | }
44 |
--------------------------------------------------------------------------------
/MotionTracking WatchKit Extension/Common/Base/BaseInterfaceController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BaseInterfaceController.swift
3 | // MotionTracking WatchKit Extension
4 | //
5 | // Created by Karim Angama on 20/01/2022.
6 | //
7 |
8 | import WatchKit
9 | import Foundation
10 | import Combine
11 |
12 | protocol InterfaceController {
13 | func setupObservers()
14 | func setupUI()
15 | }
16 |
17 | class BaseInterfaceController: WKInterfaceController, InterfaceController {
18 |
19 | /// For memory management and subscriptions cancellations
20 | var cancellable = Set()
21 |
22 | /// ViewModel of InterfaceController
23 | var viewModel: T!
24 |
25 |
26 | override init() {
27 | super.init()
28 | self.viewModel = T.init()
29 |
30 | setupUI()
31 | setupObservers()
32 | viewModel.setup()
33 | viewModel.observers()
34 | viewModel.exeUseCase()
35 |
36 | }
37 |
38 | override func awake(withContext context: Any?) {
39 |
40 | // Push context to ViewModel
41 | if let context = context {
42 | viewModel.context(object: context)
43 | }
44 |
45 | }
46 |
47 | /**
48 | * Set Observers in the InterfaceController
49 | */
50 | func setupObservers() {}
51 |
52 | /**
53 | * Manage user interface here
54 | */
55 | func setupUI() {}
56 |
57 | }
58 |
--------------------------------------------------------------------------------
/MotionTracking WatchKit AppTests/MotionTracking_WatchKit_AppTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MotionTracking_WatchKit_AppTests.swift
3 | // MotionTracking WatchKit AppTests
4 | //
5 | // Created by Karim Angama on 19/01/2022.
6 | //
7 |
8 | import XCTest
9 | @testable import MotionTracking_WatchKit_Extension
10 |
11 | class MotionTracking_WatchKit_AppTests: XCTestCase {
12 |
13 | override func setUpWithError() throws {
14 | // Put setup code here. This method is called before the invocation of each test method in the class.
15 | }
16 |
17 | override func tearDownWithError() throws {
18 | // Put teardown code here. This method is called after the invocation of each test method in the class.
19 | }
20 |
21 | func testExample() throws {
22 | // This is an example of a functional test case.
23 | // Use XCTAssert and related functions to verify your tests produce the correct results.
24 | // Any test you write for XCTest can be annotated as throws and async.
25 | // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error.
26 | // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards.
27 | }
28 |
29 | func testPerformanceExample() throws {
30 | // This is an example of a performance test case.
31 | self.measure {
32 | // Put the code you want to measure the time of here.
33 | }
34 | }
35 |
36 | }
37 |
--------------------------------------------------------------------------------
/MotionTracking/Common/Base/BaseTableViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BaseTableViewController.swift
3 | // MotionTracking
4 | //
5 | // Created by Karim Angama on 05/02/2022.
6 | //
7 |
8 | import Foundation
9 | import Combine
10 | import UIKit
11 |
12 | class BaseTableViewController: UITableViewController, ViewController {
13 |
14 | /// For memory management and subscriptions cancellations
15 | var cancellable = Set()
16 |
17 | /// ViewModel of ViewController
18 | var viewModel: T!
19 |
20 |
21 | required init?(coder: NSCoder) {
22 | super.init(coder: coder)
23 | viewModel = T.init()
24 | }
25 |
26 | override func viewDidLoad() {
27 | super.viewDidLoad()
28 | setupUI()
29 | setupBindings()
30 | setupObservers()
31 | viewModel.observers()
32 | viewModel.exeUseCase()
33 | }
34 |
35 | /**
36 | * Set data binding in the view controller
37 | */
38 | func setupBindings() {}
39 |
40 | /**
41 | * Set Observers in the view controller
42 | */
43 | func setupObservers() {
44 |
45 | // Display error message
46 | viewModel.error
47 | .receive(on: DispatchQueue.main)
48 | .sink { [weak self] error in
49 | self?.showMessageError(error)
50 | }
51 | .store(in: &cancellable)
52 |
53 |
54 | }
55 |
56 | /**
57 | * Manage user interface here
58 | */
59 | func setupUI() {}
60 |
61 | }
62 |
--------------------------------------------------------------------------------
/MotionTracking WatchKit Extension/Common/Utils/Parameters.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Parameters.swift
3 | // MotionTracking WatchKit Extension
4 | //
5 | // Created by Karim Angama on 01/02/2022.
6 | //
7 |
8 | import Foundation
9 |
10 | struct Parameters {
11 |
12 | struct Keys {
13 | static let motionIntervalConfiguration = "com.kangama.herz-configuration"
14 | static let timerConfiguration = "com.kangama.timer-configuration"
15 | static let locationConfiguration = "com.kangama.location-configuration"
16 | }
17 |
18 | static func getMotionInterval() -> Double {
19 | let value = UserDefaults.standard.double(forKey: Keys.motionIntervalConfiguration)
20 | return value == 0 ? Constants.Configuration.initialMotionInterval : value
21 | }
22 | static func setMotionInterval(value: Double) {
23 | UserDefaults.standard.set(value, forKey:Keys.motionIntervalConfiguration)
24 | }
25 |
26 | static func getTimer() -> Double {
27 | let value = UserDefaults.standard.double(forKey: Keys.timerConfiguration)
28 | return value == 0 ? Constants.Configuration.initialTimer : value
29 | }
30 | static func setTimer(value: Double) {
31 | UserDefaults.standard.set(value, forKey:Keys.timerConfiguration)
32 | }
33 |
34 | static func getLocation() -> Bool {
35 | return UserDefaults.standard.bool(forKey: Keys.locationConfiguration)
36 | }
37 | static func setLocation(value: Bool) {
38 | UserDefaults.standard.set(value, forKey:Keys.locationConfiguration)
39 | }
40 |
41 | }
42 |
--------------------------------------------------------------------------------
/MotionTrackingUITests/MotionTrackingUITests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MotionTrackingUITests.swift
3 | // MotionTrackingUITests
4 | //
5 | // Created by Karim Angama on 19/01/2022.
6 | //
7 |
8 | import XCTest
9 |
10 | class MotionTrackingUITests: XCTestCase {
11 |
12 | override func setUpWithError() throws {
13 | // Put setup code here. This method is called before the invocation of each test method in the class.
14 |
15 | // In UI tests it is usually best to stop immediately when a failure occurs.
16 | continueAfterFailure = false
17 |
18 | // 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.
19 | }
20 |
21 | override func tearDownWithError() throws {
22 | // Put teardown code here. This method is called after the invocation of each test method in the class.
23 | }
24 |
25 | func testExample() throws {
26 | // UI tests must launch the application that they test.
27 | let app = XCUIApplication()
28 | app.launch()
29 |
30 | // Use recording to get started writing UI tests.
31 | // Use XCTAssert and related functions to verify your tests produce the correct results.
32 | }
33 |
34 | func testLaunchPerformance() throws {
35 | if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) {
36 | // This measures how long it takes to launch your application.
37 | measure(metrics: [XCTApplicationLaunchMetric()]) {
38 | XCUIApplication().launch()
39 | }
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/MotionTracking WatchKit AppUITests/MotionTracking_WatchKit_AppUITests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MotionTracking_WatchKit_AppUITests.swift
3 | // MotionTracking WatchKit AppUITests
4 | //
5 | // Created by Karim Angama on 19/01/2022.
6 | //
7 |
8 | import XCTest
9 |
10 | class MotionTracking_WatchKit_AppUITests: XCTestCase {
11 |
12 | override func setUpWithError() throws {
13 | // Put setup code here. This method is called before the invocation of each test method in the class.
14 |
15 | // In UI tests it is usually best to stop immediately when a failure occurs.
16 | continueAfterFailure = false
17 |
18 | // 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.
19 | }
20 |
21 | override func tearDownWithError() throws {
22 | // Put teardown code here. This method is called after the invocation of each test method in the class.
23 | }
24 |
25 | func testExample() throws {
26 | // UI tests must launch the application that they test.
27 | let app = XCUIApplication()
28 | app.launch()
29 |
30 | // Use recording to get started writing UI tests.
31 | // Use XCTAssert and related functions to verify your tests produce the correct results.
32 | }
33 |
34 | func testLaunchPerformance() throws {
35 | if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) {
36 | // This measures how long it takes to launch your application.
37 | measure(metrics: [XCTApplicationLaunchMetric()]) {
38 | XCUIApplication().launch()
39 | }
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/MotionTracking/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // MotionTracking
4 | //
5 | // Created by Karim Angama on 19/01/2022.
6 | //
7 |
8 | import UIKit
9 |
10 | @main
11 | class AppDelegate: UIResponder, UIApplicationDelegate {
12 |
13 | let connectivityManager = ConnectivityManager()
14 |
15 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
16 | // Override point for customization after application launch.
17 | return true
18 | }
19 |
20 | // MARK: UISceneSession Lifecycle
21 |
22 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
23 | // Called when a new scene session is being created.
24 | // Use this method to select a configuration to create the new scene with.
25 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
26 | }
27 |
28 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) {
29 | // Called when the user discards a scene session.
30 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
31 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return.
32 | }
33 |
34 |
35 | }
36 |
37 |
38 | extension UIApplication {
39 |
40 | static func app() -> AppDelegate {
41 | UIApplication.shared.delegate as! AppDelegate
42 | }
43 |
44 | }
45 |
--------------------------------------------------------------------------------
/MotionTracking WatchKit Extension/Tracking/ResultViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ResultViewModel.swift
3 | // MotionTracking WatchKit Extension
4 | //
5 | // Created by Karim Angama on 31/01/2022.
6 | //
7 |
8 | import Foundation
9 | import Combine
10 |
11 | class ResultViewModel: BaseViewModel {
12 |
13 | // Propertie
14 | weak var motionManager: MotionManager?
15 |
16 | /// Output observable
17 | @Published var gravityX = "-"
18 | @Published var gravityY = "-"
19 | @Published var gravityZ = "-"
20 |
21 | @Published var accelerationX = "-"
22 | @Published var accelerationY = "-"
23 | @Published var accelerationZ = "-"
24 |
25 | @Published var rotationX = "-"
26 | @Published var rotationY = "-"
27 | @Published var rotationZ = "-"
28 |
29 |
30 | override func context(object: Any?) {
31 | motionManager = object as? MotionManager
32 | motionManager?.delegate = self
33 | }
34 |
35 | }
36 |
37 | extension ResultViewModel: MotionManagerDelegate {
38 |
39 | func didUpdateMotion(_ manager: MotionManager, result: ResultMotionEnity) {
40 | gravityX = String(format: "%.1f", result.gravityX)
41 | gravityY = String(format: "%.1f", result.gravityY)
42 | gravityZ = String(format: "%.1f", result.gravityZ)
43 |
44 | accelerationX = String(format: "%.1f", result.userAccelerationX)
45 | accelerationY = String(format: "%.1f", result.userAccelerationY)
46 | accelerationZ = String(format: "%.1f", result.userAccelerationZ)
47 |
48 | rotationX = String(format: "%.1f", result.rotationRateX)
49 | rotationY = String(format: "%.1f", result.rotationRateY)
50 | rotationZ = String(format: "%.1f", result.rotationRateZ)
51 | }
52 |
53 | }
54 |
--------------------------------------------------------------------------------
/MotionTracking WatchKit Extension/Settings/SettingsViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SettingsViewModel.swift
3 | // MotionTracking WatchKit Extension
4 | //
5 | // Created by Karim Angama on 01/02/2022.
6 | //
7 |
8 | import Foundation
9 | import Combine
10 |
11 | class SettingsViewModel: BaseViewModel {
12 |
13 | /// Output observer
14 | @Published var herzValue = Parameters.getMotionInterval()
15 | @Published var timerValue = Parameters.getTimer()
16 | @Published var locationValue = Parameters.getLocation()
17 |
18 | /// Output propertie
19 | var timeRange = Constants.Configuration.timeRange
20 |
21 | /// Input observer
22 | var indexPickerValue = PassthroughSubject()
23 |
24 |
25 | // MARK: Override methode
26 |
27 | override func exeUseCase() {
28 | super.exeUseCase()
29 |
30 | setupIndexPicker()
31 | }
32 |
33 | override func observers() {
34 | super.observers()
35 |
36 | // Set herz value to UserDefaults
37 | $herzValue.dropFirst()
38 | .sink { value in
39 | Parameters.setMotionInterval(value: value)
40 | }
41 | .store(in: &cancellable)
42 |
43 | // Set location value to UserDefaults
44 | $locationValue.dropFirst()
45 | .sink { value in
46 | Parameters.setLocation(value: value)
47 | }
48 | .store(in: &cancellable)
49 |
50 | // Set timer value to UserDefaults
51 | indexPickerValue.dropFirst()
52 | .compactMap { [weak self] index in
53 | self?.timeRange[index]
54 | }
55 | .sink { value in
56 | Parameters.setTimer(value: Double(value))
57 | }
58 | .store(in: &cancellable)
59 |
60 | }
61 |
62 | // MARK: Private methode
63 |
64 | private func setupIndexPicker() {
65 | indexPickerValue.send( timeRange.firstIndex(of: Int(Parameters.getTimer())) ?? 0 )
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # MotionTracking
2 |
3 | This application refers to the article, [Introduction to Apple WatchKit with Core Motion — Tracking Jumping Jacks](https://heartbeat.comet.ml/introduction-to-apple-watchkit-with-core-motion-tracking-jumping-jacks-259ee80d1210) by Eric Hsiao.
4 | It collect in a csv file the movement and location data of the Apple Watch, that will allow the creation of ML models so that your Apple Watch recognizes movements using the Core ML framework.
5 |
6 | The Application uses CoreMotion and LocationManager to collect the data and saves it to a file that is transferred to the iPhone using WatchConnectivity, then, you can export the data to a csv file from the iPhone.
7 |
8 |
9 | ## Screenshots
10 |
11 | ### Apple Watch
12 |
13 |
14 | ### iPhone
15 |
16 |
17 |
18 | ## Features
19 |
20 | ### Apple Watch
21 | - [x] Change sample interval
22 | - [x] Change timer of tracking
23 | - [x] Add or remove the location position to csv
24 |
25 | ### iPhone
26 | - [x] Export of csv files
27 | - [x] Delete of csv files
28 | - [x] Delete multiple files at once
29 | - [x] Choice of data to export (Timestamp, gravity, rotation, acceleration, coordinate, altitude)
30 | - [x] Export multiple files in zip format
31 |
32 |
33 | ## ToDo
34 |
35 | - [x] Create group files
36 | - [x] Organize your csv files to export to Create ML
37 |
38 |
39 | ## Requirements
40 |
41 | - iOS 14.0+
42 | - wachtOS 8.0+
43 | - Xcode 13.2.1
44 |
45 |
46 | ## Build
47 |
48 | Project uses CocoaPods for dependencies management. To build the project you need to download dependencies:
49 |
50 | ```
51 | pod install
52 | ```
53 |
54 |
55 | ## Author
56 |
57 | k.angama, karim.angama@gmail.com
58 |
59 |
60 | ## License
61 |
62 | MotionTracking is available under the MIT license. See the [LICENSE](https://github.com/k-angama/MotionTracking/blob/master/LICENSE) file for more info.
63 |
--------------------------------------------------------------------------------
/MotionTracking/Common/Base/BaseViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BaseViewController.swift
3 | // MotionTracking
4 | //
5 | // Created by Karim Angama on 03/02/2022.
6 | //
7 |
8 | import Foundation
9 | import Combine
10 | import UIKit
11 |
12 | protocol ViewController {
13 | func setupBindings()
14 | func setupObservers()
15 | func setupUI()
16 | }
17 |
18 | class BaseViewController: UIViewController, ViewController {
19 |
20 | /// For memory management and subscriptions cancellations
21 | var cancellable = Set()
22 |
23 | /// ViewModel of ViewController
24 | var viewModel: T!
25 |
26 |
27 | required init?(coder: NSCoder) {
28 | super.init(coder: coder)
29 | viewModel = T.init()
30 | }
31 |
32 | override func viewDidLoad() {
33 | super.viewDidLoad()
34 | setupUI()
35 | setupBindings()
36 | setupObservers()
37 | viewModel.setup()
38 | viewModel.observers()
39 | viewModel.exeUseCase()
40 | }
41 |
42 | /**
43 | * Set data binding in the view controller
44 | */
45 | func setupBindings() {}
46 |
47 | /**
48 | * Set Observers in the view controller
49 | */
50 | func setupObservers() {
51 |
52 | // Display error message
53 | viewModel.error
54 | .receive(on: DispatchQueue.main)
55 | .sink { [weak self] error in
56 | self?.showMessageError(error)
57 | }
58 | .store(in: &cancellable)
59 |
60 | }
61 |
62 | /**
63 | * Manage user interface here
64 | */
65 | func setupUI() {}
66 |
67 | }
68 |
69 | extension UIViewController {
70 |
71 | /**
72 | * Display error message
73 | */
74 | func showMessageError(_ error: Error, completion: (() -> Void)? = nil) {
75 | let alertController = UIAlertController(
76 | title: "Error",
77 | message: error.localizedDescription,
78 | preferredStyle: .alert)
79 | alertController.addAction(UIAlertAction(title: "ok", style: .default, handler: nil))
80 | self.present(alertController, animated: true, completion: completion)
81 | }
82 |
83 | }
84 |
--------------------------------------------------------------------------------
/MotionTracking WatchKit Extension/Home/HomeViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HomeViewModel.swift
3 | // MotionTracking WatchKit Extension
4 | //
5 | // Created by Karim Angama on 31/01/2022.
6 | //
7 |
8 | import Foundation
9 | import WatchKit
10 | import Combine
11 |
12 | class HomeViewModel: BaseViewModel {
13 |
14 | /// Propertie
15 | var motionManager:MotionManager?
16 |
17 | /// Output
18 | @Published var numberFileSending = CurrentSession.numberFileSending
19 | @Published var numberFileError = CurrentSession.numberFileError
20 | @Published var isShowLoaderIndicator = false
21 | var showAlertMessageNoLocation = PassthroughSubject()
22 | var showTrackingScreen = PassthroughSubject()
23 |
24 |
25 | // MARK: Override method
26 |
27 | override func setup() {
28 | super.setup()
29 | WKExtension.app().connectivityManager.delegate = self
30 | setupFiletranfertIndicator()
31 | }
32 |
33 | // MARK: Public method
34 |
35 | func initMotionManager() {
36 | motionManager = MotionManager(
37 | sampleInterval: Parameters.getMotionInterval(),
38 | addDataLocation: Parameters.getLocation()
39 | )
40 | motionManager?.delegateLocation = self
41 | }
42 |
43 | func handleLocationAuthorization() {
44 | motionManager?.authorization()
45 | }
46 |
47 | // MARK: Private method
48 |
49 | private func setupFiletranfertIndicator() {
50 |
51 | if WKExtension.app().connectivityManager.isFileTransfer {
52 | isShowLoaderIndicator = true
53 | }
54 | }
55 |
56 | }
57 |
58 | // MARK: Location Manager Delegate
59 |
60 | extension HomeViewModel: LocationManagerDelegate {
61 |
62 | func userAuthorization(response: Bool) {
63 | if response {
64 | showTrackingScreen.send(())
65 | }else{
66 | showAlertMessageNoLocation.send(())
67 | }
68 | }
69 |
70 | }
71 |
72 | // MARK: Connectivity Manager Delegate
73 |
74 | extension HomeViewModel: ConnectivityManagerDelegate {
75 |
76 | func didFinishFileTransfer(error: Error?) {
77 | isShowLoaderIndicator = false
78 | if error == nil {
79 | numberFileSending = CurrentSession.addNumberFileSending()
80 | }else{
81 | numberFileError = CurrentSession.addNumberFileError()
82 | }
83 | }
84 |
85 | }
86 |
--------------------------------------------------------------------------------
/MotionTracking/SceneDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SceneDelegate.swift
3 | // MotionTracking
4 | //
5 | // Created by Karim Angama on 19/01/2022.
6 | //
7 |
8 | import UIKit
9 |
10 | class SceneDelegate: UIResponder, UIWindowSceneDelegate {
11 |
12 | var window: UIWindow?
13 |
14 |
15 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
16 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
17 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
18 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
19 | guard let _ = (scene as? UIWindowScene) else { return }
20 | }
21 |
22 | func sceneDidDisconnect(_ scene: UIScene) {
23 | // Called as the scene is being released by the system.
24 | // This occurs shortly after the scene enters the background, or when its session is discarded.
25 | // Release any resources associated with this scene that can be re-created the next time the scene connects.
26 | // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead).
27 | }
28 |
29 | func sceneDidBecomeActive(_ scene: UIScene) {
30 | // Called when the scene has moved from an inactive state to an active state.
31 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive.
32 | }
33 |
34 | func sceneWillResignActive(_ scene: UIScene) {
35 | // Called when the scene will move from an active state to an inactive state.
36 | // This may occur due to temporary interruptions (ex. an incoming phone call).
37 | }
38 |
39 | func sceneWillEnterForeground(_ scene: UIScene) {
40 | // Called as the scene transitions from the background to the foreground.
41 | // Use this method to undo the changes made on entering the background.
42 | }
43 |
44 | func sceneDidEnterBackground(_ scene: UIScene) {
45 | // Called as the scene transitions from the foreground to the background.
46 | // Use this method to save data, release shared resources, and store enough scene-specific state information
47 | // to restore the scene back to its current state.
48 | }
49 |
50 |
51 | }
52 |
53 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Xcode
2 | #
3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
4 |
5 | ## User settings
6 | xcuserdata/
7 |
8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
9 | *.xcscmblueprint
10 | *.xccheckout
11 |
12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
13 | build/
14 | DerivedData/
15 | *.moved-aside
16 | *.pbxuser
17 | !default.pbxuser
18 | *.mode1v3
19 | !default.mode1v3
20 | *.mode2v3
21 | !default.mode2v3
22 | *.perspectivev3
23 | !default.perspectivev3
24 |
25 | ## Obj-C/Swift specific
26 | *.hmap
27 |
28 | ## App packaging
29 | *.ipa
30 | *.dSYM.zip
31 | *.dSYM
32 |
33 | ## Playgrounds
34 | timeline.xctimeline
35 | playground.xcworkspace
36 |
37 | # Swift Package Manager
38 | #
39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
40 | # Packages/
41 | # Package.pins
42 | # Package.resolved
43 | # *.xcodeproj
44 | #
45 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
46 | # hence it is not needed unless you have added a package configuration file to your project
47 | # .swiftpm
48 |
49 | .build/
50 |
51 | # CocoaPods
52 | #
53 | # We recommend against adding the Pods directory to your .gitignore. However
54 | # you should judge for yourself, the pros and cons are mentioned at:
55 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
56 | #
57 | Pods/
58 | _Pods.xcodeproj
59 |
60 | #
61 | # Add this line if you want to avoid checking in source code from the Xcode workspace
62 | # *.xcworkspace
63 |
64 | # Carthage
65 | #
66 | # Add this line if you want to avoid checking in source code from Carthage dependencies.
67 | # Carthage/Checkouts
68 |
69 | Carthage/Build/
70 |
71 | # Accio dependency management
72 | Dependencies/
73 | .accio/
74 |
75 | # fastlane
76 | #
77 | # It is recommended to not store the screenshots in the git repo.
78 | # Instead, use fastlane to re-generate the screenshots whenever they are needed.
79 | # For more information about the recommended setup visit:
80 | # https://docs.fastlane.tools/best-practices/source-control/#source-control
81 |
82 | fastlane/report.xml
83 | fastlane/Preview.html
84 | fastlane/screenshots/**/*.png
85 | fastlane/test_output
86 |
87 | # Code Injection
88 | #
89 | # After new code Injection tools there's a generated folder /iOSInjectionProject
90 | # https://github.com/johnno1962/injectionforxcode
91 |
92 | iOSInjectionProject/
93 |
94 | # Mac
95 | #
96 |
97 | *.DS_Store
98 |
--------------------------------------------------------------------------------
/MotionTracking WatchKit Extension/Tracking/ReadyInterfaceController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // InterfaceController.swift
3 | // MotionTracking WatchKit Extension
4 | //
5 | // Created by Karim Angama on 02/02/2022.
6 | //
7 |
8 | import Foundation
9 | import WatchKit
10 |
11 | class ReadyInterfaceController: BaseInterfaceController {
12 |
13 | @IBOutlet weak var readyLabel: WKInterfaceLabel!
14 | @IBOutlet weak var goLabel: WKInterfaceLabel!
15 |
16 | override func awake(withContext context: Any?) {
17 | super.awake(withContext: context)
18 | setupAninimation()
19 | }
20 |
21 | private func setupAninimation() {
22 | readyLabel.setAlpha(0)
23 | goLabel.setAlpha(0)
24 | self.animation()
25 | }
26 |
27 | private func animation() {
28 |
29 | let duration = 0.3
30 | animate(withDuration: duration) { [weak self] in
31 | self?.readyLabel.setVerticalAlignment(.center)
32 | self?.readyLabel.setAlpha(1)
33 | }
34 |
35 | DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
36 | self.animate(withDuration: duration) { [weak self] in
37 | self?.readyLabel.setVerticalAlignment(.bottom)
38 | self?.readyLabel.setAlpha(0)
39 | }
40 | }
41 |
42 | DispatchQueue.main.asyncAfter(deadline: .now() + 1.3) {
43 | self.animate(withDuration: duration) { [weak self] in
44 | self?.goLabel.setVerticalAlignment(.center)
45 | self?.goLabel.setAlpha(1)
46 | }
47 | }
48 |
49 | DispatchQueue.main.asyncAfter(deadline: .now() + 2.2) {
50 | self.animate(withDuration: duration) { [weak self] in
51 | self?.goLabel.setVerticalAlignment(.bottom)
52 | self?.goLabel.setAlpha(0)
53 | }
54 | }
55 |
56 | DispatchQueue.main.asyncAfter(deadline: .now() + 2.5) { [weak self] in
57 | self?.openTrackingScreen()
58 | }
59 | }
60 |
61 | private func openTrackingScreen() {
62 | guard let manager = viewModel.motionManager else { return }
63 | DispatchQueue.main.async {
64 | WKInterfaceController.reloadRootPageControllers(
65 | withNames: ["ResultInterfaceController", "TrackingInterfaceController"],
66 | contexts: [manager, manager],
67 | orientation: .horizontal,
68 | pageIndex: 1
69 | )
70 | }
71 | WKInterfaceDevice.current().play(.start)
72 | }
73 |
74 | }
75 |
--------------------------------------------------------------------------------
/MotionTracking WatchKit Extension/ComplicationController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ComplicationController.swift
3 | // MotionTracking WatchKit Extension
4 | //
5 | // Created by Karim Angama on 19/01/2022.
6 | //
7 |
8 | import ClockKit
9 |
10 |
11 | class ComplicationController: NSObject, CLKComplicationDataSource {
12 |
13 | // MARK: - Complication Configuration
14 |
15 | func getComplicationDescriptors(handler: @escaping ([CLKComplicationDescriptor]) -> Void) {
16 | let descriptors = [
17 | CLKComplicationDescriptor(identifier: "complication", displayName: "MotionTracking", supportedFamilies: CLKComplicationFamily.allCases)
18 | // Multiple complication support can be added here with more descriptors
19 | ]
20 |
21 | // Call the handler with the currently supported complication descriptors
22 | handler(descriptors)
23 | }
24 |
25 | func handleSharedComplicationDescriptors(_ complicationDescriptors: [CLKComplicationDescriptor]) {
26 | // Do any necessary work to support these newly shared complication descriptors
27 | }
28 |
29 | // MARK: - Timeline Configuration
30 |
31 | func getTimelineEndDate(for complication: CLKComplication, withHandler handler: @escaping (Date?) -> Void) {
32 | // Call the handler with the last entry date you can currently provide or nil if you can't support future timelines
33 | handler(nil)
34 | }
35 |
36 | func getPrivacyBehavior(for complication: CLKComplication, withHandler handler: @escaping (CLKComplicationPrivacyBehavior) -> Void) {
37 | // Call the handler with your desired behavior when the device is locked
38 | handler(.showOnLockScreen)
39 | }
40 |
41 | // MARK: - Timeline Population
42 |
43 | func getCurrentTimelineEntry(for complication: CLKComplication, withHandler handler: @escaping (CLKComplicationTimelineEntry?) -> Void) {
44 | // Call the handler with the current timeline entry
45 | handler(nil)
46 | }
47 |
48 | func getTimelineEntries(for complication: CLKComplication, after date: Date, limit: Int, withHandler handler: @escaping ([CLKComplicationTimelineEntry]?) -> Void) {
49 | // Call the handler with the timeline entries after the given date
50 | handler(nil)
51 | }
52 |
53 | // MARK: - Sample Templates
54 |
55 | func getLocalizableSampleTemplate(for complication: CLKComplication, withHandler handler: @escaping (CLKComplicationTemplate?) -> Void) {
56 | // This method will be called once per supported complication, and the results will be cached
57 | handler(nil)
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/MotionTracking WatchKit Extension/Settings/SettingsInterfaceController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SettingsInterfaceController.swift
3 | // MotionTracking WatchKit Extension
4 | //
5 | // Created by Karim Angama on 21/01/2022.
6 | //
7 |
8 | import WatchKit
9 | import Foundation
10 |
11 | class SettingsInterfaceController: BaseInterfaceController {
12 |
13 | @IBOutlet weak var interfacePicker: WKInterfacePicker!
14 | @IBOutlet weak var herzInterfaceSlider: WKInterfaceSlider!
15 | @IBOutlet weak var herzInterfaceLabel: WKInterfaceLabel!
16 | @IBOutlet weak var locationInterfaceSwitch: WKInterfaceSwitch!
17 |
18 |
19 | // MARK: Override method
20 |
21 | override func awake(withContext context: Any?) {
22 | super.awake(withContext: context)
23 | setupPicker()
24 | }
25 |
26 | override func setupObservers() {
27 | super.setupObservers()
28 |
29 | // Set herz value to slider
30 | viewModel.$herzValue
31 | .receive(on: DispatchQueue.main)
32 | .map { Float($0) }
33 | .assign(to: \.value, on: herzInterfaceSlider)
34 | .store(in: &cancellable)
35 |
36 | // Set timer value to picker
37 | viewModel.indexPickerValue
38 | .receive(on: DispatchQueue.main)
39 | .assign(to: \.selectedItemIndex, on: interfacePicker)
40 | .store(in: &cancellable)
41 |
42 | // Display herz
43 | viewModel.$herzValue
44 | .receive(on: DispatchQueue.main)
45 | .map { "\(Int($0))" }
46 | .assign(to: \.text, on: herzInterfaceLabel)
47 | .store(in: &cancellable)
48 |
49 | viewModel.$locationValue
50 | .receive(on: DispatchQueue.main)
51 | .assign(to: \.on, on: locationInterfaceSwitch)
52 | .store(in: &cancellable)
53 |
54 | }
55 |
56 | // MARK: Action method
57 |
58 | @IBAction func sliderAction(_ value: Float) {
59 | viewModel.herzValue = Double(value)
60 | }
61 |
62 | @IBAction func pickerAction(_ value: Int) {
63 | viewModel.indexPickerValue.send(value)
64 | }
65 |
66 | @IBAction func switchAction(_ value: Bool) {
67 | viewModel.locationValue = value
68 | }
69 |
70 | // MARK: Private method
71 |
72 | private func setupPicker() {
73 |
74 | let item = viewModel.timeRange
75 | .compactMap({ index -> WKPickerItem in
76 | let pickerItem = WKPickerItem()
77 | pickerItem.title = "\(index)s"
78 | pickerItem.caption = "Second"
79 | return pickerItem
80 | })
81 | interfacePicker.setItems(item)
82 |
83 | }
84 |
85 | }
86 |
--------------------------------------------------------------------------------
/MotionTracking WatchKit Extension/Tracking/ResultInterfaceController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ResultInterfaceController.swift
3 | // MotionTracking WatchKit Extension
4 | //
5 | // Created by Karim Angama on 20/01/2022.
6 | //
7 |
8 | import WatchKit
9 | import Foundation
10 |
11 | class ResultInterfaceController: BaseInterfaceController {
12 |
13 | @IBOutlet weak var gravityXLabel: WKInterfaceLabel!
14 | @IBOutlet weak var gravityYLabel: WKInterfaceLabel!
15 | @IBOutlet weak var gravityZLabel: WKInterfaceLabel!
16 |
17 | @IBOutlet weak var accelerationXLabel: WKInterfaceLabel!
18 | @IBOutlet weak var accelerationYLabel: WKInterfaceLabel!
19 | @IBOutlet weak var accelerationZLabel: WKInterfaceLabel!
20 |
21 | @IBOutlet weak var rotationXLabel: WKInterfaceLabel!
22 | @IBOutlet weak var rotationYLabel: WKInterfaceLabel!
23 | @IBOutlet weak var rotationZLabel: WKInterfaceLabel!
24 |
25 | override func awake(withContext context: Any?) {
26 | super.awake(withContext: context)
27 | }
28 |
29 | override func setupObservers() {
30 | super.setupObservers()
31 |
32 | viewModel.$gravityX
33 | .receive(on: DispatchQueue.main)
34 | .assign(to: \.text, on: gravityXLabel)
35 | .store(in: &cancellable)
36 |
37 | viewModel.$gravityY
38 | .receive(on: DispatchQueue.main)
39 | .assign(to: \.text, on: gravityYLabel)
40 | .store(in: &cancellable)
41 |
42 | viewModel.$gravityZ
43 | .receive(on: DispatchQueue.main)
44 | .assign(to: \.text, on: gravityZLabel)
45 | .store(in: &cancellable)
46 |
47 | viewModel.$accelerationX
48 | .receive(on: DispatchQueue.main)
49 | .assign(to: \.text, on: accelerationXLabel)
50 | .store(in: &cancellable)
51 |
52 | viewModel.$accelerationY
53 | .receive(on: DispatchQueue.main)
54 | .assign(to: \.text, on: accelerationYLabel)
55 | .store(in: &cancellable)
56 |
57 | viewModel.$accelerationZ
58 | .receive(on: DispatchQueue.main)
59 | .assign(to: \.text, on: accelerationZLabel)
60 | .store(in: &cancellable)
61 |
62 |
63 | viewModel.$rotationX
64 | .receive(on: DispatchQueue.main)
65 | .assign(to: \.text, on: rotationXLabel)
66 | .store(in: &cancellable)
67 |
68 | viewModel.$rotationY
69 | .receive(on: DispatchQueue.main)
70 | .assign(to: \.text, on: rotationYLabel)
71 | .store(in: &cancellable)
72 |
73 | viewModel.$rotationZ
74 | .receive(on: DispatchQueue.main)
75 | .assign(to: \.text, on: rotationZLabel)
76 | .store(in: &cancellable)
77 | }
78 |
79 | }
80 |
--------------------------------------------------------------------------------
/MotionTracking WatchKit Extension/Common/Manager/ConnectivityManager.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ConnectivityManager.swift
3 | // MotionTracking WatchKit Extension
4 | //
5 | // Created by Karim Angama on 20/01/2022.
6 | //
7 |
8 | import Foundation
9 | import WatchConnectivity
10 | import CoreText
11 |
12 | /**
13 | `ConnectivityManagerDelegate` exists to inform delegates of receive and finish transfer file.
14 | */
15 | @objc protocol ConnectivityManagerDelegate: AnyObject {
16 |
17 | /// Called on the delegate of the receiver. Will be called on startup if the incoming message caused the receiver to launch.
18 | @objc optional func didReceiveFile(file: URL)
19 |
20 | /// Called on the sending side after the file transfer has successfully completed or failed with an error. Will be called on next launch if the sender was not running when the transfer finished
21 | @objc optional func didFinishFileTransfer(error: Error?)
22 | }
23 |
24 | /**
25 | `ConnectivityManager` manage the transfer of files
26 |
27 | */
28 | class ConnectivityManager: NSObject {
29 |
30 | /// Session Watch Connectivity
31 | private var session = WCSession.default
32 |
33 | /// Yes if the transfer is in progress
34 | private(set) var isFileTransfer = false
35 |
36 | /// Delegate inform of receive and finish transfer file
37 | weak var delegate: ConnectivityManagerDelegate?
38 |
39 |
40 | /**
41 | Initialization
42 |
43 | Activate transfert session
44 | */
45 | override init() {
46 | super.init()
47 | if !WCSession.isSupported() {
48 | print("Session is not available.")
49 | // TODO error managment
50 | return
51 | }
52 | session.delegate = self
53 | session.activate()
54 | }
55 |
56 | /**
57 | Transfer file to iPhone
58 |
59 | @param Url txt file
60 | */
61 | func sendFile(file: URL) {
62 | let value = session.transferFile(file, metadata: nil)
63 | isFileTransfer = value.isTransferring
64 | }
65 |
66 | }
67 |
68 | extension ConnectivityManager: WCSessionDelegate {
69 |
70 | #if os(iOS)
71 | func sessionDidBecomeInactive(_ session: WCSession) { }
72 |
73 | func sessionDidDeactivate(_ session: WCSession) { }
74 | #endif
75 |
76 | func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
77 | debugPrint("activationDidComplete: \(activationState.rawValue), error: \(String(describing: error))")
78 | // TODO error managment
79 | }
80 |
81 | func session(_ session: WCSession, didFinish fileTransfer: WCSessionFileTransfer, error: Error?) {
82 | isFileTransfer = false
83 | guard let didFinishFileTransfer = delegate?.didFinishFileTransfer else { return }
84 | didFinishFileTransfer(error)
85 | }
86 |
87 | func session(_ session: WCSession, didReceive file: WCSessionFile) {
88 | guard let didReceiveFile = delegate?.didReceiveFile else { return }
89 | didReceiveFile(file.fileURL)
90 | }
91 |
92 | }
93 |
--------------------------------------------------------------------------------
/MotionTracking/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "size" : "20x20",
5 | "idiom": "iphone",
6 | "filename" : "Logo-motion-traking-20@2x.png",
7 | "scale": "2x"
8 | },
9 | {
10 | "size" : "20x20",
11 | "idiom": "iphone",
12 | "filename" : "Logo-motion-traking-20@3x.png",
13 | "scale": "3x"
14 | },
15 | {
16 | "size" : "20x20",
17 | "idiom": "ipad",
18 | "filename" : "Logo-motion-traking-20.png",
19 | "scale": "1x"
20 | },
21 | {
22 | "size" : "20x20",
23 | "idiom": "ipad",
24 | "filename" : "Logo-motion-traking-20@2x.png",
25 | "scale": "2x"
26 | },
27 | {
28 | "size" : "29x29",
29 | "idiom" : "iphone",
30 | "filename" : "Logo-motion-traking-29@2x.png",
31 | "scale" : "2x"
32 | },
33 | {
34 | "size" : "29x29",
35 | "idiom" : "iphone",
36 | "filename" : "Logo-motion-traking-29@3x.png",
37 | "scale" : "3x"
38 | },
39 | {
40 | "size" : "40x40",
41 | "idiom" : "iphone",
42 | "filename" : "Logo-motion-traking-40@2x.png",
43 | "scale" : "2x"
44 | },
45 | {
46 | "size" : "40x40",
47 | "idiom" : "iphone",
48 | "filename" : "Logo-motion-traking-40@3x.png",
49 | "scale" : "3x"
50 | },
51 | {
52 | "size" : "60x60",
53 | "idiom" : "iphone",
54 | "filename" : "Logo-motion-traking-60@2x.png",
55 | "scale" : "2x"
56 | },
57 | {
58 | "size" : "60x60",
59 | "idiom" : "iphone",
60 | "filename" : "Logo-motion-traking-60@3x.png",
61 | "scale" : "3x"
62 | },
63 | {
64 | "size" : "29x29",
65 | "idiom" : "ipad",
66 | "filename" : "Logo-motion-traking-29.png",
67 | "scale" : "1x"
68 | },
69 | {
70 | "size" : "29x29",
71 | "idiom" : "ipad",
72 | "filename" : "Logo-motion-traking-29@2x.png",
73 | "scale" : "2x"
74 | },
75 | {
76 | "size" : "40x40",
77 | "idiom" : "ipad",
78 | "filename" : "Logo-motion-traking-40.png",
79 | "scale" : "1x"
80 | },
81 | {
82 | "size" : "40x40",
83 | "idiom" : "ipad",
84 | "filename" : "Logo-motion-traking-40@2x.png",
85 | "scale" : "2x"
86 | },
87 | {
88 | "size" : "76x76",
89 | "idiom" : "ipad",
90 | "filename" : "Logo-motion-traking-76.png",
91 | "scale" : "1x"
92 | },
93 | {
94 | "size" : "76x76",
95 | "idiom" : "ipad",
96 | "filename" : "Logo-motion-traking-76@2x.png",
97 | "scale" : "2x"
98 | },
99 | {
100 | "size" : "83.5x83.5",
101 | "idiom" : "ipad",
102 | "filename" : "Logo-motion-traking-83.5@2x.png",
103 | "scale" : "2x"
104 | },
105 | {
106 | "size" : "1024x1024",
107 | "idiom" : "ios-marketing",
108 | "filename" : "Logo-motion-traking-1024.png",
109 | "scale" : "1x"
110 | }
111 | ],
112 | "info" : {
113 | "version" : 1,
114 | "author" : "xcode"
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/MotionTracking WatchKit Extension/Home/HomeInterfaceController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // InterfaceController.swift
3 | // MotionTracking WatchKit Extension
4 | //
5 | // Created by Karim Angama on 19/01/2022.
6 | //
7 |
8 | import WatchKit
9 | import Foundation
10 | import Combine
11 |
12 |
13 | class HomeInterfaceController: BaseInterfaceController {
14 |
15 | @IBOutlet weak var indicatorImage: WKInterfaceImage!
16 | @IBOutlet weak var numberSavedLabel: WKInterfaceLabel!
17 | @IBOutlet weak var numberErrorLabel: WKInterfaceLabel!
18 |
19 | override func awake(withContext context: Any?) {
20 | // Configure interface objects here.
21 | }
22 |
23 | override func setupObservers() {
24 | super.setupObservers()
25 |
26 | // Show the tracking screen
27 | viewModel.showTrackingScreen
28 | .sink(receiveValue: { [weak self] _ in
29 | self?.openReadyScreen()
30 | })
31 | .store(in: &cancellable)
32 |
33 | // Show alert message no location
34 | viewModel.showAlertMessageNoLocation
35 | .sink(receiveValue: { [weak self] _ in
36 | self?.showAlertMessageNoLocation()
37 | })
38 | .store(in: &cancellable)
39 |
40 | // Display loader indicator
41 | viewModel.$isShowLoaderIndicator
42 | .receive(on: DispatchQueue.main)
43 | .assign(to: \.isLoaderIndicator, on: indicatorImage)
44 | .store(in: &cancellable)
45 |
46 | // Display number files saved
47 | viewModel.$numberFileSending
48 | .receive(on: DispatchQueue.main)
49 | .map { "\($0)" }
50 | .assign(to: \.text, on: numberSavedLabel)
51 | .store(in: &cancellable)
52 |
53 | // Displays number files not saved
54 | viewModel.$numberFileError
55 | .receive(on: DispatchQueue.main)
56 | .map { "\($0)" }
57 | .assign(to: \.text, on: numberErrorLabel)
58 | .store(in: &cancellable)
59 |
60 |
61 | }
62 |
63 | @IBAction func openTrackingInterface() {
64 | viewModel.initMotionManager()
65 | viewModel.handleLocationAuthorization()
66 | }
67 |
68 | private func openReadyScreen() -> Void {
69 | guard let manager = viewModel.motionManager else { return }
70 | DispatchQueue.main.async {
71 | DispatchQueue.main.async {
72 | WKInterfaceController.reloadRootControllers(withNamesAndContexts: [(
73 | name: "ReadyInterfaceController",
74 | context: manager
75 | )])
76 | }
77 | }
78 | }
79 |
80 | private func showAlertMessageNoLocation() {
81 | let okAction = WKAlertAction(title: "Ok, start anyway", style: .default){ [weak self] in
82 | self?.openReadyScreen()
83 | }
84 | presentAlert(
85 | withTitle: "Geolocation",
86 | message: "You must accept the geolocation to be able to retrieve your data in the background.",
87 | preferredStyle: WKAlertControllerStyle.alert,
88 | actions: [okAction]
89 | )
90 | }
91 |
92 | }
93 |
--------------------------------------------------------------------------------
/MotionTracking WatchKit Extension/ExtensionDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ExtensionDelegate.swift
3 | // MotionTracking WatchKit Extension
4 | //
5 | // Created by Karim Angama on 19/01/2022.
6 | //
7 |
8 | import WatchKit
9 |
10 | class ExtensionDelegate: NSObject, WKExtensionDelegate {
11 |
12 | let connectivityManager = ConnectivityManager()
13 |
14 | func applicationDidFinishLaunching() {
15 | // Perform any final initialization of your application.
16 | }
17 |
18 | func applicationDidBecomeActive() {
19 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
20 | }
21 |
22 | func applicationWillResignActive() {
23 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
24 | // Use this method to pause ongoing tasks, disable timers, etc.
25 | }
26 |
27 | func handle(_ backgroundTasks: Set) {
28 | // Sent when the system needs to launch the application in the background to process tasks. Tasks arrive in a set, so loop through and process each one.
29 | for task in backgroundTasks {
30 | // Use a switch statement to check the task type
31 | switch task {
32 | case let backgroundTask as WKApplicationRefreshBackgroundTask:
33 | // Be sure to complete the background task once you’re done.
34 | backgroundTask.setTaskCompletedWithSnapshot(false)
35 | case let snapshotTask as WKSnapshotRefreshBackgroundTask:
36 | // Snapshot tasks have a unique completion call, make sure to set your expiration date
37 | snapshotTask.setTaskCompleted(restoredDefaultState: true, estimatedSnapshotExpiration: Date.distantFuture, userInfo: nil)
38 | case let connectivityTask as WKWatchConnectivityRefreshBackgroundTask:
39 | // Be sure to complete the connectivity task once you’re done.
40 | connectivityTask.setTaskCompletedWithSnapshot(false)
41 | case let urlSessionTask as WKURLSessionRefreshBackgroundTask:
42 | // Be sure to complete the URL session task once you’re done.
43 | urlSessionTask.setTaskCompletedWithSnapshot(false)
44 | case let relevantShortcutTask as WKRelevantShortcutRefreshBackgroundTask:
45 | // Be sure to complete the relevant-shortcut task once you're done.
46 | relevantShortcutTask.setTaskCompletedWithSnapshot(false)
47 | case let intentDidRunTask as WKIntentDidRunRefreshBackgroundTask:
48 | // Be sure to complete the intent-did-run task once you're done.
49 | intentDidRunTask.setTaskCompletedWithSnapshot(false)
50 | default:
51 | // make sure to complete unhandled task types
52 | task.setTaskCompletedWithSnapshot(false)
53 | }
54 | }
55 | }
56 |
57 | }
58 |
59 | extension WKExtension {
60 |
61 | static func app() -> ExtensionDelegate {
62 | return WKExtension.shared().delegate as! ExtensionDelegate
63 | }
64 |
65 | }
66 |
--------------------------------------------------------------------------------
/MotionTracking WatchKit App/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "Logo-motion-traking-24@2x.png",
5 | "idiom" : "watch",
6 | "role" : "notificationCenter",
7 | "scale" : "2x",
8 | "size" : "24x24",
9 | "subtype" : "38mm"
10 | },
11 | {
12 | "filename" : "Logo-motion-traking-27-5@2x.png",
13 | "idiom" : "watch",
14 | "role" : "notificationCenter",
15 | "scale" : "2x",
16 | "size" : "27.5x27.5",
17 | "subtype" : "42mm"
18 | },
19 | {
20 | "filename" : "Logo-motion-traking-29@2x.png",
21 | "idiom" : "watch",
22 | "role" : "companionSettings",
23 | "scale" : "2x",
24 | "size" : "29x29"
25 | },
26 | {
27 | "filename" : "Logo-motion-traking-29@3x.png",
28 | "idiom" : "watch",
29 | "role" : "companionSettings",
30 | "scale" : "3x",
31 | "size" : "29x29"
32 | },
33 | {
34 | "filename" : "Logo-motion-traking-33@2x.png",
35 | "idiom" : "watch",
36 | "role" : "notificationCenter",
37 | "scale" : "2x",
38 | "size" : "33x33",
39 | "subtype" : "45mm"
40 | },
41 | {
42 | "filename" : "Logo-motion-traking-40@2x.png",
43 | "idiom" : "watch",
44 | "role" : "appLauncher",
45 | "scale" : "2x",
46 | "size" : "40x40",
47 | "subtype" : "38mm"
48 | },
49 | {
50 | "filename" : "Logo-motion-traking-44@2x.png",
51 | "idiom" : "watch",
52 | "role" : "appLauncher",
53 | "scale" : "2x",
54 | "size" : "44x44",
55 | "subtype" : "40mm"
56 | },
57 | {
58 | "filename" : "Logo-motion-traking-46@2x.png",
59 | "idiom" : "watch",
60 | "role" : "appLauncher",
61 | "scale" : "2x",
62 | "size" : "46x46",
63 | "subtype" : "41mm"
64 | },
65 | {
66 | "filename" : "Logo-motion-traking-50@2x.png",
67 | "idiom" : "watch",
68 | "role" : "appLauncher",
69 | "scale" : "2x",
70 | "size" : "50x50",
71 | "subtype" : "44mm"
72 | },
73 | {
74 | "filename" : "Logo-motion-traking-51@2x.png",
75 | "idiom" : "watch",
76 | "role" : "appLauncher",
77 | "scale" : "2x",
78 | "size" : "51x51",
79 | "subtype" : "45mm"
80 | },
81 | {
82 | "filename" : "Logo-motion-traking-86@2x.png",
83 | "idiom" : "watch",
84 | "role" : "quickLook",
85 | "scale" : "2x",
86 | "size" : "86x86",
87 | "subtype" : "38mm"
88 | },
89 | {
90 | "filename" : "Logo-motion-traking-98@2x.png",
91 | "idiom" : "watch",
92 | "role" : "quickLook",
93 | "scale" : "2x",
94 | "size" : "98x98",
95 | "subtype" : "42mm"
96 | },
97 | {
98 | "filename" : "Logo-motion-traking-108@2x.png",
99 | "idiom" : "watch",
100 | "role" : "quickLook",
101 | "scale" : "2x",
102 | "size" : "108x108",
103 | "subtype" : "44mm"
104 | },
105 | {
106 | "filename" : "Logo-motion-traking-117@2x.png",
107 | "idiom" : "watch",
108 | "role" : "quickLook",
109 | "scale" : "2x",
110 | "size" : "117x117",
111 | "subtype" : "45mm"
112 | },
113 | {
114 | "filename" : "Logo-motion-traking-1024@1x.png",
115 | "idiom" : "watch-marketing",
116 | "scale" : "1x",
117 | "size" : "1024x1024"
118 | }
119 | ],
120 | "info" : {
121 | "author" : "xcode",
122 | "version" : 1
123 | }
124 | }
125 |
126 |
--------------------------------------------------------------------------------
/MotionTracking WatchKit Extension/Tracking/TrackingViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TrackingViewModel.swift
3 | // MotionTracking WatchKit Extension
4 | //
5 | // Created by Karim Angama on 31/01/2022.
6 | //
7 |
8 | import Foundation
9 | import WatchKit
10 | import Combine
11 |
12 | class TrackingViewModel: BaseViewModel {
13 |
14 | /// Propertie
15 | let fileTrackingManager = FileTrackingManager()
16 | var motionManager: MotionManager?
17 | var timer: Timer?
18 |
19 | /// Input observable
20 | @Published var appearScreen = false
21 |
22 | /// Output observable
23 | @Published var isStartTraking = true
24 | @Published var timerValue = Parameters.getTimer() + 0.5
25 | var isFileSaved = PassthroughSubject<(), ErrorApp>()
26 | var isShowSaveDataMessage = PassthroughSubject<(), Never>()
27 | var isStopTracking = PassthroughSubject<(), Never>()
28 |
29 |
30 | // MARK: Override method
31 |
32 | override func context(object: Any?) {
33 | motionManager = object as? MotionManager
34 | }
35 |
36 | override func observers() {
37 | super.observers()
38 |
39 | Publishers.CombineLatest($isStartTraking, $appearScreen)
40 | .eraseToAnyPublisher()
41 | .filter { !$0 && $1 }
42 | .map { _ in () }
43 | .sink {[weak self] _ in
44 | self?.isShowSaveDataMessage.send(())
45 | }
46 | .store(in: &cancellable)
47 |
48 | }
49 |
50 | // MARK: Public method
51 |
52 | func stopTracking() {
53 | motionManager?.stopUpdates()
54 | isStartTraking = false
55 | timer?.invalidate()
56 | isStopTracking.send(())
57 | }
58 |
59 | func startTraking() {
60 | motionManager?.startUpdates()
61 | isStartTraking = true
62 | startTimer()
63 | }
64 |
65 | func saveData() {
66 |
67 | guard let result = motionManager?.resultMotion else { return }
68 | do {
69 | // Write json file
70 | let json = try JSONEncoder().encode(entity: result)
71 | let filePath = try fileTrackingManager.writeFileTransfer(
72 | json: json,
73 | location: Parameters.getLocation(),
74 | time: Parameters.getTimer(),
75 | count: result.count
76 | )
77 |
78 | // Transfer json file to iPhone
79 | WKExtension.app().connectivityManager.sendFile(file: filePath)
80 | isFileSaved.send(())
81 | isFileSaved.send(completion: .finished)
82 | } catch {
83 | isFileSaved.send(completion: .failure(
84 | ErrorApp(
85 | title: "File erreur",
86 | description: "The message could not be saved: \(error.localizedDescription)"
87 | )
88 | ))
89 | }
90 | }
91 |
92 | // MARK: Private method
93 |
94 | private func startTimer() {
95 | timer = Timer.scheduledTimer(withTimeInterval: timerValue, repeats: false) { [weak self] _ in
96 | self?.stopTracking()
97 | }
98 | }
99 |
100 | }
101 |
102 |
103 | extension JSONEncoder {
104 |
105 | func encode(entity: T) throws -> String where T : Encodable {
106 | let jsonData = try self.encode(entity)
107 | if let json = String(data: jsonData, encoding: String.Encoding.utf8) {
108 | return json
109 | }
110 | throw NSError(
111 | domain: "com.kangama.MotionTracking",
112 | code: 0,
113 | userInfo: [NSLocalizedDescriptionKey: "Json error"]
114 | )
115 | }
116 |
117 | }
118 |
--------------------------------------------------------------------------------
/MotionTrackingTests/ExportFileViewModelTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ExportFileViewModelTests.swift
3 | // MotionTrackingTests
4 | //
5 | // Created by Karim Angama on 19/03/2024.
6 | //
7 |
8 | import XCTest
9 | @testable import MotionTracking
10 |
11 | final class ExportFileViewModelTests: XCTestCase {
12 |
13 | var viewModel: ExportFileViewModel!
14 |
15 | override func setUpWithError() throws {
16 | viewModel = ExportFileViewModel()
17 | }
18 |
19 | override func tearDownWithError() throws {
20 | viewModel = nil
21 | }
22 |
23 | func test_init_fields_all_true() throws {
24 | XCTAssertEqual(viewModel.fileManager.fields, CSVFileFields())
25 | }
26 |
27 | func test_gravity_is_false() throws {
28 | viewModel.observers()
29 | viewModel.gravityValue = false
30 | XCTAssertEqual(
31 | viewModel.fileManager.fields,
32 | CSVFileFields(gravityX: false, gravityY: false, gravityZ: false)
33 | )
34 | }
35 |
36 | func test_altitude_is_false() throws {
37 | viewModel.observers()
38 | viewModel.altitudeValue = false
39 | XCTAssertEqual(
40 | viewModel.fileManager.fields,
41 | CSVFileFields(locationAltitude: false)
42 | )
43 | }
44 |
45 | func test_cordinate_is_false() throws {
46 | viewModel.observers()
47 | viewModel.cordinateValue = false
48 | XCTAssertEqual(
49 | viewModel.fileManager.fields,
50 | CSVFileFields(locationLatitude: false, locationLongitude: false)
51 | )
52 | }
53 |
54 | func test_acceleration_is_false() throws {
55 | viewModel.observers()
56 | viewModel.accelerationValue = false
57 | XCTAssertEqual(
58 | viewModel.fileManager.fields,
59 | CSVFileFields(
60 | userAccelerationX: false,
61 | userAccelerationY: false,
62 | userAccelerationZ: false
63 | )
64 | )
65 | }
66 |
67 | func test_rotation_is_false() throws {
68 | viewModel.observers()
69 | viewModel.rotationValue = false
70 | XCTAssertEqual(
71 | viewModel.fileManager.fields,
72 | CSVFileFields(
73 | rotationRateX: false,
74 | rotationRateY: false,
75 | rotationRateZ: false
76 | )
77 | )
78 | }
79 |
80 | func test_attitude_is_false() throws {
81 | viewModel.observers()
82 | viewModel.attitudeValue = false
83 | XCTAssertEqual(
84 | viewModel.fileManager.fields,
85 | CSVFileFields(
86 | attitudeRoll: false,
87 | attitudePitch: false,
88 | attitudeYaw: false
89 | )
90 | )
91 | }
92 |
93 | func test_timestamp_is_false() throws {
94 | viewModel.observers()
95 | viewModel.timestampValue = false
96 | XCTAssertEqual(
97 | viewModel.fileManager.fields,
98 | CSVFileFields(timestamp: false)
99 | )
100 | }
101 |
102 | func test_rotation_and_acceleration_is_false() throws {
103 | viewModel.observers()
104 | viewModel.rotationValue = false
105 | viewModel.accelerationValue = false
106 | XCTAssertEqual(
107 | viewModel.fileManager.fields,
108 | CSVFileFields(
109 | rotationRateX: false,
110 | rotationRateY: false,
111 | rotationRateZ: false,
112 | userAccelerationX: false,
113 | userAccelerationY: false,
114 | userAccelerationZ: false
115 | )
116 | )
117 | }
118 |
119 | }
120 |
--------------------------------------------------------------------------------
/MotionTracking/Base.lproj/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
26 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
--------------------------------------------------------------------------------
/MotionTracking WatchKit Extension/Tracking/TrackingInterfaceController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TrackingInterfaceController.swift
3 | // MotionTracking WatchKit Extension
4 | //
5 | // Created by Karim Angama on 19/01/2022.
6 | //
7 |
8 | import WatchKit
9 | import Foundation
10 | import UIKit
11 |
12 | class TrackingInterfaceController: BaseInterfaceController {
13 |
14 | @IBOutlet weak var interfaceTimer: WKInterfaceTimer!
15 |
16 | // MARK: Override method
17 |
18 | override func awake(withContext context: Any?) {
19 | super.awake(withContext: context)
20 | viewModel.startTraking()
21 | }
22 |
23 | override func didAppear() {
24 | // Inform the viewmodel that the view is displayed
25 | // For the alert message 'Save data'
26 | viewModel.appearScreen = true
27 | }
28 |
29 | override func willDisappear() {
30 | // Inform the viewmodel that the view is not displayed
31 | viewModel.appearScreen = false
32 | }
33 |
34 | override func setupObservers() {
35 | super.setupObservers()
36 |
37 | // Start timer
38 | viewModel.$isStartTraking
39 | .receive(on: DispatchQueue.main)
40 | .assign(to: \.isStart, on: interfaceTimer)
41 | .store(in: &cancellable)
42 |
43 | // Display initial timer
44 | viewModel.$timerValue
45 | .receive(on: DispatchQueue.main)
46 | .map { Date(timeIntervalSinceNow: $0) }
47 | .assign(to: \.date, on: interfaceTimer)
48 | .store(in: &cancellable)
49 |
50 | // When tracking is finished, return to this screen if user is on the result screen
51 | viewModel.isStopTracking
52 | .receive(on: DispatchQueue.main)
53 | .sink(receiveValue: { [weak self] _ in
54 | self?.becomeCurrentPage() // Returns to this screen.
55 | WKInterfaceDevice.current().play(.success)
56 | })
57 | .store(in: &cancellable)
58 |
59 | // Displays the save data alert message if the timer is over
60 | // and the user returns to this screen.
61 | viewModel.isShowSaveDataMessage
62 | .receive(on: DispatchQueue.main)
63 | .sink(receiveValue: { [weak self] _ in
64 | self?.showAlertSaveData()
65 | })
66 | .store(in: &cancellable)
67 |
68 | // When file saved, return on the home screen
69 | // Display a message, if there is an error
70 | viewModel.isFileSaved
71 | .receive(on: DispatchQueue.main)
72 | .sink(receiveCompletion: { [weak self] result in
73 | switch result {
74 | case .failure(let error):
75 | self?.showAlertErrorMessage(error)
76 | case .finished: break
77 | }
78 | }, receiveValue: { [weak self] _ in
79 | self?.reloadStartScreen()
80 | })
81 | .store(in: &cancellable)
82 |
83 |
84 | }
85 |
86 | // MARK: Action method
87 |
88 | @IBAction func stopAction() {
89 | viewModel.stopTracking()
90 | }
91 |
92 | // MARK: Private method
93 |
94 | private func showAlertSaveData() {
95 | let noAction = WKAlertAction(title: "No", style: .cancel){ [weak self] in
96 | self?.reloadStartScreen()
97 | }
98 | let yesAction = WKAlertAction(title: "Yes", style: .default) { [weak self] in
99 | self?.viewModel.saveData()
100 | }
101 | presentAlert(
102 | withTitle: "Tracking stoped",
103 | message: "Do you want save data?",
104 | preferredStyle: WKAlertControllerStyle.alert,
105 | actions: [noAction, yesAction]
106 | )
107 | }
108 |
109 | private func showAlertErrorMessage(_ error: ErrorApp) {
110 | let okAction = WKAlertAction(title: "Ok", style: .cancel) { [weak self] in
111 | self?.reloadStartScreen()
112 | }
113 | presentAlert(
114 | withTitle: error.title ?? "Error",
115 | message: error.description ?? error.localizedDescription,
116 | preferredStyle: WKAlertControllerStyle.alert,
117 | actions: [okAction]
118 | )
119 | }
120 |
121 | private func reloadStartScreen() -> Void {
122 | WKInterfaceController.reloadRootPageControllers(
123 | withNames: ["InterfaceController", "SettingsInterfaceController"],
124 | contexts: [],
125 | orientation: .horizontal,
126 | pageIndex: 0
127 | )
128 | }
129 |
130 | }
131 |
--------------------------------------------------------------------------------
/MotionTracking/ExportFile/ExportFileViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ExportFileViewModel.swift
3 | // MotionTracking
4 | //
5 | // Created by Karim Angama on 05/02/2022.
6 | //
7 |
8 | import Foundation
9 | import Combine
10 |
11 | class ExportFileViewModel: BaseViewModel {
12 |
13 | typealias SwitchValueType = ((Bool, Bool, Bool, Bool, Bool), (Bool, Bool))
14 |
15 | /// Properties
16 | let fileManager = CSVFileManager()
17 |
18 | /// Output
19 | @Published var infomation: String?
20 | @Published var fileTrackingEntity: [FileTrackingEntity] = []
21 | @Published var enabledExportButton = false
22 |
23 | // Input
24 | @Published var gravityValue = true
25 | @Published var timestampValue = true
26 | @Published var rotationValue = true
27 | @Published var accelerationValue = true
28 | @Published var attitudeValue = true
29 | @Published var cordinateValue = true
30 | @Published var altitudeValue = true
31 | @Published var enabledLocationValue = true
32 | @Published var isLoading = false
33 | @Published var sharingCsvPathFile = PassthroughSubject()
34 |
35 |
36 | // MARK: Override method
37 |
38 | override func observers() {
39 | super.observers()
40 |
41 | // Disable location switches if they are not present in the file
42 | $fileTrackingEntity
43 | .sink { [weak self] entity in
44 | if let isLocation = entity.first?.isLocation, !isLocation {
45 | self?.cordinateValue = false
46 | self?.altitudeValue = false
47 | self?.enabledLocationValue = false
48 | }
49 | }
50 | .store(in: &cancellable)
51 |
52 | // Pass the information file to display it
53 | $fileTrackingEntity
54 | .sink { [weak self] entities in
55 | self?.infomation = self?.information(entities)
56 | }
57 | .store(in: &cancellable)
58 |
59 | // Test if a switch is at least true
60 | combineSwitchValue()
61 | .map { (motion, location) in
62 | motion.0 || motion.1 || motion.2 || motion.3 ||
63 | location.0 || location.1
64 | }
65 | .removeDuplicates()
66 | .assign(to: &$enabledExportButton)
67 |
68 | // Set fields for create the CSV
69 | combineSwitchValue()
70 | .map(FieldsMapper.mapper)
71 | .sink { [weak self] fields in
72 | self?.fileManager.setFiels(fields: fields)
73 | }
74 | .store(in: &cancellable)
75 |
76 | }
77 |
78 | // MARK: Public method
79 |
80 | func exportCSV() {
81 | isLoading = true
82 | if self.fileTrackingEntity.count > 1 {
83 | self.fileManager.save(
84 | filesUrl: self.fileTrackingEntity.map { $0.fileUrl },
85 | block: { zipPath, error in
86 | if let error = error {
87 | self.error(error)
88 | } else {
89 | self.sharingCsvPathFile.send(zipPath)
90 | }
91 | self.isLoading = false
92 | }
93 | )
94 | } else {
95 | guard let fileURL = fileTrackingEntity.first?.fileUrl else { return }
96 | self.fileManager.save(fileUrl: fileURL) { csvPath, error in
97 | if let error = error {
98 | self.error(error)
99 | } else if let csvPath = csvPath {
100 | self.sharingCsvPathFile.send(csvPath)
101 | }
102 | self.isLoading = false
103 | }
104 | }
105 | }
106 |
107 | // MARK: Private method
108 |
109 | private func information(_ entities: [FileTrackingEntity]) -> String{
110 | if entities.count == 1 {
111 | return entities.first?.information ?? ""
112 | }
113 | return "Files to export: \(entities.count)"
114 | }
115 |
116 | private func combineSwitchValue() -> some Publisher {
117 | Publishers.CombineLatest(
118 | Publishers.CombineLatest4(
119 | $timestampValue,
120 | $gravityValue,
121 | $rotationValue,
122 | $accelerationValue
123 | ),
124 | Publishers.CombineLatest3(
125 | $attitudeValue,
126 | $cordinateValue,
127 | $altitudeValue
128 | )
129 | ).map {
130 | (
131 | ($0.0.0, $0.0.1, $0.0.2, $0.0.3, $0.1.0),
132 | ($0.1.1, $0.1.2)
133 | )
134 | }
135 | .eraseToAnyPublisher()
136 | }
137 |
138 | }
139 |
--------------------------------------------------------------------------------
/MotionTracking/FilesList/FilesListViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FilesListViewModel.swift
3 | // MotionTracking
4 | //
5 | // Created by Karim Angama on 03/02/2022.
6 | //
7 |
8 | import Foundation
9 | import Combine
10 | import UIKit
11 |
12 | class FilesListViewModel: BaseViewModel {
13 |
14 | // Property
15 | var fileTrackingManager = FileTrackingManager()
16 |
17 | /// Output observable
18 | @Published var isFilesExist = false
19 | @Published var filesSelected: [FileTrackingEntity] = []
20 | @Published var showToolBar = false
21 | @Published var isEditingTableView = false
22 | @Published var isEditingViewController = false
23 | @Published var isEnabledButton = false
24 | @Published var isAllSelected = false
25 | @Published var stateSelectedButton = false
26 | var filesDidChange = PassthroughSubject()
27 | var removeFiles = PassthroughSubject<[Int], Never>()
28 | var inserFile = PassthroughSubject()
29 |
30 | /// Output property
31 | var files = [FileTrackingEntity]()
32 |
33 |
34 | // MARK: Override method
35 |
36 | override func exeUseCase() {
37 | super.exeUseCase()
38 | getFilesList()
39 | UIApplication.app().connectivityManager.delegate = self
40 | }
41 |
42 | override func observers() {
43 | super.observers()
44 |
45 | // Hide the message if there is a list of files
46 | Publishers.Merge3(
47 | filesDidChange,
48 | removeFiles.map { _ in () },
49 | inserFile
50 | )
51 | .map { [weak self] _ in
52 | self?.files.count ?? 0 > 0
53 | }
54 | .assign(to: \.isFilesExist, on: self)
55 | .store(in: &cancellable)
56 |
57 | $isFilesExist
58 | .filter { [weak self] value in
59 | return (self?.isEditingTableView ?? false) && !value
60 | }
61 | .assign(to: &$isEditingViewController)
62 |
63 | // Enable button when at least one row is selected
64 | $filesSelected
65 | .map { $0.count > 0}
66 | .assign(to: &$isEnabledButton)
67 |
68 | // Show toolbar when editing is open
69 | $isEditingTableView.assign(to: &$showToolBar)
70 |
71 | // Remove all selected files when editing is close
72 | $isEditingTableView.sink { [weak self] value in
73 | if !value {
74 | self?.isAllSelected = false
75 | }
76 | }
77 | .store(in: &cancellable)
78 |
79 | // Change tile selected button
80 | $isAllSelected
81 | .assign(to: &$stateSelectedButton)
82 |
83 | $filesSelected
84 | .sink { [weak self] entities in
85 | if entities.count > 0 && entities.count == self?.files.count && !(self?.isAllSelected ?? false) {
86 | self?.isAllSelected = true
87 | }
88 | }
89 | .store(in: &cancellable)
90 |
91 |
92 | }
93 |
94 | // MARK: Public method
95 |
96 | func addFile(file: URL) {
97 | do {
98 | let entity = try fileTrackingManager.moveCachesToSupportDirectory(fileUrl: file)
99 | files.insert(entity, at: 0)
100 | inserFile.send()
101 | } catch {
102 | self.error(error)
103 | }
104 | }
105 |
106 | func reloadFiles() {
107 | files.removeAll()
108 | getFilesList()
109 | }
110 |
111 | func removeFile(index: Int) {
112 | do {
113 | try fileTrackingManager.removeFile(fileUrl: files[index].fileUrl)
114 | files.remove(at: index)
115 | removeFiles.send([index])
116 | } catch {
117 | self.error(error)
118 | }
119 | }
120 |
121 | func removeMultiFiles() {
122 | var index: [Int:FileTrackingEntity] = [:]
123 | do {
124 | for entity in filesSelected {
125 | try fileTrackingManager.removeFile(fileUrl: entity.fileUrl)
126 | if let i = files.firstIndex(where: { $0 == entity }) {
127 | index[i] = entity
128 | }
129 | }
130 | } catch {
131 | self.error(error)
132 | }
133 | for (_, entity) in index {
134 | files.removeAll { $0 == entity }
135 | }
136 | removeFiles.send(index.map{ $0.key })
137 | index.removeAll()
138 | }
139 |
140 | // MARK: Private method
141 |
142 | private func getFilesList() {
143 | let arrayFiles = fileTrackingManager.filesTracking()
144 | files.append(contentsOf: arrayFiles)
145 | filesDidChange.send()
146 | }
147 |
148 | }
149 |
150 | // MARK: Connectivity Manager Delegate
151 |
152 | extension FilesListViewModel: ConnectivityManagerDelegate {
153 |
154 | func didReceiveFile(file: URL) {
155 | addFile(file: file)
156 | }
157 |
158 | }
159 |
--------------------------------------------------------------------------------
/MotionTracking/Common/Manager/FileTrackingManager.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FileTrackingManager.swift
3 | // MotionTracking
4 | //
5 | // Created by Karim Angama on 02/02/2022.
6 | //
7 |
8 | import Foundation
9 |
10 | /**
11 | Entity tracking file
12 |
13 | */
14 | struct FileTrackingEntity: Equatable {
15 | let information: String
16 | let date: Date
17 | let fileUrl: URL
18 | let isLocation: Bool
19 |
20 | static func ==(lhs: FileTrackingEntity, rhs: FileTrackingEntity) -> Bool {
21 | return lhs.information == rhs.information &&
22 | lhs.date.compare(rhs.date) == .orderedSame &&
23 | lhs.fileUrl.absoluteString == rhs.fileUrl.absoluteString &&
24 | lhs.isLocation == rhs.isLocation
25 | }
26 | }
27 |
28 |
29 |
30 | /**
31 | File Tracking manager
32 |
33 | */
34 | class FileTrackingManager {
35 |
36 | struct Const {
37 | static let nameFile = "tracking-file"
38 | static let separation = "_"
39 | static let extensionCSV = ".csv"
40 | static let extensionTXT = ".txt"
41 | static let location = "location"
42 | }
43 |
44 | // MARK: Public method
45 |
46 | /**
47 | Add file in the cache directory
48 |
49 | @param json - Tracking data
50 | @param location - Specify if there is location in the data
51 | @param time - Specify recording time
52 | @param count - Specify data count
53 | @retrun The file url stored in the cache directory
54 | */
55 | func writeFileTransfer(
56 | json: String,
57 | location: Bool,
58 | time: Double,
59 | count: Int) throws -> URL {
60 | let fileName = nameOfTransferFile(withLocation: location, second: time, count: count)
61 | let path = FileManager.default.urls(
62 | for: .cachesDirectory,
63 | in: .userDomainMask
64 | )[0].appendingPathComponent("\(fileName)\(Const.extensionTXT)")
65 | try json.write(to: path, atomically: true, encoding: .utf8)
66 | return path
67 | }
68 |
69 | /**
70 | Move the file url stored in the cache directory to application support directory
71 |
72 | @param fileUrl - The file url stored in the cache directory
73 | @retrun Tracking files entity
74 | */
75 | func moveCachesToSupportDirectory(fileUrl: URL) throws -> FileTrackingEntity {
76 | let fileName = fileUrl.lastPathComponent
77 | let url = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0].appendingPathComponent(fileName)
78 | try FileManager.default.moveItem(atPath: fileUrl.path, toPath: url.path)
79 | return fileTracking(fileUrl: url)
80 | }
81 |
82 | /**
83 | Return all tracking files entities stored in application support directory
84 |
85 | @retrun Tracking files entities array
86 | */
87 | func filesTracking() -> [FileTrackingEntity] {
88 |
89 | var arrayFile: [String]? = nil
90 | let url = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0]
91 | #if targetEnvironment(simulator)
92 | arrayFile = [
93 | "\(nameOfTransferFile(withLocation: false, second: 5, count: 200))\(Const.extensionTXT)",
94 | "\(nameOfTransferFile(withLocation: true, second: 10, count: 576))\(Const.extensionTXT)",
95 | "\(nameOfTransferFile(withLocation: false, second: 3, count: 187))\(Const.extensionTXT)",
96 | "\(nameOfTransferFile(withLocation: false, second: 10, count: 583))\(Const.extensionTXT)",
97 | "\(nameOfTransferFile(withLocation: false, second: 8, count: 370))\(Const.extensionTXT)",
98 | "\(nameOfTransferFile(withLocation: true, second: 12, count: 600))\(Const.extensionTXT)",
99 | ]
100 | #else
101 | arrayFile = try? FileManager.default.contentsOfDirectory(atPath: url.path)
102 | #endif
103 | return arrayFile?
104 | .filter { $0.contains(Const.extensionTXT) }
105 | .map { fileTracking(fileUrl: url.appendingPathComponent($0)) }
106 | .sorted {
107 | $0.date.compare($1.date) == .orderedDescending
108 | } ?? []
109 |
110 | }
111 |
112 | /**
113 | Remove file
114 |
115 | @param fileUrl - The file url
116 | */
117 | func removeFile(fileUrl: URL) throws {
118 | try FileManager.default.removeItem(at: fileUrl)
119 | }
120 |
121 | // MARK: Private method
122 |
123 | /**
124 | Create and return the name file
125 |
126 | @Param withLocation - Add the location parameter in the file name
127 | */
128 | private func nameOfTransferFile(withLocation: Bool, second: Double, count: Int) -> String {
129 | let date = Date().nowDateIso8601
130 | let withLocation = withLocation ? "\(Const.separation)\(Const.location)" : ""
131 | let information = "\(second.time)\(Const.separation)\(count)"
132 | return "\(Const.nameFile)\(Const.separation)\(date)\(withLocation)\(Const.separation)\(information)"
133 | }
134 |
135 | /**
136 | Create and return a entity with the file name
137 |
138 | @param fileUrl - The file url
139 | @return The entity file tracking
140 | */
141 | private func fileTracking(fileUrl: URL) -> FileTrackingEntity {
142 | let fileName = fileUrl.deletingPathExtension().lastPathComponent
143 | let array = fileName.components(separatedBy: Const.separation)
144 | return FileTrackingEntity(
145 | information: trackingInformation(array),
146 | date: Date.string(date: array[1]),
147 | fileUrl: fileUrl,
148 | isLocation: fileName.contains(Const.location)
149 | )
150 | }
151 |
152 | private func trackingInformation(_ array: [String]) -> String {
153 | array.count >= 4 ? "\(array[array.endIndex - 2]) - \(array.last ?? "-") records" : "No information"
154 | }
155 |
156 | }
157 |
--------------------------------------------------------------------------------
/MotionTracking/Common/Manager/CSVFileManager.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CSVFileManager.swift
3 | // MotionTracking
4 | //
5 | // Created by Karim Angama on 23/02/2022.
6 | //
7 |
8 | import Foundation
9 | import SwiftCSVExport
10 | import SSZipArchive
11 |
12 | /**
13 | Entity CSV file fields
14 |
15 | */
16 | struct CSVFileFields: Codable, Equatable {
17 |
18 | let timestamp: Bool
19 | let gravityX: Bool
20 | let gravityY: Bool
21 | let gravityZ: Bool
22 | let rotationRateX: Bool
23 | let rotationRateY: Bool
24 | let rotationRateZ: Bool
25 | let userAccelerationX: Bool
26 | let userAccelerationY: Bool
27 | let userAccelerationZ: Bool
28 | let attitudeRoll: Bool
29 | let attitudePitch: Bool
30 | let attitudeYaw: Bool
31 | let locationLatitude: Bool
32 | let locationLongitude: Bool
33 | let locationAltitude: Bool
34 |
35 | var orderedFields: [(String, Bool)] {
36 | return [
37 | ("timestamp", timestamp),
38 | ("gravityX", gravityX),
39 | ("gravityY", gravityY),
40 | ("gravityZ", gravityZ),
41 | ("rotationRateX", rotationRateX),
42 | ("rotationRateY", rotationRateY),
43 | ("rotationRateZ", rotationRateZ),
44 | ("userAccelerationX", userAccelerationX),
45 | ("userAccelerationY", userAccelerationY),
46 | ("userAccelerationZ", userAccelerationZ),
47 | ("attitudeRoll", attitudeRoll),
48 | ("attitudePitch", attitudePitch),
49 | ("attitudeYaw", attitudeYaw),
50 | ("locationLatitude", locationLatitude),
51 | ("locationLongitude", locationLongitude),
52 | ("locationAltitude", locationAltitude)
53 | ]
54 | }
55 |
56 | init(timestamp: Bool = true,
57 | gravityX: Bool = true,
58 | gravityY: Bool = true,
59 | gravityZ: Bool = true,
60 | rotationRateX: Bool = true,
61 | rotationRateY: Bool = true,
62 | rotationRateZ: Bool = true,
63 | userAccelerationX: Bool = true,
64 | userAccelerationY: Bool = true,
65 | userAccelerationZ: Bool = true,
66 | attitudeRoll: Bool = true,
67 | attitudePitch: Bool = true,
68 | attitudeYaw: Bool = true,
69 | locationLatitude: Bool = true,
70 | locationLongitude: Bool = true,
71 | locationAltitude: Bool = true
72 | ) {
73 | self.timestamp = timestamp
74 | self.gravityX = gravityX
75 | self.gravityY = gravityY
76 | self.gravityZ = gravityZ
77 | self.rotationRateX = rotationRateX
78 | self.rotationRateY = rotationRateY
79 | self.rotationRateZ = rotationRateZ
80 | self.userAccelerationX = userAccelerationX
81 | self.userAccelerationY = userAccelerationY
82 | self.userAccelerationZ = userAccelerationZ
83 | self.attitudeRoll = attitudeRoll
84 | self.attitudePitch = attitudePitch
85 | self.attitudeYaw = attitudeYaw
86 | self.locationLatitude = locationLatitude
87 | self.locationLongitude = locationLongitude
88 | self.locationAltitude = locationAltitude
89 | }
90 | }
91 |
92 | /**
93 | CSV file manager
94 |
95 | */
96 | class CSVFileManager {
97 |
98 | var fields = CSVFileFields()
99 |
100 | // MARK: Public method
101 |
102 | /**
103 | Set the fields to add in the csv file
104 |
105 | @param fields - entity
106 | */
107 | func setFiels(fields: CSVFileFields) {
108 | self.fields = fields
109 | }
110 |
111 | /**
112 | Create and save the several file csv
113 |
114 | @param block - The file url stored in the application support directory
115 | */
116 | func save(fileUrl: URL, block: @escaping (_ zipPath: String?, _ error: Error?) -> Void) {
117 | let backgroundQ = DispatchQueue.global(qos: .default)
118 | let group = DispatchGroup()
119 |
120 | var errorSave: Error?
121 | var filePath: String?
122 | backgroundQ.async(group: group, execute: { [weak self] in
123 | do {
124 | filePath = try self?.save(fileUrl: fileUrl)
125 | } catch {
126 | errorSave = error
127 | }
128 | })
129 |
130 | group.notify(queue: DispatchQueue.main, execute: {
131 | block(filePath, errorSave)
132 | })
133 | }
134 |
135 | /**
136 | Create and save the several files csv
137 |
138 | @param block - The file url stored in the application support directory
139 | */
140 | func save(filesUrl: [URL], block: @escaping (_ zipPath: String, _ error: Error?) -> Void) {
141 | var errorSave: Error?
142 | let backgroundQ = DispatchQueue.global(qos: .default)
143 | let group = DispatchGroup()
144 |
145 | let zipPath = tempZipPath()
146 | var filePath: [String] = []
147 | backgroundQ.async(group: group, execute: { [weak self] in
148 | for file in filesUrl {
149 | do {
150 | guard let path = try self?.save(fileUrl: file) else { return }
151 | filePath.append(path)
152 | } catch {
153 | errorSave = error
154 | }
155 | }
156 | })
157 | group.notify(queue: DispatchQueue.main, execute: {
158 | SSZipArchive.createZipFile(atPath: zipPath, withFilesAtPaths: filePath)
159 | block(zipPath, errorSave)
160 | })
161 |
162 | }
163 |
164 | // MARK: Private method
165 |
166 | private func tempZipPath() -> String {
167 | var path = NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true)[0]
168 | path += "/\(nameOfZipFile()).zip"
169 | return path
170 | }
171 |
172 | /**
173 | Create and save the file csv
174 |
175 | @param fileUrl - The file url stored in the application support directory
176 | */
177 | private func save(fileUrl: URL) throws -> String {
178 | let json = try String(contentsOf: fileUrl)
179 | let fileName = fileUrl.deletingPathExtension().lastPathComponent
180 | let fields = getFields()
181 | return CSVExport.exportWithString(
182 | fileName,
183 | fields: fields,
184 | values: json
185 | ).filePath
186 |
187 | }
188 |
189 | /**
190 | Create and return the name file
191 | */
192 | private func nameOfZipFile() -> String {
193 | let date = Date().nowDateIso8601
194 | return "motionTracking_\(date)"
195 | }
196 |
197 | /**
198 | Return the array fields to add in the csv file
199 |
200 | @return Array fields
201 | */
202 | private func getFields() -> [String] {
203 | fields.orderedFields.compactMap { $1 ? $0 : nil }
204 | }
205 |
206 | }
207 |
--------------------------------------------------------------------------------
/MotionTracking/ExportFile/ExportFileViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ExportFileViewController.swift
3 | // MotionTracking
4 | //
5 | // Created by Karim Angama on 05/02/2022.
6 | //
7 |
8 | import Foundation
9 | import Combine
10 | import UIKit
11 |
12 | class ExportFileViewController: BaseTableViewController {
13 |
14 | @IBOutlet weak var titleTableViewCell: UITableViewCell!
15 | @IBOutlet weak var exportButton: UIBarButtonItem!
16 | @IBOutlet weak var timestampSwitch: UISwitch!
17 | @IBOutlet weak var gravitySwitch: UISwitch!
18 | @IBOutlet weak var rotationSwitch: UISwitch!
19 | @IBOutlet weak var accelerationSwitch: UISwitch!
20 | @IBOutlet weak var cordinateSwitch: UISwitch!
21 | @IBOutlet weak var altitudeSwitch: UISwitch!
22 | @IBOutlet weak var attitudeSwitch: UISwitch!
23 |
24 | private lazy var refreshBarButton: UIBarButtonItem = {
25 | let activityIndicator: UIActivityIndicatorView = UIActivityIndicatorView(style: .medium)
26 | activityIndicator.startAnimating()
27 | return UIBarButtonItem(customView: activityIndicator)
28 | }()
29 |
30 | // MARK: Override method
31 |
32 | override func setupObservers() {
33 | super.setupObservers()
34 |
35 | // Display file name
36 | viewModel.$infomation
37 | .receive(on: DispatchQueue.main)
38 | .sink { [weak self] info in
39 | var test = self?.titleTableViewCell.defaultContentConfiguration()
40 | test?.text = info
41 | self?.titleTableViewCell.contentConfiguration = test
42 | }
43 | .store(in: &cancellable)
44 |
45 | // Enable button if a switch is at least true
46 | viewModel.$enabledExportButton
47 | .receive(on: DispatchQueue.main)
48 | .assign(to: \.isEnabled, on: exportButton)
49 | .store(in: &cancellable)
50 |
51 | // Sharing csv file
52 | viewModel.sharingCsvPathFile
53 | .receive(on: DispatchQueue.main)
54 | .sink { [weak self] path in
55 | self?.sharingCSV(filePath: path)
56 | }
57 | .store(in: &cancellable)
58 |
59 | // The switch is on if the timestamp value is true
60 | viewModel.$timestampValue
61 | .receive(on: DispatchQueue.main)
62 | .assign(to: \.isOn, on: timestampSwitch)
63 | .store(in: &cancellable)
64 |
65 | // The switch is on if the gravity value is true
66 | viewModel.$gravityValue
67 | .receive(on: DispatchQueue.main)
68 | .assign(to: \.isOn, on: gravitySwitch)
69 | .store(in: &cancellable)
70 |
71 | // The switch is on if the rotation value is true
72 | viewModel.$rotationValue
73 | .receive(on: DispatchQueue.main)
74 | .assign(to: \.isOn, on: rotationSwitch)
75 | .store(in: &cancellable)
76 |
77 | // The switch is on if the acceleration value is true
78 | viewModel.$accelerationValue
79 | .receive(on: DispatchQueue.main)
80 | .assign(to: \.isOn, on: accelerationSwitch)
81 | .store(in: &cancellable)
82 |
83 | // The switch is on if the altitude value is true
84 | viewModel.$altitudeValue
85 | .receive(on: DispatchQueue.main)
86 | .assign(to: \.isOn, on: altitudeSwitch)
87 | .store(in: &cancellable)
88 |
89 | // The switch is on if the attitude value is true
90 | viewModel.$attitudeValue
91 | .receive(on: DispatchQueue.main)
92 | .assign(to: \.isOn, on: attitudeSwitch)
93 | .store(in: &cancellable)
94 |
95 | // The switch is on if the cordinate value is true
96 | viewModel.$cordinateValue
97 | .receive(on: DispatchQueue.main)
98 | .assign(to: \.isOn, on: cordinateSwitch)
99 | .store(in: &cancellable)
100 |
101 | // Disable altitude switch if there is no location
102 | viewModel.$enabledLocationValue
103 | .receive(on: DispatchQueue.main)
104 | .assign(to: \.isEnabled, on: altitudeSwitch)
105 | .store(in: &cancellable)
106 |
107 | // Disable location switch if there is no location
108 | viewModel.$enabledLocationValue
109 | .receive(on: DispatchQueue.main)
110 | .assign(to: \.isEnabled, on: cordinateSwitch)
111 | .store(in: &cancellable)
112 |
113 | viewModel.$isLoading
114 | .receive(on: DispatchQueue.main)
115 | .sink { [weak self] value in
116 | self?.loadAnimation(value)
117 | }
118 | .store(in: &cancellable)
119 |
120 | }
121 |
122 | // MARK: Action method
123 |
124 | @IBAction func dismissScreen(_ sender: Any) {
125 | dismiss(animated: true, completion: nil)
126 | }
127 |
128 | @IBAction func exportFile(_ sender: Any) {
129 | self.viewModel.exportCSV()
130 | }
131 |
132 | @IBAction func timestampChange(_ sender: UISwitch) {
133 | viewModel.timestampValue = sender.isOn
134 | }
135 |
136 | @IBAction func gravityChange(_ sender: UISwitch) {
137 | viewModel.gravityValue = sender.isOn
138 | }
139 |
140 | @IBAction func rotationChange(_ sender: UISwitch) {
141 | viewModel.rotationValue = sender.isOn
142 | }
143 |
144 | @IBAction func accelerationChange(_ sender: UISwitch) {
145 | viewModel.accelerationValue = sender.isOn
146 | }
147 |
148 | @IBAction func cordinateChange(_ sender: UISwitch) {
149 | viewModel.cordinateValue = sender.isOn
150 | }
151 |
152 | @IBAction func altitudeChange(_ sender: UISwitch) {
153 | viewModel.altitudeValue = sender.isOn
154 | }
155 |
156 | @IBAction func attitudeChange(_ sender: UISwitch) {
157 | viewModel.attitudeValue = sender.isOn
158 | }
159 |
160 | // MARK: Private method
161 |
162 | private func loadAnimation(_ value: Bool) {
163 | if value {
164 | navigationItem.rightBarButtonItem = refreshBarButton
165 | }else{
166 | navigationItem.rightBarButtonItem = exportButton
167 | }
168 | view.isUserInteractionEnabled = !value
169 | }
170 |
171 | private func sharingCSV(filePath: String) {
172 |
173 | let firstActivityItem = NSURL(fileURLWithPath: filePath)
174 | let activityViewController : UIActivityViewController = UIActivityViewController(
175 | activityItems: [firstActivityItem], applicationActivities: nil)
176 |
177 | activityViewController.excludedActivityTypes = [
178 | UIActivity.ActivityType.postToTwitter,
179 | UIActivity.ActivityType.postToWeibo,
180 | UIActivity.ActivityType.message,
181 | UIActivity.ActivityType.print,
182 | UIActivity.ActivityType.saveToCameraRoll,
183 | UIActivity.ActivityType.postToFlickr,
184 | UIActivity.ActivityType.postToVimeo,
185 | UIActivity.ActivityType.postToTencentWeibo,
186 | UIActivity.ActivityType.openInIBooks,
187 | UIActivity.ActivityType.markupAsPDF
188 | ]
189 |
190 | DispatchQueue.main.async {
191 | self.present(activityViewController, animated: true, completion: nil)
192 | }
193 | }
194 |
195 | }
196 |
--------------------------------------------------------------------------------
/MotionTracking WatchKit Extension/Common/Manager/MotionManager.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MotionManager.swift
3 | // MotionTracking WatchKit Extension
4 | //
5 | // Created by Karim Angama on 19/01/2022.
6 | //
7 |
8 | import Foundation
9 | import CoreMotion
10 | import WatchKit
11 | import os.log
12 |
13 | /**
14 | `MotionManagerDelegate` exists to inform delegates of motion changes.
15 | These contexts can be used to enable application specific behavior.
16 | */
17 | protocol MotionManagerDelegate: AnyObject {
18 | func didUpdateMotion(_ manager: MotionManager, result: ResultMotionEnity)
19 | }
20 |
21 | /**
22 | `LocationManagerDelegate` exists to inform delegates of use the authorization of location.
23 | */
24 | protocol LocationManagerDelegate: AnyObject {
25 |
26 | /// Return yes, if user authorized
27 | func userAuthorization(response: Bool)
28 | }
29 |
30 | /**
31 | `ResultMotionEnity` motion and location data
32 | */
33 | struct ResultMotionEnity: Encodable {
34 | let timestamp: Double
35 | let gravityX: Double
36 | let gravityY: Double
37 | let gravityZ: Double
38 | let userAccelerationX: Double
39 | let userAccelerationY: Double
40 | let userAccelerationZ: Double
41 | let rotationRateX: Double
42 | let rotationRateY: Double
43 | let rotationRateZ: Double
44 | let attitudeRoll: Double
45 | let attitudePitch: Double
46 | let attitudeYaw: Double
47 | var locationLatitude: Double?
48 | var locationLongitude: Double?
49 | var locationAltitude: Double?
50 |
51 | init(timestamp: Double, gravityX: Double, gravityY: Double, gravityZ: Double,
52 | userAccelerationX: Double, userAccelerationY: Double, userAccelerationZ: Double,
53 | rotationRateX: Double, rotationRateY: Double, rotationRateZ: Double,
54 | attitudeRoll: Double, attitudePitch: Double, attitudeYaw: Double,
55 | latitude: Double? = nil, longitude: Double? = nil, altitude: Double? = nil) {
56 | self.timestamp = timestamp
57 | self.gravityX = gravityX
58 | self.gravityY = gravityY
59 | self.gravityZ = gravityZ
60 | self.userAccelerationX = userAccelerationX
61 | self.userAccelerationY = userAccelerationY
62 | self.userAccelerationZ = userAccelerationZ
63 | self.rotationRateX = rotationRateX
64 | self.rotationRateY = rotationRateY
65 | self.rotationRateZ = rotationRateZ
66 | self.attitudeRoll = attitudeRoll
67 | self.attitudePitch = attitudePitch
68 | self.attitudeYaw = attitudeYaw
69 | self.locationLatitude = latitude
70 | self.locationLongitude = longitude
71 | self.locationAltitude = altitude
72 | }
73 | }
74 |
75 | /**
76 | `MotionManager` manage the motion and location
77 | */
78 | class MotionManager: NSObject {
79 | // MARK: Properties
80 |
81 |
82 | private let locationManager = CLLocationManager()
83 | private let motionManager = CMMotionManager()
84 | private let queue = OperationQueue()
85 | private let wristLocationIsLeft = WKInterfaceDevice.current().wristLocation == .left
86 |
87 | // MARK: Application Specific Constants
88 |
89 | /// The app is using 50hz data.
90 | private var sampleInterval: Double
91 |
92 | /// Add data location
93 | private var addDataLocation: Bool
94 |
95 | // MARK: Public properties
96 |
97 | weak var delegate: MotionManagerDelegate?
98 | weak var delegateLocation: LocationManagerDelegate?
99 |
100 | // Array motion entities
101 | private(set) var resultMotion:[ResultMotionEnity] = []
102 |
103 |
104 | // MARK: Initialization
105 |
106 | required init(sampleInterval: Double, addDataLocation: Bool) {
107 | self.sampleInterval = 1.0 / sampleInterval
108 | self.addDataLocation = addDataLocation
109 |
110 | // Serial queue for sample handling and calculations.
111 | queue.maxConcurrentOperationCount = 1
112 | queue.name = "MotionManagerQueue"
113 |
114 | // Setup location
115 | locationManager.allowsBackgroundLocationUpdates = true
116 | locationManager.desiredAccuracy = kCLLocationAccuracyBest
117 | locationManager.activityType = .other
118 | }
119 |
120 | // MARK: Motion Location
121 |
122 | /**
123 | Request Always Authorization
124 | */
125 | func authorization() {
126 | locationManager.delegate = self
127 | DispatchQueue.main.async { [weak self] in
128 | self?.locationManager.requestAlwaysAuthorization()
129 | }
130 | }
131 |
132 | /**
133 | Start the location for start tracking in background
134 | */
135 | func startLocation() {
136 | locationManager.startUpdatingLocation()
137 | }
138 |
139 | private func stopLocation() {
140 | locationManager.stopUpdatingLocation()
141 | locationManager.stopUpdatingHeading()
142 | }
143 |
144 | // MARK: Motion Manager
145 |
146 | /**
147 | Start device motion updates
148 | */
149 | func startUpdates() {
150 | if !motionManager.isDeviceMotionAvailable {
151 | print("Device Motion is not available.")
152 | // TODO error managment
153 | return
154 | }
155 |
156 | resultMotion.removeAll()
157 |
158 | motionManager.deviceMotionUpdateInterval = sampleInterval
159 | motionManager.startDeviceMotionUpdates(to: queue) { (deviceMotion: CMDeviceMotion?, error: Error?) in
160 | if error != nil {
161 | print("Encountered error: \(error!)")
162 | // TODO error managment
163 | }
164 |
165 | if deviceMotion != nil {
166 | self.processDeviceMotion(deviceMotion!)
167 | }
168 | }
169 | }
170 |
171 | /**
172 | Stop device motion updates
173 | */
174 | func stopUpdates() {
175 | if motionManager.isDeviceMotionAvailable {
176 | motionManager.stopDeviceMotionUpdates()
177 | }
178 | stopLocation()
179 | }
180 |
181 | // MARK: Motion Processing
182 |
183 | private func processDeviceMotion(_ deviceMotion: CMDeviceMotion) {
184 |
185 | let timestamp = Date().timeIntervalSince1970
186 |
187 | var motionEnity = ResultMotionEnity(
188 | timestamp : timestamp,
189 | gravityX: deviceMotion.gravity.x,
190 | gravityY: deviceMotion.gravity.y,
191 | gravityZ: deviceMotion.gravity.z,
192 | userAccelerationX: deviceMotion.userAcceleration.x,
193 | userAccelerationY: deviceMotion.userAcceleration.y,
194 | userAccelerationZ: deviceMotion.userAcceleration.z,
195 | rotationRateX: deviceMotion.rotationRate.x,
196 | rotationRateY: deviceMotion.rotationRate.y,
197 | rotationRateZ: deviceMotion.rotationRate.z,
198 | attitudeRoll: deviceMotion.attitude.roll,
199 | attitudePitch: deviceMotion.attitude.pitch,
200 | attitudeYaw: deviceMotion.attitude.yaw
201 | )
202 |
203 | // If yes, add location to data
204 | if addDataLocation {
205 | motionEnity.locationLatitude = locationManager.location?.coordinate.latitude
206 | motionEnity.locationLongitude = locationManager.location?.coordinate.longitude
207 | motionEnity.locationAltitude = locationManager.location?.altitude
208 | }
209 |
210 | resultMotion.append(motionEnity)
211 |
212 | os_log("Motion: %@, %@, %@, %@, %@, %@, %@, %@, %@, %@, %@, %@, %@",
213 | String(timestamp),
214 | String(deviceMotion.gravity.x),
215 | String(deviceMotion.gravity.y),
216 | String(deviceMotion.gravity.z),
217 | String(deviceMotion.userAcceleration.x),
218 | String(deviceMotion.userAcceleration.y),
219 | String(deviceMotion.userAcceleration.z),
220 | String(deviceMotion.rotationRate.x),
221 | String(deviceMotion.rotationRate.y),
222 | String(deviceMotion.rotationRate.z),
223 | String(deviceMotion.attitude.roll),
224 | String(deviceMotion.attitude.pitch),
225 | String(deviceMotion.attitude.yaw),
226 | String(locationManager.location?.coordinate.latitude ?? 0.0),
227 | String(locationManager.location?.coordinate.longitude ?? 0.0),
228 | String(locationManager.location?.altitude ?? 0.0))
229 |
230 | updateMetricsDelegate(motion: motionEnity);
231 | }
232 |
233 | // MARK: Data and Delegate Management
234 |
235 | private func updateMetricsDelegate(motion: ResultMotionEnity) {
236 | delegate?.didUpdateMotion(self, result:motion)
237 | }
238 | }
239 |
240 | extension MotionManager: CLLocationManagerDelegate {
241 |
242 | func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
243 | guard status != .notDetermined else { return }
244 | self.delegateLocation?.userAuthorization(response: status == .authorizedAlways || status == .authorizedWhenInUse)
245 | }
246 |
247 | }
248 |
--------------------------------------------------------------------------------
/MotionTracking/FilesList/FilesListViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ViewController.swift
3 | // MotionTracking
4 | //
5 | // Created by Karim Angama on 19/01/2022.
6 | //
7 |
8 | import UIKit
9 |
10 | class FilesListViewController: BaseViewController {
11 |
12 | @IBOutlet weak var noFilesLabel: UILabel!
13 | @IBOutlet weak var tableView: UITableView!
14 |
15 |
16 | // MARK: Override methode
17 |
18 | override func setupUI() {
19 | super.setupUI()
20 |
21 | tableView.allowsMultipleSelectionDuringEditing = true
22 | navigationItem.rightBarButtonItem = editButtonItem
23 |
24 | let exportButton = UIBarButtonItem(barButtonSystemItem: .action, target: self, action: #selector(exportTapped))
25 | let trashButton = UIBarButtonItem(barButtonSystemItem: .trash, target: self, action: #selector(trashTapped))
26 | let spacer = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: self, action: nil)
27 | toolbarItems = [trashButton, spacer, exportButton]
28 | }
29 |
30 | override func setupObservers() {
31 | super.setupObservers()
32 |
33 | // Hide the message if there is a list of files
34 | viewModel.$isFilesExist
35 | .receive(on: DispatchQueue.main)
36 | .assign(to: \.isHidden, on: noFilesLabel)
37 | .store(in: &cancellable)
38 |
39 | // Disable Edit button if there are no files
40 | viewModel.$isFilesExist
41 | .receive(on: DispatchQueue.main)
42 | .assign(to: \.isEnabled, on: editButtonItem)
43 | .store(in: &cancellable)
44 |
45 | viewModel.$isEditingViewController
46 | .dropFirst()
47 | .receive(on: DispatchQueue.main)
48 | .assign(to: \.isEditing, on: self)
49 | .store(in: &cancellable)
50 |
51 | // Reload data when files added
52 | viewModel.filesDidChange
53 | .receive(on: DispatchQueue.main)
54 | .sink { [weak self] _ in
55 | self?.reloadData()
56 | }
57 | .store(in: &cancellable)
58 |
59 | // Insert row in tableview
60 | viewModel.inserFile
61 | .receive(on: DispatchQueue.main)
62 | .sink(receiveValue: { [weak self] _ in
63 | self?.inserRows()
64 | })
65 | .store(in: &cancellable)
66 |
67 | // Remove row from tableview
68 | viewModel.removeFiles
69 | .receive(on: DispatchQueue.main)
70 | .sink { [weak self] index in
71 | self?.deleteRows(index)
72 | }
73 | .store(in: &cancellable)
74 |
75 | // Show the toolbar
76 | viewModel.$showToolBar
77 | .removeDuplicates()
78 | .receive(on: DispatchQueue.main)
79 | .sink { [weak self] value in
80 | self?.navigationController?.setToolbarHidden(!value, animated: true)
81 | }
82 | .store(in: &cancellable)
83 |
84 | // Enable the edit button
85 | viewModel.$isEnabledButton
86 | .removeDuplicates()
87 | .sink { [weak self] value in
88 | self?.toolbarItems?.first?.isEnabled = value
89 | self?.toolbarItems?.last?.isEnabled = value
90 | }
91 | .store(in: &cancellable)
92 |
93 | // Show table view editing
94 | viewModel.$isEditingTableView
95 | .removeDuplicates()
96 | .receive(on: DispatchQueue.main)
97 | .sink { [weak self] value in
98 | if self?.tableView.isEditing != value {
99 | self?.tableView.setEditing(value, animated: true)
100 | }
101 | }
102 | .store(in: &cancellable)
103 |
104 | // Show select all button
105 | viewModel.$isEditingTableView
106 | .removeDuplicates()
107 | .receive(on: DispatchQueue.main)
108 | .sink { [weak self] value in
109 | if !value {
110 | self?.navigationItem.leftBarButtonItem = nil
111 | } else {
112 | self?.navigationItem.leftBarButtonItem = UIBarButtonItem(
113 | title: "Select all",
114 | style: .plain,
115 | target: self,
116 | action: #selector(self?.selectAllTapped)
117 | )
118 | }
119 | }
120 | .store(in: &cancellable)
121 |
122 | // Select all row
123 | viewModel.$isAllSelected
124 | .removeDuplicates()
125 | .receive(on: DispatchQueue.main)
126 | .sink { [weak self] value in
127 | self?.selectAllRow(value)
128 | }
129 | .store(in: &cancellable)
130 |
131 | viewModel.$stateSelectedButton
132 | .removeDuplicates()
133 | .receive(on: DispatchQueue.main)
134 | .sink { [weak self] value in
135 | if value {
136 | self?.navigationItem.leftBarButtonItem?.title = "Deselect all"
137 | }else{
138 | self?.navigationItem.leftBarButtonItem?.title = "Select all"
139 | }
140 | }
141 | .store(in: &cancellable)
142 |
143 | }
144 |
145 | /**
146 | Pass data to Export File ViewModel
147 | */
148 | override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
149 | if let navigationController = segue.destination as? UINavigationController,
150 | let viewController = navigationController.topViewController as? ExportFileViewController {
151 | viewModel.$filesSelected.assign(to: &viewController.viewModel.$fileTrackingEntity)
152 | }
153 |
154 | }
155 |
156 | override func setEditing(_ editing: Bool, animated: Bool) {
157 | super.setEditing(editing, animated: animated)
158 | viewModel.isEditingTableView = !viewModel.isEditingTableView
159 | }
160 |
161 |
162 | // MARK: Private methode
163 |
164 | private func inserRows() {
165 | tableView.insertRows(at: [IndexPath(row: 0, section: 0)], with: .automatic)
166 | }
167 |
168 | private func deleteRows(_ index: [Int]) {
169 | tableView.deleteRows(at: index.flatMap {
170 | [IndexPath(row: $0, section: 0)]
171 | }, with: .automatic)
172 | }
173 |
174 | private func reloadData() {
175 | tableView.reloadData()
176 | }
177 |
178 | @objc func trashTapped() {
179 | confirmDeleteMessage()
180 | }
181 |
182 | @objc func exportTapped() {
183 | performSegue(withIdentifier: "ExportFileSegue", sender: nil)
184 | }
185 |
186 | @objc func selectAllTapped(sender: UIBarButtonItem) {
187 | viewModel.isAllSelected = !viewModel.isAllSelected
188 | }
189 |
190 | private func selectAllRow(_ value: Bool) {
191 | for section in 0.. Int {
233 | viewModel.files.count
234 | }
235 |
236 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
237 | let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! FilesListTableViewCell
238 | cell.configuration(entity: viewModel.files[indexPath.row])
239 | return cell
240 | }
241 |
242 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
243 | filesSelected()
244 | if !tableView.isEditing {
245 | performSegue(withIdentifier: "ExportFileSegue", sender: nil)
246 | tableView.deselectRow(at: indexPath, animated: true)
247 | }
248 | }
249 |
250 | func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
251 | filesSelected()
252 | }
253 |
254 | func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
255 | if editingStyle == .delete {
256 | viewModel.removeFile(index: indexPath.row)
257 | }
258 | }
259 |
260 | }
261 |
262 |
263 |
--------------------------------------------------------------------------------
/MotionTracking WatchKit App/Base.lproj/Interface.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
30 |
33 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
46 |
47 |
48 |
49 |
50 |
51 |
54 |
55 |
56 |
57 |
58 |
59 |
62 |
63 |
64 |
65 |
66 |
67 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
92 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
130 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
145 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
192 |
193 |
194 |
195 |
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 |
211 |
215 |
216 |
217 |
218 |
219 |
222 |
225 |
226 |
227 |
228 |
229 |
232 |
235 |
236 |
237 |
238 |
239 |
242 |
245 |
246 |
247 |
248 |
249 |
250 |
251 |
252 |
253 |
254 |
255 |
256 |
260 |
261 |
262 |
263 |
264 |
267 |
270 |
271 |
272 |
273 |
274 |
277 |
280 |
281 |
282 |
283 |
284 |
287 |
290 |
291 |
292 |
293 |
294 |
295 |
296 |
297 |
298 |
299 |
300 |
301 |
305 |
306 |
307 |
308 |
309 |
312 |
315 |
316 |
317 |
318 |
319 |
322 |
325 |
326 |
327 |
328 |
329 |
332 |
335 |
336 |
337 |
338 |
339 |
340 |
341 |
342 |
343 |
344 |
345 |
346 |
347 |
348 |
349 |
350 |
351 |
352 |
353 |
354 |
355 |
356 |
357 |
358 |
359 |
360 |
361 |
362 |
363 |
364 |
365 |
366 |
367 |
368 |
369 |
370 |
371 |
372 |
373 |
374 |
375 |
376 |
377 |
378 |
379 |
380 |
381 |
382 |
383 |
384 |
385 |
386 |
387 |
388 |
389 |
390 |
391 |
392 |
393 |
394 |
395 |
396 |
397 |
398 |
--------------------------------------------------------------------------------