Fix pull-to-refresh scroll offset not resetting in InstanceBrowseView

Move .refreshable from the outer GeometryReader onto the ScrollView
itself so SwiftUI can properly coordinate the scroll offset bounce-back.
The ScrollView was inside an .overlay() which doesn't participate in
the parent's layout system, breaking the offset reset.

Closes #917
This commit is contained in:
Arkadiusz Fal
2026-02-12 01:21:54 +01:00
parent 288113d177
commit 7ac45b46a3

View File

@@ -36,6 +36,7 @@ struct InstanceBrowseView: View {
@State private var feedPage = 1 @State private var feedPage = 1
@State private var hasMoreFeedResults = true @State private var hasMoreFeedResults = true
@State private var isLoadingMoreFeed = false @State private var isLoadingMoreFeed = false
@State private var contentLoadTask: Task<Void, Never>?
@State private var feedLoadedVideoCount = 0 // Track count when last load was triggered @State private var feedLoadedVideoCount = 0 // Track count when last load was triggered
// View options (persisted per instance) // View options (persisted per instance)
@@ -101,10 +102,10 @@ struct InstanceBrowseView: View {
var body: some View { var body: some View {
let backgroundStyle: ListBackgroundStyle = listStyle == .inset ? .grouped : .plain let backgroundStyle: ListBackgroundStyle = listStyle == .inset ? .grouped : .plain
GeometryReader { geometry in
backgroundStyle.color backgroundStyle.color
.ignoresSafeArea() .ignoresSafeArea()
.overlay( .overlay(
GeometryReader { geometry in
ScrollView { ScrollView {
VStack(spacing: 0) { VStack(spacing: 0) {
// Tab picker (hidden during search) // Tab picker (hidden during search)
@@ -186,11 +187,14 @@ struct InstanceBrowseView: View {
} }
} }
} }
.refreshable {
await startContentLoad(forceRefresh: true)
}
)
.onChange(of: geometry.size.width, initial: true) { _, newWidth in .onChange(of: geometry.size.width, initial: true) { _, newWidth in
viewWidth = newWidth viewWidth = newWidth
} }
} }
)
.navigationTitle(instance.displayName) .navigationTitle(instance.displayName)
#if !os(tvOS) #if !os(tvOS)
.toolbarTitleDisplayMode(.inlineLarge) .toolbarTitleDisplayMode(.inlineLarge)
@@ -246,16 +250,13 @@ struct InstanceBrowseView: View {
// Load watch entries for hide watched feature // Load watch entries for hide watched feature
loadWatchEntries() loadWatchEntries()
await loadContent() await startContentLoad()
} }
.onReceive(NotificationCenter.default.publisher(for: .watchHistoryDidChange)) { _ in .onReceive(NotificationCenter.default.publisher(for: .watchHistoryDidChange)) { _ in
loadWatchEntries() loadWatchEntries()
} }
.onChange(of: selectedTab) { _, _ in .onChange(of: selectedTab) { _, _ in
Task { await loadContent() } Task { await startContentLoad() }
}
.refreshable {
await loadContent(forceRefresh: true)
} }
#if os(iOS) #if os(iOS)
.searchable( .searchable(
@@ -596,7 +597,7 @@ struct InstanceBrowseView: View {
Text(error) Text(error)
} actions: { } actions: {
Button(String(localized: "common.retry")) { Button(String(localized: "common.retry")) {
Task { await loadContent(forceRefresh: true) } Task { await startContentLoad(forceRefresh: true) }
} }
.buttonStyle(.bordered) .buttonStyle(.bordered)
} }
@@ -821,7 +822,17 @@ struct InstanceBrowseView: View {
watchEntriesMap = appEnvironment?.dataManager.watchEntriesMap() ?? [:] watchEntriesMap = appEnvironment?.dataManager.watchEntriesMap() ?? [:]
} }
private func loadContent(forceRefresh: Bool = false) async { private func startContentLoad(forceRefresh: Bool = false) async {
// Cancel any in-flight load before starting a new one
contentLoadTask?.cancel()
let task = Task {
await performLoadContent(forceRefresh: forceRefresh)
}
contentLoadTask = task
await task.value
}
private func performLoadContent(forceRefresh: Bool = false) async {
guard let appEnvironment else { guard let appEnvironment else {
errorMessage = "App not initialized" errorMessage = "App not initialized"
isLoading = false isLoading = false
@@ -842,7 +853,7 @@ struct InstanceBrowseView: View {
return return
} }
isLoading = true isLoading = !hasData // Only show loading spinner when no existing data
errorMessage = nil errorMessage = nil
do { do {
@@ -865,8 +876,8 @@ struct InstanceBrowseView: View {
if forceRefresh { if forceRefresh {
feedPage = 1 feedPage = 1
hasMoreFeedResults = true hasMoreFeedResults = true
feedVideos = [] // Don't clear feedVideos here keep old data visible
selectedFeedChannelID = nil // until the API call succeeds and replaces it.
} }
// Load subscriptions and feed based on instance type // Load subscriptions and feed based on instance type
@@ -937,6 +948,10 @@ struct InstanceBrowseView: View {
let playlists = try await api.userPlaylists(instance: instance, sid: credential) let playlists = try await api.userPlaylists(instance: instance, sid: credential)
userPlaylists = playlists userPlaylists = playlists
} }
} catch is CancellationError {
// Task was cancelled (e.g., by SwiftUI during pull-to-refresh) don't show error
} catch let error as APIError where error == .cancelled {
// HTTP request was cancelled don't show error
} catch { } catch {
errorMessage = error.localizedDescription errorMessage = error.localizedDescription
} }