3

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?

Rakuyo
  • 390
  • 3
  • 15
  • 1
    This may actually be worth asking over on the [Swift forums](https://forums.swift.org/), where you may get someone who works on the compiler to chime in. This seems very much to be an extremely niche edge case whose compiler diagnostic is also not particularly helpful; it may be a bug that this does not compile (this does work in Obj-C), or at least the message can be refined. FWIW, you can define the exact same protocol + properties in pure Swift (with the `@objc` annotation to allow for an `optional var`) and you'll see the same behavior. – Itai Ferber Dec 06 '21 at 02:41
  • Another issue is that you have a type mismatch between URL and NSURL. – Victor Engel Dec 06 '21 at 17:11
  • @ItaiFerber I would also like to ask a question on the Swift forum, but unfortunately, for some objective reason, I can't access this site... – Rakuyo Dec 07 '21 at 01:12
  • @Rakuyo That's unfortunate. :( I would be more than happy to post on your behalf there, but I'm afraid you wouldn't be able to view the responses... – Itai Ferber Dec 07 '21 at 01:20
  • 1
    @ItaiFerber If you can help me that would be really appreciated, you can send me an email when you have a definite conclusion or reply to me under this question. This may take some of your time, but if you don't have time then just ask me instead, as long as the question helps more people I'll be happy. – Rakuyo Dec 07 '21 at 01:25
  • 1
    @Rakuyo Absolutely. I'll ask on the forums and relay a (hopefully definitive) response here. – Itai Ferber Dec 07 '21 at 01:32
  • For anyone curious in following along with access to the forums, posted the question here: https://forums.swift.org/t/compilation-error-in-assigning-to-obj-c-optional-protocol-property/53886 – Itai Ferber Dec 07 '21 at 02:00
  • It's a known bug. See https://bugs.swift.org/browse/SR-5475 where Jordan Rose also gives an excellent workaround. – matt Dec 07 '21 at 04:33

2 Answers2

2

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!

Itai Ferber
  • 28,308
  • 5
  • 77
  • 83
0

Since it's optional, Swift can't guarantee the setter is implemented.

Victor Engel
  • 2,037
  • 2
  • 25
  • 46
  • So why does it not report an error when I try to get the value from it? The getter method must exist? Why? – Rakuyo Dec 06 '21 at 07:37
  • Retrieving the value can return an optional. It need not exist. – Victor Engel Dec 06 '21 at 13:46
  • I don't disagree with the reasoning, but theoretically, a setter could also be implemented in the same way that `optional` methods are implemented: if the setter does not exist, the operation is a no-op. I would still be curious about more authoritative reasoning — I don't think this is really documented anywhere, but again, it may be worth prodding on the Swift Forums (or digging through the compiler). – Itai Ferber Dec 06 '21 at 14:19