NetNewsWire/Multiplatform/Shared/Timeline/TimelineView.swift

135 lines
4.3 KiB
Swift

//
// TimelineView.swift
// NetNewsWire
//
// Created by Maurice Parker on 6/30/20.
// Copyright © 2020 Ranchero Software. All rights reserved.
//
import SwiftUI
struct TimelineView: View {
@Binding var timelineItems: TimelineItems
@Binding var isReadFiltered: Bool?
@EnvironmentObject private var timelineModel: TimelineModel
@State private var timelineItemFrames = [String: CGRect]()
@ViewBuilder var body: some View {
GeometryReader { geometryReaderProxy in
#if os(macOS)
VStack {
HStack {
TimelineSortOrderView()
Spacer()
Button (action: {
if let filtered = isReadFiltered {
timelineModel.changeReadFilterSubject.send(!filtered)
}
}, label: {
if isReadFiltered ?? false {
AppAssets.filterActiveImage
} else {
AppAssets.filterInactiveImage
}
})
.hidden(isReadFiltered == nil)
.padding(.top, 8).padding(.trailing)
.buttonStyle(PlainButtonStyle())
.help(isReadFiltered ?? false ? "Show Read Articles" : "Filter Read Articles")
}
ScrollViewReader { scrollViewProxy in
List(timelineItems.items, 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))
}
.id(timelineModel.listID)
.onPreferenceChange(TimelineItemFramePreferenceKey.self) { preferences in
for pref in preferences {
timelineItemFrames[pref.articleID] = pref.frame
}
}
.onChange(of: timelineModel.selectedTimelineItemIDs) { selectedArticleIDs in
let proxyFrame = geometryReaderProxy.frame(in: .global)
for articleID in selectedArticleIDs {
if let itemFrame = timelineItemFrames[articleID] {
if itemFrame.minY < proxyFrame.minY + 3 || itemFrame.maxY > proxyFrame.maxY - 35 {
withAnimation {
scrollViewProxy.scrollTo(articleID, anchor: .center)
}
}
}
}
}
}
}
.navigationTitle(Text(verbatim: timelineModel.nameForDisplay))
#else
ScrollViewReader { scrollViewProxy in
List(timelineItems.items) { timelineItem in
ZStack {
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.selectedTimelineItemID) {
EmptyView()
}.buttonStyle(PlainButtonStyle())
}
}
.id(timelineModel.listID)
.onPreferenceChange(TimelineItemFramePreferenceKey.self) { preferences in
for pref in preferences {
timelineItemFrames[pref.articleID] = pref.frame
}
}
.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 {
withAnimation {
scrollViewProxy.scrollTo(articleID, anchor: .center)
}
}
}
}
}
.navigationBarTitle(Text(verbatim: timelineModel.nameForDisplay), displayMode: .inline)
#endif
}
}
}
struct TimelineItemFramePreferenceKey: PreferenceKey {
typealias Value = [TimelineItemFramePreference]
static var defaultValue: [TimelineItemFramePreference] = []
static func reduce(value: inout [TimelineItemFramePreference], nextValue: () -> [TimelineItemFramePreference]) {
value.append(contentsOf: nextValue())
}
}
struct TimelineItemFramePreference: Equatable {
let articleID: String
let frame: CGRect
}
struct TimelineItemFramePreferenceView: View {
let timelineItem: TimelineItem
var body: some View {
GeometryReader { proxy in
Rectangle()
.fill(Color.clear)
.preference(key: TimelineItemFramePreferenceKey.self,
value: [TimelineItemFramePreference(articleID: timelineItem.article.articleID, frame: proxy.frame(in: .global))])
}
}
}