Is it possible to make user able to choose a destination for the file that he wants to download, something like DocumentPicker which you can use when choosing a file to upload?
I want something like this:
Yes, for iOS 13 and later you can ask the user to select a directory via UIDocumentPickerViewController. You'll get back a security scoped url(s) for the directories selected by the user.
Details here: https://developer.apple.com/documentation/uikit/view_controllers/providing_access_to_directories
I've pasted the sample code from that page below, but you'll want to read the documentation carefully because security scoped URLs require careful handling :)
If you need iOS 12 or earlier the user can only select files so I'm unclear on a clean way to do this (but we're on iOS 14 and iOS 15 is about to come out so hopefully you don't have to support back past iOS 13).
Here's the sample code from the link above showing how this is done:
// Create a document picker for directories.
let documentPicker =
UIDocumentPickerViewController(forOpeningContentTypes: [.folder])
documentPicker.delegate = self
// Set the initial directory.
documentPicker.directoryURL = startingDirectory
// Present the document picker.
present(documentPicker, animated: true, completion: nil)
Related
I want to show a file picker for choosing a "save to file" location on iOS. The UIDocumentViewPickerController offers the method initWithURL:inMode: (https://developer.apple.com/documentation/uikit/uidocumentpickerviewcontroller/1618684-initwithurl?language=objc) for saving files but it requires an url to a local file. Instead I want to get a file handle to which the data is written after picking. Does iOS offer such a function, or do I have to create a temporary local file first and then use the initWithURL method?
Regards,
iOS 13 offers means for picking directories (https://developer.apple.com/documentation/uikit/view_controllers/providing_access_to_directories). It is possible to create new items as well.
// Create a document picker for directories.
let documentPicker =
UIDocumentPickerViewController(documentTypes: [kUTTypeFolder as String],
in: .open)
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 a UIDocumentPickerViewController with a filetype of "public.folder" (I've also tried kUTTypeFolder), where a user can pick a default directory for files to be saved. It pulls up the correct UI for selecting folders, however, all third-party providers (Google Drive, Dropbox, etc.) are all grayed out and can't be selected. I can select iCloud Drive and On My iPhone just fine.
Here's my function for showing the controller:
#IBAction func pickDefaultDirectory(_ sender: Any) {
let documentPicker = UIDocumentPickerViewController(documentTypes: ["public.folder"], in: .open)
documentPicker.delegate = self
self.present(documentPicker, animated: true, completion: nil)
}
This is what every "pick folders via UIDocumentPickerViewController" article I've found says to do, but I've had no luck. Does anyone know why this is happening? Am I just forgetting something in there?
According to Dropbox developers, this isn't supported at the moment.
The Dropbox document picker doesn't support opening folders, but I'll pass this along as a feature request. I can't promise if or when that might be implemented though.
Dropboxforum
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