Popular Searches
Popular Course Categories
Popular Courses

Solving Swift Concurrency, API Integration, and Database Interview Problems

Solving Swift Concurrency, API Integration, and Database Interview Problems

iOS App Development Portfolio and Interview Preparation

Solving Swift Concurrency, API Integration, and Database Interview Problems

Course: iOS App Development

Topic: Solving Swift Concurrency, API Integration, and Database Interview Problems

Course Link: iOS App Development Course

Demo Link: Register for a Course Demo

Website: JustAcademy


1. Introduction to iOS Development Interview Problems

Swift concurrency, API integration, and database management are important areas of iOS application development. Interviewers frequently evaluate candidates through practical coding questions that test their ability to write asynchronous code, communicate with remote servers, manage local data, and handle errors.

These concepts are closely connected in real-world applications. For example, a shopping application may fetch product information from an API, save selected products in a local database, and update the user interface when asynchronous operations finish.

Key Areas Covered

  • Swift concurrency using async/await and Task.
  • Managing asynchronous operations and task cancellation.
  • Understanding actors and data-race prevention.
  • API integration using URLSession.
  • JSON encoding and decoding using Codable.
  • Handling network errors and HTTP responses.
  • Database operations using Core Data and SwiftData.
  • CRUD operations and data persistence.
  • Writing practical interview solutions and debugging code.

2. Understanding Swift Concurrency

Swift concurrency provides a structured way to execute asynchronous operations without blocking the main thread. It is especially useful when an application needs to perform network requests, load files, process data, or communicate with databases.

Why Concurrency Is Important

  • Prevents long-running operations from freezing the user interface.
  • Allows multiple independent tasks to make progress efficiently.
  • Improves responsiveness during API requests.
  • Provides safer ways to manage shared mutable data.
  • Makes asynchronous code easier to read and maintain.

Concurrency vs. Parallelism

Concept Meaning Example
Concurrency Multiple tasks make progress during overlapping periods. Loading images while displaying a screen.
Parallelism Multiple operations execute simultaneously on different processing resources. Processing independent data chunks on multiple CPU cores.
Asynchronous Programming An operation can suspend while waiting for a result. Waiting for an API response without blocking the main thread.
Synchronous Programming Operations execute in sequence, with each operation waiting for the previous one to finish. Reading a value and then immediately processing it.

3. Understanding async and await

The async keyword marks a function that can perform asynchronous work. The await keyword indicates a point where execution may suspend until an asynchronous operation produces a result.

Example: Basic Async Function

import Foundation

func fetchUsername() async -> String {
    return "Alex"
}

func displayUsername() async {
    let username = await fetchUsername()
    print("Username: \(username)")
}

Explanation

  • async indicates that the function supports asynchronous execution.
  • await waits for the asynchronous function's result.
  • The calling function must also support asynchronous execution when it directly awaits an async function.
  • An async function does not automatically run on a background thread.

Interview Problem 1: Convert a Synchronous Function into an Async Function

Problem: Create an asynchronous function that simulates fetching user information and returns the user's name.

import Foundation

func getUserDetails() async -> String {
    try? await Task.sleep(for: .seconds(2))
    return "John Smith"
}

func loadUser() async {
    let user = await getUserDetails()
    print(user)
}

Expected Output:

John Smith

Concept Tested: Async function declarations, suspension points, and awaiting asynchronous results.

4. Understanding Task in Swift

A Task creates an asynchronous unit of work that can execute independently of the current synchronous flow. It allows developers to start asynchronous operations from contexts that are not already asynchronous, such as button actions.

Example: Creating a Task

import SwiftUI

struct UserView: View {
    @State private var username = "Loading..."

    var body: some View {
        VStack {
            Text(username)

            Button("Load User") {
                Task {
                    username = await fetchUser()
                }
            }
        }
    }

    func fetchUser() async -> String {
        try? await Task.sleep(for: .seconds(2))
        return "Alex"
    }
}

SwiftUI button actions are synchronous closures. Creating a Task allows the action to start asynchronous work without blocking the interface.

