Home > database >  Why do properties modified by @optional become immutable?
Why do properties modified by @optional become immutable?

Time:12-07

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:

  1. Introduce an explicit method to Playback which assigns a value to assetURL

    • 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 mark assetURL as readonly)
    • 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
  2. Do 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!

  • Related