Use browser-style navigation bars on iPad

This commit is contained in:
Shadowfacts 2022-06-09 23:33:31 -04:00
parent f702df2f15
commit 08b7cf013b
7 changed files with 200 additions and 26 deletions

View File

@ -88,6 +88,7 @@
D63661C02381C144004B9E16 /* PreferencesNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D63661BF2381C144004B9E16 /* PreferencesNavigationController.swift */; };
D6370B9C24421FF30092A7FF /* Tusker.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = D6370B9A24421FF30092A7FF /* Tusker.xcdatamodeld */; };
D63A8D0B2561C27F00D9DFFF /* ProfileStatusesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D63A8D0A2561C27F00D9DFFF /* ProfileStatusesViewController.swift */; };
D63D8DF42850FE7A008D95E1 /* ViewTags.swift in Sources */ = {isa = PBXBuildFile; fileRef = D63D8DF32850FE7A008D95E1 /* ViewTags.swift */; };
D63F9C6E241D2D85004C03CF /* CompositionAttachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D63F9C6D241D2D85004C03CF /* CompositionAttachment.swift */; };
D6403CC224A6B72D00E81C55 /* VisualEffectImageButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6403CC124A6B72D00E81C55 /* VisualEffectImageButton.swift */; };
D640D76922BAF5E6004FBE69 /* DomainBlocks.plist in Resources */ = {isa = PBXBuildFile; fileRef = D640D76822BAF5E6004FBE69 /* DomainBlocks.plist */; };
@ -434,6 +435,7 @@
D63661BF2381C144004B9E16 /* PreferencesNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesNavigationController.swift; sourceTree = "<group>"; };
D6370B9B24421FF30092A7FF /* Tusker.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Tusker.xcdatamodel; sourceTree = "<group>"; };
D63A8D0A2561C27F00D9DFFF /* ProfileStatusesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileStatusesViewController.swift; sourceTree = "<group>"; };
D63D8DF32850FE7A008D95E1 /* ViewTags.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewTags.swift; sourceTree = "<group>"; };
D63F9C6D241D2D85004C03CF /* CompositionAttachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompositionAttachment.swift; sourceTree = "<group>"; };
D6403CC124A6B72D00E81C55 /* VisualEffectImageButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VisualEffectImageButton.swift; sourceTree = "<group>"; };
D640D76822BAF5E6004FBE69 /* DomainBlocks.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = DomainBlocks.plist; sourceTree = "<group>"; };
@ -1372,6 +1374,7 @@
D6C693EE216192C2007D6A6D /* TuskerNavigationDelegate.swift */,
D6DFC69F242C4CCC00ACC392 /* WeakArray.swift */,
D62E9988279DB2D100C26176 /* InstanceFeatures.swift */,
D63D8DF32850FE7A008D95E1 /* ViewTags.swift */,
D6D4DDD6212518A200E1C4BB /* Assets.xcassets */,
D6AEBB3F2321640F00E5038B /* Activities */,
D6F1F84E2193B9BE00F5FE67 /* Caching */,
@ -1821,6 +1824,7 @@
D681E4D5246E2BC30053414F /* UnmuteConversationActivity.swift in Sources */,
D6B4A4FF2506B81A000C81C1 /* AccountDisplayNameLabel.swift in Sources */,
D6C143E025354E34007DC240 /* EmojiPickerCollectionViewController.swift in Sources */,
D63D8DF42850FE7A008D95E1 /* ViewTags.swift in Sources */,
D6B053A623BD2D0C00A066FA /* AssetCollectionViewController.swift in Sources */,
D67B506D250B291200FAECFB /* BlurHashDecode.swift in Sources */,
D62275A024F1677200B82A16 /* ComposeHostingController.swift in Sources */,

View File

