当变换值的序列,这是很常见的每个元件上,以便把该序列到一个新的形式,通过使用API,如执行一些类型的操作,例如map,sort,或filter。然而,虽然这些API非常有用,但有时候我们并不是在寻找另一个值序列 - 而是将所有值都减少到一个值。
这正是Reducer所做的,本周,我们来看看它们可以在Swift中使用的几种不同方式 - 从调用标准库的reduce功能到使用Apple新的Combine框架累积异步值等等。
在Swift中使用reducers的一种非常常见的方法是在集合中对一系列嵌套数值求和。例如,假设我们正在构建一个电子邮件应用程序,并且我们希望显示用户在所有邮箱中的未读邮件总数。一种方法是简单地遍历每个邮箱并将其未读消息的数量添加到跟踪总计数的变量:
func totalUnreadCount() -> Int {
var unreadCount = 0
for mailbox in mailboxes {
unreadCount += mailbox.unreadMessages.count
}
return unreadCount
}
虽然上述工作,但这是一个常见的任务有点冗长,并要求我们跟踪一个可变的局部变量。这是Reducer进来的地方,特别是标准库的reduce
功能,可以在所有符合的类型上使用Sequence
。使用该函数,我们可以将我们的邮箱数组减少为一个Int
代表我们未读消息总数的值 - 如下所示:
func totalUnreadCount() -> Int {
return mailboxes.reduce(0) { count, mailbox in
// Reduce closures get passed the previous value, as well
// as the next element within the sequence that's being
// reduced, and then returns a new value.
count + mailbox.unreadMessages.count
}
}
该0
则传递到reduce
上面的是,将依次传递到我们关闭与序列的第一个元素沿着初始值。
由于求和数是一个常见的用例,我们甚至可能希望更进一步,并专门为此任务引入一个基于路径的关键API,这将让我们轻松地将任何序列减少到符合的任何类型Numeric
(所有标准库号类型,如Int
和Double
,do):
extension Sequence {
func sum(for keyPath: KeyPath) -> T {
return reduce(0) { sum, element in
sum + element[keyPath: keyPath]
}
}
}
要了解有关关键路径的更多信息,请查看“Swift中关键路径的强大功能”。
有了上述内容,我们现在可以快速对序列中的任何嵌套数字求和,这在许多不同的情况下都非常有用:
let unreadCount = mailboxes.sum(for: \.unreadMessages.count)
let totalScore = levels.sum(for: \.playerScore)
let meetingDuration = today.meetings.sum(for: \.duration)
当然,任何序列都可以被简化为任何类型的值,而不仅仅是数值。例如,这里是我们如何使用与上面完全相同的模式来转换图像 - 通过将Transform
值数组减少到我们正在寻找的最终图像:
extension Image {
func applying(_ transforms: [Transform]) -> Image {
return transforms.reduce(self) { image, transform in
transform.apply(to: image)
}
}
}
因此,reduce
只要我们想要将一系列值同步转换为单个结果,标准库的功能就非常有用 - 但这只是一个更大的编程概念的一个例子。
另一种情况是减速器的概念真正有用的是我们想要将多个异步结果合并为一个。例如,我们可能需要调用多个服务器端点以加载我们的一个视图所需的所有数据,或者我们可能希望将网络调用的结果与本地存储在用户设备上的数据相结合。
在WWDC 2019年,Apple推出了一个全新的框架,用于处理异步值,称为*Combine。遵循许多相同的模式,可以在流行的反应式编程框架中找到,例如React和Rx,Combine本质上允许我们将异步数据加载管道视为一系列随时间变化的值*。
这意味着,不是让每个操作只产生一个结果,而是可以在新数据或事件进入时发布多个值- 让我们编写订阅并自动响应状态变化的代码。
虽然我们将在接下来的文章中深入探讨Combine,但让我们来看看它的核心模式的价值流如何与减速器的概念相结合,为我们提供了一种处理多个异步值的强大方法。
现在让我们说我们正在开发一个音乐应用程序,并且我们已经实现了一个SongLoader
允许我们根据一系列ID加载一系列歌曲组。为此,我们首先将每个组ID转换为组合发布者,该发布者执行加载该组的网络请求,然后我们将所有这些发布者合并为单个Song.Group
值流- 如下所示:
import Combine
class SongLoader {
private let urlSession: URLSession
func loadSongs(in groupIDs: [Song.Group.ID]) -> AnyPublisher {
let publishers = groupIDs.map(loadSongs)
// We merge all of our network request publishers into
// a single stream of values, which we set to complete
// after all of our song groups have been loaded:
return Publishers.MergeMany(publishers)
.prefix(groupIDs.count)
.eraseToAnyPublisher()
}
func loadSongs(in groupID: Song.Group.ID) -> AnyPublisher {
let url = makeURLForSongGroup(withID: groupID)
// Perform our network request as a Combine publisher,
// then extract the response data and decode it as JSON:
return urlSession.dataTaskPublisher(for: url)
.map { $0.data }
.decode(type: Song.Group.self, decoder: JSONDecoder())
.eraseToAnyPublisher()
}
}
通过上面的实现,我们将在每个Song.Group
值可用时立即发出它们,这在某些情况下非常有用 - 但是,有时我们确实希望在我们对其执行操作之前等待我们的数据流完成。
例如,在我们的应用程序的一部分中,我们可能想要加载一系列歌曲组以形成视图模型,然后我们将其传递给视图控制器或SwiftUI视图以进行渲染 - 并且为了避免多次渲染传递,我们只想为ViewModel
每个加载会话发出一个值。
这是减速器的另一个很好的用例,在这种情况下,它会将所有异步加载的歌曲组累积为单个ViewModel
值,这与我们之前减少同步值的方式非常相似:
func loadViewModel() -> AnyPublisher {
let groupPublisher = songLoader.loadSongs(in: [
.recentlyPlayed, .favorites, .recommended
])
let viewModelPublisher = groupPublisher.reduce(ViewModel()) {
viewModel, group in
var viewModel = viewModel
viewModel.songs[group.id] = group.songs
return viewModel
}
return viewModelPublisher.eraseToAnyPublisher()
}
除了作为使用减速器模式的简洁方法之外,上述示例还说明了Combine的主要优势之一 - 其API设计反映了许多标准库的同步转换功能。几乎很难说我们正在减少上面的异步值,因为Combine的版本与提供的版本reduce
完全相同Sequence
- 这对于一致性和易学性都是一件好事。
最后,让我们看看对reducer模式略有不同的看法,它使我们能够封装我们改变某个状态以响应某种形式的事件或动作的方式。
例如,假设我们正在为应用程序构建同步引擎,并且我们正在使用SyncState
类型来跟踪引擎所处的当前状态:
struct SyncState {
var isActive: Bool
var interval: TimeInterval
var lastSync: Date?
}
为了使我们的应用程序成为*“良好的平台公民”*并且不浪费宝贵的系统资源,我们希望在收到某些信号时修改上述状态 - 例如当用户的设备电量不足或者电源电量不足时不再连接到WiFi。我们还想lastSync
在同步会话结束时更新我们的日期。
虽然我们可以简单地在代码库的不同部分执行上述类型的更改,但是让我们看看是否可以通过在一个中心位置执行所有操作来使事情更有条理和可预测。为此,我们首先定义一个枚举,其中包含可能影响同步引擎状态的所有信号:
enum SyncAffectingSignal {
case wiFiStatusChanged(isOnWiFi: Bool)
case powerStatusChanged(hasLowPower: Bool)
case syncCompleted
}
然后,为了执行我们的状态变异,我们将再次使用reducer - 只是这次我们不会调用任何系统提供的函数,而是定义我们自己的函数。就像我们之前使用的减速器一样,我们的新功能将减少两个数据 - 先前的状态和接收到的信号 - 并返回一个全新的状态,如下所示:
func reduce(_ state: SyncState,
with signal: SyncAffectingSignal,
currentDate: Date = Date()) -> SyncState {
var state = state
switch signal {
case .wiFiStatusChanged(let isOnWiFi):
state.interval = isOnWiFi ? 600 : 3600
case .powerStatusChanged(let hasLowPower):
state.isActive = !hasLowPower
case .syncCompleted:
state.lastSync = currentDate
}
return state
}
上述类型的模式不仅可以更容易地跟踪我们执行状态突变的位置 - 它还将代码转换为*纯函数*,除其他外,这使得测试变得微不足道:
func testLowPowerPausesSync() {
// All that we have to do in order to test our new reducer
// is to create an intial state, pass it through our
// reducer along with a signal, and verify that the correct
// state is returned as output:
let firstState = SyncState(isActive: true)
let newState = reduce(firstState, with: .powerStatusChanged(hasLowPower: true))
XCTAssertEqual(newState, SyncState(isActive: false))
}
真的很酷!在编写具有单向数据流的应用程序(我们将在即将发表的文章中探讨的技术)时,减速器的上述“风味”非常普遍,但即使没有任何彻底的架构更改,将特定状态突变建模为减速器仍然可以非常优雅。
无论我们处理的是一系列值,一个数据流,一个状态变异还是其他东西 - reducers使我们能够以一种非常简洁的方式封装将一组输入转换为单个输出的逻辑。Reducers还促进了纯函数的使用,这反过来可以帮助我们提高代码的可预测性和可测试性。
然而,虽然减速器可以成为在某些情况下部署的神奇工具,但如果我们不小心,它们也可能成为模糊性的来源。如果没有清晰的上下文,“减少”这个词并不意味着什么,但有时候构建一个薄的包装器(就像我们为总和值所做的那样),在这方面确实可以提供帮助。
你怎么看?您目前是否使用标准库的
reduce
功能,任何其他类型的减速器,或者您将尝试使用它?请通过加我们的交流群 点击此处进交流群 ,来一起交流或者发布您的问题,意见或反馈。
原文地址 https://www.swiftbysundell.com/articles/reducers-in-swift/#lets-start-with-a-summary