QLPreviewController remove or add UIBarButtonItems
I have seen this kind of question a lot on the internet but it seems no one really knows the answer?
I am using QLPreviewController for displaying PDF documents. I first used a UIWebView but I was recommended to use QLPreviewController instead for performance reasons with bigger documents.
what I want is 4 custom UIBarButtonItem's in the top right (so where the print button is).
I managed to get a custom toolbar at the bottom, but that's not really what I want.
Considering that it is not possible to add custom button at the place of the print button, I still want to remove the printbutton and use the custom toolbar instead.
EDIT (Solution): I found the solution a while ago but didn't update this post so here is how I solved the problem:
I add al the buttons manually:
// Create a toolbar to have the buttons at the right side of the navigationBar
UIToolbar* toolbar = [[UIToolbar alloc] initWithFrame:CGRectMake(0, 0, 180, 44.01)];
[toolbar setTranslucent:YES];
// Create the array to hold the buttons, which then gets added to the toolbar
NSMutableArray* buttons = [[NSMutableArray alloc] initWithCapacity:4];
// Create button 1
button1 = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemSearch target:self action:@selector(button1Pressed)];
[buttons addObject:button1];
// Create button 2
button2 = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemCompose target:self action:@selector(button2Pressed)];
[buttons addObject:button2];
// Create button 3
button3 = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemBookmarks target:self action:@selector(button3Pressed)];
[buttons addObject:button3];
// Create a action button
openButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAction target:self action:@selector(openWith)];
[buttons addObject:openButton];
// insert the buttons in the toolbar
[toolbar setItems:buttons animated:NO];
// and put the toolbar in the navigation bar
[[self navigationItem] setRightBarButtonItem:[[UIBarButtonItem alloc] initWithCustomView:toolbar]];
Solution 1:
I searched for a solution to this problem for months and finally found a way to customize the navigationbar of a QLPreviewController. Previously I was also using UIWebView to display documents as I'm not allowed to display the iOS-share button for certain confidential documents within my app and this is what the QLPreviewController does. However I wanted to have those nice features such as the table of contents with the little previews and stuff. So I looked for a reliable way to get rid of this button. Like you guys I was first looking into customizing the navigationbar of the QLPreviewController. However, as others already pointed out this is absolutely not possible since iOS6. So instead of customizing the existing navigation bar what we need to do is creating an own one and placing it in front of the QL-navigationbar, thus hiding it.
So how to do this? First of all we need to subclass QLPreviewContoller and overwrite the viewDidAppear method and viewWillLayoutSubviews like this:
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
self.qlNavigationBar = [self getNavigationBarFromView:self.view];
self.overlayNavigationBar = [[UINavigationBar alloc] initWithFrame:[self navigationBarFrameForOrientation:[[UIApplication sharedApplication] statusBarOrientation]]];
self.overlayNavigationBar.autoresizingMask = UIViewAutoresizingFlexibleWidth;
[self.view addSubview:self.overlayNavigationBar];
NSAssert(self.qlNavigationBar, @"could not find navigation bar");
if (self.qlNavigationBar) {
[self.qlNavigationBar addObserver:self forKeyPath:@"hidden" options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld) context:nil];
}
// Now initialize your custom navigation bar with whatever items you like...
UINavigationItem *item = [[UINavigationItem alloc] initWithTitle:@"Your title goes here"];
UIBarButtonItem *doneButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone target:self action:@selector(doneButtonTapped:)];
item.leftBarButtonItem = doneButton;
item.hidesBackButton = YES;
[self.overlayNavigationBar pushNavigationItem:item animated:NO];
}
- (void)viewWillLayoutSubviews {
[super viewWillLayoutSubviews];
self.overlayNavigationBar.frame = [self navigationBarFrameForOrientation:[[UIApplication sharedApplication] statusBarOrientation]];
}
qlNavigationBar is the default navigationbar owned by the QLPreviewController, overlayNavigationBar is our custom one which will hide the default one. We also add a key-value observation to the default QL navigationbar to get notified when the default navigation bar gets hidden / reappears. In the viewWillLayoutSubviews method we take care of our custom navigationbar frame.
The next thing we should do is listen for visibility changes of the quicklook navigationbar:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
// Toggle visiblity of our custom navigation bar according to the ql navigationbar
self.overlayNavigationBar.hidden = self.qlNavigationBar.isHidden;
}
So now we need to implement methods we need to get the QL navigationbar and one that always gives us the current frame for our custom navigation bar:
- (UINavigationBar*)getNavigationBarFromView:(UIView *)view {
// Find the QL Navigationbar
for (UIView *v in view.subviews) {
if ([v isKindOfClass:[UINavigationBar class]]) {
return (UINavigationBar *)v;
} else {
UINavigationBar *navigationBar = [self getNavigationBarFromView:v];
if (navigationBar) {
return navigationBar;
}
}
}
return nil;
}
- (CGRect)navigationBarFrameForOrientation:(UIInterfaceOrientation)orientation {
// We cannot use the frame of qlNavigationBar as it changes position when hidden, also there seems to be a bug in iOS7 concerning qlNavigationBar height in landscape
return CGRectMake(0.0f, self.isIOS6 ? 20.0f : 0.0f, self.view.bounds.size.width, [self navigationBarHeight:orientation]);
}
- (CGFloat)navigationBarHeight:(UIInterfaceOrientation)orientation {
if([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone) {
if(UIInterfaceOrientationIsLandscape(orientation)) {
return self.isIOS6 ? 32.0f : 52.0f;
} else {
return self.isIOS6 ? 44.0f : 64.0f;
}
} else {
return self.isIOS6 ? 44.0f : 64.0f;
}
}
What else? Well of course you need to define properties, remove the observer in dealloc as well as define and set the iOS6 property (there are plenty of examples on the web...). Also you need to customize your navigationbar and listen to the button callbacks. That's it.
I know this is a bit hacky ... hiding / replacing the default QL action button by hiding it beneath another navigationbar ...but well at least it works reliable for me and you don't access private APIs etc.
I tested my solution on all available simulators for iOS 6.0 - 7.0 as well as on iPad 2 & 3, iPhone 4S & 5 (the latter with iOS 7.0 Beta 6 installed).
Solution 2:
Update:
This no longer works in iOS 6. Quick Look runs in another process using XPC. See here for more details. I don't foresee any way to customize QLPreviewController. The following answer remains for anyone interested for pre-iOS 6.
I answered an almost identical question the other day here. The question pertained to removing the print button, which isn't too hard. One thing to note about QLPreviewController
is that it's not meant to be customized. I have built a subclass of QLPreviewController
that can be customized. I've put it here on Github. It's designed to easily remove the action button, among other features too. It wouldn't take much effort at all to replace the button with a custom one.
The biggest thing to watch out for is that the action button is re-added to the navigation bar anytime a new document is displayed. You should notice this in my code. Anytime RBFilePreviewer
removes the action button, you just need to re-add your custom buttons. To add your custom buttons, you should create a UIBarButtonItem
that holds a custom view with four buttons in it. Then set the right bar button item as the custom UIBarButtonItem
you created.
Update:
I've updated RBFilePreviewer
to allow you to set a custom right bar button item right out-of-the-box. Just call -setRightBarButtonItem:
on RBFilePreviewer
and it just works.
Solution 3:
I took the response from Lukas Gross and applied it in Swift on iOS 8 and came up with this solution that worked for me:
NOTE: I have the QLPreviewController embedded in a UINavigationController!
Code:
var QLNavigationBar: UINavigationBar?
var overlayNavigationBar: UINavigationBar?
func getQLNavigationBar(fromView view: UIView) -> UINavigationBar? {
for v in view.subviews {
if v is UINavigationBar {
return v as? UINavigationBar
} else {
if let navigationBar = self.getQLNavigationBar(fromView: (v as! UIView)) {
return navigationBar
}
}
}
return nil
}
func handleNavigationBar() {
self.QLNavigationBar = self.getQLNavigationBar(fromView: self.navigationController!.view)
self.overlayNavigationBar = UINavigationBar(frame: CGRectMake(0, 0, self.view.bounds.size.width, 64.0))
self.overlayNavigationBar?.autoresizingMask = UIViewAutoresizing.FlexibleWidth
if let qln = self.QLNavigationBar {
qln.addObserver(self, forKeyPath: "hidden", options: (NSKeyValueObservingOptions.New | NSKeyValueObservingOptions.Old), context: nil)
qln.superview?.addSubview(self.overlayNavigationBar!)
}
var item = UINavigationItem(title: self.navigationItem.title!)
var doneBtn = UIBarButtonItem(barButtonSystemItem: .Done, target: self, action: "doneBtnPressed")
item.leftBarButtonItem = doneBtn
item.hidesBackButton = true
self.overlayNavigationBar?.pushNavigationItem(item, animated: false)
self.overlayNavigationBar?.tintColor = .whiteColor()
self.overlayNavigationBar?.barTintColor = .blackColor()
self.overlayNavigationBar?.titleTextAttributes = [
NSForegroundColorAttributeName : UIColor.whiteColor() ]
}
And applying this code like this:
override func observeValueForKeyPath(keyPath: String, ofObject object: AnyObject, change: [NSObject : AnyObject], context: UnsafeMutablePointer<Void>) {
if self.QLNavigationBar!.hidden {
self.overlayNavigationBar?.hidden = self.QLNavigationBar!.hidden
}
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(0.5 * Double(NSEC_PER_SEC))), dispatch_get_main_queue(), {
self.QLNavigationBar?.superview?.sendSubviewToBack(self.QLNavigationBar!)
if !self.QLNavigationBar!.hidden {
self.overlayNavigationBar?.hidden = self.QLNavigationBar!.hidden
}
})
}
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
self.handleNavigationBar()
}
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
self.overlayNavigationBar?.frame = CGRectMake(0, 0, self.view.bounds.size.width, 64.0)
}
Solution 4:
I understand that this answer is a little late for this. But I really do find a solution for this.
#import "UINavigationItem+Custome.h"
#import <QuickLook/QuickLook.h>
#import <objc/runtime.h>
@implementation UINavigationItem (Custome)
void MethodSwizzle(Class c, SEL origSEL, SEL overrideSEL);
- (void) override_setRightBarButtonItem:(UIBarButtonItem *)item animated:(BOOL)animated{
if (item && [item.target isKindOfClass:[QLPreviewController class]] && item.action == @selector(actionButtonTapped:)){
QLPreviewController* qlpc = (QLPreviewController*)item.target;
[self override_setRightBarButtonItem:qlpc.navigationItem.rightBarButtonItem animated: animated];
}else{
[self override_setRightBarButtonItem:item animated: animated];
}
}
+ (void)load {
MethodSwizzle(self, @selector(setRightBarButtonItem:animated:), @selector(override_setRightBarButtonItem:animated:));
}
void MethodSwizzle(Class c, SEL origSEL, SEL overrideSEL) {
Method origMethod = class_getInstanceMethod(c, origSEL);
Method overrideMethod = class_getInstanceMethod(c, overrideSEL);
if (class_addMethod(c, origSEL, method_getImplementation(overrideMethod), method_getTypeEncoding(overrideMethod))) {
class_replaceMethod(c, overrideSEL, method_getImplementation(origMethod), method_getTypeEncoding(origMethod));
}else{
method_exchangeImplementations(origMethod, overrideMethod);
}
}
@end
Add this as a Category and import this into your QLPreviewController's subclass and just call the
self.navigationItem.rightBarButtonItem = nil;//something you want
It works for me. I learn this from http://nshipster.com/method-swizzling/ and thoughts from http://codego.net/507056/
Good luck, guys.