Save/create folder that it to be treated as a file with FileManager - ios

I have a iOS/CatalystMacOS-app that can create, save, open custom text-files (with my own file extension). This works fine. However, now I need more than text. I want to save optional files in this file as well. Apparently macOS (and iOS?) can treat folders as files. But I cannot get it to work as wanted. The folder is still treated as a folder, even if it has a file extension.
This is the code I use to create the folder:
func showNewFilePathDialog(from viewController: UIViewController, saveCompleted: URLCallback?) {
guard !isPresenting else {
return
}
let objectToSave = ...
// Find an available filename
var number = 0
var exportURL: URL!
var data: Data!
var fullFileName = ""
while true {
let numberText = number == 0 ? "" : number.asString()
fullFileName = "baseFileName" + "\(numberText).myFileExtension"
exportURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!.appendingPathComponent(fullFileName)
let dict = objectToSave.toDict()
let json = dict.json!
data = json.data(using: .utf8)!
if FileManager.default.fileExists(atPath: exportURL.path) {
number += 1
continue
} else {
break
}
}
do {
try FileManager.default.createDirectory(atPath: exportURL.path, withIntermediateDirectories: true, attributes: nil)
} catch {
NSLog("Couldn't create document directory")
viewController.presentErrorDialog(from: error)
return
}
// 2. Create containing json file
do {
try data.write(to: exportURL.appendingPathComponent("content.json"))
} catch {
viewController.presentErrorDialog(from: error)
return
}
isPresenting = true
self.onSaveDialogComplete = saveCompleted
let pickerViewController = UIDocumentPickerViewController(url: exportURL, in: .exportToService)
pickerViewController.delegate = self
viewController.present(pickerViewController, animated: true)
}
And then it appears like this in macOS finder:
It will show up similar in iOS, not allowing me to open the folder as a single file either.
Edit: Using UIDocument/public.composite-content/FileWrapper seems to work as well, but the problem still consists: When viewed in macOS finder it is still treated as a folder. Also when trying to open the app from the open-dialog via UIDocumentPickerViewController trying to open the file-bundle only opens the folder and wont let me open it into the app :(
This is my info.list Export Type UTIs:
Edit2: Also tried with removing all but com.apple.package but does not work either. Still cannot open my custom type as it behaves like a folder.

Got it working. Seemed as old builds of my app was interfering with the system file types. So I searched for my app name and removed old builds from my computer. Then the system recognized my file suffix and opened it right away!
But I lost the icon this time, but that's another issue :)

Related

Strange and unusable URLs when using UIDocumentPickerViewController to access iCloud directories/files

I have an iOS application that uses UIDocumentPickerController to present the user with a dialog where they can pick the location of a directory containing files for upload
This is working just fine in most cases but if they pick a directory that is located on their iCloud account, DocumentPickerViewController returns a URL that produces unusable file URLs when enumerating the iCloud directory
Here is how I set it up:
var documentPicker = UIDocumentPickerViewController(documentTypes: [kUTTypeFolder as String], in: .open)
documentPicker.delegate = self
sView.present(documentPicker, animated: true, completion: nil)
And here is how I enumerate the files in the iCloud directory that they have chosen:
NSFileCoordinator().coordinate(readingItemAt: url, error: &error) { (url) in
let access = url.startAccessingSecurityScopedResource()
var directoryContents: [URL]
do {
let keys : [URLResourceKey] = [.nameKey, .isDirectoryKey]
directoryContents = try FileManager.default.contentsOfDirectory(
at: url,
includingPropertiesForKeys: keys)
} catch {
print("error \(error))")
if access {
url.stopAccessingSecurityScopedResource()
}
return
}
if access {
url.stopAccessingSecurityScopedResource()
}
}
For each file, I pass it to FileManager to copy into the application's sandbox:
for fromURL in directoryContents {
var lastPath = fromURL.lastPathComponent
print("Lastpath is \(lastPath)")
let toURL: URL = documentURL.appendingPathComponent(lastPath)
print("fromURL - \(fromURL)")
print("toURL \(toURL)")
let fM = FileManager.default
if(fM.fileExists(atPath: toURL.path)) {
exists += 1
} else {
do {
let access = url.startAccessingSecurityScopedResource()
try fM.copyItem(at: fromURL, to: toURL)
if access {
url.stopAccessingSecurityScopedResource()
}
count += 1
} catch {
print("Error on file copy \(error)")
}
}
}
This works just fine if the directory is on, say, an external USB drive but if I point it to iCloud, this is what I get:
fromURL - file:///Users/greg/Library/Developer/CoreSimulator/Devices/FFDED20B-B142-4FC6-BA8F-C1DC193E2AB7/data/Library/Mobile%20Documents/com~apple~CloudDocs/Savvy/SavvyLink/SmallEngineDataRepo/.Flt0002_20180913P.csv.icloud
toURL file:///Users/greg/Library/Developer/CoreSimulator/Devices/FFDED20B-B142-4FC6-BA8F-C1DC193E2AB7/data/Containers/Data/Application/167AC69D-494C-4CB1-8A36-E230C5303F30/Documents/.Flt0002_20180913P.csv.icloud
Note the file names have a dot (.) prepended and a .iCloud postpended. If I look in my application's sandbox I see the files in the Documents directory but the contents of the files are NOT the contents of the files on iCloud but instead look to be themselves some kind of a URL
Thanks for any insight into what is going on here and what I am missing
What I expect to happen is to be able to take the URLs returned when enumerating the files in the iCloud directory and then pass those URLs to FileManager.copyItems to copy them into my application's sandbox
As I said, this works just fine for directories located on (say) USB drives but when used for directories on iCloud it results in copying in files with very different contents than what is on the original files in iCloud and with names changed to have a "." prepended and ".iCloud" postpended

Get the names of files in an iCloud Drive folder that haven't been downloaded yet

I’m trying to get the names of all files and folders in an iCloud Drive directory:
import Foundation
let fileManager = FileManager.default
let directoryURL = URL(string: "folderPathHere")!
do {
let directoryContents = try fileManager.contentsOfDirectory(at: directoryURL, includingPropertiesForKeys: nil, options: [.skipsSubdirectoryDescendants, .skipsHiddenFiles])
for url in directoryContents {
let fileName = fileManager.displayName(atPath: url.absoluteString)
print(fileName)
}
} catch let error {
let directoryName = fileManager.displayName(atPath: directoryURL.absoluteString)
print("Couldnt get contents of \(directoryName): \(error.localizedDescription)")
}
It appears that any iCloud files that haven’t been downloaded to the device don’t return URLs.
I know I can check if a path contains a ubiquitous item when I already know the path with the code below (even if it isn’t downloaded):
fileManager.isUbiquitousItem(at: writePath)
Is there a way to get the URLs & names of those iCloud files without downloading them first?
The directory URL is a security-scoped URL constructed from bookmark data in case that makes any difference (omitted that code here for clarity).
Thanks
Found the answer. I was skipping hidden files with ".skipsHiddenFiles", but the non-downloaded files are actually hidden files, named: ".fileName.ext.iCloud".
Remove the skips hidden files option now works as expected.
You need to use a NSFileCoordinator to access the directory in iCloud Storage, and then normalize placeholder file names for items that haven't been downloaded yet:
let iCloudDirectoryURL = URL(...)
let fileCoordinator = NSFileCoordinator(filePresenter: nil)
fileCoordinator.coordinate(
readingItemAt: iCloudDirectoryURL,
options: NSFileCoordinator.ReadingOptions(),
error: nil
) { readingURL in
do {
let contents = try FileManager.default.contentsOfDirectory(
at: readingURL, includingPropertiesForKeys: nil
)
for url in contents {
print("\(canonicalURL(url))")
}
} catch {
print("Error listing iCloud directory: '\(error)'")
}
}
func canonicalURL(_ url: URL) -> URL {
let prefix = "."
let suffix = ".icloud"
var fileName = url.lastPathComponent
if fileName.hasPrefix(prefix), fileName.hasSuffix(suffix) {
fileName.removeFirst(prefix.count)
fileName.removeLast(suffix.count)
var result = url.deletingLastPathComponent()
result.append(path: fileName)
return result
} else {
return url
}
}

Access Windows/Mac Shared Folder Locally With smb from iOS

I am trying to build an app where I am able to access(read/write) windows/mac shared folders in my local network with swift.
Is there any possible way to do that with swift?
There is an App in the App Store called "FileExplorer" https://apps.apple.com/de/app/fe-file-explorer-file-manager/id510282524 where you can access these shared folders, but I do not know how they programmed this and with which language.
I also tried to access my shared folders via this App and yes it worked I can see my shared folders on my Phone.
But there needs to be a way to do it with swift...
I already tried different things(code bellow).
In the code bellow I tried to access the shared folder of my second mac and write the Text "Write this text to the fileURL as text in iOS using Swift" into the file named "Test.txt" and after that I want to read the same file again.
#IBAction func Button(_ sender: UIButton)
{
var uc = URLComponents()
uc.scheme = "smb"
uc.user = "user"
uc.password = "password"
uc.host = "ip-adress"
uc.path = "document-directory"
// Save data to file
let fileName = "Test"
let url = uc.url
//let DocumentDirURL = URL(fileURLWithPath: "/Users/f/d/t/App/Assets/Apps/TestApp")
let DocumentDirURL = try! URL(resolvingAliasFileAt: url!)
let fileURL = DocumentDirURL.appendingPathComponent(fileName).appendingPathExtension("txt")
print("FilePath: \(fileURL.path)")
let writeString = "Write this text to the fileURL as text in iOS using Swift"
do {
// Write to the file
try writeString.write(to: fileURL, atomically: true, encoding: String.Encoding.utf8)
} catch let error as NSError {
print("Failed writing to URL: \(fileURL), Error: " + error.localizedDescription)
}
var fullString: String = "" // Used to store the file contents
do {
// Read the file contents
fullString = try String(contentsOf: fileURL, encoding: .utf8)
} catch let error as NSError {
print("Failed reading from URL: \(fileURL), Error: " + error.localizedDescription)
}
print("File Text: \(readString)")
}
If I run the code as shown, he always gives me the error
"smb scheme is not supported" and then some additional errors that he can not write/read the file because he can not access it.
When I change the code and only search on the device I am programming on and run the simulator to search for this file everything works fine. So I have problems with "smb".
Thank you for every helpful answer.
you can use amsmb2 library to do this
you can extend the template class provided to connect to download files, write files, list directories -> on an smb share
everything is asynchronous from memory, with the librarys calls including hooks for progress updates on the ui main thread etc
i believe the amsmb2 library function your after might be uploadItem
iOS 13 includes SMB (server message block protocol) support
https://9to5mac.com/2019/06/17/ios-13-beta-2-enables-smb-server-connectivity-in-the-files-app/

Swift 3.0 FileManager.fileExists(atPath:) always return false

When I use method .fileExists(atPath:)to judge whether the file is exist in file system, the method always return false to me. I checked the file system and the file do exist. Here is my code:
let filePath = url?.path
var isDir : ObjCBool = false
if(self.fileManager.fileExists(atPath: filePath!, isDirectory: &isDir)){
let result = NSData(contentsOfFile: filePath!)
}
or
let filePath = url?.path
if(self.fileManager.fileExists(atPath: filePath!)){
let result = NSData(contentsOfFile: filePath!)
}
the if clause will always be skipped.
I assume your url is an URL type. If so try this out:
let filePath = url?.path // always try to work with URL when accessing Files
if(FileManager.default.fileExists(atPath: filePath!)){ // just use String when you have to check for existence of your file
let result = NSData(contentsOf: url!) // use URL instead of String
}
Saying enough, you should change your implementation like this:
if(FileManager.default.fileExists(atPath: (url?.path)!)){ // just use String when you have to check for existence of your file
let result = NSData(contentsOf: url!) // use URL instead of String
}
EDIT: 1
There is even more better way, you can call it swift-way (:D). You don't have to explicitly check for file existence.
guard let result = NSData(contentsOf: fileURL) else {
// No data in your fileURL. So no data is received. Do your task if you got no data
// Keep in mind that you don't have access to your result here.
// You can return from here.
return
}
// You got your data successfully that was in your fileURL location. Do your task with your result.
// You can have access to your result variable here. You can do further with result constant.
print(result)
Update for Swift 3.0+ without the Objective-Cish NS prefix:
do {
let result = try Data(contentsOf: fileURL)
print(result)
} catch {
print(error)
}
in swift 3
just in case anyone gets confused like i did, here's the full snippets:
let str = "file:///Users/martian2049/Library/Developer/CoreSimulator/Devices/67D744AA-6EEC-4AFD-A840-366F4D78A18C/data/Containers/Data/Application/DD96F423-AF9F-4F4D-B370-94ADE7D6D0A5/Documents/72b8b0fb-7f71-7f31-ac9b-f9cc95dfe90d.mp3"
let url = URL(string: str)
print(url!.path,"\n")
if FileManager.default.fileExists(atPath: url!.path) {
print("FILE Yes AVAILABLE")
} else {
print("FILE NOT AVAILABLE")
}
this prints
/Users/martian2049/Library/Developer/CoreSimulator/Devices/67D744AA-6EEC-4AFD-A840-366F4D78A18C/data/Containers/Data/Application/DD96F423-AF9F-4F4D-B370-94ADE7D6D0A5/Documents/72b8b0fb-7f71-7f31-ac9b-f9cc95dfe90d.mp3
FILE Yes AVAILABLE
notice how the 'file://' got chopped off?
I want to share my experience, in case anyone else gets baffled by this.
Tested on iOS 10-11, Xcode 9.2 and Swift 3.2.
Short answer: if you save a file path to disk, you may solve by not including the Documents directory in it.
Instead, every time you need to retrieve the file with the saved path, get the Documents directory and append the path.
For an iOS app, I was saving an image to .../Documents/Pictures through the relative URL, let's say url.
As the image was saved, a path, let's say url.path, was saved too in a Core Data entity.
When I later tried retrieving the image through FileManager.default.fileExists(atPath: url.path), it always returned false.
I was testing the app on my iPhone. It turned out that, for some reason, every time I ran the app from Xcode, the app identifier folder changed!!
So:
App opened from Xcode -> Image saved -> app closed -> app opened from physical device ->
fileExists -> TRUE
App opened from Xcode -> Image saved -> app closed -> app opened from Xcode -> fileExists -> FALSE
You can check if this is your case by getting and printing the Document folder path (or URL, it doesn't matter) and comparing it with the saved path (or URL). If you get something like this:
/var/mobile/Containers/Data/Application/5D4632AE-C432-4D37-A3F7-ECD05716AD8A/Documents..
/var/mobile/Containers/Data/Application/D09904C3-D80D-48EB-ACFB-1E42D878AFA4/Documents..
you found the issue.
Just use path instead of absoluteString to remove file://
FileManager.default.fileExists(atPath: URL.init(string: "your_url")!.path)
let paths = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true);
var path = paths[0] as String;
path = path + "/YourFilePath"
if((NSFileManager.defaultManager().fileExistsAtPath(path))) {
let result = NSData(contentsOfFile: filePath!)}
Try the above code and check again
I had the same problem this worked for me
filePath.replacingOccurrences(of: "file://", with: "")
First, what does your file path looks like? If the path begins with a ~,then it must be expanded with expandingTildeInPath;
Check if the path is inaccessible to your app. iOS App can only visits its sandbox directories.

Backup Realm to iCloud Drive

I would like to backup a realm database file to an iCloud drive, like WhatsApp, I have some questions:
What is the best practice to do this?
I have a database located in a shared group folder to access it from extensions, how can I back it up? How can I show the progress bar of upload? Like WhatsApp for example?
If I put a realm file in a document folder it will be synced for each modify.
Are there some samples code that we can see?
Thanks for the help, have any ideas? links?
Just to clarify, this is a question about backing up a discrete Realm file itself to iCloud Drive, so that it would be visible in the iCloud Drive app. Not synchronizing the contents of the file to a CloudKit store.
If you leave the Realm file in the Documents directory, then if the user performs an iCloud or iTunes backup, the file will be backed up. All this means though is that if the user decides to upgrade to a new device and perform a restore using the old device's backup image, the Realm file will be restored then. If the user deletes the app from your old device before then, the iCloud backup will also be deleted.
If you want to export your Realm file so it can be permanently saved and accessed in iCloud Drive, you can export a copy of the Realm file to your app's iCloud ubiquity container. This is basically just another folder like the shared group's folder, but it's managed by iCloud. This folder sort of behaves like Dropbox in that anything you put in there is automatically synchronized.
The code would look something like this:
let containerURL = FileManager.default.url(forUbiquityContainerIdentifier: nil)
let realmArchiveURL = containerURL.appendPathComponent("MyArchivedRealm.realm")
let realm = try! Realm()
try! realm.writeCopy(toFile: realmArchiveURL)
This is a really basic example. The Apple documentation recommends you do this on a background thread since setting up the iCloud folder for the first time can create some time.
Updating this wouldn't happen automatically. You'll need to export a new copy of the Realm each time the user wants to perform a backup.
I have recently had the same requirements and I am able to achieve from below steps
Swift: 4+
Step:1
1.Setup Your cloudKit for your app with a Developer account
2. You can take reference: https://www.raywenderlich.com/1000-cloudkit-tutorial-getting-started
Step 2
- Add CloudKit Capabilities in your App
- Please check out the screenshot: https://prnt.sc/pdpda5
Step 3
- Check for cloud Enabled options for your iphone
// Return true if iCloud is enabled
func isCloudEnabled() -> Bool {
if DocumentsDirectory.iCloudDocumentsURL != nil { return true }
else { return false }
}
Step 4
- Setup the below variables for Local or iCloud Document directories
struct DocumentsDirectory {
static let localDocumentsURL = FileManager.default.urls(for: FileManager.SearchPathDirectory.documentDirectory, in: .userDomainMask).last!
static let iCloudDocumentsURL = FileManager.default.url(forUbiquityContainerIdentifier: nil)?.appendingPathComponent("Documents")
}
Step:5
Below function is used for copyRealmFileToIcloudContainer
func uploadDatabaseToCloudDrive()
{
if(isCloudEnabled() == false)
{
self.iCloudSetupNotAvailable()
return
}
let fileManager = FileManager.default
self.checkForExistingDir()
let iCloudDocumentsURL = FileManager.default.url(forUbiquityContainerIdentifier: nil)?.appendingPathComponent("Documents", isDirectory: true)
let iCloudDocumentToCheckURL = iCloudDocumentsURL?.appendingPathComponent("\(memberId)_default.realm", isDirectory: false)
let realmArchiveURL = iCloudDocumentToCheckURL//containerURL?.appendingPathComponent("MyArchivedRealm.realm")
if(fileManager.fileExists(atPath: realmArchiveURL?.path ?? ""))
{
do
{
try fileManager.removeItem(at: realmArchiveURL!)
print("REPLACE")
let realm = try! Realm()
try! realm.writeCopy(toFile: realmArchiveURL!)
}catch
{
print("ERR")
}
}
else
{
print("Need to store ")
let realm = try! Realm()
try! realm.writeCopy(toFile: realmArchiveURL!)
}
}
Step:6
- Once your realm file uploaded on the server , you can check this in your iPhone
- Steps
- 1.Go To Setting
- 2.Go To iCloud
- 3.Go To ManageStorage
- 4.You will see your application there
- 5.Tap on Application, you will able to see your realm file over there
Step:7
- Make Sure you have added the below lines in info.plist
<key>NSUbiquitousContainers</key>
<dict>
<key>iCloud.com.example.app</key>
<dict>
<key>NSUbiquitousContainerIsDocumentScopePublic</key>
<true/>
<key>NSUbiquitousContainerName</key>
<string>iCloudDemoApp</string>
<key>NSUbiquitousContainerSupportedFolderLevels</key>
<string>Any</string>
</dict>
</dict>
#yonlau as per your request sharing answer for backup realm file , This is tested once and the realm data only have when they backup on iCloud.
func DownloadDatabaseFromICloud()
{
let fileManager = FileManager.default
// Browse your icloud container to find the file you want
if let icloudFolderURL = DocumentsDirectory.iCloudDocumentsURL,
let urls = try? fileManager.contentsOfDirectory(at: icloudFolderURL, includingPropertiesForKeys: nil, options: []) {
// Here select the file url you are interested in (for the exemple we take the first)
if let myURL = urls.first {
// We have our url
var lastPathComponent = myURL.lastPathComponent
if lastPathComponent.contains(".icloud") {
// Delete the "." which is at the beginning of the file name
lastPathComponent.removeFirst()
let folderPath = myURL.deletingLastPathComponent().path
let downloadedFilePath = folderPath + "/" + lastPathComponent.replacingOccurrences(of: ".icloud", with: "")
var isDownloaded = false
while !isDownloaded {
if fileManager.fileExists(atPath: downloadedFilePath) {
isDownloaded = true
print("REALM FILE SUCCESSFULLY DOWNLOADED")
self.copyFileToLocal()
}
else
{
// This simple code launch the download
do {
try fileManager.startDownloadingUbiquitousItem(at: myURL )
} catch {
print("Unexpected error: \(error).")
}
}
}
// Do what you want with your downloaded file at path contains in variable "downloadedFilePath"
}
}
}
}
2.Copy realm file from iCloud to Document directory
func copyFileToLocal() {
if isCloudEnabled() {
deleteFilesInDirectory(url: DocumentsDirectory.localDocumentsURL)
let fileManager = FileManager.default
let enumerator = fileManager.enumerator(atPath: DocumentsDirectory.iCloudDocumentsURL!.path)
while let file = enumerator?.nextObject() as? String {
do {
try fileManager.copyItem(at: DocumentsDirectory.iCloudDocumentsURL!.appendingPathComponent(file), to: DocumentsDirectory.localDocumentsURL.appendingPathComponent(file))
print("Moved to local dir")
//HERE ACCESSING DATA AVAILABLE IN REALM GET FROM ICLOUD
let realm = RealmManager()
let array = realm.FetchObjects(type: Mood.self)
print(array?.count)
} catch let error as NSError {
print("Failed to move file to local dir : \(error)")
}
}
}
}
You could take a look at this Github project by mikemac8888.
Basically you make your model objects conform to RealmCloudObject:
class Note: Object, RealmCloudObject {
...
}
You have to implement a mapping function :
func toRecord() -> CKRecord {
...
record["text"] = self.text
record["dateModified"] = self.dateModified
}
... and the reverse function used to create Realm records out of CloudKit records:
public func changeLocalRecord(...) throws {
...
realm.create(objectClass as! Object.Type,
value: ["id": id,
"text": text,
"dateModified": NSDate(),
"ckSystemFields": recordToLocalData(record)],
update: true)
...
}
The full documentation could be read at the link I provided, obviously.

Resources