@ -15,9 +15,6 @@ protocol ComposeHostingControllerDelegate: AnyObject {
func dismissCompose(mode: ComposeUIState.DismissMode) -> Bool
}
private let VISIBILITY_BAR_BUTTON_TAG = 42001
private let LOCAL_ONLY_BAR_BUTTON_TAG = 42002
class ComposeHostingController: UIHostingController<ComposeContainerView> {
weak var delegate: ComposeHostingControllerDelegate?
@ -135,12 +132,12 @@ class ComposeHostingController: UIHostingController<ComposeContainerView> {
items.append(UIBarButtonItem(title: "CW", style: .plain, target: self, action: #selector(cwButtonPressed)))
let visibilityItem = UIBarButtonItem(image: nil, style: .plain, target: nil, action: nil)
visibilityItem.tag = VISIBILITY_BAR_BUTTON_TAG
visibilityItem.tag = ViewTags.composeVisibilityBarButton
items.append(visibilityItem)
if mastodonController.instanceFeatures.localOnlyPosts {
let item = UIBarButtonItem(image: nil, style: .plain, target: nil, action: nil)
item.tag = LOCAL_ONLY_BAR_BUTTON_TAG
item.tag = ViewTags.composeLocalOnlyBarButton
items.append(item)
localOnlyChanged(draft.localOnly)
}
@ -243,7 +240,7 @@ class ComposeHostingController: UIHostingController<ComposeContainerView> {
private func visibilityChanged(_ newVisibility: Status.Visibility) {
for toolbar in [mainToolbar, inputAccessoryToolbar] {
guard let item = toolbar?.items?.first(where: { $0.tag == VISIBILITY_BAR_BUTTON_TAG }) else {
guard let item = toolbar?.items?.first(where: { $0.tag == ViewTags.composeVisibilityBarButton }) else {
continue
}
item.image = UIImage(systemName: newVisibility.imageName)
@ -260,7 +257,7 @@ class ComposeHostingController: UIHostingController<ComposeContainerView> {
private func localOnlyChanged(_ localOnly: Bool) {
for toolbar in [mainToolbar, inputAccessoryToolbar] {
guard let item = toolbar?.items?.first(where: { $0.tag == LOCAL_ONLY_BAR_BUTTON_TAG }) else {
guard let item = toolbar?.items?.first(where: { $0.tag == ViewTags.composeLocalOnlyBarButton }) else {
continue
}
if localOnly {

View File

@ -46,7 +46,9 @@ class MainSplitViewController: UISplitViewController {
setViewController(sidebar, for: .primary)
primaryBackgroundStyle = .sidebar
setViewController(EnhancedNavigationViewController(), for: .secondary)
let secondaryNav = EnhancedNavigationViewController()
secondaryNav.useBrowserStyleNavigation = true
setViewController(secondaryNav, 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 {

View File

@ -141,7 +141,9 @@ class MainTabBarViewController: UITabBarController, UITabBarControllerDelegate {
if let vc = vc as? UINavigationController {
return vc
} else {
return EnhancedNavigationViewController(rootViewController: vc)
let nav = EnhancedNavigationViewController(rootViewController: vc)
nav.useBrowserStyleNavigation = true
return nav
}
}

View File

@ -9,43 +9,69 @@
import UIKit
class EnhancedNavigationViewController: UINavigationController {
var useBrowserStyleNavigation = false
var poppedViewControllers = [UIViewController]()
var skipResetPoppedOnNextPush = false
private var interactivePushTransition: InteractivePushTransition!
override var viewControllers: [UIViewController] {
didSet {
poppedViewControllers = []
for vc in viewControllers {
configureNavItem(vc.navigationItem)
}
updateTopNavItemState()
}
}
override func viewDidLoad() {
super.viewDidLoad()
self.interactivePushTransition = InteractivePushTransition(navigationController: self)
if let topViewController {
configureNavItem(topViewController.navigationItem)
updateTopNavItemState()
}
}
override func popViewController(animated: Bool) -> UIViewController? {
if let popped = super.popViewController(animated: animated) {
let popped = performAfterAnimating(block: {
super.popViewController(animated: animated)
}, after: {
self.updateTopNavItemState()
}, animated: animated)
if let popped {
poppedViewControllers.insert(popped, at: 0)
return popped
} else {
return nil
}
return popped
}
override func popToRootViewController(animated: Bool) -> [UIViewController]? {
if let popped = super.popToRootViewController(animated: animated) {
let popped = performAfterAnimating(block: {
super.popToRootViewController(animated: animated)
}, after: {
self.updateTopNavItemState()
}, animated: animated)
if let popped {
poppedViewControllers = popped
return popped
} else {
return nil
}
return popped
}
override func popToViewController(_ viewController: UIViewController, animated: Bool) -> [UIViewController]? {
if let popped = super.popToViewController(viewController, animated: animated) {
let popped = performAfterAnimating(block: {
super.popToViewController(viewController, animated: animated)
}, after: {
self.updateTopNavItemState()
}, animated: animated)
if let popped {
poppedViewControllers.insert(contentsOf: popped, at: 0)
return popped
} else {
return nil
}
return popped
}
override func pushViewController(_ viewController: UIViewController, animated: Bool) {
@ -54,7 +80,43 @@ class EnhancedNavigationViewController: UINavigationController {
} else {
self.poppedViewControllers = []
}
configureNavItem(viewController.navigationItem)
super.pushViewController(viewController, animated: animated)
updateTopNavItemState()
}
func pushPoppedViewController() {
guard !poppedViewControllers.isEmpty else {
return
}
skipResetPoppedOnNextPush = true
pushViewController(poppedViewControllers.removeFirst(), animated: true)
}
func pushToPoppedViewController(_ target: UIViewController) {
guard poppedViewControllers.contains(target) else {
return
}
var toInsert: [UIViewController] = []
while true {
let vc = poppedViewControllers.removeFirst()
if vc == target {
break
} else {
toInsert.append(vc)
}
}
// match the system behavior when popping multiple by animated-ly pushing the final destination one,
// and then intersiting the intermediary ones before it, as if they'd all been pushed together
performAfterAnimating(block: {
pushViewController(target, animated: true)
}, after: {
self.viewControllers.insert(contentsOf: toInsert, at: self.viewControllers.count - 1)
self.updateTopNavItemState()
}, animated: true)
}
func onWillShow() {
@ -72,6 +134,92 @@ class EnhancedNavigationViewController: UINavigationController {
}
})
}
private func configureNavItem(_ navItem: UINavigationItem) {
guard useBrowserStyleNavigation,
UIDevice.current.userInterfaceIdiom != .phone else {
return
}
navItem.style = .browser
navItem.hidesBackButton = true
if let titleView = navItem.titleView,
titleView.tag != ViewTags.navEmptyTitleView {
// blergh, i don't like changing this out from under some other view controller
// we use an empty view because otherwise the title label displays in addition to the new title view bar button item
navItem.titleView = UIView()
navItem.titleView?.tag = ViewTags.navEmptyTitleView
// TODO: centerItemGroups don't animate out during nav transitions, the just (dis)appear abruptly
navItem.centerItemGroups = [
.fixedGroup(items: [UIBarButtonItem(customView: titleView)])
]
}
let back = UIBarButtonItem(image: UIImage(systemName: "chevron.backward"), style: .plain, target: self, action: #selector(goBack))
back.tag = ViewTags.navBackBarButton
back.menu = UIMenu(children: [
UIDeferredMenuElement({ [unowned self] completion in
completion(self.viewControllers.dropLast(1).reversed().map { vc in
UIAction(title: vc.navigationItem.title ?? "Back") { [weak self] _ in
_ = self?.popToViewController(vc, animated: true)
}
})
})
])
let forward = UIBarButtonItem(image: UIImage(systemName: "chevron.forward"), style: .plain, target: self, action: #selector(goForward))
forward.tag = ViewTags.navForwardBarButton
forward.menu = UIMenu(children: [
UIDeferredMenuElement.uncached({ [unowned self] completion in
completion(poppedViewControllers.map { vc in
UIAction(title: vc.navigationItem.title ?? "Forward") { [weak self] _ in
self?.pushToPoppedViewController(vc)
}
})
})
])
navItem.leadingItemGroups = [
.fixedGroup(items: [
back,
forward,
])
]
}
private func updateTopNavItemState() {
guard useBrowserStyleNavigation,
UIDevice.current.userInterfaceIdiom != .phone,
let vc = topViewController,
let group = vc.navigationItem.leadingItemGroups.first,
group.barButtonItems.count == 2,
group.barButtonItems[0].tag == ViewTags.navBackBarButton,
group.barButtonItems[1].tag == ViewTags.navForwardBarButton else {
return
}
group.barButtonItems[0].isEnabled = viewControllers.count > 1
group.barButtonItems[1].isEnabled = !poppedViewControllers.isEmpty
}
private func performAfterAnimating<R>(block: () -> R, after: @escaping () -> Void, animated: Bool) -> R {
if animated {
CATransaction.begin()
let result = block()
CATransaction.setCompletionBlock {
after()
}
CATransaction.commit()
return result
} else {
let result = block()
after()
return result
}
}
@objc private func goBack() {
_ = popViewController(animated: true)
}
@objc private func goForward() {
pushPoppedViewController()
}
}

View File

@ -24,6 +24,12 @@ class SegmentedPageViewController: UIPageViewController, UIPageViewControllerDel
super.init(transitionStyle: .scroll, navigationOrientation: .horizontal, options: nil)
self.delegate = self
// this needs to happen in init because EnhancedNavigationViewController expects to be able to look at the titleView
// before the view has necessarily loaded
segmentedControl = UISegmentedControl(items: titles)
segmentedControl.addTarget(self, action: #selector(segmentedControlChanged), for: .valueChanged)
navigationItem.titleView = segmentedControl
}
required init?(coder: NSCoder) {
@ -35,10 +41,6 @@ class SegmentedPageViewController: UIPageViewController, UIPageViewControllerDel
view.backgroundColor = .systemBackground
segmentedControl = UISegmentedControl(items: titles)
segmentedControl.addTarget(self, action: #selector(segmentedControlChanged), for: .valueChanged)
navigationItem.titleView = segmentedControl
segmentedControl.selectedSegmentIndex = 0
selectPage(at: 0, animated: false)

19
Tusker/ViewTags.swift Normal file
View File

@ -0,0 +1,19 @@
//
// ViewTags.swift
// Tusker
//
// Created by Shadowfacts on 6/8/22.
// Copyright © 2022 Shadowfacts. All rights reserved.
//
import Foundation
struct ViewTags {
private init() {}
static let composeVisibilityBarButton = 42001
static let composeLocalOnlyBarButton = 42002
static let navBackBarButton = 42003
static let navForwardBarButton = 42004
static let navEmptyTitleView = 42005
}