ESTabBar.swift 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439
  1. //
  2. // ESTabBar.swift
  3. //
  4. // Created by Vincent Li on 2017/2/8.
  5. // Copyright (c) 2013-2018 ESTabBarController (https://github.com/eggswift/ESTabBarController)
  6. //
  7. // Permission is hereby granted, free of charge, to any person obtaining a copy
  8. // of this software and associated documentation files (the "Software"), to deal
  9. // in the Software without restriction, including without limitation the rights
  10. // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  11. // copies of the Software, and to permit persons to whom the Software is
  12. // furnished to do so, subject to the following conditions:
  13. //
  14. // The above copyright notice and this permission notice shall be included in
  15. // all copies or substantial portions of the Software.
  16. //
  17. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  18. // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  19. // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  20. // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  21. // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  22. // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  23. // THE SOFTWARE.
  24. //
  25. import UIKit
  26. /// 对原生的UITabBarItemPositioning进行扩展,通过UITabBarItemPositioning设置时,系统会自动添加insets,这使得添加背景样式的需求变得不可能实现。ESTabBarItemPositioning完全支持原有的item Position 类型,除此之外还支持完全fill模式。
  27. ///
  28. /// - automatic: UITabBarItemPositioning.automatic
  29. /// - fill: UITabBarItemPositioning.fill
  30. /// - centered: UITabBarItemPositioning.centered
  31. /// - fillExcludeSeparator: 完全fill模式,布局不覆盖tabBar顶部分割线
  32. /// - fillIncludeSeparator: 完全fill模式,布局覆盖tabBar顶部分割线
  33. public enum ESTabBarItemPositioning : Int {
  34. case automatic
  35. case fill
  36. case centered
  37. case fillExcludeSeparator
  38. case fillIncludeSeparator
  39. }
  40. /// 对UITabBarDelegate进行扩展,以支持UITabBarControllerDelegate的相关方法桥接
  41. internal protocol ESTabBarDelegate: NSObjectProtocol {
  42. /// 当前item是否支持选中
  43. ///
  44. /// - Parameters:
  45. /// - tabBar: tabBar
  46. /// - item: 当前item
  47. /// - Returns: Bool
  48. func tabBar(_ tabBar: UITabBar, shouldSelect item: UITabBarItem) -> Bool
  49. /// 当前item是否需要被劫持
  50. ///
  51. /// - Parameters:
  52. /// - tabBar: tabBar
  53. /// - item: 当前item
  54. /// - Returns: Bool
  55. func tabBar(_ tabBar: UITabBar, shouldHijack item: UITabBarItem) -> Bool
  56. /// 当前item的点击被劫持
  57. ///
  58. /// - Parameters:
  59. /// - tabBar: tabBar
  60. /// - item: 当前item
  61. /// - Returns: Void
  62. func tabBar(_ tabBar: UITabBar, didHijack item: UITabBarItem)
  63. }
  64. /// ESTabBar是高度自定义的UITabBar子类,通过添加UIControl的方式实现自定义tabBarItem的效果。目前支持tabBar的大部分属性的设置,例如delegate,items,selectedImge,itemPositioning,itemWidth,itemSpacing等,以后会更加细致的优化tabBar原有属性的设置效果。
  65. open class ESTabBar: UITabBar {
  66. internal weak var customDelegate: ESTabBarDelegate?
  67. /// tabBar中items布局偏移量
  68. public var itemEdgeInsets = UIEdgeInsets.zero
  69. /// 是否设置为自定义布局方式,默认为空。如果为空,则通过itemPositioning属性来设置。如果不为空则忽略itemPositioning,所以当tabBar的itemCustomPositioning属性不为空时,如果想改变布局规则,请设置此属性而非itemPositioning。
  70. public var itemCustomPositioning: ESTabBarItemPositioning? {
  71. didSet {
  72. if let itemCustomPositioning = itemCustomPositioning {
  73. switch itemCustomPositioning {
  74. case .fill:
  75. itemPositioning = .fill
  76. case .automatic:
  77. itemPositioning = .automatic
  78. case .centered:
  79. itemPositioning = .centered
  80. default:
  81. break
  82. }
  83. }
  84. self.reload()
  85. }
  86. }
  87. /// tabBar自定义item的容器view
  88. internal var containers = [ESTabBarItemContainer]()
  89. /// 缓存当前tabBarController用来判断是否存在"More"Tab
  90. internal weak var tabBarController: UITabBarController?
  91. /// 自定义'More'按钮样式,继承自ESTabBarItemContentView
  92. open var moreContentView: ESTabBarItemContentView? = ESTabBarItemMoreContentView.init() {
  93. didSet { self.reload() }
  94. }
  95. open override var items: [UITabBarItem]? {
  96. didSet {
  97. self.reload()
  98. }
  99. }
  100. open var isEditing: Bool = false {
  101. didSet {
  102. if oldValue != isEditing {
  103. self.updateLayout()
  104. }
  105. }
  106. }
  107. open override func setItems(_ items: [UITabBarItem]?, animated: Bool) {
  108. super.setItems(items, animated: animated)
  109. self.reload()
  110. }
  111. open override func beginCustomizingItems(_ items: [UITabBarItem]) {
  112. ESTabBarController.printError("beginCustomizingItems(_:) is unsupported in ESTabBar.")
  113. super.beginCustomizingItems(items)
  114. }
  115. open override func endCustomizing(animated: Bool) -> Bool {
  116. ESTabBarController.printError("endCustomizing(_:) is unsupported in ESTabBar.")
  117. return super.endCustomizing(animated: animated)
  118. }
  119. open override func layoutSubviews() {
  120. super.layoutSubviews()
  121. self.updateLayout()
  122. }
  123. open override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
  124. var b = super.point(inside: point, with: event)
  125. if !b {
  126. for container in containers {
  127. if container.point(inside: CGPoint.init(x: point.x - container.frame.origin.x, y: point.y - container.frame.origin.y), with: event) {
  128. b = true
  129. }
  130. }
  131. }
  132. return b
  133. }
  134. }
  135. internal extension ESTabBar /* Layout */ {
  136. func updateLayout() {
  137. guard let tabBarItems = self.items else {
  138. ESTabBarController.printError("empty items")
  139. return
  140. }
  141. let tabBarButtons = subviews.filter { subview -> Bool in
  142. if let cls = NSClassFromString("UITabBarButton") {
  143. return subview.isKind(of: cls)
  144. }
  145. return false
  146. } .sorted { (subview1, subview2) -> Bool in
  147. return subview1.frame.origin.x < subview2.frame.origin.x
  148. }
  149. if isCustomizing {
  150. for (idx, _) in tabBarItems.enumerated() {
  151. tabBarButtons[idx].isHidden = false
  152. moreContentView?.isHidden = true
  153. }
  154. for (_, container) in containers.enumerated(){
  155. container.isHidden = true
  156. }
  157. } else {
  158. for (idx, item) in tabBarItems.enumerated() {
  159. if let _ = item as? ESTabBarItem {
  160. tabBarButtons[idx].isHidden = true
  161. } else {
  162. tabBarButtons[idx].isHidden = false
  163. }
  164. if isMoreItem(idx), let _ = moreContentView {
  165. tabBarButtons[idx].isHidden = true
  166. }
  167. }
  168. for (_, container) in containers.enumerated(){
  169. container.isHidden = false
  170. }
  171. }
  172. var layoutBaseSystem = true
  173. if let itemCustomPositioning = itemCustomPositioning {
  174. switch itemCustomPositioning {
  175. case .fill, .automatic, .centered:
  176. break
  177. case .fillIncludeSeparator, .fillExcludeSeparator:
  178. layoutBaseSystem = false
  179. }
  180. }
  181. if layoutBaseSystem {
  182. // System itemPositioning
  183. for (idx, container) in containers.enumerated(){
  184. if !tabBarButtons[idx].frame.isEmpty {
  185. container.frame = tabBarButtons[idx].frame
  186. }
  187. }
  188. } else {
  189. // Custom itemPositioning
  190. var x: CGFloat = itemEdgeInsets.left
  191. var y: CGFloat = itemEdgeInsets.top
  192. switch itemCustomPositioning! {
  193. case .fillExcludeSeparator:
  194. if y <= 0.0 {
  195. y += 1.0
  196. }
  197. default:
  198. break
  199. }
  200. let width = bounds.size.width - itemEdgeInsets.left - itemEdgeInsets.right
  201. let height = bounds.size.height - y - itemEdgeInsets.bottom
  202. let eachWidth = itemWidth == 0.0 ? width / CGFloat(containers.count) : itemWidth
  203. let eachSpacing = itemSpacing == 0.0 ? 0.0 : itemSpacing
  204. for container in containers {
  205. container.frame = CGRect.init(x: x, y: y, width: eachWidth, height: height)
  206. x += eachWidth
  207. x += eachSpacing
  208. }
  209. }
  210. }
  211. }
  212. internal extension ESTabBar /* Actions */ {
  213. func isMoreItem(_ index: Int) -> Bool {
  214. return ESTabBarController.isShowingMore(tabBarController) && (index == (items?.count ?? 0) - 1)
  215. }
  216. func removeAll() {
  217. for container in containers {
  218. container.removeFromSuperview()
  219. }
  220. containers.removeAll()
  221. }
  222. func reload() {
  223. removeAll()
  224. guard let tabBarItems = self.items else {
  225. ESTabBarController.printError("empty items")
  226. return
  227. }
  228. for (idx, item) in tabBarItems.enumerated() {
  229. let container = ESTabBarItemContainer.init(self, tag: 1000 + idx)
  230. self.addSubview(container)
  231. self.containers.append(container)
  232. if let item = item as? ESTabBarItem, let contentView = item.contentView {
  233. container.addSubview(contentView)
  234. }
  235. if self.isMoreItem(idx), let moreContentView = moreContentView {
  236. container.addSubview(moreContentView)
  237. }
  238. }
  239. self.updateAccessibilityLabels()
  240. self.setNeedsLayout()
  241. }
  242. @objc func highlightAction(_ sender: AnyObject?) {
  243. guard let container = sender as? ESTabBarItemContainer else {
  244. return
  245. }
  246. let newIndex = max(0, container.tag - 1000)
  247. guard newIndex < items?.count ?? 0, let item = self.items?[newIndex], item.isEnabled == true else {
  248. return
  249. }
  250. if (customDelegate?.tabBar(self, shouldSelect: item) ?? true) == false {
  251. return
  252. }
  253. if let item = item as? ESTabBarItem {
  254. item.contentView?.highlight(animated: true, completion: nil)
  255. } else if self.isMoreItem(newIndex) {
  256. moreContentView?.highlight(animated: true, completion: nil)
  257. }
  258. }
  259. @objc func dehighlightAction(_ sender: AnyObject?) {
  260. guard let container = sender as? ESTabBarItemContainer else {
  261. return
  262. }
  263. let newIndex = max(0, container.tag - 1000)
  264. guard newIndex < items?.count ?? 0, let item = self.items?[newIndex], item.isEnabled == true else {
  265. return
  266. }
  267. if (customDelegate?.tabBar(self, shouldSelect: item) ?? true) == false {
  268. return
  269. }
  270. if let item = item as? ESTabBarItem {
  271. item.contentView?.dehighlight(animated: true, completion: nil)
  272. } else if self.isMoreItem(newIndex) {
  273. moreContentView?.dehighlight(animated: true, completion: nil)
  274. }
  275. }
  276. @objc func selectAction(_ sender: AnyObject?) {
  277. guard let container = sender as? ESTabBarItemContainer else {
  278. return
  279. }
  280. select(itemAtIndex: container.tag - 1000, animated: true)
  281. }
  282. @objc func select(itemAtIndex idx: Int, animated: Bool) {
  283. let newIndex = max(0, idx)
  284. let currentIndex = (selectedItem != nil) ? (items?.firstIndex(of: selectedItem!) ?? -1) : -1
  285. guard newIndex < items?.count ?? 0, let item = self.items?[newIndex], item.isEnabled == true else {
  286. return
  287. }
  288. if (customDelegate?.tabBar(self, shouldSelect: item) ?? true) == false {
  289. return
  290. }
  291. if (customDelegate?.tabBar(self, shouldHijack: item) ?? false) == true {
  292. customDelegate?.tabBar(self, didHijack: item)
  293. if animated {
  294. if let item = item as? ESTabBarItem {
  295. item.contentView?.select(animated: animated, completion: {
  296. item.contentView?.deselect(animated: false, completion: nil)
  297. })
  298. } else if self.isMoreItem(newIndex) {
  299. moreContentView?.select(animated: animated, completion: {
  300. self.moreContentView?.deselect(animated: animated, completion: nil)
  301. })
  302. }
  303. }
  304. return
  305. }
  306. if currentIndex != newIndex {
  307. if currentIndex != -1 && currentIndex < items?.count ?? 0{
  308. if let currentItem = items?[currentIndex] as? ESTabBarItem {
  309. currentItem.contentView?.deselect(animated: animated, completion: nil)
  310. } else if self.isMoreItem(currentIndex) {
  311. moreContentView?.deselect(animated: animated, completion: nil)
  312. }
  313. }
  314. if let item = item as? ESTabBarItem {
  315. item.contentView?.select(animated: animated, completion: nil)
  316. } else if self.isMoreItem(newIndex) {
  317. moreContentView?.select(animated: animated, completion: nil)
  318. }
  319. } else if currentIndex == newIndex {
  320. if let item = item as? ESTabBarItem {
  321. item.contentView?.reselect(animated: animated, completion: nil)
  322. } else if self.isMoreItem(newIndex) {
  323. moreContentView?.reselect(animated: animated, completion: nil)
  324. }
  325. if let tabBarController = tabBarController {
  326. var navVC: UINavigationController?
  327. if let n = tabBarController.selectedViewController as? UINavigationController {
  328. navVC = n
  329. } else if let n = tabBarController.selectedViewController?.navigationController {
  330. navVC = n
  331. }
  332. if let navVC = navVC {
  333. if navVC.viewControllers.contains(tabBarController) {
  334. if navVC.viewControllers.count > 1 && navVC.viewControllers.last != tabBarController {
  335. navVC.popToViewController(tabBarController, animated: true);
  336. }
  337. } else {
  338. if navVC.viewControllers.count > 1 {
  339. navVC.popToRootViewController(animated: animated)
  340. }
  341. }
  342. }
  343. }
  344. }
  345. delegate?.tabBar?(self, didSelect: item)
  346. self.updateAccessibilityLabels()
  347. }
  348. func updateAccessibilityLabels() {
  349. guard let tabBarItems = self.items, tabBarItems.count == self.containers.count else {
  350. return
  351. }
  352. for (idx, item) in tabBarItems.enumerated() {
  353. let container = self.containers[idx]
  354. container.accessibilityIdentifier = item.accessibilityIdentifier
  355. container.accessibilityTraits = item.accessibilityTraits
  356. if item == selectedItem {
  357. container.accessibilityTraits = container.accessibilityTraits.union(.selected)
  358. }
  359. if let explicitLabel = item.accessibilityLabel {
  360. container.accessibilityLabel = explicitLabel
  361. container.accessibilityHint = item.accessibilityHint ?? container.accessibilityHint
  362. } else {
  363. var accessibilityTitle = ""
  364. if let item = item as? ESTabBarItem {
  365. accessibilityTitle = item.accessibilityLabel ?? item.title ?? ""
  366. }
  367. if self.isMoreItem(idx) {
  368. accessibilityTitle = NSLocalizedString("More_TabBarItem", bundle: Bundle(for:ESTabBarController.self), comment: "")
  369. }
  370. let formatString = NSLocalizedString(item == selectedItem ? "TabBarItem_Selected_AccessibilityLabel" : "TabBarItem_AccessibilityLabel",
  371. bundle: Bundle(for: ESTabBarController.self),
  372. comment: "")
  373. container.accessibilityLabel = String(format: formatString, accessibilityTitle, idx + 1, tabBarItems.count)
  374. }
  375. }
  376. }
  377. }