File URL fails to resolve from bookmark after app restart/rebuild - ios

I am storing file URLs so that they be retrieved on subsequent runs of an iOS app. The URL's are generated from the user selecting local or iCloud files on their device. It seems that the given URL points to a file in Documents but which is not actually there.
I have the following code to resolve a URL from a bookmark:
BOOL isBookmarkStale = false;
NSError* error = nil;
auto nsURL = [NSURL URLByResolvingBookmarkData:bookmark
options:NSURLBookmarkResolutionWithoutUI
relativeToURL:nil
bookmarkDataIsStale:&isBookmarkStale
error:&error];
The bookmark is created with
if ([nsURL startAccessingSecurityScopedResource])
{
NSError* error = nil;
NSData* securityScopedBookmark = [nsURL bookmarkDataWithOptions:NSURLBookmarkCreationMinimalBookmark
includingResourceValuesForKeys:nil
relativeToURL:nil
error:&error];
[nsURL stopAccessingSecurityScopedResource];
}
I have verified that the stored bookmark contains the correct URL by checking the last known path with
NSDictionary *values = [NSURL resourceValuesForKeys:#[NSURLPathKey]
fromBookmarkData:static_cast<NSData*> (bookmarkData)];
NSString *path = [values objectForKey:NSURLPathKey];
Here path contains the last URL to the file. The URL strings from the local files I'm testing with look like this: /private/var/mobile/Containers/Data/Application/3BFEA0FB-AA84-4CFB-90E5-3535FA14738E/Documents/SomeFile.mp3
This seems to work while the app is running. But after restarting or rebuilding the app (debug build), then attempting to resolve a URL from a stored bookmark it fails with NSCocoaErrorDomain Code=4 and the message "The file doesn’t exist."

To get a URL to the user's documents use
[NSFileManager.defaultManager URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask].firstObject
This will stay the same on device but will change between runs on the simulator. So you need to make your URL relative to this to test in the sim.

Yea the absolute path could change, so use the relative path.
Just save the part you want after the Documents.
let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first
That will be an optional URL (because of the .first), so you might want to
if let documentsDirectory
or
guard let documentsDirectory
to make it non optional.
Then you can do documentsDirectory.appendingPathComponent(pathComponent: SomeFile.mp3)

Related

How to correctly reference/retrieve a temp file created in AppData for file upload to a server?

