3

混用swift和objc - 与objc APIs交互

 2 years ago
source link: https://rhetty.github.io/2017/04/07/%E6%B7%B7%E7%94%A8swift%E5%92%8Cobjc-%E4%B8%8Eobjc-APIs%E4%BA%A4%E4%BA%92/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

混用swift和objc - 与objc APIs交互

发表于 2017-04-07

  |  

SwiftObjective-C之间具备双向的互操作性,在一种语言中可以使用另一种语言写的代码。目前,在用 Swift 写新项目时,可能会调用以前 objc 写的代码,这是一个重要的方面。苹果很好的做到了这一点,在用原生 Swift 写代码时,可以通过导入 objc 文件,就可以初始化 objc 对象和调用方法了。

objc 的初始化方法都以init开头,with后面的参数都会放在 Swift 方法后的括号中。Swift 中初始化的方法名都为init,而后面括号中的参数可以不同:

- (instancetype)init;
- (instancetype)initWithFrame:(CGRect)frame
style:(UITableViewStyle)style;

上面的 objc 代码转化为 Swift 代码:

init() { /* ... */ }
init(frame: CGRect, style: UITableViewStyle) { /* ... */ }

在调用初始化方法时:

UITableView *myTableView = [[UITableView alloc] initWithFrame:CGRectZero style:UITableViewStyleGrouped];

在 Swift 中则是这样:

let myTableView: UITableView = UITableView(frame: .zero, style: .grouped)

类工厂方法和快捷初始化器

在 objc,可以这样用类工厂方法:

UIColor *color = [UIColor colorWithRed:0.5 green:0.0 blue:0.5 alpha:1.0];

Swift:

let color = UIColor(red: 0.5, green: 0.0, blue: 0.5, alpha: 1.0)

(在 Swift 中,类方法跟初始化方法调用起来没啥不同。。)

可失败的初始化

在 objc 中,如果初始化失败,会返回nil。在 Swift 中,这种特性叫做failable initialization。objc 中,可以通过nullability annotations反应初始化是否会失败,但这不是强制的,详见可空和可选。Swift 中,不会失败的初始化方法定义为init(...),而可能失败的初始化方法定义为init?(...)。另外,objc 的初始化方法会转换成init!(...)

比如,一个UIImage对象可能因为图片地址错误而初始化失败:

