Conditionals
Swift’s principal conditional construct is if/else if/else. The condition must be a Bool (no truthiness coercion). Swift adds the guard statement — an “early-exit if false” form that admits substantial linear code paths. The if let and guard let forms admit unwrapping optionals; the if case form admits inline pattern matching. Swift 5.9+ admits if/else as expressions returning values. The combination — strict-Bool conditions, the guard early-exit, the if let unwrapping, the expression-form, the conditional-binding shorthand — is the substance of Swift’s selection surface.
if/else
The principal form:
if condition {
// body
} else if other {
// body
} else {
// body
}
Examples:
if x > 0 {
print("positive")
} else if x < 0 {
print("negative")
} else {
print("zero")
}
The braces are required; the parentheses around the condition are not:
if (x > 0) { /* admitted but redundant */ }
if x > 0 { /* conventional */ }
The condition must be a Bool — Swift admits no truthiness coercion:
let n = 5
if n { // ERROR: Int is not Bool
}
if n != 0 { // OK
}
guard
The guard admits early exit if false:
func process(input: String?) -> String {
guard let input, !input.isEmpty else {
return "default" // exit if condition false
}
// input is unwrapped and non-empty; continue
return input.uppercased()
}
The form: guard condition else { exit-statement }. The else block must exit the scope via:
returnthrowbreakcontinue- A function returning
Never(e.g.,fatalError).
The conventional Swift discipline favours guard for precondition checks — admits substantial linear code paths:
func process(user: User?) async throws {
guard let user else { throw AuthError.notFound }
guard user.isActive else { throw AuthError.inactive }
guard user.hasPermission(.read) else { throw AuthError.forbidden }
// proceed with confidence — all preconditions checked
try await fetch(for: user)
}
The pattern is one of Swift’s most distinctive idioms.
if let and guard let
The conditional binding for optional unwrapping:
let name: String? = "Alice"
// if let — branch on presence:
if let unwrapped = name {
print(unwrapped) // String, not String?
} else {
print("no name")
}
// guard let — early exit on absence:
func greet(name: String?) {
guard let name else { return }
print("Hello, \(name)")
}
The shorthand form (Swift 5.7+) admits omitting the right-hand side when it matches the binding name:
if let name { // shorthand for `if let name = name`
print(name)
}
guard let name else { return } // shorthand
For multiple bindings:
if let user, let email = user.email, !email.isEmpty {
sendNotification(to: email)
}
guard let user, let permissions = user.permissions, !permissions.isEmpty else {
throw .insufficientPermissions
}
if case
For inline pattern matching:
let value: Int? = 42
if case .some(let n) = value {
print(n) // 42
}
if case let n? = value { // alternate optional pattern
print(n)
}
// With where:
if case .some(let n) = value, n > 0 {
print("positive: \(n)")
}
The if case admits substantial pattern matching outside switch:
enum Shape {
case circle(radius: Double)
case square(side: Double)
}
let shape: Shape = .circle(radius: 5)
if case .circle(let r) = shape {
print("radius: \(r)")
}
Treated in Pattern matching.
Ternary
The C-style ternary ?::
let max = a > b ? a : b
let status = user.isActive ? "active" : "inactive"
let display = name ?? "anonymous" // similar but for nil-coalescing
The conventional discipline:
- Use ternary for short value-returning conditionals.
- Use
if/elsefor substantial branches. - Use
??for nil-default. - Avoid nested ternaries — they admit confusion.
if/else as expressions (Swift 5.9+)
Since Swift 5.9, if/else produces a value:
let max = if a > b { a } else { b }
let status = if user.isActive {
"active"
} else if user.isPending {
"pending"
} else {
"inactive"
}
// Conventional pre-5.9 alternative:
let status = {
if user.isActive { return "active" }
if user.isPending { return "pending" }
return "inactive"
}()
The expression-form admits substantial conciseness; the conventional discipline embraces the form for short conditionals.
switch as expression (Swift 5.9+)
Same era admits switch as expression:
let category = switch n {
case 0: "zero"
case 1...10: "small"
case 11...100: "medium"
default: "large"
}
Treated in Pattern matching.
defer
The defer admits deferred execution at scope exit:
func processFile(path: String) throws -> String {
let file = try FileHandle(forReadingFrom: URL(fileURLWithPath: path))
defer { file.closeFile() } // runs at scope exit, even on throw
return try String(contentsOf: file)
}
The defer is conventional for resource cleanup; multiple defers run in LIFO order.
Compile-time conditions
For platform/version-specific code:
#if os(iOS)
import UIKit
#elseif os(macOS)
import AppKit
#elseif os(Linux)
import Foundation
#endif
#if DEBUG
print("debug build")
#endif
if #available(iOS 16, macOS 13, *) {
// use APIs available only on these versions
} else {
// fallback
}
The #if is preprocessor-style (compile-time); #available is runtime version checking.
Common patterns
Early return
func process(input: String?) throws -> Result {
guard let input else {
throw ParseError.empty
}
guard input.count <= maxLength else {
throw ParseError.tooLong
}
guard input.matches(/^[a-z]+$/) else {
throw ParseError.invalidFormat
}
// main body
return parse(input)
}
The pattern reduces nesting; the conventional Swift style.
Optional unwrapping with guard
func sendEmail(to user: User?, subject: String?, body: String?) {
guard let user, let subject, let body else {
return
}
// all unwrapped
actualSend(to: user.email, subject: subject, body: body)
}
Validation chain
func validate(form: Form) -> [ValidationError] {
var errors: [ValidationError] = []
if form.name.isEmpty {
errors.append(.emptyName)
}
if let email = form.email, !email.contains("@") {
errors.append(.invalidEmail)
}
if form.age < 0 || form.age > 150 {
errors.append(.invalidAge)
}
return errors
}
if let chain
if let user = currentUser,
let permissions = user.permissions,
permissions.contains(.admin) {
grantAccess()
}
Default value with ??
let port = config.port ?? 8080
let name = user?.name ?? "anonymous"
let timeout = options.timeout ?? defaultTimeout
Pattern with if case
let response: Result<Data, Error> = ...
if case .success(let data) = response {
process(data)
} else if case .failure(let error) = response {
handleError(error)
}
// Conventional alternative is `switch`:
switch response {
case .success(let data): process(data)
case .failure(let error): handleError(error)
}
Conditional cast
if let user = sender as? User {
user.notify()
}
// In a switch:
switch sender {
case let user as User: user.notify()
case let admin as Admin: admin.report()
default: break
}
Defer for cleanup
func writeAtomically(_ data: Data, to path: String) throws {
let tmpPath = path + ".tmp"
try data.write(to: URL(fileURLWithPath: tmpPath))
defer {
try? FileManager.default.removeItem(atPath: tmpPath)
}
try FileManager.default.moveItem(atPath: tmpPath, toPath: path)
// tmp removal admitted only if move fails
}
Conditional method call
optionalCallback?() // calls only if non-nil
if let callback = optionalCallback {
callback()
}
Bool-returning helper
extension Optional {
var isSome: Bool {
self != nil
}
}
if value.isSome {
// ...
}
where clause in if let
if let n = parseInt(input), n > 0 && n < 100 {
print("valid: \(n)")
}
The condition after , admits substantial filtering.
if/else as expression (5.9+)
let status: Status = if user.isActive {
.active
} else if user.isPending {
.pending
} else {
.inactive
}
The form admits substantial conciseness — particularly for value-returning conditionals.
#if for platform-specific code
struct Logger {
func log(_ message: String) {
#if DEBUG
print("[DEBUG] \(message)")
#else
// production logging
#endif
}
}
#available for runtime checks
if #available(iOS 16, *) {
useNewAPI()
} else {
useOldAPI()
}
A note on case let
In if/while/guard, the case let form admits binding within a pattern:
if case let .some(n) = value, n > 0 {
/* ... */
}
while case let .next(item) = iterator.next() {
/* ... */
}
for case let .matched(value) in items {
/* ... */
}
The mechanism admits substantial pattern-driven control flow; treated in Pattern matching.
A note on the conventional discipline
The contemporary Swift conditional advice:
- Use
if/else if/elsefor boolean dispatch. - Use
guardfor early-exit precondition checks. - Use
if let/guard letfor optional unwrapping. - Use the shorthand form (
if let name) for same-name patterns (5.7+). - Use
if casefor inline pattern matching. - Use
??for nil-default values. - Use
?.for optional chaining. - Use the ternary sparingly — only short value conditionals.
- Use
if/switchas expressions (5.9+) for substantial conciseness. - Use
deferfor resource cleanup. - Use
#availablefor runtime version checks.
The combination — strict-Bool conditions, the guard early-exit, the conditional binding for optionals, the case-pattern matching, the expression-form (5.9+), the defer for cleanup — is the substance of Swift’s selection surface. The discipline produces clear, type-safe, linear code with substantial flexibility for conditional logic.