16

I'm trying to display and save images with Swift. On first hit, it shows the remote image on imageview, on second hit it shows blank imageview instead of it should be local image which saved on first hit.

    var paths = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)[0] as String
    var imagePath = paths.stringByAppendingPathComponent("images/\(id)/logo.jpg" )
    var checkImage = NSFileManager.defaultManager()

    if (checkImage.fileExistsAtPath(imagePath)) {
        let getImage = UIImage(contentsOfFile: imagePath)
        self.image?.image = getImage
    } else {
        dispatch_async(dispatch_get_main_queue()) {
            let getImage =  UIImage(data: NSData(contentsOfURL: NSURL(string: remoteImage)))
            UIImageJPEGRepresentation(getImage, 100).writeToFile(imagePath, atomically: true)
            self.image?.image = getImage
        }
    }

Edit: This one worked for me.

var paths = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)[0] as String
var dirPath = paths.stringByAppendingPathComponent("images/\(id)" )
var imagePath = paths.stringByAppendingPathComponent("images/\(id)/logo.jpg" )
var checkImage = NSFileManager.defaultManager()

if (checkImage.fileExistsAtPath(imagePath)) {
    let getImage = UIImage(contentsOfFile: imagePath)
    self.image?.image = getImage
} else {
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0)) {
        checkImage.createDirectoryAtPath(dirPath, withIntermediateDirectories: true, attributes: nil, error: nil)
        let getImage =  UIImage(data: NSData(contentsOfURL: NSURL(string: remoteImage)))
        UIImageJPEGRepresentation(getImage, 100).writeToFile(imagePath, atomically: true)

        dispatch_async(dispatch_get_main_queue()) {
            self.image?.image = getImage
            return
        }
    }
}
bilmiyore
  • 163
  • 1
  • 1
  • 6

2 Answers2

21

To answer your main question, you're calling the wrong UIImage initializer. You should be calling UIImage(contentsOfFile: imagePath) in swift 2 and UIImage(contentsOf: imagePath) in swift 3.

Additionally, it looks like you're trying to do your remote fetch in the background with dispatch_async (or DispatchQueue in swift 3), but you're passing it the main queue, so you're actually blocking the main/UI thread with that. You should dispatch it to one of the background queues instead and then dispatch back to the main queue when you actually set the image in your UI:

Swift 3 :

DispatchQueue.global(qos: DispatchQoS.background.qosClass).async {
    do {
        let data = try Data(contentsOf: URL(string: self.remoteImage)!)
        let getImage = UIImage(data: data)
        try UIImageJPEGRepresentation(getImage!, 100)?.write(to: imagePath)
        DispatchQueue.main.async {
            self.image?.image = getImage
            return
        }
    }
    catch {
            return
    }
}

Swift 2 :

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0)) {
    let getImage =  UIImage(data: NSData(contentsOfURL: NSURL(string: self.remoteImage)))
    UIImageJPEGRepresentation(getImage, 100).writeToFile(imagePath, atomically: true)

    dispatch_async(dispatch_get_main_queue()) {
        self.image?.image = getImage
        return
    }
}

@Rob's answer re: fetching your remote image and saving it is really the best way to do this.

Community
  • 1
  • 1
Mike S
  • 41,895
  • 11
  • 89
  • 84
  • Thanks, i changed it to UIImage(contentsOfFile: imagePath). But when i change dispatch to dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0)) it's not showing even remote image. – bilmiyore Oct 09 '14 at 19:07
  • Thanks, i think the dispatch works better now. But still i can't load images locally. I think "var imagePath = paths.stringByAppendingPathComponent("images/\(id)/logo.jpg" )" is the problem because "if block" doesn't show up at all. – bilmiyore Oct 09 '14 at 19:24
  • Have you set a breakpoint and looked at `imagePath` to make sure it makes sense? (you could also just `println(imagePath)`) – Mike S Oct 09 '14 at 19:27
  • When i change path to "\(id)-logo.jpg" it worked. The problem should be subdirectories. – bilmiyore Oct 09 '14 at 19:30
  • Are you sure that `images` directory exists? If it doesn't you'd have to create it before you'd be able to save to that path. – Mike S Oct 09 '14 at 19:40
  • It's not. Looking for how to create directories like "images/\(id)" – bilmiyore Oct 09 '14 at 19:44
  • 1
    @bilmiyore Just call `NSFileManager` method `createDirectoryAtPath`. – Rob Oct 10 '14 at 02:11
  • Be aware that if you cycling save and reload the same picture, you will loose a little in details every time even you use 100 in loss parameter. Use PNG http://stackoverflow.com/a/36645202/1264893 lossless save – iluvatar_GR May 12 '16 at 11:14