So the app I'm making creates a file called "logfile" and I'm trying to send that file via Alamofire upload to a server. The file path printed in the console log is
/var/mobile/Containers/Data/Application/3BE13D78-3BF0-4880-A79A-27B488ED9EFE/Documents/logfile.txt
and the file path I can use to manually access the log created in the .xcappdata is
/AppData/Documents/logfile.txt
To access it, I'm using
let fileURL = Bundle.main.url(forResource: "", withExtension: "txt")
where inbetween the double quotes for "forResource", I've tried both file paths I listed in the previous paragraph as well as just the file name but I'm getting a nil value for file found for either. The file isn't recognized to be there, presumably because the file path I'm using is wrong as Alamofire is returning nil when trying to locate send the file. Anyone know the direct file path I'm supposed to use to be able to grab my file since the other two don't supposedly work? Thank you!
Use below code to get string data from text file to upload to server:
let fileName = "logfile"
let documentDirURL = try? FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
let fileURL = documentDirURL.appendingPathComponent(fileName).appendingPathExtension("txt")
print("FilePath: \(fileURL.path)")
var readString = "" // Used to store the file contents
do {
// Read the file contents
readString = try String(contentsOf: fileURL)
} catch let error as NSError {
print("Failed reading from URL: \(fileURL), Error: " + error.localizedDescription)
}
print("File Text: \(readString)") // Send 'readString' to server
If you're dynamically creating the file at runtime, it won't be in your app bundle so the Bundle class won't be able to find it. The directories you see are also dynamically-generated and not only platform-specific, but also device-specific, so you can't use the file paths directly. Instead, you'll have to ask for the proper directory at runtime from the FileManager class, like this:
guard let documents = FileManager.default.urls(for: .documentsDirectory, in: .userDomainMask).first else{
// This case will likely never happen, but forcing anything in Swift is bad
return
}
let logURL = URL(string: "logfile.txt", relativeTo: documents)
do{
let fileContents = String(contentsOf: logURL)
// Send your file to your sever here
catch{
// Handle any errors you might've encountered
}
Note that I'm guessing based on the paths you pasted in your answer you put it in your application's documents directory. That's a perfectly fine place to put this type of thing, but if I'm wrong and you put it in a different place, you'll have to modify this code to point to the right place

Xamarin iOS DocumentPicker: How to import large files?

In the DidPickDocument event of UIDocumentPickerViewController I try to import/write the selected file into the app's local Documents directory.
This works fine with "small" files (e.g < 100MB) using a subclassed UIDocument class with overriden
public override bool LoadFromContents(NSObject contents, string typeName, out NSError outError)
{
outError = null;
if (contents != null)
{
Content = ((NSData)contents).ToArray();
}
...
...and by calling
MySubclassedDoc mySubclassedDoc = new MySubclassedDoc (nsurl);
bool success = await mySubclassedDoc.OpenAsync();
File.WriteAllBytes("targetFile.xyz", mySubclassedDoc.Content);
But if the file is larger (eg. 400MB) the app crashes before LoadFromContents is called because of insufficent memory (RAM).
So there need to be a way to stream the selected file directly to a file.
How can you do this using the given NSUrl ?
Since you have got the url, there's no need to convert it to NSData then store the data to a file. We can just use this url with NSFileManager like:
url.StartAccessingSecurityScopedResource();
string docPath = NSSearchPath.GetDirectories(NSSearchPathDirectory.DocumentDirectory, NSSearchPathDomain.User)[0];
string filePath = Path.Combine(docPath, "fileName.type");
NSError error;
NSFileManager fileManager = new NSFileManager();
fileManager.Copy(url.Path, filePath, out error);
url.StopAccessingSecurityScopedResource();
In this way we can store the file to our app's own document directory.
why don’t you just clone the file over to your Documents directory before reading it instead of deserializing the contents and reserializing them?
your code seems really inefficient. If your files can be 400MB you should clearly not load all the contents into memory. I guess you must have very large binary objects in the files if they can be 400MB ; try mmapping the file and storing pointers to the individual objects instead?

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.

How can app extension access files in containing app Documents/ folder

In the app extension is there a way to get images generated from the containing app which is store in /var/mobile/Containers/Data/Application//Documents// folder?
In order to make files available to app extension you have to use Group Path, as App Extension can't access app's Document Folder, for that you have follow these steps,
Enable App Groups from Project Settings-> Capabilities.
Add a group extension something like group.yourappid.
Then use following code.
NSString *docPath=[self groupPath];
NSArray *contents=[[NSFileManager defaultManager] contentsOfDirectoryAtPath:docPath error:nil];
NSMutableArray *images=[[NSMutableArray alloc] init];
for(NSString *file in contents){
if([[file pathExtension] isEqualToString:#"png"]){
[images addObject:[docPath stringByAppendingPathComponent:file]];
}
}
-(NSString *)groupPath{
NSString *appGroupDirectoryPath = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:group.yourappid].path;
return appGroupDirectoryPath;
}
You can add or change the path extension as per your image extensions you are generating.
Note - Remember you need to generate the images in the group folder rather than in Documents Folder, so it is available to both app and extension.
Cheers.
Swift 3 Update
let fileManager = FileManager.default
let url = fileManager.containerURL(forSecurityApplicationGroupIdentifier: "YOUR_GROUP_ID")?.appendingPathComponent("logo.png")
// Write to Group Container
if !fileManager.fileExists(atPath: url.path) {
let image = UIImage(named: "name")
let imageData = UIImagePNGRepresentation(image!)
fileManager.createFile(atPath: url.path as String, contents: imageData, attributes: nil)
}
// Read from Group Container - (PushNotification attachment example)
// Add the attachment from group directory to the notification content
if let attachment = try? UNNotificationAttachment(identifier: "", url: url!) {
bestAttemptContent.attachments = [attachment]
// Serve the notification content
self.contentHandler!(self.bestAttemptContent!)
}

Correcting user submitted URL in Xcode/Objective C

I'm trying to programme a mini browser in Xcode however at the moment the UIWebView will only load URLs that include the http ://www The user submits their URL using a UITextField and the contents become a string.
I wondered if there was a way to either search the submitted string and add the http or www or both where required or format the text input so it automatically checks to see if the correct address is used.
Thanks
Do something like this:
NSString *urlString = ... // the user entered URL string
if (![urlString hasPrefix:#"http://"]) {
urlString = [#"http://" stringByAppendingString:urlString];
}
Note that this is just a rough suggestion to get you started. This code doesn't handle cases such as the URL already having a prefix of "https://" or typos such as "htp://".
A better approach might be:
NSURL *url = [NSURL URLWithString:urlString];
NSString *scheme = [url scheme];
if (scheme.length == 0) {
// The string has no scheme - add "http://"
} else {
// check for valid schemes
}

Resources