【iOS】Swift-Combine

旨在快速打通iOS开发流程

By yesmore on 2022-11-29
阅读时间 22 分钟
文章共 4k
阅读量

本章内容概览:Combine

上一节:【iOS】09_Swift编程规范、资料推荐

下一节:【iOS】11_Swift-Concurrency

同系列文章请查看:快乐码元 - iOS篇

Ⅰ.介绍

1.Combine 是什么?

WWDC 2019苹果推出Combine,Combine是一种响应式编程范式,采用声明式的Swift API。

Combine 写代码的思路是你写代码不同于以往命令式的描述如何处理数据,Combine 是要去描述好数据会经过哪些逻辑运算处理。这样代码更好维护,可以有效的减少嵌套闭包以及分散的回调等使得代码维护麻烦的苦恼。

声明式和过程时区别可见如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 所有数相加
// 命令式思维
func sum1(arr: [Int]) -> Int {
var sum: Int = 0
for v in arr {
sum += v
}
return sum
}

// 声明式思维
func sum2(arr: [Int]) -> Int {
return arr.reduce(0, +)
}

Combine 主要用来处理异步的事件和值。苹果 UI 框架都是在主线程上进行 UI 更新,Combine 通过 Publisher 的 receive 设置回主线程更新UI会非常的简单。

已有的 RxSwift 和 ReactiveSwift 框架和 Combine 的思路和用法类似。

2.Combine 的三个核心概念

  • 发布者
  • 订阅者
  • 操作符

简单举个发布数据和类属性绑定的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
let pA = Just(0)
let _ = pA.sink { v in
print("pA is: \(v)")
}

let pB = [7,90,16,11].publisher
let _ = pB
.sink { v in
print("pB: \(v)")
}

class AClass {
var p: Int = 0 {
didSet {
print("property update to \(p)")
}
}
}
let o = AClass()
let _ = pB.assign(to: \.p, on: o)

3.Combine 资料

官方文档链接 Combine | Apple Developer Documentation 。还有 Using Combine 这里有大量使用示例,内容较全。官方讨论Combine的论坛 Topics tagged combine 。StackOverflow上相关问题 Newest ‘combine’ Questions 。

WWDC上关于Combine的Session如下:

和Combine相关的Session:

Ⅱ.使用说明

1.publisher

publisher 是发布者,sink 是订阅者

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

var cc = Set<AnyCancellable>()
struct S {
let p1: String
let p2: String
}
[S(p1: "1", p2: "one"), S(p1: "2", p2: "two")]
.publisher
.print("array")
.sink {
print($0)
}
.store(in: &cc)

输出:

1
2
3
4
5
6
7
 array: receive subscription: ([yesmore的博客.AppDelegate.(unknown context at $10ac82d20).(unknown context at $10ac82da4).S(p1: "1", p2: "one"), yesmore的博客.AppDelegate.(unknown context at $10ac82d20).(unknown context at $10ac82da4).S(p1: "2", p2: "two")])
array: request unlimited
array: receive value: (S(p1: "1", p2: "one"))
S(p1: "1", p2: "one")
array: receive value: (S(p1: "2", p2: "two"))
S(p1: "2", p2: "two")
array: receive finished

2.Just

Just 是发布者,发布的数据在初始化时完成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import Combine
var cc = Set<AnyCancellable>()
struct S {
let p1: String
let p2: String
}

let pb = Just(S(p1: "1", p2: "one"))
pb
.print("pb")
.sink {
print($0)
}
.store(in: &cc)

输出:

1
2
3
4
5
pb: receive subscription: (Just)
pb: request unlimited
pb: receive value: (S(p1: "1", p2: "one"))
S(p1: "1", p2: "one")
pb: receive finished

3.PassthroughSubject

PassthroughSubject 可以传递多值,订阅者可以是一个也可以是多个,send 指明 completion 后,订阅者就没法接收到新发送的值了。

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

var cc = Set<AnyCancellable>()
struct S {
let p1: String
let p2: String
}

enum CError: Error {
case aE, bE
}
let ps1 = PassthroughSubject<S, CError>()
ps1
.print("ps1")
.sink { c in
print("completion:", c) // send 了 .finished 后会执行
} receiveValue: { s in
print("receive:", s)

}
.store(in: &cc)

