Search code examples
swiftuiios14xcode12macos-big-sur

SwiftUI 2.0: export images with .fileExporter modifier


Goal: export images with SwiftUI using the new .fileExporter modifier

Problem: The target image is not passing to .fileExporter document.

Question: How can I get .fileExporter modifier to export images?

struct ContentView: View {
    
    @State private var openFile = false
    @State private var exportFile = false


    @State private var img1 = UIImage()
    @State private var img2 = UIImage()

    @State private var target: Binding<UIImage>?    // dynamic target for importer
    
    var body: some View {
        Form {
            //image 1
            Button(action: {
                    self.target = $img1
                self.openFile.toggle()
            }){
                
                Image(uiImage: self.img1)
                .renderingMode(.original)
                .resizable()
                .frame(width: 48, height: 48)
                .clipShape(Circle())
            }
            
            //image 2
            Button(action: {
                    self.target = $img2
                self.openFile.toggle()
            }){
                
                Image(uiImage: self.img2)
                .renderingMode(.original)
                .resizable()
                .frame(width: 48, height: 48)
                .clipShape(Circle())
            }
        }
        .navigationTitle("File Importer")


        //file exporter not working. not sure what I should input as "document"
        .fileExporter(isPresented: $exportFile, document: target, contentType: .image) { result in
            if case .success = result {
                // Handle success.
            } else {
                // Handle failure.
            }
        }

        //file importer
        .fileImporter(isPresented: $openFile, allowedContentTypes: [.image]) { (res) in
            do{
                let fileUrl = try res.get()
                print(fileUrl)
                
                guard fileUrl.startAccessingSecurityScopedResource() else { return }
                if let imageData = try? Data(contentsOf: fileUrl),
                let image = UIImage(data: imageData) {
                    self.target?.wrappedValue = image
                }
                fileUrl.stopAccessingSecurityScopedResource()
                
            } catch{
                
                print ("error reading")
                print (error.localizedDescription)
            }
        }
    }
}

Solution

  • There were a couple of code problems and a couple of logic problems that I found. Here's my edited version:

    struct ContentView: View {
        
        @State private var openFile = false
        @State private var exportFile = false
    
    
        @State private var img1 = UIImage(named: "0")!
        @State private var img2 = UIImage(named: "1")!
    
        @State private var target : UIImage?
        
        var body: some View {
            Form {
                //image 1
                Button(action: {
                    self.target = img1
                    self.exportFile.toggle()
                    //self.openFile.toggle()
                }){
                    
                    Image(uiImage: self.img1)
                    .renderingMode(.original)
                    .resizable()
                    .frame(width: 48, height: 48)
                    .clipShape(Circle())
                }
                
                //image 2
                Button(action: {
                    self.target = img2
                    self.exportFile.toggle()
                    //self.openFile.toggle()
                }){
                    
                    Image(uiImage: self.img2)
                    .renderingMode(.original)
                    .resizable()
                    .frame(width: 48, height: 48)
                    .clipShape(Circle())
                }
            }
            .navigationTitle("File Importer")
    
            .fileExporter(isPresented: $exportFile, document: ImageDocument(image: target), contentType: .png, onCompletion: { (result) in
                if case .success = result {
                    print("Success")
                } else {
                    print("Failure")
                }
            })
    
    
            //file importer
            .fileImporter(isPresented: $openFile, allowedContentTypes: [.image]) { (res) in
                do{
                    let fileUrl = try res.get()
                    print(fileUrl)
                    
                    guard fileUrl.startAccessingSecurityScopedResource() else { return }
                    if let imageData = try? Data(contentsOf: fileUrl),
                    let image = UIImage(data: imageData) {
                        self.target = image
                    }
                    fileUrl.stopAccessingSecurityScopedResource()
                    
                } catch{
                    
                    print ("error reading")
                    print (error.localizedDescription)
                }
            }
        }
    }
    
    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 {
            return FileWrapper(regularFileWithContents: image.pngData()!)
        }
        
    }
    

    Things to look at:

    1. Logic error: you had an exportFile state variable, but you never modified it. That would mean fileExporter would never present.
    2. You were passing a Binding<UIImage> to a function that wants a FileDocument. I wrote a simple FileDocument implementation for images.
    3. Because I switched out your openFile toggles for exportFile, you now don't have any openFile access. That's a logic decision for you to figure out when/how to trigger those.
    4. I also didn't do any error handling for the images and made a decision to treat everything as a PNG. You may want to make different choices.

    Note that I also used images named "0" and "1" -- those were specific to my sample project. Change those to fit what works for your project.

    If you want a different type of solution to this, you can also look at using the \.exportFiles action: https://www.hackingwithswift.com/quick-start/swiftui/how-to-export-files-using-exportfilesaction