forked from shadowfacts/Tusker
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 {
|
extension UIViewController {
|
||||||
@available(iOS 16.0, *)
|
@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
|
var cur: UIViewController? = self
|
||||||
while let vc = cur {
|
while let vc = cur {
|
||||||
if let container = vc as? DuckableContainerViewController {
|
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
|
return true
|
||||||
} else {
|
} else {
|
||||||
cur = vc.parent
|
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 {
|
guard case .idle = state else {
|
||||||
if animated,
|
if animated,
|
||||||
case .ducked(_, placeholder: let placeholder) = state {
|
case .ducked(_, placeholder: let placeholder) = state {
|
||||||
|
|
|
@ -129,6 +129,8 @@
|
||||||
D646DCD82A07F3500059ECEB /* FollowRequestNotificationCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D646DCD72A07F3500059ECEB /* FollowRequestNotificationCollectionViewCell.swift */; };
|
D646DCD82A07F3500059ECEB /* FollowRequestNotificationCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D646DCD72A07F3500059ECEB /* FollowRequestNotificationCollectionViewCell.swift */; };
|
||||||
D646DCDA2A081A2C0059ECEB /* PollFinishedNotificationCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D646DCD92A081A2C0059ECEB /* PollFinishedNotificationCollectionViewCell.swift */; };
|
D646DCDA2A081A2C0059ECEB /* PollFinishedNotificationCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D646DCD92A081A2C0059ECEB /* PollFinishedNotificationCollectionViewCell.swift */; };
|
||||||
D646DCDC2A081CF10059ECEB /* StatusUpdatedNotificationCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D646DCDB2A081CF10059ECEB /* StatusUpdatedNotificationCollectionViewCell.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 */; };
|
D64AAE9126C80DC600FC57FB /* ToastView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64AAE9026C80DC600FC57FB /* ToastView.swift */; };
|
||||||
D64AAE9526C88C5000FC57FB /* ToastableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64AAE9426C88C5000FC57FB /* ToastableViewController.swift */; };
|
D64AAE9526C88C5000FC57FB /* ToastableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64AAE9426C88C5000FC57FB /* ToastableViewController.swift */; };
|
||||||
D64AAE9726C88DC400FC57FB /* ToastConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64AAE9626C88DC400FC57FB /* ToastConfiguration.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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
D64AAE9626C88DC400FC57FB /* ToastConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastConfiguration.swift; sourceTree = "<group>"; };
|
||||||
|
@ -1124,7 +1128,9 @@
|
||||||
D6FF985F255C717400845181 /* AccountSwitchingContainerViewController.swift */,
|
D6FF985F255C717400845181 /* AccountSwitchingContainerViewController.swift */,
|
||||||
D6BEA246291A0F2D002F4D01 /* Duckable+Root.swift */,
|
D6BEA246291A0F2D002F4D01 /* Duckable+Root.swift */,
|
||||||
D6F0B12A24A3071C001E48C3 /* MainSplitViewController.swift */,
|
D6F0B12A24A3071C001E48C3 /* MainSplitViewController.swift */,
|
||||||
|
D64A50472C739DEA009D7193 /* BaseMainTabBarViewController.swift */,
|
||||||
04DACE8B212CB14B009840C4 /* MainTabBarViewController.swift */,
|
04DACE8B212CB14B009840C4 /* MainTabBarViewController.swift */,
|
||||||
|
D64A50452C739DC0009D7193 /* NewMainTabBarViewController.swift */,
|
||||||
D6F0B17424A3A1AA001E48C3 /* MainSidebarViewController.swift */,
|
D6F0B17424A3A1AA001E48C3 /* MainSidebarViewController.swift */,
|
||||||
D6B93666281D937300237D0E /* MainSidebarMyProfileCollectionViewCell.swift */,
|
D6B93666281D937300237D0E /* MainSidebarMyProfileCollectionViewCell.swift */,
|
||||||
D68E525A24A3D77E0054355A /* TuskerRootViewController.swift */,
|
D68E525A24A3D77E0054355A /* TuskerRootViewController.swift */,
|
||||||
|
@ -2141,6 +2147,7 @@
|
||||||
D61F75A5293ABD6F00C0B37F /* EditFilterView.swift in Sources */,
|
D61F75A5293ABD6F00C0B37F /* EditFilterView.swift in Sources */,
|
||||||
D6BC9DB1232C61BC002CA326 /* NotificationsPageViewController.swift in Sources */,
|
D6BC9DB1232C61BC002CA326 /* NotificationsPageViewController.swift in Sources */,
|
||||||
D6ADB6EC28EA73CB009924AB /* StatusContentContainer.swift in Sources */,
|
D6ADB6EC28EA73CB009924AB /* StatusContentContainer.swift in Sources */,
|
||||||
|
D64A50462C739DC0009D7193 /* NewMainTabBarViewController.swift in Sources */,
|
||||||
D6969E9E240C81B9002843CE /* NSTextAttachment+Emoji.swift in Sources */,
|
D6969E9E240C81B9002843CE /* NSTextAttachment+Emoji.swift in Sources */,
|
||||||
D6D79F532A0FFE3200AB2315 /* ToggleableButton.swift in Sources */,
|
D6D79F532A0FFE3200AB2315 /* ToggleableButton.swift in Sources */,
|
||||||
D6BC9DB3232D4C07002CA326 /* WellnessPrefsView.swift in Sources */,
|
D6BC9DB3232D4C07002CA326 /* WellnessPrefsView.swift in Sources */,
|
||||||
|
@ -2193,6 +2200,7 @@
|
||||||
D6934F322BA7E43E002B1C8D /* LoadingGalleryContentViewController.swift in Sources */,
|
D6934F322BA7E43E002B1C8D /* LoadingGalleryContentViewController.swift in Sources */,
|
||||||
D68E525B24A3D77E0054355A /* TuskerRootViewController.swift in Sources */,
|
D68E525B24A3D77E0054355A /* TuskerRootViewController.swift in Sources */,
|
||||||
D62D2422217AA7E1005076CC /* UserActivityManager.swift in Sources */,
|
D62D2422217AA7E1005076CC /* UserActivityManager.swift in Sources */,
|
||||||
|
D64A50482C739DEA009D7193 /* BaseMainTabBarViewController.swift in Sources */,
|
||||||
D68A76E329524D2A001DA1B3 /* ListMO.swift in Sources */,
|
D68A76E329524D2A001DA1B3 /* ListMO.swift in Sources */,
|
||||||
D63CC70C2910AADB000E19DE /* TuskerSceneDelegate.swift in Sources */,
|
D63CC70C2910AADB000E19DE /* TuskerSceneDelegate.swift in Sources */,
|
||||||
D61F759D2938574B00C0B37F /* FilterRow.swift in Sources */,
|
D61F759D2938574B00C0B37F /* FilterRow.swift in Sources */,
|
||||||
|
|
|
@ -63,7 +63,7 @@ class MainSceneDelegate: UIResponder, UIWindowSceneDelegate, TuskerSceneDelegate
|
||||||
let draft = mastodonController.createDraft()
|
let draft = mastodonController.createDraft()
|
||||||
let text = components.queryItems?.first(where: { $0.name == "text" })?.value
|
let text = components.queryItems?.first(where: { $0.name == "text" })?.value
|
||||||
draft.text = text ?? ""
|
draft.text = text ?? ""
|
||||||
rootViewController.compose(editing: draft, animated: true, isDucked: false)
|
rootViewController.compose(editing: draft, animated: true, isDucked: false, completion: nil)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Assume anything else is a search query
|
// Assume anything else is a search query
|
||||||
|
@ -266,15 +266,24 @@ class MainSceneDelegate: UIResponder, UIWindowSceneDelegate, TuskerSceneDelegate
|
||||||
mastodonController.initialize()
|
mastodonController.initialize()
|
||||||
|
|
||||||
#if os(visionOS)
|
#if os(visionOS)
|
||||||
|
if #available(visionOS 2.0, *) {
|
||||||
|
return NewMainTabBarViewController(mastodonController: mastodonController)
|
||||||
|
} else {
|
||||||
return MainTabBarViewController(mastodonController: mastodonController)
|
return MainTabBarViewController(mastodonController: mastodonController)
|
||||||
|
}
|
||||||
#else
|
#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,
|
if UIDevice.current.userInterfaceIdiom == .phone,
|
||||||
#available(iOS 16.0, *) {
|
#available(iOS 16.0, *) {
|
||||||
// TODO: maybe the duckable container should be outside the account switching container
|
// TODO: maybe the duckable container should be outside the account switching container
|
||||||
return DuckableContainerViewController(child: split)
|
return DuckableContainerViewController(child: mainVC)
|
||||||
} else {
|
} else {
|
||||||
return split
|
return mainVC
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
|
@ -147,9 +147,9 @@ extension AccountSwitchingContainerViewController: TuskerRootViewController {
|
||||||
return root.stateRestorationActivity()
|
return root.stateRestorationActivity()
|
||||||
}
|
}
|
||||||
|
|
||||||
func compose(editing draft: Draft?, animated: Bool, isDucked: Bool) {
|
func compose(editing draft: Draft?, animated: Bool, isDucked: Bool, completion: (() -> Void)?) {
|
||||||
loadViewIfNeeded()
|
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)?) {
|
func select(route: TuskerRoute, animated: Bool, completion: (() -> Void)?) {
|
||||||
|
@ -157,11 +157,6 @@ extension AccountSwitchingContainerViewController: TuskerRootViewController {
|
||||||
root.select(route: route, animated: animated, completion: completion)
|
root.select(route: route, animated: animated, completion: completion)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getTabController(tab: MainTabBarViewController.Tab) -> UIViewController? {
|
|
||||||
loadViewIfNeeded()
|
|
||||||
return root.getTabController(tab: tab)
|
|
||||||
}
|
|
||||||
|
|
||||||
func getNavigationDelegate() -> TuskerNavigationDelegate? {
|
func getNavigationDelegate() -> TuskerNavigationDelegate? {
|
||||||
loadViewIfNeeded()
|
loadViewIfNeeded()
|
||||||
return root.getNavigationDelegate()
|
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
|
return activity
|
||||||
}
|
}
|
||||||
|
|
||||||
func compose(editing draft: Draft?, animated: Bool, isDucked: Bool) {
|
func compose(editing draft: Draft?, animated: Bool, isDucked: Bool, completion: (() -> Void)?) {
|
||||||
(child as? TuskerRootViewController)?.compose(editing: draft, animated: animated, isDucked: isDucked)
|
(child as? TuskerRootViewController)?.compose(editing: draft, animated: animated, isDucked: isDucked, completion: completion)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getNavigationDelegate() -> TuskerNavigationDelegate? {
|
func getNavigationDelegate() -> TuskerNavigationDelegate? {
|
||||||
|
@ -39,10 +39,6 @@ extension DuckableContainerViewController: AccountSwitchableViewController {
|
||||||
(child as? TuskerRootViewController)?.select(route: route, animated: animated, completion: completion)
|
(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) {
|
func performSearch(query: String) {
|
||||||
(child as? TuskerRootViewController)?.performSearch(query: query)
|
(child as? TuskerRootViewController)?.performSearch(query: query)
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,7 @@ import UIKit
|
||||||
import Combine
|
import Combine
|
||||||
import TuskerPreferences
|
import TuskerPreferences
|
||||||
|
|
||||||
|
@available(iOS, obsoleted: 18.0)
|
||||||
class MainSplitViewController: UISplitViewController {
|
class MainSplitViewController: UISplitViewController {
|
||||||
|
|
||||||
private let mastodonController: MastodonController
|
private let mastodonController: MastodonController
|
||||||
|
@ -578,20 +579,6 @@ extension MainSplitViewController: TuskerRootViewController {
|
||||||
completion?()
|
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? {
|
func getNavigationDelegate() -> TuskerNavigationDelegate? {
|
||||||
if traitCollection.horizontalSizeClass == .compact {
|
if traitCollection.horizontalSizeClass == .compact {
|
||||||
return tabBarViewController.getNavigationDelegate()
|
return tabBarViewController.getNavigationDelegate()
|
||||||
|
|
|
@ -9,18 +9,11 @@
|
||||||
import UIKit
|
import UIKit
|
||||||
import ComposeUI
|
import ComposeUI
|
||||||
|
|
||||||
class MainTabBarViewController: UITabBarController, UITabBarControllerDelegate {
|
@available(iOS, obsoleted: 18.0)
|
||||||
|
class MainTabBarViewController: BaseMainTabBarViewController {
|
||||||
private let mastodonController: MastodonController
|
|
||||||
|
|
||||||
private var composePlaceholder: UIViewController!
|
private var composePlaceholder: UIViewController!
|
||||||
|
|
||||||
#if !os(visionOS)
|
|
||||||
private var fastAccountSwitcher: FastAccountSwitcherViewController!
|
|
||||||
private var fastSwitcherIndicator: FastAccountSwitcherIndicatorView!
|
|
||||||
private var fastSwitcherConstraints: [NSLayoutConstraint] = []
|
|
||||||
#endif
|
|
||||||
|
|
||||||
var currentTab: Tab {
|
var currentTab: Tab {
|
||||||
return Tab(rawValue: selectedIndex)!
|
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() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
@ -62,46 +45,13 @@ class MainTabBarViewController: UITabBarController, UITabBarControllerDelegate {
|
||||||
embedInNavigationController(Tab.myProfile.createViewController(mastodonController)),
|
embedInNavigationController(Tab.myProfile.createViewController(mastodonController)),
|
||||||
]
|
]
|
||||||
|
|
||||||
#if !os(visionOS)
|
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)
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
tabBar.isSpringLoaded = true
|
tabBar.isSpringLoaded = true
|
||||||
|
|
||||||
view.backgroundColor = .appBackground
|
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) {
|
func select(tab: Tab, dismissPresented: Bool) {
|
||||||
if tab == .compose {
|
if tab == .compose {
|
||||||
compose(editing: nil)
|
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() {
|
@objc func handleComposeKeyCommand() {
|
||||||
compose(editing: nil)
|
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) {
|
func setViewController(_ viewController: UIViewController, forTab tab: Tab) {
|
||||||
viewControllers![tab.rawValue] = viewController
|
viewControllers![tab.rawValue] = viewController
|
||||||
}
|
}
|
||||||
|
@ -229,7 +116,7 @@ extension MainTabBarViewController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func getTabController(tab: Tab) -> UIViewController? {
|
private func getTabController(tab: Tab) -> UIViewController? {
|
||||||
if tab == .compose {
|
if tab == .compose {
|
||||||
return nil
|
return nil
|
||||||
} else {
|
} else {
|
||||||
|
@ -240,53 +127,21 @@ extension MainTabBarViewController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#if !os(visionOS)
|
extension MainTabBarViewController: UITabBarControllerDelegate {
|
||||||
extension MainTabBarViewController: FastAccountSwitcherViewControllerDelegate {
|
func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
|
||||||
func fastAccountSwitcherAddToViewHierarchy(_ fastAccountSwitcher: FastAccountSwitcherViewController) {
|
if viewController == composePlaceholder {
|
||||||
fastAccountSwitcher.view.translatesAutoresizingMaskIntoConstraints = false
|
compose(editing: nil)
|
||||||
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
|
return false
|
||||||
}
|
}
|
||||||
let locationInButton = myProfileButton.convert(point, from: tabBar)
|
if selectedIndex != NSNotFound,
|
||||||
return myProfileButton.bounds.contains(locationInButton)
|
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
|
||||||
#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 activity == nil {
|
|
||||||
stateRestorationLogger.fault("MainTabBarViewController: Unable to create state restoration activity, couldn't find StateRestorableViewController")
|
|
||||||
}
|
|
||||||
return activity
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -350,24 +205,6 @@ extension MainTabBarViewController: TuskerRootViewController {
|
||||||
present(vc, animated: true, completion: completion)
|
present(vc, animated: true, completion: completion)
|
||||||
return vc
|
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 {
|
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
|
@MainActor
|
||||||
protocol TuskerRootViewController: UIViewController, StateRestorableViewController, StatusBarTappableViewController {
|
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 select(route: TuskerRoute, animated: Bool, completion: (() -> Void)?)
|
||||||
func getTabController(tab: MainTabBarViewController.Tab) -> UIViewController?
|
|
||||||
func getNavigationDelegate() -> TuskerNavigationDelegate?
|
func getNavigationDelegate() -> TuskerNavigationDelegate?
|
||||||
func getNavigationController() -> NavigationControllerProtocol
|
func getNavigationController() -> NavigationControllerProtocol
|
||||||
func performSearch(query: String)
|
func performSearch(query: String)
|
||||||
|
|
|
@ -48,7 +48,7 @@ enum AppShortcutItem: String, CaseIterable {
|
||||||
case .showNotifications:
|
case .showNotifications:
|
||||||
root.select(route: .notifications, animated: false, completion: nil)
|
root.select(route: .notifications, animated: false, completion: nil)
|
||||||
case .composePost:
|
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) {
|
func compose(editing draft: Draft) {
|
||||||
if #available(iOS 16.0, *),
|
if #available(iOS 16.0, *),
|
||||||
UIDevice.current.userInterfaceIdiom == .phone {
|
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 {
|
} else {
|
||||||
DispatchQueue.main.async {
|
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
|
state = .presented
|
||||||
|
@ -123,7 +123,7 @@ class StateRestorationUserActivityHandlingContext: UserActivityHandlingContext {
|
||||||
#if !os(visionOS)
|
#if !os(visionOS)
|
||||||
if #available(iOS 16.0, *),
|
if #available(iOS 16.0, *),
|
||||||
let duckedDraft = UserActivityManager.getDuckedDraft(from: activity) {
|
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
|
#endif
|
||||||
}
|
}
|
||||||
|
|
|
@ -96,7 +96,7 @@ extension TuskerNavigationDelegate {
|
||||||
show(ConversationViewController(for: statusID, state: state, mastodonController: apiController), sender: self)
|
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 draft = draft ?? apiController.createDraft()
|
||||||
let visionIdiom = UIUserInterfaceIdiom(rawValue: 6) // .vision is not available pre-iOS 17 :S
|
let visionIdiom = UIUserInterfaceIdiom(rawValue: 6) // .vision is not available pre-iOS 17 :S
|
||||||
if [visionIdiom, .pad, .mac].contains(UIDevice.current.userInterfaceIdiom) {
|
if [visionIdiom, .pad, .mac].contains(UIDevice.current.userInterfaceIdiom) {
|
||||||
|
@ -108,16 +108,17 @@ extension TuskerNavigationDelegate {
|
||||||
options.preferredPresentationStyle = .prominent
|
options.preferredPresentationStyle = .prominent
|
||||||
#endif
|
#endif
|
||||||
UIApplication.shared.requestSceneSessionActivation(nil, userActivity: compose, options: options, errorHandler: nil)
|
UIApplication.shared.requestSceneSessionActivation(nil, userActivity: compose, options: options, errorHandler: nil)
|
||||||
|
completion?()
|
||||||
} else {
|
} else {
|
||||||
let compose = ComposeHostingController(draft: draft, mastodonController: apiController)
|
let compose = ComposeHostingController(draft: draft, mastodonController: apiController)
|
||||||
#if os(visionOS)
|
#if os(visionOS)
|
||||||
fatalError("unreachable")
|
fatalError("unreachable")
|
||||||
#else
|
#else
|
||||||
if #available(iOS 16.0, *),
|
if #available(iOS 16.0, *),
|
||||||
presentDuckable(compose, animated: animated, isDucked: isDucked) {
|
presentDuckable(compose, animated: animated, isDucked: isDucked, completion: completion) {
|
||||||
return
|
return
|
||||||
} else {
|
} else {
|
||||||
present(compose, animated: animated)
|
present(compose, animated: animated, completion: completion)
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue