diff --git a/submodules/CallListUI/Sources/CallListController.swift b/submodules/CallListUI/Sources/CallListController.swift index 00573603ec..b8e54aecd3 100644 --- a/submodules/CallListUI/Sources/CallListController.swift +++ b/submodules/CallListUI/Sources/CallListController.swift @@ -750,7 +750,7 @@ public final class CallListController: TelegramBaseController { }) } - override public func tabBarItemContextAction(sourceNode: ContextExtractedContentContainingNode, gesture: ContextGesture) { + override public func tabBarItemContextAction(sourceView: ContextExtractedContentContainingView, gesture: ContextGesture) { var items: [ContextMenuItem] = [] items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.Calls_StartNewCall, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/AddUser"), color: theme.contextMenu.primaryColor) @@ -775,30 +775,27 @@ public final class CallListController: TelegramBaseController { }) }))) - let controller = ContextController(presentationData: self.presentationData, source: .extracted(CallListTabBarContextExtractedContentSource(controller: self, sourceNode: sourceNode)), items: .single(ContextController.Items(content: .list(items))), recognizer: nil, gesture: gesture) + let controller = ContextController(presentationData: self.presentationData, source: .reference(CallListTabBarContextReferenceContentSource(controller: self, sourceView: sourceView)), items: .single(ContextController.Items(content: .list(items))), recognizer: nil, gesture: gesture) self.context.sharedContext.mainWindow?.presentInGlobalOverlay(controller) } } -private final class CallListTabBarContextExtractedContentSource: ContextExtractedContentSource { +private final class CallListTabBarContextReferenceContentSource: ContextReferenceContentSource { let keepInPlace: Bool = true - let ignoreContentTouches: Bool = true - let blurBackground: Bool = true - let actionsHorizontalAlignment: ContextActionsHorizontalAlignment = .center private let controller: ViewController - private let sourceNode: ContextExtractedContentContainingNode + private let sourceView: ContextExtractedContentContainingView - init(controller: ViewController, sourceNode: ContextExtractedContentContainingNode) { + init(controller: ViewController, sourceView: ContextExtractedContentContainingView) { self.controller = controller - self.sourceNode = sourceNode + self.sourceView = sourceView } - func takeView() -> ContextControllerTakeViewInfo? { - return ContextControllerTakeViewInfo(containingItem: .node(self.sourceNode), contentAreaInScreenSpace: UIScreen.main.bounds) - } - - func putBack() -> ContextControllerPutBackViewInfo? { - return ContextControllerPutBackViewInfo(contentAreaInScreenSpace: UIScreen.main.bounds) + func transitionInfo() -> ContextControllerReferenceViewInfo? { + return ContextControllerReferenceViewInfo( + referenceView: self.sourceView.contentView, + contentAreaInScreenSpace: UIScreen.main.bounds, + actionsPosition: .top + ) } } diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index 36b63d9971..293971cc04 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -6061,7 +6061,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController self.donePressed() } - override public func tabBarItemContextAction(sourceNode: ContextExtractedContentContainingNode, gesture: ContextGesture) { + override public func tabBarItemContextAction(sourceView: ContextExtractedContentContainingView, gesture: ContextGesture) { let _ = (combineLatest(queue: .mainQueue(), self.context.engine.peers.currentChatListFilters(), chatListFilterItems(context: self.context) @@ -6178,7 +6178,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } } - let controller = ContextController(context: strongSelf.context, presentationData: strongSelf.presentationData, source: .extracted(ChatListTabBarContextExtractedContentSource(controller: strongSelf, sourceNode: sourceNode)), items: .single(ContextController.Items(content: .list(items))), recognizer: nil, gesture: gesture) + let controller = ContextController(context: strongSelf.context, presentationData: strongSelf.presentationData, source: .reference(ChatListTabBarContextReferenceContentSource(controller: strongSelf, sourceView: sourceView)), items: .single(ContextController.Items(content: .list(items))), recognizer: nil, gesture: gesture) strongSelf.context.sharedContext.mainWindow?.presentInGlobalOverlay(controller) }) } @@ -6435,26 +6435,24 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } } -private final class ChatListTabBarContextExtractedContentSource: ContextExtractedContentSource { +private final class ChatListTabBarContextReferenceContentSource: ContextReferenceContentSource { let keepInPlace: Bool = true - let ignoreContentTouches: Bool = true - let blurBackground: Bool = true let actionsHorizontalAlignment: ContextActionsHorizontalAlignment = .center private let controller: ChatListController - private let sourceNode: ContextExtractedContentContainingNode + private let sourceView: ContextExtractedContentContainingView - init(controller: ChatListController, sourceNode: ContextExtractedContentContainingNode) { + init(controller: ChatListController, sourceView: ContextExtractedContentContainingView) { self.controller = controller - self.sourceNode = sourceNode + self.sourceView = sourceView } - func takeView() -> ContextControllerTakeViewInfo? { - return ContextControllerTakeViewInfo(containingItem: .node(self.sourceNode), contentAreaInScreenSpace: UIScreen.main.bounds) - } - - func putBack() -> ContextControllerPutBackViewInfo? { - return ContextControllerPutBackViewInfo(contentAreaInScreenSpace: UIScreen.main.bounds) + func transitionInfo() -> ContextControllerReferenceViewInfo? { + return ContextControllerReferenceViewInfo( + referenceView: self.sourceView.contentView, + contentAreaInScreenSpace: UIScreen.main.bounds, + actionsPosition: .top + ) } } diff --git a/submodules/ContactListUI/Sources/ContactsController.swift b/submodules/ContactListUI/Sources/ContactsController.swift index 661c3fdedf..2eb1303454 100644 --- a/submodules/ContactListUI/Sources/ContactsController.swift +++ b/submodules/ContactListUI/Sources/ContactsController.swift @@ -774,7 +774,7 @@ public class ContactsController: ViewController { }) } - override public func tabBarItemContextAction(sourceNode: ContextExtractedContentContainingNode, gesture: ContextGesture) { + override public func tabBarItemContextAction(sourceView: ContextExtractedContentContainingView, gesture: ContextGesture) { var items: [ContextMenuItem] = [] items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.Contacts_AddContact, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/AddUser"), color: theme.contextMenu.primaryColor) @@ -787,31 +787,28 @@ public class ContactsController: ViewController { }) }))) - let controller = ContextController(presentationData: self.presentationData, source: .extracted(ContactsTabBarContextExtractedContentSource(controller: self, sourceNode: sourceNode)), items: .single(ContextController.Items(content: .list(items))), recognizer: nil, gesture: gesture) + let controller = ContextController(presentationData: self.presentationData, source: .reference(ContactsTabBarContextReferenceContentSource(controller: self, sourceView: sourceView)), items: .single(ContextController.Items(content: .list(items))), recognizer: nil, gesture: gesture) self.context.sharedContext.mainWindow?.presentInGlobalOverlay(controller) } } -private final class ContactsTabBarContextExtractedContentSource: ContextExtractedContentSource { +private final class ContactsTabBarContextReferenceContentSource: ContextReferenceContentSource { let keepInPlace: Bool = true - let ignoreContentTouches: Bool = true - let blurBackground: Bool = true - let actionsHorizontalAlignment: ContextActionsHorizontalAlignment = .center private let controller: ViewController - private let sourceNode: ContextExtractedContentContainingNode + private let sourceView: ContextExtractedContentContainingView - init(controller: ViewController, sourceNode: ContextExtractedContentContainingNode) { + init(controller: ViewController, sourceView: ContextExtractedContentContainingView) { self.controller = controller - self.sourceNode = sourceNode + self.sourceView = sourceView } - func takeView() -> ContextControllerTakeViewInfo? { - return ContextControllerTakeViewInfo(containingItem: .node(self.sourceNode), contentAreaInScreenSpace: UIScreen.main.bounds) - } - - func putBack() -> ContextControllerPutBackViewInfo? { - return ContextControllerPutBackViewInfo(contentAreaInScreenSpace: UIScreen.main.bounds) + func transitionInfo() -> ContextControllerReferenceViewInfo? { + return ContextControllerReferenceViewInfo( + referenceView: self.sourceView.contentView, + contentAreaInScreenSpace: UIScreen.main.bounds, + actionsPosition: .top + ) } } diff --git a/submodules/Display/Source/ViewController.swift b/submodules/Display/Source/ViewController.swift index 7d3f2f8be3..3fabd72a08 100644 --- a/submodules/Display/Source/ViewController.swift +++ b/submodules/Display/Source/ViewController.swift @@ -697,7 +697,7 @@ public protocol CustomViewControllerNavigationDataSummary: AnyObject { open var tabBarItemContextActionType: TabBarItemContextActionType = .none - open func tabBarItemContextAction(sourceNode: ContextExtractedContentContainingNode, gesture: ContextGesture) { + open func tabBarItemContextAction(sourceView: ContextExtractedContentContainingView, gesture: ContextGesture) { } open func tabBarDisabledAction() { diff --git a/submodules/TabBarUI/Sources/TabBarContollerNode.swift b/submodules/TabBarUI/Sources/TabBarContollerNode.swift index 064f74ffd3..79eec853d5 100644 --- a/submodules/TabBarUI/Sources/TabBarContollerNode.swift +++ b/submodules/TabBarUI/Sources/TabBarContollerNode.swift @@ -49,9 +49,8 @@ final class TabBarControllerNode: ASDisplayNode { private var theme: PresentationTheme private let itemSelected: (Int, Bool, [ASDisplayNode]) -> Void - private let contextAction: (Int, ContextExtractedContentContainingNode, ContextGesture) -> Void + private let contextAction: (Int, ContextExtractedContentContainingView, ContextGesture) -> Void - private let tabBarNode: TabBarNode private let tabBarView = ComponentView() private let disabledOverlayNode: ASDisplayNode @@ -62,7 +61,7 @@ final class TabBarControllerNode: ASDisplayNode { private(set) var tabBarItems: [TabBarNodeItem] = [] private(set) var selectedIndex: Int = 0 - var currentControllerNode: ASDisplayNode? + private(set) var currentControllerNode: ASDisplayNode? private var layoutResult: LayoutResult? private var isUpdateRequested: Bool = false @@ -81,6 +80,9 @@ final class TabBarControllerNode: ASDisplayNode { } else { self.insertSubnode(currentControllerNode, at: 0) } + if let tabBarView = self.tabBarView.view { + self.view.bringSubviewToFront(tabBarView) + } } return { [weak self, weak previousNode] in @@ -90,11 +92,10 @@ final class TabBarControllerNode: ASDisplayNode { } } - init(theme: PresentationTheme, itemSelected: @escaping (Int, Bool, [ASDisplayNode]) -> Void, contextAction: @escaping (Int, ContextExtractedContentContainingNode, ContextGesture) -> Void, swipeAction: @escaping (Int, TabBarItemSwipeDirection) -> Void, toolbarActionSelected: @escaping (ToolbarActionOption) -> Void, disabledPressed: @escaping () -> Void) { + 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.tabBarNode = TabBarNode(theme: theme, itemSelected: itemSelected, contextAction: contextAction, swipeAction: swipeAction) self.disabledOverlayNode = ASDisplayNode() self.disabledOverlayNode.backgroundColor = theme.rootController.tabBar.backgroundColor.withAlphaComponent(0.5) self.disabledOverlayNode.alpha = 0.0 @@ -141,7 +142,6 @@ final class TabBarControllerNode: ASDisplayNode { self.theme = theme self.backgroundColor = theme.list.plainBackgroundColor - self.tabBarNode.updateTheme(theme) self.disabledOverlayNode.backgroundColor = theme.rootController.tabBar.backgroundColor.withAlphaComponent(0.5) self.toolbarNode?.updateTheme(ToolbarTheme(theme: theme)) self.requestUpdate() @@ -210,6 +210,14 @@ final class TabBarControllerNode: ASDisplayNode { 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) + } } ) }, @@ -227,9 +235,6 @@ final class TabBarControllerNode: ASDisplayNode { transition.updateFrame(view: tabBarComponentView, frame: tabBarFrame) } - //transition.updateFrame(node: self.tabBarNode, frame: tabBarFrame) - //self.tabBarNode.updateLayout(size: params.layout.size, leftInset: params.layout.safeInsets.left, rightInset: params.layout.safeInsets.right, additionalSideInsets: params.layout.additionalInsets, bottomInset: bottomInset, transition: transition) - transition.updateFrame(node: self.disabledOverlayNode, frame: tabBarFrame) if let toolbar = params.toolbar { @@ -263,11 +268,20 @@ final class TabBarControllerNode: ASDisplayNode { } func frameForControllerTab(at index: Int) -> CGRect? { - return self.tabBarNode.frameForControllerTab(at: index).flatMap { self.tabBarNode.view.convert($0, to: self.view) } + 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 { - if point.y < self.tabBarNode.frame.minY { + guard let tabBarView = self.tabBarView.view else { + return false + } + if point.y < tabBarView.frame.minY { return true } return false @@ -275,13 +289,11 @@ final class TabBarControllerNode: ASDisplayNode { func updateTabBarItems(items: [TabBarNodeItem]) { self.tabBarItems = items - self.tabBarNode.tabBarItems = items self.requestUpdate() } func updateSelectedIndex(index: Int) { self.selectedIndex = index - self.tabBarNode.selectedIndex = index self.isChangingSelectedIndex = true self.requestUpdate() } diff --git a/submodules/TabBarUI/Sources/TabBarController.swift b/submodules/TabBarUI/Sources/TabBarController.swift index 5523ab5171..a3f7fe0806 100644 --- a/submodules/TabBarUI/Sources/TabBarController.swift +++ b/submodules/TabBarUI/Sources/TabBarController.swift @@ -217,12 +217,12 @@ open class TabBarControllerImpl: ViewController, TabBarController { } })) } - }, contextAction: { [weak self] index, node, gesture in + }, 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(sourceNode: node, gesture: gesture) + strongSelf.controllers[index].tabBarItemContextAction(sourceView: view, gesture: gesture) } }, swipeAction: { [weak self] index, direction in guard let strongSelf = self else { diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index 69ea91969b..1eb1bd53a1 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -13943,7 +13943,7 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc self.controllerNode.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: transition) } - override public func tabBarItemContextAction(sourceNode: ContextExtractedContentContainingNode, gesture: ContextGesture) { + override public func tabBarItemContextAction(sourceView: ContextExtractedContentContainingView, gesture: ContextGesture) { guard let (maybePrimary, other) = self.accountsAndPeersValue, let primary = maybePrimary else { return } @@ -13994,7 +13994,7 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc })))*/ } - let controller = ContextController(presentationData: self.presentationData, source: .extracted(SettingsTabBarContextExtractedContentSource(controller: self, sourceNode: sourceNode)), items: .single(ContextController.Items(content: .list(items))), recognizer: nil, gesture: gesture) + let controller = ContextController(presentationData: self.presentationData, source: .reference(SettingsTabBarContextReferenceContentSource(controller: self, sourceView: sourceView)), items: .single(ContextController.Items(content: .list(items))), recognizer: nil, gesture: gesture) self.context.sharedContext.mainWindow?.presentInGlobalOverlay(controller) } @@ -14123,26 +14123,23 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc } } -private final class SettingsTabBarContextExtractedContentSource: ContextExtractedContentSource { +private final class SettingsTabBarContextReferenceContentSource: ContextReferenceContentSource { let keepInPlace: Bool = true - let ignoreContentTouches: Bool = true - let blurBackground: Bool = true - let actionsHorizontalAlignment: ContextActionsHorizontalAlignment = .center private let controller: ViewController - private let sourceNode: ContextExtractedContentContainingNode + private let sourceView: ContextExtractedContentContainingView - init(controller: ViewController, sourceNode: ContextExtractedContentContainingNode) { + init(controller: ViewController, sourceView: ContextExtractedContentContainingView) { self.controller = controller - self.sourceNode = sourceNode + self.sourceView = sourceView } - func takeView() -> ContextControllerTakeViewInfo? { - return ContextControllerTakeViewInfo(containingItem: .node(self.sourceNode), contentAreaInScreenSpace: UIScreen.main.bounds) - } - - func putBack() -> ContextControllerPutBackViewInfo? { - return ContextControllerPutBackViewInfo(contentAreaInScreenSpace: UIScreen.main.bounds) + func transitionInfo() -> ContextControllerReferenceViewInfo? { + return ContextControllerReferenceViewInfo( + referenceView: self.sourceView.contentView, + contentAreaInScreenSpace: UIScreen.main.bounds, + actionsPosition: .top + ) } } diff --git a/submodules/TelegramUI/Components/TabBarComponent/Sources/TabBarComponent.swift b/submodules/TelegramUI/Components/TabBarComponent/Sources/TabBarComponent.swift index e75ae06981..44af06f5df 100644 --- a/submodules/TelegramUI/Components/TabBarComponent/Sources/TabBarComponent.swift +++ b/submodules/TelegramUI/Components/TabBarComponent/Sources/TabBarComponent.swift @@ -15,14 +15,16 @@ public final class TabBarComponent: Component { public final class Item: Equatable { public let item: UITabBarItem public let action: (Bool) -> Void + public let contextAction: ((ContextGesture, ContextExtractedContentContainingView) -> Void)? fileprivate var id: AnyHashable { return AnyHashable(ObjectIdentifier(self.item)) } - public init(item: UITabBarItem, action: @escaping (Bool) -> Void) { + public init(item: UITabBarItem, action: @escaping (Bool) -> Void, contextAction: ((ContextGesture, ContextExtractedContentContainingView) -> Void)?) { self.item = item self.action = action + self.contextAction = contextAction } public static func ==(lhs: Item, rhs: Item) -> Bool { @@ -32,6 +34,9 @@ public final class TabBarComponent: Component { if lhs.item !== rhs.item { return false } + if (lhs.contextAction == nil) != (rhs.contextAction == nil) { + return false + } return true } } @@ -66,11 +71,14 @@ public final class TabBarComponent: Component { public final class View: UIView, UITabBarDelegate, UIGestureRecognizerDelegate { private let backgroundView: GlassBackgroundView private let selectionView: GlassBackgroundView.ContentImageView + private let contextGestureContainerView: ContextControllerSourceView private let nativeTabBar: UITabBar? private var itemViews: [AnyHashable: ComponentView] = [:] private var selectedItemViews: [AnyHashable: ComponentView] = [:] + private var itemWithActiveContextGesture: AnyHashable? + private var component: TabBarComponent? private weak var state: EmptyComponentState? @@ -78,6 +86,9 @@ public final class TabBarComponent: Component { self.backgroundView = GlassBackgroundView(frame: CGRect()) self.selectionView = GlassBackgroundView.ContentImageView() + self.contextGestureContainerView = ContextControllerSourceView() + self.contextGestureContainerView.isGestureEnabled = true + if #available(iOS 26.0, *) { self.nativeTabBar = UITabBar() } else { @@ -86,16 +97,110 @@ public final class TabBarComponent: Component { super.init(frame: frame) + self.addSubview(self.contextGestureContainerView) + if let nativeTabBar = self.nativeTabBar { - self.addSubview(nativeTabBar) + self.contextGestureContainerView.addSubview(nativeTabBar) nativeTabBar.delegate = self - let longPressGesture = UILongPressGestureRecognizer(target: self, action: #selector(self.onLongPressGesture(_:))) + /*let longPressGesture = UILongPressGestureRecognizer(target: self, action: #selector(self.onLongPressGesture(_:))) longPressGesture.delegate = self - self.addGestureRecognizer(longPressGesture) + self.addGestureRecognizer(longPressGesture)*/ } else { - self.addSubview(self.backgroundView) + self.contextGestureContainerView.addSubview(self.backgroundView) self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.onTapGesture(_:)))) } + + self.contextGestureContainerView.shouldBegin = { [weak self] point in + guard let self, let component = self.component else { + return false + } + for (id, itemView) in self.itemViews { + if let itemView = itemView.view { + if self.convert(itemView.bounds, from: itemView).contains(point) { + guard let item = component.items.first(where: { $0.id == id }) else { + return false + } + if item.contextAction == nil { + return false + } + + self.itemWithActiveContextGesture = id + return true + } + } + } + return false + } + /*self.contextGestureContainerView.customActivationProgress = { [weak self] progress, update in + guard let self, let itemWithActiveContextGesture = self.itemWithActiveContextGesture else { + return + } + guard let itemView = self.itemViews[itemWithActiveContextGesture]?.view else { + return + } + let scaleSide = itemView.bounds.width + let minScale: CGFloat = max(0.7, (scaleSide - 15.0) / scaleSide) + let currentScale = 1.0 * (1.0 - progress) + minScale * progress + + switch update { + case .update: + let sublayerTransform = CATransform3DScale(CATransform3DIdentity, currentScale, currentScale, 1.0) + itemView.layer.sublayerTransform = sublayerTransform + case .begin: + let sublayerTransform = CATransform3DScale(CATransform3DIdentity, currentScale, currentScale, 1.0) + itemView.layer.sublayerTransform = sublayerTransform + case .ended: + let sublayerTransform = CATransform3DScale(CATransform3DIdentity, currentScale, currentScale, 1.0) + let previousTransform = itemView.layer.sublayerTransform + itemView.layer.sublayerTransform = sublayerTransform + + itemView.layer.animate(from: NSValue(caTransform3D: previousTransform), to: NSValue(caTransform3D: sublayerTransform), keyPath: "sublayerTransform", timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, duration: 0.2) + } + }*/ + self.contextGestureContainerView.activated = { [weak self] gesture, _ in + guard let self, let component = self.component else { + return + } + guard let itemWithActiveContextGesture = self.itemWithActiveContextGesture else { + return + } + + var itemView: ItemComponent.View? + if self.nativeTabBar != nil { + itemView = self.selectedItemViews[itemWithActiveContextGesture]?.view as? ItemComponent.View + } else { + itemView = self.itemViews[itemWithActiveContextGesture]?.view as? ItemComponent.View + } + + guard let itemView else { + return + } + + DispatchQueue.main.async { [weak self] in + guard let self else { + return + } + if let nativeTabBar = self.nativeTabBar { + func cancelGestures(view: UIView) { + for recognizer in view.gestureRecognizers ?? [] { + if NSStringFromClass(type(of: recognizer)).contains("sSelectionGestureRecognizer") { + recognizer.state = .cancelled + } + } + for subview in view.subviews { + cancelGestures(view: subview) + } + } + + cancelGestures(view: nativeTabBar) + } + } + + guard let item = component.items.first(where: { $0.id == itemWithActiveContextGesture }) else { + return + } + item.contextAction?(gesture, itemView.contextContainerView) + } } required public init?(coder: NSCoder) { @@ -175,6 +280,19 @@ public final class TabBarComponent: Component { return super.hitTest(point, with: event) } + public func frameForItem(at index: Int) -> CGRect? { + guard let component = self.component else { + return nil + } + if index < 0 || index >= component.items.count { + return nil + } + guard let itemView = self.itemViews[component.items[index].id]?.view else { + return nil + } + return self.convert(itemView.bounds, from: itemView) + } + func update(component: TabBarComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let innerInset: CGFloat = 3.0 @@ -304,7 +422,7 @@ public final class TabBarComponent: Component { itemContainer.addSubview(selectedItemComponentView) } } else { - self.addSubview(itemComponentView) + self.contextGestureContainerView.addSubview(itemComponentView) } } if self.nativeTabBar != nil { @@ -363,6 +481,8 @@ public final class TabBarComponent: Component { transition.setFrame(view: nativeTabBar, frame: CGRect(origin: CGPoint(x: floor((size.width - nativeTabBar.bounds.width) * 0.5), y: 0.0), size: nativeTabBar.bounds.size)) } + transition.setFrame(view: self.contextGestureContainerView, frame: CGRect(origin: CGPoint(), size: size)) + return size } } @@ -401,6 +521,8 @@ private final class ItemComponent: Component { } final class View: UIView { + let contextContainerView: ContextExtractedContentContainingView + private var imageIcon: ComponentView? private var animationIcon: ComponentView? private let title = ComponentView() @@ -414,7 +536,11 @@ private final class ItemComponent: Component { private var setBadgeListener: Int? override init(frame: CGRect) { + self.contextContainerView = ContextExtractedContentContainingView() + super.init(frame: frame) + + self.addSubview(self.contextContainerView) } required init?(coder: NSCoder) { @@ -512,9 +638,9 @@ private final class ItemComponent: Component { if let animationIconView = animationIcon.view { if animationIconView.superview == nil { if let badgeView = self.badge?.view { - self.insertSubview(animationIconView, belowSubview: badgeView) + self.contextContainerView.contentView.insertSubview(animationIconView, belowSubview: badgeView) } else { - self.addSubview(animationIconView) + self.contextContainerView.contentView.addSubview(animationIconView) } } iconTransition.setFrame(view: animationIconView, frame: iconFrame) @@ -549,9 +675,9 @@ private final class ItemComponent: Component { if let imageIconView = imageIcon.view { if imageIconView.superview == nil { if let badgeView = self.badge?.view { - self.insertSubview(imageIconView, belowSubview: badgeView) + self.contextContainerView.contentView.insertSubview(imageIconView, belowSubview: badgeView) } else { - self.addSubview(imageIconView) + self.contextContainerView.contentView.addSubview(imageIconView) } } iconTransition.setFrame(view: imageIconView, frame: iconFrame) @@ -569,7 +695,7 @@ private final class ItemComponent: Component { let titleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) * 0.5), y: availableSize.height - 9.0 - titleSize.height), size: titleSize) if let titleView = self.title.view { if titleView.superview == nil { - self.addSubview(titleView) + self.contextContainerView.contentView.addSubview(titleView) } titleView.frame = titleFrame } @@ -600,7 +726,7 @@ private final class ItemComponent: Component { let badgeFrame = CGRect(origin: CGPoint(x: floor(availableSize.width / 2.0) + contentWidth - badgeSize.width - 5.0, y: -1.0), size: badgeSize) if let badgeView = badge.view { if badgeView.superview == nil { - self.addSubview(badgeView) + self.contextContainerView.contentView.addSubview(badgeView) } badgeTransition.setFrame(view: badgeView, frame: badgeFrame) } @@ -609,6 +735,10 @@ private final class ItemComponent: Component { badge.view?.removeFromSuperview() } + transition.setFrame(view: self.contextContainerView, frame: CGRect(origin: CGPoint(), size: availableSize)) + transition.setFrame(view: self.contextContainerView.contentView, frame: CGRect(origin: CGPoint(), size: availableSize)) + self.contextContainerView.contentRect = CGRect(origin: CGPoint(), size: availableSize) + return availableSize } }