mirror of
https://github.com/Ranchero-Software/NetNewsWire
synced 2025-08-12 06:26:36 +00:00
Change how we display progress per #3566.
This commit is contained in:
@@ -7,6 +7,7 @@
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import SwiftUI
|
||||
import Account
|
||||
import Articles
|
||||
import RSCore
|
||||
@@ -16,13 +17,15 @@ import SafariServices
|
||||
class MasterFeedViewController: UITableViewController, UndoableCommandRunner, MainControllerIdentifiable {
|
||||
|
||||
@IBOutlet weak var filterButton: UIBarButtonItem!
|
||||
private var refreshProgressView: RefreshProgressView?
|
||||
@IBOutlet weak var addNewItemButton: UIBarButtonItem! {
|
||||
didSet {
|
||||
addNewItemButton.primaryAction = nil
|
||||
}
|
||||
}
|
||||
|
||||
let refreshProgressModel = RefreshProgressModel()
|
||||
lazy var progressBarViewController = UIHostingController(rootView: RefreshProgressView(progressBarMode: refreshProgressModel))
|
||||
|
||||
var mainControllerIdentifer = MainControllerIdentifier.masterFeed
|
||||
|
||||
weak var coordinator: SceneCoordinator!
|
||||
@@ -75,8 +78,12 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner, Ma
|
||||
|
||||
refreshControl = UIRefreshControl()
|
||||
refreshControl!.addTarget(self, action: #selector(refreshAccounts(_:)), for: .valueChanged)
|
||||
refreshControl!.tintColor = .clear
|
||||
|
||||
progressBarViewController.view.backgroundColor = .clear
|
||||
let refreshProgressItemButton = UIBarButtonItem(customView: progressBarViewController.view)
|
||||
toolbarItems?.insert(refreshProgressItemButton, at: 2)
|
||||
|
||||
configureToolbar()
|
||||
becomeFirstResponder()
|
||||
}
|
||||
|
||||
@@ -595,7 +602,7 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner, Ma
|
||||
} else {
|
||||
setFilterButtonToInactive()
|
||||
}
|
||||
refreshProgressView?.update()
|
||||
refreshProgressModel.update()
|
||||
addNewItemButton?.isEnabled = !AccountManager.shared.activeAccounts.isEmpty
|
||||
|
||||
configureContextMenu()
|
||||
@@ -728,16 +735,6 @@ extension MasterFeedViewController: MasterFeedTableViewCellDelegate {
|
||||
|
||||
private extension MasterFeedViewController {
|
||||
|
||||
func configureToolbar() {
|
||||
guard let refreshProgressView = Bundle.main.loadNibNamed("RefreshProgressView", owner: self, options: nil)?[0] as? RefreshProgressView else {
|
||||
return
|
||||
}
|
||||
|
||||
self.refreshProgressView = refreshProgressView
|
||||
let refreshProgressItemButton = UIBarButtonItem(customView: refreshProgressView)
|
||||
toolbarItems?.insert(refreshProgressItemButton, at: 2)
|
||||
}
|
||||
|
||||
func setFilterButtonToActive() {
|
||||
filterButton?.image = AppAssets.filterActiveImage
|
||||
filterButton?.accLabelText = NSLocalizedString("Selected - Filter Read Feeds", comment: "Selected - Filter Read Feeds")
|
||||
|
||||
@@ -1,27 +1,80 @@
|
||||
//
|
||||
// RefeshProgressView.swift
|
||||
// NetNewsWire-iOS
|
||||
// ProgressBarView.swift
|
||||
// NetNewsWire
|
||||
//
|
||||
// Created by Maurice Parker on 10/24/19.
|
||||
// Copyright © 2019 Ranchero Software. All rights reserved.
|
||||
// Created by Maurice Parker on 11/11/22.
|
||||
// Copyright © 2022 Ranchero Software. All rights reserved.
|
||||
//
|
||||
// IndetermineProgressView inspired by https://daringsnowball.net/articles/indeterminate-linear-progress-view/
|
||||
|
||||
import UIKit
|
||||
import SwiftUI
|
||||
import Account
|
||||
|
||||
class RefreshProgressView: UIView {
|
||||
struct RefreshProgressView: View {
|
||||
|
||||
@IBOutlet weak var progressView: UIProgressView!
|
||||
@IBOutlet weak var label: UILabel!
|
||||
static let width: CGFloat = 100
|
||||
static let height: CGFloat = 5
|
||||
|
||||
override func awakeFromNib() {
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(progressDidChange(_:)), name: .AccountRefreshProgressDidChange, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(contentSizeCategoryDidChange(_:)), name: UIContentSizeCategory.didChangeNotification, object: nil)
|
||||
update()
|
||||
scheduleUpdateRefreshLabel()
|
||||
@ObservedObject var refreshProgressModel: RefreshProgressModel
|
||||
@State private var offset: CGFloat = 0
|
||||
|
||||
isAccessibilityElement = true
|
||||
accessibilityTraits = [.updatesFrequently, .notEnabled]
|
||||
init(progressBarMode: RefreshProgressModel) {
|
||||
self.refreshProgressModel = progressBarMode
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
if refreshProgressModel.isRefreshing {
|
||||
if refreshProgressModel.isIndeterminate {
|
||||
indeterminateProgressView
|
||||
} else {
|
||||
ProgressView(value: refreshProgressModel.progress)
|
||||
.progressViewStyle(LinearProgressViewStyle())
|
||||
.frame(width: Self.width, height: Self.height)
|
||||
}
|
||||
} else {
|
||||
Text(refreshProgressModel.label)
|
||||
.accessibilityLabel(refreshProgressModel.label)
|
||||
.font(.footnote)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
.frame(width: 200, height: 44)
|
||||
}
|
||||
|
||||
var indeterminateProgressView: some View {
|
||||
Rectangle()
|
||||
.foregroundColor(.gray.opacity(0.15))
|
||||
.overlay(
|
||||
Rectangle()
|
||||
.foregroundColor(Color.accentColor)
|
||||
.frame(width: Self.width * 0.26, height: Self.height)
|
||||
.clipShape(Capsule())
|
||||
.offset(x: -Self.width * 0.6, y: 0)
|
||||
.offset(x: Self.width * 1.2 * self.offset, y: 0)
|
||||
.animation(.default.repeatForever().speed(0.265), value: self.offset)
|
||||
.onAppear{
|
||||
withAnimation {
|
||||
self.offset = 1
|
||||
}
|
||||
}
|
||||
)
|
||||
.clipShape(Capsule())
|
||||
.animation(.default, value: refreshProgressModel.isRefreshing)
|
||||
.frame(width: Self.width, height: Self.height)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class RefreshProgressModel: ObservableObject {
|
||||
|
||||
@Published var isRefreshing = false
|
||||
@Published var isIndeterminate = false
|
||||
@Published var progress = 0.0
|
||||
@Published var label = String()
|
||||
|
||||
init() {
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(progressDidChange(_:)), name: .AccountRefreshProgressDidChange, object: nil)
|
||||
}
|
||||
|
||||
func update() {
|
||||
@@ -31,52 +84,32 @@ class RefreshProgressView: UIView {
|
||||
updateRefreshLabel()
|
||||
}
|
||||
}
|
||||
|
||||
override func didMoveToSuperview() {
|
||||
progressChanged(animated: false)
|
||||
}
|
||||
|
||||
|
||||
@objc func progressDidChange(_ note: Notification) {
|
||||
progressChanged(animated: true)
|
||||
}
|
||||
|
||||
@objc func contentSizeCategoryDidChange(_ note: Notification) {
|
||||
// This hack is probably necessary because custom views in the toolbar don't get
|
||||
// notifications that the content size changed.
|
||||
label.font = UIFont.preferredFont(forTextStyle: .footnote)
|
||||
}
|
||||
|
||||
|
||||
deinit {
|
||||
NotificationCenter.default.removeObserver(self)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: Private
|
||||
|
||||
private extension RefreshProgressView {
|
||||
|
||||
private extension RefreshProgressModel {
|
||||
|
||||
func progressChanged(animated: Bool) {
|
||||
// Layout may crash if not in the view hierarchy.
|
||||
// https://github.com/Ranchero-Software/NetNewsWire/issues/1764
|
||||
let isInViewHierarchy = self.superview != nil
|
||||
|
||||
let progress = AccountManager.shared.combinedRefreshProgress
|
||||
|
||||
if progress.isComplete {
|
||||
if isInViewHierarchy {
|
||||
progressView.setProgress(1, animated: animated)
|
||||
}
|
||||
let combinedRefreshProgress = AccountManager.shared.combinedRefreshProgress
|
||||
isIndeterminate = combinedRefreshProgress.isIndeterminate
|
||||
|
||||
if combinedRefreshProgress.isComplete {
|
||||
isRefreshing = false
|
||||
progress = 1
|
||||
|
||||
func completeLabel() {
|
||||
// Check that there are no pending downloads.
|
||||
if AccountManager.shared.combinedRefreshProgress.isComplete {
|
||||
self.updateRefreshLabel()
|
||||
self.label.isHidden = false
|
||||
self.progressView.isHidden = true
|
||||
if self.superview != nil {
|
||||
self.progressView.setProgress(0, animated: animated)
|
||||
}
|
||||
updateRefreshLabel()
|
||||
progress = 0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,19 +121,16 @@ private extension RefreshProgressView {
|
||||
completeLabel()
|
||||
}
|
||||
} else {
|
||||
label.isHidden = true
|
||||
progressView.isHidden = false
|
||||
if isInViewHierarchy {
|
||||
let percent = Float(progress.numberCompleted) / Float(progress.numberOfTasks)
|
||||
isRefreshing = true
|
||||
let percent = Double(combinedRefreshProgress.numberCompleted) / Double(combinedRefreshProgress.numberOfTasks)
|
||||
|
||||
// Don't let the progress bar go backwards unless we need to go back more than 25%
|
||||
if percent > progressView.progress || progressView.progress - percent > 0.25 {
|
||||
progressView.setProgress(percent, animated: animated)
|
||||
}
|
||||
// Don't let the progress bar go backwards unless we need to go back more than 25%
|
||||
if percent > progress || (progress - percent) > 0.25 {
|
||||
progress = percent
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func updateRefreshLabel() {
|
||||
if let accountLastArticleFetchEndTime = AccountManager.shared.lastArticleFetchEndTime {
|
||||
|
||||
@@ -111,17 +141,15 @@ private extension RefreshProgressView {
|
||||
let refreshed = relativeDateTimeFormatter.localizedString(for: accountLastArticleFetchEndTime, relativeTo: Date())
|
||||
let localizedRefreshText = NSLocalizedString("Updated %@", comment: "Updated")
|
||||
let refreshText = NSString.localizedStringWithFormat(localizedRefreshText as NSString, refreshed) as String
|
||||
label.text = refreshText
|
||||
label = refreshText
|
||||
|
||||
} else {
|
||||
label.text = NSLocalizedString("Updated Just Now", comment: "Updated Just Now")
|
||||
label = NSLocalizedString("Updated Just Now", comment: "Updated Just Now")
|
||||
}
|
||||
|
||||
} else {
|
||||
label.text = ""
|
||||
label = ""
|
||||
}
|
||||
|
||||
accessibilityLabel = label.text
|
||||
}
|
||||
|
||||
func scheduleUpdateRefreshLabel() {
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="17156" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17125"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
||||
<view contentMode="scaleToFill" id="ejl-zC-eNy" customClass="RefreshProgressView" customModule="NetNewsWire" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="0.0" width="461" height="90"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<progressView opaque="NO" contentMode="scaleToFill" verticalHuggingPriority="750" progress="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="Ds3-59-ooT" customClass="RoundedProgressView" customModule="NetNewsWire" customModuleProvider="target">
|
||||
<rect key="frame" x="180.5" y="42.5" width="100" height="5"/>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="100" id="ReS-sT-7EN"/>
|
||||
<constraint firstAttribute="height" constant="5" id="oDX-bb-24H"/>
|
||||
</constraints>
|
||||
</progressView>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="7mJ-VZ-zqU">
|
||||
<rect key="frame" x="214" y="34" width="33" height="22"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleFootnote"/>
|
||||
<color key="textColor" systemColor="secondaryLabelColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
<viewLayoutGuide key="safeArea" id="sNo-8i-tO3"/>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<constraints>
|
||||
<constraint firstItem="Ds3-59-ooT" firstAttribute="centerX" secondItem="ejl-zC-eNy" secondAttribute="centerX" id="5Rv-6l-HSL"/>
|
||||
<constraint firstItem="Ds3-59-ooT" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="ejl-zC-eNy" secondAttribute="leading" id="Bck-uf-0G7"/>
|
||||
<constraint firstItem="7mJ-VZ-zqU" firstAttribute="bottom" secondItem="sNo-8i-tO3" secondAttribute="bottom" id="DVn-hI-PhH"/>
|
||||
<constraint firstItem="7mJ-VZ-zqU" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="sNo-8i-tO3" secondAttribute="leading" id="Sbp-yf-ts9"/>
|
||||
<constraint firstItem="7mJ-VZ-zqU" firstAttribute="centerY" secondItem="ejl-zC-eNy" secondAttribute="centerY" id="Shb-X2-Fwc"/>
|
||||
<constraint firstItem="7mJ-VZ-zqU" firstAttribute="centerX" secondItem="ejl-zC-eNy" secondAttribute="centerX" id="lFg-fm-YmV"/>
|
||||
<constraint firstItem="sNo-8i-tO3" firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="7mJ-VZ-zqU" secondAttribute="trailing" id="mZ2-XG-Kvg"/>
|
||||
<constraint firstItem="Ds3-59-ooT" firstAttribute="centerY" secondItem="ejl-zC-eNy" secondAttribute="centerY" id="tIh-lb-KbY"/>
|
||||
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="Ds3-59-ooT" secondAttribute="trailing" id="vSU-N6-Sk5"/>
|
||||
</constraints>
|
||||
<nil key="simulatedTopBarMetrics"/>
|
||||
<nil key="simulatedBottomBarMetrics"/>
|
||||
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
|
||||
<connections>
|
||||
<outlet property="label" destination="7mJ-VZ-zqU" id="MHr-r4-qop"/>
|
||||
<outlet property="progressView" destination="Ds3-59-ooT" id="TjM-db-LxM"/>
|
||||
</connections>
|
||||
<point key="canvasLocation" x="-75" y="-117"/>
|
||||
</view>
|
||||
</objects>
|
||||
<resources>
|
||||
<systemColor name="secondaryLabelColor">
|
||||
<color red="0.23529411764705882" green="0.23529411764705882" blue="0.2627450980392157" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</systemColor>
|
||||
</resources>
|
||||
</document>
|
||||
@@ -7,6 +7,7 @@
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import SwiftUI
|
||||
import RSCore
|
||||
import Account
|
||||
import Articles
|
||||
@@ -21,7 +22,9 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
|
||||
|
||||
@IBOutlet weak var markAllAsReadButton: UIBarButtonItem!
|
||||
|
||||
private var refreshProgressView: RefreshProgressView!
|
||||
let refreshProgressModel = RefreshProgressModel()
|
||||
lazy var progressBarViewController = UIHostingController(rootView: RefreshProgressView(progressBarMode: refreshProgressModel))
|
||||
|
||||
private var refreshProgressItemButton: UIBarButtonItem!
|
||||
private var firstUnreadButton: UIBarButtonItem!
|
||||
|
||||
@@ -95,13 +98,13 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
|
||||
|
||||
refreshControl = UIRefreshControl()
|
||||
refreshControl!.addTarget(self, action: #selector(refreshAccounts(_:)), for: .valueChanged)
|
||||
refreshControl!.tintColor = .clear
|
||||
|
||||
progressBarViewController.view.backgroundColor = .clear
|
||||
refreshProgressItemButton = UIBarButtonItem(customView: progressBarViewController.view)
|
||||
|
||||
configureToolbar()
|
||||
|
||||
refreshProgressView = Bundle.main.loadNibNamed("RefreshProgressView", owner: self, options: nil)?[0] as? RefreshProgressView
|
||||
refreshProgressItemButton = UIBarButtonItem(customView: refreshProgressView!)
|
||||
|
||||
|
||||
resetUI(resetScroll: true)
|
||||
|
||||
// Load the table and then scroll to the saved position if available
|
||||
@@ -243,7 +246,7 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
|
||||
}
|
||||
|
||||
func updateUI() {
|
||||
refreshProgressView?.update()
|
||||
refreshProgressModel.update()
|
||||
updateTitleUnreadCount()
|
||||
updateToolbar()
|
||||
}
|
||||
@@ -614,12 +617,6 @@ private extension MasterTimelineViewController {
|
||||
return
|
||||
}
|
||||
|
||||
guard let refreshProgressView = Bundle.main.loadNibNamed("RefreshProgressView", owner: self, options: nil)?[0] as? RefreshProgressView else {
|
||||
return
|
||||
}
|
||||
|
||||
self.refreshProgressView = refreshProgressView
|
||||
let refreshProgressItemButton = UIBarButtonItem(customView: refreshProgressView)
|
||||
toolbarItems?.insert(refreshProgressItemButton, at: 2)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
//
|
||||
// RoundedProgressView.swift
|
||||
// NetNewsWire
|
||||
//
|
||||
// Created by Maurice Parker on 10/29/19.
|
||||
// Copyright © 2019 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class RoundedProgressView: UIProgressView {
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
subviews.forEach { subview in
|
||||
subview.layer.masksToBounds = true
|
||||
subview.layer.cornerRadius = bounds.height / 2.0
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user