Size class switching fixes for new tab/side bar

This commit is contained in:
Shadowfacts 2024-08-20 22:39:40 -04:00
parent 3d9a1086b6
commit 67e9c1245e
4 changed files with 258 additions and 50 deletions

View File

@ -132,6 +132,7 @@
D64A50462C739DC0009D7193 /* NewMainTabBarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64A50452C739DC0009D7193 /* NewMainTabBarViewController.swift */; };
D64A50482C739DEA009D7193 /* BaseMainTabBarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64A50472C739DEA009D7193 /* BaseMainTabBarViewController.swift */; };
D64A50BC2C74F8F4009D7193 /* FindInstanceViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64A50BB2C74F8F4009D7193 /* FindInstanceViewController.swift */; };
D64A50BE2C752247009D7193 /* AdaptableNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64A50BD2C752247009D7193 /* AdaptableNavigationController.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 */; };
@ -563,6 +564,7 @@
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>"; };
D64A50BB2C74F8F4009D7193 /* FindInstanceViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FindInstanceViewController.swift; sourceTree = "<group>"; };
D64A50BD2C752247009D7193 /* AdaptableNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdaptableNavigationController.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>"; };
@ -1556,6 +1558,7 @@
D6B8DB332182A59300424AF7 /* UIAlertController+Visibility.swift */,
D6C693FD2162FEEA007D6A6D /* UIViewController+Children.swift */,
D61F759129365C6C00C0B37F /* CollectionViewController.swift */,
D64A50BD2C752247009D7193 /* AdaptableNavigationController.swift */,
);
path = Utilities;
sourceTree = "<group>";
@ -2341,6 +2344,7 @@
D6C693FE2162FEEA007D6A6D /* UIViewController+Children.swift in Sources */,
D61A45EA28DF51EE002BE511 /* TimelineLikeCollectionViewController.swift in Sources */,
D64D0AB12128D9AE005A6F37 /* OnboardingViewController.swift in Sources */,
D64A50BE2C752247009D7193 /* AdaptableNavigationController.swift in Sources */,
D6934F302BA7AF91002B1C8D /* ImageGalleryContentViewController.swift in Sources */,
D6934F3E2BAA19D5002B1C8D /* ImageActivityItemSource.swift in Sources */,
D67C1795266D57D10070F250 /* FastAccountSwitcherIndicatorView.swift in Sources */,

View File

