Search code examples
swiftswiftuialamofire

UI locking up when using AlamoFire downloadProgress


I'm trying to create a download progress bar and show an alert at the same time when a download is completing.

For this task, I'm using AlamoFire with SwiftUI since it makes downloading easy. However, when I track the progress using a ProgressView with a Published variable, the entire UI locks up and I can't figure out how to fix it.

I tried adding the downloadProgress to a separate DispatchQueue, but I still have to update the UI from the main thread otherwise Xcode will complain.

How to test the attached example code:

  • Click "Start download"
  • Wait for the ProgressView to move a bit
  • Click the "Show alert" button
  • Try closing the alert, it won't close.

I would appreciate any help.

import SwiftUI import Alamofire

struct ContentView: View {
    @StateObject var viewModel: ViewModel = ViewModel()
    @State private var showAlert = false

    var body: some View {
        VStack {
            Button("Show Alert") {
                showAlert.toggle()
            }
            
            Button("Start download") {
                viewModel.startDownload()
            }
            
            if viewModel.showProgressView {
                ProgressView("Downloading…", value: viewModel.downloadProgress, total: 1.0)
                    .progressViewStyle(.linear)
            }
        }
        .alert(isPresented: $showAlert) {
            Alert(
                title: Text("Text"),
                dismissButton: .cancel()
            )
        }
    }
}

class ViewModel: ObservableObject {
    @Published var currentDownload: DownloadRequest? = nil
    @Published var downloadProgress: Double = 0.0
    @Published var showProgressView: Bool = false
    
    func startDownload() {
        print("Function called!")
        
        showProgressView.toggle()
        
        let queue = DispatchQueue(label: "alamofire", qos: .utility)
        let destination = DownloadRequest.suggestedDownloadDestination(for: .documentDirectory)

        AF.download("https://speed.hetzner.de/10GB.bin", to: destination)
            .downloadProgress(queue: queue) { progress in
                print(progress.fractionCompleted)
                
                DispatchQueue.main.async {
                    self.downloadProgress = progress.fractionCompleted
                }
            }
            .response { response in
                print(response)
            }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

Solution

  • The issue here is that you are effectively spamming the UI thread with updates, since alamofire calls the closure provided to downloadProgress very often (look at the console prints). You need to stagger the updates from AF progress a bit so that the button press to dismiss the alert can register (in Combine this would be known as debounce). What I've done here is added a little time counter so that it only updates the progress every 1 second. The time between those updates keeps the UI thread free to respond to taps etc.

    import SwiftUI
    import Alamofire
    
    struct ContentView: View {
        @StateObject var viewModel: ViewModel = ViewModel()
        @State private var showAlert = false
    
        var body: some View {
            VStack {
                Button("Show Alert") {
                    showAlert.toggle()
                }
                
                Button("Start download") {
                    viewModel.startDownload()
                }
                
                if viewModel.showProgressView {
                    ProgressView("Downloading…", value: viewModel.downloadProgress, total: 1.0)
                        .progressViewStyle(.linear)
                }
            }
            .alert(isPresented: $showAlert) {
                Alert(
                    title: Text("Text"),
                    dismissButton: .cancel()
                )
            }
        }
    }
    
    class ViewModel: ObservableObject {
        @Published var currentDownload: DownloadRequest? = nil
        @Published var downloadProgress: Double = 0.0
        @Published var showProgressView: Bool = false
        
        func startDownload() {
            print("Function called!")
            
            showProgressView.toggle()
            
            let queue = DispatchQueue(label: "net", qos: .userInitiated)
            let destination = DownloadRequest.suggestedDownloadDestination(for: .documentDirectory)
            var last = Date()
            AF.download("https://speed.hetzner.de/10GB.bin", to: destination)
                .downloadProgress(queue:queue) { progress in
                    print(progress.fractionCompleted)
                    if Date().timeIntervalSince(last) > 1 {
                        last = Date()
                        DispatchQueue.main.async {
                            self.downloadProgress = progress.fractionCompleted
                        }
                    }
                }
                .response { response in
    //                print(response)
                }
        }
    }
    
    struct ContentView_Previews: PreviewProvider {
        static var previews: some View {
            ContentView()
        }
    }