Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
184 changes: 125 additions & 59 deletions Sources/TabBarMenu/TabBarMenuCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,25 @@ final class TabBarMenuCoordinator: NSObject, UIGestureRecognizerDelegate {
private struct MoreMenuPresentation {
let menu: UIMenu
let sourceView: UIView
let context: PresentationContext
let moreTabIndex: Int
}

private final class MenuPresentationRequest {
weak var tabBarController: UITabBarController?
weak var sourceView: UIView?
let menu: UIMenu
let placementProvider: (PresentationContext, UIButton) -> TabBarMenuAnchorPlacement?

init(
tabBarController: UITabBarController?,
sourceView: UIView?,
menu: UIMenu,
placementProvider: @escaping (PresentationContext, UIButton) -> TabBarMenuAnchorPlacement?
) {
self.tabBarController = tabBarController
self.sourceView = sourceView
self.menu = menu
self.placementProvider = placementProvider
}
}

weak var delegate: TabBarMenuDelegate?
Expand All @@ -21,6 +38,7 @@ final class TabBarMenuCoordinator: NSObject, UIGestureRecognizerDelegate {
private weak var tabBarController: UITabBarController?
private var menuHostButton: UIButton?
private var cancellables = Set<AnyCancellable>()
private var menuPresentationTask: Task<Void, Never>?

@MainActor deinit {
detach()
Expand All @@ -36,6 +54,7 @@ final class TabBarMenuCoordinator: NSObject, UIGestureRecognizerDelegate {
}
menuHostButton?.removeFromSuperview()
menuHostButton = nil
resetMenuPresentationState()
self.tabBarController = tabBarController
startObservingTabs()
}
Expand All @@ -52,6 +71,7 @@ final class TabBarMenuCoordinator: NSObject, UIGestureRecognizerDelegate {
}
menuHostButton?.removeFromSuperview()
menuHostButton = nil
resetMenuPresentationState()
tabBarController = nil
}

Expand Down Expand Up @@ -157,10 +177,62 @@ final class TabBarMenuCoordinator: NSObject, UIGestureRecognizerDelegate {
return PresentationContext(containerView: containerView, tabFrame: tabFrame)
}

// Present the menu only when no tab transition is active.
private func scheduleMenuPresentation(_ request: MenuPresentationRequest) {
cancelMenuPresentationTasks()
menuPresentationTask = Task { @MainActor [weak self] in
guard let self else { return }
presentMenuWhenStable(request)
}
}

private func presentMenuWhenStable(_ request: MenuPresentationRequest) {
guard !Task.isCancelled,
let tabBarController = request.tabBarController,
let sourceView = request.sourceView else {
return
}
guard let context = makePresentationContext(for: sourceView, in: tabBarController),
context.containerView.window != nil else {
return
}
guard !Task.isCancelled else {
return
}
let hostButton = makeMenuHostButton(in: context.containerView)
let placement = request.placementProvider(context, hostButton)
guard !Task.isCancelled else {
return
}
guard tabBarController.transitionCoordinator == nil else {
hostButton.removeFromSuperview()
if menuHostButton === hostButton {
menuHostButton = nil
}
return
}
presentMenu(
request.menu,
tabFrame: context.tabFrame,
in: context.containerView,
placement: placement,
hostButton: hostButton,
sourceView: sourceView
)
}

private func resetMenuPresentationState() {
cancelMenuPresentationTasks()
}

private func cancelMenuPresentationTasks() {
menuPresentationTask?.cancel()
menuPresentationTask = nil
}

private func makeMenuPlan(
for tabIndex: Int,
in tabBarController: UITabBarController,
context: PresentationContext
in tabBarController: UITabBarController
) -> MenuPlan? {
guard let delegate else {
return nil
Expand All @@ -170,7 +242,6 @@ final class TabBarMenuCoordinator: NSObject, UIGestureRecognizerDelegate {
let plan = makeMoreMenuPlan(
for: tabIndex,
in: tabBarController,
context: context,
request: request,
delegate: delegate
) {
Expand All @@ -182,7 +253,6 @@ final class TabBarMenuCoordinator: NSObject, UIGestureRecognizerDelegate {
return makeItemMenuPlan(
for: tabIndex,
in: tabBarController,
context: context,
request: request,
delegate: delegate
)
Expand All @@ -191,28 +261,28 @@ final class TabBarMenuCoordinator: NSObject, UIGestureRecognizerDelegate {
private func makeMoreMenuPlan(
for tabIndex: Int,
in tabBarController: UITabBarController,
context: PresentationContext,
request: MoreMenuRequest,
delegate: TabBarMenuDelegate
) -> MenuPlan? {
guard request.isMoreTabIndex(tabIndex, in: tabBarController),
let menu = request.menu(in: tabBarController, delegate: delegate) else {
return nil
}
let hostButton = makeMenuHostButton(in: context.containerView)
let placement = request.menuPresentationPlacement(
in: tabBarController,
presentationContext: context,
hostButton: hostButton,
delegate: delegate
)
return MenuPlan(menu: menu, placement: placement, hostButton: hostButton)
let placementProvider: (PresentationContext, UIButton) -> TabBarMenuAnchorPlacement? = { [weak delegate, weak tabBarController] context, hostButton in
guard let delegate, let tabBarController else { return nil }
return request.menuPresentationPlacement(
in: tabBarController,
presentationContext: context,
hostButton: hostButton,
delegate: delegate
)
}
return MenuPlan(menu: menu, placementProvider: placementProvider)
}

private func makeItemMenuPlan(
for tabIndex: Int,
in tabBarController: UITabBarController,
context: PresentationContext,
request: ItemMenuRequest,
delegate: TabBarMenuDelegate
) -> MenuPlan? {
Expand All @@ -223,26 +293,27 @@ final class TabBarMenuCoordinator: NSObject, UIGestureRecognizerDelegate {
) else {
return nil
}
let hostButton = makeMenuHostButton(in: context.containerView)
let placement = request.menuPresentationPlacement(
forItemAt: tabIndex,
in: tabBarController,
presentationContext: context,
hostButton: hostButton,
delegate: delegate
)
return MenuPlan(menu: menu, placement: placement, hostButton: hostButton)
let placementProvider: (PresentationContext, UIButton) -> TabBarMenuAnchorPlacement? = { [weak delegate, weak tabBarController] context, hostButton in
guard let delegate, let tabBarController else { return nil }
return request.menuPresentationPlacement(
forItemAt: tabIndex,
in: tabBarController,
presentationContext: context,
hostButton: hostButton,
delegate: delegate
)
}
return MenuPlan(menu: menu, placementProvider: placementProvider)
}

private func presentPlannedMenu(_ plan: MenuPlan, context: PresentationContext, sourceView: UIView) {
presentMenu(
plan.menu,
tabFrame: context.tabFrame,
in: context.containerView,
placement: plan.placement,
hostButton: plan.hostButton,
sourceView: sourceView
private func presentPlannedMenu(_ plan: MenuPlan, sourceView: UIView, in tabBarController: UITabBarController) {
let request = MenuPresentationRequest(
tabBarController: tabBarController,
sourceView: sourceView,
menu: plan.menu,
placementProvider: plan.placementProvider
)
scheduleMenuPresentation(request)
}

private func presentMenu(from button: UIButton) {
Expand Down Expand Up @@ -406,19 +477,16 @@ final class TabBarMenuCoordinator: NSObject, UIGestureRecognizerDelegate {
) -> MoreMenuPresentation? {
guard let menu = request.menu(in: tabBarController, delegate: delegate),
let moreTabIndex = request.moreTabStartIndex(in: tabBarController),
let sourceView = moreTabView(in: tabBarController, moreTabIndex: moreTabIndex),
let context = makePresentationContext(for: sourceView, in: tabBarController) else {
let sourceView = moreTabView(in: tabBarController, moreTabIndex: moreTabIndex) else {
return nil
}
return MoreMenuPresentation(
menu: menu,
sourceView: sourceView,
context: context,
moreTabIndex: moreTabIndex
sourceView: sourceView
)
}

private func presentMoreMenu(request: MoreMenuRequest, in tabBarController: UITabBarController) -> Bool {
private func scheduleMoreMenuPresentation(request: MoreMenuRequest, in tabBarController: UITabBarController) -> Bool {
guard let delegate else {
return false
}
Expand All @@ -429,21 +497,22 @@ final class TabBarMenuCoordinator: NSObject, UIGestureRecognizerDelegate {
) else {
return false
}
let hostButton = makeMenuHostButton(in: presentation.context.containerView)
let placement = request.menuPresentationPlacement(
in: tabBarController,
presentationContext: presentation.context,
hostButton: hostButton,
delegate: delegate
)
presentMenu(
presentation.menu,
tabFrame: presentation.context.tabFrame,
in: presentation.context.containerView,
placement: placement,
hostButton: hostButton,
sourceView: presentation.sourceView
let placementProvider: (PresentationContext, UIButton) -> TabBarMenuAnchorPlacement? = { [weak delegate, weak tabBarController] context, hostButton in
guard let delegate, let tabBarController else { return nil }
return request.menuPresentationPlacement(
in: tabBarController,
presentationContext: context,
hostButton: hostButton,
delegate: delegate
)
}
let menuRequest = MenuPresentationRequest(
tabBarController: tabBarController,
sourceView: presentation.sourceView,
menu: presentation.menu,
placementProvider: placementProvider
)
scheduleMenuPresentation(menuRequest)
return true
Comment on lines +512 to 516

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Allow selection when scheduled menu never presents

This method always returns true after scheduling, but handleMoreSelection treats that as “menu presented” and cancels the system selection. If presentMenuWhenStable later bails out (e.g., context.containerView.window == nil or transitionCoordinator != nil), no menu appears and the user’s tap is ignored. Consider returning false (or deferring the decision) when the presentation is skipped so the default More tab selection can proceed in those cases.

Useful? React with 👍 / 👎.

}

Expand All @@ -458,19 +527,16 @@ final class TabBarMenuCoordinator: NSObject, UIGestureRecognizerDelegate {
guard request.matches(item: item, in: tabBarController) else {
return false
}
return presentMoreMenu(request: request, in: tabBarController)
return scheduleMoreMenuPresentation(request: request, in: tabBarController)
}

// MARK: - Long press

private func handleMenuTrigger(tabIndex: Int, sourceView: UIView, in tabBarController: UITabBarController) {
guard let context = makePresentationContext(for: sourceView, in: tabBarController) else {
return
}
guard let plan = makeMenuPlan(for: tabIndex, in: tabBarController, context: context) else {
guard let plan = makeMenuPlan(for: tabIndex, in: tabBarController) else {
return
}
presentPlannedMenu(plan, context: context, sourceView: sourceView)
presentPlannedMenu(plan, sourceView: sourceView, in: tabBarController)
}

@objc private func handleLongPress(_ recognizer: UILongPressGestureRecognizer) {
Expand Down
3 changes: 1 addition & 2 deletions Sources/TabBarMenu/TabBarMenuRequests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@ struct PresentationContext {
@MainActor
struct MenuPlan {
let menu: UIMenu
let placement: TabBarMenuAnchorPlacement?
let hostButton: UIButton
let placementProvider: (PresentationContext, UIButton) -> TabBarMenuAnchorPlacement?
}

@MainActor
Expand Down
Loading