Swift Language Features Guide


From Objective-C to Modern Swift

Swift Language Features Guide

From Objective-C to Modern Swift


Table of Contents

  1. Optionals
  2. Value Types vs Reference Types
  3. Protocols & Protocol-Oriented Programming
  4. Generics
  5. Closures
  6. Error Handling
  7. Memory Management in Swift
  8. Property Observers & Wrappers
  9. Extensions
  10. Access Control
  11. Enums & Pattern Matching
  12. Modern Concurrency (async/await)
  13. Collection Operators
  14. Swift vs Objective-C Key Differences

Optionals

The Basics

Coming from Objective-C, optionals are Swift’s way of handling nil safely at compile-time.

// Objective-C: nil could be anywhere
NSString *name = nil;
NSInteger length = [name length];  // Crashes at runtime

// Swift: nil is explicit
var name: String? = nil  // Optional String
let length = name?.count // Optional chaining, returns nil safely

Optional Types

// Optional declaration
var optionalString: String?           // nil by default
var optionalInt: Int? = 42
var optionalArray: [String]? = nil

// Implicitly Unwrapped Optional (use sparingly)
var definitelyHasValue: String!       // Will crash if nil when accessed
// Similar to: __nonnull in Objective-C

Unwrapping Strategies

var name: String? = "John"

// 1. Optional Binding (if let) - MOST COMMON
if let unwrappedName = name {
    print("Hello, \(unwrappedName)")
} else {
    print("No name provided")
}

// 2. Guard statement - PREFERRED for early exit
func greet(name: String?) {
    guard let unwrappedName = name else {
        print("No name")
        return  // Must exit scope
    }
    
    // unwrappedName available for rest of function
    print("Hello, \(unwrappedName)")
}

// 3. Multiple optional bindings
if let firstName = user.firstName,
   let lastName = user.lastName,
   let age = user.age,
   age >= 18 {
    print("\(firstName) \(lastName) is an adult")
}

// 4. Force unwrap (!) - AVOID unless certain
let definiteValue = name!  // Crashes if nil
// Only use when you're 100% sure it's not nil

// 5. Nil coalescing operator (??)
let displayName = name ?? "Guest"  // Provides default

// 6. Optional chaining
let count = user.profile?.bio?.count  // Returns nil if any link is nil

// 7. Optional pattern matching
if case let name? = optionalName {
    print("Has value: \(name)")
}

Optional Chaining vs Force Unwrapping

class Person {
    var address: Address?
}

class Address {
    var street: String?
}

let person: Person? = Person()

// ❌ Force unwrapping - crashes if any is nil
let street = person!.address!.street!

// ✅ Optional chaining - returns nil safely
let street = person?.address?.street  // Type: String?

// ✅ With default value
let street = person?.address?.street ?? "Unknown"

Common Patterns

// Pattern 1: Unwrap and use
func process(data: Data?) {
    guard let data = data else { return }
    // Use data safely
}

// Pattern 2: Map optional value
let uppercased = name?.uppercased()  // String?

// Pattern 3: Unwrap with condition
if let name = name, !name.isEmpty {
    print(name)
}

// Pattern 4: Try optional operations
let url = URL(string: urlString)  // Returns nil if invalid

Interview Question: Optional vs IUO

Q: “When should you use Implicitly Unwrapped Optionals?”

A: Very rarely. Main use cases:

  1. IBOutlets that will be set by Interface Builder
  2. Two-phase initialization where you can’t initialize in init but it will definitely be set before use
  3. Breaking initialization dependency cycles
// IBOutlet - set by Interface Builder
@IBOutlet weak var tableView: UITableView!

// Two-phase init
class MyViewController: UIViewController {
    var viewModel: ViewModel!  // Set in configure() before use
    
    func configure(viewModel: ViewModel) {
        self.viewModel = viewModel
    }
}

Value Types vs Reference Types

The Fundamental Difference

// Value Type (struct, enum) - COPIED
struct Point {
    var x: Int
    var y: Int
}

var point1 = Point(x: 0, y: 0)
var point2 = point1      // COPY made
point2.x = 10

print(point1.x)  // 0 - unchanged
print(point2.x)  // 10

// Reference Type (class) - SHARED
class Person {
    var name: String
    init(name: String) { self.name = name }
}

var person1 = Person(name: "John")
var person2 = person1    // SAME instance
person2.name = "Jane"

print(person1.name)  // "Jane" - same object
print(person2.name)  // "Jane"

Struct vs Class Decision Tree

Use STRUCT when:
├─ Represents simple data (Point, Size, Range)
├─ Should be compared by value equality
├─ Needs to be copied when passed around
├─ Properties are also value types
└─ No inheritance needed

Use CLASS when:
├─ Needs inheritance
├─ Need to control identity (same vs different instance)
├─ Need deinitializer
├─ Reference semantics required (shared state)
└─ Objective-C interoperability needed

Copy-on-Write (COW)

Swift’s collections use copy-on-write optimization:

// Arrays are value types but use COW
var array1 = [1, 2, 3]
var array2 = array1        // No copy yet (shares storage)

array2.append(4)           // NOW copy is made

// array1 = [1, 2, 3]
// array2 = [1, 2, 3, 4]

// Built-in types with COW: Array, Dictionary, Set, String

Implementing COW in Your Types

// Advanced pattern for custom COW
final class Storage {
    var data: [Int]
    
    init(data: [Int]) {
        self.data = data
    }
}

struct MyArray {
    private var storage: Storage
    
    init(data: [Int]) {
        self.storage = Storage(data: data)
    }
    
    var data: [Int] {
        get {
            return storage.data
        }
        set {
            // Copy-on-write: only copy if not uniquely owned
            if !isKnownUniquelyReferenced(&storage) {
                storage = Storage(data: storage.data)
            }
            storage.data = newValue
        }
    }
}

Mutability

// Struct: let = immutable struct AND properties
struct Point {
    var x: Int
    var y: Int
}

let point = Point(x: 0, y: 0)
point.x = 10  // ❌ Error: point is immutable

var mutablePoint = Point(x: 0, y: 0)
mutablePoint.x = 10  // ✅ OK

