美文网首页OCSwift我爱编程
如何将旧的Objective-C项目逐渐转为Swift项目

如何将旧的Objective-C项目逐渐转为Swift项目

作者: 大海中的海绵 | 来源:发表于2017-05-10 15:17 被阅读1063次

    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 = [UIColorcolorWithRed:0.5green:0.0blue:0.5alpha:1.0];

    在Swift中,就会是下面这种形式:

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

    访问属性

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

    被nonnull,nullable和null_resettable标记的属性导入Swift之后,会变成optional和nonoptional类型

    被readonly标记的属性导入Swift之后,变成计算属性 ({ get })。

    被weak标记的属性导入Swift之后,同样是被weak标记 (weak var)。

    被assign,copy,strong或者unsafe_unretained标记的,将会以适当的存储导入Swift。

    被class标记的属性导入Swift之后,变成类型属性。

    原子性属性(atomic和nonatomic)在对应的Swift属性中没有反应出来,但是在Swift中被访问的时候,Objective-C原子性的实现仍然会保留。

    getter=和setter=在Swift中被省略。

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

    方法

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

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

    在Objective-C中:

    [myTableViewinsertSubview:mySubviewatIndex:2];

    导入Swift后:

    myTableView.insertSubview(mySubview,at:2)

    id兼容性

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

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

    var myObject:AnyObject= UITableViewCell()myObject= NSDate()letfutureDate= myObject.addingTimeInterval(10)lettimeSinceNow= myObject.timeIntervalSinceNow

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

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

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

    // 可选链letmyChar = myObject.character?(at:5)//ifletifletfifthCharacter = myObject.character?(at:5) {print("Found \(fifthCharacter) at index 5")}

    空属性和可选

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

    被_Nonnull标记的,在导入Swift之后,会被作为非可选类型

    被_Nullable标记的,在导入Swift之后,会被作为可选类型

    没有被任何注释标记的,在导入Swift之后,会被作为隐式解包可选类型

    例如,在Objective-C中:

    @property(nullable)idnullableProperty;@property(nonnull)idnonNullProperty;@propertyidunannotatedProperty;NS_ASSUME_NONNULL_BEGIN- (id)returnsNonNullValue;- (void)takesNonNullParameter:(id)value;NS_ASSUME_NONNULL_END- (nullableid)returnsNullableValue;- (void)takesNullableParameter:(nullableid)value;- (id)returnsUnannotatedValue;- (void)takesUnannotatedParameter:(id)value;

    导入Swift之后:

    varnullableProperty:Any?varnonNullProperty:AnyvarunannotatedProperty:Any!funcreturnsNonNullValue()->AnyfunctakesNonNullParameter(value: Any)funcreturnsNullableValue()->Any?functakesNullableParameter(value: Any?)funcreturnsUnannotatedValue()->Any!functakesUnannotatedParameter(value: Any!)

    轻量级泛型

    在Swift中:

    @propertyNSArray *dates;@propertyNSCache> *cachedData;@propertyNSDictionary> *supportedLocales;

    导入Swift之后:

    vardates: [Date]varcachedData: NSCachevarsupportedLocales: [String: [Locale]]

    扩展

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

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

    extension UIBezierPath {    convenience init(triangleSideLength: CGFloat,origin: CGPoint) {        self.init()letsquareRoot = CGFloat(sqrt(3.0))letaltitude = (squareRoot * triangleSideLength) /2move(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中是这样的:

    letcompletionBlock: (Data) ->Void= {datain// ...}

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

    解决Block中的循环引用问题

    在Objective-C中:

    __weaktypeof(self) weakSelf =self;self.block = ^{  __strongtypeof(self) strongSelf = weakSelf;  [strongSelf doSomething];};

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

    self.closure = { [unownedself]inself.doSomething()}

    对象之间的比较

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

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

    Swift类型的兼容性

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

    泛型

    元组

    Swift中定义的没有Int类型原始值的枚举

    Swift中定义的结构

    Swift中定义的高阶函数

    Swift中定义的全局变量

    Swift中定义的类型别名

    Swift风格的variadics

    嵌套类型

    Curried functions

    Swift转换为Objective-C:

    可选类型,被__nullable标记

    非可选类型,被__nonnull标记

    常量和计算属性,变成只读属性

    类型属性在Objective-C中被class标记

    类型方法在Objective-C是类方法

    初始化器和实例方法变成Objective-C的实例方法

    会抛出错误的方法,在Objective-C中会多了一个NSerror **参数。如果Swift的方法没有返回值,在Objective-C中会返回一个BOOL。

    例如,在Swift中:

    classJukebox:NSObject{varlibrary:SetvarnowPlaying:String?varisCurrentlyPlaying: Bool {returnnowPlaying != nil    }classvarfavoritesPlaylist: [String]{// return an array of song names}    init(songs:String...) {        self.library=Set(songs)    }    func playSong(named name:String) throws {// play song or throw an error if unavailable}}

    转换成Objective-C后:

    @interfaceJukebox:NSObject@property(nonatomic,strong,nonnull)NSSet *library;@property(nonatomic,copy,nullable)NSString*nowPlaying;@property(nonatomic,readonly,getter=isCurrentlyPlaying)BOOLcurrentlyPlaying;@property(nonatomic,class,readonly,nonnull)NSArray * favoritesPlaylist;- (nonnullinstancetype)initWithSongs:(NSArray * __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)。

    例如:

    classPerson:NSObject{varname: Stringvarfriends: [Person] = []varbestFriend: Person? =nilinit(name: String){        self.name = name    }}letgabrielle = Person(name:"Gabrielle")letjim = Person(name:"Jim")letyuanyuan = 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值类型

    NSAffineTransformAffineTransform

    NSArrayArray

    NSCalendarCalendar

    NSCharacterSetCharacterSet

    NSDataData

    NSDateComponentsDateComponents

    NSDateIntervalDateInterval

    NSDateDate

    NSDecimalNumberDecimal

    NSDictionaryDictionary

    NSIndexPathIndexPath

    NSIndexSetIndexSet

    NSMeasurementMeasurement

    NSNotificationNotification

    NSNumberSwift的数字类型(Int和Float等等)

    NSPersonNameComponentsPersonNameComponents

    NSSetSet

    NSStringString

    NSTimeZoneTimeZone

    NSURLURL

    NSURLComponentsURLComponents

    NSURLQueryItemURLQueryItem

    NSURLRequestURLRequest

    NSUUIDUUID

    我们可以看到,就是直接把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// 直接打印一个Stringos_log("This is a log message.")// 拼接一个或多个参数let fileSize =1234567890os_log("Finished downloading file. Size: %{iec-bytes}d", fileSize)// 定义打印等级,例如info、debug、error、faultos_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。

    CATransform3D

    CLLocationCoordinate2D

    CGAffineTransform

    CGPoint

    CGRect

    CGSize

    CGVector

    CMTimeMapping

    CMTimeRange

    CMTime

    MKCoordinateSpan

    NSRange

    SCNMatrix4

    SCNVector3

    SCNVector4

    UIEdgeInsets

    UIOffset

    Cocoa设计模式

    代理

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

    classMyDelegate:NSObject,NSWindowDelegate{funcwindow(_window: NSWindow, willUseFullScreenContentSize proposedSize: NSSize)->NSSize{returnproposedSize    }}myWindow.delegate =MyDelegate()ifletfullScreenSize = myWindow.delegate?.window(myWindow, willUseFullScreenContentSize: mySize) {print(NSStringFromSize(fullScreenSize))}

    Lazy初始化

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

    在Objective-C中:

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

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

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

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

    lazyvarcurrencyFormatter: NumberFormatter = {    let formatter = NumberFormatter()    formatter.numberStyle=.currencyformatter.currencySymbol="¤"return formatter}()

    单例

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

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

    + (instancetype)sharedInstance {staticid_sharedInstance =nil;staticdispatch_once_tonceToken;dispatch_once(&onceToken, ^{        _sharedInstance = [[selfalloc] init];    });return_sharedInstance;}

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

    classSingleton{staticletsharedInstance = Singleton()}

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

    class Singleton {staticlet sharedInstance: Singleton = {        letinstance= Singleton()        // setup codereturninstance    }()}

    API可用性

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

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

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

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

    平台名称:

    iOS

    iOSApplicationExtension

    macOS

    macOSApplicationExtension

    watchOS

    watchOSApplicationExtension

    tvOS

    tvOSApplicationExtension

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

    @available(iOS8.0, macOS10.10, *)funcuseShinyNewFeature(){// ...}

    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文件创建好之后,我们还需要进行以下操作:

    把Swift中要用到的Objective-C类的头文件,以下面这种形式添加到Bridging-Header.h文件

    #import"XYZCustomCell.h"#import"XYZCustomView.h"#import"XYZCustomViewController.h"

    在Build Settings > Swift Compiler - General > Objective-C Bridging Header添加-Bridging-Header.h的路径,路径的格式:项目名/项目名称-Bridging-Header.h如图

    Objective-C Bridging Header

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

    letmyCell= XYZCustomCell()myCell.subtitle="A custom cell"

    Swift代码导入Objective-C

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

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

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

    // 初始化实例,并调用方法MySwiftClass *swiftObject = [[MySwiftClass alloc] init];[swiftObject swiftMethod];// 引用类或者协议// MyObjcClass.h@classMySwiftClass;@protocolMySwiftProtocol;@interfaceMyObjcClass:NSObject- (MySwiftClass *)returnSwiftClassInstance;- (id)returnInstanceAdoptingSwiftProtocol;// ...@end// 实现Swift协议// MyObjcClass.m#import"ProductModuleName-Swift.h"@interfaceMyObjcClass() // ...@end@implementationMyObjcClass// ...@end

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

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

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

    @objcpublicprotocolMySwiftProtocol{funcrequiredMethod()@objcoptionalfuncoptionalMethod()}

    把Objective-C代码转为Swift

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

    迁移过程

    创建一个对应Objective-C.m和.h的Swift类,创建方法:File > New > File > (iOS, watchOS, tvOS, or macOS) > Source > Swift File。类的名称可以相同,也可以不同。

    导入相关的系统框架

    如果要需要用到Objective-C的代码,需要在bridging header中导入相关的头文件

    为了让这个Swift类可以在Objective-C中使用,需要让这个类继承自Objective-C的类。如果要自定义在Objective-C中调用的Swift接口的名称,使用@objc(name)。

    我们可以通过继承Objective-C的类,实现Objective-C协议等来集成Objective-C已有的成员。

    在迁移过程中,我们要知道:1)Objective-C的语言特性转换成Swift后,是变成怎样;2)Cocoa框架中Objective-C的类型,在Swift中是什么类型;3)常用的设计模式;4)Objective-C的属性如何迁移到Swift。这些大部分内容我上面都有提到。

    Objective-C的(-)和(+)方法,对应到Swift就是func和class func。

    Objective-C的简单的宏定义改为全局常量,复杂的宏定义改为方法

    迁移完成后,在有导入Objective-C类的地方,用#import "项目名称-Swift.h"替换。

    把之前的.m文件的target membership这个勾去掉。先别着急把之前的.m和.h文件删掉,因为我们刚刚写完的Swift类可能不太完善,我们还需要用之前的文件来解决问题。

    target membership

    如果Swift的类名和之前的Objective-C的类名不一样,在用到Objective-C的类的地方,更新为新的类名。

    相关文章

      网友评论

        本文标题:如何将旧的Objective-C项目逐渐转为Swift项目

        本文链接:https://www.haomeiwen.com/subject/bhlgtxtx.html