Bridges of Siracusa County
ObjectiveCBridgeable
We live in an imperfect world. It isn't always practical to throw out all your Objective-C code and start over. Framework authors (including Apple) are even more constrained: lots of apps aren't 100% Swift and won't ever be. Making the next version of Photos.framework only available to Swift is a non-starter.
What's a team supposed to do when they want to being adopting Swift features, yet are constrained by support for Objective-C?
In practical terms you begin to make compromises. Models are classes. Not because it's a good idea, but because they need to be exposed to Objective-C. Protocols can't use associated types for the same reason. Sometimes enums with associated values are a fantastic solution... but then you remember you need to interop with Objective-C. Even adding functions and properties to enums are a no-go. You can forget generics entirely.
When you begin stripping away a lot of Swift's best features you're left with Swift code that seems more like a line-for-line port of Objective-C code and is almost always less type safe. And slower. And more awkward to use.
Magic
The Swift team has certainly been aware of these issues and the compiler has some built-in magic to make some cases easier to handle.
A Swift String
leads a dual life: it can be a truly native Swift struct with a copy-on-write backing buffer. Or it can actually hide an instance of NSString
under the covers.
The collection types like Dictionary<Key, Value>
, Set<Value>
, and Array<Element>
have similar bridging characteristics. If you ever wondered how the compiler knows to convert an Array<Int>
to NSArray <NSNumber *>*
: it comes down to the private protocol _ObjectiveCBridgeable
. Bridged collection types will automatically invoke the protocol's conversion functions for each element (go ahead and try it - you can adopt the protocol in your own types and be part of the magic).
This magic allows a type to have completely different representations on each side of the Swift <--> Objective-C bridge. Unfortunately this magic only works inside bridged collection classes.
You could still adopt the protocol to cover collections, then provide your own bridging functions @objc(realFunctionName:) func __objectivec_dont_call_me_from_swift(param: ObjCType)
. It would be horribly tedious and error-prone but in theory it could work.
But why not just have the compiler generate these functions (aka "thunks") for us?
Meet ObjectiveCBridgeable
My first ever swift-evolution proposal has been merged and scheduled for review starting today: 0058: Allow Swift types to provide custom Objective-C representations. I'm no Erica Sadun but I'm still excited about it.
From the proposal:
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.
The idea is simple: If the compiler comes across a function or property that it can't expose to Objective-C within a type otherwise exposed to Objective-C, check the types to see if they adopt ObjectiveCBridgeable
. If they do, emit thunks that call the appropriate protocol functions to perform the conversion.
In the most common case you'll provide one instance method and one initializer. Let's say we have some hypothetical Query
enum we want to bridge:
enum Query {
case ByIdentifier(String)
case ByCriteria(Criteria)
case ByCreatedDate(NSDate)
}
extension Query: ObjectiveCBridgeable {
func bridgeToObjectiveC() -> ObjCQuery {
return ObjCQuery(self)
}
init?(bridgedFromObjectiveC: ObjCQuery) {
self = bridgedFromObjectiveC._value
}
}
@objc(Queryable)
class ObjCQuery: NSObject {
private let _value: Query
init(_ value: Query) {
self._value = value
}
}
This is an extremely simple bridge: We're just round-tripping the object inside a container. Objective-C code can't really interact with it. Let's see if we can do something more useful:
extension ObjCQuery {
var identifier: String? {
guard case let .ByIdentifier(identifier) = _value else { return nil }
return identifier
}
var criteria: Criteria? {
guard case let .ByCriteria(criteria) = _value else { return nil }
return criteria
}
var createdDate: NSDate? {
guard case let .ByCreatedDate(createdDate) = _value else { return nil }
return createdDate
}
}
That's better. Objective-C code still has to check for the various properties and it isn't as nice as a Swift enum
but it works. We can even allow construction of new instances from Objective-C if we want:
extension ObjCQuery {
convenience init(queryByIdentifier: String) {
self.init(Query.ByIdentifier(queryByIdentifier))
}
}
The Process
I'm not clever or smart for coming up with this proposal; the protocol that inspired it already existed. In a way that's why I wanted to write about it. Anyone could have done what I did, including you. There are plenty of ways you can contribute, even if you're just taking on the burden of formalizing and generalizing a tool that already exists.
The Swift core team only has so many hours in the day. I also only have so many hours in mine, but I can take a small slice to write the proposal. A contribution to the community is a contribution, no matter how small.
If you don't know where to start, just pick a real pain point you're experiencing and see if you can think of a solution. It doesn't have to be a grand Higher Kinded Types proposal. If you'd rather contribute code but like me have no idea where to start, you can look at the list of Starter Bugs. LLVM has a nice tutorial for implementing a toy language that is fun and helpful.
If that seems like a too much work you can just help triage bugs. It's usually a small bite-sized effort. Just download any test project provided or look at the description then narrow it down to the smallest test case you can get and add that to the bug, along with a comment that you've verified the issue (or alternatively that you couldn't reproduce it).
Your Input
Part of the swift-evolution process is taking feedback. If you think the Swift core team isn't taking the process seriously, you're wrong. Property behaviors were proposed by Joe Groff (Swift core team member at Apple) but as a result of review got sent back for another round of changes. Swift isn't Apple (or Chris Lattner) dictating from on-high, nor is it Apple lobbing source over the wall. It truly is a community effort.
If you have ideas for improving the ObjectiveCBridgeable
proposal, it isn't too late. Join the swift-evolution mailing list, then reply to the review announcement with your thoughts. Unfortunately since it's a mailing list you may have missed the announcement message. If so you can find the announcement in the archives then create a new message replying to the same subject line, or email the review manager yourself (none other than Joe Groff).
This blog represents my own personal opinion and is not endorsed by my employer.