├── PokeList.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ ├── xcshareddata │ │ └── IDEWorkspaceChecks.plist │ └── xcuserdata │ │ └── abrown.xcuserdatad │ │ └── UserInterfaceState.xcuserstate └── xcuserdata │ └── abrown.xcuserdatad │ ├── xcdebugger │ └── Breakpoints_v2.xcbkptlist │ └── xcschemes │ └── xcschememanagement.plist └── PokeList ├── API ├── APIError.swift ├── APIService.swift ├── APISession.swift └── RequestBuilder.swift ├── AppDelegate.swift ├── Assets.xcassets ├── .DS_Store ├── AppIcon.appiconset │ └── Contents.json ├── Contents.json └── house.imageset │ ├── Contents.json │ ├── home2-white-1.pdf │ ├── home2-white-2.pdf │ └── home2-white.pdf ├── Base.lproj └── LaunchScreen.storyboard ├── Info.plist ├── PokeList.xcdatamodeld ├── .xccurrentversion └── PokeList.xcdatamodel │ └── contents ├── PokemonAPI ├── Pokemon.swift ├── PokemonAPIResponse.swift ├── PokemonEndpoint.swift ├── PokemonListItem.swift └── PokemonService.swift ├── PokemonDetail ├── PokemonDetailView.swift └── PokemonDetailViewModel.swift ├── PokemonList ├── PokemonListView.swift └── PokemonListViewModel.swift ├── Preview Content └── Preview Assets.xcassets │ └── Contents.json └── SceneDelegate.swift /PokeList.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 183300292495042100C2038D /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 183300282495042100C2038D /* AppDelegate.swift */; }; 11 | 1833002B2495042100C2038D /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1833002A2495042100C2038D /* SceneDelegate.swift */; }; 12 | 1833002E2495042100C2038D /* PokeList.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 1833002C2495042100C2038D /* PokeList.xcdatamodeld */; }; 13 | 183300302495042100C2038D /* PokemonListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1833002F2495042100C2038D /* PokemonListView.swift */; }; 14 | 183300322495042200C2038D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 183300312495042200C2038D /* Assets.xcassets */; }; 15 | 183300352495042200C2038D /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 183300342495042200C2038D /* Preview Assets.xcassets */; }; 16 | 183300382495042200C2038D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 183300362495042200C2038D /* LaunchScreen.storyboard */; }; 17 | 183300402496801100C2038D /* PokemonListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1833003F2496801100C2038D /* PokemonListViewModel.swift */; }; 18 | 183300422496804A00C2038D /* PokemonService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 183300412496804A00C2038D /* PokemonService.swift */; }; 19 | 183300442496806700C2038D /* PokemonEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 183300432496806700C2038D /* PokemonEndpoint.swift */; }; 20 | 183300472496809500C2038D /* RequestBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 183300462496809500C2038D /* RequestBuilder.swift */; }; 21 | 183300492496816D00C2038D /* APIService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 183300482496816D00C2038D /* APIService.swift */; }; 22 | 1833004B2496824000C2038D /* APIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1833004A2496824000C2038D /* APIError.swift */; }; 23 | 1833004E2496848600C2038D /* PokemonListItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1833004D2496848600C2038D /* PokemonListItem.swift */; }; 24 | 1833005224969C2D00C2038D /* APISession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1833005124969C2D00C2038D /* APISession.swift */; }; 25 | 1833005424969E1600C2038D /* PokemonAPIResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1833005324969E1600C2038D /* PokemonAPIResponse.swift */; }; 26 | 18330056249752A900C2038D /* PokemonDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18330055249752A900C2038D /* PokemonDetailView.swift */; }; 27 | 1833005A249752F600C2038D /* PokemonDetailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18330059249752F600C2038D /* PokemonDetailViewModel.swift */; }; 28 | 1833005E2497595000C2038D /* Pokemon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1833005D2497595000C2038D /* Pokemon.swift */; }; 29 | /* End PBXBuildFile section */ 30 | 31 | /* Begin PBXFileReference section */ 32 | 183300252495042100C2038D /* PokeList.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = PokeList.app; sourceTree = BUILT_PRODUCTS_DIR; }; 33 | 183300282495042100C2038D /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 34 | 1833002A2495042100C2038D /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 35 | 1833002D2495042100C2038D /* PokeList.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = PokeList.xcdatamodel; sourceTree = ""; }; 36 | 1833002F2495042100C2038D /* PokemonListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PokemonListView.swift; sourceTree = ""; }; 37 | 183300312495042200C2038D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 38 | 183300342495042200C2038D /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 39 | 183300372495042200C2038D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 40 | 183300392495042200C2038D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 41 | 1833003F2496801100C2038D /* PokemonListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PokemonListViewModel.swift; sourceTree = ""; }; 42 | 183300412496804A00C2038D /* PokemonService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PokemonService.swift; sourceTree = ""; }; 43 | 183300432496806700C2038D /* PokemonEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PokemonEndpoint.swift; sourceTree = ""; }; 44 | 183300462496809500C2038D /* RequestBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestBuilder.swift; sourceTree = ""; }; 45 | 183300482496816D00C2038D /* APIService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIService.swift; sourceTree = ""; }; 46 | 1833004A2496824000C2038D /* APIError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIError.swift; sourceTree = ""; }; 47 | 1833004D2496848600C2038D /* PokemonListItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PokemonListItem.swift; sourceTree = ""; }; 48 | 1833005124969C2D00C2038D /* APISession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APISession.swift; sourceTree = ""; }; 49 | 1833005324969E1600C2038D /* PokemonAPIResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PokemonAPIResponse.swift; sourceTree = ""; }; 50 | 18330055249752A900C2038D /* PokemonDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PokemonDetailView.swift; sourceTree = ""; }; 51 | 18330059249752F600C2038D /* PokemonDetailViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PokemonDetailViewModel.swift; sourceTree = ""; }; 52 | 1833005D2497595000C2038D /* Pokemon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Pokemon.swift; sourceTree = ""; }; 53 | /* End PBXFileReference section */ 54 | 55 | /* Begin PBXFrameworksBuildPhase section */ 56 | 183300222495042100C2038D /* Frameworks */ = { 57 | isa = PBXFrameworksBuildPhase; 58 | buildActionMask = 2147483647; 59 | files = ( 60 | ); 61 | runOnlyForDeploymentPostprocessing = 0; 62 | }; 63 | /* End PBXFrameworksBuildPhase section */ 64 | 65 | /* Begin PBXGroup section */ 66 | 1833001C2495042100C2038D = { 67 | isa = PBXGroup; 68 | children = ( 69 | 183300272495042100C2038D /* PokeList */, 70 | 183300262495042100C2038D /* Products */, 71 | ); 72 | sourceTree = ""; 73 | }; 74 | 183300262495042100C2038D /* Products */ = { 75 | isa = PBXGroup; 76 | children = ( 77 | 183300252495042100C2038D /* PokeList.app */, 78 | ); 79 | name = Products; 80 | sourceTree = ""; 81 | }; 82 | 183300272495042100C2038D /* PokeList */ = { 83 | isa = PBXGroup; 84 | children = ( 85 | 18330058249752E300C2038D /* PokemonDetail */, 86 | 18330057249752D400C2038D /* PokemonList */, 87 | 1833004C2496847300C2038D /* PokemonAPI */, 88 | 183300452496807B00C2038D /* API */, 89 | 183300282495042100C2038D /* AppDelegate.swift */, 90 | 1833002A2495042100C2038D /* SceneDelegate.swift */, 91 | 183300312495042200C2038D /* Assets.xcassets */, 92 | 183300362495042200C2038D /* LaunchScreen.storyboard */, 93 | 183300392495042200C2038D /* Info.plist */, 94 | 1833002C2495042100C2038D /* PokeList.xcdatamodeld */, 95 | 183300332495042200C2038D /* Preview Content */, 96 | ); 97 | path = PokeList; 98 | sourceTree = ""; 99 | }; 100 | 183300332495042200C2038D /* Preview Content */ = { 101 | isa = PBXGroup; 102 | children = ( 103 | 183300342495042200C2038D /* Preview Assets.xcassets */, 104 | ); 105 | path = "Preview Content"; 106 | sourceTree = ""; 107 | }; 108 | 183300452496807B00C2038D /* API */ = { 109 | isa = PBXGroup; 110 | children = ( 111 | 183300462496809500C2038D /* RequestBuilder.swift */, 112 | 183300482496816D00C2038D /* APIService.swift */, 113 | 1833004A2496824000C2038D /* APIError.swift */, 114 | 1833005124969C2D00C2038D /* APISession.swift */, 115 | ); 116 | path = API; 117 | sourceTree = ""; 118 | }; 119 | 1833004C2496847300C2038D /* PokemonAPI */ = { 120 | isa = PBXGroup; 121 | children = ( 122 | 183300412496804A00C2038D /* PokemonService.swift */, 123 | 183300432496806700C2038D /* PokemonEndpoint.swift */, 124 | 1833004D2496848600C2038D /* PokemonListItem.swift */, 125 | 1833005324969E1600C2038D /* PokemonAPIResponse.swift */, 126 | 1833005D2497595000C2038D /* Pokemon.swift */, 127 | ); 128 | path = PokemonAPI; 129 | sourceTree = ""; 130 | }; 131 | 18330057249752D400C2038D /* PokemonList */ = { 132 | isa = PBXGroup; 133 | children = ( 134 | 1833003F2496801100C2038D /* PokemonListViewModel.swift */, 135 | 1833002F2495042100C2038D /* PokemonListView.swift */, 136 | ); 137 | path = PokemonList; 138 | sourceTree = ""; 139 | }; 140 | 18330058249752E300C2038D /* PokemonDetail */ = { 141 | isa = PBXGroup; 142 | children = ( 143 | 18330055249752A900C2038D /* PokemonDetailView.swift */, 144 | 18330059249752F600C2038D /* PokemonDetailViewModel.swift */, 145 | ); 146 | path = PokemonDetail; 147 | sourceTree = ""; 148 | }; 149 | /* End PBXGroup section */ 150 | 151 | /* Begin PBXNativeTarget section */ 152 | 183300242495042100C2038D /* PokeList */ = { 153 | isa = PBXNativeTarget; 154 | buildConfigurationList = 1833003C2495042200C2038D /* Build configuration list for PBXNativeTarget "PokeList" */; 155 | buildPhases = ( 156 | 183300212495042100C2038D /* Sources */, 157 | 183300222495042100C2038D /* Frameworks */, 158 | 183300232495042100C2038D /* Resources */, 159 | ); 160 | buildRules = ( 161 | ); 162 | dependencies = ( 163 | ); 164 | name = PokeList; 165 | productName = PokeList; 166 | productReference = 183300252495042100C2038D /* PokeList.app */; 167 | productType = "com.apple.product-type.application"; 168 | }; 169 | /* End PBXNativeTarget section */ 170 | 171 | /* Begin PBXProject section */ 172 | 1833001D2495042100C2038D /* Project object */ = { 173 | isa = PBXProject; 174 | attributes = { 175 | LastSwiftUpdateCheck = 1150; 176 | LastUpgradeCheck = 1150; 177 | ORGANIZATIONNAME = ABrown; 178 | TargetAttributes = { 179 | 183300242495042100C2038D = { 180 | CreatedOnToolsVersion = 11.5; 181 | }; 182 | }; 183 | }; 184 | buildConfigurationList = 183300202495042100C2038D /* Build configuration list for PBXProject "PokeList" */; 185 | compatibilityVersion = "Xcode 9.3"; 186 | developmentRegion = en; 187 | hasScannedForEncodings = 0; 188 | knownRegions = ( 189 | en, 190 | Base, 191 | ); 192 | mainGroup = 1833001C2495042100C2038D; 193 | productRefGroup = 183300262495042100C2038D /* Products */; 194 | projectDirPath = ""; 195 | projectRoot = ""; 196 | targets = ( 197 | 183300242495042100C2038D /* PokeList */, 198 | ); 199 | }; 200 | /* End PBXProject section */ 201 | 202 | /* Begin PBXResourcesBuildPhase section */ 203 | 183300232495042100C2038D /* Resources */ = { 204 | isa = PBXResourcesBuildPhase; 205 | buildActionMask = 2147483647; 206 | files = ( 207 | 183300382495042200C2038D /* LaunchScreen.storyboard in Resources */, 208 | 183300352495042200C2038D /* Preview Assets.xcassets in Resources */, 209 | 183300322495042200C2038D /* Assets.xcassets in Resources */, 210 | ); 211 | runOnlyForDeploymentPostprocessing = 0; 212 | }; 213 | /* End PBXResourcesBuildPhase section */ 214 | 215 | /* Begin PBXSourcesBuildPhase section */ 216 | 183300212495042100C2038D /* Sources */ = { 217 | isa = PBXSourcesBuildPhase; 218 | buildActionMask = 2147483647; 219 | files = ( 220 | 183300442496806700C2038D /* PokemonEndpoint.swift in Sources */, 221 | 1833002E2495042100C2038D /* PokeList.xcdatamodeld in Sources */, 222 | 183300292495042100C2038D /* AppDelegate.swift in Sources */, 223 | 1833005E2497595000C2038D /* Pokemon.swift in Sources */, 224 | 1833004B2496824000C2038D /* APIError.swift in Sources */, 225 | 183300302495042100C2038D /* PokemonListView.swift in Sources */, 226 | 18330056249752A900C2038D /* PokemonDetailView.swift in Sources */, 227 | 183300402496801100C2038D /* PokemonListViewModel.swift in Sources */, 228 | 1833005424969E1600C2038D /* PokemonAPIResponse.swift in Sources */, 229 | 1833005A249752F600C2038D /* PokemonDetailViewModel.swift in Sources */, 230 | 1833004E2496848600C2038D /* PokemonListItem.swift in Sources */, 231 | 1833002B2495042100C2038D /* SceneDelegate.swift in Sources */, 232 | 183300422496804A00C2038D /* PokemonService.swift in Sources */, 233 | 183300492496816D00C2038D /* APIService.swift in Sources */, 234 | 183300472496809500C2038D /* RequestBuilder.swift in Sources */, 235 | 1833005224969C2D00C2038D /* APISession.swift in Sources */, 236 | ); 237 | runOnlyForDeploymentPostprocessing = 0; 238 | }; 239 | /* End PBXSourcesBuildPhase section */ 240 | 241 | /* Begin PBXVariantGroup section */ 242 | 183300362495042200C2038D /* LaunchScreen.storyboard */ = { 243 | isa = PBXVariantGroup; 244 | children = ( 245 | 183300372495042200C2038D /* Base */, 246 | ); 247 | name = LaunchScreen.storyboard; 248 | sourceTree = ""; 249 | }; 250 | /* End PBXVariantGroup section */ 251 | 252 | /* Begin XCBuildConfiguration section */ 253 | 1833003A2495042200C2038D /* Debug */ = { 254 | isa = XCBuildConfiguration; 255 | buildSettings = { 256 | ALWAYS_SEARCH_USER_PATHS = NO; 257 | CLANG_ANALYZER_NONNULL = YES; 258 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 259 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 260 | CLANG_CXX_LIBRARY = "libc++"; 261 | CLANG_ENABLE_MODULES = YES; 262 | CLANG_ENABLE_OBJC_ARC = YES; 263 | CLANG_ENABLE_OBJC_WEAK = YES; 264 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 265 | CLANG_WARN_BOOL_CONVERSION = YES; 266 | CLANG_WARN_COMMA = YES; 267 | CLANG_WARN_CONSTANT_CONVERSION = YES; 268 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 269 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 270 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 271 | CLANG_WARN_EMPTY_BODY = YES; 272 | CLANG_WARN_ENUM_CONVERSION = YES; 273 | CLANG_WARN_INFINITE_RECURSION = YES; 274 | CLANG_WARN_INT_CONVERSION = YES; 275 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 276 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 277 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 278 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 279 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 280 | CLANG_WARN_STRICT_PROTOTYPES = YES; 281 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 282 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 283 | CLANG_WARN_UNREACHABLE_CODE = YES; 284 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 285 | COPY_PHASE_STRIP = NO; 286 | DEBUG_INFORMATION_FORMAT = dwarf; 287 | ENABLE_STRICT_OBJC_MSGSEND = YES; 288 | ENABLE_TESTABILITY = YES; 289 | GCC_C_LANGUAGE_STANDARD = gnu11; 290 | GCC_DYNAMIC_NO_PIC = NO; 291 | GCC_NO_COMMON_BLOCKS = YES; 292 | GCC_OPTIMIZATION_LEVEL = 0; 293 | GCC_PREPROCESSOR_DEFINITIONS = ( 294 | "DEBUG=1", 295 | "$(inherited)", 296 | ); 297 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 298 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 299 | GCC_WARN_UNDECLARED_SELECTOR = YES; 300 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 301 | GCC_WARN_UNUSED_FUNCTION = YES; 302 | GCC_WARN_UNUSED_VARIABLE = YES; 303 | IPHONEOS_DEPLOYMENT_TARGET = 13.5; 304 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 305 | MTL_FAST_MATH = YES; 306 | ONLY_ACTIVE_ARCH = YES; 307 | SDKROOT = iphoneos; 308 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 309 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 310 | }; 311 | name = Debug; 312 | }; 313 | 1833003B2495042200C2038D /* Release */ = { 314 | isa = XCBuildConfiguration; 315 | buildSettings = { 316 | ALWAYS_SEARCH_USER_PATHS = NO; 317 | CLANG_ANALYZER_NONNULL = YES; 318 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 319 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 320 | CLANG_CXX_LIBRARY = "libc++"; 321 | CLANG_ENABLE_MODULES = YES; 322 | CLANG_ENABLE_OBJC_ARC = YES; 323 | CLANG_ENABLE_OBJC_WEAK = YES; 324 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 325 | CLANG_WARN_BOOL_CONVERSION = YES; 326 | CLANG_WARN_COMMA = YES; 327 | CLANG_WARN_CONSTANT_CONVERSION = YES; 328 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 329 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 330 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 331 | CLANG_WARN_EMPTY_BODY = YES; 332 | CLANG_WARN_ENUM_CONVERSION = YES; 333 | CLANG_WARN_INFINITE_RECURSION = YES; 334 | CLANG_WARN_INT_CONVERSION = YES; 335 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 336 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 337 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 338 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 339 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 340 | CLANG_WARN_STRICT_PROTOTYPES = YES; 341 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 342 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 343 | CLANG_WARN_UNREACHABLE_CODE = YES; 344 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 345 | COPY_PHASE_STRIP = NO; 346 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 347 | ENABLE_NS_ASSERTIONS = NO; 348 | ENABLE_STRICT_OBJC_MSGSEND = YES; 349 | GCC_C_LANGUAGE_STANDARD = gnu11; 350 | GCC_NO_COMMON_BLOCKS = YES; 351 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 352 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 353 | GCC_WARN_UNDECLARED_SELECTOR = YES; 354 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 355 | GCC_WARN_UNUSED_FUNCTION = YES; 356 | GCC_WARN_UNUSED_VARIABLE = YES; 357 | IPHONEOS_DEPLOYMENT_TARGET = 13.5; 358 | MTL_ENABLE_DEBUG_INFO = NO; 359 | MTL_FAST_MATH = YES; 360 | SDKROOT = iphoneos; 361 | SWIFT_COMPILATION_MODE = wholemodule; 362 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 363 | VALIDATE_PRODUCT = YES; 364 | }; 365 | name = Release; 366 | }; 367 | 1833003D2495042200C2038D /* Debug */ = { 368 | isa = XCBuildConfiguration; 369 | buildSettings = { 370 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 371 | CODE_SIGN_STYLE = Automatic; 372 | DEVELOPMENT_ASSET_PATHS = "\"PokeList/Preview Content\""; 373 | DEVELOPMENT_TEAM = 8N8GEA4Z3T; 374 | ENABLE_PREVIEWS = YES; 375 | INFOPLIST_FILE = PokeList/Info.plist; 376 | LD_RUNPATH_SEARCH_PATHS = ( 377 | "$(inherited)", 378 | "@executable_path/Frameworks", 379 | ); 380 | PRODUCT_BUNDLE_IDENTIFIER = com.ajb.pokelist.PokeList; 381 | PRODUCT_NAME = "$(TARGET_NAME)"; 382 | SWIFT_VERSION = 5.0; 383 | TARGETED_DEVICE_FAMILY = "1,2"; 384 | }; 385 | name = Debug; 386 | }; 387 | 1833003E2495042200C2038D /* Release */ = { 388 | isa = XCBuildConfiguration; 389 | buildSettings = { 390 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 391 | CODE_SIGN_STYLE = Automatic; 392 | DEVELOPMENT_ASSET_PATHS = "\"PokeList/Preview Content\""; 393 | DEVELOPMENT_TEAM = 8N8GEA4Z3T; 394 | ENABLE_PREVIEWS = YES; 395 | INFOPLIST_FILE = PokeList/Info.plist; 396 | LD_RUNPATH_SEARCH_PATHS = ( 397 | "$(inherited)", 398 | "@executable_path/Frameworks", 399 | ); 400 | PRODUCT_BUNDLE_IDENTIFIER = com.ajb.pokelist.PokeList; 401 | PRODUCT_NAME = "$(TARGET_NAME)"; 402 | SWIFT_VERSION = 5.0; 403 | TARGETED_DEVICE_FAMILY = "1,2"; 404 | }; 405 | name = Release; 406 | }; 407 | /* End XCBuildConfiguration section */ 408 | 409 | /* Begin XCConfigurationList section */ 410 | 183300202495042100C2038D /* Build configuration list for PBXProject "PokeList" */ = { 411 | isa = XCConfigurationList; 412 | buildConfigurations = ( 413 | 1833003A2495042200C2038D /* Debug */, 414 | 1833003B2495042200C2038D /* Release */, 415 | ); 416 | defaultConfigurationIsVisible = 0; 417 | defaultConfigurationName = Release; 418 | }; 419 | 1833003C2495042200C2038D /* Build configuration list for PBXNativeTarget "PokeList" */ = { 420 | isa = XCConfigurationList; 421 | buildConfigurations = ( 422 | 1833003D2495042200C2038D /* Debug */, 423 | 1833003E2495042200C2038D /* Release */, 424 | ); 425 | defaultConfigurationIsVisible = 0; 426 | defaultConfigurationName = Release; 427 | }; 428 | /* End XCConfigurationList section */ 429 | 430 | /* Begin XCVersionGroup section */ 431 | 1833002C2495042100C2038D /* PokeList.xcdatamodeld */ = { 432 | isa = XCVersionGroup; 433 | children = ( 434 | 1833002D2495042100C2038D /* PokeList.xcdatamodel */, 435 | ); 436 | currentVersion = 1833002D2495042100C2038D /* PokeList.xcdatamodel */; 437 | path = PokeList.xcdatamodeld; 438 | sourceTree = ""; 439 | versionGroupType = wrapper.xcdatamodel; 440 | }; 441 | /* End XCVersionGroup section */ 442 | }; 443 | rootObject = 1833001D2495042100C2038D /* Project object */; 444 | } 445 | -------------------------------------------------------------------------------- /PokeList.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /PokeList.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /PokeList.xcodeproj/project.xcworkspace/xcuserdata/abrown.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Swift-Compiled/pokelist/5746eb89655be87292faf728dbee9f92ddbc3162/PokeList.xcodeproj/project.xcworkspace/xcuserdata/abrown.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /PokeList.xcodeproj/xcuserdata/abrown.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | -------------------------------------------------------------------------------- /PokeList.xcodeproj/xcuserdata/abrown.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | PokeList.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /PokeList/API/APIError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APIError.swift 3 | // PokeList 4 | // 5 | // Created by Alex Brown on 14/06/2020. 6 | // Copyright © 2020 ABrown. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | enum APIError: Error { 12 | case decodingError 13 | case httpError(Int) 14 | case unknown 15 | } 16 | 17 | -------------------------------------------------------------------------------- /PokeList/API/APIService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APIService.swift 3 | // PokeList 4 | // 5 | // Created by Alex Brown on 14/06/2020. 6 | // Copyright © 2020 ABrown. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Combine 11 | import UIKit 12 | 13 | protocol APIService { 14 | func request(with builder: RequestBuilder) -> AnyPublisher 15 | func requestImage(with url: String) -> AnyPublisher 16 | } 17 | -------------------------------------------------------------------------------- /PokeList/API/APISession.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APISession.swift 3 | // PokeList 4 | // 5 | // Created by Alex Brown on 14/06/2020. 6 | // Copyright © 2020 ABrown. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Combine 11 | import UIKit 12 | 13 | struct APISession: APIService { 14 | func request(with builder: RequestBuilder) -> AnyPublisher where T: Decodable { 15 | 16 | let decoder = JSONDecoder() 17 | decoder.keyDecodingStrategy = .convertFromSnakeCase 18 | 19 | return URLSession.shared 20 | .dataTaskPublisher(for: builder.urlRequest) 21 | .receive(on: DispatchQueue.main) 22 | .mapError { _ in .unknown } 23 | .flatMap { data, response -> AnyPublisher in 24 | if let response = response as? HTTPURLResponse { 25 | if (200...299).contains(response.statusCode) { 26 | 27 | return Just(data) 28 | .decode(type: T.self, decoder: decoder) 29 | .mapError {_ in .decodingError} 30 | .eraseToAnyPublisher() 31 | } else { 32 | return Fail(error: APIError.httpError(response.statusCode)) 33 | .eraseToAnyPublisher() 34 | } 35 | } 36 | return Fail(error: APIError.unknown) 37 | .eraseToAnyPublisher() 38 | } 39 | .eraseToAnyPublisher() 40 | } 41 | 42 | func requestImage(with url: String) -> AnyPublisher { 43 | guard let url = URL(string: url) 44 | else { 45 | return Fail(error: .decodingError) 46 | .eraseToAnyPublisher() 47 | } 48 | 49 | return URLSession.shared.dataTaskPublisher(for: url) 50 | .receive(on: DispatchQueue.main) 51 | .mapError { _ in .unknown } 52 | .flatMap { data, response -> AnyPublisher in 53 | if let image = UIImage(data: data) { 54 | return Just(image) 55 | .mapError {_ in .decodingError } 56 | .eraseToAnyPublisher() 57 | } 58 | return Fail(error: .unknown) 59 | .eraseToAnyPublisher() 60 | } 61 | .eraseToAnyPublisher() 62 | 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /PokeList/API/RequestBuilder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RequestBuilder.swift 3 | // PokeList 4 | // 5 | // Created by Alex Brown on 14/06/2020. 6 | // Copyright © 2020 ABrown. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | protocol RequestBuilder { 12 | var urlRequest: URLRequest {get} 13 | } 14 | -------------------------------------------------------------------------------- /PokeList/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // PokeList 4 | // 5 | // Created by Alex Brown on 13/06/2020. 6 | // Copyright © 2020 ABrown. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import CoreData 11 | 12 | @UIApplicationMain 13 | class AppDelegate: UIResponder, UIApplicationDelegate { 14 | 15 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 16 | // Override point for customization after application launch. 17 | return true 18 | } 19 | 20 | // MARK: UISceneSession Lifecycle 21 | 22 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { 23 | // Called when a new scene session is being created. 24 | // Use this method to select a configuration to create the new scene with. 25 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 26 | } 27 | 28 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { 29 | // Called when the user discards a scene session. 30 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. 31 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return. 32 | } 33 | 34 | // MARK: - Core Data stack 35 | 36 | lazy var persistentContainer: NSPersistentContainer = { 37 | /* 38 | The persistent container for the application. This implementation 39 | creates and returns a container, having loaded the store for the 40 | application to it. This property is optional since there are legitimate 41 | error conditions that could cause the creation of the store to fail. 42 | */ 43 | let container = NSPersistentContainer(name: "PokeList") 44 | container.loadPersistentStores(completionHandler: { (storeDescription, error) in 45 | if let error = error as NSError? { 46 | // Replace this implementation with code to handle the error appropriately. 47 | // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. 48 | 49 | /* 50 | Typical reasons for an error here include: 51 | * The parent directory does not exist, cannot be created, or disallows writing. 52 | * The persistent store is not accessible, due to permissions or data protection when the device is locked. 53 | * The device is out of space. 54 | * The store could not be migrated to the current model version. 55 | Check the error message to determine what the actual problem was. 56 | */ 57 | fatalError("Unresolved error \(error), \(error.userInfo)") 58 | } 59 | }) 60 | return container 61 | }() 62 | 63 | // MARK: - Core Data Saving support 64 | 65 | func saveContext () { 66 | let context = persistentContainer.viewContext 67 | if context.hasChanges { 68 | do { 69 | try context.save() 70 | } catch { 71 | // Replace this implementation with code to handle the error appropriately. 72 | // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. 73 | let nserror = error as NSError 74 | fatalError("Unresolved error \(nserror), \(nserror.userInfo)") 75 | } 76 | } 77 | } 78 | 79 | } 80 | 81 | -------------------------------------------------------------------------------- /PokeList/Assets.xcassets/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Swift-Compiled/pokelist/5746eb89655be87292faf728dbee9f92ddbc3162/PokeList/Assets.xcassets/.DS_Store -------------------------------------------------------------------------------- /PokeList/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "1x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "76x76" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "scale" : "2x", 86 | "size" : "83.5x83.5" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "scale" : "1x", 91 | "size" : "1024x1024" 92 | } 93 | ], 94 | "info" : { 95 | "author" : "xcode", 96 | "version" : 1 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /PokeList/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /PokeList/Assets.xcassets/house.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "home2-white.pdf", 5 | "idiom" : "universal" 6 | }, 7 | { 8 | "appearances" : [ 9 | { 10 | "appearance" : "luminosity", 11 | "value" : "light" 12 | } 13 | ], 14 | "filename" : "home2-white-1.pdf", 15 | "idiom" : "universal" 16 | }, 17 | { 18 | "appearances" : [ 19 | { 20 | "appearance" : "luminosity", 21 | "value" : "dark" 22 | } 23 | ], 24 | "filename" : "home2-white-2.pdf", 25 | "idiom" : "universal" 26 | } 27 | ], 28 | "info" : { 29 | "author" : "xcode", 30 | "version" : 1 31 | }, 32 | "properties" : { 33 | "preserves-vector-representation" : true 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /PokeList/Assets.xcassets/house.imageset/home2-white-1.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Swift-Compiled/pokelist/5746eb89655be87292faf728dbee9f92ddbc3162/PokeList/Assets.xcassets/house.imageset/home2-white-1.pdf -------------------------------------------------------------------------------- /PokeList/Assets.xcassets/house.imageset/home2-white-2.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Swift-Compiled/pokelist/5746eb89655be87292faf728dbee9f92ddbc3162/PokeList/Assets.xcassets/house.imageset/home2-white-2.pdf -------------------------------------------------------------------------------- /PokeList/Assets.xcassets/house.imageset/home2-white.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Swift-Compiled/pokelist/5746eb89655be87292faf728dbee9f92ddbc3162/PokeList/Assets.xcassets/house.imageset/home2-white.pdf -------------------------------------------------------------------------------- /PokeList/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 | -------------------------------------------------------------------------------- /PokeList/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 | -------------------------------------------------------------------------------- /PokeList/PokeList.xcdatamodeld/.xccurrentversion: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | _XCCurrentVersionName 6 | PokeList.xcdatamodel 7 | 8 | 9 | -------------------------------------------------------------------------------- /PokeList/PokeList.xcdatamodeld/PokeList.xcdatamodel/contents: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /PokeList/PokemonAPI/Pokemon.swift: -------------------------------------------------------------------------------- 1 | // This file was generated from JSON Schema using quicktype, do not modify it directly. 2 | // To parse the JSON, add this file to your project and do: 3 | // 4 | // let pokemon = try? newJSONDecoder().decode(Pokemon.self, from: jsonData) 5 | 6 | import Foundation 7 | 8 | // MARK: - Pokemon 9 | struct Pokemon: Codable { 10 | let abilities: [Ability] 11 | let baseExperience: Int? 12 | let forms: [Species] 13 | let gameIndices: [GameIndex]? 14 | let height: Int 15 | let heldItems: [JSONAny]? 16 | let id: Int 17 | let isDefault: Bool? 18 | let locationAreaEncounters: String? 19 | let moves: [Move] 20 | let name: String 21 | let order: Int 22 | let species: Species 23 | let sprites: Sprites 24 | let stats: [Stat] 25 | let types: [TypeElement] 26 | let weight: Int 27 | } 28 | 29 | // MARK: - Ability 30 | struct Ability: Codable { 31 | let ability: Species 32 | let isHidden: Bool? 33 | let slot: Int 34 | } 35 | 36 | // MARK: - Species 37 | struct Species: Codable { 38 | let name: String 39 | let url: String 40 | } 41 | 42 | // MARK: - GameIndex 43 | struct GameIndex: Codable { 44 | let gameIndex: Int 45 | let version: Species 46 | } 47 | 48 | // MARK: - Move 49 | struct Move: Codable { 50 | let move: Species 51 | let versionGroupDetails: [VersionGroupDetail]? 52 | } 53 | 54 | // MARK: - VersionGroupDetail 55 | struct VersionGroupDetail: Codable { 56 | let levelLearnedAt: Int 57 | let moveLearnMethod, versionGroup: Species 58 | } 59 | 60 | // MARK: - Sprites 61 | struct Sprites: Codable { 62 | let backDefault: String 63 | let backFemale: String? 64 | let backShiny: String? 65 | let backShinyFemale: String? 66 | let frontDefault: String 67 | let frontFemale: String? 68 | let frontShiny: String? 69 | let frontShinyFemale: String? 70 | } 71 | 72 | // MARK: - Stat 73 | struct Stat: Codable, Identifiable { 74 | let id = UUID() 75 | let baseStat, effort: Int? 76 | let stat: Species 77 | 78 | } 79 | 80 | // MARK: - TypeElement 81 | struct TypeElement: Codable { 82 | let slot: Int 83 | let type: Species 84 | } 85 | 86 | // MARK: - Encode/decode helpers 87 | 88 | class JSONNull: Codable, Hashable { 89 | 90 | public static func == (lhs: JSONNull, rhs: JSONNull) -> Bool { 91 | return true 92 | } 93 | 94 | public var hashValue: Int { 95 | return 0 96 | } 97 | 98 | public init() {} 99 | 100 | public required init(from decoder: Decoder) throws { 101 | let container = try decoder.singleValueContainer() 102 | if !container.decodeNil() { 103 | throw DecodingError.typeMismatch(JSONNull.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Wrong type for JSONNull")) 104 | } 105 | } 106 | 107 | public func encode(to encoder: Encoder) throws { 108 | var container = encoder.singleValueContainer() 109 | try container.encodeNil() 110 | } 111 | } 112 | 113 | class JSONCodingKey: CodingKey { 114 | let key: String 115 | 116 | required init?(intValue: Int) { 117 | return nil 118 | } 119 | 120 | required init?(stringValue: String) { 121 | key = stringValue 122 | } 123 | 124 | var intValue: Int? { 125 | return nil 126 | } 127 | 128 | var stringValue: String { 129 | return key 130 | } 131 | } 132 | 133 | class JSONAny: Codable { 134 | 135 | let value: Any 136 | 137 | static func decodingError(forCodingPath codingPath: [CodingKey]) -> DecodingError { 138 | let context = DecodingError.Context(codingPath: codingPath, debugDescription: "Cannot decode JSONAny") 139 | return DecodingError.typeMismatch(JSONAny.self, context) 140 | } 141 | 142 | static func encodingError(forValue value: Any, codingPath: [CodingKey]) -> EncodingError { 143 | let context = EncodingError.Context(codingPath: codingPath, debugDescription: "Cannot encode JSONAny") 144 | return EncodingError.invalidValue(value, context) 145 | } 146 | 147 | static func decode(from container: SingleValueDecodingContainer) throws -> Any { 148 | if let value = try? container.decode(Bool.self) { 149 | return value 150 | } 151 | if let value = try? container.decode(Int64.self) { 152 | return value 153 | } 154 | if let value = try? container.decode(Double.self) { 155 | return value 156 | } 157 | if let value = try? container.decode(String.self) { 158 | return value 159 | } 160 | if container.decodeNil() { 161 | return JSONNull() 162 | } 163 | throw decodingError(forCodingPath: container.codingPath) 164 | } 165 | 166 | static func decode(from container: inout UnkeyedDecodingContainer) throws -> Any { 167 | if let value = try? container.decode(Bool.self) { 168 | return value 169 | } 170 | if let value = try? container.decode(Int64.self) { 171 | return value 172 | } 173 | if let value = try? container.decode(Double.self) { 174 | return value 175 | } 176 | if let value = try? container.decode(String.self) { 177 | return value 178 | } 179 | if let value = try? container.decodeNil() { 180 | if value { 181 | return JSONNull() 182 | } 183 | } 184 | if var container = try? container.nestedUnkeyedContainer() { 185 | return try decodeArray(from: &container) 186 | } 187 | if var container = try? container.nestedContainer(keyedBy: JSONCodingKey.self) { 188 | return try decodeDictionary(from: &container) 189 | } 190 | throw decodingError(forCodingPath: container.codingPath) 191 | } 192 | 193 | static func decode(from container: inout KeyedDecodingContainer, forKey key: JSONCodingKey) throws -> Any { 194 | if let value = try? container.decode(Bool.self, forKey: key) { 195 | return value 196 | } 197 | if let value = try? container.decode(Int64.self, forKey: key) { 198 | return value 199 | } 200 | if let value = try? container.decode(Double.self, forKey: key) { 201 | return value 202 | } 203 | if let value = try? container.decode(String.self, forKey: key) { 204 | return value 205 | } 206 | if let value = try? container.decodeNil(forKey: key) { 207 | if value { 208 | return JSONNull() 209 | } 210 | } 211 | if var container = try? container.nestedUnkeyedContainer(forKey: key) { 212 | return try decodeArray(from: &container) 213 | } 214 | if var container = try? container.nestedContainer(keyedBy: JSONCodingKey.self, forKey: key) { 215 | return try decodeDictionary(from: &container) 216 | } 217 | throw decodingError(forCodingPath: container.codingPath) 218 | } 219 | 220 | static func decodeArray(from container: inout UnkeyedDecodingContainer) throws -> [Any] { 221 | var arr: [Any] = [] 222 | while !container.isAtEnd { 223 | let value = try decode(from: &container) 224 | arr.append(value) 225 | } 226 | return arr 227 | } 228 | 229 | static func decodeDictionary(from container: inout KeyedDecodingContainer) throws -> [String: Any] { 230 | var dict = [String: Any]() 231 | for key in container.allKeys { 232 | let value = try decode(from: &container, forKey: key) 233 | dict[key.stringValue] = value 234 | } 235 | return dict 236 | } 237 | 238 | static func encode(to container: inout UnkeyedEncodingContainer, array: [Any]) throws { 239 | for value in array { 240 | if let value = value as? Bool { 241 | try container.encode(value) 242 | } else if let value = value as? Int64 { 243 | try container.encode(value) 244 | } else if let value = value as? Double { 245 | try container.encode(value) 246 | } else if let value = value as? String { 247 | try container.encode(value) 248 | } else if value is JSONNull { 249 | try container.encodeNil() 250 | } else if let value = value as? [Any] { 251 | var container = container.nestedUnkeyedContainer() 252 | try encode(to: &container, array: value) 253 | } else if let value = value as? [String: Any] { 254 | var container = container.nestedContainer(keyedBy: JSONCodingKey.self) 255 | try encode(to: &container, dictionary: value) 256 | } else { 257 | throw encodingError(forValue: value, codingPath: container.codingPath) 258 | } 259 | } 260 | } 261 | 262 | static func encode(to container: inout KeyedEncodingContainer, dictionary: [String: Any]) throws { 263 | for (key, value) in dictionary { 264 | let key = JSONCodingKey(stringValue: key)! 265 | if let value = value as? Bool { 266 | try container.encode(value, forKey: key) 267 | } else if let value = value as? Int64 { 268 | try container.encode(value, forKey: key) 269 | } else if let value = value as? Double { 270 | try container.encode(value, forKey: key) 271 | } else if let value = value as? String { 272 | try container.encode(value, forKey: key) 273 | } else if value is JSONNull { 274 | try container.encodeNil(forKey: key) 275 | } else if let value = value as? [Any] { 276 | var container = container.nestedUnkeyedContainer(forKey: key) 277 | try encode(to: &container, array: value) 278 | } else if let value = value as? [String: Any] { 279 | var container = container.nestedContainer(keyedBy: JSONCodingKey.self, forKey: key) 280 | try encode(to: &container, dictionary: value) 281 | } else { 282 | throw encodingError(forValue: value, codingPath: container.codingPath) 283 | } 284 | } 285 | } 286 | 287 | static func encode(to container: inout SingleValueEncodingContainer, value: Any) throws { 288 | if let value = value as? Bool { 289 | try container.encode(value) 290 | } else if let value = value as? Int64 { 291 | try container.encode(value) 292 | } else if let value = value as? Double { 293 | try container.encode(value) 294 | } else if let value = value as? String { 295 | try container.encode(value) 296 | } else if value is JSONNull { 297 | try container.encodeNil() 298 | } else { 299 | throw encodingError(forValue: value, codingPath: container.codingPath) 300 | } 301 | } 302 | 303 | public required init(from decoder: Decoder) throws { 304 | if var arrayContainer = try? decoder.unkeyedContainer() { 305 | self.value = try JSONAny.decodeArray(from: &arrayContainer) 306 | } else if var container = try? decoder.container(keyedBy: JSONCodingKey.self) { 307 | self.value = try JSONAny.decodeDictionary(from: &container) 308 | } else { 309 | let container = try decoder.singleValueContainer() 310 | self.value = try JSONAny.decode(from: container) 311 | } 312 | } 313 | 314 | public func encode(to encoder: Encoder) throws { 315 | if let arr = self.value as? [Any] { 316 | var container = encoder.unkeyedContainer() 317 | try JSONAny.encode(to: &container, array: arr) 318 | } else if let dict = self.value as? [String: Any] { 319 | var container = encoder.container(keyedBy: JSONCodingKey.self) 320 | try JSONAny.encode(to: &container, dictionary: dict) 321 | } else { 322 | var container = encoder.singleValueContainer() 323 | try JSONAny.encode(to: &container, value: self.value) 324 | } 325 | } 326 | } 327 | -------------------------------------------------------------------------------- /PokeList/PokemonAPI/PokemonAPIResponse.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PokemonAPIResponse.swift 3 | // PokeList 4 | // 5 | // Created by Alex Brown on 14/06/2020. 6 | // Copyright © 2020 ABrown. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct PokemonListAPIResponse: Codable { 12 | let count: Int 13 | let next: String 14 | let previous: String? 15 | let results: [PokemonListItem] 16 | } 17 | -------------------------------------------------------------------------------- /PokeList/PokemonAPI/PokemonEndpoint.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PokemonEndpoint.swift 3 | // PokeList 4 | // 5 | // Created by Alex Brown on 14/06/2020. 6 | // Copyright © 2020 ABrown. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | enum PokemonEndpoint { 12 | case pokemonList 13 | case pokemonDetail(String) 14 | } 15 | 16 | extension PokemonEndpoint: RequestBuilder { 17 | 18 | var urlRequest: URLRequest { 19 | switch self { 20 | case .pokemonList: 21 | guard let url = URL(string: "https://pokeapi.co/api/v2/pokemon") 22 | else {preconditionFailure("Invalid URL format")} 23 | let request = URLRequest(url: url) 24 | return request 25 | case .pokemonDetail(let pokemonURL): 26 | 27 | guard let url = URL(string: pokemonURL) 28 | else {preconditionFailure("Invalid URL format")} 29 | 30 | let request = URLRequest(url: url) 31 | return request 32 | } 33 | 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /PokeList/PokemonAPI/PokemonListItem.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Pokemon.swift 3 | // PokeList 4 | // 5 | // Created by Alex Brown on 14/06/2020. 6 | // Copyright © 2020 ABrown. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct PokemonListItem: Codable, Identifiable { 12 | let id = UUID() 13 | let name: String 14 | let url: String 15 | } 16 | -------------------------------------------------------------------------------- /PokeList/PokemonAPI/PokemonService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PokemonService.swift 3 | // PokeList 4 | // 5 | // Created by Alex Brown on 14/06/2020. 6 | // Copyright © 2020 ABrown. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Combine 11 | import UIKit 12 | 13 | protocol PokemonService { 14 | var apiSession: APIService {get} 15 | 16 | func getPokemonList() -> AnyPublisher 17 | func getPokemon(pokemonURL: String) -> AnyPublisher 18 | } 19 | 20 | extension PokemonService { 21 | 22 | func getPokemonList() -> AnyPublisher { 23 | return apiSession.request(with: PokemonEndpoint.pokemonList) 24 | .eraseToAnyPublisher() 25 | } 26 | 27 | func getPokemon(pokemonURL: String) -> AnyPublisher { 28 | return apiSession.request(with: PokemonEndpoint.pokemonDetail(pokemonURL)) 29 | .eraseToAnyPublisher() 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /PokeList/PokemonDetail/PokemonDetailView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PokemonDetailView.swift 3 | // PokeList 4 | // 5 | // Created by Alex Brown on 15/06/2020. 6 | // Copyright © 2020 ABrown. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct PokemonDetailView: View { 12 | @ObservedObject var viewModel: PokemonDetailViewModel 13 | 14 | init(pokemonListItem: PokemonListItem) { 15 | viewModel = PokemonDetailViewModel(pokemonListItem: pokemonListItem) 16 | } 17 | 18 | var body: some View { 19 | ScrollView { 20 | VStack(alignment: .leading) { 21 | HStack { 22 | ForEach(viewModel.pokemonImages, id: \.self) { image in 23 | Image(uiImage: image) 24 | } 25 | } 26 | Divider() 27 | 28 | VStack(alignment: .leading) { 29 | Text("Stats") 30 | .font(.title) 31 | 32 | VStack(alignment: .leading, spacing: 8) { 33 | ForEach(viewModel.pokemonStats) { stat in 34 | Text("\(stat.stat.name.capitalized): \(stat.baseStat!)") 35 | } 36 | } 37 | .padding(.top, 10) 38 | .padding(.leading, 10) 39 | } 40 | .padding() 41 | 42 | Spacer() 43 | } 44 | } 45 | .onAppear { 46 | self.viewModel.getPokemon() 47 | } 48 | .navigationBarTitle(self.viewModel.pokemonName.capitalized) 49 | } 50 | } 51 | 52 | struct PokemonDetailView_Previews: PreviewProvider { 53 | static var previews: some View { 54 | PokemonDetailView(pokemonListItem: PokemonListItem(name: "Bulbasaur", url: "")) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /PokeList/PokemonDetail/PokemonDetailViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PokemonDetailViewModel.swift 3 | // PokeList 4 | // 5 | // Created by Alex Brown on 15/06/2020. 6 | // Copyright © 2020 ABrown. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SwiftUI 11 | import Combine 12 | import UIKit 13 | 14 | class PokemonDetailViewModel: ObservableObject, PokemonService { 15 | 16 | var cancellables = Set() 17 | 18 | @Published var pokemon: Pokemon? 19 | @Published var pokemonImages: [UIImage] = [UIImage]() 20 | 21 | let pokemonListItem: PokemonListItem 22 | var apiSession: APIService 23 | 24 | var pokemonName: String { 25 | return pokemonListItem.name 26 | } 27 | 28 | var pokemonStats: [Stat] { 29 | guard let pokemon = pokemon 30 | else {return [Stat]()} 31 | return pokemon.stats 32 | } 33 | 34 | init(pokemonListItem: PokemonListItem, apiService: APIService = APISession()) { 35 | self.pokemonListItem = pokemonListItem 36 | self.apiSession = apiService 37 | } 38 | 39 | func getPokemon() { 40 | let cancellable = self.getPokemon(pokemonURL: pokemonListItem.url) 41 | .sink(receiveCompletion: { (result) in 42 | print(result) 43 | }) { (pokemon) in 44 | self.pokemon = pokemon 45 | self.getPokemonSprites() 46 | } 47 | cancellables.insert(cancellable) 48 | } 49 | 50 | func getPokemonSprites() { 51 | guard let pokemon = pokemon 52 | else {return} 53 | 54 | getPokemonSprite(urlString: pokemon.sprites.frontDefault) 55 | if let frontShiny = pokemon.sprites.frontShiny { 56 | getPokemonSprite(urlString: frontShiny) 57 | } 58 | 59 | getPokemonSprite(urlString: pokemon.sprites.backDefault) 60 | if let backShiny = pokemon.sprites.backShiny { 61 | getPokemonSprite(urlString: backShiny) 62 | } 63 | } 64 | 65 | func getPokemonSprite(urlString: String) { 66 | let cancellable = apiSession.requestImage(with: urlString) 67 | .sink(receiveCompletion: { (result) in 68 | print(result) 69 | }) { (image) in 70 | self.pokemonImages.append(image) 71 | } 72 | 73 | cancellables.insert(cancellable) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /PokeList/PokemonList/PokemonListView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // PokeList 4 | // 5 | // Created by Alex Brown on 13/06/2020. 6 | // Copyright © 2020 ABrown. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct PokemonListView: View { 12 | @ObservedObject var viewModel = PokemonListViewModel() 13 | 14 | var body: some View { 15 | NavigationView { 16 | List(self.viewModel.pokemon) { pokemon in 17 | Text(pokemon.name.capitalized) 18 | } 19 | .navigationBarTitle("Pokemon") 20 | } 21 | .onAppear { 22 | self.viewModel.getPokemonList() 23 | } 24 | } 25 | } 26 | 27 | struct PokemonListView_Previews: PreviewProvider { 28 | static var previews: some View { 29 | PokemonListView() 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /PokeList/PokemonList/PokemonListViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PokelistViewModel.swift 3 | // PokeList 4 | // 5 | // Created by Alex Brown on 14/06/2020. 6 | // Copyright © 2020 ABrown. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Combine 11 | import SwiftUI 12 | 13 | class PokemonListViewModel: ObservableObject, PokemonService { 14 | var apiSession: APIService 15 | @Published var pokemon = [PokemonListItem]() 16 | 17 | var cancellables = Set() 18 | 19 | init(apiSession: APIService = APISession()) { 20 | self.apiSession = apiSession 21 | } 22 | 23 | func getPokemonList() { 24 | let cancellable = self.getPokemonList() 25 | .sink(receiveCompletion: { result in 26 | switch result { 27 | case .failure(let error): 28 | print("Handle error: \(error)") 29 | case .finished: 30 | break 31 | } 32 | 33 | }) { (pokemon) in 34 | self.pokemon = pokemon.results 35 | } 36 | cancellables.insert(cancellable) 37 | } 38 | } 39 | 40 | -------------------------------------------------------------------------------- /PokeList/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /PokeList/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // PokeList 4 | // 5 | // Created by Alex Brown on 13/06/2020. 6 | // Copyright © 2020 ABrown. 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 | // Get the managed object context from the shared persistent container. 23 | let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext 24 | 25 | // Create the SwiftUI view and set the context as the value for the managedObjectContext environment keyPath. 26 | // Add `@Environment(\.managedObjectContext)` in the views that will need the context. 27 | let contentView = PokemonListView().environment(\.managedObjectContext, context) 28 | 29 | // Use a UIHostingController as window root view controller. 30 | if let windowScene = scene as? UIWindowScene { 31 | let window = UIWindow(windowScene: windowScene) 32 | window.rootViewController = UIHostingController(rootView: contentView) 33 | self.window = window 34 | window.makeKeyAndVisible() 35 | } 36 | } 37 | 38 | func sceneDidDisconnect(_ scene: UIScene) { 39 | // Called as the scene is being released by the system. 40 | // This occurs shortly after the scene enters the background, or when its session is discarded. 41 | // Release any resources associated with this scene that can be re-created the next time the scene connects. 42 | // The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead). 43 | } 44 | 45 | func sceneDidBecomeActive(_ scene: UIScene) { 46 | // Called when the scene has moved from an inactive state to an active state. 47 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. 48 | } 49 | 50 | func sceneWillResignActive(_ scene: UIScene) { 51 | // Called when the scene will move from an active state to an inactive state. 52 | // This may occur due to temporary interruptions (ex. an incoming phone call). 53 | } 54 | 55 | func sceneWillEnterForeground(_ scene: UIScene) { 56 | // Called as the scene transitions from the background to the foreground. 57 | // Use this method to undo the changes made on entering the background. 58 | } 59 | 60 | func sceneDidEnterBackground(_ scene: UIScene) { 61 | // Called as the scene transitions from the foreground to the background. 62 | // Use this method to save data, release shared resources, and store enough scene-specific state information 63 | // to restore the scene back to its current state. 64 | 65 | // Save changes in the application's managed object context when the application transitions to the background. 66 | (UIApplication.shared.delegate as? AppDelegate)?.saveContext() 67 | } 68 | 69 | 70 | } 71 | 72 | --------------------------------------------------------------------------------