Contents

harlanhaskins/assertions

A Swift library for coordinating access to shared resources through an automatically-invalidating assertion system.

Overview

The Assertions library provides a resource management system where multiple components can request access to shared resources. The system automatically combines these requests into a single resolved state using customizable reduction strategies.

Installation

Swift Package Manager

dependencies: [
    .package(url: "https://github.com/harlanhaskins/Assertions.git", from: "1.0.0")
]

Minimum Requirements

  • iOS 17.0+ / macOS 14.0+ / visionOS 2.0+ / tvOS 17.0+ / watchOS 10.0+
  • Swift 6.0+

Quick Start

import Assertions

// Create a manager that tracks access to the sidebar. If any client is requesting the sidebar closed,
// it will be closed.
let sidebarManager = AssertionManager<Bool>("SidebarClosed")

// Make a request to close the sidebar.
let assertion = sidebarManager.acquireAssertion(reason: "User preference", value: true)

print(sidebarManager.value) // true

// Release the request (will also happen automatically when `assertion` goes out of scope)
assertion.invalidate()

print(sidebarManager.value) // false

Core Concepts

AssertionManager

The central coordinator that manages all assertions for a specific resource type:

let manager = AssertionManager<Bool>("ResourceName")

Assertion

A non-copyable struct representing a single request for resource access:

let assertion = manager.acquireAssertion(reason: "Debug info", value: true)
// Assertion automatically invalidates when deallocated

AssertionValue

Protocol defining how multiple assertion values are combined:

extension Bool: AssertionValue {
    static var defaultValue: Bool { false }
    static func reduce(into partialResult: inout Bool, nextValue: Bool) {
        partialResult = partialResult || nextValue // OR logic
    }
}

Examples

1. UI State Management

Coordinate multiple UI components that affect shared state:

class ViewController {
    private let loadingManager = AssertionManager<Bool>("LoadingState")
    private var networkAssertion: Assertion?
    private var dataAssertion: Assertion?
    
    func startNetworkRequest() {
        networkAssertion = loadingManager.acquireAssertion(
            reason: "Network request", 
            value: true
        )
        print("Loading: \(loadingManager.value)") // true
    }
    
    func startDataProcessing() {
        dataAssertion = loadingManager.acquireAssertion(
            reason: "Data processing", 
            value: true
        )
        print("Loading: \(loadingManager.value)") // true
    }
    
    func finishNetworkRequest() {
        networkAssertion?.invalidate()
        networkAssertion = nil
        // Still loading if data processing is active
        print("Loading: \(loadingManager.value)") // depends on dataAssertion
    }
}

2. SwiftUI Integration

Use with SwiftUI for reactive UI updates:

@Observable
class AppState {
    let modalManager = AssertionManager<Bool>("ModalPresented")
    let busyManager = AssertionManager<Bool>("AppBusy")
}

struct ContentView: View {
    @State private var appState = AppState()
    
    var body: some View {
        VStack {
            if appState.modalManager.value {
                Text("Modal is presented")
            }
            
            if appState.busyManager.value {
                ProgressView("Loading...")
            }
        }
    }
}

3. Priority Management

Implement custom value types with sophisticated reduction logic:

enum Priority: AssertionValue {
    case low, medium, high, critical
    
    static var defaultValue: Priority { .low }
    
    static func reduce(into partialResult: inout Priority, nextValue: Priority) {
        if nextValue.rawValue > partialResult.rawValue {
            partialResult = nextValue
        }
    }
}

let priorityManager = AssertionManager<Priority>("TaskPriority")

let backgroundTask = priorityManager.acquireAssertion(
    reason: "Background sync", 
    value: .low
)

let userAction = priorityManager.acquireAssertion(
    reason: "User interaction", 
    value: .high
)

print(priorityManager.value) // .high (highest priority wins)

4. Audio Session Management

Coordinate multiple audio sources:

struct AudioConfiguration: AssertionValue {
    let category: String
    let volume: Float
    
    static var defaultValue: AudioConfiguration {
        AudioConfiguration(category: "ambient", volume: 0.5)
    }
    
    static func reduce(into partialResult: inout AudioConfiguration, nextValue: AudioConfiguration) {
        // Use highest volume and most restrictive category
        partialResult.volume = max(partialResult.volume, nextValue.volume)
        if nextValue.category == "playback" {
            partialResult.category = nextValue.category
        }
    }
}

