├── .gitignore ├── Assets └── SwiftUIAndCombineAllTheThings.png ├── GitHubGists.xcodeproj ├── project.pbxproj └── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── GitHubGists ├── App │ ├── AppDelegate.swift │ └── SceneDelegate.swift ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── Icon-App-20x20@1x.png │ │ ├── Icon-App-20x20@2x.png │ │ ├── Icon-App-20x20@3x.png │ │ ├── Icon-App-29x29@1x.png │ │ ├── Icon-App-29x29@2x.png │ │ ├── Icon-App-29x29@3x.png │ │ ├── Icon-App-40x40@1x.png │ │ ├── Icon-App-40x40@2x.png │ │ ├── Icon-App-40x40@3x.png │ │ ├── Icon-App-60x60@2x.png │ │ ├── Icon-App-60x60@3x.png │ │ ├── Icon-App-76x76@1x.png │ │ ├── Icon-App-76x76@2x.png │ │ ├── Icon-App-83.5x83.5@2x.png │ │ └── ItunesArtwork@2x.png │ └── Contents.json ├── Base.lproj │ └── LaunchScreen.storyboard ├── Info.plist ├── Models │ ├── Gist.swift │ └── Owner.swift ├── Services │ └── RemoteImageService.swift ├── View Models │ ├── GistCellViewModel.swift │ └── GistsViewModel.swift └── Views │ ├── ContentView.swift │ ├── GistView.swift │ ├── SafariView.swift │ └── SearchBar.swift ├── LICENSE └── README.md /.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 | -------------------------------------------------------------------------------- /Assets/SwiftUIAndCombineAllTheThings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scotteg/GitHubGists/37178c38543c1b241637e978fc717a85e2b51587/Assets/SwiftUIAndCombineAllTheThings.png -------------------------------------------------------------------------------- /GitHubGists.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | D24C222023EECBCB00759726 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D24C221F23EECBCB00759726 /* AppDelegate.swift */; }; 11 | D24C222223EECBCB00759726 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D24C222123EECBCB00759726 /* SceneDelegate.swift */; }; 12 | D24C222923EECBCF00759726 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D24C222823EECBCF00759726 /* Assets.xcassets */; }; 13 | D24C222C23EECBCF00759726 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D24C222A23EECBCF00759726 /* LaunchScreen.storyboard */; }; 14 | D2E3F48023EEDCD600939675 /* Gist.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2E3F47F23EEDCD600939675 /* Gist.swift */; }; 15 | D2E3F48223EEDD0A00939675 /* Owner.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2E3F48123EEDD0A00939675 /* Owner.swift */; }; 16 | D2E3F48423EEE01C00939675 /* GistsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2E3F48323EEE01C00939675 /* GistsViewModel.swift */; }; 17 | D2E3F48D23EF313000939675 /* RemoteImageService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2E3F48C23EF313000939675 /* RemoteImageService.swift */; }; 18 | D2E3F48F23EF3C2700939675 /* GistCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2E3F48E23EF3C2700939675 /* GistCellViewModel.swift */; }; 19 | D2E3F4B323F074C800939675 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2E3F4B223F074C800939675 /* ContentView.swift */; }; 20 | D2E3F4B523F0801100939675 /* SafariView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2E3F4B423F0801100939675 /* SafariView.swift */; }; 21 | D2E3F4B723F0803C00939675 /* SearchBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2E3F4B623F0803C00939675 /* SearchBar.swift */; }; 22 | D2E3F4B923F0F45100939675 /* GistView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2E3F4B823F0F45100939675 /* GistView.swift */; }; 23 | /* End PBXBuildFile section */ 24 | 25 | /* Begin PBXFileReference section */ 26 | D24C221C23EECBCB00759726 /* GitHubGists.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = GitHubGists.app; sourceTree = BUILT_PRODUCTS_DIR; }; 27 | D24C221F23EECBCB00759726 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 28 | D24C222123EECBCB00759726 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 29 | D24C222823EECBCF00759726 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 30 | D24C222B23EECBCF00759726 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 31 | D24C222D23EECBCF00759726 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 32 | D2E3F47F23EEDCD600939675 /* Gist.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Gist.swift; sourceTree = ""; }; 33 | D2E3F48123EEDD0A00939675 /* Owner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Owner.swift; sourceTree = ""; }; 34 | D2E3F48323EEE01C00939675 /* GistsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GistsViewModel.swift; sourceTree = ""; }; 35 | D2E3F48C23EF313000939675 /* RemoteImageService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteImageService.swift; sourceTree = ""; }; 36 | D2E3F48E23EF3C2700939675 /* GistCellViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GistCellViewModel.swift; sourceTree = ""; }; 37 | D2E3F4B223F074C800939675 /* ContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 38 | D2E3F4B423F0801100939675 /* SafariView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafariView.swift; sourceTree = ""; }; 39 | D2E3F4B623F0803C00939675 /* SearchBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchBar.swift; sourceTree = ""; }; 40 | D2E3F4B823F0F45100939675 /* GistView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GistView.swift; sourceTree = ""; }; 41 | /* End PBXFileReference section */ 42 | 43 | /* Begin PBXFrameworksBuildPhase section */ 44 | D24C221923EECBCB00759726 /* Frameworks */ = { 45 | isa = PBXFrameworksBuildPhase; 46 | buildActionMask = 2147483647; 47 | files = ( 48 | ); 49 | runOnlyForDeploymentPostprocessing = 0; 50 | }; 51 | /* End PBXFrameworksBuildPhase section */ 52 | 53 | /* Begin PBXGroup section */ 54 | D24C221323EECBCB00759726 = { 55 | isa = PBXGroup; 56 | children = ( 57 | D24C221E23EECBCB00759726 /* GitHubGists */, 58 | D24C221D23EECBCB00759726 /* Products */, 59 | ); 60 | sourceTree = ""; 61 | }; 62 | D24C221D23EECBCB00759726 /* Products */ = { 63 | isa = PBXGroup; 64 | children = ( 65 | D24C221C23EECBCB00759726 /* GitHubGists.app */, 66 | ); 67 | name = Products; 68 | sourceTree = ""; 69 | }; 70 | D24C221E23EECBCB00759726 /* GitHubGists */ = { 71 | isa = PBXGroup; 72 | children = ( 73 | D24C223623EECD4A00759726 /* App */, 74 | D24C223523EECD4600759726 /* Models */, 75 | D24C223423EECD4000759726 /* View Models */, 76 | D24C223323EECD3300759726 /* Views */, 77 | D2E3F48B23EF311A00939675 /* Services */, 78 | D24C223723EECD4E00759726 /* Supporting Files */, 79 | ); 80 | path = GitHubGists; 81 | sourceTree = ""; 82 | }; 83 | D24C223323EECD3300759726 /* Views */ = { 84 | isa = PBXGroup; 85 | children = ( 86 | D2E3F4B223F074C800939675 /* ContentView.swift */, 87 | D2E3F4B823F0F45100939675 /* GistView.swift */, 88 | D2E3F4B423F0801100939675 /* SafariView.swift */, 89 | D2E3F4B623F0803C00939675 /* SearchBar.swift */, 90 | ); 91 | path = Views; 92 | sourceTree = ""; 93 | }; 94 | D24C223423EECD4000759726 /* View Models */ = { 95 | isa = PBXGroup; 96 | children = ( 97 | D2E3F48E23EF3C2700939675 /* GistCellViewModel.swift */, 98 | D2E3F48323EEE01C00939675 /* GistsViewModel.swift */, 99 | ); 100 | path = "View Models"; 101 | sourceTree = ""; 102 | }; 103 | D24C223523EECD4600759726 /* Models */ = { 104 | isa = PBXGroup; 105 | children = ( 106 | D2E3F47F23EEDCD600939675 /* Gist.swift */, 107 | D2E3F48123EEDD0A00939675 /* Owner.swift */, 108 | ); 109 | path = Models; 110 | sourceTree = ""; 111 | }; 112 | D24C223623EECD4A00759726 /* App */ = { 113 | isa = PBXGroup; 114 | children = ( 115 | D24C221F23EECBCB00759726 /* AppDelegate.swift */, 116 | D24C222123EECBCB00759726 /* SceneDelegate.swift */, 117 | ); 118 | path = App; 119 | sourceTree = ""; 120 | }; 121 | D24C223723EECD4E00759726 /* Supporting Files */ = { 122 | isa = PBXGroup; 123 | children = ( 124 | D24C222823EECBCF00759726 /* Assets.xcassets */, 125 | D24C222D23EECBCF00759726 /* Info.plist */, 126 | D24C222A23EECBCF00759726 /* LaunchScreen.storyboard */, 127 | ); 128 | name = "Supporting Files"; 129 | sourceTree = ""; 130 | }; 131 | D2E3F48B23EF311A00939675 /* Services */ = { 132 | isa = PBXGroup; 133 | children = ( 134 | D2E3F48C23EF313000939675 /* RemoteImageService.swift */, 135 | ); 136 | path = Services; 137 | sourceTree = ""; 138 | }; 139 | /* End PBXGroup section */ 140 | 141 | /* Begin PBXNativeTarget section */ 142 | D24C221B23EECBCB00759726 /* GitHubGists */ = { 143 | isa = PBXNativeTarget; 144 | buildConfigurationList = D24C223023EECBCF00759726 /* Build configuration list for PBXNativeTarget "GitHubGists" */; 145 | buildPhases = ( 146 | D24C221823EECBCB00759726 /* Sources */, 147 | D24C221923EECBCB00759726 /* Frameworks */, 148 | D24C221A23EECBCB00759726 /* Resources */, 149 | ); 150 | buildRules = ( 151 | ); 152 | dependencies = ( 153 | ); 154 | name = GitHubGists; 155 | productName = GitHubGists; 156 | productReference = D24C221C23EECBCB00759726 /* GitHubGists.app */; 157 | productType = "com.apple.product-type.application"; 158 | }; 159 | /* End PBXNativeTarget section */ 160 | 161 | /* Begin PBXProject section */ 162 | D24C221423EECBCB00759726 /* Project object */ = { 163 | isa = PBXProject; 164 | attributes = { 165 | LastSwiftUpdateCheck = 1130; 166 | LastUpgradeCheck = 1130; 167 | ORGANIZATIONNAME = "Scott Gardner"; 168 | TargetAttributes = { 169 | D24C221B23EECBCB00759726 = { 170 | CreatedOnToolsVersion = 11.3.1; 171 | }; 172 | }; 173 | }; 174 | buildConfigurationList = D24C221723EECBCB00759726 /* Build configuration list for PBXProject "GitHubGists" */; 175 | compatibilityVersion = "Xcode 9.3"; 176 | developmentRegion = en; 177 | hasScannedForEncodings = 0; 178 | knownRegions = ( 179 | en, 180 | Base, 181 | ); 182 | mainGroup = D24C221323EECBCB00759726; 183 | productRefGroup = D24C221D23EECBCB00759726 /* Products */; 184 | projectDirPath = ""; 185 | projectRoot = ""; 186 | targets = ( 187 | D24C221B23EECBCB00759726 /* GitHubGists */, 188 | ); 189 | }; 190 | /* End PBXProject section */ 191 | 192 | /* Begin PBXResourcesBuildPhase section */ 193 | D24C221A23EECBCB00759726 /* Resources */ = { 194 | isa = PBXResourcesBuildPhase; 195 | buildActionMask = 2147483647; 196 | files = ( 197 | D24C222C23EECBCF00759726 /* LaunchScreen.storyboard in Resources */, 198 | D24C222923EECBCF00759726 /* Assets.xcassets in Resources */, 199 | ); 200 | runOnlyForDeploymentPostprocessing = 0; 201 | }; 202 | /* End PBXResourcesBuildPhase section */ 203 | 204 | /* Begin PBXSourcesBuildPhase section */ 205 | D24C221823EECBCB00759726 /* Sources */ = { 206 | isa = PBXSourcesBuildPhase; 207 | buildActionMask = 2147483647; 208 | files = ( 209 | D2E3F4B723F0803C00939675 /* SearchBar.swift in Sources */, 210 | D2E3F48D23EF313000939675 /* RemoteImageService.swift in Sources */, 211 | D2E3F4B323F074C800939675 /* ContentView.swift in Sources */, 212 | D2E3F4B923F0F45100939675 /* GistView.swift in Sources */, 213 | D2E3F48023EEDCD600939675 /* Gist.swift in Sources */, 214 | D2E3F4B523F0801100939675 /* SafariView.swift in Sources */, 215 | D2E3F48423EEE01C00939675 /* GistsViewModel.swift in Sources */, 216 | D2E3F48F23EF3C2700939675 /* GistCellViewModel.swift in Sources */, 217 | D24C222023EECBCB00759726 /* AppDelegate.swift in Sources */, 218 | D2E3F48223EEDD0A00939675 /* Owner.swift in Sources */, 219 | D24C222223EECBCB00759726 /* SceneDelegate.swift in Sources */, 220 | ); 221 | runOnlyForDeploymentPostprocessing = 0; 222 | }; 223 | /* End PBXSourcesBuildPhase section */ 224 | 225 | /* Begin PBXVariantGroup section */ 226 | D24C222A23EECBCF00759726 /* LaunchScreen.storyboard */ = { 227 | isa = PBXVariantGroup; 228 | children = ( 229 | D24C222B23EECBCF00759726 /* Base */, 230 | ); 231 | name = LaunchScreen.storyboard; 232 | sourceTree = ""; 233 | }; 234 | /* End PBXVariantGroup section */ 235 | 236 | /* Begin XCBuildConfiguration section */ 237 | D24C222E23EECBCF00759726 /* Debug */ = { 238 | isa = XCBuildConfiguration; 239 | buildSettings = { 240 | ALWAYS_SEARCH_USER_PATHS = NO; 241 | CLANG_ANALYZER_NONNULL = YES; 242 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 243 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 244 | CLANG_CXX_LIBRARY = "libc++"; 245 | CLANG_ENABLE_MODULES = YES; 246 | CLANG_ENABLE_OBJC_ARC = YES; 247 | CLANG_ENABLE_OBJC_WEAK = YES; 248 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 249 | CLANG_WARN_BOOL_CONVERSION = YES; 250 | CLANG_WARN_COMMA = YES; 251 | CLANG_WARN_CONSTANT_CONVERSION = YES; 252 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 253 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 254 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 255 | CLANG_WARN_EMPTY_BODY = YES; 256 | CLANG_WARN_ENUM_CONVERSION = YES; 257 | CLANG_WARN_INFINITE_RECURSION = YES; 258 | CLANG_WARN_INT_CONVERSION = YES; 259 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 260 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 261 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 262 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 263 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 264 | CLANG_WARN_STRICT_PROTOTYPES = YES; 265 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 266 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 267 | CLANG_WARN_UNREACHABLE_CODE = YES; 268 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 269 | COPY_PHASE_STRIP = NO; 270 | DEBUG_INFORMATION_FORMAT = dwarf; 271 | ENABLE_STRICT_OBJC_MSGSEND = YES; 272 | ENABLE_TESTABILITY = YES; 273 | GCC_C_LANGUAGE_STANDARD = gnu11; 274 | GCC_DYNAMIC_NO_PIC = NO; 275 | GCC_NO_COMMON_BLOCKS = YES; 276 | GCC_OPTIMIZATION_LEVEL = 0; 277 | GCC_PREPROCESSOR_DEFINITIONS = ( 278 | "DEBUG=1", 279 | "$(inherited)", 280 | ); 281 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 282 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 283 | GCC_WARN_UNDECLARED_SELECTOR = YES; 284 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 285 | GCC_WARN_UNUSED_FUNCTION = YES; 286 | GCC_WARN_UNUSED_VARIABLE = YES; 287 | IPHONEOS_DEPLOYMENT_TARGET = 13.2; 288 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 289 | MTL_FAST_MATH = YES; 290 | ONLY_ACTIVE_ARCH = YES; 291 | SDKROOT = iphoneos; 292 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 293 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 294 | }; 295 | name = Debug; 296 | }; 297 | D24C222F23EECBCF00759726 /* Release */ = { 298 | isa = XCBuildConfiguration; 299 | buildSettings = { 300 | ALWAYS_SEARCH_USER_PATHS = NO; 301 | CLANG_ANALYZER_NONNULL = YES; 302 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 303 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 304 | CLANG_CXX_LIBRARY = "libc++"; 305 | CLANG_ENABLE_MODULES = YES; 306 | CLANG_ENABLE_OBJC_ARC = YES; 307 | CLANG_ENABLE_OBJC_WEAK = YES; 308 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 309 | CLANG_WARN_BOOL_CONVERSION = YES; 310 | CLANG_WARN_COMMA = YES; 311 | CLANG_WARN_CONSTANT_CONVERSION = YES; 312 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 313 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 314 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 315 | CLANG_WARN_EMPTY_BODY = YES; 316 | CLANG_WARN_ENUM_CONVERSION = YES; 317 | CLANG_WARN_INFINITE_RECURSION = YES; 318 | CLANG_WARN_INT_CONVERSION = YES; 319 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 320 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 321 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 322 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 323 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 324 | CLANG_WARN_STRICT_PROTOTYPES = YES; 325 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 326 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 327 | CLANG_WARN_UNREACHABLE_CODE = YES; 328 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 329 | COPY_PHASE_STRIP = NO; 330 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 331 | ENABLE_NS_ASSERTIONS = NO; 332 | ENABLE_STRICT_OBJC_MSGSEND = YES; 333 | GCC_C_LANGUAGE_STANDARD = gnu11; 334 | GCC_NO_COMMON_BLOCKS = YES; 335 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 336 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 337 | GCC_WARN_UNDECLARED_SELECTOR = YES; 338 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 339 | GCC_WARN_UNUSED_FUNCTION = YES; 340 | GCC_WARN_UNUSED_VARIABLE = YES; 341 | IPHONEOS_DEPLOYMENT_TARGET = 13.2; 342 | MTL_ENABLE_DEBUG_INFO = NO; 343 | MTL_FAST_MATH = YES; 344 | SDKROOT = iphoneos; 345 | SWIFT_COMPILATION_MODE = wholemodule; 346 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 347 | VALIDATE_PRODUCT = YES; 348 | }; 349 | name = Release; 350 | }; 351 | D24C223123EECBCF00759726 /* Debug */ = { 352 | isa = XCBuildConfiguration; 353 | buildSettings = { 354 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 355 | CODE_SIGN_STYLE = Automatic; 356 | DEVELOPMENT_TEAM = CX893E63R7; 357 | INFOPLIST_FILE = GitHubGists/Info.plist; 358 | LD_RUNPATH_SEARCH_PATHS = ( 359 | "$(inherited)", 360 | "@executable_path/Frameworks", 361 | ); 362 | PRODUCT_BUNDLE_IDENTIFIER = com.scotteg.GitHubGists; 363 | PRODUCT_NAME = "$(TARGET_NAME)"; 364 | SWIFT_VERSION = 5.0; 365 | TARGETED_DEVICE_FAMILY = "1,2"; 366 | }; 367 | name = Debug; 368 | }; 369 | D24C223223EECBCF00759726 /* Release */ = { 370 | isa = XCBuildConfiguration; 371 | buildSettings = { 372 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 373 | CODE_SIGN_STYLE = Automatic; 374 | DEVELOPMENT_TEAM = CX893E63R7; 375 | INFOPLIST_FILE = GitHubGists/Info.plist; 376 | LD_RUNPATH_SEARCH_PATHS = ( 377 | "$(inherited)", 378 | "@executable_path/Frameworks", 379 | ); 380 | PRODUCT_BUNDLE_IDENTIFIER = com.scotteg.GitHubGists; 381 | PRODUCT_NAME = "$(TARGET_NAME)"; 382 | SWIFT_VERSION = 5.0; 383 | TARGETED_DEVICE_FAMILY = "1,2"; 384 | }; 385 | name = Release; 386 | }; 387 | /* End XCBuildConfiguration section */ 388 | 389 | /* Begin XCConfigurationList section */ 390 | D24C221723EECBCB00759726 /* Build configuration list for PBXProject "GitHubGists" */ = { 391 | isa = XCConfigurationList; 392 | buildConfigurations = ( 393 | D24C222E23EECBCF00759726 /* Debug */, 394 | D24C222F23EECBCF00759726 /* Release */, 395 | ); 396 | defaultConfigurationIsVisible = 0; 397 | defaultConfigurationName = Release; 398 | }; 399 | D24C223023EECBCF00759726 /* Build configuration list for PBXNativeTarget "GitHubGists" */ = { 400 | isa = XCConfigurationList; 401 | buildConfigurations = ( 402 | D24C223123EECBCF00759726 /* Debug */, 403 | D24C223223EECBCF00759726 /* Release */, 404 | ); 405 | defaultConfigurationIsVisible = 0; 406 | defaultConfigurationName = Release; 407 | }; 408 | /* End XCConfigurationList section */ 409 | }; 410 | rootObject = D24C221423EECBCB00759726 /* Project object */; 411 | } 412 | -------------------------------------------------------------------------------- /GitHubGists.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /GitHubGists.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /GitHubGists/App/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // GitHubGists 4 | // 5 | // Created by Scott Gardner on 2/8/20. 6 | // Copyright © 2020 Scott Gardner. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 14 | // Override point for customization after application launch. 15 | UITableView.appearance().separatorInset = .zero 16 | UIScrollView.appearance().keyboardDismissMode = .onDrag 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 | -------------------------------------------------------------------------------- /GitHubGists/App/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // GitHubGists 4 | // 5 | // Created by Scott Gardner on 2/8/20. 6 | // Copyright © 2020 Scott Gardner. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import SwiftUI 11 | 12 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 13 | lazy var window: UIWindow? = UIWindow(frame: UIScreen.main.bounds) 14 | 15 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 16 | guard let windowScene = scene as? UIWindowScene else { return } 17 | let window = UIWindow(windowScene: windowScene) 18 | let contentView = ContentView() 19 | window.rootViewController = UIHostingController(rootView: contentView) 20 | window.makeKeyAndVisible() 21 | self.window = window 22 | } 23 | 24 | func sceneDidDisconnect(_ scene: UIScene) { 25 | // Called as the scene is being released by the system. 26 | // This occurs shortly after the scene enters the background, or when its session is discarded. 27 | // Release any resources associated with this scene that can be re-created the next time the scene connects. 28 | // The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead). 29 | } 30 | 31 | func sceneDidBecomeActive(_ scene: UIScene) { 32 | // Called when the scene has moved from an inactive state to an active state. 33 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. 34 | } 35 | 36 | func sceneWillResignActive(_ scene: UIScene) { 37 | // Called when the scene will move from an active state to an inactive state. 38 | // This may occur due to temporary interruptions (ex. an incoming phone call). 39 | } 40 | 41 | func sceneWillEnterForeground(_ scene: UIScene) { 42 | // Called as the scene transitions from the background to the foreground. 43 | // Use this method to undo the changes made on entering the background. 44 | } 45 | 46 | func sceneDidEnterBackground(_ scene: UIScene) { 47 | // Called as the scene transitions from the foreground to the background. 48 | // Use this method to save data, release shared resources, and store enough scene-specific state information 49 | // to restore the scene back to its current state. 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /GitHubGists/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "ItunesArtwork@2x.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } -------------------------------------------------------------------------------- /GitHubGists/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scotteg/GitHubGists/37178c38543c1b241637e978fc717a85e2b51587/GitHubGists/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /GitHubGists/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scotteg/GitHubGists/37178c38543c1b241637e978fc717a85e2b51587/GitHubGists/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /GitHubGists/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scotteg/GitHubGists/37178c38543c1b241637e978fc717a85e2b51587/GitHubGists/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /GitHubGists/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scotteg/GitHubGists/37178c38543c1b241637e978fc717a85e2b51587/GitHubGists/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /GitHubGists/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scotteg/GitHubGists/37178c38543c1b241637e978fc717a85e2b51587/GitHubGists/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /GitHubGists/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scotteg/GitHubGists/37178c38543c1b241637e978fc717a85e2b51587/GitHubGists/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /GitHubGists/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scotteg/GitHubGists/37178c38543c1b241637e978fc717a85e2b51587/GitHubGists/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /GitHubGists/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scotteg/GitHubGists/37178c38543c1b241637e978fc717a85e2b51587/GitHubGists/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /GitHubGists/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scotteg/GitHubGists/37178c38543c1b241637e978fc717a85e2b51587/GitHubGists/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /GitHubGists/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scotteg/GitHubGists/37178c38543c1b241637e978fc717a85e2b51587/GitHubGists/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /GitHubGists/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scotteg/GitHubGists/37178c38543c1b241637e978fc717a85e2b51587/GitHubGists/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /GitHubGists/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scotteg/GitHubGists/37178c38543c1b241637e978fc717a85e2b51587/GitHubGists/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /GitHubGists/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scotteg/GitHubGists/37178c38543c1b241637e978fc717a85e2b51587/GitHubGists/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /GitHubGists/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scotteg/GitHubGists/37178c38543c1b241637e978fc717a85e2b51587/GitHubGists/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /GitHubGists/Assets.xcassets/AppIcon.appiconset/ItunesArtwork@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scotteg/GitHubGists/37178c38543c1b241637e978fc717a85e2b51587/GitHubGists/Assets.xcassets/AppIcon.appiconset/ItunesArtwork@2x.png -------------------------------------------------------------------------------- /GitHubGists/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /GitHubGists/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 | -------------------------------------------------------------------------------- /GitHubGists/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 | 50 | UISupportedInterfaceOrientations~ipad 51 | 52 | UIInterfaceOrientationPortrait 53 | UIInterfaceOrientationPortraitUpsideDown 54 | UIInterfaceOrientationLandscapeLeft 55 | UIInterfaceOrientationLandscapeRight 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /GitHubGists/Models/Gist.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Gist.swift 3 | // GitHubGists 4 | // 5 | // Created by Scott Gardner on 2/8/20. 6 | // Copyright © 2020 Scott Gardner. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct Gist: Identifiable { 12 | static let iso8601DateFormatter = ISO8601DateFormatter() 13 | 14 | static let dateFormatter: DateFormatter = { 15 | let formatter = DateFormatter() 16 | formatter.dateStyle = .short 17 | formatter.timeStyle = .short 18 | return formatter 19 | }() 20 | 21 | let id = UUID() 22 | let htmlURL: URL 23 | let updatedAtDate: Date 24 | let updatedAt: String 25 | let gistDescription: String 26 | let owner: Owner 27 | } 28 | 29 | extension Gist: Hashable { 30 | static func == (lhs: Gist, rhs: Gist) -> Bool { 31 | lhs.htmlURL == rhs.htmlURL 32 | } 33 | 34 | func hash(into hasher: inout Hasher) { 35 | hasher.combine(htmlURL) 36 | } 37 | } 38 | 39 | extension Gist: Comparable { 40 | static func <(lhs: Gist, rhs: Gist) -> Bool { 41 | lhs.updatedAtDate < rhs.updatedAtDate 42 | } 43 | } 44 | 45 | extension Gist: Decodable { 46 | enum CodingKeys: String, CodingKey { 47 | case htmlURL = "html_url" 48 | case updatedAt = "updated_at" 49 | case gistDescription = "description" 50 | case owner 51 | } 52 | 53 | init(from decoder: Decoder) throws { 54 | let container = try decoder.container(keyedBy: CodingKeys.self) 55 | let htmlURLString = try container.decode(String.self, forKey: .htmlURL) 56 | htmlURL = URL(string: htmlURLString)! 57 | let updatedAtString = try container.decode(String.self, forKey: .updatedAt) 58 | updatedAtDate = Self.iso8601DateFormatter.date(from: updatedAtString)! 59 | updatedAt = Self.dateFormatter.string(from: updatedAtDate) 60 | gistDescription = try container.decodeIfPresent(String.self, forKey: .gistDescription) ?? "No description" 61 | owner = try container.decode(Owner.self, forKey: .owner) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /GitHubGists/Models/Owner.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Owner.swift 3 | // GitHubGists 4 | // 5 | // Created by Scott Gardner on 2/8/20. 6 | // Copyright © 2020 Scott Gardner. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct Owner: Decodable, Hashable { 12 | enum CodingKeys: String, CodingKey { 13 | case login 14 | case avatarURL = "avatar_url" 15 | } 16 | 17 | let login: String 18 | let avatarURL: URL 19 | 20 | init(from decoder: Decoder) throws { 21 | let container = try decoder.container(keyedBy: CodingKeys.self) 22 | login = try container.decode(String.self, forKey: .login) 23 | let avatarURLString = try container.decode(String.self, forKey: .avatarURL) 24 | avatarURL = URL(string: avatarURLString)! 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /GitHubGists/Services/RemoteImageService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RemoteImageService.swift 3 | // GitHubGists 4 | // 5 | // Created by Scott Gardner on 2/8/20. 6 | // Copyright © 2020 Scott Gardner. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import Combine 11 | 12 | final class RemoteImageService { 13 | static let shared = RemoteImageService() 14 | 15 | private let cache = NSCache() 16 | 17 | private init() { } 18 | 19 | func avatarPublisher(for url: URL) -> AnyPublisher { 20 | if let image = cache.object(forKey: url.absoluteString as NSString) { 21 | return Just(image).eraseToAnyPublisher() 22 | } 23 | 24 | return URLSession.shared.dataTaskPublisher(for: url) 25 | .retry(2) 26 | .map(\.data) 27 | .compactMap(UIImage.init) 28 | .compactMap { [weak self] in 29 | self?.resize(image: $0) 30 | } 31 | .replaceError(with: UIImage(systemName: "person.circle.fill")!) 32 | .receive(on: DispatchQueue.main) 33 | .eraseToAnyPublisher() 34 | } 35 | 36 | private func resize(image: UIImage, size: CGSize = CGSize(width: 64, height: 64)) -> UIImage { 37 | UIGraphicsImageRenderer(size: size).image { context in 38 | image.draw(in: CGRect(origin: .zero, size: size)) 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /GitHubGists/View Models/GistCellViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GistCellViewModel.swift 3 | // GitHubGists 4 | // 5 | // Created by Scott Gardner on 2/8/20. 6 | // Copyright © 2020 Scott Gardner. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import Combine 11 | 12 | final class GistCellViewModel: ObservableObject { 13 | @Published var avatarImage = Image(systemName: "person.circle.fill") 14 | let gist: Gist 15 | lazy var gistDescription = gist.gistDescription.trimmingCharacters(in: .whitespacesAndNewlines) 16 | lazy var lastUpdatedString = "Last updated " + gist.updatedAt 17 | lazy var urlString = gist.htmlURL.absoluteString 18 | private var subscriptions = Set() 19 | 20 | init(gist: Gist) { 21 | self.gist = gist 22 | 23 | Just(gist) 24 | .map(\.owner.avatarURL) 25 | .flatMap(RemoteImageService.shared.avatarPublisher(for:)) 26 | .map(Image.init) 27 | .receive(on: DispatchQueue.main) 28 | .assign(to: \.avatarImage, on: self) 29 | .store(in: &subscriptions) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /GitHubGists/View Models/GistsViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GistsViewModel.swift 3 | // GitHubGists 4 | // 5 | // Created by Scott Gardner on 2/8/20. 6 | // Copyright © 2020 Scott Gardner. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Combine 11 | 12 | final class GistsViewModel: ObservableObject { 13 | @Published var searchText = "" 14 | @Published var gists: [Gist] = [] 15 | 16 | private let maxRetries: Int 17 | private var subscriptions = Set() 18 | 19 | init(maxRetries: UInt = 2) { 20 | self.maxRetries = Int(maxRetries) 21 | 22 | $searchText 23 | .debounce(for: 0.3, scheduler: DispatchQueue.main) 24 | .flatMap(fetchGists(for:)) 25 | .map { $0.sorted(by: >) } 26 | .assign(to: \.gists, on: self) 27 | .store(in: &subscriptions) 28 | } 29 | 30 | private func fetchGists(for username: String) -> AnyPublisher<[Gist], Never> { 31 | Just(username) 32 | .handleEvents(receiveOutput: { [weak self] in 33 | if $0.count < 3 { 34 | self?.gists = [] 35 | } 36 | }) 37 | .filter { $0.count > 2 } 38 | .tryMap(url(for:)) 39 | .flatMap(gistsPublisher(for:)) 40 | .replaceError(with: []) 41 | .receive(on: DispatchQueue.main) 42 | .eraseToAnyPublisher() 43 | } 44 | 45 | private func gistsPublisher(for url: URL) -> AnyPublisher<[Gist], Error> { 46 | URLSession.shared.dataTaskPublisher(for: url) 47 | .retry(maxRetries) 48 | .map(\.data) 49 | .decode(type: [Gist].self, decoder: JSONDecoder()) 50 | .eraseToAnyPublisher() 51 | } 52 | 53 | private func url(for username: String) throws -> URL { 54 | var components = URLComponents() 55 | components.scheme = "https" 56 | components.host = "api.github.com" 57 | components.path = "/users/\(username)/gists" 58 | guard let url = components.url else { throw URLError(.badURL) } 59 | return url 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /GitHubGists/Views/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // GitHubGists 4 | // 5 | // Created by Scott Gardner on 2/9/20. 6 | // Copyright © 2020 Scott Gardner. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct ContentView: View { 12 | @ObservedObject private var viewModel = GistsViewModel() 13 | 14 | var body: some View { 15 | NavigationView { 16 | VStack { 17 | SearchBar(text: $viewModel.searchText) 18 | 19 | List { 20 | ForEach(viewModel.gists) { gist in 21 | NavigationLink(destination: SafariView(url: gist.htmlURL) 22 | .navigationBarTitle("") 23 | .navigationBarHidden(true)) { 24 | GistView(gist: gist) 25 | } 26 | } 27 | } 28 | } 29 | .navigationBarTitle(Text("GitHub Gists")) 30 | .edgesIgnoringSafeArea(.bottom) 31 | } 32 | } 33 | } 34 | 35 | struct ContentView_Previews: PreviewProvider { 36 | static var previews: some View { 37 | ContentView() 38 | } 39 | } 40 | 41 | extension UIView { 42 | open override func touchesBegan(_ touches: Set, with event: UIEvent?) { 43 | super.touchesBegan(touches, with: event) 44 | UIApplication.shared.windows 45 | .first { $0.isKeyWindow }? 46 | .endEditing(true) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /GitHubGists/Views/GistView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GistView.swift 3 | // GitHubGists 4 | // 5 | // Created by Scott Gardner on 2/9/20. 6 | // Copyright © 2020 Scott Gardner. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct GistView: View { 12 | @ObservedObject private var viewModel: GistCellViewModel 13 | 14 | var body: some View { 15 | VStack(alignment: .leading, spacing: 8) { 16 | HStack(alignment: .top, spacing: 12) { 17 | VStack { 18 | viewModel.avatarImage 19 | .resizable() 20 | .aspectRatio(contentMode: .fill) 21 | .frame(width: 64, height: 64) 22 | .foregroundColor(Color(#colorLiteral(red: 0, green: 0.5898008943, blue: 1, alpha: 1))) 23 | .background(Color(#colorLiteral(red: 0.8039215803, green: 0.8039215803, blue: 0.8039215803, alpha: 1))) 24 | .clipShape(Circle()) 25 | 26 | Text(viewModel.gist.owner.login) 27 | .foregroundColor(Color(#colorLiteral(red: 0.501960814, green: 0.501960814, blue: 0.501960814, alpha: 1))) 28 | } 29 | 30 | Text(viewModel.gist.gistDescription) 31 | .font(.headline) 32 | .foregroundColor(Color(#colorLiteral(red: 0.501960814, green: 0.501960814, blue: 0.501960814, alpha: 1))) 33 | } 34 | 35 | Text("Last updated " + viewModel.gist.updatedAt) 36 | .font(.caption) 37 | .foregroundColor(Color(#colorLiteral(red: 0.6000000238, green: 0.6000000238, blue: 0.6000000238, alpha: 1))) 38 | 39 | Text(viewModel.gist.htmlURL.absoluteString) 40 | .font(.caption) 41 | .foregroundColor(Color(#colorLiteral(red: 0, green: 0.5898008943, blue: 1, alpha: 1))) 42 | } 43 | } 44 | 45 | init(gist: Gist) { 46 | viewModel = GistCellViewModel(gist: gist) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /GitHubGists/Views/SafariView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SafariView.swift 3 | // GitHubGists 4 | // 5 | // Created by Scott Gardner on 2/9/20. 6 | // Copyright © 2020 Scott Gardner. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import SafariServices 11 | 12 | struct SafariView: UIViewControllerRepresentable { 13 | @Environment(\.presentationMode) var presentationMode 14 | let url: URL 15 | 16 | final class Coordinator: NSObject, SFSafariViewControllerDelegate { 17 | @Binding var presentationMode: PresentationMode 18 | 19 | init(presentationMode: Binding) { 20 | _presentationMode = presentationMode 21 | } 22 | 23 | func safariViewControllerDidFinish(_ controller: SFSafariViewController) { 24 | presentationMode.dismiss() 25 | } 26 | } 27 | 28 | func makeCoordinator() -> Coordinator { 29 | Coordinator(presentationMode: presentationMode) 30 | } 31 | 32 | func makeUIViewController(context: Context) -> SFSafariViewController { 33 | let controller = SFSafariViewController(url: url) 34 | controller.preferredBarTintColor = .white 35 | controller.delegate = context.coordinator 36 | return controller 37 | } 38 | 39 | func updateUIViewController(_ uiView: SFSafariViewController, context: Context) { 40 | 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /GitHubGists/Views/SearchBar.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SearchBar.swift 3 | // GitHubGists 4 | // 5 | // Created by Scott Gardner on 2/9/20. 6 | // Copyright © 2020 Scott Gardner. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct SearchBar: UIViewRepresentable { 12 | @Binding var text: String 13 | 14 | final class Coordinator: NSObject, UISearchBarDelegate { 15 | @Binding var text: String 16 | 17 | init(text: Binding) { 18 | _text = text 19 | } 20 | 21 | func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) { 22 | text = searchText 23 | } 24 | 25 | func searchBarSearchButtonClicked(_ searchBar: UISearchBar) { 26 | searchBar.resignFirstResponder() 27 | } 28 | } 29 | 30 | func makeCoordinator() -> Coordinator { 31 | Coordinator(text: $text) 32 | } 33 | 34 | func makeUIView(context: Context) -> UISearchBar { 35 | let searchBar = UISearchBar() 36 | searchBar.searchTextField.autocapitalizationType = .none 37 | searchBar.placeholder = "Enter GitHub username" 38 | searchBar.delegate = context.coordinator 39 | return searchBar 40 | } 41 | 42 | func updateUIView(_ uiView: UISearchBar, context: Context) { 43 | uiView.text = text 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Scott Gardner 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 | # GitHubGists 2 | 3 | ### GitHub Gists demo project for my SwiftUI and Combine All The Things presentation 4 | 5 | In this webinar, you will learn the basics of SwiftUI and Combine, and then convert a UIKit app to use SwiftUI and Combine. 6 | 7 | ![ScreenShot](/Assets/SwiftUIAndCombineAllTheThings.png) 8 | --------------------------------------------------------------------------------