In my iOS app I am opening a UIDocumentPickerViewController to import .sty files (MIDI styles). For this I have declared a custom UTI in Imported UTIs and in Document Types.
The problem is that on my customer’s iPad Air 2, these files appear greyed out in the dialog so he cannot import them. On my iPad Air 2 they are not greyed out, and I can import them successfully. We can both see this when using iCloud Drive and Dropbox as file providers.
What could be different on our devices? My customer installs the app as an internal tester via TestFlight.
Also, are file extensions in the Import UTIs case-sensitive? I would not think so.
Then I wonder if I should add a document type icon or not. According to the reference the icon is not required.
iCloud Documents is enabled:
Here is the code that presents the UIDocumentPickerViewController:
let utis = [String](PlaylistItem.utiToType.keys)
let viewController = UIDocumentPickerViewController(documentTypes: utis, in: .import)
viewController.delegate = self
viewController.modalPresentationStyle = .formSheet
if #available(iOS 11, *) {
viewController.allowsMultipleSelection = true
}
self.present(viewController, animated: AppDelegate.isAnimationsEnabled, completion: nil)
with this definition of UTIs in the PlaylistItem class:
enum FileType: Int {
case other
case mid
case mp3
case m4a
case aiff // AIFF audio recording
case wave
case turboMidi // purchased MIDI file
case style
}
static let kUTTypeStyleYamaha = "com.turboreini.style" // matches document types in Info.plist
static let utiToType: [String: FileType] = [
kUTTypeMPEG4Audio as String: .m4a,
kUTTypeMP3 as String: .mp3,
kUTTypeMIDIAudio as String: .mid,
PlaylistItem.kUTTypeStyleYamaha: .style
]
This is the Document Type as captured from Xcode 9:
… and the Imported UTIs section:
Note that I picked an arbitrary identifier for style files because I couldn’t find an official one on the internet.
This issue drives me crazy. I cannot find anything wrong.
I wonder if it depends on other installed apps.
In addition to importing .sty files, the app also creates its own custom files with their own extension. For this I have defined an Exported UTI. It works fine, no files are greyed out. But the solution to importing .sty files should not be to move the entry from Imported to Exported UTIs. My app is only a “viewer” to .sty files.
Any ideas on this case would be much appreciated.
It turned out that on my customer’s iPad another app was installed that presumably also had its own UTI for the .sty extension. Once I installed it on my iPad I could reproduce the problem.
Unfortunately I was not able yet to peek into that app’s Info.plist to confirm.
However, thanks to Cocoanetics I found an API that allowed me to query the system:
let pathExt = "sty"
if let utiArray = UTTypeCreateAllIdentifiersForTag(kUTTagClassFilenameExtension, pathExt as NSString, nil)?.takeRetainedValue() as? [String] {
print("Have UTIs for .\(pathExt):")
for uti in utiArray {
if let dict = UTTypeCopyDeclaration(uti as NSString)?.takeUnretainedValue() as? [String: Any] {
print("\(uti) = \(dict)")
}
}
}
This code would show me the declared UTIs from all the apps installed on my iPad, based on the file extension. And indeed the other app showed up. But I still could not see if it was declared as an exported or imported UTI.
The workaround is to uninstall the conflicting app.
To me this is a flaw in the UTI system. I will try and contact the makers of .sty files (MIDI styles, not LaTeX files), to ask them for a proper UTI which all developers should conform to.
It seems a global registry of UTIs is needed. Apple has already created a collection but only the most popular types are covered.
Related
In short: I am currently working on an app with which you can scan a QR code. The QR code contains a path to a folder in Apple's own "Files" app. And in this folder I would like to view the PDF files and open them.
My problem: I want to access a PDF file in a folder just to open the PDF file. When opening the UIDocumentPickerViewController, I want to open the target folder directly in which the file is stored. With the directoryURL property I try to enter the target URL directly so that it is taken over by the DocumentPickerViewController, but this only opens the last directory. If I change the initializer of the Document PickerView to "for Opening ContentTypes: [.folder]", then it jumps to the right folder, but I can't see any PDF files because it only shows me the folder files. BUT! : When I test my code via the simulator, my function works. And when I run the App on my iPhone, the DomumentPickerView keeps jumping to the last directory. According to the Apple documentation about the property: "directoryURL" it says: "Set this property to specify the starting directory for the document picker. This property defaults to nil. If you specify a value, the document picker tries to start at the specified directory. Otherwise, it starts with the last directory chosen by the user.” But why does it show me the right folder when I search for content type [.folder] and when I search for content type [.pdf] only the last directory.
HEEEELP
struct DocumentPicker : UIViewControllerRepresentable {
#Binding var folderURL : URL?
#Binding var documentURL : URL?
func makeUIViewController(context: Context) -> UIDocumentPickerViewController {
let documentPicker = UIDocumentPickerViewController(forOpeningContentTypes: [.pdf], asCopy: true)
documentPicker.delegate = context.coordinator
if let url = folderURL {
documentPicker.directoryURL = url
}
return documentPicker
}
func updateUIViewController(_ uiViewController: UIDocumentPickerViewController, context: Context) {
print("Show DocumentPickerViewController")
}
}
Don’t use type .folder ; this is to get access to a whole folder and is not implemented by many file providers. Use the actual type of the file you want to open (pdf) and populate the starting directory with the URL of the directory, not the URL of the PDF file. Also keep in mind that some file providers populate their tree lazily, so the folder is not guaranteed to exist the first time you ask for it. The actual file paths may also be device specific (some file providers use UUIDs) so sharing an URL with a QR code across devices may not work.
I have implemented an AppGroup in my app in preparation for sharing data with another app. I have successfully moved files to that App Group from the default app documents directory.
FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.com.xxx.mydata")! as NSURL
Now I would like to select from the files in that container, using UIDocumentPickerViewController. In iOS 13,I should be able to set which directory the document picker starts in. My documentPicker looks like this:
#IBAction func fileAction(_ sender: UIButton)
{
// open a document picker, select a file
let importFileMenu = UIDocumentPickerViewController(documentTypes: ["public.data"],
in: UIDocumentPickerMode.import)
importFileMenu.delegate = self
if #available(iOS 13.0, *) {
print("File iOS 13+")
importFileMenu.directoryURL = FileManager.default.containerURL(
forSecurityApplicationGroupIdentifier: "group.com.xxx.mydata")!
} else {
// Fallback on earlier versions
print("File iOS <=12")
}
importFileMenu.modalPresentationStyle = .formSheet
self.present(importFileMenu, animated: true, completion: nil)
}
When I run the app, it acts as it did before iOS13, opening in the default app documents directory, and the App Group is not shown as a possibility for selection. The print statement shows "File iOS 13+".
Am I missing permissions to read from that container, or is there something else that I've missed?
Thanks!
No, sorry, it can't be done. Apple says that selecting from an AppGroup is not what UIDocumentPickerViewController is supposed to do. I spent one of my "Apple Developer Tech Support" uses on this, and that was their answer. I gave up on that for now, and went a different direction. You should be able to build your own list of files in the AppGroup and select them, just not by using UIDocumentPickerViewController.
I have defined a UTI for a custom document format. I can export files from my app and append them to text messages, email, etc. I can import the files into my app by tapping on the document icon in iMessage. By tapping on the document icon, I have the option to copy to my app. That triggers a call in my AppDelegate to handle the incoming file.
What's bugging me is that the url for the incoming file is:
file:///private/var/mobile/Containers/Data/Application/21377C94-1C3C-4766-A62A-0116B369140C/Documents/Inbox/...
Whereas, when saving documents to the .documents directory, I use this URL:
file:///var/mobile/Containers/Data/Application/21377C94-1C3C-4766-A62A-0116B369140C/Documents/...
The difference being the /private/ and /Inbox/ path components.
Question: how can I purge the /private/.../Inbox/ path of the files that were copied to my app from iMessage? I noticed this when testing my app and when I tapped on the same document icon in iMessage it started generating file copies with the same name but adding -1, then -2, then -3 to the file name of the document from iMessage. It appears that copies are building up in that /private/.../Inbox/ path.
Is that something that gets flushed on its own or can I access that directory to remove those files? It's also annoying because based upon the filename, it appears to be a different file thus allowing multiple copies of the same file to be imported with a slightly different file name.
Ok, this took a fair amount of digging, but I'll post my solution that seems to work thus far in case anyone runs across the same problem.
let fileManager = FileManager.default
// get the URL for the "Inbox"
let tmpDirURL = fileManager.urls(for: .documentDirectory, in: .userDomainMask)[0].appendingPathComponent("Inbox")
// get all the files in the "Inbox" directory
let anythingThere = try? fileManager.contentsOfDirectory(at: tmpDirURL, includingPropertiesForKeys: nil)
if anythingThere?.count ?? 0 > 0 {
for eachURL in anythingThere! {
// for each url pointing to a file in that directory, get its path extension
let pathExtension = eachURL.pathExtension
// test to see if it's a UTI that you're interested in deleting
// in my case, the three "OCC" strings are the relevant UTI extensions
if pathExtension == "OCCrcf" || pathExtension == "OCCrdi" || pathExtension == "OCCsrf" {
// attempt to delete the temporary file that was copied to the
// "Inbox" directory from importing via email, iMessage, etc.
try fileManager.removeItem(at: eachURL)
}
}
}
If anyone has a more elegant solution, please respond as well. Thanks.
I have the exact same issue as "Paul" posted here: Can not export audiofiles via "open in:" from Voice Memos App - no answers have yet been posted on this topic.
Essentially what I'm trying to do is simple:
After having recorded a Voice Memo on iOS, I select "Open With" and from the popup that is shown I want to be able to select my app.
I've tried everything I can think of and experimented with LSItemContentTypes without success.
Unfortunately I don't have enough reputation to comment on the existing post above, and I'm getting quite desperate for a solution to this. Any help is hugely appreciated, even just to know whether it's doable or not.
Thanks!
After some experimentation and much guidance from this blog post ( http://www.theappguruz.com/blog/share-extension-in-ios-8 ), it appears that it is possible to do this using a combination of app extensions (specifically an Action Extension) and app groups. I'll describe the first part which will enable you to get your recording from Voice Memos to your app extension. The second part -- getting the recording from the app extension to the containing app (your "main" app) -- can be done using app groups; please consult the blog post above for how to do this.
Create a new target within your project for the app extension, by selecting File > New > Target... from Xcode's menu. In the dialog box that prompts you to "Choose a template for your new target:" choose the "Action Extension" and click "Next".
CAUTION: Do not choose the "Share Extension" as is done in the blog post example above. That approach is more appropriate for sharing with another user or posting to a website.
Fill in the "Product Name:" for your Action Extension, e.g., MyActionExtension. Also, for "Action Type:" I selected "Presents User Interface" because this is the way Dropbox appears to do it. Selecting this option adds a view controller (ActionViewController) and storyboard (Maininterface.storyboard) to your app extension. The view controller is a good place to provide feedback to the user and to give the user an opportunity to rename the audio file before exporting it to your app.
Click "Finish." You will be prompted to "Activate “MyActionExtension” scheme?". Click "Activate" and this new scheme will be made active. Building it will build both the action extension and the containing app.
Click the disclosure triangle for the "MyActionExtension" folder in the Project Navigator (Cmd-0) to reveal the newly-created storyboard, ActionViewController source file(s), and Info.plist. You will need to customize these files for your needs. But for now ...
Build and run the scheme you just created. You will be prompted to "Choose an app to run:". Select "Voice Memos" from the list and click "Run". (You will probably need a physical device for this; I don't think the simulator has Voice Memos on it.) This will build and deploy your action extension (and its containing app) to your device. and then proceed to launch "Voice Memos" on your device. If you now make a recording with "Voice Memos" and then attempt to share it, you should see your action extension (with a blank icon) in the bottom row. If you don't see it there, tap on the "More" button in that row and set the switch for your action extension to "On". Tapping on your action extension will just bring up an empty view with a "Done" button. The template code looks for an image file, and finding none does nothing. We'll fix this in the next step.
Edit ActionViewController.swift to make the following changes:
6a. Add import statements for AVFoundation and AVKit near the top of the file:
// the next two imports are only necessary because (for our sample code)
// we have chosen to present and play the audio in our app extension.
// if all we are going to be doing is handing the audio file off to the
// containing app (the usual scenario), we won't need these two frameworks
// in our app extension.
import AVFoundation
import AVKit
6b. Replace the entirety of override func viewDidLoad() {...} with the following:
override func viewDidLoad() {
super.viewDidLoad()
// Get the item[s] we're handling from the extension context.
// For example, look for an image and place it into an image view.
// Replace this with something appropriate for the type[s] your extension supports.
print("self.extensionContext!.inputItems = (self.extensionContext!.inputItems)")
var audioFound :Bool = false
for inputItem: AnyObject in self.extensionContext!.inputItems {
let extensionItem = inputItem as! NSExtensionItem
for attachment: AnyObject in extensionItem.attachments! {
print("attachment = \(attachment)")
let itemProvider = attachment as! NSItemProvider
if itemProvider.hasItemConformingToTypeIdentifier(kUTTypeMPEG4Audio as String)
//|| itemProvider.hasItemConformingToTypeIdentifier(kUTTypeMP3 as String)
// the audio format(s) we expect to receive and that we can handle
{
itemProvider.loadItemForTypeIdentifier(kUTTypeMPEG4Audio as String,
options: nil, completionHandler: { (audioURL, error) in
NSOperationQueue.mainQueue().addOperationWithBlock {
if let audioURL = audioURL as? NSURL {
// in our sample code we just present and play the audio in our app extension
let theAVPlayer :AVPlayer = AVPlayer(URL: audioURL)
let theAVPlayerViewController :AVPlayerViewController = AVPlayerViewController()
theAVPlayerViewController.player = theAVPlayer
self.presentViewController(theAVPlayerViewController, animated: true) {
theAVPlayerViewController.player!.play()
}
}
}
})
audioFound = true
break
}
}
if (audioFound) {
break // we only handle one audio recording at a time, so stop looking for more
}
}
}
6c. Build and run as in the previous step. This time, tapping on your action extension will bring up the same view controller as before but now overlaid with the AVPlayerViewController instance containing and playing your audio recording. Also, the two print() statements I've inserted in the code should give output that looks something like the following:
self.extensionContext!.inputItems = [<NSExtensionItem: 0x127d54790> - userInfo: {
NSExtensionItemAttachmentsKey = (
"<NSItemProvider: 0x127d533c0> {types = (\n \"public.file-url\",\n \"com.apple.m4a-audio\"\n)}"
);
}]
attachment = <NSItemProvider: 0x127d533c0> {types = (
"public.file-url",
"com.apple.m4a-audio"
)}
Make the following changes to the action extension's Info.plist file:
7a. The Bundle display name defaults to whatever name you gave your action extension (MyActionExtension in this example). You might wish to change this to Save to MyApp. (By way of comparison, Dropbox uses Save to Dropbox.)
7b. Insert a line for the key CFBundleIconFile and set it to Type String (2nd column), and set its value to MyActionIcon or some such. You will then need to provide the corresponding 5 icon files. In our example, these would be: MyActionIcon.png, MyActionIcon#2x.png, MyActionIcon#3x.png, MyActionIcon~ipad.png, and MyActionIcon#2x~ipad.png. (These icons should be 60x60 points for iphone and 76x76 points for ipad. Only the alpha channel is used to determine which pixels are gray, the RGB channels are ignored.) Add these icon files to your app extension's bundle, NOT the containing app's bundle.
7c. At some point you will need to set the value for the key NSExtension > NSExtensionAttributes > NSExtensionActivationRule to something other than TRUEPREDICATE. If you want your action extension to only be activated for audio files, and not for video files, pdf files, etc., this is where you would specify such a predicate.
The above takes care of getting the audio recording from Voice Memos to your app extension. Below is an outline of how to get the audio recording from the app extension to the containing app. (I'll flesh it out later, time permitting.) This blog post ( http://www.theappguruz.com/blog/ios8-app-groups ) might also be useful.
Set up your app to use App Groups. Open the Project Navigator (Cmd-0) and click on the first line to show your project and targets. Select the target for your app, click on the "Capabilities" tab, look for the App Groups capability, and set its switch to "On". Once the various entitlements have been added, click on the "+" sign to add your App Group, giving it a name like group.com.mycompany.myapp.sharedcontainer. (It must begin with group. and should probably use some form of reverse-DNS naming.)
Repeat the above for your app extension's target, giving it the same name as above (group.com.mycompany.myapp.sharedcontainer).
Now you can write the url of the audio recording to the app group's shared container from the app extension side. In ActionViewController.swift, replace the code fragment that instantiates and presents the AVPlayerViewController with the following:
let sharedContainerDefaults = NSUserDefaults.init(suiteName:
"group.com.mycompany.myapp.sharedcontainer") // must match the name chosen above
sharedContainerDefaults?.setURL(audioURL, forKey: "SharedAudioURLKey")
sharedContainerDefaults?.synchronize()
Similarly, you can read the url of the audio recording from the containing app's side using something like this:
let sharedContainerDefaults = NSUserDefaults.init(suiteName:
"group.com.mycompany.myapp.sharedcontainer") // must match the name chosen above
let audioURL :NSURL? = sharedContainerDefaults?.URLForKey("SharedAudioURLKey")
From here, you can copy the audio file into your app's sandbox, e.g., your app's Documents directory or your app's NSTemporaryDiretory(). Read this blog post ( http://www.atomicbird.com/blog/sharing-with-app-extensions ) for ideas on how to do this in a coordinated fashion using NSFileCoordinator.
References:
Creating an App Extension
Sharing Data with Your Containing App
Does UIDocumentPickerViewController initWithDocumentTypes require a public UTI to function?
I am trying to utilize iCloud Documents to allow users to import a proprietary file type from iCloud Drive. Testing works fine for public UTI, such as: #"public.text"
If I don't include a public UTI in the initWithDocumentTypes array, I get a screen indicating:
No Documents. Documents in iCloud Drive are not available because the
iCloud Documents & Data setting is disabled.
My Imported UTI is defined in Target > Info as "com.domain.file". I have to believe this is set up correctly, as I can select one of my proprietary files in another app (e.g. Dropbox) and my app is displayed in the Open In... options.
In my import action, I've tried every variation I can think of to get my custom UTI to display the picker.
- (IBAction)importDocumentPickerTapped:(id)sender
{
UIDocumentPickerViewController *documentPicker = [[UIDocumentPickerViewController alloc initWithDocumentTypes:#[#"com.domain.file", #"com.domain.app.file", #"iCloud.com.domain.file", #"iCloud.com.domain.app.file" ] inMode:UIDocumentPickerModeImport];
documentPicker.delegate = self;
documentPicker.modalPresentationStyle = UIModalPresentationFormSheet;
[self presentViewController:documentPicker animated:YES completion:nil];
}
If I add #"public.text" to the initWithDocumentTypes array, then the iCloud Drive Locations picker screen displays as expected. I can select .txt and .rtf files, but my custom file types are grayed out and not selectable.
Note:
I have not created a Document Provider extension, as I don't believe this is required, and my file format it not common.
I see the following warning when I take any action on UIDocumentPickerViewController, even if it's Cancel and even if the action on a file (i.e. save .txt) works. I've spent quite a bit of time just trying to track down the source of this warning, to no avail.
plugin com.apple.UIKit.fileprovider.default invalidated
I had this issue and addressed it by having my custom exported UTI conform to the 'public.data' UTI.
You can set this by selecting your target and then the info tab. Scroll to the Exported UTIs section and expand it. Under your custom UTI there is a box to declare that it conforms.
See here for a list of system defined UTIs:
https://developer.apple.com/library/ios/documentation/Miscellaneous/Reference/UTIRef/Articles/System-DeclaredUniformTypeIdentifiers.html