Katana是一个现代Swift框架,用于编写可测试且易于理解的iOS应用程序的业务逻辑。Katana受到了Redux的强烈启发。
简而言之,应用程序的状态完全由单个可序列化的数据结构描述,而更改状态的唯一方法是调度一个StateUpdater
。一个StateUpdater
是一个改变状态的意图,并包含执行此操作所需的所有信息。因为所有更改都集中且按严格顺序发生,所以没有要小心处理的微妙竞争条件。
自从我们开始在生产中使用Katana以来,它就极大地帮助我们。我们的应用程序已被下载数百万次,Katana真正帮助我们快速有效地扩展应用程序。《Bending Spoons'的工程师利用Katana的功能,非常迅速且无妥协地设计和测试复杂的应用程序。
我们自己使用了许多开源项目,并希望回馈社区,希望您会发现这个有用,并且可能会做出贡献。
概述
您的整个应用状态
定义在一个单个结构体中,所有相关应用信息应放置在这里。
struct CounterState: State {
var counter: Int = 0
}
应用状态
只能通过状态更新器
来修改。一个状态更新器
表示导致应用状态
改变的事件。您通过实现一个根据当前应用状态
和状态更新器
本身来更改状态
的updateState(:)
方法来定义状态更新器
的行为。updateState
应该是一个纯函数,这意味着它只依赖于输入(即状态和状态更新器本身),并且它没有副作用,如网络交互。
struct IncrementCounter: StateUpdater {
func updateState(_ state: inout CounterState) {
state.counter += 1
}
}
存储库包含并管理您的整个应用状态
。它负责管理分发的项(例如,刚刚提到的状态更新器
)。
// ignore AppDependencies for the time being, it will be explained later on
let store = Store<CounterState, AppDependencies>()
store.dispatch(IncrementCounter())
您可以要求 Store
通知应用 State
的每次变化。
store.addListener() { oldState, newState in
// the app state has changed
}
副作用
使用纯函数更新应用的状态非常好,并且具有很多优点。然而,应用必须处理外部世界(例如,API调用、磁盘文件管理……)。对于所有这类操作,Katana提供了副作用
的概念。副作用可以用来与其他应用程序的部分进行交互,然后分发新的StateUpdater
来更新您的状态。对于更复杂的情况,您还可以分发其他副作用。
副作用
是在Hydra之上实现的,并允许您使用承诺编写逻辑。为了利用此功能,您必须采用SideEffect
协议。
struct GenerateRandomNumberFromBackend: SideEffect {
func sideEffect(_ context: SideEffectContext<CounterState, AppDependencies>) throws {
// invokes the `getRandomNumber` method that returns a promise that is fullfilled
// when the number is received. At that point we dispatch a State Updater
// that updates the state
context.dependencies.APIManager
.getRandomNumber()
.then { randomNumber in context.dispatch(SetCounter(newValue: randomNumber)) }
}
}
struct SetCounter: StateUpdater {
let newValue: Int
func updateState(_ state: inout CounterState) {
state.counter = self.newValue
}
}
此外,您还可以利用Hydra.await
运算符编写模仿async/await模式的逻辑,这种方法允许您同步方式编写异步代码。
struct GenerateRandomNumberFromBackend: SideEffect {
func sideEffect(_ context: SideEffectContext<CounterState, AppDependencies>) throws {
// invokes the `getRandomNumber` method that returns a promise that is fulfilled
// when the number is received.
let promise = context.dependencies.APIManager.getRandomNumber()
// we use Hydra.await to wait for the promise to be fulfilled
let randomNumber = try Hydra.await(promise)
// then the state is updated using the proper state updater
try Hydra.await(context.dispatch(SetCounter(newValue: randomNumber)))
}
}
为了进一步提高副作用的可用性,也存在一个可以返回值的版本。请注意,状态和依赖项类型都被擦除,以便在使用库等时更加自由。
struct PurchaseProduct: ReturningSideEffect {
let productID: ProductID
func sideEffect(_ context: AnySideEffectContext) throws -> Result<PurchaseResult, PurchaseError> {
// 0. Get the typed version of the context
guard let context = context as? SideEffectContext<CounterState, AppDependencies> else {
fatalError("Invalid context type")
}
// 1. purchase the product via storekit
let storekitResult = context.dependencies.monetization.purchase(self.productID)
if case .failure(let error) = storekitResult {
return .storekitRejected(error)
}
// 2. get the receipt
let receipt = context.dependencies.monetization.getReceipt()
// 3. validate the receipt
let validationResult = try Hydra.await(context.dispatch(Monetization.Validate(receipt)))
// 4. map error
return validationResult
.map { .init(validation: $0) }
.mapError { .validationRejected($0) }
}
}
请注意,如果这是库/应用的突出用途,则步骤0
可以封装在这个协议中:
protocol AppReturningSideEffect: ReturningSideEffect {
func sideEffect(_ context: SideEffectContext<AppState, DependenciesContainer>) -> Void
}
extension AppReturningSideEffect {
func sideEffect(_ context: AnySideEffectContext) throws -> Void {
guard let context = context as? SideEffectContext<AppState, DependenciesContainer> else {
fatalError("Invalid context type")
}
self.sideEffect(context)
}
}
依赖项
副作用示例利用了APIManager
方法。副作用可以通过上下文的dependencies
参数获取APIManager
。依赖项容器是Katana进行依赖注入的方式。我们测试我们的副作用,因为这样我们才可能去掉单例或其他坏习惯来防止我们进行测试。创建依赖容器非常简单:只需创建一个符合SideEffectDependencyContainer
协议的类,将存储泛型化为它,并在副作用中使用它。
final class AppDependencies: SideEffectDependencyContainer {
required init(dispatch: @escaping PromisableStoreDispatch, getState: @escaping GetState) {
// initialize your dependencies here
}
}
拦截器
在定义《Store》时,您可以提供一个拦截器列表,这些拦截器按给定顺序触发,每当某个项目被发送时。拦截器类似于一个“揽储全局”系统,可以用来实现日志记录或动态更改`Store`的行为。每当一个可发送的项目即将处理时,都会调用拦截器。
可发送Logger
Katana内置了一个可发送Logger
拦截器,它记录所有可发送的项目,但不包括那些列入黑名单参数的项目。
let dispatchableLogger = DispatchableLogger.interceptor(blackList: [NotToLog.self])
let store = Store<CounterState>(interceptor: [dispatchableLogger])
观察者拦截器
有时监听系统中发生的事件并对它们做出反应是有用的。Katana提供了一个观察者拦截器
,可用来实现这一结果。
具体来说,您可以指示拦截器在以下情况下发送项目:
- 当Store初始化时
- 状态以特定方式更改时
- 当Store管理特定的可发送项目时
- 当发送特定的通知到默认的通知中心时
let observerInterceptor = ObserverInterceptor.observe([
.onStart([
// list of dispatchable items dispatched when the store is initialized
])
])
let store = Store<CounterState>(interceptor: [observerInterceptor])
注意,当使用观察者拦截器
拦截副作用时,可发送的返回值不可用于拦截器本身。
关于UI的部分呢?
Katana旨在为您的应用逻辑部分提供结构。至于UI,我们提出了两个替代方案:
-
Tempura:这是我们建立在Katana之上的MVVC框架,并且在Bending Spoons的所有应用程序开发中都是非常乐意使用的。Tempura是一个轻量级的、与UIKit友好的库,允许您自动将UI与您的应用状态同步。这是我们推荐的选择。
-
Katana-UI:通过这个库,我们旨在将React迁移到UIKit,它允许您使用声明式方法创建应用。该库最初与Katana捆绑在一起,我们决定将其拆分,因为从内部我们已经不再使用了。事后看来,我们发现React类似方法与UIKit命令式现实的阻抗不匹配对我们来说是不利的。
Signpost Logger
Katana 自动集成了 Signpost API。这个集成层让你可以在 Instruments 中看到所有已发送的项目,它们持续的时间,以及如并行度这样的有用信息。此外,你可以分析你发送的项目对 CPU 的 影响,以进一步优化应用程序的性能。
Bending Spoons 指南
在 Bending Spoons 中,我们广泛使用 Katana。这些年来,我们定义了一些最佳实践,帮助我们将代码写得更加易于阅读和调试。我们决定开源它们,以便每个人都能够在使用 Katana 的时候有一个起点。你可以在这里找到它们。
从 2.x 迁移
我们强烈建议升级到新的 Katana。实际上,新 Katana 不仅仅为库添加了新的非常强大的功能,而且还被设计成与旧逻辑有极高的兼容性。你为 Katana 2.x 编写的所有操作和中间件在新 Katana 中也将继续工作。大多数破坏性变更都与简单的类型更改有关,这些更改很容易解决。
然而,如果你更愿意继续使用 Katana 2.x,你仍然可以在专用分支中访问 Katana 2.x。
中间件
在 Katana 中,我们已经将 中间件
的概念替换为新的 拦截器
概念。你仍然可以通过使用 middlewareToInterceptor
方法来使用你的中间件。
Swift 版本
某些版本的 Katana 只支持某些版本的 Swift。根据您的项目使用的是哪个版本的 Swift,您应该使用 Katana 的特定版本。使用以下表格来查看您需要哪个版本的 Katana。
Swift 版本 | Katana 版本 |
---|---|
Swift 5.0 | Katana >= 6.0 |
Swift 4.2 | Katana >= 2.0 |
Swift 4.1 | Katana < 2.0 |
从这里开始
试一下
pod try Katana
Tempura
使用 Katana 和 Tempura 制作出色的应用程序
查看文档
您还可以通过使用适当的 docset 将 Katana 添加到 Dash。
安装
Katana 可通过 CocoaPods 和 Swift Package Manager 获取,您也可以将 Katana.project
添加到您的 Xcode 项目中。
需求
-
iOS 11.0+ / macOS 10.10+
-
Xcode 9.0+
-
Swift 5.0+
Swift Package Manager
Swift Package Manager 是一个用于管理 Swift 代码分发的工具。它与 Swift 构建系统集成,以自动化依赖项的下载、编译和链接过程。
使用 Swift Package Manager 将 Katana 集成到项目中的有两种方法
- 将其添加到
Package.swift
- 从 Xcode 的
文件
->Swift 包
->添加包依赖
直接添加
在两种情况下,您只需提供此 URL:[email protected]:BendingSpoons/katana-swift.git
CocoaPods
CocoaPods 是 Cocoa 项目的依赖项管理器。您可以使用以下命令安装它
$ sudo gem install cocoapods
要使用 CocoaPods 将 Katana 集成到您的 Xcode 项目中,您需要创建一个 Podfile
。
对于 iOS 平台,内容如下
use_frameworks!
source 'https://github.com/CocoaPods/Specs.git'
platform :ios, '9.0'
target 'MyApp' do
pod 'Katana'
end
现在,只需运行
$ pod install
联系我们
- 如果您有任何疑问,您可以在推特上找到我们:@maurobolis,@luca_1987,@smaramba
特别的感谢
- Bending Spoons 团队 提供宝贵的意见;
- @orta 在如何开源项目上提供了意见;
- @danielemargutti 开发并维护了 Hydra;
贡献
- 如果你发现了一个 错误,请提交一个 issue;
- 如果你有 功能需求,请提交一个 issue;
- 如果你想贡献,请提交一个 pull request;
- 如果你对如何改进框架或推广知晓有任何想法,请联系我们;
- 如果你想在项目中尝试这个框架或编写一个演示,请发送仓库链接。
许可协议
Katana 采用了 MIT 许可协议。
关于
Katana 由 Bending Spoons 维护。我们研发了自己的技术产品,被全球数百万人所使用和喜爱。感兴趣吗?查看我们!