if let image = UIImage(contentsOfFile: "MyImage.png") {
// loaded the image successfully
} else {
// could not load the image

objc 中的属性会以以下方式转化:

  • 可空的属性(nonnull, nullable, null_resettable)会以可选或非可选类型导入,详见可空和可选
  • 只读属性导入为带getter的属性
  • weak属性转化为 Swift 属性后,用weak标记(weak var
  • 其他内存管理修饰符(assign, copy, strong, unsafe_unretained)转化为正确的存储方式
  • class性质的属性导入为 Swift 的类型属性
  • 原子性(atomic, nonatomic)不会显示在 Swift 的属性声明中,但从 Swift 中访问 objc 属性时,objc 定义的原子性将会保持。
  • 存取方法名(getter=, setter=)会被 Swift 忽略

Swift 访问属性的方式:

myTextField.textColor = .darkGray
myTextField.text = "Hello world"

objc 中,无参数有返回值的方法可以用.访问,但这类方法只会导入成 Swift 的实例方法。

objc 方法的第一部分会变成 Swift 方法的方法名,之后的部分变成参数名,在 Swift 中调用时,第一个参数不需要参数名,其他参数需要些参数名。

[myTableView insertSubview:mySubview atIndex:2];
myTableView.insertSubview(mySubview, at: 2)

调用的方法没有参数时,也要写上括号:

myTableView.layoutIfNeeded()

id 的兼容性

objc 中的id类型导入 Swift 中变成Any类型。在编译时和运行时,当 Swift 值作为id参数传给 objc,编译器会提供通用的桥接转换操作。当id值作为Any导入 Swift 时,运行时自动处理。

var x: Any = "hello" as String
x as? String // String with value "hello"
x as? NSString // NSString with value "hello"
x = "goodbye" as NSString
x as? String // String with value "goodbye"
x as? NSString // NSString with value "goodbye"

向下转换 Any

当知道Any类型的对象的确切类型时,可以将其向下转化(downcasting)为一个更确切的对象,但向下转换不保证可以成功。

条件类型转换操作符(as?)返回一个可选值。

let userDefaults = UserDefaults.standard
let lastRefreshDate = userDefaults.object(forKey: "LastRefreshDate") // lastRefreshDate is of type Any?
if let date = lastRefreshDate as? Date {
print("\(date.timeIntervalSinceReferenceDate)")

如果确定对象类型,可以用强制转换操作符(as!):

let myDate = lastRefreshDate as! Date
let timeInterval = myDate.timeIntervalSinceReferenceDate

如果强制转换错误,会导致 crash:

let myDate = lastRefreshDate as! String // Error

动态方法查找

Swift 提供AnyObject对象,可以代表一些类型的对象,并且可以动态查找任何@any方法。这样,对于 objc 中返回id的方法,你可以保持无类型访问的灵活性。

var myObject: AnyObject = UITableViewCell()
myObject = NSDate()
let futureDate = myObject.addingTimeInterval(10)
let timeSinceNow = myObject.timeIntervalSinceNow

未识别的选择子和可选链

调用一个AnyObject的不存在的方法时,会导致程序 crash。Swift 利用可选方式防止这样不安全的行为。当你调用一个AnyObject的方法时,这个方法调用会表现的像隐式解包可选值。你可以用同样的可选链的语法来在AnyObject上调用方法。

// myObject has AnyObject type and NSDate value
let myCount = myObject.count
// myCount has Int? type and nil value
let myChar = myObject.character?(at: 5)
// myChar has unichar? type and nil value
if let fifthCharacter = myObject.character?(at: 5) {
print("Found \(fifthCharacter) at index 5")
// conditional branch not executed

可空和可选

objc 用nullability annotations指定参数值、属性或返回值是否可以是NULLnil。单个类型声明使用_Nullable_Nonnull,单个属性声明使用nullablenonnullnull_resettable,整体域对于可空值使用NS_ASSUME_NONNULL_BEGINNS_ASSUME_NONNULL_END宏。如果没有提供可空信息,Swift 会导入为隐式解包可选值。

objc 声明:

@property (nullable) id nullableProperty;
@property (nonnull) id nonNullProperty;
@property id unannotatedProperty;
NS_ASSUME_NONNULL_BEGIN
- (id)returnsNonNullValue;
- (void)takesNonNullParameter:(id)value;
NS_ASSUME_NONNULL_END
- (nullable id)returnsNullableValue;
- (void)takesNullableParameter:(nullable id)value;
- (id)returnsUnannotatedValue;
- (void)takesUnannotatedParameter:(id)value;

导入 Swift:

var nullableProperty: Any?
var nonNullProperty: Any
var unannotatedProperty: Any!
func returnsNonNullValue() -> Any {}
func takesNonNullParameter(value: Any) {}
func returnsNullableValue() -> Any? {}
func takesNullableParameter(value: Any?) {}
func returnsUnannotatedValue() -> Any! {}
func takesUnannotatedParameter(value: Any!) {}

桥接可选值与非空对象

根据可选值是否有值,Swift 将可选值桥接到非空 objc 对象。如果可选值是nil,Swift 将其桥接为NSNull实例。否则,桥接为解包值。可选值的数组会桥接为NSArray

@implementation OptionalBridging
+ (void)logSomeValue:(nonnull id)valueFromSwift {
if ([valueFromSwift isKindOfClass: [NSNull class]]) {
os_log(OS_LOG_DEFAULT, "Received an NSNull value.");
} else {
os_log(OS_LOG_DEFAULT, "%s", [valueFromSwift UTF8String]);
let someValue: String? = "Bridge me, please."
let nilValue: String? = nil
OptionalBridging.logSomeValue(someValue as Any) // Bridge me, please.
OptionalBridging.logSomeValue(nilValue as Any) // Received an NSNull value.

协议限制的类(Protocol-Qualified Classes)

objc 协议限制的类导入为 Swift 中的协议类型值。

- (void)doSomethingForClass:(Class<NSCoding>)codingClass;
func doSomething(for codingClass: NSCoding.Type) {}

轻量级泛型(Lightweight Generics)

objc 使用lightweight generic的类型声明导入 Swift 将提供保存内容的类型信息。

@property NSArray<NSDate *> *dates;
@property NSCache<NSObject *, id<NSDiscardableContent>> *cachedData;
@property NSDictionary <NSString *, NSArray<NSLocale *>> *supportedLocales;
var dates: [Date]
var cachedData: NSCache<AnyObject, NSDiscardableContent>
var supportedLocales: [String: [Locale]]

所有导入 Swift 的 objc 泛型类型参数有一个类型限制,需要那个类型是一个类(T: Any)。

@interface List<T: id<NSCopying>> : NSObject
- (List<T> *)listByAppendingItemsInList:(List<T> *)otherList;
@interface ListContainer : NSObject
- (List<NSValue *> *)listOfValues;
@interface ListContainer (ObjectList)
- (List *)listOfObjects;
class List<T: NSCopying> : NSObject {
func listByAppendingItemsInList(otherList: List<T>) -> List<T> {}
class ListContainer : NSObject {
func listOfValues() -> List<NSValue> {}
extension ListContainer {
func listOfObjects() -> List<NSCopying> {}

Swift 中的扩展和 objc 中的分类类似。

extension UIBezierPath {
convenience init(triangleSideLength: CGFloat, origin: CGPoint) {
self.init()
let squareRoot = CGFloat(sqrt(3.0))
let altitude = (squareRoot * triangleSideLength) / 2
move(to: origin)
addLine(to: CGPoint(x: origin.x + triangleSideLength, y: origin.y))
addLine(to: CGPoint(x: origin.x + triangleSideLength / 2, y: origin.y + altitude))
close()

跟分类一样,扩展只能添加可计算的属性,不能添加要存储的属性(虽然 objc 可以用关联对象实现):

extension CGRect {
var area: CGFloat {
return width * height
let rect = CGRect(x: 0.0, y: 0.0, width: 10.0, height: 50.0)
let area = rect.area

不能使用扩展重载 objc 中已有的方法和属性。

用 objc block calling convertion (标记为@convention(block)属性),objc 的 block 可以自动导入为 Swift 的闭包。

void (^completionBlock)(NSData *) = ^(NSData *data) {
// ...
let completionBlock: (Data) -> Void = { data in
// ...

block 和闭包是兼容的,所以可以将闭包作为 block 传给 objc 方法。而且闭包和 Swift 方法是相同类型的,所以也可以将 Swift 方法直接传过去。

闭包和 block 相似,会捕获变量,但是方式不同:变量可以修改,而不是拷贝。也就是说,objc 中的__block是 Swift 中变量的默认行为。

捕获 self 时防止循环引用

objc 中防止循环引用:

__weak typeof(self) weakSelf = self;
self.block = ^{
__strong typeof(self) strongSelf = weakSelf;
[strongSelf doSomething];

Swift:

self.closure = { [unowned self] in
self.doSomething()

在 Swift 中,两种比较对象方式:

  • equality(==):比较对象内容
  • identity(===):是否指向相同的对象实例

==的默认实现是调用isEqual:方法,===的默认实现是检查指针是否相等。这两个操作符不应该重载。NSObject 提供的isEqual:的基本实现跟检查指针等同性一样,这个方法可以在子类中重载。

@property NSDictionary *unqualifiedDictionary;
@property NSDictionary<NSString *, NSDate *> *qualifiedDictionary;
@property NSSet *unqualifiedSet;
@property NSSet<NSString *> *qualifiedSet;
var unqualifiedDictionary: [AnyHashable: Any]
var qualifiedDictionary: [String: Date]
var unqualifiedSet: Set<AnyHashable>
var qualifiedSet: Set<String>

Swift 类型兼容

Swift-only features:

  • Generics
  • Tuples
  • Enumerations defined in Swift without Int raw value type
  • Structures defined in Swift
  • Top-level functions defined in Swift
  • Global variables defined in Swift
  • Typealiases defined in Swift
  • Swift-style variadics
  • Nested types
  • Curried functions

Swift API 转换成 objc 的方式与 objc API 如何装换成 Swift 类似,但反过来转换的时候:

  • Swift 可选类型标注为__nullable
  • 不可选类型标注为__nonnull
  • 常量属性和计算的属性变成只读的
  • 存储的变量变成读写的
  • Swift type properties become Objective-C properties with the class property attribute.
  • 类型方法变成类方法
  • 初始化和实例方法变成实例方法
  • 抛出错误的方法变成带NSError **变量的方法,如果 Swift 方法没有参数,AndReturnError:添加到 objc 方法名中,否则添加error:。如果 Swift 方法没有明确返回类型,对应的 objc 方法有一个布尔返回值。如果 Swift 方法返回非可选类型,对应的 objc 方法有一个可选的返回值。
class Jukebox: NSObject {
var library: Set<String>
var nowPlaying: String?
var isCurrentlyPlaying: Bool {
return nowPlaying != nil
class var favoritesPlaylist: [String] {
// return an array of song names
init(songs: String...) {
self.library = Set<String>(songs)
func playSong(named name: String) throws {
// play song or throw an error if unavailable
@interface Jukebox : NSObject
@property (nonatomic, strong, nonnull) NSSet<NSString *> *library;
@property (nonatomic, copy, nullable) NSString *nowPlaying;
@property (nonatomic, readonly, getter=isCurrentlyPlaying) BOOL currentlyPlaying;
@property (nonatomic, class, readonly, nonnull) NSArray<NSString *> * favoritesPlaylist;
- (nonnull instancetype)initWithSongs:(NSArray<NSString *> * __nonnull)songs OBJC_DESIGNATED_INITIALIZER;
- (BOOL)playSong:(NSString * __nonnull)name error:(NSError * __nullable * __null_unspecified)error;

不能在 Objective-C 中子类化一个 Swift 类。

在 objc 中配置 Swift 接口

@objc(name)属性可以修改你接口中暴露给 objc 的类名、属性、方法、枚举类型或枚举情况声明(enumeration case declaration)。

@objc(Color)
enum Цвет: Int {
@objc(Red)
case Красный
@objc(Black)
case Черный
@objc(Squirrel)
class Белка: NSObject {
@objc(color)
var цвет: Цвет = .Красный
@objc(initWithName:)
init (имя: String) {
// ...
@objc(hideNuts:inTree:)
func прячьОрехи(количество: Int, вДереве дерево: Дерево) {
// ...

@objc(name)在将 objc 项目迁移到 Swift 时也很有用。归档对象使用它们的类名进行归档,可以用@objc(name)将名字指定为与 objc 一样,所以以前的归档可以在新的 Swift 类中解档。

Swift 还提供了 @nonobjc 属性,使声明在 objc 中不可见。You can use it to resolve circularity for bridging methods and to allow overloading of methods for classes imported by Objective-C. If an Objective-C method is overridden by a Swift method that cannot be represented in Objective-C, such as by specifying a parameter to be a variable, that method must be marked @nonobjc.

要求动态分派

当 Swift API 被 objc runtime 导入时,不能保证属性、方法、下标或初始器的动态分派。可以用dynamic要求成员的访问通过 objc runtime 来动态分派。通常这不是必须的,但使用KVOmethod_exchangeImplementations方法时需要这么做。

标记为dynamic的声明不能再用@nonobjc标记。

在 Swift 中,objc 中的选择子用Selector结构体表示,并且可以用#selector表达式构造。要构造一个能被 objc 调用的方法的选择子,如#selector(MyViewController.tappedButton(sender:))。要构造 objc 的 getter 和 setter
方法的选择子,要用getter:setter:作为前缀,如#selector(getter: MyViewController.myButton)

import UIKit
class MyViewController: UIViewController {
let myButton = UIButton(frame: CGRect(x: 0, y: 0, width: 100, height: 50))
override init?(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
let action = #selector(MyViewController.tappedButton)
myButton.addTarget(self, action: action, forControlEvents: .touchUpInside)
func tappedButton(sender: UIButton?) {
print("tapped button")
required init?(coder: NSCoder) {
super.init(coder: coder)

An Objective-C method reference can be parenthesized, and it can use the as operator to disambiguate between overloaded functions, such as #selector(((UIView.insert(subview:at:)) as (UIView) -> (UIView, Int) -> Void)).

objc 方法的不安全调用

objc 中的perform(_ :)方法不安全,Swift 中比较好的做法是将对象转换成AnyObject,然后使用可选链。

The methods that perform a selector synchronously, such asperform(_:), return an implicitly unwrapped optional unmanaged pointer to an AnyObject instance (Unmanaged<AnyObject>!), because the type and ownership of the value returned by performing the selector can’t be determined at compile time. In contrast, the methods that perform a selector on a specific thread or after a delay, such as perform(_:on:with:waitUntilDone:modes:) and perform(_:with:afterDelay:), don’t return a value.

let string: NSString = "Hello, Cocoa!"
let selector = #selector(NSString.lowercased(with:))
let locale = Locale.current
if let result = string.perform(selector, with: locale) {
print(result.takeUnretainedValue())
// Prints "hello, cocoa!"

用一个无法识别的选择子去调用一个方法时,会调用doesNotRecognizeSelector(_:),会默认抛出一个NSInvalidArgumentException异常。

let array: NSArray = ["delta", "alpha", "zulu"]
// Not a compile-time error because NSDictionary has this selector.
let selector = #selector(NSDictionary.allKeysForObject)
// Raises an exception because NSArray does not respond to this selector.
array.perform(selector)

Keys and Key Paths

在 Swift 中,可以用#keyPath表达式生成一个通过编译器检查的 key 和 key paths,交给 KVC 方法(value(forKey:)value(forKeyPath:))和 KVO 方法(addObserver(_:forKeyPath:options:context:))使用。

class Person: NSObject {
var name: String
var friends: [Person] = []
var bestFriend: Person? = nil
init(name: String) {
self.name = name
let gabrielle = Person(name: "Gabrielle")
let jim = Person(name: "Jim")
let yuanyuan = Person(name: "Yuanyuan")
gabrielle.friends = [jim, yuanyuan]
gabrielle.bestFriend = yuanyuan
#keyPath(Person.name)
// "name"
gabrielle.value(forKey: #keyPath(Person.name))
// "Gabrielle"
#keyPath(Person.bestFriend.name)
// "bestFriend.name"
gabrielle.value(forKeyPath: #keyPath(Person.bestFriend.name))
// "Yuanyuan"
#keyPath(Person.friends.name)
// "friends.name"
gabrielle.value(forKeyPath: #keyPath(Person.friends.name))
// ["Yuanyuan", "Jim"]

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK