PAYATHON 2026

如何实现 ios26 Photos app 右上角 liquid glass 按钮的交互动画?

技术老齐

优先使用 iOS 26 的系统工具栏和 Menu,让系统处理右上角按钮的玻璃外观、按压反馈和菜单呈现动画。需要自行控制按钮的展开、分离和融合时,再使用 GlassEffectContainer 与 glassEffectID(_:in:)。

题目未提供可查看的动图,无法确定目标效果是按压时的弹性反馈、菜单展开,还是一个按钮变成多个按钮。这几种效果的实现方式各有不同,也无法保证与 Photos 的内部实现完全一致。

优先实现系统工具栏菜单

使用支持 iOS 26 SDK 的 Xcode,在 NavigationStack 的 .toolbar 中放置 Menu,将位置设为 .topBarTrailing。系统工具栏会提供对应的 Liquid Glass 样式,不需要再给按钮叠加玻璃背景。Apple 也建议优先使用标准导航结构、工具栏和控件来采用新设计,参见 Build a SwiftUI app with the new design。

import SwiftUI

@available(iOS 26.0, *)
struct RecentlyDeletedView: View {
    let onSelect: () -> Void
    let onRecoverAll: () -> Void

    var body: some View {
        NavigationStack {
            Text("最近删除的照片")
                .frame(maxWidth: .infinity, maxHeight: .infinity)
                .navigationTitle("最近删除")
                .toolbar {
                    ToolbarItem(placement: .topBarTrailing) {
                        Menu {
                            Button(
                                "选择",
                                systemImage: "checkmark.circle",
                                action: onSelect
                            )

                            Button(
                                "恢复全部",
                                systemImage: "arrow.uturn.backward",
                                action: onRecoverAll
                            )
                        } label: {
                            Label("更多", systemImage: "ellipsis")
                        }
                    }
                }
        }
    }
}

菜单项可替换为实际业务操作。如果页面已经在 NavigationStack 中,只需保留 .toolbar,不必再嵌套导航容器。

先运行这个最小示例,确认系统菜单是否符合预期。用 overlay 放置圆形按钮,再切换自定义面板,不会自动获得同样的系统菜单呈现行为。

如果只需要按钮的按压反馈

工具栏以外的独立按钮,优先使用以下写法:

Button("更多", systemImage: "ellipsis") {
    // 处理点击
}
.buttonStyle(.glass)
.buttonBorderShape(.circle)

自行绘制的交互视图可以使用 .glassEffect(.regular.interactive(), in: .capsule)。其中,.interactive() 会启用玻璃对触摸的缩放、弹性和闪光反馈,点击后的业务操作仍需由 Button 等控件处理。它不负责菜单展开或多个按钮之间的融合动画。参见 Apple 的 Liquid Glass 交互效果说明。

如果需要按钮展开、分离和融合

要自定义展开动画,需要将相关玻璃元素放在同一个 GlassEffectContainer 中,为各元素设置稳定的 glassEffectID,再通过 withAnimation 改变状态。Apple 的 Landmarks 自定义徽章示例也采用了这种方式。

以下示例中,点击“更多”会展开一个额外操作,再次点击则收回:

import SwiftUI

@available(iOS 26.0, *)
struct ExpandingGlassActions: View {
    @Namespace private var namespace
    @State private var isExpanded = false

    let onSelect: () -> Void

    var body: some View {
        GlassEffectContainer(spacing: 24) {
            HStack(spacing: 12) {
                if isExpanded {
                    Button(
                        "选择",
                        systemImage: "checkmark.circle",
                        action: onSelect
                    )
                    .buttonStyle(.glass)
                    .glassEffectID("select", in: namespace)
                }

                Button {
                    withAnimation {
                        isExpanded.toggle()
                    }
                } label: {
                    Image(
                        systemName: isExpanded ? "xmark" : "ellipsis"
                    )
                    .frame(width: 24, height: 24)
                }
                .buttonStyle(.glass)
                .buttonBorderShape(.circle)
                .glassEffectID("toggle", in: namespace)
                .accessibilityLabel(
                    isExpanded ? "收起操作" : "展开操作"
                )
            }
        }
        .padding()
    }
}

这个自定义浮动控件可以放在页面右上角,但不会创建系统 Menu。

GlassEffectContainer(spacing:) 控制玻璃形状接近时开始融合的距离,HStack(spacing:) 控制实际布局间距。示例中的 24 和 12 仅用于演示,需要根据按钮尺寸调整。参见 GlassEffectContainer。

实现时需要检查的地方

  • 同一容器内同时存在的玻璃元素,应使用不同且稳定的 ID,不要在每次刷新时生成新的 UUID()。
  • 系统工具栏按钮已有玻璃背景,再叠加 .glassEffect() 会造成重复材质。
  • 自定义玻璃视图应先确定内容尺寸和内边距,再应用玻璃效果,否则玻璃边界可能不符合预期。
  • 需要兼容旧系统时,用 if #available(iOS 26.0, *) 选择上述视图,并为旧系统提供普通按钮或材质背景。
  • 在目标设备上检查菜单呈现,以及深浅色模式和辅助功能设置下的效果。系统版本、背景内容和“减少动态效果”等设置都可能影响最终观感。

备注:内容仅供参考。