---
title: 'Project File Structure'
source: 'https://academia.sh/en/courses/ios-platform-and-tools/project-file-structure'
course: 'iOS Platform and Tools'
language: en
updated: '2026-10-21T05:03:10+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Measures that the keys a template writes and the keys the build generates are resolved by union'
    - 'Shows why two resource files added to the same target under the same name are rejected'
    - 'Measures that a device-specific asset takes precedence over a general asset and that user languages intersect with localizations'
    - 'Shows with a bounding measurement that union counts keys, not values'
---

# 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.
