Mobius.swift

codecov License

Mobius 是一个函数式响应式框架,用于管理状态演变和副作用。它强调关注点分离、可测试性以及隔离代码中有状态的部分。

Mobius.swift 是最初的 Mobius Java 框架的 Swift 和 Apple 生态系统重点实现。要了解更多信息,请参阅 wiki 用户指南。您还可以观看 Android @Scale 的演讲,其中介绍了 Mobius

此仓库包含核心 Mobius 框架以及用于常见开发场景和测试的附加组件。

兼容性

环境 详情
📱 iOS 10.0+
🛠 Xcode 12.0+
🐦 语言 Swift 5.0

安装

可以使用 Swift Package Manager 为所有 Apple 平台构建 Mobius。

将以下条目添加到您的 Package.swift

.package(url: "https://github.com/spotify/Mobius.swift", from: "0.5.0")

Mobius 实践 - 构建计数器

Mobius 的目标是让您更好地控制应用程序状态。您可以将您的状态视为应用程序中所有当前变量值的快照。在 Mobius 中,我们将所有状态封装在一个数据结构中,我们称之为 *Model*(模型)。

*Model*(模型)可以由您喜欢的任何类型表示。在本例中,我们将构建一个简单的计数器,因此我们所有的状态都可以包含在一个 Int

typealias CounterModel = Int

Mobius 不允许您直接操作状态。为了更改状态,您必须向框架发送消息,说明您想要做什么。我们称这些消息为 *Events*(事件)。在我们的例子中,我们将需要递增和递减计数器。让我们使用 enum 来定义这些情况

enum CounterEvent {
    case increment
    case decrement
}

现在我们有了 *Model*(模型)和一些 *Events*(事件),我们需要为 Mobius 提供一组规则,框架可以使用这些规则代表我们更新状态。我们通过为框架提供一个函数来实现这一点,该函数将按顺序调用每个传入的 *Event*(事件)和最新的 *Model*(模型),以便生成下一个 *Model*(模型)

func update(model: CounterModel, event: CounterEvent) -> CounterModel {
    switch event {
    case .increment: return model + 1
    case .decrement: return model - 1
    }
}

有了这些构建块,我们可以开始将我们的应用程序视为响应事件的离散状态之间的转换。但我们认为拼图中仍然缺少一块——即与状态转换相关的副作用。例如,按下“刷新”按钮可能会使我们的应用程序进入“加载中”状态,同时也会产生从后端获取最新数据的副作用。

在 Mobius 中,我们恰如其分地称这些副作用为 *Effects*(效果)。在我们的计数器示例中,假设当用户尝试递减到 0 以下时,我们播放音效。让我们创建一个 enum 来表示所有可能的效果(在本例中只有一个)

enum CounterEffect {
    case playSound
}

我们现在需要增强我们的 update 函数,使其也返回与某些状态转换相关的一组效果。这看起来像

func update(model: CounterModel, event: CounterEvent) -> Next<CounterModel, CounterEffect> {
    switch event {
    case .increment: 
        return .next(model + 1)
    case .decrement:
        if model == 0 {
            return .dispatchEffects([.playSound])
        } else {
            return .next(model - 1)
        }
    }
}

Mobius 将您在任何状态转换中返回的每个效果发送到一个名为 *Effect Handler*(效果处理器)的东西。让我们现在创建一个。

import AVFoundation

private func beep() {
    AudioServicesPlayAlertSound(SystemSoundID(1322))
}

let effectHandler = EffectRouter<CounterEffect, CounterEvent>()
    .routeCase(CounterEffect.playSound).to { beep() }
    .asConnectable

现在我们已经有了所有部分,让我们将它们组合在一起

let application = Mobius.loop(update: update, effectHandler: effectHandler)
    .start(from: 0)

让我们开始使用我们的计数器

application.dispatchEvent(.increment) // Model is now 1
application.dispatchEvent(.decrement) // Model is now 0
application.dispatchEvent(.decrement) // Sound effect plays! Model is still 0

这涵盖了 Mobius 的基本原理。要了解更多信息,请访问我们的 wiki

行为准则

本项目遵守 开放行为准则。参与即表示您需要遵守此准则。