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.
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.
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.
I hope this post was helpful. If you have any questions or comments, you are very welcome to contact me.
Until next time,
Daniel