iOS Networking & Data Guide
Complete Reference for Senior iOS Engineers
Table of Contents
- URLSession Fundamentals
- Modern async/await Networking
- Combine Framework
- JSON Parsing with Codable
- Error Handling
- Networking Layer Architecture
- Caching Strategies
- Authentication & Security
- Data Persistence
- Image Loading & Caching
- Offline Support
- Interview Questions
URLSession Fundamentals
Basic Request
// Simple data task
let url = URL(string: "https://api.example.com/users")!
let task = URLSession.shared.dataTask(with: url) { data, response, error in
// Handle response
if let error = error {
print("Error: \(error)")
return
}
guard let httpResponse = response as? HTTPURLResponse else {
print("Invalid response")
return
}
guard (200...299).contains(httpResponse.statusCode) else {
print("Status code: \(httpResponse.statusCode)")
return
}
guard let data = data else {
print("No data")
return
}
// Process data
print("Received \(data.count) bytes")
}
task.resume() // Don't forget!
URLRequest Configuration
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
request.timeoutInterval = 30
request.cachePolicy = .reloadIgnoringLocalCacheData
// Body
let body = ["name": "John", "email": "john@example.com"]
request.httpBody = try? JSONEncoder().encode(body)
let task = URLSession.shared.dataTask(with: request) { data, response, error in
// Handle response
}
task.resume()
URLSession Configuration
// Default session
let session = URLSession.shared
// Custom configuration
let configuration = URLSessionConfiguration.default
configuration.timeoutIntervalForRequest = 30
configuration.timeoutIntervalForResource = 300
configuration.waitsForConnectivity = true
configuration.allowsCellularAccess = true
configuration.httpMaximumConnectionsPerHost = 5
// HTTP headers for all requests
configuration.httpAdditionalHeaders = [
"User-Agent": "MyApp/1.0",
"Accept": "application/json"
]
// Cache policy
configuration.requestCachePolicy = .returnCacheDataElseLoad
configuration.urlCache = URLCache(
memoryCapacity: 10 * 1024 * 1024, // 10 MB
diskCapacity: 50 * 1024 * 1024 // 50 MB
)
let customSession = URLSession(configuration: configuration)
// Background session (for downloads/uploads)
let backgroundConfig = URLSessionConfiguration.background(withIdentifier: "com.example.app.background")
let backgroundSession = URLSession(configuration: backgroundConfig, delegate: self, delegateQueue: nil)
Task Types
// 1. Data Task - In-memory data
let dataTask = URLSession.shared.dataTask(with: url) { data, response, error in
// Handle in-memory data
}
// 2. Download Task - Save to disk
let downloadTask = URLSession.shared.downloadTask(with: url) { localURL, response, error in
guard let localURL = localURL else { return }
// Move to permanent location
let destinationURL = /* ... */
try? FileManager.default.moveItem(at: localURL, to: destinationURL)
}
// 3. Upload Task - Upload data
var request = URLRequest(url: url)
request.httpMethod = "POST"
let uploadTask = URLSession.shared.uploadTask(with: request, from: data) { data, response, error in
// Handle response
}
// 4. Stream Task - Bidirectional streaming
let streamTask = URLSession.shared.streamTask(withHostName: "example.com", port: 80)
URLSessionDelegate
class NetworkManager: NSObject, URLSessionDelegate, URLSessionTaskDelegate, URLSessionDataDelegate {
lazy var session: URLSession = {
let config = URLSessionConfiguration.default
return URLSession(configuration: config, delegate: self, delegateQueue: nil)
}()
// MARK: - URLSessionDelegate
func urlSession(_ session: URLSession, didBecomeInvalidWithError error: Error?) {
print("Session invalidated: \(error?.localizedDescription ?? "Unknown")")
}
// MARK: - URLSessionTaskDelegate
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
if let error = error {
print("Task failed: \(error)")
}
}
func urlSession(_ session: URLSession, task: URLSessionTask, didSendBodyData bytesSent: Int64, totalBytesSent: Int64, totalBytesExpectedToSend: Int64) {
let progress = Double(totalBytesSent) / Double(totalBytesExpectedToSend)
print("Upload progress: \(progress * 100)%")
}
// MARK: - URLSessionDataDelegate
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
print("Received \(data.count) bytes")
}
// MARK: - Authentication
func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust {
// SSL pinning
if let serverTrust = challenge.protectionSpace.serverTrust {
let credential = URLCredential(trust: serverTrust)
completionHandler(.useCredential, credential)
return
}
}
completionHandler(.performDefaultHandling, nil)
}
}
Modern async/await Networking
Basic async Request
// URLSession has built-in async/await support (iOS 15+)
func fetchUsers() async throws -> [User] {
let url = URL(string: "https://api.example.com/users")!
let (data, response) = try await URLSession.shared.data(from: url)
guard let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode) else {
throw NetworkError.invalidResponse
}
let users = try JSONDecoder().decode([User].self, from: data)
return users
}
// Usage
Task {
do {
let users = try await fetchUsers()
print("Fetched \(users.count) users")
} catch {
print("Error: \(error)")
}
}
async/await with URLRequest
func createUser(name: String, email: String) async throws -> User {
let url = URL(string: "https://api.example.com/users")!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
let body = ["name": name, "email": email]
request.httpBody = try JSONEncoder().encode(body)
let (data, response) = try await URLSession.shared.data(for: request)
guard let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode) else {
throw NetworkError.invalidResponse
}
let user = try JSONDecoder().decode(User.self, from: data)
return user
}
Cancellation Support
class UserViewModel {
private var loadTask: Task<Void, Never>?
func loadUsers() {
// Cancel previous task
loadTask?.cancel()
loadTask = Task {
do {
let users = try await fetchUsers()
// Check if cancelled
guard !Task.isCancelled else {
print("Task was cancelled")
return
}
await MainActor.run {
self.users = users
}
} catch {
print("Error: \(error)")
}
}
}
func cancelLoading() {
loadTask?.cancel()
}
}
Parallel Requests
// Sequential (slow)
func fetchDataSequential() async throws -> (users: [User], posts: [Post]) {
let users = try await fetchUsers()
let posts = try await fetchPosts()
return (users, posts)
}
// Parallel (fast)
func fetchDataParallel() async throws -> (users: [User], posts: [Post]) {
async let users = fetchUsers()
async let posts = fetchPosts()
return try await (users, posts)
}
// Task group for dynamic number of requests
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
}
}
Combine Framework
Basic Publisher
import Combine
// Create a data task publisher
func fetchUsers() -> AnyPublisher<[User], Error> {
let url = URL(string: "https://api.example.com/users")!
return URLSession.shared.dataTaskPublisher(for: url)
.map(\.data) // Extract data
.decode(type: [User].self, decoder: JSONDecoder()) // Decode JSON
.receive(on: DispatchQueue.main) // Switch to main thread
.eraseToAnyPublisher()
}
// Usage
class UserViewModel {
private var cancellables = Set<AnyCancellable>()
@Published var users: [User] = []
@Published var isLoading = false
@Published var error: Error?
func loadUsers() {
isLoading = true
fetchUsers()
.sink(
receiveCompletion: { [weak self] completion in
self?.isLoading = false
if case .failure(let error) = completion {
self?.error = error
}
},
receiveValue: { [weak self] users in
self?.users = users
}
)
.store(in: &cancellables)
}
}
Combine Operators
// Map - Transform values
publisher
.map { $0.uppercased() }
// Filter - Keep only matching values
publisher
.filter { $0.count > 3 }
// FlatMap - Transform and flatten
publisher
.flatMap { id in
fetchUserDetails(id: id)
}
// Debounce - Wait for pause in events
searchTextField.textPublisher
.debounce(for: .milliseconds(300), scheduler: DispatchQueue.main)
.sink { searchText in
// Search after user stops typing
}
// RemoveDuplicates - Ignore duplicate values
publisher
.removeDuplicates()
// Retry - Retry on failure
publisher
.retry(3)
// Catch - Handle errors
publisher
.catch { error -> AnyPublisher<[User], Never> in
print("Error: \(error)")
return Just([]).eraseToAnyPublisher()
}
// CombineLatest - Combine multiple publishers
Publishers.CombineLatest(publisher1, publisher2)
.sink { value1, value2 in
// Both have emitted
}
// Zip - Pair values from multiple publishers
Publishers.Zip(publisher1, publisher2)
.sink { value1, value2 in
// Both have emitted, paired together
}
Custom Publisher
class UserService {
private var cancellables = Set<AnyCancellable>()
func fetchUsers() -> AnyPublisher<[User], Error> {
let url = URL(string: "https://api.example.com/users")!
var request = URLRequest(url: url)
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
return URLSession.shared.dataTaskPublisher(for: request)
.tryMap { output in
// Validate response
guard let response = output.response as? HTTPURLResponse,
(200...299).contains(response.statusCode) else {
throw NetworkError.invalidResponse
}
return output.data
}
.decode(type: [User].self, decoder: JSONDecoder())
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
}
}
Subject (Manual Publisher)
class UserViewModel {
// PassthroughSubject - doesn't store value
let userSelected = PassthroughSubject<User, Never>()
// CurrentValueSubject - stores current value
let currentUser = CurrentValueSubject<User?, Never>(nil)
func selectUser(_ user: User) {
userSelected.send(user)
currentUser.send(user)
}
}
// Usage
viewModel.userSelected
.sink { user in
print("Selected: \(user.name)")
}
.store(in: &cancellables)
// Get current value
if let user = viewModel.currentUser.value {
print("Current user: \(user.name)")
}
JSON Parsing with Codable
Basic Codable
struct User: Codable {
let id: String
let name: String
let email: String
}
// Encoding
let user = User(id: "123", name: "John", email: "john@example.com")
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
let jsonData = try encoder.encode(user)
// Decoding
let decoder = JSONDecoder()
let decodedUser = try decoder.decode(User.self, from: jsonData)
Custom Keys (CodingKeys)
// API returns: { "user_id": "123", "full_name": "John Doe" }
struct User: Codable {
let id: String
let name: String
enum CodingKeys: String, CodingKey {
case id = "user_id"
case name = "full_name"
}
}
Nested JSON
// JSON:
// {
// "id": "123",
// "profile": {
// "name": "John",
// "email": "john@example.com"
// }
// }
struct User: Codable {
let id: String
let profile: Profile
struct Profile: Codable {
let name: String
let email: String
}
}
Flattening Nested JSON
// Want to flatten nested structure
struct User: Codable {
let id: String
let name: String
let email: String
enum CodingKeys: String, CodingKey {
case id
case profile
}
enum ProfileKeys: String, CodingKey {
case name
case email
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decode(String.self, forKey: .id)
let profileContainer = try container.nestedContainer(keyedBy: ProfileKeys.self, forKey: .profile)
name = try profileContainer.decode(String.self, forKey: .name)
email = try profileContainer.decode(String.self, forKey: .email)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(id, forKey: .id)
var profileContainer = container.nestedContainer(keyedBy: ProfileKeys.self, forKey: .profile)
try profileContainer.encode(name, forKey: .name)
try profileContainer.encode(email, forKey: .email)
}
}
Optional and Default Values
struct User: Codable {
let id: String
let name: String
let email: String? // Optional - allowed to be null
let age: Int // Required
let bio: String // Has default if missing
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decode(String.self, forKey: .id)
name = try container.decode(String.self, forKey: .name)
email = try container.decodeIfPresent(String.self, forKey: .email)
age = try container.decode(Int.self, forKey: .age)
// Provide default if missing
bio = try container.decodeIfPresent(String.self, forKey: .bio) ?? "No bio"
}
}
Date Handling
let decoder = JSONDecoder()
// ISO8601 format: "2023-10-24T12:00:00Z"
decoder.dateDecodingStrategy = .iso8601
// Unix timestamp (seconds since 1970)
decoder.dateDecodingStrategy = .secondsSince1970
// Milliseconds timestamp
decoder.dateDecodingStrategy = .millisecondsSince1970
// Custom format
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd"
decoder.dateDecodingStrategy = .formatted(formatter)
// Custom decoder
decoder.dateDecodingStrategy = .custom { decoder in
let container = try decoder.singleValueContainer()
let dateString = try container.decode(String.self)
// Try multiple formats
let formatters = [
DateFormatter.iso8601,
DateFormatter.custom("yyyy-MM-dd")
]
for formatter in formatters {
if let date = formatter.date(from: dateString) {
return date
}
}
throw DecodingError.dataCorruptedError(in: container, debugDescription: "Invalid date format")
}
Snake Case Conversion
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
// API: { "user_id": "123", "full_name": "John" }
struct User: Codable {
let userId: String // Automatically maps from "user_id"
let fullName: String // Automatically maps from "full_name"
}
Polymorphic JSON
// Different types based on "type" field
enum Media: Codable {
case image(Image)
case video(Video)
enum CodingKeys: String, CodingKey {
case type
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let type = try container.decode(String.self, forKey: .type)
switch type {
case "image":
let image = try Image(from: decoder)
self = .image(image)
case "video":
let video = try Video(from: decoder)
self = .video(video)
default:
throw DecodingError.dataCorrupted(
DecodingError.Context(
codingPath: decoder.codingPath,
debugDescription: "Unknown media type: \(type)"
)
)
}
}
}
struct Image: Codable {
let url: String
let width: Int
let height: Int
}
struct Video: Codable {
let url: String
let duration: Int
let thumbnail: String
}
Error Handling
Custom Error Types
enum NetworkError: Error {
case invalidURL
case noData
case decodingFailed
case invalidResponse
case serverError(statusCode: Int)
case unauthorized
case timeout
case noConnection
}
extension NetworkError: LocalizedError {
var errorDescription: String? {
switch self {
case .invalidURL:
return "The URL is invalid"
case .noData:
return "No data received from server"
case .decodingFailed:
return "Failed to parse response"
case .invalidResponse:
return "Invalid response from server"
case .serverError(let code):
return "Server error: \(code)"
case .unauthorized:
return "You are not authorized"
case .timeout:
return "Request timed out"
case .noConnection:
return "No internet connection"
}
}
}
Error Handling in async/await
func fetchUser(id: String) async throws -> User {
let url = URL(string: "https://api.example.com/users/\(id)")!
do {
let (data, response) = try await URLSession.shared.data(from: url)
guard let httpResponse = response as? HTTPURLResponse else {
throw NetworkError.invalidResponse
}
switch httpResponse.statusCode {
case 200...299:
break
case 401:
throw NetworkError.unauthorized
case 500...599:
throw NetworkError.serverError(statusCode: httpResponse.statusCode)
default:
throw NetworkError.invalidResponse
}
guard !data.isEmpty else {
throw NetworkError.noData
}
do {
let user = try JSONDecoder().decode(User.self, from: data)
return user
} catch {
throw NetworkError.decodingFailed
}
} catch let error as URLError {
// Map URLError to our error
switch error.code {
case .notConnectedToInternet, .networkConnectionLost:
throw NetworkError.noConnection
case .timedOut:
throw NetworkError.timeout
default:
throw error
}
}
}
Advanced Error Recovery
// Retry logic with exponential backoff
class NetworkRetryManager {
static func retry<T>(
maxAttempts: Int = 3,
initialDelay: TimeInterval = 1.0,
operation: @escaping () async throws -> T
) async throws -> T {
var lastError: Error?
var delay = initialDelay
for attempt in 1...maxAttempts {
do {
return try await operation()
} catch NetworkError.timeout, NetworkError.noConnection {
lastError = error
if attempt < maxAttempts {
print("Attempt \(attempt) failed, retrying in \(delay)s...")
try await Task.sleep(nanoseconds: UInt64(delay * 1_000_000_000))
delay *= 2 // Exponential backoff
}
} catch {
// Don't retry for other errors
throw error
}
}
throw lastError ?? NetworkError.timeout
}
}
// Usage
func fetchDataWithRetry() async throws -> Data {
try await NetworkRetryManager.retry {
try await fetchData()
}
}
Error Aggregation
// Handling multiple errors in parallel operations
struct MultipleErrors: Error {
let errors: [Error]
var localizedDescription: String {
errors.map { $0.localizedDescription }.joined(separator: ", ")
}
}
func fetchMultipleResources() async throws -> (users: [User], posts: [Post]) {
async let usersResult = Task { try await fetchUsers() }
async let postsResult = Task { try await fetchPosts() }
var errors: [Error] = []
var users: [User] = []
var posts: [Post] = []
do {
users = try await usersResult.value
} catch {
errors.append(error)
}
do {
posts = try await postsResult.value
} catch {
errors.append(error)
}
if !errors.isEmpty {
throw MultipleErrors(errors: errors)
}
return (users, posts)
}
Error Context Enhancement
// Adding context to errors
struct ErrorContext {
let originalError: Error
let context: String
let file: String
let line: Int
let function: String
init(
_ error: Error,
context: String,
file: String = #file,
line: Int = #line,
function: String = #function
) {
self.originalError = error
self.context = context
self.file = file
self.line = line
self.function = function
}
}
extension ErrorContext: LocalizedError {
var errorDescription: String? {
"""
Error: \(originalError.localizedDescription)
Context: \(context)
Location: \(file):\(line) in \(function)
"""
}
}
// Usage
func processData() throws {
do {
let data = try loadData()
try validateData(data)
} catch {
throw ErrorContext(
error,
context: "Failed to process user data for sync"
)
}
}
Result Type
func fetchUser(id: String, completion: @escaping (Result<User, NetworkError>) -> Void) {
let url = URL(string: "https://api.example.com/users/\(id)")!
URLSession.shared.dataTask(with: url) { data, response, error in
if let error = error {
completion(.failure(.noConnection))
return
}
guard let data = data else {
completion(.failure(.noData))
return
}
do {
let user = try JSONDecoder().decode(User.self, from: data)
completion(.success(user))
} catch {
completion(.failure(.decodingFailed))
}
}.resume()
}
// Usage
fetchUser(id: "123") { result in
switch result {
case .success(let user):
print("Got user: \(user.name)")
case .failure(let error):
print("Error: \(error.localizedDescription)")
}
}
Networking Layer Architecture
Protocol-Based Service
// MARK: - Protocols
protocol APIEndpoint {
var path: String { get }
var method: HTTPMethod { get }
var headers: [String: String]? { get }
var parameters: [String: Any]? { get }
}
enum HTTPMethod: String {
case get = "GET"
case post = "POST"
case put = "PUT"
case delete = "DELETE"
case patch = "PATCH"
}
protocol APIServiceProtocol {
func request<T: Decodable>(_ endpoint: APIEndpoint) async throws -> T
}
// MARK: - Implementation
class APIService: APIServiceProtocol {
private let baseURL: String
private let session: URLSession
init(baseURL: String, session: URLSession = .shared) {
self.baseURL = baseURL
self.session = session
}
func request<T: Decodable>(_ endpoint: APIEndpoint) async throws -> T {
let request = try buildRequest(from: endpoint)
let (data, response) = try await session.data(for: request)
try validate(response: response, data: data)
return try decode(data: data)
}
private func buildRequest(from endpoint: APIEndpoint) throws -> URLRequest {
guard let url = URL(string: baseURL + endpoint.path) else {
throw NetworkError.invalidURL
}
var request = URLRequest(url: url)
request.httpMethod = endpoint.method.rawValue
// Headers
endpoint.headers?.forEach { key, value in
request.setValue(value, forHTTPHeaderField: key)
}
// Parameters (for POST/PUT)
if let parameters = endpoint.parameters,
endpoint.method == .post || endpoint.method == .put {
request.httpBody = try JSONSerialization.data(withJSONObject: parameters)
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
}
return request
}
private func validate(response: URLResponse, data: Data) throws {
guard let httpResponse = response as? HTTPURLResponse else {
throw NetworkError.invalidResponse
}
switch httpResponse.statusCode {
case 200...299:
break
case 401:
throw NetworkError.unauthorized
default:
throw NetworkError.serverError(statusCode: httpResponse.statusCode)
}
}
private func decode<T: Decodable>(data: Data) throws -> T {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
do {
return try decoder.decode(T.self, from: data)
} catch {
throw NetworkError.decodingFailed
}
}
}
// MARK: - Endpoints
enum UserEndpoint: APIEndpoint {
case list
case detail(id: String)
case create(name: String, email: String)
case update(id: String, name: String)
case delete(id: String)
var path: String {
switch self {
case .list:
return "/users"
case .detail(let id), .update(let id, _), .delete(let id):
return "/users/\(id)"
case .create:
return "/users"
}
}
var method: HTTPMethod {
switch self {
case .list, .detail:
return .get
case .create:
return .post
case .update:
return .put
case .delete:
return .delete
}
}
var headers: [String: String]? {
return [
"Authorization": "Bearer \(AuthManager.shared.token ?? "")",
"Accept": "application/json"
]
}
var parameters: [String: Any]? {
switch self {
case .create(let name, let email):
return ["name": name, "email": email]
case .update(_, let name):
return ["name": name]
default:
return nil
}
}
}
// MARK: - Usage
class UserService {
private let apiService: APIServiceProtocol
init(apiService: APIServiceProtocol = APIService(baseURL: "https://api.example.com")) {
self.apiService = apiService
}
func fetchUsers() async throws -> [User] {
return try await apiService.request(UserEndpoint.list)
}
func fetchUser(id: String) async throws -> User {
return try await apiService.request(UserEndpoint.detail(id: id))
}
func createUser(name: String, email: String) async throws -> User {
return try await apiService.request(UserEndpoint.create(name: name, email: email))
}
}
Generic Response Wrapper
// API returns: { "data": [...], "meta": {...} }
struct APIResponse<T: Decodable>: Decodable {
let data: T
let meta: Meta?
struct Meta: Decodable {
let page: Int?
let totalPages: Int?
let totalCount: Int?
}
}
// Usage
func fetchUsers() async throws -> [User] {
let response: APIResponse<[User]> = try await apiService.request(UserEndpoint.list)
return response.data
}
Caching Strategies
URLCache (HTTP Caching)
// Configure cache
let cache = URLCache(
memoryCapacity: 10 * 1024 * 1024, // 10 MB memory
diskCapacity: 50 * 1024 * 1024, // 50 MB disk
diskPath: "network_cache"
)
URLCache.shared = cache
// Cache policies
var request = URLRequest(url: url)
// Always fetch from network
request.cachePolicy = .reloadIgnoringLocalCacheData
// Use cache if available, else network
request.cachePolicy = .returnCacheDataElseLoad
// Use cache only (fail if not cached)
request.cachePolicy = .returnCacheDataDontLoad
// Check cache manually
if let cachedResponse = URLCache.shared.cachedResponse(for: request) {
let data = cachedResponse.data
// Use cached data
} else {
// Fetch from network
}
// Store in cache manually
let response = URLResponse(/* ... */)
let cachedResponse = CachedURLResponse(response: response, data: data)
URLCache.shared.storeCachedResponse(cachedResponse, for: request)
// Clear cache
URLCache.shared.removeAllCachedResponses()
URLCache.shared.removeCachedResponse(for: request)
In-Memory Cache
class MemoryCache<Key: Hashable, Value> {
private var cache = NSCache<NSNumber, CacheEntry>()
private let lock = NSLock()
private class CacheEntry {
let value: Value
let expirationDate: Date
init(value: Value, expirationDate: Date) {
self.value = value
self.expirationDate = expirationDate
}
var isExpired: Bool {
return Date() > expirationDate
}
}
func set(_ value: Value, forKey key: Key, ttl: TimeInterval = 300) {
lock.lock()
defer { lock.unlock() }
let expirationDate = Date().addingTimeInterval(ttl)
let entry = CacheEntry(value: value, expirationDate: expirationDate)
let nsKey = NSNumber(value: key.hashValue)
cache.setObject(entry, forKey: nsKey)
}
func get(forKey key: Key) -> Value? {
lock.lock()
defer { lock.unlock() }
let nsKey = NSNumber(value: key.hashValue)
guard let entry = cache.object(forKey: nsKey) else {
return nil
}
if entry.isExpired {
cache.removeObject(forKey: nsKey)
return nil
}
return entry.value
}
func remove(forKey key: Key) {
lock.lock()
defer { lock.unlock() }
let nsKey = NSNumber(value: key.hashValue)
cache.removeObject(forKey: nsKey)
}
func removeAll() {
lock.lock()
defer { lock.unlock() }
cache.removeAllObjects()
}
}
// Usage
let userCache = MemoryCache<String, User>()
// Store
userCache.set(user, forKey: user.id, ttl: 300) // 5 minutes
// Retrieve
if let cachedUser = userCache.get(forKey: userId) {
return cachedUser
}
Multi-Layer Cache
class CachedAPIService {
private let apiService: APIServiceProtocol
private let memoryCache = MemoryCache<String, Data>()
private let diskCache: DiskCache
init(apiService: APIServiceProtocol, diskCache: DiskCache) {
self.apiService = apiService
self.diskCache = diskCache
}
func fetch<T: Decodable>(_ endpoint: APIEndpoint) async throws -> T {
let cacheKey = endpoint.path
// 1. Try memory cache
if let cachedData = memoryCache.get(forKey: cacheKey) {
return try decode(data: cachedData)
}
// 2. Try disk cache
if let cachedData = try? diskCache.data(forKey: cacheKey) {
memoryCache.set(cachedData, forKey: cacheKey)
return try decode(data: cachedData)
}
// 3. Fetch from network
let data: Data = try await apiService.request(endpoint)
// 4. Store in caches
memoryCache.set(data, forKey: cacheKey)
try? diskCache.setData(data, forKey: cacheKey)
return try decode(data: data)
}
private func decode<T: Decodable>(data: Data) throws -> T {
return try JSONDecoder().decode(T.self, from: data)
}
}
Authentication & Security
Token-Based Auth
class AuthManager {
static let shared = AuthManager()
private let tokenKey = "auth_token"
private let refreshTokenKey = "refresh_token"
var token: String? {
get {
return KeychainManager.shared.get(key: tokenKey)
}
set {
if let token = newValue {
KeychainManager.shared.set(token, forKey: tokenKey)
} else {
KeychainManager.shared.delete(key: tokenKey)
}
}
}
var refreshToken: String? {
get {
return KeychainManager.shared.get(key: refreshTokenKey)
}
set {
if let token = newValue {
KeychainManager.shared.set(token, forKey: refreshTokenKey)
} else {
KeychainManager.shared.delete(key: refreshTokenKey)
}
}
}
func login(email: String, password: String) async throws -> AuthResponse {
let endpoint = AuthEndpoint.login(email: email, password: password)
let response: AuthResponse = try await apiService.request(endpoint)
token = response.token
refreshToken = response.refreshToken
return response
}
func refreshAccessToken() async throws {
guard let refreshToken = refreshToken else {
throw AuthError.noRefreshToken
}
let endpoint = AuthEndpoint.refresh(token: refreshToken)
let response: AuthResponse = try await apiService.request(endpoint)
token = response.token
self.refreshToken = response.refreshToken
}
func logout() {
token = nil
refreshToken = nil
}
}
Keychain Storage
import Security
class KeychainManager {
static let shared = KeychainManager()
func set(_ value: String, forKey key: String) {
guard let data = value.data(using: .utf8) else { return }
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key,
kSecValueData as String: data
]
// Delete existing
SecItemDelete(query as CFDictionary)
// Add new
SecItemAdd(query as CFDictionary, nil)
}
func get(key: String) -> String? {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key,
kSecReturnData as String: true
]
var result: AnyObject?
let status = SecItemCopyMatching(query as CFDictionary, &result)
guard status == errSecSuccess,
let data = result as? Data,
let value = String(data: data, encoding: .utf8) else {
return nil
}
return value
}
func delete(key: String) {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key
]
SecItemDelete(query as CFDictionary)
}
}
Request Interceptor (Token Refresh)
class AuthenticatedAPIService: APIServiceProtocol {
private let baseService: APIServiceProtocol
private let authManager: AuthManager
init(baseService: APIServiceProtocol, authManager: AuthManager) {
self.baseService = baseService
self.authManager = authManager
}
func request<T: Decodable>(_ endpoint: APIEndpoint) async throws -> T {
do {
return try await baseService.request(endpoint)
} catch NetworkError.unauthorized {
// Token expired, refresh and retry
try await authManager.refreshAccessToken()
return try await baseService.request(endpoint)
}
}
}
SSL Pinning
class SSLPinningDelegate: NSObject, URLSessionDelegate {
private let pinnedCertificates: [Data]
init(pinnedCertificates: [Data]) {
self.pinnedCertificates = pinnedCertificates
}
func urlSession(
_ session: URLSession,
didReceive challenge: URLAuthenticationChallenge,
completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void
) {
guard challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust,
let serverTrust = challenge.protectionSpace.serverTrust else {
completionHandler(.cancelAuthenticationChallenge, nil)
return
}
// Get server certificate
guard let serverCertificate = SecTrustGetCertificateAtIndex(serverTrust, 0) else {
completionHandler(.cancelAuthenticationChallenge, nil)
return
}
let serverCertificateData = SecCertificateCopyData(serverCertificate) as Data
// Check if matches pinned certificates
if pinnedCertificates.contains(serverCertificateData) {
let credential = URLCredential(trust: serverTrust)
completionHandler(.useCredential, credential)
} else {
completionHandler(.cancelAuthenticationChallenge, nil)
}
}
}
// Usage
let pinnedCert = /* Load from bundle */
let delegate = SSLPinningDelegate(pinnedCertificates: [pinnedCert])
let session = URLSession(configuration: .default, delegate: delegate, delegateQueue: nil)
Privacy Manifest (iOS 17+)
Starting with iOS 17, Apple requires apps to declare their use of certain APIs through a privacy manifest file (PrivacyInfo.xcprivacy). This is crucial for networking code that collects data.
Required Reasons APIs for Networking
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSPrivacyTracking</key>
<false/>
<key>NSPrivacyTrackingDomains</key>
<array>
<!-- List domains used for tracking if applicable -->
</array>
<key>NSPrivacyCollectedDataTypes</key>
<array>
<dict>
<key>NSPrivacyCollectedDataType</key>
<string>NSPrivacyCollectedDataTypeUserID</string>
<key>NSPrivacyCollectedDataTypeLinked</key>
<true/>
<key>NSPrivacyCollectedDataTypeTracking</key>
<false/>
<key>NSPrivacyCollectedDataTypePurposes</key>
<array>
<string>NSPrivacyCollectedDataTypePurposeAppFunctionality</string>
</array>
</dict>
</array>
<key>NSPrivacyAccessedAPITypes</key>
<array>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>CA92.1</string> <!-- Access user defaults for app functionality -->
</array>
</dict>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategorySystemBootTime</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>35F9.1</string> <!-- Used for measuring network request timing -->
</array>
</dict>
</array>
</dict>
</plist>
Common Networking-Related Privacy Requirements
// When using device identifiers in network requests
struct NetworkTracker {
// ⚠️ Requires privacy manifest declaration
func trackEvent(_ event: String) async {
let deviceID = UIDevice.current.identifierForVendor?.uuidString
// Must declare use of device identifier in privacy manifest
let body = [
"event": event,
"device_id": deviceID,
"timestamp": Date().timeIntervalSince1970
]
// Send to analytics endpoint
try? await apiService.post("/analytics", body: body)
}
}
// When collecting user data
struct UserDataCollector {
// ⚠️ Must declare in NSPrivacyCollectedDataTypes
func collectUserData() -> UserData {
return UserData(
userId: getUserId(), // NSPrivacyCollectedDataTypeUserID
email: getEmail(), // NSPrivacyCollectedDataTypeEmailAddress
location: getLocation(), // NSPrivacyCollectedDataTypeCoarseLocation
deviceInfo: getDeviceInfo() // NSPrivacyCollectedDataTypeDeviceID
)
}
}
Third-Party SDK Compliance
// Check third-party SDKs for privacy manifest requirements
class NetworkingSDKManager {
func validateSDKPrivacyCompliance() {
// Common networking SDKs that require privacy manifests:
// - Analytics SDKs (Firebase, Amplitude, Mixpanel)
// - Crash reporting (Crashlytics, Sentry)
// - Ad networks (AdMob, Facebook Ads)
// - Social SDKs (Facebook, Twitter)
// Example: Ensure Firebase includes its privacy manifest
#if canImport(FirebaseAnalytics)
// Firebase SDK must include its own PrivacyInfo.xcprivacy
// Your app inherits these requirements
#endif
}
}
Best Practices for Privacy Compliance
// 1. Minimize data collection
struct MinimalDataNetworkClient {
func makeRequest() async throws {
// ✅ Good: Only collect necessary data
let request = APIRequest(
endpoint: "/api/data",
headers: ["Content-Type": "application/json"]
)
// ❌ Avoid: Unnecessary data collection
// let request = APIRequest(
// endpoint: "/api/data",
// headers: [
// "X-Device-ID": UIDevice.current.identifierForVendor,
// "X-User-Location": getCurrentLocation(),
// "X-Contacts-Count": getContactsCount()
// ]
// )
}
}
// 2. Declare all API usage accurately
extension NetworkManager {
// If using UserDefaults for caching
func cacheResponse(_ data: Data, for key: String) {
// ⚠️ Requires NSPrivacyAccessedAPICategoryUserDefaults
UserDefaults.standard.set(data, forKey: key)
}
// If measuring timing
func measureRequestDuration() -> TimeInterval {
// ⚠️ Requires NSPrivacyAccessedAPICategorySystemBootTime
let bootTime = ProcessInfo.processInfo.systemUptime
return bootTime
}
}
// 3. Document privacy practices
/// NetworkClient handles all API communication
/// Privacy: This class collects user ID for authentication only.
/// No tracking or third-party sharing occurs.
class NetworkClient {
// Implementation
}
Data Persistence
UserDefaults
// Simple values
UserDefaults.standard.set("John", forKey: "username")
let username = UserDefaults.standard.string(forKey: "username")
// Custom objects (Codable)
struct Settings: Codable {
let darkMode: Bool
let fontSize: Int
}
let settings = Settings(darkMode: true, fontSize: 14)
// Save
let encoder = JSONEncoder()
if let encoded = try? encoder.encode(settings) {
UserDefaults.standard.set(encoded, forKey: "settings")
}
// Load
if let data = UserDefaults.standard.data(forKey: "settings") {
let decoder = JSONDecoder()
if let decoded = try? decoder.decode(Settings.self, from: data) {
// Use decoded settings
}
}
// Property wrapper for UserDefaults
@propertyWrapper
struct UserDefault<T: Codable> {
let key: String
let defaultValue: T
var wrappedValue: T {
get {
guard let data = UserDefaults.standard.data(forKey: key) else {
return defaultValue
}
return (try? JSONDecoder().decode(T.self, from: data)) ?? defaultValue
}
set {
let data = try? JSONEncoder().encode(newValue)
UserDefaults.standard.set(data, forKey: key)
}
}
}
// Usage
struct AppSettings {
@UserDefault(key: "username", defaultValue: "Guest")
var username: String
@UserDefault(key: "isDarkMode", defaultValue: false)
var isDarkMode: Bool
}
Core Data Basics
// ⚠️ Core Data is complex - this is simplified
import CoreData
class CoreDataManager {
static let shared = CoreDataManager()
lazy var persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: "MyApp")
container.loadPersistentStores { description, error in
if let error = error {
fatalError("Core Data failed to load: \(error)")
}
}
return container
}()
var context: NSManagedObjectContext {
return persistentContainer.viewContext
}
func saveContext() {
if context.hasChanges {
do {
try context.save()
} catch {
print("Failed to save: \(error)")
}
}
}
}
// Create
func createUser(name: String, email: String) {
let context = CoreDataManager.shared.context
let user = UserEntity(context: context)
user.id = UUID()
user.name = name
user.email = email
CoreDataManager.shared.saveContext()
}
// Read
func fetchUsers() -> [UserEntity] {
let context = CoreDataManager.shared.context
let fetchRequest: NSFetchRequest<UserEntity> = UserEntity.fetchRequest()
do {
return try context.fetch(fetchRequest)
} catch {
print("Failed to fetch: \(error)")
return []
}
}
// Update
func updateUser(_ user: UserEntity, name: String) {
user.name = name
CoreDataManager.shared.saveContext()
}
// Delete
func deleteUser(_ user: UserEntity) {
let context = CoreDataManager.shared.context
context.delete(user)
CoreDataManager.shared.saveContext()
}
File System Storage
class FileStorageManager {
static let shared = FileStorageManager()
private let fileManager = FileManager.default
private var documentsDirectory: URL {
return fileManager.urls(for: .documentDirectory, in: .userDomainMask)[0]
}
func save<T: Encodable>(_ object: T, to filename: String) throws {
let url = documentsDirectory.appendingPathComponent(filename)
let encoder = JSONEncoder()
let data = try encoder.encode(object)
try data.write(to: url)
}
func load<T: Decodable>(from filename: String) throws -> T {
let url = documentsDirectory.appendingPathComponent(filename)
let data = try Data(contentsOf: url)
let decoder = JSONDecoder()
return try decoder.decode(T.self, from: data)
}
func delete(filename: String) throws {
let url = documentsDirectory.appendingPathComponent(filename)
try fileManager.removeItem(at: url)
}
func fileExists(filename: String) -> Bool {
let url = documentsDirectory.appendingPathComponent(filename)
return fileManager.fileExists(atPath: url.path)
}
}
// Usage
let users = [User(id: "1", name: "John", email: "john@example.com")]
try? FileStorageManager.shared.save(users, to: "users.json")
let loadedUsers: [User]? = try? FileStorageManager.shared.load(from: "users.json")
Image Loading & Caching
Basic Image Loading
func loadImage(from url: URL) async throws -> UIImage {
let (data, _) = try await URLSession.shared.data(from: url)
guard let image = UIImage(data: data) else {
throw ImageError.invalidData
}
return image
}
// Usage in UIImageView
Task {
if let image = try? await loadImage(from: url) {
imageView.image = image
}
}
Image Cache
class ImageCache {
static let shared = ImageCache()
private let cache = NSCache<NSURL, UIImage>()
private let fileManager = FileManager.default
private var cacheDirectory: URL {
let urls = fileManager.urls(for: .cachesDirectory, in: .userDomainMask)
return urls[0].appendingPathComponent("ImageCache")
}
init() {
// Create cache directory
try? fileManager.createDirectory(at: cacheDirectory, withIntermediateDirectories: true)
// Set cache limits
cache.countLimit = 100 // Max 100 images
cache.totalCostLimit = 50 * 1024 * 1024 // 50 MB
}
func image(for url: URL) -> UIImage? {
// Check memory cache
if let cached = cache.object(forKey: url as NSURL) {
return cached
}
// Check disk cache
let filename = url.lastPathComponent
let fileURL = cacheDirectory.appendingPathComponent(filename)
if let data = try? Data(contentsOf: fileURL),
let image = UIImage(data: data) {
// Store in memory cache
cache.setObject(image, forKey: url as NSURL)
return image
}
return nil
}
func setImage(_ image: UIImage, for url: URL) {
// Store in memory cache
cache.setObject(image, forKey: url as NSURL)
// Store in disk cache
let filename = url.lastPathComponent
let fileURL = cacheDirectory.appendingPathComponent(filename)
if let data = image.jpegData(compressionQuality: 0.8) {
try? data.write(to: fileURL)
}
}
func clearCache() {
cache.removeAllObjects()
try? fileManager.removeItem(at: cacheDirectory)
try? fileManager.createDirectory(at: cacheDirectory, withIntermediateDirectories: true)
}
}
Image Loader with Cache
class ImageLoader {
static let shared = ImageLoader()
private var tasks: [URL: Task<UIImage, Error>] = [:]
func loadImage(from url: URL) async throws -> UIImage {
// Check cache first
if let cachedImage = ImageCache.shared.image(for: url) {
return cachedImage
}
// Check if already loading
if let existingTask = tasks[url] {
return try await existingTask.value
}
// Create new task
let task = Task<UIImage, Error> {
let (data, _) = try await URLSession.shared.data(from: url)
guard let image = UIImage(data: data) else {
throw ImageError.invalidData
}
// Cache the image
ImageCache.shared.setImage(image, for: url)
return image
}
tasks[url] = task
defer {
tasks[url] = nil
}
return try await task.value
}
}
// Usage in cell
class ImageCell: UITableViewCell {
@IBOutlet weak var cellImageView: UIImageView!
private var loadTask: Task<Void, Never>?
func configure(with imageURL: URL) {
cellImageView.image = nil
// Cancel previous load
loadTask?.cancel()
loadTask = Task {
if let image = try? await ImageLoader.shared.loadImage(from: imageURL) {
// Check if cell wasn't reused
guard !Task.isCancelled else { return }
await MainActor.run {
self.cellImageView.image = image
}
}
}
}
override func prepareForReuse() {
super.prepareForReuse()
loadTask?.cancel()
cellImageView.image = nil
}
}
Offline Support
Network Reachability
import Network
class NetworkMonitor {
static let shared = NetworkMonitor()
private let monitor = NWPathMonitor()
private let queue = DispatchQueue(label: "NetworkMonitor")
@Published private(set) var isConnected = true
@Published private(set) var connectionType: ConnectionType = .unknown
enum ConnectionType {
case wifi
case cellular
case ethernet
case unknown
}
private init() {
monitor.pathUpdateHandler = { [weak self] path in
DispatchQueue.main.async {
self?.isConnected = path.status == .satisfied
if path.usesInterfaceType(.wifi) {
self?.connectionType = .wifi
} else if path.usesInterfaceType(.cellular) {
self?.connectionType = .cellular
} else if path.usesInterfaceType(.wiredEthernet) {
self?.connectionType = .ethernet
} else {
self?.connectionType = .unknown
}
}
}
monitor.start(queue: queue)
}
}
// Usage
if NetworkMonitor.shared.isConnected {
// Fetch from network
} else {
// Use cached data
}
Offline-First Architecture
protocol UserRepositoryProtocol {
func fetchUsers() async throws -> [User]
}
class UserRepository: UserRepositoryProtocol {
private let apiService: APIServiceProtocol
private let localDatabase: LocalDatabase
private let networkMonitor: NetworkMonitor
func fetchUsers() async throws -> [User] {
if networkMonitor.isConnected {
// Fetch from API
do {
let users = try await apiService.fetchUsers()
// Save to local database
try await localDatabase.save(users)
return users
} catch {
// Network failed, fallback to local
return try await localDatabase.fetchUsers()
}
} else {
// Offline, use local database
return try await localDatabase.fetchUsers()
}
}
}
Sync Queue for Offline Operations
struct PendingOperation: Codable {
let id: UUID
let type: OperationType
let data: Data
let timestamp: Date
enum OperationType: String, Codable {
case create
case update
case delete
}
}
class SyncManager {
static let shared = SyncManager()
private var pendingOperations: [PendingOperation] = []
private let queueKey = "pending_operations"
init() {
loadQueue()
observeNetworkChanges()
}
func addOperation(_ operation: PendingOperation) {
pendingOperations.append(operation)
saveQueue()
if NetworkMonitor.shared.isConnected {
Task {
await syncPendingOperations()
}
}
}
private func syncPendingOperations() async {
guard NetworkMonitor.shared.isConnected else { return }
for operation in pendingOperations {
do {
try await execute(operation)
// Remove from queue on success
pendingOperations.removeAll { $0.id == operation.id }
saveQueue()
} catch {
print("Failed to sync operation: \(error)")
// Keep in queue for retry
}
}
}
private func execute(_ operation: PendingOperation) async throws {
// Execute the operation via API
}
private func loadQueue() {
if let data = UserDefaults.standard.data(forKey: queueKey) {
pendingOperations = (try? JSONDecoder().decode([PendingOperation].self, from: data)) ?? []
}
}
private func saveQueue() {
if let data = try? JSONEncoder().encode(pendingOperations) {
UserDefaults.standard.set(data, forKey: queueKey)
}
}
private func observeNetworkChanges() {
// When network becomes available, sync
NotificationCenter.default.addObserver(
forName: .networkBecameAvailable,
object: nil,
queue: .main
) { [weak self] _ in
Task {
await self?.syncPendingOperations()
}
}
}
}
Interview Questions
Q: “Explain the difference between URLSession data task, download task, and upload task”
A:
- Data task: Retrieves data into memory. Good for API requests, small files. Data is lost if app terminates.
- Download task: Saves directly to disk. Good for large files, videos. Can resume after app termination with background session.
- Upload task: Sends data to server. Supports background uploads. Can resume after app termination.
Background sessions persist across app launches, regular sessions don’t.
Q: “How would you implement a networking layer for a large app?”
A: Key components:
- Protocol-based: Define APIEndpoint protocol for endpoints
- Generic service: APIService that handles any endpoint
- Error handling: Custom error types with proper categorization
- Authentication: Interceptor pattern for token refresh
- Caching: Multi-layer (memory → disk → network)
- Dependency injection: Testable with mock services
- Type safety: Codable for parsing, strong types everywhere
Q: “How do you handle offline support?”
A:
- Network monitoring: NWPathMonitor to detect connectivity
- Local database: Core Data or SQLite for offline storage
- Repository pattern: Abstract data source (API vs local)
- Sync queue: Queue operations when offline, sync when online
- Cache strategy: Aggressive caching with TTL
- UI feedback: Show offline banner, disable features gracefully
Q: “What’s your approach to image caching?”
A:
- Two-tier cache: Memory (NSCache) + Disk (FileManager)
- Memory limits: Set countLimit and totalCostLimit on NSCache
- Request deduplication: Don’t load same image multiple times
- Cell reuse: Cancel tasks in prepareForReuse()
- Placeholder: Show placeholder while loading
- Error handling: Show error state if load fails
Consider using SDWebImage or Kingfisher in production.
Q: “How do you secure API calls?”
A:
- HTTPS only: Never use HTTP
- Token auth: Bearer tokens, not credentials in each request
- Keychain storage: Store tokens in Keychain, not UserDefaults
- SSL pinning: Pin certificates for critical APIs
- Token refresh: Automatic refresh when 401 received
- Request signing: HMAC signatures for sensitive operations
- Rate limiting: Client-side throttling to prevent abuse
Q: “Explain Codable and how you’d handle complex JSON”
A: Codable = Encodable + Decodable. Swift automatically synthesizes encoding/decoding.
For complex JSON:
- CodingKeys: Map different property names
- Custom init(from:): Handle nested structures, default values
- Associated types: Polymorphic decoding based on type field
- KeyDecodingStrategy: convertFromSnakeCase for API consistency
- DateDecodingStrategy: Handle different date formats
Q: “What’s the difference between synchronous and asynchronous networking?”
A:
- Synchronous: Blocks thread until complete. Never use on main thread - freezes UI.
- Asynchronous: Returns immediately, calls completion handler when done. Always use for networking.
Modern approach is async/await which looks synchronous but is asynchronous under the hood.
This covers everything you need to know about networking and data for iOS interviews. Focus on async/await, caching strategies, and offline support - these are critical topics.