How to Save Document to "Files" App in Swift

How to save document to Files App in swift

If you have access to your PDFdata then you could present the user with a UIActivityViewController where they can then save it to Files, or share it with any of the other available options:

let activityViewController = UIActivityViewController(activityItems: ["Name To Present to User", pdfData], applicationActivities: nil)
present(activityViewController, animated: true, completion: nil)

Sample Image

Swift - Automatically save pdf file to Files app On My iPhone

Example of download any pdf file and automatically save inside files folder of iPhone.

    let urlString = "https://www.tutorialspoint.com/swift/swift_tutorial.pdf"
let url = URL(string: urlString)
let fileName = String((url!.lastPathComponent)) as NSString
//Mark: Create destination URL
let documentsUrl:URL = (FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first as URL?)!
let destinationFileUrl = documentsUrl.appendingPathComponent("\(fileName)")
//Mark: Create URL to the source file you want to download
let fileURL = URL(string: urlString)
let sessionConfig = URLSessionConfiguration.default
let session = URLSession(configuration: sessionConfig)
let request = URLRequest(url:fileURL!)
let task = session.downloadTask(with: request) { (tempLocalUrl, response, error) in
if let tempLocalUrl = tempLocalUrl, error == nil {
//Mark: Success
if let statusCode = (response as? HTTPURLResponse)?.statusCode {
print("Successfully downloaded. Status code: \(statusCode)")
}
do {
try FileManager.default.copyItem(at: tempLocalUrl, to: destinationFileUrl)
do {
//Mark: Show UIActivityViewController to save the downloaded file
let contents = try FileManager.default.contentsOfDirectory(at: documentsUrl, includingPropertiesForKeys: nil, options: .skipsHiddenFiles)
for indexx in 0..<contents.count {
if contents[indexx].lastPathComponent == destinationFileUrl.lastPathComponent {
let activityViewController = UIActivityViewController(activityItems: [contents[indexx]], applicationActivities: nil)
self.present(activityViewController, animated: true, completion: nil)
}
}
}
catch (let err) {
print("error: \(err)")
}
} catch (let writeError) {
print("Error creating a file \(destinationFileUrl) : \(writeError)")
}
} else {
print("Error took place while downloading a file. Error description: \(error?.localizedDescription ?? "")")
}
}
task.resume()


iOS: Open saved image inside the native Files app

After doing some research, I don't think this is unwanted behavior but more like behavior that Snapseed has subscribed to, as this behavior does not happen automatically.

However, there is some bug (I think) that does not let you unsubscribe from this behavior easily.

The place we need to look at is using LSSupportsOpeningDocumentsInPlace and CFBundleDocumentTypes, more on that here and here:

Here I created a small example to save an image from the app main bundle to the documents directory when the user taps a button using your code:

Absolutely no difference to your code

@objc
private func saveImage()
{
if let imageData
= UIImage(named: "dog")?.jpegData(compressionQuality: 1.0)
{
save(imageData: imageData,
toFolder: "image",
withFileName: "dog.jpeg")
}
}

func save(imageData: Data,
toFolder folderName: String,
withFileName fileName: String)
{
DispatchQueue.global().async
{
let manager = FileManager.default
let documentFolder = manager.urls(for: .documentDirectory,
in: .userDomainMask).last
let folder = documentFolder?.appendingPathComponent(folderName)
let file = folder?.appendingPathComponent(fileName)

do {
try manager.createDirectory(at: folder!,
withIntermediateDirectories: true,
attributes: [:])
if let file = file
{
try imageData.write(to: file)
print("file \(fileName) saved")
}
}
catch
{
print(error.localizedDescription)
}
}
}

If you add the following to your info.plist

<key>UIFileSharingEnabled</key>
<true/>
<key>LSSupportsOpeningDocumentsInPlace</key>
<true/>

This supports exposing your documents directory in the files app and after this everything works as normal:

