├── .gitignore ├── LICENSE ├── README.md ├── StateMachineUI.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm │ │ └── Package.resolved └── xcuserdata │ └── peterringset.xcuserdatad │ └── xcschemes │ └── xcschememanagement.plist ├── StateMachineUI ├── AppDelegate.swift ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json ├── Base.lproj │ └── LaunchScreen.storyboard ├── Info.plist ├── Models │ └── PixabayItem.swift ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json ├── SceneDelegate.swift ├── Services │ └── ImageService.swift ├── StateMachine.swift ├── View models │ ├── ContentViewModel.swift │ └── SearchResultsViewModel.swift └── Views │ ├── Components │ ├── ActivityIndicator.swift │ └── SearchBar.swift │ ├── ContentView.swift │ └── State views │ ├── ErrorView.swift │ ├── LoadingView.swift │ ├── SearchResultsView.swift │ ├── SearchingView.swift │ └── StartView.swift ├── StateMachineUITests ├── Info.plist └── StateMachineUITests.swift └── demo └── demo.gif /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## User settings 6 | xcuserdata/ 7 | 8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 9 | *.xcscmblueprint 10 | *.xccheckout 11 | 12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 13 | build/ 14 | DerivedData/ 15 | *.moved-aside 16 | *.pbxuser 17 | !default.pbxuser 18 | *.mode1v3 19 | !default.mode1v3 20 | *.mode2v3 21 | !default.mode2v3 22 | *.perspectivev3 23 | !default.perspectivev3 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | 28 | ## App packaging 29 | *.ipa 30 | *.dSYM.zip 31 | *.dSYM 32 | 33 | ## Playgrounds 34 | timeline.xctimeline 35 | playground.xcworkspace 36 | 37 | # Swift Package Manager 38 | # 39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 40 | # Packages/ 41 | # Package.pins 42 | # Package.resolved 43 | # *.xcodeproj 44 | # 45 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 46 | # hence it is not needed unless you have added a package configuration file to your project 47 | # .swiftpm 48 | 49 | .build/ 50 | 51 | # CocoaPods 52 | # 53 | # We recommend against adding the Pods directory to your .gitignore. However 54 | # you should judge for yourself, the pros and cons are mentioned at: 55 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 56 | # 57 | # Pods/ 58 | # 59 | # Add this line if you want to avoid checking in source code from the Xcode workspace 60 | # *.xcworkspace 61 | 62 | # Carthage 63 | # 64 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 65 | # Carthage/Checkouts 66 | 67 | Carthage/Build/ 68 | 69 | # Accio dependency management 70 | Dependencies/ 71 | .accio/ 72 | 73 | # fastlane 74 | # 75 | # It is recommended to not store the screenshots in the git repo. 76 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 77 | # For more information about the recommended setup visit: 78 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 79 | 80 | fastlane/report.xml 81 | fastlane/Preview.html 82 | fastlane/screenshots/**/*.png 83 | fastlane/test_output 84 | 85 | # Code Injection 86 | # 87 | # After new code Injection tools there's a generated folder /iOSInjectionProject 88 | # https://github.com/johnno1962/injectionforxcode 89 | 90 | iOSInjectionProject/ 91 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 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 | # StateMachineUI 2 | 3 | This repository is a sample application used in [this blog post](https://medium.com/@peterringset/building-a-state-driven-app-in-swiftui-using-state-machines-32379ca37283). The application is implemented with SwiftUI, using the MVVM architecture. The application uses [URLImage](https://github.com/dmytro-anokhin/url-image) to show and download images. 4 | 5 | ## License 6 | 7 | The application is released under the [MIT License](/LICENSE) 8 | 9 | ## Demo 10 | 11 | ![Demo](/demo/demo.gif) 12 | 13 | -------------------------------------------------------------------------------- /StateMachineUI.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 52; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 4501CDBE23C367D600BA4D6C /* SearchBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4501CDBD23C367D600BA4D6C /* SearchBar.swift */; }; 11 | 4501CDC023C3681700BA4D6C /* ActivityIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4501CDBF23C3681700BA4D6C /* ActivityIndicator.swift */; }; 12 | 4501CDC623C3688900BA4D6C /* ImageService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4501CDC523C3688900BA4D6C /* ImageService.swift */; }; 13 | 4501CDC923C368D200BA4D6C /* PixabayItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4501CDC823C368D200BA4D6C /* PixabayItem.swift */; }; 14 | 4501CDCB23C369CA00BA4D6C /* StateMachine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4501CDCA23C369CA00BA4D6C /* StateMachine.swift */; }; 15 | 4546869623C366B400E14954 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4546869523C366B400E14954 /* AppDelegate.swift */; }; 16 | 4546869823C366B400E14954 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4546869723C366B400E14954 /* SceneDelegate.swift */; }; 17 | 4546869A23C366B400E14954 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4546869923C366B400E14954 /* ContentView.swift */; }; 18 | 4546869C23C366B600E14954 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 4546869B23C366B600E14954 /* Assets.xcassets */; }; 19 | 4546869F23C366B600E14954 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 4546869E23C366B600E14954 /* Preview Assets.xcassets */; }; 20 | 454686A223C366B600E14954 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 454686A023C366B600E14954 /* LaunchScreen.storyboard */; }; 21 | 454686AD23C366B600E14954 /* StateMachineUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 454686AC23C366B600E14954 /* StateMachineUITests.swift */; }; 22 | 456DDFEC23C496A1009A1A4F /* ErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 456DDFEB23C496A1009A1A4F /* ErrorView.swift */; }; 23 | 4572B26423C3804200E5D289 /* SearchResultsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4572B26323C3804200E5D289 /* SearchResultsView.swift */; }; 24 | 4572B26623C381BB00E5D289 /* SearchResultsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4572B26523C381BB00E5D289 /* SearchResultsViewModel.swift */; }; 25 | 4572B26923C48D3500E5D289 /* URLImage in Frameworks */ = {isa = PBXBuildFile; productRef = 4572B26823C48D3500E5D289 /* URLImage */; }; 26 | 45DE733623C36B97007AB404 /* ContentViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45DE733523C36B97007AB404 /* ContentViewModel.swift */; }; 27 | 45DE733923C36CF6007AB404 /* StartView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45DE733823C36CF6007AB404 /* StartView.swift */; }; 28 | 45DE733B23C36D28007AB404 /* SearchingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45DE733A23C36D28007AB404 /* SearchingView.swift */; }; 29 | 45DE733D23C36F7F007AB404 /* LoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45DE733C23C36F7F007AB404 /* LoadingView.swift */; }; 30 | /* End PBXBuildFile section */ 31 | 32 | /* Begin PBXContainerItemProxy section */ 33 | 454686A923C366B600E14954 /* PBXContainerItemProxy */ = { 34 | isa = PBXContainerItemProxy; 35 | containerPortal = 4546868A23C366B400E14954 /* Project object */; 36 | proxyType = 1; 37 | remoteGlobalIDString = 4546869123C366B400E14954; 38 | remoteInfo = StateMachineUI; 39 | }; 40 | /* End PBXContainerItemProxy section */ 41 | 42 | /* Begin PBXFileReference section */ 43 | 4501CDBD23C367D600BA4D6C /* SearchBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchBar.swift; sourceTree = ""; }; 44 | 4501CDBF23C3681700BA4D6C /* ActivityIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityIndicator.swift; sourceTree = ""; }; 45 | 4501CDC523C3688900BA4D6C /* ImageService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageService.swift; sourceTree = ""; }; 46 | 4501CDC823C368D200BA4D6C /* PixabayItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PixabayItem.swift; sourceTree = ""; }; 47 | 4501CDCA23C369CA00BA4D6C /* StateMachine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StateMachine.swift; sourceTree = ""; }; 48 | 4546869223C366B400E14954 /* StateMachineUI.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = StateMachineUI.app; sourceTree = BUILT_PRODUCTS_DIR; }; 49 | 4546869523C366B400E14954 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 50 | 4546869723C366B400E14954 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 51 | 4546869923C366B400E14954 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 52 | 4546869B23C366B600E14954 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 53 | 4546869E23C366B600E14954 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 54 | 454686A123C366B600E14954 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 55 | 454686A323C366B600E14954 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 56 | 454686A823C366B600E14954 /* StateMachineUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = StateMachineUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 57 | 454686AC23C366B600E14954 /* StateMachineUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StateMachineUITests.swift; sourceTree = ""; }; 58 | 454686AE23C366B600E14954 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 59 | 456DDFEB23C496A1009A1A4F /* ErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorView.swift; sourceTree = ""; }; 60 | 4572B26323C3804200E5D289 /* SearchResultsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultsView.swift; sourceTree = ""; }; 61 | 4572B26523C381BB00E5D289 /* SearchResultsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultsViewModel.swift; sourceTree = ""; }; 62 | 45DE733523C36B97007AB404 /* ContentViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentViewModel.swift; sourceTree = ""; }; 63 | 45DE733823C36CF6007AB404 /* StartView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartView.swift; sourceTree = ""; }; 64 | 45DE733A23C36D28007AB404 /* SearchingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchingView.swift; sourceTree = ""; }; 65 | 45DE733C23C36F7F007AB404 /* LoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingView.swift; sourceTree = ""; }; 66 | /* End PBXFileReference section */ 67 | 68 | /* Begin PBXFrameworksBuildPhase section */ 69 | 4546868F23C366B400E14954 /* Frameworks */ = { 70 | isa = PBXFrameworksBuildPhase; 71 | buildActionMask = 2147483647; 72 | files = ( 73 | 4572B26923C48D3500E5D289 /* URLImage in Frameworks */, 74 | ); 75 | runOnlyForDeploymentPostprocessing = 0; 76 | }; 77 | 454686A523C366B600E14954 /* Frameworks */ = { 78 | isa = PBXFrameworksBuildPhase; 79 | buildActionMask = 2147483647; 80 | files = ( 81 | ); 82 | runOnlyForDeploymentPostprocessing = 0; 83 | }; 84 | /* End PBXFrameworksBuildPhase section */ 85 | 86 | /* Begin PBXGroup section */ 87 | 4501CDBB23C3678100BA4D6C /* Views */ = { 88 | isa = PBXGroup; 89 | children = ( 90 | 4501CDBC23C367C400BA4D6C /* Components */, 91 | 4546869923C366B400E14954 /* ContentView.swift */, 92 | 45DE733723C36CE6007AB404 /* State views */, 93 | ); 94 | path = Views; 95 | sourceTree = ""; 96 | }; 97 | 4501CDBC23C367C400BA4D6C /* Components */ = { 98 | isa = PBXGroup; 99 | children = ( 100 | 4501CDBF23C3681700BA4D6C /* ActivityIndicator.swift */, 101 | 4501CDBD23C367D600BA4D6C /* SearchBar.swift */, 102 | ); 103 | path = Components; 104 | sourceTree = ""; 105 | }; 106 | 4501CDC423C3687400BA4D6C /* Services */ = { 107 | isa = PBXGroup; 108 | children = ( 109 | 4501CDC523C3688900BA4D6C /* ImageService.swift */, 110 | ); 111 | path = Services; 112 | sourceTree = ""; 113 | }; 114 | 4501CDC723C368C500BA4D6C /* Models */ = { 115 | isa = PBXGroup; 116 | children = ( 117 | 4501CDC823C368D200BA4D6C /* PixabayItem.swift */, 118 | ); 119 | path = Models; 120 | sourceTree = ""; 121 | }; 122 | 4546868923C366B400E14954 = { 123 | isa = PBXGroup; 124 | children = ( 125 | 4546869423C366B400E14954 /* StateMachineUI */, 126 | 454686AB23C366B600E14954 /* StateMachineUITests */, 127 | 4546869323C366B400E14954 /* Products */, 128 | ); 129 | sourceTree = ""; 130 | }; 131 | 4546869323C366B400E14954 /* Products */ = { 132 | isa = PBXGroup; 133 | children = ( 134 | 4546869223C366B400E14954 /* StateMachineUI.app */, 135 | 454686A823C366B600E14954 /* StateMachineUITests.xctest */, 136 | ); 137 | name = Products; 138 | sourceTree = ""; 139 | }; 140 | 4546869423C366B400E14954 /* StateMachineUI */ = { 141 | isa = PBXGroup; 142 | children = ( 143 | 4546869523C366B400E14954 /* AppDelegate.swift */, 144 | 4546869B23C366B600E14954 /* Assets.xcassets */, 145 | 454686A323C366B600E14954 /* Info.plist */, 146 | 454686A023C366B600E14954 /* LaunchScreen.storyboard */, 147 | 4501CDC723C368C500BA4D6C /* Models */, 148 | 4546869D23C366B600E14954 /* Preview Content */, 149 | 4546869723C366B400E14954 /* SceneDelegate.swift */, 150 | 4501CDC423C3687400BA4D6C /* Services */, 151 | 4501CDCA23C369CA00BA4D6C /* StateMachine.swift */, 152 | 45DE733423C36B85007AB404 /* View models */, 153 | 4501CDBB23C3678100BA4D6C /* Views */, 154 | ); 155 | path = StateMachineUI; 156 | sourceTree = ""; 157 | }; 158 | 4546869D23C366B600E14954 /* Preview Content */ = { 159 | isa = PBXGroup; 160 | children = ( 161 | 4546869E23C366B600E14954 /* Preview Assets.xcassets */, 162 | ); 163 | path = "Preview Content"; 164 | sourceTree = ""; 165 | }; 166 | 454686AB23C366B600E14954 /* StateMachineUITests */ = { 167 | isa = PBXGroup; 168 | children = ( 169 | 454686AC23C366B600E14954 /* StateMachineUITests.swift */, 170 | 454686AE23C366B600E14954 /* Info.plist */, 171 | ); 172 | path = StateMachineUITests; 173 | sourceTree = ""; 174 | }; 175 | 45DE733423C36B85007AB404 /* View models */ = { 176 | isa = PBXGroup; 177 | children = ( 178 | 45DE733523C36B97007AB404 /* ContentViewModel.swift */, 179 | 4572B26523C381BB00E5D289 /* SearchResultsViewModel.swift */, 180 | ); 181 | path = "View models"; 182 | sourceTree = ""; 183 | }; 184 | 45DE733723C36CE6007AB404 /* State views */ = { 185 | isa = PBXGroup; 186 | children = ( 187 | 456DDFEB23C496A1009A1A4F /* ErrorView.swift */, 188 | 45DE733C23C36F7F007AB404 /* LoadingView.swift */, 189 | 45DE733A23C36D28007AB404 /* SearchingView.swift */, 190 | 4572B26323C3804200E5D289 /* SearchResultsView.swift */, 191 | 45DE733823C36CF6007AB404 /* StartView.swift */, 192 | ); 193 | path = "State views"; 194 | sourceTree = ""; 195 | }; 196 | /* End PBXGroup section */ 197 | 198 | /* Begin PBXNativeTarget section */ 199 | 4546869123C366B400E14954 /* StateMachineUI */ = { 200 | isa = PBXNativeTarget; 201 | buildConfigurationList = 454686B123C366B600E14954 /* Build configuration list for PBXNativeTarget "StateMachineUI" */; 202 | buildPhases = ( 203 | 4546868E23C366B400E14954 /* Sources */, 204 | 4546868F23C366B400E14954 /* Frameworks */, 205 | 4546869023C366B400E14954 /* Resources */, 206 | ); 207 | buildRules = ( 208 | ); 209 | dependencies = ( 210 | ); 211 | name = StateMachineUI; 212 | packageProductDependencies = ( 213 | 4572B26823C48D3500E5D289 /* URLImage */, 214 | ); 215 | productName = StateMachineUI; 216 | productReference = 4546869223C366B400E14954 /* StateMachineUI.app */; 217 | productType = "com.apple.product-type.application"; 218 | }; 219 | 454686A723C366B600E14954 /* StateMachineUITests */ = { 220 | isa = PBXNativeTarget; 221 | buildConfigurationList = 454686B423C366B600E14954 /* Build configuration list for PBXNativeTarget "StateMachineUITests" */; 222 | buildPhases = ( 223 | 454686A423C366B600E14954 /* Sources */, 224 | 454686A523C366B600E14954 /* Frameworks */, 225 | 454686A623C366B600E14954 /* Resources */, 226 | ); 227 | buildRules = ( 228 | ); 229 | dependencies = ( 230 | 454686AA23C366B600E14954 /* PBXTargetDependency */, 231 | ); 232 | name = StateMachineUITests; 233 | productName = StateMachineUITests; 234 | productReference = 454686A823C366B600E14954 /* StateMachineUITests.xctest */; 235 | productType = "com.apple.product-type.bundle.unit-test"; 236 | }; 237 | /* End PBXNativeTarget section */ 238 | 239 | /* Begin PBXProject section */ 240 | 4546868A23C366B400E14954 /* Project object */ = { 241 | isa = PBXProject; 242 | attributes = { 243 | LastSwiftUpdateCheck = 1130; 244 | LastUpgradeCheck = 1130; 245 | ORGANIZATIONNAME = Ringset; 246 | TargetAttributes = { 247 | 4546869123C366B400E14954 = { 248 | CreatedOnToolsVersion = 11.3; 249 | }; 250 | 454686A723C366B600E14954 = { 251 | CreatedOnToolsVersion = 11.3; 252 | TestTargetID = 4546869123C366B400E14954; 253 | }; 254 | }; 255 | }; 256 | buildConfigurationList = 4546868D23C366B400E14954 /* Build configuration list for PBXProject "StateMachineUI" */; 257 | compatibilityVersion = "Xcode 9.3"; 258 | developmentRegion = en; 259 | hasScannedForEncodings = 0; 260 | knownRegions = ( 261 | en, 262 | Base, 263 | ); 264 | mainGroup = 4546868923C366B400E14954; 265 | packageReferences = ( 266 | 4572B26723C48D3500E5D289 /* XCRemoteSwiftPackageReference "url-image" */, 267 | ); 268 | productRefGroup = 4546869323C366B400E14954 /* Products */; 269 | projectDirPath = ""; 270 | projectRoot = ""; 271 | targets = ( 272 | 4546869123C366B400E14954 /* StateMachineUI */, 273 | 454686A723C366B600E14954 /* StateMachineUITests */, 274 | ); 275 | }; 276 | /* End PBXProject section */ 277 | 278 | /* Begin PBXResourcesBuildPhase section */ 279 | 4546869023C366B400E14954 /* Resources */ = { 280 | isa = PBXResourcesBuildPhase; 281 | buildActionMask = 2147483647; 282 | files = ( 283 | 454686A223C366B600E14954 /* LaunchScreen.storyboard in Resources */, 284 | 4546869F23C366B600E14954 /* Preview Assets.xcassets in Resources */, 285 | 4546869C23C366B600E14954 /* Assets.xcassets in Resources */, 286 | ); 287 | runOnlyForDeploymentPostprocessing = 0; 288 | }; 289 | 454686A623C366B600E14954 /* Resources */ = { 290 | isa = PBXResourcesBuildPhase; 291 | buildActionMask = 2147483647; 292 | files = ( 293 | ); 294 | runOnlyForDeploymentPostprocessing = 0; 295 | }; 296 | /* End PBXResourcesBuildPhase section */ 297 | 298 | /* Begin PBXSourcesBuildPhase section */ 299 | 4546868E23C366B400E14954 /* Sources */ = { 300 | isa = PBXSourcesBuildPhase; 301 | buildActionMask = 2147483647; 302 | files = ( 303 | 4546869623C366B400E14954 /* AppDelegate.swift in Sources */, 304 | 45DE733923C36CF6007AB404 /* StartView.swift in Sources */, 305 | 4501CDC023C3681700BA4D6C /* ActivityIndicator.swift in Sources */, 306 | 45DE733B23C36D28007AB404 /* SearchingView.swift in Sources */, 307 | 4572B26423C3804200E5D289 /* SearchResultsView.swift in Sources */, 308 | 4501CDBE23C367D600BA4D6C /* SearchBar.swift in Sources */, 309 | 45DE733D23C36F7F007AB404 /* LoadingView.swift in Sources */, 310 | 4501CDCB23C369CA00BA4D6C /* StateMachine.swift in Sources */, 311 | 4546869823C366B400E14954 /* SceneDelegate.swift in Sources */, 312 | 45DE733623C36B97007AB404 /* ContentViewModel.swift in Sources */, 313 | 456DDFEC23C496A1009A1A4F /* ErrorView.swift in Sources */, 314 | 4501CDC923C368D200BA4D6C /* PixabayItem.swift in Sources */, 315 | 4572B26623C381BB00E5D289 /* SearchResultsViewModel.swift in Sources */, 316 | 4546869A23C366B400E14954 /* ContentView.swift in Sources */, 317 | 4501CDC623C3688900BA4D6C /* ImageService.swift in Sources */, 318 | ); 319 | runOnlyForDeploymentPostprocessing = 0; 320 | }; 321 | 454686A423C366B600E14954 /* Sources */ = { 322 | isa = PBXSourcesBuildPhase; 323 | buildActionMask = 2147483647; 324 | files = ( 325 | 454686AD23C366B600E14954 /* StateMachineUITests.swift in Sources */, 326 | ); 327 | runOnlyForDeploymentPostprocessing = 0; 328 | }; 329 | /* End PBXSourcesBuildPhase section */ 330 | 331 | /* Begin PBXTargetDependency section */ 332 | 454686AA23C366B600E14954 /* PBXTargetDependency */ = { 333 | isa = PBXTargetDependency; 334 | target = 4546869123C366B400E14954 /* StateMachineUI */; 335 | targetProxy = 454686A923C366B600E14954 /* PBXContainerItemProxy */; 336 | }; 337 | /* End PBXTargetDependency section */ 338 | 339 | /* Begin PBXVariantGroup section */ 340 | 454686A023C366B600E14954 /* LaunchScreen.storyboard */ = { 341 | isa = PBXVariantGroup; 342 | children = ( 343 | 454686A123C366B600E14954 /* Base */, 344 | ); 345 | name = LaunchScreen.storyboard; 346 | sourceTree = ""; 347 | }; 348 | /* End PBXVariantGroup section */ 349 | 350 | /* Begin XCBuildConfiguration section */ 351 | 454686AF23C366B600E14954 /* Debug */ = { 352 | isa = XCBuildConfiguration; 353 | buildSettings = { 354 | ALWAYS_SEARCH_USER_PATHS = NO; 355 | CLANG_ANALYZER_NONNULL = YES; 356 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 357 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 358 | CLANG_CXX_LIBRARY = "libc++"; 359 | CLANG_ENABLE_MODULES = YES; 360 | CLANG_ENABLE_OBJC_ARC = YES; 361 | CLANG_ENABLE_OBJC_WEAK = YES; 362 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 363 | CLANG_WARN_BOOL_CONVERSION = YES; 364 | CLANG_WARN_COMMA = YES; 365 | CLANG_WARN_CONSTANT_CONVERSION = YES; 366 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 367 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 368 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 369 | CLANG_WARN_EMPTY_BODY = YES; 370 | CLANG_WARN_ENUM_CONVERSION = YES; 371 | CLANG_WARN_INFINITE_RECURSION = YES; 372 | CLANG_WARN_INT_CONVERSION = YES; 373 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 374 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 375 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 376 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 377 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 378 | CLANG_WARN_STRICT_PROTOTYPES = YES; 379 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 380 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 381 | CLANG_WARN_UNREACHABLE_CODE = YES; 382 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 383 | COPY_PHASE_STRIP = NO; 384 | DEBUG_INFORMATION_FORMAT = dwarf; 385 | ENABLE_STRICT_OBJC_MSGSEND = YES; 386 | ENABLE_TESTABILITY = YES; 387 | GCC_C_LANGUAGE_STANDARD = gnu11; 388 | GCC_DYNAMIC_NO_PIC = NO; 389 | GCC_NO_COMMON_BLOCKS = YES; 390 | GCC_OPTIMIZATION_LEVEL = 0; 391 | GCC_PREPROCESSOR_DEFINITIONS = ( 392 | "DEBUG=1", 393 | "$(inherited)", 394 | ); 395 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 396 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 397 | GCC_WARN_UNDECLARED_SELECTOR = YES; 398 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 399 | GCC_WARN_UNUSED_FUNCTION = YES; 400 | GCC_WARN_UNUSED_VARIABLE = YES; 401 | IPHONEOS_DEPLOYMENT_TARGET = 13.2; 402 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 403 | MTL_FAST_MATH = YES; 404 | ONLY_ACTIVE_ARCH = YES; 405 | SDKROOT = iphoneos; 406 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 407 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 408 | }; 409 | name = Debug; 410 | }; 411 | 454686B023C366B600E14954 /* Release */ = { 412 | isa = XCBuildConfiguration; 413 | buildSettings = { 414 | ALWAYS_SEARCH_USER_PATHS = NO; 415 | CLANG_ANALYZER_NONNULL = YES; 416 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 417 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 418 | CLANG_CXX_LIBRARY = "libc++"; 419 | CLANG_ENABLE_MODULES = YES; 420 | CLANG_ENABLE_OBJC_ARC = YES; 421 | CLANG_ENABLE_OBJC_WEAK = YES; 422 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 423 | CLANG_WARN_BOOL_CONVERSION = YES; 424 | CLANG_WARN_COMMA = YES; 425 | CLANG_WARN_CONSTANT_CONVERSION = YES; 426 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 427 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 428 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 429 | CLANG_WARN_EMPTY_BODY = YES; 430 | CLANG_WARN_ENUM_CONVERSION = YES; 431 | CLANG_WARN_INFINITE_RECURSION = YES; 432 | CLANG_WARN_INT_CONVERSION = YES; 433 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 434 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 435 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 436 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 437 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 438 | CLANG_WARN_STRICT_PROTOTYPES = YES; 439 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 440 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 441 | CLANG_WARN_UNREACHABLE_CODE = YES; 442 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 443 | COPY_PHASE_STRIP = NO; 444 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 445 | ENABLE_NS_ASSERTIONS = NO; 446 | ENABLE_STRICT_OBJC_MSGSEND = YES; 447 | GCC_C_LANGUAGE_STANDARD = gnu11; 448 | GCC_NO_COMMON_BLOCKS = YES; 449 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 450 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 451 | GCC_WARN_UNDECLARED_SELECTOR = YES; 452 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 453 | GCC_WARN_UNUSED_FUNCTION = YES; 454 | GCC_WARN_UNUSED_VARIABLE = YES; 455 | IPHONEOS_DEPLOYMENT_TARGET = 13.2; 456 | MTL_ENABLE_DEBUG_INFO = NO; 457 | MTL_FAST_MATH = YES; 458 | SDKROOT = iphoneos; 459 | SWIFT_COMPILATION_MODE = wholemodule; 460 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 461 | VALIDATE_PRODUCT = YES; 462 | }; 463 | name = Release; 464 | }; 465 | 454686B223C366B600E14954 /* Debug */ = { 466 | isa = XCBuildConfiguration; 467 | buildSettings = { 468 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 469 | CODE_SIGN_STYLE = Automatic; 470 | DEVELOPMENT_ASSET_PATHS = "\"StateMachineUI/Preview Content\""; 471 | DEVELOPMENT_TEAM = Y34Y4CF9LN; 472 | ENABLE_PREVIEWS = YES; 473 | INFOPLIST_FILE = StateMachineUI/Info.plist; 474 | LD_RUNPATH_SEARCH_PATHS = ( 475 | "$(inherited)", 476 | "@executable_path/Frameworks", 477 | ); 478 | PRODUCT_BUNDLE_IDENTIFIER = com.eggsdesign.StateMachineUI; 479 | PRODUCT_NAME = "$(TARGET_NAME)"; 480 | SWIFT_VERSION = 5.0; 481 | TARGETED_DEVICE_FAMILY = "1,2"; 482 | }; 483 | name = Debug; 484 | }; 485 | 454686B323C366B600E14954 /* Release */ = { 486 | isa = XCBuildConfiguration; 487 | buildSettings = { 488 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 489 | CODE_SIGN_STYLE = Automatic; 490 | DEVELOPMENT_ASSET_PATHS = "\"StateMachineUI/Preview Content\""; 491 | DEVELOPMENT_TEAM = Y34Y4CF9LN; 492 | ENABLE_PREVIEWS = YES; 493 | INFOPLIST_FILE = StateMachineUI/Info.plist; 494 | LD_RUNPATH_SEARCH_PATHS = ( 495 | "$(inherited)", 496 | "@executable_path/Frameworks", 497 | ); 498 | PRODUCT_BUNDLE_IDENTIFIER = com.eggsdesign.StateMachineUI; 499 | PRODUCT_NAME = "$(TARGET_NAME)"; 500 | SWIFT_VERSION = 5.0; 501 | TARGETED_DEVICE_FAMILY = "1,2"; 502 | }; 503 | name = Release; 504 | }; 505 | 454686B523C366B600E14954 /* Debug */ = { 506 | isa = XCBuildConfiguration; 507 | buildSettings = { 508 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 509 | BUNDLE_LOADER = "$(TEST_HOST)"; 510 | CODE_SIGN_STYLE = Automatic; 511 | DEVELOPMENT_TEAM = Y34Y4CF9LN; 512 | INFOPLIST_FILE = StateMachineUITests/Info.plist; 513 | IPHONEOS_DEPLOYMENT_TARGET = 13.2; 514 | LD_RUNPATH_SEARCH_PATHS = ( 515 | "$(inherited)", 516 | "@executable_path/Frameworks", 517 | "@loader_path/Frameworks", 518 | ); 519 | PRODUCT_BUNDLE_IDENTIFIER = com.eggsdesign.StateMachineUITests; 520 | PRODUCT_NAME = "$(TARGET_NAME)"; 521 | SWIFT_VERSION = 5.0; 522 | TARGETED_DEVICE_FAMILY = "1,2"; 523 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/StateMachineUI.app/StateMachineUI"; 524 | }; 525 | name = Debug; 526 | }; 527 | 454686B623C366B600E14954 /* Release */ = { 528 | isa = XCBuildConfiguration; 529 | buildSettings = { 530 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 531 | BUNDLE_LOADER = "$(TEST_HOST)"; 532 | CODE_SIGN_STYLE = Automatic; 533 | DEVELOPMENT_TEAM = Y34Y4CF9LN; 534 | INFOPLIST_FILE = StateMachineUITests/Info.plist; 535 | IPHONEOS_DEPLOYMENT_TARGET = 13.2; 536 | LD_RUNPATH_SEARCH_PATHS = ( 537 | "$(inherited)", 538 | "@executable_path/Frameworks", 539 | "@loader_path/Frameworks", 540 | ); 541 | PRODUCT_BUNDLE_IDENTIFIER = com.eggsdesign.StateMachineUITests; 542 | PRODUCT_NAME = "$(TARGET_NAME)"; 543 | SWIFT_VERSION = 5.0; 544 | TARGETED_DEVICE_FAMILY = "1,2"; 545 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/StateMachineUI.app/StateMachineUI"; 546 | }; 547 | name = Release; 548 | }; 549 | /* End XCBuildConfiguration section */ 550 | 551 | /* Begin XCConfigurationList section */ 552 | 4546868D23C366B400E14954 /* Build configuration list for PBXProject "StateMachineUI" */ = { 553 | isa = XCConfigurationList; 554 | buildConfigurations = ( 555 | 454686AF23C366B600E14954 /* Debug */, 556 | 454686B023C366B600E14954 /* Release */, 557 | ); 558 | defaultConfigurationIsVisible = 0; 559 | defaultConfigurationName = Release; 560 | }; 561 | 454686B123C366B600E14954 /* Build configuration list for PBXNativeTarget "StateMachineUI" */ = { 562 | isa = XCConfigurationList; 563 | buildConfigurations = ( 564 | 454686B223C366B600E14954 /* Debug */, 565 | 454686B323C366B600E14954 /* Release */, 566 | ); 567 | defaultConfigurationIsVisible = 0; 568 | defaultConfigurationName = Release; 569 | }; 570 | 454686B423C366B600E14954 /* Build configuration list for PBXNativeTarget "StateMachineUITests" */ = { 571 | isa = XCConfigurationList; 572 | buildConfigurations = ( 573 | 454686B523C366B600E14954 /* Debug */, 574 | 454686B623C366B600E14954 /* Release */, 575 | ); 576 | defaultConfigurationIsVisible = 0; 577 | defaultConfigurationName = Release; 578 | }; 579 | /* End XCConfigurationList section */ 580 | 581 | /* Begin XCRemoteSwiftPackageReference section */ 582 | 4572B26723C48D3500E5D289 /* XCRemoteSwiftPackageReference "url-image" */ = { 583 | isa = XCRemoteSwiftPackageReference; 584 | repositoryURL = "https://github.com/dmytro-anokhin/url-image"; 585 | requirement = { 586 | kind = upToNextMajorVersion; 587 | minimumVersion = 0.9.9; 588 | }; 589 | }; 590 | /* End XCRemoteSwiftPackageReference section */ 591 | 592 | /* Begin XCSwiftPackageProductDependency section */ 593 | 4572B26823C48D3500E5D289 /* URLImage */ = { 594 | isa = XCSwiftPackageProductDependency; 595 | package = 4572B26723C48D3500E5D289 /* XCRemoteSwiftPackageReference "url-image" */; 596 | productName = URLImage; 597 | }; 598 | /* End XCSwiftPackageProductDependency section */ 599 | }; 600 | rootObject = 4546868A23C366B400E14954 /* Project object */; 601 | } 602 | -------------------------------------------------------------------------------- /StateMachineUI.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /StateMachineUI.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /StateMachineUI.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "URLImage", 6 | "repositoryURL": "https://github.com/dmytro-anokhin/url-image", 7 | "state": { 8 | "branch": null, 9 | "revision": "5d3a5dc84f9bdf948d8b136f7a1b0099af68f114", 10 | "version": "0.9.9" 11 | } 12 | } 13 | ] 14 | }, 15 | "version": 1 16 | } 17 | -------------------------------------------------------------------------------- /StateMachineUI.xcodeproj/xcuserdata/peterringset.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | StateMachineUI.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /StateMachineUI/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // StateMachineUI 4 | // 5 | // Created by Peter Ringset on 06/01/2020. 6 | // Copyright © 2020 Ringset. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | 15 | 16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 17 | // Override point for customization after application launch. 18 | return true 19 | } 20 | 21 | // MARK: UISceneSession Lifecycle 22 | 23 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { 24 | // Called when a new scene session is being created. 25 | // Use this method to select a configuration to create the new scene with. 26 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 27 | } 28 | 29 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { 30 | // Called when the user discards a scene session. 31 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. 32 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return. 33 | } 34 | 35 | 36 | } 37 | 38 | -------------------------------------------------------------------------------- /StateMachineUI/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 | } -------------------------------------------------------------------------------- /StateMachineUI/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /StateMachineUI/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 | -------------------------------------------------------------------------------- /StateMachineUI/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 | UISceneConfigurationName 33 | Default Configuration 34 | UISceneDelegateClassName 35 | $(PRODUCT_MODULE_NAME).SceneDelegate 36 | 37 | 38 | 39 | 40 | UILaunchStoryboardName 41 | LaunchScreen 42 | UIRequiredDeviceCapabilities 43 | 44 | armv7 45 | 46 | UISupportedInterfaceOrientations 47 | 48 | UIInterfaceOrientationPortrait 49 | UIInterfaceOrientationLandscapeLeft 50 | UIInterfaceOrientationLandscapeRight 51 | 52 | UISupportedInterfaceOrientations~ipad 53 | 54 | UIInterfaceOrientationPortrait 55 | UIInterfaceOrientationPortraitUpsideDown 56 | UIInterfaceOrientationLandscapeLeft 57 | UIInterfaceOrientationLandscapeRight 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /StateMachineUI/Models/PixabayItem.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PixabayItem.swift 3 | // StateMachineUI 4 | // 5 | // Created by Peter Ringset on 06/01/2020. 6 | // Copyright © 2020 Ringset. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct PixabayItem: Decodable { 12 | let id: Int 13 | let url: URL 14 | 15 | private enum CodingKeys: String, CodingKey { 16 | case id 17 | case url = "webformatURL" 18 | } 19 | } 20 | 21 | extension PixabayItem: Identifiable { } 22 | -------------------------------------------------------------------------------- /StateMachineUI/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /StateMachineUI/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // StateMachineUI 4 | // 5 | // Created by Peter Ringset on 06/01/2020. 6 | // Copyright © 2020 Ringset. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import SwiftUI 11 | 12 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | 17 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 18 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. 19 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. 20 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). 21 | 22 | // Create the SwiftUI view that provides the window contents. 23 | let stateMachine = StateMachine(state: .start) 24 | let imageService = ImageService(session: .shared) 25 | let contentViewModel = ContentViewModel(stateMachine: stateMachine, imageService: imageService) 26 | let contentView = ContentView(viewModel: contentViewModel) 27 | 28 | // Use a UIHostingController as window root view controller. 29 | if let windowScene = scene as? UIWindowScene { 30 | let window = UIWindow(windowScene: windowScene) 31 | window.rootViewController = UIHostingController(rootView: contentView) 32 | self.window = window 33 | window.makeKeyAndVisible() 34 | } 35 | } 36 | 37 | func sceneDidDisconnect(_ scene: UIScene) { 38 | // Called as the scene is being released by the system. 39 | // This occurs shortly after the scene enters the background, or when its session is discarded. 40 | // Release any resources associated with this scene that can be re-created the next time the scene connects. 41 | // The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead). 42 | } 43 | 44 | func sceneDidBecomeActive(_ scene: UIScene) { 45 | // Called when the scene has moved from an inactive state to an active state. 46 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. 47 | } 48 | 49 | func sceneWillResignActive(_ scene: UIScene) { 50 | // Called when the scene will move from an active state to an inactive state. 51 | // This may occur due to temporary interruptions (ex. an incoming phone call). 52 | } 53 | 54 | func sceneWillEnterForeground(_ scene: UIScene) { 55 | // Called as the scene transitions from the background to the foreground. 56 | // Use this method to undo the changes made on entering the background. 57 | } 58 | 59 | func sceneDidEnterBackground(_ scene: UIScene) { 60 | // Called as the scene transitions from the foreground to the background. 61 | // Use this method to save data, release shared resources, and store enough scene-specific state information 62 | // to restore the scene back to its current state. 63 | } 64 | 65 | 66 | } 67 | 68 | -------------------------------------------------------------------------------- /StateMachineUI/Services/ImageService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageService.swift 3 | // StateMachineUI 4 | // 5 | // Created by Peter Ringset on 06/01/2020. 6 | // Copyright © 2020 Ringset. All rights reserved. 7 | // 8 | 9 | import Combine 10 | import Foundation 11 | 12 | class ImageService: ObservableObject { 13 | 14 | private let session: URLSession 15 | 16 | init(session: URLSession) { 17 | self.session = session 18 | } 19 | 20 | func search(text: String) -> AnyPublisher<[PixabayItem], Error>? { 21 | guard let hostURL = URL(string: "https://pixabay.com"), 22 | let apiURL = URL(string: "api/", relativeTo: hostURL), 23 | var components = URLComponents(url: apiURL, resolvingAgainstBaseURL: true) else { 24 | return nil 25 | } 26 | components.queryItems = [ 27 | URLQueryItem(name: "key", value: ""), 28 | URLQueryItem(name: "q", value: text) 29 | ] 30 | 31 | guard let constructedURL = components.url else { 32 | return nil 33 | } 34 | 35 | let publisher = session.dataTaskPublisher(for: constructedURL) 36 | .map { $0.data } 37 | .decode(type: Response.self, decoder: JSONDecoder()) 38 | .map { $0.items } 39 | .receive(on: RunLoop.main) 40 | .eraseToAnyPublisher() 41 | 42 | return publisher 43 | } 44 | 45 | } 46 | 47 | extension ImageService { 48 | 49 | struct Response: Decodable { 50 | let items: [PixabayItem] 51 | enum CodingKeys: String, CodingKey { 52 | case items = "hits" 53 | } 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /StateMachineUI/StateMachine.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StateMachine.swift 3 | // StateMachineUI 4 | // 5 | // Created by Peter Ringset on 06/01/2020. 6 | // Copyright © 2020 Ringset. All rights reserved. 7 | // 8 | 9 | import Combine 10 | import Foundation 11 | 12 | class StateMachine { 13 | 14 | enum State { 15 | case start 16 | case searching 17 | case loading 18 | case searchResults 19 | case error 20 | } 21 | 22 | enum Event { 23 | case startSearch 24 | case cancel 25 | case search 26 | case success 27 | case failure 28 | } 29 | 30 | private(set) var state: State { 31 | didSet { stateSubject.send(self.state) } 32 | } 33 | private let stateSubject: PassthroughSubject 34 | let statePublisher: AnyPublisher 35 | 36 | init(state: State) { 37 | self.state = state 38 | self.stateSubject = PassthroughSubject() 39 | self.statePublisher = self.stateSubject.eraseToAnyPublisher() 40 | } 41 | 42 | } 43 | 44 | // MARK: - State changes 45 | 46 | extension StateMachine { 47 | 48 | @discardableResult func tryEvent(_ event: Event) -> Bool { 49 | guard let state = nextState(for: event) else { 50 | return false 51 | } 52 | 53 | self.state = state 54 | return true 55 | } 56 | 57 | private func nextState(for event: Event) -> State? { 58 | switch state { 59 | case .start: 60 | switch event { 61 | case .startSearch: return .searching 62 | case .cancel, .search, .success, .failure: return nil 63 | } 64 | case .searching: 65 | switch event { 66 | case .search: return .loading 67 | case .startSearch: return nil 68 | case .cancel: return .start 69 | case .success, .failure: return nil 70 | } 71 | case .loading: 72 | switch event { 73 | case .search, .cancel, .startSearch: return nil 74 | case .success: return .searchResults 75 | case .failure: return .error 76 | } 77 | case .searchResults: 78 | switch event { 79 | case .startSearch: return .searching 80 | case .cancel, .search, .success, .failure: return nil 81 | } 82 | case .error: 83 | switch event { 84 | case .startSearch: return .searching 85 | case .cancel, .search, .success, .failure: return nil 86 | } 87 | } 88 | } 89 | 90 | } 91 | -------------------------------------------------------------------------------- /StateMachineUI/View models/ContentViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentViewModel.swift 3 | // StateMachineUI 4 | // 5 | // Created by Peter Ringset on 06/01/2020. 6 | // Copyright © 2020 Ringset. All rights reserved. 7 | // 8 | 9 | import Combine 10 | import Foundation 11 | 12 | class ContentViewModel: ObservableObject { 13 | 14 | private let stateMachine: StateMachine 15 | private let imageService: ImageService 16 | 17 | private var stateCancellable: AnyCancellable? 18 | private var searchCancelleble: AnyCancellable? 19 | 20 | @Published var searchText: String = "" 21 | @Published var state: StateMachine.State { 22 | willSet { leaveState(state) } 23 | didSet { enterState(state) } 24 | } 25 | @Published var isSearching: Bool = false 26 | private var searchItems: [PixabayItem] = [] 27 | 28 | var showSearchCancelButton: Bool { 29 | return stateMachine.state == .searching 30 | } 31 | 32 | init(stateMachine: StateMachine, imageService: ImageService) { 33 | self.stateMachine = stateMachine 34 | self.imageService = imageService 35 | 36 | self.state = stateMachine.state 37 | 38 | self.stateCancellable = stateMachine.statePublisher.sink { state in 39 | self.state = state 40 | } 41 | } 42 | 43 | func searchStatusChanged(_ value: SearchBar.Status) { 44 | let event: StateMachine.Event = { 45 | switch value { 46 | case .searching: return .startSearch 47 | case .searched: return .search 48 | case .notSearching: return .cancel 49 | } 50 | }() 51 | stateMachine.tryEvent(event) 52 | } 53 | 54 | func createSearchResultsViewModel() -> SearchResultsViewModel { 55 | return SearchResultsViewModel(items: searchItems) 56 | } 57 | 58 | } 59 | 60 | // MARK: - Search 61 | 62 | extension ContentViewModel { 63 | 64 | func search(_ text: String) { 65 | searchText = text 66 | stateMachine.tryEvent(.search) 67 | } 68 | 69 | private func search() { 70 | searchCancelleble = imageService.search(text: searchText)?.sink(receiveCompletion: { completion in 71 | switch completion { 72 | case .finished: break 73 | case .failure: self.stateMachine.tryEvent(.failure) 74 | } 75 | }, receiveValue: { items in 76 | self.searchItems = items 77 | self.stateMachine.tryEvent(.success) 78 | }) 79 | } 80 | 81 | } 82 | 83 | // MARK: - State changes 84 | 85 | extension ContentViewModel { 86 | 87 | func leaveState(_ state: StateMachine.State) { 88 | if case .searching = state { 89 | isSearching = false 90 | } 91 | } 92 | 93 | func enterState(_ state: StateMachine.State) { 94 | if case .searching = state { 95 | isSearching = true 96 | } 97 | if case .loading = state { 98 | search() 99 | } 100 | } 101 | 102 | } 103 | -------------------------------------------------------------------------------- /StateMachineUI/View models/SearchResultsViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SearchResultsViewModel.swift 3 | // StateMachineUI 4 | // 5 | // Created by Peter Ringset on 06/01/2020. 6 | // Copyright © 2020 Ringset. All rights reserved. 7 | // 8 | 9 | import Combine 10 | import Foundation 11 | 12 | class SearchResultsViewModel: ObservableObject { 13 | 14 | let items: [PixabayItem] 15 | 16 | init(items: [PixabayItem]) { 17 | self.items = items 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /StateMachineUI/Views/Components/ActivityIndicator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ActivityIndicator.swift 3 | // StateMachineUI 4 | // 5 | // Created by Peter Ringset on 06/01/2020. 6 | // Copyright © 2020 Ringset. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct ActivityIndicator: UIViewRepresentable { 12 | 13 | @Binding var isAnimating: Bool 14 | let style: UIActivityIndicatorView.Style 15 | 16 | func makeUIView(context: UIViewRepresentableContext) -> UIActivityIndicatorView { 17 | return UIActivityIndicatorView(style: style) 18 | } 19 | 20 | func updateUIView(_ uiView: UIActivityIndicatorView, context: UIViewRepresentableContext) { 21 | isAnimating ? uiView.startAnimating() : uiView.stopAnimating() 22 | } 23 | 24 | } 25 | 26 | struct ActivityIndicator_Previews: PreviewProvider { 27 | static var previews: some View { 28 | ActivityIndicator(isAnimating: .constant(true), style: .medium) 29 | .previewLayout(.sizeThatFits) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /StateMachineUI/Views/Components/SearchBar.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SearchBar.swift 3 | // StateMachineUI 4 | // 5 | // Created by Peter Ringset on 06/01/2020. 6 | // Copyright © 2020 Ringset. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct SearchBar: UIViewRepresentable { 12 | 13 | enum Status { 14 | case notSearching, searching, searched 15 | } 16 | 17 | @Binding var text: String 18 | @Binding var isEditing: Bool 19 | let showsCancelButton: Bool 20 | let searchingChanged: (Status) -> Void 21 | 22 | class Coordinator: NSObject, UISearchBarDelegate { 23 | 24 | @Binding var text: String 25 | 26 | let searchingChanged: (Status) -> Void 27 | 28 | init(text: Binding, searchingChanged: @escaping (Status) -> Void) { 29 | _text = text 30 | self.searchingChanged = searchingChanged 31 | } 32 | 33 | func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) { 34 | searchingChanged(.searching) 35 | } 36 | 37 | func searchBarTextDidEndEditing(_ searchBar: UISearchBar) { 38 | searchingChanged(.notSearching) 39 | } 40 | 41 | func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) { 42 | text = searchText 43 | } 44 | 45 | func searchBarSearchButtonClicked(_ searchBar: UISearchBar) { 46 | searchingChanged(.searched) 47 | searchBar.resignFirstResponder() 48 | } 49 | 50 | func searchBarCancelButtonClicked(_ searchBar: UISearchBar) { 51 | searchBar.resignFirstResponder() 52 | } 53 | } 54 | 55 | func makeCoordinator() -> SearchBar.Coordinator { 56 | return Coordinator(text: $text, searchingChanged: searchingChanged) 57 | } 58 | 59 | func makeUIView(context: UIViewRepresentableContext) -> UISearchBar { 60 | let searchBar = UISearchBar(frame: .zero) 61 | searchBar.delegate = context.coordinator 62 | searchBar.autocapitalizationType = .none 63 | searchBar.backgroundImage = UIImage() 64 | return searchBar 65 | } 66 | 67 | func updateUIView(_ uiView: UISearchBar, context: UIViewRepresentableContext) { 68 | uiView.text = text 69 | uiView.showsCancelButton = showsCancelButton 70 | if isEditing && !uiView.isFirstResponder { 71 | uiView.becomeFirstResponder() 72 | } else if !isEditing && uiView.isFirstResponder { 73 | uiView.resignFirstResponder() 74 | } 75 | } 76 | 77 | } 78 | 79 | struct SearchBar_Previews: PreviewProvider { 80 | static var previews: some View { 81 | SearchBar(text: .constant(""), isEditing: .constant(false), showsCancelButton: false, searchingChanged: { _ in }) 82 | .previewLayout(.sizeThatFits) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /StateMachineUI/Views/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // StateMachineUI 4 | // 5 | // Created by Peter Ringset on 06/01/2020. 6 | // Copyright © 2020 Ringset. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct ContentView: View { 12 | 13 | @ObservedObject var viewModel: ContentViewModel 14 | 15 | var body: some View { 16 | return NavigationView { 17 | VStack(spacing: 0) { 18 | SearchBar( 19 | text: $viewModel.searchText, 20 | isEditing: $viewModel.isSearching, 21 | showsCancelButton: viewModel.showSearchCancelButton, 22 | searchingChanged: viewModel.searchStatusChanged 23 | ) 24 | 25 | self.content 26 | 27 | Spacer() 28 | }.navigationBarTitle(Text("Search images")) 29 | .edgesIgnoringSafeArea([.bottom]) 30 | } 31 | } 32 | 33 | private var content: some View { 34 | switch viewModel.state { 35 | case .start: return AnyView(StartView()) 36 | case .searching: return AnyView(SearchingView(action: { self.viewModel.search($0) })) 37 | case .loading: return AnyView(LoadingView()) 38 | case .searchResults: return AnyView(SearchResultsView(viewModel: viewModel.createSearchResultsViewModel())) 39 | case .error: return AnyView(ErrorView()) 40 | } 41 | } 42 | 43 | } 44 | 45 | struct ContentView_Previews: PreviewProvider { 46 | static var previews: some View { 47 | ContentView(viewModel: ContentViewModel(stateMachine: StateMachine(state: .start), imageService: ImageService(session: .shared))) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /StateMachineUI/Views/State views/ErrorView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ErrorView.swift 3 | // StateMachineUI 4 | // 5 | // Created by Peter Ringset on 07/01/2020. 6 | // Copyright © 2020 Ringset. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct ErrorView: View { 12 | var body: some View { 13 | VStack { 14 | HStack { 15 | Text("Ouch") 16 | Image(systemName: "exclamationmark.triangle.fill").foregroundColor(Color.orange) 17 | }.font(.headline) 18 | Text("We're sorry, something went wrong. Please try again.").font(.body) 19 | }.padding() 20 | } 21 | } 22 | 23 | struct ErrorView_Previews: PreviewProvider { 24 | static var previews: some View { 25 | ErrorView().previewLayout(.sizeThatFits) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /StateMachineUI/Views/State views/LoadingView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoadingView.swift 3 | // StateMachineUI 4 | // 5 | // Created by Peter Ringset on 06/01/2020. 6 | // Copyright © 2020 Ringset. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct LoadingView: View { 12 | var body: some View { 13 | HStack { 14 | Text("Loading...").font(.body) 15 | ActivityIndicator(isAnimating: .constant(true), style: .medium) 16 | }.padding() 17 | } 18 | } 19 | 20 | struct LoadingView_Previews: PreviewProvider { 21 | static var previews: some View { 22 | LoadingView().previewLayout(.sizeThatFits) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /StateMachineUI/Views/State views/SearchResultsView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SearchResultsView.swift 3 | // StateMachineUI 4 | // 5 | // Created by Peter Ringset on 06/01/2020. 6 | // Copyright © 2020 Ringset. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import URLImage 11 | 12 | struct SearchResultsView: View { 13 | 14 | let viewModel: SearchResultsViewModel 15 | 16 | var body: some View { 17 | GeometryReader { geometry in 18 | if !self.viewModel.items.isEmpty { 19 | List { 20 | ForEach(self.dataCollection(items: self.viewModel.items, size: geometry.size)) { rowModel in 21 | HStack(spacing: 0) { 22 | ForEach(rowModel.items) { item in 23 | URLImage(item.url, 24 | processors: [ 25 | Resize(size: self.size(for: geometry), scale: UIScreen.main.scale) 26 | ], 27 | placeholder: Image(systemName: "photo"), 28 | content: { $0.image.resizable().aspectRatio(contentMode: .fit).clipped() } 29 | ).frame(size: self.size(for: geometry), alignment: .center) 30 | } 31 | }.listRowInsets(EdgeInsets()) 32 | } 33 | }.onAppear { 34 | UITableView.appearance().separatorStyle = .none 35 | } 36 | } else { 37 | VStack(alignment: .center) { 38 | Spacer().frame(height: 20) 39 | HStack { 40 | Text("No Results") 41 | Image(systemName: "exclamationmark.triangle") 42 | }.font(.headline).frame(width: geometry.size.width) 43 | Text("Try refining your search term to get more results.").font(.body) 44 | } 45 | } 46 | } 47 | } 48 | 49 | private func size(for geometry: GeometryProxy) -> CGSize { 50 | let size = geometry.size.width / CGFloat(columnCount(for: geometry.size)) 51 | return CGSize(width: size, height: size) 52 | } 53 | 54 | private func columnCount(for size: CGSize) -> Int { 55 | return Int(ceil(size.width / 138)) 56 | } 57 | 58 | private func dataCollection(items: [PixabayItem], size: CGSize) -> [RowModel] { 59 | let strideSize = columnCount(for: size) 60 | let rowModels = stride(from: items.startIndex, to: items.endIndex, by: strideSize).compactMap { index -> RowModel? in 61 | let range = index ..< min(index + strideSize, items.endIndex) 62 | let subItems = items[range] 63 | return try? RowModel(items: Array(subItems)) 64 | } 65 | 66 | return rowModels 67 | } 68 | 69 | } 70 | 71 | extension View { 72 | 73 | func frame(size: CGSize, alignment: Alignment) -> some View { 74 | return self.frame(width: size.width, height: size.height, alignment: alignment) 75 | } 76 | 77 | } 78 | 79 | private struct RowModel: Identifiable { 80 | let id: Int 81 | let items: [PixabayItem] 82 | 83 | init(items: [PixabayItem]) throws { 84 | guard let first = items.first else { 85 | throw NSError(domain: "SM", code: 0, userInfo: [ 86 | NSLocalizedDescriptionKey: "Cannot initalize with empty array" 87 | ]) 88 | } 89 | self.id = first.id 90 | self.items = items 91 | } 92 | } 93 | 94 | struct SearchResultsView_Previews: PreviewProvider { 95 | static var previews: some View { 96 | Group { 97 | SearchResultsView(viewModel: SearchResultsViewModel(items: [ 98 | PixabayItem(id: 1, url: URL(string: "http://eggsdesign.com")!), 99 | PixabayItem(id: 2, url: URL(string: "http://eggsdesign.com")!), 100 | PixabayItem(id: 3, url: URL(string: "http://eggsdesign.com")!), 101 | PixabayItem(id: 4, url: URL(string: "http://eggsdesign.com")!) 102 | ])).previewLayout(.fixed(width: 414, height: 300)) 103 | 104 | SearchResultsView(viewModel: SearchResultsViewModel(items: [])) 105 | .previewLayout(.fixed(width: 414, height: 200)) 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /StateMachineUI/Views/State views/SearchingView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SearchingView.swift 3 | // StateMachineUI 4 | // 5 | // Created by Peter Ringset on 06/01/2020. 6 | // Copyright © 2020 Ringset. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct SearchingView: View { 12 | 13 | let action: (String) -> Void 14 | 15 | var body: some View { 16 | VStack(alignment: .leading) { 17 | Text("Recent searches").font(.system(.headline)) 18 | 19 | VStack(alignment: .leading) { 20 | Divider() 21 | textButton("Dogs") 22 | Divider() 23 | textButton("Ponies") 24 | Divider() 25 | textButton("Unicorns") 26 | Divider() 27 | textButton("Cats") 28 | Divider() 29 | } 30 | }.padding() 31 | } 32 | 33 | private func textButton(_ text: String) -> Button { 34 | return Button(action: { self.action(text) }, label: { Text(text) }) 35 | } 36 | } 37 | 38 | struct SearchingView_Previews: PreviewProvider { 39 | static var previews: some View { 40 | SearchingView(action: { _ in }).previewLayout(.sizeThatFits) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /StateMachineUI/Views/State views/StartView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StartView.swift 3 | // StateMachineUI 4 | // 5 | // Created by Peter Ringset on 06/01/2020. 6 | // Copyright © 2020 Ringset. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct StartView: View { 12 | var body: some View { 13 | VStack { 14 | Spacer().frame(height: 20) 15 | HStack { 16 | Text("Search for images").font(.headline) 17 | Image(systemName: "magnifyingglass").font(.headline) 18 | } 19 | Text("Type in the search field above, and hit the search button to find images").font(.body) 20 | }.padding() 21 | } 22 | } 23 | 24 | struct StartView_Previews: PreviewProvider { 25 | static var previews: some View { 26 | StartView().previewLayout(.sizeThatFits) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /StateMachineUITests/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 | -------------------------------------------------------------------------------- /StateMachineUITests/StateMachineUITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StateMachineUITests.swift 3 | // StateMachineUITests 4 | // 5 | // Created by Peter Ringset on 06/01/2020. 6 | // Copyright © 2020 Ringset. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import StateMachineUI 11 | 12 | class StateMachineUITests: XCTestCase { 13 | 14 | override func setUp() { 15 | // Put setup code here. This method is called before the invocation of each test method in the class. 16 | } 17 | 18 | override func tearDown() { 19 | // Put teardown code here. This method is called after the invocation of each test method in the class. 20 | } 21 | 22 | func testExample() { 23 | // This is an example of a functional test case. 24 | // Use XCTAssert and related functions to verify your tests produce the correct results. 25 | } 26 | 27 | func testPerformanceExample() { 28 | // This is an example of a performance test case. 29 | self.measure { 30 | // Put the code you want to measure the time of here. 31 | } 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /demo/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterringset/StateMachineUI/f68d8f48e92c39596d791d1cf3b2cb3316e3a94c/demo/demo.gif --------------------------------------------------------------------------------