Keentune

Swift curriculum

27 chapters
·
224 concepts
·
free
Everything the adaptive question bank can teach and test in Swift, from foundations through advanced practice. Work through it in order, or start practicing and let the questions find your level.
Start practicing Swift

New here? Read the Swift guide

A free 17-minute primer — the mental model, the mistakes beginners make, and what to practice first.

A. The basics: bindings, types and optionals

•
let binds once, and a let struct cannot have any property mutated.
•
An unannotated 42 is Int and 3.0 is Double.
•
Int and Double never mix; you write Double(i).
•
Grouping values ad hoc, naming elements, discarding with _.
•
Optional<Wrapped> is .some/.none; nil is absence, not a null pointer.
•
if let and guard let, and the shorthand if let x that rebinds the name.
•
! traps on nil instead of producing a fallback.
•
a ?? b short-circuits and evaluates b only when a is nil.
•
T! is still an Optional, checked on every access.

B. Operators and expressions

•
x = y yields no value, so if x = y cannot compile.
•
% takes the sign of the dividend, so -9 % 4 is -1.
•
Int.max + 1 traps at runtime rather than wrapping.
•
a...b includes b, a..<b excludes it, and one-sided ranges omit an end.
•
&& and || skip the right operand once the left decides.
•
Compared element by element, left to right, up to six elements.
•
?? binds looser than arithmetic, so mixed expressions need parentheses.

C. Strings and characters

•
One Character can be several Unicode scalars, like a flag emoji.
•
count is O(n); isEmpty is the cheap emptiness test.
•
You subscript with a String.Index from index(_:offsetBy:).
•
A precomposed é and a decomposed e plus accent compare equal.
•
unicodeScalars, utf8 and utf16 give three different counts of one string.
•
""" strips the closing delimiter's indentation from every line.
•
#"\n"# is two characters, and \#(x) restores interpolation.
•
A Substring keeps the whole original alive until you convert it.

D. Collections

•
A copy is independent, though the physical copy waits for a write.
•
There is no nil fallback; check indices or use first.
•
A missing key gives nil, and assigning nil removes the key.
•
counts[k, default: 0] += 1 inserts and updates in one step.
•
Set requires Hashable, and Hashable is synthesized only when every stored property conforms.
•
Iteration order may differ between runs of the same program.
•
One drops nils, the other flattens one level of nesting.
•
lazy defers element work so a chained map/filter builds no intermediate array.
•
An ArraySlice does not start at index 0.

E. Control flow and pattern matching

•
Every possible value needs a case or a default.
•
A case ends by itself; fallthrough is the explicit opt-in.
•
case let x where x > 0 binds and filters in one pattern.
•
One switch can match several values at once, and ranges as cases.
•
case "a", "b":, and every alternative must bind the same names.
•
Iterating a collection or range, and stepping by an increment.
•
They can produce a value in a let or a return.
•
Deferred blocks run on every exit, including a throw, last in first out.
•
if #available(iOS 17, *) gates newer OS API at runtime.
•
Labeling a loop or if lets break and continue name which one they act on, which is the only way out of an outer loop from inside an inner one.

F. Functions

•
The caller writes the label, the body uses the name.
•
A defaulted parameter may be skipped entirely by the caller.
•
The values arrive inside the body as an array.
•
The write-back happens on return, so inout is not a pointer.
•
Passing one variable to two inout parameters is rejected.
•
An inner function closes over enclosing locals and can be returned.
•
Never is uninhabited and marks a call that does not come back.
•
@discardableResult deliberately silences the unused-return warning.

G. Closures and capture

•
$0 and $1 work only when the types come from context.
•
The last closure moves outside the parentheses; later ones keep labels.
•
Copying a closure shares the same captured storage.
•
A captured var sees mutations made after the closure was created.
•
[x] copies the value at closure-creation time instead.
•
@escaping marks a closure stored or called after the function returns.
•
Required, because the capture keeps the instance alive.
•
[weak self] yields an optional; [unowned self] traps after dealloc.
•
@autoclosure wraps the argument expression so it evaluates only if used.

H. Enumerations

•
An enum case is useful with no raw value at all.
•
Int cases count up from zero, String cases default to their names.
•
init?(rawValue:) returns nil for an unrecognized raw value.
•
Each case may carry a different payload, extracted by pattern matching.
•
Recursion needs indirect so a case can box its own payload.
•
An enum may have methods and computed properties, never stored ones.
•
Synthesizes allCases, and cannot apply when a case has an associated value.
•
A mutating method may replace the whole value with self =.other.
•
@unknown default handles future library cases without losing the warning.

I. Value and reference semantics

•
Assigning a struct produces a value nothing else can change.
•
Two variables holding one class instance observe each other's mutations.
•
A value-type method that writes self must be marked mutating.
•
A let class reference still allows changing the object's vars.
•
=== asks "the same object", == asks "the same value".
•
Free for a struct when every stored property already conforms.
•
isKnownUniquelyReferenced is how a struct defers the copy until a write.
•
A class property makes the struct's copies share that object.

J. Properties

•
One occupies memory, the other runs a getter on every access.
•
The setter's implicit parameter, and the read-only shorthand.
•
Initialized on first access, must be var, and is not thread-safe.
•
willSet receives newValue, didSet receives oldValue.
•
Assignments inside init do not fire them.
•
Writing the property inside its own didSet does not re-trigger it.
•
static for a fixed one, class for a computed one a subclass can override.
•
A stored type property initializes on first use, exactly once.

K. Methods, subscripts and inheritance

•
A static func belongs to the type, not to any value.
•
Parameterized access declared with subscript(i: Int) -> T, read-only or read-write.
•
Structs and enums compose or conform instead.
•
Omitting override is an error, which prevents accidental shadowing.
•
final blocks overriding and lets the compiler skip dynamic dispatch.
•
You may add a setter to an inherited read-only property, never remove one.
•
class members can be overridden, static members cannot.
•
Class method calls are indirect unless devirtualized.

L. Initialization and deinitialization

•
No implicit defaults; the initializer must cover them all.
•
Unless you declare your initializer in an extension.
•
One initializes fully and calls up, the other delegates across.
•
Set your own properties, call super.init, only then use self.
•
A subclass inherits superclass initializers only under specific conditions.
•
required forces every subclass to supply the same initializer.
•
init? returns nil on failure; init! gives an implicitly unwrapped result that traps on use.
•
A property default preserves the memberwise initializer.
•
It runs when the last strong reference goes and cannot be called by hand.

M. Errors, throwing and optional chaining

•
Errors are ordinary values, most often an enum.
•
A throwing function cannot go where a non-throwing one is required.
•
Every throwing call is marked try, and the error is caught or propagated.
•
try? discards the error into nil; try! traps.
•
The function throws only when the closure it was handed throws.
•
throws(E) fixes the error type; plain throws means any Error.
•
catch let e as MyError, and the implicit error binding in a bare catch.
•
Propagation is an ordinary return path with no stack unwinding.
•
defer runs on the error path, which is how cleanup survives a throw.
•
Result<Success, Failure> stores a throw as a value, unwrapped with get().
•
The first nil ends the chain and the whole expression is optional.

N. Protocols

•
A { get } requirement may be satisfied by a settable property.
•
Needed so a struct can satisfy a method that changes it.
•
A non-final class must mark that initializer required.
•
Refining one protocol, and A & B as a single type.
•
Class-only conformance, which is what a weak reference needs.
•
Including retroactive conformance on a type you do not own.
•
A default implementation the conformer may override.
•
A member that is not a requirement is not dynamically dispatched.
•
where Element: Equatable adds members only for some conformers.
•
Array is Equatable exactly when its Element is.
•
Equatable, Hashable, Codable and CaseIterable write themselves, conditionally.

O. Generics

•
func f<T>(_ x: T) is one declaration the caller instantiates per call site.
•
The body is type-checked once, before any instantiation.
•
An unconstrained T supports almost nothing until you constrain it.
•
A protocol placeholder that each conformer fills in.
•
Usually deduced from the implementing member's signature.
•
Collection<Int> constrains the associated type inline.
•
Constraining associated types and relationships the angle brackets cannot express.
•
where C1.Element == C2.Element ties two parameters together.
•
A Box<Dog> is not a Box<any Animal>, and why that must be so; the standard-library collections are the compiler-blessed exception, so [Dog] DOES convert to [any Animal].
•
An AnySequence-style wrapper, and the dispatch it costs.
•
each T makes one function generic over an arbitrary number of types.

P. Opaque and existential types

•
The caller cannot name it, but it is a single fixed type.
•
One variable may hold different conforming types over time.
•
Type identity preserved versus erased, and the boxing the erasure costs.
•
With some P the callee chooses the type; with <T> the caller does.
•
Every return in an opaque-returning body must produce the same type.
•
Sugar for an unnamed generic parameter, not an existential.
•
Why such protocols need any and resist being used as a type.

Q. Automatic reference counting

•
The object is deallocated the instant the count reaches zero.
•
Deallocation is deterministic, and reference cycles are never collected.
•
Structs and enums are not reference counted.
•
Two objects that own each other are never freed.
•
It is zeroed automatically when the referent goes away.
•
It is not zeroed, so touching it after deallocation traps.
•
Pick by whether the reference can legally outlive the referent.
•
A stored closure capturing self keeps its own owner alive.

R. Memory safety and ownership

•
A write access to a variable may not overlap any other access to it.
•
The conflict the compiler rejects outright.
•
Locals are checked at compile time, class properties at runtime.
•
~Copyable forbids implicit duplication of a value.
•
consuming and borrowing parameters: whether the callee takes ownership or only reads.
•
An explicit transfer that ends the source binding's lifetime.
•
Value types with deterministic cleanup.
•
A withUnsafe… pointer is valid only inside its closure.

S. Concurrency: async/await, tasks and actors

•
async lets a function suspend without blocking its thread.
•
Other work runs and shared state can change across it.
•
Concurrency comes from creating tasks, not from the keyword.
•
Starts a child immediately and awaits it at the point of use.
•
A dynamic number of children, all awaited before the group returns.
•
A child task cannot outlive the scope that created it.
•
Task {} inherits context; Task.detached inherits nothing.
•
Cancellation sets a flag; the task decides where to stop.
•
At most one task runs an actor's isolated code at a time.
•
Reaching another actor's isolated member requires await.
•
An actor may interleave other work at every await, so invariants must hold there.
•
Opting a member out of isolation so it can be called synchronously.
•
@MainActor pins work to the main thread, and you can declare your own.
•
A type whose values are safe to hand across an isolation boundary.
•
for await, and AsyncStream for adapting a callback API.
•
Bridging a completion handler, with the resume-exactly-once rule.
•
The language mode that turns concurrency warnings into errors.

T. Macros

•
The expansion becomes source; nothing of the macro runs at runtime.
•
#stringify produces code in place, @Attached decorates a declaration.
•
Expression, declaration, member, peer, accessor and extension roles do different jobs.
•
Declared in your module, implemented in a separate compiler plugin.
•
An expansion may add code but never delete or rewrite what you wrote.
•
Names introduced by an expansion cannot collide with the caller's names.

U. Attributes: property wrappers, result builders and the rest

•
@propertyWrapper moves storage and access logic into a reusable type.
•
init(wrappedValue:), and passing extra arguments in the attribute.
•
$name exposes a second interface beside the wrapped value.
•
@resultBuilder rewrites a block of expressions into buildBlock calls.
•
buildOptional, buildEither and buildArray are what make if and for legal inside.
•
What it exposes to Objective-C, and the message dispatch it implies.
•
Cross-module inlining and layout promises that become ABI commitments.

V. Extensions and access control

•
Methods, computed properties and initializers only.
•
To change inherited class behavior you subclass instead.
•
open, public, package, internal, fileprivate and private, most to least visible.
•
An unmarked declaration is visible throughout its own module only.
•
A private member is still visible to extensions in the same file.
•
Only open permits subclassing or overriding from another module.
•
A public method on an internal type is still internal.
•
private(set) var publishes the getter and hides the setter.

W. Type casting, key paths and reflection

•
as? yields an optional, as! traps when the cast fails.
•
Any value at all versus any class instance.
•
case let shape as Circle combines the test and the binding.
•
type(of:) gives the dynamic type; T.self is a value of type T.Type.
•
\Person.name is a stored reference to a member, not a call to it.
•
WritableKeyPath lets you assign through the path.
•
map(\.name) relies on the implicit key-path-to-closure conversion.
•
Read-only introspection, and why it is not a serialization system.

X. Advanced operators

•
~, &, | and ^ on fixed-width integers.
•
>> on a signed integer preserves the sign bit.
•
&+, &- and &* wrap around instead of trapping.
•
Implementing + or == for your own type as a static function.
•
Declare the token, then how tightly it binds.
•
init(exactly:), init(truncatingIfNeeded:) and init(clamping:).

Z. Lexical structure

•
A reserved word becomes a usable name inside backticks; the backticks are not part of the identifier, so ` x and x` name the same thing.
•
Whitespace decides whether an operator is prefix, postfix or infix: both sides or neither means infix, left side only means prefix, right side only means postfix.
•
The lexer takes the longest substring the grammar allows, which is why an operator token can swallow characters you meant as something else.

Y. Language evolution and version gates

•
A numbered SE proposal reviewed in public is how the language changes.
•
One compiler builds Swift 5 mode and Swift 6 mode under different rules.
•
Opting into a future mode's behavior one feature at a time.
•
Source versus ABI stability: what stays compilable versus what stays binary-compatible.
•
#if swift(>=6.0) tests the language mode, #if compiler(>=6.0) the toolchain.
•
@available gates platform API, independently of the Swift version.

AA. Patterns

•
_ matches any value and ignores it, which is what makes for _ in 1...3 a loop that runs three times without naming the element.
•
An identifier matches any value and binds it, so the left-hand side of an ordinary let declaration is itself a pattern rather than a special case.
•
x? is an identifier pattern followed by a question mark and matches the some(Wrapped) case, so case let v? and case.some(let v) are the same match written two ways.
•
An expression pattern appears ONLY in a switch case label and is compared through the standard library's ~= operator, which you can overload to make a switch match your own type.
•
A case with associated values must be matched by a tuple pattern carrying one element per value, and these patterns work in if, while, guard and for-in conditions, not only in a switch.
Keentune is not affiliated with or endorsed by the organizations whose documentation informs these maps.
Start practicing Swift
All about Swift practice
Browse all free study guides
Also on your phone
All exam, test, and product names and trademarks are the property of their respective owners and are used here for identification and reference only. Keentune is independent study practice — not affiliated with, authorized, or endorsed by any of these organizations.
© 2026 SportaApp LLC