Search code examples
iosnetwork-programmingbandwidthreachability

How to detect Network Signal Strength in iOS Reachability


I am creating a new Traveling Application in iOS, this application is highly dependent on Maps and will include two Maps.

  1. My first Map will work when the user has a strong Network Signal (Apple Maps).
  2. My second Map will be used when their isn't any Network or really Low signal (Offline MapBox).

Why do I have two different maps in one Application? My Application is a Direction App, so when the user has really low network or none it will go to the offline Map MapBox. Also the Apple Maps will have Yelp integration and not the offline Map MapBox.

So my Question: How can I detect the network signal in WiFi, 4G Lte, and 3G. MapBox Offline Image


Solution

  • My original thought was to time the download of a file, and see how long it takes:

    @interface ViewController () <NSURLSessionDelegate, NSURLSessionDataDelegate>
    
    @property (nonatomic) CFAbsoluteTime startTime;
    @property (nonatomic) CFAbsoluteTime stopTime;
    @property (nonatomic) long long bytesReceived;
    @property (nonatomic, copy) void (^speedTestCompletionHandler)(CGFloat megabytesPerSecond, NSError *error);
    
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
    
        [self testDownloadSpeedWithTimout:5.0 completionHandler:^(CGFloat megabytesPerSecond, NSError *error) {
            NSLog(@"%0.1f; error = %@", megabytesPerSecond, error);
        }];
    }
    
    /// Test speed of download
    ///
    /// Test the speed of a connection by downloading some predetermined resource. Alternatively, you could add the
    /// URL of what to use for testing the connection as a parameter to this method.
    ///
    /// @param timeout             The maximum amount of time for the request.
    /// @param completionHandler   The block to be called when the request finishes (or times out).
    ///                            The error parameter to this closure indicates whether there was an error downloading
    ///                            the resource (other than timeout).
    ///
    /// @note                      Note, the timeout parameter doesn't have to be enough to download the entire
    ///                            resource, but rather just sufficiently long enough to measure the speed of the download.
    
    - (void)testDownloadSpeedWithTimout:(NSTimeInterval)timeout completionHandler:(nonnull void (^)(CGFloat megabytesPerSecond, NSError * _Nullable error))completionHandler {
        NSURL *url = [NSURL URLWithString:@"http://insert.your.site.here/yourfile"];
    
        self.startTime = CFAbsoluteTimeGetCurrent();
        self.stopTime = self.startTime;
        self.bytesReceived = 0;
        self.speedTestCompletionHandler = completionHandler;
    
        NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration ephemeralSessionConfiguration];
        configuration.timeoutIntervalForResource = timeout;
        NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:nil];
        [[session dataTaskWithURL:url] resume];
    }
    
    - (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {
        self.bytesReceived += [data length];
        self.stopTime = CFAbsoluteTimeGetCurrent();
    }
    
    - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
        CFAbsoluteTime elapsed = self.stopTime - self.startTime;
        CGFloat speed = elapsed != 0 ? self.bytesReceived / (CFAbsoluteTimeGetCurrent() - self.startTime) / 1024.0 / 1024.0 : -1;
    
        // treat timeout as no error (as we're testing speed, not worried about whether we got entire resource or not
    
        if (error == nil || ([error.domain isEqualToString:NSURLErrorDomain] && error.code == NSURLErrorTimedOut)) {
            self.speedTestCompletionHandler(speed, nil);
        } else {
            self.speedTestCompletionHandler(speed, error);
        }
    }
    
    @end
    

    Note, this measures the speed including the latency of starting the connection. You could alternatively initialize startTime in didReceiveResponse, if you wanted to factor out that initial latency.


    Having done that, in retrospect, I don't like spending time or bandwidth downloading something that has no practical benefit to the app. So, as an alternative, I might suggest a far more pragmatic approach: Why don't you just try to open a MKMapView and see how long it takes to finish downloading the map? If it fails or if it takes more than a certain amount of time, then switch to your offline map. Again, there is quite a bit of variability here (not only because network bandwidth and latency, but also because some map images appear to be cached), so make sure to set a kMaximumElapsedTime to be large enough to handle all the reasonable permutations of a successful connection (i.e., don't be too aggressive in using a low value).

    To do this, just make sure to set your view controller to be the delegate of the MKMapView. And then you can do:

    @interface ViewController () <MKMapViewDelegate>
    @property (nonatomic, strong) NSDate *startDate;
    @end
    
    static CGFloat const kMaximumElapsedTime = 5.0;
    
    @implementation ViewController
    
    // insert the rest of your implementation here
    
    #pragma mark - MKMapViewDelegate methods
    
    - (void)mapViewWillStartLoadingMap:(MKMapView *)mapView {
        NSDate *localStartDate = [NSDate date];
        self.startDate = localStartDate;
    
        double delayInSeconds = kMaximumElapsedTime;
        dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
        dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
            // Check to see if either:
            //   (a) start date property is not nil (because if it is, we 
            //       finished map download); and
            //   (b) start date property is the same as the value we set
            //       above, as it's possible this map download is done, but
            //       we're already in the process of downloading the next
            //       map.
    
            if (self.startDate && self.startDate == localStartDate)
            {
                [[[UIAlertView alloc] initWithTitle:nil
                                            message:[NSString stringWithFormat:@"Map timed out after %.1f", delayInSeconds]
                                           delegate:nil
                                  cancelButtonTitle:@"OK"
                                  otherButtonTitles:nil] show];
            }
        });
    }
    
    - (void)mapViewDidFailLoadingMap:(MKMapView *)mapView withError:(NSError *)error {
        self.startDate = nil;
    
        [[[UIAlertView alloc] initWithTitle:nil
                                    message:@"Online map failed"
                                   delegate:nil
                          cancelButtonTitle:@"OK"
                          otherButtonTitles:nil] show];
    }
    
    - (void)mapViewDidFinishLoadingMap:(MKMapView *)mapView
    {
        NSTimeInterval elapsed = [[NSDate date] timeIntervalSinceDate:self.startDate];
        self.startDate = nil;
        self.statusLabel.text = [NSString stringWithFormat:@"%.1f seconds", elapsed];
    }