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
| Surface | Best for | Bad at |
|---|---|---|
| Playgrounds (Xcode app) | Trying a language feature, prototyping a UI snippet, exploring an API | Multi-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/Linux | UI apps that ship to the App Store |
Xcode app project (.xcodeproj / .xcworkspace) | iOS/macOS/watchOS/tvOS apps you ship to users | Anything 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:
- The first line is a comment that the tool actually parses. It tells SPM the minimum Swift tools version required.
PackageDescriptionis a Swift module. The manifest is real Swift, executed in a sandbox by SPM at package-resolution time.targetsdefine build units. A target is “a thing that gets compiled into one binary or one library.”- Folder conventions are hard-coded. SPM looks in
Sources/<TargetName>/automatically. Don’t move files unless you tell SPM where they went viapath:. - 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
.mapactually return here?” - Exploring a new SwiftUI view shape.
- Pasting in a snippet from documentation and tweaking it.
When Playgrounds frustrate:
- Anything with
importof 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 runin production. - The Swift compiler itself is a Swift Package. So is SwiftLint. So is Alamofire. SPM has eaten the ecosystem.
Common misconceptions
-
“You need Xcode to write Swift.” False. On macOS, Linux, and even Windows (preview), the
swifttoolchain ships separately. You can write a complete server-side Swift app in VS Code with the Swift VS Code extension and never open Xcode. -
“
swift runand the Xcode Run button do the same thing.” Subtly different. Xcode adds build configurations, codesigning steps, and platform-specific entitlements.swift runis justswift buildthen execute. For pure CLI/library code they’re equivalent; for an iOS app they’re not even comparable. -
“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.
-
“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-xcodeprojis deprecated in Swift 5.7+. Don’t try to generate.xcodeprojfiles anymore — just open thePackage.swiftdirectly in Xcode (File → Openand pick the folder). Xcode 11+ has first-class SPM support.
WARNING: Putting non-trivial logic in
Package.swiftis an anti-pattern. The manifest runs in a sandbox at resolution time. Conditionals based onProcessInfo.processInfo.environmentwill 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