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?
CodePudding user response:
Since it's optional, Swift can't guarantee the setter is implemented.
CodePudding user response:
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 toassetURL
- Sadly, this method cannot be named
-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 markassetURL
asreadonly
) - Also sadly, this method won't be able to have a default implementation, since Objective-C doesn't support default protocol implementations, and you can't give the method an implementation in a Swift
extension
because you still can't assign to the protocol
- Sadly, this method cannot be named
Do like you would in Swift and introduce a protocol hierarchy, where, for example, an
AssetBackedPlayback
protocol inherits fromPlayback
and offersassetURL
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 anAssetBackedPlayback
in order to assign theassetURL
.
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!