如何在 SwiftUI 中对 CoreImage 滤镜做实时预览
在做图片相关功能时,有一个需求几乎绕不开: 比如: 在 UIKit 时代,我们可能会用 答案是:靠谱,但得用对方式。 这篇文章就从一个最小可用 Demo开始,一步一步把实时滤镜预览这件事讲清楚。 在 SwiftUI 里做 CoreImage 实时预览,核心其实只有三点: 如果你一上来就把所有滤镜计算都丢进 我们先定一个目标: 这是绝大多数滤镜编辑页的基础形态。 先把 CoreImage 的几个核心对象准备好: 这里有两个细节值得注意: 我们先搭一个最基础的页面结构: 到这一步,UI 是没问题的,但还没有任何滤镜逻辑。 关键思路是: 我们先写一个专门负责“生成滤镜图片”的方法: 这段代码做了几件事: 接下来是最关键的一步: 先引入一个新的状态: 然后改造 此时你已经可以看到: 但—— 当你快速拖动 Slider 时,会发现: 原因也很直接: Slider 的 一个简单、有效的方式是: 改造 这样做之后: 这一步,是“能不能实时预览”的分水岭。 如果你用 UIKit 做过类似功能,会发现: 而 SwiftUI 的优势在于: 你只需要保证一件事: 在真实项目中,我一般会遵守这几个原则: 实时预览追求的是“看起来对”, SwiftUI 并不是不适合做图像处理, 一旦你把: 这三件事理顺了,前言
用户拖动参数,图片实时变化。UIImageView + CoreImage + GCD 硬撸。
但到了 SwiftUI,很多人第一反应是:SwiftUI + CoreImage + 实时预览,这事靠谱吗?
先说结论:实时预览的关键点是什么?
body,
那基本等于在和 SwiftUI 的刷新机制正面硬刚。一个最基础的目标效果
Step 1:准备 CoreImage 的基础组件
import SwiftUI
import CoreImage
import CoreImage.CIFilterBuiltins
let context = CIContext()
let filter = CIFilter.gaussianBlur()CIContext 应该尽量复用body 里反复 new CIContextCIContext 本身是重量级对象,频繁创建会直接拖垮性能。Step 2:一个最简单的 SwiftUI 结构
struct ContentView: View {
@State private var intensity: Double = 0.5
let image = UIImage(named: "example")!
var body: some View {
VStack {
Image(uiImage: image)
.resizable()
.scaledToFit()
Slider(value: $intensity)
.padding()
}
}
}Step 3:把 CoreImage 滤镜接进来
不要直接操作 UIImage,而是用 CIImage 作为中间态。func applyProcessing() -> UIImage {
let beginImage = CIImage(image: image)
filter.inputImage = beginImage
filter.radius = Float(intensity * 20)
guard let outputImage = filter.outputImage else {
return image
}
if let cgimg = context.createCGImage(outputImage, from: beginImage!.extent) {
return UIImage(cgImage: cgimg)
}
return image
}UIImage 转成 CIImageCIContext 渲染成 CGImageUIImageStep 4:把实时预览“接”到 SwiftUI 状态上
让 SwiftUI 在 Slider 变化时刷新图片,但不炸性能。@State private var processedImage: UIImage?body:var body: some View {
VStack {
Image(uiImage: processedImage ?? image)
.resizable()
.scaledToFit()
Slider(value: $intensity)
.padding()
.onChange(of: intensity) { _ in
processedImage = applyProcessing()
}
}
}
这还不是一个“能上线”的写法。性能问题从哪开始暴露?
滤镜计算跑在主线程。
onChange 本身就在主线程,
CoreImage 渲染又是 CPU / GPU 混合操作,
自然会影响 UI 响应。Step 5:把滤镜计算移出主线程
用 Task + MainActor 控制线程切换。onChange:.onChange(of: intensity) { _ in
Task.detached {
let output = applyProcessing()
await MainActor.run {
processedImage = output
}
}
}再往前一步:为什么 SwiftUI 特别适合做这件事?
状态更新是轻的,计算是异步的。
一点真实项目里的经验总结
而不是“每一帧都是最终质量”。总结
而是不能用同步思维去写异步计算。
实时滤镜预览这件事,其实比 UIKit 时代要轻松得多。