第一部分:引言

第一章:前言

数据结构是一门经过充分研究的学科,其概念与语言无关; 来自C的数据结构在功能上和概念上与任何其他语言中的相同数据结构相同,例如Swift。 与此同时,Swift的高级表现力使其成为学习这些核心概念的理想选择,而不会牺牲太多的性能。

Swift标准库有一小组通用集合类型; 他们甚至没有涵盖每一个案例。正如您将看到的,这些 原始的类型可以用作构建更复杂和特殊用途构造的一个很好的起点。 了解比标准数组和字典更多的数据结构,您可以使用更多的工具来构建自己的应用程序。

第二章:Swift标准库

在开始构建自己的自定义数据结构之前,了解Swift标准库已经提供的主要数据结构非常重要。

字典缺乏明确的排序劣势,却又带来了一些其他的优点。 与数组不同,字典不需要担心元素的转移。 插入字典总是O(1)。 查找操作也在O(1)时间内完成,这比在需要O(n)扫描的数组中找到特定元素要快得多。

第三章:复杂度

常见的时间复杂度和空间复杂度。

对于计算机,算法的资源是内存。空间复杂度就意味着内存占用率。

总结

  • 时间复杂度衡量输入大小增加时运行算法所需的时间。

  • 空间复杂度衡量算法运行所需的资源。

  • Big O表示法用于表示时间和空间复杂性的一般形式。

  • 时间和空间复杂性是可扩展性的高级度量; 它们不测量算法本身的实际速度。

  • 对于小型数据集,时间复杂度通常无关紧要。 拟线性算法可能比线性算法慢。

第二部分:基本数据类型

第四章:链表

链表是以线性单向序列排列的值的集合。链表比连续存储选项(如Swift数组)具有几个理论上的优势:

  • 从列表前面插入和删除恒定时间。
  • 可靠的性能特征。

image-20190801154442688

如图所示,链表是一系列节点。 节点有两个职责:

1.保存一个值。

2.保存对下一个节点的引用。 空表示列表的结尾。image-20190801154554314

节点

1
2
3
4
5
6
7
8
9
10
public class Node<Value> {

public var value: Value
public var next: Node?

public init(value: Value, next: Node? = nil) {
self.value = value
self.next = next
}
}

链表

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
public struct LinkedList<Value> {

public var head: Node<Value>?
public var tail: Node<Value>?

public init() {}

public var isEmpty: Bool {
return head == nil
}

// MARK: - 增加和删除节点的方法

// 时间复杂度:O(1)
public mutating func push(_ value: Value) {
copyNodes()
head = Node(value: value, next: head)
if tail == nil {
tail = head
}
}

// 时间复杂度:O(1)
public mutating func append(_ value: Value) {
copyNodes()
guard !isEmpty else {
push(value)
return
}
tail!.next = Node(value: value)
tail = tail!.next
}

// 时间复杂度:O(i) , i 就是给定的index
public func node(at index: Int) -> Node<Value>? {
var currentNode = head
var currentIndex = 0
while currentNode != nil && currentIndex < index {
currentNode = currentNode!.next
currentIndex += 1
}
return currentNode
}

// 时间复杂度:O(1)
@discardableResult
public mutating func insert(_ value: Value, after node: Node<Value>) -> Node<Value> {
copyNodes()
guard tail !== node else {
append(value)
return tail!
}
node.next = Node(value: value, next: node.next)
return node.next!
}

// 时间复杂度:O(1)
@discardableResult
public mutating func pop() -> Value? {
copyNodes()
defer {
head = head?.next
if isEmpty {
tail = nil
}
}
return head?.value
}

// 时间复杂度:O(n)
@discardableResult
public mutating func removeLast() -> Value? {
copyNodes()
guard let head = head else {
return nil
}
guard head.next != nil else {
return pop()
}
var prev = head
var current = head
while let next = current.next {
prev = current
current = next
}
prev.next = nil
tail = prev
return current.value
}

// 时间复杂度:O(1)
@discardableResult
public mutating func remove(after node: Node<Value>) -> Value? {
copyNodes()
defer {
if node.next === tail {
tail = node
}
node.next = node.next?.next
}
return node.next?.value
}

// MARK: - 实现值类型和写入时复制
//使用COW实现价值语义的策略非常简单。在改变链接列表的内容之前,您希望执行底层存储的副本并将所有引用(头部和尾部)更新为新副本。
private mutating func copyNodes() {
guard !isKnownUniquelyReferenced(&head) else {
return
}
guard var oldNode = head else {
return
}

head = Node(value: oldNode.value)
var newNode = head

while let nextOldNode = oldNode.next {
newNode!.next = Node(value: nextOldNode.value)
newNode = newNode!.next
oldNode = nextOldNode
}

tail = newNode
}
}

// MARK: - 实现Swift的Collection协议

//Swift Collection还允许通过下标进行访问,这是一个很好的术语,用于表示索引可以映射到集合中的值。
extension LinkedList: Collection {

public struct Index: Comparable {

public var node: Node<Value>?

static public func ==(lhs: Index, rhs: Index) -> Bool {
switch (lhs.node, rhs.node) {
case let (left?, right?):
return left.next === right.next
case (nil, nil):
return true
default:
return false
}
}

static public func <(lhs: Index, rhs: Index) -> Bool {
guard lhs != rhs else {
return false
}
let nodes = sequence(first: lhs.node) { $0?.next }
return nodes.contains { $0 === rhs.node }
}
}

public var startIndex: Index {
return Index(node: head)
}

public var endIndex: Index {
return Index(node: tail?.next)
}

public func index(after i: Index) -> Index {
return Index(node: i.node?.next)
}

public subscript(position: Index) -> Value {
return position.node!.value
}
}

关键点

  • 链接列表是线性和单向的。 只要将引用从一个节点移动到另一个节点,就无法返回。
  • 链接列表的头部第一次插入具有O(1)时间复杂度。 对于头部第一次插入,数组有O(n)时间复杂度。
  • 符合Swift收集协议(如序列和集合),可为相当少的要求提供一系列有用的方法。
  • 写时复制行为使您可以实现值语义。

第五章:链表挑战

逆序打印所有节点元素

1
2
3
4
5
6
7
8
9
10
func printInReverse<T>(_ list: LinkedList<T>) {
//巧妙的递归:先迭代后打印,这样打印的就是逆序了
printInReverse(list.head)
}

private func printInReverse<T>(_ node: Node<T>?) {
guard let node = node else { return }
printInReverse(node.next)
print(node.value)
}

取中间节点

1
2
3
4
5
6
7
8
9
10
11
12
func getMiddle<T>(_ list: LinkedList<T>) -> Node<T>? {
//两个步长差一倍,这样快的遍历到终点,慢的刚好到中间
var fast = list.head
var slow = list.head

while let nextFast = fast?.next {
fast = nextFast.next
slow = slow?.next
}

return slow
}

反转链表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
extension LinkedList {
// 不断的分割链表,从头部开始,下一步时把之前分割的节点拼接在新的节点之后。
// 如此遍历到底,就实现了逆序。
// 核心思路:把每个节点的前节点变成后节点,同时通过遍历时分割成2~3个分段。
mutating func reverse() {
tail = head

var current = head?.next
// 临时存储逆序的链表
var prev = head
prev?.next = nil

while current != nil {
let next = current?.next
current?.next = prev
prev = current
current = next
}

head = prev
}
}

合并两个已经排序好的链表

例如:

1
2
3
4
5
// list1 1 -> 4 -> 10 -> 11

// list2 -1 -> 2 -> 3 -> 6

// merged list -1 -> 1 -> 2 -> 3 -> 4 -> 6 -> 10 -> 11
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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
func mergeSorted<T: Comparable>(_ left: LinkedList<T>,
_ right: LinkedList<T>) -> LinkedList<T> {
guard !left.isEmpty else {
return right
}

guard !right.isEmpty else {
return left
}

// 定义新的链表的首尾
var newHead: Node<T>?
var tail: Node<T>?

// 原有链表的游标,后面以此遍历
var currentLeft = left.head
var currentRight = right.head

// 初始设置
if let leftNode = currentLeft, let rightNode = currentRight {
if leftNode.value < rightNode.value {
newHead = leftNode
currentLeft = leftNode.next
} else {
newHead = rightNode
currentRight = rightNode.next
}
tail = newHead
}

// 遍历
while let leftNode = currentLeft, let rightNode = currentRight {
if leftNode.value < rightNode.value {
tail?.next = leftNode
currentLeft = leftNode.next
} else {
tail?.next = rightNode
currentRight = rightNode.next
}
tail = tail?.next
}

if let leftNode = currentLeft {
tail?.next = leftNode
}

if let rightNode = currentRight {
tail?.next = rightNode
}

// 处理新的链表的首尾
var list = LinkedList<T>()

list.head = newHead
// 找到真的尾部节点
list.tail = {
while let next = tail?.next {
tail = next
}
return tail
}()

return list
}

创建一个从链表中删除所有特定元素的函数

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
extension LinkedList where Value: Equatable {
mutating func removeAll(_ value: Value) {
// 头部的值相等时单独处理
while let head = self.head, head.value == value {
self.head = head.next
}

var current = head?.next
var prev = head

// 遍历
while let currentNode = current {
guard currentNode.value != value else {
prev?.next = currentNode.next
current = prev?.next
continue
}

prev = current
current = current?.next
}

tail = prev
}
}

在时间复杂度上优化,所以一般都是2~3个临时变量(牺牲一点空间复杂度)。相当于让更多人协同做一件事情,肯定更快。但是如果是资源有限的时候,比如说雇人做事,总的成本不一定会减少。但是通常情况下,电脑的内存容量是既定的,而且够用的情况下,就要优化时间复杂度,不用吝惜内存。

第六章:栈

栈无处不在。 以下是您要栈的一些常见示例:

  • 薄煎饼
  • 书籍
  • 纸张

在概念上,栈数据结构与物理栈相同。 将项目添加到栈时,将其放在栈顶部。 从栈中删除项目时,始终会删除最顶层的项目。

栈的操作

栈的操作只有两个:

  1. push:将元素添加到栈顶部。
  2. pop:删除栈的顶部元素。

这意味着您只能在数据结构的一侧添加或删除元素。在计算机科学中,栈被称为LIFO(后进先出)数据结构。最后推入的元素是第一个被弹出的元素。

实现

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
38
39
40
41
42
43
44
45
46
47
48
//泛型Element为存储的元素
public struct Stack<Element> {

//内部为数组存储所有元素
private var storage: [Element] = []

public init() { }

public init(_ elements: [Element]) {
storage = elements
}

public mutating func push(_ element: Element) {
storage.append(element)
}

@discardableResult
public mutating func pop() -> Element? {
return storage.popLast()
}

public func peek() -> Element? {
return storage.last
}

public var isEmpty: Bool {
return peek() == nil
}
}

extension Stack: CustomStringConvertible {
public var description: String {
let topDivider = "----top----\n"
let bottomDivider = "\n-----------"

let stackElements = storage
.map { "\($0)" }
.reversed()
.joined(separator: "\n")
return topDivider + stackElements + bottomDivider
}
}

extension Stack: ExpressibleByArrayLiteral {
public init(arrayLiteral elements: Element...) {
storage = elements
}
}

你可能想知道是否可以为栈采用Swift集合协议。栈的目的是限制访问数据的方式的数量,并采用诸如Collection之类的协议将通过迭代器和下标公开所有元素来违背此目标。在这种情况下,少即是多!

第七章:栈挑战

逆序打印链表,但是不能用递归

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
let list: LinkedList<Int> = {
var list = LinkedList<Int>()
list.push(3)
list.push(2)
list.push(1)
return list
}()

func printInReverse<T>(_ list: LinkedList<T>) {
var current = list.head
var stack = Stack<T>()

while let node = current {
stack.push(node.value)
current = node.next
}

while let value = stack.pop() {
print(value)
}
}

printInReverse(list)

检查括号的匹配

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var testString1 = "h((e))llo(world)()"
var testString2 = "(hello world"

func checkParentheses(_ string: String) -> Bool {
var stack = Stack<Character>()

for character in string {
if character == "(" {
stack.push(character)
} else if character == ")" {
if stack.isEmpty {
return false
} else {
stack.pop()
}
}
}
return stack.isEmpty
}

checkParentheses(testString1)
checkParentheses(testString2)

第八章:队列

我们都熟悉排队,无论是买票还是排队打印。

队列使用FIFO或先进先出顺序,这意味着添加的第一个元素将始终是第一个被删除的元素。当您需要维护元素的顺序以便稍后处理时,队列很方便。

定义

让我们先确定队列的协议:

1
2
3
4
5
6
7
8
9
10
public protocol Queue {

associatedtype Element
///在队列后面插入一个元素。如果操作成功,则返回true。
mutating func enqueue(_ element: Element) -> Bool
///删除队列前面的元素并将其返回。
mutating func dequeue() -> Element?
var isEmpty: Bool { get }
var peek: Element? { get }
}

在以下部分中,将以四种不同的方式创建队列:

  1. 数组
  2. 双向列表
  3. 环形缓冲区
  4. 双栈

用数组来实现

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
38
public struct QueueArray<T>: Queue {

private var array: [T] = []
public init() {}

public var isEmpty: Bool {
return array.isEmpty
}

public var peek: T? {
return array.first
}

public mutating func enqueue(_ element: T) -> Bool {
array.append(element)
return true
}

public mutating func dequeue() -> T? {
return isEmpty ? nil : array.removeFirst()
}
}

extension QueueArray: CustomStringConvertible {

public var description: String {
return String(describing: array)
}
}

var queue = QueueArray<String>()
queue.enqueue("Ray")
queue.enqueue("Brian")
queue.enqueue("Eric")
queue
queue.dequeue()
queue
queue.peek
优缺点
image-20190912160423263

很明显只有enqueue是O(1),其他都是O(n)。

用双向链表来实现

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
public class QueueLinkedList<T>: Queue {

private var list = DoublyLinkedList<T>()
public init() {}

public func enqueue(_ element: T) -> Bool {
list.append(element)
return true
}

public func dequeue() -> T? {
guard !list.isEmpty, let element = list.first else {
return nil
}
return list.remove(element)
}

public var peek: T? {
return list.first?.value
}

public var isEmpty: Bool {
return list.isEmpty
}
}

extension QueueLinkedList: CustomStringConvertible {

public var description: String {
return String(describing: list)
}
}

var queue = QueueLinkedList<String>()
queue.enqueue("Ray")
queue.enqueue("Brian")
queue.enqueue("Eric")
queue
queue.dequeue()
queue
queue.peek

// MARK: - DoublyLinkedList

public class Node<T> {

public var value: T
public var next: Node<T>?
public var previous: Node<T>?

public init(value: T) {
self.value = value
}
}

extension Node: CustomStringConvertible {

public var description: String {
return String(describing: value)
}
}

public class DoublyLinkedList<T> {

private var head: Node<T>?
private var tail: Node<T>?

public init() { }

public var isEmpty: Bool {
return head == nil
}

public var first: Node<T>? {
return head
}

public func append(_ value: T) {
let newNode = Node(value: value)

guard let tailNode = tail else {
head = newNode
tail = newNode
return
}

newNode.previous = tailNode
tailNode.next = newNode
tail = newNode
}

public func remove(_ node: Node<T>) -> T {
let prev = node.previous
let next = node.next

if let prev = prev {
prev.next = next
} else {
head = next
}

next?.previous = prev

if next == nil {
tail = prev
}

node.previous = nil
node.next = nil

return node.value
}
}

extension DoublyLinkedList: CustomStringConvertible {

public var description: String {
var string = ""
var current = head
while let node = current {
string.append("\(node.value) -> ")
current = node.next
}
return string + "end"
}
}

public class LinkedListIterator<T>: IteratorProtocol {

private var current: Node<T>?

init(node: Node<T>?) {
current = node
}

public func next() -> Node<T>? {
defer { current = current?.next }
return current
}
}

extension DoublyLinkedList: Sequence {

public func makeIterator() -> LinkedListIterator<T> {
return LinkedListIterator(node: head)
}
}
优缺点image-20190912161101034

出来空间复杂度是O(n),其他的都是O(1)。

双向列表的主要弱点在表中并不明显。尽管有O(1)的性能,但它的开销很高。每个元素都必须有额外的存储空间用于前向和后向引用。而且,每次创建新元素时,都需要相对昂贵的动态分配。相比之下,数组进行批量分配,速度更快。

用环形缓冲区来实现

环形缓冲区,也称为循环缓冲区,是固定大小的阵列。当最终没有更多项目需要删除时,这种数据结构战略性地包含在开头。

