├── .gitignore ├── ConcentricOnboardingExample ├── ConcentricOnboardingExample.xcodeproj │ └── project.pbxproj └── ConcentricOnboardingExample │ ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ ├── Contents.json │ ├── arrow.imageset │ │ ├── Contents.json │ │ └── arrow-right.pdf │ ├── screen 1.imageset │ │ ├── Contents.json │ │ └── grapes (7).png │ ├── screen 2.imageset │ │ ├── Contents.json │ │ └── lips.png │ ├── screen 3.imageset │ │ ├── Contents.json │ │ └── okay-svg (1).png │ └── screen 4.imageset │ │ ├── Contents.json │ │ └── dish.png │ ├── ConcentricOnboardingExampleApp.swift │ ├── ContentView.swift │ ├── MockData.swift │ ├── PageView.swift │ └── Preview Content │ └── Preview Assets.xcassets │ └── Contents.json ├── LICENSE ├── Package.swift ├── README.md └── Sources └── ConcentricOnboarding ├── ConcentricOnboarding.h ├── ConcentricOnboardingView.swift ├── Helpers ├── AnimatableShape.swift └── AnimationCompletion.swift └── Info.plist /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | # Created by https://www.gitignore.io/api/xcode,swift,carthage,cocoapods 4 | # Edit at https://www.gitignore.io/?templates=xcode,swift,carthage,cocoapods 5 | 6 | ### Carthage ### 7 | # Carthage 8 | # 9 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 10 | # Carthage/Checkouts 11 | 12 | Carthage/Build 13 | 14 | ### CocoaPods ### 15 | ## CocoaPods GitIgnore Template 16 | 17 | # CocoaPods - Only use to conserve bandwidth / Save time on Pushing 18 | # - Also handy if you have a large number of dependant pods 19 | # - AS PER https://guides.cocoapods.org/using/using-cocoapods.html NEVER IGNORE THE LOCK FILE 20 | Pods/ 21 | 22 | ### Swift ### 23 | # Xcode 24 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 25 | 26 | ## Build generated 27 | build/ 28 | DerivedData/ 29 | 30 | ## Various settings 31 | *.pbxuser 32 | !default.pbxuser 33 | *.mode1v3 34 | !default.mode1v3 35 | *.mode2v3 36 | !default.mode2v3 37 | *.perspectivev3 38 | !default.perspectivev3 39 | xcuserdata/ 40 | 41 | ## Other 42 | *.moved-aside 43 | *.xccheckout 44 | *.xcscmblueprint 45 | 46 | ## Obj-C/Swift specific 47 | *.hmap 48 | *.ipa 49 | *.dSYM.zip 50 | *.dSYM 51 | 52 | ## Playgrounds 53 | timeline.xctimeline 54 | playground.xcworkspace 55 | 56 | # Swift Package Manager 57 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 58 | # Packages/ 59 | # Package.pins 60 | # Package.resolved 61 | .build/ 62 | 63 | # CocoaPods 64 | # We recommend against adding the Pods directory to your .gitignore. However 65 | # you should judge for yourself, the pros and cons are mentioned at: 66 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 67 | # Pods/ 68 | # Add this line if you want to avoid checking in source code from the Xcode workspace 69 | # *.xcworkspace 70 | 71 | # Carthage 72 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 73 | # Carthage/Checkouts 74 | 75 | 76 | # Accio dependency management 77 | Dependencies/ 78 | .accio/ 79 | 80 | # fastlane 81 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 82 | # screenshots whenever they are needed. 83 | # For more information about the recommended setup visit: 84 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 85 | 86 | fastlane/report.xml 87 | fastlane/Preview.html 88 | fastlane/screenshots/**/*.png 89 | fastlane/test_output 90 | 91 | # Code Injection 92 | # After new code Injection tools there's a generated folder /iOSInjectionProject 93 | # https://github.com/johnno1962/injectionforxcode 94 | 95 | iOSInjectionProject/ 96 | 97 | ### Xcode ### 98 | # Xcode 99 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 100 | 101 | ## User settings 102 | 103 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 104 | 105 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 106 | 107 | ## Xcode Patch 108 | *.xcodeproj/* 109 | !*.xcodeproj/project.pbxproj 110 | !*.xcodeproj/xcshareddata/ 111 | !*.xcworkspace/contents.xcworkspacedata 112 | /*.gcno 113 | 114 | ### Xcode Patch ### 115 | **/xcshareddata/WorkspaceSettings.xcsettings 116 | 117 | # End of https://www.gitignore.io/api/xcode,swift,carthage,cocoapods 118 | 119 | Podfile.lock 120 | contents.xcworkspacedata 121 | -------------------------------------------------------------------------------- /ConcentricOnboardingExample/ConcentricOnboardingExample.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 77; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 5B24089C2D93EFE6002D6692 /* ConcentricOnboarding in Frameworks */ = {isa = PBXBuildFile; productRef = 5B24089B2D93EFE6002D6692 /* ConcentricOnboarding */; }; 11 | /* End PBXBuildFile section */ 12 | 13 | /* Begin PBXFileReference section */ 14 | 5B2408852D93EFB1002D6692 /* ConcentricOnboardingExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ConcentricOnboardingExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; 15 | /* End PBXFileReference section */ 16 | 17 | /* Begin PBXFileSystemSynchronizedRootGroup section */ 18 | 5B2408872D93EFB1002D6692 /* ConcentricOnboardingExample */ = { 19 | isa = PBXFileSystemSynchronizedRootGroup; 20 | path = ConcentricOnboardingExample; 21 | sourceTree = ""; 22 | }; 23 | /* End PBXFileSystemSynchronizedRootGroup section */ 24 | 25 | /* Begin PBXFrameworksBuildPhase section */ 26 | 5B2408822D93EFB1002D6692 /* Frameworks */ = { 27 | isa = PBXFrameworksBuildPhase; 28 | buildActionMask = 2147483647; 29 | files = ( 30 | 5B24089C2D93EFE6002D6692 /* ConcentricOnboarding in Frameworks */, 31 | ); 32 | runOnlyForDeploymentPostprocessing = 0; 33 | }; 34 | /* End PBXFrameworksBuildPhase section */ 35 | 36 | /* Begin PBXGroup section */ 37 | 5B24087C2D93EFB1002D6692 = { 38 | isa = PBXGroup; 39 | children = ( 40 | 5B2408872D93EFB1002D6692 /* ConcentricOnboardingExample */, 41 | 5B2408862D93EFB1002D6692 /* Products */, 42 | ); 43 | sourceTree = ""; 44 | }; 45 | 5B2408862D93EFB1002D6692 /* Products */ = { 46 | isa = PBXGroup; 47 | children = ( 48 | 5B2408852D93EFB1002D6692 /* ConcentricOnboardingExample.app */, 49 | ); 50 | name = Products; 51 | sourceTree = ""; 52 | }; 53 | /* End PBXGroup section */ 54 | 55 | /* Begin PBXNativeTarget section */ 56 | 5B2408842D93EFB1002D6692 /* ConcentricOnboardingExample */ = { 57 | isa = PBXNativeTarget; 58 | buildConfigurationList = 5B2408932D93EFB3002D6692 /* Build configuration list for PBXNativeTarget "ConcentricOnboardingExample" */; 59 | buildPhases = ( 60 | 5B2408812D93EFB1002D6692 /* Sources */, 61 | 5B2408822D93EFB1002D6692 /* Frameworks */, 62 | 5B2408832D93EFB1002D6692 /* Resources */, 63 | ); 64 | buildRules = ( 65 | ); 66 | dependencies = ( 67 | ); 68 | fileSystemSynchronizedGroups = ( 69 | 5B2408872D93EFB1002D6692 /* ConcentricOnboardingExample */, 70 | ); 71 | name = ConcentricOnboardingExample; 72 | packageProductDependencies = ( 73 | 5B24089B2D93EFE6002D6692 /* ConcentricOnboarding */, 74 | ); 75 | productName = ConcentricOnboardingExample; 76 | productReference = 5B2408852D93EFB1002D6692 /* ConcentricOnboardingExample.app */; 77 | productType = "com.apple.product-type.application"; 78 | }; 79 | /* End PBXNativeTarget section */ 80 | 81 | /* Begin PBXProject section */ 82 | 5B24087D2D93EFB1002D6692 /* Project object */ = { 83 | isa = PBXProject; 84 | attributes = { 85 | BuildIndependentTargetsInParallel = 1; 86 | LastSwiftUpdateCheck = 1620; 87 | LastUpgradeCheck = 1620; 88 | TargetAttributes = { 89 | 5B2408842D93EFB1002D6692 = { 90 | CreatedOnToolsVersion = 16.2; 91 | }; 92 | }; 93 | }; 94 | buildConfigurationList = 5B2408802D93EFB1002D6692 /* Build configuration list for PBXProject "ConcentricOnboardingExample" */; 95 | developmentRegion = en; 96 | hasScannedForEncodings = 0; 97 | knownRegions = ( 98 | en, 99 | Base, 100 | ); 101 | mainGroup = 5B24087C2D93EFB1002D6692; 102 | minimizedProjectReferenceProxies = 1; 103 | packageReferences = ( 104 | 5B24089A2D93EFE6002D6692 /* XCLocalSwiftPackageReference "../../ConcentricOnboarding" */, 105 | ); 106 | preferredProjectObjectVersion = 77; 107 | productRefGroup = 5B2408862D93EFB1002D6692 /* Products */; 108 | projectDirPath = ""; 109 | projectRoot = ""; 110 | targets = ( 111 | 5B2408842D93EFB1002D6692 /* ConcentricOnboardingExample */, 112 | ); 113 | }; 114 | /* End PBXProject section */ 115 | 116 | /* Begin PBXResourcesBuildPhase section */ 117 | 5B2408832D93EFB1002D6692 /* Resources */ = { 118 | isa = PBXResourcesBuildPhase; 119 | buildActionMask = 2147483647; 120 | files = ( 121 | ); 122 | runOnlyForDeploymentPostprocessing = 0; 123 | }; 124 | /* End PBXResourcesBuildPhase section */ 125 | 126 | /* Begin PBXSourcesBuildPhase section */ 127 | 5B2408812D93EFB1002D6692 /* Sources */ = { 128 | isa = PBXSourcesBuildPhase; 129 | buildActionMask = 2147483647; 130 | files = ( 131 | ); 132 | runOnlyForDeploymentPostprocessing = 0; 133 | }; 134 | /* End PBXSourcesBuildPhase section */ 135 | 136 | /* Begin XCBuildConfiguration section */ 137 | 5B2408912D93EFB3002D6692 /* Debug */ = { 138 | isa = XCBuildConfiguration; 139 | buildSettings = { 140 | ALWAYS_SEARCH_USER_PATHS = NO; 141 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 142 | CLANG_ANALYZER_NONNULL = YES; 143 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 144 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 145 | CLANG_ENABLE_MODULES = YES; 146 | CLANG_ENABLE_OBJC_ARC = YES; 147 | CLANG_ENABLE_OBJC_WEAK = YES; 148 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 149 | CLANG_WARN_BOOL_CONVERSION = YES; 150 | CLANG_WARN_COMMA = YES; 151 | CLANG_WARN_CONSTANT_CONVERSION = YES; 152 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 153 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 154 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 155 | CLANG_WARN_EMPTY_BODY = YES; 156 | CLANG_WARN_ENUM_CONVERSION = YES; 157 | CLANG_WARN_INFINITE_RECURSION = YES; 158 | CLANG_WARN_INT_CONVERSION = YES; 159 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 160 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 161 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 162 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 163 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 164 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 165 | CLANG_WARN_STRICT_PROTOTYPES = YES; 166 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 167 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 168 | CLANG_WARN_UNREACHABLE_CODE = YES; 169 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 170 | COPY_PHASE_STRIP = NO; 171 | DEBUG_INFORMATION_FORMAT = dwarf; 172 | ENABLE_STRICT_OBJC_MSGSEND = YES; 173 | ENABLE_TESTABILITY = YES; 174 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 175 | GCC_C_LANGUAGE_STANDARD = gnu17; 176 | GCC_DYNAMIC_NO_PIC = NO; 177 | GCC_NO_COMMON_BLOCKS = YES; 178 | GCC_OPTIMIZATION_LEVEL = 0; 179 | GCC_PREPROCESSOR_DEFINITIONS = ( 180 | "DEBUG=1", 181 | "$(inherited)", 182 | ); 183 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 184 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 185 | GCC_WARN_UNDECLARED_SELECTOR = YES; 186 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 187 | GCC_WARN_UNUSED_FUNCTION = YES; 188 | GCC_WARN_UNUSED_VARIABLE = YES; 189 | IPHONEOS_DEPLOYMENT_TARGET = 18.2; 190 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 191 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 192 | MTL_FAST_MATH = YES; 193 | ONLY_ACTIVE_ARCH = YES; 194 | SDKROOT = iphoneos; 195 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; 196 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 197 | SWIFT_VERSION = 6.0; 198 | }; 199 | name = Debug; 200 | }; 201 | 5B2408922D93EFB3002D6692 /* Release */ = { 202 | isa = XCBuildConfiguration; 203 | buildSettings = { 204 | ALWAYS_SEARCH_USER_PATHS = NO; 205 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 206 | CLANG_ANALYZER_NONNULL = YES; 207 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 208 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 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-with-dsym"; 236 | ENABLE_NS_ASSERTIONS = NO; 237 | ENABLE_STRICT_OBJC_MSGSEND = YES; 238 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 239 | GCC_C_LANGUAGE_STANDARD = gnu17; 240 | GCC_NO_COMMON_BLOCKS = YES; 241 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 242 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 243 | GCC_WARN_UNDECLARED_SELECTOR = YES; 244 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 245 | GCC_WARN_UNUSED_FUNCTION = YES; 246 | GCC_WARN_UNUSED_VARIABLE = YES; 247 | IPHONEOS_DEPLOYMENT_TARGET = 18.2; 248 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 249 | MTL_ENABLE_DEBUG_INFO = NO; 250 | MTL_FAST_MATH = YES; 251 | SDKROOT = iphoneos; 252 | SWIFT_COMPILATION_MODE = wholemodule; 253 | SWIFT_VERSION = 6.0; 254 | VALIDATE_PRODUCT = YES; 255 | }; 256 | name = Release; 257 | }; 258 | 5B2408942D93EFB3002D6692 /* Debug */ = { 259 | isa = XCBuildConfiguration; 260 | buildSettings = { 261 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 262 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 263 | CODE_SIGN_STYLE = Automatic; 264 | CURRENT_PROJECT_VERSION = 1; 265 | DEVELOPMENT_ASSET_PATHS = "\"ConcentricOnboardingExample/Preview Content\""; 266 | DEVELOPMENT_TEAM = FZXCM5CJ7P; 267 | ENABLE_PREVIEWS = YES; 268 | GENERATE_INFOPLIST_FILE = YES; 269 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 270 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 271 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 272 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 273 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 274 | LD_RUNPATH_SEARCH_PATHS = ( 275 | "$(inherited)", 276 | "@executable_path/Frameworks", 277 | ); 278 | MARKETING_VERSION = 1.0; 279 | PRODUCT_BUNDLE_IDENTIFIER = com.exyte.ConcentricOnboardingExample; 280 | PRODUCT_NAME = "$(TARGET_NAME)"; 281 | SWIFT_EMIT_LOC_STRINGS = YES; 282 | SWIFT_VERSION = 6.0; 283 | TARGETED_DEVICE_FAMILY = "1,2"; 284 | }; 285 | name = Debug; 286 | }; 287 | 5B2408952D93EFB3002D6692 /* Release */ = { 288 | isa = XCBuildConfiguration; 289 | buildSettings = { 290 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 291 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 292 | CODE_SIGN_STYLE = Automatic; 293 | CURRENT_PROJECT_VERSION = 1; 294 | DEVELOPMENT_ASSET_PATHS = "\"ConcentricOnboardingExample/Preview Content\""; 295 | DEVELOPMENT_TEAM = FZXCM5CJ7P; 296 | ENABLE_PREVIEWS = YES; 297 | GENERATE_INFOPLIST_FILE = YES; 298 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 299 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 300 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 301 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 302 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 303 | LD_RUNPATH_SEARCH_PATHS = ( 304 | "$(inherited)", 305 | "@executable_path/Frameworks", 306 | ); 307 | MARKETING_VERSION = 1.0; 308 | PRODUCT_BUNDLE_IDENTIFIER = com.exyte.ConcentricOnboardingExample; 309 | PRODUCT_NAME = "$(TARGET_NAME)"; 310 | SWIFT_EMIT_LOC_STRINGS = YES; 311 | SWIFT_VERSION = 6.0; 312 | TARGETED_DEVICE_FAMILY = "1,2"; 313 | }; 314 | name = Release; 315 | }; 316 | /* End XCBuildConfiguration section */ 317 | 318 | /* Begin XCConfigurationList section */ 319 | 5B2408802D93EFB1002D6692 /* Build configuration list for PBXProject "ConcentricOnboardingExample" */ = { 320 | isa = XCConfigurationList; 321 | buildConfigurations = ( 322 | 5B2408912D93EFB3002D6692 /* Debug */, 323 | 5B2408922D93EFB3002D6692 /* Release */, 324 | ); 325 | defaultConfigurationIsVisible = 0; 326 | defaultConfigurationName = Release; 327 | }; 328 | 5B2408932D93EFB3002D6692 /* Build configuration list for PBXNativeTarget "ConcentricOnboardingExample" */ = { 329 | isa = XCConfigurationList; 330 | buildConfigurations = ( 331 | 5B2408942D93EFB3002D6692 /* Debug */, 332 | 5B2408952D93EFB3002D6692 /* Release */, 333 | ); 334 | defaultConfigurationIsVisible = 0; 335 | defaultConfigurationName = Release; 336 | }; 337 | /* End XCConfigurationList section */ 338 | 339 | /* Begin XCLocalSwiftPackageReference section */ 340 | 5B24089A2D93EFE6002D6692 /* XCLocalSwiftPackageReference "../../ConcentricOnboarding" */ = { 341 | isa = XCLocalSwiftPackageReference; 342 | relativePath = ../../ConcentricOnboarding; 343 | }; 344 | /* End XCLocalSwiftPackageReference section */ 345 | 346 | /* Begin XCSwiftPackageProductDependency section */ 347 | 5B24089B2D93EFE6002D6692 /* ConcentricOnboarding */ = { 348 | isa = XCSwiftPackageProductDependency; 349 | productName = ConcentricOnboarding; 350 | }; 351 | /* End XCSwiftPackageProductDependency section */ 352 | }; 353 | rootObject = 5B24087D2D93EFB1002D6692 /* Project object */; 354 | } 355 | -------------------------------------------------------------------------------- /ConcentricOnboardingExample/ConcentricOnboardingExample/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 | -------------------------------------------------------------------------------- /ConcentricOnboardingExample/ConcentricOnboardingExample/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 | -------------------------------------------------------------------------------- /ConcentricOnboardingExample/ConcentricOnboardingExample/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /ConcentricOnboardingExample/ConcentricOnboardingExample/Assets.xcassets/arrow.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "arrow-right.pdf", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /ConcentricOnboardingExample/ConcentricOnboardingExample/Assets.xcassets/arrow.imageset/arrow-right.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/exyte/ConcentricOnboarding/5d692474b40ea4893a0f347a7d6e559d91049691/ConcentricOnboardingExample/ConcentricOnboardingExample/Assets.xcassets/arrow.imageset/arrow-right.pdf -------------------------------------------------------------------------------- /ConcentricOnboardingExample/ConcentricOnboardingExample/Assets.xcassets/screen 1.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "grapes (7).png", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /ConcentricOnboardingExample/ConcentricOnboardingExample/Assets.xcassets/screen 1.imageset/grapes (7).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/exyte/ConcentricOnboarding/5d692474b40ea4893a0f347a7d6e559d91049691/ConcentricOnboardingExample/ConcentricOnboardingExample/Assets.xcassets/screen 1.imageset/grapes (7).png -------------------------------------------------------------------------------- /ConcentricOnboardingExample/ConcentricOnboardingExample/Assets.xcassets/screen 2.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "lips.png", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /ConcentricOnboardingExample/ConcentricOnboardingExample/Assets.xcassets/screen 2.imageset/lips.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/exyte/ConcentricOnboarding/5d692474b40ea4893a0f347a7d6e559d91049691/ConcentricOnboardingExample/ConcentricOnboardingExample/Assets.xcassets/screen 2.imageset/lips.png -------------------------------------------------------------------------------- /ConcentricOnboardingExample/ConcentricOnboardingExample/Assets.xcassets/screen 3.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "okay-svg (1).png", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /ConcentricOnboardingExample/ConcentricOnboardingExample/Assets.xcassets/screen 3.imageset/okay-svg (1).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/exyte/ConcentricOnboarding/5d692474b40ea4893a0f347a7d6e559d91049691/ConcentricOnboardingExample/ConcentricOnboardingExample/Assets.xcassets/screen 3.imageset/okay-svg (1).png -------------------------------------------------------------------------------- /ConcentricOnboardingExample/ConcentricOnboardingExample/Assets.xcassets/screen 4.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "dish.png", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /ConcentricOnboardingExample/ConcentricOnboardingExample/Assets.xcassets/screen 4.imageset/dish.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/exyte/ConcentricOnboarding/5d692474b40ea4893a0f347a7d6e559d91049691/ConcentricOnboardingExample/ConcentricOnboardingExample/Assets.xcassets/screen 4.imageset/dish.png -------------------------------------------------------------------------------- /ConcentricOnboardingExample/ConcentricOnboardingExample/ConcentricOnboardingExampleApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConcentricOnboardingExampleApp.swift 3 | // ConcentricOnboardingExample 4 | // 5 | // Created by Alisa Mylnikova on 26.03.2025. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @main 11 | struct ConcentricOnboardingExampleApp: App { 12 | var body: some Scene { 13 | WindowGroup { 14 | ContentView() 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /ConcentricOnboardingExample/ConcentricOnboardingExample/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // ConcentricOnboardingExample 4 | // 5 | // Created by Daniil Manin on 20.09.2021. 6 | // 7 | 8 | import SwiftUI 9 | import ConcentricOnboarding 10 | 11 | struct ContentView: View { 12 | 13 | @State private var currentIndex: Int = 0 14 | 15 | var body: some View { 16 | ConcentricOnboardingView(pageContents: MockData.pages.map { (PageView(page: $0), $0.color) }) 17 | .duration(1.0) 18 | .nextIcon("chevron.forward") 19 | .animationDidEnd { 20 | print("Animation Did End") 21 | } 22 | } 23 | } 24 | 25 | struct ContentView_Previews: PreviewProvider { 26 | static var previews: some View { 27 | ContentView() 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /ConcentricOnboardingExample/ConcentricOnboardingExample/MockData.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockData.swift 3 | // ConcentricOnboardingExample 4 | // 5 | // Created by Daniil Manin on 20.09.2021. 6 | // 7 | 8 | import UIKit 9 | import SwiftUI 10 | 11 | struct PageData { 12 | let title: String 13 | let header: String 14 | let content: String 15 | let imageName: String 16 | let color: Color 17 | let textColor: Color 18 | } 19 | 20 | struct MockData { 21 | static let pages: [PageData] = [ 22 | PageData( 23 | title: "Eating grapes 101", 24 | header: "Step 1", 25 | content: "Break off a branch holding a few grapes and lay it on your plate.", 26 | imageName: "screen 1", 27 | color: Color(hex: "F38181"), 28 | textColor: Color(hex: "FFFFFF")), 29 | PageData( 30 | title: "Eating grapes 101", 31 | header: "Step 2", 32 | content: "Put a grape in your mouth whole.", 33 | imageName: "screen 2", 34 | color: Color(hex: "FCE38A"), 35 | textColor: Color(hex: "4A4A4A")), 36 | PageData( 37 | title: "Eating grapes 101", 38 | header: "Step 3", 39 | content: "Deposit the seeds into your thumb and first two fingers.", 40 | imageName: "screen 3", 41 | color: Color(hex: "95E1D3"), 42 | textColor: Color(hex: "4A4A4A")), 43 | PageData( 44 | title: "Eating grapes 101", 45 | header: "Step 4", 46 | content: "Place the seeds on your plate.", 47 | imageName: "screen 4", 48 | color: Color(hex: "EAFFD0"), 49 | textColor: Color(hex: "4A4A4A")), 50 | ] 51 | } 52 | 53 | /// Color converter from hex string to SwiftUI's Color 54 | extension Color { 55 | init(hex: String) { 56 | let scanner = Scanner(string: hex) 57 | var rgbValue: UInt64 = 0 58 | scanner.scanHexInt64(&rgbValue) 59 | 60 | let r = (rgbValue & 0xff0000) >> 16 61 | let g = (rgbValue & 0xff00) >> 8 62 | let b = rgbValue & 0xff 63 | 64 | self.init(red: Double(r) / 0xff, green: Double(g) / 0xff, blue: Double(b) / 0xff) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /ConcentricOnboardingExample/ConcentricOnboardingExample/PageView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PageView.swift 3 | // ConcentricOnboardingExample 4 | // 5 | // Created by Daniil Manin on 20.09.2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct PageView: View { 11 | 12 | let page: PageData 13 | let imageWidth: CGFloat = 150 14 | let textWidth: CGFloat = 350 15 | 16 | var body: some View { 17 | let size = UIImage(named: page.imageName)?.size ?? .zero 18 | let aspect = size.width / size.height 19 | 20 | return VStack(alignment: .center, spacing: 50) { 21 | Text(page.title) 22 | .font(.system(size: 40, weight: .bold, design: .rounded)) 23 | .foregroundColor(page.textColor) 24 | .frame(width: textWidth) 25 | .multilineTextAlignment(.center) 26 | Image(page.imageName) 27 | .resizable() 28 | .aspectRatio(aspect, contentMode: .fill) 29 | .frame(width: imageWidth, height: imageWidth) 30 | .cornerRadius(40) 31 | .clipped() 32 | VStack(alignment: .center, spacing: 5) { 33 | Text(page.header) 34 | .font(.system(size: 25, weight: .bold, design: .rounded)) 35 | .foregroundColor(page.textColor) 36 | .frame(width: 300, alignment: .center) 37 | .multilineTextAlignment(.center) 38 | Text(page.content) 39 | .font(Font.system(size: 18, weight: .bold, design: .rounded)) 40 | .foregroundColor(page.textColor) 41 | .frame(width: 300, alignment: .center) 42 | .multilineTextAlignment(.center) 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /ConcentricOnboardingExample/ConcentricOnboardingExample/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019 exyte 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.9 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "ConcentricOnboarding", 7 | platforms: [ 8 | .iOS(.v14) 9 | ], 10 | products: [ 11 | .library( 12 | name: "ConcentricOnboarding", 13 | targets: ["ConcentricOnboarding"]), 14 | ], 15 | dependencies: [], 16 | targets: [ 17 | .target( 18 | name: "ConcentricOnboarding", 19 | dependencies: [], 20 | swiftSettings: [ 21 | .enableExperimentalFeature("StrictConcurrency") 22 | ] 23 | ) 24 | ] 25 | ) 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 |       4 | 5 | 6 | 7 | 8 |