Important Task Concepts

  • Task: Starts an asynchronous operation.
  • Task Priority: Expresses the relative importance of a task.
  • Task Cancellation: Requests that a task stop its work.
  • Task Result: Represents the success or failure of an asynchronous operation.
  • Structured Concurrency: Organizes child tasks under a parent task with defined lifetime relationships.

5. Interview Problem 2: Run Multiple Tasks Concurrently

Problem: Fetch user details and product details concurrently instead of waiting for one request to finish before starting the other.

Solution Using async let

import Foundation

func fetchUser() async -> String {
    try? await Task.sleep(for: .seconds(2))
    return "User Data"
}

func fetchProducts() async -> String {
    try? await Task.sleep(for: .seconds(2))
    return "Product Data"
}

func loadDashboard() async {
    async let user = fetchUser()
    async let products = fetchProducts()

    let results = await (user, products)

    print(results.0)
    print(results.1)
}

Explanation

async let starts child asynchronous operations that can make progress concurrently. The application awaits both results when they are needed.

Sequential vs. Concurrent Execution

// Sequential execution
let user = await fetchUser()
let products = await fetchProducts()

// Concurrent execution
async let userTask = fetchUser()
async let productTask = fetchProducts()

let (userResult, productResult) =
    await (userTask, productTask)

Concurrent execution is useful when operations are independent. If the second operation depends on the first result, sequential execution may be necessary.

6. Understanding Task Groups

Task groups allow developers to create a dynamic number of child tasks and collect their results. They are useful when processing multiple independent items, such as downloading several images or fetching multiple records.

Example: Using a Task Group

import Foundation

func fetchItem(_ id: Int) async -> String {
    return "Item \(id)"
}

func fetchMultipleItems() async -> [String] {
    await withTaskGroup(of: String.self) { group in

        for id in 1...5 {
            group.addTask {
                await fetchItem(id)
            }
        }

        var results: [String] = []

        for await result in group {
            results.append(result)
        }

        return results
    }
}

When to Use Task Groups

  • Fetching multiple independent API resources.
  • Processing a collection of files.
  • Performing parallel image processing tasks.
  • Combining results from several asynchronous operations.

7. Understanding Actors and Data Race Conditions

A data race occurs when multiple concurrent operations access shared mutable data without proper synchronization, and at least one operation modifies that data. Data races can produce unpredictable results.

Swift actors protect their isolated mutable state by controlling access to actor-isolated properties and methods.

Example: Creating an Actor

actor BankAccount {
    private var balance = 1000

    func deposit(amount: Int) {
        balance += amount
    }

    func withdraw(amount: Int) {
        if amount <= balance {
            balance -= amount
        }
    }

    func getBalance() -> Int {
        return balance
    }
}

func performTransactions() async {
    let account = BankAccount()

    await account.deposit(amount: 500)
    await account.withdraw(amount: 200)

    let balance = await account.getBalance()
    print(balance)
}

Explanation

  • actor defines an isolated reference type.
  • Its mutable state is protected from unsynchronized external access.
  • External access to actor-isolated methods generally requires await.
  • Actors help prevent data races but do not automatically make every multi-step business operation atomic across suspension points.

8. Interview Problem 3: Prevent Concurrent Data Updates

Problem: Create a shared counter that multiple asynchronous tasks can safely update.

actor Counter {
    private var value = 0

    func increment() {
        value += 1
    }

    func getValue() -> Int {
        return value
    }
}

func testCounter() async {
    let counter = Counter()

    await withTaskGroup(of: Void.self) { group in
        for _ in 1...100 {
            group.addTask {
                await counter.increment()
            }
        }
    }

    let result = await counter.getValue()
    print(result)
}

Expected Output:

100

Concept Tested: Actor isolation, shared mutable state, task groups, and safe concurrent updates.

9. Understanding MainActor

MainActor is a global actor used to isolate operations that interact with main-thread-oriented UI state. SwiftUI views and UI-related observable models often need their state changes to occur on the main actor.

Example: MainActor Usage

import SwiftUI

@MainActor
final class ProfileViewModel: ObservableObject {
    @Published var username = ""

    func updateUsername() {
        username = "Alex"
    }
}

Important Interview Points

  • MainActor provides isolation for UI-related state.
  • It helps ensure that isolated operations execute through the main actor.
  • It is not a replacement for all background processing.
  • Long-running CPU-intensive work should be designed so it does not block UI responsiveness.

