Search code examples
swiftalamofirecombine

downloading progress with combine


I try to show progress of file downloading with combine and Alamofire. I have downloader class

class DataManager: NSObject, DataManagerProtocol {
    private(set) var value = 0.0 {
        didSet { subject.send(value) }
    }

    private let subject = PassthroughSubject<Double, Never>()

    func increment(by value: Double) {
        self.value = value
    }
    
    func saveFile(urlString: String, fileName: String) -> AnyPublisher<Double, Never> {
        download(urlString: urlString, fileName: fileName)
        return subject.eraseToAnyPublisher()
    }
    
    private func download(urlString: String, fileName: String) {
        AF.download(urlString)
            .downloadProgress { [self] progress in
                print("Download Progress: \(progress.fractionCompleted)")
                increment(by: progress.fractionCompleted)
            }
            .responseData { response in
                if let data = response.value {
                    print("data recieved")
                    self.writeToFile(data: data, fileName: fileName)
                }
            }
    }
    
    func writeToFile(data: Data, fileName: String) {
        // get path of directory
        
        guard let directory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).last else {
            return
        }
        // create file url
        let fileurl =  directory.appendingPathComponent(filename)
        if FileManager.default.fileExists(atPath: fileurl.path) {
            if let fileHandle = FileHandle(forWritingAtPath: fileurl.path) {
                print("FileExist")
            } else {
                print("Can't open file to write.")
            }
        } else {
            // if file does not exist write data for the first time
            do {
                try data.write(to: fileurl, options: .atomic)
            } catch {
                print("Unable to write in new file.")
            }
        }
    }
}

in console I see and file success downloaded

Download Progress: 0.2707762694368025 Download Progress: 0.30361701168087313 Download Progress: 0.45961053734020857 Download Progress: 0.5088716507063145 Download Progress: 0.5827633207554733 Download Progress: 0.615604062999544 Download Progress: 0.6484448052436146 Download Progress: 0.7798077742198971 Download Progress: 0.8783300009521089 Download Progress: 1.0 data recieved

but in my ViewModel i dont see publishing of progress changing

import Combine

final class RecordsListViewModel: ObservableObject {
    private var cancellable: AnyCancellable?
    private(set) var progress = PassthroughSubject<Double, Never>()
    private let dataManager: DataManagerProtocol
    
    init(dataManager: DataManagerProtocol) {
        self.dataManager = dataManager
    }
    
    func downloadFile() {
        cancellable = dataManager.saveFile(urlString: "https://i.artfile.ru/2880x1800_1455670_[www.ArtFile.ru].jpg", fileName: "filename.jpg")
            .receive(on: DispatchQueue.main)
            .sink { [weak self] completion in
                print(completion, "completion")
            } receiveValue: { progress in
                print(progress, "progress")
            }
    }
}

Solution

  • The problem is that in the saveFile method you call the download method before the ViewModel gets the subject. I would suggest splitting the saveFile method in two.

    In the DataManager:

    func getSubscription() -> AnyPublisher<Double, Never> {
        return subject.eraseToAnyPublisher()
    }
        
    func saveFile(urlString: String, fileName: String) {
        download(urlString: urlString, fileName: fileName)
        // or just put the contents of the download method here
    }
    

    In the ViewModel, get the DataManager's subject first, to prepare and be ready for the download:

    init(dataManager: DataManagerProtocol) {
        self.dataManager = dataManager
        
        cancellable = self.dataManager.getSubscription()
            .receive(on: DispatchQueue.main)
            .sink { completion in
                print(completion, "completion")
            } receiveValue: { progress in
                print(progress, "progress")
            }
    }
        
    func downloadFile() {
        dataManager.saveFile(urlString: "https://i.artfile.ru/2880x1800_1455670_[www.ArtFile.ru].jpg", fileName: "filename.jpg")
    }
    

    I tried it in a Playground, with a Timer Publisher to simulate the download. It worked perfectly. Of course, you need to change the DataManagerProtocol to something like this:

    protocol DataManagerProtocol {
        func saveFile(urlString: String, fileName: String)
        func getSubscription() -> AnyPublisher<Double, Never>
    }
    

    Edit: I just found another way to solve the problem. You only need to change one line in the code you provided:

    func saveFile(urlString: String, fileName: String) -> AnyPublisher<Double, Never> {
        defer { download(urlString: urlString, fileName: fileName) }
        return subject.eraseToAnyPublisher()
    }
    

    The defer block will be excecuted directly before the function is done, even after the return. I'm not sure if that makes your code hard to read. But I thought I should mention this option for completeness' sake. If anybody has more experience with defer, let me know in the comments if this would be more of a misuse.

    By the way, I think you want to change your increment method's content to:

    self.value += value