├── SwiftyMenu ├── Assets │ ├── .gitkeep │ └── Images.xcassets │ │ ├── Contents.json │ │ ├── downArrow.imageset │ │ ├── Disclosure_Arrow_1.png │ │ ├── Disclosure_Arrow_1@2x.png │ │ ├── Disclosure_Arrow_1@3x.png │ │ └── Contents.json │ │ └── AppIcon.appiconset │ │ └── Contents.json └── Classes │ ├── .gitkeep │ ├── SwiftMenuDisplayable.swift │ ├── UIViewExtensions.swift │ ├── SwiftyMenuState.swift │ ├── MenuAttributes │ ├── SwiftyMenuAttributes+Scroll.swift │ ├── SwiftyMenuAttributes+Accessory.swift │ ├── SwiftyMenuAttributes+PlaceHolderStyle.swift │ ├── SwiftyMenuAttributes+AnimationTiming.swift │ ├── SwiftyMenuAttributes+ErrorInformation.swift │ ├── SwiftyMenuAttributes+SeparatorStyle.swift │ ├── SwiftyMenuAttributes+MarginHorizontal.swift │ ├── SwiftyMenuAttributes+RowStyle.swift │ ├── SwiftyMenuAttributes+TextStyle.swift │ ├── SwiftyMenuAttributes+Animation.swift │ ├── SwiftyMenuAttributes+HeaderStyle.swift │ ├── SwiftyMenuAttributes+SelectionBehavior.swift │ ├── SwiftyMenuAttributes+ArrowStyle.swift │ ├── SwiftyMenuAttributes+FrameStyle.swift │ └── SwiftyMenuAttributes.swift │ ├── SwiftyMenuAnimationStyle.swift │ ├── SwiftyMenuCell.swift │ ├── SwiftyMenuDelegate.swift │ └── SwiftyMenu.swift ├── _Pods.xcodeproj ├── Screenshots ├── 1.png ├── 2.gif ├── 3.png ├── 4.gif ├── 5.gif ├── 6.gif └── Logo.png ├── Example ├── SwiftyMenu │ ├── Images.xcassets │ │ ├── Contents.json │ │ ├── arrow.down.custom.imageset │ │ │ ├── custom_arrow.png │ │ │ └── Contents.json │ │ └── AppIcon.appiconset │ │ │ └── Contents.json │ ├── String+Extensions.swift │ ├── MealSize.swift │ ├── Info.plist │ ├── AppDelegate.swift │ ├── Base.lproj │ │ ├── LaunchScreen.xib │ │ └── Main.storyboard │ └── ViewController.swift ├── Podfile ├── SwiftyMenu.xcodeproj │ ├── project.xcworkspace │ │ └── contents.xcworkspacedata │ ├── xcshareddata │ │ └── xcschemes │ │ │ └── SwiftyMenu-Example.xcscheme │ └── project.pbxproj ├── SwiftyMenu.xcworkspace │ ├── xcshareddata │ │ ├── WorkspaceSettings.xcsettings │ │ └── IDEWorkspaceChecks.plist │ └── contents.xcworkspacedata ├── Podfile.lock └── Tests │ ├── Info.plist │ ├── SwiftyMenuDelegateSpy.swift │ └── SwiftyMenuTests.swift ├── Package.swift ├── .gitignore ├── LICENSE ├── SwiftyMenu.podspec ├── CHANGELOG.md └── README.md /SwiftyMenu/Assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /SwiftyMenu/Classes/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /_Pods.xcodeproj: -------------------------------------------------------------------------------- 1 | Example/Pods/Pods.xcodeproj -------------------------------------------------------------------------------- /Screenshots/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KarimEbrahemAbdelaziz/SwiftyMenu/HEAD/Screenshots/1.png -------------------------------------------------------------------------------- /Screenshots/2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KarimEbrahemAbdelaziz/SwiftyMenu/HEAD/Screenshots/2.gif -------------------------------------------------------------------------------- /Screenshots/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KarimEbrahemAbdelaziz/SwiftyMenu/HEAD/Screenshots/3.png -------------------------------------------------------------------------------- /Screenshots/4.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KarimEbrahemAbdelaziz/SwiftyMenu/HEAD/Screenshots/4.gif -------------------------------------------------------------------------------- /Screenshots/5.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KarimEbrahemAbdelaziz/SwiftyMenu/HEAD/Screenshots/5.gif -------------------------------------------------------------------------------- /Screenshots/6.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KarimEbrahemAbdelaziz/SwiftyMenu/HEAD/Screenshots/6.gif -------------------------------------------------------------------------------- /Screenshots/Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KarimEbrahemAbdelaziz/SwiftyMenu/HEAD/Screenshots/Logo.png -------------------------------------------------------------------------------- /SwiftyMenu/Assets/Images.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Example/SwiftyMenu/Images.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /SwiftyMenu/Assets/Images.xcassets/downArrow.imageset/Disclosure_Arrow_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KarimEbrahemAbdelaziz/SwiftyMenu/HEAD/SwiftyMenu/Assets/Images.xcassets/downArrow.imageset/Disclosure_Arrow_1.png -------------------------------------------------------------------------------- /Example/SwiftyMenu/Images.xcassets/arrow.down.custom.imageset/custom_arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KarimEbrahemAbdelaziz/SwiftyMenu/HEAD/Example/SwiftyMenu/Images.xcassets/arrow.down.custom.imageset/custom_arrow.png -------------------------------------------------------------------------------- /SwiftyMenu/Assets/Images.xcassets/downArrow.imageset/Disclosure_Arrow_1@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KarimEbrahemAbdelaziz/SwiftyMenu/HEAD/SwiftyMenu/Assets/Images.xcassets/downArrow.imageset/Disclosure_Arrow_1@2x.png -------------------------------------------------------------------------------- /SwiftyMenu/Assets/Images.xcassets/downArrow.imageset/Disclosure_Arrow_1@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KarimEbrahemAbdelaziz/SwiftyMenu/HEAD/SwiftyMenu/Assets/Images.xcassets/downArrow.imageset/Disclosure_Arrow_1@3x.png -------------------------------------------------------------------------------- /Example/Podfile: -------------------------------------------------------------------------------- 1 | platform :ios, '11' 2 | use_frameworks! 3 | 4 | target 'SwiftyMenu_Example' do 5 | pod 'SwiftyMenu', :path => '../' 6 | 7 | target 'SwiftyMenu_Tests' do 8 | inherit! :search_paths 9 | 10 | 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /Example/SwiftyMenu.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Example/SwiftyMenu.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Example/SwiftyMenu.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Example/SwiftyMenu.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Example/SwiftyMenu/Images.xcassets/arrow.down.custom.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "custom_arrow.png", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Example/SwiftyMenu/String+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String+Extensions.swift 3 | // SwiftyMenu_Example 4 | // 5 | // Created by Karim Ebrahem on 03/07/2021. 6 | // Copyright © 2021 CocoaPods. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SwiftyMenu 11 | 12 | extension String: SwiftyMenuDisplayable { 13 | public var displayableValue: String { 14 | return self 15 | } 16 | 17 | public var retrievableValue: Any { 18 | return self 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Example/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - SnapKit (5.0.1) 3 | - SwiftyMenu (1.1.0): 4 | - SnapKit (~> 5.0.1) 5 | 6 | DEPENDENCIES: 7 | - SwiftyMenu (from `../`) 8 | 9 | SPEC REPOS: 10 | trunk: 11 | - SnapKit 12 | 13 | EXTERNAL SOURCES: 14 | SwiftyMenu: 15 | :path: "../" 16 | 17 | SPEC CHECKSUMS: 18 | SnapKit: 97b92857e3df3a0c71833cce143274bf6ef8e5eb 19 | SwiftyMenu: b3ba08c50917eddf6a7525b6ca1a60789400c740 20 | 21 | PODFILE CHECKSUM: 67fed0c6742bfcf9daff3311a2f4e1bc69c46ba7 22 | 23 | COCOAPODS: 1.15.2 24 | -------------------------------------------------------------------------------- /SwiftyMenu/Assets/Images.xcassets/downArrow.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "Disclosure_Arrow_1.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "Disclosure_Arrow_1@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "Disclosure_Arrow_1@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /Example/SwiftyMenu/MealSize.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MealSize.swift 3 | // SwiftyMenu_Example 4 | // 5 | // Created by Karim Ebrahem on 03/07/2021. 6 | // Copyright © 2021 CocoaPods. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SwiftyMenu 11 | 12 | struct MealSize { 13 | let id: Int 14 | let name: String 15 | } 16 | 17 | extension MealSize: SwiftyMenuDisplayable { 18 | public var displayableValue: String { 19 | return self.name 20 | } 21 | 22 | public var retrievableValue: Any { 23 | return self.id 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.3 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "SwiftyMenu", 7 | platforms: [ 8 | .iOS(.v10), 9 | ], 10 | products: [ 11 | .library( 12 | name: "SwiftyMenu", 13 | targets: ["SwiftyMenu"] 14 | ), 15 | ], 16 | dependencies: [ 17 | .package( 18 | url: "https://github.com/SnapKit/SnapKit.git", 19 | from: "5.0.1" 20 | ) 21 | ], 22 | targets: [ 23 | .target( 24 | name: "SwiftyMenu", 25 | dependencies: ["SnapKit"], 26 | path: "SwiftyMenu", 27 | resources: [.process("Assets")] 28 | ), 29 | ], 30 | swiftLanguageVersions: [.v5] 31 | ) 32 | -------------------------------------------------------------------------------- /Example/Tests/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 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Swift ### 2 | # Xcode 3 | # 4 | build/ 5 | *.pbxuser 6 | !default.pbxuser 7 | *.mode1v3 8 | !default.mode1v3 9 | *.mode2v3 10 | !default.mode2v3 11 | *.perspectivev3 12 | !default.perspectivev3 13 | xcuserdata 14 | *.xccheckout 15 | *.moved-aside 16 | DerivedData 17 | *.hmap 18 | *.ipa 19 | *.xcuserstate 20 | 21 | # CocoaPods 22 | # 23 | # We recommend against adding the Pods directory to your .gitignore. However 24 | # you should judge for yourself, the pros and cons are mentioned at: 25 | # http://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control 26 | # 27 | # Pods/ 28 | 29 | # Carthage 30 | # 31 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 32 | # Carthage/Checkouts 33 | 34 | Carthage/Build 35 | 36 | # Mac OS X 37 | .DS_Store 38 | Example/Pods 39 | -------------------------------------------------------------------------------- /Example/SwiftyMenu/Images.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 | } 54 | -------------------------------------------------------------------------------- /SwiftyMenu/Assets/Images.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 | } 54 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2024 KarimEbrahem 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 | -------------------------------------------------------------------------------- /Example/Tests/SwiftyMenuDelegateSpy.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftyMenuDelegateSpy.swift 3 | // SwiftyMenu_Example 4 | // 5 | // Created by Karim Ebrahem on 3/25/20. 6 | // Copyright © 2020 CocoaPods. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | @testable import SwiftyMenu 11 | 12 | class SwiftyMenuDelegateSpy: SwiftyMenuDelegate { 13 | var willExpandCalled = false 14 | var didExpandCalled = false 15 | var willCollapseCalled = false 16 | var didCollapseCalled = false 17 | 18 | func swiftyMenu(_ swiftyMenu: SwiftyMenu, didSelectItem item: SwiftyMenuDisplayable, atIndex index: Int) { 19 | } 20 | 21 | func swiftyMenu(_ swiftyMenu: SwiftyMenu, didDeselectItem item: SwiftyMenuDisplayable, atIndex index: Int) { 22 | } 23 | 24 | func swiftyMenu(willExpand swiftyMenu: SwiftyMenu) { 25 | willExpandCalled = true 26 | } 27 | 28 | func swiftyMenu(didExpand swiftyMenu: SwiftyMenu) { 29 | didExpandCalled = true 30 | } 31 | 32 | func swiftyMenu(willCollapse swiftyMenu: SwiftyMenu) { 33 | willCollapseCalled = true 34 | } 35 | 36 | func swiftyMenu(didCollapse swiftyMenu: SwiftyMenu) { 37 | didCollapseCalled = true 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Example/SwiftyMenu/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UIRequiredDeviceCapabilities 30 | 31 | armv7 32 | 33 | UISupportedInterfaceOrientations 34 | 35 | UIInterfaceOrientationPortrait 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /SwiftyMenu.podspec: -------------------------------------------------------------------------------- 1 | # 2 | # Be sure to run `pod lib lint SwiftyMenu.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 https://guides.cocoapods.org/syntax/podspec.html 7 | # 8 | 9 | Pod::Spec.new do |s| 10 | s.name = 'SwiftyMenu' 11 | s.version = '1.1.0' 12 | s.summary = 'SwiftyMenu is Simple Drop Down Menu for iOS.' 13 | 14 | s.description = <<-DESC 15 | This drop down is to overcome the loss of usability and user experience due to the UIPickerView. 16 | DESC 17 | 18 | s.homepage = 'https://github.com/KarimEbrahemAbdelaziz/SwiftyMenu' 19 | s.screenshots = 'https://github.com/KarimEbrahemAbdelaziz/SwiftyMenu/blob/master/Screenshots/3.png?raw=true' 20 | s.license = { :type => 'MIT', :file => 'LICENSE' } 21 | s.author = { 'KarimEbrahemAbdelaziz' => 'karimabdelazizmansour@gmail.com' } 22 | s.source = { :git => 'https://github.com/KarimEbrahemAbdelaziz/SwiftyMenu.git', :tag => s.version.to_s } 23 | s.social_media_url = 'https://www.linkedin.com/in/karimebrahem' 24 | 25 | s.ios.deployment_target = '10.0' 26 | s.swift_version = '5.0' 27 | 28 | s.source_files = 'SwiftyMenu/Classes/**/*' 29 | s.resources = 'SwiftyMenu/Assets/*' 30 | 31 | s.dependency 'SnapKit', '~> 5.0.1' 32 | end 33 | -------------------------------------------------------------------------------- /SwiftyMenu/Classes/SwiftMenuDisplayable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftMenuDisplayable.swift 3 | // 4 | // Copyright (c) 2019-2024 Karim Ebrahem (https://www.linkedin.com/in/karimebrahem) 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | // 24 | 25 | import Foundation 26 | 27 | /// `SwiftyMenuDisplayable` is the markable interface to allow custom types to be used with SwiftyMenu. 28 | public protocol SwiftyMenuDisplayable { 29 | /// The value that will be displayed in the menu item 30 | var displayableValue: String { get } 31 | 32 | /// The value that will be returned when select menu item 33 | var retrievableValue: Any { get } 34 | } 35 | -------------------------------------------------------------------------------- /SwiftyMenu/Classes/UIViewExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIViewExtensions.swift 3 | // 4 | // Copyright (c) 2019-2024 Karim Ebrahem (https://www.linkedin.com/in/karimebrahem) 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | // 24 | 25 | import UIKit 26 | 27 | extension UIView { 28 | /// Returns the `ParentViewController` for any view. 29 | var parentViewController: UIViewController? { 30 | var responder: UIResponder? = self 31 | while !(responder is UIViewController) { 32 | responder = responder?.next 33 | if responder == nil { 34 | break 35 | } 36 | } 37 | return (responder as? UIViewController) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /SwiftyMenu/Classes/SwiftyMenuState.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftyMenuState.swift 3 | // 4 | // Copyright (c) 2019-2024 Karim Ebrahem (https://www.linkedin.com/in/karimebrahem) 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | // 24 | 25 | import Foundation 26 | 27 | /// Type describing the current state of `SwiftyMenu`. 28 | public enum SwiftyMenuState { 29 | /// `SwiftyMenu` is expanded. 30 | case shown 31 | 32 | /// `SwiftyMenu` is collapsed. 33 | case hidden 34 | 35 | /// Change the current state of SwiftyMenu 36 | mutating func toggle() { 37 | switch self { 38 | case .shown: 39 | self = .hidden 40 | case .hidden: 41 | self = .shown 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /SwiftyMenu/Classes/MenuAttributes/SwiftyMenuAttributes+Scroll.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftyMenuAttributes+Scroll.swift 3 | // 4 | // Copyright (c) 2019-2024 Karim Ebrahem (https://www.linkedin.com/in/karimebrahem) 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | // 24 | 25 | import Foundation 26 | 27 | public extension SwiftyMenuAttributes { 28 | 29 | /** Describes the event of scroll user interaction */ 30 | enum Scroll { 31 | 32 | /** The scroll ability is totally disabled */ 33 | case disabled 34 | 35 | /** The scroll abiliby is enabled */ 36 | case enabled 37 | 38 | var isEnabled: Bool { 39 | switch self { 40 | case .disabled: 41 | return false 42 | default: 43 | return true 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /SwiftyMenu/Classes/MenuAttributes/SwiftyMenuAttributes+Accessory.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftyMenuAttributes+Accessory.swift 3 | // 4 | // Copyright (c) 2019-2024 Karim Ebrahem (https://www.linkedin.com/in/karimebrahem) 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | // 24 | 25 | import Foundation 26 | 27 | public extension SwiftyMenuAttributes { 28 | 29 | /** Describes the event of scroll user interaction */ 30 | enum Accessory { 31 | 32 | /** The scroll ability is totally disabled */ 33 | case disabled 34 | 35 | /** The scroll abiliby is enabled */ 36 | case enabled 37 | 38 | var isEnabled: Bool { 39 | switch self { 40 | case .disabled: 41 | return false 42 | default: 43 | return true 44 | } 45 | } 46 | } 47 | } 48 | 49 | -------------------------------------------------------------------------------- /SwiftyMenu/Classes/MenuAttributes/SwiftyMenuAttributes+PlaceHolderStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftyMenuAttributes+MenuPlaceHolderStyle.swift 3 | // 4 | // Copyright (c) 2019-2024 Karim Ebrahem (https://www.linkedin.com/in/karimebrahem) 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | // 24 | 25 | import Foundation 26 | import UIKit 27 | 28 | public extension SwiftyMenuAttributes { 29 | 30 | /** Describes the style for row of the menu */ 31 | enum PlaceHolderStyle { 32 | 33 | /** Placeholder with text and color */ 34 | case value(text: String, textColor: UIColor) 35 | 36 | var placeHolderValues: (text: String, textColor: UIColor) { 37 | switch self { 38 | case let .value(text, textColor): 39 | return (text: text, textColor: textColor) 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /SwiftyMenu/Classes/MenuAttributes/SwiftyMenuAttributes+AnimationTiming.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftyMenuAttributes+AnimationTiming.swift 3 | // 4 | // Copyright (c) 2019-2024 Karim Ebrahem (https://www.linkedin.com/in/karimebrahem) 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | // 24 | 25 | import Foundation 26 | 27 | public extension SwiftyMenuAttributes { 28 | 29 | /** Describes how long the menu animates */ 30 | enum AnimationTiming { 31 | 32 | case `default` 33 | 34 | case value(duration: Double, delay: Double) 35 | 36 | var animationTimingValues: (duration: Double, delay: Double) { 37 | switch self { 38 | case let .value(duration, delay): 39 | return (duration: duration, delay: delay) 40 | case .default: 41 | return (duration: 0.5, delay: 0.0) 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /SwiftyMenu/Classes/MenuAttributes/SwiftyMenuAttributes+ErrorInformation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftyMenuAttributes+ErrorInformation.swift 3 | // 4 | // Copyright (c) 2019-2024 Karim Ebrahem (https://www.linkedin.com/in/karimebrahem) 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | // 24 | 25 | import Foundation 26 | import UIKit 27 | 28 | public extension SwiftyMenuAttributes{ 29 | enum ErrorInformation { 30 | case `default` 31 | case custom(placeholderTextColor: UIColor = .red, iconTintColor: UIColor? = .red) 32 | 33 | public var errorInfoValues: (placeholderTextColor: UIColor, iconTintColor: UIColor?) { 34 | switch self { 35 | case .default: 36 | return (placeholderTextColor: .red, iconTintColor: .red) 37 | case let .custom(placeholderTextColor, iconTintColor): 38 | return (placeholderTextColor: placeholderTextColor, iconTintColor: iconTintColor) 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /SwiftyMenu/Classes/SwiftyMenuAnimationStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftyMenuAnimationStyle.swift 3 | // 4 | // Copyright (c) 2019-2024 Karim Ebrahem (https://www.linkedin.com/in/karimebrahem) 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | // 24 | 25 | import Foundation 26 | 27 | /// Type describing the Animation Styles used to create the animation of Expanding and Collapsing `SwiftyMenu`. 28 | public enum AnimationStyle { 29 | /// `SwiftyMenu` animation should be linear (Smooth Animation) 30 | case linear 31 | 32 | /// `SwiftyMenu` animation should be spring (Bouncy Animation) 33 | case spring(level: SpringPowerLevel) 34 | 35 | /// Defines how bouncy the animation should be 36 | /// 37 | /// - low: Bit of smooth and a bit of bounciness at the end 38 | /// - normal: Not too bouncy and not too smooth 39 | /// - high: Too bouncy 40 | public enum SpringPowerLevel: Double { 41 | case low = 0.75 42 | case normal = 1.0 43 | case high = 1.5 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /SwiftyMenu/Classes/SwiftyMenuCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftyMenuCell.swift 3 | // 4 | // Copyright (c) 2019-2024 Karim Ebrahem (https://www.linkedin.com/in/karimebrahem) 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | // 24 | 25 | import Foundation 26 | 27 | class SwiftyMenuCell: UITableViewCell { 28 | var rightMargin: CGFloat = 0.0 29 | var leftMargin: CGFloat = 0.0 30 | var isContentRightToLeft: Bool = false 31 | 32 | 33 | override func layoutSubviews() { 34 | super.layoutSubviews() 35 | 36 | if isContentRightToLeft { 37 | textLabel?.frame.origin.x = rightMargin 38 | textLabel?.frame.size.width = contentView.frame.width - rightMargin - leftMargin 39 | textLabel?.textAlignment = .right 40 | } else { 41 | textLabel?.frame.origin.x = leftMargin 42 | textLabel?.frame.size.width = contentView.frame.width - rightMargin - leftMargin 43 | textLabel?.textAlignment = .left 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /SwiftyMenu/Classes/MenuAttributes/SwiftyMenuAttributes+SeparatorStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftyMenuAttributes+SeparatorStyle.swift 3 | // 4 | // Copyright (c) 2019-2024 Karim Ebrahem (https://www.linkedin.com/in/karimebrahem) 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | // 24 | 25 | import Foundation 26 | import UIKit 27 | 28 | public extension SwiftyMenuAttributes { 29 | 30 | /** Describes the style for separator of the menu */ 31 | enum SeparatorStyle { 32 | 33 | case `default` 34 | 35 | case value(color: UIColor, isBlured: Bool, style: UITableViewCell.SeparatorStyle) 36 | 37 | var separatorStyleValues: (color: UIColor, isBlured: Bool, style: UITableViewCell.SeparatorStyle) { 38 | switch self { 39 | case let .value(color, isBlured, style): 40 | return (color: color, isBlured: isBlured, style: style) 41 | case .default: 42 | return (color: .gray, isBlured: false, style: UITableViewCell.SeparatorStyle.singleLine) 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /SwiftyMenu/Classes/MenuAttributes/SwiftyMenuAttributes+MarginHorizontal.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftyMenuAttributes+MarginHorizontal.swift 3 | // 4 | // Copyright (c) 2019-2024 Karim Ebrahem (https://www.linkedin.com/in/karimebrahem) 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | // 24 | 25 | import Foundation 26 | import UIKit 27 | 28 | public extension SwiftyMenuAttributes { 29 | enum MarginHorizontal { 30 | 31 | case `default` 32 | case value(leading: CGFloat, trailing: CGFloat) 33 | 34 | var leadingValue: CGFloat { 35 | switch self { 36 | case let .value(leading, _): 37 | return leading 38 | case .default: 39 | return 16.0 // Default leading margin 40 | } 41 | } 42 | 43 | var trailingValue: CGFloat { 44 | switch self { 45 | case let .value(_, trailing): 46 | return trailing 47 | case .default: 48 | return 16.0 // Default trailing margin 49 | } 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /SwiftyMenu/Classes/MenuAttributes/SwiftyMenuAttributes+RowStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftyMenuAttributes+RowStyle.swift 3 | // 4 | // Copyright (c) 2019-2024 Karim Ebrahem (https://www.linkedin.com/in/karimebrahem) 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | // 24 | 25 | import Foundation 26 | import UIKit 27 | 28 | public extension SwiftyMenuAttributes { 29 | 30 | /** Describes the style for row of the menu */ 31 | enum RowStyle { 32 | 33 | /** Row with default color, background color and selected color */ 34 | case `default` 35 | 36 | /** Row with color, background color and selected color */ 37 | case value(height: Int, backgroundColor: UIColor, selectedColor: UIColor) 38 | 39 | var rowStyleValues: (height: Int, backgroundColor: UIColor, selectedColor: UIColor) { 40 | switch self { 41 | case let .value(height, backgroundColor, selectedColor): 42 | return (height: height, backgroundColor: backgroundColor, selectedColor: selectedColor) 43 | case .default: 44 | return (height: 35, backgroundColor: .white, selectedColor: .clear) 45 | } 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /SwiftyMenu/Classes/MenuAttributes/SwiftyMenuAttributes+TextStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftyMenuAttributes+TextStyle.swift 3 | // 4 | // Copyright (c) 2019-2024 Karim Ebrahem (https://www.linkedin.com/in/karimebrahem) 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | // 24 | 25 | import Foundation 26 | import UIKit 27 | 28 | public extension SwiftyMenuAttributes { 29 | 30 | /** Describes the style for separator of the menu */ 31 | enum TextStyle { 32 | 33 | case `default` 34 | 35 | case value(color: UIColor, selectedColor: UIColor? = nil, separator: String, font: UIFont?, alignment: NSTextAlignment = .left) 36 | 37 | var textStyleValues: (color: UIColor, selectedColor: UIColor?, separator: String, font: UIFont?, alignment: NSTextAlignment) { 38 | switch self { 39 | case let .value(color, selectedColor, separator, font, alignment): 40 | return (color: color, selectedColor ?? color, separator: separator, font: font, alignment: alignment) 41 | case .default: 42 | return (color: .black, nil, separator: ", ", font: .systemFont(ofSize: 12), alignment: .left) 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /SwiftyMenu/Classes/MenuAttributes/SwiftyMenuAttributes+Animation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftyMenuAttributes+Animation.swift 3 | // 4 | // Copyright (c) 2019-2024 Karim Ebrahem (https://www.linkedin.com/in/karimebrahem) 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | // 24 | 25 | import Foundation 26 | 27 | extension SwiftyMenuAttributes { 28 | /// Type describing the Animation Styles used to create the animation of Expanding and Collapsing `SwiftyMenu`. 29 | public enum Animation { 30 | /// `SwiftyMenu` animation should be linear (Smooth Animation) 31 | case linear 32 | 33 | /// `SwiftyMenu` animation should be spring (Bouncy Animation) 34 | case spring(level: SpringPowerLevel) 35 | 36 | /// Defines how bouncy the animation should be 37 | /// 38 | /// - low: Bit of smooth and a bit of bounciness at the end 39 | /// - normal: Not too bouncy and not too smooth 40 | /// - high: Too bouncy 41 | public enum SpringPowerLevel: Double { 42 | case low = 0.75 43 | case normal = 1.0 44 | case high = 1.5 45 | } 46 | 47 | var springPower: Double? { 48 | switch self { 49 | case let .spring(level): 50 | return level.rawValue 51 | default: 52 | return nil 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Example/SwiftyMenu/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // SwiftyMenu 4 | // 5 | // Created by KEMansour on 04/17/2019. 6 | // Copyright (c) 2019 KEMansour. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | 17 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 18 | // Override point for customization after application launch. 19 | 20 | // For testing RTL support 21 | //UIView.appearance().semanticContentAttribute = .forceRightToLeft 22 | 23 | return true 24 | } 25 | 26 | func applicationWillResignActive(_ application: UIApplication) { 27 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 28 | // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. 29 | } 30 | 31 | func applicationDidEnterBackground(_ application: UIApplication) { 32 | // 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. 33 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 34 | } 35 | 36 | func applicationWillEnterForeground(_ application: UIApplication) { 37 | // 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. 38 | } 39 | 40 | func applicationDidBecomeActive(_ application: UIApplication) { 41 | // 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. 42 | } 43 | 44 | func applicationWillTerminate(_ application: UIApplication) { 45 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 46 | } 47 | 48 | 49 | } 50 | 51 | -------------------------------------------------------------------------------- /SwiftyMenu/Classes/MenuAttributes/SwiftyMenuAttributes+HeaderStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftyMenuAttributes+HeaderStyle.swift 3 | // 4 | // Copyright (c) 2019-2024 Karim Ebrahem (https://www.linkedin.com/in/karimebrahem) 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | // 24 | 25 | import Foundation 26 | import UIKit 27 | 28 | public extension SwiftyMenuAttributes { 29 | 30 | /** Describes the style for header of the menu */ 31 | enum HeaderStyle { 32 | 33 | /** Row with background color */ 34 | case value(backgroundColor: UIColor, contentHorizontalAlignment: UIControl.ContentHorizontalAlignment = Self.defaultContentHorizontalAlignment, height: Int) 35 | 36 | var headerStyleValues: (backgroundColor: UIColor, contentHorizontalAlignment: UIControl.ContentHorizontalAlignment, height: Int) { 37 | switch self { 38 | case let .value(backgroundColor, contentHorizontalAlignment, height): 39 | return (backgroundColor: backgroundColor, contentHorizontalAlignment: contentHorizontalAlignment, height: height) 40 | } 41 | } 42 | 43 | public static var defaultContentHorizontalAlignment: UIControl.ContentHorizontalAlignment { 44 | if #available(iOS 11.0, *) { 45 | return .leading 46 | } else { 47 | return .left 48 | } 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /SwiftyMenu/Classes/MenuAttributes/SwiftyMenuAttributes+SelectionBehavior.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftyMenuAttributes+SelectionBehavior.swift 3 | // 4 | // Copyright (c) 2019-2024 Karim Ebrahem (https://www.linkedin.com/in/karimebrahem) 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | // 24 | 25 | import Foundation 26 | 27 | public extension SwiftyMenuAttributes { 28 | 29 | /** Describes the event of scroll user interaction */ 30 | enum MultiSelection { 31 | 32 | /** The scroll ability is totally disabled */ 33 | case disabled(allowSingleDeselection: Bool = false) 34 | 35 | /** The scroll abiliby is enabled */ 36 | case enabled 37 | 38 | var isEnabled: Bool { 39 | switch self { 40 | case .disabled: 41 | return false 42 | default: 43 | return true 44 | } 45 | } 46 | } 47 | 48 | /** Describes the event of scroll user interaction */ 49 | enum HideMenuWhenSelection { 50 | 51 | /** The scroll ability is totally disabled */ 52 | case disabled 53 | 54 | /** The scroll abiliby is enabled */ 55 | case enabled 56 | 57 | var isEnabled: Bool { 58 | switch self { 59 | case .disabled: 60 | return false 61 | default: 62 | return true 63 | } 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /SwiftyMenu/Classes/MenuAttributes/SwiftyMenuAttributes+ArrowStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftyMenuAttributes+ArrowStyle.swift 3 | // 4 | // Copyright (c) 2019-2024 Karim Ebrahem (https://www.linkedin.com/in/karimebrahem) 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | // 24 | 25 | import Foundation 26 | import UIKit 27 | 28 | public extension SwiftyMenuAttributes { 29 | 30 | /** Describes the style for row of the menu */ 31 | enum ArrowStyle { 32 | 33 | case `default` 34 | 35 | case value(isEnabled: Bool, image: UIImage? = nil, tintColor: UIColor? = nil, spacingBetweenText: CGFloat = 0) // Added tintColor parameter with default nil 36 | 37 | var arrowStyleValues: (isEnabled: Bool, image: UIImage?, tintColor: UIColor?, spacingBetweenText: CGFloat) { // Added tintColor to the return type 38 | #if SWIFT_PACKAGE 39 | let frameworkBundle = Bundle.module 40 | #else 41 | let frameworkBundle = Bundle(for: SwiftyMenu.self) 42 | #endif 43 | let defaultImage = UIImage(named: "downArrow", in: frameworkBundle, compatibleWith: nil)! 44 | 45 | switch self { 46 | case let .value(isEnabled, image, tintColor, spacingBetweenText): 47 | if isEnabled && image != nil { 48 | return (isEnabled: isEnabled, image: image, tintColor: tintColor, spacingBetweenText: spacingBetweenText) 49 | } else if isEnabled { 50 | return (isEnabled: isEnabled, image: defaultImage, tintColor: tintColor, spacingBetweenText: spacingBetweenText) 51 | } else { 52 | return (isEnabled: isEnabled, image: nil, tintColor: tintColor, spacingBetweenText: spacingBetweenText) 53 | } 54 | case .default: 55 | return (isEnabled: true, image: defaultImage, tintColor: nil, spacingBetweenText: 0) // Default tintColor is nil 56 | } 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [1.0.0] - 2021-07-04 8 | ### Added 9 | - SwiftMenu different Attributes. 10 | - Support for SPM. 11 | 12 | ### Changed 13 | - Removed all `IBDesignable` from SwiftyMenu and changed the styling of the menu using `SwiftyMenuAttributes`. 14 | 15 | ### Fixed 16 | - Arrow direction when multi selection enabled. 17 | 18 | 19 | ## [0.6.5] - 2021-03-15 20 | ### Fixed 21 | - UI issues. 22 | 23 | ## [0.6.4] - 2020-07-13 24 | ### Changed 25 | - Update SnapKit version. 26 | 27 | ### Fixed 28 | - Support RTL in SwiftyMenu. 29 | 30 | ## [0.6.3] - 2020-04-06 31 | ### Added 32 | - Allow Changing separator color. 33 | - Allow Changing separator characters. 34 | 35 | ## [0.6.2] - 2020-04-03 36 | ### Fixed 37 | - Fix arrow rotation when open or close SwiftyMenu 38 | 39 | ## [0.6.1] - 2019-03-25 40 | ### Added 41 | - Add Unit Tests to SwiftMenu with Coverage of 57%. 42 | 43 | ## [0.6.0] - 2019-03-25 44 | ### Added 45 | - Restucture project code and add some Docs. 46 | - Allow to add SwiftyMenu programmatically. 47 | - Allow to toggle SwiftyMenu from Code. 48 | 49 | ## [0.5.9] - 2019-02-20 50 | ### Changed 51 | - SwiftyMenu is now can adapt on number of items it contain when the scrolling is enabled. 52 | - Remove support for Swift 4 and only focus on Swift 5 and above. 53 | 54 | ## [0.5.8] - 2019-09-03 55 | ### Fixed 56 | - Fix pre selected index in single selection didn't update label. 57 | 58 | ## [0.5.7] - 2019-09-03 59 | ### Fixed 60 | - Fix pre selected index in single selection didn't update label. 61 | 62 | ## [0.5.6] - 2019-09-02 63 | ### Fixed 64 | - Fix pre selected indices didn't update label. 65 | 66 | ## [0.5.5] - 2019-09-02 67 | ### Fixed 68 | - Fix pre selected indices didn't update label. 69 | 70 | ## [0.5.4] - 2019-09-02 71 | ### Fixed 72 | - Delete extra comma at the end when menu is multi selection. 73 | - Fix title label truncate. 74 | 75 | ## [0.5.3] - 2019-09-02 76 | ### Fixed 77 | - Delete extra comma at the end when menu is multi selection. 78 | - Fix title label truncate. 79 | 80 | ## [0.5.2] - 2019-06-17 81 | ### Fixed 82 | - Delete un expected animation while select option. 83 | - Fix arrow space in different sizes. 84 | 85 | ## [0.5.1] - 2019-06-17 86 | ### Fixed 87 | - Delete un expected animation while select option. 88 | - Fix arrow space in different sizes. 89 | 90 | ## [0.5.0] - 2019-06-01 91 | ### Added 92 | - Support to generic datasource. 93 | 94 | ## [0.4.9] - 2019-06-01 95 | ### Added 96 | - Support to generic datasource. 97 | 98 | ## [0.4.8] - 2019-05-06 99 | ### Added 100 | - Automate release new version to Cocoapods from Travis CI. 101 | - Add CHANGELOG file for the project. 102 | 103 | ## [0.4.7] - 2019-05-06 104 | ### Added 105 | - Automate release new version to Cocoapods from Travis CI. 106 | - Add CHANGELOG file for the project. 107 | -------------------------------------------------------------------------------- /SwiftyMenu/Classes/MenuAttributes/SwiftyMenuAttributes+FrameStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftyMenuAttributes+FrameStyle.swift 3 | // 4 | // Copyright (c) 2019-2024 Karim Ebrahem (https://www.linkedin.com/in/karimebrahem) 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | // 24 | 25 | import Foundation 26 | import CoreGraphics 27 | import UIKit 28 | 29 | public extension SwiftyMenuAttributes { 30 | 31 | enum ListHeight { 32 | 33 | case `default` 34 | case value(height: Int) 35 | 36 | var listHeightValue: Int { 37 | switch self { 38 | case let .value(height): 39 | return height 40 | case .default: 41 | return 0 42 | } 43 | } 44 | 45 | } 46 | 47 | /** Corner radius of the entry - Specifies the corners */ 48 | enum RoundCorners { 49 | 50 | /** *None* of the corners will be round */ 51 | case none 52 | 53 | /** *All* of the corners will be round */ 54 | case all(radius: CGFloat) 55 | 56 | var hasRoundCorners: Bool { 57 | switch self { 58 | case .none: 59 | return false 60 | default: 61 | return true 62 | } 63 | } 64 | 65 | var cornerValue: CGFloat? { 66 | switch self { 67 | case let .all(radius): 68 | return radius 69 | case .none: 70 | return nil 71 | } 72 | } 73 | } 74 | 75 | /** The border around the entry */ 76 | enum Border { 77 | 78 | /** No border */ 79 | case none 80 | 81 | /** Border wirh color and width */ 82 | case value(color: UIColor, width: CGFloat) 83 | 84 | var hasBorder: Bool { 85 | switch self { 86 | case .none: 87 | return false 88 | default: 89 | return true 90 | } 91 | } 92 | 93 | var borderValues: (color: UIColor, width: CGFloat)? { 94 | switch self { 95 | case .value(color: let color, width: let width): 96 | return(color: color, width: width) 97 | case .none: 98 | return nil 99 | } 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /SwiftyMenu/Classes/SwiftyMenuDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftyMenuDelegate.swift 3 | // 4 | // Copyright (c) 2019-2024 Karim Ebrahem (https://www.linkedin.com/in/karimebrahem) 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | // 24 | 25 | import Foundation 26 | 27 | /// `SwiftyMenuDelegate` is the delegate interface to delegate different actions of SwiftyMenu. 28 | public protocol SwiftyMenuDelegate: AnyObject { 29 | /// Called when select an item from `SwiftyMenu`. 30 | /// 31 | /// - Parameters: 32 | /// - swiftyMenu: The `SwiftyMenu` that was selected from it's items. 33 | /// - item: The `Item` that had been selected from `SwiftyMenu`. 34 | /// - index: The `Index` of the selected `Item`. 35 | func swiftyMenu(_ swiftyMenu: SwiftyMenu, didSelectItem item: SwiftyMenuDisplayable, atIndex index: Int) 36 | 37 | /// Called when select an item from `SwiftyMenu`. 38 | /// 39 | /// - Parameters: 40 | /// - swiftyMenu: The `SwiftyMenu` that was deselected from it's items. 41 | /// - item: The `Item` that had been deselected from `SwiftyMenu`. 42 | /// - index: The `Index` of the deselected `Item`. 43 | func swiftyMenu(_ swiftyMenu: SwiftyMenu, didDeselectItem item: SwiftyMenuDisplayable, atIndex index: Int) 44 | 45 | /// Called when `SwiftyMenu` will going to Expand. 46 | /// 47 | /// - Parameter swiftyMenu: The `SwiftyMenu` that is going to Expand. 48 | func swiftyMenu(willExpand swiftyMenu: SwiftyMenu) 49 | 50 | /// Called when `SwiftyMenu` Expanded. 51 | /// 52 | /// - Parameter swiftyMenu: The `SwiftyMenu` that Expanded. 53 | func swiftyMenu(didExpand swiftyMenu: SwiftyMenu) 54 | 55 | /// Called when `SwiftyMenu` will going to Collapse. 56 | /// 57 | /// - Parameter swiftyMenu: The `SwiftyMenu` that is going to Collapse. 58 | func swiftyMenu(willCollapse swiftyMenu: SwiftyMenu) 59 | 60 | /// Called when `SwiftyMenu` Collapsed. 61 | /// 62 | /// - Parameter swiftyMenu: The `SwiftyMenu` that Collapsed. 63 | func swiftyMenu(didCollapse swiftyMenu: SwiftyMenu) 64 | } 65 | 66 | /// `SwiftyMenuDelegate` Default implementation to workaround Optionality. 67 | public extension SwiftyMenuDelegate { 68 | func swiftyMenu(willExpand swiftyMenu: SwiftyMenu) { } 69 | func swiftyMenu(didExpand swiftyMenu: SwiftyMenu) { } 70 | func swiftyMenu(willCollapse swiftyMenu: SwiftyMenu) { } 71 | func swiftyMenu(didCollapse swiftyMenu: SwiftyMenu) { } 72 | } 73 | -------------------------------------------------------------------------------- /Example/SwiftyMenu/Base.lproj/LaunchScreen.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 25 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /SwiftyMenu/Classes/MenuAttributes/SwiftyMenuAttributes.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftyMenuAttributes.swift 3 | // 4 | // Copyright (c) 2019-2024 Karim Ebrahem (https://www.linkedin.com/in/karimebrahem) 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | // 24 | 25 | import Foundation 26 | import UIKit 27 | 28 | public struct SwiftyMenuAttributes { 29 | 30 | /** 31 | A settable **optional** name that matches the menu-attributes. 32 | */ 33 | public var name: String? 34 | 35 | /** 36 | Describes the scrolling behaviour of the menu. 37 | */ 38 | public var scroll = Scroll.enabled 39 | 40 | /** 41 | Describes the accessory behaviour of the menu. 42 | */ 43 | public var accessory = Accessory.enabled 44 | 45 | /** 46 | Describes the selection behaviour of the menu. 47 | */ 48 | public var multiSelect = MultiSelection.enabled 49 | 50 | /** 51 | Describes the after selection behaviour of the menu. 52 | */ 53 | public var hideOptionsWhenSelect = HideMenuWhenSelection.enabled 54 | 55 | /** 56 | Describes the style for row of the menu. 57 | */ 58 | public var rowStyle = RowStyle.default 59 | 60 | /** 61 | Describes the style for header of the menu. 62 | */ 63 | public var headerStyle = HeaderStyle.value(backgroundColor: .clear, height: 40) 64 | 65 | /** 66 | Describes the style for arrow of the menu. 67 | */ 68 | public var arrowStyle = ArrowStyle.default 69 | 70 | /** 71 | Describes the style for separator of the menu. 72 | */ 73 | public var separatorStyle = SeparatorStyle.default 74 | 75 | /** 76 | Describes the style for text of the menu. 77 | */ 78 | public var textStyle = TextStyle.default 79 | 80 | /** 81 | Describes the style for row of the menu. 82 | */ 83 | public var placeHolderStyle = PlaceHolderStyle.value(text: "", textColor: .black) 84 | 85 | /** The corner attributes */ 86 | public var roundCorners = RoundCorners.none 87 | 88 | /** The border around the menue */ 89 | public var border = Border.none 90 | 91 | /** The height of the menue */ 92 | public var height = ListHeight.default 93 | 94 | /** Describes how the menu animates while expanding */ 95 | public var expandingAnimation = Animation.linear 96 | 97 | /** Describes how the menu animates while collapsing */ 98 | public var collapsingAnimation = Animation.linear 99 | 100 | /** Describes how long the menu animates while expanding */ 101 | public var expandingTiming = AnimationTiming.default 102 | 103 | /** Describes how long the menu animates while collapsing */ 104 | public var collapsingTiming = AnimationTiming.default 105 | 106 | /** Describes errorInfo */ 107 | public var errorInfo = ErrorInformation.default 108 | 109 | /** Describes title margin horizontal */ 110 | public var titleMarginHorizontal = MarginHorizontal.default 111 | 112 | /** Describes item margin horizontal */ 113 | public var itemMarginHorizontal = MarginHorizontal.default 114 | 115 | /** Describes arrow margin right, sometime the custom image is no loaded well, so it's better add more space */ 116 | public var arrowMarginRight = CGFloat.zero 117 | 118 | /** Init with default attributes */ 119 | public init() {} 120 | } 121 | -------------------------------------------------------------------------------- /Example/SwiftyMenu.xcodeproj/xcshareddata/xcschemes/SwiftyMenu-Example.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 45 | 46 | 52 | 53 | 54 | 55 | 57 | 63 | 64 | 65 | 67 | 73 | 74 | 75 | 76 | 77 | 87 | 89 | 95 | 96 | 97 | 98 | 104 | 106 | 112 | 113 | 114 | 115 | 117 | 118 | 121 | 122 | 123 | -------------------------------------------------------------------------------- /Example/Tests/SwiftyMenuTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftyMenuTests.swift 3 | // SwiftyMenu_Tests 4 | // 5 | // Created by Karim Ebrahem on 3/25/20. 6 | // Copyright © 2020 CocoaPods. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import SwiftyMenu 11 | 12 | class SwiftyMenuTests: XCTestCase { 13 | 14 | var sut: SwiftyMenu? 15 | var swiftyMenuDelegateSpy: SwiftyMenuDelegateSpy? 16 | 17 | override func setUp() { 18 | sut = SwiftyMenu() 19 | sut?.heightConstraint = NSLayoutConstraint(item: sut!, attribute: NSLayoutConstraint.Attribute.height, relatedBy: NSLayoutConstraint.Relation.equal, toItem: nil, attribute: NSLayoutConstraint.Attribute.notAnAttribute, multiplier: 1, constant: 40) 20 | swiftyMenuDelegateSpy = SwiftyMenuDelegateSpy() 21 | sut?.delegate = swiftyMenuDelegateSpy 22 | sut?.configure(with: SwiftyMenuAttributes()) 23 | super.setUp() 24 | } 25 | 26 | override func tearDown() { 27 | sut = nil 28 | swiftyMenuDelegateSpy = nil 29 | super.tearDown() 30 | } 31 | 32 | func testWillExpandDelegateSuccessCalled() { 33 | // When 34 | sut?.toggle() 35 | 36 | // Then 37 | XCTAssertTrue(swiftyMenuDelegateSpy?.willExpandCalled ?? false) 38 | } 39 | 40 | func testDidExpandDelegateSuccessCalled() { 41 | // Given 42 | let didExpandExpectation = expectation(description: "SwiftyMenu doing some animation and will call this callback when finish") 43 | DispatchQueue.main.asyncAfter(deadline: .now() + 2) { 44 | // Then 45 | XCTAssertTrue(self.swiftyMenuDelegateSpy!.didExpandCalled) 46 | didExpandExpectation.fulfill() 47 | } 48 | // When 49 | sut?.toggle() 50 | 51 | // Wait 52 | waitForExpectations(timeout: 3) { error in 53 | if let error = error { 54 | XCTFail("waitForExpectationsWithTimeout errored: \(error)") 55 | } 56 | } 57 | 58 | } 59 | 60 | func testWillCollapseDelegateSuccessCalled() { 61 | // When 62 | sut?.toggle() 63 | sut?.toggle() 64 | 65 | // Then 66 | XCTAssertTrue(swiftyMenuDelegateSpy!.willCollapseCalled) 67 | } 68 | 69 | func testDidCollapseDelegateSuccessCalled() { 70 | // Given 71 | let didCollapseExpectation = expectation(description: "SwiftyMenu doing some animation and will call this callback when finish") 72 | DispatchQueue.main.asyncAfter(deadline: .now() + 2) { 73 | // Then 74 | XCTAssertTrue(self.swiftyMenuDelegateSpy!.didCollapseCalled) 75 | didCollapseExpectation.fulfill() 76 | } 77 | 78 | // When 79 | sut?.toggle() 80 | sut?.toggle() 81 | 82 | // Wait 83 | waitForExpectations(timeout: 3) { error in 84 | if let error = error { 85 | XCTFail("waitForExpectationsWithTimeout errored: \(error)") 86 | } 87 | } 88 | } 89 | 90 | func testWillExpandCallbackSuccessCalled() { 91 | // Given 92 | var willExpandCallbackCalled = false 93 | sut?.willExpand = { 94 | willExpandCallbackCalled = true 95 | } 96 | 97 | // When 98 | sut?.toggle() 99 | 100 | // Then 101 | XCTAssertTrue(willExpandCallbackCalled) 102 | } 103 | 104 | func testDidExpandCallbackSuccessCalled() { 105 | // Given 106 | let didExpandExpectation = expectation(description: "SwiftyMenu doing some animation and will call this callback when finish") 107 | var didExpandCallbackCalled = false 108 | sut?.didExpand = { 109 | didExpandCallbackCalled = true 110 | 111 | // Then 112 | XCTAssertTrue(didExpandCallbackCalled) 113 | didExpandExpectation.fulfill() 114 | } 115 | 116 | // When 117 | sut?.toggle() 118 | 119 | // Wait 120 | waitForExpectations(timeout: 3) { error in 121 | if let error = error { 122 | XCTFail("waitForExpectationsWithTimeout errored: \(error)") 123 | } 124 | } 125 | } 126 | 127 | func testWillCollapseCallbackSuccessCalled() { 128 | // Given 129 | var willCollapseCallbackCalled = false 130 | sut?.willCollapse = { 131 | willCollapseCallbackCalled = true 132 | } 133 | 134 | // When 135 | sut?.toggle() 136 | sut?.toggle() 137 | 138 | // Then 139 | XCTAssertTrue(willCollapseCallbackCalled) 140 | } 141 | 142 | func testDidCollapseCallbackSuccessCalled() { 143 | // Given 144 | let didCollapsExpectation = expectation(description: "SwiftyMenu doing some animation and will call this callback when finish") 145 | var didCollapseCallbackCalled = false 146 | sut?.didCollapse = { 147 | didCollapseCallbackCalled = true 148 | 149 | // Then 150 | XCTAssertTrue(didCollapseCallbackCalled) 151 | didCollapsExpectation.fulfill() 152 | } 153 | 154 | // When 155 | sut?.toggle() 156 | sut?.toggle() 157 | 158 | // Wait 159 | waitForExpectations(timeout: 3) { error in 160 | if let error = error { 161 | XCTFail("waitForExpectationsWithTimeout errored: \(error)") 162 | } 163 | } 164 | } 165 | 166 | } 167 | -------------------------------------------------------------------------------- /Example/SwiftyMenu/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // SwiftyMenu 4 | // 5 | // Created by KEMansour on 04/17/2019. 6 | // Copyright (c) 2019 KEMansour. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import SwiftyMenu 11 | import SnapKit 12 | 13 | class ViewController: UIViewController { 14 | 15 | @IBOutlet weak var contentScrollView: UIView! 16 | @IBOutlet weak var stackView: UIStackView! 17 | @IBOutlet private weak var dropDown: SwiftyMenu! 18 | @IBOutlet private weak var dropDown2: SwiftyMenu! 19 | @IBOutlet private weak var dropDown3: SwiftyMenu! 20 | @IBOutlet private weak var otherView: UIView! 21 | @IBOutlet weak var isErrorEnableSwitch: UISwitch! 22 | 23 | let indexSelectedForError = 0 24 | 25 | /// Define menu data source 26 | /// The data source type should conform to `SwiftyMenuDisplayable` 27 | private let dropDownOptionsDataSource = [ 28 | "Option 1", "Option 2", "Option 3", 29 | "Option 4", "Option 5", "Option 6", 30 | "Option 7", "Option 8", "Option 9" 31 | ] 32 | 33 | private let dropDownCodeOptionsDataSource = [ 34 | MealSize(id: 1, name: "Small"), 35 | MealSize(id: 2, name: "Medium"), 36 | MealSize(id: 3, name: "Large"), 37 | MealSize(id: 4, name: "Combo Large"), 38 | ] 39 | 40 | /// Define menu attributes 41 | private var storyboardMenuAttributes: SwiftyMenuAttributes { 42 | var attributes = SwiftyMenuAttributes() 43 | 44 | // Custom Behavior 45 | attributes.multiSelect = .enabled 46 | attributes.scroll = .enabled 47 | attributes.hideOptionsWhenSelect = .disabled 48 | 49 | // Custom UI 50 | attributes.roundCorners = .all(radius: 8) 51 | attributes.rowStyle = .value(height: 35, backgroundColor: .white, selectedColor: UIColor.gray.withAlphaComponent(0.1)) 52 | attributes.headerStyle = .value(backgroundColor: .white, height: 35) 53 | attributes.placeHolderStyle = .value(text: "Please Select Item", textColor: .lightGray) 54 | attributes.textStyle = .value(color: .gray, separator: " & ", font: .systemFont(ofSize: 12)) 55 | attributes.separatorStyle = .value(color: .black, isBlured: false, style: .singleLine) 56 | attributes.arrowStyle = .value(isEnabled: true) 57 | attributes.height = .value(height: 300) 58 | 59 | // Custom Animations 60 | attributes.expandingAnimation = .spring(level: .low) 61 | attributes.expandingTiming = .value(duration: 0.5, delay: 0) 62 | 63 | attributes.collapsingAnimation = .linear 64 | attributes.collapsingTiming = .value(duration: 0.5, delay: 0) 65 | 66 | attributes.titleMarginHorizontal = .value(leading: 5, trailing: 5) 67 | attributes.itemMarginHorizontal = .value(leading: 5, trailing: 5) 68 | 69 | attributes.errorInfo = .default 70 | 71 | return attributes 72 | } 73 | 74 | private var codeMenuAttributes: SwiftyMenuAttributes { 75 | var attributes = SwiftyMenuAttributes() 76 | 77 | // Custom Behavior 78 | attributes.multiSelect = .disabled(allowSingleDeselection: true) 79 | attributes.scroll = .disabled 80 | attributes.hideOptionsWhenSelect = .enabled 81 | 82 | // Custom UI 83 | attributes.roundCorners = .all(radius: 8) 84 | attributes.rowStyle = .value(height: 39, backgroundColor: .white, selectedColor: .white) 85 | attributes.headerStyle = .value(backgroundColor: .white, height: 40) 86 | attributes.placeHolderStyle = .value(text: "Please Select Size", textColor: .lightGray) 87 | attributes.textStyle = .value(color: .gray, separator: " & ", font: .systemFont(ofSize: 12)) 88 | attributes.separatorStyle = .value(color: .black, isBlured: false, style: .singleLine) 89 | attributes.arrowStyle = .value(isEnabled: false) 90 | attributes.accessory = .disabled 91 | 92 | // Custom Animations 93 | attributes.expandingAnimation = .linear 94 | attributes.expandingTiming = .value(duration: 0.5, delay: 0) 95 | 96 | attributes.collapsingAnimation = .linear 97 | attributes.collapsingTiming = .value(duration: 0.5, delay: 0) 98 | 99 | return attributes 100 | } 101 | 102 | override func viewDidLoad() { 103 | super.viewDidLoad() 104 | 105 | setupStoryboardMenu() 106 | setupMenu2() 107 | setupMenu3() 108 | setupCodeMenu() 109 | } 110 | 111 | /// Example of building SwiftyMenu from Storyboard 112 | private func setupStoryboardMenu() { 113 | /// Setup component 114 | dropDown.delegate = self 115 | dropDown.items = dropDownOptionsDataSource 116 | 117 | /// Configure SwiftyMenu with the attributes 118 | dropDown.configure(with: storyboardMenuAttributes) 119 | } 120 | 121 | //Example for swiftyMenu2 with custom margins 122 | private func setupMenu2() { 123 | /// Setup component 124 | dropDown2.delegate = self 125 | dropDown2.items = dropDownOptionsDataSource 126 | 127 | var attributes = storyboardMenuAttributes 128 | attributes.titleMarginHorizontal = .value(leading: 10, trailing: 20) 129 | attributes.itemMarginHorizontal = .value(leading: 10, trailing: 10) 130 | let image = UIImage(named: "arrow.down.custom") 131 | attributes.arrowStyle = .value(isEnabled: true, image: image, tintColor: .purple, spacingBetweenText: 10.0) 132 | /// Configure SwiftyMenu with the attributes 133 | dropDown2.configure(with: attributes) 134 | } 135 | 136 | private func setupMenu3() { 137 | /// Setup component 138 | dropDown3.delegate = self 139 | dropDown3.items = dropDownOptionsDataSource 140 | 141 | var attributes = storyboardMenuAttributes 142 | attributes.titleMarginHorizontal = .value(leading: 0, trailing: 0) 143 | attributes.itemMarginHorizontal = .value(leading: 40, trailing: 40) 144 | 145 | attributes.multiSelect = .disabled(allowSingleDeselection: false) 146 | attributes.arrowStyle = .value(isEnabled: false) 147 | attributes.hideOptionsWhenSelect = .enabled 148 | 149 | /// Configure SwiftyMenu with the attributes 150 | dropDown3.configure(with: attributes) 151 | } 152 | 153 | /// Example of building SwiftyMenu from Code 154 | private func setupCodeMenu() { 155 | 156 | /// Init SwiftyMenu from Code 157 | let dropDownCode = SwiftyMenu(frame: CGRect(x: 0, y: 0, width: 0, height: 40)) 158 | 159 | /// Add it as subview 160 | view.addSubview(dropDownCode) 161 | 162 | /// Add constraints to SwiftyMenu 163 | /// You must take care of `hegiht` constraint, please. 164 | dropDownCode.translatesAutoresizingMaskIntoConstraints = false 165 | //let horizontalConstraint = NSLayoutConstraint(item: dropDownCode, attribute: NSLayoutConstraint.Attribute.centerX, relatedBy: NSLayoutConstraint.Relation.equal, toItem: view, attribute: NSLayoutConstraint.Attribute.centerX, multiplier: 1, constant: 0) 166 | //let topConstraint = NSLayoutConstraint(item: dropDownCode, attribute: NSLayoutConstraint.Attribute.top, relatedBy: NSLayoutConstraint.Relation.equal, toItem: otherView, attribute: NSLayoutConstraint.Attribute.top, multiplier: 1, constant: 64) 167 | //let widthConstraint = NSLayoutConstraint(item: dropDownCode, attribute: NSLayoutConstraint.Attribute.width, relatedBy: NSLayoutConstraint.Relation.equal, toItem: nil, attribute: NSLayoutConstraint.Attribute.notAnAttribute, multiplier: 1, constant: 255) 168 | dropDownCode.heightConstraint = NSLayoutConstraint(item: dropDownCode, attribute: NSLayoutConstraint.Attribute.height, relatedBy: NSLayoutConstraint.Relation.equal, toItem: nil, attribute: NSLayoutConstraint.Attribute.notAnAttribute, multiplier: 1, constant: 40) 169 | 170 | NSLayoutConstraint.activate( 171 | [ 172 | //horizontalConstraint, 173 | //topConstraint, 174 | //widthConstraint, 175 | dropDownCode.heightConstraint, 176 | ] 177 | ) 178 | 179 | stackView.addArrangedSubview(dropDownCode) 180 | 181 | /// Setup SwiftyMenu data source 182 | dropDownCode.items = dropDownCodeOptionsDataSource 183 | 184 | /// SwiftyMenu also supports `CallBacks` 185 | dropDownCode.willExpand = { 186 | print("SwiftyMenu Code Will Expand!") 187 | } 188 | 189 | dropDownCode.didExpand = { 190 | print("SwiftyMenu Code Expanded!") 191 | } 192 | 193 | dropDownCode.willCollapse = { 194 | print("SwiftyMenu Code Will Collapse!") 195 | } 196 | 197 | dropDownCode.didCollapse = { 198 | print("SwiftyMenu Code Collapsed!") 199 | } 200 | 201 | dropDownCode.didSelectItem = { _, item, index in 202 | print("Selected from Code \(item) at index: \(index)") 203 | } 204 | 205 | dropDownCode.didDeselectItem = { _, item, index in 206 | print("Deselected from Code \(item) at index: \(index)") 207 | } 208 | 209 | /// Configure SwiftyMenu with the attributes 210 | var attributes = codeMenuAttributes 211 | attributes.border = .value(color: .black, width: 1.0) 212 | /*//attributes.arrowStyle = .default 213 | //attributes.arrowStyle = .value(isEnabled: true, image: nil, tintColor: nil) 214 | attributes.separatorStyle = .value(color: .black, isBlured: false, style: .singleLine) 215 | attributes.arrowStyle = .value(isEnabled: false) 216 | //attributes.multiSelect = .disabled(allowSingleDeselection: false) 217 | attributes.multiSelect = .enabled 218 | attributes.hideOptionsWhenSelect = .disabled*/ 219 | 220 | dropDownCode.configure(with: attributes) 221 | 222 | } 223 | 224 | @IBAction func toggleIsErrorEnable(_ sender: UISwitch) { 225 | if sender == isErrorEnableSwitch{ 226 | if sender.isOn{ 227 | if dropDown.selectedIndecis.contains(where: { $0.key == indexSelectedForError }), isErrorEnableSwitch.isOn{ 228 | dropDown.setError(hasError: true) 229 | }else{ 230 | dropDown.setError(hasError: false) 231 | } 232 | }else{ 233 | dropDown.setError(hasError: false) 234 | } 235 | } 236 | } 237 | 238 | override func viewDidAppear(_ animated: Bool) { 239 | DispatchQueue.main.asyncAfter(deadline: .now()+1){ 240 | self.dropDown2.setError(hasError: true) 241 | self.dropDown2.toggle() 242 | } 243 | } 244 | } 245 | 246 | // MARK: - SwiftMenuDelegate 247 | 248 | extension ViewController: SwiftyMenuDelegate { 249 | func swiftyMenu(_ swiftyMenu: SwiftyMenu, didSelectItem item: SwiftyMenuDisplayable, atIndex index: Int) { 250 | print("didSelectItem item: \(item), at index: \(index)") 251 | 252 | if swiftyMenu == dropDown{ 253 | if index == indexSelectedForError{ 254 | if isErrorEnableSwitch.isOn{ 255 | dropDown.setError(hasError: true) 256 | }else{ 257 | dropDown.setError(hasError: false) 258 | } 259 | }else{ 260 | if dropDown.selectedIndecis.contains(where: { $0.key == indexSelectedForError }), isErrorEnableSwitch.isOn{ 261 | dropDown.setError(hasError: true) 262 | }else{ 263 | dropDown.setError(hasError: false) 264 | } 265 | } 266 | } 267 | } 268 | 269 | func swiftyMenu(_ swiftyMenu: SwiftyMenu, didDeselectItem item: SwiftyMenuDisplayable, atIndex index: Int) { 270 | print("didDeselectItem item: \(item), at index: \(index)") 271 | 272 | if swiftyMenu == dropDown{ 273 | if index == indexSelectedForError{ 274 | if isErrorEnableSwitch.isOn{ 275 | dropDown.setError(hasError: false) 276 | } 277 | } 278 | } 279 | } 280 | 281 | func swiftyMenu(willExpand swiftyMenu: SwiftyMenu) { 282 | print("SwiftyMenu willExpand.") 283 | } 284 | 285 | func swiftyMenu(didExpand swiftyMenu: SwiftyMenu) { 286 | print("SwiftyMenu didExpand.") 287 | } 288 | 289 | func swiftyMenu(willCollapse swiftyMenu: SwiftyMenu) { 290 | print("SwiftyMenu willCollapse.") 291 | } 292 | 293 | func swiftyMenu(didCollapse swiftyMenu: SwiftyMenu) { 294 | print("SwiftyMenu didCollapse.") 295 | } 296 | 297 | } 298 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | SwiftyMenu 3 |

4 | 5 |

6 | 7 | 8 | 9 | Cocoapod 10 | 11 | Swift Package Manager 12 | Version 13 | 14 | MIT License 15 | 16 |
17 | 18 | Facebook: @KarimEbrahemAbdelaziz 19 | 20 | 21 | LinkedIn: @karimebrahem 22 | 23 |

24 | 25 | # 26 | 27 | SwiftyMenu is simple yet powerfull drop down menu component for iOS. It allow you to have drop down menu that doesn't appear over your views, which give you awesome user experience. 28 | 29 | - [Screenshots](#screenshots) 30 | - [Requirements](#requirements) 31 | - [Installation](#installation) 32 | - [Cocoapods](#cocoapods) 33 | - [Swift Package Manager](#swift-package-manager) 34 | - [Usage](#usage) 35 | - [Initialization](#initialization) 36 | - [Storyboard](#storyboard) 37 | - [Code](#code) 38 | - [Configure DataSource](#configure-datasource) 39 | - [Setup Models](#setup-models) 40 | - [Assign DataSource](#assign-datasource) 41 | - [Capture Selection](#capture-selection) 42 | - [Using Delegate](#using-delegate) 43 | - [Using Closures](#using-closures) 44 | - [UI Customization](#ui-customization) 45 | - [Placeholder](#placeholder) 46 | - [Text Style](#text-style) 47 | - [Scroll](#scroll) 48 | - [Selection Behavior](#selection-behavior) 49 | - [Row Style](#row-style) 50 | - [Frame Style](#frame-style) 51 | - [Arrow Style](#arrow-style) 52 | - [Separator Style](#separator-style) 53 | - [Header Style](#header-style) 54 | - [Accessory](#accessory) 55 | - [Animation](#animation) 56 | - [Margin Horizontal](#margin-horizontal) 57 | - [Error Info](#error-info) 58 | - [Set Error](#set-error) 59 | - [New Functionalities](#new-functionalities) 60 | - [Example Project](#example-project) 61 | - [Todo](#todo) 62 | - [Android Version](#android) 63 | - [Author](#author) 64 | - [Credits](#credits) 65 | - [License](#license) 66 | 67 | ## Screenshots 68 | 69 | 70 | 71 | 72 | 73 | ## Requirements 74 | 75 | * Xcode 10.2+ 76 | * Swift 5+ 77 | * iOS 10+ 78 | 79 | ## Installation 80 | 81 | ### CocoaPods 82 | 83 | [CocoaPods](https://cocoapods.org) is a dependency manager for Cocoa projects. For usage and installation instructions, visit their website. To integrate SwiftyMenu into your Xcode project using CocoaPods, specify it in your `Podfile`: 84 | 85 | ```ruby 86 | pod 'SwiftyMenu', '~> 1.1.0' 87 | ``` 88 | 89 | ### Swift Package Manager 90 | 91 | 1. Automatically in Xcode: 92 | 93 | - Click **File > Swift Packages > Add Package Dependency...** 94 | - Use the package URL `https://github.com/KarimEbrahemAbdelaziz/SwiftyMenu` to add TimelaneCombine to your project. 95 | 96 | 2. Manually in your **Package.swift** file add: 97 | 98 | ```swift 99 | .package(url: "https://github.com/KarimEbrahemAbdelaziz/SwiftyMenu", from: "1.1.0") 100 | ``` 101 | 102 | ## Usage 103 | 104 | `SwiftyMenu` supports initialization from `Storyboard` and `Code`. 105 | 106 | ### Initialization 107 | 108 | #### Storyboard 109 | 110 | Setup your view controller: 111 | 112 | ```swift 113 | // Connect view in storyboard with you outlet 114 | @IBOutlet private weak var dropDownMenu: SwiftyMenu! 115 | ``` 116 | Then connect `IBOutlet` to `Storyboard`, and connect the `Height` Constraints of the menu as shown below. 117 | 118 | 119 | 120 | #### Code 121 | 122 | 1. Init `SwiftyMenu` 123 | 124 | ```swift 125 | /// Init SwiftyMenu from Code 126 | let dropDownCode = SwiftyMenu(frame: CGRect(x: 0, y: 0, width: 0, height: 40)) 127 | ``` 128 | 2. Add `SwiftyMenu` as `Subview` 129 | 130 | ```swift 131 | /// Add it as subview 132 | view.addSubview(dropDownCode) 133 | ``` 134 | 3. Setup Constraints 135 | 136 | ```swift 137 | /// Add constraints to SwiftyMenu 138 | /// You must take care of `hegiht` constraint, please. 139 | dropDownCode.translatesAutoresizingMaskIntoConstraints = false 140 | 141 | let horizontalConstraint = NSLayoutConstraint(item: dropDownCode, attribute: NSLayoutConstraint.Attribute.centerX, relatedBy: NSLayoutConstraint.Relation.equal, toItem: view, attribute: NSLayoutConstraint.Attribute.centerX, multiplier: 1, constant: 0) 142 | 143 | let topConstraint = NSLayoutConstraint(item: dropDownCode, attribute: NSLayoutConstraint.Attribute.top, relatedBy: NSLayoutConstraint.Relation.equal, toItem: otherView, attribute: NSLayoutConstraint.Attribute.top, multiplier: 1, constant: 64) 144 | 145 | let widthConstraint = NSLayoutConstraint(item: dropDownCode, attribute: NSLayoutConstraint.Attribute.width, relatedBy: NSLayoutConstraint.Relation.equal, toItem: nil, attribute: NSLayoutConstraint.Attribute.notAnAttribute, multiplier: 1, constant: 255) 146 | 147 | dropDownCode.heightConstraint = NSLayoutConstraint(item: dropDownCode, attribute: NSLayoutConstraint.Attribute.height, relatedBy: NSLayoutConstraint.Relation.equal, toItem: nil, attribute: NSLayoutConstraint.Attribute.notAnAttribute, multiplier: 1, constant: 40) 148 | 149 | NSLayoutConstraint.activate( 150 | [ 151 | horizontalConstraint, 152 | topConstraint, 153 | widthConstraint, 154 | dropDownCode.heightConstraint 155 | ] 156 | ) 157 | ``` 158 | 159 | ### Configure DataSource 160 | 161 | To configure `SwiftyMenu` DataSource, you'll need to prepare your `Model` to be able to presented and retrived from the menu. Then, create and assign the DataSource to `SwiftyMenu`. 162 | 163 | #### Setup Models 164 | 165 | We are supporting Generic Data Source, all you have to do is conforming to our Generic Protocol on which type you want to add to the menu. 166 | 167 | - Example of `String` 168 | 169 | ```swift 170 | extension String: SwiftyMenuDisplayable { 171 | public var displayableValue: String { 172 | return self 173 | } 174 | 175 | public var retrivableValue: Any { 176 | return self 177 | } 178 | } 179 | ``` 180 | - Example of custom `Struct` 181 | 182 | ```swift 183 | struct MealSize { 184 | let id: Int 185 | let name: String 186 | } 187 | 188 | extension MealSize: SwiftyMenuDisplayable { 189 | public var displayableValue: String { 190 | return self.name 191 | } 192 | 193 | public var retrievableValue: Any { 194 | return self.id 195 | } 196 | } 197 | ``` 198 | 199 | #### Assign DataSource 200 | 201 | 1. Create an `Array` of your models 202 | 203 | ```swift 204 | /// Define menu data source 205 | /// The data source type must conform to `SwiftyMenuDisplayable` 206 | private let dropDownOptionsDataSource = [ 207 | MealSize(id: 1, name: "Small"), 208 | MealSize(id: 2, name: "Medium"), 209 | MealSize(id: 3, name: "Large"), 210 | MealSize(id: 4, name: "Combo Large") 211 | ] 212 | ``` 213 | 2. Assign it to `SwiftyMenu` DataSource property 214 | 215 | ```swift 216 | dropDownCode.items = dropDownOptionsDataSource 217 | ``` 218 | ### Capture Selection 219 | 220 | `SwiftyMenu` supports 2 ways to capture the selected items, `Delegate` and `Closure`s. You can use one or both of them at the same time. 221 | 222 | #### Using Delegate 223 | 224 | 1. Conform to `SwiftyMenu` Delegate protocol 225 | 226 | ```swift 227 | extension ViewController: SwiftyMenuDelegate { 228 | // Get selected option from SwiftyMenu 229 | func swiftyMenu(_ swiftyMenu: SwiftyMenu, didSelectItem item: SwiftyMenuDisplayable, atIndex index: Int) { 230 | print("Selected item: \(item), at index: \(index)") 231 | } 232 | 233 | // NEW - Get selected option from SwiftyMenu 234 | func swiftyMenu(_ swiftyMenu: SwiftyMenu, didDeselectItem item: SwiftyMenuDisplayable, atIndex index: Int){ 235 | print("deselected item: \(item), at index: \(index)") 236 | } 237 | 238 | // SwiftyMenu drop down menu will expand 239 | func swiftyMenu(willExpand swiftyMenu: SwiftyMenu) { 240 | print("SwiftyMenu willExpand.") 241 | } 242 | 243 | // SwiftyMenu drop down menu did expand 244 | func swiftyMenu(didExpand swiftyMenu: SwiftyMenu) { 245 | print("SwiftyMenu didExpand.") 246 | } 247 | 248 | // SwiftyMenu drop down menu will collapse 249 | func swiftyMenu(willCollapse swiftyMenu: SwiftyMenu) { 250 | print("SwiftyMenu willCollapse.") 251 | } 252 | 253 | // SwiftyMenu drop down menu did collapse 254 | func swiftyMenu(didCollapse swiftyMenu: SwiftyMenu) { 255 | print("SwiftyMenu didCollapse.") 256 | } 257 | } 258 | ``` 259 | 2. Assign `SwiftyMenu` delegate 260 | 261 | ```swift 262 | dropDownCode.delegate = self 263 | ``` 264 | 265 | #### Using Closures 266 | 267 | You can use callbacks to know what happen: 268 | 269 | ```swift 270 | /// SwiftyMenu also supports `CallBacks` 271 | dropDownCode.didSelectItem = { menu, item, index in 272 | print("Selected \(item) at index: \(index)") 273 | } 274 | 275 | dropDownCode.willExpand = { 276 | print("SwiftyMenu Will Expand!") 277 | } 278 | 279 | dropDownCode.didExpand = { 280 | print("SwiftyMenu Expanded!") 281 | } 282 | 283 | dropDownCode.willCollapse = { 284 | print("SwiftyMenu Will Collapse!") 285 | } 286 | 287 | dropDownCode.didCollapse = { 288 | print("SwiftyMenu Collapsed!") 289 | } 290 | ``` 291 | 292 | ### UI Customization 293 | 294 | Having an amazing drop down menu is essential. So, there're a lot of UI customization for `SwiftyMenu` (More to be added soon). 295 | 296 | To configure UI customization for `SwiftyMenu`: 297 | 298 | 1. Create `SwiftyMenuAttributes` property 299 | 300 | ```swift 301 | private var codeMenuAttributes = SwiftyMenuAttributes() 302 | ``` 303 | 2. Assign it to `SwiftyMenu` 304 | 305 | ```swift 306 | /// Configure SwiftyMenu with the attributes 307 | dropDownCode.configure(with: codeMenuAttributes) 308 | ``` 309 | Also before assigning it to `SwiftyMenu` you could customize the menu look using following attributes. 310 | 311 | #### Placeholder 312 | 313 | ```swift 314 | attributes.placeHolderStyle = .value(text: "Please Select Size", textColor: .lightGray) 315 | ``` 316 | 317 | #### Text Style 318 | 319 | ```swift 320 | attributes.textStyle = .value(color: .gray, separator: " & ", font: .systemFont(ofSize: 12)) 321 | ``` 322 | 323 | #### Scroll 324 | 325 | ```swift 326 | attributes.scroll = .disabled 327 | ``` 328 | 329 | #### Selection Behavior 330 | 331 | ```swift 332 | //NEW - Single selection disallowing deselection clicking the same item 333 | attributes.multiSelect = .disabled(allowSingleDeselection: false) 334 | //NEW - Single selection allowing deselection clicking the same item 335 | attributes.multiSelect = .disabled(allowSingleDeselection: true) 336 | //Multi selection enable 337 | attributes.multiSelect = .enabled 338 | attributes.hideOptionsWhenSelect = .enabled 339 | ``` 340 | 341 | #### Row Style 342 | 343 | ```swift 344 | attributes.rowStyle = .value(height: 40, backgroundColor: .white, selectedColor: .white) 345 | ``` 346 | 347 | #### Frame Style 348 | 349 | ```swift 350 | /// Rounded Corners 351 | attributes.roundCorners = .all(radius: 8) 352 | 353 | /// Menu Maximum Height 354 | attributes.height = .value(height: 300) 355 | 356 | /// Menu Border 357 | attributes.border = .value(color: .gray, width: 0.5) 358 | ``` 359 | 360 | #### Arrow Style 361 | 362 | ```swift 363 | /// `SwiftyMenu` have default arrow 364 | attributes.arrowStyle = .value(isEnabled: true) 365 | /// NEW - Now able to add a custom image, tint color and space between icon and text 366 | attributes.arrowStyle = .value(isEnabled: true, image: image, tintColor: .purple, spacingBetweenText: 10.0) 367 | ``` 368 | 369 | #### Separator Style 370 | 371 | ```swift 372 | attributes.separatorStyle = .value(color: .black, isBlured: false, style: .singleLine) 373 | ``` 374 | 375 | #### Header Style 376 | 377 | ```swift 378 | attributes.headerStyle = .value(backgroundColor: .white, height: 40) 379 | ``` 380 | 381 | #### Accessory 382 | 383 | ```swift 384 | attributes.accessory = .disabled 385 | ``` 386 | 387 | #### Animation 388 | 389 | ```swift 390 | attributes.expandingAnimation = .linear 391 | attributes.expandingTiming = .value(duration: 0.5, delay: 0) 392 | 393 | attributes.collapsingAnimation = .linear 394 | attributes.collapsingTiming = .value(duration: 0.5, delay: 0) 395 | ``` 396 | 397 | #### Margin Horizontal 398 | 399 | ```swift 400 | /// Margin leading and trailing for title / placeholder 401 | attributes.titleMarginHorizontal = .value(leading: 10, trailing: 20) 402 | /// Margin leading and trailing for item list 403 | attributes.itemMarginHorizontal = .value(leading: 10, trailing: 10) 404 | ``` 405 | 406 | #### Error Info 407 | 408 | ```swift 409 | /// Default use the .red color 410 | attributes.errorInfo = .default 411 | /// Custom color for placeholder and icon if it's enabled 412 | attributes.errorInfo = .custom(placeholderTextColor: .red, iconTintColor: .red) 413 | ``` 414 | 415 | #### Set Error 416 | 417 | ```swift 418 | /// Enabling this will show the config in errorInfo 419 | dropDown.setError(hasError: true) 420 | ``` 421 | 422 | ### New Functionalities 423 | * Custom arrow and color and space between arrow and title 424 | * Fix multi select title: showing now sorted by index 425 | * Margin leading and trailing for title and items 426 | * Handling deselection for multi and single selection 427 | * Seting Error with custom color for title and arrow 428 | 429 | ### Example Project 430 | 431 | You could check the full `Example` project [Here](https://github.com/KarimEbrahemAbdelaziz/SwiftyMenu/tree/master/Example). 432 | 433 | You could check the full new `Example` project [Here New Example](https://github.com/nowjordanhappy/SwiftyMenu/tree/feature/customization-ui/Example). 434 | 435 | ## TODO 436 | 437 | - [x] Automate release new version to Cocoapods from Github Actions. 438 | - [x] Add CHANGELOG file for the project. 439 | - [x] Allow custom header and options cells. 440 | - [ ] Allow different interactions to dismiss SwiftyMenu. 441 | - [x] Allow to customize the default seperator. 442 | - [x] Support Generic DataSource. 443 | - [x] Support multi selection in SwiftMenu 🔥. 444 | - [x] Support multi SwiftyMenu in one screen. 445 | - [x] Support stack view and add example. 446 | - [x] Support call backs and delegation. 447 | - [x] Support different types of Animations. 448 | - [x] Add different customization to colors for default cells. 449 | - [x] Margin horizontal for title and item 450 | - [x] Custom arrow and tint color 451 | 452 | 453 | 454 | And much more ideas to make it solid drop down menu for iOS projects 😎💪🏻 455 | 456 | ## Android 457 | 458 | - [ExpandableSelectionView](https://github.com/ashrafDoubleO7/ExpandableSelectionView), Thanks to [Ahmed Ashraf](https://github.com/ashrafDoubleO7) ❤️💪🏻 459 | 460 | ## Author 461 | 462 | Karim Ebrahem, karimabdelazizmansour@gmail.com 463 | 464 | ## License 465 | 466 | SwiftyMenu is available under the MIT license. See the `LICENSE` file for more info. 467 | 468 | ## Credits 469 | 470 | You can find me on Twitter [@k_ebrahem_](https://twitter.com/k_ebrahem_). 471 | 472 | It will be updated when necessary and fixes will be done as soon as discovered to keep it up to date. 473 | 474 | Enjoy! 475 | -------------------------------------------------------------------------------- /Example/SwiftyMenu/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 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | -------------------------------------------------------------------------------- /SwiftyMenu/Classes/SwiftyMenu.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftyMenu.swift 3 | // 4 | // Copyright (c) 2019-2024 Karim Ebrahem (https://www.linkedin.com/in/karimebrahem) 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | // 24 | 25 | import Foundation 26 | import UIKit 27 | import SnapKit 28 | 29 | /// `SwiftyMenu` is the menu class that provides common state, delegate, and callbacks handling. 30 | final public class SwiftyMenu: UIView { 31 | 32 | // MARK: - IBOutlets 33 | 34 | @IBOutlet public var heightConstraint: NSLayoutConstraint! 35 | 36 | // MARK: - Public Properties 37 | 38 | /// `selectedIndex` is a property to get and set selected item in `SwiftyMenu` when it is a Single Selection. 39 | public var selectedIndex: Int? { 40 | didSet { 41 | if selectedIndex == nil { 42 | setPlaceholder() 43 | } else { 44 | setSingleSelectedOption() 45 | } 46 | } 47 | } 48 | 49 | /// `selectedIndecis` is a property to get and set selected item in `SwiftyMenu` when it is a Multi Selection. 50 | public var selectedIndecis: [Int: Int] = [:] { 51 | didSet { 52 | setMultiSelectedOptions() 53 | } 54 | } 55 | 56 | /// `items` is the `SwiftyMenu` DataSource. 57 | /// 58 | /// The items that will appear in the `SwiftyMenu`. 59 | public var items = [SwiftyMenuDisplayable]() { 60 | didSet { 61 | self.itemsTableView.reloadData() 62 | } 63 | } 64 | 65 | /// `delegate` is the `SwiftyMenu` delegate property. 66 | public weak var delegate: SwiftyMenuDelegate? 67 | 68 | /// `separatorCharacters` is a property to get and set separator characters in `SwiftyMenu` when it is a Multi Selection. 69 | public var separatorCharacters: String? 70 | 71 | // MARK: - Public Callbacks 72 | 73 | /// `willExpand` is a completion closure that will be executed when `SwiftyMenu` is going to expand. 74 | public var willExpand: (() -> Void) = { } 75 | 76 | /// `didExpand` is a completion closure that will be executed once `SwiftyMenu` expanded. 77 | public var didExpand: (() -> Void) = { } 78 | 79 | /// `willCollapse` is a completion closure that will be executed when `SwiftyMenu` is going to collapse. 80 | public var willCollapse: (() -> Void) = { } 81 | 82 | /// `didCollapse` is a completion closure that will be executed once `SwiftyMenu` collapsed. 83 | public var didCollapse: (() -> Void) = { } 84 | 85 | /// `didSelectItem` is a completion closure that will be executed when select an item from `SwiftyMenu`. 86 | /// - Parameters: 87 | /// - swiftyMenu: The `SwiftyMenu` that was selected from it's items. 88 | /// - item: The `Item` that had been selected from `SwiftyMenu`. 89 | /// - index: The `Index` of the selected `Item`. 90 | public var didSelectItem: ((_ swiftyMenu: SwiftyMenu,_ item: SwiftyMenuDisplayable,_ index: Int) -> Void) = { _, _, _ in } 91 | 92 | /// `didDselectItem` is a completion closure that will be executed when deselect an item from `SwiftyMenu`. 93 | /// - Parameters: 94 | /// - swiftyMenu: The `SwiftyMenu` that was deselected from it's items. 95 | /// - item: The `Item` that had been deselected from `SwiftyMenu`. 96 | /// - index: The `Index` of the deselected `Item`. 97 | public var didDeselectItem: ((_ swiftyMenu: SwiftyMenu,_ item: SwiftyMenuDisplayable,_ index: Int) -> Void) = { _, _, _ in } 98 | 99 | // MARK: - Private Properties 100 | 101 | private var selectButton: UIButton! 102 | private var itemsTableView: UITableView! 103 | private var state: SwiftyMenuState = .hidden 104 | private var width: CGFloat! 105 | private var height: CGFloat! 106 | private var setuped: Bool = false 107 | private var attributes: SwiftyMenuAttributes! 108 | private var hasError: Bool = false 109 | 110 | // MARK: - Init 111 | 112 | public override init(frame: CGRect) { 113 | super.init(frame: frame) 114 | 115 | selectButton = UIButton(frame: self.frame) 116 | itemsTableView = UITableView() 117 | } 118 | 119 | public required init(coder aDecoder: NSCoder) { 120 | super.init(coder: aDecoder)! 121 | 122 | selectButton = UIButton(frame: self.frame) 123 | itemsTableView = UITableView() 124 | } 125 | 126 | // MARK: - LifeCycle 127 | 128 | public override func layoutSubviews() { 129 | super.layoutSubviews() 130 | 131 | if !setuped { 132 | setupUI() 133 | setuped = true 134 | changeTintColorArrow(hasError: hasError) 135 | } 136 | } 137 | 138 | // MARK: - Public Funcitons 139 | 140 | /// Configure `SwiftyMenu` with attributes. 141 | public func configure(with attributes: SwiftyMenuAttributes) { 142 | self.attributes = attributes 143 | } 144 | 145 | /// Expand or Collapse `SwiftyMenu` from Code. 146 | public func toggle() { 147 | handleMenuState() 148 | } 149 | 150 | /// change style for Error from Code. 151 | public func setError(hasError: Bool){ 152 | self.hasError = hasError 153 | if attributes.multiSelect.isEnabled { 154 | if selectedIndecis.isEmpty { 155 | selectButton.setTitleColor(hasError ? attributes.errorInfo.errorInfoValues.placeholderTextColor : attributes.placeHolderStyle.placeHolderValues.textColor, for: .normal) 156 | } else { 157 | selectButton.setTitleColor(hasError ? attributes.errorInfo.errorInfoValues.placeholderTextColor : attributes.textStyle.textStyleValues.color, for: .normal) 158 | } 159 | } else { 160 | if selectedIndex == nil { 161 | selectButton.setTitleColor(hasError ? attributes.errorInfo.errorInfoValues.placeholderTextColor : attributes.placeHolderStyle.placeHolderValues.textColor, for: .normal) 162 | } else { 163 | selectButton.setTitleColor(hasError ? attributes.errorInfo.errorInfoValues.placeholderTextColor : attributes.textStyle.textStyleValues.color, for: .normal) 164 | } 165 | } 166 | 167 | changeTintColorArrow(hasError: hasError) 168 | } 169 | 170 | private func changeTintColorArrow(hasError: Bool){ 171 | if attributes.arrowStyle.arrowStyleValues.isEnabled { 172 | if let image = attributes.arrowStyle.arrowStyleValues.image{ 173 | if let tintColor = attributes.arrowStyle.arrowStyleValues.tintColor{ 174 | selectButton.setImage(image.withRenderingMode(.alwaysTemplate), for: .normal) 175 | selectButton.tintColor = (hasError ? (attributes.errorInfo.errorInfoValues.iconTintColor ?? tintColor) : tintColor) 176 | }else{ 177 | if hasError, let errorColor = attributes.errorInfo.errorInfoValues.iconTintColor{ 178 | selectButton.setImage(image.withRenderingMode(.alwaysTemplate), for: .normal) 179 | selectButton.tintColor = (errorColor) 180 | }else{ 181 | selectButton.tintColor = nil 182 | selectButton.setImage(image.withRenderingMode(.alwaysOriginal), for: .normal) 183 | } 184 | } 185 | } 186 | selectButton.layoutIfNeeded() 187 | } 188 | } 189 | } 190 | 191 | // MARK: - UITableViewDataSource Functions 192 | 193 | extension SwiftyMenu: UITableViewDataSource { 194 | public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 195 | return items.count 196 | } 197 | 198 | public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 199 | 200 | let cell = tableView.dequeueReusableCell(withIdentifier: "OptionCell", for: indexPath) as! SwiftyMenuCell 201 | cell.textLabel?.text = items[indexPath.row].displayableValue 202 | cell.textLabel?.textColor = attributes.textStyle.textStyleValues.color 203 | cell.textLabel?.font = attributes.textStyle.textStyleValues.font 204 | cell.textLabel?.textAlignment = attributes.textStyle.textStyleValues.alignment 205 | cell.tintColor = attributes.textStyle.textStyleValues.color 206 | cell.backgroundColor = attributes.rowStyle.rowStyleValues.backgroundColor 207 | cell.selectionStyle = .none 208 | cell.leftMargin = attributes.itemMarginHorizontal.leadingValue 209 | cell.rightMargin = attributes.itemMarginHorizontal.trailingValue 210 | cell.isContentRightToLeft = self.isContentRightToLeft() 211 | 212 | if attributes.multiSelect.isEnabled { 213 | if selectedIndecis[indexPath.row] != nil { 214 | cell.textLabel?.textColor = attributes.textStyle.textStyleValues.selectedColor 215 | cell.accessoryType = attributes.accessory.isEnabled ? .checkmark : .none 216 | cell.backgroundColor = attributes.rowStyle.rowStyleValues.selectedColor 217 | } else { 218 | cell.accessoryType = .none 219 | } 220 | } else { 221 | if indexPath.row == selectedIndex { 222 | cell.textLabel?.textColor = attributes.textStyle.textStyleValues.selectedColor 223 | cell.accessoryType = attributes.accessory.isEnabled ? .checkmark : .none 224 | cell.backgroundColor = attributes.rowStyle.rowStyleValues.selectedColor 225 | } else { 226 | cell.accessoryType = .none 227 | } 228 | } 229 | 230 | 231 | return cell 232 | } 233 | } 234 | 235 | // MARK: - UITableViewDelegate Functions 236 | 237 | extension SwiftyMenu: UITableViewDelegate { 238 | public func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { 239 | return CGFloat(attributes.rowStyle.rowStyleValues.height) 240 | } 241 | 242 | public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 243 | 244 | if attributes.multiSelect.isEnabled { 245 | if selectedIndecis[indexPath.row] != nil { 246 | let deselectedText = self.items[selectedIndecis[indexPath.row]!] 247 | delegate?.swiftyMenu(self, didDeselectItem: deselectedText, atIndex: indexPath.row) 248 | self.didDeselectItem(self, deselectedText, indexPath.row) 249 | selectedIndecis[indexPath.row] = nil 250 | setSelectedOptionsAsTitle() 251 | tableView.reloadData() 252 | if attributes.hideOptionsWhenSelect.isEnabled { 253 | handleMenuState() 254 | } 255 | } else { 256 | //clean arrow color for error 257 | if hasError && selectedIndecis.isEmpty{ 258 | hasError = false 259 | changeTintColorArrow(hasError: hasError) 260 | } 261 | selectedIndecis[indexPath.row] = indexPath.row 262 | setSelectedOptionsAsTitle() 263 | let selectedText = self.items[selectedIndecis[indexPath.row]!] 264 | delegate?.swiftyMenu(self, didSelectItem: selectedText, atIndex: indexPath.row) 265 | self.didSelectItem(self, selectedText, indexPath.row) 266 | tableView.reloadData() 267 | if attributes.hideOptionsWhenSelect.isEnabled { 268 | handleMenuState() 269 | } 270 | } 271 | } else { 272 | if selectedIndex == indexPath.row { 273 | switch attributes.multiSelect{ 274 | case .disabled(let allowSingleDeselection): 275 | if allowSingleDeselection{ 276 | let deselectedText = self.items[self.selectedIndex!] 277 | delegate?.swiftyMenu(self, didDeselectItem: deselectedText, atIndex: indexPath.row) 278 | self.didDeselectItem(self, deselectedText, indexPath.row) 279 | selectedIndex = nil 280 | setSelectedOptionsAsTitle() 281 | tableView.reloadData() 282 | } 283 | default: 284 | break 285 | } 286 | 287 | if attributes.hideOptionsWhenSelect.isEnabled { 288 | handleMenuState() 289 | } 290 | } else { 291 | //clean arrow color for error 292 | if hasError && selectedIndex == nil{ 293 | hasError = false 294 | changeTintColorArrow(hasError: hasError) 295 | } 296 | selectedIndex = indexPath.row 297 | setSelectedOptionsAsTitle() 298 | let selectedText = self.items[self.selectedIndex!] 299 | delegate?.swiftyMenu(self, didSelectItem: selectedText, atIndex: indexPath.row) 300 | self.didSelectItem(self, selectedText, indexPath.row) 301 | tableView.reloadData() 302 | if attributes.hideOptionsWhenSelect.isEnabled { 303 | handleMenuState() 304 | } 305 | } 306 | } 307 | } 308 | } 309 | 310 | // MARK: - Setup SwiftyMenu Views Functions 311 | 312 | extension SwiftyMenu { 313 | private func setupUI () { 314 | setupView() 315 | getViewWidth() 316 | getViewHeight() 317 | setupSelectButton() 318 | setupDataTableView() 319 | setupSeparatorStyle() 320 | } 321 | 322 | public func setupSeparatorStyle() { 323 | itemsTableView.separatorStyle = attributes.separatorStyle.separatorStyleValues.style 324 | if attributes.separatorStyle.separatorStyleValues.isBlured { 325 | itemsTableView.separatorEffect = UIBlurEffect() 326 | } 327 | itemsTableView.separatorColor = attributes.separatorStyle.separatorStyleValues.color 328 | } 329 | 330 | private func isContentRightToLeft() -> Bool{ 331 | return UIView.userInterfaceLayoutDirection(for: selectButton.semanticContentAttribute) == .rightToLeft 332 | } 333 | 334 | private func setupView() { 335 | clipsToBounds = true 336 | layer.cornerRadius = attributes.roundCorners.cornerValue ?? 0 337 | layer.borderWidth = attributes.border.borderValues?.width ?? 0 338 | layer.borderColor = attributes.border.borderValues?.color.cgColor 339 | } 340 | 341 | private func setupSelectButton() { 342 | self.addSubview(selectButton) 343 | 344 | selectButton.snp.makeConstraints { maker in 345 | maker.leading.trailing.top.equalTo(self) 346 | maker.height.equalTo(height) 347 | } 348 | 349 | let color = attributes.placeHolderStyle.placeHolderValues.textColor 350 | selectButton.setTitleColor(color, for: .normal) 351 | UIView.performWithoutAnimation { 352 | selectButton.setTitle(attributes.placeHolderStyle.placeHolderValues.text, for: .normal) 353 | selectButton.layoutIfNeeded() 354 | } 355 | 356 | let isContentRightToLeft = isContentRightToLeft() 357 | 358 | let lineBreakMode: NSLineBreakMode = isContentRightToLeft ? .byTruncatingHead : .byTruncatingTail 359 | let isArrowEnable: Bool = attributes.arrowStyle.arrowStyleValues.isEnabled 360 | let arrow = isArrowEnable ? attributes.arrowStyle.arrowStyleValues.image : nil 361 | let leftPadding = isContentRightToLeft ? attributes.titleMarginHorizontal.trailingValue : attributes.titleMarginHorizontal.leadingValue 362 | let rightPadding = isContentRightToLeft ? attributes.titleMarginHorizontal.leadingValue : attributes.titleMarginHorizontal.trailingValue 363 | let font = self.attributes.textStyle.textStyleValues.font 364 | let placeholderTextColor = attributes.placeHolderStyle.placeHolderValues.textColor 365 | let spacingBetweenText = isArrowEnable ? attributes.arrowStyle.arrowStyleValues.spacingBetweenText : 0.0 366 | 367 | selectButton.backgroundColor = attributes.headerStyle.headerStyleValues.backgroundColor 368 | 369 | 370 | selectButton.contentEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) 371 | 372 | if #available(iOS 15.0, *) { 373 | var btnConfig = UIButton.Configuration.plain() 374 | btnConfig.titleAlignment = .leading 375 | btnConfig.imagePlacement = .trailing 376 | btnConfig.image = arrow 377 | btnConfig.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: leftPadding, bottom: 0, trailing: rightPadding - 2) 378 | btnConfig.imagePadding = spacingBetweenText //spacing between image and icon 379 | btnConfig.image = arrow 380 | btnConfig.titleLineBreakMode = lineBreakMode 381 | btnConfig.titleTextAttributesTransformer = UIConfigurationTextAttributesTransformer { incoming in 382 | var outgoing = incoming 383 | outgoing.font = font 384 | return outgoing 385 | } 386 | selectButton.configuration = btnConfig 387 | selectButton.translatesAutoresizingMaskIntoConstraints = false 388 | selectButton.contentMode = .scaleAspectFit 389 | selectButton.contentHorizontalAlignment = (isArrowEnable ? .fill : .leading) 390 | selectButton.clipsToBounds = true 391 | selectButton.setTitleColor(color, for: .normal) 392 | }else{ 393 | if #available(iOS 11.0, *) { 394 | selectButton.contentHorizontalAlignment = .leading 395 | } else { 396 | selectButton.contentHorizontalAlignment = .left 397 | } 398 | 399 | selectButton.titleEdgeInsets = UIEdgeInsets(top: 0, left: leftPadding, bottom: 0, right: rightPadding) 400 | selectButton.titleLabel?.lineBreakMode = lineBreakMode 401 | selectButton.titleLabel?.font = font 402 | selectButton.setTitleColor(placeholderTextColor, for: .normal) 403 | 404 | self.selectButton?.imageView?.contentMode = .scaleAspectFit 405 | 406 | self.selectButton.setImage(arrow, for: .normal) 407 | 408 | let imageWidth: CGFloat = isArrowEnable ? (arrow?.size.width ?? .zero) : 0 409 | 410 | 411 | var adjustImageEdgeInsets: UIEdgeInsets = .zero 412 | var adjustTitleEdgeInsets: UIEdgeInsets = .zero 413 | 414 | let arrowRight = rightPadding + imageWidth 415 | 416 | if isContentRightToLeft { 417 | adjustImageEdgeInsets.left = leftPadding 418 | adjustImageEdgeInsets.right = width - arrowRight - spacingBetweenText 419 | 420 | adjustTitleEdgeInsets.left = arrowRight + spacingBetweenText 421 | adjustTitleEdgeInsets.right = -imageWidth + rightPadding 422 | }else{ 423 | adjustImageEdgeInsets.left = width - arrowRight 424 | adjustImageEdgeInsets.right = rightPadding 425 | 426 | adjustTitleEdgeInsets.left = -imageWidth + leftPadding 427 | adjustTitleEdgeInsets.right = arrowRight + spacingBetweenText 428 | } 429 | 430 | 431 | self.selectButton.imageEdgeInsets = adjustImageEdgeInsets 432 | self.selectButton.titleEdgeInsets = adjustTitleEdgeInsets 433 | 434 | } 435 | 436 | selectButton.contentHorizontalAlignment = attributes.headerStyle.headerStyleValues.contentHorizontalAlignment 437 | selectButton.addTarget(self, action: #selector(handleMenuState), for: .touchUpInside) 438 | } 439 | 440 | private func setupDataTableView() { 441 | self.addSubview(itemsTableView) 442 | 443 | itemsTableView.snp.makeConstraints { maker in 444 | maker.leading.trailing.bottom.equalTo(self) 445 | maker.top.equalTo(selectButton.snp.bottom) 446 | } 447 | 448 | itemsTableView.delegate = self 449 | itemsTableView.dataSource = self 450 | itemsTableView.rowHeight = CGFloat(attributes.rowStyle.rowStyleValues.height) 451 | itemsTableView.separatorInset.left = 0 452 | itemsTableView.separatorInset.right = 0 453 | itemsTableView.backgroundColor = attributes.rowStyle.rowStyleValues.backgroundColor 454 | itemsTableView.isScrollEnabled = attributes.scroll.isEnabled 455 | itemsTableView.register(SwiftyMenuCell.self, forCellReuseIdentifier: "OptionCell") 456 | itemsTableView.showsVerticalScrollIndicator = false 457 | } 458 | 459 | @objc private func handleMenuState() { 460 | switch self.state { 461 | case .shown: 462 | collapseSwiftyMenu() 463 | case .hidden: 464 | expandSwiftyMenu() 465 | } 466 | state.toggle() 467 | } 468 | } 469 | 470 | // MARK: - Private Functions 471 | 472 | extension SwiftyMenu { 473 | private func getViewWidth() { 474 | width = self.frame.width 475 | } 476 | 477 | private func getViewHeight() { 478 | height = self.frame.height 479 | } 480 | 481 | private func setMultiSelectedOptions() { 482 | let sortedSelectedIndecis = selectedIndecis.sorted { $0.key < $1.key } 483 | let titles = sortedSelectedIndecis.map { (index, _) -> String in 484 | return items[index].displayableValue 485 | } 486 | var selectedTitle = "" 487 | selectedTitle = titles.joined(separator: separatorCharacters ?? ", ") 488 | UIView.performWithoutAnimation { 489 | selectButton.setTitle(selectedTitle, for: .normal) 490 | selectButton.layoutIfNeeded() 491 | } 492 | selectButton.setTitleColor(attributes.textStyle.textStyleValues.color, for: .normal) 493 | } 494 | 495 | private func setSingleSelectedOption() { 496 | UIView.performWithoutAnimation { 497 | selectButton.setTitle(items[selectedIndex!].displayableValue, for: .normal) 498 | selectButton.layoutIfNeeded() 499 | } 500 | selectButton.setTitleColor(attributes.textStyle.textStyleValues.color, for: .normal) 501 | } 502 | 503 | private func setPlaceholder() { 504 | UIView.performWithoutAnimation { 505 | selectButton.setTitle(attributes.placeHolderStyle.placeHolderValues.text, for: .normal) 506 | selectButton.layoutIfNeeded() 507 | } 508 | selectButton.setTitleColor(attributes.placeHolderStyle.placeHolderValues.textColor, for: .normal) 509 | } 510 | 511 | private func setSelectedOptionsAsTitle() { 512 | if attributes.multiSelect.isEnabled { 513 | if selectedIndecis.isEmpty { 514 | setPlaceholder() 515 | } else { 516 | setMultiSelectedOptions() 517 | } 518 | } else { 519 | if selectedIndex == nil { 520 | setPlaceholder() 521 | } else { 522 | setSingleSelectedOption() 523 | } 524 | } 525 | } 526 | } 527 | 528 | // MARK: - SwiftyMenu Expand and Collapse Functions 529 | 530 | extension SwiftyMenu { 531 | /// Called to Expand `SwiftyMenu`. 532 | private func expandSwiftyMenu() { 533 | delegate?.swiftyMenu(willExpand: self) 534 | self.willExpand() 535 | heightConstraint.constant = attributes.height.listHeightValue == 0 || !attributes.scroll.isEnabled || (CGFloat(Double(attributes.rowStyle.rowStyleValues.height) * Double(items.count + 1)) < CGFloat(attributes.height.listHeightValue)) ? CGFloat(Double(attributes.rowStyle.rowStyleValues.height) * Double(items.count + 1)) : CGFloat(attributes.height.listHeightValue) 536 | 537 | switch attributes.expandingAnimation { 538 | case .linear: 539 | UIView.animate(withDuration: attributes.expandingTiming.animationTimingValues.duration, 540 | delay: attributes.expandingTiming.animationTimingValues.delay, 541 | animations: animationBlock, 542 | completion: expandingAnimationCompletionBlock) 543 | 544 | case .spring(level: let powerLevel): 545 | let damping = CGFloat(0.5 / powerLevel.rawValue) 546 | let initialVelocity = CGFloat(0.5 * powerLevel.rawValue) 547 | 548 | UIView.animate(withDuration: attributes.expandingTiming.animationTimingValues.duration, 549 | delay: attributes.expandingTiming.animationTimingValues.delay, 550 | usingSpringWithDamping: damping, 551 | initialSpringVelocity: initialVelocity, 552 | options: [], 553 | animations: animationBlock, 554 | completion: expandingAnimationCompletionBlock) 555 | } 556 | } 557 | 558 | /// Called to Collapse `SwiftyMenu`. 559 | private func collapseSwiftyMenu() { 560 | delegate?.swiftyMenu(willCollapse: self) 561 | self.willCollapse() 562 | heightConstraint.constant = CGFloat(attributes.headerStyle.headerStyleValues.height) 563 | 564 | switch attributes.collapsingAnimation { 565 | case .linear: 566 | UIView.animate(withDuration: attributes.collapsingTiming.animationTimingValues.duration, 567 | delay: attributes.collapsingTiming.animationTimingValues.delay, 568 | animations: animationBlock, 569 | completion: collapsingAnimationCompletionBlock) 570 | 571 | case .spring(level: let powerLevel): 572 | let damping = CGFloat(1.0 * powerLevel.rawValue) 573 | let initialVelocity = CGFloat(10.0 * powerLevel.rawValue) 574 | 575 | UIView.animate(withDuration: attributes.collapsingTiming.animationTimingValues.duration, 576 | delay: attributes.collapsingTiming.animationTimingValues.delay, 577 | usingSpringWithDamping: damping, 578 | initialSpringVelocity: initialVelocity, 579 | options: .curveEaseIn, 580 | animations: animationBlock, 581 | completion: collapsingAnimationCompletionBlock) 582 | } 583 | } 584 | } 585 | 586 | // MARK: - SwiftyMenu Animation Functions 587 | 588 | extension SwiftyMenu { 589 | private func animationBlock() { 590 | if attributes.arrowStyle.arrowStyleValues.isEnabled { 591 | self.selectButton.imageView?.transform = self.selectButton.imageView!.transform.rotated(by: CGFloat.pi) 592 | } 593 | self.parentViewController?.view.layoutIfNeeded() 594 | } 595 | 596 | private func expandingAnimationCompletionBlock(didAppeared: Bool) { 597 | if didAppeared { 598 | self.delegate?.swiftyMenu(didExpand: self) 599 | self.didExpand() 600 | } 601 | } 602 | 603 | private func collapsingAnimationCompletionBlock(didAppeared: Bool) { 604 | if didAppeared { 605 | self.delegate?.swiftyMenu(didCollapse: self) 606 | self.didCollapse() 607 | } 608 | } 609 | } 610 | -------------------------------------------------------------------------------- /Example/SwiftyMenu.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 4B8AD907BB006589A259B86C /* Pods_SwiftyMenu_Tests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5554E47DC28F7043A9445538 /* Pods_SwiftyMenu_Tests.framework */; }; 11 | 607FACD61AFB9204008FA782 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 607FACD51AFB9204008FA782 /* AppDelegate.swift */; }; 12 | 607FACD81AFB9204008FA782 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 607FACD71AFB9204008FA782 /* ViewController.swift */; }; 13 | 607FACDB1AFB9204008FA782 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 607FACD91AFB9204008FA782 /* Main.storyboard */; }; 14 | 607FACDD1AFB9204008FA782 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 607FACDC1AFB9204008FA782 /* Images.xcassets */; }; 15 | 607FACE01AFB9204008FA782 /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 607FACDE1AFB9204008FA782 /* LaunchScreen.xib */; }; 16 | B7E5D6153C7C62E389FF49F9 /* Pods_SwiftyMenu_Example.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DD0DD0B626C9414217DC8F17 /* Pods_SwiftyMenu_Example.framework */; }; 17 | BC4F88DD22802E050056082A /* CHANGELOG.md in Resources */ = {isa = PBXBuildFile; fileRef = BC4F88DC22802E050056082A /* CHANGELOG.md */; }; 18 | BC7A3004242BF8BD00B76276 /* SwiftyMenuTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC7A3003242BF8BD00B76276 /* SwiftyMenuTests.swift */; }; 19 | BC7A3009242BF9B900B76276 /* SwiftyMenuDelegateSpy.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC7A3007242BF9B400B76276 /* SwiftyMenuDelegateSpy.swift */; }; 20 | BC8E4C6A2690AD4700029145 /* String+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC8E4C692690AD4600029145 /* String+Extensions.swift */; }; 21 | BC8E4C6C2690AD7C00029145 /* MealSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC8E4C6B2690AD7C00029145 /* MealSize.swift */; }; 22 | /* End PBXBuildFile section */ 23 | 24 | /* Begin PBXContainerItemProxy section */ 25 | 607FACE61AFB9204008FA782 /* PBXContainerItemProxy */ = { 26 | isa = PBXContainerItemProxy; 27 | containerPortal = 607FACC81AFB9204008FA782 /* Project object */; 28 | proxyType = 1; 29 | remoteGlobalIDString = 607FACCF1AFB9204008FA782; 30 | remoteInfo = SwiftyMenu; 31 | }; 32 | /* End PBXContainerItemProxy section */ 33 | 34 | /* Begin PBXFileReference section */ 35 | 1C898DFC3115A1762BA35E9F /* Pods-SwiftyMenu_Tests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SwiftyMenu_Tests.debug.xcconfig"; path = "Target Support Files/Pods-SwiftyMenu_Tests/Pods-SwiftyMenu_Tests.debug.xcconfig"; sourceTree = ""; }; 36 | 3E32D178976FCFD2A66D351A /* SwiftyMenu.podspec */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; name = SwiftyMenu.podspec; path = ../SwiftyMenu.podspec; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.ruby; }; 37 | 4D040975F688722B5C186CF7 /* LICENSE */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; name = LICENSE; path = ../LICENSE; sourceTree = ""; }; 38 | 5554E47DC28F7043A9445538 /* Pods_SwiftyMenu_Tests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_SwiftyMenu_Tests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 39 | 607FACD01AFB9204008FA782 /* SwiftyMenu_Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SwiftyMenu_Example.app; sourceTree = BUILT_PRODUCTS_DIR; }; 40 | 607FACD41AFB9204008FA782 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 41 | 607FACD51AFB9204008FA782 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 42 | 607FACD71AFB9204008FA782 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 43 | 607FACDA1AFB9204008FA782 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 44 | 607FACDC1AFB9204008FA782 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; 45 | 607FACDF1AFB9204008FA782 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/LaunchScreen.xib; sourceTree = ""; }; 46 | 607FACE51AFB9204008FA782 /* SwiftyMenu_Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SwiftyMenu_Tests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 47 | 607FACEA1AFB9204008FA782 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 48 | 62EA56F689738FB56657BC80 /* Pods-SwiftyMenu_Example.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SwiftyMenu_Example.debug.xcconfig"; path = "Target Support Files/Pods-SwiftyMenu_Example/Pods-SwiftyMenu_Example.debug.xcconfig"; sourceTree = ""; }; 49 | 7A003F545CFB45340C03F42C /* README.md */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = net.daringfireball.markdown; name = README.md; path = ../README.md; sourceTree = ""; }; 50 | 808EB17321FD09CF60EA98B2 /* Pods-SwiftyMenu_Tests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SwiftyMenu_Tests.release.xcconfig"; path = "Target Support Files/Pods-SwiftyMenu_Tests/Pods-SwiftyMenu_Tests.release.xcconfig"; sourceTree = ""; }; 51 | 97D36CF7E7BAEC32980EFB93 /* Pods-SwiftyMenu_Example.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SwiftyMenu_Example.release.xcconfig"; path = "Target Support Files/Pods-SwiftyMenu_Example/Pods-SwiftyMenu_Example.release.xcconfig"; sourceTree = ""; }; 52 | BC4F88DC22802E050056082A /* CHANGELOG.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; name = CHANGELOG.md; path = ../CHANGELOG.md; sourceTree = ""; }; 53 | BC7A3003242BF8BD00B76276 /* SwiftyMenuTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftyMenuTests.swift; sourceTree = ""; }; 54 | BC7A3007242BF9B400B76276 /* SwiftyMenuDelegateSpy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftyMenuDelegateSpy.swift; sourceTree = ""; }; 55 | BC8E4C692690AD4600029145 /* String+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Extensions.swift"; sourceTree = ""; }; 56 | BC8E4C6B2690AD7C00029145 /* MealSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MealSize.swift; sourceTree = ""; }; 57 | DD0DD0B626C9414217DC8F17 /* Pods_SwiftyMenu_Example.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_SwiftyMenu_Example.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 58 | /* End PBXFileReference section */ 59 | 60 | /* Begin PBXFrameworksBuildPhase section */ 61 | 607FACCD1AFB9204008FA782 /* Frameworks */ = { 62 | isa = PBXFrameworksBuildPhase; 63 | buildActionMask = 2147483647; 64 | files = ( 65 | B7E5D6153C7C62E389FF49F9 /* Pods_SwiftyMenu_Example.framework in Frameworks */, 66 | ); 67 | runOnlyForDeploymentPostprocessing = 0; 68 | }; 69 | 607FACE21AFB9204008FA782 /* Frameworks */ = { 70 | isa = PBXFrameworksBuildPhase; 71 | buildActionMask = 2147483647; 72 | files = ( 73 | 4B8AD907BB006589A259B86C /* Pods_SwiftyMenu_Tests.framework in Frameworks */, 74 | ); 75 | runOnlyForDeploymentPostprocessing = 0; 76 | }; 77 | /* End PBXFrameworksBuildPhase section */ 78 | 79 | /* Begin PBXGroup section */ 80 | 3180438BB32DAAD8409A8F19 /* Frameworks */ = { 81 | isa = PBXGroup; 82 | children = ( 83 | DD0DD0B626C9414217DC8F17 /* Pods_SwiftyMenu_Example.framework */, 84 | 5554E47DC28F7043A9445538 /* Pods_SwiftyMenu_Tests.framework */, 85 | ); 86 | name = Frameworks; 87 | sourceTree = ""; 88 | }; 89 | 425F0E06115A3B7EB0798B09 /* Pods */ = { 90 | isa = PBXGroup; 91 | children = ( 92 | 62EA56F689738FB56657BC80 /* Pods-SwiftyMenu_Example.debug.xcconfig */, 93 | 97D36CF7E7BAEC32980EFB93 /* Pods-SwiftyMenu_Example.release.xcconfig */, 94 | 1C898DFC3115A1762BA35E9F /* Pods-SwiftyMenu_Tests.debug.xcconfig */, 95 | 808EB17321FD09CF60EA98B2 /* Pods-SwiftyMenu_Tests.release.xcconfig */, 96 | ); 97 | path = Pods; 98 | sourceTree = ""; 99 | }; 100 | 607FACC71AFB9204008FA782 = { 101 | isa = PBXGroup; 102 | children = ( 103 | 607FACF51AFB993E008FA782 /* Podspec Metadata */, 104 | 607FACD21AFB9204008FA782 /* Example for SwiftyMenu */, 105 | 607FACE81AFB9204008FA782 /* Tests */, 106 | 607FACD11AFB9204008FA782 /* Products */, 107 | 425F0E06115A3B7EB0798B09 /* Pods */, 108 | 3180438BB32DAAD8409A8F19 /* Frameworks */, 109 | ); 110 | sourceTree = ""; 111 | }; 112 | 607FACD11AFB9204008FA782 /* Products */ = { 113 | isa = PBXGroup; 114 | children = ( 115 | 607FACD01AFB9204008FA782 /* SwiftyMenu_Example.app */, 116 | 607FACE51AFB9204008FA782 /* SwiftyMenu_Tests.xctest */, 117 | ); 118 | name = Products; 119 | sourceTree = ""; 120 | }; 121 | 607FACD21AFB9204008FA782 /* Example for SwiftyMenu */ = { 122 | isa = PBXGroup; 123 | children = ( 124 | BC8E4C682690AD3600029145 /* Extensions */, 125 | BC8E4C672690AD2800029145 /* Models */, 126 | 607FACD51AFB9204008FA782 /* AppDelegate.swift */, 127 | 607FACD71AFB9204008FA782 /* ViewController.swift */, 128 | 607FACD91AFB9204008FA782 /* Main.storyboard */, 129 | 607FACDC1AFB9204008FA782 /* Images.xcassets */, 130 | 607FACDE1AFB9204008FA782 /* LaunchScreen.xib */, 131 | 607FACD31AFB9204008FA782 /* Supporting Files */, 132 | ); 133 | name = "Example for SwiftyMenu"; 134 | path = SwiftyMenu; 135 | sourceTree = ""; 136 | }; 137 | 607FACD31AFB9204008FA782 /* Supporting Files */ = { 138 | isa = PBXGroup; 139 | children = ( 140 | 607FACD41AFB9204008FA782 /* Info.plist */, 141 | ); 142 | name = "Supporting Files"; 143 | sourceTree = ""; 144 | }; 145 | 607FACE81AFB9204008FA782 /* Tests */ = { 146 | isa = PBXGroup; 147 | children = ( 148 | BC7A3006242BF9AA00B76276 /* Spies */, 149 | 607FACE91AFB9204008FA782 /* Supporting Files */, 150 | BC7A3003242BF8BD00B76276 /* SwiftyMenuTests.swift */, 151 | ); 152 | path = Tests; 153 | sourceTree = ""; 154 | }; 155 | 607FACE91AFB9204008FA782 /* Supporting Files */ = { 156 | isa = PBXGroup; 157 | children = ( 158 | 607FACEA1AFB9204008FA782 /* Info.plist */, 159 | ); 160 | name = "Supporting Files"; 161 | sourceTree = ""; 162 | }; 163 | 607FACF51AFB993E008FA782 /* Podspec Metadata */ = { 164 | isa = PBXGroup; 165 | children = ( 166 | BC4F88DC22802E050056082A /* CHANGELOG.md */, 167 | 3E32D178976FCFD2A66D351A /* SwiftyMenu.podspec */, 168 | 7A003F545CFB45340C03F42C /* README.md */, 169 | 4D040975F688722B5C186CF7 /* LICENSE */, 170 | ); 171 | name = "Podspec Metadata"; 172 | sourceTree = ""; 173 | }; 174 | BC7A3006242BF9AA00B76276 /* Spies */ = { 175 | isa = PBXGroup; 176 | children = ( 177 | BC7A3007242BF9B400B76276 /* SwiftyMenuDelegateSpy.swift */, 178 | ); 179 | name = Spies; 180 | sourceTree = ""; 181 | }; 182 | BC8E4C672690AD2800029145 /* Models */ = { 183 | isa = PBXGroup; 184 | children = ( 185 | BC8E4C6B2690AD7C00029145 /* MealSize.swift */, 186 | ); 187 | name = Models; 188 | sourceTree = ""; 189 | }; 190 | BC8E4C682690AD3600029145 /* Extensions */ = { 191 | isa = PBXGroup; 192 | children = ( 193 | BC8E4C692690AD4600029145 /* String+Extensions.swift */, 194 | ); 195 | name = Extensions; 196 | sourceTree = ""; 197 | }; 198 | /* End PBXGroup section */ 199 | 200 | /* Begin PBXNativeTarget section */ 201 | 607FACCF1AFB9204008FA782 /* SwiftyMenu_Example */ = { 202 | isa = PBXNativeTarget; 203 | buildConfigurationList = 607FACEF1AFB9204008FA782 /* Build configuration list for PBXNativeTarget "SwiftyMenu_Example" */; 204 | buildPhases = ( 205 | 04AD95354171211AE70D72D8 /* [CP] Check Pods Manifest.lock */, 206 | 607FACCC1AFB9204008FA782 /* Sources */, 207 | 607FACCD1AFB9204008FA782 /* Frameworks */, 208 | 607FACCE1AFB9204008FA782 /* Resources */, 209 | 3736782BD9248E9D776F5581 /* [CP] Embed Pods Frameworks */, 210 | ); 211 | buildRules = ( 212 | ); 213 | dependencies = ( 214 | ); 215 | name = SwiftyMenu_Example; 216 | productName = SwiftyMenu; 217 | productReference = 607FACD01AFB9204008FA782 /* SwiftyMenu_Example.app */; 218 | productType = "com.apple.product-type.application"; 219 | }; 220 | 607FACE41AFB9204008FA782 /* SwiftyMenu_Tests */ = { 221 | isa = PBXNativeTarget; 222 | buildConfigurationList = 607FACF21AFB9204008FA782 /* Build configuration list for PBXNativeTarget "SwiftyMenu_Tests" */; 223 | buildPhases = ( 224 | B0B8349DDFC68D696EEFDFEB /* [CP] Check Pods Manifest.lock */, 225 | 607FACE11AFB9204008FA782 /* Sources */, 226 | 607FACE21AFB9204008FA782 /* Frameworks */, 227 | 607FACE31AFB9204008FA782 /* Resources */, 228 | ); 229 | buildRules = ( 230 | ); 231 | dependencies = ( 232 | 607FACE71AFB9204008FA782 /* PBXTargetDependency */, 233 | ); 234 | name = SwiftyMenu_Tests; 235 | productName = Tests; 236 | productReference = 607FACE51AFB9204008FA782 /* SwiftyMenu_Tests.xctest */; 237 | productType = "com.apple.product-type.bundle.unit-test"; 238 | }; 239 | /* End PBXNativeTarget section */ 240 | 241 | /* Begin PBXProject section */ 242 | 607FACC81AFB9204008FA782 /* Project object */ = { 243 | isa = PBXProject; 244 | attributes = { 245 | DefaultBuildSystemTypeForWorkspace = Original; 246 | LastSwiftUpdateCheck = 1130; 247 | LastUpgradeCheck = 1320; 248 | ORGANIZATIONNAME = CocoaPods; 249 | TargetAttributes = { 250 | 607FACCF1AFB9204008FA782 = { 251 | CreatedOnToolsVersion = 6.3.1; 252 | DevelopmentTeam = 89T7S3AF6B; 253 | LastSwiftMigration = 1020; 254 | }; 255 | 607FACE41AFB9204008FA782 = { 256 | CreatedOnToolsVersion = 6.3.1; 257 | DevelopmentTeam = 89T7S3AF6B; 258 | LastSwiftMigration = 1020; 259 | TestTargetID = 607FACCF1AFB9204008FA782; 260 | }; 261 | }; 262 | }; 263 | buildConfigurationList = 607FACCB1AFB9204008FA782 /* Build configuration list for PBXProject "SwiftyMenu" */; 264 | compatibilityVersion = "Xcode 3.2"; 265 | developmentRegion = en; 266 | hasScannedForEncodings = 0; 267 | knownRegions = ( 268 | en, 269 | Base, 270 | ); 271 | mainGroup = 607FACC71AFB9204008FA782; 272 | productRefGroup = 607FACD11AFB9204008FA782 /* Products */; 273 | projectDirPath = ""; 274 | projectRoot = ""; 275 | targets = ( 276 | 607FACCF1AFB9204008FA782 /* SwiftyMenu_Example */, 277 | 607FACE41AFB9204008FA782 /* SwiftyMenu_Tests */, 278 | ); 279 | }; 280 | /* End PBXProject section */ 281 | 282 | /* Begin PBXResourcesBuildPhase section */ 283 | 607FACCE1AFB9204008FA782 /* Resources */ = { 284 | isa = PBXResourcesBuildPhase; 285 | buildActionMask = 2147483647; 286 | files = ( 287 | 607FACDB1AFB9204008FA782 /* Main.storyboard in Resources */, 288 | 607FACE01AFB9204008FA782 /* LaunchScreen.xib in Resources */, 289 | 607FACDD1AFB9204008FA782 /* Images.xcassets in Resources */, 290 | BC4F88DD22802E050056082A /* CHANGELOG.md in Resources */, 291 | ); 292 | runOnlyForDeploymentPostprocessing = 0; 293 | }; 294 | 607FACE31AFB9204008FA782 /* Resources */ = { 295 | isa = PBXResourcesBuildPhase; 296 | buildActionMask = 2147483647; 297 | files = ( 298 | ); 299 | runOnlyForDeploymentPostprocessing = 0; 300 | }; 301 | /* End PBXResourcesBuildPhase section */ 302 | 303 | /* Begin PBXShellScriptBuildPhase section */ 304 | 04AD95354171211AE70D72D8 /* [CP] Check Pods Manifest.lock */ = { 305 | isa = PBXShellScriptBuildPhase; 306 | buildActionMask = 2147483647; 307 | files = ( 308 | ); 309 | inputFileListPaths = ( 310 | ); 311 | inputPaths = ( 312 | "${PODS_PODFILE_DIR_PATH}/Podfile.lock", 313 | "${PODS_ROOT}/Manifest.lock", 314 | ); 315 | name = "[CP] Check Pods Manifest.lock"; 316 | outputFileListPaths = ( 317 | ); 318 | outputPaths = ( 319 | "$(DERIVED_FILE_DIR)/Pods-SwiftyMenu_Example-checkManifestLockResult.txt", 320 | ); 321 | runOnlyForDeploymentPostprocessing = 0; 322 | shellPath = /bin/sh; 323 | shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; 324 | showEnvVarsInLog = 0; 325 | }; 326 | 3736782BD9248E9D776F5581 /* [CP] Embed Pods Frameworks */ = { 327 | isa = PBXShellScriptBuildPhase; 328 | buildActionMask = 2147483647; 329 | files = ( 330 | ); 331 | inputPaths = ( 332 | "${PODS_ROOT}/Target Support Files/Pods-SwiftyMenu_Example/Pods-SwiftyMenu_Example-frameworks.sh", 333 | "${BUILT_PRODUCTS_DIR}/SnapKit/SnapKit.framework", 334 | "${BUILT_PRODUCTS_DIR}/SwiftyMenu/SwiftyMenu.framework", 335 | ); 336 | name = "[CP] Embed Pods Frameworks"; 337 | outputPaths = ( 338 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SnapKit.framework", 339 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SwiftyMenu.framework", 340 | ); 341 | runOnlyForDeploymentPostprocessing = 0; 342 | shellPath = /bin/sh; 343 | shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-SwiftyMenu_Example/Pods-SwiftyMenu_Example-frameworks.sh\"\n"; 344 | showEnvVarsInLog = 0; 345 | }; 346 | B0B8349DDFC68D696EEFDFEB /* [CP] Check Pods Manifest.lock */ = { 347 | isa = PBXShellScriptBuildPhase; 348 | buildActionMask = 2147483647; 349 | files = ( 350 | ); 351 | inputFileListPaths = ( 352 | ); 353 | inputPaths = ( 354 | "${PODS_PODFILE_DIR_PATH}/Podfile.lock", 355 | "${PODS_ROOT}/Manifest.lock", 356 | ); 357 | name = "[CP] Check Pods Manifest.lock"; 358 | outputFileListPaths = ( 359 | ); 360 | outputPaths = ( 361 | "$(DERIVED_FILE_DIR)/Pods-SwiftyMenu_Tests-checkManifestLockResult.txt", 362 | ); 363 | runOnlyForDeploymentPostprocessing = 0; 364 | shellPath = /bin/sh; 365 | shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; 366 | showEnvVarsInLog = 0; 367 | }; 368 | /* End PBXShellScriptBuildPhase section */ 369 | 370 | /* Begin PBXSourcesBuildPhase section */ 371 | 607FACCC1AFB9204008FA782 /* Sources */ = { 372 | isa = PBXSourcesBuildPhase; 373 | buildActionMask = 2147483647; 374 | files = ( 375 | 607FACD81AFB9204008FA782 /* ViewController.swift in Sources */, 376 | BC8E4C6A2690AD4700029145 /* String+Extensions.swift in Sources */, 377 | 607FACD61AFB9204008FA782 /* AppDelegate.swift in Sources */, 378 | BC8E4C6C2690AD7C00029145 /* MealSize.swift in Sources */, 379 | ); 380 | runOnlyForDeploymentPostprocessing = 0; 381 | }; 382 | 607FACE11AFB9204008FA782 /* Sources */ = { 383 | isa = PBXSourcesBuildPhase; 384 | buildActionMask = 2147483647; 385 | files = ( 386 | BC7A3009242BF9B900B76276 /* SwiftyMenuDelegateSpy.swift in Sources */, 387 | BC7A3004242BF8BD00B76276 /* SwiftyMenuTests.swift in Sources */, 388 | ); 389 | runOnlyForDeploymentPostprocessing = 0; 390 | }; 391 | /* End PBXSourcesBuildPhase section */ 392 | 393 | /* Begin PBXTargetDependency section */ 394 | 607FACE71AFB9204008FA782 /* PBXTargetDependency */ = { 395 | isa = PBXTargetDependency; 396 | target = 607FACCF1AFB9204008FA782 /* SwiftyMenu_Example */; 397 | targetProxy = 607FACE61AFB9204008FA782 /* PBXContainerItemProxy */; 398 | }; 399 | /* End PBXTargetDependency section */ 400 | 401 | /* Begin PBXVariantGroup section */ 402 | 607FACD91AFB9204008FA782 /* Main.storyboard */ = { 403 | isa = PBXVariantGroup; 404 | children = ( 405 | 607FACDA1AFB9204008FA782 /* Base */, 406 | ); 407 | name = Main.storyboard; 408 | sourceTree = ""; 409 | }; 410 | 607FACDE1AFB9204008FA782 /* LaunchScreen.xib */ = { 411 | isa = PBXVariantGroup; 412 | children = ( 413 | 607FACDF1AFB9204008FA782 /* Base */, 414 | ); 415 | name = LaunchScreen.xib; 416 | sourceTree = ""; 417 | }; 418 | /* End PBXVariantGroup section */ 419 | 420 | /* Begin XCBuildConfiguration section */ 421 | 607FACED1AFB9204008FA782 /* Debug */ = { 422 | isa = XCBuildConfiguration; 423 | buildSettings = { 424 | ALWAYS_SEARCH_USER_PATHS = NO; 425 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 426 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 427 | CLANG_CXX_LIBRARY = "libc++"; 428 | CLANG_ENABLE_MODULES = YES; 429 | CLANG_ENABLE_OBJC_ARC = YES; 430 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 431 | CLANG_WARN_BOOL_CONVERSION = YES; 432 | CLANG_WARN_COMMA = YES; 433 | CLANG_WARN_CONSTANT_CONVERSION = YES; 434 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 435 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 436 | CLANG_WARN_EMPTY_BODY = YES; 437 | CLANG_WARN_ENUM_CONVERSION = YES; 438 | CLANG_WARN_INFINITE_RECURSION = YES; 439 | CLANG_WARN_INT_CONVERSION = YES; 440 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 441 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 442 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 443 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 444 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 445 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 446 | CLANG_WARN_STRICT_PROTOTYPES = YES; 447 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 448 | CLANG_WARN_UNREACHABLE_CODE = YES; 449 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 450 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 451 | COPY_PHASE_STRIP = NO; 452 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 453 | ENABLE_STRICT_OBJC_MSGSEND = YES; 454 | ENABLE_TESTABILITY = YES; 455 | GCC_C_LANGUAGE_STANDARD = gnu99; 456 | GCC_DYNAMIC_NO_PIC = NO; 457 | GCC_NO_COMMON_BLOCKS = YES; 458 | GCC_OPTIMIZATION_LEVEL = 0; 459 | GCC_PREPROCESSOR_DEFINITIONS = ( 460 | "DEBUG=1", 461 | "$(inherited)", 462 | ); 463 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 464 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 465 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 466 | GCC_WARN_UNDECLARED_SELECTOR = YES; 467 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 468 | GCC_WARN_UNUSED_FUNCTION = YES; 469 | GCC_WARN_UNUSED_VARIABLE = YES; 470 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 471 | MTL_ENABLE_DEBUG_INFO = YES; 472 | ONLY_ACTIVE_ARCH = YES; 473 | SDKROOT = iphoneos; 474 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 475 | }; 476 | name = Debug; 477 | }; 478 | 607FACEE1AFB9204008FA782 /* Release */ = { 479 | isa = XCBuildConfiguration; 480 | buildSettings = { 481 | ALWAYS_SEARCH_USER_PATHS = NO; 482 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 483 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 484 | CLANG_CXX_LIBRARY = "libc++"; 485 | CLANG_ENABLE_MODULES = YES; 486 | CLANG_ENABLE_OBJC_ARC = YES; 487 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 488 | CLANG_WARN_BOOL_CONVERSION = YES; 489 | CLANG_WARN_COMMA = YES; 490 | CLANG_WARN_CONSTANT_CONVERSION = YES; 491 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 492 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 493 | CLANG_WARN_EMPTY_BODY = YES; 494 | CLANG_WARN_ENUM_CONVERSION = YES; 495 | CLANG_WARN_INFINITE_RECURSION = YES; 496 | CLANG_WARN_INT_CONVERSION = YES; 497 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 498 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 499 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 500 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 501 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 502 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 503 | CLANG_WARN_STRICT_PROTOTYPES = YES; 504 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 505 | CLANG_WARN_UNREACHABLE_CODE = YES; 506 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 507 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 508 | COPY_PHASE_STRIP = NO; 509 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 510 | ENABLE_NS_ASSERTIONS = NO; 511 | ENABLE_STRICT_OBJC_MSGSEND = YES; 512 | GCC_C_LANGUAGE_STANDARD = gnu99; 513 | GCC_NO_COMMON_BLOCKS = YES; 514 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 515 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 516 | GCC_WARN_UNDECLARED_SELECTOR = YES; 517 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 518 | GCC_WARN_UNUSED_FUNCTION = YES; 519 | GCC_WARN_UNUSED_VARIABLE = YES; 520 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 521 | MTL_ENABLE_DEBUG_INFO = NO; 522 | SDKROOT = iphoneos; 523 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 524 | VALIDATE_PRODUCT = YES; 525 | }; 526 | name = Release; 527 | }; 528 | 607FACF01AFB9204008FA782 /* Debug */ = { 529 | isa = XCBuildConfiguration; 530 | baseConfigurationReference = 62EA56F689738FB56657BC80 /* Pods-SwiftyMenu_Example.debug.xcconfig */; 531 | buildSettings = { 532 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 533 | DEVELOPMENT_TEAM = 89T7S3AF6B; 534 | INFOPLIST_FILE = SwiftyMenu/Info.plist; 535 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 536 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 537 | MODULE_NAME = ExampleApp; 538 | PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.$(PRODUCT_NAME:rfc1034identifier)"; 539 | "PRODUCT_BUNDLE_IDENTIFIER[sdk=iphoneos*]" = "org.cocoapods.demo.SwiftyMenu-Example"; 540 | PRODUCT_NAME = "$(TARGET_NAME)"; 541 | SWIFT_VERSION = 5.0; 542 | }; 543 | name = Debug; 544 | }; 545 | 607FACF11AFB9204008FA782 /* Release */ = { 546 | isa = XCBuildConfiguration; 547 | baseConfigurationReference = 97D36CF7E7BAEC32980EFB93 /* Pods-SwiftyMenu_Example.release.xcconfig */; 548 | buildSettings = { 549 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 550 | DEVELOPMENT_TEAM = 89T7S3AF6B; 551 | INFOPLIST_FILE = SwiftyMenu/Info.plist; 552 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 553 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 554 | MODULE_NAME = ExampleApp; 555 | PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.$(PRODUCT_NAME:rfc1034identifier)"; 556 | "PRODUCT_BUNDLE_IDENTIFIER[sdk=iphoneos*]" = "org.cocoapods.demo.SwiftyMenu-Example"; 557 | PRODUCT_NAME = "$(TARGET_NAME)"; 558 | SWIFT_VERSION = 5.0; 559 | }; 560 | name = Release; 561 | }; 562 | 607FACF31AFB9204008FA782 /* Debug */ = { 563 | isa = XCBuildConfiguration; 564 | baseConfigurationReference = 1C898DFC3115A1762BA35E9F /* Pods-SwiftyMenu_Tests.debug.xcconfig */; 565 | buildSettings = { 566 | DEVELOPMENT_TEAM = 89T7S3AF6B; 567 | FRAMEWORK_SEARCH_PATHS = ( 568 | "$(SDKROOT)/Developer/Library/Frameworks", 569 | "$(inherited)", 570 | ); 571 | GCC_PREPROCESSOR_DEFINITIONS = ( 572 | "DEBUG=1", 573 | "$(inherited)", 574 | ); 575 | INFOPLIST_FILE = Tests/Info.plist; 576 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 577 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 578 | PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.$(PRODUCT_NAME:rfc1034identifier)"; 579 | PRODUCT_NAME = "$(TARGET_NAME)"; 580 | SWIFT_VERSION = 5.0; 581 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/SwiftyMenu_Example.app/SwiftyMenu_Example"; 582 | }; 583 | name = Debug; 584 | }; 585 | 607FACF41AFB9204008FA782 /* Release */ = { 586 | isa = XCBuildConfiguration; 587 | baseConfigurationReference = 808EB17321FD09CF60EA98B2 /* Pods-SwiftyMenu_Tests.release.xcconfig */; 588 | buildSettings = { 589 | DEVELOPMENT_TEAM = 89T7S3AF6B; 590 | FRAMEWORK_SEARCH_PATHS = ( 591 | "$(SDKROOT)/Developer/Library/Frameworks", 592 | "$(inherited)", 593 | ); 594 | INFOPLIST_FILE = Tests/Info.plist; 595 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 596 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 597 | PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.$(PRODUCT_NAME:rfc1034identifier)"; 598 | PRODUCT_NAME = "$(TARGET_NAME)"; 599 | SWIFT_VERSION = 5.0; 600 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/SwiftyMenu_Example.app/SwiftyMenu_Example"; 601 | }; 602 | name = Release; 603 | }; 604 | /* End XCBuildConfiguration section */ 605 | 606 | /* Begin XCConfigurationList section */ 607 | 607FACCB1AFB9204008FA782 /* Build configuration list for PBXProject "SwiftyMenu" */ = { 608 | isa = XCConfigurationList; 609 | buildConfigurations = ( 610 | 607FACED1AFB9204008FA782 /* Debug */, 611 | 607FACEE1AFB9204008FA782 /* Release */, 612 | ); 613 | defaultConfigurationIsVisible = 0; 614 | defaultConfigurationName = Release; 615 | }; 616 | 607FACEF1AFB9204008FA782 /* Build configuration list for PBXNativeTarget "SwiftyMenu_Example" */ = { 617 | isa = XCConfigurationList; 618 | buildConfigurations = ( 619 | 607FACF01AFB9204008FA782 /* Debug */, 620 | 607FACF11AFB9204008FA782 /* Release */, 621 | ); 622 | defaultConfigurationIsVisible = 0; 623 | defaultConfigurationName = Release; 624 | }; 625 | 607FACF21AFB9204008FA782 /* Build configuration list for PBXNativeTarget "SwiftyMenu_Tests" */ = { 626 | isa = XCConfigurationList; 627 | buildConfigurations = ( 628 | 607FACF31AFB9204008FA782 /* Debug */, 629 | 607FACF41AFB9204008FA782 /* Release */, 630 | ); 631 | defaultConfigurationIsVisible = 0; 632 | defaultConfigurationName = Release; 633 | }; 634 | /* End XCConfigurationList section */ 635 | }; 636 | rootObject = 607FACC81AFB9204008FA782 /* Project object */; 637 | } 638 | --------------------------------------------------------------------------------