Objective C - How to call UI-process from non-UI class

110 Views Asked by At

I'm trying to get the user position in iOS. I guess the problem could be related to the dispatch_async. I don't get errors during compilation but only the init function is called and the locationManager never gives an update.

#import <UIKit/UIKit.h>
#import <CoreLocation/CoreLocation.h>
 
@interface LocationC: UIViewController <CLLocationManagerDelegate> {
    CLLocationManager *locationManager;
}
 
@end
 
@implementation LocationC
 
- (instancetype) init {
    self = [super init];
    if (self) {
        locationManager = [[CLLocationManager alloc] init];
        locationManager.delegate = self;
        locationManager.distanceFilter = kCLDistanceFilterNone;
        locationManager.desiredAccuracy = kCLLocationAccuracyBest;
          
        if ([[[UIDevice currentDevice] systemVersion] floatValue] >= 8.0)
            [locationManager requestWhenInUseAuthorization];
        [locationManager startMonitoringSignificantLocationChanges];
        [locationManager startUpdatingLocation];
    }
    return self;
}
 
 //never called
- (void)locationManager:(CLLocationManager *)manager didUpdateToLocation:(CLLocation *)newLocation fromLocation:(CLLocation *)oldLocation {
    NSLog(@"test")
}
@end
 
 
class LocationGet : public claid::Module
{
    public:   
        void initialize()
        {
                dispatch_async(dispatch_get_global_queue(
                     DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void){
                //Background Thread   
                dispatch_async(dispatch_get_main_queue(), ^(void){
                    //Run UI Updates
                    LocationC *example = [[LocationC alloc] init];
                });
            });   
        }
};
2

There are 2 best solutions below

3
Arik Segal On

There might be a scenario in which the authorization status is not "Granted" when you call startMonitoring...

If the user sees the page for the first time and and location permission hasn't been authorized yet, requestWhenInUseAuthorization will display the dialogue, but you need to implement locationManager(_:didChangeAuthorization:) in order to know when the status has changed.

See this article: https://developer.apple.com/documentation/corelocation/cllocationmanager/1620562-requestwheninuseauthorization

It says: After the user makes a selection and determines the status, the location manager delivers the results to the delegate's locationManager(_:didChangeAuthorization:) method.

-- Update --

After another look, you should store a reference to the example instance, or else it would dealloc from memory before any location update is received. A UIViewController instance should be presented or pushed from another UIViewController instance, or alternatively it can be set as the root view controller of the application. Just instantiating it is not enough.

2
The Dreams Wind On

You have to wait for user permission if it's not explicitly given before starting listening to the location updates, so you enable your listener only when this permission choice is delivered:

#import <UIKit/UIKit.h>
#import <CoreLocation/CoreLocation.h>

@interface LocationC: UIViewController <CLLocationManagerDelegate>

@property (nonatomic, readonly) CLLocationManager *locationManager;

@end

@implementation LocationC

- (instancetype) init {
    if (self = [super init]) {
        _locationManager = [CLLocationManager new];
        _locationManager.delegate = self;
        _locationManager.distanceFilter = kCLDistanceFilterNone;
        _locationManager.desiredAccuracy = kCLLocationAccuracyBest;
        [self locationManagerDidChangeAuthorization:_locationManager];
    }
    return self;
}

#pragma mark CLLocationManagerDelegate

- (void)locationManagerDidChangeAuthorization:(CLLocationManager *)manager {
    switch (manager.authorizationStatus) {
        case kCLAuthorizationStatusAuthorizedWhenInUse:
        case kCLAuthorizationStatusAuthorizedAlways:
            [self startLocationListener];
            break;
        case kCLAuthorizationStatusNotDetermined:
            [manager requestWhenInUseAuthorization];
            break;
        default:
            [self stopLocationListener];
    }
}

- (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray<CLLocation *> *)locations {
    NSLog(@"New locations: %@", locations);
}

#pragma mark Private

- (void)startLocationListener {
    [_locationManager startMonitoringSignificantLocationChanges];
    [_locationManager startUpdatingLocation];
}

- (void)stopLocationListener {
    [_locationManager stopMonitoringSignificantLocationChanges];
    [_locationManager stopUpdatingLocation];
}

@end

Also, be advised that you will need to provide the user with some message explaining why exactly you request location permissions by adding NSLocationWhenInUseUsageDescription key in your app's Info.plist


EDIT:

Somehow I completely missed the part that LocationC is a view controller itself. With the extra details you provided in the comments, it seems to me that this instance is not retained by Cocoa Touch framework classes, so your example gets deallocated straight away.

Here are a couple of Cocoa Touch tweaks important to understand: UIKit expects an instance of any UIViewController (or its subclasses) to represent some view on the screen, thus if you expect LocationC to be purely responsible for non-UI logic, you should not subclass it from a UIViewController class and instead you may want to create as another controller property:

// Some view controller interface section
@interface ViewController: UIViewController
...
@property (strong, nonatomic, readonly) LocationC *locationListener;
...
@end

// Some view controller implementation section
@implementation ViewController
...
- (void)viewDidLoad {
    [super viewDidLoad];
    _locationListener = [LocationC new];
}
...
@end

If you instead expect this object to really represent a separate view, and conventionally a separate screen, then you are supposed to somehow pass the presenting view controller to the point in the code where LocationC is supposed to show up:

void initialize()
{
    // m_PresentingController is the membmer of the class which keeps a reference to the
    // presenting controller
    __weak typeof(m_PresentingController) weakController = m_PresentingController;
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        ...
        dispatch_async(dispatch_get_main_queue(), ^{
            // If the presenting controller was destroyed before response handler comes
            // int play, ignore the response
            if (!weakController) {
                return;
            }
            __strong typeof(weakController) strongController = weakController;
            [weakController presentViewController:[LocationC new] animated:YES completion:nil]
        });
    });
}

Alternatively, you can make LocationC a new root controller of your app:

void initialize()
{
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        ...
        dispatch_async(dispatch_get_main_queue(), ^{
            UIApplication.sharedApplication.delegate.window.rootViewController = [LocationC new];
        });
    });
}