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

SwiftUI实现Photos应用长按图片弹出菜单时的图片放大效果

复刻苹果Photos应用长按图片的放大上下文菜单效果

需求

实现苹果Photos应用的长按图片交互:弹出包含操作选项的上下文菜单,重点还原图片从网格位置平滑放大至全屏居中的视觉效果。

当前代码

let columns = [
    GridItem(.adaptive(minimum: 100), spacing: 2),
    GridItem(.adaptive(minimum: 100), spacing: 2),
    GridItem(.adaptive(minimum: 100), spacing: 2),
    GridItem(.adaptive(minimum: 100), spacing: 2),
    GridItem(.adaptive(minimum: 100), spacing: 2)
]

var body: some View {
    ScrollView {
        LazyVGrid(columns: columns, spacing: 2) {
            ForEach(imageUrls, id: \.self) { imageUrl in
                AsyncImage(url: imageUrl) { image in
                    image
                        .resizable()
                        .aspectRatio(1, contentMode: .fill)
                        .contextMenu {
                            Button {
                                print("Save image")
                            } label: {
                                Label("Save to Photos", systemImage: "square.and.arrow.down")
                            }
                        }
                } placeholder: {
                    ProgressView()
                }
            }
        }
    }
}

解决方案

通过状态变量跟踪选中的图片,配合全屏Overlay实现平滑放大效果,不会打乱原有网格布局:

  1. 添加状态变量记录选中的图片URL和长按触发状态
  2. 在ScrollView外层添加全屏Overlay,用于展示放大的图片
  3. 给每个网格图片添加长按手势,触发时更新状态并显示上下文菜单
  4. 利用SwiftUI内置动画实现从网格位置到全屏的平滑过渡

修改后的完整代码:

import SwiftUI

struct PhotoGridView: View {
    let columns = [
        GridItem(.adaptive(minimum: 100), spacing: 2),
        GridItem(.adaptive(minimum: 100), spacing: 2),
        GridItem(.adaptive(minimum: 100), spacing: 2),
        GridItem(.adaptive(minimum: 100), spacing: 2),
        GridItem(.adaptive(minimum: 100), spacing: 2)
    ]
    let imageUrls: [URL]
    // 跟踪选中的图片URL和菜单触发状态
    @State private var selectedImageUrl: URL?
    @State private var isShowingMenu = false

    var body: some View {
        ScrollView {
            LazyVGrid(columns: columns, spacing: 2) {
                ForEach(imageUrls, id: \.self) { imageUrl in
                    AsyncImage(url: imageUrl) { image in
                        image
                            .resizable()
                            .aspectRatio(1, contentMode: .fill)
                            .clipShape(RoundedRectangle(cornerRadius: 8)) // 可选:贴近Photos风格添加圆角
                            // 替换默认contextMenu,用长按手势触发自定义流程
                            .onLongPressGesture {
                                selectedImageUrl = imageUrl
                                withAnimation(.spring(response: 0.3, dampingFraction: 0.8)) {
                                    isShowingMenu = true
                                }
                            }
                    } placeholder: {
                        ProgressView()
                    }
                }
            }
        }
        // 全屏Overlay承载放大图片和上下文菜单
        .overlay(
            Group {
                if let selectedUrl = selectedImageUrl, isShowingMenu {
                    // 半透明背景,点击可关闭菜单
                    Color.black.opacity(0.8)
                        .ignoresSafeArea()
                        .onTapGesture {
                            withAnimation(.spring(response: 0.3, dampingFraction: 0.8)) {
                                isShowingMenu = false
                            }
                            DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
                                selectedImageUrl = nil
                            }
                        }
                    
                    // 放大显示的图片
                    AsyncImage(url: selectedUrl) { image in
                        image
                            .resizable()
                            .aspectRatio(contentMode: .fit)
                            .frame(maxWidth: .infinity, maxHeight: .infinity)
                            .padding(20)
                            .contextMenu {
                                Button {
                                    print("Save image")
                                    closeMenu()
                                } label: {
                                    Label("Save to Photos", systemImage: "square.and.arrow.down")
                                }
                                
                                Button {
                                    print("Share image")
                                    closeMenu()
                                } label: {
                                    Label("Share", systemImage: "square.and.arrow.up")
                                }
                                
                                Button {
                                    print("Favorite image")
                                    closeMenu()
                                } label: {
                                    Label("Favorite", systemImage: "star")
                                }
                            }
                    } placeholder: {
                        ProgressView()
                            .foregroundColor(.white)
                    }
                }
            }
        )
    }
    
    // 封装菜单关闭逻辑
    private func closeMenu() {
        withAnimation(.spring(response: 0.3, dampingFraction: 0.8)) {
            isShowingMenu = false
        }
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
            selectedImageUrl = nil
        }
    }
}

关键说明

  • 状态隔离:用selectedImageUrl和isShowingMenu控制放大层状态,完全不影响原有网格布局
  • 动画还原:弹簧动画参数贴近Photos应用的过渡质感,保证交互流畅自然
  • 交互闭环:点击背景或菜单选项后,先执行缩小动画再清空选中状态,避免视觉突兀
  • 布局独立:放大层通过Overlay实现,与网格布局完全分离,不会干扰网格的排列逻辑

内容的提问来源于stack exchange,提问作者Luke C

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.30 13:07:44