Swift 知识小集

以下内容均是笔者学习过程中收集的知识点,顺序比较跳跃,初衷是为了方便查阅,顺便加深记忆。内容会不断更新,如果有什么问题或者有好的 Swift 方面的语法糖或者知识点也可以提出来,我会挑选斟酌后收录,欢迎大家关注~

环境:

Swift 4.0
Xcode 9.1

最近更新:2018.01.09

取消 asyncAfter 中延迟的事件
for in 和 forEach 的区别
访问级别
几种遍历方式
weakSelf 和 strongSelf

更新:2017.12.13

优雅的定义通知名称

更新:2017.12.11

标签语句:指定跳出某个条件语句
倒序 reversed()
作用域:do 语句块

更新:2017.12.08

Swift 中的 “readonly”
自定义日志输出
Swift 中的 “@synchronized”

目录:

Associated Object
Delegate 声明为 weak
可选协议和协议扩展
单例
输出格式化
Selector
将 protocol 的方法声明为 mutating
数组遍历 enumerate
输入输出参数 inout
Default 参数
延迟加载 lazy
编译标记
换行符
字符串切割 split
KVC
Swift 中值类型和引用类型注意点
KVO
Swift UIButton 状态的叠加
Swift 中的 “@synchronized”
自定义日志输出
Swift 中的 “readonly”
作用域:do 语句块
倒序 reversed()
标签语句:指定跳出某个条件语句
优雅的定义通知名称
weakSelf 和 strongSelf
几种遍历方式
访问级别
for in 和 forEach 的区别
取消 asyncAfter 中延迟的事件

Associated Object

