In the last chapter you saw what makes a class different from a struct: reference semantics, identity, the heap. Those properties are the reason classes exist, but they're also the gateway to a second feature structs can't touch -- inheritance.
Inheritance is what people usually mean when they say "object-oriented programming": one class building on top of another, specializing it, overriding pieces of it, and standing in for it wherever the more general type is expected. It's a powerful tool and an easy one to overuse, so this chapter does two things at once -- it teaches you the mechanics (which you'll genuinely need), and it keeps nudging you toward the question should I be subclassing here at all?
We'll hang every example off one running model: the asset types an App Store submission deals with. A screenshot, an app preview video, an icon -- they're all "assets," but each has its own rules. That's exactly the shape inheritance was built for.
The is-a Relationship
Start with the most general thing your uploader cares about. Every asset has a filename and a size, and every asset can be checked for problems before upload:
class Asset {
let filename: String
var byteSizeKB: Int
init(filename: String, byteSizeKB: Int) {
self.filename = filename
self.byteSizeKB = byteSizeKB
}
func validationWarnings() -> [String] {
var warnings: [String] = []
if byteSizeKB > 500_000 {
warnings.append("\(filename) is over the 500 MB ceiling.")
}
return warnings
}
}A screenshot is a kind of image asset, which is a kind of asset. You can encode that relationship directly. A class inherits from another by naming it after a colon -- the same syntax you've seen for protocol conformance and for RawRepresentable enums, but here it means something stronger:
class ImageAsset: Asset {
var pixelWidth: Int
var pixelHeight: Int
init(filename: String, byteSizeKB: Int, pixelWidth: Int, pixelHeight: Int) {
self.pixelWidth = pixelWidth
self.pixelHeight = pixelHeight
super.init(filename: filename, byteSizeKB: byteSizeKB)
}
}ImageAsset is a subclass of Asset; Asset is its superclass. Through inheritance, ImageAsset automatically has filename, byteSizeKB, and validationWarnings() -- you didn't redeclare them, but they're there. It then adds two stored properties of its own. Subclasses are additive: they get everything the parent had, plus whatever they declare.
(Ignore the super.init line for a moment -- initialization across a hierarchy has its own rules, and we'll get to them. For now just note that a subclass has to hand the superclass's properties up to the superclass to set.)
You can keep going. A screenshot is a more specific ImageAsset:
class Screenshot: ImageAsset {
var deviceClass: String
init(filename: String, byteSizeKB: Int,
pixelWidth: Int, pixelHeight: Int, deviceClass: String) {
self.deviceClass = deviceClass
super.init(filename: filename, byteSizeKB: byteSizeKB,
pixelWidth: pixelWidth, pixelHeight: pixelHeight)
}
}Now there's a chain: Screenshot → ImageAsset → Asset. That chain is a class hierarchy, and a Screenshot instance carries every property and method declared anywhere along it.
Two rules govern how far this can go:
- Single inheritance. A Swift class has exactly one direct superclass. You cannot inherit from two classes at once. (Protocols, which you'll meet soon, are how Swift lets a type take on traits from many sources.)
- No depth limit. You can subclass a subclass of a subclass as deep as you like. In practice, deep is a smell -- more on that at the end.
Structs and enums can't inherit from anything. This is one of the few hard reasons to reach for a class: when you genuinely need an is-a hierarchy, or when you need to subclass a framework class like UIView or Operation. If you only want to share behavior, a protocol is almost always the better tool -- we'll come back to that.
Polymorphism: One Type Standing In for Many
Here's the payoff. Because a Screenshot is an Asset, you can use one anywhere an Asset is expected. Write a function that audits a whole bundle, accepting the most general type:
func auditBundle(_ assets: [Asset]) -> [String] {
assets.flatMap { $0.validationWarnings() }
}auditBundle knows nothing about screenshots or previews. It only knows it has Assets, and that every Asset can produce warnings. Yet you can hand it a mixed array:
let assets: [Asset] = [
Screenshot(filename: "home.png", byteSizeKB: 820,
pixelWidth: 1290, pixelHeight: 2796, deviceClass: "6.7-inch"),
AppPreview(filename: "trailer.mov", byteSizeKB: 48_000, durationSeconds: 42)
]
auditBundle(assets)This is polymorphism -- "many shapes." A single variable of type Asset can, at runtime, actually be a Screenshot, an AppPreview, or any other subclass, and your code treats them uniformly through the common interface. The array's element type is Asset, so as far as the compiler is concerned every element is "just an asset," even though each box holds something more specific.
Polymorphism is what makes inheritance worth the trouble: you write one function against a base type and it works for every specialization, including ones you haven't invented yet.
Overriding: Specializing Inherited Behavior
The auditBundle example only pays off if each asset type can change what validationWarnings() does. An image has dimension rules; a video has a duration limit. A subclass replaces an inherited method by overriding it:
class ImageAsset: Asset {
// ...existing stored properties and init...
override func validationWarnings() -> [String] {
var warnings = super.validationWarnings()
if pixelWidth == 0 || pixelHeight == 0 {
warnings.append("\(filename) is missing pixel dimensions.")
}
return warnings
}
}Two keywords are doing the work.
override is required, and the compiler is strict about it. If you write a method with the same signature as one in the superclass but forget override, you get an error -- Swift won't let you accidentally shadow a parent method. And if you write override on a method that doesn't match anything in the superclass, you also get an error. Either way the keyword forces you to be honest about your intent:
override func validationWarnings() -> [String] { ... } // ✅ matches the parent
override func validateWarnigns() -> [String] { ... } // ⛔ typo: nothing to override
func validationWarnings() -> [String] { ... } // ⛔ shadows parent without `override`super refers to the superclass's implementation. super.validationWarnings() runs Asset's version and hands you its result, which the override then extends. This is the whole point: Asset already knows how to check byte size, and you'd rather not copy that logic into every subclass. You call up to the parent, then add your own concern on top.
Add a video branch so the audit has something to chew on:
class AppPreview: Asset {
var durationSeconds: Double
init(filename: String, byteSizeKB: Int, durationSeconds: Double) {
self.durationSeconds = durationSeconds
super.init(filename: filename, byteSizeKB: byteSizeKB)
}
override func validationWarnings() -> [String] {
var warnings = super.validationWarnings()
if durationSeconds > 30 {
warnings.append("App previews must be 30 seconds or shorter.")
}
return warnings
}
}Now look back at auditBundle. When it calls $0.validationWarnings() on an element typed as Asset, Swift runs the most specific override for whatever the box actually contains -- Screenshot's if it's a screenshot, AppPreview's if it's a preview. That runtime "find the real type's version" behavior is called dynamic dispatch, and it's what makes polymorphism actually work. The static type (Asset) decides what you're allowed to call; the dynamic type (the real subclass) decides which implementation runs.
When to call super
super.validationWarnings() appears on the first line of every override above, and that's deliberate. Watch what happens if you defer it. Suppose Screenshot cached its warnings and called super last:
override func validationWarnings() -> [String] {
var warnings: [String] = []
if deviceClass == "6.7-inch" && (pixelWidth, pixelHeight) != (1290, 2796) {
warnings.append("6.7-inch screenshots must be 1290×2796.")
}
return warnings // ⛔ bug: the parent's byte-size and dimension checks vanished
}By returning before calling super, this override silently drops every warning ImageAsset and Asset would have produced. The general rule: call super first when you're adding to a parent's work, so the parent's behavior is in place before you layer yours on top and you never have to reason about the order things happened in. (Occasionally you genuinely want to run after the parent -- for instance, to undo or post-process its result -- but make that a deliberate choice, not an accident.)
Here's the correct Screenshot override, super-first:
class Screenshot: ImageAsset {
// ...existing stored properties and init...
override func validationWarnings() -> [String] {
var warnings = super.validationWarnings()
if deviceClass == "6.7-inch" && (pixelWidth, pixelHeight) != (1290, 2796) {
warnings.append("6.7-inch screenshots must be 1290×2796.")
}
return warnings
}
}Overriding properties, too
It isn't only methods. A subclass can override an inherited property with a computed one, and super works there as well. Give every asset a human-readable kind and let each level extend it:
class Asset {
// ...
var kind: String { "Asset" }
}
class ImageAsset: Asset {
override var kind: String { "\(super.kind) · Image" }
}
class Screenshot: ImageAsset {
override var kind: String { "\(super.kind) · \(deviceClass)" }
}
Screenshot(filename: "home.png", byteSizeKB: 820,
pixelWidth: 1290, pixelHeight: 2796, deviceClass: "6.7-inch").kind
// "Asset · Image · 6.7-inch"Each kind calls up the chain, so the string assembles itself one level at a time. You can override a read-write property the same way, supplying a get and set that delegate to super -- just remember you're overriding access, not adding storage; the stored value still lives where it was declared.
final: Closing the Door
Sometimes a class is meant to be a leaf -- nothing should subclass it, and no one should override its methods. An app icon, for instance, has exactly one set of rules and no meaningful specialization:
final class AppIcon: ImageAsset {
// 1024×1024, no alpha, no transparency. Nothing to specialize.
}final on the class forbids any subclass of AppIcon. You can also mark an individual method or property final to allow subclassing in general but lock one member against override:
class Asset {
final func checksum() -> String { /* ... */ "abc123" }
}There are two reasons to reach for final. The first is intent: it tells the next person (often future-you) "this wasn't designed to be subclassed," and the compiler enforces the message. The second is performance: a final member can't be overridden, so the compiler can skip dynamic dispatch and call it directly -- and sometimes inline it entirely. Marking classes final by default, and only un-finalizing when you deliberately design for inheritance, is a habit worth building.
Casting: Asking "What Are You Really?"
Polymorphism hands you generality, but sometimes you need to go the other way -- you're holding an Asset and you want the screenshot-specific data back. The variable's static type won't let you:
let asset: Asset = Screenshot(filename: "home.png", byteSizeKB: 820,
pixelWidth: 1290, pixelHeight: 2796,
deviceClass: "6.7-inch")
asset.deviceClass // ⛔ value of type 'Asset' has no member 'deviceClass'Even though the box contains a Screenshot, the compiler only trusts what the declared type promises. To treat it as the more specific type, you cast with one of three operators:
as-- a guaranteed cast, checked at compile time. Use it to go up the hierarchy (aScreenshotas anAsset), which can never fail.as?-- an optional downcast. It attempts to treat the value as a subtype and returnsnilif the value isn't actually that type. This is the safe one you'll reach for constantly.as!-- a forced downcast. It crashes your app if the value isn't that type. Use it only when a failure would mean a programming mistake you'd want to catch loudly, never on data you're unsure about.
as? shines in an if let or guard, exactly like optional binding:
for asset in assets {
if let shot = asset as? Screenshot {
print("Screenshot for \(shot.deviceClass)")
} else if let preview = asset as? AppPreview {
print("\(preview.durationSeconds)s preview")
} else {
print("Some other asset: \(asset.filename)")
}
}Inside the if let, shot is a full Screenshot and deviceClass is reachable again. The upcast direction needs no check:
let backUp: Asset = shot as Asset // always valid -- a Screenshot is an Assetas! is a loaded gunasset as! Screenshot reads almost identically to asset as? Screenshot, but the consequences are opposite: the ? version gives you nil to handle, the ! version terminates the process. Reach for as? and handle the nil. Save as! for the rare case where the wrong type is genuinely impossible and you'd rather crash than continue with bad assumptions.
A Subtlety: Static Type Drives Overload Resolution
Here's a corner of the language that surprises people, and casting is how you trigger it. Method overrides on a class are resolved dynamically -- the real type wins. But free-function overloads are resolved statically -- the declared type wins. Put two overloads side by side:
func uploadEndpoint(for asset: Asset) -> String { "/v1/assets" }
func uploadEndpoint(for shot: Screenshot) -> String { "/v1/screenshots" }
let shot = Screenshot(filename: "home.png", byteSizeKB: 820,
pixelWidth: 1290, pixelHeight: 2796, deviceClass: "6.7-inch")
uploadEndpoint(for: shot) // "/v1/screenshots" -- picks the Screenshot overload
uploadEndpoint(for: shot as Asset) // "/v1/assets" -- the cast changes the choiceBoth calls pass the same object. The only difference is the static type the compiler sees at the call site, and that's what it uses to choose between overloads -- at compile time, before the program ever runs. Cast to Asset and you get the general overload; leave it as Screenshot and you get the specific one.
Keep the distinction straight: calling a method on an instance uses the dynamic (real) type, so overrides win. Choosing among overloaded functions uses the static (declared) type, so casts matter. Mixing these two up is the source of a lot of "but I overrode that, why isn't it being called?" confusion.
Initialization Across a Hierarchy
Back to those super.init calls. Swift has one ironclad rule you already know -- every stored property must have a value before the instance is usable -- and inheritance makes satisfying it more involved, because now properties live at several levels of the chain. Swift's solution is two-phase initialization.
Phase one walks up the hierarchy, bottom to top, giving every stored property an initial value. A subclass initializer sets its own properties first, then calls super.init, which sets the next level up, and so on until the base class is fully initialized.
Phase two runs after the base class is initialized. Only now -- with every property in the entire hierarchy set -- is the instance fully formed, and only now are you allowed to call methods, read computed properties, or otherwise use self.
This is why Screenshot.init is shaped the way it is:
init(filename: String, byteSizeKB: Int,
pixelWidth: Int, pixelHeight: Int, deviceClass: String) {
// Phase 1: set my own property *before* handing off upward.
self.deviceClass = deviceClass
// Still phase 1: initialize ImageAsset, which initializes Asset.
super.init(filename: filename, byteSizeKB: byteSizeKB,
pixelWidth: pixelWidth, pixelHeight: pixelHeight)
// Phase 2: the whole instance is now valid -- self is usable.
// e.g. you could call validationWarnings() here, but not before super.init.
}Two rules fall straight out of this, and the compiler enforces both:
- Set your own stored properties before calling
super.init. Reverse the first two lines and you'll get an error -- you'd be handing control up the chain whiledeviceClassis still uninitialized. - Call
super.initbefore usingself. Try to callvalidationWarnings()beforesuper.initreturns and the compiler stops you, becausepixelWidth,filename, and the rest aren't set yet. Methods could read garbage.
If you forget super.init entirely, the build fails with "super.init isn't called before returning from initializer" -- the superclass never got the chance to set its properties, so the instance would be half-formed.
Asset has no superclass, so its initializer has nothing to call up to -- it just assigns its two properties and it's done. Two-phase initialization only gets interesting once there's a chain to walk. The deeper the hierarchy, the more init is really a relay race: each level sets its own baton, then passes upward.
Designated, Convenience, and Required Initializers
Every initializer you've written so far is a designated initializer -- the "real" kind that fully initializes a class and calls its superclass's designated initializer. Most classes have one or two. But you'll often want a shorthand for a common case, and that's a convenience initializer:
class Asset {
let filename: String
var byteSizeKB: Int
init(filename: String, byteSizeKB: Int) { // designated
self.filename = filename
self.byteSizeKB = byteSizeKB
}
convenience init(placeholderNamed filename: String) { // convenience
self.init(filename: filename, byteSizeKB: 0)
}
}A convenience initializer can't set stored properties itself or call super.init. Instead it must funnel through another initializer in the same class -- here, self.init(filename:byteSizeKB:). It's a friendly front door that ultimately delegates to the designated initializer doing the real work. Three rules capture the whole system:
- A designated initializer calls a designated initializer of its direct superclass (up the chain).
- A convenience initializer calls another initializer in the same class (across).
- A convenience initializer must, eventually, reach a designated initializer.
The third keyword is required. Mark a designated initializer required and every subclass is forced to provide it:
class Asset {
required init(filename: String, byteSizeKB: Int) {
self.filename = filename
self.byteSizeKB = byteSizeKB
}
}Now ImageAsset can't compile until it supplies that initializer too. Note the keyword: subclasses write required (not override), which both satisfies the parent's demand and propagates the requirement to their subclasses:
class ImageAsset: Asset {
var pixelWidth: Int
var pixelHeight: Int
required init(filename: String, byteSizeKB: Int) {
self.pixelWidth = 0
self.pixelHeight = 0
super.init(filename: filename, byteSizeKB: byteSizeKB)
}
}Why force this? Because some code wants to construct any asset subtype through a common initializer -- a loader that builds whatever asset matches a filename, say -- and required is the guarantee that the initializer will be there no matter how deep the hierarchy goes. Without it, a future subclass could quietly omit the initializer and break that code.
The Class Lifecycle: Automatic Reference Counting
Classes live on the heap, and the heap doesn't clean itself up. So how does Swift know when an instance is safe to free? It counts references. Every time you store an instance in a variable, constant, or property, its reference count goes up by one; every time such a reference goes away, the count goes down. When the count hits zero -- nobody is holding the instance anymore -- Swift frees it.
You don't write any of this counting yourself. Automatic Reference Counting (ARC) inserts the increments and decrements at compile time. Walk through it with a deinit (defined next) printing as proof:
var primary: Asset? = Asset(filename: "a.png", byteSizeKB: 10)
// count = 1 (primary)
var alias: Asset? = primary
// count = 2 (primary, alias)
var batch: [Asset?] = [primary, alias, primary]
// count = 5 (the two names + three array slots)
alias = nil
// count = 4
batch = []
// count = 1 (just primary)
primary = nil
// count = 0 → ARC frees the instanceOnly one Asset instance ever existed here; everything else is a reference to it. The instance survives exactly as long as something points at it, and not a moment longer.
Languages like Java and C# use a garbage collector that periodically scans memory for unreachable objects. ARC is different: the bookkeeping is baked into your compiled code, deallocation happens immediately when the count reaches zero, and there's no background sweeper pausing your app. The trade-off is the one this section is about -- ARC can't detect cycles on its own, so you have to.
deinit: A Last Word Before Cleanup
A deinitializer runs at the instant an instance's reference count hits zero, just before Swift reclaims the memory. It's the mirror image of init:
class Asset {
// ...existing members...
deinit {
print("🗑️ \(filename) freed")
}
}Unlike init, deinit takes no parameters, you never call it yourself, and you don't call super -- Swift runs every deinitializer in the chain automatically, child to parent. You also can't have more than one per class. Use it to release things ARC can't manage for you: close a file handle, cancel a timer, remove an observer, flush a cache to disk. For plain in-memory objects you often don't need one at all -- but it's the perfect probe for seeing ARC work, which is exactly how we'll catch the next bug.
Retain Cycles and weak
ARC frees an instance when its count reaches zero. So what happens when two instances each hold a strong reference to the other? Neither count can ever reach zero, even after you let go of both. That's a retain cycle, and it's the classic way to leak memory in Swift.
Model it with a bundle that owns its assets, where each asset also points back at the bundle it belongs to:
class AssetBundle {
let appName: String
var assets: [Asset] = []
init(appName: String) { self.appName = appName }
deinit { print("🗑️ bundle \(appName) freed") }
}
class Asset {
// ...existing members...
var bundle: AssetBundle? // strong reference back to the parent
}Wire a bundle and an asset to each other, then drop both of your references:
var bundle: AssetBundle? = AssetBundle(appName: "Shipper")
var shot: Asset? = Asset(filename: "home.png", byteSizeKB: 820)
bundle?.assets.append(shot!) // bundle → asset (strong)
shot?.bundle = bundle // asset → bundle (strong)
bundle = nil
shot = nil
// Nothing prints. Neither deinit runs. Both objects leak.Setting your variables to nil removed your references, but the two objects are still propping each other up: the bundle's assets array retains the screenshot, and the screenshot's bundle property retains the bundle. Each keeps the other's count at one. They're unreachable from your code yet permanently resident in memory.
The fix is to tell ARC that one side of the relationship doesn't count. Mark the back-reference weak:
class Asset {
// ...existing members...
weak var bundle: AssetBundle? // does not retain the parent
}A weak reference doesn't increment the count. The bundle still strongly owns its assets, but each asset only points weakly back -- so when you drop your bundle variable, the bundle's count really does reach zero, it deallocates, and that in turn releases its assets. Run the same script now and both deinit lines print.
Two requirements come with weak:
- It must be a
var, never alet-- because ARC needs to be able to change it. - It must be an optional, because when the referenced object is freed, ARC automatically sets the weak reference to
nil. A weak reference can blink out from under you at any time; the optionality is the language making you acknowledge that.
The rule of thumb for back-references and parent pointers: the owner holds its children strongly; the children point back weakly. Ownership flows one direction, and the weak link breaks the cycle.
weak's sharper sibling: unownedThere's a second non-retaining reference, unowned, for when the back-reference is guaranteed to outlive nothing -- the child can never exist without its parent, so the reference should never be nil. unowned isn't optional and doesn't auto-nil, which makes it tidier to use -- but accessing one after its target is gone is a crash, not a nil. Prefer weak unless you can prove the referenced object always outlives the reference; reach for unowned only when that guarantee genuinely holds.
When (and When Not) to Subclass
You now have the full toolkit, which makes this the right moment for the warning: inheritance is frequently the wrong tool, and Swift quietly pushes you toward alternatives.
Reach for a subclass when:
- There's a true
is-arelationship and you need the runtime substitution polymorphism gives you -- a heterogeneous[Asset]you process uniformly. - You're extending a class you don't own. If
Assetcame from a framework, subclassing might be the only way to specialize it. - Several variants genuinely share stored state and setup, and a common base class removes real duplication.
Lean away from subclassing when:
- You only want to share behavior, not identity. A protocol with a default implementation gives many unrelated types the same capability without forcing them into one family tree. Swift's standard library is built this way --
Equatable,Collection,Codableare protocols, not base classes. - A property would do. If a "localized screenshot" is just a screenshot with a
locale: String, add the property; don't mint aLocalizedScreenshotsubclass. - You feel the hierarchy getting deep. Each level couples a subclass tightly to its parent's implementation details, and a change near the root can ripple unpredictably down -- the "fragile base class" problem. Single inheritance also means you eventually hit a wall: a type can only be in one chain, and real-world categories overlap.
The honest summary: class hierarchies model what an object is; protocols model what an object can do. When you find yourself wanting to share what objects do across types that aren't really the same kind of thing, that's the signal to stop subclassing and reach for a protocol -- which is exactly where the next chapters head.
Challenges
Before moving on, work through these. Build the Asset / ImageAsset / Screenshot types in a playground first so you have something concrete to poke at.
Challenge 1: initialization order
Add print("init \(Self.self) — before super") and print("init \(Self.self) — after super") around the super.init call in each of Asset, ImageAsset, and Screenshot. Construct one Screenshot and predict the order of the six lines before you run it. Why does the "before super" set print top-down from Screenshot while the "after super" set prints bottom-up from Asset?
Challenge 2: deinitialization order
Give each of the three classes a deinit that prints its type name. Create a Screenshot inside a do { } block so it deallocates the moment the block ends. In what order do the three deinitializers fire, and how does that compare to the initialization order from Challenge 1?
Challenge 3: find and break the cycle
Here's a pair of classes with a leak:
class UploadTask {
let id: String
var asset: Asset?
init(id: String) { self.id = id }
deinit { print("🗑️ task \(id) freed") }
}
// Asset gains: var currentUpload: UploadTask?Create an Asset and an UploadTask, point each at the other, then set both of your variables to nil. Confirm neither deinit runs. Then fix it with a single keyword. Which property should carry it, and how did you decide which side "owns" the other?
Challenge 4: subclass, property, or protocol?
For each new requirement, decide whether you'd add a subclass, add a property to an existing class, or introduce a protocol -- and defend it in one sentence:
- App previews and screenshots both need to report an
aspectRatio, but icons don't. - You want to support a "dark mode variant" of any screenshot.
- A new
Watermarkablecapability should apply to images and to PDF exports, which aren't assets at all.
Key Points
- Inheritance lets a class build on another with the
class Sub: Supersyntax. A subclass gets everything the superclass has and adds its own members. Swift allows single inheritance only. - Polymorphism means a base-typed variable can hold any subclass at runtime. Code written against the base type works for every specialization, including future ones.
overrideis mandatory and checked both ways -- you can't override by accident or claim to override something that isn't there.supercalls the parent's implementation; for methods that extend a parent, callsuperfirst.- Method calls use dynamic dispatch (the real type's override wins); overloaded free functions are resolved statically (the declared type wins), which is why casting with
ascan change which overload runs. finalforbids subclassing (on a class) or overriding (on a member), documenting intent and enabling faster dispatch.- Cast down the hierarchy with
as?(safe, returnsnil) oras!(crashes on failure); cast up withas(always safe). Default toas?. - Two-phase initialization requires a subclass to set its own stored properties before calling
super.init, and to finishsuper.initbefore usingself. - Initializers come in three flavors: designated (does the real work, calls up), convenience (delegates across to the same class), and required (forced onto every subclass).
- ARC frees an instance the instant its reference count hits zero;
deinitruns just before that. ARC can't see retain cycles -- two objects strongly referencing each other -- so break them by making one sideweak(a non-retaining, always-optionalvar). - Subclass for genuine
is-arelationships and framework extension; prefer protocols when you only want to share behavior. Hierarchies model what something is; protocols model what it can do.
Speaking of which -- the next stretch of the language is all about that "what it can do" side. Enums give you precise, finite sets of cases with their own data and methods. Protocols let unrelated types agree on a shared contract without sharing a base class. And generics let you write one piece of code that works across many types at once. Together they're how Swift code stays flexible without leaning on deep inheritance -- the tools this chapter kept pointing you toward.
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