// // ESTabBar.swift // // Created by Vincent Li on 2017/2/8. // Copyright (c) 2013-2018 ESTabBarController (https://github.com/eggswift/ESTabBarController) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. // import UIKit /// 对原生的UITabBarItemPositioning进行扩展,通过UITabBarItemPositioning设置时,系统会自动添加insets,这使得添加背景样式的需求变得不可能实现。ESTabBarItemPositioning完全支持原有的item Position 类型,除此之外还支持完全fill模式。 /// /// - automatic: UITabBarItemPositioning.automatic /// - fill: UITabBarItemPositioning.fill /// - centered: UITabBarItemPositioning.centered /// - fillExcludeSeparator: 完全fill模式,布局不覆盖tabBar顶部分割线 /// - fillIncludeSeparator: 完全fill模式,布局覆盖tabBar顶部分割线 public enum ESTabBarItemPositioning : Int { case automatic case fill case centered case fillExcludeSeparator case fillIncludeSeparator } /// 对UITabBarDelegate进行扩展,以支持UITabBarControllerDelegate的相关方法桥接 internal protocol ESTabBarDelegate: NSObjectProtocol { /// 当前item是否支持选中 /// /// - Parameters: /// - tabBar: tabBar /// - item: 当前item /// - Returns: Bool func tabBar(_ tabBar: UITabBar, shouldSelect item: UITabBarItem) -> Bool /// 当前item是否需要被劫持 /// /// - Parameters: /// - tabBar: tabBar /// - item: 当前item /// - Returns: Bool func tabBar(_ tabBar: UITabBar, shouldHijack item: UITabBarItem) -> Bool /// 当前item的点击被劫持 /// /// - Parameters: /// - tabBar: tabBar /// - item: 当前item /// - Returns: Void func tabBar(_ tabBar: UITabBar, didHijack item: UITabBarItem) } /// ESTabBar是高度自定义的UITabBar子类,通过添加UIControl的方式实现自定义tabBarItem的效果。目前支持tabBar的大部分属性的设置,例如delegate,items,selectedImge,itemPositioning,itemWidth,itemSpacing等,以后会更加细致的优化tabBar原有属性的设置效果。 open class ESTabBar: UITabBar { internal weak var customDelegate: ESTabBarDelegate? /// tabBar中items布局偏移量 public var itemEdgeInsets = UIEdgeInsets.zero /// 是否设置为自定义布局方式,默认为空。如果为空,则通过itemPositioning属性来设置。如果不为空则忽略itemPositioning,所以当tabBar的itemCustomPositioning属性不为空时,如果想改变布局规则,请设置此属性而非itemPositioning。 public var itemCustomPositioning: ESTabBarItemPositioning? { didSet { if let itemCustomPositioning = itemCustomPositioning { switch itemCustomPositioning { case .fill: itemPositioning = .fill case .automatic: itemPositioning = .automatic case .centered: itemPositioning = .centered default: break } } self.reload() } } /// tabBar自定义item的容器view internal var containers = [ESTabBarItemContainer]() /// 缓存当前tabBarController用来判断是否存在"More"Tab internal weak var tabBarController: UITabBarController? /// 自定义'More'按钮样式,继承自ESTabBarItemContentView open var moreContentView: ESTabBarItemContentView? = ESTabBarItemMoreContentView.init() { didSet { self.reload() } } open override var items: [UITabBarItem]? { didSet { self.reload() } } open var isEditing: Bool = false { didSet { if oldValue != isEditing { self.updateLayout() } } } open override func setItems(_ items: [UITabBarItem]?, animated: Bool) { super.setItems(items, animated: animated) self.reload() } open override func beginCustomizingItems(_ items: [UITabBarItem]) { ESTabBarController.printError("beginCustomizingItems(_:) is unsupported in ESTabBar.") super.beginCustomizingItems(items) } open override func endCustomizing(animated: Bool) -> Bool { ESTabBarController.printError("endCustomizing(_:) is unsupported in ESTabBar.") return super.endCustomizing(animated: animated) } open override func layoutSubviews() { super.layoutSubviews() self.updateLayout() } open override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { var b = super.point(inside: point, with: event) if !b { for container in containers { if container.point(inside: CGPoint.init(x: point.x - container.frame.origin.x, y: point.y - container.frame.origin.y), with: event) { b = true } } } return b } } internal extension ESTabBar /* Layout */ { func updateLayout() { guard let tabBarItems = self.items else { ESTabBarController.printError("empty items") return } let tabBarButtons = subviews.filter { subview -> Bool in if let cls = NSClassFromString("UITabBarButton") { return subview.isKind(of: cls) } return false } .sorted { (subview1, subview2) -> Bool in return subview1.frame.origin.x < subview2.frame.origin.x } if isCustomizing { for (idx, _) in tabBarItems.enumerated() { tabBarButtons[idx].isHidden = false moreContentView?.isHidden = true } for (_, container) in containers.enumerated(){ container.isHidden = true } } else { for (idx, item) in tabBarItems.enumerated() { if let _ = item as? ESTabBarItem { tabBarButtons[idx].isHidden = true } else { tabBarButtons[idx].isHidden = false } if isMoreItem(idx), let _ = moreContentView { tabBarButtons[idx].isHidden = true } } for (_, container) in containers.enumerated(){ container.isHidden = false } } var layoutBaseSystem = true if let itemCustomPositioning = itemCustomPositioning { switch itemCustomPositioning { case .fill, .automatic, .centered: break case .fillIncludeSeparator, .fillExcludeSeparator: layoutBaseSystem = false } } if layoutBaseSystem { // System itemPositioning for (idx, container) in containers.enumerated(){ if !tabBarButtons[idx].frame.isEmpty { container.frame = tabBarButtons[idx].frame } } } else { // Custom itemPositioning var x: CGFloat = itemEdgeInsets.left var y: CGFloat = itemEdgeInsets.top switch itemCustomPositioning! { case .fillExcludeSeparator: if y <= 0.0 { y += 1.0 } default: break } let width = bounds.size.width - itemEdgeInsets.left - itemEdgeInsets.right let height = bounds.size.height - y - itemEdgeInsets.bottom let eachWidth = itemWidth == 0.0 ? width / CGFloat(containers.count) : itemWidth let eachSpacing = itemSpacing == 0.0 ? 0.0 : itemSpacing for container in containers { container.frame = CGRect.init(x: x, y: y, width: eachWidth, height: height) x += eachWidth x += eachSpacing } } } } internal extension ESTabBar /* Actions */ { func isMoreItem(_ index: Int) -> Bool { return ESTabBarController.isShowingMore(tabBarController) && (index == (items?.count ?? 0) - 1) } func removeAll() { for container in containers { container.removeFromSuperview() } containers.removeAll() } func reload() { removeAll() guard let tabBarItems = self.items else { ESTabBarController.printError("empty items") return } for (idx, item) in tabBarItems.enumerated() { let container = ESTabBarItemContainer.init(self, tag: 1000 + idx) self.addSubview(container) self.containers.append(container) if let item = item as? ESTabBarItem, let contentView = item.contentView { container.addSubview(contentView) } if self.isMoreItem(idx), let moreContentView = moreContentView { container.addSubview(moreContentView) } } self.updateAccessibilityLabels() self.setNeedsLayout() } @objc func highlightAction(_ sender: AnyObject?) { guard let container = sender as? ESTabBarItemContainer else { return } let newIndex = max(0, container.tag - 1000) guard newIndex < items?.count ?? 0, let item = self.items?[newIndex], item.isEnabled == true else { return } if (customDelegate?.tabBar(self, shouldSelect: item) ?? true) == false { return } if let item = item as? ESTabBarItem { item.contentView?.highlight(animated: true, completion: nil) } else if self.isMoreItem(newIndex) { moreContentView?.highlight(animated: true, completion: nil) } } @objc func dehighlightAction(_ sender: AnyObject?) { guard let container = sender as? ESTabBarItemContainer else { return } let newIndex = max(0, container.tag - 1000) guard newIndex < items?.count ?? 0, let item = self.items?[newIndex], item.isEnabled == true else { return } if (customDelegate?.tabBar(self, shouldSelect: item) ?? true) == false { return } if let item = item as? ESTabBarItem { item.contentView?.dehighlight(animated: true, completion: nil) } else if self.isMoreItem(newIndex) { moreContentView?.dehighlight(animated: true, completion: nil) } } @objc func selectAction(_ sender: AnyObject?) { guard let container = sender as? ESTabBarItemContainer else { return } select(itemAtIndex: container.tag - 1000, animated: true) } @objc func select(itemAtIndex idx: Int, animated: Bool) { let newIndex = max(0, idx) let currentIndex = (selectedItem != nil) ? (items?.firstIndex(of: selectedItem!) ?? -1) : -1 guard newIndex < items?.count ?? 0, let item = self.items?[newIndex], item.isEnabled == true else { return } if (customDelegate?.tabBar(self, shouldSelect: item) ?? true) == false { return } if (customDelegate?.tabBar(self, shouldHijack: item) ?? false) == true { customDelegate?.tabBar(self, didHijack: item) if animated { if let item = item as? ESTabBarItem { item.contentView?.select(animated: animated, completion: { item.contentView?.deselect(animated: false, completion: nil) }) } else if self.isMoreItem(newIndex) { moreContentView?.select(animated: animated, completion: { self.moreContentView?.deselect(animated: animated, completion: nil) }) } } return } if currentIndex != newIndex { if currentIndex != -1 && currentIndex < items?.count ?? 0{ if let currentItem = items?[currentIndex] as? ESTabBarItem { currentItem.contentView?.deselect(animated: animated, completion: nil) } else if self.isMoreItem(currentIndex) { moreContentView?.deselect(animated: animated, completion: nil) } } if let item = item as? ESTabBarItem { item.contentView?.select(animated: animated, completion: nil) } else if self.isMoreItem(newIndex) { moreContentView?.select(animated: animated, completion: nil) } } else if currentIndex == newIndex { if let item = item as? ESTabBarItem { item.contentView?.reselect(animated: animated, completion: nil) } else if self.isMoreItem(newIndex) { moreContentView?.reselect(animated: animated, completion: nil) } if let tabBarController = tabBarController { var navVC: UINavigationController? if let n = tabBarController.selectedViewController as? UINavigationController { navVC = n } else if let n = tabBarController.selectedViewController?.navigationController { navVC = n } if let navVC = navVC { if navVC.viewControllers.contains(tabBarController) { if navVC.viewControllers.count > 1 && navVC.viewControllers.last != tabBarController { navVC.popToViewController(tabBarController, animated: true); } } else { if navVC.viewControllers.count > 1 { navVC.popToRootViewController(animated: animated) } } } } } delegate?.tabBar?(self, didSelect: item) self.updateAccessibilityLabels() } func updateAccessibilityLabels() { guard let tabBarItems = self.items, tabBarItems.count == self.containers.count else { return } for (idx, item) in tabBarItems.enumerated() { let container = self.containers[idx] container.accessibilityIdentifier = item.accessibilityIdentifier container.accessibilityTraits = item.accessibilityTraits if item == selectedItem { container.accessibilityTraits = container.accessibilityTraits.union(.selected) } if let explicitLabel = item.accessibilityLabel { container.accessibilityLabel = explicitLabel container.accessibilityHint = item.accessibilityHint ?? container.accessibilityHint } else { var accessibilityTitle = "" if let item = item as? ESTabBarItem { accessibilityTitle = item.accessibilityLabel ?? item.title ?? "" } if self.isMoreItem(idx) { accessibilityTitle = NSLocalizedString("More_TabBarItem", bundle: Bundle(for:ESTabBarController.self), comment: "") } let formatString = NSLocalizedString(item == selectedItem ? "TabBarItem_Selected_AccessibilityLabel" : "TabBarItem_AccessibilityLabel", bundle: Bundle(for: ESTabBarController.self), comment: "") container.accessibilityLabel = String(format: formatString, accessibilityTitle, idx + 1, tabBarItems.count) } } } }