从零学习Swift 15: 从OC到Swift过渡

总结

作为一个iOS开发者,如何从OC过渡到Swift.今天我们就来讲解一下从OC开发转到Swift开发的注意点.

一: 条件编译

有时候我们要限制我们的代码在某些平台,某种架构,某一个语言版本下运行,这时候就用到了条件编译.

swift中的条件编译和OC中的一样:


#if os(macOS) || os(iOS)
print("在macOS 或者 iOS 平台下执行")
#elseif arch(x86_64) || arch(arm64)
print("x86 或者 arm64 架构下执行")
#elseif swift(>=5.0)
print("swift 版本要大于等于 5.0")
#elseif targetEnvironment(simulator)
print("在模拟器下执行")
#elseif canImport(Foundation)
print("如果能导入Foundation模块就执行")
#endif

debug , release条件编译:


#if DEBUG
print("debug 模式下执行此代码")
#else
print("release 模式下执行此代码")
#endif

我们也可以自定义DEBUG标签:

自定义DEBUG标签

#if MYDEBUG
print("DEBUG 模式下执行此代码")
#endif

#if TESTDEBUG
print(...)
#endif

二: 打印

OC开发中,我们会使用宏定义让NSLogDebug模式下有效,在Release模式下无效,比如这样:


#if DEBUG
#define DLOG(fmt, ...) NSLog((@"%s [Line %d] " fmt), __PRETTY_FUNCTION__, __LINE__, ##__VA_ARGS__);

#else
#define XLOG(fmt,...) {}

#endif

但是在swift中不支持宏定义,我们如何实现呢?

我们可以写一个方法,让其在Debug模式下打印,Release模式下不做任何事情:


func mylog<T>(_ msg: T,
            file: NSString = #file,
            line: Int = #line,
            fn: String = #function){
    #if DEBUG
    let str = "\(file.lastPathComponent)_ \(line) _ \(fn) _ \(msg)"
    print(str)
    #endif
}

三: API可用性说明

if #available(iOS 9.0,macOS 10.0, *){
    //对于iOS平台,只在iOS9及以上版本执行
    //对于macOS平台,只在macOS10及以上版本执行
    //*表示支持其他所有平台
}


@available(iOS 10.0,macOS 10.0,*)
class Person{
    //将run改名为fastRun
    @available(*,unavailable,renamed: "fastRun")
    func run(){
    }
    func fastRun(){
    }
    
    //从iOS 10,macOS 11 开始已经弃用此方法
    @available(iOS,deprecated: 10)
    @available(macOS,deprecated: 11)
    func eat(){
    }
}

四: swift 调用 OC

要想在swift项目中调用OC代码,需要三个步骤:

  1. 创建一个文件名为{targetName}-Bridging-Header.h的桥接文件.targetName就是你的工程名.
targetName
  1. Build Settings -> Objective-C Bridging Header中写明桥接文件路径.
桥接文件路径
  1. 在桥接文件中导入OC暴露给Swift的类.
导入用到的类

做好以上三步后,就可以在swift中调用OC类了.

OC类
在swift中调用OC

如上图所示,现在已经可以在swift中调用OC类的方法了.

需要注意的是, 在OC类中有一个-初始化方法和一个+初始化方法,但是在swift中调用初始化方法只会调用-的.不会调用+的.即使把OC中的-初始化方法声明注释掉,swift也不会调用+初始化方法.

五: @_silgen_name( )

如果有的C语言函数没有暴露给我们,但是我们又想调用,可以使用@_silgen_name( )给这个方法重命名,然后再调用.比如OCPerson.m中有一个sum方法,没有在.h头文件中暴露给外界,我们仍然可以调用它:

.m文件中没有暴露的方法
在 swift 中调用

@_silgen_name( )很有用,系统没有暴露的C语言方法都可以通过它来调用.但是注意只能是C语言方法.

六: OC 调用 Swift

如果想在OC文件中调用Swift的内容,同样也需要一个桥接文件.这个桥接文件的命名方式为{targetName-Swift.h},但是这个桥接文件不需要我们创建,Xcode默认已经创建好了.

要想在OC中成功调用Swift内容,需要2步:

  1. Swift暴露给OC的类最终要继承NSObject

  2. 要暴露给OC的成员需要用@objc修饰;或者使用@objcMembers修饰类,代表所有成员都暴露给OC.

swift类暴露给OC

做完以上两步后,我们就可以在OC类中调用Swift的东西了:

OC中调用Swift

我们在做完以上两步后,Xcode会把Swift代码生成对应的OC声明,写入到{targetName-Swift.h}文件中:

swift代码的OC声明

到目前为止我们已经可以在OCSwift之间互相调用了.我们知道OC调用方法是通过runtime,Swift调用方法是通过函数虚表.那我们在OC中调用Swift,或者在Swift中调用OC.到底走的是哪一套流程呢?

6.1: OC 调用 Swift

OC中调用Swift方法,看看其汇编底层:

可以看到,在OC中调用Swift方法,其底层走的是runtime的那一套流程.

6.2 : Swift 调用 OC

swift调用OC

汇编底层:

底层走的还是runtime机制

6.3 : Swift 类继承自 NSObject,但是在 Swift 文件中调用

以上两种情况,不管是Swift调用OC,还是OC调用Swift方法,底层都是走runtime机制.

如果是Swift代码继承自NSObject,暴露给OC.但是是在Swift文件中调用的,这时候会走哪套流程呢?

在swift中调用暴露给OC的swift方法

汇编底层如下:

可以看到,即使继承自NSObject,但是在Swift中调用Swift自己的方法,仍然走的是方法虚表流程.

OC开发中,类名方法名经常会使用一些前缀.而Swift编码没有这些规范要求.所以如果我们在OC中调用Swift.可以使用@objc重命名Swift暴露给OC的类名,方法名,属性名:

七: 选择器( Selector )

Swift也可以使用方法选择器,但是有两个前提:

  1. 必须是继承自 NSObject 的类
  2. 必须是被 @objc 或者 @objcMembers 修饰的方法才可以定义选择器

如图:

八: String

Swift中的StringOCNSStringAPI的设计上有很大的差异.

8.1 String 的拼接

Swift字符串的拼接很活,有很多拼接方法:


var str = "1"
//append拼接
str.append("_2")
//重载 +
str = str + "_3"
//重载 +=
str += "_4"
//插值
str = "\(str)_5"
print(str)
//长度
print(str.count)


8.2 插入和删除

OC中对字符串进行插入操作是通过insertString:(nonnull NSString *) atIndex:(NSUInteger),传入一个索引下标.而在Swift中,String引入了一个内部类型String.Index.

下面我们就好好认识一下String.Index

插入操作:

var str = "abc"
//str.startIndex : 在a的位置插入
str.insert(contentsOf: "1", at: str.startIndex)
//str.endIndex : 在c后面插入
str.insert(contentsOf: "2", at: str.endIndex)
//插入到第一个字符
str.insert(contentsOf: "ok", at: str.index(after: str.startIndex))
//插入到最后一个字符前面
str.insert(contentsOf: "666", at: str.index(before: str.endIndex))
//插入到从开始位置偏移4
str.insert(contentsOf: "888", at: str.index(str.startIndex, offsetBy: 4))

删除操作:


//删除第一个6
str.remove(at: str.firstIndex(of: "6")!)

//删除所有的6
str.removeAll {(c) -> Bool in
    c == "6"
}
print(str)
//删除一个区间范围的字符
let rang = str.index(str.startIndex, offsetBy: 4) ..< str.index(str.endIndex, offsetBy: -2)
str.removeSubrange(rang)


截取字符串:


var str = "123456789"
//删除前3个字符
var subStr1 = str.prefix(3)
print(subStr1)
//删除后3个字符
var subStr2 = str.suffix(3)
print(subStr2)
let range = str.index(str.startIndex, offsetBy: 3) ..< str.index(str.endIndex, offsetBy: -3)
var subStr3 = str[range]
print(subStr3)


String截取字符串返回的是Substring类型,并不是String类型.Substring可以通过base获取获取原来的字符串.

如果没有对Substring进行修改或者转换为String类型,那么Substring和它的base共享同一块内存数据.

8.3 多行字符串
Swift中可以用"""来定义多行字符串:

多行字符串

注意:多行字符串的作用域以最后一个"""为准,字符串内容不能越过最后一个"""的左边界:

8.4 String 与 NSString

StringNSString可以互相转换:


var str1: String = "good"
var str2: NSString = "better"

var str3 = str1 as NSString
var str4 = str2 as String

判断两个字符串内容是否相同,可以使用==运算符,也可以使用isEqual方法.

如果是OCNSString使用==判断两个字符串是否相同,它们的本质还是调用isEqual方法:

SwfitString使用==判断是否相等,观察汇编语言没有发现调用isEqual方法.

需要注意的是String可以和NSString互相桥接转换.
但是String不可以和NSMutableString互相转换,具体的说就是NSMutableString可以转换为String;而String不可以转换为NSMutableString

String 不可以通过 as 转换为 NSMutableString

九: 只能被类遵守的协议

有时候在开发中,我们想让一个协议只能被类遵守,不能被结构体和枚举遵守.有三种方式方法可以达到这种效果.


//AnyObject
protocol Setable1: AnyObject{
    
}
//class
protocol Setable2: class{
    
}
//@objc
@objc protocol Setable3{
    
}

@objc修饰的协议还暴露给OC遵守.

十: 可选协议

之前我们讲协议的时候说过,可以通过扩展为协议添加默认实现,从而达到可选协议的效果:

通过扩展实现可选协议

现在又多了一种方法,通过@objc定义可选协议,并且这种协议只能被类遵守:

@objc 实现可选协议

十一: @objc dynamic
@objc dynamic修饰的内容会具有动态性,比如说objc dynamic如果修饰Swift方法,那么即使在Swift文件内调用Swift方法,仍然会走runtime那一套流程.

@objc dynamic
走runtime机制

十二: KVC,KVO

swift开发中依然可以使用KVC , KVO,只不过不能像在OC中那样直接使用,必须满足两个条件:

  1. 属性所在的类,监听器必须继承自NSObject
  2. @objc dynamic修饰需要监听的属性.

class Observer: NSObject{
    
    override func observeValue(forKeyPath keyPath: String?,
                                     of object: Any?,
                                     change: [NSKeyValueChangeKey : Any]?,
                                     context: UnsafeMutableRawPointer?) {
        print("对象:\(String(describing: object)),属性:\(String(describing: keyPath)),新值:\(String(describing: change?[.newKey]))")
    }
}

class Teacher: NSObject{
    var name: String = ""
    @objc dynamic var age: Int = 3

    var observer: Observer = Observer()
    override init() {
        super.init()
        self.addObserver(observer, forKeyPath: "age", options: .new, context: nil)
        
    }

    deinit {
        self.removeObserver(observer, forKeyPath: "age")
    }

}

var teacher = Teacher()
teacher.age = 20
teacher.setValue(30, forKey: "age")

上一种方法需要创建一个继承自NSObjectObserver类,还有一种block方式实现KVO不需要创建Observer类:


class Teacher: NSObject{
    var name: String = ""
    @objc dynamic var age: Int = 3

    var observation: NSKeyValueObservation?
    override init() {
        super.init()
        
        observation = observe(\Teacher.age, options: .new) {
            (person, change) in
            print(change.newValue ?? 0)
        }
        
    }
}

var teacher = Teacher()
teacher.age = 35

第二种方式写法上要注意,要观察的类的属性书写格式为\Teacher.age

十三: 关联对象

我们知道通过扩展是不能给类添加存储属性的,因为存储属性保存在类的实例中.添加存储属性会影响到类的内存结构.

如果我们想要实现动态的给一个类添加存储属性,可以和OC一样使用关联对象:

十四: 资源名统一管理

我们在开发中会用到很多的图片资源,按钮标题,提示语,字体样式等等.在OC开发中通常会用宏定义文件把经常需要的文件,文案宏定义一下.这样我们在敲代码的时候会有提示,并且以后修改的话只需要修改宏定义文件即可.

那么在Swift中不支持宏定义.我们怎么实现这种效果呢?

我们可以像下面这样,使用枚举,把我们用到的文案列举出来:

使用枚举列举出文案

然后再调用我们自己扩展的方法:

调用自己扩展的方法

像上面的方法还绕了一个弯,我们可以更加直接点,直接在枚举中返回我们想要的东西:

直接返回需要的东西
直接使用

十五: 多线程

Swift中的多线程于OC中的大同小异.

1: 异步


        DispatchQueue.global().async {
            //子线程异步
            print("1 - ",Thread.current)

            //回到主线程
            DispatchQueue.main.async {
                print("2 - ",Thread.current)
            }
        }

可以封装成工具类,直接把任务传入进去:


typealias Task = () -> Void

struct Async{
    
    //在子线程中处理
    static func sync(_ task: @escaping Task){
        _sync(task)
    }
    
    //在子线程中处理完成后,回到主线程处理
    static func sync(_ task: @escaping Task, _ mainTask: @escaping Task){
        _sync(task, mainTask)
    }
    
    
    
    //可以接收异步线程,和主线程任务
    private static func _sync(_ task: @escaping Task, _ mainTask: Task? = nil){
        
        //创建item
        let item = DispatchWorkItem(block: task)
        //异步子线程执行item
        DispatchQueue.global().async(execute: item)
        
        if let main = mainTask{
            item.notify(queue: DispatchQueue.main, execute: main)
        }
    }
}

2: 延迟执行


    static func asyncDeleay(_ seconds: Double,
                     _ task: @escaping Task) -> DispatchWorkItem{
        _deleay(seconds, task)
    }
    
    static func asyncDeleay(_ seconds: Double,
                            _ task: @escaping Task,
                            _ mainTask: @escaping Task) -> DispatchWorkItem{
        _deleay(seconds, task, mainTask)
    }
    
    
    private static func _deleay(_ seconds: Double,
                                _ task: @escaping Task,
                                _ mainTask: Task? = nil) -> DispatchWorkItem{
        let time = DispatchTime.now() + seconds
        let item = DispatchWorkItem(block: task)
        DispatchQueue.global().asyncAfter(deadline: time, execute: item)
        if let main = mainTask{
            item.notify(queue: DispatchQueue.main, execute: main)
        }
        return item
    }

3: 多线程开发 once

swift中废弃了dispatch_once.所以要想实现单例,只能通过全局变量或者静态变量来实现.因为静态变量和全局变量在内存中只有一份,并且只会初始化一次,还是懒加载,用到的时候才初始化.


    static var once: Bool = {
        print("1")
        return true
    }()

全局变量和静态变量底层其实调用的是 dispatch_once
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345