[swift-evolution] [Idea] ObjectiveCBridgeable

Russ Bishop xenadu at gmail.com
Thu Feb 25 13:02:37 CST 2016


I wanted to float a proposal and see what the community thinks. If there is interest I’m happy to submit an official proposal. This comes from real problems we’re encountering with a large mixed codebase. Every time our nice clean Swift design hits an Objective-C boundary we end up having to go back and dumb-down the Swift API significantly because manually converting at every touch point is just too painful.


# Introduction

Provide an ObjectiveCBridgeable protocol that allows a type to control how it is represented in Objective-C by converting into and back from an entirely separate type. This frees an API designer to create truly native Swift types like structs, generics, enums with associated values, protocols with associated types, etc without having to compromise to allow interop with Objective-C.



# Motivation

When working in a mixed codebase, we find ourselves unable to use generics, enums with associated values, structs, and various other Swift features because there are chunks of Objective-C code that we can’t stop and rewrite. This leads to a catch-22 situation where your new APIs (or refactored APIs) have a compromised design,  which has a cascading effect on new Swift code that uses the compromised API. 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 Swift-ish features. 



# Proposed Solution

Today, you can adopt the private protocol _ObjectiveCBridgeable and if your parameters or return types are inside an Array, Swift will automatically call the appropriate functions to let you control the way the type bridges. This allows you to define a completely different Objective-C compatible API for the type and round-trip it appropriately.

My proposal is simple: expose the existing protocol as a public protocol ObjectiveCBridgeable: _ObjectiveCBridgeable, and have the compiler generate the appropriate Objective-C bridging outside of the existing Array support.



# Detailed Design

1. Expose the existing protocol as a public protocol 
2. When generating an Objective-C interface for a type that is itself bridgeable to Objective-C:
  a. When a function contains parameters or return types that are @nonobjc but those types adopt ObjectiveCBridgeable:
    i) Create @objc overloads that call the Swift functions but substitute the corresponding ObjectiveCType.
    ii) The overloads will automatically call the appropriate protocol functions to perform the conversion.
  b. If any @nonobjc types do not adopt ObjectiveCBridgeable, the function itself is not exposed to Objective-C (current behavior).
3. It is up to the API designer to decide if they want to allow construction of wholly new values from Objective-C and how to handle convertibility back to the Swift type in that case

An example enum 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 :)

enum Fizzer: ObjectiveCBridgeable {
    case Case1(String)
    case Case2(Int, Int)

    static func getObjectiveCType() -> Any.Type {
        return ObjCFizzer.self
    }
    static func isBridgedToObjectiveC() -> Bool {
        return true
    }
    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
    }
    static func conditionallyBridgeFromObjectiveC(source: ObjCFizzer, inout result: Fizzer?) -> Bool {
        _forceBridgeFromObjectiveC(source, result: &result)
        return true
    }
    static func forceBridgeFromObjectiveC(source: ObjCFizzer, inout result: Fizzer?) {
        if let stringValue = source._case1 {
            result = Fizzer.Case1(stringValue)
        } else if let tupleValue = source._case2 {
            result = Fizzer.Case2(tupleValue)
        } else {
            fatalError("Unable to bridge")
        }
    }
}

class ObjCFizzer: NSObject {
    private var _case1: String?
    private var _case2: (Int, Int)?
}



# Impact on existing code

No breaking changes. Adoption would be opt-in.



# Alternatives considered

The only alternative, as stated above, is not to adopt Swift features that cannot be expressed in Objective-C.


# TBD / Questions:

Should the shape of the protocol change? It may not make sense for the public protocol to support both conditional and force bridging; maybe we only provide a throwing equivalent of force bridge and let the implementer throw if the conversion is invalid? 




Russ



More information about the swift-evolution mailing list