├── .circleci └── config.yml ├── .github ├── CONTRIBUTING.md ├── FUNDING.yml ├── ISSUE_TEMPLATE.md └── ISSUE_TEMPLATE │ ├── bug-report.md │ └── feature-request.md ├── .gitignore ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── Example ├── AppDelegate.swift ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ └── Contents.json │ ├── Contents.json │ ├── background.imageset │ │ ├── Contents.json │ │ └── stars.png │ └── saturn.imageset │ │ ├── Contents.json │ │ └── saturn.png ├── Base.lproj │ └── LaunchScreen.xib ├── Info.plist ├── Launch Screen.storyboard ├── Main.storyboard ├── MainViewController.swift ├── PresentedViewController.swift └── SideMenuTableViewController.swift ├── ExampleTests ├── ExampleTests.swift └── Info.plist ├── LICENSE ├── Package.swift ├── Pod └── Classes │ ├── Deprecations.swift │ ├── Extensions.swift │ ├── Initializable.swift │ ├── Print.swift │ ├── Protected.swift │ ├── SideMenuAnimationController.swift │ ├── SideMenuInteractionController.swift │ ├── SideMenuManager.swift │ ├── SideMenuNavigationController.swift │ ├── SideMenuPresentationController.swift │ ├── SideMenuPresentationStyle.swift │ ├── SideMenuPushCoordinator.swift │ ├── SideMenuTransitionController.swift │ └── UITableViewVibrantCell.swift ├── Podfile ├── Podfile.lock ├── Pods ├── Local Podspecs │ └── SideMenu.podspec.json ├── Manifest.lock ├── Pods.xcodeproj │ └── project.pbxproj └── Target Support Files │ ├── Pods-Example │ ├── Info.plist │ ├── Pods-Example-Info.plist │ ├── Pods-Example-acknowledgements.markdown │ ├── Pods-Example-acknowledgements.plist │ ├── Pods-Example-dummy.m │ ├── Pods-Example-frameworks.sh │ ├── Pods-Example-resources.sh │ ├── Pods-Example-umbrella.h │ ├── Pods-Example.debug.xcconfig │ ├── Pods-Example.modulemap │ └── Pods-Example.release.xcconfig │ └── SideMenu │ ├── Info.plist │ ├── SideMenu-Info.plist │ ├── SideMenu-dummy.m │ ├── SideMenu-prefix.pch │ ├── SideMenu-umbrella.h │ ├── SideMenu.modulemap │ └── SideMenu.xcconfig ├── README.md ├── SideMenu.podspec ├── SideMenu.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ └── contents.xcworkspacedata └── xcshareddata │ └── xcschemes │ ├── SideMenu Example.xcscheme │ └── SideMenu.xcscheme ├── SideMenu.xcworkspace ├── contents.xcworkspacedata └── xcshareddata │ ├── IDEWorkspaceChecks.plist │ └── WorkspaceSettings.xcsettings ├── SideMenu ├── Info.plist ├── SideMenu.h └── module.modulemap └── etc ├── Dissolve.gif ├── InOut.gif ├── Screenshot1.png ├── Screenshot2.png ├── Screenshot3.png ├── SlideIn.gif └── SlideOut.gif /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # iOS CircleCI 2.0 configuration file 2 | # 3 | # Check https://circleci.com/docs/2.0/ios-migrating-from-1-2/ for more details 4 | # 5 | version: 2 6 | jobs: 7 | build: 8 | 9 | # Specify the Xcode version to use 10 | macos: 11 | xcode: "10.3.0" 12 | 13 | steps: 14 | - checkout 15 | 16 | # Install CocoaPods 17 | - run: 18 | name: Install CocoaPods 19 | command: pod install 20 | 21 | # Build the app and run tests 22 | - run: 23 | name: Build and run tests 24 | command: fastlane scan 25 | environment: 26 | SCAN_DEVICE: iPhone Xʀ 27 | SCAN_SCHEME: SideMenu Example 28 | 29 | # Collect XML test results data to show in the UI, 30 | # and save the same XML files under test-results folder 31 | # in the Artifacts tab 32 | - store_test_results: 33 | path: test_output/report.xml 34 | - store_artifacts: 35 | path: /tmp/test-results 36 | destination: scan-test-results 37 | - store_artifacts: 38 | path: ~/Library/Logs/scan 39 | destination: scan-logs 40 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution Guidelines 2 | 3 | Thank you for your interest in SideMenu! 4 | 5 | I have received a surprising amount of questions about SideMenu since putting it up here. A few people in the community have identified some problems and helped contribute to SideMenu to make it better for everyone and I'm truly grateful for the support! Keep them coming! 6 | 7 | I have also received a number of questions about people having issues implementing SideMenu, mostly from beginners learning how to code. As much as I would love to help all of you, **I do not have time to teach you**. I am only supporting bugfixes or reviewing pull requests. 8 | 9 | I spent a lot of time putting together a detailed [README](https://github.com/jonkykong/SideMenu/blob/master/README.md), adding comments about usage in code, and provided a [demo project](https://github.com/jonkykong/SideMenu/tree/master/Example). These will give you all the information you need to work through any problem, **saving _you_ the time it takes for me to personally respond.** 10 | 11 | ### If your question begins with _"How do I..."_ 12 | - That's **not** a bug. Go back through the [README](https://github.com/jonkykong/SideMenu/blob/master/README.md). Check out the [demo project](https://github.com/jonkykong/SideMenu/tree/master/Example). Stay persistent, try a few different things, and you will figure it out! **It is generally faster for you to figure it out for yourself instead of waiting for me to respond to you.** I also recommend searching and posting your questions on [stackoverflow.com](stackoverflow.com). If you're interested in hiring me to teach you, please email me using the email address listed at the top of my [README](https://github.com/jonkykong/SideMenu/blob/master/README.md). 13 | 14 | ### If your question begins with _"How can I..."_ 15 | - That's a **new feature request**. I am no longer investing my personal time to implement one-off features because SideMenu currently meets the majority of people's needs with the features it already has. However, this is a great opportunity for you to [join the proud members](https://github.com/jonkykong/SideMenu/graphs/contributors) who have contributed to this open source project! Feel free to open an issue to ask any clarifying questions for your new feature before you start building. Open a [pull request](https://github.com/jonkykong/SideMenu/pull/new/master) when you're ready for me to merge it. 16 | 17 | ### If your question is about SideMenu not working the way it's described in the [README](https://github.com/jonkykong/SideMenu/blob/master/README.md)... 18 | - This *may* be a bug. You must be able to reproduce the bug in the [demo project](https://github.com/jonkykong/SideMenu/tree/master/Example) which has a minimal amount of code. This helps ensure you don't have a bug in your code unrelated to SideMenu. If the bug is reproducable, open an issue and I will respond to it when I find time. 19 | 20 | **Again**, please do **not** email me or open any issues if you want to know how to use SideMenu or are having trouble getting it to behave a specific way not described in the [README](https://github.com/jonkykong/SideMenu/blob/master/README.md). I am not **tech support**. I am not a **teacher**. If you open an issue while failing to follow these guidelines or use the provided templates your **request for help will be ignored**. 21 | 22 | ### Thanks again for your support and for being respectful of my time. 23 | I apologize if this seems harsh, but there have been too many developers that have willfully ignored all of this and continued to contact me. 24 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [jonkykong] 2 | custom: ["https://www.paypal.me/jonkykong"] 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | ## New Issue Checklist 3 | 4 | I have read the [guidelines for contributing](https://github.com/jonkykong/SideMenu/blob/master/.github/CONTRIBUTING.md) and I understand: 5 | - [ ] My issue is happening in the **latest version** of SideMenu. 6 | - [ ] My issue was **not** solved in the [README](https://github.com/jonkykong/SideMenu/blob/master/README.md). 7 | - [ ] My issue can **not** be answered on [stackoverflow.com](stackoverflow.com). 8 | - [ ] My issue is **not** a request for new functionality that I am unwilling to build and contribute with a pull request. 9 | - [ ] My issue **is** reproducible in the [demo project](https://github.com/jonkykong/SideMenu/tree/master/Example). 10 | 11 | ## Issue Description 12 | 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: You must complete this template or your report will be automatically closed. 4 | title: '' 5 | labels: Bug - Help Wanted! 6 | assignees: '' 7 | 8 | --- 9 | 10 | **I have read the [guidelines for contributing](https://github.com/jonkykong/SideMenu/blob/master/.github/CONTRIBUTING.md) and I understand** 11 | - [ ] My issue is happening in the **latest version** of SideMenu (older versions are no longer maintained). 12 | - [ ] My issue was **not** solved in the [README](https://github.com/jonkykong/SideMenu/blob/master/README.md). 13 | - [ ] My issue can **not** be answered on [stackoverflow.com](stackoverflow.com). 14 | - [ ] My issue is **not** a request for new functionality that I am unwilling to build and contribute with a pull request. 15 | - [ ] My issue **is** reproducible in the [demo project](https://github.com/jonkykong/SideMenu/tree/master/Example). 16 | 17 | **Describe the bug** 18 | A clear and concise description of what the bug is. 19 | 20 | **To Reproduce** 21 | Steps to reproduce the behavior: 22 | 1. Go to '...' 23 | 2. Click on '....' 24 | 3. Scroll down to '....' 25 | 4. See error 26 | 27 | **Expected behavior** 28 | A clear and concise description of what you expected to happen. 29 | 30 | **Screenshots** 31 | If applicable, add screenshots to help explain your problem. 32 | 33 | **Additional context** 34 | Add any other context about the problem here. 35 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 3 | about: Suggest an idea for this project. 4 | title: '' 5 | labels: Feature Request - Help Wanted! 6 | assignees: '' 7 | 8 | --- 9 | 10 | SideMenu currently meets the majority of people's needs with the features it already has. However, this is a great opportunity for you to [join the proud members](https://github.com/jonkykong/SideMenu/graphs/contributors) who have contributed to this open source project! Feel free to open an issue to ask any clarifying questions for your new feature before you start building. Open a [pull request](https://github.com/jonkykong/SideMenu/pull/new/master) when you're ready for me to merge it. 11 | 12 | **Describe the solution you'd like** 13 | A clear and concise description of what you want to happen. 14 | 15 | **Ask any questions you have related to developing your solution** 16 | A clear and concise set of questions you'd like to ask. 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OS X 2 | .DS_Store 3 | 4 | # Xcode 5 | build/ 6 | *.pbxuser 7 | !default.pbxuser 8 | *.mode1v3 9 | !default.mode1v3 10 | *.mode2v3 11 | !default.mode2v3 12 | *.perspectivev3 13 | !default.perspectivev3 14 | xcuserdata 15 | *.xccheckout 16 | profile 17 | *.moved-aside 18 | DerivedData 19 | *.hmap 20 | *.ipa 21 | 22 | # Bundler 23 | .bundle 24 | 25 | Carthage 26 | # We recommend against adding the Pods directory to your .gitignore. However 27 | # you should judge for yourself, the pros and cons are mentioned at: 28 | # http://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control 29 | # 30 | # Note: if you ignore the Pods directory, make sure to uncomment 31 | # `pod install` in .travis.yml 32 | # 33 | # Pods/ 34 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # references: 2 | # * http://www.objc.io/issue-6/travis-ci.html 3 | # * https://github.com/supermarin/xcpretty#usage 4 | 5 | language: objective-c 6 | # cache: cocoapods 7 | # podfile: Example/Podfile 8 | # before_install: 9 | # - gem install cocoapods # Since Travis is not always on latest version 10 | # - pod install --project-directory=Example 11 | script: 12 | - set -o pipefail && xcodebuild test -workspace SideMenu.xcworkspace -scheme Example -sdk iphonesimulator ONLY_ACTIVE_ARCH=NO | xcpretty 13 | - pod lib lint 14 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at yo@massappeal.co. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /Example/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // SideMenu 4 | // 5 | // Created by jonkykong on 12/23/2015. 6 | // Copyright (c) 2015 jonkykong. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | private func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [AnyHashable: Any]?) -> Bool { 17 | // Override point for customization after application launch. 18 | return true 19 | } 20 | 21 | func applicationWillResignActive(_ application: UIApplication) { 22 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 23 | // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. 24 | } 25 | 26 | func applicationDidEnterBackground(_ application: UIApplication) { 27 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 28 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 29 | } 30 | 31 | func applicationWillEnterForeground(_ application: UIApplication) { 32 | // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. 33 | } 34 | 35 | func applicationDidBecomeActive(_ application: UIApplication) { 36 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 37 | } 38 | 39 | func applicationWillTerminate(_ application: UIApplication) { 40 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Example/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" : "ios-marketing", 45 | "size" : "1024x1024", 46 | "scale" : "1x" 47 | } 48 | ], 49 | "info" : { 50 | "version" : 1, 51 | "author" : "xcode" 52 | } 53 | } -------------------------------------------------------------------------------- /Example/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Example/Assets.xcassets/background.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "stars.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Example/Assets.xcassets/background.imageset/stars.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonkykong/SideMenu/8bd4fd128923cf5494fa726839af8afe12908ad9/Example/Assets.xcassets/background.imageset/stars.png -------------------------------------------------------------------------------- /Example/Assets.xcassets/saturn.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "saturn.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Example/Assets.xcassets/saturn.imageset/saturn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonkykong/SideMenu/8bd4fd128923cf5494fa726839af8afe12908ad9/Example/Assets.xcassets/saturn.imageset/saturn.png -------------------------------------------------------------------------------- /Example/Base.lproj/LaunchScreen.xib: -------------------------------------------------------------------------------- 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 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /Example/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | SideMenu 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | 1 25 | LSRequiresIPhoneOS 26 | 27 | UILaunchStoryboardName 28 | Launch Screen 29 | UIMainStoryboardFile 30 | Main 31 | UIRequiredDeviceCapabilities 32 | 33 | armv7 34 | 35 | UIStatusBarStyle 36 | UIStatusBarStyleLightContent 37 | UISupportedInterfaceOrientations 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationLandscapeLeft 41 | UIInterfaceOrientationLandscapeRight 42 | UIInterfaceOrientationPortraitUpsideDown 43 | 44 | UIViewControllerBasedStatusBarAppearance 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /Example/Launch Screen.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 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /Example/MainViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MainViewController.swift 3 | // 4 | // Created by Jon Kent on 11/12/15. 5 | // Copyright © 2015 Jon Kent. All rights reserved. 6 | // 7 | 8 | import SideMenu 9 | 10 | class MainViewController: UIViewController { 11 | @IBOutlet private weak var blackOutStatusBar: UISwitch! 12 | @IBOutlet private weak var blurSegmentControl: UISegmentedControl! 13 | @IBOutlet private weak var menuAlphaSlider: UISlider! 14 | @IBOutlet private weak var menuScaleFactorSlider: UISlider! 15 | @IBOutlet private weak var presentingAlphaSlider: UISlider! 16 | @IBOutlet private weak var presentingScaleFactorSlider: UISlider! 17 | @IBOutlet private weak var presentationStyleSegmentedControl: UISegmentedControl! 18 | @IBOutlet private weak var screenWidthSlider: UISlider! 19 | @IBOutlet private weak var shadowOpacitySlider: UISlider! 20 | 21 | override func viewDidLoad() { 22 | super.viewDidLoad() 23 | setupSideMenu() 24 | updateUI(settings: SideMenuSettings()) 25 | updateMenus() 26 | } 27 | 28 | override func prepare(for segue: UIStoryboardSegue, sender: Any?) { 29 | guard let sideMenuNavigationController = segue.destination as? SideMenuNavigationController else { return } 30 | sideMenuNavigationController.settings = makeSettings() 31 | } 32 | 33 | private func setupSideMenu() { 34 | // Define the menus 35 | SideMenuManager.default.leftMenuNavigationController = storyboard?.instantiateViewController(withIdentifier: "LeftMenuNavigationController") as? SideMenuNavigationController 36 | SideMenuManager.default.rightMenuNavigationController = storyboard?.instantiateViewController(withIdentifier: "RightMenuNavigationController") as? SideMenuNavigationController 37 | 38 | // Enable gestures. The left and/or right menus must be set up above for these to work. 39 | // Note that these continue to work on the Navigation Controller independent of the View Controller it displays! 40 | SideMenuManager.default.addPanGestureToPresent(toView: navigationController!.navigationBar) 41 | SideMenuManager.default.addScreenEdgePanGesturesToPresent(toView: view) 42 | } 43 | 44 | private func updateUI(settings: SideMenuSettings) { 45 | let styles:[UIBlurEffect.Style] = [.dark, .light, .extraLight] 46 | if let menuBlurEffectStyle = settings.blurEffectStyle { 47 | blurSegmentControl.selectedSegmentIndex = (styles.firstIndex(of: menuBlurEffectStyle) ?? 0) + 1 48 | } else { 49 | blurSegmentControl.selectedSegmentIndex = 0 50 | } 51 | 52 | blackOutStatusBar.isOn = settings.statusBarEndAlpha > 0 53 | menuAlphaSlider.value = Float(settings.presentationStyle.menuStartAlpha) 54 | menuScaleFactorSlider.value = Float(settings.presentationStyle.menuScaleFactor) 55 | presentingAlphaSlider.value = Float(settings.presentationStyle.presentingEndAlpha) 56 | presentingScaleFactorSlider.value = Float(settings.presentationStyle.presentingScaleFactor) 57 | screenWidthSlider.value = Float(settings.menuWidth / min(view.frame.width, view.frame.height)) 58 | shadowOpacitySlider.value = Float(settings.presentationStyle.onTopShadowOpacity) 59 | } 60 | 61 | @IBAction private func changeControl(_ control: UIControl) { 62 | if control == presentationStyleSegmentedControl { 63 | var settings = makeSettings() 64 | settings.presentationStyle = selectedPresentationStyle() 65 | updateUI(settings: settings) 66 | } 67 | updateMenus() 68 | } 69 | 70 | private func updateMenus() { 71 | let settings = makeSettings() 72 | SideMenuManager.default.leftMenuNavigationController?.settings = settings 73 | SideMenuManager.default.rightMenuNavigationController?.settings = settings 74 | } 75 | 76 | private func selectedPresentationStyle() -> SideMenuPresentationStyle { 77 | let modes: [SideMenuPresentationStyle] = [.menuSlideIn, .viewSlideOut, .viewSlideOutMenuIn, .menuDissolveIn] 78 | return modes[presentationStyleSegmentedControl.selectedSegmentIndex] 79 | } 80 | 81 | private func makeSettings() -> SideMenuSettings { 82 | let presentationStyle = selectedPresentationStyle() 83 | presentationStyle.backgroundColor = UIColor(patternImage: #imageLiteral(resourceName: "background")) 84 | presentationStyle.menuStartAlpha = CGFloat(menuAlphaSlider.value) 85 | presentationStyle.menuScaleFactor = CGFloat(menuScaleFactorSlider.value) 86 | presentationStyle.onTopShadowOpacity = shadowOpacitySlider.value 87 | presentationStyle.presentingEndAlpha = CGFloat(presentingAlphaSlider.value) 88 | presentationStyle.presentingScaleFactor = CGFloat(presentingScaleFactorSlider.value) 89 | 90 | var settings = SideMenuSettings() 91 | settings.presentationStyle = presentationStyle 92 | settings.menuWidth = min(view.frame.width, view.frame.height) * CGFloat(screenWidthSlider.value) 93 | let styles:[UIBlurEffect.Style?] = [nil, .dark, .light, .extraLight] 94 | settings.blurEffectStyle = styles[blurSegmentControl.selectedSegmentIndex] 95 | settings.statusBarEndAlpha = blackOutStatusBar.isOn ? 1 : 0 96 | 97 | return settings 98 | } 99 | } 100 | 101 | extension MainViewController: SideMenuNavigationControllerDelegate { 102 | 103 | func sideMenuWillAppear(menu: SideMenuNavigationController, animated: Bool) { 104 | print("SideMenu Appearing! (animated: \(animated))") 105 | } 106 | 107 | func sideMenuDidAppear(menu: SideMenuNavigationController, animated: Bool) { 108 | print("SideMenu Appeared! (animated: \(animated))") 109 | } 110 | 111 | func sideMenuWillDisappear(menu: SideMenuNavigationController, animated: Bool) { 112 | print("SideMenu Disappearing! (animated: \(animated))") 113 | } 114 | 115 | func sideMenuDidDisappear(menu: SideMenuNavigationController, animated: Bool) { 116 | print("SideMenu Disappeared! (animated: \(animated))") 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /Example/PresentedViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PresentedViewController.swift 3 | // 4 | // Created by Jon Kent on 12/14/15. 5 | // Copyright © 2015 Jon Kent. All rights reserved. 6 | // 7 | 8 | import UIKit 9 | 10 | class PresentedViewController: UIViewController { 11 | 12 | @IBAction private func close() { 13 | dismiss(animated: true, completion: nil) 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /Example/SideMenuTableViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SideMenuTableViewController.swift 3 | // SideMenu 4 | // 5 | // Created by Jon Kent on 4/5/16. 6 | // Copyright © 2016 CocoaPods. All rights reserved. 7 | // 8 | 9 | import SideMenu 10 | 11 | class SideMenuTableViewController: UITableViewController { 12 | 13 | override func viewWillAppear(_ animated: Bool) { 14 | super.viewWillAppear(animated) 15 | 16 | // refresh cell blur effect in case it changed 17 | tableView.reloadData() 18 | 19 | guard let menu = navigationController as? SideMenuNavigationController, menu.blurEffectStyle == nil else { 20 | return 21 | } 22 | 23 | // Set up a cool background image for demo purposes 24 | let imageView = UIImageView(image: #imageLiteral(resourceName: "saturn")) 25 | imageView.contentMode = .scaleAspectFit 26 | imageView.backgroundColor = UIColor.black.withAlphaComponent(0.2) 27 | tableView.backgroundView = imageView 28 | } 29 | 30 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 31 | let cell = super.tableView(tableView, cellForRowAt: indexPath) as! UITableViewVibrantCell 32 | 33 | if let menu = navigationController as? SideMenuNavigationController { 34 | cell.blurEffectStyle = menu.blurEffectStyle 35 | } 36 | 37 | return cell 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /ExampleTests/ExampleTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ExampleTests.swift 3 | // ExampleTests 4 | // 5 | // Created by Jon Kent on 8/10/19. 6 | // Copyright © 2019 jonkykong. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | 11 | class ExampleTests: XCTestCase { 12 | 13 | private let styleTitles = ["Slide In", "Slide Out", "In + Out", "Dissolve"] 14 | private let swipeHere = "Swipe Here" 15 | 16 | private let app = XCUIApplication() 17 | private var mainViewController: XCUIElement { 18 | return app.navigationBars[swipeHere] 19 | } 20 | 21 | override func setUp() { 22 | // Put setup code here. This method is called before the invocation of each test method in the class. 23 | 24 | // In UI tests it is usually best to stop immediately when a failure occurs. 25 | continueAfterFailure = false 26 | 27 | // UI tests must launch the application that they test. Doing this in setup will make sure it happens for each test method. 28 | XCUIApplication().launch() 29 | 30 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. 31 | } 32 | 33 | override func tearDown() { 34 | // Put teardown code here. This method is called after the invocation of each test method in the class. 35 | } 36 | 37 | func testTapLeft() { 38 | let elementsQuery = app.scrollViews.otherElements 39 | for title in styleTitles { 40 | elementsQuery.buttons[title].tap() 41 | mainViewController.buttons["Left Menu"].tap() 42 | app.tables/*@START_MENU_TOKEN@*/.staticTexts["Push View Controller 1"]/*[[".cells.staticTexts[\"Push View Controller 1\"]",".staticTexts[\"Push View Controller 1\"]"],[[[-1,1],[-1,0]]],[0]]@END_MENU_TOKEN@*/.tap() 43 | app.navigationBars["You Can Still Swipe!"].buttons[swipeHere].tap() 44 | validate() 45 | } 46 | } 47 | 48 | func testTapRight() { 49 | let elementsQuery = app.scrollViews.otherElements 50 | for title in styleTitles { 51 | elementsQuery.buttons[title].tap() 52 | mainViewController.buttons["Right Menu"].tap() 53 | app.tables/*@START_MENU_TOKEN@*/.staticTexts["Present View Controller 1"]/*[[".cells.staticTexts[\"Present View Controller 1\"]",".staticTexts[\"Present View Controller 1\"]"],[[[-1,1],[-1,0]]],[0]]@END_MENU_TOKEN@*/.tap() 54 | app.buttons["Dismiss"].tap() 55 | mainViewController.tap() 56 | validate() 57 | } 58 | } 59 | 60 | func testSwiping() { 61 | mainViewController.swipeRight() 62 | mainViewController.swipeLeft() 63 | validate() 64 | 65 | mainViewController.swipeLeft() 66 | mainViewController.swipeRight() 67 | validate() 68 | } 69 | 70 | private func validate() { 71 | XCTAssertTrue(mainViewController.exists) 72 | } 73 | 74 | /* TODO - More tests: 75 | - Rotation 76 | - All menu settings 77 | */ 78 | } 79 | -------------------------------------------------------------------------------- /ExampleTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Jonathan Kent 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.0 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "SideMenu", 8 | products: [ 9 | .library(name: "SideMenu", targets: ["SideMenu"]) 10 | ], 11 | dependencies: [], 12 | targets: [ 13 | .target(name: "SideMenu", path: "Pod/Classes") 14 | ], 15 | swiftLanguageVersions: [.v4_2, .v5] 16 | ) 17 | 18 | -------------------------------------------------------------------------------- /Pod/Classes/Deprecations.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Deprecations.swift 3 | // SideMenu 4 | // 5 | // Created by Jon Kent on 7/3/19. 6 | // 7 | 8 | import UIKit 9 | 10 | // Deprecations; to be removed at a future date. 11 | extension SideMenuManager { 12 | 13 | @available(*, deprecated, renamed: "leftMenuNavigationController") 14 | open var menuLeftNavigationController: SideMenuNavigationController? { 15 | get { return nil } 16 | set {} 17 | } 18 | 19 | @available(*, deprecated, renamed: "rightMenuNavigationController") 20 | open var menuRightNavigationController: SideMenuNavigationController? { 21 | get { return nil } 22 | set {} 23 | } 24 | 25 | @available(*, deprecated, message: "This property has been moved to the SideMenuNavigationController class.") 26 | public var menuPresentMode: SideMenuPresentationStyle { 27 | get { return .viewSlideOut } 28 | set {} 29 | } 30 | 31 | @available(*, deprecated, message: "This property has been moved to the SideMenuNavigationController class.") 32 | public var menuPushStyle: SideMenuPushStyle { 33 | get { return .default } 34 | set {} 35 | } 36 | 37 | @available(*, deprecated, message: "This property has been moved to the SideMenuNavigationController class.") 38 | public var menuAllowPushOfSameClassTwice: Bool { 39 | get { return true } 40 | set {} 41 | } 42 | 43 | @available(*, deprecated, message: "This property has been moved to the SideMenuNavigationController class.") 44 | public var menuWidth: CGFloat { 45 | get { return 0 } 46 | set {} 47 | } 48 | 49 | @available(*, deprecated, message: "This property has been moved to the SideMenuNavigationController class.") 50 | public var menuAnimationPresentDuration: Double { 51 | get { return 0.35 } 52 | set {} 53 | } 54 | 55 | @available(*, deprecated, message: "This property has been moved to the SideMenuNavigationController class.") 56 | public var menuAnimationDismissDuration: Double { 57 | get { return 0.35 } 58 | set {} 59 | } 60 | 61 | @available(*, deprecated, message: "This property has been moved to the SideMenuNavigationController class.") 62 | public var menuAnimationCompleteGestureDuration: Double { 63 | get { return 0.35 } 64 | set {} 65 | } 66 | 67 | @available(*, deprecated, message: "This property has been moved to the SideMenuPresentationStyle class.") 68 | public var menuAnimationFadeStrength: CGFloat { 69 | get { return 0 } 70 | set {} 71 | } 72 | 73 | @available(*, deprecated, message: "This property has been moved to the SideMenuPresentationStyle class.") 74 | public var menuAnimationTransformScaleFactor: CGFloat { 75 | get { return 1 } 76 | set {} 77 | } 78 | 79 | @available(*, deprecated, message: "This property has been moved to the SideMenuPresentationStyle class.") 80 | public var menuAnimationBackgroundColor: UIColor? { 81 | get { return nil } 82 | set {} 83 | } 84 | 85 | @available(*, deprecated, message: "This property has been moved to the SideMenuPresentationStyle class.") 86 | public var menuShadowOpacity: Float { 87 | get { return 0.5 } 88 | set {} 89 | } 90 | 91 | @available(*, deprecated, message: "This property has been moved to the SideMenuPresentationStyle class.") 92 | public var menuShadowColor: UIColor { 93 | get { return .black } 94 | set {} 95 | } 96 | 97 | @available(*, deprecated, message: "This property has been moved to the SideMenuPresentationStyle class.") 98 | public var menuShadowRadius: CGFloat { 99 | get { return 5 } 100 | set {} 101 | } 102 | 103 | @available(*, deprecated, message: "This property has been moved to the SideMenuNavigationController class.") 104 | public var menuPresentingViewControllerUserInteractionEnabled: Bool { 105 | get { return false } 106 | set {} 107 | } 108 | 109 | @available(*, deprecated, message: "This property has been moved to the SideMenuPresentationStyle class.") 110 | public var menuParallaxStrength: Int { 111 | get { return 0 } 112 | set {} 113 | } 114 | 115 | @available(*, deprecated, message: "This property has been moved to the SideMenuNavigationController class.") 116 | public var menuFadeStatusBar: Bool { 117 | get { return true } 118 | set {} 119 | } 120 | 121 | @available(*, deprecated, message: "This property has been moved to the SideMenuNavigationController class.") 122 | public var menuAnimationOptions: UIView.AnimationOptions { 123 | get { return .curveEaseInOut } 124 | set {} 125 | } 126 | 127 | @available(*, deprecated, message: "This property has been moved to the SideMenuNavigationController class.") 128 | public var menuAnimationCompletionCurve: UIView.AnimationCurve { 129 | get { return .easeIn } 130 | set {} 131 | } 132 | 133 | @available(*, deprecated, message: "This property has been moved to the SideMenuNavigationController class.") 134 | public var menuAnimationUsingSpringWithDamping: CGFloat { 135 | get { return 1 } 136 | set {} 137 | } 138 | 139 | @available(*, deprecated, message: "This property has been moved to the SideMenuNavigationController class.") 140 | public var menuAnimationInitialSpringVelocity: CGFloat { 141 | get { return 1 } 142 | set {} 143 | } 144 | 145 | @available(*, deprecated, message: "This property has been moved to the SideMenuNavigationController class.") 146 | public var menuDismissOnPush: Bool { 147 | get { return true } 148 | set {} 149 | } 150 | 151 | @available(*, deprecated, message: "This property has been moved to the SideMenuNavigationController class.") 152 | public var menuAlwaysAnimate: Bool { 153 | get { return false } 154 | set {} 155 | } 156 | 157 | @available(*, deprecated, message: "This property has been moved to the SideMenuNavigationController class.") 158 | public var menuDismissWhenBackgrounded: Bool { 159 | get { return true } 160 | set {} 161 | } 162 | 163 | @available(*, deprecated, message: "This property has been moved to the SideMenuNavigationController class.") 164 | public var menuBlurEffectStyle: UIBlurEffect.Style? { 165 | get { return nil } 166 | set {} 167 | } 168 | 169 | @available(*, deprecated, message: "This property has been moved to the SideMenuNavigationController class.") 170 | public weak var menuLeftSwipeToDismissGesture: UIPanGestureRecognizer? { 171 | get { return nil } 172 | set {} 173 | } 174 | 175 | @available(*, deprecated, message: "This property has been moved to the SideMenuNavigationController class.") 176 | public weak var menuRightSwipeToDismissGesture: UIPanGestureRecognizer? { 177 | get { return nil } 178 | set {} 179 | } 180 | 181 | @available(*, deprecated, message: "This property has been moved to the SideMenuNavigationController class.") 182 | public var menuEnableSwipeGestures: Bool { 183 | get { return true } 184 | set {} 185 | } 186 | 187 | @available(*, deprecated, renamed: "enableSwipeToDismissGesture") 188 | public var enableSwipeGestures: Bool { 189 | get { return true } 190 | set {} 191 | } 192 | 193 | @available(*, deprecated, renamed: "SideMenuPresentationStyle") 194 | public typealias MenuPresentMode = SideMenuPresentationStyle 195 | 196 | @available(*, deprecated, renamed: "addScreenEdgePanGesturesToPresent") 197 | @discardableResult public func menuAddScreenEdgePanGesturesToPresent(toView view: UIView, forMenu sides: [PresentDirection] = [.left, .right]) -> [UIScreenEdgePanGestureRecognizer] { 198 | return [] 199 | } 200 | 201 | @available(*, deprecated, renamed: "addPanGestureToPresent") 202 | @discardableResult public func menuAddPanGestureToPresent(toView view: UIView) -> UIPanGestureRecognizer { 203 | return UIPanGestureRecognizer() 204 | } 205 | } 206 | 207 | extension SideMenuPresentationStyle { 208 | @available(*, deprecated, renamed: "viewSlideOutMenuIn") 209 | public static var viewSlideInOut: SideMenuPresentationStyle { return viewSlideOutMenuIn } 210 | } 211 | 212 | @available(*, deprecated, renamed: "SideMenuNavigationController") 213 | public typealias UISideMenuNavigationController = SideMenuNavigationController 214 | 215 | @available(*, deprecated, renamed: "SideMenuNavigationControllerDelegate") 216 | public typealias UISideMenuNavigationControllerDelegate = SideMenuNavigationControllerDelegate 217 | -------------------------------------------------------------------------------- /Pod/Classes/Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Extensions.swift 3 | // Pods-Example 4 | // 5 | // Created by Jon Kent on 7/1/19. 6 | // 7 | 8 | import UIKit 9 | 10 | extension NSObject: InitializableClass {} 11 | 12 | internal extension UIView { 13 | 14 | @discardableResult func untransformed(_ block: () -> CGFloat) -> CGFloat { 15 | let t = transform 16 | transform = .identity 17 | let value = block() 18 | transform = t 19 | return value 20 | } 21 | 22 | func bringToFront() { 23 | superview?.bringSubviewToFront(self) 24 | } 25 | 26 | func untransform(_ block: () -> Void) { 27 | untransformed { () -> CGFloat in 28 | block() 29 | return 0 30 | } 31 | } 32 | 33 | static func animationsEnabled(_ enabled: Bool = true, _ block: () -> Void) { 34 | let a = areAnimationsEnabled 35 | setAnimationsEnabled(enabled) 36 | block() 37 | setAnimationsEnabled(a) 38 | } 39 | } 40 | 41 | internal extension UIViewController { 42 | 43 | // View controller actively displayed in that layer. It may not be visible if it's presenting another view controller. 44 | var activeViewController: UIViewController { 45 | switch self { 46 | case let navigationController as UINavigationController: 47 | return navigationController.topViewController?.activeViewController ?? self 48 | case let tabBarController as UITabBarController: 49 | return tabBarController.selectedViewController?.activeViewController ?? self 50 | case let splitViewController as UISplitViewController: 51 | return splitViewController.viewControllers.last?.activeViewController ?? self 52 | default: 53 | return self 54 | } 55 | } 56 | 57 | // View controller being displayed on screen to the user. 58 | var topMostViewController: UIViewController { 59 | let activeViewController = self.activeViewController 60 | return activeViewController.presentedViewController?.topMostViewController ?? activeViewController 61 | } 62 | 63 | var containerViewController: UIViewController { 64 | return navigationController?.containerViewController ?? 65 | tabBarController?.containerViewController ?? 66 | splitViewController?.containerViewController ?? 67 | self 68 | } 69 | 70 | @objc var isHidden: Bool { 71 | return presentingViewController == nil 72 | } 73 | } 74 | 75 | internal extension UIGestureRecognizer { 76 | 77 | convenience init(addTo view: UIView, target: Any, action: Selector) { 78 | self.init(target: target, action: action) 79 | view.addGestureRecognizer(self) 80 | } 81 | 82 | convenience init?(addTo view: UIView?, target: Any, action: Selector) { 83 | guard let view = view else { return nil } 84 | self.init(addTo: view, target: target, action: action) 85 | } 86 | 87 | func remove() { 88 | view?.removeGestureRecognizer(self) 89 | } 90 | } 91 | 92 | internal extension UIPanGestureRecognizer { 93 | 94 | var canSwitch: Bool { 95 | return !(self is UIScreenEdgePanGestureRecognizer) 96 | } 97 | 98 | var xTranslation: CGFloat { 99 | return view?.untransformed { 100 | return self.translation(in: view).x 101 | } ?? 0 102 | } 103 | 104 | var xVelocity: CGFloat { 105 | return view?.untransformed { 106 | return self.velocity(in: view).x 107 | } ?? 0 108 | } 109 | } 110 | 111 | internal extension UIApplication { 112 | 113 | var keyWindow: UIWindow? { 114 | return UIApplication.shared.windows.filter { $0.isKeyWindow }.first 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /Pod/Classes/Initializable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Initializable.swift 3 | // SideMenu 4 | // 5 | // Created by Jon Kent on 7/2/19. 6 | // 7 | 8 | import Foundation 9 | 10 | internal protocol InitializableClass: class { 11 | init() 12 | } 13 | 14 | extension InitializableClass { 15 | init(_ block: (Self) -> Void) { 16 | self.init() 17 | block(self) 18 | } 19 | 20 | @discardableResult func with(_ block: (Self) -> Void) -> Self { 21 | block(self) 22 | return self 23 | } 24 | } 25 | 26 | public protocol InitializableStruct { 27 | init() 28 | } 29 | 30 | public extension InitializableStruct { 31 | init(_ block: (inout Self) -> Void) { 32 | self.init() 33 | block(&self) 34 | } 35 | 36 | @discardableResult mutating func with(_ block: (inout Self) -> Void) -> Self { 37 | block(&self) 38 | return self 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Pod/Classes/Print.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Print.swift 3 | // SideMenu 4 | // 5 | // Created by Jon Kent on 12/5/18. 6 | // 7 | 8 | import Foundation 9 | 10 | internal enum Print: String { case 11 | cannotPush = "Attempt to push a View Controller from %@ where its navigationController == nil. It must be embedded in a UINavigationController for this to work.", 12 | emptyMenu = "The menu doesn't have a view controller to show! SideMenuNavigationController needs a view controller to display just like a UINavigationController.", 13 | menuAlreadyAssigned = "%@ was already assigned to the %@ of %@. When using multiple SideMenuManagers you may want to use new instances of SideMenuNavigationController instead of existing instances to avoid crashes if the menu is presented more than once.", 14 | menuInUse = "%@ cannot be modified while it's presented.", 15 | panGestureAdded = "%@ was called before %@ or %@ was set. Gestures will not work without a menu.", 16 | property = "A menu's %@ property can only be changed when it is hidden.", 17 | screenGestureAdded = "%@ was called before %@ was set. The gesture will not work without a menu. Use %@ to add gestures for only one menu.", 18 | transitioningDelegate = "SideMenu requires use of the transitioningDelegate. It cannot be modified." 19 | 20 | enum PropertyName: String { case 21 | leftSide 22 | } 23 | 24 | static func warning(_ print: Print, arguments: CVarArg..., required: Bool = false) { 25 | warning(String(format: print.rawValue, arguments: arguments), required: required) 26 | } 27 | 28 | static func warning(_ print: Print, arguments: PropertyName..., required: Bool = false) { 29 | warning(String(format: print.rawValue, arguments: arguments.map { $0.rawValue }), required: required) 30 | } 31 | 32 | static func warning(_ print: Print, required: Bool = false) { 33 | warning(print.rawValue, required: required) 34 | } 35 | } 36 | 37 | private extension Print { 38 | 39 | static func warning(_ message: String, required: Bool = false) { 40 | let message = "SideMenu Warning: \(message)" 41 | 42 | if required { 43 | print(message) 44 | return 45 | } 46 | #if !STFU_SIDEMENU 47 | print(message) 48 | #endif 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /Pod/Classes/Protected.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Protected.swift 3 | // SideMenu 4 | // 5 | // Created by Jon Kent on 2/9/19. 6 | // 7 | 8 | import Foundation 9 | 10 | internal final class Protected { 11 | 12 | typealias ConditionBlock = (_ oldValue: T, T) -> T 13 | 14 | private var _value: T 15 | private var condition: ConditionBlock 16 | 17 | public var value: T { 18 | get { return _value } 19 | set { _value = condition(_value, newValue) } 20 | } 21 | 22 | init(_ value: T, when condition: @escaping ConditionBlock) { 23 | self._value = value 24 | self.condition = condition 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Pod/Classes/SideMenuAnimationController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SideMenuAnimationController.swift 3 | // SideMenu 4 | // 5 | // Created by Jon Kent on 10/24/18. 6 | // 7 | 8 | import UIKit 9 | 10 | internal protocol AnimationModel { 11 | /// The animation options when a menu is displayed. Ignored when displayed with a gesture. 12 | var animationOptions: UIView.AnimationOptions { get } 13 | /// Duration of the remaining animation when the menu is partially dismissed with gestures. Default is 0.35 seconds. 14 | var completeGestureDuration: Double { get } 15 | /// Duration of the animation when the menu is dismissed without gestures. Default is 0.35 seconds. 16 | var dismissDuration: Double { get } 17 | /// The animation initial spring velocity when a menu is displayed. Ignored when displayed with a gesture. 18 | var initialSpringVelocity: CGFloat { get } 19 | /// Duration of the animation when the menu is presented without gestures. Default is 0.35 seconds. 20 | var presentDuration: Double { get } 21 | /// The animation spring damping when a menu is displayed. Ignored when displayed with a gesture. 22 | var usingSpringWithDamping: CGFloat { get } 23 | } 24 | 25 | internal protocol SideMenuAnimationControllerDelegate: class { 26 | func sideMenuAnimationController(_ animationController: SideMenuAnimationController, didDismiss viewController: UIViewController) 27 | func sideMenuAnimationController(_ animationController: SideMenuAnimationController, didPresent viewController: UIViewController) 28 | } 29 | 30 | internal final class SideMenuAnimationController: NSObject, UIViewControllerAnimatedTransitioning { 31 | 32 | typealias Model = AnimationModel & PresentationModel 33 | 34 | private var config: Model 35 | private weak var containerView: UIView? 36 | private let leftSide: Bool 37 | private weak var originalSuperview: UIView? 38 | private var presentationController: SideMenuPresentationController? 39 | private unowned var presentedViewController: UIViewController? 40 | private unowned var presentingViewController: UIViewController? 41 | weak var delegate: SideMenuAnimationControllerDelegate? 42 | 43 | init(config: Model, leftSide: Bool, delegate: SideMenuAnimationControllerDelegate? = nil) { 44 | self.config = config 45 | self.leftSide = leftSide 46 | self.delegate = delegate 47 | } 48 | 49 | func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { 50 | guard 51 | let presentedViewController = transitionContext.presentedViewController, 52 | let presentingViewController = transitionContext.presentingViewController 53 | else { return } 54 | 55 | if transitionContext.isPresenting { 56 | self.containerView = transitionContext.containerView 57 | self.presentedViewController = presentedViewController 58 | self.presentingViewController = presentingViewController 59 | self.presentationController = SideMenuPresentationController( 60 | config: config, 61 | leftSide: leftSide, 62 | presentedViewController: presentedViewController, 63 | presentingViewController: presentingViewController, 64 | containerView: transitionContext.containerView 65 | ) 66 | } 67 | 68 | transition(using: transitionContext) 69 | } 70 | 71 | func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { 72 | guard let transitionContext = transitionContext else { return 0 } 73 | return duration(presenting: transitionContext.isPresenting, interactive: transitionContext.isInteractive) 74 | } 75 | 76 | func animationEnded(_ transitionCompleted: Bool) { 77 | guard let presentedViewController = presentedViewController else { return } 78 | if presentedViewController.isHidden { 79 | delegate?.sideMenuAnimationController(self, didDismiss: presentedViewController) 80 | } else { 81 | delegate?.sideMenuAnimationController(self, didPresent: presentedViewController) 82 | } 83 | } 84 | 85 | func transition(presenting: Bool, animated: Bool = true, interactive: Bool = false, alongsideTransition: (() -> Void)? = nil, complete: Bool = true, completion: ((Bool) -> Void)? = nil) { 86 | prepare(presenting: presenting) 87 | transitionWillBegin(presenting: presenting) 88 | transition( 89 | presenting: presenting, 90 | animated: animated, 91 | interactive: interactive, 92 | animations: { [weak self] in 93 | guard let self = self else { return } 94 | self.transition(presenting: presenting) 95 | alongsideTransition?() 96 | }, completion: { [weak self] _ in 97 | guard let self = self else { return } 98 | if complete { 99 | self.transitionDidEnd(presenting: presenting, completed: true) 100 | self.finish(presenting: presenting, completed: true) 101 | } 102 | completion?(true) 103 | }) 104 | } 105 | 106 | func layout() { 107 | presentationController?.containerViewWillLayoutSubviews() 108 | } 109 | } 110 | 111 | private extension SideMenuAnimationController { 112 | 113 | func duration(presenting: Bool, interactive: Bool) -> Double { 114 | if interactive { return config.completeGestureDuration } 115 | return presenting ? config.presentDuration : config.dismissDuration 116 | } 117 | 118 | func prepare(presenting: Bool) { 119 | guard 120 | presenting, 121 | let presentingViewController = presentingViewController, 122 | let presentedViewController = presentedViewController 123 | else { return } 124 | 125 | originalSuperview = presentingViewController.view.superview 126 | containerView?.addSubview(presentingViewController.view) 127 | containerView?.addSubview(presentedViewController.view) 128 | } 129 | 130 | func transitionWillBegin(presenting: Bool) { 131 | // prevent any other menu gestures from firing 132 | containerView?.isUserInteractionEnabled = false 133 | if presenting { 134 | presentationController?.presentationTransitionWillBegin() 135 | } else { 136 | presentationController?.dismissalTransitionWillBegin() 137 | } 138 | } 139 | 140 | func transition(presenting: Bool) { 141 | if presenting { 142 | presentationController?.presentationTransition() 143 | } else { 144 | presentationController?.dismissalTransition() 145 | } 146 | } 147 | 148 | func transitionDidEnd(presenting: Bool, completed: Bool) { 149 | if presenting { 150 | presentationController?.presentationTransitionDidEnd(completed) 151 | } else { 152 | presentationController?.dismissalTransitionDidEnd(completed) 153 | } 154 | containerView?.isUserInteractionEnabled = true 155 | } 156 | 157 | func finish(presenting: Bool, completed: Bool) { 158 | guard 159 | presenting != completed, 160 | let presentingViewController = self.presentingViewController 161 | else { return } 162 | presentedViewController?.view.removeFromSuperview() 163 | originalSuperview?.addSubview(presentingViewController.view) 164 | } 165 | 166 | func transition(using transitionContext: UIViewControllerContextTransitioning) { 167 | prepare(presenting: transitionContext.isPresenting) 168 | transitionWillBegin(presenting: transitionContext.isPresenting) 169 | transition( 170 | presenting: transitionContext.isPresenting, 171 | animated: transitionContext.isAnimated, 172 | interactive: transitionContext.isInteractive, 173 | animations: { [weak self] in 174 | guard let self = self else { return } 175 | self.transition(presenting: transitionContext.isPresenting) 176 | }, completion: { [weak self] _ in 177 | guard let self = self else { return } 178 | let completed = !transitionContext.transitionWasCancelled 179 | self.transitionDidEnd(presenting: transitionContext.isPresenting, completed: completed) 180 | self.finish(presenting: transitionContext.isPresenting, completed: completed) 181 | 182 | // Called last. This causes the transition container to be removed and animationEnded() to be called. 183 | transitionContext.completeTransition(completed) 184 | }) 185 | } 186 | 187 | func transition(presenting: Bool, animated: Bool = true, interactive: Bool = false, animations: @escaping (() -> Void) = {}, completion: @escaping ((Bool) -> Void) = { _ in }) { 188 | if !animated { 189 | animations() 190 | completion(true) 191 | return 192 | } 193 | 194 | let duration = self.duration(presenting: presenting, interactive: interactive) 195 | if interactive { 196 | // IMPORTANT: The non-interactive animation block will not complete if adapted for interactive. The below animation block must be used! 197 | UIView.animate( 198 | withDuration: duration, 199 | delay: duration, // HACK: If zero, the animation briefly flashes in iOS 11. 200 | options: .curveLinear, 201 | animations: animations, 202 | completion: completion 203 | ) 204 | return 205 | } 206 | 207 | UIView.animate( 208 | withDuration: duration, 209 | delay: 0, 210 | usingSpringWithDamping: config.usingSpringWithDamping, 211 | initialSpringVelocity: config.initialSpringVelocity, 212 | options: config.animationOptions, 213 | animations: animations, 214 | completion: completion 215 | ) 216 | } 217 | } 218 | 219 | private extension UIViewControllerContextTransitioning { 220 | 221 | var isPresenting: Bool { 222 | return viewController(forKey: .from)?.presentedViewController === viewController(forKey: .to) 223 | } 224 | 225 | var presentingViewController: UIViewController? { 226 | return viewController(forKey: isPresenting ? .from : .to) 227 | } 228 | 229 | var presentedViewController: UIViewController? { 230 | return viewController(forKey: isPresenting ? .to : .from) 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /Pod/Classes/SideMenuInteractionController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SideMenuInteractiveTransitionController.swift 3 | // SideMenu 4 | // 5 | // Created by Jon Kent on 12/28/18. 6 | // 7 | 8 | import UIKit 9 | 10 | internal final class SideMenuInteractionController: UIPercentDrivenInteractiveTransition { 11 | 12 | enum State { case 13 | update(progress: CGFloat), 14 | finish, 15 | cancel 16 | } 17 | 18 | private(set) var isCancelled: Bool = false 19 | private(set) var isFinished: Bool = false 20 | 21 | init(cancelWhenBackgrounded: Bool = true, completionCurve: UIView.AnimationCurve = .easeIn) { 22 | super.init() 23 | self.completionCurve = completionCurve 24 | 25 | guard cancelWhenBackgrounded else { return } 26 | NotificationCenter.default.addObserver(self, selector: #selector(handleNotification), name: UIApplication.didEnterBackgroundNotification, object: nil) 27 | } 28 | 29 | override func cancel() { 30 | isCancelled = true 31 | super.cancel() 32 | } 33 | 34 | override func finish() { 35 | isFinished = true 36 | super.finish() 37 | } 38 | 39 | override func update(_ percentComplete: CGFloat) { 40 | guard !isCancelled && !isFinished else { return } 41 | super.update(percentComplete) 42 | } 43 | 44 | func handle(state: State) { 45 | switch state { 46 | case .update(let progress): 47 | update(progress) 48 | case .finish: 49 | finish() 50 | case .cancel: 51 | cancel() 52 | } 53 | } 54 | } 55 | 56 | private extension SideMenuInteractionController { 57 | 58 | @objc func handleNotification(notification: NSNotification) { 59 | switch notification.name { 60 | case UIApplication.didEnterBackgroundNotification: 61 | cancel() 62 | default: break 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Pod/Classes/SideMenuManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SideMenuManager.swift 3 | // 4 | // Created by Jon Kent on 12/6/15. 5 | // Copyright © 2015 Jon Kent. All rights reserved. 6 | // 7 | 8 | import UIKit 9 | 10 | @objcMembers 11 | public class SideMenuManager: NSObject { 12 | 13 | final private class SideMenuPanGestureRecognizer: UIPanGestureRecognizer {} 14 | final private class SideMenuScreenEdgeGestureRecognizer: UIScreenEdgePanGestureRecognizer {} 15 | 16 | @objc public enum PresentDirection: Int { case 17 | left = 1, 18 | right = 0 19 | 20 | init(leftSide: Bool) { 21 | self.init(rawValue: leftSide ? 1 : 0)! 22 | } 23 | 24 | var edge: UIRectEdge { 25 | switch self { 26 | case .left: return .left 27 | case .right: return .right 28 | } 29 | } 30 | 31 | var name: String { 32 | switch self { 33 | case .left: return "leftMenuNavigationController" 34 | case .right: return "rightMenuNavigationController" 35 | } 36 | } 37 | } 38 | 39 | private var _leftMenu: Protected = Protected(nil) { SideMenuManager.setMenu(fromMenu: $0, toMenu: $1) } 40 | private var _rightMenu: Protected = Protected(nil) { SideMenuManager.setMenu(fromMenu: $0, toMenu: $1) } 41 | 42 | private var switching: Bool = false 43 | 44 | /// Default instance of SideMenuManager. 45 | public static let `default` = SideMenuManager() 46 | 47 | /// Default instance of SideMenuManager (objective-C). 48 | public class var defaultManager: SideMenuManager { 49 | return SideMenuManager.default 50 | } 51 | 52 | /// The left menu. 53 | open var leftMenuNavigationController: SideMenuNavigationController? { 54 | get { 55 | if _leftMenu.value?.isHidden == true { 56 | _leftMenu.value?.leftSide = true 57 | } 58 | return _leftMenu.value 59 | } 60 | set(menu) { _leftMenu.value = menu } 61 | } 62 | 63 | /// The right menu. 64 | open var rightMenuNavigationController: SideMenuNavigationController? { 65 | get { 66 | if _rightMenu.value?.isHidden == true { 67 | _rightMenu.value?.leftSide = false 68 | } 69 | return _rightMenu.value 70 | } 71 | set(menu) { _rightMenu.value = menu } 72 | } 73 | 74 | /** 75 | Adds screen edge gestures for both left and right sides to a view to present a menu. 76 | 77 | - Parameter toView: The view to add gestures to. 78 | 79 | - Returns: The array of screen edge gestures added to `toView`. 80 | */ 81 | @discardableResult public func addScreenEdgePanGesturesToPresent(toView view: UIView) -> [UIScreenEdgePanGestureRecognizer] { 82 | return [ 83 | addScreenEdgePanGesturesToPresent(toView: view, forMenu: .left), 84 | addScreenEdgePanGesturesToPresent(toView: view, forMenu: .right) 85 | ] 86 | } 87 | 88 | /** 89 | Adds screen edge gestures to a view to present a menu. 90 | 91 | - Parameter toView: The view to add gestures to. 92 | - Parameter forMenu: The menu (left or right) you want to add a gesture for. 93 | 94 | - Returns: The screen edge gestures added to `toView`. 95 | */ 96 | @discardableResult public func addScreenEdgePanGesturesToPresent(toView view: UIView, forMenu side: PresentDirection) -> UIScreenEdgePanGestureRecognizer { 97 | if menu(forSide: side) == nil { 98 | let methodName = #function // "addScreenEdgePanGesturesToPresent" 99 | let suggestedMethodName = "addScreenEdgePanGesturesToPresent(toView:forMenu:))" 100 | Print.warning(.screenGestureAdded, arguments: methodName, side.name, suggestedMethodName) 101 | } 102 | return self.addScreenEdgeGesture(to: view, edge: side.edge) 103 | } 104 | 105 | /** 106 | Adds a pan edge gesture to a view to present menus. 107 | 108 | - Parameter toView: The view to add a pan gesture to. 109 | 110 | - Returns: The pan gesture added to `toView`. 111 | */ 112 | @discardableResult public func addPanGestureToPresent(toView view: UIView) -> UIPanGestureRecognizer { 113 | if leftMenuNavigationController ?? rightMenuNavigationController == nil { 114 | Print.warning(.panGestureAdded, arguments: #function, PresentDirection.left.name, PresentDirection.right.name, required: true) 115 | } 116 | 117 | return addPresentPanGesture(to: view) 118 | } 119 | } 120 | 121 | internal extension SideMenuManager { 122 | 123 | func setMenu(_ menu: Menu?, forLeftSide leftSide: Bool) { 124 | switch leftSide { 125 | case true: leftMenuNavigationController = menu 126 | case false: rightMenuNavigationController = menu 127 | } 128 | } 129 | 130 | private class func setMenu(fromMenu: Menu?, toMenu: Menu?) -> Menu? { 131 | if fromMenu?.isHidden == false { 132 | Print.warning(.menuInUse, arguments: PresentDirection.left.name, required: true) 133 | return fromMenu 134 | } 135 | return toMenu 136 | } 137 | } 138 | 139 | private extension SideMenuManager { 140 | 141 | @objc func handlePresentMenuScreenEdge(_ gesture: UIScreenEdgePanGestureRecognizer) { 142 | handleMenuPan(gesture) 143 | } 144 | 145 | @objc func handlePresentMenuPan(_ gesture: UIPanGestureRecognizer) { 146 | handleMenuPan(gesture) 147 | } 148 | 149 | func handleMenuPan(_ gesture: UIPanGestureRecognizer) { 150 | if let activeMenu = activeMenu { 151 | let width = activeMenu.menuWidth 152 | let distance = gesture.xTranslation / width 153 | switch (gesture.state) { 154 | case .changed: 155 | if gesture.canSwitch { 156 | switching = (distance > 0 && !activeMenu.leftSide) || (distance < 0 && activeMenu.leftSide) 157 | if switching { 158 | activeMenu.cancelMenuPan(gesture) 159 | return 160 | } 161 | } 162 | default: 163 | switching = false 164 | } 165 | 166 | } else { 167 | let leftSide: Bool 168 | if let gesture = gesture as? UIScreenEdgePanGestureRecognizer { 169 | leftSide = gesture.edges.contains(.left) 170 | } else { 171 | // not sure which way the user is swiping yet, so do nothing 172 | if gesture.xTranslation == 0 { return } 173 | leftSide = gesture.xTranslation > 0 174 | } 175 | 176 | guard let menu = menu(forLeftSide: leftSide) else { return } 177 | menu.present(from: topMostViewController, interactively: true) 178 | } 179 | 180 | activeMenu?.handleMenuPan(gesture, true) 181 | } 182 | 183 | var activeMenu: Menu? { 184 | if leftMenuNavigationController?.isHidden == false { return leftMenuNavigationController } 185 | if rightMenuNavigationController?.isHidden == false { return rightMenuNavigationController } 186 | return nil 187 | } 188 | 189 | func menu(forSide: PresentDirection) -> Menu? { 190 | switch forSide { 191 | case .left: return leftMenuNavigationController 192 | case .right: return rightMenuNavigationController 193 | } 194 | } 195 | 196 | func menu(forLeftSide leftSide: Bool) -> Menu? { 197 | return menu(forSide: leftSide ? .left : .right) 198 | } 199 | 200 | func addScreenEdgeGesture(to view: UIView, edge: UIRectEdge) -> UIScreenEdgePanGestureRecognizer { 201 | if let screenEdgeGestureRecognizer = view.gestureRecognizers?.first(where: { $0 is SideMenuScreenEdgeGestureRecognizer }) as? SideMenuScreenEdgeGestureRecognizer, 202 | screenEdgeGestureRecognizer.edges == edge { 203 | screenEdgeGestureRecognizer.remove() 204 | } 205 | return SideMenuScreenEdgeGestureRecognizer(addTo: view, target: self, action: #selector(handlePresentMenuScreenEdge(_:))).with { 206 | $0.edges = edge 207 | } 208 | } 209 | 210 | @discardableResult func addPresentPanGesture(to view: UIView) -> UIPanGestureRecognizer { 211 | if let panGestureRecognizer = view.gestureRecognizers?.first(where: { $0 is SideMenuPanGestureRecognizer }) as? SideMenuPanGestureRecognizer { 212 | return panGestureRecognizer 213 | } 214 | return SideMenuPanGestureRecognizer(addTo: view, target: self, action: #selector(handlePresentMenuPan(_:))) 215 | } 216 | 217 | var topMostViewController: UIViewController? { 218 | return UIApplication.shared.keyWindow?.rootViewController?.topMostViewController 219 | } 220 | } 221 | 222 | extension SideMenuManager: SideMenuNavigationControllerTransitionDelegate { 223 | 224 | internal func sideMenuTransitionDidDismiss(menu: Menu) { 225 | defer { switching = false } 226 | guard switching, let switchToMenu = self.menu(forLeftSide: !menu.leftSide) else { return } 227 | switchToMenu.present(from: topMostViewController, interactively: true) 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /Pod/Classes/SideMenuNavigationController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SideMenuNavigationController.swift 3 | // 4 | // Created by Jon Kent on 1/14/16. 5 | // Copyright © 2016 Jon Kent. All rights reserved. 6 | // 7 | 8 | import UIKit 9 | 10 | @objc public enum SideMenuPushStyle: Int { case 11 | `default`, 12 | popWhenPossible, 13 | preserve, 14 | preserveAndHideBackButton, 15 | replace, 16 | subMenu 17 | 18 | internal var hidesBackButton: Bool { 19 | switch self { 20 | case .preserveAndHideBackButton, .replace: return true 21 | case .default, .popWhenPossible, .preserve, .subMenu: return false 22 | } 23 | } 24 | } 25 | 26 | internal protocol MenuModel { 27 | /// Prevents the same view controller (or a view controller of the same class) from being pushed more than once. Defaults to true. 28 | var allowPushOfSameClassTwice: Bool { get } 29 | /// Forces menus to always animate when appearing or disappearing, regardless of a pushed view controller's animation. 30 | var alwaysAnimate: Bool { get } 31 | /** 32 | The blur effect style of the menu if the menu's root view controller is a UITableViewController or UICollectionViewController. 33 | 34 | - Note: If you want cells in a UITableViewController menu to show vibrancy, make them a subclass of UITableViewVibrantCell. 35 | */ 36 | var blurEffectStyle: UIBlurEffect.Style? { get } 37 | /// Animation curve of the remaining animation when the menu is partially dismissed with gestures. Default is .easeIn. 38 | var completionCurve: UIView.AnimationCurve { get } 39 | /// Automatically dismisses the menu when another view is presented from it. 40 | var dismissOnPresent: Bool { get } 41 | /// Automatically dismisses the menu when another view controller is pushed from it. 42 | var dismissOnPush: Bool { get } 43 | /// Automatically dismisses the menu when the screen is rotated. 44 | var dismissOnRotation: Bool { get } 45 | /// Automatically dismisses the menu when app goes to the background. 46 | var dismissWhenBackgrounded: Bool { get } 47 | /// Enable or disable a swipe gesture that dismisses the menu. Will not be triggered when `presentingViewControllerUserInteractionEnabled` is set to true. Default is true. 48 | var enableSwipeToDismissGesture: Bool { get } 49 | /// Enable or disable a tap gesture that dismisses the menu. Will not be triggered when `presentingViewControllerUserInteractionEnabled` is set to true. Default is true. 50 | var enableTapToDismissGesture: Bool { get } 51 | /** 52 | The push style of the menu. 53 | 54 | There are six modes in MenuPushStyle: 55 | - defaultBehavior: The view controller is pushed onto the stack. 56 | - popWhenPossible: If a view controller already in the stack is of the same class as the pushed view controller, the stack is instead popped back to the existing view controller. This behavior can help users from getting lost in a deep navigation stack. 57 | - preserve: If a view controller already in the stack is of the same class as the pushed view controller, the existing view controller is pushed to the end of the stack. This behavior is similar to a UITabBarController. 58 | - preserveAndHideBackButton: Same as .preserve and back buttons are automatically hidden. 59 | - replace: Any existing view controllers are released from the stack and replaced with the pushed view controller. Back buttons are automatically hidden. This behavior is ideal if view controllers require a lot of memory or their state doesn't need to be preserved.. 60 | - subMenu: Unlike all other behaviors that push using the menu's presentingViewController, this behavior pushes view controllers within the menu. Use this behavior if you want to display a sub menu. 61 | */ 62 | var pushStyle: SideMenuPushStyle { get } 63 | } 64 | 65 | @objc public protocol SideMenuNavigationControllerDelegate { 66 | @objc optional func sideMenuWillAppear(menu: SideMenuNavigationController, animated: Bool) 67 | @objc optional func sideMenuDidAppear(menu: SideMenuNavigationController, animated: Bool) 68 | @objc optional func sideMenuWillDisappear(menu: SideMenuNavigationController, animated: Bool) 69 | @objc optional func sideMenuDidDisappear(menu: SideMenuNavigationController, animated: Bool) 70 | } 71 | 72 | internal protocol SideMenuNavigationControllerTransitionDelegate: class { 73 | func sideMenuTransitionDidDismiss(menu: Menu) 74 | } 75 | 76 | public struct SideMenuSettings: Model, InitializableStruct { 77 | public var allowPushOfSameClassTwice: Bool = true 78 | public var alwaysAnimate: Bool = true 79 | public var animationOptions: UIView.AnimationOptions = .curveEaseInOut 80 | public var blurEffectStyle: UIBlurEffect.Style? = nil 81 | public var completeGestureDuration: Double = 0.35 82 | public var completionCurve: UIView.AnimationCurve = .easeIn 83 | public var dismissDuration: Double = 0.35 84 | public var dismissOnPresent: Bool = true 85 | public var dismissOnPush: Bool = true 86 | public var dismissOnRotation: Bool = true 87 | public var dismissWhenBackgrounded: Bool = true 88 | public var enableSwipeToDismissGesture: Bool = true 89 | public var enableTapToDismissGesture: Bool = true 90 | public var initialSpringVelocity: CGFloat = 1 91 | public var menuWidth: CGFloat = { 92 | let appScreenRect = UIApplication.shared.keyWindow?.bounds ?? UIWindow().bounds 93 | let minimumSize = min(appScreenRect.width, appScreenRect.height) 94 | return min(round(minimumSize * 0.75), 240) 95 | }() 96 | public var presentingViewControllerUserInteractionEnabled: Bool = false 97 | public var presentingViewControllerUseSnapshot: Bool = false 98 | public var presentDuration: Double = 0.35 99 | public var presentationStyle: SideMenuPresentationStyle = .viewSlideOut 100 | public var pushStyle: SideMenuPushStyle = .default 101 | public var statusBarEndAlpha: CGFloat = 0 102 | public var usingSpringWithDamping: CGFloat = 1 103 | 104 | public init() {} 105 | } 106 | 107 | internal typealias Menu = SideMenuNavigationController 108 | typealias Model = MenuModel & PresentationModel & AnimationModel 109 | 110 | @objcMembers 111 | open class SideMenuNavigationController: UINavigationController { 112 | 113 | private lazy var _leftSide = Protected(false) { [weak self] oldValue, newValue in 114 | guard self?.isHidden != false else { 115 | Print.warning(.property, arguments: .leftSide, required: true) 116 | return oldValue 117 | } 118 | return newValue 119 | } 120 | 121 | private weak var _sideMenuManager: SideMenuManager? 122 | private weak var foundViewController: UIViewController? 123 | private var originalBackgroundColor: UIColor? 124 | private var rotating: Bool = false 125 | private var transitionController: SideMenuTransitionController? 126 | private var transitionInteractive: Bool = false 127 | 128 | /// Delegate for receiving appear and disappear related events. If `nil` the visible view controller that displays a `SideMenuNavigationController` automatically receives these events. 129 | public weak var sideMenuDelegate: SideMenuNavigationControllerDelegate? 130 | 131 | /// The swipe to dismiss gesture. 132 | open private(set) weak var swipeToDismissGesture: UIPanGestureRecognizer? = nil 133 | /// The tap to dismiss gesture. 134 | open private(set) weak var tapToDismissGesture: UITapGestureRecognizer? = nil 135 | 136 | open var sideMenuManager: SideMenuManager { 137 | get { return _sideMenuManager ?? SideMenuManager.default } 138 | set { 139 | newValue.setMenu(self, forLeftSide: leftSide) 140 | 141 | if let sideMenuManager = _sideMenuManager, sideMenuManager !== newValue { 142 | let side = SideMenuManager.PresentDirection(leftSide: leftSide) 143 | Print.warning(.menuAlreadyAssigned, arguments: String(describing: self.self), side.name, String(describing: newValue)) 144 | } 145 | _sideMenuManager = newValue 146 | } 147 | } 148 | 149 | /// The menu settings. 150 | open var settings = SideMenuSettings() { 151 | didSet { 152 | setupBlur() 153 | if !enableSwipeToDismissGesture { 154 | swipeToDismissGesture?.remove() 155 | } 156 | if !enableTapToDismissGesture { 157 | tapToDismissGesture?.remove() 158 | } 159 | } 160 | } 161 | 162 | public override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { 163 | super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) 164 | setup() 165 | } 166 | 167 | public init(rootViewController: UIViewController, settings: SideMenuSettings = SideMenuSettings()) { 168 | self.settings = settings 169 | super.init(rootViewController: rootViewController) 170 | setup() 171 | } 172 | 173 | required public init?(coder aDecoder: NSCoder) { 174 | super.init(coder: aDecoder) 175 | setup() 176 | } 177 | 178 | override open func awakeFromNib() { 179 | super.awakeFromNib() 180 | sideMenuManager.setMenu(self, forLeftSide: leftSide) 181 | } 182 | 183 | override open func viewWillAppear(_ animated: Bool) { 184 | super.viewWillAppear(animated) 185 | 186 | if topViewController == nil { 187 | Print.warning(.emptyMenu) 188 | } 189 | 190 | // Dismiss keyboard to prevent weird keyboard animations from occurring during transition 191 | presentingViewController?.view.endEditing(true) 192 | 193 | foundViewController = nil 194 | activeDelegate?.sideMenuWillAppear?(menu: self, animated: animated) 195 | } 196 | 197 | override open func viewDidAppear(_ animated: Bool) { 198 | super.viewDidAppear(animated) 199 | 200 | // We had presented a view before, so lets dismiss ourselves as already acted upon 201 | if view.isHidden { 202 | dismiss(animated: false, completion: { [weak self] in 203 | self?.view.isHidden = false 204 | }) 205 | } else { 206 | activeDelegate?.sideMenuDidAppear?(menu: self, animated: animated) 207 | } 208 | } 209 | 210 | override open func viewWillDisappear(_ animated: Bool) { 211 | super.viewWillDisappear(animated) 212 | 213 | defer { activeDelegate?.sideMenuWillDisappear?(menu: self, animated: animated) } 214 | 215 | guard !isBeingDismissed else { return } 216 | 217 | // When presenting a view controller from the menu, the menu view gets moved into another transition view above our transition container 218 | // which can break the visual layout we had before. So, we move the menu view back to its original transition view to preserve it. 219 | if let presentingView = presentingViewController?.view, let containerView = presentingView.superview { 220 | containerView.addSubview(view) 221 | } 222 | 223 | if dismissOnPresent { 224 | // We're presenting a view controller from the menu, so we need to hide the menu so it isn't showing when the presented view is dismissed. 225 | transitionController?.transition(presenting: false, animated: animated, alongsideTransition: { [weak self] in 226 | guard let self = self else { return } 227 | self.activeDelegate?.sideMenuWillDisappear?(menu: self, animated: animated) 228 | }, complete: false, completion: { [weak self] _ in 229 | guard let self = self else { return } 230 | self.activeDelegate?.sideMenuDidDisappear?(menu: self, animated: animated) 231 | self.view.isHidden = true 232 | }) 233 | } 234 | } 235 | 236 | override open func viewDidDisappear(_ animated: Bool) { 237 | super.viewDidDisappear(animated) 238 | 239 | // Work-around: if the menu is dismissed without animation the transition logic is never called to restore the 240 | // the view hierarchy leaving the screen black/empty. This is because the transition moves views within a container 241 | // view, but dismissing without animation removes the container view before the original hierarchy is restored. 242 | // This check corrects that. 243 | if isBeingDismissed { 244 | transitionController?.transition(presenting: false, animated: false) 245 | } 246 | 247 | // Clear selection on UITableViewControllers when reappearing using custom transitions 248 | if let tableViewController = topViewController as? UITableViewController, 249 | let tableView = tableViewController.tableView, 250 | let indexPaths = tableView.indexPathsForSelectedRows, 251 | tableViewController.clearsSelectionOnViewWillAppear { 252 | indexPaths.forEach { tableView.deselectRow(at: $0, animated: false) } 253 | } 254 | 255 | activeDelegate?.sideMenuDidDisappear?(menu: self, animated: animated) 256 | 257 | if isBeingDismissed { 258 | transitionController = nil 259 | } else if dismissOnPresent { 260 | view.isHidden = true 261 | } 262 | } 263 | 264 | override open func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { 265 | super.viewWillTransition(to: size, with: coordinator) 266 | 267 | // Don't bother resizing if the view isn't visible 268 | guard let transitionController = transitionController, !view.isHidden else { return } 269 | 270 | rotating = true 271 | 272 | let dismiss = self.presentingViewControllerUseSnapshot || self.dismissOnRotation 273 | coordinator.animate(alongsideTransition: { _ in 274 | if dismiss { 275 | transitionController.transition(presenting: false, animated: false, complete: false) 276 | } else { 277 | transitionController.layout() 278 | } 279 | }) { [weak self] _ in 280 | guard let self = self else { return } 281 | if dismiss { 282 | self.dismissMenu(animated: false) 283 | } 284 | self.rotating = false 285 | } 286 | } 287 | 288 | open override func viewWillLayoutSubviews() { 289 | super.viewWillLayoutSubviews() 290 | transitionController?.layout() 291 | } 292 | 293 | override open func pushViewController(_ viewController: UIViewController, animated: Bool) { 294 | guard viewControllers.count > 0 else { 295 | // NOTE: pushViewController is called by init(rootViewController: UIViewController) 296 | // so we must perform the normal super method in this case 297 | return super.pushViewController(viewController, animated: animated) 298 | } 299 | 300 | var alongsideTransition: (() -> Void)? = nil 301 | if dismissOnPush { 302 | alongsideTransition = { [weak self] in 303 | guard let self = self else { return } 304 | self.dismissAnimation(animated: animated || self.alwaysAnimate) 305 | } 306 | } 307 | 308 | let pushed = SideMenuPushCoordinator(config: 309 | .init( 310 | allowPushOfSameClassTwice: allowPushOfSameClassTwice, 311 | alongsideTransition: alongsideTransition, 312 | animated: animated, 313 | fromViewController: self, 314 | pushStyle: pushStyle, 315 | toViewController: viewController 316 | ) 317 | ).start() 318 | 319 | if !pushed { 320 | super.pushViewController(viewController, animated: animated) 321 | } 322 | } 323 | 324 | override open var transitioningDelegate: UIViewControllerTransitioningDelegate? { 325 | get { 326 | guard transitionController == nil else { return transitionController } 327 | transitionController = SideMenuTransitionController(leftSide: leftSide, config: settings) 328 | transitionController?.delegate = self 329 | transitionController?.interactive = transitionInteractive 330 | transitionInteractive = false 331 | return transitionController 332 | } 333 | set { Print.warning(.transitioningDelegate, required: true) } 334 | } 335 | } 336 | 337 | // Interface 338 | extension SideMenuNavigationController: Model { 339 | 340 | @IBInspectable open var allowPushOfSameClassTwice: Bool { 341 | get { return settings.allowPushOfSameClassTwice } 342 | set { settings.allowPushOfSameClassTwice = newValue } 343 | } 344 | 345 | @IBInspectable open var alwaysAnimate: Bool { 346 | get { return settings.alwaysAnimate } 347 | set { settings.alwaysAnimate = newValue } 348 | } 349 | 350 | @IBInspectable open var animationOptions: UIView.AnimationOptions { 351 | get { return settings.animationOptions } 352 | set { settings.animationOptions = newValue } 353 | } 354 | 355 | open var blurEffectStyle: UIBlurEffect.Style? { 356 | get { return settings.blurEffectStyle } 357 | set { settings.blurEffectStyle = newValue } 358 | } 359 | 360 | @IBInspectable open var completeGestureDuration: Double { 361 | get { return settings.completeGestureDuration } 362 | set { settings.completeGestureDuration = newValue } 363 | } 364 | 365 | @IBInspectable open var completionCurve: UIView.AnimationCurve { 366 | get { return settings.completionCurve } 367 | set { settings.completionCurve = newValue } 368 | } 369 | 370 | @IBInspectable open var dismissDuration: Double { 371 | get { return settings.dismissDuration } 372 | set { settings.dismissDuration = newValue } 373 | } 374 | 375 | @IBInspectable open var dismissOnPresent: Bool { 376 | get { return settings.dismissOnPresent } 377 | set { settings.dismissOnPresent = newValue } 378 | } 379 | 380 | @IBInspectable open var dismissOnPush: Bool { 381 | get { return settings.dismissOnPush } 382 | set { settings.dismissOnPush = newValue } 383 | } 384 | 385 | @IBInspectable open var dismissOnRotation: Bool { 386 | get { return settings.dismissOnRotation } 387 | set { settings.dismissOnRotation = newValue } 388 | } 389 | 390 | @IBInspectable open var dismissWhenBackgrounded: Bool { 391 | get { return settings.dismissWhenBackgrounded } 392 | set { settings.dismissWhenBackgrounded = newValue } 393 | } 394 | 395 | @IBInspectable open var enableSwipeToDismissGesture: Bool { 396 | get { return settings.enableSwipeToDismissGesture } 397 | set { settings.enableSwipeToDismissGesture = newValue } 398 | } 399 | 400 | @IBInspectable open var enableTapToDismissGesture: Bool { 401 | get { return settings.enableTapToDismissGesture } 402 | set { settings.enableTapToDismissGesture = newValue } 403 | } 404 | 405 | @IBInspectable open var initialSpringVelocity: CGFloat { 406 | get { return settings.initialSpringVelocity } 407 | set { settings.initialSpringVelocity = newValue } 408 | } 409 | 410 | /// Whether the menu appears on the right or left side of the screen. Right is the default. This property cannot be changed after the menu has loaded. 411 | @IBInspectable open var leftSide: Bool { 412 | get { return _leftSide.value } 413 | set { _leftSide.value = newValue } 414 | } 415 | 416 | /// Indicates if the menu is anywhere in the view hierarchy, even if covered by another view controller. 417 | open override var isHidden: Bool { 418 | return super.isHidden 419 | } 420 | 421 | @IBInspectable open var menuWidth: CGFloat { 422 | get { return settings.menuWidth } 423 | set { settings.menuWidth = newValue } 424 | } 425 | 426 | @IBInspectable open var presentingViewControllerUserInteractionEnabled: Bool { 427 | get { return settings.presentingViewControllerUserInteractionEnabled } 428 | set { settings.presentingViewControllerUserInteractionEnabled = newValue } 429 | } 430 | 431 | @IBInspectable open var presentingViewControllerUseSnapshot: Bool { 432 | get { return settings.presentingViewControllerUseSnapshot } 433 | set { settings.presentingViewControllerUseSnapshot = newValue } 434 | } 435 | 436 | @IBInspectable open var presentDuration: Double { 437 | get { return settings.presentDuration } 438 | set { settings.presentDuration = newValue } 439 | } 440 | 441 | open var presentationStyle: SideMenuPresentationStyle { 442 | get { return settings.presentationStyle } 443 | set { settings.presentationStyle = newValue } 444 | } 445 | 446 | @IBInspectable open var pushStyle: SideMenuPushStyle { 447 | get { return settings.pushStyle } 448 | set { settings.pushStyle = newValue } 449 | } 450 | 451 | @IBInspectable open var statusBarEndAlpha: CGFloat { 452 | get { return settings.statusBarEndAlpha } 453 | set { settings.statusBarEndAlpha = newValue } 454 | } 455 | 456 | @IBInspectable open var usingSpringWithDamping: CGFloat { 457 | get { return settings.usingSpringWithDamping } 458 | set { settings.usingSpringWithDamping = newValue } 459 | } 460 | } 461 | 462 | extension SideMenuNavigationController: SideMenuTransitionControllerDelegate { 463 | 464 | func sideMenuTransitionController(_ transitionController: SideMenuTransitionController, didDismiss viewController: UIViewController) { 465 | sideMenuManager.sideMenuTransitionDidDismiss(menu: self) 466 | } 467 | 468 | func sideMenuTransitionController(_ transitionController: SideMenuTransitionController, didPresent viewController: UIViewController) { 469 | swipeToDismissGesture?.remove() 470 | swipeToDismissGesture = addSwipeToDismissGesture(to: view.superview) 471 | tapToDismissGesture = addTapToDismissGesture(to: view.superview) 472 | } 473 | } 474 | 475 | internal extension SideMenuNavigationController { 476 | 477 | func handleMenuPan(_ gesture: UIPanGestureRecognizer, _ presenting: Bool) { 478 | let width = menuWidth 479 | let distance = gesture.xTranslation / width 480 | let progress = max(min(distance * factor(presenting), 1), 0) 481 | switch (gesture.state) { 482 | case .began: 483 | if !presenting { 484 | dismissMenu(interactively: true) 485 | } 486 | fallthrough 487 | case .changed: 488 | transitionController?.handle(state: .update(progress: progress)) 489 | case .ended: 490 | let velocity = gesture.xVelocity * factor(presenting) 491 | let finished = velocity >= 100 || velocity >= -50 && abs(progress) >= 0.5 492 | transitionController?.handle(state: finished ? .finish : .cancel) 493 | default: 494 | transitionController?.handle(state: .cancel) 495 | } 496 | } 497 | 498 | func cancelMenuPan(_ gesture: UIPanGestureRecognizer) { 499 | transitionController?.handle(state: .cancel) 500 | } 501 | 502 | func dismissMenu(animated flag: Bool = true, interactively: Bool = false, completion: (() -> Void)? = nil) { 503 | guard !isHidden else { return } 504 | transitionController?.interactive = interactively 505 | dismiss(animated: flag, completion: completion) 506 | } 507 | 508 | // Note: although this method is syntactically reversed it allows the interactive property to scoped privately 509 | func present(from viewController: UIViewController?, interactively: Bool, completion: (() -> Void)? = nil) { 510 | guard let viewController = viewController else { return } 511 | transitionInteractive = interactively 512 | viewController.present(self, animated: true, completion: completion) 513 | } 514 | } 515 | 516 | private extension SideMenuNavigationController { 517 | 518 | weak var activeDelegate: SideMenuNavigationControllerDelegate? { 519 | guard !view.isHidden else { return nil } 520 | if let sideMenuDelegate = sideMenuDelegate { return sideMenuDelegate } 521 | return findViewController as? SideMenuNavigationControllerDelegate 522 | } 523 | 524 | var findViewController: UIViewController? { 525 | foundViewController = foundViewController ?? presentingViewController?.activeViewController 526 | return foundViewController 527 | } 528 | 529 | func dismissAnimation(animated: Bool) { 530 | transitionController?.transition(presenting: false, animated: animated, alongsideTransition: { [weak self] in 531 | guard let self = self else { return } 532 | self.activeDelegate?.sideMenuWillDisappear?(menu: self, animated: animated) 533 | }, completion: { [weak self] _ in 534 | guard let self = self else { return } 535 | self.activeDelegate?.sideMenuDidDisappear?(menu: self, animated: animated) 536 | self.dismiss(animated: false, completion: nil) 537 | self.foundViewController = nil 538 | }) 539 | } 540 | 541 | func setup() { 542 | modalPresentationStyle = .overFullScreen 543 | 544 | setupBlur() 545 | if #available(iOS 13.0, *) {} else { 546 | registerForNotifications() 547 | } 548 | } 549 | 550 | func setupBlur() { 551 | removeBlur() 552 | 553 | guard let blurEffectStyle = blurEffectStyle, 554 | let view = topViewController?.view, 555 | !UIAccessibility.isReduceTransparencyEnabled else { 556 | return 557 | } 558 | 559 | originalBackgroundColor = originalBackgroundColor ?? view.backgroundColor 560 | 561 | let blurEffect = UIBlurEffect(style: blurEffectStyle) 562 | let blurView = UIVisualEffectView(effect: blurEffect) 563 | view.backgroundColor = UIColor.clear 564 | if let tableViewController = topViewController as? UITableViewController { 565 | tableViewController.tableView.backgroundView = blurView 566 | tableViewController.tableView.separatorEffect = UIVibrancyEffect(blurEffect: blurEffect) 567 | tableViewController.tableView.reloadData() 568 | } else { 569 | blurView.autoresizingMask = [.flexibleHeight, .flexibleWidth] 570 | blurView.frame = view.bounds 571 | view.insertSubview(blurView, at: 0) 572 | } 573 | } 574 | 575 | func removeBlur() { 576 | guard let originalBackgroundColor = originalBackgroundColor, 577 | let view = topViewController?.view else { 578 | return 579 | } 580 | 581 | self.originalBackgroundColor = nil 582 | view.backgroundColor = originalBackgroundColor 583 | 584 | if let tableViewController = topViewController as? UITableViewController { 585 | tableViewController.tableView.backgroundView = nil 586 | tableViewController.tableView.separatorEffect = nil 587 | tableViewController.tableView.reloadData() 588 | } else if let blurView = view.subviews.first as? UIVisualEffectView { 589 | blurView.removeFromSuperview() 590 | } 591 | } 592 | 593 | @available(iOS, deprecated: 13.0) 594 | func registerForNotifications() { 595 | NotificationCenter.default.removeObserver(self) 596 | 597 | [UIApplication.willChangeStatusBarFrameNotification, 598 | UIApplication.didEnterBackgroundNotification].forEach { 599 | NotificationCenter.default.addObserver(self, selector: #selector(handleNotification), name: $0, object: nil) 600 | } 601 | } 602 | 603 | @available(iOS, deprecated: 13.0) 604 | @objc func handleNotification(notification: NSNotification) { 605 | guard isHidden else { return } 606 | 607 | switch notification.name { 608 | case UIApplication.willChangeStatusBarFrameNotification: 609 | // Dismiss for in-call status bar changes but not rotation 610 | if !rotating { 611 | dismissMenu() 612 | } 613 | case UIApplication.didEnterBackgroundNotification: 614 | if dismissWhenBackgrounded { 615 | dismissMenu() 616 | } 617 | default: break 618 | } 619 | } 620 | 621 | @discardableResult func addSwipeToDismissGesture(to view: UIView?) -> UIPanGestureRecognizer? { 622 | guard enableSwipeToDismissGesture else { return nil } 623 | return UIPanGestureRecognizer(addTo: view, target: self, action: #selector(handleDismissMenuPan(_:)))?.with { 624 | $0.cancelsTouchesInView = false 625 | } 626 | } 627 | 628 | @discardableResult func addTapToDismissGesture(to view: UIView?) -> UITapGestureRecognizer? { 629 | guard enableTapToDismissGesture else { return nil } 630 | return UITapGestureRecognizer(addTo: view, target: self, action: #selector(handleDismissMenuTap(_:)))?.with { 631 | $0.cancelsTouchesInView = false 632 | } 633 | } 634 | 635 | @objc func handleDismissMenuTap(_ tap: UITapGestureRecognizer) { 636 | let hitTest = view.window?.hitTest(tap.location(in: view.superview), with: nil) 637 | guard hitTest == view.superview else { return } 638 | dismissMenu() 639 | } 640 | 641 | @objc func handleDismissMenuPan(_ gesture: UIPanGestureRecognizer) { 642 | handleMenuPan(gesture, false) 643 | } 644 | 645 | func factor(_ presenting: Bool) -> CGFloat { 646 | return presenting ? presentFactor : hideFactor 647 | } 648 | 649 | var presentFactor: CGFloat { 650 | return leftSide ? 1 : -1 651 | } 652 | 653 | var hideFactor: CGFloat { 654 | return -presentFactor 655 | } 656 | } 657 | -------------------------------------------------------------------------------- /Pod/Classes/SideMenuPresentationController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BasePresentationController.swift 3 | // SideMenu 4 | // 5 | // Created by Jon Kent on 10/20/18. 6 | // 7 | 8 | import UIKit 9 | 10 | internal protocol PresentationModel { 11 | /// Draws `presentStyle.backgroundColor` behind the status bar. Default is 1. 12 | var statusBarEndAlpha: CGFloat { get } 13 | /// Enable or disable interaction with the presenting view controller while the menu is displayed. Enabling may make it difficult to dismiss the menu or cause exceptions if the user tries to present and already presented menu. `presentingViewControllerUseSnapshot` must also set to false. Default is false. 14 | var presentingViewControllerUserInteractionEnabled: Bool { get } 15 | /// Use a snapshot for the presenting vierw controller while the menu is displayed. Useful when layout changes occur during transitions. Not recommended for apps that support rotation. Default is false. 16 | var presentingViewControllerUseSnapshot: Bool { get } 17 | /// The presentation style of the menu. 18 | var presentationStyle: SideMenuPresentationStyle { get } 19 | /// Width of the menu when presented on screen, showing the existing view controller in the remaining space. Default is zero. 20 | var menuWidth: CGFloat { get } 21 | } 22 | 23 | internal protocol SideMenuPresentationControllerDelegate: class { 24 | func sideMenuPresentationControllerDidTap(_ presentationController: SideMenuPresentationController) 25 | func sideMenuPresentationController(_ presentationController: SideMenuPresentationController, didPanWith gesture: UIPanGestureRecognizer) 26 | } 27 | 28 | internal final class SideMenuPresentationController { 29 | 30 | private let config: PresentationModel 31 | private weak var containerView: UIView? 32 | private var interactivePopGestureRecognizerEnabled: Bool? 33 | private var clipsToBounds: Bool? 34 | private let leftSide: Bool 35 | private weak var presentedViewController: UIViewController? 36 | private weak var presentingViewController: UIViewController? 37 | 38 | private lazy var snapshotView: UIView? = { 39 | guard config.presentingViewControllerUseSnapshot, 40 | let view = presentingViewController?.view.snapshotView(afterScreenUpdates: true) else { 41 | return nil 42 | } 43 | 44 | view.autoresizingMask = [.flexibleHeight, .flexibleWidth] 45 | return view 46 | }() 47 | 48 | private lazy var statusBarView: UIView? = { 49 | guard config.statusBarEndAlpha > .leastNonzeroMagnitude else { return nil } 50 | 51 | return UIView { 52 | $0.backgroundColor = config.presentationStyle.backgroundColor 53 | $0.autoresizingMask = [.flexibleHeight, .flexibleWidth] 54 | $0.isUserInteractionEnabled = false 55 | } 56 | }() 57 | 58 | required init(config: PresentationModel, leftSide: Bool, presentedViewController: UIViewController, presentingViewController: UIViewController, containerView: UIView) { 59 | self.config = config 60 | self.containerView = containerView 61 | self.leftSide = leftSide 62 | self.presentedViewController = presentedViewController 63 | self.presentingViewController = presentingViewController 64 | } 65 | 66 | deinit { 67 | guard presentedViewController?.isHidden == false else { return } 68 | 69 | // Presentations must be reversed to preserve user experience 70 | dismissalTransitionWillBegin() 71 | dismissalTransition() 72 | dismissalTransitionDidEnd(true) 73 | } 74 | 75 | func containerViewWillLayoutSubviews() { 76 | guard let containerView = containerView, 77 | let presentedViewController = presentedViewController, 78 | let presentingViewController = presentingViewController 79 | else { return } 80 | 81 | presentedViewController.view.untransform { 82 | presentedViewController.view.frame = frameOfPresentedViewInContainerView 83 | } 84 | presentingViewController.view.untransform { 85 | presentingViewController.view.frame = frameOfPresentingViewInContainerView 86 | snapshotView?.frame = presentingViewController.view.bounds 87 | } 88 | 89 | guard let statusBarView = statusBarView else { return } 90 | 91 | var statusBarFrame: CGRect = self.statusBarFrame 92 | statusBarFrame.size.height -= containerView.frame.minY 93 | statusBarView.frame = statusBarFrame 94 | } 95 | 96 | func presentationTransitionWillBegin() { 97 | guard let containerView = containerView, 98 | let presentedViewController = presentedViewController, 99 | let presentingViewController = presentingViewController 100 | else { return } 101 | 102 | if let snapshotView = snapshotView { 103 | presentingViewController.view.addSubview(snapshotView) 104 | } 105 | 106 | presentingViewController.view.isUserInteractionEnabled = config.presentingViewControllerUserInteractionEnabled 107 | containerView.backgroundColor = config.presentationStyle.backgroundColor 108 | 109 | layerViews() 110 | 111 | if let statusBarView = statusBarView { 112 | containerView.addSubview(statusBarView) 113 | } 114 | 115 | dismissalTransition() 116 | config.presentationStyle.presentationTransitionWillBegin(to: presentedViewController, from: presentingViewController) 117 | } 118 | 119 | func presentationTransition() { 120 | guard let presentedViewController = presentedViewController, 121 | let presentingViewController = presentingViewController 122 | else { return } 123 | 124 | transition( 125 | to: presentedViewController, 126 | from: presentingViewController, 127 | alpha: config.presentationStyle.presentingEndAlpha, 128 | statusBarAlpha: config.statusBarEndAlpha, 129 | scale: config.presentationStyle.presentingScaleFactor, 130 | translate: config.presentationStyle.presentingTranslateFactor 131 | ) 132 | 133 | config.presentationStyle.presentationTransition(to: presentedViewController, from: presentingViewController) 134 | } 135 | 136 | func presentationTransitionDidEnd(_ completed: Bool) { 137 | guard completed else { 138 | snapshotView?.removeFromSuperview() 139 | dismissalTransitionDidEnd(!completed) 140 | return 141 | } 142 | 143 | guard let presentedViewController = presentedViewController, 144 | let presentingViewController = presentingViewController 145 | else { return } 146 | 147 | addParallax(to: presentingViewController.view) 148 | 149 | if let topNavigationController = presentingViewController as? UINavigationController { 150 | interactivePopGestureRecognizerEnabled = interactivePopGestureRecognizerEnabled ?? topNavigationController.interactivePopGestureRecognizer?.isEnabled 151 | topNavigationController.interactivePopGestureRecognizer?.isEnabled = false 152 | } 153 | 154 | containerViewWillLayoutSubviews() 155 | config.presentationStyle.presentationTransitionDidEnd(to: presentedViewController, from: presentingViewController, completed) 156 | } 157 | 158 | func dismissalTransitionWillBegin() { 159 | snapshotView?.removeFromSuperview() 160 | presentationTransition() 161 | 162 | guard let presentedViewController = presentedViewController, 163 | let presentingViewController = presentingViewController 164 | else { return } 165 | 166 | config.presentationStyle.dismissalTransitionWillBegin(to: presentedViewController, from: presentingViewController) 167 | } 168 | 169 | func dismissalTransition() { 170 | guard let presentedViewController = presentedViewController, 171 | let presentingViewController = presentingViewController 172 | else { return } 173 | 174 | transition( 175 | to: presentingViewController, 176 | from: presentedViewController, 177 | alpha: config.presentationStyle.menuStartAlpha, 178 | statusBarAlpha: 0, 179 | scale: config.presentationStyle.menuScaleFactor, 180 | translate: config.presentationStyle.menuTranslateFactor 181 | ) 182 | 183 | config.presentationStyle.dismissalTransition(to: presentedViewController, from: presentingViewController) 184 | } 185 | 186 | func dismissalTransitionDidEnd(_ completed: Bool) { 187 | guard completed else { 188 | if let snapshotView = snapshotView, let presentingViewController = presentingViewController { 189 | presentingViewController.view.addSubview(snapshotView) 190 | } 191 | presentationTransitionDidEnd(!completed) 192 | return 193 | } 194 | 195 | guard let presentedViewController = presentedViewController, 196 | let presentingViewController = presentingViewController 197 | else { return } 198 | 199 | statusBarView?.removeFromSuperview() 200 | removeStyles(from: presentingViewController.containerViewController.view) 201 | 202 | if let interactivePopGestureRecognizerEnabled = interactivePopGestureRecognizerEnabled, 203 | let topNavigationController = presentingViewController as? UINavigationController { 204 | topNavigationController.interactivePopGestureRecognizer?.isEnabled = interactivePopGestureRecognizerEnabled 205 | } 206 | 207 | presentingViewController.view.isUserInteractionEnabled = true 208 | config.presentationStyle.dismissalTransitionDidEnd(to: presentedViewController, from: presentingViewController, completed) 209 | } 210 | } 211 | 212 | private extension SideMenuPresentationController { 213 | 214 | var statusBarFrame: CGRect { 215 | if #available(iOS 13.0, *) { 216 | return containerView?.window?.windowScene?.statusBarManager?.statusBarFrame ?? .zero 217 | } else { 218 | return UIApplication.shared.statusBarFrame 219 | } 220 | } 221 | 222 | var frameOfPresentedViewInContainerView: CGRect { 223 | guard let containerView = containerView else { return .zero } 224 | var rect = containerView.bounds 225 | rect.origin.x = leftSide ? 0 : rect.width - config.menuWidth 226 | rect.size.width = config.menuWidth 227 | return rect 228 | } 229 | 230 | var frameOfPresentingViewInContainerView: CGRect { 231 | guard let containerView = containerView else { return .zero } 232 | var rect = containerView.frame 233 | if containerView.superview != nil, containerView.frame.minY > .ulpOfOne { 234 | let statusBarOffset = statusBarFrame.height - rect.minY 235 | rect.origin.y = statusBarOffset 236 | rect.size.height -= statusBarOffset 237 | } 238 | return rect 239 | } 240 | 241 | func transition(to: UIViewController, from: UIViewController, alpha: CGFloat, statusBarAlpha: CGFloat, scale: CGFloat, translate: CGFloat) { 242 | containerViewWillLayoutSubviews() 243 | 244 | to.view.transform = .identity 245 | to.view.alpha = 1 246 | 247 | let x = (leftSide ? 1 : -1) * config.menuWidth * translate 248 | from.view.alpha = alpha 249 | from.view.transform = CGAffineTransform 250 | .identity 251 | .translatedBy(x: x, y: 0) 252 | .scaledBy(x: scale, y: scale) 253 | 254 | statusBarView?.alpha = statusBarAlpha 255 | } 256 | 257 | func layerViews() { 258 | guard let presentedViewController = presentedViewController, 259 | let presentingViewController = presentingViewController 260 | else { return } 261 | 262 | statusBarView?.layer.zPosition = 2 263 | 264 | if config.presentationStyle.menuOnTop { 265 | addShadow(to: presentedViewController.view) 266 | presentedViewController.view.layer.zPosition = 1 267 | } else { 268 | addShadow(to: presentingViewController.view) 269 | presentedViewController.view.layer.zPosition = -1 270 | } 271 | } 272 | 273 | func addShadow(to view: UIView) { 274 | view.layer.shadowColor = config.presentationStyle.onTopShadowColor.cgColor 275 | view.layer.shadowRadius = config.presentationStyle.onTopShadowRadius 276 | view.layer.shadowOpacity = config.presentationStyle.onTopShadowOpacity 277 | view.layer.shadowOffset = config.presentationStyle.onTopShadowOffset 278 | clipsToBounds = clipsToBounds ?? view.clipsToBounds 279 | view.clipsToBounds = false 280 | } 281 | 282 | func addParallax(to view: UIView) { 283 | var effects: [UIInterpolatingMotionEffect] = [] 284 | 285 | let x = config.presentationStyle.presentingParallaxStrength.width 286 | if x > 0 { 287 | let horizontal = UIInterpolatingMotionEffect(keyPath: "center.x", type: .tiltAlongHorizontalAxis) 288 | horizontal.minimumRelativeValue = -x 289 | horizontal.maximumRelativeValue = x 290 | effects.append(horizontal) 291 | } 292 | 293 | let y = config.presentationStyle.presentingParallaxStrength.height 294 | if y > 0 { 295 | let vertical = UIInterpolatingMotionEffect(keyPath: "center.y", type: .tiltAlongVerticalAxis) 296 | vertical.minimumRelativeValue = -y 297 | vertical.maximumRelativeValue = y 298 | effects.append(vertical) 299 | } 300 | 301 | if effects.count > 0 { 302 | let group = UIMotionEffectGroup() 303 | group.motionEffects = effects 304 | view.motionEffects.removeAll() 305 | view.addMotionEffect(group) 306 | } 307 | } 308 | 309 | func removeStyles(from view: UIView) { 310 | view.motionEffects.removeAll() 311 | view.layer.shadowOpacity = 0 312 | view.layer.shadowOpacity = 0 313 | view.clipsToBounds = clipsToBounds ?? true 314 | clipsToBounds = false 315 | } 316 | } 317 | -------------------------------------------------------------------------------- /Pod/Classes/SideMenuPresentationStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SideMenuPresentStyle.swift 3 | // SideMenu 4 | // 5 | // Created by Jon Kent on 7/2/19. 6 | // 7 | 8 | import UIKit 9 | 10 | @objcMembers 11 | open class SideMenuPresentationStyle: InitializableClass { 12 | /// Background color behind the views and status bar color 13 | open var backgroundColor: UIColor = .black 14 | /// The starting alpha value of the menu before it appears 15 | open var menuStartAlpha: CGFloat = 1 16 | /// Whether or not the menu is on top. If false, the presenting view is on top. Shadows are applied to the view on top. 17 | open var menuOnTop: Bool = false 18 | /// The amount the menu is translated along the x-axis. Zero is stationary, negative values are off-screen, positive values are on screen. 19 | open var menuTranslateFactor: CGFloat = 0 20 | /// The amount the menu is scaled. Less than one shrinks the view, larger than one grows the view. 21 | open var menuScaleFactor: CGFloat = 1 22 | /// The color of the shadow applied to the top most view. 23 | open var onTopShadowColor: UIColor = .black 24 | /// The radius of the shadow applied to the top most view. 25 | open var onTopShadowRadius: CGFloat = 5 26 | /// The opacity of the shadow applied to the top most view. 27 | open var onTopShadowOpacity: Float = 0 28 | /// The offset of the shadow applied to the top most view. 29 | open var onTopShadowOffset: CGSize = .zero 30 | /// The ending alpha of the presenting view when the menu is fully displayed. 31 | open var presentingEndAlpha: CGFloat = 1 32 | /// The amount the presenting view is translated along the x-axis. Zero is stationary, negative values are off-screen, positive values are on screen. 33 | open var presentingTranslateFactor: CGFloat = 0 34 | /// The amount the presenting view is scaled. Less than one shrinks the view, larger than one grows the view. 35 | open var presentingScaleFactor: CGFloat = 1 36 | /// The strength of the parallax effect on the presenting view once the menu is displayed. 37 | open var presentingParallaxStrength: CGSize = .zero 38 | 39 | required public init() {} 40 | 41 | /// This method is called just before the presentation transition begins. Use this to setup any animations. The super method does not need to be called. 42 | open func presentationTransitionWillBegin(to presentedViewController: UIViewController, from presentingViewController: UIViewController) {} 43 | /// This method is called during the presentation animation. Use this to animate anything alongside the menu animation. The super method does not need to be called. 44 | open func presentationTransition(to presentedViewController: UIViewController, from presentingViewController: UIViewController) {} 45 | /// This method is called when the presentation transition ends. Use this to finish any animations. The super method does not need to be called. 46 | open func presentationTransitionDidEnd(to presentedViewController: UIViewController, from presentingViewController: UIViewController, _ completed: Bool) {} 47 | /// This method is called just before the dismissal transition begins. Use this to setup any animations. The super method does not need to be called. 48 | open func dismissalTransitionWillBegin(to presentedViewController: UIViewController, from presentingViewController: UIViewController) {} 49 | /// This method is called during the dismissal animation. Use this to animate anything alongside the menu animation. The super method does not need to be called. 50 | open func dismissalTransition(to presentedViewController: UIViewController, from presentingViewController: UIViewController) {} 51 | /// This method is called when the dismissal transition ends. Use this to finish any animations. The super method does not need to be called. 52 | open func dismissalTransitionDidEnd(to presentedViewController: UIViewController, from presentingViewController: UIViewController, _ completed: Bool) {} 53 | } 54 | 55 | public extension SideMenuPresentationStyle { 56 | /// Menu slides in over the existing view. 57 | static var menuSlideIn: SideMenuPresentationStyle { 58 | return SideMenuPresentationStyle { 59 | $0.menuOnTop = true 60 | $0.menuTranslateFactor = -1 61 | } 62 | } 63 | /// The existing view slides out to reveal the menu underneath. 64 | static var viewSlideOut: SideMenuPresentationStyle { 65 | return SideMenuPresentationStyle { 66 | $0.presentingTranslateFactor = 1 67 | } 68 | } 69 | /// The existing view slides out while the menu slides in. 70 | static var viewSlideOutMenuIn: SideMenuPresentationStyle { 71 | return SideMenuPresentationStyle { 72 | $0.menuTranslateFactor = -1 73 | $0.presentingTranslateFactor = 1 74 | } 75 | } 76 | /// The menu dissolves in over the existing view. 77 | static var menuDissolveIn: SideMenuPresentationStyle { 78 | return SideMenuPresentationStyle { 79 | $0.menuStartAlpha = 0 80 | $0.menuOnTop = true 81 | } 82 | } 83 | /// The existing view slides out while the menu partially slides in. 84 | static var viewSlideOutMenuPartialIn: SideMenuPresentationStyle { 85 | return SideMenuPresentationStyle { 86 | $0.menuTranslateFactor = -0.5 87 | $0.presentingTranslateFactor = 1 88 | } 89 | } 90 | /// The existing view slides out while the menu slides out from under it. 91 | static var viewSlideOutMenuOut: SideMenuPresentationStyle { 92 | return SideMenuPresentationStyle { 93 | $0.menuTranslateFactor = 1 94 | $0.presentingTranslateFactor = 1 95 | } 96 | } 97 | /// The existing view slides out while the menu partially slides out from under it. 98 | static var viewSlideOutMenuPartialOut: SideMenuPresentationStyle { 99 | return SideMenuPresentationStyle { 100 | $0.menuTranslateFactor = 0.5 101 | $0.presentingTranslateFactor = 1 102 | } 103 | } 104 | /// The existing view slides out and shrinks to reveal the menu underneath. 105 | static var viewSlideOutMenuZoom: SideMenuPresentationStyle { 106 | return SideMenuPresentationStyle { 107 | $0.presentingTranslateFactor = 1 108 | $0.menuScaleFactor = 0.95 109 | $0.menuOnTop = true 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /Pod/Classes/SideMenuPushCoordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PushCoordinator.swift 3 | // SideMenu 4 | // 5 | // Created by Jon Kent on 9/4/19. 6 | // 7 | 8 | import UIKit 9 | 10 | protocol CoordinatorModel { 11 | var animated: Bool { get } 12 | var fromViewController: UIViewController { get } 13 | var toViewController: UIViewController { get } 14 | } 15 | 16 | protocol Coordinator { 17 | associatedtype Model: CoordinatorModel 18 | 19 | init(config: Model) 20 | @discardableResult func start() -> Bool 21 | } 22 | 23 | internal final class SideMenuPushCoordinator: Coordinator { 24 | 25 | struct Model: CoordinatorModel { 26 | var allowPushOfSameClassTwice: Bool 27 | var alongsideTransition: (() -> Void)? 28 | var animated: Bool 29 | var fromViewController: UIViewController 30 | var pushStyle: SideMenuPushStyle 31 | var toViewController: UIViewController 32 | } 33 | 34 | private let config: Model 35 | 36 | init(config: Model) { 37 | self.config = config 38 | } 39 | 40 | @discardableResult func start() -> Bool { 41 | guard config.pushStyle != .subMenu, 42 | let fromNavigationController = config.fromViewController as? UINavigationController else { 43 | return false 44 | } 45 | let toViewController = config.toViewController 46 | let presentingViewController = fromNavigationController.presentingViewController 47 | let splitViewController = presentingViewController as? UISplitViewController 48 | let tabBarController = presentingViewController as? UITabBarController 49 | let potentialNavigationController = (splitViewController?.viewControllers.first ?? tabBarController?.selectedViewController) ?? presentingViewController 50 | guard let navigationController = potentialNavigationController as? UINavigationController else { 51 | Print.warning(.cannotPush, arguments: String(describing: potentialNavigationController.self), required: true) 52 | return false 53 | } 54 | 55 | // To avoid overlapping dismiss & pop/push calls, create a transaction block where the menu 56 | // is dismissed after showing the appropriate screen 57 | CATransaction.begin() 58 | defer { CATransaction.commit() } 59 | UIView.animationsEnabled { [weak self] in 60 | self?.config.alongsideTransition?() 61 | } 62 | 63 | if let lastViewController = navigationController.viewControllers.last, 64 | !config.allowPushOfSameClassTwice && type(of: lastViewController) == type(of: toViewController) { 65 | return false 66 | } 67 | 68 | toViewController.navigationItem.hidesBackButton = config.pushStyle.hidesBackButton 69 | 70 | switch config.pushStyle { 71 | 72 | case .default: 73 | navigationController.pushViewController(toViewController, animated: config.animated) 74 | return true 75 | 76 | // subMenu handled earlier 77 | case .subMenu: 78 | return false 79 | 80 | case .popWhenPossible: 81 | for subViewController in navigationController.viewControllers.reversed() { 82 | if type(of: subViewController) == type(of: toViewController) { 83 | navigationController.popToViewController(subViewController, animated: config.animated) 84 | return true 85 | } 86 | } 87 | navigationController.pushViewController(toViewController, animated: config.animated) 88 | return true 89 | 90 | case .preserve, .preserveAndHideBackButton: 91 | var viewControllers = navigationController.viewControllers 92 | let filtered = viewControllers.filter { preservedViewController in type(of: preservedViewController) == type(of: toViewController) } 93 | guard let preservedViewController = filtered.last else { 94 | navigationController.pushViewController(toViewController, animated: config.animated) 95 | return true 96 | } 97 | viewControllers = viewControllers.filter { subViewController in subViewController !== preservedViewController } 98 | viewControllers.append(preservedViewController) 99 | navigationController.setViewControllers(viewControllers, animated: config.animated) 100 | return true 101 | 102 | case .replace: 103 | navigationController.setViewControllers([toViewController], animated: config.animated) 104 | return true 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /Pod/Classes/SideMenuTransitionController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SideMenuTransitioningDelegate.swift 3 | // SideMenu 4 | // 5 | // Created by Jon Kent on 8/29/19. 6 | // Copyright © 2019 jonkykong. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | internal protocol SideMenuTransitionControllerDelegate: class { 12 | func sideMenuTransitionController(_ transitionController: SideMenuTransitionController, didDismiss viewController: UIViewController) 13 | func sideMenuTransitionController(_ transitionController: SideMenuTransitionController, didPresent viewController: UIViewController) 14 | } 15 | 16 | internal final class SideMenuTransitionController: NSObject, UIViewControllerTransitioningDelegate { 17 | 18 | typealias Model = MenuModel & AnimationModel & PresentationModel 19 | 20 | private let leftSide: Bool 21 | private let config: Model 22 | private var animationController: SideMenuAnimationController? 23 | private weak var interactionController: SideMenuInteractionController? 24 | 25 | var interactive: Bool = false 26 | weak var delegate: SideMenuTransitionControllerDelegate? 27 | 28 | init(leftSide: Bool, config: Model) { 29 | self.leftSide = leftSide 30 | self.config = config 31 | super.init() 32 | } 33 | 34 | func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? { 35 | animationController = SideMenuAnimationController( 36 | config: config, 37 | leftSide: leftSide, 38 | delegate: self) 39 | return animationController 40 | } 41 | 42 | func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { 43 | return animationController 44 | } 45 | 46 | func interactionControllerForPresentation(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? { 47 | return interactionController(using: animator) 48 | } 49 | 50 | func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? { 51 | return interactionController(using: animator) 52 | } 53 | 54 | internal func handle(state: SideMenuInteractionController.State) { 55 | interactionController?.handle(state: state) 56 | } 57 | 58 | func layout() { 59 | animationController?.layout() 60 | } 61 | 62 | func transition(presenting: Bool, animated: Bool = true, interactive: Bool = false, alongsideTransition: (() -> Void)? = nil, complete: Bool = true, completion: ((Bool) -> Void)? = nil) { 63 | animationController?.transition( 64 | presenting: presenting, 65 | animated: animated, 66 | interactive: interactive, 67 | alongsideTransition: alongsideTransition, 68 | complete: complete, completion: completion 69 | ) 70 | } 71 | } 72 | 73 | extension SideMenuTransitionController: SideMenuAnimationControllerDelegate { 74 | 75 | internal func sideMenuAnimationController(_ animationController: SideMenuAnimationController, didDismiss viewController: UIViewController) { 76 | delegate?.sideMenuTransitionController(self, didDismiss: viewController) 77 | } 78 | 79 | internal func sideMenuAnimationController(_ animationController: SideMenuAnimationController, didPresent viewController: UIViewController) { 80 | delegate?.sideMenuTransitionController(self, didPresent: viewController) 81 | } 82 | } 83 | 84 | private extension SideMenuTransitionController { 85 | 86 | func interactionController(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? { 87 | guard interactive else { return nil } 88 | interactive = false 89 | let interactionController = SideMenuInteractionController(cancelWhenBackgrounded: config.dismissWhenBackgrounded, completionCurve: config.completionCurve) 90 | self.interactionController = interactionController 91 | return interactionController 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /Pod/Classes/UITableViewVibrantCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UITableViewVibrantCell.swift 3 | // Pods 4 | // 5 | // Created by Jon Kent on 1/14/16. 6 | // 7 | // 8 | 9 | import UIKit 10 | 11 | open class UITableViewVibrantCell: UITableViewCell { 12 | 13 | private var vibrancyView: UIVisualEffectView = UIVisualEffectView() 14 | private var vibrancySelectedBackgroundView: UIVisualEffectView = UIVisualEffectView() 15 | private var defaultSelectedBackgroundView: UIView? 16 | open var blurEffectStyle: UIBlurEffect.Style? { 17 | didSet { 18 | updateBlur() 19 | } 20 | } 21 | 22 | // For registering with UITableView without subclassing otherwise dequeuing instance of the cell causes an exception 23 | public override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { 24 | super.init(style: style, reuseIdentifier: reuseIdentifier) 25 | } 26 | 27 | required public init?(coder aDecoder: NSCoder) { 28 | super.init(coder: aDecoder) 29 | 30 | vibrancyView.frame = bounds 31 | vibrancyView.autoresizingMask = [.flexibleHeight, .flexibleWidth] 32 | for view in subviews { 33 | vibrancyView.contentView.addSubview(view) 34 | } 35 | addSubview(vibrancyView) 36 | 37 | let blurSelectionEffect = UIBlurEffect(style: .light) 38 | vibrancySelectedBackgroundView.effect = blurSelectionEffect 39 | defaultSelectedBackgroundView = selectedBackgroundView 40 | 41 | updateBlur() 42 | } 43 | 44 | internal func updateBlur() { 45 | // shouldn't be needed but backgroundColor is set to white on iPad: 46 | backgroundColor = UIColor.clear 47 | 48 | if let blurEffectStyle = blurEffectStyle, !UIAccessibility.isReduceTransparencyEnabled { 49 | let blurEffect = UIBlurEffect(style: blurEffectStyle) 50 | vibrancyView.effect = UIVibrancyEffect(blurEffect: blurEffect) 51 | 52 | if selectedBackgroundView != nil && selectedBackgroundView != vibrancySelectedBackgroundView { 53 | vibrancySelectedBackgroundView.contentView.addSubview(selectedBackgroundView!) 54 | selectedBackgroundView = vibrancySelectedBackgroundView 55 | } 56 | } else { 57 | vibrancyView.effect = nil 58 | selectedBackgroundView = defaultSelectedBackgroundView 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Podfile: -------------------------------------------------------------------------------- 1 | source 'https://github.com/CocoaPods/Specs.git' 2 | use_frameworks! 3 | platform :ios, '10.0' 4 | 5 | target 'Example' do 6 | pod "SideMenu", :path => "." 7 | end 8 | -------------------------------------------------------------------------------- /Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - SideMenu (6.4.9) 3 | 4 | DEPENDENCIES: 5 | - SideMenu (from `.`) 6 | 7 | EXTERNAL SOURCES: 8 | SideMenu: 9 | :path: "." 10 | 11 | SPEC CHECKSUMS: 12 | SideMenu: 8ef57a3cfc024a2d3fc1c036c7fe98537baec9e0 13 | 14 | PODFILE CHECKSUM: 863f183ea1ab6f64dc8553590349c586faf8e4a1 15 | 16 | COCOAPODS: 1.9.3 17 | -------------------------------------------------------------------------------- /Pods/Local Podspecs/SideMenu.podspec.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "SideMenu", 3 | "version": "6.4.9", 4 | "summary": "Simple side menu control for iOS in Swift inspired by Facebook. Right and Left sides. No coding required.", 5 | "description": "SideMenu is a simple and versatile side menu control. It's highly customizable, but can also be implemented in storyboard without a single line of code. The are three standard animation styles to choose from along with several other options for further customization if desired. Just type SideMenuManager.menu... and code completion will show you everything you can customize.", 6 | "homepage": "https://github.com/jonkykong/SideMenu", 7 | "screenshots": [ 8 | "https://raw.githubusercontent.com/jonkykong/SideMenu/master/etc/SlideOut.gif", 9 | "https://raw.githubusercontent.com/jonkykong/SideMenu/master/etc/SlideIn.gif", 10 | "https://raw.githubusercontent.com/jonkykong/SideMenu/master/etc/Dissolve.gif", 11 | "https://raw.githubusercontent.com/jonkykong/SideMenu/master/etc/InOut.gif" 12 | ], 13 | "license": { 14 | "type": "MIT", 15 | "file": "LICENSE" 16 | }, 17 | "authors": { 18 | "jonkykong": "contact@jonkent.me" 19 | }, 20 | "source": { 21 | "git": "https://github.com/jonkykong/SideMenu.git", 22 | "tag": "6.4.9" 23 | }, 24 | "platforms": { 25 | "ios": "10.0" 26 | }, 27 | "swift_versions": "5.0", 28 | "source_files": "Pod/Classes/**/*", 29 | "swift_version": "5.0" 30 | } 31 | -------------------------------------------------------------------------------- /Pods/Manifest.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - SideMenu (6.4.9) 3 | 4 | DEPENDENCIES: 5 | - SideMenu (from `.`) 6 | 7 | EXTERNAL SOURCES: 8 | SideMenu: 9 | :path: "." 10 | 11 | SPEC CHECKSUMS: 12 | SideMenu: 8ef57a3cfc024a2d3fc1c036c7fe98537baec9e0 13 | 14 | PODFILE CHECKSUM: 863f183ea1ab6f64dc8553590349c586faf8e4a1 15 | 16 | COCOAPODS: 1.9.3 17 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-Example/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 | FMWK 17 | CFBundleShortVersionString 18 | 1.0.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | ${CURRENT_PROJECT_VERSION} 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-Example/Pods-Example-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 | FMWK 17 | CFBundleShortVersionString 18 | 1.0.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | ${CURRENT_PROJECT_VERSION} 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-Example/Pods-Example-acknowledgements.markdown: -------------------------------------------------------------------------------- 1 | # Acknowledgements 2 | This application makes use of the following third party libraries: 3 | 4 | ## SideMenu 5 | 6 | Copyright (c) 2015 Jonathan Kent 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy 9 | of this software and associated documentation files (the "Software"), to deal 10 | in the Software without restriction, including without limitation the rights 11 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | copies of the Software, and to permit persons to whom the Software is 13 | furnished to do so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included in 16 | all copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | THE SOFTWARE. 25 | 26 | Generated by CocoaPods - https://cocoapods.org 27 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-Example/Pods-Example-acknowledgements.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreferenceSpecifiers 6 | 7 | 8 | FooterText 9 | This application makes use of the following third party libraries: 10 | Title 11 | Acknowledgements 12 | Type 13 | PSGroupSpecifier 14 | 15 | 16 | FooterText 17 | Copyright (c) 2015 Jonathan Kent <contact@jonkent.me> 18 | 19 | Permission is hereby granted, free of charge, to any person obtaining a copy 20 | of this software and associated documentation files (the "Software"), to deal 21 | in the Software without restriction, including without limitation the rights 22 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 23 | copies of the Software, and to permit persons to whom the Software is 24 | furnished to do so, subject to the following conditions: 25 | 26 | The above copyright notice and this permission notice shall be included in 27 | all copies or substantial portions of the Software. 28 | 29 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 30 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 31 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 32 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 33 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 34 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 35 | THE SOFTWARE. 36 | 37 | License 38 | MIT 39 | Title 40 | SideMenu 41 | Type 42 | PSGroupSpecifier 43 | 44 | 45 | FooterText 46 | Generated by CocoaPods - https://cocoapods.org 47 | Title 48 | 49 | Type 50 | PSGroupSpecifier 51 | 52 | 53 | StringsTable 54 | Acknowledgements 55 | Title 56 | Acknowledgements 57 | 58 | 59 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-Example/Pods-Example-dummy.m: -------------------------------------------------------------------------------- 1 | #import 2 | @interface PodsDummy_Pods_Example : NSObject 3 | @end 4 | @implementation PodsDummy_Pods_Example 5 | @end 6 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-Example/Pods-Example-frameworks.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | set -u 4 | set -o pipefail 5 | 6 | function on_error { 7 | echo "$(realpath -mq "${0}"):$1: error: Unexpected failure" 8 | } 9 | trap 'on_error $LINENO' ERR 10 | 11 | if [ -z ${FRAMEWORKS_FOLDER_PATH+x} ]; then 12 | # If FRAMEWORKS_FOLDER_PATH is not set, then there's nowhere for us to copy 13 | # frameworks to, so exit 0 (signalling the script phase was successful). 14 | exit 0 15 | fi 16 | 17 | echo "mkdir -p ${CONFIGURATION_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" 18 | mkdir -p "${CONFIGURATION_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" 19 | 20 | COCOAPODS_PARALLEL_CODE_SIGN="${COCOAPODS_PARALLEL_CODE_SIGN:-false}" 21 | SWIFT_STDLIB_PATH="${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" 22 | 23 | # Used as a return value for each invocation of `strip_invalid_archs` function. 24 | STRIP_BINARY_RETVAL=0 25 | 26 | # This protects against multiple targets copying the same framework dependency at the same time. The solution 27 | # was originally proposed here: https://lists.samba.org/archive/rsync/2008-February/020158.html 28 | RSYNC_PROTECT_TMP_FILES=(--filter "P .*.??????") 29 | 30 | # Copies and strips a vendored framework 31 | install_framework() 32 | { 33 | if [ -r "${BUILT_PRODUCTS_DIR}/$1" ]; then 34 | local source="${BUILT_PRODUCTS_DIR}/$1" 35 | elif [ -r "${BUILT_PRODUCTS_DIR}/$(basename "$1")" ]; then 36 | local source="${BUILT_PRODUCTS_DIR}/$(basename "$1")" 37 | elif [ -r "$1" ]; then 38 | local source="$1" 39 | fi 40 | 41 | local destination="${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" 42 | 43 | if [ -L "${source}" ]; then 44 | echo "Symlinked..." 45 | source="$(readlink "${source}")" 46 | fi 47 | 48 | # Use filter instead of exclude so missing patterns don't throw errors. 49 | echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --links --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${source}\" \"${destination}\"" 50 | rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --links --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${source}" "${destination}" 51 | 52 | local basename 53 | basename="$(basename -s .framework "$1")" 54 | binary="${destination}/${basename}.framework/${basename}" 55 | 56 | if ! [ -r "$binary" ]; then 57 | binary="${destination}/${basename}" 58 | elif [ -L "${binary}" ]; then 59 | echo "Destination binary is symlinked..." 60 | dirname="$(dirname "${binary}")" 61 | binary="${dirname}/$(readlink "${binary}")" 62 | fi 63 | 64 | # Strip invalid architectures so "fat" simulator / device frameworks work on device 65 | if [[ "$(file "$binary")" == *"dynamically linked shared library"* ]]; then 66 | strip_invalid_archs "$binary" 67 | fi 68 | 69 | # Resign the code if required by the build settings to avoid unstable apps 70 | code_sign_if_enabled "${destination}/$(basename "$1")" 71 | 72 | # Embed linked Swift runtime libraries. No longer necessary as of Xcode 7. 73 | if [ "${XCODE_VERSION_MAJOR}" -lt 7 ]; then 74 | local swift_runtime_libs 75 | swift_runtime_libs=$(xcrun otool -LX "$binary" | grep --color=never @rpath/libswift | sed -E s/@rpath\\/\(.+dylib\).*/\\1/g | uniq -u) 76 | for lib in $swift_runtime_libs; do 77 | echo "rsync -auv \"${SWIFT_STDLIB_PATH}/${lib}\" \"${destination}\"" 78 | rsync -auv "${SWIFT_STDLIB_PATH}/${lib}" "${destination}" 79 | code_sign_if_enabled "${destination}/${lib}" 80 | done 81 | fi 82 | } 83 | 84 | # Copies and strips a vendored dSYM 85 | install_dsym() { 86 | local source="$1" 87 | warn_missing_arch=${2:-true} 88 | if [ -r "$source" ]; then 89 | # Copy the dSYM into the targets temp dir. 90 | echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${source}\" \"${DERIVED_FILES_DIR}\"" 91 | rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${source}" "${DERIVED_FILES_DIR}" 92 | 93 | local basename 94 | basename="$(basename -s .dSYM "$source")" 95 | binary_name="$(ls "$source/Contents/Resources/DWARF")" 96 | binary="${DERIVED_FILES_DIR}/${basename}.dSYM/Contents/Resources/DWARF/${binary_name}" 97 | 98 | # Strip invalid architectures so "fat" simulator / device frameworks work on device 99 | if [[ "$(file "$binary")" == *"Mach-O "*"dSYM companion"* ]]; then 100 | strip_invalid_archs "$binary" "$warn_missing_arch" 101 | fi 102 | 103 | if [[ $STRIP_BINARY_RETVAL == 1 ]]; then 104 | # Move the stripped file into its final destination. 105 | echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --links --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${DERIVED_FILES_DIR}/${basename}.framework.dSYM\" \"${DWARF_DSYM_FOLDER_PATH}\"" 106 | rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --links --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${DERIVED_FILES_DIR}/${basename}.dSYM" "${DWARF_DSYM_FOLDER_PATH}" 107 | else 108 | # The dSYM was not stripped at all, in this case touch a fake folder so the input/output paths from Xcode do not reexecute this script because the file is missing. 109 | touch "${DWARF_DSYM_FOLDER_PATH}/${basename}.dSYM" 110 | fi 111 | fi 112 | } 113 | 114 | # Copies the bcsymbolmap files of a vendored framework 115 | install_bcsymbolmap() { 116 | local bcsymbolmap_path="$1" 117 | local destination="${BUILT_PRODUCTS_DIR}" 118 | echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${bcsymbolmap_path}" "${destination}"" 119 | rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${bcsymbolmap_path}" "${destination}" 120 | } 121 | 122 | # Signs a framework with the provided identity 123 | code_sign_if_enabled() { 124 | if [ -n "${EXPANDED_CODE_SIGN_IDENTITY:-}" -a "${CODE_SIGNING_REQUIRED:-}" != "NO" -a "${CODE_SIGNING_ALLOWED}" != "NO" ]; then 125 | # Use the current code_sign_identity 126 | echo "Code Signing $1 with Identity ${EXPANDED_CODE_SIGN_IDENTITY_NAME}" 127 | local code_sign_cmd="/usr/bin/codesign --force --sign ${EXPANDED_CODE_SIGN_IDENTITY} ${OTHER_CODE_SIGN_FLAGS:-} --preserve-metadata=identifier,entitlements '$1'" 128 | 129 | if [ "${COCOAPODS_PARALLEL_CODE_SIGN}" == "true" ]; then 130 | code_sign_cmd="$code_sign_cmd &" 131 | fi 132 | echo "$code_sign_cmd" 133 | eval "$code_sign_cmd" 134 | fi 135 | } 136 | 137 | # Strip invalid architectures 138 | strip_invalid_archs() { 139 | binary="$1" 140 | warn_missing_arch=${2:-true} 141 | # Get architectures for current target binary 142 | binary_archs="$(lipo -info "$binary" | rev | cut -d ':' -f1 | awk '{$1=$1;print}' | rev)" 143 | # Intersect them with the architectures we are building for 144 | intersected_archs="$(echo ${ARCHS[@]} ${binary_archs[@]} | tr ' ' '\n' | sort | uniq -d)" 145 | # If there are no archs supported by this binary then warn the user 146 | if [[ -z "$intersected_archs" ]]; then 147 | if [[ "$warn_missing_arch" == "true" ]]; then 148 | echo "warning: [CP] Vendored binary '$binary' contains architectures ($binary_archs) none of which match the current build architectures ($ARCHS)." 149 | fi 150 | STRIP_BINARY_RETVAL=0 151 | return 152 | fi 153 | stripped="" 154 | for arch in $binary_archs; do 155 | if ! [[ "${ARCHS}" == *"$arch"* ]]; then 156 | # Strip non-valid architectures in-place 157 | lipo -remove "$arch" -output "$binary" "$binary" 158 | stripped="$stripped $arch" 159 | fi 160 | done 161 | if [[ "$stripped" ]]; then 162 | echo "Stripped $binary of architectures:$stripped" 163 | fi 164 | STRIP_BINARY_RETVAL=1 165 | } 166 | 167 | install_artifact() { 168 | artifact="$1" 169 | base="$(basename "$artifact")" 170 | case $base in 171 | *.framework) 172 | install_framework "$artifact" 173 | ;; 174 | *.dSYM) 175 | # Suppress arch warnings since XCFrameworks will include many dSYM files 176 | install_dsym "$artifact" "false" 177 | ;; 178 | *.bcsymbolmap) 179 | install_bcsymbolmap "$artifact" 180 | ;; 181 | *) 182 | echo "error: Unrecognized artifact "$artifact"" 183 | ;; 184 | esac 185 | } 186 | 187 | copy_artifacts() { 188 | file_list="$1" 189 | while read artifact; do 190 | install_artifact "$artifact" 191 | done <$file_list 192 | } 193 | 194 | ARTIFACT_LIST_FILE="${BUILT_PRODUCTS_DIR}/cocoapods-artifacts-${CONFIGURATION}.txt" 195 | if [ -r "${ARTIFACT_LIST_FILE}" ]; then 196 | copy_artifacts "${ARTIFACT_LIST_FILE}" 197 | fi 198 | 199 | if [[ "$CONFIGURATION" == "Debug" ]]; then 200 | install_framework "${BUILT_PRODUCTS_DIR}/SideMenu/SideMenu.framework" 201 | fi 202 | if [[ "$CONFIGURATION" == "Release" ]]; then 203 | install_framework "${BUILT_PRODUCTS_DIR}/SideMenu/SideMenu.framework" 204 | fi 205 | if [ "${COCOAPODS_PARALLEL_CODE_SIGN}" == "true" ]; then 206 | wait 207 | fi 208 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-Example/Pods-Example-resources.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | mkdir -p "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" 5 | 6 | RESOURCES_TO_COPY=${PODS_ROOT}/resources-to-copy-${TARGETNAME}.txt 7 | > "$RESOURCES_TO_COPY" 8 | 9 | XCASSET_FILES=() 10 | 11 | case "${TARGETED_DEVICE_FAMILY}" in 12 | 1,2) 13 | TARGET_DEVICE_ARGS="--target-device ipad --target-device iphone" 14 | ;; 15 | 1) 16 | TARGET_DEVICE_ARGS="--target-device iphone" 17 | ;; 18 | 2) 19 | TARGET_DEVICE_ARGS="--target-device ipad" 20 | ;; 21 | *) 22 | TARGET_DEVICE_ARGS="--target-device mac" 23 | ;; 24 | esac 25 | 26 | install_resource() 27 | { 28 | if [[ "$1" = /* ]] ; then 29 | RESOURCE_PATH="$1" 30 | else 31 | RESOURCE_PATH="${PODS_ROOT}/$1" 32 | fi 33 | if [[ ! -e "$RESOURCE_PATH" ]] ; then 34 | cat << EOM 35 | error: Resource "$RESOURCE_PATH" not found. Run 'pod install' to update the copy resources script. 36 | EOM 37 | exit 1 38 | fi 39 | case $RESOURCE_PATH in 40 | *.storyboard) 41 | echo "ibtool --reference-external-strings-file --errors --warnings --notices --minimum-deployment-target ${!DEPLOYMENT_TARGET_SETTING_NAME} --output-format human-readable-text --compile ${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename \"$RESOURCE_PATH\" .storyboard`.storyboardc $RESOURCE_PATH --sdk ${SDKROOT} ${TARGET_DEVICE_ARGS}" 42 | ibtool --reference-external-strings-file --errors --warnings --notices --minimum-deployment-target ${!DEPLOYMENT_TARGET_SETTING_NAME} --output-format human-readable-text --compile "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename \"$RESOURCE_PATH\" .storyboard`.storyboardc" "$RESOURCE_PATH" --sdk "${SDKROOT}" ${TARGET_DEVICE_ARGS} 43 | ;; 44 | *.xib) 45 | echo "ibtool --reference-external-strings-file --errors --warnings --notices --minimum-deployment-target ${!DEPLOYMENT_TARGET_SETTING_NAME} --output-format human-readable-text --compile ${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename \"$RESOURCE_PATH\" .xib`.nib $RESOURCE_PATH --sdk ${SDKROOT} ${TARGET_DEVICE_ARGS}" 46 | ibtool --reference-external-strings-file --errors --warnings --notices --minimum-deployment-target ${!DEPLOYMENT_TARGET_SETTING_NAME} --output-format human-readable-text --compile "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename \"$RESOURCE_PATH\" .xib`.nib" "$RESOURCE_PATH" --sdk "${SDKROOT}" ${TARGET_DEVICE_ARGS} 47 | ;; 48 | *.framework) 49 | echo "mkdir -p ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" 50 | mkdir -p "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" 51 | echo "rsync -av $RESOURCE_PATH ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" 52 | rsync -av "$RESOURCE_PATH" "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" 53 | ;; 54 | *.xcdatamodel) 55 | echo "xcrun momc \"$RESOURCE_PATH\" \"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH"`.mom\"" 56 | xcrun momc "$RESOURCE_PATH" "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcdatamodel`.mom" 57 | ;; 58 | *.xcdatamodeld) 59 | echo "xcrun momc \"$RESOURCE_PATH\" \"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcdatamodeld`.momd\"" 60 | xcrun momc "$RESOURCE_PATH" "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcdatamodeld`.momd" 61 | ;; 62 | *.xcmappingmodel) 63 | echo "xcrun mapc \"$RESOURCE_PATH\" \"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcmappingmodel`.cdm\"" 64 | xcrun mapc "$RESOURCE_PATH" "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcmappingmodel`.cdm" 65 | ;; 66 | *.xcassets) 67 | ABSOLUTE_XCASSET_FILE="$RESOURCE_PATH" 68 | XCASSET_FILES+=("$ABSOLUTE_XCASSET_FILE") 69 | ;; 70 | *) 71 | echo "$RESOURCE_PATH" 72 | echo "$RESOURCE_PATH" >> "$RESOURCES_TO_COPY" 73 | ;; 74 | esac 75 | } 76 | 77 | mkdir -p "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" 78 | rsync -avr --copy-links --no-relative --exclude '*/.svn/*' --files-from="$RESOURCES_TO_COPY" / "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" 79 | if [[ "${ACTION}" == "install" ]] && [[ "${SKIP_INSTALL}" == "NO" ]]; then 80 | mkdir -p "${INSTALL_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" 81 | rsync -avr --copy-links --no-relative --exclude '*/.svn/*' --files-from="$RESOURCES_TO_COPY" / "${INSTALL_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" 82 | fi 83 | rm -f "$RESOURCES_TO_COPY" 84 | 85 | if [[ -n "${WRAPPER_EXTENSION}" ]] && [ "`xcrun --find actool`" ] && [ -n "$XCASSET_FILES" ] 86 | then 87 | # Find all other xcassets (this unfortunately includes those of path pods and other targets). 88 | OTHER_XCASSETS=$(find "$PWD" -iname "*.xcassets" -type d) 89 | while read line; do 90 | if [[ $line != "${PODS_ROOT}*" ]]; then 91 | XCASSET_FILES+=("$line") 92 | fi 93 | done <<<"$OTHER_XCASSETS" 94 | 95 | printf "%s\0" "${XCASSET_FILES[@]}" | xargs -0 xcrun actool --output-format human-readable-text --notices --warnings --platform "${PLATFORM_NAME}" --minimum-deployment-target "${!DEPLOYMENT_TARGET_SETTING_NAME}" ${TARGET_DEVICE_ARGS} --compress-pngs --compile "${BUILT_PRODUCTS_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" 96 | fi 97 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-Example/Pods-Example-umbrella.h: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #else 4 | #ifndef FOUNDATION_EXPORT 5 | #if defined(__cplusplus) 6 | #define FOUNDATION_EXPORT extern "C" 7 | #else 8 | #define FOUNDATION_EXPORT extern 9 | #endif 10 | #endif 11 | #endif 12 | 13 | 14 | FOUNDATION_EXPORT double Pods_ExampleVersionNumber; 15 | FOUNDATION_EXPORT const unsigned char Pods_ExampleVersionString[]; 16 | 17 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-Example/Pods-Example.debug.xcconfig: -------------------------------------------------------------------------------- 1 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES 2 | FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/SideMenu" 3 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 4 | HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/SideMenu/SideMenu.framework/Headers" 5 | LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks' 6 | OTHER_LDFLAGS = $(inherited) -framework "SideMenu" 7 | OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS 8 | PODS_BUILD_DIR = ${BUILD_DIR} 9 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 10 | PODS_PODFILE_DIR_PATH = ${SRCROOT}/. 11 | PODS_ROOT = ${SRCROOT}/Pods 12 | USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES 13 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-Example/Pods-Example.modulemap: -------------------------------------------------------------------------------- 1 | framework module Pods_Example { 2 | umbrella header "Pods-Example-umbrella.h" 3 | 4 | export * 5 | module * { export * } 6 | } 7 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-Example/Pods-Example.release.xcconfig: -------------------------------------------------------------------------------- 1 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES 2 | FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/SideMenu" 3 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 4 | HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/SideMenu/SideMenu.framework/Headers" 5 | LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks' 6 | OTHER_LDFLAGS = $(inherited) -framework "SideMenu" 7 | OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS 8 | PODS_BUILD_DIR = ${BUILD_DIR} 9 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 10 | PODS_PODFILE_DIR_PATH = ${SRCROOT}/. 11 | PODS_ROOT = ${SRCROOT}/Pods 12 | USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES 13 | -------------------------------------------------------------------------------- /Pods/Target Support Files/SideMenu/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 | FMWK 17 | CFBundleShortVersionString 18 | 2.0.5 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | ${CURRENT_PROJECT_VERSION} 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Pods/Target Support Files/SideMenu/SideMenu-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 | FMWK 17 | CFBundleShortVersionString 18 | 6.4.9 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | ${CURRENT_PROJECT_VERSION} 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Pods/Target Support Files/SideMenu/SideMenu-dummy.m: -------------------------------------------------------------------------------- 1 | #import 2 | @interface PodsDummy_SideMenu : NSObject 3 | @end 4 | @implementation PodsDummy_SideMenu 5 | @end 6 | -------------------------------------------------------------------------------- /Pods/Target Support Files/SideMenu/SideMenu-prefix.pch: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #else 4 | #ifndef FOUNDATION_EXPORT 5 | #if defined(__cplusplus) 6 | #define FOUNDATION_EXPORT extern "C" 7 | #else 8 | #define FOUNDATION_EXPORT extern 9 | #endif 10 | #endif 11 | #endif 12 | 13 | -------------------------------------------------------------------------------- /Pods/Target Support Files/SideMenu/SideMenu-umbrella.h: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #else 4 | #ifndef FOUNDATION_EXPORT 5 | #if defined(__cplusplus) 6 | #define FOUNDATION_EXPORT extern "C" 7 | #else 8 | #define FOUNDATION_EXPORT extern 9 | #endif 10 | #endif 11 | #endif 12 | 13 | 14 | FOUNDATION_EXPORT double SideMenuVersionNumber; 15 | FOUNDATION_EXPORT const unsigned char SideMenuVersionString[]; 16 | 17 | -------------------------------------------------------------------------------- /Pods/Target Support Files/SideMenu/SideMenu.modulemap: -------------------------------------------------------------------------------- 1 | framework module SideMenu { 2 | umbrella header "SideMenu-umbrella.h" 3 | 4 | export * 5 | module * { export * } 6 | } 7 | -------------------------------------------------------------------------------- /Pods/Target Support Files/SideMenu/SideMenu.xcconfig: -------------------------------------------------------------------------------- 1 | CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/SideMenu 2 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 3 | OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS 4 | PODS_BUILD_DIR = ${BUILD_DIR} 5 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 6 | PODS_ROOT = ${SRCROOT} 7 | PODS_TARGET_SRCROOT = ${PODS_ROOT}/.. 8 | PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} 9 | SKIP_INSTALL = YES 10 | USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ▤ SideMenu 2 | [![CircleCI](https://circleci.com/gh/jonkykong/SideMenu.svg?style=svg)](https://circleci.com/gh/jonkykong/SideMenu) 3 | [![Version](https://img.shields.io/cocoapods/v/SideMenu.svg?style=flat-square)](http://cocoapods.org/pods/SideMenu) 4 | [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat-square)](https://github.com/Carthage/Carthage) 5 | [![License](https://img.shields.io/cocoapods/l/SideMenu.svg?style=flat-square)](http://cocoapods.org/pods/SideMenu) 6 | [![Platform](https://img.shields.io/cocoapods/p/SideMenu.svg?style=flat-square)](http://cocoapods.org/pods/SideMenu) 7 | 8 | ### If you like SideMenu, give it a ★ at the top right of this page. 9 | #### SideMenu needs your help! If you're a skilled iOS developer and want to help maintain this repository and answer issues asked by the community, please [send me an email](mailto:yo@massappeal.co?subject=I%20Want%20To%20Help!). 10 | 11 | > Hi, I'm Jon Kent and I am an iOS designer, developer, and mobile strategist. I love coffee and play the drums. 12 | > * [**Hire me**](mailto:yo@massappeal.co?subject=Let's%20build%20something%20amazing) to help you make cool stuff. *Note: If you're having a problem with SideMenu, please open an [issue](https://github.com/jonkykong/SideMenu/issues/new) and do not email me.* 13 | > * Check out my [website](http://massappeal.co) to see some of my other projects. 14 | > * Building and maintaining this **free** library takes a lot of my time and **saves you time**. Please consider paying it forward by supporting me with a small amount to my [PayPal](https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=contact%40jonkent%2eme&lc=US¤cy_code=USD&bn=PP%2dDonationsBF%3abtn_donateCC_LG%2egif%3aNonHosted). (only **13** people have donated since 12/23/15 but **thank you** to those who have!) 15 | 16 | * **[Overview](#overview)** 17 | * [Preview Samples](#preview-samples) 18 | * **[Requirements](#requirements)** 19 | * **[Installation](#installation)** 20 | * [CocoaPods](#cocoapods) 21 | * [Carthage](#carthage) 22 | * [Swift Package Manager](#swift-package-manager) 23 | * **[Usage](#usage)** 24 | * [Code-less Storyboard Implementation](#code-less-storyboard-implementation) 25 | * [Code Implementation](#code-implementation) 26 | * **[Customization](#customization)** 27 | * [SideMenuManager](#sidemenumanager) 28 | * [SideMenuNavigationController](#sidemenunavigationcontroller) 29 | * [SideMenuNavigationControllerDelegate](#sidemenunavigationcontrollerdelegate) 30 | * [Advanced](#advanced) 31 | * [Known Issues](#known-issues) 32 | * [Thank You](#thank-you) 33 | * [License](#license) 34 | 35 | ## Overview 36 | 37 | SideMenu is a simple and versatile side menu control written in Swift. 38 | - [x] **It can be implemented in storyboard without a single line of [code](#code-less-storyboard-implementation).** 39 | - [x] Eight standard animation styles to choose from (there's even a parallax effect if you want to get weird). 40 | - [x] Highly customizable without needing to write tons of custom code. 41 | - [x] Supports continuous swiping between side menus on boths sides in a single gesture. 42 | - [x] Global menu configuration. Set-up once and be done for all screens. 43 | - [x] Menus can be presented and dismissed the same as any other view controller since this control uses [custom transitions](https://developer.apple.com/library/content/featuredarticles/ViewControllerPGforiPhoneOS/CustomizingtheTransitionAnimations.html). 44 | - [x] Animations use your view controllers, not snapshots. 45 | - [x] Properly handles screen rotation and in-call status bar height changes. 46 | 47 | Check out the example project to see it in action! 48 | ### Preview Samples 49 | | Slide Out | Slide In | Dissolve | Slide In + Out | 50 | | --- | --- | --- | --- | 51 | | ![](https://raw.githubusercontent.com/jonkykong/SideMenu/master/etc/SlideOut.gif) | ![](https://raw.githubusercontent.com/jonkykong/SideMenu/master/etc/SlideIn.gif) | ![](https://raw.githubusercontent.com/jonkykong/SideMenu/master/etc/Dissolve.gif) | ![](https://raw.githubusercontent.com/jonkykong/SideMenu/master/etc/InOut.gif) | 52 | 53 | ## Requirements 54 | - [x] Xcode 11. 55 | - [x] Swift 5. 56 | - [x] iOS 10 or higher. 57 | 58 | ## Installation 59 | ### CocoaPods 60 | 61 | [CocoaPods](http://cocoapods.org) is a dependency manager for Cocoa projects. You can install it with the following command: 62 | 63 | ```bash 64 | $ gem install cocoapods 65 | ``` 66 | 67 | To integrate SideMenu into your Xcode project using CocoaPods, specify it in your `Podfile`: 68 | 69 | ```ruby 70 | source 'https://github.com/CocoaPods/Specs.git' 71 | platform :ios, '10.0' 72 | use_frameworks! 73 | 74 | pod 'SideMenu' 75 | 76 | # For Swift 5 use: 77 | # pod 'SideMenu', '~> 6.0' 78 | 79 | # For Swift 4.2 (no longer maintained) use: 80 | # pod 'SideMenu', '~> 5.0' 81 | ``` 82 | 83 | Then, run the following command: 84 | 85 | ```bash 86 | $ pod install 87 | ``` 88 | 89 | ### Carthage 90 | 91 | [Carthage](https://github.com/Carthage/Carthage) is a decentralized dependency manager that builds your dependencies and provides you with binary frameworks. 92 | 93 | You can install Carthage with [Homebrew](http://brew.sh/) using the following command: 94 | 95 | ```bash 96 | $ brew update 97 | $ brew install carthage 98 | ``` 99 | 100 | To integrate SideMenu into your Xcode project using Carthage, specify it in your `Cartfile`: 101 | 102 | ```ogdl 103 | github "jonkykong/SideMenu" "master" 104 | ``` 105 | 106 | ### Swift Package Manager 107 | 108 | The [Swift Package Manager](https://swift.org/package-manager/) is a tool for automating the distribution of Swift code and is integrated into the `swift` compiler. It is in early development, but SideMenu does support its use on supported platforms. 109 | 110 | Once you have your Swift package set up, adding SideMenu as a dependency is as easy as adding it to the `dependencies` value of your `Package.swift`. 111 | 112 | ```swift 113 | dependencies: [ 114 | .package(url: "https://github.com/jonkykong/SideMenu.git", from: "6.0.0") 115 | ] 116 | ``` 117 | 118 | ## Usage 119 | ### Code-less Storyboard Implementation 120 | 1. Create a Navigation Controller for a side menu. Set the `Custom Class` of the Navigation Controller to be `SideMenuNavigationController` in the **Identity Inspector**. Set the `Module` to `SideMenu` (ignore this step if you've manually added SideMenu to your project). Create a Root View Controller for the Navigation Controller (shown as a UITableViewController below). Set up any Triggered Segues you want in that view controller. 121 | ![](https://raw.githubusercontent.com/jonkykong/SideMenu/master/etc/Screenshot1.png) 122 | 123 | 2. Set the `Left Side` property of the `SideMenuNavigationController` to On if you want it to appear from the left side of the screen, or Off/Default if you want it to appear from the right side. 124 | ![](https://raw.githubusercontent.com/jonkykong/SideMenu/master/etc/Screenshot2.png) 125 | 126 | 3. Add a UIButton or UIBarButton to a view controller that you want to display the menu from. Set that button's Triggered Segues action to modally present the Navigation Controller from step 1. 127 | ![](https://raw.githubusercontent.com/jonkykong/SideMenu/master/etc/Screenshot3.png) 128 | 129 | That's it. *Note: you can only enable gestures in code.* 130 | ### Code Implementation 131 | First: 132 | ```swift 133 | import SideMenu 134 | ``` 135 | 136 | From a button, do something like this: 137 | ``` swift 138 | // Define the menu 139 | let menu = SideMenuNavigationController(rootViewController: YourViewController) 140 | // SideMenuNavigationController is a subclass of UINavigationController, so do any additional configuration 141 | // of it here like setting its viewControllers. If you're using storyboards, you'll want to do something like: 142 | // let menu = storyboard!.instantiateViewController(withIdentifier: "RightMenu") as! SideMenuNavigationController 143 | present(menu, animated: true, completion: nil) 144 | ``` 145 | 146 | To dismiss a menu programmatically, do something like this: 147 | ``` swift 148 | dismiss(animated: true, completion: nil) 149 | ``` 150 | 151 | To use gestures you have to use the `SideMenuManager`. In your `AppDelegate` do something like this: 152 | ``` swift 153 | // Define the menus 154 | let leftMenuNavigationController = SideMenuNavigationController(rootViewController: YourViewController) 155 | SideMenuManager.default.leftMenuNavigationController = leftMenuNavigationController 156 | 157 | let rightMenuNavigationController = SideMenuNavigationController(rootViewController: YourViewController) 158 | SideMenuManager.default.rightMenuNavigationController = rightMenuNavigationController 159 | 160 | // Setup gestures: the left and/or right menus must be set up (above) for these to work. 161 | // Note that these continue to work on the Navigation Controller independent of the view controller it displays! 162 | SideMenuManager.default.addPanGestureToPresent(toView: self.navigationController!.navigationBar) 163 | SideMenuManager.default.addScreenEdgePanGesturesToPresent(toView: self.navigationController!.view) 164 | 165 | // (Optional) Prevent status bar area from turning black when menu appears: 166 | leftMenuNavigationController.statusBarEndAlpha = 0 167 | // Copy all settings to the other menu 168 | rightMenuNavigationController.settings = leftMenuNavigationController.settings 169 | ``` 170 | That's it. 171 | ### Customization 172 | #### SideMenuManager 173 | `SideMenuManager` supports the following: 174 | ``` swift 175 | /// The left menu. 176 | open var leftMenuNavigationController: SideMenuNavigationController? 177 | /// The right menu. 178 | public var rightMenuNavigationController: SideMenuNavigationController? 179 | /** 180 | Adds screen edge gestures for both left and right sides to a view to present a menu. 181 | 182 | - Parameter toView: The view to add gestures to. 183 | 184 | - Returns: The array of screen edge gestures added to `toView`. 185 | */ 186 | @discardableResult public func addScreenEdgePanGesturesToPresent(toView view: UIView) -> [UIScreenEdgePanGestureRecognizer] 187 | /** 188 | Adds screen edge gestures to a view to present a menu. 189 | 190 | - Parameter toView: The view to add gestures to. 191 | - Parameter forMenu: The menu (left or right) you want to add a gesture for. 192 | 193 | - Returns: The screen edge gestures added to `toView`. 194 | */ 195 | @discardableResult public func addScreenEdgePanGesturesToPresent(toView view: UIView, forMenu side: PresentDirection) -> UIScreenEdgePanGestureRecognizer 196 | /** 197 | Adds a pan edge gesture to a view to present menus. 198 | 199 | - Parameter toView: The view to add a pan gesture to. 200 | 201 | - Returns: The pan gesture added to `toView`. 202 | */ 203 | @discardableResult public func addPanGestureToPresent(toView view: UIView) -> UIPanGestureRecognizer 204 | ``` 205 | #### SideMenuNavigationController 206 | `SideMenuNavigationController` supports the following: 207 | ``` swift 208 | /// Prevents the same view controller (or a view controller of the same class) from being pushed more than once. Defaults to true. 209 | var allowPushOfSameClassTwice: Bool = true 210 | /// Forces menus to always animate when appearing or disappearing, regardless of a pushed view controller's animation. 211 | var alwaysAnimate: Bool = true 212 | /// The animation options when a menu is displayed. Ignored when displayed with a gesture. 213 | var animationOptions: UIView.AnimationOptions = .curveEaseInOut 214 | /** 215 | The blur effect style of the menu if the menu's root view controller is a UITableViewController or UICollectionViewController. 216 | 217 | - Note: If you want cells in a UITableViewController menu to show vibrancy, make them a subclass of UITableViewVibrantCell. 218 | */ 219 | var blurEffectStyle: UIBlurEffect.Style? = nil 220 | /// Duration of the remaining animation when the menu is partially dismissed with gestures. Default is 0.35 seconds. 221 | var completeGestureDuration: Double = 0.35 222 | /// Animation curve of the remaining animation when the menu is partially dismissed with gestures. Default is .easeIn. 223 | var completionCurve: UIView.AnimationCurve = .curveEaseInOut 224 | /// Duration of the animation when the menu is dismissed without gestures. Default is 0.35 seconds. 225 | var dismissDuration: Double = 0.35 226 | /// Automatically dismisses the menu when another view is presented from it. 227 | var dismissOnPresent: Bool = true 228 | /// Automatically dismisses the menu when another view controller is pushed from it. 229 | var dismissOnPush: Bool = true 230 | /// Automatically dismisses the menu when the screen is rotated. 231 | var dismissOnRotation: Bool = true 232 | /// Automatically dismisses the menu when app goes to the background. 233 | var dismissWhenBackgrounded: Bool = true 234 | /// Enable or disable a swipe gesture that dismisses the menu. Will not be triggered when `presentingViewControllerUserInteractionEnabled` is set to true. Default is true. 235 | var enableSwipeToDismissGesture: Bool = true 236 | /// Enable or disable a tap gesture that dismisses the menu. Will not be triggered when `presentingViewControllerUserInteractionEnabled` is set to true. Default is true. 237 | var enableTapToDismissGesture: Bool = true 238 | /// The animation initial spring velocity when a menu is displayed. Ignored when displayed with a gesture. 239 | var initialSpringVelocity: CGFloat = 1 240 | /// Whether the menu appears on the right or left side of the screen. Right is the default. This property cannot be changed after the menu has loaded. 241 | var leftSide: Bool = false 242 | /// Width of the menu when presented on screen, showing the existing view controller in the remaining space. Default is zero. 243 | var menuWidth: CGFloat = 240 244 | /// Duration of the animation when the menu is presented without gestures. Default is 0.35 seconds. 245 | var presentDuration: Double = 0.35 246 | /// Enable or disable interaction with the presenting view controller while the menu is displayed. Enabling may make it difficult to dismiss the menu or cause exceptions if the user tries to present and already presented menu. `presentingViewControllerUseSnapshot` must also set to false. Default is false. 247 | var presentingViewControllerUserInteractionEnabled: Bool = false 248 | /// Use a snapshot for the presenting vierw controller while the menu is displayed. Useful when layout changes occur during transitions. Not recommended for apps that support rotation. Default is false. 249 | var presentingViewControllerUseSnapshot: Bool = false 250 | /// The presentation style of the menu. 251 | var presentationStyle: SideMenuPresentStyle = .viewSlideOut 252 | /** 253 | The push style of the menu. 254 | 255 | There are six modes in MenuPushStyle: 256 | - defaultBehavior: The view controller is pushed onto the stack. 257 | - popWhenPossible: If a view controller already in the stack is of the same class as the pushed view controller, the stack is instead popped back to the existing view controller. This behavior can help users from getting lost in a deep navigation stack. 258 | - preserve: If a view controller already in the stack is of the same class as the pushed view controller, the existing view controller is pushed to the end of the stack. This behavior is similar to a UITabBarController. 259 | - preserveAndHideBackButton: Same as .preserve and back buttons are automatically hidden. 260 | - replace: Any existing view controllers are released from the stack and replaced with the pushed view controller. Back buttons are automatically hidden. This behavior is ideal if view controllers require a lot of memory or their state doesn't need to be preserved.. 261 | - subMenu: Unlike all other behaviors that push using the menu's presentingViewController, this behavior pushes view controllers within the menu. Use this behavior if you want to display a sub menu. 262 | */ 263 | var pushStyle: MenuPushStyle = .default 264 | /// Draws `presentationStyle.backgroundColor` behind the status bar. Default is 0. 265 | var statusBarEndAlpha: CGFloat = 0 266 | /// The animation spring damping when a menu is displayed. Ignored when displayed with a gesture. 267 | var usingSpringWithDamping: CGFloat = 1 268 | /// Indicates if the menu is anywhere in the view hierarchy, even if covered by another view controller. 269 | var isHidden: Bool 270 | ``` 271 | #### SideMenuPresentStyle 272 | There are 8 pre-defined `SideMenuPresentStyle` options: 273 | ``` swift 274 | /// Menu slides in over the existing view. 275 | static let menuSlideIn: SideMenuPresentStyle 276 | /// The existing view slides out to reveal the menu underneath. 277 | static let viewSlideOut: SideMenuPresentStyle 278 | /// The existing view slides out while the menu slides in. 279 | static let viewSlideOutMenuIn: SideMenuPresentStyle 280 | /// The menu dissolves in over the existing view. 281 | static let menuDissolveIn: SideMenuPresentStyle 282 | /// The existing view slides out while the menu partially slides in. 283 | static let viewSlideOutMenuPartialIn: SideMenuPresentStyle 284 | /// The existing view slides out while the menu slides out from under it. 285 | static let viewSlideOutMenuOut: SideMenuPresentStyle 286 | /// The existing view slides out while the menu partially slides out from under it. 287 | static let viewSlideOutMenuPartialOut: SideMenuPresentStyle 288 | /// The existing view slides out and shrinks to reveal the menu underneath. 289 | static let viewSlideOutMenuZoom: SideMenuPresentStyle 290 | ``` 291 | #### SideMenuNavigationControllerDelegate 292 | To receive notifications when a menu is displayed from a view controller, have it adhere to the `SideMenuNavigationControllerDelegate` protocol: 293 | ``` swift 294 | extension MyViewController: SideMenuNavigationControllerDelegate { 295 | 296 | func sideMenuWillAppear(menu: SideMenuNavigationController, animated: Bool) { 297 | print("SideMenu Appearing! (animated: \(animated))") 298 | } 299 | 300 | func sideMenuDidAppear(menu: SideMenuNavigationController, animated: Bool) { 301 | print("SideMenu Appeared! (animated: \(animated))") 302 | } 303 | 304 | func sideMenuWillDisappear(menu: SideMenuNavigationController, animated: Bool) { 305 | print("SideMenu Disappearing! (animated: \(animated))") 306 | } 307 | 308 | func sideMenuDidDisappear(menu: SideMenuNavigationController, animated: Bool) { 309 | print("SideMenu Disappeared! (animated: \(animated))") 310 | } 311 | } 312 | ``` 313 | *Note: setting the `sideMenuDelegate` property on `SideMenuNavigationController` is optional. If your view controller adheres to the protocol then the methods will be called automatically.* 314 | ### Advanced 315 |
316 | Click for Details 317 | 318 | #### Multiple SideMenuManagers 319 | For simplicity, `SideMenuManager.default` serves as the primary instance as most projects will only need one menu across all screens. If you need to show a different SideMenu using gestures, such as from a modal view controller presented from a previous SideMenu, do the following: 320 | 1. Declare a variable containing your custom `SideMenuManager` instance. You may want it to define it globally and configure it in your app delegate if menus will be used on multiple screens. 321 | ``` swift 322 | let customSideMenuManager = SideMenuManager() 323 | ``` 324 | 2. Setup and display menus with your custom instance the same as you would with the `SideMenuManager.default` instance. 325 | 3. If using Storyboards, subclass your instance of `SideMenuNavigationController` and set its `sideMenuManager` property to your custom instance. This must be done before `viewDidLoad` is called: 326 | ``` swift 327 | class MySideMenuNavigationController: SideMenuNavigationController { 328 | 329 | let customSideMenuManager = SideMenuManager() 330 | 331 | override func awakeFromNib() { 332 | super.awakeFromNib() 333 | 334 | sideMenuManager = customSideMenuManager 335 | } 336 | } 337 | ``` 338 | Alternatively, you can set `sideMenuManager` from the view controller that segues to your SideMenuNavigationController: 339 | ``` swift 340 | override func prepare(for segue: UIStoryboardSegue, sender: Any?) { 341 | if let sideMenuNavigationController = segue.destination as? SideMenuNavigationController { 342 | sideMenuNavigationController.sideMenuManager = customSideMenuManager 343 | } 344 | } 345 | ``` 346 | *Important: displaying SideMenu instances directly over each other is not supported. Use `menuPushStyle = .subMenu` to display multi-level menus instead.* 347 | 348 | ### SideMenuPresentationStyle 349 | If you want to create your own custom presentation style, create a subclass of `SideMenuPresentationStyle` and set your menu's `presentationStyle` to it: 350 | ```swift 351 | class MyPresentStyle: SideMenuPresentationStyle { 352 | 353 | override init() { 354 | super.init() 355 | /// Background color behind the views and status bar color 356 | backgroundColor = .black 357 | /// The starting alpha value of the menu before it appears 358 | menuStartAlpha = 1 359 | /// Whether or not the menu is on top. If false, the presenting view is on top. Shadows are applied to the view on top. 360 | menuOnTop = false 361 | /// The amount the menu is translated along the x-axis. Zero is stationary, negative values are off-screen, positive values are on screen. 362 | menuTranslateFactor = 0 363 | /// The amount the menu is scaled. Less than one shrinks the view, larger than one grows the view. 364 | menuScaleFactor = 1 365 | /// The color of the shadow applied to the top most view. 366 | onTopShadowColor = .black 367 | /// The radius of the shadow applied to the top most view. 368 | onTopShadowRadius = 5 369 | /// The opacity of the shadow applied to the top most view. 370 | onTopShadowOpacity = 0 371 | /// The offset of the shadow applied to the top most view. 372 | onTopShadowOffset = .zero 373 | /// The ending alpha of the presenting view when the menu is fully displayed. 374 | presentingEndAlpha = 1 375 | /// The amount the presenting view is translated along the x-axis. Zero is stationary, negative values are off-screen, positive values are on screen. 376 | presentingTranslateFactor = 0 377 | /// The amount the presenting view is scaled. Less than one shrinks the view, larger than one grows the view. 378 | presentingScaleFactor = 1 379 | /// The strength of the parallax effect on the presenting view once the menu is displayed. 380 | presentingParallaxStrength = .zero 381 | } 382 | 383 | /// This method is called just before the presentation transition begins. Use this to setup any animations. The super method does not need to be called. 384 | override func presentationTransitionWillBegin(to presentedViewController: UIViewController, from presentingViewController: UIViewController) {} 385 | /// This method is called during the presentation animation. Use this to animate anything alongside the menu animation. The super method does not need to be called. 386 | override func presentationTransition(to presentedViewController: UIViewController, from presentingViewController: UIViewController) {} 387 | /// This method is called when the presentation transition ends. Use this to finish any animations. The super method does not need to be called. 388 | override func presentationTransitionDidEnd(to presentedViewController: UIViewController, from presentingViewController: UIViewController, _ completed: Bool) {} 389 | /// This method is called just before the dismissal transition begins. Use this to setup any animations. The super method does not need to be called. 390 | override func dismissalTransitionWillBegin(to presentedViewController: UIViewController, from presentingViewController: UIViewController) {} 391 | /// This method is called during the dismissal animation. Use this to animate anything alongside the menu animation. The super method does not need to be called. 392 | override func dismissalTransition(to presentedViewController: UIViewController, from presentingViewController: UIViewController) {} 393 | /// This method is called when the dismissal transition ends. Use this to finish any animations. The super method does not need to be called. 394 | override func dismissalTransitionDidEnd(to presentedViewController: UIViewController, from presentingViewController: UIViewController, _ completed: Bool) {} 395 | } 396 | ``` 397 |
398 | 399 | ## Known Issues 400 | * Issue [#258](https://github.com/jonkykong/SideMenu/issues/258). Using `presentingViewControllerUseSnapshot` can help preserve the experience. 401 | 402 | ## Thank You 403 | A special thank you to everyone that has [contributed](https://github.com/jonkykong/SideMenu/graphs/contributors) to this library to make it better. Your support is appreciated! 404 | 405 | ## License 406 | 407 | SideMenu is available under the MIT license. See the LICENSE file for more info. 408 | -------------------------------------------------------------------------------- /SideMenu.podspec: -------------------------------------------------------------------------------- 1 | # 2 | # Be sure to run `pod lib lint SideMenu.podspec' to ensure this is a 3 | # valid spec before submitting. 4 | # 5 | # Any lines starting with a # are optional, but their use is encouraged 6 | # To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html 7 | # 8 | 9 | Pod::Spec.new do |s| 10 | s.name = "SideMenu" 11 | s.version = "6.5.0" 12 | s.summary = "Simple side menu control for iOS in Swift inspired by Facebook. Right and Left sides. No coding required." 13 | 14 | # This description is used to generate tags and improve search results. 15 | # * Think: What does it do? Why did you write it? What is the focus? 16 | # * Try to keep it short, snappy and to the point. 17 | # * Write the description between the DESC delimiters below. 18 | # * Finally, don't worry about the indent, CocoaPods strips it! 19 | 20 | s.description = <<-DESC 21 | SideMenu is a simple and versatile side menu control. It's highly customizable, but can also be implemented in storyboard without a single line of code. The are three standard animation styles to choose from along with several other options for further customization if desired. Just type SideMenuManager.menu... and code completion will show you everything you can customize. 22 | DESC 23 | 24 | s.homepage = "https://github.com/jonkykong/SideMenu" 25 | s.screenshots = [ "https://raw.githubusercontent.com/jonkykong/SideMenu/master/etc/SlideOut.gif", "https://raw.githubusercontent.com/jonkykong/SideMenu/master/etc/SlideIn.gif", "https://raw.githubusercontent.com/jonkykong/SideMenu/master/etc/Dissolve.gif", "https://raw.githubusercontent.com/jonkykong/SideMenu/master/etc/InOut.gif" ] 26 | s.license = { :type => 'MIT', :file => 'LICENSE' } 27 | s.author = { "jonkykong" => "contact@jonkent.me" } 28 | s.source = { :git => "https://github.com/jonkykong/SideMenu.git", :tag => s.version.to_s } 29 | # s.social_media_url = 'https://twitter.com/' 30 | 31 | s.ios.deployment_target = '10.0' 32 | s.swift_version = '5.0' 33 | 34 | s.source_files = 'Pod/Classes/**/*' 35 | # s.resource_bundles = { 36 | # 'SideMenu' => ['Pod/Assets/*.png'] 37 | # } 38 | 39 | # s.public_header_files = 'Pod/Classes/**/*.h' 40 | # s.frameworks = 'UIKit', 'MapKit' 41 | # s.dependency 'AFNetworking', '~> 2.3' 42 | end 43 | -------------------------------------------------------------------------------- /SideMenu.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /SideMenu.xcodeproj/xcshareddata/xcschemes/SideMenu Example.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 44 | 45 | 51 | 52 | 53 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 76 | 78 | 84 | 85 | 86 | 87 | 93 | 95 | 101 | 102 | 103 | 104 | 106 | 107 | 110 | 111 | 112 | -------------------------------------------------------------------------------- /SideMenu.xcodeproj/xcshareddata/xcschemes/SideMenu.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 34 | 35 | 45 | 46 | 52 | 53 | 54 | 55 | 56 | 57 | 63 | 64 | 70 | 71 | 72 | 73 | 75 | 76 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /SideMenu.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /SideMenu.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /SideMenu.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /SideMenu/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 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | NSPrincipalClass 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /SideMenu/SideMenu.h: -------------------------------------------------------------------------------- 1 | // 2 | // SideMenu.h 3 | // SideMenu 4 | // 5 | // Created by Soheil on 31/10/16. 6 | // Copyright © 2016 jonkykong. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for SideMenu. 12 | FOUNDATION_EXPORT double SideMenuVersionNumber; 13 | 14 | //! Project version string for SideMenu. 15 | FOUNDATION_EXPORT const unsigned char SideMenuVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | 20 | -------------------------------------------------------------------------------- /SideMenu/module.modulemap: -------------------------------------------------------------------------------- 1 | framework module SideMenu { 2 | umbrella header "SideMenu.h" 3 | 4 | export * 5 | module * { export * } 6 | } 7 | -------------------------------------------------------------------------------- /etc/Dissolve.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonkykong/SideMenu/8bd4fd128923cf5494fa726839af8afe12908ad9/etc/Dissolve.gif -------------------------------------------------------------------------------- /etc/InOut.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonkykong/SideMenu/8bd4fd128923cf5494fa726839af8afe12908ad9/etc/InOut.gif -------------------------------------------------------------------------------- /etc/Screenshot1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonkykong/SideMenu/8bd4fd128923cf5494fa726839af8afe12908ad9/etc/Screenshot1.png -------------------------------------------------------------------------------- /etc/Screenshot2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonkykong/SideMenu/8bd4fd128923cf5494fa726839af8afe12908ad9/etc/Screenshot2.png -------------------------------------------------------------------------------- /etc/Screenshot3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonkykong/SideMenu/8bd4fd128923cf5494fa726839af8afe12908ad9/etc/Screenshot3.png -------------------------------------------------------------------------------- /etc/SlideIn.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonkykong/SideMenu/8bd4fd128923cf5494fa726839af8afe12908ad9/etc/SlideIn.gif -------------------------------------------------------------------------------- /etc/SlideOut.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonkykong/SideMenu/8bd4fd128923cf5494fa726839af8afe12908ad9/etc/SlideOut.gif --------------------------------------------------------------------------------