├── .gitignore
├── LICENSE
├── README.md
├── SwiftUI-Todo-Redux.xcodeproj
├── project.pbxproj
└── project.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ └── IDEWorkspaceChecks.plist
├── SwiftUI-Todo-Redux
├── AppDelegate.swift
├── Assets.xcassets
│ ├── AppIcon.appiconset
│ │ └── Contents.json
│ ├── Contents.json
│ ├── form-field-background.colorset
│ │ └── Contents.json
│ ├── tab_task.imageset
│ │ ├── Contents.json
│ │ └── second.pdf
│ └── tab_user.imageset
│ │ ├── Contents.json
│ │ └── first.pdf
├── Base.lproj
│ └── LaunchScreen.storyboard
├── Info.plist
├── Preview Content
│ └── Preview Assets.xcassets
│ │ └── Contents.json
├── SceneDelegate.swift
├── models
│ ├── DataStore.swift
│ ├── Task.swift
│ ├── TaskResponse.swift
│ ├── User.swift
│ └── UserResponse.swift
├── states
│ ├── AppState.swift
│ ├── actions
│ │ ├── Action.swift
│ │ ├── TaskActions.swift
│ │ └── UserActions.swift
│ ├── flux-substate
│ │ ├── FluxState.swift
│ │ ├── TasksState.swift
│ │ └── UsersState.swift
│ └── reducers-statemachine
│ │ ├── Reducer.swift
│ │ ├── TaskStateReducer.swift
│ │ └── UserStateReducer.swift
└── views
│ ├── HomeView.swift
│ ├── common
│ ├── FormButtons.swift
│ └── KeyboardObserver.swift
│ ├── tasks
│ ├── TaskCreate.swift
│ ├── TaskDetail.swift
│ ├── TasksList.swift
│ └── TasksRow.swift
│ └── users
│ ├── UserCreate.swift
│ ├── UserDetail.swift
│ ├── UsersList.swift
│ └── UsersRow.swift
├── SwiftUI-Todo-ReduxTests
├── Info.plist
└── SwiftUI_Todo_ReduxTests.swift
└── screenshot.png
/.gitignore:
--------------------------------------------------------------------------------
1 | # Xcode
2 | #
3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
4 | .DS_Store
5 |
6 | ## Build generated
7 | build/
8 | DerivedData/
9 |
10 | ## Various settings
11 | *.pbxuser
12 | !default.pbxuser
13 | *.mode1v3
14 | !default.mode1v3
15 | *.mode2v3
16 | !default.mode2v3
17 | *.perspectivev3
18 | !default.perspectivev3
19 | xcuserdata/
20 |
21 | ## Other
22 | *.moved-aside
23 | *.xccheckout
24 | *.xcscmblueprint
25 |
26 | ## Obj-C/Swift specific
27 | *.hmap
28 | *.ipa
29 | *.dSYM.zip
30 | *.dSYM
31 |
32 | ## Playgrounds
33 | timeline.xctimeline
34 | playground.xcworkspace
35 |
36 | # Swift Package Manager
37 | #
38 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
39 | # Packages/
40 | # Package.pins
41 | # Package.resolved
42 | .build/
43 |
44 | # CocoaPods
45 | #
46 | # We recommend against adding the Pods directory to your .gitignore. However
47 | # you should judge for yourself, the pros and cons are mentioned at:
48 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
49 | #
50 | # Pods/
51 |
52 | # Carthage
53 | #
54 | # Add this line if you want to avoid checking in source code from Carthage dependencies.
55 | # Carthage/Checkouts
56 |
57 | Carthage/Build
58 |
59 | # fastlane
60 | #
61 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
62 | # screenshots whenever they are needed.
63 | # For more information about the recommended setup visit:
64 | # https://docs.fastlane.tools/best-practices/source-control/#source-control
65 |
66 | fastlane/report.xml
67 | fastlane/Preview.html
68 | fastlane/screenshots/**/*.png
69 | fastlane/test_output
70 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 moflo
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 | # SwiftUI-Todo-Redux
2 | SwiftUI Todo Redux app example using a React/Redux monolithic state store with flux like dispatch/reduce actions
3 |
4 |
5 |
6 | ## Background
7 |
8 | SwiftUI based app using a centralized, 'monolithic' AppState() which binds to the UI using the `Combine` framework and a `PassthroughSubject` method. This architecture is based on the React/Redux pattern and has a few key benefits:
9 |
10 | 1. Global app state is maintained in single global struct, `let state = AppState()`
11 | 2. Testing and Previewing of individual views can then be isolated with local test states, eg., a local debug call to `environmentObject(sampleStore)` can be used for UI Previews anywhere in the app
12 | 3. Isolating `Actions` from `State` allows for cleaner synchronous behavior, eg., handle both server based calls and local UI-only actions in the same method or flow
13 | 4. Chnages to state are `Reduced` within a simple state machine method which can be easily tested
14 |
15 |
16 | ## File Structure
17 |
18 | The structure of the app follows a simple pattern of MVS: Models, Views and State. Models contain all the relevant state, separated into relevant gropus such as Tasks, Users, Authorization, etc. The `models` directory contains model definitions, as well as codecs and backing store (ie., API Services) for isolated testing of the models and their propoer storage. The `states` directory contains a combination of global app state, the sub-states or `flux` describiing the relevant groups (eg., Task, User), actions which the user initiates (eg., `TaskActions` or `UserActions`) and then trigger asynchronous server-based or synchronous actions. The result of the actions are then reduced (eg., `TaskStateReducer` or `UserStateReducer`) to subsequently modify the global app state.
19 |
20 | The `AppState` structure holds referenes to the group states (ie., Tasks and User lists), as well as acts as a central `dispatch` point for both actions and handling any state updates via the `Combine` framework.
21 |
22 | Finally, all UI Views are maintained within their respective hierarchy, with a Home or Root view driving all app navigation.
23 |
24 |
25 | ```
26 | .
27 | |____AppDelegate.swift
28 | |____SceneDelegate.swift
29 | |____views
30 | | |____HomeView.swift
31 | |____models
32 | | |____User.swift
33 | | |____UserResponse.swift
34 | | |____Task.swift
35 | |____states
36 | | |____AppState.swift
37 | | |____flux (substate)
38 | | | |____FluxState.swift
39 | | | |____UsersState.swift
40 | | | |____TasksState.swift
41 | | |____actions
42 | | | |____Action.swift
43 | | | |____UserActions.swift
44 | | | |____TaskActions.swift
45 | | |____reducers (statemachine)
46 | | | |____Reducer.swift
47 | | | |____UserStateReducer.swift
48 | | | |____TaskStateReducer.swift
49 | ```
50 |
51 |
52 | ## Notes
53 |
54 | - Mock API testing using [https://www.mocky.io](https://www.mocky.io)
55 |
--------------------------------------------------------------------------------
/SwiftUI-Todo-Redux.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 50;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | 8416558F22BFF4EF007DF30E /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8416558E22BFF4EE007DF30E /* HomeView.swift */; };
11 | 8416559622BFF7C6007DF30E /* TaskDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8416559022BFF7C5007DF30E /* TaskDetail.swift */; };
12 | 8416559722BFF7C6007DF30E /* UsersRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8416559122BFF7C5007DF30E /* UsersRow.swift */; };
13 | 8416559822BFF7C6007DF30E /* TasksRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8416559222BFF7C5007DF30E /* TasksRow.swift */; };
14 | 8416559922BFF7C6007DF30E /* UserDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8416559322BFF7C5007DF30E /* UserDetail.swift */; };
15 | 8416559A22BFF7C6007DF30E /* UsersList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8416559422BFF7C6007DF30E /* UsersList.swift */; };
16 | 8416559B22BFF7C6007DF30E /* TasksList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8416559522BFF7C6007DF30E /* TasksList.swift */; };
17 | 841655A322C029E6007DF30E /* SwiftUI_Todo_ReduxTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 841655A222C029E6007DF30E /* SwiftUI_Todo_ReduxTests.swift */; };
18 | 841655AA22C02AAE007DF30E /* DataStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 843B2B8722BF494B001E89B6 /* DataStore.swift */; };
19 | 841655AB22C02AAE007DF30E /* User.swift in Sources */ = {isa = PBXBuildFile; fileRef = 843B2B6822BF1E35001E89B6 /* User.swift */; };
20 | 841655AC22C02AAE007DF30E /* UserResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 843B2B6922BF1E35001E89B6 /* UserResponse.swift */; };
21 | 841655AD22C02AAE007DF30E /* Task.swift in Sources */ = {isa = PBXBuildFile; fileRef = 843B2B6A22BF1E35001E89B6 /* Task.swift */; };
22 | 841655AE22C02AAE007DF30E /* TaskResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 843B2B8822BF494B001E89B6 /* TaskResponse.swift */; };
23 | 841655B622C1E2CF007DF30E /* KeyboardObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 841655B422C1E2CF007DF30E /* KeyboardObserver.swift */; };
24 | 841655B722C1E2CF007DF30E /* FormButtons.swift in Sources */ = {isa = PBXBuildFile; fileRef = 841655B522C1E2CF007DF30E /* FormButtons.swift */; };
25 | 841655B922C28590007DF30E /* TaskCreate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 841655B822C28590007DF30E /* TaskCreate.swift */; };
26 | 841655BB22C301AF007DF30E /* UserCreate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 841655BA22C301AF007DF30E /* UserCreate.swift */; };
27 | 843B2B4B22BE9B1D001E89B6 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 843B2B4A22BE9B1D001E89B6 /* AppDelegate.swift */; };
28 | 843B2B4D22BE9B1D001E89B6 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 843B2B4C22BE9B1D001E89B6 /* SceneDelegate.swift */; };
29 | 843B2B5122BE9B1F001E89B6 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 843B2B5022BE9B1F001E89B6 /* Assets.xcassets */; };
30 | 843B2B5422BE9B1F001E89B6 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 843B2B5322BE9B1F001E89B6 /* Preview Assets.xcassets */; };
31 | 843B2B5722BE9B1F001E89B6 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 843B2B5522BE9B1F001E89B6 /* LaunchScreen.storyboard */; };
32 | 843B2B6B22BF1E35001E89B6 /* User.swift in Sources */ = {isa = PBXBuildFile; fileRef = 843B2B6822BF1E35001E89B6 /* User.swift */; };
33 | 843B2B6C22BF1E35001E89B6 /* UserResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 843B2B6922BF1E35001E89B6 /* UserResponse.swift */; };
34 | 843B2B6D22BF1E35001E89B6 /* Task.swift in Sources */ = {isa = PBXBuildFile; fileRef = 843B2B6A22BF1E35001E89B6 /* Task.swift */; };
35 | 843B2B7C22BF1EB7001E89B6 /* AppState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 843B2B6F22BF1EB7001E89B6 /* AppState.swift */; };
36 | 843B2B7D22BF1EB7001E89B6 /* Reducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 843B2B7122BF1EB7001E89B6 /* Reducer.swift */; };
37 | 843B2B7E22BF1EB7001E89B6 /* UserStateReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 843B2B7222BF1EB7001E89B6 /* UserStateReducer.swift */; };
38 | 843B2B7F22BF1EB7001E89B6 /* TaskStateReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 843B2B7322BF1EB7001E89B6 /* TaskStateReducer.swift */; };
39 | 843B2B8022BF1EB7001E89B6 /* TaskActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 843B2B7522BF1EB7001E89B6 /* TaskActions.swift */; };
40 | 843B2B8122BF1EB7001E89B6 /* Action.swift in Sources */ = {isa = PBXBuildFile; fileRef = 843B2B7622BF1EB7001E89B6 /* Action.swift */; };
41 | 843B2B8222BF1EB7001E89B6 /* UserActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 843B2B7722BF1EB7001E89B6 /* UserActions.swift */; };
42 | 843B2B8322BF1EB7001E89B6 /* UsersState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 843B2B7922BF1EB7001E89B6 /* UsersState.swift */; };
43 | 843B2B8422BF1EB7001E89B6 /* FluxState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 843B2B7A22BF1EB7001E89B6 /* FluxState.swift */; };
44 | 843B2B8522BF1EB7001E89B6 /* TasksState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 843B2B7B22BF1EB7001E89B6 /* TasksState.swift */; };
45 | 843B2B8922BF494B001E89B6 /* DataStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 843B2B8722BF494B001E89B6 /* DataStore.swift */; };
46 | 843B2B8A22BF494B001E89B6 /* TaskResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 843B2B8822BF494B001E89B6 /* TaskResponse.swift */; };
47 | /* End PBXBuildFile section */
48 |
49 | /* Begin PBXContainerItemProxy section */
50 | 841655A522C029E6007DF30E /* PBXContainerItemProxy */ = {
51 | isa = PBXContainerItemProxy;
52 | containerPortal = 843B2B3F22BE9B1D001E89B6 /* Project object */;
53 | proxyType = 1;
54 | remoteGlobalIDString = 843B2B4622BE9B1D001E89B6;
55 | remoteInfo = "SwiftUI-Todo-Redux";
56 | };
57 | /* End PBXContainerItemProxy section */
58 |
59 | /* Begin PBXFileReference section */
60 | 8416558E22BFF4EE007DF30E /* HomeView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = ""; };
61 | 8416559022BFF7C5007DF30E /* TaskDetail.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TaskDetail.swift; sourceTree = ""; };
62 | 8416559122BFF7C5007DF30E /* UsersRow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UsersRow.swift; sourceTree = ""; };
63 | 8416559222BFF7C5007DF30E /* TasksRow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TasksRow.swift; sourceTree = ""; };
64 | 8416559322BFF7C5007DF30E /* UserDetail.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserDetail.swift; sourceTree = ""; };
65 | 8416559422BFF7C6007DF30E /* UsersList.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UsersList.swift; sourceTree = ""; };
66 | 8416559522BFF7C6007DF30E /* TasksList.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TasksList.swift; sourceTree = ""; };
67 | 841655A022C029E6007DF30E /* SwiftUI-Todo-ReduxTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "SwiftUI-Todo-ReduxTests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
68 | 841655A222C029E6007DF30E /* SwiftUI_Todo_ReduxTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftUI_Todo_ReduxTests.swift; sourceTree = ""; };
69 | 841655A422C029E6007DF30E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
70 | 841655B422C1E2CF007DF30E /* KeyboardObserver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyboardObserver.swift; sourceTree = ""; };
71 | 841655B522C1E2CF007DF30E /* FormButtons.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FormButtons.swift; sourceTree = ""; };
72 | 841655B822C28590007DF30E /* TaskCreate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TaskCreate.swift; sourceTree = ""; };
73 | 841655BA22C301AF007DF30E /* UserCreate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserCreate.swift; sourceTree = ""; };
74 | 843B2B4722BE9B1D001E89B6 /* SwiftUI-Todo-Redux.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "SwiftUI-Todo-Redux.app"; sourceTree = BUILT_PRODUCTS_DIR; };
75 | 843B2B4A22BE9B1D001E89B6 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
76 | 843B2B4C22BE9B1D001E89B6 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; };
77 | 843B2B5022BE9B1F001E89B6 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
78 | 843B2B5322BE9B1F001E89B6 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; };
79 | 843B2B5622BE9B1F001E89B6 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; };
80 | 843B2B5822BE9B1F001E89B6 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
81 | 843B2B6822BF1E35001E89B6 /* User.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = User.swift; sourceTree = ""; };
82 | 843B2B6922BF1E35001E89B6 /* UserResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserResponse.swift; sourceTree = ""; };
83 | 843B2B6A22BF1E35001E89B6 /* Task.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Task.swift; sourceTree = ""; };
84 | 843B2B6F22BF1EB7001E89B6 /* AppState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppState.swift; sourceTree = ""; };
85 | 843B2B7122BF1EB7001E89B6 /* Reducer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Reducer.swift; sourceTree = ""; };
86 | 843B2B7222BF1EB7001E89B6 /* UserStateReducer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserStateReducer.swift; sourceTree = ""; };
87 | 843B2B7322BF1EB7001E89B6 /* TaskStateReducer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TaskStateReducer.swift; sourceTree = ""; };
88 | 843B2B7522BF1EB7001E89B6 /* TaskActions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TaskActions.swift; sourceTree = ""; };
89 | 843B2B7622BF1EB7001E89B6 /* Action.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Action.swift; sourceTree = ""; };
90 | 843B2B7722BF1EB7001E89B6 /* UserActions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserActions.swift; sourceTree = ""; };
91 | 843B2B7922BF1EB7001E89B6 /* UsersState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UsersState.swift; sourceTree = ""; };
92 | 843B2B7A22BF1EB7001E89B6 /* FluxState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FluxState.swift; sourceTree = ""; };
93 | 843B2B7B22BF1EB7001E89B6 /* TasksState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TasksState.swift; sourceTree = ""; };
94 | 843B2B8622BF2983001E89B6 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; };
95 | 843B2B8722BF494B001E89B6 /* DataStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataStore.swift; sourceTree = ""; };
96 | 843B2B8822BF494B001E89B6 /* TaskResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TaskResponse.swift; sourceTree = ""; };
97 | /* End PBXFileReference section */
98 |
99 | /* Begin PBXFrameworksBuildPhase section */
100 | 8416559D22C029E6007DF30E /* Frameworks */ = {
101 | isa = PBXFrameworksBuildPhase;
102 | buildActionMask = 2147483647;
103 | files = (
104 | );
105 | runOnlyForDeploymentPostprocessing = 0;
106 | };
107 | 843B2B4422BE9B1D001E89B6 /* Frameworks */ = {
108 | isa = PBXFrameworksBuildPhase;
109 | buildActionMask = 2147483647;
110 | files = (
111 | );
112 | runOnlyForDeploymentPostprocessing = 0;
113 | };
114 | /* End PBXFrameworksBuildPhase section */
115 |
116 | /* Begin PBXGroup section */
117 | 841655A122C029E6007DF30E /* SwiftUI-Todo-ReduxTests */ = {
118 | isa = PBXGroup;
119 | children = (
120 | 841655A222C029E6007DF30E /* SwiftUI_Todo_ReduxTests.swift */,
121 | 841655A422C029E6007DF30E /* Info.plist */,
122 | );
123 | path = "SwiftUI-Todo-ReduxTests";
124 | sourceTree = "";
125 | };
126 | 841655AF22C140B4007DF30E /* tasks */ = {
127 | isa = PBXGroup;
128 | children = (
129 | 8416559522BFF7C6007DF30E /* TasksList.swift */,
130 | 8416559022BFF7C5007DF30E /* TaskDetail.swift */,
131 | 841655B822C28590007DF30E /* TaskCreate.swift */,
132 | 8416559222BFF7C5007DF30E /* TasksRow.swift */,
133 | );
134 | path = tasks;
135 | sourceTree = "";
136 | };
137 | 841655B022C140FE007DF30E /* users */ = {
138 | isa = PBXGroup;
139 | children = (
140 | 8416559422BFF7C6007DF30E /* UsersList.swift */,
141 | 8416559322BFF7C5007DF30E /* UserDetail.swift */,
142 | 841655BA22C301AF007DF30E /* UserCreate.swift */,
143 | 8416559122BFF7C5007DF30E /* UsersRow.swift */,
144 | );
145 | path = users;
146 | sourceTree = "";
147 | };
148 | 841655B322C1E2CF007DF30E /* common */ = {
149 | isa = PBXGroup;
150 | children = (
151 | 841655B422C1E2CF007DF30E /* KeyboardObserver.swift */,
152 | 841655B522C1E2CF007DF30E /* FormButtons.swift */,
153 | );
154 | path = common;
155 | sourceTree = "";
156 | };
157 | 843B2B3E22BE9B1D001E89B6 = {
158 | isa = PBXGroup;
159 | children = (
160 | 843B2B8622BF2983001E89B6 /* README.md */,
161 | 843B2B4922BE9B1D001E89B6 /* SwiftUI-Todo-Redux */,
162 | 841655A122C029E6007DF30E /* SwiftUI-Todo-ReduxTests */,
163 | 843B2B4822BE9B1D001E89B6 /* Products */,
164 | );
165 | sourceTree = "";
166 | };
167 | 843B2B4822BE9B1D001E89B6 /* Products */ = {
168 | isa = PBXGroup;
169 | children = (
170 | 843B2B4722BE9B1D001E89B6 /* SwiftUI-Todo-Redux.app */,
171 | 841655A022C029E6007DF30E /* SwiftUI-Todo-ReduxTests.xctest */,
172 | );
173 | name = Products;
174 | sourceTree = "";
175 | };
176 | 843B2B4922BE9B1D001E89B6 /* SwiftUI-Todo-Redux */ = {
177 | isa = PBXGroup;
178 | children = (
179 | 843B2B4A22BE9B1D001E89B6 /* AppDelegate.swift */,
180 | 843B2B4C22BE9B1D001E89B6 /* SceneDelegate.swift */,
181 | 843B2B6422BF1E1D001E89B6 /* views */,
182 | 843B2B6722BF1E35001E89B6 /* models */,
183 | 843B2B6E22BF1EB7001E89B6 /* states */,
184 | 843B2B5522BE9B1F001E89B6 /* LaunchScreen.storyboard */,
185 | 843B2B5022BE9B1F001E89B6 /* Assets.xcassets */,
186 | 843B2B5822BE9B1F001E89B6 /* Info.plist */,
187 | 843B2B5222BE9B1F001E89B6 /* Preview Content */,
188 | );
189 | path = "SwiftUI-Todo-Redux";
190 | sourceTree = "";
191 | };
192 | 843B2B5222BE9B1F001E89B6 /* Preview Content */ = {
193 | isa = PBXGroup;
194 | children = (
195 | 843B2B5322BE9B1F001E89B6 /* Preview Assets.xcassets */,
196 | );
197 | path = "Preview Content";
198 | sourceTree = "";
199 | };
200 | 843B2B6422BF1E1D001E89B6 /* views */ = {
201 | isa = PBXGroup;
202 | children = (
203 | 8416558E22BFF4EE007DF30E /* HomeView.swift */,
204 | 841655B322C1E2CF007DF30E /* common */,
205 | 841655AF22C140B4007DF30E /* tasks */,
206 | 841655B022C140FE007DF30E /* users */,
207 | );
208 | path = views;
209 | sourceTree = "";
210 | };
211 | 843B2B6722BF1E35001E89B6 /* models */ = {
212 | isa = PBXGroup;
213 | children = (
214 | 843B2B8722BF494B001E89B6 /* DataStore.swift */,
215 | 843B2B6822BF1E35001E89B6 /* User.swift */,
216 | 843B2B6922BF1E35001E89B6 /* UserResponse.swift */,
217 | 843B2B6A22BF1E35001E89B6 /* Task.swift */,
218 | 843B2B8822BF494B001E89B6 /* TaskResponse.swift */,
219 | );
220 | path = models;
221 | sourceTree = "";
222 | };
223 | 843B2B6E22BF1EB7001E89B6 /* states */ = {
224 | isa = PBXGroup;
225 | children = (
226 | 843B2B6F22BF1EB7001E89B6 /* AppState.swift */,
227 | 843B2B7422BF1EB7001E89B6 /* actions */,
228 | 843B2B7022BF1EB7001E89B6 /* reducers-statemachine */,
229 | 843B2B7822BF1EB7001E89B6 /* flux-substate */,
230 | );
231 | name = states;
232 | path = "SwiftUI-Todo-Redux/states";
233 | sourceTree = SOURCE_ROOT;
234 | };
235 | 843B2B7022BF1EB7001E89B6 /* reducers-statemachine */ = {
236 | isa = PBXGroup;
237 | children = (
238 | 843B2B7122BF1EB7001E89B6 /* Reducer.swift */,
239 | 843B2B7222BF1EB7001E89B6 /* UserStateReducer.swift */,
240 | 843B2B7322BF1EB7001E89B6 /* TaskStateReducer.swift */,
241 | );
242 | path = "reducers-statemachine";
243 | sourceTree = "";
244 | };
245 | 843B2B7422BF1EB7001E89B6 /* actions */ = {
246 | isa = PBXGroup;
247 | children = (
248 | 843B2B7622BF1EB7001E89B6 /* Action.swift */,
249 | 843B2B7722BF1EB7001E89B6 /* UserActions.swift */,
250 | 843B2B7522BF1EB7001E89B6 /* TaskActions.swift */,
251 | );
252 | path = actions;
253 | sourceTree = "";
254 | };
255 | 843B2B7822BF1EB7001E89B6 /* flux-substate */ = {
256 | isa = PBXGroup;
257 | children = (
258 | 843B2B7A22BF1EB7001E89B6 /* FluxState.swift */,
259 | 843B2B7922BF1EB7001E89B6 /* UsersState.swift */,
260 | 843B2B7B22BF1EB7001E89B6 /* TasksState.swift */,
261 | );
262 | path = "flux-substate";
263 | sourceTree = "";
264 | };
265 | /* End PBXGroup section */
266 |
267 | /* Begin PBXNativeTarget section */
268 | 8416559F22C029E6007DF30E /* SwiftUI-Todo-ReduxTests */ = {
269 | isa = PBXNativeTarget;
270 | buildConfigurationList = 841655A722C029E6007DF30E /* Build configuration list for PBXNativeTarget "SwiftUI-Todo-ReduxTests" */;
271 | buildPhases = (
272 | 8416559C22C029E6007DF30E /* Sources */,
273 | 8416559D22C029E6007DF30E /* Frameworks */,
274 | 8416559E22C029E6007DF30E /* Resources */,
275 | );
276 | buildRules = (
277 | );
278 | dependencies = (
279 | 841655A622C029E6007DF30E /* PBXTargetDependency */,
280 | );
281 | name = "SwiftUI-Todo-ReduxTests";
282 | productName = "SwiftUI-Todo-ReduxTests";
283 | productReference = 841655A022C029E6007DF30E /* SwiftUI-Todo-ReduxTests.xctest */;
284 | productType = "com.apple.product-type.bundle.unit-test";
285 | };
286 | 843B2B4622BE9B1D001E89B6 /* SwiftUI-Todo-Redux */ = {
287 | isa = PBXNativeTarget;
288 | buildConfigurationList = 843B2B5B22BE9B1F001E89B6 /* Build configuration list for PBXNativeTarget "SwiftUI-Todo-Redux" */;
289 | buildPhases = (
290 | 843B2B4322BE9B1D001E89B6 /* Sources */,
291 | 843B2B4422BE9B1D001E89B6 /* Frameworks */,
292 | 843B2B4522BE9B1D001E89B6 /* Resources */,
293 | );
294 | buildRules = (
295 | );
296 | dependencies = (
297 | );
298 | name = "SwiftUI-Todo-Redux";
299 | productName = "SwiftUI-Todo-Redux";
300 | productReference = 843B2B4722BE9B1D001E89B6 /* SwiftUI-Todo-Redux.app */;
301 | productType = "com.apple.product-type.application";
302 | };
303 | /* End PBXNativeTarget section */
304 |
305 | /* Begin PBXProject section */
306 | 843B2B3F22BE9B1D001E89B6 /* Project object */ = {
307 | isa = PBXProject;
308 | attributes = {
309 | LastSwiftUpdateCheck = 1100;
310 | LastUpgradeCheck = 1100;
311 | ORGANIZATIONNAME = admin;
312 | TargetAttributes = {
313 | 8416559F22C029E6007DF30E = {
314 | CreatedOnToolsVersion = 11.0;
315 | TestTargetID = 843B2B4622BE9B1D001E89B6;
316 | };
317 | 843B2B4622BE9B1D001E89B6 = {
318 | CreatedOnToolsVersion = 11.0;
319 | };
320 | };
321 | };
322 | buildConfigurationList = 843B2B4222BE9B1D001E89B6 /* Build configuration list for PBXProject "SwiftUI-Todo-Redux" */;
323 | compatibilityVersion = "Xcode 9.3";
324 | developmentRegion = en;
325 | hasScannedForEncodings = 0;
326 | knownRegions = (
327 | en,
328 | Base,
329 | );
330 | mainGroup = 843B2B3E22BE9B1D001E89B6;
331 | productRefGroup = 843B2B4822BE9B1D001E89B6 /* Products */;
332 | projectDirPath = "";
333 | projectRoot = "";
334 | targets = (
335 | 843B2B4622BE9B1D001E89B6 /* SwiftUI-Todo-Redux */,
336 | 8416559F22C029E6007DF30E /* SwiftUI-Todo-ReduxTests */,
337 | );
338 | };
339 | /* End PBXProject section */
340 |
341 | /* Begin PBXResourcesBuildPhase section */
342 | 8416559E22C029E6007DF30E /* Resources */ = {
343 | isa = PBXResourcesBuildPhase;
344 | buildActionMask = 2147483647;
345 | files = (
346 | );
347 | runOnlyForDeploymentPostprocessing = 0;
348 | };
349 | 843B2B4522BE9B1D001E89B6 /* Resources */ = {
350 | isa = PBXResourcesBuildPhase;
351 | buildActionMask = 2147483647;
352 | files = (
353 | 843B2B5722BE9B1F001E89B6 /* LaunchScreen.storyboard in Resources */,
354 | 843B2B5422BE9B1F001E89B6 /* Preview Assets.xcassets in Resources */,
355 | 843B2B5122BE9B1F001E89B6 /* Assets.xcassets in Resources */,
356 | );
357 | runOnlyForDeploymentPostprocessing = 0;
358 | };
359 | /* End PBXResourcesBuildPhase section */
360 |
361 | /* Begin PBXSourcesBuildPhase section */
362 | 8416559C22C029E6007DF30E /* Sources */ = {
363 | isa = PBXSourcesBuildPhase;
364 | buildActionMask = 2147483647;
365 | files = (
366 | 841655A322C029E6007DF30E /* SwiftUI_Todo_ReduxTests.swift in Sources */,
367 | 841655AA22C02AAE007DF30E /* DataStore.swift in Sources */,
368 | 841655AD22C02AAE007DF30E /* Task.swift in Sources */,
369 | 841655AE22C02AAE007DF30E /* TaskResponse.swift in Sources */,
370 | 841655AB22C02AAE007DF30E /* User.swift in Sources */,
371 | 841655AC22C02AAE007DF30E /* UserResponse.swift in Sources */,
372 | );
373 | runOnlyForDeploymentPostprocessing = 0;
374 | };
375 | 843B2B4322BE9B1D001E89B6 /* Sources */ = {
376 | isa = PBXSourcesBuildPhase;
377 | buildActionMask = 2147483647;
378 | files = (
379 | 843B2B8322BF1EB7001E89B6 /* UsersState.swift in Sources */,
380 | 843B2B6C22BF1E35001E89B6 /* UserResponse.swift in Sources */,
381 | 843B2B8922BF494B001E89B6 /* DataStore.swift in Sources */,
382 | 843B2B8522BF1EB7001E89B6 /* TasksState.swift in Sources */,
383 | 8416559822BFF7C6007DF30E /* TasksRow.swift in Sources */,
384 | 843B2B8122BF1EB7001E89B6 /* Action.swift in Sources */,
385 | 843B2B8022BF1EB7001E89B6 /* TaskActions.swift in Sources */,
386 | 843B2B8422BF1EB7001E89B6 /* FluxState.swift in Sources */,
387 | 843B2B7F22BF1EB7001E89B6 /* TaskStateReducer.swift in Sources */,
388 | 843B2B7E22BF1EB7001E89B6 /* UserStateReducer.swift in Sources */,
389 | 843B2B8A22BF494B001E89B6 /* TaskResponse.swift in Sources */,
390 | 843B2B8222BF1EB7001E89B6 /* UserActions.swift in Sources */,
391 | 8416559B22BFF7C6007DF30E /* TasksList.swift in Sources */,
392 | 841655B722C1E2CF007DF30E /* FormButtons.swift in Sources */,
393 | 841655BB22C301AF007DF30E /* UserCreate.swift in Sources */,
394 | 8416559A22BFF7C6007DF30E /* UsersList.swift in Sources */,
395 | 841655B622C1E2CF007DF30E /* KeyboardObserver.swift in Sources */,
396 | 843B2B6B22BF1E35001E89B6 /* User.swift in Sources */,
397 | 843B2B7D22BF1EB7001E89B6 /* Reducer.swift in Sources */,
398 | 843B2B7C22BF1EB7001E89B6 /* AppState.swift in Sources */,
399 | 8416559622BFF7C6007DF30E /* TaskDetail.swift in Sources */,
400 | 843B2B4B22BE9B1D001E89B6 /* AppDelegate.swift in Sources */,
401 | 843B2B4D22BE9B1D001E89B6 /* SceneDelegate.swift in Sources */,
402 | 843B2B6D22BF1E35001E89B6 /* Task.swift in Sources */,
403 | 8416558F22BFF4EF007DF30E /* HomeView.swift in Sources */,
404 | 8416559722BFF7C6007DF30E /* UsersRow.swift in Sources */,
405 | 841655B922C28590007DF30E /* TaskCreate.swift in Sources */,
406 | 8416559922BFF7C6007DF30E /* UserDetail.swift in Sources */,
407 | );
408 | runOnlyForDeploymentPostprocessing = 0;
409 | };
410 | /* End PBXSourcesBuildPhase section */
411 |
412 | /* Begin PBXTargetDependency section */
413 | 841655A622C029E6007DF30E /* PBXTargetDependency */ = {
414 | isa = PBXTargetDependency;
415 | target = 843B2B4622BE9B1D001E89B6 /* SwiftUI-Todo-Redux */;
416 | targetProxy = 841655A522C029E6007DF30E /* PBXContainerItemProxy */;
417 | };
418 | /* End PBXTargetDependency section */
419 |
420 | /* Begin PBXVariantGroup section */
421 | 843B2B5522BE9B1F001E89B6 /* LaunchScreen.storyboard */ = {
422 | isa = PBXVariantGroup;
423 | children = (
424 | 843B2B5622BE9B1F001E89B6 /* Base */,
425 | );
426 | name = LaunchScreen.storyboard;
427 | sourceTree = "";
428 | };
429 | /* End PBXVariantGroup section */
430 |
431 | /* Begin XCBuildConfiguration section */
432 | 841655A822C029E6007DF30E /* Debug */ = {
433 | isa = XCBuildConfiguration;
434 | buildSettings = {
435 | BUNDLE_LOADER = "$(TEST_HOST)";
436 | CODE_SIGN_STYLE = Automatic;
437 | INFOPLIST_FILE = "SwiftUI-Todo-ReduxTests/Info.plist";
438 | LD_RUNPATH_SEARCH_PATHS = (
439 | "$(inherited)",
440 | "@executable_path/Frameworks",
441 | "@loader_path/Frameworks",
442 | );
443 | PRODUCT_BUNDLE_IDENTIFIER = "com.demo.SwiftUI-Todo-ReduxTests";
444 | PRODUCT_NAME = "$(TARGET_NAME)";
445 | SWIFT_VERSION = 5.0;
446 | TARGETED_DEVICE_FAMILY = "1,2";
447 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/SwiftUI-Todo-Redux.app/SwiftUI-Todo-Redux";
448 | };
449 | name = Debug;
450 | };
451 | 841655A922C029E6007DF30E /* Release */ = {
452 | isa = XCBuildConfiguration;
453 | buildSettings = {
454 | BUNDLE_LOADER = "$(TEST_HOST)";
455 | CODE_SIGN_STYLE = Automatic;
456 | INFOPLIST_FILE = "SwiftUI-Todo-ReduxTests/Info.plist";
457 | LD_RUNPATH_SEARCH_PATHS = (
458 | "$(inherited)",
459 | "@executable_path/Frameworks",
460 | "@loader_path/Frameworks",
461 | );
462 | PRODUCT_BUNDLE_IDENTIFIER = "com.demo.SwiftUI-Todo-ReduxTests";
463 | PRODUCT_NAME = "$(TARGET_NAME)";
464 | SWIFT_VERSION = 5.0;
465 | TARGETED_DEVICE_FAMILY = "1,2";
466 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/SwiftUI-Todo-Redux.app/SwiftUI-Todo-Redux";
467 | };
468 | name = Release;
469 | };
470 | 843B2B5922BE9B1F001E89B6 /* Debug */ = {
471 | isa = XCBuildConfiguration;
472 | buildSettings = {
473 | ALWAYS_SEARCH_USER_PATHS = NO;
474 | CLANG_ANALYZER_NONNULL = YES;
475 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
476 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
477 | CLANG_CXX_LIBRARY = "libc++";
478 | CLANG_ENABLE_MODULES = YES;
479 | CLANG_ENABLE_OBJC_ARC = YES;
480 | CLANG_ENABLE_OBJC_WEAK = YES;
481 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
482 | CLANG_WARN_BOOL_CONVERSION = YES;
483 | CLANG_WARN_COMMA = YES;
484 | CLANG_WARN_CONSTANT_CONVERSION = YES;
485 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
486 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
487 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
488 | CLANG_WARN_EMPTY_BODY = YES;
489 | CLANG_WARN_ENUM_CONVERSION = YES;
490 | CLANG_WARN_INFINITE_RECURSION = YES;
491 | CLANG_WARN_INT_CONVERSION = YES;
492 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
493 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
494 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
495 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
496 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
497 | CLANG_WARN_STRICT_PROTOTYPES = YES;
498 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
499 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
500 | CLANG_WARN_UNREACHABLE_CODE = YES;
501 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
502 | COPY_PHASE_STRIP = NO;
503 | DEBUG_INFORMATION_FORMAT = dwarf;
504 | ENABLE_STRICT_OBJC_MSGSEND = YES;
505 | ENABLE_TESTABILITY = YES;
506 | GCC_C_LANGUAGE_STANDARD = gnu11;
507 | GCC_DYNAMIC_NO_PIC = NO;
508 | GCC_NO_COMMON_BLOCKS = YES;
509 | GCC_OPTIMIZATION_LEVEL = 0;
510 | GCC_PREPROCESSOR_DEFINITIONS = (
511 | "DEBUG=1",
512 | "$(inherited)",
513 | );
514 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
515 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
516 | GCC_WARN_UNDECLARED_SELECTOR = YES;
517 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
518 | GCC_WARN_UNUSED_FUNCTION = YES;
519 | GCC_WARN_UNUSED_VARIABLE = YES;
520 | IPHONEOS_DEPLOYMENT_TARGET = 13.0;
521 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
522 | MTL_FAST_MATH = YES;
523 | ONLY_ACTIVE_ARCH = YES;
524 | SDKROOT = iphoneos;
525 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
526 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
527 | };
528 | name = Debug;
529 | };
530 | 843B2B5A22BE9B1F001E89B6 /* Release */ = {
531 | isa = XCBuildConfiguration;
532 | buildSettings = {
533 | ALWAYS_SEARCH_USER_PATHS = NO;
534 | CLANG_ANALYZER_NONNULL = YES;
535 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
536 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
537 | CLANG_CXX_LIBRARY = "libc++";
538 | CLANG_ENABLE_MODULES = YES;
539 | CLANG_ENABLE_OBJC_ARC = YES;
540 | CLANG_ENABLE_OBJC_WEAK = YES;
541 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
542 | CLANG_WARN_BOOL_CONVERSION = YES;
543 | CLANG_WARN_COMMA = YES;
544 | CLANG_WARN_CONSTANT_CONVERSION = YES;
545 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
546 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
547 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
548 | CLANG_WARN_EMPTY_BODY = YES;
549 | CLANG_WARN_ENUM_CONVERSION = YES;
550 | CLANG_WARN_INFINITE_RECURSION = YES;
551 | CLANG_WARN_INT_CONVERSION = YES;
552 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
553 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
554 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
555 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
556 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
557 | CLANG_WARN_STRICT_PROTOTYPES = YES;
558 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
559 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
560 | CLANG_WARN_UNREACHABLE_CODE = YES;
561 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
562 | COPY_PHASE_STRIP = NO;
563 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
564 | ENABLE_NS_ASSERTIONS = NO;
565 | ENABLE_STRICT_OBJC_MSGSEND = YES;
566 | GCC_C_LANGUAGE_STANDARD = gnu11;
567 | GCC_NO_COMMON_BLOCKS = YES;
568 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
569 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
570 | GCC_WARN_UNDECLARED_SELECTOR = YES;
571 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
572 | GCC_WARN_UNUSED_FUNCTION = YES;
573 | GCC_WARN_UNUSED_VARIABLE = YES;
574 | IPHONEOS_DEPLOYMENT_TARGET = 13.0;
575 | MTL_ENABLE_DEBUG_INFO = NO;
576 | MTL_FAST_MATH = YES;
577 | SDKROOT = iphoneos;
578 | SWIFT_COMPILATION_MODE = wholemodule;
579 | SWIFT_OPTIMIZATION_LEVEL = "-O";
580 | VALIDATE_PRODUCT = YES;
581 | };
582 | name = Release;
583 | };
584 | 843B2B5C22BE9B1F001E89B6 /* Debug */ = {
585 | isa = XCBuildConfiguration;
586 | buildSettings = {
587 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
588 | CODE_SIGN_STYLE = Automatic;
589 | DEVELOPMENT_ASSET_PATHS = "SwiftUI-Todo-Redux/Preview\\ Content";
590 | ENABLE_PREVIEWS = YES;
591 | INFOPLIST_FILE = "SwiftUI-Todo-Redux/Info.plist";
592 | LD_RUNPATH_SEARCH_PATHS = (
593 | "$(inherited)",
594 | "@executable_path/Frameworks",
595 | );
596 | PRODUCT_BUNDLE_IDENTIFIER = "com.demo.SwiftUI-Todo-Redux";
597 | PRODUCT_NAME = "$(TARGET_NAME)";
598 | SWIFT_VERSION = 5.0;
599 | TARGETED_DEVICE_FAMILY = "1,2";
600 | };
601 | name = Debug;
602 | };
603 | 843B2B5D22BE9B1F001E89B6 /* Release */ = {
604 | isa = XCBuildConfiguration;
605 | buildSettings = {
606 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
607 | CODE_SIGN_STYLE = Automatic;
608 | DEVELOPMENT_ASSET_PATHS = "SwiftUI-Todo-Redux/Preview\\ Content";
609 | ENABLE_PREVIEWS = YES;
610 | INFOPLIST_FILE = "SwiftUI-Todo-Redux/Info.plist";
611 | LD_RUNPATH_SEARCH_PATHS = (
612 | "$(inherited)",
613 | "@executable_path/Frameworks",
614 | );
615 | PRODUCT_BUNDLE_IDENTIFIER = "com.demo.SwiftUI-Todo-Redux";
616 | PRODUCT_NAME = "$(TARGET_NAME)";
617 | SWIFT_VERSION = 5.0;
618 | TARGETED_DEVICE_FAMILY = "1,2";
619 | };
620 | name = Release;
621 | };
622 | /* End XCBuildConfiguration section */
623 |
624 | /* Begin XCConfigurationList section */
625 | 841655A722C029E6007DF30E /* Build configuration list for PBXNativeTarget "SwiftUI-Todo-ReduxTests" */ = {
626 | isa = XCConfigurationList;
627 | buildConfigurations = (
628 | 841655A822C029E6007DF30E /* Debug */,
629 | 841655A922C029E6007DF30E /* Release */,
630 | );
631 | defaultConfigurationIsVisible = 0;
632 | defaultConfigurationName = Release;
633 | };
634 | 843B2B4222BE9B1D001E89B6 /* Build configuration list for PBXProject "SwiftUI-Todo-Redux" */ = {
635 | isa = XCConfigurationList;
636 | buildConfigurations = (
637 | 843B2B5922BE9B1F001E89B6 /* Debug */,
638 | 843B2B5A22BE9B1F001E89B6 /* Release */,
639 | );
640 | defaultConfigurationIsVisible = 0;
641 | defaultConfigurationName = Release;
642 | };
643 | 843B2B5B22BE9B1F001E89B6 /* Build configuration list for PBXNativeTarget "SwiftUI-Todo-Redux" */ = {
644 | isa = XCConfigurationList;
645 | buildConfigurations = (
646 | 843B2B5C22BE9B1F001E89B6 /* Debug */,
647 | 843B2B5D22BE9B1F001E89B6 /* Release */,
648 | );
649 | defaultConfigurationIsVisible = 0;
650 | defaultConfigurationName = Release;
651 | };
652 | /* End XCConfigurationList section */
653 | };
654 | rootObject = 843B2B3F22BE9B1D001E89B6 /* Project object */;
655 | }
656 |
--------------------------------------------------------------------------------
/SwiftUI-Todo-Redux.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/SwiftUI-Todo-Redux.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/SwiftUI-Todo-Redux/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // SwiftUI-Todo-Redux
4 | //
5 | // Created by moflo on 6/22/19.
6 | // Copyright © 2019 Mobile Flow LLC. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | @UIApplicationMain
12 | class AppDelegate: UIResponder, UIApplicationDelegate {
13 | func application(_: UIApplication, didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
14 | // Override point for customization after application launch.
15 | return true
16 | }
17 |
18 | func applicationWillTerminate(_: UIApplication) {
19 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
20 | }
21 |
22 | // MARK: UISceneSession Lifecycle
23 |
24 | func application(_: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options _: UIScene.ConnectionOptions) -> UISceneConfiguration {
25 | // Called when a new scene session is being created.
26 | // Use this method to select a configuration to create the new scene with.
27 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
28 | }
29 |
30 | func application(_: UIApplication, didDiscardSceneSessions _: Set) {
31 | // Called when the user discards a scene session.
32 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
33 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return.
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/SwiftUI-Todo-Redux/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "iphone",
5 | "size" : "20x20",
6 | "scale" : "2x"
7 | },
8 | {
9 | "idiom" : "iphone",
10 | "size" : "20x20",
11 | "scale" : "3x"
12 | },
13 | {
14 | "idiom" : "iphone",
15 | "size" : "29x29",
16 | "scale" : "2x"
17 | },
18 | {
19 | "idiom" : "iphone",
20 | "size" : "29x29",
21 | "scale" : "3x"
22 | },
23 | {
24 | "idiom" : "iphone",
25 | "size" : "40x40",
26 | "scale" : "2x"
27 | },
28 | {
29 | "idiom" : "iphone",
30 | "size" : "40x40",
31 | "scale" : "3x"
32 | },
33 | {
34 | "idiom" : "iphone",
35 | "size" : "60x60",
36 | "scale" : "2x"
37 | },
38 | {
39 | "idiom" : "iphone",
40 | "size" : "60x60",
41 | "scale" : "3x"
42 | },
43 | {
44 | "idiom" : "ipad",
45 | "size" : "20x20",
46 | "scale" : "1x"
47 | },
48 | {
49 | "idiom" : "ipad",
50 | "size" : "20x20",
51 | "scale" : "2x"
52 | },
53 | {
54 | "idiom" : "ipad",
55 | "size" : "29x29",
56 | "scale" : "1x"
57 | },
58 | {
59 | "idiom" : "ipad",
60 | "size" : "29x29",
61 | "scale" : "2x"
62 | },
63 | {
64 | "idiom" : "ipad",
65 | "size" : "40x40",
66 | "scale" : "1x"
67 | },
68 | {
69 | "idiom" : "ipad",
70 | "size" : "40x40",
71 | "scale" : "2x"
72 | },
73 | {
74 | "idiom" : "ipad",
75 | "size" : "76x76",
76 | "scale" : "1x"
77 | },
78 | {
79 | "idiom" : "ipad",
80 | "size" : "76x76",
81 | "scale" : "2x"
82 | },
83 | {
84 | "idiom" : "ipad",
85 | "size" : "83.5x83.5",
86 | "scale" : "2x"
87 | },
88 | {
89 | "idiom" : "ios-marketing",
90 | "size" : "1024x1024",
91 | "scale" : "1x"
92 | }
93 | ],
94 | "info" : {
95 | "version" : 1,
96 | "author" : "xcode"
97 | }
98 | }
--------------------------------------------------------------------------------
/SwiftUI-Todo-Redux/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/SwiftUI-Todo-Redux/Assets.xcassets/form-field-background.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | },
6 | "colors" : [
7 | {
8 | "idiom" : "universal",
9 | "color" : {
10 | "color-space" : "srgb",
11 | "components" : {
12 | "red" : "0xAA",
13 | "alpha" : "1.000",
14 | "blue" : "0xAA",
15 | "green" : "0xAA"
16 | }
17 | }
18 | },
19 | {
20 | "idiom" : "universal",
21 | "appearances" : [
22 | {
23 | "appearance" : "luminosity",
24 | "value" : "light"
25 | }
26 | ],
27 | "color" : {
28 | "color-space" : "srgb",
29 | "components" : {
30 | "red" : "0xAA",
31 | "alpha" : "1.000",
32 | "blue" : "0xAA",
33 | "green" : "0xAA"
34 | }
35 | }
36 | },
37 | {
38 | "idiom" : "universal",
39 | "appearances" : [
40 | {
41 | "appearance" : "luminosity",
42 | "value" : "dark"
43 | }
44 | ],
45 | "color" : {
46 | "color-space" : "srgb",
47 | "components" : {
48 | "red" : "0x66",
49 | "alpha" : "1.000",
50 | "blue" : "0x66",
51 | "green" : "0x66"
52 | }
53 | }
54 | }
55 | ]
56 | }
--------------------------------------------------------------------------------
/SwiftUI-Todo-Redux/Assets.xcassets/tab_task.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "second.pdf",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "version" : 1,
19 | "author" : "xcode"
20 | }
21 | }
--------------------------------------------------------------------------------
/SwiftUI-Todo-Redux/Assets.xcassets/tab_task.imageset/second.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/moflo/SwiftUI-Todo-Redux/cc06b44f1f5c20afee2f9f3d33e6ce5cbe283b55/SwiftUI-Todo-Redux/Assets.xcassets/tab_task.imageset/second.pdf
--------------------------------------------------------------------------------
/SwiftUI-Todo-Redux/Assets.xcassets/tab_user.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "first.pdf",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "version" : 1,
19 | "author" : "xcode"
20 | }
21 | }
--------------------------------------------------------------------------------
/SwiftUI-Todo-Redux/Assets.xcassets/tab_user.imageset/first.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/moflo/SwiftUI-Todo-Redux/cc06b44f1f5c20afee2f9f3d33e6ce5cbe283b55/SwiftUI-Todo-Redux/Assets.xcassets/tab_user.imageset/first.pdf
--------------------------------------------------------------------------------
/SwiftUI-Todo-Redux/Base.lproj/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/SwiftUI-Todo-Redux/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 | UISceneConfigurations
28 |
29 | UIWindowSceneSessionRoleApplication
30 |
31 |
32 | UILaunchStoryboardName
33 | LaunchScreen
34 | UISceneConfigurationName
35 | Default Configuration
36 | UISceneDelegateClassName
37 | $(PRODUCT_MODULE_NAME).SceneDelegate
38 |
39 |
40 |
41 |
42 | UILaunchStoryboardName
43 | LaunchScreen
44 | UIRequiredDeviceCapabilities
45 |
46 | armv7
47 |
48 | UISupportedInterfaceOrientations
49 |
50 | UIInterfaceOrientationPortrait
51 | UIInterfaceOrientationLandscapeLeft
52 | UIInterfaceOrientationLandscapeRight
53 |
54 | UISupportedInterfaceOrientations~ipad
55 |
56 | UIInterfaceOrientationPortrait
57 | UIInterfaceOrientationPortraitUpsideDown
58 | UIInterfaceOrientationLandscapeLeft
59 | UIInterfaceOrientationLandscapeRight
60 |
61 |
62 |
63 |
--------------------------------------------------------------------------------
/SwiftUI-Todo-Redux/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/SwiftUI-Todo-Redux/SceneDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SceneDelegate.swift
3 | // SwiftUI-Todo-Redux
4 | //
5 | // Created by moflo on 6/22/19.
6 | // Copyright © 2019 Mobile Flow LLC. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 | import UIKit
11 |
12 | class SceneDelegate: UIResponder, UIWindowSceneDelegate {
13 | var window: UIWindow?
14 |
15 | func scene(_ scene: UIScene, willConnectTo _: UISceneSession, options _: UIScene.ConnectionOptions) {
16 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
17 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
18 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
19 |
20 | // Use a UIHostingController as window root view controller
21 | if let windowScene = scene as? UIWindowScene {
22 | let window = UIWindow(windowScene: windowScene)
23 | // window.rootViewController = UIHostingController(rootView: HomeView())
24 | window.rootViewController = UIHostingController(rootView: HomeView().environmentObject(store))
25 | self.window = window
26 | window.makeKeyAndVisible()
27 | }
28 | }
29 |
30 | func sceneDidDisconnect(_: UIScene) {
31 | // Called as the scene is being released by the system.
32 | // This occurs shortly after the scene enters the background, or when its session is discarded.
33 | // Release any resources associated with this scene that can be re-created the next time the scene connects.
34 | // The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead).
35 | }
36 |
37 | func sceneDidBecomeActive(_: UIScene) {
38 | // Called when the scene has moved from an inactive state to an active state.
39 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive.
40 | }
41 |
42 | func sceneWillResignActive(_: UIScene) {
43 | // Called when the scene will move from an active state to an inactive state.
44 | // This may occur due to temporary interruptions (ex. an incoming phone call).
45 | }
46 |
47 | func sceneWillEnterForeground(_: UIScene) {
48 | // Called as the scene transitions from the background to the foreground.
49 | // Use this method to undo the changes made on entering the background.
50 | }
51 |
52 | func sceneDidEnterBackground(_: UIScene) {
53 | // Called as the scene transitions from the foreground to the background.
54 | // Use this method to save data, release shared resources, and store enough scene-specific state information
55 | // to restore the scene back to its current state.
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/SwiftUI-Todo-Redux/models/DataStore.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DataStore.swift
3 | // SwiftUI-Todo-Redux
4 | //
5 | // Created by moflo on 6/22/19.
6 | // Copyright © 2019 Mobile Flow LLC. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | struct DataStore {
12 | static let shared = DataStore()
13 |
14 | let baseURL = URL(string: "https://www.mocky.io/v2/")!
15 | let apiKey = "5d0ef6093200005700dc694d"
16 | let decoder = JSONDecoder()
17 |
18 | enum APIError: Error {
19 | case noResponse
20 | case jsonDecodingError(error: Error)
21 | case networkError(error: Error)
22 | }
23 |
24 | enum Endpoint {
25 | case users
26 | case user(id: String)
27 | case tasks
28 | case task(id: String)
29 |
30 | func path() -> String {
31 | switch self {
32 | case .users:
33 | return "5d12c9e60e00000d07b4a098"
34 | case let .user(id):
35 | return "5d0f9ee93200006b00dc6a71/\(id)"
36 | case .tasks:
37 | return "5d0ffc7230000096034c9e04"
38 | case let .task(id):
39 | return "5d0fa0773200005c00dc6a80/\(id)"
40 | }
41 | }
42 | }
43 |
44 | func GET(endpoint: Endpoint,
45 | params: [String: String]?,
46 | completionHandler: @escaping (Result) -> Void) {
47 | let queryURL = baseURL.appendingPathComponent(endpoint.path())
48 | var components = URLComponents(url: queryURL, resolvingAgainstBaseURL: true)!
49 | components.queryItems = [
50 | URLQueryItem(name: "api_key", value: apiKey),
51 | ]
52 | if let params = params {
53 | for (_, value) in params.enumerated() {
54 | components.queryItems?.append(URLQueryItem(name: value.key, value: value.value))
55 | }
56 | }
57 | var request = URLRequest(url: components.url!)
58 | request.httpMethod = "GET"
59 | print(request)
60 | let task = URLSession.shared.dataTask(with: request) { data, _, error in
61 | guard let data = data else {
62 | completionHandler(.failure(.noResponse))
63 | return
64 | }
65 | guard error == nil else {
66 | completionHandler(.failure(.networkError(error: error!)))
67 | return
68 | }
69 | do {
70 | let object = try self.decoder.decode(T.self, from: data)
71 | completionHandler(.success(object))
72 | } catch {
73 | print("JSON decoding error (GET)", T.self, error)
74 | completionHandler(.failure(.jsonDecodingError(error: error)))
75 | }
76 | }
77 | task.resume()
78 | }
79 |
80 | func POST(endpoint: Endpoint,
81 | params: [String: String]?,
82 | completionHandler: @escaping (Result) -> Void) {
83 | let queryURL = baseURL.appendingPathComponent(endpoint.path())
84 | var components = URLComponents(url: queryURL, resolvingAgainstBaseURL: true)!
85 | components.queryItems = [
86 | URLQueryItem(name: "api_key", value: apiKey),
87 | ]
88 | if let params = params {
89 | for (_, value) in params.enumerated() {
90 | components.queryItems?.append(URLQueryItem(name: value.key, value: value.value))
91 | }
92 | }
93 | var request = URLRequest(url: components.url!)
94 | request.httpMethod = "POST"
95 | let task = URLSession.shared.dataTask(with: request) { data, _, error in
96 | guard let data = data else {
97 | completionHandler(.failure(.noResponse))
98 | return
99 | }
100 | guard error == nil else {
101 | completionHandler(.failure(.networkError(error: error!)))
102 | return
103 | }
104 | do {
105 | let object = try self.decoder.decode(T.self, from: data)
106 | completionHandler(.success(object))
107 | } catch {
108 | print("JSON decoding error (POST)", T.self, error)
109 | completionHandler(.failure(.jsonDecodingError(error: error)))
110 | }
111 | }
112 | task.resume()
113 | }
114 | }
115 |
--------------------------------------------------------------------------------
/SwiftUI-Todo-Redux/models/Task.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Task.swift
3 | // SwiftUI-Todo-Redux
4 | //
5 | // Created by moflo on 6/22/19.
6 | // Copyright © 2019 Mobile Flow LLC. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | struct Task: Equatable, Hashable, Codable, Identifiable {
12 | let id: String
13 | var title: String
14 | var isDone: Bool
15 | let owner: User?
16 |
17 | init(title: String, isDone: Bool, owner: User? = nil) {
18 | id = UUID().uuidString
19 | self.title = title
20 | self.isDone = isDone
21 | self.owner = owner
22 | }
23 | }
24 |
25 | let testTasksModels = [Task(title: "task 1", isDone: true),
26 | Task(title: "task 2", isDone: false)]
27 |
--------------------------------------------------------------------------------
/SwiftUI-Todo-Redux/models/TaskResponse.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TaskResponse.swift
3 | // SwiftUI-Todo-Redux
4 | //
5 | // Created by moflo on 6/22/19.
6 | // Copyright © 2019 Mobile Flow LLC. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | struct TaskResponseJSON: Codable {
12 | let id: Int
13 | let tasks: [Task]
14 | }
15 |
--------------------------------------------------------------------------------
/SwiftUI-Todo-Redux/models/User.swift:
--------------------------------------------------------------------------------
1 | //
2 | // User.swift
3 | // SwiftUI-Todo-Redux
4 | //
5 | // Created by moflo on 6/22/19.
6 | // Copyright © 2019 Mobile Flow LLC. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | struct User: Equatable, Hashable, Codable, Identifiable {
12 | let id: Int
13 | var name: String
14 | var username: String
15 | let imageName = "person"
16 | }
17 |
18 | let testUsersModels = [User(id: 0, name: "user 1", username: "@user1"),
19 | User(id: 1, name: "user 2", username: "@user2")]
20 |
--------------------------------------------------------------------------------
/SwiftUI-Todo-Redux/models/UserResponse.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UserResponse.swift
3 | // SwiftUI-Todo-Redux
4 | //
5 | // Created by moflo on 6/22/19.
6 | // Copyright © 2019 Mobile Flow LLC. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | struct UserResponseJSON: Codable {
12 | let id: Int
13 | let users: [User]
14 | }
15 |
--------------------------------------------------------------------------------
/SwiftUI-Todo-Redux/states/AppState.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppState.swift
3 | // SwiftUI-Todo-Redux
4 | //
5 | // Created by moflo on 6/22/19.
6 | // Copyright © 2019 Mobile Flow LLC. All rights reserved.
7 | //
8 |
9 | import Combine
10 | import Foundation
11 | import SwiftUI
12 |
13 | final class AppState: ObservableObject {
14 | var didChange = PassthroughSubject()
15 |
16 | var usersState: UsersState
17 | var tasksState: TasksState
18 |
19 | init(usersState: UsersState = UsersState(), tasksState: TasksState = TasksState()) {
20 | self.usersState = usersState
21 | self.tasksState = tasksState
22 | }
23 |
24 | func dispatch(action: Action) {
25 | usersState = UserStateReducer().reduce(state: usersState, action: action)
26 | tasksState = TaskStateReducer().reduce(state: tasksState, action: action)
27 | DispatchQueue.main.async {
28 | self.didChange.send(self)
29 | }
30 | }
31 | }
32 |
33 | // Global Store
34 | let store = AppState()
35 |
36 | #if DEBUG
37 | let sampleStore = AppState(
38 | usersState: UsersState(users: testUsersModels),
39 | tasksState: TasksState(tasks: testTasksModels)
40 | )
41 | #endif
42 |
--------------------------------------------------------------------------------
/SwiftUI-Todo-Redux/states/actions/Action.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Action.swift
3 | // SwiftUI-Todo-Redux
4 | //
5 | // Created by moflo on 6/22/19.
6 | // Copyright © 2019 Mobile Flow LLC. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | protocol Action {}
12 |
--------------------------------------------------------------------------------
/SwiftUI-Todo-Redux/states/actions/TaskActions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TaskActions.swift
3 | // SwiftUI-Todo-Redux
4 | //
5 | // Created by moflo on 6/22/19.
6 | // Copyright © 2019 Mobile Flow LLC. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | struct TaskActions {
12 | struct getTasks: Action {
13 | init() {
14 | DataStore.shared.GET(endpoint: .tasks, params: nil) {
15 | (result: Result) in
16 | switch result {
17 | case let .success(response):
18 | store.dispatch(action: GetTaskResponse(id: 0, response: response))
19 | case .failure:
20 | break
21 | }
22 | }
23 | }
24 | }
25 |
26 | struct deletTask: Action {
27 | init(index _: Int) {}
28 | }
29 |
30 | struct move: Action {
31 | init(from _: Int, to _: Int) {}
32 | }
33 |
34 | struct editTask: Action {
35 | init(id _: Int, name _: String, description _: String, owner _: User) {}
36 | }
37 |
38 | struct markTaskDone: Action {
39 | init(id _: Int) {}
40 | }
41 |
42 | struct testEditBlankTask: Action {
43 | init() {}
44 | }
45 |
46 | struct startEditTask: Action {
47 | init() {}
48 | }
49 |
50 | struct stopEditTask: Action {
51 | init() {}
52 | }
53 |
54 | // MARK: Response Structs
55 |
56 | struct GetTaskResponse: Action {
57 | let id: Int
58 | let response: TaskResponseJSON
59 | }
60 |
61 | struct Notification: Action {
62 | let show: Bool
63 | let message: String
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/SwiftUI-Todo-Redux/states/actions/UserActions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UserActions.swift
3 | // SwiftUI-Todo-Redux
4 | //
5 | // Created by moflo on 6/22/19.
6 | // Copyright © 2019 Mobile Flow LLC. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | struct UserActions {
12 | struct getUsers: Action {
13 | init() {
14 | DataStore.shared.GET(endpoint: .users, params: nil) {
15 | (result: Result) in
16 | switch result {
17 | case let .success(response):
18 | store.dispatch(action: UserAddResponse(id: response.id, response: response))
19 | case .failure:
20 | break
21 | }
22 | }
23 | }
24 | }
25 |
26 | struct deleteUser: Action {
27 | init(index _: Int) {}
28 | }
29 |
30 | struct move: Action {
31 | init(from _: Int, to _: Int) {}
32 | }
33 |
34 | struct editUser: Action {
35 | init(id _: Int, name _: String, username _: String) {}
36 | }
37 |
38 | struct testEditFirstUser: Action {
39 | init() {}
40 | }
41 |
42 | struct startEditUser: Action {
43 | init() {}
44 | }
45 |
46 | struct stopEditUser: Action {
47 | init() {}
48 | }
49 |
50 | // MARK: Response Structs
51 |
52 | struct UserAddResponse: Action {
53 | let id: Int
54 | let response: UserResponseJSON
55 | }
56 |
57 | struct UserDeleteResponse: Action {
58 | let id: Int
59 | let response: UserResponseJSON
60 | }
61 |
62 | struct UserMoveResponse: Action {
63 | let id: Int
64 | let to: Int
65 | let from: Int
66 | let response: UserResponseJSON
67 | }
68 |
69 | struct EditUserResponse: Action {
70 | let id: Int
71 | let response: UserResponseJSON
72 | }
73 |
74 | struct TestEditResponse: Action {
75 | let id: Int
76 | let response: UserResponseJSON
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/SwiftUI-Todo-Redux/states/flux-substate/FluxState.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FluxState.swift
3 | // SwiftUI-Todo-Redux
4 | //
5 | // Created by moflo on 6/22/19.
6 | // Copyright © 2019 Mobile Flow LLC. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | protocol FluxState {}
12 |
--------------------------------------------------------------------------------
/SwiftUI-Todo-Redux/states/flux-substate/TasksState.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TasksState.swift
3 | // SwiftUI-Todo-Redux
4 | //
5 | // Created by moflo on 6/22/19.
6 | // Copyright © 2019 Mobile Flow LLC. All rights reserved.
7 | //
8 |
9 | import Combine
10 | import Foundation
11 | import SwiftUI
12 |
13 | struct TasksState: FluxState {
14 | var tasks: [Task]
15 | var hasTaskError = false
16 | var taskErrorMessage = ""
17 |
18 | init(tasks: [Task] = []) {
19 | self.tasks = tasks
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/SwiftUI-Todo-Redux/states/flux-substate/UsersState.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UsersState.swift
3 | // SwiftUI-Todo-Redux
4 | //
5 | // Created by moflo on 6/22/19.
6 | // Copyright © 2019 Mobile Flow LLC. All rights reserved.
7 | //
8 |
9 | import Combine
10 | import Foundation
11 | import SwiftUI
12 |
13 | struct UsersState: FluxState {
14 | var users: [User]
15 | var isEditingUser = false
16 |
17 | init(users: [User] = []) {
18 | self.users = users
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/SwiftUI-Todo-Redux/states/reducers-statemachine/Reducer.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Reducer.swift
3 | // SwiftUI-Todo-Redux
4 | //
5 | // Created by moflo on 6/22/19.
6 | // Copyright © 2019 Mobile Flow LLC. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | protocol Reducer {
12 | associatedtype StateType: FluxState
13 | func reduce(state: StateType, action: Action) -> StateType
14 | }
15 |
--------------------------------------------------------------------------------
/SwiftUI-Todo-Redux/states/reducers-statemachine/TaskStateReducer.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TaskStateReducer.swift
3 | // SwiftUI-Todo-Redux
4 | //
5 | // Created by moflo on 6/22/19.
6 | // Copyright © 2019 Mobile Flow LLC. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | struct TaskStateReducer: Reducer {
12 | func reduce(state: TasksState, action: Action) -> TasksState {
13 | var state = state
14 |
15 | if let action = action as? TaskActions.GetTaskResponse {
16 | let id = action.id
17 | let tasks = action.response.tasks
18 | state.tasks.append(contentsOf: tasks)
19 | }
20 |
21 | if let action = action as? TaskActions.Notification {
22 | let show = action.show
23 | let message = action.message
24 | state.hasTaskError = show
25 | state.taskErrorMessage = message
26 | }
27 | /*
28 | switch action {
29 |
30 | case TaskActions.addTask:
31 | state.tasks.append(Task(id: state.tasks.count,
32 | name: "New task \(state.tasks.count + 1)",
33 | taskname: "@newtask\(state.tasks.count + 1)"))
34 |
35 | case let TaskActions.deletTask(index):
36 | state.tasks.remove(at: index)
37 |
38 | case let TaskActions.move(from, to):
39 | let task = state.tasks.remove(at: from)
40 | state.tasks.insert(task, at: to)
41 |
42 | case let TaskActions.editTask(id, name, description, owner):
43 | var task = state.tasks[id]
44 | task.name = name
45 | task.taskname = taskname
46 | state.tasks[id] = task
47 |
48 | case let TaskActions.markTaskDone(id):
49 | var task = state.tasks[id]
50 | task.isDone.toggle()
51 |
52 | case TaskActions.testEditBlankTask:
53 | if !state.tasks.isEmpty {
54 | state.tasks[0] = Task(id: 0, name: "task1", taskname: "u\ns\ne\nr\nn\na\nm\ne")
55 | }
56 |
57 | case TaskActions.startEditTask:
58 | state.hasTaskError = true
59 |
60 | case TaskActions.stopEditTask:
61 | state.hasTaskError = false
62 |
63 | default:
64 | break
65 | }
66 | */
67 |
68 | return state
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/SwiftUI-Todo-Redux/states/reducers-statemachine/UserStateReducer.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UserStateReducer.swift
3 | // SwiftUI-Todo-Redux
4 | //
5 | // Created by moflo on 6/22/19.
6 | // Copyright © 2019 Mobile Flow LLC. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | struct UserStateReducer: Reducer {
12 | func reduce(state: UsersState, action: Action) -> UsersState {
13 | var state = state
14 |
15 | if let action = action as? UserActions.UserAddResponse {
16 | let id = action.id
17 | let users = action.response.users
18 | state.users.append(contentsOf: users)
19 | }
20 |
21 | if let action = action as? UserActions.UserDeleteResponse {
22 | let id = action.id
23 | state.users.remove(at: id)
24 | }
25 |
26 | if let action = action as? UserActions.UserMoveResponse {
27 | let from = action.from
28 | let to = action.to
29 | let user = state.users.remove(at: from)
30 | state.users.insert(user, at: to)
31 | }
32 |
33 | if let action = action as? UserActions.EditUserResponse {
34 | let id = action.id
35 | let users = action.response.users
36 | state.users[id] = users[0]
37 | }
38 |
39 | if let action = action as? UserActions.TestEditResponse {
40 | if !state.users.isEmpty {
41 | state.users[0] = User(id: 0, name: "user1", username: "u\ns\ne\nr\nn\na\nm\ne")
42 | }
43 | }
44 |
45 | if let action = action as? UserActions.startEditUser {
46 | state.isEditingUser = true
47 | }
48 |
49 | if let action = action as? UserActions.stopEditUser {
50 | state.isEditingUser = false
51 | }
52 |
53 | return state
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/SwiftUI-Todo-Redux/views/HomeView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HomeView.swift
3 | // SwiftUI-Todo-Redux
4 | //
5 | // Created by moflo on 6/22/19.
6 | // Copyright © 2019 Mobile Flow LLC. All rights reserved.
7 | //
8 |
9 | import Combine
10 | import SwiftUI
11 |
12 | struct HomeView: View {
13 | @EnvironmentObject var store: AppState
14 | @State var selectedTab = Tab.tasks
15 |
16 | enum Tab: Int {
17 | case tasks, users
18 | }
19 |
20 | var body: some View {
21 | ZStack(alignment: .bottom) {
22 | TabView(selection: self.$selectedTab) {
23 | TasksList().tabItem{ VStack { Image("tab_task"); Text("Tasks") } }.tag(Tab.tasks)
24 | UsersList().tabItem{ VStack { Image("tab_user"); Text("Team") } }.tag(Tab.users)
25 | }
26 | .edgesIgnoringSafeArea(.top)
27 |
28 | NotificationBadge(text: "Message goes here", color: .blue, show: $store.tasksState.hasTaskError)
29 | .environmentObject(store)
30 | .padding(.vertical, 66)
31 | }
32 | }
33 | }
34 |
35 | #if DEBUG
36 | struct HomeView_Previews: PreviewProvider {
37 | static var previews: some View {
38 | let sampleStore2 = AppState(
39 | usersState: UsersState(users: testUsersModels),
40 | tasksState: TasksState(tasks: testTasksModels)
41 | )
42 |
43 | sampleStore2.tasksState.hasTaskError = true
44 | sampleStore2.tasksState.taskErrorMessage = "Hello Message"
45 |
46 | return Group {
47 | HomeView().environmentObject(sampleStore)
48 |
49 | HomeView().environmentObject(sampleStore2)
50 | }
51 | }
52 | }
53 | #endif
54 |
--------------------------------------------------------------------------------
/SwiftUI-Todo-Redux/views/common/FormButtons.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FormButtons
3 | // SwiftUI-Todo-Redux
4 | //
5 | // Created by moflo on 6/22/19.
6 | // Copyright © 2019 Mobile Flow LLC. All rights reserved.
7 | //
8 |
9 | import Combine
10 | import SwiftUI
11 |
12 | // Class to respond to editText commit, change responder / focus
13 |
14 | struct FieldSetText: View {
15 | @Binding var textItem: String
16 | var label: String
17 | var placeHolder: String
18 |
19 | var body: some View {
20 | VStack(alignment: .leading) {
21 | Text(label)
22 | .font(.headline)
23 | TextField(placeHolder, text: $textItem)
24 | .padding(.all)
25 | .background(Color("form-field-background")) //, cornerRadius: 5.0)
26 | }
27 | .padding(.horizontal, 15)
28 | }
29 | }
30 |
31 | struct RoundedButton: View {
32 | var body: some View {
33 | Button(action: {}) {
34 | HStack {
35 | Spacer()
36 | Text("Save")
37 | .font(.headline)
38 | .foregroundColor(Color.white)
39 | Spacer()
40 | }
41 | }
42 | .padding(.vertical, 10.0)
43 | .background(Color.green)
44 | //, cornerRadius: 8.0)
45 | .padding(.horizontal, 40)
46 | }
47 | }
48 |
49 | struct NotificationBadge: View {
50 | @EnvironmentObject var store: AppState
51 |
52 | let text: String
53 | let color: Color
54 | @Binding var show: Bool
55 |
56 | var animation: Animation {
57 | Animation
58 | .spring(dampingFraction: 0.5)
59 | .speed(2)
60 | .delay(0.3)
61 | }
62 |
63 | var body: some View {
64 | if show {
65 | DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
66 | /// Calling a global `store` which should be refactored to local
67 | /// using `@EnvironmentObject var store: AppState` etc.
68 | self.store.dispatch(action: TaskActions.Notification(show: false, message: ""))
69 | }
70 | }
71 |
72 | return Text(text)
73 | .foregroundColor(.white)
74 | .padding()
75 | .background(color)
76 | .cornerRadius(8)
77 | .scaleEffect(show ? 1 : 0.5)
78 | .opacity(show ? 1 : 0)
79 | .animation(animation)
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/SwiftUI-Todo-Redux/views/common/KeyboardObserver.swift:
--------------------------------------------------------------------------------
1 | //
2 | // KeyboardObserver
3 | // SwiftUI-Todo-Redux
4 | //
5 | // Created by moflo on 6/22/19.
6 | // Copyright © 2019 Mobile Flow LLC. All rights reserved.
7 | //
8 |
9 | import Combine
10 | import SwiftUI
11 |
12 | // Class to respond to keyboard events
13 |
14 | final class KeyboardObserver: ObservableObject {
15 | let didChange = PassthroughSubject()
16 |
17 | public var rects: [CGRect]
18 | public var keyboardRect: CGRect = CGRect()
19 |
20 | // keyboardWillShow notification may be posted repeatedly,
21 | // this flag makes sure we only act once per keyboard appearance
22 | public var keyboardIsHidden = true
23 |
24 | public var slide: CGFloat = 0.0 {
25 | didSet {
26 | didChange.send()
27 | }
28 | }
29 |
30 | public var showField: Int = 0 {
31 | didSet {
32 | updateSlide()
33 | }
34 | }
35 |
36 | init(_ textFieldCount: Int = 0) {
37 | rects = [CGRect](repeating: CGRect(), count: textFieldCount)
38 |
39 | NotificationCenter.default.addObserver(self, selector: #selector(keyBoardWillShow(notification:)), name: UIResponder.keyboardWillShowNotification, object: nil)
40 | NotificationCenter.default.addObserver(self, selector: #selector(keyBoardDidHide(notification:)), name: UIResponder.keyboardDidHideNotification, object: nil)
41 | }
42 |
43 | deinit {
44 | NotificationCenter.default.removeObserver(self)
45 | }
46 |
47 | @objc func keyBoardWillShow(notification: Notification) {
48 | if keyboardIsHidden {
49 | keyboardIsHidden = false
50 | if let rect = notification.userInfo?["UIKeyboardFrameEndUserInfoKey"] as? CGRect {
51 | keyboardRect = rect
52 | updateSlide()
53 | }
54 | }
55 | }
56 |
57 | @objc func keyBoardDidHide(notification _: Notification) {
58 | keyboardIsHidden = true
59 | updateSlide()
60 | }
61 |
62 | func updateSlide() {
63 | if keyboardIsHidden {
64 | slide = 0
65 | } else {
66 | let tfRect = rects[self.showField]
67 | let diff = keyboardRect.minY - tfRect.maxY
68 |
69 | if diff > 0 {
70 | slide += diff
71 | } else {
72 | slide += min(diff, 0)
73 | }
74 | }
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/SwiftUI-Todo-Redux/views/tasks/TaskCreate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TaskCreate
3 | // SwiftUI-Todo-Redux
4 | //
5 | // Created by moflo on 6/22/19.
6 | // Copyright © 2019 Mobile Flow LLC. All rights reserved.
7 | //
8 |
9 | import Combine
10 | import SwiftUI
11 |
12 | // Task Creation View, presented via a call to Modal()
13 | /// Use the `@Binding` variables to dismiss this Modal
14 |
15 | struct TaskCreate: View {
16 | @EnvironmentObject var store: AppState
17 | // @ObjectBinding private var kGuardian = KeyboardObserver(textFieldCount: 1)
18 |
19 | /// Default task
20 | @State var task: Task = Task(title: "New task", isDone: false)
21 | @State var ownerName: String = ""
22 |
23 | /// Used to dismiss Modal presentation
24 | @Binding var isEditing: Bool
25 |
26 | /// Dismiss Modal presentation
27 | func doCancel() {
28 | isEditing = false
29 | }
30 |
31 | /// Dismiss Modal presentation, save updated task if valid
32 | func doSave() {
33 | isEditing = false
34 | store.dispatch(action: TaskActions.Notification(show: true, message: "New Task Created!"))
35 | }
36 |
37 | var body: some View {
38 | NavigationView {
39 | Form {
40 | Section(header: Text("Task Information")) {
41 | VStack(alignment: .leading) {
42 | FieldSetText(textItem: $task.title, label: "TITLE", placeHolder: "Task title")
43 | // FieldSetText(textData: .constant(""), label: "DESCRIPTION", placeHolder: "Task description")
44 | }
45 | .padding(.vertical, 20)
46 | .listRowInsets(EdgeInsets())
47 | }
48 | Section(header: Text("Task Owner")) {
49 | VStack(alignment: .leading) {
50 | FieldSetText(textItem: $ownerName, label: "OWNER", placeHolder: "Task owner")
51 | }
52 | .padding(.vertical, 20)
53 | .listRowInsets(EdgeInsets())
54 | }
55 |
56 | RoundedButton().padding(.vertical, 20)
57 | }
58 | // .offset(y: kGuardian.slide).animation(.basic(duration: 1.0))
59 | .navigationBarTitle(Text("New Task"))
60 | .navigationBarItems(leading:
61 | Button(action: doCancel, label: {
62 | Text("Cancel")
63 | }), trailing:
64 | Button(action: doSave, label: {
65 | Text("Save")
66 | }))
67 | }
68 | }
69 | }
70 |
71 | #if DEBUG
72 | struct TasksEdit_Previews: PreviewProvider {
73 | static var previews: some View {
74 | return TaskCreate(isEditing: .constant(true))
75 | .environmentObject(sampleStore)
76 | .previewLayout(.fixed(width: 375, height: 1000))
77 | }
78 | }
79 | #endif
80 |
--------------------------------------------------------------------------------
/SwiftUI-Todo-Redux/views/tasks/TaskDetail.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TaskDetail.swift
3 | // SwiftUI-Todo-Redux
4 | //
5 | // Created by moflo on 6/22/19.
6 | // Copyright © 2019 Mobile Flow LLC. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | // Based on "in-line" detail editing structure proposed by Apple
12 | /// https://developer.apple.com/tutorials/swiftui/working-with-ui-controls
13 |
14 | struct TaskDetail: View {
15 | @EnvironmentObject var store: AppState
16 |
17 | @Environment(\.editMode) var mode
18 | @State var task: Task
19 | /// Value of `draftTask` is set to `task` using the Form `.onAppear` call
20 | @State var draftTask: Task = Task(title: "placeholder", isDone: false)
21 | @State var ownerName: String = ""
22 |
23 | var TaskSummary: some View {
24 | VStack {
25 | Text("TaskDetail")
26 | AnyView(
27 | Text(task.title).font(.title))
28 | AnyView(
29 | Text(task.id)
30 | )
31 | if task.isDone {
32 | Text("Done")
33 | } else {
34 | Text("Not Done")
35 | }
36 | }
37 | }
38 |
39 | var TaskEdit: some View {
40 | Form {
41 | Section(header: Text("Task Information")) {
42 | VStack(alignment: .leading) {
43 | FieldSetText(textItem: $draftTask.title, label: "TITLE", placeHolder: "Task title")
44 | }
45 | .padding(.vertical, 20)
46 | .listRowInsets(EdgeInsets())
47 | }
48 | Section(header: Text("Task Owner")) {
49 | VStack(alignment: .leading) {
50 | FieldSetText(textItem: $ownerName, label: "OWNER", placeHolder: "Task owner")
51 | }
52 | .padding(.vertical, 20)
53 | .listRowInsets(EdgeInsets())
54 | }
55 |
56 | RoundedButton().padding(.vertical, 20)
57 | }
58 | .onAppear(perform: { self.draftTask = self.task })
59 | }
60 |
61 | var body: some View {
62 | VStack(alignment: .leading, spacing: 20) {
63 | HStack {
64 | if self.mode?.wrappedValue == .active {
65 | Button(action: {
66 | self.mode?.animation().wrappedValue = .inactive
67 | // Update current taskID
68 | self.task = self.draftTask
69 | // store.dispatch(aciton: TaskActions.updateTask(id: task.id, task: draftTask))
70 | self.store.dispatch(action: TaskActions.Notification(show: true, message: "Changes saved"))
71 |
72 | }) {
73 | Text("Save")
74 | }
75 | }
76 |
77 | Spacer()
78 |
79 | EditButton()
80 | }
81 | if self.mode?.wrappedValue == .inactive {
82 | TaskSummary
83 | } else {
84 | TaskEdit
85 | }
86 | }
87 | .padding()
88 | }
89 | }
90 |
91 | #if DEBUG
92 | struct TasksDetail_Previews: PreviewProvider {
93 | static var previews: some View {
94 | TaskDetail(task: sampleStore.tasksState.tasks[0]).environmentObject(sampleStore)
95 | }
96 | }
97 | #endif
98 |
--------------------------------------------------------------------------------
/SwiftUI-Todo-Redux/views/tasks/TasksList.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TasksList.swift
3 | // SwiftUI-Todo-Redux
4 | //
5 | // Created by moflo on 6/22/19.
6 | // Copyright © 2019 Mobile Flow LLC. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | struct TasksList: View {
12 | @EnvironmentObject var store: AppState
13 | @State var showEdit = false
14 |
15 | func loadPage() {
16 | print("loadPage")
17 | store.dispatch(action: TaskActions.getTasks())
18 | }
19 |
20 | var taskSection: some View {
21 | Section {
22 | ForEach(store.tasksState.tasks) { task in
23 | NavigationLink(destination: TaskDetail(task: task)) {
24 | TasksRow(task: task)
25 | }
26 | }
27 | }
28 | }
29 |
30 | // var taskCreateModal: Modal {
31 | // return Modal(TaskCreate(isEditing: $showEdit).environmentObject(store))
32 | // }
33 |
34 | var body: some View {
35 | NavigationView {
36 | List {
37 | taskSection
38 |
39 | NavigationLink(
40 | destination: TaskCreate(isEditing: $showEdit),
41 | label: { Text("Add") }
42 | )
43 | }
44 | .navigationBarTitle(Text("My Tasks"))
45 | .navigationBarItems(leading: EditButton(),
46 | trailing:
47 | HStack {
48 | Button(action: { self.showEdit.toggle() }, label: { Text("Add1") })
49 | NavigationLink(
50 | destination: TaskCreate(isEditing: $showEdit),
51 | label: { Text("Add2") }
52 | )
53 | })
54 | .sheet(isPresented: $showEdit) {
55 | TaskCreate(isEditing: self.$showEdit).environmentObject(self.store)
56 | }
57 | .onAppear {
58 | self.loadPage()
59 | }
60 | }
61 | }
62 | }
63 |
64 | #if DEBUG
65 | struct TasksList_Previews: PreviewProvider {
66 | static var previews: some View {
67 | TasksList().environmentObject(sampleStore)
68 | }
69 | }
70 | #endif
71 |
--------------------------------------------------------------------------------
/SwiftUI-Todo-Redux/views/tasks/TasksRow.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TasksRow.swift
3 | // SwiftUI-Todo-Redux
4 | //
5 | // Created by moflo on 6/22/19.
6 | // Copyright © 2019 Mobile Flow LLC. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | struct TasksRow: View {
12 | @EnvironmentObject var store: AppState
13 | let task: Task
14 |
15 | var body: some View {
16 | HStack {
17 | if task.isDone {
18 | Image(systemName: "checkmark.circle.fill")
19 | // .font(.largeTitle)
20 | .imageScale(.large)
21 | .foregroundColor(.green)
22 | } else {
23 | Image(systemName: "checkmark.circle")
24 | // .font(.largeTitle)
25 | .imageScale(.large)
26 | .foregroundColor(.gray)
27 | }
28 | VStack(alignment: .leading, spacing: CGFloat(8.0)) {
29 | Text(task.title).font(.title)
30 | Text(task.id)
31 | .foregroundColor(.secondary)
32 | }.padding(.leading, CGFloat(8.0))
33 | }.padding(8)
34 | }
35 | }
36 |
37 | #if DEBUG
38 | struct TasksRow_Previews: PreviewProvider {
39 | static var previews: some View {
40 | Group {
41 | TasksRow(task: Task(title: "New Task", isDone: true)).environmentObject(sampleStore)
42 |
43 | TasksRow(task: Task(title: "New Task", isDone: false)).environmentObject(sampleStore)
44 | }
45 | }
46 | }
47 | #endif
48 |
--------------------------------------------------------------------------------
/SwiftUI-Todo-Redux/views/users/UserCreate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UserCreate
3 | // SwiftUI-Todo-Redux
4 | //
5 | // Created by moflo on 6/22/19.
6 | // Copyright © 2019 Mobile Flow LLC. All rights reserved.
7 | //
8 |
9 | import Combine
10 | import SwiftUI
11 |
12 | // User Creation View, presented via a call to Modal()
13 | /// Use the `@Binding` variables to dismiss this Modal
14 |
15 | struct UserCreate: View {
16 | @EnvironmentObject var store: AppState
17 | // @ObjectBinding private var kGuardian = KeyboardObserver(textFieldCount: 1)
18 |
19 | /// Default user
20 | @State var user: User = User(id: 0, name: "", username: "")
21 |
22 | /// Used to dismiss Modal presentation
23 | @Binding var isEditing: Bool
24 |
25 | /// Dismiss Modal presentation
26 | func doCancel() {
27 | isEditing = false
28 | }
29 |
30 | /// Dismiss Modal presentation, save updated user if valid
31 | func doSave() {
32 | isEditing = false
33 | store.dispatch(action: TaskActions.Notification(show: true, message: "New User Created!"))
34 | }
35 |
36 | var body: some View {
37 | NavigationView {
38 | Form {
39 | Section(header: Text("User Information")) {
40 | VStack(alignment: .leading) {
41 | FieldSetText(textItem: $user.name, label: "NAME", placeHolder: "User full name")
42 | FieldSetText(textItem: $user.username, label: "USERNAME", placeHolder: "User nickname")
43 | }
44 | .padding(.vertical, 20)
45 | .listRowInsets(EdgeInsets())
46 | }
47 |
48 | RoundedButton().padding(.vertical, 20)
49 | }
50 | // .offset(y: kGuardian.slide).animation(.basic(duration: 1.0))
51 | .navigationBarTitle(Text("New User"))
52 | .navigationBarItems(leading:
53 | Button(action: doCancel, label: {
54 | Text("Cancel")
55 | }), trailing:
56 | Button(action: doSave, label: {
57 | Text("Save")
58 | }))
59 | }
60 | }
61 | }
62 |
63 | #if DEBUG
64 | struct UsersEdit_Previews: PreviewProvider {
65 | static var previews: some View {
66 | return UserCreate(isEditing: .constant(true))
67 | .environmentObject(sampleStore)
68 | .previewLayout(.fixed(width: 375, height: 1000))
69 | }
70 | }
71 | #endif
72 |
--------------------------------------------------------------------------------
/SwiftUI-Todo-Redux/views/users/UserDetail.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UserDetail.swift
3 | // SwiftUI-Todo-Redux
4 | //
5 | // Created by moflo on 6/22/19.
6 | // Copyright © 2019 Mobile Flow LLC. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | struct UserDetail: View {
12 | // Based on "in-line" detail editing structure proposed by Apple
13 | /// https://developer.apple.com/tutorials/swiftui/working-with-ui-controls
14 |
15 | @EnvironmentObject var store: AppState
16 |
17 | @Environment(\.editMode) var mode
18 | @State var user: User
19 | /// Value of `draftUser` is set to `user` using the Form `.onAppear` call
20 | @State var draftUser: User = User(id: 0, name: "placeholder user", username: "@user1")
21 |
22 | var UserSummary: some View {
23 | VStack {
24 | Text("UserDetail")
25 | AnyView(
26 | Text(user.name).font(.title))
27 | AnyView(
28 | Text(user.username)
29 | )
30 | }
31 | }
32 |
33 | var UserEdit: some View {
34 | Form {
35 | Section(header: Text("User Information")) {
36 | VStack(alignment: .leading) {
37 | FieldSetText(textItem: $draftUser.name, label: "NAME", placeHolder: "User full name")
38 | FieldSetText(textItem: $draftUser.username, label: "USERNAME", placeHolder: "User nickname")
39 | }
40 | .padding(.vertical, 20)
41 | .listRowInsets(EdgeInsets())
42 | }
43 |
44 | RoundedButton().padding(.vertical, 20)
45 | }
46 | .onAppear(perform: { self.draftUser = self.user })
47 | }
48 |
49 | var body: some View {
50 | VStack(alignment: .leading) {
51 | HStack {
52 | if self.mode?.wrappedValue == .active {
53 | Button(action: {
54 | self.mode?.animation().wrappedValue = .inactive
55 | // Update current userID
56 | self.user = self.draftUser
57 | // store.dispatch(aciton: UserActions.updateUser(id: user.id, user: draftUser))
58 | self.store.dispatch(action: TaskActions.Notification(show: true, message: "Changes saved"))
59 |
60 | }) {
61 | Text("Save")
62 | }
63 | }
64 |
65 | Spacer()
66 |
67 | EditButton()
68 | }
69 | if self.mode?.wrappedValue == .inactive {
70 | UserSummary
71 | } else {
72 | UserEdit
73 | }
74 | }
75 | .padding()
76 | }
77 | }
78 |
79 | #if DEBUG
80 | struct UserDetail_Previews: PreviewProvider {
81 | static var previews: some View {
82 | UserDetail(user: testUsersModels[0]).environmentObject(sampleStore)
83 | }
84 | }
85 | #endif
86 |
--------------------------------------------------------------------------------
/SwiftUI-Todo-Redux/views/users/UsersList.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UsersList.swift
3 | // SwiftUI-Todo-Redux
4 | //
5 | // Created by moflo on 6/22/19.
6 | // Copyright © 2019 Mobile Flow LLC. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | struct UsersList: View {
12 | @EnvironmentObject var store: AppState
13 | @State var showEdit = false
14 |
15 | func loadPage() {
16 | print("loadPage")
17 | store.dispatch(action: UserActions.getUsers())
18 | }
19 |
20 | var userSection: some View {
21 | Section {
22 | ForEach(store.usersState.users) { user in
23 | NavigationLink(destination: UserDetail(user: user)) {
24 | UsersRow(user: user)
25 | }
26 | }
27 | }
28 | }
29 |
30 | // var taskCreateModal: Modal {
31 | // return Modal(UserCreate(isEditing: $showEdit).environmentObject(store))
32 | // }
33 |
34 | var body: some View {
35 | NavigationView {
36 | List {
37 | userSection
38 |
39 | NavigationLink(
40 | destination: UserCreate(isEditing: $showEdit),
41 | label: { Text("Add") }
42 | )
43 | }
44 | .navigationBarTitle(Text("My Tasks"))
45 | .navigationBarItems(leading: EditButton(),
46 | trailing:
47 | HStack {
48 | Button(action: { self.showEdit.toggle() }, label: { Text("Add") })
49 | })
50 | .sheet(isPresented: $showEdit) {
51 | UserCreate(isEditing: self.$showEdit).environmentObject(self.store)
52 | }
53 | .onAppear {
54 | self.loadPage()
55 | }
56 | }
57 | }
58 | }
59 |
60 | #if DEBUG
61 | struct UsersList_Previews: PreviewProvider {
62 | static var previews: some View {
63 | UsersList().environmentObject(sampleStore)
64 | }
65 | }
66 | #endif
67 |
--------------------------------------------------------------------------------
/SwiftUI-Todo-Redux/views/users/UsersRow.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UsersRow.swift
3 | // SwiftUI-Todo-Redux
4 | //
5 | // Created by moflo on 6/22/19.
6 | // Copyright © 2019 Mobile Flow LLC. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | struct UsersRow: View {
12 | @EnvironmentObject var store: AppState
13 | let user: User
14 |
15 | var body: some View {
16 | HStack {
17 | VStack(alignment: .leading, spacing: CGFloat(8.0)) {
18 | Text(user.name).font(.title)
19 | Text(user.username)
20 | .foregroundColor(.secondary)
21 | }.padding(.leading, 8)
22 | }.padding(8)
23 | }
24 | }
25 |
26 | #if DEBUG
27 | struct UsersRow_Previews: PreviewProvider {
28 | static var previews: some View {
29 | UsersRow(user: sampleStore.usersState.users[0]).environmentObject(sampleStore)
30 | }
31 | }
32 | #endif
33 |
--------------------------------------------------------------------------------
/SwiftUI-Todo-ReduxTests/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 |
22 |
23 |
--------------------------------------------------------------------------------
/SwiftUI-Todo-ReduxTests/SwiftUI_Todo_ReduxTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SwiftUI_Todo_ReduxTests.swift
3 | // SwiftUI-Todo-ReduxTests
4 | //
5 | // Created by admin on 6/23/19.
6 | // Copyright © 2019 admin. All rights reserved.
7 | //
8 |
9 | import XCTest
10 |
11 | class SwiftUI_Todo_ReduxTests: XCTestCase {
12 | override func setUp() {
13 | // Put setup code here. This method is called before the invocation of each test method in the class.
14 | }
15 |
16 | override func tearDown() {
17 | // Put teardown code here. This method is called after the invocation of each test method in the class.
18 | }
19 |
20 | func testEncodingTask() {
21 | let json = #"""
22 | { "id": 1,
23 | "tasks":
24 | [ { "id": "UUID1",
25 | "title": "Task title goes here",
26 | "isDone": false
27 | }, {"id": "UUID1",
28 | "title": "Task title goes here",
29 | "isDone": false
30 | }]}
31 | """#
32 |
33 | let data = json.data(using: .utf8)!
34 | let decoder = JSONDecoder()
35 |
36 | do {
37 | let object = try decoder.decode(TaskResponseJSON.self, from: data)
38 | print(object)
39 |
40 | XCTAssertNotNil(object)
41 |
42 | } catch {
43 | print("JSON decoding error (GET)", TaskResponseJSON.self, error)
44 |
45 | XCTFail()
46 | }
47 | }
48 |
49 | func testEncodingUser() {
50 | let json = #"""
51 | { "id": 1,
52 | "users":
53 | [ { "id": 0,
54 | "name": "user1",
55 | "username": "nickname1"
56 | }, {"id": 1,
57 | "name": "user1",
58 | "username": "nickname1"
59 | }]}
60 | """#
61 |
62 | let data = json.data(using: .utf8)!
63 | let decoder = JSONDecoder()
64 |
65 | do {
66 | let object = try decoder.decode(UserResponseJSON.self, from: data)
67 | print(object)
68 |
69 | XCTAssertNotNil(object)
70 |
71 | } catch {
72 | print("JSON decoding error (GET)", UserResponseJSON.self, error)
73 |
74 | XCTFail()
75 | }
76 | }
77 |
78 | func testPerformanceExample() {
79 | // This is an example of a performance test case.
80 | measure {
81 | // Put the code you want to measure the time of here.
82 | }
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/moflo/SwiftUI-Todo-Redux/cc06b44f1f5c20afee2f9f3d33e6ce5cbe283b55/screenshot.png
--------------------------------------------------------------------------------