10. Understanding Task Cancellation

Task cancellation allows an application to stop work that is no longer required. For example, a search request should ideally stop when the user enters a new search query or leaves the screen.

Example: Checking Cancellation

import Foundation

func performLongTask() async throws {
    for index in 1...10 {
        try Task.checkCancellation()

        print("Processing: \(index)")

        try await Task.sleep(for: .seconds(1))
    }
}

Interview Problem 4: Cancel an Existing Task

import Foundation

var currentTask: Task<Void, Never>?

func startTask() {
    currentTask = Task {
        do {
            try await performLongTask()
        } catch is CancellationError {
            print("Task was cancelled")
        } catch {
            print("Error: \(error)")
        }
    }
}

func stopTask() {
    currentTask?.cancel()
    currentTask = nil
}

Cancellation is cooperative. Calling cancel() requests cancellation, but the task must check cancellation or call cancellation-aware operations to respond appropriately.

11. Understanding API Integration in iOS

API integration allows an iOS application to communicate with remote servers. APIs are used to retrieve data, submit forms, authenticate users, upload files, and synchronize application information.

Common HTTP Methods

Method Purpose Example
GET Retrieve information. Fetch a product list.
POST Create a resource or submit data. Create a new user account.
PUT Replace an existing resource. Replace a user profile.
PATCH Partially update a resource. Update a user's phone number.
DELETE Remove a resource. Delete a saved address.

API Request Lifecycle

  1. Create the API endpoint URL.
  2. Configure the URLRequest if custom headers or an HTTP method are required.
  3. Send the request using URLSession.
  4. Receive the response and response data.
  5. Validate the HTTP status code.
  6. Decode JSON data into Swift models.
  7. Update the application state.
  8. Display the result or an appropriate error message.

12. Understanding URLSession

URLSession is Apple's networking API for sending HTTP requests and receiving server responses. It supports data tasks, upload tasks, download tasks, and asynchronous networking operations.

Example: Basic GET Request

import Foundation

func fetchData() async throws -> Data {
    let url = URL(
        string: "https://jsonplaceholder.typicode.com/posts"
    )!

    let (data, response) = try await URLSession.shared.data(
        from: url
    )

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

    return data
}

Explanation

  • URL represents the API endpoint.
  • URLSession.shared provides a shared networking session.
  • data(from:) performs an asynchronous data request.
  • HTTPURLResponse provides HTTP status and header information.
  • The status-code check verifies whether the request succeeded.
  • The returned data can be decoded into a Swift model.

13. Understanding Codable and JSON Decoding

Most REST APIs return information in JSON format. Swift's Codable protocol allows developers to convert between JSON data and Swift data models.

Sample JSON Response

{
    "id": 1,
    "title": "Learn Swift",
    "body": "Swift is used to build iOS applications."
}

Creating a Codable Model

import Foundation

struct Post: Codable {
    let id: Int
    let title: String
    let body: String
}

Decoding JSON Data

func decodePost(from data: Data) throws -> Post {
    let decoder = JSONDecoder()
    return try decoder.decode(Post.self, from: data)
}

JSONDecoder converts JSON data into a Swift model. If the JSON keys do not match the model properties, developers can use CodingKeys to map them.

Handling Different JSON Key Names

struct User: Codable {
    let id: Int
    let firstName: String

    enum CodingKeys: String, CodingKey {
        case id
        case firstName = "first_name"
    }
}

14. Interview Problem 5: Fetch and Decode API Data

Problem: Fetch a list of posts from a REST API and convert the response into Swift objects.

Complete Solution

import Foundation

struct Post: Codable, Identifiable {
    let id: Int
    let title: String
    let body: String
}

enum APIError: Error {
    case invalidResponse
    case invalidStatusCode(Int)
}

func fetchPosts() async throws -> [Post] {
    let url = URL(
        string: "https://jsonplaceholder.typicode.com/posts"
    )!

    let (data, response) = try await URLSession.shared.data(
        from: url
    )

    guard let httpResponse = response as? HTTPURLResponse else {
        throw APIError.invalidResponse
    }

    guard (200...299).contains(httpResponse.statusCode) else {
        throw APIError.invalidStatusCode(
            httpResponse.statusCode
        )
    }

    return try JSONDecoder().decode([Post].self, from: data)
}

