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