// // DropdownMenu.swift // DropdownMenu // // Created by Suric on 16/5/26. // Copyright © 2016年 teambition. All rights reserved. // import UIKit public protocol DropdownMenuDelegate: class { func dropdownMenu(_ dropdownMenu: DropdownMenu, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) func dropdownMenu(_ dropdownMenu: DropdownMenu, cellForRowAt indexPath: IndexPath) -> UITableViewCell? func dropdownMenu(_ dropdownMenu: DropdownMenu, didSelectRowAt indexPath: IndexPath) func dropdownMenu(_ dropdownMenu: DropdownMenu, shouldUpdateSelectionAt indexPath: IndexPath) -> Bool func dropdownMenuCancel(_ dropdownMenu: DropdownMenu) func dropdownMenuWillDismiss(_ dropdownMenu: DropdownMenu) func dropdownMenuWillShow(_ dropdownMenu: DropdownMenu) } public extension DropdownMenuDelegate { func dropdownMenu(_ dropdownMenu: DropdownMenu, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { } func dropdownMenu(_ dropdownMenu: DropdownMenu, cellForRowAt indexPath: IndexPath) -> UITableViewCell? { return nil } func dropdownMenu(_ dropdownMenu: DropdownMenu, didSelectRowAt indexPath: IndexPath) { } func dropdownMenu(_ dropdownMenu: DropdownMenu, shouldUpdateSelectionAt indexPath: IndexPath) -> Bool { return true } func dropdownMenuCancel(_ dropdownMenu: DropdownMenu) { } func dropdownMenuWillDismiss(_ dropdownMenu: DropdownMenu) { } func dropdownMenuWillShow(_ dropdownMenu: DropdownMenu) { } } open class DropdownMenu: UIView { fileprivate weak var navigationController: UINavigationController! fileprivate var sections: [DropdownSection] = [] fileprivate var selectedIndexPath: [IndexPath] open var tableView: UITableView! fileprivate var barCoverView: UIView? open var isShow = false fileprivate var addedWindow: UIWindow? fileprivate var windowRootView: UIView? fileprivate var topConstraint: NSLayoutConstraint? fileprivate var navigationBarCoverViewHeightConstraint: NSLayoutConstraint? fileprivate let iPhoneXPortraitTopOffset: CGFloat = 88.0 fileprivate let portraitTopOffset: CGFloat = 64.0 fileprivate let landscapeTopOffset: CGFloat = 32.0 fileprivate var topLayoutConstraintConstant: CGFloat { var offset: CGFloat = 0 if !navigationController.isNavigationBarHidden { offset = navigationController.navigationBar.frame.height + navigationController.navigationBar.frame.origin.y } return offset + topOffsetY } open weak var delegate: DropdownMenuDelegate? open var token = "" open var animateDuration: TimeInterval = 0.25 open var backgroudBeginColor: UIColor = UIColor.black.withAlphaComponent(0) open var backgroudEndColor = UIColor(white: 0, alpha: 0.4) open var rowHeight: CGFloat = 50 open var sectionHeaderHeight: CGFloat = 44 open var tableViewHeight: CGFloat = 0 open var defaultBottonMargin: CGFloat = 0 open var topOffsetY: CGFloat = 0 open var textFont: UIFont = UIFont.systemFont(ofSize: 15.0) open var textColor: UIColor = UIColor(red: 56.0/255.0, green: 56.0/255.0, blue: 56.0/255.0, alpha: 1.0) open var highlightColor: UIColor = UIColor(red: 3.0/255.0, green: 169.0/255.0, blue: 244.0/255.0, alpha: 1.0) open var tableViewBackgroundColor: UIColor = UIColor(red: 242.0/255.0, green: 242.0/255.0, blue: 242.0/255.0, alpha: 1.0) { didSet { tableView.backgroundColor = tableViewBackgroundColor } } open var separatorStyle: UITableViewCell.SeparatorStyle = .singleLine { didSet { tableView.separatorStyle = separatorStyle } } open var tableViewSeperatorColor = UIColor(red: 217.0/255.0, green: 217.0/255.0, blue: 217.0/255.0, alpha: 1.0) { didSet { tableView.separatorColor = tableViewSeperatorColor } } open var zeroInsetSeperatorIndexPaths: [IndexPath] = [] open var cellBackgroundColor = UIColor.white open var displaySelected: Bool = true open var displaySectionHeader: Bool = false open var displayNavigationBarCoverView: Bool = true // section header sytle open var sectionHeaderStyle: SectionHeaderStyle = SectionHeaderStyle() required public init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } public init(navigationController: UINavigationController, items: [DropdownItem], selectedRow: Int = 0) { self.navigationController = navigationController self.sections = [DropdownSection(sectionIdentifier: "", items: items)] self.selectedIndexPath = [IndexPath(row: selectedRow, section: 0)] super.init(frame: CGRect.zero) clipsToBounds = true setupGestureView() initTableView() NotificationCenter.default.addObserver(self, selector: #selector(self.updateForOrientationChange(_:)), name: UIApplication.willChangeStatusBarOrientationNotification, object: nil) } public init(navigationController: UINavigationController, sections: [DropdownSection], selectedIndexPath: [IndexPath], dispalySectionHeader: Bool = true, sectionHeaderStyle: SectionHeaderStyle = SectionHeaderStyle()) { self.navigationController = navigationController self.sections = sections self.selectedIndexPath = selectedIndexPath self.displaySectionHeader = dispalySectionHeader super.init(frame: CGRect.zero) clipsToBounds = true setupGestureView() initTableView() NotificationCenter.default.addObserver(self, selector: #selector(self.updateForOrientationChange(_:)), name: UIApplication.willChangeStatusBarOrientationNotification, object: nil) } deinit { NotificationCenter.default.removeObserver(self) } @objc func updateForOrientationChange(_ nofication: Notification) { print("UIApplicationWillChangeStatusBarOrientation") self.hideMenu() /* var insetTop: CGFloat = 0 if #available(iOS 11.0, *) { insetTop = UIApplication.shared.keyWindow!.safeAreaInsets.top } if let _ = (nofication as NSNotification).userInfo?[UIApplication.statusBarOrientationUserInfoKey] as? Int { var topOffset = navigationController.navigationBar.frame.height + insetTop /* var topOffset: CGFloat = 0 switch oriention { case UIInterfaceOrientation.landscapeLeft.rawValue, UIInterfaceOrientation.landscapeRight.rawValue: if UIDevice.current.userInterfaceIdiom == .phone { topOffset = navigationController.navigationBar.frame.height + insetTop } else { topOffset = navigationController.navigationBar.frame.height + insetTop } case UIInterfaceOrientation.portrait.rawValue, UIInterfaceOrientation.portraitUpsideDown.rawValue: topOffset = navigationController.navigationBar.frame.height + insetTop default: break } */ topOffset = topOffset + topOffsetY topConstraint?.constant = topOffset navigationBarCoverViewHeightConstraint?.constant = topOffset UIView.animate(withDuration: 0.1, animations: { self.windowRootView?.layoutIfNeeded() }) } */ } fileprivate func setupGestureView() { let gestureView = UIView() gestureView.backgroundColor = UIColor.clear addSubview(gestureView) gestureView.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([NSLayoutConstraint.init(item: gestureView, attribute: .top, relatedBy: .equal, toItem: self, attribute: .top, multiplier: 1.0, constant: 0)]) NSLayoutConstraint.activate([NSLayoutConstraint.init(item: gestureView, attribute: .bottom, relatedBy: .equal, toItem: self, attribute: .bottom, multiplier: 1.0, constant: 0)]) NSLayoutConstraint.activate([NSLayoutConstraint.init(item: gestureView, attribute: .left, relatedBy: .equal, toItem: self, attribute: .left, multiplier: 1.0, constant: 0)]) NSLayoutConstraint.activate([NSLayoutConstraint.init(item: gestureView, attribute: .right, relatedBy: .equal, toItem: self, attribute: .right, multiplier: 1.0, constant: 0)]) gestureView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(hideMenu))) } fileprivate func initTableView() { tableView = UITableView(frame: CGRect.zero, style: .grouped) tableView.separatorStyle = separatorStyle tableView.delegate = self tableView.dataSource = self tableView.estimatedSectionFooterHeight = 0 tableView.estimatedSectionHeaderHeight = 0 addSubview(tableView) } fileprivate func layoutTableView() { tableView.translatesAutoresizingMaskIntoConstraints = false tableViewHeight = tableviewHeight() let maxHeight = navigationController.view.frame.height - topLayoutConstraintConstant - defaultBottonMargin if tableViewHeight > maxHeight { if displaySectionHeader { tableViewHeight = maxHeight } else { tableViewHeight = round(maxHeight / rowHeight) * rowHeight } } NSLayoutConstraint.activate([NSLayoutConstraint.init(item: tableView as Any, attribute: .top, relatedBy: .equal, toItem: self, attribute: .top, multiplier: 1.0, constant:0)]) NSLayoutConstraint.activate([NSLayoutConstraint.init(item: tableView as Any, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1.0, constant: tableViewHeight)]) NSLayoutConstraint.activate([NSLayoutConstraint.init(item: tableView as Any, attribute: .left, relatedBy: .equal, toItem: self, attribute: .left, multiplier: 1.0, constant: 0)]) NSLayoutConstraint.activate([NSLayoutConstraint.init(item: tableView as Any, attribute: .right, relatedBy: .equal, toItem: self, attribute: .right, multiplier: 1.0, constant: 0)]) } fileprivate func setupTopSeperatorView() { let seperatorView = UIView() seperatorView.backgroundColor = tableViewSeperatorColor addSubview(seperatorView) seperatorView.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([NSLayoutConstraint.init(item: seperatorView, attribute: .top, relatedBy: .equal, toItem: self, attribute: .top, multiplier: 1.0, constant: 0)]) NSLayoutConstraint.activate([NSLayoutConstraint.init(item: seperatorView, attribute: .left, relatedBy: .equal, toItem: self, attribute: .left, multiplier: 1.0, constant: 0)]) NSLayoutConstraint.activate([NSLayoutConstraint.init(item: seperatorView, attribute: .right, relatedBy: .equal, toItem: self, attribute: .right, multiplier: 1.0, constant: 0)]) NSLayoutConstraint.activate([NSLayoutConstraint.init(item: seperatorView, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1.0, constant: 0.5)]) } fileprivate func setupNavigationBarCoverView(on view: UIView) { barCoverView = UIView() barCoverView?.backgroundColor = UIColor.clear view.addSubview(barCoverView!) barCoverView?.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([NSLayoutConstraint.init(item: barCoverView!, attribute: .top, relatedBy: .equal, toItem: view, attribute: .top, multiplier: 1.0, constant: 0)]) navigationBarCoverViewHeightConstraint = NSLayoutConstraint.init(item: barCoverView!, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1.0, constant: topLayoutConstraintConstant) NSLayoutConstraint.activate([navigationBarCoverViewHeightConstraint!]) NSLayoutConstraint.activate([NSLayoutConstraint.init(item: barCoverView!, attribute: .left, relatedBy: .equal, toItem: view, attribute: .left, multiplier: 1.0, constant: 0)]) NSLayoutConstraint.activate([NSLayoutConstraint.init(item: barCoverView!, attribute: .right, relatedBy: .equal, toItem: view, attribute: .right, multiplier: 1.0, constant: 0)]) barCoverView?.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(hideMenu))) } fileprivate func tableviewHeight() -> CGFloat { var height: CGFloat = 0 if displaySectionHeader { height += sectionHeaderHeight * CGFloat(sections.count) } for section in sections { height += CGFloat(section.items.count) * rowHeight } return height } open func showMenu(isOnNavigaitionView: Bool = false) { delegate?.dropdownMenuWillShow(self) if isShow { hideMenu() return } isShow = true layoutTableView() setupTopSeperatorView() addedWindow = UIWindow(frame: self.navigationController.view.bounds) addedWindow?.rootViewController = UIViewController() addedWindow?.isHidden = false addedWindow?.makeKeyAndVisible() windowRootView = addedWindow! if displayNavigationBarCoverView { setupNavigationBarCoverView(on: windowRootView!) } windowRootView?.addSubview(self) translatesAutoresizingMaskIntoConstraints = false topConstraint = NSLayoutConstraint.init(item: self, attribute: .top, relatedBy: .equal, toItem: windowRootView, attribute: .top, multiplier: 1.0, constant: topLayoutConstraintConstant) NSLayoutConstraint.activate([topConstraint!]) NSLayoutConstraint.activate([NSLayoutConstraint.init(item: self, attribute: .bottom, relatedBy: .equal, toItem: windowRootView, attribute: .bottom, multiplier: 1.0, constant: 0)]) NSLayoutConstraint.activate([NSLayoutConstraint.init(item: self, attribute: .left, relatedBy: .equal, toItem: windowRootView, attribute: .left, multiplier: 1.0, constant: 0)]) NSLayoutConstraint.activate([NSLayoutConstraint.init(item: self, attribute: .right, relatedBy: .equal, toItem: windowRootView, attribute: .right, multiplier: 1.0, constant: 0)]) backgroundColor = backgroudBeginColor self.tableView.frame.origin.y = -self.tableViewHeight UIView.animate(withDuration: animateDuration, delay: 0, options: UIView.AnimationOptions(rawValue: 7<<16), animations: { self.backgroundColor = self.backgroudEndColor self.tableView.frame.origin.y = 0 }, completion: nil) } @objc open func hideMenu(isSelectAction: Bool = false) { delegate?.dropdownMenuWillDismiss(self) UIView.animate(withDuration: animateDuration, animations: { self.backgroundColor = self.backgroudBeginColor self.tableView.frame.origin.y = -self.tableViewHeight }, completion: { (finished) in if !isSelectAction { self.delegate?.dropdownMenuCancel(self) } self.barCoverView?.removeFromSuperview() self.removeFromSuperview() self.isShow = false if let _ = self.addedWindow { self.addedWindow?.isHidden = true UIApplication.shared.keyWindow?.makeKey() } }) } } extension DropdownMenu: UITableViewDataSource { public func numberOfSections(in tableView: UITableView) -> Int { return sections.count } public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return sections[section].items.count } public func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { delegate?.dropdownMenu(self, willDisplay: cell, forRowAt: indexPath) } public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { if let customCell = delegate?.dropdownMenu(self, cellForRowAt: indexPath) { return customCell } let item = sections[indexPath.section].items[indexPath.row] let cell = UITableViewCell(style: .default, reuseIdentifier: "dropdownMenuCell") switch item.style { case .default: cell.textLabel?.textColor = textColor if let image = item.image { cell.imageView?.image = image } case .highlight: cell.textLabel?.textColor = highlightColor if let image = item.image { let highlightImage = image.withRenderingMode(.alwaysTemplate) cell.imageView?.image = highlightImage cell.imageView?.tintColor = highlightColor } } cell.textLabel?.text = item.title cell.textLabel?.font = textFont cell.tintColor = highlightColor cell.backgroundColor = cellBackgroundColor if displaySelected && selectedIndexPath.contains(indexPath) { //indexPath == selectedIndexPath { cell.accessoryType = .checkmark } else { cell.accessoryType = .none } if let accesoryImage = item.accessoryImage { cell.accessoryView = UIImageView(image: accesoryImage) } if zeroInsetSeperatorIndexPaths.contains(indexPath) { cell.separatorInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) } else { cell.separatorInset = UIEdgeInsets(top: 0, left: tableView.layoutMargins.left, bottom: 0, right: 0) } return cell } public func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { return displaySectionHeader ? sections[section].sectionIdentifier : nil } } extension DropdownMenu: UITableViewDelegate { public func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { return rowHeight } public func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { return displaySectionHeader ? sectionHeaderHeight : CGFloat.leastNormalMagnitude } public func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat { return CGFloat.leastNormalMagnitude } public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { /* let shouldUpdateSelection = delegate?.dropdownMenu(self, shouldUpdateSelectionAt: indexPath) ?? true if displaySelected && shouldUpdateSelection { let item = sections[indexPath.section].items[indexPath.row] if item.accessoryImage == nil { let previousSelectedcell = tableView.cellForRow(at: selectedIndexPath) previousSelectedcell?.accessoryType = .none selectedIndexPath = indexPath let cell = tableView.cellForRow(at: indexPath) cell?.accessoryType = .checkmark } } */ tableView.deselectRow(at: indexPath, animated: true) hideMenu(isSelectAction: true) delegate?.dropdownMenu(self, didSelectRowAt: indexPath) } public func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { let sectionHeader = SectionHeader(style: sectionHeaderStyle) sectionHeader.titleLabel.text = sections[section].sectionIdentifier return sectionHeader } }