1

I have this issue with pull to refresh & navigation bar. The problem is that the navigation title jumps back to top as soon as refresh is done(without user actually releasing pull to refresh).

Is it possible to avoid? Don't remember having this issue with UIKit.

Here is what happens:

enter image description here

Here is my ViewModel code that handles the data refresh:

    @Published var alerts: [Alert] = []

    ....

    func refresh() async {
        do {
            let result = try await repository.fetchAlerts(page: 1, perPage: 20)
            alerts = result.items
        } catch {
            showErrorBanner(error)
        }
    }

View:

struct AlertsView: View {
    @StateObject private var viewModel = AlertsViewModel()
    @StateObject private var navigationManager = NavigationManager<AppRoutes>()

    var body: some View {
        NavigationStack(path: $navigationManager.path) {
            ScrollView {
                LazyVStack(alignment: .leading) {
                    ForEach(viewModel.alerts) { alert in
                        Text(alert.body)
                    }
                }
            }
            .refreshable {
                await viewModel.refresh()
            }
            .navigationTitle("Alerts")
            .navigationBarTitleDisplayMode(.large)
        }
        .environmentObject(navigationManager)
    }
}

Thought maybe it's just my frontend code messing with it, but it happens even with a simple layout(without any styles): NavigationStack - ScrollView - LazyVStack - ForEach - Text.

EDIT: This issue only happens with navigationBarTitleDisplayMode set to .large. .inline works fine...

Min. deployment target is set to iOS16.

EDIT: Looks like if try await Task.sleep(for: .seconds(1)) is added before the API call it kinda works fine.

    @Published var alerts: [Alert] = []

    ....

    func refresh() async {
        do {
            try await Task.sleep(for: .seconds(1)) <--- HERE

            let result = try await repository.fetchAlerts(page: 1, perPage: 20)
            alerts = result.items
        } catch {
            showErrorBanner(error)
        }
    }
kironet
  • 766
  • 2
  • 11
  • 27
  • I have this exact glitch. For me it seems to only appear when I mutate state or published properties from inside `refreshable` (but otherwise the callback is pointless). I've also found that it somehow interacts with `NavigationStack` / `navigationDestination`. Also, it seems to me as if simultaneously `Update NavigationAuthority bound path tried to update multiple times per frame.` pops up. – paulgessinger Mar 14 '23 at 21:14
  • Yeah, the glitch only happens when you mutate states in .refreshable... I filed a feedback for this: `FB12054304`. – kironet Mar 14 '23 at 23:50
  • Looks like if you add `try await Task.sleep(for: .seconds(1))` before the actual request it kinda works fine. – kironet May 08 '23 at 03:48

1 Answers1

0

I was having issue with pull to refresh (since I had a custom header) and was able to find a custom pull to refresh that works really well. Think it could be a glitch in the SwiftUI refreshable modifier.

import SwiftUI

struct PullToRefresh: View {
    
    var coordinateSpaceName: String
    var onRefresh: ()->Void
    
    @State var needRefresh: Bool = false
    
    var body: some View {
        GeometryReader { geo in
            if (geo.frame(in: .named(coordinateSpaceName)).midY > 50) {
                Spacer()
                    .onAppear {
                        needRefresh = true
                    }
            } else if (geo.frame(in: .named(coordinateSpaceName)).maxY < 10) {
                Spacer()
                    .onAppear {
                        if needRefresh {
                            needRefresh = false
                            onRefresh()
                        }
                    }
            }
            HStack {
                Spacer()
                if needRefresh {
                    ProgressView()
                }
// You can uncomment this if you want to see where this is occurring in your View 
//                else {
//                    Text("⬇️")
//                }
                Spacer()
            }
        }.padding(.top, -50)
    }
}

I added it to my code by adding the .coordinateSpace(name: "pullToRefresh") and then inserting the custom PullToRefresh inside of my ScrollView

 PullToRefresh(coordinateSpaceName: "pullToRefresh") {
 // These functions are the ones I want my viewModel to execute. For example, I want to shuffle the images that appear on the home page when the page is refreshed. 
                            viewModel.styles.shuffle()
                            viewModel.posts.shuffle()
                            viewModel.brands.shuffle()
                        }
katann0401
  • 11
  • 2