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.
Related
I have an Instagram scheduling app and I am trying to open this (see image below) in Swift 5.x. The goal is simple: save Image to Firebase, once it is time to post, notification!, user clicks on the notification and this (image below) opens up with the appropriate image/video to post. Everything works except for opening Instagram with the appropriate photo/video. I have tried this:
func postToInstagram(image: URL) {
let videoFileUrl: URL = image
var localId: String?
PHPhotoLibrary.shared().performChanges({
let request = PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: videoFileUrl)
localId = request?.placeholderForCreatedAsset?.localIdentifier
}, completionHandler: { success, error in
// completion handler is called on an arbitrary thread
// but since you (most likely) will perform some UI stuff
// you better move everything to the main thread.
DispatchQueue.main.async {
guard error == nil else {
// handle error
print(error)
return
}
guard let localId = localId else {
// highly unlikely that it'll be nil,
// but you should handle this error just in case
return
}
let url = URL(string: "instagram://library?LocalIdentifier=\(localId)")!
guard UIApplication.shared.canOpenURL(url) else {
// handle this error
return
}
UIApplication.shared.open(url, options: [:], completionHandler: nil)
}
})
}
and this:
func postToInstagram(image: URL, igURL: String) {
let urlStr: String = "instagram://app"
let url = URL(string: igURL)
if UIApplication.shared.canOpenURL(url!) {
print("can open")
UIApplication.shared.open(url!, options: [:], completionHandler: nil)
}
}
To no avail. The latter code works, but only opens the Instagram app itself, which is fine, but I would like to open the View in the image below rather than Instagram's home screen. I also tried changing the URL to "instagram://share" and this works but goes to publish a regular post, whereas I want the user to decide what they want to do with their image.
This is where I want to go:
Note: For everyone who will be telling me this and whoever will wonder: Yes, my URL schemes (LSApplicationQueriesSchemes) are fine. And, just to clarify, I need to fetch the image/video from Firebase before posting it.
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 {
// ...
}
}
EDIT: As requested added info.pslist and code.
I have a custom Document Type and I have registered the UTIs and a new MIME type. I basically followed the steps by this tutorial. I am using a Codable object that it is no more than a JSON file with a custom extension and a custom icon. Honestly it looks pretty cool to me. The app I am doing is is a Grocery list app that makes a lot of sense being able to share it by a note or iMessage.
Like the finalised app in the tutorial I followed it opens in mail and even in notes!!! but iMessage does not recognise the extension and shows a folder icon and does not open it.
My question is how can I tell iMessage that this file is meant to be opened by my App. Do I need an iMessage extension? I am pretty new to iOS. info.pslist:
And now code:
func exportToUrl() -> URL? {
let contents = try? JSONEncoder().encode(shoppingList)
guard let path = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else {
return nil
}
let saveUrl = path.appendingPathComponent("/list.grabgrocerieslist")
try? contents?.write(to: saveUrl, options: .atomic)
return saveUrl
}
#IBAction func sharedTapped(_ sender: UIBarButtonItem) {
guard let url = exportToUrl() else {
return
}
let activityController = UIActivityViewController(activityItems: ["Shopping List", url], applicationActivities: nil)
activityController.excludedActivityTypes = [.assignToContact, .saveToCameraRoll, .postToFacebook ]
activityController.popoverPresentationController?.barButtonItem = sender
self.present(activityController, animated: true, completion: nil)
}
Many Thanks,
I am building an action extension for GarageBand on iOS which transforms and uploads audio but no matter what I try, I just could not get to the exported file.
Let’s consider the following code — it should:
find and load shared audio from extensionContext
initialise audio player
play the sound
It works if I run the extension in Voice Memos.app — the url to the file looks like this: file:///tmp/com.apple.VoiceMemos/New%20Recording%202.m4a
Now, If I run the code in GarageBand.app, the url points to (what I presume) is GarageBand’s app container, as the url looks something like /var/…/Containers/…/Project.band/audio/Project.m4a, and the audio will not be loaded and cannot therefore be manipulated in any way.
// edit: If I try to load contents of the audio file, it looks like the data only contains aac header (?) but the rest of the file is empty
What is interesting is this: The extension renders a react-native view and if I pass the view the fileUrl (/var/…Project.band/audio/Project.m4a) and then pass it down to XMLHTTPRequest, the file gets uploaded. So it looks like the file can be accessed in some way?
I’m new to Swift/iOS development so this is kind of frustrating for me, I feel like I tried just about everything.
The code:
override func viewDidLoad() {
super.viewDidLoad()
var audioFound :Bool = false
for inputItem: Any in self.extensionContext!.inputItems {
let extensionItem = inputItem as! NSExtensionItem
for attachment: Any in extensionItem.attachments! {
print("attachment = \(attachment)")
let itemProvider = attachment as! NSItemProvider
if itemProvider.hasItemConformingToTypeIdentifier(kUTTypeAudio as String) {
itemProvider.loadItem(forTypeIdentifier: kUTTypeAudio as String,
options: nil, completionHandler: { (audioURL, error) in
OperationQueue.main.addOperation {
if let audioURL = audioURL as? NSURL {
print("audioUrl = \(audioURL)")
// in our sample code we just present and play the audio in our app extension
let theAVPlayer :AVPlayer = AVPlayer(url: audioURL as URL)
let theAVPlayerViewController :AVPlayerViewController = AVPlayerViewController()
theAVPlayerViewController.player = theAVPlayer
self.present(theAVPlayerViewController, animated: true) {
theAVPlayerViewController.player!.play()
}
}
}
})
audioFound = true
break
}
}
if (audioFound) {
break
}
}
}
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)
}
}
}