You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在SwiftUI中为Core Image Filter添加动画效果

CIFilter动画过渡的标准实现方案

你想要的「和SwiftUI内置修饰符易用性一致的自定义CIFilter动画Modifier」方案完全可行,也是苹果官方推荐的实现路径,不需要照搬WWDC示例里的重型MTLView流程。


为什么直接用CIFilter会没有过渡效果

SwiftUI内置的.blur()、.opacity()这类修饰符天生遵守Animatable协议,动画触发时系统会在设定时长内自动逐帧计算参数插值,自动刷新视图。
你之前直接用CIImage/CGImage/UIImage实现时,只在参数变化的起点和终点触发了滤镜渲染,没有把滤镜的可调参数和SwiftUI的动画插值系统绑定,自然只会瞬时跳变,没有过渡过程。

WWDC 2022里演示的MTLView+CIRenderDestination方案是面向摄像头实时流、高帧率特效这类重负载场景的,普通的静态图片、视图的滤镜过渡动画完全不需要用这套逻辑。


具体实现方式

核心逻辑非常简单:给自定义ViewModifier遵守Animatable协议,把CIFilter需要动态变化的参数(比如去饱和程度、像素块大小)设为animatableData,让系统在动画过程中自动传入插值后的参数值,每一次参数更新就实时渲染一次滤镜结果即可,调用方式和内置修饰符完全没有区别。

示例1:2秒去饱和转灰度效果

import SwiftUI
import CoreImage

struct DesaturateEffect: ViewModifier, Animatable {
    // 0=全彩色 1=全灰度,作为动画插值变量
    var desaturation: Double
    // 复用CIContext避免重复创建开销
    private let ciContext = CIContext()
    
    var animatableData: Double {
        get { desaturation }
        set { desaturation = newValue }
    }
    
    func body(content: Content) -> some View {
        content
            .overlay {
                if let filteredImage = renderFilter(content: content) {
                    Image(uiImage: filteredImage)
                        .allowsHitTesting(false)
                }
            }
    }
    
    private func renderFilter(content: Content) -> UIImage? {
        let renderer = ImageRenderer(content: content)
        guard let rawUIImage = renderer.uiImage,
              let ciImage = CIImage(image: rawUIImage),
              let filter = CIFilter(name: "CIColorControls") else {
            return nil
        }
        filter.setValue(ciImage, forKey: kCIInputImageKey)
        // 饱和度随插值参数变化
        filter.setValue(1 - Float(desaturation), forKey: kCIInputSaturationKey)
        
        guard let outputCIImage = filter.outputImage,
              let cgImage = ciContext.createCGImage(outputCIImage, from: outputCIImage.extent) else {
            return nil
        }
        return UIImage(cgImage: cgImage)
    }
}

extension View {
    func desaturate(_ level: Double) -> some View {
        modifier(DesaturateEffect(desaturation: level))
    }
}

调用时和内置动画写法完全一致:

// 触发时2秒内用EaseInOut曲线转灰度
Image("testPhoto")
    .desaturate(showGrayscale ? 1 : 0)
    .animation(.easeInOut(duration: 2), value: showGrayscale)

示例2:0.8秒像素化效果还原

逻辑和去饱和完全一致,只需要替换滤镜类型,把插值参数换成像素块尺寸即可:

  • 像素化状态:像素块大小设为20
  • 清晰状态:像素块大小设为1(和原图无差异)
  • 同样把像素块尺寸参数绑定到animatableData,系统会自动按照你指定的动画曲线(比如EaseInOut)逐帧计算中间值,自动完成从块状到清晰的过渡,不需要你自己写定时器或者帧刷新逻辑。

优化提示

  • iOS 17+可以直接用内置的.visualEffect修饰符,直接在闭包内给内容应用CIFilter,不需要自己写快照逻辑,代码量会更少。
  • 不要每次渲染都新建CIContext,设为Modifier的全局复用实例可以大幅减少卡顿。
  • 所有CIFilter的数值类参数(亮度、对比度、模糊半径、像素大小、色相角度等)都可以用这套方案做动画,不需要针对每个滤镜写特殊的动画逻辑。

内容的提问来源于stack exchange,提问作者God of Biscuits

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.28 11:21:42