When your uploader pushes a build to App Store Connect, the very first thing it needs to know is which platform the build targets -- iOS, macOS, tvOS, or visionOS. Four choices, and exactly four. How should you represent that in code?
Your first instinct might be a String:
func upload(build: URL, platform: String) { ... }
upload(build: archive, platform: "iOS")It works until it doesn't. Is it "iOS" or "ios" or "iphoneos"? What happens when a caller passes "iOs" with a stray capital, or "Android", which your uploader has no idea what to do with? Nothing stops them -- a String parameter accepts every string in the universe, and only a handful are valid.
An Int is no better. If platform: 3 means tvOS, you've just traded typos for a guessing game: nobody reading upload(build: archive, platform: 3) knows what 3 is.
What you actually want is a brand-new type whose values are exactly the four platforms, nothing more -- so the compiler rejects anything else before your app ever runs. That type is an enumeration.
An enum is a list of related, mutually exclusive values grouped under one type. Hand a function a Platform and it's guaranteed to receive one of the four real platforms -- never "Android", never 3, never a misspelling. The compiler checks it for you. Cardinal directions, card suits, user roles, HTTP methods, loading states -- anywhere you have a small, fixed set of possibilities, an enum is the right tool.
And Swift enums are far more capable than the integer constants you might know from C. As you'll see, they can hold methods, computed properties, and even data attached to each case. By the end of this chapter you'll also learn a secret that's been hiding in plain sight: the Optional you've used since Chapter 8 is itself an enum.
Declaring an Enumeration
You list the possibilities as case clauses:
enum Platform {
case iOS
case macOS
case tvOS
case visionOS
}That's a complete new type, Platform, with exactly four possible values. The convention is to write each case in lowerCamelCase, the same as a property name. If you prefer, collapse the cases onto one line separated by commas:
enum Platform {
case iOS, macOS, tvOS, visionOS
}Create a value by naming the type and the case:
let target = Platform.iOSSwitching Over an Enum
Enums and switch are made for each other. Rewrite the upload helper to take a Platform and branch on it:
func uploadLabel(for platform: Platform) -> String {
switch platform {
case .iOS:
return "iOS App"
case .macOS:
return "Mac App"
case .tvOS:
return "tvOS App"
case .visionOS:
return "visionOS App"
}
}Two things are worth slowing down on.
The dot shorthand. Inside the switch, you write .iOS, not Platform.iOS. Because the parameter is already typed Platform, the compiler infers the type and lets you drop the prefix. The same shorthand works anywhere the type is known -- you could have written let target: Platform = .iOS.
No default needed -- and that's a feature. Back in the control-flow chapters, every switch over a String or Int needed a default, because those types have infinitely many values you couldn't possibly list. An enum has a finite set, so once you've handled all four cases, the switch is exhaustive and a default would be dead code. Better still: if someone later adds a case watchOS to Platform, the compiler immediately flags this switch -- and every other non-exhaustive switch -- as an error, pointing you straight at the code that needs updating. A default clause would have silently swallowed the new case instead. Leaving default off turns the compiler into a checklist that can't be ignored.
This "add a case, break every switch" behavior is one of the best reasons to model fixed sets as enums rather than strings. The day you support a fifth platform, the compiler hands you an exact list of every place in the codebase that has to change. That's a refactoring superpower you give up the moment you reach for a default.
Enums Carry Behavior
A Swift enum isn't just a bag of labels -- it's a full type that can have methods and computed properties, just like a struct. Move the display logic into Platform so it travels with the value:
enum Platform {
case iOS, macOS, tvOS, visionOS
var displayName: String {
switch self {
case .iOS: "iOS"
case .macOS: "macOS"
case .tvOS: "tvOS"
case .visionOS: "visionOS"
}
}
var supportsTouch: Bool {
switch self {
case .iOS, .visionOS: true
case .macOS, .tvOS: false
}
}
}
Platform.iOS.displayName // "iOS"
Platform.tvOS.supportsTouch // falseInside the enum, self is the current case, so you switch over it exactly as you did with the parameter. (Notice these computed properties have no return: a switch used as the whole body of a property or function is an expression, and Swift returns its value automatically. Use it for tidy lookups like these.)
Mini-exercise
Add a computed property var hasHomeIndicator: Bool to Platform that returns true for .iOS and .visionOS and false otherwise. Then read it off Platform.macOS.
Raw Values
Unlike C, Swift enum cases are not secretly integers. Platform.iOS is its own thing -- it has no 1 hiding behind it. But sometimes you want a backing value: a stable string or number to store in a database, send over the network, or match against an API. You opt in by declaring a raw value type after the enum name.
Your backend's upload endpoint expects a lowercase slug for the platform. Back the enum with String and pin each slug:
enum Platform: String {
case iOS = "ios"
case macOS = "macos"
case tvOS = "tvos"
case visionOS = "visionos"
}
Platform.iOS.rawValue // "ios"
Platform.visionOS.rawValue // "visionos"Every case now has a rawValue property holding its slug. For String-backed enums there's a shortcut: if you don't specify the raw values, Swift uses the case names themselves:
enum FileKind: String {
case png, jpeg, mov, pdf
}
FileKind.mov.rawValue // "mov" -- the case name, for freeInt-backed enums get a different freebie -- they auto-increment. Give the first one a value (or none) and the rest count up:
enum Tier: Int {
case free // 0
case pro // 1
case proPlus // 2
}The raw values don't have to be sequential, either. When the numbers carry real meaning -- like HTTP status codes -- assign each one explicitly:
enum UploadStatus: Int {
case ok = 200
case unauthorized = 401
case payloadTooLarge = 413
case tooManyRequests = 429
}
UploadStatus.tooManyRequests.rawValue // 429Keep one thing straight: a Tier is not an Int, it merely has an Int raw value. You can't add two Tier values together, but you can add their rawValues. The raw value is a label attached to the case, not the case itself.
Building a Case From a Raw Value
Raw values flow both ways. Given a slug from the network, you can try to turn it back into a Platform with the failable initializer init?(rawValue:):
Platform(rawValue: "ios") // Optional(Platform.iOS)
Platform(rawValue: "android") // nilWhy optional? Because the string you pass in might not correspond to any case -- "android" matches nothing, so the initializer returns nil rather than inventing a value. This is a failable initializer, and it's exactly why the result is an Optional. Handle it the way you handle any optional -- bind it before use:
func platform(fromSlug slug: String) -> Platform {
guard let platform = Platform(rawValue: slug) else {
return .iOS // sensible fallback for an unrecognized slug
}
return platform
}Only force-unwrap with ! when you're certain the raw value is valid -- a literal you typed yourself, say. For anything coming from a file, a server, or a user, treat the optional with respect.
Looping Over Every Case: CaseIterable
App Store Connect wants screenshots for several device classes, and you'd like to build a checklist of all of them. Listing the cases by hand would be one more thing to forget to update. Instead, conform the enum to CaseIterable:
enum DeviceClass: CaseIterable {
case iPhone67, iPhone61, iPadPro13, mac
var label: String {
switch self {
case .iPhone67: "6.7-inch iPhone"
case .iPhone61: "6.1-inch iPhone"
case .iPadPro13: "13-inch iPad Pro"
case .mac: "Mac"
}
}
}
for device in DeviceClass.allCases {
print("Need screenshots for: \(device.label)")
}Conforming to CaseIterable makes Swift synthesize a static property allCases -- an array of every case, in the order you declared them. You didn't write allCases yourself; the compiler generated it. It's the clean way to drive a settings list, validate against every possibility, or -- as here -- build a requirements checklist that can never drift out of sync with the enum.
Mini-exercise
Add a case .appleWatch to DeviceClass (with a label of "Apple Watch"). Without touching the loop, confirm the checklist now prints five lines. What did CaseIterable save you from updating by hand?
Associated Values
So far every value of a given case has been identical -- one Platform.iOS is the same as any other. Associated values change that: they let each case carry data, and different cases can carry different data.
Think about the result of an upload. It can succeed, it can be rejected, or the server can ask you to back off and retry -- and each outcome comes with different information. A success has a build number; a rejection has a reason; a retry has a wait time. One enum captures all three:
enum UploadOutcome {
case accepted(build: Int)
case rejected(reason: String)
case retry(afterSeconds: Int)
}A few rules govern associated values:
- Each case can have zero or more associated values (
.acceptedcarries one; you could add a case with none). - Each case's associated values have their own types -- an
Inthere, aStringthere. - You can label them (
build:,reason:) just like function parameters, which makes call sites readable. - An enum has either raw values or associated values, never both -- they're different mechanisms for different jobs.
You create a value by supplying the data:
let outcome = UploadOutcome.accepted(build: 412)
let problem = UploadOutcome.rejected(reason: "Missing 6.7-inch screenshots")To read the data back out, you pattern-match and bind it with let inside the switch:
func describe(_ outcome: UploadOutcome) -> String {
switch outcome {
case .accepted(let build):
return "Build \(build) is processing."
case .rejected(let reason):
return "Rejected — \(reason)"
case .retry(let afterSeconds):
return "Server busy. Retrying in \(afterSeconds)s."
}
}
describe(outcome) // "Build 412 is processing."The bound names (build, reason, afterSeconds) are local to each case and exist only inside that branch. They don't have to match the labels in the declaration -- it's just conventional and clear to reuse them.
When you only care about one case, a full switch is overkill. Use if case or guard case to match a single pattern and bind its value:
if case .accepted(let build) = outcome {
print("Uploaded build \(build)") // runs only when outcome is .accepted
}
func requireBuild(from outcome: UploadOutcome) -> Int {
guard case .accepted(let build) = outcome else {
return -1 // not an accepted outcome
}
return build
}Read if case .accepted(let build) = outcome as "if outcome is an .accepted, pull its build number into build." It's the single-case counterpart to a switch.
Modeling success-or-failure with an enum that carries data is so common that Swift ships a built-in version: Result<Success, Failure>, with cases .success(Success) and .failure(Failure). Your UploadOutcome is the same idea, hand-rolled for a domain with three outcomes instead of two. Once associated values click, you'll see this pattern everywhere networking and error handling appear.
Enums as State Machines
An enum is, by definition, in exactly one case at a time -- never two, never none. That makes it a natural state machine: a model of something that moves through a fixed set of states one at a time. A build's journey through review is a perfect example:
enum ReviewState {
case draft
case waitingForReview
case inReview
case pendingDeveloperRelease
case readyForSale
case rejected
}A given version is in precisely one of these states. It can't be simultaneously inReview and readyForSale -- the type system makes that nonsensical state unrepresentable, which is a quieter but huge benefit: bugs that can't be expressed can't happen. Attach behavior that depends on the state:
extension ReviewState {
var canEditMetadata: Bool {
switch self {
case .draft, .rejected: true
default: false
}
}
var isTerminal: Bool {
switch self {
case .readyForSale, .rejected: true
default: false
}
}
}
ReviewState.inReview.canEditMetadata // false — locked while Apple reviews
ReviewState.draft.canEditMetadata // trueVending machines, traffic lights, download managers, onboarding flows -- anything that follows a defined sequence of states is begging to be modeled as an enum, precisely because the enum guarantees only one state holds at a time.
Mini-exercise
Model a feature flag as an enum AutoUpload with cases .enabled and .disabled, and give it a computed property var symbol: String returning "●" when enabled and "○" when disabled.
Case-less Enums as Namespaces
Back in Chapter 17 you sprinkled some magic numbers through the asset code -- a 500 MB byte ceiling, a 30-second cap on app previews. Loose constants like that are easy to mistype and hard to find. Group them under one roof:
enum UploadLimits {
static let maxAssetKB = 500_000
static let maxPreviewSeconds = 30
static let maxScreenshotsPerDevice = 10
}
UploadLimits.maxPreviewSeconds // 30UploadLimits has no cases at all -- it exists purely to hold static members under a tidy name. You might wonder why this is an enum and not a struct, since a struct could hold the same constants. Here's the reason:
let limits = UploadLimits() // ⛔ 'UploadLimits' cannot be constructed because it has no accessible initializersA struct can be instantiated, and an instance of a pure-constants type is meaningless -- there's no state to put in it. A case-less enum can't be instantiated, because creating an enum value means choosing a case, and there are none. The compiler turns "this is a namespace, not a value" from a comment into a guarantee. (Such an enum is sometimes called an uninhabited type -- a type with no possible values.)
Reach for a case-less enum whenever you want a namespace for related constants or static helpers and an instance would never make sense.
The Big Reveal: Optionals Are Enums
You've used optionals since Chapter 8 -- Int?, if let, nil, the ! and ? operators. Time to pull back the curtain: an optional is just an enum you've been using without seeing its shape. Stripped down, Optional is defined like this:
enum Optional<Wrapped> {
case none
case some(Wrapped)
}(The <Wrapped> is a generic placeholder -- it's how one enum works for Int?, String?, and every other type. Generics get their own chapter soon.) Everything optional-related maps onto these two cases:
nilis.none-- the "no value" case, with nothing attached.- A value like
42is.some(42)-- the "has a value" case, with the value as an associated value.
Which means every tool from this chapter works on optionals directly. You can switch over one:
let build: Int? = 412
switch build {
case .none:
print("No build uploaded yet")
case .some(let number):
print("Build number is \(number)")
}And the everyday syntax is just sugar over those cases:
let missing: Int? = nil
missing == nil // true
missing == .none // true — nil and .none are the same thingThe ?, !, if let, and nil you've leaned on are a friendly costume over Optional.none and Optional.some. Optionals were teaching you associated values and pattern matching all along -- you just didn't know the trick yet.
Challenges
Build these in a playground to lock the concepts in.
Challenge 1: the next tier up
Start from enum Tier: Int { case free, pro, proPlus }. Add a computed property var next: Tier that returns the next tier up the ladder, and stays at .proPlus when there's nowhere higher to go.
Hint: use rawValue + 1 with the failable Tier(rawValue:) initializer, and the nil-coalescing operator (??) to fall back to self when the next raw value doesn't exist.
Challenge 2: a user-facing message
Using the UploadOutcome enum, write func message(for outcome: UploadOutcome) -> String that returns a friendly sentence for each case -- the build number on success, the reason on rejection, and the wait time on retry. Test it against one value of each case.
Challenge 3: the missing-screenshots checklist
Using DeviceClass: CaseIterable, write func missing(have provided: [DeviceClass]) -> [DeviceClass] that returns every device class the user has not yet provided screenshots for. Calling it with [.iPhone67, .mac] should return the remaining classes.
Hint: filter DeviceClass.allCases by whether provided contains each case. (You'll need the enum to be Equatable -- which, for an enum with no associated values, Swift gives you automatically.)
Challenge 4: namespace refactor
Here's a constants holder written as a struct:
struct AppStoreLimits {
static let maxKeywordsLength = 100
static let maxPromoTextLength = 170
static let maxSubtitleLength = 30
}Convert it to a case-less enum. In a code comment, state exactly what the compiler now prevents that it didn't before, and why that's a good thing for a type that's nothing but constants.
Key Points
- An enumeration defines a new type whose values are a fixed, mutually exclusive set of cases -- a type-safe alternative to loose strings or integers.
switchover an enum can be exhaustive without adefault. Omittingdefaultmakes the compiler flag every switch when a new case is added -- a feature, not a chore.- Enums are real types: they can have methods and computed properties, switching over
selfto vary behavior per case. - Raw values (
enum X: String/: Int) give each case a backing constant accessible viarawValue. String raw values default to the case name; Int raw values auto-increment. Build a case back from a raw value with the failableinit?(rawValue:), which returns an optional. CaseIterablesynthesizes a staticallCasesarray in declaration order -- ideal for checklists, menus, and validation loops.- Associated values attach data to a case, with each case carrying its own types. Read them by binding with
letin aswitch, or withif case/guard casefor a single case. An enum has raw values or associated values, never both. - An enum is always in exactly one case, which makes it a natural state machine -- and lets you make invalid states unrepresentable.
- A case-less enum can't be instantiated, making it the right choice for a pure namespace of
staticconstants and helpers. Optionalis an enum with cases.none(which isnil) and.some(Wrapped)(the value as an associated value). The optional syntax you already know is sugar over those two cases.
You've now met the third of Swift's custom types -- struct, class, and enum -- and you keep bumping into two ideas that cut across all of them. CaseIterable, Equatable, and Optional's <Wrapped> hinted at them: protocols, the contracts that let unrelated types share capabilities, and generics, code that works across many types at once. Those are the next two chapters, and together they're what make Swift's type system feel less like a set of boxes and more like a toolkit.
Ship your apps faster
When you're ready to publish your Swift app to the App Store, Simple App Shipper handles metadata, screenshots, TestFlight, and submissions — all in one place.
Try Simple App Shipper