Visual overview
Use the workflow to follow the task, and the architecture map to separate responsibilities. These are conceptual maps; the guide below defines implementation details and verification limits.
- Define a data contract
- Transform or decode values
- Handle dates files and URLs
- Test malformed input
- Boundary 1Input values
- Boundary 2Foundation operations
- Boundary 3Domain values
Connected responsibilities, not a required class hierarchy or an execution trace.
URLSession — Async Networking
// Basic async data request
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 httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode) else {
throw URLError(.badServerResponse)
}
return try JSONDecoder().decode(User.self, from: data)
}
// Download with progress
func downloadFile(from url: URL) async throws -> URL {
let (localURL, response) = try await URLSession.shared.download(from: url)
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200 else {
throw URLError(.badServerResponse)
}
// Move from temp to permanent location
let destination = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
.appendingPathComponent(url.lastPathComponent)
try FileManager.default.moveItem(at: localURL, to: destination)
return destination
}
// Upload data
func uploadImage(_ imageData: Data) async throws -> UploadResponse {
var request = URLRequest(url: URL(string: "https://api.example.com/upload")!)
request.httpMethod = "POST"
request.setValue("image/jpeg", forHTTPHeaderField: "Content-Type")
let (data, _) = try await URLSession.shared.upload(for: request, from: imageData)
return try JSONDecoder().decode(UploadResponse.self, from: data)
}
// Custom URLSession configuration
let config = URLSessionConfiguration.default
config.timeoutIntervalForRequest = 30
config.waitsForConnectivity = true
config.requestCachePolicy = .returnCacheDataElseLoad
config.httpAdditionalHeaders = ["Authorization": "Bearer \(token)"]
let session = URLSession(configuration: config)
FileManager
let fm = FileManager.default
// App directories
let documentsURL = fm.urls(for: .documentDirectory, in: .userDomainMask).first!
let cachesURL = fm.urls(for: .cachesDirectory, in: .userDomainMask).first!
let appSupportURL = fm.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!
// Create directory
let logsDir = documentsURL.appendingPathComponent("Logs", isDirectory: true)
try fm.createDirectory(at: logsDir, withIntermediateDirectories: true)
// Write and read string
let filePath = documentsURL.appendingPathComponent("notes.txt")
try "Hello, world!".write(to: filePath, atomically: true, encoding: .utf8)
let contents = try String(contentsOf: filePath, encoding: .utf8)
// Write and read JSON
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
let jsonData = try encoder.encode(myModel)
try jsonData.write(to: documentsURL.appendingPathComponent("data.json"))
// File attributes
let attrs = try fm.attributesOfItem(atPath: filePath.path)
let fileSize = attrs[.size] as? UInt64
let modified = attrs[.modificationDate] as? Date
// List directory contents
let items = try fm.contentsOfDirectory(
at: documentsURL,
includingPropertiesForKeys: [.fileSizeKey, .creationDateKey],
options: .skipsHiddenFiles
)
// Check existence and delete
if fm.fileExists(atPath: filePath.path) {
try fm.removeItem(at: filePath)
}
// Copy and move
try fm.copyItem(at: source, to: destination)
try fm.moveItem(at: source, to: destination)
UserDefaults and @AppStorage
// Basic UserDefaults
let defaults = UserDefaults.standard
defaults.set("dark", forKey: "theme")
defaults.set(true, forKey: "notificationsEnabled")
defaults.set(42, forKey: "launchCount")
let theme = defaults.string(forKey: "theme") ?? "system"
// Register defaults (call in AppDelegate or App init)
UserDefaults.standard.register(defaults: [
"theme": "system",
"notificationsEnabled": true,
"launchCount": 0,
])
// App Groups for sharing between app and extensions
let sharedDefaults = UserDefaults(suiteName: "group.com.myapp.shared")
sharedDefaults?.set(true, forKey: "isPremium")
// @AppStorage in SwiftUI — auto-persists to UserDefaults
struct SettingsView: View {
@AppStorage("theme") private var theme = "system"
@AppStorage("fontSize") private var fontSize = 16.0
@AppStorage("isPremium", store: UserDefaults(suiteName: "group.com.myapp.shared"))
private var isPremium = false
var body: some View {
Form {
Picker("Theme", selection: $theme) {
Text("System").tag("system")
Text("Light").tag("light")
Text("Dark").tag("dark")
}
Slider(value: $fontSize, in: 12...24, step: 1) {
Text("Font Size: \(Int(fontSize))")
}
}
}
}
JSONEncoder/JSONDecoder and Codable
// Basic Codable model
struct Article: Codable, Identifiable {
let id: Int
let title: String
let body: String
let publishedAt: Date
let author: Author
let tags: [String]
struct Author: Codable {
let name: String
let avatarURL: URL
enum CodingKeys: String, CodingKey {
case name
case avatarURL = "avatar_url"
}
}
}
// Configured decoder
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
decoder.dateDecodingStrategy = .iso8601
let articles = try decoder.decode([Article].self, from: jsonData)
// Configured encoder
let encoder = JSONEncoder()
encoder.keyEncodingStrategy = .convertToSnakeCase
encoder.dateEncodingStrategy = .iso8601
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
let data = try encoder.encode(articles)
// Custom Codable for complex JSON
struct SearchResult: Codable {
let query: String
let results: [Item]
struct Item: Codable {
let id: String
let score: Double
}
enum CodingKeys: String, CodingKey {
case query = "q"
case results = "hits"
}
// Custom decoding
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
query = try container.decode(String.self, forKey: .query)
results = try container.decodeIfPresent([Item].self, forKey: .results) ?? []
}
}
Formatters
// DateFormatter
let dateFormatter = DateFormatter()
dateFormatter.dateStyle = .medium
dateFormatter.timeStyle = .short
dateFormatter.locale = Locale(identifier: "en_US")
let dateString = dateFormatter.string(from: Date()) // "Mar 30, 2026 at 3:45 PM"
// ISO 8601
let isoFormatter = ISO8601DateFormatter()
isoFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
let isoString = isoFormatter.string(from: Date())
// RelativeDateTimeFormatter
let relativeFormatter = RelativeDateTimeFormatter()
relativeFormatter.unitsStyle = .full
relativeFormatter.string(from: Date().addingTimeInterval(-3600)) // "1 hour ago"
// NumberFormatter
let currencyFormatter = NumberFormatter()
currencyFormatter.numberStyle = .currency
currencyFormatter.locale = Locale(identifier: "en_US")
currencyFormatter.string(from: 49.99) // "$49.99"
let percentFormatter = NumberFormatter()
percentFormatter.numberStyle = .percent
percentFormatter.maximumFractionDigits = 1
percentFormatter.string(from: 0.856) // "85.6%"
// Modern formatted API (iOS 15+)
let formatted = Date.now.formatted(.dateTime.month(.wide).day().year())
let price = 49.99.formatted(.currency(code: "USD"))
let percent = 0.856.formatted(.percent.precision(.fractionLength(1)))
NotificationCenter
// Define custom notification
extension Notification.Name {
static let userDidLogin = Notification.Name("userDidLogin")
static let cartUpdated = Notification.Name("cartUpdated")
}
// Post notification with userInfo
NotificationCenter.default.post(
name: .userDidLogin,
object: nil,
userInfo: ["userId": "abc123", "timestamp": Date()]
)
// Observe with closure (returns token — store it)
let token = NotificationCenter.default.addObserver(
forName: .cartUpdated,
object: nil,
queue: .main
) { notification in
if let count = notification.userInfo?["itemCount"] as? Int {
print("Cart has \(count) items")
}
}
// Observe with async sequence (iOS 17+)
func observeLoginEvents() async {
for await notification in NotificationCenter.default.notifications(named: .userDidLogin) {
if let userId = notification.userInfo?["userId"] as? String {
print("User logged in: \(userId)")
}
}
}
// Remove observer
NotificationCenter.default.removeObserver(token)
Timer and RunLoop
// Scheduled repeating timer
let timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { timer in
print("Tick")
}
timer.invalidate() // Stop the timer
// Timer with tolerance for battery efficiency
let efficientTimer = Timer.scheduledTimer(withTimeInterval: 60.0, repeats: true) { _ in
refreshData()
}
efficientTimer.tolerance = 5.0 // System can delay up to 5 seconds
// Async timer pattern
func startCountdown(from seconds: Int) async {
for remaining in stride(from: seconds, through: 0, by: -1) {
print("\(remaining)...")
try? await Task.sleep(for: .seconds(1))
}
}
// Timer publisher (Combine)
import Combine
let timerPublisher = Timer.publish(every: 1.0, on: .main, in: .common).autoconnect()
ProcessInfo and Bundle
// ProcessInfo
let processInfo = ProcessInfo.processInfo
let systemUptime = processInfo.systemUptime
let isLowPowerMode = processInfo.isLowPowerModeEnabled
let thermalState = processInfo.thermalState // .nominal, .fair, .serious, .critical
let osVersion = processInfo.operatingSystemVersion // (major: 18, minor: 0, patch: 0)
// Check for Xcode preview or simulator
#if DEBUG
let isPreview = processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1"
#endif
// Bundle
let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String
let buildNumber = Bundle.main.infoDictionary?["CFBundleVersion"] as? String
let bundleId = Bundle.main.bundleIdentifier
// Load bundled resource
if let url = Bundle.main.url(forResource: "config", withExtension: "json"),
let data = try? Data(contentsOf: url) {
let config = try JSONDecoder().decode(AppConfig.self, from: data)
}
// Localized strings
let greeting = Bundle.main.localizedString(forKey: "greeting", value: nil, table: nil)
// Or use the macro: String(localized: "greeting")