yvan vander sanden
yvan vander sanden

Reputation: 985

How do i use mapbox's new MGLOfflinePackDelegate correctly?

I'm creating an app which needs an offline map. I'm testing with MapBox, which supports offline maps since today (yay!). The code I have now seems to work for downloading the map, but the delegate to report on progress never triggers, and I don't have a clue why this is.

I have this class for my mapView:

import UIKit
import Mapbox

class MapController: UIViewController, MGLMapViewDelegate,  UIPopoverPresentationControllerDelegate {

    @IBOutlet var mapView: MGLMapView!

    override func viewDidLoad() {
        super.viewDidLoad()       
        downloadIfNeeded()      
        mapView.maximumZoomLevel = 18
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }

    func downloadIfNeeded() {
        MGLOfflineStorage.sharedOfflineStorage().getPacksWithCompletionHandler { (packs, error) in guard error == nil else {
                return
            }
            for pack in packs {
                let userInfo = NSKeyedUnarchiver.unarchiveObjectWithData(pack.context) as! [String: String]
                if userInfo["name"] == "London" {
                    // allready downloaded
                    return
                }
            }

            // define the download region
            let sw = CLLocationCoordinate2DMake(51.212120, 4.415906)
            let ne = CLLocationCoordinate2DMake(51.223781, 4.442401)

            let bounds = MGLCoordinateBounds(sw: sw, ne: ne)
            let region = MGLTilePyramidOfflineRegion(styleURL: MGLStyle.streetsStyleURL(), bounds: bounds, fromZoomLevel: 10, toZoomLevel: 12)

            let userInfo = ["name": "London"]
            let context = NSKeyedArchiver.archivedDataWithRootObject(userInfo)

            MGLOfflineStorage.sharedOfflineStorage().addPackForRegion(region, withContext: context) { (pack, error) in
                guard error == nil else {
                    return
                }

                // create popup window with delegate
                let storyboard : UIStoryboard = UIStoryboard(name: "Main", bundle: nil)
                let downloadProgress: MapDownloadController = storyboard.instantiateViewControllerWithIdentifier("MapDownloadController") as! MapDownloadController
                downloadProgress.modalPresentationStyle = .Popover
                downloadProgress.preferredContentSize = CGSizeMake(300, 150)

                let popoverMapDownloadController = downloadProgress.popoverPresentationController
                popoverMapDownloadController?.permittedArrowDirections = .Any
                popoverMapDownloadController?.delegate = self
                popoverMapDownloadController?.sourceView = self.mapView
                popoverMapDownloadController?.sourceRect = CGRect(x: self.mapView.frame.midX, y: self.mapView.frame.midY, width: 1, height: 1)
                self.presentViewController(downloadProgress, animated: true, completion: nil)

                // set popup as delegate <----
                pack!.delegate = downloadProgress

                // start downloading
                pack!.resume()
            }
        }
    }
}

And the MapDownloadController is a View which is displayed as popup (see code above) and has the MGLOfflinePackDelegate:

import UIKit
import Mapbox

class MapDownloadController: UIViewController, MGLOfflinePackDelegate {

    @IBOutlet var progress: UIProgressView!
    @IBOutlet var progressText: UILabel!

    override func viewDidLoad() {
        super.viewDidLoad()
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }

    func offlinePack(pack: MGLOfflinePack, progressDidChange progress: MGLOfflinePackProgress) {
        // this function is never called, but why? <----
        let completed = progress.countOfResourcesCompleted
        let expected = progress.countOfResourcesExpected
        let bytes = progress.countOfBytesCompleted

        let MB = bytes / 1024

        let str: String = "\(completed)/\(expected) voltooid (\(MB)MB)"
        progressText.text = str

        self.progress.setProgress(Float(completed) / Float(expected), animated: true)
    }

    func offlinePack(pack: MGLOfflinePack, didReceiveError error: NSError) {
        // neither is this one... <----
        let userInfo = NSKeyedUnarchiver.unarchiveObjectWithData(pack.context) as! [String: String]
        let strError = error.localizedFailureReason
    }

    func offlinePack(pack: MGLOfflinePack, didReceiveMaximumAllowedMapboxTiles maximumCount: UInt64) {
        // .. or this one  <----
        let userInfo = NSKeyedUnarchiver.unarchiveObjectWithData(pack.context) as! [String: String]
    }
}

This is all pretty much taken from the documentation, so why are the delegate's functions (func offlinePack) never called? I did test with breakpoints so i am sure it is not. Still, the popup is shown and the region gets downloaded. (Checked with observing network traffic and with other code which lists the offline packs.)

Upvotes: 1

Views: 1786

Answers (2)

friedbunny
friedbunny

Reputation: 2421

Here’s an extremely simple implementation of Minh’s answer, using the current v3.2.0b1 example code. Expect this answer to become outdated quickly, as we’re still working on the v3.2.0 release.

import UIKit
import Mapbox

class ViewController: UIViewController, UIPopoverPresentationControllerDelegate, MGLOfflinePackDelegate {

    @IBOutlet var mapView: MGLMapView!

    // Array of offline packs for the delegate work around (and your UI, potentially)
    var offlinePacks = [MGLOfflinePack]()

    override func viewDidLoad() {
        super.viewDidLoad()

        mapView.maximumZoomLevel = 2
        downloadOffline()
    }

    func downloadOffline() {
        // Create a region that includes the current viewport and any tiles needed to view it when zoomed further in.
        let region = MGLTilePyramidOfflineRegion(styleURL: mapView.styleURL, bounds: mapView.visibleCoordinateBounds, fromZoomLevel: mapView.zoomLevel, toZoomLevel: mapView.maximumZoomLevel)

        // Store some data for identification purposes alongside the downloaded resources.
        let userInfo = ["name": "My Offline Pack"]
        let context = NSKeyedArchiver.archivedDataWithRootObject(userInfo)

        // Create and register an offline pack with the shared offline storage object.
        MGLOfflineStorage.sharedOfflineStorage().addPackForRegion(region, withContext: context) { (pack, error) in
            guard error == nil else {
                print("The pack couldn’t be created for some reason.")
                return
            }

            // Set the pack’s delegate (assuming self conforms to the MGLOfflinePackDelegate protocol).
            pack!.delegate = self

            // Start downloading.
            pack!.resume()

            // Retain reference to pack to work around it being lost and not sending delegate messages
            self.offlinePacks.append(pack!)
        }
    }

    func offlinePack(pack: MGLOfflinePack, progressDidChange progress: MGLOfflinePackProgress) {
        let userInfo = NSKeyedUnarchiver.unarchiveObjectWithData(pack.context) as! [String: String]
        let completed = progress.countOfResourcesCompleted
        let expected = progress.countOfResourcesExpected
        print("Offline pack “\(userInfo["name"])” has downloaded \(completed) of \(expected) resources.")
    }

    func offlinePack(pack: MGLOfflinePack, didReceiveError error: NSError) {
        let userInfo = NSKeyedUnarchiver.unarchiveObjectWithData(pack.context) as! [String: String]
        print("Offline pack “\(userInfo["name"])” received error: \(error.localizedFailureReason)")
    }

    func offlinePack(pack: MGLOfflinePack, didReceiveMaximumAllowedMapboxTiles maximumCount: UInt64) {
        let userInfo = NSKeyedUnarchiver.unarchiveObjectWithData(pack.context) as! [String: String]
        print("Offline pack “\(userInfo["name"])” reached limit of \(maximumCount) tiles.")
    }

}

Upvotes: 2

Minh Nguyễn
Minh Nguyễn

Reputation: 860

(Cross-posted from this GitHub issue.)

This is a bug in the SDK. The workaround is for the completion handler to assign the passed-in MGLOfflinePack object to an ivar or other strong reference in the surrounding MapDownloadController class (example).

Upvotes: 1

Related Questions