Katana 6.0.3

Katana 6.0.3

测试已测试
语言语言 SwiftSwift
许可证 MIT
发布最后发布2021年9月
SPM支持SPM

Bending SpoonsBending Spoons维护。



Katana 6.0.3

Katana

Twitter URL Build Status Docs CocoaPods Licence

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命令式现实的阻抗不匹配对我们来说是不利的。

Tempura Katana UI

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 可通过 CocoaPodsSwift 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

联系我们

特别的感谢

贡献

  • 如果你发现了一个 错误,请提交一个 issue;
  • 如果你有 功能需求,请提交一个 issue;
  • 如果你想贡献,请提交一个 pull request;
  • 如果你对如何改进框架或推广知晓有任何想法,请联系我们;
  • 如果你想在项目中尝试这个框架或编写一个演示,请发送仓库链接。

许可协议

Katana 采用了 MIT 许可协议

关于

Katana 由 Bending Spoons 维护。我们研发了自己的技术产品,被全球数百万人所使用和喜爱。感兴趣吗?查看我们