ps1.send(S(p1: "1", p2: "one"))
ps1.send(completion: .failure(CError.aE)) // 和 .finished 一样后面就不会发送了
ps1.send(S(p1: "2", p2: "two"))
ps1.send(completion: .finished)
ps1.send(S(p1: "3", p2: "three"))

// 多个订阅者
let ps2 = PassthroughSubject<String, Never>()
ps2.send("one") // 订阅之前 send 的数据没有订阅者可以接收
ps2.send("two")

let sb1 = ps2
.print("ps2 sb1")
.sink { s in
print(s)
}

ps2.send("three") // 这个 send 的值会被 sb1

let sb2 = ps2
.print("ps2 sb2")
.sink { s in
print(s)
}

ps2.send("four") // 这个 send 的值会被 sb1 和 sb2 接受

sb1.store(in: &cc)
sb2.store(in: &cc)
ps2.send(completion: .finished)

输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ps1: receive subscription: (PassthroughSubject)
ps1: request unlimited
ps1: receive value: (S(p1: "1", p2: "one"))
receive: S(p1: "1", p2: "one")
ps1: receive error: (aE)
completion: failure(戴铭的开发小册子.AppDelegate.(unknown context at $10b15ce10).(unknown context at $10b15cf3c).CError.aE)
ps2 sb1: receive subscription: (PassthroughSubject)
ps2 sb1: request unlimited
ps2 sb1: receive value: (three)
three
ps2 sb2: receive subscription: (PassthroughSubject)
ps2 sb2: request unlimited
ps2 sb1: receive value: (four)
four
ps2 sb2: receive value: (four)
four
ps2 sb1: receive finished
ps2 sb2: receive finished

4.Empty

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

var cc = Set<AnyCancellable>()
struct S {
let p1: String
let p2: String
}

let ept = Empty<S, Never>() // 加上 completeImmediately: false 后面即使用 replaceEmpty 也不会接受值
ept
.print("ept")
.sink { c in
print("completion:", c)
} receiveValue: { s in
print("receive:", s)
}
.store(in: &cc)

ept.replaceEmpty(with: S(p1: "1", p2: "one"))
.sink { c in
print("completion:", c)
} receiveValue: { s in
print("receive:", s)
}
.store(in: &cc)

输出:

1
2
3
4
5
6
ept: receive subscription: (Empty)
ept: request unlimited
ept: receive finished
completion: finished
receive: S(p1: "1", p2: "one")
completion: finished

5.CurrentValueSubject

CurrentValueSubject 的订阅者可以收到订阅时已发出的那条数据

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

var cc = Set<AnyCancellable>()

let cs = CurrentValueSubject<String, Never>("one")
cs.send("two")
cs.send("three")
let sb1 = cs
.print("cs sb1")
.sink {
print($0)
}

cs.send("four")
cs.send("five")

let sb2 = cs
.print("cs sb2")
.sink {
print($0)
}

cs.send("six")

sb1.store(in: &cc)
sb2.store(in: &cc)

输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
cs sb1: receive subscription: (CurrentValueSubject)
cs sb1: request unlimited
cs sb1: receive value: (three)
three
cs sb1: receive value: (four)
four
cs sb1: receive value: (five)
five
cs sb2: receive subscription: (CurrentValueSubject)
cs sb2: request unlimited
cs sb2: receive value: (five)
five
cs sb1: receive value: (six)
six
cs sb2: receive value: (six)
six
cs sb1: receive cancel
cs sb2: receive cancel

6.removeDuplicates

使用 removeDuplicates,重复的值就不会发送了。

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

var cc = Set<AnyCancellable>()

let pb = ["one","two","three","three","four"]
.publisher

let sb = pb
.print("sb")
.removeDuplicates()
.sink {
print($0)
}

sb.store(in: &cc)

输出

1
2
3
4
5
6
7
8
9
10
11
12
13
sb: receive subscription: (["one", "two", "three", "three", "four"])
sb: request unlimited
sb: receive value: (one)
one
sb: receive value: (two)
two
sb: receive value: (three)
three
sb: receive value: (three)
sb: request max: (1) (synchronous)
sb: receive value: (four)
four
sb: receive finished

