SE-0436: Objective-C implementations in Swift

Hello Swift community,

The review of SE-0436: Objective-C implementations in Swift begins now and runs until May 21st, 2024.

Reviews are an important part of the Swift evolution process. All review feedback should be either on this forum thread or, if you would like to keep your feedback private, directly to me as the review manager via the forum messaging feature or email. When contacting the review manager directly, please put "SE-0436" in the subject line.

Try it out

Toolchains with this feature enabled behind the experimental feature flag ObjCImplementation are available at the links below. Note that this is an Objective-C interop feature, so only the macOS toolchain will really be able to use this feature, but other toolchains are included for completeness.

What goes into a review?

The goal of the review process is to improve the proposal under review through constructive criticism and, eventually, determine the direction of Swift. When writing your review, here are some questions you might want to answer in your review:

What is your evaluation of the proposal?

  • Is the problem being addressed significant enough to warrant a change to Swift?
  • Does this proposal fit well with the feel and direction of Swift?
  • If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?
  • How much effort did you put into your review? A glance, a quick reading, or an in-depth study?

More information about the Swift evolution process is available at swift-evolution/process.md at main · apple/swift-evolution · GitHub.

Thank you,

Freddy Kellison-Linn
Review Manager

14 Likes

final members may require Swift metadata

When does this happen? Is there a way to guarantee we don't so that we only incur Objc metadata binary size hit?

Replacing an Objective-C @implementation declaration with a Swift @objc @implementation extension is invisible to the library's Objective-C and Swift clients, so it should not be source-breaking

If you have to move the Objc header to be exported to be visible to Swift, it could be source breaking, no? Could be a name conflict at the app level, and changes the module API.

  • Factory convenience initializers (those implemented as class methods, like +[NSString stringWithCharacters:length:]).

This would be a pretty large limitation, is it doable to include in this proposal?

@objc @implementation cannot implement global declarations that cannot be created by @objc

Does this mean no static c functions in headers? Also a pretty large limitation which I hope can be included or followed up soon.

Since very few classes use lightweight generics, we have chosen to ban the combination

Raising my hand that there's demand in my codebase for this.

A member implementation must:

  • Have the same Swift name as the member it implements.

Have a question. The exists Objective-C API bridged to Swift, may contains ambiguous because Swift has default param and trailing closure syntax. Some Objective-C code will use code style like this:

@interface UIImageView (WebCache)
- (void)sd_setImageWithURL:(nullable NSURL *)url
          placeholderImage:(nullable UIImage *)placeholder
                   options:(SDWebImageOptions)options;

- (void)sd_setImageWithURL:(nullable NSURL *)url
          placeholderImage:(nullable UIImage *)placeholder
                 completed:(nullable SDExternalCompletionBlock)completedBlock;
@end

Generated Swift API (via ClangImporter):

extension UIImageView {
    open func sd_setImage(with url: URL?, placeholderImage placeholder: UIImage?, options: SDWebImageOptions = [])

    open func sd_setImage(with url: URL?, placeholderImage placeholder: UIImage?, completed completedBlock: SDExternalCompletionBlock? = nil)
}

As the proposal says, the Swift member implementation should Have the same Swift name as the member it implements.. So actually if I wan to use this feature to remove the Objective-C implementation, I need to write:

@objc(WebImage) @implementation extension UIImageView {
    open func sd_setImage(with url: URL?, placeholderImage placeholder: UIImage?, options: SDWebImageOptions = []) {
      // ...impl
      print("Foo")
    }
  
    open func sd_setImage(with url: URL?, placeholderImage placeholder: UIImage?, completed completedBlock: SDExternalCompletionBlock? = nil) {
      // ...impl
      print("Bar")
    }
}

But then, if I write as the code above, because this API is ambiguous in Swift, I can not call this in pure Swift code.

public class Tester {
  func test() {
    UIImageView().sd_setImage(with: nil, placeholderImage: nil) // <<<<----- error: Ambiguous use of 'sd_setImage'
  }
}

Possible solution

The @objc @implementation should also support NS_REFINED_FOR_SWIFT and NS_SWIFT_NAME. Which means, I can update like this to separate what Swift client see and what Objective-C client see, right ?

// For Swift client's API
extension UIImageView {
    open func sd_setImage(with url: URL?, placeholderImage placeholder: UIImage?, options: SDWebImageOptions = [], completedBlock: SDExternalCompletionBlock? = nil) {
        // ...impl
        print("Hello")
    }
}

// For Objective-C client's API
@objc(WebImage) @implementation extension UIImageView {
    open func __sd_setImage(with url: URL?, placeholderImage placeholder: UIImage?, options: SDWebImageOptions = []) {
      // ...impl
      print("Foo")
    }
  
    open func __renamed_sd_setImage(with url: URL?, placeholderImage placeholder: UIImage?, completed completedBlock: SDExternalCompletionBlock? = nil) {
      // ...impl
      print("Bar")
    }
}

What happens if you remove the default value for one of options or completedBlock?

1 Like

Honestly, this is something I should revisit; I don’t think I fully chased it down. If it is unnecessary, changing it would be a bug fix/performance improvement.

Only if you add the header to your module’s umbrella header. @implementation doesn’t require you to implement only things in your own module, so you can put the header in a private module and then import that module with internal import so it won’t be visible to your clients.

It is a large limitation, but factory inits are a large feature. Fortunately, factory inits can be moved into a category that you continue to implement in Objective-C. Many factory inits have almost no code in them, so there’s little potential for bugs.

As mentioned in the future directions, there’s already a @_cdecl @implementation prototype which can do global functions. The problem is just that there’s no way to write a C-compatible global function in Swift that will actually do ObjC bridging. If we implement that feature, it would be pretty easy to make @implementation work for it.

:saluting_face:

The problem in this code sample isn’t @implementation—you’d have the exact same problem if you implemented these methods in an Objective-C .m file. And note that you should be able to call either method just fine as long as you specify the last argument explicitly.

(However, I’m realizing now that I haven’t specified rules for implementing a member that’s imported with default arguments. It might actually be better to not have you specify default arguments in the implementation—you’re going to have to use the default arguments ClangImporter expects anyway. Same logic as for conformances, basically.)

1 Like

I am not confident that we'll ever use this as it's very possible that by the time we finally get to rewriting RealmSwift to eliminate the obj-c layer we'll also ready to drop the obj-c API anyway, but if we did I think lightweight generics would be a hard requirement. Not being able to do class factory methods would force some significant API changes, but I think that might be solveable with some objc-defined categories that wrap swift-implemented functionality, so it probably wouldn't be a blocker.

2 Likes

Can't wait to see this feature implemented. As someone working with a mix of Objective-C and Swift in a large codebase, I think this would make our lives so much easier regarding maintaining cleaner headers and support to Obj-C while still coding in Swift. Awesome!

2 Likes

Huge +1.

This will help to enable developers to utilize Swift features in legacy ObjC projects, with much less concern in a lot of aspects. Hope this feature can land!

1 Like

Yes. Workaround available. All this is because of the @objc @implementation based on ClangImporter, so I think we'd better references some link or special behavior for ClangImporter