50 companion flashcards · AI-assisted study content · Open the deck →
This deck focuses on the intermediate concepts that often trip up developers who are moving past the basics of Swift. The cards cluster around four key areas: working with optionals and the various ways to unwrap or chain through them, designing and extending protocols, writing and capturing closures, and understanding the differences between value types and reference types. Together, these topics form the backbone of idiomatic Swift, and you'll see them appear constantly in real codebases and interview questions alike.
It's a good fit if you already know the fundamentals of the language, like variables, functions, and basic object-oriented syntax, but want to firm up the more nuanced features that make Swift distinct. Whether you're preparing for a technical interview, reviewing before a project at work, or simply trying to write cleaner and safer code, these flashcards give you a structured way to test yourself on the details you might otherwise skim past.
To get the most out of the deck, try to connect related cards as you study them rather than treating each one in isolation. For example, when you review optional unwrapping, think about how it interacts with optional chaining or the nil-coalescing operator in a real function. Spacing your review over several short sessions tends to work better than cramming everything at once, and if you can, try writing a small playground in Xcode that puts each concept into practice as you go. The goal is not just to recall the answer, but to recognize when to reach for each tool in your own code.
An Optional in Swift is a type that can hold either a value or the absence of a value, represented by nil. It is declared by appending a question mark to the type, as in var name: String?. Optionals enforce compile-time safety by forcing developers to consider the case where no value exists, which is a key design choice in Swift's type system.
There are several ways to safely access the value inside an optional. Optional binding with if let unwraps the value into a new constant within the block's scope, allowing the developer to use the unwrapped value safely. The nil-coalescing operator ?? provides an alternative by supplying a default value when the optional is nil, such as let name = optionalName ?? "Unknown". For more complex logic, the guard let statement unwraps the value but, unlike if let, requires the unwrapped value to be available in the rest of the enclosing scope, with the else branch exiting via return, throw, or break. This encourages a linear, early-exit coding style that keeps the happy path uncluttered.
Beyond optional binding, Swift offers other techniques for working with optionals. Optional chaining uses ?. to access properties or call methods on an optional, returning nil if any link in the chain is nil, which avoids runtime crashes. Forced unwrapping with ! extracts the value directly but crashes if the optional is nil, so it should only be used when the developer is certain a value exists. Implicitly unwrapped optionals, declared with ! instead of ?, are automatically unwrapped on access but still crash if accessed while nil; they are intended for situations where a value is guaranteed to exist after initialization. For transformations, the map method applies a closure to the wrapped value if it is non-nil, returning a new optional, while flatMap does the same but flattens the result when the closure itself returns an optional, preventing nested optionals.
A Protocol in Swift defines a blueprint of methods, properties, and other requirements that conforming types must implement. Classes, structs, and enums can all conform to a protocol, making protocols a central tool for defining shared interfaces across different kinds of types. A simple protocol might declare a draw() method, and any type that conforms to it must provide an implementation of that method.
Protocols can be enriched with default implementations through protocol extensions. By writing an extension on the protocol itself, developers can provide a body for any requirement, and conforming types will inherit that default behavior automatically. Conforming types can still override the default by providing their own implementation, which makes protocols a powerful tool for adding shared functionality without inheritance.
Protocol composition allows a function or type to require conformance to multiple protocols at once, using the & operator to combine protocol requirements. For example, a parameter declared as Codable & Hashable must conform to both protocols. Extensions in general add new functionality to existing types without modifying their source code, and they can add computed properties, methods, initializers, subscripts, nested types, and protocol conformances. However, extensions cannot add stored properties to a type, since stored properties must be declared in the type's original definition. Extensions are commonly used to organize code by grouping related functionality, often by protocol conformance, which keeps code well-structured without requiring subclassing.
A closure in Swift is a self-contained block of functionality that can be passed around and used throughout the code. Closures have a syntax that resembles a function without a name, written with curly braces and parameters followed by a return type and the in keyword. Closures can capture and store references to variables from their surrounding context, which gives them powerful flexibility and is the foundation for many functional programming patterns in Swift.
Trailing closure syntax is a convenience that lets developers write a closure argument after a function call's parentheses when the closure is the last argument. This makes code more readable, especially for functions like sorted, where numbers.sorted { $0 < $1 } is more natural than numbers.sorted(by: { $0 < $1 }). Closures passed in this way can also use shorthand argument names like $0, $1, and so on, when the parameter types can be inferred.
Closures have an important distinction between escaping and non-escaping behavior. By default, closure parameters are non-escaping, meaning they cannot be stored or called after the function returns. When a closure needs to be stored in a property or called asynchronously, the @escaping attribute marks it as eligible to outlive the function call. Closures that capture references to class instances can create retain cycles, in which two objects hold strong references to each other and prevent deallocation. Capture lists resolve this issue by declaring how values are captured: [weak self] captures a reference as a weak optional that becomes nil when the referenced object is deallocated, while [unowned] captures without increasing the reference count but assumes the value always exists.
Swift has two fundamental categories of types based on how they are stored and passed. Value types, which include structs, enums, and tuples, are copied when assigned or passed into a function, so each copy is independent and modifications to one do not affect the original. Reference types, which include classes, share a single instance through references, so modifying the instance through one reference is visible to all other references. This difference influences when each type is appropriate.
A struct is a value type that encapsulates related properties and methods. Structs automatically receive a memberwise initializer that accepts each stored property as a parameter, and they are copied on assignment. When a method on a struct or enum needs to modify its own properties, it must be marked with the mutating keyword; without it, the compiler prevents any property changes. Apple recommends structs as the default choice in Swift because of their value semantics, simplicity, and inherent thread safety through copying. Classes, by contrast, are reference types that support inheritance, deinitializers, and reference counting, and they are best used when reference semantics, inheritance, or shared mutable state is needed. Swift's standard collections like Array, Dictionary, and Set use a copy-on-write optimization, where the underlying storage is shared between copies until one copy is mutated, at which point a true copy is made, preserving the performance benefits of value semantics.
Enums in Swift are far more powerful than in many other languages. Each case can carry associated values of any type, allowing enums to model states with additional context, such as a network result carrying either success data or an error. Associated values are extracted through pattern matching with a switch statement, where case .success(let data) binds the data for use in that branch. The if case let syntax provides a similar capability for checking a single case. Raw values, by contrast, are fixed, compile-time constants of the same type assigned to each case, useful for cases like case north = "N" in a string-backed enum. A single enum cannot use both raw values and associated values at the same time, since raw values are static while associated values are runtime data that can differ per instance.
Generics enable developers to write flexible, reusable functions and types that work with any type. Instead of duplicating logic for each type, a generic placeholder like T is used in the function or type signature and is replaced with a concrete type when the code is used. A generic swap function, for example, can operate on any type rather than only on integers or only on strings.
Generic functions and types can be constrained to require certain capabilities. Type constraints use a protocol or class requirement to ensure the generic type provides the methods or properties the implementation needs. For example, func findIndex<T: Equatable> requires T to conform to Equatable so the == operator is available. A where clause adds further constraints, allowing multiple conditions or compound constraints that cannot be expressed inline, and is also used in for-in loops, switch statements, and protocol extensions to refine conditions.
Generic types parameterize a class, struct, or enum over one or more type placeholders, making the type itself reusable. A Stack<Element> struct, for instance, can hold any kind of value while preserving type safety. Protocols can use associated types as placeholders within their declarations, defined with associatedtype, leaving the conforming type to specify the concrete type. This allows protocols to describe behavior without committing to a specific type, providing flexibility while preserving the structure of the requirement.
Swift uses a do-try-catch model for error handling that integrates well with the type system. Functions that can fail are marked with the throws keyword, and callers must explicitly handle the possibility of an error using try inside a do block, followed by catch clauses that match specific error types. A general catch clause can catch any remaining errors. Custom error types are typically defined as enums conforming to the Error protocol, with cases that can optionally carry associated values for additional context, such as case serverError(code: Int).
Swift provides three variants of try for different situations. The standard try requires the call to be inside a do-catch block so errors are explicitly handled. The try? variant converts the result into an optional, returning nil if an error is thrown, which is convenient when the caller only cares whether the call succeeded. The try! variant force-tries the call and crashes at runtime if an error is thrown, so it should only be used when an error is impossible. The rethrows keyword is used in functions whose only throwing behavior comes from a closure parameter; the function will only throw if the passed closure throws, otherwise callers do not need to use try.
Control flow in Swift is enhanced by two important statements. The guard statement requires a condition to be true to continue execution, and its else branch must exit the current scope through return, throw, break, or a similar mechanism. When used to unwrap an optional, the unwrapped value is available either only within the if block (with if let) or in the rest of the enclosing scope (with guard let), making guard let the preferred pattern for early exits. The defer statement, by contrast, runs a block of code just before the current scope exits, regardless of how it exits, which is useful for cleanup such as closing files. Multiple defer blocks within the same scope execute in reverse order of declaration, like a stack.
Memory management in Swift for class instances is handled by Automatic Reference Counting, or ARC, which tracks the number of strong references to each instance and deallocates the instance when the count drops to zero. A retain cycle occurs when two class instances hold strong references to each other, preventing either from ever being deallocated. Retain cycles can be broken by using weak or unowned references. A weak reference is always optional and is automatically set to nil when the referenced object is deallocated, making it safe to use whenever the referenced object may eventually be released. An unowned reference is non-optional and assumes the referenced object always exists; accessing it after deallocation causes a crash, so it should only be used when the lifetime of the referenced object is guaranteed.
Properties in Swift come in several forms. Stored properties hold values directly, and computed properties calculate values through a getter and an optional setter each time they are accessed. Read-only computed properties can omit the get keyword. Property observers respond to changes in a stored property's value through willSet and didSet blocks, which run before and after the change respectively, with the upcoming value available as newValue and the previous value as oldValue. Property observers cannot be applied to computed properties, which already use getters and setters, but they can be applied to inherited stored or computed properties through overriding. A lazy stored property is one whose initial value is not computed until the first time it is accessed, which is useful for expensive initialization; lazy properties must be declared with var and are not thread-safe by default.
Swift offers several advanced features beyond these basics. Type casting allows developers to check or convert the type of an instance using the is operator for type checks, the as? operator for conditional downcasts that return an optional, and the as! operator for forced downcasts that crash on failure. Access control defines five levels of visibility: open allows access and overriding from any module, public allows access but not overriding outside the module, internal is the default and limits access to the same module, fileprivate restricts access to the same file, and private restricts access to the enclosing declaration. Finally, classes can define convenience initializers as secondary entry points that must call a designated initializer from the same class, providing additional ways to construct instances with sensible defaults.
nil. It is declared using a ? after the type, e.g., var name: String?. Optionals enforce safe handling of the absence of a value at compile time.protocol Drawable {
func draw()
}func fetch(completion: @escaping (Data) -> Void)enum NetworkResult {
case success(data: Data)
case failure(error: Error)
}func findIndex<T: Equatable>(of value: T, in array: [T]) -> Int?T must conform to Equatable, ensuring the == operator is available.extension MyViewController: UITableViewDelegate {
// delegate methods here
}struct Point {
var x: Double
var y: Double
}weak var delegate: SomeDelegate? – zeroed on deallocunowned var parent: Parent – not zeroed, must always have a value[weak self] in closuresguard let name = optionalName else {
return
}name) remains available after the guard statement.open – accessible and overridable from any modulepublic – accessible from any module, not overridable outsideinternal – accessible within the same module (default)fileprivate – accessible within the same fileprivate – accessible within the enclosing declarationDrill this topic
50 flashcards on Swift Programming — free, no signup needed to start.
Study Swift Programming flashcardsLearnWiki pages are generated with AI assistance from LearnCoachAssist's reviewed study catalog and may contain errors — verify anything critical against your course materials.