Re-add undo scroll-to-top to timelines/profiles

This commit is contained in:
Shadowfacts 2022-11-01 20:49:07 -04:00
parent 6a5753fac8
commit 658c08010d
19 changed files with 251 additions and 57 deletions

View File

@ -95,6 +95,9 @@
D63661C02381C144004B9E16 /* PreferencesNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D63661BF2381C144004B9E16 /* PreferencesNavigationController.swift */; };
D6370B9C24421FF30092A7FF /* Tusker.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = D6370B9A24421FF30092A7FF /* Tusker.xcdatamodeld */; };
D63CC702290EC0B8000E19DE /* Sentry in Frameworks */ = {isa = PBXBuildFile; productRef = D63CC701290EC0B8000E19DE /* Sentry */; };
D63CC70C2910AADB000E19DE /* TuskerSceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D63CC70B2910AADB000E19DE /* TuskerSceneDelegate.swift */; };
D63CC7102911F1E4000E19DE /* UIScrollView+Top.swift in Sources */ = {isa = PBXBuildFile; fileRef = D63CC70F2911F1E4000E19DE /* UIScrollView+Top.swift */; };
D63CC7122911F57C000E19DE /* StatusBarTappableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D63CC7112911F57C000E19DE /* StatusBarTappableViewController.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 */; };
@ -447,6 +450,9 @@
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>"; };
D63CC703290EC472000E19DE /* Dist.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Dist.xcconfig; sourceTree = "<group>"; };
D63CC70B2910AADB000E19DE /* TuskerSceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TuskerSceneDelegate.swift; sourceTree = "<group>"; };
D63CC70F2911F1E4000E19DE /* UIScrollView+Top.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIScrollView+Top.swift"; sourceTree = "<group>"; };
D63CC7112911F57C000E19DE /* StatusBarTappableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusBarTappableViewController.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>"; };
@ -876,6 +882,17 @@
path = CoreData;
sourceTree = "<group>";
};
D63CC70A2910AAC6000E19DE /* Scenes */ = {
isa = PBXGroup;
children = (
D68C2AE225869BAB00548EFF /* AuxiliarySceneDelegate.swift */,
D69693F925859A8000F4E116 /* ComposeSceneDelegate.swift */,
D6AC956623C4347E008C9946 /* MainSceneDelegate.swift */,
D63CC70B2910AADB000E19DE /* TuskerSceneDelegate.swift */,
);
path = Scenes;
sourceTree = "<group>";
};
D641C780213DD7C4004B4513 /* Screens */ = {
isa = PBXGroup;
children = (
@ -1140,6 +1157,7 @@
D69693F32585941A00F4E116 /* UIWindowSceneDelegate+Close.swift */,
D62E9984279CA23900C26176 /* URLSession+Development.swift */,
D6ADB6ED28EA74E8009924AB /* UIView+Configure.swift */,
D63CC70F2911F1E4000E19DE /* UIScrollView+Top.swift */,
);
path = Extensions;
sourceTree = "<group>";
@ -1317,6 +1335,7 @@
D6E0DC8D216EDF1E00369478 /* Previewing.swift */,
D6B81F3B2560365300F6E31D /* RefreshableViewController.swift */,
D68FEC4E232C5BC300C84F23 /* SegmentedPageViewController.swift */,
D63CC7112911F57C000E19DE /* StatusBarTappableViewController.swift */,
D6A6C10425B6138A00298D0F /* StatusTablePrefetching.swift */,
D6412B0224AFF6A600F5412E /* TabBarScrollableViewController.swift */,
D6B22A0E2560D52D004D82EF /* TabbedPageViewController.swift */,
@ -1372,14 +1391,11 @@
D6D4DDDB212518A200E1C4BB /* Info.plist */,
D6D4DDCF212518A000E1C4BB /* AppDelegate.swift */,
D6CA6A91249FAD8900AD45C1 /* AudioSessionHelper.swift */,
D68C2AE225869BAB00548EFF /* AuxiliarySceneDelegate.swift */,
D69693F925859A8000F4E116 /* ComposeSceneDelegate.swift */,
D6E4269C2532A3E100C02E1C /* FuzzyMatcher.swift */,
D6B30E08254BAF63009CAEE5 /* ImageGrayscalifier.swift */,
D60E2F2B24423EAD005F8713 /* LazilyDecoding.swift */,
D64D0AAC2128D88B005A6F37 /* LocalData.swift */,
D61DC84528F498F200B82C6E /* Logging.swift */,
D6AC956623C4347E008C9946 /* MainSceneDelegate.swift */,
D6B81F432560390300F6E31D /* MenuController.swift */,
D64D8CA82463B494006B0BAA /* MultiThreadDictionary.swift */,
D6945C2E23AC47C3005C403C /* SavedDataManager.swift */,
@ -1397,6 +1413,7 @@
D6E57FA525C26FAB00341037 /* Localizable.stringsdict */,
D61959D2241E846D00A37B8E /* Models */,
D663626021360A9600C9CBA2 /* Preferences */,
D63CC70A2910AAC6000E19DE /* Scenes */,
D641C780213DD7C4004B4513 /* Screens */,
D62D241E217AA46B005076CC /* Shortcuts */,
D67B506B250B28FF00FAECFB /* Vendor */,
@ -1782,6 +1799,7 @@
0450531F22B0097E00100BA2 /* Timline+UI.swift in Sources */,
D667E5F52135BCD50057A976 /* ConversationTableViewController.swift in Sources */,
D6C7D27D22B6EBF800071952 /* AttachmentsContainerView.swift in Sources */,
D63CC7122911F57C000E19DE /* StatusBarTappableViewController.swift in Sources */,
D620483823D38190008A63EF /* StatusContentTextView.swift in Sources */,
D6D3FDE224F46A8D00FF50A5 /* ComposeUIState.swift in Sources */,
D6B22A0F2560D52D004D82EF /* TabbedPageViewController.swift in Sources */,
@ -1812,6 +1830,7 @@
D62D2422217AA7E1005076CC /* UserActivityManager.swift in Sources */,
D6CA6A92249FAD8900AD45C1 /* AudioSessionHelper.swift in Sources */,
D60D2B8223844C71001B87A3 /* BaseStatusTableViewCell.swift in Sources */,
D63CC70C2910AADB000E19DE /* TuskerSceneDelegate.swift in Sources */,
D6B9366F2828452F00237D0E /* SavedHashtag.swift in Sources */,
D6B9366D2828445000237D0E /* SavedInstance.swift in Sources */,
D60E2F272442372B005F8713 /* StatusMO.swift in Sources */,
@ -1875,6 +1894,7 @@
D6EE63FB2551F7F60065485C /* StatusCollapseButton.swift in Sources */,
D6B936712829F72900237D0E /* NSManagedObjectContext+Helpers.swift in Sources */,
D627944A23A6AD6100D38C68 /* BookmarksTableViewController.swift in Sources */,
D63CC7102911F1E4000E19DE /* UIScrollView+Top.swift in Sources */,
D64BC19223C271D9000D0238 /* MastodonActivity.swift in Sources */,
D6945C3A23AC75E2005C403C /* FindInstanceViewController.swift in Sources */,
D68E6F59253C9969001A1B4C /* MultiSourceEmojiLabel.swift in Sources */,

View File

@ -18,8 +18,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
configureSentry()
swizzleStatusBar()
AppShortcutItem.createItems(for: application)
@ -129,5 +129,28 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
}
UIApplication.shared.requestSceneSessionDestruction(scene.session, options: nil)
}
private func swizzleStatusBar() {
let selector = Selector(("handleTapAction:"))
var originalIMP: IMP?
let imp = imp_implementationWithBlock({ (self: UIStatusBarManager, sender: AnyObject) in
let original = unsafeBitCast(originalIMP!, to: (@convention(c) (UIStatusBarManager, Selector, AnyObject) -> Void).self)
guard let windowScene = self.perform(Selector(("windowScene"))).takeUnretainedValue() as? UIWindowScene,
let xPosition = sender.value(forKey: "xPosition") as? CGFloat,
let delegate = windowScene.delegate as? TuskerSceneDelegate else {
original(self, selector, sender)
return
}
switch delegate.handleStatusBarTapped(xPosition: xPosition) {
case .stop:
return
case .continue:
original(self, selector, sender)
}
} as @convention(block) (UIStatusBarManager, AnyObject) -> Void)
originalIMP = class_replaceMethod(UIStatusBarManager.self, selector, imp, "v@:@")
if originalIMP == nil {
Logging.general.error("Unable to swizzle status bar manager")
}
}
}

