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

Reply via email to