Update iOS with latest TimelineModel refactoring

This commit is contained in:
Maurice Parker
2020-07-25 06:20:21 -05:00
parent 7d7a018fe1
commit 882ebbea3e
7 changed files with 220 additions and 47 deletions

View File

@@ -0,0 +1,121 @@
//
// ReplaySubject.swift
// CombineExt
//
// Created by Jasdev Singh on 13/04/2020.
// Copyright © 2020 Combine Community. All rights reserved.
//
#if canImport(Combine)
import Combine
/// A `ReplaySubject` is a subject that can buffer one or more values. It stores value events, up to its `bufferSize` in a
/// first-in-first-out manner and then replays it to
/// future subscribers and also forwards completion events.
///
/// The implementation borrows heavily from [Entwines](https://github.com/tcldr/Entwine/blob/b839c9fcc7466878d6a823677ce608da998b95b9/Sources/Entwine/Operators/ReplaySubject.swift).
@available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
public final class ReplaySubject<Output, Failure: Error>: Subject {
public typealias Output = Output
public typealias Failure = Failure
private let bufferSize: Int
private var buffer = [Output]()
// Keeping track of all live subscriptions, so `send` events can be forwarded to them.
private var subscriptions = [Subscription<AnySubscriber<Output, Failure>>]()
private var completion: Subscribers.Completion<Failure>?
private var isActive: Bool { completion == nil }
/// Create a `ReplaySubject`, buffering up to `bufferSize` values and replaying them to new subscribers
/// - Parameter bufferSize: The maximum number of value events to buffer and replay to all future subscribers.
public init(bufferSize: Int) {
self.bufferSize = bufferSize
}
public func send(_ value: Output) {
guard isActive else { return }
buffer.append(value)
if buffer.count > bufferSize {
buffer.removeFirst()
}
subscriptions.forEach { $0.forwardValueToBuffer(value) }
}
public func send(completion: Subscribers.Completion<Failure>) {
guard isActive else { return }
self.completion = completion
subscriptions.forEach { $0.forwardCompletionToBuffer(completion) }
}
public func send(subscription: Combine.Subscription) {
subscription.request(.unlimited)
}
public func receive<Subscriber: Combine.Subscriber>(subscriber: Subscriber) where Failure == Subscriber.Failure, Output == Subscriber.Input {
let subscriberIdentifier = subscriber.combineIdentifier
let subscription = Subscription(downstream: AnySubscriber(subscriber)) { [weak self] in
guard let self = self,
let subscriptionIndex = self.subscriptions
.firstIndex(where: { $0.innerSubscriberIdentifier == subscriberIdentifier }) else { return }
self.subscriptions.remove(at: subscriptionIndex)
}
subscriptions.append(subscription)
subscriber.receive(subscription: subscription)
subscription.replay(buffer, completion: completion)
}
}
@available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
extension ReplaySubject {
final class Subscription<Downstream: Subscriber>: Combine.Subscription where Output == Downstream.Input, Failure == Downstream.Failure {
private var demandBuffer: DemandBuffer<Downstream>?
private var cancellationHandler: (() -> Void)?
fileprivate let innerSubscriberIdentifier: CombineIdentifier
init(downstream: Downstream, cancellationHandler: (() -> Void)?) {
self.demandBuffer = DemandBuffer(subscriber: downstream)
self.innerSubscriberIdentifier = downstream.combineIdentifier
self.cancellationHandler = cancellationHandler
}
func replay(_ buffer: [Output], completion: Subscribers.Completion<Failure>?) {
buffer.forEach(forwardValueToBuffer)
if let completion = completion {
forwardCompletionToBuffer(completion)
}
}
func forwardValueToBuffer(_ value: Output) {
_ = demandBuffer?.buffer(value: value)
}
func forwardCompletionToBuffer(_ completion: Subscribers.Completion<Failure>) {
demandBuffer?.complete(completion: completion)
}
func request(_ demand: Subscribers.Demand) {
_ = demandBuffer?.demand(demand)
}
func cancel() {
cancellationHandler?()
cancellationHandler = nil
demandBuffer = nil
}
}
}
#endif

View File

@@ -0,0 +1,24 @@
//
// ShareReplay.swift
// CombineExt
//
// Created by Jasdev Singh on 13/04/2020.
// Copyright © 2020 Combine Community. All rights reserved.
//
#if canImport(Combine)
import Combine
@available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
public extension Publisher {
/// A variation on [share()](https://developer.apple.com/documentation/combine/publisher/3204754-share)
/// that allows for buffering and replaying a `replay` amount of value events to future subscribers.
///
/// - Parameter count: The number of value events to buffer in a first-in-first-out manner.
/// - Returns: A publisher that replays the specified number of value events to future subscribers.
func share(replay count: Int) -> Publishers.Autoconnect<Publishers.Multicast<Self, ReplaySubject<Output, Failure>>> {
multicast { ReplaySubject(bufferSize: count) }
.autoconnect()
}
}
#endif