View File

@ -0,0 +1,45 @@
//
// UIScrollView+Top.swift
// Tusker
//
// Created by Shadowfacts on 11/1/22.
// Copyright © 2022 Shadowfacts. All rights reserved.
//
import UIKit
private var prevScrollOffsetBeforeScrollToTopKey: Void = ()
extension UIScrollView {
private var prevScrollOffsetBeforeScrollToTop: CGFloat? {
get {
if let v = (objc_getAssociatedObject(self, &prevScrollOffsetBeforeScrollToTopKey) as? NSNumber)?.doubleValue {
return CGFloat(v)
} else {
return nil
}
}
set {
if let newValue {
objc_setAssociatedObject(self, &prevScrollOffsetBeforeScrollToTopKey, NSNumber(value: newValue), .OBJC_ASSOCIATION_COPY_NONATOMIC)
} else {
objc_setAssociatedObject(self, &prevScrollOffsetBeforeScrollToTopKey, nil, .OBJC_ASSOCIATION_COPY_NONATOMIC)
}
}
}
func scrollToTop() {
let top = -adjustedContentInset.top
// +5 to add a little bit of wiggle room
let isScrolledToTop = contentOffset.y < top + 5
if isScrolledToTop {
if let prevScrollOffsetBeforeScrollToTop {
self.prevScrollOffsetBeforeScrollToTop = nil
setContentOffset(CGPoint(x: 0, y: prevScrollOffsetBeforeScrollToTop), animated: true)
}
} else {
prevScrollOffsetBeforeScrollToTop = contentOffset.y
setContentOffset(CGPoint(x: 0, y: top), animated: true)
}
}
}

View File

@ -11,7 +11,7 @@ import Pachyderm
import MessageUI
import CoreData
class MainSceneDelegate: UIResponder, UIWindowSceneDelegate {
class MainSceneDelegate: UIResponder, UIWindowSceneDelegate, TuskerSceneDelegate {
var window: UIWindow?

View File

@ -0,0 +1,30 @@
//
// TuskerSceneDelegate.swift
// Tusker
//
// Created by Shadowfacts on 10/31/22.
// Copyright © 2022 Shadowfacts. All rights reserved.
//
import UIKit
protocol TuskerSceneDelegate: UISceneDelegate {
var rootViewController: TuskerRootViewController? { get }
func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult
}
enum StatusBarTapActionResult {
case `continue`
case stop
}
extension TuskerSceneDelegate {
func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult {
if let rootViewController {
let converted = rootViewController.view.convert(CGPoint(x: xPosition, y: 0), from: nil)
return rootViewController.handleStatusBarTapped(xPosition: converted.x)
}
return .continue
}
}

View File

@ -111,6 +111,12 @@ extension AccountSwitchingContainerViewController: TuskerRootViewController {
loadViewIfNeeded()
root.presentPreferences(completion: completion)
}
func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult {
loadViewIfNeeded()
// TODO: check if fast account switcher is being presented?
return root.handleStatusBarTapped(xPosition: xPosition)
}
}
extension AccountSwitchingContainerViewController: BackgroundableViewController {

View File

@ -456,6 +456,22 @@ extension MainSplitViewController: TuskerRootViewController {
func presentPreferences(completion: (() -> Void)?) {
present(PreferencesNavigationController(mastodonController: mastodonController), animated: true, completion: completion)
}
func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult {
guard presentedViewController == nil else {
return .stop
}
if traitCollection.horizontalSizeClass == .compact {
return tabBarViewController.handleStatusBarTapped(xPosition: xPosition)
} else {
let pointInSecondary = secondaryNavController.view.convert(CGPoint(x: xPosition, y: 0), from: view)
if secondaryNavController.view.bounds.contains(pointInSecondary) {
return secondaryNavController.handleStatusBarTapped(xPosition: pointInSecondary.x)
} else {
return .continue
}
}
}
}
extension MainSplitViewController: BackgroundableViewController {

View File

@ -285,6 +285,16 @@ extension MainTabBarViewController: TuskerRootViewController {
func presentPreferences(completion: (() -> Void)?) {
present(PreferencesNavigationController(mastodonController: mastodonController), animated: true, completion: completion)
}
func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult {
guard presentedViewController == nil else {
return .stop
}
guard let vc = viewController(for: selectedTab) as? StatusBarTappableViewController else {
return .continue
}
return vc.handleStatusBarTapped(xPosition: xPosition)
}
}
extension MainTabBarViewController: BackgroundableViewController {

View File

@ -8,7 +8,7 @@
import UIKit
protocol TuskerRootViewController: UIViewController {
protocol TuskerRootViewController: UIViewController, StatusBarTappableViewController {
func presentCompose()
func select(tab: MainTabBarViewController.Tab)
func getTabController(tab: MainTabBarViewController.Tab) -> UIViewController?

View File

@ -470,3 +470,16 @@ extension ProfileStatusesViewController: StatusCollectionViewCellDelegate {
}
}
}
extension ProfileStatusesViewController: TabBarScrollableViewController {
func tabBarScrollToTop() {
collectionView.scrollToTop()
}
}
extension ProfileStatusesViewController: StatusBarTappableViewController {
func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult {
collectionView.scrollToTop()
return .stop
}
}

View File

@ -307,3 +307,15 @@ extension ProfileViewController: TabbedPageViewController {
selectPage(at: currentIndex - 1, animated: true)
}
}
extension ProfileViewController: TabBarScrollableViewController {
func tabBarScrollToTop() {
currentViewController.tabBarScrollToTop()
}
}
extension ProfileViewController: StatusBarTappableViewController {
func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult {
return currentViewController.handleStatusBarTapped(xPosition: xPosition)
}
}

View File

@ -10,8 +10,6 @@ import UIKit
import Pachyderm
import Combine
// TODO: gonna need a thing to replicate all of EnhancedTableViewController
class TimelineViewController: UIViewController, TimelineLikeCollectionViewController, RefreshableViewController {
let timeline: Timeline
weak var mastodonController: MastodonController!
@ -413,3 +411,16 @@ extension TimelineViewController: StatusCollectionViewCellDelegate {
}
}
}
extension TimelineViewController: TabBarScrollableViewController {
func tabBarScrollToTop() {
collectionView.scrollToTop()
}
}
extension TimelineViewController: StatusBarTappableViewController {
func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult {
collectionView.scrollToTop()
return .stop
}
}

View File

@ -248,3 +248,12 @@ extension EnhancedNavigationViewController: BackgroundableViewController {
}
}
}
extension EnhancedNavigationViewController: StatusBarTappableViewController {
func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult {
if let topVC = topViewController as? StatusBarTappableViewController {
return topVC.handleStatusBarTapped(xPosition: xPosition)
}
return .continue
}
}

