├── .DS_Store ├── .gitignore ├── LICENSE ├── OCExpandableButton.h ├── OCExpandableButton.m ├── OCExpandableButton.xcodeproj ├── project.pbxproj └── project.xcworkspace │ └── contents.xcworkspacedata ├── OCExpandableButton ├── Default-568h@2x.png ├── Default.png ├── Default@2x.png ├── OCAppDelegate.h ├── OCAppDelegate.m ├── OCExpandableButton-Info.plist ├── OCExpandableButton-Prefix.pch ├── OCViewController.h ├── OCViewController.m ├── en.lproj │ ├── InfoPlist.strings │ └── OCViewController.xib └── main.m ├── animation.gif └── readme.markdown /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocrickard/OCExpandableButton/380231abdc6e44842431d5658c2dda41799feaad/.DS_Store -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | DerivedData/* 2 | *.xcuserdatad -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Oliver Rickard 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 13 | all 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 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /OCExpandableButton.h: -------------------------------------------------------------------------------- 1 | // 2 | // OCExpandableButton.h 3 | // SparrowButton 4 | // 5 | // Created by Oliver Rickard on 5/18/13. 6 | // Copyright (c) 2013 Oliver Rickard. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //Determines if the content is left or right aligned. 12 | //If the content is left aligned, then the button will expand towards the right 13 | // with content 14 | typedef enum { 15 | OCExpandableButtonAlignmentLeft, 16 | OCExpandableButtonAlignmentRight 17 | } OCExpandableButtonAlignment; 18 | 19 | @class OCExpandableButton; 20 | 21 | // The delegate allows notification of expandable button opening/closure 22 | @protocol OCExpandableButtonDelegate 23 | 24 | - (void)expandableButtonOpened:(OCExpandableButton*)button; 25 | - (void)expandableButtonClosed:(OCExpandableButton*)button; 26 | 27 | @end 28 | 29 | @interface OCExpandableButton : UIView 30 | 31 | @property (nonatomic, assign) OCExpandableButtonAlignment alignment; 32 | @property (nonatomic, strong) id delegate; 33 | 34 | //initialize with a specific set of subviews 35 | - (id)initWithFrame:(CGRect)frame subviews:(NSArray *)subviews; 36 | 37 | //Standard initializer, set the subviews later. 38 | - (id)initWithFrame:(CGRect)frame; 39 | 40 | //These are the views that show in the bar when the control is expanded. 41 | - (void)setOptionViews:(NSArray *)subviews; 42 | 43 | //Opens the control if the control is currently closed. No effect if the button 44 | // is already open. 45 | - (void)open; 46 | 47 | //Closes the control if open. No effect if already closed. 48 | - (void)close; 49 | 50 | @end 51 | -------------------------------------------------------------------------------- /OCExpandableButton.m: -------------------------------------------------------------------------------- 1 | // 2 | // OCExpandableButton.m 3 | // SparrowButton 4 | // 5 | // Created by Oliver Rickard on 5/18/13. 6 | // Copyright (c) 2013 Oliver Rickard. All rights reserved. 7 | // 8 | 9 | #import "OCExpandableButton.h" 10 | #import 11 | 12 | #define kAnimationDuration 0.2f 13 | 14 | #define kInactiveGradientColors @[(__bridge id)[UIColor colorWithRed:65/255.f green:105/255.f blue:175/255.f alpha: 1].CGColor, (__bridge id)[UIColor colorWithRed:29/255.f green:47/255.f blue:97/255.f alpha: 1].CGColor] 15 | #define kInactiveStrokeColor [UIColor colorWithRed:20/255.f green:30/255.f blue:52/255.f alpha: 0.5f].CGColor 16 | #define kInactiveFillColor [UIColor whiteColor].CGColor 17 | #define kInactiveStrokeColors @[(__bridge id)[UIColor colorWithRed: 54/255.f green: 79/255.f blue: 134/255.f alpha: 1].CGColor, (__bridge id)[UIColor colorWithRed: 32/255.f green: 48/255.f blue: 87/255.f alpha: 1].CGColor] 18 | 19 | #define kActiveGradientColors @[(__bridge id)[UIColor colorWithRed:211/255.f green:214/255.f blue:218/255.f alpha:1].CGColor, (__bridge id)[UIColor colorWithRed:192/255.f green:197/255.f blue:202/255.f alpha:1].CGColor] 20 | #define kActiveStrokeColor [UIColor colorWithRed:175/255.f green:177/255.f blue:181/255.f alpha:1].CGColor 21 | #define kActiveFillColor [UIColor colorWithRed:0.494 green:0.498 blue:0.518 alpha:1].CGColor 22 | 23 | #define kBackgroundGradientColors @[(__bridge id)[UIColor colorWithRed:238/255.f green:240/255.f blue:245/255.f alpha:1].CGColor, (__bridge id)[UIColor colorWithRed:213/255.f green:218/255.f blue:224/255.f alpha:1].CGColor] 24 | #define kBackgroundStrokeColor [UIColor colorWithWhite:204/255.f alpha:1].CGColor 25 | 26 | #define kSubviewHorizontalMargin 3.f 27 | 28 | @interface OCExpandableButton () { 29 | CAShapeLayer *_arrowLayer; 30 | CAGradientLayer *_arrowGradientLayer; 31 | 32 | CAGradientLayer *_backgroundGradientLayer; 33 | 34 | NSArray *_optionViews; 35 | 36 | BOOL _active; 37 | } 38 | 39 | @end 40 | 41 | @implementation OCExpandableButton 42 | 43 | static void init(OCExpandableButton *self) { 44 | self->_active = NO; 45 | self.alignment = OCExpandableButtonAlignmentRight; 46 | self.userInteractionEnabled = YES; 47 | [self setupSublayers]; 48 | self.clipsToBounds = NO; 49 | } 50 | 51 | - (id)init { 52 | self = [super init]; 53 | if (self) { 54 | init(self); 55 | self.delegate = nil; 56 | } 57 | return self; 58 | } 59 | 60 | - (id)initWithCoder:(NSCoder *)aDecoder { 61 | self = [super initWithCoder:aDecoder]; 62 | if (self) { 63 | init(self); 64 | } 65 | return self; 66 | } 67 | 68 | - (id)initWithFrame:(CGRect)frame { 69 | self = [super initWithFrame:frame]; 70 | self.backgroundColor = [UIColor clearColor]; 71 | if(self) { 72 | init(self); 73 | } 74 | return self; 75 | } 76 | 77 | - (id)initWithFrame:(CGRect)frame subviews:(NSArray *)subviews { 78 | self = [super initWithFrame:frame]; 79 | if (self) { 80 | self.backgroundColor = [UIColor clearColor]; 81 | init(self); 82 | _optionViews = subviews; 83 | [self setupSubviews]; 84 | } 85 | return self; 86 | } 87 | 88 | - (void)dealloc { 89 | _arrowLayer = nil; 90 | _arrowGradientLayer = nil; 91 | _backgroundGradientLayer = nil; 92 | _optionViews = nil; 93 | } 94 | 95 | #pragma mark - Subview Setup 96 | 97 | - (void)setOptionViews:(NSArray *)subviews { 98 | if(subviews != _optionViews) { 99 | if(_optionViews) { 100 | for(UIView *subview in subviews) { 101 | [subview removeFromSuperview]; 102 | } 103 | } 104 | _optionViews = subviews; 105 | [self setupSubviews]; 106 | } 107 | } 108 | 109 | - (void)setupSubviews { 110 | for(UIView *view in _optionViews) { 111 | view.center = CGPointMake(CGRectGetMidX(self.bounds), CGRectGetMidY(self.bounds)); 112 | view.alpha = 0.f; 113 | view.hidden = YES; 114 | [self addSubview:view]; 115 | } 116 | } 117 | 118 | - (void)resetSubviews { 119 | for(UIView *view in _optionViews) { 120 | view.center = CGPointMake(CGRectGetMidX(self.bounds), CGRectGetMidY(self.bounds)); 121 | view.alpha = 0.f; 122 | view.hidden = YES; 123 | } 124 | } 125 | 126 | #pragma mark - Layer Setup 127 | 128 | - (void)setupSublayers { 129 | CGRect backgroundRect = self.bounds; 130 | 131 | //Construct the background layer that animates out when tapped 132 | { 133 | _backgroundGradientLayer = [CAGradientLayer layer]; 134 | _backgroundGradientLayer.frame = backgroundRect; 135 | _backgroundGradientLayer.colors = kBackgroundGradientColors; 136 | _backgroundGradientLayer.startPoint = CGPointMake(0.5f, 0.f); 137 | _backgroundGradientLayer.endPoint = CGPointMake(0.5f, 1.f); 138 | _backgroundGradientLayer.borderColor = kBackgroundStrokeColor; 139 | _backgroundGradientLayer.borderWidth = 1.f; 140 | _backgroundGradientLayer.cornerRadius = 9.f; 141 | _backgroundGradientLayer.opacity = 0.f; 142 | 143 | _backgroundGradientLayer.shadowColor = [UIColor blackColor].CGColor; 144 | _backgroundGradientLayer.shadowOffset = CGSizeMake(0.f, 1.f); 145 | _backgroundGradientLayer.shadowOpacity = 0.16f; 146 | _backgroundGradientLayer.shadowRadius = 4.f; 147 | 148 | [self.layer addSublayer:_backgroundGradientLayer]; 149 | } 150 | 151 | CGRect arrowButtonRect = CGRectInset(self.bounds, 4.f, 4.f); 152 | 153 | //Construct the blue gradient layer 154 | { 155 | _arrowGradientLayer = [CAGradientLayer layer]; 156 | _arrowGradientLayer.frame = arrowButtonRect; 157 | _arrowGradientLayer.colors = kInactiveGradientColors; 158 | _arrowGradientLayer.startPoint = CGPointMake(0.5f, 0.f); 159 | _arrowGradientLayer.endPoint = CGPointMake(0.5f, 1.f); 160 | _arrowGradientLayer.borderColor = kInactiveStrokeColor; 161 | _arrowGradientLayer.borderWidth = 1.f; 162 | _arrowGradientLayer.cornerRadius = 7.f; 163 | 164 | _arrowGradientLayer.shadowColor = [UIColor blackColor].CGColor; 165 | _arrowGradientLayer.shadowOffset = CGSizeMake(0.f, 1.f); 166 | _arrowGradientLayer.shadowOpacity = 0.4f; 167 | _arrowGradientLayer.shadowRadius = 2.f; 168 | 169 | //Disable implicit animations 170 | _arrowGradientLayer.actions = @{@"onOrderIn" : [NSNull null], @"onOrderOut" : [NSNull null], @"sublayers" : [NSNull null], @"contents" : [NSNull null], @"bounds" : [NSNull null], @"transform" : [NSNull null], @"position" : [NSNull null]}; 171 | 172 | [self.layer addSublayer:_arrowGradientLayer]; 173 | } 174 | 175 | //Construct the arrow 176 | { 177 | UIColor* arrowShadowColor = [UIColor colorWithRed: 0 green: 0 blue: 0 alpha: 0.469]; 178 | 179 | CGSize arrowShadowOffset = CGSizeMake(0.1, -0.1); 180 | CGFloat arrowShadowBlurRadius = 4; 181 | 182 | UIBezierPath* arrowPath = [UIBezierPath bezierPath]; 183 | CGFloat scaleFactor = CGRectGetWidth(arrowButtonRect)/31.f; 184 | //Construct the arrow's path - drawn by hand in PaintCode (love that 185 | // app) then scaled here so it grows with the size of the button. 186 | [arrowPath moveToPoint: CGPointMake(7.5*scaleFactor, 18.5*scaleFactor)]; 187 | [arrowPath addLineToPoint: CGPointMake(10*scaleFactor, 21*scaleFactor)]; 188 | [arrowPath addLineToPoint: CGPointMake(15.5*scaleFactor, 15.5*scaleFactor)]; 189 | [arrowPath addLineToPoint: CGPointMake(21*scaleFactor, 21*scaleFactor)]; 190 | [arrowPath addLineToPoint: CGPointMake(23.5*scaleFactor, 18.5*scaleFactor)]; 191 | [arrowPath addLineToPoint: CGPointMake(15.5*scaleFactor, 10.5*scaleFactor)]; 192 | [arrowPath addLineToPoint: CGPointMake(7.5*scaleFactor, 18.5*scaleFactor)]; 193 | [arrowPath closePath]; 194 | 195 | _arrowLayer = [CAShapeLayer layer]; 196 | _arrowLayer.frame = arrowButtonRect; 197 | _arrowLayer.path = arrowPath.CGPath; 198 | 199 | _arrowLayer.fillColor = kInactiveFillColor; 200 | _arrowLayer.shadowColor = arrowShadowColor.CGColor; 201 | _arrowLayer.shadowOffset = arrowShadowOffset; 202 | _arrowLayer.shadowRadius = arrowShadowBlurRadius; 203 | _arrowLayer.shadowPath = arrowPath.CGPath; 204 | _arrowLayer.shadowOpacity = 1.f; 205 | 206 | //Disable implicit animations 207 | _arrowLayer.actions = @{@"onOrderIn" : [NSNull null], @"onOrderOut" : [NSNull null], @"sublayers" : [NSNull null], @"contents" : [NSNull null], @"bounds" : [NSNull null], @"transform" : [NSNull null]}; 208 | 209 | [self.layer addSublayer:_arrowLayer]; 210 | } 211 | } 212 | 213 | #pragma mark - Activation 214 | 215 | - (void)toggleActive { 216 | 217 | _active = !_active; 218 | 219 | if(_active) { 220 | [self animateOpen]; 221 | } else { 222 | [self animateClose]; 223 | } 224 | } 225 | 226 | - (void)animateOpen { 227 | //The button is now active. We have to expand the frame, change the 228 | // colors of the bg, animate in the sub-buttons 229 | 230 | //Compute the new size 231 | CGFloat newWidth = kSubviewHorizontalMargin + self.bounds.size.width; 232 | for(UIView *view in _optionViews) { 233 | newWidth += view.frame.size.width + kSubviewHorizontalMargin; 234 | } 235 | 236 | //Animate in the gray background 237 | [self fadeLayer:_backgroundGradientLayer newOpacity:1.f duration:kAnimationDuration]; 238 | 239 | //Change bg colors 240 | [self animateColorChangeForLayer:_arrowGradientLayer newColors:kActiveGradientColors duration:kAnimationDuration]; 241 | _arrowGradientLayer.borderColor = kActiveStrokeColor; 242 | _arrowGradientLayer.shadowColor = [UIColor whiteColor].CGColor; 243 | _arrowGradientLayer.shadowRadius = 0; 244 | 245 | //Change the fill colors 246 | [self fadeFillColorForLayer:_arrowLayer newColor:kActiveFillColor duration:kAnimationDuration]; 247 | _arrowLayer.shadowOpacity = 0.f; 248 | 249 | //We need to handle the layout of subviews for the different alignments 250 | // differently. 251 | CGFloat directionModifier = self.alignment == OCExpandableButtonAlignmentLeft ? 1 : -1; 252 | 253 | //rotate 254 | [self rotateLayer:_arrowLayer byDegrees:90.f*directionModifier duration:kAnimationDuration]; 255 | 256 | CGFloat curX = self.bounds.size.width + kSubviewHorizontalMargin; 257 | if (self.alignment == OCExpandableButtonAlignmentRight) { 258 | curX -= newWidth; 259 | } 260 | 261 | NSTimeInterval delay = 0.02f + 0.02f*_optionViews.count; 262 | //Animate in the items 263 | for(UIView *view in _optionViews) { 264 | view.center = CGPointMake(curX + floorf(view.frame.size.width*0.5f), CGRectGetMidY(self.bounds)); 265 | view.hidden = NO; 266 | view.alpha = 0.f; 267 | view.transform = CGAffineTransformMakeScale(2.f, 2.f); 268 | [UIView animateWithDuration:kAnimationDuration delay:delay options:UIViewAnimationOptionCurveEaseOut animations:^{ 269 | view.alpha = 1.f; 270 | view.transform = CGAffineTransformIdentity; 271 | } completion:nil]; 272 | delay += 0.02f*directionModifier; 273 | curX += view.frame.size.width + kSubviewHorizontalMargin; 274 | } 275 | 276 | //Expand the bg 277 | [self setBoundsForLayer:_backgroundGradientLayer newBounds:CGRectMake(0, 0, newWidth, _backgroundGradientLayer.bounds.size.height) duration:kAnimationDuration]; 278 | CGPoint position = CGPointMake(_backgroundGradientLayer.position.x + directionModifier*0.5f*(_backgroundGradientLayer.bounds.size.width - self.bounds.size.width), CGRectGetMidY(self.bounds)); 279 | [self setPositionForLayer:_backgroundGradientLayer newPosition:position duration:kAnimationDuration]; 280 | 281 | // notify 282 | if (self.delegate != nil) 283 | [self.delegate expandableButtonOpened:self]; 284 | } 285 | 286 | - (void)animateClose { 287 | //The button is inactive. We have to contract the frame, change the 288 | // colors, and animate out the sub-buttons. 289 | 290 | //Animate out the items 291 | for(UIView *view in _optionViews) { 292 | [UIView animateWithDuration:kAnimationDuration delay:0.f options:UIViewAnimationOptionCurveEaseInOut animations:^{ 293 | view.alpha = 0.f; 294 | view.center = CGPointMake(CGRectGetMidX(self.bounds), CGRectGetMidY(self.bounds)); 295 | } completion:^(BOOL finished) { 296 | view.hidden = YES; 297 | }]; 298 | } 299 | 300 | //Animate out the gray background 301 | [self fadeLayer:_backgroundGradientLayer newOpacity:0.f duration:kAnimationDuration]; 302 | 303 | _backgroundGradientLayer.bounds = self.bounds; 304 | _backgroundGradientLayer.position = CGPointMake(CGRectGetMidX(self.bounds), CGRectGetMidY(self.bounds)); 305 | 306 | //Change bg colors 307 | [self animateColorChangeForLayer:_arrowGradientLayer newColors:kInactiveGradientColors duration:kAnimationDuration]; 308 | _arrowGradientLayer.borderColor = kInactiveStrokeColor; 309 | _arrowGradientLayer.shadowColor = [UIColor blackColor].CGColor; 310 | _arrowGradientLayer.shadowRadius = 2.f; 311 | 312 | //Change the fill colors 313 | [self fadeFillColorForLayer:_arrowLayer newColor:kInactiveFillColor duration:kAnimationDuration]; 314 | _arrowLayer.shadowOpacity = 1.f; 315 | 316 | //Switch up the frames! 317 | if(self.alignment == OCExpandableButtonAlignmentLeft) { 318 | //rotate back to the start 319 | [self rotateLayer:_arrowLayer byDegrees:-90.f duration:kAnimationDuration]; 320 | } else if(self.alignment == OCExpandableButtonAlignmentRight) { 321 | //rotate back to the start 322 | [self rotateLayer:_arrowLayer byDegrees:90.f duration:kAnimationDuration]; 323 | } 324 | 325 | // notify 326 | if (self.delegate != nil) 327 | [self.delegate expandableButtonClosed:self]; 328 | } 329 | 330 | - (void)setPositionForLayer:(CALayer *)layer newPosition:(CGPoint)point duration:(NSTimeInterval)duration { 331 | NSValue *positionAtStart = [layer valueForKeyPath:@"position"]; 332 | layer.position = point; 333 | CABasicAnimation *boundsChange = [CABasicAnimation animationWithKeyPath:@"position"]; 334 | boundsChange.duration = duration; 335 | boundsChange.fromValue = positionAtStart; 336 | boundsChange.toValue = [NSValue valueWithCGPoint:point]; 337 | boundsChange.removedOnCompletion = YES; 338 | [layer addAnimation:boundsChange forKey:@"position"]; 339 | } 340 | 341 | - (void)setBoundsForLayer:(CALayer *)layer newBounds:(CGRect)bounds duration:(NSTimeInterval)duration { 342 | NSValue *boundsAtStart = [layer valueForKeyPath:@"bounds"]; 343 | layer.bounds = bounds; 344 | CABasicAnimation *boundsChange = [CABasicAnimation animationWithKeyPath:@"bounds"]; 345 | boundsChange.duration = duration; 346 | boundsChange.fromValue = boundsAtStart; 347 | boundsChange.toValue = [NSValue valueWithCGRect:bounds]; 348 | boundsChange.removedOnCompletion = YES; 349 | [layer addAnimation:boundsChange forKey:@"bounds"]; 350 | } 351 | 352 | - (void)fadeFillColorForLayer:(CAShapeLayer *)layer newColor:(CGColorRef)color duration:(NSTimeInterval)duration { 353 | id colorAtStart = [layer valueForKeyPath:@"fillColor"]; 354 | layer.fillColor = color; 355 | CABasicAnimation *colorChange = [CABasicAnimation animationWithKeyPath:@"fillColor"]; 356 | colorChange.duration = duration; 357 | colorChange.fromValue = colorAtStart; 358 | colorChange.toValue = (__bridge id)color; 359 | colorChange.removedOnCompletion = YES; 360 | [layer addAnimation:colorChange forKey:@"fillColor"]; 361 | } 362 | 363 | - (void)fadeLayer:(CALayer *)layer newOpacity:(CGFloat)opacity duration:(NSTimeInterval)duration { 364 | NSNumber *opacityAtStart = [layer valueForKeyPath:@"opacity"]; 365 | layer.opacity = opacity; 366 | CABasicAnimation *opacityChange = [CABasicAnimation animationWithKeyPath:@"opacity"]; 367 | opacityChange.duration = duration; 368 | opacityChange.fromValue = opacityAtStart; 369 | opacityChange.toValue = @(opacity); 370 | opacityChange.removedOnCompletion = YES; 371 | [layer addAnimation:opacityChange forKey:@"opacity"]; 372 | } 373 | 374 | - (void)animateColorChangeForLayer:(CAGradientLayer *)gradientLayer newColors:(NSArray *)colors duration:(NSTimeInterval)duration { 375 | NSArray *colorsAtStart = [gradientLayer valueForKeyPath:@"colors"]; 376 | gradientLayer.colors = colors; 377 | CABasicAnimation *colorChange = [CABasicAnimation animationWithKeyPath:@"colors"]; 378 | colorChange.duration = duration; 379 | colorChange.fromValue = colorsAtStart; 380 | colorChange.toValue = colors; 381 | colorChange.removedOnCompletion = YES; 382 | [gradientLayer addAnimation:colorChange forKey:@"colors"]; 383 | } 384 | 385 | - (void)rotateLayer:(CALayer *)layer byDegrees:(CGFloat)degrees duration:(NSTimeInterval)duration { 386 | NSNumber *rotationAtStart = [layer valueForKeyPath:@"transform.rotation"]; 387 | CATransform3D transform = CATransform3DRotate(layer.transform, degrees*M_PI/180.f, 0.0, 0.0, 1.0); 388 | layer.transform = transform; 389 | CABasicAnimation *rotation = [CABasicAnimation animationWithKeyPath:@"transform.rotation"]; 390 | rotation.duration = duration; 391 | rotation.fromValue = rotationAtStart; 392 | rotation.toValue = [NSNumber numberWithFloat:([rotationAtStart floatValue] + degrees*M_PI/180.f)]; 393 | rotation.removedOnCompletion = YES; 394 | [layer addAnimation:rotation forKey:@"transform.rotation"]; 395 | } 396 | 397 | #pragma mark - Touch Handling 398 | 399 | - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { 400 | if(touches.count > 0) { 401 | UITouch *touch = [touches anyObject]; 402 | CGPoint touchPoint = [touch locationInView:self]; 403 | if(CGRectContainsPoint(self.bounds, touchPoint)) { 404 | //The user touched up inside the button. Let's toggle activation. 405 | [self toggleActive]; 406 | } 407 | } 408 | } 409 | 410 | - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { 411 | if(_active) { 412 | for(UIView *subview in _optionViews) { 413 | if(CGRectContainsPoint(subview.frame, point)) { 414 | [UIView animateWithDuration:0.4f delay:0.2f options:UIViewAnimationOptionAllowUserInteraction animations:^{ 415 | subview.transform = CGAffineTransformMakeScale(2.5f, 2.5f); 416 | } completion:^(BOOL finished) { 417 | [UIView animateWithDuration:0.15f animations:^{ 418 | subview.transform = CGAffineTransformIdentity; 419 | }]; 420 | }]; 421 | return subview; 422 | } 423 | } 424 | } 425 | return [super hitTest:point withEvent:event]; 426 | } 427 | 428 | #pragma mark - Public Methods 429 | 430 | - (void)open { 431 | if(!_active) { 432 | [self toggleActive]; 433 | } 434 | } 435 | 436 | - (void)close { 437 | if(_active) { 438 | [self toggleActive]; 439 | } 440 | } 441 | 442 | @end 443 | -------------------------------------------------------------------------------- /OCExpandableButton.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | B1E7DBAA174847E3003291C6 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B1E7DBA9174847E3003291C6 /* UIKit.framework */; }; 11 | B1E7DBAC174847E3003291C6 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B1E7DBAB174847E3003291C6 /* Foundation.framework */; }; 12 | B1E7DBAE174847E3003291C6 /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B1E7DBAD174847E3003291C6 /* CoreGraphics.framework */; }; 13 | B1E7DBB4174847E3003291C6 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = B1E7DBB2174847E3003291C6 /* InfoPlist.strings */; }; 14 | B1E7DBB6174847E3003291C6 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = B1E7DBB5174847E3003291C6 /* main.m */; }; 15 | B1E7DBBA174847E3003291C6 /* OCAppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = B1E7DBB9174847E3003291C6 /* OCAppDelegate.m */; }; 16 | B1E7DBBC174847E3003291C6 /* Default.png in Resources */ = {isa = PBXBuildFile; fileRef = B1E7DBBB174847E3003291C6 /* Default.png */; }; 17 | B1E7DBBE174847E3003291C6 /* Default@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = B1E7DBBD174847E3003291C6 /* Default@2x.png */; }; 18 | B1E7DBC0174847E3003291C6 /* Default-568h@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = B1E7DBBF174847E3003291C6 /* Default-568h@2x.png */; }; 19 | B1E7DBC3174847E3003291C6 /* OCViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = B1E7DBC2174847E3003291C6 /* OCViewController.m */; }; 20 | B1E7DBC6174847E3003291C6 /* OCViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = B1E7DBC4174847E3003291C6 /* OCViewController.xib */; }; 21 | B1E7DBCE174847F3003291C6 /* OCExpandableButton.m in Sources */ = {isa = PBXBuildFile; fileRef = B1E7DBCD174847F3003291C6 /* OCExpandableButton.m */; }; 22 | B1E7DBD0174848D3003291C6 /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B1E7DBCF174848D3003291C6 /* QuartzCore.framework */; }; 23 | /* End PBXBuildFile section */ 24 | 25 | /* Begin PBXFileReference section */ 26 | B1E7DBA6174847E3003291C6 /* OCExpandableButton.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = OCExpandableButton.app; sourceTree = BUILT_PRODUCTS_DIR; }; 27 | B1E7DBA9174847E3003291C6 /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; }; 28 | B1E7DBAB174847E3003291C6 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; 29 | B1E7DBAD174847E3003291C6 /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = System/Library/Frameworks/CoreGraphics.framework; sourceTree = SDKROOT; }; 30 | B1E7DBB1174847E3003291C6 /* OCExpandableButton-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "OCExpandableButton-Info.plist"; sourceTree = ""; }; 31 | B1E7DBB3174847E3003291C6 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; 32 | B1E7DBB5174847E3003291C6 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 33 | B1E7DBB7174847E3003291C6 /* OCExpandableButton-Prefix.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "OCExpandableButton-Prefix.pch"; sourceTree = ""; }; 34 | B1E7DBB8174847E3003291C6 /* OCAppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OCAppDelegate.h; sourceTree = ""; }; 35 | B1E7DBB9174847E3003291C6 /* OCAppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OCAppDelegate.m; sourceTree = ""; }; 36 | B1E7DBBB174847E3003291C6 /* Default.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = Default.png; sourceTree = ""; }; 37 | B1E7DBBD174847E3003291C6 /* Default@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Default@2x.png"; sourceTree = ""; }; 38 | B1E7DBBF174847E3003291C6 /* Default-568h@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Default-568h@2x.png"; sourceTree = ""; }; 39 | B1E7DBC1174847E3003291C6 /* OCViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OCViewController.h; sourceTree = ""; }; 40 | B1E7DBC2174847E3003291C6 /* OCViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OCViewController.m; sourceTree = ""; }; 41 | B1E7DBC5174847E3003291C6 /* en */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = en; path = en.lproj/OCViewController.xib; sourceTree = ""; }; 42 | B1E7DBCC174847F3003291C6 /* OCExpandableButton.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OCExpandableButton.h; sourceTree = ""; }; 43 | B1E7DBCD174847F3003291C6 /* OCExpandableButton.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCExpandableButton.m; sourceTree = ""; }; 44 | B1E7DBCF174848D3003291C6 /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; }; 45 | /* End PBXFileReference section */ 46 | 47 | /* Begin PBXFrameworksBuildPhase section */ 48 | B1E7DBA3174847E3003291C6 /* Frameworks */ = { 49 | isa = PBXFrameworksBuildPhase; 50 | buildActionMask = 2147483647; 51 | files = ( 52 | B1E7DBD0174848D3003291C6 /* QuartzCore.framework in Frameworks */, 53 | B1E7DBAA174847E3003291C6 /* UIKit.framework in Frameworks */, 54 | B1E7DBAC174847E3003291C6 /* Foundation.framework in Frameworks */, 55 | B1E7DBAE174847E3003291C6 /* CoreGraphics.framework in Frameworks */, 56 | ); 57 | runOnlyForDeploymentPostprocessing = 0; 58 | }; 59 | /* End PBXFrameworksBuildPhase section */ 60 | 61 | /* Begin PBXGroup section */ 62 | B1E7DB9D174847E3003291C6 = { 63 | isa = PBXGroup; 64 | children = ( 65 | B1E7DBCC174847F3003291C6 /* OCExpandableButton.h */, 66 | B1E7DBCD174847F3003291C6 /* OCExpandableButton.m */, 67 | B1E7DBAF174847E3003291C6 /* OCExpandableButton */, 68 | B1E7DBA8174847E3003291C6 /* Frameworks */, 69 | B1E7DBA7174847E3003291C6 /* Products */, 70 | ); 71 | sourceTree = ""; 72 | }; 73 | B1E7DBA7174847E3003291C6 /* Products */ = { 74 | isa = PBXGroup; 75 | children = ( 76 | B1E7DBA6174847E3003291C6 /* OCExpandableButton.app */, 77 | ); 78 | name = Products; 79 | sourceTree = ""; 80 | }; 81 | B1E7DBA8174847E3003291C6 /* Frameworks */ = { 82 | isa = PBXGroup; 83 | children = ( 84 | B1E7DBCF174848D3003291C6 /* QuartzCore.framework */, 85 | B1E7DBA9174847E3003291C6 /* UIKit.framework */, 86 | B1E7DBAB174847E3003291C6 /* Foundation.framework */, 87 | B1E7DBAD174847E3003291C6 /* CoreGraphics.framework */, 88 | ); 89 | name = Frameworks; 90 | sourceTree = ""; 91 | }; 92 | B1E7DBAF174847E3003291C6 /* OCExpandableButton */ = { 93 | isa = PBXGroup; 94 | children = ( 95 | B1E7DBB8174847E3003291C6 /* OCAppDelegate.h */, 96 | B1E7DBB9174847E3003291C6 /* OCAppDelegate.m */, 97 | B1E7DBC1174847E3003291C6 /* OCViewController.h */, 98 | B1E7DBC2174847E3003291C6 /* OCViewController.m */, 99 | B1E7DBC4174847E3003291C6 /* OCViewController.xib */, 100 | B1E7DBB0174847E3003291C6 /* Supporting Files */, 101 | ); 102 | path = OCExpandableButton; 103 | sourceTree = ""; 104 | }; 105 | B1E7DBB0174847E3003291C6 /* Supporting Files */ = { 106 | isa = PBXGroup; 107 | children = ( 108 | B1E7DBB1174847E3003291C6 /* OCExpandableButton-Info.plist */, 109 | B1E7DBB2174847E3003291C6 /* InfoPlist.strings */, 110 | B1E7DBB5174847E3003291C6 /* main.m */, 111 | B1E7DBB7174847E3003291C6 /* OCExpandableButton-Prefix.pch */, 112 | B1E7DBBB174847E3003291C6 /* Default.png */, 113 | B1E7DBBD174847E3003291C6 /* Default@2x.png */, 114 | B1E7DBBF174847E3003291C6 /* Default-568h@2x.png */, 115 | ); 116 | name = "Supporting Files"; 117 | sourceTree = ""; 118 | }; 119 | /* End PBXGroup section */ 120 | 121 | /* Begin PBXNativeTarget section */ 122 | B1E7DBA5174847E3003291C6 /* OCExpandableButton */ = { 123 | isa = PBXNativeTarget; 124 | buildConfigurationList = B1E7DBC9174847E3003291C6 /* Build configuration list for PBXNativeTarget "OCExpandableButton" */; 125 | buildPhases = ( 126 | B1E7DBA2174847E3003291C6 /* Sources */, 127 | B1E7DBA3174847E3003291C6 /* Frameworks */, 128 | B1E7DBA4174847E3003291C6 /* Resources */, 129 | ); 130 | buildRules = ( 131 | ); 132 | dependencies = ( 133 | ); 134 | name = OCExpandableButton; 135 | productName = OCExpandableButton; 136 | productReference = B1E7DBA6174847E3003291C6 /* OCExpandableButton.app */; 137 | productType = "com.apple.product-type.application"; 138 | }; 139 | /* End PBXNativeTarget section */ 140 | 141 | /* Begin PBXProject section */ 142 | B1E7DB9E174847E3003291C6 /* Project object */ = { 143 | isa = PBXProject; 144 | attributes = { 145 | CLASSPREFIX = OC; 146 | LastUpgradeCheck = 0460; 147 | ORGANIZATIONNAME = "Oliver Rickard"; 148 | }; 149 | buildConfigurationList = B1E7DBA1174847E3003291C6 /* Build configuration list for PBXProject "OCExpandableButton" */; 150 | compatibilityVersion = "Xcode 3.2"; 151 | developmentRegion = English; 152 | hasScannedForEncodings = 0; 153 | knownRegions = ( 154 | en, 155 | ); 156 | mainGroup = B1E7DB9D174847E3003291C6; 157 | productRefGroup = B1E7DBA7174847E3003291C6 /* Products */; 158 | projectDirPath = ""; 159 | projectRoot = ""; 160 | targets = ( 161 | B1E7DBA5174847E3003291C6 /* OCExpandableButton */, 162 | ); 163 | }; 164 | /* End PBXProject section */ 165 | 166 | /* Begin PBXResourcesBuildPhase section */ 167 | B1E7DBA4174847E3003291C6 /* Resources */ = { 168 | isa = PBXResourcesBuildPhase; 169 | buildActionMask = 2147483647; 170 | files = ( 171 | B1E7DBB4174847E3003291C6 /* InfoPlist.strings in Resources */, 172 | B1E7DBBC174847E3003291C6 /* Default.png in Resources */, 173 | B1E7DBBE174847E3003291C6 /* Default@2x.png in Resources */, 174 | B1E7DBC0174847E3003291C6 /* Default-568h@2x.png in Resources */, 175 | B1E7DBC6174847E3003291C6 /* OCViewController.xib in Resources */, 176 | ); 177 | runOnlyForDeploymentPostprocessing = 0; 178 | }; 179 | /* End PBXResourcesBuildPhase section */ 180 | 181 | /* Begin PBXSourcesBuildPhase section */ 182 | B1E7DBA2174847E3003291C6 /* Sources */ = { 183 | isa = PBXSourcesBuildPhase; 184 | buildActionMask = 2147483647; 185 | files = ( 186 | B1E7DBB6174847E3003291C6 /* main.m in Sources */, 187 | B1E7DBBA174847E3003291C6 /* OCAppDelegate.m in Sources */, 188 | B1E7DBC3174847E3003291C6 /* OCViewController.m in Sources */, 189 | B1E7DBCE174847F3003291C6 /* OCExpandableButton.m in Sources */, 190 | ); 191 | runOnlyForDeploymentPostprocessing = 0; 192 | }; 193 | /* End PBXSourcesBuildPhase section */ 194 | 195 | /* Begin PBXVariantGroup section */ 196 | B1E7DBB2174847E3003291C6 /* InfoPlist.strings */ = { 197 | isa = PBXVariantGroup; 198 | children = ( 199 | B1E7DBB3174847E3003291C6 /* en */, 200 | ); 201 | name = InfoPlist.strings; 202 | sourceTree = ""; 203 | }; 204 | B1E7DBC4174847E3003291C6 /* OCViewController.xib */ = { 205 | isa = PBXVariantGroup; 206 | children = ( 207 | B1E7DBC5174847E3003291C6 /* en */, 208 | ); 209 | name = OCViewController.xib; 210 | sourceTree = ""; 211 | }; 212 | /* End PBXVariantGroup section */ 213 | 214 | /* Begin XCBuildConfiguration section */ 215 | B1E7DBC7174847E3003291C6 /* Debug */ = { 216 | isa = XCBuildConfiguration; 217 | buildSettings = { 218 | ALWAYS_SEARCH_USER_PATHS = NO; 219 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 220 | CLANG_CXX_LIBRARY = "libc++"; 221 | CLANG_ENABLE_OBJC_ARC = YES; 222 | CLANG_WARN_CONSTANT_CONVERSION = YES; 223 | CLANG_WARN_EMPTY_BODY = YES; 224 | CLANG_WARN_ENUM_CONVERSION = YES; 225 | CLANG_WARN_INT_CONVERSION = YES; 226 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 227 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 228 | COPY_PHASE_STRIP = NO; 229 | GCC_C_LANGUAGE_STANDARD = gnu99; 230 | GCC_DYNAMIC_NO_PIC = NO; 231 | GCC_OPTIMIZATION_LEVEL = 0; 232 | GCC_PREPROCESSOR_DEFINITIONS = ( 233 | "DEBUG=1", 234 | "$(inherited)", 235 | ); 236 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 237 | GCC_WARN_ABOUT_RETURN_TYPE = YES; 238 | GCC_WARN_UNINITIALIZED_AUTOS = YES; 239 | GCC_WARN_UNUSED_VARIABLE = YES; 240 | IPHONEOS_DEPLOYMENT_TARGET = 6.1; 241 | ONLY_ACTIVE_ARCH = YES; 242 | SDKROOT = iphoneos; 243 | }; 244 | name = Debug; 245 | }; 246 | B1E7DBC8174847E3003291C6 /* Release */ = { 247 | isa = XCBuildConfiguration; 248 | buildSettings = { 249 | ALWAYS_SEARCH_USER_PATHS = NO; 250 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 251 | CLANG_CXX_LIBRARY = "libc++"; 252 | CLANG_ENABLE_OBJC_ARC = YES; 253 | CLANG_WARN_CONSTANT_CONVERSION = YES; 254 | CLANG_WARN_EMPTY_BODY = YES; 255 | CLANG_WARN_ENUM_CONVERSION = YES; 256 | CLANG_WARN_INT_CONVERSION = YES; 257 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 258 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 259 | COPY_PHASE_STRIP = YES; 260 | GCC_C_LANGUAGE_STANDARD = gnu99; 261 | GCC_WARN_ABOUT_RETURN_TYPE = YES; 262 | GCC_WARN_UNINITIALIZED_AUTOS = YES; 263 | GCC_WARN_UNUSED_VARIABLE = YES; 264 | IPHONEOS_DEPLOYMENT_TARGET = 6.1; 265 | OTHER_CFLAGS = "-DNS_BLOCK_ASSERTIONS=1"; 266 | SDKROOT = iphoneos; 267 | VALIDATE_PRODUCT = YES; 268 | }; 269 | name = Release; 270 | }; 271 | B1E7DBCA174847E3003291C6 /* Debug */ = { 272 | isa = XCBuildConfiguration; 273 | buildSettings = { 274 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 275 | GCC_PREFIX_HEADER = "OCExpandableButton/OCExpandableButton-Prefix.pch"; 276 | INFOPLIST_FILE = "OCExpandableButton/OCExpandableButton-Info.plist"; 277 | PRODUCT_NAME = "$(TARGET_NAME)"; 278 | WRAPPER_EXTENSION = app; 279 | }; 280 | name = Debug; 281 | }; 282 | B1E7DBCB174847E3003291C6 /* Release */ = { 283 | isa = XCBuildConfiguration; 284 | buildSettings = { 285 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 286 | GCC_PREFIX_HEADER = "OCExpandableButton/OCExpandableButton-Prefix.pch"; 287 | INFOPLIST_FILE = "OCExpandableButton/OCExpandableButton-Info.plist"; 288 | PRODUCT_NAME = "$(TARGET_NAME)"; 289 | WRAPPER_EXTENSION = app; 290 | }; 291 | name = Release; 292 | }; 293 | /* End XCBuildConfiguration section */ 294 | 295 | /* Begin XCConfigurationList section */ 296 | B1E7DBA1174847E3003291C6 /* Build configuration list for PBXProject "OCExpandableButton" */ = { 297 | isa = XCConfigurationList; 298 | buildConfigurations = ( 299 | B1E7DBC7174847E3003291C6 /* Debug */, 300 | B1E7DBC8174847E3003291C6 /* Release */, 301 | ); 302 | defaultConfigurationIsVisible = 0; 303 | defaultConfigurationName = Release; 304 | }; 305 | B1E7DBC9174847E3003291C6 /* Build configuration list for PBXNativeTarget "OCExpandableButton" */ = { 306 | isa = XCConfigurationList; 307 | buildConfigurations = ( 308 | B1E7DBCA174847E3003291C6 /* Debug */, 309 | B1E7DBCB174847E3003291C6 /* Release */, 310 | ); 311 | defaultConfigurationIsVisible = 0; 312 | defaultConfigurationName = Release; 313 | }; 314 | /* End XCConfigurationList section */ 315 | }; 316 | rootObject = B1E7DB9E174847E3003291C6 /* Project object */; 317 | } 318 | -------------------------------------------------------------------------------- /OCExpandableButton.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /OCExpandableButton/Default-568h@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocrickard/OCExpandableButton/380231abdc6e44842431d5658c2dda41799feaad/OCExpandableButton/Default-568h@2x.png -------------------------------------------------------------------------------- /OCExpandableButton/Default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocrickard/OCExpandableButton/380231abdc6e44842431d5658c2dda41799feaad/OCExpandableButton/Default.png -------------------------------------------------------------------------------- /OCExpandableButton/Default@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocrickard/OCExpandableButton/380231abdc6e44842431d5658c2dda41799feaad/OCExpandableButton/Default@2x.png -------------------------------------------------------------------------------- /OCExpandableButton/OCAppDelegate.h: -------------------------------------------------------------------------------- 1 | // 2 | // OCAppDelegate.h 3 | // OCExpandableButton 4 | // 5 | // Created by Oliver Rickard on 5/18/13. 6 | // Copyright (c) 2013 Oliver Rickard. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @class OCViewController; 12 | 13 | @interface OCAppDelegate : UIResponder 14 | 15 | @property (strong, nonatomic) UIWindow *window; 16 | 17 | @property (strong, nonatomic) OCViewController *viewController; 18 | 19 | @end 20 | -------------------------------------------------------------------------------- /OCExpandableButton/OCAppDelegate.m: -------------------------------------------------------------------------------- 1 | // 2 | // OCAppDelegate.m 3 | // OCExpandableButton 4 | // 5 | // Created by Oliver Rickard on 5/18/13. 6 | // Copyright (c) 2013 Oliver Rickard. All rights reserved. 7 | // 8 | 9 | #import "OCAppDelegate.h" 10 | 11 | #import "OCViewController.h" 12 | 13 | @implementation OCAppDelegate 14 | 15 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 16 | { 17 | self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; 18 | // Override point for customization after application launch. 19 | self.viewController = [[OCViewController alloc] initWithNibName:@"OCViewController" bundle:nil]; 20 | self.window.rootViewController = self.viewController; 21 | [self.window makeKeyAndVisible]; 22 | return YES; 23 | } 24 | 25 | - (void)applicationWillResignActive:(UIApplication *)application 26 | { 27 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 28 | // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. 29 | } 30 | 31 | - (void)applicationDidEnterBackground:(UIApplication *)application 32 | { 33 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 34 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 35 | } 36 | 37 | - (void)applicationWillEnterForeground:(UIApplication *)application 38 | { 39 | // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. 40 | } 41 | 42 | - (void)applicationDidBecomeActive:(UIApplication *)application 43 | { 44 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 45 | } 46 | 47 | - (void)applicationWillTerminate:(UIApplication *)application 48 | { 49 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 50 | } 51 | 52 | @end 53 | -------------------------------------------------------------------------------- /OCExpandableButton/OCExpandableButton-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | ${PRODUCT_NAME} 9 | CFBundleExecutable 10 | ${EXECUTABLE_NAME} 11 | CFBundleIdentifier 12 | com.ocrickard.${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 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /OCExpandableButton/OCExpandableButton-Prefix.pch: -------------------------------------------------------------------------------- 1 | // 2 | // Prefix header for all source files of the 'OCExpandableButton' target in the 'OCExpandableButton' project 3 | // 4 | 5 | #import 6 | 7 | #ifndef __IPHONE_4_0 8 | #warning "This project uses features only available in iOS SDK 4.0 and later." 9 | #endif 10 | 11 | #ifdef __OBJC__ 12 | #import 13 | #import 14 | #endif 15 | -------------------------------------------------------------------------------- /OCExpandableButton/OCViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // OCViewController.h 3 | // OCExpandableButton 4 | // 5 | // Created by Oliver Rickard on 5/18/13. 6 | // Copyright (c) 2013 Oliver Rickard. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface OCViewController : UIViewController 12 | 13 | @end 14 | -------------------------------------------------------------------------------- /OCExpandableButton/OCViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // OCViewController.m 3 | // OCExpandableButton 4 | // 5 | // Created by Oliver Rickard on 5/18/13. 6 | // Copyright (c) 2013 Oliver Rickard. All rights reserved. 7 | // 8 | 9 | #import "OCViewController.h" 10 | #import "OCExpandableButton.h" 11 | 12 | @interface OCViewController () { 13 | OCExpandableButton *button; 14 | } 15 | 16 | @end 17 | 18 | @implementation OCViewController 19 | 20 | - (void)viewDidLoad 21 | { 22 | [super viewDidLoad]; 23 | // Do any additional setup after loading the view, typically from a nib. 24 | 25 | 26 | } 27 | 28 | - (void)viewDidAppear:(BOOL)animated { 29 | [super viewDidAppear:animated]; 30 | 31 | NSMutableArray *subviews = [[NSMutableArray alloc] init]; 32 | 33 | for(int i = 0; i < 4; i++) { 34 | UIButton *numberButton = [[UIButton alloc] initWithFrame:CGRectMake(0, 0, 30.f, 30.f)]; 35 | numberButton.backgroundColor = [UIColor clearColor]; 36 | [numberButton setTitle:[NSString stringWithFormat:@"%d", i] forState:UIControlStateNormal]; 37 | numberButton.titleLabel.textAlignment = NSTextAlignmentCenter; 38 | numberButton.titleLabel.textColor = [UIColor colorWithRed:0.494 green:0.498 blue:0.518 alpha:1]; 39 | [numberButton addTarget:self action:@selector(tapped) forControlEvents:UIControlEventTouchUpInside]; 40 | [subviews addObject:numberButton]; 41 | } 42 | 43 | //Note to reader - the blue initial button is inset 3px on all sides from 44 | // the initial frame you provide. You should provide a square rect of any 45 | // size. 46 | button = [[OCExpandableButton alloc] initWithFrame:CGRectMake(floorf(self.view.bounds.size.width*0.5f - 39.f*0.5f), self.view.bounds.size.height - 57, 39, 39) subviews:subviews]; 47 | //You can change the alignment with: 48 | button.alignment = OCExpandableButtonAlignmentLeft; 49 | [self.view addSubview:button]; 50 | 51 | //You can open/close the button using the public methods. So if you wanted 52 | // to close the button on scroll or orientation change, you can do that. 53 | // [button open]; 54 | } 55 | 56 | - (void)tapped { 57 | NSLog(@"tapped"); 58 | } 59 | 60 | - (void)willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration { 61 | button.frame = CGRectMake(floorf(self.view.bounds.size.width*0.5f - 39.f*0.5f), self.view.bounds.size.height - 57, 39, 39); 62 | } 63 | 64 | - (void)didReceiveMemoryWarning 65 | { 66 | [super didReceiveMemoryWarning]; 67 | // Dispose of any resources that can be recreated. 68 | } 69 | 70 | @end 71 | -------------------------------------------------------------------------------- /OCExpandableButton/en.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* Localized versions of Info.plist keys */ 2 | 3 | -------------------------------------------------------------------------------- /OCExpandableButton/en.lproj/OCViewController.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 1552 5 | 12D78 6 | 3084 7 | 1187.37 8 | 626.00 9 | 10 | com.apple.InterfaceBuilder.IBCocoaTouchPlugin 11 | 2083 12 | 13 | 14 | IBProxyObject 15 | IBUIView 16 | 17 | 18 | com.apple.InterfaceBuilder.IBCocoaTouchPlugin 19 | 20 | 21 | PluginDependencyRecalculationVersion 22 | 23 | 24 | 25 | 26 | IBFilesOwner 27 | IBCocoaTouchFramework 28 | 29 | 30 | IBFirstResponder 31 | IBCocoaTouchFramework 32 | 33 | 34 | 35 | 274 36 | 37 | {{0, 20}, {320, 548}} 38 | 39 | 40 | 41 | 42 | 3 43 | MC45OAA 44 | 45 | NO 46 | 47 | 48 | IBUIScreenMetrics 49 | 50 | YES 51 | 52 | 53 | 54 | 55 | 56 | {320, 568} 57 | {568, 320} 58 | 59 | 60 | IBCocoaTouchFramework 61 | Retina 4 Full Screen 62 | 2 63 | 64 | IBCocoaTouchFramework 65 | 66 | 67 | 68 | 69 | 70 | 71 | view 72 | 73 | 74 | 75 | 7 76 | 77 | 78 | 79 | 80 | 81 | 0 82 | 83 | 84 | 85 | 86 | 87 | -1 88 | 89 | 90 | File's Owner 91 | 92 | 93 | -2 94 | 95 | 96 | 97 | 98 | 6 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | OCViewController 107 | com.apple.InterfaceBuilder.IBCocoaTouchPlugin 108 | UIResponder 109 | com.apple.InterfaceBuilder.IBCocoaTouchPlugin 110 | com.apple.InterfaceBuilder.IBCocoaTouchPlugin 111 | 112 | 113 | 114 | 115 | 116 | 17 117 | 118 | 119 | 120 | 121 | OCViewController 122 | UIViewController 123 | 124 | IBProjectSource 125 | ./Classes/OCViewController.h 126 | 127 | 128 | 129 | 130 | 0 131 | IBCocoaTouchFramework 132 | YES 133 | 3 134 | YES 135 | 2083 136 | 137 | 138 | -------------------------------------------------------------------------------- /OCExpandableButton/main.m: -------------------------------------------------------------------------------- 1 | // 2 | // main.m 3 | // OCExpandableButton 4 | // 5 | // Created by Oliver Rickard on 5/18/13. 6 | // Copyright (c) 2013 Oliver Rickard. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | #import "OCAppDelegate.h" 12 | 13 | int main(int argc, char *argv[]) 14 | { 15 | @autoreleasepool { 16 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([OCAppDelegate class])); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /animation.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocrickard/OCExpandableButton/380231abdc6e44842431d5658c2dda41799feaad/animation.gif -------------------------------------------------------------------------------- /readme.markdown: -------------------------------------------------------------------------------- 1 | #OCExpandableButton# 2 | 3 | OCExpandableButton is a VERY simple component in native Objective C that mimics the behavior of the expanding menu in the Sparrow mail app. You give it an array of subviews, and it presents them when it's activated. It is a normal subview, so you're in charge of rotation, and anything extra. 4 | 5 |

