Experimental dev diary

(AI generated summaries of topics that were interesting to me)

April 2026

withCheckedContinuation Runs Synchronously

withCheckedContinuation’s closure runs synchronously on the caller’s thread before the task suspends. From @MainActor, that’s the main thread.

@MainActor
func bad() async -> Int {
    await withCheckedContinuation { continuation in
        print(Thread.isMainThread) // true
        Thread.sleep(forTimeInterval: 2) // freezes UI for 2s
        continuation.resume(returning: 1)
    }
}

The task never gets a chance to suspend until resume() is called. While the closure blocks, the main run loop is stuck — no UI updates, no timers, nothing.

Fix: dispatch the blocking work, return immediately:

@MainActor
func fixed() async -> Int {
    await withCheckedContinuation { continuation in
        DispatchQueue.global(qos: .userInitiated).async {
            Thread.sleep(forTimeInterval: 2) // background thread
            continuation.resume(returning: 1)
        }
        // closure returns here → task suspends → main thread is free
    }
}

The closure exits right away, the task suspends, and the main thread is released. The continuation resumes from the background queue when the work finishes.

The rule: inside a continuation closure, only schedule work — never do it.

Task vs Task.detached

Both create unstructured tasks. Three things differ.

Actor isolation

Task inherits the actor context of its creator. Task.detached is always nonisolated.

actor Counter {
    var value = 0

    func increment() {
        Task { value += 1 }          // runs on Counter actor ✓
        Task.detached { value += 1 } // compile error — crosses actor boundary ✗
    }
}

Task locals

Task inherits task-local values from the calling context. Task.detached starts with no task locals.

@TaskLocal static var requestID: String = ""

TaskLocal.$requestID.withValue("abc") {
    Task { print(requestID) }          // "abc"
    Task.detached { print(requestID) } // ""
}

Priority

Task inherits the priority of its creator. Task.detached defaults to .medium unless specified explicitly.

Task { }                                    // inherits caller priority
Task.detached { }                           // .medium
Task.detached(priority: .background) { }    // explicit

When you want fire-and-forget work that’s aware of its context — use Task. When you explicitly want isolation from the current actor and environment — use Task.detached.

Actor Reentrancy

Actors prevent concurrent access to their state — but suspension points release the actor. Other tasks can mutate state while you’re awaiting.

actor Cache {
    var store: [String: Data] = [:]

    func load(_ key: String) async throws -> Data {
        if let cached = store[key] { return cached }

        let data = try await fetch(key) // actor released here

        store[key] = data // another task may have already written this
        return data
    }
}

Two callers with the same key both pass the store[key] check, both suspend on fetch, and both write the result. Redundant network requests at best, a race at worst.

Fix: re-check after the await:

let data = try await fetch(key)

if store[key] == nil {
    store[key] = data
}

Fix: track in-flight requests:

actor Cache {
    var store: [String: Data] = [:]
    var inFlight: [String: Task<Data, Error>] = [:]

    func load(_ key: String) async throws -> Data {
        if let cached = store[key] { return cached }
        if let task = inFlight[key] { return try await task.value }

        let task = Task { try await fetch(key) }
        inFlight[key] = task
        let data = try await task.value
        store[key] = data
        inFlight[key] = nil
        return data
    }
}

One fetch per key, regardless of how many callers race to load simultaneously.

The rule: never assume actor state is unchanged across an await.

March 2026

withThrowingTaskGroup

withThrowingTaskGroup runs child tasks concurrently and cancels the remaining ones the moment any single child throws.

let results = try await withThrowingTaskGroup(of: Data.self) { group in
    for url in urls {
        group.addTask { try await URLSession.shared.data(from: url).0 }
    }

    var collected: [Data] = []
    for try await data in group {
        collected.append(data)
    }
    return collected
}

All addTask closures start immediately and run in parallel. The for try await loop collects results as they finish — not in submission order.

First throw cancels the group:

group.addTask { throw URLError(.badURL) } // this one fails
group.addTask { try await slowFetch() }   // gets cancelled

