skip to Main Content

I have a paginated list that has a ProgressView as its last subview. When reaching the bottom of the list the ProgressView triggers a task that loads more results from the remote server and the user can continue scrolling. This works fine the first time (and sometimes the second time), however, after this, the progress view is not visible anymore.

struct SomeView : View {
    @ObservedObject var viewModel : ViewModel = ViewModel ()
    var body : some View {
        List {
            Section {
                ForEach (0...10)
                {
                    i in Text ("(i)")
                    .frame (maxWidth: .infinity, alignment: .center)
                }
            }
            if viewModel.hasMoreToLoad {
                Section {
                    ProgressView ()
                    .frame (maxWidth: .infinity, alignment: .center)
                    .task { await viewModel.loadMore() }
                }
            }
        }
    }
}

class ViewModel : ObservableObject {
    @Published var hasMoreToLoad : Bool = true
    @Published var isLoading : Bool = false
    
    func loadMore ()
        isLoading = true
        async { await Task.sleep(for: Duration.milliseconds(100)) }
        isLoading = false
        hasMoreToLoad = true
    }
}

The same can be observed with a conditional ProgressView within a List

List {
    if viewModel.isLoading {
        Section {
            ProgressView()
              .frame(maxWidth: .infinity, alignment: .center)
        }
    } else {
        Section {
            ForEach(0...10) { i in
                Text("(i)")
                  .frame(maxWidth: .infinity, alignment: .center)
            }
        }
    }
}

2

Answers


  1. There’s a bug in SwiftUI about ProgressView inside List that creates undefined UI behaviour (there are several posts here on SO talking about that).

    If you can’t take your ProgressView outside your List (maybe you just have a UI mockup to follow) I suggest you simply create your own ProgressView:

    struct CustomActivityIndicator: View {
        @State private var isAnimating = false
    
        private var animation: Animation {
            Animation.linear(duration: 1)
                .repeatForever(autoreverses: false)
        }
    
        var body: some View {
            Image("loading")
                .resizable()
                .frame(width: 20, height: 20)
                .rotationEffect(Angle(degrees: isAnimating ? 359 : 0))
                .animation(animation, value: isAnimating)
                .onAppear {
                    isAnimating = true
                }
        }
    }
    

    "loading" is the name of my loading asset, you can use the asset you want (you can even find the same asset used by the Apple activity indicator).

    Now you can use it like in this simple example:

    @MainActor
    class ViewModel: ObservableObject {
        private var numberOfLoading = 5 // just a fake counter to stop loading
        @Published var isLoading = false
        @Published var model = [Int]()
    
        private var hasMoreToLoad: Bool {
            numberOfLoading > 0
        }
    
        init() {
            load()
        }
    
        private func load() {
            guard hasMoreToLoad, !isLoading else { return }
            isLoading = true
            Task {
                try await Task.sleep(for: Duration.seconds(2))
                isLoading = false
    
                let newElements = (0...9).map { _ in Int.random(in: 0...99) }
                model.append(contentsOf: newElements)
                numberOfLoading -= 1 // let's pretend we are consuming some data source
            }
        }
    
        func cellDidRequestLoad(_ index: Int) {
            guard index >= model.count - 1 else { return }
            load()
        }
    }
    
    struct SomeView: View {
        @StateObject private var viewModel = ViewModel()
    
        var body: some View {
            List {
                Section {
                    ForEach(viewModel.model.indices, id: .self) { idx in
                        Text("(viewModel.model[idx])")
                            .onAppear {
                                viewModel.cellDidRequestLoad(idx)
                            }
                    }
                }
    
                if viewModel.isLoading {
                    CustomActivityIndicator()
                        .frame(maxWidth: .infinity)
                }
            }
        }
    }
    

    The result is:

    enter image description here

    Login or Signup to reply.
  2. This is duplicate question and was answered by user "EdYuTo" whose solution is working: https://stackoverflow.com/a/75431883/7901392

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search