November 22, 2025
12 min read

Junior iOS Developer Interview Questions: Swift, UIKit, SwiftUI

interview
career-advice
job-search
entry-level
Junior iOS Developer Interview Questions: Swift, UIKit, SwiftUI
Milad Bonakdar

Milad Bonakdar

Author

Practice junior iOS interview questions on Swift, UIKit, SwiftUI, networking, persistence, and async code, with concise answers for first mobile roles.


Introduction

For a junior iOS developer interview, expect questions that test whether you can explain Swift fundamentals, build simple UI in UIKit or SwiftUI, fetch and decode data, choose basic persistence, and avoid common memory or threading mistakes. The best answers are short, concrete, and tied to how you would build a small app.

Use this guide to practice the questions most likely to appear in an entry-level iOS screen: what the concept means, when you would use it, and what risk a junior developer should watch for.


Swift Fundamentals (6 Questions)

1. What is the difference between var and let in Swift?

Answer:

  • var: Declares a mutable variable. Its value can be changed after initialization.
  • let: Declares an immutable constant. Once set, its value cannot be changed.
  • Best Practice: Use let by default for safety and clarity. Only use var when you know the value will change.
let name = "John"  // Cannot be changed
var age = 25       // Can be changed
age = 26           // Valid
// name = "Jane"   // Error: Cannot assign to value

Rarity: Very Common Difficulty: Easy


2. Explain Optionals in Swift. What is Optional Binding?

Answer: Optionals represent a value that might be nil (absence of a value).

  • Declaration: Use ? after the type: var name: String?
  • Unwrapping Methods:
    • Force Unwrapping: name! (dangerous, crashes if nil)
    • Optional Binding: Safely unwrap using if let or guard let
    • Nil Coalescing: name ?? "Default"
    • Optional Chaining: user?.address?.city
var email: String? = "[email protected]"

// Optional Binding
if let unwrappedEmail = email {
    print("Email: \(unwrappedEmail)")
} else {
    print("No email")
}

// Guard statement
guard let email = email else { return }
print(email)

Rarity: Very Common Difficulty: Easy


3. What is the difference between a class and a struct in Swift?

Answer:

  • Class: Reference type (stored on heap). Supports inheritance. Passed by reference.
  • Struct: Value type (stored on stack). No inheritance. Passed by copy.
  • When to use:
    • Struct: For simple data models, when you want value semantics (independent copies)
    • Class: When you need inheritance, reference semantics, or deinitializers
struct Point {
    var x: Int
    var y: Int
}

class Person {
    var name: String
    init(name: String) { self.name = name }
}

var p1 = Point(x: 0, y: 0)
var p2 = p1  // Copy created
p2.x = 10    // p1.x is still 0

var person1 = Person(name: "John")
var person2 = person1  // Same reference
person2.name = "Jane"  // person1.name is also "Jane"

Rarity: Very Common Difficulty: Medium


4. What are Closures in Swift?

Answer: Closures are self-contained blocks of functionality that can be passed around and used in your code. They're similar to lambdas or anonymous functions in other languages.

  • Syntax: { (parameters) -> ReturnType in statements }
  • Capturing Values: Closures can capture and store references to variables from their surrounding context.
  • Common Use: Completion handlers, callbacks, array operations.
// Simple closure
let greet = { (name: String) -> String in
    return "Hello, \(name)!"
}
print(greet("Alice"))

// Trailing closure syntax
let numbers = [1, 2, 3, 4, 5]
let doubled = numbers.map { $0 * 2 }
print(doubled)  // [2, 4, 6, 8, 10]

Rarity: Very Common Difficulty: Medium


5. Explain the difference between weak and unowned references.

Answer: Both are used to prevent retain cycles (memory leaks) in reference types.

  • weak: Optional reference that automatically becomes nil when the referenced object is deallocated. Use when the reference might outlive the object.
  • unowned: Non-optional reference that assumes the object will always exist. Crashes if accessed after deallocation. Use when you're certain the reference won't outlive the object.