Concentric Onboarding

9 | 10 |

iOS library for a walkthrough or onboarding flow with tap actions written with SwiftUI

11 | 12 | ![](https://img.shields.io/github/v/tag/exyte/ConcentricOnboarding?label=Version) 13 | [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fexyte%2FConcentricOnboarding%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/exyte/ConcentricOnboarding) 14 | [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fexyte%2FConcentricOnboarding%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/exyte/ConcentricOnboarding) 15 | [![SPM](https://img.shields.io/badge/SPM-Compatible-brightgreen.svg)](https://swiftpackageindex.com/exyte/ConcentricOnboarding) 16 | [![Cocoapods](https://img.shields.io/badge/Cocoapods-Deprecated%20after%201.0.5-yellow.svg)](https://cocoapods.org/pods/ConcentricOnboarding) 17 | [![License: MIT](https://img.shields.io/badge/License-MIT-black.svg)](https://opensource.org/licenses/MIT) 18 | 19 | # Usage 20 | 1. Create `View`'s descendant class for your pages. 21 | 2. Create at least two pages and fill them with content. 22 | 3. Create an array of tuple - (page, background color). 23 | 4. Create ConcentricOnboardingView and place it in your view hierarchy. 24 | ```swift 25 | struct ContentView: View { 26 | var body: some View { 27 | return ConcentricOnboardingView(pageContents: [, ]) 28 | } 29 | } 30 | ``` 31 | 5. Pass duration as an argument if you want animation to be faster/slower 32 | ```swift 33 | ConcentricOnboardingView(pageContents: [, ]) 34 | .(duration: 2.0) 35 | ``` 36 | 37 | 6. Pass icon name as an argument if you want to change default icon on the button 38 | ```swift 39 | ConcentricOnboardingView(pageContents: [, ]) 40 | .(nextIcon: "chevron.forward") 41 | ``` 42 | 43 | ### Public interface 44 | `goToNextPage(animated: Bool = true)` - call this method manually if you need to 45 | `goToPreviousPage(animated: Bool = true)` - call this method manually if you need to 46 | 47 | ### Assignable closures 48 | 49 | `.animationWillBegin` - called before animation starts 50 | `.animationDidEnd` - called after animation ends 51 | `.didGoToLastPage` - called after animation leading to last page ends 52 | `.didChangeCurrentPage` - called after page changes 53 | `.insteadOfCyclingToFirstPage` - replaces default navigation to first page after pressing next on last page 54 | `.insteadOfCyclingToLastPage` - replaces default navigation to last page after pressing prev on first page while navigating backwards 55 | `.didPressNextButton` - replaces default button action with user's custom closure 56 | 57 | ## Examples 58 | 59 | To try the ConcentricOnboarding examples: 60 | - Clone the repo `https://github.com/exyte/ConcentricOnboarding.git` 61 | - Open `ConcentricOnboardingExample.xcodeproj` in the Xcode 62 | - Try it! 63 | 64 | ## Installation 65 | 66 | ### [Swift Package Manager](https://swift.org/package-manager/) 67 | 68 | ```swift 69 | dependencies: [ 70 | .package(url: "https://github.com/exyte/ConcentricOnboarding.git") 71 | ] 72 | ``` 73 | 74 | ## Requirements 75 | 76 | * iOS 14+ 77 | * Xcode 12+ 78 | 79 | ## Acknowledgements 80 | 81 | Many thanks to [Cuberto team](https://dribbble.com/shots/6654320-Animated-Onboarding-Screens) for the design idea and inspiration. 82 | 83 | ## Our other open source SwiftUI libraries 84 | [PopupView](https://github.com/exyte/PopupView) - Toasts and popups library 85 | [AnchoredPopup](https://github.com/exyte/AnchoredPopup) - Anchored Popup grows "out" of a trigger view (similar to Hero animation) 86 | [Grid](https://github.com/exyte/Grid) - The most powerful Grid container 87 | [ScalingHeaderScrollView](https://github.com/exyte/ScalingHeaderScrollView) - A scroll view with a sticky header which shrinks as you scroll 88 | [AnimatedTabBar](https://github.com/exyte/AnimatedTabBar) - A tabbar with a number of preset animations 89 | [MediaPicker](https://github.com/exyte/mediapicker) - Customizable media picker 90 | [Chat](https://github.com/exyte/chat) - Chat UI framework with fully customizable message cells, input view, and a built-in media picker 91 | [OpenAI](https://github.com/exyte/OpenAI) Wrapper lib for [OpenAI REST API](https://platform.openai.com/docs/api-reference/introduction) 92 | [AnimatedGradient](https://github.com/exyte/AnimatedGradient) - Animated linear gradient 93 | [FloatingButton](https://github.com/exyte/FloatingButton) - Floating button menu 94 | [ActivityIndicatorView](https://github.com/exyte/ActivityIndicatorView) - A number of animated loading indicators 95 | [ProgressIndicatorView](https://github.com/exyte/ProgressIndicatorView) - A number of animated progress indicators 96 | [FlagAndCountryCode](https://github.com/exyte/FlagAndCountryCode) - Phone codes and flags for every country 97 | [SVGView](https://github.com/exyte/SVGView) - SVG parser 98 | [LiquidSwipe](https://github.com/exyte/LiquidSwipe) - Liquid navigation animation 99 | 100 | -------------------------------------------------------------------------------- /Sources/ConcentricOnboarding/ConcentricOnboarding.h: -------------------------------------------------------------------------------- 1 | // 2 | // ConcentricOnboarding.h 3 | // ConcentricOnboarding 4 | // 5 | // Created by Alisa Mylnikova on 28/08/2019. 6 | // Copyright © 2019 Exyte. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for ConcentricOnboarding. 12 | FOUNDATION_EXPORT double ConcentricOnboardingVersionNumber; 13 | 14 | //! Project version string for ConcentricOnboarding. 15 | FOUNDATION_EXPORT const unsigned char ConcentricOnboardingVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | 20 | -------------------------------------------------------------------------------- /Sources/ConcentricOnboarding/ConcentricOnboardingView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // ConcentricOnboarding 4 | // 5 | // Created by Alisa Mylnikova on 30/07/2019. 6 | // Copyright © 2019 Exyte. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | enum Direction { 12 | case forward, backward 13 | } 14 | 15 | public struct ConcentricOnboardingView: View, Animatable where Content: View { 16 | 17 | public typealias PageContent = (view: Content, background: Color) 18 | 19 | let pageContents: [PageContent] 20 | 21 | @State private var currentIndex: Int = 0 22 | @State private var nextIndex: Int = 1 23 | @State private var progress: Double = 0 24 | @State private var direction: Direction = .forward 25 | @State private var isAnimated: Bool = false 26 | @State private var circleColor: Color = .clear 27 | @State private var backgroundColor: Color = .clear 28 | 29 | /// defaults setups, will be change via modifiers 30 | private var nextIcon: String = "chevron.forward" 31 | private var duration: Double = 1.0 32 | 33 | /// called before animation starts 34 | private var animationWillBegin: () -> Void = { } 35 | 36 | /// called after animation ends 37 | private var animationDidEnd: () -> Void = { } 38 | 39 | /// called after animation leading to last page ends 40 | private var didGoToLastPage: () -> Void = { } 41 | 42 | /// replaces default navigation to first page after pressing next on last page 43 | private var insteadOfCyclingToFirstPage: (() -> Void)? 44 | 45 | /// replaces default navigation to last page after pressing prev on first page while navigating backwards 46 | private var insteadOfCyclingToLastPage: (() -> Void)? 47 | 48 | /// replaces default button action with user's custom closure 49 | private var didPressNextButton: (() -> Void)? 50 | 51 | /// replaces default button action with user's custom closure 52 | private var didChangeCurrentPage: ((Int) -> Void)? 53 | 54 | /// animation's settings 55 | private let radius: Double = 30 56 | private let limit: Double = 15 57 | private var inAnimation: Animation { .easeIn(duration: duration / 2) } 58 | private var outAnimation: Animation { .easeOut(duration: duration / 2) } 59 | private var fullAnimation: Animation { .easeInOut(duration: duration) } 60 | 61 | public init(pageContents: [PageContent]) { 62 | self.pageContents = pageContents 63 | 64 | if pageContents.indices.contains(0) { 65 | _backgroundColor = State(initialValue: pageContents[0].background) 66 | } 67 | 68 | if pageContents.indices.contains(1) { 69 | _circleColor = State(initialValue: pageContents[1].background) 70 | } 71 | 72 | if pageContents.count < 2 { 73 | print("Warning: Add more pages.") 74 | } 75 | } 76 | 77 | public var body: some View { 78 | mainContent 79 | .edgesIgnoringSafeArea(.vertical) 80 | .onChange(of: currentIndex) { _ in 81 | currentPageChanged() 82 | } 83 | .onAnimationCompleted(for: progress) { 84 | animationCompleted() 85 | } 86 | } 87 | 88 | // MARK: - Private 89 | 90 | private var mainContent: some View { 91 | ZStack { 92 | backgroundColor 93 | currentPages 94 | button 95 | } 96 | } 97 | 98 | private var shape: some View { 99 | AnimatableShape(progress: progress, radius: radius, limit: limit, direction: direction) 100 | .foregroundColor(circleColor) 101 | } 102 | 103 | private var button: some View { 104 | ZStack { 105 | shape 106 | Button(action: tapAction) { 107 | ZStack { 108 | Circle() 109 | .foregroundColor(isAnimated ? .clear : circleColor) 110 | .frame(width: 2 * radius, height: 2 * radius) 111 | nextImage 112 | } 113 | } 114 | .disabled(isAnimated) 115 | } 116 | .offset(y: 300) 117 | } 118 | 119 | private var nextImage: some View { 120 | Image(systemName: nextIcon) 121 | .resizable() 122 | .aspectRatio(contentMode: .fit) 123 | .frame(width: 10, height: 20) 124 | .foregroundColor(backgroundColor) 125 | } 126 | 127 | private var currentPages: some View { 128 | let maxXOffset: CGFloat = UIScreen.main.bounds.width 129 | let maxYOffset: CGFloat = 40.0 130 | let coeff: CGFloat = direction == .forward ? -1 : 3 131 | 132 | return ZStack { 133 | if pageContents.count > 0 { 134 | pageContents[currentIndex].view 135 | .scaleEffect(isAnimated ? 2 / 3 : 1) 136 | .offset(x: isAnimated ? coeff * maxXOffset : 0, 137 | y: isAnimated ? maxYOffset : 0) 138 | .animation(isAnimated ? fullAnimation : .none) 139 | } 140 | 141 | if pageContents.count > 1 { 142 | pageContents[nextIndex].view 143 | .scaleEffect(isAnimated ? 1 : 2 / 3) 144 | .offset(x: isAnimated ? 0 : -coeff * maxXOffset, 145 | y: isAnimated ? 0 : maxYOffset) 146 | .animation(isAnimated ? fullAnimation : .none) 147 | } 148 | } 149 | } 150 | 151 | public func goToNextPage(animated: Bool = true) { 152 | if let block = insteadOfCyclingToFirstPage, currentIndex == pageContents.count - 1 { 153 | block() 154 | } else { 155 | animated ? goToNextPageAnimated() : goToNextPageUnanimated() 156 | } 157 | } 158 | 159 | public func goToPreviousPage(animated: Bool = true) { 160 | if let block = insteadOfCyclingToLastPage, currentIndex == 0 { 161 | block() 162 | } else { 163 | animated ? goToPrevPageAnimated() : goToPrevPageUnanimated() 164 | } 165 | } 166 | 167 | // MARK: - 168 | 169 | private func tapAction() { 170 | if let block = didPressNextButton { 171 | block() 172 | } else { 173 | goToNextPage(animated: true) 174 | } 175 | } 176 | 177 | private func currentPageChanged() { 178 | didChangeCurrentPage?(currentIndex) 179 | if currentIndex == pageContents.count - 1 { 180 | didGoToLastPage() 181 | } 182 | } 183 | 184 | private func animationCompleted() { 185 | if progress == limit { 186 | progress += 0.001 187 | withAnimation(outAnimation) { progress = 2 * limit } 188 | updateColors(forNextPage: true) 189 | } else if progress == 2 * limit { 190 | direction == .forward ? goToNextPageUnanimated() : goToPrevPageUnanimated() 191 | animationDidEnd() 192 | } 193 | } 194 | 195 | // MARK: - Next / Prev actions 196 | 197 | private func goToNextPageAnimated() { 198 | direction = .forward 199 | nextIndex = moveIndexForward(currentIndex) 200 | startAnimation() 201 | } 202 | 203 | private func goToNextPageUnanimated() { 204 | isAnimated = false 205 | direction = .forward 206 | currentIndex = moveIndexForward(currentIndex) 207 | nextIndex = moveIndexForward(currentIndex) 208 | progress = 0 209 | } 210 | 211 | private func goToPrevPageAnimated() { 212 | direction = .backward 213 | nextIndex = moveIndexBackward(currentIndex) 214 | startAnimation() 215 | } 216 | 217 | private func goToPrevPageUnanimated() { 218 | isAnimated = false 219 | direction = .backward 220 | currentIndex = moveIndexBackward(currentIndex) 221 | nextIndex = moveIndexBackward(currentIndex) 222 | progress = 0 223 | } 224 | 225 | private func startAnimation() { 226 | animationWillBegin() 227 | isAnimated = true 228 | updateColors() 229 | progress = 0 230 | withAnimation(inAnimation) { progress = limit } 231 | } 232 | 233 | private func updateColors(forNextPage: Bool = false) { 234 | backgroundColor = pageContents[forNextPage ? nextIndex : currentIndex].background 235 | circleColor = pageContents[forNextPage ? currentIndex : nextIndex].background 236 | } 237 | 238 | // MARK: - Helpers 239 | 240 | private func moveIndexForward(_ index: Int) -> Int { 241 | index + 1 < pageContents.count ? index + 1 : 0 242 | } 243 | 244 | private func moveIndexBackward(_ index: Int) -> Int { 245 | index - 1 >= 0 ? index - 1 : pageContents.count - 1 246 | } 247 | } 248 | 249 | // MARK: - 250 | 251 | extension ConcentricOnboardingView { 252 | 253 | public func duration(_ timeInterval: Double) -> ConcentricOnboardingView { 254 | var concentricOnboardingView = self 255 | concentricOnboardingView.duration = timeInterval 256 | return concentricOnboardingView 257 | } 258 | 259 | public func nextIcon(_ iconName: String) -> ConcentricOnboardingView { 260 | var concentricOnboardingView = self 261 | concentricOnboardingView.nextIcon = iconName 262 | return concentricOnboardingView 263 | } 264 | } 265 | 266 | // MARK: - Closures 267 | 268 | extension ConcentricOnboardingView { 269 | 270 | public func animationWillBegin(perform: @escaping () -> Void) -> ConcentricOnboardingView { 271 | var concentricOnboardingView = self 272 | concentricOnboardingView.animationWillBegin = perform 273 | return concentricOnboardingView 274 | } 275 | 276 | public func animationDidEnd(perform: @escaping () -> Void) -> ConcentricOnboardingView { 277 | var concentricOnboardingView = self 278 | concentricOnboardingView.animationDidEnd = perform 279 | return concentricOnboardingView 280 | } 281 | 282 | public func didGoToLastPage(perform: @escaping () -> Void) -> ConcentricOnboardingView { 283 | var concentricOnboardingView = self 284 | concentricOnboardingView.didGoToLastPage = perform 285 | return concentricOnboardingView 286 | } 287 | 288 | // MARK: - Optional methods 289 | 290 | public func insteadOfCyclingToFirstPage(perform: @escaping () -> Void) -> ConcentricOnboardingView { 291 | var concentricOnboardingView = self 292 | concentricOnboardingView.insteadOfCyclingToFirstPage = perform 293 | return concentricOnboardingView 294 | } 295 | 296 | public func insteadOfCyclingToLastPage(perform: @escaping () -> Void) -> ConcentricOnboardingView { 297 | var concentricOnboardingView = self 298 | concentricOnboardingView.insteadOfCyclingToLastPage = perform 299 | return concentricOnboardingView 300 | } 301 | 302 | public func didPressNextButton(perform: @escaping () -> Void) -> ConcentricOnboardingView { 303 | var concentricOnboardingView = self 304 | concentricOnboardingView.didPressNextButton = perform 305 | return concentricOnboardingView 306 | } 307 | 308 | public func didChangeCurrentPage(perform: @escaping (Int) -> Void) -> ConcentricOnboardingView { 309 | var concentricOnboardingView = self 310 | concentricOnboardingView.didChangeCurrentPage = perform 311 | return concentricOnboardingView 312 | } 313 | } 314 | -------------------------------------------------------------------------------- /Sources/ConcentricOnboarding/Helpers/AnimatableShape.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AnimatableShape.swift 3 | // ConcentricOnboarding 4 | // 5 | // Created by Daniil Manin on 21.09.2021. 6 | // Copyright © 2021 Exyte. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct AnimatableShape: Shape { 12 | 13 | enum AnimationType { 14 | case growing, shrinking 15 | } 16 | 17 | var progress: Double 18 | let radius: Double 19 | let limit: Double 20 | let direction: Direction 21 | 22 | var animatableData: CGFloat { 23 | get { CGFloat(progress) } 24 | set { progress = Double(newValue) } 25 | } 26 | 27 | // MARK: - Path 28 | 29 | func path(in rect: CGRect) -> Path { 30 | let (type, progress) = localValues() 31 | let r: CGFloat 32 | let delta: CGFloat 33 | let center: CGPoint 34 | 35 | if type == .growing { 36 | r = CGFloat(radius + pow(2, progress)) 37 | delta = CGFloat((1 - progress / limit) * radius) 38 | center = CGPoint(x: UIScreen.main.bounds.width / 2 + r - delta - 2.0, y: rect.height / 2) 39 | } else { 40 | r = CGFloat(radius + pow(2, (limit - progress))) 41 | delta = CGFloat((progress / limit) * radius) 42 | center = CGPoint(x: UIScreen.main.bounds.width / 2 - r + delta, y: rect.height / 2) 43 | } 44 | 45 | let rect = CGRect(x: center.x - r, y: center.y - r, width: 2 * r, height: 2 * r) 46 | return Circle().path(in: rect) 47 | } 48 | 49 | // MARK: - Private 50 | 51 | private func localValues() -> (type: AnimationType, progress: Double) { 52 | if direction == .forward { 53 | if progress <= limit { 54 | return (.growing, progress) 55 | } else if progress <= 2 * limit { 56 | return (.shrinking, progress - limit) 57 | } else { 58 | return (.growing, 0) 59 | } 60 | } else { 61 | if progress <= limit { 62 | return (.shrinking, limit - progress) 63 | } else if progress <= 2 * limit { 64 | return (.growing, 2 * limit - progress) 65 | } else { 66 | return (.shrinking, 0) 67 | } 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Sources/ConcentricOnboarding/Helpers/AnimationCompletion.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConcentricOnboardingView+Modifiers.swift 3 | // ConcentricOnboarding 4 | // 5 | // Created by Daniil Manin on 20.09.2021. 6 | // Copyright © 2021 Exyte. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct AnimationCompletionObserverModifier: AnimatableModifier where Value: VectorArithmetic { 12 | 13 | var animatableData: Value { 14 | didSet { notifyCompletion() } 15 | } 16 | 17 | private var targetValue: Value 18 | private var completion: () -> Void = { } 19 | 20 | init(observedValue: Value, completion: @escaping () -> Void = { }) { 21 | self.completion = completion 22 | self.animatableData = observedValue 23 | targetValue = observedValue 24 | } 25 | 26 | func body(content: Content) -> some View { 27 | return content 28 | } 29 | 30 | // MARK: - Private 31 | 32 | private func notifyCompletion() { 33 | if animatableData == targetValue { 34 | DispatchQueue.main.async { completion() } 35 | } 36 | } 37 | } 38 | 39 | 40 | extension View { 41 | 42 | func onAnimationCompleted(for value: Value, completion: @escaping () -> Void) -> ModifiedContent> { 43 | return modifier(AnimationCompletionObserverModifier(observedValue: value, completion: completion)) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Sources/ConcentricOnboarding/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 | $(CURRENT_PROJECT_VERSION) 21 | 22 | 23 | --------------------------------------------------------------------------------