// Class: let = immutable reference, but properties can change
class Person {
    var name: String
    init(name: String) { self.name = name }
}

let person = Person(name: "John")
person.name = "Jane"  // ✅ OK - reference is constant, not object
person = Person(name: "Bob")  // ❌ Error: can't reassign let reference

Mutating Methods

struct Point {
    var x: Int
    var y: Int
    
    // Must mark as mutating if it modifies self
    mutating func moveBy(x: Int, y: Int) {
        self.x += x
        self.y += y
    }
}

var point = Point(x: 0, y: 0)
point.moveBy(x: 10, y: 20)  // ✅ OK

let immutablePoint = Point(x: 0, y: 0)
immutablePoint.moveBy(x: 10, y: 20)  // ❌ Error: immutable

Protocols & Protocol-Oriented Programming

Basic Protocol

// Define behavior contract
protocol Drawable {
    func draw()
}

// Conform to protocol
struct Circle: Drawable {
    func draw() {
        print("Drawing circle")
    }
}

class Square: Drawable {
    func draw() {
        print("Drawing square")
    }
}

Protocol Requirements

protocol Vehicle {
    // Property requirements
    var numberOfWheels: Int { get }          // Read-only
    var color: String { get set }            // Read-write
    
    // Method requirements
    func startEngine()
    mutating func refuel()  // For value types that need to modify
    
    // Initializer requirements
    init(color: String)
}

struct Car: Vehicle {
    var numberOfWheels: Int { return 4 }
    var color: String
    
    func startEngine() {
        print("Vroom")
    }
    
    mutating func refuel() {
        print("Refueling")
    }
    
    init(color: String) {
        self.color = color
    }
}

Protocol Extensions (The Power!)

protocol Greetable {
    var name: String { get }
    func greet()
}

// Provide default implementation
extension Greetable {
    func greet() {
        print("Hello, \(name)")
    }
    
    // Add new methods to all conforming types
    func greetLoudly() {
        print("HELLO, \(name.uppercased())!")
    }
}

struct Person: Greetable {
    var name: String
    // Gets greet() and greetLoudly() for free!
}

let person = Person(name: "John")
person.greet()         // "Hello, John"
person.greetLoudly()   // "HELLO, JOHN!"

Protocol Composition

protocol Named {
    var name: String { get }
}

protocol Aged {
    var age: Int { get }
}

// Compose multiple protocols
func celebrate(person: Named & Aged) {
    print("\(person.name) is \(person.age) years old")
}

// Or create type alias
typealias Person = Named & Aged

func celebrate(person: Person) {
    print("\(person.name) is \(person.age) years old")
}

Associated Types (Like Generics for Protocols)

protocol Container {
    associatedtype Item
    
    var count: Int { get }
    mutating func append(_ item: Item)
    subscript(i: Int) -> Item { get }
}

struct IntStack: Container {
    // Type inference determines Item = Int
    var items: [Int] = []
    
    var count: Int {
        return items.count
    }
    
    mutating func append(_ item: Int) {
        items.append(item)
    }
    
    subscript(i: Int) -> Int {
        return items[i]
    }
}

// Generic implementation
struct Stack<Element>: Container {
    var items: [Element] = []
    
    var count: Int {
        return items.count
    }
    
    mutating func append(_ item: Element) {
        items.append(item)
    }
    
    subscript(i: Int) -> Element {
        return items[i]
    }
}

Protocol-Oriented Programming Pattern

// Define protocol
protocol RequestProtocol {
    var urlRequest: URLRequest { get }
}

// Provide default implementation
extension RequestProtocol {
    func execute() async throws -> Data {
        let (data, _) = try await URLSession.shared.data(for: urlRequest)
        return data
    }
}

// Specific requests just define their URL
struct UserRequest: RequestProtocol {
    let userId: String
    
    var urlRequest: URLRequest {
        let url = URL(string: "https://api.example.com/users/\(userId)")!
        return URLRequest(url: url)
    }
}

// Use it
let request = UserRequest(userId: "123")
let data = try await request.execute()  // Gets execute() from protocol extension

Class-Only Protocols

// Restrict to reference types only
protocol SomeClassOnlyProtocol: AnyObject {
    func doSomething()
}

// Now only classes can conform
class MyClass: SomeClassOnlyProtocol {
    func doSomething() {}
}

// ❌ Error: structs can't conform
struct MyStruct: SomeClassOnlyProtocol {  // Error!
    func doSomething() {}
}

// Use case: Delegates (need weak reference)
protocol MyDelegate: AnyObject {
    func didComplete()
}

class MyClass {
    weak var delegate: MyDelegate?  // ✅ Can be weak
}

Generics

Basic Generic Functions

// Non-generic (repetitive)
func swapInts(_ a: inout Int, _ b: inout Int) {
    let temp = a
    a = b
    b = temp
}

// Generic (works with any type)
func swap<T>(_ a: inout T, _ b: inout T) {
    let temp = a
    a = b
    b = temp
}

var x = 5, y = 10
swap(&x, &y)  // x = 10, y = 5

var str1 = "Hello", str2 = "World"
swap(&str1, &str2)  // str1 = "World", str2 = "Hello"

Generic Types

// Generic stack
struct Stack<Element> {
    private var items: [Element] = []
    
    var isEmpty: Bool {
        return items.isEmpty
    }
    
    mutating func push(_ item: Element) {
        items.append(item)
    }
    
    mutating func pop() -> Element? {
        return items.popLast()
    }
    
    func peek() -> Element? {
        return items.last
    }
}

// Usage
var intStack = Stack<Int>()
intStack.push(1)
intStack.push(2)
print(intStack.pop())  // Optional(2)

var stringStack = Stack<String>()
stringStack.push("Hello")

Generic Constraints

// Constrain to types that conform to Equatable
func findIndex<T: Equatable>(of valueToFind: T, in array: [T]) -> Int? {
    for (index, value) in array.enumerated() {
        if value == valueToFind {  // Requires Equatable
            return index
        }
    }
    return nil
}

let index = findIndex(of: "John", in: ["Jane", "John", "Bob"])  // 1

// Multiple constraints
func combine<T: Numeric & Comparable>(a: T, b: T) -> T {
    return a > b ? a : b
}

// Where clause for complex constraints
func allItemsMatch<C1: Container, C2: Container>(
    _ container1: C1,
    _ container2: C2
) -> Bool where C1.Item == C2.Item, C1.Item: Equatable {
    
    guard container1.count == container2.count else { return false }
    
    for i in 0..<container1.count {
        if container1[i] != container2[i] {
            return false
        }
    }
    
    return true
}

Generic Extensions

// Extend generic types
extension Stack where Element: Equatable {
    func contains(_ item: Element) -> Bool {
        return items.contains(item)
    }
}

// Only Stack<T> where T is Equatable gets this method
let intStack = Stack<Int>()
print(intStack.contains(5))  // ✅ Int is Equatable

// Extend with specific type
extension Stack where Element == String {
    func joinedString() -> String {
        return items.joined(separator: ", ")
    }
}

// Only Stack<String> gets this method
let stringStack = Stack<String>()
print(stringStack.joinedString())

Opaque Types (some keyword)

// Returns "some" protocol type - hides concrete type
protocol Shape {
    func draw() -> String
}

struct Circle: Shape {
    func draw() -> String { return "○" }
}

struct Square: Shape {
    func draw() -> String { return "□" }
}

// Return specific type but hide which one
func makeShape() -> some Shape {
    return Circle()  // Concrete type hidden
}

let shape = makeShape()
// Compiler knows it's a specific type, but caller doesn't know which
// Allows for optimizations

// Common in SwiftUI
var body: some View {
    Text("Hello")  // Actual type is complex, abstracted as "some View"
}

Closures

Closure Syntax

// Full syntax
let closure: (Int, Int) -> Int = { (a: Int, b: Int) -> Int in
    return a + b
}

// Inferred types
let closure = { (a, b) in
    return a + b
}

// Implicit return (single expression)
let closure = { (a, b) in a + b }

// Shorthand argument names
let closure = { $0 + $1 }

// Trailing closure (if last parameter)
numbers.map { $0 * 2 }

Escaping vs Non-Escaping

// Non-escaping (default) - closure executes before function returns
func processImmediately(completion: () -> Void) {
    completion()  // Executes now
}

// Escaping - closure may execute after function returns
func processLater(completion: @escaping () -> Void) {
    DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
        completion()  // Executes later
    }
}

// Common use case: storing closure
class NetworkManager {
    var completionHandlers: [() -> Void] = []
    
    func fetchData(completion: @escaping () -> Void) {
        completionHandlers.append(completion)  // Must be @escaping
    }
}

Capture Lists

class MyViewController: UIViewController {
    var name = "John"
    
    // ❌ Retain cycle - closure captures self strongly
    func setupBadClosure() {
        someAsyncOperation { result in
            self.name = result  // Strong reference to self
        }
    }
    
    // ✅ Weak self - breaks retain cycle
    func setupWeakClosure() {
        someAsyncOperation { [weak self] result in
            guard let self = self else { return }
            self.name = result
        }
    }
    
    // Unowned self - use when self definitely won't be nil
    func setupUnownedClosure() {
        someAsyncOperation { [unowned self] result in
            self.name = result  // Crashes if self is deallocated
        }
    }
    
    // Capture specific properties
    func setupPropertyCapture() {
        let name = self.name
        someAsyncOperation { [name] result in
            print(name)  // Captured copy of name
        }
    }
    
    // Multiple captures
    func setupMultipleCaptures() {
        someAsyncOperation { [weak self, weak delegate = self.delegate, id = self.id] in
            guard let self = self else { return }
            // Use captured values
        }
    }
}

Autoclosure

// Delays execution of expression
func logIfTrue(_ condition: @autoclosure () -> Bool, message: String) {
    if condition() {
        print(message)
    }
}

// Called like this (no closure syntax needed)
logIfTrue(2 > 1, message: "It's true!")  // Expression automatically becomes closure

// Common use: assert
func assert(_ condition: @autoclosure () -> Bool) {
    #if DEBUG
    if !condition() {
        fatalError("Assertion failed")
    }
    #endif
}

assert(x > 0)  // Only evaluated if DEBUG

Common Closure Patterns

// Pattern 1: Completion handler
func fetchData(completion: @escaping (Result<Data, Error>) -> Void) {
    URLSession.shared.dataTask(with: url) { data, response, error in
        if let error = error {
            completion(.failure(error))
        } else if let data = data {
            completion(.success(data))
        }
    }.resume()
}

// Pattern 2: Configuration closure
class Button {
    var tapHandler: (() -> Void)?
    
    func onTap(_ handler: @escaping () -> Void) -> Self {
        self.tapHandler = handler
        return self
    }
}

let button = Button()
    .onTap { print("Tapped!") }

// Pattern 3: Lazy initialization
class ViewController: UIViewController {
    lazy var tableView: UITableView = {
        let table = UITableView()
        table.delegate = self
        table.dataSource = self
        return table
    }()
}

Error Handling

Throwing Functions

// Define error types
enum NetworkError: Error {
    case invalidURL
    case noData
    case decodingFailed
}

// Throwing function
func fetchUser(id: String) throws -> User {
    guard let url = URL(string: "https://api.example.com/users/\(id)") else {
        throw NetworkError.invalidURL
    }
    
    let (data, _) = try await URLSession.shared.data(from: url)
    
    guard !data.isEmpty else {
        throw NetworkError.noData
    }
    
    do {
        let user = try JSONDecoder().decode(User.self, from: data)
        return user
    } catch {
        throw NetworkError.decodingFailed
    }
}

Calling Throwing Functions

// do-catch
do {
    let user = try fetchUser(id: "123")
    print("Fetched: \(user.name)")
} catch NetworkError.invalidURL {
    print("Invalid URL")
} catch NetworkError.noData {
    print("No data received")
} catch {
    print("Unknown error: \(error)")
}

// try? - converts to optional (nil on error)
if let user = try? fetchUser(id: "123") {
    print("Fetched: \(user.name)")
}

