---
title: 'Versions and Compatibility'
source: 'https://academia.sh/en/courses/ios-platform-and-tools/versions-and-compatibility'
course: 'iOS Platform and Tools'
language: en
updated: '2026-10-21T05:03:11+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Explains what the minimum supported version means'
    - 'Measures that supported versions intersect with the versions in which an interface exists'
    - 'Shows that disjoint version ranges give rejection and that an availability check does not help in that case'
    - 'Separates the minimum version decision from the rule by measuring that it lies outside it'
---

# Versions and Compatibility

Measuring that the versions an app supports intersect with the versions in which an interface exists, that disjoint ranges are rejected, and that the minimum version decision lies outside the rule.

In the previous lesson the values of the user, the device, and the app met. In this lesson the parties that meet are version ranges: the versions the app supports and the versions in which a platform interface exists. The question is the same again: **how are these two definitions resolved when they meet?** The resolution of this lesson is **intersection**, and when the intersection is empty, **rejection**.

The minimum supported version (the deployment target) is the oldest platform version the app runs on; it cannot be installed on an older system. The upper bound, on the other hand, is not declared; the app keeps running on versions newer than the one it was built against. So the model's first set is the versions the app is tested on, from the minimum up to the newest tested version. A platform interface, in turn, is added in a particular version and does not exist before that version; if it has been removed, it also does not exist from the version in which it was removed. The interface exists in some supported versions and not in others; this lesson asks how that split is resolved.

```swift
// Meeting.swift — classifies how two definitions are resolved when they meet
struct Meeting {
    let label: String
    let first: Set<String>
    let second: Set<String>
    let result: Set<String>?

    init(_ label: String, first: Set<String>, second: Set<String>, result: Set<String>?) {
        self.label = label
        self.first = first
        self.second = second
        self.result = result
    }

    var resolution: String {
        if first.isEmpty || second.isEmpty
            || first.isSubset(of: second) || second.isSubset(of: first) {
            return "out-of-scope"
        }
        guard let result, !result.isEmpty else { return "rejection" }
        if result == first.union(second) { return "union" }
        if result == first.intersection(second) { return "intersection" }
        if result == first || result == second { return "precedence" }
        return "off-rule"
    }

    static func list(_ values: Set<String>) -> String {
        values.isEmpty ? "empty" : values.sorted().joined(separator: ",")
    }

    func report() {
        let outcome = result.map(Meeting.list) ?? "none"
        print("\(label): first \(Meeting.list(first)) | second \(Meeting.list(second)) | result \(outcome) -> \(resolution)")
    }
}

func summarize(_ meetings: [Meeting]) {
    for resolution in ["precedence", "intersection", "union", "rejection"] {
        let count = meetings.filter { $0.resolution == resolution }.count
        print("\(resolution): \(count)")
    }
}
```

## The Availability Check

An availability check is a construct that tests at run time whether an interface exists. If the app also supports older versions, the version must be tested before a new interface is called; an interface called without testing is rejected at build time, because it does not exist in the older version. The piece below depends on platform interfaces, so it stands as source and no output is claimed for it.

```swift
import SwiftUI

struct DetailView: View {
    var body: some View {
        if #available(iOS 16, *) {
            Text("New interface")
        } else {
            Text("Old interface")
        }
    }
}
```

`#available` separates two branches: the new interface runs only on the new version, and the fallback branch runs on the old version. The model builds this rule of the platform. The numbers below are model values: they do not show real version numbers or the real versions of real interfaces. **PS16.**

## Intersecting Versions

The helper below takes two version ranges and returns the common versions. The ranges are expressed with integers; because `Meeting` works with sets, the versions are carried as strings. The file has no output.

```swift
// Versions.swift — the common versions of two version ranges
func commonVersions(app: ClosedRange<Int>, api: ClosedRange<Int>) -> Set<String> {
    Set(app.filter { api.contains($0) }.map(String.init))
}

func versionNames(_ range: ClosedRange<Int>) -> Set<String> {
    Set(range.map(String.init))
}
```

The app supports 14 through 17; the interface exists from 16 through 19.

```swift
// main.swift — supported and available versions intersect
let supported = 14...17
let available = 16...19

let overlap = Meeting("version-overlap",
                      first: versionNames(supported),
                      second: versionNames(available),
                      result: commonVersions(app: supported, api: available))
overlap.report()
```

```
version-overlap: first 14,15,16,17 | second 16,17,18,19 | result 16,17 -> intersection
```

14 and 15 exist in the app but not in the interface; 18 and 19 exist in the interface but the app does not support them. The versions that work are 16 and 17. **PS17.** This measurement is only for these two ranges; the bounds of the ranges are model values.

The two ends of the intersection answer two separate questions. The lower end is the version in which the interface first exists: in the older versions the app supports, the interface cannot be called, and a fallback branch is needed for these versions. The upper end is the newest version the app is tested on; the app also runs on newer versions and the interface exists there too, but untested versions do not enter the first set of the model, so they do not appear in the intersection. The upper end is not a support limit but the limit of the measurement. So the versions covered by the fallback branch are found by removing the intersection from the app's range: here, 14 and 15.

## Disjoint Versions

If the ranges do not intersect at all, there is no common version. The case where this really happens is a removed interface: the interface was added in 10, removed in 13, and exists only from 10 through 12. The app supports 13 through 15; in no version are the two present together.

```swift
// main.swift — disjoint version ranges are rejected
let newApp = 13...15
let removedApi = 10...12

let apart = Meeting("disjoint-versions",
                    first: versionNames(newApp),
                    second: versionNames(removedApi),
                    result: commonVersions(app: newApp, api: removedApi))
apart.report()
```

```
disjoint-versions: first 13,14,15 | second 10,11,12 | result empty -> rejection
```

When the result is the empty set, the resolution comes out as rejection. In a real project this means the interface cannot be used in this app at all; an `#available` condition does not help, because the branch calling it is true in no supported version. The only way is to replace the interface with the newer one that took its place. This shows that rejection is not an unresolvable state but a declaration of a boundary.

## Criteria for the Minimum Version Decision

The minimum supported version is not the result of a meeting but a decision, and it rests on three criteria.

The first is the user base. Raising the minimum version leaves out the users who stay on systems older than that version. Deciding requires knowing which versions users are on, which comes from the app's usage data and the platform's distribution figures. If the share of users who stay on the older version is small, the cost of raising is low; if it is large, the cost is high. This lesson's measurement cannot know this share; it measures only the intersection of the ranges.

The second is API access. Each version brings new interfaces, and raising the minimum version makes the interfaces in that version callable unconditionally. In the bounding measurement at the end of the lesson, in the `minimum-16` attempt the app's range becomes a subset of the interface's range and comes out as out of scope: in this case an `#available` check is no longer needed, because the interface exists in every version the app supports. The API access criterion is the value of this simplification.

The third is the maintenance load. Each supported version is a condition that has to be tested. Code that writes separate behavior for older and newer versions requires every branch to be tested separately, and the chance of defects grows with the number of branches. Raising the minimum version reduces branches but narrows the user base. The decision lies between these two costs, and the weight of each cost changes from app to app.

The three criteria oppose one another: the user base wants to keep the version low, while API access and the maintenance load want to keep it high. The resolution of this tension is not a set operation. The measurement at the end of the lesson shows this too: 14, 15, and 16 are three separate decisions, and the rule only counts the result of each; it does not choose one.

## Usage: `#available` and `@available`

There are two ways to express a version boundary in code. `#available` is a condition inside an expression that tests the version at run time. The piece at the start of the lesson uses it: if the condition is true the new interface runs, and if it is false the fallback branch runs. The required asterisk at the end declares the default for unlisted platforms.

`@available` is a mark written on a declaration, and it says from which version that declaration can be used. A function or type with the mark can be used only in that version or later; a piece of code that does not carry the mark and wants to call it must either carry the same mark itself or wrap the call in an `#available` condition. In this way the version requirement is carried upward along the call chain, and in the end it is either met with a condition or already satisfied by the minimum version.

The division of work between the two is clear. `#available` makes the decision: does the interface exist on the system that is running now. `@available` declares a requirement: which version does this declaration ask for. If the minimum version satisfies all the `@available` requirements, no `#available` condition is needed. If the intersection is partial, that is, if the interface exists in only some of the supported versions, `#available` is needed and the fallback branch covers the versions outside the intersection. When the intersection is empty, `#available` does not help: the branch that calls the interface runs in no supported version. `#available` is the code-side counterpart of a partial intersection; rejection is the notice that the interface has to be given up.

**PS19.** The explanation in this section contains no code measurement; the behavior of `#available` and `@available` is only described and has not been run.

## The Difference Between the Build Version and the Minimum Version

