Pitch: Conditional Property Setters with set(when:)

Pitch: Conditional Property Setters with set(when:)

Problem

Swift's property observers (willSet/didSet) are called on every assignment, even when the assigned value is identical to the current value. This creates unnecessary overhead in reactive systems (Combine, SwiftUI, etc.) and requires boilerplate code for conditional assignments.

Common boilerplate pattern:

if newValue != currentValue {
    currentValue = newValue
}

Real-world impact:

  • Unnecessary publisher emissions in Combine

  • Redundant SwiftUI view updates

  • Performance overhead in high-frequency updates

  • Verbose code for simple conditional logic

Proposed Solution

Extend Swift's property syntax to support conditional setters that evaluate a condition before assignment:

var value: Int {
    get { _value }
    set(when: !=) { _value = newValue }
}

For common patterns:

var highWaterMark: Int { set(when: max) }  // Only set if greater
var lowWaterMark: Int { set(when: min) }   // Only set if smaller  
var currentState: State { set(when: !=) }  // Only set if different

Key benefit: willSet/didSet and reactive publishers would only fire when the condition is met and the value actually changes.

Detailed Design

The set(when:) syntax would accept a function that takes two parameters of the property's type and returns a Bool:

var property: T {
    set(when: (T, T) -> Bool) { 
        // assignment logic
    }
}

Built-in convenience conditions functions:

// For Comparable types
max  // equivalent to { $0 < $1 }
min  // equivalent to { $0 > $1 }
!=   // equivalent to { $0 != $1 } (requires Equatable)

Custom conditions:

var temperature: Double {
    set(when: { current, new in abs(current - new) > 0.1 }) {
        _temperature = newValue
    }
}

Why Existing Approaches Don't Work

Property Wrappers

Property wrappers can't solve this because willSet/didSet are called on the wrapped property assignment, which always occurs regardless of internal conditional logic.

Attempted solution:

@propertyWrapper
struct SetWhen<T: Comparable> {
    private var value: T
    var wrappedValue: T {
        get { value }
        set { if condition(value, newValue) { value = newValue } }
    }
}

struct Example {
    @SetWhen(condition: >)
    var i: Int = 100 {
        willSet { print("Always called") }  // Problem: this always fires
    }
}

Macros

Macros could generate conditional setter logic but would need to:

  • Parse and replicate existing willSet/didSet behavior

  • Handle complex inheritance scenarios

  • Maintain debugging experience

  • Integrate with other property features (@Published, KVO, etc.)

The complexity and fragility make this approach unsuitable for a fundamental language feature.

Use Cases

High/Low Water Marks:

var maxConcurrentUsers: Int { set(when: max) }
var minResponseTime: TimeInterval { set(when: min) }

State Management:

var connectionState: ConnectionState { 
    set(when: !=) 
    didSet { notifyStateChange() }  // Only called on actual changes
}

Performance-Critical Updates:

@Published var heavyComputedValue: ComplexType { 
    set(when: !=)  // Prevents unnecessary SwiftUI updates
}

Threshold-Based Updates:

var batteryLevel: Double {
    set(when: { abs($0 - $1) > 0.01 }) {  // Only update for 1% changes
        _batteryLevel = newValue
    }
}

Questions for the Community

  1. Syntax preferences: Is set(when:) the clearest syntax, or would alternatives like set(if:) or conditionalSet: be better?

  2. Built-in conditions: Should we include convenience conditions like max, min, != or require explicit closures?

  3. Type constraints: Should built-in conditions be limited to Comparable/Equatable types, or should we provide more generic approaches?

  4. Edge cases: How should this interact with optional types, reference types, and generic constraints?

  5. Performance expectations: What performance improvements would make this feature worthwhile for the community?

Implementation Considerations

This feature would need to:

  • Integrate with existing property observer lifecycle

  • Work correctly with inheritance and protocol conformance

  • Support debugging and introspection

  • Maintain source compatibility

I believe this addresses a real pain point in Swift development and would eliminate significant boilerplate while improving performance in reactive systems.

What are your thoughts on this approach? Are there other use cases or technical considerations I should explore? Thanks for your consideration.

2 Likes