-
Notifications
You must be signed in to change notification settings - Fork 0
fix: defer tabbar menu presentation #15
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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? | ||
|
|
@@ -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() | ||
|
|
@@ -36,6 +54,7 @@ final class TabBarMenuCoordinator: NSObject, UIGestureRecognizerDelegate { | |
| } | ||
| menuHostButton?.removeFromSuperview() | ||
| menuHostButton = nil | ||
| resetMenuPresentationState() | ||
| self.tabBarController = tabBarController | ||
| startObservingTabs() | ||
| } | ||
|
|
@@ -52,6 +71,7 @@ final class TabBarMenuCoordinator: NSObject, UIGestureRecognizerDelegate { | |
| } | ||
| menuHostButton?.removeFromSuperview() | ||
| menuHostButton = nil | ||
| resetMenuPresentationState() | ||
| tabBarController = nil | ||
| } | ||
|
|
||
|
|
@@ -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 | ||
lynnswap marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
| 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 | ||
|
|
@@ -170,7 +242,6 @@ final class TabBarMenuCoordinator: NSObject, UIGestureRecognizerDelegate { | |
| let plan = makeMoreMenuPlan( | ||
| for: tabIndex, | ||
| in: tabBarController, | ||
| context: context, | ||
| request: request, | ||
| delegate: delegate | ||
| ) { | ||
|
|
@@ -182,7 +253,6 @@ final class TabBarMenuCoordinator: NSObject, UIGestureRecognizerDelegate { | |
| return makeItemMenuPlan( | ||
| for: tabIndex, | ||
| in: tabBarController, | ||
| context: context, | ||
| request: request, | ||
| delegate: delegate | ||
| ) | ||
|
|
@@ -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? { | ||
|
|
@@ -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) { | ||
|
|
@@ -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 | ||
| } | ||
|
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This method always returns Useful? React with 👍 / 👎. |
||
| } | ||
|
|
||
|
|
@@ -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) { | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.