Challenge: Proximity Notification - Who is the observer?


#1

So I solved the challenge, however I don’t think my solution was correct. I mean it works, but it just doesn’t seem to me what the authors of the book would do.

Basically I made the HeavyViewController the observer of the notification. I couldn’t figure out how to change the background color inside of HeavyRotationAppDelegate.

For the record I got everything else working inside of the HeavyRotationAppDelegate, I added the observer and got the notification inside of the HeavyRotationAppDelegate. I just couldn’t figure out how to change the background color. I couldn’t figure out how to reference the current view. Nothing I tried was working and I was getting frustrated, so I decided to move the observer and notification for the proximity sensor to the HeavyViewController. Inside there, I was able to successfully make the background color of the view change by using: [self.view setBackgroundColor:[UIColor darkGrayColor]]

So here’s what I added inside of HeavyViewController.m

- (void) viewWillAppear:(BOOL)animated
{
    [[UIDevice currentDevice] setProximityMonitoringEnabled:YES];
    [[NSNotificationCenter defaultCenter] addObserver:self 
                                             selector:@selector(proximityChanged:) 
                                                 name:UIDeviceProximityStateDidChangeNotification
                                               object:nil];
    [super viewWillAppear];
    
}

- (void)proximityChanged:(NSNotification *)note
{
    [self.view setBackgroundColor:[UIColor darkGrayColor]];
    [self.view setNeedsDisplay];
}

- (void)viewWillDisappear:(BOOL)animated
{
    [[NSNotificationCenter defaultCenter] removeObserver:self];
    [super viewWillDisappear];
}

One final note, even if this is the correct place to add the observer, I realize that this is bad code. According to Apple’s documentation, not every iOS device has a proximity sensor. So you should check if the senor is available instead of just assuming… however I decided to take liberties because I’m working with an iPhone 4 which does have a proximity sensor and I just wanted to see it work (which it does).

Again, my concern is if I added the notification and observer to the correct part of the project (HeavyRotationAppDelegate vs HeavyViewController).


#2

Why not just keep the object pointer to the HeavyViewController around when you create it in HeavyRotationAppDelegate.m?

In HeavyRotationAppDelegate.h:

@interface HeavyRotationAppDelegate : NSObject <UIApplicationDelegate> {
    HeavyViewController *hvc; 
}

In HeavyRotationAppDelegate.m remove the declaration of hvc and just leave the initialization (and be sure to release it on dealloc):

hvc = [[[HeavyViewController alloc] init] retain];

And when you handle the notifications, you can either call a method inside your HeavyViewController (which seems more “correct” to me, to let the controller for the view do the actual changing of the background) or set it directly with:

 [[hvc view] setBackgroundColor:[UIColor darkGrayColor]];

#3

You could also make the HeavyViewController the observer and have it call a method on it. Note: I am aware I havent added the code to unregister from notifications etc
if the VC is not being displayed, just showing an alternative strategy.

== HeavyRotationAppDelegate.m ==

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    UIDevice *device = [UIDevice currentDevice];
    
    // Tell it to start monitoring the accelerometer for orientation
    // and enable proximity monitoring
    [device beginGeneratingDeviceOrientationNotifications];
    [device setProximityMonitoringEnabled:YES];
    
    // Get the notification center for the app
    NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
    
    // Add yourself as an observer for orientation changes
    [nc addObserver:self 
           selector:@selector(orientationChanged:) 
               name:UIDeviceOrientationDidChangeNotification 
             object:device];
    
    HeavyViewController *hv = [[[HeavyViewController alloc] init] autorelease];
    [[self window] setRootViewController:hv];
    
    // Add HeavyView Controller as an observer when proximity changes
    [nc addObserver:hv 
           selector:@selector(proximityChanged:) 
               name:UIDeviceProximityStateDidChangeNotification 
             object:device];
    
    // Override point for customization after application launch.
    [self.window makeKeyAndVisible];
    return YES;
}

== HeavyViewController.m ==

#import "HeavyViewController.h"

@implementation HeavyViewController

- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation
{
    // Return YES if incoming orientation is Portrait
    // or either of the Landscapes, otherwise return NO
    return (toInterfaceOrientation == UIInterfaceOrientationPortrait) || UIInterfaceOrientationIsLandscape(toInterfaceOrientation);
}

- (void)proximityChanged:(NSNotification *)note
{
    // Log when the proximity of the phone has changed
    NSLog(@"Proximity changed in HeavyView Controller");
    [[self view] setBackgroundColor:[UIColor yellowColor]];
}

@end

#4

You’ll never ever see this…or if you do, you’ve found a memory leak.

Remember: alloc by itself sets the retain count to 1. Explicitly retaining that same object then bumps it up to 2. Why force the code to use TWO release statements to free the object?

Burn this into memory: an object obtained via new, alloc, or copy is owned by you (which implies its retain count is ALREADY 1). An object obtained through any other means is assumed to be autoreleased; if you want to own it (and thus bump up its retain count), then send it a retain msg.


#5

Just an observation: the above solutions set the same colour regardless of the proximity change (moved close to the skin, or moved away).

If you want to set the view’s background colour in HeavyRotationAppDelegate.m (not that this is the correct place; just if you want to), then you can use something like:

    HeavyViewController * hvc = (HeavyViewController *) [[self window] rootViewController];
    [[hvc view] setBackgroundColor: theColor];

I do not know the “correct” place to code this though. Should the delegate do everything, or receive the notification and forward it to the view controller, or register the view controller to receive the notification directly, or let the view controller register itself?

But if I’m reading it correctly, Apple’s documentation on this is kind of funny:

If the device does have a sensor and the user never touches the face then the state will remain NO anyway so…the application knows nothing about whether the device has a proximity sensor or not (though I don’t know what the app would do differently, other than report the device has the sensor).


#6

My first solution was the same as McSorley’s above, although mine crashed with an unrecognized selector error. Which I guess makes sense - how would a method in HeavyViewController.m know anything about what’s going on in HeavyRotationAppDelegate.m

…right?


#7

Hard to say without knowing which selector was unrecognized.


#8

Wow, I was pretty vague, sorry. In HeavyViewController.m I tried to use the method:

- (void)proximityChanged:(NSNotification *)note { //Change the background color here. }

But proximityChanged: is the selector I defined in HeavyRotationAppDelegate.m:

The method worked fine in HeavyRotationAppDelegate, but crashed with an unrecognized selector error when I moved it to HeavyViewController.m Which, of course is where I wanted it so that I could use

I ended up leaving it in HeavyRotationAppDelegate and using

[[hvc view] setBackgroundColor:[UIColor greenColor]]; but it sure seems like the method to change the color belongs in the controller…


#9

Thanks; that helps. :slight_smile:

The significant difference in HeavyRotationAppDelegate.m is:

//McSorley used:
    [nc addObserver:hv selector:@selector(proximityChanged:) ...];

//squarehippo used:
    [nc addObserver:self selector:@selector(proximityChanged:) ...];

The observer is the interested object instance — basically the place where the named selector is defined. If you want to define proximityChanged: in HeavyViewController.m, then you’ll need to pass a HeavyViewController pointer in the addObserver: parameter.


#10

Well, that’s just brilliant, thank you.


#11

[quote=“gc3182”]
But if I’m reading it correctly, Apple’s documentation on this is kind of funny:

If the device does have a sensor and the user never touches the face then the state will remain NO anyway so…the application knows nothing about whether the device has a proximity sensor or not (though I don’t know what the app would do differently, other than report the device has the sensor).[/quote]
The docs are wrong. They should say:
“To determine if proximity monitoring is available, attempt to enable it. If the value of the proximityMonitoringEnabled property remains NO, proximity monitoring is not available.”


#12

I used a different solution to this challenge, and it seems to work out pretty good.

I decided to use the same instance of the notification center in the HeavyRotationAppDelegate.m file that was used to monitor orientation changes. In this way, I would start observing proximity at the same time as I started observing orientation, and more or less deal with both sets of notes in one place. The app delegate seems the best place to do this, and anyway, once I have a note I can always send it off to the view, wherever that is, with a custom method.

Here is the new code for - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions:

@implementation HeavyRotationAppDelegate

@synthesize window = _window;

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    //get the device object
    UIDevice *device = [UIDevice currentDevice];
    
    //tell device to monitor accelerometer for orientation
    [device beginGeneratingDeviceOrientationNotifications];
    
   //NEW CODE tell device to monitor proximity
    [device setProximityMonitoringEnabled:YES];
    
    //get the notification center for the app
    NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
    
    //add self as observer for orientation
    [nc addObserver:self 
           selector:@selector(orientationChanged:) 
               name:UIDeviceOrientationDidChangeNotification 
             object:device];
    
     //NEW CODE add self as observer for proximity
    [nc addObserver:self 
           selector:@selector(proximityChanged:) 
               name:UIDeviceProximityStateDidChangeNotification
             object:device];
    
    HeavyViewController *hvc = [[[HeavyViewController alloc] init] autorelease];
    [[self window] setRootViewController:hvc];
    
    [[self window] makeKeyAndVisible];
    return YES;
}

So now that I have my method proximityChanged:, I need to implement it in such a way as to change my view background color. The only tricky part is knowing whether the proximity change was ON or OFF. An if statement here uses the boolean proximityState method to decide that issue, and give us room to deal with both ON and OFF proximity states.

The new code in HeavyRotationAppDelegate.m:

-(void)orientationChanged:(NSNotification *)note
{
    //Log the constant thwe represents the current orientation
    NSLog(@"orientationChanged: %d", [[note object] orientation]);
}

     //NEW CODE
-(void)proximityChanged:(NSNotification *)note
{
    NSLog(@"Proximity change, swanky!");
    
    UIDevice *device = [UIDevice currentDevice];  
    if([device proximityState]) { 
        [[[self window] rootViewController] proximate]; 
    }
    else {
    [[[self window] rootViewController] performSelector:@selector(notProximate) 
                                                                 withObject:nil 
                                                                  afterDelay:2.0];
    }
}

In order to get at my view, I want to use the view controller that exists now. I know it exists, because the app is running. So I send a message up the chain of objects that I know must exist just now, to get at my current view controller.

Ultimately, the journey of the message to the current view is broken into two discreet steps. I sent the first message chain to the current instance of the root view controller from the application delegate, because I am confident that the current view controller will know where to find its current view. The root view controller will then take care of resetting the view with it’s own special method in the second stage of the journey.

So, in this first step, self goes to its window, and window goes to its root view controller, and, whatever and wherever that object is, it gets given the message proximate if the device returns YES to the proximityState method. Alternatively the root view controller gets the message notProximate if the device returns NO to the proximityState method.

I added the delayed method variation for the notProximate method because of user friendliness. With no notProximate method at all, the original version left me with a permanent green background, which was pretty untidy. With the function but without the delay, you kind of had to look at your ear if you wanted to see the background go green, because the screen goes dark pretty soon after the color change, and then when it lights up again after you have taken it away from your ear, the notProximate method has already reset the view background color to clear. But it did go green, for a very brief time, without the delay.

The delayed method call is pretty neat. If you set the delay between 1.0 and 2.0 seconds, the user experiences a brief yet ultimately satisfying green background after they take the handset away from their ear.

So then we implement the new instance methods in our view controller class file, HeavyViewController.h:

@interface HeavyViewController : UIViewController

//NEW CODE
-(void)proximate;
-(void)notProximate;

@end[/code]

And we implement these methods in the HeavyViewController.m file:

[code]@implementation HeavyViewController

//NEW CODE
-(void)proximate
{
    NSLog(@"Swanky, we are very proximate. Do your thing.");
    [[self view] setBackgroundColor:[UIColor greenColor]];
    [[self view] setNeedsDisplay];
}

-(void)notProximate
{
    NSLog(@"Swanky, we are less proximate than we were before. We can stand down.");
    [[self view] setBackgroundColor:[UIColor clearColor]];
    [[self view] setNeedsDisplay];
}

My strategy here was to tailor my view controller class file with custom instance methods. In this way, the current instance of the root view controller at any given time has always been built in such a way as to be able to respond to the messages from our original instance of NSNotificationCenter in the application delegate. By communicating from the application delegate to the current instance of the root view controller, it is then simple enough to call the current instance of view from self, and then adjust the background color and refresh the view.

It might seem a bit odd for me to have worried about whether HeavyRotation is a user-friendly application, but anyway it seems that there are a lot of hidden methods for exactly this sort of thing in IOS, and that is one of the reasons I like it so much as a developer and a user.


#13

To my mind, McSorely’s approach seems more in keeping with OO and MVC principles. Why burden the app delegate with keeping an ivar and acting as a middleman when the controller can get the notifications directly from the Notification Center(?)

One think I noticed on my version is that after the background changes color when it’s near my head the screen goes black, almost as if Siri were about to rear herself.

Is that happening to anyone else?

mb


#14

I used a very similar approach to dpav02 but put in the ability of the display to go between light and dark grey based on proximity. Kind of futile… but hey try to do it right!

in HeavyViewController.m I changed the proximityChanged code…

- (void)proximityChanged:(NSNotification *)note
{
    if ([[note object] proximityState] == 1) {
        [self.view setBackgroundColor:[UIColor darkGrayColor]];
    } else {
        [self.view setBackgroundColor:[UIColor lightGrayColor]];
    }

    NSLog(@"proximity changed: %d", [[note object] proximityState]);
    
    [self.view setNeedsDisplay];
}

#15

Hi,

I implemented the above solution but when I test it on my iphone with ios5 it just turns off the screen. Any explanation for that?


#16

yeah my screen just turns off as well (iOS 4). we must be missing something?