Add animated weather data
This example demonstrates how to animate an image layer on a map using the Mapbox Maps SDK for iOS.
The addImageLayer
method creates an ImageSource
to manage the image displayed in the RasterLayer
on the map. It sets the image coordinates and URL, then adds a RasterLayer
with specific fade settings. Additionally, the method handles tap gestures
to start and stop the image animation using a Timer
. The manageTimer
method updates the image source with a new radar image at regular intervals,
creating a continuous animation effect.
This example code is part of the Maps SDK for iOS Examples App, a working iOS project available on Github. iOS developers are encouraged to run the examples app locally to interact with this example in an emulator and explore other features of the Maps SDK.
See our Run the Maps SDK for iOS Examples App tutorial for step-by-step instructions.
import MapboxMaps
import UIKit
final class ViewController: UIViewController {
private var mapView: MapView!
private let sourceId = "radar-source"
private var timer: Timer?
private var imageNumber = 0
private var cancelables = Set<AnyCancelable>()
override func viewDidLoad() {
super.viewDidLoad()
let center = CLLocationCoordinate2D(latitude: 41.874, longitude: -75.789)
let cameraOptions = CameraOptions(center: center, zoom: 5)
let mapInitOptions = MapInitOptions(cameraOptions: cameraOptions, styleURI: .dark)
mapView = MapView(frame: view.bounds, mapInitOptions: mapInitOptions)
mapView.autoresizingMask = [.flexibleHeight, .flexibleWidth]
// Hide the `scaleBar` at all zoom levels.
mapView.ornaments.options.scaleBar.visibility = .hidden
// This also updates the color of the info button to match the map's style.
mapView.tintColor = .lightGray
// Set the map's `CameraBoundsOptions` to limit the map's zoom level.
try? mapView.mapboxMap.setCameraBounds(with: CameraBoundsOptions(maxZoom: 5.99, minZoom: 4))
view.addSubview(mapView)
mapView.mapboxMap.onMapLoaded.observeNext { _ in
self.addImageLayer()
}.store(in: &cancelables)
}
func addImageLayer() {
// Create an `ImageSource`. This will manage the image displayed in the `RasterLayer` as well
// as the location of that image on the map.
var imageSource = ImageSource(id: sourceId)
// Set the `coordinates` property to an array of longitude, latitude pairs.
imageSource.coordinates = [
[-80.425, 46.437],
[-71.516, 46.437],
[-71.516, 37.936],
[-80.425, 37.936]
]
// Get the file path for the first radar image, then set the `url` for the `ImageSource` to that path.
let path = Bundle.main.url(forResource: "radar0", withExtension: "gif")
imageSource.url = path?.absoluteString
// Create a `RasterLayer` that will display the images from the `ImageSource`
var imageLayer = RasterLayer(id: "radar-layer", source: sourceId)
// Set `rasterFadeDuration` to `0`. This prevents visible transitions when the image is updated.
imageLayer.rasterFadeDuration = .constant(0)
do {
try mapView.mapboxMap.addSource(imageSource)
try mapView.mapboxMap.addLayer(imageLayer)
} catch {
print("Failed to add the source or layer to style. Error: \(error)")
}
// Add a tap gesture handler that will allow the animation to be stopped and started.
mapView.gestures.onMapTap.observe {[weak self] _ in
self?.manageTimer()
}.store(in: &cancelables)
manageTimer()
}
func manageTimer() {
if timer == nil {
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in
guard let self = self else { return }
// There are five radar images, number 0-4. Increment the count. When that would
// result in an `imageNumber` value greater than 4, reset `imageNumber` to `0`.
if self.imageNumber < 4 {
self.imageNumber += 1
} else {
self.imageNumber = 0
}
// Create a `UIImage` from the file at the specified path.
let path = Bundle.main.url(forResource: "radar\(self.imageNumber)", withExtension: "gif")
let image = UIImage(contentsOfFile: path!.relativePath)
do {
// Update the image used by the `ImageSource`.
try self.mapView.mapboxMap.updateImageSource(withId: self.sourceId, image: image!)
} catch {
print("Failed to update style image. Error: \(error)")
}
}
} else {
timer?.invalidate()
timer = nil
}
}
deinit {
timer?.invalidate()
}
}