The order in which I structure my classes/structs is generally stored properties first, initializers, then all functional stuff such as computed properties, methods, static stuff, etc.
This makes it easier for me when refactoring a type as if I'm wondering whether the change should have an impact on the instance's state, I can scroll to the top and quickly look at the stored properties and after enumerating over each will know that either (a) something has to be updated or (b) I'm good.
This requires more reading if there is any functional code in that section of the type, as it's just more text I have to sort through in order to figure out what it is I have to do. There is minimizing/collapsing, but in my experience it's far from satisfactory as it doesn't necessarily persist across sessions or apply to all open windows displaying the type.
One workaround I've done in the past for this is to instead of using didSet or willSet, I made the property private, and then would just define a setter giving me the ability to do whatever I wanted before/after setting it. Obviously this approach requires care as the larger the type gets, the more likely it will be to forget using the setter and just assign new values to the property itself.
A solution to this would be if a single didSet/willSet listener could be defined separately from the property definition. This would allow me to organize it below the initializers and not crowd the stored property area.
class C {
let foo: Int
// Other stored properties.
init(_ foo: Int) {
self.foo=foo
// Initialize other stored properties.
}
foo {
willSet {
// Do stuff.
}
didSet {
// Do stuff.
}
}
}
While I appreciate the motivation, I don't think that more language features would be that much better than what we have now:
class C {
var foo: Int {
willSet { foo_willSet(newValue) }
didSet { foo_didSet(oldValue) }
}
init(_ foo: Int) {
self.foo=foo
}
}
private extension C {
private func foo_willSet(_ newValue: Int) {
}
private func foo_didSet(_ oldValue: Int) {
}
}
…or
class C {
@Observed var foo: Int
init(_ foo: Int) {
_foo = .init(wrappedValue: foo)
_foo.willSet = foo_willSet
_foo.didSet = foo_didSet
}
}
private extension C {
private func foo_willSet(_ newValue: Int) {
}
private func foo_didSet(_ oldValue: Int, _ value: inout Int) {
}
}
/// A workaround for limitations of Swift's observed properties.
///
/// Limitations of Swift's property observers:
/// 1. They are not mutable.
/// 2. They cannot be referenced as closures.
@propertyWrapper public struct Observed<Value> {
public typealias WillSet = (_ newValue: Value) -> Void
public typealias DidSet = (
_ oldValue: Value,
_ value: inout Value
) -> Void
public var willSet: WillSet
public var didSet: DidSet
public var wrappedValue: Value {
willSet { willSet(newValue) }
didSet { didSet(oldValue, &wrappedValue) }
}
public var projectedValue: Self {
get { self }
set { self = newValue }
}
}
// MARK: - public
public extension Observed {
init(
wrappedValue: Value,
willSet: @escaping WillSet = { _ in },
didSet: @escaping DidSet = { _, _ in }
) {
self.wrappedValue = wrappedValue
self.willSet = willSet
self.didSet = didSet
}
}
Personally I tend to think that hidden side effects at the call sites are a bad thing. Your solution of using a function doesn't seem bad because it makes it obvious at the call sites that there are side effects. willSet/didSet/property-wrappers hide that one step further. If you were able to move those elsewhere then that would be pretty bad in terms of hidden side effects.
That includes many useful things I'm afraid. The mentioned willSet/didSet/property-wrappers, plain get/set, default function parameters, even a + b or a == b operation – do you want to write it as add(a, b), eq(a, b) to make it more obvious it can have side effects? x = nil can cause a spooky action at a distance effectively nulling out a weak reference in a different object in a different place of the app some time later. Tens and hundreds big and small useful things like these every swift developer uses every day.
No, things like a + b and a == b generally do not have side-effects, if they did then that probably wouldn't be great. In practice I only had actual difficulties reading code that (ab)used didSets.
Of course. It was just the easiest way to write the example. I recommend unowned.
If you're going to write code that refers to an instance, while it is being set, it's going to need to be a reference type—i.e. make Observed a class, not a struct. In either case, refer to the property in didSet as value, not foo.
In any case, the Observed wrapper can't have as much functionality as the missing language feature it partially recreates would have.