func loadPosts() async {
    do {
        let posts = try await fetchPosts()

        for post in posts {
            print(post.title)
        }
    } catch {
        print("Failed to fetch posts: \(error)")
    }
}

Concept Tested: URLSession, async/await, HTTP response validation, Codable, JSONDecoder, and error handling.

15. Creating POST Requests

A POST request sends information to a server to create a resource or perform an operation. For example, a registration screen can submit a user's name, email, and password to an authentication API.

Example: Sending JSON Data

import Foundation

struct NewUser: Codable {
    let name: String
    let email: String
}

func createUser() async throws {
    let url = URL(
        string: "https://example.com/api/users"
    )!

    let user = NewUser(
        name: "Alex",
        email: "[email protected]"
    )

    var request = URLRequest(url: url)
    request.httpMethod = "POST"
    request.setValue(
        "application/json",
        forHTTPHeaderField: "Content-Type"
    )

    request.httpBody = try JSONEncoder().encode(user)

    let (_, response) = try await URLSession.shared.data(
        for: request
    )

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

The example endpoint is a placeholder. A real application must use its own backend URL and follow the server's request and response contract.

16. Handling API Errors and Network Failures

Network requests can fail for several reasons, including internet connectivity problems, server errors, invalid data, authentication failures, and request timeouts. Proper error handling improves reliability and user experience.

Common HTTP Status Codes

Status Code Meaning Typical Handling
200 OK Process the successful response.
201 Created Handle the newly created resource.
204 No Content Handle successful requests without a response body.
400 Bad Request Validate request parameters and payload.
401 Unauthorized Check authentication credentials or token.
403 Forbidden Check user permissions.
404 Not Found Verify the endpoint or requested resource.
429 Too Many Requests Respect rate limits and retry policies.
500 Internal Server Error Display an appropriate server error message.
503 Service Unavailable Retry later when appropriate.

Example: Custom API Errors

enum NetworkError: Error {
    case invalidURL
    case invalidResponse
    case serverError(Int)
    case decodingFailed
}

func validateResponse(_ response: URLResponse) throws {
    guard let httpResponse = response as? HTTPURLResponse else {
        throw NetworkError.invalidResponse
    }

    guard (200...299).contains(httpResponse.statusCode) else {
        throw NetworkError.serverError(
            httpResponse.statusCode
        )
    }
}

17. Understanding API Authentication

Many APIs require authentication before users can access protected resources. Authentication verifies the user's identity, while authorization determines which operations the user is allowed to perform.

Common Authentication Methods

  • API Key: A key used by a service to identify an application or client.
  • Bearer Token: A token sent in the HTTP Authorization header.
  • OAuth 2.0: An authorization framework commonly used for delegated access.
  • JWT: A compact token format that can carry signed claims.

Example: Adding an Authorization Header

var request = URLRequest(
    url: URL(string: "https://example.com/api/profile")!
)

request.setValue(
    "Bearer YOUR_ACCESS_TOKEN",
    forHTTPHeaderField: "Authorization"
)

Do not hardcode real production credentials in source code or commit them to a public repository. Store sensitive tokens using appropriate secure storage, such as Keychain, and follow the backend's token lifecycle and refresh requirements.

18. Understanding Database Management in iOS

Databases allow iOS applications to store, retrieve, update, and delete information. Local persistence is useful when applications need to retain data between launches or provide offline functionality.

Common iOS Data Storage Options

Technology Purpose Suitable Use
UserDefaults Stores simple preferences and small values. Theme selection and onboarding status.
Core Data Manages persistent object graphs. Structured local application data.
SwiftData Provides a modern persistence framework integrated with Swift. Model-based local data storage.
SQLite Provides relational database storage. Structured records and SQL queries.
Keychain Stores sensitive credentials securely. Authentication tokens and passwords.

19. Understanding CRUD Operations

CRUD represents the four fundamental operations used to manage database records.

Operation Meaning Example
Create Insert a new record. Save a new contact.
Read Retrieve existing records. Display saved contacts.
Update Modify an existing record. Change a contact's phone number.
Delete Remove an existing record. Delete an old contact.

20. Working with UserDefaults

UserDefaults is suitable for storing small pieces of non-sensitive application data. It is not intended to replace a structured database or store passwords and access tokens.

Example: Save and Retrieve a Value

import Foundation

// Save a value
UserDefaults.standard.set(
    "Alex",
    forKey: "username"
)

// Retrieve a value
let username = UserDefaults.standard.string(
    forKey: "username"
)

print(username ?? "Guest")

// Remove a value
UserDefaults.standard.removeObject(
    forKey: "username"
)

Interview Question: When Should UserDefaults Be Used?

UserDefaults should be used for lightweight preferences such as language selection, theme settings, onboarding completion, and other simple non-sensitive values. It should not be used for large relational datasets or sensitive credentials.

21. Understanding Core Data

Core Data is Apple's framework for managing an application's model layer and persistent object graph. It supports object creation, fetching, updating, deletion, relationships, and persistence.

Important Core Data Components

  • NSManagedObject: Represents a managed object in the data model.
  • NSManagedObjectContext: Tracks changes to managed objects.
  • NSPersistentContainer: Simplifies the setup of the Core Data stack.
  • NSFetchRequest: Defines how objects are retrieved.
  • NSEntityDescription: Describes an entity in the data model.

Typical Core Data Workflow

  1. Create a Core Data model file.
  2. Define entities and attributes.
  3. Configure the persistent container.
  4. Obtain the managed object context.
  5. Create or fetch managed objects.
  6. Update or delete objects as required.
  7. Save the context to persist changes.

22. Interview Problem 6: Insert and Fetch Core Data Records

Problem: Create a contact record and retrieve saved contact records using Core Data.

Assume the Core Data model contains an entity named Contact with a string attribute named name.

Insert a Record

import CoreData

func saveContact(
    name: String,
    context: NSManagedObjectContext
) throws {
    let contact = Contact(context: context)
    contact.name = name

    try context.save()
}

Fetch Records

import CoreData

func fetchContacts(
    context: NSManagedObjectContext
) throws -> [Contact] {
    let request: NSFetchRequest<Contact> =
        Contact.fetchRequest()

    return try context.fetch(request)
}

Delete a Record

func deleteContact(
    _ contact: Contact,
    context: NSManagedObjectContext
) throws {
    context.delete(contact)
    try context.save()
}

Concept Tested: Managed objects, managed object context, fetch requests, persistence, and CRUD operations.

23. Understanding SwiftData

SwiftData is Apple's modern persistence framework for Swift applications. It uses Swift models and integrates closely with SwiftUI to simplify local data storage and retrieval.

Creating a SwiftData Model

import SwiftData

@Model
final class TaskItem {
    var title: String
    var isCompleted: Bool

    init(
        title: String,
        isCompleted: Bool = false
    ) {
        self.title = title
        self.isCompleted = isCompleted
    }
}

Configuring the Model Container

import SwiftUI
import SwiftData

@main
struct TaskApp: App {
    var body: some Scene {
        WindowGroup {
            TaskListView()
        }
        .modelContainer(for: TaskItem.self)
    }
}

Inserting and Deleting Records

import SwiftUI
import SwiftData

struct TaskListView: View {
    @Environment(\.modelContext) private var context

    @Query private var tasks: [TaskItem]

    var body: some View {
        List {
            ForEach(tasks) { task in
                Text(task.title)
            }
            .onDelete { indexSet in
                for index in indexSet {
                    context.delete(tasks[index])
                }
            }
        }
        .toolbar {
            Button("Add Task") {
                let task = TaskItem(title: "Learn SwiftData")
                context.insert(task)
            }
        }
    }
}

@Model marks a persistent model type, @Query retrieves matching records, and modelContext provides access to insert and delete operations.

24. Interview Problem 7: Implement CRUD Operations Using SwiftData

Problem: Build a simple task manager that supports adding, displaying, updating, and deleting tasks.

Solution Outline

  1. Create a model using @Model.
  2. Configure the model container in the application.
  3. Use @Query to display saved tasks.
  4. Use modelContext.insert() to create records.
  5. Modify model properties to update existing records.
  6. Use modelContext.delete() to remove records.
  7. Allow SwiftData to manage persistence through its model context.

Example: Updating a Task

func markTaskCompleted(_ task: TaskItem) {
    task.isCompleted = true
}

When the model is managed by SwiftData, modifying its properties updates the managed model state. Ensure changes are saved according to the application's persistence workflow.

25. Database Relationships and Data Modelling

Real-world applications often contain related entities. For example, an e-commerce application may have customers, orders, and order items. Relationships help organize these records and represent how they are connected.

Common Relationship Types

Relationship Meaning Example
One-to-One One record is related to one other record. User and profile.
One-to-Many One record is related to multiple records. Customer and orders.
Many-to-Many Multiple records can be related to multiple other records. Students and courses.

Example: SwiftData Relationship

import SwiftData

@Model
final class Customer {
    var name: String

    @Relationship(deleteRule: .cascade)
    var orders: [Order] = []

    init(name: String) {
        self.name = name
    }
}

@Model
final class Order {
    var orderNumber: String
    var amount: Double

    init(
        orderNumber: String,
        amount: Double
    ) {
        self.orderNumber = orderNumber
        self.amount = amount
    }
}

This example represents a customer with multiple orders. The cascade delete rule means deleting the customer also deletes related orders, so relationship deletion rules must be chosen carefully.

26. Interview Problem 8: Search and Filter Database Records

Problem: Retrieve only completed tasks from a SwiftData database.

Solution Using Predicate

import SwiftData
import SwiftUI

struct CompletedTasksView: View {
    @Query(
        filter: #Predicate<TaskItem> {
            $0.isCompleted == true
        }
    )
    private var completedTasks: [TaskItem]

    var body: some View {
        List(completedTasks) { task in
            Text(task.title)
        }
    }
}

Concept Tested: SwiftData queries, predicates, filtering, and displaying persistent records.

27. Combining API Integration with Database Storage

Many applications retrieve information from a remote API and save it locally. This approach supports offline access, faster loading, and data synchronization.

Typical Data Flow

  1. Send an API request to the backend.
  2. Receive JSON data.
  3. Decode JSON into Swift models.
  4. Validate the returned data.
  5. Save or update local database records.
  6. Display the latest stored information.
  7. Refresh the data when required.

Example: API and Database Responsibilities

Layer Responsibility
API Service Communicates with the remote server.
Model Layer Defines the structure of application data.
Repository Coordinates remote and local data sources.
Database Stores data for persistent access.
ViewModel Prepares data and state for the interface.
SwiftUI View Displays data and handles user interactions.

28. Understanding the Repository Pattern

The repository pattern separates data access from the user interface and business logic. A repository can coordinate API requests, local database operations, caching, and data synchronization.

Benefits of Repository Pattern

  • Separates networking from presentation code.
  • Centralizes data retrieval and storage logic.
  • Makes unit testing easier.
  • Supports multiple data sources.
  • Reduces direct database and API dependencies in views.

Example Repository Protocol

import Foundation

protocol ProductRepository {
    func fetchProducts() async throws -> [Product]
}

