Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
598 changes: 598 additions & 0 deletions ios-offline-maps/ios-offline-maps.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"colors" : [
{
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"images" : [
{
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "tinted"
}
],
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "map_marker.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
55 changes: 55 additions & 0 deletions ios-offline-maps/ios-offline-maps/ContentView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// Import necessary frameworks for the app
import SwiftUI // For the user interface
import MapboxMaps // For map rendering and interaction
import CoreLocation

struct ContentView: View {
@State private var showingDownloadView = false

var body: some View {
ZStack {
// Map takes full screen as the base layer
MapReader { proxy in
Map(initialViewport: .camera(
center: CLLocationCoordinate2D(
latitude: 39.5,
longitude: -98.0
),
zoom: 2)) {


}
.mapStyle(.standard()) // Use the Mapbox Standard style
.onMapLoaded { event in
// Map is loaded and ready
print("Map loaded successfully")
}
}

// Download button overlay
VStack {
HStack {
Spacer()
Button(action: {
showingDownloadView = true
}) {
HStack {
Image(systemName: "arrow.down.circle.fill")
Text("Manage Offline Regions")
}
.padding()
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(10)
.shadow(radius: 3)
}
.padding()
}
Spacer()
}
}
.sheet(isPresented: $showingDownloadView) {
TileRegionDownloadView()
}
}
}
8 changes: 8 additions & 0 deletions ios-offline-maps/ios-offline-maps/Info.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>MBXAccessToken</key>
<string>YOUR_MAPBOX_ACCESS_TOKEN</string>
</dict>
</plist>
139 changes: 139 additions & 0 deletions ios-offline-maps/ios-offline-maps/OfflineRegionManager.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
//
// OfflineRegionManager.swift
// simple-map-swiftui
//
// Created on 8/12/25.
//

import Foundation
import MapboxMaps

class OfflineRegionManager {

private static var hasDownloadedStylePack = false

static func ensureStylePackDownloaded() {
// Only download once per app session
if hasDownloadedStylePack {
return
}

let offlineManager = OfflineManager()

let stylePackLoadOptions = StylePackLoadOptions(
glyphsRasterizationMode: .ideographsRasterizedLocally,
metadata: ["name": "mapbox-standard-stylepack"],
acceptExpired: false
)

offlineManager.loadStylePack(
for: .standard,
loadOptions: stylePackLoadOptions!
) { _ in } completion: { result in
switch result {
case let .success(stylePack):
// Style pack download finishes successfully
print("Downloaded style pack: \(stylePack)")
hasDownloadedStylePack = true

case let .failure(error):
// Handle error occurred during the style pack download
if case StylePackError.canceled = error {
print("Style pack download cancelled")
} else {
print("Style pack download failed: \(error)")
}
}
}
}

static func downloadRegion(
region: OfflineRegion,
downloadingRegions: Set<String>,
onDownloadingRegionsUpdate: @escaping (Set<String>) -> Void,
onProgress: @escaping (String, Float) -> Void,
onCompletion: @escaping (String, Result<TileRegion, Error>) -> Void
) {
// Don't start if already downloading
if downloadingRegions.contains(region.id) {
return
}

let tileStore = TileStore.default
let offlineManager = OfflineManager()

// Create tileset descriptor
let tilesetDescriptor = offlineManager.createTilesetDescriptor(
for: TilesetDescriptorOptions(
styleURI: .standard, // get tiles for the Mapbox Standard style
zoomRange: 10...14,
tilesets: []
)
)

// Create load options using the region's polygon and metadata
let loadOptions = TileRegionLoadOptions(
geometry: .polygon(region.polygon),
descriptors: [tilesetDescriptor],
metadata: ["name": region.name],
acceptExpired: true
)!

// Start downloading - notify UI to add to downloading set
var updatedDownloadingRegions = downloadingRegions
updatedDownloadingRegions.insert(region.id)
onDownloadingRegionsUpdate(updatedDownloadingRegions)
onProgress(region.id, 0.0)

let _ = tileStore.loadTileRegion(
forId: region.id,
loadOptions: loadOptions
) { progress in
let totalResources = max(progress.requiredResourceCount, 1)
let progressValue = Float(progress.completedResourceCount) / Float(totalResources)
onProgress(region.id, progressValue)
} completion: { result in
// Remove from downloading set and notify completion
updatedDownloadingRegions.remove(region.id)
onDownloadingRegionsUpdate(updatedDownloadingRegions)
onCompletion(region.id, result)
}
}

static func clearAllRegions(onCompletion: @escaping () -> Void) {
let tileStore = TileStore.default

// Get all tile regions from the store
tileStore.allTileRegions { result in
switch result {
case .success(let tileRegions):
// Remove each region found in the store
for tileRegion in tileRegions {
tileStore.removeRegion(forId: tileRegion.id) { removeResult in
switch removeResult {
case .success:
print("Removed region: \(tileRegion.id)")
case .failure(let error):
print("Failed to remove region \(tileRegion.id): \(error)")
}
}
}

// Clear ambient cache after removing regions
tileStore.clearAmbientCache { cacheResult in
switch cacheResult {
case .success(let bytes):
print("Cleared \(bytes) bytes from cache")
case .failure(let error):
print("Failed to clear cache: \(error)")
}
onCompletion()
}

case .failure(let error):
print("Failed to get tile regions: \(error)")
onCompletion()
}
}
}
}
Loading