View File

@ -11,11 +11,6 @@ import SafariServices
class EnhancedTableViewController: UITableViewController {
private var prevScrollToTopOffset: CGPoint? = nil
private(set) var isCurrentlyScrollingToTop = false
private var prevScrollViewContentOffset: CGPoint?
private(set) var scrollViewDirection: CGFloat = 0
var dragEnabled = false
override func viewDidLoad() {
@ -26,38 +21,6 @@ class EnhancedTableViewController: UITableViewController {
}
}
// MARK: Scroll View Delegate
override func scrollViewShouldScrollToTop(_ scrollView: UIScrollView) -> Bool {
if let offset = prevScrollToTopOffset {
tableView.setContentOffset(offset, animated: true)
prevScrollToTopOffset = nil
return false
} else {
prevScrollToTopOffset = tableView.contentOffset
isCurrentlyScrollingToTop = true
return true
}
}
override func scrollViewDidScrollToTop(_ scrollView: UIScrollView) {
isCurrentlyScrollingToTop = false
// add one so it's not technically scrolled all the way to the top,
// otherwise there's no way of detecting a status bar press to scroll back down
tableView.contentOffset.y -= 0.5
}
override func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
prevScrollToTopOffset = nil
}
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
if let prev = prevScrollViewContentOffset {
scrollViewDirection = scrollView.contentOffset.y - prev.y
}
prevScrollViewContentOffset = scrollView.contentOffset
}
// MARK: Table View Delegate
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
@ -117,10 +80,13 @@ extension EnhancedTableViewController: UITableViewDragDelegate {
extension EnhancedTableViewController: TabBarScrollableViewController {
func tabBarScrollToTop() {
if scrollViewShouldScrollToTop(tableView) {
let topOffset = CGPoint(x: 0, y: -tableView.adjustedContentInset.top)
tableView.setContentOffset(topOffset, animated: true)
scrollViewDidScrollToTop(tableView)
}
tableView.scrollToTop()
}
}
extension EnhancedTableViewController: StatusBarTappableViewController {
func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult {
tableView.scrollToTop()
return .stop
}
}

View File

@ -105,3 +105,12 @@ extension SegmentedPageViewController: BackgroundableViewController {
}
}
}
extension SegmentedPageViewController: StatusBarTappableViewController {
func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult {
if let current = pageControllers[currentIndex] as? StatusBarTappableViewController {
return current.handleStatusBarTapped(xPosition: xPosition)
}
return .continue
}
}

View File

@ -135,12 +135,6 @@ class SplitNavigationController: UIViewController {
updateSecondaryNavVisibility()
}
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
}
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
@ -245,7 +239,24 @@ class SplitNavigationController: UIViewController {
self.updateSecondaryNavVisibility()
}
}
}
extension SplitNavigationController: StatusBarTappableViewController {
func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult {
let vcs = viewControllers
if !canShowSecondaryNav || vcs.count < 2 {
return (vcs.first! as? StatusBarTappableViewController)?.handleStatusBarTapped(xPosition: xPosition) ?? .continue
} else {
let positionInRoot = rootNav.view.convert(CGPoint(x: xPosition, y: 0), from: view)
let positionInSecondary = secondaryNav.view.convert(CGPoint(x: xPosition, y: 0), from: view)
if rootNav.view.bounds.contains(positionInRoot) {
return (rootNav.topViewController as? StatusBarTappableViewController)?.handleStatusBarTapped(xPosition: positionInRoot.x) ?? .continue
} else if secondaryNav.view.bounds.contains(positionInSecondary) {
return (secondaryNav.topViewController as? StatusBarTappableViewController)?.handleStatusBarTapped(xPosition: positionInRoot.x) ?? .continue
}
}
return .continue
}
}
private class SplitRootNavigationController: UINavigationController {

View File

@ -0,0 +1,13 @@
//
// StatusBarTappableViewController.swift
// Tusker
//
// Created by Shadowfacts on 11/1/22.
// Copyright © 2022 Shadowfacts. All rights reserved.
//
import UIKit
protocol StatusBarTappableViewController: UIViewController {
func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult
}