Swift系列二十九 - 从OC到Swift

Posted 1024星球

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Swift系列二十九 - 从OC到Swift相关的知识,希望对你有一定的参考价值。

现在大部分项目还是纯OC,即使迁移到Swift也只能是一点点模块过度,那么OC和Swift有什么样的区别呢?两者之间怎样相互调用?

一、注释

  • // MARK:类似于OC中的#pragma mark
  • // MARK: -类似于OC中的#pragma mark-
  • // TODO:用于标记未完成的任务
  • // FIXME: -用于标记待修复的问题
  • #warning("msg")用来做全局提示

示例代码:

public class Person {
    // MARK: - 属性
    var age = 0
    var weight = 0
    var height = 0
    
    // MARK: - 私有方法
    // MARK: 跑步
    private func run1() {
        // TODO: 未完成
    }
    private func run2() {
        // FIXME: 待修复
        age += 20
    }
    // MARK: 走路
    private func walk1() { }
    private func walk2() { }
    
    // MARK: - 公共方法
    public func eat1() { }
    public func eat2() { }
}

效果呈现:

使用MARK: -时,代码区对应位置也会显示一条分割线(在标记位上方,颜色很淡)。

warning效果:

注意:只能大写,不能小写,否则没有效果(androidStudio和IDEA做的比Xcode好太多)。

二、条件编译

Swift支持条件编译的内容是不多的,大概就是下面这些:

// 操作系统:macOS\\ios\\tvOS\\watchOS\\Linux\\Android\\Windows\\FreeBSD
#if os(macOS) || os(iOS)
// CPU架构:i386\\x86_64\\arm\\arm64
#elseif arch(x86_64) || arch(arm64)
// swift版本
#elseif swift(<5) && swift(>=3)
// 模拟器
#elseif targetEnvironment(simulator)
// 是否可以导入某模块
#elseif canImport(Foundation)
#else
#endif

自定义编译标记:

Xcode默认有一个DEBUG标记,我们也可以自己添加一个新的标记。Active Compilation ConditionsOther Swift Flags没有多大区别,只是在Other Swift Flags区域增加标记时需要在最前面加上-D

#if DEBUG
// debug模式
#else
// release模式
#endif

#if TEST
print("test")
#endif

#if OTHER
print("other")
#endif

在OC中是可以通过不同编译条件定义不同的宏,来控制不同环境下NSLog是否有效。但是在Swift中只能通过定义一个新的函数,通过不同环境的编译标记让其运行:

func log(_ msg: String) {
    #if DEBUG
    print(msg)
    #endif
}

我们不需要考虑在Release环境下是否有多余log函数占内存。因为编译器会自动做内联优化。

自定义精准打印:

func log(_ msg: String, file: NSString = #file, line: Int = #line, fn: String = #function) {
    #if DEBUG
    let prefix = "from:\\(file.lastPathComponent)_line:\\(line)_fn:\\(fn):"
    print(prefix, msg)
    #endif
}

func test() {
    log("测试信息")
}
test() // 输出:from:main.swift_line:20_fn:test(): 测试信息

如果在log函数内部直接使用print(#file, #line, #function, msg),每次打印都是同样的文件、同一行,同一个log函数。因为#file, #line, #function捕捉的是当前函数的环境。

为什么要使用OC的NSString,因为NSStringlastPathComponent属性用起来更加便捷。

注意:在Swift中是没有宏的。

三、版本检测

3.1. 系统版本检测

示例代码:

if #available(iOS 10, macOC 10.12, *) {
    // 对于iOS平台,只在iOS10及以上版本执行
    // 对于macOS平台,只在macOS 10.12及以上版本执行
    // 最后的*表示在其他所有平台都执行
}

3.2. API可用性说明

可以对一些废弃的API进行标记说明。

示例代码:

// Person只在iOS10及以上、macOS 10.12及以上才可以使用
@available(iOS 10, macOS 10.12, *)
class Person { }

struct Student {
    // study_已经被修改为study
    @available(*, unavailable, renamed: "study")
    func study_() { }
    func study() { }
    
    // run函数已经在iOS11被废弃
    @available(iOS, deprecated: 11)
    // run函数已经在macOS 10.11被废弃
    @available(macOS, deprecated: 10.11)
    func run() { }
}

更多用法参考: https://docs.swift.org/swift-book/ReferenceManual/Attributes.html