The error propagates out of the for try await loop, the group cancels remaining tasks, and withThrowingTaskGroup rethrows. You don’t get partial results.

Collecting partial results instead:

If you want to keep whatever succeeded, catch inside addTask:

group.addTask {
    try? await URLSession.shared.data(from: url).0 // nil on failure
}

Or use withTaskGroup (non-throwing) with a Result return type:

withTaskGroup(of: Result<Data, Error>.self) { group in
    group.addTask { await Result { try await fetch(url) } }
}

addTaskUnlessCancelled:

Skips adding the task if the group is already cancelled — useful when building the task list in a loop after some tasks have already failed:

for url in urls {
    guard group.addTaskUnlessCancelled(operation: { try await fetch(url) })
    else { break }
}

Cancellation-Sensitivity in Swift Concurrency

Task cancellation in Swift concurrency is cooperative. Cancelling a task sets a flag — it doesn’t stop execution. Your code has to check.

The flag:

let task = Task {
    for item in largeDataset {
        if Task.isCancelled { break }
        process(item)
    }
}

task.cancel()

Without the isCancelled check, task.cancel() has no effect until the task finishes naturally.

checkCancellation() as an early exit:

func processAll(_ items: [Item]) async throws {
    for item in items {
        try Task.checkCancellation() // throws CancellationError if cancelled
        await process(item)
    }
}

checkCancellation() throws CancellationError, which propagates up and unwinds the call stack. Useful when you’re deep in a chain and don’t want to thread a return value back up manually.

Structured propagation:

Cancellation flows down the task tree automatically. Cancelling a parent cancels all its children.

let parent = Task {
    async let a = fetchUsers()
    async let b = fetchPosts()
    return try await (a, b) // cancel parent → both children cancelled
}

parent.cancel()

Child tasks created with async let or inside TaskGroup inherit cancellation. Unstructured tasks (Task { }) do not — they’re detached from the parent’s cancellation scope.

Cancellation-sensitive suspension points:

try await Task.sleep(for:) throws on cancellation:

func poll() async throws {
    while true {
        try await Task.sleep(for: .seconds(5)) // exits immediately when cancelled
        await refresh()
    }
}

URLSession.data(for:) also responds to cancellation — it cancels the underlying network request and throws. Most system async APIs that involve waiting are cancellation-sensitive.

withTaskCancellationHandler for non-async cleanup:

When you’re wrapping a callback-based API, the async body may be suspended and never reach a checkCancellation call. Use this to hook cancellation explicitly:

func fetchData() async throws -> Data {
    try await withTaskCancellationHandler {
        try await withCheckedThrowingContinuation { continuation in
            let request = legacyFetch { result in
                continuation.resume(with: result)
            }
        }
    } onCancel: {
        request.cancel() // called immediately when task is cancelled
    }
}

The onCancel handler runs synchronously on cancellation, from whatever thread cancelled the task. Keep it short and thread-safe.

yyyy vs YYYY: The Date Format That Breaks Every January

Two format specifiers that look nearly identical and produce the same output 361 days a year.

yyyy — calendar year. The year the date falls in. YYYY — ISO week year. The year that owns the week the date falls in.

They diverge at year boundaries, because ISO weeks can straddle two calendar years. ISO week 1 is defined as the week containing the first Thursday of the year. That means late December dates can belong to week 1 of the next year.

December 29, 2019:

let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_US_POSIX")

formatter.dateFormat = "yyyy-MM-dd"
// → "2019-12-29" ✓

formatter.dateFormat = "YYYY-MM-dd"
// → "2020-12-29" ✗

December 29, 2019 falls in ISO week 1 of 2020. So YYYY returns 2020, but the day and month are still from 2019 — giving you a date that doesn’t exist.

Security as a UI Gate vs. Data-Level Encryption

Two architectures for protecting user data behind a PIN. One is fundamentally broken, the other isn’t.

Architecture A: The UI Gate

User enters PIN
        ↓
Compare PIN to stored value
        ↓
    Match? ──→ YES → showPhotos()
        │
        └──→ NO  → denyAccess()

The data sits unencrypted on disk. The PIN is just a boolean check. An attacker who can modify the runtime (hooking denyAccess to call showPhotos instead) bypasses the entire system without knowing the PIN.

Architecture B: PIN-Derived Encryption

User enters PIN
        ↓
key = PBKDF2(pin, salt, 100000 iterations)
        ↓
AES-256-GCM-Decrypt(key, encrypted_data)
        ↓
    Success? ──→ YES → display decrypted data
        │
        └───→ NO  → decryption fails, garbage output

There’s no comparison to bypass. The PIN is the encryption key (after derivation). Wrong PIN = wrong key = undecryptable data.

How PBKDF2 key derivation works:

import CommonCrypto

func deriveKey(pin: String, salt: Data) -> Data {
    var key = Data(count: 32) // 256-bit key
    let pinData = pin.data(using: .utf8)!

    key.withUnsafeMutableBytes { keyPtr in
        pinData.withUnsafeBytes { pinPtr in
            salt.withUnsafeBytes { saltPtr in
                CCKeyDerivationPBKDF(
                    CCPBKDFAlgorithm(kCCPBKDF2),
                    pinPtr.baseAddress, pinData.count,
                    saltPtr.baseAddress, salt.count,
                    CCPseudoRandomAlgorithm(kCCPRFHmacAlgSHA256),
                    100_000, // iterations — makes brute force slow
                    keyPtr.baseAddress, 32
                )
            }
        }
    }
    return key
}

PIN "1234" with one salt produces a7f3b2c1.... PIN "1235" with the same salt produces 9e41d0ff.... Completely different keys, completely different decryption results.

Argon2 as a PBKDF2 alternative:

PBKDF2 is CPU-hard only — each guess costs CPU time. Argon2 (RFC 9106, winner of the Password Hashing Competition) adds memory-hardness on top of that. Each guess requires allocating large amounts of RAM, which makes GPU and ASIC brute-force attacks impractical since memory is expensive to parallelize.

Debug Symbols: The dSYM Double-Edged Sword

dSYM bundles contain DWARF debug info - essential for crash symbolication, dangerous if leaked.

Source:

class SecretManager {                                    // line 3
    let apiKey = "sk_live_9a8b7c6d5e4f"                  // line 4

    func validateLicense(_ license: String) -> Bool {    // line 6
        return license == "XXXX-YYYY-ZZZZ"               // line 7
    }
}

let manager = SecretManager()                            // line 11
print(manager.validateLicense("test"))                   // line 12

Generate dSYM:

$ swiftc -g -c Demo.swift -o Demo.o
$ swiftc -g Demo.o -o Demo
$ dsymutil Demo -o Demo.dSYM

What --debug-info contains:

DW_TAG_* entries describe your code structure:

$ dwarfdump --debug-info Demo.dSYM

DW_TAG_structure_type                    # class/struct definition
  DW_AT_name          ("SecretManager")

DW_TAG_subprogram                        # function definition
  DW_AT_name          ("validateLicense")
  DW_AT_decl_file     ("/Users/dev/MyApp/Demo.swift")
  DW_AT_decl_line     (6)

DW_TAG_formal_parameter                  # function parameter
  DW_AT_name          ("license")

Address → source mapping with --lookup:

Given a crash address, find exact source location:

$ dwarfdump --lookup 0x100000dac Demo.dSYM
Line info: file 'Demo.swift', line 6    # func validateLicense

$ dwarfdump --lookup 0x100000de0 Demo.dSYM
Line info: file 'Demo.swift', line 7, column 27    # return statement

$ dwarfdump --lookup 0x100000b50 Demo.dSYM
Line info: file 'Demo.swift', line 11   # let manager = ...

Full source paths, class names, function names, parameter names, exact line/column - a roadmap to your code.

February 2026

Stripped vs Unstripped: What Survives

Let’s see what stripping actually removes.

Source:

class SecretManager {
    let apiKey = "sk_live_9a8b7c6d5e4f"
    let endpoint = "https://api.internal.company.com/v2"

    func validateLicense(_ license: String) -> Bool {
        return license == "XXXX-YYYY-ZZZZ"
    }
}

Unstripped - full symbol table:

$ nm MachODemo | xcrun swift-demangle | grep SecretManager
MachODemo.SecretManager.validateLicense(Swift.String) -> Swift.Bool
MachODemo.SecretManager.apiKey.getter : Swift.String
MachODemo.SecretManager.endpoint.getter : Swift.String
MachODemo.SecretManager.__allocating_init() -> MachODemo.SecretManager
...

Stripped - symbol table removed:

$ strip MachODemo -o MachODemo_stripped
$ nm MachODemo_stripped | grep SecretManager
(nothing)

But strings still finds everything:

$ strings MachODemo_stripped | grep -E "(sk_live|SecretManager|apiKey)"
sk_live_9a8b7c6d5e4f
https://api.internal.company.com/v2
XXXX-YYYY-ZZZZ
SecretManager
apiKey
endpoint

Stripping removes the symbol table (function addresses), but hardcoded strings and Swift reflection metadata stay embedded in __TEXT. The secrets survive.

What Mangled Swift Names Actually Leak

Let’s compare what the same class looks like in a compiled binary.

Swift source:

class UserController {
    func authenticate(with password: String) -> Bool {
        return password == "secret"
    }
}

ObjC source:

@interface UserController : NSObject
- (BOOL)authenticateWithPassword:(NSString *)password;
@end

ObjC in binary - plaintext:

$ nm ObjCAuth | grep -i user
0000000100000928 t -[UserController authenticateWithPassword:]
00000001000080c8 S _OBJC_CLASS_$_UserController

Swift in binary - mangled but decodable:

$ nm SwiftAuth | grep -i user
0000000100000cfc t _$s9SwiftAuth14UserControllerC12authenticate4withSbSS_tF
...

$ nm SwiftAuth | grep -i user | xcrun swift-demangle
0000000100000cfc t SwiftAuth.UserController.authenticate(with: Swift.String) -> Swift.Bool

The mangled symbol _$s9SwiftAuth14UserControllerC12authenticate4withSbSS_tF encodes:

  • Module: SwiftAuth (9 chars)
  • Class: UserController (14 chars, C = class)
  • Method: authenticate
  • Label: with
  • Types: Sb = Bool, SS = String

Both expose your API surface. Swift just requires one extra step.

The Compilation Pipeline: Where Obfuscation Lives

       ObjC                     Swift
         ↓                        ↓
    Preprocessor                  │
         ↓                        ↓
        AST                      AST
         │                        ↓
         │                    Raw SIL
         │                        ↓
         │                  Canonical SIL
         ↓                        ↓
      LLVM IR                  LLVM IR
         ↓                        ↓
     Assembly                 Assembly
         ↓                        ↓
    Object (.o)              Object (.o)
         └────────┬───────────────┘
                  ↓ linker
            Executable

View each stage:

# ObjC
clang -E file.m                    # preprocessed
clang -Xclang -ast-dump file.m     # AST
clang -S -emit-llvm file.m         # LLVM IR
clang -S file.m                    # assembly
clang -c file.m                    # object
clang file.o -o file               # executable

# Swift
swiftc -dump-ast file.swift        # AST
swiftc -emit-silgen file.swift     # raw SIL
swiftc -emit-sil file.swift        # canonical SIL
swiftc -emit-ir file.swift         # LLVM IR
swiftc -S file.swift               # assembly
swiftc -c file.swift               # object
swiftc file.o -o file              # executable

Where can obfuscation happen?

Level Tools Notes
Source Swift Shield Renames symbols before compile
LLVM IR OLLVM, Hikari Language agnostic, most common

Stock compilers don’t obfuscate - you need additional tooling.

Binary Spelunking 101: nm, otool, strings

Some tools for peeking inside compiled binaries. Let’s compile a simple Swift file:

class UserAuthenticator {
    private let apiKey = "sk_live_abc123secret"
    private let apiEndpoint = "https://api.myapp.com/v1/auth"

    func authenticate(username: String, password: String) -> Bool {
        return user == "admin" && pass == "supersecret123"
    }
}

