From 71a6f03fd6388444a9c11a065dd5e27b00a1109b Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Wed, 22 Jul 2020 20:40:04 -0500 Subject: [PATCH] Scroll into view when a Timeline Item is selected if it isn't completely visible --- .../Shared/Timeline/TimelineView.swift | 93 ++++++++++++++++--- 1 file changed, 80 insertions(+), 13 deletions(-) diff --git a/Multiplatform/Shared/Timeline/TimelineView.swift b/Multiplatform/Shared/Timeline/TimelineView.swift index 2d17f6a7e..9f242ac1b 100644 --- a/Multiplatform/Shared/Timeline/TimelineView.swift +++ b/Multiplatform/Shared/Timeline/TimelineView.swift @@ -11,9 +11,10 @@ import SwiftUI struct TimelineView: View { @EnvironmentObject private var timelineModel: TimelineModel + @State private var timelineItemFrames = [String: CGRect]() @ViewBuilder var body: some View { - GeometryReader { proxy in + GeometryReader { geometryReaderProxy in #if os(macOS) VStack { HStack { @@ -35,22 +36,60 @@ struct TimelineView: View { .buttonStyle(PlainButtonStyle()) .help(timelineModel.isReadFiltered ?? false ? "Show Read Articles" : "Filter Read Articles") } - List(timelineModel.timelineItems, selection: $timelineModel.selectedArticleIDs) { timelineItem in - let selected = timelineModel.selectedArticleIDs.contains(timelineItem.article.articleID) - TimelineItemView(selected: selected, width: proxy.size.width, timelineItem: timelineItem) + ScrollViewReader { scrollViewProxy in + List(timelineModel.timelineItems, selection: $timelineModel.selectedArticleIDs) { timelineItem in + let selected = timelineModel.selectedArticleIDs.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 { + timelineItemFrames[pref.articleID] = pref.frame + } + } + .onChange(of: timelineModel.selectedArticleIDs) { 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 - List(timelineModel.timelineItems) { timelineItem in - ZStack { - let selected = timelineModel.selectedArticleID == timelineItem.article.articleID - TimelineItemView(selected: selected, width: proxy.size.width, timelineItem: timelineItem) - NavigationLink(destination: ArticleContainerView(), - tag: timelineItem.article.articleID, - selection: $timelineModel.selectedArticleID) { - EmptyView() - }.buttonStyle(PlainButtonStyle()) + ScrollViewReader { scrollViewProxy in + List(timelineModel.timelineItems) { timelineItem in + ZStack { + let selected = timelineModel.selectedArticleID == 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) { + EmptyView() + }.buttonStyle(PlainButtonStyle()) + } + } + .onPreferenceChange(TimelineItemFramePreferenceKey.self) { preferences in + for pref in preferences { + timelineItemFrames[pref.articleID] = pref.frame + } + } + .onChange(of: timelineModel.selectedArticleID) { 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) @@ -59,3 +98,31 @@ struct TimelineView: View { } } + +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))]) + } + } +}