16
votes

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]];
5
RBFilePreviewer now supports the feature you are looking for without needing modification.rbrown
Is my answer sufficient to be accepted and for the bounty?rbrown
It is not really what I was looking for and i've got an other problem with the QLPreviewController now : stackoverflow.com/questions/7038438/… but I will except your answer because it was the best (and the only) one, and it helped me a bit.Justin
cant under ios6, it is now a seperate app :: see stackoverflow.com/questions/12675378/…Daij-Djan

5 Answers

27
votes

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).

17
votes

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.

3
votes

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)
}
1
votes

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.

1
votes

By mixing a bit out of the existing answers/comments I was able to get this working for my use case: I needed to display files inside a UINavigationController and keep the ability of hiding/showing the UINavigationBar when the file content is tapped

Based on the answer from Lukas Gross and the comment from nacross here's what I ended up doing:

  1. Add a (subclass of) QLPreviewController as a child view controller. This will show two navigation bars: one for the main navigation controller and one from the QLPreviewController
  2. Set up a top constraint from the container view to the top layout guide (named containerTop in the code)
  3. Set this constraint to a negative value, equal to the UINavigationBar plus the status bar, so that the QLPreviewController's UINavigationBar remains hidden under the main UINavigationBar.
  4. Using KVO, monitor the hidden property of the UINavigationBar so that we can (1) hide/show our main UINavigationBar and (2) reset the top constraint

I ended up with something like this:

var qlNavigationBar: 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) {
                return navigationBar
            }
        }
    }

    return nil
}

func setObserverForNavigationBar() {

    self.qlNavigationBar = self.getQLNavigationBar(fromView: self.view)

    if let qln = self.qlNavigationBar {
        qln.addObserver(self, forKeyPath: "hidden", options: [NSKeyValueObservingOptions.New, NSKeyValueObservingOptions.Old], context: nil)
    }

}

override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) {

    self.navigationController?.setNavigationBarHidden(self.qlNavigationBar!.hidden, animated: true)

    self.containerTop.constant = self.qlNavigationBar!.hidden ? self.getStatusBarHeight() * -1 : self.getFullNavigationBarHeight() * -1

    UIView.animateWithDuration(0.5) {
        self.view.layoutIfNeeded()
    }

}


override func viewWillAppear(animated: Bool) {
    super.viewWillAppear(animated);

    self.setObserverForNavigationBar()

    self.containerTop.constant = self.getFullNavigationBarHeight() * -1

}

override func viewWillDisappear(animated: Bool) {
    super.viewWillDisappear(animated);

    if let qln = self.qlNavigationBar {
        qln.removeObserver(self, forKeyPath: "hidden")
    }

}

func getFullNavigationBarHeight() -> CGFloat {
    if let nav = self.navigationController {
        return nav.navigationBar.frame.origin.y + nav.navigationBar.frame.size.height
    }
    return 0
}

func getStatusBarHeight() -> CGFloat {
    return UIApplication.sharedApplication().statusBarFrame.size.height
}

The animations might need a little tweaking and it is hacky, but it's better than not having this possibility. It should be possible to adapt this strategy to other scenarios without the UINavigationController

Note: If you have a crash when implementing the container view for the QLPreviewController from a storyboard, subclass the QLPreviewController and implement the initializer:

class MyPreviewController: QLPreviewController {

    required init?(coder aDecoder: NSCoder) {
        super.init(nibName: nil, bundle: nil)
    }

}