Skip to content

VAndrJ/ObservationTracking

Repository files navigation

ObservationTracking

StandWithUkraine Support Ukraine

Language SPM Platform

Swift macros that automatically generate reactive observation patterns using Swift's Observation framework. Provides both basic observation tracking and advanced cancellable observation management.

Overview

ObservationTracking offers two macros:

  • @ObservationTracking: Generates reactive observation for property assignments
  • @CancellableObservation: Adds advanced observation lifecycle management with cancellation, and selective control

Both macros work together to create robust observation patterns with minimal boilerplate code.

Requirements

  • Swift 6.0+
  • Xcode 16.0+
  • iOS 17.0+ / macOS 14.0+ / tvOS 17.0+ / watchOS 10.0+

Installation

Swift Package Manager

Add ObservationTracking to your project through Xcode or by adding it to your Package.swift:

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

Or add it through Xcode:

  1. File → Add Package Dependencies
  2. Enter the repository URL: https://github.com/VAndrJ/ObservationTracking.git
  3. Select the version range

Basic Usage

@ObservationTracking Macro

The @ObservationTracking macro automatically generates reactive observation for each property assignment in a function:

import ObservationTracking
import Observation

@Observable
class DataModel {
    var count = 0
    var name = "Hello"
    var isActive = false
}

class ViewController {
    @ObservationTracking
    private func bind() {
        label.text = model.name
        countLabel.text = "\(model.count)"
        switchControl.isOn = model.isActive
    }
}

Isolation Control

The @ObservationTracking macro supports an isolation parameter that controls how observation change handlers are generated

Isolation Options

  • .mainActor (default): Executes change handlers in Task { @MainActor in ... }.
  • .actor: Executes change handlers in Task { await ... }.
  • .none: Executes change handlers directly without Task wrapping.

Example

For a real-use case, see ExampleView in the Example project.

@CancellableObservation Macro

The @CancellableObservation macro adds advanced observation management to classes:

@CancellableObservation
@MainActor
class WeatherObserver {
    weak var model: WeatherModel?
    var temperature = 0.0
    var humidity = 0.0
    
    @ObservationTracking
    func bind() {
        temperature = model?.temperature ?? 0.0
        humidity = model?.humidity ?? 0.0
    }
    
    // Advanced control methods are automatically generated:
    // - stopObservations()
    // - startObservationsIfNeeded()
    // - observeTemperature()
    // - observeHumidity()
    // - cancelObserveTemperature()
    // - cancelObserveHumidity()
}

Global Observation Management

Control all observations at once:

// Stop all observations
observer.stopObservations()

// Restart all observations (automatically calls all @ObservationTracking functions)
observer.startObservationsIfNeeded()

Token-Based Cancellation

The macro automatically handles cancellation with tokens (randomly generated string within the one observation update cycle).

Comparison with Regular Approach

Without @ObservationTracking:

class ManualObserver {
    weak var store: SomeStore?
    var localValue1 = 0
    var localValue2 = ""
    
    func startObserving() {
        observeValue1()
        observeValue2()
    }
    
    private func observeValue1() {
        localValue1 = withObservationTracking {
            store?.value1 ?? 0
        } onChange: { [weak self] in
            Task { @MainActor in
                self?.observeValue1()
            }
        }
    }
    
    private func observeValue2() {
        localValue2 = withObservationTracking {
            store?.value2 ?? ""
        } onChange: { [weak self] in
            Task { @MainActor in
                self?.observeValue2()
            }
        }
    }
}

With @ObservationTracking:

class MacroObserver {
    weak var store: SomeStore?
    var localValue1 = 0
    var localValue2 = ""
    
    @ObservationTracking
    func startObserving() {
        localValue1 = store?.value1 ?? 0
        localValue2 = store?.value2 ?? ""
    }
}

Pros and Cons

Pros ✅

  • Reduced Boilerplate: Eliminates repetitive withObservationTracking code
  • Type Safe: Compile-time macro expansion ensures type safety
  • Clean Syntax: Makes intent clear and code more readable
  • UIKit Friendly: Perfect for updating UI elements when data changes
  • Advanced Control: Selective observation management with cancellation

Cons ❌

  • Macro Dependency: Requires understanding of Swift macros

What the Macros Generate

@ObservationTracking Expansion

Your code:

@ObservationTracking
private func bind() {
    count = model.value
    name = model.name ?? "default"
}

Generated code:

private func bind() {
    observeCount()
    observeName()
}

private func observeCount() {
    count = withObservationTracking {
        model.value
    } onChange: { [weak self] in
        Task { @MainActor in
            self?.observeCount()
        }
    }
}

private func observeName() {
    name = withObservationTracking {
        model.name ?? "default"
    } onChange: { [weak self] in
        Task { @MainActor in
            self?.observeName()
        }
    }
}

Isolation Examples

With .actor isolation:

@ObservationTracking(isolation: .actor)
private func processData() {
    result = model.computation
}

Generated code:

private func processData() {
    observeResult()
}

private func observeResult() {
    result = withObservationTracking {
        model.computation
    } onChange: { [weak self] in
        Task {
            await self?.observeResult()
        }
    }
}

With .none isolation:

@ObservationTracking(isolation: .none)
private func updateCache() {
    cached = model.data
}

Generated code:

private func updateCache() {
    observeCached()
}

private func observeCached() {
    cached = withObservationTracking {
        model.data
    } onChange: { [weak self] in
        self?.observeCached()
    }
}

@CancellableObservation Expansion

Your code:

@CancellableObservation
class Observer {
    @ObservationTracking
    func bind() {
        value = model.property
    }
}

Generated infrastructure:

class Observer {
    func bind() {
        observeValue()
    }

    func observeValue() {
        guard isObservingEnabled else { return }
        
        let token = UUID().uuidString
        observationTokens["observeValue"] = token
        value = withObservationTracking {
            model.property
        } onChange: { [weak self] in
            Task { @MainActor in
                guard let self, token == self.observationTokens["observeValue"] else {
                    return
                }
                self.observeValue()
            }
        }
    }

    func cancelObserveValue() {
        observationTokens.removeValue(forKey: "observeValue")
    }

    // Infrastructure
    private var observationTokens: [String: String] = [:]
    private var isObservingEnabled = true

    func stopObservations() {
        isObservingEnabled = false
        observationTokens.removeAll()
    }

    func startObservationsIfNeeded() {
        guard !isObservingEnabled else { return }
        isObservingEnabled = true
        bind()  // Automatically calls all @ObservationTracking functions
    }
}

How It Works

  1. Property Detection: The macro scans function bodies for property assignments (property = expression)
  2. Method Generation: Creates individual observer methods for each assignment
  3. Reactive Wrapping: Wraps right-hand side expressions with withObservationTracking
  4. Auto Re-observation: Sets up onChange callbacks that re-execute the observer methods
  5. Function Transformation: Replaces assignments in the original function with calls to observer methods
  6. Cancellation Management: Adds token-based cancellation and control infrastructure
  7. Automatic Restart: startObservationsIfNeeded() automatically calls all @ObservationTracking functions

License

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

Related

Inspired by the need for cleaner observation code in UIKit.

About

A macro for automatic observation tracking using Swift's Observation framework.

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages