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

如何在SwiftUI中通过UIViewRepresentable正确实现MTTransitions(MetalPetal)的图片过渡动画?

如何在SwiftUI中通过UIViewRepresentable正确实现MTTransitions(MetalPetal)的图片过渡动画?

我之前在把MetalPetal的MTTransitions整合到SwiftUI时也踩过一模一样的坑,你的代码里有几个关键细节没处理好,导致除了.none之外的过渡动画无法正常显示。下面是具体的问题分析和修复方案:

问题核心原因

  1. 过渡重复触发或状态混乱:updateUIView会被SwiftUI频繁调用,当前代码每次showToImage为true时都会尝试触发过渡,导致过渡逻辑被打乱,中间帧被覆盖。
  2. 主线程调度与视图渲染不同步:虽然你在updater里用了DispatchQueue.main.async,但SwiftUI的UIViewRepresentable更新机制可能干扰MTIImageView的帧渲染,导致过渡中间帧无法及时显示。
  3. 过渡实例复用问题:在父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)
    }
}

关键注意事项

  1. 用Coordinator管理状态:这是避免SwiftUI频繁调用updateUIView导致过渡混乱的核心,确保过渡只在showToImage状态变化时触发一次。
  2. 每次创建新的过渡实例:不要复用同一个MTTransition对象,否则会残留上一次过渡的状态,导致动画异常。
  3. 明确视图尺寸约束:MTIImageView必须有明确的大小,否则过渡动画会因为视图无尺寸而无法显示。
  4. 主线程更新UI:MTTransition的updater回调运行在渲染线程,必须切回主线程更新MTIImageView的image属性。

这样修改后,你测试MTCrossWarpTransition、MTZoomTransition等过渡类型应该都能正常显示动画效果了。

内容来源于stack exchange

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.07 12:13:04