├── 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 | [](https://www.linkedin.com/in/umerjabbar)
4 | [](https://twitter.com/Umer_Jabbar)
5 | 
6 | 
7 | 
8 | 
9 | [](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 |
--------------------------------------------------------------------------------