Sharing a Document with UIDocumentInteractionController - ios

I'm trying to share a document stored in the temporary directory using UIDocumentInteractionController. Essentially, I'm using the following code:
#IBAction func createDocumentButtonClicked(_ sender: Any) {
do {
// create temporary file
let tempUrl = FileManager.default.temporaryDirectory.appendingPathComponent("fileName.txt")
try "abc".write(to: tempUrl, atomically: true, encoding: .utf8)
// share file
let documentInteractionController = UIDocumentInteractionController(url: tempUrl)
documentInteractionController!.name = "filename.txt"
documentInteractionController!.presentOptionsMenu(from: view.frame, in: view, animated: true)
} catch {
// ...
}
}
When run, this code presents the share action sheet. The log indicates some problem: Could not instantiate class NSURL. Error: Error Domain=NSCocoaErrorDomain Code=4864 "The URL archive of type “public.url” contains invalid data." UserInfo={NSDebugDescription=The URL archive of type “public.url” contains invalid data.
Selecting any of the options results in failure handling the document.
This is reduced to pretty much textbook level code, yet it is not working. What am I missing?
Update:
Added context that better emphasizes the cause of the problem (see my answer below).

It turned out to be trivial, even silly. I leave the question anyhow in case someone else stumbles over it.
I need to maintain the instance of UIDocumentInteractionController outside the button action handler that presents the controller. I will update the question to better show this problem. With a small change, it works as expected:
var documentInteractionController: UIDocumentInteractionController?
#IBAction func createDocumentButtonClicked(_ sender: Any) {
do {
// create temporary file
let tempUrl = FileManager.default.temporaryDirectory.appendingPathComponent("fileName.txt")
try "abc".write(to: tempUrl, atomically: true, encoding: .utf8)
// share file
documentInteractionController = UIDocumentInteractionController(url: tempUrl)
documentInteractionController!.name = "filename.txt"
documentInteractionController!.presentOptionsMenu(from: view.frame, in: view, animated: true)
} catch {
// ...
}
}

Related

How to add file picker to the app on iOS 14+ and lower