A concrete repository can implement this protocol using URLSession, SwiftData, or another persistence solution. The protocol allows view models to depend on an abstraction rather than a specific storage implementation.

29. Interview Problem 9: Build a ViewModel for API Data

Problem: Fetch a list of posts and expose loading, success, and error states to a SwiftUI view.

Solution

import SwiftUI

@MainActor
final class PostsViewModel: ObservableObject {
    @Published var posts: [Post] = []
    @Published var isLoading = false
    @Published var errorMessage: String?

    func loadPosts() async {
        isLoading = true
        errorMessage = nil

        defer {
            isLoading = false
        }

        do {
            posts = try await fetchPosts()
        } catch {
            errorMessage = error.localizedDescription
        }
    }
}

Displaying ViewModel State

import SwiftUI

struct PostsView: View {
    @StateObject private var viewModel = PostsViewModel()

    var body: some View {
        NavigationStack {
            Group {
                if viewModel.isLoading {
                    ProgressView("Loading posts...")
                } else if let error = viewModel.errorMessage {
                    ContentUnavailableView(
                        "Unable to Load Posts",
                        systemImage: "wifi.exclamationmark",
                        description: Text(error)
                    )
                } else {
                    List(viewModel.posts) { post in
                        VStack(alignment: .leading) {
                            Text(post.title)
                                .font(.headline)

                            Text(post.body)
                                .font(.subheadline)
                        }
                    }
                }
            }
            .navigationTitle("Posts")
            .task {
                await viewModel.loadPosts()
            }
        }
    }
}

