The Problem

We often need to introduce some image loading framework to smooth the scrolling performance. These frameworks were built on an assumption that we can always provide them a URL for each and every image to load. What if we want to transform the image data into image URL, but without having to first write the data onto the disk?

The Solution

We need something that works in the middle to handle this for us. To do that, we subclass URLProtocol and define a new URL protocol. This protocol hijacks the retrieving of the image data, and instead of fetching from a remote server, we fetch the value directly from a binary data field.

🥒

Core Data heuristically decides on a per-value basis if it should save the data directly in the database or store a URI to a separate file which it manages for you.

Defining a Custom Protocol

public class ImageProtocol: URLProtocol {
    
    public enum LoadingError: Error {
        case incorrectlyEncodedURL
        case noDataAvailable
    }
    
    static let scheme = "kouchaImage"
    
    private static let context = KouchaPersistentContainer.shared.newBackgroundContext()
    
    public override class func canInit(with task: URLSessionTask) -> Bool {
        guard let url = task.currentRequest?.url else { return false }
        return url.scheme == Self.scheme
    }
    
    public override class func canonicalRequest(for request: URLRequest) -> URLRequest {
        return request
    }
    
    private func cancelWithError(_ loadingError: LoadingError) {
        loadingError.d()
        
        client?.urlProtocol(self, didFailWithError: loadingError)
    }
    
    public override func startLoading() {
        
        request.url?.i()
        
        guard let url = request.url, 
              let decodedURIString = url.host?.removingPercentEncoding else { return cancelWithError(.incorrectlyEncodedURL) }
        
        let locateURI = URL(string: decodedURIString)!
        
        var data: Data?
        
        Self.context.performAndWait {
            if let loadable = NSManagedObject.awake(from: locateURI, context: Self.context) as? ImageLoadable {
                data = loadable.imageData
            }
        }
        
        guard let loadedData = data else { return cancelWithError(.noDataAvailable) }
        
        let response = URLResponse(url: url, 
                                   mimeType: "image/png", 
                                   expectedContentLength: 0, 
                                   textEncodingName: nil)
        
        client?.urlProtocol(self, 
                            didReceive: response, 
                            cacheStoragePolicy: .allowed)
        client?.urlProtocol(self, didLoad: loadedData)
        client?.urlProtocolDidFinishLoading(self)
    }
    
    public override func stopLoading() {
        "stopped.".d()
    }
}

Hook Things Up

import Nuke

extension Nuke.ImagePipeline {
    public static let koucha: Nuke.ImagePipeline = {
        let urlSessionConfiguration = DataLoader.defaultConfiguration
        urlSessionConfiguration.protocolClasses = [ImageProtocol.self]
        let loader = DataLoader(configuration: urlSessionConfiguration)
        let piplineConfiguration = ImagePipeline.Configuration(dataLoader: loader)
        
        return ImagePipeline(configuration: piplineConfiguration)
    }()
}