Catch Error Reading File From Disk - You Don't Have Permissions - swift

I am using the following code in the AppDel. This is triggered when a user taps on a gpx file or uses the share option to share the file with my app. At this point it is the user that specified that they are allowing my app to access the file so I'm a little confused a to why this is still being denied. Any advice much appreciated.
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any]) -> Bool {
if let dir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first {
let fileURL = dir.appendingPathComponent(url.lastPathComponent)
if FileManager.default.fileExists(atPath: fileURL.path) {
print("File already exists")
}
else {
do {
try FileManager.default.copyItem(at: url, to: fileURL)
print("Did write file to disk")
}
catch {
DispatchQueue.main.async(){
print("Catch error writing file to disk: \(error)")
}
}
}
}
return true
}
The error prints to the console as follows:
Error Domain=NSCocoaErrorDomain Code=257 "The file
“Beech_Hill_Long_Route.gpx” couldn’t be opened because you don’t have
permission to view it."
UserInfo={NSFilePath=/private/var/mobile/Library/Mobile
Documents/com~apple~CloudDocs/Desktop/Beech_Hill_Long_Route.gpx,
NSUnderlyingError=0x282c19a70 {Error Domain=NSPOSIXErrorDomain Code=1
"Operation not permitted"}}

I'm less knowledgeable about exactly how this works on iOS, but I do have some knowledge about how it works for macOS, and the two should be similar. For macOS sandboxed apps the user has to select the file outside of the sandbox specifically via NSOpenPanel or NSSavePanel. The equivalent on iOS would be UIDocumentPickerViewController, but I assume an explicit share would work too. Based on that, here's how I would think about it and what I would try, if I were faced with your problem:
You've got two URLs involved in your call to FileManager.default.copy(). It's not clear which one is producing the error, so I'll consider both.
First let's look at fileURL. You're constructing it to put a file in the user's Documents directory. On macOS at least, that directory is not in the app's sandbox, which means asking the user via NSSavePanel (which also means they might decide to put it somewhere else). You might have to do the UIKit equivalent, or just make sure you're picking a location that is in your sandbox.
To test that, instead of doing the copy, try writing to fileURL to isolate just that one. For example:
do { try "TestString".data(using: .utf8)?.write(url: fileURL) }
catch { print("Write failed: \(error.localizedDescription)") }
If that fails, then your problem is with fileURL, in which case you may need to use UIDocumentPickerViewController to save it, or pick a location that's definitely in the app's sandbox.
If the test succeeds, the problem must be with the incoming URL.
I'm going to assume that url is already security scoped, because I'm not sure how sharing would even work otherwise. What I think is most likely happening behind the scenes is that when the user shares a URL with your app, iOS creates a security-scoped bookmark from the URL and sends the bookmark rather than the URL to your app. Then on your app's end, that bookmark is used to reconstitute the URL before passing it on to your app's delegate. If I'm right about that you'll need to open and close a security scope to use it:
url.startAccessingSecurityScopedResource()
// access your file here
url.stopAccessingSecurityScopedResource()
Note that stopAccessingSecurityScopeResource() has to be called on the main thread, so if you're code is happening asynchronously you'll need to schedule it to run there:
url.startAccessingSecurityScopedResource()
// access your file here
DispatchQueue.main.async { url.stopAccessingSecurityScopedResource() }
If you need to save the URL itself to use in a future run of your program... you can't. Well, you can, but it won't be valid, so you'll be right back to permissions errors. Instead you have to save a bookmark, and then in that future run, reconstruct the URL from the bookmark.
let bookmark = try url.bookmarkData(
options: .withSecurityScope,
includingResourceValuesForKeys: nil,
relativeTo: nil
)
bookmark is an instance of Data, so you can write that to UserDefaults or wherever you might want to save it. To get a URL back from it later:
var isStale = false
let url = try URL(
resolvingBookmarkData: bookmark,
options: .withSecurityScope,
relativeTo: nil,
bookmarkDataIsStale: &isStale
)
if isStale
{
let newBookmark = try url.bookmarkData(
options: .withSecurityScope,
includingResourceValuesForKeys: nil,
relativeTo: nil
)
// Save the new bookmark
}
Note that if isStale is true after the initializer returns, then you need to remake and resave the bookmark.
It's a shame that we have to go through this much trouble, but we live in a world where some people insist on doing bad things to other people's devices and data, so it's what we have to deal with the protect users from malicious data breaches.

Related

Firebase Offline Support: upload posts while user is offline and sync when user comes online in iOS Swift app

