Scroll into view when a Timeline Item is selected if it isn't completely visible

This commit is contained in:
Maurice Parker 2020-07-22 20:40:04 -05:00
parent f9cd15970f
commit 71a6f03fd6
1 changed files with 80 additions and 13 deletions

View File

@ -11,9 +11,10 @@ import SwiftUI
struct TimelineView: View { struct TimelineView: View {
@EnvironmentObject private var timelineModel: TimelineModel @EnvironmentObject private var timelineModel: TimelineModel
@State private var timelineItemFrames = [String: CGRect]()
@ViewBuilder var body: some View { @ViewBuilder var body: some View {
GeometryReader { proxy in GeometryReader { geometryReaderProxy in
#if os(macOS) #if os(macOS)
VStack { VStack {
HStack { HStack {
@ -35,22 +36,60 @@ struct TimelineView: View {
.buttonStyle(PlainButtonStyle()) .buttonStyle(PlainButtonStyle())
.help(timelineModel.isReadFiltered ?? false ? "Show Read Articles" : "Filter Read Articles") .help(timelineModel.isReadFiltered ?? false ? "Show Read Articles" : "Filter Read Articles")
} }
List(timelineModel.timelineItems, selection: $timelineModel.selectedArticleIDs) { timelineItem in ScrollViewReader { scrollViewProxy in
let selected = timelineModel.selectedArticleIDs.contains(timelineItem.article.articleID) List(timelineModel.timelineItems, selection: $timelineModel.selectedArticleIDs) { timelineItem in
TimelineItemView(selected: selected, width: proxy.size.width, timelineItem: timelineItem) 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)) .navigationTitle(Text(verbatim: timelineModel.nameForDisplay))
#else #else
List(timelineModel.timelineItems) { timelineItem in ScrollViewReader { scrollViewProxy in
ZStack { List(timelineModel.timelineItems) { timelineItem in
let selected = timelineModel.selectedArticleID == timelineItem.article.articleID ZStack {
TimelineItemView(selected: selected, width: proxy.size.width, timelineItem: timelineItem) let selected = timelineModel.selectedArticleID == timelineItem.article.articleID
NavigationLink(destination: ArticleContainerView(), TimelineItemView(selected: selected, width: geometryReaderProxy.size.width, timelineItem: timelineItem)
tag: timelineItem.article.articleID, .background(TimelineItemFramePreferenceView(timelineItem: timelineItem))
selection: $timelineModel.selectedArticleID) { NavigationLink(destination: ArticleContainerView(),
EmptyView() tag: timelineItem.article.articleID,
}.buttonStyle(PlainButtonStyle()) 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) .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))])
}
}
}