Telegram-iOS 第 2 部分的源代码演练:SSignalKit

原文地址 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.

承诺

当多个观察者对数据源感兴趣时,为该方案构建了 PromiseValuePromise 类。 支持使用信号更新数据值,同时定义为直接接受值更改。 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. 状态更改由 statePromiseValuePromise,这也提供了一个整洁的功能,以省略重复的数据更新。还有一个stateValue来保存最新的状态,因为外部ValuePromisenot 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位可保留信号, 并更新数据内和何时更改。 statePromisestateValuetext 改变了 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 对反应性编程的解决方案。 核心组件, 如 SignalPromise, 以与其他反应性框架略有不同的方式实施。它在模块中普遍使用,用于将 UI 与数据更改连接起来。

该设计鼓励大量使用封闭。有许多封闭的相互嵌套,这indents some lines 远。该项目还喜欢 exposing many actions as closures 。对于电报工程师如何保持代码质量和轻松调试信号, 这对我来说仍然是一个神话。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值