Swift 与 Objective-C 混编

8,942 阅读15分钟
原文链接: www.scyano.me
原文: Using Swift with Cocoa and Objective-C - Mix and Match

在同一个项目中使用 Swift 和 Objective-C

Swift 与 Objective-C 的兼容性允许你创建一个包含有其中任一语言所写的文件组成的项目. 你可以使用这个特性, 它叫混搭(混编)(mix and match), 来编写一个混合语言的代码库. 使用混编, 你可以用最新的 Swift 特性来实现 App 的新功能, 并且无缝衔接到已经存在的 Objective-C 代码库.

混编简介

Objective-C 和 Swift 文件可以在一个项目中共存, 无论该项目是一个 Objective-C 原始项目还是 Swift 原始项目. 你可以在已经存在的项目中直接添加另外一种语言的文件. 这个自然工作流 (natural workflow) 使得我们创建一个混合语言的 App 及 Framework 和创建一个单语言 App 或 Framework 一样简单.

开发混合语言 targets 的方法, 根据你所写项目的原始语言有些许的区别. 在同一个 target 中使用两种语言的一般的导入模式会在下面描述, 在下面的章节中也会有更详细的讲解.

Mix and Match Diagram

从同一个 App Target 中导入代码

如果你正在编写一个混合语言的 app, 你也许需要在 Swfit 中访问你的 Objective-C 代码, 或在 Objective-C 中访问你的 Swift 代码. 这个章节讲的方法适用于非框架 target (non-framework targets).

将 Objective-C 导入 Swfit

将同一个 target 中的一组 Objective-C 文件导入你的 Swift 中, 你需要依赖于一个 Objective-C 桥接头文件 (bridging header file) 在 Swift 中暴露这些文件. 在你添加一个 Swift 文件到已经存在的 Objective-C App 或添加一个Objective-C 文件到一个已经存在的 Swift App 中时, Xcode 会主动提示创建这些头文件.

Bridging Header Alert

如果你确认创建, Xcode 会和你正在创建的新文件一起, 额外创建一个头文件, 并且命名为"<your product module>-Bridging-Header.h".(稍后你将会了解更多关于 product module 的内容)

或者, 你可以通过 choosingFile > New > File > (iOS, macOS) > Source > HeaderFile 来手动创建一个桥接文件.

你需要去编辑桥接文件来将你的 Objective-C 代码暴露到 Swift 中.

从同一个 target 中将 Objective-C 代码导入到 Swift
  1. 在你的 Objective-C 桥接头文件中, 导入每一个你想暴露给 Swift 的头文件, 举个例子:
    #import "XYZCustomCell.h"
    #import "XYZCustomView.h"
    #import "XYZCustomViewController.h"
    
  2. build Settings > Swift Compiler-Code Generation 中, 确认在 Objective-C Bridging Header 选项中设置了桥接文件的地址.

    这个地址应该关联于 project, 这和你在 Build Settings 中的 info.plist 地址配置是类似的. 大部分情况下, 你应该都不需要去修改此配置.

在这个桥接头文件中公开的 Objective-C 头文件将会在 Swift 中可见. 这些 Objective-C 功能不需要任何导入语句, 就可以自动被该项目中的所有 Swift 文件访问. 你可以和使用系统类型一样, 通过 Swift 语法去使用你自定义的 Objective-C 代码.

let myCell = XYZCustomCell()
myCell.subtitle = "A custom Cell"
将 Swift 导入 Objective-C

当你将 Swift 代码导入 Objective-C 中时, 你需要依赖一个Xcode 生成的头文件 (Xcode-generated header file) 去将那些文件暴露给 Objective-C. 这些自动生成的文件是一个 Objective-C 头文件, 它定义了 Swift 中的接口. 这可以被想象成一个Swift 代码的 umbrella header. 这个头文件的名字是"<product module>-Swift.h".(稍后你将会了解更多关于 product module 的内容)

