Swift我的Swift开发Swift

【iOS开发】如何将旧的Objective-C项目逐渐转为Swi

2017-04-22  本文已影响1636人  Lebron_James

Swift从2014年发布到现在,马上接近三年,经过苹果的不断改进和完善,语法方面已经趋于稳定。如果是新建的项目,严重建议使用Swift,因为Swift必定会取代Objective-C。然后对于用Objective-C写的旧项目,我们有两个选择:1)直接整个项目用Swift重写;2)在旧项目的基础上,新的东西用Swift编写,然后再把之前用Objective-C写的代码慢慢改为Swift。我个人更偏向于在旧项目的基础上逐渐把整个项目转为Swift。下面我将会结合实际工作和苹果的官方文档《Using Swift with Cocoa and Objective-C (Swift 3.1)》来总结下如何将旧的Objective-C项目逐渐转为Swift项目。

学习Swift

首先,你要懂得Swift(这TMD不是讲废话吗 ...)。英文能力不错的建议看官方的文档《The Swift Programming Language (Swift 3.1)》,官方的文档总是最好的。不嫌弃的话,可以看看我写的《Swift文集》,总结了Swift的关键知识点。另外,大家可以看看Swift翻译组翻译的内容。

Objective-C和Swift的互用

在这部分内容里,我将会根据官方的文档,总结下Objective-C和Swift是如何互用的。

初始化

在Objective-C中,类的初始化方法通常是以init或者initWith开头的。在Swift中使用Objective-C的类时,Swift会把init开头的方法作为初始化方法,如果是以initWith开头的,在Swift中调用时,会把With去掉,例如:

在Objective-C中:

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

在Swift中调用上面的接口,就会是下面这种形式:

init() { /* ... */ }
init(frame: CGRect, style: UITableViewStyle) { /* ... */ }
类方法和便利初始化器

在Objective-C的类方法,在Swift中会被作为便利初始化器:

在Objective-C中:

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)
访问属性

Objective-C中的属性将会按照下面这个规则来导入Swift:

在Swift中,直接用点语法来访问Objective-C的属性。

方法

同样地,在Swift中也是使用点语法来访问方法。

当Objective-C的方法被导入Swift后,Objective-C的Selector的第一部分会被作为Swift的方法名。例如:

在Objective-C中:

[myTableView insertSubview:mySubview atIndex:2]; 

导入Swift后:

myTableView.insertSubview(mySubview, at: 2)
id兼容性

Objective-C的id类型,导入Swift之后,成为Swift的Any类型。

Swift还有一个类型AnyObject,可以代表所有的class类型,它可以动态的搜索任何@objc方法,而无需向下转型。例如:

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

但是我们在运行代码之前,AnyObject的具体类型是不确定的,所以上面这种写法非常危险。,例如下面这个例子,在运行的时候会crash:

myObject.character(at: 5)
// crash, myObject doesn't respond to that method

我们可以使用可选链或者if let来解决这个问题:

// 可选链
let myChar = myObject.character?(at: 5)

// if let
if let fifthCharacter = myObject.character?(at: 5) {
    print("Found \(fifthCharacter) at index 5")
}
空属性和可选

我们都知道在Objective-C中,可以使用一些注释来标记属性、参数或者返回值是否可以为空,例如_nullable_Nonull等等。他们会按照下面的规则来导入Swift:

例如,在Objective-C中:

@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中:

@property NSArray<NSDate *> *dates;
@property NSCache<NSObject *, id<NSDiscardableContent>> *cachedData;
@property NSDictionary <NSString *, NSArray<NSLocale *>> *supportedLocales;

导入Swift之后:

var dates: [Date]
var cachedData: NSCache<AnyObject, NSDiscardableContent>
var supportedLocales: [String: [Locale]]
扩展

Swift的扩展其实类似于Objective-C的分类。Swift的扩展可以对现有的类、结构和枚举添加新的成员,即使是在Objective-C中定义的类、结构和枚举,都可以进行扩展。

