Swiftui FileManager/URLSession not writing to documentDirectory when running as background task

243 Views Asked by At

Hope you're doing well! I've built an app that generates a view from a .csv file that I have hosted on my website. I've previously managed to get everything working as expected where I called the csv from the website and wrote the contents directly to a variable and then processed it from there. Obviously this wasn't good practice as the app started mis-behaving when the internet couldn't be accessed (despite writing in connectivity checks).

I've now built out the app to call the URL, save the csv with Filemanager, then when the app refreshes, it will use FileManager.default.replaceItemAt to replace the previous version if there is internet connectivity, if not the app builds from the previously stored .csv

This all works fine when the app is running, however I'm running into issues with the background processing task. It seems the app doesn't have permissions to write with FileManager when it is executed from the background task. Is there an additional step I'm missing when using this in background tasks? I've attempted to use FileManager.default.removeItem followed by FileManager.default.copyItem instead of replaceItemAt but it doesn't seem to make a difference as expected.

UPDATE 22/06 - Still scouring the internet for similar issues or examples I think I might be going down the wrong rabbit hole here. This could be issues with the way the new background task has been configured for retrieving data from my website, although the background tasks worked fine before there seems to be a bit more legwork needed for this method to work as a background task.

    func handleAppRefresh(task: BGProcessingTask) {
    //Schedules another refresh
    scheduleAppRefresh()
    DispatchQueue.global(qos: .background).async {
       pullData()
       print("BG Background Task fired")
    }

pullData() will call loadCSV() and then do some data processing. At the moment I'm just using a print straight after loadCSV() is called to validate if the downloads etc are successful.

// Function to pass the string above into variables set in the csvevent struct
func loadCSV(from csvName: String) -> [CSVEvent] {
var csvToStruct = [CSVEvent]()

// Creates destination filepath & filename
let documentsUrl:URL =  (FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first as URL?)!
let destinationFileUrl = documentsUrl.appendingPathComponent("testcsv.csv")

//Create URL to the source file to be downloaded
let fileURL = URL(string: "https://example.com/testcsv.csv")!

let sessionConfig = URLSessionConfiguration.default
let session = URLSession(configuration: sessionConfig)

let request = URLRequest(url:fileURL)

let task = session.downloadTask(with: request) { (tempLocalUrl, response, error) in
    if let tempLocalUrl = tempLocalUrl, error == nil {
        if let statusCode = (response as? HTTPURLResponse)?.statusCode {
            print("File downloaded Successfully. Response: \(statusCode)")
        }
        
        do {
            let _ = try FileManager.default.replaceItemAt(destinationFileUrl, withItemAt: tempLocalUrl)
        } catch (let writeError) {
            print("Error creating a file \(destinationFileUrl) : \(writeError)")
        }
    } else {
        print("Error" )
    }
}
task.resume()

let data = readCSV(inputFile: "testcsv.csv")

var rows = data.components(separatedBy: "\n")

rows.removeFirst()

// Iterates through each row and sets values
for row in rows {
    let csvColumns = row.components(separatedBy: ",")
    let csveventStruct = CSVEvent.init(raw: csvColumns)
    csvToStruct.append(csveventStruct)
}
print("LoadCSV has run and created testcsv.csv")
return csvToStruct
}

Any help or pointers to why these files aren't being updated in background tasks but are working fine in app would be massively appreciated!

Thanks in advance.

EDIT: adding new BGProcessingTask

func handleAppRefresh(task: BGProcessingTask) {
    //Schedules another refresh
    print("BG Background Task fired")
    scheduleAppRefresh()
    Task.detached {
        do {
            let events = try await loadCSV(from: "Eventtest").filter { !dateInPast(value: $0.date) }
            print(events)
            pullData(events: events)
        } catch {
            print(error)
        }
    }
}
1

There are 1 best solutions below

2
On

The problem is not the background task per se, the problem is the asynchronous behavior of downloadTask. readCSV is executed before the data is downloaded.

In Swift 5.5 and later async/await provides asynchronous behavior but the code can be written continuously.

func loadCSV(from csvName: String) async throws -> [CSVEvent] {
    var csvToStruct = [CSVEvent]()
    
    // Creates destination filepath & filename
    let documentsUrl:URL =  (FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first as URL?)!
    let destinationFileUrl = documentsUrl.appendingPathComponent("testcsv.csv")
    
    //Create URL to the source file to be downloaded
    let fileURL = URL(string: "https://example.com/testcsv.csv")!
    
    let sessionConfig = URLSessionConfiguration.default
    let session = URLSession(configuration: sessionConfig)
    
    let request = URLRequest(url:fileURL)
    
    let (url, response) = try await session.download(for: request)
    
    if let statusCode = (response as? HTTPURLResponse)?.statusCode {
        print("File downloaded Successfully. Response: \(statusCode)")
    }
    let _ = try FileManager.default.replaceItemAt(destinationFileUrl, withItemAt: url)        
    let data = readCSV(inputFile: "testcsv.csv")
    
    var rows = data.components(separatedBy: "\n")
    
    rows.removeFirst()
    
    // Iterates through each row and sets values
    for row in rows {
        let csvColumns = row.components(separatedBy: ",")
        let csveventStruct = CSVEvent.init(raw: csvColumns)
        csvToStruct.append(csveventStruct)
    }
    print("LoadCSV has run and created testcsv.csv")
    return csvToStruct
}

To call the function you have to wrap it in a detached Task which replaces the GCD queue

Task.detached {
    do {
        let events = try await loadCSV(csvName: "Foo")
        print("BG Background Task fired")
    } catch {
        print(error)
    }
}