├── DropDownMenuKit.xcassets ├── Contents.json └── Ionicons-chevron-up.imageset │ ├── chevron-up.pdf │ └── Contents.json ├── Example ├── Assets.xcassets │ ├── Contents.json │ ├── Ionicons-ios-search.imageset │ │ ├── ios-search.pdf │ │ └── Contents.json │ ├── Ionicons-ios-checkmark-outline.imageset │ │ ├── ios-checkmark-outline.pdf │ │ └── Contents.json │ └── AppIcon.appiconset │ │ └── Contents.json ├── AppDelegate.swift ├── Info.plist ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard └── ViewController.swift ├── DropDownMenuKit.xcodeproj ├── project.xcworkspace │ └── contents.xcworkspacedata ├── xcshareddata │ └── xcschemes │ │ ├── DropDownMenuKit.xcscheme │ │ └── DropDownMenuExample.xcscheme └── project.pbxproj ├── .travis.yml ├── DropDownMenu.h ├── TODO.md ├── Info.plist ├── DropDownMenuKit.podspec ├── NEWS.md ├── .gitignore ├── DropDownMenuCell.swift ├── LICENSE ├── README.md ├── DropDownTitleView.swift └── DropDownMenu.swift /DropDownMenuKit.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Example/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Example/Assets.xcassets/Ionicons-ios-search.imageset/ios-search.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qmathe/DropDownMenuKit/HEAD/Example/Assets.xcassets/Ionicons-ios-search.imageset/ios-search.pdf -------------------------------------------------------------------------------- /DropDownMenuKit.xcassets/Ionicons-chevron-up.imageset/chevron-up.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qmathe/DropDownMenuKit/HEAD/DropDownMenuKit.xcassets/Ionicons-chevron-up.imageset/chevron-up.pdf -------------------------------------------------------------------------------- /Example/Assets.xcassets/Ionicons-ios-checkmark-outline.imageset/ios-checkmark-outline.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qmathe/DropDownMenuKit/HEAD/Example/Assets.xcassets/Ionicons-ios-checkmark-outline.imageset/ios-checkmark-outline.pdf -------------------------------------------------------------------------------- /DropDownMenuKit.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /DropDownMenuKit.xcassets/Ionicons-chevron-up.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "chevron-up.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } -------------------------------------------------------------------------------- /Example/Assets.xcassets/Ionicons-ios-search.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "ios-search.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } -------------------------------------------------------------------------------- /Example/Assets.xcassets/Ionicons-ios-checkmark-outline.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "ios-checkmark-outline.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: objective-c 2 | notifications: 3 | email: 4 | recipients: 5 | - quentin.mathe@gmail.com 6 | xcode_project: DropDownMenuKit.xcodeproj 7 | xcode_scheme: DropDownMenuExample 8 | osx_image: xcode10.2 9 | script: xcodebuild -project DropDownMenuKit.xcodeproj -scheme DropDownMenuExample build CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO CODE_SIGNING_ALLOWED=NO 10 | -------------------------------------------------------------------------------- /Example/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2016 Quentin Mathe 3 | 4 | Date: August 2016 5 | License: MIT 6 | */ 7 | 8 | import UIKit 9 | 10 | @UIApplicationMain 11 | class AppDelegate: UIResponder, UIApplicationDelegate { 12 | 13 | var window: UIWindow? 14 | 15 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 16 | return true 17 | } 18 | 19 | } 20 | 21 | -------------------------------------------------------------------------------- /DropDownMenu.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2016 Quentin Mathe 3 | 4 | Date: August 2016 5 | License: MIT 6 | */ 7 | 8 | #import 9 | 10 | //! Project version number for DropDownMenu. 11 | FOUNDATION_EXPORT double DropDownMenuVersionNumber; 12 | 13 | //! Project version string for DropDownMenu. 14 | FOUNDATION_EXPORT const unsigned char DropDownMenuVersionString[]; 15 | 16 | // In this header, you should import all the public headers of your framework using statements like #import 17 | 18 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | DropDownMenuKit TODO 2 | ==================== 3 | 4 | - Add DropDownMenu.titleView to let the menu manages the title view status (up vs down) 5 | - Add DropDownMenuDelegate.needsBackgroundTransition to let the client cancels the background alpha animation when a menu is hidden and another one is shown immediately 6 | - Add some presentation options for DropDownMenuCell to support UI patterns commonly seen in Apple's apps 7 | - May be make easier to customize DropDownMenuCell layout 8 | - Share code or reuse IconTextCell for DropDownMenuCell 9 | - Update example to showcase hiddenContentOffset 10 | - Better documentation (API and README) 11 | - Support grouping menu cells by section 12 | -------------------------------------------------------------------------------- /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 | 0.9 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 9 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /DropDownMenuKit.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | 3 | s.name = "DropDownMenuKit" 4 | s.version = "1.0" 5 | s.summary = "UIKit drop down menu, simple yet flexible and written in Swift" 6 | s.description = "DropDownMenuKit is a custom UIKit control to show a menu attached to the navigation bar or toolbar. The menu appears with a sliding animation and can be deeply customized. For example, with icons, embedded controls, or a checkmark to denote a selected row among multiple menu cells." 7 | s.homepage = "https://github.com/qmathe/DropDownMenuKit" 8 | s.screenshots = "http://www.quentinmathe.com/github/DropDownMenuKit/App%20History%20Menu%20-%20iPhone%205.png" 9 | s.license = { :type => "MIT", :file => "LICENSE" } 10 | s.author = { "Quentin Mathé" => "quentin.mathe@gmail.com" } 11 | s.social_media_url = "http://twitter.com/quentin_mathe" 12 | 13 | s.swift_version = "5.0" 14 | s.platform = :ios, "11.0" 15 | 16 | s.source = { :git => "https://github.com/qmathe/DropDownMenuKit.git", :tag => "1.0" } 17 | s.source_files = "*.swift", "*.{h,m}" 18 | s.public_header_files = "*.h" 19 | # Each xcassets directory must be in a distinct resource bundle (otherwise multiple assets.car end up in the same bundle) 20 | s.resource_bundles = { "DropDownMenuKitAssets" => ["DropDownMenuKit.xcassets"] } 21 | end 22 | -------------------------------------------------------------------------------- /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 | APPL 17 | CFBundleShortVersionString 18 | 0.9 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 9 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UIRequiredDeviceCapabilities 30 | 31 | armv7 32 | 33 | UISupportedInterfaceOrientations 34 | 35 | UIInterfaceOrientationPortrait 36 | UIInterfaceOrientationLandscapeLeft 37 | UIInterfaceOrientationLandscapeRight 38 | 39 | UIApplicationSceneManifest 40 | 41 | UIApplicationSupportsMultipleScenes 42 | 43 | UISceneConfigurations 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /NEWS.md: -------------------------------------------------------------------------------- 1 | DropDownMenuKit NEWS 2 | ==================== 3 | 4 | 1.0 5 | --- 6 | 7 | - Fix crash on icon loading when subclassing DropDownMenuTitleView 8 | - Fix name conflicts between DropDownMenuKit and app assets when compiling as static library with CocoaPods 9 | - Update example to ensure overlays don't cover navigation bar and toolbar in iOS 11 and higher 10 | 11 | 0.9 12 | --- 13 | 14 | - Migrated to Swift 5 15 | - New custom transition API 16 | - Fixed menu cells not always visible during hiding animation on iOS 11 and higher 17 | 18 | 0.8.6 19 | ----- 20 | 21 | - Migrated to Swift 4.2 22 | 23 | 0.8.5 24 | ----- 25 | 26 | - Migrated to Swift 4 27 | - Improved example to showcase: 28 | - adding/removing menu entries 29 | - menu scrolling 30 | - long title support 31 | - Now usable in app extensions 32 | - DropDownMenuKit > Build Settings > Other Swift Flags must include -DAPP_EXTENSION 33 | 34 | 0.8.4 35 | ----- 36 | 37 | - iOS 11 support 38 | - Improved menu to become scrollable menu when too many rows are presented 39 | - DropDownMenu.visibleContentOffset has been replaced with visibleContentInsets to support this 40 | - Fixed memory leak preventing menu to be released 41 | 42 | 0.8.3 43 | ----- 44 | 45 | - New customization options 46 | - row height 47 | - icon size 48 | - up/down images 49 | - Fixed title to always appear correctly centered in navigation bar 50 | - long titles are now supported 51 | - Fixed height of menu view when setting its cells after it has been resized 52 | 53 | 0.8.2 54 | ----- 55 | 56 | - Migrated to Swift 3 57 | 58 | 0.8.1 59 | ----- 60 | 61 | - Added DropDownMenu.selectMenuCell(:) 62 | - Fixed incorrect arrow orientation after 3 quick taps on the title view 63 | - Don't let the user toggles the menu when a toggling animation is already underway 64 | 65 | 0.8 66 | --- 67 | 68 | - First release 69 | 70 | -------------------------------------------------------------------------------- /Example/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/swift 3 | 4 | ### Swift ### 5 | # Xcode 6 | # 7 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 8 | 9 | PodTestInstall/ 10 | 11 | ## Build generated 12 | build/ 13 | DerivedData/ 14 | 15 | ## Various settings 16 | *.pbxuser 17 | !default.pbxuser 18 | *.mode1v3 19 | !default.mode1v3 20 | *.mode2v3 21 | !default.mode2v3 22 | *.perspectivev3 23 | !default.perspectivev3 24 | xcuserdata/ 25 | 26 | ## Other 27 | *.moved-aside 28 | *.xccheckout 29 | *.xcscmblueprint 30 | 31 | ## Obj-C/Swift specific 32 | *.hmap 33 | *.ipa 34 | *.dSYM.zip 35 | *.dSYM 36 | 37 | ## Playgrounds 38 | timeline.xctimeline 39 | playground.xcworkspace 40 | 41 | # Swift Package Manager 42 | # 43 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 44 | # Packages/ 45 | # Package.pins 46 | # Package.resolved 47 | .build/ 48 | 49 | # CocoaPods 50 | # 51 | # We recommend against adding the Pods directory to your .gitignore. However 52 | # you should judge for yourself, the pros and cons are mentioned at: 53 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 54 | # 55 | # Pods/ 56 | # 57 | # Add this line if you want to avoid checking in source code from the Xcode workspace 58 | *.xcworkspace 59 | 60 | # Carthage 61 | # 62 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 63 | Carthage/Checkouts 64 | 65 | Carthage/Build 66 | 67 | # fastlane 68 | # 69 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 70 | # screenshots whenever they are needed. 71 | # For more information about the recommended setup visit: 72 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 73 | 74 | fastlane/report.xml 75 | fastlane/Preview.html 76 | fastlane/screenshots/**/*.png 77 | fastlane/test_output 78 | 79 | # Code Injection 80 | # 81 | # After new code Injection tools there's a generated folder /iOSInjectionProject 82 | # https://github.com/johnno1962/injectionforxcode 83 | 84 | iOSInjectionProject/ 85 | 86 | 87 | # End of https://www.gitignore.io/api/swift 88 | -------------------------------------------------------------------------------- /DropDownMenuCell.swift: -------------------------------------------------------------------------------- 1 | /** 2 | Copyright (C) 2015 Quentin Mathe 3 | 4 | Date: May 2015 5 | License: MIT 6 | */ 7 | 8 | import UIKit 9 | 10 | open class DropDownMenuCell : UITableViewCell { 11 | 12 | open var customView: UIView? { 13 | didSet { 14 | guard let customView = customView else { 15 | return 16 | } 17 | contentView.addSubview(customView) 18 | } 19 | } 20 | /// For an app extension, the selector must take an argument. 21 | open var menuAction: Selector? 22 | /// For an app extension, the menu target must not be nil. 23 | open weak var menuTarget: AnyObject? 24 | open var showsCheckmark = true 25 | open var rowHeight: CGFloat = 44 26 | 27 | // MARK: - Initialization 28 | 29 | override public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { 30 | fatalError("init(style:reuseIdentifier:) is not supported") 31 | } 32 | 33 | public init() { 34 | super.init(style: .default, reuseIdentifier: NSStringFromClass(DropDownMenuCell.self)) 35 | setUp() 36 | } 37 | 38 | required public init?(coder aDecoder: NSCoder) { 39 | super.init(coder: aDecoder) 40 | } 41 | 42 | open override func awakeFromNib() { 43 | setUp() 44 | } 45 | 46 | private func setUp() { 47 | textLabel?.text = "Untitled" 48 | } 49 | 50 | // MARK: - Layout 51 | 52 | open var iconSize = CGSize(width: 24, height: 24) 53 | 54 | override open func layoutSubviews() { 55 | 56 | if let textLabel = textLabel { 57 | if customView != nil && textLabel.text == nil { 58 | textLabel.text = "Custom View Origin Hint" 59 | } 60 | textLabel.isHidden = customView != nil 61 | } 62 | 63 | super.layoutSubviews() 64 | 65 | if let imageView = imageView, imageView.image != nil { 66 | imageView.frame.size = iconSize 67 | imageView.center = CGPoint(x: imageView.center.x, y: bounds.size.height / 2) 68 | } 69 | 70 | if let customView = customView { 71 | if let textLabel = textLabel, imageView?.image != nil { 72 | customView.frame.origin.x = textLabel.frame.origin.x 73 | } else { 74 | customView.center.x = bounds.width / 2 75 | } 76 | customView.center.y = bounds.height / 2 77 | 78 | let margin: CGFloat = 5 // imageView?.frame.origin.x ?? 15 79 | 80 | if customView.frame.maxX + margin > bounds.width { 81 | customView.frame.size.width = bounds.width - customView.frame.origin.x - margin 82 | } 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Quentin Mathé 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -- 24 | 25 | Assets.xcassets includes icons from Ionicons (http://ionicons.com) licensed under: 26 | 27 | The MIT License (MIT) 28 | 29 | Copyright (c) 2016 Drifty (http://drifty.com/) 30 | 31 | Permission is hereby granted, free of charge, to any person obtaining a copy 32 | of this software and associated documentation files (the "Software"), to deal 33 | in the Software without restriction, including without limitation the rights 34 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 35 | copies of the Software, and to permit persons to whom the Software is 36 | furnished to do so, subject to the following conditions: 37 | 38 | The above copyright notice and this permission notice shall be included in 39 | all copies or substantial portions of the Software. 40 | 41 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 42 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 43 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 44 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 45 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 46 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 47 | THE SOFTWARE. 48 | -------------------------------------------------------------------------------- /DropDownMenuKit.xcodeproj/xcshareddata/xcschemes/DropDownMenuKit.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 44 | 50 | 51 | 52 | 53 | 59 | 60 | 66 | 67 | 68 | 69 | 71 | 72 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | DropDownMenuKit 2 | =============== 3 | 4 | [![Build Status](https://travis-ci.org/qmathe/DropDownMenuKit.svg?branch=master)](https://travis-ci.org/qmathe/DropDownMenuKit) 5 | [![Platforms iOS](https://img.shields.io/badge/Platforms-iOS-lightgray.svg?style=flat)](http://www.apple.com) 6 | [![Language Swift 5](https://img.shields.io/badge/Language-Swift%205-orange.svg?style=flat)](https://swift.org) 7 | [![License MIT](https://img.shields.io/badge/license-MIT-blue.svg?style=flat)](https://github.com/qmathe/DropDownMenuKit/LICENSE) 8 | 9 | DropDownMenuKit is a custom UIKit control to show a menu attached to the navigation bar or toolbar. The menu appears with a sliding animation and can be deeply customized. For example, with icons, embedded controls, or a checkmark to denote a selected row among multiple menu cells. 10 | 11 | The control is made up of three parts: 12 | 13 | - DropDownMenu: the menu itself, a UIView subclass that contains a UITableView presenting one or more DropDownMenuCell(s) 14 | - DropDownMenuCell: a menu entry, implemented as a UITableViewCell subclass 15 | - DropDownMenuTitleView: an optional title view to toggle the menu, which is usually put in the navigation bar and acts as a disclosure indicator 16 | 17 | Screenshot 18 | Screenshot 19 | 20 | To see in action, take a look at the very beginning of [Placeboard](http://www.placeboardapp.com) demo video. 21 | 22 | Compatibility 23 | ------------- 24 | 25 | DropDownMenuKit requires at least Xcode 10.2 (introducing Swift 5) and supports iOS 11 and higher. 26 | 27 | For versions compatible with Swift 3 and 4, see branches named _swift-3/4.x_. 28 | 29 | For now, the code base remains compatible with older iOS versions (8 or higher), but these older 30 | versions are not supported anymore. 31 | 32 | Installation 33 | ------------ 34 | 35 | ### Carthage 36 | 37 | Add the following line to your Cartfile, run `carthage update` to build the framework and drag the built DropDownMenuKit.framework into your Xcode project. 38 | 39 | github "qmathe/DropDownMenuKit" 40 | 41 | ### CocoaPods 42 | 43 | Add the following lines to your Podfile and run `pod install` with CocoaPods 1.9 or newer. 44 | 45 | pod "DropDownMenuKit" 46 | 47 | ### Manually 48 | 49 | If you don't use Carthage or CocoaPods, it's possible to drag the built framework or embed the source files into your project. 50 | 51 | #### Framework 52 | 53 | Build DropDownMenuKit framework and drop it into your Xcode project. 54 | 55 | #### Files 56 | 57 | Drop DropDownMenu.swift, DropDownMenuCell.swift, DropDownTitleView.swift and DropDownMenuKit.xcassets into your Xcode project. 58 | 59 | 60 | App Extension Usage 61 | ------------------------- 62 | 63 | ### Build Settings 64 | 65 | Add **-DAPP_EXTENSION** to _DropDownMenuKit > Build Settings > Other Swift Flags_. 66 | 67 | ### Restrictions 68 | 69 | - `DropDownMenuCell.menuAction` must take a single argument 70 | - `DropDownMenuCell.menuTarget` must not be nil 71 | -------------------------------------------------------------------------------- /DropDownMenuKit.xcodeproj/xcshareddata/xcschemes/DropDownMenuExample.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 41 | 42 | 52 | 54 | 60 | 61 | 62 | 63 | 69 | 71 | 77 | 78 | 79 | 80 | 82 | 83 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /Example/Base.lproj/Main.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 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /DropDownTitleView.swift: -------------------------------------------------------------------------------- 1 | /** 2 | Copyright (C) 2015 Quentin Mathe 3 | 4 | Date: June 2015 5 | License: MIT 6 | */ 7 | 8 | import UIKit 9 | 10 | open class DropDownTitleView : UIControl { 11 | 12 | open var iconSize = CGSize(width: 12, height: 12) { 13 | didSet { 14 | setNeedsLayout() 15 | } 16 | } 17 | // When compiling as a static library with CocoaPods, image assets end ups in the main bundle 18 | // rather than the framework bundle: 19 | // AppName.app/Resources/DropDownMenuKit/Assets.car 20 | lazy var imageBundle: Bundle = { 21 | let bundle = Bundle(for: DropDownTitleView.self) 22 | 23 | if let podBundleURL = bundle.url(forResource: "DropDownMenuKitAssets", withExtension: "bundle") { 24 | guard let podBundle = Bundle(url: podBundleURL) else { 25 | fatalError("Missing image assets for DropDownMenUKit") 26 | } 27 | return podBundle 28 | } else { 29 | return bundle 30 | } 31 | }() 32 | open lazy var menuDownImageView: UIImageView = { 33 | let menuDownImageView = UIImageView(image: self.imageNamed("Ionicons-chevron-up")) 34 | 35 | menuDownImageView.tintColor = UIColor.black 36 | menuDownImageView.transform = CGAffineTransform(scaleX: 1, y: -1) 37 | 38 | return menuDownImageView 39 | }() 40 | open lazy var menuUpImageView: UIImageView = { 41 | let menuUpImageView = UIImageView(image: self.imageNamed("Ionicons-chevron-up")) 42 | 43 | menuUpImageView.tintColor = UIColor.black 44 | 45 | return menuUpImageView 46 | }() 47 | open lazy var imageView: UIView = { 48 | // For flip animation, we need a container view 49 | // See http://stackoverflow.com/questions/11847743/transitionfromview-and-strange-behavior-with-flip 50 | return UIView(frame: CGRect(origin: CGPoint.zero, size: self.iconSize)) 51 | }() 52 | open lazy var titleLabel: UILabel = { 53 | let titleLabel = UILabel() 54 | 55 | titleLabel.font = UIFont.boldSystemFont(ofSize: titleLabel.font.pointSize) 56 | titleLabel.textColor = UIColor.white 57 | 58 | return titleLabel 59 | }() 60 | open var title: String? { 61 | get { 62 | return titleLabel.text 63 | } 64 | set { 65 | titleLabel.text = newValue 66 | titleLabel.sizeToFit() 67 | titleWidth = titleLabel.frame.width 68 | layoutSubviews() 69 | } 70 | } 71 | private var titleWidth: CGFloat = 0 72 | open var isUp: Bool { return menuUpImageView.superview != nil } 73 | open var toggling = false 74 | 75 | // MARK: - Initialization 76 | 77 | override public init(frame: CGRect) { 78 | super.init(frame: frame) 79 | setUp() 80 | } 81 | 82 | required public init?(coder aDecoder: NSCoder) { 83 | super.init(coder: aDecoder) 84 | } 85 | 86 | // To support adding outlets/actions later and access them during initialization 87 | override open func awakeFromNib() { 88 | setUp() 89 | } 90 | 91 | func setUp() { 92 | imageView.addSubview(menuDownImageView) 93 | 94 | addSubview(titleLabel) 95 | addSubview(imageView) 96 | 97 | if #available(iOS 11, *) { 98 | translatesAutoresizingMaskIntoConstraints = false 99 | } else { 100 | autoresizingMask = [.flexibleWidth, .flexibleHeight] 101 | } 102 | 103 | let recognizer = UITapGestureRecognizer(target: self, action: #selector(DropDownTitleView.toggleMenu)) 104 | 105 | isUserInteractionEnabled = true 106 | addGestureRecognizer(recognizer) 107 | 108 | title = "Untitled" 109 | } 110 | 111 | open func imageNamed(_ name: String) -> UIImage { 112 | return UIImage(named: name, in: imageBundle, compatibleWith: nil)! 113 | } 114 | 115 | // MARK: - Layout 116 | 117 | private let spacing: CGFloat = 4 118 | // For iOS 11 and above 119 | override open var intrinsicContentSize: CGSize { 120 | return UIView.layoutFittingExpandedSize 121 | } 122 | 123 | // For iOS 10 and below 124 | open override func willMove(toSuperview newSuperview: UIView?) { 125 | guard let superview = newSuperview else { 126 | return 127 | } 128 | // Will trigger layoutSubviews() without having resize the title view yet 129 | // e.g. (origin = (x = 0, y = 0), size = (width = 320, height = 44) 130 | frame = superview.bounds 131 | } 132 | 133 | // Centers the title when DropDownMenu.selectMenuCell() isn't called while creating the menu 134 | open override func didMoveToWindow() { 135 | // Will trigger layoutSubviews() with the title view resized according to autoresizing 136 | // e.g. (origin = (x = 58, y = 0), size = (width = 211.5, height = 44)) 137 | layoutSubviews() 138 | } 139 | 140 | open override func layoutSubviews() { 141 | super.layoutSubviews() 142 | 143 | menuDownImageView.frame.size = iconSize 144 | menuUpImageView.frame.size = iconSize 145 | imageView.frame.size = iconSize 146 | 147 | let maxTitleWidth = frame.width - 2 * (spacing + imageView.frame.width) 148 | 149 | titleLabel.center = CGPoint(x: frame.width / 2, y: frame.height / 2) 150 | if titleWidth > maxTitleWidth { 151 | titleLabel.frame.origin.x = spacing + imageView.frame.width 152 | titleLabel.frame.size.width = maxTitleWidth 153 | } else { 154 | titleLabel.frame.size.width = titleWidth 155 | } 156 | 157 | imageView.frame.origin.x = titleLabel.frame.maxX + spacing 158 | imageView.center.y = frame.height / 2 159 | } 160 | 161 | // MARK: - Actions 162 | 163 | @IBAction open func toggleMenu() { 164 | if toggling { 165 | return 166 | } 167 | toggling = true 168 | let viewToReplace = isUp ? menuUpImageView : menuDownImageView 169 | let replacementView = isUp ? menuDownImageView : menuUpImageView 170 | let options = isUp ? UIView.AnimationOptions.transitionFlipFromTop : UIView.AnimationOptions.transitionFlipFromBottom 171 | 172 | sendActions(for: .touchUpInside) 173 | 174 | UIView.transition(from: viewToReplace, 175 | to: replacementView, 176 | duration: 0.4, 177 | options: options, 178 | completion: { (Bool) in 179 | self.sendActions(for: .valueChanged) 180 | self.toggling = false 181 | }) 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /Example/ViewController.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2016 Quentin Mathe 3 | 4 | Date: August 2016 5 | License: MIT 6 | */ 7 | 8 | import UIKit 9 | import DropDownMenuKit 10 | 11 | class ViewController: UIViewController, DropDownMenuDelegate { 12 | 13 | // MARK: - UI Elements 14 | 15 | var titleView: DropDownTitleView! 16 | @IBOutlet var navigationBarMenu: DropDownMenu! 17 | @IBOutlet var toolbarMenu: DropDownMenu! 18 | 19 | // MARK: - Initialization 20 | 21 | override func viewDidLoad() { 22 | super.viewDidLoad() 23 | let title = prepareNavigationBarMenuTitleView() 24 | 25 | prepareNavigationBarMenu(title) 26 | prepareToolbarMenu() 27 | 28 | navigationBarMenu.container = view 29 | toolbarMenu.container = view 30 | } 31 | 32 | override func viewDidAppear(_ animated: Bool) { 33 | super.viewDidAppear(animated) 34 | updateMenuContentInsets() 35 | } 36 | 37 | func prepareNavigationBarMenuTitleView() -> String { 38 | titleView = DropDownTitleView() 39 | titleView.addTarget(self, 40 | action: #selector(ViewController.willToggleNavigationBarMenu(_:)), 41 | for: .touchUpInside) 42 | titleView.addTarget(self, 43 | action: #selector(ViewController.didToggleNavigationBarMenu(_:)), 44 | for: .valueChanged) 45 | titleView.titleLabel.textColor = UIColor.black 46 | 47 | navigationItem.titleView = titleView 48 | 49 | return titleView.title! 50 | } 51 | 52 | func prepareNavigationBarMenu(_ currentChoice: String) { 53 | navigationBarMenu = DropDownMenu(frame: view.bounds) 54 | navigationBarMenu.delegate = self 55 | 56 | let firstCell = DropDownMenuCell() 57 | let longTitle = "The quick brown fox jumps over the lazy dog" 58 | 59 | firstCell.textLabel!.text = longTitle 60 | firstCell.menuAction = #selector(ViewController.choose(_:)) 61 | firstCell.menuTarget = self 62 | if currentChoice == longTitle { 63 | firstCell.accessoryType = .checkmark 64 | } 65 | 66 | let secondCell = DropDownMenuCell() 67 | let shortTitle = "Short" 68 | 69 | secondCell.textLabel!.text = shortTitle 70 | secondCell.rowHeight = 60 71 | secondCell.menuAction = #selector(ViewController.choose(_:)) 72 | secondCell.menuTarget = self 73 | if currentChoice == shortTitle { 74 | firstCell.accessoryType = .checkmark 75 | } 76 | 77 | navigationBarMenu.menuCells = [firstCell, secondCell] 78 | navigationBarMenu.selectMenuCell(secondCell) 79 | 80 | // For a simple gray overlay in background 81 | navigationBarMenu.backgroundView = UIView(frame: navigationBarMenu.bounds) 82 | navigationBarMenu.backgroundView!.backgroundColor = UIColor.black 83 | navigationBarMenu.backgroundAlpha = 0.7 84 | } 85 | 86 | func prepareToolbarMenu() { 87 | toolbarMenu = DropDownMenu(frame: view.bounds) 88 | toolbarMenu.delegate = self 89 | 90 | let selectCell = DropDownMenuCell() 91 | 92 | selectCell.textLabel!.text = "Change Title Icons" 93 | selectCell.imageView!.image = UIImage(named: "Ionicons-ios-checkmark-outline") 94 | selectCell.showsCheckmark = false 95 | selectCell.menuAction = #selector(ViewController.changeTitleIcons as (ViewController) -> () -> ()) 96 | selectCell.menuTarget = self 97 | 98 | let sortKeys = ["Name", "Date", "Size"] 99 | let sortCell = DropDownMenuCell() 100 | let sortSwitcher = UISegmentedControl(items: sortKeys) 101 | 102 | sortSwitcher.selectedSegmentIndex = sortKeys.firstIndex(of: "Name")! 103 | sortSwitcher.addTarget(self, action: #selector(ViewController.sort(_:)), for: .valueChanged) 104 | 105 | sortCell.customView = sortSwitcher 106 | sortCell.textLabel!.text = "Sort" 107 | sortCell.imageView!.image = UIImage(named: "Ionicons-ios-search") 108 | sortCell.showsCheckmark = false 109 | 110 | toolbarMenu.menuCells = [selectCell, sortCell] 111 | toolbarMenu.direction = .up 112 | 113 | // For a simple gray overlay in background 114 | toolbarMenu.backgroundView = UIView(frame: toolbarMenu.bounds) 115 | toolbarMenu.backgroundView!.backgroundColor = UIColor.black 116 | toolbarMenu.backgroundAlpha = 0.7 117 | } 118 | 119 | // MARK: - Layout 120 | 121 | // Resize overlays in order they doesn't cover navigation bar and toolbar (this can be changed 122 | // to cover more or less on screen depending on your needs). 123 | func updateOverlayInsets() { 124 | if #available(iOS 11, *) { 125 | updateOverlayInsets(for: navigationBarMenu) 126 | updateOverlayInsets(for: toolbarMenu) 127 | } 128 | } 129 | 130 | func updateOverlayInsets(for menu: DropDownMenu) { 131 | let overlayTop = menu.visibleContentInsets.top 132 | let overlayBottom = menu.visibleContentInsets.bottom 133 | let overlayHeight = menu.frame.height - overlayTop - overlayBottom 134 | 135 | menu.backgroundView?.frame.origin.y = overlayTop 136 | menu.backgroundView?.frame.size.height = overlayHeight 137 | } 138 | 139 | func updateMenuContentInsets() { 140 | var visibleContentInsets: UIEdgeInsets 141 | 142 | if #available(iOS 11, *) { 143 | visibleContentInsets = view.safeAreaInsets 144 | } else { 145 | visibleContentInsets = 146 | UIEdgeInsets(top: navigationController!.navigationBar.frame.size.height + statusBarHeight(), 147 | left: 0, 148 | bottom: navigationController!.toolbar.frame.size.height, 149 | right: 0) 150 | } 151 | 152 | navigationBarMenu.visibleContentInsets = visibleContentInsets 153 | toolbarMenu.visibleContentInsets = visibleContentInsets 154 | 155 | updateOverlayInsets() 156 | } 157 | 158 | override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { 159 | super.viewWillTransition(to: size, with: coordinator) 160 | 161 | coordinator.animate(alongsideTransition: { _ in 162 | // If we put this only in -viewDidLayoutSubviews, menu animation is 163 | // messed up when selecting an item 164 | self.updateMenuContentInsets() 165 | }, completion: nil) 166 | } 167 | 168 | // MARK: - Updating UI 169 | 170 | func validateBarItems() { 171 | navigationItem.leftBarButtonItem?.isEnabled = titleView.isUp && !navigationBarMenu.menuCells.isEmpty 172 | navigationItem.rightBarButtonItem?.isEnabled = titleView.isUp 173 | } 174 | 175 | // MARK: - Actions 176 | 177 | @IBAction func choose(_ sender: AnyObject) { 178 | titleView.title = (sender as! DropDownMenuCell).textLabel!.text 179 | } 180 | 181 | @IBAction func removeNavigationBarMenuCell() { 182 | navigationBarMenu.menuCells = Array(navigationBarMenu.menuCells.dropLast()) 183 | validateBarItems() 184 | } 185 | 186 | @IBAction func addNavigationBarMenuCell() { 187 | let cell = DropDownMenuCell() 188 | 189 | cell.textLabel!.text = String(navigationBarMenu.menuCells.count) 190 | cell.menuAction = #selector(ViewController.choose(_:)) 191 | cell.menuTarget = self 192 | 193 | navigationBarMenu.menuCells += [cell] 194 | validateBarItems() 195 | } 196 | 197 | @IBAction func changeTitleIcons() { 198 | titleView.iconSize = CGSize(width: 24, height: 24) 199 | titleView.menuDownImageView.image = UIImage(named: "Ionicons-ios-checkmark-outline") 200 | titleView.menuDownImageView.transform = CGAffineTransform.identity 201 | titleView.menuDownImageView.tintColor = UIColor.green 202 | titleView.menuUpImageView.image = UIImage(named: "Ionicons-ios-search") 203 | } 204 | 205 | @IBAction func sort(_ sender: AnyObject) { 206 | print("Sent sort action") 207 | } 208 | 209 | @IBAction func showToolbarMenu() { 210 | if titleView.isUp { 211 | titleView.toggleMenu() 212 | } 213 | toolbarMenu.show() 214 | } 215 | 216 | @IBAction func willToggleNavigationBarMenu(_ sender: DropDownTitleView) { 217 | toolbarMenu.hide() 218 | 219 | if sender.isUp { 220 | navigationBarMenu.hide() 221 | } else { 222 | navigationBarMenu.show() 223 | } 224 | } 225 | 226 | @IBAction func didToggleNavigationBarMenu(_ sender: DropDownTitleView) { 227 | print("Sent did toggle navigation bar menu action") 228 | validateBarItems() 229 | } 230 | 231 | func didTapInDropDownMenuBackground(_ menu: DropDownMenu) { 232 | if menu == navigationBarMenu { 233 | titleView.toggleMenu() 234 | } else { 235 | menu.hide() 236 | } 237 | } 238 | } 239 | 240 | 241 | func statusBarHeight() -> CGFloat { 242 | let statusBarSize = UIApplication.shared.statusBarFrame.size 243 | return min(statusBarSize.width, statusBarSize.height) 244 | } 245 | -------------------------------------------------------------------------------- /DropDownMenu.swift: -------------------------------------------------------------------------------- 1 | /** 2 | Copyright (C) 2015 Quentin Mathe 3 | 4 | Date: May 2015 5 | License: MIT 6 | */ 7 | 8 | import UIKit 9 | 10 | @objc public protocol DropDownMenuDelegate { 11 | func didTapInDropDownMenuBackground(_ menu: DropDownMenu) 12 | } 13 | 14 | public enum DropDownMenuRevealDirection { 15 | case up 16 | case down 17 | } 18 | 19 | 20 | open class DropDownMenu : UIView, UITableViewDataSource, UITableViewDelegate, UIGestureRecognizerDelegate { 21 | 22 | open weak var delegate: DropDownMenuDelegate? 23 | open weak var container: UIView? { 24 | didSet { 25 | removeFromSuperview() 26 | container?.addSubview(self) 27 | } 28 | } 29 | // The content view fills the entire container, so we can use it to fade 30 | // the background view in and out. 31 | // 32 | // By default, it contains the menu view, but other subviews can be added to 33 | // it and laid out by overriding -layoutSubviews. 34 | public let contentView = UIView(frame: .zero) 35 | public let menuView = UITableView(frame: .zero) 36 | open var menuCells = [DropDownMenuCell]() { 37 | didSet { 38 | menuView.reloadData() 39 | setNeedsLayout() 40 | } 41 | } 42 | // The background view to be faded out with the background alpha, when the 43 | // menu slides over it 44 | open var backgroundView: UIView? { 45 | didSet { 46 | oldValue?.removeFromSuperview() 47 | backgroundView?.autoresizingMask = [.flexibleWidth, .flexibleHeight] 48 | backgroundView?.alpha = oldValue?.alpha ?? 0 49 | if let backgroundView = backgroundView { 50 | insertSubview(backgroundView, belowSubview: contentView) 51 | } 52 | } 53 | } 54 | open var backgroundAlpha = CGFloat(1) 55 | 56 | // MARK: - Initialization 57 | 58 | override public init(frame: CGRect) { 59 | super.init(frame: frame) 60 | setUp() 61 | } 62 | 63 | required public init?(coder aDecoder: NSCoder) { 64 | super.init(coder: aDecoder) 65 | } 66 | 67 | open override func awakeFromNib() { 68 | setUp() 69 | } 70 | 71 | private func setUp() { 72 | menuView.frame.size = frame.size 73 | menuView.autoresizingMask = .flexibleWidth 74 | if #available(iOS 11.0, *) { 75 | // Prevent table cells to be shifted downwards abruptly before the menu slides up under the navigation bar 76 | menuView.contentInsetAdjustmentBehavior = .never 77 | menuView.insetsContentViewsToSafeArea = false 78 | } 79 | menuView.isScrollEnabled = true 80 | menuView.bounces = false 81 | menuView.showsVerticalScrollIndicator = true 82 | menuView.showsHorizontalScrollIndicator = false 83 | menuView.dataSource = self 84 | menuView.delegate = self 85 | 86 | contentView.frame.size = frame.size 87 | contentView.autoresizingMask = [.flexibleWidth] 88 | contentView.addSubview(menuView) 89 | 90 | let gesture = UITapGestureRecognizer(target: self, action: #selector(DropDownMenu.tap(_:))) 91 | gesture.delegate = self 92 | 93 | addGestureRecognizer(gesture) 94 | autoresizingMask = [.flexibleWidth, .flexibleHeight] 95 | isHidden = true 96 | addSubview(contentView) 97 | } 98 | 99 | // MARK: - Layout 100 | 101 | // This hidden insets can be used to customize the position of the menu at 102 | // the end of the hiding animation. 103 | // 104 | // If the container doesn't extend under the toolbar and navigation bar, 105 | // this is useful to ensure the hiding animation continues until the menu is 106 | // positioned outside of the screen, rather than stopping the animation when 107 | // the menu is covered by the toolbar or navigation bar. 108 | // 109 | // Left and right insets are currently ignored. 110 | open var hiddenContentInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) { 111 | didSet { 112 | setNeedsLayout() 113 | } 114 | } 115 | // This visible insets can be used to customize the position of the menu 116 | // at the end of the showing animation. 117 | // 118 | // If the container extends under the toolbar and navigation bar, this is 119 | // useful to ensure the menu won't be covered by the toolbar or navigation 120 | // bar once the showing animation is done. 121 | // 122 | // Left and right insets are currently ignored. 123 | open var visibleContentInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) { 124 | didSet { 125 | // Menu height needs to be recomputed 126 | setNeedsLayout() 127 | } 128 | } 129 | open var direction = DropDownMenuRevealDirection.down { 130 | didSet { 131 | setNeedsLayout() 132 | } 133 | } 134 | 135 | public enum Operation { 136 | case show 137 | case hide 138 | } 139 | 140 | private func updateContentViewPosition(on operation: Operation) { 141 | guard let container = container else { 142 | fatalError("DropDownMenu.container must have been set to layout subviews") 143 | } 144 | switch (operation, direction) { 145 | case (.hide, .down): 146 | contentView.frame.origin.y = hiddenContentInsets.top - contentView.frame.height 147 | case (.hide, .up): 148 | contentView.frame.origin.y = container.frame.height - hiddenContentInsets.bottom 149 | case (.show, .down): 150 | contentView.frame.origin.y = visibleContentInsets.top 151 | case (.show, .up): 152 | contentView.frame.origin.y = container.frame.height - contentView.frame.height - visibleContentInsets.bottom 153 | } 154 | } 155 | 156 | open override func layoutSubviews() { 157 | super.layoutSubviews() 158 | 159 | let contentHeight = menuCells.reduce(0) { $0 + $1.rowHeight } 160 | let maxContentHeight = frame.height - visibleContentInsets.bottom - visibleContentInsets.top 161 | let scrollable = contentHeight > maxContentHeight 162 | 163 | menuView.frame.size.height = scrollable ? maxContentHeight : contentHeight 164 | contentView.frame.size.height = menuView.frame.height 165 | updateContentViewPosition(on: isHidden ? .hide : .show) 166 | 167 | // Reset scroll view content offset after rotation 168 | if menuView.visibleCells.isEmpty { 169 | return 170 | } 171 | menuView.scrollToRow(at: IndexPath(row: 0, section: 0), at: .top, animated: false) 172 | } 173 | 174 | // MARK: - Animations 175 | 176 | public struct Transition { 177 | public struct Animation { 178 | public typealias Block = () -> () 179 | 180 | public let before: Block 181 | public let change: Block 182 | public let after: Block 183 | 184 | public init(before: @escaping Block = {}, change: @escaping Block, after: @escaping Block = {}) { 185 | self.before = before 186 | self.change = change 187 | self.after = after 188 | } 189 | } 190 | 191 | /// The duration taken by the animation when hiding/showing the menu and its background. 192 | public var duration: TimeInterval 193 | fileprivate let delay: TimeInterval = 0 194 | public let options: AnimationOptions 195 | 196 | public var show: [Animation] 197 | public var hide: [Animation] 198 | } 199 | 200 | public lazy var transition = Transition( 201 | duration: 0.4, 202 | options: AnimationOptions(), 203 | show: [showMenuAnimation, showBackgroundAnimation], 204 | hide: [hideMenuAnimation, hideBackgroundAnimation] 205 | ) 206 | 207 | /// The animation part in charge of showing the menu (doesn't include the background animation). 208 | public private(set) lazy var showMenuAnimation = Transition.Animation(change: { 209 | self.updateContentViewPosition(on: .show) 210 | }) 211 | /// The animation part in charge of showing the background (doesn't include the menu animation). 212 | public private(set) lazy var showBackgroundAnimation = Transition.Animation(change: { 213 | self.backgroundView?.alpha = self.backgroundAlpha 214 | }) 215 | /// The animation part in charge of hiding the menu (doesn't include the background animation). 216 | public private(set) lazy var hideMenuAnimation = Transition.Animation(change: { 217 | self.updateContentViewPosition(on: .hide) 218 | }) 219 | /// The animation part in charge of hiding the background (doesn't include the menu animation). 220 | /// 221 | /// Must set `self.isHidden = true` when the animation completes (this implies the background 222 | /// hiding animation is expected to have a duration equal or greater to the menu hiding animation). 223 | public private(set) lazy var hideBackgroundAnimation = Transition.Animation(change: { 224 | self.backgroundView?.alpha = 0 225 | }) 226 | 227 | // MARK: - Selection 228 | 229 | /// Selects the cell briefly and sends the cell menu action. 230 | /// 231 | /// If DropDownMenuCell.showsCheckmark is true, then the cell is marked with 232 | /// a checkmark and all other cells are unchecked. 233 | open func selectMenuCell(_ cell: DropDownMenuCell) { 234 | guard let index = menuCells.firstIndex(of: cell) else { 235 | fatalError("The menu cell to select must belong to the menu") 236 | } 237 | let indexPath = IndexPath(row: index, section: 0) 238 | 239 | menuView.selectRow(at: indexPath, animated: false, scrollPosition: .none) 240 | tableView(menuView, didSelectRowAt: indexPath) 241 | } 242 | 243 | // MARK: - Actions 244 | 245 | @IBAction open func tap(_ sender: AnyObject) { 246 | delegate?.didTapInDropDownMenuBackground(self) 247 | } 248 | 249 | // If we declare a protocol method private, it is not called anymore. 250 | open func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool { 251 | precondition(gestureRecognizer.view == self) 252 | 253 | guard let touchedView = touch.view else { 254 | return true 255 | } 256 | return !touchedView.isDescendant(of: menuView) 257 | } 258 | 259 | @IBAction open func show() { 260 | if !isHidden { 261 | return 262 | } 263 | transition.show.map { $0.before }.joined()() 264 | isHidden = false 265 | UIView.animate(withDuration: transition.duration, 266 | delay: transition.delay, 267 | options: transition.options, 268 | animations: transition.show.map { $0.change }.joined(), 269 | completion: { _ in self.transition.show.map { $0.after }.joined()() }) 270 | } 271 | 272 | @IBAction open func hide() { 273 | if isHidden { 274 | return 275 | } 276 | transition.hide.map { $0.before }.joined()() 277 | UIView.animate(withDuration: transition.duration, 278 | delay: transition.delay, 279 | options: transition.options, 280 | animations: transition.hide.map { $0.change }.joined(), 281 | completion: { _ in 282 | self.transition.hide.map { $0.after }.joined()() 283 | self.isHidden = true 284 | }) 285 | } 286 | 287 | // MARK: - Table View 288 | 289 | open func numberOfSections(in tableView: UITableView) -> Int { 290 | return 1 291 | } 292 | 293 | open func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 294 | return menuCells.count 295 | } 296 | 297 | open func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { 298 | return menuCells[indexPath.row].rowHeight 299 | } 300 | 301 | open func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 302 | return menuCells[indexPath.row] 303 | } 304 | 305 | open func tableView(_ tableView: UITableView, shouldHighlightRowAt indexPath: IndexPath) -> Bool { 306 | return menuCells[indexPath.row].menuAction != nil 307 | } 308 | 309 | open func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 310 | let cell = menuCells[indexPath.row] 311 | 312 | for cell in menuCells { 313 | cell.accessoryType = .none 314 | } 315 | cell.accessoryType = cell.showsCheckmark ? .checkmark : .none 316 | 317 | tableView.deselectRow(at: indexPath, animated: true) 318 | 319 | guard let menuAction = cell.menuAction else { 320 | return 321 | } 322 | 323 | #if APP_EXTENSION 324 | guard let menuTarget = cell.menuTarget, menuTarget.responds(to: menuAction) else { 325 | return 326 | } 327 | _ = menuTarget.perform(menuAction, with: cell) 328 | #else 329 | UIApplication.shared.sendAction(menuAction, to: cell.menuTarget, from: cell, for: nil) 330 | #endif 331 | } 332 | } 333 | 334 | private extension Array where Element == DropDownMenu.Transition.Animation.Block { 335 | func joined() -> DropDownMenu.Transition.Animation.Block { 336 | return { self.forEach { $0() } } 337 | } 338 | } 339 | -------------------------------------------------------------------------------- /DropDownMenuKit.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 6009E2461D5119BF006424BD /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6009E2441D5119BF006424BD /* AppDelegate.swift */; }; 11 | 6009E2471D5119BF006424BD /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6009E2451D5119BF006424BD /* ViewController.swift */; }; 12 | 6009E24F1D5119DF006424BD /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 6009E2491D5119DF006424BD /* LaunchScreen.storyboard */; }; 13 | 6009E2501D5119DF006424BD /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 6009E24B1D5119DF006424BD /* Main.storyboard */; }; 14 | 600E8C8A1D5B759300AFD10E /* DropDownMenuKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 600E8C831D5B759300AFD10E /* DropDownMenuKit.framework */; }; 15 | 600E8C8B1D5B759300AFD10E /* DropDownMenuKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 600E8C831D5B759300AFD10E /* DropDownMenuKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 16 | 600E8C901D5B76AA00AFD10E /* DropDownMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6009E2401D511955006424BD /* DropDownMenu.swift */; }; 17 | 600E8C911D5B76AC00AFD10E /* DropDownTitleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6009E2411D511955006424BD /* DropDownTitleView.swift */; }; 18 | 600E8C961D5B78B800AFD10E /* DropDownMenu.h in Headers */ = {isa = PBXBuildFile; fileRef = 600E8C941D5B78B800AFD10E /* DropDownMenu.h */; settings = {ATTRIBUTES = (Public, ); }; }; 19 | 600E8CA71D5C938900AFD10E /* DropDownMenuCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 600E8CA51D5C935E00AFD10E /* DropDownMenuCell.swift */; }; 20 | 60495BFF1D6C60FE0016987F /* DropDownMenuKit.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 600E8C921D5B76DD00AFD10E /* DropDownMenuKit.xcassets */; }; 21 | 60495C001D6C61A70016987F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6009E2481D5119DF006424BD /* Assets.xcassets */; }; 22 | /* End PBXBuildFile section */ 23 | 24 | /* Begin PBXContainerItemProxy section */ 25 | 600E8C881D5B759300AFD10E /* PBXContainerItemProxy */ = { 26 | isa = PBXContainerItemProxy; 27 | containerPortal = 6009E2221D5117FC006424BD /* Project object */; 28 | proxyType = 1; 29 | remoteGlobalIDString = 600E8C821D5B759300AFD10E; 30 | remoteInfo = DropDownMenu; 31 | }; 32 | /* End PBXContainerItemProxy section */ 33 | 34 | /* Begin PBXCopyFilesBuildPhase section */ 35 | 600E8C8F1D5B759300AFD10E /* Embed Frameworks */ = { 36 | isa = PBXCopyFilesBuildPhase; 37 | buildActionMask = 2147483647; 38 | dstPath = ""; 39 | dstSubfolderSpec = 10; 40 | files = ( 41 | 600E8C8B1D5B759300AFD10E /* DropDownMenuKit.framework in Embed Frameworks */, 42 | ); 43 | name = "Embed Frameworks"; 44 | runOnlyForDeploymentPostprocessing = 0; 45 | }; 46 | /* End PBXCopyFilesBuildPhase section */ 47 | 48 | /* Begin PBXFileReference section */ 49 | 6009E22A1D5117FC006424BD /* DropDownMenuExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = DropDownMenuExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; 50 | 6009E2401D511955006424BD /* DropDownMenu.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DropDownMenu.swift; sourceTree = SOURCE_ROOT; }; 51 | 6009E2411D511955006424BD /* DropDownTitleView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DropDownTitleView.swift; sourceTree = SOURCE_ROOT; }; 52 | 6009E2441D5119BF006424BD /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = AppDelegate.swift; path = Example/AppDelegate.swift; sourceTree = SOURCE_ROOT; }; 53 | 6009E2451D5119BF006424BD /* ViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ViewController.swift; path = Example/ViewController.swift; sourceTree = SOURCE_ROOT; }; 54 | 6009E2481D5119DF006424BD /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Example/Assets.xcassets; sourceTree = SOURCE_ROOT; }; 55 | 6009E24A1D5119DF006424BD /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Example/Base.lproj/LaunchScreen.storyboard; sourceTree = SOURCE_ROOT; }; 56 | 6009E24C1D5119DF006424BD /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Example/Base.lproj/Main.storyboard; sourceTree = SOURCE_ROOT; }; 57 | 6009E2521D5119F6006424BD /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = Example/Info.plist; sourceTree = SOURCE_ROOT; }; 58 | 6009E27A1D522802006424BD /* LICENSE */ = {isa = PBXFileReference; lastKnownFileType = text; path = LICENSE; sourceTree = ""; }; 59 | 6009E27B1D522802006424BD /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; 60 | 600E8C831D5B759300AFD10E /* DropDownMenuKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = DropDownMenuKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 61 | 600E8C921D5B76DD00AFD10E /* DropDownMenuKit.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = DropDownMenuKit.xcassets; sourceTree = ""; }; 62 | 600E8C941D5B78B800AFD10E /* DropDownMenu.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DropDownMenu.h; sourceTree = ""; }; 63 | 600E8C951D5B78B800AFD10E /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 64 | 600E8CA21D5C7D1800AFD10E /* TODO.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = TODO.md; sourceTree = ""; }; 65 | 600E8CA51D5C935E00AFD10E /* DropDownMenuCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DropDownMenuCell.swift; sourceTree = ""; }; 66 | 606F9FD61DB24FCB0071C87A /* NEWS.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = NEWS.md; sourceTree = ""; }; 67 | /* End PBXFileReference section */ 68 | 69 | /* Begin PBXFrameworksBuildPhase section */ 70 | 6009E2271D5117FC006424BD /* Frameworks */ = { 71 | isa = PBXFrameworksBuildPhase; 72 | buildActionMask = 2147483647; 73 | files = ( 74 | 600E8C8A1D5B759300AFD10E /* DropDownMenuKit.framework in Frameworks */, 75 | ); 76 | runOnlyForDeploymentPostprocessing = 0; 77 | }; 78 | 600E8C7F1D5B759300AFD10E /* Frameworks */ = { 79 | isa = PBXFrameworksBuildPhase; 80 | buildActionMask = 2147483647; 81 | files = ( 82 | ); 83 | runOnlyForDeploymentPostprocessing = 0; 84 | }; 85 | /* End PBXFrameworksBuildPhase section */ 86 | 87 | /* Begin PBXGroup section */ 88 | 6009E2211D5117FC006424BD = { 89 | isa = PBXGroup; 90 | children = ( 91 | 6009E27B1D522802006424BD /* README.md */, 92 | 606F9FD61DB24FCB0071C87A /* NEWS.md */, 93 | 600E8CA21D5C7D1800AFD10E /* TODO.md */, 94 | 6009E27A1D522802006424BD /* LICENSE */, 95 | 6009E2401D511955006424BD /* DropDownMenu.swift */, 96 | 600E8CA51D5C935E00AFD10E /* DropDownMenuCell.swift */, 97 | 6009E2411D511955006424BD /* DropDownTitleView.swift */, 98 | 600E8C941D5B78B800AFD10E /* DropDownMenu.h */, 99 | 600E8C951D5B78B800AFD10E /* Info.plist */, 100 | 600E8C921D5B76DD00AFD10E /* DropDownMenuKit.xcassets */, 101 | 6009E23F1D511939006424BD /* Example */, 102 | 6009E22B1D5117FC006424BD /* Products */, 103 | ); 104 | sourceTree = ""; 105 | usesTabs = 1; 106 | }; 107 | 6009E22B1D5117FC006424BD /* Products */ = { 108 | isa = PBXGroup; 109 | children = ( 110 | 6009E22A1D5117FC006424BD /* DropDownMenuExample.app */, 111 | 600E8C831D5B759300AFD10E /* DropDownMenuKit.framework */, 112 | ); 113 | name = Products; 114 | sourceTree = ""; 115 | }; 116 | 6009E23F1D511939006424BD /* Example */ = { 117 | isa = PBXGroup; 118 | children = ( 119 | 6009E2441D5119BF006424BD /* AppDelegate.swift */, 120 | 6009E2451D5119BF006424BD /* ViewController.swift */, 121 | 6009E2481D5119DF006424BD /* Assets.xcassets */, 122 | 6009E2491D5119DF006424BD /* LaunchScreen.storyboard */, 123 | 6009E24B1D5119DF006424BD /* Main.storyboard */, 124 | 6009E2521D5119F6006424BD /* Info.plist */, 125 | ); 126 | path = Example; 127 | sourceTree = ""; 128 | }; 129 | /* End PBXGroup section */ 130 | 131 | /* Begin PBXHeadersBuildPhase section */ 132 | 600E8C801D5B759300AFD10E /* Headers */ = { 133 | isa = PBXHeadersBuildPhase; 134 | buildActionMask = 2147483647; 135 | files = ( 136 | 600E8C961D5B78B800AFD10E /* DropDownMenu.h in Headers */, 137 | ); 138 | runOnlyForDeploymentPostprocessing = 0; 139 | }; 140 | /* End PBXHeadersBuildPhase section */ 141 | 142 | /* Begin PBXNativeTarget section */ 143 | 6009E2291D5117FC006424BD /* DropDownMenuExample */ = { 144 | isa = PBXNativeTarget; 145 | buildConfigurationList = 6009E23C1D5117FC006424BD /* Build configuration list for PBXNativeTarget "DropDownMenuExample" */; 146 | buildPhases = ( 147 | 6009E2261D5117FC006424BD /* Sources */, 148 | 6009E2271D5117FC006424BD /* Frameworks */, 149 | 6009E2281D5117FC006424BD /* Resources */, 150 | 600E8C8F1D5B759300AFD10E /* Embed Frameworks */, 151 | ); 152 | buildRules = ( 153 | ); 154 | dependencies = ( 155 | 600E8C891D5B759300AFD10E /* PBXTargetDependency */, 156 | ); 157 | name = DropDownMenuExample; 158 | productName = DropDownMenu; 159 | productReference = 6009E22A1D5117FC006424BD /* DropDownMenuExample.app */; 160 | productType = "com.apple.product-type.application"; 161 | }; 162 | 600E8C821D5B759300AFD10E /* DropDownMenuKit */ = { 163 | isa = PBXNativeTarget; 164 | buildConfigurationList = 600E8C8C1D5B759300AFD10E /* Build configuration list for PBXNativeTarget "DropDownMenuKit" */; 165 | buildPhases = ( 166 | 600E8C7E1D5B759300AFD10E /* Sources */, 167 | 600E8C7F1D5B759300AFD10E /* Frameworks */, 168 | 600E8C801D5B759300AFD10E /* Headers */, 169 | 600E8C811D5B759300AFD10E /* Resources */, 170 | ); 171 | buildRules = ( 172 | ); 173 | dependencies = ( 174 | ); 175 | name = DropDownMenuKit; 176 | productName = DropDownMenu; 177 | productReference = 600E8C831D5B759300AFD10E /* DropDownMenuKit.framework */; 178 | productType = "com.apple.product-type.framework"; 179 | }; 180 | /* End PBXNativeTarget section */ 181 | 182 | /* Begin PBXProject section */ 183 | 6009E2221D5117FC006424BD /* Project object */ = { 184 | isa = PBXProject; 185 | attributes = { 186 | LastSwiftUpdateCheck = 0730; 187 | LastUpgradeCheck = 1420; 188 | ORGANIZATIONNAME = "Quentin Mathé"; 189 | TargetAttributes = { 190 | 6009E2291D5117FC006424BD = { 191 | CreatedOnToolsVersion = 7.3; 192 | LastSwiftMigration = 1020; 193 | ProvisioningStyle = Manual; 194 | }; 195 | 600E8C821D5B759300AFD10E = { 196 | CreatedOnToolsVersion = 7.3; 197 | LastSwiftMigration = 1020; 198 | }; 199 | }; 200 | }; 201 | buildConfigurationList = 6009E2251D5117FC006424BD /* Build configuration list for PBXProject "DropDownMenuKit" */; 202 | compatibilityVersion = "Xcode 3.2"; 203 | developmentRegion = en; 204 | hasScannedForEncodings = 0; 205 | knownRegions = ( 206 | en, 207 | Base, 208 | ); 209 | mainGroup = 6009E2211D5117FC006424BD; 210 | productRefGroup = 6009E22B1D5117FC006424BD /* Products */; 211 | projectDirPath = ""; 212 | projectRoot = ""; 213 | targets = ( 214 | 6009E2291D5117FC006424BD /* DropDownMenuExample */, 215 | 600E8C821D5B759300AFD10E /* DropDownMenuKit */, 216 | ); 217 | }; 218 | /* End PBXProject section */ 219 | 220 | /* Begin PBXResourcesBuildPhase section */ 221 | 6009E2281D5117FC006424BD /* Resources */ = { 222 | isa = PBXResourcesBuildPhase; 223 | buildActionMask = 2147483647; 224 | files = ( 225 | 60495C001D6C61A70016987F /* Assets.xcassets in Resources */, 226 | 6009E2501D5119DF006424BD /* Main.storyboard in Resources */, 227 | 6009E24F1D5119DF006424BD /* LaunchScreen.storyboard in Resources */, 228 | ); 229 | runOnlyForDeploymentPostprocessing = 0; 230 | }; 231 | 600E8C811D5B759300AFD10E /* Resources */ = { 232 | isa = PBXResourcesBuildPhase; 233 | buildActionMask = 2147483647; 234 | files = ( 235 | 60495BFF1D6C60FE0016987F /* DropDownMenuKit.xcassets in Resources */, 236 | ); 237 | runOnlyForDeploymentPostprocessing = 0; 238 | }; 239 | /* End PBXResourcesBuildPhase section */ 240 | 241 | /* Begin PBXSourcesBuildPhase section */ 242 | 6009E2261D5117FC006424BD /* Sources */ = { 243 | isa = PBXSourcesBuildPhase; 244 | buildActionMask = 2147483647; 245 | files = ( 246 | 6009E2461D5119BF006424BD /* AppDelegate.swift in Sources */, 247 | 6009E2471D5119BF006424BD /* ViewController.swift in Sources */, 248 | ); 249 | runOnlyForDeploymentPostprocessing = 0; 250 | }; 251 | 600E8C7E1D5B759300AFD10E /* Sources */ = { 252 | isa = PBXSourcesBuildPhase; 253 | buildActionMask = 2147483647; 254 | files = ( 255 | 600E8CA71D5C938900AFD10E /* DropDownMenuCell.swift in Sources */, 256 | 600E8C911D5B76AC00AFD10E /* DropDownTitleView.swift in Sources */, 257 | 600E8C901D5B76AA00AFD10E /* DropDownMenu.swift in Sources */, 258 | ); 259 | runOnlyForDeploymentPostprocessing = 0; 260 | }; 261 | /* End PBXSourcesBuildPhase section */ 262 | 263 | /* Begin PBXTargetDependency section */ 264 | 600E8C891D5B759300AFD10E /* PBXTargetDependency */ = { 265 | isa = PBXTargetDependency; 266 | target = 600E8C821D5B759300AFD10E /* DropDownMenuKit */; 267 | targetProxy = 600E8C881D5B759300AFD10E /* PBXContainerItemProxy */; 268 | }; 269 | /* End PBXTargetDependency section */ 270 | 271 | /* Begin PBXVariantGroup section */ 272 | 6009E2491D5119DF006424BD /* LaunchScreen.storyboard */ = { 273 | isa = PBXVariantGroup; 274 | children = ( 275 | 6009E24A1D5119DF006424BD /* Base */, 276 | ); 277 | name = LaunchScreen.storyboard; 278 | sourceTree = ""; 279 | }; 280 | 6009E24B1D5119DF006424BD /* Main.storyboard */ = { 281 | isa = PBXVariantGroup; 282 | children = ( 283 | 6009E24C1D5119DF006424BD /* Base */, 284 | ); 285 | name = Main.storyboard; 286 | sourceTree = ""; 287 | }; 288 | /* End PBXVariantGroup section */ 289 | 290 | /* Begin XCBuildConfiguration section */ 291 | 6009E23A1D5117FC006424BD /* Debug */ = { 292 | isa = XCBuildConfiguration; 293 | buildSettings = { 294 | ALWAYS_SEARCH_USER_PATHS = NO; 295 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 296 | CLANG_ANALYZER_NONNULL = YES; 297 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 298 | CLANG_CXX_LIBRARY = "libc++"; 299 | CLANG_ENABLE_MODULES = YES; 300 | CLANG_ENABLE_OBJC_ARC = YES; 301 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 302 | CLANG_WARN_BOOL_CONVERSION = YES; 303 | CLANG_WARN_COMMA = YES; 304 | CLANG_WARN_CONSTANT_CONVERSION = YES; 305 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 306 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 307 | CLANG_WARN_EMPTY_BODY = YES; 308 | CLANG_WARN_ENUM_CONVERSION = YES; 309 | CLANG_WARN_INFINITE_RECURSION = YES; 310 | CLANG_WARN_INT_CONVERSION = YES; 311 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 312 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 313 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 314 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 315 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 316 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 317 | CLANG_WARN_STRICT_PROTOTYPES = YES; 318 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 319 | CLANG_WARN_UNREACHABLE_CODE = YES; 320 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 321 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 322 | COPY_PHASE_STRIP = NO; 323 | DEBUG_INFORMATION_FORMAT = dwarf; 324 | ENABLE_STRICT_OBJC_MSGSEND = YES; 325 | ENABLE_TESTABILITY = YES; 326 | GCC_C_LANGUAGE_STANDARD = gnu99; 327 | GCC_DYNAMIC_NO_PIC = NO; 328 | GCC_NO_COMMON_BLOCKS = YES; 329 | GCC_OPTIMIZATION_LEVEL = 0; 330 | GCC_PREPROCESSOR_DEFINITIONS = ( 331 | "DEBUG=1", 332 | "$(inherited)", 333 | ); 334 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 335 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 336 | GCC_WARN_UNDECLARED_SELECTOR = YES; 337 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 338 | GCC_WARN_UNUSED_FUNCTION = YES; 339 | GCC_WARN_UNUSED_VARIABLE = YES; 340 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 341 | MTL_ENABLE_DEBUG_INFO = YES; 342 | ONLY_ACTIVE_ARCH = YES; 343 | SDKROOT = iphoneos; 344 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 345 | SWIFT_VERSION = 5.0; 346 | }; 347 | name = Debug; 348 | }; 349 | 6009E23B1D5117FC006424BD /* Release */ = { 350 | isa = XCBuildConfiguration; 351 | buildSettings = { 352 | ALWAYS_SEARCH_USER_PATHS = NO; 353 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 354 | CLANG_ANALYZER_NONNULL = YES; 355 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 356 | CLANG_CXX_LIBRARY = "libc++"; 357 | CLANG_ENABLE_MODULES = YES; 358 | CLANG_ENABLE_OBJC_ARC = YES; 359 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 360 | CLANG_WARN_BOOL_CONVERSION = YES; 361 | CLANG_WARN_COMMA = YES; 362 | CLANG_WARN_CONSTANT_CONVERSION = YES; 363 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 364 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 365 | CLANG_WARN_EMPTY_BODY = YES; 366 | CLANG_WARN_ENUM_CONVERSION = YES; 367 | CLANG_WARN_INFINITE_RECURSION = YES; 368 | CLANG_WARN_INT_CONVERSION = YES; 369 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 370 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 371 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 372 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 373 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 374 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 375 | CLANG_WARN_STRICT_PROTOTYPES = YES; 376 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 377 | CLANG_WARN_UNREACHABLE_CODE = YES; 378 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 379 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 380 | COPY_PHASE_STRIP = NO; 381 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 382 | ENABLE_NS_ASSERTIONS = NO; 383 | ENABLE_STRICT_OBJC_MSGSEND = YES; 384 | GCC_C_LANGUAGE_STANDARD = gnu99; 385 | GCC_NO_COMMON_BLOCKS = YES; 386 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 387 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 388 | GCC_WARN_UNDECLARED_SELECTOR = YES; 389 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 390 | GCC_WARN_UNUSED_FUNCTION = YES; 391 | GCC_WARN_UNUSED_VARIABLE = YES; 392 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 393 | MTL_ENABLE_DEBUG_INFO = NO; 394 | SDKROOT = iphoneos; 395 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 396 | SWIFT_VERSION = 5.0; 397 | VALIDATE_PRODUCT = YES; 398 | }; 399 | name = Release; 400 | }; 401 | 6009E23D1D5117FC006424BD /* Debug */ = { 402 | isa = XCBuildConfiguration; 403 | buildSettings = { 404 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 405 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 406 | CODE_SIGN_STYLE = Manual; 407 | DEVELOPMENT_TEAM = ""; 408 | INFOPLIST_FILE = "$(SRCROOT)/Example/Info.plist"; 409 | INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; 410 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 411 | PRODUCT_BUNDLE_IDENTIFIER = com.quentinmathe.DropDownMenuExample; 412 | PRODUCT_NAME = "$(TARGET_NAME)"; 413 | PROVISIONING_PROFILE_SPECIFIER = ""; 414 | }; 415 | name = Debug; 416 | }; 417 | 6009E23E1D5117FC006424BD /* Release */ = { 418 | isa = XCBuildConfiguration; 419 | buildSettings = { 420 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 421 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 422 | CODE_SIGN_STYLE = Manual; 423 | DEVELOPMENT_TEAM = ""; 424 | INFOPLIST_FILE = "$(SRCROOT)/Example/Info.plist"; 425 | INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; 426 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 427 | PRODUCT_BUNDLE_IDENTIFIER = com.quentinmathe.DropDownMenuExample; 428 | PRODUCT_NAME = "$(TARGET_NAME)"; 429 | PROVISIONING_PROFILE_SPECIFIER = ""; 430 | }; 431 | name = Release; 432 | }; 433 | 600E8C8D1D5B759300AFD10E /* Debug */ = { 434 | isa = XCBuildConfiguration; 435 | buildSettings = { 436 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; 437 | CURRENT_PROJECT_VERSION = 1; 438 | DEFINES_MODULE = YES; 439 | DYLIB_COMPATIBILITY_VERSION = 1; 440 | DYLIB_CURRENT_VERSION = 1; 441 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 442 | INFOPLIST_FILE = Info.plist; 443 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 444 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 445 | PRODUCT_BUNDLE_IDENTIFIER = com.quentinmathe.DropDownMenuKit; 446 | PRODUCT_NAME = "$(TARGET_NAME)"; 447 | SKIP_INSTALL = YES; 448 | TARGETED_DEVICE_FAMILY = "1,2"; 449 | VERSIONING_SYSTEM = "apple-generic"; 450 | VERSION_INFO_PREFIX = ""; 451 | }; 452 | name = Debug; 453 | }; 454 | 600E8C8E1D5B759300AFD10E /* Release */ = { 455 | isa = XCBuildConfiguration; 456 | buildSettings = { 457 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; 458 | CURRENT_PROJECT_VERSION = 1; 459 | DEFINES_MODULE = YES; 460 | DYLIB_COMPATIBILITY_VERSION = 1; 461 | DYLIB_CURRENT_VERSION = 1; 462 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 463 | INFOPLIST_FILE = Info.plist; 464 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 465 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 466 | PRODUCT_BUNDLE_IDENTIFIER = com.quentinmathe.DropDownMenuKit; 467 | PRODUCT_NAME = "$(TARGET_NAME)"; 468 | SKIP_INSTALL = YES; 469 | TARGETED_DEVICE_FAMILY = "1,2"; 470 | VERSIONING_SYSTEM = "apple-generic"; 471 | VERSION_INFO_PREFIX = ""; 472 | }; 473 | name = Release; 474 | }; 475 | /* End XCBuildConfiguration section */ 476 | 477 | /* Begin XCConfigurationList section */ 478 | 6009E2251D5117FC006424BD /* Build configuration list for PBXProject "DropDownMenuKit" */ = { 479 | isa = XCConfigurationList; 480 | buildConfigurations = ( 481 | 6009E23A1D5117FC006424BD /* Debug */, 482 | 6009E23B1D5117FC006424BD /* Release */, 483 | ); 484 | defaultConfigurationIsVisible = 0; 485 | defaultConfigurationName = Release; 486 | }; 487 | 6009E23C1D5117FC006424BD /* Build configuration list for PBXNativeTarget "DropDownMenuExample" */ = { 488 | isa = XCConfigurationList; 489 | buildConfigurations = ( 490 | 6009E23D1D5117FC006424BD /* Debug */, 491 | 6009E23E1D5117FC006424BD /* Release */, 492 | ); 493 | defaultConfigurationIsVisible = 0; 494 | defaultConfigurationName = Release; 495 | }; 496 | 600E8C8C1D5B759300AFD10E /* Build configuration list for PBXNativeTarget "DropDownMenuKit" */ = { 497 | isa = XCConfigurationList; 498 | buildConfigurations = ( 499 | 600E8C8D1D5B759300AFD10E /* Debug */, 500 | 600E8C8E1D5B759300AFD10E /* Release */, 501 | ); 502 | defaultConfigurationIsVisible = 0; 503 | defaultConfigurationName = Release; 504 | }; 505 | /* End XCConfigurationList section */ 506 | }; 507 | rootObject = 6009E2221D5117FC006424BD /* Project object */; 508 | } 509 | --------------------------------------------------------------------------------