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实现平滑放大效果,不会打乱原有网格布局:
- 添加状态变量记录选中的图片URL和长按触发状态
- 在ScrollView外层添加全屏Overlay,用于展示放大的图片
- 给每个网格图片添加长按手势,触发时更新状态并显示上下文菜单
- 利用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
相关产品推荐
相关产品推荐

