如何为SwiftUI文档型图片浏览器实现过渡动画?
实现文档型图片App的缩略图到预览页缩放过渡动画
问题描述
我跟着示例搭建了一个UTType为image的文档型App,代码能正常运行,但选择图片项时没有过渡效果。想要实现类似系统「文件」App那种从图片缩略图平滑缩放至预览页的动画,尝试给MyImageFormatViewer添加动画/过渡后效果不理想,想知道怎么获取图片项的尺寸坐标,或者有没有更简便的实现方案。
附原代码:
import SwiftUI import UniformTypeIdentifiers @main struct hhhApp: App { //@State var tag: var body: some Scene { DocumentGroup(viewing: MyImageFormatDocument.self) {file in MyImageFormatViewer(document: file.$document) //.transition(.scale.animation(.easeInOut(duration: 2))) .onAppear{print("appear")} } } } struct MyImageFormatViewer:View{ //@State private var image: UIImage = document.image @State private var showDetail = false @Binding var document:MyImageFormatDocument var body: some View{ Image(uiImage: document.image) } } struct MyImageFormatDocument: FileDocument { static var readableContentTypes = [UTType.image] var image = UIImage() // create from a file init(configuration: ReadConfiguration) throws { guard let data = configuration.file.regularFileContents, let decodedImage: UIImage = UIImage(data: data) else { throw CocoaError(.fileReadCorruptFile) } image = decodedImage } // write func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper { let data = image.pngData()! //return .init(regularFileWithContents: data) return FileWrapper(regularFileWithContents: data) } init(initialImage: UIImage = UIImage()) { image = initialImage } init(fileWrapper: FileWrapper, contentType: UTType) throws { if let data = fileWrapper.regularFileContents { image = UIImage(data: data) ?? UIImage() print("init") } } func write(to fileWrapper: inout FileWrapper, contentType: UTType) throws { } }
简便实现方案:用matchedGeometryEffect做共享元素过渡
系统「文件」App的缩放动画属于共享元素过渡,SwiftUI的matchedGeometryEffect可以直接实现这个效果。因为默认的DocumentGroup无法直接控制系统文件选择器的缩略图,所以我们换成自定义的文件选择+预览流程,具体步骤如下:
1. 修改App入口,加入命名空间管理
命名空间用来标记动画中共享的元素,确保缩略图和预览图能对应上:
@main struct hhhApp: App { @State private var selectedDocument: MyImageFormatDocument? @Namespace private var animationNamespace var body: some Scene { WindowGroup { if let document = selectedDocument { MyImageFormatViewer(document: $selectedDocument, animationNamespace: animationNamespace) } else { FilePickerView(selectedDocument: $selectedDocument, animationNamespace: animationNamespace) } } } }
2. 实现自定义文件选择视图
在这个视图里展示图片缩略图,给每个缩略图绑定matchedGeometryEffect:
struct FilePickerView: View { @Binding var selectedDocument: MyImageFormatDocument? let animationNamespace: Namespace.ID var body: some View { NavigationStack { List(try! FileManager.default.contentsOfDirectory(at: URL.documentsDirectory, includingPropertiesForKeys: nil).filter { url in guard let utType = UTType(filenameExtension: url.pathExtension) else { return false } return UTType.image.conforms(to: utType) }, id: \.self) { url in if let imageData = try? Data(contentsOf: url), let image = UIImage(data: imageData) { Button(action: { withAnimation(.easeInOut(duration: 0.3)) { selectedDocument = try? MyImageFormatDocument(configuration: .init(file: .init(url: url))) } }) { Image(uiImage: image) .resizable() .scaledToFit() .frame(width: 80, height: 80) .matchedGeometryEffect(id: url, in: animationNamespace) } } } .navigationTitle("图片文件") } } }
3. 修改预览视图,绑定同一命名空间
让预览图和对应的缩略图共享同一个id和命名空间,实现缩放过渡:
struct MyImageFormatViewer: View { @Binding var document: MyImageFormatDocument? let animationNamespace: Namespace.ID var body: some View { ZStack { Color.black.opacity(0.8) .ignoresSafeArea() if let doc = document { Image(uiImage: doc.image) .resizable() .scaledToFit() .matchedGeometryEffect(id: doc.fileURL!, in: animationNamespace) .onTapGesture { withAnimation(.easeInOut(duration: 0.3)) { document = nil } } } } } }
4. 更新文档模型,保存文件URL
需要在MyImageFormatDocument里添加属性保存文件URL,用来作为共享元素的唯一标识:
struct MyImageFormatDocument: FileDocument { static var readableContentTypes = [UTType.image] var image = UIImage() var fileURL: URL? // 新增:保存文件URL init(configuration: ReadConfiguration) throws { fileURL = configuration.file.url // 初始化时保存URL guard let data = configuration.file.regularFileContents, let decodedImage: UIImage = UIImage(data: data) else { throw CocoaError(.fileReadCorruptFile) } image = decodedImage } // 其他原有代码保持不变... func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper { let data = image.pngData()! return FileWrapper(regularFileWithContents: data) } init(initialImage: UIImage = UIImage()) { image = initialImage } init(fileWrapper: FileWrapper, contentType: UTType) throws { if let data = fileWrapper.regularFileContents { image = UIImage(data: data) ?? UIImage() } } func write(to fileWrapper: inout FileWrapper, contentType: UTType) throws { } }
效果说明
点击列表里的缩略图时,图片会平滑缩放至全屏预览;点击预览图时,又会缩回到列表中的原位置,和系统「文件」App的动画效果一致。
如果一定要使用默认的DocumentGroup,则需要通过UIKit的过渡动画API实现,复杂度较高,不如自定义方案简便。
内容的提问来源于stack exchange,提问作者user21257948
相关产品推荐
相关产品推荐

