如何在SwiftUI中通过UIViewRepresentable正确实现MTTransitions(MetalPetal)的图片过渡动画?
如何在SwiftUI中通过UIViewRepresentable正确实现MTTransitions(MetalPetal)的图片过渡动画?
我之前在把MetalPetal的MTTransitions整合到SwiftUI时也踩过一模一样的坑,你的代码里有几个关键细节没处理好,导致除了.none之外的过渡动画无法正常显示。下面是具体的问题分析和修复方案:
问题核心原因
- 过渡重复触发或状态混乱:
updateUIView会被SwiftUI频繁调用,当前代码每次showToImage为true时都会尝试触发过渡,导致过渡逻辑被打乱,中间帧被覆盖。 - 主线程调度与视图渲染不同步:虽然你在
updater里用了DispatchQueue.main.async,但SwiftUI的UIViewRepresentable更新机制可能干扰MTIImageView的帧渲染,导致过渡中间帧无法及时显示。 - 过渡实例复用问题:在父View里创建的
transition实例如果被重复使用,可能会残留之前的状态,影响新的过渡效果。
修复后的完整实现
第一步:修正MetalImageTransitionView的实现
我们需要用Coordinator来管理过渡的触发状态,避免重复执行,同时确保每一帧的过渡画面都能正确渲染:
import SwiftUI import MetalPetal struct MetalImageTransitionView: UIViewRepresentable { let fromImage: MTIImage let toImage: MTIImage let effect: MTTransition let duration: Double @Binding var showToImage: Bool // 用Coordinator管理过渡状态,防止重复触发 class Coordinator: NSObject { var hasTriggeredTransition = false } func makeCoordinator() -> Coordinator { Coordinator() } func makeUIView(context: Context) -> MTIImageView { let imageView = MTIImageView() imageView.image = fromImage imageView.contentMode = .scaleAspectFill imageView.clipsToBounds = true return imageView } func updateUIView(_ uiView: MTIImageView, context: Context) { // 只有当showToImage从false变为true,且未触发过过渡时才执行 guard showToImage, !context.coordinator.hasTriggeredTransition else { // 如果切换回fromImage,重置状态以便下次触发 if !showToImage { uiView.image = fromImage context.coordinator.hasTriggeredTransition = false } return } // 标记为已触发,避免重复执行 context.coordinator.hasTriggeredTransition = true effect.duration = duration // 执行过渡动画 effect.transition(from: fromImage, to: toImage, updater: { [weak uiView] frameImage in // 必须回到主线程更新UIKit视图 DispatchQueue.main.async { uiView?.image = frameImage } }, completion: { [weak uiView, weak context] _ in DispatchQueue.main.async { // 确保最终显示目标图 uiView?.image = self.toImage // 重置状态,支持再次触发过渡 context?.coordinator.hasTriggeredTransition = false } }) } }
第二步:修正父视图MetalWaveTransitionView
调整过渡实例的创建方式,确保每次过渡都有干净的状态,同时给MTIImageView添加明确的尺寸约束:
struct MetalWaveTransitionView: View { @State private var fromImage: MTIImage? @State private var toImage: MTIImage? @State private var showToImage = false private let transitionDuration: TimeInterval = 1.5 var body: some View { ZStack { if let from = fromImage, let to = toImage { MetalImageTransitionView( fromImage: from, toImage: to, // 每次创建新的过渡实例,避免状态残留 effect: MTCrossWarpTransition(), duration: transitionDuration, showToImage: $showToImage ) // 确保视图填满父容器,动画能完整展示 .frame(maxWidth: .infinity, maxHeight: .infinity) .clipped() } } .onAppear { loadImages() // 延迟触发过渡,确保图片加载完成 DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { showToImage = true } } // 可选:添加按钮测试重复切换 .overlay( Button("切换图片") { showToImage.toggle() } .foregroundColor(.white) .padding() .background(Color.blue) .cornerRadius(8), alignment: .bottom ) } private func loadImages() { guard let fromUIImage = UIImage(named: "imgCals"), let toUIImage = UIImage(named: "imgBMI"), let fromCGImage = fromUIImage.cgImage, let toCGImage = toUIImage.cgImage else { return } fromImage = MTIImage(cgImage: fromCGImage, options: [.SRGB: false], isOpaque: true) toImage = MTIImage(cgImage: toCGImage, options: [.SRGB: false], isOpaque: true) } }
关键注意事项
- 用Coordinator管理状态:这是避免SwiftUI频繁调用
updateUIView导致过渡混乱的核心,确保过渡只在showToImage状态变化时触发一次。 - 每次创建新的过渡实例:不要复用同一个
MTTransition对象,否则会残留上一次过渡的状态,导致动画异常。 - 明确视图尺寸约束:MTIImageView必须有明确的大小,否则过渡动画会因为视图无尺寸而无法显示。
- 主线程更新UI:MTTransition的
updater回调运行在渲染线程,必须切回主线程更新MTIImageView的image属性。
这样修改后,你测试MTCrossWarpTransition、MTZoomTransition等过渡类型应该都能正常显示动画效果了。
内容来源于stack exchange
相关产品推荐
相关产品推荐

