NSMenuItem custom view not updating - objective-c

I have a NSStatusItem object which is created when the app launches (in AppDelegate.m). This object "_statusBarItem" has a Menu attached to it, of class statusBarMenu, which is subclass of NSMenu class but it also has a _panelItem property (of class NSMenuItem) created when you create an instance of statusBarMenu, as you can see below.
In statusBarItem.m
- (instancetype)initWithTitle:(NSString *)aTitle{
self = [super initWithTitle:aTitle];
if (self){
_panelItem = [[NSMenuItem alloc]init];
PanelViewController *panelViewController = [[PanelViewController alloc]init];
panelViewController.menuDelegate = self;
_panelItem.view = panelViewController.view;
[self addItem:_panelItem];
}
return self;
}
The _panelItem has a custom view i.e. a clock in a label (among other things). This view is controlled by PanelViewController class, in which when viewDidLoad method is called calls the tickTock: method shown below. _upTime is the label showing, the clock/time. It is created in the .xib file and connected
- (void)tickTock:(id)obj{
NSTimeInterval timeInterval = 7.5 * 60 * 60;
NSDate *timeToGetUp = [NSDate dateWithTimeInterval:timeInterval sinceDate:[NSDate date]];
NSDateFormatter *dateFormatter = [NSDateFormatter new];
[dateFormatter setDateFormat:#"hh:mm:ss"];
[_upTime setStringValue:[dateFormatter stringFromDate:timeToGetUp]];
[self.view setNeedsDisplay:YES];
[self.menuDelegate refreshView:self];
[self performSelector:#selector(tickTock:) withObject:NULL afterDelay:1.0];
}
As you can see that tickTock: method is called every 1.0 second. This is because I want the label to update, every second with new time. However, the label does not update even though I call setNeedsDisplay: for the PanelViewController's view. I thought this might be because I might be updating the wrong view i.e. I should have been updating the _panelItem's view, instead. So I made a menuDelegate property and made statusBarMenu conform to the protocol show below.
#protocol PanelViewControllerMenuDelgate
- (void)refreshView:(id)obj;
#end
Again the refreshView: method is called every second, it updates the panel view.
- (void)refreshView:(id)obj{
[_panelItem.view setNeedsDisplay:YES];
// [self itemChanged:_panelItem];
}
However, that still does not refresh the view, and show the new label value. I also tried itemChanged: method to the statusBarMenu obj (_statusBarItem) itself, although it did not have any different results.
What I did notice is that if I close the menu (by clicking somewhere else) and re-open it, it does show the new value of the clock/label. So what am I doing wrong, or what am I not doing, that is making the view stay the same. Should I be using some other procedure to make the _panelItem's view refresh every second?
EDIT:
I also noticed that the simulation stop running, when every click the _statusBarItem. I simply added a NSLog statement in tickTock: method and it stopped printing in the console, when ever I open the menu. This is probably why the view is not updating, since the app/Xcode pauses when ever I click the menu. Why does that happen? Also how can I stop it?

I just came up against this problem a few weeks ago. In my case, my NSTimer was not updating my NSMenuItem. I believe your problem is happening because the menu is updating on a different run loop to the perform with delay. This question is answered in a few places. But this is the code you need:
NSTimer interfaceUpdateTimer = [NSTimer timerWithTimeInterval:self.timerSettings.timerUpdateInterval//one second in your case
target:self
selector:#selector(tickTock:)
userInfo:nil
repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:interfaceUpdateTimer forMode:NSRunLoopCommonModes];
The key is NSRunLoopCommonModes. Check out the other answers I linked to; they already explain it really well. That should do the trick I think.
As a side note, neither the NSTimer nor the performSelector:withObject:afterDelay: are guaranteed to fire at exactly the time you specify. So if you need accuracy, do not rely on them to tell the time. If close enough is good enough, then you can ignore this note : )

Related

How to make NSMenu drop down periodically without user input

I have created a statusbar menu item on the top of my window like this:
- (void)createStatusBarItem {
_statusBar = [NSStatusBar systemStatusBar];
_statusItem = [_statusBar statusItemWithLength:NSSquareStatusItemLength];
_statusItem.image = [NSImage imageNamed:#"icon_off.png" ];
_statusItem.highlightMode = YES;
_statusItem.menu = [self createStatusBarMenu];
}
with the menu:
- (NSMenu *)createStatusBarMenu {
NSMenu *menu = [[NSMenu alloc] init];
...
}
I am now wanting to periodically make the menu drop down as if a user had clicked on the status bar icon. Is that even possible?
It is indeed. First create a repeating NSTimer to call a method repeatedly and store it as a property (so you can invalidate it later, etc.)
self.timer = [NSTimer timerWithTimeInterval: 5.0 target: self selector: #selector(displayMenu:) userInfo: nil repeats: YES];
[[NSRunLoop currentRunLoop] addTimer: self.timer forMode: NSRunLoopCommonModes];
Then in the method it calls, display the menu.
[self.statusItem popUpStatusItemMenu: self.statusItem.menu];
In this case it'll pop up every 5 seconds or so. However since you don't know when it closes, the user could close it and then have the timer call and reopen it a second later. So you might want to set yourself as the menu's delegate and implement menuWillOpen: and menuDidClose:. In menuDidClose: you'd probably remake the timer (or set it's fire date) in order to reset the interval so that it'll wait another 5 seconds before opening the menu from the time the menu last closed. You could also create a timer to close the menu automatically if the user didn't close it by themselves in menuWillOpen:, etcetera, so that the menu opens every X seconds and closes after X seconds by itself if nothing is done with it (you'd call the NSMenu's cancelTracking for that.)

Instant / Autosave in IOS

For background: I'm a Windows automation and data translation "expert" (or so they say grins) in my day job. I've been dabbling with Objective-C coding off and on since I bought my first Mac in 2004.
I'm working on an IOS app. My data container class knows how to save and load from disc, and each object responds to an instance method of -(void)saveToImpliedFilename{} or -(void)save:(NSString *)filename {}. There's a static call to load the data files from storage and create distinct data objects from them (they're fairly lightweight objects, so I'm not worried about loading several at a time). The app's domain is such that many of them won't ever be loaded at once anyway.
+(NSArray *)loadData {}
That's all working fine and wonderful. In storage the objects are stored as Xml and life is good.
Where I'm having trouble is when trying to modify the tutorials so that two things happen for me:
Quick note: I'm using the tutorial as a basis for POC coding, then I'll go back and start over with the "real" coding, reusing my data objects and some of the other utility I've built along the way.
Here's my list of goals and the issues:
I want the table view to tell the data objects to save at pretty much every "edit" event. The only one I can consistently get to work is reorganizing the table's order. (the save button and adding a new entry works fine)
entering a new entry into the list creates a nice modal editor with a save and a cancel button which work wonderfully. But if I edit an existing entry, I can't reproduce the save buttons' behaviors. Each time I try, the buttons' events no longer fire. I can't figure out where I'm going wrong.
I'm using the "Editable Table View" project from this tutorial series as my basis: http://www.aboutobjects.com/community/iphone_development_tutorial/tutorial.html
In the following code, the [self isModal] test is where the save/cancel buttons are made visible and wired up. Bringing up the new-entry screen is apparently the only time it's modal. I tried wiring this stuff up so that the buttons were created all the time, but again, the events never fire for either one. The next block below is where the editable table view is called explicitly with the NEW functionality, but the nonModal view of the same tableview is called by the select event on the selector table.
So...
// code snipped for the new/modal editor
- (void)viewDidLoad {
// Uncomment the following line to preserve selection between presentations.
// self.clearsSelectionOnViewWillAppear = NO;
// Uncomment the following line to display an Edit button in the navigation bar for this view controller.
// self.navigationItem.rightBarButtonItem = self.editButtonItem;
// If the user clicked the '+' button in the list view, we're
// creating a new entry rather than modifying an existing one, so
// we're in a modal nav controller. Modal nav controllers don't add
// a back button to the nav bar; instead we'll add Save and
// Cancel buttons.
//
if ([self isModal]) {
UIBarButtonItem *saveButton = [[UIBarButtonItem alloc]
initWithBarButtonSystemItem:UIBarButtonSystemItemSave
target:self
action:#selector(save)];
[[self navigationItem] setRightBarButtonItem:saveButton];
UIBarButtonItem *cancelButton = [[UIBarButtonItem alloc]
initWithBarButtonSystemItem:UIBarButtonSystemItemCancel
target:self
action:#selector(cancel)];
[[self navigationItem] setLeftBarButtonItem:cancelButton];
}
// do stuff here to display my object...
}
// this code is called from the selection table to explicitly add a new data object.
- (void)add {
vhAddVehicleViewController *controller = [[vhAddVehicleViewController alloc] initWithStyle:UITableViewStyleGrouped];
id vehicle = [[Vehicle alloc] init];
[controller setVehicle:vehicle];
[controller setListcontroller:self];
UINavigationController *newNavController = [[UINavigationController alloc] initWithRootViewController:controller];
[[self navigationController] presentViewController:newNavController animated:YES completion:nil];
}
// this is where it's called on the table selection to show the same view without the save/cancel buttons.
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
vhAddVehicleViewController *controller = [[vhAddVehicleViewController alloc] initWithStyle:UITableViewStyleGrouped];
NSUInteger index = [indexPath row];
id vehicle = [[self vehicles] objectAtIndex:index];
[controller setVehicle:vehicle];
[controller setTitle:[vehicle Vehiclename]];
[[self navigationController] pushViewController:controller animated:YES];
}
I'm assuming the issue is that presenting it makes it modal, where as pushing it doesn't...? That's fine. But when I take out the test for modal to try to keep the buttons working, no joy. The buttons draw and click when tapped, but the events don't fire.
HALP! :-)
Thanks much.
-- Chris (I logged in with my Google account so at the top of the page I'm showing as "user1820796") shrug
You forgot to call [super viewDidLoad];
Update
Try removing the cancel button that goes on the left side when pushing the view controller. See if save starts working. I think the problem is you should not add a left button to the navigation bar when the view controller is pushed.
Which method signature are you using?
- (void)save
{
NSLog(#"Saving");
}
Or
- (void)save:(id)sender
{
NSLog(#"Saving");
}
I still think this was related to push/popping the view rather than presenting the view. I switched it all to presentation and it's working how I want now.
Thanks for the assistance guys. Quite a different paradigm than I'm used to on the GUI stuff, but I'm getting there.
thanks!

-setNeedsDisplay not getting called to refresh drawRect

I have two methods that get called from within -drawRect:
- (void)drawRect:(CGRect)rect {
if(drawScheduleFlag) {
[self drawSchedule];
drawScheduleFlag = false;
}
else
[self drawGrid];
}
-drawGrid is called at initialization time. The other method (-drawSchedule) is called from this code, which is on the main thread:
- (void) calendarTouched: (CFGregorianDate) selectedDate {
// NSLog(#"calendarTouched - currentSelectDate: %d/%d/%d", selectedDate.month, selectedDate.day, selectedDate.year);
// NSLog(#"Main thread? %d", [NSThread isMainThread]);
// get data from d/b for this date (date, staff name, cust name, time, length, services required)
//------ stub -------
scheduledDate.year = 2012;
scheduledDate.month = 6;
scheduledDate.day = 20;
staffName = #"Saori";
custName = #"Brian";
startTime.hour = 11;
duration = 2;
servicesReqd = #"Nails";
drawScheduleFlag = true;
[self setNeedsDisplay];
return;
}
I know the code is being executed, but nothing happens to draw the schedule. Why is -[self setNeedsDisplay] not causing the -drawRect to be called?
UPDATE: I have put breaks in so I'm positive it's not being called. The original grid is drawn once; when the user taps a calendar date, -calendarTouched is called and completely executed. The drawScheduleFlag is set to true, and -[self setNeedsDisplay] gets called, but -drawRect does not. It appears that the UIView is not being invalidated (which -setNeedsDisplay is supposed to do), therefore -drawRect is not called.
UPDATE #2: I have a .xib for a UIViewController with two (2) UIViews in it. The first UIView takes about 1/3 of an iPad screen, the second UIView takes the bottom 2/3 of the screen. Each UIView has it's own specific class; the top UIView displays a calendar and is working correctly, capturing touches and changing the date selected.
The bottom UIView is supposed to show the schedule for the date picked in the top UIView. This is where the problem is. Since the top UIView is working, I will put up the code for the bottom UIView's class. It basically draws the schedule grid when -drawRect is first called. Once the user has selected a day, it is supposed to invalidate the UIView to draw the actual schedule on the grid.
Here is the code for Schedule.h: http://pastebin.com/NQpj0i07
Here is the code for Schedule.m: http://pastebin.com/YBbE8y0T
Here is the code for the controller: http://pastebin.com/nDqBCivj
Note that both pastebin's expire in 24 hours from Jun 28, 5:38 PM PST
Repeating what I said in the comments, the problem you are having is that your UIView subclass object is getting deallocated before the drawRect call gets executed, which means that whatever is holding the reference to the object, in this case your controller, is releasing it before you mean to, little issue that sometimes comes up with using ARC.
In the controller code that holds this view you should have something like this declared:
#property (weak, nonatomic) IBOutlet Schedule *scheduleView;
And synthesized:
#synthesize scheduleView = _scheduleView;
And in the controller's dealloc method:
self.scheduleView = nil;
If you're using storyboards, which you appear to not be using since you made a .xib file for this, you should have it hooked up properly. If not simply instantiate it and assign it.
Expanding on what I said before, I'm not sure what you're trying to do but taking a look at your code you are creating the view for a brief moment when the notification gets called and right there it's getting deallocated because no one is holding a reference to it. By doing what I said and control+click drag from the controller to the view in interface builder and hooking it up it will hold the reference to it, and you'll only have 1 Schedule object created.
After that you'll have to modify your code to work with this instance of Schedule:
- (void) testNotification:(NSNotification *) notification {
// was the calendar tapped?
if ([[notification name] isEqualToString:#"calendarUpdated"]) {
NSDictionary *passedData = notification.userInfo; // get passed data
CFGregorianDate currentSelectDate;
NSData *data = [passedData objectForKey:#"currentSelectDate"];
[data getBytes:&currentSelectDate length:sizeof(CFGregorianDate)];
[self.scheduleView calendarTouched:currentSelectDate];
}
return;
}
The other option you have if you're not using interface builder to set everything up is the following in your viewDidLoad:
- (void)viewDidLoad
{
[super viewDidLoad];
self.scheduleView = [[Schedule alloc] init];
[self.view addSubView:self.scheduleView]; // Probably some extra code for positioning it where you want
// notify me when calendar has been tapped and CFGregorianDate has been updated
[[NSNotificationCenter defaultCenter] removeObserver:self];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(testNotification:) name:#"calendarUpdated" object:nil ];
}
As a final note, considering you're new to iOS development (as we all were) you can go to iTunes and look for Stanford's iPad and iPhone Development course (CS193P) on iTunes U, it's completely free and it will teach you most of what you need to know for developing in iOS.

Flipping a UISwitch with an NSTimer

I'm trying to learn how to use NSTimers, and I thought of the following: Create a switch. Let the timer begin as the app begins, and after each second, a function that changes the state of the switch is called.
Here's what I did so far:
I declared both the timer and the switch in the header file ViewControl.h:
//Timer
{NSTimer *timer;}
#property (weak, nonatomic) IBOutlet UISwitch *zeSwitch;
Then, in the ViewControl.m file I defined the following:
- (IBAction)zeSwitch:(id)sender {
UISwitch *zeSwitchSatus = (UISwitch *) sender;
BOOL yn = zeSwitchSatus.isOn;
[zeSwitch setOn:yn animated:YES];
}
- (void)viewDidLoad
{
timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self
selector:#selector(zeSwitch) userInfo:nil repeats:YES];
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
}
So my hope was that when I run the code, I'll see a switch that is on or off. Then I'll see it changing its status automatically with time, without me interfering.
But that didn't work! I first get the image above. Nothing changes. Then it crashes when I press the switch. (But my idea is not to touch it at all.)
Any ideas?
You're pretty close. There's a few things wrong here. First, the method that you're giving to the timer is named zeSwitch: -- the colon is significant. So you need to create the timer like this:
timer = [NSTimer scheduledTimerWithTimeInterval:1
target:self
selector:#selector(zeSwitch:)
userInfo:nil //^ Note colon!
repeats:YES];
The method named zeSwitch without the colon is actually the accessor method for the switch, because you've named your property zeSwitch. You should really rename the timer's action method to clarify this. Right now, the timer is calling the accessor method for the switch every second, which doesn't really do anything.
Next, the timer passes itself to the method it calls. The sender argument in zeSwitch: is going to be the timer, not the switch. If this method was actually being called via the timer, you would get a crash because you'd be sending isOn to the timer, and it doesn't respond to that.
You've got an outlet to the switch, so you can refer to it via that outlet:
- (void)flipSwitch: (NSTimer *)tim
{
BOOL switchIsOn = [[self zeSwitch] isOn];
Notice that I've corrected the names and types in this method -- you'll also need to change the timer creation to reflect this: #selector(flipSwitch:).
Third, you want to flip the switch, so you should be setting it to the opposite of its current status. The next line needs to be:
[[self zeSwitch] setOn:!switchIsOn animated:YES];
The ! operator negates the BOOL to which it's attached, turning YES into NO and vice versa.
1) When you specify a selector that takes one parameter, you need a colon after the name, so #selector(zeSwitch:).
2) The selector that is triggered by a timer gets the timer as a parameter, not a switch, so - (IBAction)zeSwitch:(NSTimer *)timer.

How does Apple update the Airport menu while it is open? (How to change NSMenu when it is already open)

I've got a statusbar item that pops open an NSMenu, and I have a delegate set and it's hooked up correctly (-(void)menuNeedsUpdate:(NSMenu *)menu works fine). That said, that method is setup to be called before the menu is displayed, I need to listen for that and trigger an asynchronous request, later updating the menu while it is open, and I can't figure out how that's supposed to be done.
Thanks :)
EDIT
Ok, I'm now here:
When you click on the menu item (in the status bar), a selector is called that runs an NSTask. I use the notification center to listen for when that task is finished, and write:
[[NSRunLoop currentRunLoop] performSelector:#selector(updateTheMenu:) target:self argument:statusBarMenu order:0 modes:[NSArray arrayWithObject:NSEventTrackingRunLoopMode]];
and have:
- (void)updateTheMenu:(NSMenu*)menu {
NSMenuItem *mitm = [[NSMenuItem alloc] init];
[mitm setEnabled:NO];
[mitm setTitle:#"Bananas"];
[mitm setIndentationLevel:2];
[menu insertItem:mitm atIndex:2];
[mitm release];
}
This method is definitely called because if I click out of the menu and immediately back onto it, I get an updated menu with this information in it. The problem is that it's not updating -while the menu is open-.
Menu mouse tracking is done in a special run loop mode (NSEventTrackingRunLoopMode). In order to modify the menu, you need to dispatch a message so that it will be processed in the event tracking mode. The easiest way to do this is to use this method of NSRunLoop:
[[NSRunLoop currentRunLoop] performSelector:#selector(updateTheMenu:) target:self argument:yourMenu order:0 modes:[NSArray arrayWithObject:NSEventTrackingRunLoopMode]]
You can also specify the mode as NSRunLoopCommonModes and the message will be sent during any of the common run loop modes, including NSEventTrackingRunLoopMode.
Your update method would then do something like this:
- (void)updateTheMenu:(NSMenu*)menu
{
[menu addItemWithTitle:#"Foobar" action:NULL keyEquivalent:#""];
[menu update];
}
(If you want to change the layout of the menu, similar to how the Airport menu shows more info when you option click it, then keep reading. If you want to do something entirely different, then this answer may not be as relevant as you'd like.)
The key is -[NSMenuItem setAlternate:]. For an example, let's say we're going to build an NSMenu that has a Do something... action in it. You'd code that up as something like:
NSMenu * m = [[NSMenu alloc] init];
NSMenuItem * doSomethingPrompt = [m addItemWithTitle:#"Do something..." action:#selector(doSomethingPrompt:) keyEquivalent:#"d"];
[doSomethingPrompt setTarget:self];
[doSomethingPrompt setKeyEquivalentModifierMask:NSShiftKeyMask];
NSMenuItem * doSomething = [m addItemWithTitle:#"Do something" action:#selector(doSomething:) keyEquivalent:#"d"];
[doSomething setTarget:self];
[doSomething setKeyEquivalentModifierMask:(NSShiftKeyMask | NSAlternateKeyMask)];
[doSomething setAlternate:YES];
//do something with m
Now, you'd think that that would create a menu with two items in it: "Do something..." and "Do something", and you'd be partly right. Because we set the second menu item to be an alternate, and because both menu items have the same key equivalent (but different modifier masks), then only the first one (ie, the one that is by default setAlternate:NO) will show. Then when you have the menu open, if you press the modifier mask that represents the second one (ie, the option key), then the menu item will transform in real time from the first menu item to the second.
This, for example, is how the Apple menu works. If you click once on it, you'll see a few options with ellipses after them, such as "Restart..." and "Shutdown...". The HIG specifies that if there's an ellipsis, it means that the system will prompt the user for confirmation before executing the action. However, if you press the option key (with the menu still open), you'll notice they change to "Restart" and "Shutdown". The ellipses go away, which means that if you select them while the option key is pressed down, they will execute immediately without prompting the user for confirmation.
The same general functionality holds true for the menus in status items. You can have the expanded information be "alternate" items to the regular info that only shows up with the option key is pressed. Once you understand the basic principle, it's actually quite easy to implement without a whole lot of trickery.
The problem here is that you need your callback to get triggered even in menu tracking mode.
For example, -[NSTask waitUntilExit] "polls the current run loop using NSDefaultRunLoopMode until the task completes". This means that it won't get run until after the menu closes. At that point, scheduling updateTheMenu to run on NSCommonRunLoopMode doesn't help—it can't go back in time, after all. I believe that NSNotificationCenter observers also only trigger in NSDefaultRunLoopMode.
If you can find some way to schedule a callback that gets run even in the menu tracking mode, you're set; you can just call updateTheMenu directly from that callback.
- (void)updateTheMenu {
static BOOL flip = NO;
NSMenu *filemenu = [[[NSApp mainMenu] itemAtIndex:1] submenu];
if (flip) {
[filemenu removeItemAtIndex:[filemenu numberOfItems] - 1];
} else {
[filemenu addItemWithTitle:#"Now you see me" action:nil keyEquivalent:#""];
}
flip = !flip;
}
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
NSTimer *timer = [NSTimer timerWithTimeInterval:0.5
target:self
selector:#selector(updateTheMenu)
userInfo:nil
repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
}
Run this and hold down the File menu, and you'll see the extra menu item appears and disappears every half second. Obviously "every half second" isn't what you're looking for, and NSTimer doesn't understand "when my background task is finished". But there may be some equally simple mechanism that you can use.
If not, you can build it yourself out of one of the NSPort subclasses—e.g., create an NSMessagePort and have your NSTask write to that when it's done.
The only case you're really going to need to explicitly schedule updateTheMenu the way Rob Keniger described above is if you're trying to call it from outside of the run loop. For example, you could spawn a thread that fires off a child process and calls waitpid (which blocks until the process is done), then that thread would have to call performSelector:target:argument:order:modes: instead of calling updateTheMenu directly.