Home > OS >  UI locking up when using AlamoFire downloadProgress
UI locking up when using AlamoFire downloadProgress

Time:11-28

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()
    }
}

CodePudding user response:

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()
    }
}
  • Related