├── .gitignore ├── BlogApp.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcuserdata │ └── shelkford.xcuserdatad │ └── xcschemes │ └── xcschememanagement.plist ├── BlogApp ├── AllPosts.swift ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json ├── BlogAppApp.swift ├── BlogPostCardList.swift ├── BlogPostCardMain.swift ├── BlogPostView.swift ├── ContentView.swift ├── HexColorExtension.swift ├── Info.plist ├── MainView.swift ├── Models │ ├── BlogPostModel.swift │ └── BlogPostStore.swift └── Preview Content │ └── Preview Assets.xcassets │ └── Contents.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.toptal.com/developers/gitignore/api/swift 3 | # Edit at https://www.toptal.com/developers/gitignore?templates=swift 4 | 5 | ### Swift ### 6 | # Xcode 7 | # 8 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 9 | 10 | ## User settings 11 | xcuserdata/ 12 | 13 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 14 | *.xcscmblueprint 15 | *.xccheckout 16 | 17 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 18 | build/ 19 | DerivedData/ 20 | *.moved-aside 21 | *.pbxuser 22 | !default.pbxuser 23 | *.mode1v3 24 | !default.mode1v3 25 | *.mode2v3 26 | !default.mode2v3 27 | *.perspectivev3 28 | !default.perspectivev3 29 | 30 | ## Obj-C/Swift specific 31 | *.hmap 32 | 33 | ## App packaging 34 | *.ipa 35 | *.dSYM.zip 36 | *.dSYM 37 | 38 | ## Playgrounds 39 | timeline.xctimeline 40 | playground.xcworkspace 41 | 42 | # Swift Package Manager 43 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 44 | # Packages/ 45 | # Package.pins 46 | # Package.resolved 47 | # *.xcodeproj 48 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 49 | # hence it is not needed unless you have added a package configuration file to your project 50 | # .swiftpm 51 | 52 | .build/ 53 | 54 | # CocoaPods 55 | # We recommend against adding the Pods directory to your .gitignore. However 56 | # you should judge for yourself, the pros and cons are mentioned at: 57 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 58 | # Pods/ 59 | # Add this line if you want to avoid checking in source code from the Xcode workspace 60 | # *.xcworkspace 61 | 62 | # Carthage 63 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 64 | # Carthage/Checkouts 65 | 66 | Carthage/Build/ 67 | 68 | # Accio dependency management 69 | Dependencies/ 70 | .accio/ 71 | 72 | # fastlane 73 | # It is recommended to not store the screenshots in the git repo. 74 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 75 | # For more information about the recommended setup visit: 76 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 77 | 78 | fastlane/report.xml 79 | fastlane/Preview.html 80 | fastlane/screenshots/**/*.png 81 | fastlane/test_output 82 | 83 | # Code Injection 84 | # After new code Injection tools there's a generated folder /iOSInjectionProject 85 | # https://github.com/johnno1962/injectionforxcode 86 | 87 | iOSInjectionProject/ 88 | 89 | # End of https://www.toptal.com/developers/gitignore/api/swift 90 | -------------------------------------------------------------------------------- /BlogApp.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 52; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | C53A534126ECDF110093940D /* BlogAppApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = C53A534026ECDF110093940D /* BlogAppApp.swift */; }; 11 | C53A534326ECDF110093940D /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C53A534226ECDF110093940D /* ContentView.swift */; }; 12 | C53A534526ECDF140093940D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C53A534426ECDF140093940D /* Assets.xcassets */; }; 13 | C53A534826ECDF140093940D /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C53A534726ECDF140093940D /* Preview Assets.xcassets */; }; 14 | C53A535426ECE1BA0093940D /* Contentful in Frameworks */ = {isa = PBXBuildFile; productRef = C53A535326ECE1BA0093940D /* Contentful */; }; 15 | C53A535726ECE1DB0093940D /* SDWebImageSwiftUI in Frameworks */ = {isa = PBXBuildFile; productRef = C53A535626ECE1DB0093940D /* SDWebImageSwiftUI */; }; 16 | C53A535926ECE2450093940D /* MainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C53A535826ECE2450093940D /* MainView.swift */; }; 17 | C53A535B26ECE2520093940D /* AllPosts.swift in Sources */ = {isa = PBXBuildFile; fileRef = C53A535A26ECE2520093940D /* AllPosts.swift */; }; 18 | C53A535D26ECE2660093940D /* BlogPostCardMain.swift in Sources */ = {isa = PBXBuildFile; fileRef = C53A535C26ECE2660093940D /* BlogPostCardMain.swift */; }; 19 | C53A535F26ECE2740093940D /* BlogPostCardList.swift in Sources */ = {isa = PBXBuildFile; fileRef = C53A535E26ECE2740093940D /* BlogPostCardList.swift */; }; 20 | C53A536126ECE28A0093940D /* BlogPostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C53A536026ECE28A0093940D /* BlogPostView.swift */; }; 21 | C53A536326ECE2990093940D /* HexColorExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C53A536226ECE2990093940D /* HexColorExtension.swift */; }; 22 | C53A536626ECE2E20093940D /* BlogPostModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C53A536526ECE2E20093940D /* BlogPostModel.swift */; }; 23 | C53A536826ECE2EC0093940D /* BlogPostStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = C53A536726ECE2EC0093940D /* BlogPostStore.swift */; }; 24 | /* End PBXBuildFile section */ 25 | 26 | /* Begin PBXFileReference section */ 27 | C53A533D26ECDF110093940D /* BlogApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = BlogApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; 28 | C53A534026ECDF110093940D /* BlogAppApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlogAppApp.swift; sourceTree = ""; }; 29 | C53A534226ECDF110093940D /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 30 | C53A534426ECDF140093940D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 31 | C53A534726ECDF140093940D /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 32 | C53A534926ECDF140093940D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 33 | C53A535826ECE2450093940D /* MainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainView.swift; sourceTree = ""; }; 34 | C53A535A26ECE2520093940D /* AllPosts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllPosts.swift; sourceTree = ""; }; 35 | C53A535C26ECE2660093940D /* BlogPostCardMain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlogPostCardMain.swift; sourceTree = ""; }; 36 | C53A535E26ECE2740093940D /* BlogPostCardList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlogPostCardList.swift; sourceTree = ""; }; 37 | C53A536026ECE28A0093940D /* BlogPostView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlogPostView.swift; sourceTree = ""; }; 38 | C53A536226ECE2990093940D /* HexColorExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HexColorExtension.swift; sourceTree = ""; }; 39 | C53A536526ECE2E20093940D /* BlogPostModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlogPostModel.swift; sourceTree = ""; }; 40 | C53A536726ECE2EC0093940D /* BlogPostStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlogPostStore.swift; sourceTree = ""; }; 41 | /* End PBXFileReference section */ 42 | 43 | /* Begin PBXFrameworksBuildPhase section */ 44 | C53A533A26ECDF110093940D /* Frameworks */ = { 45 | isa = PBXFrameworksBuildPhase; 46 | buildActionMask = 2147483647; 47 | files = ( 48 | C53A535726ECE1DB0093940D /* SDWebImageSwiftUI in Frameworks */, 49 | C53A535426ECE1BA0093940D /* Contentful in Frameworks */, 50 | ); 51 | runOnlyForDeploymentPostprocessing = 0; 52 | }; 53 | /* End PBXFrameworksBuildPhase section */ 54 | 55 | /* Begin PBXGroup section */ 56 | C53A533426ECDF110093940D = { 57 | isa = PBXGroup; 58 | children = ( 59 | C53A533F26ECDF110093940D /* BlogApp */, 60 | C53A533E26ECDF110093940D /* Products */, 61 | ); 62 | sourceTree = ""; 63 | }; 64 | C53A533E26ECDF110093940D /* Products */ = { 65 | isa = PBXGroup; 66 | children = ( 67 | C53A533D26ECDF110093940D /* BlogApp.app */, 68 | ); 69 | name = Products; 70 | sourceTree = ""; 71 | }; 72 | C53A533F26ECDF110093940D /* BlogApp */ = { 73 | isa = PBXGroup; 74 | children = ( 75 | C53A534926ECDF140093940D /* Info.plist */, 76 | C53A534026ECDF110093940D /* BlogAppApp.swift */, 77 | C53A534226ECDF110093940D /* ContentView.swift */, 78 | C53A535826ECE2450093940D /* MainView.swift */, 79 | C53A535A26ECE2520093940D /* AllPosts.swift */, 80 | C53A535C26ECE2660093940D /* BlogPostCardMain.swift */, 81 | C53A535E26ECE2740093940D /* BlogPostCardList.swift */, 82 | C53A536026ECE28A0093940D /* BlogPostView.swift */, 83 | C53A536226ECE2990093940D /* HexColorExtension.swift */, 84 | C53A536426ECE2CD0093940D /* Models */, 85 | C53A534426ECDF140093940D /* Assets.xcassets */, 86 | C53A534626ECDF140093940D /* Preview Content */, 87 | ); 88 | path = BlogApp; 89 | sourceTree = ""; 90 | }; 91 | C53A534626ECDF140093940D /* Preview Content */ = { 92 | isa = PBXGroup; 93 | children = ( 94 | C53A534726ECDF140093940D /* Preview Assets.xcassets */, 95 | ); 96 | path = "Preview Content"; 97 | sourceTree = ""; 98 | }; 99 | C53A536426ECE2CD0093940D /* Models */ = { 100 | isa = PBXGroup; 101 | children = ( 102 | C53A536526ECE2E20093940D /* BlogPostModel.swift */, 103 | C53A536726ECE2EC0093940D /* BlogPostStore.swift */, 104 | ); 105 | path = Models; 106 | sourceTree = ""; 107 | }; 108 | /* End PBXGroup section */ 109 | 110 | /* Begin PBXNativeTarget section */ 111 | C53A533C26ECDF110093940D /* BlogApp */ = { 112 | isa = PBXNativeTarget; 113 | buildConfigurationList = C53A534C26ECDF140093940D /* Build configuration list for PBXNativeTarget "BlogApp" */; 114 | buildPhases = ( 115 | C53A533926ECDF110093940D /* Sources */, 116 | C53A533A26ECDF110093940D /* Frameworks */, 117 | C53A533B26ECDF110093940D /* Resources */, 118 | ); 119 | buildRules = ( 120 | ); 121 | dependencies = ( 122 | ); 123 | name = BlogApp; 124 | packageProductDependencies = ( 125 | C53A535326ECE1BA0093940D /* Contentful */, 126 | C53A535626ECE1DB0093940D /* SDWebImageSwiftUI */, 127 | ); 128 | productName = BlogApp; 129 | productReference = C53A533D26ECDF110093940D /* BlogApp.app */; 130 | productType = "com.apple.product-type.application"; 131 | }; 132 | /* End PBXNativeTarget section */ 133 | 134 | /* Begin PBXProject section */ 135 | C53A533526ECDF110093940D /* Project object */ = { 136 | isa = PBXProject; 137 | attributes = { 138 | LastSwiftUpdateCheck = 1250; 139 | LastUpgradeCheck = 1250; 140 | TargetAttributes = { 141 | C53A533C26ECDF110093940D = { 142 | CreatedOnToolsVersion = 12.5.1; 143 | }; 144 | }; 145 | }; 146 | buildConfigurationList = C53A533826ECDF110093940D /* Build configuration list for PBXProject "BlogApp" */; 147 | compatibilityVersion = "Xcode 9.3"; 148 | developmentRegion = en; 149 | hasScannedForEncodings = 0; 150 | knownRegions = ( 151 | en, 152 | Base, 153 | ); 154 | mainGroup = C53A533426ECDF110093940D; 155 | packageReferences = ( 156 | C53A535226ECE1BA0093940D /* XCRemoteSwiftPackageReference "contentful.swift" */, 157 | C53A535526ECE1DB0093940D /* XCRemoteSwiftPackageReference "SDWebImageSwiftUI" */, 158 | ); 159 | productRefGroup = C53A533E26ECDF110093940D /* Products */; 160 | projectDirPath = ""; 161 | projectRoot = ""; 162 | targets = ( 163 | C53A533C26ECDF110093940D /* BlogApp */, 164 | ); 165 | }; 166 | /* End PBXProject section */ 167 | 168 | /* Begin PBXResourcesBuildPhase section */ 169 | C53A533B26ECDF110093940D /* Resources */ = { 170 | isa = PBXResourcesBuildPhase; 171 | buildActionMask = 2147483647; 172 | files = ( 173 | C53A534826ECDF140093940D /* Preview Assets.xcassets in Resources */, 174 | C53A534526ECDF140093940D /* Assets.xcassets in Resources */, 175 | ); 176 | runOnlyForDeploymentPostprocessing = 0; 177 | }; 178 | /* End PBXResourcesBuildPhase section */ 179 | 180 | /* Begin PBXSourcesBuildPhase section */ 181 | C53A533926ECDF110093940D /* Sources */ = { 182 | isa = PBXSourcesBuildPhase; 183 | buildActionMask = 2147483647; 184 | files = ( 185 | C53A535D26ECE2660093940D /* BlogPostCardMain.swift in Sources */, 186 | C53A536826ECE2EC0093940D /* BlogPostStore.swift in Sources */, 187 | C53A535F26ECE2740093940D /* BlogPostCardList.swift in Sources */, 188 | C53A534326ECDF110093940D /* ContentView.swift in Sources */, 189 | C53A536326ECE2990093940D /* HexColorExtension.swift in Sources */, 190 | C53A536126ECE28A0093940D /* BlogPostView.swift in Sources */, 191 | C53A535926ECE2450093940D /* MainView.swift in Sources */, 192 | C53A534126ECDF110093940D /* BlogAppApp.swift in Sources */, 193 | C53A536626ECE2E20093940D /* BlogPostModel.swift in Sources */, 194 | C53A535B26ECE2520093940D /* AllPosts.swift in Sources */, 195 | ); 196 | runOnlyForDeploymentPostprocessing = 0; 197 | }; 198 | /* End PBXSourcesBuildPhase section */ 199 | 200 | /* Begin XCBuildConfiguration section */ 201 | C53A534A26ECDF140093940D /* Debug */ = { 202 | isa = XCBuildConfiguration; 203 | buildSettings = { 204 | ALWAYS_SEARCH_USER_PATHS = NO; 205 | CLANG_ANALYZER_NONNULL = YES; 206 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 207 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 208 | CLANG_CXX_LIBRARY = "libc++"; 209 | CLANG_ENABLE_MODULES = YES; 210 | CLANG_ENABLE_OBJC_ARC = YES; 211 | CLANG_ENABLE_OBJC_WEAK = YES; 212 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 213 | CLANG_WARN_BOOL_CONVERSION = YES; 214 | CLANG_WARN_COMMA = YES; 215 | CLANG_WARN_CONSTANT_CONVERSION = YES; 216 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 217 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 218 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 219 | CLANG_WARN_EMPTY_BODY = YES; 220 | CLANG_WARN_ENUM_CONVERSION = YES; 221 | CLANG_WARN_INFINITE_RECURSION = YES; 222 | CLANG_WARN_INT_CONVERSION = YES; 223 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 224 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 225 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 226 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 227 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 228 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 229 | CLANG_WARN_STRICT_PROTOTYPES = YES; 230 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 231 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 232 | CLANG_WARN_UNREACHABLE_CODE = YES; 233 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 234 | COPY_PHASE_STRIP = NO; 235 | DEBUG_INFORMATION_FORMAT = dwarf; 236 | ENABLE_STRICT_OBJC_MSGSEND = YES; 237 | ENABLE_TESTABILITY = YES; 238 | GCC_C_LANGUAGE_STANDARD = gnu11; 239 | GCC_DYNAMIC_NO_PIC = NO; 240 | GCC_NO_COMMON_BLOCKS = YES; 241 | GCC_OPTIMIZATION_LEVEL = 0; 242 | GCC_PREPROCESSOR_DEFINITIONS = ( 243 | "DEBUG=1", 244 | "$(inherited)", 245 | ); 246 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 247 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 248 | GCC_WARN_UNDECLARED_SELECTOR = YES; 249 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 250 | GCC_WARN_UNUSED_FUNCTION = YES; 251 | GCC_WARN_UNUSED_VARIABLE = YES; 252 | IPHONEOS_DEPLOYMENT_TARGET = 14.5; 253 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 254 | MTL_FAST_MATH = YES; 255 | ONLY_ACTIVE_ARCH = YES; 256 | SDKROOT = iphoneos; 257 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 258 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 259 | }; 260 | name = Debug; 261 | }; 262 | C53A534B26ECDF140093940D /* Release */ = { 263 | isa = XCBuildConfiguration; 264 | buildSettings = { 265 | ALWAYS_SEARCH_USER_PATHS = NO; 266 | CLANG_ANALYZER_NONNULL = YES; 267 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 268 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 269 | CLANG_CXX_LIBRARY = "libc++"; 270 | CLANG_ENABLE_MODULES = YES; 271 | CLANG_ENABLE_OBJC_ARC = YES; 272 | CLANG_ENABLE_OBJC_WEAK = YES; 273 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 274 | CLANG_WARN_BOOL_CONVERSION = YES; 275 | CLANG_WARN_COMMA = YES; 276 | CLANG_WARN_CONSTANT_CONVERSION = YES; 277 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 278 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 279 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 280 | CLANG_WARN_EMPTY_BODY = YES; 281 | CLANG_WARN_ENUM_CONVERSION = YES; 282 | CLANG_WARN_INFINITE_RECURSION = YES; 283 | CLANG_WARN_INT_CONVERSION = YES; 284 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 285 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 286 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 287 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 288 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 289 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 290 | CLANG_WARN_STRICT_PROTOTYPES = YES; 291 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 292 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 293 | CLANG_WARN_UNREACHABLE_CODE = YES; 294 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 295 | COPY_PHASE_STRIP = NO; 296 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 297 | ENABLE_NS_ASSERTIONS = NO; 298 | ENABLE_STRICT_OBJC_MSGSEND = YES; 299 | GCC_C_LANGUAGE_STANDARD = gnu11; 300 | GCC_NO_COMMON_BLOCKS = YES; 301 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 302 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 303 | GCC_WARN_UNDECLARED_SELECTOR = YES; 304 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 305 | GCC_WARN_UNUSED_FUNCTION = YES; 306 | GCC_WARN_UNUSED_VARIABLE = YES; 307 | IPHONEOS_DEPLOYMENT_TARGET = 14.5; 308 | MTL_ENABLE_DEBUG_INFO = NO; 309 | MTL_FAST_MATH = YES; 310 | SDKROOT = iphoneos; 311 | SWIFT_COMPILATION_MODE = wholemodule; 312 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 313 | VALIDATE_PRODUCT = YES; 314 | }; 315 | name = Release; 316 | }; 317 | C53A534D26ECDF140093940D /* Debug */ = { 318 | isa = XCBuildConfiguration; 319 | buildSettings = { 320 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 321 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 322 | CODE_SIGN_STYLE = Automatic; 323 | DEVELOPMENT_ASSET_PATHS = "\"BlogApp/Preview Content\""; 324 | DEVELOPMENT_TEAM = 4T7X5D3BA4; 325 | ENABLE_PREVIEWS = YES; 326 | INFOPLIST_FILE = BlogApp/Info.plist; 327 | IPHONEOS_DEPLOYMENT_TARGET = 14.0; 328 | LD_RUNPATH_SEARCH_PATHS = ( 329 | "$(inherited)", 330 | "@executable_path/Frameworks", 331 | ); 332 | PRODUCT_BUNDLE_IDENTIFIER = com.shelkford.BlogApp; 333 | PRODUCT_NAME = "$(TARGET_NAME)"; 334 | SWIFT_VERSION = 5.0; 335 | TARGETED_DEVICE_FAMILY = "1,2"; 336 | }; 337 | name = Debug; 338 | }; 339 | C53A534E26ECDF140093940D /* Release */ = { 340 | isa = XCBuildConfiguration; 341 | buildSettings = { 342 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 343 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 344 | CODE_SIGN_STYLE = Automatic; 345 | DEVELOPMENT_ASSET_PATHS = "\"BlogApp/Preview Content\""; 346 | DEVELOPMENT_TEAM = 4T7X5D3BA4; 347 | ENABLE_PREVIEWS = YES; 348 | INFOPLIST_FILE = BlogApp/Info.plist; 349 | IPHONEOS_DEPLOYMENT_TARGET = 14.0; 350 | LD_RUNPATH_SEARCH_PATHS = ( 351 | "$(inherited)", 352 | "@executable_path/Frameworks", 353 | ); 354 | PRODUCT_BUNDLE_IDENTIFIER = com.shelkford.BlogApp; 355 | PRODUCT_NAME = "$(TARGET_NAME)"; 356 | SWIFT_VERSION = 5.0; 357 | TARGETED_DEVICE_FAMILY = "1,2"; 358 | }; 359 | name = Release; 360 | }; 361 | /* End XCBuildConfiguration section */ 362 | 363 | /* Begin XCConfigurationList section */ 364 | C53A533826ECDF110093940D /* Build configuration list for PBXProject "BlogApp" */ = { 365 | isa = XCConfigurationList; 366 | buildConfigurations = ( 367 | C53A534A26ECDF140093940D /* Debug */, 368 | C53A534B26ECDF140093940D /* Release */, 369 | ); 370 | defaultConfigurationIsVisible = 0; 371 | defaultConfigurationName = Release; 372 | }; 373 | C53A534C26ECDF140093940D /* Build configuration list for PBXNativeTarget "BlogApp" */ = { 374 | isa = XCConfigurationList; 375 | buildConfigurations = ( 376 | C53A534D26ECDF140093940D /* Debug */, 377 | C53A534E26ECDF140093940D /* Release */, 378 | ); 379 | defaultConfigurationIsVisible = 0; 380 | defaultConfigurationName = Release; 381 | }; 382 | /* End XCConfigurationList section */ 383 | 384 | /* Begin XCRemoteSwiftPackageReference section */ 385 | C53A535226ECE1BA0093940D /* XCRemoteSwiftPackageReference "contentful.swift" */ = { 386 | isa = XCRemoteSwiftPackageReference; 387 | repositoryURL = "https://github.com/contentful/contentful.swift.git"; 388 | requirement = { 389 | kind = upToNextMajorVersion; 390 | minimumVersion = 5.5.1; 391 | }; 392 | }; 393 | C53A535526ECE1DB0093940D /* XCRemoteSwiftPackageReference "SDWebImageSwiftUI" */ = { 394 | isa = XCRemoteSwiftPackageReference; 395 | repositoryURL = "https://github.com/SDWebImage/SDWebImageSwiftUI.git"; 396 | requirement = { 397 | kind = upToNextMajorVersion; 398 | minimumVersion = 2.0.2; 399 | }; 400 | }; 401 | /* End XCRemoteSwiftPackageReference section */ 402 | 403 | /* Begin XCSwiftPackageProductDependency section */ 404 | C53A535326ECE1BA0093940D /* Contentful */ = { 405 | isa = XCSwiftPackageProductDependency; 406 | package = C53A535226ECE1BA0093940D /* XCRemoteSwiftPackageReference "contentful.swift" */; 407 | productName = Contentful; 408 | }; 409 | C53A535626ECE1DB0093940D /* SDWebImageSwiftUI */ = { 410 | isa = XCSwiftPackageProductDependency; 411 | package = C53A535526ECE1DB0093940D /* XCRemoteSwiftPackageReference "SDWebImageSwiftUI" */; 412 | productName = SDWebImageSwiftUI; 413 | }; 414 | /* End XCSwiftPackageProductDependency section */ 415 | }; 416 | rootObject = C53A533526ECDF110093940D /* Project object */; 417 | } 418 | -------------------------------------------------------------------------------- /BlogApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /BlogApp.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /BlogApp.xcodeproj/xcuserdata/shelkford.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | BlogApp.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | Contentful (Playground) 1.xcscheme 13 | 14 | isShown 15 | 16 | orderHint 17 | 2 18 | 19 | Contentful (Playground) 2.xcscheme 20 | 21 | isShown 22 | 23 | orderHint 24 | 3 25 | 26 | Contentful (Playground).xcscheme 27 | 28 | isShown 29 | 30 | orderHint 31 | 1 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /BlogApp/AllPosts.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AllArticles.swift 3 | // BlogAppSwiftUI 4 | // 5 | // Created by Roman Luzgin on 23.07.21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct AllPosts: View { 11 | @EnvironmentObject var store: BlogPostsStore 12 | 13 | var body: some View { 14 | NavigationView { 15 | List { 16 | ForEach(store.blogPosts) {post in 17 | NavigationLink(destination: BlogPostView(blogPost: post)) { 18 | BlogPostCardList(blogPost: post) 19 | } 20 | } 21 | } 22 | .navigationTitle("All blog posts") 23 | .listStyle(InsetListStyle()) 24 | } 25 | } 26 | } 27 | 28 | struct AllPosts_Previews: PreviewProvider { 29 | static var previews: some View { 30 | AllPosts() 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /BlogApp/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /BlogApp/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 | -------------------------------------------------------------------------------- /BlogApp/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /BlogApp/BlogAppApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BlogAppApp.swift 3 | // BlogApp 4 | // 5 | // Created by Roman Luzgin on 11.09.21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @main 11 | struct BlogAppSwiftUIApp: App { 12 | var body: some Scene { 13 | WindowGroup { 14 | ContentView() 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /BlogApp/BlogPostCardList.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ArticleCardList.swift 3 | // BlogAppSwiftUI 4 | // 5 | // Created by Roman Luzgin on 23.07.21. 6 | // 7 | 8 | import SwiftUI 9 | import SDWebImageSwiftUI 10 | 11 | struct BlogPostCardList: View { 12 | 13 | var blogPost: BlogPost 14 | 15 | var body: some View { 16 | VStack(alignment: .leading) { 17 | WebImage(url: blogPost.image) 18 | .resizable() 19 | .aspectRatio(contentMode: .fill) 20 | .frame(maxWidth: .infinity) 21 | .frame(height: 180) 22 | .frame(maxWidth: UIScreen.main.bounds.width - 60) 23 | .clipped() 24 | .clipShape(RoundedRectangle(cornerRadius: 20, style: .continuous)) 25 | VStack(spacing: 6) { 26 | HStack { 27 | Text(blogPost.title) 28 | .multilineTextAlignment(.leading) 29 | .fixedSize(horizontal: false, vertical: true) 30 | .lineLimit(3) 31 | .font(Font.title2.bold()) 32 | .foregroundColor(.primary) 33 | Spacer() 34 | } 35 | 36 | HStack { 37 | Text(blogPost.subtitle) 38 | .multilineTextAlignment(.leading) 39 | .fixedSize(horizontal: false, vertical: true) 40 | .lineLimit(3) 41 | .font(.subheadline) 42 | .foregroundColor(.secondary) 43 | Spacer() 44 | } 45 | } 46 | } 47 | .frame(maxWidth: UIScreen.main.bounds.width - 60, alignment: .leading) 48 | .padding() 49 | } 50 | } 51 | 52 | struct BlogPostCardList_Previews: PreviewProvider { 53 | static let store = BlogPostsStore() 54 | static let placeholder = BlogPost(title: "This is a placeholder", subtitle: "A subtitle for the placeholder", image: URL(string: "https://media.nature.com/lw800/magazine-assets/d41586-020-03053-2/d41586-020-03053-2_18533904.jpg"), blogpost: "Blog post") 55 | 56 | static var previews: some View { 57 | BlogPostCardList(blogPost: placeholder) 58 | .environmentObject(store) 59 | 60 | BlogPostCardList(blogPost: placeholder) 61 | .environmentObject(store) 62 | .previewDevice(PreviewDevice(rawValue: "iPhone 8")) 63 | .previewDisplayName("iPhone 8") 64 | .preferredColorScheme(.dark) 65 | } 66 | } 67 | 68 | -------------------------------------------------------------------------------- /BlogApp/BlogPostCardMain.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ArticleCardMain.swift 3 | // BlogAppSwiftUI 4 | // 5 | // Created by Roman Luzgin on 23.07.21. 6 | // 7 | 8 | import SwiftUI 9 | import SDWebImageSwiftUI 10 | 11 | struct BlogPostCardMain: View { 12 | 13 | @Environment(\.colorScheme) var colorScheme 14 | 15 | var blogPost: BlogPost 16 | 17 | var body: some View { 18 | VStack(alignment: .leading) { 19 | WebImage(url: blogPost.image) 20 | .resizable() 21 | .aspectRatio(contentMode: .fill) 22 | .frame(height: 220) 23 | .frame(maxWidth: UIScreen.main.bounds.width - 80) 24 | .clipped() 25 | .clipShape(RoundedRectangle(cornerRadius: 20, style: .continuous)) 26 | VStack(spacing: 6) { 27 | HStack { 28 | Text(blogPost.title) 29 | .multilineTextAlignment(.leading) 30 | .fixedSize(horizontal: false, vertical: true) 31 | .lineLimit(3) 32 | .font(Font.title2.bold()) 33 | .foregroundColor(.primary) 34 | Spacer() 35 | } 36 | HStack { 37 | Text(blogPost.subtitle) 38 | .multilineTextAlignment(.leading) 39 | .fixedSize(horizontal: false, vertical: true) 40 | .lineLimit(3) 41 | .font(.subheadline) 42 | .foregroundColor(.secondary) 43 | Spacer() 44 | } 45 | } 46 | .frame(height: 110) 47 | } 48 | .padding(15) 49 | .background(colorScheme == .dark ? Color(hex: "#121212") : Color.white) 50 | .frame(maxWidth: UIScreen.main.bounds.width - 50, alignment: .leading) 51 | .clipShape(RoundedRectangle(cornerRadius: 20, style: .continuous)) 52 | .shadow(color: colorScheme == .dark ? .white.opacity(0.01) : .black.opacity(0.1), radius: 15, x: 0, y: 5) 53 | } 54 | } 55 | 56 | struct ArticleCardMain_Previews: PreviewProvider { 57 | static let store = BlogPostsStore() 58 | static let placeholder = BlogPost(title: "This is a placeholder", subtitle: "A subtitle for the placeholder", image: URL(string: "https://media.nature.com/lw800/magazine-assets/d41586-020-03053-2/d41586-020-03053-2_18533904.jpg"), blogpost: "Blog post") 59 | 60 | static var previews: some View { 61 | BlogPostCardMain(blogPost: placeholder) 62 | .environmentObject(store) 63 | 64 | BlogPostCardMain(blogPost: placeholder) 65 | .environmentObject(store) 66 | .previewDevice(PreviewDevice(rawValue: "iPhone 8")) 67 | .previewDisplayName("iPhone 8") 68 | .preferredColorScheme(.dark) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /BlogApp/BlogPostView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ArticleView.swift 3 | // BlogAppSwiftUI 4 | // 5 | // Created by Roman Luzgin on 23.07.21. 6 | // 7 | 8 | import SwiftUI 9 | import SDWebImageSwiftUI 10 | 11 | struct BlogPostView: View { 12 | 13 | var blogPost: BlogPost 14 | 15 | var body: some View { 16 | ZStack { 17 | ScrollView { 18 | VStack { 19 | WebImage(url: blogPost.image) 20 | .renderingMode(.original) 21 | .resizable() 22 | .aspectRatio(contentMode: .fill) 23 | .frame(height: 310) 24 | .frame(maxWidth: UIScreen.main.bounds.width) 25 | .clipped() 26 | 27 | VStack { 28 | HStack { 29 | Text(blogPost.title) 30 | .font(.title3) 31 | .fontWeight(.heavy) 32 | .foregroundColor(.primary) 33 | .lineLimit(3) 34 | .padding(.vertical, 15) 35 | Spacer() 36 | } 37 | .frame(maxWidth: .infinity) 38 | 39 | Text(blogPost.blogpost) 40 | .multilineTextAlignment(.leading) 41 | .font(.body) 42 | .foregroundColor(Color.primary.opacity(0.9)) 43 | .padding(.bottom, 25) 44 | .frame(maxWidth: .infinity) 45 | } 46 | .padding(.horizontal, 20) 47 | 48 | Spacer() 49 | } 50 | .frame(maxWidth: .infinity) 51 | 52 | } 53 | .navigationBarTitleDisplayMode(.inline) 54 | } 55 | } 56 | } 57 | 58 | struct BlogPostView_Previews: PreviewProvider { 59 | static let placeholder = BlogPost(title: "This is a placeholder", subtitle: "A subtitle for the placeholder", image: URL(string: "https://media.nature.com/lw800/magazine-assets/d41586-020-03053-2/d41586-020-03053-2_18533904.jpg"), blogpost: "Blog post") 60 | 61 | static var previews: some View { 62 | BlogPostView(blogPost: placeholder) 63 | 64 | BlogPostView(blogPost: placeholder) 65 | .previewDevice(PreviewDevice(rawValue: "iPhone 8")) 66 | .previewDisplayName("iPhone 8") 67 | .preferredColorScheme(.dark) 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /BlogApp/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // BlogApp 4 | // 5 | // Created by Roman Luzgin on 11.09.21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ContentView: View { 11 | @StateObject var store = BlogPostsStore() 12 | 13 | var body: some View { 14 | TabView { 15 | MainView() 16 | .environmentObject(store) 17 | .tabItem { 18 | Image(systemName: "house.fill") 19 | Text("Home") 20 | } 21 | AllPosts() 22 | .environmentObject(store) 23 | .tabItem { 24 | Image(systemName: "list.dash") 25 | Text("See all") 26 | } 27 | } 28 | } 29 | } 30 | 31 | struct ContentView_Previews: PreviewProvider { 32 | static var previews: some View { 33 | ContentView() 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /BlogApp/HexColorExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HexColorExtension.swift 3 | // BlogAppSwiftUI 4 | // 5 | // Created by Roman Luzgin on 26.07.21. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | extension Color { 12 | init(hex: String) { 13 | let hex = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted) 14 | var int: UInt64 = 0 15 | Scanner(string: hex).scanHexInt64(&int) 16 | let a, r, g, b: UInt64 17 | switch hex.count { 18 | case 3: // RGB (12-bit) 19 | (a, r, g, b) = (255, (int >> 8) * 17, (int >> 4 & 0xF) * 17, (int & 0xF) * 17) 20 | case 6: // RGB (24-bit) 21 | (a, r, g, b) = (255, int >> 16, int >> 8 & 0xFF, int & 0xFF) 22 | case 8: // ARGB (32-bit) 23 | (a, r, g, b) = (int >> 24, int >> 16 & 0xFF, int >> 8 & 0xFF, int & 0xFF) 24 | default: 25 | (a, r, g, b) = (1, 1, 1, 0) 26 | } 27 | 28 | self.init( 29 | .sRGB, 30 | red: Double(r) / 255, 31 | green: Double(g) / 255, 32 | blue: Double(b) / 255, 33 | opacity: Double(a) / 255 34 | ) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /BlogApp/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 | 28 | UIApplicationSupportsIndirectInputEvents 29 | 30 | UILaunchScreen 31 | 32 | UIRequiredDeviceCapabilities 33 | 34 | armv7 35 | 36 | UISupportedInterfaceOrientations 37 | 38 | UIInterfaceOrientationPortrait 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | UISupportedInterfaceOrientations~ipad 43 | 44 | UIInterfaceOrientationPortrait 45 | UIInterfaceOrientationPortraitUpsideDown 46 | UIInterfaceOrientationLandscapeLeft 47 | UIInterfaceOrientationLandscapeRight 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /BlogApp/MainView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MainView.swift 3 | // BlogAppSwiftUI 4 | // 5 | // Created by Roman Luzgin on 23.07.21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct MainView: View { 11 | @EnvironmentObject var store: BlogPostsStore 12 | @Environment(\.colorScheme) var colorScheme 13 | 14 | var featuredPosts: [BlogPost] { 15 | return store.blogPosts.filter {$0.featured == true} 16 | } 17 | 18 | var body: some View { 19 | 20 | NavigationView { 21 | ScrollView { 22 | // featured article 23 | if featuredPosts.count > 0 { 24 | VStack { 25 | HStack { 26 | Text("Featured posts") 27 | .font(.title.bold()) 28 | Spacer() 29 | } 30 | LazyVStack { 31 | ForEach(featuredPosts) {post in 32 | NavigationLink(destination: BlogPostView(blogPost: post)) { 33 | BlogPostCardMain(blogPost: post) 34 | } 35 | } 36 | } 37 | } 38 | .padding(.horizontal, 15) 39 | .padding(.vertical, 30) 40 | } 41 | // latest articles 42 | VStack { 43 | HStack { 44 | Text("Latest posts") 45 | .font(.title.bold()) 46 | Spacer() 47 | } 48 | .padding(.horizontal, 15) 49 | 50 | ScrollView(.horizontal, showsIndicators: false) { 51 | LazyHStack(spacing: 15) { 52 | if store.blogPosts.count >= 3 { 53 | ForEach(store.blogPosts[0...2]) {post in 54 | NavigationLink(destination: BlogPostView(blogPost: post)) { 55 | BlogPostCardMain(blogPost: post) 56 | } 57 | } 58 | 59 | } else { 60 | ForEach(store.blogPosts[0.. API keys on Contentful 15 | let spaceId = "youSpaceId" 16 | let accessToken = "yourAccessToken" 17 | 18 | let client = Client(spaceId: spaceId, accessToken: accessToken) 19 | 20 | func getArray(id: String, completion: @escaping([Entry]) -> ()) { 21 | let query = Query.where(contentTypeId: id) 22 | try! query.order(by: Ordering(sys: .createdAt, inReverse: true)) // ordering the list of articles by created date 23 | 24 | client.fetchArray(of: Entry.self, matching: query) { result in 25 | switch result { 26 | case .success(let array): 27 | DispatchQueue.main.async { 28 | completion(array.items) 29 | } 30 | case .failure(let error): 31 | print(error) 32 | } 33 | } 34 | } 35 | 36 | 37 | class BlogPostsStore: ObservableObject { 38 | @Published var blogPosts: [BlogPost] = articleList 39 | 40 | init() { 41 | DispatchQueue.main.async { 42 | self.refreshView() 43 | } 44 | } 45 | func refreshView(){ 46 | blogPosts = [] 47 | DispatchQueue.main.async { 48 | getArray(id: "swiftBlog") {items in 49 | items.forEach {(item) in 50 | self.blogPosts.append( 51 | BlogPost( 52 | title: item.fields["title"] as! String, 53 | subtitle: item.fields["subtitle"] as! String, 54 | image: item.fields.linkedAsset(at: "image")?.url ?? URL(string: ""), 55 | blogpost: item.fields["blogpost"] as? String ?? "", 56 | featured: item.fields["featured"] as? Bool ?? false 57 | ) 58 | ) 59 | } 60 | } 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /BlogApp/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BlogAppSwiftUI 2 | A simple blog app with Contentful integration for content management 3 | 4 | ![Frame 4-4](https://user-images.githubusercontent.com/65537510/132950127-229ca702-164d-424e-9215-10baae7792d5.png) 5 | 6 | ## Description 7 | This is a simple blog app built to demonstrate how to implement CMS with SwiftUI and Contentful. This method is accepted by Apple and can be used in a published app. 8 | 9 | #### Libraries 10 | - [Contentful](https://github.com/contentful/contentful.swift/) 11 | - [SdWebImageSwiftUI](https://github.com/SDWebImage/SDWebImageSwiftUI) 12 | 13 | #### Release target 14 | iOS 14+ 15 | 16 | ## Contact 17 | Twitter: [@shelkford](https://twitter.com/shelkford) 18 | 19 | Download my app Wellwork on [the App Store](https://apps.apple.com/us/app/wellwork-mindful-productivity/id1537640654) 20 | 21 | --------------------------------------------------------------------------------