I have several MMPK maps on AGOL that I want to download into my app (not side load).
What are the proper steps to get the mmpk file downloaded into the app?
Solved! Go to Solution.
Hi.
I expect that the AGSPortalItem is going out of scope and is being released before fetchData completes and calls into the completion block. Try keeping a strong reference to the AGSPortalItem (maybe make it a var on your class instance). You can nil it out from within the completion block if you need to.
There is another way to download the MMPK. You can use AGSRequestOperation. The advantage here is that you have access to the download progress, and you can also avoid having to create a large Data object in memory. The AGSRequestOperation can stream the download directly to your file. You would do something like this:
func downloadMMPK(from portalItemForMMPK: AGSPortalItem) {
portalItemForMMPK.load { [weak self] error in
guard let self = self else { return }
if let error = error {
print("Unable to load the portal item: \(error.localizedDescription)")
return
}
guard let portalUrl = portalItemForMMPK.portal.url,
let itemDataUrl = URL(string: "\(portalUrl)/sharing/rest/content/items/\(portalItemForMMPK.itemID)/data"),
let docsFolder = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { return }
let mmpkLocation = docsFolder.appendingPathComponent(portalItemForMMPK.name, isDirectory: false)
let request = AGSRequestOperation(url: itemDataUrl)
request.outputFileURL = mmpkLocation
request.registerListener(self) { result, error in
if let error = error {
print("Unable to download the file: \(error.localizedDescription)")
return
}
guard let result = result as? URL,
result.isFileURL else { return }
let mmpk = AGSMobileMapPackage(fileURL: result)
mmpk.load { error in
if let error = error {
print("Unable to open the mmpk file: \(error.localizedDescription)")
return
}
if let mapFromMMPK = mmpk.maps.first {
self.mapView.map = mapFromMMPK
}
}
}
request.progressHandler = { (downloaded, total) in
print("Downloaded \(100 * downloaded/total)%. \(total - downloaded) bytes remaining")
}
self.progressView.observedProgress = request.progress
AGSOperationQueue.shared().addOperation(request)
}
}
Note, in that code, I've added a ProgressView to my ViewController. I'm actually showing progress twice - once printed to the console in the AGSRequestOperation's progressHandler, and at the same time by just having my UI's ProgressView lean on the AGSRequestOperation's progress object.
The key is defining the outputFileURL property on the AGSRequestOperation. If you do that, the registered listener will be passed a URL object, otherwise it'll get a Data object.
You should be able to create an AGSPortalItem for the AGOL item for the MMPK, and then call fetchData() on that to get NSData. You can then call NSData.writeToFile() to write that MMPK to your iOS filesystem as a .mmpk file.
Thank you Nicholas, I will give that a try.
Hello Nicholas,
I tried to load the portalItem as per your instructions but can't seem to get the portal data to load. When I call the fetchData, noting happens. I can see in the AGS logs that the request get sent but there is never a response. No errors, no data, nothing. Here is how I am calling fetchData, do you see any issues? Thanks for your assistance!
// tried this way
let portalItem = AGSPortalItem(url: portal, itemID: map.itemId)
// also tried this way
let portalItem = AGSPortalItem(url: myURL)
// load portalItem
portalItem.load {error in
if let error = error {
print("Error load mobile map package (mmpk) failed: \(error)")
return
}
if portalItem.type == .mobileMapPackage {
print("MMPK load completed")
// fetch portal data (This never returns, no error, no data, no response)
portalItem.fetchData { data, error in
if let error = error {
print("Error fetch data mobile map package failed: \(error)")
return
}
if data == nil {
print("Error MMPK fetch failed: no data")
} else {
print("MMPK fetch data mobile map package completed")
do {
try data?.write(to: downloadDirectory)
print("MMPK write data mobile map package completed")
} catch {
print("Error MMPK write data failed: \(error.localizedDescription)")
}
}
}
} else {
print("Error MMPK load mobile map package (mmpk) failed")
}
}
Hi.
I expect that the AGSPortalItem is going out of scope and is being released before fetchData completes and calls into the completion block. Try keeping a strong reference to the AGSPortalItem (maybe make it a var on your class instance). You can nil it out from within the completion block if you need to.
There is another way to download the MMPK. You can use AGSRequestOperation. The advantage here is that you have access to the download progress, and you can also avoid having to create a large Data object in memory. The AGSRequestOperation can stream the download directly to your file. You would do something like this:
func downloadMMPK(from portalItemForMMPK: AGSPortalItem) {
portalItemForMMPK.load { [weak self] error in
guard let self = self else { return }
if let error = error {
print("Unable to load the portal item: \(error.localizedDescription)")
return
}
guard let portalUrl = portalItemForMMPK.portal.url,
let itemDataUrl = URL(string: "\(portalUrl)/sharing/rest/content/items/\(portalItemForMMPK.itemID)/data"),
let docsFolder = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { return }
let mmpkLocation = docsFolder.appendingPathComponent(portalItemForMMPK.name, isDirectory: false)
let request = AGSRequestOperation(url: itemDataUrl)
request.outputFileURL = mmpkLocation
request.registerListener(self) { result, error in
if let error = error {
print("Unable to download the file: \(error.localizedDescription)")
return
}
guard let result = result as? URL,
result.isFileURL else { return }
let mmpk = AGSMobileMapPackage(fileURL: result)
mmpk.load { error in
if let error = error {
print("Unable to open the mmpk file: \(error.localizedDescription)")
return
}
if let mapFromMMPK = mmpk.maps.first {
self.mapView.map = mapFromMMPK
}
}
}
request.progressHandler = { (downloaded, total) in
print("Downloaded \(100 * downloaded/total)%. \(total - downloaded) bytes remaining")
}
self.progressView.observedProgress = request.progress
AGSOperationQueue.shared().addOperation(request)
}
}
Note, in that code, I've added a ProgressView to my ViewController. I'm actually showing progress twice - once printed to the console in the AGSRequestOperation's progressHandler, and at the same time by just having my UI's ProgressView lean on the AGSRequestOperation's progress object.
The key is defining the outputFileURL property on the AGSRequestOperation. If you do that, the registered listener will be passed a URL object, otherwise it'll get a Data object.
Excellent, I will give that a try, thanks!
Thanks Nicholas, I think this is the best solution for downloading mmpk files to an iOS app.
How we can do the same in Android ??