小技巧:有返回值的函数体暂时不写内部逻辑时,可以用fatalError()代替。

四、iOS程序的入口

AppDelegate上面默认有个@UIApplicationMain标记,这表示编译器自动生成入口代码(main函数代码),自动设置AppDelegateApp的代理。

也可以删掉@UIApplicationMain,自定义入口代码:新建一个main.swift文件,然后手动实现UIApplicationMain函数(和OC的main.m基本一致)。

// main.swift
import UIKit

// 自定义Application
class DBApplication: UIApplication {}

// 程序入口
UIApplicationMain(CommandLine.argc, CommandLine.unsafeArgv, NSStringFromClass(DBApplication.self), NSStringFromClass(AppDelegate.self))

注意:自定义入口代码的文件一定要是main.swift

五、Swift调用OC

很多第三方代码/库都是用OC写的,而我们的项目使用Swift作为开发主语言。这时候就需要Swift调用OC的技术了。

5.1. 建立桥接头文件

方式一(手动创建):

  1. 新建1个桥接头文件,文件名格式默认为:{targetName}-Bridging-Header.h(文件名称是固定写法)。

  2. Build Settings中设置头文件的位置。
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vdfrcsph-1620351458555)(http://blog.idbeny.com/nwy0h.png@normal)]

方式二(自动创建):

如果源项目是Swift,在新建OC文件时,Xcode会提示是否创建桥接头文件,选择创建即可。

头文件的作用:

OC需要暴露给Swift的一些内容放到头文件中。

5.2. 调用OC代码

  1. 在桥接头文件中导入Swift需要用到的相关OC头文件。
#import "DBPerson.h"

DBPerson.h

int sum(int a, int b);
@interface DBPerson : NSObject

@property (nonatomic, assign) NSInteger age;
@property (nonatomic, copy) NSString *name;

- (instancetype)initWithAge:(NSInteger)age name:(NSString *)name;
+ (instancetype)personWithAge:(NSInteger)age name:(NSString *)name;

- (void)run;
+ (void)run;

- (void)eat:(NSString *)food other:(NSString *)other;
+ (void)eat:(NSString *)food other:(NSString *)other;

@end

DBPerson.m

#import "DBPerson.h"

int sum(int a, int b) {
    return a + b;
}

@implementation DBPerson

- (instancetype)initWithAge:(NSInteger)age name:(NSString *)name {
    NSLog(@"-init");
    if (self = [super init]) {
        self.age = age;
        self.name = name;
    }
    return self;
}

+ (instancetype)personWithAge:(NSInteger)age name:(NSString *)name {
    NSLog(@"+init");
    return [[self alloc] initWithAge:age name:name];
}

- (void)run {
    NSLog(@"%zd %@ -run", _age, _name);
}

+ (void)run {
    NSLog(@"Person +run");
}

- (void)eat:(NSString *)food other:(NSString *)other {
    NSLog(@"%zd %@ -eat %@ %@", _age, _name, food, other);
}

+ (void)eat:(NSString *)food other:(NSString *)other {
    NSLog(@"Person +eat %@ %@", food, other);
}

@end
  1. 在Swift文件中使用导入的OC类。
var p = DBPerson(age: 10, name: "Jack") // 输出:-init
p.age = 18
p.name = "Rose"
p.run() // 输出:18 Rose -run
p.eat("Apple", other: "Water") // 输出:18 Rose -eat Apple Water

DBPerson.run() // 输出:Person +run
DBPerson.eat("Pizza", other: "Banana") // 输出:Person +eat Pizza Banana

print(sum(10, 20)) // 输出:30

5.3. 修改C函数名

如果C语言暴露给Swift的函数名和Swift中的其他函数名冲突了,可以在Swift中使用@_silgen_name修改C函数名。

示例代码:

// C
int sum(int a, int b) {
    return a + b;
}

// Swift
@_silgen_name("sum") 
func swift_sum(_ v1: Int32, _ v2: Int32) -> Int32
print(swift_sum(10, 20)) // 输出:30
print(sum(10, 20)) // 输出:30

注意Swift的函数参数类型一定要和C中原方法参数类型一致(C中的int对应Swift中的Int32)。

可以使用@_silgen_name调用底层私有API(谨慎使用)。

六、OC调用Swift

Xcode已经默认生成一个用于OC调用Swift的头文件,文件名格式是:{targetName-Swift.h}(固定格式)。

Swift暴露给OC的类最终继承自NSObject

  • 使用@objc修饰需要暴露给OC的成员
  • 使用@objcMembers修饰类
    • 代表默认所有成员都会暴露给OC(包括扩展中定义的成员)
    • 最终是否成功暴露,还需要考虑成员自身的访问级别

6.1. 调用Swift

Car.swift

import Foundation

@objcMembers class Car: NSObject {
    var price: Double
    var band: String
    init(price: Double, band: String) {
        self.price = price
        self.band = band
    }
    func run() {
        print(price, band, "run")
    }
    static func run() {
        print("Car run")
    }
}

extension Car {
    func test() {
        print(price, band, "test")
    }
}

OC调用Swift

#import "SwiftDemo-Swift.h"

void testSwift() {
    Car *car = [[Car alloc] initWithPrice:2.0 band:@"BMW"];
    [car test]; // 输出:2.0 BMW test
    [car run]; // 输出:2.0 BMW run
    [Car run]; // 输出:Car run
}

SwiftDemo-Swift.h

Xcode会根据Swift代码自动生成对应的OC声明,写入{targetName-Swift.h}文件。

提示:如果Swift代码写完之后发现在OC中无法提示或找不到,需要编译一下项目。不要修改{targetName-Swift.h}文件,因为这个文件内容是编译后自动生成的。

6.2. 重命名

可以通过@objc重命名Swift暴露给OC的符号名(类名、属性名、函数名等)。

Swift代码:

@objc(DBCar)
@objcMembers class Car: NSObject {
    var price: Double
    @objc(name)
    var band: String
    init(price: Double, band: String) {
        self.price = price
        self.band = band
    }
    @objc(drive)
    func run() {
        print(price, band, "run")
    }
    static func run() {
        print("Car run")
    }
}

extension Car {
    @objc(exec:v2:)
    func test(a: Int, b: Int) {
        print(price, band, "test")
    }
}

OC使用:

DBCar *car = [[DBCar alloc] initWithPrice:2.0 band:@"BMW"];
car.name = @"Benz";
car.price = 98.0;

[car drive]; // 输出:98.0 Benz run
[car exec:10 v2:20]; // 输出:98.0 Benz test
[DBCar run]; // 输出:Car run

SwiftDemo-Swift.h

6.3. 选择器

Swift中依然可以使用选择器,使用#selector(name)定义一个选择器。必须是被@objcMembers@objc修饰的方法才可以定义选择器。

示例代码:

@objcMembers class Person: NSObject {
    func test1(v1: Int) {
        print("test1")
    }
    func test2(v1: Int, v2: Int) {
        print("test2(v1:v2:)")
    }
    func test2(_ v1: Double, _ v2: Double) {
        print("test2(_:_:)")
    }
    func run() {
        perform(#selector(test1))
        perform(#selector(test1(v1:)))
        perform(#selector(test2(v1:v2:)))
        perform(#selector(test2(_:_:)))
        perform(#selector(test2 as (Double, Double) -> Void))
    }
}

如果没有函数重载,选择器的函数名称后面不需要写参数列表。

Swift是没有runtime概念的,所以只能是暴露给OC的成员才可以使用选择器。

6.4. 思考

  1. 为什么Swift暴露给OC的类最终要继承自NSObject

    • 因为这个类最终是要给OC使用的,OC的所有类最终都继承自NSObject
  2. p.run()底层是怎么调用的(走OC的runtime还是Swift的虚表)?反过来,OC调用Swift底层又是如何调用的?

    • OC调用Swift,Swift代码由于生成了OC代码,所以还是走runtime流程的,也就意味着必然有isa指针,而isa来自NSObject
    • Swift调用OC,最终还是走runtime。就算被@objcMembers修饰,Swift代码之间的调用还是虚表。
    • 如果Swift中的类成员(函数)必须使用OC的runtime实现时,可以将@objc替换为dynamic

以上是关于Swift系列二十九 - 从OC到Swift的主要内容,如果未能解决你的问题,请参考以下文章

Swift系列二十七 - 字符串

Swift 实践:从 OC 到 Swift

swift语言点评十九-类型转化与检查

从 OC 到 Swift 的快速入门与专业实践

Swift系列二十八 - 数组

OC与Swift混编