---
title: 'iOS Platform and Tools'
source: 'https://academia.sh/en/courses/ios-platform-and-tools'
language: en
updated: '2026-10-21T05:03:10+00:00'
license: 'CC BY-SA 4.0'
lessons: 12
outcomes:
    - 'Explains the platform layers and where frameworks sit in those layers'
    - 'Manages the project structure and the build configuration'
    - 'Applies the app lifecycle and the interface guidelines'
    - 'Distinguishes whether, when two definitions meet, one takes precedence, the intersection or the union is taken, or the meeting is rejected'
    - "Builds the platform's resolution rule with a small model and recognizes that the model is not the platform itself"
---

# iOS Platform and Tools

In an iOS app the same thing is often defined in two places: a build setting at the project and target level, the screen orientations the app supports and the device offers, the version ranges two packages request for the same dependency, the same name exported by two modules. The course measures every mechanism with a single question — when these two definitions meet, how is it resolved? Platform layers, system and domain frameworks, the app lifecycle, interface guidelines, version compatibility, project structure, the package manager, the simulator, debugging and Objective-C interoperability all pass through the same frame.

## Platform Structure

Platform layers from the core operating system to the interface layer, system frameworks and libraries, frameworks for location, health, media, graphics, augmented reality and on-device models, the app lifecycle and background work, interface design guidelines, and the version and compatibility decision.

### Platform Layers

Source: https://academia.sh/en/courses/ios-platform-and-tools/platform-layers

Measuring how eight meetings of two definitions fall into four resolutions, and ordering the platform layers from the core operating system to the interface layer.

The previous course read the Swift language through a moment measure: is an expression evaluated immediately, deferred, or never. This course moves from the language to the platform. On the platform the same thing is often defined in two places: a build setting lives in both the project and the target, the screen orientations of an app live in both the app and the device, and the version range of a package lives in both the package itself and the packages that request it. What the system does when these two definitions meet is the measuring question of the course: **how are these two definitions resolved when they meet?** The umbrella name is **resolution**.

Four shapes are expected, and at first glance they all look the same. Sometimes one of the two definitions wins (precedence), sometimes only what is in both remains (intersection), sometimes everything that is in either one is collected (union), and sometimes no result comes out at all (rejection). This lesson builds eight small meetings and counts which one falls into which resolution. The platform layers are ordered after that count, to show where the resolutions live.

#### The Core

Every lesson uses the same helper: a type that holds two definitions, the result that comes out of their meeting, and the resolution the result falls into. `Meeting` does not change the result, it only classifies it. The result always comes from the model; that is, it is the output of real operations such as dictionary merging, filtering, name lookup and range checks, not a hand-written set. The classification order is as follows: if neither set is empty and neither is a subset of the other, the measurement is valid, otherwise `out-of-scope` is written; if there is no result or it is empty, `rejection`; if the result equals the union, `union`; if it equals the intersection, `intersection`; if it equals one of the sets, `precedence`; if it fits none of these, `off-rule`. Set values are printed sorted, because the iteration order of a `Set` can change from run to run.

```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)")
    }
}
```

#### Eight Meetings

The measurement below builds eight meetings. The two sides and the result of each are produced in the same form. In the build setting, the target level is written over the project level. In name lookup, the local definition is searched first, and the same name from two imported modules stays ambiguous. In screen orientation, what the app supports is filtered by what the device offers. In the version range, the ranges requested by two packages intersect. In the inherited paths, the target expands the project value with `$(inherited)`. In the property list, the template file and the keys generated from the settings are merged.

```swift
// main.swift — eight meetings, four resolutions
func parts(_ version: String) -> [Int] {
    version.split(separator: ".").map { Int($0)! }
}

func versions(in published: [String], from lower: String, below upper: String) -> Set<String> {
    Set(published.filter {
        !parts($0).lexicographicallyPrecedes(parts(lower))
            && parts($0).lexicographicallyPrecedes(parts(upper))
    })
}

func lookup(_ name: String, local: [String: String], imported: [[String: String]]) -> Set<String>? {
    if let found = local[name] { return [found] }
    let matches = imported.compactMap { $0[name] }
    return matches.count == 1 ? Set(matches) : nil
}

func expand(_ values: [String], inherited: [String]) -> [String] {
    values.flatMap { $0 == "$(inherited)" ? inherited : [$0] }
}

// Build setting: the target level overrides the project level
let projectLevel = ["SWIFT_VERSION": "5", "TARGETED_DEVICE_FAMILY": "1"]
let targetLevel = ["SWIFT_VERSION": "6"]
let effective = projectLevel.merging(targetLevel) { _, fromTarget in fromTarget }

// Name lookup: the local definition is searched first; the same name in two modules is ambiguous
let kitNames = ["format": "Kit.format", "Logger": "Kit.Logger"]
let toolsNames = ["Logger": "Tools.Logger"]
let localNames = ["format": "local.format"]

// Screen orientation: what the app supports and what the device offers
let appOrientations: Set = ["portrait", "portrait-upside-down"]
let deviceOrientations: Set = ["portrait", "landscape-left", "landscape-right"]

// Version ranges: what two packages request for the same dependency
let published = ["1.2.0", "1.3.0", "1.4.0", "2.0.0", "2.1.0"]
let wideFirst = versions(in: published, from: "1.2.0", below: "2.0.0")
let wideSecond = versions(in: published, from: "1.3.0", below: "3.0.0")
let narrowFirst = versions(in: published, from: "1.2.0", below: "1.4.0")
let narrowSecond = versions(in: published, from: "2.0.0", below: "3.0.0")

// Inherited paths: the target expands the project value with `$(inherited)`
let projectPaths = ["Shared/Headers"]
let targetPaths = ["$(inherited)", "Target/Headers"]

// Info.plist: the template file and the keys generated from the settings
let templateKeys = ["CFBundleName": "Sample", "UILaunchScreen": "-"]
let generatedKeys = ["CFBundleName": "Sample", "NSCameraUsageDescription": "Camera"]

let meetings = [
    Meeting("build-setting",
            first: [projectLevel["SWIFT_VERSION"]!],
            second: [targetLevel["SWIFT_VERSION"]!],
            result: [effective["SWIFT_VERSION"]!]),
    Meeting("local-shadowing",
            first: [kitNames["format"]!],
            second: [localNames["format"]!],
            result: lookup("format", local: localNames, imported: [kitNames, toolsNames])),
    Meeting("screen-orientation",
            first: appOrientations,
            second: deviceOrientations,
            result: appOrientations.filter { deviceOrientations.contains($0) }),
    Meeting("version-range",
            first: wideFirst,
            second: wideSecond,
            result: wideFirst.intersection(wideSecond)),
    Meeting("inherited-paths",
            first: Set(projectPaths),
            second: Set(targetPaths.filter { $0 != "$(inherited)" }),
            result: Set(expand(targetPaths, inherited: projectPaths))),
    Meeting("plist-keys",
            first: Set(templateKeys.keys),
            second: Set(generatedKeys.keys),
            result: Set(templateKeys.merging(generatedKeys) { current, _ in current }.keys)),
    Meeting("duplicate-name",
            first: [kitNames["Logger"]!],
            second: [toolsNames["Logger"]!],
            result: lookup("Logger", local: localNames, imported: [kitNames, toolsNames])),
    Meeting("disjoint-range",
            first: narrowFirst,
            second: narrowSecond,
            result: narrowFirst.intersection(narrowSecond)),
]

for meeting in meetings {
    meeting.report()
}
summarize(meetings)
```

```
build-setting: first 5 | second 6 | result 6 -> precedence
local-shadowing: first Kit.format | second local.format | result local.format -> precedence
screen-orientation: first portrait,portrait-upside-down | second landscape-left,landscape-right,portrait | result portrait -> intersection
version-range: first 1.2.0,1.3.0,1.4.0 | second 1.3.0,1.4.0,2.0.0,2.1.0 | result 1.3.0,1.4.0 -> intersection
inherited-paths: first Shared/Headers | second Target/Headers | result Shared/Headers,Target/Headers -> union
plist-keys: first CFBundleName,UILaunchScreen | second CFBundleName,NSCameraUsageDescription | result CFBundleName,NSCameraUsageDescription,UILaunchScreen -> union
duplicate-name: first Kit.Logger | second Tools.Logger | result none -> rejection
disjoint-range: first 1.2.0,1.3.0 | second 2.0.0,2.1.0 | result empty -> rejection
precedence: 2
intersection: 2
union: 2
rejection: 2
```

The last four lines of the output give the **2/2/2/2** count: two precedence, two intersection, two union, two rejection. `build-setting` and `local-shadowing` are precedence: the value of the second side passed into the result as it was. `screen-orientation` and `version-range` are intersection: only the values contained in both sides remained in the result. `inherited-paths` and `plist-keys` are union: the result carries everything from both sets. `duplicate-name` and `disjoint-range` are rejection: in one, no result was produced at all (`none`), in the other an empty set was produced (`empty`), and the rule puts both into the same resolution.

The even distribution is a product of the meetings that were built, not of the rule: we chose the eight examples ourselves, and two fell into each of the four resolutions. For this reason we draw no conclusion from the distribution; the measurement only shows that all four resolutions really exist and that each comes from a different operation.

**PS1.** This measurement is for the eight meetings above and the rules of the model that was written. It was not measured whether the build setting resolution of a real development environment or the version resolution of a real package manager applies these rules exactly; the model builds this rule of the platform and classifies it.

#### Layers and Resolutions

The platform is read from bottom to top as four layers, and each layer is built on the layers below it. At the bottom is the **core operating system** layer: memory, processes, networking and device drivers are here. Above it comes the **core services** layer: the services that every app needs but that are not visible, such as collections, date and file work, network clients and logging. The third layer is the **media** layer: graphics, audio and video. At the top is the **interface** layer: views, event handling and everything the app keeps alive on the screen. An app speaks from the interface layer, but it takes services from all three layers beneath it as well.

The resolutions are not distributed evenly over these layers; the same pair of definitions appears in different forms in different layers. The precedence and rejection resolutions of name lookup are the work of the language and the modules; the screen orientation looks at the interface layer and the device, and therefore at the hardware below; the build setting, the inherited paths and the property list belong to how the app is built, so they live in no runtime layer and live in the project and target definitions of the development environment. The version range belongs to the declaration of the package and is resolved at build time.

This ordering has a practical consequence: knowing which layer a definition sits in tells who resolves it, that is, whether the compiler, the package manager or the system decides. In the following lessons we meet every meeting with this question.

**PS2.** The number and names of the layers are a sequence reduced to four here for teaching purposes; exactly which layer a framework belongs to depends on the internal structure of the framework, which is not measured in this lesson, and it was not measured.

#### Who Does the Resolving

Grouping the eight meetings by the party that does the resolving makes the layer question concrete. The compiler does the name lookup: it decides which definition will be visible while a source file is being compiled, and the decision is never carried to run time. The build system resolves the build setting, the inherited paths and the property list; these three are found not inside the app but inside the process that produces the app. The package manager resolves the version range and writes the result to a lock file. The system resolves the screen orientation at run time: the app declares what it supports, the device knows what it offers, and the result appears only when the two meet.

This distinction tells two things in advance. First, when the party that resolves changes, the error also shows up in a different place: a rejection given by the compiler is a build error, while a rejection given by the system is noticed while the app is running. Second, the same pair of definitions can be resolved differently in two different places; knowing how a setting meets in the project and the target does not tell how a dependency is requested from two packages.

#### The Bounding Measurement

The four resolutions do not separate when one of the two definitions contains the other: two nested sets are equal to the outer one in union, to the inner one in intersection, to one of the two in precedence; for rejection there is no conflicting element, because every element of the inner set is also in the outer one. Even though the results differ, in this case the classification says nothing. The measurement below builds all four results from the same nested pair and shows that `Meeting` leaves all four out of scope.

```swift
// main.swift — two nested definitions: all four results fall into the same class
let outer: Set = ["Shared/Headers", "Target/Headers"]
let inner: Set = ["Shared/Headers"]

let nested = [
    Meeting("nested-precedence", first: inner, second: outer, result: inner),
    Meeting("nested-intersection", first: inner, second: outer, result: inner.intersection(outer)),
    Meeting("nested-union", first: inner, second: outer, result: inner.union(outer)),
    Meeting("nested-rejection", first: inner, second: outer, result: nil),
]

for meeting in nested {
    meeting.report()
}
summarize(nested)
```

```
nested-precedence: first Shared/Headers | second Shared/Headers,Target/Headers | result Shared/Headers -> out-of-scope
nested-intersection: first Shared/Headers | second Shared/Headers,Target/Headers | result Shared/Headers -> out-of-scope
nested-union: first Shared/Headers | second Shared/Headers,Target/Headers | result Shared/Headers,Target/Headers -> out-of-scope
nested-rejection: first Shared/Headers | second Shared/Headers,Target/Headers | result none -> out-of-scope
precedence: 0
intersection: 0
union: 0
rejection: 0
```

All four lines are `out-of-scope`, and all four counters are zero. However a nested pair is combined, it cannot be written into any of the four resolutions; that is, what separates the resolutions is not the operation but the two definitions really saying different things. In the rest of the course this turns into a warning: if one definition already contains the other, no meeting is built, because even when the result changes, the classification stays the same whichever rule is chosen.

**PS3.** This measurement is for the pair in which the `inner` set is a subset of the `outer` set only. That two equal sets and the case where one of the sets is empty also give `out-of-scope` is read from the conditions of `Meeting`, but it was not measured separately.

#### Summary

- When these two definitions meet, the result falls into one of four resolutions: precedence, intersection, union or rejection.
- In eight meetings the output gave 2 precedence, 2 intersection, 2 union and 2 rejection; the distribution is a result of the examples that were built, not of the rule.
- A result that never forms (`none`) and one that falls to an empty set (`empty`) are the same resolution: rejection.
- The platform is built from four layers (core operating system, core services, media, interface); the layer of a definition tells who does the resolving.
- Two nested definitions fall into no resolution: all four of the four results came out `out-of-scope`.
- The resolution of this lesson: **the four of the core (2/2/2/2)**.

#### Next Step

