PAYATHON 2026

SwiftUI iOS 26 如何按状态更新交互式玻璃的 tint 而不中断动画?

数据小许

结论

原代码中的 .tint(saved ? overlayCol : overlayCol.opacity(0.1)) 写法有效。tint 随状态更新的动画和 .interactive() 提供的按压反馈是两种不同的动画行为。

可以先用 withAnimation 更新状态。如果仍出现“膨胀后突然缩回”,更换条件表达式也不一定能解决问题。查阅到的 Apple 文档没有说明如何在修改 tint 时保留正在进行的交互动画,也没有承诺 withAnimation 能消除这类中断。

如果可以调整交互效果,可以移除 .interactive(),让 GlassEffectContainer 处理状态切换时的玻璃过渡。这种做法改变了动画的实现方式,效果需要在目标系统和设备上验证。

为什么切换颜色可能影响按压反馈?

Apple 将 .interactive() 定义为让玻璃响应触摸和指针交互的配置;GlassEffectContainer 则用于协调多个玻璃视图的融合与过渡。两者的职责不同。Apple Liquid Glass 文档

从现象看,状态更新与按压结束时的反馈可能发生了重叠,可以从这个方向排查。仅凭这段代码,无法确认系统内部是否重建了效果,也不能断定问题由三元表达式引起。该表达式只是计算一个新的颜色值,没有显式切换两棵视图分支。

先尝试显式动画

保留现有 .glassEffect(...),只修改按钮操作:

Button(action: {
    withAnimation {
        saved.toggle()
    }
}) {
    // 保留原来的按钮内容
}

这样会让状态更新发生在动画事务中,但不保证玻璃 tint 的变化能与尚未结束的按压反馈连续衔接。如果长按后松开仍出现突变,就需要考虑替代交互,不宜继续叠加 .animation(...)。

替代方案:让容器处理状态过渡

如果希望点击后玻璃效果连贯变化,可以将两种状态放在同一个 GlassEffectContainer 中,移除按钮玻璃上的 .interactive(),并用 withAnimation 切换状态。

下面用 SF Symbols 展示代码结构;原代码中的 Image("...") 读取项目图片资源,使用时可以换回自己的资源名称。

import SwiftUI

@available(iOS 26.0, *)
struct BookmarkGlassButton: View {
    @State private var saved = false
    let overlayCol: Color

    var body: some View {
        GlassEffectContainer {
            if saved {
                bookmarkButton(isSaved: true)
                    .glassEffect(
                        .clear.tint(overlayCol),
                        in: .circle
                    )
            } else {
                bookmarkButton(isSaved: false)
                    .glassEffect(
                        .clear.tint(overlayCol.opacity(0.1)),
                        in: .circle
                    )
            }
        }
    }

    private func bookmarkButton(isSaved: Bool) -> some View {
        Button {
            withAnimation {
                saved.toggle()
            }
        } label: {
            Image(systemName: isSaved ? "bookmark.fill" : "bookmark")
                .resizable()
                .scaledToFit()
                .frame(width: 14, height: 14)
                .foregroundStyle(.white)
                .padding(8)
        }
        .buttonStyle(.plain)
        .accessibilityLabel(isSaved ? "取消收藏" : "收藏")
    }
}

示例让条件分支中的玻璃效果参与容器过渡,并使用 .buttonStyle(.plain) 避免默认按钮样式带来额外表现。关于容器协调玻璃过渡、使用 withAnimation 驱动状态变化,可以参考 Apple 的自定义活动徽章示例。这段代码是针对当前问题组织的替代实现,未在目标设备上实测。

这个方案无法保留原来的按压膨胀反馈。如果必须完整保留 .interactive() 的长按、松手动画,它不能算作等价修复。

验证时需要注意什么?

分别检查快速点击、长按后松开和连续切换,确认异常出现在按压阶段还是状态过渡阶段。不要把 .glassEffectTransition(.identity) 当作关闭按压动画的开关:该 API 控制玻璃效果加入或移出视图层级时的过渡。Apple GlassEffectTransition 文档

另外,overlayCol.opacity(0.1) 仍然是带透明度的 tint,和不设置 tint 不同。仅凭 Phone 和 Reminders 的外观,无法断定它们使用了相同的 SwiftUI 实现。尤其是按钮内嵌的蓝色圆形,可能需要单独的视觉层,不能仅靠修改整块玻璃的 tint 来推断实现。

如果必须保留现有按压反馈,应记录实际 iOS 和 SDK 版本,用最小示例复现后提交 Feedback,并在目标版本上验证。现有材料不足以确认某个版本已经修复。

备注:内容仅供参考。