UIGestureRecognizer - Get the reference to the touched UIViewController Instead of its View? - objective-c

How do I get a reference to the UIViewController of a touched view?
I am using a UIPanGestureRecognizer on the view of a UIViewController. Here's how I initialize it:
TaskUIViewController *thisTaskController = [[TaskUIViewController alloc]init];
[[self view]addSubview:[thisTaskController view]];
UIPanGestureRecognizer *panRec = [[UIPanGestureRecognizer alloc]initWithTarget:self action:#selector(handlePan:)];
[[thisTaskController view] addGestureRecognizer:panRec];
In the tiggered action triggered using the gesture recognizer I am able to get the view from the parameter using recognizer.view
- (IBAction)handlePan:(UIPanGestureRecognizer *)recognizer {
UIView *touchedView = [[UIView alloc]init];
touchedView = (UIView*)[recognizer view];
...
}
However what I really need is the underlying UIViewController of the view touched. How can I get a reference to the UIViewController that contains this view instead of only the UIView?

I would say that it is more a design issue than just getting a reference. So I would follow several simple advises:
Owner should catch events from its view. I.e. TaskUIViewController sould be a target to UIPanGestureRecognizer which you added to its view.
If a controller has a sub-controller and waits from its sub-controller some responses - implement this as delegate.
You have memory leak in your "handlePan:" method.
Here is a skeleton to solve your issue:
#protocol CallbackFromMySubcontroller <NSObject>
- (void)calbackFromTaskUIViewControllerOnPanGesture:(UIViewController*)fromController;
#end
#interface OwnerController : UIViewController <CallbackFromMySubcontroller>
#end
#implementation OwnerController
- (id)init
{
...
TaskUIViewController *thisTaskController = [[TaskUIViewController alloc] init];
...
}
- (void)viewDidLoad
{
...
[self.view addSubview:thisTaskController.view];
...
}
- (void)calbackFromTaskUIViewControllerOnPanGesture:(UIViewController*)fromController
{
NSLog(#"Yahoo. I got an event from my subController's view");
}
#end
#interface TaskUIViewController : UIViewController {
id <CallbackFromMySubcontroller> delegate;
}
#end
#implementation TaskUIViewController
- (id)initWithOwner:(id<CallbackFromMySubcontroller>)owner
{
...
delegate = owner;
...
}
- (void)viewDidLoad
{
UIPanGestureRecognizer *panRec = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(handlePan:)];
[self.view addGestureRecognizer:panRec];
[panRec release];
}
- (void)handlePan:(UIPanGestureRecognizer *)recognizer {
...
[delegate calbackFromTaskUIViewControllerOnPanGesture:self];
...
}
#end

[touchedView nextResponder] will return the UIViewController object that manages touchedView (if it has one) or touchedView's superview (if it doesn’t have a UIViewController object that manages it).
For more information, see the UIResponder Class Reference. (UIViewController and UIView are subclasses of UIResponder.)
In your case, since you happen to know that touchedView is your viewController's view (and not, for instance, a subview of your viewController's view), you can just use:
TaskUIViewController *touchedController = (TaskUIViewController *)[touchedView nextResponder];
In the more general case, you could work up the responder chain until you find an object of kind UIViewController:
id aNextResponder = [touchedView nextResponder];
while (aNextResponder != nil)
{
if ([aNextResponder isKindOfClass:[UIViewController class]])
{
// we have found the viewController that manages touchedView,
// so we break out of the while loop:
break;
}
else
{
// we have yet to find the managing viewController,
// so we examine the next responder in the responder chain
aNextResponder = [aNextResponder nextResponder];
}
}
// outside the while loop. at this point aNextResponder points to
// touchedView's managing viewController (or nil if it doesn't have one).
UIViewController *eureka = (UIViewController *)aNextResponder;

Related

Mac OSX Storyboard : communicate between NSViewController

