├── .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 | 
13 | [](https://swiftpackageindex.com/exyte/ConcentricOnboarding)
14 | [](https://swiftpackageindex.com/exyte/ConcentricOnboarding)
15 | [](https://swiftpackageindex.com/exyte/ConcentricOnboarding)
16 | [](https://cocoapods.org/pods/ConcentricOnboarding)
17 | [](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 |
--------------------------------------------------------------------------------