UISplitViewController delegate in a singleton - objective-c

I did a lot of research on UISplitView and was not able to find a way to control a Split View when the Master and the Detail has a view that changes.
Then I found a way to manage it with a singleton class that is the delegate.
My problem is that i'm not sure if it's the right way to go. I'm concerned about reusability and memory managment. Also I have a feeling that it's aginst Apple guidelines to make delegates in singletons.
This is what I have (and it's actually working):
// SharedSplitViewDelegate.h
/* In the detail view controllers:
// in the initial detail view controller
- (void)awakeFromNib
{
[super awakeFromNib];
// needs to be here, otherwise if it's booted in portrait the button is not set
self.splitViewController.delegate = [SharedSplitViewDelegate initSharedSplitViewDelegate];
}
// shared between all detail view controllers
- (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation
{
SharedSplitViewDelegate *rotationHandler = [SharedSplitViewDelegate initSharedSplitViewDelegate];
[self.toolbar setItems:[rotationHandler processButtonArray:self.toolbar.items] animated:YES];
}
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
{
return YES;
}
*/
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#interface SharedSplitViewDelegate : NSObject <UISplitViewControllerDelegate>
+ (id)initSharedSplitViewDelegate; // returns the singleton class instance
- (NSArray *)processButtonArray:(NSArray *)array; // Adds and removes the button from the toolbar array. Returns the modified array.
#end
Now the implementation:
// SharedSplitViewDelegate.m
#import "SharedSplitViewDelegate.h"
#interface SharedSplitViewDelegate()
#property (nonatomic, strong) UIBarButtonItem *button;
#property (nonatomic, strong) UIBarButtonItem *cachedButton;
#end
#implementation SharedSplitViewDelegate
#synthesize button = _button;
#synthesize cachedButton = _cachedButton;
#pragma mark - Singleton class definition
static id sharedSplitViewDelegate = nil;
+ (void)initialize
{
if (self == [SharedSplitViewDelegate class]) {
sharedSplitViewDelegate = [[self alloc] init];
}
}
+ (id)initSharedSplitViewDelegate {
return sharedSplitViewDelegate;
}
#pragma mark - Split view delegate methods
- (BOOL)splitViewController:(UISplitViewController *)svc
shouldHideViewController:(UIViewController *)vc
inOrientation:(UIInterfaceOrientation)orientation
{
if (orientation == UIInterfaceOrientationLandscapeLeft || orientation == UIInterfaceOrientationLandscapeRight) {
return NO;
} else {
return YES;
}
}
- (void)splitViewController:(UISplitViewController *)svc
willHideViewController:(UIViewController *)aViewController
withBarButtonItem:(UIBarButtonItem *)barButtonItem
forPopoverController:(UIPopoverController *)pc
{
barButtonItem.title = #"Browse";
self.button = barButtonItem;
}
- (void)splitViewController:(UISplitViewController *)svc
willShowViewController:(UIViewController *)aViewController
invalidatingBarButtonItem:(UIBarButtonItem *)barButtonItem
{
self.button = nil;
}
#pragma mark - Utility methods
- (void)setButton:(UIBarButtonItem *)button
{
if (button != _button) {
_button = button;
}
if (button != nil) {
self.cachedButton = button;
}
}
- (NSArray *)processButtonArray:(NSArray *)array
{
NSMutableArray *processedArray = [array mutableCopy];
if (self.button != nil && ![processedArray containsObject:self.button]) {
[processedArray insertObject:self.button atIndex:0];
} else if (self.button == nil && [processedArray containsObject:self.cachedButton]) {
[processedArray removeObjectAtIndex:0];
}
return [processedArray copy];
}
#end
This code is free to use and modify for everyone that would find it viable in their project :).
I'm new to StackOverflow (even though I've lurked for a couple months without an account) so every critique is warmly welcomed.

IMHO, every design pattern, architecture, is 'good' if it fits the 'problem' you have to solve (and fits your personal preferences for code organisation)
What's your problem ?
Why do you need this object ?
Could this singleton UISplitViewDelegate be your
UIApplicationDelegate ? (Keep it Simple ;-)
further discussion =>
If you UIApplicationDelegate is a mess, rather than creating sub-object, a scheme I've been using recently to organize my code : use categories and class extensions
Example :
If my ViewController class handles complex tasks whose code can be separated in groups.
let's say :
sound
core data
location-aware,
I create a category for each of these
UIViewController+soundManager
UIViewController+dataProvider
UIViewController+locationManager.
(in same file with several #interface #implementation, or in different files => i use several files)
Then along with each category I write a class-extension for properties this particular category needs.

Last time I solved this by subclassing the UISplitViewController and used it as his own delegate.

Related

Implement Delegate Method on NSTextField

I am attempting to implement a delegate method on NSTextField as described in this article from Apple. My goal is for the NSTextField to accept carriage returns and tabs. I have read elsewhere (including the linked article) that NSTextView is a better choice. However, I am working within a multiplatform framework that lacks support for NSTextView, and NSTextField will do the job if I can get it to accept carriage returns.
Based on the article, here is my code:
#interface MyTextFieldSubclass : NSTextField
{}
- (BOOL)control:(NSControl*)control textView:(NSTextView*)textView doCommandBySelector:(SEL)commandSelector;
#end
#implementation MyTextFieldSubclass
- (BOOL)control:(NSControl*)control textView:(NSTextView*)textView doCommandBySelector:(SEL)commandSelector
{
BOOL result = NO;
if (commandSelector == #selector(insertNewline:))
{
// new line action:
// always insert a line-break character and don’t cause the receiver to end editing
[textView insertNewlineIgnoringFieldEditor:self];
result = YES;
}
else if (commandSelector == #selector(insertTab:))
{
// tab action:
// always insert a tab character and don’t cause the receiver to end editing
[textView insertTabIgnoringFieldEditor:self];
result = YES;
}
return result;
}
#end
Additionally, in the Identity Inspector of the text field, I have changed the class name from the default NSTextField to my class name. However, when I run my program, the delegate method never gets called. Is there something else I have to do to set this up in Interface Builder?
There are a few parts of the documentation you linked which is pertinent that I think may have been neglected.
I've copied a few of the lines below:
Should you decide to keep using NSTextField, allowing the tab key and/or allowing enter and return keys for line-breaks can be achieved by implementing the following delegate method:
(BOOL)control:(NSControl*)control textView:(NSTextView*)textView doCommandBySelector:(SEL)commandSelector;
Note: When implementing this delegate method in your own object you should set your object up as the "delegate" for this NSTextField.
I've bolded a few of the callouts which I think might have been missed.
This method is within the NSControlTextEditingDelegate protocol within NSControl.h. As such it should be implemented by a class which implements the NSControlTextEditingDelegate (i.e. NSTextFieldDelegate)
One common way of doing this is to have the ViewController "holding" the NSTextField be the NSTextFieldDelegate.
Here's a very simple example using the sample code from Apple you linked:
ViewController.h
#import <Cocoa/Cocoa.h>
#interface ViewController : NSViewController <NSTextFieldDelegate>
#end
ViewController.m
#import "ViewController.h"
#implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
}
- (void)setRepresentedObject:(id)representedObject {
[super setRepresentedObject:representedObject];
// Update the view, if already loaded.
}
- (BOOL)control:(NSControl *)control textView:(NSTextView *)textView doCommandBySelector:(SEL)commandSelector {
BOOL result = NO;
if (commandSelector == #selector(insertNewline:))
{
// new line action:
// always insert a line-break character and don’t cause the receiver to end editing
[textView insertNewlineIgnoringFieldEditor:self];
result = YES;
}
else if (commandSelector == #selector(insertTab:))
{
// tab action:
// always insert a tab character and don’t cause the receiver to end editing
[textView insertTabIgnoringFieldEditor:self];
result = YES;
}
return result;
}
#end
Then set your NSTextField's delegate to the ViewController
No need to add a custom subclass.
Alternatively you could probably make the custom text field subclass its own delegate. Something along these lines:
#import "MyTextFieldSubclass.h"
#interface MyTextFieldSubclass() <NSTextFieldDelegate>
#end
#implementation MyTextFieldSubclass
- (instancetype)init {
self = [super init];
if (self) {
self.delegate = self;
}
return self;
}
- (instancetype)initWithCoder:(NSCoder *)coder {
self = [super initWithCoder:coder];
if (self) {
self.delegate = self;
}
return self;
}
- (instancetype)initWithFrame:(NSRect)frameRect {
self = [super initWithFrame:frameRect];
if (self) {
self.delegate = self;
}
return self;
}
- (void)drawRect:(NSRect)dirtyRect {
[super drawRect:dirtyRect];
// Drawing code here.
}
- (BOOL)control:(NSControl *)control textView:(NSTextView *)textView doCommandBySelector:(SEL)commandSelector {
BOOL result = NO;
if (commandSelector == #selector(insertNewline:))
{
// new line action:
// always insert a line-break character and don’t cause the receiver to end editing
[textView insertNewlineIgnoringFieldEditor:self];
result = YES;
}
else if (commandSelector == #selector(insertTab:))
{
// tab action:
// always insert a tab character and don’t cause the receiver to end editing
[textView insertTabIgnoringFieldEditor:self];
result = YES;
}
return result;
}
#end

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;

NSTabView blocking file-drop events active on underlying Window.

I have successfully implemented a file-drop functionality in my app. The Application window has a few NSTabView objects where dropping on them does not work. Anywhere else in the window the file-drop works fine.
I have tried to make the app delegate a delegate for the NSTabView, but this did not help.
Anyone have a setup for the NSTabView not to filter out the drop-actions so the whole window can be transparent to the file-drop actions ?
For a more generic solution than olekeh's I made it IB friendly so you can hook it up to any object that complies with the NSDraggingDestination protocol.
#import <Cocoa/Cocoa.h>
#interface DropFilesView : NSView
#property (nullable, assign) IBOutlet id<NSDraggingDestination> dropDelegate;
#end
#implementation DropFilesView
- (void)drawRect:(NSRect)dirtyRect {
[super drawRect:dirtyRect];
}
-(void) awakeFromNib {
[self registerForDraggedTypes:
[NSArray arrayWithObjects:NSFilenamesPboardType,
(NSString *)kPasteboardTypeFileURLPromise,kUTTypeData, NSURLPboardType, nil]]; //kUTTypeData
[super awakeFromNib];
}
-(NSDragOperation)draggingEntered:(id<NSDraggingInfo>)sender{
return [self.dropDelegate draggingEntered:sender];
}
- (BOOL)performDragOperation:(id < NSDraggingInfo >)sender {
return [self.dropDelegate performDragOperation:sender];
}
#end
I found the solution to this !! - I am posting it here for others who might need.
The NSTabView object has for each of its tabs an NSTabViwItem.
Under each of those, there is a regular NSView - that I subclassed with the following code: - The code assumes that you already have "draggingEntered" and "performDragOperation" in your AppDelegate as this class just forwards these messages to the app delegate. You will also need to put the declarations for those methods in you AppDelegate.h
// DropFilesView.h
#import <Cocoa/Cocoa.h>
#import "AppDelegate.h"
#interface DropFilesView : NSView
#end
and the implementation:
// DropFilesView.m
#import "DropFilesView.h"
#implementation DropFilesView
- (void)drawRect:(NSRect)dirtyRect {
[super drawRect:dirtyRect];
}
-(void) awakeFromNib {
[self registerForDraggedTypes:
[NSArray arrayWithObjects:NSFilenamesPboardType,
(NSString *)kPasteboardTypeFileURLPromise,kUTTypeData, NSURLPboardType, nil]]; //kUTTypeData
[super awakeFromNib];
}
-(NSDragOperation)draggingEntered:(id<NSDraggingInfo>)sender
{
AppDelegate* del = [AppDelegate sharedAppDelegate];
return [del draggingEntered:sender];
}
- (BOOL)performDragOperation:(id < NSDraggingInfo >)sender {
AppDelegate* del = [AppDelegate sharedAppDelegate];
return [del performDragOperation:sender];
}
#end
In Interfacebuilder, I set the new class for all the NSView objects covering areas where drop does not work, to this new one.
A similar approach can be used for NSImageView and the WebView classes. However, for the last one, do not use [super awakeFromNib] to prevent the default drag-and drop handling for the web view object.

Calling method from within another class?

I have a couple of views, one being the SettingsMenu the other being Game .
I have initialised backgroundMusic from within Game. When there is a change in SettingsMenu, I'd like it to run backgroundMusicStop in settingsMenu, however backgroundMusicStop is part of my Game class.
Game.h
-(void)backgroundMusicStop;
Game.m
-(void)backgroundMusicStop {
[backgroundMusic stop];
backgroundMusic.currentTime = 0;
}
SettingsMenu.m
-(IBAction)musicOptionSwitch:(id)sender {
if (backgroundMusicPlay == YES) {
backgroundMusicPlay = NO;
[Game backgroundMusicStop];
}
}
I've looked into it and I don't understand how to fix it, I know I need to make the method accessible from all classes, but I am confused on how to do that, any help would be greatly appreciated.
The problem is in this line of code:
[Game backgroundMusicStop];
In your implementation, Game is a class name and you try to call an instance method on a class. There are 2 options to fix it:
Create a new Game instance
Make backgroundMusicStop a class method
To create a Game instance you would need something like:
Game myGame = [[Game alloc]init];
//depending on your implementation it could be different
Or if you choose for second option you will have to change
-(void)backgroundMusicStop;
to
+(void)backgroundMusicStop;
I would also suggest you to find some book/website to understand class and instance variables/methods better and more in-depth.
I think the best architecture for what you're trying to do would be:
create one instance of a Settings object and have it keep track of the
current state of each option you want the user to control
update those values from inside your SettingsMenu
have your Game listen for changes to the Settings values
The advantage in this is that your view controllers only communicate through changes to the data model (Settings) rather than having to know about each other. Your Settings object can either be a singleton or a normal object that you get from your app delegate.
First create the settings data model:
// Settings.h
#import <Foundation/Foundation.h>
#interface Settings : NSObject
// A singleton so that the example stays simple
+ (instancetype)sharedSettings;
// YES/NO. Add other properties as needed.
#property (nonatomic, assign) BOOL musicShouldPlay;
#define MUSIC_SHOULD_PLAY #"musicShouldPlay"
#end
// Settings.m
#import "Settings.h"
#implementation Settings
// Guarantee only one instance
+ (instancetype)sharedSettings {
static dispatch_once_t onceToken;
static Settings *result = nil;
dispatch_once(&onceToken, ^{
result = [[Settings alloc] init];
});
return result;
}
- (instancetype)init {
self = [super init];
if (self) {
_musicShouldPlay = NO;
}
return self;
}
#end
The controller that makes the changes:
// SettingsViewController.m
#import "SettingsViewController.h"
#import "Settings.h"
#interface SettingsViewController ()
#property (strong, nonatomic) IBOutlet UISwitch *musicSwitch;
#end
#implementation SettingsViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Make the display consistent with previous settings
[self.musicSwitch setOn:[[Settings sharedSettings] musicShouldPlay]];
}
// I'm presenting this using a modal segue from a "Change settings" button on the main controller,
// so it has a "Done" button
- (IBAction)done:(id)sender {
[ self dismissViewControllerAnimated:YES completion:nil];
}
// Switch value changes are connected to this in the storyboard
- (IBAction)changePlayState:(id)sender {
[[Settings sharedSettings] setMusicShouldPlay:[self.musicSwitch isOn]];
}
#end
The controller that reacts to changes:
// ViewController.m
#import "ViewController.h"
#import "Settings.h"
#implementation ViewController
// Since this is the primary controller, I'll have it listen for its lifetime.
- (void)viewDidLoad {
[super viewDidLoad];
[[Settings sharedSettings] addObserver:self
forKeyPath:MUSIC_SHOULD_PLAY
options:NSKeyValueObservingOptionNew
context:nil];
}
- (void)dealloc {
[[Settings sharedSettings] removeObserver:self forKeyPath:MUSIC_SHOULD_PLAY];
}
// Here's where the KVO notifications are delivered.
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if ([keyPath isEqualToString:MUSIC_SHOULD_PLAY]) {
NSNumber *newValue = [change valueForKey:NSKeyValueChangeNewKey];
if ([newValue integerValue] == 1) {
NSLog(#"Request to turn music on");
} else {
NSLog(#"Request to turn music off");
}
}
}
#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
}