class Person {
    var name: String
    weak var apartment: Apartment?  // weak to prevent retain cycle
    init(name: String) { self.name = name }
}

class Apartment {
    var unit: String
    unowned let tenant: Person  // unowned - apartment always has tenant
    init(unit: String, tenant: Person) {
        self.unit = unit
        self.tenant = tenant
    }
}

Rarity: Common Difficulty: Medium


6. What are Protocols in Swift?

Answer: Protocols define a blueprint of methods, properties, and other requirements that suit a particular task or piece of functionality. Classes, structs, and enums can adopt protocols.

  • Similar to: Interfaces in other languages
  • Protocol Extensions: Can provide default implementations
  • Protocol-Oriented Programming: Swift's preferred paradigm
protocol Drawable {
    func draw()
}

struct Circle: Drawable {
    var radius: Double
    
    func draw() {
        print("Drawing circle with radius \(radius)")
    }
}

struct Rectangle: Drawable {
    var width: Double
    var height: Double
    
    func draw() {
        print("Drawing rectangle \(width)x\(height)")
    }
}

Rarity: Very Common Difficulty: Easy


UIKit Basics (5 Questions)

7. What is the difference between UIView and UIViewController?

Answer:

  • UIView: Represents a rectangular area on the screen. Handles drawing and event handling. Examples: UILabel, UIButton, UIImageView.
  • UIViewController: Manages a view hierarchy. Handles view lifecycle, user interactions, and navigation. Contains one root view and manages its subviews.
  • Relationship: A view controller manages views. One view controller can have many views.

Rarity: Very Common Difficulty: Easy


8. Explain the View Controller lifecycle methods.

Answer: View controllers have specific lifecycle methods called in order:

  1. viewDidLoad(): Called once when the view is loaded into memory. Setup that needs to happen once.
  2. viewWillAppear(_:): Called before the view appears on screen. Can be called multiple times.
  3. viewDidAppear(_:): Called after the view appears on screen. Start animations here.
  4. viewWillDisappear(_:): Called before the view disappears.
  5. viewDidDisappear(_:): Called after the view disappears. Clean up resources.
override func viewDidLoad() {
    super.viewDidLoad()
    // One-time setup: configure UI, load data
}

override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    // Refresh data, start listening to notifications
}

Rarity: Very Common Difficulty: Easy


9. What is Auto Layout and how do you use constraints?

Answer: Auto Layout is a constraint-based layout system that allows you to create adaptive UIs that work across different screen sizes and orientations.

  • Constraints: Define relationships between views (width, height, spacing, alignment)
  • Methods:
    • Interface Builder: Visual editor in Xcode
    • Programmatic: NSLayoutConstraint or NSLayoutAnchor API
    • Visual Format Language: String-based (less common now)
let button = UIButton()
button.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(button)

NSLayoutConstraint.activate([
    button.centerXAnchor.constraint(equalTo: view.centerXAnchor),
    button.centerYAnchor.constraint(equalTo: view.centerYAnchor),
    button.widthAnchor.constraint(equalToConstant: 200),
    button.heightAnchor.constraint(equalToConstant: 50)
])

Rarity: Very Common Difficulty: Medium


10. What is the difference between frame and bounds?

Answer:

  • frame: The view's location and size in its superview's coordinate system. Used for positioning the view.
  • bounds: The view's location and size in its own coordinate system. Origin is usually (0, 0). Used for drawing content inside the view.
  • Key Difference: frame is relative to parent, bounds is relative to itself.
let view = UIView(frame: CGRect(x: 50, y: 100, width: 200, height: 150))
print(view.frame)   // (50, 100, 200, 150) - position in superview
print(view.bounds)  // (0, 0, 200, 150) - own coordinate system

Rarity: Common Difficulty: Medium


11. How do you handle user input from a UITextField?

Answer: Multiple approaches exist:

  • Target-Action: Add target for .editingChanged event
  • Delegate: Conform to UITextFieldDelegate protocol
  • Combine: Use publishers (modern approach)
class ViewController: UIViewController, UITextFieldDelegate {
    let textField = UITextField()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // Delegate approach
        textField.delegate = self
        
        // Target-Action approach
        textField.addTarget(self, action: #selector(textChanged), for: .editingChanged)
    }
    
    @objc func textChanged(_ textField: UITextField) {
        print("Text: \(textField.text ?? "")")
    }
    
    // Delegate method
    func textFieldShouldReturn(_ textField: UITextField) -> Bool {
        textField.resignFirstResponder()
        return true
    }
}

Rarity: Very Common Difficulty: Easy


SwiftUI Basics (4 Questions)

12. What is SwiftUI and how is it different from UIKit?

Answer: SwiftUI is Apple's declarative UI framework. You describe the interface as a function of state, and SwiftUI updates the rendered view when that state changes. UIKit is imperative: you create and mutate view objects such as UIView, UILabel, and UIViewController directly.

For a junior interview, do not frame this as SwiftUI replacing UIKit everywhere. Many production apps still mix both. A strong answer is: SwiftUI is concise for new screens and state-driven UI; UIKit remains important for older codebases, custom controls, and APIs that are not fully covered by SwiftUI.

struct CounterView: View {
    @State private var count = 0

    var body: some View {
        VStack {
            Text("Count: \(count)")
            Button("Increment") {
                count += 1
            }
        }
    }
}

Rarity: Very Common Difficulty: Easy


13. Explain @State, @Binding, and @ObservedObject in SwiftUI.

Answer: These wrappers describe where state lives and who is allowed to change it.

  • @State: Private state owned by one view, usually for simple local UI state.
  • @Binding: A two-way connection to state owned by a parent view. The child can read and write it without owning it.
  • @ObservedObject: A reference to external observable model data. Use it when another object owns the data and the view should refresh when that object changes. In newer SwiftUI code, you may also see the Observation framework and @Observable, but many interviews still ask about these wrappers.
class UserData: ObservableObject {
    @Published var name = ""
}

struct ParentView: View {
    @State private var isOn = false
    @ObservedObject var userData = UserData()

    var body: some View {
        VStack {
            ChildView(isOn: $isOn)
            Text(userData.name)
        }
    }
}

struct ChildView: View {
    @Binding var isOn: Bool

    var body: some View {
        Toggle("Switch", isOn: $isOn)
    }
}

Rarity: Common Difficulty: Medium


14. How do you create a list in SwiftUI?

Answer: Use the List view to display scrollable collections of data.

struct ContentView: View {
    let fruits = ["Apple", "Banana", "Orange", "Grape"]
    
    var body: some View {
        List(fruits, id: \.self) { fruit in
            Text(fruit)
        }
    }
}

// With custom model
struct Task: Identifiable {
    let id = UUID()
    let title: String
}

struct TaskListView: View {
    let tasks = [
        Task(title: "Buy groceries"),
        Task(title: "Walk the dog")
    ]
    
    var body: some View {
        List(tasks) { task in
            Text(task.title)
        }
    }
}

Rarity: Common Difficulty: Easy


15. What are View Modifiers in SwiftUI?

Answer: View modifiers are methods that create a new view with modified properties. They're chainable.

  • Common Modifiers: .padding(), .background(), .foregroundColor(), .font(), .frame()
  • Order Matters: Modifiers are applied in sequence
Text("Hello, World!")
    .font(.title)
    .foregroundColor(.blue)
    .padding()
    .background(Color.yellow)
    .cornerRadius(10)
    .shadow(radius: 5)

// Custom modifier
struct PrimaryButtonStyle: ViewModifier {
    func body(content: Content) -> some View {
        content
            .padding()
            .background(Color.blue)
            .foregroundColor(.white)
            .cornerRadius(8)
    }
}

extension View {
    func primaryButton() -> some View {
        modifier(PrimaryButtonStyle())
    }
}

Rarity: Common Difficulty: Easy


Data & Networking (5 Questions)

16. How do you make a network request in iOS?

