mangasaske
mangasaske

Reputation: 94

how to save file in background thread using Swift?

I'm trying to make an application that uses a web service to get some data from a directory, but I also need to save the data into the device, include images, to do so I'm using Alamofire and AlamofireImage framework for consuming the webservice. I save the generated objects in a database with Realm framework and for images I save the UIImage into a file.

Basically, the ViewController has a tableView which displays de data, but it seems laggy because of the images writing into files.

This is my writing function:

func saveImage(_ image: UIImage) {
    if let data = UIImagePNGRepresentation(image) {
        let name = "images/person_directory_\(id).png"
        let docsDir = getDocumentsDirectory()
        let filename = docsDir.appendingPathComponent(name)
        let fm = FileManager.default

        if !fm.fileExists(atPath: docsDir.appendingPathComponent("images").path) {
            do {
                try fm.createDirectory(at: docsDir.appendingPathComponent("images"), withIntermediateDirectories: true, attributes: nil)
                try data.write(to: filename)
                try! realm?.write {
                    self.imageLocal = name
                }
            }
            catch {
                print(error)
            }
        }
        else {
            do {
                try data.write(to: filename, options: .atomic)
                try! realm?.write {
                    self.imageLocal = name
                }
            }
            catch {
                print(error)
            }

        }
    }
}

I call this function when Alamofire downloads the image

if person.imageLocal != nil,  let image = person.loadLocalImage() {
        print("Load form disk: \(person.imageLocal)")
        cell.imgProfile.image = image
    }
    else if !(person.image?.isEmpty)! {
        Alamofire.request(person.image!).responseImage(completionHandler: { (response) in
            if response.result.isSuccess {
                if let image = response.result.value {
                    person.saveImage(image)
                    cell.imgProfile.image = image
                    print("Downloaded: \(person.imageLocal)")
                }
            }
        })
    }

But the tableView looks laggy when scrolled and I was trying to make the writing operation into a diferent thread so it could get written without affecting the application performance by using DispatchQeue

DispatchQueue.global(qos: .background).async {
                    do {
                        try data.write(to: filename)
                    }
                    catch {
                        print(error)
                    }
                }

But even so the applications stills laggy.

UPDATE:

I tryed this as Rob suggested:

func saveImage(_ image: UIImage) {
    if let data = UIImagePNGRepresentation(image) {
        let name = "images/person_directory_\(id).png"
        do {
            try realm?.write {
                self.imageLocal = name
            }
        }
        catch {
            print("Realm error")
        }
        DispatchQueue.global().async {
            let docsDir = self.getDocumentsDirectory()
            let filename = docsDir.appendingPathComponent(name)
            let fm = FileManager.default

            if !fm.fileExists(atPath: docsDir.appendingPathComponent("images").path) {
                do {
                    try fm.createDirectory(at: docsDir.appendingPathComponent("images"), withIntermediateDirectories: true, attributes: nil)
                }
                catch {
                    print(error)
                }
            }
            do {
                try data.write(to: filename)
            }
            catch {
                print(error)
            }
        }
    }
}

I can't dispatch the Realm writing becase Realm doesn't suport multithreading. It stills scrolling laggy but not as much as the first time.

Upvotes: 1

Views: 5997

Answers (1)

SafeFastExpressive
SafeFastExpressive

Reputation: 3807

So the proper answer as @Rob gave it is

   DispatchQueue.global().async {
      do {
         try data.write(to: filename)
      }
      catch {
         print(error)
      }
    }

But just as important is to never use a reference to a UITableViewCell from an asynchronous call (again credit @Rob). For example, setting a cell value from the asynchronous parts of your code.

                cell.imgProfile.image = image

UITableViewCells are re-used, so you don't know that the original cell is still used at the same index. For a test, scroll your list really fast so your images get loaded, if you see images appear in the wrong cell, its the re-use problem.

So from an asynchronous callback you need to figure out if the cell index for the new image is visible, go get the current cell for that index to set it's image. If the index isn't visible, then store/cache the image until it's index is scrolled into view. At that point it's cell will be created with UITableView.cellForRowAtIndexPath, and you can set the image there.

Upvotes: 2

Related Questions