1.2 — Setup, Playgrounds & SPM (where Swift code actually lives)

Opening scenario

You’re three hours into your Swift journey and you have three places to write code: Xcode Playgrounds, a Swift Package, and the swift command in your terminal. Which one is “real”? When do you reach for each? You watch a tutorial that says “open a Playground,” another that says “create a new package with swift package init,” and a third that uses Xcode’s “macOS Command Line Tool” template. You feel like everyone is gatekeeping the right answer.

There is no single right answer — but there are very right answers for each situation. By the end of this chapter you’ll know exactly which surface to use, and why.

The four places Swift lives

SurfaceBest forBad at
Playgrounds (Xcode app)Trying a language feature, prototyping a UI snippet, exploring an APIMulti-file projects, long-running code, anything depending on a 3rd-party package
swift REPL (terminal)One-line sanity check (swift -e 'print(1+1)')Anything with imports beyond Foundation
Swift Package (Package.swift + folder)Real libraries, CLIs, server code, sharing code across iOS/macOS/LinuxUI apps that ship to the App Store
Xcode app project (.xcodeproj / .xcworkspace)iOS/macOS/watchOS/tvOS apps you ship to usersAnything that needs to run on Linux/server

For this chapter you’ll set up the first three. We’ll meet .xcodeproj in Phase 2 when you build your first SwiftUI app.

Concept → Why → How → Code

Concept: a Swift Package is just a folder + a manifest

Forget magic. A package is:

MyPackage/
├── Package.swift          ← the manifest (a Swift file describing the package)
├── Sources/
│   └── MyPackage/
│       └── MyPackage.swift  ← your code
└── Tests/
    └── MyPackageTests/
        └── MyPackageTests.swift

That’s it. No build files generated by Xcode. No .pbxproj to merge-conflict over. The manifest is the project file.

Why this matters

Before SPM (Swift Package Manager, shipped in Swift 3, matured around Swift 5.5), iOS engineers used CocoaPods or Carthage for dependency management — both of which generated giant .pbxproj files that constantly merge-conflicted. SPM moved dependency declaration into a small, plain-text, version-controlled Swift file. It’s why modern Swift codebases feel lightyears nicer to work in.

How: create a package right now

mkdir HelloSwift && cd HelloSwift
swift package init --type executable
swift run

You should see:

Building for debugging...
Build complete!
Hello, world!

You just compiled and ran a Swift program with one command. That’s the SPM promise.

Code: dissect the manifest

Open Package.swift:

// swift-tools-version: 6.0
import PackageDescription

let package = Package(
    name: "HelloSwift",
    targets: [
        .executableTarget(name: "HelloSwift")
    ]
)

Five things to notice:

  1. The first line is a comment that the tool actually parses. It tells SPM the minimum Swift tools version required.
  2. PackageDescription is a Swift module. The manifest is real Swift, executed in a sandbox by SPM at package-resolution time.
  3. targets define build units. A target is “a thing that gets compiled into one binary or one library.”
  4. Folder conventions are hard-coded. SPM looks in Sources/<TargetName>/ automatically. Don’t move files unless you tell SPM where they went via path:.
  5. Dependencies go in two places: at the package level (dependencies: [.package(url: …)]) and at each target that needs them (dependencies: [.product(name: …, package: …)]).

Here’s a slightly bigger example with a dependency:

// swift-tools-version: 6.0
import PackageDescription

let package = Package(
    name: "HelloSwift",
    platforms: [.macOS(.v14)],            // minimum OS we target
    dependencies: [
        .package(url: "https://github.com/apple/swift-argument-parser",
                 from: "1.3.0"),
    ],
    targets: [
        .executableTarget(
            name: "HelloSwift",
            dependencies: [
                .product(name: "ArgumentParser", package: "swift-argument-parser"),
            ]
        ),
    ]
)

Run swift build again — SPM downloads, resolves, and links the dependency, all without Xcode opening.

Playgrounds: when to reach for one

Open Xcode → File → New → Playground → macOS → Blank.

import Foundation

let names = ["Ada", "Linus", "Grace", "Dennis"]
let upper = names.map { $0.uppercased() }
print(upper)
// → ["ADA", "LINUS", "GRACE", "DENNIS"]

The result column on the right shows you the value of every expression as you type. There’s no Run button you press repeatedly — it runs continuously as you edit. Playgrounds are the fastest feedback loop in the Apple toolchain.

When Playgrounds shine:

  • “What does .map actually return here?”
  • Exploring a new SwiftUI view shape.
  • Pasting in a snippet from documentation and tweaking it.

When Playgrounds frustrate:

  • Anything with import of a 3rd-party package (you can add packages to a Playground, but it’s clunky).
  • Code that takes more than a second to run.
  • Multi-file projects.
  • Anything you’ll commit to a repo.

In the wild

  • Apple uses Playgrounds internally for evangelism — every SwiftUI session at WWDC ships a downloadable Playground.
  • Swift Playgrounds.app (the consumer iPad app, distinct from Xcode Playgrounds) is what Apple uses to teach Swift to high-school students. It’s the same kernel underneath.
  • Server-side Swift at companies like Apple itself (most of iCloud’s backend is now Swift), Kitura/Vapor users — runs as Swift Packages with swift run in production.
  • The Swift compiler itself is a Swift Package. So is SwiftLint. So is Alamofire. SPM has eaten the ecosystem.

Common misconceptions

  1. “You need Xcode to write Swift.” False. On macOS, Linux, and even Windows (preview), the swift toolchain ships separately. You can write a complete server-side Swift app in VS Code with the Swift VS Code extension and never open Xcode.

  2. swift run and the Xcode Run button do the same thing.” Subtly different. Xcode adds build configurations, codesigning steps, and platform-specific entitlements. swift run is just swift build then execute. For pure CLI/library code they’re equivalent; for an iOS app they’re not even comparable.

  3. “Playgrounds are for beginners.” Senior engineers use them constantly to verify API behavior. The first thing many of us do when learning a new framework is open a Playground and call its API to see what comes back.

  4. “SPM doesn’t support resources.” It does, since Swift 5.3. You declare them in the target with resources: [.process("Assets")].

Seasoned engineer’s take

The mental model that took me too long to develop: every Swift codebase I’ve worked on professionally is fundamentally a set of Swift Packages, plus an Xcode-shaped wrapper that turns one of them into an iOS app.

Modern iOS projects look like this:

MyAppRepo/
├── App/
│   └── MyApp.xcodeproj           ← thin wrapper, mostly Info.plist + entry point
├── Packages/
│   ├── Networking/Package.swift  ← URLSession code, Sendable models
│   ├── DesignSystem/Package.swift  ← reusable SwiftUI components
│   └── Feature-Profile/Package.swift  ← one feature module

Why? Because:

  • Each package builds and tests in isolation (faster compile, faster CI).
  • Each package can be opened in Xcode by itself for tight feedback loops.
  • You can pull a package out and reuse it in another app or on the server.
  • The “app” is just dependency-injecting features into a WindowGroup.

Companies that have moved here in public: Spotify, Airbnb, the New York Times, Lyft, Robinhood. Once you internalize “every feature is a package,” you stop fearing dependency arrows and start designing them.

TIP: Use swift package generate-xcodeproj is deprecated in Swift 5.7+. Don’t try to generate .xcodeproj files anymore — just open the Package.swift directly in Xcode (File → Open and pick the folder). Xcode 11+ has first-class SPM support.

WARNING: Putting non-trivial logic in Package.swift is an anti-pattern. The manifest runs in a sandbox at resolution time. Conditionals based on ProcessInfo.processInfo.environment will work but make your package brittle and surprising to consumers. Keep manifests boring.

Interview corner

Question: “Walk me through how you’d structure a new iOS app in 2026.”

Junior answer: “I’d open Xcode, create a new iOS app project, and start coding inside it.” → Will get you a friendly nod and a follow-up: ‘and after that?’ If you don’t have an answer, you’re done.

Mid-level answer: “I’d start with an Xcode project for the app shell, then break out feature modules into Swift Packages — one for networking, one for the design system, one per feature. Each package has its own tests. The app target depends on the packages.” → Solid. Most interviewers stop here.

Senior answer: Everything above, plus: “I’d think hard about the dependency direction upfront. Feature packages should depend on abstractions (a NetworkClient protocol in a tiny NetworkingInterface package), not on concrete implementations. The app target wires the concrete URLSession-backed implementation in at composition time. That way each feature is unit-testable with a fake client, and you can swap the networking layer without touching feature code. It costs maybe a day of upfront design and pays back forever. I’d also pick the package boundaries by team boundary if the team is more than ~6 engineers — Conway’s Law applies to module graphs.” → That’s a hire.

Red-flag answer: “I’d just use CocoaPods like we did at my last job.” → Tells the interviewer you stopped learning in 2019. CocoaPods is in maintenance mode; new iOS projects in 2026 use SPM almost universally.

Lab preview

Lab 1.B (CLI with SPM) walks you through building a real command-line tool — argument parsing, file I/O, error handling — as an executable Swift Package you could publish to GitHub today.


Now that you can run Swift, let’s look at what the language actually is. → Types, variables, optionals