class PaymentProcessor {
    let merchantId = "merchant_prod_xyz789"
    func processPayment(amount: Double) -> Bool { ... }
}

strings - extract readable text:

$ strings BinaryDemo | grep -iE "(secret|http|merchant|admin)"
sk_live_abc123secret
https://api.myapp.com/v1/auth
admin
supersecret123
merchant_prod_xyz789

nm - list symbols (functions, classes, globals):

$ nm BinaryDemo | grep Payment | head -5
00000001000013ec t _$s10BinaryDemo16PaymentProcessorC07processC06amountSbSd_tF
00000001000013b8 t _$s10BinaryDemo16PaymentProcessorC10merchantIdSSvg
...

$ nm BinaryDemo | xcrun swift-demangle | grep Payment | head -3
00000001000013ec t BinaryDemo.PaymentProcessor.processPayment(amount: Swift.Double) -> Swift.Bool
00000001000013b8 t BinaryDemo.PaymentProcessor.merchantId.getter : Swift.String

otool -L - linked libraries:

$ otool -L BinaryDemo
BinaryDemo:
  /usr/lib/libSystem.B.dylib (...)
  /System/Library/Frameworks/Foundation.framework/Versions/C/Foundation (...)
  /usr/lib/swift/libswiftCore.dylib (...)

Note: This binary was compiled with plain swiftc - no symbol stripping, obfuscation, or App Store encryption (FairPlay).

January 2026

#require in Swift Testing

The #require macro in Swift Testing safely unwraps optionals and throws if nil, stopping the test immediately. It replaces the old XCTUnwrap pattern.

Before (XCTest):

func testUserName() throws {
    let user = try XCTUnwrap(fetchUser())
    XCTAssertEqual(user.name, "John")
}

After (Swift Testing):

@Test func userName() throws {
    let user = try #require(fetchUser())
    #expect(user.name == "John")
}

It also works with boolean conditions - the test fails if the condition is false:

@Test func adminAccess() throws {
    let user = try #require(fetchUser())
    try #require(user.isAdmin)  // Fails test if not admin
    // Continue with admin-only tests...
}

The key difference from #expect: #require stops execution on failure, while #expect records the failure but continues. Use #require when subsequent code depends on the condition being true.

Tart: macOS VMs like containers

Tart from Cirrus Labs uses Apple’s Virtualization.framework to run macOS/Linux VMs on Apple Silicon. The killer feature: it distributes VM images via OCI registries, so you can pull/push them like Docker images.

tart clone ghcr.io/cirruslabs/macos-sequoia-xcode:latest my-vm
tart run my-vm

Combine with Packer to automate image creation - install Xcode, dependencies, snapshot, and push to your registry:

tart push my-vm ghcr.io/myorg/macos-ci:v1

OCI is only the distribution format - it’s not containerizing macOS (that would violate licensing). It just chunks the disk image into layers for efficient transfer and versioning. Great for CI runners.

Swift ownership: borrowing, consuming, inout

Swift 5.9 introduced ownership modifiers for non-copyable types (~Copyable). Here’s the difference:

struct DBConnection: ~Copyable {
    mutating func open() { /* ... */ }
    mutating func close() { /* ... */ }
    func query(_ sql: String) { /* ... */ }
}

borrowing - read-only access, caller keeps ownership:

func inspect(_ connection: borrowing DBConnection) {
    connection.query("SELECT 1")  // OK - read only
    // connection.open()          // Error - can't mutate
}

consuming - takes ownership, caller loses the value:

func runAndClose(_ connection: consuming DBConnection) {
    connection.open()
    connection.close()
    // connection is destroyed here
}

runAndClose(db)
// db.query("...")  // Error - db was consumed

inout - mutable borrow, caller keeps ownership:

func reopen(_ connection: inout DBConnection) {
    connection.open()  // OK - can mutate
}
// db still usable after call

Useful for modeling resources like DB connections, file handles, or locks where you want compile-time guarantees against use-after-close bugs.

rss facebook twitter github youtube mail spotify lastfm instagram linkedin google google-plus pinterest medium vimeo stackoverflow reddit quora