2

I have a SwiftUI List with .refreshable modifier that works perfectly when users manually pull to refresh.

However, I need to programmatically trigger the same refresh animation when certain conditions change (like triggered by a Button action).

When refresh() is triggered by Button, I want to show the same pull-to-refresh loading animation that appears when users manually pull down the refreshable. Currently, it seems only manually pull-to-refresh can triggers the animation?

struct ContentView: View {
    @State private var refreshTimes = 0
    var body: some View {
        List{
            Text("refresh times: \(refreshTimes)")
            Button("click me to refresh") {
                refresh()
            }
        }
        .refreshable {
            refresh()
        }
    }
    
    func refresh() {
        refreshTimes += 1
        print("refresh at :\(Date())")
    }
}

Question: Is there a way to programmatically trigger SwiftUI's native .refreshable loading animation, or do I need to implement a custom refresh indicator that mimics the native appearance?

2
  • @koen , it's totally different question . The answer you post is created at 2019 and its solution is a custom view. But my question is about system's refreshable which is bring out in 2021. And my question is not how to add it to refresh , but how to trigger animation by code not by manually dragging Commented Aug 26 at 9:20
  • this link may be of help RefreshAction Commented Aug 26 at 9:34

1 Answer 1

1

If you are fine with introspecting the underlying UIRefreshControl (i.e. not forward compatible), you can manually refresh the UIRefreshControl using this technique.

@MainActor
class RefreshController: ObservableObject {
    weak var list: UIScrollView?
    
    func refresh() {
        guard let list, let refreshControl = list.refreshControl else { return }
        list.setContentOffset(CGPoint(x: 0, y: list.contentOffset.y - (refreshControl.frame.height)), animated: true)
        refreshControl.beginRefreshing()
        refreshControl.sendActions(for: .valueChanged)
    }
}
@StateObject private var refreshController = RefreshController()
var body: some View {
    List {
        Text("Foo")
        Button("Refresh") {
            refreshController.refresh()
        }
    }
    .refreshable {
        do {
            try await Task.sleep(for: .seconds(1))
            print("Refreshed")
        } catch {
            print("Cancelled")
        }
    }
    // this will only work on the versions you specify here
    .introspect(.list, on: .iOS(.v16, .v17, .v18)) { list in
        refreshController.list = list
    }
}

Otherwise, you will need to recreate your own refresh control. Here is an example:

struct PullToRefresh: ViewModifier {
    @Binding var isRefreshing: Bool
    let onRefresh: () async -> Void
    
    func body(content: Content) -> some View {
        content
            .onScrollGeometryChange(for: CGFloat.self) {
                $0.contentOffset.y + $0.contentInsets.top
            } action: { oldValue, newValue in
                if newValue < -50, !isRefreshing {
                    isRefreshing = true
                }
            }
            // this moves the srollable content out of the way,
            // but it is not animated when pulling down
            // .safeAreaPadding(.top, isRefreshing ? 50 : 0)
            .overlay(alignment: .top) {
                HStack {
                    Spacer()
                    if isRefreshing {
                        ProgressView().controlSize(.extraLarge)
                    }
                    Spacer()
                }
                .task(id: isRefreshing) {
                    guard isRefreshing else { return }
                    await onRefresh()
                    isRefreshing = false
                }
            }
            .animation(.default, value: isRefreshing)
    }
}

This view modifier can be manually triggered setting the isRefreshing binding to true.

@State private var isRefreshing = false
var body: some View {
    List {
        Text("Foo")
        Button("Refresh") {
            isRefreshing = true
        }
    }
    .modifier(PullToRefresh(isRefreshing: $isRefreshing) {
        do {
            try await Task.sleep(for: .seconds(1))
            print("Refreshed")
        } catch {
            print("Cancelled")
        }
    })
}
Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.