├── Classes
├── ios
│ └── .gitkeep
├── osx
│ └── .gitkeep
├── UIView+Recursion.h
├── UIView+Recursion.m
├── TutorialKitView.h
├── TutorialKit.h
├── TutorialKit.m
└── TutorialKitView.m
├── Example
├── Podfile
├── TutorialKitExample
│ ├── en.lproj
│ │ └── InfoPlist.strings
│ ├── ExampleViewController.h
│ ├── AppDelegate.h
│ ├── main.m
│ ├── TutorialKitExample-Prefix.pch
│ ├── Images.xcassets
│ │ ├── AppIcon.appiconset
│ │ │ └── Contents.json
│ │ └── LaunchImage.launchimage
│ │ │ └── Contents.json
│ ├── TutorialKitExample-Info.plist
│ ├── AppDelegate.m
│ └── ExampleViewController.m
├── TutorialKitExampleTests
│ ├── en.lproj
│ │ └── InfoPlist.strings
│ ├── TutorialKitExampleTests-Info.plist
│ └── TutorialKitExampleTests.m
├── TutorialKitExample.xcworkspace
│ └── contents.xcworkspacedata
└── TutorialKitExample.xcodeproj
│ ├── project.xcworkspace
│ └── contents.xcworkspacedata
│ └── project.pbxproj
├── CHANGELOG.md
├── Assets
└── tutorialkit.gif
├── .gitignore
├── TutorialKit.podspec
├── LICENSE
├── README.md
└── Rakefile
/Classes/ios/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/Classes/osx/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/Example/Podfile:
--------------------------------------------------------------------------------
1 | pod "TutorialKit", :path => "../"
2 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # TutorialKit CHANGELOG
2 |
3 | ## 0.1.0
4 |
5 | Initial release.
6 |
--------------------------------------------------------------------------------
/Assets/tutorialkit.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lostinthepines/TutorialKit/HEAD/Assets/tutorialkit.gif
--------------------------------------------------------------------------------
/Example/TutorialKitExample/en.lproj/InfoPlist.strings:
--------------------------------------------------------------------------------
1 | /* Localized versions of Info.plist keys */
2 |
3 |
--------------------------------------------------------------------------------
/Example/TutorialKitExampleTests/en.lproj/InfoPlist.strings:
--------------------------------------------------------------------------------
1 | /* Localized versions of Info.plist keys */
2 |
3 |
--------------------------------------------------------------------------------
/Example/TutorialKitExample.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/Example/TutorialKitExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Example/TutorialKitExample/ExampleViewController.h:
--------------------------------------------------------------------------------
1 | //
2 | // ExampleViewController.h
3 | // TutorialKitExample
4 | //
5 | // Created by Alex on 4/30/14.
6 | // Copyright (c) 2014 TutorialKit. All rights reserved.
7 | //
8 |
9 | #import
10 |
11 | @interface ExampleViewController : UIViewController
12 |
13 | @end
14 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # OS X
2 | .DS_Store
3 |
4 | # Xcode
5 | build/
6 | *.pbxuser
7 | !default.pbxuser
8 | *.mode1v3
9 | !default.mode1v3
10 | *.mode2v3
11 | !default.mode2v3
12 | *.perspectivev3
13 | !default.perspectivev3
14 | xcuserdata
15 | *.xccheckout
16 | profile
17 | *.moved-aside
18 | DerivedData
19 | *.hmap
20 | *.ipa
21 |
22 | Example/Pods
23 |
--------------------------------------------------------------------------------
/Example/TutorialKitExample/AppDelegate.h:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.h
3 | // TutorialKitExample
4 | //
5 | // Created by Alex on 4/30/14.
6 | // Copyright (c) 2014 TutorialKit. All rights reserved.
7 | //
8 |
9 | #import
10 |
11 | @interface AppDelegate : UIResponder
12 |
13 | @property (strong, nonatomic) UIWindow *window;
14 |
15 | @end
16 |
--------------------------------------------------------------------------------
/Example/TutorialKitExample/main.m:
--------------------------------------------------------------------------------
1 | //
2 | // main.m
3 | // TutorialKitExample
4 | //
5 | // Created by Alex on 4/30/14.
6 | // Copyright (c) 2014 TutorialKit. All rights reserved.
7 | //
8 |
9 | #import
10 |
11 | #import "AppDelegate.h"
12 |
13 | int main(int argc, char * argv[])
14 | {
15 | @autoreleasepool {
16 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/Example/TutorialKitExample/TutorialKitExample-Prefix.pch:
--------------------------------------------------------------------------------
1 | //
2 | // Prefix header
3 | //
4 | // The contents of this file are implicitly included at the beginning of every source file.
5 | //
6 |
7 | #import
8 |
9 | #ifndef __IPHONE_3_0
10 | #warning "This project uses features only available in iOS SDK 3.0 and later."
11 | #endif
12 |
13 | #ifdef __OBJC__
14 | #import
15 | #import
16 | #endif
17 |
--------------------------------------------------------------------------------
/Example/TutorialKitExampleTests/TutorialKitExampleTests-Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | ${EXECUTABLE_NAME}
9 | CFBundleIdentifier
10 | com.daniel.${PRODUCT_NAME:rfc1034identifier}
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundlePackageType
14 | BNDL
15 | CFBundleShortVersionString
16 | 1.0
17 | CFBundleSignature
18 | ????
19 | CFBundleVersion
20 | 1
21 |
22 |
23 |
--------------------------------------------------------------------------------
/Example/TutorialKitExampleTests/TutorialKitExampleTests.m:
--------------------------------------------------------------------------------
1 | //
2 | // TutorialKitExampleTests.m
3 | // TutorialKitExampleTests
4 | //
5 | // Created by Alex on 4/30/14.
6 | // Copyright (c) 2014 TutorialKit. All rights reserved.
7 | //
8 |
9 | #import
10 |
11 | @interface TutorialKitExampleTests : XCTestCase
12 |
13 | @end
14 |
15 | @implementation TutorialKitExampleTests
16 |
17 | - (void)setUp
18 | {
19 | [super setUp];
20 | // Put setup code here. This method is called before the invocation of each test method in the class.
21 | }
22 |
23 | - (void)tearDown
24 | {
25 | // Put teardown code here. This method is called after the invocation of each test method in the class.
26 | [super tearDown];
27 | }
28 |
29 | - (void)testExample
30 | {
31 | XCTFail(@"No implementation for \"%s\"", __PRETTY_FUNCTION__);
32 | }
33 |
34 | @end
35 |
--------------------------------------------------------------------------------
/TutorialKit.podspec:
--------------------------------------------------------------------------------
1 | #
2 | # Be sure to run `pod lib lint NAME.podspec' to ensure this is a
3 | # valid spec and remove all comments before submitting the spec.
4 | #
5 | # To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html
6 | #
7 | Pod::Spec.new do |s|
8 | s.name = "TutorialKit"
9 | s.version = "0.1.8"
10 | s.summary = "In-app tutorials, tips, intros and walk-throughs."
11 | s.homepage = "https://github.com/lostinthepines/TutorialKit"
12 | s.screenshots = "https://github.com/lostinthepines/TutorialKit/raw/master/Assets/tutorialkit.gif"
13 | s.license = 'MIT'
14 | s.author = { "Alex Peterson" => "alex@inthepin.es" }
15 | s.source = { :git => "https://github.com/lostinthepines/TutorialKit.git", :tag => s.version.to_s }
16 | s.social_media_url = 'https://twitter.com/inthepines'
17 | s.platform = :ios, '5.0'
18 | s.requires_arc = true
19 | s.source_files = 'Classes'
20 | s.ios.exclude_files = 'Classes/osx'
21 | end
22 |
--------------------------------------------------------------------------------
/Example/TutorialKitExample/Images.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "iphone",
5 | "size" : "29x29",
6 | "scale" : "2x"
7 | },
8 | {
9 | "idiom" : "iphone",
10 | "size" : "40x40",
11 | "scale" : "2x"
12 | },
13 | {
14 | "idiom" : "iphone",
15 | "size" : "60x60",
16 | "scale" : "2x"
17 | },
18 | {
19 | "idiom" : "ipad",
20 | "size" : "29x29",
21 | "scale" : "1x"
22 | },
23 | {
24 | "idiom" : "ipad",
25 | "size" : "29x29",
26 | "scale" : "2x"
27 | },
28 | {
29 | "idiom" : "ipad",
30 | "size" : "40x40",
31 | "scale" : "1x"
32 | },
33 | {
34 | "idiom" : "ipad",
35 | "size" : "40x40",
36 | "scale" : "2x"
37 | },
38 | {
39 | "idiom" : "ipad",
40 | "size" : "76x76",
41 | "scale" : "1x"
42 | },
43 | {
44 | "idiom" : "ipad",
45 | "size" : "76x76",
46 | "scale" : "2x"
47 | }
48 | ],
49 | "info" : {
50 | "version" : 1,
51 | "author" : "xcode"
52 | }
53 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2014 DANIEL
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/Example/TutorialKitExample/Images.xcassets/LaunchImage.launchimage/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "orientation" : "portrait",
5 | "idiom" : "iphone",
6 | "extent" : "full-screen",
7 | "minimum-system-version" : "7.0",
8 | "scale" : "2x"
9 | },
10 | {
11 | "orientation" : "portrait",
12 | "idiom" : "iphone",
13 | "subtype" : "retina4",
14 | "extent" : "full-screen",
15 | "minimum-system-version" : "7.0",
16 | "scale" : "2x"
17 | },
18 | {
19 | "orientation" : "portrait",
20 | "idiom" : "ipad",
21 | "extent" : "full-screen",
22 | "minimum-system-version" : "7.0",
23 | "scale" : "1x"
24 | },
25 | {
26 | "orientation" : "landscape",
27 | "idiom" : "ipad",
28 | "extent" : "full-screen",
29 | "minimum-system-version" : "7.0",
30 | "scale" : "1x"
31 | },
32 | {
33 | "orientation" : "portrait",
34 | "idiom" : "ipad",
35 | "extent" : "full-screen",
36 | "minimum-system-version" : "7.0",
37 | "scale" : "2x"
38 | },
39 | {
40 | "orientation" : "landscape",
41 | "idiom" : "ipad",
42 | "extent" : "full-screen",
43 | "minimum-system-version" : "7.0",
44 | "scale" : "2x"
45 | }
46 | ],
47 | "info" : {
48 | "version" : 1,
49 | "author" : "xcode"
50 | }
51 | }
--------------------------------------------------------------------------------
/Classes/UIView+Recursion.h:
--------------------------------------------------------------------------------
1 | /*
2 | UIView+Recursion.h
3 | Created by Alex on 4/21/14.
4 | Copyright (c) 2014 DANIEL. All rights reserved.
5 |
6 | The MIT License (MIT)
7 |
8 | Permission is hereby granted, free of charge, to any person obtaining a copy of
9 | this software and associated documentation files (the "Software"), to deal in
10 | the Software without restriction, including without limitation the rights to
11 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
12 | the Software, and to permit persons to whom the Software is furnished to do so,
13 | subject to the following conditions:
14 |
15 | The above copyright notice and this permission notice shall be included in all
16 | copies or substantial portions of the Software.
17 |
18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
20 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
21 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
22 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
23 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
24 | */
25 | #import
26 |
27 | @interface UIView (Recursion)
28 |
29 | /** Find a UIView recursively. Return TRUE from the block to recurse into subview.
30 | Set stop to TRUE to stop recursing and return the subview.
31 |
32 | @param recurse The block to use to determine whether to continue recursing
33 | @return Returns the UIView if found or nil
34 | */
35 | - (UIView*)findViewRecursively:(BOOL(^)(UIView* subview, BOOL* stop))recurse;
36 |
37 | @end
38 |
--------------------------------------------------------------------------------
/Example/TutorialKitExample/TutorialKitExample-Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleDisplayName
8 | ${PRODUCT_NAME}
9 | CFBundleExecutable
10 | ${EXECUTABLE_NAME}
11 | CFBundleIdentifier
12 | com.daniel.${PRODUCT_NAME:rfc1034identifier}
13 | CFBundleInfoDictionaryVersion
14 | 6.0
15 | CFBundleName
16 | ${PRODUCT_NAME}
17 | CFBundlePackageType
18 | APPL
19 | CFBundleShortVersionString
20 | 1.0
21 | CFBundleSignature
22 | ????
23 | CFBundleVersion
24 | 1.0
25 | LSRequiresIPhoneOS
26 |
27 | UIRequiredDeviceCapabilities
28 |
29 | armv7
30 |
31 | UISupportedInterfaceOrientations
32 |
33 | UIInterfaceOrientationPortrait
34 | UIInterfaceOrientationLandscapeLeft
35 | UIInterfaceOrientationLandscapeRight
36 | UIInterfaceOrientationPortraitUpsideDown
37 |
38 | UISupportedInterfaceOrientations~ipad
39 |
40 | UIInterfaceOrientationPortrait
41 | UIInterfaceOrientationPortraitUpsideDown
42 | UIInterfaceOrientationLandscapeLeft
43 | UIInterfaceOrientationLandscapeRight
44 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/Classes/UIView+Recursion.m:
--------------------------------------------------------------------------------
1 | /*
2 | UIView+Recursion.m
3 | Created by Alex on 4/21/14.
4 | Copyright (c) 2014 DANIEL. All rights reserved.
5 |
6 | The MIT License (MIT)
7 |
8 | Permission is hereby granted, free of charge, to any person obtaining a copy of
9 | this software and associated documentation files (the "Software"), to deal in
10 | the Software without restriction, including without limitation the rights to
11 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
12 | the Software, and to permit persons to whom the Software is furnished to do so,
13 | subject to the following conditions:
14 |
15 | The above copyright notice and this permission notice shall be included in all
16 | copies or substantial portions of the Software.
17 |
18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
20 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
21 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
22 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
23 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
24 | */
25 |
26 | #import "UIView+Recursion.h"
27 |
28 | @implementation UIView (Recursion)
29 |
30 | ////////////////////////////////////////////////////////////////////////////////
31 | - (UIView*)findViewRecursively:(BOOL(^)(UIView* subview, BOOL* stop))recurse
32 | {
33 | for( UIView* subview in self.subviews ) {
34 | BOOL stop = NO;
35 | if( recurse( subview, &stop ) ) {
36 | UIView *view = [subview findViewRecursively:recurse];
37 | if(view) return view;
38 | } else if( stop ) {
39 | return subview;
40 | }
41 | }
42 | return nil;
43 | }
44 |
45 | @end
--------------------------------------------------------------------------------
/Classes/TutorialKitView.h:
--------------------------------------------------------------------------------
1 | /*
2 | TutorialKitView.h
3 | Created by Alex on 4/21/14.
4 | Copyright (c) 2014 DANIEL. All rights reserved.
5 |
6 | The MIT License (MIT)
7 |
8 | Permission is hereby granted, free of charge, to any person obtaining a copy of
9 | this software and associated documentation files (the "Software"), to deal in
10 | the Software without restriction, including without limitation the rights to
11 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
12 | the Software, and to permit persons to whom the Software is furnished to do so,
13 | subject to the following conditions:
14 |
15 | The above copyright notice and this permission notice shall be included in all
16 | copies or substantial portions of the Software.
17 |
18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
20 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
21 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
22 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
23 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
24 | */
25 | #import
26 |
27 | @interface TutorialKitView : UIView
28 |
29 | + (instancetype) tutorialViewWithMessage:(NSString *)message
30 | messageCenter:(CGPoint)messageCenter
31 | messageCenterRelative:(BOOL)relativeMessageCenter
32 | font:(UIFont *)font
33 | color:(UIColor *)color
34 | highlightView:(UIView *)view
35 | highlightPoint:(CGPoint)point
36 | highlightPointRelative:(BOOL)relativeHighlightPoint
37 | highlightRadius:(float)radius;
38 |
39 |
40 | + (instancetype) tutorialViewWithMessage:(NSString *)message
41 | messageCenter:(CGPoint)messageCenter
42 | messageCenterRelative:(BOOL)relativeMessageCenter
43 | font:(UIFont *)font
44 | color:(UIColor *)color
45 | swipeGestureStart:(CGPoint)start
46 | swipeGestureEnd:(CGPoint)end
47 | swipePositionsRelative:(BOOL)relativeSwipePositions
48 | highlightRadius:(float)radius;
49 |
50 | + (instancetype) tutorialViewWithDictionary:(NSDictionary *)values;
51 |
52 | @property (nonatomic, strong) NSDictionary *values;
53 | @property (nonatomic, strong) NSString *sequenceName;
54 | @property (nonatomic) NSInteger sequenceStep;
55 | @property (nonatomic) CGFloat blurAmount;
56 | @property (nonatomic, strong) UIColor *tintColor;
57 |
58 | @end
59 |
--------------------------------------------------------------------------------
/Example/TutorialKitExample/AppDelegate.m:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.m
3 | // TutorialKitExample
4 | //
5 | // Created by Alex on 4/30/14.
6 | // Copyright (c) 2014 TutorialKit. All rights reserved.
7 | //
8 |
9 | #import "AppDelegate.h"
10 | #import "ExampleViewController.h"
11 | #import "TutorialKit.h"
12 |
13 | @implementation AppDelegate
14 |
15 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
16 | {
17 | self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
18 | self.window.backgroundColor = [UIColor whiteColor];
19 | self.window.rootViewController = [[ExampleViewController alloc] init];
20 |
21 | NSValue *msgPoint = [NSValue valueWithCGPoint:
22 | CGPointMake(0.5,0.7)];
23 | NSValue *swipeStart = [NSValue valueWithCGPoint:
24 | CGPointMake(0.75,0.8)];
25 | NSValue *swipeEnd = [NSValue valueWithCGPoint:
26 | CGPointMake(0.25,0.8)];
27 | // set up a simple 3 step tutorial
28 | NSArray *steps = @[
29 | // Step 0
30 | @{
31 | TKHighlightViewTag: @(1001),
32 | TKMessage: @"First, press this button.",
33 | TKMessageRelativePoint: msgPoint
34 | },
35 | // Step 1
36 | @{
37 | TKSwipeGestureRelativeStartPoint: swipeStart,
38 | TKSwipeGestureRelativeEndPoint: swipeEnd,
39 | TKMessage: @"Next, swipe left.",
40 | TKMessageRelativePoint: msgPoint
41 | },
42 | // Step 2
43 | @{
44 | TKMessage: @"That's it! Yer all done!",
45 | TKMessageRelativePoint: msgPoint,
46 | TKCompleteCallback: ^{ NSLog(@"ALL DONE."); }
47 | },
48 | ];
49 |
50 | [TutorialKit addTutorialSequence:steps name:@"example"];
51 |
52 | // insert an extra step
53 | NSArray *moreSteps = @[
54 | @{
55 | TKHighlightViewTag: @(1001),
56 | TKMessage: @"Please press this button again.",
57 | TKMessageRelativePoint: msgPoint
58 | },
59 | ];
60 | [TutorialKit insertTutorialSequence:moreSteps name:@"example" beforeStep:2];
61 |
62 | // some optional defaults
63 | [TutorialKit setDefaultBlurAmount:0.5];
64 | [TutorialKit setDefaultMessageColor:UIColor.grayColor];
65 | [TutorialKit setDefaultTintColor:[UIColor colorWithWhite:1.0 alpha:0.5]];
66 |
67 | [self.window makeKeyAndVisible];
68 | return YES;
69 | }
70 |
71 | @end
72 |
--------------------------------------------------------------------------------
/Example/TutorialKitExample/ExampleViewController.m:
--------------------------------------------------------------------------------
1 | //
2 | // ExampleViewController.m
3 | // TutorialKitExample
4 | //
5 | // Created by Alex on 4/30/14.
6 | // Copyright (c) 2014 TutorialKit. All rights reserved.
7 | //
8 |
9 | #import "ExampleViewController.h"
10 | #import "TutorialKit.h"
11 |
12 | @interface ExampleViewController()
13 | @property (nonatomic, weak) UIButton *nextButton;
14 | @property (nonatomic, weak) UIButton *startButton;
15 | @end
16 | @implementation ExampleViewController
17 |
18 | - (void)viewDidLoad
19 | {
20 | [super viewDidLoad];
21 |
22 | self.view.backgroundColor = [UIColor colorWithPatternImage:self.repeatingBackground];
23 |
24 | UIButton *btn = [UIButton buttonWithType:UIButtonTypeCustom];
25 | btn.frame = CGRectMake(0, 0, 200.f, 60.f);
26 | [btn setTitle:@"START" forState:UIControlStateNormal];
27 | [btn setTitleColor:UIColor.whiteColor forState:UIControlStateNormal];
28 | [btn setTitleColor:UIColor.blackColor forState:UIControlStateHighlighted];
29 | btn.backgroundColor = [UIColor colorWithRed:0.3 green:0.7 blue:0.3 alpha:1.0];
30 | btn.layer.cornerRadius = 15.f;
31 | [btn addTarget:self action:@selector(start:) forControlEvents:UIControlEventTouchUpInside];
32 | [self.view addSubview:btn];
33 | self.startButton = btn;
34 |
35 | // a reset button
36 | btn = [UIButton buttonWithType:UIButtonTypeCustom];
37 | btn.frame = CGRectMake(0, 0, 200.f, 60.f);
38 | [btn setTitle:@"NEXT STEP" forState:UIControlStateNormal];
39 | [btn setTitleColor:UIColor.whiteColor forState:UIControlStateNormal];
40 | [btn setTitleColor:UIColor.blackColor forState:UIControlStateHighlighted];
41 | btn.backgroundColor = [UIColor colorWithRed:0.8 green:0.3 blue:0.3 alpha:1.0];
42 | btn.layer.cornerRadius = 15.f;
43 | btn.tag = 1001;
44 | [btn addTarget:self action:@selector(nextStep:) forControlEvents:UIControlEventTouchUpInside];
45 | [self.view addSubview:btn];
46 | self.nextButton = btn;
47 | }
48 |
49 | - (void)viewWillLayoutSubviews
50 | {
51 | [super viewWillLayoutSubviews];
52 |
53 | UIInterfaceOrientation orientation = UIApplication.sharedApplication.statusBarOrientation;
54 | CGPoint center = self.view.center;
55 | if(orientation == UIInterfaceOrientationLandscapeLeft ||
56 | orientation == UIInterfaceOrientationLandscapeRight) {
57 | center.x = self.view.center.y;
58 | center.y = self.view.center.x;
59 | }
60 |
61 | self.startButton.center = CGPointMake(center.x, center.y * 0.5);
62 | self.nextButton.center = CGPointMake(center.x, self.startButton.center.y + 100.f);
63 | }
64 |
65 | - (void)start:(id)sender
66 | {
67 | // TutorialKit remembers the current step in NSUserDefaults, but we want to
68 | // reset the current step every time we press this button
69 | [TutorialKit setCurrentStep:0 forTutorial:@"example"];
70 |
71 | [TutorialKit advanceTutorialSequenceWithName:@"example"];
72 | }
73 |
74 | - (void)nextStep:(id)sender
75 | {
76 | // Auto continue to the next step when the current step is over
77 | // The default is to not to continue automatically.
78 | [TutorialKit advanceTutorialSequenceWithName:@"example" andContinue:YES];
79 | }
80 |
81 | - (UIImage *)repeatingBackground
82 | {
83 | // a simple repeating background yo
84 | UIGraphicsBeginImageContext(CGSizeMake(32,32));
85 | CGContextRef ctx = UIGraphicsGetCurrentContext();
86 |
87 | [UIColor.grayColor setStroke];
88 | CGContextStrokeEllipseInRect(ctx, CGRectMake(2,2,28,28));
89 |
90 | UIImage *img = UIGraphicsGetImageFromCurrentImageContext();
91 | UIGraphicsEndImageContext();
92 |
93 | return img;
94 | }
95 | @end
96 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # TutorialKit
2 |
3 | [](http://cocoadocs.org/docsets/TutorialKit)
4 | [](http://cocoadocs.org/docsets/TutorialKit)
5 |
6 | 
7 |
8 | TutorialKit is a library for creating interactive step by step tutorials. Highlight views with action messages, and show gestures without adding a lot of extra logic code to your app.
9 |
10 |
11 | ## Usage
12 |
13 | **Simple example:**
14 |
15 | - Add a tutorial sequence (the same one used in the gif above)
16 | ```Objective-C
17 | // Set up in the AppDelegate (but could be anywhere really)
18 | NSValue *msgPoint = [NSValue valueWithCGPoint:
19 | CGPointMake(self.window.bounds.size.width * 0.5,
20 | self.window.bounds.size.height * 0.65)];
21 | NSValue *swipeStart = [NSValue valueWithCGPoint:
22 | CGPointMake(self.window.bounds.size.width * 0.75,
23 | self.window.bounds.size.height * 0.75)];
24 | NSValue *swipeEnd = [NSValue valueWithCGPoint:
25 | CGPointMake(self.window.bounds.size.width * 0.25,
26 | self.window.bounds.size.height * 0.75)];
27 |
28 | // set up a simple 3 step tutorial
29 | NSArray *steps = @[
30 | // Step 0
31 | @{
32 | TKHighlightViewTag: @(1001), // tag assigned to your UIButton
33 | TKMessage: @"First, press this button.",
34 | TKMessagePoint: msgPoint
35 | },
36 | // Step 1
37 | @{
38 | TKSwipeGestureStartPoint: swipeStart,
39 | TKSwipeGestureEndPoint: swipeEnd,
40 | TKMessage: @"Next, swipe left.",
41 | TKMessagePoint: msgPoint
42 | },
43 | // Step 2
44 | @{
45 | TKMessage: @"That's it! Yer all done!",
46 | TKMessagePoint: msgPoint,
47 | TKCompleteCallback: ^{ NSLog(@"ALL DONE."); }
48 | },
49 | ];
50 |
51 | [TutorialKit addTutorialSequence:steps name:@"example"];
52 | ```
53 |
54 | - At any point in the application where you want to advance the tutorial:
55 | ```Objective-C
56 | [TutorialKit advanceTutorialSequenceWithName:@"example"];
57 | ```
58 |
59 | - Profit.
60 |
61 | **You can also configure the TutorialKit default styles.**
62 | ```Objective-C
63 | [TutorialKit setDefaultBlurAmount:0.5];
64 | [TutorialKit setDefaultMessageColor:UIColor.grayColor];
65 | [TutorialKit setDefaultMessageFont:[UIFont fontWithName:@"Helvetica" size:20]];
66 | [TutorialKit setDefaultTintColor:[UIColor colorWithWhite:1.0 alpha:0.5]];
67 | ```
68 |
69 | **At any time you can set the tutorial step (useful if you want to start over)**
70 | ```Objective-C
71 | [TutorialKit setCurrentStep:0 forTutorial:@"example"];
72 | ```
73 |
74 | To run the example project; clone the repo, and run `pod install` from the Example directory first.
75 |
76 | ## Installation
77 |
78 | TutorialKit is available through [CocoaPods](http://cocoapods.org), to install
79 | it simply add the following line to your Podfile:
80 |
81 | pod "TutorialKit"
82 |
83 | ## Author
84 |
85 | Alex Peterson, alex@inthepin.es
86 |
87 | ## License
88 |
89 | TutorialKit is available under the MIT license. See the LICENSE file for more info.
90 |
91 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | desc "Runs the specs [EMPTY]"
2 | task :spec do
3 | # Provide your own implementation
4 | end
5 |
6 | task :version do
7 | git_remotes = `git remote`.strip.split("\n")
8 |
9 | if git_remotes.count > 0
10 | puts "-- fetching version number from github"
11 | sh 'git fetch'
12 |
13 | remote_version = remote_spec_version
14 | end
15 |
16 | if remote_version.nil?
17 | puts "There is no current released version. You're about to release a new Pod."
18 | version = "0.0.1"
19 | else
20 | puts "The current released version of your pod is " +
21 | remote_spec_version.to_s()
22 | version = suggested_version_number
23 | end
24 |
25 | puts "Enter the version you want to release (" + version + ") "
26 | new_version_number = $stdin.gets.strip
27 | if new_version_number == ""
28 | new_version_number = version
29 | end
30 |
31 | replace_version_number(new_version_number)
32 | end
33 |
34 | desc "Release a new version of the Pod (append repo=name to push to a private spec repo)"
35 | task :release do
36 | # Allow override of spec repo name using `repo=private` after task name
37 | repo = ENV["repo"] || "master"
38 |
39 | puts "* Running version"
40 | sh "rake version"
41 |
42 | unless ENV['SKIP_CHECKS']
43 | if `git symbolic-ref HEAD 2>/dev/null`.strip.split('/').last != 'master'
44 | $stderr.puts "[!] You need to be on the `master' branch in order to be able to do a release."
45 | exit 1
46 | end
47 |
48 | if `git tag`.strip.split("\n").include?(spec_version)
49 | $stderr.puts "[!] A tag for version `#{spec_version}' already exists. Change the version in the podspec"
50 | exit 1
51 | end
52 |
53 | puts "You are about to release `#{spec_version}`, is that correct? [y/n]"
54 | exit if $stdin.gets.strip.downcase != 'y'
55 | end
56 |
57 | puts "* Running specs"
58 | sh "rake spec"
59 |
60 | puts "* Linting the podspec"
61 | sh "pod lib lint"
62 |
63 | # Then release
64 | sh "git commit #{podspec_path} CHANGELOG.md -m 'Release #{spec_version}' --allow-empty"
65 | sh "git tag -a #{spec_version} -m 'Release #{spec_version}'"
66 | sh "git push origin master"
67 | sh "git push origin --tags"
68 | sh "pod push #{repo} #{podspec_path}"
69 | end
70 |
71 | # @return [Pod::Version] The version as reported by the Podspec.
72 | #
73 | def spec_version
74 | require 'cocoapods'
75 | spec = Pod::Specification.from_file(podspec_path)
76 | spec.version
77 | end
78 |
79 | # @return [Pod::Version] The version as reported by the Podspec from remote.
80 | #
81 | def remote_spec_version
82 | require 'cocoapods-core'
83 |
84 | if spec_file_exist_on_remote?
85 | remote_spec = eval(`git show origin/master:#{podspec_path}`)
86 | remote_spec.version
87 | else
88 | nil
89 | end
90 | end
91 |
92 | # @return [Bool] If the remote repository has a copy of the podpesc file or not.
93 | #
94 | def spec_file_exist_on_remote?
95 | test_condition = `if git rev-parse --verify --quiet origin/master:#{podspec_path} >/dev/null;
96 | then
97 | echo 'true'
98 | else
99 | echo 'false'
100 | fi`
101 |
102 | 'true' == test_condition.strip
103 | end
104 |
105 | # @return [String] The relative path of the Podspec.
106 | #
107 | def podspec_path
108 | podspecs = Dir.glob('*.podspec')
109 | if podspecs.count == 1
110 | podspecs.first
111 | else
112 | raise "Could not select a podspec"
113 | end
114 | end
115 |
116 | # @return [String] The suggested version number based on the local and remote
117 | # version numbers.
118 | #
119 | def suggested_version_number
120 | if spec_version != remote_spec_version
121 | spec_version.to_s()
122 | else
123 | next_version(spec_version).to_s()
124 | end
125 | end
126 |
127 | # @param [Pod::Version] version
128 | # the version for which you need the next version
129 | #
130 | # @note It is computed by bumping the last component of
131 | # the version string by 1.
132 | #
133 | # @return [Pod::Version] The version that comes next after
134 | # the version supplied.
135 | #
136 | def next_version(version)
137 | version_components = version.to_s().split(".");
138 | last = (version_components.last.to_i() + 1).to_s
139 | version_components[-1] = last
140 | Pod::Version.new(version_components.join("."))
141 | end
142 |
143 | # @param [String] new_version_number
144 | # the new version number
145 | #
146 | # @note This methods replaces the version number in the podspec file
147 | # with a new version number.
148 | #
149 | # @return void
150 | #
151 | def replace_version_number(new_version_number)
152 | text = File.read(podspec_path)
153 | text.gsub!(/(s.version( )*= ")#{spec_version}(")/,
154 | "\\1#{new_version_number}\\3")
155 | File.open(podspec_path, "w") { |file| file.puts text }
156 | end
157 |
--------------------------------------------------------------------------------
/Classes/TutorialKit.h:
--------------------------------------------------------------------------------
1 | /*
2 | TutorialKit.h
3 | Created by Alex on 4/21/14.
4 | Copyright (c) 2014 DANIEL. All rights reserved.
5 |
6 | The MIT License (MIT)
7 |
8 | Permission is hereby granted, free of charge, to any person obtaining a copy of
9 | this software and associated documentation files (the "Software"), to deal in
10 | the Software without restriction, including without limitation the rights to
11 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
12 | the Software, and to permit persons to whom the Software is furnished to do so,
13 | subject to the following conditions:
14 |
15 | The above copyright notice and this permission notice shall be included in all
16 | copies or substantial portions of the Software.
17 |
18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
20 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
21 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
22 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
23 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
24 | */
25 |
26 | #import
27 |
28 | static NSString* const TKBlurAmount = @"TKBlurAmount";
29 | static NSString* const TKMessage = @"TKMessage";
30 | static NSString* const TKMessageColor = @"TKMessageColor";
31 | static NSString* const TKMessageFont = @"TKMessageFont";
32 | static NSString* const TKMessagePoint = @"TKMessagePoint"; // absolute 0..width, 0..height
33 | static NSString* const TKMessageRelativePoint = @"TKMessageRelativePoint"; // relative 0..1, 0..1
34 | static NSString* const TKHighlightView = @"TKHighlightView";
35 | static NSString* const TKHighlightViewTag = @"TKHighlightViewTag";
36 | static NSString* const TKHighlightPoint = @"TKHighlightPoint"; // absolute 0..width, 0..height
37 | static NSString* const TKHighlightRelativePoint = @"TKHighlightRelativePoint"; // relative 0..1
38 | static NSString* const TKHighlightRadius = @"TKHighlightRadius";
39 | static NSString* const TKSwipeGestureStartPoint = @"TKSwipeGestureStartPoint";
40 | static NSString* const TKSwipeGestureRelativeStartPoint = @"TKSwipeGestureRelativeStartPoint";
41 | static NSString* const TKSwipeGestureEndPoint = @"TKSwipeGestureEndPoint";
42 | static NSString* const TKSwipeGestureRelativeEndPoint = @"TKSwipeGestureRelativeEndPoint";
43 | static NSString* const TKCompleteCallback = @"TKCompleteCallback";
44 |
45 | @interface TutorialKit : NSObject
46 |
47 | /** Adds a tutorial sequence to display
48 |
49 | Example:
50 | [self addTutorialSequence:@[
51 | // highlight a view and display a message
52 | @{
53 | TKMessage:@"first message",
54 | TKMessagePoint:[NSValue valueWithCGPoint:CGPointMake(100,100)],
55 | TKHighlightViewTag:1,
56 | TKCompleteCallback:^{ NSLog("Step 1 complete!"); }
57 | },
58 |
59 | // highlight a specific point in a view and display a message
60 | @{
61 | TKMessage:@"second message",
62 | TKMessageRelativePoint:[NSValue valueWithCGPoint:CGPointMake(0.5,0.75)],
63 | TKHighlightViewTag:1,
64 | TKHighlightRelativePoint:[NSValue valueWithCGPoint:CGPointMake(0.5,0.5)],
65 | TKHighlightRadius:@(100)
66 | TKCompleteCallback:^{ NSLog("Sequence complete!"); }
67 | },
68 |
69 | ]];
70 |
71 |
72 | @param array of tutorial sequence dictionaries
73 | @param name The name of this tutorial sequence
74 | */
75 | + (void)addTutorialSequence:(NSArray *)sequence name:(NSString*)name;
76 |
77 | /** Advance the tutorial sequence is possible and does not auto-continue
78 |
79 | @param name The name of the tutorial sequence to advance
80 | @return Returns TRUE if the tutorial sequence advanced
81 | */
82 | + (BOOL)advanceTutorialSequenceWithName:(NSString *)name;
83 |
84 | /** Advance the tutorial sequence is possible
85 |
86 | @param name The name of the tutorial sequence to advance
87 | @param continue True if should continue to next tutorial step if possible
88 | @return Returns TRUE if the tutorial sequence advanced
89 | */
90 | + (BOOL)advanceTutorialSequenceWithName:(NSString *)name
91 | andContinue:(BOOL)shouldContinue;
92 |
93 | /** Advance the tutorial sequence is possible and only if on this step and does
94 | not auto-continue.
95 |
96 | @param name The name of the tutorial sequence to advance
97 | @param step Only advance if the tutorial is on this step
98 | @return Returns TRUE if the tutorial sequence advanced
99 | */
100 | + (BOOL)advanceTutorialSequenceWithName:(NSString *)name
101 | ifOnStep:(NSInteger)step;
102 |
103 | /** Advance the tutorial sequence is possible and only if on this step and
104 | continue if requested.
105 |
106 | @param name The name of the tutorial sequence to advance
107 | @param step Only advance if the tutorial is on this step
108 | @param shouldContinue True if should continue to next tutorial step if possible
109 | @return Returns TRUE if the tutorial sequence advanced
110 | */
111 | + (BOOL)advanceTutorialSequenceWithName:(NSString *)name
112 | ifOnStep:(NSInteger)step
113 | andContinue:(BOOL)shouldContinue;
114 |
115 | /** Current step for the specified tutorial
116 |
117 | @param name The name of the tutorial sequence
118 | @return Returns the current step for the specified tutorial
119 | */
120 | + (NSInteger)currentStepForTutorialWithName:(NSString *)name;
121 |
122 |
123 | /** The UIView for the active tutorial step
124 |
125 | @return Returns the current tutorial UIView or nil if no tutorial is visible
126 | */
127 | + (UIView *)currentTutorialView;
128 |
129 | /** Dismiss the current tutorial view
130 | */
131 | + (void)dismissCurrentTutorialView;
132 |
133 | /** Inserts a tutorial sequence into the existince sequence
134 | @param array of tutorial sequence dictionaries
135 | @param name The name of this tutorial sequence
136 | @param name The name of this tutorial sequence
137 | @param step The step to after which to insert this sequence
138 | */
139 | + (void)insertTutorialSequence:(NSArray *)sequence name:(NSString*)name afterStep:(NSInteger)step;
140 |
141 | /** Inserts a tutorial sequence into the existince sequence
142 | @param array of tutorial sequence dictionaries
143 | @param name The name of this tutorial sequence
144 | @param name The name of this tutorial sequence
145 | @param step The step to before which to insert this sequence
146 | */
147 | + (void)insertTutorialSequence:(NSArray *)sequence name:(NSString*)name beforeStep:(NSInteger)step;
148 |
149 | /** Set the current step for the specified tutorial
150 |
151 | @param step The current step for the specified tutorial sequence
152 | @param name The name of the tutorial sequence
153 | */
154 | + (void)setCurrentStep:(NSInteger)step forTutorial:(NSString *)name;
155 |
156 | /** Set the default blur amount. Set to zero to disable blur
157 |
158 | @param color The amount to blur the underlying image
159 | */
160 | + (void)setDefaultBlurAmount:(CGFloat)amount;
161 |
162 | /** Set the default message color
163 |
164 | @param color The UIColor to use for messages by default
165 | */
166 | + (void)setDefaultMessageColor:(UIColor *)color;
167 |
168 | /** Set the default font to use
169 |
170 | @param font The UIFont to use for messages by default
171 | */
172 | + (void)setDefaultMessageFont:(UIFont *)font;
173 |
174 | /** Set the default background tint color
175 |
176 | @param color The UIColor to use for the background tint
177 | */
178 | + (void)setDefaultTintColor:(UIColor *)color;
179 |
180 | @end
181 |
--------------------------------------------------------------------------------
/Classes/TutorialKit.m:
--------------------------------------------------------------------------------
1 | /*
2 | TutorialKit.m
3 | Created by Alex on 4/21/14.
4 | Copyright (c) 2014 DANIEL. All rights reserved.
5 |
6 | The MIT License (MIT)
7 |
8 | Permission is hereby granted, free of charge, to any person obtaining a copy of
9 | this software and associated documentation files (the "Software"), to deal in
10 | the Software without restriction, including without limitation the rights to
11 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
12 | the Software, and to permit persons to whom the Software is furnished to do so,
13 | subject to the following conditions:
14 |
15 | The above copyright notice and this permission notice shall be included in all
16 | copies or substantial portions of the Software.
17 |
18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
20 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
21 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
22 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
23 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
24 | */
25 |
26 | #import "TutorialKit.h"
27 | #import "UIView+Recursion.h"
28 | #import "TutorialKitView.h"
29 |
30 | #define TKSequence @"TutorialKitSequence"
31 | #define TKStep @"TutorialKitStep"
32 | #define TKUserDefaultsKey @"TutorialKitUserDefaults"
33 |
34 | @interface TutorialKit()
35 | @property (nonatomic, strong) NSMutableDictionary *sequences;
36 | @property (nonatomic, strong) TutorialKitView *currentTutorialView;
37 | @property (nonatomic, strong) UIColor *backgroundTintColor;
38 | @property (nonatomic, strong) UIColor *labelColor;
39 | @property (nonatomic, strong) UIFont *labelFont;
40 | @property (nonatomic) CGFloat blurAmount;
41 | @property (nonatomic) BOOL shouldContinue;
42 | @end
43 |
44 | @implementation TutorialKit
45 |
46 | #pragma mark - Public
47 |
48 | ////////////////////////////////////////////////////////////////////////////////
49 | + (void)addTutorialSequence:(NSArray *)sequence name:(NSString*)name
50 | {
51 | NSInteger step = [TutorialKit currentStepForTutorialWithName:name];;
52 | [TutorialKit.sharedInstance.sequences setObject:@{TKSequence:sequence,TKStep:@(step)} forKey:name];
53 | }
54 |
55 | ////////////////////////////////////////////////////////////////////////////////
56 | + (BOOL)advanceTutorialSequenceWithName:(NSString *)name
57 | {
58 | return [TutorialKit advanceTutorialSequenceWithName:name andContinue:NO];
59 | }
60 |
61 | ////////////////////////////////////////////////////////////////////////////////
62 | + (BOOL)advanceTutorialSequenceWithName:(NSString *)name andContinue:(BOOL)shouldContinue
63 | {
64 | if(TutorialKit.sharedInstance.currentTutorialView) {
65 | // a tutorial view is already visible
66 | return NO;
67 | }
68 |
69 | NSMutableDictionary *sequenceData = [TutorialKit.sharedInstance.sequences objectForKey:name];
70 | if(!sequenceData) {
71 | // assert?
72 | return NO;
73 | }
74 |
75 | if(![sequenceData isKindOfClass:NSMutableDictionary.class]) {
76 | sequenceData = sequenceData.mutableCopy;
77 | }
78 |
79 | NSNumber *step = [sequenceData objectForKey:TKStep];
80 | if(nil == step) {
81 | step = @(0);
82 | [sequenceData setObject:step forKey:TKStep];
83 | }
84 |
85 | NSArray *sequence = [sequenceData objectForKey:TKSequence];
86 | if(!sequence) {
87 | // assert?
88 | return NO;
89 | }
90 |
91 | if(step.integerValue >= sequence.count || step.integerValue < 0) {
92 | // sequence step is invalid or sequence is over
93 | return NO;
94 | }
95 |
96 | NSMutableDictionary *current = [sequence objectAtIndex:step.integerValue];
97 | if(!current) {
98 | // assert?
99 | return NO;
100 | }
101 | else if(![current isKindOfClass:NSMutableDictionary.class]) {
102 | // ensure this is a mutable copy
103 | current = current.mutableCopy;
104 | }
105 |
106 | if(![current objectForKey:TKMessageFont]) {
107 | [current setObject:TutorialKit.sharedInstance.labelFont forKey:TKMessageFont];
108 | }
109 |
110 | if(![current objectForKey:TKMessageColor]) {
111 | [current setObject:TutorialKit.sharedInstance.labelColor forKey:TKMessageColor];
112 | }
113 |
114 | if(![current objectForKey:TKBlurAmount]) {
115 | [current setObject:@(TutorialKit.sharedInstance.blurAmount) forKey:TKBlurAmount];
116 | }
117 |
118 | if([current objectForKey:TKHighlightViewTag]) {
119 | UIView *highlightView = [TutorialKit.sharedInstance
120 | findViewWithTag:[current objectForKey:TKHighlightViewTag]];
121 | if(highlightView) {
122 | [current setObject:highlightView forKey:TKHighlightView];
123 | }
124 | else {
125 | // highlight view not found! assert?
126 | return NO;
127 | }
128 | }
129 |
130 | TutorialKitView *tkv = [TutorialKitView tutorialViewWithDictionary:current];
131 | if(tkv) {
132 | tkv.sequenceStep = step.integerValue;
133 | tkv.sequenceName = name;
134 | tkv.tintColor = TutorialKit.sharedInstance.backgroundTintColor;
135 | UITapGestureRecognizer *tapRecognizer = [[UITapGestureRecognizer alloc]
136 | initWithTarget:TutorialKit.sharedInstance
137 | action:@selector(dismissTutorialView:)];
138 | [tkv addGestureRecognizer:tapRecognizer];
139 | TutorialKit.sharedInstance.currentTutorialView = tkv;
140 | TutorialKit.sharedInstance.shouldContinue = shouldContinue;
141 | [TutorialKit presentTutorialView:tkv withAnimation:YES];
142 | }
143 |
144 | return tkv != NULL;
145 | }
146 |
147 | ////////////////////////////////////////////////////////////////////////////////
148 | + (BOOL)advanceTutorialSequenceWithName:(NSString *)name ifOnStep:(NSInteger)step
149 | {
150 | return [TutorialKit advanceTutorialSequenceWithName:name
151 | ifOnStep:step
152 | andContinue:NO];
153 | }
154 |
155 | ////////////////////////////////////////////////////////////////////////////////
156 | + (BOOL)advanceTutorialSequenceWithName:(NSString *)name
157 | ifOnStep:(NSInteger)step
158 | andContinue:(BOOL)shouldContinue
159 | {
160 | if([TutorialKit currentStepForTutorialWithName:name] != step) {
161 | return NO;
162 | }
163 |
164 | return [TutorialKit advanceTutorialSequenceWithName:name andContinue:shouldContinue];
165 | }
166 |
167 | ////////////////////////////////////////////////////////////////////////////////
168 | + (NSInteger)currentStepForTutorialWithName:(NSString *)name
169 | {
170 | NSMutableDictionary *sequence = [TutorialKit.sharedInstance.sequences objectForKey:name];
171 | if(sequence) {
172 | NSNumber *step = [sequence objectForKey:TKStep];
173 | if(step) return step.integerValue;
174 | }
175 | else {
176 | // check NSUserDefaults for a stored tutorial step
177 | NSInteger step = 0;
178 | if([NSUserDefaults.standardUserDefaults objectForKey:TKUserDefaultsKey]) {
179 | NSMutableDictionary *steps = [NSUserDefaults.standardUserDefaults objectForKey:TKUserDefaultsKey];
180 | if([steps objectForKey:name]) {
181 | step = [[steps objectForKey:name] integerValue];
182 | }
183 | }
184 | return step;
185 | }
186 |
187 | return 0;
188 | }
189 |
190 | ////////////////////////////////////////////////////////////////////////////////
191 | + (UIView *)currentTutorialView
192 | {
193 | return TutorialKit.sharedInstance.currentTutorialView;
194 | }
195 |
196 | ////////////////////////////////////////////////////////////////////////////////
197 | + (void)dismissCurrentTutorialView
198 | {
199 | if(TutorialKit.sharedInstance.currentTutorialView) {
200 | [TutorialKit.sharedInstance
201 | dismissTutorialView:TutorialKit.sharedInstance.currentTutorialView];
202 | }
203 | }
204 |
205 |
206 | ////////////////////////////////////////////////////////////////////////////////
207 | + (void)insertTutorialSequence:(NSArray *)sequence name:(NSString*)name afterStep:(NSInteger)step
208 | {
209 | [TutorialKit insertTutorialSequence:sequence name:name beforeStep:step + 1];
210 | }
211 |
212 | ////////////////////////////////////////////////////////////////////////////////
213 | + (void)insertTutorialSequence:(NSArray *)sequence name:(NSString*)name beforeStep:(NSInteger)step
214 | {
215 | NSDictionary *existingSequence = [TutorialKit.sharedInstance.sequences objectForKey:name];
216 | if(!existingSequence) {
217 | // @TODO Error message?
218 | return;
219 | }
220 |
221 | NSInteger curStep = [TutorialKit currentStepForTutorialWithName:name];
222 | NSArray *steps = existingSequence[TKSequence];
223 | if(step >= steps.count) {
224 | steps = [steps arrayByAddingObjectsFromArray:sequence];
225 | }
226 | else if(step > 0) {
227 | NSMutableArray *newSequence = @[].mutableCopy;
228 | [newSequence addObjectsFromArray:[steps subarrayWithRange:NSMakeRange(0, step)]];
229 | [newSequence addObjectsFromArray:sequence];
230 | [newSequence addObjectsFromArray:[steps subarrayWithRange:NSMakeRange(step, steps.count - step)]];
231 | steps = newSequence;
232 | }
233 | else {
234 | steps = [sequence arrayByAddingObjectsFromArray:steps];
235 | }
236 |
237 | [TutorialKit.sharedInstance.sequences setObject:@{TKSequence:steps,TKStep:@(curStep)} forKey:name];
238 | }
239 |
240 | ////////////////////////////////////////////////////////////////////////////////
241 | + (void)presentTutorialView:(TutorialKitView *)view withAnimation:(BOOL)animate
242 | {
243 | NSArray *windows = [UIApplication sharedApplication].windows;
244 | if(windows && windows.count > 0) {
245 | UIWindow *window = [windows objectAtIndex:0];
246 | [window addSubview:view];
247 | view.frame = window.bounds;
248 | [view setNeedsLayout];
249 |
250 | if(animate) {
251 | // fade in
252 | view.alpha = 0;
253 | [UIView animateWithDuration:0.5 animations:^{
254 | view.alpha = 1;
255 | } completion:nil];
256 | }
257 | else {
258 | view.alpha = 1.;
259 | }
260 | }
261 | }
262 |
263 | ////////////////////////////////////////////////////////////////////////////////
264 | + (void)setCurrentStep:(NSInteger)step forTutorial:(NSString *)name
265 | {
266 | NSMutableDictionary *sequence = [TutorialKit.sharedInstance.sequences objectForKey:name];
267 | if(sequence) {
268 | if(![sequence isKindOfClass:NSMutableDictionary.class]) {
269 | sequence = sequence.mutableCopy;
270 | }
271 |
272 | [sequence setObject:@(step) forKey:TKStep];
273 |
274 | // save to NSUserDefaults
275 | NSMutableDictionary *steps = [NSUserDefaults.standardUserDefaults objectForKey:TKUserDefaultsKey];
276 | if(!steps) {
277 | steps = @{}.mutableCopy;
278 | }
279 | else {
280 | steps = steps.mutableCopy;
281 | }
282 | [steps setObject:@(step) forKey:name];
283 | [NSUserDefaults.standardUserDefaults setObject:steps forKey:TKUserDefaultsKey];
284 |
285 | [TutorialKit.sharedInstance.sequences setObject:sequence forKey:name];
286 | }
287 | }
288 |
289 | ////////////////////////////////////////////////////////////////////////////////
290 | + (void)setDefaultBlurAmount:(CGFloat)amount
291 | {
292 | TutorialKit.sharedInstance.blurAmount = MAX(0.0,MIN(1.0,amount));
293 | }
294 |
295 | ////////////////////////////////////////////////////////////////////////////////
296 | + (void)setDefaultMessageColor:(UIColor *)color
297 | {
298 | TutorialKit.sharedInstance.labelColor = color;
299 | }
300 |
301 | ////////////////////////////////////////////////////////////////////////////////
302 | + (void)setDefaultMessageFont:(UIFont *)font
303 | {
304 | TutorialKit.sharedInstance.labelFont = font;
305 | }
306 |
307 | ////////////////////////////////////////////////////////////////////////////////
308 | + (void)setDefaultTintColor:(UIColor *)color
309 | {
310 | TutorialKit.sharedInstance.backgroundTintColor = color;
311 | }
312 |
313 | #pragma mark - Private
314 |
315 | ////////////////////////////////////////////////////////////////////////////////
316 | - (UIColor *)backgroundTintColor
317 | {
318 | if(!_backgroundTintColor) {
319 | _backgroundTintColor = [UIColor colorWithWhite:1.f alpha:0.5];
320 | }
321 |
322 | return _backgroundTintColor;
323 | }
324 |
325 | ////////////////////////////////////////////////////////////////////////////////
326 | - (void)dismissTutorialView:(TutorialKitView *)view
327 | {
328 | if(view.sequenceName && view.sequenceStep >= 0) {
329 | // get the tutorial sequence with this name
330 | NSMutableDictionary *sequenceData = [TutorialKit.sharedInstance.sequences
331 | objectForKey:view.sequenceName];
332 | if(!sequenceData) {
333 | // assert?
334 | return;
335 | }
336 | else if(![sequenceData isKindOfClass:NSMutableDictionary.class]) {
337 | sequenceData = sequenceData.mutableCopy;
338 | }
339 |
340 | // increment the tutorial step
341 | [sequenceData setObject:@(view.sequenceStep + 1) forKey:TKStep];
342 |
343 | // save to NSUserDefaults
344 | NSMutableDictionary *steps = [NSUserDefaults.standardUserDefaults objectForKey:TKUserDefaultsKey];
345 | if(!steps) {
346 | steps = @{}.mutableCopy;
347 | }
348 | else {
349 | steps = steps.mutableCopy;
350 | }
351 | [steps setObject:@(view.sequenceStep + 1) forKey:view.sequenceName];
352 | [NSUserDefaults.standardUserDefaults setObject:steps forKey:TKUserDefaultsKey];
353 |
354 | // run the callback if one exists
355 | if(view.values && [view.values objectForKey:TKCompleteCallback]) {
356 | void (^callback)();
357 | callback = [view.values objectForKey:TKCompleteCallback];
358 | callback();
359 | }
360 | [TutorialKit.sharedInstance.sequences setObject:sequenceData
361 | forKey:view.sequenceName];
362 | }
363 | if(view == TutorialKit.sharedInstance.currentTutorialView) {
364 | TutorialKit.sharedInstance.currentTutorialView = nil;
365 | }
366 |
367 | [UIView animateWithDuration:0.5 animations:^{
368 | view.alpha = 0;
369 | } completion:^(BOOL finished) {
370 | if(TutorialKit.sharedInstance.shouldContinue) {
371 | [TutorialKit advanceTutorialSequenceWithName:view.sequenceName];
372 | }
373 | else {
374 | [view removeFromSuperview];
375 | }
376 | }];
377 | }
378 |
379 | ////////////////////////////////////////////////////////////////////////////////
380 | - (UIView *)findViewWithTag:(NSNumber *)tag
381 | {
382 | // look for the view with this tag
383 | __block UIView *tagView = nil;
384 | for(UIWindow *window in UIApplication.sharedApplication.windows) {
385 | [window.subviews enumerateObjectsUsingBlock:^(UIView *view, NSUInteger idx, BOOL *stop) {
386 | if(view.hidden || view.alpha == 0) return;
387 |
388 | tagView = [view findViewRecursively:^BOOL(UIView *subview, BOOL *stop) {
389 | if(subview.tag == tag.integerValue && !subview.hidden &&
390 | subview.alpha != 0) {
391 | *stop = YES;
392 | return NO;
393 | }
394 | // return yes if should recurse further
395 | return !subview.hidden && subview.alpha != 0;
396 | }];
397 |
398 | if(tagView) *stop = YES;
399 | }];
400 |
401 | if(tagView) break;
402 | }
403 |
404 | return tagView;
405 | }
406 |
407 | ////////////////////////////////////////////////////////////////////////////////
408 | - (UIColor *)labelColor
409 | {
410 | if(!_labelColor) {
411 | _labelColor = UIColor.grayColor;
412 | }
413 | return _labelColor;
414 | }
415 |
416 | ////////////////////////////////////////////////////////////////////////////////
417 | - (UIFont *)labelFont
418 | {
419 | if(!_labelFont) {
420 | _labelFont = [UIFont fontWithName:@"Helvetica" size:16];
421 | }
422 | return _labelFont;
423 | }
424 |
425 | ////////////////////////////////////////////////////////////////////////////////
426 | - (void)onTapGesture:(UITapGestureRecognizer *)gesture
427 | {
428 | if(!gesture.view || ![gesture.view isKindOfClass:TutorialKitView.class]) {
429 | // assert?
430 | return;
431 | }
432 |
433 | [self dismissTutorialView:(TutorialKitView *)gesture.view];
434 | }
435 |
436 | ////////////////////////////////////////////////////////////////////////////////
437 | - (NSMutableDictionary *)sequences
438 | {
439 | if(!_sequences) {
440 | _sequences = @{}.mutableCopy;
441 | }
442 | return _sequences;
443 | }
444 |
445 | ////////////////////////////////////////////////////////////////////////////////
446 | + (TutorialKit *)sharedInstance
447 | {
448 | static TutorialKit *tk = nil;
449 | static dispatch_once_t onceToken;
450 | dispatch_once(&onceToken, ^{
451 | if(!tk) tk = [[TutorialKit alloc] init];
452 | tk.currentTutorialView = nil;
453 | tk.blurAmount = 1.0;
454 | });
455 |
456 | return tk;
457 | }
458 |
459 | @end
460 |
--------------------------------------------------------------------------------
/Example/TutorialKitExample.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 46;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | E865480F596144E2AED0CD43 /* libPods.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 2749C4FF4C81468297A5699C /* libPods.a */; };
11 | FA4A63CF1911781F002B6960 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FA4A63CE1911781F002B6960 /* Foundation.framework */; };
12 | FA4A63D11911781F002B6960 /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FA4A63D01911781F002B6960 /* CoreGraphics.framework */; };
13 | FA4A63D31911781F002B6960 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FA4A63D21911781F002B6960 /* UIKit.framework */; };
14 | FA4A63D91911781F002B6960 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = FA4A63D71911781F002B6960 /* InfoPlist.strings */; };
15 | FA4A63DB1911781F002B6960 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = FA4A63DA1911781F002B6960 /* main.m */; };
16 | FA4A63DF1911781F002B6960 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = FA4A63DE1911781F002B6960 /* AppDelegate.m */; };
17 | FA4A63E11911781F002B6960 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = FA4A63E01911781F002B6960 /* Images.xcassets */; };
18 | FA4A63E81911781F002B6960 /* XCTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FA4A63E71911781F002B6960 /* XCTest.framework */; };
19 | FA4A63E91911781F002B6960 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FA4A63CE1911781F002B6960 /* Foundation.framework */; };
20 | FA4A63EA1911781F002B6960 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FA4A63D21911781F002B6960 /* UIKit.framework */; };
21 | FA4A63F21911781F002B6960 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = FA4A63F01911781F002B6960 /* InfoPlist.strings */; };
22 | FA4A63F41911781F002B6960 /* TutorialKitExampleTests.m in Sources */ = {isa = PBXBuildFile; fileRef = FA4A63F31911781F002B6960 /* TutorialKitExampleTests.m */; };
23 | FA4A63FF1911782D002B6960 /* ExampleViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = FA4A63FE1911782D002B6960 /* ExampleViewController.m */; };
24 | /* End PBXBuildFile section */
25 |
26 | /* Begin PBXContainerItemProxy section */
27 | FA4A63EB1911781F002B6960 /* PBXContainerItemProxy */ = {
28 | isa = PBXContainerItemProxy;
29 | containerPortal = FA4A63C31911781F002B6960 /* Project object */;
30 | proxyType = 1;
31 | remoteGlobalIDString = FA4A63CA1911781F002B6960;
32 | remoteInfo = TutorialKitExample;
33 | };
34 | /* End PBXContainerItemProxy section */
35 |
36 | /* Begin PBXFileReference section */
37 | 25916008E66E40DDA70FFF5A /* Pods.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = Pods.xcconfig; path = Pods/Pods.xcconfig; sourceTree = ""; };
38 | 2749C4FF4C81468297A5699C /* libPods.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libPods.a; sourceTree = BUILT_PRODUCTS_DIR; };
39 | FA4A63CB1911781F002B6960 /* TutorialKitExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TutorialKitExample.app; sourceTree = BUILT_PRODUCTS_DIR; };
40 | FA4A63CE1911781F002B6960 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; };
41 | FA4A63D01911781F002B6960 /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = System/Library/Frameworks/CoreGraphics.framework; sourceTree = SDKROOT; };
42 | FA4A63D21911781F002B6960 /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; };
43 | FA4A63D61911781F002B6960 /* TutorialKitExample-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "TutorialKitExample-Info.plist"; sourceTree = ""; };
44 | FA4A63D81911781F002B6960 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; };
45 | FA4A63DA1911781F002B6960 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; };
46 | FA4A63DC1911781F002B6960 /* TutorialKitExample-Prefix.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "TutorialKitExample-Prefix.pch"; sourceTree = ""; };
47 | FA4A63DD1911781F002B6960 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; };
48 | FA4A63DE1911781F002B6960 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; };
49 | FA4A63E01911781F002B6960 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; };
50 | FA4A63E61911781F002B6960 /* TutorialKitExampleTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = TutorialKitExampleTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
51 | FA4A63E71911781F002B6960 /* XCTest.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = XCTest.framework; path = Library/Frameworks/XCTest.framework; sourceTree = DEVELOPER_DIR; };
52 | FA4A63EF1911781F002B6960 /* TutorialKitExampleTests-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "TutorialKitExampleTests-Info.plist"; sourceTree = ""; };
53 | FA4A63F11911781F002B6960 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; };
54 | FA4A63F31911781F002B6960 /* TutorialKitExampleTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TutorialKitExampleTests.m; sourceTree = ""; };
55 | FA4A63FD1911782D002B6960 /* ExampleViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ExampleViewController.h; sourceTree = ""; };
56 | FA4A63FE1911782D002B6960 /* ExampleViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ExampleViewController.m; sourceTree = ""; };
57 | /* End PBXFileReference section */
58 |
59 | /* Begin PBXFrameworksBuildPhase section */
60 | FA4A63C81911781F002B6960 /* Frameworks */ = {
61 | isa = PBXFrameworksBuildPhase;
62 | buildActionMask = 2147483647;
63 | files = (
64 | FA4A63D11911781F002B6960 /* CoreGraphics.framework in Frameworks */,
65 | FA4A63D31911781F002B6960 /* UIKit.framework in Frameworks */,
66 | FA4A63CF1911781F002B6960 /* Foundation.framework in Frameworks */,
67 | E865480F596144E2AED0CD43 /* libPods.a in Frameworks */,
68 | );
69 | runOnlyForDeploymentPostprocessing = 0;
70 | };
71 | FA4A63E31911781F002B6960 /* Frameworks */ = {
72 | isa = PBXFrameworksBuildPhase;
73 | buildActionMask = 2147483647;
74 | files = (
75 | FA4A63E81911781F002B6960 /* XCTest.framework in Frameworks */,
76 | FA4A63EA1911781F002B6960 /* UIKit.framework in Frameworks */,
77 | FA4A63E91911781F002B6960 /* Foundation.framework in Frameworks */,
78 | );
79 | runOnlyForDeploymentPostprocessing = 0;
80 | };
81 | /* End PBXFrameworksBuildPhase section */
82 |
83 | /* Begin PBXGroup section */
84 | FA4A63C21911781F002B6960 = {
85 | isa = PBXGroup;
86 | children = (
87 | FA4A63D41911781F002B6960 /* TutorialKitExample */,
88 | FA4A63ED1911781F002B6960 /* TutorialKitExampleTests */,
89 | FA4A63CD1911781F002B6960 /* Frameworks */,
90 | FA4A63CC1911781F002B6960 /* Products */,
91 | 25916008E66E40DDA70FFF5A /* Pods.xcconfig */,
92 | );
93 | sourceTree = "";
94 | };
95 | FA4A63CC1911781F002B6960 /* Products */ = {
96 | isa = PBXGroup;
97 | children = (
98 | FA4A63CB1911781F002B6960 /* TutorialKitExample.app */,
99 | FA4A63E61911781F002B6960 /* TutorialKitExampleTests.xctest */,
100 | );
101 | name = Products;
102 | sourceTree = "";
103 | };
104 | FA4A63CD1911781F002B6960 /* Frameworks */ = {
105 | isa = PBXGroup;
106 | children = (
107 | FA4A63CE1911781F002B6960 /* Foundation.framework */,
108 | FA4A63D01911781F002B6960 /* CoreGraphics.framework */,
109 | FA4A63D21911781F002B6960 /* UIKit.framework */,
110 | FA4A63E71911781F002B6960 /* XCTest.framework */,
111 | 2749C4FF4C81468297A5699C /* libPods.a */,
112 | );
113 | name = Frameworks;
114 | sourceTree = "";
115 | };
116 | FA4A63D41911781F002B6960 /* TutorialKitExample */ = {
117 | isa = PBXGroup;
118 | children = (
119 | FA4A63DD1911781F002B6960 /* AppDelegate.h */,
120 | FA4A63DE1911781F002B6960 /* AppDelegate.m */,
121 | FA4A63FD1911782D002B6960 /* ExampleViewController.h */,
122 | FA4A63FE1911782D002B6960 /* ExampleViewController.m */,
123 | FA4A63E01911781F002B6960 /* Images.xcassets */,
124 | FA4A63D51911781F002B6960 /* Supporting Files */,
125 | );
126 | path = TutorialKitExample;
127 | sourceTree = "";
128 | };
129 | FA4A63D51911781F002B6960 /* Supporting Files */ = {
130 | isa = PBXGroup;
131 | children = (
132 | FA4A63D61911781F002B6960 /* TutorialKitExample-Info.plist */,
133 | FA4A63D71911781F002B6960 /* InfoPlist.strings */,
134 | FA4A63DA1911781F002B6960 /* main.m */,
135 | FA4A63DC1911781F002B6960 /* TutorialKitExample-Prefix.pch */,
136 | );
137 | name = "Supporting Files";
138 | sourceTree = "";
139 | };
140 | FA4A63ED1911781F002B6960 /* TutorialKitExampleTests */ = {
141 | isa = PBXGroup;
142 | children = (
143 | FA4A63F31911781F002B6960 /* TutorialKitExampleTests.m */,
144 | FA4A63EE1911781F002B6960 /* Supporting Files */,
145 | );
146 | path = TutorialKitExampleTests;
147 | sourceTree = "";
148 | };
149 | FA4A63EE1911781F002B6960 /* Supporting Files */ = {
150 | isa = PBXGroup;
151 | children = (
152 | FA4A63EF1911781F002B6960 /* TutorialKitExampleTests-Info.plist */,
153 | FA4A63F01911781F002B6960 /* InfoPlist.strings */,
154 | );
155 | name = "Supporting Files";
156 | sourceTree = "";
157 | };
158 | /* End PBXGroup section */
159 |
160 | /* Begin PBXNativeTarget section */
161 | FA4A63CA1911781F002B6960 /* TutorialKitExample */ = {
162 | isa = PBXNativeTarget;
163 | buildConfigurationList = FA4A63F71911781F002B6960 /* Build configuration list for PBXNativeTarget "TutorialKitExample" */;
164 | buildPhases = (
165 | A2CE6EB8970A49F6A11D5574 /* Check Pods Manifest.lock */,
166 | FA4A63C71911781F002B6960 /* Sources */,
167 | FA4A63C81911781F002B6960 /* Frameworks */,
168 | FA4A63C91911781F002B6960 /* Resources */,
169 | 7ECEA285A6F444158766D75A /* Copy Pods Resources */,
170 | );
171 | buildRules = (
172 | );
173 | dependencies = (
174 | );
175 | name = TutorialKitExample;
176 | productName = TutorialKitExample;
177 | productReference = FA4A63CB1911781F002B6960 /* TutorialKitExample.app */;
178 | productType = "com.apple.product-type.application";
179 | };
180 | FA4A63E51911781F002B6960 /* TutorialKitExampleTests */ = {
181 | isa = PBXNativeTarget;
182 | buildConfigurationList = FA4A63FA1911781F002B6960 /* Build configuration list for PBXNativeTarget "TutorialKitExampleTests" */;
183 | buildPhases = (
184 | FA4A63E21911781F002B6960 /* Sources */,
185 | FA4A63E31911781F002B6960 /* Frameworks */,
186 | FA4A63E41911781F002B6960 /* Resources */,
187 | );
188 | buildRules = (
189 | );
190 | dependencies = (
191 | FA4A63EC1911781F002B6960 /* PBXTargetDependency */,
192 | );
193 | name = TutorialKitExampleTests;
194 | productName = TutorialKitExampleTests;
195 | productReference = FA4A63E61911781F002B6960 /* TutorialKitExampleTests.xctest */;
196 | productType = "com.apple.product-type.bundle.unit-test";
197 | };
198 | /* End PBXNativeTarget section */
199 |
200 | /* Begin PBXProject section */
201 | FA4A63C31911781F002B6960 /* Project object */ = {
202 | isa = PBXProject;
203 | attributes = {
204 | LastUpgradeCheck = 0510;
205 | ORGANIZATIONNAME = TutorialKit;
206 | TargetAttributes = {
207 | FA4A63E51911781F002B6960 = {
208 | TestTargetID = FA4A63CA1911781F002B6960;
209 | };
210 | };
211 | };
212 | buildConfigurationList = FA4A63C61911781F002B6960 /* Build configuration list for PBXProject "TutorialKitExample" */;
213 | compatibilityVersion = "Xcode 3.2";
214 | developmentRegion = English;
215 | hasScannedForEncodings = 0;
216 | knownRegions = (
217 | en,
218 | );
219 | mainGroup = FA4A63C21911781F002B6960;
220 | productRefGroup = FA4A63CC1911781F002B6960 /* Products */;
221 | projectDirPath = "";
222 | projectRoot = "";
223 | targets = (
224 | FA4A63CA1911781F002B6960 /* TutorialKitExample */,
225 | FA4A63E51911781F002B6960 /* TutorialKitExampleTests */,
226 | );
227 | };
228 | /* End PBXProject section */
229 |
230 | /* Begin PBXResourcesBuildPhase section */
231 | FA4A63C91911781F002B6960 /* Resources */ = {
232 | isa = PBXResourcesBuildPhase;
233 | buildActionMask = 2147483647;
234 | files = (
235 | FA4A63D91911781F002B6960 /* InfoPlist.strings in Resources */,
236 | FA4A63E11911781F002B6960 /* Images.xcassets in Resources */,
237 | );
238 | runOnlyForDeploymentPostprocessing = 0;
239 | };
240 | FA4A63E41911781F002B6960 /* Resources */ = {
241 | isa = PBXResourcesBuildPhase;
242 | buildActionMask = 2147483647;
243 | files = (
244 | FA4A63F21911781F002B6960 /* InfoPlist.strings in Resources */,
245 | );
246 | runOnlyForDeploymentPostprocessing = 0;
247 | };
248 | /* End PBXResourcesBuildPhase section */
249 |
250 | /* Begin PBXShellScriptBuildPhase section */
251 | 7ECEA285A6F444158766D75A /* Copy Pods Resources */ = {
252 | isa = PBXShellScriptBuildPhase;
253 | buildActionMask = 2147483647;
254 | files = (
255 | );
256 | inputPaths = (
257 | );
258 | name = "Copy Pods Resources";
259 | outputPaths = (
260 | );
261 | runOnlyForDeploymentPostprocessing = 0;
262 | shellPath = /bin/sh;
263 | shellScript = "\"${SRCROOT}/Pods/Pods-resources.sh\"\n";
264 | showEnvVarsInLog = 0;
265 | };
266 | A2CE6EB8970A49F6A11D5574 /* Check Pods Manifest.lock */ = {
267 | isa = PBXShellScriptBuildPhase;
268 | buildActionMask = 2147483647;
269 | files = (
270 | );
271 | inputPaths = (
272 | );
273 | name = "Check Pods Manifest.lock";
274 | outputPaths = (
275 | );
276 | runOnlyForDeploymentPostprocessing = 0;
277 | shellPath = /bin/sh;
278 | shellScript = "diff \"${PODS_ROOT}/../Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [[ $? != 0 ]] ; then\n cat << EOM\nerror: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\nEOM\n exit 1\nfi\n";
279 | showEnvVarsInLog = 0;
280 | };
281 | /* End PBXShellScriptBuildPhase section */
282 |
283 | /* Begin PBXSourcesBuildPhase section */
284 | FA4A63C71911781F002B6960 /* Sources */ = {
285 | isa = PBXSourcesBuildPhase;
286 | buildActionMask = 2147483647;
287 | files = (
288 | FA4A63FF1911782D002B6960 /* ExampleViewController.m in Sources */,
289 | FA4A63DF1911781F002B6960 /* AppDelegate.m in Sources */,
290 | FA4A63DB1911781F002B6960 /* main.m in Sources */,
291 | );
292 | runOnlyForDeploymentPostprocessing = 0;
293 | };
294 | FA4A63E21911781F002B6960 /* Sources */ = {
295 | isa = PBXSourcesBuildPhase;
296 | buildActionMask = 2147483647;
297 | files = (
298 | FA4A63F41911781F002B6960 /* TutorialKitExampleTests.m in Sources */,
299 | );
300 | runOnlyForDeploymentPostprocessing = 0;
301 | };
302 | /* End PBXSourcesBuildPhase section */
303 |
304 | /* Begin PBXTargetDependency section */
305 | FA4A63EC1911781F002B6960 /* PBXTargetDependency */ = {
306 | isa = PBXTargetDependency;
307 | target = FA4A63CA1911781F002B6960 /* TutorialKitExample */;
308 | targetProxy = FA4A63EB1911781F002B6960 /* PBXContainerItemProxy */;
309 | };
310 | /* End PBXTargetDependency section */
311 |
312 | /* Begin PBXVariantGroup section */
313 | FA4A63D71911781F002B6960 /* InfoPlist.strings */ = {
314 | isa = PBXVariantGroup;
315 | children = (
316 | FA4A63D81911781F002B6960 /* en */,
317 | );
318 | name = InfoPlist.strings;
319 | sourceTree = "";
320 | };
321 | FA4A63F01911781F002B6960 /* InfoPlist.strings */ = {
322 | isa = PBXVariantGroup;
323 | children = (
324 | FA4A63F11911781F002B6960 /* en */,
325 | );
326 | name = InfoPlist.strings;
327 | sourceTree = "";
328 | };
329 | /* End PBXVariantGroup section */
330 |
331 | /* Begin XCBuildConfiguration section */
332 | FA4A63F51911781F002B6960 /* Debug */ = {
333 | isa = XCBuildConfiguration;
334 | buildSettings = {
335 | ALWAYS_SEARCH_USER_PATHS = NO;
336 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
337 | CLANG_CXX_LIBRARY = "libc++";
338 | CLANG_ENABLE_MODULES = YES;
339 | CLANG_ENABLE_OBJC_ARC = YES;
340 | CLANG_WARN_BOOL_CONVERSION = YES;
341 | CLANG_WARN_CONSTANT_CONVERSION = YES;
342 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
343 | CLANG_WARN_EMPTY_BODY = YES;
344 | CLANG_WARN_ENUM_CONVERSION = YES;
345 | CLANG_WARN_INT_CONVERSION = YES;
346 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
347 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
348 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
349 | COPY_PHASE_STRIP = NO;
350 | GCC_C_LANGUAGE_STANDARD = gnu99;
351 | GCC_DYNAMIC_NO_PIC = NO;
352 | GCC_OPTIMIZATION_LEVEL = 0;
353 | GCC_PREPROCESSOR_DEFINITIONS = (
354 | "DEBUG=1",
355 | "$(inherited)",
356 | );
357 | GCC_SYMBOLS_PRIVATE_EXTERN = NO;
358 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
359 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
360 | GCC_WARN_UNDECLARED_SELECTOR = YES;
361 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
362 | GCC_WARN_UNUSED_FUNCTION = YES;
363 | GCC_WARN_UNUSED_VARIABLE = YES;
364 | IPHONEOS_DEPLOYMENT_TARGET = 7.1;
365 | ONLY_ACTIVE_ARCH = YES;
366 | SDKROOT = iphoneos;
367 | TARGETED_DEVICE_FAMILY = "1,2";
368 | };
369 | name = Debug;
370 | };
371 | FA4A63F61911781F002B6960 /* Release */ = {
372 | isa = XCBuildConfiguration;
373 | buildSettings = {
374 | ALWAYS_SEARCH_USER_PATHS = NO;
375 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
376 | CLANG_CXX_LIBRARY = "libc++";
377 | CLANG_ENABLE_MODULES = YES;
378 | CLANG_ENABLE_OBJC_ARC = YES;
379 | CLANG_WARN_BOOL_CONVERSION = YES;
380 | CLANG_WARN_CONSTANT_CONVERSION = YES;
381 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
382 | CLANG_WARN_EMPTY_BODY = YES;
383 | CLANG_WARN_ENUM_CONVERSION = YES;
384 | CLANG_WARN_INT_CONVERSION = YES;
385 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
386 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
387 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
388 | COPY_PHASE_STRIP = YES;
389 | ENABLE_NS_ASSERTIONS = NO;
390 | GCC_C_LANGUAGE_STANDARD = gnu99;
391 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
392 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
393 | GCC_WARN_UNDECLARED_SELECTOR = YES;
394 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
395 | GCC_WARN_UNUSED_FUNCTION = YES;
396 | GCC_WARN_UNUSED_VARIABLE = YES;
397 | IPHONEOS_DEPLOYMENT_TARGET = 7.1;
398 | SDKROOT = iphoneos;
399 | TARGETED_DEVICE_FAMILY = "1,2";
400 | VALIDATE_PRODUCT = YES;
401 | };
402 | name = Release;
403 | };
404 | FA4A63F81911781F002B6960 /* Debug */ = {
405 | isa = XCBuildConfiguration;
406 | baseConfigurationReference = 25916008E66E40DDA70FFF5A /* Pods.xcconfig */;
407 | buildSettings = {
408 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
409 | ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage;
410 | GCC_PRECOMPILE_PREFIX_HEADER = YES;
411 | GCC_PREFIX_HEADER = "TutorialKitExample/TutorialKitExample-Prefix.pch";
412 | INFOPLIST_FILE = "TutorialKitExample/TutorialKitExample-Info.plist";
413 | PRODUCT_NAME = "$(TARGET_NAME)";
414 | WRAPPER_EXTENSION = app;
415 | };
416 | name = Debug;
417 | };
418 | FA4A63F91911781F002B6960 /* Release */ = {
419 | isa = XCBuildConfiguration;
420 | baseConfigurationReference = 25916008E66E40DDA70FFF5A /* Pods.xcconfig */;
421 | buildSettings = {
422 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
423 | ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage;
424 | GCC_PRECOMPILE_PREFIX_HEADER = YES;
425 | GCC_PREFIX_HEADER = "TutorialKitExample/TutorialKitExample-Prefix.pch";
426 | INFOPLIST_FILE = "TutorialKitExample/TutorialKitExample-Info.plist";
427 | PRODUCT_NAME = "$(TARGET_NAME)";
428 | WRAPPER_EXTENSION = app;
429 | };
430 | name = Release;
431 | };
432 | FA4A63FB1911781F002B6960 /* Debug */ = {
433 | isa = XCBuildConfiguration;
434 | buildSettings = {
435 | BUNDLE_LOADER = "$(BUILT_PRODUCTS_DIR)/TutorialKitExample.app/TutorialKitExample";
436 | FRAMEWORK_SEARCH_PATHS = (
437 | "$(SDKROOT)/Developer/Library/Frameworks",
438 | "$(inherited)",
439 | "$(DEVELOPER_FRAMEWORKS_DIR)",
440 | );
441 | GCC_PRECOMPILE_PREFIX_HEADER = YES;
442 | GCC_PREFIX_HEADER = "TutorialKitExample/TutorialKitExample-Prefix.pch";
443 | GCC_PREPROCESSOR_DEFINITIONS = (
444 | "DEBUG=1",
445 | "$(inherited)",
446 | );
447 | INFOPLIST_FILE = "TutorialKitExampleTests/TutorialKitExampleTests-Info.plist";
448 | PRODUCT_NAME = "$(TARGET_NAME)";
449 | TEST_HOST = "$(BUNDLE_LOADER)";
450 | WRAPPER_EXTENSION = xctest;
451 | };
452 | name = Debug;
453 | };
454 | FA4A63FC1911781F002B6960 /* Release */ = {
455 | isa = XCBuildConfiguration;
456 | buildSettings = {
457 | BUNDLE_LOADER = "$(BUILT_PRODUCTS_DIR)/TutorialKitExample.app/TutorialKitExample";
458 | FRAMEWORK_SEARCH_PATHS = (
459 | "$(SDKROOT)/Developer/Library/Frameworks",
460 | "$(inherited)",
461 | "$(DEVELOPER_FRAMEWORKS_DIR)",
462 | );
463 | GCC_PRECOMPILE_PREFIX_HEADER = YES;
464 | GCC_PREFIX_HEADER = "TutorialKitExample/TutorialKitExample-Prefix.pch";
465 | INFOPLIST_FILE = "TutorialKitExampleTests/TutorialKitExampleTests-Info.plist";
466 | PRODUCT_NAME = "$(TARGET_NAME)";
467 | TEST_HOST = "$(BUNDLE_LOADER)";
468 | WRAPPER_EXTENSION = xctest;
469 | };
470 | name = Release;
471 | };
472 | /* End XCBuildConfiguration section */
473 |
474 | /* Begin XCConfigurationList section */
475 | FA4A63C61911781F002B6960 /* Build configuration list for PBXProject "TutorialKitExample" */ = {
476 | isa = XCConfigurationList;
477 | buildConfigurations = (
478 | FA4A63F51911781F002B6960 /* Debug */,
479 | FA4A63F61911781F002B6960 /* Release */,
480 | );
481 | defaultConfigurationIsVisible = 0;
482 | defaultConfigurationName = Release;
483 | };
484 | FA4A63F71911781F002B6960 /* Build configuration list for PBXNativeTarget "TutorialKitExample" */ = {
485 | isa = XCConfigurationList;
486 | buildConfigurations = (
487 | FA4A63F81911781F002B6960 /* Debug */,
488 | FA4A63F91911781F002B6960 /* Release */,
489 | );
490 | defaultConfigurationIsVisible = 0;
491 | defaultConfigurationName = Release;
492 | };
493 | FA4A63FA1911781F002B6960 /* Build configuration list for PBXNativeTarget "TutorialKitExampleTests" */ = {
494 | isa = XCConfigurationList;
495 | buildConfigurations = (
496 | FA4A63FB1911781F002B6960 /* Debug */,
497 | FA4A63FC1911781F002B6960 /* Release */,
498 | );
499 | defaultConfigurationIsVisible = 0;
500 | defaultConfigurationName = Release;
501 | };
502 | /* End XCConfigurationList section */
503 | };
504 | rootObject = FA4A63C31911781F002B6960 /* Project object */;
505 | }
506 |
--------------------------------------------------------------------------------
/Classes/TutorialKitView.m:
--------------------------------------------------------------------------------
1 | /*
2 | TutorialKitView.m
3 | Created by Alex on 4/21/14.
4 | Copyright (c) 2014 DANIEL. All rights reserved.
5 |
6 | The MIT License (MIT)
7 |
8 | Permission is hereby granted, free of charge, to any person obtaining a copy of
9 | this software and associated documentation files (the "Software"), to deal in
10 | the Software without restriction, including without limitation the rights to
11 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
12 | the Software, and to permit persons to whom the Software is furnished to do so,
13 | subject to the following conditions:
14 |
15 | The above copyright notice and this permission notice shall be included in all
16 | copies or substantial portions of the Software.
17 |
18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
20 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
21 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
22 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
23 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
24 |
25 | Contains modified blur code from FXBlurView
26 | https://github.com/nicklockwood/FXBlurView
27 |
28 | Copyright (C) 2013 Charcoal Design
29 |
30 | FXBlurView License:
31 |
32 | This software is provided 'as-is', without any express or implied warranty.
33 | In no event will the authors be held liable for any damages arising from the
34 | use of this software.
35 |
36 | Permission is granted to anyone to use this software for any purpose, including
37 | commercial applications, and to alter it and redistribute it freely, subject to
38 | the following restrictions:
39 |
40 | The origin of this software must not be misrepresented; you must not claim that
41 | you wrote the original software. If you use this software in a product, an
42 | acknowledgment in the product documentation would be appreciated but is not
43 | required.
44 | Altered source versions must be plainly marked as such, and must not be
45 | misrepresented as being the original software.
46 | This notice may not be removed or altered from any source distribution.
47 | */
48 |
49 | #import "TutorialKitView.h"
50 | #import "TutorialKit.h"
51 | #import
52 | #import
53 |
54 | #define kTKGestureAnimationDuration 1.8
55 | #define kTKMessagePadding (UIDevice.currentDevice.userInterfaceIdiom == UIUserInterfaceIdiomPad ? 40.0 : 15.0)
56 |
57 | extern UIColor *gTutorialLabelColor;
58 | extern UIFont *gTutorialLabelFont;
59 |
60 | @implementation UIImage (FXBlurView)
61 |
62 | ////////////////////////////////////////////////////////////////////////////////
63 | // FROM FXBlurView (modified)
64 | - (UIImage *)blurredImageWithRadius:(CGFloat)radius iterations:(NSUInteger)iterations tintColor:(UIColor *)tintColor
65 | {
66 | //image must be nonzero size
67 | if (floorf(self.size.width) * floorf(self.size.height) <= 0.0f) return self;
68 |
69 | //boxsize must be an odd integer
70 | uint32_t boxSize = (uint32_t)(radius * self.scale);
71 | if (boxSize % 2 == 0) boxSize ++;
72 |
73 | //create image buffers
74 | CGImageRef imageRef = self.CGImage;
75 | vImage_Buffer buffer1, buffer2;
76 | buffer1.width = buffer2.width = CGImageGetWidth(imageRef);
77 | buffer1.height = buffer2.height = CGImageGetHeight(imageRef);
78 | buffer1.rowBytes = buffer2.rowBytes = CGImageGetBytesPerRow(imageRef);
79 | size_t bytes = buffer1.rowBytes * buffer1.height;
80 | buffer1.data = malloc(bytes);
81 | buffer2.data = malloc(bytes);
82 |
83 | //create temp buffer
84 | void *tempBuffer = malloc((size_t)vImageBoxConvolve_ARGB8888(&buffer1, &buffer2, NULL, 0, 0, boxSize, boxSize,
85 | NULL, kvImageEdgeExtend + kvImageGetTempBufferSize));
86 |
87 | //copy image data
88 | CFDataRef dataSource = CGDataProviderCopyData(CGImageGetDataProvider(imageRef));
89 | memcpy(buffer1.data, CFDataGetBytePtr(dataSource), bytes);
90 | CFRelease(dataSource);
91 |
92 | for (NSUInteger i = 0; i < iterations; i++) {
93 | //perform blur
94 | vImageBoxConvolve_ARGB8888(&buffer1, &buffer2, tempBuffer, 0, 0, boxSize, boxSize, NULL, kvImageEdgeExtend);
95 |
96 | //swap buffers
97 | void *temp = buffer1.data;
98 | buffer1.data = buffer2.data;
99 | buffer2.data = temp;
100 | }
101 |
102 | //free buffers
103 | free(buffer2.data);
104 | free(tempBuffer);
105 |
106 | //create image context from buffer
107 | CGContextRef ctx = CGBitmapContextCreate(buffer1.data, buffer1.width, buffer1.height,
108 | 8, buffer1.rowBytes, CGImageGetColorSpace(imageRef),
109 | CGImageGetBitmapInfo(imageRef));
110 |
111 | //apply tint
112 | if (tintColor && CGColorGetAlpha(tintColor.CGColor) > 0.0f)
113 | {
114 | // CGContextSetFillColorWithColor(ctx, [tintColor colorWithAlphaComponent:0.5].CGColor);
115 | // CGContextSetBlendMode(ctx, kCGBlendModePlusLighter);
116 |
117 | // CGContextSetFillColorWithColor(ctx, tintColor.CGColor);
118 | // prevent going full alpha
119 | CGContextSetFillColorWithColor(ctx, [tintColor colorWithAlphaComponent:CGColorGetAlpha(tintColor.CGColor) * 0.9].CGColor);
120 | CGContextSetBlendMode(ctx, kCGBlendModeNormal);
121 | CGContextFillRect(ctx, CGRectMake(0, 0, buffer1.width, buffer1.height));
122 | }
123 |
124 | //create image from context
125 | imageRef = CGBitmapContextCreateImage(ctx);
126 | UIImage *image = [UIImage imageWithCGImage:imageRef scale:self.scale orientation:self.imageOrientation];
127 | CGImageRelease(imageRef);
128 | CGContextRelease(ctx);
129 | free(buffer1.data);
130 | return image;
131 | }
132 |
133 | @end
134 |
135 | @interface TutorialKitView()
136 | @property (nonatomic, weak) UILabel *messageLabel;
137 | @property (nonatomic) CGPoint gestureEnd;
138 | @property (nonatomic) CGPoint gestureStart;
139 | @property (nonatomic) BOOL gesturePointsRelative;
140 | @property (nonatomic) CGPoint messageCenter;
141 | @property (nonatomic) BOOL messageCenterRelative;
142 | @property (nonatomic) CGPoint highlightPoint;
143 | @property (nonatomic) BOOL highlightPointRelative;
144 | @property (nonatomic) float highlightRadius;
145 | @property (nonatomic, weak) UIView *highlightView;
146 | @property (nonatomic, weak) UIView *gestureView;
147 | @property (nonatomic) BOOL updating;
148 | @property (nonatomic) CGFloat blurRadius;
149 | @property (nonatomic) NSInteger blurIterations;
150 | @property (nonatomic, weak) UIImageView *blurView;
151 | @end
152 |
153 | @implementation TutorialKitView
154 |
155 | ////////////////////////////////////////////////////////////////////////////////
156 | + (instancetype) tutorialViewWithMessage:(NSString *)message
157 | messageCenter:(CGPoint)messageCenter
158 | messageCenterRelative:(BOOL)relativeMessageCenter
159 | font:(UIFont *)font
160 | color:(UIColor *)color
161 | highlightView:(UIView *)view
162 | highlightPoint:(CGPoint)point
163 | highlightPointRelative:(BOOL)relativeHighlightPoint
164 | highlightRadius:(float)radius
165 | {
166 | TutorialKitView *tkv = [[TutorialKitView alloc]
167 | initWithFrame:UIScreen.mainScreen.bounds];
168 | if(message && tkv.messageLabel) {
169 | [tkv.messageLabel setText:message];
170 | if(color) tkv.messageLabel.textColor = color;
171 | if(font) tkv.messageLabel.font = font;
172 | }
173 | tkv.messageCenterRelative = relativeMessageCenter;
174 | tkv.messageCenter = messageCenter;
175 | tkv.highlightView = view;
176 | tkv.highlightPoint = point;
177 | tkv.highlightPointRelative = relativeHighlightPoint;
178 | tkv.highlightRadius = radius;
179 | tkv.gestureView.hidden = YES;
180 | return tkv;
181 | }
182 |
183 | ////////////////////////////////////////////////////////////////////////////////
184 | + (instancetype) tutorialViewWithMessage:(NSString *)message
185 | messageCenter:(CGPoint)messageCenter
186 | messageCenterRelative:(BOOL)relativeMessageCenter
187 | font:(UIFont *)font
188 | color:(UIColor *)color
189 | swipeGestureStart:(CGPoint)start
190 | swipeGestureEnd:(CGPoint)end
191 | swipePositionsRelative:(BOOL)relativeSwipePositions
192 | highlightRadius:(float)radius
193 | {
194 | TutorialKitView *tkv = [[TutorialKitView alloc]
195 | initWithFrame:UIScreen.mainScreen.bounds];
196 | if(message && tkv.messageLabel) {
197 | [tkv.messageLabel setText:message];
198 | if(color) tkv.messageLabel.textColor = color;
199 | if(font) tkv.messageLabel.font = font;
200 | }
201 | tkv.gesturePointsRelative = relativeSwipePositions;
202 | tkv.gestureStart = start;
203 | tkv.gestureEnd = end;
204 | tkv.messageCenter = messageCenter;
205 | tkv.messageCenterRelative = relativeMessageCenter;
206 | tkv.highlightPoint = CGPointMake((start.x + end.x) / 2.f, (start.y + end.y) / 2.f);
207 | tkv.highlightRadius = radius;
208 | tkv.gestureView.hidden = CGPointEqualToPoint(tkv.gestureStart, tkv.gestureEnd) && CGPointEqualToPoint(tkv.gestureStart, CGPointZero);
209 | tkv.gestureView.center = relativeSwipePositions ? [tkv getAbsolutePoint:start] : start;
210 | [tkv animateGesture];
211 | return tkv;
212 | }
213 |
214 | ////////////////////////////////////////////////////////////////////////////////
215 | + (instancetype) tutorialViewWithDictionary:(NSDictionary *)values
216 | {
217 | // display this tutorial and advance
218 | CGPoint msgPoint = CGPointZero;
219 | BOOL msgPointRelative = NO;
220 | if([values objectForKey:TKMessagePoint]) {
221 | msgPoint = [[values objectForKey:TKMessagePoint] CGPointValue];
222 | }
223 | else if([values objectForKey:TKMessageRelativePoint]) {
224 | msgPoint = [[values objectForKey:TKMessageRelativePoint] CGPointValue];
225 | msgPointRelative = YES;
226 | }
227 |
228 | CGPoint highlightPoint = CGPointZero;
229 | BOOL highlightPointRelative = NO;
230 | if([values objectForKey:TKHighlightPoint]) {
231 | highlightPoint = [[values objectForKey:TKHighlightPoint] CGPointValue];
232 | }
233 | else if([values objectForKey:TKHighlightRelativePoint]) {
234 | highlightPoint = [[values objectForKey:TKHighlightRelativePoint] CGPointValue];
235 | highlightPointRelative = YES;
236 | }
237 |
238 | CGFloat radius = 0.0f;
239 | if([values objectForKey:TKHighlightRadius]) {
240 | radius = [[values objectForKey:TKHighlightRadius] floatValue];
241 | }
242 |
243 | TutorialKitView *tkv = nil;
244 | if([values objectForKey:TKHighlightView]) {
245 | tkv = [TutorialKitView tutorialViewWithMessage:[values objectForKey:TKMessage]
246 | messageCenter:msgPoint
247 | messageCenterRelative:msgPointRelative
248 | font:[values objectForKey:TKMessageFont]
249 | color:[values objectForKey:TKMessageColor]
250 | highlightView:[values objectForKey:TKHighlightView]
251 | highlightPoint:highlightPoint
252 | highlightPointRelative:highlightPointRelative
253 | highlightRadius:radius];
254 | }
255 | else {
256 | CGPoint swipeStart = CGPointZero;
257 | BOOL swipePointsRelative = NO;
258 | if([values objectForKey:TKSwipeGestureStartPoint]) {
259 | swipeStart = [[values objectForKey:TKSwipeGestureStartPoint] CGPointValue];
260 | }
261 | else if([values objectForKey:TKSwipeGestureRelativeStartPoint]) {
262 | swipeStart = [[values objectForKey:TKSwipeGestureRelativeStartPoint] CGPointValue];
263 | swipePointsRelative = YES;
264 | }
265 |
266 | CGPoint swipeEnd = CGPointZero;
267 | if([values objectForKey:TKSwipeGestureEndPoint]) {
268 | swipeEnd = [[values objectForKey:TKSwipeGestureEndPoint] CGPointValue];
269 | }
270 | else if([values objectForKey:TKSwipeGestureRelativeEndPoint]) {
271 | swipeEnd = [[values objectForKey:TKSwipeGestureRelativeEndPoint] CGPointValue];
272 | swipePointsRelative = YES;
273 | }
274 |
275 |
276 | tkv = [TutorialKitView tutorialViewWithMessage:[values objectForKey:TKMessage]
277 | messageCenter:msgPoint
278 | messageCenterRelative:msgPointRelative
279 | font:[values objectForKey:TKMessageFont]
280 | color:[values objectForKey:TKMessageColor]
281 | swipeGestureStart:swipeStart
282 | swipeGestureEnd:swipeEnd
283 | swipePositionsRelative:swipePointsRelative
284 | highlightRadius:radius];
285 | }
286 |
287 | if(tkv) {
288 | if([values objectForKey:TKBlurAmount]) {
289 | tkv.blurAmount = [[values objectForKey:TKBlurAmount] floatValue];
290 | }
291 | tkv.values = values;
292 | }
293 | return tkv;
294 | }
295 |
296 | ////////////////////////////////////////////////////////////////////////////////
297 | - (id)initWithFrame:(CGRect)frame
298 | {
299 | self = [super initWithFrame:frame];
300 | if (self) {
301 | self.backgroundColor = UIColor.clearColor;
302 |
303 | self.blurAmount = 1.0;
304 | self.blurIterations = 3;
305 | self.blurRadius = 40.f;
306 |
307 | self.gestureStart = CGPointZero;
308 | self.gestureEnd = CGPointZero;
309 |
310 | self.highlightView = nil;
311 |
312 | self.sequenceName = nil;
313 | self.sequenceStep = 0;
314 |
315 | self.tintColor = [UIColor colorWithWhite:1.0 alpha:0.5];
316 |
317 | self.updating = NO;
318 |
319 | UIImageView *blur = [[UIImageView alloc] initWithFrame:frame];
320 | [self addSubview:blur];
321 | self.blurView = blur;
322 |
323 | // touch indicator
324 | CGFloat radius = 36.f;
325 | if (UIDevice.currentDevice.userInterfaceIdiom == UIUserInterfaceIdiomPhone) {
326 | radius = 20.f;
327 | }
328 |
329 | UIView *gesture = [[UIView alloc] initWithFrame:CGRectMake(0,0,radius*2,radius*2)];
330 | gesture.backgroundColor = [UIColor colorWithWhite:0.5 alpha:0.7];
331 | gesture.layer.cornerRadius = radius;
332 | gesture.layer.masksToBounds = YES;
333 | gesture.alpha = 0;
334 | gesture.userInteractionEnabled = NO;
335 | [self addSubview:gesture];
336 | self.gestureView = gesture;
337 |
338 | // create the message label and add to the view
339 | UILabel *label = [[UILabel alloc] initWithFrame:CGRectZero];
340 | label.textColor = UIColor.blackColor;
341 | label.backgroundColor = UIColor.clearColor;
342 | label.numberOfLines = 0;
343 | [self addSubview:label];
344 |
345 | self.messageLabel = label;
346 |
347 | self.userInteractionEnabled = YES;
348 | self.exclusiveTouch = NO;
349 |
350 | // listen for orientation changes
351 | [NSNotificationCenter.defaultCenter
352 | addObserver:self
353 | selector:@selector(onApplicationDidChangeStatusBarOrientationNotification)
354 | name:UIApplicationDidChangeStatusBarOrientationNotification
355 | object:nil];
356 | }
357 | return self;
358 | }
359 |
360 | ////////////////////////////////////////////////////////////////////////////////
361 | - (void)dealloc
362 | {
363 | [NSNotificationCenter.defaultCenter
364 | removeObserver:self
365 | name:UIApplicationDidChangeStatusBarOrientationNotification
366 | object:nil];
367 | }
368 |
369 | ////////////////////////////////////////////////////////////////////////////////
370 | - (void)layoutSubviews
371 | {
372 | [self updateRotation];
373 | [super layoutSubviews];
374 |
375 | if(self.messageLabel && self.messageLabel.text) {
376 | [self.messageLabel sizeToFit];
377 | CGFloat maxWidth = self.frame.size.width - kTKMessagePadding * 2.f;
378 | if(self.messageLabel.frame.size.width > maxWidth) {
379 | CGSize fit = [self.messageLabel sizeThatFits:CGSizeMake(maxWidth, 99999.f)];
380 | self.messageLabel.frame = CGRectMake(0,0,fit.width,fit.height);
381 | self.messageLabel.textAlignment = NSTextAlignmentCenter;
382 | }
383 |
384 | self.messageLabel.center = self.messageCenterRelative ? [self getAbsolutePoint:self.messageCenter] : self.messageCenter;
385 |
386 | // prevent aliasing
387 | CGRect messageFrame = self.messageLabel.frame;
388 | messageFrame.origin.x = floor(messageFrame.origin.x);
389 | messageFrame.origin.y = floor(messageFrame.origin.y);
390 | self.messageLabel.frame = messageFrame;
391 | }
392 |
393 | self.blurView.frame = self.bounds;
394 |
395 | [self updateAsynchronously:YES completion:nil];
396 | }
397 |
398 | ////////////////////////////////////////////////////////////////////////////////
399 | - (void)applyMaskToImage:(UIImageView *)imageView
400 | {
401 | UIGraphicsBeginImageContext(imageView.frame.size);
402 | CGContextRef context = UIGraphicsGetCurrentContext();
403 |
404 | UIImage *maskImage = nil;
405 |
406 | if(self.highlightView) {
407 | CGContextSetRGBFillColor(context, 1.0f, 1.0f, 1.0f, 1.0f);
408 | CGContextFillRect(context, imageView.bounds);
409 |
410 | CGRect bounds = [self.highlightView.layer convertRect:self.highlightView.layer.bounds toLayer:self.layer];
411 | CGContextTranslateCTM(context, bounds.origin.x, bounds.origin.y);
412 | CGContextSetBlendMode(context, kCGBlendModeClear);
413 | [self.highlightView.layer renderInContext:context];
414 | }
415 | else {
416 | CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
417 | CGFloat locations[3] = { 0.0f, 0.5f, 1.0f };
418 | CFArrayRef colors = (__bridge CFArrayRef)@[
419 | (__bridge id)[UIColor clearColor].CGColor,
420 | (__bridge id)[UIColor clearColor].CGColor,
421 | (__bridge id)[UIColor whiteColor].CGColor
422 | ];
423 | CGGradientRef gradient = CGGradientCreateWithColors(colorSpace,
424 | colors,
425 | locations);
426 | CGPoint highlightPoint = self.highlightPointRelative ?
427 | [self getAbsolutePoint:self.highlightPoint] :
428 | self.highlightPoint;
429 |
430 | CGContextDrawRadialGradient(context,
431 | gradient,
432 | highlightPoint,
433 | 0,
434 | highlightPoint,
435 | MAX(10,self.highlightRadius),
436 | kCGGradientDrawsAfterEndLocation);
437 |
438 | CGGradientRelease(gradient);
439 | CGColorSpaceRelease(colorSpace);
440 | }
441 |
442 | maskImage = UIGraphicsGetImageFromCurrentImageContext();
443 |
444 | UIGraphicsEndImageContext();
445 |
446 | if(maskImage) {
447 | CALayer *layerMask = CALayer.layer;
448 | layerMask.frame = imageView.bounds;
449 | layerMask.contents = (id)maskImage.CGImage;
450 | imageView.layer.mask = layerMask;
451 | }
452 | }
453 |
454 | ////////////////////////////////////////////////////////////////////////////////
455 | - (void)drawTintInContext:(CGContextRef)context
456 | {
457 | CGFloat red = 0, green = 0, blue = 0, alpha = 0;
458 | [self.tintColor getRed:&red green:&green blue:&blue alpha:&alpha];
459 | CGContextSetRGBFillColor(context, red, green, blue, alpha);
460 | CGContextFillRect(context, self.layer.bounds);
461 | }
462 |
463 | ////////////////////////////////////////////////////////////////////////////////
464 | - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
465 | {
466 | // pass through and dismiss!
467 | self.gestureView.hidden = YES;
468 | [TutorialKit dismissCurrentTutorialView];
469 | return nil;
470 | }
471 |
472 | ////////////////////////////////////////////////////////////////////////////////
473 | // FROM FXBlurView (modified)
474 | - (NSArray *)prepareUnderlyingViewForSnapshot
475 | {
476 | __strong CALayer *blurlayer = self.layer;
477 | __strong CALayer *underlyingLayer = self.superview.layer;
478 | while (blurlayer.superlayer && blurlayer.superlayer != underlyingLayer) {
479 | blurlayer = blurlayer.superlayer;
480 | }
481 |
482 | NSMutableArray *layers = [NSMutableArray array];
483 | NSUInteger index = [underlyingLayer.sublayers indexOfObject:blurlayer];
484 | if (index != NSNotFound) {
485 | for (NSUInteger i = index; i < [underlyingLayer.sublayers count]; i++) {
486 | CALayer *layer = underlyingLayer.sublayers[i];
487 | if (!layer.hidden) {
488 | layer.hidden = YES;
489 | [layers addObject:layer];
490 | }
491 | }
492 | }
493 | return layers;
494 | }
495 |
496 | ////////////////////////////////////////////////////////////////////////////////
497 | - (void)onApplicationDidChangeStatusBarOrientationNotification
498 | {
499 | if(self.alpha <= 0.f) return;
500 |
501 | [UIView animateWithDuration:0.25 animations:^{
502 | self.alpha = 0;
503 | } completion:^(BOOL finished) {
504 | [self updateRotation];
505 | [self setNeedsLayout];
506 | [UIView animateWithDuration:0.5 animations:^{
507 | self.alpha = 1.0;
508 | }];
509 | }];
510 | }
511 |
512 | ////////////////////////////////////////////////////////////////////////////////
513 | - (void)setBlurAmount:(CGFloat)blurAmount
514 | {
515 | _blurAmount = blurAmount;
516 | self.blurRadius = blurAmount * 40.f;
517 | }
518 |
519 | ////////////////////////////////////////////////////////////////////////////////
520 | // FROM FXBlurView (modified)
521 | - (UIImage *)snapshotOfUnderlyingView
522 | {
523 | __strong CALayer *blurLayer = self.layer;
524 | __strong CALayer *underlyingLayer = self.superview.layer;
525 | CGRect bounds = blurLayer.bounds;
526 |
527 | CGFloat scale = 0.5;
528 | if (self.blurIterations) {
529 | CGFloat blockSize = 12.0f/self.blurIterations;
530 | scale = blockSize/MAX(blockSize * 2, self.blurRadius);
531 | scale = 1.0f/floorf(1.0f/scale);
532 | }
533 |
534 | CGSize size = bounds.size;
535 | if (self.contentMode == UIViewContentModeScaleToFill ||
536 | self.contentMode == UIViewContentModeScaleAspectFill ||
537 | self.contentMode == UIViewContentModeScaleAspectFit ||
538 | self.contentMode == UIViewContentModeRedraw) {
539 | //prevents edge artefacts
540 | size.width = floorf(size.width * scale) / scale;
541 | size.height = floorf(size.height * scale) / scale;
542 | }
543 | else if ([[UIDevice currentDevice].systemVersion floatValue] < 7.0f && [UIScreen mainScreen].scale == 1.0f) {
544 | //prevents pixelation on old devices
545 | scale = 1.0f;
546 | }
547 |
548 | UIInterfaceOrientation orientation = UIApplication.sharedApplication.statusBarOrientation;
549 |
550 | CGAffineTransform transform = CGAffineTransformIdentity;
551 | if(orientation == UIInterfaceOrientationLandscapeLeft) {
552 | transform = CGAffineTransformRotate(transform, M_PI / 2.f);
553 | transform = CGAffineTransformTranslate(transform, 0, -size.width);
554 | } else if(orientation == UIInterfaceOrientationLandscapeRight) {
555 | transform = CGAffineTransformRotate(transform, -M_PI / 2.f);
556 | transform = CGAffineTransformTranslate(transform, -size.height, 0);
557 | }
558 | else if(orientation == UIInterfaceOrientationPortraitUpsideDown) {
559 | transform = CGAffineTransformRotate(transform, M_PI);
560 | transform = CGAffineTransformTranslate(transform, -size.width, -size.height);
561 | }
562 |
563 | UIGraphicsBeginImageContextWithOptions(size, YES, scale);
564 |
565 | CGContextRef context = UIGraphicsGetCurrentContext();
566 | if(!context) return nil;
567 | CGContextConcatCTM(context, transform);
568 |
569 | NSArray *hiddenViews = [self prepareUnderlyingViewForSnapshot];
570 |
571 | [underlyingLayer renderInContext:context];
572 | for (CALayer *layer in hiddenViews) {
573 | layer.hidden = NO;
574 | }
575 | //[self drawTintInContext:context];
576 |
577 | UIImage *snapshot = UIGraphicsGetImageFromCurrentImageContext();
578 | UIGraphicsEndImageContext();
579 | return snapshot;
580 | }
581 |
582 | ////////////////////////////////////////////////////////////////////////////////
583 | - (void)animateGesture
584 | {
585 | if(self.gestureView.hidden) return;
586 |
587 | self.gestureView.center = self.gesturePointsRelative ? [self getAbsolutePoint:self.gestureStart] : self.gestureStart;
588 | [UIView animateWithDuration:kTKGestureAnimationDuration/3.f animations:^{
589 | self.gestureView.alpha = 1.0;
590 | } completion:^(BOOL finished) {
591 | [UIView animateWithDuration:kTKGestureAnimationDuration/3.f animations:^{
592 | self.gestureView.center = self.gesturePointsRelative ? [self getAbsolutePoint:self.gestureEnd] : self.gestureEnd;
593 | } completion:^(BOOL finished) {
594 | [UIView animateWithDuration:kTKGestureAnimationDuration/3.f animations:^{
595 | self.gestureView.alpha = 0;
596 | } completion:^(BOOL finished) {
597 | [self animateGesture];
598 | }];
599 | }];
600 | }];
601 | }
602 |
603 | ////////////////////////////////////////////////////////////////////////////////
604 | - (void)applyBlurImage:(UIImage *)image completion:(void (^)())completion
605 | {
606 | [self.blurView setImage:image];
607 | [self.blurView setContentScaleFactor:image.scale];
608 |
609 | // only apply mask if we have a highlight radius or a view to highlight
610 | if(self.highlightRadius > 0 || self.highlightView) {
611 | [self applyMaskToImage:self.blurView];
612 | }
613 |
614 | if (completion) {
615 | completion();
616 | }
617 | }
618 |
619 | ////////////////////////////////////////////////////////////////////////////////
620 | - (UIImage *)blurImage:(UIImage *)snapshot
621 | {
622 | return [snapshot blurredImageWithRadius:self.blurRadius
623 | iterations:self.blurIterations
624 | tintColor:self.tintColor];
625 | }
626 |
627 | ////////////////////////////////////////////////////////////////////////////////
628 | - (CGPoint)getAbsolutePoint:(CGPoint)relative
629 | {
630 | UIInterfaceOrientation orientation = UIApplication.sharedApplication.statusBarOrientation;
631 | CGSize size = self.bounds.size;
632 |
633 | // swap dimensions if bounds are not updated yet
634 | if((orientation == UIInterfaceOrientationLandscapeLeft ||
635 | orientation == UIInterfaceOrientationLandscapeRight) &&
636 | size.height > size.width) {
637 | CGFloat tmp = size.width;
638 | size.width = size.height;
639 | size.height = tmp;
640 | }
641 |
642 | return CGPointMake(MAX(0.f,MIN(1.f,relative.x)) * size.width,
643 | MAX(0.f,MIN(1.f,relative.y)) * size.height
644 | );
645 | }
646 |
647 | ////////////////////////////////////////////////////////////////////////////////
648 | - (void)updateAsynchronously:(BOOL)async completion:(void (^)())completion
649 | {
650 | if (self.blurAmount > 0.0 && !self.updating) {
651 | UIImage *snapshot = [self snapshotOfUnderlyingView];
652 | if (async) {
653 | dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
654 | UIImage *blurredImage = [self blurImage:snapshot];
655 | dispatch_sync(dispatch_get_main_queue(), ^{
656 | [self applyBlurImage:blurredImage completion:completion];
657 | });
658 | });
659 | }
660 | else {
661 | [self applyBlurImage:[self blurImage:snapshot] completion:completion];
662 | }
663 | }
664 | else if (completion) {
665 | completion();
666 | }
667 | }
668 |
669 | ////////////////////////////////////////////////////////////////////////////////
670 | - (void)updateRotation
671 | {
672 | UIInterfaceOrientation orientation = UIApplication.sharedApplication.statusBarOrientation;
673 | CGFloat angle = 0.f;
674 | BOOL swap = orientation == UIInterfaceOrientationLandscapeLeft ||
675 | orientation == UIInterfaceOrientationLandscapeRight;
676 | switch (orientation) {
677 | default:
678 | case UIInterfaceOrientationPortrait: angle = 0.f; break;
679 | case UIInterfaceOrientationPortraitUpsideDown: angle = M_PI; break;
680 | case UIInterfaceOrientationLandscapeLeft: angle = -M_PI/2.f; break;
681 | case UIInterfaceOrientationLandscapeRight: angle = M_PI/2.f; break;
682 | }
683 | self.transform = CGAffineTransformMakeRotation(angle);
684 |
685 | if(swap) {
686 | self.bounds = CGRectMake(0,
687 | 0,
688 | self.superview.frame.size.height,
689 | self.superview.frame.size.width);
690 | }
691 | else {
692 | self.frame = self.superview.frame;
693 | }
694 | }
695 |
696 | @end
697 |
--------------------------------------------------------------------------------