Swift 方法桥接到 ObjC 时报 type cannot be represented

| /

工程里 SwiftObjective-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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
struct ST {
var x: Int
var y: Int
}

class MyCls: NSObject { // 🚩 NSObject is necessary, @objc is not necessary
var x: Int = 0
var y: Int = 0
}

@objc class MyClass: NSObject {
@objc func myFunc(st: ST) { // ❌ not ok
// do something
}

@objc func myFunc1(st: CGPoint) { // ✅ ok
// do something
}

@objc func myFunc2(st: MyCls) { // ✅ ok
// do something
}
}

苹果官方针对 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 的枚举型很强大,除了能是 IntString 之外,还能带参数。如果用了高级的特性,也是没法暴露给 Objective-C。如果想暴露给 Objective-C,需要满足 Int 类型且补充 @objc 前缀,当然如果非 Int 类型 @objc 也加不上。

同时也正因为这个条件,此时带参枚举就无法使用了。

1
2
3
4
5
6
7
8
9
10
11
12
13
// @objc enum MyEnum: String // ❌ not ok,'@objc' enum raw type 'String' is not an integer type
// enum MyEnum // ❌ not ok
// enum MyEnum: Int // ❌ not ok
@objc enum MyEnum: Int { // ✅ ok
case A
case B
}

@objc class MyClass: NSObject {
@objc func myFunc(st: MyEnum) {
// do something
}
}

Swift 基础数据类型(Int、Bool) + 可选类型(Optional)

Swift 的可选类型(Optional),有时候也会影响暴露给 Objective-C,对于继承自 NSObject引用类型,加上可选类型是没问题的,毕竟 Objective-C 也有 nil 来承接,但基础值类型,如 IntBool 就没法将对应的可选类型 Int?Bool? 暴露给 Objective-C 了。

此时可以考虑去除可选类型,或者改用 NSNumber 类。根据是否是可选类型,Objective-C 侧展现的则是分别带有 _Nonnull_Nullable 标识的入参。虽然如果对不上,默认是 warning,不过建议还是开启 error,并严格按照标识去处理,这同时也教育我们,Objective-C 侧对于是否可以为 nil 的标识也要做到严谨规范,才能更好的和 Swift 交互。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@objc class MyClass: NSObject {
@objc func myFunc(st: Bool) { // ✅ ok in Objective-C is BOOL
// do something
}

@objc func myFunc1(st: Int) { // ✅ ok in Objective-C is NSInteger
// do something
}

@objc func myFunc2(st: Bool?) { // ❌ not ok
// do something
}

@objc func myFunc2(st: Int?) { // ❌ not ok
// do something
}

@objc func myFunc2(st: NSNumber) { // ✅ ok in Objective-C is NSNumber * _Nonnull
// do something
}

@objc func myFunc2(st: NSNumber?) { // ✅ ok in Objective-C is NSNumber * _Nullable
// do something
}
}

inout

Swift 里提供了 inout 标识,对入参的修改能够返回给调用方,如果带了 inout 则不能暴露给 Objective-C,可选的替代方案是使用 UnsafeMutablePointer。当然自己封装成一个对象类型也是可以的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@objc class MyClass: NSObject {
@objc func myFunc(st: inout Bool) { // ❌ not ok
// do something
}


// in Objective-C is BOOL * _Nonnull
// usage:
// BOOL st = NO;
// [[MyClass alloc] init] myFuncWithSt:&st];
@objc func mFunc(st: UnsafeMutablePointer<Bool>) { // ✅ ok
// do something ...
st.pointee = true
}
}

数组 Array

上文提到 Int 会被桥接成 NSInteger,但需要注意的是 [Int] 会被桥接成 NSArray<NSNumber *>,使用的时候务必注意。

但能够被苹果桥接的都是苹果提供的基础类型,如果是自定义的 SwiftStruct 则不行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct ST {
var x: Int
var y: Int
}

@objc class MyClass: NSObject {
@objc func myFunc(st: [ST]) { // ❌ not ok
// do something
}

@objc func myFunc(st: [Int]) { // ✅ ok in Objective-C is NSArray<NSNumber *> * _Nonnull
// do something
}
}

async

题外话,如果用到了 Swift 异步编程里的 async 等特性,其实也是可以正常暴露给 Objective-C 的,只不过会附带一个回调的 handler,比如:

1
2
3
4
5
6
7
@objc class MyClass: NSObject {
// ✅ ok ObjC is
// - (void)myFuncWithSt:(NSNumber * _Nonnull)st completionHandler:(void (^ _Nonnull)(void))completionHandler;
@objc func myFunc(st: NSNumber) async {
// do something
}
}

小结

总体来说,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 那边应该是个什么形式,如果无法得出合理的结论,那应该就是问题所在了。

这里仅列出了常见部分情况,其他情况如果想到了再补充进来。