- 原文地址:Source Code Walkthrough of Telegram-iOS Part 2: SSignalKit
- 原文作者:Bo
原文地址 hubo.dev
Telegram-iOS 在大多数模块中使用反应性编程。在项目内实现反应功能有三个框架……
Telegram-iOS 在大多数模块中使用反应性编程。在项目内实现反应功能有三个框架:
- MTSignal: 这可能是他们第一次尝试在目标-C中建立反应性范式。它主要用于模块 MtProtoKit, 它实现了 MTProto, 电报的移动协议。
- SSignalKit:它是 MTSignal 的后裔, 用于更通用的场景, 具有更丰富的原始和操作。
- SwiftSignalKit: 在Swift的等效端口。
这篇文章侧重于SwiftSignalKit解释其设计与使用案例。
设计
信号 是一个捕捉”随着时间而变化”概念的类。其签名可视为以下内容::
``` // pseudocode public final class Signal
{ public init(_ generator: @escaping(Subscriber
) -> Disposable)
public func start(next: ((T) -> Void)! = nil,
error: ((E) -> Void)! = nil,
completed: (() -> Void)! = nil) -> Disposable}
```
要设置信号,它接受一个发电机关闭,该关闭定义了生成数据()、捕获错误(和更新完成状态)的方法。设置后,该功能可以注册观察者关闭。
start
订阅者
订阅者有逻辑将数据发送到每个观察者关闭与线程安全考虑。
``` // pseudocode public final class Subscriber
{ 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
只要用户未终止,就向关闭发送新数据next -
putError
向关闭发送错误并标记已终止的订阅者error -
putCompletion
调用关闭并标记已终止的订阅者。completed
运营商
定义了一组丰富的操作员,以在信号上提供功能原始。这些原始人被分为几个类别,根据其功能:Catch, Combine, Dispatch, Loop, Mapping, Meta, Reduce, SideEffects, Single, Take, and Timing. 让我们以几个映射操作员为例:
``` public func map
(_ f: @escaping(T) -> R) -> (Signal
) -> Signal
public func filter
(_ f: @escaping(T) -> Bool) -> (Signal
) -> Signal
public func flatMap
(_服务器托管网 f: @escaping (T) -> R) -> (Signal
) -> Signal
public func mapError
(_ f: @escaping(E) -> R) -> (Signal
) -> Signal
```
操作员喜欢关闭转换并返回更改信号数据类型的功能。有一个方便的操作员,以帮助链这些运营商作为管道:map()|>
``` precedencegroup PipeRight { associativity: left higherThan: DefaultPrecedence }
infix operator |> : PipeRight
public func |>
(value: T, function: ((T) -> U)) -> U { return function(value) }
```
运营商可能受到JavaScript世界中提议的 管道运营商 的启发。通过 Swift 的尾随关闭支持,所有操作员都可以通过直观的可读性进行管道传输:|>
``` // 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
{ 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
``` public enum AddressNameValidationStatus: Equatable { case checking case invalidFormat(AddressNameFormatError) case availability(AddressNameAvailability) }
public func validateAddressNameInteractive(name: String) -> Signal
{ 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 。对于电报工程师如何保持代码质量和轻松调试信号, 这对我来说仍然是一个神话。
服务器托管,北京服务器托管,服务器租用 http://www.fwqtg.net
机房租用,北京机房租用,IDC机房托管, http://www.fwqtg.net