Instantly share code, notes, and snippets.
Created
April 23, 2019 12:51
-
Star
0
(0)
You must be signed in to star a gist -
Fork
0
(0)
You must be signed in to fork a gist
-
Save Coder-ACJHP/c2ef06058d1caf741c146bfff5d96795 to your computer and use it in GitHub Desktop.
Custom contextual menu appears from bottom to top like `UIAlertControllerStyleActionSheet`
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// 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") | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
How to use?