This example uses @MainActor to isolate UI-facing state. The .task modifier starts asynchronous work when the view appears and SwiftUI can manage the task's lifecycle.

30. Common Interview Problems and Debugging Scenarios

Problem Possible Cause Recommended Solution
UI freezes during an API request Blocking work is performed on the UI execution path. Use asynchronous networking and avoid blocking the main actor.
Data race occurs Shared mutable state is accessed concurrently. Use actor isolation or another appropriate synchronization strategy.
API returns unexpected data JSON structure does not match the Swift model. Inspect the response and correct Codable mappings.
API request fails Invalid URL, network issue, or server response. Validate the request and handle errors.
Database changes are missing Context changes were not persisted or the wrong context was used. Review the persistence workflow and context lifecycle.
Duplicate records appear Existing records are not checked before insertion. Use unique identifiers and an upsert strategy.
Task continues after screen closes Task lifecycle or cancellation is not managed correctly. Use structured tasks or explicitly cancel owned tasks.
UI displays stale data View state is not refreshed after a data operation. Use observable state and refresh the relevant data source.
Database query is slow Large datasets or inefficient filtering. Use appropriate predicates, indexes, and pagination where supported.

31. Best Practices for Solving Interview Problems

  • Understand the problem requirements before writing code.
  • Identify whether the operation is synchronous or asynchronous.
  • Use async/await for asynchronous workflows.
  • Choose async let or task groups for independent concurrent operations.
  • Use actors to protect shared mutable state when appropriate.
  • Use MainActor for UI-facing state management.
  • Handle task cancellation and errors explicitly.
  • Validate HTTP responses before decoding API data.
  • Use Codable for structured JSON conversion.
  • Keep API services and database logic separate from views.
  • Choose UserDefaults, Core Data, SwiftData, SQLite, or Keychain according to the data requirements.
  • Test edge cases such as empty responses, duplicate records, and network failures.
  • Explain the time, memory, and architectural trade-offs of a solution.

32. Practical Interview Coding Exercises

  1. Write an async function that fetches data from a remote API.
  2. Fetch two independent API resources concurrently using async let.
  3. Use a task group to process multiple independent records.
  4. Create an actor-based counter and update it from multiple tasks.
  5. Implement cancellation for a long-running asynchronous operation.
  6. Decode a JSON response using Codable.
  7. Create a POST request using URLSession and JSONEncoder.
  8. Handle different HTTP status codes using custom errors.
  9. Save application preferences using UserDefaults.
  10. Implement Core Data insert, fetch, update, and delete operations.
  11. Create a SwiftData model and display records using @Query.
  12. Build a repository abstraction for remote and local data.
  13. Create a ViewModel with loading, success, and error states.
  14. Design an offline-first data flow for a notes application.

33. Mini Project: Offline-Ready Notes Application

Build a notes application that demonstrates Swift concurrency, API integration, and database persistence in one practical project.

Project Features

  • Create and edit notes.
  • Display saved notes from a local database.
  • Delete unwanted notes.
  • Fetch sample notes from a REST API.
  • Decode server responses using Codable.
  • Save retrieved notes locally using SwiftData.
  • Display loading and error states.
  • Use async/await for networking operations.
  • Use task cancellation when appropriate.
  • Support viewing previously saved notes when offline.

Suggested Architecture

NotesApplication/
│
├── Models/
│   ├── Note.swift
│   └── APIResponse.swift
│
├── Services/
│   └── NotesAPIService.swift
│
├── Repositories/
│   └── NotesRepository.swift
│
├── ViewModels/
│   └── NotesViewModel.swift
│
├── Views/
│   ├── NotesListView.swift
│   ├── NoteDetailView.swift
│   └── AddNoteView.swift
│
└── NotesApplicationApp.swift

Expected Learning Outcome

