- 原文地址:Source Code Walkthrough of Telegram-iOS Part 2: SSignalKit
- 原文作者:Bo
- 译文出自:掘金翻译计划
Telegram-iOS 在大多数模块中使用反应性编程。在项目内实现反应功能有三个框架......
Telegram-iOS 在大多数模块中使用反应性编程。在项目内实现反应功能有三个框架:
MTSignal
: 这可能是他们第一次尝试在目标-C中建立反应性范式。它主要用于模块 MtProtoKit, 它实现了 MTProto, 电报的移动协议。SSignalKit
:它是 MTSignal 的后裔, 用于更通用的场景, 具有更丰富的原始和操作。SwiftSignalKit
: 在Swift的等效端口。
这篇文章侧重于SwiftSignalKit解释其设计与使用案例。
设计
信号
是一个捕捉"随着时间而变化"概念的类。其签名可视为以下内容::
// pseudocode
public final class Signal<T, E> {
public init(_ generator: @escaping(Subscriber<T, E>) -> Disposable)
public func start(next: ((T) -> Void)! = nil,
error: ((E) -> Void)! = nil,
completed: (() -> Void)! = nil) -> Disposable
}
复制代码
要设置信号,它接受一个发电机关闭,该关闭定义了生成数据()、捕获错误(和更新完成状态)的方法。设置后,该功能可以注册观察者关闭。start
订阅者
订阅者
有逻辑将数据发送到每个观察者关闭与线程安全考虑。
// pseudocode
public final class Subscriber<T, E> {
private var next: ((T) -> Void)!
private var error: ((E) -> Void)!
private var completed: (() -> Void)!
private var terminated = false
public init(next: ((T) -> Void)! = nil,
error: ((E) -> Void)! = nil,
completed: (() -> Void)! = nil)
public func putNext(_ next: T)
public func putError(_ error: E)
public func putCompletion()
}
复制代码
当发生错误或完成订阅者时,订阅者将终止。状态无法逆转
putNext
只要用户未终止,就向关闭发送新数据nextputError
向关闭发送错误并标记已终止的订阅者errorputCompletion
调用关闭并标记已终止的订阅者。completed
运营商
定义了一组丰富的操作员,以在信号上提供功能原始。这些原始人被分为几个类别,根据其功能:Catch
, Combine
, Dispatch
, Loop
, Mapping
, Meta
, Reduce
, SideEffects
, Single
, Take
, and Timing
. 让我们以几个映射操作员为例:
public func map<T, E, R>(_ f: @escaping(T) -> R) -> (Signal<T, E>) -> Signal<R, E>
public func filter<T, E>(_ f: @escaping(T) -> Bool) -> (Signal<T, E>) -> Signal<T, E>
public func flatMap<T, E, R>(_ f: @escaping (T) -> R) -> (Signal<T?, E>) -> Signal<R?, E>
public func mapError<T, E, R>(_ f: @escaping(E) -> R) -> (Signal<T, E>) -> Signal<T, R>
复制代码
操作员喜欢关闭转换并返回更改信号数据类型的功能。有一个方便的操作员,以帮助链这些运营商作为管道:map()|>
代码语言:javascript复制precedencegroup PipeRight {
associativity: left
higherThan: DefaultPrecedence
}
infix operator |> : PipeRight
public func |> <T, U>(value: T, function: ((T) -> U)) -> U {
return function(value)
}
复制代码
运营商可能受到JavaScript世界中提议的 管道运营商 的启发。通过 Swift 的尾随关闭支持,所有操作员都可以通过直观的可读性进行管道传输:|>
代码语言:javascript复制// pseudocode
let anotherSignal = valueSignal
|> filter { value -> Bool in
...
}
|> take(1)
|> map { value -> AnotherValue in
...
}
|> deliverOnMainQueue
复制代码
队列
这 Queue
类是 GCD 上的包装,用于管理用于在信号中发送数据的队列。一般使用案例有三个 globalMainQueue
, globalDefaultQueue
, and globalBackgroundQueue
. 没有机制可以避免overcommit
排队,我认为可以改进。
一次性
协议 Disposable
d定义了可以处置的某些东西。它通常与释放资源或取消任务相关联。四类实施此协议,可以涵盖大多数使用案例: ActionDisposable
, MetaDisposable
, DisposableSet
, and DisposableDict
.
承诺
当多个观察者对数据源感兴趣时,为该方案构建了 Promise
和 ValuePromise
类。 支持使用信号更新数据值,同时定义为直接接受值更改。 ValuePromise
让我们看看项目中的一些实际使用案例,演示了 SwiftSignalKit 的使用模式。
iOS 强制应用在访问设备上的敏感信息如: contacts, camera, location, 等. 之前请求用户授权。在与朋友聊天时,电报 iOS 具有将您的位置作为消息发送的功能。让我们看看它如何获得位置授权与信号。
工作流程是一个标准的异步任务,可以由 SwiftSignalKit 建模。authorizationStatus
访问. DeviceAccess.swift
中的功能授权状态返回信号以检查当前授权状态:
public enum AccessType {
case notDetermined
case allowed
case denied
case restricted
case unreachable
}
public static func authorizationStatus(subject: DeviceAccessSubject) -> Signal<AccessType, NoError> {
switch subject {
case .location:
return Signal { subscriber in
let status = CLLocationManager.authorizationStatus()
switch status {
case .authorizedAlways, .authorizedWhenInUse:
subscriber.putNext(.allowed)
case .denied, .restricted:
subscriber.putNext(.denied)
case .notDetermined:
subscriber.putNext(.notDetermined)
@unknown default:
fatalError()
}
subscriber.putCompletion()
return EmptyDisposable
}
}
}
复制代码
当前的实现是管道与另一个然后操作,我相信这是一个复制和粘贴代码,它应该删除。
当 LocationPickerController
它会从授权统计中观察信号,并在未确定权限时调用该信号。 DeviceAccess.authrizeAccess
Signal.start
返回一个 Disposable
实例 。 最佳做法是将其保存在字段变量中并将其处理在。 deinit
.
override public func loadDisplayNode() {
...
self.permissionDisposable =
(DeviceAccess.authorizationStatus(subject: .location(.send))
|> deliverOnMainQueue)
.start(next: { [weak self] next in
guard let strongSelf = self else {
return
}
switch next {
case .notDetermined:
DeviceAccess.authorizeAccess(
to: .location(.send),
present: { c, a in
// present an alert if user denied it
strongSelf.present(c, in: .window(.root), with: a)
},
openSettings: {
// guide user to open system settings
strongSelf.context.sharedContext.applicationBindings.openSettings()
})
case .denied:
strongSelf.controllerNode.updateState { state in
var state = state
// change the controller state to ask user to select a location
state.forceSelection = true
return state
}
default:
break
}
})
}
deinit {
self.permissionDisposable?.dispose()
}
复制代码
#2 更改用户名
让我们看看一个更复杂的例子。电报允许每个用户更改UsernameSetupController
中唯一的用户名。用户名用于生成公共链接,供他人访问您。
实施应满足要求:
- 控制器从当前用户名和当前主题开始。电报有一个强大的 theme system,所有的控制器应该是可主题的。
- 输入字符串应首先在本地验证,以检查其长度和字符。
- 有效的字符串应发送到后端以进行可用性检查。如果快速键入,请求的数量应受到限制。
- UI 反馈应遵循用户的意见。屏幕上的消息应告知新用户名的状态:它正在检查、无效、不可用或可用。当输入字符串有效且可用时,应启用正确的导航按钮。
- 一旦用户想要更新用户名,正确的导航按钮应在更新过程中显示活动指示器。
有三个数据源可能会随着时间的推移而变化:主题、经常账户和编辑状态。主题和帐户是项目的基本数据组件,因此有专用信号: SharedAccountContext.presentationData
and Account.viewTracker.peerView
. 我会试着在其他帖子中覆盖他们。让我们专注于如何一步一步地用信号建模编辑状态。
#1. 结构 UsernameSetupControllerState
使用三个元素定义数据:编辑输入文本、验证状态和更新标志。提供了多个辅助功能来更新它并获取新实例。
struct UsernameSetupControllerState: Equatable {
let editingPublicLinkText: String?
let addressNameValidationStatus: AddressNameValidationStatus?
let updatingAddressName: Bool
...
func withUpdatedEditingPublicLinkText(_ editingPublicLinkText: String?)
-> UsernameSetupControllerState {
return UsernameSetupControllerState(
editingPublicLinkText: editingPublicLinkText,
addressNameValidationStatus: self.addressNameValidationStatus,
updatingAddressName: self.updatingAddressName)
}
func withUpdatedAddressNameValidationStatus(
_ addressNameValidationStatus: AddressNameValidationStatus?)
-> UsernameSetupControllerState {
return UsernameSetupControllerState(
editingPublicLinkText: self.editingPublicLinkText,
addressNameValidationStatus: addressNameValidationStatus,
updatingAddressName: self.updatingAddressName)
}
}
enum AddressNameValidationStatus : Equatable {
case checking
case invalidFormat(TelegramCore.AddressNameFormatError)
case availability(TelegramCore.AddressNameAvailability)
}
复制代码
#2. 状态更改由 statePromise
在 ValuePromise
,这也提供了一个整洁的功能,以省略重复的数据更新。还有一个stateValue
来保存最新的状态,因为外部ValuePromise
是 not visible
这是项目内部与国家价值相匹配的价值承诺的常见模式。公开阅读访问内部价值可能是对海事组织的适当改进。ValuePromise
IMO.
let statePromise = ValuePromise(UsernameSetupControllerState(), ignoreRepeated: true)
let stateValue = Atomic(value: UsernameSetupControllerState())
复制代码
#3. 验证过程可以在管道信号中实现。操作员持有延迟 0.3 秒的请求。对于快速键入,先前的未请求将因第 4 步中的设置而取消。delay
代码语言:javascript复制public enum AddressNameValidationStatus: Equatable {
case checking
case invalidFormat(AddressNameFormatError)
case availability(AddressNameAvailability)
}
public func validateAddressNameInteractive(name: String)
-> Signal<AddressNameValidationStatus, NoError> {
if let error = checkAddressNameFormat(name) { // local check
return .single(.invalidFormat(error))
} else {
return .single(.checking) // start to request backend
|> then(addressNameAvailability(name: name) // the request
|> delay(0.3, queue: Queue.concurrentDefaultQueue()) // in a delayed manner
|> map { .availability($0) } // convert the result
)
}
}
复制代码
#4. MetaDisposable
位可保留信号, 并更新数据内和何时更改。 statePromise
和 stateValue
当 text
改变了 TextFieldNode
.调用时,将处理前一个, checkAddressNameDisposable.set()
, 在第三步触发操作员内部的取消任务。delay
TextFieldNode
文本输入的子类,并包装 UIText 字点。 ASDisplayNode
Telegram-iOS 利用 AsyncDisplayKit
的异步渲染机制,使其复杂的消息 UI 流畅且响应迅速。
let checkAddressNameDisposable = MetaDisposable()
...
if text.isEmpty {
checkAddressNameDisposable.set(nil)
statePromise.set(stateValue.modify {
$0.withUpdatedEditingPublicLinkText(text)
.withUpdatedAddressNameValidationStatus(nil)
})
} else {
checkAddressNameDisposable.set(
(validateAddressNameInteractive(name: text) |> deliverOnMainQueue)
.start(next: { (result: AddressNameValidationStatus) in
statePromise.set(stateValue.modify {
$0.withUpdatedAddressNameValidationStatus(result)
})
}))
}
复制代码
#5. 如果更改其中任何一个信号,操作员 combineLatest
三个信号中,以更新控制器 UI。
let signal = combineLatest(
presentationData,
statePromise.get() |> deliverOnMainQueue,
peerView) {
// update navigation button
// update controller UI
}
复制代码
结论
SSignalKit
是 Telegram-iOS 对反应性编程的解决方案。 核心组件, 如 Signal
和 Promise
, 以与其他反应性框架略有不同的方式实施。它在模块中普遍使用,用于将 UI 与数据更改连接起来。
该设计鼓励大量使用封闭。有许多封闭的相互嵌套,这indents some lines
远。该项目还喜欢 exposing many actions as closures
。对于电报工程师如何保持代码质量和轻松调试信号, 这对我来说仍然是一个神话。