7.flatMap

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

var cc = Set<AnyCancellable>()

struct S {
let p: AnyPublisher<String, Never>
}

let s1 = S(p: Just("one").eraseToAnyPublisher())
let s2 = S(p: Just("two").eraseToAnyPublisher())
let s3 = S(p: Just("three").eraseToAnyPublisher())

let pb = [s1, s2, s3].publisher

let sb = pb
.print("sb")
.flatMap {
$0.p
}
.sink {
print($0)
}

sb.store(in: &cc)

输出

1
2
3
4
5
6
7
8
9
sb: receive subscription: ([戴铭的开发小册子.AppDelegate.(unknown context at $101167070).(unknown context at $1011670f4).S(p: AnyPublisher), 戴铭的开发小册子.AppDelegate.(unknown context at $101167070).(unknown context at $1011670f4).S(p: AnyPublisher), 戴铭的开发小册子.AppDelegate.(unknown context at $101167070).(unknown context at $1011670f4).S(p: AnyPublisher)])
sb: request unlimited
sb: receive value: (S(p: AnyPublisher))
one
sb: receive value: (S(p: AnyPublisher))
two
sb: receive value: (S(p: AnyPublisher))
three
sb: receive finished

8.append

append 会在发布者发布结束后追加发送数据,发布者不结束,append 的数据不会发送。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import Combine

var cc = Set<AnyCancellable>()

let pb = PassthroughSubject<String, Never>()

let sb = pb
.print("sb")
.append("five", "six")
.sink {
print($0)
}

sb.store(in: &cc)

pb.send("one")
pb.send("two")
pb.send("three")
pb.send(completion: .finished)

输出

1
2
3
4
5
6
7
8
9
sb: receive subscription: ([戴铭的开发小册子.AppDelegate.(unknown context at $101167070).(unknown context at $1011670f4).S(p: AnyPublisher), 戴铭的开发小册子.AppDelegate.(unknown context at $101167070).(unknown context at $1011670f4).S(p: AnyPublisher), 戴铭的开发小册子.AppDelegate.(unknown context at $101167070).(unknown context at $1011670f4).S(p: AnyPublisher)])
sb: request unlimited
sb: receive value: (S(p: AnyPublisher))
one
sb: receive value: (S(p: AnyPublisher))
two
sb: receive value: (S(p: AnyPublisher))
three
sb: receive finished

9.prepend

prepend 会在发布者发布前先发送数据,发布者不结束也不会受影响。发布者和集合也可以被打平发布。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import Combine

var cc = Set<AnyCancellable>()

let pb1 = PassthroughSubject<String, Never>()
let pb2 = ["nine", "ten"].publisher

let sb = pb1
.print("sb")
.prepend(pb2)
.prepend(["seven","eight"])
.prepend("five", "six")
.sink {
print($0)
}

sb.store(in: &cc)

pb1.send("one")
pb1.send("two")
pb1.send("three")

输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
five
six
seven
eight
nine
ten
sb: receive subscription: (PassthroughSubject)
sb: request unlimited
sb: receive value: (one)
one
sb: receive value: (two)
two
sb: receive value: (three)
three
sb: receive cancel

10.merge

订阅者可以通过 merge 合并多个发布者发布的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import Combine

var cc = Set<AnyCancellable>()

let ps1 = PassthroughSubject<String, Never>()
let ps2 = PassthroughSubject<String, Never>()

let sb1 = ps1.merge(with: ps2)
.sink {
print($0)
}

ps1.send("one")
ps1.send("two")
ps2.send("1")
ps2.send("2")
ps1.send("three")

sb1.store(in: &cc)

输出

1
2
3
4
5
6
7
8
9
10
11
12
13
sb1: receive subscription: (Merge)
sb1: request unlimited
sb1: receive value: (one)
one
sb1: receive value: (two)
two
sb1: receive value: (1)
1
sb1: receive value: (2)
2
sb1: receive value: (three)
three
sb1: receive cancel

11.zip

zip 会合并多个发布者发布的数据,只有当多个发布者都发布了数据后才会组合成一个数据给订阅者。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import Combine