I am using firebase in an iOS-Swift project in which I have to enable offline support for uploading posts, in the post there is a picture and caption just like Instagram, so what I want is when user is offline and he/she wants to upload a post, his/her picture get saved in cache and when user comes online that photo get uploaded and give back a download url that we can use for saving posts-details it in database.
sample code is:
let photoIDString = UUID().uuidString
let storageRef = Storage.storage().reference(forURL: "storage ref URL").child("posts").child(photoIDString)
storageRef.putData(imageData, metadata: nil, completion: { (metadata, error) in
guard let metadata = metadata else {
return
}
if error != nil {
return
}
storageRef.downloadURL(completion: { ( url, error ) in
guard let downloadURL = url else {
return
}
let photoUrl = downloadURL.absoluteString
self.sendDataToDatabase(photoUrl: photoUrl)
})
}
)
I want to know what changes should I make in my code to provide the offline capability. Any code snippet will help more.
The problem is better view as re-send to server when there is an error.
For your offline case, you can check if the error return is a network error, or manually check network connection availability.
You can create a re-send array of object
e.g
var resendList : [YourObjectType]
// when failed to send to server
resendList.append(yourFailedObject)
And then, 2 solutions:
Check the network connectivity manually and reupload in when the app become active in func applicationDidBecomeActive(_ application: UIApplication) in appDelegate. For checking connectivity you can try the method here: Check for internet connection with Swift But this has a problem that, the user has to go out the app and back again with network connected
Keep track(listen to notification) on the connectivity change, using a suggestion method by https://stackoverflow.com/a/27310748/4919289 and reupload it to server
and loop through all objects in resendList and re-upload again.
I am not an iOS developer, but I can share logical flow and some references.
When user clicks on upload: Check if network is available?
if yes: upload the post.
if no:
save the post to app storage or offline database
set broadcast receiver to receive broadcast when device comes online. This link may be helpful.
upload post when device comes online.
If you are looking for solution that is offered by Firebase, you may find more details here.
Firebase offers you plenty of ways to do this in their documentation. https://firebase.google.com/docs/database/ios/offline-capabilities
When uploading to the firebase server, it will queue itself and wait until it has a internet connection again to upload. If this happens to timeout or you want to do it your own way just attempt to upload with a completionHandler on the setValue or updateChild functions - if not successfully and the error message is because of internet, add it to a local cache to the phone with the data and the path to the firebase server.
onLoad, attempt the same upload again until it succeeds, once it succeeds - clear the local cache.

CloudKit CKShare URL Goes Nowhere

I have successfully saved a CKShare URL to CloudKit, and I can see that the user is INVITED in the CloudKit Dashboard. My Mac app emailed the URL to that person, but when they click it, all they see it this screen on icloud.com:
Clicking OK makes everything disappear so all you see is the background on the web page.
My understanding is that the URL is supposed to open my Mac app where it will fire userDidAcceptCloudKitShareWith in my app delegate. But it does nothing.
Could this be because my app is in development and not in the Mac App Store yet? Do I need a custom URL scheme to get it to open my app?
Documentation on this stuff is pretty sparse. I'd love any help someone can provide.
I have since learned that you must specify a fallback URL for your CloudKit container. In cases where the app isn't installed (or isn't recognized, which seems to be the case when doing dev builds in Xcode like I am), CloudKit will forward share URL to somewhere you specify. They append the unique share ID to the URL so that you can process it on your own web page.
In the CloudKit dashboard, go to Environment Settings... and you'll see this popup:
I have it redirect to https://myapp.com/share/?id= and on my web page where it redirects to, I do a $_GET['id'] to grab the id. I then do another redirect to my application using a custom URL scheme and pass the share ID (e.g. myapp://abc123 where abc123 is the share ID).
In my app delegate, I receive the URL like this:
func application(_ application: NSApplication, open urls: [URL]) {
if let url = urls.first, let shareId = url.host{
fetchShare(shareId) //<-- sharedId = abc123
}
}
I then use CKFetchShareMetadataOperation to look up the URL of the share and CKAcceptSharesOperation to accept it like this:
func fetchShare(shareId: String){
if let url = URL(string: "https://www.icloud.com/share/\(shareId)"){
let operation = CKFetchShareMetadataOperation(shareURLs: [url])
operation.perShareMetadataBlock = { url, metadata, error in
if let metadata = metadata{
//:::
acceptShare(metadata: metadata)
}
}
operation.fetchShareMetadataCompletionBlock = { error in
if let error = error{
print("fetch Share error: \(error)")
}
}
CKContainer.default().add(operation)
}
}
func acceptShare(metadata: CKShareMetadata){
let operation = CKAcceptSharesOperation(shareMetadatas: [metadata])
operation.acceptSharesCompletionBlock = { error in
if let error = error{
print("accept share error: \(error)")
}else{
//Share accepted!
}
}
CKContainer.default().add(operation)
}
I think there are easier ways to work through this using NSItemProvider and NSSharingService, but I'm doing a lot of custom UI and wanted to have full control of the share workflow.
I hope this helps someone. :)

HTTP Get API call always fails "NSLocalizedDescription=A server with the specified hostname could not be found"

