diff --git a/Tusker/Screens/Main/NewMainTabBarViewController.swift b/Tusker/Screens/Main/NewMainTabBarViewController.swift index d3311cd607..1605873745 100644 --- a/Tusker/Screens/Main/NewMainTabBarViewController.swift +++ b/Tusker/Screens/Main/NewMainTabBarViewController.swift @@ -7,11 +7,19 @@ // import UIKit +import Combine +import Pachyderm @available(iOS 18.0, *) class NewMainTabBarViewController: BaseMainTabBarViewController { private let composePlaceholder = UIViewController() + + private var listsGroup: UITabGroup! + + private var cancellables = Set() + + private var navigationStacks = [String: [UIViewController]]() override func viewDidLoad() { super.viewDidLoad() @@ -26,19 +34,54 @@ class NewMainTabBarViewController: BaseMainTabBarViewController { 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) + 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) + bookmarksTab.preferredPlacement = .optional + let 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) + + listsGroup = UITabGroup(title: "Lists", image: nil, identifier: Tab.lists.rawValue, children: []) { _ in + // this closure is necessary to prevent UIKit from crashing (FB14860961) + return MultiColumnNavigationController() + } + listsGroup.preferredPlacement = .sidebarOnly + listsGroup.sidebarActions = [ + UIAction(title: "New List…", image: UIImage(systemName: "plus"), handler: { _ in + fatalError("TODO") + }) + ] + reloadLists(mastodonController.lists) + + if UIDevice.current.userInterfaceIdiom == .phone { + self.tabs = [ + homeTab, + notificationsTab, + composeTab, + exploreTab, + myProfileTab, + ] + } else { + self.tabs = [ + homeTab, + notificationsTab, + exploreTab, + bookmarksTab, + favoritesTab, + myProfileTab, + composeTab, + listsGroup, + ] } - self.tabs = topLevelTabs - setupFastAccountSwitcher() + + mastodonController.$lists + .sink { [unowned self] in self.reloadLists($0) } + .store(in: &cancellables) } private func makeViewController(for tab: UITab) -> UIViewController { @@ -55,9 +98,19 @@ class NewMainTabBarViewController: BaseMainTabBarViewController { return composePlaceholder case .explore: root = ExploreViewController(mastodonController: mastodonController) + case .bookmarks: + root = BookmarksViewController(mastodonController: mastodonController) + case .favorites: + root = FavoritesViewController(mastodonController: mastodonController) case .myProfile: root = MyProfileViewController(mastodonController: mastodonController) + case .lists: + fatalError("unreachable") } + return NewMainTabBarViewController.embedInNavigationController(root) + } + + private static func embedInNavigationController(_ vc: UIViewController) -> UIViewController { let nav: any NavigationControllerProtocol if UIDevice.current.userInterfaceIdiom == .phone { nav = EnhancedNavigationViewController() @@ -72,10 +125,18 @@ class NewMainTabBarViewController: BaseMainTabBarViewController { nav = MultiColumnNavigationController() } } - nav.viewControllers = [root] + nav.viewControllers = [vc] return nav } + 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)) + } + } + } + @objc func handleComposeKeyCommand() { compose(editing: nil) } @@ -94,37 +155,11 @@ extension NewMainTabBarViewController { case notifications case compose case explore + case bookmarks + case favorites 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" - } - } + case lists } } @@ -156,6 +191,34 @@ extension NewMainTabBarViewController: UITabBarControllerDelegate { if let vc = newTab.viewController as? MultiColumnNavigationController { self.updateViewControllerSafeAreaInsets(vc) } + + // 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. + // The managingNavigationController API would theoretically solve this, but split-screen/multi-column + // nav can't straightforwardly be implemented as UINavigationController subclasses. + // Unfortunately this, in turn, means that when switching between tabs in the same group, we don't + // get the new transition animation. + // This would be much less complicated if the controller just used the individual VCs of items in a group. + 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) + } + + if let previousTab { + navigationStacks[previousTab.identifier] = nav.viewControllers + } + + if let existing = navigationStacks[newTab.identifier] { + nav.viewControllers = existing + } else if let newNav = newTab.viewController as? any NavigationControllerProtocol { + nav.viewControllers = newNav.viewControllers + } else { + fatalError("unreachable") + } + } } }