// try! - force try (crashes on error)
let user = try! fetchUser(id: "123")  // Use only when 100% certain it won't throw

Result Type

// Modern approach: Result<Success, Failure>
func fetchUser(id: String, completion: @escaping (Result<User, NetworkError>) -> Void) {
    // ... async work
    
    if let user = user {
        completion(.success(user))
    } else {
        completion(.failure(.noData))
    }
}

// Using Result
fetchUser(id: "123") { result in
    switch result {
    case .success(let user):
        print("Got user: \(user.name)")
    case .failure(let error):
        print("Error: \(error)")
    }
}

// Converting between Result and throwing
func fetchUser(id: String) async throws -> User {
    return try await withCheckedThrowingContinuation { continuation in
        fetchUser(id: id) { result in
            continuation.resume(with: result)
        }
    }
}

Error Protocol

// Custom error with additional info
struct ValidationError: Error {
    let field: String
    let message: String
}

// Localized error
extension NetworkError: LocalizedError {
    var errorDescription: String? {
        switch self {
        case .invalidURL:
            return "The URL is invalid"
        case .noData:
            return "No data was received"
        case .decodingFailed:
            return "Failed to decode the response"
        }
    }
}

Memory Management in Swift

ARC Basics

// Automatic Reference Counting (like Objective-C ARC)
class Person {
    let name: String
    init(name: String) {
        self.name = name
        print("\(name) initialized")
    }
    
    deinit {
        print("\(name) deallocated")
    }
}

var person: Person? = Person(name: "John")  // Reference count = 1
person = nil  // Reference count = 0, deinit called

Strong Reference Cycles

// ❌ Retain cycle
class Person {
    var apartment: Apartment?
}

class Apartment {
    var tenant: Person?  // Strong reference
}

var person: Person? = Person()
var apartment: Apartment? = Apartment()

person!.apartment = apartment  // person → apartment (strong)
apartment!.tenant = person     // apartment → person (strong)

// Cycle! Neither will deallocate
person = nil
apartment = nil
// Both still in memory!

Weak References

class Person {
    var apartment: Apartment?
}

class Apartment {
    weak var tenant: Person?  // ✅ Weak reference
}

var person: Person? = Person()
var apartment: Apartment? = Apartment()

person!.apartment = apartment
apartment!.tenant = person

person = nil  // Person deallocated
print(apartment!.tenant)  // nil (weak reference automatically set to nil)

Unowned References

// Use when reference will NEVER be nil during its lifetime
class Customer {
    let name: String
    var card: CreditCard?
    
    init(name: String) {
        self.name = name
    }
}

class CreditCard {
    let number: String
    unowned let customer: Customer  // Customer will always exist
    
    init(number: String, customer: Customer) {
        self.number = number
        self.customer = customer
    }
}

var customer: Customer? = Customer(name: "John")
customer!.card = CreditCard(number: "1234", customer: customer!)

customer = nil  // Both deallocated correctly

Weak vs Unowned Decision

Use WEAK when:
├─ Reference might become nil
├─ Reference is optional
└─ Example: delegate, parent reference

Use UNOWNED when:
├─ Reference will never be nil during lifetime
├─ Reference is non-optional
├─ Guaranteed lifetime relationship
└─ Example: child has unowned reference to parent

Closure Capture List Patterns

// Pattern 1: Weak self with guard
someAsync { [weak self] in
    guard let self = self else { return }
    self.updateUI()
}

// Pattern 2: Weak self without guard (ok if operation is safe)
someAsync { [weak self] in
    self?.updateUI()  // Does nothing if self is nil
}

// Pattern 3: Capture specific strong references
class ViewController {
    let viewModel: ViewModel
    
    func setup() {
        someAsync { [viewModel] in
            // Captures viewModel strongly, not self
            viewModel.process()
        }
    }
}

// Pattern 4: Weak self, then strong for duration
someAsync { [weak self] in
    guard let self = self else { return }
    
    // self is strong for duration of closure
    self.performLongOperation()
    self.updateUI()
    self.cleanup()
}

Property Observers & Wrappers

Property Observers

class StepCounter {
    var totalSteps: Int = 0 {
        willSet {
            print("About to set totalSteps to \(newValue)")
        }
        didSet {
            print("Added \(totalSteps - oldValue) steps")
            
            if totalSteps > oldValue {
                print("Made progress!")
            }
        }
    }
}

let counter = StepCounter()
counter.totalSteps = 100
// "About to set totalSteps to 100"
// "Added 100 steps"
// "Made progress!"

Computed Properties

struct Rectangle {
    var width: Double
    var height: Double
    
    // Computed property (no storage)
    var area: Double {
        get {
            return width * height
        }
        set {
            // newValue is implicit parameter
            width = sqrt(newValue)
            height = sqrt(newValue)
        }
    }
    
    // Read-only computed property
    var perimeter: Double {
        return 2 * (width + height)
    }
}

Lazy Properties

class ImageLoader {
    // Only created when first accessed
    lazy var cache: NSCache<NSURL, UIImage> = {
        let cache = NSCache<NSURL, UIImage>()
        cache.countLimit = 100
        return cache
    }()
    
    // Lazy with value type
    lazy var expensiveArray: [Int] = {
        return (0..<10000).map { $0 * $0 }
    }()
}

// Not created yet
let loader = ImageLoader()

// NOW cache is created
loader.cache.setObject(image, forKey: url)

Property Wrappers

// Define a property wrapper
@propertyWrapper
struct Clamped<Value: Comparable> {
    private var value: Value
    private let range: ClosedRange<Value>
    
    var wrappedValue: Value {
        get { value }
        set { value = min(max(newValue, range.lowerBound), range.upperBound) }
    }
    
    init(wrappedValue: Value, _ range: ClosedRange<Value>) {
        self.range = range
        self.value = min(max(wrappedValue, range.lowerBound), range.upperBound)
    }
}

// Use it
struct Audio {
    @Clamped(0...100) var volume: Int = 50
}

var audio = Audio()
audio.volume = 150  // Clamped to 100
audio.volume = -10  // Clamped to 0

// Built-in property wrappers in SwiftUI
@State private var count = 0
@Binding var isOn: Bool
@StateObject private var viewModel = ViewModel()
@Published var items: [String]

Common Property Wrapper Patterns

// UserDefaults wrapper
@propertyWrapper
struct UserDefault<T> {
    let key: String
    let defaultValue: T
    
    var wrappedValue: T {
        get {
            return UserDefaults.standard.object(forKey: key) as? T ?? defaultValue
        }
        set {
            UserDefaults.standard.set(newValue, forKey: key)
        }
    }
}

// Usage
struct Settings {
    @UserDefault(key: "username", defaultValue: "Guest")
    var username: String
    
    @UserDefault(key: "isDarkMode", defaultValue: false)
    var isDarkMode: Bool
}

var settings = Settings()
settings.username = "John"  // Automatically saves to UserDefaults

Extensions

Basic Extensions

// Add computed property
extension Int {
    var squared: Int {
        return self * self
    }
}

print(5.squared)  // 25

// Add methods
extension String {
    func trimmed() -> String {
        return self.trimmingCharacters(in: .whitespacesAndNewlines)
    }
    
    func isValidEmail() -> Bool {
        let emailRegex = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}"
        return NSPredicate(format: "SELF MATCHES %@", emailRegex).evaluate(with: self)
    }
}

let email = "  test@example.com  ".trimmed()
print(email.isValidEmail())  // true

Protocol Conformance

// Add protocol conformance retroactively
extension Int: CustomStringConvertible {
    public var description: String {
        return "The number \(self)"
    }
}

print(42)  // "The number 42"

// Conditional conformance
extension Array: Equatable where Element: Equatable {
    // Array is Equatable only when its elements are
}

Constrained Extensions

// Extend only specific types
extension Collection where Element == Int {
    func sum() -> Int {
        return reduce(0, +)
    }
}

[1, 2, 3].sum()  // 6
// ["a", "b"].sum()  // ❌ Error: String is not Int

// Extend protocol with constraints
extension Collection where Element: Comparable {
    func sorted() -> [Element] {
        return sorted(by: <)
    }
}

Extending Your Own Types

struct User {
    let firstName: String
    let lastName: String
}

// Group related functionality
extension User {
    var fullName: String {
        return "\(firstName) \(lastName)"
    }
    
    var initials: String {
        let first = firstName.prefix(1)
        let last = lastName.prefix(1)
        return "\(first)\(last)"
    }
}

// Conformance in extension (organizational)
extension User: Codable {}
extension User: Equatable {}
extension User: CustomStringConvertible {
    var description: String {
        return fullName
    }
}

Access Control

Access Levels

// open - Most permissive (can subclass/override from other modules)
open class OpenClass {
    open func openMethod() {}
}

// public - Accessible from other modules (can't subclass from other modules)
public class PublicClass {
    public func publicMethod() {}
}

// internal - Default, accessible within same module
internal class InternalClass {  // 'internal' keyword optional
    func internalMethod() {}
}

// fileprivate - Accessible within same file
fileprivate class FilePrivateClass {
    fileprivate func filePrivateMethod() {}
}

// private - Most restrictive, accessible within same declaration
private class PrivateClass {
    private func privateMethod() {}
}

Access Control Hierarchy

open > public > internal > fileprivate > private

Practical Usage

// Public API, internal implementation
public struct User {
    public let id: String
    public let name: String
    private var internalData: [String: Any] = [:]
    
    public init(id: String, name: String) {
        self.id = id
        self.name = name
    }
    
    // Public method
    public func displayName() -> String {
        return processName()  // Can call private method
    }
    
    // Private implementation detail
    private func processName() -> String {
        return name.uppercased()
    }
}

// Subclassing control
open class BaseViewController {
    open func setupUI() {}  // Can override from anywhere
}

public class SpecialViewController {
    public func setupUI() {}  // Can't override from other modules
}

// File-level sharing
class MyViewController {
    fileprivate var sharedData: String = ""
}

extension MyViewController {
    fileprivate func processData() {
        // Can access sharedData (same file)
    }
}

Enums & Pattern Matching

Basic Enums

enum Direction {
    case north
    case south
    case east
    case west
}

// Shorthand when type is known
var direction: Direction = .north
direction = .east

Associated Values

enum Result<Success, Failure: Error> {
    case success(Success)
    case failure(Failure)
}

// Different data for each case
enum Barcode {
    case upc(Int, Int, Int, Int)
    case qrCode(String)
}

let productBarcode = Barcode.upc(8, 85909, 51226, 3)
let qrBarcode = Barcode.qrCode("ABCDEFG")

// Extract values
switch productBarcode {
case .upc(let numberSystem, let manufacturer, let product, let check):
    print("UPC: \(numberSystem), \(manufacturer), \(product), \(check)")
case .qrCode(let code):
    print("QR code: \(code)")
}

// Shorthand
switch productBarcode {
case let .upc(numberSystem, manufacturer, product, check):
    print("UPC: \(numberSystem), \(manufacturer), \(product), \(check)")
case let .qrCode(code):
    print("QR code: \(code)")
}

Raw Values

// String raw values
enum Planet: String {
    case mercury = "Mercury"
    case venus = "Venus"
    case earth = "Earth"
}

print(Planet.earth.rawValue)  // "Earth"

// Int raw values (auto-increment)
enum Priority: Int {
    case low = 1
    case medium  // 2
    case high    // 3
}

// Initialize from raw value (returns optional)
if let priority = Priority(rawValue: 2) {
    print(priority)  // medium
}

Recursive Enums

indirect enum ArithmeticExpression {
    case number(Int)
    case addition(ArithmeticExpression, ArithmeticExpression)
    case multiplication(ArithmeticExpression, ArithmeticExpression)
}

// (5 + 4) * 2
let five = ArithmeticExpression.number(5)
let four = ArithmeticExpression.number(4)
let sum = ArithmeticExpression.addition(five, four)
let product = ArithmeticExpression.multiplication(sum, ArithmeticExpression.number(2))

func evaluate(_ expression: ArithmeticExpression) -> Int {
    switch expression {
    case let .number(value):
        return value
    case let .addition(left, right):
        return evaluate(left) + evaluate(right)
    case let .multiplication(left, right):
        return evaluate(left) * evaluate(right)
    }
}

print(evaluate(product))  // 18

Pattern Matching

let point = (x: 1, y: 2)

// Switch with tuples
switch point {
case (0, 0):
    print("Origin")
case (_, 0):
    print("On x-axis")
case (0, _):
    print("On y-axis")
case (-2...2, -2...2):
    print("Inside the box")
default:
    print("Outside the box")
}

// Value binding
switch point {
case (let x, 0):
    print("On x-axis at \(x)")
case (0, let y):
    print("On y-axis at \(y)")
case let (x, y):
    print("At (\(x), \(y))")
}

// Where clause
switch point {
case let (x, y) where x == y:
    print("On the diagonal")
case let (x, y) where x == -y:
    print("On the negative diagonal")
case let (x, y):
    print("At (\(x), \(y))")
}

If case / guard case

enum Result {
    case success(String)
    case failure(Error)
}

let result = Result.success("Data loaded")

// Extract associated value with if case
if case let .success(message) = result {
    print(message)  // "Data loaded"
}

// Guard with pattern matching
func handle(result: Result) {
    guard case let .success(message) = result else {
        print("Failed")
        return
    }
    print("Success: \(message)")
}

Modern Concurrency (async/await)

Basic async/await

// Async function
func fetchUser(id: String) async throws -> User {
    let url = URL(string: "https://api.example.com/users/\(id)")!
    let (data, _) = try await URLSession.shared.data(from: url)
    let user = try JSONDecoder().decode(User.self, from: data)
    return user
}

// Calling async function
Task {
    do {
        let user = try await fetchUser(id: "123")
        print("Fetched: \(user.name)")
    } catch {
        print("Error: \(error)")
    }
}

Tasks

// Create a task
let task = Task {
    let user = try await fetchUser(id: "123")
    return user
}

// Get result
let user = try await task.value

// Cancel task
task.cancel()

// Check if cancelled
Task {
    guard !Task.isCancelled else { return }
    let data = try await fetchData()
    processData(data)
}

// Detached task (doesn't inherit context)
Task.detached {
    // Runs independently
}

Task Groups

// Parallel execution
func fetchMultipleUsers(ids: [String]) async throws -> [User] {
    try await withThrowingTaskGroup(of: User.self) { group in
        for id in ids {
            group.addTask {
                try await fetchUser(id: id)
            }
        }
        
        var users: [User] = []
        for try await user in group {
            users.append(user)
        }
        return users
    }
}

// Usage
let users = try await fetchMultipleUsers(ids: ["1", "2", "3"])

Actors

// Thread-safe class
actor BankAccount {
    private var balance: Double = 0
    
    func deposit(amount: Double) {
        balance += amount
    }
    
    func withdraw(amount: Double) -> Bool {
        guard balance >= amount else { return false }
        balance -= amount
        return true
    }
    
    func getBalance() -> Double {
        return balance
    }
}

// Usage (all methods are async)
let account = BankAccount()

Task {
    await account.deposit(amount: 100)
    let balance = await account.getBalance()
    print(balance)  // 100
}

@MainActor

// Ensure code runs on main thread
@MainActor
class ViewModel: ObservableObject {
    @Published var users: [User] = []
    
    func loadUsers() async {
        let users = try? await fetchUsers()
        self.users = users ?? []  // Safely updates on main thread
    }
}

// Mark specific functions
class MyClass {
    @MainActor
    func updateUI() {
        // Always runs on main thread
    }
}

// Use in closures
Task { @MainActor in
    // Runs on main thread
    label.text = "Updated"
}

AsyncSequence

// Async iteration
for try await line in URL(string: "https://example.com/file.txt")!.lines {
    print(line)
}

// Custom AsyncSequence
struct Counter: AsyncSequence {
    typealias Element = Int
    let limit: Int
    
    struct AsyncIterator: AsyncIteratorProtocol {
        let limit: Int
        var current = 0
        
        mutating func next() async -> Int? {
            guard current < limit else { return nil }
            current += 1
            try? await Task.sleep(nanoseconds: 1_000_000_000)  // 1 second
            return current
        }
    }
    
    func makeAsyncIterator() -> AsyncIterator {
        AsyncIterator(limit: limit)
    }
}

// Usage
for await number in Counter(limit: 5) {
    print(number)  // 1, 2, 3, 4, 5 (one per second)
}

Sendable Protocol & Data Race Safety

The Sendable protocol (Swift 5.5+) marks types that can be safely shared across concurrent contexts without causing data races. Swift 6.0 enforces strict concurrency checking by default.

Understanding Sendable

// Sendable types are safe to share across threads
protocol Sendable {}

// Most value types are implicitly Sendable
struct User: Sendable {  // Automatic conformance
    let name: String
    let age: Int
}

// Reference types need explicit conformance
final class Counter: @unchecked Sendable {
    // @unchecked means you're responsible for thread safety
    private let lock = NSLock()
    private var value = 0

    func increment() {
        lock.lock()
        defer { lock.unlock() }
        value += 1
    }
}

When Types are Sendable

// ✅ Automatically Sendable:
// - Basic value types (Int, String, Bool, etc.)
// - Structs with all Sendable properties
// - Enums with Sendable associated values
// - Immutable classes with Sendable properties
// - Actors (always Sendable)

struct Location: Sendable {  // ✅ Auto-conforms
    let latitude: Double
    let longitude: Double
}

enum Status: Sendable {  // ✅ Auto-conforms
    case active
    case inactive(reason: String)
}

final class ImmutableUser: Sendable {  // ✅ Can conform
    let name: String
    let id: UUID

    init(name: String, id: UUID) {
        self.name = name
        self.id = id
    }
}

// ❌ Not automatically Sendable:
// - Classes with mutable state
// - Structs/enums containing non-Sendable types
// - Closures capturing non-Sendable values

class MutableUser {  // ❌ Cannot be Sendable
    var name: String = ""
    var loginCount: Int = 0
}

Sendable Closures

// @Sendable marks closures that can be shared safely
func performAsync(work: @Sendable @escaping () -> Void) {
    Task.detached {
        work()  // Safe to call from any thread
    }
}

// Using @Sendable closures
func example() {
    let immutableValue = 42  // Captured values must be Sendable

    performAsync { @Sendable in
        print(immutableValue)  // ✅ Int is Sendable
    }

    var mutableValue = 0
    performAsync { @Sendable in
        // ❌ Error: Capture of 'mutableValue' with non-sendable type
        // print(mutableValue)
    }
}

Sendable with async/await

// Functions taking Sendable parameters
func process(user: sending User) async {
    // 'sending' keyword ensures the value is sent, not shared
    await saveToDatabase(user)
}

// Actor isolation and Sendable
actor DataStore {
    private var cache: [String: any Sendable] = [:]

    func store<T: Sendable>(_ value: T, key: String) {
        cache[key] = value
    }

    func retrieve<T: Sendable>(_ type: T.Type, key: String) -> T? {
        cache[key] as? T
    }
}

Conditional Sendable Conformance

// Generic types can conditionally conform
struct Box<T> {
    let value: T
}

// Box is Sendable only if T is Sendable
extension Box: Sendable where T: Sendable {}

// Usage
let intBox = Box(value: 42)  // ✅ Sendable
let userBox = Box(value: MutableUser())  // ❌ Not Sendable

@unchecked Sendable

// Use when you guarantee thread safety manually
final class ThreadSafeCache: @unchecked Sendable {
    private var storage: [String: Any] = [:]
    private let queue = DispatchQueue(label: "cache.queue",
                                     attributes: .concurrent)

    func set(_ value: Any, for key: String) {
        queue.async(flags: .barrier) {
            self.storage[key] = value
        }
    }

    func get(_ key: String) -> Any? {
        queue.sync {
            storage[key]
        }
    }
}

Global Variables and Sendable

// Global variables must be Sendable in strict concurrency
let globalConfig = Configuration()  // ✅ If Configuration is Sendable

// Use @unchecked for legacy code
@unchecked Sendable
var legacyGlobalState: LegacyClass?  // Be careful!

Best Practices

// 1. Prefer value types for concurrent code
struct Message: Sendable {
    let id: UUID
    let text: String
    let timestamp: Date
}

// 2. Use actors for mutable shared state
actor MessageQueue {
    private var messages: [Message] = []

    func enqueue(_ message: Message) {
        messages.append(message)
    }

    func dequeue() -> Message? {
        messages.isEmpty ? nil : messages.removeFirst()
    }
}

// 3. Mark completion handlers as @Sendable
func fetchData(completion: @Sendable @escaping (Result<Data, Error>) -> Void) {
    Task.detached {
        do {
            let data = try await loadData()
            completion(.success(data))
        } catch {
            completion(.failure(error))
        }
    }
}

// 4. Use sending parameters for ownership transfer
func transfer(data: sending Data) async {
    // Takes ownership of data, original can't be used
    await process(data)
}

Common Errors and Solutions

// Error: Type does not conform to Sendable
struct BadExample {
    let closure: () -> Void  // ❌ Closures aren't automatically Sendable
}

// Solution: Mark closure as @Sendable
struct GoodExample: Sendable {
    let closure: @Sendable () -> Void  // ✅
}

// Error: Capture of non-Sendable type
class ViewModel {
    var count = 0

    func startTimer() {
        Task.detached {
            // ❌ Error: Capture of 'self' with non-sendable type
            self.count += 1
        }
    }
}

// Solution: Use actor or ensure Sendable
actor ViewModelActor {
    var count = 0

    func startTimer() {
        Task.detached {
            await self.incrementCount()  // ✅ Actor is Sendable
        }
    }

    func incrementCount() {
        count += 1
    }
}

Swift 6.0 New Features (2024-2025)

Typed Throws

// Define specific error types
enum ValidationError: Error {
    case tooShort
    case tooLong
    case invalidFormat
}

// Function can only throw ValidationError
func validate(_ input: String) throws(ValidationError) -> Bool {
    guard input.count >= 3 else {
        throw ValidationError.tooShort
    }
    guard input.count <= 20 else {
        throw ValidationError.tooLong
    }
    return true
}

// Compiler knows the error type
do {
    try validate("ab")
} catch {
    // error is ValidationError, not Error
    switch error {
    case .tooShort:
        print("Input too short")
    case .tooLong:
        print("Input too long")
    case .invalidFormat:
        print("Invalid format")
    }
}

Complete Concurrency Checking

// Swift 6.0 enables strict concurrency by default
// Compile with: -strict-concurrency=complete

class LegacyClass {  // Warning: not Sendable
    var counter = 0
}

// Swift 6.0 will warn about data races
func problematicCode() {
    let legacy = LegacyClass()

    Task {
        legacy.counter += 1  // ⚠️ Data race warning
    }

    Task {
        print(legacy.counter)  // ⚠️ Data race warning
    }
}

// Fix with actor or @MainActor
@MainActor
class SafeClass {
    var counter = 0
}

Noncopyable Types

// Resource that shouldn't be copied
@noncopyable
struct UniqueResource {
    private let handle: Int

    init() {
        handle = createResource()
    }

    deinit {
        destroyResource(handle)
    }

    // Use 'consuming' to transfer ownership
    consuming func take() -> UniqueResource {
        return self
    }

    // Use 'borrowing' for read-only access
    borrowing func read() -> Int {
        return handle
    }
}

// Usage
func useResource() {
    let resource = UniqueResource()
    let value = resource.read()  // Borrowing
    let transferred = resource.take()  // Consuming - resource no longer usable
    // print(resource.handle)  // ❌ Error: resource was consumed
}

Parameter Packs (Variadic Generics)

// Work with multiple types at once
func zip<each T, each U>(_ first: repeat each T, with second: repeat each U) -> (repeat (each T, each U)) {
    return (repeat (each first, each second))
}

// Usage
let result = zip(1, "hello", true, with: 2.0, "world", false)
// result is ((Int, Double), (String, String), (Bool, Bool))

Improved Existential Types

// Swift 6.0 allows 'any' keyword for clarity
protocol Drawable {
    func draw()
}

