# Mapsgl Apple

> This skill should be used when working with the Xweather MapsGL SDK for Apple platforms (the MapsGL iOS/iPadOS/macCatalyst/visionOS SDK) — installing it via Swift Package Manager, CocoaPods, Carthage or xcframeworks, creating a MapboxMapController or MapLibreMapController, and adding, removing, styling, animating or inspecting MapsGL weather layers in Swift. Use it whenever a task mentions MapsGL…

- **Type:** Skill
- **Install:** `agentstack add skill-vaisala-xweather-xweather-agent-skills-mapsgl-apple`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [vaisala-xweather](https://agentstack.voostack.com/s/vaisala-xweather)
- **Installs:** 0
- **Category:** [Agent Skills](https://agentstack.voostack.com/c/agent-skills)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [vaisala-xweather](https://github.com/vaisala-xweather)
- **Source:** https://github.com/vaisala-xweather/xweather-agent-skills/tree/main/plugins/xweather/skills/mapsgl-apple

## Install

```sh
agentstack add skill-vaisala-xweather-xweather-agent-skills-mapsgl-apple
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# MapsGL for Apple platforms

The Xweather MapsGL SDK for Apple platforms renders weather and custom map data client-side with
Metal, layered on top of **Mapbox Maps** or **MapLibre Native**. It requires an active Xweather
account with Weather API + Maps access (client id + secret).

Platform support comes from the package manifest: **iOS 16+, macCatalyst 16+, visionOS 1+**. There is
no native macOS (AppKit) target — a "macOS" app here means Mac Catalyst.

Docs: https://www.xweather.com/docs/mapsgl-apple-sdk/getting-started ·
Distribution + demo app: https://github.com/vaisala-xweather/mapsgl-apple-sdk

## Ask which map provider unless the context tells you

The map provider is not a stylistic preference that can be defaulted: Mapbox and MapLibre resolve
*different Swift Package branches*, different transitive SDKs, and different map-view types. Getting
it wrong means the code doesn't compile and the package graph has to be redone.

So **infer it when the context actually says, and ask when it doesn't.** Never pick one by default.

**Infer it** from evidence like:

- the user named the provider in their request;
- the project already integrates one — an `import MapsGLMapbox` / `import MapsGLMapLibre`, a resolved
  `mapbox-maps-ios` or `maplibre-gl-native-distribution` dependency, a `Podfile` naming one, an
  `MLNMapView` or `MapboxMaps.MapView` in the source, or a `MBXAccessToken` in an Info.plist;
- the project already uses the provider's SDK elsewhere, even without MapsGL — a Mapbox-based map
  screen means Mapbox.

When you infer, **say which provider you picked and what told you**, so a wrong read is cheap to
correct.

**Ask** when the evidence is absent or contradictory — a greenfield app, a project with no map
dependency yet, or one carrying traces of both. Weak circumstantial signals ("we want a dark map
style", "our designer sent Mapbox screenshots") are not evidence of an integration; ask rather than
build the whole package graph on them.

Trade-offs to offer alongside the question, briefly:

| | Mapbox Maps | MapLibre Native |
|---|---|---|
| Basemap key | Mapbox account access token required, plus a secret token to download the SDK | None — but the style URL's tile provider may need one (CARTO's public styles don't) |
| Cost | Mapbox map-load pricing applies | No basemap vendor cost |
| SwiftUI | Native `Map` view | `MLNMapView` wrapped in a `UIViewRepresentable` |
| MapsGL constraint | Must set the **mercator** projection — the default globe projection is incompatible | None |

## How to write MapsGL Apple examples

**Default to SwiftUI.** Produce UIKit only when the project is UIKit (a `UIViewController`-based app,
storyboards/XIBs, an `AppDelegate`/`SceneDelegate` pair with no SwiftUI `App`) or the user asks for
it. Match the surrounding project over the default whenever the two disagree — including Swift
concurrency style, view-model conventions, and how the app already stores secrets.

**Never hardcode a version number.** Resolve the current release when you need one:

```bash
curl -s https://www.xweather.com/docs/api/releases/versions \
  | python3 -c 'import json,sys; print(json.load(sys.stdin)["products"]["mapsgl-apple-sdk"]["version"])'
```

That endpoint is the release source of truth for every Xweather product, keyed by product id —
`mapsgl-apple-sdk` here, alongside `mapsgl`, `weather-api`, `maps`, and others. It's a small public
JSON document, no auth needed.

Most of the time you don't need a version at all: **prefer the Swift Package branch channels**
(below), which track the latest release without a pin. A version is only needed for a deliberate
pin, a CocoaPods/Carthage requirement, or an API-reference URL.

**Credentials never go in source.** Put the Xweather client id/secret and any Mapbox token in a
gitignored plist, xcconfig, or the keychain — whatever the project already uses — and read them at
runtime. The demo app's `AccessKeys.plist` pattern is a reasonable model when the project has none.
Write `"FILL_IN_WITH_YOUR_CLIENT_ID"`-style placeholders rather than inventing plausible keys.

**Every example must include the Xweather attribution.** It's a requirement of using the product, not
a nicety, so build it into the view rather than mentioning it afterwards. See "Attribution is
required".

## API reference

The full API reference is DocC, published per SDK version:

```
https://cdn.aerisapi.com/sdk/ios/mapsgl/docs/v{version}/documentation/mapsglmaps
```

Substitute the version from the releases endpoint above — **there is no `latest` alias**;
`.../docs/latest/...` 404s. Only the `mapsglmaps` module is published; `MapsGLCore`,
`MapsGLRenderer`, and the two adapter modules have no hosted DocC.

The version index page, which lists every published version, is
https://www.xweather.com/docs/mapsgl-apple-sdk/api-reference.

A machine-readable symbol index sits alongside it at
`https://cdn.aerisapi.com/sdk/ios/mapsgl/docs/v{version}/index/index.json` — useful for checking
whether a symbol exists in a given release before writing code against it.

`references/api-reference.md` carries the surface an agent needs most (controller, service, timeline,
controls, descriptors) so the common cases need no network call.

## Core concepts

| Concept | What it is |
|---|---|
| `XweatherAccount(id:secret:)` | Wraps client id/secret credentials used for all data requests |
| `MapController` | Adapter between the underlying map (`MapboxMaps.MapboxMap` / `MLNMapView`) and MapsGL — the object almost everything below is called on. Concrete: `MapboxMapController`, `MapLibreMapController` |
| `WeatherService` | The account-bound weather data service, reachable as `controller.service`. Namespaces every built-in layer configuration and `WeatherService.LayerCode` |
| `WeatherService.LayerCode` | Enum identifying a built-in weather layer — `.radar`, `.temperatures`, `.windParticles` |
| `WeatherService.` | Per-layer configuration struct (`WeatherService.Temperatures`) holding `layer`, `legend`, and `presentation`. Instantiate it to override defaults |
| Source descriptors | Where custom layer data comes from — `ImageSourceDescriptor`, `EncodedSourceDescriptor`, `VectorSourceDescriptor`, `GeoJSONSourceDescriptor` |
| Layer descriptors | How data is rendered — `RasterLayerDescriptor`, `SampleLayerDescriptor`, `ParticleLayerDescriptor`, `GridLayerDescriptor`, `ContourLayerDescriptor`, `FillLayerDescriptor`, `LineLayerDescriptor`, `CircleLayerDescriptor`, `SymbolLayerDescriptor`, `HeatmapLayerDescriptor` |
| `paint` | Per-descriptor style config, namespaced by render type — `paint.sample`, `paint.fill`, `paint.stroke`. See `references/styles.md` |
| `Expression` | Data-driven paint values and layer filters, built with static factories — `Expression.get("COLOR")`. See `references/expressions.md` |
| `ColorScaleOptions` / `ColorStop` | Maps a continuous data range to colors, used by `paint.sample.colorScale` and bar legends |
| `LegendControl` | Manages the legends visible on a map; auto-syncs with built-in weather layers. See `references/legends.md` |
| `DataInspectorControl` | Tap-to-inspect callout showing raw layer values at a coordinate |
| `controller.timeline` | Drives time animation across every animated layer at once. See `references/timeline.md` |

Built-in **weather layers** are pre-wired combinations of a source + styled layer(s), addressed by a
single `LayerCode` case. Prefer these over hand-building sources and layers unless visualizing custom
or non-weather data.

## Setup

### 1. Credentials

Two independent sets, both required:

1. **Xweather account keys** — `CLIENT_ID` / `CLIENT_SECRET` from
   https://data.portal.xweather.com/account/keys. Passed as `XweatherAccount(id:secret:)`.
2. **The map provider's own credentials** —
   - Mapbox → a public access token set on `MapboxOptions.accessToken`, **plus** a secret download
     token configured in `~/.netrc` so SPM/CocoaPods can fetch the Mapbox SDK at all. The secret
     token is a build-time requirement; forgetting it fails resolution, not runtime.
   - MapLibre → nothing for the SDK. The basemap `styleURL` points at a tile provider, which may need
     its own key (CARTO's public Positron/Dark Matter styles do not).

If nothing renders, check both sets before investigating MapsGL.

### 2. Install

**Swift Package Manager (preferred).** Add `https://github.com/vaisala-xweather/mapsgl-apple-sdk` and
pick the **branch matching your provider** — the package manifest at the repo root is
provider-specific per branch, so the branch *is* the provider choice:

| Channel | Branch | Resolves | Product |
|---|---|---|---|
| Latest Mapbox | `master` | `mapbox-maps-ios` 11.x | `MapsGL` |
| Latest MapLibre | `maplibre` | `maplibre-gl-native-distribution` 6.18+ | `MapsGL` |
| Pinned Mapbox | `release/x.y.z` | as above, frozen | `MapsGL` |
| Pinned MapLibre | `release/maplibre/x.y.z` | as above, frozen | `MapsGL` |

The product name is `MapsGL` on every branch; what differs is which adapter target it includes. Add
the `MapsGL` library product to the app target. Xcode resolves the three binary xcframeworks
(`MapsGLCore`, `MapsGLRenderer`, `MapsGLMaps`) plus the provider SDK and `turf-swift` automatically.

Use a branch channel unless the user asked to pin. Pinning to `release/…` is the right call for
release-managed apps; note that it also freezes the provider SDK range.

**CocoaPods** — `pod 'MapsGL'`, then `pod install` and open the generated `.xcworkspace`. CocoaPods
builds a single `MapsGL` module, so **`import MapsGL` replaces the adapter import**
(`import MapsGLMapbox` / `import MapsGLMapLibre`) in every source file. This is the most common
CocoaPods build error.

**Carthage** (`github "vaisala-xweather/mapsgl-apple-sdk" ~> x.y.z`, then
`carthage update --use-xcframeworks`) and **manual xcframework embedding** (download `MapsGL.zip`
from the releases page, embed the three xcframeworks as "Embed & Sign") both require adding the
provider SDK yourself and dropping the matching adapter *source directory*
(`MapsGLMapbox/` or `MapsGLMapLibre/`) straight into the project. When the adapter is compiled into
your target that way, **remove the `import MapsGLMapbox` / `import MapsGLMapLibre` lines** — the
types are already in your module.

### 3. Imports

```swift
import MapsGLMaps      // always
import MapsGLMapbox    // SPM, Mapbox channel   — omit for CocoaPods/Carthage/manual
import MapsGLMapLibre  // SPM, MapLibre channel — omit for CocoaPods/Carthage/manual
import MapsGL          // CocoaPods only, in place of the adapter import
import Combine         // controller events return AnyCancellable
```

### 4. Create the controller, then wait for load

```swift
let account = XweatherAccount(id: clientID, secret: clientSecret)
let controller = MapboxMapController(map: map, account: account)

controller.onLoad.observe { _ in
    _ = try? controller.addWeatherLayer(for: .radar)
}.store(in: &cancellables)
```

| Provider | Controller | Map argument |
|---|---|---|
| Mapbox | `MapboxMapController` | `MapboxMaps.MapView`, or `MapboxMaps.MapboxMap` + `window:` |
| MapLibre | `MapLibreMapController` | `MLNMapView` |

**Every layer/source call must be gated behind load**, and the observation returns an `AnyCancellable`
you must retain — `.store(in: &cancellables)`. Drop it and the observer is torn down immediately and
the map stays empty, with no error.

Use `onLoad.observe { … }`. The older `subscribe(to: MapEvents.Load.self) { … }` is **deprecated**
("Use available on.observe() methods instead") — note that the web docs' SwiftUI sample still
shows it, so copying from there warns on a new project. Same for `asyncSubscribe` and
`subscribeToNext`; only `publisher(for:)` survives, for when you want Combine operators.

**The layer and source API is `@MainActor`.** `addWeatherLayer`, `removeWeatherLayer`,
`setWeatherLayerVisibility`, `weatherLayer(for:)`, `addSource`, `addLayer`, `addImage`, and
`add(legendControl:)` are all main-actor-isolated. The `onLoad` observer already runs on the main
thread, so the usual path needs nothing extra — but a call made from a detached task or a
non-isolated callback needs `await MainActor.run { … }`, or it won't compile under strict
concurrency.

**Mapbox requires the mercator projection.** The current Mapbox styles (streets/outdoor/satellite
streets v12, light/dark v11) default to the globe projection, which MapsGL cannot render onto:

```swift
try map.setProjection(.init(name: .mercator))
```

Symptom when missing: the basemap draws normally and MapsGL layers simply never appear.

## Complete example — SwiftUI

**Mapbox.** `MapReader` hands back a `MapboxMap`, so use the `window:`-taking initializer:

```swift
import SwiftUI
import Combine
import MapboxMaps
import MapsGLMaps
import MapsGLMapbox

struct WeatherMapView: View {
    // Read these from a gitignored plist / xcconfig / keychain — never commit them.
    private let xweatherClientID = "FILL_IN_WITH_YOUR_CLIENT_ID"
    private let xweatherClientSecret = "FILL_IN_WITH_YOUR_CLIENT_SECRET"

    final class Coordinator: ObservableObject {
        var controller: MapboxMapController?
        var cancellables: Set = []
    }
    @StateObject private var coordinator = Coordinator()

    var body: some View {
        MapReader { proxy in
            Map(initialViewport: .camera(
                center: CLLocationCoordinate2D(latitude: 39.65, longitude: -93.10),
                zoom: 3.5
            ))
            .mapStyle(.light)
            .ignoresSafeArea()
            .overlay(alignment: .bottomTrailing) { XweatherAttribution() }
            .onAppear {
                guard let map = proxy.map, coordinator.controller == nil else { return }

                // MapsGL cannot render onto Mapbox's default globe projection.
                try? map.setProjection(.init(name: .mercator))

                let controller = MapboxMapController(
                    map: map,
                    window: UIWindow?.none,
                    account: XweatherAccount(id: xweatherClientID, secret: xweatherClientSecret)
                )
                coordinator.controller = controller

                controller.onLoad.observe { _ in
                    do {
                        try controller.addWeatherLayer(for: .radar)

                        var winds = WeatherService.WindParticles(service: controller.service)
                        winds.layer.paint.particle.density = .high
                        try controller.addWeatherLayer(config: winds)
                    } catch {
                        NSLog("Failed to add weather layer: \(error)")
                    }
                }.store(in: &coordinator.cancellables)
            }
        }
    }
}
```

Set the Mapbox token once, before any map is created — in the `App` initializer or an
`@main` type's `init()`:

```swift
MapboxOptions.accessToken = "FILL_IN_WITH_YOUR_MAPBOX_PUBLIC_ACCESS_TOKEN"
```

**MapLibre.** MapLibre ships no SwiftUI view, so wrap `MLNMapView`. No token, and no projection call:

```swift
import SwiftUI
import Combine
import MapLibre
import MapsGLMaps
import MapsGLMapLibre

struct WeatherMapView: UIViewRepresentable {
    private let xweatherClientID = "FILL_IN_WITH_YOUR_CLIENT_ID"
    private let xweatherClientSecret = "FILL_IN_WITH_YOUR_CLIENT_SECRET"

    final class Coordinator {
        var controller: MapLibreMapController?
        var cancellables: Set = []
    }
    func makeCoordinator() -> Coordinator { Coordinator() }

    func makeUIView(context: Context) -> MLNMapView {
        let mapView = MLNMapView(frame: .zero)
        // Any MapLibre-compatible style. CARTO's public styles need no key.
        mapView.styleURL = URL(string: "https://basemaps.cartocdn.com/gl/positron-gl-style/style.json")!
        mapView.setCenter(
            CLLocationCoordinate2D(latitude: 39.65, longitude: -93.10),
            zoomLevel: 3.5,
            animated: false
        )

        let controller = MapLi

…

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [vaisala-xweather](https://github.com/vaisala-xweather)
- **Source:** [vaisala-xweather/xweather-agent-skills](https://github.com/vaisala-xweather/xweather-agent-skills)
- **License:** MIT

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** yes
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** no
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-vaisala-xweather-xweather-agent-skills-mapsgl-apple
- Seller: https://agentstack.voostack.com/s/vaisala-xweather
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
