diff --git a/NetNewsWire.xcodeproj/project.pbxproj b/NetNewsWire.xcodeproj/project.pbxproj index 4515b4e74..9661c637f 100644 --- a/NetNewsWire.xcodeproj/project.pbxproj +++ b/NetNewsWire.xcodeproj/project.pbxproj @@ -463,6 +463,7 @@ 51FE10092346739D0056195D /* ActivityType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51D87EE02311D34700E63F03 /* ActivityType.swift */; }; 51FE100A234673A00056195D /* ActivityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51934CCD2310792F006127BE /* ActivityManager.swift */; }; 51FFF0C4235EE8E5002762AA /* VibrantButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51FFF0C3235EE8E5002762AA /* VibrantButton.swift */; }; + 540F420A2745A63B00F639C1 /* PullUpToMarkAsReadTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 540F42092745A63B00F639C1 /* PullUpToMarkAsReadTableViewController.swift */; }; 55E15BCB229D65A900D6602A /* AccountsReaderAPI.xib in Resources */ = {isa = PBXBuildFile; fileRef = 55E15BC1229D65A900D6602A /* AccountsReaderAPI.xib */; }; 55E15BCC229D65A900D6602A /* AccountsReaderAPIWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55E15BCA229D65A900D6602A /* AccountsReaderAPIWindowController.swift */; }; 5F323809231DF9F000706F6B /* VibrantTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F323808231DF9F000706F6B /* VibrantTableViewCell.swift */; }; @@ -1390,6 +1391,7 @@ 51FD413A2342BD0500880194 /* MasterTimelineUnreadCountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MasterTimelineUnreadCountView.swift; sourceTree = ""; }; 51FE10022345529D0056195D /* UserNotificationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserNotificationManager.swift; sourceTree = ""; }; 51FFF0C3235EE8E5002762AA /* VibrantButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VibrantButton.swift; sourceTree = ""; }; + 540F42092745A63B00F639C1 /* PullUpToMarkAsReadTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PullUpToMarkAsReadTableViewController.swift; sourceTree = ""; }; 55E15BC1229D65A900D6602A /* AccountsReaderAPI.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = AccountsReaderAPI.xib; sourceTree = ""; }; 55E15BCA229D65A900D6602A /* AccountsReaderAPIWindowController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountsReaderAPIWindowController.swift; sourceTree = ""; }; 5F323808231DF9F000706F6B /* VibrantTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VibrantTableViewCell.swift; sourceTree = ""; }; @@ -2105,6 +2107,7 @@ children = ( 51C4526E2265091600C03939 /* MasterTimelineViewController.swift */, 51D6A5BB23199C85001C27D8 /* MasterTimelineDataSource.swift */, + 540F42092745A63B00F639C1 /* PullUpToMarkAsReadTableViewController.swift */, 5148F4542336DB7000F8CD8B /* MasterTimelineTitleView.swift */, 5148F44A2336DB4700F8CD8B /* MasterTimelineTitleView.xib */, 51FD413A2342BD0500880194 /* MasterTimelineUnreadCountView.swift */, @@ -4173,6 +4176,7 @@ 512AF9C2236ED52C0066F8BE /* ImageHeaderView.swift in Sources */, 512392C124E33A3C00F11704 /* RedditSelectTypeTableViewController.swift in Sources */, 515A5181243E90260089E588 /* ExtensionPointIdentifer.swift in Sources */, + 540F420A2745A63B00F639C1 /* PullUpToMarkAsReadTableViewController.swift in Sources */, 51A1699F235E10D700EB091F /* AboutViewController.swift in Sources */, 51C45290226509C100C03939 /* PseudoFeed.swift in Sources */, 512392C624E3451400F11704 /* TwitterSelectAccountTableViewController.swift in Sources */, diff --git a/iOS/MasterTimeline/MasterTimelineViewController.swift b/iOS/MasterTimeline/MasterTimelineViewController.swift index 5e2212b0b..c91011cb1 100644 --- a/iOS/MasterTimeline/MasterTimelineViewController.swift +++ b/iOS/MasterTimeline/MasterTimelineViewController.swift @@ -11,7 +11,7 @@ import RSCore import Account import Articles -class MasterTimelineViewController: UITableViewController, UndoableCommandRunner { +class MasterTimelineViewController: PullUpToMarkAsReadTableViewController, UndoableCommandRunner { private var numberOfTextLines = 0 private var iconSize = IconSize.medium @@ -418,6 +418,8 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner } override func scrollViewDidScroll(_ scrollView: UIScrollView) { + super.scrollViewDidScroll(scrollView) + if scrollView.isTracking { scrollPositionQueue.add(self, #selector(scrollPositionDidChange)) } diff --git a/iOS/MasterTimeline/PullUpToMarkAsReadTableViewController.swift b/iOS/MasterTimeline/PullUpToMarkAsReadTableViewController.swift new file mode 100644 index 000000000..a31d202ce --- /dev/null +++ b/iOS/MasterTimeline/PullUpToMarkAsReadTableViewController.swift @@ -0,0 +1,149 @@ +// +// PullUpToMarkAsReadTableViewController.swift +// NetNewsWire-iOS +// +// Created by Rob Everhardt on 17-11-2021. +// Copyright © 2021 Ranchero Software. All rights reserved. +// + +import UIKit + + +class PullUpToMarkAsReadTableViewController: UITableViewController { + private let textPull = "Pull up to mark as read" + private let textRelease = "Release to mark as read" + private let textMarkingAsRead = "Marking as read..." + private let pullUpToMarkAsReadFooterHeight = 52.0 + + public var isMarkingAsRead = false + public var isDragging = false + + private var textLabel: UILabel? + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + addPullUpToMarkAsReadFooter() + } + + func addPullUpToMarkAsReadFooter() { + let markAsReadFooterView = UIView.init(frame: CGRect( + x: 0.0, + y: view.frame.size.height - view.safeAreaInsets.bottom - pullUpToMarkAsReadFooterHeight, + width: view.frame.size.width, + height: pullUpToMarkAsReadFooterHeight + )) + + markAsReadFooterView.backgroundColor = UIColor.clear + + let label = UILabel.init(frame: CGRect( + x: 0.0, + y: 0.0, + width: view.frame.size.width, + height: pullUpToMarkAsReadFooterHeight + )) + label.backgroundColor = UIColor.clear + label.font = UIFont.boldSystemFont(ofSize: 12) + label.textAlignment = NSTextAlignment.center + + textLabel = label + markAsReadFooterView.addSubview(label) + +// self.tableView.addSubview(markAsReadFooterView) + let backgroundView = UIView() + backgroundView.addSubview(markAsReadFooterView) + self.tableView.backgroundView = backgroundView + } + + override func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { + if (isMarkingAsRead) { + return + } + isDragging = true + } + + override func scrollViewDidScroll(_ scrollView: UIScrollView) { + guard let label = textLabel else { + return + } + let visibleHeight = view.frame.size.height - + view.safeAreaInsets.top + - view.safeAreaInsets.bottom + let currentOffset = scrollView.contentOffset.y + view.safeAreaInsets.top + let scrolledOutOfViewWhenBottomIsReached = max(scrollView.contentSize.height - visibleHeight,0) + let pullUpVisibleHeight = max(currentOffset - scrolledOutOfViewWhenBottomIsReached,0) + + if (isMarkingAsRead) { + // Update the content inset, good for section headers + if (pullUpVisibleHeight < pullUpToMarkAsReadFooterHeight) { + self.tableView.contentInset = UIEdgeInsets.init( + top: 0, + left: 0, + bottom: pullUpVisibleHeight, + right: 0 + ); + } + } else if (isDragging && scrollView.contentOffset.y > 0) { + // Update the label + UIView.animate(withDuration: 0.25) { + if (pullUpVisibleHeight > self.pullUpToMarkAsReadFooterHeight) { + // User is scrolling above the header + label.text = self.textRelease; + } else { + // User is scrolling somewhere within the header + label.text = self.textPull; + } + } + } + } + + override func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { + if (isMarkingAsRead) { + return + } + isDragging = false + + let visibleHeight = view.frame.size.height - + view.safeAreaInsets.top + - view.safeAreaInsets.bottom + let currentOffset = scrollView.contentOffset.y + view.safeAreaInsets.top + let scrolledOutOfViewWhenBottomIsReached = max(scrollView.contentSize.height - visibleHeight,0) + let pullUpVisibleHeight = max(currentOffset - scrolledOutOfViewWhenBottomIsReached,0) + + if (pullUpVisibleHeight > pullUpToMarkAsReadFooterHeight) { + startMarkingAsRead() + } + } + + func startMarkingAsRead() { + isMarkingAsRead = true + UIView.animate(withDuration: 0.25) { + self.tableView.contentInset = UIEdgeInsets.init( + top: 0, + left: 0, + bottom: self.pullUpToMarkAsReadFooterHeight, + right: 0 + ); + if let label = self.textLabel { + label.text = self.textMarkingAsRead + } + } + + markAsRead() + } + + func stopMarkingAsRead() { + isMarkingAsRead = false + if let label = textLabel { + label.text = textPull + } + self.tableView.contentInset = UIEdgeInsets.zero + } + + func markAsRead() { + // to override by implementation, which should also call stopMarkingAsRead when done + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + self.stopMarkingAsRead() + } + } +}