例如下面这个例子,为UIBezierPath添加一个便利初始化器,可用来画一个等边三角形:

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()
    }
}
闭包

Objective-C的block,导入Swift之后变为Closure。例如在Objective-C中有一个block:

void (^completionBlock)(NSData *) = ^(NSData *data) {
   // ...
}

在Swift中是这样的:

let completionBlock: (Data) -> Void = { data in
    // ...
}

Objective-C的block和Swift的Closure基本上可以说是等价的,但是有一点不同的是:外部的变量在Swift的Closure中是可变的,我们可以直接在Closure内部更新变量的值;而在Objective-C中,需要用__block标记变量。

解决Block中的循环引用问题

在Objective-C中:

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

在Swift中是这样解决的,[unowned self]被称为捕获列表(Capture List):

self.closure = { [unowned self] in
    self.doSomething()
}
对象之间的比较

在Swift中,比较两个对象是否相等有两种方法:1) ==:比较两个对象的内容是否相等;2) ===:比较两个常量或者变量是否引用着同一个对象实例。

Swift为继承自NSObject的子类提供了默认的=====实现,并实现了Equatable协议。默认的==实现调用了isEqual:方法,默认的===实现检查指针是否相等。我们不能重写从Objective-C导入的类的这两个操作符。

Swift类型的兼容性

下面这些Swift特有的类型,是不兼容Objective-C的:

Swift转换为Objective-C:

例如,在Swift中:

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
    }
}

转换成Objective-C后:

@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;
@end
自定义Swift在Objective-C的接口

我们可以使用@objc(name)自定义Swift的类、属性、方法、枚举类型或者枚举case在Objective-C中使用时的名字。

例如,在Swift中:

@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, вДереве дерево: Дерево) {
        // ...
    }
}

Swift还提供了一个属性@nonobjc,被这个属性标记的成员将不能在Objective-C中使用。

需要动态调度

当Swift的API被Objective-C runtime使用时,不能保证能动态调度属性、方法、下标或者初始化器。Swift的编译器仍然会反虚拟化或者内联成员访问来优化代码的属性,并绕过Objective-C runtime。

我们可以使用dynamic在使用Objective-C runtime时动态的访问成员。需要动态调度的情况是非常少的。但是,在Objective-C runtime中使用key-value observing或者method_exchangeImplementations时,我们就需要动态调度,在运行的时候来动态地替换一个方法的实现。

注意:使用了dynamic标记的声明,不能再使用@nonobjc。因为使用了@nonobjc,就意味着不能在Objective-C中使用,而dynamic就是为了给Objective-C使用,这两个属性是完全冲突的。

Selector

在Objective-C中,我们使用@selector来构造一个Selector;而在Swift中,我们要使用#selector

Key和Key Path

在Swift中,可以使用#keyPath来生成编译器检查(也就是说编译的时候就能知道key和keyPath是否有误,而不必等到运行时才能确定)的key和keyPath,然后就可以给这些方法使用:value(forKey:)value(forKeyPath:)addObserver(_:forKeyPath:options:context:)#keyPath支持链式方法或者属性,如#keyPath(Person.bestFriend.name)

例如:

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"]

Cocoa Frameworks

Swift能自动地将一些类型在Swift和Objective-C之间互相转换。例如我们可以传一个String值给NSString参数。

Foundation
桥接类型

Swift Foundation提供了下列桥接值类型:

Objective-C引用类型 Swift值类型
NSAffineTransform AffineTransform
NSArray Array
NSCalendar Calendar
NSCharacterSet CharacterSet
NSData Data
NSDateComponents DateComponents
NSDateInterval DateInterval
NSDate Date
NSDecimalNumber Decimal
NSDictionary Dictionary
NSIndexPath IndexPath
NSIndexSet IndexSet
NSMeasurement Measurement
NSNotification Notification
NSNumber Swift的数字类型(IntFloat等等)
NSPersonNameComponents PersonNameComponents
NSSet Set
NSString String
NSTimeZone TimeZone
NSURL URL
NSURLComponents URLComponents
NSURLQueryItem URLQueryItem
NSURLRequest URLRequest
NSUUID UUID

我们可以看到,就是直接把Objective-C的前缀NS去掉,就是Swift的值类型(但是有些情况例外)。这些Swift的值类型拥有Objective-C引用类型的所有方法。任何使用Objective-C引用类型的地方,都可以使用对应的Swift值类型。

统一的Logging

统一的logging系统提供了一些平台通用的API来打印一些信息,但是这个API只在 iOS 10.0, macOS 10.12, tvOS 10.0和watchOS 3.0以后的版本才可用。

下面是使用的例子:

import os.log
 
 // 直接打印一个String
os_log("This is a log message.")

// 拼接一个或多个参数
let fileSize = 1234567890
os_log("Finished downloading file. Size: %{iec-bytes}d", fileSize)

// 定义打印等级,例如info、debug、error、fault
os_log("This is additional info that may be helpful for troubleshooting.", type: .info)

// 打印信息到特定的子系统
let customLog = OSLog("com.your_company.your_subsystem_name.plist", "your_category_name")
os_log("This is info that may be helpful during development or debugging.", log: customLog, type: .debug)
Cocoa的结构

当Swift的结构被桥接成Objective-C时,下面这些结构会变成NSValue

Cocoa设计模式

代理

代理设计模式,是我们经常用到的。在Objective-C中,在调用代理方法之前,我们首先要检查代理是否有实现这个代理方法。而在Swift中,我们可以使用可选链来调用代理方法。例如:

class MyDelegate: NSObject, NSWindowDelegate {
    func window(_ window: NSWindow, willUseFullScreenContentSize proposedSize: NSSize) -> NSSize {
        return proposedSize
    }
}
myWindow.delegate = MyDelegate()
if let fullScreenSize = myWindow.delegate?.window(myWindow, willUseFullScreenContentSize: mySize) {
    print(NSStringFromSize(fullScreenSize))
}
Lazy初始化

一个lazy属性只会在第一次被访问的时候才会初始化,相当于在Objective-C的懒加载(重写getter方法)。当需要进行比较复杂或者耗时的计算才能初始化一个属性时,我们应该尽量使用lazy属性。

在Objective-C中:

@property NSXMLDocument *XML;
 
- (NSXMLDocument *)XML {
    if (_XML == nil) {
        _XML = [[NSXMLDocument alloc] initWithContentsOfURL:[[Bundle mainBundle] URLForResource:@"/path/to/resource" withExtension:@"xml"] options:0 error:nil];
    }
 
    return _XML;
}

而在Swift,我们使用lazy属性:

lazy var XML: XMLDocument = try! XMLDocument(contentsOf: Bundle.main.url(forResource: "document", withExtension: "xml")!, options: 0)

对于其他需要更复杂的初始化的属性,可以写成:

lazy var currencyFormatter: NumberFormatter = {
    let formatter = NumberFormatter()
    formatter.numberStyle = .currency
    formatter.currencySymbol = "¤"
    return formatter
}()
单例

单例模式使我们在开发中经常用到的。

在Objective-C中,我们通常用GCD来实现:

+ (instancetype)sharedInstance {
    static id _sharedInstance = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        _sharedInstance = [[self alloc] init];
    });
 
    return _sharedInstance;
}

在Swift中,直接使用static类型属性即可,可以保证只初始化一次,即使时在多线程中被同时访问。

class Singleton {
    static let sharedInstance = Singleton()
}

如果我们需要其他设置,可以写成:

class Singleton {
    static let sharedInstance: Singleton = {
        let instance = Singleton()
        // setup code
        return instance
    }()
}
API可用性

