I have a UIPopoverController which is being presented from a UIBarButtonItem. I want touches outside of the popover to dismiss the popover. When presenting a popover from a bar button, the other bar buttons are automatically included in the popovers passthrough views. In order to prevent that I set the passthrough views to nil (or @[ ]) after presenting the popover, like so:
- (IBAction) consoleBarButtonHit:(id)sender {
UIViewController *consoleNavigationController=[self.storyboard instantiateViewControllerWithIdentifier:@"consoleNavigationController"];
_consolePopoverController=[[UIPopoverController alloc] initWithContentViewController:consoleNavigationController];
_consolePopoverController.delegate=self;
[_consolePopoverController presentPopoverFromBarButtonItem:sender permittedArrowDirections:UIPopoverArrowDirectionAny animated:YES];
// this must be done _after_ presenting the popover in order to work
_consolePopoverController.passthroughViews=nil;
}
That's all fine and good, but the problem that I'm having is that after rotating the device while the popover is visible the bar buttons are being automatically re-added as passthrough views and don't cause the popover to be dismissed.
If I could somehow get the bar buttons view (or rect) then I could present the popover using
-presentPopoverFromRect:inView:permittedArrowDirections:animated:
which would likely fix this problem, but I don't know of any non-hackish way of finding that rect/view from a UIBarButtonItem.
I really don't want the selectors called when the other bar buttons are hit to dismiss the popover programmatically, that's not their responsibility and will likely cause problems for me later.
Any ideas?
So I came up with a solution, that's a little odd, but keeps things modular, works well. I've created a class called PropertyEnforcer which registers itself as a KVO observer of an object's property and re-sets that property any time it changes.
PropertyEnforcer.h:
#import <Foundation/Foundation.h>
@interface PropertyEnforcer : NSObject
+ (void) enforceProperty:(NSString*)keyPath ofObject:(id)target toValue:(id)value;
@end
PropertyEnforcer.m:
#import "PropertyEnforcer.h"
#import <objc/runtime.h>
@interface PropertyEnforcer ()
@property (retain) NSString *keyPath;
@property (retain) id value;
@property (assign) id target;
@end
@implementation PropertyEnforcer
- (void) dealloc {
[_target removeObserver:self forKeyPath:_keyPath context:NULL];
[super dealloc];
}
- (void) observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if( (([_target valueForKey:_keyPath] == nil) && (_value==nil)) || [[_target valueForKey:_keyPath] isEqual:_value]) {
return;
} else {
[_target setValue:_value forKeyPath:_keyPath];
}
}
+ (void) enforceProperty:(NSString*)keyPath ofObject:(id)target toValue:(id)value {
PropertyEnforcer *enforcer=[[PropertyEnforcer alloc] init];
enforcer.value=value;
enforcer.keyPath=keyPath;
enforcer.target=target;
[target addObserver:enforcer forKeyPath:keyPath options:NSKeyValueObservingOptionInitial|NSKeyValueObservingOptionNew context:NULL];
objc_setAssociatedObject(target,
_cmd, // using this technique we can only attach one PropertyEnforcer per target
enforcer,
OBJC_ASSOCIATION_RETAIN);
[enforcer release];
}
@end
Now I can change the change the original code to:
- (IBAction) consoleBarButtonHit:(id)sender {
UIViewController *consoleNavigationController=[self.storyboard instantiateViewControllerWithIdentifier:@"consoleNavigationController"];
_consolePopoverController=[[UIPopoverController alloc] initWithContentViewController:consoleNavigationController];
_consolePopoverController.delegate=self;
[_consolePopoverController presentPopoverFromBarButtonItem:sender permittedArrowDirections:UIPopoverArrowDirectionAny animated:YES];
// make sure those passthroughViews are always nil !
[PropertyEnforcer enforceProperty:@"passthroughViews" ofObject:_consolePopoverController toValue:nil];
}
The PropertyEnforcer registers itself as an associated object so we don't ever have to keep track of it. It will automatically unregister itself as a KVO observer and be destroyed whenever the UIPopoverController is destroyed.
This is the best, least hackish, solution that I could come up with.
The solution I went for was to leave passthroughViews
alone and instead disable/re-enable individual buttons (UIBarButtonItem
instances) in a toolbar or a navigation bar when UIPopoverPresentationController
is presented and dismissed, based on its transition.
(iOS 8: UIPopoverPresentationController
instead of UIPopoverController
.)
UIPopoverPresentationController+managedBarButtonItems.h
@interface UIPopoverPresentationController (managedBarButtonItems)
@property (nonatomic, retain) NSArray* managedBarButtonItems;
@end
UIPopoverPresentationController+managedBarButtonItems.m
#import "UIPopoverPresentationController+managedBarButtonItems.h"
#import <objc/runtime.h>
//
// scope: private, in-terms-of
//
@interface UIBarButtonItem (wasEnabled)
@property (nonatomic) BOOL wasEnabled;
@end
@implementation UIBarButtonItem (wasEnabled)
- (BOOL)wasEnabled
{
return [objc_getAssociatedObject(self, @selector(wasEnabled)) boolValue];
}
- (void)setWasEnabled:(BOOL)wasIt
{
objc_setAssociatedObject(self, @selector(wasEnabled), [NSNumber numberWithBool:wasIt], OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
// FYI: "Associated objects are released [automatically] after the dealloc method of the original object has finished."
@end
//
// scope: consumable
//
@implementation UIPopoverPresentationController (managedBarButtonItems)
- (NSArray*)managedBarButtonItems
{
return objc_getAssociatedObject(self, @selector(managedBarButtonItems));
}
- (void)setManagedBarButtonItems:(NSArray*)items
{
objc_setAssociatedObject(self, @selector(managedBarButtonItems), items, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
// FYI: "Associated objects are released [automatically] after the dealloc method of the original object has finished."
- (void)presentationTransitionDidEnd:(BOOL)completed
{
[super presentationTransitionDidEnd:completed];
if (self.barButtonItem && self.managedBarButtonItems)
{
for (UIBarButtonItem* button in self.managedBarButtonItems)
{
if (button.action != /* actuator */ self.barButtonItem.action)
{
button.wasEnabled = button.enabled, button.enabled = NO;
}
}
}
}
- (void)dismissalTransitionDidEnd:(BOOL)completed
{
[super dismissalTransitionDidEnd:completed];
if (self.barButtonItem && self.managedBarButtonItems)
{
for (UIBarButtonItem* button in self.managedBarButtonItems)
{
if (button.action != /* actuator */ self.barButtonItem.action)
{
button.enabled = button.wasEnabled;
}
}
}
}
@end
Usage:
UIAlertController* actionSheet = [UIAlertController
alertControllerWithTitle:@"Actions" message:nil
preferredStyle:UIAlertControllerStyleActionSheet];
UIPopoverPresentationController* presenter = actionSheet.popoverPresentationController;
// chosen anchor UIBarButtonItem
presenter.barButtonItem = anchorButton;
// disabled UIViewController buttons
presenter.managedBarButtonItems = self.toolbarItems;
Also possible:
// disabled UINavigationController buttons
presenter.managedBarButtonItems =
[[NSArray arrayWithArray:self.navigationItem.leftBarButtonItems]
arrayByAddingObject:self.navigationItem.rightBarButtonItem];