From e88e4f65a50f6c6a9f890e1b4e6a207e28f29807 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Sat, 25 Jul 2020 10:31:22 -0500 Subject: [PATCH] Switch TimelineItems to use an OrderedDictionary --- .../Shared/Timeline/TimelineModel.swift | 13 ++-- .../Shared/Timeline/TimelineView.swift | 12 ++-- NetNewsWire.xcodeproj/project.pbxproj | 12 ++++ Shared/Extensions/OrderedDictionary.swift | 64 +++++++++++++++++++ 4 files changed, 90 insertions(+), 11 deletions(-) create mode 100644 Shared/Extensions/OrderedDictionary.swift diff --git a/Multiplatform/Shared/Timeline/TimelineModel.swift b/Multiplatform/Shared/Timeline/TimelineModel.swift index fbdd80890..1ad30ad0a 100644 --- a/Multiplatform/Shared/Timeline/TimelineModel.swift +++ b/Multiplatform/Shared/Timeline/TimelineModel.swift @@ -30,7 +30,7 @@ class TimelineModel: ObservableObject, UndoableCommandRunner { @Published var selectedTimelineItemID: String? = nil // Don't use directly. Use selectedTimelineItemsPublisher @Published var isReadFiltered: Bool? = nil - var timelineItemsPublisher: AnyPublisher<[TimelineItem], Never>? + var timelineItemsPublisher: AnyPublisher, Never>? var selectedTimelineItemsPublisher: AnyPublisher<[TimelineItem], Never>? var readFilterEnabledTable = [FeedIdentifier: Bool]() @@ -124,9 +124,9 @@ class TimelineModel: ObservableObject, UndoableCommandRunner { return self?.fetchArticles(feeds: feeds) ?? Set
() } .combineLatest(sortDirectionPublisher, groupByPublisher) - .compactMap { [weak self] articles, sortDirection, groupBy -> [TimelineItem] in + .compactMap { [weak self] articles, sortDirection, groupBy in let sortedArticles = Array(articles).sortedByDate(sortDirection ? .orderedDescending : .orderedAscending, groupByFeed: groupBy) - return self?.buildTimelineItems(articles: sortedArticles) ?? [TimelineItem]() + return self?.buildTimelineItems(articles: sortedArticles) ?? OrderedDictionary() } .share(replay: 1) .eraseToAnyPublisher() @@ -284,10 +284,11 @@ private extension TimelineModel { return fetchedArticles } - func buildTimelineItems(articles: [Article]) -> [TimelineItem] { - var items = [TimelineItem]() + func buildTimelineItems(articles: [Article]) -> OrderedDictionary { + var items = OrderedDictionary() for (index, article) in articles.enumerated() { - items.append(TimelineItem(index: index, article: article)) + let item = TimelineItem(index: index, article: article) + items[item.id] = item } return items } diff --git a/Multiplatform/Shared/Timeline/TimelineView.swift b/Multiplatform/Shared/Timeline/TimelineView.swift index f7da66f45..18c7ca5dc 100644 --- a/Multiplatform/Shared/Timeline/TimelineView.swift +++ b/Multiplatform/Shared/Timeline/TimelineView.swift @@ -11,7 +11,7 @@ import SwiftUI struct TimelineView: View { @EnvironmentObject private var timelineModel: TimelineModel - @State private var timelineItems = [TimelineItem]() + @State private var timelineItems = OrderedDictionary() @State private var timelineItemFrames = [String: CGRect]() @ViewBuilder var body: some View { @@ -38,10 +38,12 @@ struct TimelineView: View { .help(timelineModel.isReadFiltered ?? false ? "Show Read Articles" : "Filter Read Articles") } ScrollViewReader { scrollViewProxy in - 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)) + List(timelineItems.keys, id: \.self, selection: $timelineModel.selectedTimelineItemIDs) { timelineItemID in + if let timelineItem = timelineItems[timelineItemID] { + let selected = timelineModel.selectedTimelineItemIDs.contains(timelineItem.article.articleID) + TimelineItemView(selected: selected, width: geometryReaderProxy.size.width, timelineItem: timelineItem) + .background(TimelineItemFramePreferenceView(timelineItem: timelineItem)) + } } .onPreferenceChange(TimelineItemFramePreferenceKey.self) { preferences in for pref in preferences { diff --git a/NetNewsWire.xcodeproj/project.pbxproj b/NetNewsWire.xcodeproj/project.pbxproj index 4deafb144..acec1923f 100644 --- a/NetNewsWire.xcodeproj/project.pbxproj +++ b/NetNewsWire.xcodeproj/project.pbxproj @@ -477,6 +477,11 @@ 51C452AF2265108300C03939 /* ArticleArray.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F204DF1FAACBB30076E152 /* ArticleArray.swift */; }; 51C452B42265141B00C03939 /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 51C452B32265141B00C03939 /* WebKit.framework */; }; 51C452B82265178500C03939 /* styleSheet.css in Resources */ = {isa = PBXBuildFile; fileRef = 51C452B72265178500C03939 /* styleSheet.css */; }; + 51C65AD524CC834F008EB3BD /* OrderedDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C65AD424CC834F008EB3BD /* OrderedDictionary.swift */; }; + 51C65AD624CC834F008EB3BD /* OrderedDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C65AD424CC834F008EB3BD /* OrderedDictionary.swift */; }; + 51C65AD724CC834F008EB3BD /* OrderedDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C65AD424CC834F008EB3BD /* OrderedDictionary.swift */; }; + 51C65AD824CC834F008EB3BD /* OrderedDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C65AD424CC834F008EB3BD /* OrderedDictionary.swift */; }; + 51C65AD924CC834F008EB3BD /* OrderedDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C65AD424CC834F008EB3BD /* OrderedDictionary.swift */; }; 51C9DE5823EA2EF4003D5A6D /* WrapperScriptMessageHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C9DE5723EA2EF4003D5A6D /* WrapperScriptMessageHandler.swift */; }; 51CE1C0923621EDA005548FC /* RefreshProgressView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 51CE1C0823621EDA005548FC /* RefreshProgressView.xib */; }; 51CE1C0B23622007005548FC /* RefreshProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51CE1C0A23622006005548FC /* RefreshProgressView.swift */; }; @@ -2108,6 +2113,7 @@ 51C4528B2265095F00C03939 /* AddFolderViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddFolderViewController.swift; sourceTree = ""; }; 51C452B32265141B00C03939 /* WebKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebKit.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS12.2.sdk/System/Library/Frameworks/WebKit.framework; sourceTree = DEVELOPER_DIR; }; 51C452B72265178500C03939 /* styleSheet.css */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.css; path = styleSheet.css; sourceTree = ""; }; + 51C65AD424CC834F008EB3BD /* OrderedDictionary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrderedDictionary.swift; sourceTree = ""; }; 51C9DE5723EA2EF4003D5A6D /* WrapperScriptMessageHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WrapperScriptMessageHandler.swift; sourceTree = ""; }; 51CE1C0823621EDA005548FC /* RefreshProgressView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = RefreshProgressView.xib; sourceTree = ""; }; 51CE1C0A23622006005548FC /* RefreshProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshProgressView.swift; sourceTree = ""; }; @@ -3582,6 +3588,7 @@ 51126DA3225FDE2F00722696 /* RSImage-Extensions.swift */, 84411E701FE5FBFA004B527F /* SmallIconProvider.swift */, 51BC4ADD247277DF000A6ED8 /* URL-Extensions.swift */, + 51C65AD424CC834F008EB3BD /* OrderedDictionary.swift */, ); path = Extensions; sourceTree = ""; @@ -5247,6 +5254,7 @@ 51E4995324A8734D00B667CB /* RedditFeedProvider-Extensions.swift in Sources */, 5177471024B3029400EB0F74 /* ArticleViewController.swift in Sources */, 65082A5224C72B88009FA994 /* SettingsCredentialsAccountModel.swift in Sources */, + 51C65AD824CC834F008EB3BD /* OrderedDictionary.swift in Sources */, 172199C924AB228900A31D04 /* SettingsView.swift in Sources */, 51B8BCC224C25C3E00360B00 /* SidebarContextMenu.swift in Sources */, 51A8005124CC453C00F41F1D /* ReplaySubject.swift in Sources */, @@ -5385,6 +5393,7 @@ 1769E32224BC5925000E1E8E /* AccountsPreferencesModel.swift in Sources */, 51E4991624A8090300B667CB /* ArticleUtilities.swift in Sources */, 51919FF224AB864A00541E64 /* TimelineModel.swift in Sources */, + 51C65AD924CC834F008EB3BD /* OrderedDictionary.swift in Sources */, 51E4991A24A8090F00B667CB /* IconImage.swift in Sources */, 1799E6AA24C2F93F00511E91 /* InspectorPlatformModifier.swift in Sources */, 51B8104624C0E6D200C6C32D /* TimelineTextSizer.swift in Sources */, @@ -5640,6 +5649,7 @@ 65ED403F235DEF6C0081F399 /* ArticleRenderer.swift in Sources */, 65ED4040235DEF6C0081F399 /* GeneralPrefencesViewController.swift in Sources */, 179DB1DFBCF9177104B12E0F /* AccountsNewsBlurWindowController.swift in Sources */, + 51C65AD624CC834F008EB3BD /* OrderedDictionary.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -5797,6 +5807,7 @@ D3555BF524664566005E48C3 /* ArticleSearchBar.swift in Sources */, B24E9ADE245AB88400DA5718 /* NSAttributedString+NetNewsWire.swift in Sources */, C5A6ED5223C9AF4300AB6BE2 /* TitleActivityItemSource.swift in Sources */, + 51C65AD724CC834F008EB3BD /* OrderedDictionary.swift in Sources */, 51DC37092402F1470095D371 /* MasterFeedDataSourceOperation.swift in Sources */, 51C4529B22650A1000C03939 /* FaviconDownloader.swift in Sources */, 84DEE56622C32CA4005FC42C /* SmartFeedDelegate.swift in Sources */, @@ -5928,6 +5939,7 @@ 848F6AE51FC29CFB002D422E /* FaviconDownloader.swift in Sources */, 511B9806237DCAC90028BCAA /* UserInfoKey.swift in Sources */, 84C9FC7722629E1200D921D6 /* AdvancedPreferencesViewController.swift in Sources */, + 51C65AD524CC834F008EB3BD /* OrderedDictionary.swift in Sources */, 849EE72120391F560082A1EA /* SharingServicePickerDelegate.swift in Sources */, 5108F6B62375E612001ABC45 /* CacheCleaner.swift in Sources */, 849A97981ED9EFAA007D329B /* Node-Extensions.swift in Sources */, diff --git a/Shared/Extensions/OrderedDictionary.swift b/Shared/Extensions/OrderedDictionary.swift new file mode 100644 index 000000000..46e260d15 --- /dev/null +++ b/Shared/Extensions/OrderedDictionary.swift @@ -0,0 +1,64 @@ +// +// OrderedDictionary.swift +// SwiftDataStructures +// +// Created by Tim Ekl on 6/2/14. +// Copyright (c) 2014 Tim Ekl. Available under MIT License. See LICENSE.md. +// + +import Foundation + +struct OrderedDictionary { + var keys: Array = [] + var values: Dictionary = [:] + + var count: Int { + assert(keys.count == values.count, "Keys and values array out of sync") + return self.keys.count; + } + + // Explicitly define an empty initializer to prevent the default memberwise initializer from being generated + init() {} + + subscript(index: Int) -> Tv? { + get { + let key = self.keys[index] + return self.values[key] + } + set(newValue) { + let key = self.keys[index] + if (newValue != nil) { + self.values[key] = newValue + } else { + self.values[key] = nil + self.keys.remove(at: index) + } + } + } + + subscript(key: Tk) -> Tv? { + get { + return self.values[key] + } + set(newValue) { + if newValue == nil { + self.values[key] = nil + self.keys = self.keys.filter {$0 != key} + } else { + let oldValue = self.values.updateValue(newValue!, forKey: key) + if oldValue == nil { + self.keys.append(key) + } + } + } + } + + var description: String { + var result = "{\n" + for i in 0..