{"id":15055385,"url":"https://github.com/hollyoops/recoilswift","last_synced_at":"2025-04-03T03:09:59.775Z","repository":{"id":40395892,"uuid":"385093461","full_name":"hollyoops/RecoilSwift","owner":"hollyoops","description":"A New, Functional, Modern Reactive State Management Library for UIKit and SwiftUI (The iOS implementation of Recoil)","archived":false,"fork":false,"pushed_at":"2023-08-15T10:01:59.000Z","size":12805,"stargazers_count":237,"open_issues_count":0,"forks_count":6,"subscribers_count":6,"default_branch":"master","last_synced_at":"2025-04-03T03:09:53.702Z","etag":null,"topics":["functional-programming","ios","mvvm","recoil","redux","reswift","state-mangement","swift","swiftui","tca"],"latest_commit_sha":null,"homepage":"","language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/hollyoops.png","metadata":{"files":{"readme":"README-ZH.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2021-07-12T01:39:46.000Z","updated_at":"2025-03-12T09:35:02.000Z","dependencies_parsed_at":"2024-08-21T15:33:55.081Z","dependency_job_id":"71a97992-3e54-4295-aa53-3e0735ad6bd0","html_url":"https://github.com/hollyoops/RecoilSwift","commit_stats":null,"previous_names":[],"tags_count":7,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hollyoops%2FRecoilSwift","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hollyoops%2FRecoilSwift/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hollyoops%2FRecoilSwift/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hollyoops%2FRecoilSwift/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/hollyoops","download_url":"https://codeload.github.com/hollyoops/RecoilSwift/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":246927835,"owners_count":20856198,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["functional-programming","ios","mvvm","recoil","redux","reswift","state-mangement","swift","swiftui","tca"],"created_at":"2024-09-24T21:41:43.271Z","updated_at":"2025-04-03T03:09:59.747Z","avatar_url":"https://github.com/hollyoops.png","language":"Swift","funding_links":[],"categories":[],"sub_categories":[],"readme":"# RecoilSwift\n\n[![Version](https://img.shields.io/github/v/tag/hollyoops/recoilswift?label=version\u0026style=flat)](https://github.com/hollyoops/recoilswift)\n[![License](https://img.shields.io/github/license/hollyoops/recoilswift?style=flat)](https://github.com/hollyoops/recoilswift)\n[![Main workflow](https://github.com/hollyoops/RecoilSwift/actions/workflows/main.yml/badge.svg)](https://github.com/hollyoops/RecoilSwift/actions/workflows/main.yml)\n[![codecov](https://codecov.io/gh/hollyoops/RecoilSwift/branch/master/graph/badge.svg?token=AZ9YSL9H0H)](https://codecov.io/gh/hollyoops/RecoilSwift)\n\n\nRecoilSwift是一个针对`SwiftUI`的轻量级、可组合的状态管理框架，同时兼容`UIKit`。它可以作为传统的`MVVM`或者`Redux-like`架构方案（如:`reswift`、`TCA`）的替代者。\n\n\u003e **注意：** 从0.3版本开始，RecoilSwift已经支持了`UIKit`。如果你想在`UIKit`中使用RecoilSwift，你可以查看master分支的例子。但需要注意的是，我们目前仍处于beta阶段，未来的接口可能会有所调整。\n\n## Recoil概览\n\n`Recoil`是由`Facebook`提出的一种可组合的应用状态管理方案。它简化了`Redux`，可以作为`Redux`的优雅替代者。\n\n想要更快速的了解Recoil，你可以观看下面的视频，或者访问[官网](https://recoiljs.org/)。\n\n[![Watch the video](./Docs/Images/Recoil.png)](https://www.youtube.com/watch?v=_ISAA_Jt9kI)\n\n## 动机\n\n当前的iOS架构模式（如:`MVVM`）在配合声明式编程时存在一些问题，而且痛点众多。因此，在声明式的UI框架中，很多开发者更倾向于选择`Redux-like`的状态管理架构方案（如:`ReSwift`、`TCA`）。但`Redux`方案复杂，学习成本较高，同时模板代码过多，写起来比较累。`Recoil`应运而生，它主要有以下特点：\n\n- 概念简单，易于上手  \n- 原子化状态，状态可组合  \n- 响应式编程  \n- 声明式编程，无模板代码，降低代码量\n\n使用Recoil后，代码会变得更加简洁，同时不同的组件可以非常方便地共享状态。\n\n## API 文档\n\n:closed_book: [**API 文档**](https://recoilswift-hollyoops.netlify.app/)\n\n## 基本概念\n\n在Recoil中，有两个基本的概念：\n\n1. 原子（`Atoms`）：原子是状态的基本单元，是一种有状态的对象。原子可以被读取和写入，其类型可以是任意数据类型。\n2. 选择器（`Selectors`）：选择器从一个或多个原子中派生出新的状态，这种派生状态可以被订阅以获取状态的更新，它们也可以作为其他选择器的输入。\n\n通常，我们会在`Atoms`中存放源数据，在`selector`中放置业务逻辑。而选择器的业务单元是可以被组合的。如下图所示：\n\n![\u003cimg src=\"image.png\" width=\"700\" height=\"378\"/\u003e](./Docs/Images/Flow.png)\n\n上图中， 黄色的是`Atoms`, 棕色的是 `Selectors`， 箭头表示状态的组合，依赖关系。\n\n- `Atom`不能依赖其他`Atom` 。\n- `Selector` 可以组合其他 `Selector` 或 `Atom` 并自动建立依赖关系，它是响应式的，任何上游的值变动，下游的选择器都会自动重新执行求值\n\n**总而言之：**\n\n- Recoil的状态是原子化的，可以轻松地组合和重用。\n- Recoil的状态是响应式的，自动建立依赖关系。任何上游的值变动，下游的选择器都会自动重新执行计算逻辑，获取最新的值，并刷新UI。\n- Recoil的状态独立于UI组件，轻松实现跨组件的状态共享。\n\n这三个特性使你的代码更加简洁，同时提高了代码的重用性。\n\n## 安装\n\n- 前置条件： iOS 13+，Xcode 14.3+\n  \n- [**Swift Package Manager**](https://swift.org/package-manager/)\n\n```swift\n.Pagckage(url: \"https://github.com/hollyoops/RecoilSwift.git\", from: \"master\")\n```\n\n- [**CocoaPods**](https://cocoapods.org) \n\n你也可以用CocoaPods安装\n\n```ruby\npod 'RecoilSwift'\n```\n\n## 基本用法\n\n你可以在 `UIKit` 和 `SwiftUI` 中使用 RecoilSwift。\n\n(在UIKit中 使用RecoilSwift, **请查看 [更多用法](#更多用法)**)\n\n### 在SwiftUI中 使用RecoilSwift\n\n在 SwiftUI 中，RecoilSwift 提供了两类方式的 API：基于 `PropertyWrapper` 的 API 和基于 Hooks 的 API。`PropertyWrapper`API 更符合 iOS 规范，更适合原生开发者。Hooks API 更贴合官方 API，更适合前端开发者。\n\n下面是基于 `PropertyWrapper` 的 API 使用方式，Hooks API 的使用方式请查看[这里](#更多用法)。\n\n### RecoilRoot\n\n首先，请使用 `RecoilRoot` 包裹你的View。\n\n```swift\nstruct YourApp: App {\n    var body: some scene {\n        WindowGroup {\n            RecoilRoot {\n                AppView()\n            }\n        }\n    }\n}\n```\n\n### 创建并使用状态\n\nRecoilSwift 提供两种定义状态的方式：使用 `State Function` 创建状态和继承协议生成自定义状态。\n\n#### 使用`State Function` 创建状态：\n\n通过`atom` 和 `selector` 函数创建状态，这种方式的优势是 API 更贴近官方 API，某些情况下更简洁。但你需要遵循以下模式。\n\n```swift\nstruct CartState {\n    /// 1. 定义计算属性\n    static var allCartItem: Atom\u003c[CartItem]\u003e {\n        /// 2. 用函数创建状态\n        atom { [CartItem]() }\n    }\n\n    /// UI显示逻辑：如果商品个数小于10个，则显示原本数量，否者个显示9+\n    static var numberOfProductBadge: RecoilSwift.Selector\u003cString?\u003e {\n        selector { accessor -\u003e String? in\n            /// 注意：下面这个简单的 `get` 方法，是从其他的`atom/selector`中获取数据\n            /// 其实它还和`allCartItem`建立了上下游关系。当allCartItem数据发生变化,\n            /// 当前`numberOfProductBadge` 会自动重新计算。这让不同状态可以组合，重用，非常强大!\n            let items = try accessor.get(allCartItem)\n            let count = items.reduce(into: 0) { result, item in\n                result += item.count\n            }\n            return count \u003c 10 ? \"\\(count)\" : \"9+\"\n        }\n    }\n}\n```\n\n这里数据源是allCartItem，它是我们用 `atom` 函数创建的 同步Atom，表示购物车内商品列表。`numberOfProductBadge` 是一个我们用 `selector` 函数创建的同步Selector，表示购物车里所有商品的个数的总和。当购物车里面的商品列表发生的变化，这个`numberOfProductBadge`自动发生重新计算，并刷新UI。\n\n在UI上这样使用: \n\n```swift\nstruct YourView: View { \n    @RecoilScope var recoil\n\n    var body: some View { \n     // 当 `numberOfProductBadge` 的值发生改变，`View` 会自动重新渲染，拿到最新的值\n     let badge = recoil.useValue(CartState.numberOfProductBadge)\n      \n      Text(badge)\n    }\n}\n```\n\n### 创建自定义状态：\n\n如果你不想使用函数创建状态，你可以自己定义一个类，并继承以下协议之一来生成自定义状态：\n\n- `SyncAtomNode` 同步的Atom协议\n- `AsyncAtomNode` 异步的Atom协议\n- `SyncSelectorNode` 同步Selector协议\n- `AsyncSelectorNode` 异步Selector协议\n\n```swift\nstruct AllCartItem: SyncAtomNode, Hashable {\n  typealias T = [CartItem]\n  func defaultValue() -\u003e [CartItem] {\n    []\n  }\n}\n\nstruct NumberOfProductBadge: SyncSelectorNode, Hashable {\n  typealias T = String?\n  func getValue(accessor: StateAccessor) -\u003e String? {\n      let items = try accessor.get(AllCartItem()) //创建对象\n      let count = items.reduce(into: 0) { result, item in\n          result += item.count\n      }\n      return count \u003c 10 ? \"\\(count)\" : \"9+\"\n  }\n}\n```\n在UI上这样使用: \n\n```swift\nstruct YourView: View { \n    @RecoilScope var recoil\n\n    var body: some View { \n     let badge = recoil.useValue(NumberOfProductBadge())\n      \n      Text(badge)\n    }\n}\n```\n\n### 创建并使用带参数状态:\n\n有些时候你的状态可能需要接受一些外部的参数。这个时候这个时候你就需要用到带参的状态。和定义状态一样，RecoilSwift提供两种方式去定义带参的状态：\n\n**1. atomFamily \u0026 selectorFamily 函数创建带参数的状态：**\n\n```Swift\nvar remoteDataById: AsyncSelectorFamily\u003cString, String\u003e {\n   selectorFamily { (id: String, get: Getter) async -\u003e [String] in\n      let posts = try await fetchAllData()\n      return posts[id]\n   }\n}\n\nstruct YourView: View { \n  @RecoilScope var recoil\n  var body: some View {\n    let loadable = recoil.useLoadable(remoteDataById(id))\n        \n    return VStack {\n        if loadable.isLoading {\n            ProgressView()\n        }\n        \n        if let err = loadable.errors.first {\n            errorView(err)\n        }\n\n        // when data fulfill\n        if let names = loadable.data {\n           dataView(allBook: names, onRetry: loadable.load)\n        }\n    }\n  }\n}\n```\n\n**2. 使用带参数的自定义状态：**\n\n我们自定义了一个异步 `Selector`，它远程获取一篇文章的内容\n\n```swift\nstruct RemoteData: AsyncSelectorNode, Hashable {\n  typealias T = String\n  let id: String\n\n  func getValue(accessor: StateAccessor) async throws -\u003e String {\n      let posts = try await fetchAllData()\n      return posts[id]\n  }\n}\n```\n然后这样使用：\n\n```swift\nvar body: some View {\n    let loadable = recoil.useLoadable(RemoteData(id))\n    ...\n}\n```\n\n## 调试状态\n\n有时候，我们想查看整个应用的状态图，确保状态之间的关系正确无误。RecoilSwift 提供了 `SnapshotView` 来帮助你调试状态。你只需在 RecoilRoot 中启用 `shakeToDebug`，然后摇动手机即可自动弹出应用状态图。\n\n```swift\n  RecoilRoot(shakeToDebug: true) {\n    content\n  }\n```\n\n![demo](./Docs/Images/StateSnapshot.jpg)\n\n上图中， 黄色的是Atoms, 棕色的是 Selectors。 箭头表示状态的组合，依赖关系。\n\n## 更多用法\n\n### 如何在RecoilSwift中进行状态测试 \n---\n在RecoilSwift中，您可以借助`@RecoilTestScope`来进行状态测试。\n\n```swift\nfinal class AtomAccessTests: XCTestCase {\n    /// 1. 初始化scope\n    @RecoilTestScope var recoil\n    override func setUp() {\n        _recoil.purge()\n    }\n    \n    func test_should_returnUpdatedValue_when_useRecoilState_given_stringAtom() {\n        /// 通过 `useRecoilXXX` API 订阅状态\n        let value = recoil.useBinding(TestModule.stringAtom, default: \"\")\n        XCTAssertEqual(value.wrappedValue, \"rawValue\")\n        \n        value.wrappedValue = \"newValue\"\n\n        /// 通过 `useRecoilValue` API 订阅并获取状态的最新值 \n        let newValue = recoil.useValue(TestModule.stringAtom)\n        XCTAssertEqual(newValue, \"newValue\")\n    }\n}\n```\n\n#### **测试View 渲染：**\n\n有时，您可能需要进行更全面的端到端测试。例如，您可能希望模拟View的渲染，此时，可以借助`ViewRenderHelper`进行从视图到状态的端到端测试。\n`ViewRenderHelper` 能够模拟视图的多次渲染，\n\n```swift\n/// 1. 引入测试框架\nimport RecoilSwiftTestKit\n\nfinal class AtomAccessWithViewRenderTests: XCTestCase {\n    // ...\n    func test_should_atom_value_when_useValue_given_stringAtom() async {\n        /// `ViewRenderHelper` 的回调可能会被多次触发，\n        let view = ViewRenderHelper { recoil, sut in\n            let value = recoil.useValue(TestModule.stringAtom)\n            /// 一旦`expect` 的期望得到满足，测试即视为成功，否则在超时时，测试将失败\n            sut.expect(value).equalTo(\"rawValue\")\n        }\n        \n        /// 模拟视图渲染\n        await view.waitForRender()\n    }\n}\n```\n\n\u003cdetails\u003e\u003csummary\u003e**点击查看如何使用`HookTester`进行Hook API测试**\u003c/summary\u003e\n\n```swift\nfinal class AtomReadWriteTests: XCTestCase {\n    @RecoilTestScope var recoil\n    override func setUp() {\n        _recoil.purge()\n    }\n    \n    func test_should_return_rawValue_when_read_only_atom_given_stringAtom() {\n        /// 注意：需要定义HookTest，并将Scope传入\n        let tester = HookTester(scope: _recoil) {\n            useRecoilValue(TestModule.stringAtom)\n        }\n        \n        XCTAssertEqual(tester.value, \"rawValue\")\n    }\n}    \n```\n\n\u003c/details\u003e\n\n#### **Stub/Mock状态：**\n  \n很多时候我们的Selector， 会依赖其他状态。 比如下面的代码, `state` 依赖了一个上游的状态 (`state -\u003e upstreamState`):\n\n```swift\nstruct MultipleTen {\n    static var state: Selector\u003cInt\u003e {\n        selector { context in\n            try context.get(upstreamState) * 10\n        }\n    }\n    \n    static var upstreamState: Atom\u003cInt\u003e {\n        atom {  0 }\n    }\n}\n```\n\n但是我们在单元测试时候，很多时候我们不想要测试这个 `UpstreamState`. 我们想要stub/mock它。 我们可以通过下面的代码来`RecoilTestScope`的stub， 方法来`stub`状态:\n\n```swift\n func test_should_return_upstream_asyncError_when_get_value_given_upstream_states_hasError() async throws {\n        // stub  `upstreamState`  让其返回错误， 你也可以stub返回其他的正确值\n        // _recoil.stubState(node: AsyncMultipleTen.upstreamState, value: 100)\n        _recoil.stubState(node: AsyncMultipleTen.upstreamState, error: MyError.param)\n        \n        do {\n            _ = try await accessor.get(AsyncMultipleTen.state)\n            XCTFail(\"should throw error\")\n        } catch {\n            XCTAssertEqual(error as? MyError, MyError.param)\n        }\n    }\n```\n\n### UIKit 用法\n---\n你也可以在 UIKit 中使用 RecoilSwift，甚至在 UIKit 和 SwiftUI 中混合使用。你唯一需要做的就是让你的 `UIViewController` 或 `UIView` 继承 `RecoilUIScope` 协议。\n\n```swift\n/// 1. 继承 RecoilUIScope 协议\nextension BooksViewController: RecoilUIScope {\n\n  /// 2. 实现 refresh 方法，该方法会在你订阅的状态发生改变时被调用\n  func refresh() {\n\n    /// 3. 获取并订阅状态的值\n    let value = recoil.useValue(MyState())\n\n    // 4. 将状态的值绑定到 UI 上\n    valueLabel.text = value\n    ...\n  }\n}\n```\n\n\u003cdetails\u003e\n\u003csummary\u003e稍微复杂的例子\u003c/summary\u003e\n\n```swift\nextension BooksViewController: RecoilUIScope {\n    func refresh() {\n        let booksLoader = recoil.useLoadable(BookList.currentBooks)\n        \n        if let error = booksLoader.errors.first {\n            loadingSpinner.stopAnimating()\n            tableView.isHidden = true\n            emptyDataLabel.isHidden = true\n            errorLabel.text = error.localizedDescription\n            errorLabel.isHidden = false\n        } else if let books = booksLoader.data {\n            loadingSpinner.stopAnimating()\n            \n            if books.isEmpty {\n                tableView.isHidden = true\n                emptyDataLabel.isHidden = false\n            } else {\n                tableView.isHidden = false\n                emptyDataLabel.isHidden = true\n                self.books = books\n                tableView.reloadData()\n            }\n        } else {\n            tableView.isHidden = true\n            emptyDataLabel.isHidden = true\n            loadingSpinner.startAnimating()\n        }\n    }\n}\n```\n\n\u003c/details\u003e\n\n**更多请查看 `Example` 里面的UIKit的例子**\n\n### Hooks API 用法\n---\nRecoilSwift 提供了一套基于 Hooks API 的用法，Hooks 非常接近官方的 API，Hook API 以 `use` 开头，例如 `useRecoilXXX`。这种方式更适合前端开发者，没有任何学习门槛。\n\n由于基于 Hooks API，因此你的 View 必须满足 [Hooks 的规范](https://github.com/ra1028/SwiftUI-Hooks#rules-of-hooks)。\n\n```swift\n/// 1. 继承 `HookView` 接口\nstruct YourView: HookView {\n    /// 2. 实现 `hookBody`\n    var hookBody: some View {\n        /// 3. 使用 Hooks API，订阅状态\n        let names = useRecoilValue(namesState)\n        let filteredNames = useRecoilValue(filteredNamesState)\n\n        return VStack {\n            Text(\"Original names: \\(names.joined(separator: \",\"))\")\n            Text(\"Filtered names: \\(filteredNames.wrappedValue.joined(separator: \",\"))\")\n\n            Button(\"Reset to original\") {\n                filteredNames.wrappedValue = names\n            }\n        }\n    }\n}\n```\n\n请注意，使用 Hooks API 的 View 须继承 `HookView` 接口，并实现 `hookBody` 属性。或者用 `HookScope` 包裹住你的Hooks API代码的。你可以使用 `useRecoilValue` 等一系列`Hook API`来订阅状态，并根据需要更新状态。\n\n**请查看 [这里](./Docs/Hooks.md)**\n\n\n## Demo\n\n以下示例非常简单，但强烈建议查看对应的代码。类似 Redux，Recoil 面向状态编程，使页面间的状态共享和重用变得十分容易。并且状态逻辑都是纯函数，测试也非常简单。\n\n![UIKIt](./Docs/Images/UIKitExample.gif)\n![demo](./Docs/Images/Example.gif)\n\n\n## 资料:\n\n* Facebook Recoil (Recoil.js) \n  * [Recoil website](https://recoiljs.org/)\n  * [Official facebook recoil repo](https://github.com/facebookexperimental/Recoil)\n  \n* Recoil for Android\n  * [Rekoil](https://github.com/musotec/rekoil)\n\n* Hooks\n  * [React Hooks](https://reactjs.org/docs/hooks-intro.html)\n  * [SwiftUI Hooks](https://github.com/ra1028/SwiftUI-Hooks)\n\n## 贡献\n\n欢迎你对 RecoilSwift 做出贡献。你可以通过提交 issue 或者 pull request 来帮助我们改进 RecoilSwift。\n\n最后，如果你喜欢我们的项目，别忘了给我们一个 star ⭐，这是对我们工作的最大鼓励。\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhollyoops%2Frecoilswift","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fhollyoops%2Frecoilswift","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhollyoops%2Frecoilswift/lists"}