关键词:NS_REFINED_FOR_SWIFT
前言
宏 NS_REFINED_FOR_SWIFT
于 Xcode 7 引入,它可用于在 Swift 中隐藏 Objective-C API,以便在 Swift 中提供相同 API 的更好版本,同时仍然可以使用原始 Objective-C 实现。具体的应用场景有:
- 你想在 Swift 中使用某个 Objective-C API 时,使用不同的方法声明,但要使用类似的底层实现
- 你想在 Swift 中使用某个 Objective-C API 时,采用一些 Swift 的特有类型,比如元组(具体例子可以看 Example_Apple)
- 你想在 Swift 中使用某个 Objective-C API 时,重新排列、组合、重命名参数等等,以使该 API 与其它 Swift API 更匹配
- 利用 Swift 支持默认参数值的优势,来减少导入到 Swift 中的一组 Objective-C API 数量(具体例子可以看 Example_SDWebImage)
- 解决 Swift 调用 Objective-C 的 API 时可能由于数据类型等不一致导致无法达到预期的问题。例如,Objective-C 里的方法采用了 C 风格的多参数类型;或者 Objective-C 方法返回 NSNotFound,在 Swift 中期望返回 nil 等等(具体例子可以看 Example_3)
Example_Apple
这个是 Apple|Improving Objective-C API Declarations for Swift 的例子。
以下是在 Objective-C 中声明的 API:
@interface Color : NSObject
- (void)getRed:(nullable CGFloat *)red
green:(nullable CGFloat *)green
blue:(nullable CGFloat *)blue
alpha:(nullable CGFloat *)alpha;
@end
默认情况下,生成的 Swift API 是下面这样的:
open func getRed(_ red: UnsafeMutablePointer<CGFloat>?,
green: UnsafeMutablePointer<CGFloat>?,
blue: UnsafeMutablePointer<CGFloat>?,
alpha: UnsafeMutablePointer<CGFloat>?)
在 Swift 中调用该方法需要传递四个 in-out 参数,易用性较低。
let color = Color()
var r: CGFloat = 0.0
var g: CGFloat = 0.0
var b: CGFloat = 0.0
var a: CGFloat = 0.0
color.getRed(&r, green: &g, blue: &b, alpha: &a)
如果你在 Swift 中设计这样一个 API,你会如何设计呢?
可以设计一个只读计算属性,其类型是一个包含 rgba 四个元素的元组。
var rgba: (red: CGFloat, green: CGFloat, blue: CGFloat, alpha: CGFloat)
怎么样?API 更简洁更易用了,同时用到了 Swift 的特有类型 —— 元组。
接下来我们来看看,如何通过使用 NS_REFINED_FOR_SWIFT
做这样一个 API 适配。
1. 将宏 NS_REFINED_FOR_SWIFT 添加到 Objective-C API 中
首先,将 NS_REFINED_FOR_SWIFT
作为后缀添加到 Objective-C API 中,生成的 Swift API 将会以双下划线 __
开头重命名,且在 Swift 中调用时不会有代码补全提示,相当于隐藏了 API。这样可以一定程度上防止调用者意外地直接使用该 Objective-C API,而没有使用适配后的 Swift API。
这里是可以一定程度上防止而不是绝对,因为如果调用者了解该规则的话,仍然可以以
__
开头拼接 Objective-C API 名称进行调用。但既然使用了NS_REFINED_FOR_SWIFT
做 API 适配,就表明该方法不应该原样使用,遵守规范吧!
@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
2. 在 Swift 中添加适配 API
在 Swift 中添加一个新的 API,来对 Objective-C API 进行适配改进。这里就是实现只读计算属性 rgba,在实现中调用以 __
开头重命名的 Objective-C API。
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(&r, green: &g, blue: &b, alpha: &a)
return (red: r, green: g, blue: b, alpha: a)
}
}
现在调用方式是不是好极了!
let color = Color()
var r = color.rgba.red
var g = color.rgba.green
var b = color.rgba.blue
var a = color.rgba.alpha
Example_SDWebImage
接下来让我们看看 SDWebImage 是怎么使用 NS_REFINED_FOR_SWIFT
的。
Objective-C 方法不支持默认参数值,通常是提供一个多参数的全能方法,然后提供几个少参数的便利方法来调用全能方法,并为一些参数赋默认值。这种方式组合不灵活且扩展不方便,而且在迭代过程中这一组 API 数量可能会越来越多。像 UIImageView (WebCache) 分类中扩展的方法就多达 9 个。
那么,在 Objective-C API 导入到 Swift 时,如何利用上 Swift 可以为方法参数赋默认值的优点,来减少这组 API 的数量呢?可以使用 NS_REFINED_FOR_SWIFT
宏,SDWebImage 为其中 4 个方法添加上了该宏。
// UIImageView+WebCache.h
- (void)sd_setImageWithURL:(nullable NSURL *)url NS_REFINED_FOR_SWIFT;
- (void)sd_setImageWithURL:(nullable NSURL *)url
placeholderImage:(nullable UIImage *)placeholder NS_REFINED_FOR_SWIFT;
- (void)sd_setImageWithURL:(nullable NSURL *)url
placeholderImage:(nullable UIImage *)placeholder
options:(SDWebImageOptions)options NS_REFINED_FOR_SWIFT;
- (void)sd_setImageWithURL:(nullable NSURL *)url
placeholderImage:(nullable UIImage *)placeholder
completed:(nullable SDExternalCompletionBlock)completedBlock NS_REFINED_FOR_SWIFT;
在 Swift 中调用的时候,代码补全只会提示剩下的 5 个 API。
你还是可以调用 sd_setImage(with url: URL?, placeholderImage placeholder: UIImage?):
imageView.sd_setImage(with: nil, placeholderImage: nil)
但这时候它不是调用 Objective-C 的 - (void)sd_setImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder; 方法,而是调用 - (void)sd_setImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder options:(SDWebImageOptions)options completed:(nullable SDExternalCompletionBlock)completedBlock; 方法,并为 options、completed 参数赋默认值。
// Objective-C API
- (void)sd_setImageWithURL:(nullable NSURL *)url
placeholderImage:(nullable UIImage *)placeholder
options:(SDWebImageOptions)options
completed:(nullable SDExternalCompletionBlock)completedBlock;
// In Swift, this API is imported like this:
open func sd_setImage(with url: URL?, placeholderImage placeholder: UIImage?, options: SDWebImageOptions = [], completed completedBlock: SDExternalCompletionBlock? = nil)
如果你想调用 Objective-C 的 - (void)sd_setImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder 方法,那就以双下划线
__
开头调用,但是不建议更没必要这样使用,请遵守规范!
我们来看看 Generated Swift Interface:
extension UIImageView {
open func sd_setImage(with url: URL?, placeholderImage placeholder: UIImage?, options: SDWebImageOptions = [], context: [SDWebImageContextOption : Any]?)
open func sd_setImage(with url: URL?, completed completedBlock: SDExternalCompletionBlock? = nil)
open func sd_setImage(with url: URL?, placeholderImage placeholder: UIImage?, options: SDWebImageOptions = [], completed completedBlock: SDExternalCompletionBlock? = nil)
open func sd_setImage(with url: URL?, placeholderImage placeholder: UIImage?, options: SDWebImageOptions = [], progress progressBlock: SDImageLoaderProgressBlock?, completed completedBlock: SDExternalCompletionBlock? = nil)
open func sd_setImage(with url: URL?, placeholderImage placeholder: UIImage?, options: SDWebImageOptions = [], context: [SDWebImageContextOption : Any]?, progress progressBlock: SDImageLoaderProgressBlock?, completed completedBlock: SDExternalCompletionBlock? = nil)
}
由此,我们可以推断出,Objective-C 方法在导入到 Swift 中时,哪些参数支持默认值:
- 可选枚举
- 作为最后一个参数的 block
Example_3
NS_REFINED_FOR_SWIFT
宏也可以用于解决 Swift 调用 Objective-C 的 API 时可能由于数据类型等不一致导致无法达到预期的问题。例如,Objective-C 里的方法采用了 C 风格的多参数类型;或者 Objective-C 方法返回 NSNotFound,在 Swift 中期望返回 nil 等等。
举个具体的例子:
// Declare in Objective-C
@interface MyClass : NSObject
- (NSInteger)indexOfString:(NSString *)aString;
@end
// Generated Swift Interface
open func index(of aString: String) -> Int
这个 Objective-C API 在 Swift 中使用没有问题,不足的地方在于它将返回一个有效的 NSInteger 值或者 NSNotFound,而在 Swift 中我们更期望的是它返回一个 Int 或者 nil(也就是返回一个 Int?),这样就可以使用可选绑定了。
下面我们来看看如何为该 Objective-C API 做适配。
首先,该 API 添加上宏 NS_REFINED_FOR_SWIFT
,它在 Swift 中会就被重命名为 __index(of: aString)
,且不会在 Generated Swift Interface 中显示。
- (NSUInteger)indexOfString:(NSString *)aString NS_REFINED_FOR_SWIFT;
其次,在 Swift 中 extension MyClass 并重新定义 indexOfString 方法,调用 __index(of: aString)
即调用 Objective-C 中的 indexOfString 方法,判断返回值如果为 NSNotFound,就返回 nil,这样就完成了 API 的适配。
extension MyClass {
func index(of aString: String) -> Int? {
let index = Int(__index(of: aString))
if (index == NSNotFound) {
return nil
}
return index
}
}
Example_4
将 Objective-C 的 + (instancetype)sharedInstance;
方法在 Swift 中的变为 shared
属性。
@interface MyClass : NSObject
+ (instancetype)sharedInstance NS_REFINED_FOR_SWIFT;
@end
extension MyClass {
static var shared: MyClass {
return __sharedInstance()
}
}
NS_REFINED_FOR_SWIFT 宏对 Objective-C API 的重命名规则
NS_REFINED_FOR_SWIFT
可用于方法和属性。添加了 NS_REFINED_FOR_SWIFT
的 Objective-C API 在导入到 Swift 时,具体的 API 重命名规则如下:
- 对于初始化方法,在其第一个参数标签前面加 "__"
// Objective-C API
- (instancetype)initWithClassName:(NSString *)name NS_REFINED_FOR_SWIFT;
// In Swift
init(__className: String)
- 对于其它方法,在其基名前面加 "__"
// Objective-C API
- (NSString *)displayNameForMode:(DisplayMode)mode NS_REFINED_FOR_SWIFT;
// In Swift
func __displayNameForMode(mode: DisplayMode) -> String
- 下标方法将被视为任何其它方法,在方法名前面加 "__",而不是作为 Swift 下标导入
- 其他声明将在其名称前加上 "__",例如属性
// Objective-C API
@property DisplayMode mode NS_REFINED_FOR_SWIFT;
// In Swift
var __mode: DisplayMode { get set }
注意:
NS_REFINED_FOR_SWIFT
和NS_SWIFT_NAME
一起用的话,NS_REFINED_FOR_SWIFT
不生效,而是以NS_SWIFT_NAME
指定的名称重命名 Objective-C API。
小结
本文通过几个例子讲解了宏 NS_REFINED_FOR_SWIFT
的应用场景,让 Objective-C API 在 Swift 中更好地呈现,相信它在你混编的过程中一定会有用武之地的!
参考
- Apple|Xcode Release Notes - Xcode 7
- Apple|Improving Objective-C API Declarations for Swift
- Apple|WWDC20 10680 - Refine Objective-C frameworks for Swift
- Jacob Bandes-Storch|Help Yourself to Some Swift
- Medium|Adapting Objective-C APIs to Swift With NS_REFINED_FOR_SWIFT