The two version concepts are often confused. The build version is the version of the platform SDK the app is built with, and it determines which interfaces the app knows. The minimum version is the oldest system the app will run on. The build version can be new and the minimum version old; the app knows the new interfaces but also runs on older systems. This is exactly why `#available` exists: to close this gap. The compiler recognizes the interface, but at run time it is not certain whether the system carries it.

This difference shows up in the measurement as follows: the first set is the range between the app's minimum version and the newest version it is tested on, and the second set is the versions in which the interface exists. The build version enters neither of these sets; it only makes it possible to write the interface in code. The intersection tells in which versions the interface really exists at run time.

## Deprecated Interfaces

The platform replaces an interface with a new one over time. The old interface is first marked as deprecated: it keeps working but the compiler gives a warning. Later, after a long time, it can be removed entirely. The versions in which a removed interface exists never intersect with the range of an app that supports versions from the removal onward; the disjoint versions measurement above builds this case. The minimum version decision affects this transition. If the new interface exists only in new versions and the app also supports older versions, both must be kept: the new interface on the new version, the old interface on the old version, and an `#available` condition between the two.

The maintenance load of these two paths is clear. At the moment the minimum version is raised to the version in which the new interface exists, the old path can be deleted and the condition becomes unnecessary. The `minimum-16` attempt in the bounding measurement below is exactly this: the app's range stays inside the interface's range and the meeting disappears. The code of the deleted fallback branch also leaves the maintenance load.

## Testing at the Version Boundary

Every `#available` condition creates two paths, and both must be tested. The new path must be run on a system with the new version, and the fallback path on a system with the minimum version. An untested fallback path breaks at exactly the moment it is needed, that is, on a user with an older version. For this reason the oldest supported version should be a target that is tested regularly during development; an app tested only on the newest version may not really support the range it says it supports.

The cost of testing works like a fourth criterion of the minimum version decision: every supported version is an environment that has to be tested. The rejection case, in which the intersection comes out empty, can therefore also be read as a warning: if there is no common version, there is also no environment in which to test the path that uses the interface; that path runs on none of the supported systems and should be removed from the code.

## The Bounding Measurement

The minimum version decision is not a meeting: which version becomes the minimum is decided by a decision and not by a rule. The rule only counts the set. In the measurement below, the minimum version for the same interface is tried as 14, 15, and 16; in the first two the app's range partly overlaps the interface's range, and in the third it lies entirely inside it.

```swift
// main.swift — the minimum version decision lies outside the rule
let api = 16...19
let attempts = [
    Meeting("minimum-14", first: versionNames(14...17), second: versionNames(api),
            result: commonVersions(app: 14...17, api: api)),
    Meeting("minimum-15", first: versionNames(15...17), second: versionNames(api),
            result: commonVersions(app: 15...17, api: api)),
    Meeting("minimum-16", first: versionNames(16...17), second: versionNames(api),
            result: commonVersions(app: 16...17, api: api)),
]
for attempt in attempts {
    attempt.report()
}
```

```
minimum-14: first 14,15,16,17 | second 16,17,18,19 | result 16,17 -> intersection
minimum-15: first 15,16,17 | second 16,17,18,19 | result 16,17 -> intersection
minimum-16: first 16,17 | second 16,17,18,19 | result 16,17 -> out-of-scope
```

In all three attempts the common versions are the same: 16 and 17. The first two are intersections, and the third is out of scope because the app's range is a subset of the interface's range. So making the minimum version 14 or 15 does not change the result, while making it 16 removes the meeting: an `#available` check is no longer needed. This decision is made not by measurement but by the cost of support; the rule only counts the result of each choice, it does not say which will be chosen.

**PS18.** This measurement is for three minimum values and a single interface range. The support cost of each choice was not measured.

## Summary

- The minimum supported version is the oldest version the app will run on, and it is a decision.
- The versions the app supports and the versions in which the interface exists intersect: 16 and 17 remained.
- If the ranges are disjoint (for example, a removed interface), no common version remains and the resolution comes out as rejection; `#available` does not help either, and the interface is replaced with the newer one.
- Choosing 14 or 15 as the minimum version gives the same intersection; choosing 16 takes the meeting out of scope.
- The rule counts the set; it does not choose the lower bound.
- The resolution of this lesson: **intersection**.

## Next Step

The structure of the platform ends here: layers, frameworks, domain frameworks, the lifecycle, design guidelines, and versions. In the next topic the project and the target meet: the same setting can be defined at the project level and at the target level, and the development environment resolves which one is valid through this meeting.
