feat: add scroll position record and shortcut bar
This commit is contained in:
parent
25d6515c98
commit
3bc1a3de39
|
@ -573,6 +573,8 @@
|
|||
DBE54AC62636C89F004E7C0B /* NotificationPreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBE54AC52636C89F004E7C0B /* NotificationPreference.swift */; };
|
||||
DBE54ACC2636C8FD004E7C0B /* NotificationPreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBE54AC52636C89F004E7C0B /* NotificationPreference.swift */; };
|
||||
DBF156DF2701B17600EC00B7 /* SidebarAddAccountCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBF156DE2701B17600EC00B7 /* SidebarAddAccountCollectionViewCell.swift */; };
|
||||
DBF156E22702DA6900EC00B7 /* UIStatusBarManager+HandleTapAction.m in Sources */ = {isa = PBXBuildFile; fileRef = DBF156E12702DA6900EC00B7 /* UIStatusBarManager+HandleTapAction.m */; };
|
||||
DBF156E42702DB3F00EC00B7 /* HandleTapAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBF156E32702DB3F00EC00B7 /* HandleTapAction.swift */; };
|
||||
DBF1D24E269DAF5D00C1C08A /* SearchDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBF1D24D269DAF5D00C1C08A /* SearchDetailViewController.swift */; };
|
||||
DBF1D251269DB01200C1C08A /* SearchHistoryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBF1D250269DB01200C1C08A /* SearchHistoryViewController.swift */; };
|
||||
DBF1D257269DBAC600C1C08A /* SearchDetailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBF1D256269DBAC600C1C08A /* SearchDetailViewModel.swift */; };
|
||||
|
@ -1355,6 +1357,9 @@
|
|||
DBE54AC52636C89F004E7C0B /* NotificationPreference.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationPreference.swift; sourceTree = "<group>"; };
|
||||
DBF156DD27006F5D00EC00B7 /* CoreData 2.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "CoreData 2.xcdatamodel"; sourceTree = "<group>"; };
|
||||
DBF156DE2701B17600EC00B7 /* SidebarAddAccountCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarAddAccountCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||
DBF156E02702DA6800EC00B7 /* Mastodon-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Mastodon-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||
DBF156E12702DA6900EC00B7 /* UIStatusBarManager+HandleTapAction.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "UIStatusBarManager+HandleTapAction.m"; sourceTree = "<group>"; };
|
||||
DBF156E32702DB3F00EC00B7 /* HandleTapAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HandleTapAction.swift; sourceTree = "<group>"; };
|
||||
DBF1D24D269DAF5D00C1C08A /* SearchDetailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchDetailViewController.swift; sourceTree = "<group>"; };
|
||||
DBF1D250269DB01200C1C08A /* SearchHistoryViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchHistoryViewController.swift; sourceTree = "<group>"; };
|
||||
DBF1D256269DBAC600C1C08A /* SearchDetailViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchDetailViewModel.swift; sourceTree = "<group>"; };
|
||||
|
@ -1737,6 +1742,9 @@
|
|||
DB6180EC26391C6C0018D199 /* TransitioningMath.swift */,
|
||||
DB75BF1D263C1C1B00EDBF1F /* CustomScheduler.swift */,
|
||||
DBAC649A267DF8C8007FE9FD /* ActivityIndicatorNode.swift */,
|
||||
DBF156E32702DB3F00EC00B7 /* HandleTapAction.swift */,
|
||||
DBF156E12702DA6900EC00B7 /* UIStatusBarManager+HandleTapAction.m */,
|
||||
DBF156E02702DA6800EC00B7 /* Mastodon-Bridging-Header.h */,
|
||||
);
|
||||
path = Vender;
|
||||
sourceTree = "<group>";
|
||||
|
@ -3437,7 +3445,7 @@
|
|||
TargetAttributes = {
|
||||
DB427DD125BAA00100D1B89D = {
|
||||
CreatedOnToolsVersion = 12.4;
|
||||
LastSwiftMigration = 1220;
|
||||
LastSwiftMigration = 1300;
|
||||
};
|
||||
DB427DE725BAA00100D1B89D = {
|
||||
CreatedOnToolsVersion = 12.4;
|
||||
|
@ -4067,6 +4075,7 @@
|
|||
DB3667A1268ABB2E0027D07F /* ComposeStatusAttachmentItem.swift in Sources */,
|
||||
DB1D186C25EF5BA7003F1F23 /* PollTableView.swift in Sources */,
|
||||
DBA94434265CBB5300C537E1 /* ProfileFieldSection.swift in Sources */,
|
||||
DBF156E42702DB3F00EC00B7 /* HandleTapAction.swift in Sources */,
|
||||
DB023295267F0AB800031745 /* ASMetaEditableTextNode.swift in Sources */,
|
||||
2D5981A125E4A593000FB903 /* MastodonConfirmEmailViewModel.swift in Sources */,
|
||||
DB4F096C269EFA2000D62E92 /* SearchResultViewController+StatusProvider.swift in Sources */,
|
||||
|
@ -4114,6 +4123,7 @@
|
|||
2DAC9E46262FC9FD0062E1A6 /* SuggestionAccountTableViewCell.swift in Sources */,
|
||||
DB4FFC2C269EC39600D62E92 /* SearchTransitionController.swift in Sources */,
|
||||
DBA5E7A9263BD3A4004598BB /* ContextMenuImagePreviewViewController.swift in Sources */,
|
||||
DBF156E22702DA6900EC00B7 /* UIStatusBarManager+HandleTapAction.m in Sources */,
|
||||
DB45FADD25CA6F6B005A8AC7 /* APIService+CoreData+MastodonUser.swift in Sources */,
|
||||
2D32EABA25CB9B0500C9ED86 /* UIView.swift in Sources */,
|
||||
2D38F20825CD491300561493 /* DisposeBagCollectable.swift in Sources */,
|
||||
|
@ -4758,6 +4768,7 @@
|
|||
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Mastodon/Vender/Mastodon-Bridging-Header.h";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
|
@ -4786,6 +4797,7 @@
|
|||
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Mastodon/Vender/Mastodon-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
|
@ -5316,6 +5328,7 @@
|
|||
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Mastodon/Vender/Mastodon-Bridging-Header.h";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
|
@ -5552,6 +5565,7 @@
|
|||
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Mastodon/Vender/Mastodon-Bridging-Header.h";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
|
|
|
@ -18,7 +18,7 @@ import MastodonUI
|
|||
final class ComposeViewController: UIViewController, NeedsDependency {
|
||||
|
||||
static let minAutoCompleteVisibleHeight: CGFloat = 100
|
||||
|
||||
|
||||
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
|
||||
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
|
||||
|
||||
|
@ -137,6 +137,16 @@ extension ComposeViewController {
|
|||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
let groups = [UIBarButtonItemGroup(barButtonItems: [
|
||||
composeToolbarView.mediaBarButtonItem,
|
||||
composeToolbarView.pollBarButtonItem,
|
||||
composeToolbarView.contentWarningBarButtonItem,
|
||||
composeToolbarView.visibilityBarButtonItem,
|
||||
], representativeItem: nil)]
|
||||
|
||||
tableView.inputAssistantItem.trailingBarButtonGroups = groups
|
||||
textEditorView()?.textView.inputAssistantItem.trailingBarButtonGroups = groups
|
||||
|
||||
viewModel.title
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] title in
|
||||
|
@ -330,13 +340,21 @@ extension ComposeViewController {
|
|||
// bind media button toolbar state
|
||||
viewModel.isMediaToolbarButtonEnabled
|
||||
.receive(on: DispatchQueue.main)
|
||||
.assign(to: \.isEnabled, on: composeToolbarView.mediaButton)
|
||||
.sink { [weak self] isMediaToolbarButtonEnabled in
|
||||
guard let self = self else { return }
|
||||
// self.composeToolbarView.mediaBarButtonItem.isEnabled = isMediaToolbarButtonEnabled
|
||||
self.composeToolbarView.mediaButton.isEnabled = isMediaToolbarButtonEnabled
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
// bind poll button toolbar state
|
||||
viewModel.isPollToolbarButtonEnabled
|
||||
.receive(on: DispatchQueue.main)
|
||||
.assign(to: \.isEnabled, on: composeToolbarView.pollButton)
|
||||
.sink { [weak self] isPollToolbarButtonEnabled in
|
||||
guard let self = self else { return }
|
||||
// self.composeToolbarView.pollBarButtonItem.isEnabled = isPollToolbarButtonEnabled
|
||||
self.composeToolbarView.pollButton.isEnabled = isPollToolbarButtonEnabled
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
Publishers.CombineLatest(
|
||||
|
@ -347,10 +365,14 @@ extension ComposeViewController {
|
|||
.sink { [weak self] isPollComposing, isPollToolbarButtonEnabled in
|
||||
guard let self = self else { return }
|
||||
guard isPollToolbarButtonEnabled else {
|
||||
self.composeToolbarView.pollButton.accessibilityLabel = L10n.Scene.Compose.Accessibility.appendPoll
|
||||
let accessibilityLabel = L10n.Scene.Compose.Accessibility.appendPoll
|
||||
// self.composeToolbarView.pollBarButtonItem.accessibilityLabel = accessibilityLabel
|
||||
self.composeToolbarView.pollButton.accessibilityLabel = accessibilityLabel
|
||||
return
|
||||
}
|
||||
self.composeToolbarView.pollButton.accessibilityLabel = isPollComposing ? L10n.Scene.Compose.Accessibility.removePoll : L10n.Scene.Compose.Accessibility.appendPoll
|
||||
let accessibilityLabel = isPollComposing ? L10n.Scene.Compose.Accessibility.removePoll : L10n.Scene.Compose.Accessibility.appendPoll
|
||||
// self.composeToolbarView.pollBarButtonItem.accessibilityLabel = accessibilityLabel
|
||||
self.composeToolbarView.pollButton.accessibilityLabel = accessibilityLabel
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
|
|
|
@ -27,6 +27,41 @@ final class ComposeToolbarView: UIView {
|
|||
|
||||
weak var delegate: ComposeToolbarViewDelegate?
|
||||
|
||||
// barButtonItem
|
||||
let mediaBarButton: UIButton = {
|
||||
let button = UIButton()
|
||||
button.setImage(UIImage(systemName: "photo"), for: .normal)
|
||||
return button
|
||||
}()
|
||||
private(set) lazy var mediaBarButtonItem: UIBarButtonItem = {
|
||||
let barButtonItem = UIBarButtonItem(customView: mediaBarButton)
|
||||
barButtonItem.accessibilityLabel = L10n.Scene.Compose.Accessibility.appendAttachment
|
||||
return barButtonItem
|
||||
}()
|
||||
|
||||
let pollBarButtonItem: UIBarButtonItem = {
|
||||
let barButtonItem = UIBarButtonItem()
|
||||
barButtonItem.image = UIImage(systemName: "list.bullet")
|
||||
barButtonItem.accessibilityLabel = L10n.Scene.Compose.Accessibility.appendPoll
|
||||
return barButtonItem
|
||||
}()
|
||||
|
||||
let contentWarningBarButtonItem: UIBarButtonItem = {
|
||||
let barButtonItem = UIBarButtonItem()
|
||||
barButtonItem.image = UIImage(systemName: "exclamationmark.shield")
|
||||
barButtonItem.accessibilityLabel = L10n.Scene.Compose.Accessibility.enableContentWarning
|
||||
return barButtonItem
|
||||
}()
|
||||
|
||||
let visibilityBarButtonItem: UIBarButtonItem = {
|
||||
let barButtonItem = UIBarButtonItem()
|
||||
barButtonItem.image = UIImage(systemName: "person.3")
|
||||
barButtonItem.accessibilityLabel = L10n.Scene.Compose.Accessibility.postVisibilityMenu
|
||||
return barButtonItem
|
||||
}()
|
||||
|
||||
// button
|
||||
|
||||
let mediaButton: UIButton = {
|
||||
let button = HighlightDimmableButton()
|
||||
ComposeToolbarView.configureToolbarButtonAppearance(button: button)
|
||||
|
|
|
@ -17,6 +17,8 @@ import AlamofireImage
|
|||
|
||||
final class HomeTimelineViewController: UIViewController, NeedsDependency, MediaPreviewableViewController {
|
||||
|
||||
let logger = Logger(subsystem: "HomeTimelineViewController", category: "UI")
|
||||
|
||||
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
|
||||
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
|
||||
|
||||
|
@ -218,6 +220,31 @@ extension HomeTimelineViewController {
|
|||
}
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
NotificationCenter.default
|
||||
.publisher(for: .statusBarTapped, object: nil)
|
||||
.throttle(for: 0.5, scheduler: DispatchQueue.main, latest: false)
|
||||
.sink { [weak self] notification in
|
||||
guard let self = self else { return }
|
||||
guard let _ = self.view.window else { return } // displaying
|
||||
|
||||
// https://developer.limneos.net/index.php?ios=13.1.3&framework=UIKitCore.framework&header=UIStatusBarTapAction.h
|
||||
guard let action = notification.object as AnyObject?,
|
||||
let xPosition = action.value(forKey: "xPosition") as? Double
|
||||
else { return }
|
||||
|
||||
let viewFrameInWindow = self.view.convert(self.view.frame, to: nil)
|
||||
guard xPosition >= viewFrameInWindow.minX && xPosition <= viewFrameInWindow.maxX else { return }
|
||||
|
||||
// works on iOS 14
|
||||
self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): receive notification \(xPosition)")
|
||||
|
||||
// check if scroll to top
|
||||
guard self.shouldRestoreScrollPosition() else { return }
|
||||
self.restorePositionWhenScrollToTop()
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
|
@ -234,9 +261,15 @@ extension HomeTimelineViewController {
|
|||
|
||||
viewModel.viewDidAppear.send()
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 2) { [weak self] in
|
||||
guard let self = self else { return }
|
||||
// always try to refresh timeline after appear
|
||||
if let timestamp = viewModel.lastAutomaticFetchTimestamp.value {
|
||||
let now = Date()
|
||||
if now.timeIntervalSince(timestamp) > 60 {
|
||||
self.viewModel.lastAutomaticFetchTimestamp.value = now
|
||||
self.viewModel.homeTimelineNeedRefresh.send()
|
||||
} else {
|
||||
// do nothing
|
||||
}
|
||||
} else {
|
||||
self.viewModel.homeTimelineNeedRefresh.send()
|
||||
}
|
||||
}
|
||||
|
@ -394,9 +427,62 @@ extension HomeTimelineViewController: TableViewCellHeightCacheableContainer {
|
|||
// MARK: - UIScrollViewDelegate
|
||||
extension HomeTimelineViewController {
|
||||
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
switch scrollView {
|
||||
case tableView:
|
||||
aspectScrollViewDidScroll(scrollView)
|
||||
viewModel.homeTimelineNavigationBarTitleViewModel.handleScrollViewDidScroll(scrollView)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
func scrollViewShouldScrollToTop(_ scrollView: UIScrollView) -> Bool {
|
||||
switch scrollView {
|
||||
case tableView:
|
||||
// handle scrollToTop
|
||||
savePositionBeforeScrollToTop()
|
||||
return true
|
||||
default:
|
||||
assertionFailure()
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
private func savePositionBeforeScrollToTop() {
|
||||
guard let diffableDataSource = viewModel.diffableDataSource else { return }
|
||||
guard let anchorIndexPaths = tableView.indexPathsForVisibleRows?.sorted() else { return }
|
||||
guard !anchorIndexPaths.isEmpty else { return }
|
||||
let anchorIndexPath = anchorIndexPaths[anchorIndexPaths.count / 2]
|
||||
guard let anchorItem = diffableDataSource.itemIdentifier(for: anchorIndexPath) else { return }
|
||||
|
||||
aspectScrollViewDidScroll(scrollView)
|
||||
viewModel.homeTimelineNavigationBarTitleViewModel.handleScrollViewDidScroll(scrollView)
|
||||
let offset: CGFloat = {
|
||||
guard let anchorCell = tableView.cellForRow(at: anchorIndexPath) else { return 0 }
|
||||
let cellFrameInView = tableView.convert(anchorCell.frame, to: view)
|
||||
return cellFrameInView.origin.y
|
||||
}()
|
||||
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): save position record for \(anchorIndexPath) with offset: \(offset)")
|
||||
viewModel.scrollPositionRecord.value = HomeTimelineViewModel.ScrollPositionRecord(
|
||||
item: anchorItem,
|
||||
offset: offset,
|
||||
timestamp: Date()
|
||||
)
|
||||
}
|
||||
|
||||
private func shouldRestoreScrollPosition() -> Bool {
|
||||
// check if scroll to top
|
||||
guard self.tableView.safeAreaInsets.top > 0 else { return false }
|
||||
let zeroOffset = -self.tableView.safeAreaInsets.top
|
||||
return abs(self.tableView.contentOffset.y - zeroOffset) < 2.0
|
||||
}
|
||||
|
||||
private func restorePositionWhenScrollToTop() {
|
||||
guard let diffableDataSource = self.viewModel.diffableDataSource else { return }
|
||||
guard let record = self.viewModel.scrollPositionRecord.value,
|
||||
let indexPath = diffableDataSource.indexPath(for: record.item)
|
||||
else { return }
|
||||
|
||||
self.tableView.scrollToRow(at: indexPath, at: .middle, animated: true)
|
||||
self.viewModel.scrollPositionRecord.value = nil
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -544,6 +630,8 @@ extension HomeTimelineViewController: ScrollViewContainer {
|
|||
} else {
|
||||
let indexPath = IndexPath(row: 0, section: 0)
|
||||
guard viewModel.diffableDataSource?.itemIdentifier(for: indexPath) != nil else { return }
|
||||
// save position
|
||||
savePositionBeforeScrollToTop()
|
||||
tableView.scrollToRow(at: indexPath, at: .top, animated: true)
|
||||
}
|
||||
}
|
||||
|
@ -572,7 +660,12 @@ extension HomeTimelineViewController: StatusTableViewCellDelegate {
|
|||
// MARK: - HomeTimelineNavigationBarTitleViewDelegate
|
||||
extension HomeTimelineViewController: HomeTimelineNavigationBarTitleViewDelegate {
|
||||
func homeTimelineNavigationBarTitleView(_ titleView: HomeTimelineNavigationBarTitleView, logoButtonDidPressed sender: UIButton) {
|
||||
scrollToTop(animated: true)
|
||||
if shouldRestoreScrollPosition() {
|
||||
restorePositionWhenScrollToTop()
|
||||
} else {
|
||||
savePositionBeforeScrollToTop()
|
||||
scrollToTop(animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
func homeTimelineNavigationBarTitleView(_ titleView: HomeTimelineNavigationBarTitleView, buttonDidPressed sender: UIButton) {
|
||||
|
|
|
@ -28,6 +28,8 @@ final class HomeTimelineViewModel: NSObject {
|
|||
let isFetchingLatestTimeline = CurrentValueSubject<Bool, Never>(false)
|
||||
let viewDidAppear = PassthroughSubject<Void, Never>()
|
||||
let homeTimelineNavigationBarTitleViewModel: HomeTimelineNavigationBarTitleViewModel
|
||||
let lastAutomaticFetchTimestamp = CurrentValueSubject<Date?, Never>(nil)
|
||||
let scrollPositionRecord = CurrentValueSubject<ScrollPositionRecord?, Never>(nil)
|
||||
|
||||
weak var contentOffsetAdjustableTimelineViewControllerDelegate: ContentOffsetAdjustableTimelineViewControllerDelegate?
|
||||
weak var tableView: UITableView?
|
||||
|
@ -153,3 +155,12 @@ final class HomeTimelineViewModel: NSObject {
|
|||
}
|
||||
|
||||
extension HomeTimelineViewModel: SuggestionAccountViewModelDelegate { }
|
||||
|
||||
|
||||
extension HomeTimelineViewModel {
|
||||
struct ScrollPositionRecord {
|
||||
let item: Item
|
||||
let offset: CGFloat
|
||||
let timestamp: Date
|
||||
}
|
||||
}
|
||||
|
|
|
@ -155,19 +155,3 @@ extension SceneDelegate {
|
|||
return true
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
class TestWindow: UIWindow {
|
||||
|
||||
override func sendEvent(_ event: UIEvent) {
|
||||
event.allTouches?.forEach({ (touch) in
|
||||
let location = touch.location(in: self)
|
||||
let view = hitTest(location, with: event)
|
||||
print(view.debugDescription)
|
||||
})
|
||||
|
||||
super.sendEvent(event)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
//
|
||||
// HandleTapAction.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by Cirno MainasuK on 2021-9-28.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
@objc class HandleTapAction: NSObject {
|
||||
@objc static let statusBarTappedNotification = Notification(name: .statusBarTapped)
|
||||
}
|
||||
|
||||
extension Notification.Name {
|
||||
static let statusBarTapped = Notification.Name(rawValue: "org.joinmastodon.app.statusBarTapped")
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
//
|
||||
// Use this file to import your target's public headers that you would like to expose to Swift.
|
||||
//
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
#import <Foundation/Foundation.h>
|
||||
#import <UIKit/UIKit.h>
|
||||
#import <objc/message.h>
|
||||
#import <objc/runtime.h>
|
||||
|
||||
@implementation UIStatusBarManager (CAPHandleTapAction)
|
||||
|
||||
+ (void)load {
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
Class class = [self class];
|
||||
SEL originalSelector = NSSelectorFromString(@"handleTapAction:");
|
||||
SEL swizzledSelector = @selector(custom_handleTapAction:);
|
||||
|
||||
Method originalMethod = class_getInstanceMethod(self, originalSelector);
|
||||
Method swizzledMethod = class_getInstanceMethod(self, swizzledSelector);
|
||||
|
||||
BOOL didAddMethod = class_addMethod(class,
|
||||
originalSelector,
|
||||
method_getImplementation(swizzledMethod),
|
||||
method_getTypeEncoding(swizzledMethod));
|
||||
if (didAddMethod) {
|
||||
class_replaceMethod(class,
|
||||
swizzledSelector,
|
||||
method_getImplementation(originalMethod),
|
||||
method_getTypeEncoding(originalMethod));
|
||||
} else {
|
||||
method_exchangeImplementations(originalMethod, swizzledMethod);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
-(void)custom_handleTapAction:(id)sender {
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:@"org.joinmastodon.app.statusBarTapped" object:sender];
|
||||
[self custom_handleTapAction:sender];
|
||||
}
|
||||
|
||||
@end
|
Loading…
Reference in New Issue