# Manage offline regions

> **Note (legacy): 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.31.0, in the [Maps SDK documentation](https://docs.mapbox.com/ios/ja/maps/guides/).

![](https://docs.mapbox.com/ios/ja/assets/ideal-img/maps-examples-offline-storage.10795f8.480.png)

For more information about uses and limitations, see our [Offline maps guide](https://docs.mapbox.com/help/troubleshooting/mobile-offline/).

**Swift**

Title: `ViewController`

[View on GitHub](https://github.com/mapbox/ios-sdk-examples/tree/298e050be7352eb28cee6f03e02945593140c1f3/Examples/Swift/ManageOfflineRegionsExample.swift)

```swift
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)
        }
    }
}
```

**Objective C**

Title: `ViewController`

[View on GitHub](https://github.com/mapbox/ios-sdk-examples/tree/298e050be7352eb28cee6f03e02945593140c1f3/Examples/ObjectiveC/ManageOfflineRegionsExample.m)

```swift
#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
```