注册
环信即时通讯云

环信即时通讯云

单聊、群聊、聊天室...
环信开发文档

环信开发文档

Demo体验

Demo体验

场景Demo,开箱即用
RTE开发者社区

RTE开发者社区

汇聚音视频领域技术干货,分享行业资讯
技术讨论区

技术讨论区

技术交流、答疑
资源下载

资源下载

收集了海量宝藏开发资源
iOS Library

iOS Library

不需要辛辛苦苦的去找轮子, 这里都有
Android Library

Android Library

不需要辛辛苦苦的去找轮子, 这里都有

iOS RXSwift 5.13

iOS
skipWhile跳过 Observable 中头几个元素,直到元素的判定为否skipWhile 操作符可以让你忽略源 Observable 中头几个元素,直到元素的判定为否后,它才镜像源 Obser...
继续阅读 »


skipWhile

跳过 Observable 中头几个元素,直到元素的判定为否

skipWhile 操作符可以让你忽略源 Observable 中头几个元素,直到元素的判定为否后,它才镜像源 Observable


演示

let disposeBag = DisposeBag()

Observable.of(1, 2, 3, 4, 3, 2, 1)
.skipWhile { $0 < 4 }
.subscribe(onNext: { print($0) })
.disposed(by: disposeBag)

输出结果:

4
3
2
1

skipUntil

跳过 Observable 中头几个元素,直到另一个 Observable 发出一个元素

skipUntil 操作符可以让你忽略源 Observable 中头几个元素,直到另一个 Observable 发出一个元素后,它才镜像源 Observable


演示

let disposeBag = DisposeBag()

let sourceSequence = PublishSubject<String>()
let referenceSequence = PublishSubject<String>()

sourceSequence
.skipUntil(referenceSequence)
.subscribe(onNext: { print($0) })
.disposed(by: disposeBag)

sourceSequence.onNext("🐱")
sourceSequence.onNext("🐰")
sourceSequence.onNext("🐶")

referenceSequence.onNext("🔴")

sourceSequence.onNext("🐸")
sourceSequence.onNext("🐷")
sourceSequence.onNext("🐵")

输出结果:

🐸
🐷
🐵

skip

跳过 Observable 中头 n 个元素

skip 操作符可以让你跳过 Observable 中头 n 个元素,只关注后面的元素。


演示

let disposeBag = DisposeBag()

Observable.of("🐱", "🐰", "🐶", "🐸", "🐷", "🐵")
.skip(2)
.subscribe(onNext: { print($0) })
.disposed(by: disposeBag)

输出结果:

🐶
🐸
🐷
🐵

single

限制 Observable 只有一个元素,否出发出一个 error 事件

single 操作符将限制 Observable 只产生一个元素。如果 Observable 只有一个元素,它将镜像这个 Observable 。如果 Observable 没有元素或者元素数量大于一,它将产生一个 error 事件。


shareReplay

使观察者共享 Observable,观察者会立即收到最新的元素,即使这些元素是在订阅前产生的

shareReplay 操作符将使得观察者共享源 Observable,并且缓存最新的 n 个元素,将这些元素直接发送给新的观察者。

scan

持续的将 Observable 的每一个元素应用一个函数,然后发出每一次函数返回的结果

scan 操作符将对第一个元素应用一个函数,将结果作为第一个元素发出。然后,将结果作为参数填入到第二个元素的应用函数中,创建第二个元素。以此类推,直到遍历完全部的元素。

这种操作符在其他地方有时候被称作是 accumulator


演示

let disposeBag = DisposeBag()

Observable.of(10, 100, 1000)
.scan(1) { aggregateValue, newValue in
aggregateValue + newValue
}
.subscribe(onNext: { print($0) })
.disposed(by: disposeBag)

输出结果:

11
111
1111
收起阅读 »

iOS RXSwift 5.12

iOS
sample不定期的对 Observable 取样sample 操作符将不定期的对源 Observable 进行取样操作。通过第二个 Observable 来控制取样时机。一旦第二个&nbs...
继续阅读 »

sample

不定期的对 Observable 取样

sample 操作符将不定期的对源 Observable 进行取样操作。通过第二个 Observable 来控制取样时机。一旦第二个 Observable 发出一个元素,就从源 Observable 中取出最后产生的元素。

retry

如果源 Observable 产生一个错误事件,重新对它进行订阅,希望它不会再次产生错误

retry 操作符将不会将 error 事件,传递给观察者,然而,它会从新订阅源 Observable,给这个 Observable 一个重试的机会,让它有机会不产生 error 事件。retry 总是对观察者发出 next 事件,即便源序列产生了一个 error 事件,所以这样可能会产生重复的元素(如上图所示)。


演示 1

let disposeBag = DisposeBag()
var count = 1

let sequenceThatErrors = Observable<String>.create { observer in
observer.onNext("🍎")
observer.onNext("🍐")
observer.onNext("🍊")

if count == 1 {
observer.onError(TestError.test)
print("Error encountered")
count += 1
}

observer.onNext("🐶")
observer.onNext("🐱")
observer.onNext("🐭")
observer.onCompleted()

return Disposables.create()
}

sequenceThatErrors
.retry()
.subscribe(onNext: { print($0) })
.disposed(by: disposeBag)

输出结果:

🍎
🍐
🍊
Error encountered
🍎
🍐
🍊
🐶
🐱
🐭

演示 2

let disposeBag = DisposeBag()
var count = 1

let sequenceThatErrors = Observable<String>.create { observer in
observer.onNext("🍎")
observer.onNext("🍐")
observer.onNext("🍊")

if count < 5 {
observer.onError(TestError.test)
print("Error encountered")
count += 1
}

observer.onNext("🐶")
observer.onNext("🐱")
observer.onNext("🐭")
observer.onCompleted()

return Disposables.create()
}

sequenceThatErrors
.retry(3)
.subscribe(onNext: { print($0) })
.disposed(by: disposeBag)

输出结果:

🍎
🍐
🍊
Error encountered
🍎
🍐
🍊
Error encountered
🍎
🍐
🍊
Error encountered
Unhandled error happened: test
subscription called from:


replay

确保观察者接收到同样的序列,即使是在 Observable 发出元素后才订阅

可被连接的 Observable 和普通的 Observable 十分相似,不过在被订阅后不会发出元素,直到 connect 操作符被应用为止。这样一来你可以控制 Observable 在什么时候开始发出元素。

replay 操作符将 Observable 转换为可被连接的 Observable,并且这个可被连接的 Observable 将缓存最新的 n 个元素。当有新的观察者对它进行订阅时,它就把这些被缓存的元素发送给观察者。


演示

let intSequence = Observable<Int>.interval(1, scheduler: MainScheduler.instance)
.replay(5)

_ = intSequence
.subscribe(onNext: { print("Subscription 1:, Event: \($0)") })

DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
_ = intSequence.connect()
}

DispatchQueue.main.asyncAfter(deadline: .now() + 4) {
_ = intSequence
.subscribe(onNext: { print("Subscription 2:, Event: \($0)") })
}

DispatchQueue.main.asyncAfter(deadline: .now() + 8) {
_ = intSequence
.subscribe(onNext: { print("Subscription 3:, Event: \($0)") })
}

输出结果:

Subscription 1:, Event: 0
Subscription 2:, Event: 0
Subscription 1:, Event: 1
Subscription 2:, Event: 1
Subscription 1:, Event: 2
Subscription 2:, Event: 2
Subscription 1:, Event: 3
Subscription 2:, Event: 3
Subscription 1:, Event: 4
Subscription 2:, Event: 4
Subscription 3:, Event: 0
Subscription 3:, Event: 1
Subscription 3:, Event: 2
Subscription 3:, Event: 3
Subscription 3:, Event: 4
Subscription 1:, Event: 5
Subscription 2:, Event: 5
Subscription 3:, Event: 5
Subscription 1:, Event: 6
Subscription 2:, Event: 6
Subscription 3:, Event: 6
...
收起阅读 »

iOS RXSwift 5.10

iOS
repeatElement创建 Observable 重复的发出某个元素repeatElement 操作符将创建一个 Observable,这个 Observable 将无止尽的发出同一个元素。演示创...
继续阅读 »

repeatElement

创建 Observable 重复的发出某个元素

repeatElement 操作符将创建一个 Observable,这个 Observable 将无止尽的发出同一个元素。

演示

创建 Observable 重复的发出某个元素:

let id = Observable.repeatElement(0)

它相当于:

let id = Observable<Int>.create { observer in
observer.onNext(0)
observer.onNext(0)
observer.onNext(0)
observer.onNext(0)
... // 无数次
return Disposables.create()
}

refCount

将可被连接的 Observable 转换为普通 Observable

可被连接的 Observable 和普通的 Observable 十分相似,不过在被订阅后不会发出元素,直到 connect 操作符被应用为止。这样一来你可以控制 Observable 在什么时候开始发出元素。

refCount 操作符将自动连接和断开可被连接的 Observable。它将可被连接的 Observable 转换为普通 Observable。当第一个观察者对它订阅时,那么底层的 Observable 将被连接。当最后一个观察者离开时,那么底层的 Observable 将被断开连接。

reduce

持续的将 Observable 的每一个元素应用一个函数,然后发出最终结果

reduce 操作符将对第一个元素应用一个函数。然后,将结果作为参数填入到第二个元素的应用函数中。以此类推,直到遍历完全部的元素后发出最终结果。

这种操作符在其他地方有时候被称作是 accumulatoraggregatecompressfold 或者 inject


演示

let disposeBag = DisposeBag()

Observable.of(10, 100, 1000)
.reduce(1, accumulator: +)
.subscribe(onNext: { print($0) })
.disposed(by: disposeBag)

输出结果:

1111
收起阅读 »

iOS RXSwift 5.11

iOS
publish将 Observable 转换为可被连接的 Observablepublish 会将 Observable 转换为可被连接的 Observable。可被连接的 Obs...
继续阅读 »

publish

将 Observable 转换为可被连接的 Observable

publish 会将 Observable 转换为可被连接的 Observable可被连接的 Observable 和普通的 Observable 十分相似,不过在被订阅后不会发出元素,直到 connect 操作符被应用为止。这样一来你可以控制 Observable 在什么时候开始发出元素。


演示

let intSequence = Observable<Int>.interval(1, scheduler: MainScheduler.instance)
.publish()

_ = intSequence
.subscribe(onNext: { print("Subscription 1:, Event: \($0)") })

DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
_ = intSequence.connect()
}

DispatchQueue.main.asyncAfter(deadline: .now() + 4) {
_ = intSequence
.subscribe(onNext: { print("Subscription 2:, Event: \($0)") })
}

DispatchQueue.main.asyncAfter(deadline: .now() + 6) {
_ = intSequence
.subscribe(onNext: { print("Subscription 3:, Event: \($0)") })
}

输出结果:

Subscription 1:, Event: 0
Subscription 1:, Event: 1
Subscription 2:, Event: 1
Subscription 1:, Event: 2
Subscription 2:, Event: 2
Subscription 1:, Event: 3
Subscription 2:, Event: 3
Subscription 3:, Event: 3
Subscription 1:, Event: 4
Subscription 2:, Event: 4
Subscription 3:, Event: 4
Subscription 1:, Event: 5
Subscription 2:, Event: 5
Subscription 3:, Event: 5
Subscription 1:, Event: 6
Subscription 2:, Event: 6
Subscription 3:, Event: 6
...

observeOn

指定 Observable 在那个 Scheduler 发出通知

ReactiveX 使用 Scheduler 来让 Observable 支持多线程。你可以使用 observeOn 操作符,来指示 Observable 在哪个 Scheduler 发出通知。

注意⚠️:一旦产生了 onError 事件, observeOn 操作符将立即转发。他不会等待 onError 之前的事件全部被收到。这意味着 onError 事件可能会跳过一些元素提前发送出去,如上图所示。

subscribeOn 操作符非常相似。它指示 Observable 在哪个 Scheduler 发出执行。

默认情况下,Observable 创建,应用操作符以及发出通知都会在 Subscribe 方法调用的 Scheduler 执行。subscribeOn 操作符将改变这种行为,它会指定一个不同的 Scheduler 来让 Observable 执行,observeOn 操作符将指定一个不同的 Scheduler 来让 Observable 通知观察者。

如上图所示,subscribeOn 操作符指定 Observable 在那个 Scheduler 开始执行,无论它处于链的那个位置。 另一方面 observeOn 将决定后面的方法在哪个 Scheduler 运行。因此,你可能会多次调用 observeOn 来决定某些操作符在哪个线程运行。

never

创建一个永远不会发出元素的 Observable

never 操作符将创建一个 Observable,这个 Observable 不会产生任何事件。


演示

创建一个不会产生任何事件的 Observable

let id = Observable<Int>.never()

它相当于:

let id = Observable<Int>.create { observer in
return Disposables.create()
}
收起阅读 »

iOS RXSwift 5.9

iOS
materialize将序列产生的事件,转换成元素通常,一个有限的 Observable 将产生零个或者多个 onNext 事件,然后产生一个 onCompleted 或者 onError&...
继续阅读 »

materialize

将序列产生的事件,转换成元素

通常,一个有限的 Observable 将产生零个或者多个 onNext 事件,然后产生一个 onCompleted 或者 onError 事件。

materialize 操作符将 Observable 产生的这些事件全部转换成元素,然后发送出来。

merge

将多个 Observables 合并成一个

通过使用 merge 操作符你可以将多个 Observables 合并成一个,当某一个 Observable 发出一个元素时,他就将这个元素发出。

如果,某一个 Observable 发出一个 onError 事件,那么被合并的 Observable 也会将它发出,并且立即终止序列。


演示

let disposeBag = DisposeBag()

let subject1 = PublishSubject<String>()
let subject2 = PublishSubject<String>()

Observable.of(subject1, subject2)
.merge()
.subscribe(onNext: { print($0) })
.disposed(by: disposeBag)

subject1.onNext("🅰️")

subject1.onNext("🅱️")

subject2.onNext("①")

subject2.onNext("②")

subject1.onNext("🆎")

subject2.onNext("③")

输出结果:

🅰️
🅱️


🆎

map

通过一个转换函数,将 Observable 的每个元素转换一遍

map 操作符将源 Observable 的每个元素应用你提供的转换方法,然后返回含有转换结果的 Observable


演示

let disposeBag = DisposeBag()
Observable.of(1, 2, 3)
.map { $0 * 10 }
.subscribe(onNext: { print($0) })
.disposed(by: disposeBag)

输出结果:

10
20
30

参考

just

创建 Observable 发出唯一的一个元素

just 操作符将某一个元素转换为 Observable


演示

一个序列只有唯一的元素 0

let id = Observable.just(0)

它相当于:

let id = Observable<Int>.create { observer in
observer.onNext(0)
observer.onCompleted()
return Disposables.create()
}

interval

创建一个 Observable 每隔一段时间,发出一个索引数

interval 操作符将创建一个 Observable,它每隔一段设定的时间,发出一个索引数的元素。它将发出无数个元素。

ignoreElements

忽略掉所有的元素,只发出 error 或 completed 事件

ignoreElements 操作符将阻止 Observable 发出 next 事件,但是允许他发出 error 或 completed 事件。

如果你并不关心 Observable 的任何元素,你只想知道 Observable 在什么时候终止,那就可以使用 ignoreElements 操作符。

收起阅读 »

iOS RXSwift 5.8

iOS
groupBy将源 Observable 分解为多个子 Observable,并且每个子 Observable 将源 Observable 中“相似”的元素发送出来groupBy ...
继续阅读 »

groupBy

将源 Observable 分解为多个子 Observable,并且每个子 Observable 将源 Observable 中“相似”的元素发送出来

groupBy 操作符将源 Observable 分解为多个子 Observable,然后将这些子 Observable 发送出来。

它会将元素通过某个键进行分组,然后将分组后的元素序列以 Observable 的形态发送出来。

from

将其他类型或者数据结构转换为 Observable

当你在使用 Observable 时,如果能够直接将其他类型转换为 Observable,这将是非常省事的。from 操作符就提供了这种功能。


演示

将一个数组转换为 Observable

let numbers = Observable.from([0, 1, 2])

它相当于:

let numbers = Observable<Int>.create { observer in
observer.onNext(0)
observer.onNext(1)
observer.onNext(2)
observer.onCompleted()
return Disposables.create()
}

将一个可选值转换为 Observable

let optional: Int? = 1
let value = Observable.from(optional: optional)

它相当于:

let optional: Int? = 1
let value = Observable<Int>.create { observer in
if let element = optional {
observer.onNext(element)
}
observer.onCompleted()
return Disposables.create()
}

flatMapLatest

将 Observable 的元素转换成其他的 Observable,然后取这些 Observables 中最新的一个

flatMapLatest 操作符将源 Observable 的每一个元素应用一个转换方法,将他们转换成 Observables。一旦转换出一个新的 Observable,就只发出它的元素,旧的 Observables 的元素将被忽略掉。


演示

tips:与 flatMap 比较更容易理解

let disposeBag = DisposeBag()
let first = BehaviorSubject(value: "👦🏻")
let second = BehaviorSubject(value: "🅰️")
let variable = Variable(first)

variable.asObservable()
.flatMapLatest { $0 }
.subscribe(onNext: { print($0) })
.disposed(by: disposeBag)

first.onNext("🐱")
variable.value = second
second.onNext("🅱️")
first.onNext("🐶")

输出结果:

👦🏻
🐱
🅰️
🅱️

flatMap

将 Observable 的元素转换成其他的 Observable,然后将这些 Observables 合并

flatMap 操作符将源 Observable 的每一个元素应用一个转换方法,将他们转换成 Observables。 然后将这些 Observables 的元素合并之后再发送出来。

这个操作符是非常有用的,例如,当 Observable 的元素本身拥有其他的 Observable 时,你可以将所有 Observables 的元素发送出来。


演示

let disposeBag = DisposeBag()
let first = BehaviorSubject(value: "👦🏻")
let second = BehaviorSubject(value: "🅰️")
let variable = Variable(first)

variable.asObservable()
.flatMap { $0 }
.subscribe(onNext: { print($0) })
.disposed(by: disposeBag)

first.onNext("🐱")
variable.value = second
second.onNext("🅱️")
first.onNext("🐶")

输出结果:

👦🏻
🐱
🅰️
🅱️
🐶
收起阅读 »

iOS RXSwift 5.7

iOS
filter仅仅发出 Observable 中通过判定的元素filter 操作符将通过你提供的判定方法过滤一个 Observable。演示let disposeBag = DisposeBag() Observable...
继续阅读 »

filter

仅仅发出 Observable 中通过判定的元素

filter 操作符将通过你提供的判定方法过滤一个 Observable


演示

let disposeBag = DisposeBag()

Observable.of(2, 30, 22, 5, 60, 1)
.filter { $0 > 10 }
.subscribe(onNext: { print($0) })
.disposed(by: disposeBag)

输出结果:

30
22
60

error

创建一个只有 error 事件的 Observable

error 操作符将创建一个 Observable,这个 Observable 只会产生一个 error 事件。


演示

创建一个只有 error 事件的 Observable

let error: Error = ...
let id = Observable<Int>.error(error)

它相当于:

let error: Error = ...
let id = Observable<Int>.create { observer in
observer.onError(error)
return Disposables.create()
}

elementAt

只发出 Observable 中的第 n 个元素

elementAt 操作符将拉取 Observable 序列中指定索引数的元素,然后将它作为唯一的元素发出。


演示

let disposeBag = DisposeBag()

Observable.of("🐱", "🐰", "🐶", "🐸", "🐷", "🐵")
.elementAt(3)
.subscribe(onNext: { print($0) })
.disposed(by: disposeBag)

输出结果:

🐸


empty

创建一个空 Observable

empty 操作符将创建一个 Observable,这个 Observable 只有一个完成事件。


演示

创建一个空 Observable

let id = Observable<Int>.empty()

它相当于:

let id = Observable<Int>.create { observer in
observer.onCompleted()
return Disposables.create()
}
收起阅读 »

iOS RXSwift 5.6

iOS
delay将 Observable 的每一个元素拖延一段时间后发出delay 操作符将修改一个 Observable,它会将 Observable 的所有元素都拖延一段设定好的时间, 然后才将它们发送...
继续阅读 »

delay

将 Observable 的每一个元素拖延一段时间后发出

delay 操作符将修改一个 Observable,它会将 Observable 的所有元素都拖延一段设定好的时间, 然后才将它们发送出来。


delaySubscription

进行延时订阅

delaySubscription 操作符将在经过所设定的时间后,才对 Observable 进行订阅操作。


dematerialize

dematerialize 操作符将 materialize 转换后的元素还原


distinctUntilChanged

阻止 Observable 发出相同的元素

distinctUntilChanged 操作符将阻止 Observable 发出相同的元素。如果后一个元素和前一个元素是相同的,那么这个元素将不会被发出来。如果后一个元素和前一个元素不相同,那么这个元素才会被发出来。


演示

let disposeBag = DisposeBag()

Observable.of("🐱", "🐷", "🐱", "🐱", "🐱", "🐵", "🐱")
.distinctUntilChanged()
.subscribe(onNext: { print($0) })
.disposed(by: disposeBag)

输出结果:

🐱
🐷
🐱
🐵
🐱



do

当 Observable 产生某些事件时,执行某个操作

当 Observable 的某些事件产生时,你可以使用 do 操作符来注册一些回调操作。这些回调会被单独调用,它们会和 Observable 原本的回调分离。

收起阅读 »

iOS RXSwift 5.5

iOS
deferred直到订阅发生,才创建 Observable,并且为每位订阅者创建全新的 Observabledeferred 操作符将等待观察者订阅它,才创建一个 Observable,它会通过一个构建函数为每一位订阅者创建新的 Observable。看上去每...
继续阅读 »

deferred

直到订阅发生,才创建 Observable,并且为每位订阅者创建全新的 Observable

deferred 操作符将等待观察者订阅它,才创建一个 Observable,它会通过一个构建函数为每一位订阅者创建新的 Observable。看上去每位订阅者都是对同一个 Observable 产生订阅,实际上它们都获得了独立的序列。

在一些情况下,直到订阅时才创建 Observable 是可以保证拿到的数据都是最新的。

debug

打印所有的订阅,事件以及销毁信息


演示

let disposeBag = DisposeBag()

let sequence = Observable<String>.create { observer in
observer.onNext("🍎")
observer.onNext("🍐")
observer.onCompleted()
return Disposables.create()
}

sequence
.debug("Fruit")
.subscribe()
.disposed(by: disposeBag)

输出结果:

2017-11-06 20:49:43.187: Fruit -> subscribed
2017-11-06 20:49:43.188: Fruit -> Event next(🍎)
2017-11-06 20:49:43.188: Fruit -> Event next(🍐)
2017-11-06 20:49:43.188: Fruit -> Event completed
2017-11-06 20:49:43.189: Fruit -> isDisposed

debounce

过滤掉高频产生的元素

debounce 操作符将发出这种元素,在 Observable 产生这种元素后,一段时间内没有新元素产生。

收起阅读 »

iOS RXSwift 5.4

iOS
connect通知 ConnectableObservable 可以开始发出元素了ConnectableObservable 和普通的 Observable 十分相似,不过在被订阅后不会发出元素,直到 ...
继续阅读 »


connect

通知 ConnectableObservable 可以开始发出元素了

ConnectableObservable 和普通的 Observable 十分相似,不过在被订阅后不会发出元素,直到 connect 操作符被应用为止。这样一来你可以等所有观察者全部订阅完成后,才发出元素。


演示

let intSequence = Observable<Int>.interval(1, scheduler: MainScheduler.instance)
.publish()

_ = intSequence
.subscribe(onNext: { print("Subscription 1:, Event: \($0)") })

DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
_ = intSequence.connect()
}

DispatchQueue.main.asyncAfter(deadline: .now() + 4) {
_ = intSequence
.subscribe(onNext: { print("Subscription 2:, Event: \($0)") })
}

DispatchQueue.main.asyncAfter(deadline: .now() + 6) {
_ = intSequence
.subscribe(onNext: { print("Subscription 3:, Event: \($0)") })
}

输出结果:

Subscription 1:, Event: 0
Subscription 1:, Event: 1
Subscription 2:, Event: 1
Subscription 1:, Event: 2
Subscription 2:, Event: 2
Subscription 1:, Event: 3
Subscription 2:, Event: 3
Subscription 3:, Event: 3
Subscription 1:, Event: 4
Subscription 2:, Event: 4
Subscription 3:, Event: 4
Subscription 1:, Event: 5
Subscription 2:, Event: 5
Subscription 3:, Event: 5
Subscription 1:, Event: 6
Subscription 2:, Event: 6
Subscription 3:, Event: 6
...

create

通过一个构建函数完整的创建一个 Observable

create 操作符将创建一个 Observable,你需要提供一个构建函数,在构建函数里面描述事件(nexterrorcompleted)的产生过程。

通常情况下一个有限的序列,只会调用一次观察者的 onCompleted 或者 onError 方法。并且在调用它们后,不会再去调用观察者的其他方法。


演示

创建一个 [0, 1, ... 8, 9] 的序列:

let id = Observable<Int>.create { observer in
observer.onNext(0)
observer.onNext(1)
observer.onNext(2)
observer.onNext(3)
observer.onNext(4)
observer.onNext(5)
observer.onNext(6)
observer.onNext(7)
observer.onNext(8)
observer.onNext(9)
observer.onCompleted()
return Disposables.create()
}
收起阅读 »

iOS RXSwift 5.3

iOS
concat让两个或多个 Observables 按顺序串连起来concat 操作符将多个 Observables 按顺序串联起来,当前一个 Observable 元素发送完毕后,后一个&n...
继续阅读 »

concat

让两个或多个 Observables 按顺序串连起来

concat 操作符将多个 Observables 按顺序串联起来,当前一个 Observable 元素发送完毕后,后一个 Observable 才可以开始发出元素。

concat 将等待前一个 Observable 产生完成事件后,才对后一个 Observable 进行订阅。如果后一个是“热” Observable ,在它前一个 Observable 产生完成事件前,所产生的元素将不会被发送出来。

startWith 和它十分相似。但是startWith不是在后面添加元素,而是在前面插入元素。

merge 和它也是十分相似。merge并不是将多个 Observables 按顺序串联起来,而是将他们合并到一起,不需要 Observables 按先后顺序发出元素。


演示

let disposeBag = DisposeBag()

let subject1 = BehaviorSubject(value: "🍎")
let subject2 = BehaviorSubject(value: "🐶")

let variable = Variable(subject1)

variable.asObservable()
.concat()
.subscribe { print($0) }
.disposed(by: disposeBag)

subject1.onNext("🍐")
subject1.onNext("🍊")

variable.value = subject2

subject2.onNext("I would be ignored")
subject2.onNext("🐱")

subject1.onCompleted()

subject2.onNext("🐭")

输出结果:

next(🍎)
next(🍐)
next(🍊)
next(🐱)
next(🐭)


concatMap

将 Observable 的元素转换成其他的 Observable,然后将这些 Observables 串连起来

concatMap 操作符将源 Observable 的每一个元素应用一个转换方法,将他们转换成 Observables。然后让这些 Observables 按顺序的发出元素,当前一个 Observable 元素发送完毕后,后一个 Observable 才可以开始发出元素。等待前一个 Observable 产生完成事件后,才对后一个 Observable 进行订阅。


演示

let disposeBag = DisposeBag()

let subject1 = BehaviorSubject(value: "🍎")
let subject2 = BehaviorSubject(value: "🐶")

let variable = Variable(subject1)

variable.asObservable()
.concatMap { $0 }
.subscribe { print($0) }
.disposed(by: disposeBag)

subject1.onNext("🍐")
subject1.onNext("🍊")

variable.value = subject2

subject2.onNext("I would be ignored")
subject2.onNext("🐱")

subject1.onCompleted()

subject2.onNext("🐭")

输出结果:

next(🍎)
next(🍐)
next(🍊)
next(🐱)
next(🐭)
收起阅读 »

iOS RXSwift 5.2

iOS
buffer缓存元素,然后将缓存的元素集合,周期性的发出来buffer 操作符将缓存 Observable 中发出的新元素,当元素达到某个数量,或者经过了特定的时间,它就会将这个元素集合发送出来。catchError从一个错误事件...
继续阅读 »

buffer

缓存元素,然后将缓存的元素集合,周期性的发出来

buffer 操作符将缓存 Observable 中发出的新元素,当元素达到某个数量,或者经过了特定的时间,它就会将这个元素集合发送出来。

catchError

从一个错误事件中恢复,将错误事件替换成一个备选序列

catchError 操作符将会拦截一个 error 事件,将它替换成其他的元素或者一组元素,然后传递给观察者。这样可以使得 Observable 正常结束,或者根本都不需要结束。

这里存在其他版本的 catchError 操作符。


演示

let disposeBag = DisposeBag()

let sequenceThatFails = PublishSubject<String>()
let recoverySequence = PublishSubject<String>()

sequenceThatFails
.catchError {
print("Error:", $0)
return recoverySequence
}
.subscribe { print($0) }
.disposed(by: disposeBag)

sequenceThatFails.onNext("😬")
sequenceThatFails.onNext("😨")
sequenceThatFails.onNext("😡")
sequenceThatFails.onNext("🔴")
sequenceThatFails.onError(TestError.test)

recoverySequence.onNext("😊")

输出结果:

next(😬)
next(😨)
next(😡)
next(🔴)
Error: test
next(😊)

catchErrorJustReturn

catchErrorJustReturn 操作符会将error 事件替换成其他的一个元素,然后结束该序列。


演示

let disposeBag = DisposeBag()
let sequenceThatFails = PublishSubject<String>()

sequenceThatFails
.catchErrorJustReturn("😊")
.subscribe { print($0) }
.disposed(by: disposeBag)

sequenceThatFails.onNext("😬")
sequenceThatFails.onNext("😨")
sequenceThatFails.onNext("😡")
sequenceThatFails.onNext("🔴")
sequenceThatFails.onError(TestError.test)

输出结果:

next(😬)
next(😨)
next(😡)
next(🔴)
next(😊)
completed



combineLatest

当多个 Observables 中任何一个发出一个元素,就发出一个元素。这个元素是由这些 Observables 中最新的元素,通过一个函数组合起来的

combineLatest 操作符将多个 Observables 中最新的元素通过一个函数组合起来,然后将这个组合的结果发出来。这些源 Observables 中任何一个发出一个元素,他都会发出一个元素(前提是,这些 Observables 曾经发出过元素)。


演示

tips: 可与 zip 比较学习

let disposeBag = DisposeBag()

let first = PublishSubject<String>()
let second = PublishSubject<String>()

Observable.combineLatest(first, second) { $0 + $1 }
.subscribe(onNext: { print($0) })
.disposed(by: disposeBag)

first.onNext("1")
second.onNext("A")
first.onNext("2")
second.onNext("B")
second.onNext("C")
second.onNext("D")
first.onNext("3")
first.onNext("4")

输出结果:

1A
2A
2B
2C
2D
3D
4D
收起阅读 »

iOS RXSwift 5.1

iOS
如何选择操作符?下面这个决策树可以帮助你找到需要的操作符。决策树我想要创建一个 Observable产生特定的一个元素:just经过一段延时:timer从一个序列拉取元素:from重复的产生某一个元素:repeatElement存在自定义逻辑:cre...
继续阅读 »

如何选择操作符?

下面这个决策树可以帮助你找到需要的操作符。


决策树

我想要创建一个 Observable

  • 产生特定的一个元素:just
    • 经过一段延时:timer
  • 从一个序列拉取元素:from
  • 重复的产生某一个元素:repeatElement
  • 存在自定义逻辑:create
  • 每次订阅时产生:deferred
  • 每隔一段时间,发出一个元素:interval
    • 在一段延时后:timer
  • 一个空序列,只有一个完成事件:empty
  • 一个任何事件都没有产生的序列:never

我想要创建一个 Observable 通过组合其他的 Observables

  • 任意一个 Observable 产生了元素,就发出这个元素:merge
  • 让这些 Observables 一个接一个的发出元素,当上一个 Observable 元素发送完毕后,下一个 Observable 才能开始发出元素:concat
  • 组合多个 Observables 的元素
    • 当每一个 Observable 都发出一个新的元素:zip
    • 当任意一个 Observable 发出一个新的元素:combineLatest

我想要转换 Observable 的元素后,再将它们发出来

  • 对每个元素直接转换:map
  • 转换到另一个 ObservableflatMap
    • 只接收最新的元素转换的 Observable 所产生的元素:flatMapLatest
    • 每一个元素转换的 Observable 按顺序产生元素:concatMap
  • 基于所有遍历过的元素: scan

我想要将产生的每一个元素,拖延一段时间后再发出:delay

我想要将产生的事件封装成元素发送出来

我想要忽略掉所有的 next 事件,只接收 completed 和 error 事件:ignoreElements

我想创建一个新的 Observable 在原有的序列前面加入一些元素:startWith

我想从 Observable 中收集元素,缓存这些元素之后在发出:buffer

我想将 Observable 拆分成多个 Observableswindow

  • 基于元素的共同特征:groupBy

我想只接收 Observable 中特定的元素

  • 发出唯一的元素:single

我想重新从 Observable 中发出某些元素

  • 通过判定条件过滤出一些元素:filter
  • 仅仅发出头几个元素:take
  • 仅仅发出尾部的几个元素:takeLast
  • 仅仅发出第 n 个元素:elementAt
  • 跳过头几个元素
    • 跳过头 n 个元素:skip
    • 跳过头几个满足判定的元素:skipWhileskipWhileWithIndex
    • 跳过某段时间内产生的头几个元素:skip
    • 跳过头几个元素直到另一个 Observable 发出一个元素:skipUntil
  • 只取头几个元素
    • 只取头几个满足判定的元素:takeWhiletakeWhileWithIndex
    • 只取某段时间内产生的头几个元素:take
    • 只取头几个元素直到另一个 Observable 发出一个元素:takeUntil
  • 周期性的对 Observable 抽样:sample
  • 发出那些元素,这些元素产生后的特定的时间内,没有新的元素产生:debounce
  • 直到元素的值发生变化,才发出新的元素:distinctUntilChanged
  • 在开始发出元素时,延时后进行订阅:delaySubscription

我想要从一些 Observables 中,只取第一个产生元素的 Observableamb

我想评估 Observable 的全部元素

  • 并且对每个元素应用聚合方法,待所有元素都应用聚合方法后,发出结果:reduce
  • 并且对每个元素应用聚合方法,每次应用聚合方法后,发出结果:scan

我想把 Observable 转换为其他的数据结构:as...

我想在某个 Scheduler 应用操作符:subscribeOn

我想要 Observable 发生某个事件时, 采取某个行动:do

我想要 Observable 发出一个 error 事件:error

  • 如果规定时间内没有产生元素:timeout

我想要 Observable 发生错误时,优雅的恢复

  • 如果规定时间内没有产生元素,就切换到备选 Observable :timeout
  • 如果产生错误,将错误替换成某个元素 :catchErrorJustReturn
  • 如果产生错误,就切换到备选 Observable :catchError
  • 如果产生错误,就重试 :retry

我创建一个 Disposable 资源,使它与 Observable 具有相同的寿命:using

我创建一个 Observable,直到我通知它可以产生元素后,才能产生元素:publish

  • 并且,就算是在产生元素后订阅,也要发出全部元素:replay
  • 并且,一旦所有观察者取消观察,他就被释放掉:refCount
  • 通知它可以产生元素了:connect


amb

在多个源 Observables 中, 取第一个发出元素或产生事件的 Observable,然后只发出它的元素

当你传入多个 Observables 到 amb 操作符时,它将取其中一个 Observable:第一个产生事件的那个 Observable,可以是一个 nexterror 或者 completed 事件。 amb 将忽略掉其他的 Observables

收起阅读 »

iOS RXSwift 4.9

iOS
Schedulers - 调度器Schedulers 是 Rx 实现多线程的核心模块,它主要用于控制任务在哪个线程或队列运行。如果你曾经使用过 GCD, 那你对以下代码应该不会陌生:// 后台取得数据,主线程处理结果 D...
继续阅读 »

Schedulers - 调度器

Schedulers 是 Rx 实现多线程的核心模块,它主要用于控制任务在哪个线程或队列运行。

如果你曾经使用过 GCD, 那你对以下代码应该不会陌生:

// 后台取得数据,主线程处理结果
DispatchQueue.global(qos: .userInitiated).async {
let data = try? Data(contentsOf: url)
DispatchQueue.main.async {
self.data = data
}
}

如果用 RxSwift 来实现,大致是这样的:

let rxData: Observable<Data> = ...

rxData
.subscribeOn(ConcurrentDispatchQueueScheduler(qos: .userInitiated))
.observeOn(MainScheduler.instance)
.subscribe(onNext: { [weak self] data in
self?.data = data
})
.disposed(by: disposeBag)

使用 subscribeOn

我们用 subscribeOn 来决定数据序列的构建函数在哪个 Scheduler 上运行。以上例子中,由于获取 Data 需要花很长的时间,所以用 subscribeOn 切换到 后台 Scheduler 来获取 Data。这样可以避免主线程被阻塞。

使用 observeOn

我们用 observeOn 来决定在哪个 Scheduler 监听这个数据序列。以上例子中,通过使用 observeOn 方法切换到主线程来监听并且处理结果。

一个比较典型的例子就是,在后台发起网络请求,然后解析数据,最后在主线程刷新页面。你就可以先用 subscribeOn 切到后台去发送请求并解析数据,最后用 observeOn 切换到主线程更新页面。


MainScheduler

MainScheduler 代表主线程。如果你需要执行一些和 UI 相关的任务,就需要切换到该 Scheduler 运行。

SerialDispatchQueueScheduler

SerialDispatchQueueScheduler 抽象了串行 DispatchQueue。如果你需要执行一些串行任务,可以切换到这个 Scheduler 运行。

ConcurrentDispatchQueueScheduler

ConcurrentDispatchQueueScheduler 抽象了并行 DispatchQueue。如果你需要执行一些并发任务,可以切换到这个 Scheduler 运行。

OperationQueueScheduler

OperationQueueScheduler 抽象了 NSOperationQueue

它具备 NSOperationQueue 的一些特点,例如,你可以通过设置 maxConcurrentOperationCount,来控制同时执行并发任务的最大数量。

Error Handling - 错误处理

一旦序列里面产出了一个 error 事件,整个序列将被终止。RxSwift 主要有两种错误处理机制:

  • retry - 重试
  • catch - 恢复

retry - 重试

retry 可以让序列在发生错误后重试:

// 请求 JSON 失败时,立即重试,
// 重试 3 次后仍然失败,就将错误抛出

let rxJson: Observable<JSON> = ...

rxJson
.retry(3)
.subscribe(onNext: { json in
print("取得 JSON 成功: \(json)")
}, onError: { error in
print("取得 JSON 失败: \(error)")
})
.disposed(by: disposeBag)

以上的代码非常直接 retry(3) 就是当发生错误时,就进行重试操作,并且最多重试 3 次。

retryWhen

如果我们需要在发生错误时,经过一段延时后重试,那可以这样实现:

// 请求 JSON 失败时,等待 5 秒后重试,

let retryDelay: Double = 5 // 重试延时 5 秒

rxJson
.retryWhen { (rxError: Observable<Error>) -> Observable<Int> in
return Observable.timer(retryDelay, scheduler: MainScheduler.instance)
}
.subscribe(...)
.disposed(by: disposeBag)

这里我们需要用到 retryWhen 操作符,这个操作符主要描述应该在何时重试,并且通过闭包里面返回的 Observable 来控制重试的时机:

.retryWhen { (rxError: Observable<Error>) -> Observable<Int> in
...
}

闭包里面的参数是 Observable<Error> 也就是所产生错误的序列,然后返回值是一个 Observable。当这个返回的 Observable 发出一个元素时,就进行重试操作。当它发出一个 error 或者 completed 事件时,就不会重试,并且将这个事件传递给到后面的观察者。

如果需要加上一个最大重试次数的限制:

// 请求 JSON 失败时,等待 5 秒后重试,
// 重试 4 次后仍然失败,就将错误抛出

let maxRetryCount = 4 // 最多重试 4 次
let retryDelay: Double = 5 // 重试延时 5 秒

rxJson
.retryWhen { (rxError: Observable<Error>) -> Observable<Int> in
return rxError.flatMapWithIndex { (error, index) -> Observable<Int> in
guard index < maxRetryCount else {
return Observable.error(error)
}
return Observable<Int>.timer(retryDelay, scheduler: MainScheduler.instance)
}
}
.subscribe(...)
.disposed(by: disposeBag)

我们这里要实现的是,如果重试超过 4 次,就将错误抛出。如果错误在 4 次以内时,就等待 5 秒后重试:

...
rxError.flatMapWithIndex { (error, index) -> Observable<Int> in
guard index < maxRetryCount else {
return Observable.error(error)
}
return Observable<Int>.timer(retryDelay, scheduler: MainScheduler.instance)
}
...

我们用 flatMapWithIndex 这个操作符,因为它可以给我们提供错误的索引数 index。然后用这个索引数判断是否超过最大重试数,如果超过了,就将错误抛出。如果没有超过,就等待 5 秒后重试。


catchError - 恢复

catchError 可以在错误产生时,用一个备用元素或者一组备用元素将错误替换掉:

searchBar.rx.text.orEmpty
...
.flatMapLatest { query -> Observable<[Repository]> in
...
return searchGitHub(query)
.catchErrorJustReturn([])
}
...
.bind(to: ...)
.disposed(by: disposeBag)

我们开头的 Github 搜索就用到了catchErrorJustReturn。当错误产生时,就返回一个空数组,于是就会显示一个空列表页。

你也可以使用 catchError,当错误产生时,将错误事件替换成一个备选序列:

// 先从网络获取数据,如果获取失败了,就从本地缓存获取数据

let rxData: Observable<Data> = ... // 网络请求的数据
let cahcedData: Observable<Data> = ... // 之前本地缓存的数据

rxData
.catchError { _ in cahcedData }
.subscribe(onNext: { date in
print("获取数据成功: \(date.count)")
})
.disposed(by: disposeBag)

Result

如果我们只是想给用户错误提示,那要如何操作呢?

以下提供一个最为直接的方案,不过这个方案存在一些问题:

// 当用户点击更新按钮时,
// 就立即取出修改后的用户信息。
// 然后发起网络请求,进行更新操作,
// 一旦操作失败就提示用户失败原因

updateUserInfoButton.rx.tap
.withLatestFrom(rxUserInfo)
.flatMapLatest { userInfo -> Observable<Void> in
return update(userInfo)
}
.observeOn(MainScheduler.instance)
.subscribe(onNext: {
print("用户信息更新成功")
}, onError: { error in
print("用户信息更新失败: \(error.localizedDescription)")
})
.disposed(by: disposeBag)

这样实现是非常直接的。但是一旦网络请求操作失败了,序列就会终止。整个订阅将被取消。如果用户再次点击更新按钮,就无法再次发起网络请求进行更新操作了。

为了解决这个问题,我们需要选择合适的方案来进行错误处理。例如,使用系统自带的枚举 Result

public enum Result<Success, Failure> where Failure : Error {
case success(Success)
case failure(Failure)
}

然后之前的代码需要修改成:

updateUserInfoButton.rx.tap
.withLatestFrom(rxUserInfo)
.flatMapLatest { userInfo -> Observable<Result<Void, Error>> in
return update(userInfo)
.map(Result.success) // 转换成 Result
.catchError { error in Observable.just(Result.failure(error)) }
}
.observeOn(MainScheduler.instance)
.subscribe(onNext: { result in
switch result { // 处理 Result
case .success:
print("用户信息更新成功")
case .failure(let error):
print("用户信息更新失败: \(error.localizedDescription)")
}
})
.disposed(by: disposeBag)

这样我们的错误事件被包装成了 Result.failure(Error) 元素,就不会终止整个序列。即便网络请求失败了,整个订阅依然存在。如果用户再次点击更新按钮,也是能够发起网络请求进行更新操作的。

另外你也可以使用 materialize 操作符来进行错误处理。这里就不详细介绍了,如你想了解如何使用 materialize 可以参考这篇文章 How to handle errors in RxSwift!

收起阅读 »

iOS RXSwift 4.9

iOS
Disposable - 可被清除的资源通常来说,一个序列如果发出了 error 或者 completed 事件,那么所有内部资源都会被释放。如果你需要提前释放这些资源或取消订阅的话,那么你可以对返回的 可被清...
继续阅读 »

Disposable - 可被清除的资源

通常来说,一个序列如果发出了 error 或者 completed 事件,那么所有内部资源都会被释放。如果你需要提前释放这些资源或取消订阅的话,那么你可以对返回的 可被清除的资源(Disposable) 调用 dispose 方法:

var disposable: Disposable?

override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)

self.disposable = textField.rx.text.orEmpty
.subscribe(onNext: { text in print(text) })
}

override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)

self.disposable?.dispose()
}

调用 dispose 方法后,订阅将被取消,并且内部资源都会被释放。通常情况下,你是不需要手动调用 dispose 方法的,这里只是做个演示而已。我们推荐使用 清除包(DisposeBag) 或者 takeUntil 操作符 来管理订阅的生命周期。

DisposeBag - 清除包

因为我们用的是 Swift ,所以我们更习惯于使用 ARC 来管理内存。那么我们能不能用 ARC 来管理订阅的生命周期了。答案是肯定了,你可以用 清除包(DisposeBag) 来实现这种订阅管理机制:

var disposeBag = DisposeBag()

override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)

textField.rx.text.orEmpty
.subscribe(onNext: { text in print(text) })
.disposed(by: self.disposeBag)
}

override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)

self.disposeBag = DisposeBag()
}

当 清除包 被释放的时候,清除包 内部所有 可被清除的资源(Disposable) 都将被清除。在输入验证中我们也多次看到 清除包 的身影:

var disposeBag = DisposeBag() // 来自父类 ViewController

override func viewDidLoad() {
super.viewDidLoad()

...

usernameValid
.bind(to: passwordOutlet.rx.isEnabled)
.disposed(by: disposeBag)

usernameValid
.bind(to: usernameValidOutlet.rx.isHidden)
.disposed(by: disposeBag)

passwordValid
.bind(to: passwordValidOutlet.rx.isHidden)
.disposed(by: disposeBag)

everythingValid
.bind(to: doSomethingOutlet.rx.isEnabled)
.disposed(by: disposeBag)

doSomethingOutlet.rx.tap
.subscribe(onNext: { [weak self] in self?.showAlert() })
.disposed(by: disposeBag)
}

这个例子中 disposeBag 和 ViewController 具有相同的生命周期。当退出页面时, ViewController 就被释放,disposeBag 也跟着被释放了,那么这里的 5 次绑定(订阅)也就被取消了。这正是我们所需要的。

takeUntil

另外一种实现自动取消订阅的方法就是使用 takeUntil 操作符,上面那个输入验证的演示代码也可以通过使用 takeUntil 来实现:

override func viewDidLoad() {
super.viewDidLoad()

...

_ = usernameValid
.takeUntil(self.rx.deallocated)
.bind(to: passwordOutlet.rx.isEnabled)

_ = usernameValid
.takeUntil(self.rx.deallocated)
.bind(to: usernameValidOutlet.rx.isHidden)

_ = passwordValid
.takeUntil(self.rx.deallocated)
.bind(to: passwordValidOutlet.rx.isHidden)

_ = everythingValid
.takeUntil(self.rx.deallocated)
.bind(to: doSomethingOutlet.rx.isEnabled)

_ = doSomethingOutlet.rx.tap
.takeUntil(self.rx.deallocated)
.subscribe(onNext: { [weak self] in self?.showAlert() })
}

这将使得订阅一直持续到控制器的 dealloc 事件产生为止。

注意⚠️:这里配图中所使用的 Observable 都是“热” Observable,它可以帮助我们理解订阅的生命周期。如果你想要了解 “冷热” Observable 之间的区别,可以参考官方文档 Hot and Cold Observables

收起阅读 »

iOS RXSwift 4.8

iOS
Operator - 操作符操作符可以帮助大家创建新的序列,或者变化组合原有的序列,从而生成一个新的序列。我们之前在输入验证例子中就多次运用到操作符。例如,通过 map 方法将输入的用户名,转换为用户名是否有效。然后用这个转化后来的序列来控...
继续阅读 »

Operator - 操作符

操作符可以帮助大家创建新的序列,或者变化组合原有的序列,从而生成一个新的序列。

我们之前在输入验证例子中就多次运用到操作符。例如,通过 map 方法将输入的用户名,转换为用户名是否有效。然后用这个转化后来的序列来控制红色提示语是否隐藏。我们还通过 combineLatest 方法,将用户名是否有效密码是否有效合并成两者是否同时有效。然后用这个合成后来的序列来控制按钮是否可点击。

这里 map 和 combineLatest 都是操作符,它们可以帮助我们构建所需要的序列。现在,我们再来看几个例子:

filter - 过滤

你可以用 filter 创建一个新的序列。这个序列只发出温度大于 33 度的元素。

map - 转换

你可以用 map 创建一个新的序列。这个序列将原有的 JSON 转换成 Model 。这种转换实际上就是解析 JSON 。

zip - 配对

你可以用 zip 来合成一个新的序列。这个序列将汉堡序列的元素和薯条序列的元素配对后,生成一个新的套餐序列。

如何使用操作符

使用操作符是非常容易的。你可以直接调用实例方法,或者静态方法:

  • 温度过滤

    // 温度
    let rxTemperature: Observable<Double> = ...

    // filter 操作符
    rxTemperature.filter { temperature in temperature > 33 }
    .subscribe(onNext: { temperature in
    print("高温:\(temperature)度")
    })
    .disposed(by: disposeBag)
  • 解析 JSON

    // JSON
    let json: Observable<JSON> = ...

    // map 操作符
    json.map(Model.init)
    .subscribe(onNext: { model in
    print("取得 Model: \(model)")
    })
    .disposed(by: disposeBag)
  • 合成套餐

    // 汉堡
    let rxHamburg: Observable<Hamburg> = ...
    // 薯条
    let rxFrenchFries: Observable<FrenchFries> = ...

    // zip 操作符
    Observable.zip(rxHamburg, rxFrenchFries)
    .subscribe(onNext: { (hamburg, frenchFries) in
    print("取得汉堡: \(hamburg) 和薯条:\(frenchFries)")
    })
    .disposed(by: disposeBag)

决策树

Rx 提供了充分的操作符来帮我们创建序列。当然如果内置操作符无法满足你的需求时,你还可以创建自定义的操作符。

如果你不确定该如何选择操作符,可以参考 决策树。它会引导你找出合适的操作符。

操作符列表

26个英文字母我都认识,可是连成一个句子我就不怎么认得了...

这里提供一个操作符列表,它们就好比是26个英文字母。你如果要将它们的作用全部都发挥出来,是需要学习如何将它们连成一个句子的:

收起阅读 »

iOS RXSwift 4.7

iOS
ReplaySubjectReplaySubject 将对观察者发送全部的元素,无论观察者是何时进行订阅的。这里存在多个版本的 ReplaySubject,有的只会将最新的 n 个元素发送给观察者,有的只会将限制时间段内最新的元素发送给观察...
继续阅读 »

ReplaySubject

ReplaySubject 将对观察者发送全部的元素,无论观察者是何时进行订阅的。

这里存在多个版本的 ReplaySubject,有的只会将最新的 n 个元素发送给观察者,有的只会将限制时间段内最新的元素发送给观察者。

如果把 ReplaySubject 当作观察者来使用,注意不要在多个线程调用 onNextonError 或 onCompleted。这样会导致无序调用,将造成意想不到的结果。


演示

let disposeBag = DisposeBag()
let subject = ReplaySubject<String>.create(bufferSize: 1)

subject
.subscribe { print("Subscription: 1 Event:", $0) }
.disposed(by: disposeBag)

subject.onNext("🐶")
subject.onNext("🐱")

subject
.subscribe { print("Subscription: 2 Event:", $0) }
.disposed(by: disposeBag)

subject.onNext("🅰️")
subject.onNext("🅱️")

输出结果:

Subscription: 1 Event: next(🐶)
Subscription: 1 Event: next(🐱)
Subscription: 2 Event: next(🐱)
Subscription: 1 Event: next(🅰️)
Subscription: 2 Event: next(🅰️)
Subscription: 1 Event: next(🅱️)
Subscription: 2 Event: next(🅱️)

BehaviorSubject

当观察者对 BehaviorSubject 进行订阅时,它会将源 Observable 中最新的元素发送出来(如果不存在最新的元素,就发出默认元素)。然后将随后产生的元素发送出来。

如果源 Observable 因为产生了一个 error 事件而中止, BehaviorSubject 就不会发出任何元素,而是将这个 error 事件发送出来。


演示

let disposeBag = DisposeBag()
let subject = BehaviorSubject(value: "🔴")

subject
.subscribe { print("Subscription: 1 Event:", $0) }
.disposed(by: disposeBag)

subject.onNext("🐶")
subject.onNext("🐱")

subject
.subscribe { print("Subscription: 2 Event:", $0) }
.disposed(by: disposeBag)

subject.onNext("🅰️")
subject.onNext("🅱️")

subject
.subscribe { print("Subscription: 3 Event:", $0) }
.disposed(by: disposeBag)

subject.onNext("🍐")
subject.onNext("🍊")

输出结果:

Subscription: 1 Event: next(🔴)
Subscription: 1 Event: next(🐶)
Subscription: 1 Event: next(🐱)
Subscription: 2 Event: next(🐱)
Subscription: 1 Event: next(🅰️)
Subscription: 2 Event: next(🅰️)
Subscription: 1 Event: next(🅱️)
Subscription: 2 Event: next(🅱️)
Subscription: 3 Event: next(🅱️)
Subscription: 1 Event: next(🍐)
Subscription: 2 Event: next(🍐)
Subscription: 3 Event: next(🍐)
Subscription: 1 Event: next(🍊)
Subscription: 2 Event: next(🍊)
Subscription: 3 Event: next(🍊)

Variable (已弃用)

Variable 是早期添加到 RxSwift 的概念,通过 “setting” 和 “getting”, 他可以帮助我们从原先命令式的思维方式,过渡到响应式的思维方式

但这只是我们一厢情愿的想法。许多开发者滥用 Variable,来构建 重度命令式 系统,而不是 Rx 的 声明式 系统。这对于新手很常见,并且他们无法意识到,这是代码的坏味道。所以在 RxSwift 4.x 中 Variable 被轻度弃用,仅仅给出一个运行时警告。

在 RxSwift 5.x 中,他被官方的正式的弃用了,并且在需要时,推荐使用 BehaviorRelay 或者 BehaviorSubject


ControlProperty

ControlProperty 专门用于描述 UI 控件属性的,它具有以下特征:

  • 不会产生 error 事件
  • 一定在 MainScheduler 订阅(主线程订阅)
  • 一定在 MainScheduler 监听(主线程监听)
  • 共享附加作用
收起阅读 »

iOS RXSwift 4.6

iOS
Observable & Observer 既是可监听序列也是观察者在我们所遇到的事物中,有一部分非常特别。它们既是可监听序列也是观察者。例如:textField的当前文本。它可以看成是由用户输入,而产生的一个文本序列。也可以是由外部文本序列,来控制当...
继续阅读 »

Observable & Observer 既是可监听序列也是观察者

在我们所遇到的事物中,有一部分非常特别。它们既是可监听序列也是观察者

例如:textField的当前文本。它可以看成是由用户输入,而产生的一个文本序列。也可以是由外部文本序列,来控制当前显示内容的观察者

// 作为可监听序列
let observable = textField.rx.text
observable.subscribe(onNext: { text in show(text: text) })
// 作为观察者
let observer = textField.rx.text
let text: Observable<String?> = ...
text.bind(to: observer)

有许多 UI 控件都存在这种特性,例如:switch的开关状态,segmentedControl的选中索引号,datePicker的选中日期等等。

参考

另外,框架里面定义了一些辅助类型,它们既是可监听序列也是观察者。如果你能合适的应用这些辅助类型,它们就可以帮助你更准确的描述事物的特征:


AsyncSubject

AsyncSubject 将在源 Observable 产生完成事件后,发出最后一个元素(仅仅只有最后一个元素),如果源 Observable 没有发出任何元素,只有一个完成事件。那 AsyncSubject 也只有一个完成事件。

它会对随后的观察者发出最终元素。如果源 Observable 因为产生了一个 error 事件而中止, AsyncSubject 就不会发出任何元素,而是将这个 error 事件发送出来。


演示

let disposeBag = DisposeBag()
let subject = AsyncSubject<String>()

subject
.subscribe { print("Subscription: 1 Event:", $0) }
.disposed(by: disposeBag)

subject.onNext("🐶")
subject.onNext("🐱")
subject.onNext("🐹")
subject.onCompleted()

输出结果:

Subscription: 1 Event: next(🐹)
Subscription: 1 Event: completed

PublishSubject

PublishSubject 将对观察者发送订阅后产生的元素,而在订阅前发出的元素将不会发送给观察者。如果你希望观察者接收到所有的元素,你可以通过使用 Observable 的 create 方法来创建 Observable,或者使用 ReplaySubject

如果源 Observable 因为产生了一个 error 事件而中止, PublishSubject 就不会发出任何元素,而是将这个 error 事件发送出来。


演示

let disposeBag = DisposeBag()
let subject = PublishSubject<String>()

subject
.subscribe { print("Subscription: 1 Event:", $0) }
.disposed(by: disposeBag)

subject.onNext("🐶")
subject.onNext("🐱")

subject
.subscribe { print("Subscription: 2 Event:", $0) }
.disposed(by: disposeBag)

subject.onNext("🅰️")
subject.onNext("🅱️")

输出结果:

Subscription: 1 Event: next(🐶)
Subscription: 1 Event: next(🐱)
Subscription: 1 Event: next(🅰️)
Subscription: 2 Event: next(🅰️)
Subscription: 1 Event: next(🅱️)
Subscription: 2 Event: next(🅱️)
收起阅读 »

iOS RXSwift 4.5

iOS
Observer - 观察者观察者 是用来监听事件,然后它需要这个事件做出响应。例如:弹出提示框就是观察者,它对点击按钮这个事件做出响应。响应事件的都是观察者在 Observable 章节,我们举了个几个例子来介绍什么是可监听序列...
继续阅读 »

Observer - 观察者

观察者 是用来监听事件,然后它需要这个事件做出响应。例如:弹出提示框就是观察者,它对点击按钮这个事件做出响应。

响应事件的都是观察者

在 Observable 章节,我们举了个几个例子来介绍什么是可监听序列。那么我们还是用这几个例子来解释一下什么是观察者

  • 当室温高于 33 度时,打开空调降温

    1

    打开空调降温就是观察者 Observer<Double>

  • 当《海贼王》更新一集时,我们就立即观看这一集

    1

    观看这一集就是观察者 Observer<OnePieceEpisode>

  • 当取到 JSON 时,将它打印出来

    1

    将它打印出来就是观察者 Observer<JSON>

  • 当任务结束后,提示用户任务已完成

    1

    提示用户任务已完成就是观察者 Observer<Void>

如何创建观察者

现在我们已经知道观察者主要是做什么的了。那么我们要怎么创建它们呢?

和 Observable 一样,框架已经帮我们创建好了许多常用的观察者。例如:view 是否隐藏,button 是否可点击, label 的当前文本,imageView 的当前图片等等。

另外,有一些自定义的观察者是需要我们自己创建的。这里介绍一下创建观察者最基本的方法,例如,我们创建一个弹出提示框的的观察者

tap.subscribe(onNext: { [weak self] in
self?.showAlert()
}, onError: { error in
print("发生错误: \(error.localizedDescription)")
}, onCompleted: {
print("任务完成")
})

创建观察者最直接的方法就是在 Observable 的 subscribe 方法后面描述,事件发生时,需要如何做出响应。而观察者就是由后面的 onNextonErroronCompleted的这些闭包构建出来的。

以上是创建观察者最常见的方法。当然你还可以通过其他的方式来创建观察者,可以参考一下 AnyObserver 和 Binder

特征观察者

和 Observable 一样,观察者也存特征观察者,例如:


AnyObserver

AnyObserver 可以用来描叙任意一种观察者。

例如:


打印网络请求结果:

URLSession.shared.rx.data(request: URLRequest(url: url))
.subscribe(onNext: { data in
print("Data Task Success with count: \(data.count)")
}, onError: { error in
print("Data Task Error: \(error)")
})
.disposed(by: disposeBag)

可以看作是:

let observer: AnyObserver<Data> = AnyObserver { (event) in
switch event {
case .next(let data):
print("Data Task Success with count: \(data.count)")
case .error(let error):
print("Data Task Error: \(error)")
default:
break
}
}

URLSession.shared.rx.data(request: URLRequest(url: url))
.subscribe(observer)
.disposed(by: disposeBag)

用户名提示语是否隐藏:

usernameValid
.bind(to: usernameValidOutlet.rx.isHidden)
.disposed(by: disposeBag)

可以看作是:

let observer: AnyObserver<Bool> = AnyObserver { [weak self] (event) in
switch event {
case .next(let isHidden):
self?.usernameValidOutlet.isHidden = isHidden
default:
break
}
}

usernameValid
.bind(to: observer)
.disposed(by: disposeBag)

下一节将介绍 Binder 以及 usernameValidOutlet.rx.isHidden 的由来。


Binder

Binder 主要有以下两个特征:

  • 不会处理错误事件
  • 确保绑定都是在给定 Scheduler 上执行(默认 MainScheduler

一旦产生错误事件,在调试环境下将执行 fatalError,在发布环境下将打印错误信息。


示例

在介绍 AnyObserver 时,我们举了这样一个例子:

let observer: AnyObserver<Bool> = AnyObserver { [weak self] (event) in
switch event {
case .next(let isHidden):
self?.usernameValidOutlet.isHidden = isHidden
default:
break
}
}

usernameValid
.bind(to: observer)
.disposed(by: disposeBag)

由于这个观察者是一个 UI 观察者,所以它在响应事件时,只会处理 next 事件,并且更新 UI 的操作需要在主线程上执行。

因此一个更好的方案就是使用 Binder

let observer: Binder<Bool> = Binder(usernameValidOutlet) { (view, isHidden) in
view.isHidden = isHidden
}

usernameValid
.bind(to: observer)
.disposed(by: disposeBag)

Binder 可以只处理 next 事件,并且保证响应 next 事件的代码一定会在给定 Scheduler 上执行,这里采用默认的 MainScheduler


复用

由于页面是否隐藏是一个常用的观察者,所以应该让所有的 UIView 都提供这种观察者:

extension Reactive where Base: UIView {
public var isHidden: Binder<Bool> {
return Binder(self.base) { view, hidden in
view.isHidden = hidden
}
}
}
usernameValid
.bind(to: usernameValidOutlet.rx.isHidden)
.disposed(by: disposeBag)

这样你不必为每个 UI 控件单独创建该观察者。这就是 usernameValidOutlet.rx.isHidden 的由来,许多 UI 观察者 都是这样创建的:

  • 按钮是否可点击 button.rx.isEnabled

    extension Reactive where Base: UIControl {
    public var isEnabled: Binder<Bool> {
    return Binder(self.base) { control, value in
    control.isEnabled = value
    }
    }
    }
  • label 的当前文本 label.rx.text

    extension Reactive where Base: UILabel {
    public var text: Binder<String?> {
    return Binder(self.base) { label, text in
    label.text = text
    }
    }
    }

你也可以用这种方式来创建自定义的 UI 观察者

收起阅读 »

iOS RXSwift 4.4

iOS
SignalSignal 和 Driver 相似,唯一的区别是,Driver 会对新观察者回放(重新发送)上一个元素,而 Signal 不会对新观察者回放上一个元素。他有如下特性:不会产生 ...
继续阅读 »

Signal

Signal 和 Driver 相似,唯一的区别是,Driver 对新观察者回放(重新发送)上一个元素,而 Signal 不会对新观察者回放上一个元素。

他有如下特性:

  • 不会产生 error 事件
  • 一定在 MainScheduler 监听(主线程监听)
  • 共享附加作用

现在,我们来看看以下代码是否合理:

let textField: UITextField = ...
let nameLabel: UILabel = ...
let nameSizeLabel: UILabel = ...

let state: Driver<String?> = textField.rx.text.asDriver()

let observer = nameLabel.rx.text
state.drive(observer)

// ... 假设以下代码是在用户输入姓名后运行

let newObserver = nameSizeLabel.rx.text
state.map { $0?.count.description }.drive(newObserver)

这个例子只是将用户输入的姓名绑定到对应的标签上。当用户输入姓名后,我们创建了一个新的观察者,用于订阅姓名的字数。那么问题来了,订阅时,展示字数的标签会立即更新吗?

嗯、、、 因为 Driver 会对新观察者回放上一个元素(当前姓名),所以这里是会更新的。在对他进行订阅时,标签的默认文本会被刷新。这是合理的。

那如果我们用 Driver 来描述点击事件呢,这样合理吗?

let button: UIButton = ...
let showAlert: (String) -> Void = ...

let event: Driver<Void> = button.rx.tap.asDriver()

let observer: () -> Void = { showAlert("弹出提示框1") }
event.drive(onNext: observer)

// ... 假设以下代码是在用户点击 button 后运行

let newObserver: () -> Void = { showAlert("弹出提示框2") }
event.drive(onNext: newObserver)

当用户点击一个按钮后,我们创建一个新的观察者,来响应点击事件。此时会发生什么?Driver 会把上一次的点击事件回放给新观察者。所以,这里的 newObserver 在订阅时,就会接受到上次的点击事件,然后弹出提示框。这似乎不太合理。

因此像这类型的事件序列,用 Driver 建模就不合适。于是我们就引入了 Signal:

...

let event: Signal<Void> = button.rx.tap.asSignal()

let observer: () -> Void = { showAlert("弹出提示框1") }
event.emit(onNext: observer)

// ... 假设以下代码是在用户点击 button 后运行

let newObserver: () -> Void = { showAlert("弹出提示框2") }
event.emit(onNext: newObserver)

在同样的场景中,Signal 不会把上一次的点击事件回放给新观察者,而只会将订阅后产生的点击事件,发布给新观察者。这正是我们所需要的。

结论

一般情况下状态序列我们会选用 Driver 这个类型,事件序列我们会选用 Signal 这个类型。

参考


ControlEvent

ControlEvent 专门用于描述 UI 控件所产生的事件,它具有以下特征:

  • 不会产生 error 事件
  • 一定在 MainScheduler 订阅(主线程订阅)
  • 一定在 MainScheduler 监听(主线程监听)
  • 共享附加作用
收起阅读 »

iOS RXSwift 4.3

iOS
MaybeMaybe 是 Observable 的另外一个版本。它介于 Single 和 Completable 之间,它要么只能发出一个元素,要么产生一个 completed&n...
继续阅读 »

Maybe

Maybe 是 Observable 的另外一个版本。它介于 Single 和 Completable 之间,它要么只能发出一个元素,要么产生一个 completed 事件,要么产生一个 error 事件。

  • 发出一个元素或者一个 completed 事件或者一个 error 事件
  • 不会共享附加作用

如果你遇到那种可能需要发出一个元素,又可能不需要发出时,就可以使用 Maybe

如何创建 Maybe

创建 Maybe 和创建 Observable 非常相似:

func generateString() -> Maybe<String> {
return Maybe<String>.create { maybe in
maybe(.success("RxSwift"))

// OR

maybe(.completed)

// OR

maybe(.error(error))

return Disposables.create {}
}
}

之后,你可以这样使用 Maybe

generateString()
.subscribe(onSuccess: { element in
print("Completed with element \(element)")
}, onError: { error in
print("Completed with an error \(error.localizedDescription)")
}, onCompleted: {
print("Completed with no element")
})
.disposed(by: disposeBag)

你同样可以对 Observable 调用 .asMaybe() 方法,将它转换为 Maybe

Driver

Driver(司机?) 是一个精心准备的特征序列。它主要是为了简化 UI 层的代码。不过如果你遇到的序列具有以下特征,你也可以使用它:

  • 不会产生 error 事件
  • 一定在 MainScheduler 监听(主线程监听)
  • 共享附加作用

这些都是驱动 UI 的序列所具有的特征。

为什么要使用 Driver ?

我们举个例子来说明一下,为什么要使用 Driver

这是文档简介页的例子:

let results = query.rx.text
.throttle(0.3, scheduler: MainScheduler.instance)
.flatMapLatest { query in
fetchAutoCompleteItems(query)
}

results
.map { "\($0.count)" }
.bind(to: resultCount.rx.text)
.disposed(by: disposeBag)

results
.bind(to: resultsTableView.rx.items(cellIdentifier: "Cell")) {
(_, result, cell) in
cell.textLabel?.text = "\(result)"
}
.disposed(by: disposeBag)

这段代码的主要目的是:

  • 取出用户输入稳定后的内容
  • 向服务器请求一组结果
  • 将返回的结果绑定到两个 UI 元素上:tableView 和 显示结果数量的label

那么这里存在什么问题?

  • 如果 fetchAutoCompleteItems 的序列产生了一个错误(网络请求失败),这个错误将取消所有绑定,当用户输入一个新的关键字时,是无法发起新的网络请求。
  • 如果 fetchAutoCompleteItems 在后台返回序列,那么刷新页面也会在后台进行,这样就会出现异常崩溃。
  • 返回的结果被绑定到两个 UI 元素上。那就意味着,每次用户输入一个新的关键字时,就会分别为两个 UI 元素发起 HTTP 请求,这并不是我们想要的结果。

一个更好的方案是这样的:

let results = query.rx.text
.throttle(0.3, scheduler: MainScheduler.instance)
.flatMapLatest { query in
fetchAutoCompleteItems(query)
.observeOn(MainScheduler.instance) // 结果在主线程返回
.catchErrorJustReturn([]) // 错误被处理了,这样至少不会终止整个序列
}
.share(replay: 1) // HTTP 请求是被共享的

results
.map { "\($0.count)" }
.bind(to: resultCount.rx.text)
.disposed(by: disposeBag)

results
.bind(to: resultsTableView.rx.items(cellIdentifier: "Cell")) {
(_, result, cell) in
cell.textLabel?.text = "\(result)"
}
.disposed(by: disposeBag)

在一个大型系统内,要确保每一步不被遗漏是一件不太容易的事情。所以更好的选择是合理运用编译器和特征序列来确保这些必备条件都已经满足。

以下是使用 Driver 优化后的代码:

let results = query.rx.text.asDriver()        // 将普通序列转换为 Driver
.throttle(0.3, scheduler: MainScheduler.instance)
.flatMapLatest { query in
fetchAutoCompleteItems(query)
.asDriver(onErrorJustReturn: []) // 仅仅提供发生错误时的备选返回值
}

results
.map { "\($0.count)" }
.drive(resultCount.rx.text) // 这里改用 `drive` 而不是 `bindTo`
.disposed(by: disposeBag) // 这样可以确保必备条件都已经满足了

results
.drive(resultsTableView.rx.items(cellIdentifier: "Cell")) {
(_, result, cell) in
cell.textLabel?.text = "\(result)"
}
.disposed(by: disposeBag)

首先第一个 asDriver 方法将 ControlProperty 转换为 Driver

然后第二个变化是:

.asDriver(onErrorJustReturn: [])

任何可监听序列都可以被转换为 Driver,只要他满足 3 个条件:

  • 不会产生 error 事件
  • 一定在 MainScheduler 监听(主线程监听)
  • 共享附加作用

那么要如何确定条件都被满足?通过 Rx 操作符来进行转换。asDriver(onErrorJustReturn: []) 相当于以下代码:

let safeSequence = xs
.observeOn(MainScheduler.instance) // 主线程监听
.catchErrorJustReturn(onErrorJustReturn) // 无法产生错误
.share(replay: 1, scope: .whileConnected)// 共享附加作用
return Driver(raw: safeSequence) // 封装

最后使用 drive 而不是 bindTo

drive 方法只能被 Driver 调用。这意味着,如果你发现代码所存在 drive,那么这个序列不会产生错误事件并且一定在主线程监听。这样你可以安全的绑定 UI 元素。

收起阅读 »

iOS RXSwift 4.2

iOS
SingleSingle 是 Observable 的另外一个版本。不像 Observable 可以发出多个元素,它要么只能发出一个元素,要么产生一个 error 事件。发出一个元素,或一个...
继续阅读 »

Single

Single 是 Observable 的另外一个版本。不像 Observable 可以发出多个元素,它要么只能发出一个元素,要么产生一个 error 事件。

一个比较常见的例子就是执行 HTTP 请求,然后返回一个应答错误。不过你也可以用 Single 来描述任何只有一个元素的序列。

如何创建 Single

创建 Single 和创建 Observable 非常相似:

func getRepo(_ repo: String) -> Single<[String: Any]> {

return Single<[String: Any]>.create { single in
let url = URL(string: "https://api.github.com/repos/\(repo)")!
let task = URLSession.shared.dataTask(with: url) {
data, _, error in

if let error = error {
single(.error(error))
return
}

guard let data = data,
let json = try? JSONSerialization.jsonObject(with: data, options: .mutableLeaves),
let result = json as? [String: Any] else {
single(.error(DataError.cantParseJSON))
return
}

single(.success(result))
}

task.resume()

return Disposables.create { task.cancel() }
}
}

之后,你可以这样使用 Single

getRepo("ReactiveX/RxSwift")
.subscribe(onSuccess: { json in
print("JSON: ", json)
}, onError: { error in
print("Error: ", error)
})
.disposed(by: disposeBag)

订阅提供一个 SingleEvent 的枚举:

public enum SingleEvent<Element> {
case success(Element)
case error(Swift.Error)
}
  • success - 产生一个单独的元素
  • error - 产生一个错误

你同样可以对 Observable 调用 .asSingle() 方法,将它转换为 Single

Completable

Completable 是 Observable 的另外一个版本。不像 Observable 可以发出多个元素,它要么只能产生一个 completed 事件,要么产生一个 error 事件。

  • 发出零个元素
  • 发出一个 completed 事件或者一个 error 事件
  • 不会共享附加作用

Completable 适用于那种你只关心任务是否完成,而不需要在意任务返回值的情况。它和 Observable<Void> 有点相似。

如何创建 Completable

创建 Completable 和创建 Observable 非常相似:

func cacheLocally() -> Completable {
return Completable.create { completable in
// Store some data locally
...
...

guard success else {
completable(.error(CacheError.failedCaching))
return Disposables.create {}
}

completable(.completed)
return Disposables.create {}
}
}

之后,你可以这样使用 Completable

cacheLocally()
.subscribe(onCompleted: {
print("Completed with no error")
}, onError: { error in
print("Completed with an error: \(error.localizedDescription)")
})
.disposed(by: disposeBag)

订阅提供一个 CompletableEvent 的枚举:

public enum CompletableEvent {
case error(Swift.Error)
case completed
}
  • completed - 产生完成事件
  • error - 产生一个错误
收起阅读 »

iOS RXSwift 4.1

iOS
Observable - 可监听序列所有的事物都是序列之前我们提到,Observable 可以用于描述元素异步产生的序列。这样我们生活中许多事物都可以通过它来表示,例如:Observable<Double> 温度你可以将温度看作...
继续阅读 »

Observable - 可监听序列

1

所有的事物都是序列

之前我们提到,Observable 可以用于描述元素异步产生的序列。这样我们生活中许多事物都可以通过它来表示,例如:

  • Observable<Double> 温度

    你可以将温度看作是一个序列,然后监测这个温度值,最后对这个值做出响应。例如:当室温高于 33 度时,打开空调降温。

    1

  • Observable<OnePieceEpisode> 《海贼王》动漫

    你也可以把《海贼王》的动漫看作是一个序列。然后当《海贼王》更新一集时,我们就立即观看这一集。

    1

  • Observable<JSON> JSON

    你可以把网络请求的返回的 JSON 看作是一个序列。然后当取到 JSON 时,将它打印出来。

    1

  • Observable<Void> 任务回调

    你可以把任务回调看作是一个序列。当任务结束后,提示用户任务已完成。

    1

如何创建序列

现在我们已经可以把生活中的许多事物看作是一个序列了。那么我们要怎么创建这些序列呢?

实际上,框架已经帮我们创建好了许多常用的序列。例如:button的点击,textField的当前文本,switch的开关状态,slider的当前数值等等。

另外,有一些自定义的序列是需要我们自己创建的。这里介绍一下创建序列最基本的方法,例如,我们创建一个 [0, 1, ... 8, 9] 的序列:

1

let numbers: Observable<Int> = Observable.create { observer -> Disposable in

observer.onNext(0)
observer.onNext(1)
observer.onNext(2)
observer.onNext(3)
observer.onNext(4)
observer.onNext(5)
observer.onNext(6)
observer.onNext(7)
observer.onNext(8)
observer.onNext(9)
observer.onCompleted()

return Disposables.create()
}

创建序列最直接的方法就是调用 Observable.create,然后在构建函数里面描述元素的产生过程。 observer.onNext(0) 就代表产生了一个元素,他的值是 0。后面又产生了 9 个元素分别是 1, 2, ... 8, 9 。最后,用 observer.onCompleted() 表示元素已经全部产生,没有更多元素了。

你可以用这种方式来封装功能组件,例如,闭包回调:

1

typealias JSON = Any

let json: Observable<JSON> = Observable.create { (observer) -> Disposable in

let task = URLSession.shared.dataTask(with: ...) { data, _, error in

guard error == nil else {
observer.onError(error!)
return
}

guard let data = data,
let jsonObject = try? JSONSerialization.jsonObject(with: data, options: .mutableLeaves)
else {
observer.onError(DataError.cantParseJSON)
return
}

observer.onNext(jsonObject)
observer.onCompleted()
}

task.resume()

return Disposables.create { task.cancel() }
}

在闭包回调中,如果任务失败,就调用 observer.onError(error!)。如果获取到目标元素,就调用 observer.onNext(jsonObject)。由于我们的这个序列只有一个元素,所以在成功获取到元素后,就直接调用 observer.onCompleted() 来表示任务结束。最后 Disposables.create { task.cancel() } 说明如果数据绑定被清除(订阅被取消)的话,就取消网络请求。

这样一来我们就将传统的闭包回调转换成序列了。然后可以用 subscribe 方法来响应这个请求的结果:

json
.subscribe(onNext: { json in
print("取得 json 成功: \(json)")
}, onError: { error in
print("取得 json 失败 Error: \(error.localizedDescription)")
}, onCompleted: {
print("取得 json 任务成功完成")
})
.disposed(by: disposeBag)

这里subscribe后面的onNext,onErroronCompleted 分别响应我们创建 json 时,构建函数里面的onNext,onErroronCompleted 事件。我们称这些事件为 Event:

Event - 事件

public enum Event<Element> {
case next(Element)
case error(Swift.Error)
case completed
}
  • next - 序列产生了一个新的元素
  • error - 创建序列时产生了一个错误,导致序列终止
  • completed - 序列的所有元素都已经成功产生,整个序列已经完成

你可以合理的利用这些 Event 来实现业务逻辑。

决策树

现在我们知道如何用最基本的方法创建序列。你还可参考 决策树 来选择其他的方式创建序列。

特征序列

我们都知道 Swift 是一个强类型语言,而强类型语言相对于弱类型语言的一个优点是更加严谨。我们可以通过类型来判断出,实例有哪些特征。同样的在 RxSwift 里面 Observable 也存在一些特征序列,这些特征序列可以帮助我们更准确的描述序列。并且它们还可以给我们提供语法糖,让我们能够用更加优雅的方式书写代码,他们分别是:

ℹ️ 提示:由于可被观察的序列(Observable)名字过长,很多时候会增加阅读难度,所以笔者在必要时会将它简写为:序列

收起阅读 »

iOS RXSwift 4

iOS
数据绑定(订阅)在 RxSwift 里有一个比较重要的概念就是数据绑定(订阅)。就是指将可监听序列绑定到观察者上:我们对比一下这两段代码:let image: UIImage = UIImage(named: ...) imageView....
继续阅读 »

数据绑定(订阅)

在 RxSwift 里有一个比较重要的概念就是数据绑定(订阅)。就是指将可监听序列绑定到观察者上:

我们对比一下这两段代码:

let image: UIImage = UIImage(named: ...)
imageView.image = image
let image: Observable<UIImage> = ...
image.bind(to: imageView.rx.image)

第一段代码我们非常熟悉,它就是将一个单独的图片设置到imageView上。

第二段代码则是将一个图片序列 “同步” 到imageView上。这个序列里面的图片可以是异步产生的。这里定义的 image 就是上图中蓝色部分(可监听序列),imageView.rx.image就是上图中橙色部分(观察者)。而这种 “同步机制” 就是数据绑定(订阅)

RxSwift 核心

这一章主要介绍 RxSwift 的核心内容:

// Observable<String>
let text = usernameOutlet.rx.text.orEmpty.asObservable()

// Observable<Bool>
let passwordValid = text
// Operator
.map { $0.characters.count >= minimalUsernameLength }

// Observer<Bool>
let observer = passwordValidOutlet.rx.isHidden

// Disposable
let disposable = passwordValid
// Scheduler 用于控制任务在那个线程队列运行
.subscribeOn(MainScheduler.instance)
.observeOn(MainScheduler.instance)
.bind(to: observer)


...

// 取消绑定,你可以在退出页面时取消绑定
disposable.dispose()

下面几节会详细介绍这几个组件的功能和用法。

ℹ️ 提示:这一章主要介绍一些偏理论方面的知识。你如果觉得阅读起来比较乏味的话,可以先快速地浏览一遍,了解 RxSwift 的核心组件大概有哪些内容。待以后遇到实际问题时,在回来查询。你可以直接跳到 更多例子 章节,去了解如何应用 RxSwift


收起阅读 »

iOS RXSwift 3.2

iOS
函数式编程 -> 函数响应式编程现在大家已经了解我们是如何运用函数式编程来操作序列的。其实我们可以把这种操作序列的方式再升华一下。例如,你可以把一个按钮的点击事件看作是一个序列:// 假设用户在进入页面到离开页面期间,总共点击按钮 3 次 // 按钮点...
继续阅读 »

函数式编程 -> 函数响应式编程

现在大家已经了解我们是如何运用函数式编程来操作序列的。其实我们可以把这种操作序列的方式再升华一下。例如,你可以把一个按钮的点击事件看作是一个序列:

// 假设用户在进入页面到离开页面期间,总共点击按钮 3 次

// 按钮点击序列
let taps: Array<Void> = [(), (), ()]

// 每次点击后弹出提示框
taps.forEach { showAlert() }

这样处理点击事件是非常理想的,但是问题是这个序列里面的元素(点击事件)是异步产生的,传统序列是无法描叙这种元素异步产生的情况。为了解决这个问题,于是就产生了可监听序列Observable<Element>。它也是一个序列,只不过这个序列里面的元素可以是同步产生的,也可以是异步产生的:

// 按钮点击序列
let taps: Observable<Void> = button.rx.tap.asObservable()

// 每次点击后弹出提示框
taps.subscribe(onNext: { showAlert() })

这里 taps 就是按钮点击事件的序列。然后我们通过弹出提示框,来对每一次点击事件做出响应。这种编程方式叫做响应式编程。我们结合函数式编程以及响应式编程就得到了函数响应式编程

passwordOutlet.rx.text.orEmpty
.map { $0.characters.count >= minimalPasswordLength }
.bind(to: passwordValidOutlet.rx.isHidden)
.disposed(by: disposeBag)

我们通过不同的构建函数,来创建所需要的数据序列。最后通过适当的方式来响应这个序列。这就是函数响应式编程

收起阅读 »

iOS RXSwift 3.1

iOS
函数响应式编程函数响应式编程是种编程范式。它是通过构建函数操作数据序列,然后对这些序列做出响应的编程方式。它结合了函数式编程以及响应式编程这里先介绍一下函数式编程。函数式编程函数式编程是种编程范式,它需要我们将函数作为参数传递,或者作为返回值返还。我们可以通过...
继续阅读 »

函数响应式编程

函数响应式编程是种编程范式。它是通过构建函数操作数据序列,然后对这些序列做出响应的编程方式。它结合了函数式编程以及响应式编程

这里先介绍一下函数式编程


函数式编程

函数式编程是种编程范式,它需要我们将函数作为参数传递,或者作为返回值返还。我们可以通过组合不同的函数来得到想要的结果。

我们来看一下这几个例子:

// 全校学生
let allStudents: [Student] = getSchoolStudents()

// 三年二班的学生
let gradeThreeClassTwoStudents: [Student] = allStudents
.filter { student in student.grade == 3 && student.class == 2 }

由于我们想要得到三年二班的学生,所以我们把三年二班的判定函数作为参数传递给 filter 方法,这样就能从全校学生中过滤出三年二班的学生。

// 三年二班的每一个男同学唱一首《一剪梅》
gradeThreeClassTwoStudents
.filter { student in student.sex == .male }
.forEach { boy in boy.singASong(name: "一剪梅") }

同样的我们将性别的判断函数传递给 filter 方法,这样就能从三年二班的学生中过滤出男同学,然后将唱歌作为函数传递给 forEach 方法。于是每一个男同学都要唱《一剪梅》😄。

// 三年二班学生成绩高于90分的家长上台领奖
gradeThreeClassTwoStudents
.filter { student in student.score > 90 }
.map { student in student.parent }
.forEach { parent in parent.receiveAPrize() }

用分数判定来筛选出90分以上的同学,然后用map转换为学生家长,最后用forEach让每个家长上台领奖。

// 由高到低打印三年二班的学生成绩
gradeThreeClassTwoStudents
.sorted { student0, student1 in student0.score > student1.score }
.forEach { student in print("score: \(student.score), name: \(student.name)") }

将排序逻辑的函数传递给 sorted方法,这样学生就按成绩高低排序,最后用forEach将成绩和学生名字打印出来。

整体结构

值得注意的是,我们先从三年二班筛选出男同学,后来又从三年二班筛选出分数高于90的学生。都是用的 filter 方法,只是传递了不同的判定函数,从而得出了不同的筛选结果。如果现在要实现这个需求:二年一班分数不足60的学生唱一首《我有罪》。

相信大家要不了多久就可以找到对应的实现方法。

这就是函数式编程,它使我们可以通过组合不同的方法,以及不同的函数来获取目标结果。你可以想象如果我们用传统的 for 循环来完成相同的逻辑,那将会是一件多么繁琐的事情。所以函数试编程的优点是显而易见的:

  • 灵活
  • 高复用
  • 简洁
  • 易维护
  • 适应各种需求变化

如果想了解更多有关于函数式编程的知识。可以参考这本书籍 《函数式 Swift》

收起阅读 »

iOS RXSwift 二

iOS
你好 RxSwift!我的第一个 RxSwift 应用程序 - 输入验证:这是一个模拟用户登录的程序。当用户输入用户名时,如果用户名不足 5 个字就给出红色提示语,并且无法输入密码,当用户名符合要求时才可以输入密码。同样的当用户输入的密码不到 5 个字时也给出...
继续阅读 »

你好 RxSwift!

我的第一个 RxSwift 应用程序 - 输入验证:

这是一个模拟用户登录的程序。

  • 当用户输入用户名时,如果用户名不足 5 个字就给出红色提示语,并且无法输入密码,当用户名符合要求时才可以输入密码。
  • 同样的当用户输入的密码不到 5 个字时也给出红色提示语。
  • 当用户名和密码有一个不符合要求时底部的绿色按钮不可点击,只有当用户名和密码同时有效时按钮才可点击。
  • 当点击绿色按钮后弹出一个提示框,这个提示框只是用来做演示而已。

你可以下载这个例子并在模拟器上运行,这样可以帮助于你理解整个程序的交互:

这个页面主要由 5 各元素组成:

  1. 用户名输入框
  2. 用户名提示语(红色)
  3. 密码输入框
  4. 密码提示语(红色)
  5. 操作按钮(绿色)
class SimpleValidationViewController : ViewController {

@IBOutlet weak var usernameOutlet: UITextField!
@IBOutlet weak var usernameValidOutlet: UILabel!

@IBOutlet weak var passwordOutlet: UITextField!
@IBOutlet weak var passwordValidOutlet: UILabel!

@IBOutlet weak var doSomethingOutlet: UIButton!
...
}

这里需要完成 4 个交互:

  • 当用户名输入不到 5 个字时显示提示语,并且无法输入密码

    override func viewDidLoad() {
    super.viewDidLoad()

    ...

    // 用户名是否有效
    let usernameValid = usernameOutlet.rx.text.orEmpty
    // 用户名 -> 用户名是否有效
    .map { $0.count >= minimalUsernameLength }
    .share(replay: 1)

    ...

    // 用户名是否有效 -> 密码输入框是否可用
    usernameValid
    .bind(to: passwordOutlet.rx.isEnabled)
    .disposed(by: disposeBag)

    // 用户名是否有效 -> 用户名提示语是否隐藏
    usernameValid
    .bind(to: usernameValidOutlet.rx.isHidden)
    .disposed(by: disposeBag)

    ...
    }

    当用户修改用户名输入框的内容时就会产生一个新的用户名, 然后通过 map 方法将它转化成用户名是否有效, 最后通过 bind(to: ...) 来决定密码输入框是否可用以及提示语是否隐藏。

  • 当密码输入不到 5 个字时显示提示文字

    override func viewDidLoad() {
    super.viewDidLoad()

    ...

    // 密码是否有效
    let passwordValid = passwordOutlet.rx.text.orEmpty
    // 密码 -> 密码是否有效
    .map { $0.count >= minimalPasswordLength }
    .share(replay: 1)

    ...

    // 密码是否有效 -> 密码提示语是否隐藏
    passwordValid
    .bind(to: passwordValidOutlet.rx.isHidden)
    .disposed(by: disposeBag)

    ...
    }

    这个和用用户名来控制提示语的逻辑是一样的。

  • 当用户名和密码都符合要求时,绿色按钮才可点击

    override func viewDidLoad() {
    super.viewDidLoad()

    ...

    // 用户名是否有效
    let usernameValid = ...

    // 密码是否有效
    let passwordValid = ...

    ...

    // 所有输入是否有效
    let everythingValid = Observable.combineLatest(
    usernameValid,
    passwordValid
    ) { $0 && $1 } // 取用户名和密码同时有效
    .share(replay: 1)

    ...

    // 所有输入是否有效 -> 绿色按钮是否可点击
    everythingValid
    .bind(to: doSomethingOutlet.rx.isEnabled)
    .disposed(by: disposeBag)

    ...
    }

    通过 Observable.combineLatest(...) { ... } 来将用户名是否有效以及密码是都有效合并出两者是否同时有效,然后用它来控制绿色按钮是否可点击。

  • 点击绿色按钮后,弹出一个提示框

    override func viewDidLoad() {
    super.viewDidLoad()

    ...

    // 点击绿色按钮 -> 弹出提示框
    doSomethingOutlet.rx.tap
    .subscribe(onNext: { [weak self] in self?.showAlert() })
    .disposed(by: disposeBag)
    }

    func showAlert() {
    let alertView = UIAlertView(
    title: "RxExample",
    message: "This is wonderful",
    delegate: nil,
    cancelButtonTitle: "OK"
    )

    alertView.show()
    }

    在点击绿色按钮后,弹出一个提示框

这样 4 个交互都完成了,现在我们纵观全局看下这个程序是一个什么样的结构:

然后看一下完整的代码:

override func viewDidLoad() {
super.viewDidLoad()

usernameValidOutlet.text = "Username has to be at least \(minimalUsernameLength) characters"
passwordValidOutlet.text = "Password has to be at least \(minimalPasswordLength) characters"

let usernameValid = usernameOutlet.rx.text.orEmpty
.map { $0.count >= minimalUsernameLength }
.share(replay: 1)

let passwordValid = passwordOutlet.rx.text.orEmpty
.map { $0.count >= minimalPasswordLength }
.share(replay: 1)

let everythingValid = Observable.combineLatest(
usernameValid,
passwordValid
) { $0 && $1 }
.share(replay: 1)

usernameValid
.bind(to: passwordOutlet.rx.isEnabled)
.disposed(by: disposeBag)

usernameValid
.bind(to: usernameValidOutlet.rx.isHidden)
.disposed(by: disposeBag)

passwordValid
.bind(to: passwordValidOutlet.rx.isHidden)
.disposed(by: disposeBag)

everythingValid
.bind(to: doSomethingOutlet.rx.isEnabled)
.disposed(by: disposeBag)

doSomethingOutlet.rx.tap
.subscribe(onNext: { [weak self] in self?.showAlert() })
.disposed(by: disposeBag)
}

func showAlert() {
let alertView = UIAlertView(
title: "RxExample",
message: "This is wonderful",
delegate: nil,
cancelButtonTitle: "OK"
)

alertView.show()
}

你会发现你可以用几行代码完成如此复杂的交互。这可以大大提升我们的开发效率。

更多疑问

  • share(replay: 1) 是用来做什么的?

    我们用 usernameValid 来控制用户名提示语是否隐藏以及密码输入框是否可用。shareReplay 就是让他们共享这一个源,而不是为他们单独创建新的源。这样可以减少不必要的开支。

  • disposed(by: disposeBag) 是用来做什么的?

    和我们所熟悉的对象一样,每一个绑定也是有生命周期的。并且这个绑定是可以被清除的。disposed(by: disposeBag)就是将绑定的生命周期交给 disposeBag 来管理。当 disposeBag 被释放的时候,那么里面尚未清除的绑定也就被清除了。这就相当于是在用 ARC 来管理绑定的生命周期。 这个内容会在 Disposable 章节详细介绍。

收起阅读 »

iOS RXSwift 一

iOS
为什么要使用 RxSwift ?我们先看一下 RxSwift 能够帮助我们做些什么:Target Action传统实现方法:button.addTarget(self, action: #selector(buttonTapped), for: .touchU...
继续阅读 »

为什么要使用 RxSwift ?

我们先看一下 RxSwift 能够帮助我们做些什么:

Target Action

传统实现方法:

button.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
func buttonTapped() {
print("button Tapped")
}

通过 Rx 来实现:

button.rx.tap
.subscribe(onNext: {
print("button Tapped")
})
.disposed(by: disposeBag)

你不需要使用 Target Action,这样使得代码逻辑清晰可见。

代理

传统实现方法:

class ViewController: UIViewController {
...
override func viewDidLoad() {
super.viewDidLoad()
scrollView.delegate = self
}
}

extension ViewController: UIScrollViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
print("contentOffset: \(scrollView.contentOffset)")
}
}

通过 Rx 来实现:

class ViewController: UIViewController {
...
override func viewDidLoad() {
super.viewDidLoad()

scrollView.rx.contentOffset
.subscribe(onNext: { contentOffset in
print("contentOffset: \(contentOffset)")
})
.disposed(by: disposeBag)
}
}

你不需要书写代理的配置代码,就能获得想要的结果。

闭包回调

传统实现方法:

URLSession.shared.dataTask(with: URLRequest(url: url)) {
(data, response, error) in
guard error == nil else {
print("Data Task Error: \(error!)")
return
}

guard let data = data else {
print("Data Task Error: unknown")
return
}

print("Data Task Success with count: \(data.count)")
}.resume()

通过 Rx 来实现:

URLSession.shared.rx.data(request: URLRequest(url: url))
.subscribe(onNext: { data in
print("Data Task Success with count: \(data.count)")
}, onError: { error in
print("Data Task Error: \(error)")
})
.disposed(by: disposeBag)

回调也变得十分简单

通知

传统实现方法:

var ntfObserver: NSObjectProtocol!

override func viewDidLoad() {
super.viewDidLoad()

ntfObserver = NotificationCenter.default.addObserver(
forName: .UIApplicationWillEnterForeground,
object: nil, queue: nil) { (notification) in
print("Application Will Enter Foreground")
}
}

deinit {
NotificationCenter.default.removeObserver(ntfObserver)
}

通过 Rx 来实现:

override func viewDidLoad() {
super.viewDidLoad()

NotificationCenter.default.rx
.notification(.UIApplicationWillEnterForeground)
.subscribe(onNext: { (notification) in
print("Application Will Enter Foreground")
})
.disposed(by: disposeBag)
}

你不需要去管理观察者的生命周期,这样你就有更多精力去关注业务逻辑。

多个任务之间有依赖关系

例如,先通过用户名密码取得 Token 然后通过 Token 取得用户信息,

传统实现方法:

/// 用回调的方式封装接口
enum API {

/// 通过用户名密码取得一个 token
static func token(username: String, password: String,
success: (String)
-> Void,
failure: (Error) -> Void) { ... }

/// 通过 token 取得用户信息
static func userinfo(token: String,
success: (UserInfo)
-> Void,
failure: (Error) -> Void) { ... }
}
/// 通过用户名和密码获取用户信息
API.token(username: "beeth0ven", password: "987654321",
success: { token in
API.userInfo(token: token,
success: { userInfo in
print("获取用户信息成功: \(userInfo)")
},
failure: { error in
print("获取用户信息失败: \(error)")
})
},
failure: { error in
print("获取用户信息失败: \(error)")
})

通过 Rx 来实现:

/// 用 Rx 封装接口
enum API {

/// 通过用户名密码取得一个 token
static func token(username: String, password: String) -> Observable<String> { ... }

/// 通过 token 取得用户信息
static func userInfo(token: String) -> Observable<UserInfo> { ... }
}
/// 通过用户名和密码获取用户信息
API.token(username: "beeth0ven", password: "987654321")
.flatMapLatest(API.userInfo)
.subscribe(onNext: { userInfo in
print("获取用户信息成功: \(userInfo)")
}, onError: { error in
print("获取用户信息失败: \(error)")
})
.disposed(by: disposeBag)

这样你可以避免回调地狱,从而使得代码易读,易维护。

等待多个并发任务完成后处理结果

例如,需要将两个网络请求合并成一个,

通过 Rx 来实现:

/// 用 Rx 封装接口
enum API {

/// 取得老师的详细信息
static func teacher(teacherId: Int) -> Observable<Teacher> { ... }

/// 取得老师的评论
static func teacherComments(teacherId: Int) -> Observable<[Comment]> { ... }
}
/// 同时取得老师信息和老师评论
Observable.zip(
API.teacher(teacherId: teacherId),
API.teacherComments(teacherId: teacherId)
).subscribe(onNext: { (teacher, comments) in
print("获取老师信息成功: \(teacher)")
print("获取老师评论成功: \(comments.count) 条")
}, onError: { error in
print("获取老师信息或评论失败: \(error)")
})
.disposed(by: disposeBag)

这样你可用寥寥几行代码来完成相当复杂的异步操作。


那么为什么要使用 RxSwift ?

  • 复合 - Rx 就是复合的代名词
  • 复用 - 因为它易复合
  • 清晰 - 因为声明都是不可变更的
  • 易用 - 因为它抽象的了异步编程,使我们统一了代码风格
  • 稳定 - 因为 Rx 是完全通过单元测试的
收起阅读 »

iOS底层-内存对齐

iOS
一、什么是内存对齐? 我们先看下以下例子: struct struct0 { int a; char c; }s; NSLog(@"s : %lu", sizeof(s)); //输出8 在32位下,int占4byte, char占1b...
继续阅读 »

一、什么是内存对齐?


我们先看下以下例子:


struct struct0 {
int a;
char c;
}s;

NSLog(@"s : %lu", sizeof(s)); //输出8

32位下int4bytechar1byte,那么放到结构体应该是4+1=5byte啊,但实际得到的结果却是8,这就是内存对齐导致的。



元素是按照定义顺序一个一个放到内存中去的,但并不是紧密排列的。 从结构体存储的 首地址开始,每个元素放置到内存中时,它都会认为内存是按照自己的大小(通常它为4或8)来划分的,因此元素放置的位置一定会在自己宽度的整数倍上开始,这就是所谓的内存对齐



二、为什么要进行内存对齐?


1.平台限制



  • 各个硬件平台对存储空间的处理上有很大的不同。一些平台对某些特定类型的数据只能从某些特定地址开始存取,它一般会以双字节,四字节,8字节,16字节甚至32字节为单位来存取内存。


2.性能原因



  • 例如在32位下的int型变量,如果按照对齐的方式,读一次就可以读出,如果忽略对齐,int存放在奇数位或者不在4的倍数位上,则需要读两次,再进行拼接才能得到数据。

    • 例如,没有内存对齐机制,一个int变量从地址1开始存储,如下图:




截屏2021-06-09 11.51.18.png
这种情况,处理器需要:剔除不要的,再合并到寄存器,做了些额外的操作。而在内存对齐的环境中,一次就可以读出数据,提高了效率


三、内存对齐规则


1. 概念:



  • 每个特定平台上的编译器都有自己的默认对齐系数(也叫对齐模数)。可以通过预编译命令#pragma pack(n),n = 1,2,4,8,16来改变这一系数,这个n就是要制定的对齐系数

  • 有效对齐值:给定值#pragma pack(n)和结构体中最长数据类型长度中较小的那个。有效对齐值也叫对齐单位

  • Xcode中默认#pragma pack(8),也就是8字节对齐


2. 规则:



  • (1): 结构体第一个成员的偏移量(offset)为0,以后每个成员相对于结构体首地址的 offset都是该成员大小与有效对齐值中较小那个的整数倍,如有需要编译器会在成员之间加上填充字节

  • (2): 结构体的总大小为有效对齐值的整数倍,如有需要编译器会在最末一个成员之后加上填充字节


3.举例:


结构体


环境(Xcode 12.3)

例一


struct Struct1 {
double a;
char b;
int c;
short d;
}s1;

分析



  • 对齐系数为:pack(8),也就是8字节,最长的数据类型是:double,也是8字节,则 有效对齐值min(8,8) = 8

  • 根据规则(1)(2),可得出:


截屏2021-06-09 14.45.19.png



每个成员的大小都要和有效对齐值对比,取比较小的数,然后这个数的整数倍作为这个成员变量的offset起始存储位置,如果中间不是整数倍,则需要填充字节。成员变量存储完后,如果整体大小不是有效对齐值的整数倍,则需要在 末尾填充字节 至整体大小为有效对齐值的整数倍为止。



分析的结果是 24字节,我们打印来验证下:


截屏2021-06-09 13.25.26.png


打印的结果和我们分析的一样~


例二


struct Struct2 {
double a;
int b;
char c;
short d;
}s2;

printf("\n Struct1 size: %lu\n", sizeof(s2)); // 输出 16


这两个结构体成员变量都是一样的,只不过位置换了下,怎么内存大小也变了?啊这...



我们继续来分析下:


截屏2021-06-09 14.45.43.png
分析得到的结果是16字节,我们再来打印验证下:

截屏2021-06-09 13.40.57.png


原来结构体成员变量的顺序对内存也是有影响的


例三


struct Struct3 {
double a; // 8 [0,7]
int b; // 4 [8,11]
char c; // 1 [12]
short d; // 2 (13, [14,15]
int e; // 4 [16, 19]
struct Struct1 {
double a; // 8 (20, 21, 22, 23, [24, 31]
char b; // 1 [32]
int c; // 4 (33, 34, 35 [36, 39]
short d; // 2 [40, 41],42, 43, 44, 45, 46, 47, 48
}s1;
}s3;

分析



  • 对齐系数为:pack(8) = 8Struct1中最大为8字节Struct3其他的成员最大为8字节, 则Struct3 有效对齐值min(pack(8),8) = 8


截屏2021-06-09 17.02.54.png
打印验证下:


截屏2021-06-09 15.30.12.png

结果是正确的~


可能存在的误区


  1. offset:计算成员的offset时,有可能会将成员大小的整数倍当做其offset,这个是错误的,offset是由有效对齐值成员大小 二者的最小值的整数倍决定的

  2. 整体大小:整体大小(如果需要补充字节,就是补充后的)是最大成员的整数倍,这是不准确的。整体大小是有效对齐值的整数倍


我们在举一个例子:


32位下


// 32 位下对齐系数 pack(4)
struct Struct4 {
int a; // 4 [0,3]
double b; // 8 [4,11]
}s4;

printf("\n\n Struct4 size: %lu\n", sizeof(s4)); // 输出 12

64位下


// 64 位下对齐系数 pack(8)
struct Struct4 {
int a; // 4 [0,3]
double b; // 8 (4, 5, 6, 7, [8, 15]
}s4;

printf("\n\n Struct4 size: %lu\n", sizeof(s4)); // 输出 16

打印如下:


image_2021-06-09_17-35-22.png




  • 32位中,Struct4有效对齐值min(pack(4), 8) = 4a从首地址开始,boffsetmin( min(pack(4), 8), 8) = 4,所以得出结果为12

  • 64位中,Struct4有效对齐值min(pack(8), 8) = 8a从首地址开始,boffsetmin( min(pack(8), 8), 8) = 8,所以得出结果为16



总结


  • 计算成员的offset是由有效对齐值成员大小二者的最小值的整数倍决定的。

  • 整体大小(如果需要补充字节,就是补充后的)是有效对齐值的整数倍。

  • 结构体成员的顺序不同会导致内存不同,而对象的本质就是一个结构体,我们可以调整对象属性的位置来达到内存优化的目的。


对象


获取对象内存大小的方式


  • sizeof

  • class_getInstanceSize

  • malloc_size


sizeof


  1. sizeof运算符,而不是一个函数

  2. sizeof传进来的是类型,用来计算这个类型占多大内存,这个在编译器编译阶段就会确定大小并直接转化成8 、16、24这样的常数,而不是在运行时计算。参数可以是数组、指针、类型、对象、结构体、函数等。

  3. 它的功能是:获得保证能容纳实现所建立的最大对象的字节大小


class_getInstanceSize


  • class_getInstanceSizeruntime提供的api,计算对象实际占用的内存大小,采用8字节对齐的方式进行运算。


malloc_size


  • malloc_size用来计算对象实际分配的内存大小,这个是由系统采用16字节对齐的方式运算的


可以通过下面代码测试:


@interface LGPerson : NSObject

@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *nickName;
@property (nonatomic, assign) int age;
@property (nonatomic, assign) long height;

@end
// LGPerson 中有4个属性 NSString *name,NSString *nickName,int age,long height
int main(int argc, const char * argv[]) {
LGPerson *person = [LGPerson alloc];
person.name = @"Cc";
person.nickName = @"KC";

NSLog(@"\n%lu\n - %lu\n - %lu\n", sizeof(person), class_getInstanceSize([LGPerson class]), malloc_size((__bridge const void *)(person)));
// 输出 8, 40, 48
return 0;
}

链接:https://juejin.cn/post/6971750424198152200

收起阅读 »

iOS底层-多线程之GCD(下)

iOS
前言 前面的文章讲述了同步和异步的底层分析步骤,今天来讲GCD实际的应用相关的函数及原理,主要是:栅栏函数,信号量,线程组和Dispatch_source 栅栏函数 栅栏函数有一个比较直接的效果:控制任务的执行顺序,导致同步的效果。 栅栏函数有两种: di...
继续阅读 »

前言


前面的文章讲述了同步和异步的底层分析步骤,今天来讲GCD实际的应用相关的函数及原理,主要是:栅栏函数信号量线程组Dispatch_source


栅栏函数



  • 栅栏函数有一个比较直接的效果:控制任务的执行顺序,导致同步的效果

  • 栅栏函数有两种:

    • dispatch_barrier_async

    • dispatch_barrier_sync




下面通过案例来分析他们的作用:


dispatch_barrier_async


先来看看异步栅栏的案例:


- (void)testAsync_barrier {
dispatch_queue_t concurrent = dispatch_queue_create("wushuang.concurrent", DISPATCH_QUEUE_CONCURRENT);
NSLog(@" 开始啦 ~ ");
dispatch_async(concurrent, ^{
sleep(1);
NSLog(@"1");
});
dispatch_async(concurrent, ^{
sleep(1);
NSLog(@"2");
});
dispatch_barrier_async(concurrent, ^{
NSLog(@"——————— 大栅栏 ———————");
});
dispatch_async(concurrent, ^{
NSLog(@"3");
});
dispatch_async(concurrent, ^{
NSLog(@"4");
});
NSLog(@" ~~ 你过来呀 ~~ ");
}
复制代码


  • 为了更直接的观察,在栅栏前面的异步函数里加上了sleep,打印结果如下:


截屏2021-08-18 14.07.21.png



  • 从打印结果可以看出来,栅栏函数拦住的是同一个线程中的任务,并 不会阻塞线程


dispatch_barrier_sync


再来看看同步栅栏:


- (void)testSync_barrier {
dispatch_queue_t concurrent = dispatch_queue_create("wushuang.concurrent", DISPATCH_QUEUE_CONCURRENT);
NSLog(@" 开始啦 ~ ");
dispatch_async(concurrent, ^{
sleep(1);
NSLog(@"1");
});
dispatch_async(concurrent, ^{
sleep(1);
NSLog(@"2");
});
dispatch_barrier_sync(concurrent, ^{
NSLog(@"——————— 同步大栅栏 ———————");
});
dispatch_async(concurrent, ^{
NSLog(@"3");
});
dispatch_async(concurrent, ^{
NSLog(@"4");
});
NSLog(@" ~~ 你过来呀 ~~ ");
}
复制代码

先看看打印结果:


截屏2021-08-18 14.14.17.png



  • 从结果中能够看出来,同步栅栏函数也拦住的是同一个队列中的任务,但 阻塞线程


全局队列栅栏


我们平常用的栅栏函数都是创建的并发队列,那么使用在全局队列使用呢?


- (void)testGlobalBarrier {
dispatch_queue_t global = dispatch_get_global_queue(0, 0);
NSLog(@" 开始啦 ~ ");
dispatch_async(global, ^{
sleep(1);
NSLog(@"1");
});
dispatch_async(global, ^{
sleep(1);
NSLog(@"2");
});
dispatch_barrier_async(global, ^{
NSLog(@"——————— 大栅栏 ———————");
});
dispatch_async(global, ^{
NSLog(@"3");
});

dispatch_async(global, ^{
NSLog(@"4");
});
NSLog(@" ~~ 你过来呀 ~~ ");
}
复制代码


  • 打印结果如下:


截屏2021-08-18 14.27.05.png



  • 结果什么也没有拦住,说明全局队列比较特殊。



疑问:
1. 为什么栅栏函数可以控制任务
2. 为什么栅栏函数在全局队列不起作用




底层源码



  • dispatch_barrier_sync来分析,搜索跟流程最终会进入_dispatch_barrier_sync_f_inline方法


截屏2021-08-18 16.28.41.png



  • 通过符号断点调试,发现最终会走_dispatch_sync_f_slow方法,同时设置DC_FLAG_BARRIER标记,再跟进


截屏2021-08-18 16.40.18.png



  • 再根据下符号断点,确定走_dispatch_sync_invoke_and_complete_recurse方法,并且将DC_FLAG_BARRIER参数传入


截屏2021-08-18 16.42.27.png



  • 进入_dispatch_sync_function_invoke_inline方法能够发现是_dispatch_client_callout方法,也就是栅栏函数的调用,但我们研究的核心是为什么会控制任务,通过下符号断点发现,栅栏函数执行后会走_dispatch_sync_complete_recurse方法


截屏2021-08-18 16.46.26.png



  • 于是跟进_dispatch_sync_complete_recurse方法


static void
_dispatch_sync_complete_recurse(dispatch_queue_t dq, dispatch_queue_t stop_dq,
uintptr_t dc_flags)
{
bool barrier = (dc_flags & DC_FLAG_BARRIER);
do {
if (dq == stop_dq) return;
if (barrier) {
dx_wakeup(dq, 0, DISPATCH_WAKEUP_BARRIER_COMPLETE);
} else {
_dispatch_lane_non_barrier_complete(upcast(dq)._dl, 0);
}
dq = dq->do_targetq;
barrier = (dq->dq_width == 1);
} while (unlikely(dq->do_targetq));
}
复制代码


  • 此处是一个do-while循环,首先dc_flags传入的是DC_FLAG_BARRIER,所以(dc_flags & DC_FLAG_BARRIER)一定有值的,于是在循环中会走dx_wakeup,去唤醒队列中的任务,唤醒完成后也就是栅栏结束,就会走_dispatch_lane_non_barrier_complete函数,也就是继续栅栏之后的流程。

  • 再查看此时dx_wakeup函数,此时flag传入为DISPATCH_WAKEUP_BARRIER_COMPLETE


截屏2021-08-18 17.01.29.png


_dispatch_lane_wakeup



  • 并发流程_dispatch_root_queue_wakeup中,根据flag判断会走_dispatch_lane_barrier_complete方法:


截屏2021-08-18 17.06.05.png



  • 当串行或者栅栏时,会调用_dispatch_lane_drain_barrier_waiter阻塞任务,直到确认当前队列前面任务执行完就会继续后面任务的执行


_dispatch_root_queue_wakeup


void
_dispatch_root_queue_wakeup(dispatch_queue_global_t dq,
DISPATCH_UNUSED dispatch_qos_t qos, dispatch_wakeup_flags_t flags)
{
if (!(flags & DISPATCH_WAKEUP_BLOCK_WAIT)) {
DISPATCH_INTERNAL_CRASH(dq->dq_priority,
"Don't try to wake up or override a root queue");
}
if (flags & DISPATCH_WAKEUP_CONSUME_2) {
return _dispatch_release_2_tailcall(dq);
}
}
复制代码


  • 全局并发队列源码中什么没做,此时的栅栏相当于一个普通的异步并发函数没起作用,为什么呢?全局并发是系统创建的并发队列,如果阻塞可能会导致系统任务出现问题,所以在使用栅栏函数时,不能使用全部并发队列


总结





    1. 栅栏函数可以阻塞当前线程的任务,达到控制任务的效果,但只能在创建的并发队列中使用





    1. 栅栏函数也可以在多读单写的场景中使用





    1. 栅栏函数只能在当前线程使用,如果多个线程就会起不到想要的效果




信号量



  • 程序员的自我修养中这本书的26页,有对二元信号量的讲解,它只有01两个状态,多元信号量简称信号量

  • GCD中的信号量dispatch_semaphore_t中主要有三个函数:

    • dispatch_semaphore_create:创建信号

    • dispatch_semaphore_wait:等待信号

    • dispatch_semaphore_signal:释放信号




案例分析


- (void)testDispatchSemaphore {
dispatch_queue_t global = dispatch_get_global_queue(0, 0);
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);

NSLog(@"~~ 开始 ~~");
dispatch_async(global, ^{
NSLog(@"~~ 0 ~~");
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"~~ 1 ~~");
});

dispatch_async(global, ^{
NSLog(@"~~ 2 ~~");
sleep(2);
dispatch_semaphore_signal(semaphore);
NSLog(@"~~ 3 ~~");
});
NSLog(@" ~~ 你过来呀 ~~ ");
}
复制代码


  • 运行结果如下:


截屏2021-08-18 19.52.23.png



  • 从打印结果中可以看出来,在先走了一个异步任务所以打印了0,但是由于没有信号,所以在dispatch_semaphore_wait就原地等待,导致1没法执行,此时第二个异步任务执行了就打印了2,然后dispatch_semaphore_signal释放信号,之后1就可以打印了


源码解读


再来看看源码分析


dispatch_semaphore_create


截屏2021-08-18 22.33.40.png



  • 首先如果信号为小于0,则返回一个DISPATCH_BAD_INPUT类型对象,也就是返回个_Nonnull

  • 如果信号大于等于0,就会对dispatch_semaphore_t对象dsema进行一些赋值,并返回dsema对象


dispatch_semaphore_wait


intptr_t
dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout)
{
long value = os_atomic_dec2o(dsema, dsema_value, acquire);
if (likely(value >= 0)) {
return 0;
}
return _dispatch_semaphore_wait_slow(dsema, timeout);
}
复制代码


  • 等待信号主要是通过os_atomic_dec2o函数对信号进行自减,当值大于等于0时返回0

  • 当值小于0时,会走_dispatch_semaphore_wait_slow方法


_dispatch_semaphore_wait_slow

截屏2021-08-18 23.02.51.png




  • 当设置了timeout(超时时间),则会根据类型进行相关操作,本文使用的是DISPATCH_TIME_FOREVER,此时调用_dispatch_sema4_wait进行等待处理:


    截屏2021-08-18 23.10.06.png



    • 这里主要是一个do-while循环里队等待信号,当不满足条件后才会跳出循环,所以会出现一个等待的效果




dispatch_semaphore_signal


intptr_t
dispatch_semaphore_signal(dispatch_semaphore_t dsema)
{
long value = os_atomic_inc2o(dsema, dsema_value, release);
if (likely(value > 0)) {
return 0;
}
if (unlikely(value == LONG_MIN)) {
DISPATCH_CLIENT_CRASH(value,
"Unbalanced call to dispatch_semaphore_signal()");
}
return _dispatch_semaphore_signal_slow(dsema);
}
复制代码


  • 发送信号主要是通过os_atomic_inc2o对信号进行自增,如果自增后的结果大于0,就返回0

  • 如果自增后还是小于0,就会走到_dispatch_semaphore_signal_slow方法


_dispatch_semaphore_signal_slow

intptr_t
_dispatch_semaphore_signal_slow(dispatch_semaphore_t dsema)
{
_dispatch_sema4_create(&dsema->dsema_sema, _DSEMA4_POLICY_FIFO);
_dispatch_sema4_signal(&dsema->dsema_sema, 1);
return 1;
}
复制代码


  • 这里有个_dispatch_sema4_signal函数进行缓慢发送信号


截屏2021-08-18 23.14.05.png


调度组



  • 调度组最直接的作用就是:控制任务的执行顺序Api主要有以下几个方法:

    • dispatch_group_create:创建组

    • dispatch_group_async:进组任务

    • dispatch_group_notify:组任务执行完毕的通知

    • dispatch_group_enter:进组

    • dispatch_group_leave:出组

    • dispatch_group_wait:等待组任务时间




案例分析


dispatch_group_async


- (void)testDispatchGroup1 {
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t globalQueue = dispatch_get_global_queue(0, 0);
dispatch_group_async(group, globalQueue, ^{
sleep(1);
NSLog(@"1");
});

NSLog(@"2");
dispatch_group_async(group, globalQueue, ^{
sleep(1);
NSLog(@"3");
});

NSLog(@"4");
dispatch_group_notify(group, globalQueue, ^{
NSLog(@"5");
});

NSLog(@"~~ 6 ~~");
}
复制代码


  • 讲任务1和3放到组任务,然后将5任务放到dispatch_group_notify中执行,输出结果如下:


截屏2021-08-19 11.24.37.png



  • 输出结果可以得出结论:



      1. 调度组不会阻塞线程





      1. 组任务执行没有顺序,相当于异步并发队列





      1. 组任务执行完后才会执行dispatch_group_notify任务






dispatch_group_wait





    1. 将任务1和3分别放到两个任务组,然后在下面执行dispatch_group_wait等待10秒




- (void)testDispatchGroup1 {
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t globalQueue = dispatch_get_global_queue(0, 0);
dispatch_group_async(group, globalQueue, ^{
sleep(5);
NSLog(@"1");
});

NSLog(@"2");
dispatch_group_async(group, globalQueue, ^{
sleep(5);
NSLog(@"3");
});

dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC * 10);
dispatch_group_wait(group, time);

NSLog(@"~~ 6 ~~");
}
复制代码


  • 输出结果如下:


截屏2021-08-19 11.49.47.png

发现等待的是10秒,但5秒后任务执行完,立即执行任务6





    1. 在将等待时间改成3秒




截屏2021-08-19 11.53.34.png

这里是等3秒,发现组任务还没执行完就去执行6任务



  • 总结dispatch_group_wait的作用是阻塞调度组之外的任务



      1. 当等待时间结束时,组任务还没完成,就结束阻塞执行其他任务





      1. 当组任务完成,等待时间还未结束时,会结束阻塞执行其他任务






进组 + 出组


- (void)testDispatchGroup2 {
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t globalQueue = dispatch_get_global_queue(0, 0);

dispatch_group_enter(group);
dispatch_async(globalQueue, ^{
sleep(1);
NSLog(@"1");
dispatch_group_leave(group);
});

NSLog(@"2");

dispatch_group_enter(group);
dispatch_async(globalQueue, ^{
sleep(1);
NSLog(@"3");
dispatch_group_leave(group);
});

dispatch_group_notify(group, globalQueue, ^{
NSLog(@"4");
});

NSLog(@"5");
}
复制代码


  • 进组+出组的案例基本和上面的一致,只是将进组任务拆分成进组+出组,执行结果如下:


截屏2021-08-19 11.29.38.png




  • 输出结果和dispatch_group_async基本一致,说明他们两个作用相同




  • 进组+出组组合有几个 注意事项,他们必须是成对出现,必须先进组后出组,否则会出现以下问题:





      1. 如果少一个dispatch_group_leave,则dispatch_group_notify不会执行




    截屏2021-08-19 11.40.04.png





      1. 如果少一个dispatch_group_enter,则任务执行完会走notify但不成对的dispatch_group_leave处会崩溃




    截屏2021-08-19 11.37.34.png





      1. 如果先leaveenter,此时会直接崩溃在leave




    截屏2021-08-19 11.43.49.png





疑问:
1. 调度组是如何达到流程控制的?
2. 为什么进组+出组要搭配使用,且效果和dispatch_group_async是一样的?
3. 为什么先dispatch_group_leave会崩溃?



带着问题我们去源码中探索究竟


原理分析


dispatch_group_creat


dispatch_group_t
dispatch_group_create(void)
{
return _dispatch_group_create_with_count(0);
}
复制代码

dispatch_group_create方法会调用_dispatch_group_create_with_count方法,并传入参数0


static inline dispatch_group_t
_dispatch_group_create_with_count(uint32_t n)
{
dispatch_group_t dg = _dispatch_object_alloc(DISPATCH_VTABLE(group),
sizeof(struct dispatch_group_s));
dg->do_next = DISPATCH_OBJECT_LISTLESS;
dg->do_targetq = _dispatch_get_default_queue(false);
if (n) {
os_atomic_store2o(dg, dg_bits,
(uint32_t)-n * DISPATCH_GROUP_VALUE_INTERVAL, relaxed);
os_atomic_store2o(dg, do_ref_cnt, 1, relaxed); // <rdar://22318411>
}
return dg;
}
复制代码


  • 方法的核心是创建dispatch_group_t对象dg,并对它的do_nextdo_targetq参数进行赋值,然后调用os_atomic_store2o进行存储


dispatch_group_enter


void
dispatch_group_enter(dispatch_group_t dg)
{
// The value is decremented on a 32bits wide atomic so that the carry
// for the 0 -> -1 transition is not propagated to the upper 32bits.
uint32_t old_bits = os_atomic_sub_orig2o(dg, dg_bits,
DISPATCH_GROUP_VALUE_INTERVAL, acquire);
uint32_t old_value = old_bits & DISPATCH_GROUP_VALUE_MASK;
if (unlikely(old_value == 0)) {
_dispatch_retain(dg); // <rdar://problem/22318411>
}
if (unlikely(old_value == DISPATCH_GROUP_VALUE_MAX)) {
DISPATCH_CLIENT_CRASH(old_bits,
"Too many nested calls to dispatch_group_enter()");
}
}
复制代码

主要调用os_atomic_sub_orig2o进行自减操作,也就是由0 -> -1,这块和信号量的操作很像,但没有wait的步骤


dispatch_group_leave


void
dispatch_group_leave(dispatch_group_t dg)
{
// The value is incremented on a 64bits wide atomic so that the carry for
// the -1 -> 0 transition increments the generation atomically.
uint64_t new_state, old_state = os_atomic_add_orig2o(dg, dg_state,
DISPATCH_GROUP_VALUE_INTERVAL, release);
uint32_t old_value = (uint32_t)(old_state & DISPATCH_GROUP_VALUE_MASK);

if (unlikely(old_value == DISPATCH_GROUP_VALUE_1)) {
old_state += DISPATCH_GROUP_VALUE_INTERVAL;
do {
new_state = old_state;
if ((old_state & DISPATCH_GROUP_VALUE_MASK) == 0) {
new_state &= ~DISPATCH_GROUP_HAS_WAITERS;
new_state &= ~DISPATCH_GROUP_HAS_NOTIFS;
} else {
// If the group was entered again since the atomic_add above,
// we can't clear the waiters bit anymore as we don't know for
// which generation the waiters are for
new_state &= ~DISPATCH_GROUP_HAS_NOTIFS;
}
if (old_state == new_state) break;
} while (unlikely(!os_atomic_cmpxchgv2o(dg, dg_state,
old_state, new_state, &old_state, relaxed)));
return _dispatch_group_wake(dg, old_state, true);
}

if (unlikely(old_value == 0)) {
DISPATCH_CLIENT_CRASH((uintptr_t)old_value,
"Unbalanced call to dispatch_group_leave()");
}
}
复制代码

通过os_atomic_add_orig2o进行自增(-1 ~ 0)得到old_state = 0


DISPATCH_GROUP_VALUE_MASK       0x00000000fffffffcULL
DISPATCH_GROUP_VALUE_1          DISPATCH_GROUP_VALUE_MASK
复制代码




    1. old_value等于old_state & DISPATCH_GROUP_VALUE_MASK等于0,此时old_value != DISPATCH_GROUP_VALUE_1,再由于判断是unlikely,所以会进入if判断





    1. 由于DISPATCH_GROUP_VALUE_INTERVAL = 4,所以此时old_state = 4,再进入do-while循环,此时会走else判断new_state &= ~DISPATCH_GROUP_HAS_NOTIFS= 4 & ~2 = 4,这时old_statenew_state相等,然后跳出循环执行_dispatch_group_wake方法,也就是唤醒dispatch_group_notify方法





    1. 假如enter两次,则old_state=-1,加上4后3,也就是在do-while循环时,new_state = old_state = 3。然后new_state &= ~DISPATCH_GROUP_HAS_NOTIFS = 3 & ~2 = 1,然后不等与old_state会再进行循环,当再次进行leave自增操作后,新的循环判断会走到_dispatch_group_wake函数进行唤醒




dispatch_group_notify


static inline void
_dispatch_group_notify(dispatch_group_t dg, dispatch_queue_t dq,
dispatch_continuation_t dsn)
{
uint64_t old_state, new_state;
dispatch_continuation_t prev;

dsn->dc_data = dq;
_dispatch_retain(dq);

prev = os_mpsc_push_update_tail(os_mpsc(dg, dg_notify), dsn, do_next);
if (os_mpsc_push_was_empty(prev)) _dispatch_retain(dg);
os_mpsc_push_update_prev(os_mpsc(dg, dg_notify), prev, dsn, do_next);
if (os_mpsc_push_was_empty(prev)) {
os_atomic_rmw_loop2o(dg, dg_state, old_state, new_state, release, {
new_state = old_state | DISPATCH_GROUP_HAS_NOTIFS;
if ((uint32_t)old_state == 0) {
os_atomic_rmw_loop_give_up({
return _dispatch_group_wake(dg, new_state, false);
});
}
});
}
}
复制代码

主要是通过os_atomic_rmw_loop2o进行do-while循环判断,知道old_state == 0时就会走_dispatch_group_wake唤醒,也就是会去走block执行



  • 此时还有一个问题没有解决,就是dispatch_group_async为什么和进组+出组效果一样,再来分析下源码


dispatch_group_async


void
dispatch_group_async(dispatch_group_t dg, dispatch_queue_t dq,
dispatch_block_t db)
{
dispatch_continuation_t dc = _dispatch_continuation_alloc();
uintptr_t dc_flags = DC_FLAG_CONSUME | DC_FLAG_GROUP_ASYNC;
dispatch_qos_t qos;

qos = _dispatch_continuation_init(dc, dq, db, 0, dc_flags);
_dispatch_continuation_group_async(dg, dq, dc, qos);
}
复制代码

这个函数内容比较熟悉,与异步的函数很像,但此时dc_flags = DC_FLAG_CONSUME | DC_FLAG_GROUP_ASYNC,然后走进_dispatch_continuation_group_async函数:


static inline void
_dispatch_continuation_group_async(dispatch_group_t dg, dispatch_queue_t dq,
dispatch_continuation_t dc, dispatch_qos_t qos)
{
dispatch_group_enter(dg);
dc->dc_data = dg;
_dispatch_continuation_async(dq, dc, qos, dc->dc_flags);
}
复制代码

这里我们看到了dispatch_group_enter,但是没有看到leave函数,我猜想肯定在block执行的地方会执行leave,不然不能确保组里任务执行完,于是根据global类型的dq_push = _dispatch_root_queue_push最终找到_dispatch_continuation_invoke_inline函数


截屏2021-08-19 15.57.50.png


如果是普通异步类型会走到_dispatch_client_callout函数,如果是DC_FLAG_GROUP_ASYNC组类型,会走_dispatch_continuation_with_group_invoke函数


static inline void
_dispatch_continuation_with_group_invoke(dispatch_continuation_t dc)
{
struct dispatch_object_s *dou = dc->dc_data;
unsigned long type = dx_type(dou);
if (type == DISPATCH_GROUP_TYPE) {
_dispatch_client_callout(dc->dc_ctxt, dc->dc_func);
_dispatch_trace_item_complete(dc);
dispatch_group_leave((dispatch_group_t)dou);
} else {
DISPATCH_INTERNAL_CRASH(dx_type(dou), "Unexpected object type");
}
}
复制代码

此时,当类型是DISPATCH_GROUP_TYPE时,就会先执行_dispatch_client_callout,然后执行dispatch_group_leave,至此前面的问题全部解决


信号源Dispatch_source



  • 信号源Dispatch_source是一个尽量不占用资源,且CPU负荷非常小的Api,它不受Runloop影响,是和Runloop平级的一套Api。主要有以下几个函数组成:



      1. dispatch_source_create:创建信号源





      1. dispatch_source_set_event_handler:设置信号源回调





      1. dispatch_source_merge_data:源时间设置数据





      1. dispatch_source_get_data:获取信号源数据





      1. dispatch_resume:继续





      1. dispatch_suspend:挂起





  • 在任一线程上调用函数dispatch_source_merge_data后,会执行Dispatch Source事先定义好的句柄(可以理解为一个block),这个过程叫Custom event用户事件,是Dispatch Source支持处理的一种事件


信号源类型



  • 创建源
    dispatch_source_t
    dispatch_source_create(dispatch_source_type_t type,
    uintptr_t handle,
    uintptr_t mask,
    dispatch_queue_t _Nullable queue);
    复制代码


    • 第一个参数是dispatch_source_type_t类型的type,然后handlemask都是uintptr_t类型的,最后要传入一个队列



  • 使用方法:
    dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_DATA_ADD, 0, 0, dispatch_get_main_queue());
    复制代码


  • 源的类型dispatch_source_type_t



      1. DISPATCH_SOURCE_TYPE_DATA_ADD:用于ADD合并数据





      1. DISPATCH_SOURCE_TYPE_DATA_OR:用于按位或合并数据





      1. DISPATCH_SOURCE_TYPE_DATA_REPLACE跟踪通过调用dispatch_source_merge_data获得的数据的分派源,新获得的数据值将替换 尚未交付给源处理程序 的现有数据值





      1. DISPATCH_SOURCE_TYPE_MACH_SEND:用于监视Mach端口无效名称通知的调度源,只能发送没有接收权限





      1. DISPATCH_SOURCE_TYPE_MACH_RECV:用于监视Mach端口挂起消息





      1. DISPATCH_SOURCE_TYPE_MEMORYPRESSURE:用于监控系统内存压力变化





      1. DISPATCH_SOURCE_TYPE_PROC:用于监视外部进程的事件





      1. DISPATCH_SOURCE_TYPE_READ监视文件描述符以获取可读取的挂起字节的分派源





      1. DISPATCH_SOURCE_TYPE_SIGNAL监控当前进程以获取信号的调度源





      1. DISPATCH_SOURCE_TYPE_TIMER:基于计时器提交事件处理程序块的分派源





      1. DISPATCH_SOURCE_TYPE_VNODE:用于监视文件描述符中定义的事件的分派源





      1. DISPATCH_SOURCE_TYPE_WRITE监视文件描述符以获取可写入字节的可用缓冲区空间的分派源。






定时器


下面使用Dispatch Source来封装一个定时器


- (void)testTimer {

self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(0, 0));
dispatch_time_t startTime = dispatch_time(DISPATCH_TIME_NOW, 0);
dispatch_source_set_timer(self.timer, startTime, 1 * NSEC_PER_SEC, 0);

__block int a = 0;
dispatch_source_set_event_handler(self.timer, ^{
a++;
NSLog(@"a 的 值 %d", a);
});

dispatch_resume(self.timer);
self.isRunning = YES;
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
if (self.isRunning) {
dispatch_suspend(self.timer);
// dispatch_source_cancel(self.timer);
self.isRunning = NO;
NSLog(@" 中场休息下~ ");
} else {
dispatch_resume(self.timer);
self.isRunning = YES;
NSLog(@" 继续喝~ ");
}
}
复制代码

输出结果如下:


截屏2021-08-19 18.34.34.png



  • 创建定时器dispatch_source_create时,一定要用属性或者实例变量接收,不然定时器不会执行

  • dispatch_source_set_timer的第二个参数start是从什么时候开始,第三个参数interval是时间间隔,leeway是计时器的纳秒偏差

  • 计时器停止有两种:

    • dispatch_resume:计时器暂停但一直在线,可以唤醒

    • dispatch_source_cancel:计时器释放,执行dispatch_resume唤醒,会崩溃




作者:无双3
链接:https://juejin.cn/post/6998091011054370853
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

iOS底层-多线程之GCD(上)

iOS
iOS底层-多线程之GCD(上)前言说到多线程,我们肯定就不会忽视GCD,因为它用法比较简洁,Api也比较易懂,对于处理多个任务等都是比较简单的,接来下将对GCD进行总结和探究。简介GCD全称是Grand Central Dispatch,纯C语言Api,提供...
继续阅读 »

iOS底层-多线程之GCD(上)

前言

说到多线程,我们肯定就不会忽视GCD,因为它用法比较简洁,Api也比较易懂,对于处理多个任务等都是比较简单的,接来下将对GCD进行总结和探究。

简介

    1. GCD全称是Grand Central Dispatch,纯C语言Api,提供了非常多的强大函数
    1. GCD优势:
      1. GCD是苹果公司为多核的并行运算提出的解决方案
      1. GCD会自动利用更多的CPU内核(如:双核、四核)
      1. GCD自动管理线程的生命周期:创建线程调度任务销毁线程,程序员只需要告诉GCD想要执行什么任务,不需要编写任何线程管理代码

函数

  • 任务:GCD的任务使用block封装,block没有参数没有返回值

执行任务的函数:

  • 异步dispatch_async:不用等待当前语句执行完毕,就可以执行下一条语句
    • 会开启线程执行block的任务
    • 异步是多线程的代名词
  • 同步dispatch_sync:必须等待当前语句执行完毕,才会执行下一条语句
    • 不会开启线程
    • 在当前线程执行block任务

队列

队列分为串行队列并行队列,他们是一个数据结构,都遵循FIFO(先进先出)原则

串行队列

  • 串行队列在同一时间只能执行一个任务,如图所示:

截屏2021-08-08 18.03.50.png

  • 在根据FIFO原则先进先出,所以后面的任务必须等前面的任务执行完毕才能执行,就导致串行队列是顺序执行

并行队列

  • 并行队列是一次可以调度多个任务,但并不一定都能执行,线程的状态必须是runable时才能执行,所以先调度不一定先执行:

截屏2021-08-08 18.03.58.png

  • 如果可以看出,并行在同一时间能执行多个任务

案例分析

    1. 并发异步任务
- (void)textDemo1{
dispatch_queue_t queue = dispatch_queue_create("wushuang.concurrent", DISPATCH_QUEUE_CONCURRENT); //并发队列
NSLog(@"1");
dispatch_async(queue, ^{
NSLog(@"2");
dispatch_sync(queue, ^{
NSLog(@"3");
});
NSLog(@"4");
});
NSLog(@"5");
}

  • 执行顺序是什么呢,分析如下:

    • 首先1在主线程且顺序执行,所以1最新打印
    • 然后进入dispatch_async异步函数的block块,块里先执行2,同步函数会阻塞4的打印,所以快中3在2和4中间
    • 5是看异步函数执行的速度,有可能在1后面,也可能在234后面
  • 所以可能的结果是15234125341235412345

    1. 串行异步任务
- (void)textDemo1{
dispatch_queue_t queue = dispatch_queue_create("wushuang.serial", NULL); //串行队列
NSLog(@"1");
dispatch_async(queue, ^{
NSLog(@"2");
dispatch_sync(queue, ^{
NSLog(@"3");
});
NSLog(@"4");
});
NSLog(@"5");
}

  • 这个顺序回是怎样的呢,再来分析下
    • 首先1先执行
    • 然后执行dispatch_async块中的代码,块中的同步函数次时与第一种不一样,根据串行队列FIFO原则且是顺序执行,所以3执行的前提是dispatch_async函数执行完,但dispatch_async函数执行完的前提是块中dispatch_sync及之后代码能执行完,于是就出现了相互等待,造成了死锁

函数与关系

4种组合

函数与队列可以分为四种组合异步函数串行队列并发队列异步函数同步函数并发队列同步函数串行队列

    1. 异步函数串行队列开启线程,任务一个接着一个
    1. 异步函数并发队列开启线程,在当前线程执行任务,任务执行没有顺序,和cpu调度有关
    1. 同步函数并发队列不会开启线程,在当前线程执行任务,任务一个接着一个
    1. 同步函数串行队列不会开启线程,在当前线程执行任务,任务一个接着一个执行,会产生阻塞

主队列和全局队列

  • 主队列:专门在主线程上调度任务的串行队列不会开启线程,如果当前主线程正在执行任务,那么无论主队列中当前被添加了什么任务,都不会被调度dispatch_get_main_queue()
  • 全局队列:为了方便程序员的使用,苹果提供了全局队列dispatch_get_global_queue(0,0),全局队列是并发队列,在使用多线程时,如果对队列没有特殊要求,在执行异步任务时,可以直接使用全局队列
案例
- (void)viewDidLoad {
[super viewDidLoad];

dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"会来吗?");
});
}

  • ViewDidLoad中执行主线程同步任务,那么会打印吗?结果产生死锁
  • 分析:因为ViewDidLoad,在主线程上,主线程上调度任务的是主队列,主队列遵循FIFO原则,而要执行该同步任务也在主队列执行,所以必须等ViewDidLoad函数执行完才能执行,而ViewDidLoad函数执行完的前提是该同步任务执行完,所以就产生了相互等待,产生死锁

图解总结

截屏2021-08-08 22.17.19.png

队列源码分析

根据队列的介绍,我们知道有2种队列:串行并发,其中主队列是特殊的串行队列,全局队列是特殊的并发队列,那么在他们是怎么区分的呢?我们去打印堆栈看看:

截屏2021-08-09 16.16.42.png

主队列

在源码中搜索dispatch_get_main_queue

/
The main queue is meant to be used in application context to interact with the main thread and the main runloop.

Returns the main queue. This queue is created automatically on behalf of the main thread before main() is called.
*/

dispatch_queue_main_t
dispatch_get_main_queue(void)
{
return DISPATCH_GLOBAL_OBJECT(dispatch_queue_main_t, _dispatch_main_q);
}

  • 根据注释可知:

    1. 主队列与程序的主线程runloop进行交互
    2. 主队列在main()之前程序自动创建的
  • 根据代码可知它的核心是调用了DISPATCH_GLOBAL_OBJECT函数,其中的有两个参数,第一个是类型,再来看看第二个参数_dispatch_main_q,搜索得到:

struct dispatch_queue_static_s _dispatch_main_q = {
DISPATCH_GLOBAL_OBJECT_HEADER(queue_main),
#if !DISPATCH_USE_RESOLVERS
.do_targetq = _dispatch_get_default_queue(true),
#endif
.dq_state = DISPATCH_QUEUE_STATE_INIT_VALUE(1) |
DISPATCH_QUEUE_ROLE_BASE_ANON,
.dq_label = "com.apple.main-thread",
.dq_atomic_flags = DQF_THREAD_BOUND | DQF_WIDTH(1),
.dq_serialnum = 1,
};

  • 根据观察可发现队列的区分可能与dq_atomic_flagsdq_serialnum两个参数有关,值分别为DQF_THREAD_BOUND | DQF_WIDTH(1)1

全局队列

  • 再搜索dispatch_get_global_queue
dispatch_queue_global_t
dispatch_get_global_queue(intptr_t identifier, uintptr_t flags);

dispatch_queue_global_t
dispatch_get_global_queue(intptr_t priority, uintptr_t flags)
{
dispatch_assert(countof(_dispatch_root_queues) ==
DISPATCH_ROOT_QUEUE_COUNT);

if (flags & ~(unsigned long)DISPATCH_QUEUE_OVERCOMMIT) {
return DISPATCH_BAD_INPUT;
}
dispatch_qos_t qos = _dispatch_qos_from_queue_priority(priority);
#if !HAVE_PTHREAD_WORKQUEUE_QOS
if (qos == QOS_CLASS_MAINTENANCE) {
qos = DISPATCH_QOS_BACKGROUND;
} else if (qos == QOS_CLASS_USER_INTERACTIVE) {
qos = DISPATCH_QOS_USER_INITIATED;
}
#endif
if (qos == DISPATCH_QOS_UNSPECIFIED) {
return DISPATCH_BAD_INPUT;
}
return _dispatch_get_root_queue(qos, flags & DISPATCH_QUEUE_OVERCOMMIT);
}

  • identifier可以设置一些优先级,有四种优先级:

    1. DISPATCH_QUEUE_PRIORITY_HIGH
    2. DISPATCH_QUEUE_PRIORITY_DEFAULT
    3. DISPATCH_QUEUE_PRIORITY_LOW
    4. DISPATCH_QUEUE_PRIORITY_BACKGROUND
  • flags是留给将来使用,任务非0值都可能导致NULL,通常传0

  • 这里返回的是_dispatch_get_root_queue函数的调用

static inline dispatch_queue_global_t
_dispatch_get_root_queue(dispatch_qos_t qos, bool overcommit)
{
if (unlikely(qos < DISPATCH_QOS_MIN || qos > DISPATCH_QOS_MAX)) {
DISPATCH_CLIENT_CRASH(qos, "Corrupted priority");
}
return &_dispatch_root_queues[2 * (qos - 1) + overcommit];
}

  • 再继续查看_dispatch_root_queues函数:

截屏2021-08-09 16.53.42.png

此时找到了与dq_atomic_flags中相关参数DQF_WIDTH,传入的值为DISPATCH_QUEUE_WIDTH_POOL,也就是DQF_WIDTH(DISPATCH_QUEUE_WIDTH_FULL - 1),但不能确定dq_serialnum,它的值跟label有关,再来打印下_dispatch_root_queueslabel

globalQueue: <OS_dispatch_queue_global: com.apple.root.default-qos>

  • 于是根据label在源码中确定dq_serialnum10,目前还是不能确定那个队列的区分和哪个参数有关

自定义队列

  • 再来搜索队列的创建dispatch_queue_create,得到:
dispatch_queue_t
dispatch_queue_create(const char *label, dispatch_queue_attr_t attr)
{
return _dispatch_lane_create_with_target(label, attr,
DISPATCH_TARGET_QUEUE_DEFAULT, true);
}

  • 在搜索返回函数_dispatch_lane_create_with_target
static dispatch_queue_t
_dispatch_lane_create_with_target(const char *label, dispatch_queue_attr_t dqa,
dispatch_queue_t tq, bool legacy) // tq NULL, legacy true
{
dispatch_queue_attr_info_t dqai = _dispatch_queue_attr_to_info(dqa); // 面向对象封装

...

dispatch_lane_t dq = _dispatch_object_alloc(vtable,
sizeof(struct dispatch_lane_s)); // 开辟内存
_dispatch_queue_init(dq, dqf, dqai.dqai_concurrent ?
DISPATCH_QUEUE_WIDTH_MAX : 1, DISPATCH_QUEUE_ROLE_INNER |
(dqai.dqai_inactive ? DISPATCH_QUEUE_INACTIVE : 0)); // 初始化

dq->dq_label = label; //label赋值
dq->dq_priority = _dispatch_priority_make((dispatch_qos_t)dqai.dqai_qos,
dqai.dqai_relpri);
if (overcommit == _dispatch_queue_attr_overcommit_enabled) {
dq->dq_priority |= DISPATCH_PRIORITY_FLAG_OVERCOMMIT;
}
if (!dqai.dqai_inactive) {
dispatch_queue_priority_inherit_from_target(dq, tq);
dispatch_lane_inherit_wlh_from_target(dq, tq);
}
_dispatch_retain(tq);
dq->do_targetq = tq;
_dispatch_object_debug(dq, "%s", __func__);
return _dispatch_trace_queue_create(dq)._dq; // 创建痕迹标识,方便查找
}

  • 该函数第一个参数label我们比较熟悉,就是创建的线程的名字
  • _dispatch_queue_attr_to_info传入的第二参数:
dispatch_queue_attr_info_t
_dispatch_queue_attr_to_info(dispatch_queue_attr_t dqa)
{
dispatch_queue_attr_info_t dqai = { }; // 初始为NULL,
if (!dqa) return dqai;

...
}
  • 这里先初始一个dispatch_queue_attr_info_t类型对象,然后在根据dpa类型进行相关赋值,如果dqa为不存在则直接返回,这就是串行可以传NULL的原因
  • 做好相关的准备工作后,接着在调用_dispatch_object_alloc方法对线程开辟内存
  • 再调用初始化函数_dispatch_queue_init,此处第三个参数有判断是否并判断,如果是并发传入为DISPATCH_QUEUE_WIDTH_MAX,串行则传入1,继续查看方法的实现:

截屏2021-08-09 17.32.24.png

  • 此处可以看出又出现了DQF_WIDTH()函数和dq_serialnum,并发DQF_WIDTH(DISPATCH_QUEUE_WIDTH_FULL - 2),串行为DQF_WIDTH(1),但根据队列类型传入的参数只和DQF_WIDTH有关,那么dq_serialnum是什么呢?
  • 搜索_dispatch_queue_serial_numbers
unsigned long volatile _dispatch_queue_serial_numbers =
DISPATCH_QUEUE_SERIAL_NUMBER_INIT;

  • 然后再搜索DISPATCH_QUEUE_SERIAL_NUMBER_INIT
// skip zero
// 1 - main_q
// 2 - mgr_q
// 3 - mgr_root_q
// 4,5,6,7,8,9,10,11,12,13,14,15 - global queues
// 17 - workloop_fallback_q
// we use 'xadd' on Intel, so the initial value == next assigned
#define DISPATCH_QUEUE_SERIAL_NUMBER_INIT 17

  • 根据注释得知1代表 main_14~15 代码 global queues,但能区分队列吗,还得看看os_atomic_inc_orig函数的实现:
#define os_atomic_inc_orig(p, m) \
os_atomic_add_orig((p), 1, m)


#define os_atomic_add_orig(p, v, m) \
os_atomic_c11_op_orig((p), (v), m, add, +)


#define _os_atomic_c11_op_orig(p, v, m, o, op) \
atomic_fetch_##o##_explicit(_os_atomic_c11_atomic(p), v, \
memory_order_##m)


  • 最终得到C++方法atomic_fetch_add_explicit方法,网页搜索:

截屏2021-08-09 17.48.29.png

  • 原来是原子相关的操作,没啥用

总结:
1. 串行队列:DQF_WIDTH(1)
2. 全局队列:DQF_WIDTH(DISPATCH_QUEUE_WIDTH_FULL - 1)
3. 创建的并发队列:DQF_WIDTH(DISPATCH_QUEUE_WIDTH_FULL - 2)

队列的继承

  • 在队列开辟内存时调用的是_dispatch_object_alloc方法,为什么不是_dispatch_dispatch_alloc?接下来根据队列的类型进行分析下

  • 先搜索队列的类型dispatch_queue_t

DISPATCH_DECL(dispatch_queue);

  • 再查看DISPATCH_DECL的实现:
#define DISPATCH_DECL(name) \
typedef struct name##_s : public dispatch_object_s {} *name##_t


  • 根据传入的参数dispatch_queue,得到:
struct dispatch_queue_s : public dispatch_object_s {} *dispatch_queue_t

  • 于是得到队列的继承关系:dispatch_queue_t : dispatch_queue_s : dispatch_object_s
收起阅读 »

Xcode 12 使用技巧

iOS
1 class成员构造函数生成Swift 可以为 struct 合成成员构造函数,但不能为 class 合成。Xcode 可以帮助生成代码,先选中类名,然后选择菜单 Editor —> Refactor —> Generate Memberwise...
继续阅读 »

1 class成员构造函数生成

Swift 可以为 struct 合成成员构造函数,但不能为 class 合成。Xcode 可以帮助生成代码,先选中类名,然后选择菜单 Editor —> Refactor —> Generate Memberwise Initializer。

2 设置App的“外观”

运行 App 到模拟器以后可以找到环境面板,点开它可以设置 Dynamic Type size, 暗黑模式等以查看 App 的变化。

3 选中代码块

选择某个代码块的左侧括号{,然后双击。

4 检查拼写错误

选择 Edit —> Format —> Spelling and Grammar —> Check Spelling While Typing,将检查代码是否有错别字。

5 修复多个错误

程序出现多个错误时,可以选择 Editor —> Fix All Issues 修复多个错误。

6 搜索查看

在 Find navigator 面板里搜索某个内容时,如果出现多个结果,在使用完一个结果时可以使用 Backspace 剔除该结果,这样剩下的都是未操作过的搜索结果。

7 Canvas切换

Canvas 暂停时,按 Opt+Cmd+P 恢复预览。也使用 Opt+Cmd+Return 来完全隐藏画布。

8 模拟器分屏

选中模拟器,进入 Window 菜单,选择 Tile Window To Right Of Screen,然后选择左边的 Xcode 进行屏幕空间分割调整,这样模拟器就一直在右边显示。

9 代码提示宽度

当代码提示出现以后,如果某个方法特别长,可以选中提示面板的边缘,并将其拖动到想要的宽度。

10 快速添加断点

使用 Cmd+\ 在当前行上添加或删除断点。 

11 测试顺序

有时一个测试的输出会影响另一个测试的输入。此时可以进入 Product 菜单,按住 Option,然后点击 Test。在 Info 选项卡中,单击 Options,然后选中 Randomize Execution Order,这样进行测试时每次都会以不同的顺序运行。

12 筛选方法和设备

可以使用 Ctrl+6 快速查看当前文件的方法列表,列表出现以后可以直接输入过滤信息进行方法的筛选,这个操作方式也可以用于模拟器的过滤筛选。

13 查看interface

按住 Ctrl+Cmd+↑,会生成当前文件的 interface,显示当前文件的属性、函数签名和注释。如果存在该文件的测试文件,可以再按一次就会跳转到测试文件。

14 快速补齐文档注释

在某个方法上按住 Option+Cmd+/ 就会生成文档注释。

15 快速查找文件

  • 选中项目或者文件夹,右击选择 Sort By Name,此时文件就会按照 A-Z 的顺序排序。
  • 项目文件的最下方法,有个过滤框,可以输入关键字进行查找。

16 代码变化提醒

Xcode 偏好设置 —> Source Control —> 勾选 Show Source Control changes,然后进行代码的修改,在修改代码的左边会看到一个蓝色的条状提醒,点击它点并选择 Show Change,就会同时显示新旧代码。

17 使用minimap

在浏览长代码时,可以通过 Editor —> Minimap 调出 minimap,方便查看代码。

18 运行最后一次测试

编写失败的测试很常见,Xcode 有一个快捷键可以只运行最后一个测试:Ctrl+Opt+Cmd+G。

19 修改快捷键

Xcode 偏好设置 —> Key Bindings,然后根据需要搜索和修改。

20 查找选项

Show the Find navigator 界面,每个菜单都可以通过点击弹出更多选项,合理搭配可以提高查找的效率。比如可以点击放大镜查看最近的搜索。

21 粘贴代码格式化

有时候从别的地方粘贴代码到项目中时缩进不对,可以使用 Ctrl+I 进行格式化。

22 内购测试

可以在没有 App Store Connect 的情况下测试应用内购买。创建一个新的 StoreKit Config 文件,并添加 IAP。然后进入菜单 Product,按住 Option 然后点击 Run,在弹出窗口的 Options 选项卡中,更改 StoreKit Configuration 为添加的 StoreKit Config 文件,就可以测试添加的 IAP。

23 查看Build Settings含义

一般很难记住 Build Settings 的作用,可以选择其中一项使用 Quick Help 检查器查看大多数 Settings 的文档,或者按住 Option 并双击以获得内联帮助。

24 多文件Canvas预览

当一个视图被分割成不同文件时,Canvas 预览起来有点困难,此时在预览界面,使用底部的图钉来保持当前预览的活动状态,这样可以在预览一个文件的同时更改另一个文件并能及时反馈到预览里。

收起阅读 »

iOS - 数据存储

iOS
Bundle简单理解就是资源文件包,会将许多图片、xib、文本文件组织在一起,打包成一个 Bundle 文件,这样可以在其他项目中引用包内的资源。// 获取当前项目的Bundle let bundle = Bundle.main // 加载资源 let mp...
继续阅读 »

Bundle

简单理解就是资源文件包,会将许多图片、xib、文本文件组织在一起,打包成一个 Bundle 文件,这样可以在其他项目中引用包内的资源。

// 获取当前项目的Bundle
let bundle = Bundle.main

// 加载资源
let mp3 = Bundle.main.path(forResource: "xxx", ofType: "mp3")

沙盒

每一个 App 只能在自己的创建的文件系统(存储区域)中进行文件的操作,不能访问其他 App 的文件系统(存储区域),该文件系统(存储区域)被成为沙盒。所有的非代码文件都要保存在此,例如图像,图标,声音,plist,文本文件等。

沙盒机制保证了 App 的安全性,因为只能访问自己沙盒文件下的文件。

Home目录

沙盒的主目录,可以通过它查看沙盒目录的整体结构。

// 获取程序的Home目录
let homeDirectory = NSHomeDirectory()

Documents目录

保存应用程序运行时生成的持久化数据。可被iTunes备份,可备份到 iCloud。

// 方法1
let documentPaths = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)
let documentPath = documentPaths[0]

// 方法2
let documentPath2 = NSHomeDirectory() + "/Documents"

上面的获取方式最后得到的是String,如果希望获取的是URL,可以通过下面的方式:

let manager = FileManager.default
let urlForDocument = manager.urls(for: .documentDirectory, in:.userDomainMask)
let url: URL = urlForDocument[0]

NSSearchPathForDirectoriesInDomains

  • 访问沙盒目录常用的函数,它返回值为一个数组,在 iOS 中由于只有一个唯一路径,所以直接取数组第一个元素即可。
func NSSearchPathForDirectoriesInDomains(
_ directory: FileManager.SearchPathDirectory,
_ domainMask: FileManager.SearchPathDomainMask,
_ expandTilde: Bool) -> [String]

  • directory:指定搜索的目录名称。
  • domainMask:搜索主目录的位置。userDomainMask 表示搜索的范围限制于当前应用的沙盒目录(参考定义注释)。
  • expandTilde:是否获取完整的路径。
let documentPaths = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, false)
let documentPath = documentPaths[0] // ~/Documents

let documentPaths2 = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)
let documentPath2 = documentPaths2[0] // /Users/yangfan/Library/Developer/XCPGDevices/982B6CBA-747B-4831-9D87-F82160197333/data/Containers/Data/Application/56C657D5-B36B-449D-AC6C-E2417EA65D00/Documents

Library目录

存储程序的默认设置和其他信息,其下有两个重要目录:

  • Library/Preferences 目录:包含应用程序的偏好设置文件。不应该直接创建偏好设置文件,而是应该使用UserDefaults类来取得和设置应用程序的偏好。
  • Library/Caches 目录:主要存放缓存文件,此目录下文件不会在应用退出时删除。
// Library目录-方法1
let libraryPaths = NSSearchPathForDirectoriesInDomains(.libraryDirectory, .userDomainMask, true)
let libraryPath = libraryPaths[0]

// Library目录-方法2
let libraryPath2 = NSHomeDirectory() + "/Library"

// Cache目录-方法1
let cachePaths = NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true)
let cachePath = cachePaths[0]

// Cache目录-方法2
let cachePath2 = NSHomeDirectory() + "/Library/Caches"
  • tmp目录:存储临时文件,当在退出程序或设备重启时,文件会被清除。
// 方法1
let tmpDir = NSTemporaryDirectory()

// 方法2
let tmpDir2 = NSHomeDirectory() + "/tmp"

注意

每次编译代码会生成新的沙盒路径,所以模拟器运行同一个 App 时所得到的沙盒路径是不一样的,但上架的 App 在真机上运行不存在这种情况。

plist读写

class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// 获取本地plist
let path = Bundle.main.path(forResource: "cityData", ofType: "plist")
if let path = path {
let root = NSDictionary(contentsOfFile: path) // 借助于NSDictionary
// print(root!.allKeys)
// print(root!.allKeys[31])
// 获取所有数据
let cities = root![root!.allKeys[31]] as! NSArray // 借助于NSArray
// print(cities)
// 沙盒路径
let documentDir = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first
let filePath = documentDir! + "/localData.plist"
// 写入沙盒
cities.write(toFile: filePath, atomically: true)
}
}
}

偏好设置

  • 一般用于保存如用户名、密码、版本等轻量级数据。
  • 通过UserDefaults来设置和读取偏好设置。
  • 偏好设置以key-value的方式进行读写操作。
  • 默认情况下数据自动以plist形式存储在沙盒的Library/Preferences目录。

案例

  • 记住密码
class ViewController: UIViewController {
@IBOutlet weak var username: UITextField!
@IBOutlet weak var password: UITextField!
@IBOutlet weak var swit: UISwitch!
// UserDefaults
let userDefaults = UserDefaults.standard

override func viewDidLoad() {
super.viewDidLoad()

// 取出存储的数据
let name = userDefaults.string(forKey: "name")
let pwd = userDefaults.string(forKey: "pwd")
let isOn = userDefaults.standard.bool(forKey: "isOn")
// 填充输入框
username.text = name
password.text = pwd
// 设置开关状态
swit.isOn = isOn
}

@IBAction func login(_ sender: Any) {
print("密码已经记住")
}

@IBAction func remember(_ sender: Any) {
let swit = sender as! UISwitch
// 如果记住密码开关打开
if swit.isOn {
let name = username.text
let pwd = password.text
// 存储用户名和密码
userDefaults.set(name, forKey: "name")
userDefaults.set(pwd, forKey: "pwd")
// 同时存储开关的状态
userDefaults.set(swit.isOn, forKey: "isOn")
// 最后进行同步
userDefaults.synchronize()
}
}
}
  • 新特性界面
class SceneDelegate: UIResponder, UIWindowSceneDelegate { 
var window: UIWindow?
// 当前版本号
var currentVersion: Double!
// UserDefaults
let userDefaults = UserDefaults.standard

func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {

guard let windowScene = (scene as? UIWindowScene) else { return }
window = UIWindow(windowScene: windowScene)
if isNewVersion {
// 新特性界面
let newVC = UIViewController()
newVC.view.backgroundColor = .green
window?.rootViewController = newVC
// 存储当前版本号
userDefaults.set(currentVersion, forKey: "localVersion")
userDefaults.synchronize()
} else {
// 主界面
let mainVC = UIViewController()
mainVC.view.backgroundColor = .red
window?.rootViewController = mainVC
}

window?.makeKeyAndVisible()
}
}

extension SceneDelegate {
// 是否新版本
private var isNewVersion: Bool {
// 获取当前版本号
currentVersion = Double(Bundle.main.infoDictionary!["CFBundleShortVersionString"] as! String)!
// 本地版本号
let localVersion = userDefaults.double(forKey: "localVersion")
// 比较大小
return currentVersion > localVersion
}
}

默认值

如果需要在使用时设置 UserDefaults 的默认值,可以使用register方法。

enum Keys: String {
case name // 名字
case isRem // 记住密码
}

// 设置默认值
UserDefaults.standard.register(defaults: [
Keys.name.rawValue: "UserA",
Keys.isRem.rawValue: false
])

注意:在设置默认值后如果修改了其中的属性值,即使再次执行register方法也不会重置。

跨域

一般情况下使用UserDefaults.standard没有太大问题,但当 App 足够复杂时就会产生几个问题:

  • 需要保证设置数据 key 具有唯一性,防止产生冲突。
  • 同一个 plist 文件越来越大造成的读写效率降低。
  • 无法便捷的清除特定的偏好设置数据。

因此还有另外一种获取 UserDefaults 对象的方法:UserDefaults(suiteName: String?),可以根据传入的 suiteName 参数进行处理:

  • 传入 nil:等同于UserDefaults.standard
  • 传入 App Groups 的 ID:操作共享目录中的 plist 文件,以便在跨 App 或宿主 App 与扩展应用之间(如 App 与 Widget)共享数据。
  • 传入其他值:操作Documents/Library/Preferences目录下以suiteName命名的 plist 文件。

可以通过如下的方式删除指定suiteName的 plist 文件里的全部数据。

let userDefaults = UserDefaults(suiteName: "abc")
userDefaults?.removePersistentDomain(forName: "abc")

归档与反归档

  • 归档(序列化)是把对象转为Data,反归档(反序列化)是从Data还原出对象。
  • 可以存储自定义数据。
  • 存储的数据需要继承自NSObject并遵循NSSecureCoding协议。

案例

  • 自定义对象
class Person: NSObject, NSSecureCoding {   
var name:String?
var age:Int?

override init() {
}

static var supportsSecureCoding: Bool = true

// 编码- 归档调用
func encode(with aCoder: NSCoder) {
aCoder.encode(age, forKey: "age")
aCoder.encode(name, forKey: "name")
}

// 解码-反归档调用
required init?(coder aDecoder: NSCoder) {
super.init()
age = aDecoder.decodeObject(forKey: "age") as? Int
name = aDecoder.decodeObject(forKey: "name") as? String
}
}
  • 归档与反归档
class ViewController: UIViewController {
var data: Data!
var origin: Person!

override func viewDidLoad() {
super.viewDidLoad()
}

// 归档
@IBAction func archiver(_ sender: Any) {
let p = Person()
p.age = 20
p.name = "zhangsan"

do {
try data = NSKeyedArchiver.archivedData(withRootObject: p, requiringSecureCoding: true)
} catch {
print(error)
}
}

// 反归档
@IBAction func unarchiver(_ sender: Any) {
do {
try origin = NSKeyedUnarchiver.unarchivedObject(ofClass: Person.self, from: data)
print(origin!.age!)
print(origin!.name!)
} catch {
print(error)
}
}
}

数据库—sqlite3

由于 Swift 直接操作 sqlite3 非常不方便,所以借助于SQLite.swift的框架。

  • Model
struct Person {    
var name : String = ""
var phone : String = ""
var address : String = ""
}
  • DBTools
import SQLite

struct DBTools {
// 数据库路径
let dbPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first! + "/person.db"
// 数据库连接
var db: Connection!
// 表名与字段
let personTable = Table("t_person") // 表名
let personID = Expression<Int>("id") // id
let personName = Expression<String>("name") // name
let personPhone = Expression<String>("phone") // phone
let personAddress = Expression<String>("address") // address

// MARK: - 构造函数,数据库有则连接 没有就创建后连接
init() {
do {
db = try Connection(dbPath)
print("数据库创建/连接成功")
} catch {
print("数据库创建/连接失败")
}
}

// MARK: - 创建表格,表若存在不会再次创建,直接进入catch
func createTable() {
// 创表
do {
try db.run(personTable.create(block: { t in
t.column(personID, primaryKey: .autoincrement)
t.column(personName)
t.column(personPhone)
t.column(personAddress)
}))
print("数据表创建成功")
} catch {
print("数据表创建失败")
}
}

// MARK: - 插入数据
func insertPerson(person: Person) {
let insert = personTable.insert(personName <- person.name, personPhone <- person.phone, personAddress <- person.address)
// 插入
do {
try db.run(insert)
print("插入数据成功")
} catch {
print("插入数据失败")
}
}

// MARK: - 删除数据
func deletePerson(name: String) {
// 筛选数据
let p = personTable.filter(personName == name)
// 删除
do {
let row = try db.run(p.delete())
if row == 0 {
print("暂无数据删除")
} else {
print("数据删除成功")
}
} catch {
print("删除数据失败")
}
}

// MARK: - 更新数据
func updatePerson(person: Person) {
// 筛选数据
let p = personTable.filter(personName == person.name)
// 更新
do {
let row = try db.run(p.update(personPhone <- person.phone, personAddress <- person.address))
if row == 0 {
print("暂无数据更新")
} else {
print("数据更新成功")
}
} catch {
print("数据更新失败")
}
}

// MARK: - 查询数据
func selectPerson() -> [Person]? {
// 保存查询结果
var response: [Person] = []
// 查询
do {
let select = try db.prepare(personTable)
for person in select {
let p = Person(name: person[personName], phone: person[personPhone], address: person[personAddress])
response.append(p)
}

if !response.isEmpty {
print("数据查询成功")
} else {
print("对不起,暂无数据")
}
return response
} catch {
print("数据查询失败")
return nil
}
}
}
  • ViewController
class ViewController: UIViewController {    
var dbTools: DBTools?

override func viewDidLoad() {
super.viewDidLoad()
}

@IBAction func createDB(_ sender: Any) {
dbTools = DBTools()
}

@IBAction func createTab(_ sender: Any) {
dbTools?.createTable()
}

@IBAction func insertData(_ sender: Any) {
let p = Person(name: "zhangsan", phone: "18888888888", address: "AnHuiWuhu")
dbTools?.insertPerson(person: p)
}

@IBAction func deleteData(_ sender: Any) {
dbTools?.deletePerson(name: "zhangsan")
}

@IBAction func updateData(_ sender: Any) {
let p = Person(name: "zhangsan", phone: "17777777777", address: "JiangSuNanJing")
dbTools?.updatePerson(person: p)
}

@IBAction func selectData(_ sender: Any) {
let person = dbTools?.selectPerson()
if let person = person {
for p in person {
print(p)
}
}
}
}
收起阅读 »

iOS - 触摸与手势识别

iOS
触摸概念UITouch用于描述触摸的窗口、位置、运动和力度。一个手指触摸屏幕,就会生成一个 UITouch 对象,如果多个手指同时触摸,就会生成多个 UITouch 对象。属性 (1)window:触摸时所处的 UIWindow。 (2)view:触摸时所处的...
继续阅读 »

触摸

概念

UITouch

用于描述触摸的窗口、位置、运动和力度。一个手指触摸屏幕,就会生成一个 UITouch 对象,如果多个手指同时触摸,就会生成多个 UITouch 对象。

  • 属性 (1)window:触摸时所处的 UIWindow。 (2)view:触摸时所处的 UIView。 (3)tapCount:短时间内点按屏幕的次数。可据此判断单击和双击操作。 (4)timestamp:时间戳,单位秒。记录了触摸事件产生或变化时的时间。 (5)phase:触摸事件的周期,即触摸开始、触摸点移动、触摸结束和中途取消。

  • 方法

// 返回一个CGPoint类型的值,表示触摸在view上的位置。
// 返回的位置是针对view的坐标系。
// 调用时传入的view参数为空的话,返回的是触摸点在整个窗口的位置 。
open func location(in view: UIView?) -> CGPoint

// 该方法记录了前一个坐标值,返回值的含义与上面一样。
open func previousLocation(in view: UIView?) -> CGPoint

UIEvent

一个完整的触摸操作是一个 UIEvent,它包含一组相关的 UITouch 对象,可以通过 UIEvent 的allTouches属性获得 UITouch 的集合。

UIResponder

  • 响应者对象。
  • 只有继承了 UIResponder 的对象才能接收并处理触摸事件。
  • AppDelegate、UIApplication、UIWindow、UIViewController、UIView 都继承自 UIResponder,因此它们都是响应者对象,都能够接收并处理触摸事件。
  • 响应者通过下列几个方法来响应触摸事件。
// 手指触碰屏幕,触摸开始
open func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?)
// 手指在屏幕上移动
open func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?)
// 手指离开屏幕,触摸结束
open func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?)
// 触摸结束前,某个系统事件中断了触摸,如电话来电
open func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?)

触摸事件传递与响应

当触摸事件产生以后,App 里有很多的 UIView 或 UIViewController,到底应该谁去响应这个事件呢?在响应之前,必须要找到那个最合适的对象(最佳响应者),这个过程称之为事件传递或寻找最佳响应者(Hit-Testing)。

事件传递

  1. 当 iOS 程序中发生触摸事件后,系统会将事件加入到 UIApplication 管理的一个任务队列中。
  2. UIApplication 取出最前面的事件传递给 UIWindow。
  3. UIWindow 接收到事件后,首先判断自己能否响应触摸事件。如果能,那么 UIWindow 会从后往前遍历自己的子 UIView,将事件向下传递。
  4. 遍历每一个子 UIView 时,都会重复上面的操作(判断能否响应触摸事件,能则继续遍历子 UIView,直到找到一个 UIView)直到找到最合适的 UIView。如果没有找到合适的,那么事件不再往下传递,而当前 UIView 就是最合适的对象。

两个方法

寻找最佳响应者的原理是什么?需要借助以下两个方法。

// 寻找最佳响应者的核心方法,传递事件的桥梁
// 1. 判断点是否在当前view的内部(即调用第二个方法)
// 2. 如果在(即返回true)则遍历其子UIView继续
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
}

// 判断点是否在这个View的内部
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
}

  • UIApplication 调用 UIWindow 的hitTest方法将触摸事件传递给 UIWindow,如果 UIWindow 能够响应触摸事件,则调用其子 UIView 的hitTest方法将事件传递给其子 UIView,这样循环寻找与传递下去,直到获取最佳响应者。
  • 通过这两个方法可以做很多事情,其中一个经典的案例是自定义中间有凸起按钮的 UITabBar。此时需要重写 UITabBar 的point方法,判断当前触摸位置是否在中间凸起按钮的坐标范围内,如果在返回 true。这样可以让触摸事件传递到凸起按钮,并让其成为最佳响应者。

事件响应

  1. 当找到最合适的响应者之后,响应者对于触摸事件,有以下 3 种操作: (1)不拦截,事件会沿着默认的响应链自动传递。(默认操作) (2)拦截,事件不再往上传递,重写touchesBegan方法,但不调用父类的touchesBegan方法。 (3)不拦截,事件继续往上传递,重写touchesBegan方法,并调用父类的touchesBegan方法,即super.touchesBegan(touches, with: event)

  2. 响应者对于触摸事件的响应和传递都是在touchesBegan方法中完成的。该方法默认是将事件顺着响应者链向上传递,即将事件交给上一个响应者进行处理。每一个响应者对象都有一个next属性,用来获取下一个响应者。默认的next对象为: (1)UIView:若当前响应者是 UIViewController 的view,则next是 UIViewController,否则上一个响应者是其父 UIView。 (2)UIViewController:若当前响应者是 UIWindow 的rootViewController,则next是 UIWindow;若是被 present 显示的则nextpresentingViewController。 (3)UIWindow:next为 UIApplication。 (4)UIApplication:next为 AppDelegate。 (5)AppDelegate:next为 nil。

事件不响应的原因

  1. 触摸点不在当前范围内。
  2. alpha < 0.01,透明度小于 0.01。
  3. hidden = true,隐藏不可见。
  4. userInteractionEnabled = false,不允许交互。

手势识别

类型

  • UITapGestureRecognizer:轻点手势识别。
  • UILongPressGestureRecognizer:长按手势识别。
  • UIPinchGestureRecognizer:捏合手势识别。
  • UIRotationGestureRecognizer:旋转手势识别。
  • UISwipeGestureRecognizer:轻扫手势识别。
  • UIPanGestureRecognizer:拖动手势识别。
  • UIScreenEdgePanGestureRecognizer:屏幕边缘拖动手势识别。

使用步骤

  1. 创建手势实例,指定回调方法,当手势开始,改变、或结束时,回调方法被调用。
  2. 将手势添加到需要的 UIView 上。每个手势只对应一个 UIView,当屏幕触摸在当前 UIView 里时,如果手势和预定的一样,回调方法就会调用。
  3. 手势可以通过 storyboard 或者纯代码使用。
class ViewController: UIViewController {
@IBOutlet var blueView: UIView!

override func viewDidLoad() {
super.viewDidLoad()

// 创建手势
let tap = UITapGestureRecognizer(target: self, action: #selector(gesture))
// UITapGestureRecognizer可以设置tap次数
tap.numberOfTapsRequired = 2

let longPress = UILongPressGestureRecognizer(target: self, action: #selector(gesture))

let pinch = UIPinchGestureRecognizer(target: self, action: #selector(gesture))

let rotate = UIRotationGestureRecognizer(target: self, action: #selector(gesture))

let swipe = UISwipeGestureRecognizer(target: self, action: #selector(gesture))
// UISwipeGestureRecognizer需要设置direction
swipe.direction = .right

let pan = UIPanGestureRecognizer(target: self, action: #selector(gesture))

let edgePan = UIScreenEdgePanGestureRecognizer(target: self, action: #selector(gesture))
// UIScreenEdgePanGestureRecognizer需要设置edges
edgePan.edges = UIRectEdge.all

// 添加手势
blueView.addGestureRecognizer(edgePan)
}

@objc func gesture(gestureRecognizer: UIGestureRecognizer) {
print(#function)
}
}

代理

class ViewController: UIViewController {    
@IBOutlet weak var blueView: UIView!

override func viewDidLoad() {
super.viewDidLoad()

let gestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(gesture))
// 设置代理
gestureRecognizer.delegate = self
// 添加手势
blueView.addGestureRecognizer(gestureRecognizer)
}

@objc func gesture(gestureRecognizer:UIGestureRecognizer){
print(#function)
}
}

extension ViewController: UIGestureRecognizerDelegate {
// 手势识别器是否解释此次手势
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
if gestureRecognizer.state == .possible {
print("手势开始")
return true
}
else if gestureRecognizer.state == .cancelled {
print("手势结束")
return true
}
return true
}
}

注意

  1. 一个手势只能对应一个 UIView,但是一个 UIView 可以有多个手势。
  2. 继承自 UIControl 的 UIView 都可以通过 Target-Action 方式添加事件,如果同时给它们添加手势识别, 则 Target-Action 的行为会失效,因为手势识别的优先级更高。
收起阅读 »

iOS14开发- 国际化

iOS
介绍如果 App 需要给不同语言的用户使用,需要进行国际化处理。如果 App 需要进行国际化,在开发之初就需要考虑,在开发时统一使用某一种语言(建议英文),待开发完成以后再进行国际化处理。配置国际化语言在进行国际化之前,必须要添加需要国际化的语言,选中国际化的...
继续阅读 »

介绍

如果 App 需要给不同语言的用户使用,需要进行国际化处理。如果 App 需要进行国际化,在开发之初就需要考虑,在开发时统一使用某一种语言(建议英文),待开发完成以后再进行国际化处理。

配置国际化语言

在进行国际化之前,必须要添加需要国际化的语言,选中国际化的项目 —> PROJECT —> Info —> Localizations,点击+添加需要的国际化语言(默认已经存在英文)。

App名国际化

  1. 新建一个Strings File,必须命名为InfoPlist.strings
  2. 在 Xcode 的右侧文件检查器中找到Localization,点击Localize...,然后勾选配置的国际化语言。
  3. InfoPlist.strings左侧多了一个箭头,点击箭头可以展开,Strings File里面都是形如Key = Value的键值对,操作时一定要保证多个国际化文件中Key的一致性
  4. 设置 App 名字的 Key 为"CFBundleName"
// 英文名
"CFBundleName" = "I18N";

// 中文名
"CFBundleName" = "国际化";

文本国际化

  1. 新建一个Strings File,必须命名为Localizable.strings
  2. 在 Xcode 的右侧文件检查器中找到Localization,点击Localize...,然后勾选配置的国际化语言。
  3. Localizable.strings的各个国际化版本中写上需要国际化文本的Key = Value对。
"title" = "Info";
"message" = "This is a Dialog";
"btnTitle" = "Cancel";

"title" = "温馨提示";
"message" = "这是一个对话框";
"btnTitle" = "取消";

  1. 在需要的地方使用NSLocalizedString(key, comment)读取,其中第一个参数就是上面的key
NSLocalizedString("title", comment: "")
NSLocalizedString("message", comment: "")
NSLocalizedString("btnTitle", comment: "")

图片国际化

图片和文本国际化的使用方式一样,首先在Localizable.strings中进行图片名称的设置,然后通过NSLocalizedString(key, comment)来读取图片名,再根据不同的图片名获取不同的图片。

let imageName = NSLocalizedString("img", comment: "")
let image = UIImage(named: imageName)
imageView.image = image

storyboard/xib国际化

  1. 二者使用方式几乎一样,以 storyboard 为例。
  2. 配置国际化语言时,会弹出选择需要国际化的 storyboard 的对话框,选择以后对应的 storyboard 左侧就会多一个箭头,点击箭头可以展开,里面有storyboard名.stringsStrings File
  3. 选中storyboard名.strings,在 Xcode 的右侧文件检查器中找到Localization,点击Localize...
  4. storyboard名.strings文件对应位置填写相应的国际化信息。
/* Class = "UIButton"; normalTitle = "Button"; ObjectID = "bRH-7c-qiE"; */
"bRH-7c-qiE.normalTitle" = "Button";

/* Class = "UILabel"; text = "UserName"; ObjectID = "dp4-Bf-56s"; */
"dp4-Bf-56s.text" = "UserName";

/* Class = "UITextField"; placeholder = "Please input your name"; ObjectID = "fen-IE-aUn"; */
"fen-IE-aUn.placeholder" = "Please input your name";
/* Class = "UIButton"; normalTitle = "Button"; ObjectID = "bRH-7c-qiE"; */
"bRH-7c-qiE.normalTitle" = "登录";

/* Class = "UILabel"; text = "UserName"; ObjectID = "dp4-Bf-56s"; */
"dp4-Bf-56s.text" = "用户名";

/* Class = "UITextField"; placeholder = "Please input your name"; ObjectID = "fen-IE-aUn"; */
"fen-IE-aUn.placeholder" = "请输入用户名";

注意:如果在弹出的对话框中没有勾选 storyboard,也可以选中 storyboard 文件,再在 Xcode 的右侧文件检查器中找到Localization,点击Localize...,选择 Base,点击Localize,然后在Localization中勾选需要的国际化语言,会生成各个国际化语言的Strings File,最后进行国际化信息的填充。

收起阅读 »

iOS 开发Tips

iOS
开发Tips关于Xcode 12的Tab贡献者:highway不知道有多少同学困惑于Xcode 12的新tab模式,反正我是觉得这种嵌套的tab形式还不如旧版简洁明了。想切回旧版本tab模式的,可以按照此文操作: How to fix the inc...
继续阅读 »

开发Tips

关于Xcode 12的Tab

贡献者:highway

不知道有多少同学困惑于Xcode 12的新tab模式,反正我是觉得这种嵌套的tab形式还不如旧版简洁明了。

想切回旧版本tab模式的,可以按照此文操作: How to fix the incomprehensible tabs in Xcode 12

通过实验发现,Xcode 12下的“子tab”有以下几个特点:

A.当单击文件打开时,tab将显示为斜体,如果双击,则以普通字体显示。斜体表示为“临时”tab,普通字体表示为“静态”tab;

B.双击tab顶部文件名,或者对“临时”tab编辑后,“临时”tab将切换为“静态”tab;

C.如果当前位于“静态”tab,新打开的文件会新起一个tab,并排在当前tab之后;

D.新打开的“临时”文件会在原有的“临时”tab中打开,而不会新起一个“临时”tab;

E.使用Command + Shift + O打开的是“临时”文件。

modalPresentationCapturesStatusBarAppearance

贡献者:beatman423

这边遇到的问题是非全屏present一个导航控制器的时候,咋也控制不了这个导航控制器以及其子控制器的状态栏的style和hidden。后来找到了UIViewController的这个属性,将其设置为YES就可以了。

该属性的描述是:

Specifies whether a view controller, presented non-fullscreen, takes over control of status bar appearance from the presenting view controller. Defaults to NO.

那些Bug

fishhook在某些场景下只生效一次

贡献者:皮拉夫大王在此

问题背景

之前我们监控到钥匙串的API在主线程访问时存在卡死的情况,因此hook 相关API,将访问移到子线程。因此使用到fishook,当时测试并没有发现异常。

问题描述

前段时间在做技术优化时发现我们的hook代码只生效了一次,下次访问API时变成了直接访问系统原方法。

问题原因

由于hook之前没有调用过钥匙串API,因此可能此时并没有做bind,在我们hook后bind信息被替换成我们的函数,因此首次调用hook成功。但是在hook方法中我们又调用了原函数,此时触发了bind,内存中的函数地址又被替换成系统函数,因此第二次调用时hook失败。 解决方案:见https://github.com/facebook/fishhook/issues/36

编程概念

整理编辑:师大小海腾zhangferry

什么是 Homebrew

Homebrew 是一款 Mac OS 平台下的软件包管理工具,拥有安装、卸载、更新、查看、搜索等很多实用的功能。简单的一条指令,就可以实现包管理,而不用你关心各种依赖和文件路径的情况,十分方便快捷。

安装方法:

$ /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

国内镜像:

$ /bin/bash -c "$(curl -fsSL https://gitee.com/cunkai/HomebrewCN/raw/master/Homebrew.sh)"

什么是 Ruby

Ruby 是一种开源的面向对象程序设计的服务器端脚本语言,在 20 世纪 90 年代中期由日本的松本行弘设计并开发。在 Ruby 社区,松本也被称为马茨(Matz)。

Ruby的设计和Objective-C有些类似,都是受Smalltalk的影响。而这也一定程度促进了iOS开发工具较为广泛的使用Ruby写成。

较为知名的几个由Ruby写成的iOS开发工具有:CocoaPods、Fastlane、xcpretty。那这些库为啥使用Ruby开来发呢?

来自CocoaPods的主要作者Eloy Duran的说法:

Ruby和Objective-C具有很多来自Smalltalk的特性,有一定相似性;使用Ruby可以在Bundler和RubyGem之间分享代码;早期阶段MacRuby提供了很多解析Xcode projects的方法;作为CLI工具,Ruby具有强大的字符串处理能力。

来自Fastlane工具链的作者之一Felix的说法:

已经有部分iOS工具选择了Ruby,像是CocoaPods以及给Fastlane的开发带来灵感的nomad-cli。使用Ruby将会更容易与这些工具进行对接。

参考来源:A History of Ruby inside iOS Development

什么是 Rails

Rails(也叫Ruby on Rails)框架首次提出是在 2004 年 7 月,它的研发者是 26 岁的丹麦人 David Heinemeier Hansson。Rails 是使用 Ruby 语言编写的 Web 应用开发框架,目的是通过解决快速开发中的共通问题,简化 Web 应用的开发。与其他编程语言和框架相比,使用 Rails 只需编写更少代码就能实现更多功能。有经验的 Rails 程序员常说,Rails 让 Web 应用开发变得更有趣。

Rails的两大哲学是:不要自我重复(DRY),多约定,少配置。

松本行弘说过:Ruby能拥有现在的人气,基本上都是Ruby on Rails所作出的贡献。

什么是 rbenv

rbenv 和 RVM 都是目前流行的 Ruby 环境管理工具,它们都能提供不同版本的 Ruby 环境管理和切换。

进行 Ruby 版本管理的时候更推荐 rbenv 的方式,你也可以参考 rbenv 官方的 Why choose rbenv over RVM?,当前 rbenv 有两种安装方式:

手动安装

git clone https://github.com/rbenv/rbenv.git ~/.rbenv
# 用来编译安装 ruby
git clone https://github.com/rbenv/ruby-build.git ~/.rbenv/plugins/ruby-build
# 用来管理 gemset, 可选, 因为有 bundler 也没什么必要
git clone git://github.com/jamis/rbenv-gemset.git ~/.rbenv/plugins/rbenv-gemset
# 通过 rbenv update 命令来更新 rbenv 以及所有插件, 推荐
git clone git://github.com/rkh/rbenv-update.git ~/.rbenv/plugins/rbenv-update
# 使用 Ruby China 的镜像安装 Ruby, 国内用户推荐
git clone git://github.com/AndorChen/rbenv-china-mirror.git ~/.rbenv/plugins/rbenv-china-mirror

homebrew安装

$ brew install rbenv

配置

安装完成后,把以下的设置信息放到你的 Shell 配置文件里面,以保证每次打开终端的时候都会初始化 rbenv。

export PATH="$HOME/.rbenv/bin:$PATH" 
eval "$(rbenv init -)"
# 下面这句选填
export RUBY_BUILD_MIRROR_URL=https://cache.ruby-china.com

配置Ruby 环境,需重开一个终端。

$ rbenv install --list               # 列出所有 ruby 版本
$ rbenv install 2.7.3 # 安装 2.7.3版本
$ rbenv versions # 列出安装的版本
$ rbenv version # 列出正在使用的版本
# 下面三个命令可以根据需求使用
$ rbenv global 2.7.3 # 默认使用 1.9.3-p392
$ rbenv shell 2.7.3 # 当前的 shell 使用 2.7.3, 会设置一个`RBENV_VERSION` 环境变量
$ rbenv local 2.7.3 # 当前目录使用 2.7.3, 会生成一个 `.rbenv-version` 文件

什么是 RubyGems

The RubyGems software allows you to easily download, install, and use ruby software packages on your system. The software package is called a “gem” which contains a packaged Ruby application or library.

RubyGems 是 Ruby 的一个依赖包管理工具,管理着 Gem。用 Ruby 编写的工具或依赖包都称为 Gem。

RubyGems 还提供了 Ruby 组件的托管服务,可以集中式的查找和安装 library 和 apps。当我们使用 gem install 命令安装 Gem 时,会通过 rubygems.org 来查询对应的 Gem Package。而 iOS 日常中的很多工具都是 Gem 提供的,例如 Bundler,fastlane,jazzy,CocoaPods 等。

在默认情况下 Gems 总是下载 library 的最新版本,这无法确保所安装的 library 版本符合我们预期。因此还需要 Gem Bundler 配合。

参考:版本管理工具及 Ruby 工具链环境

什么是 Bundler

Bundler 是一个管理 Gem 依赖的 Gem,用来检查和安装指定 Gem 的特定版本,它可以隔离不同项目中 Gem 的版本和依赖环境的差异。

你可以执行 gem install bundler 命令安装 Bundler,接着执行 bundle init 就可以生成一个 Gemfile 文件,你可以在该文件中指定 CocoaPods 和 fastlane 等依赖包的特定版本号,比如:

source "https://rubygems.org"
gem "cocoapods", "1.10.0"
gem "fastlane", "> 2.174.0"

然后执行 bundle install 来安装 Gem。 Bundler 会自动生成一个 Gemfile.lock 文件来锁定所安装的 Gem 的版本。

这一步只是安装指定版本的 Gem,使用的时候我们需要在 Gem 命令前增加 bundle exec,以保证我们使用的是项目级别的 Gem 版本(也就是 Gemfile.lock 文件中锁定的 Gem 版本),而不是操作系统级别的 Gem 版本。

$ bundle exec pod install
$ bundle exec fastlane beta

参考:iOS开发进阶

优秀博客

整理编辑:皮拉夫大王在此

1、我在Uber亲历的最严重的工程灾难 -- 来自公众号:infoQ

准备或者已经接入Swfit可以先了解下

2、美团 iOS 工程 zsource 命令背后的那些事儿 -- 来自公众号: 美团技术团队

美团技术团队历史文章,对DWARF文件的另一种应用。文章还原了作者解决问题的思路历程,除了技术本身外,解决问题的思路历程也是值得借鉴的。

3、NSObject方法调用过程详细分析 -- 来自掘金:maniac_kk

字节跳动maniac_kk同学的一篇优质文章,无论深度还是广度都是非常不错的,很多底层知识融会贯通,值得细细品味

4、iOS疑难Crash的寄存器赋值追踪排查技术 -- 来自简书:欧阳大哥

在缺少行号信息时如何通过寄存器赋值推断出具体的问题代码,具有很高的参考价值,在遇到疑难问题时可以考虑是否能借鉴此思路

5、抖音 iOS 工程架构演进 -- 来自掘金:字节跳动技术团队

业务的发展引起工程架构做出调整,文章介绍了抖音的工程架构演进历程。作为日活过亿的产品,其工程架构的演变对多数APP来说都具有一定的借鉴意义。

6、Swift的一次函数式之旅 -- 来自公众号:搜狐技术产品

编程本身是抽象的,编程范例就是我们如何抽象这个世界的方法,而函数式编程就是其中一个编程范例。在函数式编程的世界里一切皆函数,那如何利用这个思想解决实际问题呢?文中给出了两个有趣的例子,希望可以帮你解决对函数式编程的疑惑。

7、Category无法覆写系统方法? -- 来自公众号:iOS成长之路

这是一次非常有趣的解决问题经历,以至于我认为解决方式可能比问题本身更有意思。解决完全没有头绪的问题,我们应该避免陷入不断的猜测和佐证中。深挖问题,找到正确方向才更容易出现转机。

学习资料

整理编辑:Mimosa

1、CS-Notes

该「Notes」包含技术面试必备基础知识、Leetcode、计算机操作系统、计算机网络、系统设计、Java、Python、C++等内容,知识结构简练,内容扎实。该仓库的内容皆为作者及 Contributors 的原创,目前在 Github 上获 126k Stars。

2、Learn Git Branching

入门级的 Git 使用教程,用图形化的方式来介绍 Git 的各个命令,每一关都有一个小测试来巩固知识点。编者自己过了一遍了,体验很不错,同时填补了我自己一些 Git 知识上的漏洞和误区。

开发利器

整理编辑:brave723

OpenInTerminal

地址:https://github.com/Ji4n1ng/OpenInTerminal

软件状态:免费,开源

使用介绍

OpenInTerminal 是一款开发辅助工具,可以增强 Finder 工具栏以及右键菜单增加在当前位置打开终端的功能。另外还支持:在编辑器中打开当前目录以及在编辑器中打开选择的文件夹或文件

核心功能
  • 在终端(或编辑器)中打开目录或文件
  • 打开自定义应用
  • 支持 终端iTerm

SnippetsLib

地址:https://apps.apple.com/cn/app/snippetslab/id1006087419?mt=12

软件状态:$9.99

使用介绍

SnippetsLab是一款mac代码片段管理工具,使用SnippetsLab可以提高工作效率。它可以帮助您收集和组织有价值的代码片段,您可以随时轻松访问它们

收起阅读 »

iOS 14开发-网络

iOS
基础知识App如何通过网络请求数据?App 通过一个 URL 向特定的主机发送一个网络请求加载需要的资源。URL 一般是使用 HTTP(HTTPS)协议,该协议会通过 IP(或域名)定位到资源所在的主机,然后等待主机处理和响应。主机通过本次网络请求指...
继续阅读 »

基础知识

App如何通过网络请求数据?

客户服务器模型

  1. App 通过一个 URL 向特定的主机发送一个网络请求加载需要的资源。URL 一般是使用 HTTP(HTTPS)协议,该协议会通过 IP(或域名)定位到资源所在的主机,然后等待主机处理和响应。
  2. 主机通过本次网络请求指定的端口号找到对应的处理软件,然后将网络请求转发给该软件进行处理(处理的软件会运行在特定的端口)。针对 HTTP(HTTPS)请求,处理的软件会随着开发语言的不同而不同,如 Java 的 Tomcat、PHP 的 Apache、.net 的 IIS、Node.js 的 JavaScript 运行时等)
  3. 处理软件针对本次请求进行分析,分析的内容包括请求的方法、路径以及携带的参数等。然后根据这些信息,进行相应的业务逻辑处理,最后通过主机将处理后的数据返回(返回的数据一般为 JSON 字符串)。
  4. App 接收到主机返回的数据,进行解析处理,最后展示到界面上。
  5. 发送请求获取资源的一方称为客户端。接收请求提供服务的一方称为服务端

基本概念

URL

  • Uniform Resource Locator(统一资源定位符),表示网络资源的地址或位置。
  • 互联网上的每个资源都有一个唯一的 URL,通过它能找到该资源。
  • URL 的基本格式协议://主机地址/路径

HTTP/HTTPS

  • HTTP—HyperTextTransferProtocol:超文本传输协议。
  • HTTPS—Hyper Text Transfer Protocol over Secure Socket Layer 或 Hypertext Transfer Protocol Secure:超文本传输安全协议。

请求方法

  • 在 HTTP/1.1 协议中,定义了 8 种发送 HTTP 请求的方法,分别是GET、POST、HEAD、PUT、DELETE、OPTIONS、TRACE、CONNECT
  • 最常用的是 GET 与 POST

响应状态码

状态码描述含义
200Ok请求成功
400Bad Request客户端请求的语法出现错误,服务端无法解析
404Not Found服务端无法根据客户端的请求找到对应的资源
500Internal Server Error服务端内部出现问题,无法完成响应

请求响应过程

请求响应过程

JSON

  • JavaScript Object Notation。
  • 一种轻量级的数据格式,一般用于数据交互。
  • 服务端返回给 App 客户端的数据,一般都是 JSON 格式。

语法

  • 数据以键值对key : value形式存在。
  • 多个数据由,分隔。
  • 花括号{}保存对象。
  • 方括号[]保存数组。

key与value

  • 标准 JSON 数据的 key 必须用双引号""
  • JSON 数据的 value 类型:
    • 数字(整数或浮点数)
    • 字符串("表示)
    • 布尔值(true 或 false)
    • 数组([]表示)
    • 对象({}表示)
    • null

解析

  • 厘清当前 JSON 数据的层级关系(借助于格式化工具)。
  • 明确每个 key 对应的 value 值的类型。
  • 解析技术
    • Codable 协议(推荐)。
    • JSONSerialization。
    • 第三方框架。

URLSession

使用步骤

  1. 创建请求资源的 URL。
  2. 创建 URLRequest,设置请求参数。
  3. 创建 URLSessionConfiguration 用于设置 URLSession 的工作模式和网络设置。
  4. 创建 URLSession。
  5. 通过 URLSession 构建 URLSessionTask,共有 3 种任务。 (1)URLSessionDataTask:请求数据的 Task。  (2)URLSessionUploadTask:上传数据的 Task。 (3)URLSessionDownloadTask:下载数据的 Task。 
  6. 启动任务。
  7. 处理服务端响应,有 2 种方式。 (1)通过 completionHandler(闭包)处理服务端响应。 (2)通过 URLSessionDataDelegate(代理)处理请求与响应过程的事件和接收服务端返回的数据。

基本使用

class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// get()
// post()
}

func get() {
// 1. 确定URL
let url = URL(string: "http://v.juhe.cn/toutiao/index?type=top&key=申请的key")
// 2. 创建请求
let urlRequest = URLRequest(url: url!)
// cachePolicy: 缓存策略,App最常用的缓存策略是returnCacheDataElseLoad,表示先查看缓存数据,没有缓存再请求
// timeoutInterval:超时时间
// let urlRequest = URLRequest(url: url!, cachePolicy: .returnCacheDataElseLoad, timeoutInterval: 5)
let config = URLSessionConfiguration.default
// 3. 创建URLSession
let session = URLSession(configuration: config)
// 4. 创建任务
let task = session.dataTask(with: urlRequest) { data, _, error in
if error != nil {
print(error!)
} else {
if let data = data {
print(String(data: data, encoding: .utf8)!)
}
}
}
// 5. 启动任务
task.resume()
}

func post() {
let url = URL(string: "http://v.juhe.cn/toutiao/index")
var urlRequest = URLRequest(url: url!)
// 指明请求方法
urlRequest.httpMethod = "POST"
// 指明参数
let params = "type=top&key=申请的key"
// 设置请求体
urlRequest.httpBody = params.data(using: .utf8)
let config = URLSessionConfiguration.default
// delegateQueue决定了代理方法在哪个线程中执行
let session = URLSession(configuration: config, delegate: self, delegateQueue: OperationQueue())
let task = session.dataTask(with: urlRequest)
task.resume()
}
}

// MARK:- URLSessionDataDelegate
extension ViewController: URLSessionDataDelegate {
// 开始接收数据
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) {
// 允许接收服务器的数据,默认情况下请求之后不接收服务器的数据即不会调用后面获取数据的代理方法
completionHandler(URLSession.ResponseDisposition.allow)
}

// 获取数据
// 根据请求的数据量该方法可能会调用多次,这样data返回的就是总数据的一段,此时需要用一个全局的Data进行追加存储
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
let result = String(data: data, encoding: .utf8)
if let result = result {
print(result)
}
}

// 获取结束
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
if let error = error {
print(error)
} else {
print("=======成功=======")
}
}
}

注意:如果网络请求是 HTTP 而非 HTTPS,默认情况下,iOS 会阻断该请求,此时需要在 Info.plist 中进行如下配置。

<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>

URL转码与解码

  • 当请求参数带中文时,必须进行转码操作。
let url = "https://www.baidu.com?name=张三"
.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!
print(url) // URL中文转码
print(url.removingPercentEncoding!) // URL中文解码

  • 有时候只需要对URL中的中文处理,而不需要针对整个URL。
let str = "阿楚姑娘"
.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!
let url = URL(string: "https://music.163.com/#/search/m/?s=\(str)&type=1")

下载数据

class ViewController: UIViewController {
// 下载进度
@IBOutlet var downloadProgress: UIProgressView!
// 下载图片
@IBOutlet var downloadImageView: UIImageView!

override func viewDidLoad() {
super.viewDidLoad()

download()
}

func download() {
let url = URL(string: "http://172.20.53.240:8080/AppTestAPI/wall.png")!
let request = URLRequest(url: url)
let session = URLSession(configuration: URLSessionConfiguration.default, delegate: self, delegateQueue: OperationQueue())
let task = session.downloadTask(with: request)
task.resume()
}
}

extension ViewController: URLSessionDownloadDelegate {
// 下载完成
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
// 存入沙盒
let savePath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first!
// 文件类型根据下载的内容决定
let fileName = "\(Int(Date().timeIntervalSince1970)).png"
let filePath = savePath + "/" + fileName
print(filePath)
do {
try FileManager.default.moveItem(at: location, to: URL(fileURLWithPath: filePath))
// 显示到界面
DispatchQueue.main.async {
self.downloadImageView.image = UIImage(contentsOfFile: filePath)
}
} catch {
print(error)
}
}

// 计算进度
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
DispatchQueue.main.async {
self.downloadProgress.setProgress(Float(totalBytesWritten) / Float(totalBytesExpectedToWrite), animated: true)
}
}
}

上传数据

上传数据需要服务端配合,不同的服务端代码可能会不一样,下面的上传代码适用于本人所写的服务端代码

  • 数据格式。

上传数据格式

  • 实现。
class ViewController: UIViewController {
let YFBoundary = "AnHuiWuHuYungFan"
@IBOutlet var uploadInfo: UILabel!
@IBOutlet var uploadProgress: UIProgressView!

override func viewDidLoad() {
super.viewDidLoad()

upload()
}

func upload() {
// 1. 确定URL
let url = URL(string: "http://172.20.53.240:8080/AppTestAPI/UploadServlet")!
// 2. 确定请求
var request = URLRequest(url: url)
// 3. 设置请求头
let head = "multipart/form-data;boundary=\(YFBoundary)"
request.setValue(head, forHTTPHeaderField: "Content-Type")
// 4. 设置请求方式
request.httpMethod = "POST"
// 5. 创建NSURLSession
let session = URLSession(configuration: URLSessionConfiguration.default, delegate: self, delegateQueue: OperationQueue())
// 6. 获取上传的数据(按照固定格式拼接)
var data = Data()
let header = headerString(mimeType: "image/png", uploadFile: "wall.png")
data.append(header.data(using: .utf8)!)
data.append(uploadData())
let tailer = tailerString()
data.append(tailer.data(using: .utf8)!)
// 7. 创建上传任务 上传的数据来自getData方法
let task = session.uploadTask(with: request, from: data) { _, _, error in
// 上传完毕后
if error != nil {
print(error!)
} else {
DispatchQueue.main.async {
self.uploadInfo.text = "上传成功"
}
}
}
// 8. 执行上传任务
task.resume()
}

// 开始标记
func headerString(mimeType: String, uploadFile: String) -> String {
var data = String()
// --Boundary\r\n
data.append("--" + YFBoundary + "\r\n")
// 文件参数名 Content-Disposition: form-data; name="myfile"; filename="wall.jpg"\r\n
data.append("Content-Disposition:form-data; name=\"myfile\";filename=\"\(uploadFile)\"\r\n")
// Content-Type 上传文件的类型 MIME\r\n\r\n
data.append("Content-Type:\(mimeType)\r\n\r\n")

return data
}

// 结束标记
func tailerString() -> String {
// \r\n--Boundary--\r\n
return "\r\n--" + YFBoundary + "--\r\n"
}

func uploadData() -> Data {
let image = UIImage(named: "wall.png")
let imageData = image!.pngData()
return imageData!
}
}

extension ViewController: URLSessionTaskDelegate {
// 上传进去
func urlSession(_ session: URLSession, task: URLSessionTask, didSendBodyData bytesSent: Int64, totalBytesSent: Int64, totalBytesExpectedToSend: Int64) {
DispatchQueue.main.async {
self.uploadProgress.setProgress(Float(totalBytesSent) / Float(totalBytesExpectedToSend), animated: true)
}
}

// 上传出错
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
if let error = error {
print(error)
}
}
}

URLCache

  • 网络缓存有很多好处:节省流量、更快加载、断网可用。
  • 使用 URLCache 管理缓存区域的大小和数据。
  • 每一个 App 都默认创建了一个 URLCache 作为缓存管理者,可以通过URLCache.shared获取,也可以自定义。
// 创建URLCache
// memoryCapacity:内存缓存容量
// diskCapacity:硬盘缓存容量
// directory:硬盘缓存路径
let cache = URLCache(memoryCapacity: 10 * 1024 * 1024, diskCapacity: 100 * 1024 * 1024, directory: FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first)
// 替换默认的缓存管理对象
URLCache.shared = cache

  • 常见属性与方法。
let url = URL(string: "http://v.juhe.cn/toutiao/index?type=top&key=申请的key")
let urlRequest = URLRequest(url: url!, cachePolicy: .returnCacheDataElseLoad, timeoutInterval: 5)
let cache = URLCache.shared

// 内存缓存大小
cache.memoryCapacity
// 硬盘缓存大小
cache.diskCapacity
// 已用内存缓存大小
cache.currentMemoryUsage
// 已用硬盘缓存大小
cache.currentDiskUsage
// 获取某个请求的缓存
let cacheResponse = cache.cachedResponse(for: urlRequest)
// 删除某个请求的缓存
cache.removeCachedResponse(for: urlRequest)
// 删除某个时间点开始的缓存
cache.removeCachedResponses(since: Date().addingTimeInterval(-60 * 60 * 48))
// 删除所有缓存
cache.removeAllCachedResponses()

WKWebView

  • 用于加载 Web 内容的控件。
  • 使用时必须导入WebKit模块。

基本使用

  • 加载网页。
// 创建URL
let url = URL(string: "https://www.abc.edu.cn")
// 创建URLRequest
let request = URLRequest(url: url!)
// 创建WKWebView
let webView = WKWebView(frame: UIScreen.main.bounds)
// 加载网页
webView.load(request)

  • 加载本地资源。
// 文件夹路径
let basePath = Bundle.main.path(forResource: "localWeb", ofType: nil)!
// 文件夹URL
let baseUrl = URL(fileURLWithPath: basePath, isDirectory: true)
// html路径
let filePath = basePath + "/index.html"
// 转成文件
let fileContent = try? NSString(contentsOfFile: filePath, encoding: String.Encoding.utf8.rawValue)
// 创建WKWebView
let webView = WKWebView(frame: UIScreen.main.bounds)
// 加载html
webView.loadHTMLString(fileContent! as String, baseURL: baseUrl)

注意:如果是本地资源是文件夹,拖进项目时,需要勾选Create folder references,然后用Bundle.main.path(forResource: "文件夹名", ofType: nil)获取资源路径。

与JavaScript交互

创建WKWebView

lazy var webView: WKWebView = {
// 创建WKPreferences
let preferences = WKPreferences()
// 开启JavaScript
preferences.javaScriptEnabled = true
// 创建WKWebViewConfiguration
let configuration = WKWebViewConfiguration()
// 设置WKWebViewConfiguration的WKPreferences
configuration.preferences = preferences
// 创建WKUserContentController
let userContentController = WKUserContentController()
// 配置WKWebViewConfiguration的WKUserContentController
configuration.userContentController = userContentController
// 给WKWebView与Swift交互起一个名字:callbackHandler,WKWebView给Swift发消息的时候会用到
// 此句要求实现WKScriptMessageHandler
configuration.userContentController.add(self, name: "callbackHandler")
// 创建WKWebView
var webView = WKWebView(frame: UIScreen.main.bounds, configuration: configuration)
// 让WKWebView翻动有回弹效果
webView.scrollView.bounces = true
// 只允许WKWebView上下滚动
webView.scrollView.alwaysBounceVertical = true
// 设置代理WKNavigationDelegate
webView.navigationDelegate = self
// 返回
return webView
}()

创建HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0,user-scalable=no"/>
</head>
<body>
iOS传过来的值:<span id="name"></span>
<button onclick="responseSwift()">响应iOS</button>
<script type="text/javascript">
// 给Swift调用
function sayHello(name) {
document.getElementById("name").innerHTML = name
return "Swift你也好!"
}
// 调用Swift方法
function responseSwift() {
// 这里的callbackHandler是创建WKWebViewConfiguration是定义的
window.webkit.messageHandlers.callbackHandler.postMessage("JavaScript发送消息给Swift")
}
</script>
</body>
</html>

两个协议

  • WKNavigationDelegate:判断页面加载完成,只有在页面加载完成后才能在实现 Swift 调用 JavaScript。WKWebView 调用 JavaScript:
// 加载完毕以后执行
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
// 调用JavaScript方法
webView.evaluateJavaScript("sayHello('WebView你好!')") { (result, err) in
// result是JavaScript返回的值
print(result, err)
}
}

  • WKScriptMessageHandler:JavaScript 调用 Swift 时需要用到协议中的一个方法来。JavaScript 调用 WKWebView:
// Swift方法,可以在JavaScript中调用
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
print(message.body)
}

ViewController

class ViewController: UIViewController {
// 懒加载WKWebView
...

// 加载本地html
let html = try! String(contentsOfFile: Bundle.main.path(forResource: "index", ofType: "html")!, encoding: String.Encoding.utf8)

override func viewDidLoad() {
super.viewDidLoad()
// 标题
title = "WebView与JavaScript交互"
// 加载html
webView.loadHTMLString(html, baseURL: nil)
view.addSubview(webView)
}
}

// 遵守两个协议
extension ViewController: WKNavigationDelegate, WKScriptMessageHandler {
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
...
}

func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
...
}
}

SFSafariViewController

  • iOS 9 推出的一种 UIViewController,用于加载与显示 Web 内容,打开效果类似 Safari 浏览器的效果。
  • 使用时必须导入SafariServices模块。
import SafariServices

class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
}

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
showSafariViewController()
}

func showSafariViewController() {
// URL
let url = URL(string: "https://www.baidu.com")
// 创建SFSafariViewController
let sf = SFSafariViewController(url: url!)
// 设置代理
sf.delegate = self
// 显示
present(sf, animated: true, completion: nil)
}
}

extension ViewController: SFSafariViewControllerDelegate {
// 点击左上角的完成(done)
func safariViewControllerDidFinish(_ controller: SFSafariViewController) {
print(#function)
}

// 加载完成
func safariViewController(_ controller: SFSafariViewController, didCompleteInitialLoad didLoadSuccessfully: Bool) {
print(#function)
}
}
收起阅读 »

iOS 14开发-定位与地图

iOS
定位CoreLocation 是 iOS 中用于设备定位的框架。通过这个框架可以实现定位进而获取位置信息如经度、纬度、海拔信息等。模块与常见类定位所包含的类都在CoreLocation模块中,使用时必须导入。CLLocationManager:定位管理器,可以...
继续阅读 »

定位

CoreLocation 是 iOS 中用于设备定位的框架。通过这个框架可以实现定位进而获取位置信息如经度、纬度、海拔信息等。

模块与常见类

  • 定位所包含的类都在CoreLocation模块中,使用时必须导入。
  • CLLocationManager:定位管理器,可以理解为定位不能自己工作,需要有个类对它进行全过程管理。
  • CLLocationManagerDelegate:定位管理代理,不管是定位成功与失败,都会有相应的代理方法进行回调。
  • CLLocation:表示某个位置的地理信息,包含经纬度、海拔等。
  • CLPlacemark:位置信息,包含的信息如国家、城市、街道等。
  • CLGeocoder:地理编码。

工作流程

  1. 创建CLLocationManager,设置代理并发起定位。
  2. 实现CLLocationManagerDelegate中定位成功和失败的代理方法。
  3. 在成功的代理方法中获取CLLocation对象并通过CLGeocoder进行反向地理编码获取对应的位置信息CLPlacemark
  4. 通过CLPlacemark获取具体的位置信息。

权限

授权对话框

  • 程序中调用requestWhenInUseAuthorization发起定位授权。
  • 程序中调用requestAlwaysAuthorization发起定位授权。

前台定位

  • 需要在 Info.plist 中配置Privacy - Location When In Use Usage Description
  • 程序中调用requestWhenInUseAuthorization发起定位授权。
  • 弹出的授权对话框新增了精确位置开关,同时新增了小地图展示当前位置。

后台定位

  • 需要勾选 Capabilities —> Background Modes —> Location updates
  • 程序中允许后台定位:locationManager.allowsBackgroundLocationUpdates = true
  • 此时授权分为 2 种情况: (1)Privacy - Location When In Use Usage Description + requestWhenInUseAuthorization:可以后台定位,但会在设备顶部出现蓝条(刘海屏设备会出现在左边刘海)。 (2)Privacy - Location When In Use Usage Description + Privacy - Location Always and When In Use Usage Description + requestAlwaysAuthorization:可以后台定位,不会出现蓝条。这种方式会出现 2 次授权对话框:第一次和前台定位一样,在同意使用While Using App模式后,继续使用定位才会弹出第二次,询问是否切换到Always模式。

精度控制

  • iOS 14 新增了一种定位精度控制,在定位授权对话框中有一个精度切换开关,可以切换精确和模糊定位(默认精确)。
  • 可以通过CLLocationManageraccuracyAuthorization属性获取当前的定位精度权限。
  • 当已经获得定位权限且当前用户选择的是模糊定位,则可以使用CLLocationManagerrequestTemporaryFullAccuracyAuthorization(withPurposeKey purposeKey: String, completion: ((Error?) -> Void)? = nil)方法申请一次临时精确定位权限,其中purposeKey为 Info.plist 中配置的Privacy - Location Temporary Usage Description Dictionary字段下某个具体原因的 key,可以设置多个 key 以应对不同的定位使用场景。
  • requestTemporaryFullAccuracyAuthorization方法并不能用于申请定位权限,只能用于从模糊定位升级为精确定位;如果没有获得定位权限,直接调用此 API 无效。
  • 如果不想使用精确定位,则可以在 Info.plist 中配置Privacy - Location Default Accuracy ReducedYES,此时申请定位权限的小地图中不再有精度切换开关。需要注意 2 点: (1)如果发现该字段不是 Bool 型,需要以源码形式打开 Info.plist,然后手动修改<key>NSLocationDefaultAccuracyReduced</key>为 Bool 型的值,否则无法生效。 (2)配置该字段后,如果 Info.plist 中还配置了Privacy - Location Temporary Usage Description Dictionary,则仍可以通过requestTemporaryFullAccuracyAuthorization申请临时的精确定位权限,会再次弹出授权对话框进行确认。

模拟器定位

由于定位需要 GPS,一般情况下需要真机进行测试。但对于模拟器,也可以进行虚拟定位,主要有 3 种方式。

  • 方式一
    (1)新建一个gpx文件,可以取名XXX.gpx,然后将自己的定位信息填写进 xml 对应的位置。 (2)gpx文件设置完成以后,首先需要运行一次 App,然后选择Edit Scheme,在Options中选择自己的gpx文件,这样模拟器运行的时候就会读取该文件的位置信息。然后可以选择Debug—>Simulate Location或底部调试栏上的定位按钮进行gpx文件或位置信息的切换。
<?xml version="1.0"?>
<gpx version="1.1" creator="Xcode">
<!--安徽商贸职业技术学院 谷歌地球:31.2906511800,118.3623587000-->
<wpt lat="31.2906511800" lon="118.3623587000">
<name>安徽商贸职业技术学院</name>
<cmt>中国安徽省芜湖市弋江区文昌西路24号 邮政编码: 241002</cmt>
<desc>中国安徽省芜湖市弋江区文昌西路24号 邮政编码: 241002</desc>
</wpt>
</gpx>

  • 方式二:运行程序开始定位 —> 模拟器菜单 —> Features —> Location —> Custom Location —> 输入经纬度。

实现步骤

  1. 导入CoreLocation模块。
  2. 创建CLLcationManager对象,设置参数和代理,配置 Info.plist 并请求定位授权。
  3. 调用CLLcationManager对象的startUpdatingLocation()requestLocation()方法进行定位。
  4. 实现代理方法,在定位成功的方法中进行位置信息的处理。
import CoreLocation
import UIKit

class ViewController: UIViewController {
// CLLocationManager
lazy var locationManager = CLLocationManager()
// CLGeocoder
lazy var gecoder = CLGeocoder()

override func viewDidLoad() {
super.viewDidLoad()

setupManager()
}

func setupManager() {
// 默认情况下每当位置改变时LocationManager就调用一次代理。通过设置distanceFilter可以实现当位置改变超出一定范围时LocationManager才调用相应的代理方法。这样可以达到省电的目的。
locationManager.distanceFilter = 300
// 精度 比如为10 就会尽量达到10米以内的精度
locationManager.desiredAccuracy = kCLLocationAccuracyBest
// 代理
locationManager.delegate = self
// 第一种:能后台定位但是会在顶部出现大蓝条(打开后台定位的开关)
// 允许后台定位
locationManager.allowsBackgroundLocationUpdates = true
locationManager.requestWhenInUseAuthorization()
// 第二种:能后台定位并且不会出现大蓝条
// locationManager.requestAlwaysAuthorization()
}

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
// 以下2个方法都会调用代理方法
// 1. 发起位置更新(定位)会一直轮询,耗电
locationManager.startUpdatingLocation()
// 2. 只请求一次用户的位置,省电
// locationManager.requestLocation()
}
}

extension ViewController: CLLocationManagerDelegate {
// 定位成功
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
if let location = locations.last {
// 反地理编码转换成具体的地址
gecoder.reverseGeocodeLocation(location) { placeMarks, _ in
// CLPlacemark -- 国家 城市 街道
if let placeMark = placeMarks?.first {
print(placeMark)
// print("\(placeMark.country!) -- \(placeMark.name!) -- \(placeMark.locality!)")
}
}
}
// 停止位置更新
locationManager.stopUpdatingLocation()
}

// 定位失败
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
print(error.localizedDescription)
}
}

地图

  • 地图所包含的类都在MapKit模块中,使用时必须导入。
  • 除了可以显示地图,还支持在地图上进行标记处理。
  • 地图看似很复杂,其实它仅仅是一个控件 MKMapView,就和以前学习过的 UIButton、UITableView 等一样,可以在 storyboard 和代码中使用。
  • 地图上如果想要显示用户的位置,必须与定位配合,那么就需要创建定位管理器、设置权限等(参考定位知识),同时需要通过 storyboard 或者代码设置地图的相关属性。

准备工作

  1. 添加一个地图并设置相关属性。
  2. Info.plist 中配置定位权限。
  3. 创建 CLLocationManager 对象并请求定位权限。

基本使用

显示地图,同时显示用户所处的位置。点击用户的位置,显示一个气泡展示用户位置的具体信息。

import MapKit

class ViewController: UIViewController {
@IBOutlet var mapView: MKMapView!
lazy var locationManager: CLLocationManager = CLLocationManager()

override func viewDidLoad() {
super.viewDidLoad()

setupMapView()
}

func setupManager() {
locationManager.requestWhenInUseAuthorization()
// 不需要发起定位
}

func setupMapView() {
// 设置定位
setupManager()
// 地图类型
mapView.mapType = .hybridFlyover
// 显示兴趣点
mapView.showsPointsOfInterest = true
// 显示指南针
mapView.showsCompass = true
// 显示交通
mapView.showsTraffic = true
// 显示建筑
mapView.showsBuildings = true
// 显示级别
mapView.showsScale = true
// 用户跟踪模式
mapView.userTrackingMode = .followWithHeading
}
}

缩放级别

在之前功能的基础上实现地图的任意视角(“缩放级别”)。

// 设置“缩放级别”
func setRegion() {
if let location = location {
// 设置范围,显示地图的哪一部分以及显示的范围大小
let region = MKCoordinateRegion(center: mapView.userLocation.coordinate, latitudinalMeters: 500, longitudinalMeters: 500)
// 调整范围
let adjustedRegion = mapView.regionThatFits(region)
// 地图显示范围
mapView.setRegion(adjustedRegion, animated: true)
}
}

标注

在地图上可以添加标注来显示一个个关键的信息点,用于对用户的提示。

分类

  • MKPinAnnotationView:系统自带的标注,继承于 MKAnnotationView,形状跟棒棒糖类似,可以设置糖的颜色,和显示的时候是否有动画效果 (Swift 不推荐使用)。
  • MKMarkerAnnotationView:iOS 11 推出,建议使用。
  • MKAnnotationView:可以用指定的图片作为标注的样式,但显示的时候没有动画效果,如果没有指定图片会什么都不显示(自定义时使用)。

创建模型

class MapFlag: NSObject, MKAnnotation {
// 标题
let title: String?
// 副标题
let subtitle: String?
// 经纬度
let coordinate: CLLocationCoordinate2D
// 附加信息
let urlString: String

init(title: String?, subtitle: String?, coordinate: CLLocationCoordinate2D, urlString: String) {
self.title = title
self.subtitle = subtitle
self.coordinate = coordinate
self.urlString = urlString
}
}

添加标注

  • 添加系统标注,点击能够显示标题和副标题。
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
let flag = MapFlag(title: "标题", subtitle: "副标题", coordinate: CLLocationCoordinate2D(latitude: 31.2906511800, longitude: 118.3623587000), urlString: "https://www.baidu.com")
mapView.addAnnotation(flag)
}

  • 添加系统标注,点击以气泡形式显示标题、副标题及自定义内容,此时需要重写地图的代理方法,返回标注的样式。
extension ViewController: MKMapViewDelegate {
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
guard let annotation = annotation as? MapFlag else {
return nil
}
// 如果是用户的位置,使用默认样式
if annotation == mapView.userLocation {
return nil
}
// 标注的标识符
let identifier = "marker"
// 获取AnnotationView
var annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: identifier) as? MKMarkerAnnotationView
// 判空
if annotationView == nil {
annotationView = MKMarkerAnnotationView(annotation: annotation, reuseIdentifier: identifier)
// 显示气泡
annotationView?.canShowCallout = true
// 左边显示的辅助视图
annotationView?.leftCalloutAccessoryView = UIImageView(image: UIImage(systemName: "heart"))
// 右边显示的辅助视图
let button = UIButton(type: .detailDisclosure, primaryAction: UIAction(handler: { _ in
print(annotation.urlString)
}))
annotationView?.rightCalloutAccessoryView = button
}

return annotationView
}
}

  • 如果希望标注的图标为自定义样式,只需要稍加更改代理方法并设置自己的标注图片即可。
extension ViewController: MKMapViewDelegate {
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
guard let annotation = annotation as? MapFlag else {
return nil
}
// 如果是用户的位置,使用默认样式
if annotation == mapView.userLocation {
return nil
}
// 标注的标识符
let identifier = "custom"
// 标注的自定义图片
let annotationImage = ["pin.circle.fill", "car.circle.fill", "airplane.circle.fill", "cross.circle.fill"]
// 获取AnnotationView
var annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: identifier)
// 判空
if annotationView == nil {
annotationView = MKAnnotationView(annotation: annotation, reuseIdentifier: identifier)
// 图标,每次随机取一个
annotationView?.image = UIImage(systemName: annotationImage.randomElement()!)
// 显示气泡
annotationView?.canShowCallout = true
// 左边显示的辅助视图
annotationView?.leftCalloutAccessoryView = UIImageView(image: UIImage(systemName: "heart"))
// 右边显示的辅助视图
let button = UIButton(type: .detailDisclosure, primaryAction: UIAction(handler: { _ in
print(annotation.urlString)
}))
annotationView?.rightCalloutAccessoryView = button
// 弹出的位置偏移
annotationView?.calloutOffset = CGPoint(x: -5.0, y: 5.0)
}

return annotationView
}
}

// 点击地图插入一个标注,标注的标题和副标题显示的是标注的具体位置
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
let touchPoint = touches.first?.location(in: mapView)
// 将坐标转换成为经纬度,然后赋值给标注
let coordinate = mapView.convert(touchPoint!, toCoordinateFrom: mapView)
let location = CLLocation(latitude: coordinate.latitude, longitude: coordinate.longitude)
let gecoder = CLGeocoder()
// 反地理编码转换成具体的地址
gecoder.reverseGeocodeLocation(location) { placeMarks, _ in
let placeMark = placeMarks?.first
if let placeMark = placeMark {
let flag = MapFlag(title: placeMark.locality, subtitle: placeMark.subLocality, coordinate: coordinate, urlString: "https://www.baidu.com")
self.mapView.addAnnotation(flag)
}
}
}
收起阅读 »

iOS 14开发- 通知

iOS
iOS 中的通知主要分为 2 种,本地通知和远程通知。本地通知使用步骤导入UserNotifications模块。申请权限。创建通知内容UNMutableNotificationContent,可以设置: (1)title:通知标题。 (2)subtitle:...
继续阅读 »

iOS 中的通知主要分为 2 种,本地通知和远程通知。

本地通知

使用步骤

  1. 导入UserNotifications模块。
  2. 申请权限。
  3. 创建通知内容UNMutableNotificationContent,可以设置: (1)title:通知标题。 (2)subtitle:通知副标题。 (3)body:通知体。 (4)sound:声音。 (5)badge:角标。 (6)userInfo:额外信息。 (7)categoryIdentifier:分类唯一标识符。 (8)attachments:附件,可以是图片、音频和视频,通过下拉通知显示。
  4. 指定本地通知触发条件,有 3 种触发方式: (1)UNTimeIntervalNotificationTrigger:一段时间后触发。 (2)UNCalendarNotificationTrigger:指定日期时间触发。 (3)UNLocationNotificationTrigger:根据位置触发。
  5. 根据通知内容和触发条件创建UNNotificationRequest
  6. UNNotificationRequest添加到UNUserNotificationCenter

案例

  • 申请授权(异步操作)。
import UserNotifications

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// 请求通知权限
UNUserNotificationCenter.current()
.requestAuthorization(options: [.alert, .sound, .badge]) { // 横幅,声音,标记
(accepted, error) in
if !accepted {
print("用户不允许通知")
}
}

return true
}

  • 发送通知。
import CoreLocation
import UIKit
import UserNotifications

class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
}

// 一段时间后触发
@IBAction func timeInterval(_ sender: Any) {
// 设置推送内容
let content = UNMutableNotificationContent()
content.title = "你好"
content.subtitle = "Hi"
content.body = "这是一条基于时间间隔的测试通知"
content.sound = UNNotificationSound(named: UNNotificationSoundName(rawValue: "feiji.wav"))
content.badge = 1
content.userInfo = ["username": "YungFan", "career": "Teacher"]
content.categoryIdentifier = "testUserNotifications1"
setupAttachment(content: content)

// 设置通知触发器
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 5, repeats: false)

// 设置请求标识符
let requestIdentifier = "com.abc.testUserNotifications2"
// 设置一个通知请求
let request = UNNotificationRequest(identifier: requestIdentifier, content: content, trigger: trigger)
// 将通知请求添加到发送中心
UNUserNotificationCenter.current().add(request, withCompletionHandler: nil)
}

// 指定日期时间触发
@IBAction func dateInterval(_ sender: Any) {
// 设置推送内容
let content = UNMutableNotificationContent()
content.title = "你好"
content.body = "这是一条基于日期的测试通知"

// 时间
var components = DateComponents()
components.year = 2021
components.month = 5
components.day = 20
// 每周一上午8点
// var components = DateComponents()
// components.weekday = 2 // 周一
// components.hour = 8 // 上午8点
// components.minute = 30 // 30分
// 设置通知触发器
let trigger = UNCalendarNotificationTrigger(dateMatching: components, repeats: false)

// 设置请求标识符
let requestIdentifier = "com.abc.testUserNotifications3"
// 设置一个通知请求
let request = UNNotificationRequest(identifier: requestIdentifier, content: content, trigger: trigger)
// 将通知请求添加到发送中心
UNUserNotificationCenter.current().add(request, withCompletionHandler: nil)
}

// 根据位置触发
@IBAction func locationInterval(_ sender: Any) {
// 设置推送内容
let content = UNMutableNotificationContent()
content.title = "你好"
content.body = "这是一条基于位置的测试通知"

// 位置
let coordinate = CLLocationCoordinate2D(latitude: 31.29065118, longitude: 118.3623587)
let region = CLCircularRegion(center: coordinate, radius: 500, identifier: "center")
region.notifyOnEntry = true // 进入此范围触发
region.notifyOnExit = false // 离开此范围不触发
// 设置触发器
let trigger = UNLocationNotificationTrigger(region: region, repeats: true)
// 设置请求标识符
let requestIdentifier = "com.abc.testUserNotifications"

// 设置一个通知请求
let request = UNNotificationRequest(identifier: requestIdentifier, content: content, trigger: trigger)
// 将通知请求添加到发送中心
UNUserNotificationCenter.current().add(request, withCompletionHandler: nil)
}
}

extension ViewController {
func setupAttachment(content: UNMutableNotificationContent) {
let imageURL = Bundle.main.url(forResource: "img", withExtension: ".png")!
do {
let imageAttachment = try UNNotificationAttachment(identifier: "iamgeAttachment", url: imageURL, options: nil)
content.attachments = [imageAttachment]
} catch {
print(error.localizedDescription)
}
}
}

远程通知(消息推送)

远程通知是指在联网的情况下,由远程服务器推送给客户端的通知,又称 APNs(Apple Push Notification Services)。在联网状态下,所有设备都会与 Apple 服务器建立长连接,因此不管应用是打开还是关闭的情况,都能接收到服务器推送的远程通知。

远程通知流程.png

实现原理

  1. App 打开后首先发送 UDID 和 BundleID 给 APNs 注册,并返回 deviceToken(图中步骤 1,2,3)。
  2. App 获取 deviceToken 后,通过 API 将 App 的相关信息和 deviceToken 发送给应用服务器,服务器将其记录下来。(图中步骤 4)
  3. 当要推送通知时,应用服务器按照 App 的相关信息找到存储的 deviceToken,将通知和 deviceToken 发送给 APNs。(图中步骤 5)
  4. APNs 通过 deviceToken,找到指定设备的指定 App, 并将通知推送出去。(图中步骤 6)

实现步骤

证书方式

  1. 在开发者网站的 Identifiers 中添加 App IDs,并在 Capabilities 中开启 Push Notifications
  2. 在 Certificates 中创建一个 Apple Push Notification service SSL (Sandbox & Production) 的 APNs 证书并关联第一步中的 App IDs,然后将证书下载到本地安装(安装完可以导出 P12 证书)。
  3. 在项目中选择 Capability,接着开启 Push Notifications,然后在 Background Modes 中勾选 Remote notifications
  4. 申请权限。
  5. 通过UIApplication.shared.registerForRemoteNotifications()向 APNs 请求 deviceToken。
  6. 通过func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data)获取 deviceToken。如果正常获取到 deviceToken,即表示注册成功,可以进行远程通知的推送,最后需要将其发送给应用服务器。注意:
    • App 重新启动后,deviceToken 不会变化。
    • App 卸载后重新安装,deviceToken 发生变化。
  7. 通知测试。

Token方式

  1. 在开发者网站的 Membership 中找到 Team ID 并记录。
  2. 在 Certificates, Identifiers & Profiles 的 Keys 中注册一个 Key 并勾选 Apple Push Notifications service (APNs) ,最后将生成的 Key ID 记录并将 P8 的 AuthKey 下载到本地(只能下载一次)。
  3. 在项目中选择 Capability,接着开启 Push Notifications,然后在 Background Modes 中勾选 Remote notifications
  4. 申请权限。
  5. 通过UIApplication.shared.registerForRemoteNotifications()向 APNs 请求 deviceToken。
  6. 通过func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data)获取 deviceToken。如果正常获取到 deviceToken,即表示注册成功,可以进行远程通知的推送,最后需要将其发送给应用服务器。
  7. 通知测试。

Token Authentication 是 APNs 新推出的推送鉴权方式,它如下优势: (1)同一个开发者账号下的所有 App 无论是测试还是正式版都能使用同一个 Key 来发送而不需要为每个 App 生成证书。 (2)生成 Key 的过程相对简单,不需要繁琐的证书操作过程,并且它不再有过期时间,无需像证书那样需要定期重新生成。。

AppDelegate

import UserNotifications

class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// 请求通知权限
UNUserNotificationCenter.current()
.requestAuthorization(options: [.alert, .sound, .badge]) {
accepted, _ in
if !accepted {
print("用户不允许通知。")
}
}

// 向APNs请求deviceToken
UIApplication.shared.registerForRemoteNotifications()

return true
}

// deviceToken请求成功回调
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
var deviceTokenString = String()
let bytes = [UInt8](deviceToken)
for item in bytes {
deviceTokenString += String(format: "x", item & 0x000000FF)
}

// 打印获取到的token字符串
print(deviceTokenString)

// 通过网络将token发送给服务端
}

// deviceToken请求失败回调
func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
print(error.localizedDescription)
}
}

注意:远程通知不支持模拟器(直接进入deviceToken请求失败回调),必须在真机测试。

测试

真机测试

  1. 将 App 安装到真机上。
  2. 通过软件(如 APNs)或者第三方进行测试,但都需要进行相关内容的设置。 (1)证书方式需要:P12 证书 + Bundle Identifier + deviceToken。 (2)Token 方式需要:P8 AuthKey + Team ID + Key ID + Bundle Identifier + deviceToken

模拟器测试—使用JSON文件

  • JSON文件。
{
"aps":{
"alert":{
"title":"测试",
"subtitle":"远程推送",
"body":"这是一条从远处而来的通知"
},
"sound":"default",
"badge":1
}
}

  • 命令。
xcrun simctl push booted developer.yf.TestUIKit /Users/yangfan/Desktop/playload.json

模拟器测试—使用APNS文件

另一种方法是将 APNs 文件直接拖到 iOS 模拟器中。准备一个后缀名为.apns的文件,其内容和上面的 JSON 文件差不多,但是添加了一个Simulator Target Bundle,用于描述 App 的Bundle Identifier

  • APNs文件。
{
"Simulator Target Bundle": "developer.yf.TestUIKit",
"aps":{
"alert":{
"title":"测试",
"subtitle":"远程推送",
"body":"这是一条从远处而来的通知"
},
"sound":"default",
"badge":1
}
}

前台处理

默认情况下,App 只有在后台才能收到通知提醒,在前台无法收到通知提醒,如果前台也需要提醒可以进行如下处理。

  • 创建 UNUserNotificationCenterDelegate。
class NotificationHandler: NSObject, UNUserNotificationCenterDelegate {
// 前台展示通知
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
// 前台通知一般不设置badge
completionHandler([.list, .banner, .sound])

// 如果不想显示某个通知,可以直接用 []
// completionHandler([])
}
}

  • 设置代理。
class AppDelegate: UIResponder, UIApplicationDelegate {
// 自定义通知回调类,实现通知代理
let notificationHandler = NotificationHandler()

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// 设置代理
UNUserNotificationCenter.current().delegate = notificationHandler

return true
}
}

角标设置

  • 不论是本地还是远程通知,前台通知一般不会设置角标提醒,所以只需要针对后台通知处理角标即可。
  • 通知的角标不需要手动设置,会自动根据通知进行设置
// 手动添加角标
UIApplication.shared.applicationIconBadgeNumber = 10

// 清理角标
UIApplication.shared.applicationIconBadgeNumber = 0
收起阅读 »

iOS KVO的基本使用

iOS
iOS - 关于 KVO 的一些总结1. 什么是 KVOKVO的全称是Key-Value Observing,俗称“键值观察/监听”,是苹果提供的一套事件通知机制,允许一个对象观察/监听另一个对象指定属性值的改变。当被观察对象属性值发生改变时,会触发KVO的监...
继续阅读 »

iOS - 关于 KVO 的一些总结

1. 什么是 KVO

  • KVO的全称是Key-Value Observing,俗称“键值观察/监听”,是苹果提供的一套事件通知机制,允许一个对象观察/监听另一个对象指定属性值的改变。当被观察对象属性值发生改变时,会触发KVO的监听方法来通知观察者。KVO是在MVC应用程序中的各层之间进行通信的一种特别有用的技术。
  • KVONSNotification都是iOS中观察者模式的一种实现。
  • KVO可以监听单个属性的变化,也可以监听集合对象的变化。监听集合对象变化时,需要通过KVCmutableArrayValueForKey:等可变代理方法获得集合代理对象,并使用代理对象进行操作,当代理对象的内部对象发生改变时,会触发KVO的监听方法。集合对象包含NSArrayNSSet
  • KVOKVC有着密切的关系,如果想要深入了解KVO,建议先学习KVC


传送门:iOS - 关于 KVC 的一些总结

2. KVO 的基本使用

KVO使用三部曲:添加/注册KVO监听、实现监听方法以接收属性改变通知、 移除KVO监听。

  1. 调用方法addObserver:forKeyPath:options:context: 给被观察对象添加观察者;
  2. 在观察者类中实现observeValueForKeyPath:ofObject:change:context:方法以接收属性改变的通知消息;
  3. 当观察者不需要再监听时,调用removeObserver:forKeyPath:方法将观察者移除。需要注意的是,至少需要在观察者销毁之前,调用此方法,否则可能会导致Crash

2.1 注册方法

/*
** target: 被观察对象
** observer:观察者对象
** keyPath: 被观察对象的属性的关键路径,不能为nil
** options: 观察的配置选项,包括观察的内容(枚举类型):
NSKeyValueObservingOptionNew:观察新值
NSKeyValueObservingOptionOld:观察旧值
NSKeyValueObservingOptionInitial:观察初始值,如果想在注册观察者后,立即接收一次回调,可以加入该枚举值
NSKeyValueObservingOptionPrior:分别在值改变前后触发方法(即一次修改有两次触发)
** context: 可以传入任意数据(任意类型的对象或者C指针),在监听方法中可以接收到这个数据,是KVO中的一种传值方式
如果传的是一个对象,必须在移除观察之前持有它的强引用,否则在监听方法中访问context就可能导致Crash
*/

- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath
options:(NSKeyValueObservingOptions)options context:(nullable void *)context;

2.2 监听方法

如果对象被注册成为观察者,则该对象必须能响应以下监听方法,即该对象所属类中必须实现监听方法。当被观察对象属性发生改变时就会调用监听方法。如果没有实现就会导致Crash

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
/*
** keyPath:被观察对象的属性的关键路径
** object: 被观察对象
** change: 字典 NSDictionary,属性值更改的详细信息,根据注册方法中options参数传入的枚举来返回
key为 NSKeyValueChangeKey 枚举类型
{
1.NSKeyValueChangeKindKey:存储本次改变的信息(change字典中默认包含这个key)
{
对应枚举类型 NSKeyValueChange
typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
NSKeyValueChangeSetting = 1,
NSKeyValueChangeInsertion = 2,
NSKeyValueChangeRemoval = 3,
NSKeyValueChangeReplacement = 4,
};
如果是对被观察对象属性(包括集合)进行赋值操作,kind 字段的值为 NSKeyValueChangeSetting
如果被观察的是集合对象,且进行的是(插入、删除、替换)操作,则会根据集合对象的操作方式来设置 kind 字段的值
插入:NSKeyValueChangeInsertion
删除:NSKeyValueChangeRemoval
替换:NSKeyValueChangeReplacement
}
2.NSKeyValueChangeNewKey:存储新值(如果options中传入NSKeyValueObservingOptionNew,change字典中就会包含这个key)
3.NSKeyValueChangeOldKey:存储旧值(如果options中传入NSKeyValueObservingOptionOld,change字典中就会包含这个key)
4.NSKeyValueChangeIndexesKey:如果被观察的是集合对象,且进行的是(插入、删除、替换)操作,则change字典中就会包含这个key,
这个key的value是一个NSIndexSet对象,包含更改关系中的索引
5.NSKeyValueChangeNotificationIsPriorKey:如果options中传入NSKeyValueObservingOptionPrior,则在改变前通知的change字典中会包含这个key。
这个key对应的value是NSNumber包装的YES,我们可以这样来判断是不是在改变前的通知[change[NSKeyValueChangeNotificationIsPriorKey] boolValue] == YES]
}
** context:注册方法中传入的context
*/

}

2.3 移除方法

在调用注册方法后,KVO并不会对观察者进行强引用,所以需要注意观察者的生命周期。至少需要在观察者销毁之前,调用以下方法移除观察者,否则如果在观察者被释放后,再次触发KVO监听方法就会导致Crash

- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context;

2.4 使用示例

以下使用KVOperson对象添加观察者为当前viewController,监听person对象的name属性值的改变。当name值改变时,触发KVO的监听方法。

- (void)viewDidLoad {
[super viewDidLoad];

self.person = [HTPerson new];
[self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld) context:NULL];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
self.person.name= @"张三";
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
NSLog(@"keyPath:%@",keyPath);
NSLog(@"object:%@",object);
NSLog(@"change:%@",change);
NSLog(@"context:%@",context);
}

- (void)dealloc
{
[self.person removeObserver:self forKeyPath:@"name"];
}

keyPath:name
object:
change:{ kind = 1; new = "\U70b9\U51fb"; old = ""; }
context:(null)

2.5 实际应用

KVO主要用来做键值观察操作,想要一个值发生改变后通知另一个对象,则用KVO实现最为合适。斯坦福大学的iOS教程中有一个很经典的案例,通过KVOModelController之间进行通信。如图所示: 斯坦福大学 KVO示例

2.6 KVO 触发监听方法的方式

KVO触发分为自动触发和手动触发两种方式。

2.6.1 自动触发

① 如果是监听对象特定属性值的改变,通过以下方式改变属性值会触发KVO

  • 使用点语法
  • 使用setter方法
  • 使用KVCsetValue:forKey:方法
  • 使用KVCsetValue:forKeyPath:方法

② 如果是监听集合对象的改变,需要通过KVCmutableArrayValueForKey:等方法获得代理对象,并使用代理对象进行操作,当代理对象的内部对象发生改变时,会触发KVO。集合对象包含NSArrayNSSet

2.6.2 手动触发

① 普通对象属性或是成员变量使用:

- (void)willChangeValueForKey:(NSString *)key;
- (void)didChangeValueForKey:(NSString *)key;

② NSArray对象使用:

- (void)willChange:(NSKeyValueChange)changeKind valuesAtIndexes:(NSIndexSet *)indexes forKey:(NSString *)key;
- (void)didChange:(NSKeyValueChange)changeKind valuesAtIndexes:(NSIndexSet *)indexes forKey:(NSString *)key;

③ NSSet对象使用:

- (void)willChange:(NSKeyValueChange)changeKind valuesAtIndexes:(NSIndexSet *)indexes forKey:(NSString *)key;
- (void)didChange:(NSKeyValueChange)changeKind valuesAtIndexes:(NSIndexSet *)indexes forKey:(NSString *)key;


收起阅读 »

ios Category无法覆写系统方法?

iOS
Category无法覆写系统方法?这是一次非常有趣的解决问题经历,以至于我认为解决方式可能比问题本身更有意思,另一点就是人多力量大,多人讨论就会获得多种思路。首次提出这个问题的是反向抽烟,他遇到了不能用 Category 覆写系统方法的现象。问题抛到我这,我验...
继续阅读 »

Category无法覆写系统方法?

这是一次非常有趣的解决问题经历,以至于我认为解决方式可能比问题本身更有意思,另一点就是人多力量大,多人讨论就会获得多种思路。

首次提出这个问题的是反向抽烟,他遇到了不能用 Category 覆写系统方法的现象。问题抛到我这,我验证了这个有点奇怪的现象,并决定好好探究一下,重看了 Category 那部分源码仍没有找到合理解释,于是将这个问题抛到开发群里,最后由皮拉夫大王在此给出了最为合理的解释。之后我又顺着他的思路找到了一些更有力的证据。以下是这一过程的经历。

问题提出

以下内容出自反向抽烟:

背景:想为 UITextField 提供单独的属性 placeholderColor ,用来直接设置占位符的颜色,这个时候使用分类设置属性,重写 setter 和 getter,set中直接使用 KVC 的方式对属性的颜色赋值;这个时候就有个bug,如果在其他类中使用 UITextField 这个控件的时候,先设置颜色,再设置文字,会发现占位符的颜色没有发生改变。

解决思路:首先想到 UITextField 中的 Label 是使用的懒加载,当有文字设置的时候,就会初始化这个label,这时候就考虑先设置颜色根本就没起到作用;

解决办法:在分类中 placeholderColor 的 setter 方法中,使用runtime的objc_setAssociatedObject先把颜色保存起来,这样就能保证先设置的颜色不会丢掉,然后需要重写 placeholder的setter方法,让在设置完文字的时候,拿到先前保存的颜色,故要在placeholderColor 的getter中用objc_getAssociatedObject取,这里有个问题点,在分类中重写 placeholder 的setter方法的话,在外面设置 placeholder 的时候,根本不走自己重写的这个 setPlaceholder方法,而走系统自带的,这里我还没研究。然后为了解决这个问题,我自己写了个setDsyPlaceholder方法,在setDsyPlaceholder里面对标签赋值,同时添加已经保存好的颜色,然后与setPlaceholder做交换,bug修复。

这里大家先不要关注解决 placeholderColor 的方式是否正确,以免思路走偏。我们应该避免使用Category 覆写系统方法的,但这里引出了一个问题:如果就是要覆写系统的方法,为啥没被执行?

问题探索

我测试发现自定义类是可以通过 Category 覆写的,只有系统方法不可以。当时选的是 UIViewController 的viewDidLoad 方法,其他几个 UIViewController 方法也试了都不可以。

测试代码如下:

1
2
3
4
5
6
7
8
9
#import "UIViewController+Test.h"

@implementation UIViewController (Test)

- (void)viewDidLoad {
NSLog(@"viewDidLoad");
}

@end

所以猜测:系统方法被做了特殊处理都不能覆写,只有自定义类可以覆写

有一个解释是:系统方法是会被缓存的,方法查找走了缓存,没有查完整的方法表。

这个说法好像能说得通,但是系统缓存是库的层面,方法列表的缓存又是另一个维度了。方法列表的缓存应该是应用间独立进行的,这样才能保证不同应用对系统库的修改不会相互影响,所以这个解释站不住脚。

这时有朋友提出他们之前使用Category 覆写过 UIScreen 的 mainScreen,是可以成功的。我试了下确实可以,观察之后发现该属性是一个类属性。又试了其他几个系统库的类属性,也都是可以的。

所以猜测变成了:只有系统实例方法不能被覆写,类属性,类方法可以覆写

这时已经感觉奇怪了,这个规律也说不通。后来又有朋友测试通过 Xcode10.3 能够覆写系统方法,好嘛。。。

这时的猜测又变成了:苹果在某个特定版本开始才做了系统方法覆写的拦截

可靠的证据

皮拉夫大王在此提出了很关键的信息,他验证了iOS12系统可以覆写系统方法(后来验证iOS13状况相同),iOS14不能覆写。

但iOS14的情况并不是所有的系统方法都覆盖不了,能否覆盖与类方法还是实例方法无关。

例如:UIResponder的分类,重写init 和 isFirstResponderinit可以覆盖,isFirstResponder不能覆盖。在iOS14的系统上NS的类,很多都可以被分类覆盖,但是UIKit的类,在涉及到UI的方法时,很多都无法覆盖。

这里猜测:系统做了白名单,命中白名单的函数会被系统拦截和处理

以下是对 iOS14 状况的验证,覆写isFirstResponder,打印method_list

1
2
3
4
5
6
7
8
unsigned int count;
Method *list = class_copyMethodList(UIResponder.class, &count);
for (int i = 0; i < count; i++) {
Method m = list[i];
if ([NSStringFromSelector(method_getName(m)) isEqualToString:@"isFirstResponder"]) {
IMP imp = method_getImplementation(m);
}
}

isFirstResponder会命中两次,两次po imp的结果是:

1
2
3
4
//第一次
(libMainThreadChecker.dylib`__trampolines + 67272)
//第二次
(UIKitCore`-[UIResponder isFirstResponder])

同样的代码,在iOS12的设备也会命中两次,结果为:

1
2
3
4
//第一次
(SwiftDemo`-[UIResponder(xx) isFirstResponder] at WBOCTest.m:38)
//第二次
(UIKitCore`-[UIResponder isFirstResponder])

所以可以确认的是,分类方法是可以正常添加到系统类的,但在iOS14的系统中,覆写的方法却被libMainThreadChecker.dylib里的方法接管了,导致没有执行。

那么问题来了,这个libMainThreadChecker.dylib库是干嘛的,它做了什么?

这个库对应了Main Thread Checker这个功能,它是在Xcode9新增的,因为开销比较小,只占用1-2%的CPU,启动时间占用时间不到0.1s,所以被默认置为开的状态。它在调试期的作用是帮助我们定位那些应该在主线程执行,却没有放到主线程的代码执行情况。

另外官方文档还有一个解释

The Main Thread Checker tool dynamically replaces system methods that must execute on the main thread with variants that check the current thread. The tool replaces only system APIs with well-known thread requirements, and doesn’t replace all system APIs. Because the replacements occur in system frameworks, Main Thread Checker doesn’t require you to recompile your app.

这个家伙会动态的替换尝试重写需要在主线程执行的系统方法,但也不是所有的系统方法。

终于找到了!这很好的解释了为什么本应被覆盖的系统方法却指向了libMainTreadChecker.dylib这个库,同时也解释了为什么有些方法可以覆写,有些却不可以。

测试发现当我们关闭了这个开关,iOS14的设备就可以正常执行覆写的方法了。

到此基本完事了,但还留有一个小疑问,那就是为什么iOS14之前的设备,不受这个开关的影响?目前没有找到实质的证据表明苹果是如何处理的,但可以肯定的是跟 Main Thread Checker 这个功能有关。

总结

稍微抽象下一开始处理问题的方式:遇到问题 -> 猜想 -> 佐证 -> 推翻猜想 -> 重新猜想 -> 再佐证。

这其实是错误的流程,猜想和佐证可以,但他们一般只会成为一个验证的样例,而不能带给我们答案。所以正确的处理方式是,不要把太多时间浪费在猜想和佐证猜想上,而应该去深挖问题本身。新的解题思路可以是这样的:遇到问题 -> 猜想 -> 深挖 -> 根据挖到的点佐证结果。

链接:https://zhangferry.com/2021/04/21/overwrite_system_category/

收起阅读 »

iOS14开发-网络

iOS
基础知识App如何通过网络请求数据?App 通过一个 URL 向特定的主机发送一个网络请求加载需要的资源。URL 一般是使用 HTTP(HTTPS)协议,该协议会通过 IP(或域名)定位到资源所在的主机,然后等待主机处理和响应。主机通过本次网络请求指...
继续阅读 »

基础知识

App如何通过网络请求数据?

客户服务器模型

  1. App 通过一个 URL 向特定的主机发送一个网络请求加载需要的资源。URL 一般是使用 HTTP(HTTPS)协议,该协议会通过 IP(或域名)定位到资源所在的主机,然后等待主机处理和响应。
  2. 主机通过本次网络请求指定的端口号找到对应的处理软件,然后将网络请求转发给该软件进行处理(处理的软件会运行在特定的端口)。针对 HTTP(HTTPS)请求,处理的软件会随着开发语言的不同而不同,如 Java 的 Tomcat、PHP 的 Apache、.net 的 IIS、Node.js 的 JavaScript 运行时等)
  3. 处理软件针对本次请求进行分析,分析的内容包括请求的方法、路径以及携带的参数等。然后根据这些信息,进行相应的业务逻辑处理,最后通过主机将处理后的数据返回(返回的数据一般为 JSON 字符串)。
  4. App 接收到主机返回的数据,进行解析处理,最后展示到界面上。
  5. 发送请求获取资源的一方称为客户端。接收请求提供服务的一方称为服务端

基本概念

URL

  • Uniform Resource Locator(统一资源定位符),表示网络资源的地址或位置。
  • 互联网上的每个资源都有一个唯一的 URL,通过它能找到该资源。
  • URL 的基本格式协议://主机地址/路径

HTTP/HTTPS

  • HTTP—HyperTextTransferProtocol:超文本传输协议。
  • HTTPS—Hyper Text Transfer Protocol over Secure Socket Layer 或 Hypertext Transfer Protocol Secure:超文本传输安全协议。

请求方法

  • 在 HTTP/1.1 协议中,定义了 8 种发送 HTTP 请求的方法,分别是GET、POST、HEAD、PUT、DELETE、OPTIONS、TRACE、CONNECT
  • 最常用的是 GET 与 POST

响应状态码

状态码描述含义
200Ok请求成功
400Bad Request客户端请求的语法出现错误,服务端无法解析
404Not Found服务端无法根据客户端的请求找到对应的资源
500Internal Server Error服务端内部出现问题,无法完成响应

请求响应过程

请求响应过程

JSON

  • JavaScript Object Notation。
  • 一种轻量级的数据格式,一般用于数据交互。
  • 服务端返回给 App 客户端的数据,一般都是 JSON 格式。

语法

  • 数据以键值对key : value形式存在。
  • 多个数据由,分隔。
  • 花括号{}保存对象。
  • 方括号[]保存数组。

key与value

  • 标准 JSON 数据的 key 必须用双引号""
  • JSON 数据的 value 类型:
    • 数字(整数或浮点数)
    • 字符串("表示)
    • 布尔值(true 或 false)
    • 数组([]表示)
    • 对象({}表示)
    • null

解析

  • 厘清当前 JSON 数据的层级关系(借助于格式化工具)。
  • 明确每个 key 对应的 value 值的类型。
  • 解析技术
    • Codable 协议(推荐)。
    • JSONSerialization。
    • 第三方框架。

URLSession

使用步骤

  1. 创建请求资源的 URL。
  2. 创建 URLRequest,设置请求参数。
  3. 创建 URLSessionConfiguration 用于设置 URLSession 的工作模式和网络设置。
  4. 创建 URLSession。
  5. 通过 URLSession 构建 URLSessionTask,共有 3 种任务。 (1)URLSessionDataTask:请求数据的 Task。  (2)URLSessionUploadTask:上传数据的 Task。 (3)URLSessionDownloadTask:下载数据的 Task。 
  6. 启动任务。
  7. 处理服务端响应,有 2 种方式。 (1)通过 completionHandler(闭包)处理服务端响应。 (2)通过 URLSessionDataDelegate(代理)处理请求与响应过程的事件和接收服务端返回的数据。

基本使用

class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// get()
// post()
}

func get() {
// 1. 确定URL
let url = URL(string: "http://v.juhe.cn/toutiao/index?type=top&key=申请的key")
// 2. 创建请求
let urlRequest = URLRequest(url: url!)
// cachePolicy: 缓存策略,App最常用的缓存策略是returnCacheDataElseLoad,表示先查看缓存数据,没有缓存再请求
// timeoutInterval:超时时间
// let urlRequest = URLRequest(url: url!, cachePolicy: .returnCacheDataElseLoad, timeoutInterval: 5)
let config = URLSessionConfiguration.default
// 3. 创建URLSession
let session = URLSession(configuration: config)
// 4. 创建任务
let task = session.dataTask(with: urlRequest) { data, _, error in
if error != nil {
print(error!)
} else {
if let data = data {
print(String(data: data, encoding: .utf8)!)
}
}
}
// 5. 启动任务
task.resume()
}

func post() {
let url = URL(string: "http://v.juhe.cn/toutiao/index")
var urlRequest = URLRequest(url: url!)
// 指明请求方法
urlRequest.httpMethod = "POST"
// 指明参数
let params = "type=top&key=申请的key"
// 设置请求体
urlRequest.httpBody = params.data(using: .utf8)
let config = URLSessionConfiguration.default
// delegateQueue决定了代理方法在哪个线程中执行
let session = URLSession(configuration: config, delegate: self, delegateQueue: OperationQueue())
let task = session.dataTask(with: urlRequest)
task.resume()
}
}

// MARK:- URLSessionDataDelegate
extension ViewController: URLSessionDataDelegate {
// 开始接收数据
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) {
// 允许接收服务器的数据,默认情况下请求之后不接收服务器的数据即不会调用后面获取数据的代理方法
completionHandler(URLSession.ResponseDisposition.allow)
}

// 获取数据
// 根据请求的数据量该方法可能会调用多次,这样data返回的就是总数据的一段,此时需要用一个全局的Data进行追加存储
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
let result = String(data: data, encoding: .utf8)
if let result = result {
print(result)
}
}

// 获取结束
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
if let error = error {
print(error)
} else {
print("=======成功=======")
}
}
}

注意:如果网络请求是 HTTP 而非 HTTPS,默认情况下,iOS 会阻断该请求,此时需要在 Info.plist 中进行如下配置。

<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>

URL转码与解码

  • 当请求参数带中文时,必须进行转码操作。
let url = "https://www.baidu.com?name=张三"
.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!
print(url) // URL中文转码
print(url.removingPercentEncoding!) // URL中文解码

  • 有时候只需要对URL中的中文处理,而不需要针对整个URL。
let str = "阿楚姑娘"
.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!
let url = URL(string: "https://music.163.com/#/search/m/?s=\(str)&type=1")

下载数据

class ViewController: UIViewController {
// 下载进度
@IBOutlet var downloadProgress: UIProgressView!
// 下载图片
@IBOutlet var downloadImageView: UIImageView!

override func viewDidLoad() {
super.viewDidLoad()

download()
}

func download() {
let url = URL(string: "http://172.20.53.240:8080/AppTestAPI/wall.png")!
let request = URLRequest(url: url)
let session = URLSession(configuration: URLSessionConfiguration.default, delegate: self, delegateQueue: OperationQueue())
let task = session.downloadTask(with: request)
task.resume()
}
}

extension ViewController: URLSessionDownloadDelegate {
// 下载完成
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
// 存入沙盒
let savePath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first!
// 文件类型根据下载的内容决定
let fileName = "\(Int(Date().timeIntervalSince1970)).png"
let filePath = savePath + "/" + fileName
print(filePath)
do {
try FileManager.default.moveItem(at: location, to: URL(fileURLWithPath: filePath))
// 显示到界面
DispatchQueue.main.async {
self.downloadImageView.image = UIImage(contentsOfFile: filePath)
}
} catch {
print(error)
}
}

// 计算进度
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
DispatchQueue.main.async {
self.downloadProgress.setProgress(Float(totalBytesWritten) / Float(totalBytesExpectedToWrite), animated: true)
}
}
}

上传数据

上传数据需要服务端配合,不同的服务端代码可能会不一样,下面的上传代码适用于本人所写的服务端代码

  • 数据格式。

上传数据格式

  • 实现。
class ViewController: UIViewController {
let YFBoundary = "AnHuiWuHuYungFan"
@IBOutlet var uploadInfo: UILabel!
@IBOutlet var uploadProgress: UIProgressView!

override func viewDidLoad() {
super.viewDidLoad()

upload()
}

func upload() {
// 1. 确定URL
let url = URL(string: "http://172.20.53.240:8080/AppTestAPI/UploadServlet")!
// 2. 确定请求
var request = URLRequest(url: url)
// 3. 设置请求头
let head = "multipart/form-data;boundary=\(YFBoundary)"
request.setValue(head, forHTTPHeaderField: "Content-Type")
// 4. 设置请求方式
request.httpMethod = "POST"
// 5. 创建NSURLSession
let session = URLSession(configuration: URLSessionConfiguration.default, delegate: self, delegateQueue: OperationQueue())
// 6. 获取上传的数据(按照固定格式拼接)
var data = Data()
let header = headerString(mimeType: "image/png", uploadFile: "wall.png")
data.append(header.data(using: .utf8)!)
data.append(uploadData())
let tailer = tailerString()
data.append(tailer.data(using: .utf8)!)
// 7. 创建上传任务 上传的数据来自getData方法
let task = session.uploadTask(with: request, from: data) { _, _, error in
// 上传完毕后
if error != nil {
print(error!)
} else {
DispatchQueue.main.async {
self.uploadInfo.text = "上传成功"
}
}
}
// 8. 执行上传任务
task.resume()
}

// 开始标记
func headerString(mimeType: String, uploadFile: String) -> String {
var data = String()
// --Boundary\r\n
data.append("--" + YFBoundary + "\r\n")
// 文件参数名 Content-Disposition: form-data; name="myfile"; filename="wall.jpg"\r\n
data.append("Content-Disposition:form-data; name=\"myfile\";filename=\"\(uploadFile)\"\r\n")
// Content-Type 上传文件的类型 MIME\r\n\r\n
data.append("Content-Type:\(mimeType)\r\n\r\n")

return data
}

// 结束标记
func tailerString() -> String {
// \r\n--Boundary--\r\n
return "\r\n--" + YFBoundary + "--\r\n"
}

func uploadData() -> Data {
let image = UIImage(named: "wall.png")
let imageData = image!.pngData()
return imageData!
}
}

extension ViewController: URLSessionTaskDelegate {
// 上传进去
func urlSession(_ session: URLSession, task: URLSessionTask, didSendBodyData bytesSent: Int64, totalBytesSent: Int64, totalBytesExpectedToSend: Int64) {
DispatchQueue.main.async {
self.uploadProgress.setProgress(Float(totalBytesSent) / Float(totalBytesExpectedToSend), animated: true)
}
}

// 上传出错
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
if let error = error {
print(error)
}
}
}

URLCache

  • 网络缓存有很多好处:节省流量、更快加载、断网可用。
  • 使用 URLCache 管理缓存区域的大小和数据。
  • 每一个 App 都默认创建了一个 URLCache 作为缓存管理者,可以通过URLCache.shared获取,也可以自定义。
// 创建URLCache
// memoryCapacity:内存缓存容量
// diskCapacity:硬盘缓存容量
// directory:硬盘缓存路径
let cache = URLCache(memoryCapacity: 10 * 1024 * 1024, diskCapacity: 100 * 1024 * 1024, directory: FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first)
// 替换默认的缓存管理对象
URLCache.shared = cache

  • 常见属性与方法。
let url = URL(string: "http://v.juhe.cn/toutiao/index?type=top&key=申请的key")
let urlRequest = URLRequest(url: url!, cachePolicy: .returnCacheDataElseLoad, timeoutInterval: 5)
let cache = URLCache.shared

// 内存缓存大小
cache.memoryCapacity
// 硬盘缓存大小
cache.diskCapacity
// 已用内存缓存大小
cache.currentMemoryUsage
// 已用硬盘缓存大小
cache.currentDiskUsage
// 获取某个请求的缓存
let cacheResponse = cache.cachedResponse(for: urlRequest)
// 删除某个请求的缓存
cache.removeCachedResponse(for: urlRequest)
// 删除某个时间点开始的缓存
cache.removeCachedResponses(since: Date().addingTimeInterval(-60 * 60 * 48))
// 删除所有缓存
cache.removeAllCachedResponses()

WKWebView

  • 用于加载 Web 内容的控件。
  • 使用时必须导入WebKit模块。

基本使用

  • 加载网页。
// 创建URL
let url = URL(string: "https://www.abc.edu.cn")
// 创建URLRequest
let request = URLRequest(url: url!)
// 创建WKWebView
let webView = WKWebView(frame: UIScreen.main.bounds)
// 加载网页
webView.load(request)

  • 加载本地资源。
// 文件夹路径
let basePath = Bundle.main.path(forResource: "localWeb", ofType: nil)!
// 文件夹URL
let baseUrl = URL(fileURLWithPath: basePath, isDirectory: true)
// html路径
let filePath = basePath + "/index.html"
// 转成文件
let fileContent = try? NSString(contentsOfFile: filePath, encoding: String.Encoding.utf8.rawValue)
// 创建WKWebView
let webView = WKWebView(frame: UIScreen.main.bounds)
// 加载html
webView.loadHTMLString(fileContent! as String, baseURL: baseUrl)

注意:如果是本地资源是文件夹,拖进项目时,需要勾选Create folder references,然后用Bundle.main.path(forResource: "文件夹名", ofType: nil)获取资源路径。

与JavaScript交互

创建WKWebView

lazy var webView: WKWebView = {
// 创建WKPreferences
let preferences = WKPreferences()
// 开启JavaScript
preferences.javaScriptEnabled = true
// 创建WKWebViewConfiguration
let configuration = WKWebViewConfiguration()
// 设置WKWebViewConfiguration的WKPreferences
configuration.preferences = preferences
// 创建WKUserContentController
let userContentController = WKUserContentController()
// 配置WKWebViewConfiguration的WKUserContentController
configuration.userContentController = userContentController
// 给WKWebView与Swift交互起一个名字:callbackHandler,WKWebView给Swift发消息的时候会用到
// 此句要求实现WKScriptMessageHandler
configuration.userContentController.add(self, name: "callbackHandler")
// 创建WKWebView
var webView = WKWebView(frame: UIScreen.main.bounds, configuration: configuration)
// 让WKWebView翻动有回弹效果
webView.scrollView.bounces = true
// 只允许WKWebView上下滚动
webView.scrollView.alwaysBounceVertical = true
// 设置代理WKNavigationDelegate
webView.navigationDelegate = self
// 返回
return webView
}()

创建HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0,user-scalable=no"/>
</head>
<body>
iOS传过来的值:<span id="name"></span>
<button onclick="responseSwift()">响应iOS</button>
<script type="text/javascript">
// 给Swift调用
function sayHello(name) {
document.getElementById("name").innerHTML = name
return "Swift你也好!"
}
// 调用Swift方法
function responseSwift() {
// 这里的callbackHandler是创建WKWebViewConfiguration是定义的
window.webkit.messageHandlers.callbackHandler.postMessage("JavaScript发送消息给Swift")
}
</script>
</body>
</html>

两个协议

  • WKNavigationDelegate:判断页面加载完成,只有在页面加载完成后才能在实现 Swift 调用 JavaScript。WKWebView 调用 JavaScript:
// 加载完毕以后执行
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
// 调用JavaScript方法
webView.evaluateJavaScript("sayHello('WebView你好!')") { (result, err) in
// result是JavaScript返回的值
print(result, err)
}
}

  • WKScriptMessageHandler:JavaScript 调用 Swift 时需要用到协议中的一个方法来。JavaScript 调用 WKWebView:
// Swift方法,可以在JavaScript中调用
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
print(message.body)
}

ViewController

class ViewController: UIViewController {
// 懒加载WKWebView
...

// 加载本地html
let html = try! String(contentsOfFile: Bundle.main.path(forResource: "index", ofType: "html")!, encoding: String.Encoding.utf8)

override func viewDidLoad() {
super.viewDidLoad()
// 标题
title = "WebView与JavaScript交互"
// 加载html
webView.loadHTMLString(html, baseURL: nil)
view.addSubview(webView)
}
}

// 遵守两个协议
extension ViewController: WKNavigationDelegate, WKScriptMessageHandler {
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
...
}

func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
...
}
}

SFSafariViewController

  • iOS 9 推出的一种 UIViewController,用于加载与显示 Web 内容,打开效果类似 Safari 浏览器的效果。
  • 使用时必须导入SafariServices模块。
import SafariServices

class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
}

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
showSafariViewController()
}

func showSafariViewController() {
// URL
let url = URL(string: "https://www.baidu.com")
// 创建SFSafariViewController
let sf = SFSafariViewController(url: url!)
// 设置代理
sf.delegate = self
// 显示
present(sf, animated: true, completion: nil)
}
}

extension ViewController: SFSafariViewControllerDelegate {
// 点击左上角的完成(done)
func safariViewControllerDidFinish(_ controller: SFSafariViewController) {
print(#function)
}

// 加载完成
func safariViewController(_ controller: SFSafariViewController, didCompleteInitialLoad didLoadSuccessfully: Bool) {
print(#function)
}
}
收起阅读 »

Swift 5.5 新特性

iOS
Swift 5.5 内置于 Xcode 13,虽然版本号只增加了 0.1,看似是一个小版本升级,但却带来了非常多的新内容,其中最大的更新是引入了全新的并发编程方式。条件编译支持表达式SwiftUI 在跨平台时会使用到条件 Modifier,之前的解决方案是自己...
继续阅读 »

Swift 5.5 内置于 Xcode 13,虽然版本号只增加了 0.1,看似是一个小版本升级,但却带来了非常多的新内容,其中最大的更新是引入了全新的并发编程方式。

条件编译支持表达式

SwiftUI 在跨平台时会使用到条件 Modifier,之前的解决方案是自己写一套判断体系, Swift 5.5 以后,原生支持条件编译表达式,跨平台更加方便。

struct ContentView: View {
var body: some View {
Text("SwiftUI")
#if os(iOS)
.foregroundColor(.blue)
#elseif os(macOS)
.foregroundColor(.green)
#else
.foregroundColor(.pink)
#endif
}
}
复制代码

CGFloat与Double支持隐式转换

let number1: CGFloat = 12.34
let number2: Double = 56.78
let result = number1 + number2 // result为Double类型
复制代码

下面的代码在 Swift 5.5 之前会报错,因为scale为 Double 类型,而 SwiftUI 中需要绑定 CGFloat 类型。

struct ContentView: View {
@State private var scale = 1.0 // Double类型

var body: some View {
VStack {
Image(systemName: "heart")
.scaleEffect(scale) // 隐式转换为CGFloat

Slider(value: $scale, in: 0 ... 1)
}
}
}
复制代码

在通用上下文中扩展静态成员查找(static member lookup)

这个新特性使得 SwiftUI 中的部分语法更加简洁好用。

struct ContentView: View {
@Binding var name: String

var body: some View {
HStack {
Text(name)

TextField("", text: $name)
// .textFieldStyle(RoundedBorderTextFieldStyle()) // 以前写法
.textFieldStyle(.roundedBorder) // 新写法,更简洁
}
}
}
复制代码

局部变量支持lazy

func lazyInLocalContext() {
print("lazy之前")
lazy var swift = "Hello Swift 5.5"
print("lazy之后")

print(swift)
}

// 调用
lazyInLocalContext()

/* 输出
lazy之前
lazy之后
Hello Swift 5.5
*/
复制代码

函数和闭包参数支持属性包装

  • Swift 5.1 中引入了属性包装。
  • Swift 5.4 将属性包装支持到局部变量。
  • Swift 5.5 将属性包装支持到函数和闭包参数。
@propertyWrapper struct Trimmed {
private var value: String = ""

var wrappedValue: String {
get { value }
set { value = newValue.trimmingCharacters(in: .whitespacesAndNewlines) }
}

init(wrappedValue initialValue: String) {
wrappedValue = initialValue
}
}

struct Post {
func trimed(@Trimmed content: String) { // 函数参数支持PropertyWrapper
print(content)
}
}

let post = Post()
post.trimed(content: " Swift 5.5 Property Wrappers ")
复制代码

带有关联值的枚举支持Codable

有了该功能之后,枚举就可以像结构体、类一样用来作为数据模型了。

  • 枚举到 JSON。
// 定义带有关联值的枚举
enum Score: Codable {
case number(score: Double)
case letter(score: String)
}

// 创建对象
let scores: [Score] = [.number(score: 98.5), .letter(score: "优")]

// 转JSON
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
do {
let result = try encoder.encode(scores)
let json = String(decoding: result, as: UTF8.self)
print(json)
} catch {
print(error.localizedDescription)
}
复制代码
  • JSON 到枚举。
enum Score: Codable {
case number(score: Double)
case letter(score: String)
}

// JSON
let json = """
[
{
"number" : {
"score" : 98.5
}
},
{
"letter" : {
"score" : "优"
}
}
]
"""

// 转枚举
let decoder = JSONDecoder()
do {
let scores = try decoder.decode([Score].self, from: json.data(using: .utf8)!)
for score in scores {
switch score {
case let .number(value):
print(value)
case let .letter(value):
print(value)
}
}
} catch {
print(error.localizedDescription)
}
复制代码

并发编程

内容较多且尚不稳定,后面会单独出《Swift 5.5 Concurrency》

收起阅读 »

iOS - Core Graphics快速入门——从一行代码说起

iOS
Core Graphics入门想必每个第一次接触Core Graphics的开发者都被无数的API、混乱的代码逻辑折腾得头疼不已,甚至望而却步。即使是绘制一个简单的矩形也看上去非常繁琐。本文换一个角度,整理一下有关Core Graphics的知识,也算作是这段...
继续阅读 »

Core Graphics入门

想必每个第一次接触Core Graphics的开发者都被无数的API、混乱的代码逻辑折腾得头疼不已,甚至望而却步。即使是绘制一个简单的矩形也看上去非常繁琐。本文换一个角度,整理一下有关Core Graphics的知识,也算作是这段时间学习的总结。

Core Graphics和UIKit的区别

首先从概念上了解一下:


根据苹果的描述,UIKit是我们最容易也是最常接触到的框架。绝大多数图形界面都由UIKit完成。但是UIKit依赖于Core Graphics框架,也是基于Core Graphics框架实现的。如果想要完成某些更底层的功能或者追求极致的性能,那么依然推荐使用Core Graphics完成。

Core Graphics和UIKit在实际使用中也存在以下这些差异:

  1. Core Graphics其实是一套基于C的API框架,使用了Quartz作为绘图引擎。这也就意味着Core Graphics不是面向对象的。
  2. Core Graphics需要一个图形上下文(Context)。所谓的图形上下文(Context),说白了就是一张画布。这一点非常容易理解,Core Graphics提供了一系列绘图API,自然需要指定在哪里画图。因此很多API都需要一个上下文(Context)参数。
  3. Core Graphics的图形上下文(Context)是堆栈式的。只能在栈顶的上下文(画布)上画图。
  4. Core Graphics中有一些API,名称不同却有着相似的功能,新手只需要掌握一种,并能够看懂其他的即可。

从一行代码说起

下面这行代码应该是很多人最早也是最常写的代码。它简单到我们根本不用思考它的本质。

[self.view addSubview:myButton];

细想一下,UIButton也是继承自UIView。这段代码表示,UIKit绘图的基本思想是通过UIView的叠加实现最终的整体效果。它主要涉及三个内容:画布、被添加的控件和添加方法。这里的self.view其实就充当了一张画布。通过添加不同的UI控件达到最终效果。我们顺着这个线索整理一下Core Graphics的编程思路。

Core Graphics的基本使用

为了使用Core Graphics来绘图,最简单的方法就是自定义一个类继承自UIView,并重写子类的drawRect方法。在这个方法中绘制图形。
Core Graphics必须一个画布,才能把东西画在这个画布上。在drawRect方法方法中,我们可以直接获取当前栈顶的上下文(Context)。下面的代码演示了具体操作步骤:

- (void)drawRect:(CGRect)rect {
CGContextRef ctx = UIGraphicsGetCurrentContext();
}

现在我们已经完成了Core Graphics绘图的三分之一——创建一个画布。
接下来需要考虑被画上去的东西。这在UIKit中往往是一个UI控件,如Button、Label等。而在Core Graphics中通常表现为一些基本图形:三角形、矩形、圆形、以及这些图形的边框等。

这通常会涉及到非常多的API,但是如果总结一下不难发现,任何一个要绘制的东西(为了避免混淆就不称为对象了)一定有一个边框,或者称为边界。在一个几英寸的屏幕上画出无界的图形是不可能的。所以一旦确定了一个边框,我们就可以设置边框的各种绘图属性、边框内部区域的绘图属性、绘制边框还是内部区域等。

这就引出了Core Graphics中的路径(Path)的概念。在前一段代码的基础上演示路径的使用:

- (void)drawSomething{
CGContextRef context = UIGraphicsGetCurrentContext();//获取上下文
CGMutablePathRef path = CGPathCreateMutable();//创建路径
CGPathMoveToPoint(path, nil, 20, 50);//移动到指定位置(设置路径起点)
CGPathAddLineToPoint(path, nil, 20, 100);//绘制直线(从起始位置开始)
CGContextAddPath(context, path);//把路径添加到上下文(画布)中
}

这里通过CGPathCreateMutable方法创建了一个路径。路径的外在表现就像一条折线。为了绘制一条路径,需要用CGPathMoveToPoint函数指定路径的起点。CGPathAddLineToPoint函数表示在路径的最后结束点和新的点之间再加一条直线。相当于拓展了原来路径。通过这样的简单的点的累加,可以绘制非常复杂的折线。

但这存在两个问题:

  1. 绘制矩形等规则多边形的过程过于繁琐
  2. 无法绘制曲线。

这些问题Core Graphics早已提供了解决办法。注意到之前我们添加了一个非常普通的自定义路径。Core Graphics中还提供了很多预先设置好的路径。不妨在drawRect方法中输入“cgcontextadd”试试看。

技术分享

这些方法由Core Graphics提供,可以用来绘制圆形、椭圆、矩形、二次曲线等路径。创建完路径后还要记得调用CGContextAddPath方法将路径添加到上下文中。路径只是我们画的一条线而已,不把他画到上,他就没有什么卵用。

添加好路径后,就要开始画图了。正如前面提出的问题所说,画图的时候需要考虑画不画边框、画不画边框内部的区域,边框的粗细、颜色、内部区域颜色等问题。Core Graphics提供了另一个方法集合”CGContextSet”来进行这些设置。常见的设置内容如下:

- (void)drawSomething{
CGContextRef context = UIGraphicsGetCurrentContext();//获取上下文
CGMutablePathRef path = CGPathCreateMutable();//创建路径
CGPathMoveToPoint(path, nil, 20, 50);//移动到指定位置(设置路径起点)
CGPathAddLineToPoint(path, nil, 20, 100);//绘制直线(从起始位置开始)
CGContextAddPath(context, path);//把路径添加到上下文(画布)中

//设置图形上下文状态属性
CGContextSetRGBStrokeColor(context, 1.0, 0, 0, 1);//设置笔触颜色
CGContextSetRGBFillColor(context, 0, 1.0, 0, 1);//设置填充色
CGContextSetLineWidth(context, 2.0);//设置线条宽度
CGContextSetLineCap(context, kCGLineCapRound);//设置顶点样式
CGContextSetLineJoin(context, kCGLineJoinRound);//设置连接点样式
CGFloat lengths[2] = { 18, 9 };
CGContextSetLineDash(context, 0, lengths, 2);
CGContextSetShadowWithColor(context, CGSizeMake(2, 2), 0, [UIColor blackColor].CGColor);
CGContextDrawPath(context, kCGPathFillStroke);//最后一个参数是填充类型
}

设置属性的前三行就不再解释了,看一些注释足矣。顶点指的是路径的起始点和结束点,连接点指的是路径中的转折点(折现才有)。SetLineDash用于绘制虚线,具体用法参见——《IOS中使用Quartz 2D绘制虚线》。SetShadow方法用于绘制阴影,第二个参数是一个CGSize对象,用于表示阴影偏移量,第三个参数表示模糊度,数值越大,阴影越模糊,第一个参数是一个CGColor,表示阴影颜色,需要由UIColor转换得到。

至此,我们完成了Core Graphics绘图的第二步,也是最复杂的一部分:设置绘图内容。这相当于此前那行代码的中的UI控件。

设置好了绘图的属性之后,就可以调用CGContextDrawPath方法绘图了。第一个参数表示要在哪一个上下文中绘图,第二个参数表示填充类型。在填充类型中可以选择只绘制边框、只填充、同时绘制边框和填充内部区域、奇偶规则填充等。


从方法名不难看出,但是也需要注意的是,这些设置都是对上下文(context)生效的。这样会导致,所有的边框颜色、粗细都一样。一个简单的解决办法就是在需要修改设置之前调用一次CGContextDrawPath方法绘图。再修改设置,修改设置之后再次绘制。

图画完了,还得做一下清理工作。CGPathCreateMutable方法返回的路径是一个Core Fundation Object。而这并不在ARC的管理范围之内。所以需要手动释放对象。

- (void)drawRect:(CGRect)rect {
CGContextRef context = UIGraphicsGetCurrentContext();//获取上下文
CGMutablePathRef path = CGPathCreateMutable();//创建路径
/*
绘图
*/

CGPathRelease(path);
}

这样就完成了Core Graphics绘图的第三部分——开始绘图。
再总结一下使用Core Graphics绘图的步骤:

  1. 获取上下文(画布)
  2. 创建路径(自定义或者调用系统的API)并添加到上下文中。
  3. 进行绘图内容的设置(画笔颜色、粗细、填充区域颜色、阴影、连接点形状等)
  4. 开始绘图(CGContextDrawPath)
  5. 释放路径(CGPathRelease)
收起阅读 »

iOS - 绘图框架CoreGraphics分析

iOS
由于CoreGraphics框架有太多的API,对于初次接触或者对该框架不是十分了解的人,在绘图时,对API的选择会感到有些迷茫,甚至会觉得iOS的图形绘制有些繁琐。因此,本文主要介绍一下iOS的绘图方法和分析一下CoreGraphics框架的绘图原理。一、绘...
继续阅读 »

由于CoreGraphics框架有太多的API,对于初次接触或者对该框架不是十分了解的人,在绘图时,对API的选择会感到有些迷茫,甚至会觉得iOS的图形绘制有些繁琐。因此,本文主要介绍一下iOS的绘图方法和分析一下CoreGraphics框架的绘图原理。

一、绘图系统简介

iOS的绘图框架有多种,我们平常最常用的就是UIKit,其底层是依赖CoreGraphics实现的,而且绝大多数的图形界面也都是由UIKit完成,并且UIImage、NSString、UIBezierPath、UIColor等都知道如何绘制自己,也提供了一些方法来满足我们常用的绘图需求。除了UIKit,还有CoreGraphics、Core Animation,Core Image,OpenGL ES等多种框架,来满足不同的绘图要求。各个框架的大概介绍如下:

  • UIKit:最常用的视图框架,封装度最高,都是OC对象

  • CoreGraphics:主要绘图系统,常用于绘制自定义视图,纯C的API,使用Quartz2D做引擎

  • CoreAnimation:提供强大的2D和3D动画效果

  • CoreImage:给图片提供各种滤镜处理,比如高斯模糊、锐化等

  • OpenGL-ES:主要用于游戏绘制,但它是一套编程规范,具体由设备制造商实现

绘图系统


二、绘图方式

实际的绘图包括两部分:视图绘制视图布局,它们实现的功能是不同的,在理解这两个概念之前,需要了解一下什么是绘图周期,因为都是在绘图周期中进行绘制的。

绘图周期:

  • iOS在运行循环中会整合所有的绘图请求,并一次将它们绘制出来

  • 不能在子线程中绘制,也不能进行复杂的操作,否则会造成主线程卡顿

1.视图绘制

调用UIView的drawRect:方法进行绘制。如果调用一个视图的setNeedsDisplay方法,那么该视图就被标记为重新绘制,并且会在下一次绘制周期中重新绘制,自动调用drawRect:方法。

2.视图布局

调用UIView的layoutSubviews方法。如果调用一个视图的setNeedsLayout方法,那么该视图就被标记为需要重新布局,UIKit会自动调用layoutSubviews方法及其子视图的layoutSubviews方法。

在绘图时,我们应该尽量多使用布局,少使用绘制,是因为布局使用的是GPU,而绘制使用的是CPU。GPU对于图形处理有优势,而CPU要处理的事情较多,且不擅长处理图形,所以尽量使用GPU来处理图形。

三、绘图状态切换

iOS的绘图有多种对应的状态切换,比如:pop/push、save/restore、context/imageContext和CGPathRef/UIBezierPath等,下面分别进行介绍:

1.pop / push

设置绘图的上下文环境(context)

push:UIGraphicsPushContext(context)把context压入栈中,并把context设置为当前绘图上下文

pop:UIGraphicsPopContext将栈顶的上下文弹出,恢复先前的上下文,但是绘图状态不变

下面绘制的视图是黑色

- (void)drawRect:(CGRect)rect {
[[UIColor redColor] setFill];
UIGraphicsPushContext(UIGraphicsGetCurrentContext());
[[UIColor blackColor] setFill];
UIGraphicsPopContext();
UIRectFill(CGRectMake(90, 340, 100, 100)); // black color
}

2.save / restore

设置绘图的状态(state)

save:CGContextSaveGState 压栈当前的绘图状态,仅仅是绘图状态,不是绘图上下文

restore:恢复刚才保存的绘图状态

下面绘制的视图是红色

- (void)drawRect:(CGRect)rect {
[[UIColor redColor] setFill];
CGContextSaveGState(UIGraphicsGetCurrentContext());
[[UIColor blackColor] setFill];
CGContextRestoreGState(UIGraphicsGetCurrentContext());
UIRectFill(CGRectMake(90, 200, 100, 100)); // red color
}

3.context / imageContext

iOS的绘图必须在一个上下文中绘制,所以在绘图之前要获取一个上下文。如果是绘制图片,就需要获取一个图片的上下文;如果是绘制其它视图,就需要一个非图片上下文。对于上下文的理解,可以认为就是一张画布,然后在上面进行绘图操作。

context:图形上下文,可以通过UIGraphicsGetCurrentContext:获取当前视图的上下文

imageContext:图片上下文,可以通过UIGraphicsBeginImageContextWithOptions:获取一个图片上下文,然后绘制完成后,调用UIGraphicsGetImageFromCurrentImageContext获取绘制的图片,最后要记得关闭图片上下文UIGraphicsEndImageContext。

4.CGPathRef / UIBezierPath

图形的绘制需要绘制一个路径,然后再把路径渲染出来,而CGPathRef就是CoreGraphics框架中的路径绘制类,UIBezierPath是封装CGPathRef的面向OC的类,使用更加方便,但是一些高级特性还是不及CGPathRef。

四、具体绘图方法

由于iOS常用的绘图框架有UIKit和CoreGraphics两个,所以绘图的方法也有多种,下面介绍一下iOS的几种常用的绘图方法。

1.图片类型的上下文

图片上下文的绘制不需要在drawRect:方法中进行,在一个普通的OC方法中就可以绘制

使用UIKit实现

// 获取图片上下文
UIGraphicsBeginImageContextWithOptions(CGSizeMake(100,100), NO, 0);
// 绘图
UIBezierPath* p = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(0,0,100,100)];
[[UIColor blueColor] setFill];
[p fill];
// 从图片上下文中获取绘制的图片
UIImage* im = UIGraphicsGetImageFromCurrentImageContext();
// 关闭图片上下文
UIGraphicsEndImageContext();

使用CoreGraphics实现

// 获取图片上下文
UIGraphicsBeginImageContextWithOptions(CGSizeMake(100,100), NO, 0);
// 绘图
CGContextRef con = UIGraphicsGetCurrentContext();
CGContextAddEllipseInRect(con, CGRectMake(0,0,100,100));
CGContextSetFillColorWithColor(con, [UIColor blueColor].CGColor);
CGContextFillPath(con);
// 从图片上下文中获取绘制的图片
UIImage* im = UIGraphicsGetImageFromCurrentImageContext();
// 关闭图片上下文
UIGraphicsEndImageContext();

2.drawRect:

在UIView子类的drawRect:方法中实现图形重新绘制,绘图步骤如下:

  • 获取上下文

  • 绘制图形

  • 渲染图形

UIKit方法

- (void) drawRect: (CGRect) rect {
UIBezierPath* p = [UIBezierPathbezierPathWithOvalInRect:CGRectMake(0,0,100,100)];
[[UIColor blueColor] setFill];
[p fill];
}

CoreGraphics

- (void) drawRect: (CGRect) rect {
CGContextRef con = UIGraphicsGetCurrentContext();
CGContextAddEllipseInRect(con, CGRectMake(0,0,100,100));
CGContextSetFillColorWithColor(con, [UIColor blueColor].CGColor);
CGContextFillPath(con);
}

3.drawLayer:inContext:

在UIView子类的drawLayer:inContext:方法中也可以实现绘图任务,它是一个图层的代理方法,而为了能够调用该方法,需要给图层的delegate设置代理对象,其中代理对象不能是UIView对象,因为UIView对象已经是它内部根层(隐式层)的代理对象,再将它设置为另一个层的代理对象就会出问题。

一个view被添加到其它view上时,图层的变化如下:

  • 先隐式地把此view的layer的CALayerDelegate设置成此view

  • 调用此view的self.layer的drawInContext方法

  • 由于drawLayer方法的注释:If defined, called by the default implementation of -drawInContext:说明了drawInContext里if([self.delegate responseToSelector:@selector(drawLayer:inContext:)])就执行drawLayer:inContext:方法,这里我们因为实现了drawLayer:inContext:所以会执行

  • [super drawLayer:layer inContext:ctx]会让系统自动调用此view的drawRect:方法,至此self.layer画出来了

  • 在self.layer上再加一个子layer,当调用[layer setNeedsDisplay];时会自动调用此layer的drawInContext方法

  • 如果drawRect不重写,就不会调用其layer的drawInContext方法,也就不会调用drawLayer:inContext方法

调用内部根层的drawLayer:inContext:

//如果drawRect不重写,就不会调用其layer的drawInContext方法,也就不会调用drawLayer:inContext方法
-(void)drawRect:(CGRect)rect{
NSLog(@"2-drawRect:");
NSLog(@"drawRect里的CGContext:%@",UIGraphicsGetCurrentContext());
//得到的当前图形上下文正是drawLayer中传递过来的
[super drawRect:rect];
}
#pragma mark - CALayerDelegate
-(void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx{
NSLog(@"1-drawLayer:inContext:");
NSLog(@"drawLayer里的CGContext:%@",ctx);
// 如果去掉此句就不会执行drawRect!!!!!!!!
[super drawLayer:layer inContext:ctx];
}

调用外部代理对象的drawLayer:inContext:

由于不能把UIView对象设置为CALayerDelegate的代理,所以我们需要创建一个NSObject对象,然后实现drawLayer:inContext:方法,这样就可以在代理对象里绘制所需图形。另外,在设置代理时,不需要遵守CALayerDelegate的代理协议,即这个方法是NSObject的,不需要显式地指定协议。

// MyLayerDelegate.m
- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx
{
CGContextAddEllipseInRect(ctx, CGRectMake(100,100,100,100));
CGContextSetFillColorWithColor(ctx, [UIColor blueColor].CGColor);
CGContextFillPath(ctx);
}
// ViewController.m
@interface ViewController () @property (nonatomic, strong) id myLayerDelegate;
@end
@implementation ViewController
- (void)viewDidLoad {
// 设置layer的delegate为NSObject子类对象
_myLayerDelegate = [[MyLayerDelegate alloc] init];
MyView *myView = [[MyView alloc] initWithFrame:self.view.bounds];
[self.view addSubview:myView];
CALayer *layer = [CALayer layer];
layer.backgroundColor = [UIColor magentaColor].CGColor;
layer.bounds = CGRectMake(0, 0, 300, 500);
layer.anchorPoint = CGPointZero;
layer.delegate = _myLayerDelegate;
[layer setNeedsDisplay];
[myView.layer addSublayer:layer];
}

详细实现过程

当UIView需要显示时,它内部的层会准备好一个CGContextRef(图形上下文),然后调用delegate(这里就是UIView)的drawLayer:inContext:方法,并且传入已经准备好的CGContextRef对象。而UIView在drawLayer:inContext:方法中又会调用自己的drawRect:方法。平时在drawRect:中通过UIGraphicsGetCurrentContext()获取的就是由层传入的CGContextRef对象,在drawRect:中完成的所有绘图都会填入层的CGContextRef中,然后被拷贝至屏幕。

iOS绘图框架分析如上,如有不足之处,欢迎指出,共同进步。(本文图片来自互联网,版权归原作者所有)

收起阅读 »

Swift算法俱乐部:Swift队列数据结构(Queue)

准备开始队列(Queue)是一个列表,您只能在后面插入新项目并从前面删除项目。 这可确保入队的第一个元素也是首先出队的元素。 先到先出在许多算法中,我们希望在某个时间点将项目添加到临时列表中,然后在以后再次将它们从列表中拉出。 添加和删除这些项目的顺序非常重要...
继续阅读 »

准备开始

队列(Queue)是一个列表,您只能在后面插入新项目并从前面删除项目。 这可确保入队的第一个元素也是首先出队的元素。 先到先出
在许多算法中,我们希望在某个时间点将项目添加到临时列表中,然后在以后再次将它们从列表中拉出。 添加和删除这些项目的顺序非常重要。

队列提供先进先出或先入先出的顺序。 首先插入的元素也是第一个出来的元素(和堆栈(Stack)非常类似,是LIFO或后进先出。)

这是一个栗子
理解队列的最简单方法是看看它是如何使用的。

想象一下你有一个队列。 以下是你如何入选一个数字:

queue.enqueue(10)

队列现在是[10]。 然后,继续将下一个号码添加到队列中:

queue.enqueue(3)

队列现在是[10,3]。 继续添加:

queue.enqueue(57)

队列现在是[10,3,57]。 我们可以将队列中的第一个元素从队列中拉出:

queue.dequeue()

将返回10,因为这是插入的第一个数字。 队列现在将是[3,57]。 每个项目都向上移动一个地方。

queue.dequeue()

这将返回3.下一个出列将返回57,依此类推。 如果队列为空,则出队将返回零。

实现队列
在本节中,将实现一个存储Int值的简单通用队列。
创建一个新的playground,添加如下代码:

public struct Queue {

}

playground还包含LinkedList的代码(可以通过转到查看 Project Navigators Show Project Navigator并打开Sources LinkedList来看到这一点。

入队(Enqueue)

队列需要入队方法。 我们使用项目中包含的LinkedList实现来实现队列。 在花括号之间添加以下内容:

// 1
fileprivate var list = LinkedList<Int>()

// 2
public mutating func enqueue(_ element: Int) {
list.append(element)
}
  1. 添加了一个fileprivate LinkedList变量,用于将这些项目存储在队列中。
  2. 已经添加了一个方法来排列项目。 这个方法会改变底层的LinkedList,所以明确地指定了在方法前加上mutating关键字。

出列(Dequeue)

队列也需要一个出队方法。

// 1
public mutating func dequeque() -> Int? {
// 2
guard !list.isEmpty, let element = list.first else { return nil}

list.remove(element)

return element.value
}
  1. 添加一个返回队列中第一个项目的出队方法。 返回类型可以为空来处理队列为空。
  2. 使用guard语句处理队列为空。 如果这个队列是空的,那么guard将会进入else块。

查看(Peek)

队列还需要一个peek方法,它在队列的开始处返回该项目而不删除它。

public func peek() -> Int? {
return list.first?.value
}

IsEmpty

队列可以是空的。 添加一个isEmpty属性,该属性将返回基于LinkedList的值:

public var isEmpty: Bool {
return list.isEmpty
}

打印队列

让我们试试新队列。 在队列实现下面,将以下内容写入playground中:

var queue = Queue()
queue.enqueue(10)
queue.enqueue(3)
queue.enqueue(57)

定义队列后,尝试将队列打印到控制台:

print(queue)

输出如下:

Queue(list: [10, 3, 57])

这输出的样式不是很好。 要显示更可读的输出字符串,可以使队列采用CustomStringConvertable协议。 为此,请在Queue类的实现下方添加以下内容:

// 1
extension Queue: CustomStringConvertible {
// 2
public var description: String {
// 3
return list.description
}
}
  1. 声明Queue类的扩展,让它遵循CustomStringConvertible协议。 该协议期望使用字符串类型实现带名称描述的计算属性。
  2. 声明了description属性。 这是一个计算属性,它是一个返回String的只读属性。
  3. 返回基于LinkedList的描述。

现在控制台的输出编程如下样式:

[10, 3, 57]

Swift通用队列实现
此时,我们已经实现了一个存储Int值的通用队列,并提供了在Queue类中查看,排队和出列项目的功能。
在本节中,我们使用泛型从队列中抽象出类型需求。

将Queue类的实现更新为以下内容:

// 1
public struct Queue<T> {
// 2
fileprivate var list = LinkedList<T>()

// 3
public mutating func enqueue(_ element: T) {
list.append(element)
}

// 4
public mutating func dequeque() -> T? {

guard !list.isEmpty, let element = list.first else { return nil}

list.remove(element)

return element.value
}
// 5
public func peek() -> T? {
return list.first?.value
}

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

修正测试代码如下:

var queue = Queue<Int>()
queue.enqueue(10)
queue.enqueue(3)
queue.enqueue(57)
print(queue)

还可以尝试使用不同类型的Queue:

var queue2 = Queue<String>()
queue2.enqueue("mad")
queue2.enqueue("lad")
if let first = queue2.dequeque() {
print(first)
}
print(queue2)


收起阅读 »

iOS 类方法load和initialize的区别

Objective-C作为一门面向对象语言,有类和对象的概念。编译后,类相关的数据结构会保留在目标文件中,在运行时得到解析和使用。在应用程序运行起来的时候,类的信息会有加载和初始化过程。就像Application有生命周期回调方法一样,在Objective-C...
继续阅读 »

Objective-C作为一门面向对象语言,有类和对象的概念。编译后,类相关的数据结构会保留在目标文件中,在运行时得到解析和使用。在应用程序运行起来的时候,类的信息会有加载和初始化过程。
就像Application有生命周期回调方法一样,在Objective-C的类被加载和初始化的时候,也可以收到方法回调,可以在适当的情况下做一些定制处理。而这正是load和initialize方法可以帮我们做到的。

  • (void)load;
  • (void)initialize;

可以看到这两个方法都是以“+”开头的类方法,返回为空。通常情况下,我们在开发过程中可能不必关注这两个方法。如果有需要定制,我们可以在自定义的NSObject子类中给出这两个方法的实现,这样在类的加载和初始化过程中,自定义的方法可以得到调用。
+load

顾名思义,+load方法在这个文件被程序装载时调用。只要是在Compile Sources中出现的文件总是会被装载,这与这个类是否被用到无关,因此+load方法总是在main函数之前调用。
调用方式:
会循环调用所有类的 +load 方法。注意,这里是(调用分类的 +load 方法也是如此)直接使用函数内存地址的方式 (*load_method)(cls, SEL_load); 对 +load 方法进行调用的,而不是使用发送消息 objc_msgSend 的方式。
这样的调用方式就使得 +load 方法拥有了一个非常有趣的特性,那就是子类、父类和分类中的 +load 方法的实现是被区别对待的。也就是说如果子类没有实现 +load 方法,那么当它被加载时 runtime 是不会去调用父类的 +load 方法的。同理,当一个类和它的分类都实现了 +load 方法时,两个方法都会被调用。
要点:

  • 调用时机比较早,运行环境有不确定因素。具体说来,在iOS上通常就是App启动时进行加载,但当load调用的时候,并不能保证所有类都加载完成且可用,必要时还要自己负责做auto release处理。

补充上面一点,对于有依赖关系的两个库中,被依赖的类的+load会优先调用。但在一个库之内,父、子类、类别之间调用有顺序,不同类之间调用顺序是不确定的。

  • 关于继承:对于一个类而言,没有+load方法实现就不会调用,不会考虑对NSObject的继承,就是不会沿用父类的+load。
  • 父类和本类的调用:父类的方法优先于子类的方法。一个类的+load方法不用写明[super load],父类就会收到调用。
  • 本类和Category的调用:本类的方法优先于类别(Category)中的方法。Category的+load也会收到调用,但顺序上在本类的+load调用之后。
  • 不会直接触发initialize的调用。

+initialize

+initialize 方法是在类或它的子类收到第一条消息之前被调用的,这里所指的消息包括实例方法和类方法的调用,并且只会调用一次。initialize方法实际上是一种惰性调用,也就是说如果一个类一直没被用到,那它的initialize方法也不会被调用,这一点有利于节约资源。
调用方式:
runtime 使用了发送消息 objc_msgSend 的方式对 +initialize 方法进行调用。也就是说 +initialize 方法的调用与普通方法的调用是一样的,走的都是发送消息的流程。换言之,如果子类没有实现 +initialize 方法,那么继承自父类的实现会被调用;如果一个类的分类实现了 +initialize 方法,那么就会对这个类中的实现造成覆盖。
要点:

  • initialize的自然调用是在第一次主动使用当前类的时候。
  • 在initialize方法收到调用时,运行环境基本健全。
  • 关于继承:和load不同,即使子类不实现initialize方法,会把父类的实现继承过来调用一遍,就是会沿用父类的+initialize。(沿用父类的方法中,self还是指子类)
  • 父类和本类的调用:子类的+initialize将要调用时会激发父类调用的+initialize方法,所以也不需要在子类写明[super initialize]。(本着除主动调用外,只会调用一次的原则,如果父类的+initialize方法调用过了,则不会再调用)
  • 本类和Category的调用:Category中的+initialize方法会覆盖本类的方法,只执行一个Category的+initialize方法。

类别(Category)

对于+initialize,只有最后一个类别执行,本类的+initialize和前面类别的+initialize被隐藏。
而对于+load,本类和本类的所有类别都执行,并且如果Apple的文档中介绍顺序一样:先执行类自身的实现,再执行类别中的实现。
扩展

因为两个方法只会被系统调用一次(除主动调用外),并且是线程安全的,可以用来作为单例的实现。(可以用+initialize,+load有些隐患,看这里)
�注意

  • 在使用时都不要过重地依赖于这两个方法,除非真正必要。
  • 谨慎在分类中实现+initialize方法,因为如果在分类中实现了,本类实现的+initialize方法将不会被调用。
  • 谨慎在分类中实现+load方法。因为如果在本类中实现+load方法混淆A、B两个方法,分类中也混淆A、B,因为本类和分类的+load都实现了,所以都会调用,A、B在本类中置换后,又在分类中置换了回来。
  • load方法通常用来进行Method Swizzle,initialize方法一般用于初始化全局变量或静态变量。
  • load和initialize方法内部使用了锁,因此它们是线程安全的。实现时要尽可能保持简单,避免阻塞线程,不要再使用锁。

问题

问题:

  1. 子类、父类、分类中的相应方法什么时候会被调用?
  2. 需不需要在子类的实现中显式地调用父类的实现?

解答:

  1. super的方法会成功调用,但是这是多余的,因为runtime会自动对父类的+load方法进行调用,而+initialize则会随子类自动激发父类的方法(如Apple文档中所言)不需要显示调用。另一方面,如果父类中的方法用到的self(像示例中的方法),其指代的依然是类自身,而不是父类。

总结

收起阅读 »

iOS - swift常用的关键词解释和用法

deinit: 当一个类的实例即将被销毁时,会调用这个方法。class Person { var name:String var age:Int var gender:String deinit {...
继续阅读 »

deinit: 当一个类的实例即将被销毁时,会调用这个方法。

class Person  
{
var name:String
var age:Int
var gender:String

deinit
{
//从堆中释放,并释放的资源
}
}

extension:允许给已有的类、结构体、枚举、协议类型,添加新功能。

class Person  
{
var name:String = ""
var age:Int = 0
var gender:String = ""
}

extension Person
{
func printInfo()
{
print("My name is \(name), I'm \(age) years old and I'm a \(gender).")
}
}

inout:将一个值传入函数,并可以被函数修改,然后将值传回到调用处,来替换初始值。适用于引用类型和值类型。 其实就是声明参数为指针

func dangerousOp(_ error:inout NSError?)  
{
error = NSError(domain: "", code: 0, userInfo: ["":""])
}

var potentialError:NSError?
dangerousOp(&potentialError)

//代码运行到这里,potentialError 不再是 nil,而是已经被初始化

internal:访问控制权限,允许同一个模块下的所有源文件访问,如果在不同模块下则不允许访问。


public:可以被任何人访问。但其他 module 中不可以被 override 和继承,而在 module 内可以被 override 和继承。


open:访问控制权限,允许在定义的模块外也可以访问源文件里的所有类,并进行子类化。对于类成员,允许在定义的模块之外访问和重写。

open var foo:String? //这个属性允许在 app 内或 app 外重写和访问。在开发框架的时候,会应用到这个访问修饰符。

private:访问控制权限,只允许实体在定义的类以及相同源文件内的 extension 中访问。

class Person  
{
private var jobTitle:String = ""
}

// 当 extension 和 class 不在同一个源文件时
extension Person
{
// 无法编译通过,只有在同一个源文件下才可以访问
func printJobTitle()
{
print("My job is \(jobTitle)")
}
}

fileprivate:访问控制权限,只允许在定义源文件中访问。// 同文件中访问

class Person  
{
fileprivate var jobTitle:String = ""
}

extension Person
{
//当 extension 和 class 在同一个文件中时,允许访问
func printJobTitle()
{
print("My job is (jobTitle)")
}
}


从高到低排序如下:

open > public > interal > fileprivate > private



static:用于定义类方法,在类型本身进行调用。此外还可以定义静态成员。

class Person  
{
var jobTitle:String?

static func assignRandomName(_ aPerson:Person)
{
aPerson.jobTitle = "Some random job"
}
}

let somePerson = Person()
Person.assignRandomName(somePerson)
//somePerson.jobTitle 的值是 "Some random job"


在方法的 func 关键字之前加上关键字 static 或者 class 都可以用于指定类方法.不同的是用class关键字指定的类方法可以被子类重写, 如下:

override class func work() { print("Teacher: University Teacher")}

但是用 static 关键字指定的类方法是不能被子类重写的, 根据报错信息: Class method overrides a 'final' class method.



我们可以知道被 static 指定的类方法包含 final 关键字的特性--防止被重写.


struct:通用、灵活的结构体,是程序的基础组成部分,并提供了默认初始化方法。与 class 不同,当 struct 在代码中被传递时,是被拷贝的,并不使用引用计数。除此之外,struct 没有下面的这些功能:



  • 使用继承。

  • 运行时的类型转换。

  • 使用析构方法。



数据类型:struct是值类型,class是引用类型。

值类型变量直接包含数据,赋值时也是值拷贝,或者叫深拷贝,所以多个变量的操作不会相互影响。

引用类型变量存储的是对数据的引用地址,后者称为对象,赋值时,是将对象的引用地址复制过去,也叫浅拷贝,因此若多个变量指向同一个对象时,操作会相互影响。

值类型数据没有引用计数,也就不会因为循环引用导致内存泄漏,而引用类型存在引用计数,需要小心循环引用导致的内存泄漏

拷贝时,struct是深拷贝,拷贝的是内容,class则需要选用正确的深浅拷贝类型。



因为值类型数据是深拷贝,所以是线程安全的,而引用类型数据则不是



  • property的初始化:初始化属性时,class 需要创建一个带形参的constructor;struct可以把属性放在默认的constructor 的参数里。

  • immutable变量:swift用var和let区分可变数据和不可变数据,struct遵循这个特性;对class则不适用。

  • mutating function:struct 的 function 改变 property 时,需加上 mutating,而 class 不用。

  • 速度:struct分配在栈中,class分配在堆中,也就意味着struct更迅速。

  • NSUserDefaults:struct 不能被序列化成 NSData 对象,class可以。

  • 继承: struct不可以继承,class可以继承。

  • swift与oc混合开发时,oc调用swift需要继承NSObject,这就导致了class可以继承,所以可以调用class,但struct不能继承,所以不能调用struct


typealias:给代码中已经存在的类,取别名。

typealias JSONDictionary = [String: AnyObject]

func parseJSON(_ deserializedData:JSONDictionary){}

defer:用于在程序离开当前作用域之前,执行一段代码。 // dafer 是倒叙 先加入后执行




  • 关闭文件

    func foo() {
    let fileDescriptor = open(url.path, O_EVTONLY)
    defer {
    close(fileDescriptor)
    }
    // use fileDescriptor...
    }



  • 加/解锁:下面是 swift 里类似 Objective-C 的 synchronized block 的一种写法,可以使用任何一个 NSObject 作 lock

    func foo() {
    objc_sync_enter(lock)
    defer {
    print("003")
    objc_sync_exit(lock)
    }
    defer {
    print("002")
    }
    print("001")
    // do something...
    }



defer 的执行时机:

defer 的执行时机紧接在离开作用域之后,但是是在其他语句之前。这个特性为 defer 带来了一些很“微妙”的使用方式。比如从 0 开始的自增:

class Foo {
var num = 0
func foo() -> Int {
defer { num += 1 }
return num
}

// 没有 `defer` 的话我们可能要这么写
// func foo() -> Int {
// num += 1
// return num - 1
// }
}

let f = Foo()
f.foo() // 0
f.foo() // 1
f.num // 2

fallthrough:显式地允许从当前 case 跳转到下一个相邻 case 继续执行代码。

let box = 1

switch box
{
case 0:
print("Box equals 0")
fallthrough
case 1:
print("Box equals 0 or 1")
default:
print("Box doesn't equal 0 or 1")
// Box equals 0 和 Box equals 0 or 1 都执行了
}

guard:当有一个以上的条件不满足要求时,将离开当前作用域。同时还提供解包可选类型的功能。

private func printRecordFromLastName(userLastName: String?)
{
guard let name = userLastName, name != "Null" else
{
//userLastName = "Null",需要提前退出
return
}
//继续执行代码
print(dataStore.findByLastName(name))
}


1.guard关键字必须使用在函数中。

2.guard关键字必须和else同时出现。

3.guard关键字只有条件为false的时候才能走else语句 相反执行后边语句。



repeat:在使用循环的判断条件之前,先执行一次循环中的代码。类似于 do while 循环


repeat

{

print("Always executes at least once before the condition is considered")

}

while 1 > 2


where:要求关联类型必须遵守特定协议,或者类型参数和关联类型必须保持一致。也可以用于在 case 中提供额外条件,用于满足控制表达式。



  • 增加判断条件
for i in 0…3 where i % 2 == 0  
{
print(i) //打印 0 和 2
}


  • 协议使用where, 只有基类实现了当前协议才能添加扩展。 换个说法, 多个类实现了同一个协议,该语法根据类名分别为这些类添加扩展, 注意是分别(以类名区分)!!!
protocol SomeProtocol {
func someMethod()
}
class A: SomeProtocol {
let a = 1
func someMethod() {
print("call someMethod")
}
}
class B {
let a = 2
}
//基类A继承了SomeProtocol协议才能添加扩展
extension SomeProtocol where Self: A {
func showParamA() {
print(self.a)
}
}
//反例,不符合where条件
extension SomeProtocol where Self: B {
func showParamA() {
print(self.a)
}
}
let objA = A()
let objB = B() //类B没实现SomeProtocol, 所有没有协议方法
objA.showParamA() //输出1

as:类型转换运算符,用于尝试将值转成其它类型。




  • as : 数值类型转换

    let age = 28 as Int
    let money = 20 as CGFloat
    let cost = (50 / 2) as Double
    switch person1 { 
    case let person1 as Student:
    print("是Student类型,打印学生成绩单...")
    case let person1 as Teacher:
    print("是Teacher类型,打印老师工资单...")
    default: break
    }



  • as!:向下转型(Downcasting)时使用。由于是强制类型转换,如果转换失败会报 runtime 运行错误。

    let a = 13 as! String
    print(a)
    //会crash
    let a = 13 as? String
    print(a)
    //输出为nil



is:类型检查运算符,用于确定实例是否为某个子类类型。

class Person {}  
class Programmer : Person {}
class Nurse : Person {}

let people = [Programmer(), Nurse()]

for aPerson in people
{
if aPerson is Programmer
{
print("This person is a dev")
}
else if aPerson is Nurse
{
print("This person is a nurse")
}
}

nil:在 Swift 中表示任意类型的无状态值。



Swift的nil和OC中的nil不一样.在OC中,nil是一个指向不存在对象的指针.而在Swift中,nil不是指针,它是一个不确定的值.用来表示值缺失.任何类型的optional都可以被设置为nil. 而在OC中,基本数据类型和结构体是不能被设置为nil的. 给optional的常量或者变量赋值为nil.来表示他们的值缺失情况.一个optional常量或者变量如果在初始化的时候没有被赋值,他们自动会设置成nil.

class Person{}  
struct Place{}

//任何 Swift 类型或实例可以为 nil
var statelessPerson:Person? = nil
var statelessPlace:Place? = nil
var statelessInt:Int? = nil
var statelessString:String? = nil

super:在子类中,暴露父类的方法、属性、下标。

class Person  
{
func printName()
{
print("Printing a name. ")
}
}

class Programmer : Person
{
override func printName()
{
super.printName()
print("Hello World!")
}
}

let aDev = Programmer()
aDev.printName() //打印 Printing a name. Hello World!

self:任何类型的实例都拥有的隐式属性,等同于实例本身。此外还可以用于区分函数参数和成员属性名称相同的情况。

class Person  
{
func printSelf()
{
print("This is me: \(self)")
}
}

let aPerson = Person()
aPerson.printSelf() //打印 "This is me: Person"

Self:在协议中,表示遵守当前协议的实体类型。

protocol Printable  
{
func printTypeTwice(otherMe:Self)
}

struct Foo : Printable
{
func printTypeTwice(otherMe: Foo)
{
print("I am me plus \(otherMe)")
}
}

let aFoo = Foo()
let anotherFoo = Foo()

aFoo.printTypeTwice(otherMe: anotherFoo) //打印 I am me plus Foo()

_:用于匹配或省略任意值的通配符。

for _ in 0..<3  
{
print("Just loop 3 times, index has no meaning")
}

另外一种用法:

let _ = Singleton() //忽略不使用的变量

convenience:


在 Swift 中,为保证安全性,init 方法只能调用一次,且在 init 完成后,保证所有非 Optional 的属性都已经被初始化。


每个类都有指定的初始化方法:designated initializer,这些初始化方法是子类必须调用的,为的就是保证父类的属性都初始化完成了。


而如果不想实现父类的 designated initializer,可以添加 convenience 关键字,自己实现初始化逻辑。

convenience 初始化不能调用父类的初始化方法,只能调用同一个类中的 designated initializer。

由于 convenience 初始化不安全,所以 Swift 不允许 convenience initializer 被子类重写,限制其作用范围。

class People {
var name: String
init(name: String) {
self.name = name
}
}

通过extension给原有的People类增加init方法:

// 使用convenience增加init方法
extension People {
convenience init(smallName: String) {
self.init(name: smallName)
}
}

接下来,Student类继承父类People

class Student: People {
var grade: Int

init(name: String, grade: Int) {
self.grade = grade
super.init(name: name)
// 无法调用
// super.init(smallName: name)
}

// 可以被重写
override init(name: String) {
grade = 1
super.init(name: name)
}

// 无法重写,编译不通过
override init(smallName: String) {
grade = 1
super.init(smallName: smallName)
}
}

子类对象调用父类的convenience的init方法:只要在子类中实现重写了父类convenience方法所需要的init方法的话,我们在子类中就可以使用父类的convenience初始化方法了

class People {

var name: String

init(name: String) {
self.name = name
}
}
// 使用convenience增加init方法
extension People {
convenience init(smallName: String) {
self.init(name: smallName)
}
}


// 子类
class Teacher: People {

var course: String

init(name: String, course: String) {
self.course = course
super.init(name: name)
}

override init(name: String) {
self.course = "math"
super.init(name: name)
}
}

// 调用convenience的init方法
let xiaoming = Teacher(smallName: "xiaoming")


  • 总结:子类的designated初始化方法必须调用父类的designated方法,以保证父类也完成初始化。


required


对于某些我们希望子类中一定实现的designated初始化方法,我们可以通过添加required关键字进行限制,强制子类对这个方法重写。

required修饰符的使用规则:



  • required修饰符只能用于修饰类初始化方法。

  • 当子类含有异于父类的初始化方法时(初始化方法参数类型和数量异于父类),子类必须要实现父类的required初始化方法,并且也要使用required修饰符而不是override。

  • 当子类没有初始化方法时,可以不用实现父类的required初始化方法。
    class MyClass {
var str:String
required init(str:String) {
self.str = str
}
}
class MySubClass:MyClass
{
init(i:Int) {
super.init(str:String(i))
}

}
// 编译错误
MySubClass(i: 123)

会报错,因为你没有实现父类中必须实现的方法。正确的写法:


    class MyClass {
var str:String
required init(str:String) {
self.str = str
}
}
class MySubClass:MyClass
{
init(i:Int) {
super.init(str:String(i))
}
required init(str: String) {
fatalError("init(str:) has not been implemented")
}
}
// 编译错误
MySubClass(i: 123)

从上面的代码中,不难看出子类需要添加异于父类的初始化方法,必须要重写有required的修饰符的初始化方法,并且也要使用required修饰符而不是override,请千万注意!


如果子类中并没有不同于父类的初始化方法,Swift会默认使用父类的初始化方法:

class MyClass{
var str: String?
required init(str: String?) {
self.str = str
}
}
class MySubClass: MyClass{
}
var MySubClass(str: "hello swift")

在这种情况下,编译器不会报错,因为如果子类没有任何初始化方法时,Swift会默认使用父类的初始化方法。


以#开头的关键字


#available:基于平台参数,通过 if,while,guard 语句的条件,在运行时检查 API 的可用性。

if #available(iOS 10, *)  
{
print("iOS 10 APIs are available")
}

#colorLiteral:在 playground 中使用的字面表达式,用于创建颜色选取器,选取后赋值给变量。

let aColor = #colorLiteral //创建颜色选取器

#column:一种特殊的字面量表达式,用于获取字面量表示式的起始列数。

class Person  
{
func printInfo()
{
print("Some person info - on column \(#column)")
}
}

let aPerson = Person()
aPerson.printInfo() //Some person info - on column 47

#function:特殊字面量表达式,返回函数名称。在方法中,返回方法名。在属性的 getter 或者 setter 中,返回属性名。在特殊的成员中,比如 init 或 subscript 中,返回关键字名称。在文件的最顶层时,返回当前所在模块名称。

class Person
{
func printInfo()
{
print("Some person info - inside function \(#function)")
}
}

let aPerson = Person()
aPerson.printInfo() //Some person info - inside function printInfo()

#line:特殊字面量表达式,用于获取当前代码的行数。

class Person  
{
func printInfo()
{
print("Some person info - on line number \(#line)")
}
}

let aPerson = Person()
aPerson.printInfo() //Some person info - on line number 5

#selector:用于创建 Objective-C selector 的表达式,可以静态检查方法是否存在,并暴露给 Objective-C。

//静态检查,确保 doAnObjCMethod 方法存在  
control.sendAction(#selector(doAnObjCMethod), to: target, forEvent: event)

dynamic && @objc


@objc


OC 是基于运行时,遵循了 KVC 和动态派发,而 Swift 为了追求性能,在编译时就已经确定,而不需要在运行时的,在 Swift 类型文件中,为了解决这个问题,需要暴露给 OC 使用的任何地方(类,属性,方法等)的生命前面加上 @objc 修饰符

如果用 Swift 写的 class 是继承 NSObject 的话, Swift 会默认自动为所有非 private 的类和成员加上@objc


在Swift中,我们在给button添加点击事件时,对应的点击事件的触发方法就需要用@objc来修饰


dynamic


Swift 中的函数可以是静态调用,静态调用会更快。当函数是静态调用的时候,就不能从字符串查找到对应的方法地址了。这样 Swift 跟 Objective-C 交互时,Objective-C 动态查找方法地址,就有可能找不到 Swift 中定义的方法。


这样就需要在 Swift 中添加一个提示关键字,告诉编译器这个方法是可能被动态调用的,需要将其添加到查找表中。这个就是关键字 dynamic 的作用。


didSet


属性观察者,当值存储到属性后马上调用。

var data = [1,2,3]  
{
didSet
{
tableView.reloadData()
}
}

final


防止方法、属性、下标被重写。

final class Person {}  
class Programmer : Person {} //编译错误


get


返回成员的值。还可以用在计算型属性上,间接获取其它属性的值。

class Person  
{
var name:String
{
get { return self.name }
set { self.name = newValue}
}

var indirectSetName:String
{
get
{
if let aFullTitle = self.fullTitle
{
return aFullTitle
}
return ""
}

set (newTitle)
{
//如果没有定义 newTitle,可以使用 newValue
self.fullTitle = "(self.name) :(newTitle)"
}
}
}


infix


指明一个用于两个值之间的运算符。如果一个全新的全局运算符被定义为 infix,还需要指定优先级。

let twoIntsAdded = 2 + 3


indirect


指明在枚举类型中,存在成员使用相同枚举类型的实例作为关联值的情况。

indirect enum Entertainment  
{
case eventType(String)
case oneEvent(Entertainment)
case twoEvents(Entertainment, Entertainment)
}

let dinner = Entertainment.eventType("Dinner")
let movie = Entertainment.eventType("Movie")

let dateNight = Entertainment.twoEvents(dinner, movie)


lazy


指明属性的初始值,直到第一次被使用时,才进行初始化。

class Person  
{
lazy var personalityTraits = {
//昂贵的数据库开销
return ["Nice", "Funny"]
}()
}
let aPerson = Person()
aPerson.personalityTraits //当 personalityTraits 首次被访问时,数据库才开始工作

left


指明运算符的结合性是从左到右。在没有使用大括号时,可以用于正确判断同一优先级运算符的执行顺序。

//"-" 运算符的结合性是从左到右
10-2-4 //根据结合性,可以看做 (10-2) - 4


mutating


允许在方法中修改结构体或者枚举实例的属性值。

struct Person  
{
var job = ""

mutating func assignJob(newJob:String)
{
self = Person(job: newJob)
}
}

var aPerson = Person()
aPerson.job //""

aPerson.assignJob(newJob: "iOS Engineer at Buffer")
aPerson.job //iOS Engineer at Buffer


none


是一个没有结合性的运算符。不允许这样的运算符相邻出现。

//"<" 是非结合性的运算符
1 < 2 < 3 //编译失败


nonmutating


指明成员的 setter 方法不会修改实例的值,但可能会有其它后果。

enum Paygrade  
{
case Junior, Middle, Senior, Master

var experiencePay:String?
{
get
{
database.payForGrade(String(describing:self))
}

nonmutating set
{
if let newPay = newValue
{
database.editPayForGrade(String(describing:self), newSalary:newPay)
}
}
}
}

let currentPay = Paygrade.Middle

//将 Middle pay 更新为 45k, 但不会修改 experiencePay 值
currentPay.experiencePay = "$45,000"

optional


用于指明协议中的可选方法。遵守该协议的实体类可以不实现这个方法。

@objc protocol Foo  
{
func requiredFunction()
@objc optional func optionalFunction()
}

class Person : Foo
{
func requiredFunction()
{
print("Conformance is now valid")
}
}

override


指明子类会提供自定义实现,覆盖父类的实例方法、类型方法、实例属性、类型属性、下标。如果没有实现,则会直接继承自父类。

class Person  
{
func printInfo()
{
print("I'm just a person!")
}
}

class Programmer : Person
{
override func printInfo()
{
print("I'm a person who is a dev!")
}
}

let aPerson = Person()
let aDev = Programmer()

aPerson.printInfo() //打印 I'm just a person!
aDev.printInfo() //打印 I'm a person who is a dev!

postfix


位于值后面的运算符。

var optionalStr:String? = "Optional"  
print(optionalStr!)

precedence


指明某个运算符的优先级高于别的运算符,从而被优先使用。

infix operator ~ { associativity right precedence 140 }  
4 ~ 8

prefix


位于值前面的运算符。


var anInt = 2  
anInt = -anInt //anInt 等于 -2


required


确保编译器会检查该类的所有子类,全部实现了指定的构造器方法。

class Person  
{
var name:String?

required init(_ name:String)
{
self.name = name
}
}

class Programmer : Person
{
//如果不实现这个方法,编译不会通过
required init(_ name: String)
{
super.init(name)
}
}

right


指明运算符的结合性是从右到左的。在没有使用大括号时,可以用于正确判断同一优先级运算符的顺序。

//"??" 运算符结合性是从右到左
var box:Int?
var sol:Int? = 2

let foo:Int = box ?? sol ?? 0 //Foo 等于 2


set


通过获取的新值来设置成员的值。同样可以用于计算型属性来间接设置其它属性。如果计算型属性的 setter 没有定义新值的名称,可以使用默认的 newValue。

class Person  
{
var name:String
{
get { return self.name }
set { self.name = newValue}
}

var indirectSetName:String
{
get
{
if let aFullTitle = self.fullTitle
{
return aFullTitle
}
return ""
}

set (newTitle)
{
//如果没有定义 newTitle,可以使用 newValue
self.fullTitle = "(self.name) :(newTitle)"
}
}
}


Type


表示任意类型的类型,包括类类型、结构类型、枚举类型、协议类型。

class Person {}  
class Programmer : Person {}

let aDev:Programmer.Type = Programmer.self

unowned


让循环引用中的实例 A 不要强引用实例 B。前提条件是实例 B 的生命周期要长于 A 实例。

class Person  
{
var occupation:Job?
}

//当 Person 实例不存在时,job 也不会存在。job 的生命周期取决于持有它的 Person。
class Job
{
unowned let employee:Person

init(with employee:Person)
{
self.employee = employee
}
}

weak


允许循环引用中的实例 A 弱引用实例 B ,而不是强引用。实例 B 的生命周期更短,并会被先释放。

class Person  
{
var residence:House?
}

class House
{
weak var occupant:Person?
}

var me:Person? = Person()
var myHome:House? = House()

me!.residence = myHome
myHome!.occupant = me

me = nil
myHome!.occupant // myHome 等于 nil


willSet


属性观察者,在值存储到属性之前调用。

class Person  
{
var name:String?
{
willSet(newValue) {print("I've got a new name, it's (newValue)!")}
}
}

let aPerson = Person()
aPerson.name = "Jordan" //在赋值之前,打印 "I've got a new name, it's Jordan!"



链接:https://www.jianshu.com/p/46cf5c77dee7
收起阅读 »

iOS 常用技巧

1、递归查看 view 的子视图(私有方法,没有代码提示)[self.view recursiveDescription] 2、// 定义一个特殊字符的集合 NSCharacterSet *set = [NSCharacterSet characterSet...
继续阅读 »

1、递归查看 view 的子视图(私有方法,没有代码提示)

[self.view recursiveDescription]

2、

// 定义一个特殊字符的集合
NSCharacterSet *set = [NSCharacterSet characterSetWithCharactersInString:
@"@/:;()¥「」"、[]{}#%-*+=_\\|~<>$€^•'@#$%^&*()_+'\""];
// 过滤字符串的特殊字符
NSString *newString = [trimString stringByTrimmingCharactersInSet:set];

3、Transform 属性

//平移按钮
CGAffineTransform transForm = self.buttonView.transform;
self.buttonView.transform = CGAffineTransformTranslate(transForm, 10, 0);

//旋转按钮
CGAffineTransform transForm = self.buttonView.transform;
self.buttonView.transform = CGAffineTransformRotate(transForm, M_PI_4);

//缩放按钮
self.buttonView.transform = CGAffineTransformScale(transForm, 1.2, 1.2);

//初始化复位
self.buttonView.transform = CGAffineTransformIdentity;

4、去掉分割线多余15pt

首先在viewDidLoad方法加入以下代码:
if ([self.tableView respondsToSelector:@selector(setSeparatorInset:)]) {
[self.tableView setSeparatorInset:UIEdgeInsetsZero];
}
if ([self.tableView respondsToSelector:@selector(setLayoutMargins:)]) {
[self.tableView setLayoutMargins:UIEdgeInsetsZero];
}
然后在重写willDisplayCell方法
- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell
forRowAtIndexPath:(NSIndexPath *)indexPath{
if ([cell respondsToSelector:@selector(setSeparatorInset:)]) {
[cell setSeparatorInset:UIEdgeInsetsZero];
}
if ([cell respondsToSelector:@selector(setLayoutMargins:)]) {
[cell setLayoutMargins:UIEdgeInsetsZero];
}
}

5、计算耗时方法时间间隔

// 获取时间间隔
#define TICK CFAbsoluteTime start = CFAbsoluteTimeGetCurrent();
#define TOCK NSLog(@"Time: %f", CFAbsoluteTimeGetCurrent() - start)

6、Color颜色宏定义

// 随机颜色
#define RANDOM_COLOR [UIColor colorWithRed:arc4random_uniform(256) / 255.0 green:arc4random_uniform(256) / 255.0 blue:arc4random_uniform(256) / 255.0 alpha:1]
// 颜色(RGB)
#define RGBCOLOR(r, g, b) [UIColor colorWithRed:(r)/255.0f green:(g)/255.0f blue:(b)/255.0f alpha:1]
// 利用这种方法设置颜色和透明值,可不影响子视图背景色
#define RGBACOLOR(r, g, b, a) [UIColor colorWithRed:(r)/255.0f green:(g)/255.0f blue:(b)/255.0f alpha:(a)]

7、退出iOS应用

- (void)exitApplication {
AppDelegate *app = [UIApplication sharedApplication].delegate;
UIWindow *window = app.window;

[UIView animateWithDuration:1.0f animations:^{
window.alpha = 0;
} completion:^(BOOL finished) {
exit(0);
}];
}

8、NSArray 快速求总和 最大值 最小值 和 平均值

NSArray *array = [NSArray arrayWithObjects:@"2.0", @"2.3", @"3.0", @"4.0", @"10", nil];
CGFloat sum = [[array valueForKeyPath:@"@sum.floatValue"] floatValue];
CGFloat avg = [[array valueForKeyPath:@"@avg.floatValue"] floatValue];
CGFloat max =[[array valueForKeyPath:@"@max.floatValue"] floatValue];
CGFloat min =[[array valueForKeyPath:@"@min.floatValue"] floatValue];
NSLog(@"%f\n%f\n%f\n%f",sum,avg,max,min);

9、Debug栏打印时自动把Unicode编码转化成汉字


 DXXcodeConsoleUnicodePlugin 插件

10、自动生成模型代码的插件

ESJsonFormat-for-Xcode

11、设置滑动的时候隐藏navigationbar

self.navigationController.hidesBarsOnSwipe = YES

12、隐藏导航栏上的返回字体

//Swift
UIBarButtonItem.appearance().setBackButtonTitlePositionAdjustment(UIOffsetMake(0, -60), forBarMetrics: .Default)
//OC
[[UIBarButtonItem appearance] setBackButtonTitlePositionAdjustment:UIOffsetMake(0, -60) forBarMetrics:UIBarMetricsDefault];

13、设置导航栏透明

//方法一:设置透明度
[[[self.navigationController.navigationBar subviews]objectAtIndex:0] setAlpha:0.1];
//方法二:设置背景图片
/**
* 设置导航栏,使其透明
*
*/
- (void)setNavigationBarColor:(UIColor *)color targetController:(UIViewController *)targetViewController{
//导航条的颜色 以及隐藏导航条的颜色targetViewController.navigationController.navigationBar.shadowImage = [[UIImage alloc]init];
CGRect rect=CGRectMake(0.0f, 0.0f, 1.0f, 1.0f); UIGraphicsBeginImageContext(rect.size);
CGContextRef context = UIGraphicsGetCurrentContext(); CGContextSetFillColorWithColor(context, [color CGColor]); CGContextFillRect(context, rect);
UIImage *theImage = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); [targetViewController.navigationController.navigationBar setBackgroundImage:theImage forBarMetrics:UIBarMetricsDefault];
}

14、解决同时按两个按钮进两个view的问题

[button setExclusiveTouch:YES];

15、修改 textFieldplaceholder 字体颜色和大小

  textField.placeholder = @"请输入手机号码";
[textField setValue:[UIColor redColor] forKeyPath:@"_placeholderLabel.textColor"];
[textField setValue:[UIFont boldSystemFontOfSize:13] forKeyPath:@"_placeholderLabel.font"];

16、UIImage 与字符串互转

//图片转字符串  
-(NSString *)UIImageToBase64Str:(UIImage *) image
{
NSData *data = UIImageJPEGRepresentation(image, 1.0f);
NSString *encodedImageStr = [data base64EncodedStringWithOptions:NSDataBase64Encoding64CharacterLineLength];
return encodedImageStr;
}

//字符串转图片
-(UIImage *)Base64StrToUIImage:(NSString *)_encodedImageStr
{
NSData *_decodedImageData = [[NSData alloc] initWithBase64Encoding:_encodedImageStr];
UIImage *_decodedImage = [UIImage imageWithData:_decodedImageData];
return _decodedImage;
}


链接:https://www.jianshu.com/p/adb3ebf5354c 收起阅读 »

iOS - 图层性能 二

混合和过度绘制    在第12章有提到,GPU每一帧可以绘制的像素有一个最大限制(就是所谓的fill rate),这个情况下可以轻易地绘制整个屏幕的所有像素。但是如果由于重叠图层的关系需要不停地重绘同一区域的话,掉帧就可...
继续阅读 »

混合和过度绘制

    在第12章有提到,GPU每一帧可以绘制的像素有一个最大限制(就是所谓的fill rate),这个情况下可以轻易地绘制整个屏幕的所有像素。但是如果由于重叠图层的关系需要不停地重绘同一区域的话,掉帧就可能发生了。

    GPU会放弃绘制那些完全被其他图层遮挡的像素,但是要计算出一个图层是否被遮挡也是相当复杂并且会消耗处理器资源。同样,合并不同图层的透明重叠像素(即混合)消耗的资源也是相当客观的。所以为了加速处理进程,不到必须时刻不要使用透明图层。任何情况下,你应该这样做:

  • 给视图的backgroundColor属性设置一个固定的,不透明的颜色
  • 设置opaque属性为YES

    这样做减少了混合行为(因为编译器知道在图层之后的东西都不会对最终的像素颜色产生影响)并且计算得到了加速,避免了过度绘制行为因为Core Animation可以舍弃所有被完全遮盖住的图层,而不用每个像素都去计算一遍。

    如果用到了图像,尽量避免透明除非非常必要。如果图像要显示在一个固定的背景颜色或是固定的背景图之前,你没必要相对前景移动,你只需要预填充背景图片就可以避免运行时混色了。

    如果是文本的话,一个白色背景的UILabel(或者其他颜色)会比透明背景要更高效。

    最后,明智地使用shouldRasterize属性,可以将一个固定的图层体系折叠成单张图片,这样就不需要每一帧重新合成了,也就不会有因为子图层之间的混合和过度绘制的性能问题了。

减少图层数量

    初始化图层,处理图层,打包通过IPC发给渲染引擎,转化成OpenGL几何图形,这些是一个图层的大致资源开销。事实上,一次性能够在屏幕上显示的最大图层数量也是有限的。

    确切的限制数量取决于iOS设备,图层类型,图层内容和属性等。但是总得说来可以容纳上百或上千个,下面我们将演示即使图层本身并没有做什么也会遇到的性能问题。

裁切

    在对图层做任何优化之前,你需要确定你不是在创建一些不可见的图层,图层在以下几种情况下回事不可见的:

  • 图层在屏幕边界之外,或是在父图层边界之外。
  • 完全在一个不透明图层之后。
  • 完全透明

    Core Animation非常擅长处理对视觉效果无意义的图层。但是经常性地,你自己的代码会比Core Animation更早地想知道一个图层是否是有用的。理想状况下,在图层对象在创建之前就想知道,以避免创建和配置不必要图层的额外工作。

    举个例子。清单15.3 的代码展示了一个简单的滚动3D图层矩阵。这看上去很酷,尤其是图层在移动的时候(见图15.1),但是绘制他们并不是很麻烦,因为这些图层就是一些简单的矩形色块。

清单15.3 绘制3D图层矩阵

#import "ViewController.h"
#import

#define WIDTH 10
#define HEIGHT 10
#define DEPTH 10
#define SIZE 100
#define SPACING 150
#define CAMERA_DISTANCE 500

@interface ViewController ()

@property (nonatomic, strong) IBOutlet UIScrollView *scrollView;

@end

@implementation ViewController

- (void)viewDidLoad
{
[super viewDidLoad];

//set content size
self.scrollView.contentSize = CGSizeMake((WIDTH - 1)*SPACING, (HEIGHT - 1)*SPACING);

//set up perspective transform
CATransform3D transform = CATransform3DIdentity;
transform.m34 = -1.0 / CAMERA_DISTANCE;
self.scrollView.layer.sublayerTransform = transform;

//create layers
for (int z = DEPTH - 1; z >= 0; z--) {
for (int y = 0; y < HEIGHT; y++) {
for (int x = 0; x < WIDTH; x++) {
//create layer
CALayer *layer = [CALayer layer];
layer.frame = CGRectMake(0, 0, SIZE, SIZE);
layer.position = CGPointMake(x*SPACING, y*SPACING);
layer.zPosition = -z*SPACING;
//set background color
layer.backgroundColor = [UIColor colorWithWhite:1-z*(1.0/DEPTH) alpha:1].CGColor;
//attach to scroll view
[self.scrollView.layer addSublayer:layer];
}
}
}

//log
NSLog(@"displayed: %i", DEPTH*HEIGHT*WIDTH);
}
@end

图15.1 滚动的3D图层矩阵

    WIDTHHEIGHTDEPTH常量控制着图层的生成。在这个情况下,我们得到的是10*10*10个图层,总量为1000个,不过一次性显示在屏幕上的大约就几百个。

    如果把WIDTHHEIGHT常量增加到100,我们的程序就会慢得像龟爬了。这样我们有了100000个图层,性能下降一点儿也不奇怪。

    但是显示在屏幕上的图层数量并没有增加,那么根本没有额外的东西需要绘制。程序慢下来的原因其实是因为在管理这些图层上花掉了不少功夫。他们大部分对渲染的最终结果没有贡献,但是在丢弃这么图层之前,Core Animation要强制计算每个图层的位置,就这样,我们的帧率就慢了下来。

    我们的图层是被安排在一个均匀的栅格中,我们可以计算出哪些图层会被最终显示在屏幕上,根本不需要对每个图层的位置进行计算。这个计算并不简单,因为我们还要考虑到透视的问题。如果我们直接这样做了,Core Animation就不用费神了。

    既然这样,让我们来重构我们的代码吧。改造后,随着视图的滚动动态地实例化图层而不是事先都分配好。这样,在创造他们之前,我们就可以计算出是否需要他。接着,我们增加一些代码去计算可视区域这样就可以排除区域之外的图层了。清单15.4是改造后的结果。

清单15.4 排除可视区域之外的图层

#import "ViewController.h"
#import

#define WIDTH 100
#define HEIGHT 100
#define DEPTH 10
#define SIZE 100
#define SPACING 150
#define CAMERA_DISTANCE 500
#define PERSPECTIVE(z) (float)CAMERA_DISTANCE/(z + CAMERA_DISTANCE)

@interface ViewController ()

@property (nonatomic, weak) IBOutlet UIScrollView *scrollView;

@end

@implementation ViewController

- (void)viewDidLoad
{
[super viewDidLoad];
//set content size
self.scrollView.contentSize = CGSizeMake((WIDTH - 1)*SPACING, (HEIGHT - 1)*SPACING);
//set up perspective transform
CATransform3D transform = CATransform3DIdentity;
transform.m34 = -1.0 / CAMERA_DISTANCE;
self.scrollView.layer.sublayerTransform = transform;
}

- (void)viewDidLayoutSubviews
{
[self updateLayers];
}

- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
[self updateLayers];
}

- (void)updateLayers
{
//calculate clipping bounds
CGRect bounds = self.scrollView.bounds;
bounds.origin = self.scrollView.contentOffset;
bounds = CGRectInset(bounds, -SIZE/2, -SIZE/2);
//create layers
NSMutableArray *visibleLayers = [NSMutableArray array];
for (int z = DEPTH - 1; z >= 0; z--)
{
//increase bounds size to compensate for perspective
CGRect adjusted = bounds;
adjusted.size.width /= PERSPECTIVE(z*SPACING);
adjusted.size.height /= PERSPECTIVE(z*SPACING);
adjusted.origin.x -= (adjusted.size.width - bounds.size.width) / 2;
adjusted.origin.y -= (adjusted.size.height - bounds.size.height) / 2;
for (int y = 0; y < HEIGHT; y++) {
//check if vertically outside visible rect
if (y*SPACING < adjusted.origin.y || y*SPACING >= adjusted.origin.y + adjusted.size.height)
{
continue;
}
for (int x = 0; x < WIDTH; x++) {
//check if horizontally outside visible rect
if (x*SPACING < adjusted.origin.x ||x*SPACING >= adjusted.origin.x + adjusted.size.width)
{
continue;
}

//create layer
CALayer *layer = [CALayer layer];
layer.frame = CGRectMake(0, 0, SIZE, SIZE);
layer.position = CGPointMake(x*SPACING, y*SPACING);
layer.zPosition = -z*SPACING;
//set background color
layer.backgroundColor = [UIColor colorWithWhite:1-z*(1.0/DEPTH) alpha:1].CGColor;
//attach to scroll view
[visibleLayers addObject:layer];
}
}
}
//update layers
self.scrollView.layer.sublayers = visibleLayers;
//log
NSLog(@"displayed: %i/%i", [visibleLayers count], DEPTH*HEIGHT*WIDTH);
}
@end

    这个计算机制并不具有普适性,但是原则上是一样。(当你用一个UITableView或者UICollectionView时,系统做了类似的事情)。这样做的结果?我们的程序可以处理成百上千个『虚拟』图层而且完全没有性能问题!因为它不需要一次性实例化几百个图层。

对象回收

    处理巨大数量的相似视图或图层时还有一个技巧就是回收他们。对象回收在iOS颇为常见;UITableViewUICollectionView都有用到,MKMapView中的动画pin码也有用到,还有其他很多例子。

    对象回收的基础原则就是你需要创建一个相似对象池。当一个对象的指定实例(本例子中指的是图层)结束了使命,你把它添加到对象池中。每次当你需要一个实例时,你就从池中取出一个。当且仅当池中为空时再创建一个新的。

    这样做的好处在于避免了不断创建和释放对象(相当消耗资源,因为涉及到内存的分配和销毁)而且也不必给相似实例重复赋值。

    好了,让我们再次更新代码吧(见清单15.5)

清单15.5 通过回收减少不必要的分配

@interface ViewController () 

@property (nonatomic, weak) IBOutlet UIScrollView *scrollView;
@property (nonatomic, strong) NSMutableSet *recyclePool;


@end

@implementation ViewController

- (void)viewDidLoad
{
[super viewDidLoad]; //create recycle pool
self.recyclePool = [NSMutableSet set];
//set content size
self.scrollView.contentSize = CGSizeMake((WIDTH - 1)*SPACING, (HEIGHT - 1)*SPACING);
//set up perspective transform
CATransform3D transform = CATransform3DIdentity;
transform.m34 = -1.0 / CAMERA_DISTANCE;
self.scrollView.layer.sublayerTransform = transform;
}

- (void)viewDidLayoutSubviews
{
[self updateLayers];
}

- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
[self updateLayers];
}

- (void)updateLayers {

//calculate clipping bounds
CGRect bounds = self.scrollView.bounds;
bounds.origin = self.scrollView.contentOffset;
bounds = CGRectInset(bounds, -SIZE/2, -SIZE/2);
//add existing layers to pool
[self.recyclePool addObjectsFromArray:self.scrollView.layer.sublayers];
//disable animation
[CATransaction begin];
[CATransaction setDisableActions:YES];
//create layers
NSInteger recycled = 0;
NSMutableArray *visibleLayers = [NSMutableArray array];
for (int z = DEPTH - 1; z >= 0; z--)
{
//increase bounds size to compensate for perspective
CGRect adjusted = bounds;
adjusted.size.width /= PERSPECTIVE(z*SPACING);
adjusted.size.height /= PERSPECTIVE(z*SPACING);
adjusted.origin.x -= (adjusted.size.width - bounds.size.width) / 2; adjusted.origin.y -= (adjusted.size.height - bounds.size.height) / 2;
for (int y = 0; y < HEIGHT; y++) {
//check if vertically outside visible rect
if (y*SPACING < adjusted.origin.y ||
y*SPACING >= adjusted.origin.y + adjusted.size.height)
{
continue;
}
for (int x = 0; x < WIDTH; x++) {
//check if horizontally outside visible rect
if (x*SPACING < adjusted.origin.x ||
x*SPACING >= adjusted.origin.x + adjusted.size.width)
{
continue;
}
//recycle layer if available
CALayer *layer = [self.recyclePool anyObject]; if (layer)
{

recycled ++;
[self.recyclePool removeObject:layer]; }
else
{
layer = [CALayer layer];
layer.frame = CGRectMake(0, 0, SIZE, SIZE); }
//set position
layer.position = CGPointMake(x*SPACING, y*SPACING); layer.zPosition = -z*SPACING;
//set background color
layer.backgroundColor =
[UIColor colorWithWhite:1-z*(1.0/DEPTH) alpha:1].CGColor;
//attach to scroll view
[visibleLayers addObject:layer]; }
} }
[CATransaction commit]; //update layers
self.scrollView.layer.sublayers = visibleLayers;
//log
NSLog(@"displayed: %i/%i recycled: %i",
[visibleLayers count], DEPTH*HEIGHT*WIDTH, recycled);
}
@end

    本例中,我们只有图层对象这一种类型,但是UIKit有时候用一个标识符字符串来区分存储在不同对象池中的不同的可回收对象类型。

    你可能注意到当设置图层属性时我们用了一个CATransaction来抑制动画效果。在之前并不需要这样做,因为在显示之前我们给所有图层设置一次属性。但是既然图层正在被回收,禁止隐式动画就有必要了,不然当属性值改变时,图层的隐式动画就会被触发。

Core Graphics绘制

    当排除掉对屏幕显示没有任何贡献的图层或者视图之后,长远看来,你可能仍然需要减少图层的数量。例如,如果你正在使用多个UILabel或者UIImageView实例去显示固定内容,你可以把他们全部替换成一个单独的视图,然后用-drawRect:方法绘制出那些复杂的视图层级。

    这个提议看上去并不合理因为大家都知道软件绘制行为要比GPU合成要慢而且还需要更多的内存空间,但是在因为图层数量而使得性能受限的情况下,软件绘制很可能提高性能呢,因为它避免了图层分配和操作问题。

    你可以自己实验一下这个情况,它包含了性能和栅格化的权衡,但是意味着你可以从图层树上去掉子图层(用shouldRasterize,与完全遮挡图层相反)。

-renderInContext: 方法

    用Core Graphics去绘制一个静态布局有时候会比用层级的UIView实例来得快,但是使用UIView实例要简单得多而且比用手写代码写出相同效果要可靠得多,更边说Interface Builder来得直接明了。为了性能而舍弃这些便利实在是不应该。

    幸好,你不必这样,如果大量的视图或者图层真的关联到了屏幕上将会是一个大问题。没有与图层树相关联的图层不会被送到渲染引擎,也没有性能问题(在他们被创建和配置之后)。

    使用CALayer-renderInContext:方法,你可以将图层及其子图层快照进一个Core Graphics上下文然后得到一个图片,它可以直接显示在UIImageView中,或者作为另一个图层的contents。不同于shouldRasterize —— 要求图层与图层树相关联 —— ,这个方法没有持续的性能消耗。

    当图层内容改变时,刷新这张图片的机会取决于你(不同于shouldRasterize,它自动地处理缓存和缓存验证),但是一旦图片被生成,相比于让Core Animation处理一个复杂的图层树,你节省了相当客观的性能。

总结

    本章学习了使用Core Animation图层可能遇到的性能瓶颈,并讨论了如何避免或减小压力。你学习了如何管理包含上千虚拟图层的场景(事实上只创建了几百个)。同时也学习了一些有用的技巧,选择性地选取光栅化或者绘制图层内容在合适的时候重新分配给CPU和GPU。这些就是我们要讲的关于Core Animation的全部了(至少可以等到苹果发明什么新的玩意儿)。


收起阅读 »