Swiftgram/submodules/TabBarUI/Sources/TabBarController.swift
Kylmakalle 35caff956b Smaller tabbar if names are hidden, closes #115
Fix spacing for smaller devices
2025-04-10 10:00:33 +03:00

594 lines
25 KiB
Swift

import SGSimpleSettings
import Foundation
import UIKit
import AsyncDisplayKit
import SwiftSignalKit
import Display
import TelegramPresentationData
public final class TabBarControllerTheme {
public let backgroundColor: UIColor
public let tabBarBackgroundColor: UIColor
public let tabBarSeparatorColor: UIColor
public let tabBarIconColor: UIColor
public let tabBarSelectedIconColor: UIColor
public let tabBarTextColor: UIColor
public let tabBarSelectedTextColor: UIColor
public let tabBarBadgeBackgroundColor: UIColor
public let tabBarBadgeStrokeColor: UIColor
public let tabBarBadgeTextColor: UIColor
public let tabBarExtractedIconColor: UIColor
public let tabBarExtractedTextColor: UIColor
public init(backgroundColor: UIColor, tabBarBackgroundColor: UIColor, tabBarSeparatorColor: UIColor, tabBarIconColor: UIColor, tabBarSelectedIconColor: UIColor, tabBarTextColor: UIColor, tabBarSelectedTextColor: UIColor, tabBarBadgeBackgroundColor: UIColor, tabBarBadgeStrokeColor: UIColor, tabBarBadgeTextColor: UIColor, tabBarExtractedIconColor: UIColor, tabBarExtractedTextColor: UIColor) {
self.backgroundColor = backgroundColor
self.tabBarBackgroundColor = tabBarBackgroundColor
self.tabBarSeparatorColor = tabBarSeparatorColor
self.tabBarIconColor = tabBarIconColor
self.tabBarSelectedIconColor = tabBarSelectedIconColor
self.tabBarTextColor = tabBarTextColor
self.tabBarSelectedTextColor = tabBarSelectedTextColor
self.tabBarBadgeBackgroundColor = tabBarBadgeBackgroundColor
self.tabBarBadgeStrokeColor = tabBarBadgeStrokeColor
self.tabBarBadgeTextColor = tabBarBadgeTextColor
self.tabBarExtractedIconColor = tabBarExtractedIconColor
self.tabBarExtractedTextColor = tabBarExtractedTextColor
}
public convenience init(rootControllerTheme: PresentationTheme) {
let theme = rootControllerTheme.rootController.tabBar
self.init(backgroundColor: rootControllerTheme.list.plainBackgroundColor, tabBarBackgroundColor: theme.backgroundColor, tabBarSeparatorColor: theme.separatorColor, tabBarIconColor: theme.iconColor, tabBarSelectedIconColor: theme.selectedIconColor, tabBarTextColor: theme.textColor, tabBarSelectedTextColor: theme.selectedTextColor, tabBarBadgeBackgroundColor: theme.badgeBackgroundColor, tabBarBadgeStrokeColor: theme.badgeStrokeColor, tabBarBadgeTextColor: theme.badgeTextColor, tabBarExtractedIconColor: rootControllerTheme.contextMenu.extractedContentTintColor, tabBarExtractedTextColor: rootControllerTheme.contextMenu.extractedContentTintColor)
}
}
public final class TabBarItemInfo: NSObject {
public let previewing: Bool
public init(previewing: Bool) {
self.previewing = previewing
super.init()
}
override public func isEqual(_ object: Any?) -> Bool {
if let object = object as? TabBarItemInfo {
if self.previewing != object.previewing {
return false
}
return true
} else {
return false
}
}
public static func ==(lhs: TabBarItemInfo, rhs: TabBarItemInfo) -> Bool {
if lhs.previewing != rhs.previewing {
return false
}
return true
}
}
public enum TabBarContainedControllerPresentationUpdate {
case dismiss
case present
case progress(CGFloat)
}
public protocol TabBarContainedController {
func presentTabBarPreviewingController(sourceNodes: [ASDisplayNode])
func updateTabBarPreviewingControllerPresentation(_ update: TabBarContainedControllerPresentationUpdate)
}
open class TabBarControllerImpl: ViewController, TabBarController {
private var validLayout: ContainerViewLayout?
private var tabBarControllerNode: TabBarControllerNode {
get {
return super.displayNode as! TabBarControllerNode
}
}
open override func updateNavigationCustomData(_ data: Any?, progress: CGFloat, transition: ContainedViewLayoutTransition) {
for controller in self.controllers {
controller.updateNavigationCustomData(data, progress: progress, transition: transition)
}
}
public private(set) var controllers: [ViewController] = []
private let _ready = Promise<Bool>()
override open var ready: Promise<Bool> {
return self._ready
}
private var _selectedIndex: Int?
public var selectedIndex: Int {
get {
if let _selectedIndex = self._selectedIndex {
return _selectedIndex
} else {
return 0
}
} set(value) {
let index = max(0, min(self.controllers.count - 1, value))
if self._selectedIndex != index {
self._selectedIndex = index
self.updateSelectedIndex(animated: true)
}
}
}
public var currentController: ViewController?
override public var transitionNavigationBar: NavigationBar? {
return self.currentController?.navigationBar
}
private let pendingControllerDisposable = MetaDisposable()
private var navigationBarPresentationData: NavigationBarPresentationData
private var showTabNames: Bool
private var theme: TabBarControllerTheme
public var cameraItemAndAction: (item: UITabBarItem, action: () -> Void)?
public init(showTabNames: Bool, navigationBarPresentationData: NavigationBarPresentationData, theme: TabBarControllerTheme) {
self.showTabNames = showTabNames
self.navigationBarPresentationData = navigationBarPresentationData
self.theme = theme
super.init(navigationBarPresentationData: nil)
self.scrollToTop = { [weak self] in
guard let strongSelf = self else {
return
}
if let controller = strongSelf.currentController {
controller.scrollToTop?()
}
}
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.pendingControllerDisposable.dispose()
}
public func updateTheme(navigationBarPresentationData: NavigationBarPresentationData, theme: TabBarControllerTheme) {
if self.theme !== theme {
self.theme = theme
self.navigationBarPresentationData = navigationBarPresentationData
if self.isNodeLoaded {
self.tabBarControllerNode.updateTheme(theme, navigationBarPresentationData: navigationBarPresentationData)
}
}
}
private var debugTapCounter: (Double, Int) = (0.0, 0)
public func sourceNodesForController(at index: Int) -> [ASDisplayNode]? {
return self.tabBarControllerNode.tabBarNode.sourceNodesForController(at: index)
}
public func viewForCameraItem() -> UIView? {
if let (cameraItem, _) = self.cameraItemAndAction {
if let cameraItemIndex = self.tabBarControllerNode.tabBarNode.tabBarItems.firstIndex(where: { $0.item === cameraItem }) {
return self.tabBarControllerNode.tabBarNode.viewForControllerTab(at: cameraItemIndex)
}
}
return nil
}
public func frameForControllerTab(controller: ViewController) -> CGRect? {
if let index = self.controllers.firstIndex(of: controller) {
var index = index
if let (cameraItem, _) = self.cameraItemAndAction {
if let cameraItemIndex = self.tabBarControllerNode.tabBarNode.tabBarItems.firstIndex(where: { $0.item === cameraItem }) {
if index == cameraItemIndex {
} else if index > cameraItemIndex {
index -= 1
}
}
}
return self.tabBarControllerNode.tabBarNode.frameForControllerTab(at: index).flatMap { self.tabBarControllerNode.tabBarNode.view.convert($0, to: self.view) }
} else {
return nil
}
}
public func isPointInsideContentArea(point: CGPoint) -> Bool {
if point.y < self.tabBarControllerNode.tabBarNode.frame.minY {
return true
}
return false
}
public func updateIsTabBarEnabled(_ value: Bool, transition: ContainedViewLayoutTransition) {
self.tabBarControllerNode.updateIsTabBarEnabled(value, transition: transition)
}
public func updateIsTabBarHidden(_ value: Bool, transition: ContainedViewLayoutTransition) {
self.tabBarControllerNode.tabBarNode.isHidden = value
self.tabBarControllerNode.tabBarHidden = value
if let layout = self.validLayout {
self.containerLayoutUpdated(layout, transition: .animated(duration: 0.4, curve: .slide))
}
}
override open func loadDisplayNode() {
// MARK: Swiftgram
self.displayNode = TabBarControllerNode(showTabNames: self.showTabNames, theme: self.theme, navigationBarPresentationData: self.navigationBarPresentationData, itemSelected: { [weak self] index, longTap, itemNodes in
if let strongSelf = self {
var index = index
if let (cameraItem, cameraAction) = strongSelf.cameraItemAndAction {
if let cameraItemIndex = strongSelf.tabBarControllerNode.tabBarNode.tabBarItems.firstIndex(where: { $0.item === cameraItem }) {
if index == cameraItemIndex {
cameraAction()
return
} else if index > cameraItemIndex {
index -= 1
}
}
}
if longTap, let controller = strongSelf.controllers[index] as? TabBarContainedController {
controller.presentTabBarPreviewingController(sourceNodes: itemNodes)
return
}
let timestamp = CACurrentMediaTime()
if strongSelf.debugTapCounter.0 < timestamp - 0.4 {
strongSelf.debugTapCounter.0 = timestamp
strongSelf.debugTapCounter.1 = 0
}
if strongSelf.debugTapCounter.0 >= timestamp - 0.4 {
strongSelf.debugTapCounter.0 = timestamp
strongSelf.debugTapCounter.1 += 1
}
if strongSelf.debugTapCounter.1 >= 10 {
strongSelf.debugTapCounter.1 = 0
strongSelf.controllers[index].tabBarItemDebugTapAction?()
}
if let validLayout = strongSelf.validLayout {
var updatedLayout = validLayout
var tabBarHeight: CGFloat
var options: ContainerViewLayoutInsetOptions = []
if validLayout.metrics.widthClass == .regular {
options.insert(.input)
}
let bottomInset: CGFloat = validLayout.insets(options: options).bottom
if !validLayout.safeInsets.left.isZero {
tabBarHeight = 34.0 + bottomInset
} else {
tabBarHeight = 49.0 + bottomInset
}
if !strongSelf.showTabNames { tabBarHeight -= 12.0 } // MARK: Swiftgram
updatedLayout.intrinsicInsets.bottom = tabBarHeight
strongSelf.controllers[index].containerLayoutUpdated(updatedLayout, transition: .immediate)
}
let startTime = CFAbsoluteTimeGetCurrent()
strongSelf.pendingControllerDisposable.set((strongSelf.controllers[index].ready.get()
|> deliverOnMainQueue).start(next: { _ in
if let strongSelf = self {
let readyTime = CFAbsoluteTimeGetCurrent() - startTime
if readyTime > 0.5 {
print("TabBarController: controller took \(readyTime) to become ready")
}
if strongSelf.selectedIndex == index {
if let controller = strongSelf.currentController {
if longTap {
controller.longTapWithTabBar?()
} else {
controller.scrollToTopWithTabBar?()
}
}
} else {
strongSelf.selectedIndex = index
}
}
}))
}
}, contextAction: { [weak self] index, node, gesture in
guard let strongSelf = self else {
return
}
if index >= 0 && index < strongSelf.tabBarControllerNode.tabBarNode.tabBarItems.count {
var index = index
if let (cameraItem, _) = strongSelf.cameraItemAndAction {
if let cameraItemIndex = strongSelf.tabBarControllerNode.tabBarNode.tabBarItems.firstIndex(where: { $0.item === cameraItem }) {
if index == cameraItemIndex {
return
} else if index > cameraItemIndex {
index -= 1
}
}
}
strongSelf.controllers[index].tabBarItemContextAction(sourceNode: node, gesture: gesture)
}
}, swipeAction: { [weak self] index, direction in
guard let strongSelf = self else {
return
}
if index >= 0 && index < strongSelf.tabBarControllerNode.tabBarNode.tabBarItems.count {
var index = index
if let (cameraItem, _) = strongSelf.cameraItemAndAction {
if let cameraItemIndex = strongSelf.tabBarControllerNode.tabBarNode.tabBarItems.firstIndex(where: { $0.item === cameraItem }) {
if index == cameraItemIndex {
return
} else if index > cameraItemIndex {
index -= 1
}
}
}
strongSelf.controllers[index].tabBarItemSwipeAction(direction: direction)
}
}, toolbarActionSelected: { [weak self] action in
self?.currentController?.toolbarActionSelected(action: action)
}, disabledPressed: { [weak self] in
self?.currentController?.tabBarDisabledAction()
})
self.updateSelectedIndex()
self.displayNodeDidLoad()
}
public func updateBackgroundAlpha(_ alpha: CGFloat, transition: ContainedViewLayoutTransition) {
let alpha = max(0.0, min(1.0, alpha))
transition.updateAlpha(node: self.tabBarControllerNode.tabBarNode.backgroundNode, alpha: alpha, delay: 0.1)
transition.updateAlpha(node: self.tabBarControllerNode.tabBarNode.separatorNode, alpha: alpha, delay: 0.1)
}
private func updateSelectedIndex(animated: Bool = false) {
if !self.isNodeLoaded {
return
}
var animated = animated
if let layout = self.validLayout, case .regular = layout.metrics.widthClass {
animated = false
}
var tabBarSelectedIndex = self.selectedIndex
if let (cameraItem, _) = self.cameraItemAndAction {
if let cameraItemIndex = self.tabBarControllerNode.tabBarNode.tabBarItems.firstIndex(where: { $0.item === cameraItem }) {
if tabBarSelectedIndex >= cameraItemIndex {
tabBarSelectedIndex += 1
}
}
}
self.tabBarControllerNode.tabBarNode.selectedIndex = tabBarSelectedIndex
var transitionScale: CGFloat = 0.998
if let currentView = self.currentController?.view {
transitionScale = (currentView.frame.height - 3.0) / currentView.frame.height
}
if let currentController = self.currentController {
currentController.willMove(toParent: nil)
//self.tabBarControllerNode.currentControllerNode = nil
if animated {
currentController.view.layer.animateScale(from: 1.0, to: transitionScale, duration: 0.12, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { completed in
if completed {
currentController.view.layer.removeAllAnimations()
}
})
}
currentController.removeFromParent()
currentController.didMove(toParent: nil)
self.currentController = nil
}
if let _selectedIndex = self._selectedIndex, _selectedIndex < self.controllers.count {
self.currentController = self.controllers[_selectedIndex]
}
if let currentController = self.currentController {
currentController.willMove(toParent: self)
self.addChild(currentController)
let commit = self.tabBarControllerNode.setCurrentControllerNode(currentController.displayNode)
if animated {
currentController.view.layer.animateScale(from: transitionScale, to: 1.0, duration: 0.15, delay: 0.1, timingFunction: kCAMediaTimingFunctionSpring)
currentController.view.layer.allowsGroupOpacity = true
currentController.view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1, completion: { completed in
if completed {
currentController.view.layer.allowsGroupOpacity = false
}
commit()
})
} else {
commit()
}
currentController.didMove(toParent: self)
currentController.displayNode.recursivelyEnsureDisplaySynchronously(true)
self.statusBar.statusBarStyle = currentController.statusBar.statusBarStyle
}
if let layout = self.validLayout {
self.containerLayoutUpdated(layout, transition: .immediate)
}
}
public func updateLayout(transition: ContainedViewLayoutTransition = .immediate) {
if let layout = self.validLayout {
self.containerLayoutUpdated(layout, transition: transition)
}
}
override open func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
self.validLayout = layout
self.tabBarControllerNode.containerLayoutUpdated(layout, toolbar: self.currentController?.toolbar, transition: transition)
if let currentController = self.currentController {
currentController.view.frame = CGRect(origin: CGPoint(), size: layout.size)
var updatedLayout = layout
var tabBarHeight: CGFloat
var options: ContainerViewLayoutInsetOptions = []
if updatedLayout.metrics.widthClass == .regular {
options.insert(.input)
}
let bottomInset: CGFloat = updatedLayout.insets(options: options).bottom
if !updatedLayout.safeInsets.left.isZero {
tabBarHeight = 34.0 + bottomInset
if !self.showTabNames { tabBarHeight -= 6.0 } // MARK: Swiftgram
} else {
tabBarHeight = 49.0 + bottomInset
if !self.showTabNames { tabBarHeight -= 12.0 } // MARK: Swiftgram
}
if !self.tabBarControllerNode.tabBarHidden {
updatedLayout.intrinsicInsets.bottom = tabBarHeight
}
currentController.containerLayoutUpdated(updatedLayout, transition: transition)
}
}
public func updateControllerLayout(controller: ViewController) {
guard let layout = self.validLayout else {
return
}
if self.controllers.contains(where: { $0 === controller }) {
let currentController = controller
currentController.view.frame = CGRect(origin: CGPoint(), size: layout.size)
var updatedLayout = layout
var tabBarHeight: CGFloat
var options: ContainerViewLayoutInsetOptions = []
if updatedLayout.metrics.widthClass == .regular {
options.insert(.input)
}
let bottomInset: CGFloat = updatedLayout.insets(options: options).bottom
if !updatedLayout.safeInsets.left.isZero {
tabBarHeight = 34.0 + bottomInset
if !self.showTabNames { tabBarHeight -= 6.0 } // MARK: Swiftgram
} else {
tabBarHeight = 49.0 + bottomInset
if !self.showTabNames { tabBarHeight -= 12.0 } // MARK: Swiftgram
}
if !self.tabBarControllerNode.tabBarHidden {
updatedLayout.intrinsicInsets.bottom = tabBarHeight
}
currentController.containerLayoutUpdated(updatedLayout, transition: .immediate)
}
}
override open func navigationStackConfigurationUpdated(next: [ViewController]) {
super.navigationStackConfigurationUpdated(next: next)
for controller in self.controllers {
controller.navigationStackConfigurationUpdated(next: next)
}
}
override open func viewWillDisappear(_ animated: Bool) {
if let currentController = self.currentController {
currentController.viewWillDisappear(animated)
}
}
override open func viewWillAppear(_ animated: Bool) {
if let currentController = self.currentController {
currentController.viewWillAppear(animated)
}
}
override open func viewDidAppear(_ animated: Bool) {
if let currentController = self.currentController {
currentController.viewDidAppear(animated)
}
}
override open func viewDidDisappear(_ animated: Bool) {
if let currentController = self.currentController {
currentController.viewDidDisappear(animated)
}
}
public func setControllers(_ controllers: [ViewController], selectedIndex: Int?) {
var updatedSelectedIndex: Int? = selectedIndex
if updatedSelectedIndex == nil, let selectedIndex = self._selectedIndex, selectedIndex < self.controllers.count {
if let index = controllers.firstIndex(where: { $0 === self.controllers[selectedIndex] }) {
updatedSelectedIndex = index
} else {
updatedSelectedIndex = 0
}
}
self.controllers = controllers
var tabBarItems = self.controllers.map({ TabBarNodeItem(item: $0.tabBarItem, contextActionType: $0.tabBarItemContextActionType) })
if let (cameraItem, _) = self.cameraItemAndAction {
tabBarItems.insert(TabBarNodeItem(item: cameraItem, contextActionType: .none), at: Int(floor(CGFloat(controllers.count) / 2)))
}
self.tabBarControllerNode.tabBarNode.tabBarItems = tabBarItems
let signals = combineLatest(self.controllers.map({ $0.tabBarItem }).map { tabBarItem -> Signal<Bool, NoError> in
if let tabBarItem = tabBarItem, tabBarItem.image == nil {
return Signal { [weak tabBarItem] subscriber in
let index = tabBarItem?.addSetImageListener({ image in
if image != nil {
subscriber.putNext(true)
subscriber.putCompletion()
}
})
return ActionDisposable {
Queue.mainQueue().async {
if let index = index {
tabBarItem?.removeSetImageListener(index)
}
}
}
}
|> runOn(.mainQueue())
} else {
return .single(true)
}
})
|> map { items -> Bool in
for item in items {
if !item {
return false
}
}
return true
}
|> filter { $0 }
|> take(1)
let allReady = signals
|> deliverOnMainQueue
|> mapToSignal { _ -> Signal<Bool, NoError> in
// wait for tab bar items to be applied
return .single(true)
|> delay(0.0, queue: Queue.mainQueue())
}
self._ready.set(allReady)
if let updatedSelectedIndex = updatedSelectedIndex {
self.selectedIndex = updatedSelectedIndex
self.updateSelectedIndex()
}
}
}