I use storyboard in a OS X cocoa application project with a SplitView controller and 2 others view controller LeftViewController and RightViewController.
In the LeftViewController i have a tableView that display an array of name. The datasource and delegate of the tableview is the LeftViewController.
In the RightViewController i just have a centered label that display the select name. I want to display in the right view the name selected in the left view.
To configure the communication between the 2 views controllers i use the AppDelegate and i define 2 property for each controller in AppDelegate.h
The 2 property are initialized in the viewDidLoad of view controller using the NSInvocation bellow :
#implementation RightViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
id delg = [[NSApplication sharedApplication] delegate];
SEL sel1 = NSSelectorFromString(#"setRightViewController:");
NSMethodSignature * mySignature1 = [delg methodSignatureForSelector:sel1];
NSInvocation * myInvocation1 = [NSInvocation
invocationWithMethodSignature:mySignature1];
id me = self;
[myInvocation1 setTarget:delg];
[myInvocation1 setSelector:sel1];
[myInvocation1 setArgument:&me atIndex:2];
[myInvocation1 invoke];
}
I have the same in LeftViewController.
Then if i click on a name in the table view, i send a message to the delegate with the name in parameter and the delegate update the label of the RightViewController with the given name. It works fine but according to apple best practice it’s not good.
Is there another way to communicate between 2 view controller inside a storyboard ?
I've already read a lot of post but found nothing for OS X.
You can download the simple project here : http://we.tl/4rAl9HHIf1
This is more advanced topic of app architecture (how to pass data).
Dirty quick solution: post NSNotification together with forgotten representedObject:
All NSViewControllers have a nice property of type id called representedObject. This is one of the ways how to pass data onto NSViewController. Bind your label to this property. For this simple example we will set representedObject some NSString instance. You can use complex object structure as well. Someone can explain in comments why storyboards stopped to show representedObject (Type safety in swift?)
Next we add notification observer and set represented object in handler.
#implementation RightViewController
- (void)viewDidLoad
{
[super viewDidLoad];
[[NSNotificationCenter defaultCenter] addObserverForName:#"SelectionDidChange" object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification * _Nonnull note) {
//[note object] contains our NSString instance
[self setRepresentedObject:[note object]];
}];
}
#end
Left view controller and its table:
Once selection changes we post a notification with our string.
#interface RightViewController () <NSTableViewDelegate, NSTableViewDataSource>
#end
#implementation RightViewController
- (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView
{
return [[self names] count];
}
- (nullable id)tableView:(NSTableView *)tableView objectValueForTableColumn:(nullable NSTableColumn *)tableColumn row:(NSInteger)row
{
return [self names][row];
}
- (NSArray<NSString *>*)names
{
return #[#"Cony", #"Brown", #"James", #"Mark", #"Kris"];
}
- (void)tableViewSelectionDidChange:(NSNotification *)notification
{
NSTableView *tableView = [notification object];
NSInteger selectedRow = [tableView selectedRow];
if (selectedRow >= 0) {
NSString *name = [self names][selectedRow];
if (name) {
[[NSNotificationCenter defaultCenter] postNotificationName:#"SelectionDidChange" object:name];
}
}
}
PS: don't forget to hook tableview datasource and delegate in storyboard
Why is this solution dirty? Because once your app grows you will end up in notification hell. Also view controller as data owner? I prefer window controller/appdelegate to be Model owner.
Result:
AppDelegate as Model owner.
Our left view controller will get it's data from AppDelegate. It is important that AppDelegate controls the data flow and sets the data (not the view controller asking AppDelegate it's table content cause you will end up in data synchronization mess). We can do this again using representedObject. Once it's set we reload our table (there are more advanced solutions like NSArrayController and bindings). Don't forget to hook tableView in storyboard. We also modify tableview's delegate methos the tableViewSelectionDidChange to modify our model object (AppDelegate.selectedName)
#import "LeftViewController.h"
#import "AppDelegate.h"
#interface LeftViewController () <NSTableViewDelegate, NSTableViewDataSource>
#property (weak) IBOutlet NSTableView *tableView;
#end
#implementation LeftViewController
- (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView
{
return [[self representedObject] count];
}
- (nullable id)tableView:(NSTableView *)tableView objectValueForTableColumn:(nullable NSTableColumn *)tableColumn row:(NSInteger)row
{
return [self representedObject][row];
}
- (void)setRepresentedObject:(id)representedObject
{
[super setRepresentedObject:representedObject];
//we need to reload table contents once
[[self tableView] reloadData];
}
- (void)tableViewSelectionDidChange:(NSNotification *)notification
{
NSTableView *tableView = [notification object];
NSInteger selectedRow = [tableView selectedRow];
if (selectedRow >= 0) {
NSString *name = [self representedObject][selectedRow];
[(AppDelegate *)[NSApp delegate] setSelectedName:name];
} else {
[(AppDelegate *)[NSApp delegate] setSelectedName:nil];
}
}
In RightViewController we delete all code. Why? Cause we will use binding AppDelegate.selectedName <--> RightViewController.representedObject
#implementation RightViewController
#end
Finally AppDelegate. It needs to expose some properties. What is interesting is how do I get my hands on all my controllers? One way (best) is to instantiate our own window controller and remember it as property. The other way is to ask NSApp for it's windows (be careful here with multiwindow app). From there we just ask contentViewController and loop through childViewControllers. Once we have our controllers we just set/bind represented objects.
#interface AppDelegate : NSObject <NSApplicationDelegate>
#property (nonatomic) NSString *selectedName;
#property (nonatomic) NSMutableArray <NSString *>*names;
#end
#import "AppDelegate.h"
#import "RightViewController.h"
#import "LeftViewController.h"
#interface AppDelegate () {
}
#property (weak, nonatomic) RightViewController *rightSplitViewController;
#property (weak, nonatomic) LeftViewController *leftSplitViewController;
#property (strong, nonatomic) NSWindowController *windowController;
#end
#implementation AppDelegate
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
_names = [#[#"Cony", #"Brown", #"James", #"Mark", #"Kris"] mutableCopy];
_selectedName = nil;
NSStoryboard *storyboard = [NSStoryboard storyboardWithName:#"Main"
bundle:[NSBundle mainBundle]];
NSWindowController *windowController = [storyboard instantiateControllerWithIdentifier:#"windowWC"];
[self setWindowController:windowController];
[[self windowController] showWindow:nil];
[[self leftSplitViewController] setRepresentedObject:[self names]];
[[self rightSplitViewController] bind:#"representedObject" toObject:self withKeyPath:#"selectedName" options:nil];
}
- (RightViewController *)rightSplitViewController
{
if (!_rightSplitViewController) {
NSArray<NSViewController *>*vcs = [[[self window] contentViewController] childViewControllers];
for (NSViewController *vc in vcs) {
if ([vc isKindOfClass:[RightViewController class]]) {
_rightSplitViewController = (RightViewController *)vc;
break;
}
}
}
return _rightSplitViewController;
}
- (LeftViewController *)leftSplitViewController
{
if (!_leftSplitViewController) {
NSArray<NSViewController *>*vcs = [[[self window] contentViewController] childViewControllers];
for (NSViewController *vc in vcs) {
if ([vc isKindOfClass:[LeftViewController class]]) {
_leftSplitViewController = (LeftViewController *)vc;
break;
}
}
}
return _leftSplitViewController;
}
- (NSWindow *)window
{
return [[self windowController] window];
}
//VALID SOLUTION IF YOU DON'T INSTANTIATE STORYBOARD
//- (NSWindow *)window
//{
// return [[NSApp windows] firstObject];
//}
#end
Result: works exactly the same
PS: If you instantiate own window Controller don't forget to delete initial controller from Storyboard
Why is this better? Cause all changes goes to model and models sends triggers to redraw views. Also you will end up in smaller view controllers.
What can be done more? NSObjectController is the best glue between your model objects and views. It also prevents retain cycle that sometimes can happen with bindings (more advanced topic). NSArrayController and so on...
Caveats: not a solution for XIBs
I managed to get what i want by adding the following code in AppDelegate.m :
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
// Insert code here to initialize your application
//
NSStoryboard *storyboard = [NSStoryboard storyboardWithName:#"Main"
bundle:[NSBundle mainBundle]];
self.windowController = [storyboard instantiateControllerWithIdentifier:#"windowController"];
self.window = self.windowController.window;
self.splitViewController = (NSSplitViewController*)self.windowController.contentViewController;
NSSplitViewItem *item0 = [self.splitViewController.splitViewItems objectAtIndex:0];
NSSplitViewItem *item1 = [self.splitViewController.splitViewItems objectAtIndex:1];
self.leftViewController = (OMNLeftViewController*)item0.viewController;
self.rightViewController = (OMNRightViewController*)item1.viewController;
[self.window makeKeyAndOrderFront:self];
[self.windowController showWindow:nil];
}
We also need to edit the storyboard NSWindowController object as follow :
Uncheck the checkbox 'Is initial controller' because we add it programmatically in AppDelegate.m.
Now the left and right view can communicate. Just define a property named rightView in OMNLeftViewController.h :
self.leftViewController.rightView = self.rightViewController;

How can I redraw an image in an custom NSView for an NSStatusItem?

I am having some troubles understanding how to wire a custom NSView for an NSMenuItem to support both animation and dragging and dropping. I have the following subclass of NSView handling the bulk of the job. It draws my icon when the application launches correctly, but I have been unable to correctly setup the subview to change when I invoke the setIcon function from another caller. Is there some element of the design that I am missing?
TrayIconView.m
#import "TrayIconView.h"
#implementation TrayIconView
#synthesize statusItem;
static NSImageView *_imageView;
- (id)initWithFrame:(NSRect)frame
{
self = [super initWithFrame:frame];
if (self) {
statusItem = nil;
isMenuVisible = NO;
_imageView = [[NSImageView alloc] initWithFrame:[self bounds]];
[self addSubview:_imageView];
}
return self;
}
- (void)drawRect:(NSRect)dirtyRect
{
// Draw status bar background, highlighted if menu is showing
[statusItem drawStatusBarBackgroundInRect:[self bounds]
withHighlight:isMenuVisible];
}
- (void)mouseDown:(NSEvent *)event {
[[self menu] setDelegate:self];
[statusItem popUpStatusItemMenu:[self menu]];
[self setNeedsDisplay:YES];
}
- (void)rightMouseDown:(NSEvent *)event {
// Treat right-click just like left-click
[self mouseDown:event];
}
- (void)menuWillOpen:(NSMenu *)menu {
isMenuVisible = YES;
[self setNeedsDisplay:YES];
}
- (void)menuDidClose:(NSMenu *)menu {
isMenuVisible = NO;
[menu setDelegate:nil];
[self setNeedsDisplay:YES];
}
- (void)setIcon:(NSImage *)icon {
[_imageView setImage:icon];
}
TrayIconView.h
#import <Cocoa/Cocoa.h>
#interface TrayIconView : NSView
{
BOOL isMenuVisible;
}
#property (retain, nonatomic) NSStatusItem *statusItem;
- (void)setIcon:(NSImage *)icon;
#end
The solution to this problem was actually outside of the view detailed here. The caller of the interface was being double instantiated on accident, thus nulling out the reference to the previously created NSView. After correcting that concern the app draws and works just fine.
With regard to dragging, I just implemented a subclass of NSView that implemented the Cocoa draggable protocol and added it as a subview to this parent class. That allows dragging onto the currently established NSRect that contains the menubar icon.

UIScrollView implementing a delegate

I have a view controller that has three scroll views as subviews. I would like to have the child scrolling views inform the parent view controller whenever the page has changed.
I thought I would do this by setting up a delegation pattern. My child (a subclass of UIScrollView) sets it up in the header file:
#interface TriptychScrollView : UIScrollView <UIScrollViewDelegate> {
id delegate;
}
- (id)delegate;
- (void)setDelegate:(id)newDelegate;
#end
#protocol RCScrollDidChange
#optional
- (void)scrollView:(TriptychScrollView *)scrollView imageDidChange:(NSNumber *)index;
#end
The source file has the delegate accessor methods:
- (id)delegate {
return delegate;
}
- (void)setDelegate:(id)newDelegate {
delegate = newDelegate;
}
Unfortunately, this means my setting the delegate to self call is being ignored:
- (id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
[self setDelegate:self];
}
return self;
}
.. and therefore the two methods scrollViewDidScroll: and scrollViewDidEndDecelerating: are not getting called! This means I've effectively lost control of my scrolling views.
Am I doing this incorrectly? Or is there a better way for the child subviews to send messages back to their parents?
I may not understand the whole problem, but I dont see a reason for implementing your own subclasses of UIScrollView, and if you are insistent on doing so, dont shadow it's .delegate property or make sure and call super.
I would approach this by doing the following:
assuming all of these scrollviews are contained by some other view;
UIScrollView *a = ....
a.delegate = self;
a.tag = 1;
UIScrollView *b = ....
b.delegate = self;
b.tag = 2;
UIScrollView *c = ....
c.delegate = self;
c.tag = 3
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
if (scrollView.tag == 1)
//handle a
else if (scrollView.tag == 2)
//handle b
else if (scrollView.tag == 3)
//handle c
}

