← Vaisala Xweather API & MapsCONTENT HISTORYWHAT CHANGED · RULE-BASED ANALYSIS
Update to Vaisala Xweather API & Maps
Snapshot Sep 30, 2026 · 23:15 UTC · version 0.14.1
Collection source: not recorded for this historical snapshot.
First saved snapshot
No earlier snapshot is available to establish a change.
Compare saved observations
Download comparison JSONFull technical diff · 0 changed fields
Full snapshot data
{
"name": "mapsgl-apple",
"description": "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 on iOS or Apple platforms, mapsgl-apple-sdk, MapsGLMaps, MapsGLMapbox, MapsGLMapLibre, XweatherAccount, WeatherService.LayerCode, addWeatherLayer in Swift, or a native weather map in SwiftUI or UIKit. Also use it for questions about MapsGL session usage or cost in an Apple app — sessions, the 5-minute clock intervals, the access multiplier, and which view and app lifecycle events should attach and detach weather layers. Also covers Xweather's attribution requirement — the 'Powered by Vaisala Xweather' credit and logo rules that apply wherever Xweather data or imagery is displayed.",
"included_files": [
{
"relative_path": "references/api-reference.md",
"size_in_bytes": 19036
},
{
"relative_path": "references/expressions.md",
"size_in_bytes": 9301
},
{
"relative_path": "references/layers.md",
"size_in_bytes": 15398
},
{
"relative_path": "references/legends.md",
"size_in_bytes": 10561
},
{
"relative_path": "references/sessions.md",
"size_in_bytes": 4696
},
{
"relative_path": "references/setup.md",
"size_in_bytes": 15018
},
{
"relative_path": "references/styles.md",
"size_in_bytes": 14304
},
{
"relative_path": "references/timeline.md",
"size_in_bytes": 9234
}
],
"skill_md_contents": "---\nname: mapsgl-apple\ndescription: 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 on iOS or Apple platforms, mapsgl-apple-sdk, MapsGLMaps, MapsGLMapbox, MapsGLMapLibre, XweatherAccount, WeatherService.LayerCode, addWeatherLayer in Swift, or a native weather map in SwiftUI or UIKit. Also use it for questions about MapsGL session usage or cost in an Apple app — sessions, the 5-minute clock intervals, the access multiplier, and which view and app lifecycle events should attach and detach weather layers. Also covers Xweather's attribution requirement — the 'Powered by Vaisala Xweather' credit and logo rules that apply wherever Xweather data or imagery is displayed.\nlicense: MIT\n---\n\n# MapsGL for Apple platforms\n\nThe Xweather MapsGL SDK for Apple platforms renders weather and custom map data client-side with\nMetal, layered on top of **Mapbox Maps** or **MapLibre Native**. It requires an active Xweather\naccount with Weather API + Maps access (client id + secret).\n\nPlatform support comes from the package manifest: **iOS 16+, macCatalyst 16+, visionOS 1+**. There is\nno native macOS (AppKit) target — a \"macOS\" app here means Mac Catalyst.\n\nDocs: https://www.xweather.com/docs/mapsgl-apple-sdk/getting-started ·\nDistribution + demo app: https://github.com/vaisala-xweather/mapsgl-apple-sdk\n\n## Ask which map provider unless the context tells you\n\nThe map provider is not a stylistic preference that can be defaulted: Mapbox and MapLibre resolve\n*different Swift Package branches*, different transitive SDKs, and different map-view types. Getting\nit wrong means the code doesn't compile and the package graph has to be redone.\n\nSo **infer it when the context actually says, and ask when it doesn't.** Never pick one by default.\n\n**Infer it** from evidence like:\n\n- the user named the provider in their request;\n- the project already integrates one — an `import MapsGLMapbox` / `import MapsGLMapLibre`, a resolved\n `mapbox-maps-ios` or `maplibre-gl-native-distribution` dependency, a `Podfile` naming one, an\n `MLNMapView` or `MapboxMaps.MapView` in the source, or a `MBXAccessToken` in an Info.plist;\n- the project already uses the provider's SDK elsewhere, even without MapsGL — a Mapbox-based map\n screen means Mapbox.\n\nWhen you infer, **say which provider you picked and what told you**, so a wrong read is cheap to\ncorrect.\n\n**Ask** when the evidence is absent or contradictory — a greenfield app, a project with no map\ndependency yet, or one carrying traces of both. Weak circumstantial signals (\"we want a dark map\nstyle\", \"our designer sent Mapbox screenshots\") are not evidence of an integration; ask rather than\nbuild the whole package graph on them.\n\nTrade-offs to offer alongside the question, briefly:\n\n| | Mapbox Maps | MapLibre Native |\n|---|---|---|\n| 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) |\n| Cost | Mapbox map-load pricing applies | No basemap vendor cost |\n| SwiftUI | Native `Map` view | `MLNMapView` wrapped in a `UIViewRepresentable` |\n| MapsGL constraint | Must set the **mercator** projection — the default globe projection is incompatible | None |\n\n## How to write MapsGL Apple examples\n\n**Default to SwiftUI.** Produce UIKit only when the project is UIKit (a `UIViewController`-based app,\nstoryboards/XIBs, an `AppDelegate`/`SceneDelegate` pair with no SwiftUI `App`) or the user asks for\nit. Match the surrounding project over the default whenever the two disagree — including Swift\nconcurrency style, view-model conventions, and how the app already stores secrets.\n\n**Never hardcode a version number.** Resolve the current release when you need one:\n\n```bash\ncurl -s https://www.xweather.com/docs/api/releases/versions \\\n | python3 -c 'import json,sys; print(json.load(sys.stdin)[\"products\"][\"mapsgl-apple-sdk\"][\"version\"])'\n```\n\nThat endpoint is the release source of truth for every Xweather product, keyed by product id —\n`mapsgl-apple-sdk` here, alongside `mapsgl`, `weather-api`, `maps`, and others. It's a small public\nJSON document, no auth needed.\n\nMost of the time you don't need a version at all: **prefer the Swift Package branch channels**\n(below), which track the latest release without a pin. A version is only needed for a deliberate\npin, a CocoaPods/Carthage requirement, or an API-reference URL.\n\n**Credentials never go in source.** Put the Xweather client id/secret and any Mapbox token in a\ngitignored plist, xcconfig, or the keychain — whatever the project already uses — and read them at\nruntime. The demo app's `AccessKeys.plist` pattern is a reasonable model when the project has none.\nWrite `\"FILL_IN_WITH_YOUR_CLIENT_ID\"`-style placeholders rather than inventing plausible keys.\n\n**Every example must include the Xweather attribution.** It's a requirement of using the product, not\na nicety, so build it into the view rather than mentioning it afterwards. See \"Attribution is\nrequired\".\n\n## API reference\n\nThe full API reference is DocC, published per SDK version:\n\n```\nhttps://cdn.aerisapi.com/sdk/ios/mapsgl/docs/v{version}/documentation/mapsglmaps\n```\n\nSubstitute the version from the releases endpoint above — **there is no `latest` alias**;\n`.../docs/latest/...` 404s. Only the `mapsglmaps` module is published; `MapsGLCore`,\n`MapsGLRenderer`, and the two adapter modules have no hosted DocC.\n\nThe version index page, which lists every published version, is\nhttps://www.xweather.com/docs/mapsgl-apple-sdk/api-reference.\n\nA machine-readable symbol index sits alongside it at\n`https://cdn.aerisapi.com/sdk/ios/mapsgl/docs/v{version}/index/index.json` — useful for checking\nwhether a symbol exists in a given release before writing code against it.\n\n`references/api-reference.md` carries the surface an agent needs most (controller, service, timeline,\ncontrols, descriptors) so the common cases need no network call.\n\n## Core concepts\n\n| Concept | What it is |\n|---|---|\n| `XweatherAccount(id:secret:)` | Wraps client id/secret credentials used for all data requests |\n| `MapController` | Adapter between the underlying map (`MapboxMaps.MapboxMap` / `MLNMapView`) and MapsGL — the object almost everything below is called on. Concrete: `MapboxMapController`, `MapLibreMapController` |\n| `WeatherService` | The account-bound weather data service, reachable as `controller.service`. Namespaces every built-in layer configuration and `WeatherService.LayerCode` |\n| `WeatherService.LayerCode` | Enum identifying a built-in weather layer — `.radar`, `.temperatures`, `.windParticles` |\n| `WeatherService.<Name>` | Per-layer configuration struct (`WeatherService.Temperatures`) holding `layer`, `legend`, and `presentation`. Instantiate it to override defaults |\n| Source descriptors | Where custom layer data comes from — `ImageSourceDescriptor`, `EncodedSourceDescriptor`, `VectorSourceDescriptor`, `GeoJSONSourceDescriptor` |\n| Layer descriptors | How data is rendered — `RasterLayerDescriptor`, `SampleLayerDescriptor`, `ParticleLayerDescriptor`, `GridLayerDescriptor`, `ContourLayerDescriptor`, `FillLayerDescriptor`, `LineLayerDescriptor`, `CircleLayerDescriptor`, `SymbolLayerDescriptor`, `HeatmapLayerDescriptor` |\n| `paint` | Per-descriptor style config, namespaced by render type — `paint.sample`, `paint.fill`, `paint.stroke`. See `references/styles.md` |\n| `Expression` | Data-driven paint values and layer filters, built with static factories — `Expression.get(\"COLOR\")`. See `references/expressions.md` |\n| `ColorScaleOptions` / `ColorStop` | Maps a continuous data range to colors, used by `paint.sample.colorScale` and bar legends |\n| `LegendControl` | Manages the legends visible on a map; auto-syncs with built-in weather layers. See `references/legends.md` |\n| `DataInspectorControl` | Tap-to-inspect callout showing raw layer values at a coordinate |\n| `controller.timeline` | Drives time animation across every animated layer at once. See `references/timeline.md` |\n\nBuilt-in **weather layers** are pre-wired combinations of a source + styled layer(s), addressed by a\nsingle `LayerCode` case. Prefer these over hand-building sources and layers unless visualizing custom\nor non-weather data.\n\n## Setup\n\n### 1. Credentials\n\nTwo independent sets, both required:\n\n1. **Xweather account keys** — `CLIENT_ID` / `CLIENT_SECRET` from\n https://data.portal.xweather.com/account/keys. Passed as `XweatherAccount(id:secret:)`.\n2. **The map provider's own credentials** —\n - Mapbox → a public access token set on `MapboxOptions.accessToken`, **plus** a secret download\n token configured in `~/.netrc` so SPM/CocoaPods can fetch the Mapbox SDK at all. The secret\n token is a build-time requirement; forgetting it fails resolution, not runtime.\n - MapLibre → nothing for the SDK. The basemap `styleURL` points at a tile provider, which may need\n its own key (CARTO's public Positron/Dark Matter styles do not).\n\nIf nothing renders, check both sets before investigating MapsGL.\n\n### 2. Install\n\n**Swift Package Manager (preferred).** Add `https://github.com/vaisala-xweather/mapsgl-apple-sdk` and\npick the **branch matching your provider** — the package manifest at the repo root is\nprovider-specific per branch, so the branch *is* the provider choice:\n\n| Channel | Branch | Resolves | Product |\n|---|---|---|---|\n| Latest Mapbox | `master` | `mapbox-maps-ios` 11.x | `MapsGL` |\n| Latest MapLibre | `maplibre` | `maplibre-gl-native-distribution` 6.18+ | `MapsGL` |\n| Pinned Mapbox | `release/x.y.z` | as above, frozen | `MapsGL` |\n| Pinned MapLibre | `release/maplibre/x.y.z` | as above, frozen | `MapsGL` |\n\nThe product name is `MapsGL` on every branch; what differs is which adapter target it includes. Add\nthe `MapsGL` library product to the app target. Xcode resolves the three binary xcframeworks\n(`MapsGLCore`, `MapsGLRenderer`, `MapsGLMaps`) plus the provider SDK and `turf-swift` automatically.\n\nUse a branch channel unless the user asked to pin. Pinning to `release/…` is the right call for\nrelease-managed apps; note that it also freezes the provider SDK range.\n\n**CocoaPods** — `pod 'MapsGL'`, then `pod install` and open the generated `.xcworkspace`. CocoaPods\nbuilds a single `MapsGL` module, so **`import MapsGL` replaces the adapter import**\n(`import MapsGLMapbox` / `import MapsGLMapLibre`) in every source file. This is the most common\nCocoaPods build error.\n\n**Carthage** (`github \"vaisala-xweather/mapsgl-apple-sdk\" ~> x.y.z`, then\n`carthage update --use-xcframeworks`) and **manual xcframework embedding** (download `MapsGL.zip`\nfrom the releases page, embed the three xcframeworks as \"Embed & Sign\") both require adding the\nprovider SDK yourself and dropping the matching adapter *source directory*\n(`MapsGLMapbox/` or `MapsGLMapLibre/`) straight into the project. When the adapter is compiled into\nyour target that way, **remove the `import MapsGLMapbox` / `import MapsGLMapLibre` lines** — the\ntypes are already in your module.\n\n### 3. Imports\n\n```swift\nimport MapsGLMaps // always\nimport MapsGLMapbox // SPM, Mapbox channel — omit for CocoaPods/Carthage/manual\nimport MapsGLMapLibre // SPM, MapLibre channel — omit for CocoaPods/Carthage/manual\nimport MapsGL // CocoaPods only, in place of the adapter import\nimport Combine // controller events return AnyCancellable\n```\n\n### 4. Create the controller, then wait for load\n\n```swift\nlet account = XweatherAccount(id: clientID, secret: clientSecret)\nlet controller = MapboxMapController(map: map, account: account)\n\ncontroller.onLoad.observe { _ in\n _ = try? controller.addWeatherLayer(for: .radar)\n}.store(in: &cancellables)\n```\n\n| Provider | Controller | Map argument |\n|---|---|---|\n| Mapbox | `MapboxMapController` | `MapboxMaps.MapView`, or `MapboxMaps.MapboxMap` + `window:` |\n| MapLibre | `MapLibreMapController` | `MLNMapView` |\n\n**Every layer/source call must be gated behind load**, and the observation returns an `AnyCancellable`\nyou must retain — `.store(in: &cancellables)`. Drop it and the observer is torn down immediately and\nthe map stays empty, with no error.\n\nUse `onLoad.observe { … }`. The older `subscribe(to: MapEvents.Load.self) { … }` is **deprecated**\n(\"Use available on<event>.observe() methods instead\") — note that the web docs' SwiftUI sample still\nshows it, so copying from there warns on a new project. Same for `asyncSubscribe` and\n`subscribeToNext`; only `publisher(for:)` survives, for when you want Combine operators.\n\n**The layer and source API is `@MainActor`.** `addWeatherLayer`, `removeWeatherLayer`,\n`setWeatherLayerVisibility`, `weatherLayer(for:)`, `addSource`, `addLayer`, `addImage`, and\n`add(legendControl:)` are all main-actor-isolated. The `onLoad` observer already runs on the main\nthread, so the usual path needs nothing extra — but a call made from a detached task or a\nnon-isolated callback needs `await MainActor.run { … }`, or it won't compile under strict\nconcurrency.\n\n**Mapbox requires the mercator projection.** The current Mapbox styles (streets/outdoor/satellite\nstreets v12, light/dark v11) default to the globe projection, which MapsGL cannot render onto:\n\n```swift\ntry map.setProjection(.init(name: .mercator))\n```\n\nSymptom when missing: the basemap draws normally and MapsGL layers simply never appear.\n\n## Complete example — SwiftUI\n\n**Mapbox.** `MapReader` hands back a `MapboxMap`, so use the `window:`-taking initializer:\n\n```swift\nimport SwiftUI\nimport Combine\nimport MapboxMaps\nimport MapsGLMaps\nimport MapsGLMapbox\n\nstruct WeatherMapView: View {\n // Read these from a gitignored plist / xcconfig / keychain — never commit them.\n private let xweatherClientID = \"FILL_IN_WITH_YOUR_CLIENT_ID\"\n private let xweatherClientSecret = \"FILL_IN_WITH_YOUR_CLIENT_SECRET\"\n\n final class Coordinator: ObservableObject {\n var controller: MapboxMapController?\n var cancellables: Set<AnyCancellable> = []\n }\n @StateObject private var coordinator = Coordinator()\n\n var body: some View {\n MapReader { proxy in\n Map(initialViewport: .camera(\n center: CLLocationCoordinate2D(latitude: 39.65, longitude: -93.10),\n zoom: 3.5\n ))\n .mapStyle(.light)\n .ignoresSafeArea()\n .overlay(alignment: .bottomTrailing) { XweatherAttribution() }\n .onAppear {\n guard let map = proxy.map, coordinator.controller == nil else { return }\n\n // MapsGL cannot render onto Mapbox's default globe projection.\n try? map.setProjection(.init(name: .mercator))\n\n let controller = MapboxMapController(\n map: map,\n window: UIWindow?.none,\n account: XweatherAccount(id: xweatherClientID, secret: xweatherClientSecret)\n )\n coordinator.controller = controller\n\n controller.onLoad.observe { _ in\n do {\n try controller.addWeatherLayer(for: .radar)\n\n var winds = WeatherService.WindParticles(service: controller.service)\n winds.layer.paint.particle.density = .high\n try controller.addWeatherLayer(config: winds)\n } catch {\n NSLog(\"Failed to add weather layer: \\(error)\")\n }\n }.store(in: &coordinator.cancellables)\n }\n }\n }\n}\n```\n\nSet the Mapbox token once, before any map is created — in the `App` initializer or an\n`@main` type's `init()`:\n\n```swift\nMapboxOptions.accessToken = \"FILL_IN_WITH_YOUR_MAPBOX_PUBLIC_ACCESS_TOKEN\"\n```\n\n**MapLibre.** MapLibre ships no SwiftUI view, so wrap `MLNMapView`. No token, and no projection call:\n\n```swift\nimport SwiftUI\nimport Combine\nimport MapLibre\nimport MapsGLMaps\nimport MapsGLMapLibre\n\nstruct WeatherMapView: UIViewRepresentable {\n private let xweatherClientID = \"FILL_IN_WITH_YOUR_CLIENT_ID\"\n private let xweatherClientSecret = \"FILL_IN_WITH_YOUR_CLIENT_SECRET\"\n\n final class Coordinator {\n var controller: MapLibreMapController?\n var cancellables: Set<AnyCancellable> = []\n }\n func makeCoordinator() -> Coordinator { Coordinator() }\n\n func makeUIView(context: Context) -> MLNMapView {\n let mapView = MLNMapView(frame: .zero)\n // Any MapLibre-compatible style. CARTO's public styles need no key.\n mapView.styleURL = URL(string: \"https://basemaps.cartocdn.com/gl/positron-gl-style/style.json\")!\n mapView.setCenter(\n CLLocationCoordinate2D(latitude: 39.65, longitude: -93.10),\n zoomLevel: 3.5,\n animated: false\n )\n\n let controller = MapLibreMapController(\n map: mapView,\n account: XweatherAccount(id: xweatherClientID, secret: xweatherClientSecret)\n )\n context.coordinator.controller = controller\n\n controller.onLoad.observe { _ in\n _ = try? controller.addWeatherLayer(for: .radar)\n }.store(in: &context.coordinator.cancellables)\n\n return mapView\n }\n\n func updateUIView(_ uiView: MLNMapView, context: Context) {}\n}\n```\n\nPlace the attribution over either map — required in both cases:\n\n```swift\nstruct XweatherAttribution: View {\n var body: some View {\n Link(destination: URL(string: \"https://www.xweather.com/\")!) {\n Text(\"Powered by Vaisala Xweather\")\n .font(.caption2)\n .padding(.horizontal, 6).padding(.vertical, 3)\n .background(.thinMaterial, in: RoundedRectangle(cornerRadius: 3))\n }\n .padding(8)\n }\n}\n```\n\nFor UIKit, the shape is the same — build the map view in `viewDidLoad`, create the controller, and\nobserve `onLoad`. A worked UIKit example for both providers is in `references/setup.md`, and the\nrepo's `Demo/UIKit/MapViewController.swift` is the maintained version.\n\nThe demo app is the best full reference and covers both channels:\nhttps://github.com/vaisala-xweather/mapsgl-apple-sdk/tree/master/Demo\n\n## Weather layers\n\n```swift\ntry controller.addWeatherLayer(for: .temperatures)\ntry controller.addWeatherLayer(for: .windParticles)\n\nfor code in [WeatherService.LayerCode.dewPoints, .windParticles] {\n try controller.addWeatherLayer(for: code)\n}\n\ncontroller.hasWeatherLayer(for: .radar) // Bool\ncontroller.weatherLayer(for: .temperatures) // (any MapsGLLayer)?\ncontroller.setWeatherLayerVisibility(for: .radar, visible: false) // cheap toggle\ncontroller.removeWeatherLayer(for: .radar) // frees resources\ncontroller.weatherLayerIds // [String] of active layer ids\n```\n\nOnly `addWeatherLayer` throws. `removeWeatherLayer` and `setWeatherLayerVisibility` do **not** —\nsome doc pages show `try removeWeatherLayer(…)`, which won't compile.\n\nFor toggling a layer on and off from UI, use `setWeatherLayerVisibility` rather than\nremove/re-add — the source and layer resources stay loaded instead of being disposed and rebuilt.\n\n**A layer code is not a layer id.** Weather layers get a generated id that accounts for any\ncustomization, so `getLayer(id:)` won't find one by code. Use `weatherLayer(for:)`, and use the\nreturned layer's `.id` when you need a real id — e.g. to insert another layer relative to it:\n\n```swift\nif let temps = controller.weatherLayer(for: .temperatures) {\n try controller.addWeatherLayer(for: .windParticles, beforeId: temps.id)\n}\n```\n\n**Never guess a layer code.** `references/layers.md` lists every `LayerCode` case with the\nconfiguration struct and descriptor type for each — grep it first, no network call needed. The Swift\ncase names are *not* transforms of the JS/Raster Maps codes (`air-quality-pm2p5` is\n`.particulateMatter2p5Micron`), and the Apple SDK supports fewer layers than the MapsGL JavaScript\nSDK, so a code\nthat works on the web may not exist here at all.\n\nFor descriptions, animatability, coverage, data range and cost multiplier — data attributes, identical\nacross SDKs — see https://www.xweather.com/docs/mapsgl/weather-layers. For what the authenticated\naccount can actually render, ask at runtime:\n\n```swift\ncontroller.service.loadLayerMetadata { result in\n if case .success(let metadata) = result { /* [WeatherLayerMetadata] */ }\n}\n```\n\n## Styling\n\nOverride a built-in layer by instantiating its configuration struct, mutating `layer.paint`, and\nadding it with `addWeatherLayer(config:)`:\n\n```swift\nvar config = WeatherService.Temperatures(service: controller.service)\nconfig.layer.paint.opacity = 0.5 // on the layer paint — NOT paint.sample.opacity\nconfig.layer.quality = .low\ntry controller.addWeatherLayer(config: config)\n```\n\n**`opacity` lives on the layer paint, not inside the render-type namespace.**\n`paint.sample.opacity` doesn't compile — `SamplePaint` has no such member, though the web\ndocumentation shows it. Same for the other encoded types: it's `paint.opacity` everywhere.\n\n`addWeatherLayer(for:)` takes no overrides — a customized layer must go through\n`addWeatherLayer(config:)`.\n\nWhich `paint` sub-object to reach for is set by the layer's descriptor type: a `SampleLayerDescriptor`\nstyles through `paint.sample`, a `LineLayerDescriptor` through `paint.stroke`. `references/layers.md`\ngives the descriptor per layer; `references/styles.md` gives the property tables per render type.\n\nCustom color scale — **stop values are in metric units**, always, regardless of display units:\n\n```swift\nvar scale = ColorScaleOptions(stops: [\n ColorStop(-17.78, .fromString(\"#464ab5\")), // 0 °F\n ColorStop(0.00, .fromString(\"#6bea99\")), // 32 °F\n ColorStop(15.56, .fromString(\"#fdff87\")), // 60 °F\n ColorStop(37.78, .fromString(\"#901436\")), // 100 °F\n])\nscale.interval = 2.775 // hard steps every 5 °F; omit for a smooth gradient\nscale.interpolate = false // and disable interpolation for categorical bands\n\nvar config = WeatherService.Temperatures(service: controller.service)\nconfig.layer.paint.sample.colorScale = .colorScale(scale)\nconfig.layer.paint.sample.drawRange = ...2.22 // only draw ≤ 36 °F\ntry controller.addWeatherLayer(config: config)\n```\n\n`DataQuality` on Apple has **four** cases — `.low`, `.medium`, `.high`, `.exact`. The web docs table\nalso lists `minimal` and `normal`; those are JS-only and don't compile here. Lower quality means fewer\ntile requests and smoother, less detailed output — a good lever on constrained devices.\n\nUse `Expression` for anything data-driven, on both weather and custom layers:\n\n```swift\npaint: .init(fill: .init(color: .expression(Expression.get(\"COLOR\"))))\n```\n\nSee `references/expressions.md` for the operator set and `references/styles.md` for filters and masks.\n\n## Custom sources and layers\n\nAdd the source first, then a layer referencing it by id:\n\n```swift\nvar source = VectorSourceDescriptor(id: \"alerts\")\nsource.url = URL(string: \"https://maps{s}.aerisapi.com/[CLIENT_ID]_[CLIENT_SECRET]/alerts/{z}/{x}/{y}/0.pbf\")\nsource.zoomRange = 4...8\n_ = try controller.addSource(source)\n\nlet layer = FillLayerDescriptor(\n id: \"alerts-fill\",\n source: source.id,\n paint: .init(\n fill: .init(color: .expression(Expression.get(\"COLOR\"))),\n stroke: .init(color: .constant(.black))\n )\n)\n_ = try controller.addLayer(layer)\n\ncontroller.removeLayer(id: \"alerts-fill\")\ncontroller.removeSource(id: \"alerts\") // only after no layers reference it\n```\n\nFor a layer you created, the id you chose *is* the real layer id, so `getLayer(id:)` works directly.\nFull source and descriptor options: `references/api-reference.md`.\n\n## Animating over time\n\n`controller.timeline` drives every animated layer at once:\n\n```swift\ncontroller.timeline.setStartDate(usingRelativeTime: \"-3 hours\")\ncontroller.timeline.setEndDate(usingRelativeTime: \"now\")\ncontroller.timeline.duration = 2 // seconds per animation loop\ncontroller.timeline.endDelay = 1 // seconds held on the last frame\ncontroller.timeline.play()\n```\n\nFull API — offsets, relative-time strings, `goTo`, playback state, and the `onAdvance` signal for\ndriving a scrubber — in `references/timeline.md`.\n\n## Legends and data inspection\n\n```swift\nlet legendControl = LegendControl()\ncontroller.add(legendControl: legendControl) // built-in weather legends sync automatically\n\nlet inspector = controller.addDataInspectorControl(constrainedTo: mapView)\n```\n\nBoth controls are UIKit views; you place them yourself. In SwiftUI use the provided wrappers instead\nof hosting the raw views:\n\n```swift\nLegendControlView(mapControllerProvider: { coordinator.controller })\n .frame(maxWidth: 300)\n\nsomeMapView.dataInspectorOverlay(mapControllerProvider: { coordinator.controller })\n```\n\n**If you override a layer's colors, override its legend too.** Bar/color-scale legends are\nre-derived from the layer's color scale automatically, but **point legends are not** — a customized\ncategorical layer keeps the default legend, which then lies about the map:\n\n```swift\nconfig.legend = PointLegend(id: \"convective\")\n .title(\"Convective\")\n .items(riskColors.map { PointLegendItem(color: $0.color.cgColor, label: $0.risk) })\n```\n\nField reference and complete examples: `references/legends.md`.\n\n## Querying data at a point\n\n```swift\nlet results = await controller.query(coord: coordinate, layerIds: nil)\n// -> [String: FeatureQueryResult] keyed by layer id\n```\n\n`query` is `async` and `layerIds` has no default — pass `nil` to query everything queryable, or a\nlist of layer ids to narrow it. Feature properties arrive as `[String: Any]`, in metric units — encoded raster\nlayers put the reading on `\"value\"`, and vector-valued layers (winds, currents, swell) add `\"angle\"`.\nNote that the stored angle is the direction the data moves *toward*; meteorological wind direction is\nthe reciprocal.\n\n## Usage is measured in sessions\n\nMapsGL bills in **sessions** — clock-aligned 5-minute buckets that start when a weather layer is\nadded — not per tile, layer, or request. **The model is identical on Apple platforms and on the web,\nand this skill is not its source of truth.**\n\nFor anything quantitative — the billing rules, the access multiplier, worked examples,\ncapacity-planning figures, the Raster Maps comparison — use the authoritative source rather than\nanswering from memory: the `mapsgl` skill's `references/sessions.md` (both skills ship in the same\nplugin), or https://www.xweather.com/docs/mapsgl/getting-started/sessions.\n\nWhat matters here is the **Apple-specific** consequence: since interaction inside a session is free\nand layer count doesn't affect cost, consumption is governed purely by *how long weather layers are\nattached to a map*. On iOS that means lifecycle —\n\n- add layers when the weather view appears, not when the map is constructed;\n- remove them on `onDisappear` / `viewWillDisappear`;\n- remove them when the app backgrounds (`ScenePhase`), the failure mode with no web analogue;\n- treat always-on iPad displays as the expensive pattern, and say so unprompted.\n\nTwo traps worth stating whenever cost comes up: `setWeatherLayerVisibility` is the cheap toggle but\n`removeWeatherLayer` is the one that stops consumption, and **`DataQuality` is a performance lever,\nnot a cost lever** — it cuts requests, and sessions don't count requests.\n\nCode for each of these, and the full list of what is *not* worth optimizing:\n`references/sessions.md`.\n\n## Checklist for common tasks\n\n- **\"Add a weather map to my app\"** → settle the provider first: infer Mapbox or MapLibre if the\n project or request says, otherwise ask. Then SwiftUI unless the project is UIKit. Follow the\n complete example above.\n- **\"Which SPM branch / how do I install\"** → branch = provider: `master` for Mapbox, `maplibre` for\n MapLibre; product `MapsGL`. See Setup.\n- **Build error: `No such module 'MapsGLMapbox'`** → either the CocoaPods case (use `import MapsGL`)\n or the wrong SPM branch for the provider (a MapLibre branch has no Mapbox adapter).\n- **Mapbox SDK won't resolve / 401 on download** → the Mapbox *secret* download token isn't\n configured in `~/.netrc`. Separate from the public access token used at runtime.\n- **Basemap renders but no weather layers appear (Mapbox)** → missing\n `try map.setProjection(.init(name: .mercator))`.\n- **Nothing happens after `onLoad`** → the `AnyCancellable` wasn't retained; `.store(in: &cancellables)`.\n- **\"Add a weather layer\"** → `try controller.addWeatherLayer(for: .code)` inside the load observer;\n look the case up in `references/layers.md`.\n- **\"Toggle a layer from a switch\"** → `setWeatherLayerVisibility(for:visible:)`, not\n remove/re-add. Neither call throws.\n- **\"Change a layer's colors/thresholds\"** → instantiate its `WeatherService.<Name>` config, set\n `layer.paint.sample.colorScale`, add via `addWeatherLayer(config:)`. Stops are metric.\n- **\"Restyle a composite layer\"** (`.stormcells`, `.roads`, `.boundaries`, …) → you can't; its\n `layers` member is a `let`. Add the constituent layers individually. Full list in\n `references/layers.md`.\n- **`getLayer(id:)` returns nil for a weather layer** → expected; a code is not an id. Use\n `weatherLayer(for:)`.\n- **\"Compiler rejects `.normal` / `.minimal` quality\"** → Apple has only `.low`, `.medium`, `.high`,\n `.exact`.\n- **`value of type 'SamplePaint' has no member 'opacity'`** → it's `paint.opacity`, not\n `paint.sample.opacity`. The web docs show the wrong path.\n- **`'GridLayerDescriptor' cannot be constructed because it has no accessible initializers`** → only\n the vector descriptors (`fill`, `line`, `circle`, `symbol`, `heatmap`) have a public init. Start from\n a built-in grid layer's config and mutate `config.layer` instead. Same for `SamplePaint`.\n- **Deprecation warning on `subscribe(to:)` / `asyncSubscribe` / `subscribeToNext`** → use\n `on<Event>.observe { … }`. The web docs' SwiftUI sample still shows the deprecated form.\n- **`call to main actor-isolated instance method … in a synchronous nonisolated context`** → the\n controller's layer/source API and `LegendControl`'s mutators are `@MainActor`; annotate the helper.\n- **\"Animate over time / add a time scrubber\"** → `controller.timeline`, see `references/timeline.md`.\n- **\"Show a legend\"** → `LegendControl` + `controller.add(legendControl:)`, or `LegendControlView` in\n SwiftUI. Override `config.legend` whenever you customized a categorical layer's paint.\n- **\"Show values on tap\"** → `addDataInspectorControl(constrainedTo:)`, or `.dataInspectorOverlay(…)`\n in SwiftUI; customize with `DataInspectorPresentation`.\n- **\"How many accesses / how much does this cost?\"** → sessions, not tiles or layers. Get the model\n and the arithmetic from the authoritative source — the `mapsgl` skill's `references/sessions.md` or\n https://www.xweather.com/docs/mapsgl/getting-started/sessions — then apply the iOS lifecycle\n guidance in `references/sessions.md`.\n- **\"What's the latest version / where are the API docs?\"** → releases endpoint for the version, then\n `https://cdn.aerisapi.com/sdk/ios/mapsgl/docs/v{version}/documentation/mapsglmaps`. There is no\n `latest` alias.\n\n## Attribution is required\n\nXweather requires attribution wherever its data or imagery is displayed. This applies to **all\nproducts** — Weather API, Raster Maps, and MapsGL alike. Build it into anything you produce, and say\nso when handing over code that will end up in front of users.\n\nThe minimum is a link to `https://www.xweather.com/` reading \"Powered by Vaisala Xweather\":\n\n```swift\nLink(\"Powered by Vaisala Xweather\", destination: URL(string: \"https://www.xweather.com/\")!)\n```\n\nThe logo may be substituted for the \"Xweather\" text. Light and dark variants exist in SVG and PNG at\n`https://www.xweather.com/assets/logos/vaisala-xweather-logo-dark.svg` — swap `-dark` for `-light`\nover a dark background, or `.svg` for `.png`. Bundle the asset rather than loading it over the network\nin a shipping app. Using the logo brings rules: keep it unmodified, leave at least a **10pt buffer**\nof space around it, and only adjust lightness or opacity in greyscale. Don't rotate it, don't recolour\nit (monotone black or white excepted), and don't use the symbol without the Xweather name.\n\nFull guide: https://www.xweather.com/docs/weather-api/resources/attribution\n\n## Reference files\n\n- `references/setup.md` — install paths per package manager, provider-specific project setup, UIKit examples, credential handling, and the build errors each mistake produces\n- `references/api-reference.md` — `MapController`, `WeatherService`, source and layer descriptors, controls, events, and query API, plus how to reach the hosted DocC for a given version\n- `references/layers.md` — every `WeatherService.LayerCode` case with configuration struct, descriptor type and paint namespaces; composite layers listed up front\n- `references/styles.md` — paint property spec for every render type, `DataQuality`, color scales, filters and masks\n- `references/expressions.md` — `Expression` factory reference for data-driven paint and filters\n- `references/legends.md` — `LegendControl`, bar and point legend configuration, SwiftUI and UIKit placement\n- `references/timeline.md` — timeline and animation API, including the inherited `TimeAnimation`/`Animation` surface\n- `references/sessions.md` — the Apple-specific half of session cost: view and app lifecycle teardown, backgrounding, and the two traps. Points at the `mapsgl` skill and the public docs for the billing model itself\n"
}SHA-256: 26057ba8b65d25060af3625cde1dfcd4cf8b3c1c1865d9f04b1fe3d24e6cb2fb