Completing a location fetch within 'pull to refresh'? - objective-c

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.

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];
}
}

UISwitch does sometimes not send UIControlEvents

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.

ios CLLocationManager won't get location second time

I have a getLocation method that initializes my CLLocationManager and calls startUpdatingLocation. After I get the user's location, I call stopUpdatingLocation.
Later, when the user presses refresh, I call a method reloadData which calls getLocation so I can get the user's latest location. However, this never calls the locationManager:didUpdateToLocation:fromLocation
method.. so I never get the user's latest location. What could be the issue?
-(void) getLocation {
self.locationManager = [[CLLocationManager alloc] init];
self.locationManager.desiredAccuracy = kCLLocationAccuracyBest;
self.locationManager.delegate = self;
[self.locationManager startUpdatingLocation];
}
- (void) locationManager: (CLLocationManager *)manager
didUpdateToLocation: (CLLocation *)newLocation
fromLocation:(CLLocation *)oldLocation
{
if (oldLocation!= nil) { // make sure to wait until second latlong value
[self setLatitude:newLocation.coordinate.latitude];
[self setLongitude: newLocation.coordinate.longitude];
[self.locationManager stopUpdatingLocation];
self.locationManager.delegate = nil;
self.locationManager = nil;
[self makeUseOfLocationWithLat:[self getLatitude] andLon:[self getLongitude]];
}
}
-(void) reloadData {
[self getLocation];
}
Is it really necessary to allocate a new CLLocationManager? Try just allocate it once (in your init for example) and just call startUpdatingLocation and stopUpdatingLocation on demand.
For me, this solution works great.
Do you move during testing this? Because I think the callback will only be triggered, when:
you call startUpdatingLocation
your location changes
the location manager gets better results (more detailed)
So I think for your use-case: as you don't move, the callback locationManager:didUpdateToLocation:fromLocation: will only be called once after you hit refresh and as there is no old location, you will not go into the if case.
Another thing: you should definitely have only one CLLocationManager in your class. I only use one per project/application (wrapped in a singleton). This is the preferred way to do! Also I think this will retain the oldLocation value so that your problem may be resolved by changing this.
Best,
Christian
you can try this ,this will help u to update for every time when open the page.Cause if u put startUpdatingLocation in viewdidload ,it will get load only for the first time
-(void)viewWillAppear:(BOOL)animated
{
[locationManager startUpdatingLocation];
}

Is [self autorelease] acceptable?

There are cases where you want to present an alert style view controller using your own animations (instead of using presentModalViewController:animated: or UIAlertView).
The right way of releasing this view controller would be in a callback called when the view disappears, but setting up a delegate and all that seems overkill.
So I do this:
- (void)dismiss
{
[UIView animateWithDuration:0.3 delay:0 options:UIViewAnimationOptionCurveEaseIn animations:^{
self.view.alpha = 0;
} completion:^(BOOL finished) {
[self.view removeFromSuperview];
[self autorelease];
}];
}
and the presenting object would not release or autorelease the view controller. Memory management wise I see no issue with this. Is it bad practice?
I would consider this bad practice.
Only objects that have called retain on this object should call release or autorelease on it.
I do assume that you haven't called [self retain]
I'm not even sure this would work as you expect it to. Can you guarantee that self would need releasing at that point and why couldn't an object that has retained it called release itself?. Are you trying to force self to dealloc?
If self was deallocated at this point, any other object that was expecting self to still be alive would be passing messages to nil, or worse, the memory might be re-allocated and those objects would be sending messages to arbitrary objects.
It seems like bad practice however don't see any effect on memory. In terms of modal view why don't you just pop it back if thats what you want to achieve using:
[self.navigationController popViewControllerAnimated:YES];

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];
}
}