UISwitch does sometimes not send UIControlEvents - objective-c

I have a model object which includes a boolean flag. I want to show the value of the flag using a UISwitch. The value can be changed in two ways:
First by the user by toggling the switch. I register the UIControlEventTouchUpInside for that. I also tried UIControlEventValueChanged – it has the exact same effect.
Second by some external state change. I use a timer to check for that state change and set the on property of the switch accordingly.
However, setting the value of the switch in the timer method has the effect that the touchUpInside action is sometimes not triggered even though the user has touched the switch.
So I face the following problem: If I set the switch state in the timer when the state changes externally, I loose some state changes from the user. If I don't use the timer I get all state changes from the user. However, I miss all the external state changes, then.
Now I have run out of ideas. How can I achieve what I want, getting both types of state changes in the model and reflected them correctly in the switch view?
Here is a minimal example that shows the problem. I have replaced the model object by a simple boolean flag, and in the timer I don't change the flag at all, I just call setOn:animated:. I count the invocations of the action method. Like that I can easily find out how many touches were missed:
#import "BPAppDelegate.h"
#import "BPViewController.h"
#implementation BPAppDelegate {
NSTimer *repeatingTimer;
}
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions: (NSDictionary *)launchOptions
{
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
BPViewController *viewController = [[BPViewController alloc] init];
self.window.rootViewController = viewController;
[self.window makeKeyAndVisible];
[self startTimer];
return YES;
}
- (void) startTimer {
repeatingTimer = [NSTimer scheduledTimerWithTimeInterval: 0.2
target: self.window.rootViewController
selector: #selector(timerFired:)
userInfo: nil
repeats: YES];
}
#end
#import "BPViewController.h"
#implementation BPViewController {
UISwitch *uiSwitch;
BOOL value;
int count;
}
- (void)viewDidLoad
{
[super viewDidLoad];
value = true;
uiSwitch = [[UISwitch alloc] init];
uiSwitch.on = value;
[uiSwitch addTarget:self action:#selector(touchUpInside:) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:uiSwitch];
}
- (void)touchUpInside: (UISwitch *)sender {
count++;
value = !value;
NSLog(#"touchUpInside: value: %d, switch: %d, count: %d", value, sender.isOn, count);
}
- (void) timerFired: (NSTimer*) theTimer {
NSLog(#"timerFired: value: %d, switch: %d, count: %d", value, uiSwitch.isOn, count);
// set the value according to some external state. For the example just leave it.
[uiSwitch setOn:value animated:false];
}
#end

Use UIControlEventValueChanged to determine when the switch has been toggled (programmatically or by the user). Don't use touchUpInside. Also touchUpInside doesn't work well for when the user drags the UISwitch.
Also, don't replicate a property (e.g. your value property) that is already maintained by the UISwitch (i.e. the "on" property)...its redundant and will just get you into trouble

EDIT: The following code does exactly what you want, if not exactly the way you want it. If insures that both timer changes and touch changes to the control receive an action method. The method is only going to be sent once per change, which is hopefully what you want.
Note the switch to UIControlEventValueChanged.
[sw addTarget:self action:#selector(touched:) forControlEvents:UIControlEventValueChanged];
- (void) timerFired: (NSTimer*) theTimer
{
// set the value according to some external state. For the example just leave it.
BOOL oldOn = sw.on;
BOOL newOn = YES;
[sw setOn:newOn];
if(newOn != oldOn) {
for(id target in [sw allTargets]) {
for(NSString *action in [sw actionsForTarget:target forControlEvent:UIControlEventValueChanged]) {
[target performSelector:NSSelectorFromString(action) withObject:self];
}
}
}
}
EDIT2:
I [original poster] just saw the new code in your answer and tried it out. I am not sure it is doing exactly what I want. However, I am not sure if I understand it correctly. I don't want to do exactly the same thing in both cases.
Q: When the user touches the switch I want to toggle the boolean value in the model. I not necessarily need to change the switch programmatically because it is done automatically by touching it anyway. I want to count the touches to make sure none are lost.
A: Well, that's not really possible. However, you can certainly add a counter to the action method and see if the taps you make equal the counter. When the user taps the switch, then "sender == theSwitch". If you send the action method otherwise, you can use a different sender (to differentiate them).
If I get the timer event in my minimal example I just want to leave the boolean value like it currently is. However, I need to set the switch state programmatically so that the view reflects the correct model state. – Bernhard Pieber 3 mins ago
Thats fine - but you keep saying you want the action method called. This code does that. If you misstated that, and you don't want the action method called, then delete the "if" block.
- (void)insureValueIs:(BOOL)val
{
BOOL oldVal = sw.on;
if(val != oldVal) {
for(id target in [sw allTargets]) {
for(NSString *action in [sw actionsForTarget:target forControlEvent:UIControlEventValueChanged]) {
[target performSelector:NSSelectorFromString(action) withObject:self];
}
}
}
}
Of course, I do want to achieve the same thing: That the model state is correctly reflected in the view. Does that make sense?
Frankly, you have not done a good job in describing EXACTLY what you want, and I keep responding to your comments, but even now I am still not totally clear. I believe you have all the information you need here to do whatever it is you want.

Your timer is firing so often it probably prevents the switch ever finishing the animated transition to the alternative value, so it never sends the valueChanged event message. You are setting the value to YES every 0.2 seconds which is faster, I think, than the duration of the animation that occurs when the user taps the switch.

My original code works as expected in iOS 7. So it seems to have been a bug in iOS 6.

Related

shouldPerformSegueWithIdentifier always executed first

I have a function called "openButtonPressed" which gets executed when a button on the ui gets pressed. Now I would like to show a loading view at first and then execute the segue. For some reason, the segue always gets called first.
Does somebody have a clue?
Thank you!
- (void)openButtonPressed:(id)sender
{
NSDebug(#"Open Button");
[self showLoadingView];
static NSString* segueToContinue = kSegueToContinue;
if ([self shouldPerformSegueWithIdentifier:segueToContinue sender:self]) {
[self performSegueWithIdentifier:segueToContinue sender:self];
}
}
- (void)showLoadingView
{
if(!self.loadingView) {
self.loadingView = [[LoadingView alloc] init];
[self.view addSubview:self.loadingView];
}
[self.loadingView show];
}
It looks like LoadingView is being initialized without a size.
It also may be executing the segue very quickly and you don't have any time to see the loading view.
You can set a breakpoint at the performSegue call and use the view debugger in Xcode to confirm if the view was loaded and what size the view was initialized to.
Maybe the view is not in the right place or in the right size. But even if you fix that you need to add a delay so the user can see the loading... I replicated your scenario here and after giving the view a initWithFrame it appears, but cannot be seen because is too fast...
If you want to show the loading for a period of time before the performSegue, you could use the performSelector:withObject:afterDelay.
If you need to wait an action from the loadingView, create a completionHandler in the loadingView initializer...
The hints below were very helpful to me and totally a better solution but I ended up using [CATransaction flush]; to force the refresh.
- (void)openButtonPressed:(id)sender
{
NSDebug(#"Open Button");
[self showLoadingView];
[CATransaction flush];
static NSString* segueToContinue = kSegueToContinue;
if ([self shouldPerformSegueWithIdentifier:segueToContinue sender:self]) {
[self performSegueWithIdentifier:segueToContinue sender:self];
}
}

textShouldEndEditing in NSOutlineTableView is getting called twice

I just implemented following method that suppose to take some action after the value of a NSTextField is changed in my NSOutlineView
-(BOOL)control:(NSControl *)control textShouldEndEditing:(NSText *)fieldEditor
{
NSLog(#"end editing");
NSTextField* tf = (NSTextField*)control;
if(selectedItem && [selectedItem isKindOfClass:[HSCategoryClass class]])
{
HSCategoryClass* c = selectedItem;
c.name = tf.stringValue;
// request the update from DB
[[NSNotificationCenter defaultCenter] postNotificationName:#"updatingCategoryName"
object:c
userInfo:#{#"sender":self}];
}
return YES;
}
However, when I'm done editing and hit enter key or navigate anywhere outside of the text field this method is getting called twice instead of just once.
Does anyone know why is this?!
Any kind of help is highly appreciated!
That routine does not signify that editing has ended. Instead, it's called to find out if it should end (hence the name of the method). It can be called by the framework any number of times, and you shouldn't be relying on it for this purpose.
Instead override the NSOutlineView's textDidEndEditing: method.
Be sure to call super.
So you'd subclass the NSOutlineView and in your subclass:
- (void)textDidEndEditing:(NSNotification *)aNotification
{
// do your stuff
[super textDidEndEditing:aNotification];
}

Completing a location fetch within 'pull to refresh'?

I have implemented pull to refresh within my app which when pulled starts the location manager to get a fix on the users location then presents a modal view controller showing that location.
The problem I'm having is that the modal view controller is being presented before the users location has been obtained resulting a blank map for a few more seconds until it's obtained and updated.
I have a property that holds the users current location. Is it possible to 'hold' until that property is not nil (ie. a location has been established) before the pull to refresh calls 'showMap', perhaps if it can't locate the user after a set time it just presents an error? I tried using a 'while' loop to constantly check the currentLocation property but it didn't seem like the right thing to do and didn't work anyway.
This is my pull to refresh code which is set-up in viewDidLoad:
__typeof (&*self) __weak weakSelf = self;
[self.scrollView addPullToRefreshWithActionHandler:^ {
int64_t delayInSeconds = 1.0;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, delayInSeconds * NSEC_PER_SEC);
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
[weakSelf showMap];
});
}];
When the pull to refresh is used it calls these methods:
- (void)showMap
{
[self.locationManager updateCurrentLocation];
[NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:#selector(finishRefresh) userInfo:nil repeats:NO];
}
- (void)finishRefresh
{
[self.scrollView.pullToRefreshController didFinishRefresh];
[self performSegueWithIdentifier:#"showMap" sender:self];
}
Abstract
To avoid map being shown prematurely, you should actually wait for the asynchronous update operation, and the cleanest pattern to do so will be delegation. Here is a nice Apple Doc on its application in Cocoa/Cocoa Touch.
Then it will work roughly like this:
Pull-to-Refresh triggers location update.
When location update is finished, locationManager notifies your controller and the controller presents a map.
How it can be done
I don't know the interface of your location manager class, but if it was CLLocationManager, it could have been done this way.
CLLocationManager has a delegate property. Your view controller should:
conform to CLLocationManagerDelegate protocol
add itself as a delegate to locationManager
implement locationManager:didUpdateLocations: method — it gets called when location data has arrived and is ready for use.
The implementation of this method should look like roughly this:
- (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations
{
/*
* shouldShowMap — BOOL property of View Controller, actually optional
* but allowing to filter out handling of unwanted location updates
* so they will not trigger map segue every time
*/
if (self.shouldShowMap) {
[self performSegueWithIdentifier:#"showMap" sender:self];
self.shouldShowMap = NO;
}
// stop updating location — you need only one update, not a stream of consecutive updates
[self.locationManager stopUpdatingLocation];
}
Your Pull-to-Refresh handler should look like this:
__typeof (&*self) __weak weakSelf = self;
[self.scrollView addPullToRefreshWithActionHandler:^ {
self.shouldShowMap = YES;
[weakSelf.locationManager startUpdatingLocation];
}];
Hope it helped a bit. The best way to learn (well, in most cases) is to look at how stuff is implemented in system frameworks. Think in that direction and you'll definitely come up with a solution :)
If you need clarifications, feel free to ask, I might have not explained it clearly enough.

Grand Central Dispatch. When using GCD [spinner startAnimating] similar to [myView setNeedsDisplay]?

In Grand Central Dispatch I want to start a spinner - UIActivityIndicatorView - spinning prior to beginning long running task:
dispatch_async(cloudQueue, ^{
dispatch_async(dispatch_get_main_queue(),
^{
[self spinnerSpin:YES];
});
[self performLongRunningTask];
dispatch_async(dispatch_get_main_queue(),
^{
[self spinnerSpin:NO];
});
});
Here is the spinnerSpin method:
- (void)spinnerSpin:(BOOL)spin {
ALog(#"spinner %#", (YES == spin) ? #"spin" : #"stop");
if (spin == [self.spinner isAnimating]) return;
if (YES == spin) {
self.hidden = NO;
[self.spinner startAnimating];
} else {
[self.spinner stopAnimating];
self.hidden = YES;
}
}
One thing I have never seen discussed is the difference - if any - between [myView setNeedsDisplay] and [myActivityIndicatorView startAnimating]. Do they behave the same?
Thanks,
Doug
The [UIView setNeedsDisplay] method has nothing to do with a UIActivityIndicatorView's animation state.
setNeedsDisplay simply informs the system that this view's state has changed in a way that invalidates its currently drawn representation. In other words, it asks the system to invoke that view's drawRect method on the next drawing cycle.
You very rarely need to invoke setNeedsDisplay from outside of a view, from code that is consuming the view. This method is meant to be invoked by the view's internal logic code, whenever something changes in its internal state that requires a redraw of the view.
The [UIActivityIndicatorView startAnimating] method is specific to the UIActivityIndicatorView class and simply asks the indicator to start animating (e.g. spinning). This method is instant, without requiring you to call any other method.
On a side note, you could simplify your code by simply calling startAnimating or stopAnimating without manually showing/hiding it. The UIActivityIndicatorView class has a hidesWhenStopped boolean property that defaults to YES, which means that the spinner will show itself as soon as it starts animating, and hide itself when it stops animating.
So your spinnerSpin: method could be refactored like this (as long as you haven't set the hidesWhenStopped property to NO):
- (void)spinnerSpin:(BOOL)spin {
if (YES == spin) {
[self.spinner startAnimating];
} else {
[self.spinner stopAnimating];
}
}

Updating UI Elements with Controller Constructor

I have an NSSlider (slider) and an NSLabel (label) on a window. I also have a class "Controller" that updates the label whenever the slider's value is changed.
The default position of the slider is 0.5, I'm trying to get is where Controller's constructor updates the label upon program launch.
The following is the implementation file for my attempt to do this. Everything works fine except the label is always 0 when I start the program.
#implementation Controller
{
}
-(id)init
{
NSLog(#"initializing...");
[self updateLabel];
return self;
}
- (IBAction)sliderChanged:(id)sender
{
[self updateLabel];
}
- (void)updateLabel
{
label.text = [NSString stringWithFormat:#"%.1f", slider.value];
}
#end
In the console I see the text "initializing...", but the label is never updated. What am I missing?
The controller may be getting initialized (where is your call to [super init]?), but that doesn't mean the outlets are hooked up. The proper way to do that would be to rely on a viewDidLoad, windowDidLoad, or awakeFromNib method.
you should achieve this with bindings and without any "glue code" in controllers.
Here's some reference on how to use them: http://cocoadevcentral.com/articles/000080.php