Merge commit '7621e2f8dec938cf48181c8b10afc9b01f444e68' into beta

This commit is contained in:
Ilya Laktyushin
2025-12-06 02:17:48 +04:00
commit 8344b97e03
28070 changed files with 7995182 additions and 0 deletions
+26
View File
@@ -0,0 +1,26 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "TabBarUI",
module_name = "TabBarUI",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/AsyncDisplayKit",
"//submodules/Display",
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/AnimatedStickerNode",
"//submodules/TelegramAnimatedStickerNode",
"//submodules/TelegramPresentationData",
"//submodules/ComponentFlow",
"//submodules/Components/ComponentDisplayAdapters",
"//submodules/TelegramUI/Components/TabBarComponent",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,314 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import TelegramPresentationData
import ComponentFlow
import ComponentDisplayAdapters
import TabBarComponent
private extension ToolbarTheme {
convenience init(theme: PresentationTheme) {
self.init(barBackgroundColor: theme.rootController.tabBar.backgroundColor, barSeparatorColor: .clear, barTextColor: theme.rootController.tabBar.textColor, barSelectedTextColor: theme.rootController.tabBar.selectedTextColor)
}
}
final class TabBarControllerNode: ASDisplayNode {
private struct Params: Equatable {
let layout: ContainerViewLayout
let toolbar: Toolbar?
let isTabBarHidden: Bool
init(
layout: ContainerViewLayout,
toolbar: Toolbar?,
isTabBarHidden: Bool
) {
self.layout = layout
self.toolbar = toolbar
self.isTabBarHidden = isTabBarHidden
}
}
private struct LayoutResult {
let params: Params
let bottomInset: CGFloat
init(params: Params, bottomInset: CGFloat) {
self.params = params
self.bottomInset = bottomInset
}
}
private final class View: UIView {
var onLayout: (() -> Void)?
override func layoutSubviews() {
super.layoutSubviews()
self.onLayout?()
}
}
private var theme: PresentationTheme
private let itemSelected: (Int, Bool, [ASDisplayNode]) -> Void
private let contextAction: (Int, ContextExtractedContentContainingView, ContextGesture) -> Void
private let tabBarView = ComponentView<Empty>()
private let disabledOverlayNode: ASDisplayNode
private var toolbarNode: ToolbarNode?
private let toolbarActionSelected: (ToolbarActionOption) -> Void
private let disabledPressed: () -> Void
private(set) var tabBarItems: [TabBarNodeItem] = []
private(set) var selectedIndex: Int = 0
private(set) var currentControllerNode: ASDisplayNode?
private var layoutResult: LayoutResult?
private var isUpdateRequested: Bool = false
private var isChangingSelectedIndex: Bool = false
func setCurrentControllerNode(_ node: ASDisplayNode?) -> () -> Void {
guard node !== self.currentControllerNode else {
return {}
}
let previousNode = self.currentControllerNode
self.currentControllerNode = node
if let currentControllerNode = self.currentControllerNode {
if let previousNode {
self.insertSubnode(currentControllerNode, aboveSubnode: previousNode)
} else {
self.insertSubnode(currentControllerNode, at: 0)
}
if let tabBarView = self.tabBarView.view {
self.view.bringSubviewToFront(tabBarView)
}
}
return { [weak self, weak previousNode] in
if previousNode !== self?.currentControllerNode {
previousNode?.removeFromSupernode()
}
}
}
init(theme: PresentationTheme, itemSelected: @escaping (Int, Bool, [ASDisplayNode]) -> Void, contextAction: @escaping (Int, ContextExtractedContentContainingView, ContextGesture) -> Void, swipeAction: @escaping (Int, TabBarItemSwipeDirection) -> Void, toolbarActionSelected: @escaping (ToolbarActionOption) -> Void, disabledPressed: @escaping () -> Void) {
self.theme = theme
self.itemSelected = itemSelected
self.contextAction = contextAction
self.disabledOverlayNode = ASDisplayNode()
self.disabledOverlayNode.backgroundColor = theme.rootController.tabBar.backgroundColor.withAlphaComponent(0.5)
self.disabledOverlayNode.alpha = 0.0
self.toolbarActionSelected = toolbarActionSelected
self.disabledPressed = disabledPressed
super.init()
self.setViewBlock({
return View(frame: CGRect())
})
(self.view as? View)?.onLayout = { [weak self] in
guard let self else {
return
}
if self.isUpdateRequested {
self.isUpdateRequested = false
if let layoutResult = self.layoutResult {
let _ = self.updateImpl(params: layoutResult.params, transition: .immediate)
}
}
}
self.backgroundColor = theme.list.plainBackgroundColor
//self.addSubnode(self.tabBarNode)
//self.addSubnode(self.disabledOverlayNode)
}
override func didLoad() {
super.didLoad()
self.disabledOverlayNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.disabledTapGesture(_:))))
}
@objc private func disabledTapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
self.disabledPressed()
}
}
func updateTheme(_ theme: PresentationTheme) {
self.theme = theme
self.backgroundColor = theme.list.plainBackgroundColor
self.disabledOverlayNode.backgroundColor = theme.rootController.tabBar.backgroundColor.withAlphaComponent(0.5)
self.toolbarNode?.updateTheme(ToolbarTheme(theme: theme))
self.requestUpdate()
}
func updateIsTabBarEnabled(_ value: Bool, transition: ContainedViewLayoutTransition) {
transition.updateAlpha(node: self.disabledOverlayNode, alpha: value ? 0.0 : 1.0)
}
var tabBarHidden = false {
didSet {
if self.tabBarHidden != oldValue {
self.requestUpdate()
}
}
}
func containerLayoutUpdated(_ layout: ContainerViewLayout, toolbar: Toolbar?, transition: ContainedViewLayoutTransition) -> CGFloat {
let params = Params(layout: layout, toolbar: toolbar, isTabBarHidden: self.tabBarHidden)
if let layoutResult = self.layoutResult, layoutResult.params == params {
return layoutResult.bottomInset
} else {
let bottomInset = self.updateImpl(params: params, transition: transition)
self.layoutResult = LayoutResult(params: params, bottomInset: bottomInset)
return bottomInset
}
}
private func requestUpdate() {
self.isUpdateRequested = true
self.view.setNeedsLayout()
}
private func updateImpl(params: Params, transition: ContainedViewLayoutTransition) -> CGFloat {
var options: ContainerViewLayoutInsetOptions = []
if params.layout.metrics.widthClass == .regular {
options.insert(.input)
}
var bottomInset: CGFloat = params.layout.insets(options: options).bottom
if bottomInset == 0.0 {
bottomInset = 8.0
} else {
bottomInset = max(bottomInset, 8.0)
}
let sideInset: CGFloat = 20.0
var selectedId: AnyHashable?
if self.selectedIndex < self.tabBarItems.count {
selectedId = ObjectIdentifier(self.tabBarItems[self.selectedIndex].item)
}
var tabBarTransition = ComponentTransition(transition)
if self.isChangingSelectedIndex {
self.isChangingSelectedIndex = false
tabBarTransition = .spring(duration: 0.4)
}
if self.tabBarView.view == nil {
tabBarTransition = .immediate
}
let tabBarSize = self.tabBarView.update(
transition: tabBarTransition,
component: AnyComponent(TabBarComponent(
theme: self.theme,
items: self.tabBarItems.map { item in
let itemId = AnyHashable(ObjectIdentifier(item.item))
return TabBarComponent.Item(
item: item.item,
action: { [weak self] isLongTap in
guard let self else {
return
}
if let index = self.tabBarItems.firstIndex(where: { AnyHashable(ObjectIdentifier($0.item)) == itemId }) {
self.itemSelected(index, isLongTap, [])
}
},
contextAction: { [weak self] gesture, sourceView in
guard let self else {
return
}
if let index = self.tabBarItems.firstIndex(where: { AnyHashable(ObjectIdentifier($0.item)) == itemId }) {
self.contextAction(index, sourceView, gesture)
}
}
)
},
selectedId: selectedId,
isTablet: params.layout.metrics.isTablet
)),
environment: {},
containerSize: CGSize(width: params.layout.size.width - sideInset * 2.0, height: 100.0)
)
let tabBarFrame = CGRect(origin: CGPoint(x: floor((params.layout.size.width - tabBarSize.width) * 0.5), y: params.layout.size.height - (self.tabBarHidden ? 0.0 : (tabBarSize.height + bottomInset))), size: tabBarSize)
if let tabBarComponentView = self.tabBarView.view {
if tabBarComponentView.superview == nil {
self.view.addSubview(tabBarComponentView)
}
transition.updateFrame(view: tabBarComponentView, frame: tabBarFrame)
transition.updateAlpha(layer: tabBarComponentView.layer, alpha: params.toolbar == nil ? 1.0 : 0.0)
}
transition.updateFrame(node: self.disabledOverlayNode, frame: tabBarFrame)
let toolbarHeight = 50.0 + params.layout.insets(options: options).bottom
let toolbarFrame = CGRect(origin: CGPoint(x: 0.0, y: params.layout.size.height - toolbarHeight), size: CGSize(width: params.layout.size.width, height: toolbarHeight))
if let toolbar = params.toolbar {
if let toolbarNode = self.toolbarNode {
transition.updateFrame(node: toolbarNode, frame: toolbarFrame)
toolbarNode.updateLayout(size: toolbarFrame.size, leftInset: params.layout.safeInsets.left, rightInset: params.layout.safeInsets.right, additionalSideInsets: params.layout.additionalInsets, bottomInset: bottomInset, toolbar: toolbar, transition: transition)
} else {
let toolbarNode = ToolbarNode(theme: ToolbarTheme(theme: self.theme), displaySeparator: true, left: { [weak self] in
self?.toolbarActionSelected(.left)
}, right: { [weak self] in
self?.toolbarActionSelected(.right)
}, middle: { [weak self] in
self?.toolbarActionSelected(.middle)
})
toolbarNode.frame = toolbarFrame
toolbarNode.updateLayout(size: toolbarFrame.size, leftInset: params.layout.safeInsets.left, rightInset: params.layout.safeInsets.right, additionalSideInsets: params.layout.additionalInsets, bottomInset: bottomInset, toolbar: toolbar, transition: .immediate)
self.addSubnode(toolbarNode)
self.toolbarNode = toolbarNode
if transition.isAnimated {
toolbarNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
}
} else if let toolbarNode = self.toolbarNode {
self.toolbarNode = nil
transition.updateAlpha(node: toolbarNode, alpha: 0.0, completion: { [weak toolbarNode] _ in
toolbarNode?.removeFromSupernode()
})
}
return params.layout.size.height - tabBarFrame.minY
}
func frameForControllerTab(at index: Int) -> CGRect? {
guard let tabBarView = self.tabBarView.view as? TabBarComponent.View else {
return nil
}
guard let itemFrame = tabBarView.frameForItem(at: index) else {
return nil
}
return self.view.convert(itemFrame, from: tabBarView)
}
func isPointInsideContentArea(point: CGPoint) -> Bool {
guard let tabBarView = self.tabBarView.view else {
return false
}
if point.y < tabBarView.frame.minY {
return true
}
return false
}
func updateTabBarItems(items: [TabBarNodeItem]) {
self.tabBarItems = items
self.requestUpdate()
}
func updateSelectedIndex(index: Int) {
self.selectedIndex = index
self.isChangingSelectedIndex = true
self.requestUpdate()
}
}
@@ -0,0 +1,461 @@
import Foundation
import UIKit
import AsyncDisplayKit
import SwiftSignalKit
import Display
import TelegramPresentationData
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 theme: PresentationTheme
public init(theme: PresentationTheme) {
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(theme: PresentationTheme) {
if self.theme !== theme {
self.theme = theme
if self.isNodeLoaded {
self.tabBarControllerNode.updateTheme(theme)
}
}
}
private var debugTapCounter: (Double, Int) = (0.0, 0)
public func frameForControllerTab(controller: ViewController) -> CGRect? {
if let index = self.controllers.firstIndex(of: controller) {
return self.tabBarControllerNode.frameForControllerTab(at: index)
} else {
return nil
}
}
public func isPointInsideContentArea(point: CGPoint) -> Bool {
return self.tabBarControllerNode.isPointInsideContentArea(point: point)
}
public func updateIsTabBarEnabled(_ value: Bool, transition: ContainedViewLayoutTransition) {
self.tabBarControllerNode.updateIsTabBarEnabled(value, transition: transition)
}
public func updateIsTabBarHidden(_ value: Bool, transition: ContainedViewLayoutTransition) {
self.tabBarControllerNode.tabBarHidden = value
if let layout = self.validLayout {
self.containerLayoutUpdated(layout, transition: .animated(duration: 0.4, curve: .slide))
}
}
override open func loadDisplayNode() {
self.displayNode = TabBarControllerNode(theme: self.theme, itemSelected: { [weak self] index, longTap, itemNodes in
if let strongSelf = self {
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
}
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, view, gesture in
guard let strongSelf = self else {
return
}
if index >= 0 && index < strongSelf.tabBarControllerNode.tabBarItems.count {
strongSelf.controllers[index].tabBarItemContextAction(sourceView: view, gesture: gesture)
}
}, swipeAction: { [weak self] index, direction in
guard let strongSelf = self else {
return
}
if index >= 0 && index < strongSelf.tabBarControllerNode.tabBarItems.count {
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) {
}
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
}
let tabBarSelectedIndex = self.selectedIndex
self.tabBarControllerNode.updateSelectedIndex(index: 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
let bottomInset = 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
if !self.tabBarControllerNode.tabBarHidden {
updatedLayout.intrinsicInsets.bottom = bottomInset
}
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
} else {
tabBarHeight = 49.0 + bottomInset
}
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
let tabBarItems = self.controllers.map({ TabBarNodeItem(item: $0.tabBarItem, contextActionType: $0.tabBarItemContextActionType) })
self.tabBarControllerNode.updateTabBarItems(items: 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()
}
}
}
@@ -0,0 +1,813 @@
import Foundation
import UIKit
import AsyncDisplayKit
import SwiftSignalKit
import Display
import UIKitRuntimeUtils
import AnimatedStickerNode
import TelegramAnimatedStickerNode
import TelegramPresentationData
private extension CGRect {
var center: CGPoint {
return CGPoint(x: self.midX, y: self.midY)
}
}
private let separatorHeight: CGFloat = 1.0 / UIScreen.main.scale
private func tabBarItemImage(_ image: UIImage?, title: String, backgroundColor: UIColor, tintColor: UIColor, horizontal: Bool, imageMode: Bool, centered: Bool = false) -> (UIImage, CGFloat) {
let font = horizontal ? Font.regular(13.0) : Font.medium(10.0)
let titleSize = (title as NSString).boundingRect(with: CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude), options: [.usesLineFragmentOrigin], attributes: [NSAttributedString.Key.font: font], context: nil).size
let imageSize: CGSize
if let image = image {
if horizontal {
let factor: CGFloat = 0.8
imageSize = CGSize(width: floor(image.size.width * factor), height: floor(image.size.height * factor))
} else {
imageSize = image.size
}
} else {
imageSize = CGSize()
}
let horizontalSpacing: CGFloat = 4.0
let size: CGSize
let contentWidth: CGFloat
if horizontal {
let width = max(1.0, centered ? imageSize.width : ceil(titleSize.width) + horizontalSpacing + imageSize.width)
size = CGSize(width: width, height: 34.0)
contentWidth = size.width
} else {
let width = max(1.0, centered ? imageSize.width : max(ceil(titleSize.width), imageSize.width), 1.0)
size = CGSize(width: width, height: 45.0)
contentWidth = imageSize.width
}
UIGraphicsBeginImageContextWithOptions(size, false, 0.0)
if let context = UIGraphicsGetCurrentContext() {
context.setFillColor(backgroundColor.cgColor)
context.fill(CGRect(origin: CGPoint(), size: size))
if let image = image, imageMode {
let imageRect: CGRect
if horizontal {
imageRect = CGRect(origin: CGPoint(x: 0.0, y: floor((size.height - imageSize.height) / 2.0)), size: imageSize)
} else {
imageRect = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - imageSize.width) / 2.0), y: centered ? floor((size.height - imageSize.height) / 2.0) : 0.0), size: imageSize)
}
context.saveGState()
context.translateBy(x: imageRect.midX, y: imageRect.midY)
context.scaleBy(x: 1.0, y: -1.0)
context.translateBy(x: -imageRect.midX, y: -imageRect.midY)
if image.renderingMode == .alwaysOriginal {
context.draw(image.cgImage!, in: imageRect)
} else {
context.clip(to: imageRect, mask: image.cgImage!)
context.setFillColor(tintColor.cgColor)
context.fill(imageRect)
}
context.restoreGState()
}
}
if !imageMode {
if horizontal {
(title as NSString).draw(at: CGPoint(x: imageSize.width + horizontalSpacing, y: floor((size.height - titleSize.height) / 2.0)), withAttributes: [NSAttributedString.Key.font: font, NSAttributedString.Key.foregroundColor: tintColor])
} else {
(title as NSString).draw(at: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) / 2.0), y: size.height - titleSize.height - 1.0), withAttributes: [NSAttributedString.Key.font: font, NSAttributedString.Key.foregroundColor: tintColor])
}
}
let resultImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return (resultImage!, contentWidth)
}
private let badgeFont = Font.regular(13.0)
private final class TabBarItemNode: ASDisplayNode {
let extractedContainerNode: ContextExtractedContentContainingNode
let containerNode: ContextControllerSourceNode
let imageNode: ASImageNode
let animationContainerNode: ASDisplayNode
let animationNode: AnimatedStickerNode
let textImageNode: ASImageNode
let contextImageNode: ASImageNode
let contextTextImageNode: ASImageNode
var contentWidth: CGFloat?
var isSelected: Bool = false
let ringImageNode: ASImageNode
var ringColor: UIColor? {
didSet {
if let ringColor = self.ringColor {
self.ringImageNode.image = generateCircleImage(diameter: 29.0, lineWidth: 1.0, color: ringColor, backgroundColor: nil)
} else {
self.ringImageNode.image = nil
}
}
}
var swiped: ((TabBarItemSwipeDirection) -> Void)?
var pointerInteraction: PointerInteraction?
override init() {
self.extractedContainerNode = ContextExtractedContentContainingNode()
self.containerNode = ContextControllerSourceNode()
self.ringImageNode = ASImageNode()
self.ringImageNode.isUserInteractionEnabled = false
self.ringImageNode.displayWithoutProcessing = true
self.ringImageNode.displaysAsynchronously = false
self.imageNode = ASImageNode()
self.imageNode.isUserInteractionEnabled = false
self.imageNode.displayWithoutProcessing = true
self.imageNode.displaysAsynchronously = false
self.imageNode.isAccessibilityElement = false
self.animationContainerNode = ASDisplayNode()
self.animationNode = DefaultAnimatedStickerNodeImpl()
self.animationNode.autoplay = true
self.animationNode.automaticallyLoadLastFrame = true
self.textImageNode = ASImageNode()
self.textImageNode.isUserInteractionEnabled = false
self.textImageNode.displayWithoutProcessing = true
self.textImageNode.displaysAsynchronously = false
self.textImageNode.isAccessibilityElement = false
self.contextImageNode = ASImageNode()
self.contextImageNode.isUserInteractionEnabled = false
self.contextImageNode.displayWithoutProcessing = true
self.contextImageNode.displaysAsynchronously = false
self.contextImageNode.isAccessibilityElement = false
self.contextImageNode.alpha = 0.0
self.contextTextImageNode = ASImageNode()
self.contextTextImageNode.isUserInteractionEnabled = false
self.contextTextImageNode.displayWithoutProcessing = true
self.contextTextImageNode.displaysAsynchronously = false
self.contextTextImageNode.isAccessibilityElement = false
self.contextTextImageNode.alpha = 0.0
super.init()
self.isAccessibilityElement = true
self.extractedContainerNode.contentNode.addSubnode(self.ringImageNode)
self.extractedContainerNode.contentNode.addSubnode(self.textImageNode)
self.extractedContainerNode.contentNode.addSubnode(self.imageNode)
self.extractedContainerNode.contentNode.addSubnode(self.animationContainerNode)
self.animationContainerNode.addSubnode(self.animationNode)
self.extractedContainerNode.contentNode.addSubnode(self.contextTextImageNode)
self.extractedContainerNode.contentNode.addSubnode(self.contextImageNode)
self.containerNode.addSubnode(self.extractedContainerNode)
self.containerNode.targetNodeForActivationProgress = self.extractedContainerNode.contentNode
self.addSubnode(self.containerNode)
self.extractedContainerNode.willUpdateIsExtractedToContextPreview = { [weak self] isExtracted, transition in
guard let strongSelf = self else {
return
}
transition.updateAlpha(node: strongSelf.ringImageNode, alpha: isExtracted ? 0.0 : 1.0)
transition.updateAlpha(node: strongSelf.imageNode, alpha: isExtracted ? 0.0 : 1.0)
transition.updateAlpha(node: strongSelf.animationNode, alpha: isExtracted ? 0.0 : 1.0)
transition.updateAlpha(node: strongSelf.textImageNode, alpha: isExtracted ? 0.0 : 1.0)
transition.updateAlpha(node: strongSelf.contextImageNode, alpha: isExtracted ? 1.0 : 0.0)
transition.updateAlpha(node: strongSelf.contextTextImageNode, alpha: isExtracted ? 1.0 : 0.0)
}
}
override func didLoad() {
super.didLoad()
self.pointerInteraction = PointerInteraction(node: self, style: .rectangle(CGSize(width: 90.0, height: 50.0)))
}
@objc private func swipeGesture(_ gesture: UISwipeGestureRecognizer) {
if case .ended = gesture.state {
self.containerNode.cancelGesture()
switch gesture.direction {
case .left:
self.swiped?(.left)
default:
self.swiped?(.right)
}
}
}
}
private final class TabBarNodeContainer {
let item: UITabBarItem
let updateBadgeListenerIndex: Int
let updateTitleListenerIndex: Int
let updateImageListenerIndex: Int
let updateSelectedImageListenerIndex: Int
let imageNode: TabBarItemNode
let badgeContainerNode: ASDisplayNode
let badgeBackgroundNode: ASImageNode
let badgeTextNode: ImmediateTextNode
var badgeValue: String?
var appliedBadgeValue: String?
var titleValue: String?
var appliedTitleValue: String?
var imageValue: UIImage?
var appliedImageValue: UIImage?
var selectedImageValue: UIImage?
var appliedSelectedImageValue: UIImage?
init(item: TabBarNodeItem, imageNode: TabBarItemNode, updateBadge: @escaping (String) -> Void, updateTitle: @escaping (String, Bool) -> Void, updateImage: @escaping (UIImage?) -> Void, updateSelectedImage: @escaping (UIImage?) -> Void, contextAction: @escaping (ContextExtractedContentContainingNode, ContextGesture) -> Void, swipeAction: @escaping (TabBarItemSwipeDirection) -> Void) {
self.item = item.item
self.imageNode = imageNode
self.imageNode.isAccessibilityElement = true
self.imageNode.accessibilityTraits = .button
self.badgeContainerNode = ASDisplayNode()
self.badgeContainerNode.isUserInteractionEnabled = false
self.badgeContainerNode.isAccessibilityElement = false
self.badgeBackgroundNode = ASImageNode()
self.badgeBackgroundNode.isUserInteractionEnabled = false
self.badgeBackgroundNode.displayWithoutProcessing = true
self.badgeBackgroundNode.displaysAsynchronously = false
self.badgeBackgroundNode.isAccessibilityElement = false
self.badgeTextNode = ImmediateTextNode()
self.badgeTextNode.maximumNumberOfLines = 1
self.badgeTextNode.isUserInteractionEnabled = false
self.badgeTextNode.displaysAsynchronously = false
self.badgeTextNode.isAccessibilityElement = false
self.badgeContainerNode.addSubnode(self.badgeBackgroundNode)
self.badgeContainerNode.addSubnode(self.badgeTextNode)
self.badgeValue = item.item.badgeValue ?? ""
self.updateBadgeListenerIndex = UITabBarItem_addSetBadgeListener(item.item, { value in
updateBadge(value ?? "")
})
self.titleValue = item.item.title
self.updateTitleListenerIndex = item.item.addSetTitleListener { value, animated in
updateTitle(value ?? "", animated)
}
self.imageValue = item.item.image
self.updateImageListenerIndex = item.item.addSetImageListener { value in
updateImage(value)
}
self.selectedImageValue = item.item.selectedImage
self.updateSelectedImageListenerIndex = item.item.addSetSelectedImageListener { value in
updateSelectedImage(value)
}
imageNode.containerNode.activated = { [weak self] gesture, _ in
guard let strongSelf = self else {
return
}
contextAction(strongSelf.imageNode.extractedContainerNode, gesture)
}
imageNode.swiped = { [weak imageNode] direction in
guard let imageNode = imageNode, imageNode.isSelected else {
return
}
swipeAction(direction)
}
imageNode.containerNode.isGestureEnabled = item.contextActionType != .none
let contextActionType = item.contextActionType
imageNode.containerNode.shouldBegin = { [weak imageNode] _ in
switch contextActionType {
case .none:
return false
case .always:
return true
case .whenActive:
return imageNode?.isSelected ?? false
}
}
}
deinit {
self.item.removeSetBadgeListener(self.updateBadgeListenerIndex)
self.item.removeSetTitleListener(self.updateTitleListenerIndex)
self.item.removeSetImageListener(self.updateImageListenerIndex)
self.item.removeSetSelectedImageListener(self.updateSelectedImageListenerIndex)
}
}
final class TabBarNodeItem {
let item: UITabBarItem
let contextActionType: TabBarItemContextActionType
init(item: UITabBarItem, contextActionType: TabBarItemContextActionType) {
self.item = item
self.contextActionType = contextActionType
}
}
class TabBarNode: ASDisplayNode, ASGestureRecognizerDelegate {
var tabBarItems: [TabBarNodeItem] = [] {
didSet {
self.reloadTabBarItems()
}
}
var reduceMotion: Bool = false
var selectedIndex: Int? {
didSet {
if self.selectedIndex != oldValue {
if let oldValue = oldValue {
self.updateNodeImage(oldValue, layout: true)
}
if let selectedIndex = self.selectedIndex {
self.updateNodeImage(selectedIndex, layout: true)
}
}
}
}
private let itemSelected: (Int, Bool, [ASDisplayNode]) -> Void
private let contextAction: (Int, ContextExtractedContentContainingNode, ContextGesture) -> Void
private let swipeAction: (Int, TabBarItemSwipeDirection) -> Void
private var theme: PresentationTheme
private var validLayout: (CGSize, CGFloat, CGFloat, UIEdgeInsets, CGFloat)?
private var horizontal: Bool = false
private var centered: Bool = false
private var badgeImage: UIImage
let backgroundNode: NavigationBackgroundNode
private var tabBarNodeContainers: [TabBarNodeContainer] = []
private var tapRecognizer: TapLongTapOrDoubleTapGestureRecognizer?
init(theme: PresentationTheme, itemSelected: @escaping (Int, Bool, [ASDisplayNode]) -> Void, contextAction: @escaping (Int, ContextExtractedContentContainingNode, ContextGesture) -> Void, swipeAction: @escaping (Int, TabBarItemSwipeDirection) -> Void) {
self.itemSelected = itemSelected
self.contextAction = contextAction
self.swipeAction = swipeAction
self.theme = theme
self.backgroundNode = NavigationBackgroundNode(color: theme.rootController.tabBar.backgroundColor)
self.badgeImage = generateStretchableFilledCircleImage(diameter: 18.0, color: theme.rootController.tabBar.badgeBackgroundColor, strokeColor: theme.rootController.tabBar.badgeStrokeColor, strokeWidth: 1.0, backgroundColor: nil)!
super.init()
self.isAccessibilityContainer = false
self.accessibilityTraits = [.tabBar]
self.isOpaque = false
self.backgroundColor = nil
self.isExclusiveTouch = true
self.addSubnode(self.backgroundNode)
}
override func didLoad() {
super.didLoad()
let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapLongTapOrDoubleTapGesture(_:)))
recognizer.delegate = self.wrappedGestureRecognizerDelegate
recognizer.tapActionAtPoint = { _ in
return .keepWithSingleTap
}
self.tapRecognizer = recognizer
self.view.addGestureRecognizer(recognizer)
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
if otherGestureRecognizer is UIPanGestureRecognizer {
return false
}
return true
}
@objc private func tapLongTapOrDoubleTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) {
switch recognizer.state {
case .ended:
if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation {
if case .tap = gesture {
self.tapped(at: location, longTap: false)
}
}
default:
break
}
}
func updateTheme(_ theme: PresentationTheme) {
if self.theme !== theme {
self.theme = theme
self.backgroundNode.updateColor(color: theme.rootController.tabBar.backgroundColor, transition: .immediate)
self.badgeImage = generateStretchableFilledCircleImage(diameter: 18.0, color: theme.rootController.tabBar.badgeBackgroundColor, strokeColor: theme.rootController.tabBar.badgeStrokeColor, strokeWidth: 1.0, backgroundColor: nil)!
for container in self.tabBarNodeContainers {
if let attributedText = container.badgeTextNode.attributedText, !attributedText.string.isEmpty {
container.badgeTextNode.attributedText = NSAttributedString(string: attributedText.string, font: badgeFont, textColor: theme.rootController.tabBar.badgeTextColor)
}
}
for i in 0 ..< self.tabBarItems.count {
self.updateNodeImage(i, layout: false)
self.tabBarNodeContainers[i].badgeBackgroundNode.image = self.badgeImage
}
if let validLayout = self.validLayout {
self.updateLayout(size: validLayout.0, leftInset: validLayout.1, rightInset: validLayout.2, additionalSideInsets: validLayout.3, bottomInset: validLayout.4, transition: .immediate)
}
}
}
func frameForControllerTab(at index: Int) -> CGRect? {
let container = self.tabBarNodeContainers[index]
return container.imageNode.frame
}
func viewForControllerTab(at index: Int) -> UIView? {
let container = self.tabBarNodeContainers[index]
return container.imageNode.view
}
private func reloadTabBarItems() {
for node in self.tabBarNodeContainers {
node.imageNode.removeFromSupernode()
}
self.centered = self.theme.rootController.tabBar.textColor == .clear
var tabBarNodeContainers: [TabBarNodeContainer] = []
for i in 0 ..< self.tabBarItems.count {
let item = self.tabBarItems[i]
let node = TabBarItemNode()
let container = TabBarNodeContainer(item: item, imageNode: node, updateBadge: { [weak self] value in
self?.updateNodeBadge(i, value: value)
}, updateTitle: { [weak self] _, _ in
self?.updateNodeImage(i, layout: true)
}, updateImage: { [weak self] _ in
self?.updateNodeImage(i, layout: true)
}, updateSelectedImage: { [weak self] _ in
self?.updateNodeImage(i, layout: true)
}, contextAction: { [weak self] node, gesture in
self?.tapRecognizer?.cancel()
self?.contextAction(i, node, gesture)
}, swipeAction: { [weak self] direction in
self?.swipeAction(i, direction)
})
if item.item.ringSelection {
node.ringColor = self.theme.rootController.tabBar.selectedIconColor
} else {
node.ringColor = nil
}
if let selectedIndex = self.selectedIndex, selectedIndex == i {
let (textImage, contentWidth) = tabBarItemImage(item.item.selectedImage, title: item.item.title ?? "", backgroundColor: .clear, tintColor: self.theme.rootController.tabBar.selectedTextColor, horizontal: self.horizontal, imageMode: false, centered: self.centered)
let (image, imageContentWidth): (UIImage, CGFloat)
if let _ = item.item.animationName {
(image, imageContentWidth) = (UIImage(), 0.0)
node.animationNode.isHidden = false
let animationSize: Int = Int(51.0 * UIScreen.main.scale)
node.animationNode.visibility = true
if !node.isSelected {
node.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(name: item.item.animationName ?? ""), width: animationSize, height: animationSize, playbackMode: .once, mode: .direct(cachePathPrefix: nil))
}
node.animationNode.setOverlayColor(self.theme.rootController.tabBar.selectedIconColor, replace: true, animated: false)
node.animationNode.updateLayout(size: CGSize(width: 51.0, height: 51.0))
} else {
(image, imageContentWidth) = tabBarItemImage(item.item.selectedImage, title: item.item.title ?? "", backgroundColor: .clear, tintColor: self.theme.rootController.tabBar.selectedIconColor, horizontal: self.horizontal, imageMode: true, centered: self.centered)
node.animationNode.isHidden = true
node.animationNode.visibility = false
}
let (contextTextImage, _) = tabBarItemImage(item.item.image, title: item.item.title ?? "", backgroundColor: .clear, tintColor: self.theme.contextMenu.extractedContentTintColor, horizontal: self.horizontal, imageMode: false, centered: self.centered)
let (contextImage, _) = tabBarItemImage(item.item.image, title: item.item.title ?? "", backgroundColor: .clear, tintColor: self.theme.contextMenu.extractedContentTintColor, horizontal: self.horizontal, imageMode: true, centered: self.centered)
node.textImageNode.image = textImage
node.imageNode.image = image
node.contextTextImageNode.image = contextTextImage
node.contextImageNode.image = contextImage
node.accessibilityLabel = item.item.title
node.accessibilityTraits = [.button, .selected]
node.contentWidth = max(contentWidth, imageContentWidth)
node.isSelected = true
} else {
let (textImage, contentWidth) = tabBarItemImage(item.item.image, title: item.item.title ?? "", backgroundColor: .clear, tintColor: self.theme.rootController.tabBar.textColor, horizontal: self.horizontal, imageMode: false, centered: self.centered)
let (image, imageContentWidth) = tabBarItemImage(item.item.image, title: item.item.title ?? "", backgroundColor: .clear, tintColor: self.theme.rootController.tabBar.iconColor, horizontal: self.horizontal, imageMode: true, centered: self.centered)
let (contextTextImage, _) = tabBarItemImage(item.item.image, title: item.item.title ?? "", backgroundColor: .clear, tintColor: self.theme.contextMenu.extractedContentTintColor, horizontal: self.horizontal, imageMode: false, centered: self.centered)
let (contextImage, _) = tabBarItemImage(item.item.image, title: item.item.title ?? "", backgroundColor: .clear, tintColor: self.theme.contextMenu.extractedContentTintColor, horizontal: self.horizontal, imageMode: true, centered: self.centered)
node.animationNode.isHidden = true
node.animationNode.visibility = false
node.textImageNode.image = textImage
node.accessibilityLabel = item.item.title
node.accessibilityTraits = [.button]
node.imageNode.image = image
node.contextTextImageNode.image = contextTextImage
node.contextImageNode.image = contextImage
node.contentWidth = max(contentWidth, imageContentWidth)
node.isSelected = false
}
container.badgeBackgroundNode.image = self.badgeImage
node.extractedContainerNode.contentNode.addSubnode(container.badgeContainerNode)
tabBarNodeContainers.append(container)
self.addSubnode(node)
}
self.tabBarNodeContainers = tabBarNodeContainers
self.setNeedsLayout()
}
private func updateNodeImage(_ index: Int, layout: Bool) {
if index < self.tabBarNodeContainers.count && index < self.tabBarItems.count {
let node = self.tabBarNodeContainers[index].imageNode
let item = self.tabBarItems[index]
self.centered = self.theme.rootController.tabBar.textColor == .clear
if item.item.ringSelection {
node.ringColor = self.theme.rootController.tabBar.selectedIconColor
} else {
node.ringColor = nil
}
let previousImageSize = node.imageNode.image?.size ?? CGSize()
let previousTextImageSize = node.textImageNode.image?.size ?? CGSize()
if let selectedIndex = self.selectedIndex, selectedIndex == index {
let (textImage, contentWidth) = tabBarItemImage(item.item.selectedImage, title: item.item.title ?? "", backgroundColor: .clear, tintColor: self.theme.rootController.tabBar.selectedTextColor, horizontal: self.horizontal, imageMode: false, centered: self.centered)
let (image, imageContentWidth): (UIImage, CGFloat)
if let _ = item.item.animationName {
(image, imageContentWidth) = (UIImage(), 0.0)
node.animationNode.isHidden = false
let animationSize: Int = Int(51.0 * UIScreen.main.scale)
node.animationNode.visibility = true
if !node.isSelected {
node.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(name: item.item.animationName ?? ""), width: animationSize, height: animationSize, playbackMode: .once, mode: .direct(cachePathPrefix: nil))
}
node.animationNode.setOverlayColor(self.theme.rootController.tabBar.selectedIconColor, replace: true, animated: false)
node.animationNode.updateLayout(size: CGSize(width: 51.0, height: 51.0))
} else {
if item.item.ringSelection {
(image, imageContentWidth) = (item.item.selectedImage ?? UIImage(), item.item.selectedImage?.size.width ?? 0.0)
} else {
(image, imageContentWidth) = tabBarItemImage(item.item.selectedImage, title: item.item.title ?? "", backgroundColor: .clear, tintColor: self.theme.rootController.tabBar.selectedIconColor, horizontal: self.horizontal, imageMode: true, centered: self.centered)
}
node.animationNode.isHidden = true
node.animationNode.visibility = false
}
let (contextTextImage, _) = tabBarItemImage(item.item.image, title: item.item.title ?? "", backgroundColor: .clear, tintColor: self.theme.contextMenu.extractedContentTintColor, horizontal: self.horizontal, imageMode: false, centered: self.centered)
let (contextImage, _) = tabBarItemImage(item.item.image, title: item.item.title ?? "", backgroundColor: .clear, tintColor: self.theme.contextMenu.extractedContentTintColor, horizontal: self.horizontal, imageMode: true, centered: self.centered)
node.textImageNode.image = textImage
node.accessibilityLabel = item.item.title
node.accessibilityTraits = [.button, .selected]
node.imageNode.image = image
node.contextTextImageNode.image = contextTextImage
node.contextImageNode.image = contextImage
node.contentWidth = max(contentWidth, imageContentWidth)
node.isSelected = true
if !self.reduceMotion && item.item.ringSelection {
ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut).updateTransformScale(node: node.ringImageNode, scale: 1.0, delay: 0.1)
node.imageNode.layer.animateScale(from: 1.0, to: 0.87, duration: 0.1, removeOnCompletion: false, completion: { [weak node] _ in
node?.imageNode.layer.animateScale(from: 0.87, to: 1.0, duration: 0.14, removeOnCompletion: false, completion: { [weak node] _ in
node?.imageNode.layer.removeAllAnimations()
})
})
}
} else {
let (textImage, contentWidth) = tabBarItemImage(item.item.image, title: item.item.title ?? "", backgroundColor: .clear, tintColor: self.theme.rootController.tabBar.textColor, horizontal: self.horizontal, imageMode: false, centered: self.centered)
let (image, imageContentWidth): (UIImage, CGFloat)
if item.item.ringSelection {
(image, imageContentWidth) = (item.item.image ?? UIImage(), item.item.image?.size.width ?? 0.0)
} else {
(image, imageContentWidth) = tabBarItemImage(item.item.image, title: item.item.title ?? "", backgroundColor: .clear, tintColor: self.theme.rootController.tabBar.iconColor, horizontal: self.horizontal, imageMode: true, centered: self.centered)
}
let (contextTextImage, _) = tabBarItemImage(item.item.image, title: item.item.title ?? "", backgroundColor: .clear, tintColor: self.theme.contextMenu.extractedContentTintColor, horizontal: self.horizontal, imageMode: false, centered: self.centered)
let (contextImage, _) = tabBarItemImage(item.item.image, title: item.item.title ?? "", backgroundColor: .clear, tintColor: self.theme.contextMenu.extractedContentTintColor, horizontal: self.horizontal, imageMode: true, centered: self.centered)
node.animationNode.stop()
node.animationNode.isHidden = true
node.animationNode.visibility = false
node.textImageNode.image = textImage
node.accessibilityLabel = item.item.title
node.accessibilityTraits = [.button]
node.imageNode.image = image
node.contextTextImageNode.image = contextTextImage
node.contextImageNode.image = contextImage
node.contentWidth = max(contentWidth, imageContentWidth)
node.isSelected = false
ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut).updateTransformScale(node: node.ringImageNode, scale: 0.5)
}
let updatedImageSize = node.imageNode.image?.size ?? CGSize()
let updatedTextImageSize = node.textImageNode.image?.size ?? CGSize()
if previousImageSize != updatedImageSize || previousTextImageSize != updatedTextImageSize {
if let validLayout = self.validLayout, layout {
self.updateLayout(size: validLayout.0, leftInset: validLayout.1, rightInset: validLayout.2, additionalSideInsets: validLayout.3, bottomInset: validLayout.4, transition: .immediate)
}
}
}
}
private func updateNodeBadge(_ index: Int, value: String) {
self.tabBarNodeContainers[index].badgeValue = value
if self.tabBarNodeContainers[index].badgeValue != self.tabBarNodeContainers[index].appliedBadgeValue {
if let validLayout = self.validLayout {
self.updateLayout(size: validLayout.0, leftInset: validLayout.1, rightInset: validLayout.2, additionalSideInsets: validLayout.3, bottomInset: validLayout.4, transition: .immediate)
}
}
}
private func updateNodeTitle(_ index: Int, value: String) {
self.tabBarNodeContainers[index].titleValue = value
if self.tabBarNodeContainers[index].titleValue != self.tabBarNodeContainers[index].appliedTitleValue {
if let validLayout = self.validLayout {
self.updateLayout(size: validLayout.0, leftInset: validLayout.1, rightInset: validLayout.2, additionalSideInsets: validLayout.3, bottomInset: validLayout.4, transition: .immediate)
}
}
}
func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, additionalSideInsets: UIEdgeInsets, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) {
self.validLayout = (size, leftInset, rightInset, additionalSideInsets, bottomInset)
transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: size))
self.backgroundNode.update(size: size, transition: transition)
let horizontal = !leftInset.isZero
if self.horizontal != horizontal {
self.horizontal = horizontal
for i in 0 ..< self.tabBarItems.count {
self.updateNodeImage(i, layout: false)
}
}
if self.tabBarNodeContainers.count != 0 {
var tabBarNodeContainers = self.tabBarNodeContainers
var width = size.width
var callsTabBarNodeContainer: TabBarNodeContainer?
if tabBarNodeContainers.count == 4 {
callsTabBarNodeContainer = tabBarNodeContainers[1]
}
if additionalSideInsets.right > 0.0 {
width -= additionalSideInsets.right
if let callsTabBarNodeContainer = callsTabBarNodeContainer {
tabBarNodeContainers.remove(at: 1)
transition.updateAlpha(node: callsTabBarNodeContainer.imageNode, alpha: 0.0)
callsTabBarNodeContainer.imageNode.isUserInteractionEnabled = false
}
} else {
if let callsTabBarNodeContainer = callsTabBarNodeContainer {
transition.updateAlpha(node: callsTabBarNodeContainer.imageNode, alpha: 1.0)
callsTabBarNodeContainer.imageNode.isUserInteractionEnabled = true
}
}
let distanceBetweenNodes = width / CGFloat(tabBarNodeContainers.count)
let internalWidth = distanceBetweenNodes * CGFloat(tabBarNodeContainers.count - 1)
let leftNodeOriginX = (width - internalWidth) / 2.0
for i in 0 ..< tabBarNodeContainers.count {
let container = tabBarNodeContainers[i]
let node = container.imageNode
let nodeSize = node.textImageNode.image?.size ?? CGSize()
let originX = floor(leftNodeOriginX + CGFloat(i) * distanceBetweenNodes - nodeSize.width / 2.0)
let horizontalHitTestInset = distanceBetweenNodes / 2.0 - nodeSize.width / 2.0
let nodeFrame = CGRect(origin: CGPoint(x: originX, y: 3.0), size: nodeSize)
transition.updateFrame(node: node, frame: nodeFrame)
node.extractedContainerNode.frame = CGRect(origin: CGPoint(), size: nodeFrame.size)
node.extractedContainerNode.contentNode.frame = node.extractedContainerNode.bounds
node.extractedContainerNode.contentRect = node.extractedContainerNode.bounds
node.containerNode.frame = CGRect(origin: CGPoint(), size: nodeFrame.size)
node.hitTestSlop = UIEdgeInsets(top: -3.0, left: -horizontalHitTestInset, bottom: -3.0, right: -horizontalHitTestInset)
node.containerNode.hitTestSlop = UIEdgeInsets(top: -3.0, left: -horizontalHitTestInset, bottom: -3.0, right: -horizontalHitTestInset)
node.accessibilityFrame = nodeFrame.insetBy(dx: -horizontalHitTestInset, dy: 0.0).offsetBy(dx: 0.0, dy: size.height - nodeSize.height - bottomInset)
if node.ringColor == nil {
node.imageNode.frame = CGRect(origin: CGPoint(), size: nodeFrame.size)
}
node.textImageNode.frame = CGRect(origin: CGPoint(), size: nodeFrame.size)
node.contextImageNode.frame = CGRect(origin: CGPoint(), size: nodeFrame.size)
node.contextTextImageNode.frame = CGRect(origin: CGPoint(), size: nodeFrame.size)
let scaleFactor: CGFloat = horizontal ? 0.8 : 1.0
node.animationContainerNode.subnodeTransform = CATransform3DMakeScale(scaleFactor, scaleFactor, 1.0)
let animationOffset: CGPoint = self.tabBarItems[i].item.animationOffset
let ringImageFrame: CGRect
let imageFrame: CGRect
if horizontal {
node.animationNode.frame = CGRect(origin: CGPoint(x: -10.0 - UIScreenPixel, y: -4.0 - UIScreenPixel), size: CGSize(width: 51.0, height: 51.0))
ringImageFrame = CGRect(origin: CGPoint(x: UIScreenPixel, y: 5.0 + UIScreenPixel), size: CGSize(width: 23.0, height: 23.0))
imageFrame = ringImageFrame.insetBy(dx: -1.0 + UIScreenPixel, dy: -1.0 + UIScreenPixel)
} else {
node.animationNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((nodeSize.width - 51.0) / 2.0), y: -10.0 - UIScreenPixel).offsetBy(dx: animationOffset.x, dy: animationOffset.y), size: CGSize(width: 51.0, height: 51.0))
ringImageFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((nodeSize.width - 29.0) / 2.0), y: 1.0), size: CGSize(width: 29.0, height: 29.0))
imageFrame = ringImageFrame.insetBy(dx: -1.0, dy: -1.0)
}
node.ringImageNode.bounds = CGRect(origin: CGPoint(), size: ringImageFrame.size)
node.ringImageNode.position = ringImageFrame.center
if node.ringColor != nil {
node.imageNode.bounds = CGRect(origin: CGPoint(), size: imageFrame.size)
node.imageNode.position = imageFrame.center
}
if container.badgeValue != container.appliedBadgeValue {
container.appliedBadgeValue = container.badgeValue
if let badgeValue = container.badgeValue, !badgeValue.isEmpty {
container.badgeTextNode.attributedText = NSAttributedString(string: badgeValue, font: badgeFont, textColor: self.theme.rootController.tabBar.badgeTextColor)
container.badgeContainerNode.isHidden = false
} else {
container.badgeContainerNode.isHidden = true
}
}
if !container.badgeContainerNode.isHidden {
var hasSingleLetterValue: Bool = false
if let string = container.badgeTextNode.attributedText?.string {
hasSingleLetterValue = string.count == 1
}
let badgeSize = container.badgeTextNode.updateLayout(CGSize(width: 200.0, height: 100.0))
let backgroundSize = CGSize(width: hasSingleLetterValue ? 18.0 : max(18.0, badgeSize.width + 10.0 + 1.0), height: 18.0)
let backgroundFrame: CGRect
if horizontal {
backgroundFrame = CGRect(origin: CGPoint(x: 13.0, y: 0.0), size: backgroundSize)
} else {
let contentWidth: CGFloat = 25.0
backgroundFrame = CGRect(origin: CGPoint(x: floor(node.frame.width / 2.0) + contentWidth - backgroundSize.width - 5.0, y: self.centered ? 6.0 : -1.0), size: backgroundSize)
}
transition.updateFrame(node: container.badgeContainerNode, frame: backgroundFrame)
container.badgeBackgroundNode.frame = CGRect(origin: CGPoint(), size: backgroundFrame.size)
container.badgeContainerNode.subnodeTransform = CATransform3DMakeScale(scaleFactor, scaleFactor, 1.0)
container.badgeTextNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((backgroundFrame.size.width - badgeSize.width) / 2.0), y: 1.0), size: badgeSize)
}
}
}
}
private func tapped(at location: CGPoint, longTap: Bool) {
if let bottomInset = self.validLayout?.4 {
if location.y > self.bounds.size.height - bottomInset {
return
}
var closestNode: (Int, CGFloat)?
for i in 0 ..< self.tabBarNodeContainers.count {
let node = self.tabBarNodeContainers[i].imageNode
if !node.isUserInteractionEnabled {
continue
}
let distance = abs(location.x - node.position.x)
if let previousClosestNode = closestNode {
if previousClosestNode.1 > distance {
closestNode = (i, distance)
}
} else {
closestNode = (i, distance)
}
}
if let closestNode = closestNode {
let container = self.tabBarNodeContainers[closestNode.0]
let previousSelectedIndex = self.selectedIndex
self.itemSelected(closestNode.0, longTap, [container.imageNode.imageNode, container.imageNode.textImageNode, container.badgeContainerNode])
if previousSelectedIndex != closestNode.0 {
if let selectedIndex = self.selectedIndex, let _ = self.tabBarItems[selectedIndex].item.animationName {
container.imageNode.animationNode.play(firstFrame: false, fromIndex: nil)
}
}
}
}
}
}