├── .gitignore ├── ALButtonMenu.podspec ├── ALButtonMenu.xcodeproj ├── project.pbxproj └── project.xcworkspace │ └── contents.xcworkspacedata ├── Demo ├── ALDemoAppDelegate.h ├── ALDemoAppDelegate.m ├── ALDemoRootViewController.h ├── ALDemoRootViewController.m ├── ALDemoViewController.h ├── ALDemoViewController.m ├── Assets.xcassets │ └── AppIcon.appiconset │ │ └── Contents.json ├── Base.lproj │ └── LaunchScreen.storyboard ├── Info.plist ├── UIColor+ALComplementary.h ├── UIColor+ALComplementary.m └── main.m ├── LICENSE ├── README.md ├── Screenshots ├── demo1.gif └── demo2.gif └── Source ├── ALAnimationController.h ├── ALAnimationController.m ├── ALAnimationTransitionDelegate.h ├── ALAnimationTransitionDelegate.m ├── ALButton.h ├── ALButton.m ├── ALButtonMenu.h ├── ALButtonViewModel.h ├── ALButtonViewModel.m ├── ALButtonViewModel_ALPrivate.h ├── ALButton_ALPrivate.h ├── ALMenuButton.h ├── ALMenuButton.m ├── ALMenuCollectionViewCell.h ├── ALMenuCollectionViewCell.m ├── ALMenuCollectionViewLayout.h ├── ALMenuCollectionViewLayout.m ├── ALMenuItem.h ├── ALMenuViewController.h ├── ALMenuViewController.m ├── ALMenuViewControllerViewModel.h ├── ALMenuViewControllerViewModel.m ├── ALMenuViewController_ALPrivate.h ├── ALNavigationCoordinator+ALSnapping.h ├── ALNavigationCoordinator+ALSnapping.m ├── ALNavigationCoordinator.h ├── ALNavigationCoordinator.m ├── ALNavigationCoordinatorViewModel.h ├── ALNavigationCoordinatorViewModel.m ├── ALTouchGestureRecognizer.h ├── ALTouchGestureRecognizer.m ├── ALUtils.h ├── UIBezierPath+ALScaling.h ├── UIBezierPath+ALScaling.m ├── UIButton+ALBounce.h ├── UIButton+ALBounce.m ├── UIView+ALLayout.h └── UIView+ALLayout.m /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## User settings 6 | xcuserdata/ 7 | 8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 9 | *.xcscmblueprint 10 | *.xccheckout 11 | 12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 13 | build/ 14 | DerivedData/ 15 | *.moved-aside 16 | *.pbxuser 17 | !default.pbxuser 18 | *.mode1v3 19 | !default.mode1v3 20 | *.mode2v3 21 | !default.mode2v3 22 | *.perspectivev3 23 | !default.perspectivev3 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | 28 | ## App packaging 29 | *.ipa 30 | *.dSYM.zip 31 | *.dSYM 32 | 33 | ## Playgrounds 34 | timeline.xctimeline 35 | playground.xcworkspace 36 | 37 | # Swift Package Manager 38 | # 39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 40 | # Packages/ 41 | # Package.pins 42 | # Package.resolved 43 | # *.xcodeproj 44 | # 45 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 46 | # hence it is not needed unless you have added a package configuration file to your project 47 | # .swiftpm 48 | 49 | .build/ 50 | 51 | # CocoaPods 52 | # 53 | # We recommend against adding the Pods directory to your .gitignore. However 54 | # you should judge for yourself, the pros and cons are mentioned at: 55 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 56 | # 57 | # Pods/ 58 | # 59 | # Add this line if you want to avoid checking in source code from the Xcode workspace 60 | # *.xcworkspace 61 | 62 | # Carthage 63 | # 64 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 65 | # Carthage/Checkouts 66 | 67 | Carthage/Build/ 68 | 69 | # Accio dependency management 70 | Dependencies/ 71 | .accio/ 72 | 73 | # fastlane 74 | # 75 | # It is recommended to not store the screenshots in the git repo. 76 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 77 | # For more information about the recommended setup visit: 78 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 79 | 80 | fastlane/report.xml 81 | fastlane/Preview.html 82 | fastlane/screenshots/**/*.png 83 | fastlane/test_output 84 | 85 | # Code Injection 86 | # 87 | # After new code Injection tools there's a generated folder /iOSInjectionProject 88 | # https://github.com/johnno1962/injectionforxcode 89 | 90 | iOSInjectionProject/ 91 | -------------------------------------------------------------------------------- /ALButtonMenu.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | 3 | s.name = "ALButtonMenu" 4 | s.version = "1.1.0" 5 | s.summary = "A simple, fully customizable menu solution for iOS." 6 | s.description = <<-DESC 7 | ALButtonMenu is a customizable menu solution for iOS. Create a menu view controller (or use 8 | the one provided) and specify the characteristics of the shortcut button. Then tap the button 9 | to quickly show and hide the menu using an animated mask transition effect. 10 | DESC 11 | s.homepage = "https://github.com/lobianco/ALButtonMenu" 12 | s.screenshots = "https://raw.githubusercontent.com/lobianco/ALButtonMenu/master/Screenshots/demo1.gif", "https://raw.githubusercontent.com/lobianco/ALButtonMenu/master/Screenshots/demo2.gif" 13 | s.license = "MIT" 14 | s.author = { "Anthony Lobianco" => "anthony@lobian.co" } 15 | s.social_media_url = "https://twitter.com/lobnco" 16 | s.platform = :ios, "8.0" 17 | s.source = { :git => "https://github.com/lobianco/ALButtonMenu.git", :tag => "#{s.version}" } 18 | s.source_files = "Source/*.{h,m}" 19 | s.private_header_files = "Source/*_ALPrivate.h" 20 | s.requires_arc = true 21 | 22 | end 23 | -------------------------------------------------------------------------------- /ALButtonMenu.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 20178EF51D9EB780007E5BC1 /* ALButtonViewModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 20178EF41D9EB780007E5BC1 /* ALButtonViewModel.m */; }; 11 | 2019B5E11DA337FE0091DA39 /* ALTouchGestureRecognizer.m in Sources */ = {isa = PBXBuildFile; fileRef = 2019B5E01DA337FE0091DA39 /* ALTouchGestureRecognizer.m */; }; 12 | 2019B5E41DA3442A0091DA39 /* UIButton+ALBounce.m in Sources */ = {isa = PBXBuildFile; fileRef = 2019B5E31DA3442A0091DA39 /* UIButton+ALBounce.m */; }; 13 | 2019B5E71DA3476F0091DA39 /* ALMenuButton.m in Sources */ = {isa = PBXBuildFile; fileRef = 2019B5E61DA3476F0091DA39 /* ALMenuButton.m */; }; 14 | 20300B9C1E1F3E72001295A6 /* ALMenuCollectionViewLayout.m in Sources */ = {isa = PBXBuildFile; fileRef = 20300B9B1E1F3E72001295A6 /* ALMenuCollectionViewLayout.m */; }; 15 | 20300BA01E1F5E6F001295A6 /* ALMenuCollectionViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 20300B9F1E1F5E6F001295A6 /* ALMenuCollectionViewCell.m */; }; 16 | 2030F2A21D9DC6F90078C0A7 /* UIView+ALLayout.m in Sources */ = {isa = PBXBuildFile; fileRef = 2030F2A11D9DC6F90078C0A7 /* UIView+ALLayout.m */; }; 17 | 203BE36D1D9729A60005D690 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 203BE36C1D9729A60005D690 /* main.m */; }; 18 | 203BE3701D9729A60005D690 /* ALDemoAppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 203BE36F1D9729A60005D690 /* ALDemoAppDelegate.m */; }; 19 | 203BE3731D9729A60005D690 /* ALDemoViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 203BE3721D9729A60005D690 /* ALDemoViewController.m */; }; 20 | 203BE3781D9729A60005D690 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 203BE3771D9729A60005D690 /* Assets.xcassets */; }; 21 | 203BE37B1D9729A60005D690 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 203BE3791D9729A60005D690 /* LaunchScreen.storyboard */; }; 22 | 203BE3841D973AD00005D690 /* ALButton.m in Sources */ = {isa = PBXBuildFile; fileRef = 203BE3831D973AD00005D690 /* ALButton.m */; }; 23 | 203BE3871D973C720005D690 /* ALAnimationController.m in Sources */ = {isa = PBXBuildFile; fileRef = 203BE3861D973C720005D690 /* ALAnimationController.m */; }; 24 | 203BE38A1D973D0F0005D690 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 203BE3891D973D0F0005D690 /* UIKit.framework */; }; 25 | 203BE38C1D973D180005D690 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 203BE38B1D973D180005D690 /* Foundation.framework */; }; 26 | 203BE3921D9742730005D690 /* ALNavigationCoordinator.m in Sources */ = {isa = PBXBuildFile; fileRef = 203BE3911D9742730005D690 /* ALNavigationCoordinator.m */; }; 27 | 203BE3951D97431D0005D690 /* ALAnimationTransitionDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 203BE3941D97431D0005D690 /* ALAnimationTransitionDelegate.m */; }; 28 | 203C5ED71D9B4C3C00A52EF8 /* ALNavigationCoordinatorViewModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 203C5ED61D9B4C3C00A52EF8 /* ALNavigationCoordinatorViewModel.m */; }; 29 | 203C5EDA1D9B62F200A52EF8 /* ALNavigationCoordinator+ALSnapping.m in Sources */ = {isa = PBXBuildFile; fileRef = 203C5ED91D9B62F200A52EF8 /* ALNavigationCoordinator+ALSnapping.m */; }; 30 | 20801F171D9C34AD0028B322 /* ALMenuViewControllerViewModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 20801F161D9C34AD0028B322 /* ALMenuViewControllerViewModel.m */; }; 31 | 20D6706C1E31ABC900F657C4 /* ALDemoRootViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 20D6706B1E31ABC900F657C4 /* ALDemoRootViewController.m */; }; 32 | 20DA4A001D9AC28200C6BF3C /* UIBezierPath+ALScaling.m in Sources */ = {isa = PBXBuildFile; fileRef = 20DA49FF1D9AC28100C6BF3C /* UIBezierPath+ALScaling.m */; }; 33 | 20DA4A081D9B313800C6BF3C /* ALMenuViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 20DA4A071D9B313800C6BF3C /* ALMenuViewController.m */; }; 34 | 20EB58841D9EAEAF00D8291A /* UIColor+ALComplementary.m in Sources */ = {isa = PBXBuildFile; fileRef = 20EB58831D9EAEAF00D8291A /* UIColor+ALComplementary.m */; }; 35 | /* End PBXBuildFile section */ 36 | 37 | /* Begin PBXFileReference section */ 38 | 20178EF31D9EB780007E5BC1 /* ALButtonViewModel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ALButtonViewModel.h; sourceTree = ""; }; 39 | 20178EF41D9EB780007E5BC1 /* ALButtonViewModel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ALButtonViewModel.m; sourceTree = ""; }; 40 | 2019B5DF1DA337FE0091DA39 /* ALTouchGestureRecognizer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ALTouchGestureRecognizer.h; sourceTree = ""; }; 41 | 2019B5E01DA337FE0091DA39 /* ALTouchGestureRecognizer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ALTouchGestureRecognizer.m; sourceTree = ""; }; 42 | 2019B5E21DA3442A0091DA39 /* UIButton+ALBounce.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIButton+ALBounce.h"; sourceTree = ""; }; 43 | 2019B5E31DA3442A0091DA39 /* UIButton+ALBounce.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIButton+ALBounce.m"; sourceTree = ""; }; 44 | 2019B5E51DA3476F0091DA39 /* ALMenuButton.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ALMenuButton.h; sourceTree = ""; }; 45 | 2019B5E61DA3476F0091DA39 /* ALMenuButton.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ALMenuButton.m; sourceTree = ""; }; 46 | 2019B5E81DA3485E0091DA39 /* ALButton_ALPrivate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ALButton_ALPrivate.h; sourceTree = ""; }; 47 | 201A14F71E28551D00D9CD4C /* ALButtonViewModel_ALPrivate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ALButtonViewModel_ALPrivate.h; sourceTree = ""; }; 48 | 20300B9A1E1F3E72001295A6 /* ALMenuCollectionViewLayout.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ALMenuCollectionViewLayout.h; sourceTree = ""; }; 49 | 20300B9B1E1F3E72001295A6 /* ALMenuCollectionViewLayout.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ALMenuCollectionViewLayout.m; sourceTree = ""; }; 50 | 20300B9E1E1F5E6F001295A6 /* ALMenuCollectionViewCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ALMenuCollectionViewCell.h; sourceTree = ""; }; 51 | 20300B9F1E1F5E6F001295A6 /* ALMenuCollectionViewCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ALMenuCollectionViewCell.m; sourceTree = ""; }; 52 | 2030F29E1D9DABA00078C0A7 /* ALUtils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ALUtils.h; sourceTree = ""; }; 53 | 2030F29F1D9DB7DD0078C0A7 /* ALMenuViewController_ALPrivate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ALMenuViewController_ALPrivate.h; sourceTree = ""; }; 54 | 2030F2A01D9DC6F90078C0A7 /* UIView+ALLayout.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIView+ALLayout.h"; sourceTree = ""; }; 55 | 2030F2A11D9DC6F90078C0A7 /* UIView+ALLayout.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIView+ALLayout.m"; sourceTree = ""; }; 56 | 203BE3681D9729A60005D690 /* ALButtonMenu.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ALButtonMenu.app; sourceTree = BUILT_PRODUCTS_DIR; }; 57 | 203BE36C1D9729A60005D690 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 58 | 203BE36E1D9729A60005D690 /* ALDemoAppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ALDemoAppDelegate.h; sourceTree = ""; }; 59 | 203BE36F1D9729A60005D690 /* ALDemoAppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ALDemoAppDelegate.m; sourceTree = ""; }; 60 | 203BE3711D9729A60005D690 /* ALDemoViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ALDemoViewController.h; sourceTree = ""; }; 61 | 203BE3721D9729A60005D690 /* ALDemoViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ALDemoViewController.m; sourceTree = ""; }; 62 | 203BE3771D9729A60005D690 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 63 | 203BE37A1D9729A60005D690 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 64 | 203BE37C1D9729A60005D690 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 65 | 203BE3821D973AD00005D690 /* ALButton.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ALButton.h; sourceTree = ""; }; 66 | 203BE3831D973AD00005D690 /* ALButton.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ALButton.m; sourceTree = ""; }; 67 | 203BE3851D973C720005D690 /* ALAnimationController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ALAnimationController.h; sourceTree = ""; }; 68 | 203BE3861D973C720005D690 /* ALAnimationController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ALAnimationController.m; sourceTree = ""; }; 69 | 203BE3891D973D0F0005D690 /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; }; 70 | 203BE38B1D973D180005D690 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; 71 | 203BE3901D9742730005D690 /* ALNavigationCoordinator.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ALNavigationCoordinator.h; sourceTree = ""; }; 72 | 203BE3911D9742730005D690 /* ALNavigationCoordinator.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ALNavigationCoordinator.m; sourceTree = ""; }; 73 | 203BE3931D97431D0005D690 /* ALAnimationTransitionDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ALAnimationTransitionDelegate.h; sourceTree = ""; }; 74 | 203BE3941D97431D0005D690 /* ALAnimationTransitionDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ALAnimationTransitionDelegate.m; sourceTree = ""; }; 75 | 203C5ED51D9B4C3C00A52EF8 /* ALNavigationCoordinatorViewModel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ALNavigationCoordinatorViewModel.h; sourceTree = ""; }; 76 | 203C5ED61D9B4C3C00A52EF8 /* ALNavigationCoordinatorViewModel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ALNavigationCoordinatorViewModel.m; sourceTree = ""; }; 77 | 203C5ED81D9B62F200A52EF8 /* ALNavigationCoordinator+ALSnapping.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ALNavigationCoordinator+ALSnapping.h"; sourceTree = ""; }; 78 | 203C5ED91D9B62F200A52EF8 /* ALNavigationCoordinator+ALSnapping.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "ALNavigationCoordinator+ALSnapping.m"; sourceTree = ""; }; 79 | 20801F151D9C34AD0028B322 /* ALMenuViewControllerViewModel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ALMenuViewControllerViewModel.h; sourceTree = ""; }; 80 | 20801F161D9C34AD0028B322 /* ALMenuViewControllerViewModel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ALMenuViewControllerViewModel.m; sourceTree = ""; }; 81 | 20B4977B1DA48AC100E1E008 /* ALMenuItem.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ALMenuItem.h; sourceTree = ""; }; 82 | 20D6706A1E31ABC900F657C4 /* ALDemoRootViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ALDemoRootViewController.h; sourceTree = ""; }; 83 | 20D6706B1E31ABC900F657C4 /* ALDemoRootViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ALDemoRootViewController.m; sourceTree = ""; }; 84 | 20DA49FE1D9AC28100C6BF3C /* UIBezierPath+ALScaling.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIBezierPath+ALScaling.h"; sourceTree = ""; }; 85 | 20DA49FF1D9AC28100C6BF3C /* UIBezierPath+ALScaling.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIBezierPath+ALScaling.m"; sourceTree = ""; }; 86 | 20DA4A061D9B313800C6BF3C /* ALMenuViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ALMenuViewController.h; sourceTree = ""; }; 87 | 20DA4A071D9B313800C6BF3C /* ALMenuViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ALMenuViewController.m; sourceTree = ""; }; 88 | 20EB58821D9EAEAF00D8291A /* UIColor+ALComplementary.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIColor+ALComplementary.h"; sourceTree = ""; }; 89 | 20EB58831D9EAEAF00D8291A /* UIColor+ALComplementary.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIColor+ALComplementary.m"; sourceTree = ""; }; 90 | 20FBE7C11E402A0500A5095B /* ALButtonMenu.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ALButtonMenu.h; sourceTree = ""; }; 91 | /* End PBXFileReference section */ 92 | 93 | /* Begin PBXFrameworksBuildPhase section */ 94 | 203BE3651D9729A60005D690 /* Frameworks */ = { 95 | isa = PBXFrameworksBuildPhase; 96 | buildActionMask = 2147483647; 97 | files = ( 98 | 203BE38C1D973D180005D690 /* Foundation.framework in Frameworks */, 99 | 203BE38A1D973D0F0005D690 /* UIKit.framework in Frameworks */, 100 | ); 101 | runOnlyForDeploymentPostprocessing = 0; 102 | }; 103 | /* End PBXFrameworksBuildPhase section */ 104 | 105 | /* Begin PBXGroup section */ 106 | 20300B9D1E1F3E7F001295A6 /* Collection View Layout */ = { 107 | isa = PBXGroup; 108 | children = ( 109 | 20300B9A1E1F3E72001295A6 /* ALMenuCollectionViewLayout.h */, 110 | 20300B9B1E1F3E72001295A6 /* ALMenuCollectionViewLayout.m */, 111 | ); 112 | name = "Collection View Layout"; 113 | sourceTree = ""; 114 | }; 115 | 20300BA11E1F5E74001295A6 /* Collection View Cell */ = { 116 | isa = PBXGroup; 117 | children = ( 118 | 20300B9E1E1F5E6F001295A6 /* ALMenuCollectionViewCell.h */, 119 | 20300B9F1E1F5E6F001295A6 /* ALMenuCollectionViewCell.m */, 120 | ); 121 | name = "Collection View Cell"; 122 | sourceTree = ""; 123 | }; 124 | 2030F2A31D9DC6FD0078C0A7 /* Utilities */ = { 125 | isa = PBXGroup; 126 | children = ( 127 | 2030F29E1D9DABA00078C0A7 /* ALUtils.h */, 128 | ); 129 | name = Utilities; 130 | sourceTree = ""; 131 | }; 132 | 203BE35F1D9729A60005D690 = { 133 | isa = PBXGroup; 134 | children = ( 135 | 20DA4A041D9B09E600C6BF3C /* Demo */, 136 | 20DA4A051D9B09F100C6BF3C /* Source */, 137 | ); 138 | sourceTree = ""; 139 | }; 140 | 203BE3691D9729A60005D690 /* Products */ = { 141 | isa = PBXGroup; 142 | children = ( 143 | 203BE3681D9729A60005D690 /* ALButtonMenu.app */, 144 | ); 145 | name = Products; 146 | sourceTree = ""; 147 | }; 148 | 203BE36B1D9729A60005D690 /* Supporting Files */ = { 149 | isa = PBXGroup; 150 | children = ( 151 | 203BE36C1D9729A60005D690 /* main.m */, 152 | ); 153 | name = "Supporting Files"; 154 | sourceTree = ""; 155 | }; 156 | 203BE3881D973D0F0005D690 /* Frameworks */ = { 157 | isa = PBXGroup; 158 | children = ( 159 | 203BE38B1D973D180005D690 /* Foundation.framework */, 160 | 203BE3891D973D0F0005D690 /* UIKit.framework */, 161 | ); 162 | name = Frameworks; 163 | sourceTree = ""; 164 | }; 165 | 20B5DD9D1D9C1DE4007B5766 /* Navigation Controller */ = { 166 | isa = PBXGroup; 167 | children = ( 168 | 203BE3901D9742730005D690 /* ALNavigationCoordinator.h */, 169 | 203BE3911D9742730005D690 /* ALNavigationCoordinator.m */, 170 | 203C5ED51D9B4C3C00A52EF8 /* ALNavigationCoordinatorViewModel.h */, 171 | 203C5ED61D9B4C3C00A52EF8 /* ALNavigationCoordinatorViewModel.m */, 172 | 203BE3821D973AD00005D690 /* ALButton.h */, 173 | 2019B5E81DA3485E0091DA39 /* ALButton_ALPrivate.h */, 174 | 203BE3831D973AD00005D690 /* ALButton.m */, 175 | 2019B5E51DA3476F0091DA39 /* ALMenuButton.h */, 176 | 2019B5E61DA3476F0091DA39 /* ALMenuButton.m */, 177 | 20178EF31D9EB780007E5BC1 /* ALButtonViewModel.h */, 178 | 201A14F71E28551D00D9CD4C /* ALButtonViewModel_ALPrivate.h */, 179 | 20178EF41D9EB780007E5BC1 /* ALButtonViewModel.m */, 180 | 2019B5DF1DA337FE0091DA39 /* ALTouchGestureRecognizer.h */, 181 | 2019B5E01DA337FE0091DA39 /* ALTouchGestureRecognizer.m */, 182 | ); 183 | name = "Navigation Controller"; 184 | sourceTree = ""; 185 | }; 186 | 20DA4A041D9B09E600C6BF3C /* Demo */ = { 187 | isa = PBXGroup; 188 | children = ( 189 | 203BE36E1D9729A60005D690 /* ALDemoAppDelegate.h */, 190 | 203BE36F1D9729A60005D690 /* ALDemoAppDelegate.m */, 191 | 20D6706A1E31ABC900F657C4 /* ALDemoRootViewController.h */, 192 | 20D6706B1E31ABC900F657C4 /* ALDemoRootViewController.m */, 193 | 203BE3711D9729A60005D690 /* ALDemoViewController.h */, 194 | 203BE3721D9729A60005D690 /* ALDemoViewController.m */, 195 | 20EB58851D9EAECE00D8291A /* Categories */, 196 | 203BE3771D9729A60005D690 /* Assets.xcassets */, 197 | 203BE3791D9729A60005D690 /* LaunchScreen.storyboard */, 198 | 203BE37C1D9729A60005D690 /* Info.plist */, 199 | 203BE36B1D9729A60005D690 /* Supporting Files */, 200 | 203BE3691D9729A60005D690 /* Products */, 201 | 203BE3881D973D0F0005D690 /* Frameworks */, 202 | ); 203 | path = Demo; 204 | sourceTree = ""; 205 | }; 206 | 20DA4A051D9B09F100C6BF3C /* Source */ = { 207 | isa = PBXGroup; 208 | children = ( 209 | 20FBE7C11E402A0500A5095B /* ALButtonMenu.h */, 210 | 20B5DD9D1D9C1DE4007B5766 /* Navigation Controller */, 211 | 20DA4A091D9B324200C6BF3C /* Menu Controller */, 212 | 20DA4A0D1D9B32D700C6BF3C /* Animations */, 213 | 20DA4A0E1D9B32E800C6BF3C /* Categories */, 214 | 2030F2A31D9DC6FD0078C0A7 /* Utilities */, 215 | ); 216 | path = Source; 217 | sourceTree = ""; 218 | }; 219 | 20DA4A091D9B324200C6BF3C /* Menu Controller */ = { 220 | isa = PBXGroup; 221 | children = ( 222 | 20300B9D1E1F3E7F001295A6 /* Collection View Layout */, 223 | 20300BA11E1F5E74001295A6 /* Collection View Cell */, 224 | 20DA4A061D9B313800C6BF3C /* ALMenuViewController.h */, 225 | 2030F29F1D9DB7DD0078C0A7 /* ALMenuViewController_ALPrivate.h */, 226 | 20DA4A071D9B313800C6BF3C /* ALMenuViewController.m */, 227 | 20801F151D9C34AD0028B322 /* ALMenuViewControllerViewModel.h */, 228 | 20801F161D9C34AD0028B322 /* ALMenuViewControllerViewModel.m */, 229 | 20B4977B1DA48AC100E1E008 /* ALMenuItem.h */, 230 | ); 231 | name = "Menu Controller"; 232 | sourceTree = ""; 233 | }; 234 | 20DA4A0D1D9B32D700C6BF3C /* Animations */ = { 235 | isa = PBXGroup; 236 | children = ( 237 | 203BE3931D97431D0005D690 /* ALAnimationTransitionDelegate.h */, 238 | 203BE3941D97431D0005D690 /* ALAnimationTransitionDelegate.m */, 239 | 203BE3851D973C720005D690 /* ALAnimationController.h */, 240 | 203BE3861D973C720005D690 /* ALAnimationController.m */, 241 | ); 242 | name = Animations; 243 | sourceTree = ""; 244 | }; 245 | 20DA4A0E1D9B32E800C6BF3C /* Categories */ = { 246 | isa = PBXGroup; 247 | children = ( 248 | 203C5ED81D9B62F200A52EF8 /* ALNavigationCoordinator+ALSnapping.h */, 249 | 203C5ED91D9B62F200A52EF8 /* ALNavigationCoordinator+ALSnapping.m */, 250 | 20DA49FE1D9AC28100C6BF3C /* UIBezierPath+ALScaling.h */, 251 | 20DA49FF1D9AC28100C6BF3C /* UIBezierPath+ALScaling.m */, 252 | 2019B5E21DA3442A0091DA39 /* UIButton+ALBounce.h */, 253 | 2019B5E31DA3442A0091DA39 /* UIButton+ALBounce.m */, 254 | 2030F2A01D9DC6F90078C0A7 /* UIView+ALLayout.h */, 255 | 2030F2A11D9DC6F90078C0A7 /* UIView+ALLayout.m */, 256 | ); 257 | name = Categories; 258 | sourceTree = ""; 259 | }; 260 | 20EB58851D9EAECE00D8291A /* Categories */ = { 261 | isa = PBXGroup; 262 | children = ( 263 | 20EB58821D9EAEAF00D8291A /* UIColor+ALComplementary.h */, 264 | 20EB58831D9EAEAF00D8291A /* UIColor+ALComplementary.m */, 265 | ); 266 | name = Categories; 267 | sourceTree = ""; 268 | }; 269 | /* End PBXGroup section */ 270 | 271 | /* Begin PBXNativeTarget section */ 272 | 203BE3671D9729A60005D690 /* ALButtonMenu */ = { 273 | isa = PBXNativeTarget; 274 | buildConfigurationList = 203BE37F1D9729A60005D690 /* Build configuration list for PBXNativeTarget "ALButtonMenu" */; 275 | buildPhases = ( 276 | 203BE3641D9729A60005D690 /* Sources */, 277 | 203BE3651D9729A60005D690 /* Frameworks */, 278 | 203BE3661D9729A60005D690 /* Resources */, 279 | ); 280 | buildRules = ( 281 | ); 282 | dependencies = ( 283 | ); 284 | name = ALButtonMenu; 285 | productName = ALShortcutButton; 286 | productReference = 203BE3681D9729A60005D690 /* ALButtonMenu.app */; 287 | productType = "com.apple.product-type.application"; 288 | }; 289 | /* End PBXNativeTarget section */ 290 | 291 | /* Begin PBXProject section */ 292 | 203BE3601D9729A60005D690 /* Project object */ = { 293 | isa = PBXProject; 294 | attributes = { 295 | LastUpgradeCheck = 1250; 296 | ORGANIZATIONNAME = "Anthony Lobianco"; 297 | TargetAttributes = { 298 | 203BE3671D9729A60005D690 = { 299 | CreatedOnToolsVersion = 8.0; 300 | DevelopmentTeam = V5BZVUUAYU; 301 | ProvisioningStyle = Automatic; 302 | }; 303 | }; 304 | }; 305 | buildConfigurationList = 203BE3631D9729A60005D690 /* Build configuration list for PBXProject "ALButtonMenu" */; 306 | compatibilityVersion = "Xcode 3.2"; 307 | developmentRegion = en; 308 | hasScannedForEncodings = 0; 309 | knownRegions = ( 310 | en, 311 | Base, 312 | ); 313 | mainGroup = 203BE35F1D9729A60005D690; 314 | productRefGroup = 203BE3691D9729A60005D690 /* Products */; 315 | projectDirPath = ""; 316 | projectRoot = ""; 317 | targets = ( 318 | 203BE3671D9729A60005D690 /* ALButtonMenu */, 319 | ); 320 | }; 321 | /* End PBXProject section */ 322 | 323 | /* Begin PBXResourcesBuildPhase section */ 324 | 203BE3661D9729A60005D690 /* Resources */ = { 325 | isa = PBXResourcesBuildPhase; 326 | buildActionMask = 2147483647; 327 | files = ( 328 | 203BE37B1D9729A60005D690 /* LaunchScreen.storyboard in Resources */, 329 | 203BE3781D9729A60005D690 /* Assets.xcassets in Resources */, 330 | ); 331 | runOnlyForDeploymentPostprocessing = 0; 332 | }; 333 | /* End PBXResourcesBuildPhase section */ 334 | 335 | /* Begin PBXSourcesBuildPhase section */ 336 | 203BE3641D9729A60005D690 /* Sources */ = { 337 | isa = PBXSourcesBuildPhase; 338 | buildActionMask = 2147483647; 339 | files = ( 340 | 2019B5E11DA337FE0091DA39 /* ALTouchGestureRecognizer.m in Sources */, 341 | 20DA4A001D9AC28200C6BF3C /* UIBezierPath+ALScaling.m in Sources */, 342 | 2019B5E41DA3442A0091DA39 /* UIButton+ALBounce.m in Sources */, 343 | 2019B5E71DA3476F0091DA39 /* ALMenuButton.m in Sources */, 344 | 203BE3871D973C720005D690 /* ALAnimationController.m in Sources */, 345 | 203BE3731D9729A60005D690 /* ALDemoViewController.m in Sources */, 346 | 20300B9C1E1F3E72001295A6 /* ALMenuCollectionViewLayout.m in Sources */, 347 | 20178EF51D9EB780007E5BC1 /* ALButtonViewModel.m in Sources */, 348 | 20EB58841D9EAEAF00D8291A /* UIColor+ALComplementary.m in Sources */, 349 | 203BE3921D9742730005D690 /* ALNavigationCoordinator.m in Sources */, 350 | 203C5ED71D9B4C3C00A52EF8 /* ALNavigationCoordinatorViewModel.m in Sources */, 351 | 203BE3951D97431D0005D690 /* ALAnimationTransitionDelegate.m in Sources */, 352 | 20DA4A081D9B313800C6BF3C /* ALMenuViewController.m in Sources */, 353 | 20801F171D9C34AD0028B322 /* ALMenuViewControllerViewModel.m in Sources */, 354 | 20300BA01E1F5E6F001295A6 /* ALMenuCollectionViewCell.m in Sources */, 355 | 203BE3701D9729A60005D690 /* ALDemoAppDelegate.m in Sources */, 356 | 203BE3841D973AD00005D690 /* ALButton.m in Sources */, 357 | 2030F2A21D9DC6F90078C0A7 /* UIView+ALLayout.m in Sources */, 358 | 203BE36D1D9729A60005D690 /* main.m in Sources */, 359 | 203C5EDA1D9B62F200A52EF8 /* ALNavigationCoordinator+ALSnapping.m in Sources */, 360 | 20D6706C1E31ABC900F657C4 /* ALDemoRootViewController.m in Sources */, 361 | ); 362 | runOnlyForDeploymentPostprocessing = 0; 363 | }; 364 | /* End PBXSourcesBuildPhase section */ 365 | 366 | /* Begin PBXVariantGroup section */ 367 | 203BE3791D9729A60005D690 /* LaunchScreen.storyboard */ = { 368 | isa = PBXVariantGroup; 369 | children = ( 370 | 203BE37A1D9729A60005D690 /* Base */, 371 | ); 372 | name = LaunchScreen.storyboard; 373 | sourceTree = ""; 374 | }; 375 | /* End PBXVariantGroup section */ 376 | 377 | /* Begin XCBuildConfiguration section */ 378 | 203BE37D1D9729A60005D690 /* Debug */ = { 379 | isa = XCBuildConfiguration; 380 | buildSettings = { 381 | ALWAYS_SEARCH_USER_PATHS = NO; 382 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 383 | CLANG_ANALYZER_NONNULL = YES; 384 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 385 | CLANG_CXX_LIBRARY = "libc++"; 386 | CLANG_ENABLE_MODULES = YES; 387 | CLANG_ENABLE_OBJC_ARC = YES; 388 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 389 | CLANG_WARN_BOOL_CONVERSION = YES; 390 | CLANG_WARN_COMMA = YES; 391 | CLANG_WARN_CONSTANT_CONVERSION = YES; 392 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 393 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 394 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 395 | CLANG_WARN_EMPTY_BODY = YES; 396 | CLANG_WARN_ENUM_CONVERSION = YES; 397 | CLANG_WARN_INFINITE_RECURSION = YES; 398 | CLANG_WARN_INT_CONVERSION = YES; 399 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 400 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 401 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 402 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 403 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 404 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 405 | CLANG_WARN_STRICT_PROTOTYPES = YES; 406 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 407 | CLANG_WARN_SUSPICIOUS_MOVES = YES; 408 | CLANG_WARN_UNREACHABLE_CODE = YES; 409 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 410 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 411 | COPY_PHASE_STRIP = NO; 412 | DEBUG_INFORMATION_FORMAT = dwarf; 413 | ENABLE_STRICT_OBJC_MSGSEND = YES; 414 | ENABLE_TESTABILITY = YES; 415 | GCC_C_LANGUAGE_STANDARD = gnu99; 416 | GCC_DYNAMIC_NO_PIC = NO; 417 | GCC_NO_COMMON_BLOCKS = YES; 418 | GCC_OPTIMIZATION_LEVEL = 0; 419 | GCC_PREPROCESSOR_DEFINITIONS = ( 420 | "DEBUG=1", 421 | "$(inherited)", 422 | ); 423 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 424 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 425 | GCC_WARN_UNDECLARED_SELECTOR = YES; 426 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 427 | GCC_WARN_UNUSED_FUNCTION = YES; 428 | GCC_WARN_UNUSED_VARIABLE = YES; 429 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 430 | MTL_ENABLE_DEBUG_INFO = YES; 431 | ONLY_ACTIVE_ARCH = YES; 432 | SDKROOT = iphoneos; 433 | TARGETED_DEVICE_FAMILY = "1,2"; 434 | }; 435 | name = Debug; 436 | }; 437 | 203BE37E1D9729A60005D690 /* Release */ = { 438 | isa = XCBuildConfiguration; 439 | buildSettings = { 440 | ALWAYS_SEARCH_USER_PATHS = NO; 441 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 442 | CLANG_ANALYZER_NONNULL = YES; 443 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 444 | CLANG_CXX_LIBRARY = "libc++"; 445 | CLANG_ENABLE_MODULES = YES; 446 | CLANG_ENABLE_OBJC_ARC = YES; 447 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 448 | CLANG_WARN_BOOL_CONVERSION = YES; 449 | CLANG_WARN_COMMA = YES; 450 | CLANG_WARN_CONSTANT_CONVERSION = YES; 451 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 452 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 453 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 454 | CLANG_WARN_EMPTY_BODY = YES; 455 | CLANG_WARN_ENUM_CONVERSION = YES; 456 | CLANG_WARN_INFINITE_RECURSION = YES; 457 | CLANG_WARN_INT_CONVERSION = YES; 458 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 459 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 460 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 461 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 462 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 463 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 464 | CLANG_WARN_STRICT_PROTOTYPES = YES; 465 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 466 | CLANG_WARN_SUSPICIOUS_MOVES = YES; 467 | CLANG_WARN_UNREACHABLE_CODE = YES; 468 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 469 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 470 | COPY_PHASE_STRIP = NO; 471 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 472 | ENABLE_NS_ASSERTIONS = NO; 473 | ENABLE_STRICT_OBJC_MSGSEND = YES; 474 | GCC_C_LANGUAGE_STANDARD = gnu99; 475 | GCC_NO_COMMON_BLOCKS = YES; 476 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 477 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 478 | GCC_WARN_UNDECLARED_SELECTOR = YES; 479 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 480 | GCC_WARN_UNUSED_FUNCTION = YES; 481 | GCC_WARN_UNUSED_VARIABLE = YES; 482 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 483 | MTL_ENABLE_DEBUG_INFO = NO; 484 | SDKROOT = iphoneos; 485 | TARGETED_DEVICE_FAMILY = "1,2"; 486 | VALIDATE_PRODUCT = YES; 487 | }; 488 | name = Release; 489 | }; 490 | 203BE3801D9729A60005D690 /* Debug */ = { 491 | isa = XCBuildConfiguration; 492 | buildSettings = { 493 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 494 | DEVELOPMENT_TEAM = V5BZVUUAYU; 495 | INFOPLIST_FILE = "$(SRCROOT)/Demo/Info.plist"; 496 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 497 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 498 | PRODUCT_BUNDLE_IDENTIFIER = com.lobianco.ALButtonMenu; 499 | PRODUCT_NAME = "$(TARGET_NAME)"; 500 | }; 501 | name = Debug; 502 | }; 503 | 203BE3811D9729A60005D690 /* Release */ = { 504 | isa = XCBuildConfiguration; 505 | buildSettings = { 506 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 507 | DEVELOPMENT_TEAM = V5BZVUUAYU; 508 | INFOPLIST_FILE = "$(SRCROOT)/Demo/Info.plist"; 509 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 510 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 511 | PRODUCT_BUNDLE_IDENTIFIER = com.lobianco.ALButtonMenu; 512 | PRODUCT_NAME = "$(TARGET_NAME)"; 513 | }; 514 | name = Release; 515 | }; 516 | /* End XCBuildConfiguration section */ 517 | 518 | /* Begin XCConfigurationList section */ 519 | 203BE3631D9729A60005D690 /* Build configuration list for PBXProject "ALButtonMenu" */ = { 520 | isa = XCConfigurationList; 521 | buildConfigurations = ( 522 | 203BE37D1D9729A60005D690 /* Debug */, 523 | 203BE37E1D9729A60005D690 /* Release */, 524 | ); 525 | defaultConfigurationIsVisible = 0; 526 | defaultConfigurationName = Release; 527 | }; 528 | 203BE37F1D9729A60005D690 /* Build configuration list for PBXNativeTarget "ALButtonMenu" */ = { 529 | isa = XCConfigurationList; 530 | buildConfigurations = ( 531 | 203BE3801D9729A60005D690 /* Debug */, 532 | 203BE3811D9729A60005D690 /* Release */, 533 | ); 534 | defaultConfigurationIsVisible = 0; 535 | defaultConfigurationName = Release; 536 | }; 537 | /* End XCConfigurationList section */ 538 | }; 539 | rootObject = 203BE3601D9729A60005D690 /* Project object */; 540 | } 541 | -------------------------------------------------------------------------------- /ALButtonMenu.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Demo/ALDemoAppDelegate.h: -------------------------------------------------------------------------------- 1 | // 2 | // ALDemoAppDelegate.h 3 | // ALButtonMenu 4 | // 5 | // Copyright © 2016 Anthony Lobianco. All rights reserved. 6 | // 7 | 8 | #import 9 | 10 | @interface ALDemoAppDelegate : UIResponder 11 | 12 | @property (strong, nonatomic) UIWindow *window; 13 | 14 | 15 | @end 16 | 17 | -------------------------------------------------------------------------------- /Demo/ALDemoAppDelegate.m: -------------------------------------------------------------------------------- 1 | // 2 | // ALDemoAppDelegate.m 3 | // ALButtonMenu 4 | // 5 | // Copyright © 2016 Anthony Lobianco. All rights reserved. 6 | // 7 | 8 | #import "ALDemoAppDelegate.h" 9 | 10 | #import "ALDemoRootViewController.h" 11 | 12 | @implementation ALDemoAppDelegate 13 | 14 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 15 | { 16 | ALDemoRootViewController *rootViewController = [[ALDemoRootViewController alloc] init]; 17 | 18 | self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; 19 | self.window.rootViewController = rootViewController; 20 | 21 | [self.window makeKeyAndVisible]; 22 | 23 | return YES; 24 | } 25 | 26 | @end 27 | -------------------------------------------------------------------------------- /Demo/ALDemoRootViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // ALDemoRootViewController.h 3 | // ALButtonMenu 4 | // 5 | // Copyright © 2017 Anthony Lobianco. All rights reserved. 6 | // 7 | 8 | #import 9 | 10 | @interface ALDemoRootViewController : UIViewController 11 | 12 | @end 13 | -------------------------------------------------------------------------------- /Demo/ALDemoRootViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // ALDemoRootViewController.m 3 | // ALButtonMenu 4 | // 5 | // Copyright © 2017 Anthony Lobianco. All rights reserved. 6 | // 7 | 8 | #import "ALDemoRootViewController.h" 9 | 10 | #import "UIColor+ALComplementary.h" 11 | 12 | #import "ALButtonMenu.h" 13 | 14 | #import "ALDemoViewController.h" 15 | 16 | static NSUInteger const kNumberOfMenuItems = 4; 17 | static NSUInteger const kMenuColumns = 2; 18 | static CGFloat const kMenuItemSpacing = 15.f; 19 | static CGSize const kMenuItemSize = { 100.f, 100.f }; 20 | 21 | @interface ALDemoRootViewController () 22 | 23 | @property (nonatomic) ALNavigationCoordinator *navigationCoordinator; 24 | 25 | // these properties are made thread-safe in their getter methods 26 | @property (nonatomic, copy, readonly) NSArray *> *dummyMenuItems; 27 | @property (nonatomic, copy, readonly) NSArray *dummyViewControllers; 28 | 29 | @end 30 | 31 | @implementation ALDemoRootViewController 32 | 33 | @synthesize dummyMenuItems = _dummyMenuItems; 34 | @synthesize dummyViewControllers = _dummyViewControllers; 35 | 36 | - (void)viewDidLoad 37 | { 38 | [super viewDidLoad]; 39 | 40 | NSParameterAssert(self.dummyMenuItems.count == self.dummyViewControllers.count); 41 | 42 | ALDemoViewController *rootViewController = self.dummyViewControllers[0]; 43 | UINavigationController *rootNavigationController = [[UINavigationController alloc] initWithRootViewController:rootViewController]; 44 | rootNavigationController.navigationBarHidden = YES; 45 | 46 | ALMenuViewControllerLayout layout; 47 | layout.columns = kMenuColumns; 48 | layout.itemSpacing = kMenuItemSpacing; 49 | layout.itemSize = kMenuItemSize; 50 | 51 | NSArray *> *items = self.dummyMenuItems; 52 | 53 | ALMenuViewControllerViewModel *menuViewModel = [[ALMenuViewControllerViewModel alloc] initWithItems:items layout:layout]; 54 | UIViewController *menuViewController = [[ALMenuViewController alloc] initWithViewModel:menuViewModel]; 55 | 56 | ALNavigationCoordinatorViewModel *navViewModel = [[ALNavigationCoordinatorViewModel alloc] init]; 57 | navViewModel.buttonCanBeRepositioned = YES; 58 | 59 | self.navigationCoordinator = [[ALNavigationCoordinator alloc] initWithViewModel:navViewModel menuViewController:menuViewController navigationController:rootNavigationController]; 60 | self.navigationCoordinator.delegate = self; 61 | 62 | UIViewController *childViewController = self.navigationCoordinator.navigationController; 63 | 64 | [self addChildViewController:childViewController]; 65 | [self.view addSubview:childViewController.view]; 66 | [childViewController didMoveToParentViewController:self]; 67 | 68 | [self.navigationCoordinator viewDidLoad]; 69 | } 70 | 71 | #pragma mark - Private methods 72 | 73 | - (NSArray *> *)dummyMenuItems 74 | { 75 | static dispatch_once_t onceToken; 76 | dispatch_once(&onceToken, ^{ 77 | NSMutableArray *> *buttons = [[NSMutableArray alloc] init]; 78 | 79 | for (NSInteger i = 0; i < kNumberOfMenuItems; i++) 80 | { 81 | ALButtonViewModel *viewModel = [[ALButtonViewModel alloc] init]; 82 | viewModel.color = self.dummyViewControllers[i].color; 83 | 84 | // since the button hasn't been added to the view heirarchy yet, we can construct 85 | // a path with a placeholder size and a proportional corner radius (in this case, 86 | // we want 10% rounder corners). then when the view is layed out, the mask path 87 | // will be upscaled for us automatically but the proportions will stay the same. 88 | // 89 | viewModel.maskPath = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(0.f, 0.f, 20.f, 20.f) cornerRadius:2.f]; 90 | 91 | ALButton *button = [[ALButton alloc] initWithViewModel:viewModel]; 92 | 93 | UILabel *numberLabel = [[UILabel alloc] init]; 94 | numberLabel.translatesAutoresizingMaskIntoConstraints = NO; 95 | numberLabel.text = [NSString stringWithFormat:@"%@", @(i + 1)]; 96 | numberLabel.textColor = [UIColor colorWithWhite:0.1 alpha:1.f]; 97 | numberLabel.font = [UIFont boldSystemFontOfSize:28.f]; 98 | numberLabel.textAlignment = NSTextAlignmentCenter; 99 | 100 | [button addSubview:numberLabel]; 101 | [numberLabel.leadingAnchor constraintEqualToAnchor:button.leadingAnchor].active = YES; 102 | [numberLabel.trailingAnchor constraintEqualToAnchor:button.trailingAnchor].active = YES; 103 | [numberLabel.topAnchor constraintEqualToAnchor:button.topAnchor].active = YES; 104 | [numberLabel.bottomAnchor constraintEqualToAnchor:button.bottomAnchor].active = YES; 105 | 106 | [buttons addObject:button]; 107 | } 108 | 109 | _dummyMenuItems = [buttons copy]; 110 | }); 111 | 112 | return _dummyMenuItems; 113 | } 114 | 115 | - (NSArray *)dummyViewControllers 116 | { 117 | static dispatch_once_t onceToken; 118 | dispatch_once(&onceToken, ^{ 119 | NSMutableArray *viewControllers = [[NSMutableArray alloc] init]; 120 | 121 | for (NSInteger i = 0; i < kNumberOfMenuItems; ++i) 122 | { 123 | ALDemoViewController *viewController = [[ALDemoViewController alloc] init]; 124 | viewController.index = i + 1; 125 | viewController.color = [UIColor al_neutralColor]; 126 | viewControllers[i] = viewController; 127 | } 128 | 129 | _dummyViewControllers = [viewControllers copy]; 130 | }); 131 | 132 | return _dummyViewControllers; 133 | } 134 | 135 | #pragma mark - ALNavigationCoordinatorDelegate 136 | 137 | - (UIViewController *)navigationCoordinator:(ALNavigationCoordinator *)navigationCoordinator viewControllerForMenuItemAtIndex:(NSUInteger)index 138 | { 139 | return self.dummyViewControllers[index]; 140 | } 141 | 142 | #pragma mark - Status bar 143 | 144 | - (UIViewController *)childViewControllerForStatusBarHidden 145 | { 146 | return self.navigationCoordinator.menuViewController; 147 | } 148 | 149 | #pragma mark - Rotation 150 | 151 | - (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id)coordinator 152 | { 153 | [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator]; 154 | 155 | [self.navigationCoordinator viewWillTransitionToSize:size withTransitionCoordinator:coordinator]; 156 | } 157 | 158 | @end 159 | -------------------------------------------------------------------------------- /Demo/ALDemoViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // ALDemoViewController.h 3 | // ALButtonMenu 4 | // 5 | // Copyright © 2016 Anthony Lobianco. All rights reserved. 6 | // 7 | 8 | #import 9 | 10 | @interface ALDemoViewController : UIViewController 11 | 12 | @property (nonatomic) NSUInteger index; 13 | @property (nonatomic) UIColor *color; 14 | 15 | @end 16 | 17 | -------------------------------------------------------------------------------- /Demo/ALDemoViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // ALDemoViewController.m 3 | // ALButtonMenu 4 | // 5 | // Copyright © 2016 Anthony Lobianco. All rights reserved. 6 | // 7 | 8 | #import "ALDemoViewController.h" 9 | 10 | #import "UIColor+ALComplementary.h" 11 | 12 | @implementation ALDemoViewController 13 | 14 | - (NSString *)title 15 | { 16 | return [NSString stringWithFormat:@"VC %@", @(self.index)]; 17 | } 18 | 19 | - (void)viewDidLoad 20 | { 21 | [super viewDidLoad]; 22 | 23 | self.view.backgroundColor = [[self.color al_complementaryColor] al_neutralColor]; 24 | 25 | UIColor *nearBlackColor = [UIColor colorWithWhite:0.1f alpha:1.f]; 26 | 27 | UIView *labelContainer = [[UIView alloc] init]; 28 | labelContainer.translatesAutoresizingMaskIntoConstraints = NO; 29 | labelContainer.backgroundColor = self.color; 30 | labelContainer.layer.borderColor = [nearBlackColor CGColor]; 31 | labelContainer.layer.borderWidth = 4.f; 32 | labelContainer.layer.cornerRadius = 12.f; 33 | 34 | UILabel *label = [[UILabel alloc] init]; 35 | label.translatesAutoresizingMaskIntoConstraints = NO; 36 | label.textAlignment = NSTextAlignmentCenter; 37 | label.text = [NSString stringWithFormat:@"%@", @(self.index)]; 38 | label.font = [UIFont systemFontOfSize:100.f]; 39 | label.textColor = nearBlackColor; 40 | label.numberOfLines = 0; 41 | 42 | [labelContainer addSubview:label]; 43 | [label.topAnchor constraintEqualToAnchor:labelContainer.topAnchor constant:20.f].active = YES; 44 | [label.leftAnchor constraintEqualToAnchor:labelContainer.leftAnchor constant:20.f].active = YES; 45 | [label.bottomAnchor constraintEqualToAnchor:labelContainer.bottomAnchor constant:-20.f].active = YES; 46 | [label.rightAnchor constraintEqualToAnchor:labelContainer.rightAnchor constant:-20.f].active = YES; 47 | 48 | [self.view addSubview:labelContainer]; 49 | [labelContainer.centerXAnchor constraintEqualToAnchor:self.view.centerXAnchor].active = YES; 50 | [labelContainer.centerYAnchor constraintEqualToAnchor:self.view.centerYAnchor].active = YES; 51 | [labelContainer.widthAnchor constraintEqualToAnchor:labelContainer.heightAnchor].active = YES; 52 | } 53 | 54 | @end 55 | -------------------------------------------------------------------------------- /Demo/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | } 88 | ], 89 | "info" : { 90 | "version" : 1, 91 | "author" : "xcode" 92 | } 93 | } -------------------------------------------------------------------------------- /Demo/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /Demo/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIRequiredDeviceCapabilities 26 | 27 | armv7 28 | 29 | UISupportedInterfaceOrientations 30 | 31 | UIInterfaceOrientationPortrait 32 | UIInterfaceOrientationLandscapeLeft 33 | UIInterfaceOrientationLandscapeRight 34 | 35 | UISupportedInterfaceOrientations~ipad 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationPortraitUpsideDown 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | UIViewControllerBasedStatusBarAppearance 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /Demo/UIColor+ALComplementary.h: -------------------------------------------------------------------------------- 1 | // 2 | // UIColor+ALComplementary.h 3 | // ALButtonMenu 4 | // 5 | // Copyright © 2016 Anthony Lobianco. All rights reserved. 6 | // 7 | 8 | #import 9 | 10 | @interface UIColor (ALComplementary) 11 | 12 | - (UIColor *)al_complementaryColor; 13 | - (UIColor *)al_complementaryColorWithAlpha:(CGFloat)alpha; 14 | 15 | - (UIColor *)al_neutralColor; 16 | + (UIColor *)al_neutralColor; 17 | 18 | @end 19 | -------------------------------------------------------------------------------- /Demo/UIColor+ALComplementary.m: -------------------------------------------------------------------------------- 1 | // 2 | // UIColor+ALComplementary.m 3 | // ALButtonMenu 4 | // 5 | // Copyright © 2016 Anthony Lobianco. All rights reserved. 6 | // 7 | 8 | #import "UIColor+ALComplementary.h" 9 | 10 | @implementation UIColor (ALComplementary) 11 | 12 | - (UIColor *)al_complementaryColor 13 | { 14 | return [self al_complementaryColorWithAlpha:1.f]; 15 | } 16 | 17 | - (UIColor *)al_complementaryColorWithAlpha:(CGFloat)alpha 18 | { 19 | CGFloat r, g, b; 20 | [self getRed:&r green:&g blue:&b alpha:nil]; 21 | 22 | return [UIColor colorWithRed:(1.f - r) green:(1.f - g) blue:(1.f - b) alpha:alpha]; 23 | } 24 | 25 | - (UIColor *)al_neutralColor 26 | { 27 | CGFloat r, g, b; 28 | [self getRed:&r green:&g blue:&b alpha:nil]; 29 | 30 | NSUInteger red = ((r * 255.f) + 256) / 2; 31 | NSUInteger green = ((g * 255.f) + 256) / 2; 32 | NSUInteger blue = ((b * 255.f) + 256) / 2; 33 | 34 | return [UIColor colorWithRed:(red / 255.f) green:(green / 255.f) blue:(blue / 255.f) alpha:1.f]; 35 | } 36 | 37 | + (UIColor *)al_neutralColor 38 | { 39 | NSUInteger red = arc4random_uniform(255); 40 | NSUInteger green = arc4random_uniform(255); 41 | NSUInteger blue = arc4random_uniform(255); 42 | 43 | return [[UIColor colorWithRed:(red / 255.f) green:(green / 255.f) blue:(blue / 255.f) alpha:1.f] al_neutralColor]; 44 | } 45 | 46 | @end 47 | -------------------------------------------------------------------------------- /Demo/main.m: -------------------------------------------------------------------------------- 1 | // 2 | // main.m 3 | // ALButtonMenu 4 | // 5 | // Copyright © 2016 Anthony Lobianco. All rights reserved. 6 | // 7 | 8 | #import 9 | #import "ALDemoAppDelegate.h" 10 | 11 | int main(int argc, char * argv[]) { 12 | @autoreleasepool { 13 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([ALDemoAppDelegate class])); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Anthony Lobianco 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ALButtonMenu 2 | 3 | ALButtomMenu is a fast, customizable, fully documented menu solution for iOS. 4 | 5 | ### Preview 6 | 7 | ![Preview1](https://github.com/lobianco/ALButtonMenu/blob/main/Screenshots/demo1.gif?raw=true) ![Preview2](https://github.com/lobianco/ALButtonMenu/blob/main/Screenshots/demo2.gif?raw=true) 8 | 9 | ## Installation 10 | 11 | Installation is easy. 12 | 13 | ### Cocoapods 14 | 15 | 1. `pod 'ALButtonMenu'` in your Podfile 16 | 2. `#import ` in your view of choice 17 | 18 | ### Manually 19 | 20 | 1. [Download the .zip](https://github.com/lobianco/ALButtonMenu/archive/master.zip) from Github and copy `ALButtonMenu/Source` directory to your project 21 | 2. `#import "ALButtonMenu.h"` in your view of choice 22 | 23 | ## Example Usage 24 | 25 | Refer to the demo project for an interactive example, or just take a look at the code and comments below. 26 | 27 | ```objc 28 | 29 | // 30 | // MyRootViewController.m 31 | // 32 | 33 | // this, or whatever init method you choose to use 34 | - (instancetype)init 35 | { 36 | if ((self = [super init]) == NO) 37 | { 38 | return nil; 39 | } 40 | 41 | // the layout that we'll use for the menu view controller 42 | ALMenuViewControllerLayout layout; 43 | 44 | // the menu items will be displayed in a grid with this many columns. however, in landscape mode, 45 | // this value will be used for the number of rows instead. 46 | // 47 | layout.columns = 2; 48 | 49 | // the spacing between menu items 50 | layout.itemSpacing = 15.f; 51 | 52 | // the size of the menu items 53 | layout.itemSize = CGSizeMake(100.f, 100.f); 54 | 55 | // can be an array of any number of items that inherit from ALButton or conform to the protocol 56 | NSArray *> *items = [self allMenuItems]; 57 | 58 | // create the view model for the menu view controller 59 | ALMenuViewControllerViewModel *menuViewModel = [[ALMenuViewControllerViewModel alloc] initWithItems:items layout:layout]; 60 | 61 | // tweak the default values. see ALMenuViewControllerViewModel.h for configurable properties 62 | menuViewModel.appearingAnimation = ALMenuViewControllerAppearingAnimationOrigin; 63 | 64 | // the menu view controller can be an instance of ALMenuViewController, or any class that conforms 65 | // to the protocol 66 | // 67 | ALMenuViewController *menuViewController = [[ALMenuViewController alloc] initWithViewModel:menuViewModel]; 68 | 69 | // an instance of your view controller class 70 | MyViewController *viewController = [[MyViewController alloc] init]; 71 | 72 | // create the view model for the navigation coordinator 73 | ALNavigationCoordinatorViewModel *navViewModel = [[ALNavigationCoordinatorViewModel alloc] init]; 74 | 75 | // tweak the default values. see ALNavigationCoordinatorViewModel.h for configurable properties. 76 | navViewModel.buttonCanBeRepositioned = YES; 77 | 78 | // create the navigation coordinator with the menu view controller and your app's root view controller. the 79 | // root view controller can be an instance of UIViewController or UINavigationController 80 | // 81 | _navigationCoordinator = [[ALNavigationCoordinator alloc] initWithViewModel:navViewModel menuViewController:menuViewController rootViewController:rootViewController]; 82 | 83 | // and be sure to assign yourself as the delegate. if you configure the navigation coordinator with a navigation 84 | // controller (instead of a root view controller), the coordinator will need to assign itself as that navigation 85 | // controller's delegate, so you can optionally receive those delegate callbacks via this assignment. just 86 | // implement the methods. 87 | // 88 | _navigationCoordinator.delegate = self; 89 | 90 | return self; 91 | } 92 | 93 | - (void)viewDidLoad 94 | { 95 | [super viewDidLoad]; 96 | 97 | // the navigation coordinator creates a navigation controller configured with the provided 98 | // menu view controller and root view controller. we need to add that navigation controller 99 | // to the view heirarchy 100 | // 101 | UIViewController *childViewController = self.navigationCoordinator.navigationController; 102 | 103 | // then add it as a child view controller 104 | [self addChildViewController:childViewController]; 105 | [self.view addSubview:childViewController.view]; 106 | [childViewController didMoveToParentViewController:self]; 107 | 108 | // then notify the navigation coordinator about our viewDidLoad event 109 | [self.navigationCoordinator viewDidLoad]; 110 | } 111 | 112 | #pragma mark - ALNavigationCoordinatorDelegate 113 | 114 | // be sure to implement the navigation coordinator's delegate method. it will fire when an item in the menu view controller is 115 | // tapped. return your specific UIViewController instance for that index. 116 | // 117 | - (UIViewController *)navigationCoordinator:(ALNavigationCoordinator *)navigationCoordinator viewControllerForMenuItemAtIndex:(NSUInteger)index 118 | { 119 | return [[MyViewController alloc] init]; 120 | } 121 | 122 | #pragma mark - Status bar 123 | 124 | // optionally, return the menu view controller in this method to hide the status bar when the menu is shown. 125 | - (UIViewController *)childViewControllerForStatusBarHidden 126 | { 127 | return self.navigationCoordinator.menuViewController; 128 | } 129 | 130 | #pragma mark - Rotation 131 | 132 | // be sure to alert the navigation coordinator about size change events. 133 | - (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id)coordinator 134 | { 135 | [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator]; 136 | 137 | [self.navigationCoordinator viewWillTransitionToSize:size withTransitionCoordinator:coordinator]; 138 | } 139 | 140 | ``` 141 | 142 | ## Contact Me 143 | 144 | You can reach me anytime at the addresses below. If you use ALButtonMenu, feel free to give me a shoutout on Twitter to let me know how you like it. I'd love to hear your thoughts! 145 | 146 | Github: [lobianco](https://github.com/lobianco)
147 | Email: [anthony@lobian.co](mailto:anthony@lobian.co) 148 | 149 | ## Credits & License 150 | 151 | ALButtonMenu is developed and maintained by Anthony Lobianco. Licensed under the MIT License. Basically, I would appreciate attribution if you use it. 152 | 153 | Enjoy! 154 | -------------------------------------------------------------------------------- /Screenshots/demo1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lobianco/ALButtonMenu/cb6b35a28af1167e80e9bdcae828fdd4ce5459be/Screenshots/demo1.gif -------------------------------------------------------------------------------- /Screenshots/demo2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lobianco/ALButtonMenu/cb6b35a28af1167e80e9bdcae828fdd4ce5459be/Screenshots/demo2.gif -------------------------------------------------------------------------------- /Source/ALAnimationController.h: -------------------------------------------------------------------------------- 1 | // 2 | // ALAnimationController.h 3 | // ALButtonMenu 4 | // 5 | // Copyright © 2016 Anthony Lobianco. All rights reserved. 6 | // 7 | 8 | #import 9 | #import 10 | 11 | #import "ALNavigationCoordinatorViewModel.h" 12 | 13 | NS_ASSUME_NONNULL_BEGIN 14 | 15 | @class ALAnimationController; 16 | 17 | @protocol ALAnimationControllerDelegate 18 | 19 | @optional 20 | 21 | - (void)animationController:(ALAnimationController *)animationController didShowViewController:(UIViewController *)viewController; 22 | - (void)animationController:(ALAnimationController *)animationController didHideViewController:(UIViewController *)viewController; 23 | 24 | @end 25 | 26 | @interface ALAnimationController : NSObject 27 | 28 | @property (nonatomic) ALNavigationCoordinatorAnimation appearingAnimation; 29 | @property (nonatomic) ALNavigationCoordinatorAnimation disappearingAnimation; 30 | @property (nullable, nonatomic, weak) id delegate; 31 | @property (nonatomic) CAShapeLayer *initialShapeLayer; 32 | @property (nonatomic, getter=isPresenting) BOOL presenting; 33 | 34 | @end 35 | 36 | NS_ASSUME_NONNULL_END 37 | -------------------------------------------------------------------------------- /Source/ALAnimationController.m: -------------------------------------------------------------------------------- 1 | // 2 | // ALAnimationController.m 3 | // ALButtonMenu 4 | // 5 | // Copyright © 2016 Anthony Lobianco. All rights reserved. 6 | // 7 | 8 | #import "ALAnimationController.h" 9 | 10 | #import "UIView+ALLayout.h" 11 | 12 | static NSTimeInterval const kAnimationDuration = 0.3; 13 | static CGFloat const kDimmingViewMaxAlpha = 0.5f; 14 | static CGFloat const kExpandViewLayerScaleFactor = 1.5f; 15 | static CGFloat const kCASpringAnimationDamping = 15.f; 16 | static CGFloat const kCASpringAnimationStiffness = 115.f; 17 | static CGFloat const kUIKSpringDamping = 0.53f; 18 | static CGFloat const kUIKSpringVelocity = 0.f; 19 | 20 | @interface ALAnimationController () 21 | 22 | @property (nonatomic, weak) id transitionContext; 23 | 24 | @end 25 | 26 | @implementation ALAnimationController 27 | 28 | #pragma mark - UIViewControllerAnimatedTransitioning 29 | 30 | - (void)animateTransition:(id)transitionContext 31 | { 32 | self.transitionContext = transitionContext; 33 | 34 | UIView *containerView = transitionContext.containerView; 35 | UIViewController *toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey]; 36 | UIView *toView = toViewController.view; 37 | UIViewController *fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey]; 38 | UIView *fromView = fromViewController.view; 39 | 40 | // make sure the frames are correct, in case a rotation event occurred 41 | CGRect finalToViewFrame = [transitionContext finalFrameForViewController:toViewController]; 42 | toView.frame = finalToViewFrame; 43 | 44 | if ([self isPresenting]) 45 | { 46 | [self animatePresentationWithContainerView:containerView fromView:fromView toView:toView]; 47 | } 48 | else 49 | { 50 | [self animateDismissalWithContainerView:containerView fromView:fromView toView:toView]; 51 | } 52 | } 53 | 54 | - (NSTimeInterval)transitionDuration:(id)transitionContext 55 | { 56 | return kAnimationDuration; 57 | } 58 | 59 | #pragma mark - Animation methods 60 | 61 | - (void)animatePresentationWithContainerView:(UIView *)containerView fromView:(UIView *)fromView toView:(UIView *)toView 62 | { 63 | // containerView already contains fromView 64 | [containerView addSubview:toView]; 65 | 66 | // create a mask to gradually reveal the incoming view 67 | CAShapeLayer *maskLayer = self.initialShapeLayer; 68 | CATransform3D maskFromTransform = CATransform3DIdentity; 69 | CGFloat scale = [self scaleForView:toView]; 70 | CATransform3D maskToTransform = CATransform3DScale(CATransform3DIdentity, scale, scale, 1.f); 71 | // set the final transform on the layer before the animation begins 72 | maskLayer.transform = maskToTransform; 73 | 74 | // mask the incoming view 75 | toView.layer.mask = maskLayer; 76 | 77 | // animate the mask 78 | CABasicAnimation *maskLayerAnimation = [self transformAnimationWithFromTransform:maskFromTransform toTransform:maskToTransform timingFunctionName:kCAMediaTimingFunctionEaseIn springy:NO]; 79 | maskLayerAnimation.delegate = self; 80 | [maskLayer addAnimation:maskLayerAnimation forKey:@"maskLayer"]; 81 | 82 | // create the dimming effect as the menu is shown 83 | UIView *dimmingView = [self addDimmingViewToContainerView:containerView belowView:toView]; 84 | dimmingView.alpha = 0.f; 85 | 86 | // animate the outgoing view. we can animate these layer-backed views with 87 | // UIView animation methods. 88 | // 89 | 90 | BOOL shouldAnimateTransform = self.disappearingAnimation != ALNavigationCoordinatorAnimationNone; 91 | 92 | if (shouldAnimateTransform) 93 | { 94 | // set fromView's anchor point to be based on the location of the initial shape 95 | // layer, so the expansion animation will look like it's originating from the 96 | // location of the initial shape. note that since we're adjusting the anchor 97 | // point, we'll also have to adjust the position to keep the view from jumping 98 | // to a new location onscreen. 99 | // 100 | [self adjustAnchorPointAndPositionForView:fromView]; 101 | } 102 | 103 | // no springs here (not needed, the view will be out of sight before the spring 104 | // effect would be noticed) 105 | // 106 | [UIView animateWithDuration:maskLayerAnimation.duration 107 | delay:0. 108 | options:UIViewAnimationOptionCurveEaseInOut|UIViewAnimationOptionBeginFromCurrentState 109 | animations:^{ 110 | if (shouldAnimateTransform) 111 | { 112 | fromView.transform = CGAffineTransformScale(CGAffineTransformIdentity, kExpandViewLayerScaleFactor, kExpandViewLayerScaleFactor); 113 | } 114 | dimmingView.alpha = kDimmingViewMaxAlpha; 115 | } 116 | completion:nil]; 117 | 118 | // no need to animate the incoming view, it will be our custom menu VC that 119 | // will perform its own animations. what's more, animating the incoming 120 | // view by scaling it up from a small size can potentially conflict with 121 | // the layer mask's revealing animation. 122 | // 123 | } 124 | 125 | - (void)animateDismissalWithContainerView:(UIView *)containerView fromView:(UIView *)fromView toView:(UIView *)toView 126 | { 127 | // containerView already contains fromView 128 | [containerView insertSubview:toView belowSubview:fromView]; 129 | 130 | // mask the outgoing view 131 | CAShapeLayer *maskLayer = self.initialShapeLayer; 132 | CGFloat scale = [self scaleForView:fromView]; 133 | CATransform3D maskFromTransform = CATransform3DScale(CATransform3DIdentity, scale, scale, 1.f); 134 | CATransform3D maskToTransform = CATransform3DIdentity; 135 | maskLayer.transform = CATransform3DIdentity; 136 | fromView.layer.mask = maskLayer; 137 | 138 | // animate the mask 139 | CABasicAnimation *maskLayerAnimation = [self transformAnimationWithFromTransform:maskFromTransform toTransform:maskToTransform timingFunctionName:kCAMediaTimingFunctionEaseOut springy:YES]; 140 | maskLayerAnimation.delegate = self; 141 | [maskLayer addAnimation:maskLayerAnimation forKey:@"maskLayer"]; 142 | 143 | // create the dimming effect as the menu is hidden 144 | UIView *dimmingView = [self addDimmingViewToContainerView:containerView belowView:fromView]; 145 | dimmingView.alpha = kDimmingViewMaxAlpha; 146 | 147 | BOOL shouldAnimateTransform = self.appearingAnimation != ALNavigationCoordinatorAnimationNone; 148 | 149 | if (shouldAnimateTransform) 150 | { 151 | // animate the incoming view 152 | [self adjustAnchorPointAndPositionForView:toView]; 153 | 154 | toView.layer.transform = CATransform3DScale(CATransform3DIdentity, kExpandViewLayerScaleFactor, kExpandViewLayerScaleFactor, 1.f); 155 | } 156 | 157 | [UIView animateWithDuration:maskLayerAnimation.duration 158 | delay:0. 159 | usingSpringWithDamping:kUIKSpringDamping 160 | initialSpringVelocity:kUIKSpringVelocity 161 | options:UIViewAnimationOptionCurveEaseOut|UIViewAnimationOptionBeginFromCurrentState 162 | animations:^{ 163 | if (shouldAnimateTransform) 164 | { 165 | toView.transform = CGAffineTransformIdentity; 166 | } 167 | dimmingView.alpha = 0.f; 168 | } 169 | completion:nil]; 170 | 171 | // don't animate the outgoing view, it interferes with the mask animation. let 172 | // the view's controller (our custom menu VC) do the animations. 173 | // 174 | } 175 | 176 | #pragma mark - Helper methods 177 | 178 | - (UIView *)addDimmingViewToContainerView:(UIView *)containerView belowView:(UIView *)view 179 | { 180 | // create the dimming view, which will fade in/out as the menu is shown/hidden 181 | UIView *dimmingView = [[UIView alloc] init]; 182 | dimmingView.translatesAutoresizingMaskIntoConstraints = NO; 183 | dimmingView.backgroundColor = [UIColor blackColor]; 184 | 185 | // then add and constrain 186 | [containerView insertSubview:dimmingView belowSubview:view]; 187 | [dimmingView al_pinToSuperview]; 188 | 189 | return dimmingView; 190 | } 191 | 192 | - (CGFloat)scaleForView:(UIView *)view 193 | { 194 | CAShapeLayer *initialShapeLayer = self.initialShapeLayer; 195 | CGRect initialBounds = initialShapeLayer.bounds; 196 | 197 | // calculate a scale big enough for any convex shape to be able to expand 198 | // its size proportionally and still cover the entire view. inscribed values 199 | // refer to the dimensions of the largest rectangle that can be drawn 200 | // inside an ellipse with frame == intialBounds. 201 | // 202 | CGFloat width = CGRectGetWidth(initialBounds); 203 | CGFloat inscribedWidth = sqrt(2) * (width / 2.f); 204 | CGFloat height = CGRectGetHeight(initialBounds); 205 | CGFloat inscribedHeight = sqrt(2) * (height / 2.f); 206 | CGRect insetBounds = CGRectInset(initialBounds, (width - inscribedWidth) / 2.f, (height - inscribedHeight) / 2.f); 207 | CGFloat maxXDistance = MAX(CGRectGetMidX(insetBounds), CGRectGetWidth(view.frame) - CGRectGetMidX(insetBounds)); 208 | CGFloat maxYDistance = MAX(CGRectGetMidY(insetBounds), CGRectGetHeight(view.frame) - CGRectGetMidY(insetBounds)); 209 | CGFloat scale = MAX(((maxXDistance / CGRectGetWidth(insetBounds)) * 2.f), ((maxYDistance / CGRectGetHeight(insetBounds)) * 2.f)); 210 | 211 | return scale; 212 | } 213 | 214 | - (CABasicAnimation *)transformAnimationWithFromTransform:(CATransform3D)fromTransform toTransform:(CATransform3D)toTransform timingFunctionName:(NSString *)timingFunction springy:(BOOL)springy 215 | { 216 | //TODO: allow interrupting current animation from current state 217 | CABasicAnimation *animation = nil; 218 | static NSString * const transformKeyPath = @"transform"; 219 | 220 | if (springy) 221 | { 222 | CASpringAnimation *springAnimation = [CASpringAnimation animationWithKeyPath:transformKeyPath]; 223 | springAnimation.damping = kCASpringAnimationDamping; 224 | springAnimation.stiffness = kCASpringAnimationStiffness; 225 | springAnimation.duration = MAX(kAnimationDuration, springAnimation.settlingDuration); 226 | 227 | animation = springAnimation; 228 | } 229 | else 230 | { 231 | animation = [CABasicAnimation animationWithKeyPath:transformKeyPath]; 232 | animation.duration = kAnimationDuration; 233 | } 234 | 235 | animation.fromValue = [NSValue valueWithCATransform3D:fromTransform]; 236 | animation.toValue = [NSValue valueWithCATransform3D:toTransform]; 237 | animation.timingFunction = [CAMediaTimingFunction functionWithName:timingFunction]; 238 | 239 | return animation; 240 | } 241 | 242 | - (void)adjustAnchorPointAndPositionForView:(UIView *)view 243 | { 244 | CAShapeLayer *initialShapeLayer = self.initialShapeLayer; 245 | CGRect initialBounds = initialShapeLayer.bounds; 246 | 247 | CGFloat xAnchor = CGRectGetMidX(initialBounds) / CGRectGetWidth(view.frame); 248 | CGFloat yAnchor = CGRectGetMidY(initialBounds) / CGRectGetHeight(view.frame); 249 | CGPoint newAnchorPoint = CGPointMake(xAnchor, yAnchor); 250 | 251 | [view al_adjustPositionForNewAnchorPoint:newAnchorPoint]; 252 | } 253 | 254 | #pragma mark - CAAnimationDelegate 255 | 256 | - (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag 257 | { 258 | [self cleanupAfterAnimation]; 259 | } 260 | 261 | #pragma mark - Cleanup methods 262 | 263 | - (void)cleanupAfterAnimation 264 | { 265 | id transitionContext = self.transitionContext; 266 | 267 | UIViewController *toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey]; 268 | UIView *toView = toViewController.view; 269 | UIViewController *fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey]; 270 | UIView *fromView = fromViewController.view; 271 | 272 | [transitionContext completeTransition:transitionContext.transitionWasCancelled == NO]; 273 | 274 | fromView.layer.mask = nil; 275 | toView.layer.mask = nil; 276 | 277 | fromView.layer.transform = CATransform3DIdentity; 278 | toView.layer.transform = CATransform3DIdentity; 279 | 280 | fromView.transform = CGAffineTransformIdentity; 281 | toView.transform = CGAffineTransformIdentity; 282 | } 283 | 284 | - (void)animationEnded:(BOOL)transitionCompleted 285 | { 286 | id transitionContext = self.transitionContext; 287 | 288 | UIViewController *toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey]; 289 | UIViewController *fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey]; 290 | 291 | if ([self isPresenting]) 292 | { 293 | if ([self.delegate respondsToSelector:@selector(animationController:didShowViewController:)]) 294 | { 295 | [self.delegate animationController:self didShowViewController:toViewController]; 296 | } 297 | } 298 | else 299 | { 300 | if ([self.delegate respondsToSelector:@selector(animationController:didHideViewController:)]) 301 | { 302 | [self.delegate animationController:self didHideViewController:fromViewController]; 303 | } 304 | } 305 | 306 | self.transitionContext = nil; 307 | } 308 | 309 | @end 310 | -------------------------------------------------------------------------------- /Source/ALAnimationTransitionDelegate.h: -------------------------------------------------------------------------------- 1 | // 2 | // ALAnimationTransition.h 3 | // ALButtonMenu 4 | // 5 | // Copyright © 2016 Anthony Lobianco. All rights reserved. 6 | // 7 | 8 | #import 9 | #import 10 | 11 | #import "ALNavigationCoordinatorViewModel.h" 12 | 13 | NS_ASSUME_NONNULL_BEGIN 14 | 15 | @class ALAnimationTransitionDelegate; 16 | 17 | // hey dawg, i heard you like delegates 18 | @protocol ALAnimationTransitionDelegateDelegate 19 | 20 | @optional 21 | 22 | // always referring to the VC on the top of the stack 23 | - (void)transitionDelegate:(ALAnimationTransitionDelegate *)transitionDelegate didShowViewController:(UIViewController *)viewController; 24 | - (void)transitionDelegate:(ALAnimationTransitionDelegate *)transitionDelegate didHideViewController:(UIViewController *)viewController; 25 | 26 | @end 27 | 28 | @interface ALAnimationTransitionDelegate : NSObject 29 | 30 | @property (nonatomic) ALNavigationCoordinatorAnimation appearingAnimation; 31 | @property (nonatomic, weak) id delegate; 32 | @property (nonatomic) ALNavigationCoordinatorAnimation disappearingAnimation; 33 | @property (nonatomic) CAShapeLayer *initialShapeLayer; 34 | @property (nonatomic) Class viewControllerClassForCustomAnimations; 35 | 36 | - (instancetype)initWithNavigationControllerDelegate:(nullable id)delegate; 37 | 38 | @end 39 | 40 | NS_ASSUME_NONNULL_END 41 | -------------------------------------------------------------------------------- /Source/ALAnimationTransitionDelegate.m: -------------------------------------------------------------------------------- 1 | // 2 | // ALAnimationTransition.m 3 | // ALButtonMenu 4 | // 5 | // Copyright © 2016 Anthony Lobianco. All rights reserved. 6 | // 7 | 8 | #import "ALAnimationTransitionDelegate.h" 9 | 10 | #import "ALAnimationController.h" 11 | #import "ALUtils.h" 12 | 13 | @interface ALAnimationTransitionDelegate () 14 | 15 | @property (nonatomic, weak) id navigationControllerDelegate; 16 | @property (nonatomic) Class navigationControllerDelegateClass; 17 | 18 | @end 19 | 20 | @implementation ALAnimationTransitionDelegate 21 | 22 | - (instancetype)init 23 | { 24 | return [self initWithNavigationControllerDelegate:nil]; 25 | } 26 | 27 | - (instancetype)initWithNavigationControllerDelegate:(id)navigationControllerDelegate 28 | { 29 | AL_INIT([super init]); 30 | 31 | _navigationControllerDelegate = navigationControllerDelegate; 32 | _navigationControllerDelegateClass = [navigationControllerDelegate class]; 33 | 34 | return self; 35 | } 36 | 37 | #pragma mark - UINavigationControllerDelegate 38 | 39 | - (id)navigationController:(UINavigationController *)navigationController animationControllerForOperation:(UINavigationControllerOperation)operation fromViewController:(UIViewController *)fromVC toViewController:(UIViewController *)toVC 40 | { 41 | Class classForCustomAnimations = self.viewControllerClassForCustomAnimations; 42 | 43 | if ([toVC isKindOfClass:classForCustomAnimations] == NO && [fromVC isKindOfClass:classForCustomAnimations] == NO) 44 | { 45 | return nil; 46 | } 47 | 48 | NSParameterAssert(self.initialShapeLayer != nil); 49 | ALAnimationController *animationController = [[ALAnimationController alloc] init]; 50 | 51 | // animationController will live long enough to provide us delegate callbacks 52 | animationController.delegate = self; 53 | 54 | animationController.presenting = operation == UINavigationControllerOperationPush; 55 | animationController.initialShapeLayer = self.initialShapeLayer; 56 | animationController.appearingAnimation = self.appearingAnimation; 57 | animationController.disappearingAnimation = self.disappearingAnimation; 58 | 59 | return animationController; 60 | } 61 | 62 | #pragma mark - ALAnimationControllerDelegate 63 | 64 | - (void)animationController:(ALAnimationController *)animationController didShowViewController:(UIViewController *)viewController 65 | { 66 | if ([self.delegate respondsToSelector:@selector(transitionDelegate:didShowViewController:)]) 67 | { 68 | [self.delegate transitionDelegate:self didShowViewController:viewController]; 69 | } 70 | } 71 | 72 | - (void)animationController:(ALAnimationController *)animationController didHideViewController:(UIViewController *)viewController 73 | { 74 | if ([self.delegate respondsToSelector:@selector(transitionDelegate:didHideViewController:)]) 75 | { 76 | [self.delegate transitionDelegate:self didHideViewController:viewController]; 77 | } 78 | } 79 | 80 | #pragma mark - Message forwarding 81 | 82 | - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector 83 | { 84 | return [self.navigationControllerDelegateClass instanceMethodSignatureForSelector:aSelector]; 85 | } 86 | 87 | - (void)forwardInvocation:(NSInvocation *)anInvocation 88 | { 89 | if ([self.navigationControllerDelegate respondsToSelector:anInvocation.selector] == NO) 90 | { 91 | return; 92 | } 93 | 94 | [anInvocation invokeWithTarget:self.navigationControllerDelegate]; 95 | } 96 | 97 | @end 98 | -------------------------------------------------------------------------------- /Source/ALButton.h: -------------------------------------------------------------------------------- 1 | // 2 | // ALButton.h 3 | // ALButtonMenu 4 | // 5 | // Copyright © 2016 Anthony Lobianco. All rights reserved. 6 | // 7 | 8 | #import 9 | 10 | #import "ALMenuItem.h" 11 | 12 | NS_ASSUME_NONNULL_BEGIN 13 | 14 | @class ALButtonViewModel; 15 | 16 | @interface ALButton : UIButton 17 | 18 | @property (nullable, nonatomic, readonly) UIBezierPath *scaledMaskPath; 19 | @property (nonatomic, readonly) ALButtonViewModel *viewModel; 20 | 21 | - (instancetype)initWithViewModel:(ALButtonViewModel *)viewModel NS_DESIGNATED_INITIALIZER; 22 | 23 | @end 24 | 25 | NS_ASSUME_NONNULL_END 26 | -------------------------------------------------------------------------------- /Source/ALButton.m: -------------------------------------------------------------------------------- 1 | // 2 | // ALButton.m 3 | // ALButtonMenu 4 | // 5 | // Copyright © 2016 Anthony Lobianco. All rights reserved. 6 | // 7 | 8 | #import "ALButton_ALPrivate.h" 9 | 10 | #import "UIBezierPath+ALScaling.h" 11 | #import "UIButton+ALBounce.h" 12 | 13 | #import "ALUtils.h" 14 | 15 | static NSTimeInterval const kButtonScaleAnimationDuration = 0.25; 16 | static CGFloat const kButtonShrinkScaleFactor = 0.9f; 17 | 18 | @interface ALButton () 19 | 20 | @property (nonatomic) CGSize configuredMaskSize; 21 | @property (nonatomic) UIImageView *imgView; 22 | @property (nonatomic) CALayer *maskContainerLayer; 23 | @property (nonatomic) ALTouchGestureRecognizer *touchGestureRecognizer; 24 | 25 | @end 26 | 27 | @implementation ALButton 28 | 29 | @synthesize delegate = _delegate; 30 | 31 | - (instancetype)initWithViewModel:(ALButtonViewModel *)viewModel 32 | { 33 | AL_INIT([super initWithFrame:CGRectZero]); 34 | 35 | _viewModel = viewModel; 36 | _viewModel.delegate = self; 37 | 38 | self.backgroundColor = [UIColor clearColor]; 39 | 40 | [self configureGestureRecognizers]; 41 | 42 | return self; 43 | } 44 | 45 | - (instancetype)initWithFrame:(CGRect)frame 46 | { 47 | NSAssert(NO, @"Hello there! Please use designated initializer."); 48 | AL_INIT([self initWithViewModel:[ALButtonViewModel new]]); 49 | return nil; 50 | } 51 | 52 | - (instancetype)initWithCoder:(NSCoder *)aDecoder 53 | { 54 | NSAssert(NO, @"Hello there! Please use designated initializer."); 55 | AL_INIT([self initWithViewModel:[ALButtonViewModel new]]); 56 | return nil; 57 | } 58 | 59 | - (UIBezierPath *)scaledMaskPath 60 | { 61 | return [self.viewModel.maskPath al_scaledToRect:self.bounds]; // bounds, not frame 62 | } 63 | 64 | - (void)didMoveToSuperview 65 | { 66 | [super didMoveToSuperview]; 67 | 68 | [self updateValues]; 69 | } 70 | 71 | - (void)layoutSubviews 72 | { 73 | [super layoutSubviews]; 74 | 75 | if (self.imgView != nil) 76 | { 77 | self.imgView.frame = CGRectMake(0.f, 0.f, self.frame.size.width, self.frame.size.height); 78 | } 79 | } 80 | 81 | - (void)layoutSublayersOfLayer:(CALayer *)layer 82 | { 83 | [super layoutSublayersOfLayer:layer]; 84 | 85 | if (CGSizeEqualToSize(layer.bounds.size, CGSizeZero)) 86 | { 87 | // wait until the view has been layed out 88 | return; 89 | } 90 | 91 | if ([self needsToUpdateMask]) 92 | { 93 | [self removeMask]; 94 | [self applyMaskIfNecessary]; 95 | } 96 | 97 | // update color outside above conditional to save on CPU cycles 98 | if (self.maskContainerLayer != nil) 99 | { 100 | self.backgroundColor = [UIColor clearColor]; 101 | self.maskContainerLayer.backgroundColor = [self.viewModel.color CGColor]; 102 | } 103 | else 104 | { 105 | self.backgroundColor = self.viewModel.color; 106 | } 107 | } 108 | 109 | - (void)updateValues 110 | { 111 | [self updateImageIfNecessary]; 112 | 113 | [self.layer setNeedsLayout]; 114 | [self.layer layoutIfNeeded]; 115 | } 116 | 117 | - (void)updateImageIfNecessary 118 | { 119 | if (self.viewModel.image == nil) 120 | { 121 | [self.imgView removeFromSuperview]; 122 | } 123 | else 124 | { 125 | if (self.imgView == nil) 126 | { 127 | self.imgView = [[UIImageView alloc] initWithImage:self.viewModel.image]; 128 | 129 | // add to view heirarchy 130 | [self addSubview:self.imgView]; 131 | } 132 | else 133 | { 134 | self.imgView.image = self.viewModel.image; 135 | } 136 | } 137 | } 138 | 139 | #pragma mark - Masking 140 | 141 | - (void)removeMask 142 | { 143 | // remove old mask layer 144 | [self.maskContainerLayer removeFromSuperlayer]; 145 | 146 | // nil out values for future needsToUpdateMaskContainerLayer checks 147 | self.maskContainerLayer = nil; 148 | self.configuredMaskSize = CGSizeZero; 149 | } 150 | 151 | - (void)applyMaskIfNecessary 152 | { 153 | if ([self shouldMask] == NO) 154 | { 155 | return; 156 | } 157 | 158 | // apply a mask layer to a container layer, so we can clip ourself 159 | // to the provided path shape but still be able to apply a shadow 160 | // if desired. 161 | // 162 | 163 | CAShapeLayer *maskLayer = [CAShapeLayer layer]; 164 | maskLayer.path = self.scaledMaskPath.CGPath; 165 | maskLayer.bounds = self.bounds; 166 | maskLayer.position = CGPointMake(CGRectGetMidX(self.bounds), CGRectGetMidY(self.bounds)); 167 | 168 | self.maskContainerLayer = [CALayer layer]; 169 | self.maskContainerLayer.bounds = self.bounds; 170 | self.maskContainerLayer.position = CGPointMake(CGRectGetMidX(self.bounds), CGRectGetMidY(self.bounds)); 171 | self.maskContainerLayer.mask = maskLayer; 172 | 173 | self.configuredMaskSize = self.bounds.size; 174 | 175 | // send it to the back so other subviews can still appear above it 176 | [self.layer insertSublayer:self.maskContainerLayer atIndex:0]; 177 | } 178 | 179 | - (BOOL)needsToUpdateMask 180 | { 181 | BOOL hasMask = self.maskContainerLayer != nil; 182 | BOOL shouldMask = [self shouldMask]; 183 | 184 | if (hasMask != shouldMask) 185 | { 186 | return YES; 187 | } 188 | 189 | if (shouldMask && CGSizeEqualToSize(self.configuredMaskSize, self.bounds.size) == NO) 190 | { 191 | return YES; 192 | } 193 | 194 | return NO; 195 | } 196 | 197 | - (BOOL)shouldMask 198 | { 199 | return self.viewModel.maskPath != nil; 200 | } 201 | 202 | #pragma mark - Gestures 203 | 204 | - (void)configureGestureRecognizers 205 | { 206 | self.touchGestureRecognizer = [[ALTouchGestureRecognizer alloc] initWithTarget:self action:@selector(handleTouchGesture:)]; 207 | self.touchGestureRecognizer.delegate = self; 208 | 209 | [self addGestureRecognizer:self.touchGestureRecognizer]; 210 | } 211 | 212 | - (void)handleTouchGesture:(ALTouchGestureRecognizer *)sender 213 | { 214 | switch (sender.state) 215 | { 216 | case UIGestureRecognizerStateBegan: 217 | { 218 | if (self.viewModel.bounces) 219 | { 220 | [self al_transformToSize:kButtonShrinkScaleFactor duration:kButtonScaleAnimationDuration]; 221 | } 222 | 223 | break; 224 | } 225 | case UIGestureRecognizerStateEnded: 226 | { 227 | if (self.viewModel.bounces && self.viewModel.bouncesOnTouchUp) 228 | { 229 | [self al_restoreWithDuration:kButtonScaleAnimationDuration]; 230 | } 231 | 232 | if ([self.delegate respondsToSelector:@selector(buttonWasTapped:)]) 233 | { 234 | [self.delegate buttonWasTapped:self]; 235 | } 236 | 237 | break; 238 | } 239 | case UIGestureRecognizerStateCancelled: 240 | case UIGestureRecognizerStateFailed: 241 | { 242 | if (self.viewModel.bounces) 243 | { 244 | [self al_restoreWithDuration:kButtonScaleAnimationDuration]; 245 | } 246 | 247 | break; 248 | } 249 | case UIGestureRecognizerStateChanged: 250 | case UIGestureRecognizerStatePossible: 251 | { 252 | // do nothing 253 | break; 254 | } 255 | } 256 | } 257 | 258 | #pragma mark - UIGestureRecognizerDelegate 259 | 260 | - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer 261 | { 262 | return YES; 263 | } 264 | 265 | #pragma mark - ALButtonViewModelDelegate 266 | 267 | - (void)viewModelDidUpdate:(ALButtonViewModel *)viewModel 268 | { 269 | [self updateValues]; 270 | } 271 | 272 | #pragma mark - Hit tests 273 | 274 | - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event 275 | { 276 | UIView *view = [super hitTest:point withEvent:event]; 277 | return (view == self || [self.subviews containsObject:view]) ? self : view; 278 | } 279 | 280 | - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event 281 | { 282 | return CGRectContainsPoint(CGRectInset(self.bounds, -(self.viewModel.touchArea), -(self.viewModel.touchArea)), point); 283 | } 284 | 285 | @end 286 | -------------------------------------------------------------------------------- /Source/ALButtonMenu.h: -------------------------------------------------------------------------------- 1 | // 2 | // ALButtonMenu.h 3 | // ALButtonMenu 4 | // 5 | // Copyright © 2017 Anthony Lobianco. All rights reserved. 6 | // 7 | 8 | #import "ALButton.h" 9 | #import "ALButtonViewModel.h" 10 | #import "ALMenuViewController.h" 11 | #import "ALMenuViewControllerViewModel.h" 12 | #import "ALNavigationCoordinator.h" 13 | #import "ALNavigationCoordinatorViewModel.h" 14 | -------------------------------------------------------------------------------- /Source/ALButtonViewModel.h: -------------------------------------------------------------------------------- 1 | // 2 | // ALButtonViewModel.h 3 | // ALButtonMenu 4 | // 5 | // Copyright © 2016 Anthony Lobianco. All rights reserved. 6 | // 7 | 8 | #import 9 | #import 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | @interface ALButtonViewModel : NSObject 14 | 15 | /** 16 | Should the button bounce on touch down. 17 | 18 | Default is YES. 19 | */ 20 | @property (nonatomic) BOOL bounces; 21 | 22 | /** 23 | Should the button bounce on touch up. If bounces == NO, this value will be ignored. 24 | 25 | Default is YES. 26 | */ 27 | @property (nonatomic) BOOL bouncesOnTouchUp; 28 | 29 | /** 30 | The button color. 31 | 32 | Default is black. 33 | */ 34 | @property (nonatomic) UIColor *color; 35 | 36 | /** 37 | An optional image that can be displayed on the button. 38 | 39 | Default is nil. 40 | */ 41 | @property (nullable, nonatomic) UIImage *image; 42 | 43 | /** 44 | A bezier path can be specified to give the button a custom shape. It will be scaled down to 45 | proportionally fit inside the button. 46 | 47 | Default is nil. 48 | */ 49 | @property (nullable, nonatomic) UIBezierPath *maskPath; 50 | 51 | /** 52 | The distance outside of the button's bounds that will still register as a touch on the button. 53 | 54 | Default is 0.f. 55 | */ 56 | @property (nonatomic) CGFloat touchArea; 57 | 58 | @end 59 | 60 | NS_ASSUME_NONNULL_END 61 | -------------------------------------------------------------------------------- /Source/ALButtonViewModel.m: -------------------------------------------------------------------------------- 1 | // 2 | // ALButtonViewModel.m 3 | // ALButtonMenu 4 | // 5 | // Copyright © 2016 Anthony Lobianco. All rights reserved. 6 | // 7 | 8 | #import "ALButtonViewModel_ALPrivate.h" 9 | 10 | #import "ALUtils.h" 11 | 12 | @implementation ALButtonViewModel 13 | 14 | - (instancetype)init 15 | { 16 | AL_INIT([super init]); 17 | 18 | [self configureDefaults]; 19 | 20 | return self; 21 | } 22 | 23 | - (void)configureDefaults 24 | { 25 | _bounces = YES; 26 | _bouncesOnTouchUp = YES; 27 | _color = [UIColor blackColor]; 28 | } 29 | 30 | - (void)setCanReposition:(BOOL)canReposition 31 | { 32 | if (_canReposition == canReposition) 33 | { 34 | return; 35 | } 36 | 37 | _canReposition = canReposition; 38 | 39 | [self.delegate viewModelDidUpdate:self]; 40 | } 41 | 42 | - (void)setColor:(UIColor *)color 43 | { 44 | if (_color == color) 45 | { 46 | return; 47 | } 48 | 49 | _color = color; 50 | 51 | [self.delegate viewModelDidUpdate:self]; 52 | } 53 | 54 | - (void)setImage:(UIImage *)image 55 | { 56 | if (_image == image) 57 | { 58 | return; 59 | } 60 | 61 | _image = image; 62 | 63 | [self.delegate viewModelDidUpdate:self]; 64 | } 65 | 66 | - (void)setMaskPath:(UIBezierPath *)maskPath 67 | { 68 | if (_maskPath == maskPath) 69 | { 70 | return; 71 | } 72 | 73 | _maskPath = maskPath; 74 | 75 | [self.delegate viewModelDidUpdate:self]; 76 | } 77 | 78 | @end 79 | -------------------------------------------------------------------------------- /Source/ALButtonViewModel_ALPrivate.h: -------------------------------------------------------------------------------- 1 | // 2 | // ALButtonViewModel_ALPrivate.h 3 | // ALButtonMenu 4 | // 5 | // Copyright © 2017 Anthony Lobianco. All rights reserved. 6 | // 7 | 8 | #import "ALButtonViewModel.h" 9 | 10 | NS_ASSUME_NONNULL_BEGIN 11 | 12 | @class ALButtonViewModel; 13 | 14 | @protocol ALButtonViewModelDelegate 15 | 16 | - (void)viewModelDidUpdate:(ALButtonViewModel *)viewModel; 17 | 18 | @end 19 | 20 | @interface ALButtonViewModel () 21 | 22 | @property (nonatomic) BOOL canReposition; 23 | @property (nonatomic, weak) id delegate; 24 | 25 | @end 26 | 27 | NS_ASSUME_NONNULL_END 28 | -------------------------------------------------------------------------------- /Source/ALButton_ALPrivate.h: -------------------------------------------------------------------------------- 1 | // 2 | // ALButton_ALPrivate.h 3 | // ALButtonMenu 4 | // 5 | // Copyright © 2016 Anthony Lobianco. All rights reserved. 6 | // 7 | 8 | #import "ALButton.h" 9 | 10 | #import "ALTouchGestureRecognizer.h" 11 | #import "ALButtonViewModel_ALPrivate.h" 12 | 13 | @interface ALButton (Private) 14 | 15 | @property (nonatomic, readonly) ALTouchGestureRecognizer *touchGestureRecognizer; 16 | 17 | - (void)configureGestureRecognizers NS_REQUIRES_SUPER; 18 | - (void)viewModelDidUpdate:(ALButtonViewModel *)viewModel NS_REQUIRES_SUPER; 19 | 20 | @end 21 | -------------------------------------------------------------------------------- /Source/ALMenuButton.h: -------------------------------------------------------------------------------- 1 | // 2 | // ALMenuButton.h 3 | // ALButtonMenu 4 | // 5 | // Copyright © 2016 Anthony Lobianco. All rights reserved. 6 | // 7 | 8 | #import "ALButton.h" 9 | 10 | NS_ASSUME_NONNULL_BEGIN 11 | 12 | @class ALMenuButton; 13 | 14 | @protocol ALMenuButtonDelegate 15 | 16 | @optional 17 | 18 | - (void)buttonBeganLongPress:(ALMenuButton *)button atLocation:(CGPoint)location; 19 | - (void)buttonLongPressedMoved:(ALMenuButton *)button toLocation:(CGPoint)location; 20 | - (void)buttonEndedLongPress:(ALMenuButton *)button; 21 | 22 | @end 23 | 24 | @interface ALMenuButton : ALButton 25 | 26 | @property (nullable, nonatomic, weak) id delegate; 27 | @property (nonatomic) CGSize shadowOffset; 28 | 29 | - (void)setShadowHidden:(BOOL)hidden animated:(BOOL)animated; 30 | 31 | @end 32 | 33 | NS_ASSUME_NONNULL_END 34 | -------------------------------------------------------------------------------- /Source/ALMenuButton.m: -------------------------------------------------------------------------------- 1 | // 2 | // ALMenuButton.m 3 | // ALButtonMenu 4 | // 5 | // Copyright © 2016 Anthony Lobianco. All rights reserved. 6 | // 7 | 8 | #import "ALMenuButton.h" 9 | #import "ALButton_ALPrivate.h" 10 | 11 | #import "ALUtils.h" 12 | 13 | static NSTimeInterval const kLongPressMinimumDuration = 0.3; 14 | static NSTimeInterval const kButtonShadowAnimationDuration = 0.1; 15 | static CGFloat const kButtonShadowAlpha = 0.4f; 16 | static CGFloat const kButtonShadowRadius = 0.f; 17 | 18 | @interface ALMenuButton () 19 | 20 | @property (nonatomic) UILongPressGestureRecognizer *longPressGestureRecognizer; 21 | @property (nonatomic, getter=isShadowHidden) BOOL shadowHidden; 22 | 23 | @end 24 | 25 | @implementation ALMenuButton 26 | 27 | @dynamic delegate; 28 | 29 | - (instancetype)initWithViewModel:(ALButtonViewModel *)viewModel 30 | { 31 | AL_INIT([super initWithViewModel:viewModel]); 32 | 33 | _shadowHidden = YES; 34 | 35 | self.layer.shadowColor = [UIColor blackColor].CGColor; 36 | self.layer.shadowRadius = kButtonShadowRadius; 37 | 38 | return self; 39 | } 40 | 41 | - (void)viewModelDidUpdate:(ALButtonViewModel *)viewModel 42 | { 43 | [super viewModelDidUpdate:viewModel]; 44 | 45 | [self updateGestureEnabled]; 46 | } 47 | 48 | - (void)layoutSublayersOfLayer:(CALayer *)layer 49 | { 50 | [super layoutSublayersOfLayer:layer]; 51 | 52 | // always keep the shadow path updated here 53 | self.layer.shadowPath = self.scaledMaskPath.CGPath ?: [UIBezierPath bezierPathWithRect:self.bounds].CGPath; 54 | } 55 | 56 | #pragma mark - Shadow 57 | 58 | - (void)setShadowOffset:(CGSize)shadowOffset 59 | { 60 | self.layer.shadowOffset = shadowOffset; 61 | } 62 | 63 | - (void)setShadowHidden:(BOOL)hidden animated:(BOOL)animated 64 | { 65 | if (_shadowHidden == hidden) 66 | { 67 | return; 68 | } 69 | 70 | _shadowHidden = hidden; 71 | 72 | void (^animateShadow)(CGFloat, CGFloat) = ^(CGFloat from, CGFloat to) { 73 | CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"shadowOpacity"]; 74 | animation.fromValue = @(from); 75 | animation.toValue = @(to); 76 | animation.duration = kButtonShadowAnimationDuration; 77 | 78 | [self.layer addAnimation:animation forKey:@"shadowOpacity"]; 79 | }; 80 | 81 | if (_shadowHidden) 82 | { 83 | if (animated) 84 | { 85 | animateShadow(kButtonShadowAlpha, 0.f); 86 | } 87 | 88 | self.layer.shadowOpacity = 0.f; 89 | } 90 | else 91 | { 92 | if (animated) 93 | { 94 | animateShadow(0.f, kButtonShadowAlpha); 95 | } 96 | 97 | self.layer.shadowOpacity = kButtonShadowAlpha; 98 | } 99 | } 100 | 101 | #pragma mark - Gestures 102 | 103 | - (void)configureGestureRecognizers 104 | { 105 | [super configureGestureRecognizers]; 106 | 107 | self.longPressGestureRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleLongPressGesture:)]; 108 | self.longPressGestureRecognizer.minimumPressDuration = kLongPressMinimumDuration; 109 | self.longPressGestureRecognizer.delegate = self; 110 | 111 | [self addGestureRecognizer:self.longPressGestureRecognizer]; 112 | 113 | [self updateGestureEnabled]; 114 | } 115 | 116 | - (void)updateGestureEnabled 117 | { 118 | self.longPressGestureRecognizer.enabled = self.viewModel.canReposition; 119 | } 120 | 121 | - (void)handleLongPressGesture:(UILongPressGestureRecognizer *)sender 122 | { 123 | CGPoint location = [sender locationInView:self]; 124 | 125 | switch (sender.state) 126 | { 127 | case UIGestureRecognizerStateBegan: 128 | { 129 | // cancel the touch gesture so it doesn't fire when our touch ends 130 | [self.touchGestureRecognizer cancel]; 131 | 132 | if ([self.delegate respondsToSelector:@selector(buttonBeganLongPress:atLocation:)]) 133 | { 134 | [self.delegate buttonBeganLongPress:self atLocation:location]; 135 | } 136 | 137 | break; 138 | } 139 | case UIGestureRecognizerStateChanged: 140 | { 141 | if ([self.delegate respondsToSelector:@selector(buttonLongPressedMoved:toLocation:)]) 142 | { 143 | [self.delegate buttonLongPressedMoved:self toLocation:location]; 144 | } 145 | 146 | break; 147 | } 148 | case UIGestureRecognizerStateEnded: 149 | case UIGestureRecognizerStateCancelled: 150 | case UIGestureRecognizerStateFailed: 151 | { 152 | if ([self.delegate respondsToSelector:@selector(buttonEndedLongPress:)]) 153 | { 154 | [self.delegate buttonEndedLongPress:self]; 155 | } 156 | 157 | break; 158 | } 159 | case UIGestureRecognizerStatePossible: 160 | { 161 | // do nothing 162 | break; 163 | } 164 | } 165 | } 166 | 167 | #pragma mark - UIGestureRecognizerDelegate 168 | 169 | - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer 170 | { 171 | if (gestureRecognizer == self.longPressGestureRecognizer) 172 | { 173 | // don't active long press if the touch gesture was cancelled 174 | return self.touchGestureRecognizer.state == UIGestureRecognizerStateBegan; 175 | } 176 | 177 | return YES; 178 | } 179 | 180 | @end 181 | -------------------------------------------------------------------------------- /Source/ALMenuCollectionViewCell.h: -------------------------------------------------------------------------------- 1 | // 2 | // ALMenuCollectionViewCell.h 3 | // ALButtonMenu 4 | // 5 | // Copyright © 2017 Anthony Lobianco. All rights reserved. 6 | // 7 | 8 | #import 9 | 10 | NS_ASSUME_NONNULL_BEGIN 11 | 12 | @protocol ALMenuItem; 13 | 14 | @interface ALMenuCollectionViewCell : UICollectionViewCell 15 | 16 | @property (nullable, nonatomic) UIView *button; 17 | 18 | @end 19 | 20 | NS_ASSUME_NONNULL_END 21 | -------------------------------------------------------------------------------- /Source/ALMenuCollectionViewCell.m: -------------------------------------------------------------------------------- 1 | // 2 | // ALMenuCollectionViewCell.m 3 | // ALButtonMenu 4 | // 5 | // Copyright © 2017 Anthony Lobianco. All rights reserved. 6 | // 7 | 8 | #import "ALMenuCollectionViewCell.h" 9 | 10 | #import "UIView+ALLayout.h" 11 | 12 | #import "ALMenuItem.h" 13 | #import "ALUtils.h" 14 | 15 | @implementation ALMenuCollectionViewCell 16 | 17 | - (instancetype)initWithFrame:(CGRect)frame 18 | { 19 | AL_INIT([super initWithFrame:frame]); 20 | 21 | self.translatesAutoresizingMaskIntoConstraints = NO; 22 | 23 | return self; 24 | } 25 | 26 | - (void)setButton:(UIView *)button 27 | { 28 | if (_button == button) 29 | { 30 | return; 31 | } 32 | 33 | button.translatesAutoresizingMaskIntoConstraints = NO; 34 | 35 | if (button == nil) 36 | { 37 | [_button removeFromSuperview]; 38 | } 39 | else 40 | { 41 | [self.contentView addSubview:button]; 42 | [button al_pinToSuperview]; 43 | } 44 | 45 | _button = button; 46 | } 47 | 48 | @end 49 | -------------------------------------------------------------------------------- /Source/ALMenuCollectionViewLayout.h: -------------------------------------------------------------------------------- 1 | // 2 | // ALMenuCollectionViewLayout.h 3 | // ALButtonMenu 4 | // 5 | // Copyright © 2017 Anthony Lobianco. All rights reserved. 6 | // 7 | 8 | #import 9 | 10 | NS_ASSUME_NONNULL_BEGIN 11 | 12 | @class ALMenuCollectionViewLayout; 13 | 14 | @protocol ALMenuCollectionViewLayoutDelegate 15 | 16 | - (void)menuCollectionViewLayout:(ALMenuCollectionViewLayout *)layout didChangeSize:(CGSize)newSize; 17 | 18 | @end 19 | 20 | @interface ALMenuCollectionViewLayout : UICollectionViewLayout 21 | 22 | @property (nullable, nonatomic, weak) id delegate; 23 | @property (nonatomic) CGSize itemSize; 24 | @property (nonatomic) CGFloat itemSpacing; 25 | @property (nonatomic) NSUInteger numberOfColumns; 26 | 27 | @end 28 | 29 | NS_ASSUME_NONNULL_END 30 | -------------------------------------------------------------------------------- /Source/ALMenuCollectionViewLayout.m: -------------------------------------------------------------------------------- 1 | // 2 | // ALMenuCollectionViewLayout.m 3 | // ALButtonMenu 4 | // 5 | // Copyright © 2017 Anthony Lobianco. All rights reserved. 6 | // 7 | 8 | #import "ALMenuCollectionViewLayout.h" 9 | 10 | #import "ALUtils.h" 11 | 12 | @interface ALMenuCollectionViewLayout () 13 | 14 | @property (nonatomic) NSMutableArray *itemAttributes; 15 | @property (nonatomic) CGSize contentSize; 16 | 17 | @end 18 | 19 | @implementation ALMenuCollectionViewLayout 20 | 21 | - (void)prepareLayout 22 | { 23 | [super prepareLayout]; 24 | 25 | NSUInteger numberOfColumns = self.numberOfColumns; 26 | 27 | NSParameterAssert(numberOfColumns > 0); 28 | 29 | UICollectionView *collectionView = self.collectionView; 30 | id dataSource = collectionView.dataSource; 31 | 32 | NSInteger numberOfItems = [dataSource collectionView:collectionView numberOfItemsInSection:0]; 33 | 34 | NSParameterAssert(numberOfColumns <= numberOfItems); 35 | 36 | NSUInteger numberOfRows = ceil((CGFloat)numberOfItems / numberOfColumns); 37 | 38 | if (UIInterfaceOrientationIsLandscape([[UIApplication sharedApplication] statusBarOrientation])) 39 | { 40 | // flip the rows/columns for landscape orientations 41 | numberOfColumns = numberOfRows; 42 | numberOfRows = self.numberOfColumns; 43 | } 44 | 45 | CGFloat itemWidth = self.itemSize.width; 46 | CGFloat itemHeight = self.itemSize.height; 47 | CGFloat itemSpacing = self.itemSpacing; 48 | 49 | // calculate content size 50 | CGFloat contentWidth = numberOfColumns * itemWidth + ((numberOfColumns - 1) * itemSpacing); 51 | CGFloat contentHeight = numberOfRows * itemHeight + ((numberOfRows - 1) * itemSpacing); 52 | self.contentSize = CGSizeMake(contentWidth, contentHeight); 53 | 54 | // empty attribuets array 55 | self.itemAttributes = [[NSMutableArray alloc] init]; 56 | 57 | // then preload attributes 58 | for (NSUInteger i = 0; i < numberOfItems; ++i) 59 | { 60 | NSIndexPath *indexPath = [NSIndexPath indexPathForItem:i inSection:0]; 61 | UICollectionViewLayoutAttributes *attributes = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath]; 62 | 63 | NSInteger indexOfItemInCurrentRow = (i % numberOfColumns); 64 | NSInteger currentRow = (i / (CGFloat)numberOfColumns); 65 | 66 | CGRect frame; 67 | frame.size.width = itemWidth; 68 | frame.size.height = itemHeight; 69 | frame.origin.x = ((CGFloat)indexOfItemInCurrentRow * itemWidth) + (itemSpacing * (CGFloat)(indexOfItemInCurrentRow)); 70 | frame.origin.y = ((CGFloat)currentRow * itemHeight) + (itemSpacing * (CGFloat)(currentRow)); 71 | attributes.frame = frame; 72 | 73 | self.itemAttributes[i] = attributes; 74 | } 75 | 76 | // then notify the delegate 77 | [self.delegate menuCollectionViewLayout:self didChangeSize:self.contentSize]; 78 | } 79 | 80 | - (CGSize)collectionViewContentSize 81 | { 82 | return self.contentSize; 83 | } 84 | 85 | - (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect 86 | { 87 | return [self.itemAttributes copy]; 88 | } 89 | 90 | - (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath 91 | { 92 | return [[self.itemAttributes copy] objectAtIndex:indexPath.row]; 93 | } 94 | 95 | - (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds 96 | { 97 | return CGRectEqualToRect(newBounds, self.collectionView.bounds) == NO; 98 | } 99 | 100 | - (void)prepareForAnimatedBoundsChange:(CGRect)oldBounds 101 | { 102 | [super prepareForAnimatedBoundsChange:oldBounds]; 103 | [self invalidateLayout]; 104 | } 105 | 106 | @end 107 | -------------------------------------------------------------------------------- /Source/ALMenuItem.h: -------------------------------------------------------------------------------- 1 | // 2 | // ALMenuItem.h 3 | // ALButtonMenu 4 | // 5 | // Copyright © 2016 Anthony Lobianco. All rights reserved. 6 | // 7 | 8 | #import 9 | #import 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | @protocol ALMenuItem; 14 | 15 | @protocol ALMenuItemDelegate 16 | 17 | - (void)buttonWasTapped:(UIView *)button; 18 | 19 | @end 20 | 21 | @protocol ALMenuItem 22 | 23 | @property (nullable, nonatomic, weak) id delegate; 24 | 25 | @end 26 | 27 | NS_ASSUME_NONNULL_END 28 | -------------------------------------------------------------------------------- /Source/ALMenuViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // ALMenuViewController.h 3 | // ALButtonMenu 4 | // 5 | // Copyright © 2016 Anthony Lobianco. All rights reserved. 6 | // 7 | 8 | #import 9 | 10 | #import "ALMenuViewControllerViewModel.h" 11 | 12 | NS_ASSUME_NONNULL_BEGIN 13 | 14 | @protocol ALMenuViewController; 15 | 16 | @protocol ALMenuViewControllerDelegate 17 | 18 | - (void)menuViewController:(UIViewController *)menuViewController didSelectOptionAtIndex:(NSUInteger)index; 19 | 20 | @end 21 | 22 | @protocol ALMenuViewController 23 | 24 | // this delegate should be considered readonly. use it only to forward the protocol messages above. 25 | @property (nullable, nonatomic, weak) id delegate; 26 | 27 | @end 28 | 29 | @interface ALMenuViewController : UIViewController 30 | 31 | @property (nonatomic, readonly) ALMenuViewControllerViewModel *viewModel; 32 | 33 | - (instancetype)initWithViewModel:(ALMenuViewControllerViewModel *)viewModel; 34 | 35 | @end 36 | 37 | NS_ASSUME_NONNULL_END 38 | -------------------------------------------------------------------------------- /Source/ALMenuViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // ALMenuViewController.m 3 | // ALButtonMenu 4 | // 5 | // Copyright © 2016 Anthony Lobianco. All rights reserved. 6 | // 7 | 8 | #import "ALMenuViewController_ALPrivate.h" 9 | 10 | #import "UIView+ALLayout.h" 11 | 12 | #import "ALMenuItem.h" 13 | #import "ALMenuCollectionViewCell.h" 14 | #import "ALMenuCollectionViewLayout.h" 15 | #import "ALNavigationCoordinator.h" 16 | #import "ALUtils.h" 17 | 18 | static NSTimeInterval const kStatusBarAnimationDuration = 0.3; 19 | static NSTimeInterval const kItemAppearanceAnimationDuration = 0.35; 20 | static NSTimeInterval const kItemAppearanceAnimationDelay = 0.08; 21 | static CGFloat const kItemAppearanceAnimationDamping = 0.6f; 22 | static CGFloat const kItemAppearanceAnimationVelocity = 0.2f; 23 | static NSTimeInterval const kContainerViewAppearingAnimationDuration = 0.6; 24 | static NSTimeInterval const kContainerViewDisappearingAnimationDuration = 0.5; 25 | 26 | static NSString * const kCollectionViewCellReuseIdentifier = @"menuCollectionViewCellReuseIdentifier"; 27 | 28 | @interface ALMenuViewController () 29 | < 30 | ALMenuItemDelegate, 31 | ALMenuCollectionViewLayoutDelegate, 32 | UICollectionViewDelegate, 33 | UICollectionViewDataSource 34 | > 35 | 36 | @property (nonatomic) UICollectionView *collectionView; 37 | @property (nonatomic) UIView *containerView; 38 | @property (nonatomic, getter=isStatusBarHidden) BOOL statusBarHidden; 39 | @property (nonatomic, weak) NSLayoutConstraint *containerViewWidthConstraint; 40 | @property (nonatomic, weak) NSLayoutConstraint *containerViewHeightConstraint; 41 | @property (nonatomic, weak) NSLayoutConstraint *containerViewCenterXConstraint; 42 | @property (nonatomic, weak) NSLayoutConstraint *containerViewCenterYConstraint; 43 | @property (nonatomic) CGFloat transformXOffset; 44 | @property (nonatomic) CGFloat transformYOffset; 45 | 46 | @end 47 | 48 | @implementation ALMenuViewController 49 | 50 | @synthesize delegate = _delegate; 51 | 52 | - (instancetype)initWithViewModel:(ALMenuViewControllerViewModel *)viewModel 53 | { 54 | AL_INIT([super init]); 55 | 56 | _viewModel = viewModel; 57 | 58 | return self; 59 | } 60 | 61 | - (void)viewDidLoad 62 | { 63 | [super viewDidLoad]; 64 | 65 | [self buildView]; 66 | } 67 | 68 | - (void)viewWillAppear:(BOOL)animated 69 | { 70 | [super viewWillAppear:animated]; 71 | 72 | if (self.viewModel.shouldHideStatusBar) 73 | { 74 | [self hideStatusBarAnimated:animated]; 75 | } 76 | 77 | [self reloadCollectionView]; 78 | [self handleViewAnimationsForViewWillAppear]; 79 | } 80 | 81 | - (void)viewDidAppear:(BOOL)animated 82 | { 83 | [super viewDidAppear:animated]; 84 | 85 | [self handleViewAnimationsForViewDidAppear]; 86 | } 87 | 88 | - (void)viewWillDisappear:(BOOL)animated 89 | { 90 | [super viewWillDisappear:animated]; 91 | 92 | if (self.viewModel.shouldHideStatusBar) 93 | { 94 | [self showStatusBarAnimated:animated]; 95 | } 96 | 97 | [self handleViewAnimationsForViewWillDisappear]; 98 | } 99 | 100 | - (void)viewDidDisappear:(BOOL)animated 101 | { 102 | [super viewDidDisappear:animated]; 103 | 104 | [self handleViewAnimationsForViewDidDisappear]; 105 | } 106 | 107 | #pragma mark - Private methods 108 | 109 | - (void)navigationCoordinator:(ALNavigationCoordinator *)navigationCoordinator willShowViewControllerFromPoint:(CGPoint)point 110 | { 111 | if (self.viewModel.appearingAnimation != ALMenuViewControllerAppearingAnimationOrigin) 112 | { 113 | return; 114 | } 115 | 116 | // force view to layout so containerView has correct dimensions 117 | [self.view setNeedsLayout]; 118 | [self.view layoutIfNeeded]; 119 | 120 | // convert it to container view's coordinate space, since that's what we'll be animating 121 | CGPoint convertedPoint = [self.view convertPoint:point fromView:navigationCoordinator.navigationController.view]; 122 | CGPoint currentPoint = self.containerView.center; 123 | CGFloat xOffset = convertedPoint.x - currentPoint.x; 124 | CGFloat yOffset = convertedPoint.y - currentPoint.y; 125 | 126 | // apply the points immediately and animate to center of screen 127 | self.containerViewCenterXConstraint.constant = xOffset; 128 | self.containerViewCenterYConstraint.constant = yOffset; 129 | 130 | // apply new constraints immediately 131 | [self.view setNeedsLayout]; 132 | [self.view layoutIfNeeded]; 133 | } 134 | 135 | - (void)navigationCoordinator:(ALNavigationCoordinator *)navigationCoordinator willHideViewControllerFromPoint:(CGPoint)point 136 | { 137 | if (self.viewModel.disappearingAnimation == ALMenuViewControllerDisappearingAnimationNone) 138 | { 139 | return; 140 | } 141 | 142 | // convert it to container view's coordinate space, since that's what we'll be animating 143 | CGPoint convertedPoint = [self.view convertPoint:point fromView:navigationCoordinator.navigationController.view]; 144 | CGPoint currentPoint = self.containerView.center; 145 | CGFloat xOffset = convertedPoint.x - currentPoint.x; 146 | CGFloat yOffset = convertedPoint.y - currentPoint.y; 147 | 148 | // save the point for later to animate away from center of screen 149 | self.transformXOffset = xOffset; 150 | self.transformYOffset = yOffset; 151 | } 152 | 153 | #pragma mark - View layout 154 | 155 | - (void)buildView 156 | { 157 | NSUInteger numberOfItems = [self.viewModel numberOfItems]; 158 | 159 | if (numberOfItems == 0) 160 | { 161 | return; 162 | } 163 | 164 | self.containerView = [[UIView alloc] init]; 165 | self.containerView.backgroundColor = [UIColor clearColor]; 166 | self.containerView.translatesAutoresizingMaskIntoConstraints = NO; 167 | self.containerView.clipsToBounds = NO; 168 | 169 | // setup container view and constrain 170 | [self.view addSubview:self.containerView]; 171 | 172 | self.containerViewCenterXConstraint = [self.containerView.centerXAnchor constraintEqualToAnchor:self.view.centerXAnchor]; 173 | self.containerViewCenterXConstraint.priority = UILayoutPriorityRequired; 174 | self.containerViewCenterXConstraint.active = YES; 175 | 176 | self.containerViewCenterYConstraint = [self.containerView.centerYAnchor constraintEqualToAnchor:self.view.centerYAnchor]; 177 | self.containerViewCenterYConstraint.priority = UILayoutPriorityRequired; 178 | self.containerViewCenterYConstraint.active = YES; 179 | 180 | // setup collection view 181 | ALMenuCollectionViewLayout *layout = [[ALMenuCollectionViewLayout alloc] init]; 182 | layout.delegate = self; 183 | layout.itemSize = self.viewModel.layout.itemSize; 184 | layout.itemSpacing = self.viewModel.layout.itemSpacing; 185 | layout.numberOfColumns = self.viewModel.layout.columns; 186 | 187 | self.collectionView = [[UICollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:layout]; 188 | self.collectionView.backgroundColor = [UIColor clearColor]; 189 | self.collectionView.clipsToBounds = NO; 190 | self.collectionView.translatesAutoresizingMaskIntoConstraints = NO; 191 | self.collectionView.delegate = self; 192 | self.collectionView.dataSource = self; 193 | 194 | [self.collectionView registerClass:[ALMenuCollectionViewCell class] forCellWithReuseIdentifier:kCollectionViewCellReuseIdentifier]; 195 | 196 | [self.containerView addSubview:self.collectionView]; 197 | [self.collectionView al_pinToSuperview]; 198 | 199 | self.containerViewWidthConstraint = [self.containerView.widthAnchor constraintEqualToConstant:self.collectionView.contentSize.width]; 200 | self.containerViewWidthConstraint.active = YES; 201 | 202 | self.containerViewHeightConstraint = [self.containerView.heightAnchor constraintEqualToConstant:self.collectionView.contentSize.height]; 203 | self.containerViewHeightConstraint.active = YES; 204 | } 205 | 206 | - (void)reloadCollectionView 207 | { 208 | [self.collectionView.collectionViewLayout invalidateLayout]; 209 | [self.collectionView reloadData]; 210 | } 211 | 212 | #pragma mark - ALMenuCollectionViewLayoutDelegate 213 | 214 | - (void)menuCollectionViewLayout:(ALMenuCollectionViewLayout *)layout didChangeSize:(CGSize)newSize 215 | { 216 | self.containerViewWidthConstraint.constant = newSize.width; 217 | self.containerViewHeightConstraint.constant = newSize.height; 218 | } 219 | 220 | #pragma mark - UICollectionViewDelegate 221 | 222 | - (BOOL)collectionView:(UICollectionView *)collectionView shouldSelectItemAtIndexPath:(NSIndexPath *)indexPath 223 | { 224 | return NO; 225 | } 226 | 227 | #pragma mark - UICollectionViewDataSource 228 | 229 | - (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath 230 | { 231 | UIView *button = [self.viewModel itemAtIndex:indexPath.row]; 232 | button.delegate = self; 233 | 234 | ALMenuCollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:kCollectionViewCellReuseIdentifier forIndexPath:indexPath]; 235 | cell.button = button; 236 | 237 | return cell; 238 | } 239 | 240 | - (void)collectionView:(UICollectionView *)collectionView didEndDisplayingCell:(UICollectionViewCell *)cell forItemAtIndexPath:(NSIndexPath *)indexPath 241 | { 242 | ALMenuCollectionViewCell *c = (id)cell; 243 | c.button = nil; 244 | } 245 | 246 | - (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section 247 | { 248 | return [self.viewModel numberOfItems]; 249 | } 250 | 251 | #pragma mark - Subview animations 252 | 253 | - (NSArray *> *)viewsForAppearingAnimation 254 | { 255 | ALMenuViewControllerAppearingAnimation appearingAnimation = self.viewModel.appearingAnimation; 256 | NSArray *> *views = nil; 257 | 258 | if (appearingAnimation == ALMenuViewControllerAppearingAnimationIndividual) 259 | { 260 | views = self.viewModel.items; 261 | } 262 | else if (appearingAnimation == ALMenuViewControllerAppearingAnimationOrigin 263 | || appearingAnimation == ALMenuViewControllerAppearingAnimationCenter) 264 | { 265 | views = @[self.containerView]; 266 | } 267 | 268 | return views; 269 | } 270 | 271 | - (void)handleViewAnimationsForViewWillAppear 272 | { 273 | if (self.viewModel.appearingAnimation == ALMenuViewControllerAppearingAnimationNone) 274 | { 275 | return; 276 | } 277 | 278 | NSArray *views = [self viewsForAppearingAnimation]; 279 | 280 | [self prepareForAppearingAnimationForViews:views]; 281 | 282 | if (self.viewModel.appearingAnimation == ALMenuViewControllerAppearingAnimationOrigin) 283 | { 284 | NSParameterAssert(views.count == 1); 285 | 286 | // perform animation immediately for container view if we have an origin item 287 | // appearance animation set 288 | // 289 | NSParameterAssert(self.containerViewCenterXConstraint.constant != 0.f && self.containerViewCenterYConstraint.constant != 0.f); 290 | 291 | self.containerViewCenterXConstraint.constant = 0.f; 292 | self.containerViewCenterYConstraint.constant = 0.f; 293 | 294 | [self 295 | executeAppearingAnimationForViews:views 296 | duration:kContainerViewAppearingAnimationDuration 297 | additionalAnimations:^{ 298 | [self.view layoutIfNeeded]; 299 | } 300 | completion:^{ 301 | [self cleanupAfterAnimations]; 302 | }]; 303 | } 304 | } 305 | 306 | - (void)handleViewAnimationsForViewDidAppear 307 | { 308 | if (self.viewModel.appearingAnimation == ALMenuViewControllerAppearingAnimationNone 309 | || self.viewModel.appearingAnimation == ALMenuViewControllerAppearingAnimationOrigin) 310 | { 311 | return; 312 | } 313 | 314 | // the None animation already took place in handleViewAnimationsForViewWillAppear 315 | [self executeAppearingAnimationForViews:[self viewsForAppearingAnimation] duration:kItemAppearanceAnimationDuration]; 316 | } 317 | 318 | - (void)handleViewAnimationsForViewWillDisappear 319 | { 320 | if (self.viewModel.disappearingAnimation == ALMenuViewControllerDisappearingAnimationNone) 321 | { 322 | return; 323 | } 324 | 325 | [self executeDisappearingAnimationForContainerViewWithCompletion:^(BOOL finished) { 326 | [self cleanupAfterAnimations]; 327 | }]; 328 | } 329 | 330 | - (void)handleViewAnimationsForViewDidDisappear 331 | { 332 | // do nothing 333 | } 334 | 335 | - (void)prepareForAppearingAnimationForViews:(NSArray *)views 336 | { 337 | for (UIView *view in views) 338 | { 339 | view.transform = CGAffineTransformScale(CGAffineTransformIdentity, AL_SCALE_ZERO, AL_SCALE_ZERO); 340 | } 341 | } 342 | 343 | - (void)executeAppearingAnimationForViews:(NSArray *)views duration:(NSTimeInterval)duration 344 | { 345 | [self executeAppearingAnimationForViews:views duration:duration additionalAnimations:nil completion:nil]; 346 | } 347 | 348 | - (void)executeAppearingAnimationForViews:(NSArray *)views duration:(NSTimeInterval)duration additionalAnimations:(void (^) (void))additionalAnimations completion:(void (^) (void))completion 349 | { 350 | NSTimeInterval delay = 0.; 351 | 352 | for (NSInteger i = 0; i < views.count; i++) 353 | { 354 | UIView *view = views[i]; 355 | 356 | void (^actualAnimations)(void) = ^{ 357 | view.transform = CGAffineTransformIdentity; 358 | if (additionalAnimations != nil) 359 | { 360 | additionalAnimations(); 361 | } 362 | }; 363 | 364 | void (^actualCompletion)(BOOL) = ^(BOOL finished) { 365 | if ((i == views.count - 1) && completion != nil) 366 | { 367 | completion(); 368 | } 369 | }; 370 | 371 | [UIView animateWithDuration:duration 372 | delay:delay 373 | usingSpringWithDamping:kItemAppearanceAnimationDamping 374 | initialSpringVelocity:kItemAppearanceAnimationVelocity 375 | options:UIViewAnimationOptionCurveEaseInOut|UIViewAnimationOptionBeginFromCurrentState 376 | animations:actualAnimations 377 | completion:actualCompletion]; 378 | 379 | delay += kItemAppearanceAnimationDelay; 380 | } 381 | } 382 | 383 | - (void)executeDisappearingAnimationForContainerViewWithCompletion:(void (^) (BOOL))completion 384 | { 385 | NSParameterAssert(self.transformXOffset != 0.f && self.transformYOffset != 0.f); 386 | 387 | self.containerViewCenterXConstraint.constant = self.transformXOffset; 388 | self.containerViewCenterYConstraint.constant = self.transformYOffset; 389 | 390 | [UIView animateWithDuration:kContainerViewDisappearingAnimationDuration 391 | delay:0. 392 | options:UIViewAnimationOptionCurveEaseIn|UIViewAnimationOptionBeginFromCurrentState 393 | animations:^{ 394 | // animate constraints 395 | [self.view layoutIfNeeded]; 396 | 397 | // and animate transform 398 | self.containerView.transform = CGAffineTransformScale(CGAffineTransformIdentity, AL_SCALE_ZERO, AL_SCALE_ZERO); 399 | } 400 | completion:completion]; 401 | } 402 | 403 | - (void)cleanupAfterAnimations 404 | { 405 | // reset transform 406 | self.containerView.transform = CGAffineTransformIdentity; 407 | 408 | // then reset the constraint points 409 | self.containerViewCenterXConstraint.constant = 0.f; 410 | self.containerViewCenterYConstraint.constant = 0.f; 411 | 412 | self.transformXOffset = 0.f; 413 | self.transformYOffset = 0.f; 414 | } 415 | 416 | #pragma mark - Status bar 417 | 418 | - (BOOL)prefersStatusBarHidden 419 | { 420 | if (self.viewModel.shouldHideStatusBar == NO) 421 | { 422 | return NO; 423 | } 424 | 425 | return [self isStatusBarHidden]; 426 | } 427 | 428 | - (UIStatusBarAnimation)preferredStatusBarUpdateAnimation 429 | { 430 | return UIStatusBarAnimationSlide; 431 | } 432 | 433 | - (void)showStatusBarAnimated:(BOOL)animated 434 | { 435 | [self toggleStatusBar:YES animated:animated]; 436 | } 437 | 438 | - (void)hideStatusBarAnimated:(BOOL)animated 439 | { 440 | [self toggleStatusBar:NO animated:animated]; 441 | } 442 | 443 | - (void)toggleStatusBar:(BOOL)show animated:(BOOL)animated 444 | { 445 | NSParameterAssert(self.viewModel.shouldHideStatusBar); 446 | self.statusBarHidden = show == NO; 447 | 448 | [UIView animateWithDuration:animated ? kStatusBarAnimationDuration : 0. animations:^{ 449 | [self setNeedsStatusBarAppearanceUpdate]; 450 | }]; 451 | } 452 | 453 | #pragma mark - ALMenuItemDelegate 454 | 455 | - (void)buttonWasTapped:(UIView *)button 456 | { 457 | [self.delegate menuViewController:self didSelectOptionAtIndex:[self.viewModel indexOfItem:button]]; 458 | } 459 | 460 | #pragma mark - Rotation 461 | 462 | - (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id)coordinator 463 | { 464 | [self reloadCollectionView]; 465 | } 466 | 467 | @end 468 | -------------------------------------------------------------------------------- /Source/ALMenuViewControllerViewModel.h: -------------------------------------------------------------------------------- 1 | // 2 | // ALMenuViewControllerViewModel.h 3 | // ALButtonMenu 4 | // 5 | // Copyright © 2016 Anthony Lobianco. All rights reserved. 6 | // 7 | 8 | #import 9 | #import 10 | 11 | #import "ALMenuItem.h" 12 | 13 | NS_ASSUME_NONNULL_BEGIN 14 | 15 | struct ALMenuViewControllerLayout 16 | { 17 | NSUInteger columns; 18 | CGSize itemSize; 19 | CGFloat itemSpacing; 20 | }; 21 | 22 | typedef struct ALMenuViewControllerLayout ALMenuViewControllerLayout; 23 | 24 | typedef NS_ENUM(NSUInteger, ALMenuViewControllerAppearingAnimation) 25 | { 26 | // no animation 27 | ALMenuViewControllerAppearingAnimationNone = 0, 28 | 29 | // the menu items will simultaneously expand outward from the menu button's center. 30 | ALMenuViewControllerAppearingAnimationOrigin, 31 | 32 | // the menu items will appear simultaneously from the center of the screen. 33 | ALMenuViewControllerAppearingAnimationCenter, 34 | 35 | // the menu items will appear individually. 36 | ALMenuViewControllerAppearingAnimationIndividual, 37 | }; 38 | 39 | typedef NS_ENUM(NSUInteger, ALMenuViewControllerDisappearingAnimation) 40 | { 41 | // no animation 42 | ALMenuViewControllerDisappearingAnimationNone = 0, 43 | 44 | // the menu items will contract simultaneously towards the menu button's center. 45 | ALMenuViewControllerDisappearingAnimationOrigin, 46 | }; 47 | 48 | @protocol ALMenuViewControllerViewModel 49 | 50 | /** 51 | The animation type for presenting the menu view controller when the menu is being shown. 52 | 53 | Default is ALMenuViewControllerAppearingAnimationIndividual. 54 | */ 55 | @property (nonatomic) ALMenuViewControllerAppearingAnimation appearingAnimation; 56 | 57 | /** 58 | The animation type for dismissing the menu view controller when the menu is hiding. 59 | 60 | Default is ALMenuViewControllerDisappearingAnimationOrigin. 61 | */ 62 | @property (nonatomic) ALMenuViewControllerDisappearingAnimation disappearingAnimation; 63 | 64 | 65 | /** 66 | Should the status bar be hidden when the menu is shown. 67 | 68 | Default is YES. 69 | */ 70 | @property (nonatomic) BOOL shouldHideStatusBar; 71 | 72 | - (NSUInteger)numberOfItems; 73 | - (nullable UIView *)itemAtIndex:(NSUInteger)index; 74 | - (NSUInteger)indexOfItem:(UIView *)item; 75 | 76 | @end 77 | 78 | @interface ALMenuViewControllerViewModel : NSObject 79 | 80 | @property (nullable, nonatomic, readonly) NSArray *> *items; 81 | 82 | /** 83 | The layout details to use for the menu items. 84 | */ 85 | @property (nonatomic, readonly) ALMenuViewControllerLayout layout; 86 | 87 | - (instancetype)init NS_UNAVAILABLE; 88 | - (instancetype)initWithItems:(nullable NSArray *> *)items layout:(ALMenuViewControllerLayout)layout NS_DESIGNATED_INITIALIZER; 89 | 90 | @end 91 | 92 | NS_ASSUME_NONNULL_END 93 | -------------------------------------------------------------------------------- /Source/ALMenuViewControllerViewModel.m: -------------------------------------------------------------------------------- 1 | // 2 | // ALMenuViewControllerViewModel.m 3 | // ALButtonMenu 4 | // 5 | // Copyright © 2016 Anthony Lobianco. All rights reserved. 6 | // 7 | 8 | #import "ALMenuViewControllerViewModel.h" 9 | 10 | #import "ALButton.h" 11 | #import "ALButtonViewModel.h" 12 | #import "ALUtils.h" 13 | 14 | @interface ALMenuViewControllerViewModel () 15 | 16 | @property (nonatomic) NSArray *> *items; 17 | 18 | @end 19 | 20 | @implementation ALMenuViewControllerViewModel 21 | 22 | @synthesize appearingAnimation = _appearingAnimation; 23 | @synthesize disappearingAnimation = _disappearingAnimation; 24 | @synthesize shouldHideStatusBar = _shouldHideStatusBar; 25 | 26 | - (instancetype)initWithItems:(NSArray *> *)items layout:(ALMenuViewControllerLayout)layout 27 | { 28 | AL_INIT([super init]); 29 | 30 | _items = items; 31 | _layout = layout; 32 | 33 | [self configureDefaults]; 34 | 35 | return self; 36 | } 37 | 38 | - (void)configureDefaults 39 | { 40 | _appearingAnimation = ALMenuViewControllerAppearingAnimationIndividual; 41 | _disappearingAnimation = ALMenuViewControllerDisappearingAnimationOrigin; 42 | _shouldHideStatusBar = YES; 43 | } 44 | 45 | #pragma mark - ALMenuViewControllerViewModel 46 | 47 | - (NSUInteger)numberOfItems 48 | { 49 | return self.items.count; 50 | } 51 | 52 | - (UIView *)itemAtIndex:(NSUInteger)index 53 | { 54 | if (index >= [self numberOfItems]) 55 | { 56 | return nil; 57 | } 58 | 59 | return self.items[index]; 60 | } 61 | 62 | - (NSUInteger)indexOfItem:(UIView *)item 63 | { 64 | return [self.items indexOfObject:item]; 65 | } 66 | 67 | @end 68 | -------------------------------------------------------------------------------- /Source/ALMenuViewController_ALPrivate.h: -------------------------------------------------------------------------------- 1 | // 2 | // ALMenuViewController_ALPrivate.h 3 | // ALButtonMenu 4 | // 5 | // Copyright © 2016 Anthony Lobianco. All rights reserved. 6 | // 7 | 8 | #import "ALMenuViewController.h" 9 | 10 | @class ALNavigationCoordinator; 11 | 12 | @interface ALMenuViewController () 13 | 14 | // point will be in navigation controller's coordinate space 15 | - (void)navigationCoordinator:(ALNavigationCoordinator *)navigationCoordinator willShowViewControllerFromPoint:(CGPoint)point; 16 | - (void)navigationCoordinator:(ALNavigationCoordinator *)navigationCoordinator willHideViewControllerFromPoint:(CGPoint)point; 17 | 18 | @end 19 | -------------------------------------------------------------------------------- /Source/ALNavigationCoordinator+ALSnapping.h: -------------------------------------------------------------------------------- 1 | // 2 | // ALNavigationCoordinator+ALSnapping.h 3 | // ALButtonMenu 4 | // 5 | // Copyright © 2016 Anthony Lobianco. All rights reserved. 6 | // 7 | 8 | #import "ALNavigationCoordinator.h" 9 | 10 | @interface ALNavigationCoordinator (ALSnapping) 11 | 12 | // these methods take into account only the available snapLocations provided by view model 13 | - (ALNavigationCoordinatorSnapLocation)al_snapLocationNearestPoint:(CGPoint)point; 14 | - (CGPoint)al_pointForSnapLocation:(ALNavigationCoordinatorSnapLocation)location; 15 | 16 | @end 17 | -------------------------------------------------------------------------------- /Source/ALNavigationCoordinator+ALSnapping.m: -------------------------------------------------------------------------------- 1 | // 2 | // ALNavigationCoordinator+ALSnapping.m 3 | // ALButtonMenu 4 | // 5 | // Copyright © 2016 Anthony Lobianco. All rights reserved. 6 | // 7 | 8 | #import "ALNavigationCoordinator+ALSnapping.h" 9 | 10 | #import "ALUtils.h" 11 | 12 | @interface ALSnapLocationWrapper : NSObject 13 | 14 | @property (nonatomic) CGPoint point; 15 | @property (nonatomic) ALNavigationCoordinatorSnapLocation snapLocation; 16 | 17 | @end 18 | 19 | @implementation ALSnapLocationWrapper 20 | 21 | @end 22 | 23 | @implementation ALNavigationCoordinator (ALSnapping) 24 | 25 | - (ALNavigationCoordinatorSnapLocation)al_snapLocationNearestPoint:(CGPoint)point 26 | { 27 | ALNavigationCoordinatorSnapLocation snapLocations = self.viewModel.snapLocations; 28 | 29 | if (snapLocations == ALNavigationCoordinatorSnapLocationNone) 30 | { 31 | return ALNavigationCoordinatorSnapLocationNone; 32 | } 33 | 34 | NSArray *wrappers = [self al_wrappersForSnapLocations:snapLocations]; 35 | 36 | ALNavigationCoordinatorSnapLocation location = ALNavigationCoordinatorSnapLocationNone; 37 | CGFloat shortestDistance = CGFLOAT_MAX; 38 | 39 | for (ALSnapLocationWrapper *wrapper in wrappers) 40 | { 41 | CGPoint locationPoint = wrapper.point; 42 | CGFloat distance = hypot(point.x - locationPoint.x, point.y - locationPoint.y); 43 | if (distance < shortestDistance) 44 | { 45 | shortestDistance = distance; 46 | location = wrapper.snapLocation; 47 | } 48 | } 49 | 50 | return location; 51 | } 52 | 53 | - (CGPoint)al_pointForSnapLocation:(ALNavigationCoordinatorSnapLocation)location 54 | { 55 | ALNavigationCoordinatorSnapLocation snapLocations __unused = self.viewModel.snapLocations; 56 | NSParameterAssert(AL_OPTION_SET(snapLocations, location)); 57 | 58 | NSArray *wrappers = [self al_wrappersForSnapLocations:location]; 59 | NSParameterAssert(wrappers.count == 1); 60 | 61 | return wrappers[0].point; 62 | } 63 | 64 | #pragma mark - Internal methods 65 | 66 | - (NSArray *)al_wrappersForSnapLocations:(ALNavigationCoordinatorSnapLocation)locations 67 | { 68 | if (locations == ALNavigationCoordinatorSnapLocationNone) 69 | { 70 | return @[[self al_wrapperForLocation:ALNavigationCoordinatorSnapLocationNone withPoint:CGPointZero]]; 71 | } 72 | 73 | CGRect frame = self.navigationController.view.frame; 74 | 75 | CGFloat statusBarHeight = CGRectGetHeight([UIApplication sharedApplication].statusBarFrame); 76 | frame.origin.y += statusBarHeight; 77 | frame.size.height -= statusBarHeight; 78 | 79 | if ([self.navigationController isNavigationBarHidden] == NO) 80 | { 81 | CGFloat navBarHeight = CGRectGetHeight(self.navigationController.navigationBar.frame); 82 | frame.origin.y += navBarHeight; 83 | frame.size.height -= navBarHeight; 84 | } 85 | 86 | CGFloat snapPadding = self.viewModel.snapPadding; 87 | 88 | CGPoint topLeft = CGPointMake(CGRectGetMinX(frame) + snapPadding, CGRectGetMinY(frame) + snapPadding); 89 | CGPoint top = CGPointMake(CGRectGetMidX(frame), CGRectGetMinY(frame) + snapPadding); 90 | CGPoint topRight = CGPointMake(CGRectGetMaxX(frame) - snapPadding, CGRectGetMinY(frame) + snapPadding); 91 | CGPoint right = CGPointMake(CGRectGetMaxX(frame) - snapPadding, CGRectGetMidY(frame)); 92 | CGPoint bottomRight = CGPointMake(CGRectGetMaxX(frame) - snapPadding, CGRectGetMaxY(frame) - snapPadding); 93 | CGPoint bottom = CGPointMake(CGRectGetMidX(frame), CGRectGetMaxY(frame) - snapPadding); 94 | CGPoint bottomLeft = CGPointMake(CGRectGetMinX(frame) + snapPadding, CGRectGetMaxY(frame) - snapPadding); 95 | CGPoint left = CGPointMake(CGRectGetMinX(frame) + snapPadding, CGRectGetMidY(frame)); 96 | CGPoint middle = CGPointMake(CGRectGetMidX(frame), CGRectGetMidY(frame)); 97 | 98 | NSMutableArray *points = [[NSMutableArray alloc] init]; 99 | 100 | void (^addPointIfNecessary)(ALNavigationCoordinatorSnapLocation, CGPoint) = ^(ALNavigationCoordinatorSnapLocation location, CGPoint point) { 101 | 102 | if (AL_OPTION_SET(locations, location)) 103 | { 104 | [points addObject:[self al_wrapperForLocation:location withPoint:point]]; 105 | } 106 | 107 | }; 108 | 109 | addPointIfNecessary(ALNavigationCoordinatorSnapLocationTopLeft, topLeft); 110 | addPointIfNecessary(ALNavigationCoordinatorSnapLocationTop, top); 111 | addPointIfNecessary(ALNavigationCoordinatorSnapLocationTopRight, topRight); 112 | addPointIfNecessary(ALNavigationCoordinatorSnapLocationRight, right); 113 | addPointIfNecessary(ALNavigationCoordinatorSnapLocationBottomRight, bottomRight); 114 | addPointIfNecessary(ALNavigationCoordinatorSnapLocationBottom, bottom); 115 | addPointIfNecessary(ALNavigationCoordinatorSnapLocationBottomLeft, bottomLeft); 116 | addPointIfNecessary(ALNavigationCoordinatorSnapLocationLeft, left); 117 | addPointIfNecessary(ALNavigationCoordinatorSnapLocationMiddle, middle); 118 | 119 | return [points copy]; 120 | } 121 | 122 | - (ALSnapLocationWrapper *)al_wrapperForLocation:(ALNavigationCoordinatorSnapLocation)location withPoint:(CGPoint)point 123 | { 124 | ALSnapLocationWrapper *wrapper = [[ALSnapLocationWrapper alloc] init]; 125 | wrapper.snapLocation = location; 126 | wrapper.point = point; 127 | return wrapper; 128 | } 129 | 130 | @end 131 | -------------------------------------------------------------------------------- /Source/ALNavigationCoordinator.h: -------------------------------------------------------------------------------- 1 | // 2 | // ALNavigationCoordinator.h 3 | // ALButtonMenu 4 | // 5 | // Copyright © 2016 Anthony Lobianco. All rights reserved. 6 | // 7 | 8 | #import 9 | 10 | #import "ALMenuViewController.h" 11 | #import "ALNavigationCoordinatorViewModel.h" 12 | 13 | NS_ASSUME_NONNULL_BEGIN 14 | 15 | @class ALNavigationCoordinator; 16 | 17 | @protocol ALNavigationCoordinatorDelegate 18 | 19 | - (UIViewController *)navigationCoordinator:(ALNavigationCoordinator *)navigationCoordinator viewControllerForMenuItemAtIndex:(NSUInteger)index; 20 | 21 | @end 22 | 23 | @interface ALNavigationCoordinator : NSObject 24 | 25 | /** 26 | This delegate will also forward UINavigationControllerDelegate callbacks for the navigationController from init. 27 | */ 28 | @property (nullable, nonatomic, weak) id delegate; 29 | 30 | /** 31 | Can be an instance of [ALMenuViewController class] or any custom subclass that inherits from . 32 | */ 33 | @property (nonatomic, readonly) UIViewController *menuViewController; 34 | 35 | /** 36 | You will not be able to register as this object's delegate because ALNavigationCoordinator will hijack it, 37 | but if register as id delegate above then you will also receive forwarded 38 | UINavigationControllerDelegate callbacks (if you decide to implement them). Add this object as a child 39 | view controller of your own view controller. 40 | */ 41 | @property (nonatomic, readonly) UINavigationController *navigationController; 42 | 43 | /** 44 | The viewModel object provided in init. 45 | */ 46 | @property (nonatomic, readonly) ALNavigationCoordinatorViewModel *viewModel; 47 | 48 | - (instancetype)initWithViewModel:(ALNavigationCoordinatorViewModel *)viewModel menuViewController:(UIViewController *)menuViewController rootViewController:(UIViewController *)rootViewController; 49 | 50 | - (instancetype)initWithViewModel:(ALNavigationCoordinatorViewModel *)viewModel menuViewController:(UIViewController *)menuViewController navigationController:(UINavigationController *)navigationController; 51 | 52 | /** 53 | These methods should be called from the respective methods of navigationController's parent view controller. 54 | */ 55 | - (void)viewDidLoad; 56 | - (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id)coordinator; 57 | 58 | @end 59 | 60 | NS_ASSUME_NONNULL_END 61 | -------------------------------------------------------------------------------- /Source/ALNavigationCoordinator.m: -------------------------------------------------------------------------------- 1 | // 2 | // ALNavigationCoordinator.m 3 | // ALButtonMenu 4 | // 5 | // Copyright © 2016 Anthony Lobianco. All rights reserved. 6 | // 7 | 8 | #import "ALNavigationCoordinator.h" 9 | 10 | #import "ALNavigationCoordinator+ALSnapping.h" 11 | #import "UIButton+ALBounce.h" 12 | 13 | #import "ALAnimationTransitionDelegate.h" 14 | #import "ALMenuButton.h" 15 | #import "ALButtonViewModel_ALPrivate.h" 16 | #import "ALMenuViewController_ALPrivate.h" 17 | #import "ALMenuViewControllerViewModel.h" 18 | #import "ALNavigationCoordinatorViewModel.h" 19 | #import "ALUtils.h" 20 | 21 | static NSTimeInterval const kButtonDisappearAnimationDuration = 0.2; 22 | static NSTimeInterval const kButtonReappearAnimationDuration = 0.3; 23 | static NSTimeInterval const kButtonScaleAnimationDuration = 0.3; 24 | static NSTimeInterval const kButtonScaleAndMoveAnimationDuration = 0.4; 25 | static CGFloat const kButtonMinimumScaleFactor = 0.88f; 26 | static CGFloat const kButtonExpandScaleFactor = 1.4f; 27 | static CGFloat const kButtonMaximumShadowOffset = 5.f; 28 | 29 | @interface ALNavigationCoordinator () 30 | < 31 | ALMenuButtonDelegate, 32 | ALAnimationTransitionDelegateDelegate, 33 | ALMenuViewControllerDelegate 34 | > 35 | 36 | @property (nonatomic) ALMenuButton *button; 37 | @property (nonatomic) ALNavigationCoordinatorSnapLocation currentButtonSnapLocation; 38 | @property (nonatomic) CGPoint previousButtonMovePoint; 39 | @property (nonatomic, getter=isShowingMenu) BOOL showingMenu; 40 | @property (nonatomic) ALAnimationTransitionDelegate *transitionDelegate; 41 | @property (nonatomic) BOOL wasNavigationBarShowing; 42 | 43 | @end 44 | 45 | @implementation ALNavigationCoordinator 46 | 47 | - (instancetype)initWithViewModel:(ALNavigationCoordinatorViewModel *)viewModel menuViewController:(nonnull UIViewController *)menuViewController rootViewController:(nonnull UIViewController *)rootViewController 48 | { 49 | UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:rootViewController]; 50 | navigationController.navigationBarHidden = YES; 51 | 52 | AL_INIT([self initWithViewModel:viewModel menuViewController:menuViewController navigationController:navigationController]); 53 | 54 | return self; 55 | } 56 | 57 | - (instancetype)initWithViewModel:(ALNavigationCoordinatorViewModel *)viewModel menuViewController:(UIViewController *)menuViewController navigationController:(UINavigationController *)navigationController 58 | { 59 | AL_INIT([super init]); 60 | 61 | _viewModel = viewModel; 62 | 63 | _menuViewController = menuViewController; 64 | _menuViewController.delegate = self; 65 | 66 | _navigationController = navigationController; 67 | 68 | return self; 69 | } 70 | 71 | - (void)viewDidLoad 72 | { 73 | self.menuViewController.view.backgroundColor = self.viewModel.buttonDefaultColor; 74 | 75 | // i made the effort to use auto layout on this button (for the sizing and dragging) 76 | // but there's no point. sizing with AL works fine, but since the button can potentially 77 | // have a mask applied, any animations to the size constraints would not propogate 78 | // to the button's layer's mask. regular transform animations, on the other hand, 79 | // handle it perfectly. and positioning with AL works fine as well, but offers no 80 | // benefit over manual positioning (even during a screen rotation, which was the reason 81 | // i investigated AL in the first place. the button can be dragged around by the user, 82 | // and as such can never have constant positioning constraints applied to it [you may 83 | // be thinking "just update the .constant property" but that won't properly translate 84 | // to the new view dimensions after a screen rotation]). 85 | // 86 | ALButtonViewModel *buttonViewModel = [[ALButtonViewModel alloc] init]; 87 | buttonViewModel.bouncesOnTouchUp = NO; 88 | buttonViewModel.color = self.viewModel.buttonDefaultColor; 89 | buttonViewModel.canReposition = self.viewModel.buttonCanBeRepositioned; 90 | buttonViewModel.maskPath = self.viewModel.buttonPath; 91 | buttonViewModel.touchArea = self.viewModel.buttonTouchArea; 92 | 93 | self.button = [[ALMenuButton alloc] initWithViewModel:buttonViewModel]; 94 | self.button.frame = CGRectMake(0.f, 0.f, self.viewModel.buttonSize.width, self.viewModel.buttonSize.height); 95 | self.button.delegate = self; 96 | 97 | CGPoint buttonDefaultPosition = [self buttonDefaultPosition]; 98 | self.currentButtonSnapLocation = self.viewModel.initialSnapLocation; 99 | 100 | CGPoint buttonCenter = CGPointZero; 101 | 102 | if (self.currentButtonSnapLocation == ALNavigationCoordinatorSnapLocationNone) 103 | { 104 | buttonCenter = buttonDefaultPosition; 105 | } 106 | else 107 | { 108 | buttonCenter = [self al_pointForSnapLocation:self.currentButtonSnapLocation]; 109 | } 110 | 111 | [self updateButtonCenterWithPoint:buttonCenter]; 112 | 113 | // add button 114 | [self.navigationController.view addSubview:self.button]; 115 | 116 | // set an initial shape layer 117 | NSParameterAssert(self.transitionDelegate != nil); 118 | self.transitionDelegate.initialShapeLayer = [self initialShapeLayerForTransition]; 119 | } 120 | 121 | #pragma mark - Setters 122 | 123 | - (void)setDelegate:(id)delegate 124 | { 125 | if (_delegate == delegate) 126 | { 127 | return; 128 | } 129 | 130 | _delegate = delegate; 131 | 132 | [self configureTransitionDelegateWithNavigationControllerDelegate:delegate]; 133 | } 134 | 135 | #pragma mark - Delegate setup 136 | 137 | - (void)configureTransitionDelegateWithNavigationControllerDelegate:(id)navigationControllerDelegate 138 | { 139 | NSParameterAssert(self.viewModel != nil && self.menuViewController != nil); 140 | 141 | self.navigationController.delegate = nil; 142 | self.transitionDelegate.delegate = nil; 143 | self.transitionDelegate = [[ALAnimationTransitionDelegate alloc] initWithNavigationControllerDelegate:navigationControllerDelegate]; 144 | self.transitionDelegate.delegate = self; 145 | self.transitionDelegate.appearingAnimation = self.viewModel.rootControllerAppearingAnimation; 146 | self.transitionDelegate.disappearingAnimation = self.viewModel.rootControllerDisappearingAnimation; 147 | self.transitionDelegate.viewControllerClassForCustomAnimations = [self.menuViewController class]; 148 | self.navigationController.delegate = self.transitionDelegate; 149 | } 150 | 151 | #pragma mark - ALMenuViewControllerDelegate 152 | 153 | - (void)menuViewController:(UIViewController *)menuViewController didSelectOptionAtIndex:(NSUInteger)index 154 | { 155 | // ask for the next VC from delegate 156 | UIViewController *viewController = [self.delegate navigationCoordinator:self viewControllerForMenuItemAtIndex:index]; 157 | 158 | // hide the menu 159 | [self toggleMenuWithCompletionHandler:^{ 160 | 161 | // then push the VC on the stack 162 | if ([self.navigationController.viewControllers containsObject:viewController]) 163 | { 164 | [self.navigationController popToViewController:viewController animated:YES]; 165 | } 166 | else 167 | { 168 | [self.navigationController pushViewController:viewController animated:YES]; 169 | } 170 | 171 | }]; 172 | } 173 | 174 | #pragma mark - ALAnimationTransitionDelegateDelegate 175 | 176 | - (void)transitionDelegate:(ALAnimationTransitionDelegate *)transitionDelegate didShowViewController:(UIViewController *)viewController 177 | { 178 | if ([self isShowingMenu] == NO) 179 | { 180 | return; 181 | } 182 | 183 | // do something? 184 | } 185 | 186 | - (void)transitionDelegate:(ALAnimationTransitionDelegate *)transitionDelegate didHideViewController:(UIViewController *)viewController 187 | { 188 | // now that the animation controller finished the mask transform animation, 189 | // we can restore the button's identity 190 | // 191 | self.button.userInteractionEnabled = YES; 192 | self.button.transform = CGAffineTransformIdentity; 193 | } 194 | 195 | #pragma mark - ALMenuItemDelegate 196 | 197 | - (void)buttonWasTapped:(UIView *)button 198 | { 199 | self.button.userInteractionEnabled = NO; 200 | 201 | [self toggleMenu]; 202 | } 203 | 204 | #pragma mark - ALMenuButtonDelegate 205 | 206 | - (void)buttonBeganLongPress:(ALMenuButton *)button atLocation:(CGPoint)location 207 | { 208 | self.previousButtonMovePoint = [self.navigationController.view convertPoint:location fromView:button]; 209 | 210 | if (self.viewModel.buttonShouldShowShadowDuringReposition) 211 | { 212 | [button setShadowHidden:NO animated:YES]; 213 | } 214 | 215 | [button al_transformToSize:kButtonExpandScaleFactor duration:kButtonScaleAnimationDuration]; 216 | } 217 | 218 | - (void)buttonLongPressedMoved:(ALMenuButton *)button toLocation:(CGPoint)location 219 | { 220 | CGPoint convertedPoint = [self.navigationController.view convertPoint:location fromView:self.button]; 221 | CGPoint center = self.button.center; 222 | center.x += convertedPoint.x - self.previousButtonMovePoint.x; 223 | center.y += convertedPoint.y - self.previousButtonMovePoint.y; 224 | 225 | // update button center 226 | [self updateButtonCenterWithPoint:center]; 227 | 228 | // then save point for next pass 229 | self.previousButtonMovePoint = convertedPoint; 230 | } 231 | 232 | - (void)buttonEndedLongPress:(ALMenuButton *)button 233 | { 234 | self.button.userInteractionEnabled = NO; 235 | 236 | CGPoint currentPosition = self.button.center; 237 | CGPoint finalPosition = currentPosition; 238 | 239 | self.currentButtonSnapLocation = [self al_snapLocationNearestPoint:currentPosition]; 240 | 241 | if (self.currentButtonSnapLocation != ALNavigationCoordinatorSnapLocationNone) 242 | { 243 | finalPosition = [self al_pointForSnapLocation:self.currentButtonSnapLocation]; 244 | } 245 | 246 | void (^completion)(BOOL) = ^(BOOL finished) { 247 | self.transitionDelegate.initialShapeLayer = [self initialShapeLayerForTransition]; 248 | self.button.userInteractionEnabled = YES; 249 | }; 250 | 251 | NSTimeInterval duration = 0; 252 | 253 | void (^animations)(void) = nil; 254 | 255 | if (CGPointEqualToPoint(self.button.center, finalPosition) == NO) 256 | { 257 | duration = kButtonScaleAndMoveAnimationDuration; 258 | 259 | animations = ^{ 260 | [self updateButtonCenterWithPoint:finalPosition]; 261 | }; 262 | } 263 | else 264 | { 265 | duration = kButtonScaleAnimationDuration; 266 | } 267 | 268 | [button al_restoreWithDuration:duration alongsideAnimations:animations completion:completion]; 269 | 270 | if (self.viewModel.buttonShouldShowShadowDuringReposition) 271 | { 272 | [button setShadowHidden:YES animated:YES]; 273 | } 274 | } 275 | 276 | #pragma mark - Navigation stack 277 | 278 | - (void)willShowViewController:(UIViewController *)viewController 279 | { 280 | NSParameterAssert(viewController == self.menuViewController); 281 | 282 | if ([self isShowingMenu]) 283 | { 284 | return; 285 | } 286 | 287 | if ([self.menuViewController respondsToSelector:@selector(navigationCoordinator:willShowViewControllerFromPoint:)]) 288 | { 289 | // give the menu VC a point to use as the anchor point for 290 | [(id)self.menuViewController navigationCoordinator:self willShowViewControllerFromPoint:[self initialShapeLayerForTransition].position]; 291 | } 292 | 293 | self.button.transform = CGAffineTransformScale(CGAffineTransformIdentity, AL_SCALE_ZERO, AL_SCALE_ZERO); 294 | self.button.viewModel.color = self.viewModel.buttonActiveColor; 295 | 296 | [self.button al_restoreWithDuration:kButtonReappearAnimationDuration alongsideAnimations:nil completion:^(BOOL finished) { 297 | self.button.userInteractionEnabled = YES; 298 | }]; 299 | } 300 | 301 | - (void)willHideViewController:(UIViewController *)viewController 302 | { 303 | NSParameterAssert(viewController == self.menuViewController); 304 | 305 | if ([self isShowingMenu] == NO) 306 | { 307 | return; 308 | } 309 | 310 | if ([self.menuViewController respondsToSelector:@selector(navigationCoordinator:willHideViewControllerFromPoint:)]) 311 | { 312 | // give the menu VC a point to use as the anchor point for 313 | [(id)self.menuViewController navigationCoordinator:self willHideViewControllerFromPoint:[self initialShapeLayerForTransition].position]; 314 | } 315 | 316 | // disable springiness for this one animation, and re-enable it after 317 | self.button.springy = NO; 318 | 319 | void (^animations)(void) = ^{ 320 | self.button.viewModel.color = self.viewModel.buttonDefaultColor; 321 | }; 322 | 323 | void (^completion)(BOOL) = ^(BOOL finished) { 324 | // limit the size that the button can shrink to during the animation controller's animation 325 | self.button.transform = CGAffineTransformScale(CGAffineTransformIdentity, kButtonMinimumScaleFactor, kButtonMinimumScaleFactor); 326 | self.button.springy = YES; 327 | }; 328 | 329 | [self.button al_transformToSize:AL_SCALE_ZERO duration:kButtonDisappearAnimationDuration alongsideAnimations:animations completion:completion]; 330 | } 331 | 332 | - (void)toggleMenu 333 | { 334 | [self toggleMenuWithCompletionHandler:nil]; 335 | } 336 | 337 | - (void)toggleMenuWithCompletionHandler:(void (^) (void))completion 338 | { 339 | if ([self isShowingMenu]) 340 | { 341 | [self willHideViewController:self.menuViewController]; 342 | 343 | [CATransaction begin]; 344 | [CATransaction setCompletionBlock:completion]; 345 | [self.navigationController popViewControllerAnimated:YES]; 346 | [CATransaction commit]; 347 | 348 | if (self.wasNavigationBarShowing) 349 | { 350 | [self.navigationController setNavigationBarHidden:NO animated:YES]; 351 | } 352 | } 353 | else 354 | { 355 | self.wasNavigationBarShowing = self.navigationController.navigationBarHidden == NO; 356 | 357 | if (self.wasNavigationBarShowing) 358 | { 359 | [self.navigationController setNavigationBarHidden:YES animated:YES]; 360 | } 361 | 362 | [self willShowViewController:self.menuViewController]; 363 | 364 | [CATransaction begin]; 365 | [CATransaction setCompletionBlock:completion]; 366 | [self.navigationController pushViewController:self.menuViewController animated:YES]; 367 | [CATransaction commit]; 368 | } 369 | 370 | self.showingMenu = [self isShowingMenu] == NO; 371 | } 372 | 373 | #pragma mark - Button helpers 374 | 375 | - (CGPoint)buttonDefaultPosition 376 | { 377 | return [self al_pointForSnapLocation:self.viewModel.initialSnapLocation]; 378 | } 379 | 380 | - (CAShapeLayer *)initialShapeLayerForTransition 381 | { 382 | NSParameterAssert(self.button != nil); 383 | 384 | // calculate a shape layer in this view's coordinate space 385 | 386 | CAShapeLayer *shapeLayer = [[CAShapeLayer alloc] init]; 387 | shapeLayer.bounds = self.button.frame; 388 | shapeLayer.position = self.button.center; 389 | 390 | if (self.button.scaledMaskPath) 391 | { 392 | CGAffineTransform transform = CGAffineTransformMakeTranslation(self.button.frame.origin.x, self.button.frame.origin.y); 393 | CGPathRef path = CGPathCreateCopyByTransformingPath(self.button.scaledMaskPath.CGPath, &transform); 394 | shapeLayer.path = path; 395 | CGPathRelease(path); 396 | } 397 | else 398 | { 399 | shapeLayer.path = [UIBezierPath bezierPathWithRect:self.button.frame].CGPath; 400 | } 401 | 402 | return shapeLayer; 403 | } 404 | 405 | - (CGSize)offsetForMenuButtonShadowWithButtonCenter:(CGPoint)buttonCenter 406 | { 407 | // offsets are from button center to view center 408 | 409 | CGRect viewBounds = self.navigationController.view.bounds; 410 | CGPoint viewCenter = CGPointMake(CGRectGetMidX(viewBounds), CGRectGetMidY(viewBounds)); 411 | CGSize viewSize = viewBounds.size; 412 | 413 | CGFloat xCenterToEdge = viewSize.width - viewCenter.x; 414 | CGFloat yCenterToEdge = viewSize.height - viewCenter.y; 415 | 416 | CGFloat xFromButtonCenter = buttonCenter.x - viewCenter.x; 417 | CGFloat yFromButtonCenter = buttonCenter.y - viewCenter.y; 418 | 419 | CGFloat xOffset = (kButtonMaximumShadowOffset * (xFromButtonCenter / xCenterToEdge)); 420 | CGFloat yOffset = (kButtonMaximumShadowOffset * (yFromButtonCenter / yCenterToEdge)); 421 | 422 | return CGSizeMake(xOffset, yOffset); 423 | } 424 | 425 | - (void)updateButtonCenterWithPoint:(CGPoint)point 426 | { 427 | self.button.center = point; 428 | self.button.shadowOffset = [self offsetForMenuButtonShadowWithButtonCenter:point]; 429 | } 430 | 431 | #pragma mark - Rotation 432 | 433 | - (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id)coordinator 434 | { 435 | [coordinator 436 | animateAlongsideTransition:^(id _Nonnull context) { 437 | // update button position. our view will have correct dimensions in this scope 438 | CGPoint buttonCenter = CGPointZero; 439 | 440 | if (self.currentButtonSnapLocation != ALNavigationCoordinatorSnapLocationNone) 441 | { 442 | buttonCenter = [self al_pointForSnapLocation:self.currentButtonSnapLocation]; 443 | } 444 | else 445 | { 446 | buttonCenter = [self buttonDefaultPosition]; 447 | } 448 | 449 | [self updateButtonCenterWithPoint:buttonCenter]; 450 | } 451 | completion:^(id _Nonnull context) { 452 | // when rotation finishes, update shape layer 453 | self.transitionDelegate.initialShapeLayer = [self initialShapeLayerForTransition]; 454 | }]; 455 | } 456 | 457 | @end 458 | -------------------------------------------------------------------------------- /Source/ALNavigationCoordinatorViewModel.h: -------------------------------------------------------------------------------- 1 | // 2 | // ALNavigationCoordinatorViewModel.h 3 | // ALButtonMenu 4 | // 5 | // Copyright © 2016 Anthony Lobianco. All rights reserved. 6 | // 7 | 8 | #import 9 | #import 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | typedef NS_ENUM(NSUInteger, ALNavigationCoordinatorAnimation) 14 | { 15 | // no animation 16 | ALNavigationCoordinatorAnimationNone = 0, 17 | 18 | // the nav controller will expand away from the button's center when showing the menu and 19 | // contract towards the button's center when hiding the menu. 20 | // 21 | ALNavigationCoordinatorAnimationOrigin, 22 | }; 23 | 24 | typedef NS_OPTIONS(NSInteger, ALNavigationCoordinatorSnapLocation) 25 | { 26 | // none 27 | ALNavigationCoordinatorSnapLocationNone = 0, 28 | 29 | // individual positions 30 | ALNavigationCoordinatorSnapLocationTopLeft = (1UL << 0), 31 | ALNavigationCoordinatorSnapLocationTop = (1UL << 1), 32 | ALNavigationCoordinatorSnapLocationTopRight = (1UL << 2), 33 | ALNavigationCoordinatorSnapLocationRight = (1UL << 3), 34 | ALNavigationCoordinatorSnapLocationBottomRight = (1UL << 4), 35 | ALNavigationCoordinatorSnapLocationBottom = (1UL << 5), 36 | ALNavigationCoordinatorSnapLocationBottomLeft = (1UL << 6), 37 | ALNavigationCoordinatorSnapLocationLeft = (1UL << 7), 38 | ALNavigationCoordinatorSnapLocationMiddle = (1UL << 8), 39 | 40 | // edges 41 | ALNavigationCoordinatorSnapLocationAllTop = (ALNavigationCoordinatorSnapLocationTopLeft 42 | | ALNavigationCoordinatorSnapLocationTop 43 | | ALNavigationCoordinatorSnapLocationTopRight), 44 | 45 | ALNavigationCoordinatorSnapLocationAllRight = (ALNavigationCoordinatorSnapLocationTopRight 46 | | ALNavigationCoordinatorSnapLocationRight 47 | | ALNavigationCoordinatorSnapLocationBottomRight), 48 | 49 | ALNavigationCoordinatorSnapLocationAllBottom = (ALNavigationCoordinatorSnapLocationBottomRight 50 | | ALNavigationCoordinatorSnapLocationBottom 51 | | ALNavigationCoordinatorSnapLocationBottomLeft), 52 | 53 | ALNavigationCoordinatorSnapLocationAllLeft = (ALNavigationCoordinatorSnapLocationBottomLeft 54 | | ALNavigationCoordinatorSnapLocationLeft 55 | | ALNavigationCoordinatorSnapLocationTopLeft), 56 | 57 | // corners 58 | ALNavigationCoordinatorSnapLocationCorners = (ALNavigationCoordinatorSnapLocationTopLeft 59 | | ALNavigationCoordinatorSnapLocationTopRight 60 | | ALNavigationCoordinatorSnapLocationBottomRight 61 | | ALNavigationCoordinatorSnapLocationBottomLeft), 62 | 63 | // all 64 | ALNavigationCoordinatorSnapLocationAll = (ALNavigationCoordinatorSnapLocationTopLeft 65 | | ALNavigationCoordinatorSnapLocationTop 66 | | ALNavigationCoordinatorSnapLocationTopRight 67 | | ALNavigationCoordinatorSnapLocationRight 68 | | ALNavigationCoordinatorSnapLocationBottomRight 69 | | ALNavigationCoordinatorSnapLocationBottom 70 | | ALNavigationCoordinatorSnapLocationBottomLeft 71 | | ALNavigationCoordinatorSnapLocationLeft 72 | | ALNavigationCoordinatorSnapLocationMiddle) 73 | }; 74 | 75 | @protocol ALNavigationCoordinatorViewModelDataSource 76 | 77 | /** 78 | Data source method to use in conjunction with ALNavigationCoordinatorModeReplace. 79 | 80 | @param index The index of the button that was tapped. 81 | 82 | @return The view controller that corresponds with that index. It will be set as the new root controller. 83 | */ 84 | - (UIViewController *)viewControllerForItemAtIndex:(NSUInteger)index; 85 | 86 | @end 87 | 88 | @interface ALNavigationCoordinatorViewModel : NSObject 89 | 90 | /** 91 | The animation type for presenting the root controller when the menu is dismissed. 92 | 93 | Default is ALNavigationCoordinatorAnimationOrigin. 94 | */ 95 | @property (nonatomic) ALNavigationCoordinatorAnimation rootControllerAppearingAnimation; 96 | 97 | /** 98 | The animation type for dismissing the root controller when the menu is presented. 99 | 100 | Default is ALNavigationCoordinatorAnimationOrigin. 101 | */ 102 | @property (nonatomic) ALNavigationCoordinatorAnimation rootControllerDisappearingAnimation; 103 | 104 | /** 105 | The menu button's color when the menu is shown. 106 | 107 | Default is a pale white color. 108 | */ 109 | @property (nonatomic) UIColor *buttonActiveColor; 110 | 111 | /** 112 | If YES, the menu button can be repositioned via long press. See snappingLocations below. 113 | 114 | Default is NO. 115 | */ 116 | @property (nonatomic) BOOL buttonCanBeRepositioned; 117 | 118 | /** 119 | The menu button's color when the menu is not shown. 120 | 121 | Default is a dark grey color. 122 | */ 123 | @property (nonatomic) UIColor *buttonDefaultColor; 124 | 125 | /** 126 | A bezier path can be specified to give the menu button a custom shape. It will be scaled down to 127 | proportionally fit inside the button. 128 | 129 | Default is a circle path. 130 | */ 131 | @property (nullable, nonatomic) UIBezierPath *buttonPath; 132 | 133 | /** 134 | Show a drop shadow while menu button is being dragged around (if buttonCanBeRepositioned is YES). 135 | 136 | Default is YES. 137 | */ 138 | @property (nonatomic) BOOL buttonShouldShowShadowDuringReposition; 139 | 140 | /** 141 | The button's size. 142 | 143 | Default is { 50.f, 50.f }. 144 | */ 145 | @property (nonatomic) CGSize buttonSize; 146 | 147 | /** 148 | The number of points outside the button's bounds that will still register as a touch on the button. 149 | 150 | Default is 10.f. 151 | */ 152 | @property (nonatomic) CGFloat buttonTouchArea; 153 | 154 | /** 155 | The data source that will provide view controller information in conjunction with ALNavigationCoordinatorModeNotify. 156 | */ 157 | @property (nullable, nonatomic, weak) id dataSource; 158 | 159 | /** 160 | The snap location that the button will initially begin at. If value is ALNavigationCoordinatorSnapLocationNone, 161 | the menu button's default position will be { x = snapPadding, y = snapPadding }. 162 | 163 | Default is ALNavigationCoordinatorSnapLocationBottomLeft. 164 | */ 165 | @property (nonatomic) ALNavigationCoordinatorSnapLocation initialSnapLocation; 166 | 167 | /** 168 | The individual locations that the menu button will snap to if buttonCanBeRepositioned is YES. 169 | 170 | Default is ALNavigationCoordinatorSnapLocationCorners. 171 | */ 172 | @property (nonatomic) ALNavigationCoordinatorSnapLocation snapLocations; 173 | 174 | /** 175 | The padding from the edge of the screen to the center of the button. 176 | 177 | Default is 60.f. 178 | */ 179 | @property (nonatomic) CGFloat snapPadding; 180 | 181 | @end 182 | 183 | NS_ASSUME_NONNULL_END 184 | -------------------------------------------------------------------------------- /Source/ALNavigationCoordinatorViewModel.m: -------------------------------------------------------------------------------- 1 | // 2 | // ALNavigationCoordinatorViewModel.m 3 | // ALButtonMenu 4 | // 5 | // Copyright © 2016 Anthony Lobianco. All rights reserved. 6 | // 7 | 8 | #import "ALNavigationCoordinatorViewModel.h" 9 | 10 | #import "ALUtils.h" 11 | 12 | static NSInteger const kDefaultSnappingLocations = ALNavigationCoordinatorSnapLocationCorners; 13 | static CGFloat const kDefaultSnapPadding = 60.f; 14 | static CGFloat const kDefaultButtonSize = 50.f; 15 | static CGFloat const kDefaultButtonTouchArea = 10.f; 16 | static CGFloat const kButtonActiveWhiteValue = 0.9f; 17 | static CGFloat const kButtonDefaultWhiteValue = 0.1f; 18 | 19 | @implementation ALNavigationCoordinatorViewModel 20 | 21 | - (instancetype)init 22 | { 23 | AL_INIT([super init]); 24 | 25 | [self configureDefaults]; 26 | 27 | return self; 28 | } 29 | 30 | - (void)configureDefaults 31 | { 32 | _buttonActiveColor = [UIColor colorWithWhite:kButtonActiveWhiteValue alpha:1.f]; 33 | _buttonCanBeRepositioned = NO; 34 | _buttonDefaultColor = [UIColor colorWithWhite:kButtonDefaultWhiteValue alpha:1.f]; 35 | _buttonShouldShowShadowDuringReposition = YES; 36 | _buttonSize = CGSizeMake(kDefaultButtonSize, kDefaultButtonSize); 37 | _buttonPath = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(0.f, 0.f, _buttonSize.width, _buttonSize.height)]; 38 | _buttonTouchArea = kDefaultButtonTouchArea; 39 | _initialSnapLocation = ALNavigationCoordinatorSnapLocationBottomLeft; 40 | _rootControllerAppearingAnimation = ALNavigationCoordinatorAnimationOrigin; 41 | _rootControllerDisappearingAnimation = ALNavigationCoordinatorAnimationOrigin; 42 | _snapLocations = kDefaultSnappingLocations; 43 | _snapPadding = kDefaultSnapPadding; 44 | } 45 | 46 | @end 47 | -------------------------------------------------------------------------------- /Source/ALTouchGestureRecognizer.h: -------------------------------------------------------------------------------- 1 | // 2 | // ALTouchGestureRecognizer.h 3 | // ALButtonMenu 4 | // 5 | // Copyright © 2016 Anthony Lobianco. All rights reserved. 6 | // 7 | 8 | #import 9 | 10 | @interface ALTouchGestureRecognizer : UIGestureRecognizer 11 | 12 | - (void)cancel; 13 | 14 | @end 15 | -------------------------------------------------------------------------------- /Source/ALTouchGestureRecognizer.m: -------------------------------------------------------------------------------- 1 | // 2 | // ALTouchGestureRecognizer.m 3 | // ALButtonMenu 4 | // 5 | // Copyright © 2016 Anthony Lobianco. All rights reserved. 6 | // 7 | 8 | #import "ALTouchGestureRecognizer.h" 9 | #import 10 | 11 | static CGFloat const kMoveDistanceUntilFailure = 10.f; 12 | 13 | @interface ALTouchGestureRecognizer () 14 | 15 | @property (nonatomic) CGPoint touchPoint; 16 | 17 | @end 18 | 19 | @implementation ALTouchGestureRecognizer 20 | 21 | #pragma mark - UIControl 22 | 23 | - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event 24 | { 25 | [super touchesBegan:touches withEvent:event]; 26 | 27 | if (self.state != UIGestureRecognizerStatePossible) 28 | { 29 | return; 30 | } 31 | 32 | if (touches.count > 1) 33 | { 34 | self.state = UIGestureRecognizerStateFailed; 35 | return; 36 | } 37 | 38 | CGPoint touchPoint = [[touches anyObject] locationInView:self.view]; 39 | self.touchPoint = touchPoint; 40 | 41 | self.state = UIGestureRecognizerStateBegan; 42 | } 43 | 44 | - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event 45 | { 46 | [super touchesMoved:touches withEvent:event]; 47 | 48 | CGPoint touchPoint = [[touches anyObject] locationInView:self.view]; 49 | CGFloat distance = hypot(self.touchPoint.x - touchPoint.x, self.touchPoint.y - touchPoint.y); 50 | 51 | self.state = distance > kMoveDistanceUntilFailure ? UIGestureRecognizerStateCancelled : UIGestureRecognizerStateChanged; 52 | } 53 | 54 | - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event 55 | { 56 | [super touchesEnded:touches withEvent:event]; 57 | 58 | self.state = UIGestureRecognizerStateEnded; 59 | } 60 | 61 | - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event 62 | { 63 | [super touchesCancelled:touches withEvent:event]; 64 | 65 | self.state = UIGestureRecognizerStateCancelled; 66 | } 67 | 68 | - (void)reset 69 | { 70 | [super reset]; 71 | 72 | self.touchPoint = CGPointZero; 73 | self.state = UIGestureRecognizerStatePossible; 74 | } 75 | 76 | #pragma mark - Public methods 77 | 78 | - (UIGestureRecognizerState)state 79 | { 80 | UIGestureRecognizerState state = [super state]; 81 | return state == UIGestureRecognizerStateChanged ? UIGestureRecognizerStateBegan : state; 82 | } 83 | 84 | - (void)cancel 85 | { 86 | self.enabled = NO; 87 | self.enabled = YES; 88 | } 89 | 90 | #pragma mark - UIGestureRecognizerSubclass 91 | 92 | - (BOOL)canBePreventedByGestureRecognizer:(UIGestureRecognizer *)preventingGestureRecognizer 93 | { 94 | return YES; 95 | } 96 | 97 | - (BOOL)canPreventGestureRecognizer:(UIGestureRecognizer *)preventedGestureRecognizer 98 | { 99 | return NO; 100 | } 101 | 102 | @end 103 | -------------------------------------------------------------------------------- /Source/ALUtils.h: -------------------------------------------------------------------------------- 1 | // 2 | // ALUtils.h 3 | // ALButtonMenu 4 | // 5 | // Copyright © 2016 Anthony Lobianco. All rights reserved. 6 | // 7 | 8 | #ifndef ALUtils_h 9 | #define ALUtils_h 10 | 11 | #define AL_SCALE_ZERO 0.001 12 | #define AL_INIT(_init) if ((self = _init) == nil) return nil 13 | #define AL_OPTION_SET(_options, _option) ((_options & _option) == _option) 14 | 15 | #endif /* ALUtils_h */ 16 | -------------------------------------------------------------------------------- /Source/UIBezierPath+ALScaling.h: -------------------------------------------------------------------------------- 1 | // 2 | // UIBezierPath+ALScaling.h 3 | // ALButtonMenu 4 | // 5 | // Copyright © 2016 Anthony Lobianco. All rights reserved. 6 | // 7 | 8 | #import 9 | 10 | @interface UIBezierPath (ALScaling) 11 | 12 | - (UIBezierPath *)al_scaledToRect:(CGRect)rect; 13 | 14 | @end 15 | -------------------------------------------------------------------------------- /Source/UIBezierPath+ALScaling.m: -------------------------------------------------------------------------------- 1 | // 2 | // UIBezierPath+ALScaling.m 3 | // ALButtonMenu 4 | // 5 | // Copyright © 2016 Anthony Lobianco. All rights reserved. 6 | // 7 | 8 | #import "UIBezierPath+ALScaling.h" 9 | 10 | @implementation UIBezierPath (ALScaling) 11 | 12 | - (UIBezierPath *)al_scaledToRect:(CGRect)rect 13 | { 14 | // with help from http://stackoverflow.com/a/15936794 - thanks David! 15 | 16 | CGRect boundingBox = CGPathGetBoundingBox(self.CGPath); 17 | 18 | CGFloat boundingBoxAspectRatio = CGRectGetWidth(boundingBox) / CGRectGetHeight(boundingBox); 19 | CGFloat viewAspectRatio = CGRectGetWidth(rect) / CGRectGetHeight(rect); 20 | 21 | CGFloat scale = boundingBoxAspectRatio > viewAspectRatio ? CGRectGetWidth(rect) / CGRectGetWidth(boundingBox) : CGRectGetHeight(rect) / CGRectGetHeight(boundingBox); 22 | 23 | // scale and translate the path 24 | CGAffineTransform transform = CGAffineTransformScale(CGAffineTransformIdentity, scale, scale); 25 | transform = CGAffineTransformTranslate(transform, -(CGRectGetMinX(boundingBox)), -(CGRectGetMinY(boundingBox))); 26 | 27 | // center the path in the rect 28 | CGSize scaledSize = CGSizeApplyAffineTransform(boundingBox.size, CGAffineTransformMakeScale(scale, scale)); 29 | CGSize centerOffset = CGSizeMake((CGRectGetWidth(rect) - scaledSize.width) / (scale * 2.f), (CGRectGetHeight(rect) - scaledSize.height) / (scale * 2.f)); 30 | transform = CGAffineTransformTranslate(transform, centerOffset.width, centerOffset.height); 31 | 32 | // apply the transformation 33 | CGPathRef transformedPath = CGPathCreateCopyByTransformingPath(self.CGPath, &transform); 34 | UIBezierPath *scaledPath = [UIBezierPath bezierPathWithCGPath:transformedPath]; 35 | CGPathRelease(transformedPath); 36 | 37 | return scaledPath; 38 | } 39 | 40 | @end 41 | -------------------------------------------------------------------------------- /Source/UIButton+ALBounce.h: -------------------------------------------------------------------------------- 1 | // 2 | // UIButton+ALBounce.h 3 | // ALButtonMenu 4 | // 5 | // Copyright © 2016 Anthony Lobianco. All rights reserved. 6 | // 7 | 8 | #import 9 | 10 | @interface UIButton (ALBounce) 11 | 12 | @property (nonatomic, getter=isSpringy) BOOL springy; 13 | 14 | - (void)al_transformToSize:(CGFloat)size duration:(NSTimeInterval)duration; 15 | - (void)al_transformToSize:(CGFloat)size duration:(NSTimeInterval)duration alongsideAnimations:(void (^) (void))animations; 16 | - (void)al_transformToSize:(CGFloat)size duration:(NSTimeInterval)duration alongsideAnimations:(void (^) (void))animations completion:(void (^) (BOOL))completion; 17 | 18 | - (void)al_restoreWithDuration:(NSTimeInterval)duration; 19 | - (void)al_restoreWithDuration:(NSTimeInterval)duration alongsideAnimations:(void (^) (void))animations; 20 | - (void)al_restoreWithDuration:(NSTimeInterval)duration alongsideAnimations:(void (^) (void))animations completion:(void (^) (BOOL))completion; 21 | 22 | @end 23 | -------------------------------------------------------------------------------- /Source/UIButton+ALBounce.m: -------------------------------------------------------------------------------- 1 | // 2 | // UIButton+ALBounce.m 3 | // ALButtonMenu 4 | // 5 | // Copyright © 2016 Anthony Lobianco. All rights reserved. 6 | // 7 | 8 | #import "UIButton+ALBounce.h" 9 | 10 | #import 11 | 12 | static void * kSpringyPropertyKey = &kSpringyPropertyKey; 13 | 14 | static CGFloat const kButtonAnimationDamping = 0.6f; 15 | static CGFloat const kButtonAnimationVelocity = 0.2f; 16 | static UIViewAnimationOptions kButtonAnimationOptions = UIViewAnimationOptionAllowUserInteraction|UIViewAnimationOptionBeginFromCurrentState; 17 | 18 | @implementation UIButton (ALBounce) 19 | 20 | #pragma mark - Properties 21 | 22 | - (BOOL)isSpringy 23 | { 24 | id obj = objc_getAssociatedObject(self, kSpringyPropertyKey); 25 | return obj != nil ? [obj boolValue] : YES; 26 | } 27 | 28 | - (void)setSpringy:(BOOL)springy 29 | { 30 | objc_setAssociatedObject(self, kSpringyPropertyKey, @(springy), OBJC_ASSOCIATION_RETAIN_NONATOMIC); 31 | } 32 | 33 | #pragma mark - Public methods 34 | 35 | - (void)al_transformToSize:(CGFloat)size duration:(NSTimeInterval)duration 36 | { 37 | [self al_transformToSize:size duration:duration alongsideAnimations:nil]; 38 | } 39 | 40 | - (void)al_transformToSize:(CGFloat)size duration:(NSTimeInterval)duration alongsideAnimations:(void (^)(void))animations 41 | { 42 | [self al_transformToSize:size duration:duration alongsideAnimations:animations completion:nil]; 43 | } 44 | 45 | - (void)al_transformToSize:(CGFloat)size duration:(NSTimeInterval)duration alongsideAnimations:(void (^)(void))animations completion:(void (^)(BOOL))completion 46 | { 47 | void (^actualAnimations)(void) = ^{ 48 | self.transform = CGAffineTransformScale(CGAffineTransformIdentity, size, size); 49 | if (animations) 50 | { 51 | animations(); 52 | } 53 | }; 54 | 55 | [self animateWithAnimations:actualAnimations 56 | duration:duration 57 | completion:completion]; 58 | } 59 | 60 | - (void)al_restoreWithDuration:(NSTimeInterval)duration 61 | { 62 | [self al_restoreWithDuration:duration alongsideAnimations:nil]; 63 | } 64 | 65 | - (void)al_restoreWithDuration:(NSTimeInterval)duration alongsideAnimations:(void (^)(void))animations 66 | { 67 | [self al_restoreWithDuration:duration alongsideAnimations:animations completion:nil]; 68 | } 69 | 70 | - (void)al_restoreWithDuration:(NSTimeInterval)duration alongsideAnimations:(void (^)(void))animations completion:(void (^)(BOOL))completion 71 | { 72 | void (^actualAnimations)(void) = ^{ 73 | self.transform = CGAffineTransformIdentity; 74 | if (animations) 75 | { 76 | animations(); 77 | } 78 | }; 79 | 80 | [self animateWithAnimations:actualAnimations duration:duration completion:completion]; 81 | } 82 | 83 | #pragma mark - Internal methods 84 | 85 | - (void)animateWithAnimations:(void (^) (void))animations duration:(NSTimeInterval)duration completion:(void (^) (BOOL))completion 86 | { 87 | if ([self isSpringy]) 88 | { 89 | [UIView animateWithDuration:duration 90 | delay:0. 91 | usingSpringWithDamping:kButtonAnimationDamping 92 | initialSpringVelocity:kButtonAnimationVelocity 93 | options:kButtonAnimationOptions 94 | animations:animations 95 | completion:completion]; 96 | } 97 | else 98 | { 99 | [UIView animateWithDuration:duration 100 | delay:0. 101 | options:kButtonAnimationOptions 102 | animations:animations 103 | completion:completion]; 104 | } 105 | } 106 | 107 | @end 108 | -------------------------------------------------------------------------------- /Source/UIView+ALLayout.h: -------------------------------------------------------------------------------- 1 | // 2 | // UIView+ALLayout.h 3 | // ALButtonMenu 4 | // 5 | // Copyright © 2016 Anthony Lobianco. All rights reserved. 6 | // 7 | 8 | #import 9 | 10 | @interface UIView (ALLayout) 11 | 12 | #pragma mark - Positioning 13 | 14 | - (void)al_adjustPositionForNewAnchorPoint:(CGPoint)anchorPoint; 15 | 16 | #pragma mark - Auto Layout 17 | 18 | - (void)al_pinToSuperview; 19 | - (void)al_pinToView:(UIView *)view; 20 | 21 | @end 22 | -------------------------------------------------------------------------------- /Source/UIView+ALLayout.m: -------------------------------------------------------------------------------- 1 | // 2 | // UIView+ALLayout.m 3 | // ALButtonMenu 4 | // 5 | // Copyright © 2016 Anthony Lobianco. All rights reserved. 6 | // 7 | 8 | #import "UIView+ALLayout.h" 9 | 10 | @implementation UIView (ALLayout) 11 | 12 | #pragma mark - Positioning 13 | 14 | - (void)al_adjustPositionForNewAnchorPoint:(CGPoint)anchorPoint 15 | { 16 | CGPoint currentPosition = CGPointMake(CGRectGetWidth(self.bounds) * self.layer.anchorPoint.x, CGRectGetHeight(self.bounds) * self.layer.anchorPoint.y); 17 | currentPosition = CGPointApplyAffineTransform(currentPosition, self.transform); 18 | CGPoint newPosition = CGPointMake(CGRectGetWidth(self.bounds) * anchorPoint.x, CGRectGetHeight(self.bounds) * anchorPoint.y); 19 | newPosition = CGPointApplyAffineTransform(newPosition, self.transform); 20 | 21 | CGPoint translatedPosition = self.layer.position; 22 | 23 | translatedPosition.x -= currentPosition.x; 24 | translatedPosition.x += newPosition.x; 25 | 26 | translatedPosition.y -= currentPosition.y; 27 | translatedPosition.y += newPosition.y; 28 | 29 | self.layer.position = translatedPosition; 30 | self.layer.anchorPoint = anchorPoint; 31 | } 32 | 33 | #pragma mark - Auto Layout 34 | 35 | - (void)al_pinToSuperview 36 | { 37 | [self al_pinToView:self.superview]; 38 | } 39 | 40 | - (void)al_pinToView:(UIView *)view 41 | { 42 | NSParameterAssert(view != nil); 43 | 44 | // forgetting to set this is the bane of my professional career 45 | self.translatesAutoresizingMaskIntoConstraints = NO; 46 | 47 | // then constrain 48 | [self.leadingAnchor constraintEqualToAnchor:view.leadingAnchor].active = YES; 49 | [self.trailingAnchor constraintEqualToAnchor:view.trailingAnchor].active = YES; 50 | [self.topAnchor constraintEqualToAnchor:view.topAnchor].active = YES; 51 | [self.bottomAnchor constraintEqualToAnchor:view.bottomAnchor].active = YES; 52 | } 53 | 54 | @end 55 | --------------------------------------------------------------------------------