A struct's properties hold its data. Its methods decide what it can do. You met methods briefly in Chapter 13 -- a function defined inside a struct -- but there's a lot of nuance once you start using them in real apps: the implicit self, custom initializers, mutating methods, type-level methods, and the extension keyword that lets you add behavior to types you don't own.
This chapter walks through all of that.
Method Refresher
You've been calling methods all along. When you write myArray.append(7) or myString.uppercased(), you're calling a method on an instance:
var apps = ["Tally", "Notes"]
apps.append("Shipper")
apps // ["Tally", "Notes", "Shipper"]A method is just a function -- but defined inside a type. You access it with dot syntax on an instance.
Method vs Computed Property
In Chapter 14 you saw computed properties: blocks of code that look like properties but run on every read. That overlaps with methods. So when should something be a method, and when a computed property?
Two simple rules:
- If you want both read and write, use a computed property with a
getandset. Methods can't be assigned to. - If the work is cheap and feels like asking the value for a fact about itself, use a computed property. If the work is expensive, hits a network or disk, or feels like asking the value to do something, use a method.
shoppingCart.itemCount -- a property. Cheap, returns a fact.
shoppingCart.checkout() -- a method. Triggers work, may fail, side-effecting.
When in doubt, pick whichever reads better at the call site.
The Implicit self
Imagine a shopping cart for in-app purchases. Each item has a name and a price; the cart holds a list of items.
struct Item {
let name: String
let priceCents: Int
}
struct Cart {
var items: [Item] = []
func subtotalCents() -> Int {
var total = 0
for item in items {
total += item.priceCents
}
return total
}
}Inside subtotalCents(), the method refers to items directly -- not cart.items, not self.items, just items. Swift secretly passes the instance into every method as a hidden parameter named self. When you say items, the compiler reads it as self.items.
You can write self.items explicitly. Most of the time it's noise:
func subtotalCents() -> Int {
self.items.reduce(0) { $0 + $1.priceCents } // works, but `items` is enough
}The one place self. is essential is when a parameter has the same name as a property. We'll see that in a moment.
These are different. self (lowercase) is the instance -- this particular cart. Self (capital S) is the type -- the Cart blueprint itself. You use Self to access type-level (static) members from inside an instance method. Same word, different worlds.
Initializers, Revisited
You met initializers in Chapter 13: every struct gets a free memberwise initializer that takes one argument per stored property. With the Cart above, that's Cart(items: [Item]). Default values shrink the call site -- since items defaults to [], Cart() works too.
Sometimes you need an initializer that transforms its inputs rather than just storing them. Maybe you want to seed a cart from a saved CSV string, or from a single bundle ID. Write a custom initializer with the init keyword:
struct Cart {
var items: [Item] = []
init(items: [Item] = []) {
self.items = items
}
init(singleProduct name: String, priceCents: Int) {
self.items = [Item(name: name, priceCents: priceCents)]
}
}Notice:
- An initializer uses the keyword
init, notfunc, and has no return type. - Inside, you must assign every stored property before the initializer returns. Swift checks this at compile time.
self.items = itemsis the canonical use ofself.to disambiguate -- without it,items = itemswould assign the parameter to itself.
Now you can build a cart either way:
let empty = Cart()
let oneShot = Cart(singleProduct: "Pro Upgrade", priceCents: 999)The "custom init swallows the memberwise init" gotcha
There's a catch. The moment you write a custom init inside the struct body, Swift stops generating the memberwise one. It assumes you have a reason to take control. So this fails:
struct Cart {
var items: [Item] = []
init(singleProduct name: String, priceCents: Int) {
self.items = [Item(name: name, priceCents: priceCents)]
}
}
let c = Cart(items: []) // ⛔ no longer existsThe clean fix: put your custom initializer in an extension instead of in the main body. Extensions don't disable the synthesized memberwise init:
struct Cart {
var items: [Item] = []
}
extension Cart {
init(singleProduct name: String, priceCents: Int) {
self.items = [Item(name: name, priceCents: priceCents)]
}
}
let a = Cart(items: []) // ✅ memberwise, still here
let b = Cart(singleProduct: "Pro", priceCents: 999) // ✅ custom, also hereThis idiom is worth memorizing. Whenever you find yourself adding a convenience initializer to a struct, default to writing it in an extension.
Mutating Methods
Structs are value types. By default, methods on a struct cannot change the struct's properties -- because doing so would mean mutating the copy you'd just been handed, which is almost always a bug. Swift makes you opt in.
To write a method that changes a property, mark it mutating:
extension Cart {
mutating func add(_ item: Item) {
items.append(item)
}
mutating func clear() {
items.removeAll()
}
}
var cart = Cart()
cart.add(Item(name: "Pro Upgrade", priceCents: 999))
cart.add(Item(name: "Credit Pack", priceCents: 499))
cart.items.count // 2Two rules follow from the mutating annotation:
- A
mutatingmethod can only be called on avarinstance.let cart = Cart(); cart.add(...)won't compile. - The compiler now treats the hidden
selfasinout-- writes inside the method propagate back to the caller's variable.
If you don't need to mutate, don't mark it. Most methods on most structs are not mutating.
Mini-exercise
Add a mutating func remove(at index: Int) to Cart that removes the item at the given index. (Don't worry about index validation for this exercise -- assume the caller passes a valid index.)
Type Methods: Functions on the Type Itself
Most methods belong to instances -- cart.add(item) only makes sense if you have a cart. But sometimes the work doesn't need any particular instance. It belongs to the type.
Mark these with static:
struct PriceFormatter {
static func usd(_ cents: Int) -> String {
let dollars = Double(cents) / 100
return String(format: "$%.2f", dollars)
}
static func usdOrFree(_ cents: Int) -> String {
cents == 0 ? "Free" : usd(cents)
}
}
PriceFormatter.usd(999) // "$9.99"
PriceFormatter.usdOrFree(0) // "Free"
PriceFormatter.usdOrFree(499) // "$4.99"PriceFormatter has no instance properties and you never write PriceFormatter(). It exists purely as a namespace -- a folder that groups related functions under one name. This pattern is everywhere in real Swift code: Date.now, URL.cachesDirectory, Color.accentColor. They're all static members.
Inside one static method, you can call another with no prefix (notice how usdOrFree calls usd directly). From an instance method, you'd refer to type members via Self.:
extension Cart {
func formattedTotal() -> String {
Self.priceFormatter(subtotalCents()) // assumes a static `priceFormatter`
}
}Mini-exercise
Add a static func mbDisplay(_ bytes: Int) -> String to PriceFormatter (or to a new ByteFormatter namespace) that returns "4.2 MB" for 4_400_000. Use Double(bytes) / 1_000_000 and String(format: "%.1f MB", ...).
Extensions: Adding to Types You Don't Own
You've already used extension to keep the memberwise init alive. But extensions can do far more: they can add methods and computed properties to any type, including the ones Swift's standard library ships -- String, Int, Array, even Bool.
Say you want a clean way to truncate long strings for previews. Rather than write a free function truncate(string, length), add a method right on String:
extension String {
func truncated(to limit: Int, ellipsis: String = "…") -> String {
if count <= limit { return self }
return prefix(limit) + ellipsis
}
}
"Pattern matching with Swift regex literals".truncated(to: 20)
// "Pattern matching wit…"Now every String in your codebase has .truncated(to:). The original String type didn't change -- you've added behavior the compiler attaches at call sites.
You can extend your own structs the same way to group related behavior:
extension Cart {
var itemCount: Int { items.count }
var isEmpty: Bool { items.isEmpty }
}There's only one hard limit: an extension can't add stored properties, because that would change the type's memory layout. Computed properties, methods, initializers, and protocol conformances are all fair game.
Adding methods to String or Int is genuinely useful inside your own project, but it's a global change in that module -- anyone reading code that uses "foo".truncated(to: 10) has to know it's from your extension. Save these for things that read better as instance methods than as free functions, and resist the temptation to add five-character convenience accessors that obscure where the behavior comes from.
Challenges
Challenge 1: cart receipt
Add an instance method receipt() -> String to Cart that returns a multi-line string formatted like a printed sales slip. Example output for a 2-item cart:
Pro Upgrade .......... $9.99
Credit Pack .......... $4.99
─────────────────────────────
Total ............... $14.98Hints: build a single String and append to it inside a for loop; use PriceFormatter.usd(_:) from earlier for the prices; you don't have to match the dot leaders exactly -- just produce a readable multi-line block.
Challenge 2: a ByteFormatter namespace
Build a new ByteFormatter struct with three static methods that return human-readable size strings:
ByteFormatter.kb(_ bytes: Int)→"1.5 KB"for1500ByteFormatter.mb(_ bytes: Int)→"3.2 MB"for3_200_000ByteFormatter.auto(_ bytes: Int)→ returnskb(...)ifbytes < 1_000_000, otherwisemb(...)
Use String(format: "%.1f KB", ...) style formatting (import Foundation if you need it).
Challenge 3: extend String with a bundle-ID validator
The App Store wants bundle IDs in reverse-DNS format -- at least one dot, no spaces, all lowercase alphanumerics plus dots and hyphens. Examples:
"com.example.shipper"-- valid"com.example"-- valid"example"-- no dot, invalid"Com.Example"-- uppercase, invalid"com.example app"-- space, invalid
Write an extension String that adds a computed property isValidBundleID: Bool. Test it on all five examples above. You can implement the check with String methods (contains, allSatisfy, etc.) or with a regex from Chapter 12 -- your call.
Key Points
- A method is a function defined inside a type. Call it with dot syntax on an instance.
- Inside a method, the keyword
selfrefers to the current instance. You can usually leaveself.off; only write it when you need to disambiguate from a parameter of the same name. - Use a method for actions or expensive computations; use a computed property for cheap values that feel like an attribute. When in doubt, pick whichever reads naturally at the call site.
- Initializers use the
initkeyword and must assign every stored property before returning. Writing a custominitin the struct body disables the synthesized memberwise initializer -- put custom inits in anextensionto keep both. mutatingmarks a method that changes the struct's properties. Required because structs are value types; the compiler enforces it. Mutating methods can only be called onvarinstances.- Type methods with
staticbelong to the type itself, not to any instance. They're perfect for namespacing related utility functions --PriceFormatter.usd(...),ByteFormatter.kb(...), etc. - Extensions let you add methods, computed properties, initializers, and protocol conformances to any type -- including standard library types you don't own. You cannot add stored properties via extensions.
You now know enough Swift to model a real domain with confidence: pick the right type, give it properties, give it behavior, and grow it through extensions as the codebase evolves. In Chapter 16: Classes, you'll meet the other side of the named-type coin -- reference types, shared mutation, object identity, and when to reach for a class instead of the struct you've been using until now.
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