let audioManager = AssertionManager<AudioConfiguration>("AudioSession")

class MusicPlayer {
    private var audioAssertion: Assertion?
    
    func startPlayback() {
        audioAssertion = audioManager.acquireAssertion(
            reason: "Music playback",
            value: AudioConfiguration(category: "playback", volume: 0.8)
        )
    }
    
    func stopPlayback() {
        audioAssertion?.invalidate()
        audioAssertion = nil
    }
}

5. Feature Flag System

Manage dynamic feature enabling with multiple conditions:

struct FeatureFlags: AssertionValue {
    let experimentalUI: Bool
    let debugMode: Bool
    let betaFeatures: Bool
    
    static var defaultValue: FeatureFlags {
        FeatureFlags(experimentalUI: false, debugMode: false, betaFeatures: false)
    }
    
    static func reduce(into partialResult: inout FeatureFlags, nextValue: FeatureFlags) {
        // OR logic for all flags
        partialResult.experimentalUI = partialResult.experimentalUI || nextValue.experimentalUI
        partialResult.debugMode = partialResult.debugMode || nextValue.debugMode
        partialResult.betaFeatures = partialResult.betaFeatures || nextValue.betaFeatures
    }
}

let flagManager = AssertionManager<FeatureFlags>("AppFeatures")

// Enable experimental UI for beta users
let betaAssertion = flagManager.acquireAssertion(
    reason: "Beta user",
    value: FeatureFlags(experimentalUI: true, debugMode: false, betaFeatures: true)
)

// Enable debug mode for developers
let debugAssertion = flagManager.acquireAssertion(
    reason: "Debug build",
    value: FeatureFlags(experimentalUI: false, debugMode: true, betaFeatures: false)
)

print(flagManager.value) 
// FeatureFlags(experimentalUI: true, debugMode: true, betaFeatures: true)

Advanced Usage

Manual Assertion Management

let manager = AssertionManager<Bool>("ManualExample")

// Create assertion with explicit lifecycle
let assertion = manager.acquireAssertion(reason: "Manual control", value: true)

// Check if assertion is still valid
if assertion.isValid {
    print("Assertion active: \(manager.value)")
}

// Manually invalidate when done
assertion.invalidate()
print("After invalidation: \(manager.value)")

Delegate Pattern

For synchronous updates instead of Observable:

class MyDelegate: AssertionManagerDelegate {
    func assertionManager<Value>(_ manager: AssertionManager<Value>, didUpdateValue value: Value) {
        print("Value updated to: \(value)")
    }
}

let delegate = MyDelegate()
let manager = AssertionManager<Bool>("Delegated")
manager.delegate = delegate

let assertion = manager.acquireAssertion(reason: "Delegate test", value: true)
print("After assertion granted")
// Prints: "Value updated to: true" then "After assertion granted"

Custom Reduction Strategies

struct MinMaxValue: AssertionValue {
    let min: Int
    let max: Int
    
    static var defaultValue: MinMaxValue {
        MinMaxValue(min: 0, max: 100)
    }
    
    static func reduce(into partialResult: inout MinMaxValue, nextValue: MinMaxValue) {
        partialResult.min = Swift.min(partialResult.min, nextValue.min)
        partialResult.max = Swift.max(partialResult.max, nextValue.max)
    }
}

Debugging

The library includes helpful debugging features:

let manager = AssertionManager<Bool>("DebugExample")

let assertion1 = manager.acquireAssertion(reason: "First reason", value: true)
let assertion2 = manager.acquireAssertion(reason: "Second reason", value: false)

print(manager.description)
// Output includes all active assertions with their reasons and UUIDs

Thread Safety

AssertionManager is designed for use on the main thread. For concurrent access, wrap operations in appropriate synchronization:

await MainActor.run {
    let assertion = manager.acquireAssertion(reason: "Background task", value: true)
}

Best Practices

  1. Use descriptive reasons: They help with debugging and understanding resource usage
  2. Prefer automatic cleanup: Let assertions invalidate on deallocation when possible
  3. Keep assertions local: Store them as properties of the component that needs the resource
  4. Use Observable: Leverage SwiftUI integration for reactive updates
  5. Test reduction logic: Ensure your custom AssertionValue types behave correctly

License

This project is licensed under the MIT License. See LICENSE file for details.

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

Package Metadata

Repository: harlanhaskins/assertions

Default branch: main

README: README.md