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

SwiftUI:如何提取ButtonStyle中的动画以实现复用?

解决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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.25 16:07:09