Skip to content

Instantly share code, notes, and snippets.

@Coder-ACJHP
Created April 23, 2019 12:51
Show Gist options
  • Save Coder-ACJHP/c2ef06058d1caf741c146bfff5d96795 to your computer and use it in GitHub Desktop.
Save Coder-ACJHP/c2ef06058d1caf741c146bfff5d96795 to your computer and use it in GitHub Desktop.
Custom contextual menu appears from bottom to top like `UIAlertControllerStyleActionSheet`
//
// ContextMenu.swift
//
// Created by Onur Işık on 23.04.2019.
// Copyright © 2019 Coder ACJHP. All rights reserved.
//
import UIKit
protocol UIContextMenuDelegate: AnyObject {
func contextMenu(_ contextMenu: UICContextMenu?, didSelectAspectRatio ratio: CGFloat)
func contextMenu(_ contextMenu: UICContextMenu?, didCancelled: Bool)
}
class UICContextMenu: UIView, UIGestureRecognizerDelegate, UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
private var shadowView: UIView!
private var contextMenu: UIView!
private let posiY: CGFloat = 50
private let contextMenuWidth: CGFloat = 280
private let contextMenuHeight: CGFloat = 340
private lazy var collectionView: UICollectionView = {
let flowLayout = UICollectionViewFlowLayout()
flowLayout.scrollDirection = .vertical
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: flowLayout)
collectionView.register(AspectRatioCell.self, forCellWithReuseIdentifier: cellId)
collectionView.register(ReusableHeaderView.self,
forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader,
withReuseIdentifier: headerId)
collectionView.delegate = self
collectionView.dataSource = self
collectionView.showsVerticalScrollIndicator = false
collectionView.showsHorizontalScrollIndicator = false
collectionView.frame = CGRect(x: 0, y: 0, width: contextMenuWidth, height: contextMenuHeight)
collectionView.backgroundColor = .clear
return collectionView
}()
private lazy var keyWindowFrame: CGRect = {
if let keyWindow = UIApplication.shared.keyWindow {
return keyWindow.frame
}
return .zero
}()
private lazy var ExpandedRect: CGRect = {
return CGRect(x: keyWindowFrame.width / 2 - contextMenuWidth / 2,
y: keyWindowFrame.height - contextMenuHeight - posiY,
width: contextMenuWidth,
height: contextMenuHeight)
}()
private lazy var CollapsedRect: CGRect = {
return CGRect(x: keyWindowFrame.width / 2 - contextMenuWidth / 2,
y: keyWindowFrame.height - posiY,
width: contextMenuWidth,
height: 0)
}()
private let cellId = "customCellId"
private let headerId = "customHeaderId"
private var aspectRatiosList = [
AspectRatio(kind: .SixtyNine, name: Ratios.SixtyNine.rawValue),
AspectRatio(kind: .NineSixty, name: Ratios.NineSixty.rawValue),
AspectRatio(kind: .FourThree, name: Ratios.FourThree.rawValue),
AspectRatio(kind: .ThreeFour, name: Ratios.ThreeFour.rawValue),
AspectRatio(kind: .OneOne, name: Ratios.OneOne.rawValue),
AspectRatio(kind: .Cancel, name: Ratios.Cancel.rawValue)
]
public weak var delegate: UIContextMenuDelegate?
override init(frame: CGRect) {
super.init(frame: frame)
guard let keyWindow = UIApplication.shared.keyWindow else { return }
shadowView = UIView(frame: keyWindow.bounds)
shadowView.tag = 1000
shadowView!.backgroundColor = UIColor.init(white: 0, alpha: 0.5)
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTap(_:)))
tapGesture.delegate = self
shadowView!.addGestureRecognizer(tapGesture)
keyWindow.addSubview(shadowView!)
contextMenu = UIView(frame: CGRect(x: 0, y: keyWindowFrame.height, width: contextMenuWidth, height: 0))
contextMenu.backgroundColor = .white
contextMenu.center.x = keyWindow.center.x
contextMenu.layer.cornerRadius = 10
contextMenu.layer.masksToBounds = true
shadowView.addSubview(contextMenu)
contextMenu.addSubview(collectionView)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public func showContextMenu() {
UIView.animate(withDuration: 0.5,
delay: 0,
usingSpringWithDamping: 1,
initialSpringVelocity: 1,
options: .curveEaseIn,
animations: {
self.contextMenu.frame = self.ExpandedRect
}, completion: nil)
}
@objc private func handleTap(_ gestureRecognizer: UITapGestureRecognizer) {
guard let tappedView = gestureRecognizer.view else { return }
if tappedView.tag == shadowView.tag {
removeContextMenu()
}
}
fileprivate func removeContextMenu() {
UIView.transition(with: shadowView,
duration: 0.3,
options: .curveEaseOut,
animations: {
self.contextMenu.frame = self.CollapsedRect
self.shadowView.alpha = 0
}) { (_) in
self.shadowView.removeFromSuperview()
}
}
// Best comparition way to detect tapped view (for subviews)
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
return touch.view == gestureRecognizer.view
}
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return aspectRatiosList.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellId, for: indexPath) as! AspectRatioCell
cell.aspectRatio = aspectRatiosList[indexPath.item]
return cell
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let selectedCell = collectionView.cellForItem(at: indexPath) as! AspectRatioCell
UIView.transition(with: self,
duration: 0.15,
options: .transitionCrossDissolve,
animations: {
selectedCell.nameLabel.textColor = .white
selectedCell.backgroundColor = .lightGray
}, completion: {[weak self] (_) in
switch selectedCell.aspectRatio.kind {
case .SixtyNine?:
self?.delegate?.contextMenu(self, didSelectAspectRatio: 16/9)
case .NineSixty?:
self?.delegate?.contextMenu(self, didSelectAspectRatio: 9/16)
case .FourThree?:
self?.delegate?.contextMenu(self, didSelectAspectRatio: 4/3)
case .ThreeFour?:
self?.delegate?.contextMenu(self, didSelectAspectRatio: 3/4)
case .OneOne?:
self?.delegate?.contextMenu(self, didSelectAspectRatio: 1/1)
default:
self?.delegate?.contextMenu(self, didCancelled: true)
}
self?.removeContextMenu()
})
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
return 0
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
return 0
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: contextMenuWidth, height: contextMenuHeight / CGFloat(aspectRatiosList.count) - 12)
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
return .init(top: 10, left: 0, bottom: 0, right: 0)
}
func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
let headerView = collectionView.dequeueReusableSupplementaryView(ofKind: UICollectionView.elementKindSectionHeader,
withReuseIdentifier: headerId, for: indexPath)
return headerView
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
return .init(width: contextMenuWidth, height: 60)
}
}
enum Ratios: String {
case SixtyNine = "16 : 9"
case NineSixty = "9 : 16"
case FourThree = "4 : 3"
case ThreeFour = "3 : 4"
case OneOne = "1 : 1"
case Cancel = "CANCEL"
}
struct AspectRatio {
var kind: Ratios!
var name: String?
}
class ReusableHeaderView: UICollectionReusableView {
private var label = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
label = UILabel(frame: .zero)
label.textColor = .black
label.text = "Choose aspect ratio for crop"
label.textAlignment = .center
label.numberOfLines = 0
label.font = UIFont.boldSystemFont(ofSize: 17)
label.translatesAutoresizingMaskIntoConstraints = false
addSubview(label)
let borderLine = UIView()
borderLine.backgroundColor = .lightGray
borderLine.translatesAutoresizingMaskIntoConstraints = false
addSubview(borderLine)
NSLayoutConstraint.activate([
label.leadingAnchor.constraint(equalTo: leadingAnchor),
label.trailingAnchor.constraint(equalTo: trailingAnchor),
label.topAnchor.constraint(equalTo: topAnchor),
label.bottomAnchor.constraint(equalTo: bottomAnchor),
borderLine.leadingAnchor.constraint(equalTo: leadingAnchor),
borderLine.trailingAnchor.constraint(equalTo: trailingAnchor),
borderLine.heightAnchor.constraint(equalToConstant: 1),
borderLine.bottomAnchor.constraint(equalTo: bottomAnchor)
])
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
class AspectRatioCell: UICollectionViewCell {
var aspectRatio: AspectRatio! {
didSet {
nameLabel.text = aspectRatio.name
if aspectRatio.name == Ratios.Cancel.rawValue {
nameLabel.textColor = .red
}
}
}
var nameLabel = UILabel()
private var seperatorLine = UIView()
private lazy var darkestGray = UIColor.init(white: 0.30, alpha: 1.0)
override init(frame: CGRect) {
super.init(frame: frame)
nameLabel = UILabel(frame: .zero)
nameLabel.textColor = .black
nameLabel.textAlignment = .center
nameLabel.font = UIFont.boldSystemFont(ofSize: 20)
nameLabel.translatesAutoresizingMaskIntoConstraints = false
addSubview(nameLabel)
seperatorLine.backgroundColor = .lightGray
seperatorLine.translatesAutoresizingMaskIntoConstraints = false
addSubview(seperatorLine)
NSLayoutConstraint.activate([
nameLabel.leadingAnchor.constraint(equalTo: leadingAnchor),
nameLabel.trailingAnchor.constraint(equalTo: trailingAnchor),
nameLabel.topAnchor.constraint(equalTo: topAnchor),
nameLabel.bottomAnchor.constraint(equalTo: bottomAnchor),
seperatorLine.leadingAnchor.constraint(equalTo: leadingAnchor),
seperatorLine.trailingAnchor.constraint(equalTo: trailingAnchor),
seperatorLine.heightAnchor.constraint(equalToConstant: 0.5),
seperatorLine.bottomAnchor.constraint(equalTo: bottomAnchor)
])
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
@Coder-ACJHP
Copy link
Author

Coder-ACJHP commented Apr 23, 2019

How to use?

//  Created by Onur Işık on 23.04.2019.
//  Copyright © 2019 Coder ACJHP. All rights reserved.
//

import UIKit

class TestViewController: UIViewController, UIContextMenuDelegate {

    private var contextMenu: UICContextMenu!
    
    override func viewDidLoad() {
        super.viewDidLoad()

        contextMenu = UICContextMenu(frame: .zero)
        contextMenu.delegate = self
        contextMenu.showContextMenu()
    }
    
    func contextMenu(_ contextMenu: UICContextMenu?, didSelectAspectRatio ratio: CGFloat) {
        print(ratio)
    }
    
    func contextMenu(_ contextMenu: UICContextMenu?, didCancelled: Bool) {
        print(didCancelled)
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment