Hyper legible Swift
A practical guide to sentence‑style APIs, parameter labels, and domain language, so your Swift reads like plain English while precisely capturing behavior and requirements.
Introduction
While my Swift experience is short, it's been very exciting. Swift is unusually expressive - with the right naming and parameter labels, your code can read almost like normal English while still being exact about behavior and product requirements. This article distills the style I've found myself gravitating towards: intention revealing Swift with sentence‑style call sites and domain‑specific naming.
The goal is simple: code that a teammate or an AI partner can read and immediately understand what happens, and why.
What "reads like English" means in practice
The style has five core principles:
- Intent over mechanics. Favor names that state what you're doing, not how you're doing it.
- Call‑site clarity. Design method/func names + argument labels to read like a sentence, following Swift's guidance to "strive for fluent usage".
- Ubiquitous domain language. Use the vocabulary of your product, consistently.
- Step‑down narrative. Top‑level orchestrates; helpers hold details.
- Omit needless words. Drop redundancies the type system or scope already carry, adhering to Swift's principle of "clarity at the point of use" and "omit needless words".
Examples: Before and After
Here are three real examples from a project I'm currently working on (taken from a Calendar style timeline view):
1. Tap handling
The tap handling function has 3 requirements:
- To unfocus an input field when the user taps on a timeline
- To toggle an event "edit mode" on if tapping on an event
- To get rid of "edit mode" state if tapping on empty space
Before
1private func handleTap(at location: CGPoint, _: CGFloat) {
2 if titleFocused {
3 titleFocused = false
4 return
5 }
6 if let item = hitTester.findEvent(at: location, in: currentEventLayoutAttributes) {
7 setEventToolbar(for: item)
8 editingEventId = item.id
9 } else if dragState == nil {
10 editingEventId = nil
11 }
12}
After
1private func handleTap(at location: CGPoint, _: CGFloat) {
2 unfocusTitleIfEditing()
3 if let item = event(at: location) {
4 toggleEditMode(.on, for: item)
5 } else if !isDragActive {
6 editingEventId = nil
7 }
8}
Why this reads better: Verbs tell intent (unfocus, toggleEditMode), and event(at:)
forms a readable phrase that clearly indicates we're finding an event. We drop redundant suffixes where the type already says "event".
2. Drag handling
I handle drags on my timeline for
- Event handles (start time/end time)
- Event move
- New event creation
Before
1private func handleDragChanged(to location: CGPoint, _: CGFloat) {
2 movedFingerToDate = yToDate(location.y)
3
4 if isCreatingEvent, let current = movedFingerToDate { /* ... */ }
5
6 if let editingEventId,
7 let event = app.schedule.itemById[editingEventId],
8 let start = touchedDownAtDate,
9 let current = movedFingerToDate,
10 let state = dragState {
11 /* compute offsets, update state, manage previews */
12 }
13}
After
1private func handleDragChanged(to location: CGPoint, _: CGFloat) {
2 date = yToDate(location.y)
3
4 switch currentlyDragging {
5 case .toCreateEvent:
6 updateDragOffset(for: .eventCreation, at: date)
7 case .toMoveEvent:
8 updateDragOffset(for: .eventDrag, at: date)
9 case .toUpdateEventHandle:
10 updateDragOffset(for: .eventHandles, at: date)
11 }
12}
Why this reads better: The function becomes a traffic director. Each branch states a high‑level intent and delegates detail to focused helpers.
3. Transformation pipelines
Before
1private func arrangeOverlappingEvents(_ events: [EventLayoutAttributes]) -> [EventLayoutAttributes] {
2 guard !events.isEmpty else { return [] }
3
4 let eventsByStartTime = sortByStartTime(events)
5 let overlappingGroups = groupOverlappingEvents(eventsByStartTime)
6
7 return overlappingGroups.flatMap { group in
8 layoutEventsInGroup(group)
9 }
10}
After
1private func arrangeOverlappingEvents(_ events: [EventLayoutAttributes]) -> [EventLayoutAttributes] {
2 guard !events.isEmpty else { return [] }
3
4 let ordered = sort(events)
5 let groups = groupOverlapping(events: ordered)
6 return groups.flatMap { layout(in: $0) }
7}
Why this reads better: The transformation becomes a linear story: sort → group → layout. Redundant "events" references are removed where types already carry that meaning.
The secret sauce - parameter labels ❤️
Swift's external parameter labels are what make English‑like call sites possible, following the "Argument Labels" guidance. Swift's approach to verb + preposition phrasing mirrors natural language, as shown in the canonical example move(from:to:)
where labels carry the prepositions.
Rules in one glance
- Functions: First parameter has no external label by default; subsequent parameters use their internal name
- Methods: Same default, but you can add labels to improve readability
- Initializers: All parameters have external labels by default; use
_
to suppress for value-preserving conversions
Based on Swift's Argument Labels section
Pitfalls
- Too many prepositions (
updateDragOffset(for:withCurrentPosition:)
) can read clunky; if a label is cargo, rename the parameter type/role to carry meaning and simplify labels (e.g.,updateDragOffset(for: .eventCreation, at:)
).
Use these tools to craft readable sentences:
Reads: "start event resize at location for event"
1// Existing API (kept): starts a resize interaction, returns whether it actually began.
2func startEventResize(at location: CGPoint, for event: Event) -> Bool { /* ... */ }
3
4// Usage: if the resize starts, reveal handles and capture the initial frame.
5let location = CGPoint(x: 120, y: 340)
6let didBegin = startEventResize(at: location, for: event)
7
8if didBegin {
9 ui.showResizeHandles(for: event)
10 interaction.captureInitialFrame(of: event)
11} else {
12 feedback.warn(.noResizeTargetAt(location))
13}
Reads: "toggle edit mode on/off for item"
1// Existing API (kept): explicitly set edit mode state for an item.
2func toggleEditMode(_ state: EditState, for item: Item) { /* ... */ }
3
4// Usage: enter edit mode to rename, then exit after saving.
5toggleEditMode(.on, for: item)
6editor.beginRenaming(item)
7editor.commitChanges(for: item)
8toggleEditMode(.off, for: item)
Reads: "move event from source slot to target slot on calendar resolving conflicts with policy"
1enum ConflictPolicy { case pushLater(maxMinutes: Int), decline, mergeNotes }
2struct MoveOutcome { let didMove: Bool; let notes: String }
3
4func moveEvent(from source: TimeSlot, to target: TimeSlot, on calendar: CalendarID, resolvingConflictsWith policy: ConflictPolicy) -> MoveOutcome {
5 /* ... */
6}
7
8// Usage: move the standup by 30 minutes, pushing conflicts by up to 15 minutes.
9let outcome = moveEvent(
10 from: .init(start: nineAM, end: nineThirty),
11 to: .init(start: nineThirty, end: tenAM),
12 on: .team("iOS"),
13 resolvingConflictsWith: .pushLater(maxMinutes: 15)
14)
15
16if outcome.didMove { log.info(outcome.notes) } else { toast.warn("Couldn’t move event") }
Reads: "schedule reminder at time for task repeating rule"
1enum NotificationChannel { case push, email, banner }
2enum RecurrenceRule { case none, weekly(weekday: Weekday, hour: Int, minute: Int) }
3typealias ReminderID = UUID
4
5func scheduleReminder(at time: Date, for task: Task, on channel: NotificationChannel, repeating rule: RecurrenceRule) throws -> ReminderID {
6 /* ... */
7}
8
9// Usage: schedule a weekly Monday reminder at 09:00 via push.
10let mondayNine = dateFactory.make(hour: 9, minute: 0, weekday: .monday)
11let id = try scheduleReminder(
12 at: mondayNine,
13 for: Task(title: "Call client"),
14 on: .push,
15 repeating: .weekly(weekday: .monday, hour: 9, minute: 0)
16)
17reminders.track(id, for: "Call client")
Reads: "export schedule as format to destination including fields since date"
1enum ExportFormat { case csv, json, ics }
2struct Destination { let folder: URL }
3struct ExportField: OptionSet { let rawValue: Int; static let attendees = ExportField(rawValue: 1<<0); static let notes = ExportField(rawValue: 1<<1) }
4
5func exportSchedule(as format: ExportFormat, to destination: Destination, including fields: ExportField, since date: Date) throws -> URL {
6 /* ... */
7}
8
9// Usage: export as CSV to the Downloads folder, including attendees and notes since the 1st.
10let fileURL = try exportSchedule(
11 as: .csv,
12 to: Destination(folder: FileManager.default.urls(for: .downloadsDirectory, in: .userDomainMask).first!),
13 including: [.attendees, .notes],
14 since: dateFactory.firstOfMonth()
15)
16share.present(fileURL)
Why This Pays Off
- Instant comprehension. Even without having looked at the code before, you will instantly understand what it's responsible for, and generally how.
- Self‑documentation. Call sites teach API semantics; comments become optional. Business logic, product requirements, and flow are made clear.
- LLM‑ready code. Clear intent + explicit requirements make AI completions and refactors safer. Anecdotally, I've found this style to be less token intensive too, which humans and LLMs appreciate alike.
As coding has increasingly become a conversation between humans and AI, clarity of requirements usually makes or breaks that interaction. The more you encode them into your code, the better and more effective LLM output will be.
How to make it work
Several key things make hyper-legible Swift work in practice. Start by establishing a consistent verb taxonomy - pick verbs like start/update/finish, show/hide, enable/disable and stick to them throughout your codebase. Avoid vague verbs entirely; replace handle/process/manage with outcome verbs like startEventResize, applyTheme, or retrySync that tell you exactly what happens. Design explicit boolean properties with names like isDraggingEvent, isCreatingEvent, or isDragInactive that read naturally in conditionals.
Follow the step-down rule where top-level orchestration reads like a checklist while you push implementation details into well-named helpers. Avoid leaky types by preferring role nouns like group or range over implementation details like array or dict. Most importantly, design the call site first - write the usage you want to read, then implement to match that vision. Finally, name for searchability by giving public symbols extra words when needed for discoverability.
Concrete checklist
- Name functions with verb + (prepositional) phrase to read like English
- Use
_
to omit the first external label when the verb + first argument reads clearly - Use
in:
,at:
,for:
,on:
,by:
,from:
,to:
to complete the sentence - Keep top-level functions to ~10–20 lines; push mechanics into helpers
- Remove redundant type words from names when the type conveys it
- Establish a verb taxonomy and reuse it
- Design pipelines for transformations: parse → validate → transform → render
- Prefer truthy booleans that read as assertions:
isEmpty
,isDragInactive
,hasSelection
. Follow Swift's guidance that "uses of Boolean methods and properties should read as assertions about the receiver". - Replace vague verbs (handle, process, manage) with outcome verbs
- Read call sites aloud; rename until they sound natural
When not to apply these patterns
- Don't omit labels when the parameter's meaning isn't obvious from the base name (e.g.,
wait(5)
vswait(forSeconds: 5)
). Swift's guidance emphasizes clarity over brevity. - Don't force grammatical continuity for initializers/factories; first args to
init
/make…
shouldn't "continue" the base name. Apple's examples show why:String(describing:)
notString(_:)
. - Prefer nonmutating/mutating naming pairs (
sorted
/sort
) over ad-hoc verbs. This follows Swift's mutating/nonmutating conventions.
Verb → Preposition cheat sheet
Common verbs and their typical preposition pairings:
Verb | Preposition you'll usually pair | Example |
---|---|---|
insert | at: | insert(_:at:) |
move | from: / to: | move(from:to:) |
distance | to: | distance(to:) |
toggle | flags → on: /off: or to: ; entities → for: | toggleNotifications(on:) |
update | with: / from: | update(with:) |
layout | in: | layout(in:) |
Each example links back to canonical patterns in the Swift API Guidelines
Initializer and factory examples
Here's how to handle label choices for initializers and factory methods:
1// Bad: forces grammatical continuity with base name
2let link = Link(to: destination)
3
4// Good: initializer labels describe roles; base name stands alone
5let link = Link(target: destination)
6
7// Good (conversion): omit first label for value-preserving conversions
8let big = Int64(smallInt) // init(_:), per guidelines
9
10// Factory methods: labels describe what you're making
11let button = UIButton.makeSystemButton(withTitle: "Save")
12let view = ContainerView.makeScrollable(content: childView)
The prompt
The best part about this style is that you can fire and forget a prompt for it on your favourite AI tool, and get consistently good results on both new and existing code. One shot refactors with this prompt practically never break anything for me, and yield very satisfying output. LLM's excel at English, and likewise seem to excel at their task when reading/writing English. Here's the prompt I use:
naming-and-structure.md
1---
2alwaysApply: true
3---
4
5We write intention‑revealing Swift that reads like English and encodes product requirements in names.
6
7## Principles
8- **Intent over mechanics.** Names state outcomes (what), not procedures (how).
9- **Sentence‑style call sites.** Verb + prepositions form readable phrases.
10- **Ubiquitous domain language.** Use the product's nouns/verbs consistently.
11- **Step‑down narrative.** Orchestrate at top, detail in helpers.
12- **Omit needless words.** Types and scope carry context.
13
14## Parameter Labels
15- Omit first external label (`_`) if verb + first arg reads clearly.
16- Use `in:`, `at:`, `for:`, `on:`, `by:`, `from:`, `to:` to complete the sentence.
17- Prefer concrete role nouns: `group`, `item`, `range`.
18
19## Verb Taxonomy
20- Lifecycle: `start`, `update`, `finish`, `cancel`
21- Visibility: `show`, `hide`, `reveal`, `dismiss`
22- State: `enable`, `disable`, `select`, `deselect`, `toggle`
23- Effects: `apply`, `commit`, `rollback`, `retry`, `refresh`
24- Safety: `ensure`, `validate`, `protect`, `require`
25
26## Structure
27- Keep orchestration funcs small; push details into well‑named helpers.
28- Favor linear pipelines for transforms: `sort → group → layout`.
29- Replace comments with named helpers; names should explain *why*.
30- Avoid `handle/process/manage` unless nothing else is truly clearer.
31
32## Examples
33
34**Before**
35```swift
36func handleResizeHandleTouch(location: CGPoint, event: Event) -> Bool { /* ... */ }
37```
38
39**After**
40```swift
41func startEventResize(at location: CGPoint, for event: Event) -> Bool { /* ... */ }
42```
43
44**Before**
45```swift
46func handleEventMoveTouch(location: CGPoint, event: Event) -> Bool { /* ... */ }
47```
48
49**After**
50```swift
51func startEventMove(at location: CGPoint, for event: Event) -> Bool { /* ... */ }
52```
53
54**Before**
55```swift
56func handleDeletionGesture(inLeftMargin locationX: CGFloat) { /* ... */ }
57```
58
59**After**
60```swift
61func previewDeleteEvent(at x: CGFloat) { /* ... */ }
62```
63
64**Pipeline**
65```swift
66let ordered = sort(events)
67let groups = groupOverlapping(events: ordered)
68let laidOut = groups.flatMap { layout(in: $0) }
69```
70
71## Renaming examples
72
73| Old (Vague) | New (Intent) |
74|-------------|--------------|
75| handleResizeHandleTouch | startEventResize |
76| handleEventMoveTouch | startEventMove |
77| handleDeletionGesture | previewDeleteEvent |
78| updateDragGestureOffset | updateDragOffset |
79| updateDeletionCloseness | updateDeletePreview |
80
81## Review Checklist
82- Does the call site read like a sentence? If not, relabel.
83- Can a new engineer infer the outcome from names alone?
84- Are there any redundant words that types/scopes already imply?
85- Are verbs reused consistently across the module?
86- Is orchestration short and linear? If not, extract helpers.
87
88## Anti‑Patterns
89- Buckets like handle/process/manage everywhere.
90- Over‑shortening that creates ambiguity (sort in global scope).
91- Comment walls explaining intent - encode it in names instead.
92
93## LLM Synergy
94- Keep behavior and requirements in names and labels so models can infer intent in limited context windows. Prefer small, composable helpers with narrow effects.
Conclusion
By encoding requirements directly into your APIs, you enable humans and AI to collaborate safely and effectively. Write the call site as an English sentence, then implement the function that makes that sentence true. The results are extremely satisfying - reading and writing Swift becomes second nature, and you're rarely stuck with the question of what on earth does this do?