有些类和方法并不是在所有平台或者版本都可用的,所有有时我们需要进行API可用性检查。

例如,CLLocationManagerrequestWhenInUseAuthorization方法只能在iOS 8.0和macOS 10.10以后的版本才能使用:

let locationManager = CLLocationManager()
if #available(iOS 8.0, macOS 10.10, *) {
    locationManager.requestWhenInUseAuthorization()
}

*是为了处理未来的平台。

平台名称:

同样地,我们在写自己的API时,也可以指定那些平台可以使用:

@available(iOS 8.0, macOS 10.10, *)
func useShinyNewFeature() {
    // ...
}

Swift和Objective-C混编

把Objective-C代码导入Swift

为了把Objective-C代码导入Swift中,我们需要用到Objective-C bridging header。当你把Objective-C文件拖入Swift项目中时,Xcode会提示你是否新建一个bridging header,如下图:

Create Bridging Header

点击Create Bridging Header,项目的文件路径下就会创建一个名为项目名称-Bridging-Header.h的文件(如果项目名称不是英文,将会以_代替;如果第一个是字母,也会以_代替)。

当然,我们也可以手动创建:File > New > File > (iOS, watchOS, tvOS, or macOS) > Source > Header File。

-Bridging-Header.h文件创建好之后,我们还需要进行以下操作:

#import "XYZCustomCell.h"
#import "XYZCustomView.h"
#import "XYZCustomViewController.h"
Objective-C Bridging Header

这样我们就配置完成了,可以在Swift中调用Objective-C的代码:

let myCell = XYZCustomCell()
myCell.subtitle = "A custom cell"
Swift代码导入Objective-C

当需要在Objective-C中使用Swift的代码时,我们依赖于Xcode自动生成的头文件,这个头文件的名称是项目名-Swift.h(如果项目名称不是英文,将会以_代替;如果第一个是字母,也会以_代替)。

默认情况下,这个自动生成的头文件包含了在Swift中被public或者open标记的声明,如果这个项目中有Objective-C bridging header,那么,internal标记的声明也包含在内。被privatefileprivate标记的不包含在内。私有的声明不会暴露给Objective-C,除非他们被@IBAction@IBOutlet或者@objc标记。

当需要在Objective-C中使用Swift的代码时,直接导入头文件项目名-Swift.h,然后我们就可以在Objective-C中调用Swift的接口,用法与Objective-C的语法相同:

// 初始化实例,并调用方法
MySwiftClass *swiftObject = [[MySwiftClass alloc] init];
[swiftObject swiftMethod];

// 引用类或者协议
// MyObjcClass.h
@class MySwiftClass;
@protocol MySwiftProtocol;
 
@interface MyObjcClass : NSObject
- (MySwiftClass *)returnSwiftClassInstance;
- (id <MySwiftProtocol>)returnInstanceAdoptingSwiftProtocol;
// ...
@end

// 实现Swift协议
// MyObjcClass.m
#import "ProductModuleName-Swift.h"
 
@interface MyObjcClass () <MySwiftProtocol>
// ...
@end
 
@implementation MyObjcClass
// ...
@end



注意:如果是刚刚写的Swift代码,马上就想在Objective-C调用,我们需要先编译一下,然后Objective-C中才能访问到Swift的接口。

声明可以被Objective-C使用的Swift协议

为了声明一个可以被Objective-C使用的Swift协议,我们要用@objc标记,如果协议的方法是optional,也需要用@objc

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

把Objective-C代码转为Swift

前面讲了一大堆基础知识,就是为了更好地将Objective-C代码转为Swift。

迁移过程
target membership

有任何问题,欢迎大家留言!

欢迎加入我管理的Swift开发群:536353151,本群只讨论Swift相关内容。

原创文章,转载请注明出处。谢谢!

上一篇 下一篇

猜你喜欢

热点阅读