Swift 方法桥接到 ObjC 时报 type cannot be represented
工程里 Swift 和 Objective-C 混编已经是常态,经常遇到 Objective-C 代码去调用 Swift 里的方法。最新的 Swift 需要手动标注 @objc 才能把方法暴露。(印象中似乎在 Swift 2.x 的时代,是默认暴露)
但经常会遇到 method cannot be marked @objc because the type of the parameter cannot be represented in objective-c 类似的错误,直觉为参数、返回值,无法从 Swift 桥接到 Objective-C,有时候一眼就知道缺了什么,有时候则要思考半天,这里简单整理一下几种常见的情况。
环境
Xcode 15.2
Swift 5.9
Swift 结构体(Struct)
众所周知,Swift 结构体和 Objective-C 的结构体是浑然不同的事物。 Swift 倾向于值类型,会更多使用 Swift 结构体,但需要暴露给 Objective-C 的时候,就不太方便了。但基础库里内置的结构体,比如 CGPoint 等,苹果帮我们做了桥接,是可以的。比如:
1 | struct ST { |
苹果官方针对 Swift 中选择 Class 还是 Structure 有一段描述,见 Choosing Between Structures and Classes,其中一段节选:
Use Classes When You Need Objective-C Interoperability
If you use an Objective-C API that needs to process your data, or you need to fit your data model into an existing class hierarchy defined in an Objective-C framework, you might need to use classes and class inheritance to model your data. For example, many Objective-C frameworks expose classes that you are expected to subclass.
当您需要 Objective-C 互操作性时使用类(Class)
如果您使用需要处理数据的 Objective-C API,或者需要将数据模型放入 Objective-C 框架中定义的现有类层次结构中,则可能需要使用类和类继承来对数据进行建模。例如,许多 Objective-C 框架公开了您希望子类化的类。
所以,遇到需要暴露给 Objective-C 的场景时,还是选择 Class 吧,或者用 Class 做一层封装。
Swift 枚举型(Enum)
Swift 的枚举型很强大,除了能是 Int、String 之外,还能带参数。如果用了高级的特性,也是没法暴露给 Objective-C。如果想暴露给 Objective-C,需要满足 Int 类型且补充 @objc 前缀,当然如果非 Int 类型 @objc 也加不上。
同时也正因为这个条件,此时带参枚举就无法使用了。
1 | // @objc enum MyEnum: String // ❌ not ok,'@objc' enum raw type 'String' is not an integer type |
Swift 基础数据类型(Int、Bool) + 可选类型(Optional)
Swift 的可选类型(Optional),有时候也会影响暴露给 Objective-C,对于继承自 NSObject 的 引用类型,加上可选类型是没问题的,毕竟 Objective-C 也有 nil 来承接,但基础值类型,如 Int、Bool 就没法将对应的可选类型 Int?、Bool? 暴露给 Objective-C 了。
此时可以考虑去除可选类型,或者改用 NSNumber 类。根据是否是可选类型,Objective-C 侧展现的则是分别带有 _Nonnull 和 _Nullable 标识的入参。虽然如果对不上,默认是 warning,不过建议还是开启 error,并严格按照标识去处理,这同时也教育我们,Objective-C 侧对于是否可以为 nil 的标识也要做到严谨规范,才能更好的和 Swift 交互。
1 | @objc class MyClass: NSObject { |
inout
Swift 里提供了 inout 标识,对入参的修改能够返回给调用方,如果带了 inout 则不能暴露给 Objective-C,可选的替代方案是使用 UnsafeMutablePointer。当然自己封装成一个对象类型也是可以的。
1 | @objc class MyClass: NSObject { |
数组 Array
上文提到 Int 会被桥接成 NSInteger,但需要注意的是 [Int] 会被桥接成 NSArray<NSNumber *>,使用的时候务必注意。
但能够被苹果桥接的都是苹果提供的基础类型,如果是自定义的 Swift 的 Struct 则不行。
1 | struct ST { |
async
题外话,如果用到了 Swift 异步编程里的 async 等特性,其实也是可以正常暴露给 Objective-C 的,只不过会附带一个回调的 handler,比如:
1 | @objc class MyClass: NSObject { |
小结
总体来说,Swift 自身复杂的语法特性,导致部分无法暴露给 Objective-C。此时遇到类似 method cannot be marked @objc because the type of the parameter cannot be represented in objective-c 的报错,可以先排查下,是否是 Swift 特有的特性,比如 Swift 结构体、枚举型,并尝试补充 @objc 等标识。如果遇到困境,可以思考下,如果能暴露给 Objective-C,在 Objective-C 那边应该是个什么形式,如果无法得出合理的结论,那应该就是问题所在了。
这里仅列出了常见部分情况,其他情况如果想到了再补充进来。