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