QLPreviewController remove or add UIBarButtonItems
Asked Answered
C

5

17

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]];
Cankerworm answered 5/8, 2011 at 13:30 Comment(4)
RBFilePreviewer now supports the feature you are looking for without needing modification.Aedile
Is my answer sufficient to be accepted and for the bounty?Aedile
It is not really what I was looking for and i've got an other problem with the QLPreviewController now : #7038938 but I will except your answer because it was the best (and the only) one, and it helped me a bit.Cankerworm
cant under ios6, it is now a seperate app :: see #12675878Jose
A
17

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.

Aedile answered 8/8, 2011 at 18:46 Comment(7)
Excellent work! However, is it possible to change the whole navigationItem tinColor (or whole background color)? I was unable to achieve that :(Stubbs
I just added the ability to set the tint color of both the navigation bar and the toolbar. You just need to pull the latest version.You can actually set the tint color without my latest addition, but you have to do it differently depending on whether you push in on the navigation stack or if you present it modally. My addition makes the two situations identical.Aedile
Thanks, I take a look at that. Until now, I was able to implement that by subclassing QLPreviewController, overriding viewWillAppear method, traversing through self.view and searching for UINavigationBar view object, and then setting its tintColor :)Stubbs
I don't suppose this has been updated for iOS 6 at all has it? Apparently Apple changed the QLpreviewController navigation bar and where it's at or somethingFenn
No, it has not been updated for iOS 6. Hopefully I can find time soon to figure out any new quirks.Aedile
@Aedile cant under ios6, it is now a seperate app :: see #12675878Jose
It does not work for iOS 8 GM Seed. Some other suggestions please.Cuman
H
29

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

Highlight answered 6/9, 2013 at 13:0 Comment(9)
This needs to be selected as the answer and should be a lot higher! It is hacky, but since we can't directly customize the QLPreviewController's navigation bar, its the best answer and it works. I don't like the above answer which is saying, "hey download my broken code that isn't working for iOS 6 or 7."Ostler
It does not work for iOS 8 GM Seed. Some other suggestions please.Cuman
What exactly is your issue? Can you please elaborate? I just tested it and it worked fine for meHighlight
Thanks for sharing this Lukas. Absolutely this works, and so far (regardless of the subclassing-and-put-my-view-first-please) seems the only viable option. Well done.Minorite
Using QLPreviewController as a child view controller would always result in two navigation bars in iOS8. Using your method of finding the QL navigationBar I was able to use KVO to force it to always stay hidden. Allowing me to use the UINavigationController of my parent controller to customise the bar button items.Cramfull
I am using your code with iOS 8: "qlNavigationBar is the default navigationbar owned by the QLPreviewController". But i can not file the qlNavigationBar?Iceberg
@ZuzooVn do you mean you can't find the property? You need to define it in your header file.Tronna
one more bump. Clever solution, hackiness be damned :)Speaking
Really helpful. Thanks a lot.Ladyship
A
17

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.

Aedile answered 8/8, 2011 at 18:46 Comment(7)
Excellent work! However, is it possible to change the whole navigationItem tinColor (or whole background color)? I was unable to achieve that :(Stubbs
I just added the ability to set the tint color of both the navigation bar and the toolbar. You just need to pull the latest version.You can actually set the tint color without my latest addition, but you have to do it differently depending on whether you push in on the navigation stack or if you present it modally. My addition makes the two situations identical.Aedile
Thanks, I take a look at that. Until now, I was able to implement that by subclassing QLPreviewController, overriding viewWillAppear method, traversing through self.view and searching for UINavigationBar view object, and then setting its tintColor :)Stubbs
I don't suppose this has been updated for iOS 6 at all has it? Apparently Apple changed the QLpreviewController navigation bar and where it's at or somethingFenn
No, it has not been updated for iOS 6. Hopefully I can find time soon to figure out any new quirks.Aedile
@Aedile cant under ios6, it is now a seperate app :: see #12675878Jose
It does not work for iOS 8 GM Seed. Some other suggestions please.Cuman
B
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)
}
Boeke answered 22/7, 2015 at 15:20 Comment(0)
F
1

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.

Frankpledge answered 27/11, 2014 at 8:1 Comment(0)
F
1

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

}
Farriery answered 13/5, 2016 at 15:2 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.