├── .gitignore ├── LICENSE ├── README.md ├── Reminders.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm │ │ └── Package.resolved └── xcshareddata │ └── xcschemes │ └── Reminders.xcscheme ├── Reminders ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── appstore1024.png │ │ ├── ipad152.png │ │ ├── ipad76.png │ │ ├── ipadNotification20.png │ │ ├── ipadNotification40.png │ │ ├── ipadPro167.png │ │ ├── ipadSettings29.png │ │ ├── ipadSettings58.png │ │ ├── ipadSpotlight40.png │ │ ├── ipadSpotlight80.png │ │ ├── iphone120.png │ │ ├── iphone180.png │ │ ├── mac1024.png │ │ ├── mac128.png │ │ ├── mac16.png │ │ ├── mac256.png │ │ ├── mac32.png │ │ ├── mac512.png │ │ ├── mac64.png │ │ ├── notification40.png │ │ ├── notification60.png │ │ ├── settings58.png │ │ ├── settings87.png │ │ ├── spotlight120.png │ │ └── spotlight80.png │ └── Contents.json ├── Info.plist ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json └── RemindersApp.swift ├── RemindersPackage ├── .gitignore ├── Package.swift ├── README.md ├── Sources │ ├── App │ │ ├── AppDelegate.swift │ │ └── AppView.swift │ ├── AppCore │ │ └── AppCore.swift │ ├── AppDelegateCore │ │ ├── AppDelegateCore.swift │ │ └── Environment │ │ │ └── AppDelegateEnvironment+Mocks.swift │ ├── NotificationCenterClient │ │ ├── NotificationCenterClient+Mocks.swift │ │ └── NotificationCenterClient.swift │ ├── NotificationCenterClientLive │ │ └── NotificationCenterClient+Live.swift │ ├── ReminderDetail │ │ ├── IconToggleRow.swift │ │ └── ReminderDetail.swift │ ├── ReminderDetailCore │ │ ├── Environment │ │ │ └── ReminderDetailEnvironment+Mocks.swift │ │ └── ReminderDetailCore.swift │ ├── RemindersList │ │ └── RemindersList.swift │ ├── RemindersListCore │ │ └── RemindersListCore.swift │ ├── RemindersListRow │ │ └── RemindersListRow.swift │ ├── RemindersListRowCore │ │ ├── Environment │ │ │ └── ReminderListRowEnvironment+Mocks.swift │ │ └── RemindersListRowCore.swift │ ├── SharedModels │ │ ├── Reminder+UNNotificationRequest.swift │ │ └── Reminder.swift │ ├── UIApplicationClient │ │ ├── UIApplicationClient+Mocks.swift │ │ └── UIApplicationClient.swift │ ├── UIApplicationClientLive │ │ └── UIApplicationClient+Live.swift │ ├── UserNotificationClient │ │ ├── UserNotificationClient+Mocks.swift │ │ └── UserNotificationClient.swift │ ├── UserNotificationClientLive │ │ └── UserNotificationClient+Live.swift │ ├── WatchOSApp │ │ └── WatchOSAppView.swift │ └── WatchRemindersListRow │ │ └── RemindersListRow.swift └── Tests │ ├── ReminderDetailCoreTests │ └── ReminderDetailCoreTests.swift │ ├── RemindersListCoreTests │ └── RemindersListCoreTests.swift │ └── RemindersListRowCoreTests │ └── RemindersListRowCoreTests.swift ├── RemindersWatch Extension ├── Assets.xcassets │ ├── Complication.complicationset │ │ ├── Circular.imageset │ │ │ └── Contents.json │ │ ├── Contents.json │ │ ├── Extra Large.imageset │ │ │ └── Contents.json │ │ ├── Graphic Bezel.imageset │ │ │ └── Contents.json │ │ ├── Graphic Circular.imageset │ │ │ └── Contents.json │ │ ├── Graphic Corner.imageset │ │ │ └── Contents.json │ │ ├── Graphic Extra Large.imageset │ │ │ └── Contents.json │ │ ├── Graphic Large Rectangular.imageset │ │ │ └── Contents.json │ │ ├── Modular.imageset │ │ │ └── Contents.json │ │ └── Utilitarian.imageset │ │ │ └── Contents.json │ └── Contents.json ├── ComplicationController.swift ├── Info.plist ├── NotificationController.swift ├── NotificationView.swift ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json ├── PushNotificationPayload.apns └── RemindersApp.swift ├── RemindersWatch ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ ├── 100.png │ │ ├── 1024.png │ │ ├── 172.png │ │ ├── 196.png │ │ ├── 216.png │ │ ├── 48.png │ │ ├── 55.png │ │ ├── 58.png │ │ ├── 80.png │ │ ├── 87.png │ │ ├── 88.png │ │ └── Contents.json │ └── Contents.json └── Info.plist └── Screenshots ├── reminder_detail.png ├── reminder_notification.png ├── reminders_list.png └── reminders_list_watch.png /.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 | # 59 | # Add this line if you want to avoid checking in source code from the Xcode workspace 60 | # *.xcworkspace 61 | 62 | # Carthage 63 | # 64 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 65 | # Carthage/Checkouts 66 | 67 | Carthage/Build/ 68 | 69 | # Accio dependency management 70 | Dependencies/ 71 | .accio/ 72 | 73 | # fastlane 74 | # 75 | # It is recommended to not store the screenshots in the git repo. 76 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 77 | # For more information about the recommended setup visit: 78 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 79 | 80 | fastlane/report.xml 81 | fastlane/Preview.html 82 | fastlane/screenshots/**/*.png 83 | fastlane/test_output 84 | 85 | # Code Injection 86 | # 87 | # After new code Injection tools there's a generated folder /iOSInjectionProject 88 | # https://github.com/johnno1962/injectionforxcode 89 | 90 | iOSInjectionProject/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Luka Hristic 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Reminders 2 | 3 | A simple app for trying [The Composable Architecture (TCA)](https://github.com/pointfreeco/swift-composable-architecture) written in SwiftUI to create, edit and complete reminders, getting scheduled local notifications: 4 | 5 | Reminders list | Reminder detail | Local notification | WatchOS 6 | :-------------------------:|:-------------------------:|:-------------------------:|:-------------------------: 7 | Reminders list|Reminder detail|Local notification|WatchOS 8 | 9 | 10 | The app presents the following key features (which most of them are the same as the [Tic-Tac-Toe TCA example](https://github.com/pointfreeco/swift-composable-architecture/tree/main/Examples/TicTacToe)): 11 | - Usage of the `UserNotifications` framework for generating notifications for scheduled reminders. 12 | - Comprehensive test suite for every feature, including integration tests of features working in unison. 13 | - Fully controlled side effects. Every feature is provided with all the dependencies it needs to do its work, which makes testing very easy. 14 | - Highly modularized: every feature is isolated into its own module with minimal dependencies between them, allowing to compile and run features in isolation without building the entire application. 15 | - The core logic of the application is put into modules named like `*Core`, and they are kept separate from modules containing UI, which is what allows us to share code across iOS and watchOS but also it could work for macOS and tvOS apps. 16 | 17 | ## Requirements 18 | - iOS 14.5+ (Swift 5) 19 | - Xcode 12.5.1 20 | 21 | ## Usage 22 | 1. Clone the repo 23 | 2. Open the workspace file `Reminders.xcodeproj` 24 | 3. Select an app target: 25 | - `Reminders` for iOS app 26 | - `RemindersWatch` for WatchOS app 27 | 4. Run the app 28 | 29 | ## Author 30 | Luka Hristic 31 | 32 | luka.hristic.dev@gmail.com 33 | 34 | ## License 35 | MIT License 36 | 37 | Copyright (c) 2021 Luka Hristic 38 | 39 | Permission is hereby granted, free of charge, to any person obtaining a copy 40 | of this software and associated documentation files (the "Software"), to deal 41 | in the Software without restriction, including without limitation the rights 42 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 43 | copies of the Software, and to permit persons to whom the Software is 44 | furnished to do so, subject to the following conditions: 45 | 46 | The above copyright notice and this permission notice shall be included in all 47 | copies or substantial portions of the Software. 48 | 49 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 50 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 51 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 52 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 53 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 54 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 55 | SOFTWARE. 56 | -------------------------------------------------------------------------------- /Reminders.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 52; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 0B1093A22684D8E800D9A4DD /* RemindersApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B1093A12684D8E800D9A4DD /* RemindersApp.swift */; }; 11 | 0B1093A62684D8EA00D9A4DD /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0B1093A52684D8EA00D9A4DD /* Assets.xcassets */; }; 12 | 0B1093A92684D8EA00D9A4DD /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0B1093A82684D8EA00D9A4DD /* Preview Assets.xcassets */; }; 13 | 0B60A5AC26DBE6A500531DF0 /* App in Frameworks */ = {isa = PBXBuildFile; productRef = 0B60A5AB26DBE6A500531DF0 /* App */; }; 14 | 0BBDA4A7271E00910055F30B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0BBDA4A6271E00910055F30B /* Assets.xcassets */; }; 15 | 0BBDA4AE271E00910055F30B /* RemindersWatch Extension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 0BBDA4AD271E00910055F30B /* RemindersWatch Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 16 | 0BBDA4B3271E00910055F30B /* RemindersApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BBDA4B2271E00910055F30B /* RemindersApp.swift */; }; 17 | 0BBDA4B7271E00910055F30B /* NotificationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BBDA4B6271E00910055F30B /* NotificationController.swift */; }; 18 | 0BBDA4B9271E00910055F30B /* NotificationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BBDA4B8271E00910055F30B /* NotificationView.swift */; }; 19 | 0BBDA4BB271E00910055F30B /* ComplicationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BBDA4BA271E00910055F30B /* ComplicationController.swift */; }; 20 | 0BBDA4BD271E00920055F30B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0BBDA4BC271E00920055F30B /* Assets.xcassets */; }; 21 | 0BBDA4C0271E00920055F30B /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0BBDA4BF271E00920055F30B /* Preview Assets.xcassets */; }; 22 | 0BBDA4C5271E00920055F30B /* RemindersWatch.app in Embed Watch Content */ = {isa = PBXBuildFile; fileRef = 0BBDA4A4271E00900055F30B /* RemindersWatch.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 23 | 0BBDA4CF271E03990055F30B /* WatchOSApp in Frameworks */ = {isa = PBXBuildFile; productRef = 0BBDA4CE271E03990055F30B /* WatchOSApp */; }; 24 | /* End PBXBuildFile section */ 25 | 26 | /* Begin PBXContainerItemProxy section */ 27 | 0BBDA4AF271E00910055F30B /* PBXContainerItemProxy */ = { 28 | isa = PBXContainerItemProxy; 29 | containerPortal = 0B1093962684D8E800D9A4DD /* Project object */; 30 | proxyType = 1; 31 | remoteGlobalIDString = 0BBDA4AC271E00910055F30B; 32 | remoteInfo = "RemindersWatch Extension"; 33 | }; 34 | 0BBDA4C3271E00920055F30B /* PBXContainerItemProxy */ = { 35 | isa = PBXContainerItemProxy; 36 | containerPortal = 0B1093962684D8E800D9A4DD /* Project object */; 37 | proxyType = 1; 38 | remoteGlobalIDString = 0BBDA4A3271E00900055F30B; 39 | remoteInfo = RemindersWatch; 40 | }; 41 | /* End PBXContainerItemProxy section */ 42 | 43 | /* Begin PBXCopyFilesBuildPhase section */ 44 | 0BBDA4C6271E00920055F30B /* Embed Watch Content */ = { 45 | isa = PBXCopyFilesBuildPhase; 46 | buildActionMask = 2147483647; 47 | dstPath = "$(CONTENTS_FOLDER_PATH)/Watch"; 48 | dstSubfolderSpec = 16; 49 | files = ( 50 | 0BBDA4C5271E00920055F30B /* RemindersWatch.app in Embed Watch Content */, 51 | ); 52 | name = "Embed Watch Content"; 53 | runOnlyForDeploymentPostprocessing = 0; 54 | }; 55 | 0BBDA4C9271E00920055F30B /* Embed App Extensions */ = { 56 | isa = PBXCopyFilesBuildPhase; 57 | buildActionMask = 2147483647; 58 | dstPath = ""; 59 | dstSubfolderSpec = 13; 60 | files = ( 61 | 0BBDA4AE271E00910055F30B /* RemindersWatch Extension.appex in Embed App Extensions */, 62 | ); 63 | name = "Embed App Extensions"; 64 | runOnlyForDeploymentPostprocessing = 0; 65 | }; 66 | /* End PBXCopyFilesBuildPhase section */ 67 | 68 | /* Begin PBXFileReference section */ 69 | 0B10939E2684D8E800D9A4DD /* Reminders.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Reminders.app; sourceTree = BUILT_PRODUCTS_DIR; }; 70 | 0B1093A12684D8E800D9A4DD /* RemindersApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemindersApp.swift; sourceTree = ""; }; 71 | 0B1093A52684D8EA00D9A4DD /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 72 | 0B1093A82684D8EA00D9A4DD /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 73 | 0B1093AA2684D8EA00D9A4DD /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 74 | 0B60A53026DAABD900531DF0 /* RemindersPackage */ = {isa = PBXFileReference; lastKnownFileType = folder; path = RemindersPackage; sourceTree = ""; }; 75 | 0BBDA4A4271E00900055F30B /* RemindersWatch.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = RemindersWatch.app; sourceTree = BUILT_PRODUCTS_DIR; }; 76 | 0BBDA4A6271E00910055F30B /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 77 | 0BBDA4A8271E00910055F30B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 78 | 0BBDA4AD271E00910055F30B /* RemindersWatch Extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "RemindersWatch Extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; 79 | 0BBDA4B2271E00910055F30B /* RemindersApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemindersApp.swift; sourceTree = ""; }; 80 | 0BBDA4B6271E00910055F30B /* NotificationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationController.swift; sourceTree = ""; }; 81 | 0BBDA4B8271E00910055F30B /* NotificationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationView.swift; sourceTree = ""; }; 82 | 0BBDA4BA271E00910055F30B /* ComplicationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComplicationController.swift; sourceTree = ""; }; 83 | 0BBDA4BC271E00920055F30B /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 84 | 0BBDA4BF271E00920055F30B /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 85 | 0BBDA4C1271E00920055F30B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 86 | 0BBDA4C2271E00920055F30B /* PushNotificationPayload.apns */ = {isa = PBXFileReference; lastKnownFileType = text; path = PushNotificationPayload.apns; sourceTree = ""; }; 87 | /* End PBXFileReference section */ 88 | 89 | /* Begin PBXFrameworksBuildPhase section */ 90 | 0B10939B2684D8E800D9A4DD /* Frameworks */ = { 91 | isa = PBXFrameworksBuildPhase; 92 | buildActionMask = 2147483647; 93 | files = ( 94 | 0B60A5AC26DBE6A500531DF0 /* App in Frameworks */, 95 | ); 96 | runOnlyForDeploymentPostprocessing = 0; 97 | }; 98 | 0BBDA4AA271E00910055F30B /* Frameworks */ = { 99 | isa = PBXFrameworksBuildPhase; 100 | buildActionMask = 2147483647; 101 | files = ( 102 | 0BBDA4CF271E03990055F30B /* WatchOSApp in Frameworks */, 103 | ); 104 | runOnlyForDeploymentPostprocessing = 0; 105 | }; 106 | /* End PBXFrameworksBuildPhase section */ 107 | 108 | /* Begin PBXGroup section */ 109 | 0B1093952684D8E800D9A4DD = { 110 | isa = PBXGroup; 111 | children = ( 112 | 0B60A53026DAABD900531DF0 /* RemindersPackage */, 113 | 0B1093A02684D8E800D9A4DD /* Reminders */, 114 | 0BBDA4A5271E00900055F30B /* RemindersWatch */, 115 | 0BBDA4B1271E00910055F30B /* RemindersWatch Extension */, 116 | 0B10939F2684D8E800D9A4DD /* Products */, 117 | 0B60A59F26DBE4A200531DF0 /* Frameworks */, 118 | ); 119 | sourceTree = ""; 120 | }; 121 | 0B10939F2684D8E800D9A4DD /* Products */ = { 122 | isa = PBXGroup; 123 | children = ( 124 | 0B10939E2684D8E800D9A4DD /* Reminders.app */, 125 | 0BBDA4A4271E00900055F30B /* RemindersWatch.app */, 126 | 0BBDA4AD271E00910055F30B /* RemindersWatch Extension.appex */, 127 | ); 128 | name = Products; 129 | sourceTree = ""; 130 | }; 131 | 0B1093A02684D8E800D9A4DD /* Reminders */ = { 132 | isa = PBXGroup; 133 | children = ( 134 | 0B1093A12684D8E800D9A4DD /* RemindersApp.swift */, 135 | 0B1093A52684D8EA00D9A4DD /* Assets.xcassets */, 136 | 0B1093AA2684D8EA00D9A4DD /* Info.plist */, 137 | 0B1093A72684D8EA00D9A4DD /* Preview Content */, 138 | ); 139 | path = Reminders; 140 | sourceTree = ""; 141 | }; 142 | 0B1093A72684D8EA00D9A4DD /* Preview Content */ = { 143 | isa = PBXGroup; 144 | children = ( 145 | 0B1093A82684D8EA00D9A4DD /* Preview Assets.xcassets */, 146 | ); 147 | path = "Preview Content"; 148 | sourceTree = ""; 149 | }; 150 | 0B60A59F26DBE4A200531DF0 /* Frameworks */ = { 151 | isa = PBXGroup; 152 | children = ( 153 | ); 154 | name = Frameworks; 155 | sourceTree = ""; 156 | }; 157 | 0BBDA4A5271E00900055F30B /* RemindersWatch */ = { 158 | isa = PBXGroup; 159 | children = ( 160 | 0BBDA4A6271E00910055F30B /* Assets.xcassets */, 161 | 0BBDA4A8271E00910055F30B /* Info.plist */, 162 | ); 163 | path = RemindersWatch; 164 | sourceTree = ""; 165 | }; 166 | 0BBDA4B1271E00910055F30B /* RemindersWatch Extension */ = { 167 | isa = PBXGroup; 168 | children = ( 169 | 0BBDA4B2271E00910055F30B /* RemindersApp.swift */, 170 | 0BBDA4B6271E00910055F30B /* NotificationController.swift */, 171 | 0BBDA4B8271E00910055F30B /* NotificationView.swift */, 172 | 0BBDA4BA271E00910055F30B /* ComplicationController.swift */, 173 | 0BBDA4BC271E00920055F30B /* Assets.xcassets */, 174 | 0BBDA4C1271E00920055F30B /* Info.plist */, 175 | 0BBDA4C2271E00920055F30B /* PushNotificationPayload.apns */, 176 | 0BBDA4BE271E00920055F30B /* Preview Content */, 177 | ); 178 | path = "RemindersWatch Extension"; 179 | sourceTree = ""; 180 | }; 181 | 0BBDA4BE271E00920055F30B /* Preview Content */ = { 182 | isa = PBXGroup; 183 | children = ( 184 | 0BBDA4BF271E00920055F30B /* Preview Assets.xcassets */, 185 | ); 186 | path = "Preview Content"; 187 | sourceTree = ""; 188 | }; 189 | /* End PBXGroup section */ 190 | 191 | /* Begin PBXNativeTarget section */ 192 | 0B10939D2684D8E800D9A4DD /* Reminders */ = { 193 | isa = PBXNativeTarget; 194 | buildConfigurationList = 0B1093C32684D8EA00D9A4DD /* Build configuration list for PBXNativeTarget "Reminders" */; 195 | buildPhases = ( 196 | 0B10939A2684D8E800D9A4DD /* Sources */, 197 | 0B10939B2684D8E800D9A4DD /* Frameworks */, 198 | 0B10939C2684D8E800D9A4DD /* Resources */, 199 | 0BBDA4C6271E00920055F30B /* Embed Watch Content */, 200 | ); 201 | buildRules = ( 202 | ); 203 | dependencies = ( 204 | 0BBDA4C4271E00920055F30B /* PBXTargetDependency */, 205 | ); 206 | name = Reminders; 207 | packageProductDependencies = ( 208 | 0B60A5AB26DBE6A500531DF0 /* App */, 209 | ); 210 | productName = Reminders; 211 | productReference = 0B10939E2684D8E800D9A4DD /* Reminders.app */; 212 | productType = "com.apple.product-type.application"; 213 | }; 214 | 0BBDA4A3271E00900055F30B /* RemindersWatch */ = { 215 | isa = PBXNativeTarget; 216 | buildConfigurationList = 0BBDA4CD271E00920055F30B /* Build configuration list for PBXNativeTarget "RemindersWatch" */; 217 | buildPhases = ( 218 | 0BBDA4A2271E00900055F30B /* Resources */, 219 | 0BBDA4C9271E00920055F30B /* Embed App Extensions */, 220 | ); 221 | buildRules = ( 222 | ); 223 | dependencies = ( 224 | 0BBDA4B0271E00910055F30B /* PBXTargetDependency */, 225 | ); 226 | name = RemindersWatch; 227 | productName = RemindersWatch; 228 | productReference = 0BBDA4A4271E00900055F30B /* RemindersWatch.app */; 229 | productType = "com.apple.product-type.application.watchapp2"; 230 | }; 231 | 0BBDA4AC271E00910055F30B /* RemindersWatch Extension */ = { 232 | isa = PBXNativeTarget; 233 | buildConfigurationList = 0BBDA4CC271E00920055F30B /* Build configuration list for PBXNativeTarget "RemindersWatch Extension" */; 234 | buildPhases = ( 235 | 0BBDA4A9271E00910055F30B /* Sources */, 236 | 0BBDA4AA271E00910055F30B /* Frameworks */, 237 | 0BBDA4AB271E00910055F30B /* Resources */, 238 | ); 239 | buildRules = ( 240 | ); 241 | dependencies = ( 242 | ); 243 | name = "RemindersWatch Extension"; 244 | packageProductDependencies = ( 245 | 0BBDA4CE271E03990055F30B /* WatchOSApp */, 246 | ); 247 | productName = "RemindersWatch Extension"; 248 | productReference = 0BBDA4AD271E00910055F30B /* RemindersWatch Extension.appex */; 249 | productType = "com.apple.product-type.watchkit2-extension"; 250 | }; 251 | /* End PBXNativeTarget section */ 252 | 253 | /* Begin PBXProject section */ 254 | 0B1093962684D8E800D9A4DD /* Project object */ = { 255 | isa = PBXProject; 256 | attributes = { 257 | LastSwiftUpdateCheck = 1250; 258 | LastUpgradeCheck = 1240; 259 | TargetAttributes = { 260 | 0B10939D2684D8E800D9A4DD = { 261 | CreatedOnToolsVersion = 12.4; 262 | }; 263 | 0BBDA4A3271E00900055F30B = { 264 | CreatedOnToolsVersion = 12.5.1; 265 | }; 266 | 0BBDA4AC271E00910055F30B = { 267 | CreatedOnToolsVersion = 12.5.1; 268 | }; 269 | }; 270 | }; 271 | buildConfigurationList = 0B1093992684D8E800D9A4DD /* Build configuration list for PBXProject "Reminders" */; 272 | compatibilityVersion = "Xcode 9.3"; 273 | developmentRegion = en; 274 | hasScannedForEncodings = 0; 275 | knownRegions = ( 276 | en, 277 | Base, 278 | ); 279 | mainGroup = 0B1093952684D8E800D9A4DD; 280 | packageReferences = ( 281 | 0B1093CF2684D91700D9A4DD /* XCRemoteSwiftPackageReference "swift-composable-architecture" */, 282 | ); 283 | productRefGroup = 0B10939F2684D8E800D9A4DD /* Products */; 284 | projectDirPath = ""; 285 | projectRoot = ""; 286 | targets = ( 287 | 0B10939D2684D8E800D9A4DD /* Reminders */, 288 | 0BBDA4A3271E00900055F30B /* RemindersWatch */, 289 | 0BBDA4AC271E00910055F30B /* RemindersWatch Extension */, 290 | ); 291 | }; 292 | /* End PBXProject section */ 293 | 294 | /* Begin PBXResourcesBuildPhase section */ 295 | 0B10939C2684D8E800D9A4DD /* Resources */ = { 296 | isa = PBXResourcesBuildPhase; 297 | buildActionMask = 2147483647; 298 | files = ( 299 | 0B1093A92684D8EA00D9A4DD /* Preview Assets.xcassets in Resources */, 300 | 0B1093A62684D8EA00D9A4DD /* Assets.xcassets in Resources */, 301 | ); 302 | runOnlyForDeploymentPostprocessing = 0; 303 | }; 304 | 0BBDA4A2271E00900055F30B /* Resources */ = { 305 | isa = PBXResourcesBuildPhase; 306 | buildActionMask = 2147483647; 307 | files = ( 308 | 0BBDA4A7271E00910055F30B /* Assets.xcassets in Resources */, 309 | ); 310 | runOnlyForDeploymentPostprocessing = 0; 311 | }; 312 | 0BBDA4AB271E00910055F30B /* Resources */ = { 313 | isa = PBXResourcesBuildPhase; 314 | buildActionMask = 2147483647; 315 | files = ( 316 | 0BBDA4C0271E00920055F30B /* Preview Assets.xcassets in Resources */, 317 | 0BBDA4BD271E00920055F30B /* Assets.xcassets in Resources */, 318 | ); 319 | runOnlyForDeploymentPostprocessing = 0; 320 | }; 321 | /* End PBXResourcesBuildPhase section */ 322 | 323 | /* Begin PBXSourcesBuildPhase section */ 324 | 0B10939A2684D8E800D9A4DD /* Sources */ = { 325 | isa = PBXSourcesBuildPhase; 326 | buildActionMask = 2147483647; 327 | files = ( 328 | 0B1093A22684D8E800D9A4DD /* RemindersApp.swift in Sources */, 329 | ); 330 | runOnlyForDeploymentPostprocessing = 0; 331 | }; 332 | 0BBDA4A9271E00910055F30B /* Sources */ = { 333 | isa = PBXSourcesBuildPhase; 334 | buildActionMask = 2147483647; 335 | files = ( 336 | 0BBDA4B7271E00910055F30B /* NotificationController.swift in Sources */, 337 | 0BBDA4BB271E00910055F30B /* ComplicationController.swift in Sources */, 338 | 0BBDA4B3271E00910055F30B /* RemindersApp.swift in Sources */, 339 | 0BBDA4B9271E00910055F30B /* NotificationView.swift in Sources */, 340 | ); 341 | runOnlyForDeploymentPostprocessing = 0; 342 | }; 343 | /* End PBXSourcesBuildPhase section */ 344 | 345 | /* Begin PBXTargetDependency section */ 346 | 0BBDA4B0271E00910055F30B /* PBXTargetDependency */ = { 347 | isa = PBXTargetDependency; 348 | target = 0BBDA4AC271E00910055F30B /* RemindersWatch Extension */; 349 | targetProxy = 0BBDA4AF271E00910055F30B /* PBXContainerItemProxy */; 350 | }; 351 | 0BBDA4C4271E00920055F30B /* PBXTargetDependency */ = { 352 | isa = PBXTargetDependency; 353 | target = 0BBDA4A3271E00900055F30B /* RemindersWatch */; 354 | targetProxy = 0BBDA4C3271E00920055F30B /* PBXContainerItemProxy */; 355 | }; 356 | /* End PBXTargetDependency section */ 357 | 358 | /* Begin XCBuildConfiguration section */ 359 | 0B1093C12684D8EA00D9A4DD /* Debug */ = { 360 | isa = XCBuildConfiguration; 361 | buildSettings = { 362 | ALWAYS_SEARCH_USER_PATHS = NO; 363 | CLANG_ANALYZER_NONNULL = YES; 364 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 365 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 366 | CLANG_CXX_LIBRARY = "libc++"; 367 | CLANG_ENABLE_MODULES = YES; 368 | CLANG_ENABLE_OBJC_ARC = YES; 369 | CLANG_ENABLE_OBJC_WEAK = YES; 370 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 371 | CLANG_WARN_BOOL_CONVERSION = YES; 372 | CLANG_WARN_COMMA = YES; 373 | CLANG_WARN_CONSTANT_CONVERSION = YES; 374 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 375 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 376 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 377 | CLANG_WARN_EMPTY_BODY = YES; 378 | CLANG_WARN_ENUM_CONVERSION = YES; 379 | CLANG_WARN_INFINITE_RECURSION = YES; 380 | CLANG_WARN_INT_CONVERSION = YES; 381 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 382 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 383 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 384 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 385 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 386 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 387 | CLANG_WARN_STRICT_PROTOTYPES = YES; 388 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 389 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 390 | CLANG_WARN_UNREACHABLE_CODE = YES; 391 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 392 | COPY_PHASE_STRIP = NO; 393 | DEBUG_INFORMATION_FORMAT = dwarf; 394 | ENABLE_STRICT_OBJC_MSGSEND = YES; 395 | ENABLE_TESTABILITY = YES; 396 | GCC_C_LANGUAGE_STANDARD = gnu11; 397 | GCC_DYNAMIC_NO_PIC = NO; 398 | GCC_NO_COMMON_BLOCKS = YES; 399 | GCC_OPTIMIZATION_LEVEL = 0; 400 | GCC_PREPROCESSOR_DEFINITIONS = ( 401 | "DEBUG=1", 402 | "$(inherited)", 403 | ); 404 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 405 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 406 | GCC_WARN_UNDECLARED_SELECTOR = YES; 407 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 408 | GCC_WARN_UNUSED_FUNCTION = YES; 409 | GCC_WARN_UNUSED_VARIABLE = YES; 410 | IPHONEOS_DEPLOYMENT_TARGET = 14.4; 411 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 412 | MTL_FAST_MATH = YES; 413 | ONLY_ACTIVE_ARCH = YES; 414 | SDKROOT = iphoneos; 415 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 416 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 417 | }; 418 | name = Debug; 419 | }; 420 | 0B1093C22684D8EA00D9A4DD /* Release */ = { 421 | isa = XCBuildConfiguration; 422 | buildSettings = { 423 | ALWAYS_SEARCH_USER_PATHS = NO; 424 | CLANG_ANALYZER_NONNULL = YES; 425 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 426 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 427 | CLANG_CXX_LIBRARY = "libc++"; 428 | CLANG_ENABLE_MODULES = YES; 429 | CLANG_ENABLE_OBJC_ARC = YES; 430 | CLANG_ENABLE_OBJC_WEAK = YES; 431 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 432 | CLANG_WARN_BOOL_CONVERSION = YES; 433 | CLANG_WARN_COMMA = YES; 434 | CLANG_WARN_CONSTANT_CONVERSION = YES; 435 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 436 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 437 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 438 | CLANG_WARN_EMPTY_BODY = YES; 439 | CLANG_WARN_ENUM_CONVERSION = YES; 440 | CLANG_WARN_INFINITE_RECURSION = YES; 441 | CLANG_WARN_INT_CONVERSION = YES; 442 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 443 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 444 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 445 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 446 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 447 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 448 | CLANG_WARN_STRICT_PROTOTYPES = YES; 449 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 450 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 451 | CLANG_WARN_UNREACHABLE_CODE = YES; 452 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 453 | COPY_PHASE_STRIP = NO; 454 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 455 | ENABLE_NS_ASSERTIONS = NO; 456 | ENABLE_STRICT_OBJC_MSGSEND = YES; 457 | GCC_C_LANGUAGE_STANDARD = gnu11; 458 | GCC_NO_COMMON_BLOCKS = YES; 459 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 460 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 461 | GCC_WARN_UNDECLARED_SELECTOR = YES; 462 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 463 | GCC_WARN_UNUSED_FUNCTION = YES; 464 | GCC_WARN_UNUSED_VARIABLE = YES; 465 | IPHONEOS_DEPLOYMENT_TARGET = 14.4; 466 | MTL_ENABLE_DEBUG_INFO = NO; 467 | MTL_FAST_MATH = YES; 468 | SDKROOT = iphoneos; 469 | SWIFT_COMPILATION_MODE = wholemodule; 470 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 471 | VALIDATE_PRODUCT = YES; 472 | }; 473 | name = Release; 474 | }; 475 | 0B1093C42684D8EA00D9A4DD /* Debug */ = { 476 | isa = XCBuildConfiguration; 477 | buildSettings = { 478 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 479 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 480 | CODE_SIGN_STYLE = Automatic; 481 | DEVELOPMENT_ASSET_PATHS = "\"Reminders/Preview Content\""; 482 | DEVELOPMENT_TEAM = BGC446KJB8; 483 | ENABLE_PREVIEWS = YES; 484 | INFOPLIST_FILE = Reminders/Info.plist; 485 | IPHONEOS_DEPLOYMENT_TARGET = 14.0; 486 | LD_RUNPATH_SEARCH_PATHS = ( 487 | "$(inherited)", 488 | "@executable_path/Frameworks", 489 | ); 490 | PRODUCT_BUNDLE_IDENTIFIER = com.hristic.luka.Reminders; 491 | PRODUCT_NAME = "$(TARGET_NAME)"; 492 | SWIFT_VERSION = 5.0; 493 | TARGETED_DEVICE_FAMILY = "1,2"; 494 | }; 495 | name = Debug; 496 | }; 497 | 0B1093C52684D8EA00D9A4DD /* Release */ = { 498 | isa = XCBuildConfiguration; 499 | buildSettings = { 500 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 501 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 502 | CODE_SIGN_STYLE = Automatic; 503 | DEVELOPMENT_ASSET_PATHS = "\"Reminders/Preview Content\""; 504 | DEVELOPMENT_TEAM = BGC446KJB8; 505 | ENABLE_PREVIEWS = YES; 506 | INFOPLIST_FILE = Reminders/Info.plist; 507 | IPHONEOS_DEPLOYMENT_TARGET = 14.0; 508 | LD_RUNPATH_SEARCH_PATHS = ( 509 | "$(inherited)", 510 | "@executable_path/Frameworks", 511 | ); 512 | PRODUCT_BUNDLE_IDENTIFIER = com.hristic.luka.Reminders; 513 | PRODUCT_NAME = "$(TARGET_NAME)"; 514 | SWIFT_VERSION = 5.0; 515 | TARGETED_DEVICE_FAMILY = "1,2"; 516 | }; 517 | name = Release; 518 | }; 519 | 0BBDA4C7271E00920055F30B /* Debug */ = { 520 | isa = XCBuildConfiguration; 521 | buildSettings = { 522 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 523 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 524 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 525 | CODE_SIGN_STYLE = Automatic; 526 | DEVELOPMENT_TEAM = BGC446KJB8; 527 | IBSC_MODULE = RemindersWatch_Extension; 528 | INFOPLIST_FILE = RemindersWatch/Info.plist; 529 | PRODUCT_BUNDLE_IDENTIFIER = com.hristic.luka.Reminders.watchkitapp; 530 | PRODUCT_NAME = "$(TARGET_NAME)"; 531 | SDKROOT = watchos; 532 | SKIP_INSTALL = YES; 533 | SWIFT_VERSION = 5.0; 534 | TARGETED_DEVICE_FAMILY = 4; 535 | WATCHOS_DEPLOYMENT_TARGET = 7.4; 536 | }; 537 | name = Debug; 538 | }; 539 | 0BBDA4C8271E00920055F30B /* Release */ = { 540 | isa = XCBuildConfiguration; 541 | buildSettings = { 542 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 543 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 544 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 545 | CODE_SIGN_STYLE = Automatic; 546 | DEVELOPMENT_TEAM = BGC446KJB8; 547 | IBSC_MODULE = RemindersWatch_Extension; 548 | INFOPLIST_FILE = RemindersWatch/Info.plist; 549 | PRODUCT_BUNDLE_IDENTIFIER = com.hristic.luka.Reminders.watchkitapp; 550 | PRODUCT_NAME = "$(TARGET_NAME)"; 551 | SDKROOT = watchos; 552 | SKIP_INSTALL = YES; 553 | SWIFT_VERSION = 5.0; 554 | TARGETED_DEVICE_FAMILY = 4; 555 | WATCHOS_DEPLOYMENT_TARGET = 7.4; 556 | }; 557 | name = Release; 558 | }; 559 | 0BBDA4CA271E00920055F30B /* Debug */ = { 560 | isa = XCBuildConfiguration; 561 | buildSettings = { 562 | ASSETCATALOG_COMPILER_COMPLICATION_NAME = Complication; 563 | CODE_SIGN_STYLE = Automatic; 564 | DEVELOPMENT_ASSET_PATHS = "\"RemindersWatch Extension/Preview Content\""; 565 | DEVELOPMENT_TEAM = BGC446KJB8; 566 | ENABLE_PREVIEWS = YES; 567 | INFOPLIST_FILE = "RemindersWatch Extension/Info.plist"; 568 | LD_RUNPATH_SEARCH_PATHS = ( 569 | "$(inherited)", 570 | "@executable_path/Frameworks", 571 | "@executable_path/../../Frameworks", 572 | ); 573 | PRODUCT_BUNDLE_IDENTIFIER = com.hristic.luka.Reminders.watchkitapp.watchkitextension; 574 | PRODUCT_NAME = "${TARGET_NAME}"; 575 | SDKROOT = watchos; 576 | SKIP_INSTALL = YES; 577 | SWIFT_VERSION = 5.0; 578 | TARGETED_DEVICE_FAMILY = 4; 579 | WATCHOS_DEPLOYMENT_TARGET = 7.4; 580 | }; 581 | name = Debug; 582 | }; 583 | 0BBDA4CB271E00920055F30B /* Release */ = { 584 | isa = XCBuildConfiguration; 585 | buildSettings = { 586 | ASSETCATALOG_COMPILER_COMPLICATION_NAME = Complication; 587 | CODE_SIGN_STYLE = Automatic; 588 | DEVELOPMENT_ASSET_PATHS = "\"RemindersWatch Extension/Preview Content\""; 589 | DEVELOPMENT_TEAM = BGC446KJB8; 590 | ENABLE_PREVIEWS = YES; 591 | INFOPLIST_FILE = "RemindersWatch Extension/Info.plist"; 592 | LD_RUNPATH_SEARCH_PATHS = ( 593 | "$(inherited)", 594 | "@executable_path/Frameworks", 595 | "@executable_path/../../Frameworks", 596 | ); 597 | PRODUCT_BUNDLE_IDENTIFIER = com.hristic.luka.Reminders.watchkitapp.watchkitextension; 598 | PRODUCT_NAME = "${TARGET_NAME}"; 599 | SDKROOT = watchos; 600 | SKIP_INSTALL = YES; 601 | SWIFT_VERSION = 5.0; 602 | TARGETED_DEVICE_FAMILY = 4; 603 | WATCHOS_DEPLOYMENT_TARGET = 7.4; 604 | }; 605 | name = Release; 606 | }; 607 | /* End XCBuildConfiguration section */ 608 | 609 | /* Begin XCConfigurationList section */ 610 | 0B1093992684D8E800D9A4DD /* Build configuration list for PBXProject "Reminders" */ = { 611 | isa = XCConfigurationList; 612 | buildConfigurations = ( 613 | 0B1093C12684D8EA00D9A4DD /* Debug */, 614 | 0B1093C22684D8EA00D9A4DD /* Release */, 615 | ); 616 | defaultConfigurationIsVisible = 0; 617 | defaultConfigurationName = Release; 618 | }; 619 | 0B1093C32684D8EA00D9A4DD /* Build configuration list for PBXNativeTarget "Reminders" */ = { 620 | isa = XCConfigurationList; 621 | buildConfigurations = ( 622 | 0B1093C42684D8EA00D9A4DD /* Debug */, 623 | 0B1093C52684D8EA00D9A4DD /* Release */, 624 | ); 625 | defaultConfigurationIsVisible = 0; 626 | defaultConfigurationName = Release; 627 | }; 628 | 0BBDA4CC271E00920055F30B /* Build configuration list for PBXNativeTarget "RemindersWatch Extension" */ = { 629 | isa = XCConfigurationList; 630 | buildConfigurations = ( 631 | 0BBDA4CA271E00920055F30B /* Debug */, 632 | 0BBDA4CB271E00920055F30B /* Release */, 633 | ); 634 | defaultConfigurationIsVisible = 0; 635 | defaultConfigurationName = Release; 636 | }; 637 | 0BBDA4CD271E00920055F30B /* Build configuration list for PBXNativeTarget "RemindersWatch" */ = { 638 | isa = XCConfigurationList; 639 | buildConfigurations = ( 640 | 0BBDA4C7271E00920055F30B /* Debug */, 641 | 0BBDA4C8271E00920055F30B /* Release */, 642 | ); 643 | defaultConfigurationIsVisible = 0; 644 | defaultConfigurationName = Release; 645 | }; 646 | /* End XCConfigurationList section */ 647 | 648 | /* Begin XCRemoteSwiftPackageReference section */ 649 | 0B1093CF2684D91700D9A4DD /* XCRemoteSwiftPackageReference "swift-composable-architecture" */ = { 650 | isa = XCRemoteSwiftPackageReference; 651 | repositoryURL = "https://github.com/pointfreeco/swift-composable-architecture.git"; 652 | requirement = { 653 | kind = upToNextMajorVersion; 654 | minimumVersion = 0.27.1; 655 | }; 656 | }; 657 | /* End XCRemoteSwiftPackageReference section */ 658 | 659 | /* Begin XCSwiftPackageProductDependency section */ 660 | 0B60A5AB26DBE6A500531DF0 /* App */ = { 661 | isa = XCSwiftPackageProductDependency; 662 | productName = App; 663 | }; 664 | 0BBDA4CE271E03990055F30B /* WatchOSApp */ = { 665 | isa = XCSwiftPackageProductDependency; 666 | productName = WatchOSApp; 667 | }; 668 | /* End XCSwiftPackageProductDependency section */ 669 | }; 670 | rootObject = 0B1093962684D8E800D9A4DD /* Project object */; 671 | } 672 | -------------------------------------------------------------------------------- /Reminders.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Reminders.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Reminders.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "combine-schedulers", 6 | "repositoryURL": "https://github.com/pointfreeco/combine-schedulers", 7 | "state": { 8 | "branch": null, 9 | "revision": "c37e5ae8012fb654af776cc556ff8ae64398c841", 10 | "version": "0.5.0" 11 | } 12 | }, 13 | { 14 | "package": "swift-case-paths", 15 | "repositoryURL": "https://github.com/pointfreeco/swift-case-paths", 16 | "state": { 17 | "branch": null, 18 | "revision": "d226d167bd4a68b51e352af5655c92bce8ee0463", 19 | "version": "0.7.0" 20 | } 21 | }, 22 | { 23 | "package": "swift-collections", 24 | "repositoryURL": "https://github.com/apple/swift-collections", 25 | "state": { 26 | "branch": null, 27 | "revision": "07e47b1e93e5a1e0ef0c50fcb2d6739fb6be4003", 28 | "version": "1.0.0" 29 | } 30 | }, 31 | { 32 | "package": "swift-composable-architecture", 33 | "repositoryURL": "https://github.com/pointfreeco/swift-composable-architecture.git", 34 | "state": { 35 | "branch": null, 36 | "revision": "a1aac6cfd654051d0ab5e626462afff36c399219", 37 | "version": "0.27.1" 38 | } 39 | }, 40 | { 41 | "package": "swift-custom-dump", 42 | "repositoryURL": "https://github.com/pointfreeco/swift-custom-dump", 43 | "state": { 44 | "branch": null, 45 | "revision": "1a2947d25d43e295c6f5e83f54696d590620b364", 46 | "version": "0.1.3" 47 | } 48 | }, 49 | { 50 | "package": "swift-identified-collections", 51 | "repositoryURL": "https://github.com/pointfreeco/swift-identified-collections", 52 | "state": { 53 | "branch": null, 54 | "revision": "c8e6a40209650ab619853cd4ce89a0aa51792754", 55 | "version": "0.3.0" 56 | } 57 | }, 58 | { 59 | "package": "xctest-dynamic-overlay", 60 | "repositoryURL": "https://github.com/pointfreeco/xctest-dynamic-overlay", 61 | "state": { 62 | "branch": null, 63 | "revision": "50a70a9d3583fe228ce672e8923010c8df2deddd", 64 | "version": "0.2.1" 65 | } 66 | } 67 | ] 68 | }, 69 | "version": 1 70 | } 71 | -------------------------------------------------------------------------------- /Reminders.xcodeproj/xcshareddata/xcschemes/Reminders.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 32 | 33 | 39 | 40 | 41 | 42 | 44 | 50 | 51 | 52 | 54 | 60 | 61 | 62 | 63 | 64 | 74 | 76 | 82 | 83 | 84 | 85 | 91 | 93 | 99 | 100 | 101 | 102 | 104 | 105 | 108 | 109 | 110 | -------------------------------------------------------------------------------- /Reminders/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "notification40.png", 5 | "idiom" : "iphone", 6 | "scale" : "2x", 7 | "size" : "20x20" 8 | }, 9 | { 10 | "filename" : "notification60.png", 11 | "idiom" : "iphone", 12 | "scale" : "3x", 13 | "size" : "20x20" 14 | }, 15 | { 16 | "filename" : "settings58.png", 17 | "idiom" : "iphone", 18 | "scale" : "2x", 19 | "size" : "29x29" 20 | }, 21 | { 22 | "filename" : "settings87.png", 23 | "idiom" : "iphone", 24 | "scale" : "3x", 25 | "size" : "29x29" 26 | }, 27 | { 28 | "filename" : "spotlight80.png", 29 | "idiom" : "iphone", 30 | "scale" : "2x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "filename" : "spotlight120.png", 35 | "idiom" : "iphone", 36 | "scale" : "3x", 37 | "size" : "40x40" 38 | }, 39 | { 40 | "filename" : "iphone120.png", 41 | "idiom" : "iphone", 42 | "scale" : "2x", 43 | "size" : "60x60" 44 | }, 45 | { 46 | "filename" : "iphone180.png", 47 | "idiom" : "iphone", 48 | "scale" : "3x", 49 | "size" : "60x60" 50 | }, 51 | { 52 | "filename" : "ipadNotification20.png", 53 | "idiom" : "ipad", 54 | "scale" : "1x", 55 | "size" : "20x20" 56 | }, 57 | { 58 | "filename" : "ipadNotification40.png", 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "20x20" 62 | }, 63 | { 64 | "filename" : "ipadSettings29.png", 65 | "idiom" : "ipad", 66 | "scale" : "1x", 67 | "size" : "29x29" 68 | }, 69 | { 70 | "filename" : "ipadSettings58.png", 71 | "idiom" : "ipad", 72 | "scale" : "2x", 73 | "size" : "29x29" 74 | }, 75 | { 76 | "filename" : "ipadSpotlight40.png", 77 | "idiom" : "ipad", 78 | "scale" : "1x", 79 | "size" : "40x40" 80 | }, 81 | { 82 | "filename" : "ipadSpotlight80.png", 83 | "idiom" : "ipad", 84 | "scale" : "2x", 85 | "size" : "40x40" 86 | }, 87 | { 88 | "filename" : "ipad76.png", 89 | "idiom" : "ipad", 90 | "scale" : "1x", 91 | "size" : "76x76" 92 | }, 93 | { 94 | "filename" : "ipad152.png", 95 | "idiom" : "ipad", 96 | "scale" : "2x", 97 | "size" : "76x76" 98 | }, 99 | { 100 | "filename" : "ipadPro167.png", 101 | "idiom" : "ipad", 102 | "scale" : "2x", 103 | "size" : "83.5x83.5" 104 | }, 105 | { 106 | "filename" : "appstore1024.png", 107 | "idiom" : "ios-marketing", 108 | "scale" : "1x", 109 | "size" : "1024x1024" 110 | }, 111 | { 112 | "filename" : "mac16.png", 113 | "idiom" : "mac", 114 | "scale" : "1x", 115 | "size" : "16x16" 116 | }, 117 | { 118 | "filename" : "mac32.png", 119 | "idiom" : "mac", 120 | "scale" : "2x", 121 | "size" : "16x16" 122 | }, 123 | { 124 | "filename" : "mac32.png", 125 | "idiom" : "mac", 126 | "scale" : "1x", 127 | "size" : "32x32" 128 | }, 129 | { 130 | "filename" : "mac64.png", 131 | "idiom" : "mac", 132 | "scale" : "2x", 133 | "size" : "32x32" 134 | }, 135 | { 136 | "filename" : "mac128.png", 137 | "idiom" : "mac", 138 | "scale" : "1x", 139 | "size" : "128x128" 140 | }, 141 | { 142 | "filename" : "mac256.png", 143 | "idiom" : "mac", 144 | "scale" : "2x", 145 | "size" : "128x128" 146 | }, 147 | { 148 | "filename" : "mac256.png", 149 | "idiom" : "mac", 150 | "scale" : "1x", 151 | "size" : "256x256" 152 | }, 153 | { 154 | "filename" : "mac512.png", 155 | "idiom" : "mac", 156 | "scale" : "2x", 157 | "size" : "256x256" 158 | }, 159 | { 160 | "filename" : "mac512.png", 161 | "idiom" : "mac", 162 | "scale" : "1x", 163 | "size" : "512x512" 164 | }, 165 | { 166 | "filename" : "mac1024.png", 167 | "idiom" : "mac", 168 | "scale" : "2x", 169 | "size" : "512x512" 170 | } 171 | ], 172 | "info" : { 173 | "author" : "xcode", 174 | "version" : 1 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /Reminders/Assets.xcassets/AppIcon.appiconset/appstore1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukeleleh/reminders-app/1b7ed21aca68c463ec2b85bee5d1e363504f929b/Reminders/Assets.xcassets/AppIcon.appiconset/appstore1024.png -------------------------------------------------------------------------------- /Reminders/Assets.xcassets/AppIcon.appiconset/ipad152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukeleleh/reminders-app/1b7ed21aca68c463ec2b85bee5d1e363504f929b/Reminders/Assets.xcassets/AppIcon.appiconset/ipad152.png -------------------------------------------------------------------------------- /Reminders/Assets.xcassets/AppIcon.appiconset/ipad76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukeleleh/reminders-app/1b7ed21aca68c463ec2b85bee5d1e363504f929b/Reminders/Assets.xcassets/AppIcon.appiconset/ipad76.png -------------------------------------------------------------------------------- /Reminders/Assets.xcassets/AppIcon.appiconset/ipadNotification20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukeleleh/reminders-app/1b7ed21aca68c463ec2b85bee5d1e363504f929b/Reminders/Assets.xcassets/AppIcon.appiconset/ipadNotification20.png -------------------------------------------------------------------------------- /Reminders/Assets.xcassets/AppIcon.appiconset/ipadNotification40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukeleleh/reminders-app/1b7ed21aca68c463ec2b85bee5d1e363504f929b/Reminders/Assets.xcassets/AppIcon.appiconset/ipadNotification40.png -------------------------------------------------------------------------------- /Reminders/Assets.xcassets/AppIcon.appiconset/ipadPro167.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukeleleh/reminders-app/1b7ed21aca68c463ec2b85bee5d1e363504f929b/Reminders/Assets.xcassets/AppIcon.appiconset/ipadPro167.png -------------------------------------------------------------------------------- /Reminders/Assets.xcassets/AppIcon.appiconset/ipadSettings29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukeleleh/reminders-app/1b7ed21aca68c463ec2b85bee5d1e363504f929b/Reminders/Assets.xcassets/AppIcon.appiconset/ipadSettings29.png -------------------------------------------------------------------------------- /Reminders/Assets.xcassets/AppIcon.appiconset/ipadSettings58.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukeleleh/reminders-app/1b7ed21aca68c463ec2b85bee5d1e363504f929b/Reminders/Assets.xcassets/AppIcon.appiconset/ipadSettings58.png -------------------------------------------------------------------------------- /Reminders/Assets.xcassets/AppIcon.appiconset/ipadSpotlight40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukeleleh/reminders-app/1b7ed21aca68c463ec2b85bee5d1e363504f929b/Reminders/Assets.xcassets/AppIcon.appiconset/ipadSpotlight40.png -------------------------------------------------------------------------------- /Reminders/Assets.xcassets/AppIcon.appiconset/ipadSpotlight80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukeleleh/reminders-app/1b7ed21aca68c463ec2b85bee5d1e363504f929b/Reminders/Assets.xcassets/AppIcon.appiconset/ipadSpotlight80.png -------------------------------------------------------------------------------- /Reminders/Assets.xcassets/AppIcon.appiconset/iphone120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukeleleh/reminders-app/1b7ed21aca68c463ec2b85bee5d1e363504f929b/Reminders/Assets.xcassets/AppIcon.appiconset/iphone120.png -------------------------------------------------------------------------------- /Reminders/Assets.xcassets/AppIcon.appiconset/iphone180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukeleleh/reminders-app/1b7ed21aca68c463ec2b85bee5d1e363504f929b/Reminders/Assets.xcassets/AppIcon.appiconset/iphone180.png -------------------------------------------------------------------------------- /Reminders/Assets.xcassets/AppIcon.appiconset/mac1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukeleleh/reminders-app/1b7ed21aca68c463ec2b85bee5d1e363504f929b/Reminders/Assets.xcassets/AppIcon.appiconset/mac1024.png -------------------------------------------------------------------------------- /Reminders/Assets.xcassets/AppIcon.appiconset/mac128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukeleleh/reminders-app/1b7ed21aca68c463ec2b85bee5d1e363504f929b/Reminders/Assets.xcassets/AppIcon.appiconset/mac128.png -------------------------------------------------------------------------------- /Reminders/Assets.xcassets/AppIcon.appiconset/mac16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukeleleh/reminders-app/1b7ed21aca68c463ec2b85bee5d1e363504f929b/Reminders/Assets.xcassets/AppIcon.appiconset/mac16.png -------------------------------------------------------------------------------- /Reminders/Assets.xcassets/AppIcon.appiconset/mac256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukeleleh/reminders-app/1b7ed21aca68c463ec2b85bee5d1e363504f929b/Reminders/Assets.xcassets/AppIcon.appiconset/mac256.png -------------------------------------------------------------------------------- /Reminders/Assets.xcassets/AppIcon.appiconset/mac32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukeleleh/reminders-app/1b7ed21aca68c463ec2b85bee5d1e363504f929b/Reminders/Assets.xcassets/AppIcon.appiconset/mac32.png -------------------------------------------------------------------------------- /Reminders/Assets.xcassets/AppIcon.appiconset/mac512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukeleleh/reminders-app/1b7ed21aca68c463ec2b85bee5d1e363504f929b/Reminders/Assets.xcassets/AppIcon.appiconset/mac512.png -------------------------------------------------------------------------------- /Reminders/Assets.xcassets/AppIcon.appiconset/mac64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukeleleh/reminders-app/1b7ed21aca68c463ec2b85bee5d1e363504f929b/Reminders/Assets.xcassets/AppIcon.appiconset/mac64.png -------------------------------------------------------------------------------- /Reminders/Assets.xcassets/AppIcon.appiconset/notification40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukeleleh/reminders-app/1b7ed21aca68c463ec2b85bee5d1e363504f929b/Reminders/Assets.xcassets/AppIcon.appiconset/notification40.png -------------------------------------------------------------------------------- /Reminders/Assets.xcassets/AppIcon.appiconset/notification60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukeleleh/reminders-app/1b7ed21aca68c463ec2b85bee5d1e363504f929b/Reminders/Assets.xcassets/AppIcon.appiconset/notification60.png -------------------------------------------------------------------------------- /Reminders/Assets.xcassets/AppIcon.appiconset/settings58.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukeleleh/reminders-app/1b7ed21aca68c463ec2b85bee5d1e363504f929b/Reminders/Assets.xcassets/AppIcon.appiconset/settings58.png -------------------------------------------------------------------------------- /Reminders/Assets.xcassets/AppIcon.appiconset/settings87.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukeleleh/reminders-app/1b7ed21aca68c463ec2b85bee5d1e363504f929b/Reminders/Assets.xcassets/AppIcon.appiconset/settings87.png -------------------------------------------------------------------------------- /Reminders/Assets.xcassets/AppIcon.appiconset/spotlight120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukeleleh/reminders-app/1b7ed21aca68c463ec2b85bee5d1e363504f929b/Reminders/Assets.xcassets/AppIcon.appiconset/spotlight120.png -------------------------------------------------------------------------------- /Reminders/Assets.xcassets/AppIcon.appiconset/spotlight80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukeleleh/reminders-app/1b7ed21aca68c463ec2b85bee5d1e363504f929b/Reminders/Assets.xcassets/AppIcon.appiconset/spotlight80.png -------------------------------------------------------------------------------- /Reminders/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Reminders/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UIApplicationSceneManifest 24 | 25 | UIApplicationSupportsMultipleScenes 26 | 27 | 28 | UIApplicationSupportsIndirectInputEvents 29 | 30 | UILaunchScreen 31 | 32 | UIRequiredDeviceCapabilities 33 | 34 | armv7 35 | 36 | UISupportedInterfaceOrientations 37 | 38 | UIInterfaceOrientationPortrait 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | UISupportedInterfaceOrientations~ipad 43 | 44 | UIInterfaceOrientationPortrait 45 | UIInterfaceOrientationPortraitUpsideDown 46 | UIInterfaceOrientationLandscapeLeft 47 | UIInterfaceOrientationLandscapeRight 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /Reminders/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Reminders/RemindersApp.swift: -------------------------------------------------------------------------------- 1 | import App 2 | import SwiftUI 3 | 4 | @main 5 | struct RemindersApp: App { 6 | @UIApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate 7 | 8 | var body: some Scene { 9 | WindowGroup { 10 | RemindersAppView(store: appDelegate.store) 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /RemindersPackage/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | -------------------------------------------------------------------------------- /RemindersPackage/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.3 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "RemindersPackage", 8 | platforms: [ 9 | .iOS(.v14), 10 | .watchOS(.v7) 11 | ], 12 | products: [ 13 | .library(name: "App", targets: ["App"]), 14 | .library(name: "AppCore", targets: ["AppCore"]), 15 | 16 | .library(name: "AppDelegateCore", targets: ["AppDelegateCore"]), 17 | 18 | .library(name: "NotificationCenterClient", targets: ["NotificationCenterClient"]), 19 | .library(name: "NotificationCenterClientLive", targets: ["NotificationCenterClientLive"]), 20 | 21 | .library(name: "ReminderDetailCore", targets: ["ReminderDetailCore"]), 22 | .library(name: "ReminderDetail", targets: ["ReminderDetail"]), 23 | 24 | .library(name: "RemindersListCore", targets: ["RemindersListCore"]), 25 | .library(name: "RemindersList", targets: ["RemindersList"]), 26 | 27 | .library(name: "RemindersListRowCore", targets: ["RemindersListRowCore"]), 28 | .library(name: "RemindersListRow", targets: ["RemindersListRow"]), 29 | 30 | .library(name: "SharedModels", targets: ["SharedModels"]), 31 | 32 | .library(name: "UIApplicationClient", targets: ["UIApplicationClient"]), 33 | .library(name: "UIApplicationClientLive", targets: ["UIApplicationClientLive"]), 34 | 35 | .library(name: "UserNotificationClient", targets: ["UserNotificationClient"]), 36 | .library(name: "UserNotificationClientLive", targets: ["UserNotificationClientLive"]), 37 | 38 | .library(name: "WatchOSApp", targets: ["WatchOSApp"]), 39 | 40 | .library(name: "WatchRemindersListRow", targets: ["WatchRemindersListRow"]), 41 | ], 42 | dependencies: [ 43 | .package(url: "https://github.com/pointfreeco/swift-composable-architecture.git", from: "0.27.1") 44 | ], 45 | targets: [ 46 | .target( 47 | name: "AppCore", 48 | dependencies: [ 49 | .product(name: "ComposableArchitecture", package: "swift-composable-architecture"), 50 | "AppDelegateCore", 51 | "RemindersListCore" 52 | ] 53 | ), 54 | .target( 55 | name: "App", 56 | dependencies: [ 57 | "AppCore", 58 | "NotificationCenterClientLive", 59 | "RemindersList", 60 | "UIApplicationClientLive", 61 | "UserNotificationClientLive" 62 | ] 63 | ), 64 | 65 | .target( 66 | name: "AppDelegateCore", 67 | dependencies: [ 68 | .product(name: "ComposableArchitecture", package: "swift-composable-architecture"), 69 | "UserNotificationClient" 70 | ] 71 | ), 72 | 73 | .target( 74 | name: "NotificationCenterClient", 75 | dependencies: [ 76 | .product(name: "ComposableArchitecture", package: "swift-composable-architecture") 77 | ] 78 | ), 79 | .target( 80 | name: "NotificationCenterClientLive", 81 | dependencies: ["NotificationCenterClient"] 82 | ), 83 | 84 | .target( 85 | name: "ReminderDetailCore", 86 | dependencies: [ 87 | .product(name: "ComposableArchitecture", package: "swift-composable-architecture"), 88 | "NotificationCenterClient", 89 | "SharedModels", 90 | "UIApplicationClient", 91 | "UserNotificationClient" 92 | ] 93 | ), 94 | .testTarget( 95 | name: "ReminderDetailCoreTests", 96 | dependencies: ["ReminderDetailCore"] 97 | ), 98 | .target( 99 | name: "ReminderDetail", 100 | dependencies: ["ReminderDetailCore"] 101 | ), 102 | 103 | .target( 104 | name: "RemindersListCore", 105 | dependencies: [ 106 | .product(name: "ComposableArchitecture", package: "swift-composable-architecture"), 107 | "SharedModels", 108 | "ReminderDetailCore", 109 | "RemindersListRowCore" 110 | ] 111 | ), 112 | .testTarget( 113 | name: "RemindersListCoreTests", 114 | dependencies: ["RemindersListCore"] 115 | ), 116 | .target( 117 | name: "RemindersList", 118 | dependencies: ["ReminderDetail", "RemindersListCore", "RemindersListRow"] 119 | ), 120 | 121 | .target( 122 | name: "RemindersListRowCore", 123 | dependencies: [ 124 | .product(name: "ComposableArchitecture", package: "swift-composable-architecture"), 125 | "SharedModels", 126 | "UserNotificationClient" 127 | ] 128 | ), 129 | .testTarget( 130 | name: "RemindersListRowCoreTests", 131 | dependencies: ["RemindersListRowCore"] 132 | ), 133 | .target( 134 | name: "RemindersListRow", 135 | dependencies: ["RemindersListRowCore"] 136 | ), 137 | 138 | .target( 139 | name: "SharedModels", 140 | dependencies: [] 141 | ), 142 | 143 | .target( 144 | name: "UIApplicationClient", 145 | dependencies: [ 146 | .product(name: "ComposableArchitecture", package: "swift-composable-architecture") 147 | ] 148 | ), 149 | .target( 150 | name: "UIApplicationClientLive", 151 | dependencies: ["UIApplicationClient"] 152 | ), 153 | 154 | .target( 155 | name: "UserNotificationClient", 156 | dependencies: [ 157 | .product(name: "ComposableArchitecture", package: "swift-composable-architecture") 158 | ] 159 | ), 160 | .target( 161 | name: "UserNotificationClientLive", 162 | dependencies: ["UserNotificationClient"] 163 | ), 164 | 165 | .target(name: "WatchOSApp", dependencies: [ 166 | "AppCore", 167 | "WatchRemindersListRow", 168 | "UserNotificationClientLive" 169 | ]), 170 | 171 | .target(name: "WatchRemindersListRow", dependencies: ["RemindersListRowCore"]) 172 | ] 173 | ) 174 | -------------------------------------------------------------------------------- /RemindersPackage/README.md: -------------------------------------------------------------------------------- 1 | # RemindersPackage 2 | 3 | The core logic of the application is put into modules named like `*Core`, and they are kept separate from modules containing UI, which is what allow us to share code across iOS and watchOS but also it could work for macOS and tvOS apps. 4 | -------------------------------------------------------------------------------- /RemindersPackage/Sources/App/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import AppCore 2 | import AppDelegateCore 3 | import ComposableArchitecture 4 | import NotificationCenterClientLive 5 | import RemindersListCore 6 | import SharedModels 7 | import SwiftUI 8 | import UIApplicationClientLive 9 | import UserNotificationClientLive 10 | 11 | public final class AppDelegate: NSObject, UIApplicationDelegate { 12 | public let store = Store( 13 | initialState: AppState( 14 | appDelegateState: AppDelegateState(), 15 | remindersListState: RemindersListState( 16 | list: [Reminder(id: UUID(), title: "Buy fruit", notes: "Bananas", isCompleted: false, date: Date())], 17 | detailState: nil 18 | ) 19 | ), 20 | reducer: appReducer, 21 | environment: .live 22 | ) 23 | 24 | private(set) lazy var viewStore = ViewStore( 25 | self.store.scope(state: { _ in () }), 26 | removeDuplicates: == 27 | ) 28 | 29 | public func application( 30 | _ application: UIApplication, 31 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil 32 | ) -> Bool { 33 | self.viewStore.send(.appDelegate(.didFinishLaunching)) 34 | return true 35 | } 36 | } 37 | 38 | extension AppEnvironment { 39 | static var live: Self { 40 | AppEnvironment( 41 | userNotifications: .live, 42 | applicationClient: .live, 43 | notificationCenterClient: .live, 44 | mainQueue: DispatchQueue.main.eraseToAnyScheduler() 45 | ) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /RemindersPackage/Sources/App/AppView.swift: -------------------------------------------------------------------------------- 1 | import AppCore 2 | import ComposableArchitecture 3 | import RemindersList 4 | import SwiftUI 5 | 6 | public struct RemindersAppView: View { 7 | let store: Store 8 | 9 | public init(store: Store) { 10 | self.store = store 11 | } 12 | 13 | public var body: some View { 14 | WithViewStore(store) { viewStore in 15 | RemindersList(store: store.scope(state: \.remindersListState, action: AppAction.remindersList)) 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /RemindersPackage/Sources/AppCore/AppCore.swift: -------------------------------------------------------------------------------- 1 | import AppDelegateCore 2 | import ComposableArchitecture 3 | import NotificationCenterClient 4 | import RemindersListCore 5 | import ReminderDetailCore 6 | import UIApplicationClient 7 | import UserNotificationClient 8 | 9 | public struct AppState: Equatable { 10 | public var appDelegateState: AppDelegateState 11 | public var remindersListState: RemindersListState 12 | 13 | public init(appDelegateState: AppDelegateState, remindersListState: RemindersListState) { 14 | self.appDelegateState = appDelegateState 15 | self.remindersListState = remindersListState 16 | } 17 | } 18 | 19 | public enum AppAction: Equatable { 20 | case appDelegate(AppDelegateAction) 21 | case remindersList(RemindersListAction) 22 | } 23 | 24 | public struct AppEnvironment { 25 | var userNotifications: UserNotificationClient 26 | var applicationClient: UIApplicationClient 27 | var notificationCenterClient: NotificationCenterClient 28 | var mainQueue: AnySchedulerOf 29 | 30 | public init( 31 | userNotifications: UserNotificationClient, 32 | applicationClient: UIApplicationClient, 33 | notificationCenterClient: NotificationCenterClient, 34 | mainQueue: AnySchedulerOf 35 | ) { 36 | self.userNotifications = userNotifications 37 | self.applicationClient = applicationClient 38 | self.notificationCenterClient = notificationCenterClient 39 | self.mainQueue = mainQueue 40 | } 41 | } 42 | 43 | public let appReducer = Reducer.combine( 44 | remindersListReducer.pullback( 45 | state: \.remindersListState, 46 | action: /AppAction.remindersList, 47 | environment: { environment in 48 | RemindersListEnvironment( 49 | uuid: UUID.init, 50 | userNotifications: environment.userNotifications, 51 | applicationClient: environment.applicationClient, 52 | notificationCenterClient: environment.notificationCenterClient, 53 | mainQueue: environment.mainQueue 54 | ) 55 | } 56 | ), 57 | appDelegateReducer.pullback( 58 | state: \.appDelegateState, 59 | action: /AppAction.appDelegate, 60 | environment: { 61 | AppDelegateEnvironment(userNotifications: $0.userNotifications) 62 | } 63 | ), 64 | Reducer { state, action, _ in 65 | switch action { 66 | case let .appDelegate(.userTapOnNotification(identifier)): 67 | guard 68 | let reminderIdentifier = UUID(uuidString: identifier), 69 | let reminder = state.remindersListState.list[id: reminderIdentifier] 70 | else { 71 | return .none 72 | } 73 | state.remindersListState.detailState = .init(ReminderDetailState(initialReminder: reminder), id: reminder.id) 74 | return .none 75 | case .remindersList, .appDelegate: 76 | return .none 77 | } 78 | } 79 | ) 80 | -------------------------------------------------------------------------------- /RemindersPackage/Sources/AppDelegateCore/AppDelegateCore.swift: -------------------------------------------------------------------------------- 1 | import ComposableArchitecture 2 | import UserNotifications 3 | import UserNotificationClient 4 | 5 | public struct AppDelegateState: Equatable { 6 | public init() { } 7 | } 8 | 9 | public enum AppDelegateAction: Equatable { 10 | case didFinishLaunching 11 | case userNotifications(UserNotificationClient.DelegateEvent) 12 | case userTapOnNotification(identifier: String) 13 | } 14 | 15 | public struct AppDelegateEnvironment { 16 | var userNotifications: UserNotificationClient 17 | 18 | public init(userNotifications: UserNotificationClient) { 19 | self.userNotifications = userNotifications 20 | } 21 | } 22 | 23 | public let appDelegateReducer = Reducer { state, action, environment in 24 | switch action { 25 | case .didFinishLaunching: 26 | return environment.userNotifications.delegate 27 | .map(AppDelegateAction.userNotifications) 28 | case let .userNotifications(.willPresentNotification(_, completionHandler)): 29 | return .fireAndForget { 30 | completionHandler(.banner) 31 | } 32 | case let .userNotifications(.didReceiveResponse(response, completionHandler)): 33 | completionHandler() 34 | return Effect(value: .userTapOnNotification(identifier: response.notification.request.identifier)) 35 | case .userNotifications: 36 | return .none 37 | case .userTapOnNotification: 38 | return .none 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /RemindersPackage/Sources/AppDelegateCore/Environment/AppDelegateEnvironment+Mocks.swift: -------------------------------------------------------------------------------- 1 | #if DEBUG 2 | import UserNotificationClient 3 | 4 | public extension AppDelegateEnvironment { 5 | static let failing = Self( 6 | userNotifications: .failing 7 | ) 8 | } 9 | #endif 10 | -------------------------------------------------------------------------------- /RemindersPackage/Sources/NotificationCenterClient/NotificationCenterClient+Mocks.swift: -------------------------------------------------------------------------------- 1 | import ComposableArchitecture 2 | import XCTestDynamicOverlay 3 | 4 | extension NotificationCenterClient { 5 | #if DEBUG 6 | public static let failing = Self( 7 | publisher: { _ in .failing("\(Self.self).publisher is unimplemented") } 8 | ) 9 | #endif 10 | 11 | public static let noop = Self( 12 | publisher: { _ in .none } 13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /RemindersPackage/Sources/NotificationCenterClient/NotificationCenterClient.swift: -------------------------------------------------------------------------------- 1 | import ComposableArchitecture 2 | 3 | public struct NotificationCenterClient { 4 | public var publisher: (Notification) -> Effect 5 | 6 | public init(publisher: @escaping (Notification) -> Effect) { 7 | self.publisher = publisher 8 | } 9 | } 10 | 11 | extension NotificationCenterClient { 12 | public enum Notification { 13 | case didBecomeActive 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /RemindersPackage/Sources/NotificationCenterClientLive/NotificationCenterClient+Live.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import NotificationCenterClient 3 | 4 | extension NotificationCenterClient { 5 | public static let live = Self( 6 | publisher: { notification in 7 | NotificationCenter.default.publisher(for: UIKit.Notification.Name(notification: notification)) 8 | .map { _ in () } 9 | .eraseToEffect() 10 | } 11 | ) 12 | } 13 | 14 | // MARK: - 15 | 16 | extension Notification.Name { 17 | init(notification: NotificationCenterClient.Notification) { 18 | switch notification { 19 | case .didBecomeActive: 20 | self = UIApplication.didBecomeActiveNotification 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /RemindersPackage/Sources/ReminderDetail/IconToggleRow.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct IconToggleRow: View { 4 | let icon: String 5 | let label: String 6 | let toggleBinding: Binding 7 | 8 | public var body: some View { 9 | HStack { 10 | Label { 11 | Text(label) 12 | } icon: { 13 | Text(icon) 14 | } 15 | Spacer() 16 | Toggle("", isOn: toggleBinding) 17 | } 18 | .padding(4) 19 | } 20 | } 21 | 22 | struct IconToggleRow_Previews: PreviewProvider { 23 | static var previews: some View { 24 | IconToggleRow(icon: "🗓", label: "Date", toggleBinding: .constant(true)) 25 | .previewLayout(.sizeThatFits) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /RemindersPackage/Sources/ReminderDetail/ReminderDetail.swift: -------------------------------------------------------------------------------- 1 | import ComposableArchitecture 2 | import ReminderDetailCore 3 | import SharedModels 4 | import SwiftUI 5 | import UIApplicationClient 6 | import UserNotificationClient 7 | 8 | public struct ReminderDetail: View { 9 | public let store: Store 10 | 11 | public init(store: Store) { 12 | self.store = store 13 | } 14 | 15 | public var body: some View { 16 | WithViewStore(store) { viewStore in 17 | NavigationView { 18 | Form { 19 | Section { 20 | TextField( 21 | "Title", 22 | text: viewStore.binding( 23 | get: \.currentReminder.title, 24 | send: ReminderDetailAction.titleTextFieldChanged 25 | ) 26 | ) 27 | 28 | TextField( 29 | "Notes", 30 | text: viewStore.binding( 31 | get: \.currentReminder.notes, 32 | send: ReminderDetailAction.notesTextFieldChanged 33 | ) 34 | ) 35 | } 36 | 37 | Section { 38 | IconToggleRow( 39 | icon: "🗓", 40 | label: "Date", 41 | toggleBinding: viewStore.binding( 42 | get: \.isDateToggleOn, 43 | send: ReminderDetailAction.toggleDateField 44 | ) 45 | ) 46 | if viewStore.isDateToggleOn { 47 | DatePicker( 48 | "Start Date", 49 | selection: viewStore.binding( 50 | get: \.currentReminder.displayedDate, 51 | send: ReminderDetailAction.dateChanged 52 | ), 53 | in: Date()... 54 | ) 55 | .datePickerStyle(GraphicalDatePickerStyle()) 56 | } 57 | } 58 | } 59 | .actionSheet( 60 | store.scope(state: \.cancelSheet), 61 | dismiss: .cancelSheetDismissed 62 | ) 63 | .alert( 64 | store.scope(state: \.alert), 65 | dismiss: .cancelAlertDismissed 66 | ) 67 | .navigationTitle("Details") 68 | .navigationBarItems( 69 | leading: Button("Cancel") { viewStore.send(.cancelButtonTap) }, 70 | trailing: Button("Done") { viewStore.send(.doneButtonTap) } 71 | ) 72 | } 73 | .onAppear(perform: { viewStore.send(.onAppear) }) 74 | } 75 | } 76 | } 77 | 78 | struct ReminderDetail_Previews: PreviewProvider { 79 | static var previews: some View { 80 | let store = Store( 81 | initialState: ReminderDetailState( 82 | initialReminder: Reminder(id: UUID(), title: "Buy groceries", notes: "", isCompleted: false) 83 | ), 84 | reducer: reminderDetailReducer, 85 | environment: ReminderDetailEnvironment( 86 | currentDate: Date.init, 87 | userNotificationClient: .noop, 88 | applicationClient: .noop, 89 | notificationCenterClient: .noop, 90 | mainQueue: .main 91 | ) 92 | ) 93 | ReminderDetail(store: store) 94 | } 95 | } 96 | 97 | private extension Reminder { 98 | var displayedDate: Date { 99 | date ?? Date() 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /RemindersPackage/Sources/ReminderDetailCore/Environment/ReminderDetailEnvironment+Mocks.swift: -------------------------------------------------------------------------------- 1 | #if DEBUG 2 | import XCTestDynamicOverlay 3 | import Foundation 4 | import UIApplicationClient 5 | import NotificationCenterClient 6 | import UserNotificationClient 7 | 8 | extension ReminderDetailEnvironment { 9 | public static let failing = Self( 10 | currentDate: { Date() }, 11 | userNotificationClient: .failing, 12 | applicationClient: .failing, 13 | notificationCenterClient: .failing, 14 | mainQueue: .failing 15 | ) 16 | } 17 | #endif 18 | -------------------------------------------------------------------------------- /RemindersPackage/Sources/ReminderDetailCore/ReminderDetailCore.swift: -------------------------------------------------------------------------------- 1 | import ComposableArchitecture 2 | import NotificationCenterClient 3 | import SharedModels 4 | import UIApplicationClient 5 | import UserNotificationClient 6 | 7 | public struct ReminderDetailState: Equatable { 8 | public let initialReminder: Reminder 9 | public var currentReminder: Reminder 10 | public var cancelSheet: ActionSheetState? 11 | public var alert: AlertState? 12 | public var isDateToggleOn: Bool 13 | 14 | public var userNotificationSettings: UserNotificationClient.Notification.Settings? 15 | 16 | public init(initialReminder: Reminder) { 17 | self.initialReminder = initialReminder 18 | self.currentReminder = initialReminder 19 | self.isDateToggleOn = initialReminder.date != nil 20 | } 21 | } 22 | 23 | public enum ReminderDetailAction: Equatable { 24 | case onAppear 25 | case didBecomeActive 26 | case titleTextFieldChanged(String) 27 | case notesTextFieldChanged(String) 28 | case dateChanged(Date) 29 | case toggleDateField(Bool) 30 | case cancelButtonTap 31 | case cancelSheetDismissed 32 | case dontSaveChangesButtonTap 33 | 34 | case cancelAlertDismissed 35 | case openNotificationSettings 36 | 37 | case doneButtonTap 38 | case closeDetail 39 | case closeAndSaveDetail 40 | 41 | case addNotificationRequest(Date) 42 | case userNotificationSettingsResponse(UserNotificationClient.Notification.Settings) 43 | case userNotificationAuthorizationResponse(result: Result, date: Date) 44 | } 45 | 46 | public struct ReminderDetailEnvironment { 47 | var currentDate: () -> Date 48 | var userNotificationClient: UserNotificationClient 49 | var applicationClient: UIApplicationClient 50 | var notificationCenterClient: NotificationCenterClient 51 | var mainQueue: AnySchedulerOf 52 | 53 | public init( 54 | currentDate: @escaping () -> Date, 55 | userNotificationClient: UserNotificationClient, 56 | applicationClient: UIApplicationClient, 57 | notificationCenterClient: NotificationCenterClient, 58 | mainQueue: AnySchedulerOf 59 | ) { 60 | self.currentDate = currentDate 61 | self.userNotificationClient = userNotificationClient 62 | self.applicationClient = applicationClient 63 | self.notificationCenterClient = notificationCenterClient 64 | self.mainQueue = mainQueue 65 | } 66 | } 67 | 68 | public let reminderDetailReducer = Reducer { state, action, environment in 69 | struct DidBecomeActiveId: Hashable { } 70 | 71 | switch action { 72 | case .onAppear: 73 | return .merge( 74 | environment.userNotificationClient.getNotificationSettings 75 | .receive(on: environment.mainQueue) 76 | .eraseToEffect() 77 | .map(ReminderDetailAction.userNotificationSettingsResponse), 78 | environment.notificationCenterClient.publisher(.didBecomeActive) 79 | .map { _ in .didBecomeActive } 80 | .eraseToEffect() 81 | .cancellable(id: DidBecomeActiveId()) 82 | ) 83 | case .didBecomeActive: 84 | return environment.userNotificationClient.getNotificationSettings 85 | .receive(on: environment.mainQueue) 86 | .eraseToEffect() 87 | .map(ReminderDetailAction.userNotificationSettingsResponse) 88 | case let .titleTextFieldChanged(text): 89 | state.currentReminder.title = text 90 | return .none 91 | case let .notesTextFieldChanged(text): 92 | state.currentReminder.notes = text 93 | return .none 94 | case let .dateChanged(date): 95 | state.currentReminder.date = date 96 | return .none 97 | case let .toggleDateField(isOn): 98 | state.isDateToggleOn = isOn 99 | state.currentReminder.date = isOn ? environment.currentDate() : nil 100 | return .none 101 | case .cancelButtonTap: 102 | if state.initialReminder == state.currentReminder { 103 | return Effect(value: .closeDetail) 104 | } else { 105 | state.cancelSheet = ActionSheetState( 106 | title: TextState("Confirmation"), 107 | buttons: [ 108 | .cancel(), 109 | .destructive(TextState("Don't save changes"), action: .send(.dontSaveChangesButtonTap)) 110 | ] 111 | ) 112 | } 113 | return .none 114 | case .cancelSheetDismissed: 115 | state.cancelSheet = nil 116 | return .none 117 | case .dontSaveChangesButtonTap: 118 | return Effect(value: .closeDetail) 119 | case .cancelAlertDismissed: 120 | state.alert = nil 121 | return .none 122 | case .openNotificationSettings: 123 | guard let settingsURL = URL(string: environment.applicationClient.openSettingsURLString()) else { return .none } 124 | return environment.applicationClient.open(settingsURL).fireAndForget() 125 | case .doneButtonTap: 126 | guard 127 | let date = state.currentReminder.date, 128 | let userNotificationSettings = state.userNotificationSettings 129 | else { 130 | return Effect(value: .closeAndSaveDetail) 131 | } 132 | 133 | switch userNotificationSettings.authorizationStatus { 134 | case .authorized, .provisional: 135 | return Effect(value: .addNotificationRequest(date)) 136 | case .denied, .ephemeral: 137 | state.alert = AlertState( 138 | title: TextState("Enable push notifications"), 139 | primaryButton: .default(TextState("Go to settings"), action: .send(.openNotificationSettings)), 140 | secondaryButton: .cancel() 141 | ) 142 | return .none 143 | case .notDetermined: 144 | return environment.userNotificationClient.requestAuthorization([.alert, .badge, .sound]) 145 | .mapError { $0 as NSError } 146 | .receive(on: environment.mainQueue) 147 | .catchToEffect() 148 | .map { ReminderDetailAction.userNotificationAuthorizationResponse(result: $0, date: date) } 149 | @unknown default: 150 | return .none 151 | } 152 | case .closeDetail, .closeAndSaveDetail: 153 | return .cancel(id: DidBecomeActiveId()) 154 | case let .addNotificationRequest(date): 155 | return .concatenate( 156 | environment.userNotificationClient.add(state.currentReminder.makeNotificationRequest(scheduleDate: date)) 157 | .fireAndForget(), 158 | Effect(value: .closeAndSaveDetail) 159 | .receive(on: environment.mainQueue) 160 | .eraseToEffect() 161 | ) 162 | case let .userNotificationSettingsResponse(settings): 163 | state.userNotificationSettings = settings 164 | return .none 165 | case let .userNotificationAuthorizationResponse(result, date): 166 | let isGranted = (try? result.get()) ?? false 167 | return isGranted ? Effect(value: .addNotificationRequest(date)) : .none 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /RemindersPackage/Sources/RemindersList/RemindersList.swift: -------------------------------------------------------------------------------- 1 | import ComposableArchitecture 2 | import ReminderDetail 3 | import RemindersListRow 4 | import RemindersListCore 5 | import SharedModels 6 | import SwiftUI 7 | 8 | public struct RemindersList: View { 9 | let store: Store 10 | 11 | public init(store: Store) { 12 | self.store = store 13 | } 14 | 15 | public var body: some View { 16 | WithViewStore(store) { viewStore in 17 | NavigationView { 18 | List { 19 | ForEachStore( 20 | store.scope(state: \.list, action: RemindersListAction.reminderRow), 21 | content: RemindersListRow.init(store:) 22 | ) 23 | } 24 | .navigationTitle("Reminders") 25 | .navigationBarItems(trailing: Button("Add") { viewStore.send(.addButtonTap) }) 26 | .sheet( 27 | item: viewStore.binding(get: \.detailState, send: RemindersListAction.sheetSelection), 28 | content: { reminder in 29 | IfLetStore( 30 | store.scope(state: \.detailState?.value, action: RemindersListAction.reminderDetail), 31 | then: ReminderDetail.init(store:), 32 | else: { Text("") } 33 | ) 34 | } 35 | ) 36 | } 37 | .navigationViewStyle(StackNavigationViewStyle()) 38 | } 39 | } 40 | } 41 | 42 | struct RemindersList_Previews: PreviewProvider { 43 | static var previews: some View { 44 | let store = Store( 45 | initialState: RemindersListState( 46 | list: [ 47 | Reminder(id: UUID(), title: "", notes: "", isCompleted: false), 48 | Reminder(id: UUID(), title: "Buy groceries", notes: "Also banana", isCompleted: false), 49 | ] 50 | ), 51 | reducer: remindersListReducer, 52 | environment: RemindersListEnvironment( 53 | uuid: UUID.init, 54 | userNotifications: .noop, 55 | applicationClient: .noop, 56 | notificationCenterClient: .noop, 57 | mainQueue: DispatchQueue.main.eraseToAnyScheduler() 58 | ) 59 | ) 60 | RemindersList(store: store) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /RemindersPackage/Sources/RemindersListCore/RemindersListCore.swift: -------------------------------------------------------------------------------- 1 | import ComposableArchitecture 2 | import NotificationCenterClient 3 | import ReminderDetailCore 4 | import RemindersListRowCore 5 | import SharedModels 6 | import UIApplicationClient 7 | import UserNotificationClient 8 | 9 | public struct RemindersListState: Equatable { 10 | public var list: IdentifiedArrayOf 11 | public var detailState: Identified? 12 | 13 | public init(list: IdentifiedArrayOf, detailState: Identified? = nil) { 14 | self.list = list 15 | self.detailState = detailState 16 | } 17 | 18 | } 19 | 20 | public enum RemindersListAction: Equatable { 21 | case addButtonTap 22 | case reminderRow(id: UUID, action: RemindersListRowAction) 23 | case sheetSelection(Identified?) 24 | case reminderDetail(ReminderDetailAction) 25 | case todoDelayCompleted 26 | 27 | } 28 | 29 | public struct RemindersListEnvironment { 30 | var uuid: () -> UUID 31 | var userNotifications: UserNotificationClient 32 | var applicationClient: UIApplicationClient 33 | var notificationCenterClient: NotificationCenterClient 34 | var mainQueue: AnySchedulerOf 35 | 36 | public init( 37 | uuid: @escaping () -> UUID, 38 | userNotifications: UserNotificationClient, 39 | applicationClient: UIApplicationClient, 40 | notificationCenterClient: NotificationCenterClient, 41 | mainQueue: AnySchedulerOf 42 | ) { 43 | self.uuid = uuid 44 | self.userNotifications = userNotifications 45 | self.applicationClient = applicationClient 46 | self.notificationCenterClient = notificationCenterClient 47 | self.mainQueue = mainQueue 48 | } 49 | } 50 | 51 | public let remindersListReducer = Reducer.combine( 52 | remindersListRowReducer.forEach( 53 | state: \.list, 54 | action: /RemindersListAction.reminderRow, 55 | environment: { environment in 56 | RemindersListRowEnvironment(userNotificationClient: environment.userNotifications) 57 | } 58 | ), 59 | 60 | reminderDetailReducer 61 | .pullback(state: \Identified.value, action: .self, environment: { $0 }) 62 | .optional() 63 | .pullback( 64 | state: \.detailState, 65 | action: /RemindersListAction.reminderDetail, 66 | environment: { environment in ReminderDetailEnvironment( 67 | currentDate: Date.init, 68 | userNotificationClient: environment.userNotifications, 69 | applicationClient: environment.applicationClient, 70 | notificationCenterClient: environment.notificationCenterClient, 71 | mainQueue: environment.mainQueue 72 | ) } 73 | ), 74 | 75 | Reducer { state, action, environment in 76 | switch action { 77 | case .addButtonTap: 78 | state.list.insert(Reminder(id: environment.uuid()), at: 0) 79 | return .none 80 | case let .reminderRow(id, .infoButtonTap): 81 | state.list[id: id].map { 82 | state.detailState = Identified(ReminderDetailState(initialReminder: $0), id: $0.id) 83 | } 84 | return .none 85 | case let .reminderRow(id, action: .checkboxTap): 86 | struct CancelDelayId: Hashable {} 87 | 88 | return Effect(value: .todoDelayCompleted) 89 | .debounce(id: CancelDelayId(), for: 1, scheduler: environment.mainQueue.animation()) 90 | case let .sheetSelection(.some(identified)): 91 | state.detailState = identified 92 | return .none 93 | case .sheetSelection(.none): 94 | state.detailState = nil 95 | return .none 96 | case .reminderRow: 97 | return .none 98 | case .todoDelayCompleted: 99 | let sortedList = state.list 100 | .enumerated() 101 | .sorted { lhs, rhs in 102 | (!lhs.element.isCompleted && rhs.element.isCompleted) || lhs.offset < rhs.offset 103 | } 104 | .map(\.element) 105 | state.list = IdentifiedArrayOf(uniqueElements: sortedList) 106 | return .none 107 | case .reminderDetail(.closeAndSaveDetail): 108 | state.detailState.map { state.list[id: $0.id] = $0.currentReminder } 109 | state.detailState = nil 110 | return .none 111 | case .reminderDetail(.closeDetail): 112 | state.detailState = nil 113 | return .none 114 | case .reminderDetail: 115 | return .none 116 | } 117 | } 118 | ) 119 | -------------------------------------------------------------------------------- /RemindersPackage/Sources/RemindersListRow/RemindersListRow.swift: -------------------------------------------------------------------------------- 1 | import ComposableArchitecture 2 | import RemindersListRowCore 3 | import SharedModels 4 | import UserNotificationClient 5 | import SwiftUI 6 | 7 | public struct RemindersListRow: View { 8 | let store: Store 9 | @State private var isTextFieldFocused = false 10 | 11 | public init(store: Store) { 12 | self.store = store 13 | } 14 | 15 | public var body: some View { 16 | WithViewStore(store) { viewStore in 17 | HStack(alignment: .firstTextBaseline) { 18 | Button(action: { viewStore.send(.checkboxTap) }) { 19 | Image(systemName: viewStore.isCompleted ? "checkmark.square" : "square") 20 | } 21 | .buttonStyle(PlainButtonStyle()) 22 | 23 | VStack(alignment: .leading, spacing: 4) { 24 | TextField( 25 | "Untitled", 26 | text: viewStore.binding(get: \.title, send: RemindersListRowAction.textFieldChanged), 27 | onEditingChanged: { isTextFieldFocused = $0 } 28 | ) 29 | 30 | if !viewStore.notes.isEmpty { 31 | Text(viewStore.notes) 32 | .font(.subheadline) 33 | .foregroundColor(.gray) 34 | } 35 | 36 | if let date = viewStore.date { 37 | HStack { 38 | Text(date, style: .date) 39 | Text(date, style: .time) 40 | } 41 | .font(.subheadline) 42 | .foregroundColor(.gray) 43 | } 44 | } 45 | 46 | Button(action: { viewStore.send(.infoButtonTap) }) { 47 | Image(systemName: "info") 48 | .padding(6) 49 | .foregroundColor(Color.white) 50 | .background(Color.blue) 51 | .clipShape(Circle()) 52 | } 53 | .buttonStyle(BorderlessButtonStyle()) 54 | .opacity(isTextFieldFocused ? 1.0 : 0.0) 55 | } 56 | .foregroundColor(viewStore.isCompleted ? .gray : nil) 57 | } 58 | } 59 | } 60 | 61 | struct RemindersListRow_Previews: PreviewProvider { 62 | static var previews: some View { 63 | let store = Store( 64 | initialState: Reminder( 65 | id: UUID(), 66 | title: "Buy groceries", 67 | notes: "Remember banana", 68 | isCompleted: false, 69 | date: Date() 70 | ), 71 | reducer: remindersListRowReducer, 72 | environment: RemindersListRowEnvironment(userNotificationClient: .noop) 73 | ) 74 | RemindersListRow(store: store) 75 | .previewLayout(.sizeThatFits) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /RemindersPackage/Sources/RemindersListRowCore/Environment/ReminderListRowEnvironment+Mocks.swift: -------------------------------------------------------------------------------- 1 | #if DEBUG 2 | import XCTestDynamicOverlay 3 | import Foundation 4 | 5 | extension RemindersListRowEnvironment { 6 | public static let failing = Self( 7 | userNotificationClient: .failing 8 | ) 9 | } 10 | #endif 11 | -------------------------------------------------------------------------------- /RemindersPackage/Sources/RemindersListRowCore/RemindersListRowCore.swift: -------------------------------------------------------------------------------- 1 | import ComposableArchitecture 2 | import SharedModels 3 | import UserNotificationClient 4 | 5 | public enum RemindersListRowAction: Equatable { 6 | case checkboxTap 7 | case textFieldChanged(String) 8 | case infoButtonTap 9 | } 10 | 11 | public struct RemindersListRowEnvironment { 12 | var userNotificationClient: UserNotificationClient 13 | 14 | public init(userNotificationClient: UserNotificationClient) { 15 | self.userNotificationClient = userNotificationClient 16 | } 17 | 18 | } 19 | 20 | public let remindersListRowReducer = Reducer { state, action, environment in 21 | switch action { 22 | case .checkboxTap: 23 | state.isCompleted.toggle() 24 | 25 | switch state.date { 26 | case let .some(date) where state.isCompleted: 27 | return environment.userNotificationClient.removePendingNotificationRequestsWithIdentifiers([state.id.uuidString]) 28 | .fireAndForget() 29 | case let .some(date) where !state.isCompleted: 30 | return environment.userNotificationClient.add(state.makeNotificationRequest(scheduleDate: date)) 31 | .fireAndForget() 32 | default: 33 | return .none 34 | } 35 | 36 | case let .textFieldChanged(text): 37 | state.title = text 38 | return .none 39 | case .infoButtonTap: 40 | return .none 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /RemindersPackage/Sources/SharedModels/Reminder+UNNotificationRequest.swift: -------------------------------------------------------------------------------- 1 | import UserNotifications 2 | 3 | public extension Reminder { 4 | func makeNotificationRequest(scheduleDate: Date) -> UNNotificationRequest { 5 | let content = UNMutableNotificationContent() 6 | content.title = title 7 | content.body = notes 8 | 9 | let trigger = UNCalendarNotificationTrigger( 10 | dateMatching: Calendar.current.dateComponents([.year, .month, .day, .hour, .minute], from: scheduleDate), 11 | repeats: false 12 | ) 13 | 14 | return UNNotificationRequest( 15 | identifier: id.uuidString, 16 | content: content, 17 | trigger: trigger 18 | ) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /RemindersPackage/Sources/SharedModels/Reminder.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct Reminder: Equatable, Identifiable { 4 | public let id: UUID 5 | public var title = "" 6 | public var notes = "" 7 | public var isCompleted = false 8 | public var date: Date? = nil 9 | 10 | public init(id: UUID, title: String = "", notes: String = "", isCompleted: Bool = false, date: Date? = nil) { 11 | self.id = id 12 | self.title = title 13 | self.notes = notes 14 | self.isCompleted = isCompleted 15 | self.date = date 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /RemindersPackage/Sources/UIApplicationClient/UIApplicationClient+Mocks.swift: -------------------------------------------------------------------------------- 1 | import ComposableArchitecture 2 | import XCTestDynamicOverlay 3 | 4 | extension UIApplicationClient { 5 | #if DEBUG 6 | public static let failing = Self( 7 | open: { _ in .failing("\(Self.self).open is unimplemented") }, 8 | openSettingsURLString: { 9 | XCTFail("\(Self.self).openSettingsURLString is unimplemented") 10 | return "" 11 | } 12 | ) 13 | #endif 14 | 15 | public static let noop = Self( 16 | open: { _ in .none }, 17 | openSettingsURLString: { "settings://reminders/settings" } 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /RemindersPackage/Sources/UIApplicationClient/UIApplicationClient.swift: -------------------------------------------------------------------------------- 1 | import ComposableArchitecture 2 | 3 | public struct UIApplicationClient { 4 | public var open: (URL) -> Effect 5 | public var openSettingsURLString: () -> String 6 | 7 | public init( 8 | open: @escaping (URL) -> Effect, 9 | openSettingsURLString: @escaping () -> String 10 | ) { 11 | self.open = open 12 | self.openSettingsURLString = openSettingsURLString 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /RemindersPackage/Sources/UIApplicationClientLive/UIApplicationClient+Live.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import UIKit 3 | import UIApplicationClient 4 | 5 | extension UIApplicationClient { 6 | public static let live = Self( 7 | open: { url in 8 | .future { callback in 9 | UIApplication.shared.open(url, options: [:]) { bool in 10 | callback(.success(bool)) 11 | } 12 | } 13 | }, 14 | openSettingsURLString: { UIApplication.openSettingsURLString } 15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /RemindersPackage/Sources/UserNotificationClient/UserNotificationClient+Mocks.swift: -------------------------------------------------------------------------------- 1 | extension UserNotificationClient { 2 | public static let noop = Self( 3 | add: { _ in .none }, 4 | delegate: .none, 5 | getNotificationSettings: .none, 6 | removeDeliveredNotificationsWithIdentifiers: { _ in .none }, 7 | removePendingNotificationRequestsWithIdentifiers: { _ in .none }, 8 | requestAuthorization: { _ in .none } 9 | ) 10 | } 11 | 12 | #if DEBUG 13 | import XCTestDynamicOverlay 14 | 15 | extension UserNotificationClient { 16 | public static let failing = Self( 17 | add: { _ in .failing("\(Self.self).add is not implemented") }, 18 | delegate: .failing("\(Self.self).delegate is not implemented"), 19 | getNotificationSettings: .failing("\(Self.self).getNotificationSettings is not implemented"), 20 | removeDeliveredNotificationsWithIdentifiers: { _ in 21 | .failing("\(Self.self).removeDeliveredNotificationsWithIdentifiers is not implemented") 22 | }, 23 | removePendingNotificationRequestsWithIdentifiers: { _ in 24 | .failing("\(Self.self).removePendingNotificationRequestsWithIdentifiers is not implemented") 25 | }, 26 | requestAuthorization: { _ in .failing("\(Self.self).requestAuthorization is not implemented") 27 | } 28 | ) 29 | } 30 | #endif 31 | -------------------------------------------------------------------------------- /RemindersPackage/Sources/UserNotificationClient/UserNotificationClient.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import ComposableArchitecture 3 | import UserNotifications 4 | 5 | public struct UserNotificationClient { 6 | public var add: (UNNotificationRequest) -> Effect 7 | public var delegate: Effect 8 | public var getNotificationSettings: Effect 9 | public var removeDeliveredNotificationsWithIdentifiers: ([String]) -> Effect 10 | public var removePendingNotificationRequestsWithIdentifiers: ([String]) -> Effect 11 | public var requestAuthorization: (UNAuthorizationOptions) -> Effect 12 | 13 | public init( 14 | add: @escaping (UNNotificationRequest) -> Effect, 15 | delegate: Effect, 16 | getNotificationSettings: Effect, 17 | removeDeliveredNotificationsWithIdentifiers: @escaping ([String]) -> Effect, 18 | removePendingNotificationRequestsWithIdentifiers: @escaping ([String]) -> Effect, 19 | requestAuthorization: @escaping (UNAuthorizationOptions) -> Effect 20 | ) { 21 | self.add = add 22 | self.delegate = delegate 23 | self.getNotificationSettings = getNotificationSettings 24 | self.removeDeliveredNotificationsWithIdentifiers = removeDeliveredNotificationsWithIdentifiers 25 | self.removePendingNotificationRequestsWithIdentifiers = removePendingNotificationRequestsWithIdentifiers 26 | self.requestAuthorization = requestAuthorization 27 | } 28 | 29 | public enum DelegateEvent: Equatable { 30 | case didReceiveResponse(Notification.Response, completionHandler: () -> Void) 31 | case openSettingsForNotification(Notification?) 32 | case willPresentNotification(Notification, completionHandler: (UNNotificationPresentationOptions) -> Void) 33 | 34 | public static func == (lhs: Self, rhs: Self) -> Bool { 35 | switch (lhs, rhs) { 36 | case let (.didReceiveResponse(lhs, _), .didReceiveResponse(rhs, _)): 37 | return lhs == rhs 38 | case let (.openSettingsForNotification(lhs), .openSettingsForNotification(rhs)): 39 | return lhs == rhs 40 | case let (.willPresentNotification(lhs, _), .willPresentNotification(rhs, _)): 41 | return lhs == rhs 42 | default: 43 | return false 44 | } 45 | } 46 | } 47 | 48 | public struct Notification: Equatable { 49 | public var date: Date 50 | public var request: UNNotificationRequest 51 | 52 | public init( 53 | date: Date, 54 | request: UNNotificationRequest 55 | ) { 56 | self.date = date 57 | self.request = request 58 | } 59 | 60 | public struct Response: Equatable { 61 | public var notification: Notification 62 | 63 | public init(notification: Notification) { 64 | self.notification = notification 65 | } 66 | } 67 | 68 | public struct Settings: Equatable { 69 | public var authorizationStatus: UNAuthorizationStatus 70 | 71 | public init(authorizationStatus: UNAuthorizationStatus) { 72 | self.authorizationStatus = authorizationStatus 73 | } 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /RemindersPackage/Sources/UserNotificationClientLive/UserNotificationClient+Live.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import ComposableArchitecture 3 | import UserNotifications 4 | import UserNotificationClient 5 | 6 | extension UserNotificationClient { 7 | public static let live = Self( 8 | add: { request in 9 | .future { callback in 10 | UNUserNotificationCenter.current().add(request) { error in 11 | if let error = error { 12 | callback(.failure(error)) 13 | } else { 14 | callback(.success(())) 15 | } 16 | } 17 | } 18 | }, 19 | delegate: 20 | Effect 21 | .run { subscriber in 22 | var delegate: Optional = Delegate(subscriber: subscriber) 23 | UNUserNotificationCenter.current().delegate = delegate 24 | return AnyCancellable { 25 | delegate = nil 26 | } 27 | } 28 | .share() 29 | .eraseToEffect(), 30 | getNotificationSettings: .future { callback in 31 | UNUserNotificationCenter.current().getNotificationSettings { settings in 32 | callback(.success(.init(rawValue: settings))) 33 | } 34 | }, 35 | removeDeliveredNotificationsWithIdentifiers: { identifiers in 36 | .fireAndForget { 37 | UNUserNotificationCenter.current() 38 | .removeDeliveredNotifications(withIdentifiers: identifiers) 39 | } 40 | }, 41 | removePendingNotificationRequestsWithIdentifiers: { identifiers in 42 | .fireAndForget { 43 | UNUserNotificationCenter.current() 44 | .removePendingNotificationRequests(withIdentifiers: identifiers) 45 | } 46 | }, 47 | requestAuthorization: { options in 48 | .future { callback in 49 | UNUserNotificationCenter.current() 50 | .requestAuthorization(options: options) { granted, error in 51 | if let error = error { 52 | callback(.failure(error)) 53 | } else { 54 | callback(.success(granted)) 55 | } 56 | } 57 | } 58 | } 59 | ) 60 | } 61 | 62 | extension UserNotificationClient.Notification { 63 | public init(rawValue: UNNotification) { 64 | self.init(date: rawValue.date, request: rawValue.request) 65 | } 66 | } 67 | 68 | extension UserNotificationClient.Notification.Response { 69 | public init(rawValue: UNNotificationResponse) { 70 | self.init(notification: .init(rawValue: rawValue.notification)) 71 | } 72 | } 73 | 74 | extension UserNotificationClient.Notification.Settings { 75 | public init(rawValue: UNNotificationSettings) { 76 | self.init(authorizationStatus: rawValue.authorizationStatus) 77 | } 78 | } 79 | 80 | extension UserNotificationClient { 81 | fileprivate class Delegate: NSObject, UNUserNotificationCenterDelegate { 82 | let subscriber: Effect.Subscriber 83 | 84 | init(subscriber: Effect.Subscriber) { 85 | self.subscriber = subscriber 86 | } 87 | 88 | func userNotificationCenter( 89 | _ center: UNUserNotificationCenter, 90 | didReceive response: UNNotificationResponse, 91 | withCompletionHandler completionHandler: @escaping () -> Void 92 | ) { 93 | self.subscriber.send( 94 | .didReceiveResponse(.init(rawValue: response), completionHandler: completionHandler) 95 | ) 96 | } 97 | 98 | #if os(iOS) 99 | func userNotificationCenter( 100 | _ center: UNUserNotificationCenter, 101 | openSettingsFor notification: UNNotification? 102 | ) { 103 | self.subscriber.send( 104 | .openSettingsForNotification(notification.map(Notification.init(rawValue:))) 105 | ) 106 | } 107 | #endif 108 | 109 | func userNotificationCenter( 110 | _ center: UNUserNotificationCenter, 111 | willPresent notification: UNNotification, 112 | withCompletionHandler completionHandler: 113 | @escaping (UNNotificationPresentationOptions) -> Void 114 | ) { 115 | self.subscriber.send( 116 | .willPresentNotification( 117 | .init(rawValue: notification), 118 | completionHandler: completionHandler 119 | ) 120 | ) 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /RemindersPackage/Sources/WatchOSApp/WatchOSAppView.swift: -------------------------------------------------------------------------------- 1 | import AppCore 2 | import ComposableArchitecture 3 | import RemindersListCore 4 | import WatchRemindersListRow 5 | import SharedModels 6 | import SwiftUI 7 | 8 | public struct WatchOSAppView: View { 9 | let store: Store 10 | 11 | public init(store: Store) { 12 | self.store = store 13 | } 14 | 15 | public var body: some View { 16 | WithViewStore(store) { viewStore in 17 | RemindersList(store: store.scope(state: \.remindersListState, action: AppAction.remindersList)) 18 | } 19 | } 20 | } 21 | 22 | struct RemindersList: View { 23 | let store: Store 24 | 25 | var body: some View { 26 | WithViewStore(store) { viewStore in 27 | NavigationView { 28 | List { 29 | ForEachStore( 30 | store.scope(state: \.list, action: RemindersListAction.reminderRow), 31 | content: RemindersListRow.init(store:) 32 | ) 33 | } 34 | } 35 | .navigationViewStyle(StackNavigationViewStyle()) 36 | } 37 | } 38 | } 39 | 40 | struct RemindersList_Previews: PreviewProvider { 41 | static var previews: some View { 42 | let store = Store( 43 | initialState: RemindersListState( 44 | list: [ 45 | Reminder(id: UUID(), title: "Buy groceries", notes: "Also banana", isCompleted: false) 46 | ] 47 | ), 48 | reducer: remindersListReducer, 49 | environment: RemindersListEnvironment( 50 | uuid: UUID.init, 51 | userNotifications: .noop, 52 | applicationClient: .noop, 53 | notificationCenterClient: .noop, 54 | mainQueue: DispatchQueue.main.eraseToAnyScheduler() 55 | ) 56 | ) 57 | RemindersList(store: store) 58 | .previewDevice("Apple Watch Series 5 - 40mm") 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /RemindersPackage/Sources/WatchRemindersListRow/RemindersListRow.swift: -------------------------------------------------------------------------------- 1 | import ComposableArchitecture 2 | import RemindersListRowCore 3 | import SharedModels 4 | import UserNotificationClient 5 | import SwiftUI 6 | 7 | public struct RemindersListRow: View { 8 | let store: Store 9 | 10 | public init(store: Store) { 11 | self.store = store 12 | } 13 | 14 | public var body: some View { 15 | WithViewStore(store) { viewStore in 16 | HStack(alignment: .firstTextBaseline) { 17 | Button(action: { viewStore.send(.checkboxTap) }) { 18 | Image(systemName: viewStore.isCompleted ? "checkmark.square" : "square") 19 | } 20 | .buttonStyle(PlainButtonStyle()) 21 | 22 | VStack(alignment: .leading, spacing: 4) { 23 | Text(viewStore.title) 24 | 25 | if !viewStore.notes.isEmpty { 26 | Text(viewStore.notes) 27 | .font(.subheadline) 28 | .foregroundColor(.gray) 29 | } 30 | 31 | if let date = viewStore.date { 32 | VStack(alignment: .leading) { 33 | Text(date, style: .date) 34 | Text(date, style: .time) 35 | } 36 | .font(.subheadline) 37 | .foregroundColor(.gray) 38 | } 39 | } 40 | } 41 | .foregroundColor(viewStore.isCompleted ? .gray : nil) 42 | } 43 | } 44 | } 45 | 46 | struct RemindersListRow_Previews: PreviewProvider { 47 | static var previews: some View { 48 | let store = Store( 49 | initialState: Reminder( 50 | id: UUID(), 51 | title: "Buy groceries", 52 | notes: "Remember banana", 53 | isCompleted: false, 54 | date: Date() 55 | ), 56 | reducer: remindersListRowReducer, 57 | environment: RemindersListRowEnvironment(userNotificationClient: .noop) 58 | ) 59 | RemindersListRow(store: store) 60 | .previewLayout(.sizeThatFits) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /RemindersPackage/Tests/ReminderDetailCoreTests/ReminderDetailCoreTests.swift: -------------------------------------------------------------------------------- 1 | import ComposableArchitecture 2 | @testable import ReminderDetailCore 3 | import SharedModels 4 | import XCTest 5 | 6 | class ReminderDetailCoreTests: XCTestCase { 7 | let scheduler = DispatchQueue.test 8 | 9 | var defaultEnvironment: ReminderDetailEnvironment { 10 | var environment = ReminderDetailEnvironment.failing 11 | environment.applicationClient.openSettingsURLString = { "settings:reminders//reminders/settings" } 12 | environment.mainQueue = .immediate 13 | environment.notificationCenterClient.publisher = { _ in .none } 14 | return environment 15 | } 16 | 17 | func testTitleTextFieldChanged() { 18 | let reminder = Reminder(id: UUID(), title: "Buy milk", isCompleted: false) 19 | let initialState = ReminderDetailState(initialReminder: reminder) 20 | let store = TestStore( 21 | initialState: initialState, 22 | reducer: reminderDetailReducer, 23 | environment: defaultEnvironment 24 | ) 25 | 26 | store.send(.titleTextFieldChanged("Buy fruit")) { 27 | $0.currentReminder.title = "Buy fruit" 28 | } 29 | } 30 | 31 | func testNotesTextFieldChanged() { 32 | let reminder = Reminder(id: UUID(), title: "Buy milk", notes: "") 33 | let initialState = ReminderDetailState(initialReminder: reminder) 34 | let store = TestStore( 35 | initialState: initialState, 36 | reducer: reminderDetailReducer, 37 | environment: defaultEnvironment 38 | ) 39 | 40 | store.send(.notesTextFieldChanged("Semi-skimmed")) { 41 | $0.currentReminder.notes = "Semi-skimmed" 42 | } 43 | } 44 | 45 | func testDateChanged() { 46 | let reminder = Reminder(id: UUID()) 47 | let initialState = ReminderDetailState(initialReminder: reminder) 48 | let store = TestStore( 49 | initialState: initialState, 50 | reducer: reminderDetailReducer, 51 | environment: defaultEnvironment 52 | ) 53 | 54 | let newDate = Date() 55 | store.send(.dateChanged(newDate)) { 56 | $0.currentReminder.date = newDate 57 | } 58 | } 59 | 60 | func testToggleDateField() { 61 | let reminder = Reminder(id: UUID(), isCompleted: false) 62 | let initialState = ReminderDetailState(initialReminder: reminder) 63 | let currentTestDate = Date() 64 | 65 | var environment = defaultEnvironment 66 | environment.currentDate = { currentTestDate } 67 | 68 | let store = TestStore( 69 | initialState: initialState, 70 | reducer: reminderDetailReducer, 71 | environment: environment 72 | ) 73 | 74 | store.send(.toggleDateField(true)) { 75 | $0.isDateToggleOn = true 76 | $0.currentReminder.date = currentTestDate 77 | } 78 | 79 | store.send(.toggleDateField(false)) { 80 | $0.isDateToggleOn = false 81 | $0.currentReminder.date = nil 82 | } 83 | } 84 | 85 | func testConfirmDismissWithChanges() { 86 | let reminder = Reminder(id: UUID(), title: "Buy milk", isCompleted: false) 87 | let initialState = ReminderDetailState(initialReminder: reminder) 88 | let store = TestStore( 89 | initialState: initialState, 90 | reducer: reminderDetailReducer, 91 | environment: defaultEnvironment 92 | ) 93 | 94 | store.send(.titleTextFieldChanged("Buy fruit")) { 95 | $0.currentReminder.title = "Buy fruit" 96 | } 97 | 98 | store.send(.cancelButtonTap) { 99 | $0.cancelSheet = ActionSheetState( 100 | title: TextState("Confirmation"), 101 | buttons: [.cancel(), .destructive(TextState("Don't save changes"), action: .send(.dontSaveChangesButtonTap))] 102 | ) 103 | } 104 | 105 | store.assert( 106 | .send(.dontSaveChangesButtonTap), 107 | .receive(.closeDetail) 108 | ) 109 | } 110 | 111 | func testCancelWithoutChanges() { 112 | let reminder = Reminder(id: UUID(), title: "Buy milk", isCompleted: false) 113 | let initialState = ReminderDetailState(initialReminder: reminder) 114 | let store = TestStore( 115 | initialState: initialState, 116 | reducer: reminderDetailReducer, 117 | environment: defaultEnvironment 118 | ) 119 | 120 | store.assert( 121 | .send(.cancelButtonTap), 122 | .receive(.closeDetail) 123 | ) 124 | } 125 | 126 | func testDoneButton_WithoutDate() { 127 | let reminder = Reminder(id: UUID(), date: nil) 128 | let initialState = ReminderDetailState(initialReminder: reminder) 129 | let store = TestStore( 130 | initialState: initialState, 131 | reducer: reminderDetailReducer, 132 | environment: defaultEnvironment 133 | ) 134 | 135 | store.send(.doneButtonTap) 136 | 137 | store.receive(.closeAndSaveDetail) 138 | } 139 | 140 | func testDoneButton_WithDate_NotDeterminedNotificationPermissions_ButUserAccepts() { 141 | let reminderDate = Date.distantFuture 142 | let reminder = Reminder(id: UUID(), date: reminderDate) 143 | let initialState = ReminderDetailState(initialReminder: reminder) 144 | 145 | var scheduledNoticationDate: Date? 146 | var environment = defaultEnvironment 147 | environment.userNotificationClient.getNotificationSettings = .init(value: .init(authorizationStatus: .notDetermined)) 148 | environment.userNotificationClient.requestAuthorization = { _ in .init(value: true) } 149 | environment.userNotificationClient.add = { request in 150 | scheduledNoticationDate = (request.trigger as? UNCalendarNotificationTrigger)?.nextTriggerDate() 151 | return .init(value: ()) 152 | } 153 | 154 | let store = TestStore( 155 | initialState: initialState, 156 | reducer: reminderDetailReducer, 157 | environment: environment 158 | ) 159 | 160 | store.send(.onAppear) 161 | 162 | store.receive(.userNotificationSettingsResponse(.init(authorizationStatus: .notDetermined))) { 163 | $0.userNotificationSettings = .init(authorizationStatus: .notDetermined) 164 | } 165 | 166 | store.send(.doneButtonTap) 167 | 168 | store.receive(.userNotificationAuthorizationResponse(result: .success(true), date: reminderDate)) 169 | 170 | store.receive(.addNotificationRequest(reminderDate)) 171 | 172 | XCTAssertEqual(reminderDate, scheduledNoticationDate) 173 | 174 | store.receive(.closeAndSaveDetail) 175 | } 176 | 177 | func testDoneButton_WithDate_NotDeterminedNotificationPermissions_ButUserDenies() { 178 | let reminderDate = Date.distantFuture 179 | let reminder = Reminder(id: UUID(), date: reminderDate) 180 | let initialState = ReminderDetailState(initialReminder: reminder) 181 | 182 | var environment = defaultEnvironment 183 | environment.userNotificationClient.getNotificationSettings = .init(value: .init(authorizationStatus: .notDetermined)) 184 | environment.userNotificationClient.requestAuthorization = { _ in .init(value: false) } 185 | let store = TestStore( 186 | initialState: initialState, 187 | reducer: reminderDetailReducer, 188 | environment: environment 189 | ) 190 | 191 | store.send(.onAppear) 192 | 193 | store.receive(.userNotificationSettingsResponse(.init(authorizationStatus: .notDetermined))) { 194 | $0.userNotificationSettings = .init(authorizationStatus: .notDetermined) 195 | } 196 | 197 | store.send(.doneButtonTap) 198 | 199 | store.receive(.userNotificationAuthorizationResponse(result: .success(false), date: reminderDate)) 200 | 201 | store.send(.closeDetail) 202 | } 203 | 204 | func testDoneButton_WithDate_AuthorizedNotificationPermissions() { 205 | let reminderDate = Date.distantFuture 206 | let reminder = Reminder(id: UUID(), date: reminderDate) 207 | let initialState = ReminderDetailState(initialReminder: reminder) 208 | 209 | var scheduledNoticationDate: Date? 210 | var environment = defaultEnvironment 211 | environment.userNotificationClient.getNotificationSettings = .init(value: .init(authorizationStatus: .authorized)) 212 | environment.userNotificationClient.add = { request in 213 | scheduledNoticationDate = (request.trigger as? UNCalendarNotificationTrigger)?.nextTriggerDate() 214 | return .init(value: ()) 215 | } 216 | 217 | let store = TestStore( 218 | initialState: initialState, 219 | reducer: reminderDetailReducer, 220 | environment: environment 221 | ) 222 | 223 | store.send(.onAppear) 224 | 225 | store.receive(.userNotificationSettingsResponse(.init(authorizationStatus: .authorized))) { 226 | $0.userNotificationSettings = .init(authorizationStatus: .authorized) 227 | } 228 | 229 | store.send(.doneButtonTap) 230 | 231 | store.receive(.addNotificationRequest(reminderDate)) 232 | 233 | XCTAssertEqual(reminderDate, scheduledNoticationDate) 234 | 235 | store.receive(.closeAndSaveDetail) 236 | } 237 | 238 | func testDoneButton_WithDate_DeniedNotificationPermissions() { 239 | let reminderDate = Date.distantFuture 240 | let reminder = Reminder(id: UUID(), date: reminderDate) 241 | let initialState = ReminderDetailState(initialReminder: reminder) 242 | 243 | var openSettingsUrl: URL? 244 | var environment = defaultEnvironment 245 | environment.userNotificationClient.getNotificationSettings = .init(value: .init(authorizationStatus: .denied)) 246 | environment.applicationClient.open = { url in 247 | openSettingsUrl = url 248 | return .init(value: true) 249 | } 250 | 251 | let store = TestStore( 252 | initialState: initialState, 253 | reducer: reminderDetailReducer, 254 | environment: environment 255 | ) 256 | 257 | store.send(.onAppear) 258 | 259 | store.receive(.userNotificationSettingsResponse(.init(authorizationStatus: .denied))) { 260 | $0.userNotificationSettings = .init(authorizationStatus: .denied) 261 | } 262 | 263 | store.send(.doneButtonTap) { 264 | $0.alert = AlertState( 265 | title: TextState("Enable push notifications"), 266 | primaryButton: .default(TextState("Go to settings"), action: .send(.openNotificationSettings)), 267 | secondaryButton: .cancel() 268 | ) 269 | } 270 | 271 | store.send(.openNotificationSettings) 272 | 273 | XCTAssertEqual(openSettingsUrl, URL(string: "settings:reminders//reminders/settings")) 274 | 275 | store.send(.closeDetail) 276 | } 277 | } 278 | -------------------------------------------------------------------------------- /RemindersPackage/Tests/RemindersListCoreTests/RemindersListCoreTests.swift: -------------------------------------------------------------------------------- 1 | import ComposableArchitecture 2 | @testable import Reminders 3 | import RemindersListCore 4 | import ReminderDetailCore 5 | import SharedModels 6 | import UIApplicationClient 7 | import UserNotificationClient 8 | import XCTest 9 | 10 | class RemindersListCoreTests: XCTestCase { 11 | let scheduler = DispatchQueue.test 12 | 13 | var defaultEnvironment: RemindersListEnvironment { 14 | RemindersListEnvironment( 15 | uuid: UUID.incrementing, 16 | userNotifications: .noop, 17 | applicationClient: .noop, 18 | notificationCenterClient: .noop, 19 | mainQueue: scheduler.eraseToAnyScheduler() 20 | ) 21 | } 22 | 23 | func testAddButtonTap() { 24 | let store = TestStore( 25 | initialState: RemindersListState(list: []), 26 | reducer: remindersListReducer, 27 | environment: defaultEnvironment 28 | ) 29 | 30 | store.send(.addButtonTap) { 31 | $0.list = [ 32 | Reminder(id: UUID(uuidString: "00000000-0000-0000-0000-000000000000")!, title: "", isCompleted: false) 33 | ] 34 | } 35 | 36 | store.send(.addButtonTap) { 37 | $0.list = [ 38 | Reminder(id: UUID(uuidString: "00000000-0000-0000-0000-000000000001")!, title: "", isCompleted: false), 39 | Reminder(id: UUID(uuidString: "00000000-0000-0000-0000-000000000000")!, title: "", isCompleted: false) 40 | ] 41 | } 42 | } 43 | 44 | func testCheckboxTap() { 45 | let reminder = Reminder( 46 | id: UUID(uuidString: "00000000-0000-0000-0000-000000000000")!, 47 | title: "", 48 | isCompleted: false 49 | ) 50 | let store = TestStore( 51 | initialState: RemindersListState(list: [reminder]), 52 | reducer: remindersListReducer, 53 | environment: defaultEnvironment 54 | ) 55 | 56 | store.assert( 57 | .send(.reminderRow(id: reminder.id, action: .checkboxTap)) { 58 | $0.list[id: reminder.id]?.isCompleted = true 59 | }, 60 | .do { 61 | self.scheduler.advance(by: 1) 62 | }, 63 | .receive(.todoDelayCompleted) 64 | ) 65 | } 66 | 67 | func testRemindersSorting() { 68 | let reminder1 = Reminder( 69 | id: UUID(uuidString: "00000000-0000-0000-0000-000000000000")!, 70 | title: "", 71 | isCompleted: false 72 | ) 73 | let reminder2 = Reminder( 74 | id: UUID(uuidString: "00000000-0000-0000-0000-000000000001")!, 75 | title: "", 76 | isCompleted: false 77 | ) 78 | let store = TestStore( 79 | initialState: RemindersListState(list: [reminder1, reminder2]), 80 | reducer: remindersListReducer, 81 | environment: defaultEnvironment 82 | ) 83 | 84 | store.assert( 85 | .send(.reminderRow(id: reminder1.id, action: .checkboxTap)) { 86 | $0.list[id: reminder1.id]?.isCompleted = true 87 | }, 88 | .do { 89 | self.scheduler.advance(by: 1) 90 | }, 91 | .receive(.todoDelayCompleted) { 92 | $0.list = [ 93 | $0.list[id: reminder2.id]!, 94 | $0.list[id: reminder1.id]! 95 | ] 96 | } 97 | ) 98 | } 99 | 100 | func testRemindersSortingCancellation() { 101 | let reminder1 = Reminder( 102 | id: UUID(uuidString: "00000000-0000-0000-0000-000000000000")!, 103 | title: "", 104 | isCompleted: false 105 | ) 106 | let reminder2 = Reminder( 107 | id: UUID(uuidString: "00000000-0000-0000-0000-000000000001")!, 108 | title: "", 109 | isCompleted: false 110 | ) 111 | let store = TestStore( 112 | initialState: RemindersListState(list: [reminder1, reminder2]), 113 | reducer: remindersListReducer, 114 | environment: defaultEnvironment 115 | ) 116 | 117 | store.assert( 118 | .send(.reminderRow(id: reminder1.id, action: .checkboxTap)) { 119 | $0.list[id: reminder1.id]?.isCompleted = true 120 | }, 121 | .do { 122 | self.scheduler.advance(by: 0.5) 123 | }, 124 | .send(.reminderRow(id: reminder1.id, action: .checkboxTap)) { 125 | $0.list[id: reminder1.id]?.isCompleted = false 126 | }, 127 | .do { 128 | self.scheduler.advance(by: 1.0) 129 | }, 130 | .receive(.todoDelayCompleted) 131 | ) 132 | } 133 | 134 | func testInfoButtonSelection() { 135 | let reminder = Reminder( 136 | id: UUID(uuidString: "00000000-0000-0000-0000-000000000000")!, 137 | title: "", 138 | isCompleted: false 139 | ) 140 | let store = TestStore( 141 | initialState: RemindersListState(list: [reminder]), 142 | reducer: remindersListReducer, 143 | environment: defaultEnvironment 144 | ) 145 | 146 | store.send(.reminderRow(id: reminder.id, action: .infoButtonTap)) { 147 | $0.detailState = Identified(ReminderDetailState(initialReminder: reminder), id: reminder.id) 148 | } 149 | 150 | store.send(.sheetSelection(nil)) { 151 | $0.detailState = nil 152 | } 153 | 154 | store.send(.reminderRow(id: reminder.id, action: .infoButtonTap)) { 155 | $0.detailState = Identified(ReminderDetailState(initialReminder: reminder), id: reminder.id) 156 | } 157 | } 158 | 159 | func testSelectionSheet() { 160 | let reminder = Reminder( 161 | id: UUID(uuidString: "00000000-0000-0000-0000-000000000000")!, 162 | title: "", 163 | isCompleted: false 164 | ) 165 | let identifiedReminder = Identified(ReminderDetailState(initialReminder: reminder), id: reminder.id) 166 | let store = TestStore( 167 | initialState: RemindersListState(list: [reminder]), 168 | reducer: remindersListReducer, 169 | environment: defaultEnvironment 170 | ) 171 | 172 | store.send(.sheetSelection(identifiedReminder)) { 173 | $0.detailState = identifiedReminder 174 | } 175 | 176 | store.send(.sheetSelection(nil)) { 177 | $0.detailState = nil 178 | } 179 | } 180 | 181 | func testListReminderGetsUpdatedFromDetail() { 182 | let reminder = Reminder( 183 | id: UUID(uuidString: "00000000-0000-0000-0000-000000000000")!, 184 | title: "", 185 | isCompleted: false 186 | ) 187 | let store = TestStore( 188 | initialState: RemindersListState(list: [reminder]), 189 | reducer: remindersListReducer, 190 | environment: defaultEnvironment 191 | ) 192 | 193 | store.send(.reminderRow(id: reminder.id, action: .infoButtonTap)) { 194 | $0.detailState = Identified(ReminderDetailState(initialReminder: reminder), id: reminder.id) 195 | } 196 | 197 | store.send(.reminderDetail(.titleTextFieldChanged("Buy milk"))) { 198 | $0.detailState?.currentReminder.title = "Buy milk" 199 | } 200 | 201 | store.send(.reminderDetail(.closeAndSaveDetail)) { 202 | let updatedDetailState = $0.detailState! 203 | $0.list[id: updatedDetailState.id] = updatedDetailState.currentReminder 204 | $0.detailState = nil 205 | } 206 | } 207 | 208 | func testCloseDetail() { 209 | let reminder = Reminder( 210 | id: UUID(uuidString: "00000000-0000-0000-0000-000000000000")!, 211 | title: "", 212 | isCompleted: false 213 | ) 214 | let store = TestStore( 215 | initialState: RemindersListState(list: [reminder]), 216 | reducer: remindersListReducer, 217 | environment: defaultEnvironment 218 | ) 219 | 220 | store.send(.reminderRow(id: reminder.id, action: .infoButtonTap)) { 221 | $0.detailState = Identified(ReminderDetailState(initialReminder: reminder), id: reminder.id) 222 | } 223 | 224 | store.send(.reminderDetail(.closeDetail)) { 225 | $0.detailState = nil 226 | } 227 | } 228 | } 229 | 230 | private extension UUID { 231 | // A deterministic, auto-incrementing "UUID" generator for testing. 232 | static var incrementing: () -> UUID { 233 | var uuid = 0 234 | return { 235 | defer { uuid += 1 } 236 | return UUID(uuidString: "00000000-0000-0000-0000-\(String(format: "%012x", uuid))")! 237 | } 238 | } 239 | } 240 | -------------------------------------------------------------------------------- /RemindersPackage/Tests/RemindersListRowCoreTests/RemindersListRowCoreTests.swift: -------------------------------------------------------------------------------- 1 | import ComposableArchitecture 2 | @testable import RemindersListRowCore 3 | import SharedModels 4 | import XCTest 5 | 6 | class RemindersListRowCoreTests: XCTestCase { 7 | var defaultEnvironment: RemindersListRowEnvironment { 8 | RemindersListRowEnvironment.failing 9 | } 10 | 11 | func testCheckboxTap_WithDate_Completing() { 12 | var environment = defaultEnvironment 13 | var canceledNotificationId: String? 14 | environment.userNotificationClient.removePendingNotificationRequestsWithIdentifiers = { identifiers in 15 | canceledNotificationId = identifiers.first 16 | return .none 17 | } 18 | 19 | let reminder = Reminder(id: UUID(), isCompleted: false, date: Date()) 20 | let store = TestStore( 21 | initialState: reminder, 22 | reducer: remindersListRowReducer, 23 | environment: environment 24 | ) 25 | 26 | store.send(.checkboxTap) { 27 | $0.isCompleted = true 28 | } 29 | 30 | XCTAssertEqual(canceledNotificationId, reminder.id.uuidString) 31 | } 32 | 33 | func testCheckboxTap_WithDate_Uncompleting() { 34 | var environment = defaultEnvironment 35 | var scheduledNotificationId: String? 36 | environment.userNotificationClient.add = { request in 37 | scheduledNotificationId = request.identifier 38 | return .none 39 | } 40 | 41 | let reminder = Reminder(id: UUID(), isCompleted: true, date: Date()) 42 | let store = TestStore( 43 | initialState: reminder, 44 | reducer: remindersListRowReducer, 45 | environment: environment 46 | ) 47 | 48 | store.send(.checkboxTap) { 49 | $0.isCompleted = false 50 | } 51 | 52 | XCTAssertEqual(scheduledNotificationId, reminder.id.uuidString) 53 | } 54 | 55 | func testCheckboxTap_WithoutDate() { 56 | let store = TestStore( 57 | initialState: Reminder(id: UUID(), isCompleted: false), 58 | reducer: remindersListRowReducer, 59 | environment: defaultEnvironment 60 | ) 61 | 62 | store.send(.checkboxTap) { 63 | $0.isCompleted = true 64 | } 65 | 66 | store.send(.checkboxTap) { 67 | $0.isCompleted = false 68 | } 69 | } 70 | 71 | func testTextFieldChanged() { 72 | let store = TestStore( 73 | initialState: Reminder(id: UUID(), title: "Buy milk"), 74 | reducer: remindersListRowReducer, 75 | environment: defaultEnvironment 76 | ) 77 | 78 | store.send(.textFieldChanged("Buy fruit")) { 79 | $0.title = "Buy fruit" 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /RemindersWatch Extension/Assets.xcassets/Complication.complicationset/Circular.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "watch", 5 | "scale" : "2x", 6 | "screen-width" : "<=145" 7 | }, 8 | { 9 | "idiom" : "watch", 10 | "scale" : "2x", 11 | "screen-width" : ">161" 12 | }, 13 | { 14 | "idiom" : "watch", 15 | "scale" : "2x", 16 | "screen-width" : ">145" 17 | }, 18 | { 19 | "idiom" : "watch", 20 | "scale" : "2x", 21 | "screen-width" : ">183" 22 | } 23 | ], 24 | "info" : { 25 | "author" : "xcode", 26 | "version" : 1 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /RemindersWatch 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 | -------------------------------------------------------------------------------- /RemindersWatch Extension/Assets.xcassets/Complication.complicationset/Extra Large.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "watch", 5 | "scale" : "2x", 6 | "screen-width" : "<=145" 7 | }, 8 | { 9 | "idiom" : "watch", 10 | "scale" : "2x", 11 | "screen-width" : ">161" 12 | }, 13 | { 14 | "idiom" : "watch", 15 | "scale" : "2x", 16 | "screen-width" : ">145" 17 | }, 18 | { 19 | "idiom" : "watch", 20 | "scale" : "2x", 21 | "screen-width" : ">183" 22 | } 23 | ], 24 | "info" : { 25 | "author" : "xcode", 26 | "version" : 1 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /RemindersWatch Extension/Assets.xcassets/Complication.complicationset/Graphic Bezel.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "watch", 5 | "scale" : "2x", 6 | "screen-width" : ">161" 7 | }, 8 | { 9 | "idiom" : "watch", 10 | "scale" : "2x", 11 | "screen-width" : ">183" 12 | } 13 | ], 14 | "info" : { 15 | "author" : "xcode", 16 | "version" : 1 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /RemindersWatch Extension/Assets.xcassets/Complication.complicationset/Graphic Circular.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "watch", 5 | "scale" : "2x", 6 | "screen-width" : ">161" 7 | }, 8 | { 9 | "idiom" : "watch", 10 | "scale" : "2x", 11 | "screen-width" : ">183" 12 | } 13 | ], 14 | "info" : { 15 | "author" : "xcode", 16 | "version" : 1 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /RemindersWatch Extension/Assets.xcassets/Complication.complicationset/Graphic Corner.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "watch", 5 | "scale" : "2x", 6 | "screen-width" : ">161" 7 | }, 8 | { 9 | "idiom" : "watch", 10 | "scale" : "2x", 11 | "screen-width" : ">183" 12 | } 13 | ], 14 | "info" : { 15 | "author" : "xcode", 16 | "version" : 1 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /RemindersWatch Extension/Assets.xcassets/Complication.complicationset/Graphic Extra Large.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "watch", 5 | "scale" : "2x", 6 | "screen-width" : "<=145" 7 | }, 8 | { 9 | "idiom" : "watch", 10 | "scale" : "2x", 11 | "screen-width" : ">161" 12 | }, 13 | { 14 | "idiom" : "watch", 15 | "scale" : "2x", 16 | "screen-width" : ">145" 17 | }, 18 | { 19 | "idiom" : "watch", 20 | "scale" : "2x", 21 | "screen-width" : ">183" 22 | } 23 | ], 24 | "info" : { 25 | "author" : "xcode", 26 | "version" : 1 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /RemindersWatch Extension/Assets.xcassets/Complication.complicationset/Graphic Large Rectangular.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "watch", 5 | "scale" : "2x", 6 | "screen-width" : ">161" 7 | }, 8 | { 9 | "idiom" : "watch", 10 | "scale" : "2x", 11 | "screen-width" : ">183" 12 | } 13 | ], 14 | "info" : { 15 | "author" : "xcode", 16 | "version" : 1 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /RemindersWatch Extension/Assets.xcassets/Complication.complicationset/Modular.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "watch", 5 | "scale" : "2x", 6 | "screen-width" : "<=145" 7 | }, 8 | { 9 | "idiom" : "watch", 10 | "scale" : "2x", 11 | "screen-width" : ">161" 12 | }, 13 | { 14 | "idiom" : "watch", 15 | "scale" : "2x", 16 | "screen-width" : ">145" 17 | }, 18 | { 19 | "idiom" : "watch", 20 | "scale" : "2x", 21 | "screen-width" : ">183" 22 | } 23 | ], 24 | "info" : { 25 | "author" : "xcode", 26 | "version" : 1 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /RemindersWatch Extension/Assets.xcassets/Complication.complicationset/Utilitarian.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "watch", 5 | "scale" : "2x", 6 | "screen-width" : "<=145" 7 | }, 8 | { 9 | "idiom" : "watch", 10 | "scale" : "2x", 11 | "screen-width" : ">161" 12 | }, 13 | { 14 | "idiom" : "watch", 15 | "scale" : "2x", 16 | "screen-width" : ">145" 17 | }, 18 | { 19 | "idiom" : "watch", 20 | "scale" : "2x", 21 | "screen-width" : ">183" 22 | } 23 | ], 24 | "info" : { 25 | "author" : "xcode", 26 | "version" : 1 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /RemindersWatch Extension/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /RemindersWatch Extension/ComplicationController.swift: -------------------------------------------------------------------------------- 1 | import ClockKit 2 | 3 | 4 | class ComplicationController: NSObject, CLKComplicationDataSource { 5 | 6 | // MARK: - Complication Configuration 7 | 8 | func getComplicationDescriptors(handler: @escaping ([CLKComplicationDescriptor]) -> Void) { 9 | let descriptors = [ 10 | CLKComplicationDescriptor(identifier: "complication", displayName: "Reminders", supportedFamilies: CLKComplicationFamily.allCases) 11 | // Multiple complication support can be added here with more descriptors 12 | ] 13 | 14 | // Call the handler with the currently supported complication descriptors 15 | handler(descriptors) 16 | } 17 | 18 | func handleSharedComplicationDescriptors(_ complicationDescriptors: [CLKComplicationDescriptor]) { 19 | // Do any necessary work to support these newly shared complication descriptors 20 | } 21 | 22 | // MARK: - Timeline Configuration 23 | 24 | func getTimelineEndDate(for complication: CLKComplication, withHandler handler: @escaping (Date?) -> Void) { 25 | // Call the handler with the last entry date you can currently provide or nil if you can't support future timelines 26 | handler(nil) 27 | } 28 | 29 | func getPrivacyBehavior(for complication: CLKComplication, withHandler handler: @escaping (CLKComplicationPrivacyBehavior) -> Void) { 30 | // Call the handler with your desired behavior when the device is locked 31 | handler(.showOnLockScreen) 32 | } 33 | 34 | // MARK: - Timeline Population 35 | 36 | func getCurrentTimelineEntry(for complication: CLKComplication, withHandler handler: @escaping (CLKComplicationTimelineEntry?) -> Void) { 37 | // Call the handler with the current timeline entry 38 | handler(nil) 39 | } 40 | 41 | func getTimelineEntries(for complication: CLKComplication, after date: Date, limit: Int, withHandler handler: @escaping ([CLKComplicationTimelineEntry]?) -> Void) { 42 | // Call the handler with the timeline entries after the given date 43 | handler(nil) 44 | } 45 | 46 | // MARK: - Sample Templates 47 | 48 | func getLocalizableSampleTemplate(for complication: CLKComplication, withHandler handler: @escaping (CLKComplicationTemplate?) -> Void) { 49 | // This method will be called once per supported complication, and the results will be cached 50 | handler(nil) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /RemindersWatch Extension/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | RemindersWatch Extension 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleVersion 22 | 1 23 | CLKComplicationPrincipalClass 24 | $(PRODUCT_MODULE_NAME).ComplicationController 25 | NSExtension 26 | 27 | NSExtensionAttributes 28 | 29 | WKAppBundleIdentifier 30 | com.hristic.luka.Reminders.watchkitapp 31 | 32 | NSExtensionPointIdentifier 33 | com.apple.watchkit 34 | 35 | WKRunsIndependentlyOfCompanionApp 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /RemindersWatch Extension/NotificationController.swift: -------------------------------------------------------------------------------- 1 | import WatchKit 2 | import SwiftUI 3 | import UserNotifications 4 | 5 | class NotificationController: WKUserNotificationHostingController { 6 | 7 | override var body: NotificationView { 8 | return NotificationView() 9 | } 10 | 11 | override func willActivate() { 12 | // This method is called when watch view controller is about to be visible to user 13 | super.willActivate() 14 | } 15 | 16 | override func didDeactivate() { 17 | // This method is called when watch view controller is no longer visible 18 | super.didDeactivate() 19 | } 20 | 21 | override func didReceive(_ notification: UNNotification) { 22 | // This method is called when a notification needs to be presented. 23 | // Implement it if you use a dynamic notification interface. 24 | // Populate your dynamic notification interface as quickly as possible. 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /RemindersWatch Extension/NotificationView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct NotificationView: View { 4 | var body: some View { 5 | Text("Hello, World!") 6 | } 7 | } 8 | 9 | struct NotificationView_Previews: PreviewProvider { 10 | static var previews: some View { 11 | NotificationView() 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /RemindersWatch Extension/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /RemindersWatch 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 | -------------------------------------------------------------------------------- /RemindersWatch Extension/RemindersApp.swift: -------------------------------------------------------------------------------- 1 | import AppCore 2 | import AppDelegateCore 3 | import ComposableArchitecture 4 | import RemindersListCore 5 | import SharedModels 6 | import SwiftUI 7 | import UserNotificationClientLive 8 | import WatchOSApp 9 | 10 | @main 11 | struct RemindersApp: App { 12 | public let store = Store( 13 | initialState: AppState( 14 | appDelegateState: AppDelegateState(), 15 | remindersListState: RemindersListState( 16 | list: [ 17 | Reminder(id: UUID(), title: "Buy fruit", notes: "Bananas", isCompleted: false, date: Date()), 18 | Reminder(id: UUID(), title: "Buy milk", notes: "", isCompleted: false), 19 | Reminder(id: UUID(), title: "Charge phone", notes: "", isCompleted: true) 20 | ], 21 | detailState: nil 22 | ) 23 | ), 24 | reducer: appReducer, 25 | environment: .live 26 | ) 27 | 28 | @SceneBuilder var body: some Scene { 29 | WindowGroup { 30 | NavigationView { 31 | WatchOSAppView(store: store) 32 | } 33 | } 34 | 35 | WKNotificationScene(controller: NotificationController.self, category: "myCategory") 36 | } 37 | } 38 | 39 | extension AppEnvironment { 40 | static var live: Self { 41 | AppEnvironment( 42 | userNotifications: .live, 43 | applicationClient: .noop, 44 | notificationCenterClient: .noop, 45 | mainQueue: DispatchQueue.main.eraseToAnyScheduler() 46 | ) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /RemindersWatch/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /RemindersWatch/Assets.xcassets/AppIcon.appiconset/100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukeleleh/reminders-app/1b7ed21aca68c463ec2b85bee5d1e363504f929b/RemindersWatch/Assets.xcassets/AppIcon.appiconset/100.png -------------------------------------------------------------------------------- /RemindersWatch/Assets.xcassets/AppIcon.appiconset/1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukeleleh/reminders-app/1b7ed21aca68c463ec2b85bee5d1e363504f929b/RemindersWatch/Assets.xcassets/AppIcon.appiconset/1024.png -------------------------------------------------------------------------------- /RemindersWatch/Assets.xcassets/AppIcon.appiconset/172.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukeleleh/reminders-app/1b7ed21aca68c463ec2b85bee5d1e363504f929b/RemindersWatch/Assets.xcassets/AppIcon.appiconset/172.png -------------------------------------------------------------------------------- /RemindersWatch/Assets.xcassets/AppIcon.appiconset/196.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukeleleh/reminders-app/1b7ed21aca68c463ec2b85bee5d1e363504f929b/RemindersWatch/Assets.xcassets/AppIcon.appiconset/196.png -------------------------------------------------------------------------------- /RemindersWatch/Assets.xcassets/AppIcon.appiconset/216.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukeleleh/reminders-app/1b7ed21aca68c463ec2b85bee5d1e363504f929b/RemindersWatch/Assets.xcassets/AppIcon.appiconset/216.png -------------------------------------------------------------------------------- /RemindersWatch/Assets.xcassets/AppIcon.appiconset/48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukeleleh/reminders-app/1b7ed21aca68c463ec2b85bee5d1e363504f929b/RemindersWatch/Assets.xcassets/AppIcon.appiconset/48.png -------------------------------------------------------------------------------- /RemindersWatch/Assets.xcassets/AppIcon.appiconset/55.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukeleleh/reminders-app/1b7ed21aca68c463ec2b85bee5d1e363504f929b/RemindersWatch/Assets.xcassets/AppIcon.appiconset/55.png -------------------------------------------------------------------------------- /RemindersWatch/Assets.xcassets/AppIcon.appiconset/58.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukeleleh/reminders-app/1b7ed21aca68c463ec2b85bee5d1e363504f929b/RemindersWatch/Assets.xcassets/AppIcon.appiconset/58.png -------------------------------------------------------------------------------- /RemindersWatch/Assets.xcassets/AppIcon.appiconset/80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukeleleh/reminders-app/1b7ed21aca68c463ec2b85bee5d1e363504f929b/RemindersWatch/Assets.xcassets/AppIcon.appiconset/80.png -------------------------------------------------------------------------------- /RemindersWatch/Assets.xcassets/AppIcon.appiconset/87.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukeleleh/reminders-app/1b7ed21aca68c463ec2b85bee5d1e363504f929b/RemindersWatch/Assets.xcassets/AppIcon.appiconset/87.png -------------------------------------------------------------------------------- /RemindersWatch/Assets.xcassets/AppIcon.appiconset/88.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukeleleh/reminders-app/1b7ed21aca68c463ec2b85bee5d1e363504f929b/RemindersWatch/Assets.xcassets/AppIcon.appiconset/88.png -------------------------------------------------------------------------------- /RemindersWatch/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "48.png", 5 | "idiom" : "watch", 6 | "role" : "notificationCenter", 7 | "scale" : "2x", 8 | "size" : "24x24", 9 | "subtype" : "38mm" 10 | }, 11 | { 12 | "filename" : "55.png", 13 | "idiom" : "watch", 14 | "role" : "notificationCenter", 15 | "scale" : "2x", 16 | "size" : "27.5x27.5", 17 | "subtype" : "42mm" 18 | }, 19 | { 20 | "filename" : "58.png", 21 | "idiom" : "watch", 22 | "role" : "companionSettings", 23 | "scale" : "2x", 24 | "size" : "29x29" 25 | }, 26 | { 27 | "filename" : "87.png", 28 | "idiom" : "watch", 29 | "role" : "companionSettings", 30 | "scale" : "3x", 31 | "size" : "29x29" 32 | }, 33 | { 34 | "filename" : "80.png", 35 | "idiom" : "watch", 36 | "role" : "appLauncher", 37 | "scale" : "2x", 38 | "size" : "40x40", 39 | "subtype" : "38mm" 40 | }, 41 | { 42 | "filename" : "88.png", 43 | "idiom" : "watch", 44 | "role" : "appLauncher", 45 | "scale" : "2x", 46 | "size" : "44x44", 47 | "subtype" : "40mm" 48 | }, 49 | { 50 | "filename" : "100.png", 51 | "idiom" : "watch", 52 | "role" : "appLauncher", 53 | "scale" : "2x", 54 | "size" : "50x50", 55 | "subtype" : "44mm" 56 | }, 57 | { 58 | "filename" : "172.png", 59 | "idiom" : "watch", 60 | "role" : "quickLook", 61 | "scale" : "2x", 62 | "size" : "86x86", 63 | "subtype" : "38mm" 64 | }, 65 | { 66 | "filename" : "196.png", 67 | "idiom" : "watch", 68 | "role" : "quickLook", 69 | "scale" : "2x", 70 | "size" : "98x98", 71 | "subtype" : "42mm" 72 | }, 73 | { 74 | "filename" : "216.png", 75 | "idiom" : "watch", 76 | "role" : "quickLook", 77 | "scale" : "2x", 78 | "size" : "108x108", 79 | "subtype" : "44mm" 80 | }, 81 | { 82 | "filename" : "1024.png", 83 | "idiom" : "watch-marketing", 84 | "scale" : "1x", 85 | "size" : "1024x1024" 86 | } 87 | ], 88 | "info" : { 89 | "author" : "xcode", 90 | "version" : 1 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /RemindersWatch/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /RemindersWatch/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | Reminders 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleVersion 22 | 1 23 | UISupportedInterfaceOrientations 24 | 25 | UIInterfaceOrientationPortrait 26 | UIInterfaceOrientationPortraitUpsideDown 27 | 28 | WKCompanionAppBundleIdentifier 29 | com.hristic.luka.Reminders 30 | WKWatchKitApp 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /Screenshots/reminder_detail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukeleleh/reminders-app/1b7ed21aca68c463ec2b85bee5d1e363504f929b/Screenshots/reminder_detail.png -------------------------------------------------------------------------------- /Screenshots/reminder_notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukeleleh/reminders-app/1b7ed21aca68c463ec2b85bee5d1e363504f929b/Screenshots/reminder_notification.png -------------------------------------------------------------------------------- /Screenshots/reminders_list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukeleleh/reminders-app/1b7ed21aca68c463ec2b85bee5d1e363504f929b/Screenshots/reminders_list.png -------------------------------------------------------------------------------- /Screenshots/reminders_list_watch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukeleleh/reminders-app/1b7ed21aca68c463ec2b85bee5d1e363504f929b/Screenshots/reminders_list_watch.png --------------------------------------------------------------------------------