DropdownMenu.swift 19 KB

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