iOS client for Grain grain.social
ios photography atproto
7
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix: pull-to-refresh race with scenePhase auto-reload

When returning from background after 5+ minutes, onChange(scenePhase)
fired loadInitial which set isLoading=true. If that request hung on a
stale socket, pull-to-refresh hit the guard and silently returned.
Now loadInitial cancels any in-flight load before starting a new one.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

+23 -16
+23 -16
Grain/ViewModels/FeedViewModel.swift
··· 9 9 10 10 private var cursor: String? 11 11 private var hasMore = true 12 + private var loadTask: Task<Void, Never>? 12 13 private let client: XRPCClient 13 14 private let feedName: String 14 15 private let actor: String? ··· 44 45 } 45 46 46 47 func loadInitial(auth: AuthContext? = nil) async { 47 - guard !isLoading else { return } 48 + loadTask?.cancel() 48 49 isLoading = true 49 50 error = nil 50 51 cursor = nil 51 52 hasMore = true 52 53 53 - do { 54 - let response = try await client.getFeed( 55 - feed: feedName, 56 - actor: actor, 57 - camera: camera, 58 - location: location, 59 - tag: tag, 60 - auth: auth 61 - ) 62 - galleries = response.items ?? [] 63 - cursor = response.cursor 64 - hasMore = response.cursor != nil 65 - } catch { 66 - self.error = error 54 + let task = Task { 55 + do { 56 + let response = try await client.getFeed( 57 + feed: feedName, 58 + actor: actor, 59 + camera: camera, 60 + location: location, 61 + tag: tag, 62 + auth: auth 63 + ) 64 + guard !Task.isCancelled else { return } 65 + galleries = response.items ?? [] 66 + cursor = response.cursor 67 + hasMore = response.cursor != nil 68 + } catch { 69 + guard !Task.isCancelled else { return } 70 + self.error = error 71 + } 72 + isLoading = false 67 73 } 68 - isLoading = false 74 + loadTask = task 75 + await task.value 69 76 } 70 77 71 78 func loadMore(auth: AuthContext? = nil) async {