After completing this project, learners should understand how to combine asynchronous networking, JSON decoding, persistent local storage, observable application state, and SwiftUI presentation into a maintainable iOS application.

34. Frequently Asked Interview Questions and Answers

Q1. What is Swift concurrency?

Swift concurrency is a language and runtime feature set that helps developers write asynchronous and concurrent code using tools such as async/await, Task, task groups, actors, and MainActor.

Q2. What is the difference between async and await?

async marks a function as asynchronous, while await marks a point where the calling task may suspend until an asynchronous result is available.

Q3. What is a Task in Swift?

A Task represents an asynchronous unit of work. It can be used to start asynchronous operations and manage their execution and cancellation.

Q4. What is structured concurrency?

Structured concurrency organizes child tasks within a defined parent scope, making task lifetime, completion, and cancellation easier to manage.

Q5. What is the purpose of an actor?

An actor protects its isolated mutable state and helps prevent data races when multiple concurrent tasks need access to shared data.

Q6. What is MainActor?

MainActor is a global actor commonly used to isolate UI-related state and operations so they execute through the main actor.

Q7. What is URLSession?

URLSession is Apple's networking API for performing HTTP requests, downloading data, and uploading files.

Q8. What is Codable?

Codable is a Swift type alias for the Encodable and Decodable protocols. It helps convert Swift values to and from external representations such as JSON.

Q9. What is the difference between GET and POST?

GET is commonly used to retrieve resources, while POST is commonly used to submit data or create resources on a server.

Q10. What is Core Data?

Core Data is Apple's framework for managing persistent object graphs and performing structured data operations.

Q11. What is SwiftData?

SwiftData is Apple's modern persistence framework that provides Swift-oriented model definitions and integrates with SwiftUI.

Q12. What are CRUD operations?

CRUD stands for Create, Read, Update, and Delete. These are the fundamental operations used to manage database records.

Q13. What is the difference between UserDefaults and Core Data?

UserDefaults is designed for small preference values, while Core Data manages structured objects, relationships, and larger persistent datasets.

Q14. How do you handle API errors in iOS?

Use throwing functions, do-catch blocks, HTTP status validation, custom error types, and meaningful UI error states.

Q15. What is the repository pattern?

The repository pattern separates data access from business logic and presentation by providing a centralized interface for retrieving and storing data.

Q16. Why is task cancellation important?

Task cancellation prevents unnecessary work when an operation is no longer needed, such as when a user leaves a screen or changes a search query.

Q17. What is a data race?

A data race occurs when concurrent operations access shared mutable data without appropriate synchronization and at least one operation modifies that data.

Q18. What is the difference between async let and a task group?

async let is convenient for a known number of child operations, while task groups support dynamically adding multiple child tasks and collecting their results.

35. Quick Revision Checklist

  • Understand concurrency and asynchronous programming.
  • Write functions using async and await.
  • Create and manage Tasks.
  • Use async let for independent asynchronous operations.
  • Use task groups for multiple child tasks.
  • Understand actors and data-race prevention.
  • Apply MainActor to UI-related state.
  • Handle cancellation and errors.
  • Perform GET and POST requests using URLSession.
  • Decode and encode JSON using Codable.
  • Validate HTTP status codes.
  • Understand API authentication and secure token storage.
  • Perform CRUD operations with Core Data and SwiftData.
  • Understand database relationships and filtering.
  • Separate networking, persistence, and presentation using a repository architecture.
  • Write unit tests for asynchronous and database-related logic.

36. Conclusion

Solving Swift concurrency, API integration, and database interview problems helps iOS developers demonstrate practical programming and application architecture skills. Swift concurrency makes asynchronous operations easier to manage, URLSession provides reliable networking capabilities, and Core Data or SwiftData supports persistent local storage.

By practising these coding problems, learners gain experience with async/await, task groups, actors, API requests, JSON decoding, error handling, CRUD operations, and data synchronization. These skills are valuable when developing real-world iOS applications and preparing for technical interviews.


Course Resources

Explore the complete iOS App Development Training at JustAcademy.

Register through the JustAcademy Course Demo Registration page.

Visit the JustAcademy Official Website to explore additional training programs.

Previous Next
whatsapp