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