Self-correction: will/didChangeValue still do accept string key paths; it’s 
just that in that case they are not respelled and still use “forKey:” as their 
first argument label. Not sure why that didn’t come up in autocomplete when I 
was doing my tests the other day. I still think that making @objc broadly 
available would be a more elegant solution.

> On Jun 10, 2017, at 11:47 AM, Charles Srstka via swift-evolution 
> <[email protected]> wrote:
> 
> INTRODUCTION:
> 
> This pitch proposes to allow the @objc keyword on all property declarations, 
> even ones whose type cannot be represented in Objective-C.
> 
> MOTIVATION:
> 
> You’re thinking, “But that’s crazy. Why would you ever want to do that?” But 
> hear me out:
> 
> - In Swift 4, barring cases where it’s required for technical reasons, 
> properties are exposed to Objective-C only if there’s an actual @objc keyword 
> present. This keyword represents a clear statement of intent that this 
> protocol is meant to be visible to Objective-C, and means that expanding the 
> scope of the @objc keyword will not increase code size anywhere other than 
> where a deliberate decision has been made to do so.
> 
> - Since Swift 3, all Swift types are in fact bridgeable to Objective-C, and 
> an ‘Any’ is bridged to an ‘id’.
> 
> - While it’s true that Objective-C will generally get an opaque object that 
> it won’t know what to do with, that doesn’t mean that there aren’t cases 
> where this can still be useful, such as:
> 
> - Value transformers. Even if a type is completely opaque to Objective-C, 
> that doesn’t mean a value transformer can’t convert it into something that 
> Objective-C can use. There are some quite useful general-purpose value 
> transformers that could be written for this task, such as:
> 
> class CustomStringConvertibleValueTransformer: ValueTransformer {
>     override class func transformedValueClass() -> AnyClass { return 
> NSString.self }
>     override class func allowsReverseTransformation() -> Bool { return false }
>     
>     override func transformedValue(_ value: Any?) -> Any? {
>         return (value as? CustomStringConvertible)?.description
>     }
> }
> 
> With this value transformer, an enum like this one from the Swift manual 
> could be exposed to Objective-C and bound to a UI element in Interface 
> Builder, resulting in a meaningful value being shown in the UI:
> 
> enum Suit: CustomStringConvertible {
>     case spades, hearts, diamonds, clubs
>     var description: String {
>         switch self {
>         case .spades:
>             return "spades"
>         case .hearts:
>             return "hearts"
>         case .diamonds:
>             return "diamonds"
>         case .clubs:
>             return "clubs"
>         }
>     }
> }
> 
> Once we have generalized existentials, we could write this value transformer 
> as well, which would be able to handle all Swift enums backed by 
> ObjC-representable types without any special hacks:
> 
> class RawRepresentableValueTransformer: ValueTransformer {
>     override class func transformedValueClass() -> AnyClass { return 
> AnyObject.self }
>     override class func allowsReverseTransformation() -> Bool { return false }
>     
>     override func transformedValue(_ value: Any?) -> Any? {
>         return (value as? RawRepresentable)?.rawValue
>     }
> }
> 
> - KVO dependencies. Even without a value transformer, a property of a 
> non-ObjC-representable type may be a dependency of other properties which 
> *are* ObjC-representable, as in this example:
> 
> class PlayingCard: NSObject {
>     @objc dynamic var suit: Suit // currently an error
>     
>     init(suit: Suit) {
>         self.suit = suit
>         super.init()
>     }
> 
>     @objc private static let keyPathsForValuesAffectingSuitName: Set<String> 
> = [#keyPath(suit)]
>     @objc var suitName: String { return self.suit.description }
> }
> 
> Although the ‘suit’ property is not representable in Objective-C, the 
> ‘suitName’ property is, and it would behoove us to allow it to be updated 
> when ‘suit’ changes. Currently, we have to resort to various hacks to 
> accomplish this. One can manually send the KVO notifications on the original 
> property instead of near the dependent ones, which can be error-prone:
> 
> class ErrorPronePlayingCard: NSObject {
>     var suit: Suit {
>         willSet { self.willChangeValue(for: \.suitName) }
>         didSet { self.didChangeValue(for: \.suitName) }
>     }
>     
>     init(suit: Suit) {
>         self.suit = suit
>         super.init()
>     }
>     
>     @objc var suitName: String { return self.suit.description }
> }
> 
> Formerly, one could simply use arbitrary strings as key paths, which was ugly 
> since it required a separate override of value(forKey:) to avoid exceptions 
> if someone actually tried to acquire a value using the key. Also, it doesn’t 
> seem to work anymore in Swift 4, since the will/didChangeValue methods now 
> expect a KeyPath object instead of a String:
> 
> class DontWorkNoMorePlayingCard: NSObject {
>     var suit: Suit {
>         willSet { self.willChangeValue(for: "suit") } // error: this requires 
> a KeyPath now
>         didSet { self.didChangeValue(for: "suit") }
>     }
>     
>     override func value(forKey key: String) -> Any? {
>         switch key {
>         case "suit":
>             return self.suit
>         default:
>             return super.value(forKey: key)
>         }
>     }
>     
>     init(suit: Suit) {
>         self.suit = suit
>         super.init()
>     }
>     
>     @objc private static let keyPathsForValuesAffectingSuitName: Set<String> 
> = ["suit"]
>     @objc var suitName: String { return self.suit.description }
> }
> 
> One can ugly up the class with separate Any-typed properties, which serve no 
> purpose other than to facilitate KVO dependencies:
> 
> class UglyPlayingCard: NSObject {
>     @objc private var _objCSuit: Any { return self.suit }
>     var suit: Suit {
>         willSet { self.willChangeValue(for: \._objCSuit) }
>         didSet { self.didChangeValue(for: \._objCSuit) }
>     }
>     
>     init(suit: Suit) {
>         self.suit = suit
>         super.init()
>     }
>     
>     @objc private static let keyPathsForValuesAffectingSuitName: Set<String> 
> = [#keyPath(_objCSuit)]
>     @objc var suitName: String { return self.suit.description }
> }
> 
> None of these hacks would be necessary if it were possible to simply put 
> @objc on any property declaration.
> 
> DETAILED DESIGN:
> 
> For read-only properties, implementation is easy; just expose the type to 
> Objective-C as ‘id’, so:
> 
> @objc private(set) var suit: Suit
> 
> becomes:
> 
> @property (nonatomic, readonly) id suit;
> 
> For writable properties, there is the danger that an Objective-C client could 
> pass an object of the wrong type to the setter. For this case, we could 
> generate a thunk for the setter that checks the type of incoming values, and 
> simply ignores values of the wrong type:
> 
> func setter(_ val: Any) {
>     if let suit = val as? Suit {
>         self.suit = suit
>     }
> }
> 
> Alternatively, we could fatalError if the wrong type is sent.
> 
> func setter(_ val: Any) {
>     if let suit = val as? Suit {
>         self.suit = suit
>     } else {
>         fatalError("Passed-in value is not a Suit")
>     }
> }
> 
> For dynamic properties, we would want to reroute all sets through this thunk 
> to ensure that the automatically-added KVO notifications will be fired. This 
> will add a small performance cost due to the dynamic type check, but 
> generally speaking, KVO is not a tool that one uses in performance-critical 
> sections.
> 
> One special-case that would be nice to add would be to make AnyKeyPath, 
> PartialKeyPath, KeyPath, and friends bridge to Objective-C as a string, which 
> would allow us to get rid of the one remaining time #keyPath needed to be 
> used in the examples for this pitch, and declare dependencies instead as:
> 
> @objc private static let keyPathsForValuesAffectingSuitName: 
> Set<PartialKeyPath<PlayingCard>> = [\.suit]
> 
> which would expose itself to the Objective-C type system as: 
> 
> @property (class, readonly) NSSet<NSString *> 
> *keyPathsForValuesAffectingSuitName;
> 
> allowing the KVO system to process it as normal.
> 
> IMPACT ON EXISTING CODE:
> 
> None; this is purely additive.
> 
> ALTERNATIVES CONSIDERED:
> 
> Keep on using the hacks described above.
> 
> Charles
> 
> _______________________________________________
> swift-evolution mailing list
> [email protected]
> https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
[email protected]
https://lists.swift.org/mailman/listinfo/swift-evolution

Reply via email to