Two of the eight meetings were name lookup: the local definition shadowed the imported one, and the same name in two modules gave a rejection. In the next lesson we read frameworks and libraries with this eye: what happens when two modules open the same name, and why qualifying one removes the meeting.

### Frameworks and Libraries

Source: https://academia.sh/en/courses/ios-platform-and-tools/frameworks-and-libraries

Measuring that the visible names of two imported modules merge, that the same name is rejected, and that a local definition shadows an imported one.

The previous lesson sorted four of the eight meetings into four resolutions; one name lookup meeting gave precedence and one gave rejection. This lesson reads those two meetings closely. When an app imports several modules, the names each module opens meet in the same scope. The question does not change: **how are these two definitions resolved when they meet?** The resolution examined in this lesson is **union**.

First, two concepts need to be separated. A library is ready-made code that the app calls: the app chooses when to call it. A framework builds its own skeleton and calls the app's code inside that skeleton; the app enters the flow of the framework as a piece that handles an event. On the platform, frameworks that come with the system are called system frameworks, while code that an app adds as a dependency is mostly called a package or a library. Both are imported as a **module**, and the names both open stand side by side in the same source file. What is measured is this side-by-side arrival.

```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)")
    }
}
```

A module is a surface made of the names it opens to the outside. The helper below models three things: the name of a module and the names it opens, the total of the names that are visible when two or more modules are imported, and a lookup that finds which definition a name comes from. The lookup first checks the local definition; if there is no local definition, it searches among the imported modules; if the definition found is not unique, there is no result. There is also a use qualified with the module name: when `Graphics.Canvas` is written, only the name in the `Graphics` module is a candidate. This file has no output; it stands as a helper and is compiled together with the measured blocks that follow.

```swift
// Modules.swift — module surface, visible names and name lookup
struct Module {
    let name: String
    let exports: [String]
}

func visibleNames(of modules: [Module]) -> Set<String> {
    Set(modules.flatMap { $0.exports })
}

func qualified(_ name: String, from module: Module) -> Set<String>? {
    module.exports.contains(name) ? [module.name + "." + name] : nil
}

func owners(of name: String, in modules: [Module], local: Set<String>) -> Set<String>? {
    if local.contains(name) { return ["local." + name] }
    let found = modules.filter { $0.exports.contains(name) }.map { $0.name + "." + name }
    return found.count == 1 ? Set(found) : nil
}
```

#### Importing Two Modules

When two modules are imported, the names both open are visible in the same scope. The measurement below builds a graphics module and a storage module and looks at the names they open with `visibleNames`. There is no shared name; for this reason no name replaces another, and no definition overrides another.

```swift
// main.swift — importing two modules collects the visible names
let graphics = Module(name: "Graphics", exports: ["Canvas", "draw", "render"])
let storage = Module(name: "Storage", exports: ["load", "save"])

let imports = Meeting("visible-names",
                      first: Set(graphics.exports),
                      second: Set(storage.exports),
                      result: visibleNames(of: [graphics, storage]))
imports.report()
```

```
visible-names: first Canvas,draw,render | second load,save | result Canvas,draw,load,render,save -> union
```

The result is the whole of both surfaces: all five names are visible, and none dropped. This is a union, and it is the ordinary result of adding a framework; when a new module is imported, the number of names the app can see does not fall, it grows. This is also the source of the danger: as the number of visible names grows, so does the chance that two modules open the same name.

**PS4.** This measurement is for a pair of two modules with no shared name. The case where the names of one module are a subset of the other's was not measured; in that case the meeting becomes `out-of-scope`.

#### System Framework and Package

Where an imported module comes from does not change how names meet, but it changes who controls it. System frameworks are present on the device together with the operating system; the app links them at build time and uses the system's own copy while running. For this reason their version depends not on the app but on the version of the operating system it runs on, and whether a name exists can change with that version; we will measure this in the version lesson. A library added as a package is requested with a dependency declaration, resolved at build time and shipped with the app; the app chooses its version.

Each `import` line is valid for a single file. The names visible in a file are the union of the surfaces of the modules that file imports, plus the app's own definitions. Another file cannot see the top-level names of a module unless it imports the same module. So the meeting is not a one-time event for an app; it is a meeting set up separately for each file: a name on which two modules collide is experienced only by the files that import both.

#### The Same Name and a Local Definition

When two modules open the same name and the use is not qualified, the lookup finds two candidates. There is no information about which one is meant, and the compiler does not pick one: there is no result, and the resolution is rejection. If the same name is also defined in the app's own source, the picture changes. The local definition comes before the imported one; that is, the imported name still exists but does not appear in the lookup, which is called shadowing, and the resolution is precedence. The measurement below builds both cases: the graphics and charts modules both open the name `Canvas`, and the app defines its own `render`.

```swift
// main.swift — the same name stays ambiguous, a local definition shadows it
let graphics = Module(name: "Graphics", exports: ["Canvas", "draw", "render"])
let charts = Module(name: "Charts", exports: ["Canvas", "plot"])
let local: Set = ["render"]

let cases = [
    Meeting("same-name",
            first: qualified("Canvas", from: graphics)!,
            second: qualified("Canvas", from: charts)!,
            result: owners(of: "Canvas", in: [graphics, charts], local: [])),
    Meeting("local-definition",
            first: qualified("render", from: graphics)!,
            second: owners(of: "render", in: [], local: local)!,
            result: owners(of: "render", in: [graphics, charts], local: local)),
]

for item in cases {
    item.report()
}
summarize(cases)
```

```
same-name: first Graphics.Canvas | second Charts.Canvas | result none -> rejection
local-definition: first Graphics.render | second local.render | result local.render -> precedence
precedence: 1
intersection: 0
union: 0
rejection: 1
```

In the `same-name` line the result is `none`: the lookup saw two candidates and chose neither. In the `local-definition` line the result is the second side, that is, the local definition; the imported `Graphics.render` dropped out. The surfaces of the same two modules had given a union in the previous measurement; the resolution of the meeting came not from the surfaces themselves but from who holds the name being looked up. The total of the surfaces causes no trouble as long as no name is looked up; once the lookup starts, the name being looked up has to resolve to a single definition.

An example outside the lesson that builds the same shadowing with a real system module follows. Because the piece depends on a platform module, it stands as source and no output is claimed for it.

```swift
// Source snippet: a local Data definition shadows the name Foundation.Data
import Foundation

struct Data {
    let bytes: [UInt8]
}

let own = Data(bytes: [1, 2])
let system: Foundation.Data = Foundation.Data()
```

The local `Data` takes the bare name; the type in the system framework stays reachable only by qualifying it with the module name. The model builds this rule of the platform: the lookup in the measured block reproduces the same precedence in the `local-definition` line.

**PS5.** This measurement is only for the precedence of name lookup and for the pair in which two imported modules open the same name. The case where a name is defined in both a type and a function, names resolved by overloading, and the case where three modules open the same name were not measured.

#### The Surface a Module Opens

The surface a module opens to the outside (its public surface) is all the declarations that the code importing it can see: types, functions, constants, protocols. Declarations that stay inside the module do not enter the surface, and the importing code cannot reach them. This distinction is the practical face of the difference between a library and a framework. A library generally opens a small surface, and the calling code chooses which function to call and when. A framework opens a wider surface; a significant part of this surface consists of places the app is expected to fill: conforming to a protocol, subclassing a class, writing an event handler. The framework manages the flow, and the app supplies its own piece.

The width of the surface also determines the chance of name collisions. When a source file imports several modules, the names each module opens become visible in that file. As the number of modules grows and the surfaces widen, so does the chance that two modules open the same name. The compiler does not report this collision at import time; it reports it if a name cannot be resolved at the place where it is used. For this reason, in the `same-name` measurement of the previous sections, the resolution does not appear until the line that uses the name: the two definitions stand side by side, and the rejection is experienced only at the moment of use.

Another consequence is that a surface is a contract. The surface of a system framework comes with the system version, and the app cannot change it. The surface of a package is fixed by the version the app chose as a dependency, and when the party that writes the package changes it, the app is not affected until it selects the new version. Which party can change its surface at what speed determines how each dependency is to be handled; this is not the subject of the measurement, only the context of this section.

#### When a Qualified Name Is Needed

A qualified name puts the module name in front of a name to say explicitly which module that name comes from. As long as there is no collision, qualification is unnecessary and makes reading harder; if a name exists in only one module, it already has a single meaning. When there is a collision, qualification is the way in the language to choose one of the two candidates.

Qualification works differently from a local definition. A local definition is a declaration written in the app's own module, and it comes before declarations of the same name opened by imported modules; the resolution is precedence, and the reader can reach this result even without seeing it. Qualification, on the other hand, makes it visible to the reader: the name itself says which module it comes from, and the resolution no longer depends on a meeting, because a single definition remains. This is also why, in the limiting measurement at the end of the lesson, the `qualified-name` meeting comes out out of scope: qualification does not make two definitions meet, it removes the meeting.

A practical rule is this: if there is no collision, do not qualify; if there is a collision and the local definition is not the wanted result, consider renaming instead of qualifying. Changing the name of an imported module may not be in your hands, but changing the name of a local definition is. Qualification is the tool for cases in which renaming is not possible.

#### The Bounding Measurement

Rejection is not always the final state. When a use is qualified with a module name, the candidate set drops to one, because only the name in the written module is searched. This removes the meeting: the candidates before qualification are two, the candidate after is one, and the second is a subset of the first. The measurement below builds this.

```swift
// main.swift — a name qualified with its module narrows the candidates to one
let graphics = Module(name: "Graphics", exports: ["Canvas", "draw", "render"])
let charts = Module(name: "Charts", exports: ["Canvas", "plot"])

let all = qualified("Canvas", from: graphics)!.union(qualified("Canvas", from: charts)!)
let written = all.filter { $0.hasPrefix("Graphics.") }

let narrowed = Meeting("qualified-name", first: all, second: written, result: written)
narrowed.report()
```

```
qualified-name: first Charts.Canvas,Graphics.Canvas | second Graphics.Canvas | result Graphics.Canvas -> out-of-scope
```

The output is `out-of-scope`: the second candidate set is a subset of the first, so the two definitions no longer meet. Rejection is a situation that can be resolved. Here both colliding names belong to imported modules, and renaming either of them is not in your hands; this is exactly the case the rule above reserves for qualification, and the resolution is to qualify the name. A union causes no trouble as long as the name is not used.

**PS6.** This measurement is only for filtering by the `Module.name` prefix; the case in which the name of a module itself collides with the name of another module was not measured.

#### Summary

- A framework calls the app's code in its own flow, while the app calls a library; both are imported as modules, and the names they open meet in the same scope.
- The surfaces of two modules give a union when there is no shared name: the number of visible names only grows.
- If the same name is used from two modules without qualification, the lookup gives no result and the resolution is rejection.
- A local definition shadows an imported name; the resolution of this meeting is precedence.
- A name qualified with its module narrows the candidate set to one and the meeting disappears: the output is `out-of-scope`.
- The resolution of this lesson: **union**.

#### Next Step

In this lesson, two definitions met as names. In the next lesson what meets is a permission: the permissions the app asks for and what the user grants. The domain frameworks that touch the user's data or a capability of the device (location, health, media, augmented reality) ask for these permissions; the resolution is intersection.

### Domain Frameworks

Source: https://academia.sh/en/courses/ios-platform-and-tools/domain-frameworks

Measuring that requested permissions intersect with granted ones, that a capability missing from the device gives rejection, and that the scope of a permission does not change the rule.

The previous lesson measured two modules opening the same name; what met was a name. In this lesson what meets is an authority: what the app asks for and what the user grants. The question stays the same: **how are these two definitions resolved when they meet?** The resolution examined in this lesson is **intersection**: in the result, only the authorities contained in both sides remain.

Some of the system frameworks are general purpose: collections, dates, files, networking. Others are dedicated to a single domain and are called domain frameworks. The location framework (Core Location) reports where the device is; the map framework (MapKit) shows it on a map; the health framework (HealthKit) reads and writes health data; the media framework (AVFoundation) manages audio and video capture and playback; the graphics frameworks take over drawing; the augmented reality framework (ARKit) turns the image from the camera into a model of the surroundings; the on-device model framework (Core ML) runs a trained model on the device. What these frameworks have in common is that most of them touch the user's data or a capability of the device. What they touch is defined in two separate places: the app declares what it asks for, and the user and the device determine what they offer.

```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)")
    }
}
```

#### Permission, Usage Description and Device Capability

Three concepts need to be separated. A usage description is the reason, written in the app's property list (`Info.plist`) for a permission, that is shown to the user; if there is no description, the permission question never reaches the user. A permission is the authority that the user gives or does not give to the app, and it can change independently of the app's request. A device capability is a feature the hardware offers: a camera, an accelerometer, or a depth sensor. The app says what it asks for, the user says what they grant, the device says what it offers; a feature of the app works only if the three parties agree.

When location permission is requested, the usage description is written to the property list as a key, and the code asks for the authority only while the app is in use. The two pieces below depend on a platform framework, so they stand as source and no output is claimed for them.

```xml
<key>NSLocationWhenInUseUsageDescription</key>
<string>Location is used to show nearby stops.</string>
```

```swift
// Source snippet: location permission is requested only while the app is in use
import CoreLocation

let manager = CLLocationManager()
manager.requestWhenInUseAuthorization()
```

The helper below models two of these three parties: a filter that finds which of the requested permissions are granted, and a filter that finds which of the requested capabilities exist on the device. Both filter one set against the other, so the result is the intersection of two sets. This file has no output.

```swift
// Permissions.swift — filters requested permissions and capabilities against what is granted
func granted(requested: Set<String>, grants: [String: String]) -> Set<String> {
    requested.filter { grants[$0] != nil }
}

func usable(requested: Set<String>, hardware: Set<String>) -> Set<String> {
    requested.filter { hardware.contains($0) }
}
```

#### Requested and Granted

In the measurement below the new version of the app requests three permissions: camera, location, and health. The user has declined the camera prompt and granted the location and health permissions. The microphone, on the other hand, is a permission that the previous version of the app requested and the user granted at the time: the new version no longer requests the microphone, but the granted permission stays in the app's record on the device. So each side has an element the other lacks.

