SwiftUI中使用scaleEffect时如何阻止视图重绘?
问题根源
你遇到的问题是因为@State var scale的变化会触发整个ContentView的body重建,导致按钮的background(.debug)每次都会重新计算随机颜色,这不仅体现为重绘,还带来不必要的性能开销。scaleEffect本身是视图修饰符,状态变化会驱动视图更新,但我们可以通过一些方法避免不必要的重绘。
解决方案1:用UIKit实现缩放(彻底避免SwiftUI重绘)
利用UIKit的视图变换实现缩放,这种操作属于GPU加速的图层级修改,不会触发视图重绘,是性能最优的方案:
import SwiftUI struct ScalableUIButton: UIViewRepresentable { let title: String let action: () -> Void func makeUIView(context: Context) -> UIButton { let button = UIButton(type: .system) button.setTitle(title, for: .normal) button.setTitleColor(.white, for: .normal) // 仅初始化时生成一次背景色,后续不再重新计算 button.backgroundColor = Color.debug.uiColor button.contentEdgeInsets = UIEdgeInsets(top: 8, left: 16, bottom: 8, right: 16) button.addTarget(context.coordinator, action: #selector(Coordinator.handleTap), for: .touchUpInside) return button } func updateUIView(_ uiView: UIButton, context: Context) {} func makeCoordinator() -> Coordinator { Coordinator(parent: self) } class Coordinator: NSObject { private let parent: ScalableUIButton init(parent: ScalableUIButton) { self.parent = parent } @objc func handleTap() { parent.action() // 执行缩放动画 UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 0.68, initialSpringVelocity: 0) { parent.uiView?.transform = CGAffineTransform(scaleX: 1.1, y: 1.1) } completion: { _ in UIView.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 0.7) { parent.uiView?.transform = .identity } } } } } // 给Color添加转UIColor的扩展 extension Color { var uiColor: UIColor { UIColor(self) } } // 原有的debug颜色扩展 @available(iOS 13.0, *) public extension ShapeStyle where Self == Color { static var debug: Color { #if DEBUG return Color( red: .random(in: 0...1), green: .random(in: 0...1), blue: .random(in: 0...1) ).opacity(0.7) #else return Color(.clear) #endif } } // 使用示例 struct ContentView: View { var body: some View { VStack { ScalableUIButton(title: "Button") { // 这里添加按钮点击后的业务逻辑 print("Button tapped") } } } }
解决方案2:缓存静态内容(减少重绘开销)
如果不想切换到UIKit,可以将不需要随动画变化的内容(比如背景色)缓存起来,避免每次重绘时重复计算:
struct ContentView: View { @State var scale = false // 仅初始化时生成一次背景色,后续保持不变 @State private var cachedDebugColor = Color.debug var body: some View { VStack { Button { withAnimation(.interactiveSpring(response: 0.5, dampingFraction: 0.68, blendDuration: 0.6)) { scale.toggle() } DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { withAnimation(.spring()) { scale.toggle() } } } label: { Text("Button") .padding() .foregroundColor(.white) .background(cachedDebugColor) .scaleEffect(scale ? 1.1 : 1) } } } } @available(iOS 13.0, *) public extension ShapeStyle where Self == Color { static var debug: Color { #if DEBUG return Color( red: .random(in: 0...1), green: .random(in: 0...1), blue: .random(in: 0...1) ).opacity(0.7) #else return Color(.clear) #endif } }
解决方案3:用AnimatableModifier封装缩放逻辑
通过自定义AnimatableModifier,让动画参数的变化仅影响缩放效果,缩小视图重绘的范围:
struct ScaleAnimationModifier: AnimatableModifier { var scale: CGFloat = 1 // 绑定动画数据,SwiftUI自动处理插值 var animatableData: CGFloat { get { scale } set { scale = newValue } } func body(content: Content) -> some View { content.scaleEffect(scale) } } extension View { func scaleAnimation(to scale: CGFloat) -> some View { modifier(ScaleAnimationModifier(scale: scale)) } } struct ContentView: View { @State var scale: CGFloat = 1 var body: some View { VStack { Button { // 直接驱动动画参数变化 withAnimation(.interactiveSpring(response: 0.5, dampingFraction: 0.68, blendDuration: 0.6)) { scale = 1.1 } DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { withAnimation(.spring()) { scale = 1 } } } label: { Text("Button") .padding() .foregroundColor(.white) .background(.debug) .scaleAnimation(to: scale) } } } } @available(iOS 13.0, *) public extension ShapeStyle where Self == Color { static var debug: Color { #if DEBUG return Color( red: .random(in: 0...1), green: .random(in: 0...1), blue: .random(in: 0...1) ).opacity(0.7) #else return Color(.clear) #endif } }
内容的提问来源于stack exchange,提问作者fdvfarzin
相关产品推荐
相关产品推荐

