SwiftUI sharing or exporting files with FileExporter or UIActivityViewController

Hello everybody,

I implemented an export functionality in my PRTracker app and wanted to share what I learned. At first, I was a little confused by the resources online and what was described in the book SwiftUI for Masterminds (affiliate link) by J.D. Gauchat, which I’m reading right now. My goal was the functionality to create a CSV file with all the personal records in my app and “export” or “share” that file with myself or others with the common sharing UI everybody probably knows from iOS apps. My first approach was to try a method explained in the book using FileExporter.

To begin with, I created a button in the navigation bar of my main view, which should then be used to create the document and share it.

Share Button
Share Button

I did that with a .toolbar view modifier:

.toolbar {
	ToolbarItem (placement: .navigationBarTrailing){
		Button(action: {
			// The button action depends on the method and will be described together with them down below
            // .... 
              }   
			}, label: {
				Image(systemName: "square.and.arrow.up")
			})
		}
	 }

Exporting a file with FileExporter

Now, the FileExplorer is pretty easy to use. It is a view modifier like .sheet or .toolbar, but you need a document object that implements the FileDocument protocol. This object represents the file you export (or import with a FileImporter).

Since I aimed to export it as a CSV file, a simple implementation for plaintext files is enough.

import UniformTypeIdentifiers

struct TextDocument: FileDocument {
    init(configuration: ReadConfiguration) throws {
        if let data = configuration.file.regularFileContents {
            if let text = String(data: data, encoding: .utf8) {
                documentText = text
            } else {
                throw CocoaError(.fileReadCorruptFile)
            }
        } else {
                throw CocoaError(.fileReadCorruptFile)
            }
        }
    
    func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper {
        let data = documentText.data(using: .utf8)
        let wrapper = FileWrapper(regularFileWithContents: data!)
        return wrapper
    }
    
    static var readableContentTypes: [UTType] = [.plainText]
    
    var documentText: String
    init() {
        documentText = ""
        
    }
}

Then I could use it in my MainView like this:

@State private var document = TextDocument()
@State private var displayShareSheet: Bool = false

// the other views here...

.toolbar {
	ToolbarItem (placement: .navigationBarTrailing){
		Button(action: {
			addDataToDocument() // sets document.documentText with the data I want in my file   
			}      
		}, label: {
			Image(systemName: "square.and.arrow.up")
                   })
            }
        }

.fileExporter(isPresented: $displayShareSheet, document: document, contentType: .plainText, defaultFilename: "PRexport.csv", onCompletion: { result in
                        print("Document exported")
                        document.documentText = ""
                    })

The arguments for the FileExporter are the following:

  • isPresented: a binding to the boolean value if the exporter should be shown
  • document: the FileDocument object we created and want to export
  • contentType: The content type to use for the exported file – plaintext in that case
  • defaultFilename: what our file should be called
  • onCompletion: the function we want to invoke once the export is finished

While this did work, it was not what I wanted.

The FileExporter allows you to save the file on your iPhone. Then, you could open it from the Files app or send it from there. Even though this works, it is cumbersome and not what I intended to do.

FileExporter in Action
FileExporter in Action

Share files using an UIActivityViewController

After more research online, I found what I was looking for – the UIActivityViewController, a UIKit view controller with no SwiftUI implementation. I also found an article by Hoye Lam about implementing it in SwiftUI, which I adapted to meet my needs. Every UIKit ViewController can be adapted to SwiftUI using the UIViewControllerRepresentable protocol.

You start by creating the view that adopts the protocol and define a typealias for the UIView you want to implement that looks like this:

typealias UIViewControllerType = UIActivityViewController

XCode can then generate the functions you need to implement for the protocol. In the case of the UIActivityViewController , it looks like this:

 struct ActivityView: UIViewControllerRepresentable {
    @Binding var activityItems: [Any]
    var excludedActivityTypes: [UIActivity.ActivityType]? = nil
    
    func makeUIViewController(context: Context) -> UIActivityViewController {
        
        let controller = UIActivityViewController(activityItems: activityItems, applicationActivities: nil)
        
        controller.excludedActivityTypes = excludedActivityTypes
        
        return controller
        
    }
    
    func updateUIViewController(_ uiViewController: UIActivityViewController, context: Context) {}
}

The view has two parameters:

  • activityItems: is the array of data objects we want to share.
  • applicationActivites: an array that represents the custom services that the application supports, can be nil if there are no custom services.

Additionally, we can set the excludeActivityTypes property if we want some services not to be displayed.

At first, I tried to share the TextDocument object I used before for the FileExporter. Sadly, that did not work, or I couldn’t get it to work. To share a file with the UIActivityViewController, add the URL of the file to the activity items. So I created an ApplicationData class (inspired by the before mentioned book) that could save the file on the device first and then return the URL of it to use it with the ActivityView.

import Foundation

@Observable class ApplicationData {
    @ObservationIgnored let manager: FileManager
    @ObservationIgnored let directories: [URL]?
    
    init() {
        manager = FileManager.default
        directories = manager.urls(for: .documentDirectory, in: .userDomainMask)
    }
    
    func saveFile(name: String, data: String) -> URL? {
        if let docURL = directories?.first {
            let newFileURL = docURL.appendingPathComponent(name)
            let path = newFileURL.path
            do {
                try manager.removeItem(atPath: path)
            } catch {}
            manager.createFile(atPath: path, contents: data.data(using: .utf8))
            return newFileURL
        } else {
            return nil
        }
    }
}

And with that, I could use the ActivityView like that:

@State private var document = TextDocument()
@State private var displayShareSheet: Bool = false
@State var shareSheetItems: [Any] = []
@State private var appData = ApplicationData()

// my views here

.toolbar {
	ToolbarItem (placement: .navigationBarTrailing){
		Button(action: {
			addDataToDocument() // the function from before, because I was too lazy to change it
                                
			if let docURL = appData.saveFile(name: "PRExport.csv", data: document.documentText) {
					shareSheetItems.append(docURL)
					displayShareSheet = true       
                                }
						}, label: {
					Image(systemName: "square.and.arrow.up")
                            })
                        }
                    }
                  .sheet(isPresented: $displayShareSheet, onDismiss: {
                      shareSheetItems = []
                  }, content: {
                ActivityView(activityItems: $shareSheetItems)
            })

Which achieved the result I wanted. This image is from the simulator, but on an actual device, it is possible to send the file via AirDrop, e-mail, and all the other things we are used to from the share menu.

UIActivityView
UIActivityView

I hope this post was helpful. If you have any questions or comments, you are very welcome to contact me.

Until next time,

Daniel

Share the Post:

Related Posts