├── .gitignore ├── Assets └── tutorialkit.gif ├── CHANGELOG.md ├── Classes ├── TutorialKit.h ├── TutorialKit.m ├── TutorialKitView.h ├── TutorialKitView.m ├── UIView+Recursion.h ├── UIView+Recursion.m ├── ios │ └── .gitkeep └── osx │ └── .gitkeep ├── Example ├── Podfile ├── TutorialKitExample.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ └── contents.xcworkspacedata ├── TutorialKitExample.xcworkspace │ └── contents.xcworkspacedata ├── TutorialKitExample │ ├── AppDelegate.h │ ├── AppDelegate.m │ ├── ExampleViewController.h │ ├── ExampleViewController.m │ ├── Images.xcassets │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ └── LaunchImage.launchimage │ │ │ └── Contents.json │ ├── TutorialKitExample-Info.plist │ ├── TutorialKitExample-Prefix.pch │ ├── en.lproj │ │ └── InfoPlist.strings │ └── main.m └── TutorialKitExampleTests │ ├── TutorialKitExampleTests-Info.plist │ ├── TutorialKitExampleTests.m │ └── en.lproj │ └── InfoPlist.strings ├── LICENSE ├── README.md ├── Rakefile └── TutorialKit.podspec /.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 | -------------------------------------------------------------------------------- /Assets/tutorialkit.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lostinthepines/TutorialKit/4674ef74fac934ebe4b56f4d7cd9ae8674d7858b/Assets/tutorialkit.gif -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # TutorialKit CHANGELOG 2 | 3 | ## 0.1.0 4 | 5 | Initial release. 6 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/ios/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lostinthepines/TutorialKit/4674ef74fac934ebe4b56f4d7cd9ae8674d7858b/Classes/ios/.gitkeep -------------------------------------------------------------------------------- /Classes/osx/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lostinthepines/TutorialKit/4674ef74fac934ebe4b56f4d7cd9ae8674d7858b/Classes/osx/.gitkeep -------------------------------------------------------------------------------- /Example/Podfile: -------------------------------------------------------------------------------- 1 | pod "TutorialKit", :path => "../" 2 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Example/TutorialKitExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Example/TutorialKitExample.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /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/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.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/TutorialKitExample/en.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* Localized versions of Info.plist keys */ 2 | 3 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /Example/TutorialKitExampleTests/en.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* Localized versions of Info.plist keys */ 2 | 3 | -------------------------------------------------------------------------------- /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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TutorialKit 2 | 3 | [![Version](http://cocoapod-badges.herokuapp.com/v/TutorialKit/badge.png)](http://cocoadocs.org/docsets/TutorialKit) 4 | [![Platform](http://cocoapod-badges.herokuapp.com/p/TutorialKit/badge.png)](http://cocoadocs.org/docsets/TutorialKit) 5 | 6 | ![alt tag](https://github.com/lostinthepines/TutorialKit/raw/master/Assets/tutorialkit.gif) 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 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------