1

I have a protocol

protocol Example: class {
    var value: Bool { get set }
    func foo()
    func bar()
}

And extension:

extension Example {

//    var value: Bool { // Error: Extensions must not contain stored properties
//        didSet {
//            switch value {
//            case true:
//                foo()
//            case false:
//                bar()
//            }
//        }
//    }

    func foo() {
        // logic...
    }
    func bar() {
        // logic...
    }
}
  • When value is set to true, I want foo() to be called
  • When value is set to false, I want bar() to be called

However, I do not want to redundantly implement didSet{ } logic into every class that conforms to Example

But, if I try to add didSet{ } logic into the extension, Xcode says "Extensions must not contain stored properties".

What is the best practice for adding default property-observing logic without having to copy/paste into every conforming class?

The Goal:

I want any subclass of UIView to conform to my protocol Expandable. The requirements of my protocol are isExpanded: Bool, expand(), and collapse. I want isExpanded = true to call expand(), and isExpanded = false to call collapse() (much like the behavior of setting isHidden). But for every subclass of UIView, I don't want to have rewrite any logic. I'd like to just make the class conform to Expandable, and jump right in to setting isExpanded.

Michael
  • 1,115
  • 9
  • 24
  • 1
    Compare https://stackoverflow.com/a/33863728/1187415: Extensions can add new computed properties, but they cannot add stored properties, **or add property observers** to existing properties. – Martin R Jun 11 '18 at 00:20
  • @MartinR, I know I can not add this to an extension, I want to know how to add default property observers for protocols – Michael Jun 11 '18 at 00:22
  • 2
    You can't. Your question is in practice identical to Martin's case. If this is becoming a major issue, you probably wanted Example to be a class with subclasses rather than a protocol. (Yes, I am aware that many people disparage classes. Swift often strongly encourages classes and gives you dramatically more flexibility with them than it does with protocols; do not try to use protocols to recreate "classes without classes." I am also aware that Swift lacks an "abstract class" type which would be very useful for this. That is true.) – Rob Napier Jun 11 '18 at 01:26
  • @RobNapier I can't have multiple inheritances, and the subclasses that I want to conform already inherit from a different class – Michael Jun 11 '18 at 02:12
  • Then this is definitely impossible. You cannot attach property observers to a class just by making it conform to a protocol. Telling Swift that a class conforms to a protocol can't reach into instances and change their internal structure in memory (which is required in order to add the observer). Since protocol extension can be applied in other modules, Swift may not even know about the extension at compile-time. – Rob Napier Jun 11 '18 at 13:12
  • You will need to use an observer pattern like KVO, RxSwift, or https://gist.github.com/rnapier/981e86fbf345b049c1df41f63e4a2c6e. (Though only KVO can really work on something that you don't control, and it has to be an NSObject subclass and KVO compliant for the property.) There is no general, universal way to do what you're describing. Swift is free to have completely optimized out property accessors for improved performance before you get the chance to add an observer. – Rob Napier Jun 11 '18 at 13:18
  • @RobNapier, you're gonna need to explain that a bit for me – Michael Jun 11 '18 at 18:33
  • @MichaelAustin What you're trying to do is impossible, so it comes back to what is your underlying goal? Then we can discuss ways to implement that. – Rob Napier Jun 11 '18 at 20:17
  • @RobNapier I clarified the goal in context. Thank you for bearing through this. – Michael Jun 12 '18 at 15:56
  • I don't quite understand this bit: "But for every subclass of UIView, I don't want to have rewrite any logic." Can every subclass of UIView be expanded and collapsed in the same way? – Rob Napier Jun 12 '18 at 16:16
  • Obviously, I have some logic in each view bc they can't all be collapsed and expanded in the same way, I just didn't want to rewrite logic that is redundant between each view. – Michael Jun 12 '18 at 18:40

2 Answers2

-1

You don't need observers for what you're describing. You just need some storage for your state. Since you know this is an NSObject, you can do that with the ObjC runtime.

// Since we know it's a UIView, we can use the ObjC runtime to store stuff on it
private var expandedProperty = 0

// In Xcode 10b1, you can make Expandable require this, but it's probably
// nicer to still allow non-UIViews to conform.
extension Expandable where Self: UIView {
    // We'll need a primitive form to prevent infinite loops. It'd be nice to make this private, but
    // then it's not possible to access it in custom versions of expand()
    var _isExpanded: Bool {
        get {
            // If it's not know, default to expanded
            return objc_getAssociatedObject(self, &expandedProperty) as? Bool ?? true
        }
        set {
            objc_setAssociatedObject(self, &expandedProperty, newValue, .OBJC_ASSOCIATION_ASSIGN)
        }
    }
    var isExpanded: Bool {
        get {
            return _isExpanded
        }
        set {
            _isExpanded = newValue
            if newValue { expand() } else { collapse() }
        }
    }
    func expand() {
        _isExpanded = true  // Bypassing the infinite loop
        print("expand")

    }
    func collapse() {
        _isExpanded = false
        print("collapse")
    }
}

If you didn't know that this were an NSObject, you can get the same thing with a global (private) dictionary that maps ObjectIdentifier -> Bool. It just leaks a tiny amount of memory (~16 bytes per view that you collapse).

That said, I wouldn't do it this way. Having two ways to do the same thing makes everything much more complicated. I would either have just isExpanded as settable, or have isExpanded as read-only and a expand and collapse. Then you don't need the extra _isExpanded.

Rob Napier
  • 286,113
  • 34
  • 456
  • 610
  • Yeah, I agree this solution is over-complicated and un-Swifty... In your last sentence, you say "I would either have just isExpanded as settable". Can you elaborate on that? I didn't think a variable could only be settable, but this might be the Swifty solution I'm looking for. Thanks Rob! – Michael Jun 12 '18 at 17:58
  • I'm sorry, I meant that I would just have `isExpanded` as a read/write property without expand/collapse, or I'd have `isExpanded` as a read-only property with expand/collapse. – Rob Napier Jun 12 '18 at 18:20
  • To have "write-only", you'd create a `setExpanded(Bool)` method and not use a property. – Rob Napier Jun 12 '18 at 18:20
  • ah I see. Do you think you could draft that up as an answer for me to accept? I think I like that the best. – Michael Jun 12 '18 at 18:25
-3

You have to implement getter, setter explicitly:

protocol Example {
   var value: Bool { get set }
   func foo()
   func bar()
}

extension Example {
   var value: Bool {
       get { return value }

       set(newValue) {
           value = newValue
           value ? foo() : bar()
        }
    }


   func foo() {
       print("foo")
   }

   func bar() {
       print("bar")
   }
}
tphduy
  • 119
  • 7