I have an Objective-C protocol that contains a property as follows:
#import <Foundation/Foundation.h>
@protocol Playback <NSObject>
@optional
@property (nonatomic, nonnull) NSURL *assetURL;
@end
PlayerController
has a property of type id<Playback>
:
@interface PlayerController: NSObject
@property (nonatomic, strong, nonnull) id<Playback> currentPlayerManager;
@end
I tried to write the following code in Swift, but I got an error:
var player = PlayerController()
var pla = player.currentPlayerManager
pla.assetURL = URL(string: "123") // ❌ Cannot assign to property: 'pla' is immutable
If I comment out the @optional
for the Playback
protocol, then it compiles fine.
This makes me wonder why @optional
would cause this error?
From Jordan Rose (who worked on Swift at the time that SE-0070 was implemented) on the forums:
Normally optional requirements add an extra level of optionality:
- Methods become optional themselves (
f.bar?()
)- Property getters wrap the value in an extra level of
Optional
(if let bar = f.bar
)But there's nowhere to put that extra level of Optional for a property setter. That's really the entire story: we never figured out how to expose optional property setters in a safe way, and didn't want to pick any particular unsafe solution. If someone can think of something that'd be great!
So the answer appears to be: at the time that optional
protocol requirements were intentionally limited to Objective-C protocols in Swift (SE-0070), no spelling for an explicit implementation of this was decided on, and it appears that this functionality is uncommon enough that this hasn't really come up since.
Until (and if) this is supported, there are two potential workarounds:
Introduce an explicit method to Playback
which assigns a value to assetURL
-setAssetURL:
because it will be imported into Swift as if it were the property setter instead of a method, and you still won't be able to call it. (This is still true if you mark assetURL
as readonly
)extension
because you still can't assign to the protocolDo like you would in Swift and introduce a protocol hierarchy, where, for example, an AssetBackedPlayback
protocol inherits from Playback
and offers assetURL
as a non-@optional
-property instead:
@protocol Playback <NSObject>
// Playback methods
@end
@protocol AssetBackedPlayback: Playback
@property (nonatomic, nonnull) NSURL *assetURL;
@end
You would then need to find a way to expose PlayerController.currentPlayerManager
as an AssetBackedPlayback
in order to assign the assetURL
.
Some additional alternatives from Jordan:
I think the original recommended workaround was "write a
static inline
function in Objective-C to do it for you", but that's not wonderful either.setValue(_:forKey:)
can also be good enough in practice if it's not in a hot path.
The static inline
function recommendation can function similarly to a default protocol implementation, but you do need to remember to call that function instead of accessing the property directly.
setValue(_:forKey:)
will also work, but incurs a noticeable performance penalty because it supports a lot of dynamism through the Objective-C runtime, and is significantly more complicated than a simple assignment. Depending on your use-case, the cost may be acceptable in order to avoid complexity!