var cc = Set<AnyCancellable>()

let ps1 = PassthroughSubject<String, Never>()
let ps2 = PassthroughSubject<String, Never>()
let ps3 = PassthroughSubject<String, Never>()

let sb1 = ps1.zip(ps2, ps3)
.print("sb1")
.sink {
print($0)
}

ps1.send("one")
ps1.send("two")
ps1.send("three")
ps2.send("1")
ps2.send("2")
ps1.send("four")
ps2.send("3")
ps3.send("一")

sb1.store(in: &cc)

输出

1
2
3
4
5
sb1: receive subscription: (Zip)
sb1: request unlimited
sb1: receive value: (("one", "1", "一"))
("one", "1", "一")
sb1: receive cancel

12.combineLatest

combineLatest 会合并多个发布者发布的数据,只有当多个发布者都发布了数据后才会触发合并,合并每个发布者发布的最后一个数据。

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

var cc = Set<AnyCancellable>()

let ps1 = PassthroughSubject<String, Never>()
let ps2 = PassthroughSubject<String, Never>()
let ps3 = PassthroughSubject<String, Never>()

let sb1 = ps1.combineLatest(ps2, ps3)
.print("sb1")
.sink {
print($0)
}

ps1.send("one")
ps1.send("two")
ps1.send("three")
ps2.send("1")
ps2.send("2")
ps1.send("four")
ps2.send("3")
ps3.send("一")
ps3.send("二")

sb1.store(in: &cc)

输出

1
2
3
4
5
6
7
sb1: receive subscription: (CombineLatest)
sb1: request unlimited
sb1: receive value: (("four", "3", "一"))
("four", "3", "一")
sb1: receive value: (("four", "3", "二"))
("four", "3", "二")
sb1: receive cancel

13.Scheduler

Scheduler 处理队列。

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

var cc = Set<AnyCancellable>()

let sb1 = ["one","two","three"].publisher
.print("sb1")
.subscribe(on: DispatchQueue.global())
.handleEvents(receiveOutput: {
print("receiveOutput",$0)
})
.receive(on: DispatchQueue.main)
.sink {
print($0)
}
sb1.store(in: &cc)

输出

1
2
3
4
5
6
7
8
9
10
11
12
sb1: receive subscription: ([1, 2, 3])
sb1: request unlimited
sb1: receive value: (1)
receiveOutput 1
sb1: receive value: (2)
receiveOutput 2
sb1: receive value: (3)
receiveOutput 3
sb1: receive finished
1
2
3

Ⅲ.使用场景

1.网络请求

网络URLSession.dataTaskPublisher使用例子如下:

1
2
let req = URLRequest(url: URL(string: "http://www.starming.com")!)
let dpPublisher = URLSession.shared.dataTaskPublisher(for: req)

一个请求Github接口并展示结果的例子

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
//
// CombineSearchAPI.swift
// SwiftOnly (iOS)
//
// Created by Ming Dai on 2021/11/4.
//

import SwiftUI
import Combine

struct CombineSearchAPI: View {
var body: some View {
GithubSearchView()
}
}

// MARK: Github View
struct GithubSearchView: View {
@State var str: String = "Swift"
@StateObject var ss: SearchStore = SearchStore()
@State var repos: [GithubRepo] = []
var body: some View {
NavigationView {
List {
TextField("输入:", text: $str, onCommit: fetch)
ForEach(self.ss.repos) { repo -> GithubRepoCell in
GithubRepoCell(repo: repo)
}
}
.navigationTitle("搜索")
}
.onAppear(perform: fetch)
}

private func fetch() {
self.ss.search(str: self.str)
}
}

struct GithubRepoCell: View {
let repo: GithubRepo
var body: some View {
VStack(alignment: .leading, spacing: 20) {
Text(self.repo.name)
Text(self.repo.description)
}
}
}

// MARK: Github Service
struct GithubRepo: Decodable, Identifiable {
let id: Int
let name: String
let description: String
}

struct GithubResp: Decodable {
let items: [GithubRepo]
}

