Search code examples
iosswiftcore-graphicsnsurlios13

Loading images from external storage using Core Graphics not working iOS 13


I am attempting to load photos located on external storage (SD card) using core graphics in iOS 13 (beta). The code below works fine when the files are on the device. When the files are on external storage however it fails returning nil and I don't know why.

I believe I am using the correct security scoping. I loaded the file URLs from a security scoped folder url as per Providing Access to Directories

guard folderUrl.startAccessingSecurityScopedResource() else {
    return nil
}
defer { folderUrl.stopAccessingSecurityScopedResource() }

guard let imageSource = CGImageSourceCreateWithURL(imageURL as CFURL, options) else {
    throw Error.failedToOpenImage(message: "Failed to open image at \(imageURL)")
}

Solution

  • So... for my own project, where I ran into the same issue, I now have the following function to give me a thumbnail, going from elegant and quick to brute force.

    static func thumbnailForImage(at url: URL, completion: (Result<UIImage, Error>) -> Void) {
        let shouldStopAccessing = url.startAccessingSecurityScopedResource()
        defer { if shouldStopAccessing { url.stopAccessingSecurityScopedResource() } }
    
        let coordinator = NSFileCoordinator()
        var error: NSError?
    
        coordinator.coordinate(readingItemAt: url, options: .withoutChanges, error: &error) { url in
    
            var thumbnailImage: UIImage?
            var storedError: NSError?
            var imageSource: CGImageSource?
    
            print("Strategy 1: Via URL resource key")
            do {
                let resourceKeys = Set([URLResourceKey.thumbnailDictionaryKey])
                let resources = try url.resourceValues(forKeys: resourceKeys)
                if let dict = resources.thumbnailDictionary, let resource = dict[URLThumbnailDictionaryItem.NSThumbnail1024x1024SizeKey] {
                    thumbnailImage = resource
                } else {
                    throw "No thumbnail dictionary"
                }
            } catch let error {
                storedError = error as NSError
            }
    
            let options = [kCGImageSourceCreateThumbnailFromImageIfAbsent: true, kCGImageSourceShouldAllowFloat: true, kCGImageSourceCreateThumbnailWithTransform: true]
    
            if thumbnailImage == nil {
                print("Strategy 2: Via CGImageSourceCreateWithURL")
                imageSource = CGImageSourceCreateWithURL(url as CFURL, options as CFDictionary)
            }
    
            if thumbnailImage == nil && imageSource == nil {
                print("Strategy 3: Via CGImageSourceCreateWithData")
    
                let data = try? Data.init(contentsOf: url)
                if let data = data {
                    imageSource = CGImageSourceCreateWithData(data as CFData, options as CFDictionary)
                }
            }
    
            if let imageSource = imageSource, thumbnailImage == nil {
                print("Attempting thumbnail creation from source created in strategy 2 or 3")
    
                if let image = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, options as CFDictionary) {
                    thumbnailImage = UIImage(cgImage: image)
                }
            }
    
            if let thumbnailImage = thumbnailImage {
                print("Success")
                completion(.success(thumbnailImage))
            } else {
                print("Failure")
                if let error = storedError { completion(.failure(error)) }
                else { completion(.failure("Everything just fails...")) }
            }
        }
    
        if let error = error { completion(.failure(error)) }
    }
    

    Basically it works by trying to get a thumbnail via the URL resources first. This is the quickest and nicest way, of it works. If that fails, I try CGImageSourceCreateWithURL. That works most of the time, except on remote storage. I suspect that's still a bug and submitted a feedback ticket to apple for this. I suggest you do the same. Last attempt, just try to read the entire file using NSData and creating an image source via CGImageSourceCreateWithData...

    So far, if it's an image file I, this seems to produce a thumbnail most of the time. It can be quite slow though, having to read the entire file.