I've created an ASP.NET Core web api which I am running locally in visual studio, I am able to browse to this in Safari and tested the API using postman.
When I use the following swift code in my MacOS app:
let url = URL(string: "http://localhost:5000/api/values")
let task = URLSession.shared.dataTask(with: url!) { data, response, error in
guard error == nil else {
print(error!)
return
}
guard let data = data else {
print("Data is empty")
return
}
let json = try! JSONSerialization.jsonObject(with: data, options: [])
print(json)
}
task.resume()
It always fails with error
NSLocalizedDescription=A server with the specified hostname could not
be found.
Do I have to change some settings so that my app can make Http requests to the API?
Thanks
You probably already figured out a solution or a way around the problem, but since I reached the same question when having this problem I will provide my answer anyway in case anyone else finds this question.
For me the problem was that the MacOS app was not allowed to connect to the internet since it was automatically running in an app sandbox. The solution was to add "Outgoing Connections" to the app capabilities. See screenshot below.

Set Permission to delete file in OS X swift 3.0

I coded for osx application to remove file from directory path but I had received error as "couldn’t be removed because you don’t have permission to access it". So I want to set access permission code to remove that list of files. If any one have idea or solution please help me. The code as pasted below for reference :
if fileManager.fileExists(atPath: fullpath) == true {
print("File Available \(fullpath)")
do {
_ = try fileManager.removeItem(atPath: fullpath)
} catch let error as NSError{
print(error.localizedDescription)
}
}
In above code, I had found the exist file in directory but I got error that "couldn’t be removed because you don’t have permission to access it".
I want to get solution of access permission and remove file from directory. Thanks in advance.
If you want your app to be able to access files outside your own app, you need to turn App Sandbox off in your app target's capabilities.

Call back url for iOS app?

I am working on a project where I share video on Vimeo. In this My app open a video where user needs to press authorize button to authorize the app at Vimeo and to get access tokens. So, for this, my app opens safari and open Vimeo's site there. The user needs to press allow button then it has to come back again to the app. But I am not able to know what should be the call back url to make the Safari/Vimeo to come back to my app.
Please suggest your views regarding this.
You need to set a custom URL scheme for your app by editing your app's Info.plist. There's plenty of documentation about this on Apple's developer website. Here's an article that goes into detail: http://iosdevelopertips.com/cocoa/launching-your-own-application-via-a-custom-url-scheme.html
Then your website just needs to open a url that uses your app's url scheme (eg: myappscheme://do/something/cool?foo=bar). If your app cares about any data passed in to it via your website then implement the "application:openURL:sourceApplication:annotation:" method and inspect the NSURL passed in. You can read more about this in Apple's documentation: http://developer.apple.com/library/ios/#documentation/uikit/reference/UIApplicationDelegate_Protocol/Reference/Reference.html
You need to implement something called 'URL Scheme' to your app, which means register your app to a certain url, so it can be opened from.
1) You should add a row to your info.plist file.
2) You need to listen to the url in your app, and do what needed.
Google for more info...
To support a custom URL scheme:
Define the format for your app's URLs.
Register your scheme so that the system directs appropriate URLs to your app.
Handle the URLs that your app receives.
URLs must start with your custom scheme name. Add parameters for any options your app supports. For example, a photo library app might define a URL format that includes the name or index of a photo album to display.
an example is :
myphotoapp:albumname?name="foods"
myphotoapp:albumname?index=1
Register Your URL Scheme
click on project target and goto info page
in the info page expand URL Types section and hit the + button
fill fields with appropriate values.
Handle Incoming URLs
The system delivers the URL to your app by calling your app delegate's application(_:open:options:)method. you can useNSURLComponents` APIs to extract the components. Obtain additional information about the URL, such as which app opened it, from the system-provided options dictionary.
func application(_ application: UIApplication,
open url: URL,
options: [UIApplicationOpenURLOptionsKey : Any] = [:] ) -> Bool {
// Determine who sent the URL.
let sendingAppID = options[.sourceApplication]
print("source application = \(sendingAppID ?? "Unknown")")
// Process the URL.
guard let components = NSURLComponents(url: url, resolvingAgainstBaseURL: true),
let albumPath = components.path,
let params = components.queryItems else {
print("Invalid URL or album path missing")
return false
}
if let photoIndex = params.first(where: { $0.name == "index" })?.value {
print("albumPath = \(albumPath)")
print("photoIndex = \(photoIndex)")
return true
} else {
print("Photo index missing")
return false
}
}
If your app has opted into Scenes, and your app is not running, the system delivers the URL to the scene(_:willConnectTo:options:) delegate method after launch, and to scene(_:openURLContexts:) when your app opens a URL while running or suspended in memory.
func scene(_ scene: UIScene,
willConnectTo session: UISceneSession,
options connectionOptions: UIScene.ConnectionOptions) {
// Determine who sent the URL.
if let urlContext = connectionOptions.urlContexts.first {
let sendingAppID = urlContext.options.sourceApplication
let url = urlContext.url
print("source application = \(sendingAppID ?? "Unknown")")
print("url = \(url)")
// Process the URL similarly to the UIApplicationDelegate example.
}
}