- Proposal: SE-0058
- Authors: Russ Bishop, Doug Gregor
- Review Manager: Joe Groff
- Status: Rejected
- Review: (pitch) (review) (deferral) (rejection)
Provide an ObjectiveCBridgeable
protocol that allows a Swift type to control how it is represented in Objective-C by converting into and back from an entirely separate @objc
type. This frees library authors to create truly native Swift APIs while still supporting Objective-C.
There is currently no good way to define a Swift-y API that makes use of generics, enums with associated values, structs, protocols with associated types, and other Swift features while still exposing that API to Objective-C.
This is especially prevelent in a mixed codebase. Often an API must be dumbed-down or Swift features eschewed because rewriting the entire codebase is impractical and Objective-C code must be able to call the new Swift code. This results in a situation where new code or refactored code adopts an Objective-C compatible API which is compromised, less type safe, and isn't as nice to work with as a truly native Swift API.
The cascading effect is even worse because when the last vestiges of Objective-C have been swept away, you're left with a mountain of Swift code that essentially looks like a direct port of Objective-C code and doesn't take advantage of any of Swift's modern features.
For framework and library authors it presents an awful choice:
- Write mountains of glue code to convert between Swift and Objective-C versions of your types.
- Write your shiny new framework in Swift, but in an Objective-C style using only
@objc
types. - Write your shiny new framework in Objective-C.
Choice #1 is not practical in the real world with ship dates, resulting in most teams choosing #2 or #3.
Today you can adopt the private protocol _ObjectiveCBridgeable
and when a bridged collection (like Array
<--> NSArray
) is passed between Swift and Objective-C, Swift will automatically call the appropriate functions to control the way the type bridges. This allows a Swift type to have a completely different representation in Objective-C.
The solution proposed is to expose a new protocol ObjectiveCBridgeable
and have the compiler generate the appropriate Objective-C bridging thunks for any function or property of an @objc
type, not just for values inside collections.
/// A type adopting `ObjectiveCBridgeable` will be exposed
/// to Objective-C as the type `ObjectiveCType` (or one of its
/// subclasses).
public protocol ObjectiveCBridgeable {
/// The `@objc` class (or base class) type
/// representing `Self` in Objective-C.
associatedtype ObjectiveCType : AnyObject
/// Returns `true` iff instances of `Self` can be converted to
/// Objective-C. Even if this property returns `true`, a given
/// instance of `Self.ObjectiveCType` may, or may not, convert
/// successfully to `Self`.
///
/// A default implementation returns `true`. If a Swift type is
/// generic and should only be bridged for some type arguments,
/// provide your own implementation that returns `false` for
/// non-bridged type parameters.
///
/// struct Foo<T>: ObjectiveCBridgeable {
/// static var isBridgedToObjectiveC: Bool {
/// return !(T is NonBridgedType)
/// }
/// }
///
static var isBridgedToObjectiveC: Bool { get }
/// Convert `self` to an instance of
/// `ObjectiveCType` (or one of its subclasses)
@warn_unused_result
func bridgeToObjectiveC() -> ObjectiveCType
/// Attempt to construct a value of the `Self` type from
/// an Objective-C object of the bridged class type
///
/// This bridging initializer is used for bridging when
/// interoperating with Objective-C code, either in the body of an
/// Objective-C thunk or when calling Objective-C code.
///
/// - note: This initializer should eagerly perform the
/// conversion without defering any work for later,
/// returning `nil` if the conversion fails.
init?(bridgedFromObjectiveC: ObjectiveCType)
/// Bridge from an Objective-C object of the bridged class type to a
/// value of the Self type.
///
/// This bridging initializer is used for unconditional bridging when
/// interoperating with Objective-C code, either in the body of an
/// Objective-C thunk or when calling Objective-C code, and may
/// defer complete checking until later. For example, when bridging
/// from `NSArray` to `Array<Element>`, we can defer the checking
/// for the individual elements of the array.
///
/// - parameter unconditionallyBridgedFromObjectiveC:
/// The Objective-C object from which we are bridging.
/// This optional value will only be `nil` in cases where
/// an Objective-C method has returned a `nil` despite being marked
/// as `_Nonnull`/`nonnull`. In most such cases, bridging will
/// generally force the value immediately. However, this gives
/// bridging the flexibility to substitute a default value to cope
/// with historical decisions, e.g., an existing Objective-C method
/// that returns `nil` for an "empty result" rather than (say) an
/// empty array. In such cases, when `nil` does occur, the
/// implementation of `Swift.Array`'s conformance to
/// `ObjectiveCBridgeable` will produce an empty array rather than
/// dynamically failing.
///
/// A default implementation calls `init?(bridgedFromObjectiveC:)`
/// and aborts if the conversion fails.
init(unconditionallyBridgedFromObjectiveC: ObjectiveCType?)
}
public extension ObjectiveCBridgeable {
static var isBridgedToObjectiveC: Bool { return true }
init(unconditionallyBridgedFromObjectiveC source: ObjectiveCType?) {
self.init(bridgedFromObjectiveC: source!)!
}
}
- Expose the protocol
ObjectiveCBridgeable
. This protocol will replace the old private protocol_ObjectiveCBridgeable
. - When generating an Objective-C interface for an
@objc
class type:- When a function contains parameters or return types that are
@nonobjc
but those types adoptObjectiveCBridgeable
:- Create
@objc
thunks that call the Swift functions but substitute the correspondingObjectiveCType
. - The thunks will call the appropriate protocol functions to perform the conversion.
- Create
- If any
@nonobjc
types do not adoptObjectiveCBridgeable
, the function itself is not exposed to Objective-C (current behavior).
- When a function contains parameters or return types that are
- Swift Standard library types like
String
,Array
,Dictionary
, andSet
will adopt the new protocol, thus demoting their bridging behavior from magic to regular behavior. - A clang attribute will be provided to indicate which Swift type bridges to an Objective-C class. A convenience
SWIFT_BRIDGED()
macro will be provided.- If the
ObjectiveCType
is defined in Objective-C, the programmer should annotate the@interface
declaration withSWIFT_BRIDGED("SwiftTypeName")
. - If the
ObjectiveCType
is defined in Swift, it must be an@objc
class type. The compiler will annotate the generated bridging header withSWIFT_BRIDGED()
automatically.
- If the
- The Swift type and
ObjectiveCType
must be defined in the same module. If theObjectiveCType
is defined in Objective-C then it must come from the same-named Objective-C module.
The compiler generates automatic thunks only when there is no ambiguity, while explicit casts and bridged collection types always use the protocol implementation.
- For
ObjectiveCType
s defined in Objective-C:- Omitting the
SWIFT_BRIDGED()
attribute causes Objective-C APIs using theObjectiveCType
to import without automatic bridging. - There are no implicit conversions from the Swift type to its Objective-C bridged type; if an Objective-C parameter imports as
SomeObjectiveCType
, attempting to passFoo<T>
is an error. However a user may explicitly cast to anObjectiveCBridgeable
Swift type (eg:funcTakingObjCObject(Foo<T> as! SomeObjectiveCType
). - Bridged collection types will still observe the protocol conformance if cast to a Swift type (eg:
NSArray as? [Int]
will call theObjectiveCBridgeable
implementation onArray
, which itself will call the implementation onInt
for the elements)
- Omitting the
- A Swift type may bridge to an Objective-C base class then provide different subclass instances at runtime, but no other Swift type may bridge to that base class or any of its subclasses.
- The compiler should emit a diagnostic when it detects two Swift types attempting to bridge to the same
ObjectiveCType
.
- The compiler should emit a diagnostic when it detects two Swift types attempting to bridge to the same
- An exception to these rules exists for trivially convertable built-in types like
NSInteger
<-->Int
when specified outside of a bridged collection type. In those cases the compiler will continue the existing behavior, bypassing theObjectiveCBridgeable
protocol. The effect is that types likeInt
will not bridge toNSNumber
unless contained inside a collection type (seeBuiltInBridgeable below
).
Adding or removing conformance to ObjectiveCBridgeable
, or changing the ObjectiveCType
is a fragile (breaking) change.
Here is an enum with associated values that adopts the protocol and bridges by converting itself into an object representation.
Note: The ways you can represent the type in Objective-C are endless; I’d prefer not to bikeshed that particular bit :) The Objective-C type is merely one representation you could choose to allow getting and setting the enum's associated values
enum Fizzer {
case Case1(String)
case Case2(Int, Int)
}
extension Fizzer: ObjectiveCBridgeable {
func bridgeToObjectiveC() -> ObjCFizzer {
let bridge = ObjCFizzer()
switch self {
case let .Case1(x):
bridge._case1 = x
case let .Case2(x, y):
bridge._case2 = (x, y)
}
return bridge
}
init?(bridgedFromObjectiveC source: ObjCFizzer) {
if let stringValue = source._case1 {
self = Fizzer.Case1(stringValue)
} else if let tupleValue = source._case2 {
self = Fizzer.Case2(tupleValue.0, tupleValue.1)
} else {
return nil
}
}
}
class ObjCFizzer: NSObject {
private var _case1: String?
private var _case2: (Int, Int)?
var fizzyString: String? { return _case1 }
var fizzyX: Int? { return _case2?.0 }
var fizzyY: Int? { return _case2?.1 }
func setTupleCase(x: Int, y: Int) {
_case1 = nil
_case2 = (x, y)
}
func setStringCase(string: String) {
_case1 = string
_case2 = nil
}
}
None. There are no breaking changes and adoption is opt-in.
The main alternative, as stated above, is not to adopt Swift features that cannot be expressed in Objective-C.
The less feasible alternative is to provide bridging manually by segmenting methods and properties into @objc
and @nonobjc
variants, then manually converting at all the touch points. In practice I don't expect this to be very common due to the painful overhead it imposes. Developers are much more likely to avoid using Swift features (even subconsciously).
The idea of allowing multiple Swift types to bridge to the same Objective-C type was discussed. It should technically be possible since the same-module rule wouldn't allow the shape of an imported API to vary based on which modules you imported. The compiler could skip generating the thunks in this case and the user would need to provide @objc
equivalent members that performed casting to resolve the ambiguity.
However there doesn't appear to be a convincing case to support such complex behavior so in the interests of simplicity this proposal keeps this kind of ambiguity an error. It can always be relaxed in the future if desired.
On the mailing list the idea of a protocol to supersede ObjectiveCBridgeable
for built-in types like Int
was brought up but not considered essential for this proposal. (The protocol would be decorative only, not having any functions or properties).
These types are special because they bridge differently inside collections vs outside. The compiler already has magic knowledge of these types and I don't anticipate the list of types will ever get any longer. The only benefit of a BuiltInBridgeable
protocol would be to explicitly declare which types have this "magic".
This can always be implemented in the future if it is desired.
It is intended that when and if Swift 3 adopts conditional protocol conformance that the standard library types such as Array
and Dictionary
will declare conditional conformance to ObjectiveCBridgeable
if their element types are ObjectiveCBridgeable
(with explicitly declared conformance for built-ins like Int
).
On April 12, 2016, the core team decided to defer this proposal from
Swift 3. We agree that it would be valuable to give library authors the
ability to bridge their own types from Objective-C into Swift using the
same mechanisms as Foundation. However, we lack the confidence and
implementation experience to commit to _ObjectiveCBridgeable
in its
current form as public API. In its current form, as its name suggests, the
protocol was designed to accommodate the specific needs of bridging
Objective-C object types to Swift value types. In the future, we may
want to bridge with other platforms, including C++ value types or
other object systems such as COM, GObject, JVM, or CLR. It isn't clear at
this point whether these would be served by a generalization of the existing
mechanism, or by bespoke bridging protocols tailored to each case. This
is a valuable area to explore, but we feel that it is too early at this
point to accept our current design as public API.