forked from shadowfacts/Tusker
Initial multi-column navigation controller implementation
This commit is contained in:
parent
04deb08bcf
commit
9c368f295e
|
@ -92,6 +92,7 @@
|
||||||
D62D2422217AA7E1005076CC /* UserActivityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62D2421217AA7E1005076CC /* UserActivityManager.swift */; };
|
D62D2422217AA7E1005076CC /* UserActivityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62D2421217AA7E1005076CC /* UserActivityManager.swift */; };
|
||||||
D62D2424217ABF3F005076CC /* NSUserActivity+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62D2423217ABF3F005076CC /* NSUserActivity+Extensions.swift */; };
|
D62D2424217ABF3F005076CC /* NSUserActivity+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62D2423217ABF3F005076CC /* NSUserActivity+Extensions.swift */; };
|
||||||
D62D2426217ABF63005076CC /* UserActivityType.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62D2425217ABF63005076CC /* UserActivityType.swift */; };
|
D62D2426217ABF63005076CC /* UserActivityType.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62D2425217ABF63005076CC /* UserActivityType.swift */; };
|
||||||
|
D62D67C52A97D8CD00167EE2 /* MultiColumnNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62D67C42A97D8CD00167EE2 /* MultiColumnNavigationController.swift */; };
|
||||||
D62E9985279CA23900C26176 /* URLSession+Development.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62E9984279CA23900C26176 /* URLSession+Development.swift */; };
|
D62E9985279CA23900C26176 /* URLSession+Development.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62E9984279CA23900C26176 /* URLSession+Development.swift */; };
|
||||||
D62E9987279D094F00C26176 /* StatusMetaIndicatorsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62E9986279D094F00C26176 /* StatusMetaIndicatorsView.swift */; };
|
D62E9987279D094F00C26176 /* StatusMetaIndicatorsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62E9986279D094F00C26176 /* StatusMetaIndicatorsView.swift */; };
|
||||||
D62FF04823D7CDD700909D6E /* AttributedStringHelperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62FF04723D7CDD700909D6E /* AttributedStringHelperTests.swift */; };
|
D62FF04823D7CDD700909D6E /* AttributedStringHelperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62FF04723D7CDD700909D6E /* AttributedStringHelperTests.swift */; };
|
||||||
|
@ -488,6 +489,7 @@
|
||||||
D62D2421217AA7E1005076CC /* UserActivityManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserActivityManager.swift; sourceTree = "<group>"; };
|
D62D2421217AA7E1005076CC /* UserActivityManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserActivityManager.swift; sourceTree = "<group>"; };
|
||||||
D62D2423217ABF3F005076CC /* NSUserActivity+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSUserActivity+Extensions.swift"; sourceTree = "<group>"; };
|
D62D2423217ABF3F005076CC /* NSUserActivity+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSUserActivity+Extensions.swift"; sourceTree = "<group>"; };
|
||||||
D62D2425217ABF63005076CC /* UserActivityType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserActivityType.swift; sourceTree = "<group>"; };
|
D62D2425217ABF63005076CC /* UserActivityType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserActivityType.swift; sourceTree = "<group>"; };
|
||||||
|
D62D67C42A97D8CD00167EE2 /* MultiColumnNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiColumnNavigationController.swift; sourceTree = "<group>"; };
|
||||||
D62E9984279CA23900C26176 /* URLSession+Development.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URLSession+Development.swift"; sourceTree = "<group>"; };
|
D62E9984279CA23900C26176 /* URLSession+Development.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URLSession+Development.swift"; sourceTree = "<group>"; };
|
||||||
D62E9986279D094F00C26176 /* StatusMetaIndicatorsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusMetaIndicatorsView.swift; sourceTree = "<group>"; };
|
D62E9986279D094F00C26176 /* StatusMetaIndicatorsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusMetaIndicatorsView.swift; sourceTree = "<group>"; };
|
||||||
D62FF04723D7CDD700909D6E /* AttributedStringHelperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributedStringHelperTests.swift; sourceTree = "<group>"; };
|
D62FF04723D7CDD700909D6E /* AttributedStringHelperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributedStringHelperTests.swift; sourceTree = "<group>"; };
|
||||||
|
@ -1409,6 +1411,7 @@
|
||||||
D625E4812588262A0074BB2B /* DraggableTableViewCell.swift */,
|
D625E4812588262A0074BB2B /* DraggableTableViewCell.swift */,
|
||||||
D693DE5623FE1A6A0061E07D /* EnhancedNavigationViewController.swift */,
|
D693DE5623FE1A6A0061E07D /* EnhancedNavigationViewController.swift */,
|
||||||
D6E77D0E286F773900D8B732 /* SplitNavigationController.swift */,
|
D6E77D0E286F773900D8B732 /* SplitNavigationController.swift */,
|
||||||
|
D62D67C42A97D8CD00167EE2 /* MultiColumnNavigationController.swift */,
|
||||||
D6BC8747219738E1006163F1 /* EnhancedTableViewController.swift */,
|
D6BC8747219738E1006163F1 /* EnhancedTableViewController.swift */,
|
||||||
D693DE5823FE24300061E07D /* InteractivePushTransition.swift */,
|
D693DE5823FE24300061E07D /* InteractivePushTransition.swift */,
|
||||||
D6C693FB2162FE6F007D6A6D /* LoadingViewController.swift */,
|
D6C693FB2162FE6F007D6A6D /* LoadingViewController.swift */,
|
||||||
|
@ -2160,6 +2163,7 @@
|
||||||
D6B81F442560390300F6E31D /* MenuController.swift in Sources */,
|
D6B81F442560390300F6E31D /* MenuController.swift in Sources */,
|
||||||
D691771129A2B76A0054D7EF /* MainActor+Unsafe.swift in Sources */,
|
D691771129A2B76A0054D7EF /* MainActor+Unsafe.swift in Sources */,
|
||||||
D65B4B542971F71D00DABDFB /* EditedReport.swift in Sources */,
|
D65B4B542971F71D00DABDFB /* EditedReport.swift in Sources */,
|
||||||
|
D62D67C52A97D8CD00167EE2 /* MultiColumnNavigationController.swift in Sources */,
|
||||||
D6A6C11525B62E9700298D0F /* CacheExpiry.swift in Sources */,
|
D6A6C11525B62E9700298D0F /* CacheExpiry.swift in Sources */,
|
||||||
D67C57AF21E28EAD00C3118B /* Array+Uniques.swift in Sources */,
|
D67C57AF21E28EAD00C3118B /* Array+Uniques.swift in Sources */,
|
||||||
D68A76F129539116001DA1B3 /* FlipView.swift in Sources */,
|
D68A76F129539116001DA1B3 /* FlipView.swift in Sources */,
|
||||||
|
|
|
@ -20,8 +20,8 @@ class MainSplitViewController: UISplitViewController {
|
||||||
|
|
||||||
private var tabBarViewController: MainTabBarViewController!
|
private var tabBarViewController: MainTabBarViewController!
|
||||||
|
|
||||||
private var secondaryNavController: SplitNavigationController! {
|
private var secondaryNavController: NavigationControllerProtocol! {
|
||||||
viewController(for: .secondary) as? SplitNavigationController
|
viewController(for: .secondary) as? NavigationControllerProtocol
|
||||||
}
|
}
|
||||||
|
|
||||||
private var sidebarVisibile: Bool {
|
private var sidebarVisibile: Bool {
|
||||||
|
@ -59,9 +59,12 @@ class MainSplitViewController: UISplitViewController {
|
||||||
} else {
|
} else {
|
||||||
hide(.primary)
|
hide(.primary)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let multiColumnNav = MultiColumnNavigationController()
|
||||||
|
setViewController(multiColumnNav, for: .secondary)
|
||||||
|
|
||||||
let splitNav = SplitNavigationController()
|
// let splitNav = SplitNavigationController()
|
||||||
setViewController(splitNav, for: .secondary)
|
// setViewController(splitNav, for: .secondary)
|
||||||
// don't unnecesarily construct a content VC unless the we're in actually split mode
|
// 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
|
// when we change from compact -> split for the first time, the VC will be transferred anyways
|
||||||
if traitCollection.horizontalSizeClass != .compact {
|
if traitCollection.horizontalSizeClass != .compact {
|
||||||
|
@ -573,8 +576,9 @@ extension MainSplitViewController: TuskerRootViewController {
|
||||||
return tabBarViewController.handleStatusBarTapped(xPosition: xPosition)
|
return tabBarViewController.handleStatusBarTapped(xPosition: xPosition)
|
||||||
} else {
|
} else {
|
||||||
let pointInSecondary = secondaryNavController.view.convert(CGPoint(x: xPosition, y: 0), from: view)
|
let pointInSecondary = secondaryNavController.view.convert(CGPoint(x: xPosition, y: 0), from: view)
|
||||||
if secondaryNavController.view.bounds.contains(pointInSecondary) {
|
if secondaryNavController.view.bounds.contains(pointInSecondary),
|
||||||
return secondaryNavController.handleStatusBarTapped(xPosition: pointInSecondary.x)
|
let statusBarTappable = secondaryNavController as? StatusBarTappableViewController {
|
||||||
|
return statusBarTappable.handleStatusBarTapped(xPosition: pointInSecondary.x)
|
||||||
} else {
|
} else {
|
||||||
return .continue
|
return .continue
|
||||||
}
|
}
|
||||||
|
|
|
@ -83,8 +83,10 @@ enum TuskerRoute {
|
||||||
// case myProfile
|
// case myProfile
|
||||||
//}
|
//}
|
||||||
//
|
//
|
||||||
protocol NavigationControllerProtocol {
|
protocol NavigationControllerProtocol: UIViewController {
|
||||||
|
var viewControllers: [UIViewController] { get set }
|
||||||
var topViewController: UIViewController? { get }
|
var topViewController: UIViewController? { get }
|
||||||
|
@discardableResult
|
||||||
func popToRootViewController(animated: Bool) -> [UIViewController]?
|
func popToRootViewController(animated: Bool) -> [UIViewController]?
|
||||||
func pushViewController(_ vc: UIViewController, animated: Bool)
|
func pushViewController(_ vc: UIViewController, animated: Bool)
|
||||||
}
|
}
|
||||||
|
|
|
@ -110,9 +110,9 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
|
||||||
// just setting layout.configuration.contentInsetsReference doesn't work with UICollectionViewCompositionalLayout.list
|
// just setting layout.configuration.contentInsetsReference doesn't work with UICollectionViewCompositionalLayout.list
|
||||||
let layout = UICollectionViewCompositionalLayout { sectionIndex, environment in
|
let layout = UICollectionViewCompositionalLayout { sectionIndex, environment in
|
||||||
let section = NSCollectionLayoutSection.list(using: config, layoutEnvironment: environment)
|
let section = NSCollectionLayoutSection.list(using: config, layoutEnvironment: environment)
|
||||||
if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac {
|
// if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac {
|
||||||
section.contentInsetsReference = .readableContent
|
// section.contentInsetsReference = .readableContent
|
||||||
}
|
// }
|
||||||
return section
|
return section
|
||||||
}
|
}
|
||||||
collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
|
collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
|
||||||
|
|
|
@ -0,0 +1,325 @@
|
||||||
|
//
|
||||||
|
// MultiColumnNavigationController.swift
|
||||||
|
// Tusker
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 8/24/23.
|
||||||
|
// Copyright © 2023 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class MultiColumnNavigationController: UIViewController {
|
||||||
|
|
||||||
|
private var isManuallyUpdating = false
|
||||||
|
var viewControllers: [UIViewController] = [] {
|
||||||
|
didSet {
|
||||||
|
guard isViewLoaded,
|
||||||
|
!isManuallyUpdating else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
updateViews()
|
||||||
|
scrollToEnd(animated: false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var scrollView = UIScrollView()
|
||||||
|
private var stackView = UIStackView()
|
||||||
|
|
||||||
|
init() {
|
||||||
|
super.init(nibName: nil, bundle: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
view.backgroundColor = .appSecondaryBackground
|
||||||
|
|
||||||
|
scrollView.contentInsetAdjustmentBehavior = .always
|
||||||
|
scrollView.contentInset = UIEdgeInsets(top: 0, left: 8, bottom: 0, right: 8)
|
||||||
|
scrollView.alwaysBounceHorizontal = true
|
||||||
|
scrollView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
|
||||||
|
stackView.axis = .horizontal
|
||||||
|
stackView.spacing = 8
|
||||||
|
stackView.alignment = .fill
|
||||||
|
stackView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
scrollView.addSubview(stackView)
|
||||||
|
|
||||||
|
view.addSubview(scrollView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||||
|
scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||||
|
scrollView.topAnchor.constraint(equalTo: view.topAnchor),
|
||||||
|
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||||
|
|
||||||
|
stackView.leadingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.leadingAnchor),
|
||||||
|
stackView.trailingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.trailingAnchor),
|
||||||
|
stackView.topAnchor.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor),
|
||||||
|
stackView.bottomAnchor.constraint(equalTo: scrollView.contentLayoutGuide.bottomAnchor),
|
||||||
|
stackView.heightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.heightAnchor),
|
||||||
|
])
|
||||||
|
|
||||||
|
updateViews()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateViews() {
|
||||||
|
var i = 0
|
||||||
|
while i < viewControllers.count {
|
||||||
|
let needsCloseButton = i > 0
|
||||||
|
if i <= stackView.arrangedSubviews.count - 1 {
|
||||||
|
let existing = stackView.arrangedSubviews[i] as! ColumnView
|
||||||
|
existing.setContent(viewControllers[i], needsCloseButton: needsCloseButton)
|
||||||
|
} else {
|
||||||
|
let new = ColumnView(owner: self, contentViewController: viewControllers[i], needsCloseButton: needsCloseButton)
|
||||||
|
stackView.addArrangedSubview(new)
|
||||||
|
}
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
while i < stackView.arrangedSubviews.count {
|
||||||
|
let toRemove = stackView.arrangedSubviews[i] as! ColumnView
|
||||||
|
toRemove.willRemoveColumn()
|
||||||
|
stackView.removeArrangedSubview(toRemove)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func show(_ vc: UIViewController, sender: Any?) {
|
||||||
|
if let sender = sender as? UIViewController {
|
||||||
|
var index: Int? = nil
|
||||||
|
var current: UIViewController? = sender
|
||||||
|
while let c = current {
|
||||||
|
index = viewControllers.firstIndex(of: c)
|
||||||
|
if index != nil {
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
current = c.parent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let index {
|
||||||
|
replaceViewControllers([vc], after: index, animated: true)
|
||||||
|
} else {
|
||||||
|
pushViewController(vc, animated: true)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
pushViewController(vc, animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
SplitNavigationController.clearSelectedRow(sender: sender)
|
||||||
|
}
|
||||||
|
|
||||||
|
func replaceViewControllers(_ vcs: [UIViewController], after afterIndex: Int, animated: Bool) {
|
||||||
|
if afterIndex == viewControllers.count - 1 && vcs.count == 1 {
|
||||||
|
pushViewController(vcs[0], animated: animated)
|
||||||
|
} else {
|
||||||
|
viewControllers = Array(viewControllers[...afterIndex]) + vcs
|
||||||
|
scrollToEnd(animated: animated)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func scrollToEnd(animated: Bool) {
|
||||||
|
scrollColumnToEnd(columnIndex: viewControllers.count - 1, animated: animated)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func scrollColumnToEnd(columnIndex: Int, animated: Bool) {
|
||||||
|
scrollView.layoutIfNeeded()
|
||||||
|
let column = stackView.arrangedSubviews[columnIndex]
|
||||||
|
let columnFrame = column.convert(column.bounds, to: scrollView)
|
||||||
|
let offset: CGFloat
|
||||||
|
if columnFrame.maxX < scrollView.bounds.width - scrollView.adjustedTrailingContentInset {
|
||||||
|
offset = -scrollView.adjustedLeadingContentInset
|
||||||
|
} else {
|
||||||
|
offset = columnFrame.minX + scrollView.adjustedLeadingContentInset - (scrollView.bounds.width - columnFrame.width)
|
||||||
|
}
|
||||||
|
scrollView.setContentOffset(CGPoint(x: offset, y: -scrollView.adjustedContentInset.top), animated: animated)
|
||||||
|
}
|
||||||
|
|
||||||
|
fileprivate func closeColumn(_ vc: UIViewController) {
|
||||||
|
let index = viewControllers.firstIndex(of: vc)!
|
||||||
|
guard index > 0 else {
|
||||||
|
// Can't close the last column
|
||||||
|
return
|
||||||
|
}
|
||||||
|
isManuallyUpdating = true
|
||||||
|
defer { isManuallyUpdating = false }
|
||||||
|
viewControllers.removeSubrange(index...)
|
||||||
|
animateChanges {
|
||||||
|
for column in self.stackView.arrangedSubviews[index...] {
|
||||||
|
column.layer.opacity = 0
|
||||||
|
}
|
||||||
|
self.scrollColumnToEnd(columnIndex: index - 1, animated: false)
|
||||||
|
} completion: {
|
||||||
|
self.updateViews()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private func animateChanges(_ animations: @escaping () -> Void, completion: (() -> Void)? = nil) {
|
||||||
|
let animator = UIViewPropertyAnimator(duration: 0.3, timingParameters: UISpringTimingParameters())
|
||||||
|
animator.addAnimations(animations)
|
||||||
|
animator.addCompletion { _ in
|
||||||
|
completion?()
|
||||||
|
}
|
||||||
|
animator.startAnimation()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension MultiColumnNavigationController: NavigationControllerProtocol {
|
||||||
|
var topViewController: UIViewController? {
|
||||||
|
viewControllers.last
|
||||||
|
}
|
||||||
|
|
||||||
|
func popToRootViewController(animated: Bool) -> [UIViewController]? {
|
||||||
|
let removed = Array(viewControllers.dropFirst())
|
||||||
|
viewControllers = [viewControllers.first!]
|
||||||
|
return removed
|
||||||
|
}
|
||||||
|
|
||||||
|
func pushViewController(_ vc: UIViewController, animated: Bool) {
|
||||||
|
isManuallyUpdating = true
|
||||||
|
defer { isManuallyUpdating = false }
|
||||||
|
viewControllers.append(vc)
|
||||||
|
updateViews()
|
||||||
|
scrollToEnd(animated: animated)
|
||||||
|
if animated {
|
||||||
|
let column = stackView.arrangedSubviews.last!
|
||||||
|
column.layer.opacity = 0
|
||||||
|
animateChanges {
|
||||||
|
column.layer.opacity = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ColumnView: UIView {
|
||||||
|
private unowned let owner: MultiColumnNavigationController
|
||||||
|
private let contentView = UIView()
|
||||||
|
private let navigationController: ColumnNavigationController
|
||||||
|
private var contentViewController: UIViewController!
|
||||||
|
|
||||||
|
init(owner: MultiColumnNavigationController, contentViewController: UIViewController, needsCloseButton: Bool) {
|
||||||
|
self.owner = owner
|
||||||
|
self.navigationController = ColumnNavigationController(owner: owner)
|
||||||
|
super.init(frame: .zero)
|
||||||
|
|
||||||
|
contentView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
addSubview(contentView)
|
||||||
|
|
||||||
|
layer.shadowOpacity = 0.2
|
||||||
|
layer.shadowRadius = 8
|
||||||
|
layer.shadowOffset = .zero
|
||||||
|
contentView.layer.masksToBounds = false
|
||||||
|
contentView.layer.cornerRadius = 12.5
|
||||||
|
contentView.layer.cornerCurve = .continuous
|
||||||
|
contentView.layer.masksToBounds = true
|
||||||
|
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
contentView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||||
|
contentView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||||
|
contentView.topAnchor.constraint(equalTo: topAnchor),
|
||||||
|
contentView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||||
|
|
||||||
|
widthAnchor.constraint(equalToConstant: 400),
|
||||||
|
])
|
||||||
|
|
||||||
|
setContent(contentViewController, needsCloseButton: needsCloseButton)
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
func setContent(_ viewController: UIViewController, needsCloseButton: Bool) {
|
||||||
|
guard viewController !== contentViewController else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
contentViewController?.removeViewAndController()
|
||||||
|
|
||||||
|
if navigationController.parent != owner {
|
||||||
|
navigationController.removeViewAndController()
|
||||||
|
|
||||||
|
owner.addChild(navigationController)
|
||||||
|
navigationController.didMove(toParent: owner)
|
||||||
|
navigationController.view.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
contentView.addSubview(navigationController.view)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
navigationController.view.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
|
||||||
|
navigationController.view.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
|
||||||
|
navigationController.view.topAnchor.constraint(equalTo: contentView.topAnchor),
|
||||||
|
navigationController.view.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
contentViewController = viewController
|
||||||
|
navigationController.setViewControllers([viewController], animated: false)
|
||||||
|
|
||||||
|
if needsCloseButton {
|
||||||
|
installCloseBarButton(navigationItem: viewController.navigationItem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func willRemoveColumn() {
|
||||||
|
navigationController.removeViewAndController()
|
||||||
|
navigationController.setViewControllers([], animated: false)
|
||||||
|
removeCloseBarButton(navigationItem: contentViewController.navigationItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func installCloseBarButton(navigationItem: UINavigationItem) {
|
||||||
|
let item = UIBarButtonItem(image: UIImage(systemName: "xmark"), style: .done, target: self, action: #selector(closeNavigationColumn))
|
||||||
|
item.accessibilityLabel = "Close Column"
|
||||||
|
if navigationItem.leftBarButtonItems != nil {
|
||||||
|
navigationItem.leftBarButtonItems!.insert(item, at: 0)
|
||||||
|
} else {
|
||||||
|
navigationItem.leftBarButtonItems = [item]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func removeCloseBarButton(navigationItem: UINavigationItem) {
|
||||||
|
navigationItem.leftBarButtonItems = (navigationItem.leftBarButtonItems ?? []).filter {
|
||||||
|
$0.action != #selector(closeNavigationColumn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func closeNavigationColumn() {
|
||||||
|
owner.closeColumn(contentViewController)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ColumnNavigationController: UINavigationController {
|
||||||
|
unowned let owner: MultiColumnNavigationController
|
||||||
|
|
||||||
|
init(owner: MultiColumnNavigationController) {
|
||||||
|
self.owner = owner
|
||||||
|
super.init(nibName: nil, bundle: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder aDecoder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override func show(_ vc: UIViewController, sender: Any?) {
|
||||||
|
owner.show(vc, sender: sender)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension UIScrollView {
|
||||||
|
var adjustedLeadingContentInset: CGFloat {
|
||||||
|
if traitCollection.layoutDirection == .leftToRight {
|
||||||
|
adjustedContentInset.left
|
||||||
|
} else {
|
||||||
|
adjustedContentInset.right
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var adjustedTrailingContentInset: CGFloat {
|
||||||
|
if traitCollection.layoutDirection == .leftToRight {
|
||||||
|
adjustedContentInset.right
|
||||||
|
} else {
|
||||||
|
adjustedContentInset.left
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -49,19 +49,7 @@ class SplitNavigationController: UIViewController {
|
||||||
rootNav.showImpl = { [unowned self] vc, sender in
|
rootNav.showImpl = { [unowned self] vc, sender in
|
||||||
if self.canShowSecondaryNav {
|
if self.canShowSecondaryNav {
|
||||||
self.setSecondaryViewControllers([vc], animated: true)
|
self.setSecondaryViewControllers([vc], animated: true)
|
||||||
|
SplitNavigationController.clearSelectedRow(sender: sender)
|
||||||
// the split nav shouldn't really be reaching down into the inner VCs like this,
|
|
||||||
// but I can't think of a cleaner way
|
|
||||||
if let tableVC = sender as? UITableViewController,
|
|
||||||
let selectedIndexPath = tableVC.tableView.indexPathForSelectedRow {
|
|
||||||
tableVC.tableView.deselectRow(at: selectedIndexPath, animated: true)
|
|
||||||
} else if let sender = sender as? UIViewController,
|
|
||||||
let collectionView = (sender as? CollectionViewController)?.collectionView ?? sender.view as? UICollectionView {
|
|
||||||
// the collection view's animation speed is weirdly fast, so we do it slower
|
|
||||||
UIView.animate(withDuration: 0.5, delay: 0) {
|
|
||||||
collectionView.indexPathsForSelectedItems?.forEach { collectionView.deselectItem(at: $0, animated: false) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
self.rootNav.pushViewController(vc, animated: true)
|
self.rootNav.pushViewController(vc, animated: true)
|
||||||
}
|
}
|
||||||
|
@ -118,6 +106,21 @@ class SplitNavigationController: UIViewController {
|
||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static func clearSelectedRow(sender: Any?) {
|
||||||
|
// the split nav shouldn't really be reaching down into the inner VCs like this,
|
||||||
|
// but I can't think of a cleaner way
|
||||||
|
if let tableVC = sender as? UITableViewController,
|
||||||
|
let selectedIndexPath = tableVC.tableView.indexPathForSelectedRow {
|
||||||
|
tableVC.tableView.deselectRow(at: selectedIndexPath, animated: true)
|
||||||
|
} else if let sender = sender as? UIViewController,
|
||||||
|
let collectionView = (sender as? CollectionViewController)?.collectionView ?? sender.view as? UICollectionView {
|
||||||
|
// the collection view's animation speed is weirdly fast, so we do it slower
|
||||||
|
UIView.animate(withDuration: 0.5, delay: 0) {
|
||||||
|
collectionView.indexPathsForSelectedItems?.forEach { collectionView.deselectItem(at: $0, animated: false) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue