Move MapLibre into the Components folder

Continue grouping the standalone modules under Components. The MapLibre
frameworks were nested inside the app folder even though they are separate
targets, so move them alongside the other components and update the
XcodeGen includes.
This commit is contained in:
Stefan Ceriu
2026-08-12 15:03:47 +03:00
parent f73285db3e
commit d595e4fd83
11 changed files with 70 additions and 70 deletions
@@ -0,0 +1,19 @@
//
// Copyright 2026 Element Creations Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
// Please see LICENSE files in the repository root for full details.
//
import SwiftUI
/// Implemented by MapLibreShim's entry point, which the app instantiates after
/// `dlopen`-ing the framework.
public protocol MapLibreShimProtocol {
/// Routes MapLibre's logs into the app's logger. Call once after loading the shim.
func configureLogging(_ handler: @escaping @Sendable (MapLogSeverity, String) -> Void)
/// Builds the interactive map view. The app-side `MapLibreMapView` forwards every
/// body evaluation here so SwiftUI updates flow through to the underlying map.
func makeMapView(configuration: MapViewConfiguration) -> AnyView
}
@@ -0,0 +1,56 @@
//
// Copyright 2026 Element Creations Ltd.
// Copyright 2023-2025 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
// Please see LICENSE files in the repository root for full details.
//
import CoreLocation
import Foundation
/**
Behavior mode of the current user's location, can be hidden, only shown and shown following the user
*/
public enum ShowUserLocationMode: Equatable {
/// this mode will show the user pin in map
case show
/// this mode will show the user pin in map and track him, panning the map automatically
case showAndFollow
/// this mode will not show the user pin in map
case hide
/// this mode will not show the user pin in map and will follow the marker with the given id,
/// panning the map automatically.
case hideAndFollowMarker(id: String)
/// The id of the marker the map should follow when in the `hideAndFollowMarker` mode, nil otherwise.
public var followedMarkerID: String? {
guard case .hideAndFollowMarker(let id) = self else { return nil }
return id
}
}
public enum MapLibreError: Error, Hashable {
case failedLoadingMap
case failedLocatingUser
}
/// The severity of a map log message forwarded to the app.
public enum MapLogSeverity {
case error
case warning
case info
case debug
case verbose
}
/// A marker shown on the map, identified so that it can be moved in place when its coordinate changes.
public struct MapAnnotation {
public let id: String
public let coordinate: CLLocationCoordinate2D
public init(id: String, coordinate: CLLocationCoordinate2D) {
self.id = id
self.coordinate = coordinate
}
}
@@ -0,0 +1,76 @@
//
// Copyright 2026 Element Creations Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
// Please see LICENSE files in the repository root for full details.
//
import CoreLocation
import SwiftUI
/// Everything the map implementation needs in order to render, expressed without app
/// types so that the implementation framework can be loaded on demand.
public struct MapViewConfiguration {
/// the final zoom level used when the first user location emit
public let zoomLevel: Double
/// The initial zoom level used when the map it firstly loaded and the user location is not yet available, in case of annotations this property is not being used
public let initialZoomLevel: Double
/// The initial map center
public let mapCenter: CLLocationCoordinate2D
/// Map annotations
public let annotations: [MapAnnotation]
/// The style to load the map with, for either the dark or the light appearance.
public let styleURL: (_ isDark: Bool) -> URL?
public let tintColor: UIColor
/// The content of the marker representing the annotation with the given id.
public let markerView: (_ annotationID: String) -> AnyView?
/// Behavior mode of the current user's location, can be hidden, only shown and shown following the user
public let showsUserLocationMode: Binding<ShowUserLocationMode>
/// Bind view errors if any
public let error: Binding<MapLibreError?>
/// Coordinate of the center of the map
public let mapCenterCoordinate: Binding<CLLocationCoordinate2D?>
public let hasLoadedUserLocation: Binding<Bool>
public let isLocationAuthorized: Binding<Bool?>
/// The radius of uncertainty for the location, measured in meters.
public let geolocationUncertainty: Binding<CLLocationAccuracy?>
/// Called when the user pan on the map
public let userDidPan: (() -> Void)?
public init(zoomLevel: Double,
initialZoomLevel: Double,
mapCenter: CLLocationCoordinate2D,
annotations: [MapAnnotation] = [],
styleURL: @escaping (_ isDark: Bool) -> URL?,
tintColor: UIColor,
markerView: @escaping (_ annotationID: String) -> AnyView?,
showsUserLocationMode: Binding<ShowUserLocationMode>,
error: Binding<MapLibreError?>,
mapCenterCoordinate: Binding<CLLocationCoordinate2D?>,
hasLoadedUserLocation: Binding<Bool>,
isLocationAuthorized: Binding<Bool?>,
geolocationUncertainty: Binding<CLLocationAccuracy?>,
userDidPan: (() -> Void)? = nil) {
self.zoomLevel = zoomLevel
self.initialZoomLevel = initialZoomLevel
self.mapCenter = mapCenter
self.annotations = annotations
self.styleURL = styleURL
self.tintColor = tintColor
self.markerView = markerView
self.showsUserLocationMode = showsUserLocationMode
self.error = error
self.mapCenterCoordinate = mapCenterCoordinate
self.hasLoadedUserLocation = hasLoadedUserLocation
self.isLocationAuthorized = isLocationAuthorized
self.geolocationUncertainty = geolocationUncertainty
self.userDidPan = userDidPan
}
}
@@ -0,0 +1,19 @@
# The types shared between the app and the MapLibreShim framework. The app links this
# (tiny, initialiser-free) framework instead of MapLibre, see MapLibreShim's target.yml.
name: MapLibreInterface
targets:
MapLibreInterface:
type: framework
platform: iOS
settings:
base:
PRODUCT_BUNDLE_IDENTIFIER: $(BASE_BUNDLE_IDENTIFIER).maplibre-interface
GENERATE_INFOPLIST_FILE: YES
SWIFT_VERSION: 6.2
SWIFT_APPROACHABLE_CONCURRENCY: YES
SWIFT_DEFAULT_ACTOR_ISOLATION: MainActor
sources:
- path: ../Sources
@@ -0,0 +1,126 @@
//
// Copyright 2025 Element Creations Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
// Please see LICENSE files in the repository root for full details.
//
import CoreLocation
import QuartzCore
/// Smoothly animates a `MapLibreAnnotation`'s coordinate from its current
/// position to a new one using a `CADisplayLink` for frame-accurate updates.
///
/// ## How it works
///
/// MapLibre repositions annotation views whenever it observes a KVO change on
/// the annotation's `coordinate` property (which must be `@objc dynamic`).
/// Rather than setting the destination coordinate in one step— which would
/// cause the pin to jump — we interpolate between the start and end coordinates
/// over a configurable duration, updating the coordinate on every display frame.
/// Because MapLibre handles the actual view positioning through KVO, the
/// annotation moves correctly even while the user pans or zooms the map,
/// and even if the annotation is currently off-screen.
///
/// Only one animation runs per annotation at a time; starting a new animation
/// cancels the previous one and begins from the annotation's current position.
final class CoordinateAnimator {
private var displayLink: CADisplayLink?
private weak var annotation: MapLibreAnnotation?
private let startCoordinate: CLLocationCoordinate2D
private let endCoordinate: CLLocationCoordinate2D
private let duration: CFTimeInterval
private var startTime: CFTimeInterval?
/// Keeps a strong reference to each active animator so it isn't deallocated
/// while the display link is running. Keyed by annotation ID.
private static var activeAnimators: [String: CoordinateAnimator] = [:]
private init(annotation: MapLibreAnnotation,
to end: CLLocationCoordinate2D,
duration: CFTimeInterval) {
self.annotation = annotation
startCoordinate = annotation.coordinate
endCoordinate = end
self.duration = duration
}
/// Starts animating the annotation's coordinate to `end` over `duration` seconds.
/// If the annotation is already being animated, the in-flight animation is
/// cancelled and a new one starts from the current position.
static func animate(annotation: MapLibreAnnotation,
to end: CLLocationCoordinate2D,
duration: CFTimeInterval) {
guard annotation.coordinate.latitude != end.latitude
|| annotation.coordinate.longitude != end.longitude else {
return
}
// Cancel any in-flight animation for this annotation
activeAnimators[annotation.id]?.stop()
let animator = CoordinateAnimator(annotation: annotation, to: end, duration: duration)
activeAnimators[annotation.id] = animator
animator.start()
}
// MARK: - Private
private func start() {
let displayLink = CADisplayLink(target: self, selector: #selector(tick))
// Using .common so the animation keeps running during scroll tracking
displayLink.add(to: .main, forMode: .common)
self.displayLink = displayLink
}
private func stop() {
displayLink?.invalidate()
displayLink = nil
if let annotation {
Self.activeAnimators.removeValue(forKey: annotation.id)
}
}
/// Called on every display frame. Linearly interpolates between the start
/// and end coordinates using an ease-in-out curve, then updates the
/// annotation's coordinate to trigger MapLibre's KVO repositioning.
@objc private func tick(_ displayLink: CADisplayLink) {
guard let annotation else {
stop()
return
}
if startTime == nil {
startTime = displayLink.timestamp
}
let elapsed = displayLink.timestamp - (startTime ?? displayLink.timestamp)
let linearProgress = min(elapsed / duration, 1.0)
let easedProgress = quadraticEaseInOut(linearProgress)
let latitude = startCoordinate.latitude + (endCoordinate.latitude - startCoordinate.latitude) * easedProgress
let longitude = startCoordinate.longitude + (endCoordinate.longitude - startCoordinate.longitude) * easedProgress
annotation.coordinate = CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
if linearProgress >= 1.0 {
stop()
}
}
/// Quadratic ease-in-out: accelerates during the first half, decelerates
/// during the second half.
///
/// - Parameter t: Linear progress in the range `0...1`.
/// - Returns: Eased progress in the range `0...1`.
private func quadraticEaseInOut(_ t: Double) -> Double {
if t < 0.5 {
// Ease-in: parabolic acceleration from 0 to the midpoint
return 2 * t * t
} else {
// Ease-out: parabolic deceleration from the midpoint to 1
// Equivalent to: 1 - 2 * (1 - t)^2
let shifted = t - 1
return 1 - 2 * shifted * shifted
}
}
}
@@ -0,0 +1,60 @@
//
// Copyright 2026 Element Creations Ltd.
// Copyright 2023-2025 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
// Please see LICENSE files in the repository root for full details.
//
import CoreLocation
import MapLibre
import MapLibreInterface
import SwiftUI
final class MapLibreAnnotation: NSObject, MLNAnnotation, Identifiable {
let id: String
// @objc dynamic is required to make animations work
@objc dynamic var coordinate: CLLocationCoordinate2D
// MARK: - Setup
init(_ annotation: MapAnnotation) {
id = annotation.id
coordinate = annotation.coordinate
super.init()
}
}
final class MapLibreAnnotationView: MLNUserLocationAnnotationView {
private var hostingController: UIHostingController<AnyView>?
// MARK: - Setup
override init(annotation: MLNAnnotation?, reuseIdentifier: String?) {
super.init(annotation: annotation, reuseIdentifier:
reuseIdentifier)
}
convenience init(annotation: MapLibreAnnotation, content: AnyView?) {
self.init(annotation: annotation, reuseIdentifier: "\(Self.self)")
let hostingController = UIHostingController(rootView: content ?? AnyView(EmptyView()))
self.hostingController = hostingController
let view: UIView = hostingController.view
view.backgroundColor = .clear
view.anchorPoint = .init(x: 0.5, y: 1.0)
addSubview(view)
view.bounds.size = view.intrinsicContentSize
}
func updateContent(_ content: AnyView?) {
hostingController?.rootView = content ?? AnyView(EmptyView())
if let hostedView = hostingController?.view {
hostedView.bounds.size = hostedView.intrinsicContentSize
}
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError()
}
}
@@ -0,0 +1,297 @@
//
// Copyright 2026 Element Creations Ltd.
// Copyright 2023-2025 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
// Please see LICENSE files in the repository root for full details.
//
import MapLibre
import MapLibreInterface
import SwiftUI
struct MapLibreMapView: UIViewRepresentable {
// MARK: - Properties
@Environment(\.colorScheme) private var colorScheme
let configuration: MapViewConfiguration
// MARK: - UIViewRepresentable
func makeUIView(context: Context) -> MLNMapView {
let mapView = makeMapView()
mapView.delegate = context.coordinator
setupMap(mapView: mapView)
return mapView
}
func updateUIView(_ mapView: MLNMapView, context: Context) {
// Don't set the same value twice. Otherwise, if there is an error loading the map, a loop
// is caused as the `error` binding being set, which triggers this update, which sets a
// new URL, which causes another error, and so it goes on round and round in a circle.
let dynamicMapURL = configuration.styleURL(colorScheme == .dark)
if mapView.styleURL != dynamicMapURL {
mapView.styleURL = dynamicMapURL
}
// If the center coordinate was updated externally (not by the map itself), move the map.
// Not applied while following a marker, where the camera is driven by the marker's position.
if configuration.showsUserLocationMode.wrappedValue.followedMarkerID == nil,
let newCenter = configuration.mapCenterCoordinate.wrappedValue,
newCenter.latitude != context.coordinator.lastReportedCenter?.latitude
|| newCenter.longitude != context.coordinator.lastReportedCenter?.longitude {
context.coordinator.lastReportedCenter = newCenter
mapView.setCenter(newCenter, animated: true)
}
// Update existing annotation views with fresh SwiftUI content.
// This handles the case where the annotation's view data changes after
// the annotation was initially placed (e.g. user avatar loads asynchronously).
updateAnnotations(in: mapView)
showUserLocation(in: mapView)
context.coordinator.updateMarkerFollowing(in: mapView, markerID: configuration.showsUserLocationMode.wrappedValue.followedMarkerID)
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
// MARK: - Private
private func setupMap(mapView: MLNMapView) {
mapView.addAnnotations(configuration.annotations.map(MapLibreAnnotation.init))
mapView.zoomLevel = configuration.annotations.isEmpty ? configuration.initialZoomLevel : configuration.zoomLevel
mapView.centerCoordinate = configuration.mapCenter
}
private func updateAnnotations(in mapView: MLNMapView) {
let existingByID = Dictionary(uniqueKeysWithValues:
(mapView.annotations ?? []).compactMap { $0 as? MapLibreAnnotation }.map { ($0.id, $0) })
let updatedByID = Dictionary(uniqueKeysWithValues: configuration.annotations.map { ($0.id, $0) })
let existingIDs = Set(existingByID.keys)
let updatedIDs = Set(updatedByID.keys)
// Remove annotations that are no longer present
let removedIDs = existingIDs.subtracting(updatedIDs)
if !removedIDs.isEmpty {
let toRemove = removedIDs.compactMap { existingByID[$0] }
mapView.removeAnnotations(toRemove)
}
// Add new annotations
let addedIDs = updatedIDs.subtracting(existingIDs)
if !addedIDs.isEmpty {
let toAdd = addedIDs.compactMap { updatedByID[$0] }.map(MapLibreAnnotation.init)
mapView.addAnnotations(toAdd)
}
// Update existing annotations that are still present
let keptIDs = existingIDs.intersection(updatedIDs)
for id in keptIDs {
guard let existingAnnotation = existingByID[id],
let updatedAnnotation = updatedByID[id] else {
continue
}
CoordinateAnimator.animate(annotation: existingAnnotation,
to: updatedAnnotation.coordinate,
duration: 1.0)
if let annotationView = mapView.view(for: existingAnnotation) as? MapLibreAnnotationView {
annotationView.updateContent(configuration.markerView(id))
}
}
}
private func makeMapView() -> MLNMapView {
let mapView = MLNMapView(frame: .zero, styleURL: configuration.styleURL(colorScheme == .dark))
mapView.logoViewPosition = .topLeft
mapView.attributionButtonPosition = .topLeft
mapView.attributionButtonMargins = .init(x: mapView.logoView.frame.maxX + 8, y: mapView.logoView.center.y / 2)
mapView.tintColor = configuration.tintColor
mapView.allowsRotating = false
mapView.allowsTilting = false
return mapView
}
private func showUserLocation(in mapView: MLNMapView) {
switch (configuration.showsUserLocationMode.wrappedValue, configuration.annotations) {
case (.showAndFollow, _):
mapView.userTrackingMode = .follow
case (.show, let annotations) where !annotations.isEmpty:
// In the show mode, if there are annotations, we check the authorizationStatus,
// if it's not determined, we wont prompt the user with a request for permissions,
// because they should be able to see the annotations without sharing their location information.
guard mapView.locationManager.authorizationStatus != .notDetermined else { return }
fallthrough
case (.show, _):
mapView.showsUserLocation = true
mapView.setUserTrackingMode(.none, animated: false, completionHandler: nil)
case (.hide, _), (.hideAndFollowMarker, _):
// In hideAndFollowMarker mode the camera following is handled by the coordinator.
mapView.showsUserLocation = false
mapView.setUserTrackingMode(.none, animated: false, completionHandler: nil)
}
}
}
// MARK: - Coordinator
extension MapLibreMapView {
class Coordinator: NSObject, MLNMapViewDelegate {
// MARK: - Properties
var mapLibreView: MapLibreMapView
private var previousUserLocation: MLNUserLocation?
/// Tracks the last center coordinate reported by the map (or set programmatically),
/// so that `updateUIView` can tell apart external binding changes from internal ones.
var lastReportedCenter: CLLocationCoordinate2D?
/// The annotation the camera is currently locked on while in the `hideAndFollowMarker` mode.
private weak var followedAnnotation: MapLibreAnnotation?
/// Observes the followed annotation's coordinate, updated every frame by the `CoordinateAnimator`.
private var followedAnnotationObservation: NSKeyValueObservation?
// MARK: - Setup
init(_ mapLibreView: MapLibreMapView) {
self.mapLibreView = mapLibreView
}
// MARK: - Marker following
/// Keeps the camera locked on the marker with the given id, or stops following when nil.
func updateMarkerFollowing(in mapView: MLNMapView, markerID: String?) {
guard let markerID else {
stopFollowingMarker()
return
}
guard let annotation = (mapView.annotations ?? [])
.compactMap({ $0 as? MapLibreAnnotation })
.first(where: { $0.id == markerID }) else {
// The marker isn't on the map (yet), resolve it again on the next update.
stopFollowingMarker()
return
}
guard annotation !== followedAnnotation else { return }
startFollowingMarker(annotation, in: mapView)
}
private func startFollowingMarker(_ annotation: MapLibreAnnotation, in mapView: MLNMapView) {
followedAnnotation = annotation
followedAnnotationObservation = nil
// Animate to the marker first, attaching the per-frame tracking only on completion
// so that it doesn't cancel the transition.
mapView.setCenter(annotation.coordinate,
zoomLevel: mapView.zoomLevel,
direction: -1, // negative value keeps the current direction
animated: true) { [weak self, weak annotation, weak mapView] in
guard let self, let annotation, let mapView, annotation === followedAnnotation else { return }
// Mirror every animated coordinate update to keep the camera as smooth as the marker.
followedAnnotationObservation = annotation.observe(\.coordinate) { [weak mapView] annotation, _ in
// The coordinate is only ever updated by SwiftUI so KVO fires on the main actor.
MainActor.assumeIsolated {
mapView?.setCenter(annotation.coordinate, animated: false)
}
}
}
}
private func stopFollowingMarker() {
followedAnnotation = nil
followedAnnotationObservation = nil
}
// MARK: - MLNMapViewDelegate
func mapView(_ mapView: MLNMapView, viewFor annotation: MLNAnnotation) -> MLNAnnotationView? {
guard let annotation = annotation as? MapLibreAnnotation else {
return nil
}
return MapLibreAnnotationView(annotation: annotation, content: mapLibreView.configuration.markerView(annotation.id))
}
func mapViewDidFailLoadingMap(_ mapView: MLNMapView, withError error: Error) {
if mapLibreView.configuration.error.wrappedValue != .failedLoadingMap {
mapLibreView.configuration.error.wrappedValue = .failedLoadingMap
}
}
func mapView(_ mapView: MLNMapView, didUpdate userLocation: MLNUserLocation?) {
guard let userLocation else { return }
mapLibreView.configuration.hasLoadedUserLocation.wrappedValue = true
if previousUserLocation == nil, mapLibreView.configuration.annotations.isEmpty {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
mapView.setCenter(userLocation.coordinate, zoomLevel: self.mapLibreView.configuration.zoomLevel, animated: true)
}
}
previousUserLocation = userLocation
updateGeolocationUncertainty(location: userLocation)
}
func mapView(_ mapView: MLNMapView, didChangeLocationManagerAuthorization manager: MLNLocationManager) {
switch manager.authorizationStatus {
case .denied, .restricted:
mapLibreView.configuration.isLocationAuthorized.wrappedValue = false
case .authorizedAlways, .authorizedWhenInUse:
mapLibreView.configuration.isLocationAuthorized.wrappedValue = true
case .notDetermined:
mapLibreView.configuration.isLocationAuthorized.wrappedValue = nil
@unknown default:
break
}
}
func mapView(_ mapView: MLNMapView, regionDidChangeAnimated animated: Bool) {
// While following a marker, don't flood the center binding with per-frame camera updates.
// Leaving `lastReportedCenter` untouched avoids an external re-centering when following stops.
guard mapLibreView.configuration.showsUserLocationMode.wrappedValue.followedMarkerID == nil else { return }
// Avoid `Publishing changes from within view update` warnings
DispatchQueue.main.async { [mapLibreView, weak self] in
let center = mapView.centerCoordinate
self?.lastReportedCenter = center
mapLibreView.configuration.mapCenterCoordinate.wrappedValue = center
}
}
func mapView(_ mapView: MLNMapView, shouldChangeFrom oldCamera: MLNMapCamera, to newCamera: MLNMapCamera, reason: MLNCameraChangeReason) -> Bool {
// Send userDidPan only for gestures that actually change the map center. The reason
// is an option set and gestures can come combined with other reasons, so check for
// containment instead of an exact match.
let centerChangingGestures: MLNCameraChangeReason = [.gesturePan, .gesturePinch, .gestureRotate]
if !reason.isDisjoint(with: centerChangingGestures) {
// Stop following immediately, the camera would fight the gesture otherwise.
stopFollowingMarker()
mapLibreView.configuration.userDidPan?()
}
return true
}
// MARK: Callout
func mapView(_ mapView: MLNMapView, annotationCanShowCallout annotation: MLNAnnotation) -> Bool {
false
}
// MARK: Private
private func updateGeolocationUncertainty(location: MLNUserLocation) {
guard let clLocation = location.location, clLocation.horizontalAccuracy >= 0 else {
mapLibreView.configuration.geolocationUncertainty.wrappedValue = nil
return
}
mapLibreView.configuration.geolocationUncertainty.wrappedValue = clLocation.horizontalAccuracy
}
}
}
@@ -0,0 +1,39 @@
//
// Copyright 2026 Element Creations Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
// Please see LICENSE files in the repository root for full details.
//
import MapLibre
import MapLibreInterface
import SwiftUI
/// The shim's entry point: the app looks this class up by name through the ObjC runtime
/// after `dlopen`-ing the framework, keeping MapLibre off the app's launch path.
@objc(MapLibreShim)
public final class MapLibreShim: NSObject, MapLibreShimProtocol {
public func configureLogging(_ handler: @escaping @Sendable (MapLogSeverity, String) -> Void) {
MLNLoggingConfiguration.shared.loggingLevel = .debug
MLNLoggingConfiguration.shared.handler = { loggingLevel, _, _, message in
switch loggingLevel {
case .error:
handler(.error, message)
case .warning:
handler(.warning, message)
case .info:
handler(.info, message)
case .debug:
handler(.debug, message)
case .verbose:
handler(.verbose, message)
default:
break
}
}
}
public func makeMapView(configuration: MapViewConfiguration) -> AnyView {
AnyView(MapLibreMapView(configuration: configuration))
}
}
@@ -0,0 +1,26 @@
# The interactive map, split out of the app so MapLibre's static initialisers stay off the
# launch path: the app links only the tiny MapLibreInterface framework, while MapLibreShim (the
# only image linking MapLibre) is embedded unlinked and dlopen'd on first map use.
name: MapLibreShim
targets:
MapLibreShim:
type: framework
platform: iOS
settings:
base:
PRODUCT_BUNDLE_IDENTIFIER: $(BASE_BUNDLE_IDENTIFIER).maplibre-shim
GENERATE_INFOPLIST_FILE: YES
SWIFT_VERSION: 6.2
SWIFT_APPROACHABLE_CONCURRENCY: YES
SWIFT_DEFAULT_ACTOR_ISOLATION: MainActor
dependencies:
- target: MapLibreInterface
embed: false
- package: MapLibre
embed: false
sources:
- path: ../Sources