Saving UIImage to documents directory showing in Files app iOS Swift

However, if you want your app to be part of a group that opens certain file formats, you modify your info.plist to add the CFBundleDocumentTypes to suggest that your app is able to open specific files, like images in our case.

To do that, we add the following to info.plist

<key>CFBundleDocumentTypes</key>
<array>
<dict>
<key>LSItemContentTypes</key>
<array>
<string>public.image</string>
</array>
<key>CFBundleTypeName</key>
<string>image</string>
<key>LSHandlerRank</key>
<string>Default</string>
</dict>
</array>

This would allow your app to be listed when some wants to share or open an image and by adding this, then we get the behavior shown in your gif which opens the app instead of the preview in the files app

Opening image inside my app from files app iOS Swift

Unfortunately, it seems once this behavior is assigned to your app, even if you remove the above above key from info.plist, this behavior persists.

The only way I could restore the original behavior was by using a fresh bundle identifier and only using these two keys in the info.plist in order to expose your documents directory to the Files app

<key>UIFileSharingEnabled</key>
<true/>
<key>LSSupportsOpeningDocumentsInPlace</key>
<true/>

I believe this is some sort of bug as there should be a simple way to unsubscribe from this behavior.

Hope this helps even though it does not fully solve your problem

How can I display my App documents in the Files app for iPhone

If you would like to expose your App document files inside Apple's Files App you need to include the "Supports Document Browser" key in your info plist file and set it to YES:

Sample Image

How can I write a file in a folder located at Apple's Files App in SwiftUI?

As I have already posted in comments you can NOT programmatically save a file out of your APP Bundle. You can use a UIDocumentInteractionController and ask the user to choose the location where the file is supposed to be written.

So if you are working with SwiftUI this gets a bit more complicated than the regular Storyboard approach as you can see in this post because you need to implement UIViewControllerRepresentable for UIDocumentInteractionController:


struct DocumentInteractionController: UIViewControllerRepresentable {

fileprivate var isExportingDocument: Binding<Bool>
fileprivate let viewController = UIViewController()
fileprivate let documentInteractionController: UIDocumentInteractionController

init(_ isExportingDocument: Binding<Bool>, url: URL) {
self.isExportingDocument = isExportingDocument
documentInteractionController = .init(url: url)
}

func makeUIViewController(context: UIViewControllerRepresentableContext<DocumentInteractionController>) -> UIViewController { viewController }

func updateUIViewController(_ controller: UIViewController, context: UIViewControllerRepresentableContext<DocumentInteractionController>) {
if isExportingDocument.wrappedValue && documentInteractionController.delegate == nil {
documentInteractionController.uti = documentInteractionController.url?.typeIdentifier ?? "public.data, public.content"
documentInteractionController.name = documentInteractionController.url?.localizedName
documentInteractionController.presentOptionsMenu(from: controller.view.frame, in: controller.view, animated: true)
documentInteractionController.delegate = context.coordinator
documentInteractionController.presentPreview(animated: true)
}
}

func makeCoordinator() -> Coordintor { .init(self) }
}

And its Coordinator:

class Coordintor: NSObject, UIDocumentInteractionControllerDelegate {
let documentInteractionController: DocumentInteractionController
init(_ controller: DocumentInteractionController) {
documentInteractionController = controller
}
func documentInteractionControllerViewControllerForPreview(_ controller: UIDocumentInteractionController) -> UIViewController { documentInteractionController.viewController }

func documentInteractionControllerDidDismissOptionsMenu(_ controller: UIDocumentInteractionController) {
controller.delegate = nil
documentInteractionController.isExportingDocument.wrappedValue = false
}
}

Now you can create your DocumentInteraction View and its previews:

struct DocumentInteraction: View {
@State private var isExportingDocument = false
var body: some View {
VStack {
Button("Export Document") { self.isExportingDocument = true }
.background(DocumentInteractionController($isExportingDocument,
url: Bundle.main.url(forResource: "sample", withExtension: "pdf")!))
}
}
}

