A let constant can be assigned exactly once, while a var variable can be reassigned any number of times; use let by default and switch to var only when the value really has to change.
Deferred initialization: a let can be declared first and assigned later, once on every path: let x: Int; if ok { x = 1 } else { x = 2 }.
Value types: let freezes the whole struct or enum: no property assignment and no mutating method calls.
Reference types: let freezes only the reference; the object's var properties can still change through it.
Compiler help: Xcode warns when a var is never mutated, and immutable data is easier to reason about and to share between threads.
iOSJunior
Swift Basics
Variables, optionals, types, control flow, closures, value vs reference
Questions closed
0 / 25
- 0Review
- 9Asked often
Questions
An optional either holds a value or holds nil, so "this may be missing" is checked by the compiler instead of crashing at runtime; T? is shorthand for the generic enum Optional<T> with the cases .some(T) and .none.
How it is built
nil is simply Optional.none; the enum adopts ExpressibleByNilLiteral, which is what makes the literal work
Because it is an ordinary enum, you can switch over .some / .none and call map / flatMap on it
For class references nil is stored as a null pointer, so UIView? costs no extra memory; Int? needs one extra byte (MemoryLayout<Int?>.size is 9)
Safe ways to unwrap
if let / guard let: bind the value only when it exists; if let name reuses the same name
?. (optional chaining): the call runs only when there is a value, and the whole expression becomes optional
?? (nil-coalescing): supply a default, as in name ?? "Guest"
! (force unwrap): crashes on nil, so keep it for values that truly cannot be missing, such as a URL built from a string literal
Both unwrap an optional, but if let keeps the value inside its own braces, while guard let makes it available for the rest of the scope and forces the else branch to leave that scope.
**The else must exit**: with return, throw, break, continue or a Never call such as fatalError(); the compiler rejects an else that falls through.
**Use guard for preconditions at the top of a function: the nil case means "nothing to do here", and the main path stays unindented instead of nesting deeper with every check.
Use if let** when the nil case is just another branch, or the value is needed only for a few lines.
Both accept several conditions separated by commas, including plain Boolean checks: guard let user, user.isActive else { return }.
It prints Hello, Optional("Ada"), then Hello, Ada, then 5: interpolating an optional prints the description of the Optional wrapper, and ?? binds more loosely than +.
Line 1: name is still a String?, so interpolation describes the wrapper, not the text. The compiler even warns: "String interpolation produces a debug description for an optional value".
Line 2: ?? turns it into a plain String first, so the value itself is printed.
Line 3: the expression parses as bonus ?? (0 + 1). bonus is 5, so the default is never used. Write (bonus ?? 0) + 1 to get 6.
Decide what nil should look like once (with ??, if let or a computed property next to the data) instead of at every print. String(describing:) only silences the warning; it still prints Optional("Ada").
String! is an implicitly unwrapped optional: the same Optional<String> underneath, but the compiler inserts a force unwrap wherever you use it as a non-optional, so reading it while it is nil crashes exactly like ! does.
Still an optional: if let, ?. and ?? work on it, and it can be assigned to a String? with no conversion.
Why outlets use it: an @IBOutlet is nil from init until the storyboard or nib loads the view, but it is always connected by the time viewDidLoad runs.
The usual crash: touching the outlet before the view has loaded: right after creating the controller, or in the presenting controller's prepare(for:sender:). Pass data into a plain property and apply it to the outlet in viewDidLoad.
Other legitimate uses: a test's sut that is filled in setUp(), and Objective-C APIs without nullability annotations.
Where you control construction, inject the value through init and make the property non-optional instead.
A value type is copied when you assign it or pass it to a function, so every variable owns its own data; a reference type is shared, so several variables point at one instance and see each other's changes.
Value types: struct, enum, tuples, and the standard collections Array, Dictionary, Set and String.
Reference types: class, actor and closures. They have identity (===), classes can be inherited, and they have deinit.
Memory: class instances live on the heap and are freed by ARC when the last strong reference disappears. Value types are stored inline: on the stack for local variables or inside whatever contains them, although the compiler may move them to the heap, for example when an escaping closure captures one.
Cost of a copy: the standard collections use copy-on-write: a copy shares the buffer until one side mutates it, so passing an array around is cheap.
Default choice: start with a struct; pick a class when you need shared mutable state, identity, inheritance or Objective-C interop.
== asks whether two values are equal by content and comes from the Equatable protocol; === asks whether two class references point at the very same instance.
== works for any Equatable type. Structs and enums get == synthesized once they declare Equatable conformance and all stored properties are Equatable; classes have to implement it themselves.
=== and !== take AnyObject?, so they work only with class and actor instances. Structs have no identity, so the question does not apply to them.
Two different objects can be == and still not ===: for example, two User instances decoded from the same JSON.
is checks a value's runtime type, as? attempts a downcast and returns an optional, as! forces the downcast and crashes if it fails, and plain as is an upcast or bridging conversion the compiler can prove safe.
is returns a Bool: is this value of that type, or does it conform to that protocol
as?: the safe downcast, usually inside if let or guard let
as! behaves like a force unwrap: crashes when the guess is wrong
as: upcasting (Dog to Animal), giving a literal a type (3 as Double) and bridging (String to NSString)
Any can hold a value of any type, including structs, enums, optionals and functions; AnyObject holds only class instances, so it is also the constraint for class-only protocols.
Class-only protocols: protocol Delegate: AnyObject is what allows weak var delegate.
Reading untyped data: a value from [String: Any] or a notification userInfo needs as? before you can use it.
New code: prefer Codable models or generics over Any, which gives up compile-time checking.
Choose by what you need: an Array keeps order and allows duplicates, a Set stores unique elements with fast lookup, and a Dictionary maps unique keys to values.
Array: ordered, index access in O(1), append in amortised O(1); contains and inserting or removing at the front are O(n).
Set: unordered and unique, elements must be Hashable; insert and contains are O(1) on average, plus union, intersection and subtracting.
Dictionary: unordered key-value pairs with Hashable keys; lookup is O(1) on average and returns an optional, or use dict[key, default: 0].
Order is not guaranteed: iteration order of a Set or Dictionary can change between launches, because hashing is seeded per process. Never depend on it.
All three are value types with copy-on-write. A classic fix: contains on an array inside a loop is O(n²); build a Set once and look up in it.
map transforms every element one-to-one, compactMap transforms and drops the nil results, and flatMap turns each element into a sequence and joins all of them into one array.
map over [String?] gives [Int?]: the nils are still there.
compactMap is the idiomatic way to parse a list where some entries fail: ["1", "x"].compactMap(Int.init) gives [1].
flatMap flattens one level: [[1, 2], [3]] becomes [1, 2, 3]. The old flatMap overload that removed nils was renamed compactMap in Swift 4.1.
On an Optional, map transforms the wrapped value, and flatMap is for a transform that itself returns an optional, so you get Int? instead of Int??.
filter keeps matching elements and reduce folds a sequence into one value.
Swift's switch matches patterns rather than just integers, must be exhaustive, and never falls through into the next case unless you write fallthrough.
Patterns: enum cases with associated values, ranges (case 0..<50), tuples, types (case let x as String), optionals and strings.
**where**: adds a condition to a case: case .running(let p) where p > 0.9.
Exhaustive: every possible value must be handled. Model state as an enum and switch without default: adding a new case then breaks every switch that forgot it, a compile error instead of a bug.
**@unknown default: for enums from Apple frameworks that may gain cases: the code compiles, but you get a warning when a new case appears.
No implicit fallthrough**: each case needs at least one statement; write break to do nothing.
One pattern only: if case and guard case match a single pattern without a full switch.
Swift never converts numeric types implicitly, so Int plus Double is a compile error until you write Double(count); Float is 32-bit, Double is 64-bit and the default, and CGFloat is the type Core Graphics and UIKit use for sizes.
Literals are the exception: a literal like 3 adapts to the expected type, so let x: Double = 3 compiles. Without context an integer literal becomes Int, which is why let half = 1 / 2 is 0.
**Float: about 7 significant digits; used for Metal, ML and audio buffers where memory matters.
Double: about 15–16 digits; the default for floating-point literals and the right choice for most calculations.
CGFloat**: 64-bit on iPhone, iPad and Mac; since Swift 5.5 it converts implicitly to and from Double, the one exception to the rule.
A closure is a block of code you can store in a variable and pass around, and it remembers the variables of the scope where it was created; closures, like functions, are reference types.
Syntax: { (a: Int, b: Int) -> Int in a + b } shrinks to { $0 + $1 } when the types can be inferred, and a closure passed as the last argument can be written after the parentheses as a trailing closure.
Capturing: a closure captures the variable itself, not a snapshot of its value, so it sees later changes and can modify the variable. A capture list such as [value] or [weak self] changes that.
Reference type: assigning a closure to a second constant does not copy its captured state; both names share it. This is also why a closure stored in a property of self that uses self creates a retain cycle.
Escaping: closure parameters are non-escaping by default; a closure that is stored or called after the function returns must be marked @escaping.
Neither keeps the object alive: weak is an optional that becomes nil when the object is deallocated, while unowned is non-optional and crashes if you touch it after the object is gone. Use [weak self] in an escaping closure that self itself owns.
**weak**: always optional (weak var, or weak let since Swift 6.2). The safe default when the other object may disappear first: delegates, callbacks held by a service that outlives the screen.
**unowned: only when the referenced object is guaranteed to outlive the reference, such as a child pointing back to the parent that owns it. A wrong guess is a crash, not a silent bug.
When [weak self] is needed**: when self owns the closure, directly or through something it holds: a stored callback, a Timer, a Combine sink kept in cancellables, a notification observer.
When it is not: non-escaping closures (map, filter, forEach) and short-lived escaping ones such as UIView.animate or DispatchQueue.main.async, which run once and are released; a strong capture there only delays deallocation briefly.
Inside the closure: guard let self else { return } unwraps once; since Swift 5.8 you can then call methods without writing self..
A function that can fail is marked throws, every call to it must be marked with try, and the error is either handled in do/catch or passed up to the caller, so failure is visible in the function's signature.
Errors: any type conforming to Error; usually an enum, so the caller can match specific cases and read their associated values.
try propagates the error; the enclosing function must also be throws, or the call must sit inside do/catch.
try? turns the result into an optional and drops the error; fine when you do not care why it failed.
try! crashes on error, with the same risk as a force unwrap.
**Result<Success, Failure>**: stores success or failure as a value: useful for completion handlers, caching, or passing an outcome on to someone else. With async/await, plain throws covers most cases.
**rethrows**: the function throws only if the closure you pass it throws, which is how map works.
Typed throws: since Swift 6 you can write throws(AuthError), so catch knows the exact error type. Untyped throws stays the default for most APIs, because a typed error locks the error type into the public signature.
It prints Changed and then 2: the array itself was copied, so appending to copy does not touch original, but both arrays hold references to the same Item objects.
**Array is a value type**: var copy = original produces an independent array (copy-on-write makes that cheap), which is why original.count stays 2.
The elements are class instances: copying an array copies the references, not the objects. copy[0] and original[0] are the same instance, so the new title is visible through both.
Fix 1: make Item a struct; then copy[0].title = "Changed" modifies only the copy, and the first line prints A.
Fix 2: if Item has to stay a class, copy the objects deliberately: original.map { Item(title: $0.title) }.
The same rule applies to a struct with a class property: value semantics end at the first reference you store.
if var existing = items.first(...) binds a copy of the struct, so quantity += 1 changes that local copy and the array keeps the old value: every product stays at quantity 1.
Value semantics: CartItem is a struct, so first(where:) returns a copy. if var only makes that copy mutable; it has no link back to items and is thrown away at the closing brace.
The fix: look up the index with firstIndex(where:) and mutate through the subscript: items[index].quantity += 1 modifies the element in the array's own storage.
Same trap elsewhere: for var item in items { item.quantity += 1 } and guard var also mutate copies. Loop over items.indices, or use dict[key]?.quantity += 1 for a dictionary.
Why it slips through review: += reads the variable, so the compiler gives no "never read" warning. With a class CartItem the same code works, which is why this bug often appears when a model is turned from a class into a struct.
Catch it: a unit test that adds the same product twice and asserts quantity == 2.
It prints captured: 2 and then capture list: 1: a normal closure captures the variable itself and reads it at call time, while a capture list copies the value at the moment the closure is created.
Without a capture list: the closure holds on to the variable's storage, so it sees the assignment made after it was created, and it could also change the variable.
**With [value]: the current value is copied into an immutable constant inside the closure; later changes outside are invisible to it.
For class instances**: [object] copies the reference, not the object: the closure still sees changes to the object's properties and keeps the object alive.
**[weak self] and [unowned self]**: the same mechanism with different ownership, used to break retain cycles.
It prints 0 1, then C, B, A: a defer block runs when its scope exits, after the return value has already been computed, and several defer blocks run in reverse order.
**After return is evaluated**: next() returns the old value 0, then the deferred increment runs, so counter is already 1 when it is printed.
Last in, first out: the most recently registered defer runs first, which is why B comes before A.
Any exit: it runs on return, on a thrown error, on break and when execution reaches the closing brace, which makes it the place for paired cleanup: isLoading = false, lock.unlock(), closing a file.
The scope is the braces: a defer inside a loop body runs at the end of every iteration, not at the end of the function.
Only if reached: a defer written after an early return never runs, and nothing runs on a crash or fatalError().
An inout parameter is copy-in, copy-out: the function works on a mutable copy of your variable and writes it back when it returns, so the caller sees the change; you pass the argument with &.
Only mutable storage: a var, a property or an array element; a let or a literal does not compile.
Exclusive access: passing the same variable to two inout parameters is rejected at compile time or by a runtime trap, because the two write-backs would conflict.
Cannot escape: an escaping closure cannot capture an inout parameter; there would be nothing to write back to later.
**mutating methods**: inside one, self is effectively an inout parameter.
Class instances: the reference is already shared, so a function can change the object's properties without inout. Marking a class parameter inout means something else: the function may replace the caller's object with a different one.
Prefer returning a new value; use inout for in-place algorithms on large values.
A Swift Character is a grapheme cluster that can take several bytes, so there is no constant-time way to jump to the fourth one; String deliberately has no integer subscript, and you move through it with String.Index instead.
Indices are opaque: get them from startIndex, index(_:offsetBy:) or firstIndex(of:); an index from one string is meaningless in another.
**count is O(n)**: it walks the whole string, so check isEmpty rather than count == 0.
Code-unit views: utf8, utf16 and unicodeScalars when you need bytes or code units.
**Substring**: what slicing returns (prefix, split, s[range]). It shares the original string's memory, which makes slicing cheap but keeps the whole original alive. Convert with String(sub) before storing it.
dropFirst returns an ArraySlice that keeps the original array's indices, so its first element sits at index 1, not 0, and rest[0] really is out of range.
dropFirst, dropLast, prefix, suffix and range subscripts such as items[2...] all return an ArraySlice, not a new Array.
count counts elements, but valid indices run from startIndex up to, but not including, endIndex: for a four-element array, count is 3 and the indices are 1, 2 and 3.
enumerated() yields offsets, not indices, so rest[offset] hides the same bug.
Safe access: rest.first, rest[rest.startIndex], a loop over rest.indices, or Array(rest) to get indices starting at 0.
Memory: a slice keeps the whole base array alive; convert with Array(slice) before storing it in a property. Substring behaves the same way.
Double is binary floating point, and values like 1.1 or 0.1 have no exact binary representation, so each operation adds a tiny error that shows up once the result is printed; store money as integer minor units or as Decimal.
Integer minor units: an Int number of cents: exact, fast and trivial to sum. Ask the backend to send amounts this way.
**Decimal: base-10 arithmetic with up to 38 significant digits; the right choice once tax rates and percentages appear.
Build Decimal from text**: Decimal(string: "19.99"). Decimal(19.99) and let d: Decimal = 19.99 go through Double first and bring the error with them.
Converting is a trap too: Int(4.35 * 100) is 434, because the product is 434.99999999999994 and Int(_:) truncates.
Round once, on purpose: the rounding mode and the step (per line or per order) are business rules; NSDecimalRound takes both the scale and the mode.
Format at the edge: amount.formatted(.currency(code: "USD")) applies the user's locale.
Double remains the right type for measurements, geometry and animation.
assert is checked only in Debug builds, precondition is checked in both Debug and Release, and fatalError always stops the app; pick by whether a broken assumption is allowed to continue in production.
**assert / assertionFailure**: catch programmer mistakes during development. In a Release build (-O) the condition is not even evaluated, so never put side effects inside it.
**precondition / preconditionFailure**: guard invariants whose violation would corrupt data; they stop the app in Release too. Only -Ounchecked removes them, and few apps build with it.
**fatalError**: traps in every build and returns Never, so the compiler accepts it where a value or an exit is required: an unreachable default, or a required init?(coder:) you do not support.
Cheap messages: the message argument is an @autoclosure, so building the string costs nothing when the check passes.
Not error handling: invalid user input or a failed request should throw or return an optional, not crash the app.