I'm newbie in iOS development, so some things which I will show and ask here can be stupid and please don't be angry :) So, I need to add support of picking files from local storage in my app. This feature will be used for picking file -> encoding to Base64 and then sending to remote server. Right now I have some problems with adding this functionality to my app. I had found this tutorial and did everything what was mentioned here:
added import - import MobileCoreServices
added implementation - UIDocumentPickerDelegate
added this code scope for showing picker:
let documentPicker = UIDocumentPickerViewController(documentTypes: [String(kUTTypeText),String(kUTTypeContent),String(kUTTypeItem),String(kUTTypeData)], in: .import)
documentPicker.delegate = self
self.present(documentPicker, animated: true)
and also added handler of selected file:
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
print(urls)
}
In general file chooser appears on simulator screen, but I see warning in XCode:
'init(documentTypes:in:)' was deprecated in iOS 14.0
I visited the official guideline and here also found similar info about deprecation some method. So, how I can solve my problem with file choosing by the way which will be fully compatible with the latest iOS version. And another question - how I can then encode selected file? Right now I have an ability of file choosing and printing its location, but I need to get its data like name, content for encoding and some others. Maybe someone faced with similar problems and knows a solution? I need to add it in ordinary viewcontroller, so when I tried to add this implementation:
UIDocumentPickerViewController
I saw such error message:
Multiple inheritance from classes 'UIViewController' and 'UIDocumentPickerViewController'
I will be so pleased for any info: tutorials or advice :)
I decided to post my own solution of my problem. As I am new in ios development my answer can contain some logical problems :) Firstly I added some dialogue for choosing file type after pressing Attach button:
#IBAction func attachFile(_ sender: UIBarButtonItem) {
let attachSheet = UIAlertController(title: nil, message: "File attaching", preferredStyle: .actionSheet)
attachSheet.addAction(UIAlertAction(title: "File", style: .default,handler: { (action) in
let supportedTypes: [UTType] = [UTType.png,UTType.jpeg]
let documentPicker = UIDocumentPickerViewController(forOpeningContentTypes: supportedTypes)
documentPicker.delegate = self
documentPicker.allowsMultipleSelection = false
documentPicker.shouldShowFileExtensions = true
self.present(documentPicker, animated: true, completion: nil)
}))
attachSheet.addAction(UIAlertAction(title: "Photo/Video", style: .default,handler: { (action) in
self.chooseImage()
}))
attachSheet.addAction(UIAlertAction(title: "Cancel", style: .cancel))
self.present(attachSheet, animated: true, completion: nil)
}
then when a user will choose File he will be moved to ordinary directory where I handle his selection:
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
var selectedFileData = [String:String]()
let file = urls[0]
do{
let fileData = try Data.init(contentsOf: file.absoluteURL)
selectedFileData["filename"] = file.lastPathComponent
selectedFileData["data"] = fileData.base64EncodedString(options: .lineLength64Characters)
}catch{
print("contents could not be loaded")
}
}
as you can see in scope above I formed special dicionary for storing data before sending it to a server. Here you can also see encoding to Base64.
When the user will press Photo/Video item in alert dialogue he will be moved to gallery for picture selecting:
func chooseImage() {
imagePicker.allowsEditing = false
imagePicker.sourceType = .photoLibrary
present(imagePicker, animated: true, completion: nil)
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
var selectedImageData = [String:String]()
guard let fileUrl = info[UIImagePickerController.InfoKey.imageURL] as? URL else { return }
print(fileUrl.lastPathComponent)
if let pickedImage = info[UIImagePickerController.InfoKey.originalImage] as? UIImage {
selectedImageData["filename"] = fileUrl.lastPathComponent
selectedImageData["data"] = pickedImage.pngData()?.base64EncodedString(options: .lineLength64Characters)
}
dismiss(animated: true, completion: nil)
}
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
dismiss(animated: true, completion: nil)
}
via my method all file content will be encoded to base64 string.
P.S. Also I'm so pleased to #MaticOblak because he showed me the initial point for my research and final solution. His solution also good, but I have managed to solve my problem in way which is more convenient for my project :)
As soon as you have file URL you can use that URL to retrieve the data it contains. When you have the data you can convert it to Base64 and send it to server. You gave no information about how you will send it to server but the rest may look something like this:
func sendFileWithURL(_ url: URL, completion: #escaping ((_ error: Error?) -> Void)) {
func finish(_ error: Error?) {
DispatchQueue.main.async {
completion(error)
}
}
DispatchQueue(label: "DownloadingFileData." + UUID().uuidString).async {
do {
let data: Data = try Data(contentsOf: url)
let base64String = data.base64EncodedString()
// TODO: send string to server and call the completion
finish(nil)
} catch {
finish(error)
}
}
}
and you would use it as
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
urls.forEach { sendFileWithURL($0) { <#Your code here#> } }
}
To break it down:
To get file data you can use Data(contentsOf: url). This method even works on remote files so you could for instance use an URL of an image link anywhere on internet you have access to. It is important to know that this method will pause your thread which is usually not what you want.
To avoid breaking the current thread we create a new queue using DispatchQueue(label: "DownloadingFileData." + UUID().uuidString). The name of the queue is not very important but can be useful when debugging.
When data is received we convert it to Base64 string using data.base64EncodedString() and this data can then be sent to server. You just need to fill in the TODO: part.
Retrieving your file data can have some errors. Maybe access restriction or file no longer there or no internet connection... This is handled by throwing. If the statement with try fails for any reason then the catch parts executes and you receive an error.
Since all of this is done on background thread it usually makes sense to go back to main thread. This is what the finish function does. If you do not require that you can simply remove it and have:
func sendFileWithURL(_ url: URL, completion: #escaping ((_ error: Error?) -> Void)) {
DispatchQueue(label: "DownloadingFileData." + UUID().uuidString).async {
do {
let data: Data = try Data(contentsOf: url)
let base64String = data.base64EncodedString()
// TODO: send string to server and call the completion
completion(nil)
} catch {
completion(error)
}
}
}
There are other things to consider in this approach. For instance you can see if user selects multiple files then each of them will open its own queue and start the process. That means that if user selects multiple files it is possible that at some point many or all of them will be loaded in memory. That may take too much memory and crash your application. It is for you to decide if this approach is fine for you or you wish to serialize the process. The serialization should be very simple with queues. All you need is to have a single one:
private lazy var fileProcessingQueue: DispatchQueue = DispatchQueue(label: "DownloadingFileData.main")
func sendFileWithURL(_ url: URL, completion: #escaping ((_ error: Error?) -> Void)) {
func finish(_ error: Error?) {
DispatchQueue.main.async {
completion(error)
}
}
fileProcessingQueue.async {
do {
let data: Data = try Data(contentsOf: url)
let base64String = data.base64EncodedString()
// TODO: send string to server and call the completion
finish(nil)
} catch {
finish(error)
}
}
}
Now one operation will finish before another one starts. But that may only apply for getting file data and conversion to base64 string. If uploading is then done on another thread (Which usually is) then you may still have multiple ongoing requests which may contain all of the data needed to upload.

UIDocument Creation on iOS 11: reader is not permitted to access the URL

Since iOS 11 I have encountered the following error every time I am creating a new document using UIDocument API:
[ERROR] Could not get attribute values for item /var/mobile/Containers/Data/Application/XXXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXX/Documents/myDoc-XXXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXX.myFile (n).
Error: Error Domain=NSFileProviderInternalErrorDomain Code=1
"The reader is not permitted to access the URL."
UserInfo={NSLocalizedDescription=The reader is not permitted to access the URL.}
Unlike similar questions (1, 2, 3) on SO on this, I am not using UIDocumentBrowserViewController. I am simply creating a UIDocument and call save() to the Documents directory myself. The closest question I found uses UIManagedDocument. However, in my case, the file is still created and written successfully despite the error message.
Here's the gist of my save routine:
#IBAction func createDoc(_ sender: Any) {
let uuid = UUID().uuidString
let doc = Document(baseName: "myDoc-\(uuid)")
doc.save(to: doc.fileURL, for: .forCreating) { (completed) in
if (completed) {
doc.close(completionHandler: nil)
self.verifyNumberOfFiles()
}
}
}
My UIDocument subclass is also almost blank for simplicity of this question:
class Document: UIDocument {
let fileExtension = "myFile"
override init(fileURL url: URL) {
super.init(fileURL: url)
}
/// Convenience method for `init(fileURL:)`
convenience init(baseName: String) {
self.init(fileURL: documentsDirectory.appendingPathComponent(baseName).appendingPathExtension(Document.fileExtension))
}
override func contents(forType typeName: String) throws -> Any {
return NSData()
}
override func load(fromContents contents: Any, ofType typeName: String?) throws {
}
}
I'm always writing to Documents folder, and my lookup routine can verify that my files are successfully created:
public var documentsDirectory: URL {
return FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).last!
}
func loadFileURLs(from dirURL: URL) -> [URL]? {
return try? FileManager().contentsOfDirectory(at: dirURL, includingPropertiesForKeys: nil)
}
What I have discovered so far:
The error appears even when I set up my UTI already. See this and this.
I can verify that my UTI works when I send a "myFile" to my device over AirDrop and it correctly triggers my app to open.
The error appears on iOS 11 only. The same code doesn't reproduce the error on iOS 10, like in the question above.
I tried adding UISupportsDocumentBrowser key although I'm not using the browser but it's not dissolve the error.
What is happening? Is this just a "noise" error message on iOS 11?
Here's 🔨 my GitHub code online if anyone is interested.
A workaround for this is to create the file by saving its data to the disk, and then open it as you would with an existing file.
Your createDoc(_:) method would then like this:
#IBAction func createDoc(_ sender: Any) {
let uuid = UUID().uuidString
let baseName = "myDoc-\(uuid)"
let url = documentsDirectory
.appendingPathComponent(baseName)
.appendingPathExtension(Document.fileExtension)
do {
let emptyFileData = Data()
try emptyFileData.write(to: url)
let document = Document(fileURL: url)
document.open() { completed in
guard completed else {
// handle error
return
}
doc.close(completionHandler: nil)
self.verifyNumberOfFiles()
}
} catch {
// handle error
}
}
In Xcode 9.3 it is possible to specify a new item in info.plist:
Supports Document Browser (YES)
This enables access to the application's documents directory (for example, /var/mobile/Containers/Data/Application/3C21358B-9E7F-4AA8-85A6-A8B901E028F5/Documents on a device). Apple Developer doc here.

Import PDF into own App with iOS Action Extension

I'm looking for a possibility to import a PDF in order to do some further tasks with it, just like described in this Question: Importing PDF files to app
After two days of looking around in the inter webs I found that an action extension might be the solution, this is how far I got:
override func viewDidLoad() {
super.viewDidLoad()
let fileItem = self.extensionContext!.inputItems.first as! NSExtensionItem
let textItemProvider = fileItem.attachments!.first as! NSItemProvider
let identifier = kUTTypePDF as String
if textItemProvider.hasItemConformingToTypeIdentifier(identifier) {
textItemProvider.loadItemForTypeIdentifier(identifier, options: nil, completionHandler: handleCompletion)
}
}
func handleCompletion(pdfFile: NSSecureCoding?, error: NSError!) {
print("PDF loaded - What to do now?")
}
The completion handler is called properly so I assume the PDF is loaded - but then I don't now how to proceed. If the action extension only handles images or text it could easily be downcasted, but the only way to work with files I know is with path names - which I do not have and don't know how to obtain. Plus, I'm pretty sure Sandboxing is also part of the party.
I guess I only need a push in the right direction which Class or Protocol could be suitable for my need - any suggestions highly appreciated.
For anyone else looking for an answer - I found out by myself, and it's embarrassingly easy:
func handleCompletion(fileURL: NSSecureCoding?, error: NSError!) {
if let fileURL = fileURL as? NSURL {
let newFileURL = NSURL(fileURLWithPath: NSTemporaryDirectory().stringByAppendingString("test.pdf"))
let fileManager = NSFileManager.defaultManager()
do {
try fileManager.copyItemAtURL(fileURL, toURL: newFileURL)
// Do further stuff
}
catch {
print(error)
}
}
}