详见:[https://github.com/raywenderlich/swift-algorithm-club/tree/master/Ring%20Buffer](https://github.com/raywenderlich/swift-algorithm-club/tree/master/Ring Buffer)

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
public struct QueueRingBuffer<T>: Queue {

private var ringBuffer: RingBuffer<T>

public init(count: Int) {
ringBuffer = RingBuffer<T>(count: count)
}

public var isEmpty: Bool {
return ringBuffer.isEmpty
}

public var peek: T? {
return ringBuffer.first
}

public mutating func enqueue(_ element: T) -> Bool {
return ringBuffer.write(element)
}

public mutating func dequeue() -> T? {
return isEmpty ? nil : ringBuffer.read()
}
}

extension QueueRingBuffer: CustomStringConvertible {

public var description: String {
return String(describing: ringBuffer)
}
}

var queue = QueueRingBuffer<String>(count: 10)
queue.enqueue("Ray")
queue.enqueue("Brian")
queue.enqueue("Eric")
queue
queue.dequeue()
queue
queue.peek

// MARK: - RingBuffer

public struct RingBuffer<T> {

private var array: [T?]
private var readIndex = 0
private var writeIndex = 0

public init(count: Int) {
array = Array<T?>(repeating: nil, count: count)
}

public var first: T? {
return array[readIndex]
}

public mutating func write(_ element: T) -> Bool {
if !isFull {
array[writeIndex % array.count] = element
writeIndex += 1
return true
} else {
return false
}
}

public mutating func read() -> T? {
if !isEmpty {
let element = array[readIndex % array.count]
readIndex += 1
return element
} else {
return nil
}
}

private var availableSpaceForReading: Int {
return writeIndex - readIndex
}

public var isEmpty: Bool {
return availableSpaceForReading == 0
}

private var availableSpaceForWriting: Int {
return array.count - availableSpaceForReading
}

public var isFull: Bool {
return availableSpaceForWriting == 0
}
}

extension RingBuffer: CustomStringConvertible {
public var description: String {
let values = (0..<availableSpaceForReading).map {
String(describing: array[($0 + readIndex) % array.count]!)
}
return "[" + values.joined(separator: ", ") + "]"
}
}
优缺点

image-20190912162139308

基于环缓冲区的队列具有与链表实现相同的入队和出队时间复杂度。唯一的区别是空间复杂性。环形缓冲区具有固定大小,这意味着入队可能会失败。

用双栈来实现

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
38
39
40
41
42
43
44
public struct QueueStack<T> : Queue {

private var leftStack: [T] = []
private var rightStack: [T] = []
public init() {}

public var isEmpty: Bool {
return leftStack.isEmpty && rightStack.isEmpty
}

public var peek: T? {
return !leftStack.isEmpty ? leftStack.last : rightStack.first
}

public mutating func enqueue(_ element: T) -> Bool {
rightStack.append(element)
return true
}

public mutating func dequeue() -> T? {
if leftStack.isEmpty {
leftStack = rightStack.reversed()
rightStack.removeAll()
}
return leftStack.popLast()
}
}

extension QueueStack: CustomStringConvertible {

public var description: String {
let printList = leftStack.reversed() + rightStack
return String(describing: printList)
}
}

var queue = QueueStack<String>()
queue.enqueue("Ray")
queue.enqueue("Brian")
queue.enqueue("Eric")
queue
queue.dequeue()
queue
queue.peek

出队:image-20190912162831309

入队:

![image-20190912162918069](/Users/will/Library/Application Support/typora-user-images/image-20190912162918069.png)

优缺点
![image-20190912163001741](/Users/will/Library/Application Support/typora-user-images/image-20190912163001741.png)

与基于数组的实现相比,通过利用两个堆栈,您可以将dequeue(_ :)转换为分摊的O(1)操作。

此外,您的双栈实现是完全动态的,并且没有基于环形缓冲区的队列实现所具有的固定大小限制。

总结

  • 队列采用FIFO策略,首先必须先删除添加的元素。

  • Enqueue将元素插入队列的后面。

  • Dequeue删除队列前面的元素。

  • 数组中的元素在连续的内存块中布局,而链表中的元素更加分散,可能存在缓存未命中。

  • 基于环路缓冲区队列的实现适用于具有固定大小的队列。

  • 与其他数据结构相比,利用两个栈可以将出列dequeue(_ :)时间复杂度提高到摊销的O(1)操作。

  • 双栈实现在空间复杂度方面击败了链表。

第九章:队列挑战

解释栈和队列之间的区别

为每个数据结构提供两个实际示例。

画逐步图表

演示四种实现时,每一步对队列的有影响。

如队列为:”SWIFT”

1
2
3
4
5
6
7
enqueue("R") 
enqueue("O")
dequeue()
enqueue("C")
dequeue()
dequeue()
enqueue("K")

给队列添加next协议

用来大富翁游戏,指派下一个玩家

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public protocol BoardGameManager {
associatedtype Player
mutating func nextPlayer() -> Player?
}

extension QueueArray: BoardGameManager {

public typealias Player = T

public mutating func nextPlayer() -> T? {
guard let person = dequeue() else {
return nil
}
enqueue(person)
return person
}
}

逆序排列队列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
extension QueueArray {

func reversed() -> QueueArray {
var queue = self
var stack = Stack<T>()
while let element = queue.dequeue() {
stack.push(element)
}
while let element = stack.pop() {
queue.enqueue(element)
}
return queue
}
}

引言

写这本书的目的

每个开发人员都应该研究调试代码的艺术。 但是,有些人会从本书中获得更多。 本书是为:

  1. 希望更好地使用LLDB进行调试的开发人员

  2. 希望使用LLDB构建复杂调试命令的开发人员

  3. 希望深入了解Swift和Objective-C内部的开发人员

  4. 有兴趣了解:通过逆向工程,他们可以做些什么的开发人员

  5. 对现代主动逆向工程策略感兴趣的开发人员

  6. 希望在发现有关其计算机或软件问题的答案时有所帮助的开发人员

自定义LLDB脚本repo:

https://github.com/DerekSelander/LLDB

这些脚本将有助于您的调试/逆向工程,并为您自己的LLDB脚本提供新颖的想法。

第一节:开始LLDB命令

到本节结束时,您将能够使用调试器来执行调试所需的大多数基本任务,以及创建自己的简单自定义命令

第1章:入门

想知道为什么命令是po? po代表打印对象。 还有p,它只是打印RDI的内容。 po通常更有用,因为它提供了NSObject的描述或debugDescription方法(如果可用)。

如果您想将调试提升到一个新的水平,汇编(Assembly)是一项重要的技能。

它可以让您深入了解Apple的代码 - 即使您没有任何源代码可供阅读。 它将使您更好地了解Swift编译器团队如何使用Swift在Objective-C中跳出,并且它将使您更好地了解Apple设备上的一切是如何工作的。

如果可以,我将始终选择在调试器中使用Objective-C,因为使用Objective-C LLDB比使用Swift更稳定。

第2章:Help和Apropos

help将转储(dump)所有可用的命令,包括从〜/ .lldbinit加载的自定义命令。

1
(lldb) help breakpoint

apropos命令可以为您执行此操作;这有点像使用搜索引擎在网络上找到一些东西。

1
(lldb) apropos swift

第3章:使用LLDB Attaching

LLDB“Attaching”的短语实际上有点误导。 名为debugserver(位于Xcode.app/Contents/SharedFrameworks/LLDB.framework/Resources/)的程序负责附加(attaching)到目标进程。

如果它是远程进程,例如在远程设备上运行的iOS,watchOS或tvOS应用程序,则会在该远程设备上启动远程调试服务器。 LLDB的工作是启动,连接和协调调试服务器,以处理调试应用程序时的所有交互。

附加到现有进程

1
lldb -n Xcode

附加到未来的进程

1
lldb -n Finder -w

launch可选的参数

这告诉LLDB使用/bin/ls(文件列表命令)作为目标可执行文件。

1
lldb -f /bin/ls

使用 process 的选项 -w 更改工作目录

1
(lldb) process launch -w /Applications

直接传递参数给程序,也就是/bin/ls

1
(lldb) process launch -- /Applications

等价于

1
$ ls /Applications

-X选项可扩展您提供的任何shell参数,例如代字号。

1
(lldb) process launch -X true -- ~/Desktop

runprocess launch -X true —的缩写,所以

1
run ~/Desktop

stdin也有一个选项-i,用来处理标准的输入输出。

1
2
(lldb) target delete
(lldb) target create /usr/bin/wc
1
$ echo "hello world" > /tmp/wc_input.txt
1
2
3
4
(lldb) process launch -i /tmp/wc_input.txt
Process 24511 launched: '/usr/bin/wc' (x86_64)
1 2 12
Process 24511 exited with status = 0 (0x00000000)

等同于

1
2
$ wc < /tmp/wc_input.txt
1 2 12

第4章:在代码中停止

无论您是在技术堆栈中使用Swift,Objective-C,C ++,C还是完全不同的语言,您都需要学习如何创建断点。 可以轻松地在Xcode中单击侧面板以使用GUI创建断点,但LLDB控制台可以让您更好地控制断点。

Signals(信号)

Unix信号是进程间通信的基本形式。

例如,其中一个信号SIGSTOP可用于保存状态并暂停执行进程,而其对应的SIGCONT则被发送到程序以恢复执行。调试器可以使用这两个信号暂停并继续执行程序。

Xcode 断点

符号断点(Symbolic breakpoints)是Xcode的一个很好的调试功能。它们允许您在应用程序中的某个符号上设置断点。例如[NSObject init],它引用NSObject实例的init方法。

您将学习如何在第10章“汇编,寄存器和调用约定”中正确使用和操作寄存器,但是现?在,只需知道 arg1与​ $rdi寄存器同义,并且可以被认为是持有实例的调用init时的类。

还有Swift错误断点,它通过在swift_willThrow方法上创建断点来随时停止Swift抛出错误。如果您正在处理任何容易出错的API,这是一个很好的选择,因为它可以让您快速诊断情况,而不会对代码的正确性做出错误的假设。

LLDB断点语法

image命令是一个很好的工具,可以帮助内省对设置断点至关重要的细节。

1
(lldb) image lookup -n "-[UIViewController viewDidLoad]"
1
(lldb) image lookup -rn test
Objective-C properties
1
2
3
@interface TestClass : NSObject 
@property (nonatomic, strong) NSString *name;
@end
1
(lldb) image lookup -n "-[TestClass name]"
Objective-C properties and dot notation
1
2
3
4
5
6
7
8
9
10
TestClass *a = [[TestClass alloc] init];

// Both equivalent for setters
[a setName:@"hello, world"];
a.name = @"hello, world";

// Both equivalent for getters
NSString *b;
b = [a name]; // b = @"hello, world"
b = a.name; // b = @"hello, world"

重要的是要知道您是在处理Objective-C代码并尝试使用点表示法在setter和getter属性上创建断点。

Swift properties
1
2
3
class SwiftTestClass: NSObject { 
var name: String!
}

In the LLDB console, type the following:

1
(lldb) image lookup -rn Signals.SwiftTestClass.name.setter

You’ll get output similar to below:

1
2
3
4
5
1 match found in /Users/derekselander/Library/Developer/Xcode/ DerivedData/Signals-atqcdyprrotlrvdanihoufkwzyqh/Build/Products/Debugiphonesimulator/Signals.app/Signals:

Address: Signals[0x000000010000cc70] (Signals.__TEXT.__text + 44816)

Summary: Signals`Signals.SwiftTestClass.name.setter : Swift.ImplicitlyUnwrappedOptional<Swift.String> at SwiftTestClass.swift: 28

使用以下正则表达式查询同时搜索name属性的SwiftTestClass setter和getter:

1
(lldb) image lookup -rn Signals.SwiftTestClass.name

创建断点

有几种不同的方法可以创建断点。最基本的方法是只输入字母b,后跟断点名称。这在Objective-C和C中相当容易,因为名称简短且易于键入(例如 - [NSObject init]或 - [UIView setAlpha:])。输入C ++和Swift非常棘手,因为编译器会将您的方法转换为具有相当长名称的符号。

1
(lldb) b -[UIViewController viewDidLoad]

与许多速记命令一样,b是另一个更长的LLDB命令的缩写。

正则表达式断点和范围

rb命令将扩展到rbreak(假设您没有任何以“rb”开头的其他LLDB命令)。

1
(lldb) rb SwiftTestClass.name.setter

这将在包含短语name.setter的任何内容上生成断点:

1
(lldb) rb name\.setter	

在UIViewController的每个Objective-C实例方法上创建一个断点:

1
(lldb) rb '\-\[UIViewController\ '

删除所有断点:

1
(lldb) breakpoint delete

在断点中的UIViewController之后,这提供了带有一个或多个字母数字字符后跟空格的可选括号。

1
(lldb) rb '\-\[UIViewController(\(\w+\))?\ '

使用正则表达式断点可以使用单个表达式捕获各种断点。

您可以使用-f选项将断点的范围限制为特定文件。

如果您正在调试DetailViewController.swift,这将非常有用。它将在此文件中的所有属性getter / setter,块/闭包,扩展/类别和函数/方法上设置断点。 -f称为范围限制。

1
(lldb) rb . -f DetailViewController.swift

使用-s可以限制此共享库中设置断点。

1
(lldb) rb . -s UIKit

-o选项为此提供了解决方案。它创造了所谓的“一次性”断点。当这些断点命中时,断点将被删除。所以它只会打一次。

1
2
(lldb) breakpoint delete 
(lldb) rb . -s UIKit -o
其他很酷的断点选项

-L选项允许您按源代码的语言进行过滤。因此,如果您只想在Signals应用程序的Commons模块中使用Swift代码,则可以执行以下操作:

1
(lldb) breakpoint set -L swift -r . -s Commons

这将在Commons模块中的每个Swift方法上设置断点。

如果你想在Swift语句if let寻找一些有趣的东西,如果让它完全忘记你的应用程序在哪里,该怎么办?您可以使用源正则表达式断点来帮助确定感兴趣的位置!像这样:

1
(lldb) breakpoint set -A -p "if let"

这将在包含if let的每个源代码位置创建一个断点。 -A选项表示搜索项目已知的所有源文件。

如果想进一限制文件范围:

1
(lldb) breakpoint set -p "if let" -f MasterViewController.swift -f DetailViewController.swift

这将获取所有源文件(-A),但只过滤那些属于Signals可执行文件(使用-s Signals选项)的文件。

再来一个很酷的断点选项示例?好的。每当viewDidLoad被命中时,将创建一个打印UIViewController的断点,但是将通过LLDB控制台而不是符号断点窗口来执行此操作。然后将这个断点导出到文件中,这样就可以通过使用断点读取和断点写入命令展现给同事。

1
2
3
4
5
6
(lldb) breakpoint delete //清理断点
(lldb) breakpoint set -n "-[UIViewController viewDidLoad]" -C "po $arg1" -G1 // 打印所有符合条件的实例对象
(lldb) breakpoint write -f /tmp/br.json //断点写入文件
(lldb) platform shell cat /tmp/br.json //shell读取文件
(lldb) breakpoint delete //清理断点
(lldb) breakpoint read -f /tmp/br.json //还能从文件中导入断点
修改和移除断点

断点会从1开始分配ID

1
2
3
4
5
6
7
(lldb) b main
(lldb) breakpoint list 1
(lldb) breakpoint list 1 -b //简洁:没有位置(location)
(lldb) breakpoint list 1 3 //多个
(lldb) breakpoint list 1-3 //范围
(lldb) breakpoint delete 1 //删除
(lldb) breakpoint delete 1.1 //仅删除第一个子断点

第5章:表达式

格式化 p 和 po

po通常用于Swift和Objective-C代码中以打印出感兴趣的项目。这可以是对象中的实例变量,对象的本地引用或寄存器,如本书前面所述。它甚至可以是一个任意的内存引用 - 只要该地址有一个对象!

po实际上是expression -O -- 的简写表达式。 -O参数用于打印对象的描述。

1
2
3
4
5
6
7
8
9
10
11
(lldb) help po
Evaluate an expression on the current thread. Displays any returned
value with formatting controlled by the type's author. Expects 'raw'
input (see 'help raw-input'.)

Syntax: po <expr>

Command Options Usage:
po <expr>

'po' is an abbreviation for 'expression -O --'

po经常被忽视的兄弟p,是另一个省略-O选项的缩写,expression -- 。打印出的p的格式更依赖于LLDB类型系统。

1
2
3
4
5
6
7
8
9
10
11
(lldb) help p
Evaluate an expression on the current thread. Displays any returned
value with LLDB's default formatting. Expects 'raw' input (see 'help
raw-input'.)

Syntax: p <expr>

Command Options Usage:
p <expr>

'p' is an abbreviation for 'expression --'

例如:

1
2
3
4
5
6
7
8
9
10
11
12
override var description: String { 
return "Yay! debugging " + super.description
}

override var debugDescription: String {
return "debugDescription: " + super.debugDescription
}

override func viewDidLoad() {
super.viewDidLoad()
print("\(self)")
}

在viewDidLoad中添加Xcode断点:

1
(lldb) po self

输出:

1
debugDescription: Yay! debugging <Signals.MasterViewController: 0x7fb71fd04080>

Swift 和 Objective-C 调试上下(文contexts)

重要的是要注意调试程序时有两个调试上下文:非Swift调试上下文和Swift上下文。默认情况下,当您停止使用Objective-C代码时,LLDB将使用非Swift(Objective-C)调试上下文,而如果您在Swift代码中停止,则LLDB将使用Swift上下文。听起来合乎逻辑,对吧?

但是你也可以指定上下文:

1
(lldb) expression -l objc -O -- [UIApplication sharedApplication]

在这里,您告诉LLDB使用Objective-C的objc语言。如有必要,您还可以使用objc + +作为Objective-C ++。

用户定义变量

如前所述,LLDB将在打印对象时代表您自动创建局部变量。您也可以创建自己的变量。

但是记得名字前面加$

1
2
3
4
5
6
7
(lldb) po id $test = [NSObject new] 
(lldb) po $test
<NSObject: 0x60000001d190>
(lldb) expression -l swift -O -- $test //切换至Swift上下文,同样可用
<NSObject: 0x60000001d190>
(lldb) expression -l swift -O -- $test.description //但是不能期待一切都正常
error: <EXPR>:3:1: error: use of unresolved identifier '$test' $test.description ^~~~~

这是一个正在积极开发的领域,Objective-C和Swift之间通过LLDB的桥梁可能会随着时间的推移而有所改善。

在Xcoode创建符号断点:

1
Signals.MasterContainerViewController.viewDidLoad() -> ()

然后

1
2
3
(lldb) p self
(lldb) po $R0.title //$R0是上一步打印出来的self的变量的名字
(lldb) expression -l swift -- $R0.title = "new title" //还可以修改

通过键入continue或按Xcode中的播放按钮来恢复应用程序。就会发现标题已经更新为 ’new title‘

类型格式化(Type formatting)

LLDB的一个不错的选择是能够格式化基本数据类型的输出。这使得LLDB成为了解编译器如何格式化基本C类型的绝佳工具。当你正在探索汇编部分时,这是必须知道的,你将在本书后面做。

1
2
3
4
(lldb) expression -G x -- 10 //指定十进制格式化输出
(lldb) p/x 10 //十进制
(lldb) p/t 10 //二进制
(lldb) p/d 'D' //十进制
1
2
-G <gdb-format> ( --gdb-format <gdb-format> )
Specify a format using a GDB format specifier string.

输出格式的完整列表如下(取自https://sourceware.org/gdb/ onlinedocs / gdb / Output-Formats.html):

  • x:十六进制
  • d:十进制
  • u:无符号十进制
  • o:八进制
  • t:二进制
  • a:地址
  • c:字符常量
  • f:浮点型
  • s:字符串

如果这些格式不够,您可以使用LLDB的额外格式化程序,但您将无法使用GDB格式化语法。

1
2
(lldb) expression -f Y -- 1430672467
(int) $0 = 53 54 46 55 STFU
1
2
-f <format> ( --format <format> )
Specify a format to be used for display.

LLDB具有以下格式化程序(摘自http://lldb.llvm.org/varformats.html):

  • B:布尔值

  • b:二进制

  • y:字节

  • Y:ASCII字节

  • c:字符

  • C:可打印字符

  • F:复杂的浮点型(包含实部和虚部)

  • s:c-string

  • i:十进制

  • E:枚举

  • x:十六进制

  • f:浮点型

  • o:八进制

  • O:MacOS OSType

  • U:UTF-16

  • u:无符号十进制

  • p:指针

第6章:线程,帧和步进(Thread, Frame & Stepping Around)

您已经学习了如何创建断点,如何打印和修改值,以及如何在调试器中暂停时执行代码。但到目前为止,您已经处于高度干燥的状态,如何在调试器中移动并检查数据之外的数据。现在是时候了!

在本章中,您将学习如何在LLDB当前暂停时将调试器移入和移出功能。

这是一项关键技能,因为您经常需要在输入或退出代码片段时随时检查值。

(栈的第一课)Stack 101

当计算机程序执行时,它将值存储在堆和栈中。两者都有其优点。作为高级调试器,您需要充分了解这些工作原理。现在,让我们简要介绍一下这个栈。

栈是LIFO(后进先出)队列,用于存储对当前正在执行的代码的引用。这种LIFO排序意味着最近添加的任何内容都会被删除。想想一栈盘子。在顶部添加一个盘子,它将是你首先取下的盘子。

栈指针指向栈的当前顶部。在板块类比中,栈指针指向顶板,告诉您从哪里取下一块板,或者在哪里放下一块板。

image-20190628093457893

在此图中,高地址显示在顶部(0xFFFFFFFF),低地址显示在底部(0x00000000),显示栈将向下增长。

一些插图喜欢在底部具有高地址以与板类比匹配,因为栈将显示为向上增长。但是,我相信任何展示栈的图表都应显示从高地址向下增长,因为这会在以后讨论栈指针的偏移时引起较少的麻烦。

检查栈的帧

这里我将使用 iPhone X Simulator。

添加断点:

1
Signals.MasterViewController.viewWillAppear(Swift.Bool) -> ()

image-20190628094640418

在Debug Navigator面板中,将显示栈跟踪,显示栈帧列表,第一个是viewWillAppear(_ :)。接下来是Swift / Objective-C桥接方法,@objc MasterViewController.viewWillAppear(Bool) ->():。这个方法是自动生成的,所以Objective-C可以进入Swift代码。

之后,有一些来自UIKit的Objective-C代码栈帧。深入挖掘一下,你会看到一些属于CoreAnimation的C ++代码。更深入的是,你会看到一些方法都包含属于CoreFoundation的名称CFRunLoop。最后,最重要的是,main函数。

下面进入LLDB console:

1
(lldb) thread backtrace

也可以使用bt

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
(lldb) help bt
Show the current thread's call stack. Any numeric argument displays at
most that many frames. The argument 'all' displays all threads. Expects
'raw' input (see 'help raw-input'.)

Syntax: bt [<digit> | all]

'bt' is an abbreviation for '_regexp-bt'
(lldb) help thread backtrace
Show thread call stacks. Defaults to the current thread, thread indexes
can be specified as arguments.
Use the thread-index "all" to see all threads.
Use the thread-index "unique" to see threads grouped by unique call stacks.

Syntax: thread backtrace <cmd-options>

Command Options Usage:
thread backtrace [-c <count>] [-s <frame-index>] [-e <boolean>]

-c <count> ( --count <count> )
How many frames to display (-1 for all)

-e <boolean> ( --extended <boolean> )
Show the extended backtrace, if available

-s <frame-index> ( --start <frame-index> )
Frame in which to start the backtrace
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
(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
* frame #0: 0x0000000107044301 Signals`MasterViewController.viewWillAppear(animated=false, self=0x00007f9a26e0a670) at MasterViewController.swift:54:5
frame #1: 0x0000000107044993 Signals`@objc MasterViewController.viewWillAppear(_:) at <compiler-generated>:0
frame #2: 0x0000000110fc9437 UIKitCore`-[UIViewController _setViewAppearState:isAnimating:] + 687
frame #3: 0x0000000110fc9995 UIKitCore`__52-[UIViewController _setViewAppearState:isAnimating:]_block_invoke + 265
frame #4: 0x000000010957b33a CoreFoundation`-[__NSSingleObjectArrayI enumerateObjectsWithOptions:usingBlock:] + 58
frame #5: 0x0000000110fc9729 UIKitCore`-[UIViewController _setViewAppearState:isAnimating:] + 1441
frame #6: 0x0000000110fc9ba2 UIKitCore`-[UIViewController __viewWillAppear:] + 131
frame #7: 0x0000000110f275c9 UIKitCore`-[UINavigationController _startTransition:fromViewController:toViewController:] + 868
frame #8: 0x0000000110f283b5 UIKitCore`-[UINavigationController _startDeferredTransitionIfNeeded:] + 896
frame #9: 0x0000000110f296a7 UIKitCore`-[UINavigationController __viewWillLayoutSubviews] + 150
frame #10: 0x0000000110f0a38d UIKitCore`-[UILayoutContainerView layoutSubviews] + 217
frame #11: 0x0000000111a939c1 UIKitCore`-[UIView(CALayerDelegate) layoutSublayersOfLayer:] + 1417
frame #12: 0x000000010ea1feae QuartzCore`-[CALayer layoutSublayers] + 173
frame #13: 0x000000010ea24b88 QuartzCore`CA::Layer::layout_if_needed(CA::Transaction*) + 396
frame #14: 0x000000010ea30ee4 QuartzCore`CA::Layer::layout_and_display_if_needed(CA::Transaction*) + 72
frame #15: 0x000000010e9a03aa QuartzCore`CA::Context::commit_transaction(CA::Transaction*) + 328
frame #16: 0x000000010e9d7584 QuartzCore`CA::Transaction::commit() + 608
frame #17: 0x00000001115deccb UIKitCore`__34-[UIApplication _firstCommitBlock]_block_invoke_2 + 128
frame #18: 0x0000000109592aec CoreFoundation`__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__ + 12
frame #19: 0x00000001095922b0 CoreFoundation`__CFRunLoopDoBlocks + 336
frame #20: 0x000000010958cb34 CoreFoundation`__CFRunLoopRun + 1252
frame #21: 0x000000010958c302 CoreFoundation`CFRunLoopRunSpecific + 626
frame #22: 0x000000010e8e22fe GraphicsServices`GSEventRunModal + 65
frame #23: 0x00000001115c5ba2 UIKitCore`UIApplicationMain + 140
frame #24: 0x000000010704b88b Signals`main at AppDelegate.swift:32:7
frame #25: 0x000000010af06541 libdyld.dylib`start + 1
(lldb) frame info
frame #0: 0x0000000107044301 Signals`MasterViewController.viewWillAppear(animated=false, self=0x00007f9a26e0a670) at MasterViewController.swift:54:5

如您所见,此输出与Debug Navigator中找到的内容相匹配。那么,如果您只是从Debug Navigator中看到所有内容,为什么这甚至很重要?好吧,使用LLDB控制台可以对您想要查看的信息进行细致的控制。此外,您将制作自定义LLDB脚本,其中这些命令将变得非常有用。知道Xcode从哪里获取信息也很好,对吧?

然后:

1
2
(lldb) frame select 1
frame #1: 0x0000000107044993 Signals`@objc MasterViewController.viewWillAppear(_:) at <compiler-generated>:0

记下汇编中的绿线。在该行之前是负责执行viewWillAppear(_:)callq指令,您在之前设置了断点。

不要让汇编太模糊你的眼睛。你还没有走出汇编树林……

步进

掌握LLDB时,您可以在程序暂停时执行的三个最重要的导航操作围绕着逐步执行程序。通过LLDB,您可以跳过(step over),单步执行(step in)或退出(step out)代码。

跳过
1
2
(lldb) run //'run' is an abbreviation for 'process launch -X true --'
(lldb) next //'next' is an abbreviation for 'thread step-over'
单步执行
1
(lldb) step //'step' is an abbreviation for 'thread step-in'
退出
1
(lldb) finish //'finish' is an abbreviation for 'thread step-out'

请记住,只需按Enter键,LLDB将执行您输入的最后一个命令。

Xcode 界面中步进

虽然使用控制台可以获得更加精细的控制,但Xcode已经为LLDB控制台上方的按钮提供了这些选项。

检查栈中的数据

frame 命令的一个非常有趣的选项是 frame variable 子命令。

此命令将获取可执行文件头中的调试符号信息(如果您的应用程序被剥离,则为dYSM …稍后会详细介绍)并转储该特定栈帧的信息。由于调试信息,frame variable命令可以使用适当的选项轻松告诉您函数中所有变量的范围以及程序中的任何全局变量。

1
2
(lldb) frame variable
(lldb) frame variable -F self //以平面格式打印结果
1
2
-F ( --flat )
Display results in a flat format that uses expression paths for each variable or member.

image-20190701112507258

提供有关Apple私有API的ivars的更多信息,而不是Variables View。

第7章:镜像(Image)

现在是时候探索通过LLDB的权力找到感兴趣代码的最佳工具之一。在本章中,您将深入了解image命令。

image命令是target modules子命令的别名。image命令专门查询 module 信息;也就是说,代码在进程中加载和执行。Module 可以包含许多东西,包括主要的可执行文件(main executables),框架(framework)或插件(plugin)。但是,这些module中的大多数通常以动态库(dynamic libraries)的形式出现。动态库的示例包括适用于iOS的UIKit或适用于macOS的AppKit。

Module

1
2
3
(lldb) image list 
(lldb) image list Foundation
[ 0] FD4BF3C6-63C9-3A30-BEEA-D39F6F8EEB13 0x0000000107d70000 /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/Frameworks/Foundation.framework/Foundation

这是一种有用的方法,可以找到有关所需module的信息。

让我们来探索这个输出。 那里有一些有趣的东西:

  1. 首先打印module的UUID(FD4BF3C6-63C9-3A30-BEEA-D39F6F8EEB13)。UUID对于搜索符号信息和唯一标识Foundation module的版本非常重要。

  2. 在UUID之后是加载地址(0x0000000107d70000)。 这标识将Foundation module加载到Signals可执行文件的进程空间中。

  3. 最后,您拥有module在磁盘上所在位置的完整路径。

1
(lldb) image dump symtab UIKitCore -s address

这将转储UIKitCore可用的所有符号表信息。由于-s address参数,此命令按私有UIKitCore模块中实现函数的地址对输出进行排序。

但是可读性不佳,需要另外一个命令来搜索:

1
2
3
4
(lldb) image lookup -n "-[UIViewController viewDidLoad]"
1 match found in /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/PrivateFrameworks/UIKitCore.framework/UIKitCore:
Address: UIKitCore[0x000000000034adf9] (UIKitCore.__TEXT.__text + 3443497)
Summary: UIKitCore`-[UIViewController viewDidLoad]

这将转储与UIViewController的viewDidLoad实例方法相关的信息。

如果想用正则来模糊搜索:

1
(lldb) image lookup -rn UIViewController

但是这还不够,因为结果包含了UIViewControllerBuiltinTransitionViewAnimator,这并不是我们想要的,所改为:

1
2
(lldb) image lookup -rn '\[UIViewController\ '
(lldb) image lookup -rn \[UIViewController\s //等价的

但是如果搜索Category,命名规则:UIViewController(CategoryName),改进如下:

1
(lldb) image lookup -rn '\[UIViewController\(\w+\)\ '

当然这些都是参考正则表达式的规则,然后用在实际的需求中。

寻找代码

您可以使用上面的image lookup命令找到UIViewController方法。您还使用它来寻找第4章“在代码中停止”中如何命名Swift属性setter和getter。

但是Block如何搜索呢?如下面例子

1
2
3
dispatch_once(&onceToken, ^{
sharedSignalHandler = [[UnixSignalHandler alloc] initPrivate];
});

我们先在Xcode添加断点,然后:

1
2
(lldb) frame info
frame #0: 0x00000001048e1250 Commons`__34+[UnixSignalHandler sharedHandler]_block_invoke(.block_descriptor=0x00000001048e8230) at UnixSignalHandler.m:72:28

可以看到完整的函数名: __34+[UnixSignalHandler sharedHandler]_block_invoke

让我们先试试:

1
(lldb) image lookup -rn _block_invoke

结果太多,添加范围限制:

1
(lldb) image lookup -rn _block_invoke Commons

现在创建断点:

1
(lldb) rb appendSignal.*_block_invoke -s Commons

之后我回到终端:

1
pkill -SIGIO Signals

就会触发刚刚创建的断点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2019-07-01 14:16:15.922561+0800 Signals[7583:429444] Appending new signal: SIGIO
(lldb) frame variable
(__block_literal_5 *) .block_descriptor = 0x0000600002f1d580
(int) sig = <read memory from 0x41 failed (0 of 4 bytes read)>

(siginfo_t *) siginfo = <read memory from 0x39 failed (0 of 8 bytes read)>

(UnixSignalHandler *const) self = <read memory from 0x31 failed (0 of 8 bytes read)>

(lldb) next
(lldb) frame variable
(__block_literal_5 *) .block_descriptor = 0x0000600002f1d580
(int) sig = 23
(siginfo_t *) siginfo = 0x00007ffeecdd9e78
(UnixSignalHandler *) self = 0x0000600002f091c0
(UnixSignal *) unixSignal = 0x0000000106ca4454

您需要跳过一个语句,因此块执行了一些初始逻辑来设置函数,也称为函数序言(function prologue)。函数序言是与汇编相关的主题,您将在第II节中了解。

这实际上非常有趣。首先,您会看到一个block的对象,正在调用的。然后有sig和siginfo参数传递给Objective-C方法,在该方法中调用此块。这些如何传递到block中?

好吧,当创建一个block时,编译器足够聪明,可以确定它正在使用哪些参数。然后它创建一个将这些作为参数的函数。调用块时,调用此函数,并传入相关参数。

1
2
3
4
5
6
7
8
9
10
11
12
(lldb) image lookup -t __block_literal_5
Best match found in /Users/will/Library/Developer/Xcode/DerivedData/Signals-bgjklehkjddsnxcexztwhrufpsbn/Build/Products/Debug-iphonesimulator/Signals.app/Frameworks/Commons.framework/Commons:
id = {0x100000be1}, name = "__block_literal_5", byte-size = 52, decl = UnixSignalHandler.m:127, compiler_type = "struct __block_literal_5 {
void *__isa;
int __flags;
int __reserved;
void (*__FuncPtr)();
__block_descriptor_withcopydispose *__descriptor;
UnixSignalHandler *const self;
siginfo_t *siginfo;
int sig;
}"

这是定义block的对象!

正如您所看到的,这几乎与头文件一样好,可以告诉您如何在block中导航内存。如果将内存中的引用转换为__block_literal_5类型,则可以轻松打印出block引用的所有变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
(lldb) po ((__block_literal_5 *)0x0000600002f1d580)
<__NSMallocBlock__: 0x600002f1d580>
(lldb) p/x ((__block_literal_5 *)0x0000600002f1d580)->__FuncPtr
(void (*)()) $1 = 0x0000000103143a80 (Commons`__38-[UnixSignalHandler appendSignal:sig:]_block_invoke_2 at UnixSignalHandler.m:127)
(lldb) image lookup -a 0x0000000103143a80
Address: Commons[0x0000000000001a80] (Commons.__TEXT.__text + 2240)
Summary: Commons`__38-[UnixSignalHandler appendSignal:sig:]_block_invoke_2 at UnixSignalHandler.m:127
(lldb) po ((__block_literal_5 *)0x0000600002f1d580)->sig //打印出传递给block的所有参数
23
(lldb) p *(__block_literal_5 *)0x0000600002f1d580 //可以使用p命令转储完整的结构并解除引用指针
(__block_literal_5) $4 = {
__isa = 0x0000000106d6e170
__flags = -1023410174
__reserved = 0
__FuncPtr = 0x0000000103143a80 (Commons`__38-[UnixSignalHandler appendSignal:sig:]_block_invoke_2 at UnixSignalHandler.m:127)
__descriptor = 0x000000010314a2c0
self = 0x0000600002f091c0
siginfo = 0x00007ffeecdd9e78
sig = 23
}

深入研究(Snooping around)

好的,您已经发现了如何以静态方式检查私有类的实例变量,但是该块内存地址太过诱人而无法置之不理。 尝试将其打印出来并使用动态分析进行探索。

直接打印block的内存地址:

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
(lldb) po 0x0000600002f1d580
<__NSMallocBlock__: 0x600002f1d580>
(lldb) image lookup -rn __NSMallocBlock__ // 没有结果
(lldb) po [__NSMallocBlock__ superclass]
__NSMallocBlock
(lldb) image lookup -rn __NSMallocBlock
5 matches found in /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation:
Address: CoreFoundation[0x000000000018b600] (CoreFoundation.__TEXT.__text + 1614448)
Summary: CoreFoundation`-[__NSMallocBlock retain] Address: CoreFoundation[0x000000000018b620] (CoreFoundation.__TEXT.__text + 1614480)
Summary: CoreFoundation`-[__NSMallocBlock release] Address: CoreFoundation[0x000000000018b630] (CoreFoundation.__TEXT.__text + 1614496)
Summary: CoreFoundation`-[__NSMallocBlock retainCount] Address: CoreFoundation[0x000000000018b640] (CoreFoundation.__TEXT.__text + 1614512)
Summary: CoreFoundation`-[__NSMallocBlock _tryRetain] Address: CoreFoundation[0x000000000018b650] (CoreFoundation.__TEXT.__text + 1614528)
Summary: CoreFoundation`-[__NSMallocBlock _isDeallocating]

(lldb) po [__NSMallocBlock superclass]
NSBlock

(lldb) image lookup -rn 'NSBlock\ '
6 matches found in /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation:
Address: CoreFoundation[0x000000000018b4c0] (CoreFoundation.__TEXT.__text + 1614128)
Summary: CoreFoundation`+[NSBlock allocWithZone:] Address: CoreFoundation[0x000000000018b4e0] (CoreFoundation.__TEXT.__text + 1614160)
Summary: CoreFoundation`+[NSBlock alloc] Address: CoreFoundation[0x000000000018b500] (CoreFoundation.__TEXT.__text + 1614192)
Summary: CoreFoundation`-[NSBlock copy] Address: CoreFoundation[0x000000000018b510] (CoreFoundation.__TEXT.__text + 1614208)
Summary: CoreFoundation`-[NSBlock copyWithZone:] Address: CoreFoundation[0x000000000018b520] (CoreFoundation.__TEXT.__text + 1614224)
Summary: CoreFoundation`-[NSBlock invoke] Address: CoreFoundation[0x000000000018b530] (CoreFoundation.__TEXT.__text + 1614240)
Summary: CoreFoundation`-[NSBlock performAfterDelay:]

您现在将尝试在block上调用此方法。但是,当保留此block的引用释放其控制时,您不希望块消失,从而降低retainCount,并可能释放block。

有一种简单的方法来保持这个block - 只需 retain 它!在LLDB中键入以下内容,将地址替换为block的地址:

1
2
3
4
5
6
7
(lldb) po id $block = (id)0x0000600002f1d580
(lldb) po [$block retain]
<__NSMallocBlock__: 0x600002f1d580>

(lldb) po [$block invoke]
2019-07-01 16:05:33.855039+0800 Signals[7583:452921] Appending new signal: SIGIO
nil

这表明你已经再次调用了block!

这种用于探索公共和私有类,然后探索它们实现的方法的方法,是了解程序覆盖范围内的内容的好方法。稍后您将对方法使用相同的发现过程,然后分析这些方法执行的程序集,为您提供原始方法源代码的非常接近的近似值。

Private debugging methods

image lookup 命令可以很好地搜索私有方法以及您在Apple开发职业生涯中看到的公共方法。

但是,在调试自己的代码时,有一些隐藏的方法非常有用。

例如,以_开头的方法通常表示自己是一个私有(也可能是重要的!)方法。

让我们尝试在所有以下划线字符开头的模块中搜索任何Objective-C方法,并在其中包含单词“description”。

如果您在阅读此行代码时有些迷惑不解,强烈建议您仔细阅读 https://docs.python.org/2/library/re.html 以了解正则表达式查询;从现在开始,它只会变得更加复杂。

1
2
3
4
5
(lldb) image lookup -rn (?i)\ _\w+description\] //不区分大小写
(lldb) image lookup -rn NSObject\(IvarDescription\)
(lldb) po [[UIApplication sharedApplication] _ivarDescription]
(lldb) image lookup -rn '\[UIStatusBar\ set' //查找所以set开头方法
(lldb) po (BOOL)[[UIStatusBar class] isSubclassOfClass:[UIView class]]

通过地址调用对象:

1
2
3
4
5
(lldb) po [[UIApplication sharedApplication] statusBar]
<UIStatusBar_Modern: 0x7f9965e03f00; frame = (0 0; 375 44); autoresize = W+BM; layer = <CALayer: 0x6000025dd640>>

(lldb) po [0x7f9965e03f00 setBackgroundColor:[UIColor purpleColor]]
purpleColor

未完待续

原文为 @twostrawsSwiftUI By Example,就是简单记录一些主要内容。

Introduction

A brief explanation of the basics of SwiftUI

What is SwiftUI?

  1. 声明式对比命令式,能够更好地处理不同状态的UI
  2. 跨平台:支持 iOS, macOS, tvOS, watchOS

SwiftUI vs Interface Builder and storyboards

  1. IB难以阅读和修改
  2. IB难以看出修改了内容
  3. IB和Swift交互不够友好,充满了Objective-C设计
  4. SwiftUI是个仅支持Swift 的库,这样才可以充分利用Swift的特性

Anyway, we’ll get onto exactly how SwiftUI works soon. For now, the least you need to know is that SwiftUI fixes many problems people had with the old Swift + Interface Builder approach:

  • We no longer have to argue about programmatic or storyboard-based design, because SwiftUI gives us both at the same time.
  • We no longer have to worry about creating source control problems when committing user interface work, because code is much easier to read and manage than storyboard XML.
  • We no longer need to worry so much about stringly typed APIs – there are still some, but significantly fewer.
  • We no longer need to worry about calling functions that don’t exist, because our user interface gets checked by the Swift compiler.

So, I hope you’ll agree there are lots of benefits to be had from moving to SwiftUI!

Frequently asked questions about SwiftUI

Does SwiftUI replace UIKit?

No. Many parts of SwiftUI directly build on top of existing UIKit components, such as UITableView. Of course, many other parts don’t – they are new controls rendered by SwiftUI and not UIKit.

But the point isn’t to what extent UIKit is involved. Instead, the point is that we don’t care. SwiftUI more or less completely masks UIKit’s behavior, so if you write your app for SwiftUI and Apple replaces UIKit with a singing elephant in two years you don’t have to care – as long as Apple makes the elephant compatible with the same methods and properties that UIKit exposed to SwiftUI, your code doesn’t change.

Is SwiftUI fast?

SwiftUI is screamingly fast – in all my tests so far it seems to outpace UIKit. Having spoken to the team who made it I’m starting to get an idea why: first, they aggressively flatten their layer hierarchy so the system has to do less drawing, but second many operations bypass Core Animation entirely and go straight to Metal for extra speed.

So, yes: SwiftUI is incredibly fast, and all without us having to do any extra work.

How to follow this quick start guide

最好顺序阅读该教程

Migrating from UIKit to SwiftUI

如果你用过UIKit,不难发现SwiftUI就是把UI前缀去掉既是对应的组件。

Here’s a list to get you started, with UIKit class names followed by SwiftUI names:

  • UITableView: List
  • UICollectionView: No SwiftUI equivalent
  • UILabel: Text
  • UITextField: TextField
  • UITextField with isSecureTextEntry set to true: SecureField
  • UITextView: No SwiftUI equivalent
  • UISwitch: Toggle
  • UISlider: Slider
  • UIButton: Button
  • UINavigationController: NavigationView
  • UIAlertController with style .alert: Alert
  • UIAlertController with style .actionSheet: ActionSheet
  • UIStackView with horizontal axis: HStack
  • UIStackView with vertical axis: VStack
  • UIImageView: Image
  • UISegmentedControl: SegmentedControl
  • UIStepper: Stepper
  • UIDatePicker: DatePicker
  • NSAttributedString: Incompatible with SwiftUI; use Text instead.

Text and images

Getting started with basic controls

What’s in the basic template?

SceneDelegate.swift is responsible for managing the way your app is shown.

1
2
3
4
let window = UIWindow(frame: UIScreen.main.bounds)
window.rootViewController = UIHostingController(rootView: ContentView())
self.window = window
window.makeKeyAndVisible()

Open ContentView.swift and let’s look at some actual SwiftUI code.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import SwiftUI

struct ContentView : View {
var body: some View {
Text("Hello World")
}
}

#if DEBUG
struct ContentView_Previews : PreviewProvider {
static var previews: some View {
ContentView()
}
}
#endif

First, notice how ContentView is a struct.

Second, ContentView conforms to the View protocol.

第三,body的返回类型是some viewsome关键字是Swift 5.1中的新关键字,是名为opaque return types的功能的一部分,在这种情况下,就是字面意思:”这将返回某种View,但SwiftUI不需要知道(或关心)什么。”

Finally, below ContentView is a similar-but-different struct called ContentView_Previews.

How to create static labels with a Text view

1
2
Text("Hello World")
.lineLimit(3)
1
2
Text("This is an extremely long string that will never fit even the widest of Phones")
.truncationMode(.middle)

How to style text views with fonts, colors, line spacing, and more

1
2
3
4
5
6
7
Text("This is an extremely long string that will never fit even the widest of Phones")
.truncationMode(.middle)
.font(Font.body)
.foregroundColor(Color.green)
.background(Color.gray)
.lineLimit(nil)
.lineSpacing(30)

How to format text inside text views

1
2
3
4
5
6
7
8
9
10
11
12
13
struct ContentView: View {
static let taskDateFormat: DateFormatter = {
let formatter = DateFormatter()
formatter.dateStyle = .long
return formatter
}()

var dueDate = Date()

var body: some View {
Text("Task due date: \(dueDate, formatter: Self.taskDateFormat)")
}
}

How to draw images using Image views

1
2
3
var body: some View {
Image("example-image")
}
1
2
3
Image(systemName: "cloud.heavyrain.fill")
.foregroundColor(.red)
.font(.largeTitle)

How to adjust the way an image is fitted to its space

1
2
3
Image("example-image")
.resizable()
.aspectRatio(contentMode: .fill)

How to render a gradient

1
2
3
4
Text("Hello World")
.padding()
.foregroundColor(.white)
.background(LinearGradient(gradient: Gradient(colors: [.white, .black]), startPoint: .top, endPoint: .bottom), cornerRadius: 0)

支持更多颜色的渐变

1
2
3
4
Text("Hello World")
.padding()
.foregroundColor(.white)
.background(LinearGradient(gradient: Gradient(colors: [.white, .red, .black]), startPoint: .leading, endPoint: .trailing), cornerRadius: 0)

How to display solid shapes

1
2
3
Rectangle()
.fill(Color.red)
.frame(width: 200, height: 200)

How to use images and other views as a backgrounds

1
2
3
4
5
6
Text("Hacking with Swift")
.font(.largeTitle)
.background(
Image("example-image")
.resizable()
.frame(width: 100, height: 100))

View layout

Position views in a grid structure and more

How to create stacks using VStack and HStack

1
2
3
4
VStack {
Text("SwiftUI")
Text("rocks")
}

How to customize stack layouts with alignment and spacing

1
2
3
4
VStack(alignment: .leading, spacing: 20) {
Text("SwiftUI")
Text("rocks")
}

How to control spacing around individual views using padding

1
2
Text("SwiftUI")
.padding(.bottom, 100)

How to layer views on top of each other using ZStack

1
2
3
4
5
ZStack {
Rectangle()
.fill(Color.red)
Text("Hacking with Swift")
}

How to return different view types

第一种方案

1
2
3
4
5
6
7
8
9
var body: some View {
Group {
if Bool.random() {
Image("example-image")
} else {
Text("Better luck next time")
}
}
}

第二种方案

If you haven’t heard of this concept, it effectively forces Swift to forget about what specific type is inside the AnyView, allowing them to look like they are the same thing. This has a performance cost, though, so don’t use it often.

1
2
3
4
5
6
7
var body: AnyView {
if Bool.random() {
return AnyView(Image("example-image"))
} else {
return AnyView(Text("Better luck next time"))
}
}

How to create views in a loop using ForEach

1
2
3
4
5
6
7
VStack(alignment: .leading) {
ForEach((1...10).reversed()) {
Text("\($0)…")
}

Text("Ready or not, here I come!")
}
1
2
3
4
5
6
7
8
9
10
11
12
13
struct ContentView : View {
let colors: [Color] = [.red, .green, .blue]

var body: some View {
VStack {
ForEach(colors.identified(by: \.self)) { color in
Text(color.description.capitalized)
.padding()
.background(color)
}
}
}
}

How to create different layouts using size classes

1
2
3
4
5
6
7
8
9
10
11
struct ContentView : View {
@Environment(\.horizontalSizeClass) var horizontalSizeClass: UserInterfaceSizeClass?

var body: some View {
if horizontalSizeClass == .compact {
return Text("Compact")
} else {
return Text("Regular")
}
}
}

How to place content outside the safe area

1
2
3
4
Text("Hello World")
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
.background(Color.red)
.edgesIgnoringSafeArea(.all)

Reading input

Respond to interaction and control your program state

Working with state

SwiftUI solves this problem by removing state from our control. When we add properties to our views they are effectively inert – they have values, sure, but changing them doesn’t do anything. But if we added the special @State attribute before them, SwiftUI will automatically watch for changes and update any parts of our views that use that state.

How to create a toggle switch

相反,我们应该定义一个@State布尔属性,用于存储切换的当前值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct ContentView : View {
@State var showGreeting = true

var body: some View {
VStack {
Toggle(isOn: $showGreeting) {
Text("Show welcome message")
}.padding()

if showGreeting {
Text("Hello World!")
}
}
}
}

How to create a tappable button

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct ContentView : View {
@State var showDetails = false

var body: some View {
VStack {
Button(action: {
self.showDetails.toggle()
}) {
Text("Show details")
}

if showDetails {
Text("You should follow me on Twitter: @twostraws")
.font(.largeTitle)
.lineLimit(nil)
}
}
}
}

How to read text from a TextField

1
2
3
4
5
6
7
8
9
10
struct ContentView : View {
@State var name: String = "Tim"

var body: some View {
VStack {
TextField($name)
Text("Hello, \(name)!")
}
}
}

How to add a border to a TextField

1
2
TextField($yourBindingHere)
.textFieldStyle(.roundedBorder)

How to create secure text fields using SecureField

1
2
3
4
5
6
7
8
9
10
struct ContentView : View {
@State var password: String = ""

var body: some View {
VStack {
SecureField($password)
Text("You entered: \(password)")
}
}
}

How to create a Slider and read values from it

1
2
3
4
5
6
7
8
9
10
struct ContentView : View {
@State var celsius: Double = 0

var body: some View {
VStack {
Slider(value: $celsius, from: -100, through: 100, by: 0.1)
Text("\(celsius) Celsius is \(celsius * 9 / 5 + 32) Fahrenheit")
}
}
}

How to create a date picker and read values from it

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct ContentView : View {
var colors = ["Red", "Green", "Blue", "Tartan"]
@State var selectedColor = 0

var body: some View {
VStack {
Picker(selection: $selectedColor, label: Text("Please choose a color")) {
ForEach(0 ..< colors.count) {
Text(self.colors[$0]).tag($0)
}
}
Text("You selected: \(colors[selectedColor])")
}
}
}

How to create a segmented control and read values from it

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct ContentView : View {
@State var favoriteColor = 0
var colors = ["Red", "Green", "Blue"]

var body: some View {
VStack {
SegmentedControl(selection: $favoriteColor) {
ForEach(0..<colors.count) { index in
Text(self.colors[index]).tag(index)
}
}

Text("Value: \(colors[favoriteColor])")
}
}
}

How to read tap and double-tap gestures

1
2
3
4
Image("example-image")
.tapAction(count: 2) {
print("Double tapped!")
}

How to add a gesture recognizer to a view

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct ContentView : View {
@State var scale: Length = 1.0

var body: some View {
Image("example-image")
.scaleEffect(scale)

.gesture(
TapGesture()
.onEnded { _ in
self.scale += 0.1
}
)
}
}

Lists

Create scrolling tables of data

Working with lists

SwiftUI’s List view is similar to UITableView in that it can show static or dynamic table view cells based on your needs.

How to create a list of static items

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct RestaurantRow: View {
var name: String

var body: some View {
Text("Restaurant: \(name)")
}
}

struct ContentView: View {
var body: some View {
List {
RestaurantRow(name: "Joe's Original")
RestaurantRow(name: "The Real Joe's Original")
RestaurantRow(name: "Original Joe's")
}
}
}

How to create a list of dynamic items

In order to handle dynamic items, you must first tell SwiftUI how it can identify which item is which. This is done using the Identifiable protocol, which has only one requirement: some sort of id value that SwiftUI can use to see which item is which.

ForEach一样,可以使用符合Identifiable 协议的Model

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
struct Restaurant: Identifiable {
var id = UUID()
var name: String
}

struct RestaurantRow: View {
var restaurant: Restaurant

var body: some View {
Text("Come and eat at \(restaurant.name)")
}
}

struct ContentView: View {
var body: some View {
let first = Restaurant(name: "Joe's Original")
let second = Restaurant(name: "The Real Joe's Original")
let third = Restaurant(name: "Original Joe's")
let restaurants = [first, second, third]

return List(restaurants) { restaurant in
RestaurantRow(restaurant: restaurant)
}
// return List(restaurants, rowContent: RestaurantRow.init)
}
}

How to add sections to a list

1
2
3
4
5
Section(header: Text("Other tasks"), footer: Text("End")) {
TaskRow()
TaskRow()
TaskRow()
}

How to make a grouped list

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct ExampleRow: View {
var body: some View {
Text("Example Row")
}
}

struct ContentView : View {
var body: some View {
List {
Section(header: Text("Examples")) {
ExampleRow()
ExampleRow()
ExampleRow()
}
}.listStyle(.grouped)
}
}

Working with implicit stacking

What happens if you create a dynamic list and put more than one thing in each row? SwiftUI’s solution is simple, flexible, and gives us great behavior by default: it creates an implicit HStack to hold your items, so they automatically get laid out horizontally.

List 会隐式的创建一个HStack 封装所有元素到Row中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct ExampleRow: View {
var body: some View {
Text("Example Row")
}
}

struct ContentView : View {
var body: some View {
List {
Section(header: Text("Examples")) {
ExampleRow()
ExampleRow()
ExampleRow()
}
}.listStyle(.grouped)
}
}

Containers

Place your views inside a navigation controller

Working with containers

SwiftUI is designed to be composed right out of the box, which means you can place one view inside another as much as you need.

常见的容器有: NavigationView, TabbedView, Group

How to embed a view in a navigation view

1
2
3
4
NavigationView {
Text("SwiftUI")
.navigationBarTitle(Text("Welcome"))
}

How to add bar items to a navigation view

1
2
3
4
5
6
7
8
9
10
11
12
var body: some View {
NavigationView {
Text("SwiftUI")
.navigationBarTitle(Text("Welcome"))
.navigationBarItems(trailing:
Button(action: {
print("Help tapped!")
}) {
Text("Help")
})
}
}

How to group views together

Stack 不能超过10个元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var body: some View {
VStack {
Group {
Text("Line")
Text("Line")
Text("Line")
Text("Line")
Text("Line")
Text("Line")
}

Group {
Text("Line")
Text("Line")
Text("Line")
Text("Line")
Text("Line")
}
}
}

Alerts and action sheets

Show modal notifications when something happens

Working with presentations

SwiftUI’s declarative approach to programming means that we don’t create and present alert and action sheets in the same way as we did in UIKit. Instead, we define the conditions in which they should be shown, tell it what they should look like, then leave it to figure the rest out for itself.

How to show an alert

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct ContentView : View {
@State var showingAlert = false

var body: some View {
Button(action: {
self.showingAlert = true
}) {
Text("Show Alert")
}
.presentation($showingAlert) {
Alert(title: Text("Important message"), message: Text("Wear sunscreen"), dismissButton: .default(Text("Got it!")))
}
}
}

How to add actions to alert buttons

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct ContentView : View {
@State var showingAlert = false

var body: some View {
Button(action: {
self.showingAlert = true
}) {
Text("Show Alert")
}
.presentation($showingAlert) {
Alert(title: Text("Are you sure you want to delete this?"), message: Text("There is no undo"), primaryButton: .destructive(Text("Delete")) {
print("Deleting...")
}, secondaryButton: .cancel())
}
}
}

How to show an action sheet

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct ContentView : View {
@State var showingSheet = false

var sheet: ActionSheet {
ActionSheet(title: Text("Action"), message: Text("Quote mark"), buttons: [.default(Text("Woo"), onTrigger: {
self.showingSheet = false
})])
}

var body: some View {
Button(action: {
self.showingSheet = true
}) {
Text("Woo")
}
.presentation(showingSheet ? sheet : nil)
}
}

Presenting views

Move your user from one view to another

How to push a new view using NavigationButton

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct DetailView: View {
var body: some View {
Text("Detail")
}
}

struct ContentView : View {
var body: some View {
NavigationView {
NavigationButton(destination: DetailView()) {
Text("Click")
}.navigationBarTitle(Text("Navigation"))
}
}
}

How to push a new view when a list row is tapped

SwiftUI doesn’t have a direct equivalent of the didSelectRowAt method of UITableView, but it doesn’t need one because we can combine NavigationButtonwith a list row and get the behavior for free.

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
struct Restaurant: Identifiable {
var id = UUID()
var name: String
}

struct RestaurantRow: View {
var restaurant: Restaurant

var body: some View {
Text(restaurant.name)
}
}

struct RestaurantView: View {
var restaurant: Restaurant

var body: some View {
Text("Come and eat at \(restaurant.name)")
.font(.largeTitle)
}
}

struct ContentView: View {
var body: some View {
let first = Restaurant(name: "Joe's Original")
let restaurants = [first]

return NavigationView {
List(restaurants) { restaurant in
NavigationButton(destination: RestaurantView(restaurant: restaurant)) {
RestaurantRow(restaurant: restaurant)
}
}.navigationBarTitle(Text("Select a restaurant"))
}
}
}

How to present a new view using PresentationButton

1
2
3
4
5
6
7
8
9
10
struct DetailView: View {
var body: some View {
Text("Detail")
}
}
struct ContentView : View {
var body: some View {
PresentationButton(Text("Click to show"), destination: DetailView())
}
}

Transforming views

Clip, size, scale, spin, and more

How to give a view a custom frame

1
2
3
4
5
6
7
8
9
10
11
12
13
Button(action: {
print("Button tapped")
}) {
Text("Welcome")
.frame(minWidth: 0, maxWidth: 200, minHeight: 0, maxHeight: 200)
.font(.largeTitle)
}

Text("Please log in")
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
.font(.largeTitle)
.foregroundColor(.white)
.background(Color.red)

How to adjust the position of a view

1
2
3
4
5
6
7
VStack {
Text("Home")
Text("Options")
.offset(y: 15)
.padding(.bottom, 15)
Text("Help")
}

How to color the padding around a view

Padding 的先后顺序影响结果,因为代码是顺序执行的。

1
2
3
4
5
6
7
8
9
Text("Hacking with Swift")
.background(Color.black)
.foregroundColor(.white)
.padding()

Text("Hacking with Swift")
.padding()
.background(Color.black)
.foregroundColor(.white)

How to stack modifiers to create more advanced effects

1
2
3
4
5
6
7
8
9
Text("Forecast: Sun")
.font(.largeTitle)
.foregroundColor(.white)
.padding()
.background(Color.red)
.padding()
.background(Color.orange)
.padding()
.background(Color.yellow)

How to draw a border around a view

1
2
3
Text("Hacking with Swift")
.padding()
.border(Color.red, width: 4, cornerRadius: 16)

How to draw a shadow around a view

1
2
3
4
Text("Hacking with Swift")
.padding()
.border(Color.red, width: 4)
.shadow(color: .red, radius: 5, x: 20, y: 20)

How to clip a view so only part is visible

1
2
3
4
5
6
7
8
9
Button(action: {
print("Button tapped")
}) {
Image(systemName: "bolt.fill")
.foregroundColor(.white)
.padding()
.background(Color.green)
.clipShape(Circle())
}

How to rotate a view

1
2
3
4
5
6
7
8
9
10
11
struct ContentView: View {
@State var rotation: Double = 0

var body: some View {
VStack {
Slider(value: $rotation, from: 0.0, through: 360.0, by: 1.0)
Text("Up we go")
.rotationEffect(.degrees(rotation))
}
}
}

How to rotate a view in 3D

SwiftUI’s rotation3DEffect() modifier lets us rotate views in 3D space to create beautiful effects in almost no code.

1
2
3
4
Text("EPISODE LLVM")
.font(.largeTitle)
.foregroundColor(.yellow)
.rotation3DEffect(.degrees(45), axis: (x: 1, y: 0, z: 0))

How to scale a view up or down

1
2
Text("Up we go")
.scaleEffect(5)

How to round the corners of a view

1
2
3
4
Text("Round Me")
.padding()
.background(Color.red)
.cornerRadius(25)

How to adjust the opacity of a view

1
2
3
4
Text("Now you see me")
.padding()
.background(Color.red)
.opacity(0.3)

How to adjust the accent color of a view

iOS uses tint colors to give apps a coordinated theme, and the same functionality is available in SwiftUI under the name accent colors.

How to mask one view with another

1
2
3
4
5
Image("stripes")
.resizable()
.frame(width: 300, height: 300)
.mask(Text("SWIFT!")
.font(Font.system(size: 72).weight(.black)))

How to blur a view

1
2
Text("Welcome to my SwiftUI app")
.blur(radius: 2)

How to blend views together

1
2
3
4
5
ZStack {
Image("paul-hudson")
Image("example-image")
.blendMode(.multiply)
}

How to adjust views by tinting, and desaturating, and more

1
2
Image("paul-hudson")
.contrast(0.5)

Animation

Bring your views to life with movement

How to create a basic animation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct ContentView: View {
@State var angle: Double = 0
@State var borderThickness: Length = 1

var body: some View {
Button(action: {
self.angle += 45
self.borderThickness += 1
}) {
Text("Tap here")
.padding()
.border(Color.red, width: borderThickness)
.rotationEffect(.degrees(angle))
.animation(.basic())
}
}
}

How to create a spring animation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct ContentView: View {
@State var angle: Double = 0

var body: some View {
Button(action: {
self.angle += 45
}) {
Text("Tap here")
.padding()
.rotationEffect(.degrees(angle))
.animation(.spring())
}
}
}

How to create an explicit animation

Explicit animations are often helpful because they cause every affected view to animation, not just those that have implicit animations attached. For example, if view A has to make room for view B as part of the animation, but only view B has an animation attached, then view A will jump to its new position without animating unless you use explicit animations.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct ContentView: View {
@State var opacity: Double = 1

var body: some View {
Button(action: {
withAnimation {
self.opacity -= 0.2
}
}) {
Text("Tap here")
.padding()
.opacity(opacity)
}
}
}

How to add and remove views with a transition

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct ContentView: View {
@State var showDetails = false

var body: some View {
VStack {
Button(action: {
withAnimation {
self.showDetails.toggle()
}
}) {
Text("Tap to show details")
}

if showDetails {
Text("Details go here.")
}
}
}
}

By default, SwiftUI uses a fade animation to insert or remove views, but you can change that if you want by attaching a transition() modifier to a view.

1
2
Text("Details go here.")
.transition(.move(edge: .bottom))

How to combine transitions

1
Text("Details go here.").transition(AnyTransition.opacity.combined(with: .slide))

或者使用拓展来封装常用的过渡效果

1
2
3
4
5
6
7
extension AnyTransition {
static var moveAndScale: AnyTransition {
AnyTransition.move(edge: .bottom).combined(with: .scale())
}
}
// Usage
Text("Details go here.").transition(.moveAndScale)

How to create asymmetric transitions

SwiftUI lets us specify one transition when adding a view and another when removing it, all done using the asymmetric() transition type.

1
Text("Details go here.").transition(.asymmetric(insertion: .move(edge: .leading), removal: .move(edge: .bottom)))

Composing views

Make your UI structure easier to understand

How to create and compose custom views

都是基于数据驱动UI,所以每一级封装都是传Model:User

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
struct User {
var name: String
var jobTitle: String
var emailAddress: String
var profilePicture: String
}

struct ProfilePicture: View {
var imageName: String

var body: some View {
Image(imageName)
.resizable()
.frame(width: 100, height: 100)
.clipShape(Circle())
}
}

struct EmailAddress: View {
var address: String

var body: some View {
HStack {
Image(systemName: "envelope")
Text(address)
}
}
}

struct UserDetails: View {
var user: User

var body: some View {
VStack(alignment: .leading) {
Text(user.name)
.font(.largeTitle)
.foregroundColor(.primary)
Text(user.jobTitle)
.foregroundColor(.secondary)
EmailAddress(address: user.emailAddress)
}
}
}

struct UserView: View {
var user: User

var body: some View {
HStack {
ProfilePicture(imageName: user.profilePicture)
UserDetails(user: user)
}
}
}

struct ContentView: View {
let user = User(name: "Paul Hudson", jobTitle: "Editor, Hacking with Swift", emailAddress: "paul@hackingwithswift.com", profilePicture: "paul-hudson")

var body: some View {
UserView(user: user)
}
}

How to combine text views together

这样子拼接文本很方便

1
2
3
4
5
6
7
8
var body: some View {
Text("SwiftUI ")
.font(.largeTitle)
+ Text("is ")
.font(.headline)
+ Text("awesome")
.font(.footnote)
}

但是有个修改器不适合拼接如:foregroundColor

这时候就需要换成能够使用+的修改器,如color

1
2
3
4
5
6
Text("SwiftUI ")
.color(.red)
+ Text("is ")
.color(.orange)
+ Text("awesome")
.color(.blue)

How to store views as properties

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct ContentView : View {
let title = Text("Paul Hudson")
.font(.largeTitle)
let subtitle = Text("Author")
.foregroundColor(.secondary)

var body: some View {
VStack {
title
.color(.red)
subtitle
}
}
}

How to create custom modifiers

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct PrimaryLabel: ViewModifier {
func body(content: Content) -> some View {
content
.padding()
.background(Color.red)
.foregroundColor(Color.white)
.font(.largeTitle)
}
}

struct ContentView: View {
var body: some View {
Text("Hello, SwiftUI")
.modifier(PrimaryLabel())
}
}

Tooling

Build better apps with help from Xcode

How to preview your layout at different Dynamic Type sizes

1
2
3
4
5
6
7
8
9
10
#if DEBUG
struct ContentView_Previews : PreviewProvider {
static var previews: some View {
Group {
ContentView()
.environment(\.colorScheme, .dark)
}
}
}
#endif

How to preview your layout in light and dark mode

If you want to see both light and dark mode side by side, place multiple previews in a group, like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
#if DEBUG
struct ContentView_Previews : PreviewProvider {
static var previews: some View {
Group {
ContentView()
.environment(\.colorScheme, .light)

ContentView()
.environment(\.colorScheme, .dark)
}
}
}
#endif

How to preview your layout in different devices

1
2
ContentView()
.previewDevice(PreviewDevice(rawValue: "iPhone SE"))

How to preview your layout in a navigation view

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct ContentView : View {
var body: some View {
Text("Hello World")
.navigationBarTitle(Text("Welcome"))
}
}

#if DEBUG
struct ContentView_Previews : PreviewProvider {
static var previews: some View {
NavigationView {
ContentView()
}
}
}
#endif

How to use Instruments to profile your SwiftUI code and identify slow layouts

示例代码

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
import Combine
import SwiftUI

class FrequentUpdater: BindableObject {
var didChange = PassthroughSubject<Void, Never>()
var timer: Timer?

init() {
timer = Timer.scheduledTimer(
withTimeInterval: 0.01,
repeats: true
) { _ in
self.didChange.send(())
}
}
}

struct ContentView : View {
@ObjectBinding var updater = FrequentUpdater()
@State var tapCount = 0

var body: some View {
VStack {
Text("\(UUID().uuidString)")

Button(action: {
self.tapCount += 1
}) {
Text("Tap count: \(tapCount)")
}
}
}
}

检测我们的代码

默认情况下,SwiftUI工具告诉我们各种各样的事情:

1.在此期间创建了多少视图以及创建它们需要多长时间(“View Body”)
2.视图的属性是什么以及它们如何随时间变化(“View Properties”)
3.发生了多少次Core Animation提交(“Core Animation Commits”)
4.每个函数调用的确切时间(“Time Profiler”)

监视body调用

如果您选择View Body轨道 - 这是instrument列表中的第一行 - 您应该能够看到乐器将结果分解为SwiftUI和您的项目,前者是原始类型,如文本视图和按钮,以及后者包含您的自定义视图类型。在我们的例子中,这意味着“ContentView”应该出现在自定义视图中,因为这是我们视图的名称。

现在,您在这里看不到的是您的代码与SwiftUI视图的完美一对一映射,因为SwiftUI积极地折叠其视图层次结构以尽可能少地完成工作。所以,不要指望在代码中看到任何VStack创建 - 这个应用程序实际上是免费的。

在这个屏幕上,重要的数字是计数和平均持续时间 - 每件事创建的次数,以及花费的时间。因为这是一个压力测试你应该看到非常高的数字,但我们的布局是微不足道的,所以平均持续时间可能是几十微秒。

img

跟踪状态(state)变化

接下来,选择“View Properties”轨道,这是仪器列表中的第二行。这将显示所有视图的所有属性,包括其当前值和所有先前值。

我们的示例应用程序有一个按钮,通过在数字中添加一个来更改其标签,并且在此工具中可见 - 请查看视图类型ContentView和属性类型State

可悲的是,Instruments还没有(还能)向我们展示那里的确切属性名称,如果你跟踪了几个整数状态,这可能会更加令人困惑。然而,它确实有一个不同的技巧:在记录窗口的顶部是一个标记当前视图位置的箭头,如果你拖动它,你会看到应用程序状态随时间的变化 - 每次你点击按钮,你会看到状态整数上升一个,你可以前进和后退来看它发生。

这可以释放巨大的能力,因为它可以让我们直接看到状态变化导致慢速重绘或其他工作 - 这几乎就像是在时间机器中,您可以在运行期间的每个点检查应用程序的确切状态。

img

识别慢速绘图

虽然SwiftUI能够直接调用Metal以提高性能,但大多数情况下它更喜欢使用Core Animation进行渲染。这意味着我们会自动从Instruments获取内置的Core Animation分析工具,包括检测昂贵提交(expensive commits)的能力。

当多个更改放在一个组中时,Core Animation的效果最佳,称为transaction。我们在一个事务中有效地堆叠了一系列工作,然后要求CA继续渲染工作 - 称为提交事务。

因此,当Instruments向我们展示昂贵的Core Animation提交时,它真正向我们展示的是SwiftUI因为更新而被迫重绘屏幕上的像素的次数。理论上,这应该只在我们的应用程序的实际状态导致不同的视图层次结构时发生,因为SwiftUI应该能够将我们的body属性的新输出与先前的输出进行比较。

img

寻找缓慢的函数调用

Time Profiler,它向我们展示了在代码的每个部分花费了多少时间。这与乐器中的常规时间分析器完全相同,但如果您之前没有尝试过,那么您至少需要知道:

  1. 右侧的扩展详细信息窗口默认显示最重的堆栈跟踪,这是运行时间最长的代码段。明亮的代码(白色或黑色,取决于您的macOS配色方案)是您编写的代码;昏暗代码(灰色)是系统库代码。

  2. 在左侧,您可以看到创建的所有线程,以及公开指示器,让您深入了解它们调用的函数以及这些函数调用的函数等。大多数工作将在“start”内部进行。

  3. 为避免混乱,您可能需要单击底部的“调用树”按钮,然后选择“隐藏系统库”。这只会显示您编写的代码,但是如果您的问题是您使用的系统库很糟糕,这可能没有帮助。

  4. 要直接了解具体细节,您还可以单击“调用树”并选择“反转调用树”以翻转事物,以便叶子功能(树末端的功能)显示在顶部,现在可以向下钻取公开指示器(向上钻取?)到调用它们的函数。

最后一些提示

在您收取配置自己的代码之前,有一些事情需要注意:

  1. 在检查应用程序性能的一小部分时,您应该单击并拖动相关范围,以便仅查看该应用程序部分的统计信息。这使您可以专注于特定操作的性能,例如响应按下按钮。
  2. 即使你在仪器中看到纯色条,它们只是从远处看起来那样 - 你可以通过按住Cmd并按 - 和+来查看更多细节
  3. 要获得最准确的数字,请始终在真实设备上进行配置。
  4. 如果要通过分析代码进行更改,请始终一次进行一次更改。如果你进行两次更改,可能会使你的性能提高20%而另一种会降低10%,但是将它们合在一起意味着你可能会认为它们整体性能提高了10%。
  5. Instruments在release模式下运行您的代码,从而实现Swift的所有优化。这也会影响您添加到代码中的任何调试标志,因此请小心。

What now?

How to continue learning SwiftUI after the basics

SwiftUI tips and tricks

SwiftUI拥有强大的标题功能,但也有许多较小的提示和技巧可以帮助您编写更好的应用程序。

@State 设为私有

1
@State private var score = 0

具有常量绑定的原型

1
2
TextField(.constant("Hello"))
.textFieldStyle(.roundedBorder)

使用语义颜色

1
Color.red

依靠自适应填充

1
2
Text("Row 1")
.padding(10)

合并文本视图

1
2
3
4
5
6
7
8
9
10
11
12
struct ContentView : View {
var body: some View {
Text("Colored ")
.color(.red)
+
Text("SwifUI ")
.color(.green)
+
Text("Text")
.color(.blue)
}
}

如何使print()工作

右键单击预览画布(preview canvas)中的播放按钮,然后选择“调试预览(Debug Preview)”。

依靠隐式HStack

1
2
3
4
5
6
7
8
9
10
struct ContentView : View {
let imageNames = ["paul-hudson", "swiftui"]

var body: some View {
List(imageNames.identified(by: \.self)) { image in
Image(image).resizable().frame(width: 40)
Text(image)
}
}
}

分割大视图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct ContentView : View {
let users = ["Paul Hudson", "Taylor Swift"]

var body: some View {
NavigationView {
List(users.identified(by: \.self)) { user in
NavigationButton(destination: Text("Detail View")) {
Image("example-image").resizable().frame(width: 50, height: 50)

VStack(alignment: .leading) {
Text("Johnny Appleseed").font(.headline)
Text("Occupation: Programmer")
}
}
}.navigationBarTitle(Text("Users"))
}
}
}

更好的预览

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#if DEBUG
struct ContentView_Previews : PreviewProvider {
static var previews: some View {
Group {
ContentView()
.environment(\.sizeCategory, .accessibilityExtraExtraExtraLarge)
ContentView()
.environment(\.colorScheme, .dark)
NavigationView {
ContentView()
}
}
}
}
#endif

创建自定义修改器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct PrimaryLabel: ViewModifier {
func body(content: Content) -> some View {
content
.padding()
.background(Color.black)
.foregroundColor(Color.white)
.font(.largeTitle)
.cornerRadius(10)
}
}

struct ContentView : View {
var body: some View {
Text("Hello World")
.modifier(PrimaryLabel())
}
}

动画变化很容易

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct ContentView : View {
@State var showingWelcome = false

var body: some View {
VStack {
Toggle(isOn: $showingWelcome.animation(.spring())) {
Text("Toggle label")
}

if showingWelcome {
Text("Hello World")
}
}
}
}

微星 MSI Z390 GAMING PRO CARBON AC 华硕 Vega64 黑苹果

机器配置

主板:微星 MSI MPG Z390 GAMING PRO CARBON AC

CPU:Intel i9 9900K

内存:Avexir DDR4 2400

显卡:华硕 ASUS AREZ-STRIX-RXVEGA64-O8G-GAMING

电源:EVGA SuperNOVA 750 G+

散热:MASTERLIQUID ML240R RGB

固态:三星 970 EVO 500G

板载声卡:ALC S1220A

板载有线网卡:Intel I219V7

显示器:戴尔 P2715Q 4K显示器

机箱:追风者 PK-515ETG

安装过程

重要提示:已降 BIOS 至 Version 7B17v10,不在卡 no nvram variable。

  1. 【黑果小兵】macOS Mojave 10.14.3 18D42 正式版 with Clover 4859原版镜像
  2. 执行:300系列主板请于drivers64UEFI目录中移除AptioMemoryFix-64.efi添加OsxAptioFix2Drv-free2000.efi该驱动位于**/EFI/CLOVER/drivers-off目录下。顺利进入macOS**安装界面
  3. 系统后序安装和复制 EFI 到硬盘教程:联想小新Air 13安装黑苹果兼macOS Mojave安装教程
  4. 进入系统设置页面前:卡在 AppleUSBHostResources panic。解决方案是boot 添加 safe mode
  5. 合并 WindowsClover 引导
  6. 完善驱动
  7. 结束

附图

![Hackintosh-i9 2019-03-15](https://gewill.org/assets/Hackintosh-i9 2019-03-15.png)总结

  1. 小兵教程很好用。
  2. 就是一直卡在 BIOS 部分,所以无论做什么努力的都没有效果。所以购买主板一定要看 tonymacx86.com 的 Buyer’s Guide 和网友的成功贴。我只之前微星 Z170A M3 安装黑苹果成功,就想当然的买了微星的这块主板。活生生的因为 BIOS 问题,至少1个月的安装时间浪费了。
  3. 买主板别买带无解的无线网卡,明明可以省200块,去买同系列的微星 MPG Z390 GAMING PRO CARBON。然后去买免驱的无线蓝牙网卡:Broadcom 94360CD。

EFI

EFI已上传 https://github.com/gewill/Hackintosh-Installation-with-MSI-Z390-and-i9-9900K

处理方法

由于环信将 i386 x86_64 armv7 arm64 几个平台都合并到了一起,所以使用动态库上传appstore时需要将i386 x86_64两个平台删除后,才能正常提交审核。

所以我们使用 fastlane sh 在打包gym 时前后分别将 x86 的移除remove86framework和添加add86framework 。这样就可以继续使用 fastlane 一键打包。

如果出错,使用 pod 删除并重新安装完整版的环信 SDK。

Fastfile

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
38
39
40
41
desc "Deploy a new version to the App Store"
lane :release do
remove86framework
gym
add86framework
deliver(force: true)
# frameit
end

desc "Remove x86 framework"
lane :remove86framework do
sh(%(
cd ../Pods/HyphenateLite/iOS_Pods_IMSDK*
pwd

if [ ! -d "./bak" ]; then
mkdir ./bak
fi
if [ -d "./bak/HyphenateLite.framework" ]; then
rm -rf ./bak/HyphenateLite.framework
fi
cp -r HyphenateLite.framework ./bak
lipo HyphenateLite.framework/HyphenateLite -thin armv7 -output HyphenateLite_armv7
lipo HyphenateLite.framework/HyphenateLite -thin arm64 -output HyphenateLite_arm64
lipo -create HyphenateLite_armv7 HyphenateLite_arm64 -output HyphenateLite
mv HyphenateLite HyphenateLite.framework/

))
end

desc "Add x86 framework back"
lane :add86framework do
sh(%(
cd ../Pods/HyphenateLite/iOS_Pods_IMSDK*
pwd
if [ -d "./HyphenateLite.framework" ]; then
rm -rf ./HyphenateLite.framework
fi
cp -r ./bak/HyphenateLite.framework HyphenateLite.framework
))
end

参考文档

  1. 环信:http://docs.easemob.com/im/300iosclientintegration/20iossdkimport#%E9%9B%86%E6%88%90%E5%8A%A8%E6%80%81%E5%BA%93%E4%B8%8A%E4%BC%A0appstore
  2. fastlane sh 文档:https://docs.fastlane.tools/actions/sh/

一旦你开始编写完整的应用程序与 RxSwift 和 RxCocoa, 你也需要照顾更多的中间主题比简单地观察事件和处理他们与 Rx。

在一个完整的生产质量应用程序中, 您需要构建一个错误处理策略, 执行更高级的多线程处理, 创建一个坚实的测试套件等等。

在这部分, 你将通过学习四个挑战性的章节, 这将解除您的 Rx 从一个菜鸟级的状态到一个实战经验的战士。

第十四章:错误处理实践

生活将是美好的, 如果我们生活在一个完美的世界, 但不幸的事情往往不像预期的那样去。即使是最好的 RxSwift 开发人员也不能避免遇到错误, 因此他们需要知道如何优雅和高效地处理它们。在本章中, 您将学习如何处理错误, 如何通过重试来管理错误恢复, 或者只是向整个宇宙投降, 让错误继续。

管理错误

常见错误类型:网络连接失败、无效的输入和API错误或者HTTP错误。

在 RxSwift 中, 错误处理是框架的一部分, 可以通过两种方式进行处理:  

  • Catch: 从错误中恢复并使用默认值。 
  • Retry: 重试有限 (或无限制) 次数。

本章项目的起始版本没有任何实际的错误处理。所有错误都是用一个返回虚拟版本的 catchErrorJustReturn 捕获的。这听起来像是一个方便的解决方案, 但是在 RxSwift 中有更好的处理方法。在任何一流的应用程序中, 都应该有一个一致的、信息错误处理方法。

抛出错误

一个好的起点是处理 RxCocoa 错误,它包装底层 Apple 框架返回的系统错误。

1
public func data(request: URLRequest) -> Observable<Data> {...}
1
2
3
4
5
if 200 ..< 300 ~= response.statusCode { 
return data
} else {
throw RxCocoaURLError.httpRequestFailed(response: response, data: data)
}

用 catch 处理错误

在解释如何抛出错误之后, 是时候看看如何处理错误了。最基本的方法是使用 catch。catch 的工作方式很像在Swift 中的 do-try-catch 流程。执行可观察的操作, 如果出现错误, 则返回包装错误的事件。

在 RxSwift 中有两个主要的运算符来捕获错误。

1
func catchError(_ handler:) -> RxSwift.Observable<Self.E>
1
func catchErrorJustReturn(_ element:) -> RxSwift.Observable<Self.E>

一个常见的陷阱

链式调用时候,一个错误就会导致整个订阅以错误结束。有的时候我们想知道错误的仅有一步细节,如 HTTP 404 错误,属于 API 返回错误的具体信息的约定,就需要移除 .catchErrorJustReturn(ApiController.Weather.empty) 来避免出现不期望的错误事件发射。

捕捉错误

我们先定义一个属性来缓存天气数据。

1
var cache = [String: Weather]()

再把输入框的序列改为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
let textSearch = searchInput.flatMap { text in
return ApiController.shared.currentWeather(city: text ?? "Error")
.do(onNext: { data in
if let text = text {
self.cache[text] = data
}
}, onError: { [weak self] e in
guard let strongSelf = self else { return }
DispatchQueue.main.async {
strongSelf.showError(error: e)
}
})
.retryWhen(retryHandler)
.catchError { error in
if let text = text, let cachedData = self.cache[text] {
return Observable.just(cachedData)
} else {
return Observable.just(ApiController.Weather.empty)
}
}
}

错误的重试机制

关于重试上面已经设计到了 .retryWhen(retryHandler),具体处理实现如下:

1
2
3
4
5
6
7
8
9
10
11
let retryHandler: (Observable<Error>) -> Observable<Int> = { e in
return e.enumerated().flatMap { (attempt, error) -> Observable<Int> in
if attempt >= maxAttempts - 1 {
return Observable.error(error)
} else if let casted = error as? ApiController.ApiError, casted == .invalidKey {
return ApiController.shared.apiKey.filter {$0 != ""}.map { _ in return 1 }
}
print("== retrying after \(attempt + 1) seconds ==")
return Observable<Int>.timer(Double(attempt + 1), scheduler: MainScheduler.instance).take(1)
}
}

或者简化版:.retry(3)

自定义错误

创建自定义错误遵循一般的 Swift 原则, 所以没有一个好的 Swift 程序员不会知道, 但它仍然是好的, 看看如何处理错误和创建定制的操作符。

创建自定义错误

先定义错误枚举,然后在结果过滤中抛出定义的错误即可。

1
2
3
4
5
enum ApiError: Error {
case cityNotFound
case serverFailure
case invalidKey
}
1
2
3
4
5
6
7
8
9
10
11
return session.rx.response(request: request).map() { response, data in
if 200 ..< 300 ~= response.statusCode {
return try JSON(data: data)
} else if response.statusCode == 401 {
throw ApiError.invalidKey
} else if 400 ..< 500 ~= response.statusCode {
throw ApiError.cityNotFound
} else {
throw ApiError.serverFailure
}
}

订阅者可以根据实际场景把错误展示给用户,这部分和 Swift 中自定义错误一致,就不展开讲了。

错误处理进阶

比如处理API 中 401 类型,需要提示用户授权失败,需要重新登录或授权。本例中需要是指API Key 失效,需要用户填写新的有效的API Key。

1
2
3
else if response.statusCode == 401 {
throw ApiError.invalidKey
}

Materialize 和 dematerialize

materialize 和 dematerialize 通常一起使用, 并且有能力完全打破原始可观察的合同。当没有其他选择来处理特定的情况时, 请小心地使用它们, 并且只有在必要的时候。

常见的应用是:进行日志记录

1
2
3
4
5
observableToLog.materialize()
.do(onNext: { (event) in
myAdvancedLogEvent(event)
})
.dematerialize()

第十五章:介绍调度程序

到目前为止,您已经设法使用调度程序,同时避免任何有关它们如何处理线程或并发的解释。 在前面的章节中,您使用了隐式使用某种并发/线程级别的方法,例如bufferdelaySubscriptioninterval运算符。

您可能感觉调度程序有一些神奇的东西,但在您了解调度程序之前,您还需要了解那些observeOn函数的全部内容。

本章将介绍调度程序背后的美观,在这里您将了解为什么Rx抽象如此强大以及为什么使用异步编程远比使用锁或队列更省心。

调度程序到底是什么?

在你着手学习时调度程序, 了解他们是什么和他们不是什么是很重要的。总而言之, 调度程序是一个过程发生的上下文。此上下文可以是线程、调度队列或类似的实体, 甚至是在 OperationQueueScheduler 内部使用的 NSOperation。

下图是个好的例子:

schedulerExample

在此图中,具有缓存运算符的概念。 observable向服务器发出请求并检索一些数据。此数据由名为cache的自定义运算符处理,该运算符将数据存储在某处。 在此之后,数据被传递给不同调度程序中的所有订阅者,很可能是位于主线程之上的MainScheduler,使得UI的更新成为可能。

揭开调度程序

调度程序虽然工作方式和GCD类似,却不是等价的。

要记住的重要一点是, 调度程序不是线程, 并且它们没有与线程的一对一关系。始终检查计划程序执行操作的上下文, 而不是线程。在本章的后面部分, 您将遇到一些好的例子来帮助您理解这一点。

设置项目

编写两个函数打印当前调度程序所在线程。

切换调度程序

Rx 中最重要的事情之一是随时切换调度程序的能力, 没有任何限制, 除了内部进程生成事件所强加的约束之外。

fruit 是在主线程上生成的, 但是将它移动到后台线程是很好的。要在后台线程中创建 fruit , 必须使用 subscribeOn

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let fruit = Observable<String>.create { observer in
observer.onNext("[apple]")
sleep(2)
observer.onNext("[pineapple]")
sleep(2)
observer.onNext("[strawberry]")
return Disposables.create()
}

fruit
.subscribeOn(globalScheduler)
.dump()
.observeOn(MainScheduler.instance)
.dumpingSubscription()
.disposed(by: bag)

观察是 Rx 的三基本概念之一。它涉及实体生成事件, 以及这些事件的观察者。在这种情况下, 在对应 subscribeOn 的情况下, 操作符 observeOn 改变了观察发生的调度程序。

这是一个非常常见的模式。您使用后台进程从服务器检索数据并处理接收的数据, 只切换到 MainScheduler 处理 final 事件并在用户界面中显示数据。

陷阱

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
let globalScheduler = ConcurrentDispatchQueueScheduler(queue: DispatchQueue.global())
let bag = DisposeBag()
let animal = BehaviorSubject(value: "[dog]")


let animalsThread = Thread() {
sleep(3)
animal.onNext("[cat]")
sleep(3)
animal.onNext("[tiger]")
sleep(3)
animal.onNext("[fox]")
sleep(3)
animal.onNext("[leopard]")
}

animalsThread.name = "Animals Thread"
animalsThread.start()


animal.subscribeOn(MainScheduler.instance)
.dump()
.observeOn(globalScheduler)
.dumpingSubscription()
.disposed(by:bag)


RunLoop.main.run(until: Date(timeIntervalSinceNow: 13))

输出结果:

1
2
3
4
5
6
7
8
9
10
00s | [D] [dog] received on Main Thread
00s | [S] [dog] received on Anonymous Thread
03s | [D] [cat] received on Animals Thread
03s | [S] [cat] received on Anonymous Thread
06s | [D] [tiger] received on Animals Thread
06s | [S] [tiger] received on Anonymous Thread
09s | [D] [fox] received on Animals Thread
09s | [S] [fox] received on Anonymous Thread
12s | [D] [leopard] received on Animals Thread
12s | [S] [leopard] received on Anonymous Thread

结果出人意料,并没有如愿发生在主线程上。这是一个常见的和危险的陷阱, 它来自于在默认情况下认为 Rx 是异步的或多线程的,但这不是事实。

Rx 和一般抽象是自由线程的;在处理数据时, 没有发生魔术般的线程切换。如果不指定其他的线程, 则始终在原始线程上执行计算。

任何线程切换都是在程序员使用运算符 subscribeOnobserveOn 的显式请求之后发生的。

认为 Rx 做一些线程处理默认会陷入一个常见的陷阱。上面发生的事情是对 Subject 的误用。原始计算在指定的线程上发生, 并且这些事件使用 Thread() { ... } 在该线程中被推入。由于 Subject 的性质, Rx 没有能力切换原始的计算调度程序, 并移动到另一个线程, 因为没有直接控制的 Subject 被推出。

但是下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
let globalScheduler = ConcurrentDispatchQueueScheduler(queue: DispatchQueue.global())
let bag = DisposeBag()
let animal = BehaviorSubject(value: "[dog]")

let fruit = Observable<String>.create { observer in
observer.onNext("[apple]")
sleep(2)
observer.onNext("[pineapple]")
sleep(2)
observer.onNext("[strawberry]")
return Disposables.create()
}

fruit.subscribeOn(globalScheduler)
.dump()
.observeOn(MainScheduler.instance)
.dumpingSubscription()
.disposed(by:bag)

RunLoop.main.run(until: Date(timeIntervalSinceNow: 13))

输出:

1
2
3
4
5
6
00s | [D] [apple] received on Anonymous Thread
00s | [S] [apple] received on Main Thread
02s | [D] [pineapple] received on Anonymous Thread
02s | [S] [pineapple] received on Main Thread
04s | [D] [strawberry] received on Anonymous Thread
04s | [S] [strawberry] received on Main Thread

为什么这适用于fruit 线程呢? 这是因为使用Observable.create可以让Rx控制Thread块内部发生的事情,这样你就可以更加精确地定制线程处理。
这种意想不到的结果通常被称为“冷和热”可观测问题。
在上面的例子中,你正在处理热观察。 observable在订阅期间没有任何副作用,但它确实有自己的上下文,其中生成事件并且RxSwift无法控制它(即,它运行自己的Thread)。
相反,冷观察不会在任何观察者订阅之前产生任何元素。 这实际上意味着它没有自己的上下文,直到订阅时,它创建一些上下文并开始生成元素。

热 vs 冷

上面的部分谈到了冷热观测量的话题。冷热观测量的话题是相当固执己见的, 产生了很多争论, 所以让我们简单在这里看看。这个概念可以归结为一个非常简单的问题:

HotVSCold

一些副作用的例子是:

  • 向服务器发送请求
  • 编辑本地数据库
  • 写入文件系统
  • 发射火箭

最佳实践和内置调度程序

调度程序是一个非平凡的主题, 因此它们会为最常见的用例提供一些最佳实践。在本节中, 您将获得串行和并发调度程序的快速介绍, 了解它们如何处理数据并查看哪种类型对特定上下文更有效。

串行与并发调度程序

虑到调度程序只是一个上下文, 它可以是任何东西 (调度队列、线程、自定义上下文), 并且所有转换序列的运算符都需要保留隐式保证, 因此需要确保您使用的是正确的计划程序。

  • 如果您使用的是串行调度程序, Rx 将按顺序进行计算。对于串行调度队列, 调度程序还可以在底层执行自己的优化。

  • 在并发计划程序中, Rx 将尝试同时运行代码, 但 observeOnsubscribeOn 将保留执行任务所需的顺序, 并确保订阅代码在正确的计划程序上结束。

    MainScheduler

MainScheduler 位于主线程的顶端。此计划程序用于处理用户界面上的更改并执行其他高优先级任务。作为在 iOS、tvOS 或 macOS 上开发应用程序的一般做法, 不应使用此计划程序执行长时间运行的任务, 因此应避免诸如服务器请求或其他繁重任务之类的事情。

MainScheduler 还用于在使用单位和更多特定的(如:Driver)执行所有计算。如前一章所述, Driver 确保始终在 MainScheduler 中执行计算, 以使您能够将数据直接绑定到应用程序的用户界面。

SerialDispatchQueueScheduler

SerialDispatchQueueScheduler 负责在串行 DispatchQueue 。这个调度程序在使用 observeOn 时,有很多优化的优势。

你可以使用此计划程序来处理以串行方式更好地安排的后台作业。例如, 如果应用程序与服务器的单个路径 (如Firebase 或 GraphQL 应用程序) 进行对话, 则可能需要避免调度多个同时请求, 这会给接收端造成太大的压力。这个调度程序是你绝对想要的, 像串行任务队列一样先进。

ConcurrentDispatchQueueScheduler

ConcurrentDispatchQueueScheduler 与 SerialDispatchQueueScheduler类似,负责在DispatchQueue上抽象工作。 主要区别在于,调度程序使用并行队列而不是串行队列。

此类调度程序在使用 observeOn 时没有进行优化, 因此在决定使用哪种调度程序时, 请记住说明这一点。

对于需要同时结束的多个长时间运行的任务,并发的调度程序可能是一个不错的选择。 将多个可观察对象与阻塞运算符组合在一起,以便在准备好时将所有结果组合在一起,可以防止串行调度程序以最佳状态执行。 相反,并发调度程序可以执行多个并发任务并优化结果的收集。

OperationQueueScheduler

OperationQueueScheduler类似于ConcurrentDispatchQueueScheduler,但不是通过DispatchQueue抽象工作,它在NSOperationQueue上执行工作。 有时你需要对正在运行的并发作业进行更多控制,而对于并发DispatchQueue则无法做到这一点。

如果需要调整最大并发作业数,则这是作业的调度程序。 您可以定义maxConcurrentOperationCount来限制并发操作的数量以满足您的应用程序的需要。

TestScheduler

TestScheduler是一种特殊的野兽。 它仅用于测试,因此尽量不要在生产代码中使用此调度程序。 这种特殊的调度程序简化了操作员测试; 它是RxTest库的一部分。 您将在关于测试的专用章节中了解如何使用此调度程序,但是让我们快速浏览一下,因为您正在进行调度程序的全程浏览。

第十六章:使用 RxTest 进行测试

本章将向您介绍 RxTest, 以及以后的 RxBlocking, 通过编写针对多个 RxSwift 操作的测试, 还可以编写针对生产 RxSwift 代码的测试。

本章例子是:一个将Hex颜色值转换为RGB值的应用。架构是MVVM。

使用 RxTest 测试操作符

RxTest 是 RxSwift 外一个独立的库。它是托管在 RxSwift 仓库, 但需要一个单独的 pod 安装和导入。RxTest 为测试 RxSwift 代码提供了许多有用的补充, 如 TestScheduler, 它是一个虚拟时间计划程序, 它使你可以对测试的时间线性操作进行粒状控制, 并且包括 next(_:_:), completed(_:_:)error(_:_:) 在测试中 specified 时间启用将这些事件添加到观测量中。它还增加了热和冷的观测量, 你可以想到的热和冷三明治。当然, 不是真的三明治。

热和冷的序列是什么?

RxSwift 很长的时间来简化和简化你的 Rx 代码, 并且有热和冷的序列的区别, 当它涉及到观测量, 在 RxSwift 可以被认为是可观测的特征而不是具体的类型。

热序列:

  • 使用资源,无论是否有订阅者。
  • 生成元素,无论是否有订阅者。
  • 主要用于有状态类型,如变量。

冷序列:

  • 仅在订阅时消耗资源。
  • 仅在有订阅者时才生成元素。
  • 主要用于网络等异步操作。

下面是 amb 的测试用例:

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
func testAmb() {

let observer = scheduler.createObserver(String.self)

let observableA = scheduler.createHotObservable([
next(100, "a"),
next(110, "b"),
next(300, "c")
])

let observableB = scheduler.createHotObservable([
next(90, "1"),
next(200, "2"),
next(300, "3")
])

let ambObservable = observableA.amb(observableB)

scheduler.scheduleAt(0) {
self.subscription = ambObservable.subscribe(observer)
}

scheduler.start()

let results = observer.events.map {
$0.value.element!
}

XCTAssertEqual(results, ["1", "2", "3"])
}

上面是同步测试,如果想测试异步错误,最简单地是使用 RxBlocking

使用 RxBlocking

RxBlocking 是另一个库, 寄存在 RxSwift 仓库。它的主要目的是通过它的 toBlocking(timeout:) 方法将 observable 转换为 BlockingObservable

1
2
3
4
5
func testToArray() {
let scheduler = ConcurrentDispatchQueueScheduler(qos: .default)
let toArrayObservable = Observable.of(1, 2).subscribeOn(scheduler)
XCTAssertEqual(try! toArrayObservable.toBlocking().toArray(), [1, 2])
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func testToArrayMaterialized() {
let scheduler = ConcurrentDispatchQueueScheduler(qos: .default)

let toArrayObservable = Observable.of(1, 2).subscribeOn(scheduler)

let result = toArrayObservable
.toBlocking()
.materialize()

switch result {
case .completed(elements: let elements):
XCTAssertEqual(elements, [1, 2])
case .failed(_, error: let error):
XCTFail(error.localizedDescription)
}
}

测试 RxSwift 生产代码

下面例子是测试MVVM中VM层:

使用 toBlocking 很方便编写异步测试。

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
import XCTest
import RxSwift
import RxCocoa
import RxTest
@testable import Testing

class TestingViewModel: XCTestCase {

var viewModel: ViewModel!
var scheduler: ConcurrentDispatchQueueScheduler!

override func setUp() {
super.setUp()

viewModel = ViewModel()
scheduler = ConcurrentDispatchQueueScheduler(qos: .default)
}

func testColorIsRedWhenHexStringIsFF0000_async() {

let disposeBag = DisposeBag()

let expect = expectation(description: #function)

let expectedColor = UIColor(red: 1.0, green: 0.0, blue: 0.0, alpha: 1.0)

var result: UIColor!

viewModel.color.asObservable()
.skip(1)
.subscribe(onNext: {
result = $0
expect.fulfill()
})
.disposed(by: disposeBag)

viewModel.hexString.value = "#ff0000"

waitForExpectations(timeout: 1.0) { error in
guard error == nil else {
XCTFail(error!.localizedDescription)
return
}

XCTAssertEqual(expectedColor, result)
}
}

func testColorIsRedWhenHexStringIsFF0000() {

let colorObservable = viewModel.color.asObservable().subscribeOn(scheduler)

viewModel.hexString.value = "#ff0000"

do {
guard let result = try colorObservable.toBlocking(timeout: 1.0).first() else { return }

XCTAssertEqual(result, .red)
} catch {
print(error)
}
}


func testRgbIs010WhenHexStringIs00FF00() {

let rgbObservable = viewModel.rgb.asObservable().subscribeOn(scheduler)

viewModel.hexString.value = "#00ff00"

let result = try! rgbObservable.toBlocking().first()!

XCTAssertEqual(0 * 255, result.0)
XCTAssertEqual(1 * 255, result.1)
XCTAssertEqual(0 * 255, result.2)
}

func testColorNameIsRayWenderlichGreenWhenHexStringIs006636() {

let colorNameObservable = viewModel.colorName.asObservable().subscribeOn(scheduler)

viewModel.hexString.value = "#006636"

XCTAssertEqual(try! colorNameObservable.toBlocking().first()!, "rayWenderlichGreen")
}
}

第十七章:创建自定义响应式拓展

在学习了RxSwift和RxCocoa,以及如何编写测试,这里我们将学习如何给Apple和第三方库编写RxSwift的拓展。本章例子是给NSURLSession编写拓展,来实现网络请求、缓存。但却是一个教学实例,如果生产项目可以有很多网络库可以直接使用,如RxAlamofire、RxMoya等。

如何创建拓展

创建一个Cocoa或库的拓展不是一个简单的任务。你会发现这个过程可能很棘手,你的解决方案在继续之前可能需要一些前期思考。

这里我们将学习如何拓展URLSession,加上Rx的命名空间。

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
extension Reactive where Base: URLSession {

func response(request: URLRequest) -> Observable<(HTTPURLResponse, Data)> {
return Observable.create { observer in
// content goes here

let task = self.base.dataTask(with: request) { (data, response, error) in
guard let response = response, let data = data else {
observer.on(.error(error ?? RxURLSessionError.unknown))
return
}

guard let httpResponse = response as? HTTPURLResponse else {
observer.on(.error(RxURLSessionError.invalidResponse(response: response)))
return
}
observer.onNext((httpResponse, data))
observer.on(.completed)
}
task.resume()

return Disposables.create(with: task.cancel)
}
}

func data(request: URLRequest) -> Observable<Data> {
if let url = request.url?.absoluteString, let data = internalCache[url] {
return Observable.just(data)
}

return response(request: request).cache().map { (response, data) -> Data in
if 200 ..< 300 ~= response.statusCode {
return data
} else {
throw RxURLSessionError.requestFailed(response: response, data: data)
}
}
}

func string(request: URLRequest) -> Observable<String> {
return data(request: request).map { d in
return String(data: d, encoding: .utf8) ?? ""
}
}

func json(request: URLRequest) -> Observable<JSON> {
return data(request: request).map { d in
return try JSON(data: d)
}
}

func image(request: URLRequest) -> Observable<UIImage> {
return data(request: request).map { d in
return UIImage(data: d) ?? UIImage()
}
}

}

如何创建自定义运算符

这里我们演示如何使用运算符进行缓存网络请求结果(HTTPURLResponse、Data)的序列,简单起见使用字典。

1
2
3
4
5
6
7
8
9
10
11
fileprivate var internalCache = [String: Data]()

extension ObservableType where E == (HTTPURLResponse, Data) {
func cache() -> Observable<E> {
return self.do(onNext: { (response, data) in
if let url = response.url?.absoluteString, 200 ..< 300 ~= response.statusCode {
internalCache[url] = data
}
})
}
}

如何使用封装的拓展

在cell中更具给定url下载gif并显示出来。

1
2
3
4
5
6
7
8
9
10
11
12
13
func downloadAndDisplay(gif stringUrl: String) {
guard let url = URL(string: stringUrl) else { return }
let request = URLRequest(url: url)
activityIndicator.startAnimating()

let s = URLSession.shared.rx.data(request: request)
.observeOn(MainScheduler.instance)
.subscribe(onNext: { imageData in
self.gifImageView.animate(withGIFData: imageData)
self.activityIndicator.stopAnimating()
})
disposable.setDisposable(s)
}

测试封装的拓展

这里我们使用RxNimble来辅助我们编写测试。RxNimble 使测试更容易编写, 并有助于代码更简明。

RxBlocking版本:

1
2
let result = try! observable.toBlocking().first()
expect(result) == 42

RxNimble版本:

1
expect(observable).first == 42

完整测试代码:

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
import XCTest
import RxSwift
import RxBlocking
import Nimble
import RxNimble
import OHHTTPStubs
import SwiftyJSON

@testable import iGif

class iGifTests: XCTestCase {

let obj = ["array":["foo","bar"], "foo":"bar"] as [String : Any]
let request = URLRequest(url: URL(string: "http://raywenderlich.com")!)
let errorRequest = URLRequest(url: URL(string: "http://rw.com")!)

override func setUp() {
super.setUp()
// Put setup code here. This method is called before the invocation of each test method in the class.
stub(condition: isHost("raywenderlich.com")) { _ in
return OHHTTPStubsResponse(jsonObject: self.obj, statusCode: 200, headers: nil)
}
stub(condition: isHost("rw.com")) { _ in
return OHHTTPStubsResponse(error: RxURLSessionError.unknown)
}
}

override func tearDown() {
// Put teardown code here. This method is called after the invocation of each test method in the class.
super.tearDown()
OHHTTPStubs.removeAllStubs()
}

func testData() {
let observable = URLSession.shared.rx.data(request: self.request)
expect(observable.toBlocking().firstOrNil()).toNot(beNil())
}

func testString() {
let observable = URLSession.shared.rx.string(request: self.request)
let string = "{\"array\":[\"foo\",\"bar\"],\"foo\":\"bar\"}"
expect(observable.toBlocking().firstOrNil()) == string
}

func testJSON() {
let observable = URLSession.shared.rx.json(request: self.request)
let string = "{\"array\":[\"foo\",\"bar\"],\"foo\":\"bar\"}"
let json = try? JSON(data: string.data(using: .utf8)!)
expect(observable.toBlocking().firstOrNil()) == json
}

func testError() {
var erroredCorrectly = false
let observable = URLSession.shared.rx.json(request: self.errorRequest)
do {
let _ = try observable.toBlocking().first()
assertionFailure()
} catch (RxURLSessionError.unknown) {
erroredCorrectly = true
} catch {
assertionFailure()
}
expect(erroredCorrectly) == true
}
}

extension BlockingObservable {
func firstOrNil() -> E? {
do {
return try first()
} catch {
return nil
}
}
}

常用封装

RxSwift社区非常活跃,并且已经有很多扩展和封装。 一些基于Apple组件,而另一些则基于许多iOS和macOS项目中广泛使用的第三方库。
你可以在 http://community.rxswift.org 找到最新的封装列表。

下面几个常用的封装:

  • RxDataSources
  • RxAlamofire
  • RxBluetoothKit

小结

关于何时需要抽象,没有真正明确的规则,但建议如果框架满足以下一个或多个条件,则应用此策略:

  • 使用带有完成和失败信息的回调
  • 使用大量委托异步返回信息
  • 需要与应用程序的其他RxSwift部分进行互操作

由于Rx是一个多平台框架,因此它不会对您的Rx驱动的应用程序运行的设备做出任何假设。 RxSwift严格遵循RxPython,RxRuby,RxJS和所有其他平台所遵循的通用API设计,因此它不包含任何特定功能或与UIKit或Cocoa的集成,以帮助您开发iOS或macOS。

RxCocoa是一个独立的库(尽管它与RxSwift捆绑在一起),它允许您使用许多预构建的功能来更好地与UIKit和Cocoa集成。

RxCocoa将为您提供开箱即用的类来进行反应式网络,对用户交互作出反应,将数据模型绑定到UI控件等等。

第十二章:开始学习 RxCocoa

更新天气信息界面,根据API获取的数据。

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
ApiController.shared.currentWeather(city: "RxSwift")
.observeOn(MainScheduler.instance)
.subscribe(onNext: { data in
self.tempLabel.text = "\(data.temperature)° C"
self.iconLabel.text = data.icon
self.humidityLabel.text = "\(data.humidity)%"
self.cityNameLabel.text = data.cityName
})
.disposed(by:bag)

let search = searchCityName.rx.controlEvent(.editingDidEndOnExit).asObservable()
.map { self.searchCityName.text }
.filter { ($0 ?? "").characters.count > 0 }
.flatMapLatest { text in
return ApiController.shared.currentWeather(city: text ?? "Error")
.catchErrorJustReturn(ApiController.Weather.empty)
}
.asDriver(onErrorJustReturn: ApiController.Weather.empty)

search.map { "\($0.temperature)° C" }
.drive(tempLabel.rx.text)
.disposed(by:bag)

search.map { $0.icon }
.drive(iconLabel.rx.text)
.disposed(by:bag)

search.map { "\($0.humidity)%" }
.drive(humidityLabel.rx.text)
.disposed(by:bag)

search.map { $0.cityName }
.drive(cityNameLabel.rx.text)
.disposed(by:bag)

第十三章:RxCocoa 中级

添加搜索框支持

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
override func viewDidLoad() {
super.viewDidLoad()
style()

let searchInput = searchCityName.rx.controlEvent(.editingDidEndOnExit).asObservable()
.map { self.searchCityName.text }
.filter { ($0 ?? "").count > 0 }

let textSearch = searchInput.flatMap { text in
return ApiController.shared.currentWeather(city: text ?? "Error")
.catchErrorJustReturn(ApiController.Weather.dummy)
}

let mapInput = mapView.rx.regionDidChangeAnimated
.skip(1)
.map { _ in self.mapView.centerCoordinate }

let mapSearch = mapInput.flatMap { coordinate in
return ApiController.shared.currentWeather(lat: coordinate.latitude, lon: coordinate.longitude)
.catchErrorJustReturn(ApiController.Weather.dummy)
}

let currentLocation = locationManager.rx.didUpdateLocations
.map { locations in
return locations[0]
}
.filter { location in
return location.horizontalAccuracy < kCLLocationAccuracyHundredMeters
}

let geoInput = geoLocationButton.rx.tap.asObservable()
.do(onNext: {
self.locationManager.requestWhenInUseAuthorization()
self.locationManager.startUpdatingLocation()
})

let geoLocation = geoInput.flatMap {
return currentLocation.take(1)
}

let geoSearch = geoLocation.flatMap { location in
return ApiController.shared.currentWeather(lat: location.coordinate.latitude, lon: location.coordinate.longitude)
.catchErrorJustReturn(ApiController.Weather.dummy)
}

let search = Observable.from([geoSearch, textSearch, mapSearch])
.merge()
.asDriver(onErrorJustReturn: ApiController.Weather.dummy)

let running = Observable.from([searchInput.map { _ in true },
geoInput.map { _ in true },
mapInput.map { _ in true},
search.map { _ in false }.asObservable()])
.merge()
.startWith(true)
.asDriver(onErrorJustReturn: false)

running
.skip(1)
.drive(activityIndicator.rx.isAnimating)
.disposed(by: bag)

running
.drive(tempLabel.rx.isHidden)
.disposed(by: bag)

running
.drive(iconLabel.rx.isHidden)
.disposed(by: bag)

running
.drive(humidityLabel.rx.isHidden)
.disposed(by: bag)

running
.drive(cityNameLabel.rx.isHidden)
.disposed(by: bag)

search.map { "\($0.temperature)° C" }
.drive(tempLabel.rx.text)
.disposed(by: bag)

search.map { $0.icon }
.drive(iconLabel.rx.text)
.disposed(by: bag)

search.map { "\($0.humidity)%" }
.drive(humidityLabel.rx.text)
.disposed(by: bag)

search.map { $0.cityName }
.drive(cityNameLabel.rx.text)
.disposed(by: bag)

locationManager.rx.didUpdateLocations
.subscribe(onNext: { locations in
print(locations)
})
.disposed(by: bag)

mapButton.rx.tap
.subscribe(onNext: {
self.mapView.isHidden = !self.mapView.isHidden
})
.disposed(by: bag)

mapView.rx.setDelegate(self)
.disposed(by: bag)

search.map { [$0.overlay()] }
.drive(mapView.rx.overlays)
.disposed(by: bag)
}

第五章:Filtering Operator

Ignoring

忽略所有event,转化为一个 Comletable。

1
func ignoreElements() -> Completable

仅发射指定index的event,其余都忽略。

1
func elementAt(_ index: Int) -> Observable<String>

或者直接使用 .filter ,是 RxSwift 对 Swift 标准库中 .filter 对应版本。具体用法很简单,和 Swift 版一样。

Skipping

.skip(n) 即可跳过前 n 个 event。

.skipWhile 跳过符合条件的 event,但是遇到不符合条件的 event 后,不再跳过该 event 以及后面的event。

1
2
3
4
5
6
7
8
9
Observable.of(2, 2, 3, 4, 5)
.skipWhile { integer in
print("\(integer) skipWhile")
return integer % 2 == 0
}
.subscribe(onNext: {
print($0)
})
.disposed(by: disposeBag)

Console 输出:

1
2
3
4
5
6
2 skipWhile
2 skipWhile
3 skipWhile
3
4
5

.skipUntil(trigger) 持续跳过直到另一个Observable生成元素。

Taking

Taking 和 Skipping 是相反的。

.take(n) 只发射前 n 个 event 。

.takeWhile { } 放射符合条件的event,直到遇到不符合条件的元素,不再发射。

1
2
3
4
5
6
7
8
9
10
11
12
let disposeBag = DisposeBag()

Observable.of(2, 2, 4, 4, 6, 6)
.enumerated()
.takeWhile { index, integer in
integer % 2 == 0 && index < 3
}
.map { $0.element }
.subscribe(onNext: {
print($0)
})
.disposed(by: disposeBag)

Console 输出:

1
2
3
2
2
4

.takeUntil(trigger) 类似的,持续发射,直到另一个Observable生成元素,后面不再发射。

Distinct

.distinctUntilChanged() 只发射不同于前一个的元素。

1
2
3
4
5
6
Observable.of("A", "A", "B", "B", "A")
.distinctUntilChanged()
.subscribe(onNext: {
print($0)
})
.disposed(by: disposeBag)

Console 输出:

1
2
3
A
B
A

distinctUntilChanged(_:) 对阻止重复发射不符合 Equatable 类型的元素时很有用。因为你可以控制前后两个元素是否相等。

第六章:Filtering 实践

上一章介绍了RxSwift中函数式编程的观念。操作符操作一个Observable,输出一个心得Observable。得益于此,我么可以链式调用操作符。

改进 Combinestagram 项目

就是把上一章的 filtering 操作符应用在项目中。

基于时间的操作符

基于时间的操作符使用 Scheduler

take(_:scheduler:) 是一个过滤操作符,和 take(1)takeWhile(...) 类似。

只会发送指定时间段内的元素。一旦时间点过了,就会发送 .complete 事件。

1
2
3
4
5
6
7
8
9
10
11
private func errorMessage() {
alert(title: "No access to Camera Roll",
text: "You can grant access to Combinestagram from the Settings app")
.asObservable()
.take(5.0, scheduler: MainScheduler.instance)
.subscribe(onCompleted: { [weak self] in
self?.dismiss(animated: true, completion: nil)
_ = self?.navigationController?.popViewController(animated: true)
})
.disposed(by: bag)
}

以下是一些合适使用 throttle 的场景:

  • 搜索输入框的订阅,然后发送当前文本内容的API请求。通过 throttle , 你可以让用户快速输入,同时仅在用户完成输入时才你的向服务器发送请求。
  • 当用户点击按钮来展示一个 modal view controller 时,你可以阻止双击以及展示两次 modal view controller。通过 throttle 点击事件,可以现实只接收最后一次点击事件,不论用户双击或三击。
  • 如果你只关心用户拖动手势时停留的地方。你可以使用 throttle 当前触摸位置,只考虑那些停止改变的位置的元素。
1
2
3
4
5
6
7
8
images.asObservable()
.throttle(0.5, scheduler: MainScheduler.instance)
.subscribe(onNext: { [weak self] photos in
guard let preview = self?.imagePreview else { return }
preview.image = UIImage.collage(images: photos,
size: preview.frame.size)
})
.disposed(by: bag)

第七章:Transforming Operator

当你决定学习 RxSwift,或许会觉得它是深奥的库。也许让你想起来当初学习 iOS 或者 Swift 的时候。但学习到第七章你应该意识到 RxSwift 并非魔法。它是精心构建的API,能让极大地提升效率和简化代码。学到这里你应该会感觉不错。

本章你将学习运算符中最重要的一类:transforming 运算符。你将一直使用 transforming,准备Observable数据以供订阅者使用。需要强调的是,RxSwift 中的 transforming 运算符与 Swift 标准库之间有相似之处 ,如 map (_:)flatMap (_:)

Transforming 元素

将可观察到的单个元素转换为所有这些元素的数组的一种简便方法是使用 toArraytoArray

1
2
3
4
5
6
7
let disposeBag = DisposeBag()
Observable.of("A", "B", "C")
.toArray()
.subscribe(onNext: {
print($0)
})
.disposed(by: disposeBag)

map 可将每个元素进行 transforming 。map

1
2
3
4
5
6
7
8
9
10
11
12
let disposeBag = DisposeBag()
let formatter = NumberFormatter()
formatter.numberStyle = .spellOut

Observable<NSNumber>.of(123, 4, 56)
.map {
formatter.string(from: $0) ?? ""
}
.subscribe(onNext: {
print($0)
})
.disposed(by: disposeBag)
1
2
3
4
5
6
7
8
9
10
let disposeBag = DisposeBag()
Observable.of(1, 2, 3, 4, 5, 6)
.enumerated()
.map { index, integer in
index > 2 ? integer * 2 : integer
}
.subscribe(onNext: {
print($0)
})
.disposed(by: disposeBag)

Transforming 内部 Observable

RxSwift 包含 flatMap 系列中的几个运算符, 允许你深入Observable,处理其Observable类型的属性。flatMap

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
struct Student {
var score: BehaviorSubject<Int>
}

let disposeBag = DisposeBag()

let ryan = Student(score: BehaviorSubject(value: 80))
let charlotte = Student(score: BehaviorSubject(value: 90))

let student = PublishSubject<Student>()

student
.flatMap {
$0.score
}
.subscribe(onNext: {
print($0)
})
.disposed(by: disposeBag)

student.onNext(ryan)
ryan.score.onNext(85)

student.onNext(charlotte)
ryan.score.onNext(95)

charlotte.score.onNext(100)

What makes

flatMapLatest different is that it will automatically switch to the latest observable and

unsubscribe from the the previous one.

flatMapLatestflatMap 略有不同之处是它会自动切换到最后一个 Observable,注销订阅之前的 Observable。

flatMapLatest

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
let disposeBag = DisposeBag()

let ryan = Student(score: BehaviorSubject(value: 80))
let charlotte = Student(score: BehaviorSubject(value: 90))

let student = PublishSubject<Student>()

student
.flatMapLatest {
$0.score
}
.subscribe(onNext: {
print($0)
})
.disposed(by: disposeBag)

student.onNext(ryan)

ryan.score.onNext(85)

student.onNext(charlotte)

ryan.score.onNext(95)

charlotte.score.onNext(100)

// Only one thing to point out here that’s different from the previous example of flatMap:
// Changing ryan’s score here will have no effect. It will not be printed out. This is because flatMapLatest has already switched to the latest observable, for charlotte.

观察事件

使用具体化运算符 materialize, 可以将 Observable 发射的每个事件包装成 Observable。比如下面的例子中 $0.score.materialize()BehaviorSubject<Int> 封装为 Observable<Event<Int>> 。这样的话,我的就可以自己处理 .error 和 .complete 事件,而不终结该 Observable 。

对应的我们再使用 .dematerialize() 转回 Observable<Int> ,即可正常订阅。

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
enum MyError: Error {
case anError
}

let disposeBag = DisposeBag()

let ryan = Student(score: BehaviorSubject(value: 80))
let charlotte = Student(score: BehaviorSubject(value: 100))

let student: BehaviorSubject<Student> = BehaviorSubject(value: ryan)

let studentScore: Observable<Event<Int>> = student
.flatMapLatest {
$0.score.materialize()
}

studentScore
.filter {
guard $0.error == nil else {
print($0.error!)
return false
}

return true
}
.dematerialize()
.subscribe(onNext: {
print("subscribe: \($0)")
})
.disposed(by: disposeBag)

ryan.score.onNext(85)
ryan.score.onError(MyError.anError)
ryan.score.onNext(90)
student.onNext(charlotte)

Console 输出结果:

1
2
3
4
subscribe: 80
subscribe: 85
anError
subscribe: 100

第八章:Transforming 实践

在上一章中学习了 RxSwift 的主要功能,包括:map 和 flatMap 。

开始 GitFeed 项目

本章中要处理的项目将显示 GitHub 仓库的动态, 如所有最新的 like, fork 和 comment.。

该项目将有两个不同的故事情节:

  • 主要的情节是要接触到 GitHub 的 JSON API, 接收 JSON 响应, 并最终将其转换为对象集合.  
  • 次要将提取的对象保存到磁盘并在 “新” 活动列表之前的表中显示它们。从服务器获取事件。

网络获取数据

我们将会将 URLSession 添加个响应式拓展。

CocoaAPIToRxAPI

具体就是把网络API的请求和响应分别封装起来:

其中 share(replay:, scope:) ,可以避免 URLSession.rx.response(request:) 因过早 .complete 造成订阅时重新发送网络请求。原理就是封装多个 Observable 的元素为一个新的 Observable,并指定缓冲区最大元素数量,生命周期。定义如下:

1
func share(replay: Int = default, scope: SubjectLifetimeScope = default)

代码如下:

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
func fetchEvents(repo: String) {
// 1. 使用 map 构造一个请求
let response = Observable.from([repo])
.map { urlString -> URL in
return URL(string: "https://api.github.com/repos/\(urlString)/events")!
}
.map { [weak self] url -> URLRequest in
var request = URLRequest(url: url)
if let modifiedHeader = self?.lastModified.value {
request.addValue(modifiedHeader as String,
forHTTPHeaderField: "Last-Modified")
}
return request
}
.flatMap { request -> Observable<(response: HTTPURLResponse, data: Data)> in
return URLSession.shared.rx.response(request: request)
}
.share(replay: 1, scope: .whileConnected)

// 2. 转换响应
response
.filter { response, _ in
// 检查右边的值是否在左边的 range 内
return 200..<300 ~= response.statusCode
}
.map { _, data -> [[String: Any]] in
guard let jsonObject = try? JSONSerialization.jsonObject(with: data, options: []),
let result = jsonObject as? [[String: Any]] else {
return []
}
return result
}
.filter { objects in
return objects.count > 0
}
.map { objects in
return objects.flatMap(Event.init)
}
.subscribe(onNext: { [weak self] newEvents in
self?.processEvents(newEvents)
})
.disposed(by: bag)

response
.filter { response, _ in
return 200..<400 ~= response.statusCode
}
.flatMap { response, _ -> Observable<NSString> in
guard let value = response.allHeaderFields["Last-Modified"] as? NSString else {
return Observable.empty()
}
return Observable.just(value)
}
.subscribe(onNext: { [weak self] modifiedHeader in
guard let strongSelf = self else { return }
strongSelf.lastModified.value = modifiedHeader
try? modifiedHeader.write(to: strongSelf.modifiedFileURL, atomically: true,
encoding: String.Encoding.utf8.rawValue)
})
.disposed(by: bag)
}

第九章:合并Operator

在前面的章节中, 你学习了如何创建、filter 和转换可观察序列。RxSwift filtering 和转换运算符的行为与 Swift 的标准集合运算符非常相似。你看到了 RxSwift 与 flatMap 的真正力量, 它让你用很少的代码执行很多任务. 

这一章将向你展示几种不同的组合序列的方法, 以及如何在每个序列中组合数据。你将使用的一些操作员与 Swift 集合操作员非常相似。它们有助于结合异步序列中的元素, 就像使用 Swift 数组一样。

前缀和串联

startWith(_:) 运算符赋值给定初始值的可观察序列。此值必须与可观察元素的类型相同。

1
2
let numbers = Observable.of(2, 3, 4)
let observable = numbers.startWith(1) observable.subscribe(onNext: { value in print(value) })

类似地 .concat(_:) 可以合并两个序列。

1
2
3
let first = Observable.of(1, 2, 3) let second = Observable.of(4, 5, 6)
let observable = Observable.concat([first, second])
observable.subscribe(onNext: { value in print(value) })

Merge

Merge 也是合并,不过会按照顺序发射事件。

merge

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
let left = PublishSubject<String>()
let right = PublishSubject<String>()

let source = Observable.of(left.asObservable(), right.asObservable())

let observable = source.merge()
let disposable = observable.subscribe(onNext: { value in
print(value)
})

var leftValues = ["Berlin", "Munich", "Frankfurt"]
var rightValues = ["Madrid", "Barcelona", "Valencia"]

repeat {
if arc4random_uniform(2) == 0 {
if !leftValues.isEmpty {
left.onNext("Left: " + leftValues.removeFirst())
}
} else if !rightValues.isEmpty {
right.onNext("Right: " + rightValues.removeFirst())
}
} while !leftValues.isEmpty || !rightValues.isEmpty

disposable.dispose()

Console 输出:

1
2
3
4
5
6
Right: Madrid
Right: Barcelona
Left: Berlin
Left: Munich
Left: Frankfurt
Right: Valencia

Combining 元素

RxSwift中一组基本的运算符是 combineLatest 。他们组合来自几个序列的值:

Combining elements

Every time one of the inner (combined) sequences emits a value, it calls a closure you provide. You receive the last value from each of the inner sequences. This has many concrete applications, such as observing several text fields at once and combining their value, watching the status of multiple sources, and so on.

每当内部(组合)序列中的一个发出一个值时,它会调用您提供的闭包。 你会收到每个内部序列的最后一个值。 这里有很多具体的应用场景,例如一次观察几个文本字段并结合它们的价值,观察多个来源的状态等等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
let left = PublishSubject<String>()
let right = PublishSubject<String>()

let observable = Observable.combineLatest(left, right, resultSelector: {
lastLeft, lastRight in
"\(lastLeft) \(lastRight)"
})
let disposable = observable.subscribe(onNext: { value in
print(value)
})

print("> Sending a value to Left")
left.onNext("Hello,")
print("> Sending a value to Right")
right.onNext("world")
print("> Sending another value to Right")
right.onNext("RxSwift")
print("> Sending another value to Left")
left.onNext("Have a good day,")

disposable.dispose()

Console 输出:

1
2
3
4
5
6
7
> Sending a value to Left
> Sending a value to Right
Hello, world
> Sending another value to Right
Hello, RxSwift
> Sending another value to Left
Have a good day, RxSwift

zip 总是按照顺序一对一对的组合。类似把两个数组按照相同的下标进行组合。

zip

1
2
3
4
5
6
7
8
9
10
11
12
13
enum Weather {
case cloudy
case sunny
}
let left: Observable<Weather> = Observable.of(.sunny, .cloudy, .cloudy, .sunny)
let right = Observable.of("Lisbon", "Copenhagen", "London", "Madrid", "Vienna")

let observable = Observable.zip(left, right) { weather, city in
return "It's \(weather) in \(city)"
}
observable.subscribe(onNext: { value in
print(value)
})

Console 输出:

1
2
3
4
It's sunny in Lisbon
It's cloudy in Copenhagen
It's cloudy in London
It's sunny in Madrid

Triggers

应用程序有不同的需求,必须管理多个输入源。 你经常需要立即接受来自多个观察对象的输入。 有些会简单地在代码中触发行为,而其他的将提供数据。 RxSwift 已经覆盖了强大的运算符这会让你的生活更轻松。 或者,至少你的编码生活!

首先看看 withLatestFrom(_:)。 经常被初学者忽略,它在处理用户界面等方面是一个有用的配套工具。trigger

1
2
3
4
5
6
7
8
9
10
11
12
13
let button = PublishSubject<Void>()
let textField = PublishSubject<String>()

let observable = button.withLatestFrom(textField)
_ = observable.subscribe(onNext: { value in
print(value)
})

textField.onNext("Par")
textField.onNext("Pari")
textField.onNext("Paris")
button.onNext(())
button.onNext(())

Console 输出:

1
2
Paris
Paris

sample(_:) 和 几乎只有一个变化:每当触发器observable发射一个值时,sample(_:) 从其它的可观察值发出最新值,但只有在自上一次元素后才到达。 如果没有新的数据到达,sample(_:) 将不会发射任何东西。sample

Switches

Switches

amb(_:) 运算符订阅左侧和右侧的可观察序列。 它等待其中任何一个发出一个元素,然后退订另一个序列。 之后,它只传递第一个活动可观察元素。 它可以从模棱两可中选出一个:首先,你不知道你感兴趣的是哪一个序列,等其中先发射一个序列再做决定。

这个运算符经常被忽视。 它有一些精选的实际应用场景,如连接到冗余服务器,并坚持使用第一个响应的服务器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let left = PublishSubject<String>()
let right = PublishSubject<String>()

let observable = left.amb(right)
let disposable = observable.subscribe(onNext: { value in
print(value)
})

left.onNext("Lisbon")
right.onNext("Copenhagen")
left.onNext("London")
left.onNext("Madrid")
right.onNext("Vienna")

disposable.dispose()

更流行的选项是 switchLatest() 运算符:

可以自由切换偏爱那个子序列发射的元素。switchLatest

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
let one = PublishSubject<String>()
let two = PublishSubject<String>()
let three = PublishSubject<String>()

let source = PublishSubject<Observable<String>>()

let observable = source.switchLatest()
let disposable = observable.subscribe(onNext: { value in
print(value)
})

source.onNext(one)
one.onNext("Some text from sequence one")
two.onNext("Some text from sequence two")

source.onNext(two)
two.onNext("More text from sequence two")
one.onNext("and also from sequence one")

source.onNext(three)
two.onNext("Why don't you seem me?")
one.onNext("I'm alone, help me")
three.onNext("Hey it's three. I win.")

source.onNext(one)
one.onNext("Nope. It's me, one!")

disposable.dispose()

Console 输出:

1
2
3
4
5
Some text from sequence one
More text from sequence two
Hey it's three. I win.
Nope. It's me, one!

形成可观察序列的心智模型可能很困难。 别担心, 你会习惯它。 练习是顺序理解序列的关键。 随着您的体验增长,请随时审查这些示例! 你将在下一章中更好地了解如何使用它。

Combining 一个序列中元素

所有的厨师都知道你减少得越多,酱汁就越美味。 虽然不是针对厨师,但 RxSwift 拥有将酱汁减少到最有味道的组分的工具。

reduce(_:_:) 和 Swift 标准库中类似。reduce

1
2
3
4
5
6
7
8
let source = Observable.of(1, 3, 5, 7, 9)
let observable = source.reduce(0, accumulator: { summary, newValue in
return summary + newValue
})

observable.subscribe(onNext: { value in
print(value)
})

Console 输出:

1
2
25

scan(_:accumulator:)reduce(_:_:) 只有一个最终结果不同:每次发射事件,都会计算结果并发射。scan

1
2
3
4
5
6
let source = Observable.of(1, 3, 5, 7, 9)

let observable = source.scan(0, accumulator: +)
observable.subscribe(onNext: { value in
print(value)
})

Console 输出:

1
2
3
4
5
6
1
4
9
16
25

第十章:合并Operator实践

在前一章中,你学习了将操作符合并,并通过对一些相当令人头脑灵活的概念进行越来越详细的练习。 一些运算符可能会让你对这些反应性概念的真实应用感到疑惑。
在“……实践”一章中,你将有机会尝试一些最强大的运算符。 你将学会解决类似于你在自己的应用程序中遇到的问题。

准备网络后端服务

这里使用NASA的EONET服务。

我们称之为EONET服务。 它抽象访问由EONET服务器公开的数据,将它们作为服务提供给你的应用程序。 你会看到,结合Rx,这种模式将找到许多应用程序。 它可以让你在应用程序内清晰地分离数据生产和消耗。 你可以轻松替换或模拟生产环境,而不会对用户方面产生任何影响。

先封装一个通用网络请求,然后把 categories 封装为单例,方便所有订阅者订阅。

Categories view controller

categories view controller 显示 categories 列表。

Adding the event download service

Getting events for categories

Events view controller

Wiring the days selector

Splitting event downloads

第十一章:基于时间的Operator

时间就是一切。响应式编程背后的核心思想是基于时间异步数据流的模型。在这方面, RxSwift 提供了一系列操作, 使你能够处理时间和序列在一段时间内的响应和转换方式。正如你将在本章中看到的, 管理序列的时间维度是简单而直接的。

📒RxSwift I:开始学习 RxSwift

Rx_Logo_M.png

第一章:RxSwift 介绍

RxSwift是一个组合异步和事件驱动编程的库,通过使用可观察序列和功能样式运算符来,从而允许通过调度程序进行参数化执行。

RxSwift 在本质上简化了开发异步程序,允许代码对新数据作出反应,并以顺序和孤立的方式处理它。

介绍异步编程

51526546882_.pic.jpg

使用 Cocoa 和 UIKit 异步的API的问题在于:复杂的异步代码变得非常难写,部分原因是苹果SDK提供的API种类繁多。如NotificationCenter、Delegate、GCD、闭包、Combine。

异步编程词汇表:

  1. 状态(State),具体地说:共享可变状态
  2. 命令式编程(Imperative programming)
  3. 副作用(Side effects)
  4. 声明式编程(Declarative code):声明式代码让您可以定义行为片段。只要有相关事件发生,RxSwift 就会运行这些行为,并提供一个不可变的、孤立的数据片段来处理。这样,您可以使用异步代码,但做出与简单 for 循环相同的假设:您正在使用不可变数据并且可以以顺序、确定的方式执行代码。
  5. 响应式系统(Reactive systems)

响应式系统(Reactive systems)是一个相当抽象的术语,它涵盖了Web或iOS应用程序,它们显示了大多数或全部以下特性:

  • 响应式设计(Responsive):始终保持UI更新,代表了最新的应用程序状态。
  • 能复原的(Resilient):每个行为都是独立定义的,并提供灵活的错误恢复。
  • 灵活的(Elastic): 该代码处理不同的工作负载,通常实现诸如懒惰驱动数据收集、事件节流和资源共享等特性。
  • 消息驱动(Message driven):组件使用基于消息的通信来提高可重用性和隔离性,解耦类的生命周期和实现。

RxSwift 基础

61526549641_.pic_hd.jpg

这个标志是一只电鳗。(Rx 项目曾经被称为Volta)

RxSwift 是微软开源的 ReactiveX 的Swift语言的实现。

RxSwift 在传统的Cocoa编程和纯函数编程之间找到了最佳位置。它允许您对事件作出反应, 方法是使用不可变的代码定义以确定性的、可组合的方式处理异步输入部分。

Rx代码的三个组成部分是 Observable, OperatorScheduler

Observable<Element>类提供了Rx代码的基础:异步产生一系列事件的能力,它可以“携带”数据的不可变快照。简单来说,它允许类在一段时间内订阅其他类发出的值。

ObservableType 协议 (Observable需要遵循的) 非常简单。Observable可能发出 (并且Observer能接受) 仅三类型事件:

- next 下一个事件: “携带” 最新 (或 “下一个 “) 数据值的事件。这是Observer “接收” 值的方式。

- completed 已完成的事件: 此事件以成功终止事件序列。这意味着Observable完成其生命周期成功, 不会发出任何其他事件。

- error 错误事件: Observable终止带有错误, 不会发出其他事件.

两种不同的可观测序列: 有限和无限的。

由于它们是高度解耦和可组合的, 所以这些方法通常称为Operator。比如filter。

Operator也是高度可组合的,它们总是把数据作为输入并输出它们的结果,所以你可以用许多不同的方式轻松地将它们连接起来,实现比单个Operator自己能做的更多的事情。

SchedulerGCDOperationQueue的Rx等价物。

RxSwift将充当你的订阅(在左边)和Scheduler(在右边)之间的调度器,将工件发送到正确的上下文,并无缝地允许它们与彼此的输出一起工作。

71526551674_.pic_hd.jpg

要读取此关系图, 请在不同的计划程序中按预定的顺序 (1、2、3、…) 来执行彩色作品。例如:

·蓝色网络订阅以在基于自定义 NSOperation 的计划程序上运行的一段代码 (1) 开始。

·数据输出块作为下一个块 (2) 的输入, 它运行在一个不同的调度程序上, 它位于并发后台 GCD 队列中。

·最后, 在主线程调度程序上计划最后一块蓝色代码 (3), 以便用新数据更新 UI。

App architecture 应用的架构

值得一提的是,RxSwift并没有以任何方式改变应用程序的架构;它主要处理事件、异步数据序列和通用通信协议。

通过在苹果开发文档中实现MVC体系结构,可以创建具有Rx的应用程序。如果你喜欢的话,你也可以选择实现MVP架构或MVVM。RxSwift也可以帮你实现自己的单向数据架构。

微软的MVVM架构是专门针对在平台上创建的事件驱动软件开发的,该平台提供数据绑定。RxSwift和MVVM很好地结合在一起,在这本书的末尾,你会看到这个模式以及如何用RxSwift来实现它。

MVVM和RxSwift结合在一起的原因是,ViewModel允许您公开Observable属性,这些属性可以直接绑定到View Controller 代码中的UIKit控件。这使得绑定模型数据到UI非常简单地展示和编码:

81526552550_.pic.jpg

本书中的所有其他示例都使用MVC架构来保持示例代码简单易懂。

RxCocoa

RxSwift是通用Rx API的实现。因此,它不涉及任何Cocoa或UIKit类。

RxCocoa是RxSwift的配套库,所有的类都有助于UIKit和Cocoa的开发。除了具有一些高级类之外,RxCocoa还为许多UI组件添加了响应式扩展,以便您可以订阅不同的UI事件。

例如,使用RxCocoa订阅UISwitch的状态变化是非常容易的,例如:

1
2
3
4
toggleSwitch.rx.isOn
.subcribe(onNext: {enabled in
print(enabled ? "it's ON" : "it's OFF")
})

RxCocoa adds the rx.isOn property (among others) to the UISwitch class so you can subscribe to generally useful event sequences.

RxCocoa将rx.isOn属性(其中之一)添加到UISwitch类,这样您就可以订阅通常有用的Observable序列。

101526552877_.pic.jpg

安装

官方git:https://github.com/ReactiveX/RxSwift

使用 CocoaPodsCarthageSwift Package Manager 均很方便集成RxSwift。

RxSwift 和 Combine

RxSwift 和 Combine(以及 Swift 中的其他相应式编程框架)共享许多通用语言和非常相似的概念。

RxSwift是一个较旧的,完善的框架,具有一些自己的原始概念,运算符名称和类型多样性,这主要是由于其多平台跨语言标准,该标准也适用于Linux,这对于Server-Side Swift非常有用。它也是开源的,所以如果你愿意,你可以直接为其核心做出贡献,并确切地看到它的特定部分是如何工作的。它与所有支持 Swift 的 Apple 平台版本兼容,一直支持 iOS 8。

Combine 是 Apple 新的、闪亮的框架,涵盖了类似的概念,但专门针对 Swift 和 Apple 自己的平台量身定制。它与 Swift 标准库共享许多通用语言,因此即使新手也觉得 API 非常熟悉。它仅支持从iOS 13,macOS 10.15等开始的较新的Apple平台。不幸的是,截至今天,它还没有开源,并且不支持Linux。

幸运的是,由于 RxSwift 和 Combine 非常相似,因此您的 RxSwift 知识可以轻松转移到 Combine,反之亦然。RxCombine(https://github.com/CombineCommunity/RxCombine)等项目允许您根据需要混合搭配 RxSwift Observables 和 Combine Publishers。

RxSwift 6.5.0 也迎来了 Swift Concurrency 的支持。提供了互操作性:

  1. await 调用Observablevalues
  2. 封装 async Task 为 Observable

社区

RxSwift社区非常友好,思想开放,并且热衷于讨论模式,常用技巧或互相帮助。

更多的Rx库和实验,像雨后春笋一样的涌现,可以在这里找到:https://github.com/RxSwiftCommunity

可能最好的方式来满足许多对RxSwift感兴趣的人,这是Slack的频道:http://rxswift-slack.herokuapp.com

Slack频道拥有约5000名成员! 日常的主题包括:互相帮助,讨论RxSwift或其同伴库的潜在新功能,以及共享RxSwift博客文章和会议讲座。

第二章:Observables

Observable 是什么

Observable、observable sequence 和 sequence 在 Rx 都是一个意思。或者在其他Rx实现中称之为stream。

最好称之为 Observable,不过翻译过来还是序列顺口些。

Observable 的生命周期

  • observable发出包含元素的next事件。 它可以继续这样做,直到它:
  • …发出error事件并终止,或
  • …发出completed事件并终止。
  • 一旦observable被终止,它不能再发出事件。

将这种概念化的最佳方法之一是使用弹珠图(Marble Diagrams 基于时间轴上绘制的值)。

141526623515_.pic.jpg

151526623523_.pic.jpg

创建 Observable

类型擦除的ObservableType,也就是 Swift 中的泛型。

它代表了一种推式风格队列。

let observable: Observable = Observable.just(one)

订阅 Observable

订阅observable sequence的事件处理程序。

func subscribe(_ on: @escaping (RxSwift.Event<Self.E>) -> Swift.Void) -> Disposable

1
2
3
4
5
6
7
8
9
let one = 1
let two = 2
let three = 3

let observable = Observable.of(one, two, three)

observable.subscribe(onNext: { element in
print(element)
})

在指定的范围内生成一个整数的observable sequence,使用指定的scheduler生成和发送Observer消息。

1
static func range(start: Self.E, count: Self.E, scheduler: ImmediateSchedulerType = default) -> RxSwift.Observable<Self.E>
1
2
3
4
5
6
7
8
let observable = Observable<Int>.range(start: 1, count: 10)
observable
.subscribe(onNext: { i in
let n = Double(i)
let fibonacci = Int(((pow(1.61803, n) - pow(0.61803, n)) /
2.23606).rounded())
print(fibonacci)
})

Disposing 和 terminating

请记住, 在收到订阅之前, Observable的内容不会执行任何事情。它是触发一个Observable的开始发出事件的订阅, 直到它发出. error 或.completed完成事件并终止。您可以通过取消对它的订阅来手动终止Observable。

subscription.dispose()

单独管理每个订阅将是单调乏味的, 因此 RxSwift 引入 DisposeBag 类型。DisposeBag 持有 disposable协议的对象,通常是使用.disposed(by:) 方法, 并将调用dispose(), 当DisposeBag即将deallocated。

1
2
3
4
5
6
let observable = Observable.of("A", "B", "C")

let subscription = observable.subscribe { event in
print(event)
}
subscription.dispose()

create操作符接受一个名为subscribe的参数。 它的工作是提供对可观察对象进行调用订阅的实现。 换句话说,它定义了将发送给订阅者的所有事件。

1
static func create(_ subscribe: @escaping (AnyObserver<String>) -> Disposable) -> Observable<String>

创建 observable 工厂类

可以创建一个可观察的工厂,向每个订阅者发布一个新的Observable,而不是创建一个等待订阅者的Observable。

1
static func deferred(_ observableFactory: @escaping () throws -> Observable<Int>) -> Observable<Int>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
let disposeBag = DisposeBag()

var flip = false

let factory: Observable<Int> = Observable.deferred {
flip = !flip

if flip {
return Observable.of(1, 2, 3)
} else {
return Observable.of(4, 5, 6)
}
}

for _ in 0...3 {
factory.subscribe(onNext: {
print($0, terminator: "")
})
.disposed(by: disposeBag)

print()
}
1
2
3
4
123 
456
123
456

使用Traits

Traits是具有比常规Observable更窄的行为集合的一种Observable。它们的使用是可选的;您可以在任何可能使用trait的地方使用常规Observable。他们的目的是提供一种给你的API或者代码的读者更清楚地表达意图的方式。

RxSwift中有三种Traits: Single, Maybe 和 Completable.

Singles将发出.success(value)或.error事件。 .success(value)实际上是.next和.completed事件的组合。 这对于一次性进程非常有用,它可以成功并产生一个值或失败,例如下载数据或从磁盘加载数据。

下面的例子是读取 Copyright.txt 的文件内容:

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
38
39
40
41
let disposeBag = DisposeBag()

enum FileReadError: Error {
case fileNotFound, unreadable, encodingFailed
}

func loadText(from filename: String) -> Single<String> {
return Single.create { single in
let disposable = Disposables.create()

guard let path = Bundle.main.path(forResource: filename, ofType: "txt") else {
single(.error(FileReadError.fileNotFound))
return disposable
}

guard let data = FileManager.default.contents(atPath: path) else {
single(.error(FileReadError.unreadable))
return disposable
}

guard let contents = String(data: data, encoding: .utf8) else {
single(.error(FileReadError.encodingFailed))
return disposable
}

single(.success(contents))

return disposable
}
}

loadText(from: "Copyright")
.subscribe {
switch $0 {
case .success(let string):
print(string)
case .error(let error):
print(error)
}
}
.disposed(by: disposeBag)

副作用

do Operator允许插入副作用,处理程序执行过程中并不会以任何方式更改发出的事件的操作。

打印debug 信息

debug Operator, 它将打印observable的每个事件的信息。

第三章:Subjects

Subject 是什么

既可以作为Observable,也可以作为Observer,这就是所谓的 Subjects。

也是最方便互操作的,BehaviorRelay比较常用。

RxSwift 中有四个subject类型:

  1. PublishSubject: 开始为空, 只向订阅者发出新元素.
  2. ReplaySubject: 用缓冲区大小, 并将保持元素的缓冲区大小, 并将其重播到新订阅者.
  3. BehaviorSubject: 从初始值开始, 将其重播或将最新的元素给新订阅者.
  4. AsyncSubject:仅发出序列中的最后一个next事件,并且仅当subject收到completed事件时才发出。这是一个很少使用的主题。

PublishSubject

1
2
3
/// Represents an object that is both an observable sequence as well as an observer.
/// Each notification is broadcasted to all subscribed observers.
public final class PublishSubject<Element>

当你只是想让订阅者只接受在订阅的时候以后发生的新的事件,直到他们unsubscribe,或者subject已经terminated以.completed或.error事件的方式,PublishSubject就可以派上用场。

RxSwiftPublishSubject

第一个订阅者在将 1 添加到subject后进行订阅,因此它不会收到该事件。不过,它确实得到了2和3。由于第二个订阅者在添加2之前不会加入,因此它只能获得3。

1
2
3
4
5
6
extension ObservableType {
/// 添加带有`id`的观察者并打印每个发出的事件。
func addObserver(_ id: String) -> Disposable {
subscribe { print("观察者:", id, "事件:", $0) }
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 从订阅开始向所有观察者广播新事件。
example("PublishSubject") {
let disposeBag = DisposeBag()
let subject = PublishSubject<String>()

subject.addObserver("1").disposed(by: disposeBag)
subject.onNext("🐶")
subject.onNext("🐱")

subject.addObserver("2").disposed(by: disposeBag)
subject.onNext("🅰️")
subject.onNext("🅱️")

subject.onCompleted()
}
1
2
3
4
5
6
7
8
9
--- PublishSubject example ---
观察者: 1 事件: next(🐶)
观察者: 1 事件: next(🐱)
观察者: 1 事件: next(🅰️)
观察者: 2 事件: next(🅰️)
观察者: 1 事件: next(🅱️)
观察者: 2 事件: next(🅱️)
观察者: 1 事件: completed
观察者: 2 事件: completed

ReplaySubject

1
2
3
4
/// Represents an object that is both an observable sequence as well as an observer.
///
/// Each notification is broadcasted to all subscribed and future observers, subject to buffer trimming policies.
public class ReplaySubject<Element>

ReplaySubject将临时缓存, 或缓冲区, 它们发出的最新元素, 由您选择的 specified 大小决定。然后, 他们会将该缓冲区重播到新订阅者。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 向所有观察者广播新事件,并向新观察者广播之前指定的bufferSize大小的事件数。
example("ReplaySubject") {
let disposeBag = DisposeBag()
let subject = ReplaySubject<String>.create(bufferSize: 1)

subject.addObserver("1").disposed(by: disposeBag)
subject.onNext("🐶")
subject.onNext("🐱")

subject.addObserver("2").disposed(by: disposeBag)
subject.onNext("🅰️")
subject.onNext("🅱️")

subject.onCompleted()
}
1
2
3
4
5
6
7
8
9
10
--- ReplaySubject example ---
观察者: 1 事件: next(🐶)
观察者: 1 事件: next(🐱)
观察者: 2 事件: next(🐱) // 相比PublishSubject多了订阅前一个事件
观察者: 1 事件: next(🅰️)
观察者: 2 事件: next(🅰️)
观察者: 1 事件: next(🅱️)
观察者: 2 事件: next(🅱️)
观察者: 1 事件: completed
观察者: 2 事件: completed

BehaviorSubject

1
2
3
/// Represents a value that changes over time.
/// Observers can subscribe to the subject to receive the last (or initial) value and all subsequent notifications.
public final class BehaviorSubject<Element>

当您希望使用最新数据预先填充View时, BehaviorSubject非常有用。例如, 可以将用户详情页中的控件绑定到BehaviorSubject, 以便在应用程序获取新数据时, 可以使用最新值来预先填充显示。

RxSwiftBehaviorSubject

1
2
3
4
5
6
7
8
9
10
11
12
13
14
example("BehaviorSubject") {
let disposeBag = DisposeBag()
let subject = BehaviorSubject(value: "🔴")

subject.addObserver("1").disposed(by: disposeBag)
subject.onNext("🐶")
subject.onNext("🐱")

subject.addObserver("2").disposed(by: disposeBag)
subject.onNext("🅰️")
subject.onNext("🅱️")

subject.onCompleted()
}
1
2
3
4
5
6
7
8
9
10
11
--- BehaviorSubject example ---
观察者: 1 事件: next(🔴) // 相比ReplaySubject多了订阅前一个事件
观察者: 1 事件: next(🐶)
观察者: 1 事件: next(🐱)
观察者: 2 事件: next(🐱)
观察者: 1 事件: next(🅰️)
观察者: 2 事件: next(🅰️)
观察者: 1 事件: next(🅱️)
观察者: 2 事件: next(🅱️)
观察者: 1 事件: completed
观察者: 2 事件: completed

Relay

Relay在保持其replay行为的同时包装了subject。与其他subject不同,可以使用 accept(_:) 添加值,而非 onNext(_:)。这是因为Relay只能接受值,即不能向它们添加错误或已完成的事件。

PublishRelay将包装PublishSubject ,而BehaviorRelay将包装BehaviorSubject。中继与包装主体的区别在于,它们可以保证永远不会终止。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import RxRelay

example("PublishRelay") {
let disposeBag = DisposeBag()
let subject = PublishRelay<String>()

subject.addObserver("1").disposed(by: disposeBag)
subject.accept("🐶")
subject.accept("🐱")

subject.addObserver("2").disposed(by: disposeBag)
subject.accept("🅰️")
subject.accept("🅱️")
}
1
2
3
4
5
6
7
--- PublishRelay example ---
观察者: 1 事件: next(🐶) //next事件和PublishSubject一样,少了completed事件
观察者: 1 事件: next(🐱)
观察者: 1 事件: next(🅰️)
观察者: 2 事件: next(🅰️)
观察者: 1 事件: next(🅱️)
观察者: 2 事件: next(🅱️)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import RxRelay

example("BehaviorRelay") {
let disposeBag = DisposeBag()
let subject = BehaviorRelay<String>(value: "🔴")

subject.addObserver("1").disposed(by: disposeBag)
subject.accept("🐶")
subject.accept("🐱")

subject.addObserver("2").disposed(by: disposeBag)
subject.accept("🅰️")
subject.accept("🅱️")
}
1
2
3
4
5
6
7
8
9
--- BehaviorRelay example ---
观察者: 1 事件: next(🔴) //next事件和BehaviorSubject,少了completed事件
观察者: 1 事件: next(🐶)
观察者: 1 事件: next(🐱)
观察者: 2 事件: next(🐱)
观察者: 1 事件: next(🅰️)
观察者: 2 事件: next(🅰️)
观察者: 1 事件: next(🅱️)
观察者: 2 事件: next(🅱️)

第四章:Observables 和 Subjects 实践

本章重点是在一个完整的应用开发中使用RxSwift,一步一步的学会如何把概念应用到实际项目中。

在 view controller 中使用 subject/relay

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
override func viewDidLoad() {
super.viewDidLoad()

images
.subscribe(onNext: { [weak imagePreview] photos in
guard let preview = imagePreview else { return }
preview.image = photos.collage(size: preview.frame.size)
})
.disposed(by: bag)

images
.subscribe(onNext: { [weak self] photos in
self?.updateUI(photos: photos)
})
.disposed(by: bag)
}

使用 subject 在 view controller 之间传值

view controller 之间传值可以通过 delegate,但是用 subject 更好。

Talkingtootherviewcontrollersviadelegate

Talkingtootherviewcontrollersviasubjects

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@IBAction func actionAdd() {
let photosViewController = storyboard!.instantiateViewController(
withIdentifier: "PhotosViewController") as! PhotosViewController

navigationController!.pushViewController(photosViewController, animated: true)

photosViewController.selectedPhotos
.subscribe(
onNext: { [weak self] newImage in
guard let images = self?.images else { return }
images.accept(images.value + [newImage])
},
onDisposed: {
print("completed photo selection")
}
)
.disposed(by: bag)
}

封装已有API为Observable

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
import Foundation
import Photos
import RxSwift
import UIKit

class PhotoWriter {
enum Errors: Error {
case couldNotSavePhoto
}

static func save(_ image: UIImage) -> Observable<String> {
return Observable.create { observer in
var savedAssetId: String?
PHPhotoLibrary.shared().performChanges({
let request = PHAssetChangeRequest.creationRequestForAsset(from: image)
savedAssetId = request.placeholderForCreatedAsset?.localIdentifier
}, completionHandler: { success, error in
DispatchQueue.main.async {
if success, let id = savedAssetId {
observer.onNext(id)
observer.onCompleted()
} else {
observer.onError(error ?? Errors.couldNotSavePhoto)
}
}
})

return Disposables.create()
}
}
}

RxSwift traits 实践

Single

Single 只有.success.error 两种事件 。适合作为封装网络接口的返回值,要么成功,要么失败。single

1
2
3
4
5
6
7
8
9
PhotoWriter.save(image)
.asSingle()
.subscribe(onSuccess: { [weak self] id in
self?.showMessage("Saved with id: \(id)")
self?.actionClear()
}, onError: { [weak self] error in
self?.showMessage("Error", description: error.localizedDescription)
})
.disposed(by: bag)

Maybe

Maybe 有三种事件:.success.completed.error

和 Single 一样,你既可以通过 Maybe.create({ ... }) 直接创建,也可以使用 .asMaybe()

maybe

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
PhotoWriter.save(image)
.asMaybe()
.subscribe(
onSuccess: { [weak self] id in
self?.showMessage("Saved with id: \(id)")
self?.actionClear()
},
onError: { [weak self] error in
self?.showMessage("Error", description: error.localizedDescription)
},
onCompleted: { [weak self] in
self?.showMessage("Saved")
}
)
.disposed(by: bag)

Completable

Completable 只有.completed.error 两种事件。适合只关心是否完成,而不需要传递value的情况。使用方法:Completable.create({ ... })

completable

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
class PhotoWriter {
enum Errors: Error {
case couldNotSavePhoto
}

static func save(_ image: UIImage) -> Completable {
return Completable.create { completable in
PHPhotoLibrary.shared().performChanges({
PHAssetChangeRequest.creationRequestForAsset(from: image)
}, completionHandler: { success, error in
DispatchQueue.main.async {
if success {
completable(.completed)
} else {
completable(.error(error ?? Errors.couldNotSavePhoto))
}
}
})
return Disposables.create()
}
}
}

PhotoWriter.save(image)
.subscribe(
onCompleted: { [weak self] in
self?.showMessage("Saved")
},
onError: { [weak self] error in
self?.showMessage("Error", description: error.localizedDescription)
}
)
.disposed(by: bag)

RealmsSchema

系列文章目录:

  1. Realm 笔记 (一)
  2. Realm 笔记 (二)

第七章:Realm配置

打开一个Realm:let realm = try! Realm()

Realm没有使用单例,而是每次直接尝试新建一个Realm实例,有以下几个运行时优化:

  1. 调用Realm()返回同一个共享实例,而不受创建该Realm实例的线程限制。而Object和Realm实例被限制在创建的线程中,所以不能跨线程分享。
  2. Realm也提供了多个安全措施,如:使用不同的密钥或文件不存在均会报错。

书中均使用 try! ,但实际生产环境你可以进行错误处理。

deleteRealmIfMigrationNeeded 新版直接删除Realm文件,不做迁移处理。

存在文件在Documents文件夹有几个好处:自动备份到用户的iCloud storage,方便用户使用iTunes备份和访问。当然苹果推荐存储文件在Library文件夹。

App Bundle 文件夹是只读的,所以该目录中的Realm是能作为只读数据库。

加密在Realm极为简单,只要在配置中添加一个64位的密钥即可。

第八章:多个Realm和共享Realm

创建一个RealmProvider以方便操作Realm,尤其是涉及多个Realm或加密等情况。

img

如需转换JSON、CSV或纯文本为Realm,并内置到App Bundle。此时可以创建一个Target和来Tools.swift,自动生成Realm文件。把生成的Realm文件拖入Xcode即可。

img

第九章:依赖注入和测试

在本章中,您将学习两个重要主题:如何使用依赖注入来改进 Flash Cards 应用的架构,以及如何编写由Realm支持的同步和异步测试。

本章不会深入研究诸如测试驱动开发等主题,而是专注测试使用 Realm Object的类的技巧,并且依赖于特定于Realm的功能,例如更改通知。

测试 cards 场景(Scene)

  • CardsViewController:设置所需的手势识别器并根据用户的输入更新UI。本章不会介绍UI测试。
  • CardsModel:从Realm中抽象查询对象的简单封装。为它编写测试将意味着在测试中重复相同的代码并比较输出,而不提供实际值。除此之外,你真正要在这里测试的将是Realm的底层实现,它已经经过了充分测试。
  • CardsViewModel:实现此场景的业务逻辑。它格式化Model的输出,以显示在屏幕上,并提供与模型交互的方法。这将是一些单元测试的完美候选!

img

这里只是简单测试 ViewModel 初始化和更新状态。

img

测试 sets 场景

在显示列表的场景中,您的视图模型依赖于Realm通知来动态更新数据。你必须:

  • 模拟Realm框架以测试依赖通知的 ViewModel 和其他高级Realm功能。
  • 编写依赖Realm通知的测试

为了使测试有效,您需要稍微更改代码以提供一个方法将测试的 Realm providers 注入到Model和ViewModel中。

img

img

异步测试需要使用XCTestExpectation,这个期望类将帮助你等待某些条件在您的测试中得到满足,然后再转到您测试断言的部分。

在使用XCTWaiter类来持有测试的执行,直到满足期望为止。 XCTWaiter是Apple的XCTest框架中的一个便捷类,它暂时停止当前代码的执行,而不会阻塞当前线程。 XCTWaiter定期检查期望的状态,因此当异步代码将执行计数增加到3时,XCTWaiter将负责恢复执行测试。

步骤如下:

  • 初始化XCTestExpectation和expectedFulfillmentCount
  • 在异步回调中fulfill,
  • 最后添加定期检查结果的帮助类XCTWaiter。

img

测试 Word of Today 场景

img

小结:

在本章中,你将亲身体验用Realm编写同步和异步测试的简易性。本章中重点是:

  • 测试自己的逻辑,而不是Realm,因为它已经在全球数百万用户中得到了充分测试和使用。
  • “哑”模型和视图控制器可以更轻松地测试视图模型,您通常可以在其中放置MVVM应用程序的逻辑。
  • 编写需要这些依赖项的特定测试版本的测试时,使用中央provider结构来抽取某些依赖项的检索很有用。

通常,测试基于Realm的代码可能不会改变您构建自己的测试套件的方式。借助本章的经验,您应该能够为您的Realm项目编写可靠的测试,无论你心意哪一种架构。

注意:对于单元测试,模拟Realm本身是一个偏爱的问题。如果你想模拟Realm并从测试套件中删除Realm依赖关系,你可以模拟关键的方法,比如对象(^)、过滤器(^)等等。风险是你必须增加和保持的代码量,以保持与Realm自己行为的一致,尤其是已经有了基于内存的Realm这个特别棒的方案。

笔者觉得模拟Realm的唯一好处是,Realm依赖增加了CI服务器需要安装,构建和测试应用程序的时间。

第十章:高效率的多线程

Realm线程:多线程访问同一个Realm总是可以读写到最新的数据。

线程间传递Object,有以下两个替代方法:

  • 传递Object的主键
  • 使用ThreadSafeReference:let ref = ThreadSafeReference(to: myRealmObject)let myRealmobject = realm.resolve(ref)

主线程:访问带主键合适数量的数据时,无需担心性能问题。如在View Controller lifecycle 和 UIKit delegate 中直接读取Object,和更新UI。因为总是在同一个Realm文件的快照中工作,所以速度最快。

img

后台线程:(不推荐)由于是异步并发执行,会因为Realm 快照之间同步问题,产生性能和体积大的问题。

专用线程:(推荐)可以解决主线程影响UI,后台线程性能问题。使用Thread,自己维护一个线程的生命周期,所有读写都在该线程操作。还可以缓存任务批量提交读写事务,进一步提高性能。

img

img

简版(无缓冲机制)

img

未完待续…

0%