@ -71,11 +71,12 @@ class BaseMainTabBarViewController: UITabBarController {
private func repositionFastSwitcherIndicator() {
guard let myProfileButton = findMyProfileTabBarButton(),
myProfileButton.window != nil else {
myProfileButton.window != nil,
let fastSwitcherIndicator else {
fastSwitcherIndicator?.isHidden = true
return
}
fastSwitcherIndicator?.isHidden = false
fastSwitcherIndicator.isHidden = false
NSLayoutConstraint.deactivate(fastSwitcherConstraints)
let isPortrait = view.bounds.width < view.bounds.height
if traitCollection.horizontalSizeClass == .compact && isPortrait {
@ -156,7 +157,7 @@ extension BaseMainTabBarViewController: StateRestorableViewController {
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 {
} else if let vc = (selectedViewController as? any NavigationControllerProtocol)?.topViewController as? StateRestorableViewController {
activity = vc.stateRestorationActivity()
}
if activity == nil {

View File

@ -15,11 +15,19 @@ class NewMainTabBarViewController: BaseMainTabBarViewController {
private let composePlaceholder = UIViewController()
private var homeTab: UITab!
private var notificationsTab: UITab!
private var composeTab: UITab!
private var exploreTab: UITab!
private var bookmarksTab: UITab!
private var favoritesTab: UITab!
private var myProfileTab: UITab!
private var listsGroup: UITabGroup!
private var cancellables = Set<AnyCancellable>()
private var navigationStacks = [String: [UIViewController]]()
private var isCompact: Bool?
override func viewDidLoad() {
super.viewDidLoad()
@ -34,19 +42,19 @@ class NewMainTabBarViewController: BaseMainTabBarViewController {
self.makeViewController(for: tab)
}
let homeTab = UITab(title: "Home", image: UIImage(systemName: "house"), identifier: Tab.home.rawValue, viewControllerProvider: viewControllerProvider)
let notificationsTab = UITab(title: "Notifications", image: UIImage(systemName: "bell"), identifier: Tab.notifications.rawValue, viewControllerProvider: viewControllerProvider)
let composeTab = UITab(title: "Compose", image: UIImage(systemName: "pencil"), identifier: Tab.compose.rawValue, viewControllerProvider: viewControllerProvider)
let exploreTab = UITab(title: "Explore", image: UIImage(systemName: "magnifyingglass"), identifier: Tab.explore.rawValue, viewControllerProvider: viewControllerProvider)
let bookmarksTab = UITab(title: "Bookmarks", image: UIImage(systemName: "bookmark"), identifier: Tab.bookmarks.rawValue, viewControllerProvider: viewControllerProvider)
homeTab = UITab(title: "Home", image: UIImage(systemName: "house"), identifier: Tab.home.rawValue, viewControllerProvider: viewControllerProvider)
notificationsTab = UITab(title: "Notifications", image: UIImage(systemName: "bell"), identifier: Tab.notifications.rawValue, viewControllerProvider: viewControllerProvider)
composeTab = UITab(title: "Compose", image: UIImage(systemName: "pencil"), identifier: Tab.compose.rawValue, viewControllerProvider: viewControllerProvider)
exploreTab = UITab(title: "Explore", image: UIImage(systemName: "magnifyingglass"), identifier: Tab.explore.rawValue, viewControllerProvider: viewControllerProvider)
bookmarksTab = UITab(title: "Bookmarks", image: UIImage(systemName: "bookmark"), identifier: Tab.bookmarks.rawValue, viewControllerProvider: viewControllerProvider)
bookmarksTab.preferredPlacement = .optional
let favoritesTab = UITab(title: "Favorites", image: UIImage(systemName: "star"), identifier: Tab.favorites.rawValue, viewControllerProvider: viewControllerProvider)
favoritesTab = UITab(title: "Favorites", image: UIImage(systemName: "star"), identifier: Tab.favorites.rawValue, viewControllerProvider: viewControllerProvider)
favoritesTab.preferredPlacement = .optional
let myProfileTab = UITab(title: "My Profile", image: UIImage(systemName: "person"), identifier: Tab.myProfile.rawValue, viewControllerProvider: viewControllerProvider)
myProfileTab = UITab(title: "My Profile", image: UIImage(systemName: "person"), identifier: Tab.myProfile.rawValue, viewControllerProvider: viewControllerProvider)
listsGroup = UITabGroup(title: "Lists", image: nil, identifier: Tab.lists.rawValue, children: []) { _ in
// this closure is necessary to prevent UIKit from crashing (FB14860961)
return MultiColumnNavigationController()
return AdaptableNavigationController()
}
listsGroup.preferredPlacement = .sidebarOnly
listsGroup.sidebarActions = [
@ -65,6 +73,72 @@ class NewMainTabBarViewController: BaseMainTabBarViewController {
myProfileTab,
]
} else {
self.updatePadTabs()
registerForTraitChanges([UITraitHorizontalSizeClass.self]) { (self: NewMainTabBarViewController, previousTraitCollection) in
self.updatePadTabs()
let vcToUpdate = self.selectedTab!.parent?.viewController ?? self.selectedTab!.viewController!
self.updateViewControllerSafeAreaInsets(vcToUpdate)
}
}
setupFastAccountSwitcher()
mastodonController.$lists
.sink { [unowned self] in self.reloadLists($0) }
.store(in: &cancellables)
}
private func updatePadTabs() {
let wasCompact = isCompact
if self.traitCollection.horizontalSizeClass == .compact {
isCompact = true
var exploreNavStack: [UIViewController]? = nil
if selectedTab?.parent == listsGroup {
let nav = listsGroup.viewController as! any NavigationControllerProtocol
exploreNavStack = nav.viewControllers
nav.viewControllers = []
}
self.tabs = [
homeTab,
notificationsTab,
composeTab,
exploreTab,
myProfileTab,
]
if let exploreNavStack {
selectedTab = exploreTab
let nav = exploreTab.viewController as! any NavigationControllerProtocol
nav.viewControllers = exploreNavStack
}
} else {
isCompact = false
var newTab: (UITab, [UIViewController])? = nil
if wasCompact == true,
selectedTab == exploreTab {
let nav = exploreTab.viewController as! any NavigationControllerProtocol
// skip over the ExploreViewController
if nav.viewControllers.count > 1 {
switch nav.viewControllers[1] {
case let listVC as ListTimelineViewController:
if let tab = listsGroup.tab(forIdentifier: "list:\(listVC.list.id)") {
newTab = (tab, Array(nav.viewControllers[1...]))
nav.viewControllers = [
nav.viewControllers[0], // leave the ExploreVC in place
InlineTrendsViewController(mastodonController: mastodonController), // re-insert an InlineTrendsVC
]
}
default:
break
}
}
}
self.tabs = [
homeTab,
notificationsTab,
@ -75,13 +149,17 @@ class NewMainTabBarViewController: BaseMainTabBarViewController {
composeTab,
listsGroup,
]
if let (tab, navStack) = newTab {
let nav = tab.parent!.viewController as! any NavigationControllerProtocol
nav.viewControllers = navStack
// Setting the tab now seems to be clobbered by the UITabBarController itself updating in response
// to the size class change. So wait until it finishes to do so.
DispatchQueue.main.async {
self.selectedTab = tab
}
}
}
setupFastAccountSwitcher()
mastodonController.$lists
.sink { [unowned self] in self.reloadLists($0) }
.store(in: &cancellables)
}
private func makeViewController(for tab: UITab) -> UIViewController {
@ -100,7 +178,11 @@ class NewMainTabBarViewController: BaseMainTabBarViewController {
if UIDevice.current.userInterfaceIdiom == .phone {
root = ExploreViewController(mastodonController: mastodonController)
} else {
root = InlineTrendsViewController(mastodonController: mastodonController)
let nav = AdaptableNavigationController(viewControllersToPrependInCompact: [
ExploreViewController(mastodonController: mastodonController)
])
nav.viewControllers = [InlineTrendsViewController(mastodonController: mastodonController)]
return nav
}
case .bookmarks:
root = BookmarksViewController(mastodonController: mastodonController)
@ -111,24 +193,11 @@ class NewMainTabBarViewController: BaseMainTabBarViewController {
case .lists:
fatalError("unreachable")
}
return NewMainTabBarViewController.embedInNavigationController(root)
return embedInNavigationController(root)
}
private static func embedInNavigationController(_ vc: UIViewController) -> UIViewController {
let nav: any NavigationControllerProtocol
if UIDevice.current.userInterfaceIdiom == .phone {
nav = EnhancedNavigationViewController()
} else {
// TODO: need to figure out how to update the navigation controller if the pref changes
switch Preferences.shared.widescreenNavigationMode {
case .stack:
nav = EnhancedNavigationViewController()
case .splitScreen:
nav = SplitNavigationController()
case .multiColumn:
nav = MultiColumnNavigationController()
}
}
private func embedInNavigationController(_ vc: UIViewController) -> UIViewController {
let nav = AdaptableNavigationController()
nav.viewControllers = [vc]
return nav
}
@ -136,7 +205,7 @@ class NewMainTabBarViewController: BaseMainTabBarViewController {
private func reloadLists(_ lists: [List]) {
listsGroup.children = lists.map { list in
UITab(title: list.title, image: UIImage(systemName: "list.bullet"), identifier: "list:\(list.id)") { [unowned self] _ in
NewMainTabBarViewController.embedInNavigationController(ListTimelineViewController(for: list, mastodonController: self.mastodonController))
return ListTimelineViewController(for: list, mastodonController: self.mastodonController)
}
}
}
@ -145,7 +214,10 @@ class NewMainTabBarViewController: BaseMainTabBarViewController {
compose(editing: nil)
}
fileprivate func updateViewControllerSafeAreaInsets(_ vc: MultiColumnNavigationController) {
fileprivate func updateViewControllerSafeAreaInsets(_ vc: UIViewController) {
guard vc is MultiColumnNavigationController || (vc as? AdaptableNavigationController)?.current is MultiColumnNavigationController else {
return
}
// When in sidebar mode, for multi column mode, don't leave an inset for the floating tab bar, because it leaves a massive gap.
// The floating tab bar seems to always be 88pt tall, regardless of, e.g., Dynamic Type size.
vc.additionalSafeAreaInsets = UIEdgeInsets(top: sidebar.isHidden ? 0 : -88, left: 0, bottom: 0, right: 0)
@ -192,10 +264,8 @@ extension NewMainTabBarViewController: UITabBarControllerDelegate {
}
func tabBarController(_ tabBarController: UITabBarController, didSelectTab newTab: UITab, previousTab: UITab?) {
if let vc = newTab.viewController as? MultiColumnNavigationController {
self.updateViewControllerSafeAreaInsets(vc)
}
self.updateViewControllerSafeAreaInsets(newTab.viewController!)
// All tabs in a tab group deliberately share the same view controller, so we have to do this ourselves.
// I think this is pretty unfortunate API design--half the time, the tab bar controller takes care of
// this, but the rest of the time it's up to you.
@ -207,9 +277,7 @@ extension NewMainTabBarViewController: UITabBarControllerDelegate {
if let group = newTab.parent,
group.identifier == Tab.lists.rawValue,
let nav = group.viewController as? any NavigationControllerProtocol {
if let multiColumn = nav as? MultiColumnNavigationController {
updateViewControllerSafeAreaInsets(multiColumn)
}
updateViewControllerSafeAreaInsets(nav)
if let previousTab {
navigationStacks[previousTab.identifier] = nav.viewControllers
@ -217,8 +285,8 @@ extension NewMainTabBarViewController: UITabBarControllerDelegate {
if let existing = navigationStacks[newTab.identifier] {
nav.viewControllers = existing
} else if let newNav = newTab.viewController as? any NavigationControllerProtocol {
nav.viewControllers = newNav.viewControllers
} else if let newVC = newTab.viewController {
nav.viewControllers = [newVC]
} else {
fatalError("unreachable")
}
@ -229,11 +297,10 @@ extension NewMainTabBarViewController: UITabBarControllerDelegate {
@available(iOS 18.0, *)
extension NewMainTabBarViewController: UITabBarController.Sidebar.Delegate {
func tabBarController(_ tabBarController: UITabBarController, sidebarVisibilityWillChange sidebar: UITabBarController.Sidebar, animator: any UITabBarController.Sidebar.Animating) {
if let vc = selectedViewController as? MultiColumnNavigationController {
animator.addAnimations {
self.updateViewControllerSafeAreaInsets(vc)
vc.view.layoutIfNeeded()
}
let vc = selectedTab!.parent?.viewController ?? selectedTab!.viewController!
animator.addAnimations {
self.updateViewControllerSafeAreaInsets(vc)
vc.view.layoutIfNeeded()
}
}
}

View File

@ -0,0 +1,136 @@
//
// AdaptableNavigationController.swift
// Tusker
//
// Created by Shadowfacts on 8/20/24.
// Copyright © 2024 Shadowfacts. All rights reserved.
//
import UIKit
import Combine
@available(iOS 17.0, *)
class AdaptableNavigationController: UIViewController {
private let viewControllersToPrependInCompact: [UIViewController]
private var initialViewControllers: [UIViewController] = []
private lazy var regular = makeRegularNavigationController()
private lazy var compact = makeCompactNavigationController()
private var _current: (any NavigationControllerProtocol)?
var current: any NavigationControllerProtocol {
traitCollection.horizontalSizeClass == .regular ? regular : compact
}
init(viewControllersToPrependInCompact: [UIViewController] = []) {
self.viewControllersToPrependInCompact = viewControllersToPrependInCompact
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
updateNavigationController()
registerForTraitChanges([UITraitHorizontalSizeClass.self]) { (self: AdaptableNavigationController, previousTraitCollection) in
self.updateNavigationController()
}
}
private func updateNavigationController() {
let isTransferring: Bool
var stack: [UIViewController]
if let _current {
_current.removeViewAndController()
stack = _current.viewControllers
isTransferring = true
} else {
stack = initialViewControllers
initialViewControllers = []
isTransferring = false
}
if traitCollection.horizontalSizeClass == .regular {
if isTransferring {
stack.removeFirst(viewControllersToPrependInCompact.count)
}
} else {
stack.insert(contentsOf: viewControllersToPrependInCompact, at: 0)
}
_current = current
current.viewControllers = stack
addChild(current)
current.view.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(current.view)
NSLayoutConstraint.activate([
current.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
current.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
current.view.topAnchor.constraint(equalTo: view.topAnchor),
current.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
])
current.didMove(toParent: self)
}
private func makeRegularNavigationController() -> any NavigationControllerProtocol {
// TODO: need to figure out how to update the navigation controller if the pref changes
switch Preferences.shared.widescreenNavigationMode {
case .stack:
return EnhancedNavigationViewController()
case .splitScreen:
return SplitNavigationController()
case .multiColumn:
return MultiColumnNavigationController()
}
}
private func makeCompactNavigationController() -> any NavigationControllerProtocol {
EnhancedNavigationViewController()
}
}
@available(iOS 17.0, *)
extension AdaptableNavigationController: NavigationControllerProtocol {
var viewControllers: [UIViewController] {
get {
_current?.viewControllers ?? initialViewControllers
}
set {
if let _current {
_current.viewControllers = newValue
} else {
initialViewControllers = newValue
}
}
}
var topViewController: UIViewController? {
if let _current {
return _current.topViewController
} else {
return initialViewControllers.last
}
}
func popToRootViewController(animated: Bool) -> [UIViewController]? {
if let _current {
return _current.popToRootViewController(animated: animated)
} else {
defer { initialViewControllers = [] }
return initialViewControllers
}
}
func pushViewController(_ vc: UIViewController, animated: Bool) {
if let _current {
_current.pushViewController(vc, animated: animated)
} else {
initialViewControllers.append(vc)
}
}
}