Refine Objective-C frameworks for Swift
Swift & UI 进阶 16m

优化 Objective-C 框架以适配 Swift

Refine Objective-C frameworks for Swift

2020年6月25日

在 Apple 官方观看视频

一句话判断

如果你的 Objective-C 框架被 Swift 项目使用,NS_REFINED_FOR_SWIFTNS_SWIFT_NAME、nullability 注解这些修饰符不是可选的——它们决定了你的 API 在 Swift 端看起来是「原生」还是「缝合怪」。

这场 Session 讲了什么

这场 Session 专注于如何让 Objective-C 框架在 Swift 中提供更好的调用体验。当 Swift 代码调用 Objective-C API 时,编译器会自动做一层桥接,但默认的桥接结果往往不够 Swift-idiomatic。

核心内容包括:

  • NS_SWIFT_NAME:重命名 API,让它符合 Swift 的命名规范
  • NS_REFINED_FOR_SWIFT:隐藏原始 API,用 Swift extension 提供更好的接口
  • Nullability 注解(nullablenonnull_Null_unspecified):控制可选类型
  • NS_ERROR_ENUMNS_SWIFT_NOTHROW:控制错误处理方式
  • Generics(轻量级泛型):让集合类型在 Swift 中有具体类型而非 [Any]

Session 还介绍了 Xcode 12 中新增的 Swift Interface 生成工具,可以预览你的 Objective-C API 在 Swift 端的实际呈现效果。

值得深挖的点

NS_REFINED_FOR_SWIFT 的使用场景

有些 Objective-C API 的设计模式在 Swift 中有更好的替代方案。比如一个返回 NSError** 的方法,在 Swift 中应该变成 throws。用 NS_REFINED_FOR_SWIFT 隐藏原始 API(名字前加 __ 前缀),然后在 Swift extension 中提供一个原生风格的包装。这比直接暴露 Objective-C API 优雅得多。

方法签名的 Swift 化

Objective-C 的 initWithFrame: 在 Swift 中自动变成 init(frame:)。但如果你有一个 downloadFileAtPath:withCompletion: 方法,Swift 会看到 downloadFile(atPath:withCompletion:)。用 NS_SWIFT_NAME(downloadFile(at:completion:) 可以让参数名更简洁。

代码片段

// Objective-C 头文件 —— 优化前
@interface FileDownloader : NSObject

// 默认桥接后 Swift 端看到:
// downloadFile(atPath: String!, withCompletion: ((Data?, Error?) -> Void)!)
- (void)downloadFileAtPath:(NSString *)path 
            withCompletion:(void (^)(NSData * _Nullable, NSError * _Nullable))completion;

// 所有可能为 nil 的参数都是隐式解包的 Optional(!),很危险
- (nullable NSString *)fileExtensionForPath:(NSString *)path;

@end
// Objective-C 头文件 —— 优化后
NS_ASSUME_NONNULL_BEGIN

@interface FileDownloader : NSObject

// NS_SWIFT_NAME:重命名让 Swift 端更自然
- (void)downloadFileAtPath:(NSString *)path 
            withCompletion:(void (^)(NSData * _Nullable, NSError * _Nullable))completion
    NS_SWIFT_NAME(downloadFile(at:completion:));

// NS_REFINED_FOR_SWIFT:隐藏原始 API,在 extension 中提供 Swift 原生版本
- (BOOL)writeData:(NSData *)data toPath:(NSString *)path error:(NSError **)error
    NS_REFINED_FOR_SWIFT;

// Nullability 注解:明确参数和返回值是否可为 nil
- (nullable NSString *)fileExtensionForPath:(NSString *)path;

// NS_ERROR_ENUM:定义 Swift 端的错误类型
typedef NS_ERROR_ENUM(FileDownloaderErrorDomain, FileDownloaderError, NSInteger) {
    FileDownloaderErrorNetworkUnavailable = 1001,
    FileDownloaderErrorFileNotFound = 1002,
    FileDownloaderErrorDiskFull = 1003,
};

@end

NS_ASSUME_NONNULL_END
// Swift Extension —— 为 refined API 提供 Swift 原生接口
extension FileDownloader {
    
    // 包装 __writeData 方法的 throws 版本
    func writeData(_ data: Data, to path: String) throws {
        var error: NSError?
        let success = __writeData(data, toPath: path, error: &error)
        if !success {
            throw error ?? FileDownloaderError(.networkUnavailable)
        }
    }
    
    // 提供 Async/Await 风格的下载方法
    func downloadFile(at path: String) async throws -> Data {
        try await withCheckedThrowingContinuation { continuation in
            downloadFile(at: path) { data, error in
                if let error = error {
                    continuation.resume(throwing: error)
                } else if let data = data {
                    continuation.resume(returning: data)
                }
            }
        }
    }
}

最佳实践

  1. NS_ASSUME_NONNULL_BEGIN/END 包裹整个头文件。大部分参数和返回值不应该是 nil,用 nonnull 作为默认,只在少数允许 nil 的地方标记 nullable。这样 Swift 端大部分类型都是非可选的,使用更安全。
  2. 给集合类型加泛型注解NSArray<NSString *> * 在 Swift 中会被桥接为 [String],而 NSArray * 只会变成 [Any]。后者需要频繁类型转换,使用体验很差。
  3. NS_SWIFT_NAME 调整枚举值名称。Objective-C 枚举值通常带前缀(FileDownloaderErrorNetworkUnavailable),在 Swift 中用 NS_SWIFT_NAME(NetworkUnavailable) 去掉冗余前缀。

还有什么值得关注

  • Xcode 12 的 swift-ide-test 命令行工具可以生成 Objective-C API 的 Swift 接口预览,方便你查看 Swift 端的实际呈现。
  • CF_SWIFT_NAME 用于 Core Foundation 类型的 Swift 命名,语法与 NS_SWIFT_NAME 相同。
  • 如果你的框架是用模块化(Modules)方式构建的,确保 module.modulemap 文件正确配置,否则 Swift 无法正确导入。
WWDC 2020