10

Your code that dispatches NSData(contentsOfURL:) (now known as Data(contentsOf:)) to the main queue. If you're going to use that synchronous method to request remote image, you should do this on a background queue.

Also, you are taking the NSData, converting it to a UIImage, and then converting it back to a NSData using UIImageJPEGRepresentation. Don't round-trip it though UIImageJPEGRepresentation as you will alter the original payload and will change the size of the asset. Just just confirm that the data contained an image, but then write that original NSData

Thus, in Swift 3, you probably want to do something like:

DispatchQueue.global().async {
    do {
        let data = try Data(contentsOf: URL(string: urlString)!)
        if let image = UIImage(data: data) {
            try data.write(to: fileURL)
            DispatchQueue.main.async {
                self.imageView?.image = image
            }
        }
    } catch {
        print(error)
    }
}

Even better, you should use NSURLSession because you can better diagnose problems, it's cancelable, etc. (And don't use the deprecated NSURLConnection.) I'd also check the statusCode of the response. For example:

func requestImage(_ url: URL, fileURL: URL) {
    let task = URLSession.shared.dataTask(with: url) { data, response, error in
        // check for fundamental network issues (e.g. no internet, etc.)

        guard let data = data, error == nil else {
            print("dataTask error: \(error?.localizedDescription ?? "Unknown error")")
            return
        }

        // make sure web server returned 200 status code (and not 404 for bad URL or whatever)

        guard let httpResponse = response as? HTTPURLResponse, 200 ..< 300 ~= httpResponse.statusCode else {
            print("Error; Text of response = \(String(data: data, encoding: .utf8) ?? "(Cannot display)")")
            return
        }

        // save image and update UI

        if let image = UIImage(data: data) {
            do {
                // add directory if it doesn't exist

                let directory = fileURL.deletingLastPathComponent()
                try? FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)

                // save file

                try data.write(to: fileURL, options: .atomic)
            } catch let fileError {
                print(fileError)
            }

            DispatchQueue.main.async {
                print("image = \(image)")
                self.imageView?.image = image
            }
        }
    }
    task.resume()

}

Note, the just-in-time creation of the folder is only necessary if you haven't created it already. Personally, when I build the original path, I'd create the folder there rather than in the completion handler, but you can do this any way you want. Just make sure the folder exists before you write the file.

Regardless, hopefully this illustrates the main points, namely that you should save the original asset and that you should do this in the background.

For Swift 2 renditions, see previous revision of this answer.

Community
  • 1
  • 1
Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • I just tried this but couldn't load any remote or local image. – bilmiyore Oct 09 '14 at 19:22
  • 1
    @bilmiyore FYI, I revised my answer to improve error handling to diagnose what's going wrong, if anything. In terms of local image loading, I didn't touch that. – Rob Oct 09 '14 at 19:28
  • Thanks @Rob, that worked too. There is only one problem which that i can't make it work with subdirectories. "\(id)-logo.jpg" path is working but "\(id)\logo.jpg" is not. – bilmiyore Oct 09 '14 at 19:33
  • @bilmiyore You cannot just save to nonexistent subdirectory. You have to create that subdirectory (with `NSFileManager`) if it doesn't exist. BTW, see my final example, in which I show you how to use `writeToFile` in a manner that it gives you meaningful error message. – Rob Oct 09 '14 at 19:50