├── 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 | 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 | --------------------------------------------------------------------------------