作为一个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
标签:
#if MYDEBUG
print("DEBUG 模式下执行此代码")
#endif
#if TESTDEBUG
print(...)
#endif
二: 打印
在OC
开发中,我们会使用宏定义让NSLog
在Debug
模式下有效,在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
代码,需要三个步骤:
- 创建一个文件名为
{targetName}-Bridging-Header.h
的桥接文件.targetName
就是你的工程名.
- 在
Build Settings -> Objective-C Bridging Header
中写明桥接文件路径.
- 在桥接文件中导入
OC
暴露给Swift
的类.
做好以上三步后,就可以在swift
中调用OC
类了.
如上图所示,现在已经可以在swift
中调用OC
类的方法了.
需要注意的是, 在OC
类中有一个-
初始化方法和一个+
初始化方法,但是在swift
中调用初始化方法只会调用-
的.不会调用+
的.即使把OC
中的-
初始化方法声明注释掉,swift
也不会调用+
初始化方法.
五: @_silgen_name( )
如果有的C语言函数没有暴露给我们,但是我们又想调用,可以使用@_silgen_name( )
给这个方法重命名,然后再调用.比如OC
的Person.m
中有一个sum
方法,没有在.h
头文件中暴露给外界,我们仍然可以调用它:
@_silgen_name( )
很有用,系统没有暴露的C语言方法都可以通过它来调用.但是注意只能是C语言方法.
六: OC 调用 Swift
如果想在OC
文件中调用Swift
的内容,同样也需要一个桥接文件.这个桥接文件的命名方式为{targetName-Swift.h}
,但是这个桥接文件不需要我们创建,Xcode
默认已经创建好了.
要想在OC
中成功调用Swift
内容,需要2步:
Swift
暴露给OC
的类最终要继承NSObject
要暴露给
OC
的成员需要用@objc
修饰;或者使用@objcMembers
修饰类,代表所有成员都暴露给OC
.
做完以上两步后,我们就可以在OC
类中调用Swift
的东西了:
我们在做完以上两步后,Xcode
会把Swift
代码生成对应的OC声明
,写入到{targetName-Swift.h}
文件中:
到目前为止我们已经可以在OC
与Swift
之间互相调用了.我们知道OC
调用方法是通过runtime
,Swift
调用方法是通过函数虚表.那我们在OC
中调用Swift
,或者在Swift
中调用OC
.到底走的是哪一套流程呢?
6.1: OC 调用 Swift
在OC
中调用Swift
方法,看看其汇编底层:
可以看到,在OC
中调用Swift
方法,其底层走的是runtime
的那一套流程.
6.2 : Swift 调用 OC
汇编底层:
6.3 : Swift 类继承自 NSObject,但是在 Swift 文件中调用
以上两种情况,不管是Swift
调用OC
,还是OC
调用Swift
方法,底层都是走runtime
机制.
如果是Swift
代码继承自NSObject
,暴露给OC
.但是是在Swift
文件中调用的,这时候会走哪套流程呢?
汇编底层如下:
可以看到,即使继承自NSObject
,但是在Swift
中调用Swift
自己的方法,仍然走的是方法虚表流程.
在OC
开发中,类名方法名经常会使用一些前缀.而Swift
编码没有这些规范要求.所以如果我们在OC
中调用Swift
.可以使用@objc
重命名Swift
暴露给OC
的类名,方法名,属性名:
七: 选择器( Selector )
Swift
也可以使用方法选择器,但是有两个前提:
必须是继承自 NSObject 的类
必须是被 @objc 或者 @objcMembers 修饰的方法才可以定义选择器
如图:
八: String
Swift
中的String
和OC
的NSString
在API
的设计上有很大的差异.
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
String
和NSString
可以互相转换:
var str1: String = "good"
var str2: NSString = "better"
var str3 = str1 as NSString
var str4 = str2 as String
判断两个字符串内容是否相同,可以使用==
运算符,也可以使用isEqual
方法.
如果是OC
的NSString
使用==
判断两个字符串是否相同,它们的本质还是调用isEqual
方法:
Swfit
的String
使用==
判断是否相等,观察汇编语言没有发现调用isEqual
方法.
需要注意的是String
可以和NSString
互相桥接转换.
但是String
不可以和NSMutableString
互相转换,具体的说就是NSMutableString
可以转换为String
;而String
不可以转换为NSMutableString
九: 只能被类遵守的协议
有时候在开发中,我们想让一个协议只能被类遵守,不能被结构体和枚举遵守.有三种方式方法可以达到这种效果.
//AnyObject
protocol Setable1: AnyObject{
}
//class
protocol Setable2: class{
}
//@objc
@objc protocol Setable3{
}
被@objc
修饰的协议还暴露给OC
遵守.
十: 可选协议
之前我们讲协议的时候说过,可以通过扩展为协议添加默认实现,从而达到可选协议的效果:
现在又多了一种方法,通过@objc
定义可选协议,并且这种协议只能被类遵守:
十一: @objc dynamic
@objc dynamic
修饰的内容会具有动态性,比如说objc dynamic
如果修饰Swift
方法,那么即使在Swift
文件内调用Swift
方法,仍然会走runtime
那一套流程.
十二: KVC,KVO
在swift
开发中依然可以使用KVC , KVO
,只不过不能像在OC
中那样直接使用,必须满足两个条件:
- 属性所在的类,监听器必须继承自
NSObject
- 用
@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")
上一种方法需要创建一个继承自NSObject
的Observer
类,还有一种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
}()