```swift
// main.swift — requested permissions intersect with granted ones
let requested: Set = ["camera", "location", "health"]
let grants = ["location": "when-in-use", "microphone": "granted", "health": "granted"]

let request = Meeting("permission-request",
                      first: requested,
                      second: Set(grants.keys),
                      result: granted(requested: requested, grants: grants))
request.report()
```

```
permission-request: first camera,health,location | second health,location,microphone | result health,location -> intersection
```

The `camera`, which was requested but declined, dropped out, and the `microphone`, which is left over from the previous version and not requested by the new one, dropped out as well. The two names that remain are present on both sides: the set of authorities the app can use is the common part of what it asks for in this version and what is granted in the user's record. Within a single version the granted permissions are a subset of the requested ones; the surplus in the record arises only when the request set changes between versions. It differs from the union in the previous lesson in direction: there the number of names grew, here it shrinks.

**PS7.** This measurement is for a single case made of three requests and three granted permissions in the record. Revoking a permission later, the user denying a permission without asking, and a permission being granted only once were not measured.

#### A Capability the Device Lacks

On the capability side, the intersection appears in two forms. The app asks for two capabilities and the device offers only one of them: the result is an intersection, and the app works with the remaining capability. The app asks only for a capability the device lacks: the intersection is empty and the resolution is rejection. The measurement below builds these two cases side by side.

```swift
// main.swift — a capability the device lacks empties the intersection
let hardware: Set = ["accelerometer", "camera"]

let cases = [
    Meeting("capability-partial",
            first: ["camera", "depth-sensor"],
            second: hardware,
            result: usable(requested: ["camera", "depth-sensor"], hardware: hardware)),
    Meeting("capability-none",
            first: ["depth-sensor"],
            second: hardware,
            result: usable(requested: ["depth-sensor"], hardware: hardware)),
]

for item in cases {
    item.report()
}
summarize(cases)
```

```
capability-partial: first camera,depth-sensor | second accelerometer,camera | result camera -> intersection
capability-none: first depth-sensor | second accelerometer,camera | result empty -> rejection
precedence: 0
intersection: 1
union: 0
rejection: 1
```

`capability-partial` continues with the loss of one capability; `capability-none` has no result. Both come from the same rule, because both are produced by the same filter, and the difference lies only in how close the requested set is to the device. Rejection here is not an error but information: if the app needs that capability, it is better to declare this before the app is installed on the device than to run into an unexpected gap after installation. Whether a capability is required or optional is a separate decision and was not measured here.

**PS8.** This measurement is for a device with two capabilities and a request with a single capability. A capability changing with the version or the region was not measured.

#### What Domain Frameworks Offer and Ask For

Each domain framework offers something different and asks for something different. The summary below is conceptual; it carries no claim about numbers or versions.

The location framework provides the geographic location of the device. Because it touches the user's data, it asks for a permission, and the permission can be granted in two scopes: only while the app is in use, or always. The map framework shows a location on a map; drawing the map needs no separate permission, but showing the user's own location depends on the permission of the location framework. The health framework reads and writes health data; for each data type the read and write permissions are asked separately, and the user may grant only some of them. The media framework reaches capture devices such as the camera and microphone, and audio and video playback; capture asks for a permission, while playback in most cases does not. The graphics frameworks take over drawing and, because they do not touch the user's data, ask for no permission. The augmented reality framework turns the camera image into a model of the surroundings; because it uses the camera, it depends on the camera permission, and the device must also carry hardware able to do this work. The on-device model framework runs a trained model on the device; as long as the model runs locally it asks for no permission, but if the input of the model comes from the camera or the microphone, the permission for that input is needed.

This list shows two things. The first is that a permission is a property not of the framework but of the data the framework touches: two different frameworks can share the same camera permission. The second is that a framework not asking for a permission does not mean it will run on every device; a device capability is a separate condition.

#### Usage Description

A usage description is the explanatory text, written in the app's property list, that is shown to the user in the permission dialog. For every data type for which a permission is requested, this description is mandatory, and if it is missing the platform either never shows the permission prompt or terminates the app. The description tells the user what the app wants this data for. A good description is concrete and explains only that feature; a broad and vague description leads the user to deny the permission.

An important distinction: a usage description is not a permission. The description is the precondition for showing the permission prompt. The meeting between what the app asks for and what the user grants happens after the description is written, when the permission prompt is shown. For this reason the earlier measurements bring the requested and the granted sets together and do not count the description as a separate party.

#### Permission Scope

Permission scope is how broad the granted permission is. In the location example there are two scopes: only while in use, and always. As the scope grows, so does the work the app can do; whether this difference reaches the meeting rule is shown by the limiting measurement at the end of the lesson. The scope is not a bargain; the user can narrow the scope from settings, and the app sees this while it runs. The app should assume that when the scope narrows, the work it can do shrinks, and should ask again at run time for the scope that was granted; a scope read once at start-up can lose its validity while the app runs.

The scope has two consequences. If the app keeps the scope it asks for as narrow as possible, the user grants the permission more readily and the app is responsible for less work. If the app asks for a broad scope, the platform may remind the user of this at certain intervals. In both cases the resolution is the intersection: the common part of what the app asked for and what the user granted.

#### When to Show the Permission Prompt

The timing of the permission prompt affects the likelihood that the permission is granted. A flow that asks for three permissions as soon as the app opens has not yet shown the user why they are needed; the user denies, and the denial is in most cases permanent. Asking for location permission when the "places near me" button is tapped on the map, and for camera permission when the first photo is taken, shows the user the context of the request and makes the explanation in the usage description concrete.

A denied permission is also a result, and the app must handle it. When a permission is not granted, the rest of the app should keep working, and only that feature should be disabled. If location is denied, the map still opens, but the user is asked to enter an address. If the user wants to change their decision later, the way is to direct them to settings; the platform does not ask the same permission a second time with a dialog. This shows why a permission is a meeting of two sides: the user's answer determines the app's next step. The rejection in the `capability-none` measurement is a similar situation: when no common result emerges, the app should offer a fallback path.

#### The Bounding Measurement

Location permission can be granted in two scopes: only while the app is in use, and always. The scope matters a great deal to the user, but not to the meeting rule: the rule looks at the common elements of two sets and sees the permission name as the element; it does not see in which scope the permission was granted. The measurement below builds the same request with a location permission in two different scopes.

```swift
// main.swift — the scope of a permission does not change the intersection
let requested: Set = ["camera", "location", "health"]
let whenInUse = ["location": "when-in-use", "microphone": "granted", "health": "granted"]
let always = ["location": "always", "microphone": "granted", "health": "granted"]

let scopes = [
    Meeting("location-when-in-use",
            first: requested,
            second: Set(whenInUse.keys),
            result: granted(requested: requested, grants: whenInUse)),
    Meeting("location-always",
            first: requested,
            second: Set(always.keys),
            result: granted(requested: requested, grants: always)),
]

for item in scopes {
    item.report()
}
```

```
location-when-in-use: first camera,health,location | second health,location,microphone | result health,location -> intersection
location-always: first camera,health,location | second health,location,microphone | result health,location -> intersection
```

The two lines are the same apart from the label: the two measurements give the same result and the same class. The difference the scope brings, namely that a location permission granted always is enough for more work than one granted while in use, lies in the meaning of the permission and stays outside this measurement. The class of the rule is not affected by the scope; to read the scope, the result has to be examined separately. For this reason a "while in use" permission enters the intersection count but does not say what the permission is enough for; the difference between reading a result as "the permission exists" and reading it as "the permission exists under every condition" stays outside the classification.

**PS9.** This measurement is only for two scopes and a single permission name. A scope changing a behavior such as receiving location in the background does not exist in this model and was not measured.

#### Summary

- A domain framework is a system framework dedicated to a single domain: location, health, media, graphics, augmented reality, and the on-device model are examples.
- The app asks for a permission, the user grants it, the device offers a capability; what is usable is the common part of what was requested and what was granted.
- Requested permissions intersect with granted ones: the camera dropped out, and location and health remained.
- A capability the device lacks empties the intersection, and the resolution is rejection.
- The scope of a permission (only while in use, or always) does not change the class of the rule; the rule looks at the set and does not read the scope.
- The resolution of this lesson: **intersection**.

#### Next Step

Up to here, the two definitions were two faces of one set. In the next lesson both become delegates that handle the same event: the app delegate and the scene delegate. If both define a handler for the same event, we measure which one runs; the resolution is precedence.

### The App Lifecycle

Source: https://academia.sh/en/courses/ios-platform-and-tools/app-lifecycle

Measuring that the scene delegate's handler takes precedence over the app delegate's, and that declared background modes intersect with those the platform permits.

In the previous lesson, what the app asks for met what the user grants, and the result was an intersection. In this lesson the two definitions become two handlers: both the app delegate and the scene delegate can handle the same event. The question is the same again: **how are these two definitions resolved when they meet?** The resolution examined in this lesson is **precedence**: one of the two handlers runs and the other does not.

The app lifecycle is the sequence of states an app passes through from its start to its termination. The app may not be running, it may be active in the foreground, it may be in the foreground but not active (for example, when a system alert covers the screen), it may be running in the background, or it may be suspended. Each state transition is an event, and the platform announces this event to the app by calling a handler. Where the handlers are defined is one of two places.

```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 App Delegate and the Scene Delegate

An app can have more than one window on the screen; each group of windows is managed as a scene, and each scene has its own lifecycle. The app delegate looks after events that concern the whole app: launch, notification registration, system-level alerts. The scene delegate handles the events that belong to that scene: the scene connecting, moving to the foreground, moving to the background. In an app that uses scenes, the foreground and background transitions arrive at the scene delegate; the older app-level handlers for these transitions are no longer called. This rule is the answer to the question of which handler runs when both are defined.

The piece below depends on a platform framework, so it stands as source and no output is claimed for it. Both delegates define a handler for the same transition.

```swift
// Source snippet: both delegates can handle the move to the background
import UIKit

final class AppDelegate: UIResponder, UIApplicationDelegate {
    func applicationDidEnterBackground(_ application: UIApplication) {
        // app-level handler
    }
}

final class SceneDelegate: UIResponder, UIWindowSceneDelegate {
    func sceneDidEnterBackground(_ scene: UIScene) {
        // scene-level handler
    }
}
```

The helper below models two rules. The first looks for a handler for an event: it returns the one in the scene delegate if there is one, otherwise the one in the app delegate, and gives no result if neither exists. The second filters the background modes the app declares against those the platform permits. The file has no output.

```swift
// Handlers.swift — resolves the event handler and the background modes
func handler(for event: String, scene: [String: String], app: [String: String]) -> Set<String>? {
    if let found = scene[event] { return [found] }
    if let found = app[event] { return [found] }
    return nil
}

func permittedModes(declared: Set<String>, platform: Set<String>) -> Set<String> {
    declared.filter { platform.contains($0) }
}
```

#### The Same Event, Two Handlers

The measurement below sets up two handlers for the `enter-foreground` event: one in the app delegate and one in the scene delegate. The app delegate also defines an event that only it handles. `handler` returns which handler will run.

```swift
// main.swift — the scene handler takes precedence over the app handler
let appHandlers = ["enter-foreground": "app.enter-foreground", "enter-background": "app.enter-background"]
let sceneHandlers = ["enter-foreground": "scene.enter-foreground"]

let event = "enter-foreground"
let both = Meeting("handler",
                   first: [appHandlers[event]!],
                   second: [sceneHandlers[event]!],
                   result: handler(for: event, scene: sceneHandlers, app: appHandlers))
both.report()
```

```
handler: first app.enter-foreground | second scene.enter-foreground | result scene.enter-foreground -> precedence
```

The result is the second side, the scene handler: the app handler dropped out. This precedence works as a rule and not as a race; it does not depend on which was written first or which was compiled first. If the scene handler were not defined, the app handler would remain, as in the `enter-background` event; in that case no meeting is formed, because there is only one definition.

**PS10.** This measurement is only for the `enter-foreground` event and a handler defined in both delegates. The model builds this rule of the platform; whether the real system follows the same order for other events (launch, notification registration) was not measured.

#### Background Modes

The app declares in its property list what it can do in the background: receiving location updates, playing audio, fetching data. The declaration alone is not enough; the platform applies its own conditions to each mode and may not permit every declared mode. The modes that can run are the common part of those declared and those the platform permits.

```xml
<key>UIBackgroundModes</key>
<array>
    <string>location</string>
    <string>audio</string>
    <string>fetch</string>
</array>
```

The measurement below declares three modes: location, audio, and fetch. The ones the platform permits are audio, fetch, and notification. The mode names are model names, not the real keys in the property list.

```swift
// main.swift — declared modes intersect with those the platform permits
let declared: Set = ["location", "audio", "fetch"]
let platform: Set = ["audio", "fetch", "notification"]

let modes = Meeting("background-modes",
                    first: declared,
                    second: platform,
                    result: permittedModes(declared: declared, platform: platform))
