Telegram-iOS 源码分析:Part 2: SSignalKit

Introduction

Telegram-iOS uses reactive programming in most modules. There are three frameworks to achieve reactive functions inside the project:
MTSignal: it might be their first attempt for a reactive paradigm in Objective-C. It’s mainly used in the module MtProtoKit, which implements MTProto, Telegram’s mobile protocol.
SSignalKit: it’s a descendant of MTSignal for more general scenarios with richer primitives and operations.
SwiftSignalKit: an equivalent port in Swift.
This post focuses on SwiftSignalKit to explain its design with use cases.

Design

Signal

Signal is a class that captures the concept of “change over time”. Its signature can be viewed as below:

// 伪代码
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
}

To set up a signal, it accepts a generator closure which defines the ways to generate data(), catch errors(), and update completion state. Once it’s set up, the function startcan register observer closures.

Subscriber

Subscriber has the logics to dispatch data to each observer closure with thread safety consideration.

// 伪代码
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()
}

A subscriber is terminated when an error occurred or it’s completed. The state can not be reversed.
putNext sends new data to the next closure as long as the subscriber is not terminated
putError sends an error to the error closure and marks the subscriber terminated
putCompletion invokes the completed closure and marks the subscriber terminated.

Operators

A rich set of operators are defined to provide functional primitives on Signal. These primitives are grouped into several categories according to their functions: Catch, Combine, Dispatch, Loop, Mapping, Meta, Reduce, SideEffects, Single, Take, and Timing. Let’s take several mapping operators as an example:

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>

The operator like map() takes a transformation closure and returns a function to change the data type of a Signal. There is a handy |>operator to help chain these operators as pipes:

//自定义操作符   |>
precedencegroup PipeRight {
    associativity: left
    higherThan: DefaultPrecedence
}

infix operator |> : PipeRight

public func |> <T, U>(value: T, function: ((T) -> U)) -> U {
    return function(value)
}

The operator |>might be inspired by the proposed pipeline operator in the JavaScript world. By the trailing closure support from Swift, all operators can be pipelined with intuitive readability:

// 伪代码
let anotherSignal = valueSignal
    |> filter { value -> Bool in
      ...
    }
    |> take(1)
    |> map { value -> AnotherValue in
      ...
    }
    |> deliverOnMainQueue

Queue

The class Queue is a wrapper over GCD to manage the queue used to dispatch data in a Signal. There are three preset queues for general use cases: globalMainQueue, globalDefaultQueue, and globalBackgroundQueue. There is no mechanism to avoid overcommit to queues, which I think could be improved.

Disposable

The protocol Disposable defines something that can be disposed of. It’s usually associated with freeing resources or canceling tasks. Four classes implement this protocol and could cover most use cases: ActionDisposable, MetaDisposable, DisposableSet, and DisposableDict.

Promise

The classes Promise and ValuePromise are built for the scenario when multiple observers are interested in a data source. Promise supports using a Signal to update the data value, while ValuePromise is defined to accept the value changes directly.

Use Cases

Let’s check out some real use cases in the project, which demonstrate the usage pattern of SwiftSignalKit.
#1 Request Authorization
iOS enforces apps to request authorization from the user before accessing sensitive information on devices, such as contacts, camera, location, etc. While chatting with a friend, Telegram-iOS has a feature to send your location as a message. Let’s see how it gets the location authorization with Signal.

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
            }
    }
}

The workflow is a standard asynchronous task that can be modeled by SwiftSignalKit. The function authorizationStatus inside DeviceAccess.swift returns a Signal to check the current authorization status:

The current implementation is piped with another then operation, which I believe it’s a piece of copy-and-paste code, and it should be removed.
When aLocationPickerController is present, it observes on the signal from authorizationStatus and invokes DeviceAccess.authrizeAccess if the permission is not determined.
Signal.start returns an instance of Disposable. The best practice is to hold it in a field variable and dispose of it in 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 Change Username

Let’s check out a more complex example. Telegram allows each user to change the unique username in UsernameSetupController. The username is used to generate a public link for others to reach you.
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值