Answer: Use URLSession. In modern Swift, a junior-friendly answer can show async/await, then mention that older codebases may still use completion handlers. A good production answer also validates the HTTP response, handles errors, decodes into Codable models, and updates UI on the main actor.

struct User: Codable {
    let id: Int
    let name: String
}

func fetchUser(id: Int) async throws -> User {
    let url = URL(string: "https://api.example.com/users/\(id)")!
    let (data, response) = try await URLSession.shared.data(from: url)

    guard let http = response as? HTTPURLResponse, (200...299).contains(http.statusCode) else {
        throw URLError(.badServerResponse)
    }

    return try JSONDecoder().decode(User.self, from: data)
}

Rarity: Very Common Difficulty: Medium


17. What is Codable in Swift?

Answer: Codable is a type alias for Encodable & Decodable protocols. It allows easy conversion between Swift types and external representations like JSON.

  • Automatic Synthesis: Swift automatically generates encoding/decoding code if all properties are Codable.
  • Custom Keys: Use CodingKeys enum to map different property names.
struct User: Codable {
    let id: Int
    let name: String
    let email: String
}

// Decoding JSON
let json = """
{
    "id": 1,
    "name": "John Doe",
    "email": "[email protected]"
}
""".data(using: .utf8)!

let decoder = JSONDecoder()
let user = try decoder.decode(User.self, from: json)

// Encoding to JSON
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
let jsonData = try encoder.encode(user)

Rarity: Very Common Difficulty: Easy


18. How do you persist data locally in iOS?

Answer: Multiple options for local data persistence:

  • UserDefaults: Simple key-value storage for small amounts of data (settings, preferences)
  • File System: Save files to Documents or Cache directory
  • Core Data: Apple's framework for object graph management and persistence
  • SQLite: Relational database
  • Keychain: Secure storage for sensitive data (passwords, tokens)
// UserDefaults
UserDefaults.standard.set("John", forKey: "username")
let username = UserDefaults.standard.string(forKey: "username")

// File System
let documentsPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
let filePath = documentsPath.appendingPathComponent("data.json")

// Write
try data.write(to: filePath)

// Read
let loadedData = try Data(contentsOf: filePath)

Rarity: Very Common Difficulty: Easy


19. What is the difference between synchronous and asynchronous operations?

Answer: A synchronous operation blocks the current thread until it finishes. That is dangerous on the main thread because the app can stop responding. An asynchronous operation starts work and lets the current thread continue; the result arrives later through async/await, a completion handler, a delegate, or a publisher.

For iOS interviews, connect the concept to UI responsiveness: network calls, file work, and heavy processing should not block the main thread, and UI changes should run on the main actor.

func loadProfile() async {
    do {
        let user = try await fetchUser(id: 42)
        await MainActor.run {
            self.nameLabel.text = user.name
        }
    } catch {
        await MainActor.run {
            self.errorLabel.text = "Could not load profile"
        }
    }
}

Rarity: Common Difficulty: Easy


20. What is Grand Central Dispatch (GCD)?

Answer: GCD is Apple's lower-level API for scheduling work on dispatch queues. The main queue is used for UI work, and global queues can run background work with different quality-of-service priorities.

In newer Swift code, you will often use Swift concurrency (async/await, Task, and actors) first because it makes asynchronous code easier to read. GCD still matters because many UIKit projects, older codebases, and callback-based APIs use DispatchQueue.main.async or background queues.

DispatchQueue.global(qos: .userInitiated).async {
    let image = renderThumbnail()

    DispatchQueue.main.async {
        self.imageView.image = image
    }
}

Rarity: Common Difficulty: Medium


Newsletter subscription

Weekly career tips that actually work

Get the latest insights delivered straight to your inbox

Your Next Interview is Just One Resume Away

Create a professional, optimized resume in minutes. No design skills needed—just proven results.

Create my resume

Share this post

Cut Your Resume Writing Time by 90%

The average job seeker spends 3+ hours formatting a resume. Our AI does it in under 15 minutes, getting you to the application phase 12x faster.