forked from zelo72/mastodon-ios
332 lines
12 KiB
Swift
332 lines
12 KiB
Swift
//
|
|
// RootSplitViewController.swift
|
|
// Mastodon
|
|
//
|
|
// Created by Cirno MainasuK on 2021-9-22.
|
|
//
|
|
|
|
import os.log
|
|
import UIKit
|
|
import Combine
|
|
import CoreDataStack
|
|
|
|
final class RootSplitViewController: UISplitViewController, NeedsDependency {
|
|
|
|
var disposeBag = Set<AnyCancellable>()
|
|
|
|
static let sidebarWidth: CGFloat = 89
|
|
|
|
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
|
|
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
|
|
|
|
private var isPrimaryDisplay = false
|
|
|
|
private(set) lazy var contentSplitViewController: ContentSplitViewController = {
|
|
let contentSplitViewController = ContentSplitViewController()
|
|
contentSplitViewController.context = context
|
|
contentSplitViewController.coordinator = coordinator
|
|
contentSplitViewController.delegate = self
|
|
return contentSplitViewController
|
|
}()
|
|
|
|
private(set) lazy var searchViewController: SearchViewController = {
|
|
let searchViewController = SearchViewController()
|
|
searchViewController.context = context
|
|
searchViewController.coordinator = coordinator
|
|
return searchViewController
|
|
}()
|
|
|
|
lazy var compactMainTabBarViewController = MainTabBarController(context: context, coordinator: coordinator)
|
|
|
|
let separatorLine = UIView.separatorLine
|
|
|
|
init(context: AppContext, coordinator: SceneCoordinator) {
|
|
self.context = context
|
|
self.coordinator = coordinator
|
|
super.init(style: .doubleColumn)
|
|
|
|
primaryEdge = .trailing
|
|
primaryBackgroundStyle = .sidebar
|
|
preferredDisplayMode = .twoBesideSecondary
|
|
preferredSplitBehavior = .tile
|
|
delegate = self
|
|
|
|
// disable edge swipe gesture
|
|
presentsWithGesture = false
|
|
|
|
if #available(iOS 14.5, *) {
|
|
displayModeButtonVisibility = .never
|
|
} else {
|
|
// Fallback on earlier versions
|
|
}
|
|
|
|
setViewController(searchViewController, for: .primary)
|
|
setViewController(contentSplitViewController, for: .secondary)
|
|
setViewController(compactMainTabBarViewController, for: .compact)
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
deinit {
|
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
|
}
|
|
|
|
}
|
|
|
|
extension RootSplitViewController {
|
|
|
|
override func viewDidLoad() {
|
|
super.viewDidLoad()
|
|
|
|
updateBehavior(size: view.frame.size)
|
|
|
|
setupBackground(theme: ThemeService.shared.currentTheme.value)
|
|
ThemeService.shared.currentTheme
|
|
.receive(on: DispatchQueue.main)
|
|
.sink { [weak self] theme in
|
|
guard let self = self else { return }
|
|
self.setupBackground(theme: theme)
|
|
}
|
|
.store(in: &disposeBag)
|
|
}
|
|
|
|
override func viewWillAppear(_ animated: Bool) {
|
|
super.viewWillAppear(animated)
|
|
|
|
updateBehavior(size: view.frame.size)
|
|
}
|
|
|
|
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
|
|
super.viewWillTransition(to: size, with: coordinator)
|
|
|
|
self.updateBehavior(size: size)
|
|
}
|
|
|
|
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
|
super.traitCollectionDidChange(previousTraitCollection)
|
|
|
|
setupBackground(theme: ThemeService.shared.currentTheme.value)
|
|
}
|
|
|
|
private func updateBehavior(size: CGSize) {
|
|
if size.width > 960 {
|
|
show(.primary)
|
|
isPrimaryDisplay = true
|
|
|
|
} else {
|
|
hide(.primary)
|
|
isPrimaryDisplay = false
|
|
}
|
|
|
|
switch (contentSplitViewController.currentSupplementaryTab, isPrimaryDisplay) {
|
|
case (.search, true):
|
|
// needs switch to other tab when primary display
|
|
// use FIFO queue save tab history
|
|
contentSplitViewController.currentSupplementaryTab = .home
|
|
default:
|
|
// do nothing
|
|
break
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
extension RootSplitViewController {
|
|
|
|
private func setupBackground(theme: Theme) {
|
|
// this set column separator line color
|
|
view.backgroundColor = theme.separator
|
|
}
|
|
|
|
}
|
|
|
|
// MARK: - ContentSplitViewControllerDelegate
|
|
extension RootSplitViewController: ContentSplitViewControllerDelegate {
|
|
func contentSplitViewController(_ contentSplitViewController: ContentSplitViewController, sidebarViewController: SidebarViewController, didSelectTab tab: MainTabBarController.Tab) {
|
|
guard let _ = MainTabBarController.Tab.allCases.firstIndex(of: tab) else {
|
|
assertionFailure()
|
|
return
|
|
}
|
|
switch tab {
|
|
case .search:
|
|
guard isPrimaryDisplay else {
|
|
// only control search tab behavior when primary display
|
|
fallthrough
|
|
}
|
|
guard let navigationController = searchViewController.navigationController else { return }
|
|
if navigationController.viewControllers.count == 1 {
|
|
searchViewController.searchBarTapPublisher.send()
|
|
} else {
|
|
navigationController.popToRootViewController(animated: true)
|
|
}
|
|
|
|
default:
|
|
let previousTab = contentSplitViewController.currentSupplementaryTab
|
|
contentSplitViewController.currentSupplementaryTab = tab
|
|
|
|
if previousTab == tab,
|
|
let navigationController = contentSplitViewController.mainTabBarController.selectedViewController as? UINavigationController
|
|
{
|
|
navigationController.popToRootViewController(animated: true)
|
|
}
|
|
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - UISplitViewControllerDelegate
|
|
extension RootSplitViewController: UISplitViewControllerDelegate {
|
|
|
|
private static func transform(from: UITabBarController, to: UITabBarController) {
|
|
let sourceNavigationControllers = from.viewControllers ?? []
|
|
let targetNavigationControllers = to.viewControllers ?? []
|
|
|
|
for (source, target) in zip(sourceNavigationControllers, targetNavigationControllers) {
|
|
guard let source = source as? UINavigationController,
|
|
let target = target as? UINavigationController
|
|
else { continue }
|
|
let viewControllers = source.popToRootViewController(animated: false) ?? []
|
|
_ = target.popToRootViewController(animated: false)
|
|
target.viewControllers.append(contentsOf: viewControllers)
|
|
}
|
|
|
|
to.selectedIndex = from.selectedIndex
|
|
}
|
|
|
|
private static func transform(from: UINavigationController, to: UINavigationController) {
|
|
let viewControllers = from.popToRootViewController(animated: false) ?? []
|
|
to.viewControllers.append(contentsOf: viewControllers)
|
|
}
|
|
|
|
// .regular to .compact
|
|
func splitViewController(
|
|
_ svc: UISplitViewController,
|
|
topColumnForCollapsingToProposedTopColumn proposedTopColumn: UISplitViewController.Column
|
|
) -> UISplitViewController.Column {
|
|
switch proposedTopColumn {
|
|
case .compact:
|
|
RootSplitViewController.transform(from: contentSplitViewController.mainTabBarController, to: compactMainTabBarViewController)
|
|
compactMainTabBarViewController.currentTab.value = contentSplitViewController.currentSupplementaryTab
|
|
|
|
default:
|
|
assertionFailure()
|
|
}
|
|
|
|
return proposedTopColumn
|
|
}
|
|
|
|
// .compact to .regular
|
|
func splitViewController(
|
|
_ svc: UISplitViewController,
|
|
displayModeForExpandingToProposedDisplayMode proposedDisplayMode: UISplitViewController.DisplayMode
|
|
) -> UISplitViewController.DisplayMode {
|
|
let compactNavigationController = compactMainTabBarViewController.selectedViewController as? UINavigationController
|
|
|
|
if let topMost = compactNavigationController?.topMost,
|
|
topMost is AccountListViewController {
|
|
topMost.dismiss(animated: false, completion: nil)
|
|
}
|
|
|
|
RootSplitViewController.transform(from: compactMainTabBarViewController, to: contentSplitViewController.mainTabBarController)
|
|
|
|
let tab = compactMainTabBarViewController.currentTab.value
|
|
if tab == .search {
|
|
contentSplitViewController.currentSupplementaryTab = .home
|
|
} else {
|
|
contentSplitViewController.currentSupplementaryTab = compactMainTabBarViewController.currentTab.value
|
|
}
|
|
|
|
return proposedDisplayMode
|
|
}
|
|
|
|
}
|
|
|
|
// MARK: - WizardViewControllerDelegate
|
|
extension RootSplitViewController: WizardViewControllerDelegate {
|
|
|
|
func readyToLayoutItem(_ wizardViewController: WizardViewController, item: WizardViewController.Item) -> Bool {
|
|
guard traitCollection.horizontalSizeClass != .compact else {
|
|
return compactMainTabBarViewController.readyToLayoutItem(wizardViewController, item: item)
|
|
}
|
|
|
|
switch item {
|
|
case .multipleAccountSwitch:
|
|
return contentSplitViewController.sidebarViewController.viewModel.isReadyForWizardAvatarButton
|
|
}
|
|
}
|
|
|
|
|
|
func layoutSpotlight(_ wizardViewController: WizardViewController, item: WizardViewController.Item) -> UIBezierPath {
|
|
guard traitCollection.horizontalSizeClass != .compact else {
|
|
return compactMainTabBarViewController.layoutSpotlight(wizardViewController, item: item)
|
|
}
|
|
|
|
switch item {
|
|
case .multipleAccountSwitch:
|
|
guard let frame = avatarButtonFrameInWizardView(wizardView: wizardViewController.view)
|
|
else {
|
|
assertionFailure()
|
|
return UIBezierPath()
|
|
}
|
|
return UIBezierPath(ovalIn: frame)
|
|
}
|
|
}
|
|
|
|
func layoutWizardCard(_ wizardViewController: WizardViewController, item: WizardViewController.Item) {
|
|
guard traitCollection.horizontalSizeClass != .compact else {
|
|
return compactMainTabBarViewController.layoutWizardCard(wizardViewController, item: item)
|
|
}
|
|
|
|
guard let frame = avatarButtonFrameInWizardView(wizardView: wizardViewController.view) else {
|
|
return
|
|
}
|
|
|
|
let anchorView = UIView()
|
|
anchorView.frame = frame
|
|
wizardViewController.backgroundView.addSubview(anchorView)
|
|
|
|
let wizardCardView = WizardCardView()
|
|
wizardCardView.arrowRectCorner = .allCorners // no arrow
|
|
wizardCardView.titleLabel.text = item.title
|
|
wizardCardView.descriptionLabel.text = item.description
|
|
|
|
wizardCardView.translatesAutoresizingMaskIntoConstraints = false
|
|
wizardViewController.backgroundView.addSubview(wizardCardView)
|
|
NSLayoutConstraint.activate([
|
|
wizardCardView.centerYAnchor.constraint(equalTo: anchorView.centerYAnchor),
|
|
wizardCardView.leadingAnchor.constraint(equalTo: anchorView.trailingAnchor, constant: 20), // 20pt spacing
|
|
wizardCardView.widthAnchor.constraint(equalToConstant: 320),
|
|
])
|
|
wizardCardView.setContentHuggingPriority(.defaultLow, for: .vertical)
|
|
}
|
|
|
|
private func avatarButtonFrameInWizardView(wizardView: UIView) -> CGRect? {
|
|
guard let diffableDataSource = contentSplitViewController.sidebarViewController.viewModel.diffableDataSource,
|
|
let indexPath = diffableDataSource.indexPath(for: .tab(.me)),
|
|
let cell = contentSplitViewController.sidebarViewController.collectionView.cellForItem(at: indexPath) as? SidebarListCollectionViewCell,
|
|
let contentView = cell._contentView,
|
|
let frame = sourceViewFrameInTargetView(
|
|
sourceView: contentView.avatarButton,
|
|
targetView: wizardView
|
|
)
|
|
else {
|
|
assertionFailure()
|
|
return nil
|
|
}
|
|
return frame
|
|
}
|
|
|
|
private func sourceViewFrameInTargetView(
|
|
sourceView: UIView,
|
|
targetView: UIView
|
|
) -> CGRect? {
|
|
guard let superview = sourceView.superview else {
|
|
assertionFailure()
|
|
return nil
|
|
}
|
|
return superview.convert(sourceView.frame, to: targetView)
|
|
}
|
|
}
|