diff --git a/Tusker.xcodeproj/project.pbxproj b/Tusker.xcodeproj/project.pbxproj index d378c250..612fbfcf 100644 --- a/Tusker.xcodeproj/project.pbxproj +++ b/Tusker.xcodeproj/project.pbxproj @@ -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 = ""; }; D6370B9B24421FF30092A7FF /* Tusker.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Tusker.xcdatamodel; sourceTree = ""; }; D63A8D0A2561C27F00D9DFFF /* ProfileStatusesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileStatusesViewController.swift; sourceTree = ""; }; + D63D8DF32850FE7A008D95E1 /* ViewTags.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewTags.swift; sourceTree = ""; }; D63F9C6D241D2D85004C03CF /* CompositionAttachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompositionAttachment.swift; sourceTree = ""; }; D6403CC124A6B72D00E81C55 /* VisualEffectImageButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VisualEffectImageButton.swift; sourceTree = ""; }; D640D76822BAF5E6004FBE69 /* DomainBlocks.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = DomainBlocks.plist; sourceTree = ""; }; @@ -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 */, diff --git a/Tusker/Screens/Compose/ComposeHostingController.swift b/Tusker/Screens/Compose/ComposeHostingController.swift index 81e2a148..602e4c69 100644 --- a/Tusker/Screens/Compose/ComposeHostingController.swift +++ b/Tusker/Screens/Compose/ComposeHostingController.swift @@ -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 { weak var delegate: ComposeHostingControllerDelegate? @@ -135,12 +132,12 @@ class ComposeHostingController: UIHostingController { 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 { 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 { 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 { diff --git a/Tusker/Screens/Main/MainSplitViewController.swift b/Tusker/Screens/Main/MainSplitViewController.swift index d38b8f4a..5ae30e54 100644 --- a/Tusker/Screens/Main/MainSplitViewController.swift +++ b/Tusker/Screens/Main/MainSplitViewController.swift @@ -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 { diff --git a/Tusker/Screens/Main/MainTabBarViewController.swift b/Tusker/Screens/Main/MainTabBarViewController.swift index b8ac7645..8319dfc4 100644 --- a/Tusker/Screens/Main/MainTabBarViewController.swift +++ b/Tusker/Screens/Main/MainTabBarViewController.swift @@ -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 } } diff --git a/Tusker/Screens/Utilities/EnhancedNavigationViewController.swift b/Tusker/Screens/Utilities/EnhancedNavigationViewController.swift index bc23b2da..b20de711 100644 --- a/Tusker/Screens/Utilities/EnhancedNavigationViewController.swift +++ b/Tusker/Screens/Utilities/EnhancedNavigationViewController.swift @@ -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(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() + } } diff --git a/Tusker/Screens/Utilities/SegmentedPageViewController.swift b/Tusker/Screens/Utilities/SegmentedPageViewController.swift index 787c6fd3..d7550782 100644 --- a/Tusker/Screens/Utilities/SegmentedPageViewController.swift +++ b/Tusker/Screens/Utilities/SegmentedPageViewController.swift @@ -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) diff --git a/Tusker/ViewTags.swift b/Tusker/ViewTags.swift new file mode 100644 index 00000000..e5c54735 --- /dev/null +++ b/Tusker/ViewTags.swift @@ -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 +}