Download an offline map
A newer version of the Maps SDK is available
This page uses v6.4.1 of the Mapbox Maps SDK. A newer version of the SDK is available. Learn about the latest version, v11.9.0, in the Maps SDK documentation.
See our mobile offline help page for more information about uses and limitations.
For a more comprehensive example that manages offline packs, see MBXOfflinePacksTableViewController
.
import Mapbox
class ViewController: UIViewController, MGLMapViewDelegate {
var mapView: MGLMapView!
var progressView: UIProgressView!
override func viewDidLoad() {
super.viewDidLoad()
mapView = MGLMapView(frame: view.bounds, styleURL: MGLStyle.darkStyleURL)
mapView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
mapView.tintColor = .gray
mapView.delegate = self
view.addSubview(mapView)
mapView.setCenter(CLLocationCoordinate2D(latitude: 22.27933, longitude: 114.16281),
zoomLevel: 13, animated: false)
// Setup offline pack notification handlers.
NotificationCenter.default.addObserver(self, selector: #selector(offlinePackProgressDidChange), name: NSNotification.Name.MGLOfflinePackProgressChanged, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(offlinePackDidReceiveError), name: NSNotification.Name.MGLOfflinePackError, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(offlinePackDidReceiveMaximumAllowedMapboxTiles), name: NSNotification.Name.MGLOfflinePackMaximumMapboxTilesReached, object: nil)
}
func mapViewDidFinishLoadingMap(_ mapView: MGLMapView) {
// Start downloading tiles and resources for z13-14.
startOfflinePackDownload()
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
// When leaving this view controller, suspend offline downloads.
guard let packs = MGLOfflineStorage.shared.packs else { return }
for pack in packs {
if let userInfo = NSKeyedUnarchiver.unarchiveObject(with: pack.context) as? [String: String] {
print("Suspending download of offline pack: “\(userInfo["name"] ?? "unknown")”")
}
pack.suspend()
}
}
func startOfflinePackDownload() {
// Create a region that includes the current viewport and any tiles needed to view it when zoomed further in.
// Because tile count grows exponentially with the maximum zoom level, you should be conservative with your `toZoomLevel` setting.
let region = MGLTilePyramidOfflineRegion(styleURL: mapView.styleURL, bounds: mapView.visibleCoordinateBounds, fromZoomLevel: mapView.zoomLevel, toZoomLevel: 14)
// Store some data for identification purposes alongside the downloaded resources.
let userInfo = ["name": "My Offline Pack"]
let context = NSKeyedArchiver.archivedData(withRootObject: userInfo)
// Create and register an offline pack with the shared offline storage object.
MGLOfflineStorage.shared.addPack(for: region, withContext: context) { (pack, error) in
guard error == nil else {
// The pack couldn’t be created for some reason.
print("Error: \(error?.localizedDescription ?? "unknown error")")
return
}
// Start downloading.
pack!.resume()
}
}
// MARK: - MGLOfflinePack notification handlers
@objc func offlinePackProgressDidChange(notification: NSNotification) {
// Get the offline pack this notification is regarding,
// and the associated user info for the pack; in this case, `name = My Offline Pack`
if let pack = notification.object as? MGLOfflinePack,
let userInfo = NSKeyedUnarchiver.unarchiveObject(with: pack.context) as? [String: String] {
let progress = pack.progress
// or notification.userInfo![MGLOfflinePackProgressUserInfoKey]!.MGLOfflinePackProgressValue
let completedResources = progress.countOfResourcesCompleted
let expectedResources = progress.countOfResourcesExpected
// Calculate current progress percentage.
let progressPercentage = Float(completedResources) / Float(expectedResources)
// Setup the progress bar.
if progressView == nil {
progressView = UIProgressView(progressViewStyle: .default)
let frame = view.bounds.size
progressView.frame = CGRect(x: frame.width / 4, y: frame.height * 0.75, width: frame.width / 2, height: 10)
view.addSubview(progressView)
}
progressView.progress = progressPercentage
// If this pack has finished, print its size and resource count.
if completedResources == expectedResources {
let byteCount = ByteCountFormatter.string(fromByteCount: Int64(pack.progress.countOfBytesCompleted), countStyle: ByteCountFormatter.CountStyle.memory)
print("Offline pack “\(userInfo["name"] ?? "unknown")” completed: \(byteCount), \(completedResources) resources")
} else {
// Otherwise, print download/verification progress.
print("Offline pack “\(userInfo["name"] ?? "unknown")” has \(completedResources) of \(expectedResources) resources — \(String(format: "%.2f", progressPercentage * 100))%.")
}
}
}
@objc func offlinePackDidReceiveError(notification: NSNotification) {
if let pack = notification.object as? MGLOfflinePack,
let userInfo = NSKeyedUnarchiver.unarchiveObject(with: pack.context) as? [String: String],
let error = notification.userInfo?[MGLOfflinePackUserInfoKey.error] as? NSError {
print("Offline pack “\(userInfo["name"] ?? "unknown")” received error: \(error.localizedFailureReason ?? "unknown error")")
}
}
@objc func offlinePackDidReceiveMaximumAllowedMapboxTiles(notification: NSNotification) {
if let pack = notification.object as? MGLOfflinePack,
let userInfo = NSKeyedUnarchiver.unarchiveObject(with: pack.context) as? [String: String],
let maximumCount = (notification.userInfo?[MGLOfflinePackUserInfoKey.maximumCount] as AnyObject).uint64Value {
print("Offline pack “\(userInfo["name"] ?? "unknown")” reached limit of \(maximumCount) tiles.")
}
}
}
#import "ViewController.h"
@import Mapbox;
@interface ViewController () <MGLMapViewDelegate>
@property (nonatomic) MGLMapView *mapView;
@property (nonatomic) UIProgressView *progressView;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.mapView = [[MGLMapView alloc] initWithFrame:self.view.bounds styleURL:[MGLStyle darkStyleURL]];
self.mapView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
self.mapView.tintColor = [UIColor lightGrayColor];
self.mapView.delegate = self;
[self.view addSubview:self.mapView];
[self.mapView setCenterCoordinate:CLLocationCoordinate2DMake(22.27933, 114.16281)
zoomLevel:13
animated:NO];
// Setup offline pack notification handlers.
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(offlinePackProgressDidChange:) name:MGLOfflinePackProgressChangedNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(offlinePackDidReceiveError:) name:MGLOfflinePackErrorNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(offlinePackDidReceiveMaximumAllowedMapboxTiles:) name:MGLOfflinePackMaximumMapboxTilesReachedNotification object:nil];
}
- (void)mapViewDidFinishLoadingMap:(MGLMapView *)mapView {
// Start downloading tiles and resources for z13-14.
[self startOfflinePackDownload];
}
- (void)viewDidDisappear:(BOOL)animated {
[super viewDidDisappear:animated];
// When leaving this view controller, suspend offline downloads.
for (MGLOfflinePack *pack in MGLOfflineStorage.sharedOfflineStorage.packs) {
NSDictionary *userInfo = [NSKeyedUnarchiver unarchiveObjectWithData:pack.context];
NSLog(@"Suspending download of offline pack: %@", userInfo[@"name"]);
[pack suspend];
}
}
- (void)dealloc {
// Remove offline pack observers.
NSLog(@"Removing offline pack notification observers");
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (void)startOfflinePackDownload {
// Create a region that includes the current viewport and any tiles needed to view it when zoomed further in.
// Because tile count grows exponentially with the maximum zoom level, you should be conservative with your `toZoomLevel` setting.
id <MGLOfflineRegion> region = [[MGLTilePyramidOfflineRegion alloc] initWithStyleURL:self.mapView.styleURL bounds:self.mapView.visibleCoordinateBounds fromZoomLevel:self.mapView.zoomLevel toZoomLevel:14];
// Store some data for identification purposes alongside the downloaded resources.
NSDictionary *userInfo = @{ @"name": @"My Offline Pack" };
NSData *context = [NSKeyedArchiver archivedDataWithRootObject:userInfo];
// Create and register an offline pack with the shared offline storage object.
[[MGLOfflineStorage sharedOfflineStorage] addPackForRegion:region withContext:context completionHandler:^(MGLOfflinePack *pack, NSError *error) {
if (error != nil) {
// The pack couldn’t be created for some reason.
NSLog(@"Error: %@", error.localizedFailureReason);
} else {
// Start downloading.
[pack resume];
}
}];
}
#pragma mark - MGLOfflinePack notification handlers
- (void)offlinePackProgressDidChange:(NSNotification *)notification {
MGLOfflinePack *pack = notification.object;
// Get the associated user info for the pack; in this case, `name = My Offline Pack`
NSDictionary *userInfo = [NSKeyedUnarchiver unarchiveObjectWithData:pack.context];
MGLOfflinePackProgress progress = pack.progress;
// or [notification.userInfo[MGLOfflinePackProgressUserInfoKey] MGLOfflinePackProgressValue]
uint64_t completedResources = progress.countOfResourcesCompleted;
uint64_t expectedResources = progress.countOfResourcesExpected;
// Calculate current progress percentage.
float progressPercentage = (float)completedResources / expectedResources;
// Setup the progress bar.
if (!self.progressView) {
self.progressView = [[UIProgressView alloc] initWithProgressViewStyle:UIProgressViewStyleDefault];
CGSize frame = self.view.bounds.size;
self.progressView.frame = CGRectMake(frame.width / 4, frame.height * 0.75f, frame.width / 2, 10);
[self.view addSubview:self.progressView];
}
[self.progressView setProgress:progressPercentage animated:YES];
// If this pack has finished, print its size and resource count.
if (completedResources == expectedResources) {
NSString *byteCount = [NSByteCountFormatter stringFromByteCount:progress.countOfBytesCompleted countStyle:NSByteCountFormatterCountStyleMemory];
NSLog(@"Offline pack “%@” completed: %@, %llu resources", userInfo[@"name"], byteCount, completedResources);
} else {
// Otherwise, print download/verification progress.
NSLog(@"Offline pack “%@” has %llu of %llu resources — %.2f%%.", userInfo[@"name"], completedResources, expectedResources, progressPercentage * 100);
}
}
- (void)offlinePackDidReceiveError:(NSNotification *)notification {
MGLOfflinePack *pack = notification.object;
NSDictionary *userInfo = [NSKeyedUnarchiver unarchiveObjectWithData:pack.context];
NSError *error = notification.userInfo[MGLOfflinePackUserInfoKeyError];
NSLog(@"Offline pack “%@” received error: %@", userInfo[@"name"], error.localizedFailureReason);
}
- (void)offlinePackDidReceiveMaximumAllowedMapboxTiles:(NSNotification *)notification {
MGLOfflinePack *pack = notification.object;
NSDictionary *userInfo = [NSKeyedUnarchiver unarchiveObjectWithData:pack.context];
uint64_t maximumCount = [notification.userInfo[MGLOfflinePackUserInfoKeyMaximumCount] unsignedLongLongValue];
NSLog(@"Offline pack “%@” reached limit of %llu tiles.", userInfo[@"name"], maximumCount);
}
@end