final class GithubSearchManager {
func search(str: String) -> AnyPublisher<GithubResp, Never> {
guard var urlComponents = URLComponents(string: "https://api.github.com/search/repositories") else {
preconditionFailure("链接无效")
}
urlComponents.queryItems = [URLQueryItem(name: "q", value: str)]

guard let url = urlComponents.url else {
preconditionFailure("链接无效")
}
let sch = DispatchQueue(label: "API", qos: .default, attributes: .concurrent)

return URLSession.shared
.dataTaskPublisher(for: url)
.receive(on: sch)
.tryMap({ element -> Data in
print(String(decoding: element.data, as: UTF8.self))
return element.data
})
.decode(type: GithubResp.self, decoder: JSONDecoder())
.catch { _ in
Empty().eraseToAnyPublisher()
}
.eraseToAnyPublisher()
}
}

final class SearchStore: ObservableObject {
@Published var query: String = ""
@Published var repos: [GithubRepo] = []
private let searchManager: GithubSearchManager
private var cancellable = Set<AnyCancellable>()

init(searchManager: GithubSearchManager = GithubSearchManager()) {
self.searchManager = searchManager
$query
.debounce(for: .milliseconds(500), scheduler: RunLoop.main)
.flatMap { query -> AnyPublisher<[GithubRepo], Never> in
return searchManager.search(str: query)
.map {
$0.items
}
.eraseToAnyPublisher()
}
.receive(on: DispatchQueue.main)
.assign(to: \.repos, on: self)
.store(in: &cancellable)
}
func search(str: String) {
self.query = str
}
}

抽象基础网络能力,方便扩展,代码如下:

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
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
//
// CombineAPI.swift
// SwiftOnly (iOS)
//
// Created by Ming Dai on 2021/11/4.
//

import SwiftUI
import Combine

struct CombineAPI: View {
var body: some View {
RepListView(vm: .init())
}
}

struct RepListView: View {
@ObservedObject var vm: RepListVM

var body: some View {
NavigationView {
List(vm.repos) { rep in
RepListCell(rep: rep)
}
.alert(isPresented: $vm.isErrorShow) { () -> Alert in
Alert(title: Text("出错了"), message: Text(vm.errorMessage))
}
.navigationBarTitle(Text("仓库"))
}
.onAppear {
vm.apply(.onAppear)
}
}
}

struct RepListCell: View {
@State var rep: RepoModel
var body: some View {
HStack() {
VStack() {
AsyncImage(url: URL(string: rep.owner.avatarUrl ?? ""), content: { image in
image
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 100, height: 100)
},
placeholder: {
ProgressView()
.frame(width: 100, height: 100)
})
Text("\(rep.owner.login)")
.font(.system(size: 10))
}
VStack(alignment: .leading, spacing: 10) {
Text("\(rep.name)")
.font(.title)
Text("\(rep.stargazersCount)")
.font(.title3)
Text("\(String(describing: rep.description ?? ""))")
Text("\(String(describing: rep.language ?? ""))")
.font(.title3)
}
.font(.system(size: 14))
}

}
}


// MARK: Repo View Model
final class RepListVM: ObservableObject, UnidirectionalDataFlowType {
typealias InputType = Input
private var cancellables: [AnyCancellable] = []

// Input
enum Input {
case onAppear
}
func apply(_ input: Input) {
switch input {
case .onAppear:
onAppearSubject.send(())
}
}
private let onAppearSubject = PassthroughSubject<Void, Never>()

// Output
@Published private(set) var repos: [RepoModel] = []
@Published var isErrorShow = false
@Published var errorMessage = ""
@Published private(set) var shouldShowIcon = false

private let resSubject = PassthroughSubject<SearchRepoModel, Never>()
private let errSubject = PassthroughSubject<APISevError, Never>()

private let apiSev: APISev

init(apiSev: APISev = APISev()) {
self.apiSev = apiSev
bindInputs()
bindOutputs()
}

private func bindInputs() {
let req = SearchRepoRequest()
let resPublisher = onAppearSubject
.flatMap { [apiSev] in
apiSev.response(from: req)
.catch { [weak self] error -> Empty<SearchRepoModel, Never> in
self?.errSubject.send(error)
return .init()
}
}
let resStream = resPublisher
.share()
.subscribe(resSubject)

// 其它异步事件,比如日志等操作都可以做成Stream加到下面数组内。
cancellables += [resStream]
}

private func bindOutputs() {
let repStream = resSubject
.map {
$0.items
}
.assign(to: \.repos, on: self)
let errMsgStream = errSubject
.map { error -> String in
switch error {
case .resError: return "network error"
case .parseError: return "parse error"
}
}
.assign(to: \.errorMessage, on: self)
let errStream = errSubject
.map { _ in
true
}
.assign(to: \.isErrorShow, on: self)
cancellables += [repStream,errStream,errMsgStream]
}

}


protocol UnidirectionalDataFlowType {
associatedtype InputType
func apply(_ input: InputType)
}

// MARK: Repo Request and Models

struct SearchRepoRequest: APIReqType {
typealias Res = SearchRepoModel

var path: String {
return "/search/repositories"
}
var qItems: [URLQueryItem]? {
return [
.init(name: "q", value: "Combine"),
.init(name: "order", value: "desc")
]
}
}

struct SearchRepoModel: Decodable {
var items: [RepoModel]
}

struct RepoModel: Decodable, Hashable, Identifiable {
var id: Int64
var name: String
var fullName: String
var description: String?
var stargazersCount: Int = 0
var language: String?
var owner: OwnerModel
}

struct OwnerModel: Decodable, Hashable, Identifiable {
var id: Int64
var login: String
var avatarUrl: String?
}


// MARK: API Request Fundation

protocol APIReqType {
associatedtype Res: Decodable
var path: String { get }
var qItems: [URLQueryItem]? { get }
}

protocol APISevType {
func response<Request>(from req: Request) -> AnyPublisher<Request.Res, APISevError> where Request: APIReqType
}

final class APISev: APISevType {
private let rootUrl: URL
init(rootUrl: URL = URL(string: "https://api.github.com")!) {
self.rootUrl = rootUrl
}

func response<Request>(from req: Request) -> AnyPublisher<Request.Res, APISevError> where Request : APIReqType {
let path = URL(string: req.path, relativeTo: rootUrl)!
var comp = URLComponents(url: path, resolvingAgainstBaseURL: true)!
comp.queryItems = req.qItems
print(comp.url?.description ?? "url wrong")
var req = URLRequest(url: comp.url!)
req.addValue("application/json", forHTTPHeaderField: "Content-Type")

let de = JSONDecoder()
de.keyDecodingStrategy = .convertFromSnakeCase
return URLSession.shared.dataTaskPublisher(for: req)
.map { data, res in
print(String(decoding: data, as: UTF8.self))
return data
}
.mapError { _ in
APISevError.resError
}
.decode(type: Request.Res.self, decoder: de)
.mapError(APISevError.parseError)
.receive(on: RunLoop.main)
.eraseToAnyPublisher()
}
}

enum APISevError: Error {
case resError
case parseError(Error)
}

2.KVO

例子如下:

1
2
3
4
5
6
7
8
9
10
private final class KVOObject: NSObject {
@objc dynamic var intV: Int = 0
@objc dynamic var boolV: Bool = false
}

let o = KVOObject()
let _ = o.publisher(for: \.intV)
.sink { v in
print("value : \(v)")
}

3.通知

使用例子如下:

1
2
3
4
5
6
7
8
extension Notification.Name {
static let noti = Notification.Name("nameofnoti")
}

let notiPb = NotificationCenter.default.publisher(for: .noti, object: nil)
.sink {
print($0)
}

退到后台接受通知的例子如下:

1
2
3
4
5
6
7
8
9
10
11
class A {
var storage = Set<AnyCancellable>()

init() {
NotificationCenter.default.publisher(for: UIWindowScene.didEnterBackgroundNotification)
.sink { _ in
print("enter background")
}
.store(in: &self.storage)
}
}

4.Timer

使用方式如下:

1
2
3
4
5
let timePb = Timer.publish(every: 1.0, on: RunLoop.main, in: .default)
let timeSk = timePb.sink { r in
print("r is \(r)")
}
let cPb = timePb.connect()

参考资料:


Tips: Please indicate the source and original author when reprinting or quoting this article.