Migrate from SkyPath SDK 3.x to 4.0
This guide is provided to ease the transition of existing applications using SkyPath v3.x to v4.0 APIs.
v4.0 renames the turbulence API to observations, replaces the mutable dataQuery with a validated fetchConfig, and moves start/simulation/analytics onto dedicated entry points.
Most renamed APIs ship an @available(*, unavailable, renamed:) shim, so Xcode will point at the replacement with a fix-it. Work through the sections below in order and the compiler will find most of the rest.
Before you start
- Raise your deployment target to iOS 17.0. The v4.0 binary is built for iOS 17. With a lower target the build only shows a linker warning (
building for iOS-simulator-16.0, but linking with dylib ... built for newer version 17.0), and the app can fail at launch on iOS versions below 17.
- Expect errors in waves. The compiler stops at the first errors it hits in each file, so fixing one round can surface a new one. Rebuild until it's clean.
- Search for enum case patterns the compiler doesn't flag. An unavailable, renamed case used as a
switch pattern (case .turbulence:, case .dev(serverUrl:):) compiles with no error and no fix-it. Search your project for these names and rename them by hand:
grep -rnE "case \.(turbulence|none|dev)\b" --include="*.swift" .
.none also matches Optional.none, so check each hit — only TurbulenceSeverity.none needs renaming to .smooth.
1. Initialization
- Build an
AuthConfig from your existing start(...) arguments — airline becomes companyId.
- Replace the
start(...) call with initialize(with:completion:).
- Delete any
stop() calls; the SDK does not need to be stopped.
Call initialize(with:completion:) once per app launch. Switching the environment at runtime is not supported. If you used stop() followed by start(...) to change environment, save the new environment and apply it on the next launch instead.
- Replace
StartError handling with GeneralError (or the generic SPError).
- Remove any reads of
SkyPath.env.
- Replace
Environment.dev(serverUrl:) with Environment.staging(serverUrl:), including switch patterns (the compiler doesn't flag those, see Before you start). If your app has an environment picker that lists both dev and staging, remove the dev entry — applying the fix-it as-is leaves two identical staging entries.
SkyPath.shared.start(apiKey: key, airline: icao, userId: userId, env: env) { error in }
let config = AuthConfig(apiKey: key, companyId: icao, userId: userId, env: env)
SkyPath.shared.initialize(with: config) { error in }
| 3.x | 4.0 |
|---|
start(apiKey:airline:userId:env:completion:) | initialize(with:completion:) |
stop() | Removed — stopping the SDK is no longer available, and switching the environment at runtime is not supported |
StartError | Removed — errors are GeneralError / other SPError types |
SkyPath.env | Removed |
Environment.dev(serverUrl:) | Environment.staging(serverUrl:) |
2. Fetch configuration
- Rename the type
DataQuery to FetchConfig everywhere.
- Replace every
SkyPath.shared.dataQuery.<field> = value assignment with updateFetchConfig { $0.<field> = value } — fetchConfig is now read-only.
- Group fields you used to set one by one into a single
updateFetchConfig closure, so validation and the fetch check run once.
- Add
try and error handling — updateFetchConfig(_:) and setFetchConfig(_:) throw a QueryError on a malformed polygon/viewport or an unsupported aircraftICAO.
- Rename the three toggles:
dataUpdateEnabled, dataUpdateInBackgroundIsEnabled, peerEnabled.
- Delete
fetchData(...), fetchingStatus(...) and updateDate(of:) calls.
SkyPath.shared.dataQuery.polygon = polygon
SkyPath.shared.dataQuery.types = [.turbulence]
do {
try SkyPath.shared.updateFetchConfig {
$0.polygon = polygon
$0.types = [.observations]
$0.aircraftICAO = "B737"
}
} catch {
print(error)
}
do {
try SkyPath.shared.setFetchConfig(FetchConfig(polygon: polygon))
} catch {
print(error)
}
| 3.x | 4.0 |
|---|
SkyPath.dataQuery (settable) | SkyPath.fetchConfig (read-only) + updateFetchConfig(_:) / setFetchConfig(_:) |
DataQuery | FetchConfig |
SkyPath.dataUpdateEnabled | SkyPath.isFetchEnabled |
SkyPath.dataUpdateInBackgroundIsEnabled | SkyPath.isBackgroundFetchEnabled |
SkyPath.peerEnabled | SkyPath.isPeerEnabled |
fetchData(refresh:types:) | Removed — data is fetched automatically; change fetchConfig instead |
fetchingStatus(for:dataType:) | Removed |
updateDate(of:) | Removed — use SkyPath.dataUpdatedAt |
fetchData(refresh:types:) was a public API and has no replacement: there is no way to trigger a
fetch manually any more. The SDK fetches on its own schedule and re-evaluates whenever fetchConfig
changes, so remove any "refresh data" button or pull-to-refresh that called it.
3. Data types
- Rename the type
DataTypeOptions to DataType everywhere.
- Rename the case
.turbulence to .observations, including case .turbulence: in switch statements — the compiler doesn't flag those.
- Change every
DataTypeOptions value to a Set<DataType> literal — types, viewportTypes, and your own properties.
DataType is now a single value, not an option set. A function that took DataTypeOptions and passed it to types needs a Set<DataType> parameter, and callers passing one type must wrap it: setTypes([selectedType]). A list of individual layers, such as [DataType] for a picker, can stay an array.
- Replace
DataTypeOptions.all, .asArray and .rawValue usages with an explicit Set<DataType>.
- Replace
DataAreaType.any with .route, .viewport or .all.
SkyPath.shared.dataQuery.types = [.turbulence, .oneLayer]
if DataTypeOptions.forecast.enabled { ... }
try SkyPath.shared.updateFetchConfig { $0.types = [.observations, .oneLayer] }
if DataType.forecast.enabled { ... }
| 3.x | 4.0 |
|---|
DataTypeOptions | DataType |
DataTypeOptions.turbulence | DataType.observations |
DataTypeOptions.all / .asArray / .rawValue | Removed — use a Set<DataType> literal |
DataAreaType.any | Removed — use .route, .viewport, or .all |
DataType.forecast still requires .oneLayer to also be in types, and entitlement via DataType.forecast.enabled.
4. Observations (was Turbulence)
- Rename the call
turbulence(with:) to observations(with:).
- Rename the types
TurbulenceQuery → ObservationsQuery and TurbulenceItem → ObservationsItem everywhere, including your own properties, arrays and function signatures.
- Update
QueryResult<TurbulenceItem> to QueryResult<ObservationsItem>.
- Update
TurbulenceCluster.turbulence and .nearestMaxSevTurbulence call sites — they now hand back ObservationsItem.
- Rename
TurbulenceItemable.turbulenceItem to asTurbulenceItem.
- Delete
ObservationsQuery.sevs — filter by minSev instead, see §16 Queries.
| 3.x | 4.0 |
|---|
SkyPath.turbulence(with:) | SkyPath.observations(with:) |
TurbulenceQuery | ObservationsQuery |
TurbulenceItem | ObservationsItem |
QueryResult<TurbulenceItem> | QueryResult<ObservationsItem> |
TurbulenceCluster.turbulence / .nearestMaxSevTurbulence | Now [ObservationsItem] / ObservationsItem? |
Array<TurbulenceItem>.geoJSON(withType:) | Array<ObservationsItem>.geoJSON(withType:) |
TurbulenceItemable.turbulenceItem | asTurbulenceItem (now returns ObservationsItem) |
ObservationsQuery.sevs | Removed — use minSev |
5. Aircraft
- Delete all uses of the
Aircraft and AircraftSize types — they are no longer public.
- Replace
SkyPath.aircraft = ... with updateFetchConfig { $0.aircraftICAO = "B737" }.
- Delete
aircrafts() calls; there is no list of supported aircraft any more.
- Replace
aircraft(byId:) validation with a do/catch around updateFetchConfig — an unsupported ICAO throws.
- Read the current value from
SkyPath.fetchConfig.aircraftICAO instead of SkyPath.aircraft.
if let aircraft = SkyPath.shared.aircraft(byId: "B737") {
SkyPath.shared.aircraft = aircraft
}
do {
try SkyPath.shared.updateFetchConfig { $0.aircraftICAO = "B737" }
} catch {
print(error)
}
| 3.x | 4.0 |
|---|
SkyPath.aircraft | SkyPath.fetchConfig.aircraftICAO (default "B737") |
SkyPath.aircrafts() | Removed |
SkyPath.aircraft(byId:) | Removed — set aircraftICAO and handle the thrown error |
Aircraft, AircraftSize | Removed |
6. Notifications
- Delete
notifications(with:) calls and move that logic into didReceiveNotification(_:), starting monitoring with startMonitoringNotifications(with:).
- Rename
NotificationResult.turbulence to .observations.
- Make
NotificationQuery.minSev non-optional — remove the ? and drop nil assignments; the default is now .light.
- Delete
switchToBeamOnDistanceFromRoute, excludeOwnTimeSpan, tiles, sevs and angleSpan from your NotificationQuery setup.
- Rename
widthAround to routeWidth and double the value — routeWidth is the total corridor width, widthAround was the distance from the route line to one side.
- Remove any
NotificationResultType.tiles branches.
- Drop any rounding you do on
altRange — the bounds are rounded to a thousand feet internally.
var query = NotificationQuery(minSev: .moderate, sevs: nil, angleSpan: 15, route: route, widthAround: 20)
let result = try SkyPath.shared.notifications(with: query).get()
let query = NotificationQuery(minSev: .moderate, route: route, routeWidth: 40)
SkyPath.shared.startMonitoringNotifications(with: query)
| 3.x | 4.0 |
|---|
SkyPath.notifications(with:) | Removed — startMonitoringNotifications(with:) + didReceiveNotification(_:) |
NotificationResult.turbulence | NotificationResult.observations |
NotificationQuery.minSev: TurbulenceSeverity? | minSev: TurbulenceSeverity, default .light |
NotificationQuery.switchToBeamOnDistanceFromRoute | Removed — beam mode kicks in automatically when far from the route line |
NotificationQuery.excludeOwnTimeSpan | Removed |
NotificationQuery.tiles / NotificationResultType.tiles | Removed |
NotificationQuery.sevs | Removed — use minSev |
NotificationQuery.angleSpan | Removed — the beam always uses 15 degrees |
NotificationQuery.widthAround (per side, default 20 NM) | NotificationQuery.routeWidth (total width, default 40 NM) |
7. Recording state
- Replace
inPosition, isHorizontal and inNoise reads with a single SkyPath.recordingState check.
- Replace the
didUpdateRecordingStatus(to:) / didChangeDevicePosition(_:horizontal:) delegate methods with didUpdateRecordingState(to:) — see §9 Delegate.
- Map your pilot-facing messages onto the four
RecordingState cases.
Both 3.x callbacks collapse into didUpdateRecordingState(to:), and state == .recording carries what
each of them used to report:
- what
didUpdateRecordingStatus(to: true) reported is now state == .recording;
- what
didChangeDevicePosition(inPosition:horizontal:) reported is now inPosition == (state == .recording)
and horizontal == (state == .horizontal).
So if you handled both, move both bodies into the single method and derive the old booleans from state.
switch SkyPath.shared.recordingState {
case .recording, .notInPosition, .horizontal, .noise: break
}
func didUpdateRecordingState(to state: RecordingState) { }
| 3.x | 4.0 |
|---|
SkyPath.inPosition | SkyPath.recordingState == .recording |
SkyPath.isHorizontal | SkyPath.recordingState == .horizontal |
SkyPath.inNoise | SkyPath.recordingState == .noise |
8. App events
- Replace
setMapLayer(enabled:) with reportAppEvent(.mapLayer(enabled:)).
- Replace
notifiedTurbulenceSeverity(_:coordinate:altFt:dataSource:) with reportAppEvent(.notifiedTurbulence(severity:coordinate:altFt:dataSource:)).
SkyPath.shared.setMapLayer(enabled: true)
SkyPath.shared.notifiedTurbulenceSeverity(sev, coordinate: coord, altFt: alt, dataSource: source)
SkyPath.shared.reportAppEvent(.mapLayer(enabled: true))
SkyPath.shared.reportAppEvent(.notifiedTurbulence(severity: sev, coordinate: coord, altFt: alt, dataSource: source))
| 3.x | 4.0 |
|---|
setMapLayer(enabled:) | reportAppEvent(.mapLayer(enabled:)) |
notifiedTurbulenceSeverity(_:coordinate:altFt:dataSource:) | reportAppEvent(.notifiedTurbulence(severity:coordinate:altFt:dataSource:)) |
9. Delegate
- Implement all available
SkyPathDelegate methods — there are no default implementations any more, so a missing one is a compile error.
- Rename
didReceiveNewTurbulenceData(areaType:) to didReceiveNewObservationsData(areaType:).
- Merge
didFailToFetchNewData(with:type:) and locationManagerDidFail(withError:) into didFail(with:dataType:). dataType is optional: it carries the failing DataType for data-fetch errors and is nil for everything else (authorization, token refresh, location).
- Delete the six removed callbacks listed below — the SDK no longer calls them.
The 4.0 protocol is exactly: didReceiveNewObservationsData(areaType:), didReceiveNewOneLayer(areaType:), didFail(with:dataType:), didReceiveNotification(_:), didUpdateRecordingState(to:).
func didFailToFetchNewData(with error: SPError, type: DataTypeOptions) { }
func locationManagerDidFail(withError error: Error) { }
func didFail(with error: SPError, dataType: DataType?) {
switch dataType {
case .observations, .oneLayer: reloadTurbulenceLayer()
case .forecast: reloadForecastLayer()
case nil: break
default: break
}
}
| 3.x | 4.0 |
|---|
didReceiveNewTurbulenceData(areaType:) | didReceiveNewObservationsData(areaType:) |
didFailToFetchNewData(with:type:) | didFail(with:dataType:) |
locationManagerDidFail(withError:) | didFail(with:dataType:) (dataType is nil) |
didUpdateRecordingStatus(to:) / didChangeDevicePosition(_:horizontal:) | didUpdateRecordingState(to:) |
detectedTurbulence(_:) | Removed |
serverReachabilityUpdated(to:) | Removed |
didUpdateLowPowerMode(_:) | Removed |
didUpdateFetchingStatus(to:areaType:dataType:) | Removed |
didUpdateConfig() | Removed |
didUpdatePeers(discovered:connected:) | Removed |
10. Severity colors
- Rename
TurbulenceSeverity.none to .smooth everywhere, including switch cases.
- Define your own colors for the severity levels — colors were removed from the SDK. Use the reference palette from the Data page.
- Repoint
.color, .borderColor, .colorOpacity and .colorWithOpacity call sites at your own colors.
| 3.x | 4.0 |
|---|
TurbulenceSeverity.none | TurbulenceSeverity.smooth |
.color, .borderColor, .colorOpacity, .colorWithOpacity | Removed — provide your own |
11. Logging
- Replace
SkyPath.logger.printLevel = ... and SkyPath.logger.level = ... with SkyPath.loggerLevel = ....
There is no fix-it for logger: the compiler reports a misleading value of tuple type 'Void' has no member 'level' instead.
- Replace
SkyPath.logger.exportLogs(...) with SkyPath.exportLogs(...).
- Delete every other
logger usage — the Logger type is no longer public.
| 3.x | 4.0 |
|---|
SkyPath.logger.printLevel / SkyPath.logger.level | SkyPath.loggerLevel |
SkyPath.logger.exportLogs(notifyOnQueue:completion:) | SkyPath.exportLogs(notifyOnQueue:completion:) |
LoggingLevel | PrintLevel |
Logger, logger.isEnabled, .logsDirectory, .maximumNumberOfLogFiles, .rollingFrequency | Removed |
12. Flight
- Rename
Flight.fnum to Flight.num.
- Change
Flight(dep:dest:fnum:fnumManual:) to Flight(dep:dest:num:) — the fnumManual argument is gone.
- Remove reads of
Flight.id, .depLat, .depLng, .destLat, .destLng; keep those values on your own model if you need them.
- Remove observers of the
didUpdateFlight notification.
| 3.x | 4.0 |
|---|
Flight.fnum | Flight.num |
Flight(dep:dest:fnum:fnumManual:) | Flight(dep:dest:num:) |
Flight.id, .fnumManual, .depLat, .depLng, .destLat, .destLng | Removed |
Notification.Name.didUpdateFlight | Removed |
13. Testing & simulation
- Insert
.testing between SkyPath.shared and every simulation call — the methods themselves keep their names and signatures.
- Move
resetCache(all:issuedAt:) onto SkyPath.shared.testing as well.
- Keep the simulation calls on the staging environment only (
env: .staging(serverUrl: nil) in AuthConfig).
From 4.0 the SDK enforces this: every simulation API is a no-op on any other environment and logs an error.
resetCache(all:issuedAt:) is the exception and still works on any environment.
SkyPath.shared.enableSimulation(true)
SkyPath.shared.simulatedLocation(location)
SkyPath.shared.testing.enableSimulation(true)
SkyPath.shared.testing.simulatedLocation(location)
| 3.x | 4.0 |
|---|
SkyPath.enableSimulation(_:) | SkyPath.testing.enableSimulation(_:) |
SkyPath.simulatedLocation(_:) | SkyPath.testing.simulatedLocation(_:) |
SkyPath.enablePushSimulated(_:) | SkyPath.testing.enablePushSimulated(_:) |
SkyPath.simulateTurbulence(sev:) | SkyPath.testing.simulateTurbulence(sev:) |
SkyPath.simulateBadLocation(_:) | SkyPath.testing.simulateBadLocation(_:) |
SkyPath.simulateBadLocation (property) | SkyPath.testing.simulateBadLocation |
SkyPath.resetCache(all:issuedAt:) | SkyPath.testing.resetCache(all:issuedAt:) |
14. Errors
- Remove the four deleted
QueryError cases from your switch statements.
- Add
invalidRoute where you handle polygon/route problems.
- Handle the two new
GeneralError cases if you switch exhaustively: unauthorized(message:) (HTTP 401) and location(error:).
3.x QueryError case | 4.0 |
|---|
altRangeNotRoundToThousandFeet | Removed |
polygonTooManyCoordinates | Removed — use invalidPolygon |
invalidCoordinates | Removed — use invalidPolygon |
dataQueryInvalid(message:) | Removed — use invalidPolygon / invalidRoute / general(error:) |
| — | invalidRoute added |
15. Removed with no replacement
- Move local-notification scheduling into your app using Apple's
UserNotifications framework.
- Replace
Tile usages with the plain String tile keys — Itemable.tileKey / Itemable.tileKeyByCoord on any item, QueryResult.tiles on any result — see §16 Queries.
- Delete any UI driven by
serverReachable or lowPowerMode.
| 3.x | 4.0 |
|---|
SPLocalNotificationManager | Removed — use UserNotifications directly |
Tile, Itemable.tile | Removed — use the String tile keys from Itemable.tileKey / Itemable.tileKeyByCoord |
Tile.key | Itemable.tileKey |
Tile.keyByCoord | Itemable.tileKeyByCoord |
SkyPath.serverReachable | Removed — reachability status is no longer available |
SkyPath.lowPowerMode | Removed — low power mode status is no longer available |
Aircraft, AircraftSize | Removed — see §5 Aircraft |
Logger, LoggingLevel | Removed — see §11 Logging |
StartError | Removed — see §1 Initialization |
16. Queries
- Delete
OneLayerQuery.sevs and ObservationsQuery.sevs — filter by minSev instead.
- Replace
Tile.key / Tile.keyByCoord with Itemable.tileKey / Itemable.tileKeyByCoord wherever you build the tiles or excludeTiles collections.
- Optionally, write code that configures both query types against the new
DataQueryable protocol.
ObservationsQuery and OneLayerQuery now conform to DataQueryable, which declares the properties
they have in common: ts, dataHistoryTime, altRange, minSev, resultOptions, aggregate,
excludeTiles, route, routeWidth, polygon and viewport. tiles is not part of it, because
ObservationsQuery.tiles is an Array and OneLayerQuery.tiles is a Set.
Tile is not public, so the tile keys it used to produce now come from the items and results themselves.
Itemable.tileKey is the h3Hex-altTile key of one altitude block (e.g. 852aaa37fffffff-38) and
Itemable.tileKeyByCoord is the h3Hex key covering that hexagon at every altitude.
query.excludeTiles = Set(items.map { Tile(h3Hex: $0.h3Hex, alt: $0.altTile).key })
query.excludeTiles = Set(items.map(\.tileKey))
| 3.x | 4.0 |
|---|
OneLayerQuery.sevs | Removed — use minSev |
ObservationsQuery.sevs | Removed — use minSev |
OneLayerQuery.widthAround | OneLayerQuery.routeWidth (total corridor width) |
ObservationsQuery.widthAround | ObservationsQuery.routeWidth (total corridor width) |
Tile.key | Itemable.tileKey |
Tile.keyByCoord | Itemable.tileKeyByCoord |
| — | DataQueryable added |
The altRange bounds of both queries are rounded to a thousand feet internally, so drop any rounding
you do before setting them.