swiftswiftuiuiimagecore-imageciimage

How to apply a color filter to an Async image in Swift UI?


I can't work out how to dynamically filter (color filter) images that are being loaded. Eg I want to dynamically increase the saturation.

Filtering an image in SwiftUI looks straight forward using [CI library] (https://www.hackingwithswift.com/books/ios-swiftui/basic-image-filtering-using-core-image).

But I cant work out how to get a Core Image object from Async Image to be manipulated.

I would imagine it working something like this:

AsyncImage(url: URL(string: "https://example.com/icon.png")) { image in
    Image(uiImage: myFilter(image.uiImage.ciImage).uiImage).resizable()
} placeholder: {
    ProgressView()
}
.frame(width: 50, height: 50)

Or maybe Image could be extended?


Solution

  • AsyncImage can't give you a UIImage, but it is relatively easy to write your own AsyncImage that gives you a UIImage.

    struct AsyncUIImage<Content: View, Placeholder: View>: View {
        let url: URL
        let contentBuilder: (UIImage) -> Content
        let placeholder: Placeholder
        
        @State private var content: Content?
        
        init(url: URL, @ViewBuilder content: @escaping (UIImage) -> Content, @ViewBuilder placeholder: () -> Placeholder) {
            self.url = url
            self.contentBuilder = content
            self.placeholder = placeholder()
        }
        
        var body: some View {
            if let content {
                content
            } else {
                placeholder
                    .task {
                        do {
                            let (data, _) = try await URLSession.shared.data(from: url)
                            if let uiImage = UIImage(data: data) {
                                content = contentBuilder(uiImage)
                            }
                        } catch {
                            print(error)
                        }
                    }
            }
        }
    }
    

    Example usage:

    var body: some View {
        AsyncUIImage(url: URL(string: "https://picsum.photos/200")!) { uiImage in
            VStack {
                if let ciImage = CIImage(image: uiImage) {
                    processImage(ciImage)
                }
                Image(uiImage: uiImage)
            }
        } placeholder: {
            ProgressView()
        }
    
    }
    
    func processImage(_ ciImage: CIImage) -> Image? {
        let context = CIContext()
        let filter = CIFilter.sepiaTone()
        filter.inputImage = ciImage
        filter.intensity = 1
        guard let output = filter.outputImage else { return nil }
        guard let cgImage = context.createCGImage(output, from: output.extent) else { return nil }
        return Image(uiImage: UIImage(cgImage: cgImage))
    }
    

    Also note that the effects of some CIFilters can be achieved using SwiftUI view modifiers, such as blur, colorInvert, hueRotation, etc. For these filters, you can just apply them to the Image produced by AsyncImage.