Merge pull request #2210 from philviso/swiftui-progressview

Implement iOS progress view
This commit is contained in:
Maurice Parker
2020-07-04 10:23:20 -05:00
committed by GitHub
7 changed files with 224 additions and 9 deletions

View File

@@ -69,7 +69,7 @@ struct AppAssets {
}()
static var faviconTemplateImage: RSImage = {
return RSImage(named: "FaviconTemplateImage")!
return RSImage(named: "faviconTemplateImage")!
}()
static var settingsImage: Image = {

View File

@@ -0,0 +1,29 @@
//
// PreviewProvider+RefreshProgressModel.swift
// NetNewsWire
//
// Created by Phil Viso on 7/3/20.
// Copyright © 2020 Ranchero Software. All rights reserved.
//
import Account
import Foundation
import RSWeb
import SwiftUI
extension PreviewProvider {
static func refreshProgressModel(lastRefreshDate: Date?,
tasksCompleted: Int,
totalTasks: Int) -> RefreshProgressModel {
return RefreshProgressModel { () -> Date? in
return lastRefreshDate
} combinedRefreshProgressProvider: { () -> CombinedRefreshProgress in
let progress = DownloadProgress(numberOfTasks: totalTasks)
progress.numberRemaining = totalTasks - tasksCompleted
return CombinedRefreshProgress(downloadProgressArray: [progress])
}
}
}

View File

@@ -0,0 +1,101 @@
//
// RefreshProgressModel.swift
// NetNewsWire
//
// Created by Phil Viso on 7/2/20.
// Copyright © 2020 Ranchero Software. All rights reserved.
//
import Account
import Combine
import Foundation
import SwiftUI
class RefreshProgressModel: ObservableObject {
enum State {
case refreshProgress(Float)
case lastRefreshDateText(String)
case none
}
@Published var state = State.none
private static var dateFormatter: RelativeDateTimeFormatter = {
let formatter = RelativeDateTimeFormatter()
formatter.dateTimeStyle = .named
return formatter
}()
private let lastRefreshDate: () -> Date?
private let combinedRefreshProgress: () -> CombinedRefreshProgress
private static let lastRefreshDateTextUpdateInterval = 60
private static let lastRefreshDateTextRelativeDateFormattingThreshold = 60.0
init(lastRefreshDateProvider: @escaping () -> Date?,
combinedRefreshProgressProvider: @escaping () -> CombinedRefreshProgress) {
self.lastRefreshDate = lastRefreshDateProvider
self.combinedRefreshProgress = combinedRefreshProgressProvider
updateState()
observeRefreshProgress()
scheduleLastRefreshDateTextUpdate()
}
// MARK: Observing account changes
private func observeRefreshProgress() {
NotificationCenter.default.addObserver(self, selector: #selector(updateState), name: .AccountRefreshProgressDidChange, object: nil)
}
// MARK: Refreshing state
@objc private func updateState() {
let progress = combinedRefreshProgress()
if !progress.isComplete {
let fractionCompleted = Float(progress.numberCompleted) / Float(progress.numberOfTasks)
self.state = .refreshProgress(fractionCompleted)
} else if let lastRefreshDate = self.lastRefreshDate() {
let text = localizedLastRefreshText(lastRefreshDate: lastRefreshDate)
self.state = .lastRefreshDateText(text)
} else {
self.state = .none
}
}
private func scheduleLastRefreshDateTextUpdate() {
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(Self.lastRefreshDateTextUpdateInterval)) {
self.updateState()
self.scheduleLastRefreshDateTextUpdate()
}
}
private func localizedLastRefreshText(lastRefreshDate: Date) -> String {
let now = Date()
if now > lastRefreshDate.addingTimeInterval(Self.lastRefreshDateTextRelativeDateFormattingThreshold) {
let localizedDate = Self.dateFormatter.localizedString(for: lastRefreshDate, relativeTo: now)
let formatString = NSLocalizedString("Updated %@", comment: "Updated") as NSString
return NSString.localizedStringWithFormat(formatString, localizedDate) as String
} else {
return NSLocalizedString("Updated Just Now", comment: "Updated Just Now")
}
}
}
extension RefreshProgressModel {
convenience init() {
self.init(
lastRefreshDateProvider: { AccountManager.shared.lastArticleFetchEndTime },
combinedRefreshProgressProvider: { AccountManager.shared.combinedRefreshProgress }
)
}
}

View File

@@ -11,10 +11,19 @@ import Account
final class SceneModel: ObservableObject {
@Published var refreshProgressState = RefreshProgressModel.State.none
var sidebarModel: SidebarModel?
var timelineModel: TimelineModel?
var articleModel: ArticleModel?
private let refreshProgressModel: RefreshProgressModel
init(refreshProgressModel: RefreshProgressModel = RefreshProgressModel()) {
self.refreshProgressModel = refreshProgressModel
self.refreshProgressModel.$state.assign(to: self.$refreshProgressState)
}
}
// MARK: SidebarModelDelegate

View File

@@ -0,0 +1,57 @@
//
// RefreshProgressView.swift
// NetNewsWire
//
// Created by Phil Viso on 7/2/20.
// Copyright © 2020 Ranchero Software. All rights reserved.
//
import SwiftUI
struct RefreshProgressView: View {
@EnvironmentObject var sceneModel: SceneModel
@ViewBuilder var body: some View {
switch sceneModel.refreshProgressState {
case .refreshProgress(let progress):
ProgressView(value: progress)
.frame(width: progressViewWidth())
case .lastRefreshDateText(let text):
Text(text)
.lineLimit(1)
.font(.caption)
.foregroundColor(.secondary)
case .none:
EmptyView()
}
}
// MARK -
private func progressViewWidth() -> CGFloat {
#if os(iOS)
return 100.0
#endif
#if os(macOS)
return 40.0
#endif
}
}
struct RefreshProgressView_Previews: PreviewProvider {
static var previews: some View {
Group {
RefreshProgressView()
.environmentObject(refreshProgressModel(lastRefreshDate: nil, tasksCompleted: 1, totalTasks: 2))
.previewDisplayName("Refresh in progress")
RefreshProgressView()
.environmentObject(refreshProgressModel(lastRefreshDate: Date(timeIntervalSinceNow: -120.0), tasksCompleted: 0, totalTasks: 0))
.previewDisplayName("Last refreshed with date")
}
.previewLayout(.sizeThatFits)
}
}

View File

@@ -8,7 +8,6 @@
import SwiftUI
struct SidebarToolbar: ViewModifier {
@EnvironmentObject private var appSettings: AppDefaults
@@ -25,23 +24,25 @@ struct SidebarToolbar: ViewModifier {
AppAssets.settingsImage
.font(.title3)
.foregroundColor(.accentColor)
Spacer()
}).help("Settings")
}
ToolbarItem(placement: .automatic, content: {
ToolbarItem {
Spacer()
Text("Last updated")
.font(.caption)
.foregroundColor(.secondary)
}
ToolbarItem(placement: .automatic) {
RefreshProgressView()
}
ToolbarItem {
Spacer()
})
}
ToolbarItem(placement: .automatic, content: {
Button(action: {
viewModel.showActionSheet = true
}, label: {
Spacer()
AppAssets.addMenuImage
.font(.title3)
.foregroundColor(.accentColor)