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

如何为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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.03 15:15:27