Xcode: Unable to view one URL (no error messages)

I posted this question earlier but it was marked as a duplicate which in my opinion it is not. Here are the details.
I have a view that I use to access 7 URLs. Just one of these URLs does not load. The URL is correct and it loads from iOS Safari. The 6 other URLs load without problem. All URLs are http. How do I debug this? There are no error messages.
The suggestion referred to when my previous question was closed as duplicate was that I change NSAppTransportSecurity to NSAllowsArbitraryLoads YES, but that was already the case in info.plist. Is there somewhere else that I need to change ATS?
override func viewDidLoad ()
{
super.viewDidLoad ()
if Reachability.isConnectedToNetwork() == false
{
showAlert ()
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let returnNC = myStack.pop()
let vc = storyboard.instantiateViewControllerWithIdentifier(returnNC!)
self.presentViewController(vc, animated: true, completion: nil)
}
else {
let fullURL = defaults.objectForKey("URL") as? String
let url = NSURL(string: fullURL!)
let request = NSURLRequest(URL: url!)
myWebView.loadRequest(request)
}
}
func webView(webView: UIWebView!, didFailLoadWithError error: NSError!) {
print("Webview fail with error \(error)");
}
To track this down, you need more information. Even though the web page exists in the browser, there may be other issues (such as transient connections / 404 responses, etc.).
UIWebView's didFailLoadWithError() function doesn't return response codes, which could provide some information that can help you debug. See the answer at this related StackOverflow post. If you find anything useful, update your question.

UIDocumentInteractionController always fails

I'm trying to use the UIDocumentInteractionController. I've checked everything I can think off and everything appears to have all the data that is needed.
When calling presentOpenInMenuFromRect(inView:, animated:) the Bool that is returned is always false. My understanding is that its because there aren't any apps that support the UTI. I've tried using a flat out PNG and still nothing. Whats interesting is why it wouldn't work on my iPhone which has plenty of apps that support loading images via the UIDocumentInteractionController, but here is the kicker. This code was working fine before I upgraded the project to Swift 1.2.
Am I overlooking something now? I've checked the docs and couldn't find anything that I was missing.
Note: I have tested this on a device and in the simulator and both return the same.
let image = UIImage(named: "screenshot")
if let paths = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true) {
if paths.count > 0 {
if let dirPath = paths[0] as? String {
let path = dirPath.stringByAppendingPathComponent("screenshot.ig") // igo
dispatch_async(dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0), { () -> Void in
if let image = image {
NSFileManager.defaultManager().removeItemAtPath(path, error: nil)
if UIImagePNGRepresentation(image).writeToFile(path, atomically: true) {
}
}
})
if let url = NSURL(fileURLWithPath: path) {
let documentController = UIDocumentInteractionController(URL: url)
documentController.UTI = "com.instagram.photo" // com.instagram.exclusivegram
documentController.annotation = ["InstagramCaption": ""]
if !documentController.presentOpenInMenuFromRect(sender.bounds, inView: self.view, animated: true) {
println("Can't do it")
}
}
}
}
}
I decided to go with the presentOptionsMenuFromRect(:, inView:, animated:) which does what I am trying to accomplish. No idea why presentOpenInMenuFromRect(: inView:, animated:) decided to break down, but I did mange to get it all working.
The problem is you are writing your file asynchronously on a background queue, but do not wait before opening your interaction controller. So when it attempts to open your file, it cannot find it because it hasn't been written just yet. You need to let the write operation succeed, then attempt to open the interaction controller.
Change your code like so:
dispatch_async(dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0), { () -> Void in
if let image = image {
NSFileManager.defaultManager().removeItemAtPath(path, error: nil)
if UIImagePNGRepresentation(image).writeToFile(path, atomically: true) {
dispatch_async(dispatch_get_main_queue(), { () -> Void in
if let url = NSURL(fileURLWithPath: path) {
let documentController = UIDocumentInteractionController(URL: url)
documentController.UTI = "com.instagram.photo" // com.instagram.exclusivegram
documentController.annotation = ["InstagramCaption": ""]
if !documentController.presentOpenInMenuFromRect(sender.bounds, inView: self.view, animated: true) {
println("Can't do it")
}
}
}
}
}
})
I had the same problem on an iPad which was pretty much right out of the box. I couldn't open txt or png files (the two tests I ran). presentOpenInMenuFromRect always returned NO.
I found that the accepted answer of using presentOptionsMenuFromRect did launch that window, but it wasn't precisely what I wanted. (It could be what some of you will want though).
It turned out that my device simply didn't have an app associated with either of these types by default! (How ridiculous is that?) I assumed they were so common that would never be an issue.
So, I randomly installed the first free file manager app that I found in the app store (USB Disk Free was what I picked). Then, my device gave me the option to open either of those types with that app.

Resources