Two column navigation on iPad
This commit is contained in:
parent
efb96eddf3
commit
5b70c713b2
|
@ -291,6 +291,7 @@
|
|||
D6E77D09286D25FA00D8B732 /* TrendingHashtagCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E77D08286D25FA00D8B732 /* TrendingHashtagCollectionViewCell.swift */; };
|
||||
D6E77D0B286D426E00D8B732 /* TrendingLinkCardCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E77D0A286D426E00D8B732 /* TrendingLinkCardCollectionViewCell.swift */; };
|
||||
D6E77D0D286E6B7300D8B732 /* TrendingLinkCardCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6E77D0C286E6B7300D8B732 /* TrendingLinkCardCollectionViewCell.xib */; };
|
||||
D6E77D0F286F773900D8B732 /* SplitNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E77D0E286F773900D8B732 /* SplitNavigationController.swift */; };
|
||||
D6E9CDA8281A427800BBC98E /* PostService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E9CDA7281A427800BBC98E /* PostService.swift */; };
|
||||
D6EAE0DB2550CC8A002DB0AC /* FocusableTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6EAE0DA2550CC8A002DB0AC /* FocusableTextField.swift */; };
|
||||
D6EBF01523C55C0900AE061B /* UIApplication+Scenes.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6EBF01423C55C0900AE061B /* UIApplication+Scenes.swift */; };
|
||||
|
@ -645,6 +646,7 @@
|
|||
D6E77D08286D25FA00D8B732 /* TrendingHashtagCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingHashtagCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||
D6E77D0A286D426E00D8B732 /* TrendingLinkCardCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingLinkCardCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||
D6E77D0C286E6B7300D8B732 /* TrendingLinkCardCollectionViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = TrendingLinkCardCollectionViewCell.xib; sourceTree = "<group>"; };
|
||||
D6E77D0E286F773900D8B732 /* SplitNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplitNavigationController.swift; sourceTree = "<group>"; };
|
||||
D6E9CDA7281A427800BBC98E /* PostService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostService.swift; sourceTree = "<group>"; };
|
||||
D6EAE0DA2550CC8A002DB0AC /* FocusableTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FocusableTextField.swift; sourceTree = "<group>"; };
|
||||
D6EBF01423C55C0900AE061B /* UIApplication+Scenes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIApplication+Scenes.swift"; sourceTree = "<group>"; };
|
||||
|
@ -1298,6 +1300,7 @@
|
|||
D653F410267D1E32004E32B1 /* DiffableTimelineLikeTableViewController.swift */,
|
||||
D625E4812588262A0074BB2B /* DraggableTableViewCell.swift */,
|
||||
D693DE5623FE1A6A0061E07D /* EnhancedNavigationViewController.swift */,
|
||||
D6E77D0E286F773900D8B732 /* SplitNavigationController.swift */,
|
||||
D6BC8747219738E1006163F1 /* EnhancedTableViewController.swift */,
|
||||
D693DE5823FE24300061E07D /* InteractivePushTransition.swift */,
|
||||
D6C693FB2162FE6F007D6A6D /* LoadingViewController.swift */,
|
||||
|
@ -1804,6 +1807,7 @@
|
|||
D662AEF2263A4BE10082A153 /* ComposePollView.swift in Sources */,
|
||||
D677284A24ECBDF400C732D3 /* ComposeCurrentAccount.swift in Sources */,
|
||||
D62D2424217ABF3F005076CC /* NSUserActivity+Extensions.swift in Sources */,
|
||||
D6E77D0F286F773900D8B732 /* SplitNavigationController.swift in Sources */,
|
||||
D6B30E09254BAF63009CAEE5 /* ImageGrayscalifier.swift in Sources */,
|
||||
D6A6C10F25B62D2400298D0F /* DiskCache.swift in Sources */,
|
||||
D6B81F3C2560365300F6E31D /* RefreshableViewController.swift in Sources */,
|
||||
|
|
|
@ -20,8 +20,11 @@ class MainSplitViewController: UISplitViewController {
|
|||
|
||||
private var tabBarViewController: MainTabBarViewController!
|
||||
|
||||
private var secondaryNavController: UINavigationController! {
|
||||
viewController(for: .secondary) as? UINavigationController
|
||||
// private var secondaryNavController: UINavigationController! {
|
||||
// viewController(for: .secondary) as? UINavigationController
|
||||
// }
|
||||
private var secondaryNavController: SplitNavigationController! {
|
||||
viewController(for: .secondary) as? SplitNavigationController
|
||||
}
|
||||
|
||||
init(mastodonController: MastodonController) {
|
||||
|
@ -46,9 +49,10 @@ class MainSplitViewController: UISplitViewController {
|
|||
setViewController(sidebar, for: .primary)
|
||||
primaryBackgroundStyle = .sidebar
|
||||
|
||||
let secondaryNav = EnhancedNavigationViewController()
|
||||
secondaryNav.useBrowserStyleNavigation = true
|
||||
setViewController(secondaryNav, for: .secondary)
|
||||
// let secondaryNav = EnhancedNavigationViewController()
|
||||
// secondaryNav.useBrowserStyleNavigation = true
|
||||
let splitNav = SplitNavigationController()
|
||||
setViewController(splitNav, 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 {
|
||||
|
|
|
@ -142,7 +142,7 @@ class MainTabBarViewController: UITabBarController, UITabBarControllerDelegate {
|
|||
return vc
|
||||
} else {
|
||||
let nav = EnhancedNavigationViewController(rootViewController: vc)
|
||||
nav.useBrowserStyleNavigation = true
|
||||
// nav.useBrowserStyleNavigation = true
|
||||
return nav
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,262 @@
|
|||
//
|
||||
// SplitNavigationController.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 7/1/22.
|
||||
// Copyright © 2022 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class SplitNavigationController: UIViewController {
|
||||
|
||||
private let rootNav = SplitRootNavigationController()
|
||||
private let secondaryNav = SplitSecondaryNavigationController()
|
||||
private let separatorView = UIView()
|
||||
|
||||
private var constraints: [NSLayoutConstraint] = []
|
||||
|
||||
var viewControllers: [UIViewController] {
|
||||
get {
|
||||
return rootNav.viewControllers + secondaryNav.viewControllers
|
||||
}
|
||||
set {
|
||||
if newValue.isEmpty {
|
||||
rootNav.viewControllers = []
|
||||
secondaryNav.viewControllers = []
|
||||
} else if canShowSecondaryNav {
|
||||
var newValue = newValue
|
||||
rootNav.viewControllers = [newValue.removeFirst()]
|
||||
secondaryNav.viewControllers = newValue
|
||||
} else {
|
||||
rootNav.viewControllers = newValue
|
||||
secondaryNav.viewControllers = []
|
||||
}
|
||||
updateSecondaryNavVisibility()
|
||||
}
|
||||
}
|
||||
|
||||
/// This property is only valid after the view has been laid out.
|
||||
private var canShowSecondaryNav: Bool {
|
||||
// minimum of 360pt for each column
|
||||
// this allows split navigation on all ipads in portrait w/ sidebar hidden and in landscape (regardless of sidebar)
|
||||
(viewIfLoaded?.bounds.width ?? 0) >= 720
|
||||
}
|
||||
|
||||
init(rootViewController: UIViewController? = nil) {
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
|
||||
rootNav.showImpl = { [unowned self] vc, sender in
|
||||
if self.canShowSecondaryNav {
|
||||
self.setSecondaryViewControllers([vc], animated: true)
|
||||
|
||||
// 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 {
|
||||
self.rootNav.pushViewController(vc, animated: true)
|
||||
}
|
||||
}
|
||||
secondaryNav.closeSecondaryImpl = { [unowned self] in
|
||||
self.popToRootViewController(animated: true)
|
||||
}
|
||||
|
||||
if let rootViewController {
|
||||
rootNav.viewControllers = [rootViewController]
|
||||
}
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
embedChild(rootNav, layout: false)
|
||||
embedChild(secondaryNav, layout: false)
|
||||
rootNav.view.translatesAutoresizingMaskIntoConstraints = false
|
||||
secondaryNav.view.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
separatorView.backgroundColor = .separator
|
||||
separatorView.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.addSubview(separatorView)
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
rootNav.view.topAnchor.constraint(equalTo: view.topAnchor),
|
||||
rootNav.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||
rootNav.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
|
||||
separatorView.topAnchor.constraint(equalTo: view.topAnchor),
|
||||
separatorView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||
separatorView.leadingAnchor.constraint(equalTo: rootNav.view.trailingAnchor),
|
||||
separatorView.widthAnchor.constraint(equalToConstant: 0.5),
|
||||
|
||||
secondaryNav.view.topAnchor.constraint(equalTo: view.topAnchor),
|
||||
secondaryNav.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||
secondaryNav.view.leadingAnchor.constraint(equalTo: separatorView.trailingAnchor),
|
||||
])
|
||||
|
||||
updateSecondaryNavVisibility()
|
||||
}
|
||||
|
||||
override func show(_ vc: UIViewController, sender: Any?) {
|
||||
if !canShowSecondaryNav {
|
||||
rootNav.pushViewController(vc, animated: true)
|
||||
} else if rootNav.viewControllers.isEmpty {
|
||||
rootNav.pushViewController(vc, animated: false)
|
||||
} else {
|
||||
secondaryNav.pushViewController(vc, animated: true)
|
||||
}
|
||||
updateSecondaryNavVisibility()
|
||||
}
|
||||
|
||||
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||
super.traitCollectionDidChange(previousTraitCollection)
|
||||
|
||||
|
||||
}
|
||||
|
||||
override func viewWillLayoutSubviews() {
|
||||
super.viewWillLayoutSubviews()
|
||||
|
||||
if !isLayingOutForAnimation {
|
||||
updateSecondaryNavVisibility()
|
||||
}
|
||||
}
|
||||
|
||||
private func updateSecondaryNavVisibility() {
|
||||
guard isViewLoaded else {
|
||||
return
|
||||
}
|
||||
|
||||
if canShowSecondaryNav {
|
||||
if rootNav.viewControllers.count > 1 {
|
||||
var vcs = rootNav.viewControllers
|
||||
let root = vcs.removeFirst()
|
||||
rootNav.viewControllers = [root]
|
||||
// this shouldn't be necessary since the vcs are removed from their parent vc by setting rootNav.viewControllers
|
||||
// but it doesn't remove the views from their superview (until the next runloop iteration?)
|
||||
// so we need to do that ourselves before we can set them on the secondary nav (otherwise it raises an exception)
|
||||
vcs.forEach { $0.removeViewAndController() }
|
||||
secondaryNav.viewControllers = vcs
|
||||
}
|
||||
} else {
|
||||
if !secondaryNav.viewControllers.isEmpty {
|
||||
let firstSecondary = secondaryNav.viewControllers.first!
|
||||
// remove the left bar button item so that the builtin Back item shows
|
||||
if firstSecondary.navigationItem.leftBarButtonItem?.tag == ViewTags.splitNavCloseSecondaryButton {
|
||||
firstSecondary.navigationItem.leftBarButtonItem = nil
|
||||
}
|
||||
rootNav.viewControllers.append(contentsOf: secondaryNav.viewControllers)
|
||||
secondaryNav.viewControllers = []
|
||||
}
|
||||
}
|
||||
|
||||
setSecondaryVisible(canShowSecondaryNav && !secondaryNav.viewControllers.isEmpty)
|
||||
}
|
||||
|
||||
private func setSecondaryVisible(_ visible: Bool) {
|
||||
guard isViewLoaded else {
|
||||
return
|
||||
}
|
||||
|
||||
NSLayoutConstraint.deactivate(constraints)
|
||||
if visible {
|
||||
constraints = [
|
||||
rootNav.view.trailingAnchor.constraint(equalTo: view.centerXAnchor),
|
||||
secondaryNav.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||
]
|
||||
} else {
|
||||
constraints = [
|
||||
rootNav.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||
secondaryNav.view.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.5),
|
||||
]
|
||||
}
|
||||
NSLayoutConstraint.activate(constraints)
|
||||
}
|
||||
|
||||
private func setSecondaryViewControllers(_ vcs: [UIViewController], animated: Bool) {
|
||||
if animated {
|
||||
if vcs.isEmpty {
|
||||
popToRootViewController(animated: true)
|
||||
} else {
|
||||
let wasVisible = !secondaryNav.viewControllers.isEmpty
|
||||
secondaryNav.viewControllers = vcs
|
||||
secondaryNav.view.frame = CGRect(x: view.bounds.width, y: 0, width: view.bounds.width / 2, height: view.bounds.height)
|
||||
secondaryNav.view.layoutIfNeeded()
|
||||
if !wasVisible {
|
||||
let animator = UIViewPropertyAnimator(duration: 0.35, curve: .easeInOut) {
|
||||
self.updateSecondaryNavVisibility()
|
||||
self.view.layoutIfNeeded()
|
||||
}
|
||||
animator.startAnimation()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
secondaryNav.viewControllers = vcs
|
||||
updateSecondaryNavVisibility()
|
||||
}
|
||||
}
|
||||
|
||||
private var isLayingOutForAnimation = false
|
||||
|
||||
func popToRootViewController(animated: Bool) {
|
||||
if animated {
|
||||
// we don't update secondaryNav.viewControllers until after the animation is completed
|
||||
// otherwise the secondary nav's contents disappear immediately, rather than sliding off-screen
|
||||
let animator = UIViewPropertyAnimator(duration: 0.35, curve: .easeInOut) {
|
||||
self.isLayingOutForAnimation = true
|
||||
self.setSecondaryVisible(false)
|
||||
self.view.layoutIfNeeded()
|
||||
}
|
||||
animator.addCompletion { _ in
|
||||
self.secondaryNav.viewControllers = []
|
||||
self.isLayingOutForAnimation = false
|
||||
// self.updateSecondaryNavVisibility()
|
||||
}
|
||||
animator.startAnimation()
|
||||
} else {
|
||||
self.secondaryNav.viewControllers = []
|
||||
self.updateSecondaryNavVisibility()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private class SplitRootNavigationController: UINavigationController {
|
||||
fileprivate var showImpl: ((UIViewController, Any?) -> Void)!
|
||||
|
||||
override func show(_ vc: UIViewController, sender: Any?) {
|
||||
showImpl(vc, sender)
|
||||
}
|
||||
}
|
||||
|
||||
private class SplitSecondaryNavigationController: EnhancedNavigationViewController {
|
||||
fileprivate var closeSecondaryImpl: (() -> Void)!
|
||||
|
||||
override var viewControllers: [UIViewController] {
|
||||
didSet {
|
||||
if let first = viewControllers.first {
|
||||
configureSecondarySplitCloseButton(for: first)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func configureSecondarySplitCloseButton(for viewController: UIViewController) {
|
||||
guard viewController.navigationItem.leftBarButtonItem?.tag != ViewTags.splitNavCloseSecondaryButton else {
|
||||
return
|
||||
}
|
||||
let item = UIBarButtonItem(title: "Close", style: .done, target: self, action: #selector(closeSecondary))
|
||||
item.tag = ViewTags.splitNavCloseSecondaryButton
|
||||
viewController.navigationItem.leftBarButtonItem = item
|
||||
}
|
||||
|
||||
@objc private func closeSecondary() {
|
||||
closeSecondaryImpl()
|
||||
}
|
||||
|
||||
}
|
|
@ -10,7 +10,7 @@ import UIKit
|
|||
|
||||
// Based on MVCTodo by Dave DeLong: https://github.com/davedelong/MVCTodo/blob/841649dd6aa31bacda3ad7ef9a9a836f66281e50/MVCTodo/Extensions/UIViewController.swift
|
||||
extension UIViewController {
|
||||
func embedChild(_ newChild: UIViewController, in container: UIView? = nil) {
|
||||
func embedChild(_ newChild: UIViewController, in container: UIView? = nil, layout: Bool = true) {
|
||||
// if the view controller is already a child of something else, remove it
|
||||
if let oldParent = newChild.parent, oldParent != self {
|
||||
newChild.beginAppearanceTransition(false, animated: false)
|
||||
|
@ -36,7 +36,7 @@ extension UIViewController {
|
|||
newChild.beginAppearanceTransition(true, animated: false)
|
||||
addChild(newChild)
|
||||
newChild.didMove(toParent: self)
|
||||
targetContainer.embedSubview(newChild.view)
|
||||
targetContainer.embedSubview(newChild.view, layout: layout)
|
||||
newChild.endAppearanceTransition()
|
||||
} else {
|
||||
// the view controller is already a child
|
||||
|
@ -45,7 +45,7 @@ extension UIViewController {
|
|||
// we don't do the appearance transition stuff here,
|
||||
// because the vc is already a child, so *presumably*
|
||||
// that transition stuff has already appened
|
||||
targetContainer.embedSubview(newChild.view)
|
||||
targetContainer.embedSubview(newChild.view, layout: layout)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -57,16 +57,18 @@ extension UIViewController {
|
|||
|
||||
// Based on MVCTodo by Dave DeLong: https://github.com/davedelong/MVCTodo/blob/841649dd6aa31bacda3ad7ef9a9a836f66281e50/MVCTodo/Extensions/UIView.swift
|
||||
extension UIView {
|
||||
func embedSubview(_ subview: UIView) {
|
||||
func embedSubview(_ subview: UIView, layout: Bool = true) {
|
||||
if subview.superview == self { return }
|
||||
|
||||
if subview.superview != nil {
|
||||
subview.removeFromSuperview()
|
||||
}
|
||||
|
||||
subview.frame = bounds
|
||||
addSubview(subview)
|
||||
|
||||
if layout {
|
||||
subview.frame = bounds
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
subview.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||
subview.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||
|
@ -74,6 +76,7 @@ extension UIView {
|
|||
subview.bottomAnchor.constraint(equalTo: bottomAnchor)
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
func isContainedWithin(_ other: UIView) -> Bool {
|
||||
var current: UIView? = self
|
||||
|
|
|
@ -16,4 +16,5 @@ struct ViewTags {
|
|||
static let navBackBarButton = 42003
|
||||
static let navForwardBarButton = 42004
|
||||
static let navEmptyTitleView = 42005
|
||||
static let splitNavCloseSecondaryButton = 42006
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue