Manage offline regions
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.7.0, in the Maps SDK documentation.
For more information about uses and limitations, see our Offline maps guide.
import Mapbox
import Foundation
class ViewController: UIViewController, MGLMapViewDelegate {
lazy var mapView: MGLMapView = {
let mapView = MGLMapView(frame: CGRect.zero)
mapView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
mapView.tintColor = .gray
mapView.delegate = self
mapView.translatesAutoresizingMaskIntoConstraints = false
return mapView
}()
lazy var downloadButton: UIButton = {
let downloadButton = UIButton(frame: CGRect.zero)
downloadButton.backgroundColor = UIColor.systemBlue
downloadButton.setTitleColor(UIColor.white, for: .normal)
downloadButton.setTitle("Download Region", for: .normal)
downloadButton.addTarget(self, action: #selector(startOfflinePackDownload), for: .touchUpInside)
downloadButton.layer.cornerRadius = view.bounds.width / 30
downloadButton.translatesAutoresizingMaskIntoConstraints = false
return downloadButton
}()
lazy var tableView: UITableView = {
let tableView = UITableView(frame: CGRect.zero)
tableView.delegate = self
tableView.dataSource = self
tableView.translatesAutoresizingMaskIntoConstraints = false
return tableView
}()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(mapView)
view.addSubview(tableView)
mapView.addSubview(downloadButton)
let centerCoordinate = CLLocationCoordinate2D(latitude: 22.27933, longitude: 114.16281)
mapView.setCenter(centerCoordinate, zoomLevel: 13, animated: false)
// Set up constraints for map view, table view, and download button.
installConstraints()
}
func setupOfflinePackHandler() {
NotificationCenter.default.addObserver(self,
selector: #selector(offlinePackProgressDidChange),
name: NSNotification.Name.MGLOfflinePackProgressChanged,
object: nil)
}
func installConstraints() {
NSLayoutConstraint.activate([
mapView.topAnchor.constraint(equalTo: view.topAnchor),
mapView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
mapView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
mapView.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 0.75),
tableView.topAnchor.constraint(equalTo: mapView.bottomAnchor),
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
tableView.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 0.4),
downloadButton.topAnchor.constraint(equalTo: view.topAnchor, constant: 100),
downloadButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 5),
downloadButton.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.45),
downloadButton.heightAnchor.constraint(equalTo: downloadButton.widthAnchor, multiplier: 0.2)
])
}
/*
For the purposes of this example, remove any offline packs
that exist before the example is re-loaded.
*/
override func viewWillAppear(_ animated: Bool) {
MGLOfflineStorage.shared.resetDatabase { (error) in
if let error = error {
// Handle the error here if packs can't be removed.
print(error)
} else {
MGLOfflineStorage.shared.reloadPacks()
}
}
}
@objc func startOfflinePackDownload(selector: NSNotification) {
// Setup offline pack notification handlers.
setupOfflinePackHandler()
/**
Create a region that includes the current map camera, to be captured
in an offline map. Note: Because tile count grows exponentially as zoom level
increases, you should be conservative with your `toZoomLevel` setting.
*/
let region = MGLTilePyramidOfflineRegion(styleURL: mapView.styleURL,
bounds: mapView.visibleCoordinateBounds,
fromZoomLevel: mapView.zoomLevel,
toZoomLevel: mapView.zoomLevel + 2)
// Store some data for identification purposes alongside the offline pack.
let userInfo = ["name": "\(region.bounds)"]
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 {
// Handle the error if the offline pack couldn’t be created.
print("Error: \(error?.localizedDescription ?? "unknown error")")
return
}
// Begin downloading the map for offline use.
pack!.resume()
}
}
// MARK: - MGLOfflinePack notification handlers
@objc func offlinePackProgressDidChange(notification: NSNotification) {
/**
Get the offline pack this notification is referring to,
along with its associated metadata.
*/
if let pack = notification.object as? MGLOfflinePack,
let userInfo = NSKeyedUnarchiver.unarchiveObject(with: pack.context) as? [String: String] {
// At this point, the offline pack has finished downloading.
if pack.state == .complete {
let byteCount = ByteCountFormatter.string(fromByteCount: Int64(pack.progress.countOfBytesCompleted), countStyle: ByteCountFormatter.CountStyle.memory)
let packName = userInfo["name"] ?? "unknown"
print("""
Offline pack “\(packName)” completed download:
- Bytes: \(byteCount)
- Resource count: \(pack.progress.countOfResourcesCompleted)")
""")
NotificationCenter.default.removeObserver(self, name: NSNotification.Name.MGLOfflinePackProgressChanged,
object: nil)
}
}
// Reload the table to update the progress percentage for each offline pack.
self.tableView.reloadData()
}
}
fileprivate extension MGLOfflinePackProgress {
var percentCompleted: Float {
let percentage = Float((countOfResourcesCompleted / countOfResourcesExpected) * 100)
return percentage
}
var formattedCountOfBytesCompleted: String {
return ByteCountFormatter.string(fromByteCount: Int64(countOfBytesCompleted),
countStyle: .memory)
}
}
extension ViewController: UITableViewDelegate, UITableViewDataSource {
// Create the table view which will display the downloaded regions.
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if let packs = MGLOfflineStorage.shared.packs {
return packs.count
} else {
return 0
}
}
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let label = UILabel()
label.backgroundColor = UIColor.systemBlue
label.textColor = UIColor.white
label.font = UIFont.preferredFont(forTextStyle: .headline)
label.textAlignment = .center
if MGLOfflineStorage.shared.packs != nil {
label.text = "Offline maps"
} else {
label.text = "No offline maps"
}
return label
}
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return 50.0
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell.init(style: .subtitle, reuseIdentifier: "cell")
if let packs = MGLOfflineStorage.shared.packs {
let pack = packs[indexPath.row]
cell.textLabel?.text = "Region \(indexPath.row + 1): size: \(pack.progress.formattedCountOfBytesCompleted)"
cell.detailTextLabel?.text = "Percent completion: \(pack.progress.percentCompleted)%"
}
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
guard let packs = MGLOfflineStorage.shared.packs else { return }
if let selectedRegion = packs[indexPath.row].region as? MGLTilePyramidOfflineRegion {
mapView.setVisibleCoordinateBounds(selectedRegion.bounds, animated: true)
}
}
}
#import "ViewController.h"
@import Mapbox;
@interface ViewController () <MGLMapViewDelegate, UITableViewDelegate, UITableViewDataSource>
@property (strong, nonatomic) MGLMapView *mapView;
@property (nonatomic) UIButton *downloadButton;
@property (nonatomic) UITableView *tableView;
@end
@implementation ViewController
- (MGLMapView *)configureMapView {
MGLMapView *mapView = [[MGLMapView alloc] initWithFrame:CGRectZero];
mapView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
mapView.delegate = self;
mapView.tintColor = UIColor.grayColor;
mapView.translatesAutoresizingMaskIntoConstraints = NO;
return mapView;
}
- (UIButton *)configureDownloadButton
{
UIButton *downloadButton = [[UIButton alloc] initWithFrame: CGRectZero];
downloadButton.backgroundColor = UIColor.systemBlueColor;
[downloadButton setTitleColor:[UIColor whiteColor]
forState:UIControlStateNormal];
[downloadButton setTitle:@"Download Region" forState: UIControlStateNormal];
[downloadButton addTarget:self action: @selector(startOfflinePackDownload:) forControlEvents:UIControlEventTouchUpInside];
downloadButton.layer.cornerRadius = self.view.bounds.size.width / 30;
downloadButton.translatesAutoresizingMaskIntoConstraints = NO;
return downloadButton;
}
- (UITableView *)configureTableView
{
UITableView *tableView = [[UITableView alloc] initWithFrame:CGRectZero];
tableView.delegate = self;
tableView.dataSource = self;
tableView.translatesAutoresizingMaskIntoConstraints = NO;
[tableView registerClass:[UITableViewCell class]
forCellReuseIdentifier:@"cell"];
return tableView;
}
- (void)viewDidLoad {
[super viewDidLoad];
self.downloadButton = [self configureDownloadButton];
self.mapView = [self configureMapView];
self.tableView = [self configureTableView];
[self.view addSubview:self.mapView];
[self.view addSubview:self.tableView];
[self.mapView addSubview:self.downloadButton];
[self.mapView setCenterCoordinate:CLLocationCoordinate2DMake(22.27933, 114.16281)
zoomLevel:13
animated:NO];
// Set up constraints for map view, table view, and download button.
[self installConstraints];
}
-(void)setupOfflinePackHandler {
// Setup offline pack notification handlers.
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(offlinePackProgressDidChange:) name:MGLOfflinePackProgressChangedNotification object:nil];
}
-(void)installConstraints {
NSArray *constraints = @[
[self.mapView.topAnchor constraintEqualToAnchor:self.view.topAnchor],
[self.mapView.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor],
[self.mapView.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor],
[self.mapView.heightAnchor constraintEqualToAnchor:self.view.heightAnchor multiplier:0.75],
[self.tableView.topAnchor constraintEqualToAnchor:self.mapView.bottomAnchor],
[self.tableView.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor],
[self.tableView.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor],
[self.tableView.heightAnchor constraintEqualToAnchor:self.view.heightAnchor multiplier:0.4],
[self.downloadButton.topAnchor constraintEqualToAnchor:self.view.topAnchor constant:100],
[self.downloadButton.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor constant:5],
[self.downloadButton.widthAnchor constraintEqualToAnchor:self.view.widthAnchor multiplier:0.45],
[self.downloadButton.heightAnchor constraintEqualToAnchor:self.downloadButton.widthAnchor multiplier:0.2]
];
[NSLayoutConstraint activateConstraints:constraints];
}
/*
For the purposes of this example, remove any offline packs
that exist before the example is re-loaded.
*/
-(void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:true];
// When leaving this view controller, suspend offline downloads.
[[MGLOfflineStorage sharedOfflineStorage]
resetDatabaseWithCompletionHandler:^(NSError * _Nullable error) {
if (error != nil) {
// Handle the error here if packs can't be removed.
NSLog(@"Error: %@", error.localizedFailureReason);
} else {
// Start downloading.
[[MGLOfflineStorage sharedOfflineStorage] reloadPacks];
}
}];
};
- (void)startOfflinePackDownload: (UIButton *)sender {
// Setup offline pack notification handlers.
[self setupOfflinePackHandler];
/**
Create a region that includes the current map camera, to be captured
in an offline map. Note: Because tile count grows exponentially as zoom level
increases, you should be conservative with your `toZoomLevel` setting.
*/
MGLTilePyramidOfflineRegion *region = [[MGLTilePyramidOfflineRegion alloc] initWithStyleURL:self.mapView.styleURL
bounds:self.mapView.visibleCoordinateBounds
fromZoomLevel:self.mapView.zoomLevel
toZoomLevel: self.mapView.zoomLevel + 2];
// 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) {
// Handle the error if the offline pack couldn’t be created.
NSLog(@"Error: %@", error.localizedFailureReason);
} else {
// Begin downloading the map for offline use.
[pack resume];
}
}];
}
#pragma mark - MGLOfflinePack notification handlers
- (void)offlinePackProgressDidChange:(NSNotification *)notification {
/**
Get the offline pack this notification is referring to,
along with its associated metadata.
*/
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;
uint64_t completedResources = progress.countOfResourcesCompleted;
uint64_t expectedResources = progress.countOfResourcesExpected;
// Calculate current progress percentage.
float progressPercentage = (float)completedResources / expectedResources;
// At this point, the offline pack has finished downloading.
if (pack.state == MGLOfflinePackStateComplete) {
NSString *byteCount = [NSByteCountFormatter
stringFromByteCount:progress.countOfBytesCompleted countStyle:NSByteCountFormatterCountStyleMemory];
NSLog(@"Offline pack “%@” completed: %@, %llu resources",
userInfo[@"name"], byteCount, completedResources);
[[NSNotificationCenter defaultCenter] removeObserver:self name:MGLOfflinePackProgressChangedNotification object:nil];
} else {
// Otherwise, print download/verification progress.
NSLog(@"Offline pack “%@” has %llu of %llu resources — %.2f%%.",
userInfo[@"name"], completedResources,
expectedResources, progressPercentage * 100);
}
// Reload the table to update the progress percentage for each offline pack.
[self.tableView reloadData];
}
#pragma mark - UITableView configuration
// Create the table view which will display the downloaded regions.
- (NSInteger)tableView:(nonnull UITableView *)tableView
numberOfRowsInSection:(NSInteger)section {
if (MGLOfflineStorage.sharedOfflineStorage.packs.count != 0) {
return MGLOfflineStorage.sharedOfflineStorage.packs.count;
} else {
return 0;
}
}
- (UIView *)tableView:(UITableView *)tableView
viewForHeaderInSection:(NSInteger)section {
UILabel * label = [[UILabel alloc] init];
label.backgroundColor = UIColor.systemBlueColor;
label.textColor = UIColor.whiteColor;
label.font = [UIFont preferredFontForTextStyle: UIFontTextStyleHeadline];
label.textAlignment = NSTextAlignmentCenter;
if (MGLOfflineStorage.sharedOfflineStorage.packs != nil) {
label.text = @"Offline maps";
} else {
label.text = @"No offline maps";
}
return label;
}
- (CGFloat)tableView:(UITableView *)tableView
heightForHeaderInSection:(NSInteger)section {
return 50.0;
}
- (nonnull UITableViewCell *)tableView:(nonnull UITableView *)tableView
cellForRowAtIndexPath:(nonnull NSIndexPath *)indexPath {
UITableViewCell *cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle
reuseIdentifier:@"cell"];
if (MGLOfflineStorage.sharedOfflineStorage.packs.count != 0 ) {
MGLOfflinePack * pack = MGLOfflineStorage.sharedOfflineStorage.packs[indexPath.row];
NSInteger number = indexPath.row + 1;
NSInteger countOfBytesCompleted = MGLOfflineStorage.sharedOfflineStorage.packs[indexPath.row].progress.countOfBytesCompleted;
NSString *formattedCountOfBytedCompleted = [NSByteCountFormatter stringFromByteCount:countOfBytesCompleted countStyle:NSByteCountFormatterCountStyleMemory];
UInt64 countOfResourcesCompleted = pack.progress.countOfResourcesCompleted;
UInt64 countOfResourcesExpected = pack.progress.countOfResourcesExpected;
float percentCompleted = ((countOfResourcesCompleted)/(countOfResourcesExpected) * 100);
cell.textLabel.text = [NSString stringWithFormat:@"Region %ld: size: %@", number, formattedCountOfBytedCompleted];
cell.detailTextLabel.text = [NSString stringWithFormat:@"Percent completion: %.1f%%", percentCompleted];
}
return cell;
}
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
MGLTilePyramidOfflineRegion *selectedRegion = MGLOfflineStorage.sharedOfflineStorage.packs[indexPath.row].region;
if ([selectedRegion isKindOfClass:[MGLTilePyramidOfflineRegion class]]) {
[self.mapView setVisibleCoordinateBounds:selectedRegion.bounds animated:YES];
}
}
@end