// Old way (still works)
let shapes: [Drawable] = []

// New way (clearer)
let shapes: [any Drawable] = []

// Use 'some' for opaque types
func makeShape() -> some Drawable {
    return Circle()
}

Collection Operators

Map, Filter, Reduce

let numbers = [1, 2, 3, 4, 5]

// map - transform each element
let doubled = numbers.map { $0 * 2 }  // [2, 4, 6, 8, 10]

// filter - keep elements that match condition
let evens = numbers.filter { $0 % 2 == 0 }  // [2, 4]

// reduce - combine elements into single value
let sum = numbers.reduce(0, +)  // 15
let product = numbers.reduce(1, *)  // 120

// reduce with closure
let concatenated = numbers.reduce("") { result, number in
    return result + String(number)
}  // "12345"

CompactMap

// Remove nils and unwrap
let strings = ["1", "2", "three", "4"]
let numbers = strings.compactMap { Int($0) }  // [1, 2, 4]

// vs map (keeps nils)
let maybeNumbers = strings.map { Int($0) }  // [Optional(1), Optional(2), nil, Optional(4)]

FlatMap

// Flatten nested arrays
let nested = [[1, 2], [3, 4], [5, 6]]
let flattened = nested.flatMap { $0 }  // [1, 2, 3, 4, 5, 6]

// Combine map + flatten
let words = ["Hello", "World"]
let letters = words.flatMap { $0 }  // ["H", "e", "l", "l", "o", "W", "o", "r", "l", "d"]

Chaining

let numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

let result = numbers
    .filter { $0 % 2 == 0 }     // [2, 4, 6, 8, 10]
    .map { $0 * $0 }             // [4, 16, 36, 64, 100]
    .reduce(0, +)                // 220

Other Useful Methods

let numbers = [1, 2, 3, 4, 5]

// contains
numbers.contains(3)  // true
numbers.contains { $0 > 3 }  // true

// first(where:)
numbers.first { $0 > 3 }  // Optional(4)

// allSatisfy
numbers.allSatisfy { $0 > 0 }  // true

// sorted
numbers.sorted()  // [1, 2, 3, 4, 5]
numbers.sorted(by: >)  // [5, 4, 3, 2, 1]

// prefix/suffix
Array(numbers.prefix(3))  // [1, 2, 3]
Array(numbers.suffix(2))  // [4, 5]

// dropFirst/dropLast
Array(numbers.dropFirst(2))  // [3, 4, 5]
Array(numbers.dropLast(2))  // [1, 2, 3]

Swift vs Objective-C Key Differences

Nullability

// Objective-C
NSString *name;  // Could be nil
NSString *_Nullable optionalName;  // Explicitly optional
NSString *_Nonnull requiredName;    // Can't be nil

// Swift
var name: String  // Never nil
var optionalName: String?  // Can be nil

Method Calls

// Objective-C
[object methodWithParameter:value];
[object methodWithFirst:value1 second:value2];

// Swift
object.method(parameter: value)
object.method(first: value1, second: value2)

Properties

// Objective-C
@property (nonatomic, strong) NSString *name;
@property (nonatomic, weak) id<MyDelegate> delegate;
@property (nonatomic, copy) NSString *title;

// Swift
var name: String  // Strong by default
weak var delegate: MyDelegate?
var title: String  // Value type, no need for copy

Blocks vs Closures

// Objective-C
typedef void (^CompletionHandler)(NSString *result, NSError *error);

- (void)fetchDataWithCompletion:(CompletionHandler)completion {
    completion(@"Data", nil);
}

// Swift
typealias CompletionHandler = (String?, Error?) -> Void

func fetchData(completion: @escaping CompletionHandler) {
    completion("Data", nil)
}

Collections

// Objective-C (mutable vs immutable)
NSArray *immutable = @[@"a", @"b"];
NSMutableArray *mutable = [NSMutableArray arrayWithArray:immutable];

// Swift (let vs var)
let immutable = ["a", "b"]  // Immutable array
var mutable = ["a", "b"]    // Mutable array

Protocols

// Objective-C
@protocol MyProtocol <NSObject>
@required
- (void)requiredMethod;
@optional
- (void)optionalMethod;
@end

// Swift
protocol MyProtocol {
    func requiredMethod()
}

// Optional methods via extension
extension MyProtocol {
    func optionalMethod() {
        // Default implementation
    }
}

Interview Questions

Q: “What’s the difference between class and struct?”

  • Struct: Value type, copied on assignment, no inheritance, no deinit
  • Class: Reference type, shared on assignment, inheritance, has deinit

Q: “When would you use map vs flatMap vs compactMap?”

  • map: Transform each element (keeps structure)
  • flatMap: Transform and flatten nested collections
  • compactMap: Transform and remove nils

Q: “Explain weak vs unowned”

  • weak: Optional, automatically becomes nil when deallocated
  • unowned: Non-optional, crashes if accessed after deallocation
  • Use weak when reference might become nil, unowned when it never will

Q: “What’s the difference between @escaping and non-escaping closures?”

  • Non-escaping (default): Executes before function returns
  • @escaping: May execute after function returns (needs annotation)

Q: “How do you prevent retain cycles?”

  • Use [weak self] in closures
  • Use weak for delegates
  • Use unowned for guaranteed lifetime relationships
  • Avoid strong reference cycles in closures

Quick Reference Card

// Optionals
var optional: String?
if let unwrapped = optional { }
guard let unwrapped = optional else { return }
let value = optional ?? "default"

// Value vs Reference
struct ValueType { }  // Copied
class ReferenceType { }  // Shared

// Closures
{ (params) -> ReturnType in
    // body
}
{ $0 + $1 }  // Shorthand

// Memory
weak var delegate: Delegate?
unowned let parent: Parent
[weak self] in { }

// Collections
array.map { }
array.filter { }
array.reduce(0, +)
array.compactMap { }

// Async
async throws -> Value
try await function()
Task { }
@MainActor

// Error Handling
do { try } catch { }
try? optional()
try! force()

This covers the essential Swift features you’ll need. Focus on optionals, value/reference types, and closures - these are where most Objective-C developers trip up initially.