How do I prevent UIPopoverController passthroughVi

2019-05-14 11:26发布

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?

2条回答
放我归山
2楼-- · 2019-05-14 11:59

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.

查看更多
淡お忘
3楼-- · 2019-05-14 12:19

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];
查看更多
登录 后发表回答