---
title: 'The Development Environment'
source: 'https://academia.sh/en/courses/ios-platform-and-tools/development-environment'
course: 'iOS Platform and Tools'
language: en
updated: '2026-10-21T05:03:10+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Measures which value is in effect when the same build setting is defined at the project and the target level'
    - 'Shows how the inheritance marker in search paths decides between precedence and union'
    - 'Measures that the minimum version, a single-valued setting, is overridden at the target, while the versions the app supports intersect with the versions of the run destinations'
    - 'Explains that a run scheme is a choice rather than a definition and produces no meeting'
---

# The Development Environment

Measuring how the same build setting defined at two levels, expanded search paths and version ranges meet across the project, the target and the run scheme.

The previous topic's last lesson put two definitions side by side: the minimum version the app supports defined one set, the version of the platform interface defined another, and what took effect was their intersection. This lesson carries the same question into the layers of the development environment itself. Building an app depends on settings written in more than one place: a single **project**, one or more **targets** beneath it, and the **run scheme** that runs them for a particular purpose. The same setting can be written in both the project and the target. If the two conflict, which one is in effect, do both apply together, or does the build never start?

The course's measurement axis is this question: **when these two definitions meet, how is it resolved?** There are four resolutions. If one definition overrides the other, the resolution is precedence; if only the shared part remains, intersection; if the two combine, union; if the two cannot be accepted together, rejection. In the course's opening lesson we measured eight meetings with a single classifier and found the distribution 2/2/2/2. This lesson uses that classifier unchanged and measures only its own meetings. The classifier counts the rule, it does not read intent: if one definition is a subset of the other, or one is empty, there is no meeting and the case is out of scope.

## The Classifier

`Meeting` holds the two definitions as sets, takes the result of the resolution as a third set, and names the result by looking at the two definitions. The result is never written by hand; in every measurement it comes from a small model the lesson builds. The output line always has the same shape: label, first definition, second definition, result and resolution.

```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 Same Setting at Two Levels

A **build setting** is a key and value pair that tells the compiler and the linker what to do: the optimization level, the minimum supported version, whether warnings are on or off. The project defines these pairs as defaults for all targets. Each target also carries its own build settings. If a key is written at both levels, the platform's rule is clear: the target's value overrides the project's. If a key is written only in the project, the target inherits it as it is.

We build this behavior with a small model. The model merges two dictionaries and keeps the target's value for conflicting keys. We chose three keys: the optimization level, written at both levels with different values; the minimum version, written at both levels with different values; and the warning level, written only in the project. For each key one meeting is built: the first definition is the project's value, the second definition is the target's value, and the result is the value the model treats as effective.

```swift
// main.swift — if the same key is defined at two levels, the target level wins
func resolveSettings(project: [String: String], target: [String: String]) -> [String: String] {
    project.merging(target) { _, targetValue in targetValue }
}

func single(_ value: String?) -> Set<String> {
    value.map { [$0] } ?? []
}

func settingMeeting(_ key: String, project: [String: String], target: [String: String]) -> Meeting {
    let effectiveValues = resolveSettings(project: project, target: target)
    return Meeting(key, first: single(project[key]), second: single(target[key]), result: single(effectiveValues[key]))
}

let projectSettings = ["optimization": "none", "deployment": "15.0", "warnings": "all"]
let targetSettings = ["optimization": "speed", "deployment": "17.0"]

