├── .gitignore ├── .travis.yml ├── LICENSE ├── MinimalTabBar.podspec ├── Pod ├── JDMinimalTabBar.h ├── JDMinimalTabBar.m ├── JDMinimalTabBarButton.h ├── JDMinimalTabBarButton.m ├── JDMinimalTabBarController.h ├── JDMinimalTabBarController.m ├── JDViewHitTestOverride.h └── JDViewHitTestOverride.m └── README.md /.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 | # Bundler 23 | .bundle 24 | 25 | # We recommend against adding the Pods directory to your .gitignore. However 26 | # you should judge for yourself, the pros and cons are mentioned at: 27 | # http://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control 28 | # 29 | # Note: if you ignore the Pods directory, make sure to uncomment 30 | # `pod install` in .travis.yml 31 | # 32 | # Pods/ 33 | Example/Pods 34 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # references: 2 | # * http://www.objc.io/issue-6/travis-ci.html 3 | # * https://github.com/supermarin/xcpretty#usage 4 | 5 | language: objective-c 6 | # cache: cocoapods 7 | # podfile: Example/Podfile 8 | # before_install: cd Example && pod install && cd - 9 | install: 10 | - gem install xcpretty --no-rdoc --no-ri --no-document --quiet 11 | script: 12 | - set -o pipefail && xcodebuild test -workspace Example/MinimalTabBar.xcworkspace -scheme MinimalTabBar -sdk iphonesimulator ONLY_ACTIVE_ARCH=NO | xcpretty -c 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 jamesdunay@gmail.com 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /MinimalTabBar.podspec: -------------------------------------------------------------------------------- 1 | 2 | Pod::Spec.new do |s| 3 | s.name = "MinimalTabBar" 4 | s.version = "0.1.0" 5 | s.summary = "A minimizing tabbar for iOS" 6 | s.description = <<-DESC 7 | MinimalTabBar is an elegant replacement for the standard UITabBar found on iOS. It includes optional navigation gestures to improve UX and reduce the UI footprint. Like the UITabBar, implimentation requires an array of ViewControllers and UITabBar items. 8 | DESC 9 | s.homepage = "https://github.com/jamesdunay/MinimalTabBar" 10 | # s.screenshots = "www.example.com/screenshots_1", "www.example.com/screenshots_2" 11 | s.license = 'MIT' 12 | s.author = { "jamesdunay@gmail.com" => "jamesdunay@gmail.com" } 13 | s.source = { :git => "https://github.com/jamesdunay/MinimalTabBar.git", :tag => s.version.to_s } 14 | # s.social_media_url = 'https://twitter.com/' 15 | 16 | s.platform = :ios, '7.0' 17 | s.requires_arc = true 18 | 19 | s.source_files = 'Pod/*{h,m}' 20 | end 21 | -------------------------------------------------------------------------------- /Pod/JDMinimalTabBar.h: -------------------------------------------------------------------------------- 1 | // 2 | // MinimalBar.h 3 | // MinimalTabBar 4 | // 5 | // Created by james.dunay on 11/19/14. 6 | // Copyright (c) 2014 James.Dunay. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @protocol MinimalBarDelegate 12 | 13 | - (void)didSwitchToIndex:(NSUInteger)pageIndex; 14 | - (void)changeToPageIndex:(NSUInteger)pageIndex; 15 | - (void)manualOffsetScrollview:(CGFloat)offset; 16 | - (void)displayAllScreensWithStartingDisplayOn:(CGFloat)startingPosition; 17 | - (void)sendScrollViewToPoint:(CGPoint)point; 18 | - (void)displayViewAtIndex:(NSInteger)index; 19 | 20 | @end 21 | 22 | @interface JDMinimalTabBar : UIView 23 | 24 | @property (nonatomic) CGFloat displayOverviewYCoord; 25 | @property (nonatomic) CGFloat screenHeight; 26 | @property (nonatomic) CGSize defaultFrameSize; 27 | @property (nonatomic) BOOL showTitles; 28 | @property (nonatomic) BOOL hidesTitlesWhenSelected; 29 | 30 | @property (nonatomic) id mMinimalBarDelegate; 31 | @property (nonatomic, strong) UIColor* defaultTintColor; 32 | @property (nonatomic, strong) UIColor* selectedTintColor; 33 | 34 | - (void)scrollOverviewButtonsWithPercentage:(CGFloat)offsetPercentage; 35 | - (void)returnMenuToSelected:(NSUInteger)index; 36 | - (void)createButtonItems:(NSArray *)viewControllers; 37 | 38 | @end -------------------------------------------------------------------------------- /Pod/JDMinimalTabBar.m: -------------------------------------------------------------------------------- 1 | // 2 | // MinimalBar.m 3 | // MinimalTabBar 4 | // 5 | // Created by james.dunay on 11/19/14. 6 | // Copyright (c) 2014 James.Dunay. All rights reserved. 7 | // 8 | 9 | #import "JDMinimalTabBar.h" 10 | #import "JDMinimalTabBarButton.h" 11 | #import 12 | 13 | #pragma Mark Enums --- 14 | 15 | typedef enum : NSUInteger { 16 | MenuStateDefault = (1 << 0), 17 | MenuStateDisplayed = (1 << 1), 18 | MenuStateFullyOpened = (1 << 2), 19 | } MenuState; 20 | 21 | @interface JDMinimalTabBar () 22 | 23 | @property (nonatomic) CGFloat firstX; 24 | @property (nonatomic) CGFloat firstY; 25 | @property (nonatomic) CGFloat lastXOffset; 26 | @property (nonatomic, strong) NSMutableArray *buttons; 27 | @property (nonatomic, strong) NSArray *adjustableButtonConstaints; 28 | @property (nonatomic) BOOL isDisplayingAll; 29 | @end 30 | 31 | @implementation JDMinimalTabBar 32 | 33 | - (void)layoutSubviews { 34 | [super layoutSubviews]; 35 | [self addConstraints:[self defaultConstraints]]; 36 | } 37 | 38 | 39 | 40 | #pragma Mark Minimal Bar Buttons --- 41 | 42 | - (void)createButtonItems:(NSArray *)viewControllers { 43 | self.buttons = [[NSMutableArray alloc] init]; 44 | 45 | [viewControllers enumerateObjectsUsingBlock: ^(UIViewController* viewController, NSUInteger idx, BOOL *stop) { 46 | 47 | JDMinimalTabBarButton *mbButton = [[JDMinimalTabBarButton alloc] initWithButtonWithTabBarItem:viewController.tabBarItem]; 48 | mbButton.defaultTintColor = _defaultTintColor; 49 | mbButton.selectedTintColor = _selectedTintColor; 50 | mbButton.showTitle = _showTitles; 51 | mbButton.hideTitleWhenSelected = _hidesTitlesWhenSelected; 52 | 53 | UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(touchedButton:)]; 54 | UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(panSelectedButton:)]; 55 | UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(touchAndHold:)]; 56 | 57 | [mbButton addGestureRecognizer:tap]; 58 | [mbButton addGestureRecognizer:pan]; 59 | [mbButton addGestureRecognizer:longPress]; 60 | 61 | [self.buttons addObject:mbButton]; 62 | [self addSubview:mbButton]; 63 | }]; 64 | 65 | [self shouldEnablePanGestures:NO]; 66 | } 67 | 68 | 69 | 70 | #pragma mark Tint Color --- 71 | 72 | -(void)setDefaultTintColor:(UIColor *)defaultTintColor{ 73 | _defaultTintColor = defaultTintColor; 74 | [self.buttons enumerateObjectsUsingBlock: ^(JDMinimalTabBarButton *mbButton, NSUInteger idx, BOOL *stop) { 75 | mbButton.tintColor = defaultTintColor; 76 | }]; 77 | } 78 | 79 | -(void)setSelectedTintColor:(UIColor *)selectedTintColor{ 80 | _selectedTintColor = selectedTintColor; 81 | [self.buttons enumerateObjectsUsingBlock: ^(JDMinimalTabBarButton *mbButton, NSUInteger idx, BOOL *stop) { 82 | mbButton.selectedTintColor = selectedTintColor; 83 | }]; 84 | } 85 | 86 | 87 | 88 | #pragma mark Button Constraints --- 89 | 90 | - (NSArray *)defaultConstraints { 91 | NSMutableArray *constraints = [[NSMutableArray alloc] init]; 92 | [self.buttons enumerateObjectsUsingBlock: ^(JDMinimalTabBarButton *mbButton, NSUInteger idx, BOOL *stop) { 93 | [constraints addObject:[NSLayoutConstraint constraintWithItem:mbButton 94 | attribute:NSLayoutAttributeWidth 95 | relatedBy:NSLayoutRelationEqual 96 | toItem:nil 97 | attribute:NSLayoutAttributeNotAnAttribute 98 | multiplier:1.f 99 | constant:self.frame.size.width / self.numberOfViews]]; 100 | 101 | [constraints addObject:[NSLayoutConstraint constraintWithItem:mbButton 102 | attribute:NSLayoutAttributeTop 103 | relatedBy:NSLayoutRelationEqual 104 | toItem:self 105 | attribute:NSLayoutAttributeTop 106 | multiplier:1.f 107 | constant:0]]; 108 | 109 | [constraints addObject:[NSLayoutConstraint constraintWithItem:mbButton 110 | attribute:NSLayoutAttributeBottom 111 | relatedBy:NSLayoutRelationEqual 112 | toItem:self 113 | attribute:NSLayoutAttributeBottom 114 | multiplier:1.f 115 | constant:0]]; 116 | }]; 117 | 118 | if (!self.adjustableButtonConstaints) { 119 | self.adjustableButtonConstaints = [self defaultAdjustableConstraints]; 120 | [constraints addObjectsFromArray:self.adjustableButtonConstaints]; 121 | } 122 | return [constraints copy]; 123 | } 124 | 125 | -(NSArray*)defaultAdjustableConstraints{ 126 | NSMutableArray* constraints = [[NSMutableArray alloc] init]; 127 | [self.buttons enumerateObjectsUsingBlock: ^(JDMinimalTabBarButton *mbButton, NSUInteger idx, BOOL *stop) { 128 | NSLayoutConstraint *adjustableConstraint = [NSLayoutConstraint constraintWithItem:self.buttons[idx] 129 | attribute:NSLayoutAttributeLeft 130 | relatedBy:NSLayoutRelationEqual 131 | toItem:self 132 | attribute:NSLayoutAttributeLeft 133 | multiplier:1.f 134 | constant:(self.frame.size.width / self.numberOfViews) * idx]; 135 | [constraints addObject:adjustableConstraint]; 136 | }]; 137 | return [constraints copy]; 138 | } 139 | 140 | 141 | 142 | #pragma Mark Tap Button --- 143 | 144 | - (void)touchedButton:(id)sender { 145 | JDMinimalTabBarButton *mbButton = (JDMinimalTabBarButton *)[sender view]; 146 | if (!self.isDisplayingAll) { 147 | switch ([mbButton buttonState]) { 148 | case ButtonStateDisplayedInactive: 149 | [mbButton setButtonState:ButtonStateSelected]; 150 | [self collapseAllButtons]; 151 | [self.mMinimalBarDelegate changeToPageIndex:mbButton.tag]; 152 | break; 153 | case ButtonStateDisplayedActive: 154 | [mbButton setButtonState:ButtonStateSelected]; 155 | [self collapseAllButtons]; 156 | [self.mMinimalBarDelegate changeToPageIndex:mbButton.tag]; 157 | break; 158 | case ButtonStateSelected: 159 | [self displayAllButtons]; 160 | break; 161 | default: 162 | break; 163 | } 164 | }else{ 165 | [self.mMinimalBarDelegate displayViewAtIndex:mbButton.tag]; 166 | } 167 | } 168 | 169 | 170 | 171 | #pragma Mark Visual Button Adjustments --- 172 | 173 | - (void)collapseAllButtons { 174 | 175 | [self shouldEnablePanGestures:YES]; 176 | CGFloat mbButtonWidth = self.frame.size.width / self.buttons.count; 177 | 178 | void (^alphaAnimation)() = ^{ [self hideNonSelectedMenuItems]; }; 179 | 180 | void (^animations)(void) = ^{ 181 | [self.adjustableButtonConstaints enumerateObjectsUsingBlock: ^(NSLayoutConstraint *constraint, NSUInteger idx, BOOL *stop) { 182 | constraint.constant = mbButtonWidth * (self.adjustableButtonConstaints.count / 2); 183 | }]; 184 | [self layoutIfNeeded]; 185 | }; 186 | 187 | [UIView animateWithDuration:.65f 188 | delay:0.f 189 | usingSpringWithDamping:85 190 | initialSpringVelocity:12 191 | options:UIViewAnimationOptionCurveEaseOut | UIViewAnimationOptionAllowUserInteraction 192 | animations:animations 193 | completion:nil]; 194 | 195 | [UIView animateWithDuration:0.10 196 | delay:0.0 197 | options:UIViewAnimationOptionCurveEaseInOut 198 | animations:alphaAnimation 199 | completion:nil]; 200 | } 201 | 202 | - (void)displayAllButtons { 203 | [self shouldEnablePanGestures:NO]; 204 | 205 | [self.buttons enumerateObjectsUsingBlock: ^(JDMinimalTabBarButton *mbButton, NSUInteger idx, BOOL *stop) { 206 | if (mbButton.buttonState == ButtonStateSelected) mbButton.buttonState = ButtonStateDisplayedActive; 207 | else mbButton.buttonState = ButtonStateDisplayedInactive; 208 | }]; 209 | 210 | void (^alphaAnimations)(void) = ^{ [self showAllButtons]; }; 211 | void (^movementAnimations)(void) = ^{ 212 | [self.adjustableButtonConstaints enumerateObjectsUsingBlock: ^(NSLayoutConstraint *constraint, NSUInteger idx, BOOL *stop) { 213 | constraint.constant = self.frame.size.width / self.adjustableButtonConstaints.count * idx; 214 | }]; 215 | [self layoutIfNeeded]; 216 | }; 217 | 218 | [UIView animateWithDuration:.8f 219 | delay:0.f 220 | usingSpringWithDamping:150 221 | initialSpringVelocity:16 222 | options:UIViewAnimationOptionAllowUserInteraction | UIViewAnimationOptionCurveEaseOut 223 | animations:movementAnimations 224 | completion:nil]; 225 | 226 | [UIView animateWithDuration:.4f 227 | delay:0.025f 228 | options:UIViewAnimationOptionCurveEaseOut | UIViewAnimationOptionAllowUserInteraction 229 | animations:alphaAnimations 230 | completion:nil]; 231 | } 232 | 233 | - (void)showAllButtons { 234 | [self.buttons enumerateObjectsUsingBlock: ^(JDMinimalTabBarButton *mbButton, NSUInteger idx, BOOL *stop) { 235 | mbButton.alpha = 1.f; 236 | }]; 237 | } 238 | 239 | - (void)hideNonSelectedMenuItems { 240 | [self.buttons enumerateObjectsUsingBlock: ^(JDMinimalTabBarButton *mbButton, NSUInteger idx, BOOL *stop) { 241 | if (mbButton.buttonState != ButtonStateSelected) { 242 | mbButton.alpha = 0.f; 243 | } 244 | }]; 245 | } 246 | 247 | - (void)setShowTitles:(BOOL)showTitles{ 248 | _showTitles = showTitles; 249 | [self.buttons enumerateObjectsUsingBlock: ^(JDMinimalTabBarButton *mbButton, NSUInteger idx, BOOL *stop) { 250 | mbButton.showTitle = showTitles; 251 | }]; 252 | } 253 | 254 | -(void)setHidesTitlesWhenSelected:(BOOL)hidesTitlesWhenSelected{ 255 | _hidesTitlesWhenSelected = hidesTitlesWhenSelected; 256 | [self.buttons enumerateObjectsUsingBlock: ^(JDMinimalTabBarButton *mbButton, NSUInteger idx, BOOL *stop) { 257 | mbButton.hideTitleWhenSelected = hidesTitlesWhenSelected; 258 | }]; 259 | } 260 | 261 | #pragma Mark Pan Methods --- 262 | 263 | - (void)panSelectedButton:(UIPanGestureRecognizer *)gesture { 264 | CGPoint translatedPoint = [gesture translationInView:self]; 265 | if ([gesture state] == UIGestureRecognizerStateBegan) { 266 | self.firstX = [[gesture view] center].x; 267 | self.firstY = [[gesture view] center].y; 268 | } 269 | 270 | //Matching swipe with scrollview 271 | [self.mMinimalBarDelegate manualOffsetScrollview:(self.lastXOffset - translatedPoint.x)]; 272 | self.lastXOffset = translatedPoint.x; 273 | 274 | //Reset View center to match finger swiping 275 | translatedPoint = CGPointMake(self.firstX + translatedPoint.x, self.firstY); 276 | [[gesture view] setCenter:translatedPoint]; 277 | 278 | if ([gesture state] == UIGestureRecognizerStateEnded) { 279 | CGPoint endingLocation = [gesture translationInView:self]; 280 | CGFloat velocityX = (0.2 *[gesture velocityInView:self].x); 281 | CGFloat animationDuration = (ABS(velocityX) * .0002) + .2; 282 | 283 | if (endingLocation.x <= -50) [self switchToPageNext:YES]; 284 | else if (endingLocation.x >= 50) [self switchToPageNext:NO]; 285 | else [self returnScrollViewToSelectedTab]; 286 | 287 | void (^animations)(void) = ^{ [[gesture view] setCenter:CGPointMake(self.firstX, self.firstY)]; }; 288 | 289 | [UIView animateWithDuration:animationDuration animations:animations completion:nil]; 290 | self.lastXOffset = 0; 291 | } 292 | } 293 | 294 | - (void)switchToPageNext:(BOOL)next { 295 | NSInteger indextAdjustment = next ? next : -1; 296 | NSInteger activeIndex = [self indexOfActiveButton]; 297 | NSInteger targetedIndex = activeIndex + indextAdjustment; 298 | 299 | if (targetedIndex >= 0 && targetedIndex < self.buttons.count) { 300 | JDMinimalTabBarButton *activeButton = self.buttons[activeIndex]; 301 | JDMinimalTabBarButton *nextButton = self.buttons[activeIndex + indextAdjustment]; 302 | 303 | [activeButton setButtonState:ButtonStateDisplayedInactive]; 304 | [nextButton setButtonState:ButtonStateSelected]; 305 | 306 | [self bringSubviewToFront:nextButton]; 307 | void (^animations)(void) = ^{ 308 | [self.adjustableButtonConstaints enumerateObjectsUsingBlock: ^(NSLayoutConstraint *constraint, NSUInteger idx, BOOL *stop) { 309 | if ([(JDMinimalTabBarButton *)constraint.firstItem isEqual : self.buttons[[self indexOfActiveButton]]]) { 310 | constraint.constant = self.frame.size.width / 5 * 2; 311 | } 312 | }]; 313 | activeButton.alpha = 0; 314 | nextButton.alpha = 1; 315 | }; 316 | 317 | [UIView animateWithDuration:.2 animations:animations completion:nil]; 318 | [self.mMinimalBarDelegate didSwitchToIndex:nextButton.tag]; 319 | } 320 | } 321 | 322 | - (void)returnScrollViewToSelectedTab { 323 | NSInteger activeIndex = [self indexOfActiveButton]; 324 | [self.mMinimalBarDelegate sendScrollViewToPoint:CGPointMake(activeIndex * self.frame.size.width, 0)]; 325 | } 326 | 327 | 328 | 329 | #pragma Mark Touch And Hold 330 | 331 | - (void)positionAllButtonsForOverView { 332 | [self shouldEnablePanGestures:NO]; 333 | 334 | void (^animations)(void) = animations = ^{ 335 | 336 | [self.adjustableButtonConstaints enumerateObjectsUsingBlock: ^(NSLayoutConstraint *constraint, NSUInteger idx, BOOL *stop) { 337 | constraint.constant = (self.frame.size.width / self.adjustableButtonConstaints.count * idx) + (idx * 10); 338 | }]; 339 | [self layoutIfNeeded]; 340 | 341 | CGFloat spacingMultiplyer = ([self indexOfActiveButton] * 10); 342 | CGFloat defaultPosition = (self.frame.size.width - (self.frame.size.width/self.buttons.count))/2; 343 | CGFloat buttonOffset = [self indexOfActiveButton] * (self.frame.size.width/self.buttons.count); 344 | CGFloat xPosToScrollButtonsTo = defaultPosition - spacingMultiplyer - buttonOffset; 345 | 346 | self.frame = CGRectMake(xPosToScrollButtonsTo, self.frame.origin.y, self.frame.size.width, self.frame.size.height); 347 | 348 | [self showAllButtonsInOverviewMode]; 349 | }; 350 | 351 | [UIView animateWithDuration:.6f 352 | delay:0.f 353 | usingSpringWithDamping:150 354 | initialSpringVelocity:15 355 | options:UIViewAnimationOptionAllowUserInteraction | UIViewAnimationOptionCurveEaseOut 356 | animations:animations 357 | completion:nil]; 358 | } 359 | 360 | - (void)showAllButtonsInOverviewMode { 361 | [self.buttons enumerateObjectsUsingBlock: ^(JDMinimalTabBarButton *mbButton, NSUInteger idx, BOOL *stop) { 362 | mbButton.buttonState = ButtonStateDisplayedInactive; 363 | mbButton.alpha = 1.f; 364 | }]; 365 | } 366 | 367 | - (void)scrollOverviewButtonsWithPercentage:(CGFloat)offsetPercentage { 368 | if (self.isDisplayingAll) { 369 | CGFloat squareButtonDimension = self.frame.size.width / self.buttons.count; 370 | CGFloat defaultPosition = (self.frame.size.width - squareButtonDimension) / 2; 371 | CGFloat offsetAmount = offsetPercentage * (squareButtonDimension + 10); 372 | self.frame = CGRectMake(defaultPosition - offsetAmount, self.frame.origin.y, self.frame.size.width, self.frame.size.height); 373 | } 374 | } 375 | 376 | - (void)touchAndHold:(UIGestureRecognizer *)longPressGesture { 377 | if (!self.isDisplayingAll) { 378 | self.isDisplayingAll = YES; 379 | [self.mMinimalBarDelegate displayAllScreensWithStartingDisplayOn:longPressGesture.view.tag]; 380 | [self positionAllButtonsForOverView]; 381 | } 382 | } 383 | 384 | 385 | 386 | #pragma Mark Helper Methods --- 387 | 388 | - (void)shouldEnablePanGestures:(BOOL)enable { 389 | [self.buttons enumerateObjectsUsingBlock: ^(JDMinimalTabBarButton *mbButton, NSUInteger idx, BOOL *stop) { 390 | [mbButton.gestureRecognizers enumerateObjectsUsingBlock: ^(UIGestureRecognizer *gesture, NSUInteger idx, BOOL *stop) { 391 | if ([gesture isKindOfClass:[UIPanGestureRecognizer class]]) { 392 | gesture.enabled = enable; 393 | } 394 | }]; 395 | }]; 396 | } 397 | 398 | - (NSUInteger)indexOfActiveButton { 399 | __block NSUInteger index = 0; 400 | [self.buttons enumerateObjectsUsingBlock: ^(JDMinimalTabBarButton *mbButton, NSUInteger idx, BOOL *stop) { 401 | if (mbButton.buttonState == ButtonStateSelected) { 402 | index = idx; 403 | } 404 | }]; 405 | NSLog(@"index %ld", index); 406 | return index; 407 | } 408 | 409 | - (void)selectedButtonAtIndex:(NSInteger)index { 410 | [self.buttons[index] setButtonState:ButtonStateSelected]; 411 | [self bringSubviewToFront:self.buttons[index]]; 412 | } 413 | 414 | - (NSUInteger)numberOfViews { 415 | return [self.buttons count]; 416 | } 417 | 418 | - (void)returnMenuToSelected:(NSUInteger)index { 419 | _isDisplayingAll = NO; 420 | [self selectedButtonAtIndex:index]; 421 | [self collapseAllButtons]; 422 | } 423 | 424 | @end 425 | -------------------------------------------------------------------------------- /Pod/JDMinimalTabBarButton.h: -------------------------------------------------------------------------------- 1 | // 2 | // MinimalButton.h 3 | // MinimalTabBar 4 | // 5 | // Created by james.dunay on 12/7/14. 6 | // Copyright (c) 2014 James.Dunay. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | typedef enum : NSUInteger { 12 | ButtonStateDisplayedInactive = (1 << 0), 13 | ButtonStateSelected = (1 << 1), 14 | ButtonStateDisplayedActive = (1 << 2) 15 | } ButtonState; 16 | 17 | 18 | 19 | @interface JDMinimalTabBarButton : UIButton 20 | 21 | @property (nonatomic) ButtonState buttonState; 22 | 23 | @property (nonatomic, strong) UIColor* selectedTintColor; 24 | @property (nonatomic, strong) UIColor* defaultTintColor; 25 | @property (nonatomic) UILabel* title; 26 | @property (nonatomic) BOOL showTitle; 27 | @property (nonatomic) BOOL hideTitleWhenSelected; 28 | 29 | -(id)initWithButtonWithTabBarItem:(UITabBarItem*)tabBarItem; 30 | 31 | @end -------------------------------------------------------------------------------- /Pod/JDMinimalTabBarButton.m: -------------------------------------------------------------------------------- 1 | // 2 | // MinimalButton.m 3 | // MinimalTabBar 4 | // 5 | // Created by james.dunay on 12/7/14. 6 | // Copyright (c) 2014 James.Dunay. All rights reserved. 7 | // 8 | 9 | #import "JDMinimalTabBarButton.h" 10 | 11 | @implementation JDMinimalTabBarButton 12 | 13 | -(id)initWithButtonWithTabBarItem:(UITabBarItem*)tabBarItem{ 14 | self = [super init]; 15 | if (self) { 16 | 17 | self.tag = tabBarItem.tag; 18 | self.translatesAutoresizingMaskIntoConstraints = NO; 19 | 20 | _buttonState = ButtonStateDisplayedInactive; 21 | 22 | [[self imageView] setContentMode:UIViewContentModeScaleAspectFit]; 23 | 24 | UIImage *defaultImage = [tabBarItem.image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; 25 | UIImage *selectedImage = [tabBarItem.selectedImage imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; 26 | 27 | [self setImage:defaultImage forState:UIControlStateNormal]; 28 | [self setImage:selectedImage forState:UIControlStateSelected]; 29 | 30 | _title = [[UILabel alloc] init]; 31 | _title.translatesAutoresizingMaskIntoConstraints = NO; 32 | _title.text = tabBarItem.title; 33 | _title.font = [UIFont fontWithName:@"Avenir-Heavy" size:10.f]; 34 | _title.textColor = [UIColor whiteColor]; 35 | [_title sizeToFit]; 36 | 37 | [self addSubview:_title]; 38 | 39 | [self addConstraints:[self defaultConstraints]]; 40 | } 41 | return self; 42 | } 43 | 44 | 45 | -(NSArray*)defaultConstraints{ 46 | 47 | NSMutableArray* constraints = [[NSMutableArray alloc] init]; 48 | 49 | [constraints addObject:[NSLayoutConstraint constraintWithItem:_title 50 | attribute:NSLayoutAttributeBottom 51 | relatedBy:NSLayoutRelationEqual 52 | toItem:self 53 | attribute:NSLayoutAttributeBottom 54 | multiplier:1.f 55 | constant:-5]]; 56 | 57 | [constraints addObject:[NSLayoutConstraint constraintWithItem:_title 58 | attribute:NSLayoutAttributeCenterX 59 | relatedBy:NSLayoutRelationEqual 60 | toItem:self 61 | attribute:NSLayoutAttributeCenterX 62 | multiplier:1.f 63 | constant:0]]; 64 | 65 | return [constraints copy]; 66 | } 67 | 68 | -(void)setButtonState:(ButtonState)buttonState{ 69 | _buttonState = buttonState; 70 | 71 | switch (buttonState) { 72 | case ButtonStateDisplayedActive: 73 | [self setButtonToTintColor:_selectedTintColor]; 74 | [self setSelected:NO]; 75 | break; 76 | 77 | case ButtonStateDisplayedInactive: 78 | [self setButtonToTintColor:_defaultTintColor]; 79 | [self setSelected:NO]; 80 | break; 81 | 82 | case ButtonStateSelected: 83 | [self setButtonToTintColor:_selectedTintColor]; 84 | [self setSelected:YES]; 85 | break; 86 | 87 | default: 88 | break; 89 | } 90 | } 91 | 92 | -(void)setSelected:(BOOL)selected{ 93 | [super setSelected:selected]; 94 | if (_hideTitleWhenSelected) { 95 | _title.hidden = selected; 96 | } 97 | } 98 | 99 | -(void)setHighlighted:(BOOL)highlighted{ 100 | // Need to disable Highlighting to keep icon from flicker 101 | } 102 | 103 | -(void)setDefaultTintColor:(UIColor *)defaultTintColor{ 104 | _defaultTintColor = defaultTintColor; 105 | [self setButtonToTintColor:defaultTintColor]; 106 | } 107 | 108 | -(void)setSelectedTintColor:(UIColor *)selectedTintColor{ 109 | _selectedTintColor = selectedTintColor; 110 | } 111 | 112 | -(void)setButtonToTintColor:(UIColor*)tintColor{ 113 | _title.textColor = tintColor; 114 | self.imageView.tintColor = tintColor; 115 | } 116 | 117 | -(void)setShowTitle:(BOOL)showTitle{ 118 | _showTitle = showTitle; 119 | _title.hidden = !showTitle; 120 | } 121 | 122 | 123 | 124 | @end 125 | -------------------------------------------------------------------------------- /Pod/JDMinimalTabBarController.h: -------------------------------------------------------------------------------- 1 | // 2 | // UIMinimalBarController.h 3 | // MinimalTabBar 4 | // 5 | // Created by james.dunay on 11/19/14. 6 | // Copyright (c) 2014 James.Dunay. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "JDMinimalTabBar.h" 11 | 12 | @interface JDMinimalTabBarController : UIViewController 13 | 14 | @property (nonatomic, strong) NSArray *viewControllers; 15 | @property (nonatomic, strong) JDMinimalTabBar *minimalBar; 16 | 17 | @end 18 | -------------------------------------------------------------------------------- /Pod/JDMinimalTabBarController.m: -------------------------------------------------------------------------------- 1 | // 2 | // UIMinimalBarController.m 3 | // MinimalTabBar 4 | // 5 | // Created by james.dunay on 11/19/14. 6 | // Copyright (c) 2014 James.Dunay. All rights reserved. 7 | // 8 | 9 | #import "JDMinimalTabBarController.h" 10 | #import "JDViewHitTestOverride.h" 11 | #import 12 | 13 | static CGFloat minimalBarHeight = 70.f; 14 | 15 | @interface JDMinimalTabBarController () 16 | @property (nonatomic, strong) UIScrollView *scrollView; 17 | @property (nonatomic, strong) JDViewHitTestOverride *coverView; 18 | @property (nonatomic) CGAffineTransform viewControllerTransform; 19 | @property (nonatomic) CGAffineTransform scrollViewTransform; 20 | @end 21 | 22 | @implementation JDMinimalTabBarController 23 | 24 | - (id)init { 25 | self = [super init]; 26 | if (self) { 27 | } 28 | return self; 29 | } 30 | 31 | - (void)viewDidLoad { 32 | [super viewDidLoad]; 33 | [self setupViews]; 34 | } 35 | 36 | - (void)setupViews { 37 | _minimalBar = [[JDMinimalTabBar alloc] init]; 38 | _viewControllers = [[NSArray alloc] init]; 39 | _scrollView = [[UIScrollView alloc] init]; 40 | 41 | _coverView = [[JDViewHitTestOverride alloc] init]; 42 | _coverView.translatesAutoresizingMaskIntoConstraints = NO; 43 | _coverView.scrollView = _scrollView; 44 | [self.view addSubview:_coverView]; 45 | 46 | [_scrollView setPagingEnabled:YES]; 47 | [_scrollView setDelegate:self]; 48 | [_scrollView setClipsToBounds:NO]; 49 | [_scrollView setAutoresizesSubviews:NO]; 50 | [_scrollView setFrame:self.view.frame]; 51 | _scrollViewTransform = _scrollView.transform; 52 | [_coverView addSubview:_scrollView]; 53 | 54 | _minimalBar.mMinimalBarDelegate = self; 55 | _minimalBar.screenHeight = self.view.frame.size.height; 56 | _minimalBar.translatesAutoresizingMaskIntoConstraints = NO; 57 | 58 | 59 | [self.view addSubview:_minimalBar]; 60 | 61 | [self.view addConstraints:[self defaultConstraints]]; 62 | [self allowScrollViewUserInteraction:NO]; 63 | } 64 | 65 | - (NSArray *)defaultConstraints { 66 | NSMutableArray *constraints = [[NSMutableArray alloc] init]; 67 | 68 | [constraints addObjectsFromArray: 69 | [NSLayoutConstraint constraintsWithVisualFormat:@"H:|[_coverView]|" 70 | options:0 71 | metrics:0 72 | views:NSDictionaryOfVariableBindings(_coverView)]]; 73 | 74 | [constraints addObjectsFromArray: 75 | [NSLayoutConstraint constraintsWithVisualFormat:@"V:|[_coverView]|" 76 | options:0 77 | metrics:0 78 | views:NSDictionaryOfVariableBindings(_coverView)]]; 79 | 80 | [constraints addObjectsFromArray: 81 | [NSLayoutConstraint constraintsWithVisualFormat:@"H:|[_minimalBar]|" 82 | options:0 83 | metrics:0 84 | views:NSDictionaryOfVariableBindings(_minimalBar) 85 | ]]; 86 | 87 | 88 | [constraints addObject:[NSLayoutConstraint constraintWithItem:_minimalBar 89 | attribute:NSLayoutAttributeHeight 90 | relatedBy:NSLayoutRelationEqual 91 | toItem:nil 92 | attribute:NSLayoutAttributeNotAnAttribute 93 | multiplier:1 94 | constant:minimalBarHeight 95 | ]]; 96 | 97 | [constraints addObject:[NSLayoutConstraint constraintWithItem:_minimalBar 98 | attribute:NSLayoutAttributeBottom 99 | relatedBy:NSLayoutRelationEqual 100 | toItem:self.view 101 | attribute:NSLayoutAttributeBottom 102 | multiplier:1 103 | constant:0.f 104 | ]]; 105 | 106 | return [constraints copy]; 107 | } 108 | 109 | #pragma Mark Delegate Methods 110 | #pragma Mark Tapped Delegates 111 | 112 | - (void)changeToPageIndex:(NSUInteger)pageIndex { 113 | CGFloat xPoint = pageIndex * self.view.frame.size.width; 114 | [self.scrollView setContentOffset:CGPointMake(xPoint, 0)]; 115 | } 116 | 117 | 118 | 119 | #pragma Mark Pan Delegates 120 | 121 | - (void)manualOffsetScrollview:(CGFloat)offset { 122 | CGPoint newOffset = CGPointMake(self.scrollView.contentOffset.x + offset, 0); 123 | if (newOffset.x >= 0 && newOffset.x <= (self.scrollView.contentSize.width - self.view.frame.size.width)) { 124 | self.scrollView.contentOffset = newOffset; 125 | } 126 | } 127 | 128 | - (void)sendScrollViewToPoint:(CGPoint)point { 129 | [self.scrollView setContentOffset:point animated:YES]; 130 | } 131 | 132 | - (void)didSwitchToIndex:(NSUInteger)pageIndex { 133 | CGFloat xPoint = pageIndex * self.scrollView.frame.size.width; 134 | [UIView animateWithDuration:.2f animations: ^{ 135 | [self.scrollView setContentOffset:CGPointMake(xPoint, 0)]; 136 | }]; 137 | } 138 | 139 | 140 | 141 | #pragma Display/Overview Methods 142 | 143 | #pragma Mark Touch-n-Hold Delegate 144 | 145 | - (void)displayAllScreensWithStartingDisplayOn:(CGFloat)startingPosition { 146 | [[UIApplication sharedApplication] setStatusBarStyle:UIStatusBarStyleLightContent]; 147 | 148 | [self sendViewsToBackPosition:YES]; 149 | [self allowScrollViewUserInteraction:YES]; 150 | 151 | [self.scrollView.subviews enumerateObjectsUsingBlock: ^(UIView *view, NSUInteger idx, BOOL *stop) { 152 | UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(selectedView:)]; 153 | [view addGestureRecognizer:tap]; 154 | 155 | view.frame = CGRectMake(view.frame.origin.x, view.frame.origin.y, view.frame.size.width, view.frame.size.height); 156 | }]; 157 | } 158 | 159 | #pragma Mark End Delegate 160 | 161 | - (void)selectedView:(UITapGestureRecognizer *)tap { 162 | NSInteger selectedTag = [tap view].tag; 163 | [self displayViewAtIndex:selectedTag]; 164 | } 165 | 166 | - (void)displayViewAtIndex:(NSInteger)index{ 167 | 168 | [self sendViewsToBackPosition:NO]; 169 | [self.minimalBar returnMenuToSelected:index]; 170 | [UIView animateWithDuration:.25f 171 | delay:0.f 172 | usingSpringWithDamping:.98 173 | initialSpringVelocity:11 174 | options:UIViewAnimationOptionCurveLinear | UIViewAnimationOptionAllowUserInteraction 175 | animations: ^{ 176 | [self allowScrollViewUserInteraction:NO]; 177 | self.scrollView.contentOffset = CGPointMake(self.view.frame.size.width * index, 0); 178 | } 179 | 180 | completion:nil]; 181 | } 182 | 183 | #pragma Mark ScrollView Depth Toggle 184 | - (void)sendViewsToBackPosition:(BOOL)sendBack { 185 | void (^scrollViewAnimation)(void) = ^{ 186 | if (sendBack) { 187 | CATransform3D transform = CATransform3DIdentity; 188 | transform.m34 = 1.0f / -750.f; 189 | transform = CATransform3DTranslate(transform, 0.f, 30.f, -100.f); 190 | transform = CATransform3DRotate(transform, 20.f * M_PI/180, -1, 0, 0); 191 | _scrollView.layer.transform = transform; 192 | }else{ 193 | _scrollView.transform = _scrollViewTransform; 194 | } 195 | }; 196 | 197 | void (^viewControllerAnimation)(void) = ^{ 198 | [_scrollView.subviews enumerateObjectsUsingBlock: ^(UIView *view, NSUInteger idx, BOOL *stop) { 199 | CGFloat scaleAmount = sendBack ? .9f : 1.f; 200 | view.transform = CGAffineTransformScale(_viewControllerTransform, scaleAmount, scaleAmount); 201 | }]; 202 | }; 203 | 204 | [UIView animateWithDuration:.6f 205 | delay:0.f 206 | usingSpringWithDamping:150 207 | initialSpringVelocity:15 208 | options:UIViewAnimationOptionAllowUserInteraction | UIViewAnimationOptionCurveEaseOut 209 | animations:^{ 210 | scrollViewAnimation(); 211 | viewControllerAnimation(); 212 | } completion:^(BOOL finished) { 213 | 214 | }]; 215 | } 216 | 217 | #pragma ScrollView Delegate 218 | 219 | - (void)scrollViewDidScroll:(UIScrollView *)scrollView { 220 | CGFloat contentOffSet = scrollView.contentOffset.x; 221 | [_minimalBar scrollOverviewButtonsWithPercentage:contentOffSet / _coverView.frame.size.width]; 222 | } 223 | 224 | 225 | 226 | #pragma Interface Usability 227 | 228 | - (void)allowScrollViewUserInteraction:(BOOL)allowInteraction { 229 | [_scrollView setScrollEnabled:allowInteraction]; 230 | } 231 | 232 | 233 | 234 | #pragma Mark Setters 235 | 236 | - (void)setViewControllers:(NSArray *)viewControllers { 237 | _viewControllers = viewControllers; 238 | 239 | [_scrollView setContentSize:CGSizeMake(self.view.frame.size.width * _viewControllers.count, self.view.frame.size.height)]; 240 | 241 | if (self.scrollView.subviews) { 242 | [self.scrollView.subviews enumerateObjectsUsingBlock: ^(UIView *view, NSUInteger idx, BOOL *stop) { 243 | [view removeFromSuperview]; 244 | }]; 245 | } 246 | 247 | [_viewControllers enumerateObjectsUsingBlock: ^(UIViewController* viewController, NSUInteger idx, BOOL *stop) { 248 | viewController.view.frame = CGRectMake(self.view.frame.size.width * idx, 0, self.view.frame.size.width, self.view.frame.size.height); 249 | viewController.view.translatesAutoresizingMaskIntoConstraints = NO; 250 | viewController.view.tag = idx; 251 | viewController.tabBarItem.tag = idx; 252 | 253 | _viewControllerTransform = viewController.view.transform; 254 | [self.scrollView addSubview:viewController.view]; 255 | }]; 256 | 257 | [self.view addConstraints:[self defaultConstraints]]; 258 | [_minimalBar createButtonItems:_viewControllers]; 259 | } 260 | 261 | @end 262 | -------------------------------------------------------------------------------- /Pod/JDViewHitTestOverride.h: -------------------------------------------------------------------------------- 1 | // 2 | // UIViewHitTestOverride.h 3 | // MinimalTabBar 4 | // 5 | // Created by james.dunay on 12/3/14. 6 | // Copyright (c) 2014 James.Dunay. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface JDViewHitTestOverride : UIView 12 | 13 | @property(nonatomic, strong)UIScrollView* scrollView; 14 | 15 | @end 16 | -------------------------------------------------------------------------------- /Pod/JDViewHitTestOverride.m: -------------------------------------------------------------------------------- 1 | // 2 | // UIViewHitTestOverride.m 3 | // MinimalTabBar 4 | // 5 | // Created by james.dunay on 12/3/14. 6 | // Copyright (c) 2014 James.Dunay. All rights reserved. 7 | // 8 | 9 | #import "JDViewHitTestOverride.h" 10 | 11 | @implementation JDViewHitTestOverride 12 | 13 | - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { 14 | UIView* child = [super hitTest:point withEvent:event]; 15 | return child == self ? self.scrollView : child; 16 | } 17 | 18 | @end 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![CI Status](http://img.shields.io/travis/jamesdunay@gmail.com/MinimalTabBar.svg?style=flat)](https://travis-ci.org/jamesdunay@gmail.com/MinimalTabBar) 2 | [![Version](https://img.shields.io/cocoapods/v/MinimalTabBar.svg?style=flat)](http://cocoadocs.org/docsets/MinimalTabBar) 3 | [![License](https://img.shields.io/cocoapods/l/MinimalTabBar.svg?style=flat)](http://cocoadocs.org/docsets/MinimalTabBar) 4 | [![Platform](https://img.shields.io/cocoapods/p/MinimalTabBar.svg?style=flat)](http://cocoadocs.org/docsets/MinimalTabBar) 5 | 6 | 7 | # MinimalTabBar 8 | 9 | A new, and elegant, solution to the TabBar on iOS. 10 | MinimalTabBar gets it's name by hiding once you have selected an item, leaving your `UIViewControllers` uncluttered. 11 | 12 | ![](http://i.imgur.com/of7jv2j.gif) 13 | 14 | 15 | ## Demo 16 | For a useable demo please look at the repo [MinimalTabBarDemo](https://github.com/jamesdunay/MinimalTabBarDemo) 17 | 18 | 19 | ## Gestures 20 | The MinimalTabBar has a number of gestures to allow unique user-interaction. While minimized the user has three seperate gestures to control navigation. 21 | 22 | * **Tap** Opens the MinimalTabBar 23 | * **Swipe** Slides the user between adjacent `UIViewControllers` 24 | * **Long** Press Gives the user a complete look at the app 25 | 26 | 27 | 28 | 29 | ## Implimentation 30 | Implimentation mirrors Apple's `UITabBar` very closely. Assuming your `UIViewControllers` have `UITabBar` items it's only two steps. 31 | ```objc 32 | JDMinimalTabBarController *minimalTabBarViewController = [[JDMinimalTabBarController alloc] init]; 33 | ``` 34 | 35 | Once you've created your MinimalTabBar assigning it `UIViewControllers` is easy. Use the `UITabBar` item's `name`, `image`, and `selectedImage` to control the look of each tab. 36 | ```objc 37 | [minimalTabBarViewController setViewControllers:@[sectionOneVC, sectionTwoVC, sectionThreeVC, sectionFourVC, sectionFiveVC]]; 38 | ``` 39 | 40 | *MinimalTabBarController's MinimalBar you can set the following attributes:* 41 | 42 | * `@property (nonatomic, strong) UIColor* defaultTintColor;` 43 | * `@property (nonatomic, strong) UIColor* selectedTintColor;` 44 | * `@property (nonatomic) BOOL showTitles;` 45 | * `@property (nonatomic) BOOL hidesTitlesWhenSelected;` 46 | 47 | *You can also provide the MinimalBar with a background color if you so wish* 48 | 49 | ## Notes 50 | 51 | If you have any comments or run into any errors, please let me know as I am making changes frequently. 52 | 53 | 54 | 55 | ## Usage 56 | 57 | To run the example project, clone the repo, and run `pod install` from the Example directory first. 58 | 59 | ## Installation 60 | 61 | MinimalTabBar is available through [CocoaPods](http://cocoapods.org). To install 62 | it, simply add the following line to your Podfile: 63 | 64 | pod "MinimalTabBar" 65 | 66 | ## Author 67 | 68 | jamesdunay [at] gmail 69 | 70 | 71 | ## License 72 | 73 | MinimalTabBar is available under the MIT license. See the LICENSE file for more info. 74 | 75 | --------------------------------------------------------------------------------