Use new UITabBarController API on iOS 18
This commit is contained in:
parent
348dcc558c
commit
a8f6aa6ed7
|
@ -33,11 +33,11 @@ public enum DuckAttemptAction {
|
|||
|
||||
extension UIViewController {
|
||||
@available(iOS 16.0, *)
|
||||
public func presentDuckable(_ viewController: DuckableViewController, animated: Bool, isDucked: Bool = false) -> Bool {
|
||||
public func presentDuckable(_ viewController: DuckableViewController, animated: Bool, isDucked: Bool = false, completion: (() -> Void)? = nil) -> Bool {
|
||||
var cur: UIViewController? = self
|
||||
while let vc = cur {
|
||||
if let container = vc as? DuckableContainerViewController {
|
||||
container.presentDuckable(viewController, animated: animated, isDucked: isDucked, completion: nil)
|
||||
container._presentDuckable(viewController, animated: animated, isDucked: isDucked, completion: completion)
|
||||
return true
|
||||
} else {
|
||||
cur = vc.parent
|
||||
|
|
|
@ -58,7 +58,7 @@ public class DuckableContainerViewController: UIViewController {
|
|||
])
|
||||
}
|
||||
|
||||
func presentDuckable(_ viewController: DuckableViewController, animated: Bool, isDucked: Bool, completion: (() -> Void)?) {
|
||||
func _presentDuckable(_ viewController: DuckableViewController, animated: Bool, isDucked: Bool, completion: (() -> Void)?) {
|
||||
guard case .idle = state else {
|
||||
if animated,
|
||||
case .ducked(_, placeholder: let placeholder) = state {
|
||||
|
|
|
@ -129,6 +129,8 @@
|
|||
D646DCD82A07F3500059ECEB /* FollowRequestNotificationCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D646DCD72A07F3500059ECEB /* FollowRequestNotificationCollectionViewCell.swift */; };
|
||||
D646DCDA2A081A2C0059ECEB /* PollFinishedNotificationCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D646DCD92A081A2C0059ECEB /* PollFinishedNotificationCollectionViewCell.swift */; };
|
||||
D646DCDC2A081CF10059ECEB /* StatusUpdatedNotificationCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D646DCDB2A081CF10059ECEB /* StatusUpdatedNotificationCollectionViewCell.swift */; };
|
||||
D64A50462C739DC0009D7193 /* NewMainTabBarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64A50452C739DC0009D7193 /* NewMainTabBarViewController.swift */; };
|
||||
D64A50482C739DEA009D7193 /* BaseMainTabBarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64A50472C739DEA009D7193 /* BaseMainTabBarViewController.swift */; };
|
||||
D64AAE9126C80DC600FC57FB /* ToastView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64AAE9026C80DC600FC57FB /* ToastView.swift */; };
|
||||
D64AAE9526C88C5000FC57FB /* ToastableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64AAE9426C88C5000FC57FB /* ToastableViewController.swift */; };
|
||||
D64AAE9726C88DC400FC57FB /* ToastConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64AAE9626C88DC400FC57FB /* ToastConfiguration.swift */; };
|
||||
|
@ -560,6 +562,8 @@
|
|||
D646DCD72A07F3500059ECEB /* FollowRequestNotificationCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowRequestNotificationCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||
D646DCD92A081A2C0059ECEB /* PollFinishedNotificationCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollFinishedNotificationCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||
D646DCDB2A081CF10059ECEB /* StatusUpdatedNotificationCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusUpdatedNotificationCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||
D64A50452C739DC0009D7193 /* NewMainTabBarViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewMainTabBarViewController.swift; sourceTree = "<group>"; };
|
||||
D64A50472C739DEA009D7193 /* BaseMainTabBarViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseMainTabBarViewController.swift; sourceTree = "<group>"; };
|
||||
D64AAE9026C80DC600FC57FB /* ToastView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastView.swift; sourceTree = "<group>"; };
|
||||
D64AAE9426C88C5000FC57FB /* ToastableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastableViewController.swift; sourceTree = "<group>"; };
|
||||
D64AAE9626C88DC400FC57FB /* ToastConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastConfiguration.swift; sourceTree = "<group>"; };
|
||||
|
@ -1124,7 +1128,9 @@
|
|||
D6FF985F255C717400845181 /* AccountSwitchingContainerViewController.swift */,
|
||||
D6BEA246291A0F2D002F4D01 /* Duckable+Root.swift */,
|
||||
D6F0B12A24A3071C001E48C3 /* MainSplitViewController.swift */,
|
||||
D64A50472C739DEA009D7193 /* BaseMainTabBarViewController.swift */,
|
||||
04DACE8B212CB14B009840C4 /* MainTabBarViewController.swift */,
|
||||
D64A50452C739DC0009D7193 /* NewMainTabBarViewController.swift */,
|
||||
D6F0B17424A3A1AA001E48C3 /* MainSidebarViewController.swift */,
|
||||
D6B93666281D937300237D0E /* MainSidebarMyProfileCollectionViewCell.swift */,
|
||||
D68E525A24A3D77E0054355A /* TuskerRootViewController.swift */,
|
||||
|
@ -2141,6 +2147,7 @@
|
|||
D61F75A5293ABD6F00C0B37F /* EditFilterView.swift in Sources */,
|
||||
D6BC9DB1232C61BC002CA326 /* NotificationsPageViewController.swift in Sources */,
|
||||
D6ADB6EC28EA73CB009924AB /* StatusContentContainer.swift in Sources */,
|
||||
D64A50462C739DC0009D7193 /* NewMainTabBarViewController.swift in Sources */,
|
||||
D6969E9E240C81B9002843CE /* NSTextAttachment+Emoji.swift in Sources */,
|
||||
D6D79F532A0FFE3200AB2315 /* ToggleableButton.swift in Sources */,
|
||||
D6BC9DB3232D4C07002CA326 /* WellnessPrefsView.swift in Sources */,
|
||||
|
@ -2193,6 +2200,7 @@
|
|||
D6934F322BA7E43E002B1C8D /* LoadingGalleryContentViewController.swift in Sources */,
|
||||
D68E525B24A3D77E0054355A /* TuskerRootViewController.swift in Sources */,
|
||||
D62D2422217AA7E1005076CC /* UserActivityManager.swift in Sources */,
|
||||
D64A50482C739DEA009D7193 /* BaseMainTabBarViewController.swift in Sources */,
|
||||
D68A76E329524D2A001DA1B3 /* ListMO.swift in Sources */,
|
||||
D63CC70C2910AADB000E19DE /* TuskerSceneDelegate.swift in Sources */,
|
||||
D61F759D2938574B00C0B37F /* FilterRow.swift in Sources */,
|
||||
|
|
|
@ -63,7 +63,7 @@ class MainSceneDelegate: UIResponder, UIWindowSceneDelegate, TuskerSceneDelegate
|
|||
let draft = mastodonController.createDraft()
|
||||
let text = components.queryItems?.first(where: { $0.name == "text" })?.value
|
||||
draft.text = text ?? ""
|
||||
rootViewController.compose(editing: draft, animated: true, isDucked: false)
|
||||
rootViewController.compose(editing: draft, animated: true, isDucked: false, completion: nil)
|
||||
}
|
||||
} else {
|
||||
// Assume anything else is a search query
|
||||
|
@ -266,15 +266,24 @@ class MainSceneDelegate: UIResponder, UIWindowSceneDelegate, TuskerSceneDelegate
|
|||
mastodonController.initialize()
|
||||
|
||||
#if os(visionOS)
|
||||
return MainTabBarViewController(mastodonController: mastodonController)
|
||||
if #available(visionOS 2.0, *) {
|
||||
return NewMainTabBarViewController(mastodonController: mastodonController)
|
||||
} else {
|
||||
return MainTabBarViewController(mastodonController: mastodonController)
|
||||
}
|
||||
#else
|
||||
let split = MainSplitViewController(mastodonController: mastodonController)
|
||||
let mainVC: UIViewController & AccountSwitchableViewController
|
||||
if #available(iOS 18.0, *) {
|
||||
mainVC = NewMainTabBarViewController(mastodonController: mastodonController)
|
||||
} else {
|
||||
mainVC = MainSplitViewController(mastodonController: mastodonController)
|
||||
}
|
||||
if UIDevice.current.userInterfaceIdiom == .phone,
|
||||
#available(iOS 16.0, *) {
|
||||
// TODO: maybe the duckable container should be outside the account switching container
|
||||
return DuckableContainerViewController(child: split)
|
||||
return DuckableContainerViewController(child: mainVC)
|
||||
} else {
|
||||
return split
|
||||
return mainVC
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
|
|
@ -147,9 +147,9 @@ extension AccountSwitchingContainerViewController: TuskerRootViewController {
|
|||
return root.stateRestorationActivity()
|
||||
}
|
||||
|
||||
func compose(editing draft: Draft?, animated: Bool, isDucked: Bool) {
|
||||
func compose(editing draft: Draft?, animated: Bool, isDucked: Bool, completion: (() -> Void)?) {
|
||||
loadViewIfNeeded()
|
||||
root.compose(editing: draft, animated: animated, isDucked: isDucked)
|
||||
root.compose(editing: draft, animated: animated, isDucked: isDucked, completion: completion)
|
||||
}
|
||||
|
||||
func select(route: TuskerRoute, animated: Bool, completion: (() -> Void)?) {
|
||||
|
@ -157,11 +157,6 @@ extension AccountSwitchingContainerViewController: TuskerRootViewController {
|
|||
root.select(route: route, animated: animated, completion: completion)
|
||||
}
|
||||
|
||||
func getTabController(tab: MainTabBarViewController.Tab) -> UIViewController? {
|
||||
loadViewIfNeeded()
|
||||
return root.getTabController(tab: tab)
|
||||
}
|
||||
|
||||
func getNavigationDelegate() -> TuskerNavigationDelegate? {
|
||||
loadViewIfNeeded()
|
||||
return root.getNavigationDelegate()
|
||||
|
|
|
@ -0,0 +1,184 @@
|
|||
//
|
||||
// BaseMainTabBarViewController.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 8/19/24.
|
||||
// Copyright © 2024 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class BaseMainTabBarViewController: UITabBarController {
|
||||
|
||||
let mastodonController: MastodonController
|
||||
|
||||
#if !os(visionOS)
|
||||
private(set) var fastAccountSwitcher: FastAccountSwitcherViewController!
|
||||
private var fastSwitcherIndicator: FastAccountSwitcherIndicatorView!
|
||||
private var fastSwitcherConstraints: [NSLayoutConstraint] = []
|
||||
#endif
|
||||
|
||||
init(mastodonController: MastodonController) {
|
||||
self.mastodonController = mastodonController
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func show(_ vc: UIViewController, sender: Any?) {
|
||||
if let nav = selectedViewController as? UINavigationController {
|
||||
nav.pushViewController(vc, animated: true)
|
||||
} else {
|
||||
present(vc, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
// Fast account switcher is not supported on visionOS
|
||||
#if !os(visionOS)
|
||||
func setupFastAccountSwitcher() {
|
||||
fastAccountSwitcher = FastAccountSwitcherViewController()
|
||||
fastAccountSwitcher.delegate = self
|
||||
fastAccountSwitcher.view.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
tabBar.addGestureRecognizer(fastAccountSwitcher.createSwitcherGesture())
|
||||
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(tabBarTapped))
|
||||
tapRecognizer.cancelsTouchesInView = false
|
||||
tabBar.addGestureRecognizer(tapRecognizer)
|
||||
|
||||
if findMyProfileTabBarButton() != nil {
|
||||
fastSwitcherIndicator = FastAccountSwitcherIndicatorView()
|
||||
fastSwitcherIndicator.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.addSubview(fastSwitcherIndicator)
|
||||
}
|
||||
}
|
||||
|
||||
override func viewDidLayoutSubviews() {
|
||||
super.viewDidLayoutSubviews()
|
||||
|
||||
// i hate that we have to do this so often :S
|
||||
// but doing it only in viewWillAppear makes it not appear initially
|
||||
// doing it in viewWillAppear inside a DispatchQueue.main.async works initially but then it disappears when long-pressed
|
||||
repositionFastSwitcherIndicator()
|
||||
}
|
||||
|
||||
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||
super.traitCollectionDidChange(previousTraitCollection)
|
||||
|
||||
repositionFastSwitcherIndicator()
|
||||
}
|
||||
|
||||
private func repositionFastSwitcherIndicator() {
|
||||
guard let myProfileButton = findMyProfileTabBarButton() else {
|
||||
return
|
||||
}
|
||||
NSLayoutConstraint.deactivate(fastSwitcherConstraints)
|
||||
let isPortrait = view.bounds.width < view.bounds.height
|
||||
if traitCollection.horizontalSizeClass == .compact && isPortrait {
|
||||
fastSwitcherConstraints = [
|
||||
fastSwitcherIndicator.centerYAnchor.constraint(equalTo: myProfileButton.centerYAnchor, constant: -4),
|
||||
// tab bar button image width is 30
|
||||
fastSwitcherIndicator.leftAnchor.constraint(equalTo: myProfileButton.centerXAnchor, constant: 15 + 2),
|
||||
]
|
||||
} else {
|
||||
fastSwitcherConstraints = [
|
||||
fastSwitcherIndicator.centerYAnchor.constraint(equalTo: myProfileButton.centerYAnchor),
|
||||
fastSwitcherIndicator.trailingAnchor.constraint(equalTo: myProfileButton.trailingAnchor),
|
||||
]
|
||||
}
|
||||
NSLayoutConstraint.activate(fastSwitcherConstraints)
|
||||
}
|
||||
|
||||
private func findMyProfileTabBarButton() -> UIView? {
|
||||
let tabBarButtons = tabBar.subviews.filter { String(describing: type(of: $0)).lowercased().contains("button") }
|
||||
let tabCount: Int?
|
||||
if #available(iOS 18.0, *) {
|
||||
tabCount = viewControllers?.count ?? tabs.count
|
||||
} else {
|
||||
tabCount = viewControllers?.count
|
||||
}
|
||||
// sanity check that there is 1 button per VC
|
||||
guard tabBarButtons.count == tabCount,
|
||||
let myProfileButton = tabBarButtons.last else {
|
||||
return nil
|
||||
}
|
||||
return myProfileButton
|
||||
}
|
||||
|
||||
@objc private func tabBarTapped(_ recognizer: UITapGestureRecognizer) {
|
||||
fastAccountSwitcher.hide()
|
||||
}
|
||||
#endif // !os(visionOS)
|
||||
|
||||
}
|
||||
|
||||
#if !os(visionOS)
|
||||
extension BaseMainTabBarViewController: FastAccountSwitcherViewControllerDelegate {
|
||||
func fastAccountSwitcherAddToViewHierarchy(_ fastAccountSwitcher: FastAccountSwitcherViewController) {
|
||||
fastAccountSwitcher.view.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.addSubview(fastAccountSwitcher.view)
|
||||
NSLayoutConstraint.activate([
|
||||
fastAccountSwitcher.accountsStack.bottomAnchor.constraint(equalTo: fastAccountSwitcher.view.bottomAnchor),
|
||||
|
||||
fastAccountSwitcher.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
fastAccountSwitcher.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||
fastAccountSwitcher.view.topAnchor.constraint(equalTo: view.topAnchor),
|
||||
fastAccountSwitcher.view.bottomAnchor.constraint(equalTo: tabBar.topAnchor),
|
||||
|
||||
// The safe area insets don't automatically propagate for some reason, so do it ourselves.
|
||||
fastAccountSwitcher.view.safeAreaLayoutGuide.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
|
||||
fastAccountSwitcher.view.safeAreaLayoutGuide.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
|
||||
])
|
||||
}
|
||||
|
||||
func fastAccountSwitcher(_ fastAccountSwitcher: FastAccountSwitcherViewController, triggerZoneContains point: CGPoint) -> Bool {
|
||||
guard let myProfileButton = findMyProfileTabBarButton() else {
|
||||
return false
|
||||
}
|
||||
let locationInButton = myProfileButton.convert(point, from: tabBar)
|
||||
return myProfileButton.bounds.contains(locationInButton)
|
||||
}
|
||||
}
|
||||
#endif // !os(visionOS)
|
||||
|
||||
extension BaseMainTabBarViewController: TuskerNavigationDelegate {
|
||||
var apiController: MastodonController! { mastodonController }
|
||||
}
|
||||
|
||||
extension BaseMainTabBarViewController: StateRestorableViewController {
|
||||
func stateRestorationActivity() -> NSUserActivity? {
|
||||
var activity: NSUserActivity?
|
||||
if let presentedNav = presentedViewController as? UINavigationController,
|
||||
let compose = presentedNav.viewControllers.first as? ComposeHostingController {
|
||||
let draft = compose.controller.draft
|
||||
activity = UserActivityManager.editDraftActivity(id: draft.id, accountID: draft.accountID)
|
||||
} else if let vc = (selectedViewController as! UINavigationController).topViewController as? StateRestorableViewController {
|
||||
activity = vc.stateRestorationActivity()
|
||||
}
|
||||
if activity == nil {
|
||||
stateRestorationLogger.fault("MainTabBarViewController: Unable to create state restoration activity, couldn't find StateRestorableViewController")
|
||||
}
|
||||
return activity
|
||||
}
|
||||
}
|
||||
|
||||
extension BaseMainTabBarViewController: BackgroundableViewController {
|
||||
func sceneDidEnterBackground() {
|
||||
if let selectedVC = selectedViewController as? BackgroundableViewController {
|
||||
selectedVC.sceneDidEnterBackground()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension BaseMainTabBarViewController: StatusBarTappableViewController {
|
||||
func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult {
|
||||
guard presentedViewController == nil else {
|
||||
return .stop
|
||||
}
|
||||
guard let vc = selectedViewController as? StatusBarTappableViewController else {
|
||||
return .continue
|
||||
}
|
||||
return vc.handleStatusBarTapped(xPosition: xPosition)
|
||||
}
|
||||
}
|
|
@ -23,8 +23,8 @@ extension DuckableContainerViewController: AccountSwitchableViewController {
|
|||
return activity
|
||||
}
|
||||
|
||||
func compose(editing draft: Draft?, animated: Bool, isDucked: Bool) {
|
||||
(child as? TuskerRootViewController)?.compose(editing: draft, animated: animated, isDucked: isDucked)
|
||||
func compose(editing draft: Draft?, animated: Bool, isDucked: Bool, completion: (() -> Void)?) {
|
||||
(child as? TuskerRootViewController)?.compose(editing: draft, animated: animated, isDucked: isDucked, completion: completion)
|
||||
}
|
||||
|
||||
func getNavigationDelegate() -> TuskerNavigationDelegate? {
|
||||
|
@ -39,10 +39,6 @@ extension DuckableContainerViewController: AccountSwitchableViewController {
|
|||
(child as? TuskerRootViewController)?.select(route: route, animated: animated, completion: completion)
|
||||
}
|
||||
|
||||
func getTabController(tab: MainTabBarViewController.Tab) -> UIViewController? {
|
||||
return (child as? TuskerRootViewController)?.getTabController(tab: tab)
|
||||
}
|
||||
|
||||
func performSearch(query: String) {
|
||||
(child as? TuskerRootViewController)?.performSearch(query: query)
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ import UIKit
|
|||
import Combine
|
||||
import TuskerPreferences
|
||||
|
||||
@available(iOS, obsoleted: 18.0)
|
||||
class MainSplitViewController: UISplitViewController {
|
||||
|
||||
private let mastodonController: MastodonController
|
||||
|
@ -578,20 +579,6 @@ extension MainSplitViewController: TuskerRootViewController {
|
|||
completion?()
|
||||
}
|
||||
|
||||
func getTabController(tab: MainTabBarViewController.Tab) -> UIViewController? {
|
||||
if traitCollection.horizontalSizeClass == .compact {
|
||||
return tabBarViewController?.getTabController(tab: tab)
|
||||
} else {
|
||||
if tab == .compose {
|
||||
return nil
|
||||
} else if case .tab(tab) = sidebar.selectedItem {
|
||||
return secondaryNavController
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getNavigationDelegate() -> TuskerNavigationDelegate? {
|
||||
if traitCollection.horizontalSizeClass == .compact {
|
||||
return tabBarViewController.getNavigationDelegate()
|
||||
|
|
|
@ -9,18 +9,11 @@
|
|||
import UIKit
|
||||
import ComposeUI
|
||||
|
||||
class MainTabBarViewController: UITabBarController, UITabBarControllerDelegate {
|
||||
|
||||
private let mastodonController: MastodonController
|
||||
@available(iOS, obsoleted: 18.0)
|
||||
class MainTabBarViewController: BaseMainTabBarViewController {
|
||||
|
||||
private var composePlaceholder: UIViewController!
|
||||
|
||||
#if !os(visionOS)
|
||||
private var fastAccountSwitcher: FastAccountSwitcherViewController!
|
||||
private var fastSwitcherIndicator: FastAccountSwitcherIndicatorView!
|
||||
private var fastSwitcherConstraints: [NSLayoutConstraint] = []
|
||||
#endif
|
||||
|
||||
var currentTab: Tab {
|
||||
return Tab(rawValue: selectedIndex)!
|
||||
}
|
||||
|
@ -33,16 +26,6 @@ class MainTabBarViewController: UITabBarController, UITabBarControllerDelegate {
|
|||
}
|
||||
}
|
||||
|
||||
init(mastodonController: MastodonController) {
|
||||
self.mastodonController = mastodonController
|
||||
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
|
@ -62,46 +45,13 @@ class MainTabBarViewController: UITabBarController, UITabBarControllerDelegate {
|
|||
embedInNavigationController(Tab.myProfile.createViewController(mastodonController)),
|
||||
]
|
||||
|
||||
#if !os(visionOS)
|
||||
fastAccountSwitcher = FastAccountSwitcherViewController()
|
||||
fastAccountSwitcher.delegate = self
|
||||
fastAccountSwitcher.view.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
tabBar.addGestureRecognizer(fastAccountSwitcher.createSwitcherGesture())
|
||||
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(tabBarTapped))
|
||||
tapRecognizer.cancelsTouchesInView = false
|
||||
tabBar.addGestureRecognizer(tapRecognizer)
|
||||
|
||||
if findMyProfileTabBarButton() != nil {
|
||||
fastSwitcherIndicator = FastAccountSwitcherIndicatorView()
|
||||
fastSwitcherIndicator.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.addSubview(fastSwitcherIndicator)
|
||||
}
|
||||
#endif
|
||||
setupFastAccountSwitcher()
|
||||
|
||||
tabBar.isSpringLoaded = true
|
||||
|
||||
view.backgroundColor = .appBackground
|
||||
}
|
||||
|
||||
// Fast account switcher is not supported on visionOS
|
||||
#if !os(visionOS)
|
||||
override func viewDidLayoutSubviews() {
|
||||
super.viewDidLayoutSubviews()
|
||||
|
||||
// i hate that we have to do this so often :S
|
||||
// but doing it only in viewWillAppear makes it not appear initially
|
||||
// doing it in viewWillAppear inside a DispatchQueue.main.async works initially but then it disappears when long-pressed
|
||||
repositionFastSwitcherIndicator()
|
||||
}
|
||||
|
||||
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||
super.traitCollectionDidChange(previousTraitCollection)
|
||||
|
||||
repositionFastSwitcherIndicator()
|
||||
}
|
||||
#endif
|
||||
|
||||
func select(tab: Tab, dismissPresented: Bool) {
|
||||
if tab == .compose {
|
||||
compose(editing: nil)
|
||||
|
@ -119,53 +69,6 @@ class MainTabBarViewController: UITabBarController, UITabBarControllerDelegate {
|
|||
}
|
||||
}
|
||||
|
||||
override func show(_ vc: UIViewController, sender: Any?) {
|
||||
if let nav = selectedViewController as? UINavigationController {
|
||||
nav.pushViewController(vc, animated: true)
|
||||
} else {
|
||||
present(vc, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
#if !os(visionOS)
|
||||
private func repositionFastSwitcherIndicator() {
|
||||
guard let myProfileButton = findMyProfileTabBarButton() else {
|
||||
return
|
||||
}
|
||||
NSLayoutConstraint.deactivate(fastSwitcherConstraints)
|
||||
let isPortrait = view.bounds.width < view.bounds.height
|
||||
if traitCollection.horizontalSizeClass == .compact && isPortrait {
|
||||
fastSwitcherConstraints = [
|
||||
fastSwitcherIndicator.centerYAnchor.constraint(equalTo: myProfileButton.centerYAnchor, constant: -4),
|
||||
// tab bar button image width is 30
|
||||
fastSwitcherIndicator.leftAnchor.constraint(equalTo: myProfileButton.centerXAnchor, constant: 15 + 2),
|
||||
]
|
||||
} else {
|
||||
fastSwitcherConstraints = [
|
||||
fastSwitcherIndicator.centerYAnchor.constraint(equalTo: myProfileButton.centerYAnchor),
|
||||
fastSwitcherIndicator.trailingAnchor.constraint(equalTo: myProfileButton.trailingAnchor),
|
||||
]
|
||||
}
|
||||
NSLayoutConstraint.activate(fastSwitcherConstraints)
|
||||
}
|
||||
#endif
|
||||
|
||||
private func findMyProfileTabBarButton() -> UIView? {
|
||||
let tabBarButtons = tabBar.subviews.filter { String(describing: type(of: $0)).lowercased().contains("button") }
|
||||
// sanity check that there is 1 button per VC
|
||||
guard tabBarButtons.count == viewControllers!.count,
|
||||
let myProfileButton = tabBarButtons.last else {
|
||||
return nil
|
||||
}
|
||||
return myProfileButton
|
||||
}
|
||||
|
||||
#if !os(visionOS)
|
||||
@objc private func tabBarTapped(_ recognizer: UITapGestureRecognizer) {
|
||||
fastAccountSwitcher.hide()
|
||||
}
|
||||
#endif
|
||||
|
||||
@objc func handleComposeKeyCommand() {
|
||||
compose(editing: nil)
|
||||
}
|
||||
|
@ -179,22 +82,6 @@ class MainTabBarViewController: UITabBarController, UITabBarControllerDelegate {
|
|||
}
|
||||
}
|
||||
|
||||
func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
|
||||
if viewController == composePlaceholder {
|
||||
compose(editing: nil)
|
||||
return false
|
||||
}
|
||||
if selectedIndex != NSNotFound,
|
||||
viewController == viewControllers![selectedIndex],
|
||||
let nav = viewController as? UINavigationController,
|
||||
nav.viewControllers.count == 1,
|
||||
let scrollableVC = nav.viewControllers.first as? TabBarScrollableViewController {
|
||||
scrollableVC.tabBarScrollToTop()
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func setViewController(_ viewController: UIViewController, forTab tab: Tab) {
|
||||
viewControllers![tab.rawValue] = viewController
|
||||
}
|
||||
|
@ -229,7 +116,7 @@ extension MainTabBarViewController {
|
|||
}
|
||||
}
|
||||
|
||||
func getTabController(tab: Tab) -> UIViewController? {
|
||||
private func getTabController(tab: Tab) -> UIViewController? {
|
||||
if tab == .compose {
|
||||
return nil
|
||||
} else {
|
||||
|
@ -240,53 +127,21 @@ extension MainTabBarViewController {
|
|||
}
|
||||
}
|
||||
|
||||
#if !os(visionOS)
|
||||
extension MainTabBarViewController: FastAccountSwitcherViewControllerDelegate {
|
||||
func fastAccountSwitcherAddToViewHierarchy(_ fastAccountSwitcher: FastAccountSwitcherViewController) {
|
||||
fastAccountSwitcher.view.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.addSubview(fastAccountSwitcher.view)
|
||||
NSLayoutConstraint.activate([
|
||||
fastAccountSwitcher.accountsStack.bottomAnchor.constraint(equalTo: fastAccountSwitcher.view.bottomAnchor),
|
||||
|
||||
fastAccountSwitcher.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
fastAccountSwitcher.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||
fastAccountSwitcher.view.topAnchor.constraint(equalTo: view.topAnchor),
|
||||
fastAccountSwitcher.view.bottomAnchor.constraint(equalTo: tabBar.topAnchor),
|
||||
|
||||
// The safe area insets don't automatically propagate for some reason, so do it ourselves.
|
||||
fastAccountSwitcher.view.safeAreaLayoutGuide.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
|
||||
fastAccountSwitcher.view.safeAreaLayoutGuide.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
|
||||
])
|
||||
}
|
||||
|
||||
func fastAccountSwitcher(_ fastAccountSwitcher: FastAccountSwitcherViewController, triggerZoneContains point: CGPoint) -> Bool {
|
||||
guard let myProfileButton = findMyProfileTabBarButton() else {
|
||||
extension MainTabBarViewController: UITabBarControllerDelegate {
|
||||
func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
|
||||
if viewController == composePlaceholder {
|
||||
compose(editing: nil)
|
||||
return false
|
||||
}
|
||||
let locationInButton = myProfileButton.convert(point, from: tabBar)
|
||||
return myProfileButton.bounds.contains(locationInButton)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
extension MainTabBarViewController: TuskerNavigationDelegate {
|
||||
var apiController: MastodonController! { mastodonController }
|
||||
}
|
||||
|
||||
extension MainTabBarViewController: StateRestorableViewController {
|
||||
func stateRestorationActivity() -> NSUserActivity? {
|
||||
var activity: NSUserActivity?
|
||||
if let presentedNav = presentedViewController as? UINavigationController,
|
||||
let compose = presentedNav.viewControllers.first as? ComposeHostingController {
|
||||
let draft = compose.controller.draft
|
||||
activity = UserActivityManager.editDraftActivity(id: draft.id, accountID: draft.accountID)
|
||||
} else if let vc = (selectedViewController as! UINavigationController).topViewController as? StateRestorableViewController {
|
||||
activity = vc.stateRestorationActivity()
|
||||
if selectedIndex != NSNotFound,
|
||||
viewController == viewControllers![selectedIndex],
|
||||
let nav = viewController as? UINavigationController,
|
||||
nav.viewControllers.count == 1,
|
||||
let scrollableVC = nav.viewControllers.first as? TabBarScrollableViewController {
|
||||
scrollableVC.tabBarScrollToTop()
|
||||
return false
|
||||
}
|
||||
if activity == nil {
|
||||
stateRestorationLogger.fault("MainTabBarViewController: Unable to create state restoration activity, couldn't find StateRestorableViewController")
|
||||
}
|
||||
return activity
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -350,24 +205,6 @@ extension MainTabBarViewController: TuskerRootViewController {
|
|||
present(vc, animated: true, completion: completion)
|
||||
return vc
|
||||
}
|
||||
|
||||
func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult {
|
||||
guard presentedViewController == nil else {
|
||||
return .stop
|
||||
}
|
||||
guard let vc = viewController(for: currentTab) as? StatusBarTappableViewController else {
|
||||
return .continue
|
||||
}
|
||||
return vc.handleStatusBarTapped(xPosition: xPosition)
|
||||
}
|
||||
}
|
||||
|
||||
extension MainTabBarViewController: BackgroundableViewController {
|
||||
func sceneDidEnterBackground() {
|
||||
if let selectedVC = selectedViewController as? BackgroundableViewController {
|
||||
selectedVC.sceneDidEnterBackground()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension MainTabBarViewController: AccountSwitchableViewController {
|
||||
|
|
|
@ -0,0 +1,222 @@
|
|||
//
|
||||
// NewMainTabBarViewController.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 8/19/24.
|
||||
// Copyright © 2024 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
@available(iOS 18.0, *)
|
||||
class NewMainTabBarViewController: BaseMainTabBarViewController {
|
||||
|
||||
private let composePlaceholder = UIViewController()
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
mode = .tabSidebar
|
||||
delegate = self
|
||||
tabBar.isSpringLoaded = true
|
||||
view.backgroundColor = .appBackground
|
||||
|
||||
let viewControllerProvider = { [unowned self] (tab: UITab) -> UIViewController in
|
||||
self.makeViewController(for: tab)
|
||||
}
|
||||
|
||||
let topLevelTabs = [
|
||||
Tab.home,
|
||||
.notifications,
|
||||
.compose,
|
||||
.explore,
|
||||
.myProfile
|
||||
].map {
|
||||
UITab(title: $0.title, image: UIImage(systemName: $0.imageName), identifier: $0.rawValue, viewControllerProvider: viewControllerProvider)
|
||||
}
|
||||
|
||||
self.tabs = topLevelTabs
|
||||
|
||||
setupFastAccountSwitcher()
|
||||
}
|
||||
|
||||
private func makeViewController(for tab: UITab) -> UIViewController {
|
||||
guard let tab = Tab(rawValue: tab.identifier) else {
|
||||
fatalError("unreachable")
|
||||
}
|
||||
let root: UIViewController
|
||||
switch tab {
|
||||
case .home:
|
||||
root = TimelinesPageViewController(mastodonController: mastodonController)
|
||||
case .notifications:
|
||||
root = NotificationsPageViewController(mastodonController: mastodonController)
|
||||
case .compose:
|
||||
return composePlaceholder
|
||||
case .explore:
|
||||
root = ExploreViewController(mastodonController: mastodonController)
|
||||
case .myProfile:
|
||||
root = MyProfileViewController(mastodonController: mastodonController)
|
||||
}
|
||||
return EnhancedNavigationViewController(rootViewController: root)
|
||||
}
|
||||
|
||||
@objc func handleComposeKeyCommand() {
|
||||
compose(editing: nil)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@available(iOS 18.0, *)
|
||||
extension NewMainTabBarViewController {
|
||||
enum Tab: String, Hashable, CaseIterable {
|
||||
case home
|
||||
case notifications
|
||||
case compose
|
||||
case explore
|
||||
case myProfile
|
||||
|
||||
var title: String {
|
||||
switch self {
|
||||
case .home:
|
||||
"Home"
|
||||
case .notifications:
|
||||
"Notifications"
|
||||
case .compose:
|
||||
"Compose"
|
||||
case .explore:
|
||||
"Explore"
|
||||
case .myProfile:
|
||||
"My Profile"
|
||||
}
|
||||
}
|
||||
|
||||
var imageName: String {
|
||||
switch self {
|
||||
case .home:
|
||||
"house"
|
||||
case .notifications:
|
||||
"bell"
|
||||
case .compose:
|
||||
"pencil"
|
||||
case .explore:
|
||||
"magnifyingglass"
|
||||
case .myProfile:
|
||||
"person"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 18.0, *)
|
||||
extension NewMainTabBarViewController: UITabBarControllerDelegate {
|
||||
func tabBarController(_ tabBarController: UITabBarController, shouldSelectTab tab: UITab) -> Bool {
|
||||
if tab.identifier == Tab.compose.rawValue {
|
||||
let currentTab = selectedTab
|
||||
compose(editing: nil) {
|
||||
// returning false for shouldSelectTab doesn't prevent the UITabBar from being updated (FB14857254)
|
||||
// but we need it to change to _something_ so that we can change back to the current tab
|
||||
self.selectedTab = tab
|
||||
self.selectedTab = currentTab
|
||||
}
|
||||
return false
|
||||
} else if let selectedTab,
|
||||
selectedTab == tab,
|
||||
let nav = selectedViewController as? any NavigationControllerProtocol,
|
||||
nav.viewControllers.count == 1,
|
||||
let scrollableVC = nav.viewControllers[0] as? TabBarScrollableViewController {
|
||||
scrollableVC.tabBarScrollToTop()
|
||||
return false
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 18.0, *)
|
||||
extension NewMainTabBarViewController: TuskerRootViewController {
|
||||
func select(route: TuskerRoute, animated: Bool, completion: (() -> Void)?) {
|
||||
func doSelect() {
|
||||
switch route {
|
||||
case .timelines:
|
||||
selectedTab = tab(forIdentifier: Tab.home.rawValue)
|
||||
case .notifications:
|
||||
selectedTab = tab(forIdentifier: Tab.notifications.rawValue)
|
||||
case .myProfile:
|
||||
selectedTab = tab(forIdentifier: Tab.myProfile.rawValue)
|
||||
case .explore:
|
||||
selectedTab = tab(forIdentifier: Tab.explore.rawValue)
|
||||
case .bookmarks:
|
||||
selectedTab = tab(forIdentifier: Tab.explore.rawValue)
|
||||
let nav = getNavigationController()
|
||||
nav.popToRootViewController(animated: animated)
|
||||
nav.pushViewController(BookmarksViewController(mastodonController: mastodonController), animated: animated)
|
||||
case .list(let id):
|
||||
selectedTab = tab(forIdentifier: Tab.explore.rawValue)
|
||||
if let list = mastodonController.getCachedList(id: id) {
|
||||
let nav = getNavigationController()
|
||||
nav.popToRootViewController(animated: animated)
|
||||
nav.pushViewController(ListTimelineViewController(for: list, mastodonController: mastodonController), animated: animated)
|
||||
}
|
||||
}
|
||||
completion?()
|
||||
}
|
||||
if presentedViewController != nil {
|
||||
dismiss(animated: animated) {
|
||||
doSelect()
|
||||
}
|
||||
} else {
|
||||
doSelect()
|
||||
}
|
||||
}
|
||||
|
||||
func getNavigationDelegate() -> (any TuskerNavigationDelegate)? {
|
||||
return self
|
||||
}
|
||||
|
||||
func getNavigationController() -> any NavigationControllerProtocol {
|
||||
return selectedViewController as! any NavigationControllerProtocol
|
||||
}
|
||||
|
||||
func performSearch(query: String) {
|
||||
selectedTab = tab(forIdentifier: Tab.explore.rawValue)
|
||||
guard let exploreNavController = selectedViewController as? any NavigationControllerProtocol,
|
||||
let exploreController = exploreNavController.viewControllers.first as? ExploreViewController else {
|
||||
return
|
||||
}
|
||||
|
||||
exploreNavController.popToRootViewController(animated: false)
|
||||
|
||||
// setting searchController.isActive directly doesn't work until the view has loaded/appeared for the first time
|
||||
if exploreController.isViewLoaded {
|
||||
exploreController.searchController.isActive = true
|
||||
} else {
|
||||
exploreController.searchControllerStatusOnAppearance = true
|
||||
// we still need to load the view so that we can setup the search query
|
||||
exploreController.loadViewIfNeeded()
|
||||
}
|
||||
|
||||
exploreController.searchController.searchBar.text = query
|
||||
exploreController.resultsController.performSearch(query: query)
|
||||
}
|
||||
|
||||
func presentPreferences(completion: (() -> Void)?) -> PreferencesNavigationController? {
|
||||
let vc = PreferencesNavigationController(mastodonController: mastodonController)
|
||||
present(vc, animated: true, completion: completion)
|
||||
return vc
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 18.0, *)
|
||||
extension NewMainTabBarViewController: AccountSwitchableViewController {
|
||||
var isFastAccountSwitcherActive: Bool {
|
||||
#if os(visionOS)
|
||||
return false
|
||||
#else
|
||||
if let fastAccountSwitcher {
|
||||
return !fastAccountSwitcher.view.isHidden
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
|
@ -11,9 +11,8 @@ import ComposeUI
|
|||
|
||||
@MainActor
|
||||
protocol TuskerRootViewController: UIViewController, StateRestorableViewController, StatusBarTappableViewController {
|
||||
func compose(editing draft: Draft?, animated: Bool, isDucked: Bool)
|
||||
func compose(editing draft: Draft?, animated: Bool, isDucked: Bool, completion: (() -> Void)?)
|
||||
func select(route: TuskerRoute, animated: Bool, completion: (() -> Void)?)
|
||||
func getTabController(tab: MainTabBarViewController.Tab) -> UIViewController?
|
||||
func getNavigationDelegate() -> TuskerNavigationDelegate?
|
||||
func getNavigationController() -> NavigationControllerProtocol
|
||||
func performSearch(query: String)
|
||||
|
|
|
@ -48,7 +48,7 @@ enum AppShortcutItem: String, CaseIterable {
|
|||
case .showNotifications:
|
||||
root.select(route: .notifications, animated: false, completion: nil)
|
||||
case .composePost:
|
||||
root.compose(editing: nil, animated: false, isDucked: false)
|
||||
root.compose(editing: nil, animated: false, isDucked: false, completion: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -109,10 +109,10 @@ class StateRestorationUserActivityHandlingContext: UserActivityHandlingContext {
|
|||
func compose(editing draft: Draft) {
|
||||
if #available(iOS 16.0, *),
|
||||
UIDevice.current.userInterfaceIdiom == .phone {
|
||||
self.root.compose(editing: draft, animated: false, isDucked: true)
|
||||
self.root.compose(editing: draft, animated: false, isDucked: true, completion: nil)
|
||||
} else {
|
||||
DispatchQueue.main.async {
|
||||
self.root.compose(editing: draft, animated: true, isDucked: false)
|
||||
self.root.compose(editing: draft, animated: true, isDucked: false, completion: nil)
|
||||
}
|
||||
}
|
||||
state = .presented
|
||||
|
@ -123,7 +123,7 @@ class StateRestorationUserActivityHandlingContext: UserActivityHandlingContext {
|
|||
#if !os(visionOS)
|
||||
if #available(iOS 16.0, *),
|
||||
let duckedDraft = UserActivityManager.getDuckedDraft(from: activity) {
|
||||
self.root.compose(editing: duckedDraft, animated: false, isDucked: true)
|
||||
self.root.compose(editing: duckedDraft, animated: false, isDucked: true, completion: nil)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
|
|
@ -96,7 +96,7 @@ extension TuskerNavigationDelegate {
|
|||
show(ConversationViewController(for: statusID, state: state, mastodonController: apiController), sender: self)
|
||||
}
|
||||
|
||||
func compose(editing draft: Draft?, animated: Bool = true, isDucked: Bool = false) {
|
||||
func compose(editing draft: Draft?, animated: Bool = true, isDucked: Bool = false, completion: (() -> Void)? = nil) {
|
||||
let draft = draft ?? apiController.createDraft()
|
||||
let visionIdiom = UIUserInterfaceIdiom(rawValue: 6) // .vision is not available pre-iOS 17 :S
|
||||
if [visionIdiom, .pad, .mac].contains(UIDevice.current.userInterfaceIdiom) {
|
||||
|
@ -108,16 +108,17 @@ extension TuskerNavigationDelegate {
|
|||
options.preferredPresentationStyle = .prominent
|
||||
#endif
|
||||
UIApplication.shared.requestSceneSessionActivation(nil, userActivity: compose, options: options, errorHandler: nil)
|
||||
completion?()
|
||||
} else {
|
||||
let compose = ComposeHostingController(draft: draft, mastodonController: apiController)
|
||||
#if os(visionOS)
|
||||
fatalError("unreachable")
|
||||
#else
|
||||
if #available(iOS 16.0, *),
|
||||
presentDuckable(compose, animated: animated, isDucked: isDucked) {
|
||||
presentDuckable(compose, animated: animated, isDucked: isDucked, completion: completion) {
|
||||
return
|
||||
} else {
|
||||
present(compose, animated: animated)
|
||||
present(compose, animated: animated, completion: completion)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue