Flipping a UISwitch with an NSTimer - objective-c

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.

Related

Should I write NSTimer code in view controller or separate class with either delegation/notification pattern?

I have 5 buttons in my MotorViewController that act as on/off switches for 5 motors. Press button A, motor A will run indefinitely 'til you press the button again to stop it.
I've just added a 6th button that will tell the motor A to run for 2 minutes. I've added the NSTimer code in my ViewController and everything works fine. After 2 minutes, I call my method, runPump, and the motor shuts off automatically.
I've been optimizing my MotorViewController quite heavily, and this will be the first time optimizing for an NSTimer.
Here's the code:
#import "MotorViewController.h"
#interface MotorViewController()
#property (nonatomic, strong) NSTimer *counterTimer;
#end
#implementation MotorViewController
{
int _count;
}
- (void)viewDidLoad
{
_count = 0;
}
// called from the 6th button action method (code is implied)
- (void)setupTimerForCalib
{
self.counterTimer = [NSTimer scheduledTimerWithTimeInterval:1.0
target:self
selector:#selector(timerCount)
userInfo:nil
repeats:YES];
[[NSRunLoop mainRunLoop] addTimer:self.counterTimer forMode:NSRunLoopCommonModes];
NSLog(#"timer started");
}
- (void)timerCount {
_count++;
NSLog(#"count: %d", _count);
if (_count == 120) {
_count = 0;
[self.counterTimer invalidate];
NSLog(#"timer ended");
// timer has ended, shut pump A (SALINE) off
[self setPumpInfo:SALINE select:0];
[self runPump];
}
}
I have another view controller that I'd like to have use these methods, so a better reason yet to not just keep them in MotorViewController.
Should I keep these NSTimer methods within MotorViewController, or create a delegation class for them? Or (after grazing around on the web a bit), set up an NSNotification that, after the 2 minutes, calls setPumpInfo:select: and runPump?
Whichever the best option, could you also explain the reasoning for that over the other. I'm trying to learn more about design patterns and know how to use them in the right scenarios. Thanks!
I would have an NSObject subclass modelling your pump.
I would give this a setInfo and both a run and stop method (at least).
Your ViewControllers should be controlling the views and interacting with your models, so they would create the new pump object (model) that they are interacting with.
Now, you might want to add another method to your Pump: runAfterDelay:(NSTimeInterval)delay forDuration:(NSTimeInterval) duration and embed the NSTimer within the Pump class.
You can then use pumps in your view controllers as follows:
-(void) startPump {
[self.pump setInfo:SALINE select:0];
[self.pump runAfterDelay: 120 forDuration: 120];
}
Keep the logic out of your view controllers, so you don't have to replicate it.

Any bugs or changes in NSTimer with OC?

I'm testing NSTimer with OC and Swift. When I write with OC, here is something I don't quite understand: is it REQUIRED to add the timer to the NSRunLoop after the timer is inited? If not required, why can't it be invoked in a loop even if I set the repeats to YES?
Here is my code, I just init a timer and set repeats to YES, and what I expect is the code timerTick: should be invoked every 2 seconds, but it doesn't work as I expect... until I add the timer to the NSRunLoop.
OC:
-(void)viewDidLoad {
[super viewDidLoad];
NSTimer *timer = [NSTimer timerWithTimeInterval:2 target:self selector:#selector(timerTick:) userInfo:nil repeats:YES];
}
-(void) timerTick:(NSTimer*) timer{
NSLog(#"ticked,%#",#"aa");
}
I rewrote the same code with Swift
As you can see, I don't add the timer to NSRunLoop, however it works as I expected: the runTimeCode method is invoked every 2 seconds.
var timer:NSTimer!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
timer = NSTimer.scheduledTimerWithTimeInterval(2.0, target: self, selector: "runTimeCode", userInfo: nil, repeats: true)
}
func runTimeCode(){
NSLog("ticked")
}
My Question
Is there any bug in OC with iOS9.2 of NSTimer? I searched a lot with Google, but I didn't find anything saying it is REQUIRED if you want to let the timer works correctly.
How do I use NSTimer?
Is NSTimer usage different between OC and Swift?
First of all, you are using different methods. How come you would get the same result? scheduledTimerWithTimeInterval will automatically add to the NSRunLoop after initializing it. However, timerWithTimeInterval won't. You will need to manually call fire or add it to the NSRunLoop in order to trigger it.
Just like the methods' name mean, scheduledTimerWithTimeInterval will do scheduling for you. As for timerWithTimeInterval, it just gives you a timer. Apple is pretty restricted on the names. Sometimes, you can guess its purpose based on it.
With Objective-C, you should try this method :
NSTimer *timer = [NSTimer scheduleTimerWithTimeInterval:2 target:self userinfo:...];
You can see all method's content in Xcode.

NSMenuItem custom view not updating

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 : )

Does NSTimer run across entire app?

If I start an NSTimer like this:
#property (strong) NSTimer * messageTimer;
self.messageTimer = [NSTimer scheduledTimerWithTimeInterval:10.0
target:self
selector:#selector(checkForMessages)
userInfo:nil
repeats:YES];
Does it continue to run when I switch to different view controllers?
Until I cancel it with:
[messageTimer invalidate]; self. messageTimer = nil;
Yes.
Okay, now here is an extended description. NSTimer registers itself on nearest NSRunLoop, that is, current dispatch loop (they may nest). This loop asks various sources for events and calls corresponding callbacks.
When it is time for NSTimer to fire, it returns YES to NSRunLoop and that runs passed callback. There is no such thing as "other current view controller". It is all about first responder and view hierarchy, neither doesn't have any effect on run loops.

Automatically Loading At App Start

[Cocoa/Objective-C]
I adapted a timer routine (for current time) from this site (and it works great - thanks). It's currently attached to a button.
My question is: How do I make it start when my app starts up (and not use a button) (in other languages, I'd simply put the action listener or timer in the Form)...?
Thank for any help on this!
In your application delegate you'll find a method called
- (void)applicationDidFinishLaunching:(UIApplication *)application
I guess that would be where to start a timer on app startup.
Put it in your awakeFromNib method. This is called on all objects that get deserialized from your nib (like your application delegate), but it isn't called until all objects are deserialized and wired (so you can use your text field, for instance). For example:
- (void)timerFired:(NSTimer*)timer
{
NSLog(#"Timer completed!");
}
- (void)awakeFromNib
{
[NSTimer scheduledTimerWithTimeInterval:30.0 target:self selector:#selector(timerFired:) userInfo:nil repeats:NO];
}
Obviously, in this simple example the timer could have been created in either the applicationDidFinishLaunching: method or the awakeFromNib method since it doesn't interact with any other serialized objects, but in your case, it sounds like you need the awakeFromNib method.