modes.report()
```

```
background-modes: first audio,fetch,location | second audio,fetch,notification | result audio,fetch -> intersection
```

`location` was declared but is not permitted on the platform side, so it dropped out; `notification` exists on the platform but the app did not declare it, so it dropped out too. The two remaining modes were found on both sides. This meeting has the same form as the permission flow in the previous lesson and gives the same resolution; the difference is that the party that supplies the result is the platform and not a user.

**PS11.** This measurement is for three declared modes and three permitted modes. A mode depending on additional conditions (for example an authority) does not exist in this model and was not measured.

#### State Transitions and Expected Work

Apart from the not-running state, an app has four states. In the inactive state the app is in the foreground but receives no events; for example, it stays briefly in this state when a system alert covers the screen or during a transition. In the active state the app is in the foreground and receives events. In the background state the app is not visible on the screen but can run code. In the suspended state the app stays in memory but does not run code.

The work expected from the app differs at each transition. When moving from active to inactive, the app pauses work that would keep the user waiting: animations, game loops, timers. When moving to the background, the app saves unsaved data, releases shared resources, and frees heavy memory; this work must be short, because the platform gives the app only a limited time for it. When returning to the foreground, the app refreshes its interface with current data and resumes the work it paused. While suspended, the app can do nothing; when memory is needed, the platform can terminate a suspended app without notice. For this reason the final save must be finished before the move to the suspended state; the app does not assume that it will be given a chance before it is terminated.

That the order of transitions is not measured is therefore a deliberate limit: in a real system the order matters, and work done in the wrong order can lead to data loss. But the question of this lesson is the meeting and not the order; which handler will be called is determined by the definitions of the two delegates, not by the order.

#### The Limits of Background Modes

Background modes give the app the right to work in the background, but this right is limited by the purpose of the mode. An app that plays audio can keep playing in the background, but if it stops playing audio it also loses its right to work in the background. An app that receives location updates is woken only when the location changes. In the fetch mode the platform wakes the app at a time of its own choosing, and the app has to finish its work within a short time; the app cannot determine this time.

What these limits have in common is that the right does not come from the declaration. In the previous measurement, the location mode was declared but dropped out because it was not among those the platform permits. Declaring a mode is a request, not a right; to limit unnecessary resource consumption, the platform checks that the mode is actually used and may restrict a mode that is not used. For this reason the app should declare only the modes it really uses and keep its background work in line with the purpose of the mode.

One last limit belongs to the user. The user can turn off background refresh for the app; in that case even a mode that is declared and permitted by the platform does not run. The intersection in the previous measurement is therefore a lower bound: a third party, the user's setting, can narrow the set further.

#### Scene Lifetime and Preserving State

The lifecycle of the scene delegate is separate from that of the app. The platform can detach a scene in the background from the system when memory is needed; the scene closes, but the app process may keep running. When the user returns to the scene, the platform reconnects it and this appears as a new connection: the scene delegate receives the connection event, but it has to rebuild the earlier state of the interface itself. For this reason the user-related state of a scene, for example which screen it was on and an input left half finished, should be saved when the scene moves to the background and restored when it reconnects.

This division of work also shows why the precedence in the previous measurement is useful. In an app with multiple windows each scene has its own foreground and background transitions: one can be in the foreground while another is in the background. The app delegate sees this situation as a single event; only the scene delegate knows which scene moved to which state. That the scene handler takes precedence over the app handler is therefore not a preference but a necessity: the right owner of an event is the party that knows the event best.

The app delegate is not disabled entirely. System-level work, for example remote notification registration and the first launch of the app, still belongs to it. The rule draws this boundary: an event that belongs to a scene goes to the scene, and an event that belongs to the app goes to the app. The cases in which both handlers are really defined for the same event are the only cases in which the resolution comes out as precedence; for the remaining events no meeting is formed in the first place.

#### The Bounding Measurement

When describing the lifecycle, the order of the transitions comes to the front: first to the foreground, then to the background, then to the foreground again. This measurement does not measure the order. What is measured is only the meeting of two definitions, and the meeting is independent of the order in which a transition arrives. To see this, we resolve the same two events twice in reverse order.

```swift
// main.swift — the order of transitions does not change the meeting
let appHandlers = ["enter-foreground": "app.enter-foreground", "enter-background": "app.enter-background"]
let sceneHandlers = ["enter-foreground": "scene.enter-foreground", "enter-background": "scene.enter-background"]
let forward = ["enter-foreground", "enter-background"]
let backward = ["enter-background", "enter-foreground"]

func resolved(_ events: [String], scene: [String: String], app: [String: String]) -> Set<String> {
    Set(events.compactMap { handler(for: $0, scene: scene, app: app) }.flatMap { $0 })
}

let orders = [
    Meeting("order-forward",
            first: Set(forward.map { appHandlers[$0]! }),
            second: Set(forward.map { sceneHandlers[$0]! }),
            result: resolved(forward, scene: sceneHandlers, app: appHandlers)),
    Meeting("order-backward",
            first: Set(backward.map { appHandlers[$0]! }),
            second: Set(backward.map { sceneHandlers[$0]! }),
            result: resolved(backward, scene: sceneHandlers, app: appHandlers)),
]

for item in orders {
    item.report()
}
```

```
order-forward: first app.enter-background,app.enter-foreground | second scene.enter-background,scene.enter-foreground | result scene.enter-background,scene.enter-foreground -> precedence
order-backward: first app.enter-background,app.enter-foreground | second scene.enter-background,scene.enter-foreground | result scene.enter-background,scene.enter-foreground -> precedence
```

The two lines are the same apart from the label: the sets are printed sorted, and the order in which the transitions are resolved does not change the result. Under which conditions and in which order the transitions are triggered in a real system is a separate subject; it does not enter the measurement axis of this course.

**PS12.** This measurement is for two events and two orders; in both of them the scene delegate handles both events. The mixed case in which the scene handles only one was not measured.

#### Summary

- The app lifecycle is the sequence of state transitions between foreground and background, and each transition is announced to a handler.
- In an app that uses scenes, the foreground and background transitions arrive at the scene delegate.
- If two handlers are defined for the same event, the scene handler takes precedence over the app handler: the output is precedence.
- Declared background modes intersect with those the platform permits; location dropped out, and audio and fetch remained.
- The order of transitions does not change the class of the meeting; what is measured is only the meeting of two definitions.
- The resolution of this lesson: **precedence**.

#### Next Step

The precedence between the scene and the app was a resolution in which one side takes the place of the other. In the next lesson the same resolution is set up between the user and the app: the user's text size setting takes precedence over the app's default. We read the interface design guidelines with this in mind.

### Interface Design Guidelines

Source: https://academia.sh/en/courses/ios-platform-and-tools/interface-design-guidelines

Measuring that the user's text size takes precedence over the app default, that screen orientations intersect, and that colors merge.

In the previous lesson the scene handler took precedence over the app handler. In this lesson the same resolution brings together two new parties: the app's default text size and the user's setting. The question does not change: **how are these two definitions resolved when they meet?** Three meetings will be measured: precedence, intersection, and union.

Interface design guidelines are the expectations the platform sets for an interface: the size of a touch target, the readability of text, the consistency of colors in light and dark appearance, and the orientation of the screen. These guidelines are not a compiler check; an app that does not follow them still builds and runs. A guideline sets an expectation, and the platform enforces a part of these expectations through settings. The measurements in this lesson do not measure the guideline itself; they measure the meeting of the three settings the guideline relies on.

```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)")
    }
}
```

#### Dynamic Type

The user chooses a preferred text size in the system settings. Dynamic Type is how this preference reaches the app: instead of writing a fixed point size, the app asks for a text style, and the platform applies the user's size to the style. For interface accessibility this means that a user with low vision can read the app at their own size.

The piece below depends on a platform framework, so it stands as source and no output is claimed for it.

```swift
import SwiftUI

struct SummaryHeader: View {
    var body: some View {
        Text("Meeting summary")
            .font(.body)
    }
}
```

`.font(.body)` does not write a fixed size; it asks for a style. The real point size is determined by the user's setting; the model builds this rule of the platform.

**PS13.** The numbers below are model values: they do not show the real point size of a real style. Because `Meeting` works with sets, the sizes are carried as strings.

The app's default is 16, and the size the user chose is 22. The resolution gives the one the user chose.

```swift
// main.swift — the user's text size takes precedence over the app default
let appDefault = "16"
let userChoice = "22"

let size = Meeting("text-size",
                   first: [appDefault],
                   second: [userChoice],
                   result: [userChoice])
size.report()
```

```
text-size: first 16 | second 22 | result 22 -> precedence
```

The app's 16 dropped out and the user's 22 remained. This is a precedence in which, because the setting belongs to the user, the user's word and not the app's carries. Instead of breaking this precedence, the app builds its interface for growing text: lines get longer and the layout moves downward.

#### Screen Orientation

Screen orientation is the set of directions in which the interface can be rotated. The app declares the orientations it supports; the device declares the orientations it offers. The orientations it can turn to are the common part of the two. The property list fragment below declares the app side.

```xml
<key>UISupportedInterfaceOrientations</key>
<array>
    <string>UIInterfaceOrientationPortrait</string>
    <string>UIInterfaceOrientationLandscapeLeft</string>
</array>
```

The helper below gives the common orientations of two sets. The file has no output.

```swift
// Orientations.swift — the screen orientations shared by the app and the device
func sharedOrientations(app: Set<String>, device: Set<String>) -> Set<String> {
    app.intersection(device)
}
```

The app supports portrait, upside-down portrait, and left landscape; the device offers portrait, right landscape, and left landscape. Neither set is a subset of the other.

```swift
// main.swift — screen orientations intersect
let appSide: Set = ["portrait", "portrait-upside-down", "landscape-left"]
let deviceSide: Set = ["portrait", "landscape-right", "landscape-left"]

let turns = Meeting("orientation-support",
                    first: appSide,
                    second: deviceSide,
                    result: sharedOrientations(app: appSide, device: deviceSide))
turns.report()
```

```
orientation-support: first landscape-left,portrait,portrait-upside-down | second landscape-left,landscape-right,portrait | result landscape-left,portrait -> intersection
```

`portrait-upside-down` existed only in the app and `landscape-right` only on the device; both dropped out. **PS14.** The orientation names are model names, and these sets do not represent the real orientations of a real device. This measurement shares its resolution with the background modes of the previous lesson: neither side hands over its own value, and only what is common is left.

#### Color Palette

The system interface offers its own named colors, and the app adds its own colors. The system colors, which take separate values in light and dark appearance, are ready at hand for the app; the app also defines its own brand colors. The palette that can be used is the sum of the two. The helper below merges the names of the two palettes.

```swift
// Palette.swift — merges the system and app color names
func mergedColorNames(system: [String: String], app: [String: String]) -> Set<String> {
    Set(system.keys).union(app.keys)
}
```

```swift
// main.swift — system and app colors merge
let systemColors = ["background": "system", "text": "system", "accent": "system"]
let appColors = ["brand": "app", "accent": "app"]

let palette = Meeting("color-palette",
                      first: Set(systemColors.keys),
                      second: Set(appColors.keys),
                      result: mergedColorNames(system: systemColors, app: appColors))
palette.report()
```

```
color-palette: first accent,background,text | second accent,brand | result accent,background,brand,text -> union
```

Four names came from two sources. `accent` is present on both sides and appears once in the union. This measurement counts only the union of names; which one is valid when the same name carries different values on the two sides is a separate precedence question and was not measured here.

#### The Expectation of Platform Consistency

The basic expectation of the interface design guidelines is consistency: if an app follows the patterns the platform has made familiar, the user uses it without learning it. The back gesture is in the same place and has the same form in every app; the share button carries the same icon; a list row is swiped in the same way. If an app breaks these patterns, the user has to learn a rule specific to that app. Consistency is not a prohibition but a calculation of cost: an app that breaks a pattern takes the user's learning burden upon itself.

The link of this expectation to the settings is this: the platform patterns are written into the system components. When the app uses a system component, it receives automatically the pattern the guideline calls for, Dynamic Type, and the appearance (light or dark). When the app draws its own component, it has to provide each of these behaviors itself. The precedence and union measurements in the earlier sections are both a result of this division of work: the owner of the setting is the user, and the component follows it.

#### Accessibility

Interface accessibility is not limited to text size. For a user who uses a screen reader, every control must have a name and a role: an icon button that is only an image means the reader cannot say anything about it. For a user with color blindness, information must not be given by color alone; the state should also be indicated with an icon or text. For a user who chooses to reduce motion, transition animations should be simplified. For a user who chooses dark appearance, colors must be readable on both light and dark backgrounds; for this reason system colors are not fixed colors but names that change with the appearance.

None of these is a requirement for the app; the compiler does not check them. But all of them are settings that belong to the user, and a setting takes precedence over the app's default. The precedence measured in Dynamic Type is only one example of this general rule: whatever the user's setting is, the app must be prepared to meet it.

#### Navigation and the Touch Target

Navigation is how the user moves between the sections of an app. The guideline recognizes a few patterns: a hierarchy at the top that can be navigated back through, a tab bar at the bottom that switches between equivalent sections, and a sheet that opens from the top for a temporary task. The pattern must fit the structure of the content: equivalent sections belong in the tab bar, and content that proceeds step by step belongs in a hierarchy. A pattern that does not fit the structure makes the user lose direction.

A touch target is the area the user hits with a finger. The guideline expects this area to be wide enough for a finger to hit comfortably: a small icon, even if it looks small, should provide a sufficient area together with the space around it. Space should also be left between two adjacent targets, otherwise the user touches the wrong one. The touch target grows together with the text size: if the user has enlarged the text, the targets also need to grow; otherwise the growing text becomes unreadable in a cramped layout.

All of these topics say the same thing: a guideline is an expectation and not a class of resolution. This is also the reason a result such as 20 will be marked `off-rule` in the bounding measurement at the end of the lesson; unless the guideline says "suitable", the measurement cannot assign a class.

#### Light and Dark Appearance

The platform offers two appearances: light and dark. The user chooses one or lets the system change it during the day. The app must follow this choice, because the choice is the user's setting. An app that writes a fixed color code can become unreadable on a dark background, while an app that uses system color names automatically receives a value that changes with the appearance. The `background` and `text` names in the color palette measurement matter for this reason: the name is fixed, and the value behind it changes with the appearance.

The app's own colors must also be defined separately for the two appearances. A brand color must be dark enough on a light background and light enough on a dark background, otherwise the contrast between text and background falls and readability is lost. Contrast is not only about beauty but about accessibility: text with low contrast cannot be read by a user with weak eyesight.

#### Safe Area and Adaptive Layout

The safe area is the part of the screen where content can be placed safely. The rounded corners of the device, the region where the camera enters the screen, and the system bars can cover part of the content; the platform defines the safe area outside these regions. Text and controls should be placed inside the safe area. A background color or image, on the other hand, can extend outside the safe area to the edge of the screen, because being covered causes no problem.

Adaptive layout is a layout adapting itself to the size and orientation of the screen, such as one column on a narrow screen and two columns on a wide one. Here there is a link to the screen orientation measurement: the intersection of the orientations the app supports and the device offers says in which orientations the layout must work. If only portrait remains in the intersection, there is no need to write a landscape layout; if the intersection is wide, a layout must be tested for each orientation. So the intersection is also a work list.

#### Departing from the Guideline

Because a guideline is an expectation, departing from it is possible, but the departure must have a reason. A game or a drawing app may not follow the standard navigation patterns, because the content itself demands a different form of interaction. A reading app, on the other hand, should not break the standard patterns, because nothing is gained in return for breaking them. The cost of departing is the user's learning burden; if there is a gain that justifies this burden, departing is reasonable.

But some expectations are not open to departure: following the user's text size setting, the accessibility settings, and the choice of appearance. These are settings that belong to the user, and the app cannot treat them as void. This is what the precedence measurement shows: the user's setting comes before the app's default; the app may depart from the guidelines, but it cannot depart against the user's setting.

#### The Bounding Measurement

A guideline is an expectation, not a class of resolution. For this reason a result that does not follow the guideline is not counted as one of the four resolutions. In the measurement below, the app says 16 and the user says 22; the result comes out as 20. This is neither the app's value nor the user's value; it is a value in between.

```swift
// main.swift — an off-rule result does not fall into one of the four
let clamped = Meeting("clamped-size",
                      first: ["16"],
                      second: ["22"],
                      result: ["20"])