let settingMeetings = ["optimization", "deployment", "warnings"].map {
    settingMeeting($0, project: projectSettings, target: targetSettings)
}
for meeting in settingMeetings { meeting.report() }
summarize(settingMeetings)
```

```
optimization: first none | second speed | result speed -> precedence
deployment: first 15.0 | second 17.0 | result 17.0 -> precedence
warnings: first all | second empty | result all -> out-of-scope
precedence: 2
intersection: 0
union: 0
rejection: 0
```

When two different values met, the target's won: two meetings came out as **precedence**. The third line was not counted as a meeting. The warning level was never written in the target, so the second definition was empty and the classifier left it out of scope. This is the right result: a setting that was not written is not a definition, it is only the ground for inheritance.

The consequence of this finding is a practical rule. Rewriting in the target a setting that is written in the project ignores the project's value. If you want to change a setting for all targets from one place, you have to remove the same key from the targets; otherwise the change in the project reaches no target.

**DE1.** This measurement key is for settings that carry a plain value. Settings whose value is a list are measured separately in the next section. The form in which a value refers to another setting was not measured.

## The Inheritance Marker

List-valued settings behave differently. Search paths are a good example: when looking for headers and modules the compiler consults a list of directories, and the project and the target can each add directories to this list. If the target writes its own list, it overrides the project's list. But if the target puts the inheritance marker in its list, that is, writes the expression `$(inherited)`, the platform replaces this marker with the project's list. The same place of writing leads to two different resolutions: without the marker, precedence; with the marker, union.

The model builds this replacement as a flattening: each element of the target list expands to the project's list if it is the marker, and to itself otherwise. In the meeting, the first definition is the project's directories and the second definition is the directories the target wrote itself, that is, those outside the marker.

```swift
// main.swift — the inheritance marker merges the upper level's list into the target list
func expandPaths(project: [String], target: [String]) -> [String] {
    target.flatMap { $0 == "$(inherited)" ? project : [$0] }
}

func pathMeeting(_ label: String, project: [String], target: [String]) -> Meeting {
    let own = Set(target.filter { $0 != "$(inherited)" })
    return Meeting(label, first: Set(project), second: own, result: Set(expandPaths(project: project, target: target)))
}

let projectPaths = ["libs/shared", "libs/vendor"]
let withInherited = pathMeeting("with-inherited", project: projectPaths, target: ["$(inherited)", "src/local"])
let withoutInherited = pathMeeting("without-inherited", project: projectPaths, target: ["src/local"])
withInherited.report()
withoutInherited.report()
```

```
with-inherited: first libs/shared,libs/vendor | second src/local | result libs/shared,libs/vendor,src/local -> union
without-inherited: first libs/shared,libs/vendor | second src/local | result src/local -> precedence
```

The content of the target list was `src/local` in both cases; the only difference was the marker. With the marker the result is three directories and the resolution came out as **union**. Without the marker the result is the target's list and the resolution is again precedence. That two resolutions can sit side by side in the same field shows that guessing the resolution without looking at the type of the setting would mislead: for a plain value precedence is the only option, while in a list the marker chooses between two options.

The absence of the marker is a source of silent errors. A third-party directory added at the project level never enters the build because the target's list was written without the marker, and the symptom is the compiler failing to find a header. The error message points at the missing header, not at the target's list; for this reason problems of this kind are often looked for in the wrong place.

**DE2.** The measurement is for search paths and for two-level inheritance. In a chain of inheritance with more layers, the marker expanding separately at each layer was not measured. A list in which the marker is written more than once was not measured either.

## Platform Versions

The minimum version is not a set but a single value. The measurement at the start of the lesson already showed this: when the project wrote 15.0 and the target wrote 17.0, the effective value was 17.0 and the resolution came out as precedence. Versions do not intersect between the project and the target; the target's minimum version overrides the project's.

Intersection appears in another meeting. In the previous topic's last lesson we defined the versions the app supports as the versions the app is tested on. The lower end of this set is the target's effective minimum version. The other definition is the **run destinations** at hand in the development environment, that is, the versions of the simulators and devices the app can be run on. The app can only be run on a version that is in both sets: a simulator on an unsupported version cannot be chosen for a run, and a version that is not at hand cannot be tested. For this reason the model takes the intersection of the two sets.

```swift
// main.swift — the versions the app supports intersect with the versions of the run destinations
func runnableVersions(supported: Set<String>, destinations: Set<String>) -> Set<String> {
    supported.intersection(destinations)
}

