Swift5.0 - day10- 从 OC 到 Swift
一、OC 到 Swift 基础差异
-
1.1、提示符:MARK、TODO、FIXME、 #warning("")
-
<1>、
// MARK:
类似于OC中的#pragma mark
// MARK: 测试 func test() -> () { }
// MARK:` 类似于OC中的 `#pragma mark
-
<2>、
// MARK: -
类似于OC中的#pragma mark -
// MARK: - 上面的方法 // MARK: 方法一 func test() -> () { } // MARK: - 下面的方法 // MARK: 方法一 func test3() -> () { }
// MARK: -` 类似于OC中的 `#pragma mark -
-
<3>、
// TODO:
用于标记未完成的任务func test2() -> () { // TODO: 未完成的任务 }
// TODO: 用于标记未完成的任务
-
<4>、
// FIXME:
用于标记待修复的问题func test3() -> () { // FIXME: 待修复 }
// FIXME: 用于标记待修复的问题
-
<5>、
#warning("")
警告信息`#warning("")` 警告信息
-
-
1.2、条件编译
// 操作系统: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
- 自定义条件编译
-
系统默认的是
DEBUG
,但是我们可以改名字,根据自己的需要了// debug模式 #if DEBUG // release模式 #else #endif #if TEST print("test") #endif #if OTHER print("other") #endif
-
- 自定义条件编译
-
1.3、DUBUG 和 Release 模式下的打印
-
OC 中
#ifdef DEBUG #define JKLog(...) NSLog(@"%s 第%d行: %@\n\n",__func__,__LINE__,[NSString stringWithFormat:__VA_ARGS__]) #else #define JKLog(...) #endif
-
Swift 中
/// 自定义打印 /// - Parameter msg: 打印的内容 /// - Parameter file: 文件路径 /// - Parameter line: 打印内容所在的函数 /// - Parameter fn: 打印内容的函数名 func JKLog<T>(_ msg: T, file: NSString = #file, line:Int = #line, fn: String = #function) { #if DEBUG let prefix = "------\n当前文件是:\(file.lastPathComponent)\n第 \(line) 行\n函数名:\(fn)\n打印内容:msg\n------" print(prefix) #endif }
-
-
1.4、系统版本检测
- 对于iOS平台,只在iOS10及以上版本执行
- 对于macOS平台,只在macOS 10.12及以上版本执行
- 最后的*表示在其他所有平台都执行
if #available(iOS 10, macOS 10.12, *) { }
-
1.5、API 可用性说明
@available(iOS 10, macOS 10.15, *) class Person {} struct Student { // 方法更名 @available(*, unavailable, renamed: "study") func study_() {} func study() {} // 在某个系统下废弃了 @available(iOS, deprecated: 11) @available(macOS, deprecated: 10.12) func run() {} }
- 方法更名:
@available(*, unavailable, renamed: "study")
study_() 方法改为 study() 方法 - 更多的说明方法可以 参考官方
- 方法更名:
-
1.6、拓展小技巧
-
当我们在写一个函数的时候,可能里面不知道些什么,又不想让它报错,我们可以在作用域内写上:
fatalError()
func testDo() -> Int { fatalError() }
-
二、Swift 项目说明
-
2.1、iOS 程序的入口
-
在AppDelegate上面默认有个
@UIApplicationMain
标记,这表示:编译器自动生成入口代码(main函数代码),自动设置AppDelegate
为APP的代理 -
也可以删掉
@UIApplicationMain
,自定义入口代码:新建一个main.swift
文件,如下:JKUIApplication
使我们自定义的入口import UIKit class JKUIApplication: UIApplication {} UIApplicationMain(CommandLine.argc, CommandLine.unsafeArgv, NSStringFromClass(JKUIApplication.self), NSStringFromClass(UIApplication.self))
-
-
2.2、Swift 调用 OC
- 先建一个桥接文件:直接见一个OC类,Xcode会直接提示我们创建一个桥接文件,文件名格式默认为:
{targetName}-Bridging-Header.h
桥接文件
文件名格式默认为: `{targetName}-Bridging-Header.h`
bridging 路径 - 先建一个桥接文件:直接见一个OC类,Xcode会直接提示我们创建一个桥接文件,文件名格式默认为:
-
在
{targetName}-Bridging-Header.h
文件中#import OC需要暴露给Swift的内容,如下-
自定义类 Animal, 其中 .h 和 .m文件内容如下
// .h文件 #import <Foundation/Foundation.h> NS_ASSUME_NONNULL_BEGIN int sum(int a, int b); @interface Animal : 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 NS_ASSUME_NONNULL_END // .m 文件 #import "Animal.h" @implementation Animal - (instancetype)initWithAge:(NSInteger)age name:(NSString *)name { if (self = [super init]) { self.age = age; self.name = name; } return self; } + (instancetype)personWithAge:(NSInteger)age name:(NSString *)name { return [[self alloc] initWithAge:age name:name]; } + (void)run { NSLog(@"Animal +run"); } - (void)run { NSLog(@"%zd %@ -run", _age, _name); } + (void)eat:(NSString *)food other:(NSString *)other { NSLog(@"Animal +eat %@ %@", food, other); } - (void)eat:(NSString *)food other:(NSString *)other { NSLog(@"%zd %@ -eat %@ %@", _age, _name, food, other); } @end int sum(int a, int b) { return a + b; }
-
在 {targetName}-Bridging-Header.h 桥接文件里面 导入 OC的文件,如下
#import "Animal.h"
-
调用 Swift 调用 OC 代码
let p = Animal(age: 10, name: "Jack") p.age = 18 p.name = "Rose" // 18 Rose -run p.run() // 18 Rose -eat Apple Water p.eat("Apple", other: "Water") // Animal +run Animal.run() // Animal +eat Pizza Banana Animal.eat("Pizza", other: "Banana") // 30 print(sum(10, 20))
-
Swift 调用
@_silgen_name
如果我们在C 语言有一个方法和 swift 里面的方法重名,那么在调用的时候,在Swift项目里面会 优先调用 swift 里面的方法,如果我们也想调在Swift里面调用C的方法,我们可以使用@_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
-
-
2.3、OC 调用 Swift
Xcode已经默认生成一个用于OC调用Swift的头文件,文件名格式是:{targetName}-Swift.h
-
(1)、在 OC 的文件里面导入
{targetName}-Swift.h
文件, -
(2)、要求 Swift要在OC里面使用的类继承于
NSObject
,如果 Swift 类里面的某个成员或者方法我们想要暴露给外面,就要在 某个成员或者方法 前面加@objc
-
(3)、使用
@objcMembers
修饰类
代表默认所有成员都会暴露给OC(包括扩展中定义的成员)
最终是否成功暴露,还需要考虑成员自身的访问级别@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") } }
提示:Xcode会根据Swift代码生成对应的OC声明,写入{targetName}-Swift.h 文件
-
上述代码在编译后就会在
{targetName}-Swift.h
文件 中生成如下代码@interface Car : NSObject @property (nonatomic) double price; @property (nonatomic, copy) NSString * _Nonnull band; - (nonnull instancetype)initWithPrice:(double)price band:(NSString * _Nonnull)band OBJC_DESIGNATED_INITIALIZER; - (void)run; + (void)run; - (nonnull instancetype)init SWIFT_UNAVAILABLE; + (nonnull instancetype)new SWIFT_UNAVAILABLE_MSG("-init is unavailable"); @end @interface Car (SWIFT_EXTENSION(JKSwiftDemo)) - (void)test; @end
在 OC 文件中使用Swift的类,先导入
#import "targetName-Swift.h"
#import "targetName-Swift.h" Car *c = [[Car alloc] initWithPrice:10.5 band:@"BMW"]; c.band = @"Bently"; c.price = 108.5; [c run]; // 108.5 Bently run [c test]; // 108.5 Bently test [Car run]; // Car run
-
通过
@objc
重命名Swift暴露给OC的符号名(类名、属性名、函数名等),如下@objc(JKCar) @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(newTest) func test() { print(price, band, "test") } }
在OC里面的调用
JKCar *c = [[JKCar alloc] initWithPrice:10.5 band:@"BMW"]; c.name = @"Bently"; c.price = 108.5; [c drive]; // 108.5 Bently run [JKCar run]; // Car run
-
-
-
2.4、根据2.2和2.3 列举的几个问题
- 1.为什么 Swift 暴露给 OC 的类最终要继承自 NSObject?
答:因为 在OC 里面用类 ,必然继承于 NSObject,在OC里面调用方法必然还要用到 runtime 那套流程,里面牵扯到 isa指针,isa指针来自NSObject - 2.p.run()底层是怎么调用的?反过来,OC调用 Swift 底层又是如何调用?
答:前面的:OC的东西在Swift里面调用,我们可以看到调用了runtime那套机制;后面的:Swift的东西在OC里面调用,打断点看汇编可以发现调用的也是runtime那套机制 - 3.Swift 里面的 car.run() 底层是怎么调用的?
答:走的是Swift那套流程,如果我们强行让它走OC那套runtime机制,可以在 run() 函数前加dynamic
- 1.为什么 Swift 暴露给 OC 的类最终要继承自 NSObject?
-
2.5、选择器(Selector)
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)) } }
三、字符串
-
3.1、Swift的字符串类型String,跟OC的NSString,在API设计上还是有较大差异
-
空字符串
var emptyStr1 = "" var emptyStr2 = String()
-
字符串前缀和后缀的判断
var str = "123456" print(str.hasPrefix("123")) // true print(str.hasSuffix("456")) // true
-
其他用法
var str: String = "1" // 拼接,jack_rose str.append("_2") // 重载运算符 + str = str + "_3" // 重载运算符 += str += "_4" // \()插值 str = "\(str)_5" // 长度,9,1_2_3_4_5 print(str.count)
-
-
3.2、String的插入和删除
var str = "1_2" // 插入 单个字符,结果是:1_2_ str.insert("_", at: str.endIndex) // 插入 字符串,结果是:1_2_3_4 str.insert(contentsOf: "3_4", at: str.endIndex) // 在某个索引后面插入,结果是:1666_2_3_4 str.insert(contentsOf: "666", at: str.index(after: str.startIndex)) // 在某个索引后面插入,结果是:1666_2_3_8884 str.insert(contentsOf: "888", at: str.index(before: str.endIndex)) // 在某个索引后面插入,偏移索引,结果是:1666hello_2_3_8884 str.insert(contentsOf: "hello", at: str.index(str.startIndex, offsetBy: 4)) // 删除值为1的第一个索引的值,,结果是:666hello_2_3_8884 str.remove(at: str.firstIndex(of: "1")!) // 删除值为字符为 6 的字符,结果是:hello_2_3_8884 str.removeAll { $0 == "6" } //删除某个区间的字符 var range = str.index(str.endIndex, offsetBy: -4)..<str.index(before: str.endIndex) // hello_2_3_4 str.removeSubrange(range)
-
3.3、Substring 子串
-
String可以通过下标、 prefix、 suffix等截取子串,子串类型不是String,而是Substring
-
Substring和它的base,共享字符串数据
-
Substring发生修改 或者 转为String时,会分配新的内存存储字符串数据,也就是深度拷贝
var str = "1_2_3_4_5" // 1_2 var substr1 = str.prefix(3) // 4_5 var substr2 = str.suffix(3) // 1_2 var range = str.startIndex..<str.index(str.startIndex, offsetBy: 3) var substr3 = str[range] // 最初的String,1_2_3_4_5 print(substr3.base) // Substring -> String var str2 = String(substr3)
-
prefix(3)
代表从 头 截取三位 -
suffix(3)
代表从 尾 截取三位 - 子串在没有进行修改前 和 原字符串公用一块内存,在子串进行修改后,那么就要进行深度拷贝了
-
-
-
3.4、String 与 Character
for c in "jack" { // c是Character类型 print(c) } var str = "jack" // c是Character类型 var c = str[str.startIndex]
-
3.5、String 相关的协议
- BidirectionalCollection 协议包含的部分内容
- startIndex 、 endIndex 属性、index 方法
- String、Array 都遵守了这个协议
- RangeReplaceableCollection 协议包含的部分内容
- append、insert、remove 方法
- String、Array 都遵守了这个协议
- Dictionary、Set 也有实现上述协议中声明的一些方法,只是并没有遵守上述协议
- BidirectionalCollection 协议包含的部分内容
-
3.6、多行String
-
放在 三个双引号之间的代表是多行,如下
let str = """ 1 "2" 3 '4' """
-
如果需要显示三个 引号,至少转义一个引号
let str = """ Escaping the first quote \""" Escaping two quotes \"\"" Escaping all three quotes \"\"\" """
-
缩进以结尾的 三引号为对齐线
let str = """ 1 "2" 3 '4' """
-
以下两个字符串是等价的
let str1 = "These are the same." let str2 = """ These are the same. """
-
-
3.7、String 与 NSString
-
String 与 NSString 之间可以随时随地桥接转换
-
如果你觉得String的API过于复杂难用,可以考虑将String转为NSString
var str1: String = "jack" var str2: NSString = "rose" var str3 = str1 as NSString var str4 = str2 as String // OC的使用 var str5 = str3.substring(with: NSRange(location: 0, length: 2)) print(str5)
-
比较字符串内容是否等价
String使用 == 运算符 NSString使用 isEqual 方法,也可以使用 == 运算符(本质还是调用了isEqual方法)
-
Swift、OC桥接转换表
Swift、OC桥接转换表
- 提示:不可以由
不可变
强转成可变的
- 提示:不可以由
-
四、OC 与 Swift 其他的不同点
-
4.1、只能被class继承的协议
protocol Runnable1: AnyObject {} protocol Runnable2: class {} @objc protocol Runnable3 {}
被
@objc
修饰的协议,还可以暴露给OC去遵守实现 -
4.2.可选协议
-
第一种:可以通过
@objc
定义可选协议,这种协议只能被class 遵守@objc protocol Runnable { func run1() @objc optional func run2() func run3() } class Dog: Runnable { func run3() { print("Dog run3") } func run1() { print("Dog run1") } } var d = Dog() d.run1() // Dog run1 d.run3() // Dog run3
-
第二种:我们可以通过扩展,如下Dog类就不需要实现
run2()
,因为扩展中已经实现protocol Runnable { func run1() func run2() } extension Runnable { func run2(){ } } class Dog: Runnable { func run1() { print("Dog run1") } } var d = Dog() d.run1() // Dog run1
-
-
4.3、dynamic
被@objc dynamic
修饰的内容会具有动态性,比如调用方法会走runtime那一套流程class Dog: NSObject { @objc dynamic func test1() {} func test2() {} } var d = Dog() d.test1() d.test2()
-
4.4、KVO / KVC
-
Swift 支持 KVC \ KVO 的条件 ,必须满足以下条件
-
(1)、属性所在的类、监听器最终继承自 NSObject
-
(2)、 用
@objc dynamic
修饰对应的属性class Observer: NSObject { override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { print("observeValue", change?[.newKey] as Any) } } class Person: NSObject { @objc dynamic var age: Int = 0 var observer: Observer = Observer() override init() { super.init() self.addObserver(observer, forKeyPath: "age", options: .new, context: nil) } deinit { self.removeObserver(observer, forKeyPath: "age") } } var p = Person() // observeValue Optional(20) p.age = 20 // observeValue Optional(25) p.setValue(25, forKey: "age")
-
-
4.5、Block式的KVO
class Person: NSObject { @objc dynamic var age: Int = 0 var observation: NSKeyValueObservation? override init() { super.init() observation = observe(\Person.age, options: .new) { (person, change) in print(change.newValue as Any) } } } var p = Person() // Optional(20) p.age = 20 // Optional(25) p.setValue(25, forKey: "age")
-
4.6、关联对象
-
在Swift中,class依然可以使用关联对象
-
默认情况,extension不可以增加存储属性 ,借助关联对象,可以实现类似extension为class增加存储属性的效果
class Person {} extension Person { private static var AGE_KEY: Void? var age: Int { get { (objc_getAssociatedObject(self, &Self.AGE_KEY) as? Int) ?? 0 } set { objc_setAssociatedObject(self, &Self.AGE_KEY, newValue, .OBJC_ASSOCIATION_ASSIGN) } } } var p = Person1() print(p.age) // 0 p.age = 10 print(p.age) // 10
提示:唯一的存储空间
private static var AGE_KEY: Void?
,我们使用Void和Bool 都是 1 个存储空间,节省内存
-
-
4.7、资源名管理
-
平时的做法:直接加载图片名字或者按钮的名字,如下
let img = UIImage(named: "logo") let btn = UIButton(type: .custom) btn.setTitle("添加", for: .normal)
-
优化后的做法,先定义一个资源枚举 JKResource
enum JKResource { /// 按钮名字 enum string: String { case add = "添加" } /// 图片名字 enum image: String { case logo } enum segue: String { case login_main } } // 调用 let img = UIImage(named: JKResource.image.logo.rawValue) let btn = UIButton(type: .custom) btn.setTitle(JKResource.string.add.rawValue, for: .normal)
- 提示:这种做法实际上是参考了Android的资源名管理方式
-
通过扩展进一步管理资源名
extension UIImage { convenience init?(_ name: R.image) { self.init(named: name.rawValue) } } extension UIButton { func setTitle(_ title: R.string, for state: UIControl.State) { setTitle(title.rawValue, for: state) } }
-
资源名管理的其他思路
enum JKResource { enum image { static var logo = UIImage(named: "logo") } enum font { static func arial(_ size: CGFloat) -> UIFont? { UIFont(name: "Arial", size: size) } } } // 使用如下 let img = JKResource.image.logo let font = JKResource.font.arial(14)
更多优秀的思路参考如下
-
五、多线程
-
5.1、多线程开发-异步
public typealias Task = () -> Void public struct JKAsyncs { public static func async(_ task: @escaping Task) { _async(task) } public static func async(_ task: @escaping Task, _ mainTask: @escaping Task) { _async(task, mainTask) } private static func _async(_ task: @escaping Task, _ mainTask: Task? = nil) { let item = DispatchWorkItem(block: task) DispatchQueue.global().async(execute: item) if let main = mainTask { item.notify(queue: DispatchQueue.main, execute: main) } } } // 调用如下 JKAsyncs.async({ print(Thread.current) // 自线程 }) { print(Thread.current) // 主线程 }
提示:开辟线程任务可能是在大括号之外完成所以加上
@escaping
:逃逸闭包-
DispatchWorkItem
的使用,子线程和其他线程分开,更加的直观let item = DispatchWorkItem { print(Thread.current) } DispatchQueue.global().async(execute: item) item.notify(queue: DispatchQueue.main) { print(Thread.current) }
-
-
5.2、多线程开发-主线程延迟
-
平时的用法
let seconds:Double = 5 let item = DispatchWorkItem { print("\(seconds)秒后打印",Thread.current) } DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + seconds, execute: item)
-
写到封装的
JKAsyncs
结构体里面@discardableResult public static func delay(_ seconds: Double, _ block: @escaping Task) -> DispatchWorkItem { let item = DispatchWorkItem(block: block) DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + seconds, execute: item) return item }
提示:
@discardableResult
代表在使用的时候我们可以忽略函数的返回值,也就是可以不用接收返回值- 返回值itme的作用:
item.cancel()
取消 方法的执行,也就是取消延迟 - 延迟操作是在子线程
- 返回值itme的作用:
-
-
5.3、多线程开发-异步延迟
@discardableResult public static func asyncDelay(_ seconds: Double, _ task: @escaping Task) -> DispatchWorkItem { return _asyncDelay(seconds, task) } @discardableResult public static func asyncDelay(_ seconds: Double, _ task: @escaping Task, _ mainTask: @escaping Task) -> DispatchWorkItem { return _asyncDelay(seconds, task, mainTask) } private static func _asyncDelay(_ seconds: Double, _ task: @escaping Task, _ mainTask: Task? = nil) -> DispatchWorkItem { let item = DispatchWorkItem(block: task) DispatchQueue.global().asyncAfter(deadline: DispatchTime.now() + seconds, execute: item) if let main = mainTask { item.notify(queue: DispatchQueue.main, execute: main) } return item }
-
5.4、多线程开发 - once:一次性代码,
dispatch_once
在 Swift 中已被废弃,取而代之 如下方式-
可以用类型属性或者全局变量\常量(整个程序启动后只有一份内存)
fileprivate let initTask2: Void = { print("initTask2---------") }() class ViewController: UIViewController { static let initTask1: Void = { print("initTask1---------") }() override func viewDidLoad() { super.viewDidLoad() let _ = Self.initTask1 let _ = initTask2 let _ = Self.initTask1 let _ = initTask2 } }
打印结果
initTask1--------- initTask2---------
提示:默认自带
lazy + dispatch_once
效果- 第一个字母大写的
Self
代表当前的类 - 懒加载的属性里面只会走一次
- 第一个字母大写的
-
-
5.5、多线程开发-加锁(线程同步技术,防止资源抢夺)
-
第一种锁:gcd 信号量
class Cache { private static var data = [String: Any]() // 设置信号量的锁 private static var lock = DispatchSemaphore(value: 1) static func get(_ key: String) -> Any? { data[key] } static func set(_ key: String, _ value: Any) { // 加锁 lock.wait() defer { // 解锁 lock.signal() } data[key] = value } }
-
NSLock锁
class Cache { private static var data = [String: Any]() private static var lock = NSLock() static func get(_ key: String) -> Any? { data[key] } static func set(_ key: String, _ value: Any) { // 加锁 lock.lock() defer { // 解锁 lock.unlock() } data[key] = value }
-
递归锁:如果一个调用存在调用自身(递归),那么我们就是用递归锁:
NSRecursiveLock()
,加锁解锁和上面 NSLock锁 一样
-