├── InstagramStoryTutorial.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcuserdata │ └── jmb.xcuserdatad │ ├── xcdebugger │ └── Breakpoints_v2.xcbkptlist │ └── xcschemes │ └── xcschememanagement.plist ├── InstagramStoryTutorial ├── AppDelegate.swift ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ └── Contents.json │ ├── Contents.json │ ├── image01.imageset │ │ ├── Contents.json │ │ └── image01.jpg │ ├── image02.imageset │ │ ├── Contents.json │ │ └── image02.jpg │ ├── image03.imageset │ │ ├── Contents.json │ │ └── image03.jpg │ ├── image04.imageset │ │ ├── Contents.json │ │ └── image04.jpg │ ├── image05.imageset │ │ ├── Contents.json │ │ └── image05.jpg │ ├── image06.imageset │ │ ├── Contents.json │ │ └── image06.jpg │ └── image07.imageset │ │ ├── Contents.json │ │ └── image07.jpg ├── Base.lproj │ └── LaunchScreen.storyboard ├── ContentView.swift ├── Info.plist ├── LoadingRectangle.swift ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json ├── SceneDelegate.swift └── StoryTimer.swift ├── InstagramStoryTutorialTests ├── Info.plist └── InstagramStoryTutorialTests.swift ├── README.md └── images └── InstagramStoryTutorial_SwiftUI.gif /InstagramStoryTutorial.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 8C36E75624462ED000FF2C19 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C36E75524462ED000FF2C19 /* AppDelegate.swift */; }; 11 | 8C36E75824462ED000FF2C19 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C36E75724462ED000FF2C19 /* SceneDelegate.swift */; }; 12 | 8C36E75A24462ED000FF2C19 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C36E75924462ED000FF2C19 /* ContentView.swift */; }; 13 | 8C36E75C24462ED300FF2C19 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8C36E75B24462ED300FF2C19 /* Assets.xcassets */; }; 14 | 8C36E75F24462ED300FF2C19 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8C36E75E24462ED300FF2C19 /* Preview Assets.xcassets */; }; 15 | 8C36E76224462ED300FF2C19 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 8C36E76024462ED300FF2C19 /* LaunchScreen.storyboard */; }; 16 | 8C36E76D24462ED300FF2C19 /* InstagramStoryTutorialTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C36E76C24462ED300FF2C19 /* InstagramStoryTutorialTests.swift */; }; 17 | 8C36E7782446366A00FF2C19 /* StoryTimer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C36E7772446366A00FF2C19 /* StoryTimer.swift */; }; 18 | 8C36E77A24463EF200FF2C19 /* LoadingRectangle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C36E77924463EF200FF2C19 /* LoadingRectangle.swift */; }; 19 | /* End PBXBuildFile section */ 20 | 21 | /* Begin PBXContainerItemProxy section */ 22 | 8C36E76924462ED300FF2C19 /* PBXContainerItemProxy */ = { 23 | isa = PBXContainerItemProxy; 24 | containerPortal = 8C36E74A24462ED000FF2C19 /* Project object */; 25 | proxyType = 1; 26 | remoteGlobalIDString = 8C36E75124462ED000FF2C19; 27 | remoteInfo = InstagramStoryTutorial; 28 | }; 29 | /* End PBXContainerItemProxy section */ 30 | 31 | /* Begin PBXFileReference section */ 32 | 8C36E75224462ED000FF2C19 /* InstagramStoryTutorial.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = InstagramStoryTutorial.app; sourceTree = BUILT_PRODUCTS_DIR; }; 33 | 8C36E75524462ED000FF2C19 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 34 | 8C36E75724462ED000FF2C19 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 35 | 8C36E75924462ED000FF2C19 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 36 | 8C36E75B24462ED300FF2C19 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 37 | 8C36E75E24462ED300FF2C19 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 38 | 8C36E76124462ED300FF2C19 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 39 | 8C36E76324462ED300FF2C19 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 40 | 8C36E76824462ED300FF2C19 /* InstagramStoryTutorialTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = InstagramStoryTutorialTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 41 | 8C36E76C24462ED300FF2C19 /* InstagramStoryTutorialTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstagramStoryTutorialTests.swift; sourceTree = ""; }; 42 | 8C36E76E24462ED300FF2C19 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 43 | 8C36E7772446366A00FF2C19 /* StoryTimer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoryTimer.swift; sourceTree = ""; }; 44 | 8C36E77924463EF200FF2C19 /* LoadingRectangle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingRectangle.swift; sourceTree = ""; }; 45 | /* End PBXFileReference section */ 46 | 47 | /* Begin PBXFrameworksBuildPhase section */ 48 | 8C36E74F24462ED000FF2C19 /* Frameworks */ = { 49 | isa = PBXFrameworksBuildPhase; 50 | buildActionMask = 2147483647; 51 | files = ( 52 | ); 53 | runOnlyForDeploymentPostprocessing = 0; 54 | }; 55 | 8C36E76524462ED300FF2C19 /* Frameworks */ = { 56 | isa = PBXFrameworksBuildPhase; 57 | buildActionMask = 2147483647; 58 | files = ( 59 | ); 60 | runOnlyForDeploymentPostprocessing = 0; 61 | }; 62 | /* End PBXFrameworksBuildPhase section */ 63 | 64 | /* Begin PBXGroup section */ 65 | 8C36E74924462ED000FF2C19 = { 66 | isa = PBXGroup; 67 | children = ( 68 | 8C36E75424462ED000FF2C19 /* InstagramStoryTutorial */, 69 | 8C36E76B24462ED300FF2C19 /* InstagramStoryTutorialTests */, 70 | 8C36E75324462ED000FF2C19 /* Products */, 71 | ); 72 | sourceTree = ""; 73 | }; 74 | 8C36E75324462ED000FF2C19 /* Products */ = { 75 | isa = PBXGroup; 76 | children = ( 77 | 8C36E75224462ED000FF2C19 /* InstagramStoryTutorial.app */, 78 | 8C36E76824462ED300FF2C19 /* InstagramStoryTutorialTests.xctest */, 79 | ); 80 | name = Products; 81 | sourceTree = ""; 82 | }; 83 | 8C36E75424462ED000FF2C19 /* InstagramStoryTutorial */ = { 84 | isa = PBXGroup; 85 | children = ( 86 | 8C36E75524462ED000FF2C19 /* AppDelegate.swift */, 87 | 8C36E75724462ED000FF2C19 /* SceneDelegate.swift */, 88 | 8C36E75924462ED000FF2C19 /* ContentView.swift */, 89 | 8C36E75B24462ED300FF2C19 /* Assets.xcassets */, 90 | 8C36E76024462ED300FF2C19 /* LaunchScreen.storyboard */, 91 | 8C36E76324462ED300FF2C19 /* Info.plist */, 92 | 8C36E75D24462ED300FF2C19 /* Preview Content */, 93 | 8C36E7772446366A00FF2C19 /* StoryTimer.swift */, 94 | 8C36E77924463EF200FF2C19 /* LoadingRectangle.swift */, 95 | ); 96 | path = InstagramStoryTutorial; 97 | sourceTree = ""; 98 | }; 99 | 8C36E75D24462ED300FF2C19 /* Preview Content */ = { 100 | isa = PBXGroup; 101 | children = ( 102 | 8C36E75E24462ED300FF2C19 /* Preview Assets.xcassets */, 103 | ); 104 | path = "Preview Content"; 105 | sourceTree = ""; 106 | }; 107 | 8C36E76B24462ED300FF2C19 /* InstagramStoryTutorialTests */ = { 108 | isa = PBXGroup; 109 | children = ( 110 | 8C36E76C24462ED300FF2C19 /* InstagramStoryTutorialTests.swift */, 111 | 8C36E76E24462ED300FF2C19 /* Info.plist */, 112 | ); 113 | path = InstagramStoryTutorialTests; 114 | sourceTree = ""; 115 | }; 116 | /* End PBXGroup section */ 117 | 118 | /* Begin PBXNativeTarget section */ 119 | 8C36E75124462ED000FF2C19 /* InstagramStoryTutorial */ = { 120 | isa = PBXNativeTarget; 121 | buildConfigurationList = 8C36E77124462ED300FF2C19 /* Build configuration list for PBXNativeTarget "InstagramStoryTutorial" */; 122 | buildPhases = ( 123 | 8C36E74E24462ED000FF2C19 /* Sources */, 124 | 8C36E74F24462ED000FF2C19 /* Frameworks */, 125 | 8C36E75024462ED000FF2C19 /* Resources */, 126 | ); 127 | buildRules = ( 128 | ); 129 | dependencies = ( 130 | ); 131 | name = InstagramStoryTutorial; 132 | productName = InstagramStoryTutorial; 133 | productReference = 8C36E75224462ED000FF2C19 /* InstagramStoryTutorial.app */; 134 | productType = "com.apple.product-type.application"; 135 | }; 136 | 8C36E76724462ED300FF2C19 /* InstagramStoryTutorialTests */ = { 137 | isa = PBXNativeTarget; 138 | buildConfigurationList = 8C36E77424462ED300FF2C19 /* Build configuration list for PBXNativeTarget "InstagramStoryTutorialTests" */; 139 | buildPhases = ( 140 | 8C36E76424462ED300FF2C19 /* Sources */, 141 | 8C36E76524462ED300FF2C19 /* Frameworks */, 142 | 8C36E76624462ED300FF2C19 /* Resources */, 143 | ); 144 | buildRules = ( 145 | ); 146 | dependencies = ( 147 | 8C36E76A24462ED300FF2C19 /* PBXTargetDependency */, 148 | ); 149 | name = InstagramStoryTutorialTests; 150 | productName = InstagramStoryTutorialTests; 151 | productReference = 8C36E76824462ED300FF2C19 /* InstagramStoryTutorialTests.xctest */; 152 | productType = "com.apple.product-type.bundle.unit-test"; 153 | }; 154 | /* End PBXNativeTarget section */ 155 | 156 | /* Begin PBXProject section */ 157 | 8C36E74A24462ED000FF2C19 /* Project object */ = { 158 | isa = PBXProject; 159 | attributes = { 160 | LastSwiftUpdateCheck = 1120; 161 | LastUpgradeCheck = 1120; 162 | ORGANIZATIONNAME = TrailingClosure; 163 | TargetAttributes = { 164 | 8C36E75124462ED000FF2C19 = { 165 | CreatedOnToolsVersion = 11.2; 166 | }; 167 | 8C36E76724462ED300FF2C19 = { 168 | CreatedOnToolsVersion = 11.2; 169 | TestTargetID = 8C36E75124462ED000FF2C19; 170 | }; 171 | }; 172 | }; 173 | buildConfigurationList = 8C36E74D24462ED000FF2C19 /* Build configuration list for PBXProject "InstagramStoryTutorial" */; 174 | compatibilityVersion = "Xcode 9.3"; 175 | developmentRegion = en; 176 | hasScannedForEncodings = 0; 177 | knownRegions = ( 178 | en, 179 | Base, 180 | ); 181 | mainGroup = 8C36E74924462ED000FF2C19; 182 | productRefGroup = 8C36E75324462ED000FF2C19 /* Products */; 183 | projectDirPath = ""; 184 | projectRoot = ""; 185 | targets = ( 186 | 8C36E75124462ED000FF2C19 /* InstagramStoryTutorial */, 187 | 8C36E76724462ED300FF2C19 /* InstagramStoryTutorialTests */, 188 | ); 189 | }; 190 | /* End PBXProject section */ 191 | 192 | /* Begin PBXResourcesBuildPhase section */ 193 | 8C36E75024462ED000FF2C19 /* Resources */ = { 194 | isa = PBXResourcesBuildPhase; 195 | buildActionMask = 2147483647; 196 | files = ( 197 | 8C36E76224462ED300FF2C19 /* LaunchScreen.storyboard in Resources */, 198 | 8C36E75F24462ED300FF2C19 /* Preview Assets.xcassets in Resources */, 199 | 8C36E75C24462ED300FF2C19 /* Assets.xcassets in Resources */, 200 | ); 201 | runOnlyForDeploymentPostprocessing = 0; 202 | }; 203 | 8C36E76624462ED300FF2C19 /* Resources */ = { 204 | isa = PBXResourcesBuildPhase; 205 | buildActionMask = 2147483647; 206 | files = ( 207 | ); 208 | runOnlyForDeploymentPostprocessing = 0; 209 | }; 210 | /* End PBXResourcesBuildPhase section */ 211 | 212 | /* Begin PBXSourcesBuildPhase section */ 213 | 8C36E74E24462ED000FF2C19 /* Sources */ = { 214 | isa = PBXSourcesBuildPhase; 215 | buildActionMask = 2147483647; 216 | files = ( 217 | 8C36E77A24463EF200FF2C19 /* LoadingRectangle.swift in Sources */, 218 | 8C36E75624462ED000FF2C19 /* AppDelegate.swift in Sources */, 219 | 8C36E7782446366A00FF2C19 /* StoryTimer.swift in Sources */, 220 | 8C36E75824462ED000FF2C19 /* SceneDelegate.swift in Sources */, 221 | 8C36E75A24462ED000FF2C19 /* ContentView.swift in Sources */, 222 | ); 223 | runOnlyForDeploymentPostprocessing = 0; 224 | }; 225 | 8C36E76424462ED300FF2C19 /* Sources */ = { 226 | isa = PBXSourcesBuildPhase; 227 | buildActionMask = 2147483647; 228 | files = ( 229 | 8C36E76D24462ED300FF2C19 /* InstagramStoryTutorialTests.swift in Sources */, 230 | ); 231 | runOnlyForDeploymentPostprocessing = 0; 232 | }; 233 | /* End PBXSourcesBuildPhase section */ 234 | 235 | /* Begin PBXTargetDependency section */ 236 | 8C36E76A24462ED300FF2C19 /* PBXTargetDependency */ = { 237 | isa = PBXTargetDependency; 238 | target = 8C36E75124462ED000FF2C19 /* InstagramStoryTutorial */; 239 | targetProxy = 8C36E76924462ED300FF2C19 /* PBXContainerItemProxy */; 240 | }; 241 | /* End PBXTargetDependency section */ 242 | 243 | /* Begin PBXVariantGroup section */ 244 | 8C36E76024462ED300FF2C19 /* LaunchScreen.storyboard */ = { 245 | isa = PBXVariantGroup; 246 | children = ( 247 | 8C36E76124462ED300FF2C19 /* Base */, 248 | ); 249 | name = LaunchScreen.storyboard; 250 | sourceTree = ""; 251 | }; 252 | /* End PBXVariantGroup section */ 253 | 254 | /* Begin XCBuildConfiguration section */ 255 | 8C36E76F24462ED300FF2C19 /* Debug */ = { 256 | isa = XCBuildConfiguration; 257 | buildSettings = { 258 | ALWAYS_SEARCH_USER_PATHS = NO; 259 | CLANG_ANALYZER_NONNULL = YES; 260 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 261 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 262 | CLANG_CXX_LIBRARY = "libc++"; 263 | CLANG_ENABLE_MODULES = YES; 264 | CLANG_ENABLE_OBJC_ARC = YES; 265 | CLANG_ENABLE_OBJC_WEAK = YES; 266 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 267 | CLANG_WARN_BOOL_CONVERSION = YES; 268 | CLANG_WARN_COMMA = YES; 269 | CLANG_WARN_CONSTANT_CONVERSION = YES; 270 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 271 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 272 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 273 | CLANG_WARN_EMPTY_BODY = YES; 274 | CLANG_WARN_ENUM_CONVERSION = YES; 275 | CLANG_WARN_INFINITE_RECURSION = YES; 276 | CLANG_WARN_INT_CONVERSION = YES; 277 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 278 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 279 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 280 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 281 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 282 | CLANG_WARN_STRICT_PROTOTYPES = YES; 283 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 284 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 285 | CLANG_WARN_UNREACHABLE_CODE = YES; 286 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 287 | COPY_PHASE_STRIP = NO; 288 | DEBUG_INFORMATION_FORMAT = dwarf; 289 | ENABLE_STRICT_OBJC_MSGSEND = YES; 290 | ENABLE_TESTABILITY = YES; 291 | GCC_C_LANGUAGE_STANDARD = gnu11; 292 | GCC_DYNAMIC_NO_PIC = NO; 293 | GCC_NO_COMMON_BLOCKS = YES; 294 | GCC_OPTIMIZATION_LEVEL = 0; 295 | GCC_PREPROCESSOR_DEFINITIONS = ( 296 | "DEBUG=1", 297 | "$(inherited)", 298 | ); 299 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 300 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 301 | GCC_WARN_UNDECLARED_SELECTOR = YES; 302 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 303 | GCC_WARN_UNUSED_FUNCTION = YES; 304 | GCC_WARN_UNUSED_VARIABLE = YES; 305 | IPHONEOS_DEPLOYMENT_TARGET = 13.2; 306 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 307 | MTL_FAST_MATH = YES; 308 | ONLY_ACTIVE_ARCH = YES; 309 | SDKROOT = iphoneos; 310 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 311 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 312 | }; 313 | name = Debug; 314 | }; 315 | 8C36E77024462ED300FF2C19 /* Release */ = { 316 | isa = XCBuildConfiguration; 317 | buildSettings = { 318 | ALWAYS_SEARCH_USER_PATHS = NO; 319 | CLANG_ANALYZER_NONNULL = YES; 320 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 321 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 322 | CLANG_CXX_LIBRARY = "libc++"; 323 | CLANG_ENABLE_MODULES = YES; 324 | CLANG_ENABLE_OBJC_ARC = YES; 325 | CLANG_ENABLE_OBJC_WEAK = YES; 326 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 327 | CLANG_WARN_BOOL_CONVERSION = YES; 328 | CLANG_WARN_COMMA = YES; 329 | CLANG_WARN_CONSTANT_CONVERSION = YES; 330 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 331 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 332 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 333 | CLANG_WARN_EMPTY_BODY = YES; 334 | CLANG_WARN_ENUM_CONVERSION = YES; 335 | CLANG_WARN_INFINITE_RECURSION = YES; 336 | CLANG_WARN_INT_CONVERSION = YES; 337 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 338 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 339 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 340 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 341 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 342 | CLANG_WARN_STRICT_PROTOTYPES = YES; 343 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 344 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 345 | CLANG_WARN_UNREACHABLE_CODE = YES; 346 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 347 | COPY_PHASE_STRIP = NO; 348 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 349 | ENABLE_NS_ASSERTIONS = NO; 350 | ENABLE_STRICT_OBJC_MSGSEND = YES; 351 | GCC_C_LANGUAGE_STANDARD = gnu11; 352 | GCC_NO_COMMON_BLOCKS = YES; 353 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 354 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 355 | GCC_WARN_UNDECLARED_SELECTOR = YES; 356 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 357 | GCC_WARN_UNUSED_FUNCTION = YES; 358 | GCC_WARN_UNUSED_VARIABLE = YES; 359 | IPHONEOS_DEPLOYMENT_TARGET = 13.2; 360 | MTL_ENABLE_DEBUG_INFO = NO; 361 | MTL_FAST_MATH = YES; 362 | SDKROOT = iphoneos; 363 | SWIFT_COMPILATION_MODE = wholemodule; 364 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 365 | VALIDATE_PRODUCT = YES; 366 | }; 367 | name = Release; 368 | }; 369 | 8C36E77224462ED300FF2C19 /* Debug */ = { 370 | isa = XCBuildConfiguration; 371 | buildSettings = { 372 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 373 | CODE_SIGN_STYLE = Automatic; 374 | DEVELOPMENT_ASSET_PATHS = "\"InstagramStoryTutorial/Preview Content\""; 375 | DEVELOPMENT_TEAM = W3U8G84792; 376 | ENABLE_PREVIEWS = YES; 377 | INFOPLIST_FILE = InstagramStoryTutorial/Info.plist; 378 | LD_RUNPATH_SEARCH_PATHS = ( 379 | "$(inherited)", 380 | "@executable_path/Frameworks", 381 | ); 382 | PRODUCT_BUNDLE_IDENTIFIER = com.trailingclosure.InstagramStoryTutorial; 383 | PRODUCT_NAME = "$(TARGET_NAME)"; 384 | SWIFT_VERSION = 5.0; 385 | TARGETED_DEVICE_FAMILY = "1,2"; 386 | }; 387 | name = Debug; 388 | }; 389 | 8C36E77324462ED300FF2C19 /* Release */ = { 390 | isa = XCBuildConfiguration; 391 | buildSettings = { 392 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 393 | CODE_SIGN_STYLE = Automatic; 394 | DEVELOPMENT_ASSET_PATHS = "\"InstagramStoryTutorial/Preview Content\""; 395 | DEVELOPMENT_TEAM = W3U8G84792; 396 | ENABLE_PREVIEWS = YES; 397 | INFOPLIST_FILE = InstagramStoryTutorial/Info.plist; 398 | LD_RUNPATH_SEARCH_PATHS = ( 399 | "$(inherited)", 400 | "@executable_path/Frameworks", 401 | ); 402 | PRODUCT_BUNDLE_IDENTIFIER = com.trailingclosure.InstagramStoryTutorial; 403 | PRODUCT_NAME = "$(TARGET_NAME)"; 404 | SWIFT_VERSION = 5.0; 405 | TARGETED_DEVICE_FAMILY = "1,2"; 406 | }; 407 | name = Release; 408 | }; 409 | 8C36E77524462ED300FF2C19 /* Debug */ = { 410 | isa = XCBuildConfiguration; 411 | buildSettings = { 412 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 413 | BUNDLE_LOADER = "$(TEST_HOST)"; 414 | CODE_SIGN_STYLE = Automatic; 415 | DEVELOPMENT_TEAM = W3U8G84792; 416 | INFOPLIST_FILE = InstagramStoryTutorialTests/Info.plist; 417 | IPHONEOS_DEPLOYMENT_TARGET = 13.2; 418 | LD_RUNPATH_SEARCH_PATHS = ( 419 | "$(inherited)", 420 | "@executable_path/Frameworks", 421 | "@loader_path/Frameworks", 422 | ); 423 | PRODUCT_BUNDLE_IDENTIFIER = com.trailingclosure.InstagramStoryTutorialTests; 424 | PRODUCT_NAME = "$(TARGET_NAME)"; 425 | SWIFT_VERSION = 5.0; 426 | TARGETED_DEVICE_FAMILY = "1,2"; 427 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/InstagramStoryTutorial.app/InstagramStoryTutorial"; 428 | }; 429 | name = Debug; 430 | }; 431 | 8C36E77624462ED300FF2C19 /* Release */ = { 432 | isa = XCBuildConfiguration; 433 | buildSettings = { 434 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 435 | BUNDLE_LOADER = "$(TEST_HOST)"; 436 | CODE_SIGN_STYLE = Automatic; 437 | DEVELOPMENT_TEAM = W3U8G84792; 438 | INFOPLIST_FILE = InstagramStoryTutorialTests/Info.plist; 439 | IPHONEOS_DEPLOYMENT_TARGET = 13.2; 440 | LD_RUNPATH_SEARCH_PATHS = ( 441 | "$(inherited)", 442 | "@executable_path/Frameworks", 443 | "@loader_path/Frameworks", 444 | ); 445 | PRODUCT_BUNDLE_IDENTIFIER = com.trailingclosure.InstagramStoryTutorialTests; 446 | PRODUCT_NAME = "$(TARGET_NAME)"; 447 | SWIFT_VERSION = 5.0; 448 | TARGETED_DEVICE_FAMILY = "1,2"; 449 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/InstagramStoryTutorial.app/InstagramStoryTutorial"; 450 | }; 451 | name = Release; 452 | }; 453 | /* End XCBuildConfiguration section */ 454 | 455 | /* Begin XCConfigurationList section */ 456 | 8C36E74D24462ED000FF2C19 /* Build configuration list for PBXProject "InstagramStoryTutorial" */ = { 457 | isa = XCConfigurationList; 458 | buildConfigurations = ( 459 | 8C36E76F24462ED300FF2C19 /* Debug */, 460 | 8C36E77024462ED300FF2C19 /* Release */, 461 | ); 462 | defaultConfigurationIsVisible = 0; 463 | defaultConfigurationName = Release; 464 | }; 465 | 8C36E77124462ED300FF2C19 /* Build configuration list for PBXNativeTarget "InstagramStoryTutorial" */ = { 466 | isa = XCConfigurationList; 467 | buildConfigurations = ( 468 | 8C36E77224462ED300FF2C19 /* Debug */, 469 | 8C36E77324462ED300FF2C19 /* Release */, 470 | ); 471 | defaultConfigurationIsVisible = 0; 472 | defaultConfigurationName = Release; 473 | }; 474 | 8C36E77424462ED300FF2C19 /* Build configuration list for PBXNativeTarget "InstagramStoryTutorialTests" */ = { 475 | isa = XCConfigurationList; 476 | buildConfigurations = ( 477 | 8C36E77524462ED300FF2C19 /* Debug */, 478 | 8C36E77624462ED300FF2C19 /* Release */, 479 | ); 480 | defaultConfigurationIsVisible = 0; 481 | defaultConfigurationName = Release; 482 | }; 483 | /* End XCConfigurationList section */ 484 | }; 485 | rootObject = 8C36E74A24462ED000FF2C19 /* Project object */; 486 | } 487 | -------------------------------------------------------------------------------- /InstagramStoryTutorial.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /InstagramStoryTutorial.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /InstagramStoryTutorial.xcodeproj/xcuserdata/jmb.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | -------------------------------------------------------------------------------- /InstagramStoryTutorial.xcodeproj/xcuserdata/jmb.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | InstagramStoryTutorial.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /InstagramStoryTutorial/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // InstagramStoryTutorial 4 | // 5 | // Created by Jean-Marc Boullianne on 4/14/20. 6 | // Copyright © 2020 TrailingClosure. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | 15 | 16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 17 | // Override point for customization after application launch. 18 | return true 19 | } 20 | 21 | // MARK: UISceneSession Lifecycle 22 | 23 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { 24 | // Called when a new scene session is being created. 25 | // Use this method to select a configuration to create the new scene with. 26 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 27 | } 28 | 29 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { 30 | // Called when the user discards a scene session. 31 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. 32 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return. 33 | } 34 | 35 | 36 | } 37 | 38 | -------------------------------------------------------------------------------- /InstagramStoryTutorial/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /InstagramStoryTutorial/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /InstagramStoryTutorial/Assets.xcassets/image01.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "image01.jpg", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /InstagramStoryTutorial/Assets.xcassets/image01.imageset/image01.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jboullianne/InstagramStoryTutorial-SwiftUI/3c493ae8d0e5ca91f3f1b24844167f788fa3ae17/InstagramStoryTutorial/Assets.xcassets/image01.imageset/image01.jpg -------------------------------------------------------------------------------- /InstagramStoryTutorial/Assets.xcassets/image02.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "image02.jpg", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /InstagramStoryTutorial/Assets.xcassets/image02.imageset/image02.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jboullianne/InstagramStoryTutorial-SwiftUI/3c493ae8d0e5ca91f3f1b24844167f788fa3ae17/InstagramStoryTutorial/Assets.xcassets/image02.imageset/image02.jpg -------------------------------------------------------------------------------- /InstagramStoryTutorial/Assets.xcassets/image03.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "image03.jpg", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /InstagramStoryTutorial/Assets.xcassets/image03.imageset/image03.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jboullianne/InstagramStoryTutorial-SwiftUI/3c493ae8d0e5ca91f3f1b24844167f788fa3ae17/InstagramStoryTutorial/Assets.xcassets/image03.imageset/image03.jpg -------------------------------------------------------------------------------- /InstagramStoryTutorial/Assets.xcassets/image04.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "image04.jpg", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /InstagramStoryTutorial/Assets.xcassets/image04.imageset/image04.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jboullianne/InstagramStoryTutorial-SwiftUI/3c493ae8d0e5ca91f3f1b24844167f788fa3ae17/InstagramStoryTutorial/Assets.xcassets/image04.imageset/image04.jpg -------------------------------------------------------------------------------- /InstagramStoryTutorial/Assets.xcassets/image05.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "image05.jpg", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /InstagramStoryTutorial/Assets.xcassets/image05.imageset/image05.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jboullianne/InstagramStoryTutorial-SwiftUI/3c493ae8d0e5ca91f3f1b24844167f788fa3ae17/InstagramStoryTutorial/Assets.xcassets/image05.imageset/image05.jpg -------------------------------------------------------------------------------- /InstagramStoryTutorial/Assets.xcassets/image06.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "image06.jpg", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /InstagramStoryTutorial/Assets.xcassets/image06.imageset/image06.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jboullianne/InstagramStoryTutorial-SwiftUI/3c493ae8d0e5ca91f3f1b24844167f788fa3ae17/InstagramStoryTutorial/Assets.xcassets/image06.imageset/image06.jpg -------------------------------------------------------------------------------- /InstagramStoryTutorial/Assets.xcassets/image07.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "image07.jpg", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /InstagramStoryTutorial/Assets.xcassets/image07.imageset/image07.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jboullianne/InstagramStoryTutorial-SwiftUI/3c493ae8d0e5ca91f3f1b24844167f788fa3ae17/InstagramStoryTutorial/Assets.xcassets/image07.imageset/image07.jpg -------------------------------------------------------------------------------- /InstagramStoryTutorial/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 | -------------------------------------------------------------------------------- /InstagramStoryTutorial/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // InstagramStoryTutorial 4 | // 5 | // Created by Jean-Marc Boullianne on 4/14/20. 6 | // Copyright © 2020 TrailingClosure. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct ContentView: View { 12 | 13 | var imageNames:[String] = ["image01","image02","image03","image04","image05","image06","image07"] 14 | @ObservedObject var storyTimer: StoryTimer = StoryTimer(items: 7, interval: 1.0) 15 | 16 | var body: some View { 17 | GeometryReader { geometry in 18 | ZStack(alignment: .top) { 19 | Image(self.imageNames[Int(self.storyTimer.progress)]) 20 | .resizable() 21 | .edgesIgnoringSafeArea(.all) 22 | .scaledToFill() 23 | .frame(width: geometry.size.width, height: nil, alignment: .center) 24 | .animation(.none) 25 | HStack(alignment: .center, spacing: 4) { 26 | ForEach(self.imageNames.indices) { x in 27 | LoadingRectangle(progress: min( max( (CGFloat(self.storyTimer.progress) - CGFloat(x)), 0.0) , 1.0) ) 28 | .frame(width: nil, height: 4, alignment: .leading) 29 | .animation(.linear) 30 | } 31 | }.padding() 32 | HStack(alignment: .center, spacing: 0) { 33 | Rectangle() 34 | .foregroundColor(.clear) 35 | .contentShape(Rectangle()) 36 | .onTapGesture { 37 | self.storyTimer.advance(by: -1) 38 | } 39 | Rectangle() 40 | .foregroundColor(.clear) 41 | .contentShape(Rectangle()) 42 | .onTapGesture { 43 | self.storyTimer.advance(by: 1) 44 | } 45 | } 46 | } 47 | .onAppear { self.storyTimer.start() } 48 | .onDisappear {self.storyTimer.cancel() } 49 | 50 | } 51 | } 52 | } 53 | 54 | struct ContentView_Previews: PreviewProvider { 55 | static var previews: some View { 56 | ContentView() 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /InstagramStoryTutorial/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 | UIStatusBarStyle 47 | UIStatusBarStyleDarkContent 48 | UISupportedInterfaceOrientations 49 | 50 | UIInterfaceOrientationPortrait 51 | UIInterfaceOrientationLandscapeLeft 52 | UIInterfaceOrientationLandscapeRight 53 | 54 | UISupportedInterfaceOrientations~ipad 55 | 56 | UIInterfaceOrientationPortrait 57 | UIInterfaceOrientationPortraitUpsideDown 58 | UIInterfaceOrientationLandscapeLeft 59 | UIInterfaceOrientationLandscapeRight 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /InstagramStoryTutorial/LoadingRectangle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoadingRectangle.swift 3 | // InstagramStoryTutorial 4 | // 5 | // Created by Jean-Marc Boullianne on 4/14/20. 6 | // Copyright © 2020 TrailingClosure. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct LoadingRectangle: View { 12 | 13 | var progress:CGFloat 14 | 15 | var body: some View { 16 | GeometryReader { geometry in 17 | ZStack(alignment: .leading) { 18 | Rectangle() 19 | .foregroundColor(Color.white.opacity(0.3)) 20 | .cornerRadius(5) 21 | Rectangle() 22 | .frame(width: geometry.size.width * self.progress, height: nil, alignment: .leading) 23 | .foregroundColor(Color.white.opacity(0.9)) 24 | .cornerRadius(5) 25 | } 26 | } 27 | } 28 | } 29 | 30 | struct LoadingRectangle_Previews: PreviewProvider { 31 | static var previews: some View { 32 | LoadingRectangle(progress: 0.7) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /InstagramStoryTutorial/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /InstagramStoryTutorial/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // InstagramStoryTutorial 4 | // 5 | // Created by Jean-Marc Boullianne on 4/14/20. 6 | // Copyright © 2020 TrailingClosure. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import SwiftUI 11 | 12 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | 17 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 18 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. 19 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. 20 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). 21 | 22 | // Create the SwiftUI view that provides the window contents. 23 | let contentView = ContentView() 24 | 25 | // Use a UIHostingController as window root view controller. 26 | if let windowScene = scene as? UIWindowScene { 27 | let window = UIWindow(windowScene: windowScene) 28 | window.rootViewController = UIHostingController(rootView: contentView) 29 | self.window = window 30 | window.makeKeyAndVisible() 31 | } 32 | } 33 | 34 | func sceneDidDisconnect(_ scene: UIScene) { 35 | // Called as the scene is being released by the system. 36 | // This occurs shortly after the scene enters the background, or when its session is discarded. 37 | // Release any resources associated with this scene that can be re-created the next time the scene connects. 38 | // The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead). 39 | } 40 | 41 | func sceneDidBecomeActive(_ scene: UIScene) { 42 | // Called when the scene has moved from an inactive state to an active state. 43 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. 44 | } 45 | 46 | func sceneWillResignActive(_ scene: UIScene) { 47 | // Called when the scene will move from an active state to an inactive state. 48 | // This may occur due to temporary interruptions (ex. an incoming phone call). 49 | } 50 | 51 | func sceneWillEnterForeground(_ scene: UIScene) { 52 | // Called as the scene transitions from the background to the foreground. 53 | // Use this method to undo the changes made on entering the background. 54 | } 55 | 56 | func sceneDidEnterBackground(_ scene: UIScene) { 57 | // Called as the scene transitions from the foreground to the background. 58 | // Use this method to save data, release shared resources, and store enough scene-specific state information 59 | // to restore the scene back to its current state. 60 | } 61 | 62 | 63 | } 64 | 65 | -------------------------------------------------------------------------------- /InstagramStoryTutorial/StoryTimer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StoryTimer.swift 3 | // InstagramStoryTutorial 4 | // 5 | // Created by Jean-Marc Boullianne on 4/14/20. 6 | // Copyright © 2020 TrailingClosure. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Combine 11 | 12 | class StoryTimer: ObservableObject { 13 | 14 | @Published var progress: Double 15 | private var interval: TimeInterval 16 | private var max: Int 17 | private let publisher: Timer.TimerPublisher 18 | private var cancellable: Cancellable? 19 | 20 | 21 | init(items: Int, interval: TimeInterval) { 22 | self.max = items 23 | self.progress = 0 24 | self.interval = interval 25 | self.publisher = Timer.publish(every: 0.1, on: .main, in: .default) 26 | } 27 | 28 | func start() { 29 | self.cancellable = self.publisher.autoconnect().sink(receiveValue: { _ in 30 | var newProgress = self.progress + (0.1 / self.interval) 31 | if Int(newProgress) >= self.max { newProgress = 0 } 32 | self.progress = newProgress 33 | }) 34 | } 35 | 36 | func cancel() { 37 | self.cancellable?.cancel() 38 | } 39 | 40 | func advance(by number: Int) { 41 | let newProgress = max((Int(self.progress) + number) % self.max , 0) 42 | self.progress = Double(newProgress) 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /InstagramStoryTutorialTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /InstagramStoryTutorialTests/InstagramStoryTutorialTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InstagramStoryTutorialTests.swift 3 | // InstagramStoryTutorialTests 4 | // 5 | // Created by Jean-Marc Boullianne on 4/14/20. 6 | // Copyright © 2020 TrailingClosure. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import InstagramStoryTutorial 11 | 12 | class InstagramStoryTutorialTests: XCTestCase { 13 | 14 | override func setUp() { 15 | // Put setup code here. This method is called before the invocation of each test method in the class. 16 | } 17 | 18 | override func tearDown() { 19 | // Put teardown code here. This method is called after the invocation of each test method in the class. 20 | } 21 | 22 | func testExample() { 23 | // This is an example of a functional test case. 24 | // Use XCTAssert and related functions to verify your tests produce the correct results. 25 | } 26 | 27 | func testPerformanceExample() { 28 | // This is an example of a performance test case. 29 | self.measure { 30 | // Put the code you want to measure the time of here. 31 | } 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Instagram Story Tutorial - Using SwiftUI 2 | 3 | Written for [TrailingClosure.com](https://trailingclosure.com/swiftui-instagram-story-tutorial/). 4 | 5 | > Recreate the popular Instagram Scrolling Photos view using SwiftUI. This tutorial walks you through step-by-step in creating each individual piece of the UI. 6 | 7 | ![example](images/InstagramStoryTutorial_SwiftUI.gif) 8 | -------------------------------------------------------------------------------- /images/InstagramStoryTutorial_SwiftUI.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jboullianne/InstagramStoryTutorial-SwiftUI/3c493ae8d0e5ca91f3f1b24844167f788fa3ae17/images/InstagramStoryTutorial_SwiftUI.gif --------------------------------------------------------------------------------