Experimental dev diary

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

April 2026

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