原文地址: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
只要用户未终止,就向关闭发送新数据nextputError
向关闭发送错误并标记已终止的订阅者errorputCompletion
调用关闭并标记已终止的订阅者。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
. 我会试着在其他帖子中覆盖他们。让我们专注于如何一步一步地用信号建模编辑状态。
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
。对于电报工程师如何保持代码质量和轻松调试信号, 这对我来说仍然是一个神话。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)