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
