diff --git a/.spi.yml b/.spi.yml new file mode 100644 index 0000000..27a3a49 --- /dev/null +++ b/.spi.yml @@ -0,0 +1,4 @@ +version: 1 +builder: + configs: + - documentation_targets: [OpenObservation] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..1f9c314 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Kyle-Ye + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 5d37b7b..64ce20c 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,10 @@ dependencies: [ ] ``` +## Documentation + +Comprehensive documentation is available on [Swift Package Index](https://swiftpackageindex.com/OpenSwiftUIProject/OpenObservation/main/documentation/openobservation). + ## Usage ```swift @@ -66,4 +70,6 @@ withObservationTracking { ## License - **OpenObservation code**: MIT License -- **Code derived from Swift project**: Apache License v2.0 with Runtime Library Exception \ No newline at end of file +- **Code derived from Swift project**: Apache License v2.0 with Runtime Library Exception + +See LICENSE file. diff --git a/Sources/OpenObservation/Observable.swift b/Sources/OpenObservation/Observable.swift index 52b05e4..a6daaca 100644 --- a/Sources/OpenObservation/Observable.swift +++ b/Sources/OpenObservation/Observable.swift @@ -15,7 +15,7 @@ /// Conforming to this protocol signals to other APIs that the type supports /// observation. However, applying the `Observable` protocol by itself to a /// type doesn't add observation functionality to the type. Instead, always use -/// the ``Observation/Observable()`` macro when adding observation +/// the ``OpenObservation/Observable()`` macro when adding observation /// support to a type. public protocol Observable { } @@ -24,7 +24,7 @@ public protocol Observable { } /// Defines and implements conformance of the Observable protocol. /// /// This macro adds observation support to a custom type and conforms the type -/// to the ``Observation/Observable`` protocol. For example, the following code +/// to the ``OpenObservation/Observable`` protocol. For example, the following code /// applies the `Observable` macro to the type `Car` making it observable: /// /// @Observable diff --git a/Sources/OpenObservation/ObservationRegistrar.swift b/Sources/OpenObservation/ObservationRegistrar.swift index 5519737..eee5a56 100644 --- a/Sources/OpenObservation/ObservationRegistrar.swift +++ b/Sources/OpenObservation/ObservationRegistrar.swift @@ -12,7 +12,7 @@ /// Provides storage for tracking and access to data changes. /// /// You don't need to create an instance of `ObservationRegistrar` when using -/// the ``Observation/Observable()`` macro to indicate observability of a type. +/// the ``OpenObservation/Observable()`` macro to indicate observability of a type. public struct ObservationRegistrar: Sendable { internal class ValueObservationStorage { func emit(_ element: Element) -> Bool { return false } @@ -296,8 +296,8 @@ public struct ObservationRegistrar: Sendable { /// Creates an instance of the observation registrar. /// /// You don't need to create an instance of - /// ``Observation/ObservationRegistrar`` when using the - /// ``Observation/Observable()`` macro to indicate observably + /// ``OpenObservation/ObservationRegistrar`` when using the + /// ``OpenObservation/Observable()`` macro to indicate observably /// of a type. public init() { } diff --git a/Sources/OpenObservation/OpenObservation.docc/GettingStarted.md b/Sources/OpenObservation/OpenObservation.docc/GettingStarted.md new file mode 100644 index 0000000..ffe9000 --- /dev/null +++ b/Sources/OpenObservation/OpenObservation.docc/GettingStarted.md @@ -0,0 +1,403 @@ +# Getting Started + +Learn how to use OpenObservation in your Swift projects. + +## Overview + +OpenObservation provides a powerful observation framework that makes it easy to track changes to properties in your Swift objects. This guide will walk you through the basics of using the framework. + +## Installation + +### Swift Package Manager + +Add OpenObservation to your `Package.swift`: + +```swift +dependencies: [ + .package(url: "https://github.com/OpenSwiftUIProject/OpenObservation", from: "1.0.0") +] +``` + +Then add it to your target dependencies: + +```swift +.target( + name: "YourApp", + dependencies: ["OpenObservation"] +) +``` + +## Basic Usage + +### Making a Class Observable + +Use the `@Observable` macro to make any class observable: + +```swift +import OpenObservation + +@Observable +class TodoItem { + var title: String + var isCompleted: Bool + var priority: Int + + init(title: String, isCompleted: Bool = false, priority: Int = 0) { + self.title = title + self.isCompleted = isCompleted + self.priority = priority + } +} +``` + +That's it! All stored properties are now automatically observable. + +### Tracking Property Changes + +Use `withObservationTracking` to observe changes to specific properties: + +```swift +let todo = TodoItem(title: "Learn Observation") + +withObservationTracking { + // Only the properties accessed here will be tracked + print("Title: \(todo.title)") + print("Completed: \(todo.isCompleted)") +} onChange: { + print("Todo item changed!") +} + +// Later... +todo.title = "Master Observation" // Triggers onChange (called once) +todo.isCompleted = true // Does NOT trigger onChange again +todo.priority = 1 // Does NOT trigger onChange (not tracked) +``` + +**Important:** The `onChange` closure is called only **once** after the first change to any tracked property. Subsequent changes to tracked properties will not trigger the closure again. To continue observing, you need to re-establish the observation tracking. + +## Common Patterns + +### View Model Pattern + +```swift +@Observable +class TodoListViewModel { + var todos: [TodoItem] = [] + var filter: FilterOption = .all + + var filteredTodos: [TodoItem] { + switch filter { + case .all: + return todos + case .active: + return todos.filter { !$0.isCompleted } + case .completed: + return todos.filter { $0.isCompleted } + } + } + + func addTodo(_ title: String) { + todos.append(TodoItem(title: title)) + } + + func toggleTodo(at index: Int) { + todos[index].isCompleted.toggle() + } +} + +enum FilterOption { + case all, active, completed +} +``` + +### Observing Nested Objects + +```swift +@Observable +class User { + var name: String + var profile: Profile + + init(name: String, profile: Profile) { + self.name = name + self.profile = profile + } +} + +@Observable +class Profile { + var bio: String = "" + var avatarURL: URL? +} + +// Usage +let user = User(name: "Alice", profile: Profile()) + +withObservationTracking { + print(user.name) + print(user.profile.bio) // Tracks both user.profile AND profile.bio +} onChange: { + print("User or profile changed") // Called once on first change +} + +// First change triggers onChange +user.name = "Bob" // Triggers onChange +// Subsequent changes don't trigger +user.profile.bio = "New bio" // Does NOT trigger onChange again +``` + +### Ignoring Properties + +Use `@ObservationIgnored` to exclude properties from observation: + +```swift +@Observable +class DataModel { + var publicData: String = "visible" + + @ObservationIgnored + var internalCache: [String: Any] = [:] // Never triggers observations + + @ObservationIgnored + var debugInfo: String = "" // Not tracked +} +``` + +## Advanced Usage + +### Custom Observation Tracking + +For more control, you can use the lower-level APIs: + +```swift +@Observable +class CustomModel { + var value: Int = 0 + + func startTracking() { + withObservationTracking { + _ = self.value + } onChange: { [weak self] in + self?.handleChange() + } + } + + private func handleChange() { + print("Value changed to: \(value)") + // Re-establish observation + startTracking() + } +} +``` + +### Conditional Tracking + +Track different properties based on state: + +```swift +@Observable +class ConditionalModel { + var useAdvancedMode = false + var basicValue = 0 + var advancedValue = 0 + + func setupObservation() { + withObservationTracking { + if useAdvancedMode { + print("Advanced: \(advancedValue)") + } else { + print("Basic: \(basicValue)") + } + } onChange: { + print("Relevant value changed") + self.setupObservation() // Re-establish with new conditions + } + } +} +``` + +### Working with One-Shot Observations + +Since `onChange` is called only once, you may need to re-establish observations: + +```swift +@Observable +class RepeatingObserver { + var value: Int = 0 + + func observeContinuously() { + withObservationTracking { + print("Current value: \(value)") + } onChange: { [weak self] in + print("Value changed!") + // Re-establish observation for next change + self?.observeContinuously() + } + } +} + +## Best Practices + +### 1. Keep Observable Classes Focused + +Observable classes should have a single responsibility: + +```swift +// Good: Focused responsibility +@Observable +class UserSettings { + var theme: Theme + var notifications: Bool + var language: String +} + +// Avoid: Too many responsibilities +@Observable +class AppState { + var user: User + var settings: Settings + var network: NetworkStatus + var cache: Cache + // Too much in one class! +} +``` + +### 2. Use Computed Properties Wisely + +Computed properties in observable classes are recalculated when dependencies change: + +```swift +@Observable +class ShoppingCart { + var items: [CartItem] = [] + + // Good: Simple computed property + var totalPrice: Double { + items.reduce(0) { $0 + $1.price * Double($1.quantity) } + } + + // Avoid: Heavy computation in computed properties + var complexAnalysis: Analysis { + // Don't do expensive operations here + performExpensiveAnalysis(items) + } +} +``` + +### 3. Avoid Observation Loops + +Be careful not to create infinite observation loops: + +```swift +@Observable +class LoopModel { + var value1: Int = 0 { + didSet { + // Avoid: This could create a loop + // value2 = value1 * 2 + } + } + var value2: Int = 0 + + // Better: Use methods for derived updates + func updateValue2() { + value2 = value1 * 2 + } +} +``` + +### 4. Clean Up Observations + +While observations are automatically cleaned up, you can manually cancel them: + +```swift +class ViewController { + var observation: ObservationTracking? + + func startObserving() { + let tracking = ObservationTracking(nil) + ObservationTracking._installTracking(tracking, willSet: { _ in + // Handle changes + }) + observation = tracking + } + + func stopObserving() { + observation?.cancel() + observation = nil + } +} +``` + +## Troubleshooting + +### Properties Not Being Observed + +Make sure you're accessing the property within the tracking closure: + +```swift +// Wrong: Property accessed outside tracking +let title = todo.title +withObservationTracking { + print(title) // Not tracking todo.title! +} onChange: { } + +// Correct: Property accessed inside tracking +withObservationTracking { + print(todo.title) // Now tracking todo.title +} onChange: { } +``` + +### onChange Not Firing + +Remember that `onChange` is called only once. Common issues: + +1. **One-shot behavior**: The `onChange` closure fires only once after the first change: + +```swift +withObservationTracking { + print(model.value) +} onChange: { + print("Changed!") // Only called once +} + +model.value = 1 // Triggers onChange +model.value = 2 // Does NOT trigger onChange again +``` + +2. **Object retention**: Ensure the observed object is retained: + +```swift +// Wrong: Object might be deallocated +func setupObservation() { + let model = MyModel() // Local variable + withObservationTracking { + print(model.value) + } onChange: { + // Might never fire if model is deallocated + } +} + +// Correct: Keep a strong reference +class Controller { + let model = MyModel() // Instance variable + + func setupObservation() { + withObservationTracking { + print(model.value) + } onChange: { + // Will fire once when model.value changes + self.setupObservation() // Re-establish for continuous observation + } + } +} +``` + +## Next Steps + +Now that you understand the basics of OpenObservation: + +- Read for a deep dive into the internals +- Check for optimization tips +- Explore the API documentation for ``Observable`` and ``ObservationRegistrar`` +- Look at example projects in the GitHub repository \ No newline at end of file diff --git a/Sources/OpenObservation/OpenObservation.docc/HowObservationWorks.md b/Sources/OpenObservation/OpenObservation.docc/HowObservationWorks.md new file mode 100644 index 0000000..a171f74 --- /dev/null +++ b/Sources/OpenObservation/OpenObservation.docc/HowObservationWorks.md @@ -0,0 +1,328 @@ +# How Swift Observation Works + +A deep dive into the internals of the Observation framework. + +## Overview + +The Observation framework provides a revolutionary approach to property observation in Swift. Unlike traditional patterns that require explicit registration and management of observers, Observation uses compile-time transformations and runtime tracking to provide automatic, fine-grained property observation. + +## The Core Problem + +Traditional observation patterns in Swift have significant limitations: + +- **KVO (Key-Value Observing)**: Requires Objective-C runtime, type-unsafe string keys, and complex boilerplate +- **Combine**: Heavy framework dependency, requires explicit publishers for each property +- **Property Wrappers**: Can't track which specific properties are accessed + +The Observation framework solves these issues by providing: +- Fine-grained, type-safe property observation +- Automatic tracking of only accessed properties +- Zero boilerplate for basic usage +- Cross-platform compatibility + +## The @Observable Macro Magic + +When you mark a class with `@Observable`, a sophisticated transformation occurs: + +```swift +// What you write: +@Observable class Car { + var name: String = "Tesla" + var speed: Int = 0 +} + +// What the macro generates: +class Car { + // Original property becomes computed with special accessors + var name: String { + @storageRestrictions(initializes: _name) + init(initialValue) { + _name = initialValue + } + + get { + access(keyPath: \.name) // Track property access + return _name + } + + set { + withMutation(keyPath: \.name) { + _name = newValue + } + } + + _modify { + access(keyPath: \.name) + _$observationRegistrar.willSet(self, keyPath: \.name) + defer { + _$observationRegistrar.didSet(self, keyPath: \.name) + } + yield &_name + } + } + + // Backing storage (hidden from public API) + @ObservationIgnored private var _name: String = "Tesla" + + // The observation machinery + @ObservationIgnored private let _$observationRegistrar = ObservationRegistrar() + + // Helper methods for tracking + internal nonisolated func access( + keyPath: KeyPath + ) { + _$observationRegistrar.access(self, keyPath: keyPath) + } + + internal nonisolated func withMutation( + keyPath: KeyPath, + _ mutation: () throws -> MutationResult + ) rethrows -> MutationResult { + try _$observationRegistrar.withMutation(of: self, keyPath: keyPath, mutation) + } +} + +extension Car: Observable {} +``` + +## The Tracking Mechanism + +The tracking system uses thread-local storage to transparently record property accesses: + +### Step 1: Setting Up Tracking Context + +```swift +withObservationTracking { + // Code that accesses properties + print(car.name) + print(car.speed) +} onChange: { + // Called when tracked properties change + print("Car properties changed!") +} +``` + +### Step 2: Thread-Local Storage + +When `withObservationTracking` executes: + +1. A thread-local pointer is set to an `_AccessList` +2. This list will collect all property accesses within the closure +3. The pointer is visible to all code executing on the same thread + +### Step 3: Property Access Recording + +When you access `car.name`: + +1. The property getter calls `access(keyPath: \.name)` +2. `access` checks for thread-local tracking context +3. If found, it records the keyPath in the `_AccessList` +4. The actual property value is returned normally + +### Step 4: Observer Registration + +After the tracking closure completes: + +1. The collected `_AccessList` contains all accessed properties +2. Observers are registered ONLY for these specific properties +3. The `onChange` closure is stored for later execution + +### Step 5: Change Detection + +When a tracked property changes: + +1. `willSet` is called before the mutation +2. The actual value is updated +3. `didSet` is called after the mutation +4. All registered `onChange` closures fire once + +## The ObservationRegistrar + +The `ObservationRegistrar` is the central hub managing all observations for an object: + +### Key Responsibilities + +- **State Management**: Maintains a map of observers to properties +- **Registration**: Handles observer registration and cancellation +- **Notification**: Triggers observer callbacks on property changes +- **Cleanup**: Automatically removes observers when no longer needed + +### Observer Types + +The registrar supports four types of observations: + +1. **willSetTracking**: Called before property changes +2. **didSetTracking**: Called after property changes +3. **computed**: For computed property observers +4. **values**: For value-based observations + +### Lifecycle Management + +Observers are automatically cleaned up when: +- The tracking is explicitly cancelled +- The observed object is deallocated +- The observation fires (for one-shot observations) +- The tracking context goes out of scope + +## Design Decisions Explained + +### Why Thread-Local Storage? + +Thread-local storage enables transparent tracking without API changes: + +- No need to pass context through every property access +- Scoped to current execution context +- Automatically cleaned up when scope exits +- No interference between threads + +### Why Separate Backing Storage? + +The backing storage pattern (`_name` for property `name`) enables: + +- Intercepting all property access +- Maintaining source compatibility +- Fine-grained control over getters/setters +- Avoiding infinite recursion in accessors + +### Why Only Classes? + +The framework only supports classes because: + +- Reference semantics are required for observation +- Structs would need to copy observers on mutation +- Actors have complex isolation requirements +- Enums don't have mutable storage + +## Performance Characteristics + +The Observation framework is designed for optimal performance: + +### Lazy Registration +- Observers only registered for accessed properties +- No overhead for unobserved properties +- Registration happens after access tracking + +### One-Shot Notifications +- `onChange` fires once per mutation cycle +- Multiple property changes coalesce +- Reduces unnecessary UI updates + +### Automatic Cleanup +- No manual observer removal needed +- No retain cycles with proper usage +- Memory-efficient observer storage + +### Lock-Free Design +- Uses critical sections only for state mutations +- No locks during property access +- Minimal contention in multi-threaded scenarios + +## Advanced Features + +### @ObservationIgnored + +Excludes specific properties from observation: + +```swift +@Observable class Model { + var tracked: String = "I'm observed" + @ObservationIgnored var ignored: Int = 42 // Never tracked +} +``` + +### @ObservationTracked + +Explicitly marks properties for tracking when not using `@Observable`: + +```swift +class PartiallyObservable { + @ObservationTracked var observed: String = "tracked" + var normal: Int = 0 // Not tracked +} +``` + +### Nested Tracking + +Observations can be nested - inner tracking contexts merge with outer ones: + +```swift +withObservationTracking { + // Outer tracking + print(model1.property) + + withObservationTracking { + // Inner tracking + print(model2.property) + } onChange: { + // Fires for model2 changes + } +} onChange: { + // Fires for model1 OR model2 changes +} +``` + +## Real-World Usage Pattern + +Here's a complete example showing typical usage: + +```swift +@Observable class ViewModel { + var items: [Item] = [] + var isLoading = false + var errorMessage: String? + + func loadData() async { + isLoading = true + errorMessage = nil + + do { + let data = try await fetchItems() + items = data + } catch { + errorMessage = error.localizedDescription + } + + isLoading = false + } +} + +// In your UI layer +class ViewController { + let viewModel = ViewModel() + + func setupObservation() { + withObservationTracking { + // Only track properties we actually use + if viewModel.isLoading { + showLoadingSpinner() + } else if let error = viewModel.errorMessage { + showError(error) + } else { + showItems(viewModel.items) + } + } onChange: { + // Re-render when any tracked property changes + Task { @MainActor in + self.setupObservation() // Re-establish observation + } + } + } +} +``` + +## Comparison with Other Patterns + +| Feature | Observation | KVO | Combine | Property Wrappers | +|---------|------------|-----|---------|-------------------| +| Type Safety | ✅ | ❌ | ✅ | ✅ | +| Fine-grained Tracking | ✅ | ❌ | ❌ | ❌ | +| Zero Boilerplate | ✅ | ❌ | ❌ | ✅ | +| Cross-platform | ✅ | ❌ | ✅ | ✅ | +| Automatic Cleanup | ✅ | ❌ | ❌ | ✅ | +| Performance | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | + +## Conclusion + +The Observation framework represents a significant advancement in Swift's observation capabilities. By combining compile-time macro transformations with clever runtime tracking, it provides a powerful, efficient, and easy-to-use observation system that works seamlessly across platforms. + +The key innovation is the transparent property access tracking through thread-local storage, which enables fine-grained observation without explicit registration or management. This makes it ideal for modern reactive UI frameworks and data binding scenarios. \ No newline at end of file diff --git a/Sources/OpenObservation/OpenObservation.docc/Info.plist b/Sources/OpenObservation/OpenObservation.docc/Info.plist new file mode 100644 index 0000000..96b3e1a --- /dev/null +++ b/Sources/OpenObservation/OpenObservation.docc/Info.plist @@ -0,0 +1,12 @@ + + + + + CFBundleName + OpenObservation + CFBundleDisplayName + OpenObservation + CDDefaultCodeListingLanguage + swift + + diff --git a/Sources/OpenObservation/OpenObservation.docc/OpenObservation.md b/Sources/OpenObservation/OpenObservation.docc/OpenObservation.md new file mode 100644 index 0000000..7bc235d --- /dev/null +++ b/Sources/OpenObservation/OpenObservation.docc/OpenObservation.md @@ -0,0 +1,36 @@ +# ``OpenObservation`` + +A backport implementation of Swift's Observation framework with access to @_spi(SwiftUI) APIs. + +## Overview + +OpenObservation provides a powerful, type-safe mechanism for observing changes to properties in Swift objects. It's designed to overcome the limitations of traditional observation patterns like KVO and Combine, offering fine-grained property tracking with minimal boilerplate. + +This framework is particularly useful when: +- Building reactive UI frameworks +- Implementing data binding patterns +- Creating observable view models +- Porting SwiftUI code to non-Apple platforms + +## Topics + +### Essentials + +- ``Observable`` +- ``ObservationRegistrar`` +- + +### Observation Tracking + +- ``withObservationTracking(_:onChange:)`` + +### Advanced Usage + +- +- + +### Macros + +- ``Observable()`` +- ``ObservationTracked()`` +- ``ObservationIgnored()`` diff --git a/Sources/OpenObservation/OpenObservation.docc/PerformanceConsiderations.md b/Sources/OpenObservation/OpenObservation.docc/PerformanceConsiderations.md new file mode 100644 index 0000000..b09e9a0 --- /dev/null +++ b/Sources/OpenObservation/OpenObservation.docc/PerformanceConsiderations.md @@ -0,0 +1,448 @@ +# Performance Considerations + +Optimize your use of the Observation framework for maximum performance. + +## Overview + +While the Observation framework is designed to be highly efficient, understanding its performance characteristics helps you write optimal code. This guide covers key performance considerations and best practices. + +## Key Performance Features + +### Fine-Grained Tracking + +The framework only observes properties that are actually accessed: + +```swift +@Observable +class LargeModel { + var property1 = "a" + var property2 = "b" + var property3 = "c" + // ... 100 more properties + + var property104 = "z" +} + +let model = LargeModel() + +withObservationTracking { + // Only property1 and property3 are tracked + print(model.property1) + print(model.property3) +} onChange: { + // Only fires when property1 or property3 change + // No overhead for the other 102 properties! +} +``` + +### Lazy Registration + +Observers are registered lazily after property access: + +```swift +withObservationTracking { + if condition { + print(model.expensiveProperty) // Only tracked if condition is true + } +} onChange: { + // Observer only registered if condition was true +} +``` + +### Coalesced Updates + +Multiple property changes trigger a single onChange: + +```swift +withObservationTracking { + print(model.firstName) + print(model.lastName) +} onChange: { + // Fires once even if both properties change + updateUI() +} + +// Both changes trigger only one onChange +model.firstName = "John" +model.lastName = "Doe" +``` + +## Performance Best Practices + +### 1. Minimize Tracking Scope + +Only track properties you actually need: + +```swift +// Inefficient: Tracking everything +withObservationTracking { + let user = model.currentUser + print(user.name) + print(user.email) + print(user.profile) + print(user.settings) + print(user.preferences) +} onChange: { } + +// Efficient: Track only what you use +withObservationTracking { + print(model.currentUser.name) // Only track name +} onChange: { } +``` + +### 2. Avoid Computed Property Chains + +Long chains of computed properties can impact performance: + +```swift +@Observable +class InefficientModel { + var base = 1 + + // Avoid: Deep computed property chains + var level1: Int { base * 2 } + var level2: Int { level1 * 2 } + var level3: Int { level2 * 2 } + var level4: Int { level3 * 2 } // Recalculates entire chain +} + +@Observable +class EfficientModel { + var base = 1 + + // Better: Cache intermediate values if needed + private var _cachedResult: Int? + + var result: Int { + if let cached = _cachedResult { + return cached + } + let computed = base * 16 // Direct calculation + _cachedResult = computed + return computed + } + + func invalidateCache() { + _cachedResult = nil + } +} +``` + +### 3. Use @ObservationIgnored for Cache + +Mark cache and temporary properties as ignored: + +```swift +@Observable +class DataModel { + var sourceData: [Item] = [] + + @ObservationIgnored + private var _filteredCache: [Item]? + + @ObservationIgnored + private var _sortedCache: [Item]? + + var filteredItems: [Item] { + if let cached = _filteredCache { + return cached + } + let filtered = sourceData.filter { $0.isActive } + _filteredCache = filtered + return filtered + } + + // Clear cache when source changes + private func dataDidChange() { + _filteredCache = nil + _sortedCache = nil + } +} +``` + +### 4. Batch Updates + +Group related changes together: + +```swift +@Observable +class BatchUpdateModel { + var items: [String] = [] + var count = 0 + var lastUpdated: Date? + + // Inefficient: Multiple separate updates + func inefficientAdd(_ item: String) { + items.append(item) // Trigger 1 + count += 1 // Trigger 2 + lastUpdated = Date() // Trigger 3 + } + + // Efficient: Batch updates + func efficientAdd(_ items: [String]) { + self.items.append(contentsOf: items) + self.count = self.items.count + self.lastUpdated = Date() + // All changes trigger once + } +} +``` + +### 5. Avoid Observation in Tight Loops + +Don't create observations inside performance-critical loops: + +```swift +// Inefficient: Creating observations in a loop +for item in largeArray { + withObservationTracking { + process(item.value) + } onChange: { + handleChange(item) + } +} + +// Efficient: Single observation for all items +withObservationTracking { + for item in largeArray { + process(item.value) + } +} onChange: { + handleBatchChange() +} +``` + +## Memory Considerations + +### Weak References in Closures + +Prevent retain cycles with weak references: + +```swift +class ViewController { + let model = MyModel() + + func setupObservation() { + withObservationTracking { + print(model.value) + } onChange: { [weak self] in + self?.updateUI() // Weak reference prevents retain cycle + } + } +} +``` + +### Observation Cleanup + +Observations are automatically cleaned up, but you can help: + +```swift +class LongLivedController { + var activeObservations: [ObservationTracking] = [] + + func startObserving() { + let tracking = ObservationTracking(nil) + // ... setup tracking + activeObservations.append(tracking) + } + + func cleanup() { + // Explicitly cancel all observations + activeObservations.forEach { $0.cancel() } + activeObservations.removeAll() + } + + deinit { + cleanup() + } +} +``` + +## Benchmarking + +### Measuring Performance + +Use Instruments and benchmarks to measure performance: + +```swift +import XCTest + +class ObservationBenchmarks: XCTestCase { + func testObservationOverhead() { + let model = LargeModel() + + measure { + withObservationTracking { + // Access multiple properties + _ = model.property1 + _ = model.property2 + _ = model.property3 + } onChange: { } + } + } + + func testBatchUpdatePerformance() { + let model = BatchModel() + let items = (0..<1000).map { "Item \($0)" } + + measure { + model.batchUpdate(items) + } + } +} +``` + +### Performance Metrics + +Typical performance characteristics: + +| Operation | Time Complexity | Space Complexity | +|-----------|----------------|------------------| +| Property Access | O(1) | O(1) | +| Observer Registration | O(n) | O(n) | +| Change Notification | O(m) | O(1) | +| Observer Cleanup | O(n) | O(1) | + +Where: +- n = number of tracked properties +- m = number of registered observers + +## Platform-Specific Optimizations + +### Swift Toolchain Support + +When available, use native toolchain support: + +```swift +#if OPENOBSERVATION_SWIFT_TOOLCHAIN_SUPPORTED +// Uses optimized C++ implementation +#else +// Falls back to pure Swift implementation +#endif +``` + +### Compiler Optimizations + +Enable optimizations in release builds: + +```swift +// Package.swift +.target( + name: "YourApp", + dependencies: ["OpenObservation"], + swiftSettings: [ + .unsafeFlags(["-O"], .when(configuration: .release)) + ] +) +``` + +## Common Performance Pitfalls + +### 1. Observing in View Drawing + +Avoid creating observations during view drawing: + +```swift +// Bad: Creating observation in draw +override func draw(_ rect: CGRect) { + withObservationTracking { + drawContent(model.data) + } onChange: { + setNeedsDisplay() // Can cause infinite loop! + } +} + +// Good: Setup observation once +override func viewDidLoad() { + withObservationTracking { + _ = model.data + } onChange: { [weak self] in + self?.setNeedsDisplay() + } +} +``` + +### 2. Excessive Granularity + +Balance between granularity and performance: + +```swift +// Too granular: Observing individual characters +@Observable +class CharacterModel { + var char1 = "a" + var char2 = "b" + var char3 = "c" + // ... 100 more properties +} + +// Better: Group related data +@Observable +class TextModel { + var text = "abc..." // Single property for text + var metadata: TextMetadata // Group related properties +} +``` + +### 3. Recursive Observations + +Avoid observations that trigger themselves: + +```swift +@Observable +class RecursiveModel { + var value = 0 + + func problematicSetup() { + withObservationTracking { + print(value) + } onChange: { [weak self] in + self?.value += 1 // Triggers itself! + self?.problematicSetup() // Infinite recursion + } + } +} +``` + +## Profiling Tools + +### Using Instruments + +Profile your app with Instruments: + +1. **Time Profiler**: Identify observation-related bottlenecks +2. **Allocations**: Track memory usage of observations +3. **System Trace**: Analyze threading behavior + +### Custom Performance Logging + +Add performance logging for debugging: + +```swift +@Observable +class InstrumentedModel { + var value = 0 { + willSet { + let start = CFAbsoluteTimeGetCurrent() + defer { + let elapsed = CFAbsoluteTimeGetCurrent() - start + if elapsed > 0.001 { // Log slow updates + print("Slow update: \(elapsed)s") + } + } + } + } +} +``` + +## Conclusion + +The Observation framework is designed for excellent performance out of the box. By following these guidelines: + +- Track only necessary properties +- Use @ObservationIgnored for cache +- Batch related updates +- Avoid observation in tight loops +- Profile and measure your specific use cases + +You can build highly performant reactive applications that scale efficiently with your data complexity. \ No newline at end of file diff --git a/Sources/OpenObservationClient/main.swift b/Sources/OpenObservationClient/main.swift index a4e1b46..887a3f9 100644 --- a/Sources/OpenObservationClient/main.swift +++ b/Sources/OpenObservationClient/main.swift @@ -7,15 +7,133 @@ import OpenObservation +// MARK: - Basic Observable Classes + @Observable -class A { +class Person { var name = "Alice" + var age = 30 + + @ObservationIgnored + var internalCounter = 0 } -let a = A() -a.access(keyPath: \.name) -print(a.name) -a.withMutation(keyPath: \.name) { - a.name = "Bob" +@Observable +class Model { + var firstName: String = "First" + var lastName: String = "Last" + + var fullName: String { + "\(firstName) \(lastName)" + } +} + +// MARK: - Nested Observable Example + +@Observable +class ShoppingCart { + var items: [CartItem] = [] + var discount: Double = 0.0 + + var total: Double { + let subtotal = items.reduce(0) { $0 + $1.price * Double($1.quantity) } + return subtotal * (1 - discount) + } + + func addItem(_ item: CartItem) { + items.append(item) + } +} + +@Observable +class CartItem { + var name: String + var price: Double + var quantity: Int = 1 + + init(name: String, price: Double) { + self.name = name + self.price = price + } +} + +// MARK: - Example Usage + +print("=== Basic Observation Example ===") +let person = Person() + +withObservationTracking { + print("Person: \(person.name), Age: \(person.age)") +} onChange: { + print("Person data changed! (onChange called once)") +} + +// Note: onChange is called only ONCE after the first property change +person.name = "Bob" // This triggers onChange +person.age = 35 // This does NOT trigger onChange again +// internalCounter won't trigger onChange due to @ObservationIgnored +person.internalCounter = 10 + +print("\n=== Model with Computed Property ===") +let model = Model() + +let result = withObservationTracking { + print("Full name: \(model.fullName)") + return model.fullName.count +} onChange: { + print("Model properties changed! (onChange called once)") } -print(a.name) + +print("Initial full name length: \(result)") + +// Only the first property change triggers onChange +model.firstName = "John" // This triggers onChange +model.lastName = "Doe" // This does NOT trigger onChange again + +print("\n=== Shopping Cart Example ===") +let cart = ShoppingCart() + +withObservationTracking { + print("Cart total: $\(cart.total)") + print("Items count: \(cart.items.count)") +} onChange: { + print("Cart updated! (onChange called once)") +} + +// Only the first change triggers onChange +cart.addItem(CartItem(name: "Coffee", price: 4.99)) // This triggers onChange +cart.addItem(CartItem(name: "Sandwich", price: 8.99)) // This does NOT trigger onChange again +cart.discount = 0.1 // This does NOT trigger onChange again + +print("\n=== Selective Property Observation ===") +let anotherPerson = Person() + +// Only observe the name property +withObservationTracking { + print("Observing only name: \(anotherPerson.name)") +} onChange: { + print("Name changed! (onChange called once)") +} + +anotherPerson.name = "Charlie" // This triggers onChange +anotherPerson.age = 25 // This doesn't trigger onChange (age wasn't accessed) + +print("\n=== Multiple Observation Tracking ===") +let person2 = Person() + +// Setting up first observation +withObservationTracking { + print("First observation - Name: \(person2.name)") +} onChange: { + print("First onChange triggered") +} + +// Setting up second observation +withObservationTracking { + print("Second observation - Age: \(person2.age)") +} onChange: { + print("Second onChange triggered") +} + +person2.name = "David" // Triggers first onChange +person2.age = 40 // Triggers second onChange