默认情况下, 这个生成文件包含了 Swift 定义的被 publicopen 修饰符所标记的接口声明. 如果你的 App Target 拥有一个 Objective-C 桥接头文件, 生成文件也包含被 internal 所修饰的声明. 被 private 和 fileprivate 修饰符标记的的声明不会出现在生成文件里.private 声明不会暴露给 Objective-C, 除非他们显式的标记为 @IBAction, @IBOutlet, 或者 @objc. 如果你的 App Target 是编译为可测试的, 在一个单元测试 target 中, 你可以通过在模块导入语句前添加 @testable 来访问 internal 修饰的声明, 就像访问 public 修饰的声明一样.

@tesetable import Module

关于访问权限修饰符, 参考Access Control 中的 The Swift Programming Language (Swift 4.1).

你不需要特意去做任何事情来创建这个自动生成的头文件, 只需要导入它, 然后在 Objective-C 代码中使用其中的功能. 注意, 生成文件中的 Swift 接口, 包含它使用的所有 Objective-C 类型的引用. 如果你在 Swift 代码中使用你自己的 Objective-C 类型, 在你导入 Swift 生成文件到你的 .m 文件之前, 确保你已经导入了这些类型的 Objective-C 头文件.

在一个 target 内, 将 Swift 代码导入 Objective-C
  • 在同一个 target 内, 将 Swift 代码导入 Objective-C .m 文件内使用如下语法, 并替换成合适的名称:
    #import "ProductModuleName-Swift.h"
    

导入后, target 内的 Swift 文件将可在 Objective-C .m 文件内访问. 关于在 Objective-C 内使用 Swift, 见下表.

Import into Swift Import Into Objective-C
Swift - #import "ProductModuleName-Swift.h"
Objective-C Objective-C 桥接文件 #import "Header.h"

从同一个 Framework Target 中导入代码

如果你编写一个混合语言的 framework, 你可能需要在 Swift 和 Objective-C 内互相访问.

将 Objective-C 导入 Swift

在一个 framework 中, 将一组 Objective-C 文件导入你的 Swift 代码, 你需要将这些文件导入到 framework 的 Objective-C umbrella header 中.

在同一个 framework 内, 将 Objective-C 代码导入到 Swift 中
  1. 在 framework target 内, 确保 Build Settings -> Packaging -> Defines Module 设置为 Yes.

  2. 在你的 umbrella header 内, 导入所有你想暴露给 Swift 的 Objective-C 头文件 (注, 在 Build Phases > Headers > Public 中添加这些文件). 举个例子:

    #import <XYZ/XYZCustomCell.h>
    #import <XYZ/XYZCustomView.h>
    #import <XYZ/XYZCustomViewController.h>
    

你在 umbrella header 内公开地暴露的头文件将会在 Swift 内可见. framework 中的 Objective-C 内容不需要任何导入语句, 将自动在所有 Swift 文件内可用. 和你使用系统类型一样, 使用相同的 Swift 语法来使用你自定义的 Objective-C 代码.

let myOtherCell = XYZCustomCell()
myOtherCell.subtitle = "Another custom cell"
将 Swift 导入 Objective-C

在同一个 framework target 内, 将一组 Swift 文件导入作为 Objective-C 代码, 你不需要在 framework 的 umbrella header 内导入任何东西. 相反的, 导入 Xcode 自动生成的头文件到你想使用 Swift 代码的 Objective-C .m 文件内.

因为这个自动生成的头文件是这个 framework 的公开接口 (public interface) 的一部分, 只有被 public 或者 open 修饰符标记的声明才会出现在生成文件中.

internal 修饰符标记, 并且继承于NSObject 类的 Swift 方法和属性, 在 Objective-C 运行时是可以被访问到的. 然而, 他们在编译时将不可被访问, 也不会出现在 framwork 内的生成文件里.

更多访问权限内容, 查看 Access Control in The Swift Programming Language (Swift 4.1).

在同一个 framework 内, 将 Swift 导入 Objective-C

  1. 在 framework target 内, 确保 Build Settings -> Packaging -> Defines Module 设置为 Yes.

  2. 将 Swift 代码导入到 Objective-C .m 文件内, 使用下面的语法, 并替换成合适的名字:

    #import <ProductName/ProductModuleName-Swift.h>
    

包含这个导入语句, 你的 Swift 文件内容将对 Objective-C .m 文件可见. 关于在 Objective-C 中使用 Swift, 见下表:

Import into Swift Import Into Objective-C
Swift - #import <ProductName/ProductModuleName-Swift.h>
Objective-C Objective-C umbrella header #import "header.h"
导入外部的 Frameworks

你可以导入外部的纯 Objective-C frameworks, 纯 Swift framworks, 或者一个混合语言的framework. 不论 framework 由一种还是两种语言编写, 导入这个外部 framework 的方法是一样的. 当你导入一个外部 framework, 确保你所导入的 framework的 Defines Module 设置为Yes.

你可以使用下面的语法将 framework 导入到不同 target 的任何 Swift 文件中:

import FrameworkName

你可以使用下面的语法将一个 framework 导入到不同 target 的任何 Objective-C.m 文件中:

@import FrameworkName;
Import into Swift Import into Objective-C
Any language framework import FrameworkName @import FrameworkName;
在 Objective-C 中使用 Swift

一旦你将 Swift 代码导入到 Objective-C 中, 使用常规的 Objective-C 语法来使用 Swift 类.

MySwiftClass *swiftObject = [[MySwiftClass alloc] init];
[swiftObject swiftMethod];

一个在 Objective-C 中可访问及可用的 Swift 类必须是一个 Objective-C 类的子类. 更多关于在 Objective-C 中你可以访问什么以及 Swift 接口是如何被导入的, 查看 Swift type Comopatibility

在一个 Objective-C 头文件中引用一个 Swift 类或协议

当你的代码从其他模块引用了 Swift 类或协议, 使用 @import 将Swift 模块导入到你的 Objective-C 内. 然而, 为了防止循环引用, 不要从同一个模块内将 Swift 代码导入到一个 Objective-C .h 文件中. 相反的, 在 Objective-C接口中, 你可以前置声明一个 Swift 类或协议去引用它.

// MyobjcClass.h
@class MySwiftClass;
@protocol MySwiftProtocol;

@interface MyObjcClass: NSObject
- (MySwiftClass *)returnSwiftClassInstance;
- (id<MySswiftProtocol>)returnInstanceAdoptingSwiftProtocol;
// ...
@end

前置声明 Swift 类和协议仅仅可以被用来作为方法或属性的类型声明.

声明一个可以被 Objective-C 类所接受的 Swift 协议

声明一个可被 Objective-C 类所接受的 Swift 协议, 需要使用 @objc 属性来标记协议声明.

@objc public protocol MySwiftProtocol {
  func requiredMethod()
  @objc optional func optionalMethod()
}

遵循协议的 Objective-C 类必须实现协议中所有初始化方法, 属性, 下标方法 ( subscripts) 和方法. 任何可选的协议必须被 @objc 所标记, 并且由 optional 修饰符修饰.

如果你需要在 Objective-C 中去声明一个 weak 属性来满足 Swift 协议要求, 那么将 Swift 协议中的这个属性使用 weak 修饰符标记. 请注意, 应用 weak 修饰符对于遵循这个协议的 Swift 类型将不会有任何的影响.

遵循 Swift 协议的 Objective-C 实现.

一个 Objective-C类可以通过导入 Xcode 为 Swift 代码自动生成的文件而在implementation 中实现一个 Swift 协议.

// MyObjcClass.m
#import "ProductModuleName-Swift.h"
@interface MyObjcClass () <MySwiftProtocol>
// ...
@end

@implementation MyObjcClass
// ...
@end

声明一个可以被 Objective-C 使用的 Swift 错误类型

遵循于 Error 协议, rawValueInt, 并且被 @objc 属性声明的 Swift 枚举, 将在 Objective-C 中产生一个 NS_ENUM 声明, 同样也会在生成文件中产生一个对应的 NSString 类型的 error domain 常量. 例如, 给出下面的 Swift 枚举声明:

@objc public enum CustomError: Int, Error {
case a, b, c
}

这里是在生成文件中对应的 Objective-C 声明:

// Project-Swift.h
typedef SWIFT_ENUM(NSInteger, CustomError) {
CustomErrorA = 0,
CustomErrorB = 1,
CustomErrorC = 2,
};
static NSString * const CustomErrorDomain = @"Project.CustomError";

重写 Objective-C 接口的 Swift 命名

Swift 编译器自动导入 Objective-C 代码作为常规的 Swift 代码. 它导入 Objective-C 类工厂方法作为 Swift 初始化方法, 并导入截断了名称的 Objective-C 枚举值.

也许有边界情况没有被自动处理. 如果你需要去修改导入到 Swift 中的 Objective-C 方法名称, 枚举值, 或可选值, 你可以使用 NS_SWIFT_NAME 宏来自定义被导入的名称.

类工厂方法

如果 Swfit 编辑器识别一个类工厂方法失败了, 你可以使用 NS_SWIFT_NAME 宏, 传递这个初始化方法的 Swift 签名让它正确导入. 例如:

+ (instancetype)recordWithRPM:(NSUInteger)RPM NS_SWIFT_NAME(init(rpm:));

如果 Swift 编译器错误地识别了一个方法来作为类工厂方法, 你可以使用 NS_SWIFT_NAME 宏, 传递这个方法的 Swift 签名来让它正确导入. 例如:

+ (id)recordWithQuality:(double)quality NS_SWIFT_NAME(record(quality:));
枚举

默认情况下, Swift 通过截断枚举值的命名前缀来导入枚举. 你可以使用NS_SWIFT_NAME 宏, 来自定义传递到 Swfit 中枚举值的名称, 例如:

typedef NS_ENUM(NSInteger, ABCRecordSide) {
ABCRecordSideA,
ABCRecordSideB NS_SWIFT_NAME(flipSide),
};
改善 Objective-C 声明

当保持原本的实现可被优化的接口 (refined interface) 访问, 你可以在一个 Objective-C 方法声明中使用 NS_REFINED_FOR_SWIFT 宏来提供一个优化的在 Swift 中的接口. 例如, 一个包含有一个或多个指针类型参数的 Objective-C 方法, 在 Swift 中可以被优化为一个元祖.

  • 被导入到 Swift 中的初始化方法, 将会在他们的第一个参数标签之前添加双下划线(__).
  • 被导入到 Swift 中的下标方法, 将会在他们的基础命名之前添加双下划线, 而不是作为 Swift 下标方法, 如果他们的 gettersetter 方法中的任何一个被标记为 NS_REFINED_FOR_SWIFT.
  • 其他方法被导入时, 将会在他们的基础命名之前添加双下划线.

有如下的 Objectivc-C 声明:

@interface Color : NSObject
- (void)getRed:(nullable CGFloat *)red
green:(nullable CGFloat *)green
blue:(nullable CGFloat *)blue
alpha:(nullable CGFloat *)alpha NS_REFINED_FOR_SWIFT;
@end

你可以在扩展中提供一个优化的 Swift 接口, 像这样:

extension Color {
var RGBA: (red: CGFloat, green: CGFloat, blue: CGFloat, alpha: CGFloat) {
var r: CGFloat = 0.0
var g: CGFloat = 0.0
var b: CGFloat = 0.0
var a: CGFloat = 0.0
__getRed(red: &r, green: &g, blue: &b, alpha: &a)
// 返回元祖
return (red: r, green: g, blue: b, alpha: a)
}
}
让 Objective-C 接口在 Swift 中不可用

一些 Objective-C 接口可能并不适用或必须去暴露为 Swift 接口. 为了防止一个 Objective-C 声明被 Swift 导入, 使用 NS_SWIFT_UNAVALIABLE 宏, 传递一个消息去重定向到任何已经存在的可替代 API.

例如, 一个 Objective-C 类提供了一个使用键值对的便利构造器, 可能会建议一个 Swift 用户去使用字典字面量代替:

+ (instancetype)collectionWithValues:(NSArray *)values forKeys:(NSArray<NSCopying> *)keys NS_SWIFT_UNAVAILABLE("Use a dictionary literal instead");

在 Swift 代码中尝试去调用 +collectionWithValues:forKeys: 方法将会导致一个编译时错误.

让一个 Objective-C 声明在编译时对于 Objective-C 和 Swift 都不可用, 可以使用 NS_UNAVAILABLE 宏. 这个宏用起来就像 NS_SWIFT_UNAVAILABLE 一样, 只是这个宏会忽略自定义的错误消息, 另外它在编译时也限制了 Objective-C 访问其声明.

为 Objective-C APIs 添加可用信息

在 Swift 中, 你可以使用 @available 属性来控制一个声明是否可以被一个指定的目标平台使用. 类似的, 你可以使用可用条件 #available 来基于所需的平台和版本条件去执行代码.

两种类型的可用描述在 Objective-C 中都有对应的语法, 见下文.

这个例子呈现了在 Swift 中用于声明的可用信息:

@available(iOS 11, macOS 10.13, *)
func newMethod() {
// Use iOS 11 APIs.
}

这里是怎么添加可用信息到 Objective-C.

@interface MyViewController : UIViewController
- (void) newMethod API_AVAILABLE(ios(11), macosx(10.13));
@end

下面的例子呈现了怎么在 Swift 的条件语句中使用可用信息.

if #available(iOS 11, *) {
// Use iOS 11 APIs.
} else {
// Alternative code for earlier versions of iOS.
}

下面的例子同样呈现了怎么在 Objective-C 中使用可用信息

if (@available(iOS 11, *)) {
// Use iOS 11 APIs.
} else {
// Alternative code for earlier versions of iOS.
}

更多关于指定平台的可用信息, 查看 Declaration Attributes in The Swift Programming Language (Swift 4.1).

给你的 Product Module 命名

Swift 代码产生的生成文件的名称, 以及 Xcode 为你创建的 Objective-C 桥接头文件的名称, 都是由你的 product module 名称而来. 默认情况下, 你的 product module 名称和你的产品名称是一样的. 然而, 如果你的产品名称有任何非首字母数字化的字符, 就像 ., 它们将会被替换为单下划线. 如果名字由数字开头, 第一个数字将会被下划线代替.

你也可以提供一个自定义的名字作为 product module 名称, 然后在命名桥接和生成头文件时, Xcode 将会使用这个名字. 你可以通过修改 Product Module Name 设置选项来实现.

注意, 你不能够重写一个 framework 的 product module 名称.

问题解决贴士和提醒

  • 将你的 Swift 和 Objective-C 代码视为一个代码集合, 并且提防他们的命名冲突.
  • 如果你正在编写 frameworks, 确保 Packaging->Defines Module(DEFINES_MODULE) 设置选项为 YES.
  • 如果你正在使用 Objective-C 桥接文件, 确保你的 Objective-C 桥接文件(Swift Compiler -> Code Generation -> SWIFT_OBJC_BRIDGING_HEADER) 设置了关联你项目的地址(例如, "MyApp/MyApp-Bridging-Header.h")
  • 当你命名 Objective-C 桥接头文件和Swift 生成头文件时, Xcode 使用的是你的 product module 名称 (PRODUCT_MODULE_NAME), 而不是你的 target 名称 (TARGET_NAME).
  • 为了让 Swift 类 在 Objective-C 内可用, 它必须是 Objective-C 类的子类, 或者被 @objc 标记.
  • 当你将 Swift 代码放入 Objective-C 中时, 记住, Objective-C 不能获取到某些Swift 独有的特性.
  • 如果你在 Swift 代码内使用了你自己的 Objective-C 类型, 在你导入 Swift 生成文件到 Objective-C .m 文件中时, 确保你导入 那些类型的 Objective-C 头文件.
  • Swift 声明标记为 private 或者 fileprivate 的将不会出现在生成头文件中. private 声明不会暴露给 Objective-C, 除非你使用 @IBAction, @IBOutlet, @objc 显式声明.
  • 对于 app targets, 如果它拥有一个 Objective-C 桥接头文件, 则被 internal修饰符标记的 Swift 声明会出现在生成文件中.
  • 对于 framework targets, 只有声明为 publicopen 的声明会出现在生成文件中. 你依然可以在 framework 的 Objective-C 部分中, 使用被标记为 internal 的 Swift 函数和属性1, 只要他们继承于 Objective-C 的类. 更多信息, 查看 Access Control

Interacing with C APIs

Migrating Your Objective-C Code to Swift

Copyright © 2018 Apple Inc. All rights reserved. Terms of Use | Privacy Policy | Updated: 2018-03-29

注释:


  1. 通过 performSelectorAccess internal Swift in Objective-C within the same framework  ↩