let appSupported: Set<String> = ["17", "18"]
let destinationVersions: Set<String> = ["15", "16", "17"]
let rangeMeeting = Meeting(
    "deployment-range",
    first: appSupported,
    second: destinationVersions,
    result: runnableVersions(supported: appSupported, destinations: destinationVersions)
)
rangeMeeting.report()
```

```
deployment-range: first 17,18 | second 15,16,17 | result 17 -> intersection
```

The resolution is **intersection**. The app supports eighteen but there is no run destination on eighteen at hand; there are destinations on fifteen and sixteen at hand but the app does not support them. The two sets meet only at seventeen. This set can also come out empty: if none of the run destinations is on a supported version, the result is empty and the resolution can be nothing other than rejection. In such a case the app can be built, but no debugging session is opened because there is no destination to run on.

**DE3.** The measurement treated versions as a set of exact values. The form in which version ranges are written with an upper bound, and in which decimal versions are compared, was not measured in this lesson; it is the subject of the package manager lesson.

## The Run Scheme

A scheme does something different from these three settings. The project and the target define settings; the scheme says, for an action such as running or archiving, which **build configuration** to use. A configuration is a bundle of settings: the debug configuration turns optimization off, the release configuration turns it on. A scheme defines no setting; it selects one of the configurations the targets already carry.

This difference is measurable. We can write the scheme's choice as two sets as well: the first definition is the configurations the project offers, the second is the one the scheme chose. The choice is a part of what is offered.

## The Bounding Measurement

Because a scheme is a choice and not a definition, this claim has to be tested with a measurement. In the model below the project offers two configurations and the scheme chooses one configuration for each of two actions. Each action is built as a meeting; if the classifier treats the choice as a definition, it has to find a name for a resolution.

```swift
// main.swift — a scheme is a choice, not a definition; without two definitions there is no meeting
func pickConfiguration(action: String, scheme: [String: String]) -> String {
    scheme[action, default: "none"]
}

let available: Set<String> = ["debug", "release"]
let scheme = ["run": "debug", "archive": "release"]

let schemeMeetings = ["run", "archive"].map { action -> Meeting in
    let chosen = pickConfiguration(action: action, scheme: scheme)
    return Meeting("scheme-" + action, first: available, second: [chosen], result: [chosen])
}
for meeting in schemeMeetings { meeting.report() }
summarize(schemeMeetings)
```

```
scheme-run: first debug,release | second debug | result debug -> out-of-scope
scheme-archive: first debug,release | second release | result release -> out-of-scope
precedence: 0
intersection: 0
union: 0
rejection: 0
```

Both actions came out of scope and all four counters stayed at zero. The chosen configuration was a subset of what was offered every time; for this reason the classifier saw no meeting. The scheme's effect on the effective settings is real, but that effect is a choice, not the resolution of a meeting: choosing a configuration brings that configuration's settings into play, and the settings' own meetings are resolved by the rules at the start of this section.

This draws the limit of the lesson's rule. The rule names the resolution when two definitions exist; where no definition exists it stays silent. Writing a setting in the scheme is possible, an environment variable for example, but then the scheme becomes the owner of a definition and a new meeting arises. Environment variables are the business of the debugging topic.

**DE4.** This measurement is for the scheme in its choosing form. The case where the scheme carries settings of its own, and the case where one action uses more than one configuration together, were not measured.

## Summary

- If the same build setting is defined in the project and the target, the target level wins; a setting written only in the project is inherited by the target and produces no meeting.
- In list-valued settings the inheritance marker decides the resolution: union with the marker, precedence without it.
- The minimum version is single-valued and is overridden at the target; the intersection of the versions the app supports and the versions of the run destinations is the versions the app can be run on, and if the intersection is empty, the result is rejection.
- A run scheme is a choice, not a definition; since the chosen configuration is a subset of what is offered, it does not count as a meeting.
- This lesson's resolution: **precedence**. When the same build setting is defined at two levels, the target level overrides the project's value.

## Next Step

In this lesson the project's and the target's settings met. An app's settings do not live only in these two places: a property list that comes from a template, the keys the build generates itself, and the resource groups added to the same target raise the same question. The next lesson measures, in the project file structure, the meeting between the keys the template writes and the keys the build generates.
