├── Images ├── example1.gif ├── example2.gif ├── example3.gif ├── example4.png └── menu_with_icons.jpeg ├── Example ├── ContextMenuSwift │ ├── Images.xcassets │ │ ├── Contents.json │ │ ├── icons8-trash.imageset │ │ │ ├── icons8-trash.png │ │ │ ├── icons8-trash-1.png │ │ │ ├── icons8-trash-2.png │ │ │ └── Contents.json │ │ ├── icons8-upload.imageset │ │ │ ├── icons8-share.png │ │ │ ├── icons8-share-1.png │ │ │ ├── icons8-share-2.png │ │ │ └── Contents.json │ │ └── AppIcon.appiconset │ │ │ └── Contents.json │ ├── CustomCell.swift │ ├── Info.plist │ ├── AppDelegate.swift │ ├── CustomCell.xib │ ├── ViewController.swift │ └── Base.lproj │ │ ├── LaunchScreen.xib │ │ └── Main.storyboard └── ContextMenuSwift.xcodeproj │ ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist │ ├── xcshareddata │ └── xcschemes │ │ └── ContextMenuSwift-Example.xcscheme │ └── project.pbxproj ├── .gitignore ├── Package.swift ├── Tests └── ContextMenuSwiftTests │ └── ContextMenuSwiftTests.swift ├── .travis.yml ├── ContextMenuSwift.podspec ├── Sources └── ContextMenuSwift │ ├── ContextMenuCell.swift │ ├── ContextMenuTextCell.swift │ └── ContextMenu.swift └── README.md /Images/example1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umerjabbar/ContextMenuSwift/HEAD/Images/example1.gif -------------------------------------------------------------------------------- /Images/example2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umerjabbar/ContextMenuSwift/HEAD/Images/example2.gif -------------------------------------------------------------------------------- /Images/example3.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umerjabbar/ContextMenuSwift/HEAD/Images/example3.gif -------------------------------------------------------------------------------- /Images/example4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umerjabbar/ContextMenuSwift/HEAD/Images/example4.png -------------------------------------------------------------------------------- /Images/menu_with_icons.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umerjabbar/ContextMenuSwift/HEAD/Images/menu_with_icons.jpeg -------------------------------------------------------------------------------- /Example/ContextMenuSwift/Images.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 8 | -------------------------------------------------------------------------------- /Example/ContextMenuSwift/Images.xcassets/icons8-trash.imageset/icons8-trash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umerjabbar/ContextMenuSwift/HEAD/Example/ContextMenuSwift/Images.xcassets/icons8-trash.imageset/icons8-trash.png -------------------------------------------------------------------------------- /Example/ContextMenuSwift/Images.xcassets/icons8-upload.imageset/icons8-share.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umerjabbar/ContextMenuSwift/HEAD/Example/ContextMenuSwift/Images.xcassets/icons8-upload.imageset/icons8-share.png -------------------------------------------------------------------------------- /Example/ContextMenuSwift/Images.xcassets/icons8-trash.imageset/icons8-trash-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umerjabbar/ContextMenuSwift/HEAD/Example/ContextMenuSwift/Images.xcassets/icons8-trash.imageset/icons8-trash-1.png -------------------------------------------------------------------------------- /Example/ContextMenuSwift/Images.xcassets/icons8-trash.imageset/icons8-trash-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umerjabbar/ContextMenuSwift/HEAD/Example/ContextMenuSwift/Images.xcassets/icons8-trash.imageset/icons8-trash-2.png -------------------------------------------------------------------------------- /Example/ContextMenuSwift/Images.xcassets/icons8-upload.imageset/icons8-share-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umerjabbar/ContextMenuSwift/HEAD/Example/ContextMenuSwift/Images.xcassets/icons8-upload.imageset/icons8-share-1.png -------------------------------------------------------------------------------- /Example/ContextMenuSwift/Images.xcassets/icons8-upload.imageset/icons8-share-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umerjabbar/ContextMenuSwift/HEAD/Example/ContextMenuSwift/Images.xcassets/icons8-upload.imageset/icons8-share-2.png -------------------------------------------------------------------------------- /Example/ContextMenuSwift.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Example/ContextMenuSwift.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.6 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "ContextMenuSwift", 7 | platforms: [.iOS(.v10)], 8 | products: [ 9 | .library(name: "ContextMenuSwift", targets: ["ContextMenuSwift"]) 10 | ], 11 | dependencies: [], 12 | targets: [ 13 | .target( 14 | name: "ContextMenuSwift", 15 | dependencies: [] 16 | ) 17 | ] 18 | ) 19 | -------------------------------------------------------------------------------- /Tests/ContextMenuSwiftTests/ContextMenuSwiftTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import ContextMenuSwift 3 | 4 | final class ContextMenuSwiftTests: XCTestCase { 5 | func testExample() throws { 6 | // This is an example of a functional test case. 7 | // Use XCTAssert and related functions to verify your tests produce the correct 8 | // results. 9 | XCTAssertEqual(ContextMenuSwift().text, "Hello, World!") 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Example/ContextMenuSwift/Images.xcassets/icons8-trash.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "icons8-trash.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "icons8-trash-1.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "icons8-trash-2.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Example/ContextMenuSwift/Images.xcassets/icons8-upload.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "icons8-share.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "icons8-share-1.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "icons8-share-2.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # references: 2 | # * https://www.objc.io/issues/6-build-tools/travis-ci/ 3 | # * https://github.com/supermarin/xcpretty#usage 4 | 5 | osx_image: xcode7.3 6 | language: objective-c 7 | # cache: cocoapods 8 | # podfile: Example/Podfile 9 | # before_install: 10 | # - gem install cocoapods # Since Travis is not always on latest version 11 | # - pod install --project-directory=Example 12 | script: 13 | - set -o pipefail && xcodebuild test -enableCodeCoverage YES -workspace Example/ContextMenuSwift.xcworkspace -scheme ContextMenuSwift-Example -sdk iphonesimulator9.3 ONLY_ACTIVE_ARCH=NO | xcpretty 14 | - pod lib lint 15 | -------------------------------------------------------------------------------- /Example/ContextMenuSwift/CustomCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomCell.swift 3 | // ContextMenuSwiftDemo 4 | // 5 | // Created by Umer Jabbar on 13/06/2020. 6 | // Copyright © 2020 Umer jabbar. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import ContextMenuSwift 11 | 12 | class CustomCell: ContextMenuCell { 13 | 14 | var action: ((Bool) -> Void)? 15 | 16 | // override func setup(item: ContextMenuItem, style: ContextMenuConstants? = nil) { 17 | // super.setup(contentView, tableView: tableView, item: item) 18 | // } 19 | 20 | @IBAction func switchTapAction(_ sender: UISwitch) { 21 | self.action?(sender.isOn) 22 | 23 | print("asd") 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /Example/ContextMenuSwift/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 | -------------------------------------------------------------------------------- /Example/ContextMenuSwift/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 | UIInterfaceOrientationLandscapeLeft 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /ContextMenuSwift.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |spec| 2 | 3 | spec.name = "ContextMenuSwift" 4 | spec.version = "1.0.0" 5 | spec.summary = "A CocoaPods library written in Swift" 6 | 7 | spec.description = <<-DESC 8 | This CocoaPods library helps you with context menu for older ios versions. 9 | DESC 10 | 11 | spec.homepage = "https://github.com/umerjabbar/ContextMenuSwift" 12 | spec.license = { :type => 'Apache License, Version 2.0', :text => <<-LICENSE 13 | Licensed under the Apache License, Version 2.0 (the "License"); 14 | you may not use this file except in compliance with the License. 15 | You may obtain a copy of the License at 16 | 17 | http://www.apache.org/licenses/LICENSE-2.0 18 | 19 | Unless required by applicable law or agreed to in writing, software 20 | distributed under the License is distributed on an "AS IS" BASIS, 21 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 22 | See the License for the specific language governing permissions and 23 | limitations under the License. 24 | LICENSE 25 | } 26 | spec.author = { "Umer Jabbar" => "umerabduljabbar@icloud.com" } 27 | 28 | spec.ios.deployment_target = "10.0" 29 | spec.swift_versions = ["4.2", "5.0"] 30 | 31 | spec.source = { :git => "https://github.com/umerjabbar/ContextMenuSwift.git", :tag => "#{spec.version}" } 32 | spec.source_files = "Sources/ContextMenuSwift/**/*.{h,m,swift,xib}" 33 | 34 | end 35 | -------------------------------------------------------------------------------- /Sources/ContextMenuSwift/ContextMenuCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContextMenuTVC.swift 3 | // ContextMenuSwift 4 | // 5 | // Created by Umer Jabbar on 13/06/2020. 6 | // Copyright © 2020 Umer jabbar. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | open class ContextMenuCell: UITableViewCell { 12 | 13 | static let identifier = "ContextMenuCell" 14 | 15 | public weak var contextMenu: ContextMenu? 16 | public weak var tableView: UITableView? 17 | public var item: ContextMenuItem! 18 | public var style : ContextMenuConstants? = nil 19 | 20 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { 21 | super.init(style: style, reuseIdentifier: reuseIdentifier) 22 | commonInit() 23 | } 24 | 25 | required public init?(coder aDecoder: NSCoder) { 26 | super.init(coder: aDecoder) 27 | commonInit() 28 | } 29 | 30 | override open func awakeFromNib() { 31 | super.awakeFromNib() 32 | // Initialization code 33 | } 34 | 35 | override open func setSelected(_ selected: Bool, animated: Bool) { 36 | super.setSelected(selected, animated: animated) 37 | } 38 | 39 | override open func setHighlighted(_ highlighted: Bool, animated: Bool) { 40 | super.setHighlighted(highlighted, animated: animated) 41 | 42 | if highlighted { 43 | self.contentView.backgroundColor = UIColor.lightGray.withAlphaComponent(0.3) 44 | } else{ 45 | self.contentView.backgroundColor = .clear 46 | } 47 | } 48 | 49 | open func commonInit() { 50 | 51 | } 52 | 53 | open func setup(){ 54 | 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /Example/ContextMenuSwift/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // ContextMenuSwift 4 | // 5 | // Created by umerjabbar on 12/16/2021. 6 | // Copyright (c) 2021 umerjabbar. 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: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { 18 | // Override point for customization after application launch. 19 | return true 20 | } 21 | 22 | func applicationWillResignActive(_ application: UIApplication) { 23 | // 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. 24 | // 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. 25 | } 26 | 27 | func applicationDidEnterBackground(_ application: UIApplication) { 28 | // 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. 29 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 30 | } 31 | 32 | func applicationWillEnterForeground(_ application: UIApplication) { 33 | // 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. 34 | } 35 | 36 | func applicationDidBecomeActive(_ application: UIApplication) { 37 | // 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. 38 | } 39 | 40 | func applicationWillTerminate(_ application: UIApplication) { 41 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 42 | } 43 | 44 | 45 | } 46 | 47 | -------------------------------------------------------------------------------- /Sources/ContextMenuSwift/ContextMenuTextCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContextMenuTextCell.swift 3 | // 4 | // 5 | // Created by Umer on 31/05/2023. 6 | // 7 | 8 | import UIKit 9 | 10 | class ContextMenuTextCell: ContextMenuCell { 11 | 12 | lazy var titleLabel: UILabel = { 13 | let tLabel = UILabel() 14 | tLabel.translatesAutoresizingMaskIntoConstraints = false 15 | return tLabel 16 | }() 17 | lazy var iconImageView: UIImageView = { 18 | let imgView = UIImageView() 19 | imgView.translatesAutoresizingMaskIntoConstraints = false 20 | imgView.heightAnchor.constraint(equalToConstant: 20).isActive = true 21 | imgView.widthAnchor.constraint(equalToConstant: 20).isActive = true 22 | return imgView 23 | }() 24 | lazy var stackView: UIStackView = { 25 | let stackView = UIStackView(arrangedSubviews: [titleLabel, iconImageView]) 26 | stackView.translatesAutoresizingMaskIntoConstraints = false 27 | stackView.axis = .horizontal 28 | stackView.alignment = .center 29 | stackView.distribution = .fill 30 | stackView.spacing = 8 31 | return stackView 32 | }() 33 | 34 | override func commonInit() { 35 | super.commonInit() 36 | 37 | contentView.addSubview(stackView) 38 | 39 | NSLayoutConstraint.activate([ 40 | stackView.topAnchor.constraint(equalTo: contentView.topAnchor), 41 | stackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16), 42 | stackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), 43 | stackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16), 44 | ]) 45 | } 46 | 47 | override func awakeFromNib() { 48 | super.awakeFromNib() 49 | // Initialization code 50 | } 51 | 52 | override func setSelected(_ selected: Bool, animated: Bool) { 53 | super.setSelected(selected, animated: animated) 54 | 55 | // Configure the view for the selected state 56 | } 57 | 58 | override open func prepareForReuse() { 59 | super.prepareForReuse() 60 | 61 | titleLabel.text = nil 62 | iconImageView.image = nil 63 | 64 | } 65 | 66 | open override func setup(){ 67 | titleLabel.text = item.title 68 | if let menuConstants = style { 69 | titleLabel.textColor = menuConstants.LabelDefaultColor 70 | titleLabel.font = menuConstants.LabelDefaultFont 71 | iconImageView.tintColor = menuConstants.LabelDefaultColor 72 | } 73 | iconImageView.image = item.image 74 | iconImageView.isHidden = (item.image == nil) 75 | } 76 | 77 | } 78 | -------------------------------------------------------------------------------- /Example/ContextMenuSwift/CustomCell.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /Example/ContextMenuSwift/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // ContextMenuSwift 4 | // 5 | // Created by umerjabbar on 12/16/2021. 6 | // Copyright (c) 2021 umerjabbar. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import ContextMenuSwift 11 | 12 | class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource { 13 | 14 | @IBOutlet weak var cv1: UIView! 15 | @IBOutlet weak var cv2: UIView! 16 | @IBOutlet weak var cv3: UIView! 17 | @IBOutlet weak var canvas: UIView! 18 | 19 | override func viewDidLoad() { 20 | super.viewDidLoad() 21 | 22 | } 23 | 24 | @IBAction func buttonAction(_ sender: UIButton) { 25 | let share = ContextMenuItemWithImage(title: "Share", image: #imageLiteral(resourceName: "icons8-upload")) 26 | let edit = "Edit" 27 | let delete = ContextMenuItemWithImage(title: "Delete", image: #imageLiteral(resourceName: "icons8-trash")) 28 | // CM.nibView = UINib(nibName: "CustomCell", bundle: .main) 29 | CM.MenuConstants.horizontalDirection = .right 30 | CM.items = [share, edit, delete] 31 | CM.showMenu(viewTargeted: self.cv1, delegate: self) 32 | // let vc1 = UIView(frame: CGRect(x: 0, y: 0, width: CM.MenuConstants.MenuWidth, height: 50)) 33 | // vc1.backgroundColor = .purple 34 | // let vc2 = UIView(frame: CGRect(x: 0, y: 0, width: CM.MenuConstants.MenuWidth, height: 10)) 35 | // vc2.backgroundColor = .purple 36 | // CM.headerView = vc1 37 | // CM.footerView = vc2 38 | // DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { 39 | // CM.items = (0.. Int { 46 | return 8 47 | } 48 | 49 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 50 | let cell = tableView.dequeueReusableCell(withIdentifier: "ContextMenuCell") 51 | return cell! 52 | } 53 | } 54 | 55 | extension ViewController : ContextMenuDelegate { 56 | func contextMenuDidSelect(_ contextMenu: ContextMenu, cell: ContextMenuCell, targetedView: UIView, didSelect item: ContextMenuItem, forRowAt index: Int) -> Bool { 57 | print("contextMenuDidSelect", item.title) 58 | return true 59 | } 60 | 61 | func contextMenuDidDeselect(_ contextMenu: ContextMenu, cell: ContextMenuCell, targetedView: UIView, didSelect item: ContextMenuItem, forRowAt index: Int) { 62 | print("contextMenuDidDeselect") 63 | } 64 | 65 | func contextMenuDidAppear(_ contextMenu: ContextMenu) { 66 | print("contextMenuDidAppear") 67 | } 68 | 69 | func contextMenuDidDisappear(_ contextMenu: ContextMenu) { 70 | print("contextMenuDidDisappear") 71 | } 72 | 73 | } 74 | 75 | 76 | -------------------------------------------------------------------------------- /Example/ContextMenuSwift/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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ContextMenuSwift 2 | 3 | [![Linkedin: umerjabbar](http://img.shields.io/badge/linkedin-umerjabbar-70a1fb.svg?style=flat)](https://www.linkedin.com/in/umerjabbar) 4 | [![Twitter: @Umer_Jabbar](http://img.shields.io/badge/twitter-%40Umer_Jabbar-70a1fb.svg?style=flat)](https://twitter.com/Umer_Jabbar) 5 | ![License](https://img.shields.io/cocoapods/l/Hero.svg?style=flat) 6 | ![Xcode 10.0+](https://img.shields.io/badge/Xcode-9.0%2B-blue.svg) 7 | ![iOS 10.0+](https://img.shields.io/badge/iOS-10.0%2B-blue.svg) 8 | ![Swift 4.0+](https://img.shields.io/badge/Swift-4.0%2B-orange.svg) 9 | [![Cocoapods](http://img.shields.io/badge/Cocoapods-available-green.svg?style=flat)](https://cocoapods.org/pods/ContextMenuSwift) 10 | 11 | ## Installation 📱 12 | 13 | Just add `ContextMenuSwift` to your Podfile and `pod install`. Done! 14 | 15 | ```ruby 16 | pod 'ContextMenuSwift' 17 | ``` 18 | 19 | ## Usage ✨ 20 | 21 | ### Example 1 22 | 23 | 24 | 25 | Show the menu of string values on your view 26 | 27 | ```swift 28 | CM.items = ["Item 1", "Item 2", "Item 3"] 29 | CM.showMenu(viewTargeted: YourView, delegate: self, animated: true) 30 | ``` 31 | 32 | ### Example 2 33 | 34 | 35 | 36 | Update menu items async 37 | 38 | ```swift 39 | CM.items = ["Item 1", "Item 2", "Item 3"] 40 | CM.showMenu(viewTargeted: YourView, delegate: self, animated: true) 41 | DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { 42 | CM.items = ["Item 1"] 43 | CM.updateView(animated: true) 44 | } 45 | ``` 46 | 47 | ### Example 3 48 | 49 | 50 | 51 | Update targeted view async 52 | 53 | ```swift 54 | CM.items = ["Item 1", "Item 2", "Item 3"] 55 | CM.showMenu(viewTargeted: YourView, delegate: self, animated: true) 56 | DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { 57 | CM.changeViewTargeted(newView: YourView) 58 | CM.updateView(animated: true) 59 | } 60 | ``` 61 | 62 | ### Example 4 63 | 64 | 65 | 66 | Change the horizontal direction of menu 67 | 68 | ```swift 69 | CM.MenuConstants.horizontalDirection = .right 70 | CM.items = ["Item 1", "Item 2", "Item 3"] 71 | CM.showMenu(viewTargeted: YourView, delegate: self, animated: true) 72 | ``` 73 | 74 | ### Example 5 75 | 76 | 77 | 78 | Show menu with icons 79 | 80 | ```swift 81 | let share = ContextMenuItemWithImage(title: "Share", image: #imageLiteral(resourceName: "icons8-upload")) 82 | let edit = "Edit" 83 | let delete = ContextMenuItemWithImage(title: "Delete", image: #imageLiteral(resourceName: "icons8-trash")) 84 | CM.items = [share, edit, delete] 85 | CM.showMenu(viewTargeted: YourView, delegate: self, animated: true) 86 | ``` 87 | 88 | ### Delegate 89 | 90 | You can check events by implement ContextMenuDelegate 91 | ```swift 92 | extension ViewController : ContextMenuDelegate { 93 | 94 | func contextMenu(_ contextMenu: ContextMenu, targetedView: UIView, didSelect item: ContextMenuItem, forRowAt index: Int) -> Bool { 95 | print(item.title) 96 | return true //should dismiss on tap 97 | } 98 | 99 | func contextMenuDidAppear(_ contextMenu: ContextMenu) { 100 | print("contextMenuDidAppear") 101 | } 102 | 103 | func contextMenuDidDisappear(_ contextMenu: ContextMenu) { 104 | print("contextMenuDidDisappear") 105 | } 106 | 107 | } 108 | ``` 109 | 110 | ## Requirements 111 | 112 | * Xcode 9+ 113 | * Swift 4.0 114 | * iOS 10+ 115 | 116 | ## License 117 | 118 | This project is under MIT license. For more information, see `LICENSE` file. 119 | 120 | ## Credits 121 | 122 | ContextMenuSwift was developed while trying to implement iOS 13 context menu with a tap gesture. 123 | 124 | 125 | It will be updated when necessary and fixes will be done as soon as discovered to keep it up to date. 126 | 127 | You can find me on Twitter [@Umer_Jabbar](https://twitter.com/Umer_Jabbar) and Linkedin [umerjabbar](https://www.linkedin.com/in/umerjabbar/). 128 | 129 | Enjoy! 🤓 130 | -------------------------------------------------------------------------------- /Example/ContextMenuSwift.xcodeproj/xcshareddata/xcschemes/ContextMenuSwift-Example.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 45 | 46 | 48 | 54 | 55 | 56 | 57 | 58 | 64 | 65 | 66 | 67 | 68 | 69 | 80 | 82 | 88 | 89 | 90 | 91 | 92 | 93 | 99 | 101 | 107 | 108 | 109 | 110 | 112 | 113 | 116 | 117 | 118 | -------------------------------------------------------------------------------- /Example/ContextMenuSwift.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 52; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 0D59C979276B98930075FA82 /* CustomCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D59C976276B98930075FA82 /* CustomCell.swift */; }; 11 | 0D59C97A276B98930075FA82 /* CustomCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 0D59C977276B98930075FA82 /* CustomCell.xib */; }; 12 | 0D59C97C276B9DA90075FA82 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D59C97B276B9DA90075FA82 /* ViewController.swift */; }; 13 | 0DFC0C2B2A2757F700610225 /* ContextMenuSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 0DFC0C2A2A2757F700610225 /* ContextMenuSwift */; }; 14 | 607FACD61AFB9204008FA782 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 607FACD51AFB9204008FA782 /* AppDelegate.swift */; }; 15 | 607FACDB1AFB9204008FA782 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 607FACD91AFB9204008FA782 /* Main.storyboard */; }; 16 | 607FACDD1AFB9204008FA782 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 607FACDC1AFB9204008FA782 /* Images.xcassets */; }; 17 | 607FACE01AFB9204008FA782 /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 607FACDE1AFB9204008FA782 /* LaunchScreen.xib */; }; 18 | /* End PBXBuildFile section */ 19 | 20 | /* Begin PBXFileReference section */ 21 | 0D59C976276B98930075FA82 /* CustomCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomCell.swift; sourceTree = ""; }; 22 | 0D59C977276B98930075FA82 /* CustomCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = CustomCell.xib; sourceTree = ""; }; 23 | 0D59C97B276B9DA90075FA82 /* ViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 24 | 0DFC0C282A2757DC00610225 /* ContextMenuSwift */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = ContextMenuSwift; path = ..; sourceTree = ""; }; 25 | 607FACD01AFB9204008FA782 /* ContextMenuSwift_Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ContextMenuSwift_Example.app; sourceTree = BUILT_PRODUCTS_DIR; }; 26 | 607FACD41AFB9204008FA782 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 27 | 607FACD51AFB9204008FA782 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 28 | 607FACDA1AFB9204008FA782 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 29 | 607FACDC1AFB9204008FA782 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; 30 | 607FACDF1AFB9204008FA782 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/LaunchScreen.xib; sourceTree = ""; }; 31 | BA24FBF0AE1B3C4FAAE6BEC2 /* ContextMenuSwift.podspec */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; name = ContextMenuSwift.podspec; path = ../ContextMenuSwift.podspec; sourceTree = ""; }; 32 | DD757130CABAA8DC6998E34C /* README.md */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = net.daringfireball.markdown; name = README.md; path = ../README.md; sourceTree = ""; }; 33 | /* End PBXFileReference section */ 34 | 35 | /* Begin PBXFrameworksBuildPhase section */ 36 | 607FACCD1AFB9204008FA782 /* Frameworks */ = { 37 | isa = PBXFrameworksBuildPhase; 38 | buildActionMask = 2147483647; 39 | files = ( 40 | 0DFC0C2B2A2757F700610225 /* ContextMenuSwift in Frameworks */, 41 | ); 42 | runOnlyForDeploymentPostprocessing = 0; 43 | }; 44 | /* End PBXFrameworksBuildPhase section */ 45 | 46 | /* Begin PBXGroup section */ 47 | 0DFC0C272A2757DC00610225 /* Packages */ = { 48 | isa = PBXGroup; 49 | children = ( 50 | 0DFC0C282A2757DC00610225 /* ContextMenuSwift */, 51 | ); 52 | name = Packages; 53 | sourceTree = ""; 54 | }; 55 | 0DFC0C292A2757F700610225 /* Frameworks */ = { 56 | isa = PBXGroup; 57 | children = ( 58 | ); 59 | name = Frameworks; 60 | sourceTree = ""; 61 | }; 62 | 607FACC71AFB9204008FA782 = { 63 | isa = PBXGroup; 64 | children = ( 65 | 0DFC0C272A2757DC00610225 /* Packages */, 66 | 607FACF51AFB993E008FA782 /* Podspec Metadata */, 67 | 607FACD21AFB9204008FA782 /* Example for ContextMenuSwift */, 68 | 607FACD11AFB9204008FA782 /* Products */, 69 | 0DFC0C292A2757F700610225 /* Frameworks */, 70 | ); 71 | sourceTree = ""; 72 | }; 73 | 607FACD11AFB9204008FA782 /* Products */ = { 74 | isa = PBXGroup; 75 | children = ( 76 | 607FACD01AFB9204008FA782 /* ContextMenuSwift_Example.app */, 77 | ); 78 | name = Products; 79 | sourceTree = ""; 80 | }; 81 | 607FACD21AFB9204008FA782 /* Example for ContextMenuSwift */ = { 82 | isa = PBXGroup; 83 | children = ( 84 | 607FACD51AFB9204008FA782 /* AppDelegate.swift */, 85 | 0D59C976276B98930075FA82 /* CustomCell.swift */, 86 | 0D59C977276B98930075FA82 /* CustomCell.xib */, 87 | 0D59C97B276B9DA90075FA82 /* ViewController.swift */, 88 | 607FACD91AFB9204008FA782 /* Main.storyboard */, 89 | 607FACDC1AFB9204008FA782 /* Images.xcassets */, 90 | 607FACDE1AFB9204008FA782 /* LaunchScreen.xib */, 91 | 607FACD31AFB9204008FA782 /* Supporting Files */, 92 | ); 93 | name = "Example for ContextMenuSwift"; 94 | path = ContextMenuSwift; 95 | sourceTree = ""; 96 | }; 97 | 607FACD31AFB9204008FA782 /* Supporting Files */ = { 98 | isa = PBXGroup; 99 | children = ( 100 | 607FACD41AFB9204008FA782 /* Info.plist */, 101 | ); 102 | name = "Supporting Files"; 103 | sourceTree = ""; 104 | }; 105 | 607FACF51AFB993E008FA782 /* Podspec Metadata */ = { 106 | isa = PBXGroup; 107 | children = ( 108 | BA24FBF0AE1B3C4FAAE6BEC2 /* ContextMenuSwift.podspec */, 109 | DD757130CABAA8DC6998E34C /* README.md */, 110 | ); 111 | name = "Podspec Metadata"; 112 | sourceTree = ""; 113 | }; 114 | /* End PBXGroup section */ 115 | 116 | /* Begin PBXNativeTarget section */ 117 | 607FACCF1AFB9204008FA782 /* ContextMenuSwift_Example */ = { 118 | isa = PBXNativeTarget; 119 | buildConfigurationList = 607FACEF1AFB9204008FA782 /* Build configuration list for PBXNativeTarget "ContextMenuSwift_Example" */; 120 | buildPhases = ( 121 | 607FACCC1AFB9204008FA782 /* Sources */, 122 | 607FACCD1AFB9204008FA782 /* Frameworks */, 123 | 607FACCE1AFB9204008FA782 /* Resources */, 124 | ); 125 | buildRules = ( 126 | ); 127 | dependencies = ( 128 | ); 129 | name = ContextMenuSwift_Example; 130 | packageProductDependencies = ( 131 | 0DFC0C2A2A2757F700610225 /* ContextMenuSwift */, 132 | ); 133 | productName = ContextMenuSwift; 134 | productReference = 607FACD01AFB9204008FA782 /* ContextMenuSwift_Example.app */; 135 | productType = "com.apple.product-type.application"; 136 | }; 137 | /* End PBXNativeTarget section */ 138 | 139 | /* Begin PBXProject section */ 140 | 607FACC81AFB9204008FA782 /* Project object */ = { 141 | isa = PBXProject; 142 | attributes = { 143 | LastSwiftUpdateCheck = 0830; 144 | LastUpgradeCheck = 0830; 145 | ORGANIZATIONNAME = CocoaPods; 146 | TargetAttributes = { 147 | 607FACCF1AFB9204008FA782 = { 148 | CreatedOnToolsVersion = 6.3.1; 149 | LastSwiftMigration = 0900; 150 | ProvisioningStyle = Manual; 151 | }; 152 | }; 153 | }; 154 | buildConfigurationList = 607FACCB1AFB9204008FA782 /* Build configuration list for PBXProject "ContextMenuSwift" */; 155 | compatibilityVersion = "Xcode 3.2"; 156 | developmentRegion = English; 157 | hasScannedForEncodings = 0; 158 | knownRegions = ( 159 | English, 160 | en, 161 | Base, 162 | ); 163 | mainGroup = 607FACC71AFB9204008FA782; 164 | productRefGroup = 607FACD11AFB9204008FA782 /* Products */; 165 | projectDirPath = ""; 166 | projectRoot = ""; 167 | targets = ( 168 | 607FACCF1AFB9204008FA782 /* ContextMenuSwift_Example */, 169 | ); 170 | }; 171 | /* End PBXProject section */ 172 | 173 | /* Begin PBXResourcesBuildPhase section */ 174 | 607FACCE1AFB9204008FA782 /* Resources */ = { 175 | isa = PBXResourcesBuildPhase; 176 | buildActionMask = 2147483647; 177 | files = ( 178 | 607FACDB1AFB9204008FA782 /* Main.storyboard in Resources */, 179 | 607FACE01AFB9204008FA782 /* LaunchScreen.xib in Resources */, 180 | 0D59C97A276B98930075FA82 /* CustomCell.xib in Resources */, 181 | 607FACDD1AFB9204008FA782 /* Images.xcassets in Resources */, 182 | ); 183 | runOnlyForDeploymentPostprocessing = 0; 184 | }; 185 | /* End PBXResourcesBuildPhase section */ 186 | 187 | /* Begin PBXSourcesBuildPhase section */ 188 | 607FACCC1AFB9204008FA782 /* Sources */ = { 189 | isa = PBXSourcesBuildPhase; 190 | buildActionMask = 2147483647; 191 | files = ( 192 | 0D59C979276B98930075FA82 /* CustomCell.swift in Sources */, 193 | 0D59C97C276B9DA90075FA82 /* ViewController.swift in Sources */, 194 | 607FACD61AFB9204008FA782 /* AppDelegate.swift in Sources */, 195 | ); 196 | runOnlyForDeploymentPostprocessing = 0; 197 | }; 198 | /* End PBXSourcesBuildPhase section */ 199 | 200 | /* Begin PBXVariantGroup section */ 201 | 607FACD91AFB9204008FA782 /* Main.storyboard */ = { 202 | isa = PBXVariantGroup; 203 | children = ( 204 | 607FACDA1AFB9204008FA782 /* Base */, 205 | ); 206 | name = Main.storyboard; 207 | sourceTree = ""; 208 | }; 209 | 607FACDE1AFB9204008FA782 /* LaunchScreen.xib */ = { 210 | isa = PBXVariantGroup; 211 | children = ( 212 | 607FACDF1AFB9204008FA782 /* Base */, 213 | ); 214 | name = LaunchScreen.xib; 215 | sourceTree = ""; 216 | }; 217 | /* End PBXVariantGroup section */ 218 | 219 | /* Begin XCBuildConfiguration section */ 220 | 607FACED1AFB9204008FA782 /* Debug */ = { 221 | isa = XCBuildConfiguration; 222 | buildSettings = { 223 | ALWAYS_SEARCH_USER_PATHS = NO; 224 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 225 | CLANG_CXX_LIBRARY = "libc++"; 226 | CLANG_ENABLE_MODULES = YES; 227 | CLANG_ENABLE_OBJC_ARC = YES; 228 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 229 | CLANG_WARN_BOOL_CONVERSION = YES; 230 | CLANG_WARN_COMMA = YES; 231 | CLANG_WARN_CONSTANT_CONVERSION = YES; 232 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 233 | CLANG_WARN_EMPTY_BODY = YES; 234 | CLANG_WARN_ENUM_CONVERSION = YES; 235 | CLANG_WARN_INFINITE_RECURSION = YES; 236 | CLANG_WARN_INT_CONVERSION = YES; 237 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 238 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 239 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 240 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 241 | CLANG_WARN_STRICT_PROTOTYPES = YES; 242 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 243 | CLANG_WARN_UNREACHABLE_CODE = YES; 244 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 245 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 246 | COPY_PHASE_STRIP = NO; 247 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 248 | ENABLE_STRICT_OBJC_MSGSEND = YES; 249 | ENABLE_TESTABILITY = YES; 250 | GCC_C_LANGUAGE_STANDARD = gnu99; 251 | GCC_DYNAMIC_NO_PIC = NO; 252 | GCC_NO_COMMON_BLOCKS = YES; 253 | GCC_OPTIMIZATION_LEVEL = 0; 254 | GCC_PREPROCESSOR_DEFINITIONS = ( 255 | "DEBUG=1", 256 | "$(inherited)", 257 | ); 258 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 259 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 260 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 261 | GCC_WARN_UNDECLARED_SELECTOR = YES; 262 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 263 | GCC_WARN_UNUSED_FUNCTION = YES; 264 | GCC_WARN_UNUSED_VARIABLE = YES; 265 | IPHONEOS_DEPLOYMENT_TARGET = 10.0; 266 | MTL_ENABLE_DEBUG_INFO = YES; 267 | ONLY_ACTIVE_ARCH = YES; 268 | SDKROOT = iphoneos; 269 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 270 | }; 271 | name = Debug; 272 | }; 273 | 607FACEE1AFB9204008FA782 /* Release */ = { 274 | isa = XCBuildConfiguration; 275 | buildSettings = { 276 | ALWAYS_SEARCH_USER_PATHS = NO; 277 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 278 | CLANG_CXX_LIBRARY = "libc++"; 279 | CLANG_ENABLE_MODULES = YES; 280 | CLANG_ENABLE_OBJC_ARC = YES; 281 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 282 | CLANG_WARN_BOOL_CONVERSION = YES; 283 | CLANG_WARN_COMMA = YES; 284 | CLANG_WARN_CONSTANT_CONVERSION = YES; 285 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 286 | CLANG_WARN_EMPTY_BODY = YES; 287 | CLANG_WARN_ENUM_CONVERSION = YES; 288 | CLANG_WARN_INFINITE_RECURSION = YES; 289 | CLANG_WARN_INT_CONVERSION = YES; 290 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 291 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 292 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 293 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 294 | CLANG_WARN_STRICT_PROTOTYPES = YES; 295 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 296 | CLANG_WARN_UNREACHABLE_CODE = YES; 297 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 298 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 299 | COPY_PHASE_STRIP = NO; 300 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 301 | ENABLE_NS_ASSERTIONS = NO; 302 | ENABLE_STRICT_OBJC_MSGSEND = YES; 303 | GCC_C_LANGUAGE_STANDARD = gnu99; 304 | GCC_NO_COMMON_BLOCKS = YES; 305 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 306 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 307 | GCC_WARN_UNDECLARED_SELECTOR = YES; 308 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 309 | GCC_WARN_UNUSED_FUNCTION = YES; 310 | GCC_WARN_UNUSED_VARIABLE = YES; 311 | IPHONEOS_DEPLOYMENT_TARGET = 10.0; 312 | MTL_ENABLE_DEBUG_INFO = NO; 313 | SDKROOT = iphoneos; 314 | SWIFT_COMPILATION_MODE = wholemodule; 315 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 316 | VALIDATE_PRODUCT = YES; 317 | }; 318 | name = Release; 319 | }; 320 | 607FACF01AFB9204008FA782 /* Debug */ = { 321 | isa = XCBuildConfiguration; 322 | buildSettings = { 323 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 324 | CODE_SIGN_STYLE = Manual; 325 | DEVELOPMENT_TEAM = ""; 326 | INFOPLIST_FILE = ContextMenuSwift/Info.plist; 327 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 328 | LD_RUNPATH_SEARCH_PATHS = ( 329 | "$(inherited)", 330 | "@executable_path/Frameworks", 331 | ); 332 | MODULE_NAME = ExampleApp; 333 | PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.$(PRODUCT_NAME:rfc1034identifier)"; 334 | PRODUCT_NAME = "$(TARGET_NAME)"; 335 | PROVISIONING_PROFILE_SPECIFIER = ""; 336 | SWIFT_SWIFT3_OBJC_INFERENCE = Default; 337 | SWIFT_VERSION = 4.0; 338 | }; 339 | name = Debug; 340 | }; 341 | 607FACF11AFB9204008FA782 /* Release */ = { 342 | isa = XCBuildConfiguration; 343 | buildSettings = { 344 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 345 | CODE_SIGN_STYLE = Manual; 346 | DEVELOPMENT_TEAM = ""; 347 | INFOPLIST_FILE = ContextMenuSwift/Info.plist; 348 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 349 | LD_RUNPATH_SEARCH_PATHS = ( 350 | "$(inherited)", 351 | "@executable_path/Frameworks", 352 | ); 353 | MODULE_NAME = ExampleApp; 354 | PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.$(PRODUCT_NAME:rfc1034identifier)"; 355 | PRODUCT_NAME = "$(TARGET_NAME)"; 356 | PROVISIONING_PROFILE_SPECIFIER = ""; 357 | SWIFT_SWIFT3_OBJC_INFERENCE = Default; 358 | SWIFT_VERSION = 4.0; 359 | }; 360 | name = Release; 361 | }; 362 | /* End XCBuildConfiguration section */ 363 | 364 | /* Begin XCConfigurationList section */ 365 | 607FACCB1AFB9204008FA782 /* Build configuration list for PBXProject "ContextMenuSwift" */ = { 366 | isa = XCConfigurationList; 367 | buildConfigurations = ( 368 | 607FACED1AFB9204008FA782 /* Debug */, 369 | 607FACEE1AFB9204008FA782 /* Release */, 370 | ); 371 | defaultConfigurationIsVisible = 0; 372 | defaultConfigurationName = Release; 373 | }; 374 | 607FACEF1AFB9204008FA782 /* Build configuration list for PBXNativeTarget "ContextMenuSwift_Example" */ = { 375 | isa = XCConfigurationList; 376 | buildConfigurations = ( 377 | 607FACF01AFB9204008FA782 /* Debug */, 378 | 607FACF11AFB9204008FA782 /* Release */, 379 | ); 380 | defaultConfigurationIsVisible = 0; 381 | defaultConfigurationName = Release; 382 | }; 383 | /* End XCConfigurationList section */ 384 | 385 | /* Begin XCSwiftPackageProductDependency section */ 386 | 0DFC0C2A2A2757F700610225 /* ContextMenuSwift */ = { 387 | isa = XCSwiftPackageProductDependency; 388 | productName = ContextMenuSwift; 389 | }; 390 | /* End XCSwiftPackageProductDependency section */ 391 | }; 392 | rootObject = 607FACC81AFB9204008FA782 /* Project object */; 393 | } 394 | -------------------------------------------------------------------------------- /Sources/ContextMenuSwift/ContextMenu.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomFocusedView.swift 3 | // Seekr 4 | // 5 | // Created by macmin on 29/04/2020. 6 | // Copyright © 2020 macmin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public protocol ContextMenuItem { 12 | var title : String { 13 | get 14 | } 15 | var image : UIImage? { 16 | get 17 | } 18 | } 19 | 20 | extension ContextMenuItem { 21 | public var image: UIImage? { 22 | get { return nil } 23 | } 24 | } 25 | 26 | extension String : ContextMenuItem { 27 | public var title: String { 28 | get { 29 | return "\(self)" 30 | } 31 | } 32 | } 33 | public struct ContextMenuItemWithImage: ContextMenuItem { 34 | public var title: String 35 | public var image: UIImage? 36 | 37 | public init(title: String, image: UIImage) { 38 | self.title = title 39 | self.image = image 40 | } 41 | } 42 | 43 | public protocol ContextMenuDelegate : AnyObject { 44 | func contextMenuDidSelect(_ contextMenu: ContextMenu, cell: ContextMenuCell, targetedView: UIView, didSelect item: ContextMenuItem, forRowAt index: Int) -> Bool 45 | func contextMenuDidDeselect(_ contextMenu: ContextMenu, cell: ContextMenuCell, targetedView: UIView, didSelect item: ContextMenuItem, forRowAt index: Int) 46 | func contextMenuDidAppear(_ contextMenu: ContextMenu) 47 | func contextMenuDidDisappear(_ contextMenu: ContextMenu) 48 | } 49 | extension ContextMenuDelegate { 50 | func contextMenuDidAppear(_ contextMenu: ContextMenu){} 51 | func contextMenuDidDisappear(_ contextMenu: ContextMenu){} 52 | } 53 | 54 | public var CM : ContextMenu = ContextMenu() 55 | 56 | public class ContextMenuConstants { 57 | 58 | public enum HorizontalDirection { 59 | case left 60 | case center 61 | case right 62 | } 63 | 64 | public var MaxZoom : CGFloat = 1.05 65 | public var MinZoom : CGFloat = 0.95 66 | public var MenuDefaultHeight : CGFloat = 120 67 | public var MenuWidth : CGFloat = 250 68 | public var MenuMarginSpace : CGFloat = 20 69 | public var TopMarginSpace : CGFloat = 0 70 | public var BottomMarginSpace : CGFloat = 0 71 | public var HorizontalMarginSpace : CGFloat = 20 72 | public var ItemDefaultHeight : CGFloat = 44 73 | 74 | public var LabelDefaultFont : UIFont = .systemFont(ofSize: 14) 75 | public var LabelDefaultColor : UIColor = { 76 | if #available(iOS 13.0, *) { 77 | UIColor.label.withAlphaComponent(0.95) 78 | } else { 79 | UIColor.black.withAlphaComponent(0.95) 80 | } 81 | }() 82 | public var ItemDefaultColor : UIColor = { 83 | if #available(iOS 13.0, *) { 84 | UIColor.systemBackground.withAlphaComponent(0.95) 85 | } else { 86 | UIColor.white.withAlphaComponent(0.95) 87 | } 88 | }() 89 | 90 | public var MenuCornerRadius : CGFloat = 12 91 | public var BlurEffectEnabled : Bool = true 92 | public var BlurEffectDefault : UIBlurEffect = UIBlurEffect(style: .dark) 93 | public var BackgroundViewColor : UIColor = UIColor.black.withAlphaComponent(0.6) 94 | 95 | public var DismissOnItemTap : Bool = false 96 | public var horizontalDirection: HorizontalDirection = .left 97 | } 98 | 99 | open class ContextMenu: NSObject { 100 | 101 | // MARK:- open Variables 102 | open var MenuConstants = ContextMenuConstants() 103 | open var viewTargeted: UIView! 104 | open var placeHolderView : UIView? 105 | open var headerView : UIView? 106 | open var footerView : UIView? 107 | open var nibView: UINib? 108 | open var cellClassView: ContextMenuCell.Type = ContextMenuTextCell.self 109 | open var closeAnimation = true 110 | 111 | open var onItemTap : ((_ index: Int, _ item: ContextMenuItem) -> Bool)? 112 | open var onViewAppear : ((UIView) -> Void)? 113 | open var onViewDismiss : ((UIView) -> Void)? 114 | 115 | open var items = [ContextMenuItem]() 116 | 117 | // MARK:- Private Variables 118 | private weak var delegate : ContextMenuDelegate? 119 | 120 | private var mainViewRect : CGRect 121 | private var customView = UIView() 122 | private var blurEffectView = UIVisualEffectView() 123 | private var closeButton = UIButton() 124 | private var targetedImageView = UIImageView() 125 | private var menuView = UIView() 126 | public var tableView = UITableView() 127 | private var tableViewConstraint : NSLayoutConstraint? 128 | private var zoomedTargetedSize = CGRect() 129 | 130 | private var menuHeight : CGFloat = 180 131 | private var isLandscape : Bool = false 132 | 133 | private var touchGesture : UITapGestureRecognizer? 134 | private var closeGesture : UITapGestureRecognizer? 135 | 136 | private var tvH : CGFloat = 0.0 137 | private var tvW : CGFloat = 0.0 138 | private var tvY : CGFloat = 0.0 139 | private var tvX : CGFloat = 0.0 140 | private var mH : CGFloat = 0.0 141 | private var mW : CGFloat = 0.0 142 | private var mY : CGFloat = 0.0 143 | private var mX : CGFloat = 0.0 144 | 145 | private var topMarginSpace: CGFloat { 146 | customView.safeAreaInsets.top + MenuConstants.TopMarginSpace 147 | } 148 | private var bottomMarginSpace: CGFloat { 149 | customView.safeAreaInsets.bottom + MenuConstants.BottomMarginSpace 150 | } 151 | 152 | // MARK:- Init Functions 153 | public init(window: UIView? = nil) { 154 | let wind = window ?? UIApplication.shared.windows.first ?? UIApplication.shared.windows.first(where: {$0.isKeyWindow}) 155 | self.customView = wind! 156 | self.mainViewRect = wind!.frame 157 | } 158 | 159 | init?(viewTargeted: UIView, window: UIView? = nil) { 160 | if let wind = window ?? UIApplication.shared.windows.first ?? UIApplication.shared.windows.first(where: {$0.isKeyWindow}) { 161 | self.customView = wind 162 | self.viewTargeted = viewTargeted 163 | self.mainViewRect = self.customView.frame 164 | } else { 165 | return nil 166 | } 167 | } 168 | 169 | public init(viewTargeted: UIView, window: UIView) { 170 | self.viewTargeted = viewTargeted 171 | self.customView = window 172 | self.mainViewRect = window.frame 173 | } 174 | 175 | deinit { 176 | print("Deinit") 177 | } 178 | 179 | // MARK:- Show, Change, Update Menu Functions 180 | open func showMenu(viewTargeted: UIView, delegate: ContextMenuDelegate, animated: Bool = true){ 181 | NotificationCenter.default.addObserver(self, selector: #selector(self.rotated), name: UIDevice.orientationDidChangeNotification, object: nil) 182 | DispatchQueue.main.async { 183 | self.delegate = delegate 184 | self.viewTargeted = viewTargeted 185 | if !self.items.isEmpty { 186 | self.menuHeight = (CGFloat(self.items.count) * self.MenuConstants.ItemDefaultHeight) + (self.headerView?.frame.height ?? 0) + (self.footerView?.frame.height ?? 0) // + CGFloat(self.items.count - 1) 187 | } else { 188 | self.menuHeight = self.MenuConstants.MenuDefaultHeight 189 | } 190 | self.addBlurEffectView() 191 | self.addMenuView() 192 | self.addTargetedImageView() 193 | self.openAllViews() 194 | } 195 | } 196 | 197 | open func changeViewTargeted(newView: UIView, animated: Bool = true){ 198 | DispatchQueue.main.async { 199 | guard self.viewTargeted != nil else { 200 | print("targetedView is nil") 201 | return 202 | } 203 | self.viewTargeted.alpha = 1 204 | if let gesture = self.touchGesture { 205 | self.viewTargeted.removeGestureRecognizer(gesture) 206 | } 207 | self.viewTargeted = newView 208 | self.targetedImageView.image = self.getRenderedImage(afterScreenUpdates: true) 209 | if let gesture = self.touchGesture { 210 | self.viewTargeted.addGestureRecognizer(gesture) 211 | } 212 | self.updateTargetedImageViewPosition(animated: animated) 213 | } 214 | } 215 | 216 | open func updateView(animated: Bool = true){ 217 | DispatchQueue.main.async { 218 | guard self.viewTargeted != nil else { 219 | print("targetedView is nil") 220 | return 221 | } 222 | guard self.customView.subviews.contains(self.targetedImageView) else {return} 223 | if !self.items.isEmpty { 224 | self.menuHeight = (CGFloat(self.items.count) * self.MenuConstants.ItemDefaultHeight) + (self.headerView?.frame.height ?? 0) + (self.footerView?.frame.height ?? 0) // + CGFloat(self.items.count - 1) 225 | } else { 226 | self.menuHeight = self.MenuConstants.MenuDefaultHeight 227 | } 228 | self.viewTargeted.alpha = 0 229 | self.addMenuView() 230 | self.updateTargetedImageViewPosition(animated: animated) 231 | } 232 | } 233 | 234 | open func closeMenu() { 235 | self.closeAllViews() 236 | } 237 | 238 | open func closeMenu(withAnimation animation: Bool) { 239 | closeAllViews(withAnimation: animation) 240 | } 241 | 242 | // MARK:- Get Rendered Image Functions 243 | func getRenderedImage(afterScreenUpdates: Bool = false) -> UIImage{ 244 | let renderer = UIGraphicsImageRenderer(size: viewTargeted.bounds.size) 245 | let viewSnapShotImage = renderer.image { ctx in 246 | viewTargeted.contentScaleFactor = 3 247 | viewTargeted.drawHierarchy(in: viewTargeted.bounds, afterScreenUpdates: afterScreenUpdates) 248 | } 249 | return viewSnapShotImage 250 | } 251 | 252 | func addBlurEffectView() { 253 | 254 | if !customView.subviews.contains(blurEffectView) { 255 | customView.addSubview(blurEffectView) 256 | } 257 | if MenuConstants.BlurEffectEnabled { 258 | blurEffectView.effect = MenuConstants.BlurEffectDefault 259 | blurEffectView.backgroundColor = .clear 260 | } else { 261 | blurEffectView.effect = nil 262 | blurEffectView.backgroundColor = MenuConstants.BackgroundViewColor 263 | } 264 | 265 | blurEffectView.frame = CGRect(x: mainViewRect.origin.x, y: mainViewRect.origin.y, width: mainViewRect.width, height: mainViewRect.height) 266 | if closeGesture == nil { 267 | blurEffectView.isUserInteractionEnabled = true 268 | closeGesture = UITapGestureRecognizer(target: self, action: #selector(self.dismissViewAction(_:))) 269 | blurEffectView.addGestureRecognizer(closeGesture!) 270 | } 271 | } 272 | 273 | @objc func dismissViewAction(_ sender: UITapGestureRecognizer? = nil){ 274 | self.closeAllViews() 275 | } 276 | 277 | func addCloseButton() { 278 | 279 | if !customView.subviews.contains(closeButton) { 280 | customView.addSubview(closeButton) 281 | } 282 | closeButton.frame = CGRect(x: mainViewRect.origin.x, y: mainViewRect.origin.y, width: mainViewRect.width, height: mainViewRect.height) 283 | closeButton.setTitle("", for: .normal) 284 | closeButton.actionHandler(controlEvents: .touchUpInside) { //[weak self] in 285 | self.closeAllViews() 286 | } 287 | } 288 | 289 | func addTargetedImageView() { 290 | 291 | if !customView.subviews.contains(targetedImageView) { 292 | customView.addSubview(targetedImageView) 293 | } 294 | 295 | let rect = viewTargeted.convert(mainViewRect.origin, to: nil) 296 | 297 | targetedImageView.image = self.getRenderedImage() 298 | targetedImageView.frame = CGRect(x: rect.x, 299 | y: rect.y, 300 | width: viewTargeted.frame.width, 301 | height: viewTargeted.frame.height) 302 | targetedImageView.layer.shadowColor = UIColor.black.cgColor 303 | targetedImageView.layer.shadowRadius = 16 304 | targetedImageView.layer.shadowOpacity = 0 305 | targetedImageView.isUserInteractionEnabled = true 306 | 307 | } 308 | 309 | func addMenuView() { 310 | 311 | if !customView.subviews.contains(menuView) { 312 | customView.addSubview(menuView) 313 | tableView = UITableView() 314 | } else { 315 | tableView.removeFromSuperview() 316 | tableView = UITableView() 317 | } 318 | 319 | let rect = viewTargeted.convert(mainViewRect.origin, to: nil) 320 | 321 | menuView.backgroundColor = MenuConstants.ItemDefaultColor 322 | menuView.layer.cornerRadius = MenuConstants.MenuCornerRadius 323 | menuView.clipsToBounds = true 324 | menuView.frame = CGRect( 325 | x: rect.x, 326 | y: rect.y, 327 | width: self.viewTargeted.frame.width, height: self.viewTargeted.frame.height 328 | ) 329 | menuView.addSubview(tableView) 330 | 331 | tableView.dataSource = self 332 | tableView.delegate = self 333 | tableView.frame = menuView.bounds 334 | if let nibView = nibView { 335 | tableView.register(nibView, forCellReuseIdentifier: "ContextMenuCell") 336 | } else { 337 | tableView.register(cellClassView, forCellReuseIdentifier: "ContextMenuCell") 338 | } 339 | tableView.tableHeaderView = self.headerView 340 | tableView.tableFooterView = self.footerView 341 | tableView.clipsToBounds = true 342 | tableView.isScrollEnabled = true 343 | tableView.alwaysBounceVertical = false 344 | tableView.allowsMultipleSelection = true 345 | tableView.backgroundColor = .clear 346 | tableView.reloadData() 347 | } 348 | 349 | func openAllViews(animated: Bool = true){ 350 | let rect = self.viewTargeted.convert(self.mainViewRect.origin, to: nil) 351 | viewTargeted.alpha = 0 352 | blurEffectView.alpha = 0 353 | closeButton.isUserInteractionEnabled = true 354 | targetedImageView.alpha = 1 355 | targetedImageView.layer.shadowOpacity = 0.0 356 | targetedImageView.isUserInteractionEnabled = true 357 | targetedImageView.frame = CGRect(x: rect.x, y: rect.y, width: self.viewTargeted.frame.width, height: self.viewTargeted.frame.height) 358 | menuView.alpha = 0 359 | menuView.isUserInteractionEnabled = true 360 | // menuView.transform = CGAffineTransform.identity.scaledBy(x: 0, y: 0) 361 | menuView.frame = CGRect(x: rect.x, y: rect.y, width: self.viewTargeted.frame.width, height: self.viewTargeted.frame.height) 362 | 363 | if animated { 364 | UIView.animate(withDuration: 0.2) { 365 | self.blurEffectView.alpha = 1 366 | self.targetedImageView.layer.shadowOpacity = 0.2 367 | } 368 | } else { 369 | self.blurEffectView.alpha = 1 370 | self.targetedImageView.layer.shadowOpacity = 0.2 371 | } 372 | self.updateTargetedImageViewPosition(animated: animated) 373 | self.onViewAppear?(self.viewTargeted) 374 | 375 | self.delegate?.contextMenuDidAppear(self) 376 | } 377 | 378 | func closeAllViews() { 379 | NotificationCenter.default.removeObserver(self, name: UIDevice.orientationDidChangeNotification, object: nil) 380 | DispatchQueue.main.async { 381 | self.targetedImageView.isUserInteractionEnabled = false 382 | self.menuView.isUserInteractionEnabled = false 383 | self.closeButton.isUserInteractionEnabled = false 384 | 385 | let rect = self.viewTargeted.convert(self.mainViewRect.origin, to: nil) 386 | if self.closeAnimation { 387 | UIView.animate(withDuration: 0.2, delay: 0, options: [.layoutSubviews, .curveEaseInOut, .allowUserInteraction], animations: { 388 | self.prepareViewsForRemoveFromSuperView(with: rect) 389 | }) { (_) in 390 | DispatchQueue.main.async { 391 | self.removeAllViewsFromSuperView() 392 | } 393 | } 394 | } else { 395 | DispatchQueue.main.async { 396 | self.prepareViewsForRemoveFromSuperView(with: rect) 397 | self.removeAllViewsFromSuperView() 398 | } 399 | } 400 | self.onViewDismiss?(self.viewTargeted) 401 | self.delegate?.contextMenuDidDisappear(self) 402 | } 403 | } 404 | 405 | func closeAllViews(withAnimation animation: Bool = true) { 406 | NotificationCenter.default.removeObserver(self, name: UIDevice.orientationDidChangeNotification, object: nil) 407 | DispatchQueue.main.async { 408 | self.targetedImageView.isUserInteractionEnabled = false 409 | self.menuView.isUserInteractionEnabled = false 410 | self.closeButton.isUserInteractionEnabled = false 411 | 412 | let rect = self.viewTargeted.convert(self.mainViewRect.origin, to: nil) 413 | if animation { 414 | UIView.animate(withDuration: 0.2, delay: 0, options: [.layoutSubviews, .curveEaseInOut, .allowUserInteraction], animations: { 415 | self.prepareViewsForRemoveFromSuperView(with: rect) 416 | }) { (_) in 417 | DispatchQueue.main.async { 418 | self.removeAllViewsFromSuperView() 419 | } 420 | } 421 | } else { 422 | DispatchQueue.main.async { 423 | self.prepareViewsForRemoveFromSuperView(with: rect) 424 | self.removeAllViewsFromSuperView() 425 | } 426 | } 427 | self.onViewDismiss?(self.viewTargeted) 428 | self.delegate?.contextMenuDidDisappear(self) 429 | } 430 | } 431 | 432 | func prepareViewsForRemoveFromSuperView(with rect: CGPoint) { 433 | self.blurEffectView.alpha = 0 434 | self.targetedImageView.layer.shadowOpacity = 0 435 | self.targetedImageView.frame = CGRect(x: rect.x, y: rect.y, width: self.viewTargeted.frame.width, height: self.viewTargeted.frame.height) 436 | self.menuView.alpha = 0 437 | self.menuView.frame = CGRect(x: rect.x, y: rect.y, width: self.viewTargeted.frame.width, height: self.viewTargeted.frame.height) 438 | } 439 | 440 | func removeAllViewsFromSuperView() { 441 | self.viewTargeted?.alpha = 1 442 | self.targetedImageView.alpha = 0 443 | self.targetedImageView.removeFromSuperview() 444 | self.blurEffectView.removeFromSuperview() 445 | self.closeButton.removeFromSuperview() 446 | self.menuView.removeFromSuperview() 447 | self.tableView.removeFromSuperview() 448 | } 449 | 450 | @objc func rotated() { 451 | if UIDevice.current.orientation.isLandscape, !isLandscape { 452 | self.updateView() 453 | isLandscape = true 454 | print("Landscape") 455 | } else if !UIDevice.current.orientation.isLandscape, isLandscape { 456 | self.updateView() 457 | isLandscape = false 458 | print("Portrait") 459 | } 460 | } 461 | 462 | func getZoomedTargetedSize() -> CGRect{ 463 | 464 | let rect = viewTargeted.convert(mainViewRect.origin, to: nil) 465 | let targetedImageFrame = viewTargeted.frame 466 | 467 | let backgroundWidth = mainViewRect.width - (2 * MenuConstants.HorizontalMarginSpace) 468 | let backgroundHeight = mainViewRect.height - topMarginSpace - bottomMarginSpace 469 | 470 | var zoomFactor = MenuConstants.MaxZoom 471 | 472 | var updatedWidth = targetedImageFrame.width // * zoomFactor 473 | var updatedHeight = targetedImageFrame.height // * zoomFactor 474 | 475 | if backgroundWidth > backgroundHeight { 476 | 477 | let zoomFactorHorizontalWithMenu = (backgroundWidth - MenuConstants.MenuWidth - MenuConstants.MenuMarginSpace)/updatedWidth 478 | let zoomFactorVerticalWithMenu = backgroundHeight/updatedHeight 479 | 480 | if zoomFactorHorizontalWithMenu < zoomFactorVerticalWithMenu { 481 | zoomFactor = zoomFactorHorizontalWithMenu 482 | } else { 483 | zoomFactor = zoomFactorVerticalWithMenu 484 | } 485 | if zoomFactor > MenuConstants.MaxZoom { 486 | zoomFactor = MenuConstants.MaxZoom 487 | } 488 | 489 | // Menu Height 490 | if self.menuHeight > backgroundHeight { 491 | self.menuHeight = backgroundHeight + MenuConstants.MenuMarginSpace 492 | } 493 | } else { 494 | 495 | let zoomFactorHorizontalWithMenu = backgroundWidth/(updatedWidth) 496 | let zoomFactorVerticalWithMenu = backgroundHeight/(updatedHeight + self.menuHeight + MenuConstants.MenuMarginSpace) 497 | 498 | if zoomFactorHorizontalWithMenu < zoomFactorVerticalWithMenu { 499 | zoomFactor = zoomFactorHorizontalWithMenu 500 | } else { 501 | zoomFactor = zoomFactorVerticalWithMenu 502 | } 503 | if zoomFactor > MenuConstants.MaxZoom { 504 | zoomFactor = MenuConstants.MaxZoom 505 | } else if zoomFactor < MenuConstants.MinZoom { 506 | zoomFactor = MenuConstants.MinZoom 507 | } 508 | } 509 | 510 | updatedWidth = (updatedWidth * zoomFactor) 511 | updatedHeight = (updatedHeight * zoomFactor) 512 | 513 | let updatedX = rect.x - (updatedWidth - targetedImageFrame.width)/2 514 | let updatedY = rect.y - (updatedHeight - targetedImageFrame.height)/2 515 | 516 | return CGRect(x: updatedX, y: updatedY, width: updatedWidth, height: updatedHeight) 517 | 518 | } 519 | 520 | func fixTargetedImageViewExtrudings() { // here I am checking for extruding part of ImageView 521 | 522 | if tvY > mainViewRect.height - bottomMarginSpace - tvH { 523 | tvY = mainViewRect.height - bottomMarginSpace - tvH 524 | } 525 | else if tvY < topMarginSpace { 526 | tvY = topMarginSpace 527 | } 528 | 529 | if tvX < MenuConstants.HorizontalMarginSpace { 530 | tvX = MenuConstants.HorizontalMarginSpace 531 | } 532 | else if tvX > mainViewRect.width - MenuConstants.HorizontalMarginSpace - tvW { 533 | tvX = mainViewRect.width - MenuConstants.HorizontalMarginSpace - tvW 534 | } 535 | } 536 | 537 | func updateHorizontalTargetedImageViewRect() { 538 | 539 | let rightClippedSpace = (tvW + MenuConstants.MenuMarginSpace + mW + tvX + MenuConstants.HorizontalMarginSpace) - mainViewRect.width 540 | let leftClippedSpace = -(tvX - MenuConstants.MenuMarginSpace - mW - MenuConstants.HorizontalMarginSpace) 541 | 542 | if leftClippedSpace > 0, rightClippedSpace > 0 { 543 | 544 | let diffY = mainViewRect.width - (mW + MenuConstants.MenuMarginSpace + tvW + MenuConstants.HorizontalMarginSpace + MenuConstants.HorizontalMarginSpace) 545 | if diffY > 0 { 546 | if (tvX + tvW/2) > mainViewRect.width/2 { //right 547 | tvX = tvX + leftClippedSpace 548 | mX = tvX - MenuConstants.MenuMarginSpace - mW 549 | } else { //left 550 | tvX = tvX - rightClippedSpace 551 | mX = tvX + MenuConstants.MenuMarginSpace + tvW 552 | } 553 | } else { 554 | if (tvX + tvW/2) > mainViewRect.width/2 { //right 555 | tvX = mainViewRect.width - MenuConstants.HorizontalMarginSpace - tvW 556 | mX = MenuConstants.HorizontalMarginSpace 557 | } else { //left 558 | tvX = MenuConstants.HorizontalMarginSpace 559 | mX = tvX + tvW + MenuConstants.MenuMarginSpace 560 | } 561 | } 562 | } 563 | else if rightClippedSpace > 0 { 564 | mX = tvX - MenuConstants.MenuMarginSpace - mW 565 | } 566 | else if leftClippedSpace > 0 { 567 | mX = tvX + MenuConstants.MenuMarginSpace + tvW 568 | } 569 | else { 570 | mX = tvX + MenuConstants.MenuMarginSpace + tvW 571 | } 572 | 573 | if mH >= (mainViewRect.height - topMarginSpace - bottomMarginSpace) { 574 | mY = topMarginSpace 575 | mH = mainViewRect.height - topMarginSpace - bottomMarginSpace 576 | } 577 | else if (tvY + mH) <= (mainViewRect.height - bottomMarginSpace) { 578 | mY = tvY 579 | } 580 | else if (tvY + mH) > (mainViewRect.height - bottomMarginSpace){ 581 | mY = tvY - ((tvY + mH) - (mainViewRect.height - bottomMarginSpace)) 582 | } 583 | } 584 | 585 | func updateVerticalTargetedImageViewRect() { 586 | 587 | let bottomClippedSpace = (tvH + MenuConstants.MenuMarginSpace + mH + tvY + bottomMarginSpace) - mainViewRect.height 588 | let topClippedSpace = -(tvY - MenuConstants.MenuMarginSpace - mH - topMarginSpace) 589 | 590 | // not enought space down 591 | 592 | if topClippedSpace > 0, bottomClippedSpace > 0 { 593 | 594 | let diffY = mainViewRect.height - (mH + MenuConstants.MenuMarginSpace + tvH + topMarginSpace + bottomMarginSpace) 595 | if diffY > 0 { 596 | if (tvY + tvH/2) > mainViewRect.height/2 { //down 597 | tvY = tvY + topClippedSpace 598 | mY = tvY - MenuConstants.MenuMarginSpace - mH 599 | } else { //up 600 | tvY = tvY - bottomClippedSpace 601 | mY = tvY + MenuConstants.MenuMarginSpace + tvH 602 | } 603 | } else { 604 | if (tvY + tvH/2) > mainViewRect.height/2 { //down 605 | tvY = mainViewRect.height - bottomMarginSpace - tvH 606 | mY = topMarginSpace 607 | mH = mainViewRect.height - topMarginSpace - bottomMarginSpace - MenuConstants.MenuMarginSpace - tvH 608 | } else { //up 609 | tvY = topMarginSpace 610 | mY = tvY + tvH + MenuConstants.MenuMarginSpace 611 | mH = mainViewRect.height - topMarginSpace - bottomMarginSpace - MenuConstants.MenuMarginSpace - tvH 612 | } 613 | } 614 | } 615 | else if bottomClippedSpace > 0 { 616 | mY = tvY - MenuConstants.MenuMarginSpace - mH 617 | } 618 | else if topClippedSpace > 0 { 619 | mY = tvY + MenuConstants.MenuMarginSpace + tvH 620 | } 621 | else { 622 | mY = tvY + MenuConstants.MenuMarginSpace + tvH 623 | } 624 | 625 | } 626 | 627 | func updateTargetedImageViewRect() { 628 | 629 | self.mainViewRect = self.customView.frame 630 | 631 | let targetedImagePosition = getZoomedTargetedSize() 632 | 633 | tvH = targetedImagePosition.height 634 | tvW = targetedImagePosition.width 635 | tvY = targetedImagePosition.origin.y 636 | tvX = targetedImagePosition.origin.x 637 | mH = menuHeight 638 | mW = MenuConstants.MenuWidth 639 | mY = tvY + MenuConstants.MenuMarginSpace 640 | mX = MenuConstants.HorizontalMarginSpace 641 | 642 | self.fixTargetedImageViewExtrudings() 643 | 644 | let backgroundWidth = mainViewRect.width - (2 * MenuConstants.HorizontalMarginSpace) 645 | let backgroundHeight = mainViewRect.height - topMarginSpace - bottomMarginSpace 646 | 647 | if backgroundHeight > backgroundWidth { 648 | self.updateHorizontalDirection() 649 | self.updateVerticalTargetedImageViewRect() 650 | } 651 | else { 652 | self.updateHorizontalTargetedImageViewRect() 653 | } 654 | 655 | tableView.frame = CGRect(x: 0, y: 0, width: mW, height: mH) 656 | tableView.layoutIfNeeded() 657 | 658 | } 659 | 660 | func updateTargetedImageViewPosition(animated: Bool = true){ 661 | 662 | self.updateTargetedImageViewRect() 663 | 664 | if animated { 665 | UIView.animate(withDuration: 0.2, delay: 0, options: [.layoutSubviews, .curveEaseInOut, .allowUserInteraction]) { [weak self] in 666 | self?.updateTargetedImageViewPositionFrame() 667 | } 668 | } else { 669 | self.updateTargetedImageViewPositionFrame() 670 | } 671 | } 672 | 673 | func updateTargetedImageViewPositionFrame() { 674 | let weakSelf = self 675 | 676 | weakSelf.menuView.alpha = 1 677 | weakSelf.menuView.frame = CGRect( 678 | x: weakSelf.mX, 679 | y: weakSelf.mY, 680 | width: weakSelf.mW, 681 | height: weakSelf.mH 682 | ) 683 | 684 | weakSelf.targetedImageView.frame = CGRect( 685 | x: weakSelf.tvX, 686 | y: weakSelf.tvY, 687 | width: weakSelf.tvW, 688 | height: weakSelf.tvH 689 | ) 690 | 691 | weakSelf.blurEffectView.frame = CGRect( 692 | x: weakSelf.mainViewRect.origin.x, 693 | y: weakSelf.mainViewRect.origin.y, 694 | width: weakSelf.mainViewRect.width, 695 | height: weakSelf.mainViewRect.height 696 | ) 697 | weakSelf.closeButton.frame = CGRect( 698 | x: weakSelf.mainViewRect.origin.x, 699 | y: weakSelf.mainViewRect.origin.y, 700 | width: weakSelf.mainViewRect.width, 701 | height: weakSelf.mainViewRect.height 702 | ) 703 | } 704 | 705 | func updateHorizontalDirection() { 706 | switch MenuConstants.horizontalDirection { 707 | case .left: 708 | mX = MenuConstants.MenuMarginSpace 709 | case .center: 710 | mX = (mainViewRect.width / 2) - (mW / 2) 711 | case .right: 712 | mX = mainViewRect.width - MenuConstants.MenuMarginSpace - mW 713 | } 714 | } 715 | } 716 | 717 | extension ContextMenu : UITableViewDataSource, UITableViewDelegate { 718 | 719 | open func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 720 | return self.items.count 721 | } 722 | 723 | open func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 724 | let cell = tableView.dequeueReusableCell(withIdentifier: "ContextMenuCell", for: indexPath) as! ContextMenuCell 725 | cell.contextMenu = self 726 | cell.tableView = tableView 727 | cell.style = self.MenuConstants 728 | cell.item = self.items[indexPath.row] 729 | cell.setup() 730 | return cell 731 | } 732 | 733 | open func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 734 | let item = self.items[indexPath.row] 735 | if self.onItemTap?(indexPath.row, item) ?? false { 736 | self.closeAllViews() 737 | } 738 | if self.delegate?.contextMenuDidSelect(self, cell: tableView.cellForRow(at: indexPath) as! ContextMenuCell, targetedView: self.viewTargeted, didSelect: self.items[indexPath.row], forRowAt: indexPath.row) ?? false { 739 | self.closeAllViews() 740 | } 741 | } 742 | 743 | open func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) { 744 | self.delegate?.contextMenuDidDeselect(self, cell: tableView.cellForRow(at: indexPath) as! ContextMenuCell, targetedView: self.viewTargeted, didSelect: self.items[indexPath.row], forRowAt: indexPath.row) 745 | } 746 | 747 | open func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { 748 | return MenuConstants.ItemDefaultHeight 749 | } 750 | 751 | open func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat { 752 | return MenuConstants.ItemDefaultHeight 753 | } 754 | 755 | } 756 | 757 | 758 | 759 | @objc class ClosureSleeve: NSObject { 760 | let closure: () -> Void 761 | 762 | init (_ closure: @escaping () -> Void) { 763 | self.closure = closure 764 | } 765 | 766 | @objc func invoke () { 767 | closure() 768 | } 769 | } 770 | 771 | extension UIControl { 772 | func actionHandler(controlEvents control: UIControl.Event = .touchUpInside, ForAction action: @escaping () -> Void) { 773 | let sleeve = ClosureSleeve(action) 774 | addTarget(sleeve, action: #selector(ClosureSleeve.invoke), for: control) 775 | objc_setAssociatedObject(self, "[\(arc4random())]", sleeve, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN) 776 | } 777 | } 778 | -------------------------------------------------------------------------------- /Example/ContextMenuSwift/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 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 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | --------------------------------------------------------------------------------