Objective-C 的 runtime 里的 Associated Object 允许我们在使用 Category 扩展现有的类的功能的时候,直接添加实例变量。在 Swift 中 extension 不能添加存储属性,我们可以利用 Associated Object 来实现,比如下面的 title 「实际上」是一个存储属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// MyClass.swift
class MyClass {}
// MyClassExtension.swift
private var key: Void?
extension MyClass {
var title: String? {
get {
return swift_getAssociatedObject(self, &key) as? String
}
set {
swift_setAssociatedObject(self,
&key, newValue,
.swift_ASSOCIATION_RETAIN_NONATOMIC)
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 测试
func printTitle(_ input: MyClass) {
if let title = input.title {
print("Title: \(title)")
} else {
print("没有设置")
}
}
let a = MyClass()
printTitle(a)
a.title = "Swifter.tips"
printTitle(a)
// 输出:
// 没有设置
// Title: Swifter.tips”

Delegate 声明为 weak

Swift 中 Delegate 需要被声明成 weak,来避免访问到已被回收的内存而导致崩溃,如果我们像下面这样,是编译不过的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
protocol MyClassDelegate {
func method()
}
class MyClass {
weak var delegate: MyClassDelegate?
}
class ViewController: UIViewController, MyClassDelegate {
// ...
var someInstance: MyClass!
override func viewDidLoad() {
super.viewDidLoad()
someInstance = MyClass()
someInstance.delegate = self
}
func method() {
print("Do something")
}
//...
}
// 编译失败
// 'weak' may only be applied to class and class-bound protocol types, not 'MyClassDelegate'

这是因为 Swift 的 protocol 是可以被除了 class 以外的其他类型遵守的,而对于像 struct 或是 enum 这样的类型,本身就不通过引用计数来管理内存,所以也不可能用 weak 这样的 ARC 的概念来进行修饰。

想要在 Swift 中使用 weak delegate,我们就需要将 protocol 限制在 class 内:

  • 一种做法是将 protocol 声明为 Objective-C 的,这可以通过在 protocol 前面加上 @objc 关键字来达到,Objective-C 的 protocol 都只有类能实现,因此使用 weak 来修饰就合理了:
1
2
3
@objc protocol MyClassDelegate {
func method()
}
  • 另一种可能更好的办法是在 protocol 声明的名字后面加上 class,这可以为编译器显式地指明这个 protocol 只能由 class 来实现,避免了过多的不必要的 Objective-C 兼容:
1
2
3
protocol MyClassDelegate: class {
func method()
}

可选协议和协议扩展

Objective-C 中的 protocol 里存在 @optional 关键字,被这个关键字修饰的方法并非必须要被实现,原生的 Swift protocol 里没有可选项,所有定义的方法都是必须实现的,如果不是实现是无法编译的:

1
2
3
4
class ViewController: UIViewController,MyProtocol { }
// 编译失败
// Type 'ViewController' does not conform to protocol 'MyProtocol'

如果我们想要像 Objective-C 里那样定义可选的协议方法,就需要将协议本身和可选方法都定义为 Objective-C 的,也即在 protocol 定义之前加上 @objc,方法之前加上 @objc optional

1
2
3
@objc protocol MyProtocol {
@objc optional func myMethod()
}

另外,对于所有的声明,它们的前缀修饰是完全分开的,也就是说你不能像是在 Objective-C 里那样用一个 @optional 指定接下来的若干个方法都是可选的了,必须对每一个可选方法添加前缀,对于没有前缀的方法来说,它们是默认必须实现的:

1
2
3
4
5
@objc protocol MyProtocol {
@objc optional func optionalMethod() // 可选
func necessaryMethod() // 必须
@objc optional func anotherOptionalMethod() // 可选
}

一个不可避免的限制是,使用 @objc 修饰的 protocol 就只能被 class 实现了,也就是说,对于 structenum 类型,我们是无法令它们所实现的协议中含有可选方法或者属性的。另外,实现它的 class 中的方法还必须也被标注为 @objc,或者整个类就是继承自 NSObject。对于这种问题,在 Swift 2.0 中,我们有了另一种选择,那就是使用 protocol extension。我们可以在声明一个 protocol 之后再用 extension 的方式给出部分方法默认的实现,这样这些方法在实际的类中就是可选实现的了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
protocol MyProtocol {
func optionalMethod() // 可选
func necessaryMethod() // 必须
func anotherOptionalMethod() // 可选
}
extension MyProtocol {
//默认的可选实现
func optionalMethod() {
print("optionalMethod")
}
//默认的可选实现
func anotherOptionalMethod() {
print("anotherOptionalMethod")
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class ViewController: UIViewController,MyProtocol {
// 必须的实现
func necessaryMethod() {
print("necessaryMethod")
}
override func viewDidLoad() {
super.viewDidLoad()
self.optionalMethod();
self.necessaryMethod();
self.anotherOptionalMethod();
}
}
// 输出:
// optionalMethod
// necessaryMethod
// necessaryMethod

单例

Swift 中的单例非常简单,Swift 1.2 以及之后:

1
2
3
4
class Singleton {
static let sharedInstance = Singleton()
private init() {}
}

这种写法不但是线程安全的,也是懒加载的,let 定义的属性本身就是线程安全的,同时 static 定义的是一个 class constant,拥有全局作用域和懒加载特性。

另外,这个类型中加入了一个私有的初始化方法,来覆盖默认的公开初始化方法,这让项目中的其他地方不能够通过 init 来生成自己的 Singleton 实例,也保证了类型单例的唯一性。如果你需要的是类似 default 的形式的单例 (也就是说这个类的使用者可以创建自己的实例) 的话,可以去掉这个私有的 init 方法。

输出格式化

在 Objective-C 中的 %@ 这样的格式在指定的位置设定占位符,然后通过参数的方式将实际要输出的内容补充完整。例如 Objective-C 中常用的向控制台输出的 NSLog 方法就使用了这种格式化方法:

1
2
3
4
5
6
float a = 1.234567;
NSString *b = @"Helllo";
NSLog(@"float:%.2f str:%p",a,b);
// 输出:
// float:1.23 str:0x1024a1078

对应 Swift 中我们可以这样:

1
2
3
4
5
6
7
let a = 1.234567
let b = "Helllo"
let c = String(format:"float:%.2f str:%p",a,b)
print(c)
// 输出:
// float:1.23 str:0x604000249e10

Selector

@selector 是 Objective-C 时代的一个关键字,它可以将一个方法转换并赋值给一个 SEL 类型,它的表现很类似一个动态的函数指针。在 Swift 中没有 @selector 了,取而代之,从 Swift 2.2 开始我们使用 #selector 来从暴露给 Objective-C 的代码中获取一个 selector,并且因为 selector 是 Objective-C runtime 的概念,在 Swift 4 中,默认情况下所有的 Swift 方法在 Objective-C 中都是不可见的,所以你需要在这类方法前面加上 @objc 关键字,将这个方法暴露给 Objective-C,才能进行使用:

1
2
3
4
5
6
7
8
9
10
11
let btn = UIButton.init(type: .system)
btn.backgroundColor = UIColor.red
btn.frame = CGRect(x: 100, y: 100, width: 150, height: 40)
btn.setTitle("Button", for: .normal)
//无参数
btn.addTarget(self, action: #selector(btnClick), for: .touchUpInside)
view.addSubview(btn)
@objc func btnClick() {
print("button click !")
}
1
2
3
4
5
6
7
8
...
//有参数
btn.addTarget(self, action: #selector(btnClick(_ :)), for: .touchUpInside)
...
@objc func btnClick(_ button: UIButton) {
print("button click !")
}

将 protocol 的方法声明为 mutating

Swift 的 protocol 不仅可以被 class 类型实现,也适用于 structenum,因为这个原因,我们在写给别人用的协议时需要多考虑是否使用 mutating 来修饰方法。Swift 的 mutating 关键字修饰方法是为了能在该方法中修改 struct 或是 enum 的变量,所以如果你没在协议方法里写 mutating 的话,别人如果用 struct 或者 enum 来实现这个协议的话,就不能在方法里改变自己的变量了,比如下面的代码是编译不过的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
protocol Vehicle {
func changeColor()
}
struct MyCar: Vehicle {
var color = "blue"
func changeColor() {
color = "red"
}
}
// 编译失败
// Cannot assign to property: 'self' is immutable

我们应该加上 mutating 关键字:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
protocol Vehicle {
mutating func changeColor()
}
struct MyCar: Vehicle {
var color = "blue"
mutating func changeColor() {
color = "red"
}
}
override func viewDidLoad() {
super.viewDidLoad()
var car = MyCar()
print(car.color)
car.changeColor()
print(car.color)
}
// 输出:
// blue
// 输出:
// red

数组遍历 enumerate

使用 NSArray 时一个很常遇见的的需求是在枚举数组内元素的同时也想使用对应的下标索引,在 Objective-C 中最方便的方式是使用 NSArray 的 enumerateObjectsUsingBlock: ,在 Swift 中存在一个效率,安全性和可读性都很好的替代,那就是快速枚举某个数组的EnumerateGenerator,它的元素是同时包含了元素下标索引以及元素本身的多元组:

1
2
3
4
5
6
7
8
9
10
11
let arr = ["a","b","c","d","e"]
for (idx, str) in arr.enumerated() {
print("idx: \(idx) str: \(str)")
}
// 输出:
idx: 0 str: a
idx: 1 str: b
idx: 2 str: c
idx: 3 str: d
idx: 4 str: e

输入输出参数 inout

函数参数默认是常量,如果试图在函数体中更改参数值将会导致编译错误,比如下面的例子中尝试着交换值:

1
2
3
4
5
6
7
8
9
func swapTwoInts(_ a: Int, _ b: Int) {
let temporaryA = a
a = b
b = temporaryA
}
// 编译失败
// Cannot assign to value: 'a' is a 'let' constant
// Cannot assign to value: 'b' is a 'let' constant

如果想要一个函数可以修改参数的值,并且想要在这些修改在函数调用结束后仍然存在,那么就应该把这个参数定义为输入输出参数(In-Out Parameters):

1
2
3
4
5
func swapTwoInts(_ a: inout Int, _ b: inout Int) {
let temporaryA = a
a = b
b = temporaryA
}

Default 参数

Swift 的方法是支持默认参数的,也就是说在声明方法时,可以给某个参数指定一个默认使用的值。在调用该方法时要是传入了这个参数,则使用传入的值,如果缺少这个输入参数,那么直接使用设定的默认值进行调用。和其他很多语言的默认参数相比较,Swift 中的默认参数限制更少,并没有所谓 “默认参数之后不能再出现无默认值的参数”这样的规则,举个例子,下面两种方法的声明在 Swift 里都是合法可用的:

1
2
3
4
5
6
7
func sayHello1(str1: String = "Hello", str2: String, str3: String) {
print(str1 + str2 + str3)
}
func sayHello2(str1: String, str2: String, str3: String = "World") {
print(str1 + str2 + str3)
}
1
2
3
4
sayHello1(str2: " ", str3: "World")
sayHello2(str1: "Hello", str2: " ")
//输出都是 Hello World

延迟加载 lazy

延时加载或者说延时初始化是很常用的优化方法,在构建和生成新的对象的时候,内存分配会在运行时耗费不少时间,如果有一些对象的属性和内容非常复杂的话,这个时间更是不可忽略。另外,有些情况下我们并不会立即用到一个对象的所有属性,而默认情况下初始化时,那些在特定环境下不被使用的存储属性,也一样要被初始化和赋值,也是一种浪费。在 Objective-C 中,一个延迟加载一般是这样的:

1
2
3
4
5
6
7
8
9
10
11
// ClassA.h
@property (nonatomic, copy) NSString *testString;
// ClassA.m
- (NSString *)testString {
if (!_testString) {
_testString = @"Hello";
NSLog(@"只在首次访问输出");
}
return _testString;
}

对应在 Swift 中,使用 lazy 作为属性修饰符时,只能声明属性是变量,且我们需要显式地指定属性类型,否则会编译错误:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class ClassA {
lazy let str: String = {
let str = "Hello"
print("只在首次访问输出")
return str
}()
}
// 编译失败
// 'lazy' cannot be used on a let
class ClassA {
lazy var str = {
let str = "Hello"
print("只在首次访问输出")
return str
}()
}
// 编译失败
// Unable to infer complex closure return type

我们应该声明为 var 并指定好类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class ClassA {
lazy var str: String = {
let str = "Hello"
print("只在首次访问输出")
return str
}()
}
override func viewDidLoad() {
super.viewDidLoad()
let ca = ClassA()
print(ca.str)
print(ca.str)
print(ca.str)
}
// 输出:
// 只在首次访问输出
// Hello
// Hello
// Hello

如果不需要做什么额外工作的话,也可以对这个 lazy 的属性直接写赋值语句:

1
lazy var str: String = "Hello"

我们还可以利用 lazy 配合像 map 或是 filter 这类接受闭包并进行运行的方法一起,让整个行为变成延时进行的。在某些情况下这么做也对性能会有不小的帮助。例如,直接使用 map 时:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
let data = 1...3
let result = data.map {
(i: Int) -> Int in
print("正在处理 \(i)")
return i * 2
}
print("准备访问结果")
for i in result {
print("操作后结果为 \(i)")
}
print("操作完毕")
// 输出:
// 正在处理 1
// 正在处理 2
// 正在处理 3
// 准备访问结果
// 操作后结果为 2
// 操作后结果为 4
// 操作后结果为 6
// 操作完毕

而如果我们先进行一次 lazy 操作的话,我们就能得到延时运行版本的容器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
let data = 1...3
let result = data.lazy.map {
(i: Int) -> Int in
print("正在处理 \(i)")
return i * 2
}
print("准备访问结果")
for i in result {
print("操作后结果为 \(i)")
}
print("操作完毕")
// 准备访问结果
// 正在处理 1
// 操作后结果为 2
// 正在处理 2
// 操作后结果为 4
// 正在处理 3
// 操作后结果为 6
// 操作完毕

对于那些不需要完全运行,可能提前退出的情况,使用 lazy 来进行性能优化效果会非常有效。

编译标记

在 Objective-C 中,我们经常在代码中插入 #param 符号来标记代码的区间,这样在 Xcode 的导航栏中我们就可以看到组织分块后的方法列表。在 Swift 中我们可以用 MARK: 来代替:

在 Objective-C 中还有一个很常用的编译标记,那就是 #warning,一个 #warning 标记可以在 Xcode 的代码编辑器中显示为明显的黄色警告条,非常适合用来提示代码的维护者和使用者需要对某些东西加以关注。在 Swift 中我们可以用 FIXME:TODO: 配合 shell 来代替:

脚本:

1
2
3
TAGS="TODO:|FIXME:"
echo "searching ${SRCROOT} for ${TAGS}"
find "${SRCROOT}" \( -name "*.swift" \) -print0 | xargs -0 egrep --with-filename --line-number --only-matching "($TAGS).*\$" | perl -p -e "s/($TAGS)/ warning: \$1/"

效果:


换行符

在 Swift 3 中,需要换行时是需要 \n

1
2
3
4
5
let str = "xxxx\nxxx"
// 输出:
// xxxx
// xxx

在 swift 4 中,我们可以使用 """

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let jsonStr = """
{
"id": 123455,
"nickname": "xxxx",
"isMale": true,
"birthday": "2000年3月24日",
"personalURL": "https://xxxxxx.github.io"
}
"""
// 输出:
{
"id": 123455,
"nickname": "xxxx",
"isMale": true,
"birthday": "2000年3月24日",
"personalURL": "https://xxxxxx.github.io"
}

字符串切割 split

我们需要切割某个字符串时可以用 split 方法,需要注意的是,返回的结果是个数组

1
2
3
4
5
let str = "Hello,world !"
print(str.split(separator: ","))
// 输出:
// ["Hello", "world !"]

KVC

1
2
3
class MyClass {
var name = "ifelseboyxx"
}

Swift 4 中 Apple 引入了新的 KeyPath 的表达方式,现在,对于类型 MyClass 中的变量 name,对应的 KeyPath 可以写为 \MyClass.name,利用 KVC 修改 name 值的话,我们可以这么操作:

1
2
3
4
5
6
7
8
9
10
let object = MyClass()
print("name: \(object.name)")
// set
object[keyPath: \MyClass.name] = "ifelseboy"
// get
print("name: \(object[keyPath: \MyClass.name])")
// 输出:
// name: ifelseboyxx
// name: ifelseboy

另外 Swift 4 中 struct 同样支持 KVC :

1
2
3
4
5
6
7
8
9
10
11
12
struct MyStruct {
var age: Int
}
var obj = MyStruct(age: 18)
print("我今年 \(obj.age) 岁了")
obj[keyPath: \MyStruct.age] = 8
print("我今年 \(obj[keyPath: \MyStruct.age]) 岁了")
// 输出:
// 我今年 18 岁了
// 我今年 8 岁了

Swift 中值类型和引用类型注意点

KVC 一节中代码里有个注意点:

1
2
3
var obj = MyStruct(age: 18)
//替换为
let obj = MyStruct(age: 18)

是编译不过的,会报错:

1
Cannot assign to immutable expression of type 'Int'

笔者初次也犯了这样的错误,想当然的认为 MyClasslet 声明的是没有问题的,struct 也一样:

1
let object = MyClass()

其实原因很简单,swift 中 Class 是引用类型的,而 struct 是值类型的:值类型在被赋给一个变量,或被传给函数时,实际是做了一次拷贝。引用类型在被赋给一个变量,或被传给函数时,传递的是引用。

KVO

很遗憾,依然只有 NSObject 才能支持 KVO,另外由于 Swift 为了效率,默认禁用了动态派发,因此想用 Swift 来实现 KVO,我们还需要做额外的工作,那就是将想要观测的对象标记为 dynamic@objc,下面的 🌰 是 ViewController 监听 MyClassdate 属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class MyClass: NSObject {
@objc dynamic var date = Date()
}
class ViewController: UIViewController {
var myObject: MyClass!
var observation: NSKeyValueObservation?
override func viewDidLoad() {
super.viewDidLoad()
myObject = MyClass()
print("当前日期:\(myObject.date)")
observation = myObject.observe(\MyClass.date, options: [.old,.new], changeHandler: { (_, change) in
if let newDate = change.newValue , let oldDate = change.oldValue {
print("日期发生变化 old:\(oldDate) new:\(newDate) ")
}
})
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 1) {
self.myObject.date = Date()
}
}
}
// 输出:
// 当前日期:2017-12-07 06:31:26 +0000
// 日期发生变化 old:2017-12-07 06:31:26 +0000 new:2017-12-07 06:31:27 +0000

在 Objective-C 中我们几乎可以没有限制地对所有满足 KVC 的属性进行监听,而现在我们需要属性有 dynamic@objc 进行修饰。大多数情况下,我们想要观察的类包含这两个修饰 (除非这个类的开发者有意为之,否则一般也不会有人愿意多花功夫在属性前加上它们,因为这毕竟要损失一部分性能),并且有时候我们很可能也无法修改想要观察的类的源码。遇到这样的情况的话,一个可能可行的方案是继承这个类并且将需要观察的属性使用 dynamic@objc 进行重写。比如刚才我们的 MyClass 中如果 date 没有相应标注的话,我们可能就需要一个新的 MyChildClass了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class MyClass: NSObject {
var date = Date()
}
class MyChildClass: MyClass {
@objc dynamic override var date: Date {
get { return super.date }
set { super.date = newValue }
}
}
class ViewController: UIViewController {
var myObject: MyChildClass!
var observation: NSKeyValueObservation?
override func viewDidLoad() {
super.viewDidLoad()
myObject = MyChildClass()
print("当前日期:\(myObject.date)")
observation = myObject.observe(\MyChildClass.date, options: [.old,.new], changeHandler: { (_, change) in
if let newDate = change.newValue , let oldDate = change.oldValue {
print("日期发生变化 old:\(oldDate) new:\(newDate) ")
}
})
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 1) {
self.myObject.date = Date()
}
}
}
// 输出:
// 当前日期:2017-12-07 06:36:50 +0000
// 日期发生变化 old:2017-12-07 06:36:50 +0000 new:2017-12-07 06:36:51 +0000

Swift UIButton 状态的叠加

在 Objective-C 中,如果我们想叠加按钮的某个状态,可以这么写:

1
2
UIButton * button = [UIButton buttonWithType:UIButtonTypeCustom];
[button setTitle:@"Test" forState:UIControlStateNormal | UIControlStateSelected];

对应的 Swift 我们可以这么写:

1
2
let btn = UIButton.init(type: .custom)
btn.setTitle("hehe", for: [.normal ,.selected])

把需要叠加的状态用个数组装起来就行了。

Swift 中的 “@synchronized”

在 Objective-C 中,我们可以用 @synchronized 这个关键字可以用来修饰一个变量,并为其自动加上和解除互斥锁。这样,可以保证变量在作用范围内不会被其他线程改变:

1
2
3
4
5
- (void)myMethod:(id)anObj {
@synchronized(anObj) {
// 在括号内持有 anObj 锁
}
}

虽然这个方法很简单好用,但是很不幸的是在 Swift 中它已经 (或者是暂时) 不存在了。其实 @synchronized 在幕后做的事情是调用了 objc_sync 中的 objc_sync_enterobjc_sync_exit 方法,并且加入了一些异常判断。因此,在 Swift 中,如果我们忽略掉那些异常的话,我们想要 lock 一个变量的话,可以这样写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func myMethod(anObj: AnyObject!) {
objc_sync_enter(anObj)
// 在 enter 和 exit 之间持有 anObj 锁
objc_sync_exit(anObj)
}
```
更进一步,如果我们喜欢以前的那种形式,甚至可以写一个全局的方法,并接受一个闭包,来将 `objc_sync_enter` 和 `objc_sync_exit` 封装起来:
```swift
func synchronized(_ lock: AnyObject, closure: () -> ()) {
objc_sync_enter(lock)
closure()
objc_sync_exit(lock)
}
// 使用:
synchronized(self) {
}

这样使用起来就和 Objective-C 中 @synchronized 很像了。

再举个 🌰 ,如果我们想为某个类实现一个线程安全的 setter,可以这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Obj {
var _str = "123"
var str: String {
get {
return _str
}
set {
synchronized(self) {
_str = newValue
}
}
}
}

自定义日志输出

在 Objective-C 中,我们通常会自定义日志输出来完善信息以及避免 release 下的输出,比如下面这种,可以额外提供行数、方法名等信息:

1
2
3
4
5
#ifdef DEBUG
#define XXLog(fmt, ...) NSLog((@"%s [Line %d] " fmt), __PRETTY_FUNCTION__, __LINE__, ##__VA_ARGS__)
#else
#define XXLog(...)
#endif
1
2
3
4
5
6
7
8
9
10
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// Override point for customization after application launch.
XXLog(@"ifelseboyxx");
return YES;
}
// 输出:
// 2017-12-08 13:32:02.211306+0800 Demo[17902:88775537] -[AppDelegate application:didFinishLaunchingWithOptions:] [Line 28] ifelseboyxx

在 Swift 中,我们可以这样自定义:

1
2
3
4
5
6
7
8
9
func xxprint<T>(_ message: T, filePath: String = #file, line: Int = #line, function: String = #function) {
#if DEBUG
let fileName = (filePath as NSString).lastPathComponent.replacingOccurrences(of: ".Swift", with: "")
let dateFormatter = DateFormatter()
dateFormatter.locale = Locale.current
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
print("[" + dateFormatter.string(from: Date()) + " " + fileName + " " + function + " \(line)" + "]:" + "\(message)")
#endif
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
xxprint("ifelseboyxx")
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 1) {
xxprint("ifelseboyxx")
}
}
}
// 输出:
// [2017-12-08 13:49:38 ViewController.swift viewDidLoad() 27]:ifelseboyxx
// [2017-12-08 13:49:39 ViewController.swift viewDidLoad() 29]:ifelseboyxx

Swift 中的 “readonly”

在 Objective-C 中,我们通常把属性声明为 readonly 来提醒别人:“不要修改!!”,通常这么写:

1
2
3
4
5
@interface Person : NSObject
@property (nonatomic, readonly, copy) NSString *name;
@end

如果外部尝试修改的话,会编译错误:

1
2
3
4
5
6
7
8
9
10
- (void)viewDidLoad {
[super viewDidLoad];
Person *p = [Person new];
p.name = @"ifelseboyxx";
}
// 编译错误:
// Assignment to readonly property

有些情况下,我们希望内部可以点语法访问 name 属性,也就是 self.name,但是因为是 readonly 的,会编译错误:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@implementation Person
- (instancetype)init {
self = [super init];
if (self) {
self.name = @"ifelseboyxx";
}
return self;
}
@end
// 编译错误:
// Assignment to readonly property

这时候我们就会在内部的 extension 重新声明一个 readwrite的同样的属性,也就是“外部只读,内部可写”

1
2
3
4
5
@interface Person ()
@property (nonatomic, readwrite, copy) NSString *name;
@end

在 Swift 中,我们可能有同样的场景。这里就不得不提到 privatefileprivate 关键字了。
private表示声明为私有的实体只能在其声明的范围内被访问。比如我在 MyClass 中声明了一个私有的 name 属性,外部访问的话会编译错误:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class MyClass {
private var name: String = "Test"
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let only = MyClass()
print(only.name)
only.name = "ifelseboyxxv587"
}
}
// 编译异常:
// 'name' is inaccessible due to 'private' protection level

fileprivate,看命名我们大概能猜到,就是将对实体的访问权限于它声明的源文件。通俗点讲,比如我上面的代码都是在 ViewController.swift 这个文件里的,我把 private 修改为 fileprivate,就不会编译错误了:

1
2
3
class MyClass {
fileprivate var name: String = "Test"
}

那么如果非 ViewController.swift 文件,也想访问 MyClassname 属性该怎么办呢?我们可以把 name 属性声明为 fileprivate(set),就要就达到类似 Objective-C 中的 readonly 效果了 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// ViewController.swift 文件
class MyClass {
fileprivate(set) var name: String = "Test"
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let only = MyClass()
print(only.name)
only.name = "ifelseboyxxv587"
print(only.name)
}
}
// 编译正常,ViewController.swift 文件内可读可写
// 输出:
// Test
// ifelseboyxxv587
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// AppDelegate.swift 文件
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
let only = MyClass()
print(only.name) //只能读
only.name = "ifelseboyxxv587" //这里报错,不能写
return true
}
// 编译异常:
// Cannot assign to property: 'name' setter is inaccessible

作用域:do 语句块

在 Objective-C 中,我们可以利用 {} 来开辟新的作用域,来避免对象名称重复的问题:

1
2
3
4
5
6
7
8
9
10
11
12
NSString *ha = @"测试一";
{
NSString *ha = @"测试二";
NSLog(@"%@",ha);
}
NSLog(@"%@",ha);
// 输出:
// 2017-12-11 16:55:20.303132+0800 Demo[48418:93027416] 测试二
// 2017-12-11 16:55:20.303316+0800 Demo[48418:93027416] 测试一

在 Swift 中,取代 {} 的是 do {}

1
2
3
4
5
6
7
8
9
10
11
12
let ha = "测试一"
do {
let ha = "测试二"
print(ha)
}
print(ha)
// 输出:
// 测试二
// 测试一

倒序 reversed()

在 Objective-C 中,我们如果想倒序数组一般这样:

1
2
3
4
5
6
7
8
9
10
NSArray *array = @[@"1",@"2",@"3"];
NSArray *reversedArray = [[array reverseObjectEnumerator] allObjects];
// 输出:
// 2017-12-11 17:39:57.127466+0800 Demo[49004:93210504] (
3,
2,
1
)

在 Swift 中,相对简单点:

1
2
3
4
5
let arr:[String] = ["1","2","3"]
let reversedArr:[String] = arr.reversed()
// 输出:
// ["3", "2", "1"]

标签语句:指定跳出某个条件语句

在 Objective-C 中,如果遇到多层嵌套的条件语句,我们如果想要指定跳出某个条件语句是很不方便的。比如有两个循环,一旦找到它们相同的,就立刻停止循环,我们可能会这么做:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
NSArray *arr1 = @[@"1",@"2",@"3",@"4",@"5"];
NSArray *arr2 = @[@"4",@"6",@"8",@"9",@"2"];
BOOL finded = NO;
for (NSString *x in arr1) {
if (finded) {
break;
}
NSLog(@"x:%@",x);
for (NSString *y in arr2) {
NSLog(@"y:%@",y);
if ([x isEqualToString:y]) {
NSLog(@"找到相等的了:%@",x);
finded = YES;
break;
}
}
}

我们需要借助 finded 这个 BOOL,来方便我们跳出循环。在 Swift 中,我们就可以利用标签语句,来指定具体跳出哪个循环,语法是这样的:

1
2
3
标签名: 条件语句 {
}

上面的 🌰 我们可以这么写:

1
2
3
4
5
6
7
8
9
10
11
12
13
let arr1 = ["1","2","3","4","5"]
let arr2 = ["4","6","8","9","2"]
label: for x in arr1 {
print("x: \(x)")
for y in arr2 {
print("y: \(y)")
if x == y {
print("找到相等的了:\(y)")
break label
}
}
}

上面代码,我们把第一层循环定义了标签:label。在第二层循环中,一旦条件成立,立刻跳出第一层循环 label。这个特性,可以说十分方便了!

优雅的定义通知名称

在 Objective-C 中,我们自定义通知时,对于名称的定义一般都有规范:

1
2
3
4
5
// xxx.h
UIKIT_EXTERN NSString * const XXXXNotification;
// xxx.m
NSString * const XXXXNotification = @"XXXXNotification";

在 Swift 中,我们可以参考 Alamofire 的方式,创建个专门存放通知名的文件,扩展 Notification.Name 并以结构体 struct 方式声明:

1
2
3
4
5
6
7
8
9
// XXNotification.swift 文件
import Foundation
extension Notification.Name {
public struct Task {
public static let 通知名 = Notification.Name(rawValue: "org.target名称.notification.name.task.通知名")
}
}

然后我们就可以愉快的使用了:

1
2
3
4
5
6
7
8
// add
NotificationCenter.default.addObserver(self, selector: #selector(myNotification(_ :)), name: NSNotification.Name.Task.通知名, object: self)
// post
NotificationCenter.default.post(name: NSNotification.Name.Task.通知名, object: self)
// remove
NotificationCenter.default.removeObserver(self, name: NSNotification.Name.Task.通知名, object: self)

weakSelf 和 strongSelf

在 Objective-C 中,为了防止 block 循环引用,我们通用利用 __weak__strong 搭配使用:

1
2
3
__weak typeof(self) weakSelf = self;
__strong typeof(weakSelf) strongSelf = weakSelf;

对应的 Swift 中我们可以这样:

1
2
3
4
5
6
7
resource.request().onComplete { [weak self] response in
guard let strongSelf = self else {
return
}
let model = strongSelf.updateModel(response)
strongSelf.updateUI(model)
}

几种遍历方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
for _ in 0..<3 {
print("Hello three times")
}
// 带索引
for (index, person) in attendeeList.enumerated() {
print("\(person) is at position #\(index)")
}
// 每隔两个打印
for index in stride(from: 0, to: 5, by: 2) {
print(index)
}
// 输出: 0 2 4
// 倒叙
for index in (0...3).reversed() {
print(index)
}
// 输出:3 2 1 0

访问级别

Swift 中的访问级别有以下五种

  • open:公开权限, 最高的权限,可以被其他模块访问,继承及复写。
  • public:公有访问权限,类或者类的公有属性或者公有方法可以从文件或者模块的任何地方进行访问。那么什么样才能成为一个模块呢?一个 App 就是一个模块,一个第三方 API,第三等方框架等都是一个完整的模块,这些模块如果要对外留有访问的属性或者方法,就应该使用 public 的访问权限。public 的权限在 Swift 3.0 后无法在其他模块被复写方法/属性或被继承。
  • fileprivate:文件私有访问权限,被 fileprivate 修饰的类或者类的属性或方法可以在同一个物理文件中访问。如果超出该物理文件,那么有着 fileprivate 访问权限的类,属性和方法就不能被访问。
  • private:私有访问权限,被 private 修饰的类或者类的属性或方法可以在同一个物理文件中的同一个类型(包含 extension)访问。如果超出该物理文件或不属于同一类型,那么有着 private 访问权限的属性和方法就不能被访问。
  • internal: 顾名思义,internal 是内部的意思,即有着 internal 访问权限的属性和方法说明在模块内部可以访问,超出模块内部就不可被访问了。在 Swift 中默认就是 internal 的访问权限。

for in 和 forEach 的区别

for in 能使用 returnbreakcontinue 关键字,forEach 不能使用 breakcontinue 关键字。它们的主要区别在于 return 关键字结果的不同:

for in 中,return 会导致循环终止:

1
2
3
4
5
6
7
8
9
10
11
let array = ["1", "2", "3", "4", "5"]
for element in array {
if element == "3" {
return
}
print(element)
}
// 输出:
// 1
// 2

forEach 会跳过当前循环,完成剩余的循环:

1
2
3
4
5
6
7
8
9
10
11
12
13
let array = ["1", "2", "3", "4", "5"]
array.forEach { (element) in
if element == "3" {
return
}
print(element)
}
// 输出:
// 1
// 2
// 4
// 5

这样看来,forEach 中的 return 关键字倒是有点类似 for in 中的 continue 了!

取消 asyncAfter 中延迟的事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class ViewController: UIViewController {
private let workItem = DispatchWorkItem {
print("test")
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 3, execute: workItem)
}
// 这里点击取消之前延迟的事情
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
print(workItem.isCancelled ? "取消了" : "未取消")
workItem.cancel()
print(workItem.isCancelled ? "取消了" : "未取消")
}
}
// 输出:
// 未取消
// 取消了