SwiftUI:如何提取ButtonStyle中的动画以实现复用?
你的代码无法触发动画的核心问题是错误地给configuration添加了@State属性包装器:ButtonStyleConfiguration是由系统传递的外部状态值,不需要用@State管理,添加后会阻断值的更新,导致动画无法响应isPressed的变化。
正确的复用方案
方案1:修正原修饰器结构
直接移除@State,让修饰器接收外部传递的configuration即可:
struct FButtonAnimation: ViewModifier { // 移除@State,直接使用普通属性接收配置 let configuration: ButtonStyleConfiguration func body(content: Content) -> some View { content .scaleEffect(configuration.isPressed ? 0.9 : 1) .animation(.easeOut(duration: 0.2), value: configuration.isPressed) } } struct FButtonStyle: ButtonStyle { func makeBody(configuration: Configuration) -> some View { configuration.label .frame(maxWidth: .infinity) .padding([.top, .bottom], 6) .foregroundColor(configuration.role == .cancel ? .black : .white) .background((configuration.role == .cancel ? Color.white : Color.accentColor).opacity(configuration.isPressed ? 0.8 : 1)) .cornerRadius(6) .shadow(color: .black.opacity(0.25), radius: 2, x: 0, y: 0.5) .modifier(FButtonAnimation(configuration: configuration)) } }
方案2:更通用的动画修饰器(推荐)
如果希望动画能在非ButtonStyle场景也复用,建议将isPressed作为单独参数传递,而非依赖整个ButtonStyleConfiguration,同时给修饰器添加默认参数提升易用性:
// 通用的按压缩放动画修饰器 struct PressScaleAnimation: ViewModifier { let isPressed: Bool let scaleFactor: CGFloat let duration: Double // 提供默认参数,简化调用 init(isPressed: Bool, scaleFactor: CGFloat = 0.9, duration: Double = 0.2) { self.isPressed = isPressed self.scaleFactor = scaleFactor self.duration = duration } func body(content: Content) -> some View { content .scaleEffect(isPressed ? scaleFactor : 1) .animation(.easeOut(duration: duration), value: isPressed) } } // 扩展View,支持链式调用 extension View { func pressScaleAnimation(isPressed: Bool, scaleFactor: CGFloat = 0.9, duration: Double = 0.2) -> some View { self.modifier(PressScaleAnimation(isPressed: isPressed, scaleFactor: scaleFactor, duration: duration)) } }
在ButtonStyle中使用:
struct FButtonStyle: ButtonStyle { func makeBody(configuration: Configuration) -> some View { configuration.label .frame(maxWidth: .infinity) .padding([.top, .bottom], 6) .foregroundColor(configuration.role == .cancel ? .black : .white) .background((configuration.role == .cancel ? Color.white : Color.accentColor).opacity(configuration.isPressed ? 0.8 : 1)) .cornerRadius(6) .shadow(color: .black.opacity(0.25), radius: 2, x: 0, y: 0.5) // 直接调用扩展方法,传递按压状态 .pressScaleAnimation(isPressed: configuration.isPressed) } }
这种方式的优势是:不仅能在ButtonStyle里复用,还能给其他可交互View添加相同动画,比如自定义的TapGesture组件:
@State private var isTapped = false Text("点击缩放") .onTapGesture { isTapped.toggle() DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { isTapped = false } } .pressScaleAnimation(isPressed: isTapped)
内容的提问来源于stack exchange,提问作者Peter Lapisu
相关产品推荐
相关产品推荐

