mirror of
https://github.com/Dimillian/IceCubesApp.git
synced 2024-12-23 07:36:48 +00:00
Cleanup + use ScrollPoxy
This commit is contained in:
parent
576b52e8c8
commit
9ec6a4ef66
3 changed files with 26 additions and 103 deletions
|
@ -1,43 +0,0 @@
|
|||
import Models
|
||||
import Nuke
|
||||
import Observation
|
||||
import SwiftUI
|
||||
import UIKit
|
||||
|
||||
@Observable final class TimelineMediaPrefetcher: NSObject, UICollectionViewDataSourcePrefetching {
|
||||
private let prefetcher = ImagePrefetcher()
|
||||
|
||||
weak var viewModel: TimelineViewModel?
|
||||
|
||||
func collectionView(_: UICollectionView, prefetchItemsAt indexPaths: [IndexPath]) {
|
||||
let imageURLs = getImageURLs(for: indexPaths)
|
||||
prefetcher.startPrefetching(with: imageURLs)
|
||||
}
|
||||
|
||||
func collectionView(_: UICollectionView, cancelPrefetchingForItemsAt indexPaths: [IndexPath]) {
|
||||
let imageURLs = getImageURLs(for: indexPaths)
|
||||
prefetcher.stopPrefetching(with: imageURLs)
|
||||
}
|
||||
|
||||
private func getImageURLs(for indexPaths: [IndexPath]) -> [URL] {
|
||||
guard let viewModel, case let .display(statuses, _) = viewModel.statusesState else {
|
||||
return []
|
||||
}
|
||||
return indexPaths.compactMap {
|
||||
$0.row < statuses.endIndex ? statuses[$0.row] : nil
|
||||
}.flatMap(getImages)
|
||||
}
|
||||
}
|
||||
|
||||
private func getImages(for status: Status) -> [URL] {
|
||||
var urls = status.mediaAttachments.compactMap {
|
||||
if $0.supportedType == .image {
|
||||
return status.mediaAttachments.count > 1 ? $0.previewUrl ?? $0.url : $0.url
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if let url = status.card?.image {
|
||||
urls.append(url)
|
||||
}
|
||||
return urls
|
||||
}
|
|
@ -6,7 +6,6 @@ import Network
|
|||
import StatusKit
|
||||
import SwiftData
|
||||
import SwiftUI
|
||||
import SwiftUIIntrospect
|
||||
|
||||
@MainActor
|
||||
public struct TimelineView: View {
|
||||
|
@ -20,11 +19,9 @@ public struct TimelineView: View {
|
|||
@Environment(RouterPath.self) private var routerPath
|
||||
|
||||
@State private var viewModel = TimelineViewModel()
|
||||
@State private var prefetcher = TimelineMediaPrefetcher()
|
||||
@State private var contentFilter = TimelineContentFilter.shared
|
||||
|
||||
@State private var wasBackgrounded: Bool = false
|
||||
@State private var collectionView: UICollectionView?
|
||||
|
||||
@Binding var timeline: TimelineFilter
|
||||
@Binding var pinnedFilters: [TimelineFilter]
|
||||
|
@ -67,18 +64,6 @@ public struct TimelineView: View {
|
|||
.if(canFilterTimeline && !pinnedFilters.isEmpty) { view in
|
||||
view.toolbarBackground(.hidden, for: .navigationBar)
|
||||
}
|
||||
.onChange(of: viewModel.scrollToIndex) { _, newValue in
|
||||
if let collectionView,
|
||||
let newValue,
|
||||
let rows = collectionView.dataSource?.collectionView(collectionView, numberOfItemsInSection: 0),
|
||||
rows > newValue
|
||||
{
|
||||
collectionView.scrollToItem(at: .init(row: newValue, section: 0),
|
||||
at: .top,
|
||||
animated: false)
|
||||
viewModel.scrollToIndex = nil
|
||||
}
|
||||
}
|
||||
.toolbar {
|
||||
toolbarTitleView
|
||||
toolbarTagGroupButton
|
||||
|
@ -193,25 +178,23 @@ public struct TimelineView: View {
|
|||
.id(client.id)
|
||||
.environment(\.defaultMinListRowHeight, 1)
|
||||
.listStyle(.plain)
|
||||
#if !os(visionOS)
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(theme.primaryBackgroundColor)
|
||||
#endif
|
||||
.introspect(.list, on: .iOS(.v17, .v18)) { (collectionView: UICollectionView) in
|
||||
DispatchQueue.main.async {
|
||||
self.collectionView = collectionView
|
||||
}
|
||||
prefetcher.viewModel = viewModel
|
||||
collectionView.isPrefetchingEnabled = true
|
||||
collectionView.prefetchDataSource = prefetcher
|
||||
#if !os(visionOS)
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(theme.primaryBackgroundColor)
|
||||
#endif
|
||||
.onChange(of: viewModel.scrollToId) { _, newValue in
|
||||
if let newValue {
|
||||
proxy.scrollTo(newValue, anchor: .top)
|
||||
viewModel.scrollToId = nil
|
||||
}
|
||||
.onChange(of: selectedTabScrollToTop) { _, newValue in
|
||||
if newValue == 0, routerPath.path.isEmpty {
|
||||
withAnimation {
|
||||
proxy.scrollTo(ScrollToView.Constants.scrollToTop, anchor: .top)
|
||||
}
|
||||
}
|
||||
.onChange(of: selectedTabScrollToTop) { _, newValue in
|
||||
if newValue == 0, routerPath.path.isEmpty {
|
||||
withAnimation {
|
||||
proxy.scrollTo(ScrollToView.Constants.scrollToTop, anchor: .top)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ import SwiftUI
|
|||
|
||||
@MainActor
|
||||
@Observable class TimelineViewModel {
|
||||
var scrollToIndex: Int?
|
||||
var scrollToId: String?
|
||||
var statusesState: StatusesState = .loading
|
||||
var timeline: TimelineFilter = .home {
|
||||
willSet {
|
||||
|
@ -229,13 +229,11 @@ extension TimelineViewModel: StatusesFetcher {
|
|||
{
|
||||
await datasource.set(cachedStatuses)
|
||||
let statuses = await datasource.getFiltered()
|
||||
if let latestSeenId = await cache.getLatestSeenStatus(for: client, filter: timeline.id)?.first,
|
||||
let index = await datasource.indexOf(statusId: latestSeenId),
|
||||
index > 0
|
||||
if let latestSeenId = await cache.getLatestSeenStatus(for: client, filter: timeline.id)?.first
|
||||
{
|
||||
// Restore cache and scroll to latest seen status.
|
||||
scrollToId = latestSeenId
|
||||
statusesState = .display(statuses: statuses, nextPageState: .hasNextPage)
|
||||
scrollToIndex = index + 1
|
||||
} else {
|
||||
// Restore cache and scroll to top.
|
||||
withAnimation {
|
||||
|
@ -299,6 +297,9 @@ extension TimelineViewModel: StatusesFetcher {
|
|||
}
|
||||
|
||||
private func updateTimelineWithNewStatuses(_ newStatuses: [Status]) async {
|
||||
defer {
|
||||
canStreamEvents = true
|
||||
}
|
||||
let topStatus = await datasource.getFiltered().first
|
||||
await datasource.insert(contentOf: newStatuses, at: 0)
|
||||
if let lastVisible = visibleStatuses.last {
|
||||
|
@ -313,33 +314,15 @@ extension TimelineViewModel: StatusesFetcher {
|
|||
visibleStatuses.contains(where: { $0.id == topStatus.id }),
|
||||
scrollToTopVisible
|
||||
{
|
||||
updateTimelineWithScrollToTop(newStatuses: newStatuses, statuses: statuses, nextPageState: .hasNextPage)
|
||||
scrollToId = topStatus.id
|
||||
statusesState = .display(statuses: statuses, nextPageState: .hasNextPage)
|
||||
} else {
|
||||
updateTimelineWithAnimation(statuses: statuses, nextPageState: .hasNextPage)
|
||||
withAnimation {
|
||||
statusesState = .display(statuses: statuses, nextPageState: .hasNextPage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh the timeline while keeping the scroll position to the top status.
|
||||
private func updateTimelineWithScrollToTop(newStatuses: [Status], statuses: [Status], nextPageState: StatusesState.PagingState) {
|
||||
pendingStatusesObserver.disableUpdate = true
|
||||
statusesState = .display(statuses: statuses, nextPageState: nextPageState)
|
||||
scrollToIndex = newStatuses.count + 1
|
||||
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.pendingStatusesObserver.disableUpdate = false
|
||||
self?.canStreamEvents = true
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh the timeline while keeping the user current position.
|
||||
// It works because a side effect of withAnimation is that it keep scroll position IF the List is not scrolled to the top.
|
||||
private func updateTimelineWithAnimation(statuses: [Status], nextPageState: StatusesState.PagingState) {
|
||||
withAnimation {
|
||||
statusesState = .display(statuses: statuses, nextPageState: nextPageState)
|
||||
canStreamEvents = true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
enum NextPageError: Error {
|
||||
case internalError
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue