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/didSetbehavior -
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
-
Syntax preferences: Is
set(when:)the clearest syntax, or would alternatives likeset(if:)orconditionalSet:be better? -
Built-in conditions: Should we include convenience conditions like
max,min,!=or require explicit closures? -
Type constraints: Should built-in conditions be limited to
Comparable/Equatabletypes, or should we provide more generic approaches? -
Edge cases: How should this interact with optional types, reference types, and generic constraints?
-
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.