struct DocumentInteraction_Previews: PreviewProvider {
static var previews: some View { DocumentInteraction() }
}

You will need those helpers as well:

extension URL {
var typeIdentifier: String? { (try? resourceValues(forKeys: [.typeIdentifierKey]))?.typeIdentifier }
var localizedName: String? { (try? resourceValues(forKeys: [.localizedNameKey]))?.localizedName }
}

Sample project

Showing iOS app files within in the Files app

As well as UIFileSharingEnabled add this to your Info.plist

<key>LSSupportsOpeningDocumentsInPlace</key>
<true/>

Here is my test code. Works well for me on macOS 12, using Xcode 13.2,
targets: ios-15 and macCatalyst 12. Tested on real ios-15 devices.

import SwiftUI

@main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}

struct ContentView: View {
let image = UIImage(systemName: "globe")!
var body: some View {
Image(uiImage: image).resizable().frame(width: 111, height: 111)
Button(action: {
saveImage(file: "globe")
}) {
Text("save test file")
}
}

func saveImage(file: String) {
do {
let fileURL = try FileManager.default
.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
.appendingPathComponent(file)
.appendingPathExtension("png")
try image.pngData()?.write(to: fileURL)
} catch {
print("could not create file: \(file)")
}
}

}

EDIT1: Starting from a "new" project in Xcode.

I created a new ios project from scratch, using Xcode 13.2 on macos 12.2-beta.
Copied and pasted the code in my answer. Then in the Targets->Info

I added the following entries:

Application supports iTunes file sharing YES

and

Supports opening documents in place YES

Compiled and ran the project on a iPhone ios-15. In the Files app,
went to On My iPhone and the TestApp folder was there, and inside was the "globe"
image file.

Added the mac catalyst in General -> Deployment Info,
removed the App sandbox from the Signing & Capabilities.

Compiled and ran the project on a iMac macos 12.2, and in the Documents
directory was the globe.png file.

How to write a file to a folder located at Apple's Files App in Swift

You can use UIDocumentInteractionController and let the user select where he wants to save your file when you share your url. The user just needs to select save to files and choose which directory to save the file you are exporting.

You can use UIDocumentInteractionController to share any file type located inside your App bundle, at your Documents directory or another folder accessible from your App.

class ViewController: UIViewController {
let documentInteractionController = UIDocumentInteractionController()
func share(url: URL) {
documentInteractionController.url = url
documentInteractionController.uti = url.typeIdentifier ?? "public.data, public.content"
documentInteractionController.name = url.localizedName ?? url.lastPathComponent
documentInteractionController.presentOptionsMenu(from: view.frame, in: view, animated: true)
}
@IBAction func shareAction(_ sender: UIButton) {
guard let url = URL(string: "https://www.ibm.com/support/knowledgecenter/SVU13_7.2.1/com.ibm.ismsaas.doc/reference/AssetsImportCompleteSample.csv?view=kc") else { return }
URLSession.shared.dataTask(with: url) { data, response, error in
guard let data = data, error == nil else { return }
let tmpURL = FileManager.default.temporaryDirectory
.appendingPathComponent(response?.suggestedFilename ?? "fileName.csv")
do {
try data.write(to: tmpURL)
DispatchQueue.main.async {
self.share(url: tmpURL)
}
} catch {
print(error)
}

}.resume()
}
}


extension URL {
var typeIdentifier: String? {
return (try? resourceValues(forKeys: [.typeIdentifierKey]))?.typeIdentifier
}
var localizedName: String? {
return (try? resourceValues(forKeys: [.localizedNameKey]))?.localizedName
}
}

edit/update:

If you would like to expose the files located on your App Bundle's document directory you can check this post How can I display my App documents in the Files app for iPhone



Related Topics



Leave a reply



Submit