Skip to content

Instantly share code, notes, and snippets.

@johnnewman
Last active March 10, 2022 22:31
Show Gist options
  • Save johnnewman/266f921682ce799aed24765e04a10a04 to your computer and use it in GitHub Desktop.
Save johnnewman/266f921682ce799aed24765e04a10a04 to your computer and use it in GitHub Desktop.
CircleLayer transition property pulse
//
// CircleTransitionPulse.swift
//
// Created by John Newman on 3/10/22.
// Copyright © 2022 Roadtrippers LLC. All rights reserved.
//
import Foundation
import MapboxMaps
import UIKit
class StyleLayerPulseRenderer {
private let pulseLayerIdentifier = "my-pulse-layer"
private let pulseSourceIdentifier = "my-pulse-source"
private var map: MapView
private var layerPosition: LayerPosition
init(map: MapView, layerPosition: LayerPosition) {
self.map = map
self.layerPosition = layerPosition
}
func startPulseAnimation(forAnnotation annotation: PointAnnotation) {
guard !map.mapboxMap.style.sourceExists(withId: pulseSourceIdentifier),
!map.mapboxMap.style.layerExists(withId: pulseLayerIdentifier) else {
print("Pulse layer/source already exist in style. Aborting.")
return
}
var pulseSource = GeoJSONSource()
pulseSource.data = .feature(.init(geometry: annotation.geometry))
var pulseLayer = CircleLayer(id: pulseLayerIdentifier)
pulseLayer.source = pulseSourceIdentifier
// Set up starting values
pulseLayer.circleStrokeColor = .constant(StyleColor(UIColor.blue))
pulseLayer.circleOpacity = .constant(0)
pulseLayer.circleRadius = .constant(0)
pulseLayer.circleStrokeWidth = .constant(0)
pulseLayer.circleStrokeOpacity = .constant(0)
// Avoid any Mapbox animations except the ones we want.
pulseLayer.circleBlurTransition = .init(duration: 0, delay: 0)
pulseLayer.circleColorTransition = .init(duration: 0, delay: 0)
pulseLayer.circleRadiusTransition = .init(duration: 0, delay: 0)
pulseLayer.circleOpacityTransition = .init(duration: 0, delay: 0)
pulseLayer.circleTranslateTransition = .init(duration: 0, delay: 0)
pulseLayer.circleStrokeColorTransition = .init(duration: 0, delay: 0)
pulseLayer.circleStrokeWidthTransition = .init(duration: 0, delay: 0)
pulseLayer.circleStrokeOpacityTransition = .init(duration: 0, delay: 0)
try! map.mapboxMap.style.addSource(pulseSource, id: pulseSourceIdentifier)
try! map.mapboxMap.style.addLayer(pulseLayer, layerPosition: layerPosition)
newPulseIteration(pulseLayerIdentifier)
}
private func newPulseIteration(_ identifier: String) {
// Animate the pulse.
map.mapboxMap.style.animateCircleLayer(identifier) {
// Wait for the entire animation to finish (3 seconds).
DispatchQueue.main.asyncAfter(deadline: .pulseRepeatInterval) {
// Reset the style layer back to its starting state.
self.map.mapboxMap.style.resetCircleLayer(identifier) {
// Once reset, run another pulse after a pause.
self.newPulseIteration(identifier)
}
}
}
}
}
struct PulseProperties {
static let strokeWidth = (start: 3.0, end: 5.0)
static let strokeOpacity = (start: 1.0, end: 0.0)
static let radius = (start: 0.0, end: 62.0)
}
extension TimeInterval {
static var pulseRepeatInterval: Self { 3 }
static var pulseDuration: Self { 1.5 }
static var pauseBeforePulsing: Self { pulseRepeatInterval - pulseDuration }
}
extension DispatchTime {
// The circle-xxx-transition properties cannot be updated alongside their
// circle-xxx properties. If you try that, the updates don't animate. We must
// first update the transition properties, wait for those updates to take hold,
// then run the circle layer updates.
static var mapboxStyleUpdateDelay: Self {
// TODO: Is there a Mapbox callback for when the transition properties are ready?
.now() + 0.05
}
static var pauseBeforePulsing: Self {
.now() + TimeInterval.pauseBeforePulsing
}
static var pulseRepeatInterval: Self {
.now() + TimeInterval.pulseRepeatInterval
}
}
extension StyleTransition {
static var noDelay: StyleTransition {
.init(duration: 0, delay: 0)
}
static var pulse: StyleTransition {
.init(duration: .pulseDuration, delay: .pauseBeforePulsing)
}
}
extension Style {
func resetCircleLayer(_ identifier: String, completion: @escaping (() -> Void)) {
updateCircleLayerTransitions(identifier, transition: .noDelay) {
self.updateCircleLayerPulseProperties(identifier, atStart: true, completion: completion)
}
}
func animateCircleLayer(_ identifier: String, completion: @escaping (() -> Void)) {
updateCircleLayerTransitions(identifier, transition: .pulse) {
self.updateCircleLayerPulseProperties(identifier, atStart: false, completion: completion)
}
}
private func updateCircleLayerTransitions(_ identifier: String, transition: StyleTransition, completion: @escaping (() -> Void)) {
do {
try updateLayer(withId: identifier, type: CircleLayer.self) { layer in
layer.circleRadiusTransition = transition
layer.circleStrokeWidthTransition = transition
layer.circleStrokeOpacityTransition = transition
}
DispatchQueue.main.asyncAfter(deadline: .mapboxStyleUpdateDelay) {
completion()
}
} catch {
print("Error updating circle transition properties: \(error)")
}
}
private func updateCircleLayerPulseProperties(_ identifier: String, atStart: Bool, completion: @escaping (() -> Void)) {
do {
try self.updateLayer(withId: identifier, type: CircleLayer.self) { layer in
layer.circleRadius = .constant(atStart ? PulseProperties.radius.start : PulseProperties.radius.end)
layer.circleStrokeWidth = .constant(atStart ? PulseProperties.strokeWidth.start : PulseProperties.strokeWidth.end)
layer.circleStrokeOpacity = .constant(atStart ? PulseProperties.strokeOpacity.start : PulseProperties.strokeOpacity.end)
}
completion()
} catch {
print("Error updating circle pulse properties: \(error)")
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment