D9ac48ee5d2f56d1726877c22f486260
Apple 官方异步编程框架:Swift Combine 应用

WWDC 2019 Session 721 - Combine in Practice

引言

Combine 是 WWDC2019 推出的异步编程框架,在上篇文章中,介绍了 Combine 的基本概念。在这篇文章中,着重介绍一些 Combine 的实际应用。

发布者(Publisher)

假设有一个 MagicTrick 类型的 JSON 数据,这个数据的来源是 NotificationCenter,数据会以 Data 的形式放在 NotificationUserInfo 中。让我们使用 Combine 的发布者来改造这个数据的发送。

extension Notification.Name{
    static var newTrickDownloaded:Notification.Name {
        return Notification.Name("aa")
    }
}

class MagicTrick:Codable {
    var name:String = ""
}

let trickNamePublisher = NotificationCenter.Publisher(center: .default, name: .newTrickDownloaded)

由于 NotificationCenter 发布者的 Output 类型是 Notification,需要类型转换。

map

这个操作符可以改变发布者的类型。

let trickNamePublisher = NotificationCenter.Publisher(center: .default, name: .newTrickDownloaded)
    .map{ notification -> Data in
        let userInfo = notification.userInfo
        return userInfo?["data"] as! Data
    }

这时候 Output 变成了 Data 类型,还不能实际使用。要转换成实际类型,可以用 Codable + JSONDecoder 进行解析,解析时可能会抛出错误,就需要使用 try 关键字。

tryMap

这个操作符允许在转换值的闭包内抛出异常。

let trickNamePublisher = NotificationCenter.Publisher(center: .default, name: .newTrickDownloaded)
    .map{ notification -> Data in
        let userInfo = notification.userInfo
        return userInfo?["data"] as! Data
    }.tryMap { data -> MagicTrick in
        let decoder = JSONDecoder()
        return try decoder.decode(MagicTrick.self, from: data)
    }

decode

而 Combine 为 Codable,还提供了便捷的 decode 方法。这个操作符允许传入 Decodable 类型和解码器,将解码器支持的上游数据类型解码,发送给下游订阅者。

let trickNamePublisher = NotificationCenter.Publisher(center: .default, name: .newTrickDownloaded)
    .map{ notification -> Data in
        let userInfo = notification.userInfo
        return userInfo?["data"] as! Data
    }
    .decode(type: MagicTrick.self, decoder: JSONDecoder())

接下来应该处理解码过程出现的错误了,因为每一个发布者需要描述了他们产生或者允许的错误类型,所以 Combine 中提供了各种各样的错误处理的操作符,对错误做出反应或是从错误中恢复并做一些兜底处理。

assertNoFailure

这个操作符可以在你确认上游发布者不会产生错误时使用,会将错误类型转为 Never,但当错误发生时,将会崩溃。

let trickNamePublisher = NotificationCenter.Publisher(center: .default, name: .newTrickDownloaded)
    .map{ notification -> Data in
        let userInfo = notification.userInfo
        return userInfo?["data"] as! Data
    }
    .decode(type: MagicTrick.self, decoder: JSONDecoder())
    .assertNoFailure()

catch

这个操作符允许在上游发布者发生错误时,提供一个默认的发布者替换上游的发布者,发送值给下游的订阅者,以便做默认兜底方案。

let trickNamePublisher = NotificationCenter.Publisher(center: .default, name: .newTrickDownloaded)
    .map{ notification -> Data in
        let userInfo = notification.userInfo
        return userInfo?["data"] as! Data
    }
    .decode(type: MagicTrick.self, decoder: JSONDecoder())
    .catch{ _ in
        return Publishers.Just(MagicTrick())
    }

Just:是一个很简单的发布者,用需要产生的值进行初始化,就会将该值发送一次给下游订阅者并结束。


flatMap

上面的例子在错误发生后,生成一个 Just 的发布者作为上游发布者的替代品,但 Just 发布者只会产生一个值就结束了,整个事件流就会结束。但我们需要的是当错误发生时,catch 只处理这次错误,但不替换上游的发布者,也就是上游可以继续产生值。为了不影响上游,我们需要一个新的发布者,能将上游的值用新的发布者发送给下游,catch 只影响这个新的发布者,这个时候就需要 flatMap 了。

Combine 里的 flatMap 和函数式编程高阶函数里的 flatMap 一样,可以将包装的类型进行转换,在这里包装就是发布者,类型就是发布者的 Input,也就是说在 flatMap 里面可以返回一个新 Input 类型的发布者。

let trickNamePublisher = NotificationCenter.Publisher(center: .default, name: .newTrickDownloaded)
    .map{ notification -> Data in
        let userInfo = notification.userInfo
        return userInfo?["data"] as! Data
    }
    .flatMap{ data in
        return Publishers.Just(data)
            .decode(type: MagicTrick.self, decoder: JSONDecoder())
            .catch{ _ in
                return Publishers.Just(MagicTrick())
        }
    }

上面例子中,每次 NotificationCenter 的产生的值都在 flatMap 里被生成的 Just 的发布者并发送,当错误发生时,catch 只会替换这个 Just 的发布者,而不会影响 flatMap 上游的发布者,这样就能保证 catch 是对每次错误进行处理的了。

valueForKey

当只需要发送上游发布者的某一个属性的值时,可以使用 ValueForKey 操作符,它允许使用 Key Path 指定上游的某一个属性。这个操作符的声明式 API 如下(还有对应多个 Key Path 的):

public func publisher<T>(for keyPath: KeyPath<Self.Output, T>) -> Publishers.ValueForKey<Self, T>

使用例子如下:

let trickNamePublisher = NotificationCenter.Publisher(center: .default, name: .newTrickDownloaded)
    .map{ notification -> Data in
        let userInfo = notification.userInfo
        return userInfo?["data"] as! Data
    }
    .flatMap{ data in
        return Publishers.Just(data)
            .decode(type: MagicTrick.self, decoder: JSONDecoder())
            .catch{ _ in
                return Publishers.Just(MagicTrick())
        }
    }
        .publisher(for: \.name)

当对值的处理完后,我们需要考虑需不需要对发送和接收的调度进行特殊处理。

debounce

这个操作符作用是去抖动,也就是只关心在某一段时间间隔里最后的消息结果,比如间隔为 1 秒,那么这一秒内多次从上游接受的值只有最后一个会发送给下游。

let trickNamePublisher = NotificationCenter.Publisher(center: .default, name: .newTrickDownloaded)
    .map{ notification -> Data in
        let userInfo = notification.userInfo
        return userInfo?["data"] as! Data
    }
    .flatMap{ data in
        return Publishers.Just(data)
            .decode(type: MagicTrick.self, decoder: JSONDecoder())
            .catch{ _ in
                return Publishers.Just(MagicTrick())
        }
    }
    .debounce(for: 0.5, scheduler: RunLoop.main)

receive(on:)

这个操作符作用是指定订阅者使用哪个调度者里接收值,同理还有 subscibe(on:) 用于指定发布者使用哪个调度者发送值。

调度者(Scheduler):定义了什么时候和在哪个上下文中执行,在 Cocoa 框架中,RunLoopDispatchQueue 已经适配了调度者协议,可以很方便的使用

let trickNamePublisher = NotificationCenter.Publisher(center: .default, name: .newTrickDownloaded)
    .map{ notification -> Data in
        let userInfo = notification.userInfo
        return userInfo?["data"] as! Data
    }
    .flatMap{ data in
        return Publishers.Just(data)
            .decode(type: MagicTrick.self, decoder: JSONDecoder())
            .catch{ _ in
                return Publishers.Just(MagicTrick())
        }
    }
    .receive(on: DispatchQueue.main)

@Published

这个注解是一个属性包装器(Property Wrapper),可以方便地为任何属性生成其对应类型的发布者。这个发布者会在属性值发生变化时发送消息。

属性包装器(Property Wrapper):Swift 5.1 的新特性,可以抽象对属性 Set/Get 方法的通用逻辑。可以在提案中了解更多。

@propertyDelegate public struct Published<Value> : Publisher {
    public typealias Output = Value
    public typealias Failure = Never
    public init(initialValue: Value)
    public func receive<S>(subscriber: S) where Value == S.Input, S : Subscriber, S.Failure == Published<Value>.Failure
    public var value: Value
}

有了这个特性,对已有的代码进行改造接入 Combine 框架就更方便了(截止至 beta2 版本的 Xcode,$ 符号还不能使用):

@Published var foo:String = "test"

let subscription = $foo.sink {
    print("foo is \($0)")
}

这个关键字其实印证了一点,Combine 和 Swift 语言生态的结合其实是很紧密的,也许将来为了 SwiftUI 和 Combine 这种重量级框架更方便使用,Apple 还会接纳更多社区优秀的提案。

Future

Future 是单次执行的发布者,在闭包中主动产生一个值,之后就结束(和 Javascript 的 Promise,Java 的 Future 类似的设计理念)。初始化闭包会返回一个 excutor 闭包,完成同步/异步操作候,通过主动调用 excutor 闭包(Promise 的 fulfill)将值发送给下游,而在 Combine 框架里这个闭包类型的是 Promise 类型,Promise 类型其实是 (Result<Output, Failure>) -> Void 的别名。

extension Publishers {
    final public class Future<Output, Failure> : Publisher where Failure : Error {
        public typealias Promise = (Result<Output, Failure>) -> Void
        public init(_ attemptToFulfill: @escaping (@escaping Publishers.Future<Output, Failure>.Promise) -> Void)
        final public func receive<S>(subscriber: S) where Output == S.Input, Failure == S.Failure, S : Subscriber
    }
}

通过 Future,可以将任何代码适配 Combine 框架,包括现有的同步,异步代码。

let trickNamePublisher = NotificationCenter.Publisher(center: .default, name: .newTrickDownloaded)
    .map{ notification -> Data in
        let userInfo = notification.userInfo
        return userInfo?["data"] as! Data
    }
    .flatMap{ data in
        return Publishers.Future { promise in
            DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
                promise(.success(data))
            }
        }
    }

AnyPublisher

当我们使用 Combine 框架里的方法来生成发布者时,发布者的实际类型可能千奇百怪,对外暴露或者想进行复用时,内部的实际类型其实是不需要关心的,只需要关心 Output 和错误的类型就可以了。这个时候,通用类型 AnyPublisher 就登场了。Combine 提供了 eraseToAnyPublisher 方法,可以将各种各样的发布者统一转换为 AnyPublisher。同样的,还有 eraseToAnySubscribereraseToAnySubject

top Created with Sketch.