View File

@@ -75,6 +75,7 @@ private extension SidebarModel {
.removeDuplicates(by: { previousFeeds, currentFeeds in
return previousFeeds.elementsEqual(currentFeeds, by: { $0.feedID == $1.feedID })
})
.share(replay: 1)
.eraseToAnyPublisher()
}
@@ -107,7 +108,7 @@ private extension SidebarModel {
.compactMap { [weak self] _, readFilter, selectedFeeds in
self?.rebuildSidebarItems(isReadFiltered: readFilter, selectedFeeds: selectedFeeds)
}
.share()
.share(replay: 1)
.eraseToAnyPublisher()
}

View File

@@ -26,8 +26,8 @@ class TimelineModel: ObservableObject, UndoableCommandRunner {
weak var delegate: TimelineModelDelegate?
@Published var nameForDisplay = ""
@Published var selectedArticleIDs = Set<String>() // Don't use directly. Use selectedArticles
@Published var selectedArticleID: String? = nil // Don't use directly. Use selectedArticles
@Published var selectedTimelineItemIDs = Set<String>() // Don't use directly. Use selectedTimelineItemsPublisher
@Published var selectedTimelineItemID: String? = nil // Don't use directly. Use selectedTimelineItemsPublisher
@Published var isReadFiltered: Bool? = nil
var timelineItemsPublisher: AnyPublisher<[TimelineItem], Never>?
@@ -40,15 +40,16 @@ class TimelineModel: ObservableObject, UndoableCommandRunner {
private var cancellables = Set<AnyCancellable>()
private var sortDirectionSubject = PassthroughSubject<Bool, Never>()
private var groupByFeedSubject = PassthroughSubject<Bool, Never>()
private var sortDirectionSubject = ReplaySubject<Bool, Never>(bufferSize: 1)
private var groupByFeedSubject = ReplaySubject<Bool, Never>(bufferSize: 1)
init(delegate: TimelineModelDelegate) {
self.delegate = delegate
// subscribeToArticleStatusChanges()
subscribeToUserDefaultsChanges()
subscribeToReadFilterChanges()
subscribeToArticleFetchChanges()
subscribeToSelectedArticleSelectionChanges()
// subscribeToArticleStatusChanges()
// subscribeToAccountDidDownloadArticles()
}
@@ -78,6 +79,31 @@ class TimelineModel: ObservableObject, UndoableCommandRunner {
// }.store(in: &cancellables)
// }
func subscribeToReadFilterChanges() {
guard let selectedFeedsPublisher = delegate?.selectedFeedsPublisher else { return }
selectedFeedsPublisher.sink { [weak self] feeds in
guard let self = self else { return }
guard feeds.count == 1, let timelineFeed = feeds.first else {
self.isReadFiltered = nil
return
}
guard timelineFeed.defaultReadFilterType != .alwaysRead else {
self.isReadFiltered = nil
return
}
if let feedID = timelineFeed.feedID, let readFilterEnabled = self.readFilterEnabledTable[feedID] {
self.isReadFiltered = readFilterEnabled
} else {
self.isReadFiltered = timelineFeed.defaultReadFilterType == .read
}
}
.store(in: &cancellables)
}
func subscribeToUserDefaultsChanges() {
let kickStartNote = Notification(name: Notification.Name("Kick Start"))
NotificationCenter.default.publisher(for: UserDefaults.didChangeNotification)
@@ -97,11 +123,12 @@ class TimelineModel: ObservableObject, UndoableCommandRunner {
.map { [weak self] feeds -> Set<Article> in
return self?.fetchArticles(feeds: feeds) ?? Set<Article>()
}
.combineLatest($isReadFiltered, sortDirectionPublisher, groupByPublisher)
.compactMap { [weak self] articles, filtered, sortDirection, groupBy -> [TimelineItem] in
.combineLatest(sortDirectionPublisher, groupByPublisher)
.compactMap { [weak self] articles, sortDirection, groupBy -> [TimelineItem] in
let sortedArticles = Array(articles).sortedByDate(sortDirection ? .orderedDescending : .orderedAscending, groupByFeed: groupBy)
return self?.buildTimelineItems(articles: sortedArticles) ?? [TimelineItem]()
}
.share(replay: 1)
.eraseToAnyPublisher()
}
@@ -218,24 +245,6 @@ private extension TimelineModel {
}
// MARK: Timeline Management
// func resetReadFilter() {
// guard feeds.count == 1, let timelineFeed = feeds.first else {
// isReadFiltered = nil
// return
// }
//
// guard timelineFeed.defaultReadFilterType != .alwaysRead else {
// isReadFiltered = nil
// return
// }
//
// if let feedID = timelineFeed.feedID, let readFilterEnabled = readFilterEnabledTable[feedID] {
// isReadFiltered = readFilterEnabled
// } else {
// isReadFiltered = timelineFeed.defaultReadFilterType == .read
// }
// }
func sortParametersDidChange() {
// performBlockAndRestoreSelection {

View File

@@ -38,8 +38,8 @@ struct TimelineView: View {
.help(timelineModel.isReadFiltered ?? false ? "Show Read Articles" : "Filter Read Articles")
}
ScrollViewReader { scrollViewProxy in
List(timelineItems, selection: $timelineModel.selectedArticleIDs) { timelineItem in
let selected = timelineModel.selectedArticleIDs.contains(timelineItem.article.articleID)
List(timelineItems, selection: $timelineModel.selectedTimelineItemIDs) { timelineItem in
let selected = timelineModel.selectedTimelineItemIDs.contains(timelineItem.article.articleID)
TimelineItemView(selected: selected, width: geometryReaderProxy.size.width, timelineItem: timelineItem)
.background(TimelineItemFramePreferenceView(timelineItem: timelineItem))
}
@@ -48,7 +48,7 @@ struct TimelineView: View {
timelineItemFrames[pref.articleID] = pref.frame
}
}
.onChange(of: timelineModel.selectedArticleIDs) { selectedArticleIDs in
.onChange(of: timelineModel.selectedTimelineItemIDs) { selectedArticleIDs in
let proxyFrame = geometryReaderProxy.frame(in: .global)
for articleID in selectedArticleIDs {
if let itemFrame = timelineItemFrames[articleID] {
@@ -70,14 +70,14 @@ struct TimelineView: View {
.navigationTitle(Text(verbatim: timelineModel.nameForDisplay))
#else
ScrollViewReader { scrollViewProxy in
List(timelineModel.timelineItems) { timelineItem in
List(timelineItems) { timelineItem in
ZStack {
let selected = timelineModel.selectedArticleID == timelineItem.article.articleID
let selected = timelineModel.selectedTimelineItemID == timelineItem.article.articleID
TimelineItemView(selected: selected, width: geometryReaderProxy.size.width, timelineItem: timelineItem)
.background(TimelineItemFramePreferenceView(timelineItem: timelineItem))
NavigationLink(destination: ArticleContainerView(),
tag: timelineItem.article.articleID,
selection: $timelineModel.selectedArticleID) {
selection: $timelineModel.selectedTimelineItemID) {
EmptyView()
}.buttonStyle(PlainButtonStyle())
}
@@ -87,7 +87,7 @@ struct TimelineView: View {
timelineItemFrames[pref.articleID] = pref.frame
}
}
.onChange(of: timelineModel.selectedArticleID) { selectedArticleID in
.onChange(of: timelineModel.selectedTimelineItemID) { selectedArticleID in
let proxyFrame = geometryReaderProxy.frame(in: .global)
if let articleID = selectedArticleID, let itemFrame = timelineItemFrames[articleID] {
if itemFrame.minY < proxyFrame.minY + 3 || itemFrame.maxY > proxyFrame.maxY - 3 {
@@ -98,6 +98,12 @@ struct TimelineView: View {
}
}
}
.onReceive(timelineModel.timelineItemsPublisher!) { items in
// Animations crash on iPadOS right now
// withAnimation {
timelineItems = items
// }
}
.navigationBarTitle(Text(verbatim: timelineModel.nameForDisplay), displayMode: .inline)
#endif
}

View File

@@ -61,9 +61,9 @@ class ArticleViewController: UIViewController {
view.bottomAnchor.constraint(equalTo: pageViewController.view.bottomAnchor)
])
selectedArticlesCancellable = sceneModel?.timelineModel.$selectedArticles.sink { [weak self] articles in
self?.articles = articles
}
// selectedArticlesCancellable = sceneModel?.timelineModel.$selectedArticles.sink { [weak self] articles in
// self?.articles = articles
// }
let controller = createWebViewController(currentArticle, updateView: true)
self.pageViewController.setViewControllers([controller], direction: .forward, animated: false, completion: nil)