I'm using the UIDocumentInteractionController in iOS 7.1 and it's performing really badly.
I'm using it in a UICollectionViewController to view documents in a collection view.
On pressing an item in the collection view, it takes about around 6 (yes, that's six) seconds to appear. From a user experience perspective, they've pressed the screen a few more times before it appears because it takes so long.
I'm using the same code since iOS 6, but it seems particularly bad now. If anyone has any thoughts as to how I can speed things up, that would be greatly appreciated.
Essentially, I have the following in my header file:
interface MyViewController : UICollectionViewController <UIDocumentInteractionControllerDelegate>
{
UIDocumentInteractionController *docController;
}
#end
and in the implementation, I'm just doing the following:
In viewDidLoad (recently moved to here to see if it improves things):
docController = [[UIDocumentInteractionController alloc] init];
docController.delegate = self;
And then in the collectionView:didSelectItemAtIndexPath: I'm doing this:
NSURL *fileURL = [NSURL fileURLWithPath:[[NSBundle mainBundle] pathForResource:document.Link ofType:#"" ]];
[docController setURL:fileURL];
PresentationViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:#"DocumentCell" forIndexPath:indexPath];
CGRect rect1 = cell.frame;
bool didShow = [docController presentOptionsMenuFromRect:rect1 inView:collectionView animated:YES];
where document is just a class with a string for the URL.
Let me know if you need any further detail.
Thanks in advance for any assistance anyone can provide.
-- Update:
After some NSLogs, I noticed that it's definitely the following line that's slow:
bool didShow = [docController presentOptionsMenuFromRect:rect1 inView:collectionView animated:YES];
TL;DR:
The method you are using is a synchronous request that uses your document data for find which apps are capable of reading your file. You need to swap with the asynchronous version that restricts the enumeration to only apps that can parse your file type.
Remove this method:
- (BOOL)presentOptionsMenuFromRect:(CGRect)rect
inView:(UIView *)view
animated:(BOOL)animated
And replace with this method:
- (BOOL)presentOpenInMenuFromRect:(CGRect)rect
inView:(UIView *)view
animated:(BOOL)animated
Excerpt from the Apple Docs:
This method is similar to the presentOptionsMenuFromRect:inView:animated: method, but presents a menu restricted to a list of apps capable of opening the current document. This determination is made based on the document type (as indicated by the UTI property) and on the document types supported by the installed apps. To support one or more document types, an app must register those types in its Info.plist file using the CFBundleDocumentTypes key.
If there are no registered apps that support opening the document, the document interaction controller does not display a menu.
This method displays the options menu asynchronously. The document interaction controller dismisses the menu automatically when the user selects an appropriate option. You can also dismiss it programmatically using the dismissMenuAnimated: method.
I was encountering a similar problem with:
UIDocumentInteractionController.presentPreviewAnimated
It would take an incredibly long time to complete. I found adding a brief delay between saving the file to be previewed and presenting the preview fixed the problem:
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(100 * NSEC_PER_MSEC)), dispatch_get_main_queue(), {
self.controller.presentPreviewAnimated(false)
})
Swift 4.2
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self.controller.presentPreviewAnimated(false)
}
Related
I am working on implementing support for a CarPlay audio app, and am attempting to display listings in the simulator. I have implemented MPPlayableContentDataSource, but find that it is called inconsistently. It is called the first time the app is launched on a simulator, and if CarPlay is open on launch, I can make the first item display by scrolling up an otherwise empty listing to trigger a redraw.
CarPlay does not seem able to call the data source, however, and on a subsequent launch I see an empty screen or a spinner followed by the message Unable to connect to "AppName". I have tried different things but the main points are as follows:
In application: didFinishLaunchingWithOptions:
self.contentDataSource = [[MYContentDataSource alloc] init];
self.contentDelegate = [[MYContentDelegate alloc] init];
MPPlayableContentManager *contentManager = [MPPlayableContentManager sharedContentManager];
contentManager.dataSource = self.contentDataSource;
contentManager.delegate = self.contentDelegate;
[contentManager beginUpdates];
[contentManager endUpdates];
I've played around with beginUpdates endUpdates and reloadData methods of the content manager, but none of these result in the content datasource actually being called.
I've implemented numberOfChildItemsAtIndexPath and contentItemAtIndexPath in the datasource, which appear to be called correctly, although only on the first launch of the app on a fresh simulator.
The main points:
- (NSInteger)numberOfChildItemsAtIndexPath:(NSIndexPath *)indexPath {
return 3;
}
- (MPContentItem *)contentItemAtIndexPath:(NSIndexPath *)indexPath {
NSUInteger categoryId = [indexPath indexAtPosition:0];
MPContentItem *contentItem = [[MPContentItem alloc] initWithIdentifier:[NSString stringWithFormat:#"CAT-%lu", (unsigned long)categoryId]];
contentItem.title = [NSString stringWithFormat:#"Category %lu", (unsigned long)categoryId];
contentItem.subtitle = #"Subtitle";
contentItem.playable = NO;
contentItem.container = YES;
}
I've also tried retaining (or not) the reference to the MPPlayableContentManager.
I have the same behavior on an actual head unit. Any help would be appreciated.
After banging my head against the wall for quite a while, I got the following answer from Apple. Turns out that MPRemoteCommandCenter and MPNowPlayingInfoCenter are needed for CarPlay to work.
1. Start responding to MPRemoteCommandCenter events at app launch
2. Set the MPNowPlayingInfoCenter dictionary at app launch
These are required for MPPlayableContentDataSource to function correctly.
They are mentioned in the doc, but it isn't clear that they are needed for the catalog display to work. That solved the problem.
This has got to be the dumbest question of the day, but I'm just not getting it.
I create a Quicklook, which shows just fine. When I hit the Done button, it just reappears. How do I intercept the Done button? Or more generally, control what is displayed in what I assume is a navbar. Here is the relevant code:
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
/*
* get the path to the pdf resource.
*/
NSString *path = [[NSBundle mainBundle] pathForResource:#"article" ofType:#"pdf"];
NSURL *docURL = [NSURL fileURLWithPath:path];
/*
* create the Quicklook controller.
*/
QLPreviewController *qlController = [[QLPreviewController alloc] init];
PreviewItem *item = [[PreviewItem alloc] initPreviewURL:docURL WithTitle:#"Article"];
self.pdfDatasource = [[PDFDataSource alloc] initWithPreviewItem:item];
qlController.dataSource = self.pdfDatasource;
/*
* present the document.
*/
[self presentViewController:qlController animated:YES completion:nil];
}
I assume I am missing something obvious.
Thank you,
Ken
Did you tried Taking your ViewDidAppear code To ViewDidLoad ? As when you click on done button all the views of the Controller are being Loaded again except ViewDidLoad. So the quicklook view Appears again. Just Try
The trick was to roll everything back into the original viewcontroller. That way when I hit the done button it goes back to the original viewcontroller, which is exactly what I wanted. so instead of having a separate class, I just incorporated the calls right into my main viewcontroller. I suspect that there is still a way to do with a cunning use of delegates, but in case anyone else is having the same issue, this was a solution that worked for me.
Thank you for your attention and help.
Ken
I am trying to implement a feature in iOS project that when you select a piece of text and highlight it you can then choose from the menu options to use another app like the default dictionary. Is it possible to do this? If so where can I find such documentation or tutorials?
You are describing the iOS menu. Look at the documentation on classes such as UIMenu, UIMenuItem, and UIMenuController.
I've found a solution to my problem.
Thanks to the author of this article:
http://blog.studiovillegas.com/2014/02/06/ios-uipasteboard-uimenucontroller-and-uimenuitem/
To add a custom menu item on to the default menu controller.
ViewController.h
- (void)longPressGestureRecognizer:(UIGestureRecognizer *)recognizer
{
UIMenuItem *mi = [self.label menuItemOpenPleco];
UIMenuController *menuController = [UIMenuController sharedMenuController];
menuController.menuItems = #[mi];
}
PasteboardLabel {h,m}
#interface PasteboardLabel : UILabel
- (UIMenuItem *)menuItemOpenPleco;
#end
#implementation PasteboardLabel
- (UIMenuItem *)menuItemOpenPleco
{
return [[UIMenuItem alloc] initWithTitle:#"Open Pleco" action:#selector(openPleco:)];
}
- (void)openPleco:(id)sender
{
NSString *selectedText = [self textInRange:[self selectedTextRange]];
UIPasteboard *pb = [UIPasteboard generalPasteboard];
pb.string = selectedText;
NSString *urlString = [NSString stringWithFormat:#"plecoapi://x-callback-url/q?s=%#", pb.string];
NSURL *url = [[NSURL alloc] initWithString:urlString];
[[UIApplication sharedApplication] openURL:url];
}
#end
I've found that there's a dearth of examples of adding custom menu items, or explanations of how they work. So I wanted to resolve that by sharing a few important tidbits then showing an example.
The UIMenuController "talks" with UIViews, not with UIViewControllers. This means that your UIMenuController related code needs to go into subclasses of UIView rather than a UIViewController.
Notice the word The at the start of my prior example. There's only one UIMenuController, a singleton which is shared from when your application first starts until it ends. This means that you should only add your item once, and that you shouldn't be writing over the existing array of items.
The appearance of the button in the UIMenu is based on whether or not the UIView that was tapped responds to the selector. This means you need to implement the method if you want the button to appear, and that you don't need to worry about it appearing when unrelated views are tapped unless you pick a selector name for which other UIViews also have methods.
So, having said all that, I made a subclass of a UITextView (which means its a subclass of UIView per my first bullet) and then I gave it this initialize method, along with an implementation for my selector.
+ (void)initialize {
static dispatch_once_t addInsert;
dispatch_once(&addInsert, ^{
UIMenuController *mController = [UIMenuController sharedMenuController];
UIMenuItem *insert = [[UIMenuItem alloc] initWithTitle:#"Insert..."
action:#selector(insert:)];
mController.menuItems = [mController.menuItems arrayByAddingObject:insert];
});
}
- (void)insert:(id)sender {
NSLog(#"Insert... pressed!");
}
The important points above here:
It's in the class initialize method, which is called by the runtime before the first time any other method in your class is invoked. In practice means the code is handled just before the first time an instance of your custom view will be appearing on screen.
I added a dispatch_once guard around it. If my class is subclassed, it's possible that those subclasses will call this initialize method. Maybe those subclasses show up before this one does, so I don't want to prevent the initialize method from running then. I just want to prevent it from running multiple times. Thus why I wrapped the code in a dispatch_once.
I didn't just set the menuItems to a new array of items - I assigned it to a new array of items that extended the existing array of items with my new item.
Hope you find all of that helpful. It's not very complicated, and you can certainly go about implementing my second point in other ways - I tried to pick a way that seemed safest to me, but there are certainly simpler ways of doing it.
I have a UIActivity subclass that creates its own activityViewController:
- (UIViewController *)activityViewController {
WSLInProgressViewController* progressView = [[[WSLInProgressViewController alloc] init] autorelease];
progressView.message = [NSString stringWithFormat:NSLocalizedString(#"Posting to %#...",#"Posting to..."),
self.activityType];
return progressView;
}
I've add a full repro on GitHub.
According to the documentation, you aren't supposed to dismiss this manually. Instead, the OS does that when you call activityDidFinish:. This works fine when ran on an iPhone.
When I say "works," this is the sequence of events that I'm expecting (and see on the iPhone):
Display the UIActivityViewController
User presses my custom activity
My view controller appears
I call activityDidFinish:
My custom view controller is dismissed
The UIActivityViewController is also dismissed
However, when I run this same code on the iPad Simulator -- the only difference being that I put the UIActivityViewController in a popup, as the documentation says you should -- the activityViewController never dismisses.
As I say, this is code wo/the popUP works on the iPhone and I have stepped through the code so I know that activityDidFinish: is getting called.
I found this Radar talking about the same problem in iOS6 beta 3, but it seems such fundamental functionality that I suspect a bug in my code rather than OS (also note that it works correctly with the Twitter and Facebook functionality!).
Am I missing something? Do I need to do something special in the activityViewController when it's run in a UIPopoverViewController? Is the "flow" supposed to be different on the iPad?
The automatic dismissal only appears to happen when your 'activity' controller is directly presented, not wrapped in anything. So just before showing the popup it's wrapped in, add a completion handler
activity.completionHandler = ^(NSString *activityType, BOOL completed){
[self.popup dismissPopoverAnimated:YES];
};
and you'll be good.
I see the question is quite old, but we've been debugging the same view-controller-not-dismissing issue here and I hope my answer will provide some additional details and a better solution than calling up -dismissPopoverAnimated: manually.
The documentation on the UIActivity is quite sparse and while it hints on the way an implementation should be structured, the question shows it's not so obvious as it could be.
The first thing you should notice is the documentation states you should not be dismissing the view controller manually in anyway. This actually holds true.
What the documentation doesn't say, and what comes as an observable thing when you come across debugging the non-dissmissing-view-controller issue, is iOS will call your -activityViewController method when it needs a reference to the subject view controller. As it turns out, probably only on iPad, iOS doesn't actually store the returned view controller instance anywhere in it's structures and then, when it wants to dismiss the view controller, it merely asks your -activityViewController for the object and then dismisses it. The view controller instantiated in the first call to the method (when it was shown) is thus never dismissed. Ouch. This is the cause of the issue.
How do we properly fix this?
Skimming the UIActivity docs further one may stumble accross the -prepareWithActivityItems: method. The particular hint lies along the following text:
If the implementation of your service requires displaying additional UI to the user, you can use this method to prepare your view controller object and make it available from the activityViewController method.
So, the idea is to instantiate your view controller in the -prepareWithActivityItems: method and tackle it into an instance variable. Then merely return the same instance from your -activityViewController method.
Given this, the view controller will be properly hidden after you call the -activityDidFinish: method w/o any further manual intervention.
Bingo.
NB! Digging this a bit further, the -prepareWithActivityItems: should not instantiate a new view controller each time it's called. If you have previously created one, you should merely re-use it. In our case it happily crashed if we didn't.
I hope this helps someone. :)
I had the same problem. It solved for me saving activityViewController as member and return stored controller. Activity return new object and dismiss invoked on new one.
- (UIViewController *)activityViewController {
if (!self.detaisController) {
// create detailsController
}
return self.detailsController;
}
I pass through the UIActivity to another view then call the following...
[myActivity activityDidFinish:YES];
This works on my device as well as in the simulator. Make sure you're not overriding the activityDidFinish method in your UIActivity .m file as I was doing previously. You can see the code i'm using here.
a workaround is to ask the calling ViewController to perform segue to your destination ViewController via - (void)performActivity although Apple does not recommend to do so.
For example:
- (void)performActivity
{
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad)
{
[self.delegate performSomething]; // (delegate is the calling VC)
[self activityDidFinish: YES];
}
}
- (UIViewController *)activityViewController
{
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone)
{
UIViewController* vc=XXX;
return vc;
}
else
{
return nil;
}
}
Do you use storyboards? Maybe in your iPad storyboard, the UIActivityIndicatorView doesn't have a check on "Hides When Stopped"?
Hope it helps!
So I had the same problem, I had a custom UIActivity with a custom activityViewController and when it was presented modally it would not dismiss not matter what I tried. The work around I choose to go with so that the experience remained the same to the user was to still use a custom UIActivity but give that activity a delegate. So in my UIActiviy subclass I have the following:
- (void)performActivity
{
if ([self.delegate respondsToSelector:#selector(showViewController)]) {
[self.delegate showViewController];
}
[self activityDidFinish:YES];
}
- (UIViewController *)activityViewController
{
return nil;
}
Then I make the view controller that shows the UIActivityViewController the delegate and it shows the view controller that you would otherwise show in activityViewController in the delegate method.
what about releasing at the end? Using non-arc project!
[progressView release];
Many Users have the same problem as u do! Another solution is:
UIActivityIndicatorView *progress= [[UIActivityIndicatorView alloc] initWithFrame:CGRectMake(125, 50, 30, 30)];
progress.activityIndicatorViewStyle = UIActivityIndicatorViewStyleWhiteLarge;
[alert addSubview:progress];
[progress startAnimating];
If you are using storyboard be sure that when u click on the activityind. "Hides When Stopped" is clicked!
Hope that helped...
I am trying to display a file using QLPreviewController. The QL view displays correctly (is pushed on top of my Navigation Controller) but the content is blank. However, no errors are displayed and application doesn't crash.
Checks on existence of file return true. (A proof is that if I use [self.docInteractionController presentPreviewAnimated:YES]; where docInteractionController is a UIDocumentInteractionController the file is correctly shown).
The code is taken directly from Apple sample code DocInteraction.
previewController.dataSource = self;
previewController.delegate = self;
// start previewing the document at the current section index
previewController.currentPreviewItemIndex = 0; //I want the first (and only) document
[[self navigationController] pushViewController:previewController animated:YES];
[previewController release];
The current view is a QLPreviewControllerDataSource, QLPreviewControllerDelegate,, and the delegate methods are as follow:
- (NSInteger) numberOfPreviewItemsInPreviewController: (QLPreviewController *) controller
{
return self.documentURLs.count;
}
- (id)previewController:(QLPreviewController *)previewController previewItemAtIndex: (NSInteger)index
{
return [self.documentURLs objectAtIndex:index];
}
documentURLs is a NSArray that contains the fileURLs of the documents. The same fileURL passed to the UIDocumentInteractionController displays correctly. I don't necessarily have to use QuickLook, I may just rely on UIDocumentInteractionController, however the fact that it's not working is really annoying.
Thank you in advance
Giovanni
Make a sample that demoes the issue. If you find that it still occurs on iOS 7, pls file a bug report.
I reported a bug on this class (pass nil URL to get loading indicator) and it got fixed within 2 weeks.