forked from shadowfacts/Tusker
714 lines
32 KiB
Swift
714 lines
32 KiB
Swift
//
|
|
// MainSplitViewController.swift
|
|
// Tusker
|
|
//
|
|
// Created by Shadowfacts on 6/23/20.
|
|
// Copyright © 2020 Shadowfacts. All rights reserved.
|
|
//
|
|
|
|
import UIKit
|
|
import Combine
|
|
import TuskerPreferences
|
|
|
|
class MainSplitViewController: UISplitViewController {
|
|
|
|
private let mastodonController: MastodonController
|
|
|
|
private var sidebar: MainSidebarViewController!
|
|
private var fastAccountSwitcher: FastAccountSwitcherViewController?
|
|
|
|
// Keep track of navigation stacks per-item so that we can only ever use a single navigation controller
|
|
private var navigationStacks: [MainSidebarViewController.Item: [UIViewController]] = [:]
|
|
|
|
private var tabBarViewController: MainTabBarViewController!
|
|
|
|
private var navigationMode: WidescreenNavigationMode!
|
|
private var secondaryNavController: NavigationControllerProtocol! {
|
|
viewController(for: .secondary) as? NavigationControllerProtocol
|
|
}
|
|
|
|
private var cancellables = Set<AnyCancellable>()
|
|
|
|
private var sidebarVisibile: Bool {
|
|
get {
|
|
(UserDefaults.standard.object(forKey: "MainSplitViewControllerSidebarVisible") as? Bool) ?? true
|
|
}
|
|
set {
|
|
UserDefaults.standard.set(newValue, forKey: "MainSplitViewControllerSidebarVisible")
|
|
}
|
|
}
|
|
|
|
init(mastodonController: MastodonController) {
|
|
self.mastodonController = mastodonController
|
|
|
|
super.init(style: .doubleColumn)
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
override func viewDidLoad() {
|
|
super.viewDidLoad()
|
|
|
|
preferredDisplayMode = .oneBesideSecondary
|
|
preferredSplitBehavior = .tile
|
|
delegate = self
|
|
|
|
sidebar = MainSidebarViewController(mastodonController: mastodonController)
|
|
sidebar.sidebarDelegate = self
|
|
setViewController(sidebar, for: .primary)
|
|
primaryBackgroundStyle = .sidebar
|
|
if sidebarVisibile {
|
|
show(.primary)
|
|
} else {
|
|
hide(.primary)
|
|
}
|
|
|
|
let nav: UIViewController
|
|
let visionIdiom = UIUserInterfaceIdiom(rawValue: 6)
|
|
if [visionIdiom, .pad, .mac].contains(UIDevice.current.userInterfaceIdiom) {
|
|
navigationMode = Preferences.shared.widescreenNavigationMode
|
|
switch navigationMode! {
|
|
case .stack:
|
|
nav = EnhancedNavigationViewController()
|
|
case .splitScreen:
|
|
nav = SplitNavigationController()
|
|
case .multiColumn:
|
|
nav = MultiColumnNavigationController()
|
|
}
|
|
} else {
|
|
navigationMode = .stack
|
|
nav = EnhancedNavigationViewController()
|
|
}
|
|
setViewController(nav, for: .secondary)
|
|
|
|
// don't unnecesarily construct a content VC unless the we're in actually split mode
|
|
// when we change from compact -> split for the first time, the VC will be transferred anyways
|
|
if traitCollection.horizontalSizeClass != .compact {
|
|
doSelect(item: .tab(.timelines))
|
|
}
|
|
|
|
if UIDevice.current.userInterfaceIdiom != .mac {
|
|
let switcher = FastAccountSwitcherViewController()
|
|
fastAccountSwitcher = switcher
|
|
switcher.itemOrientation = .iconsLeading
|
|
switcher.view.translatesAutoresizingMaskIntoConstraints = false
|
|
switcher.delegate = self
|
|
// accessing .view unconditionally loads the view, which we don't want to happen
|
|
// because the sidebar view not being loaded is how we know not to transfer nav state
|
|
// in splitViewControllerDidCollapse on devices where the sidebar is never shown
|
|
sidebar.onViewDidLoad = { [unowned self] in
|
|
self.sidebar.view.addGestureRecognizer(switcher.createSwitcherGesture())
|
|
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(sidebarTapped))
|
|
tapRecognizer.cancelsTouchesInView = false
|
|
self.sidebar.view.addGestureRecognizer(tapRecognizer)
|
|
}
|
|
}
|
|
|
|
tabBarViewController = MainTabBarViewController(mastodonController: mastodonController)
|
|
setViewController(tabBarViewController, for: .compact)
|
|
|
|
addKeyCommand(MenuController.composeCommand)
|
|
|
|
MenuController.sidebarItemKeyCommands.forEach(addKeyCommand(_:))
|
|
|
|
Preferences.shared.$widescreenNavigationMode
|
|
.sink { [unowned self] in
|
|
self.updateNavigationMode($0)
|
|
}
|
|
.store(in: &cancellables)
|
|
}
|
|
|
|
private func updateNavigationMode(_ mode: WidescreenNavigationMode) {
|
|
let visionIdiom = UIUserInterfaceIdiom(rawValue: 6)
|
|
guard [visionIdiom, .pad, .mac].contains(UIDevice.current.userInterfaceIdiom),
|
|
mode != navigationMode else {
|
|
return
|
|
}
|
|
navigationMode = mode
|
|
|
|
let viewControllers = secondaryNavController.viewControllers
|
|
secondaryNavController.viewControllers = []
|
|
// Setting viewControllers = [] doesn't remove the VC views from their superviews immediately,
|
|
// so do that ourselves so we can re-parent the VCs to the new nav controller.
|
|
for viewController in viewControllers {
|
|
viewController.viewIfLoaded?.removeFromSuperview()
|
|
}
|
|
|
|
let newNav: NavigationControllerProtocol
|
|
switch mode {
|
|
case .stack:
|
|
newNav = EnhancedNavigationViewController()
|
|
case .splitScreen:
|
|
newNav = SplitNavigationController()
|
|
case .multiColumn:
|
|
newNav = MultiColumnNavigationController()
|
|
}
|
|
newNav.viewControllers = viewControllers
|
|
self.setViewController(newNav, for: .secondary)
|
|
}
|
|
|
|
private func select(newItem: MainSidebarViewController.Item, oldItem: MainSidebarViewController.Item?) {
|
|
if let oldItem,
|
|
newItem != oldItem {
|
|
navigationStacks[oldItem] = secondaryNavController.viewControllers
|
|
}
|
|
doSelect(item: newItem)
|
|
}
|
|
|
|
private func doSelect(item: MainSidebarViewController.Item) {
|
|
secondaryNavController.viewControllers = getOrCreateNavigationStack(item: item)
|
|
}
|
|
|
|
func navigationStackFor(item: MainSidebarViewController.Item) -> [UIViewController]? {
|
|
if sidebar.selectedItem == item {
|
|
return secondaryNavController.viewControllers
|
|
} else {
|
|
return navigationStacks[item]
|
|
}
|
|
}
|
|
|
|
func getOrCreateNavigationStack(item: MainSidebarViewController.Item) -> [UIViewController] {
|
|
if let existing = navigationStacks[item], existing.count > 0 {
|
|
return existing
|
|
} else {
|
|
let new = [item.createRootViewController(mastodonController)!]
|
|
navigationStacks[item] = new
|
|
return new
|
|
}
|
|
}
|
|
|
|
override func show(_ vc: UIViewController, sender: Any?) {
|
|
if traitCollection.horizontalSizeClass == .regular {
|
|
secondaryNavController.show(vc, sender: sender)
|
|
} else {
|
|
super.show(vc, sender: sender)
|
|
}
|
|
}
|
|
|
|
@objc func handleSidebarCommandTimelines() {
|
|
select(newItem: .tab(.timelines), oldItem: sidebar.selectedItem)
|
|
sidebar.select(item: .tab(.timelines), animated: false)
|
|
}
|
|
|
|
@objc func handleSidebarCommandNotifications() {
|
|
select(newItem: .tab(.notifications), oldItem: sidebar.selectedItem)
|
|
sidebar.select(item: .tab(.notifications), animated: false)
|
|
}
|
|
|
|
@objc func handleSidebarCommandExplore() {
|
|
select(newItem: .tab(.explore), oldItem: sidebar.selectedItem)
|
|
sidebar.select(item: .tab(.explore), animated: false)
|
|
}
|
|
|
|
@objc func handleSidebarCommandBookmarks() {
|
|
select(newItem: .bookmarks, oldItem: sidebar.selectedItem)
|
|
sidebar.select(item: .bookmarks, animated: false)
|
|
}
|
|
|
|
@objc func handleSidebarCommandMyProfile() {
|
|
select(newItem: .tab(.myProfile), oldItem: sidebar.selectedItem)
|
|
sidebar.select(item: .tab(.myProfile), animated: false)
|
|
}
|
|
|
|
@objc private func sidebarTapped() {
|
|
fastAccountSwitcher?.hide()
|
|
}
|
|
|
|
@objc func handleComposeKeyCommand() {
|
|
compose(editing: nil)
|
|
}
|
|
|
|
}
|
|
|
|
extension MainSplitViewController: UISplitViewControllerDelegate {
|
|
/// Transfer the navigation stack for a sidebar item to a destination navgiation controller.
|
|
/// - Parameter dropFirst: Remove the first view controller from the item's navigation stack before transferring.
|
|
/// - Parameter append: Append the item's navigation stack to the destination nav controller's instead of replacing it.
|
|
private func transferNavigationStack(from item: MainSidebarViewController.Item, to destination: UINavigationController, dropFirst: Bool = false, append: Bool = false) {
|
|
var itemNavStack: [UIViewController]
|
|
if item == sidebar.selectedItem {
|
|
itemNavStack = secondaryNavController.viewControllers
|
|
secondaryNavController.viewControllers = []
|
|
// Sometimes removing a VC from the viewControllers array doesn't immediately remove it's view from the hierarchy
|
|
for vc in itemNavStack {
|
|
vc.viewIfLoaded?.removeFromSuperview()
|
|
}
|
|
} else {
|
|
itemNavStack = navigationStacks[item] ?? []
|
|
navigationStacks.removeValue(forKey: item)
|
|
}
|
|
if itemNavStack.isEmpty {
|
|
itemNavStack = [item.createRootViewController(mastodonController)!]
|
|
}
|
|
|
|
if dropFirst {
|
|
itemNavStack.remove(at: 0)
|
|
}
|
|
|
|
if append {
|
|
destination.viewControllers += itemNavStack
|
|
} else {
|
|
destination.viewControllers = itemNavStack
|
|
}
|
|
}
|
|
|
|
func splitViewControllerDidCollapse(_ svc: UISplitViewController) {
|
|
// on iPhones, the sidebar VC is never loaded, but since this method is still called, we can't do anything
|
|
guard sidebar.isViewLoaded else { return }
|
|
|
|
// Transfer the nav stacks for all the sidebar items that map 1 <-> 1 with tabs
|
|
for tab in [MainTabBarViewController.Tab.timelines, .notifications, .myProfile] {
|
|
let tabNav = tabBarViewController.viewController(for: tab) as! UINavigationController
|
|
if tabNav.isViewLoaded {
|
|
transferNavigationStack(from: .tab(tab), to: tabNav)
|
|
}
|
|
}
|
|
|
|
// Since several sidebar items map to the single Explore tab, we only transfer the
|
|
// navigation stack of the most-recently used one.
|
|
let mostRecentExploreItem: (MainSidebarViewController.Item, Date)? =
|
|
sidebar.exploreTabItems.compactMap {
|
|
if let timestamp = sidebar.itemLastSelectedTimestamps[$0] {
|
|
return ($0, timestamp)
|
|
} else {
|
|
return nil
|
|
}
|
|
}.min {
|
|
$0.1 > $1.1
|
|
}
|
|
if let mostRecentExploreItem = mostRecentExploreItem?.0,
|
|
mostRecentExploreItem != .tab(.explore) {
|
|
let exploreNav = tabBarViewController.viewController(for: .explore) as! UINavigationController
|
|
// Pop back to root, so we're appending to the Explore VC instead of some other VC
|
|
exploreNav.popToRootViewController(animated: false)
|
|
// Append so we don't replace the Explore VC
|
|
transferNavigationStack(from: mostRecentExploreItem, to: exploreNav, append: true)
|
|
}
|
|
|
|
// Switch the tab bar to focus the same item as the sidebar has selected
|
|
switch sidebar.selectedItem {
|
|
case nil:
|
|
break
|
|
|
|
case .tab(.explore):
|
|
// Search sidebar item maps to the Explore tab with the search controller/results visible
|
|
// The nav stack can't be copied directly, since the split VC uses a different SearchViewController
|
|
// so that explore items aren't shown multiple times.
|
|
|
|
let exploreNav = tabBarViewController.viewController(for: .explore) as! UINavigationController
|
|
// make sure there's a root ExploreViewController
|
|
let explore: ExploreViewController
|
|
if let existing = exploreNav.viewControllers.first as? ExploreViewController {
|
|
explore = existing
|
|
exploreNav.popToRootViewController(animated: false)
|
|
} else {
|
|
// If the Explore tab hasn't been loaded before, it's root view controller won't be loaded yet, so create and add it manually.
|
|
explore = ExploreViewController(mastodonController: mastodonController)
|
|
exploreNav.viewControllers = [explore]
|
|
}
|
|
// Make sure viewDidLoad is called so that the searchController/resultsController have been initialized
|
|
explore.loadViewIfNeeded()
|
|
|
|
let search = secondaryNavController.viewControllers.first as! InlineTrendsViewController
|
|
if search.searchController?.isActive == true {
|
|
// Copy the search query from the search VC to the Explore VC's search controller.
|
|
let query = search.searchController.searchBar.text ?? ""
|
|
explore.searchController.searchBar.text = query
|
|
// Instruct the explore controller to show its search controller immediately upon its first appearance.
|
|
// explore.searchController.isActive can't be set directly, see FB7814561
|
|
explore.searchControllerStatusOnAppearance = !query.isEmpty
|
|
// Copy the results from the search VC's results controller to avoid the delay introduced by an extra network request
|
|
explore.resultsController.loadResults(from: search.resultsController)
|
|
} else {
|
|
// if there is more than just the InlineTrendsVC, and the search VC is not active,
|
|
// then the user selected something from the trends screen
|
|
if secondaryNavController.viewControllers.count >= 2 {
|
|
// make sure there's a corresponding trends VC in the collapsed nav that they can go back to
|
|
exploreNav.pushViewController(TrendsViewController(mastodonController: mastodonController), animated: false)
|
|
}
|
|
}
|
|
|
|
// Transfer the navigation stack, dropping the search VC, to keep anything the user has opened
|
|
transferNavigationStack(from: .tab(.explore), to: exploreNav, dropFirst: true, append: true)
|
|
|
|
tabBarViewController.select(tab: .explore, dismissPresented: false)
|
|
|
|
case let .tab(tab):
|
|
// sidebar items that map 1 <-> 1 can be transferred directly
|
|
tabBarViewController.select(tab: tab, dismissPresented: false)
|
|
|
|
case .bookmarks, .favorites, .list(_), .savedHashtag(_), .savedInstance(_):
|
|
tabBarViewController.select(tab: .explore, dismissPresented: false)
|
|
// Make sure the Explore VC doesn't show its search bar when it appears, in case the user was previously
|
|
// in compact mode and performing a search.
|
|
let exploreNav = tabBarViewController.viewController(for: .explore) as! UINavigationController
|
|
let explore = exploreNav.viewControllers.first as! ExploreViewController
|
|
explore.searchControllerStatusOnAppearance = false
|
|
|
|
case .listsHeader, .addList, .savedHashtagsHeader, .addSavedHashtag, .savedInstancesHeader, .addSavedInstance:
|
|
// These items are not selectable in the sidebar collection view, so this code is unreachable.
|
|
fatalError("unexpected selected sidebar item: \(sidebar.selectedItem!)")
|
|
}
|
|
}
|
|
|
|
/// Transfer a navigation stack from a navigation controller belonging to the tab bar VC to a sidebar item.
|
|
/// - Parameter skipFirst:The number of view controllers that should be skipped from the source navigation controller.
|
|
/// - Parameter prepend: An optional view controller to prepend to the beginning of the navigation stack being moved.
|
|
private func transferNavigationStack(from navController: UINavigationController, to item: MainSidebarViewController.Item, skipFirst: Int = 0, prepend: UIViewController? = nil) {
|
|
let viewControllersToMove = navController.viewControllers.dropFirst(skipFirst)
|
|
navController.viewControllers.removeLast(navController.viewControllers.count - skipFirst)
|
|
|
|
// Sometimes removing a VC from the viewControllers array doesn't immediately remove it's view from the hierarchy
|
|
for vc in viewControllersToMove {
|
|
vc.viewIfLoaded?.removeFromSuperview()
|
|
}
|
|
|
|
if let prepend = prepend {
|
|
navigationStacks[item] = [prepend] + viewControllersToMove
|
|
} else {
|
|
navigationStacks[item] = Array(viewControllersToMove)
|
|
}
|
|
}
|
|
|
|
func splitViewControllerDidExpand(_ svc: UISplitViewController) {
|
|
// For each sidebar item, transfer the existing navigation stasck from the tab bar controller to ourself.
|
|
var exploreItem: MainSidebarViewController.Item?
|
|
for tab in MainTabBarViewController.Tab.allCases {
|
|
guard let tabNavController = tabBarViewController.viewController(for: tab) as? UINavigationController,
|
|
tabNavController.isViewLoaded else { continue }
|
|
let tabNavigationStack = tabNavController.viewControllers
|
|
|
|
switch tab {
|
|
case .timelines, .notifications, .myProfile:
|
|
// Items that map 1 <-> 1 to tabs can be transferred directly.
|
|
let item = MainSidebarViewController.Item.tab(tab)
|
|
transferNavigationStack(from: tabNavController, to: item)
|
|
|
|
case .explore:
|
|
// The Explore tab is more complicated since it encapsulates a bunch of screens which have top-level sidebar items.
|
|
|
|
var skipFirst = 1
|
|
var toPrepend: UIViewController? = nil
|
|
|
|
// If the tab navigation stack has only one item or the search controller is active, it corresponds to the Search item
|
|
// For other items, the 2nd VC in the nav stack determines which sidebar item they map to.
|
|
// Search screen has special considerations, all others can be transferred directly.
|
|
if tabNavigationStack.count == 1 || ((tabNavigationStack.first as? ExploreViewController)?.searchController?.isActive ?? false) {
|
|
exploreItem = .tab(.explore)
|
|
// reuse the existing VC, if there is one
|
|
let searchVC = getOrCreateNavigationStack(item: .tab(.explore)).first! as! InlineTrendsViewController
|
|
// load the view so that the search controller is accessible
|
|
searchVC.loadViewIfNeeded()
|
|
let explore = tabNavigationStack.first as! ExploreViewController
|
|
if let exploreSearchControler = explore.searchController,
|
|
let query = exploreSearchControler.searchBar.text {
|
|
// Transfer query to search VC
|
|
searchVC.searchController.searchBar.text = query
|
|
// If there is a query, make the search VC activate itself upon appearing
|
|
searchVC.searchControllerStatusOnAppearance = !query.isEmpty
|
|
// Transfer the results from the explore VC, to avoid an extra network request
|
|
searchVC.resultsController.loadResults(from: explore.resultsController)
|
|
}
|
|
// Insert the new search VC at the beginning of the new search nav stack
|
|
toPrepend = searchVC
|
|
} else {
|
|
switch tabNavigationStack[1] {
|
|
case is BookmarksViewController:
|
|
exploreItem = .bookmarks
|
|
case is FavoritesViewController:
|
|
exploreItem = .favorites
|
|
case let listVC as ListTimelineViewController:
|
|
exploreItem = .list(listVC.list)
|
|
case let hashtagVC as HashtagTimelineViewController where sidebar.hasItem(.savedHashtag(hashtagVC.hashtagName)):
|
|
exploreItem = .savedHashtag(hashtagVC.hashtagName)
|
|
case let instanceVC as InstanceTimelineViewController:
|
|
exploreItem = .savedInstance(instanceVC.instanceURL)
|
|
case is TrendsViewController:
|
|
exploreItem = .tab(.explore)
|
|
// skip transferring the ExploreViewController and TrendsViewController
|
|
skipFirst = 2
|
|
// prepend the InlineTrendsViewController
|
|
toPrepend = getOrCreateNavigationStack(item: .tab(.explore)).first!
|
|
default:
|
|
// transfer the navigation stack prepending, the existing explore VC
|
|
// if there was other stuff on the explore stack, it will get discarded
|
|
toPrepend = getOrCreateNavigationStack(item: .tab(.explore)).first!
|
|
exploreItem = .tab(.explore)
|
|
}
|
|
}
|
|
transferNavigationStack(from: tabNavController, to: exploreItem!, skipFirst: skipFirst, prepend: toPrepend)
|
|
|
|
case .compose:
|
|
// The compose tab can't be activated, this is unreachable.
|
|
fatalError("unreachable")
|
|
}
|
|
}
|
|
|
|
// Transfer the selected tab from the tab bar VC to the sidebar
|
|
switch tabBarViewController.currentTab {
|
|
case .timelines, .notifications, .myProfile:
|
|
// These tabs map 1 <-> 1 with sidebar items
|
|
let item = MainSidebarViewController.Item.tab(tabBarViewController.currentTab)
|
|
sidebar.select(item: item, animated: false)
|
|
doSelect(item: item)
|
|
|
|
case .explore:
|
|
// If the explore tab is active, the sidebar item is determined above when transferring the explore VC's nav stack
|
|
sidebar.select(item: exploreItem!, animated: false)
|
|
doSelect(item: exploreItem!)
|
|
|
|
default:
|
|
return
|
|
}
|
|
}
|
|
|
|
func splitViewController(_ svc: UISplitViewController, willHide column: UISplitViewController.Column) {
|
|
if column == .primary {
|
|
sidebarVisibile = false
|
|
}
|
|
}
|
|
|
|
func splitViewController(_ svc: UISplitViewController, willShow column: UISplitViewController.Column) {
|
|
if column == .primary {
|
|
sidebarVisibile = true
|
|
}
|
|
}
|
|
}
|
|
|
|
extension MainSplitViewController: MainSidebarViewControllerDelegate {
|
|
func sidebarRequestPresentCompose(_ sidebarViewController: MainSidebarViewController) {
|
|
compose(editing: nil)
|
|
}
|
|
|
|
func sidebar(_ sidebarViewController: MainSidebarViewController, didSelectItem item: MainSidebarViewController.Item, previousItem: MainSidebarViewController.Item?) {
|
|
select(newItem: item, oldItem: previousItem)
|
|
}
|
|
|
|
func sidebar(_ sidebarViewController: MainSidebarViewController, showViewController viewController: UIViewController, previousItem: MainSidebarViewController.Item?) {
|
|
if let previousItem {
|
|
navigationStacks[previousItem] = secondaryNavController.viewControllers
|
|
}
|
|
secondaryNavController.viewControllers = [viewController]
|
|
}
|
|
|
|
func sidebar(_ sidebarViewController: MainSidebarViewController, scrollToTopFor item: MainSidebarViewController.Item) {
|
|
(secondaryNavController as? TabBarScrollableViewController)?.tabBarScrollToTop()
|
|
}
|
|
}
|
|
|
|
fileprivate extension MainSidebarViewController.Item {
|
|
@MainActor
|
|
func createRootViewController(_ mastodonController: MastodonController) -> UIViewController? {
|
|
switch self {
|
|
case .tab(.explore):
|
|
return InlineTrendsViewController(mastodonController: mastodonController)
|
|
case let .tab(tab):
|
|
return tab.createViewController(mastodonController)
|
|
case .bookmarks:
|
|
return BookmarksViewController(mastodonController: mastodonController)
|
|
case .favorites:
|
|
return FavoritesViewController(mastodonController: mastodonController)
|
|
case let .list(list):
|
|
return ListTimelineViewController(for: list, mastodonController: mastodonController)
|
|
case let .savedHashtag(name):
|
|
return HashtagTimelineViewController(forNamed: name, mastodonController: mastodonController)
|
|
case let .savedInstance(url):
|
|
return InstanceTimelineViewController(for: url, parentMastodonController: mastodonController)
|
|
case .listsHeader, .addList, .savedHashtagsHeader, .addSavedHashtag, .savedInstancesHeader, .addSavedInstance:
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
|
|
extension MainSplitViewController: TuskerNavigationDelegate {
|
|
var apiController: MastodonController! { mastodonController }
|
|
}
|
|
|
|
extension MainSplitViewController: StateRestorableViewController {
|
|
func stateRestorationActivity() -> NSUserActivity? {
|
|
if traitCollection.horizontalSizeClass == .compact {
|
|
return tabBarViewController.stateRestorationActivity()
|
|
} else if let currentItem = sidebar.selectedItem,
|
|
let navStack = navigationStackFor(item: currentItem),
|
|
let top = navStack.last as? StateRestorableViewController {
|
|
return top.stateRestorationActivity()
|
|
} else {
|
|
stateRestorationLogger.fault("MainSplitViewController: Unable to create state restoration activity")
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
|
|
extension MainSplitViewController: TuskerRootViewController {
|
|
func select(route: TuskerRoute, animated: Bool, completion: (() -> Void)?) {
|
|
guard traitCollection.horizontalSizeClass != .compact else {
|
|
tabBarViewController?.select(route: route, animated: animated, completion: completion)
|
|
return
|
|
}
|
|
guard presentedViewController == nil else {
|
|
dismiss(animated: animated) {
|
|
self.select(route: route, animated: animated, completion: completion)
|
|
}
|
|
return
|
|
}
|
|
let item: MainSidebarViewController.Item
|
|
switch route {
|
|
case .timelines:
|
|
item = .tab(.timelines)
|
|
case .notifications:
|
|
item = .tab(.notifications)
|
|
case .myProfile:
|
|
item = .tab(.myProfile)
|
|
case .explore:
|
|
item = .tab(.explore)
|
|
case .bookmarks:
|
|
item = .bookmarks
|
|
case .list(id: let id):
|
|
if let list = mastodonController.getCachedList(id: id) {
|
|
item = .list(list)
|
|
} else {
|
|
return
|
|
}
|
|
}
|
|
let oldItem = sidebar.selectedItem
|
|
sidebar.select(item: item, animated: false)
|
|
select(newItem: item, oldItem: oldItem)
|
|
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()
|
|
} else {
|
|
return self
|
|
}
|
|
}
|
|
|
|
func getNavigationController() -> NavigationControllerProtocol {
|
|
if traitCollection.horizontalSizeClass == .compact {
|
|
return tabBarViewController.getNavigationController()
|
|
} else {
|
|
return secondaryNavController
|
|
}
|
|
}
|
|
|
|
func performSearch(query: String) {
|
|
guard traitCollection.horizontalSizeClass != .compact else {
|
|
// ensure the tab bar VC is loaded
|
|
loadViewIfNeeded()
|
|
tabBarViewController.performSearch(query: query)
|
|
return
|
|
}
|
|
|
|
if sidebar.selectedItem != .tab(.explore) {
|
|
select(newItem: .tab(.explore), oldItem: sidebar.selectedItem)
|
|
}
|
|
|
|
guard let searchViewController = secondaryNavController.viewControllers.first as? InlineTrendsViewController else {
|
|
return
|
|
}
|
|
|
|
secondaryNavController.popToRootViewController(animated: false)
|
|
|
|
if searchViewController.isViewLoaded {
|
|
DispatchQueue.main.async {
|
|
searchViewController.searchController.isActive = true
|
|
}
|
|
} else {
|
|
searchViewController.searchControllerStatusOnAppearance = true
|
|
searchViewController.loadViewIfNeeded()
|
|
}
|
|
|
|
searchViewController.searchController.searchBar.text = query
|
|
searchViewController.resultsController.performSearch(query: query)
|
|
}
|
|
|
|
func presentPreferences(completion: (() -> Void)?) -> PreferencesNavigationController? {
|
|
let vc = PreferencesNavigationController(mastodonController: mastodonController)
|
|
present(vc, animated: true, completion: completion)
|
|
return vc
|
|
}
|
|
|
|
func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult {
|
|
guard presentedViewController == nil else {
|
|
return .continue
|
|
}
|
|
if traitCollection.horizontalSizeClass == .compact {
|
|
return tabBarViewController.handleStatusBarTapped(xPosition: xPosition)
|
|
} else {
|
|
let pointInSecondary = secondaryNavController.view.convert(CGPoint(x: xPosition, y: 0), from: view)
|
|
if secondaryNavController.view.bounds.contains(pointInSecondary),
|
|
let statusBarTappable = secondaryNavController as? StatusBarTappableViewController {
|
|
return statusBarTappable.handleStatusBarTapped(xPosition: pointInSecondary.x)
|
|
} else {
|
|
return .continue
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
extension MainSplitViewController: BackgroundableViewController {
|
|
func sceneDidEnterBackground() {
|
|
if traitCollection.horizontalSizeClass == .compact {
|
|
tabBarViewController.sceneDidEnterBackground()
|
|
} else {
|
|
// todo: should this do the same for the sidebar VC as well?
|
|
if let contentVC = viewController(for: .secondary) as? BackgroundableViewController {
|
|
contentVC.sceneDidEnterBackground()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
extension MainSplitViewController: FastAccountSwitcherViewControllerDelegate {
|
|
func fastAccountSwitcherAddToViewHierarchy(_ fastAccountSwitcher: FastAccountSwitcherViewController) {
|
|
view.addSubview(fastAccountSwitcher.view)
|
|
let currentAccount = fastAccountSwitcher.accountViews.first(where: \.isCurrent)!
|
|
let myProfileCell = sidebar.myProfileCell()!
|
|
NSLayoutConstraint.activate([
|
|
currentAccount.centerYAnchor.constraint(equalTo: myProfileCell.centerYAnchor),
|
|
|
|
fastAccountSwitcher.view.leadingAnchor.constraint(equalTo: sidebar.view.trailingAnchor),
|
|
fastAccountSwitcher.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
|
fastAccountSwitcher.view.topAnchor.constraint(equalTo: view.topAnchor),
|
|
fastAccountSwitcher.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
|
])
|
|
}
|
|
func fastAccountSwitcher(_ fastAccountSwitcher: FastAccountSwitcherViewController, triggerZoneContains point: CGPoint) -> Bool {
|
|
guard !isCollapsed,
|
|
let cell = sidebar.myProfileCell() else {
|
|
return false
|
|
}
|
|
let cellRect = cell.convert(cell.bounds, to: sidebar.view)
|
|
return cellRect.contains(point)
|
|
}
|
|
}
|
|
|
|
extension MainSplitViewController: AccountSwitchableViewController {
|
|
var isFastAccountSwitcherActive: Bool {
|
|
if isCollapsed {
|
|
return tabBarViewController.isFastAccountSwitcherActive
|
|
} else if let fastAccountSwitcher {
|
|
return !fastAccountSwitcher.view.isHidden
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
}
|