UINavigationController: Apply Common Padding/Margin to all Popped View Controller's Views

I have a UINavigationController and I would like the view of every view controller that is popped onto the stack to have a common padding/margin (e.g. 25 pixels on all sides). What is the best way to accomplish this?
I originally thought that I could implement UINavigationControllerDelegate and inside the navigationController:didShowViewController:animated or navigationController:willShowViewController:animated methods, simply change the frame of the view controller that was about to be displayed. This does not seem to have an effect though.
I tried to do the same thing inside the view controller's viewDidAppear and viewWillAppear methods, but this also did not work. Ideally, I don't want to put any logic in the controllers anyway, as they may not always be used inside a navigation controller.
One last idea that I haven't tried yet is to create a "wrapper" UIViewController that would actually get pushed onto this stack. This wrapper would add the real view controller's view as a subview with a frame that would provide the desired margin. The downside here is that I would need to subclass UINavigationController and override pushViewController:animated, where the wrapper would be initialized and pushed. Apple's documentation indicates that UINavigationController is not meant to be subclassed.
Thanks in advance.
I solved this by putting a "wrapper" UIView around the UIViewController's view instead of the UIViewController itself. The wrapper view then pads the subview by setting the subview's frame in the layoutSubviews method.
I've attached the code I used for convenience. To use, replace your UINavigationController with the PaddedNavigationController, and set the PaddedNavigationController's insets property.
PaddedNavigationController.h:
#import <Foundation/Foundation.h>
#interface PaddedNavigationController : UINavigationController
{
UIEdgeInsets _insets;
}
#property (nonatomic, assign) UIEdgeInsets insets;
#end
PaddedNavigationController.m:
#import "PaddedNavigationController.h"
#interface PaddedView : UIView
{
UIView *_view;
UIEdgeInsets _insets;
}
#property (nonatomic, assign) UIEdgeInsets insets;
+ (PaddedView *) wrapView:(UIView *)view withInsets:(UIEdgeInsets)insets;
- (id) initWithView:(UIView *)view insets:(UIEdgeInsets)insets;
#end
#implementation PaddedNavigationController
#synthesize insets = _insets;
- (void) pushViewController:(UIViewController *)viewController animated:(BOOL)animated
{
//check if the UIViewController's view has already been wrapped by the PaddedView; don't want to wrap it twice
if(![viewController.view isKindOfClass:[PaddedView class]])
{
viewController.view = [PaddedView wrapView:viewController.view withInsets:self.insets];
}
[super pushViewController:viewController animated:animated];
}
- (void) setInsets:(UIEdgeInsets)insets
{
_insets = insets;
//loop through this navigation controller's view controllers and set the new insets on any PaddedViews
for(UIViewController *viewController in self.viewControllers)
{
if([viewController.view isKindOfClass:[PaddedView class]])
{
PaddedView *padded = (PaddedView *)viewController.view;
padded.insets = insets;
}
}
}
#end
#implementation PaddedView
#synthesize insets = _insets;
+ (PaddedView *) wrapView:(UIView *)view withInsets:(UIEdgeInsets)insets
{
return [[[PaddedView alloc] initWithView:view insets:insets] autorelease];
}
- (id) initWithView:(UIView *)view insets:(UIEdgeInsets)insets
{
if(self = [super initWithFrame:view.frame])
{
_insets = insets;
self.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
_view = [view retain];
[self addSubview:view];
}
return self;
}
- (void) dealloc
{
[_view release];
[super dealloc];
}
- (void) layoutSubviews
{
//apply the insets to the subview
_view.frame = CGRectMake(self.insets.left, self.insets.top, self.frame.size.width - self.insets.left - self.insets.right, self.frame.size.height - self.insets.top - self.insets.bottom);
}
- (void) setInsets:(UIEdgeInsets)insets
{
_insets = insets;
//we need to re-layout the subviews as the insets have changed
[self layoutSubviews];
}
#end

Determine if a view is inside of a Popover view

We have common views that we use in our application in many locations inside of UINavigationControllers. Occasionally the UINavigationControllers are inside of popover views. Now the views we put into the nav controllers modify their navigation controller's toolbar buttons and, in some cases, use custom buttons that we've created. We need to be able to figure out from the UIViewcontroller itself if the view is inside of a popoverview so we can display the correctly colored buttons.
We can easily get the Navigation controller reference from the UIViewController, using UIViewController.navigationController, but there doesn't seem to be anything for finding a UIPopoverController.
Does anyone have any good ideas for how to do this?
Thanks!
As Artem said we have UIPopoverPresentationController since iOS8. To determine if view is in popover you can use its .arrowDirection property for example.
Check it in viewWillApear() of presented view controller:
// get it from parent NavigationController
UIPopoverPresentationController* popoverPresentationVC = self.parentViewController.popoverPresentationController;
if (UIPopoverArrowDirectionUnknown > popoverPresentationVC.arrowDirection) {
// presented as popover
} else {
// presented as modal view controller (on iPhone)
}
Here's another solution; define a protocol (e.g. PopoverSensitiveController) that has only one method:
#import "Foundation/Foundation.h"
#protocol PopoverSensitiveController
-(void) setIsInPopover:(BOOL) inPopover;
#end
A view controller that wants to know if it is in a popover then defines a property isInPopover; for example:
#import
#import "PopoverSensitiveController.h"
#pragma mark -
#pragma mark Interface
#interface MyViewController : UIViewController {
}
#pragma mark -
#pragma mark Properties
#property (nonatomic) BOOL isInPopover;
#pragma mark -
#pragma mark Instance Methods
...other stuff...
#end
Finally, in the splitView delegate (the assumption is that your app uses a split view controller):
#import "MySplitViewControllerDelegate.h"
#import "SubstitutableDetailViewController.h"
#import "PopoverSensitiveController.h"
#pragma mark -
#pragma mark Implementation
#implementation MySplitViewControllerDelegate
#pragma mark -
#pragma mark UISplitViewControllerDelegate protocol methods
-(void) splitViewController:(UISplitViewController *) splitViewController willHideViewController:(UIViewController *) aViewController withBarButtonItem:(UIBarButtonItem *) barButtonItem forPopoverController:(UIPopoverController *) pc {
// Keep references to the popover controller and the popover button, and tell the detail view controller to show the button
popoverController = [pc retain];
popoverButtonItem = [barButtonItem retain];
if ([[splitViewController.viewControllers objectAtIndex:1] respondsToSelector:#selector(showRootPopoverButtonItem:)]) {
UIViewController *detailViewController = [splitViewController.viewControllers objectAtIndex:1];
[detailViewController showRootPopoverButtonItem:barButtonItem];
}
if ([[splitViewController.viewControllers objectAtIndex:1] respondsToSelector:#selector(showRootPopoverButtonItem:)]) {
UIViewController *detailViewController = [splitViewController.viewControllers objectAtIndex:1];
[detailViewController showRootPopoverButtonItem:barButtonItem];
}
// If the view controller wants to know, tell it that it is a popover
if ([aViewController respondsToSelector:#selector(setIsInPopover:)]) {
[(id) aViewController setIsInPopover:YES];
}
// Make sure the proper view controller is in the popover controller and the size is as requested
popoverController.contentViewController = aViewController;
popoverController.popoverContentSize = aViewController.contentSizeForViewInPopover;
}
-(void) splitViewController:(UISplitViewController *) splitViewController willShowViewController:(UIViewController *) aViewController invalidatingBarButtonItem:(UIBarButtonItem *) barButtonItem {
// Tell the detail view controller to hide the button.
if ([[splitViewController.viewControllers objectAtIndex:1] respondsToSelector:#selector(invalidateRootPopoverButtonItem:)]) {
UIViewController *detailViewController = [splitViewController.viewControllers objectAtIndex:1];
[detailViewController invalidateRootPopoverButtonItem:barButtonItem];
}
// If the view controller wants to know, tell it that it is not in a popover anymore
if ([aViewController respondsToSelector:#selector(setIsInPopover:)]) {
[(id) aViewController setIsInPopover:NO];
}
// Now clear out everything
[popoverController release];
popoverController = nil;
[popoverButtonItem release];
popoverButtonItem = nil;
}
-(void) setPopoverButtonForSplitViewController:(UISplitViewController *) splitViewController {
// Deal with the popover button
UIViewController *detailViewController = [splitViewController.viewControllers objectAtIndex:1];
[detailViewController showRootPopoverButtonItem:popoverButtonItem];
// If the view controller wants to know, tell it that it is a popover (initialize the controller properly)
if ([[splitViewController.viewControllers objectAtIndex:0] respondsToSelector:#selector(setIsInPopover:)]) {
[(id) [splitViewController.viewControllers objectAtIndex:0] setIsInPopover:YES];
}
}
Then where ever in the view controller you want to know if you are in a popover, simply use the isInPopover property.
In iOS8 you can use popoverPresentationController property of UIViewController to check if it is contained in a popover presentation controller. From documentation, it returns: "The nearest ancestor in the view controller hierarchy that is a popover presentation controller. (read-only)"
I was recently looking for a way to determine wether or not a view was being displayed in a popover. This is what I came up with:
UIView *v=theViewInQuestion;
for (;v.superview != nil; v=v.superview) {
if (!strcmp(object_getClassName(v), "UIPopoverView")) {
NSLog(#"\n\n\nIM IN A POPOVER!\n\n\n\n");
}
Basically you climb the view's superview tree looking to see if any of its superviews is a UIPopoverView. The one caveat here is that the class UIPopoverView is an undocumented private class. I'm relying on the fact that the class name won't change in the future. YMMV.
In your case:
theViewInQuestion = theViewControllerInQuestion.view;
I'd be interested to see if anyone else comes up with a better solution.
Modification of the accepted answer for iOS5.1 and newer:
for (UIView *v = self.view; v.superview != nil; v=v.superview) {
if ([v isKindOfClass:[NSClassFromString(#"_UIPopoverView") class]]) {
NSLog(#"\n\n\nIM IN A POPOVER!\n\n\n\n");
}
}
** NOTE **
See comments about the reliability of this code.
My approach for this: (available with iOS 8 or greater)
- (BOOL)isContainedInPopover
{
UIPopoverPresentationController* popoverPresentationVC = self.parentViewController.popoverPresentationController;
return (popoverPresentationVC != nil);
}
Parent view controller will be the navigation controller which if inside a popover, will have a non-nil popoverPresentationController property.
By working with SpareTime's code I came to this, which works as expected. Nice code, nice solution:
Using the standard UISplitViewController example.
/* MasterViewController.h */
#import "UIPopoverViewDelegate.h"
#interface masterViewController : UITableViewController <UIPopoverViewDelegate>
#property (nonatomic) BOOL isInPopover;
#end
/* MasterViewController.m */
#import "MasterViewController.h"
#implementation MasterViewController
#synthesize isInPopover = _isInPopover;
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
if (self.isInPopover)
{
// Code for appearing in popover
}
else
{
// Code for not appearing in popover
}
}
#end
/* DetailViewController.h */
#import "UIPopoverViewDelegate.h"
#interface detailViewController : UIViewController <UISplitViewControllerDelegate>
#end
/* DetailViewController.m */
#import "DetailViewController.h"
#implementation detailViewController
- (void)splitViewController:(UISplitViewController *)splitController willHideViewController:(UIViewController *)viewController withBarButtonItem:(UIBarButtonItem *)barButtonItem forPopoverController:(UIPopoverController *)popoverController
{
/* This method is called when transitioning to PORTRAIT orientation. */
UIViewController *hiddenViewController = [(UINavigationController *)viewController childViewControllers].lastObject;
if ([hiddenViewController respondsToSelector:#selector(setIsInPopover:)])
[(id <UIPopoverViewDelegate>)hiddenViewController setIsInPopover:YES];
}
- (void)splitViewController:(UISplitViewController *)splitController willShowViewController:(UIViewController *)viewController invalidatingBarButtonItem:(UIBarButtonItem *)barButtonItem
{
/* This method is called when transitioning to LANDSCAPE orientation. */
UIViewController *shownViewController = [(UINavigationController *)viewController childViewControllers].lastObject;
if ([shownViewController respondsToSelector:#selector(setIsInPopover:)])
[(id <UIPopoverViewDelegate>)shownViewController setIsInPopover:NO];
}
#end
/* UIPopoverViewDelegate.h */
#protocol UIPopoverViewDelegate
#required
-(void)setIsInPopover:(BOOL)inPopover;
#end
In case that someone else is still looking for a solution i came up with one good enough for me.
Just override this method
func presentationController(_ controller: UIPresentationController, viewControllerForAdaptivePresentationStyle style: UIModalPresentationStyle) -> UIViewController? {
(controller.presentedViewController as? YourViewControler).isPopover = false
return controller.presentedViewController
}
Here is an example of YourViewController
class AdvisorHomeFilterViewController: UIViewController {
// MARK: - Properties
var isPopover = true
}
If it is popover it will not call 'viewControllerForAdaptivePresentationStyle' method and it will stay true, in case it is not popover it will set it to false.
I wanted to put up a button in the view if the view wasn't displayed in a popover. I know the width of the popover because I just set it. So i can test whether I'm on an iPad and if the width of the frame is the same as what I set.
- (void)viewWillAppear:(BOOL)animated {
[self setContentSizeForViewInPopover:CGSizeMake(400, 500)];
NSInteger frameWidth = self.view.frame.size.width;
//Let you go back to the game if on an iPod.
if ( ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) && !(frameWidth == 400) ) { ---code to display a button --}
All these 'Exact Classname Matching Approaches' are very prone to fail and break at even the slightest changes Apple will make. Also doing one-char-vars and cryptic for-loops is not exactly a solution fitting to my style.
I use followingpiece of code:
- (BOOL) isInPopOver {
UIView *currentView = self.view;
while( currentView ) {
NSString *classNameOfCurrentView = NSStringFromClass([currentView class]);
NSLog( #"CLASS-DETECTED: %#", classNameOfCurrentView );
NSString *searchString = #"UIPopoverView";
if( [classNameOfCurrentView rangeOfString:searchString options:NSCaseInsensitiveSearch].location != NSNotFound ) {
return YES;
}
currentView = currentView.superview;
}
return NO;
}
All the solutions above seems a little bit complicated. I'm using a variable called isInPopover which I set to true if the view controller is presented in a popover. In the view controller in popoverControllerDidDismissPopover or in viewWillDisappear I set the boolean value to false. It does work and is very simple.
Since self.popoverPresentationController is created lazily in most recent iOS versions, one should check for nil-ness of self.popoverPresentationController.presentingViewController, if not nil this would mean self is currently presented in a popover.
Swift 4 version (function can be added in extension UIViewController):
func isInPopover() -> Bool {
guard UIDevice.current.userInterfaceIdiom == .pad else { return false }
var checkingVC: UIViewController? = self
repeat {
if checkingVC?.modalPresentationStyle == .popover {
return true
}
checkingVC = checkingVC?.parent
} while checkingVC != nil
return false
}