Reputation: 759
I followed the Swift project UsingFiles github and its video to write files in my SwiftUI project. The project UsingFiles can write files to Files APP and then the writing files can be seen in the Files APP. But I followed the code as following, the files cannot be seen in Files APP.
let file = "\(UUID().uuidString).txt"
let contents = "Some text..."
let dir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
let fileURL = dir.appendingPathComponent(file)
do {
try contents.write(to: fileURL, atomically: false, encoding: .utf8)
}
catch {
print("Error: \(error)")
}
The UsingFiles writing files is file:///Users/fmac/Library/Developer/CoreSimulator/Devices/11111111-881E-488A-9571-E61B83EB6062/data/Containers/Data/Application/11111111-AF89-4B15-B3B5-E13A63A19F8D/Documents/E018F056-83EA-4D70-87C4-16F755AA404A.txt
.
My writing files is file:///Users/fmac/Library/Developer/CoreSimulator/Devices/11111111-881E-488A-9571-E61B83EB6062/data/Containers/Data/Application/11111111-B191-4ACD-98B1-004E619C2EC7/Documents/C9E0F52E-040F-4647-94A3-88E0DA171AB5.txt
I can find the writing files of UsingFiles in the directory UsingFiles
as following:
But I cannot find the writing file in Files APP in my SwiftUI project. Is there something wrong of the code in SwiftUI? Why I cannot find the writing file in Files APP?
Upvotes: 4
Views: 6893
Reputation: 2124
This answer is entirely based on the earlier answer by lilunxp. If you like my answer please also like that answer.
I think the accepted answer is an overkill for what the OP wants. My understanding is that the OP simply wants to be able to write a file from the app, and have that file be visible and accessible from within Apple's Files app. That is very easy to do.
The all important thing is to do exactly what the answer from lilunxp says to do: "Try add 'Application supports iTunes file sharing' and 'Supports opening documents in place' to 'info.plist'. And change them value to true". That seems crystal clear, but just in case, here's a screenshot from Xcode 15.2 that shows how: just add the bottom two lines.
The OP already has the right code, but let me repeat it here and embellish a little by also creating subdirectories that the user can browse from Apple's Files app. This is the entire code needed:
import SwiftUI
struct ContentView: View {
var body: some View {
VStack {
Button("Make files user can access outside this app") {
makeFilesThatUserCanAccessOutsideThisApp()
}
}
.padding()
}
}
func makeFilesThatUserCanAccessOutsideThisApp() {
guard let docFolder = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else {
fatalError()
}
let contents = "Some text..."
let mySOFileNameA = "MySOFileA.txt"
let mySOFileNameB = "MySOFileB.txt"
let mySOFolderUrl = docFolder.appendingPathComponent("MySOFolder")
do {
// write one file at the top level:
try contents.write(to: docFolder.appendingPathComponent(mySOFileNameA), atomically: true, encoding: .utf8)
// write two files into the subdirectory (after first creating the subdirectory if necessary):
try FileManager.default.createDirectory(at: mySOFolderUrl, withIntermediateDirectories: true, attributes: nil)
try contents.write(to: mySOFolderUrl.appendingPathComponent(mySOFileNameA), atomically: true, encoding: .utf8)
try contents.write(to: mySOFolderUrl.appendingPathComponent(mySOFileNameB), atomically: true, encoding: .utf8)
}
catch {
fatalError(error.localizedDescription)
}
}
Now the rest is just a matter of opening Apple's Files app, then selecting Browse > On My iPhone. This is what it looks like in iOS 17.3:
Both views are from Apple's Files app. The left view shows a "folder" for my app (I called it My SO App). The system automatically creates this "top level folder": notice in the code above that it was only necessary to use FileManager.default.createDirectory
to create the subdirectory. The right view shows the contents of the top level folder. As expected it shows a file and a subdirectory (that contains two items). The user is able to open these text files, and easily "share it to other people or send the written file by email" as the OP expressed in a comment.
Upvotes: 6
Reputation: 41
Try add “Application supports iTunes file sharing” and “Supports opening documents in place” to “info.plist”. And change them value to true.
Upvotes: 4
Reputation: 61
I used UIActivityViewController to save a file on my phone. It's simple and works.
import SwiftUI
struct ContentView: View {
var body: some View {
Button(action: wirtieFile) {
Image(systemName: "square.and.arrow.up")
}
}
func wirtieFile() -> Void{
let file = "test.txt"
let dir = NSURL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(file)
let contents = "test..."
do {
try contents.write(to: dir!, atomically: true, encoding: .utf8)
} catch {
print(error.localizedDescription)
}
var filesToShare = [Any]()
filesToShare.append(dir!)
let av = UIActivityViewController(activityItems: filesToShare, applicationActivities: nil)
UIApplication.shared.windows.first?.rootViewController?.present(av, animated: true, completion: nil)
}
}
Upvotes: 5
Reputation: 236538
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 }
}
Upvotes: 1