let inside = Meeting("within-bounds",
                     first: ["16"],
                     second: ["22"],
                     result: ["22"])
clamped.report()
inside.report()
```

```
clamped-size: first 16 | second 22 | result 20 -> off-rule
within-bounds: first 16 | second 22 | result 22 -> precedence
```

20 did not fall into any of the four resolutions and became `off-rule`. A guideline saying "limit the text size" does not mean that this resolution is added to the four classes; if the result is a value that is not the output of the four operations on sets, the measurement does not classify it and marks it as it is.

**PS15.** This measurement is only for 16, 22, and 20 with model values. Whether a real system actually produces an intermediate value was not measured.

#### Summary

- Interface design guidelines are the expectations the platform sets for an interface; they are not a compiler check.
- In Dynamic Type the user's setting takes precedence over the app's default: the output is precedence.
- The screen orientations the app supports and those the device offers intersect; only the common orientations remain.
- System colors and app colors merge; a shared name appears once.
- An off-rule result is not counted as one of the four resolutions and is marked separately.
- The resolution of this lesson: **precedence**.

#### Next Step

In the earlier lessons the parties that met were settings. In the next lesson the parties that meet are version ranges: the versions the app supports and the versions in which an interface exists intersect, and when they are disjoint the result is rejection.

### Versions and Compatibility

Source: https://academia.sh/en/courses/ios-platform-and-tools/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.

## The Development Environment

Project, target and scheme, project file structure and configuration, the package manager and version constraints, the simulator and device difference, debugging basics with breakpoints, stepping and logs, interoperability with Objective-C.

### The Development Environment

Source: https://academia.sh/en/courses/ios-platform-and-tools/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.

### Project File Structure

Source: https://academia.sh/en/courses/ios-platform-and-tools/project-file-structure

Measuring the meeting of the property list keys a template writes with the keys the build generates, same-named resource files, device-specific assets and localizations.

The previous lesson brought the build settings of the project and the target together: when the same key was written in two places the target won, and in list values the inheritance marker chose union. An app's file structure carries more than these two layers. A project is born from a template, and the template writes a **property list (Info.plist)**. The build adds its own keys to this list. Resource files and images are added to targets, and a separate folder is opened for each language. Every arrangement in the file system creates a new place for a meeting.

The measurement axis stays the same: **when these two definitions meet, how is it resolved?** The resolution is one of four forms: precedence, intersection, union or rejection. In this lesson we measure four places in the file structure and leave the question of which resolution each falls into to the classifier. The classifier is the same as in the previous lesson; its rule has not changed. Two definitions where one is a subset of the other, or where one is empty, do not count as a meeting.

#### The Classifier

The class below takes two definitions as sets, takes the result of the resolution as a third set, and names the resolution. The result is never written by hand; in every measurement it comes from a model.

```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)")
    }
}
```

#### Template and Generated Keys

A new project is born from a template, and the template writes a set of keys into the app's property list: the display name, the launch screen, the supported screen orientations. The file is an XML document made of key and value pairs. The build, for its part, adds its own keys to the list: the name of the executable, the target platform, the build version. As a source, the contributions of these two writers do not appear in a single file; the part the template writes stays in the project directory, while the part the build generates appears later inside the bundle.

The fragment below is an abridged form of a property list the template writes. The file is named `Info.plist`, and the first three keys come from the template.

```xml
<dict>
    <key>CFBundleDisplayName</key>
    <string>Demo</string>
    <key>UILaunchScreen</key>
    <dict/>
    <key>UISupportedInterfaceOrientations</key>
    <array>
        <string>UIInterfaceOrientationPortrait</string>
    </array>
</dict>
```

Because the file produces no measurement result, it stands only as source. The model rebuilds this file's keys with plain names: the template writes the `display-name`, `launch-screen` and `orientation` keys; the build generates the `executable`, `platform` and `version` keys. The two sets share no key. The question is which keys end up inside the bundle.

```swift
// main.swift — the keys the template writes and the keys the build generates merge
func mergeProperties(template: [String: String], generated: [String: String]) -> [String: String] {
    template.merging(generated) { _, generatedValue in generatedValue }
}

let templateEntries = ["display-name": "Demo", "launch-screen": "main", "orientation": "portrait"]
let generatedEntries = ["executable": "demo", "platform": "ios", "version": "1.0.42"]
let combinedEntries = mergeProperties(template: templateEntries, generated: generatedEntries)

let propertyMeeting = Meeting(
    "properties",
    first: Set(templateEntries.keys),
    second: Set(generatedEntries.keys),
    result: Set(combinedEntries.keys)
)
propertyMeeting.report()
```

```
properties: first display-name,launch-screen,orientation | second executable,platform,version | result display-name,executable,launch-screen,orientation,platform,version -> union
```

The result is the union of the two sets, and the resolution came out as **union**. The template and the build do not replace each other; the two sit side by side in the same file. This differs from the precedence in settings: in the previous lesson the target overrode the project's value, whereas here the two writers write to different fields, so nobody overrides anybody.

**DE5.** The measurement covers the case where the two writers write with disjoint key sets. The non-disjoint case, where the same key is found in both writers, is handled separately in this lesson's bounding measurement.

#### Same-Named Resource Files

Resource files are tied to targets by membership. The same file can be a member of two targets; that is sharing and causes no problem, because the file is a single file. Because each target is copied into its own bundle, two separate files carrying the same name in two separate targets do not collide either. The problem arises when two separate files with the same name are added to the same target; for example, two `strings.txt` files sitting in two separate folders of the project. The target's resources copied into the bundle sit in a flat directory; if two files carry the same name, both ask for the same output path and the second cannot be written over the first. The build reports this situation in the copy step as an error saying that multiple commands produce the same output.

The model builds this behavior as follows: if the names of two resource groups added to the same target are disjoint, the result is the union of the two sets; if they intersect, there is no result. In the classifier, the absence of a result means rejection. Below are three resource groups added to the same target. The main group and the onboarding group share the name `strings.txt`; the settings group shares no name with either of the other two.

```swift
// main.swift — two resource files added to the same target under the same name collide on copy
func copyResources(_ first: Set<String>, _ second: Set<String>) -> Set<String>? {
    first.isDisjoint(with: second) ? first.union(second) : nil
}

let mainGroup: Set<String> = ["logo.png", "strings.txt"]
let onboardingGroup: Set<String> = ["badge.png", "strings.txt"]
let settingsGroup: Set<String> = ["icon.png", "legal.txt"]

let clash = Meeting("same-name", first: mainGroup, second: onboardingGroup, result: copyResources(mainGroup, onboardingGroup))
let distinct = Meeting("distinct-names", first: mainGroup, second: settingsGroup, result: copyResources(mainGroup, settingsGroup))
clash.report()
distinct.report()
```

```
same-name: first logo.png,strings.txt | second badge.png,strings.txt | result none -> rejection
distinct-names: first logo.png,strings.txt | second icon.png,legal.txt | result icon.png,legal.txt,logo.png,strings.txt -> union
```

When there was a shared name, `none` was printed for the result and the resolution came out as **rejection**. When there was no shared name, the sets merged. The difference between rejection and union depends on a single condition: the names being disjoint. How similar the definitions are, or whether the contents of the two files are identical, does not change the result. Two files with the same content are still rejected, because copying looks at the name, not the content.

The remedy is clear: either one of the files is renamed, or it is removed from the target. If the contents of the two files are identical, they are merged into a single file; if another target also needs that file, the file is shared through membership. This is the natural consequence of keeping the file in a single place, and it removes the collision at its source.

**DE6.** The measurement treated the name collision at file level. The case where same-named files sit in different subdirectories and the bundle preserves those directories was not measured.

#### Device-Specific Assets

An **asset catalog** keeps images and colors in named sets. An image set can gather several forms under one name: a general form and a special form for a particular device class. At run time the platform picks the form that fits the device; if a special form exists, it is chosen in place of the general one. A special form written for the phone replaces the general form on the phone.

The model builds this choice as a lookup: it returns the special form if there is one, otherwise the general one. We write two meetings: a special form is defined for the phone, and none is defined for the tablet.

```swift
// main.swift — a device-specific asset takes precedence over the general asset
func assetMeeting(_ idiom: String, variants: [String: String]) -> Meeting {
    let general: Set<String> = ["icon-universal"]
    let specific: Set<String> = variants[idiom].map { [$0] } ?? []
    let picked = variants[idiom] ?? "icon-universal"
    return Meeting("icon-" + idiom, first: general, second: specific, result: [picked])
}

let iconVariants = ["phone": "icon-phone"]
let assetMeetings = ["phone", "tablet"].map { assetMeeting($0, variants: iconVariants) }
for meeting in assetMeetings { meeting.report() }
```

```
icon-phone: first icon-universal | second icon-phone | result icon-phone -> precedence
icon-tablet: first icon-universal | second empty | result icon-universal -> out-of-scope
```

When a special form was defined for the phone, the two definitions met and the special one won: the resolution is **precedence**. There is no special form for the tablet; because the second definition stayed empty, the classifier did not count this as a meeting. The tablet still shows an icon, because the general form comes into play. The absence of a definition produces no resolution; it only lets the other definition be in effect on its own.

This arrangement leads to the conclusion that a general form must always exist. A set that has only special forms cannot find an image on a device class for which none is defined. The general form is a safety net, and the special forms are exceptions written on top of it.

**DE7.** The measurement is for two device classes and a single special form. The case where more than one special form exists in the same device class, for example forms separated by scale factor, was not measured.

#### Localization

**Localization** keeps an app's texts in separate folders per language. The app declares which languages it offers through the names of these folders. The user, in the settings, states the languages they prefer in an order. The platform brings these two definitions together and chooses one of the languages that both exist in the app and are preferred by the user. A language that is missing from either cannot be chosen.

The model builds this meeting as the intersection of the sets. It sets the ordering aside and looks only at which languages are shared.

```swift
// main.swift — the languages the user prefers intersect with the app's localizations
func matchingLanguages(preferred: Set<String>, localized: Set<String>) -> Set<String> {
    preferred.intersection(localized)
}

let preferredLanguages: Set<String> = ["de", "fr", "tr"]
let localizedLanguages: Set<String> = ["en", "tr", "de"]

let languageMeeting = Meeting(
    "languages",
    first: preferredLanguages,
    second: localizedLanguages,
    result: matchingLanguages(preferred: preferredLanguages, localized: localizedLanguages)
)
languageMeeting.report()
```

```
languages: first de,fr,tr | second de,en,tr | result de,tr -> intersection
```

The user prefers French but the app has no French folder; the app offers English but the user has not written it in the preference list. What is in effect is only the languages the two share, and the resolution came out as **intersection**. If the intersection is empty, the app's development language comes into play; for the classifier this is no different from a rejection, because in the end the two definitions have no value in common.

**DE8.** The measurement counts only which languages are shared. The user's order of preference and region-dependent language variants (two regional forms of the same language) were not measured.

#### The Bounding Measurement

In the first measurement the key sets of the template and the build were disjoint. In real projects they may not be: the template writes a value for the version key, and the build writes its own value for the same key. What does union do in that case? The classifier's rule counts keys. The same data can be measured with two eyes: one looks at the keys, one at the values.

```swift
// main.swift — union counts keys, it does not count the values of the same key
let templateValues = ["display-name": "Demo", "version": "1.0"]
let generatedValues = ["executable": "demo", "version": "1.0.42"]
let combined = templateValues.merging(generatedValues) { _, generatedValue in generatedValue }

let keyView = Meeting(
    "key-view",
    first: Set(templateValues.keys),
    second: Set(generatedValues.keys),
    result: Set(combined.keys)
)
let valueView = Meeting(
    "value-view",
    first: Set(templateValues.values),
    second: Set(generatedValues.values),
    result: Set(combined.values)
)
keyView.report()
valueView.report()
print("version:", combined["version"] ?? "none")
```

```
key-view: first display-name,version | second executable,version | result display-name,executable,version -> union
value-view: first 1.0,Demo | second 1.0.42,demo | result 1.0.42,Demo,demo -> off-rule
version: 1.0.42
```

Looking at the keys, the result is the union of the two sets: the resolution is union. Looking at the values, the same data fits no resolution and the classifier writes `off-rule`. The value `1.0` the template wrote is not in the result; for the colliding key the build's value remained and the template's value was lost. Union guarantees only the union of the keys; which value remains for a colliding key is a separate precedence rule.

The upshot: when you see union in a file, you need to know that it is correct for the keys, and that for the values of colliding keys a precedence is sought separately. The previous lesson measured this precedence at the two levels of the same key; here the same rule appears again inside the property list.

#### Summary

- When the keys the template writes and the keys the build generates are disjoint, the two sets merge; neither overrides the other.
- Two resource files added to the same target under the same name collide on copy and the result is rejected; the same name in separate targets does not collide. Rejection depends only on whether the names are disjoint.
- A device-specific asset is superior to the general asset; if no special form exists, the general one is in effect on its own and no meeting arises.
- The languages the user prefers intersect with the app's localizations.
- This lesson's resolution: **union**. The keys the template writes and the keys the build generates merge in the same property list, but union counts keys, not the value of a colliding key.

#### Next Step

This lesson measured the meeting of local files and keys. Not everything that enters an app is local: packages come from outside, and each package asks for a version range for its own dependencies. Two packages asking for different version ranges of the same dependency is a counterpart of the disjoint-and-shared-set question in this lesson. The next lesson measures the meeting of the version ranges of two packages.

### The Package Manager

Source: https://academia.sh/en/courses/ios-platform-and-tools/package-manager

Measuring the intersection of the version ranges two packages ask for the same dependency, a local package replacing a remote one, and the union of package products.

The previous lesson measured the meeting of local files and keys. An app is not made only of its own files; it takes code that others have written as a **package**. The platform's **package manager** reads the **dependency** list of each package, downloads these dependencies and decides which version of each is to be used. The decision gets difficult at one point: two packages ask for the same third package, and each accepts different versions of it.

The measurement axis stays the same: when two definitions meet, how is it resolved? In this lesson the meeting definitions are the version constraints of packages. A version constraint describes a set of accepted versions; when two constraints meet, the classifier says which resolution the result falls into.

#### The Classifier

```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)")
    }
}
```

This lesson needs one more small helper to build version sets. A constraint consists of a lower bound and an upper bound; the lower bound is inclusive and the upper bound is exclusive. The helper splits a version text into numbers, compares two versions in order and filters the versions a constraint accepts out of the available versions. The file contains only definitions and has no output.

```swift
// Requirement.swift — counts the versions a version constraint accepts
func versionKey(_ text: String) -> [Int] {
    text.split(separator: ".").compactMap { Int($0) }
}

func isBefore(_ left: String, _ right: String) -> Bool {
    versionKey(left).lexicographicallyPrecedes(versionKey(right))
}

struct Requirement {
    let from: String
    let upTo: String

    func accepts(_ version: String) -> Bool {
        !isBefore(version, from) && isBefore(version, upTo)
    }
}

func accepted(_ requirement: Requirement, among available: [String]) -> Set<String> {
    Set(available.filter { requirement.accepts($0) })
}

func rangeMeeting(_ label: String, _ first: Requirement, _ second: Requirement, available: [String]) -> Meeting {
    let left = accepted(first, among: available)
    let right = accepted(second, among: available)
    return Meeting(label, first: left, second: right, result: left.intersection(right))
}
```

#### Version Ranges

Packages are numbered with **semantic versioning**: three numbers separated by dots. The first number increases on an incompatible change, the second on a compatible new feature, the third on a fix. When a package wants another package, it usually writes a **version range**: "greater than or equal to one, less than two". The upper bound stops before the version in which the incompatible change arrives.

The package definition is written in the `Package.swift` file. Because the file uses the platform's package description framework, it stands only as source. The dependency line carries an address and a range.

```swift
import PackageDescription

let package = Package(
    name: "Formatter",
    dependencies: [
        .package(url: "https://example.com/parser.git", "1.0.0"..<"2.0.0")
    ],
    targets: [
        .target(name: "Formatter", dependencies: [.product(name: "Parser", package: "parser")])
    ]
)
```

This file produces no measurement result. A second package asks for the same dependency with the range `"1.2.0"..<"3.0.0"`. To measure the two, we take the versions the package has published as a fixed list: six versions between `1.0.0` and `2.1.0`. Each range is filtered from this list, and the meeting of the two sets is given to the classifier. First two overlapping ranges, then two disjoint ranges.

```swift
// main.swift — the version ranges two packages ask for the same dependency
let published = ["1.0.0", "1.1.0", "1.2.0", "1.3.0", "2.0.0", "2.1.0"]

let overlapping = rangeMeeting(
    "overlapping",
    Requirement(from: "1.0.0", upTo: "2.0.0"),
    Requirement(from: "1.2.0", upTo: "3.0.0"),
    available: published
)
let disjoint = rangeMeeting(
    "disjoint",
    Requirement(from: "1.0.0", upTo: "1.2.0"),
    Requirement(from: "2.0.0", upTo: "3.0.0"),
    available: published
)
overlapping.report()
disjoint.report()
```

```
overlapping: first 1.0.0,1.1.0,1.2.0,1.3.0 | second 1.2.0,1.3.0,2.0.0,2.1.0 | result 1.2.0,1.3.0 -> intersection
disjoint: first 1.0.0,1.1.0 | second 2.0.0,2.1.0 | result empty -> rejection
```

**DE9.** The measurement was made for a range form with an inclusive lower bound and an exclusive upper bound. A constraint pinned to a single version and a constraint carrying only a lower bound were not measured. The version list is limited to the six published versions.

In the first meeting the versions common to the two ranges are `1.2.0` and `1.3.0`: the resolution is **intersection**. In the second meeting there is no common version: the result is empty and the resolution is **rejection**. In this case the platform's package manager stops dependency resolution and reports which two constraints conflict.

Rejection is not an error but a piece of information. There are two conflicting constraints in the dependency list, and one of the two must change. The package manager cannot choose which one to loosen; that decision belongs to the package author.

#### The Local Package

During development it is possible to use a dependency not with its published version but with a directory on disk. Such a **local package** replaces the remote package of the same name. The platform chooses the local one; it never downloads the remote version. To change the source of a dependency and see the result immediately, there is no need to publish a new version.

The model builds this with a selection function: it returns the local source if there is one, otherwise the remote source. We measure for two packages: one also exists locally, the other only remotely.

```swift
// main.swift — a local package replaces the remote package of the same name
func chooseSource(remote: String, local: String?) -> String {
    local ?? remote
}

func sourceMeeting(_ label: String, local: String?) -> Meeting {
    let chosen = chooseSource(remote: "remote", local: local)
    return Meeting(label, first: ["remote"], second: local.map { [$0] } ?? [], result: [chosen])
}

let sourceMeetings = [
    sourceMeeting("parser-package", local: "local"),
    sourceMeeting("lexer-package", local: nil)
]
for meeting in sourceMeetings { meeting.report() }
```

```
parser-package: first remote | second local | result local -> precedence
lexer-package: first remote | second empty | result remote -> out-of-scope
```

When a local source was defined, the two sources met and the local one won: **precedence**. When none was defined, the second definition was empty and there was no meeting. When the same name exists in two sources, the constraints of the remote version are also disabled; the local package is accepted whatever version it is at. For this reason a forgotten local package can lead to constraints being ignored unexpectedly.

**DE10.** The measurement compared two kinds of source, local and remote. The case where two different remote addresses carry the same package name was not measured.

#### Package Products

A package declares not only its code but also linkable outputs called **products**: a library, an executable. The app target chooses the products to link by name. The products of two packages form a single set of usable products for the app. If both packages offer a product name, the set does not carry that name twice.

The model takes the union of the product sets of the two packages. The first package offers the `parser` and `tokens` products, the second package the `formatter` and `tokens` products.

```swift
// main.swift — the products of two packages merge into a single set
func availableProducts(_ first: Set<String>, _ second: Set<String>) -> Set<String> {
    first.union(second)
}

let parserProducts: Set<String> = ["parser", "tokens"]
let formatterProducts: Set<String> = ["formatter", "tokens"]

let productMeeting = Meeting(
    "products",
    first: parserProducts,
    second: formatterProducts,
    result: availableProducts(parserProducts, formatterProducts)
)
productMeeting.report()
```

```
products: first parser,tokens | second formatter,tokens | result formatter,parser,tokens -> union
```

Three products came out and the resolution is **union**. The shared `tokens` name caused no collision, because in this measurement a product name is an identity and the set does not count the same identity twice. In a real project, two separate packages having two different products with the same name is a different problem; the model does not distinguish this case.

**DE11.** The measurement treated the product name as an identity. The collision of two different products carrying the same name and a product being offered only on a particular platform were not measured.

#### The Bounding Measurement

In the first measurement the intersection gave two versions: `1.2.0` and `1.3.0`. The app, however, is built with a single version. The resolver picks one version from the intersection, and usually picks the highest; the chosen version is written to a lock file and reused in later resolutions. For the same meeting, the result is given once as the accepted set and once as the chosen version.

```swift
// main.swift — the resolver picks the highest version from the intersection
func highest(_ versions: Set<String>) -> String {
    versions.max { isBefore($0, $1) } ?? "none"
}

let catalog = ["1.0.0", "1.1.0", "1.2.0", "1.3.0", "2.0.0", "2.1.0"]
let firstRange = Requirement(from: "1.0.0", upTo: "2.0.0")
let secondRange = Requirement(from: "1.2.0", upTo: "3.0.0")
let leftSet = accepted(firstRange, among: catalog)
let rightSet = accepted(secondRange, among: catalog)
let commonSet = leftSet.intersection(rightSet)

let acceptedSet = Meeting("accepted-set", first: leftSet, second: rightSet, result: commonSet)
let chosenVersion = Meeting("chosen-version", first: leftSet, second: rightSet, result: [highest(commonSet)])
acceptedSet.report()
chosenVersion.report()
```

```
accepted-set: first 1.0.0,1.1.0,1.2.0,1.3.0 | second 1.2.0,1.3.0,2.0.0,2.1.0 | result 1.2.0,1.3.0 -> intersection
chosen-version: first 1.0.0,1.1.0,1.2.0,1.3.0 | second 1.2.0,1.3.0,2.0.0,2.1.0 | result 1.3.0 -> off-rule
```

When the accepted set was given, the resolution was intersection; when the single version the resolver chose was given, the classifier wrote `off-rule`. A single version is neither the union of the two sets, nor their intersection, nor either of them itself. The rule describes only the accepted set; which element to choose from that set is a separate decision.

The practical consequence: even when the constraints look resolvable, it is not the constraints but the resolver's selection rule and the lock file that determine which version the app will be built with. A new resolution made without a lock file can pick a higher version; the constraints do not forbid this, because the chosen version is still inside the intersection.

**DE12.** The model always took the selection as the highest version. The case where the resolver keeps a version in the lock file was not measured.

#### Summary

- The version ranges two packages ask for the same dependency are resolved by the intersection of the accepted version sets.
- Disjoint ranges with no common version are rejected and resolution stops.
- A local package replaces the remote package of the same name; if there is no local source, no meeting arises.
- The products of two packages merge in the set of products the app can use.
- This lesson's resolution: **intersection**. The rule counts the accepted set, not the single version the resolver chooses.

#### Next Step

In this lesson the constraints of packages met and a common set of versions came out. After the app is built it has to run somewhere: during development this place is mostly the simulator, but the simulator does not accept everything a physical device accepts. The next lesson measures the meeting of the capabilities the app requires and the capabilities the simulator offers.

### Simulator and Device

Source: https://academia.sh/en/courses/ios-platform-and-tools/simulator-and-device

Measuring a capability the app requires being absent from the simulator, the capabilities the simulator and the device share, and a simulated location replacing the real one.

The previous lesson brought the version constraints of packages together. Where the built app will run is a separate question. During development the app mostly runs in a **simulator**: an environment that runs on the computer and imitates a device. A real **device** carries its own hardware. Both run the same code, but they do not offer the same capabilities.

The measurement axis is the same: when two definitions meet, how is it resolved? In this lesson one definition is the capabilities the app requires, and the other is the capabilities an environment offers. A capability here carries a name: camera, location, network, motion sensor.

#### The Classifier

```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)")
    }
}
```

#### Differences and Limitations

The difference between the simulator and the device is a difference of the environment the code runs in, not of the code. The same built app runs in two places, but the machine underneath is not the same. The simulator runs on the development computer as a process: it uses the computer's processor, memory and network connection. The device is a separate machine with its own processor, limited memory, battery and thermal limit. For this reason a screen that looks smooth in the simulator can stutter on the device, and an operation that ends without trouble in the simulator can be terminated under memory pressure on the device.

It helps to divide the differences into three classes.

- **Hardware difference:** The simulator is an imitation of hardware. Hardware-bound capabilities such as the camera, the motion and orientation sensors, the vibration motor, biometric verification and wireless connection units are either not offered at all or are imitated with values the developer supplies by hand.
- **Environment difference:** The network, the file system and permission flows look the same in the two environments but do not run under the same conditions. The simulator uses the computer's fast and stable connection; the device may be on a weak, intermittent or metered connection.
- **Lifecycle difference:** On the device the user puts the app in the background, locks the device, switches to another app, receives a call. In the simulator these events can be triggered, but they do not occur on their own, so the developer often forgets to try them.

A limitation can be the absence of a capability, but it can also be a misleading presence. In the simulator location is always ready, the permission flow is short and the accuracy is perfect. On the device, location must first find the satellites, its accuracy drops indoors and the user may deny permission. A screen that passes without trouble in the simulator may never have seen these three situations.

The sentences in this section are not things the model measures; they are a rough map of the difference between environments. The measurement is made below in this lesson, with sets of capability names.

#### When a Required Capability Is Absent from the Simulator

If an app uses the camera, it declares this in the project settings and asks the user for permission on first use. Because the simulator runs on the computer, it does not present the computer's camera as the device's camera; a camera call either fails in the simulator or returns empty. For location the situation is different: the simulator can give the app a location chosen by the developer as if it were the device's location.

The code that asks for location permission depends on the platform's location framework, so it stands only as source.

```swift
// location permission — SOURCE
import CoreLocation

let manager = CLLocationManager()
manager.requestWhenInUseAuthorization()
```

The first measurement is the meeting of the capabilities an app requires with the capabilities the simulator offers. The app wants the camera and the motion sensor; the simulator offers location, network and keychain. The two sets are disjoint.

```swift
// main.swift — a capability the app requires is absent from the simulator
let required: Set<String> = ["camera", "motion"]
let simulator: Set<String> = ["location", "network", "keychain"]

let missing = Meeting(
    "required-vs-simulator",
    first: required,
    second: simulator,
    result: required.intersection(simulator)
)
missing.report()
```

```
required-vs-simulator: first camera,motion | second keychain,location,network | result empty -> rejection
```

There is no common capability and the resolution is **rejection**: the app cannot find the capabilities it requires in this simulator. In practice this corresponds to a flow that cannot be tested in the simulator once the camera screen is reached. The remedy is not to change the code but to test that flow on a real device.

**DE13.** The measurement was made for two disjoint capability sets. The mixed case, where only some of the required capabilities exist in the simulator, was not measured separately in this table.

#### Capabilities the Simulator and the Device Share

If the same app is to run both in the simulator and on the device, it is safe to rely on the capabilities the two share. The simulator offers location, network and keychain; the device offers camera, location, network and motion sensor. The meeting of the two gives the capabilities that behave the same in both environments.

```swift
// main.swift — the capabilities the simulator and the device share
let simulatorSet: Set<String> = ["location", "network", "keychain"]
let deviceSet: Set<String> = ["camera", "location", "network", "motion"]

let shared = Meeting(
    "simulator-vs-device",
    first: simulatorSet,
    second: deviceSet,
    result: simulatorSet.intersection(deviceSet)
)
shared.report()
```

```
simulator-vs-device: first keychain,location,network | second camera,location,motion,network | result location,network -> intersection
```

The resolution is **intersection**: location and network exist in both environments. The keychain appeared only in the simulator set, because in this model it was not written into the device set. This is not a claim about reality but the input of the measurement: we count what is written into the sets, not the platform's real hardware.

**DE14.** The capability sets are the input of the model; they are not claimed to reflect the platform's real capability list. The set of a third environment, for example a different piece of hardware, was not measured.

#### The Simulated Location

In the simulator, location does not come from a real satellite reading; the developer chooses a location or plays back a route. When the app has two pieces of location information, the simulated location replaces the real location. This replacement is a **precedence** relation: one wins, the other becomes invisible.

```swift
// main.swift — the simulated location replaces the real location
let realFix = ["real-fix"]
let simulatedFix = ["simulated-fix"]

let fixMeeting = Meeting(
    "location-source",
    first: Set(realFix),
    second: Set(simulatedFix),
    result: Set(simulatedFix)
)
fixMeeting.report()
```

```
location-source: first real-fix | second simulated-fix | result simulated-fix -> precedence
```

The resolution is **precedence**: the result is the same as the second definition. When the app reads location in the simulator, it sees the chosen location, not a location in the field. For this reason location-dependent faults can stay hidden if they are tested only in the simulator: since no real location arrives, situations such as accuracy, latency and permission changes never occur.

**DE15.** The measurement covers a single pair of location sources. How the real location arrives when there is no simulated location, and how far apart the two sources can drift at the same time, were not measured.

These differences produce a list of things that must be tried on the device. Hardware-bound flows such as the camera and microphone, network behavior on an intermittent or slow connection, memory use and heating during long runs, the state of an app that is put in the background and returned to, and the smoothness of touch and scrolling felt with a real finger head this list. None of these can be tested safely in the simulator, because the simulator either does not offer the capability or runs it flawlessly with the computer's power. The practical rule is plain: the simulator is for fast feedback, and the device is the last exam before release.

#### Installing on a Device

To install an app in the simulator one need not prove anything to anyone; to install on a device one does. The platform verifies by signature who produced every app that will run on a device and what it is permitted to do. The preparation consists of four parts.

- **Developer identity (certificate):** The digital identity of the person or team that signs the app. It consists of a certificate and a private key tied to it; without the private key a signature cannot be made, so the key may need to be moved to another computer.
- **App identifier:** The package name that identifies the app uniquely in the world, often written in reverse domain name form. Two apps cannot carry the same identifier.
- **Registered device:** An app signed for development opens only on devices registered in advance. The device's unique identifier is added to the team's device list.
- **Entitlements:** The right of the app to use special capabilities such as notifications, keychain sharing or background execution. Even if a capability is used in code, it does not work on the device if the entitlement is not in the profile.

A **provisioning profile** ties these four parts together in a single file: which app identifier, when signed with which certificate, can run on which devices, with which entitlements. The target's settings point to this profile. The signing settings are also defined at project and target level; the value in the target takes precedence over the one in the project.

Two more steps may be needed on the device. The first is making the device ready for development: in the relevant versions of the platform the device asks for a developer mode to be turned on. The second is the device owner trusting, in the settings, the developer of a development-signed app installed on the device for the first time. Both are done once, but when skipped the app installs and still does not open; this is one of the situations that surprises beginners most.

The result of this preparation is also a meeting, but this lesson's model does not measure it: whether the certificate, the profile and the device list fit one another belongs to the platform's signature check. The only thing measured in this lesson is the capability names of the app and the environment once the preparation is complete.

#### The Bounding Measurement

The model counts whether a capability exists; it cannot count how well that capability works. Code that runs in the simulator may not run on the device: memory, processor and sensor noise differ in the two environments. To see this, let the simulator and the device offer the same set of capabilities; let one be fast and the other slow. The model compares the sets and sees that the two sets are the same.

```swift
// main.swift — the same capability sets: a performance difference is invisible to the model
let capabilities: Set<String> = ["location", "network"]

let sameCapabilities = Meeting(
    "same-capabilities",
    first: capabilities,
    second: capabilities,
    result: capabilities
)
sameCapabilities.report()
```

```
same-capabilities: first location,network | second location,network | result location,network -> out-of-scope
```

When the two definitions are the same, the classifier sees no meeting: the result is **out-of-scope**. This does not mean the two environments behave the same; it says only that the capability names are the same. Differences of performance, duration and accuracy are outside this model.

The practical rule is this: a test that passes in the simulator shows that the capability exists; it does not show that the capability runs fast and accurately enough on real hardware. Flows with time and memory concerns should be tested on at least one real device before release. In the same way, flows that depend on sensor data can be tested in the simulator only with chosen values; the noise or interruption of a real sensor does not occur there.

**DE16.** The measurement counted the existence of a capability. The same capability running at a different speed or with a different accuracy in the two environments, and the memory and processor difference, were not measured.

#### Summary

- If a capability the app requires is absent from the simulator, the meeting is rejected; the flow is tested on a real device.
- The capabilities the simulator and the device share are the intersection of the sets.
- The simulated location replaces the real location; the resolution is precedence.
- The model counts the existence of a capability; a performance difference stays outside the measurement.
- This lesson's resolution: **rejection**. If a required capability is not in the environment, the app cannot find it in that environment.

#### Next Step

This lesson measured where the app will run and what will be missing. When something goes wrong while it runs, the problem has to be found. The next lesson covers the basics of debugging: the union of the breakpoints of two sources, the intersection of filters, and the precedence of the scheme setting over the code's default.

### Debugging Basics

Source: https://academia.sh/en/courses/ios-platform-and-tools/debugging-basics

Measuring the union of breakpoints, the intersection of log filters, a scheme environment variable taking precedence, and the rejection of a breakpoint placed on a line with no code.

When something unexpected happens while the app runs in the simulator or on a device, a **debugger** is used to find the problem. The most basic tool is the **breakpoint**: it stops execution at a particular line of code and lets you inspect the values at that moment. After stopping, the code is advanced line by line, which is called **stepping**, and the output is read from the messages written to the **log**.

The measurement axis stays the same: when two definitions meet, how is it resolved? In this lesson the meeting definitions are sets of breakpoints, log filters and debugging settings.

#### The Classifier

```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)")
    }
}
```

#### Breakpoints

A breakpoint is placed on a line of a file. In practice breakpoints can come from two places: those placed by clicking in the margin of the development environment, and those added from the debugger's command line with `breakpoint set`. When the two come together in the same project, the set of active breakpoints is the union of the two sources. A point placed on the same line from both sources is counted once.

The model shows each breakpoint with a `file:line` identity. The first source places points on lines twelve and thirty of the cart file; the second source places points on line thirty of the cart file and line eight of the order file.

```swift
// main.swift — the breakpoints of two sources merge
let gutter: Set<String> = ["Cart.swift:12", "Cart.swift:30"]
let console: Set<String> = ["Cart.swift:30", "Order.swift:8"]

let breakpoints = Meeting(
    "breakpoint-sources",
    first: gutter,
    second: console,
    result: gutter.union(console)
)
breakpoints.report()
```

```
breakpoint-sources: first Cart.swift:12,Cart.swift:30 | second Cart.swift:30,Order.swift:8 | result Cart.swift:12,Cart.swift:30,Order.swift:8 -> union
```

The resolution is **union**: three separate points are active. The shared `Cart.swift:30` was counted once. For this reason the number of stops is not the sum of the source counts: a point defined in both sources stops once.

**DE17.** The measurement treated breakpoints as file and line identities. Conditional breakpoints, debugger-specific actions and disabling a point were not measured.

#### Breakpoint Kinds

A plain breakpoint stops every time it is reached. In a loop or in a frequently called function this leads to hundreds of needless stops. Two kinds solve this.

- **Conditional breakpoint:** It stops only when the given expression is true. On the command line the condition is given with the `-c` flag: `breakpoint set -f Cart.swift -l 30 -c 'count > 3'`. The condition is evaluated every time the point is reached; so on a line that is passed very often, the condition itself can cause slowness.
- **Action breakpoint:** It runs an action before stopping or instead of stopping. The action can print the value of an expression, run a command or play a sound. If an automatic continue option is added to the point, the app does not stop; the point behaves like a temporary log line without touching the code. On the command line `breakpoint command add` does this job.

There are two more kinds of breakpoint. An **exception breakpoint** stops at the moment the app throws an exception, before the crash; it lets you see not only the result but the cause in the crash trace. A **symbolic breakpoint** is placed on the name of a function instead of a line, so that every call of a system function whose source you do not have can be stopped at. Because neither carries a file and line identity, both are outside the measurement below.

It is also possible to **disable** a point without deleting it. A disabled point keeps its definition but does not stop; when the problem is solved it is deleted, and if it is needed again it is turned back on.

None of these kinds is the subject of the measurement above. The measurement looks only at the location of the point; whether the condition turns out true belongs to run time and cannot be expressed with a set.

#### Log Filters

While running, an app writes a great many messages. To keep them readable, two filters are used together: the **log level** filter drops messages below a certain severity, and the **category** filter keeps the messages of a certain subsystem. A message is visible only if it passes both filters. The code that writes to the log depends on the platform's logging framework, so it stands only as source.

```swift
// log — SOURCE
import os

let logger = Logger(subsystem: "com.example.shop", category: "cart")
logger.info("cart updated")
```

The model shows each message with an identity. The level filter keeps warnings and above; the category filter keeps the messages of the cart subsystem.

```swift
// main.swift — the intersection of the level and category filters
let levelPass: Set<String> = ["m2", "m3", "m5", "m6"]
let categoryPass: Set<String> = ["m3", "m4", "m5"]

let filters = Meeting(
    "log-filters",
    first: levelPass,
    second: categoryPass,
    result: levelPass.intersection(categoryPass)
)
filters.report()
```

```
log-filters: first m2,m3,m5,m6 | second m3,m4,m5 | result m3,m5 -> intersection
```

The resolution is **intersection**: only the two messages that pass both filters are visible. If one of the filters is loosened, the visible set grows; if one is tightened, it shrinks. A message not being visible does not mean it was not written; it says only that it could not pass one of the filters.

**DE18.** The measurement took the two filters as separate sets. Cases where the filters depend on each other and a message belonging to more than one category were not measured.

Filters alone are not enough; the level at which a message is written also matters. The platform's logging framework separates messages in order of severity.

- **Debug level (debug):** Details useful only during development. They are usually not kept in the release build.
- **Info level (info):** Events that are useful to know in the ordinary flow.
- **Notice level (notice):** The default level; situations that are not a problem but worth recording.
- **Error level (error):** Situations in which an operation failed.
- **Fault level (fault):** Situations that break the consistency of the app or the system and call for immediate action.

A category is the name of a subsystem: network, database, interface. The app defines its own subsystems and categories. A good naming makes it possible to look only at network messages when a problem arises. Messages containing personal data are hidden by default; unless the developer states otherwise, their values do not appear in the log.

#### The Environment Variable in the Scheme

Run settings are kept in the scheme. The scheme can supply **environment variables** when the app starts. If the code carries a default value for a setting and the scheme gives a value for the same setting, at run time the value in the scheme takes the place of the code's. This replacement is a **precedence**: this is why verbose logging can be turned on and off without changing the code.

```swift
// main.swift — a scheme environment variable takes precedence over the code's default
func effectiveLogging(codeDefault: String, schemeValue: String?) -> String {
    schemeValue ?? codeDefault
}

let chosen = effectiveLogging(codeDefault: "verbose-off", schemeValue: "verbose-on")
let environment = Meeting(
    "scheme-environment",
    first: ["verbose-off"],
    second: ["verbose-on"],
    result: [chosen]
)
environment.report()
```

```
scheme-environment: first verbose-off | second verbose-on | result verbose-on -> precedence
```

The resolution is **precedence**: the result is the same as the second definition. Had there been no value in the scheme, the code's default would have applied. Because a scheme can be developer-specific, an environment variable in a scheme that is not shared does not appear on another developer's machine; differences of the "it works on mine" kind sometimes come from here.

**DE19.** The measurement covered a single pair of settings. Cases where several environment variables affect one another, and the app re-reading a setting within itself, were not measured.

#### A Line With No Code

A breakpoint cannot be placed on every line. An empty line, a comment line or a lone curly brace produces no executable code; the debugger moves the point to the nearest line of code or does not make it active at all. The model measures this behavior not as a fixed rule but as a meeting: the requested line meets the set of lines that contain code.

```swift
// main.swift — a breakpoint placed on a line with no code
let codeLines: Set<String> = ["10", "11", "13"]
let requested: Set<String> = ["12"]

let emptyLine = Meeting(
    "breakpoint-on-empty-line",
    first: requested,
    second: codeLines,
    result: requested.intersection(codeLines)
)
emptyLine.report()
```

```
breakpoint-on-empty-line: first 12 | second 10,11,13 | result empty -> rejection
```

There is no common line and the resolution is **rejection**: there is no code to stop at on line twelve. The debugger moving the point to a neighboring line is outside this model; the model counts whether the point stays on the requested line.

#### Stepping Commands and Inspecting Variables

When the app stops at a point, three basic commands manage progress. Their counterparts on the debugger command line are given in code font.

- **Step into:** the `step` command. It enters the body of the call on the current line and stops at its first line.
- **Step over:** the `next` command. It runs the call in one step and stops at the next line. If there is a breakpoint inside the call, it still stops there.
- **Step out:** the `finish` command. It runs until the current function returns and stops at the caller's line.

If you are interested in what is inside a call you step into it; if you are not, you step over it; if you went too deep by mistake, you step out. Stepping into code you do not know, such as the standard library, often wastes time.

After stopping, values are inspected. `frame variable` lists the local variables in the current frame. `print` evaluates an expression. `po` gives the printable description of the expression as an object; this is more readable if the type provides its own description. The variables view shows the same information graphically. By walking the call chain the variables of the upper frames can also be seen. Changing a value is possible too; an expression such as `expression count = 0` tests an assumption without rebuilding the app.

All of these commands talk to a running process, so they have no counterpart in the model. The measurement below counts only the trace stepping leaves behind, that is, which lines were visited.

#### The Bounding Measurement

Stepping advances the code line by line. **Step over** passes a call in one step, **step into** enters the call. Two steppings that visit the same lines can advance in a different order. The model counts the set of visited lines; it cannot count the order. The code below prints two visit sequences and then meets their sets.

```swift
// main.swift — the order of stepping is invisible to the model
let overOrder = ["10", "11", "13"]
let intoOrder = ["13", "10", "11"]
print("step-over: " + overOrder.joined(separator: ","))
print("step-into: " + intoOrder.joined(separator: ","))

let stepping = Meeting(
    "stepping-order",
    first: Set(overOrder),
    second: Set(intoOrder),
    result: Set(overOrder)
)
stepping.report()
```

```
step-over: 10,11,13
step-into: 13,10,11
stepping-order: first 10,11,13 | second 10,11,13 | result 10,11,13 -> out-of-scope
```

The two sequences advanced in different orders but visited the same three lines. Because the sets are the same, the classifier saw no meeting: the result is **out-of-scope**. Order can matter in determining where a fault appears; the model does not count it.

**DE20.** The measurement counted the set of visited lines. The order of steps, repeated visits and the time spent inside a call were not measured.

#### Summary

- Breakpoints from two sources merge; a shared point is counted once.
- Messages that pass both the level and the category filter are visible; the resolution is intersection.
- An environment variable in the scheme takes the place of the code's default.
- A breakpoint requested on a line with no code is rejected within the model.
- This lesson's resolution: **union**. The order of stepping stays outside the measurement.

#### Next Step

In this lesson a fault was searched for inside the running app. The last lesson takes up the meeting of two languages in the same project: the result of two Swift methods that fall onto the same Objective-C selector is measured.

### Objective-C Interoperability

Source: https://academia.sh/en/courses/ios-platform-and-tools/objective-c-interoperability

Measures the rejection of two Swift methods that fall onto the same Objective-C selector, the union of the names exposed by the bridging header and the generated header, and the precedence of a name given with NS_SWIFT_NAME; closes the course.

Much of the older code in existing projects is still written in **Objective-C**, and new code is often Swift. The two languages live side by side in the same app. This works because Swift can expose part of its declarations to the Objective-C runtime and can import Objective-C headers. There is a limit in return: Objective-C methods are identified by a name called a **selector**, and two methods in the same class cannot carry the same selector. Swift can also tell methods apart by type information, so the meeting of these two rules has a consequence.

The measurement axis is the same one last time: when two definitions meet, how is it resolved? In this lesson the meeting definitions are selector names, the class names that headers expose, and the Swift names a method receives from two different sources. The lesson is also the close of the course.

#### The Classifier

```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 Selector

If a Swift class derives from `NSObject` or is marked with `@objc`, its methods are visible to Objective-C. Every visible method gets a selector. The selector is built from the method's name and parameter labels: the method `save(_:)` becomes the selector `save:`, the method `save(name:)` becomes `saveWithName:`, and the parameterless `reset()` becomes `reset`. The real rule also takes prepositions and a few special cases into account; the lesson's model simplifies them and distinguishes only an unlabeled parameter from a labeled one.

A selector carries no parameter type. For this reason two overloads that are legitimate in Swift fall onto the same selector in Objective-C.

```swift
@objc class Store: NSObject {
    @objc func save(_ value: Int) { }
    @objc func save(_ value: String) { }
}
```

These two methods are distinct in Swift because their parameter types differ. The compiler still does not accept this class: both ask for the selector `save:`. We do not print the compiler's rejection; the lesson counts the collision only at the model level. The model builds a selector from each method, compares the selectors of two groups, and if there is a collision leaves the class without exposing any selector. In the second attempt the colliding method is labeled `save(name:)` and the same rule now produces a result.

```swift
// main.swift — the same selector causes two Swift methods to be rejected
struct SwiftMethod {
    let name: String
    let labels: [String]
}

func selector(for method: SwiftMethod) -> String {
    var text = method.name
    for (index, label) in method.labels.enumerated() {
        if index == 0 {
            text += label == "_" ? ":" : "With" + label.prefix(1).uppercased() + label.dropFirst() + ":"
        } else {
            text += (label == "_" ? "" : label) + ":"
        }
    }
    return text
}

func exposed(_ first: Set<String>, _ second: Set<String>) -> Set<String> {
    first.isDisjoint(with: second) ? first.union(second) : []
}

let intSave = SwiftMethod(name: "save", labels: ["_"])
let stringSave = SwiftMethod(name: "save", labels: ["_"])
let namedSave = SwiftMethod(name: "save", labels: ["name"])
let reset = SwiftMethod(name: "reset", labels: [])
let clear = SwiftMethod(name: "clear", labels: [])

let groupA: Set<String> = [selector(for: intSave), selector(for: reset)]
let groupB: Set<String> = [selector(for: stringSave), selector(for: clear)]
let groupC: Set<String> = [selector(for: namedSave), selector(for: clear)]

Meeting("same-selector", first: groupA, second: groupB, result: exposed(groupA, groupB)).report()
Meeting("labelled-overload", first: groupA, second: groupC, result: exposed(groupA, groupC)).report()
```

```
same-selector: first reset,save: | second clear,save: | result empty -> rejection
labelled-overload: first reset,save: | second clear,saveWithName: | result clear,reset,save:,saveWithName: -> union
```

The first meeting is a **rejection**: both groups want the selector `save:`, there is a collision, and the class exposes no selector, so the result is empty. In the second meeting, once the label of the colliding method became `name`, the selector became `saveWithName:`, the collision was gone, and all four selectors were exposed: a **union**. So what causes the rejection is not Swift's overloading but Objective-C's selector, which does not see types. The fix is usually to give one of the methods a label.

**DE21.** The measurement built the selector from the name and the labels. The special rule for prepositions, writing the selector by hand with `@objc(name:)`, and the compiler's real error message were not measured.

#### The Bridging Header

Objective-C code is opened to Swift with a **bridging header**. The header imports, in one place, the Objective-C headers that Swift needs to see. The reverse direction is solved by a header the compiler generates: classes exposed in Swift with `@objc` become visible to Objective-C in a generated header named `<ModuleName>-Swift.h`.

```objc
// Bridging-Header.h
#import "LegacyStore.h"
#import "LegacyLogger.h"
```

```objc
// the Objective-C side imports the generated header
#import "App-Swift.h"
```

In a mixed-language module both hold at the same time. The bridging header opens the names `LegacyStore` and `LegacyLogger` to Swift. The generated header opens the names `Router` and `Formatter` to Objective-C. The set of names the module can use in both languages is the union of the names opened by the two headers; no name replaces another header's name outside its own header.

```swift
// main.swift — the names opened by the two headers merge
let bridgingHeader: Set<String> = ["LegacyStore", "LegacyLogger"]
let generatedHeader: Set<String> = ["Router", "Formatter"]
let moduleNames = bridgingHeader.union(generatedHeader)

Meeting(
    "mixed-module",
    first: bridgingHeader,
    second: generatedHeader,
    result: moduleNames
).report()
```

```
mixed-module: first LegacyLogger,LegacyStore | second Formatter,Router | result Formatter,LegacyLogger,LegacyStore,Router -> union
```

Four names came from two headers, and all four are in the module's set. Because the names of the two headers do not intersect, the union and the disjoint set give the same result; the resolution is still **union**. If a class loses its `@objc` marking while the header stays unchanged, the set of the generated header shrinks and the use on the Objective-C side breaks at build time.

**DE22.** The measurement took the names opened by the headers as a set. Members that appear in the generated header but are not opened because they are not marked `@objc`, two headers defining the same name differently, and the order of compilation were not measured.

#### A Given Name and Precedence

When Objective-C methods are imported into Swift, the compiler gives them a Swift name by rule. Often the resulting name does not fit Swift's style. The **NS_SWIFT_NAME** attribute in the header lets the developer give the Swift name by hand.

```objc
// LegacyLoader.h
- (void)fetchItemsWithCompletion:(void (^)(NSArray *))completion
    NS_SWIFT_NAME(fetch(then:));
```

There are two names here: `fetchItems(completion:)`, generated by rule, and `fetch(then:)`, written by the developer. Only one of them is visible on the Swift side. The given name replaces the generated name; the two are not visible together. The model chooses the visible name of a method from the given name if there is one, and otherwise from the generated one.

```swift
// main.swift — a name given with NS_SWIFT_NAME takes precedence over the generated name
struct ImportedMethod {
    let generated: String
    let given: String?
    var visible: String { given ?? generated }
}

let loader = ImportedMethod(generated: "fetchItems(completion:)", given: "fetch(then:)")

Meeting(
    "swift-name",
    first: [loader.generated],
    second: [loader.given ?? loader.generated],
    result: [loader.visible]
).report()
```

```
swift-name: first fetchItems(completion:) | second fetch(then:) | result fetch(then:) -> precedence
```

The given name appeared alone: the resolution is **precedence**. The generated name `fetchItems(completion:)` can no longer be written on the Swift side; only `fetch(then:)` is valid. This rule has a cost: Objective-C and Swift code refer to the same method by two names, so the name has to be chosen with everyone on the team who reads both languages in mind.

**DE23.** The measurement took the two names of a single method. A given name that is not a valid Swift name, two methods in the same class receiving the same name, and the old name being kept with a warning were not measured.

#### The Bounding Measurement

The last limit is dispatch. When a method is marked `@objc dynamic`, the call is resolved not by Swift's direct call but by the Objective-C runtime's message send. Because an Objective-C class can also override the same selector in a subclass, which body the call goes to is decided at run time. This is not a question of names but a question of choosing a body, and it is not the measure of this course. The axis on which the type determines the body of a call was measured in an earlier course (M08/K02).

To put the question to the model, we write the selector of both sides: the Swift body says `refresh`, and the override in Objective-C also says `refresh`.

```swift
// main.swift — dynamic dispatch is invisible in the name set
let swiftBody: Set<String> = ["refresh"]
let objcOverride: Set<String> = ["refresh"]

Meeting(
    "dynamic-dispatch",
    first: swiftBody,
    second: objcOverride,
    result: swiftBody
).report()
```

```
dynamic-dispatch: first refresh | second refresh | result refresh -> out-of-scope
```

Both sides carry the same selector and the result is **out-of-scope**. Because the sets are the same, the classifier saw no meeting; which body will run cannot be read from these sets.

**DE24.** The measurement counted the selector name of the two bodies. Which body runs, whether the override calls `super`, and the direct dispatch of a call that is not `dynamic` were not measured.

#### Summary

- Two Swift methods that fall onto the same Objective-C selector collide; in this case the model rejects the class, and the collision ends when a label changes.
- The bridging header opens names to Swift and the generated header opens names to Objective-C; the names of a mixed-language module are the union of the two sets.
- A name given with `NS_SWIFT_NAME` takes precedence over the name generated by rule.
- Dynamic dispatch is out-of-scope: which body a call goes to is a question of choosing a body, not of names.
- This lesson's resolution: **rejection**.

#### Course Wrap-Up

The twelve lessons of this course answered a single question: when two definitions meet, how is it resolved? Each lesson set up a meeting, measured the result, and stated it with one of four names. The table puts the lesson, the meeting, the resolution and the lesson's bounding measurement side by side. The first six lessons cover the structure of the platform, the last six the development environment.

| Lesson | Meeting | Resolution | Bounding measurement |
|---|---|---|---|
| Platform Layers | The four layers of the platform | the core's four (2/2/2/2) | Two nested definitions stay `out-of-scope` |
| Frameworks and Libraries | Framework and library names | union | A qualified name counts as a separate name |
| Domain Frameworks | Domain permissions | intersection | The scope of a permission is not read |
| The App Lifecycle | Lifecycle events | precedence | The order of transitions is not measured |
| Interface Design Guidelines | Guideline and app values | precedence | A guideline is an expectation and stays `off-rule` |
| Versions and Compatibility | Version ranges | intersection | The minimum version is a choice |
| The Development Environment | Two levels of the same setting | precedence | The scheme choice is out-of-scope |
| Project File Structure | Template and build keys | union | Keys are counted, not values |
| The Package Manager | Version constraints | intersection | The set is counted, not the chosen version |
| Simulator and Device | Required and offered capabilities | rejection | Performance is not measured |
| Debugging Basics | Breakpoint sources | union | The order of stepping is not measured |
| Objective-C Interoperability | Methods falling onto the same selector | rejection | Dynamic dispatch is out-of-scope |

The distribution of the eleven lessons other than the opening lesson is as follows: three are precedence (The Development Environment, The App Lifecycle, Interface Design Guidelines), three are intersection (Domain Frameworks, Versions and Compatibility, The Package Manager), three are union (Frameworks and Libraries, Project File Structure, Debugging Basics), and two are rejection (Simulator and Device, Objective-C Interoperability). This count comes from the table's resolution column. The core at the opening of the course, on the other hand, had distributed eight meetings 2/2/2/2. The two are not the same thing: the core counts eight meetings, the table's distribution counts the eleven lessons other than the opening one.

The habit the course leaves is to name first the place where two definitions meet, and then leave the resolution to measurement. The platform, the project and the tools ask the same question from different sides; knowing which set an answer comes from is more reliable than applying a wrong rule by intuition. Next comes the interface built on this platform: M30/K03 covers the declarative interface, the view tree and state.