6 | 7 | ##Usage## 8 | 9 | Usage of the control is totally simple, it works just like any other UIView: 10 | 11 | ```objc 12 | button = [[OCExpandableButton alloc] initWithFrame:CGRectMake(self.view.bounds.size.width - 57, self.view.bounds.size.height - 57, 37, 37) subviews:subviews]; 13 | [self.view addSubview:button]; 14 | ``` 15 | 16 | The array of subviews will be positioned and aligned upon opening of the control. The frame for the control should be a square region. The blue "arrow" button will be inset by 4 pixels from this initial rect. 17 | 18 | If you want to manually open/close the component (say the screen rotates, or the user begins to scroll), then you can use the following methods: 19 | 20 | ```objc 21 | //Opens the control if the control is currently closed. No effect if the button 22 | // is already open. 23 | - (void)open; 24 | 25 | //Closes the control if open. No effect if already closed. 26 | - (void)close; 27 | ``` 28 | 29 | You can make the component reveal with left or right alignment using: 30 | ```objc 31 | button.alignment = OCExpandableButtonAlignmentLeft; 32 | ``` 33 | or 34 | ```objc 35 | button.alignment = OCExpandableButtonAlignmentRight; 36 | ``` 37 | 38 | You can use the delegate property in order to notify of the control's opening/closure. 39 | 40 | @interface MyClass : NSObject 41 | ... 42 | @end 43 | 44 | @implementation MyClass 45 | ... 46 | - (void)expandableButtonClosed:(OCExpandableButton*)button 47 | { ... } 48 | - (void)expandableButtonOpened:(OCExpandableButton*)button 49 | { ... } 50 | 51 | TODO: 52 | 53 | - Implement inner shadows like they have in Sparrow - Not sure what the right API looks like here. Maybe just letting user specify images, or maybe using masks and drawing inner shadows manually? 54 | - Suggestions? 55 | --------------------------------------------------------------------------------