swiftswiftuicore-imagecifilterviewmodifier

How to animate a Core Image Filter in SwiftUI


I want to apply a given CIFilter but instead of the effect showing up instantly, I want to animate it. For example, I want to desaturate a color image to grey scale over 2 seconds, or resolve a blocky image by depixellating it to a full-resolution image using an EaseInOut animation curve over 0.8 seconds.

If you're using one of the built in SwiftUI view modifiers like .blur(), you're golden. Just append some .animate() variant and you're done.

But given that you have to jump through hoops whether you go the UIImage, CGImage, CIImage route, or the MTLView, CIRenderDestination, ContentView example from the WWDC 2022 sample code, I'm a bit confused.

Ideally I guess I'd just like to write View Modifiers for each effect I want to do, so that they're as usable as the SwiftUI built-in ones, but I don't know if that's possible. Is there one blessed way of doing this? Any ideas?


Solution

  • Here is an approach adapting Animatable. You can easily adapt it to your needs:

    struct ContentView: View {
        
        @State private var animate = false
    
        var body: some View {
            VStack {
                PixellatedImage(imageName: "yoga", scale: animate ? 100 : 1)
                    .scaledToFit()
            }
            .padding()
            .onAppear {
                withAnimation(Animation.linear(duration: 3.0).repeatForever()) {
                    self.animate.toggle()
                }
            }
        }
    }
    
    
    struct PixellatedImage: View, Animatable {
        
        let imageName: String
        var scale: CGFloat
        
        var animatableData: CGFloat {
            get { scale }
            set { scale = newValue }
        }
        
        var body: some View {
            Image(uiImage: pixellatedImage(imageName: imageName, scale: scale))
                .resizable()
        }
    }
    
    
    func pixellatedImage(imageName: String, scale: Double) -> UIImage {
        if let inputImage = UIImage(named: imageName) {
            let context = CIContext(options: nil)
            
            if let currentFilter = CIFilter(name: "CIPixellate") {
                let beginImage = CIImage(image: inputImage)
                currentFilter.setValue(beginImage, forKey: kCIInputImageKey)
                currentFilter.setValue(scale, forKey: kCIInputScaleKey)
                
                if let output = currentFilter.outputImage {
                    if let cgimg = context.createCGImage(output, from: output.extent) {
                        let processedImage = UIImage(cgImage: cgimg)
                        return processedImage
                    }
                }
            }
        }
        return UIImage() // empty default instead of nil
    }