Search code examples
swiftmetalmetalkitmtktextureloader

Use a remote image for MTKTextureLoader


I'm trying to load my texture from a url with this code:

let textureLoader = MTKTextureLoader(device: device)
var texture: MTLTexture?
let origin = NSString(string: MTKTextureLoader.Origin.bottomLeft.rawValue)
let textureLoaderOptions = [MTKTextureLoader.Option.SRGB: 0, MTKTextureLoader.Option.origin: origin] as [MTKTextureLoader.Option: Any]

do {
   texture = try textureLoader.newTexture(URL: my-url-here, options: textureLoaderOptions)
} catch {
   print("texture not created")
}

It was working fine when I loaded the texture from within the app, but I can't seem to get it to work using an external url. Anyone had any luck with this?

I tried Load a remote image using MTKTextureLoader but was unable to get it to work as is, and I'm not quite knowledgeable enough to figure out how to update it.


Solution

  • Here's an extension to MTKTextureLoader (compatible with Swift 5) that downloads an image from a remote URL and creates a Metal texture from it. It follows essentially the same pattern as the existing asynchronous loading API.

    extension MTKTextureLoader {
        static let errorDomain = "com.example.MTKTextureLoader.RemoteExtensions"
    
        func newTexture(remoteURL url: URL, options: [MTKTextureLoader.Option : Any]? = nil, completionHandler: @escaping MTKTextureLoader.Callback) {
            let downloadTask = URLSession.shared.downloadTask(with: URLRequest(url: url)) { (maybeFileURL, maybeResponse, maybeError) in
                var anError: Swift.Error? = maybeError
                if let tempURL = maybeFileURL, let response = maybeResponse {
                    if let cachePath = NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true).first {
                        let cachesURL = URL(fileURLWithPath: cachePath, isDirectory: true)
                        let cachedFileURL = cachesURL.appendingPathComponent(response.suggestedFilename ?? NSUUID().uuidString)
                        try? FileManager.default.moveItem(at: tempURL, to: cachedFileURL)
                        return self.newTexture(URL: cachedFileURL, options: options, completionHandler: completionHandler)
                    } else {
                        anError = NSError(domain: MTKTextureLoader.errorDomain,
                                          code: 1,
                                          userInfo: [NSLocalizedDescriptionKey : "Unable to find user caches directory"])
                    }
                } else {
                    anError = NSError(domain: MTKTextureLoader.errorDomain,
                                      code: 2,
                                      userInfo: [NSLocalizedDescriptionKey : "Download from URL failed"])
                }
                completionHandler(nil, anError)
            }
            downloadTask.resume()
        }
    }
    

    Note that if your app is sandboxed, you should enable "Outgoing Connections" in your entitlements if you haven't already done so.

    Also, mashing up concerns like downloading, caching, and texture loading isn't a best practice, so if you're going to be loading a lot of remote files, I'd recommend refactoring this toward a more general remote resource caching system. This is for demonstration purposes only.