SwiftUI exporting or sharing files
EDIT: Removed all code and references to UIButton
.
Thanks to @Matteo_Pacini for his answer to this question for showing us this technique. As with his answer (and comment), (1) this is rough around the edges and (2) I'm not sure this is how Apple wants us to use UIViewControllerRepresentable
and I really hope they provide a better SwiftUI
("SwiftierUI"?) replacement in a future beta.
I put in a lot of work in UIKit
because I want this to look good on an iPad, where a sourceView
is needed for the popover. The real trick is to display a (SwiftUI) View
that gets the UIActivityViewController
in the view hierarchy and trigger present
from UIKit
.
My needs were to present a single image to share, so things are targeted in that direction. Let's say you have an image, stored as a @State
variable - in my example the image is called vermont.jpg and yes, things are hard-coded for that.
First, create a UIKit
class of type `UIViewController to present the share popover:
class ActivityViewController : UIViewController {
var uiImage:UIImage!
@objc func shareImage() {
let vc = UIActivityViewController(activityItems: [uiImage!], applicationActivities: [])
vc.excludedActivityTypes = [
UIActivity.ActivityType.postToWeibo,
UIActivity.ActivityType.assignToContact,
UIActivity.ActivityType.addToReadingList,
UIActivity.ActivityType.postToVimeo,
UIActivity.ActivityType.postToTencentWeibo
]
present(vc,
animated: true,
completion: nil)
vc.popoverPresentationController?.sourceView = self.view
}
}
The main things are;
- You need a "wrapper"
UIViewController
to be able topresent
things. - You need
var uiImage:UIImage!
to set theactivityItems
.
Next up, wrap this into a UIViewControllerRepresentable
:
struct SwiftUIActivityViewController : UIViewControllerRepresentable {
let activityViewController = ActivityViewController()
func makeUIViewController(context: Context) -> ActivityViewController {
activityViewController
}
func updateUIViewController(_ uiViewController: ActivityViewController, context: Context) {
//
}
func shareImage(uiImage: UIImage) {
activityViewController.uiImage = uiImage
activityViewController.shareImage()
}
}
The only two things of note are:
- Instantiating
ActivityViewController
to return it up toContentView
- Creating
shareImage(uiImage:UIImage
) to call it.
Finally, you have ContentView
:
struct ContentView : View {
let activityViewController = SwiftUIActivityViewController()
@State var uiImage = UIImage(named: "vermont.jpg")
var body: some View {
VStack {
Button(action: {
self.activityViewController.shareImage(uiImage: self.uiImage!)
}) {
ZStack {
Image(systemName:"square.and.arrow.up").renderingMode(.original).font(Font.title.weight(.regular))
activityViewController
}
}.frame(width: 60, height: 60).border(Color.black, width: 2, cornerRadius: 2)
Divider()
Image(uiImage: uiImage!)
}
}
}
Note that there's some hard-coding and (ugh) force-unwrapping of uiImage
, along with an unnecessary use of @State
. These are there because I plan to use `UIImagePickerController next to tie this all together.
The things of note here:
- Instantiating
SwiftUIActivityViewController
, and usingshareImage
as the Button action. - Using it to also be button display. Don't forget, even a
UIViewControllerRepresentable
is really just considered a SwiftUIView
!
Change the name of the image to one you have in your project, and this should work. You'll get a centered 60x60 button with the image below it.
Exporting Data Via CSV File
I think this CodableCSV project will be a really good starting point for you.
Here's the end of my modified createCsv()
:
let myRows = [
["Quanity", "Item", "Cost", "Total", "Total (USD)"],
[sCount, barName, sCost, sTotal, newTotal]
]
do {
let string = try CSVWriter.encode(rows: myRows, into: String.self)
print(string)
return MessageDocument(message: string)
} catch {
fatalError("Unexpected error encoding CSV: \(error)")
}
When I click on the 'Export' button, I see, from that print statement:
Quanity,Item,Cost,Total,Total (USD)
"1,600",ChocoBar,€4.95,"€7,920.00","$8,954.27"
You're going to need to be deliberate about adding title
, subTitle
, "Sale in Dollars"
because the rows they're on need to have the same number of columns as your data—CSV isn't Excel in this regard; where in Excel you can put data in any cell, no imposed structure—so something like:
let myRows = [
["Henry's Chocolate Sales Frankfurt", "", "", ""], // 3 empty (placeholder) columns
...
]
SwiftUI exporting the content of Canvas
Apply a frame to the canvas and it should work. E.g.
canvas.frame(width: 300, height: 300)
SwiftUI .fileExporter() with a .csv file in the View
The compilation error doesn't have anything to do with the way the data is formatted -- it's just that there isn't a signature for fileExporter
that matches what you're providing. In particular, you're trying to just pass Data
directly to document
, which exports a FileDocument
or ReferenceFileDocument
.
Here's an example, with some borrowed/modified code from https://www.hackingwithswift.com/quick-start/swiftui/how-to-export-files-using-fileexporter:
struct ContentView : View {
@State var isExporting = false
var body: some View {
VStack {
Button("Export") {
isExporting = true
}
.fileExporter(isPresented: $isExporting, document: CSVFile(initialText: "Item 1, Item2, Item3"), contentType: UTType.commaSeparatedText) { result in
}
}
}
}
struct CSVFile: FileDocument {
// tell the system we support only plain text
static var readableContentTypes = [UTType.commaSeparatedText]
static var writableContentTypes = [UTType.commaSeparatedText]
// by default our document is empty
var text = ""
// a simple initializer that creates new, empty documents
init(initialText: String = "") {
text = initialText
}
// this initializer loads data that has been saved previously
init(configuration: ReadConfiguration) throws {
if let data = configuration.file.regularFileContents {
text = String(decoding: data, as: UTF8.self)
}
}
// this will be called when the system wants to write our data to disk
func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper {
let data = Data(text.utf8)
return FileWrapper(regularFileWithContents: data)
}
}
Swift UI exporting content of canvas
This works fine!
The issue you are encountering is that in your sample code, canvasPallete
didn't get a frame modifier when snapshot()
was called on it. So it got rendered at a size that's not what you want, probably (0,0).
If you change the snapshot line it will work:
let image = canvasPallete
.frame(width: 300, height: 300)
.snapshot()
One more thing! In iOS 16, there's new API for rendering a view to an Image. So for iOS 16 and up, you can skip the View extension and write:
let renderer = ImageRenderer(content: canvasPallete)
if let image = renderer.uiImage {
UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil)
}
Show ProgressView when Exporting File in SwiftUI
Here is a possible solution. There may be a better way to do this! I am using a ZStack{}
to attach the fileExporter
. Is there a different way or other solution?
struct ContentView: View {
@State private var isExporting: Bool = false
@State private var isLoading: Bool = false
@State private var document: FileExport? = nil
var body: some View {
VStack {
Button(action: {
isExporting = true
isLoading = true
DispatchQueue.global(qos: .background).async {
document = FileExport(contents: "This is a simple text")
DispatchQueue.main.async {
isLoading = false
}
}
}, label: {
Text("Export File")
})
.padding()
if isLoading {
ProgressView()
.zIndex(1.0)
}
if isExporting && !isLoading {
ZStack {}
.fileExporter(isPresented: $isExporting, document: document, contentType: .plainText) { result in
if case .success = result {
print(try! result.get())
print("File Saved")
} else {
print("File Not Saved")
}
}
}
}
}
}
Related Topics
Expected Hexadecimal Code in Braces After Unicode Escape
Suddenly Getting Compiler Crash "Arrayforcecast" in Swift Xcode Beta 6
Swift 3.0 Iterate Over String.Index Range
How to Retrieve Alamofire Response Header for a Request
Swift Error: Binary Operator '&&' Cannot Be Applied to Two 'Bool' Operands
Getting Optional("") When Trying to Get Value from Keychain
How to Get the Height of a Uilabel in Swift
Multiplying Variables and Doubles in Swift
Swift Instance Member Cannot Be Used on Type
Swift Closures Causing Strong Retain Cycle with Self
Diagnosing Exc_Bad_Instruction in Swift Standard Library
Swiftui Background Color of List MAC Os
Check If Class Conforms to Protocol
Xcode 8.3 Can't Support Swift 2.3
How to Create Enum with Raw Type of Cgpoint
Cannot Invoke 'Join' with an Argument List of Type (String, [String]) in Swift 2.0