Search code examples
swiftswiftuiuiimage

Problems exporting a list of images with .fileExporter()


I'm trying to generate some images from SwiftUI views, and then export them to the filesystem. I've looked online and can't seem to get things to work. Tapping the file export button does nothing.

Should I be converting the images to a different format? I feel like I'm missing something obvious, this is harder than it should be.

struct Temp: View {
    let screenshots: [ScreenshotModel]
    let size: CGSize

    @State private var isShowingExport = false

    @State private var exportImages = [UIImage]()

    var body: some View {
        ScrollView {
            VStack {
                ForEach(exportImages, id: \.self) {
                    Image(uiImage: $0)
                }

                Button("Export to Files", systemImage: "folder") {
                    isShowingExport.toggle()
                }
            }
        }
        .task {
            await generateImages()
        }
        .fileExporter(
            isPresented: $isShowingExport,
            items: exportImages.map { $0.pngData()! }
        ) { result in
            switch result {
            case .success:
                print("Success")
            case .failure(let error):
                print(error)
            }
        } onCancellation: { }
    }

    func generateImages() async {
        exportImages = await withTaskGroup(of: UIImage.self) { group in
            for screenshot in screenshots {
                group.addTask {
                    await generateImage(screenshot: screenshot)
                }
            }

            var allImages = [UIImage]()
            for await image in group {
                allImages.append(image)
            }

            return allImages
        }
    }

    @MainActor
    func generateImage(screenshot: ScreenshotModel) -> UIImage {
        let content = ScreenshotPreviewView(
            screenshotModel: screenshot,
            size: size
        ).frame(size: size)

        let renderer = ImageRenderer(content: content)

        return renderer.uiImage!
    }
}

Solution

  • You could use this approach using .fileExporter(isPresented: ..., documents: ...) and ImageDocument: FileDocument as shown.

    Example code:

    Note in particular the fw.filename to give each file a different name.

    struct ContentView: View {
        var body: some View {
            // for testing
            Temp(screenshots: [ScreenshotModel(name: "house"),ScreenshotModel(name: "globe")], size: .zero)
        }
    }
    
    // for testing
    struct ScreenshotModel: Identifiable {
        let id = UUID()
        var name: String
    }
    
    struct Temp: View {
        let screenshots: [ScreenshotModel]
        let size: CGSize
    
        @State private var isShowingExport = false
    
        @State private var exportImages = [UIImage]()
    
        var body: some View {
            ScrollView {
                VStack {
                    ForEach(exportImages, id: \.self) {
                        Image(uiImage: $0)
                    }
    
                    Button("Export to Files", systemImage: "folder") {
                        isShowingExport.toggle()
                    }
                }
            }
            .task {
                await generateImages()
            }
            .fileExporter(
                isPresented: $isShowingExport,
                documents: exportImages.map{ImageDocument(image: $0)} // <-- here
            ) { result in
                switch result {
                case .success:
                    print("-----> Success")
                case .failure(let error):
                    print(error)
                }
            } onCancellation: { }
        }
    
        func generateImages() async {
            exportImages = await withTaskGroup(of: UIImage.self) { group in
                for screenshot in screenshots {
                    group.addTask {
                        await generateImage(screenshot: screenshot)
                    }
                }
    
                var allImages = [UIImage]()
                for await image in group {
                    allImages.append(image)
                }
    
                return allImages
            }
        }
    
        @MainActor
        func generateImage(screenshot: ScreenshotModel) async -> UIImage { // <-- here
    //        let content = ScreenshotPreviewView(
    //            screenshotModel: screenshot,
    //            size: size
    //        ).frame(size: size)
    //
    //        let renderer = ImageRenderer(content: content)
    //
    //        return renderer.uiImage!
            
            // for testing
            return UIImage(systemName: screenshot.name)!
        }
    }
    
    struct ImageDocument: FileDocument {
      static var readableContentTypes: [UTType] { [.png] }
    
      var image: UIImage
    
      init(image: UIImage?) {
        self.image = image ?? UIImage()
      }
    
      init(configuration: ReadConfiguration) throws {
        guard let data = configuration.file.regularFileContents,
              let image = UIImage(data: data)
        else {
          throw CocoaError(.fileReadCorruptFile)
        }
        self.image = image
      }
    
      func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper {
          let fw = FileWrapper(regularFileWithContents: image.pngData()!)
          fw.filename = String(UUID().uuidString.prefix(6))  // <--- for testing
          return fw
      }
    }