DropdownMenu.swift 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416
  1. //
  2. // DropdownMenu.swift
  3. // DropdownMenu
  4. //
  5. // Created by Suric on 16/5/26.
  6. // Copyright © 2016年 teambition. All rights reserved.
  7. //
  8. import UIKit
  9. public protocol DropdownMenuDelegate: class {
  10. func dropdownMenu(_ dropdownMenu: DropdownMenu, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath)
  11. func dropdownMenu(_ dropdownMenu: DropdownMenu, cellForRowAt indexPath: IndexPath) -> UITableViewCell?
  12. func dropdownMenu(_ dropdownMenu: DropdownMenu, didSelectRowAt indexPath: IndexPath)
  13. func dropdownMenu(_ dropdownMenu: DropdownMenu, shouldUpdateSelectionAt indexPath: IndexPath) -> Bool
  14. func dropdownMenuCancel(_ dropdownMenu: DropdownMenu)
  15. func dropdownMenuWillDismiss(_ dropdownMenu: DropdownMenu)
  16. func dropdownMenuWillShow(_ dropdownMenu: DropdownMenu)
  17. }
  18. public extension DropdownMenuDelegate {
  19. func dropdownMenu(_ dropdownMenu: DropdownMenu, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { }
  20. func dropdownMenu(_ dropdownMenu: DropdownMenu, cellForRowAt indexPath: IndexPath) -> UITableViewCell? { return nil }
  21. func dropdownMenu(_ dropdownMenu: DropdownMenu, didSelectRowAt indexPath: IndexPath) { }
  22. func dropdownMenu(_ dropdownMenu: DropdownMenu, shouldUpdateSelectionAt indexPath: IndexPath) -> Bool { return true }
  23. func dropdownMenuCancel(_ dropdownMenu: DropdownMenu) { }
  24. func dropdownMenuWillDismiss(_ dropdownMenu: DropdownMenu) { }
  25. func dropdownMenuWillShow(_ dropdownMenu: DropdownMenu) { }
  26. }
  27. open class DropdownMenu: UIView {
  28. fileprivate weak var navigationController: UINavigationController!
  29. fileprivate var sections: [DropdownSection] = []
  30. fileprivate var selectedIndexPath: [IndexPath]
  31. open var tableView: UITableView!
  32. fileprivate var barCoverView: UIView?
  33. open var isShow = false
  34. fileprivate var addedWindow: UIWindow?
  35. fileprivate var windowRootView: UIView?
  36. fileprivate var topConstraint: NSLayoutConstraint?
  37. fileprivate var navigationBarCoverViewHeightConstraint: NSLayoutConstraint?
  38. fileprivate let iPhoneXPortraitTopOffset: CGFloat = 88.0
  39. fileprivate let portraitTopOffset: CGFloat = 64.0
  40. fileprivate let landscapeTopOffset: CGFloat = 32.0
  41. fileprivate var topLayoutConstraintConstant: CGFloat {
  42. var offset: CGFloat = 0
  43. if !navigationController.isNavigationBarHidden {
  44. offset = navigationController.navigationBar.frame.height + navigationController.navigationBar.frame.origin.y
  45. }
  46. return offset + topOffsetY
  47. }
  48. open weak var delegate: DropdownMenuDelegate?
  49. open var token = ""
  50. open var animateDuration: TimeInterval = 0.25
  51. open var backgroudBeginColor: UIColor = UIColor.black.withAlphaComponent(0)
  52. open var backgroudEndColor = UIColor(white: 0, alpha: 0.4)
  53. open var rowHeight: CGFloat = 50
  54. open var sectionHeaderHeight: CGFloat = 44
  55. open var tableViewHeight: CGFloat = 0
  56. open var defaultBottonMargin: CGFloat = 0
  57. open var topOffsetY: CGFloat = 0
  58. open var textFont: UIFont = UIFont.systemFont(ofSize: 15.0)
  59. open var textColor: UIColor = UIColor(red: 56.0/255.0, green: 56.0/255.0, blue: 56.0/255.0, alpha: 1.0)
  60. open var highlightColor: UIColor = UIColor(red: 3.0/255.0, green: 169.0/255.0, blue: 244.0/255.0, alpha: 1.0)
  61. open var tableViewBackgroundColor: UIColor = UIColor(red: 242.0/255.0, green: 242.0/255.0, blue: 242.0/255.0, alpha: 1.0) {
  62. didSet {
  63. tableView.backgroundColor = tableViewBackgroundColor
  64. }
  65. }
  66. open var separatorStyle: UITableViewCell.SeparatorStyle = .singleLine {
  67. didSet {
  68. tableView.separatorStyle = separatorStyle
  69. }
  70. }
  71. open var tableViewSeperatorColor = UIColor(red: 217.0/255.0, green: 217.0/255.0, blue: 217.0/255.0, alpha: 1.0) {
  72. didSet {
  73. tableView.separatorColor = tableViewSeperatorColor
  74. }
  75. }
  76. open var zeroInsetSeperatorIndexPaths: [IndexPath] = []
  77. open var cellBackgroundColor = UIColor.white
  78. open var displaySelected: Bool = true
  79. open var displaySectionHeader: Bool = false
  80. open var displayNavigationBarCoverView: Bool = true
  81. // section header sytle
  82. open var sectionHeaderStyle: SectionHeaderStyle = SectionHeaderStyle()
  83. required public init?(coder aDecoder: NSCoder) {
  84. fatalError("init(coder:) has not been implemented")
  85. }
  86. public init(navigationController: UINavigationController, items: [DropdownItem], selectedRow: Int = 0) {
  87. self.navigationController = navigationController
  88. self.sections = [DropdownSection(sectionIdentifier: "", items: items)]
  89. self.selectedIndexPath = [IndexPath(row: selectedRow, section: 0)]
  90. super.init(frame: CGRect.zero)
  91. clipsToBounds = true
  92. setupGestureView()
  93. initTableView()
  94. NotificationCenter.default.addObserver(self, selector: #selector(self.updateForOrientationChange(_:)), name: UIApplication.willChangeStatusBarOrientationNotification, object: nil)
  95. }
  96. public init(navigationController: UINavigationController, sections: [DropdownSection], selectedIndexPath: [IndexPath], dispalySectionHeader: Bool = true, sectionHeaderStyle: SectionHeaderStyle = SectionHeaderStyle()) {
  97. self.navigationController = navigationController
  98. self.sections = sections
  99. self.selectedIndexPath = selectedIndexPath
  100. self.displaySectionHeader = dispalySectionHeader
  101. super.init(frame: CGRect.zero)
  102. clipsToBounds = true
  103. setupGestureView()
  104. initTableView()
  105. NotificationCenter.default.addObserver(self, selector: #selector(self.updateForOrientationChange(_:)), name: UIApplication.willChangeStatusBarOrientationNotification, object: nil)
  106. }
  107. deinit {
  108. NotificationCenter.default.removeObserver(self)
  109. }
  110. @objc func updateForOrientationChange(_ nofication: Notification) {
  111. print("UIApplicationWillChangeStatusBarOrientation")
  112. self.hideMenu()
  113. /*
  114. var insetTop: CGFloat = 0
  115. if #available(iOS 11.0, *) {
  116. insetTop = UIApplication.shared.keyWindow!.safeAreaInsets.top
  117. }
  118. if let _ = (nofication as NSNotification).userInfo?[UIApplication.statusBarOrientationUserInfoKey] as? Int {
  119. var topOffset = navigationController.navigationBar.frame.height + insetTop
  120. /*
  121. var topOffset: CGFloat = 0
  122. switch oriention {
  123. case UIInterfaceOrientation.landscapeLeft.rawValue, UIInterfaceOrientation.landscapeRight.rawValue:
  124. if UIDevice.current.userInterfaceIdiom == .phone {
  125. topOffset = navigationController.navigationBar.frame.height + insetTop
  126. } else {
  127. topOffset = navigationController.navigationBar.frame.height + insetTop
  128. }
  129. case UIInterfaceOrientation.portrait.rawValue, UIInterfaceOrientation.portraitUpsideDown.rawValue:
  130. topOffset = navigationController.navigationBar.frame.height + insetTop
  131. default:
  132. break
  133. }
  134. */
  135. topOffset = topOffset + topOffsetY
  136. topConstraint?.constant = topOffset
  137. navigationBarCoverViewHeightConstraint?.constant = topOffset
  138. UIView.animate(withDuration: 0.1, animations: {
  139. self.windowRootView?.layoutIfNeeded()
  140. })
  141. }
  142. */
  143. }
  144. fileprivate func setupGestureView() {
  145. let gestureView = UIView()
  146. gestureView.backgroundColor = UIColor.clear
  147. addSubview(gestureView)
  148. gestureView.translatesAutoresizingMaskIntoConstraints = false
  149. NSLayoutConstraint.activate([NSLayoutConstraint.init(item: gestureView, attribute: .top, relatedBy: .equal, toItem: self, attribute: .top, multiplier: 1.0, constant: 0)])
  150. NSLayoutConstraint.activate([NSLayoutConstraint.init(item: gestureView, attribute: .bottom, relatedBy: .equal, toItem: self, attribute: .bottom, multiplier: 1.0, constant: 0)])
  151. NSLayoutConstraint.activate([NSLayoutConstraint.init(item: gestureView, attribute: .left, relatedBy: .equal, toItem: self, attribute: .left, multiplier: 1.0, constant: 0)])
  152. NSLayoutConstraint.activate([NSLayoutConstraint.init(item: gestureView, attribute: .right, relatedBy: .equal, toItem: self, attribute: .right, multiplier: 1.0, constant: 0)])
  153. gestureView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(hideMenu)))
  154. }
  155. fileprivate func initTableView() {
  156. tableView = UITableView(frame: CGRect.zero, style: .grouped)
  157. tableView.separatorStyle = separatorStyle
  158. tableView.delegate = self
  159. tableView.dataSource = self
  160. tableView.estimatedSectionFooterHeight = 0
  161. tableView.estimatedSectionHeaderHeight = 0
  162. addSubview(tableView)
  163. }
  164. fileprivate func layoutTableView() {
  165. tableView.translatesAutoresizingMaskIntoConstraints = false
  166. tableViewHeight = tableviewHeight()
  167. let maxHeight = navigationController.view.frame.height - topLayoutConstraintConstant - defaultBottonMargin
  168. if tableViewHeight > maxHeight {
  169. if displaySectionHeader {
  170. tableViewHeight = maxHeight
  171. } else {
  172. tableViewHeight = round(maxHeight / rowHeight) * rowHeight
  173. }
  174. }
  175. NSLayoutConstraint.activate([NSLayoutConstraint.init(item: tableView as Any, attribute: .top, relatedBy: .equal, toItem: self, attribute: .top, multiplier: 1.0, constant:0)])
  176. NSLayoutConstraint.activate([NSLayoutConstraint.init(item: tableView as Any, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1.0, constant: tableViewHeight)])
  177. NSLayoutConstraint.activate([NSLayoutConstraint.init(item: tableView as Any, attribute: .left, relatedBy: .equal, toItem: self, attribute: .left, multiplier: 1.0, constant: 0)])
  178. NSLayoutConstraint.activate([NSLayoutConstraint.init(item: tableView as Any, attribute: .right, relatedBy: .equal, toItem: self, attribute: .right, multiplier: 1.0, constant: 0)])
  179. }
  180. fileprivate func setupTopSeperatorView() {
  181. let seperatorView = UIView()
  182. seperatorView.backgroundColor = tableViewSeperatorColor
  183. addSubview(seperatorView)
  184. seperatorView.translatesAutoresizingMaskIntoConstraints = false
  185. NSLayoutConstraint.activate([NSLayoutConstraint.init(item: seperatorView, attribute: .top, relatedBy: .equal, toItem: self, attribute: .top, multiplier: 1.0, constant: 0)])
  186. NSLayoutConstraint.activate([NSLayoutConstraint.init(item: seperatorView, attribute: .left, relatedBy: .equal, toItem: self, attribute: .left, multiplier: 1.0, constant: 0)])
  187. NSLayoutConstraint.activate([NSLayoutConstraint.init(item: seperatorView, attribute: .right, relatedBy: .equal, toItem: self, attribute: .right, multiplier: 1.0, constant: 0)])
  188. NSLayoutConstraint.activate([NSLayoutConstraint.init(item: seperatorView, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1.0, constant: 0.5)])
  189. }
  190. fileprivate func setupNavigationBarCoverView(on view: UIView) {
  191. barCoverView = UIView()
  192. barCoverView?.backgroundColor = UIColor.clear
  193. view.addSubview(barCoverView!)
  194. barCoverView?.translatesAutoresizingMaskIntoConstraints = false
  195. NSLayoutConstraint.activate([NSLayoutConstraint.init(item: barCoverView!, attribute: .top, relatedBy: .equal, toItem: view, attribute: .top, multiplier: 1.0, constant: 0)])
  196. navigationBarCoverViewHeightConstraint = NSLayoutConstraint.init(item: barCoverView!, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1.0, constant: topLayoutConstraintConstant)
  197. NSLayoutConstraint.activate([navigationBarCoverViewHeightConstraint!])
  198. NSLayoutConstraint.activate([NSLayoutConstraint.init(item: barCoverView!, attribute: .left, relatedBy: .equal, toItem: view, attribute: .left, multiplier: 1.0, constant: 0)])
  199. NSLayoutConstraint.activate([NSLayoutConstraint.init(item: barCoverView!, attribute: .right, relatedBy: .equal, toItem: view, attribute: .right, multiplier: 1.0, constant: 0)])
  200. barCoverView?.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(hideMenu)))
  201. }
  202. fileprivate func tableviewHeight() -> CGFloat {
  203. var height: CGFloat = 0
  204. if displaySectionHeader {
  205. height += sectionHeaderHeight * CGFloat(sections.count)
  206. }
  207. for section in sections {
  208. height += CGFloat(section.items.count) * rowHeight
  209. }
  210. return height
  211. }
  212. open func showMenu(isOnNavigaitionView: Bool = false) {
  213. delegate?.dropdownMenuWillShow(self)
  214. if isShow {
  215. hideMenu()
  216. return
  217. }
  218. isShow = true
  219. layoutTableView()
  220. setupTopSeperatorView()
  221. addedWindow = UIWindow(frame: self.navigationController.view.bounds)
  222. addedWindow?.rootViewController = UIViewController()
  223. addedWindow?.isHidden = false
  224. addedWindow?.makeKeyAndVisible()
  225. windowRootView = addedWindow!
  226. if displayNavigationBarCoverView {
  227. setupNavigationBarCoverView(on: windowRootView!)
  228. }
  229. windowRootView?.addSubview(self)
  230. translatesAutoresizingMaskIntoConstraints = false
  231. topConstraint = NSLayoutConstraint.init(item: self, attribute: .top, relatedBy: .equal, toItem: windowRootView, attribute: .top, multiplier: 1.0, constant: topLayoutConstraintConstant)
  232. NSLayoutConstraint.activate([topConstraint!])
  233. NSLayoutConstraint.activate([NSLayoutConstraint.init(item: self, attribute: .bottom, relatedBy: .equal, toItem: windowRootView, attribute: .bottom, multiplier: 1.0, constant: 0)])
  234. NSLayoutConstraint.activate([NSLayoutConstraint.init(item: self, attribute: .left, relatedBy: .equal, toItem: windowRootView, attribute: .left, multiplier: 1.0, constant: 0)])
  235. NSLayoutConstraint.activate([NSLayoutConstraint.init(item: self, attribute: .right, relatedBy: .equal, toItem: windowRootView, attribute: .right, multiplier: 1.0, constant: 0)])
  236. backgroundColor = backgroudBeginColor
  237. self.tableView.frame.origin.y = -self.tableViewHeight
  238. UIView.animate(withDuration: animateDuration, delay: 0, options: UIView.AnimationOptions(rawValue: 7<<16), animations: {
  239. self.backgroundColor = self.backgroudEndColor
  240. self.tableView.frame.origin.y = 0
  241. }, completion: nil)
  242. }
  243. @objc open func hideMenu(isSelectAction: Bool = false) {
  244. delegate?.dropdownMenuWillDismiss(self)
  245. UIView.animate(withDuration: animateDuration, animations: {
  246. self.backgroundColor = self.backgroudBeginColor
  247. self.tableView.frame.origin.y = -self.tableViewHeight
  248. }, completion: { (finished) in
  249. if !isSelectAction {
  250. self.delegate?.dropdownMenuCancel(self)
  251. }
  252. self.barCoverView?.removeFromSuperview()
  253. self.removeFromSuperview()
  254. self.isShow = false
  255. if let _ = self.addedWindow {
  256. self.addedWindow?.isHidden = true
  257. UIApplication.shared.keyWindow?.makeKey()
  258. }
  259. })
  260. }
  261. }
  262. extension DropdownMenu: UITableViewDataSource {
  263. public func numberOfSections(in tableView: UITableView) -> Int {
  264. return sections.count
  265. }
  266. public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
  267. return sections[section].items.count
  268. }
  269. public func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
  270. delegate?.dropdownMenu(self, willDisplay: cell, forRowAt: indexPath)
  271. }
  272. public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
  273. if let customCell = delegate?.dropdownMenu(self, cellForRowAt: indexPath) {
  274. return customCell
  275. }
  276. let item = sections[indexPath.section].items[indexPath.row]
  277. let cell = UITableViewCell(style: .default, reuseIdentifier: "dropdownMenuCell")
  278. switch item.style {
  279. case .default:
  280. cell.textLabel?.textColor = textColor
  281. if let image = item.image {
  282. cell.imageView?.image = image
  283. }
  284. case .highlight:
  285. cell.textLabel?.textColor = highlightColor
  286. if let image = item.image {
  287. let highlightImage = image.withRenderingMode(.alwaysTemplate)
  288. cell.imageView?.image = highlightImage
  289. cell.imageView?.tintColor = highlightColor
  290. }
  291. }
  292. cell.textLabel?.text = item.title
  293. cell.textLabel?.font = textFont
  294. cell.tintColor = highlightColor
  295. cell.backgroundColor = cellBackgroundColor
  296. if displaySelected && selectedIndexPath.contains(indexPath) { //indexPath == selectedIndexPath {
  297. cell.accessoryType = .checkmark
  298. } else {
  299. cell.accessoryType = .none
  300. }
  301. if let accesoryImage = item.accessoryImage {
  302. cell.accessoryView = UIImageView(image: accesoryImage)
  303. }
  304. if zeroInsetSeperatorIndexPaths.contains(indexPath) {
  305. cell.separatorInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
  306. } else {
  307. cell.separatorInset = UIEdgeInsets(top: 0, left: tableView.layoutMargins.left, bottom: 0, right: 0)
  308. }
  309. return cell
  310. }
  311. public func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
  312. return displaySectionHeader ? sections[section].sectionIdentifier : nil
  313. }
  314. }
  315. extension DropdownMenu: UITableViewDelegate {
  316. public func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
  317. return rowHeight
  318. }
  319. public func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
  320. return displaySectionHeader ? sectionHeaderHeight : CGFloat.leastNormalMagnitude
  321. }
  322. public func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
  323. return CGFloat.leastNormalMagnitude
  324. }
  325. public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
  326. /*
  327. let shouldUpdateSelection = delegate?.dropdownMenu(self, shouldUpdateSelectionAt: indexPath) ?? true
  328. if displaySelected && shouldUpdateSelection {
  329. let item = sections[indexPath.section].items[indexPath.row]
  330. if item.accessoryImage == nil {
  331. let previousSelectedcell = tableView.cellForRow(at: selectedIndexPath)
  332. previousSelectedcell?.accessoryType = .none
  333. selectedIndexPath = indexPath
  334. let cell = tableView.cellForRow(at: indexPath)
  335. cell?.accessoryType = .checkmark
  336. }
  337. }
  338. */
  339. tableView.deselectRow(at: indexPath, animated: true)
  340. hideMenu(isSelectAction: true)
  341. delegate?.dropdownMenu(self, didSelectRowAt: indexPath)
  342. }
  343. public func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
  344. let sectionHeader = SectionHeader(style: sectionHeaderStyle)
  345. sectionHeader.titleLabel.text = sections[section].sectionIdentifier
  346. return sectionHeader
  347. }
  348. }