mirror of
https://github.com/Ranchero-Software/NetNewsWire
synced 2025-08-12 06:26:36 +00:00
Merge branch 'swiftui' into swiftui
This commit is contained in:
@@ -104,6 +104,16 @@ struct AppAssets {
|
||||
#endif
|
||||
}()
|
||||
|
||||
static var timelineStarred: Image = {
|
||||
return Image(systemName: "star.fill")
|
||||
|
||||
}()
|
||||
|
||||
static var timelineUnread: Image = {
|
||||
return Image(systemName: "circle.fill")
|
||||
|
||||
}()
|
||||
|
||||
static var todayFeedImage: IconImage = {
|
||||
#if os(macOS)
|
||||
return IconImage(NSImage(systemSymbolName: "sun.max.fill", accessibilityDescription: nil)!)
|
||||
|
||||
59
Multiplatform/Shared/Images/ArticleIconImageLoader.swift
Normal file
59
Multiplatform/Shared/Images/ArticleIconImageLoader.swift
Normal file
@@ -0,0 +1,59 @@
|
||||
//
|
||||
// ArticleIconImageLoader.swift
|
||||
// NetNewsWire
|
||||
//
|
||||
// Created by Maurice Parker on 7/1/20.
|
||||
// Copyright © 2020 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Account
|
||||
import Articles
|
||||
|
||||
final class ArticleIconImageLoader: ObservableObject {
|
||||
|
||||
private var article: Article?
|
||||
|
||||
@Published var image: IconImage?
|
||||
|
||||
init() {
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(faviconDidBecomeAvailable(_:)), name: .FaviconDidBecomeAvailable, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(webFeedIconDidBecomeAvailable(_:)), name: .WebFeedIconDidBecomeAvailable, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(avatarDidBecomeAvailable(_:)), name: .AvatarDidBecomeAvailable, object: nil)
|
||||
}
|
||||
|
||||
func loadImage(for article: Article) {
|
||||
guard image == nil else { return }
|
||||
self.article = article
|
||||
image = article.iconImage()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private extension ArticleIconImageLoader {
|
||||
|
||||
@objc func faviconDidBecomeAvailable(_ note: Notification) {
|
||||
guard let article = article else { return }
|
||||
image = article.iconImage()
|
||||
}
|
||||
|
||||
@objc func webFeedIconDidBecomeAvailable(_ note: Notification) {
|
||||
guard let article = article, let noteFeed = note.userInfo?[UserInfoKey.webFeed] as? WebFeed, noteFeed == article.webFeed else {
|
||||
return
|
||||
}
|
||||
image = article.iconImage()
|
||||
}
|
||||
|
||||
@objc func avatarDidBecomeAvailable(_ note: Notification) {
|
||||
guard let article = article, let authors = article.authors, let avatarURL = note.userInfo?[UserInfoKey.url] as? String else {
|
||||
return
|
||||
}
|
||||
|
||||
for author in authors {
|
||||
if author.avatarURL == avatarURL {
|
||||
image = article.iconImage()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
//
|
||||
// FeedImageLoader.swift
|
||||
// FeedIconImageLoader.swift
|
||||
// NetNewsWire
|
||||
//
|
||||
// Created by Maurice Parker on 6/29/20.
|
||||
@@ -9,7 +9,7 @@
|
||||
import SwiftUI
|
||||
import Account
|
||||
|
||||
final class FeedImageLoader: ObservableObject {
|
||||
final class FeedIconImageLoader: ObservableObject {
|
||||
|
||||
private var feed: Feed?
|
||||
|
||||
@@ -21,8 +21,27 @@ final class FeedImageLoader: ObservableObject {
|
||||
}
|
||||
|
||||
func loadImage(for feed: Feed) {
|
||||
guard image == nil else { return }
|
||||
self.feed = feed
|
||||
|
||||
fetchImage()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private extension FeedIconImageLoader {
|
||||
|
||||
@objc func faviconDidBecomeAvailable(_ note: Notification) {
|
||||
fetchImage()
|
||||
}
|
||||
|
||||
@objc func webFeedIconDidBecomeAvailable(_ note: Notification) {
|
||||
guard let feed = feed as? WebFeed, let noteFeed = note.userInfo?[UserInfoKey.webFeed] as? WebFeed, feed == noteFeed else {
|
||||
return
|
||||
}
|
||||
fetchImage()
|
||||
}
|
||||
|
||||
func fetchImage() {
|
||||
if let webFeed = feed as? WebFeed {
|
||||
if let feedIconImage = appDelegate.webFeedIconDownloader.icon(for: webFeed) {
|
||||
image = feedIconImage
|
||||
@@ -40,19 +59,3 @@ final class FeedImageLoader: ObservableObject {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private extension FeedImageLoader {
|
||||
|
||||
@objc func faviconDidBecomeAvailable(_ note: Notification) {
|
||||
guard let feed = feed else { return }
|
||||
loadImage(for: feed)
|
||||
}
|
||||
|
||||
@objc func webFeedIconDidBecomeAvailable(_ note: Notification) {
|
||||
guard let feed = feed as? WebFeed, let noteFeed = note.userInfo?[UserInfoKey.webFeed] as? WebFeed, feed == noteFeed else {
|
||||
return
|
||||
}
|
||||
loadImage(for: feed)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -13,20 +13,10 @@ struct IconImageView: View {
|
||||
var iconImage: IconImage
|
||||
|
||||
var body: some View {
|
||||
#if os(macOS)
|
||||
return Image(nsImage: iconImage.image)
|
||||
return Image(rsImage: iconImage.image)
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 20, height: 20, alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/)
|
||||
.cornerRadius(4)
|
||||
#endif
|
||||
#if os(iOS)
|
||||
return Image(uiImage: iconImage.image)
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 20, height: 20, alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/)
|
||||
.cornerRadius(4)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
58
Multiplatform/Shared/Previews/PreviewArticles.swift
Normal file
58
Multiplatform/Shared/Previews/PreviewArticles.swift
Normal file
@@ -0,0 +1,58 @@
|
||||
//
|
||||
// PreviewArticles.swift
|
||||
// NetNewsWire
|
||||
//
|
||||
// Created by Maurice Parker on 7/1/20.
|
||||
// Copyright © 2020 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Articles
|
||||
|
||||
enum PreviewArticles {
|
||||
|
||||
static var basicUnread: Article {
|
||||
return makeBasicArticle(read: false, starred: false)
|
||||
}
|
||||
|
||||
static var basicRead: Article {
|
||||
return makeBasicArticle(read: true, starred: false)
|
||||
}
|
||||
|
||||
static var basicStarred: Article {
|
||||
return makeBasicArticle(read: false, starred: true)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private extension PreviewArticles {
|
||||
|
||||
static var shortTitle: String {
|
||||
return "Short article title"
|
||||
}
|
||||
|
||||
static var shortSummary: String {
|
||||
return "Summary of article to be shown after title."
|
||||
}
|
||||
|
||||
static func makeBasicArticle(read: Bool, starred: Bool) -> Article {
|
||||
let articleID = "prototype"
|
||||
let status = ArticleStatus(articleID: articleID, read: read, starred: starred, dateArrived: Date())
|
||||
return Article(accountID: articleID,
|
||||
articleID: articleID,
|
||||
webFeedID: articleID,
|
||||
uniqueID: articleID,
|
||||
title: shortTitle,
|
||||
contentHTML: nil,
|
||||
contentText: nil,
|
||||
url: nil,
|
||||
externalURL: nil,
|
||||
summary: shortSummary,
|
||||
imageURL: nil,
|
||||
datePublished: Date(),
|
||||
dateModified: nil,
|
||||
authors: nil,
|
||||
status: status)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -20,10 +20,6 @@ final class SceneModel: ObservableObject {
|
||||
|
||||
extension SceneModel: SidebarModelDelegate {
|
||||
|
||||
func sidebarSelectionDidChange(_: SidebarModel, feeds: [Feed]?) {
|
||||
print("**** sidebar selection changed ***")
|
||||
}
|
||||
|
||||
func unreadCount(for feed: Feed) -> Int {
|
||||
// TODO: Get the count from the timeline if Feed is the current timeline
|
||||
return feed.unreadCount
|
||||
|
||||
@@ -29,11 +29,7 @@ final class SidebarExpandedContainers: ObservableObject {
|
||||
|
||||
subscript(_ containerID: ContainerIdentifier) -> Bool {
|
||||
get {
|
||||
if expandedTable.contains(containerID) {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
return expandedTable.contains(containerID)
|
||||
}
|
||||
set(newValue) {
|
||||
if newValue {
|
||||
|
||||
@@ -11,13 +11,14 @@ import Account
|
||||
|
||||
struct SidebarItemView: View {
|
||||
|
||||
@StateObject var feedImageLoader = FeedImageLoader()
|
||||
@StateObject var feedIconImageLoader = FeedIconImageLoader()
|
||||
var sidebarItem: SidebarItem
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
if let image = feedImageLoader.image {
|
||||
if let image = feedIconImageLoader.image {
|
||||
IconImageView(iconImage: image)
|
||||
.frame(width: 20, height: 20, alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/)
|
||||
}
|
||||
Text(verbatim: sidebarItem.nameForDisplay)
|
||||
Spacer()
|
||||
@@ -27,7 +28,7 @@ struct SidebarItemView: View {
|
||||
}
|
||||
.onAppear {
|
||||
if let feed = sidebarItem.feed {
|
||||
feedImageLoader.loadImage(for: feed)
|
||||
feedIconImageLoader.loadImage(for: feed)
|
||||
}
|
||||
}.contextMenu(menuItems: {
|
||||
menuItems
|
||||
|
||||
@@ -11,7 +11,6 @@ import RSCore
|
||||
import Account
|
||||
|
||||
protocol SidebarModelDelegate: class {
|
||||
func sidebarSelectionDidChange(_: SidebarModel, feeds: [Feed]?)
|
||||
func unreadCount(for: Feed) -> Int
|
||||
}
|
||||
|
||||
|
||||
@@ -16,8 +16,6 @@ struct SidebarView: View {
|
||||
@StateObject private var expandedContainers = SidebarExpandedContainers()
|
||||
@EnvironmentObject private var sidebarModel: SidebarModel
|
||||
|
||||
// @State private var selected = Set<FeedIdentifier>()
|
||||
|
||||
var body: some View {
|
||||
List() {
|
||||
ForEach(sidebarModel.sidebarItems) { sidebarItem in
|
||||
@@ -27,13 +25,19 @@ struct SidebarView: View {
|
||||
if let containerID = sidebarItem.containerID {
|
||||
DisclosureGroup(isExpanded: $expandedContainers[containerID]) {
|
||||
ForEach(sidebarItem.children) { sidebarItem in
|
||||
SidebarItemView(sidebarItem: sidebarItem)
|
||||
NavigationLink(destination: (TimelineContainerView(feed: sidebarItem.feed))) {
|
||||
SidebarItemView(sidebarItem: sidebarItem)
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
SidebarItemView(sidebarItem: sidebarItem)
|
||||
NavigationLink(destination: (TimelineContainerView(feed: sidebarItem.feed))) {
|
||||
SidebarItemView(sidebarItem: sidebarItem)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
SidebarItemView(sidebarItem: sidebarItem)
|
||||
NavigationLink(destination: (TimelineContainerView(feed: sidebarItem.feed))) {
|
||||
SidebarItemView(sidebarItem: sidebarItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
//
|
||||
// Image-Extensions.swift
|
||||
// NetNewsWire
|
||||
//
|
||||
// Created by Maurice Parker on 7/1/20.
|
||||
// Copyright © 2020 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import RSCore
|
||||
|
||||
extension Image {
|
||||
|
||||
init(rsImage: RSImage) {
|
||||
#if os(macOS)
|
||||
self = Image(nsImage: rsImage)
|
||||
#endif
|
||||
#if os(iOS)
|
||||
self = Image(uiImage: rsImage)
|
||||
#endif
|
||||
}
|
||||
|
||||
}
|
||||
@@ -7,28 +7,26 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Account
|
||||
|
||||
struct TimelineContainerView: View {
|
||||
|
||||
@EnvironmentObject private var sceneModel: SceneModel
|
||||
@StateObject private var timelineModel = TimelineModel()
|
||||
var feed: Feed? = nil
|
||||
|
||||
@ViewBuilder var body: some View {
|
||||
TimelineView()
|
||||
.environmentObject(timelineModel)
|
||||
.listStyle(SidebarListStyle())
|
||||
.onAppear {
|
||||
sceneModel.timelineModel = timelineModel
|
||||
timelineModel.delegate = sceneModel
|
||||
timelineModel.rebuildTimelineItems()
|
||||
}
|
||||
if let feed = feed {
|
||||
TimelineView()
|
||||
.environmentObject(timelineModel)
|
||||
.onAppear {
|
||||
sceneModel.timelineModel = timelineModel
|
||||
timelineModel.delegate = sceneModel
|
||||
timelineModel.rebuildTimelineItems(feed)
|
||||
}
|
||||
} else {
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
struct TimelineContainerView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
TimelineContainerView()
|
||||
.environmentObject(SceneModel())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,9 +9,36 @@
|
||||
import SwiftUI
|
||||
import Articles
|
||||
|
||||
enum TimelineItemStatus {
|
||||
case showStar
|
||||
case showUnread
|
||||
case showNone
|
||||
}
|
||||
|
||||
struct TimelineItem: Identifiable {
|
||||
|
||||
var id: String
|
||||
var article: Article
|
||||
|
||||
var id: String {
|
||||
return article.articleID
|
||||
}
|
||||
|
||||
var status: TimelineItemStatus {
|
||||
if article.status.starred == true {
|
||||
return .showStar
|
||||
}
|
||||
if article.status.read == false {
|
||||
return .showUnread
|
||||
}
|
||||
return .showNone
|
||||
}
|
||||
|
||||
var byline: String {
|
||||
return article.webFeed?.nameForDisplay ?? ""
|
||||
}
|
||||
|
||||
var dateTimeString: String {
|
||||
return ArticleStringFormatter.dateString(article.logicalDatePublished)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
43
Multiplatform/Shared/Timeline/TimelineItemStatusView.swift
Normal file
43
Multiplatform/Shared/Timeline/TimelineItemStatusView.swift
Normal file
@@ -0,0 +1,43 @@
|
||||
//
|
||||
// TimelineItemStatusView.swift
|
||||
// NetNewsWire
|
||||
//
|
||||
// Created by Maurice Parker on 7/1/20.
|
||||
// Copyright © 2020 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct TimelineItemStatusView: View {
|
||||
|
||||
var status: TimelineItemStatus
|
||||
|
||||
@ViewBuilder var statusView: some View {
|
||||
switch status {
|
||||
case .showUnread:
|
||||
AppAssets.timelineUnread
|
||||
.resizable()
|
||||
.frame(width: 8, height: 8, alignment: .center)
|
||||
.padding(.all, 2)
|
||||
.foregroundColor(.accentColor)
|
||||
case .showStar:
|
||||
AppAssets.timelineStarred
|
||||
.resizable()
|
||||
.frame(width: 10, height: 10, alignment: .center)
|
||||
.foregroundColor(.yellow)
|
||||
case .showNone:
|
||||
AppAssets.timelineUnread
|
||||
.resizable()
|
||||
.frame(width: 8, height: 8, alignment: .center)
|
||||
.padding(.all, 2)
|
||||
.opacity(0)
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
statusView
|
||||
.padding(.top, 4)
|
||||
.padding(.leading, 4)
|
||||
}
|
||||
|
||||
}
|
||||
65
Multiplatform/Shared/Timeline/TimelineItemView.swift
Normal file
65
Multiplatform/Shared/Timeline/TimelineItemView.swift
Normal file
@@ -0,0 +1,65 @@
|
||||
//
|
||||
// TimelineItemView.swift
|
||||
// NetNewsWire
|
||||
//
|
||||
// Created by Maurice Parker on 7/1/20.
|
||||
// Copyright © 2020 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct TimelineItemView: View {
|
||||
|
||||
@StateObject var articleIconImageLoader = ArticleIconImageLoader()
|
||||
var timelineItem: TimelineItem
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
HStack(alignment: .top) {
|
||||
TimelineItemStatusView(status: timelineItem.status)
|
||||
if let image = articleIconImageLoader.image {
|
||||
IconImageView(iconImage: image)
|
||||
.frame(width: AppDefaults.timelineIconSize.size.width, height: AppDefaults.timelineIconSize.size.height, alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/)
|
||||
}
|
||||
VStack {
|
||||
Text(verbatim: timelineItem.article.title ?? "N/A")
|
||||
.fontWeight(.semibold)
|
||||
.lineLimit(AppDefaults.timelineNumberOfLines)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(.trailing, 4)
|
||||
Spacer()
|
||||
HStack {
|
||||
Text(verbatim: timelineItem.byline)
|
||||
.lineLimit(1)
|
||||
.truncationMode(.tail)
|
||||
.font(.footnote)
|
||||
.foregroundColor(.secondary)
|
||||
Spacer()
|
||||
Text(verbatim: timelineItem.dateTimeString)
|
||||
.lineLimit(1)
|
||||
.font(.footnote)
|
||||
.foregroundColor(.secondary)
|
||||
.padding(.trailing, 4)
|
||||
}
|
||||
}
|
||||
}
|
||||
Divider()
|
||||
}
|
||||
.onAppear {
|
||||
articleIconImageLoader.loadImage(for: timelineItem.article)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct TimelineItemView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
Group {
|
||||
TimelineItemView(timelineItem: TimelineItem(article: PreviewArticles.basicRead))
|
||||
.frame(maxWidth: 250)
|
||||
TimelineItemView(timelineItem: TimelineItem(article: PreviewArticles.basicUnread))
|
||||
.frame(maxWidth: 250)
|
||||
TimelineItemView(timelineItem: TimelineItem(article: PreviewArticles.basicStarred))
|
||||
.frame(maxWidth: 250)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@
|
||||
import Foundation
|
||||
import RSCore
|
||||
import Account
|
||||
import Articles
|
||||
|
||||
protocol TimelineModelDelegate: class {
|
||||
func timelineRequestedWebFeedSelection(_: TimelineModel, webFeed: WebFeed)
|
||||
@@ -20,19 +21,103 @@ class TimelineModel: ObservableObject {
|
||||
|
||||
@Published var timelineItems = [TimelineItem]()
|
||||
|
||||
private var feeds = [Feed]()
|
||||
private var fetchSerialNumber = 0
|
||||
private let fetchRequestQueue = FetchRequestQueue()
|
||||
private var exceptionArticleFetcher: ArticleFetcher?
|
||||
private var isReadFiltered = false
|
||||
|
||||
private var articles = [Article]()
|
||||
|
||||
private var sortDirection = AppDefaults.timelineSortDirection {
|
||||
didSet {
|
||||
if sortDirection != oldValue {
|
||||
sortParametersDidChange()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var groupByFeed = AppDefaults.timelineGroupByFeed {
|
||||
didSet {
|
||||
if groupByFeed != oldValue {
|
||||
sortParametersDidChange()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init() {
|
||||
}
|
||||
|
||||
// MARK: API
|
||||
|
||||
func rebuildTimelineItems() {
|
||||
|
||||
func rebuildTimelineItems(_ feed: Feed) {
|
||||
feeds = [feed]
|
||||
fetchAndReplaceArticlesAsync()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: Private
|
||||
|
||||
private extension TimelineModel {
|
||||
|
||||
func sortParametersDidChange() {
|
||||
performBlockAndRestoreSelection {
|
||||
let unsortedArticles = Set(articles)
|
||||
replaceArticles(with: unsortedArticles)
|
||||
}
|
||||
}
|
||||
|
||||
func performBlockAndRestoreSelection(_ block: (() -> Void)) {
|
||||
// let savedSelection = selectedArticleIDs()
|
||||
block()
|
||||
// restoreSelection(savedSelection)
|
||||
}
|
||||
|
||||
// MARK: Article Fetching
|
||||
|
||||
func fetchAndReplaceArticlesAsync() {
|
||||
cancelPendingAsyncFetches()
|
||||
|
||||
var fetchers = feeds as [ArticleFetcher]
|
||||
if let fetcher = exceptionArticleFetcher {
|
||||
fetchers.append(fetcher)
|
||||
exceptionArticleFetcher = nil
|
||||
}
|
||||
|
||||
fetchUnsortedArticlesAsync(for: fetchers) { [weak self] (articles) in
|
||||
self?.replaceArticles(with: articles)
|
||||
}
|
||||
}
|
||||
|
||||
func cancelPendingAsyncFetches() {
|
||||
fetchSerialNumber += 1
|
||||
fetchRequestQueue.cancelAllRequests()
|
||||
}
|
||||
|
||||
func fetchUnsortedArticlesAsync(for representedObjects: [Any], completion: @escaping ArticleSetBlock) {
|
||||
// The callback will *not* be called if the fetch is no longer relevant — that is,
|
||||
// if it’s been superseded by a newer fetch, or the timeline was emptied, etc., it won’t get called.
|
||||
precondition(Thread.isMainThread)
|
||||
cancelPendingAsyncFetches()
|
||||
let fetchOperation = FetchRequestOperation(id: fetchSerialNumber, readFilter: isReadFiltered ?? true, representedObjects: representedObjects) { [weak self] (articles, operation) in
|
||||
precondition(Thread.isMainThread)
|
||||
guard !operation.isCanceled, let strongSelf = self, operation.id == strongSelf.fetchSerialNumber else {
|
||||
return
|
||||
}
|
||||
completion(articles)
|
||||
}
|
||||
fetchRequestQueue.add(fetchOperation)
|
||||
}
|
||||
|
||||
func replaceArticles(with unsortedArticles: Set<Article>) {
|
||||
articles = Array(unsortedArticles).sortedByDate(sortDirection, groupByFeed: groupByFeed)
|
||||
timelineItems = articles.map { TimelineItem(article: $0) }
|
||||
|
||||
// TODO: Update unread counts and other item done in didSet on AppKit
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Notifications
|
||||
|
||||
}
|
||||
|
||||
@@ -9,13 +9,23 @@
|
||||
import SwiftUI
|
||||
|
||||
struct TimelineView: View {
|
||||
var body: some View {
|
||||
Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/)
|
||||
}
|
||||
}
|
||||
|
||||
@EnvironmentObject private var timelineModel: TimelineModel
|
||||
|
||||
struct TimelineView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
TimelineView()
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
LazyVStack() {
|
||||
ForEach(timelineModel.timelineItems) { timelineItem in
|
||||
TimelineItemView(timelineItem: timelineItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// var body: some View {
|
||||
// List(timelineModel.timelineItems) { timelineItem in
|
||||
// TimelineItemView(timelineItem: timelineItem)
|
||||
// }
|
||||
// }
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user