apache sca_具有sca逻辑的应用

apache sca

APP开发(APP DEVELOPMENT)

This is the second article of a series on how to write an app using SwiftUI and Swift Composable Architecture by https://pointfree.co.

这是关于如何通过https://pointfree.co使用SwiftUI和Swift Composable Architecture编写应用程序的系列文章的第二篇

The first episode can be found here.

第一集可以在这里找到

In last week's article, we explored how to write the UI of the Snake classic game. We used SwiftUI, the new framework from Apple. In the final code, we had the UI and a first version of the logic. The logic was intertwined in the body field of our ContentView: not a really good thing! When we write an app we would like to decouple the UI from the Logic.

在上周的文章中,我们探讨了如何编写Snake经典游戏的UI。 我们使用了Apple的新框架SwiftUI。 在最后的代码中,我们拥有UI和逻辑的第一个版本。 逻辑与ContentViewbody字段交织在一起:这不是一件好事! 在编写应用程序时,我们希望将UI与逻辑分离。

There are several solutions that can help here. At Bending Spoons we developed our own framework for that, called Katana. However, as I like to explore new technologies, I want to try a new framework I have been studying in the past months: Swift Composable Architecture (SCA) by https://pointfree.co.

有几种解决方案可以在这里提供帮助。 在Bending Spoons,我们为此开发了自己的框架,称为Katana 。 但是,当我喜欢探索新技术时,我想尝试一个过去几个月来我研究的新框架: https : //pointfree.co的Swift Composable Architecture (SCA)。

药丸中的SCA (SCA in Pills)

Swift Composable Architecture is another Redux-like framework for state management. It shares the same main concepts of this kind of solutions:

Swift Composable Architecture是另一个类似于Redux的状态管理框架。 它具有与此类解决方案相同的主要概念:

  • The state, which is a structure that holds all the data of the applications. The state is immutable and the only way to update it is to create a new state starting from the previous one.

    state ,是一种结构,其中包含应用程序的所有数据。 state是不可变的,更新state的唯一方法是从上一个状态创建一个新状态。

  • A set of actions, that are all and the only operations to update the state.

    一组action ,它们是更新状态的全部且唯一的操作。

  • One or more reducers, that are pure functions which, provided the state and an action, are able to create the new state in a deterministic way.

    一个或多个reducer是纯函数,它们提供状态和动作,并能够以确定性方式创建新状态。

All of this is glued together by a class called the Store. The Store is basically the entry point, the manager that orchestrates all the state updates and the data flows.

所有这些都通过一个称为Store的类粘合在一起。 Store基本上是入口点,是协调所有state更新和数据流的管理器。

It is the Store that keeps a reference to both the state and the reducer and it is the Store that, by executing the reducer, updates the state, and notifies all the interested entities that there is a new version of the state.

它是Store ,保持在两参考statereducer ,它是Store ,通过执行reducer ,更新state ,并通知所有有关实体存在的一个新版本的state

Under the hoods, SCA uses the new Combine framework to publish new updates on the state and to manage all the listeners.

在幕后,SCA使用新的Combine框架在状态上发布新更新并管理所有侦听器。

为什么SCA与众不同? (Why is SCA different?)

SCA is slightly different from many other frameworks I have the chance to look at. The main pillars of SCA are:

SCA与我有机会看到的许多其他框架略有不同。 SCA的主要Struts是:

  • composability

    可组合性
  • testability

    可测性
  • ergonomics

    人机工程学

Almost all its construct comes from the functional programming world. They are simple units on which the creators built many useful operations: map, flatMap, zip, pullback. All these operations allow the combination of the basic units of work in more complex functions: for example, we can use the map operation to change the return type of a function or we can use pullback to change the type of the parameters used by the function itself. This aspect pushes a lot toward the composition of small units and so enable the modularization of our apps.

它的几乎所有构造都来自功能编程世界。 它们是创建者在其上构建了许多有用操作的简单单元: mapflatMapzippullback 。 所有这些操作都允许将基本工作单元组合成更复杂的功能:例如,我们可以使用map操作更改函数的返回类型,也可以使用pullback更改函数使用的参数的类型本身。 这方面推动了小型单位的组成,因此实现了我们应用程序的模块化

Another subtle difference is that SCA makes almost no use of protocols. Everything is a value type, being it a struct or an enum. When there is a class, it is because other Apple’s framework requires it, like Combine. This makes writing code extremely easy, reducing the required boilerplate and enhancing testability: it’s brutally easy to swap a mocked implementation of some logic in place of the live one.

另一个细微的区别是SCA几乎不使用协议。 一切都是值类型,无论是struct还是enum 。 当有一个class ,是因为其他Apple框架也需要它,例如Combine 。 这使得编写代码极其容易,从而减少了所需的样板并增强了可测试性:以某种模拟的逻辑代替现场逻辑,实在是太残酷了。

With these premises in mind, let’s update our code with these new ideas.

考虑到这些前提,让我们用这些新想法更新代码。

安装Swift可组合架构 (Installing Swift Composable Architecture)

First thing first, to use SCA we need to install it. The Composable Architecture leverages the “new” Swift Package manager. Thanks to Apple, we now have it automatically integrated into Xcode. So We can just:

首先,要使用SCA,我们需要安装它。 可组合体系结构利用了“新的” Swift软件包管理器。 多亏了Apple,我们现在将其自动集成到Xcode中。 因此,我们可以:

  1. Click on File > Swift Packages > Add Package Dependency

    单击File > Swift Packages > Add Package Dependency

  2. Insert the URL https://github.com/pointfreeco/swift-composable-architecture

    插入URL https://github.com/pointfreeco/swift-composable-architecture

  3. Click on Next and wait for Xcode to download all the dependencies

    单击Next ,等待Xcode下载所有依赖项

  4. Click on Next again to use the latest available version

    再次单击Next以使用最新的可用版本

  5. Tick the Composable Architecture checkbox

    勾选可组合架构复选框
  6. Click on Finish

    点击Finish

In the Project navigator, we will see three new packages appear, below our project.

在项目导航器中,我们将在项目下方看到三个新的软件包。

Image for post

定义国家 (Defining the State)

Now, let’s move on. We need to create the State that the App will use.

现在,让我们继续前进。 我们需要建立的State ,该应用程序将使用。

Thanks to the aggressive use of value type we just need a struct that holds the Locations of the snake (head and body) and of the mouse. We already have something similar: we need to convert our old GameEngine into the AppState.

由于值类型的积极使用,我们只需要一个结构即可容纳蛇(头部和身体)和鼠标的Location 。 我们已经有了类似的东西:我们需要将旧的GameEngine转换为AppState

  1. rename the GameEngine into AppState

    GameEngine重命名为AppState

  2. change it from being a final class to be a struct

    将其从final class转变为struct

  3. Remove the @Published property wrappers and the conformance to ObservableObject

    删除@Published属性包装和对ObservableObject的一致性

  4. Remove the moveSnake method (we will have a reducer for that)

    删除moveSnake方法( moveSnake我们将有一个moveSnake器)

  5. Add the conformance to Equatable (SCA uses the protocol to avoid to publish twice the same state).

    Equatable添加一致性(SCA使用该协议以避免两次发布相同状态)。

That is the resulting code for the AppState:

那就是AppState的结果代码:

struct AppState: Equatable {
  var snake: Snake
  var mouse: Location


  init(snake: Snake) {
    self.snake = snake
    self.mouse = Self.randomLocation(with: snake)
  }


  static func randomLocation(with snake: Snake) -> Location {
    var randomRow: Int
    var randomCol: Int
    var newMouseLocation: Location
    repeat {
      randomRow = Int.random(in: 0..<11)
      randomCol = Int.random(in: 0..<11)
      newMouseLocation = Location(randomRow, randomCol)
    } while snake.allPositions.contains(newMouseLocation)


    return newMouseLocation
  }
}

定义动作(Defining the Actions)

Actions are what the user or the app can do in order to update the state. Having all the available actions listed allows us to immediately detect all and the only ways in which the state can be updated.

用户或应用可以执行操作来更新状态。 列出所有可用的操作,使我们能够立即检测到状态更新的所有唯一方法。

The right way to model them is through enum: this has the added benefit that, if we ever update the actions, the compiler will warn us to properly handle the new cases or the changed ones.

对其进行建模的正确方法是通过enum :这具有额外的好处,即如果我们更新了动作,编译器将警告我们正确处理新的情况或更改的情况。

The available actions for our game are very simple: we actually have a single move action which receives a Direction parameter to work with.

游戏中可用的动作非常简单:实际上,我们只有一个move动作,可以接收Direction参数来使用。

enum Direction: Equatable {
  case up, down, left, right


  func isOppositeOf(_ direction: Direction) -> Bool {
    switch (self, direction) {
    case (.up, .down), (.down, .up), (.left, .right), (.right, .left):
      return true
    default:
      return false
    }
  }
}


enum AppAction {
  case move(Direction)
}

编写减速器(Writing the Reducers)

Finally, we can now write the core of our game. The reducer encapsulates the logic of the app in the form of action handling.

最后,我们现在可以编写游戏的核心了。 reducer以动作处理的形式封装了应用程序的逻辑。

It is called reducer because it mimics the reduce(into:) function: starting from the state it reduces the action into it, creating a new state in the process.

这就是所谓的减速,因为它模仿了reduce(into:)功能:从起始state它降低了action进去,创建一个新的state在这个过程中。

In SCA, the reducer is a pure function with 3 parameters:

在SCA中,reducer是具有3个参数的纯函数:

  • the current AppState

    当前的AppState

  • an action

    一种行为
  • the environment.

    环境。

The environment is an object that contains all the logic to interact with the external world. We are not going to use the environment today, so we can safely set it to Void.

environment是一个包含与外部世界交互的所有逻辑的对象。 今天我们将不使用环境,因此可以安全地将其设置为Void

The reducer is also able to return some Effect: effects are pieces of logic that allows the app to interact with the external world, like interacting with some remote APIs or with the phone sensors. An effect can conclude its processing by issuing another action into the reducer. We are not going to use the Effects in today’s article, but they are an important concept for the Composable Architecture and we will explore them in a future article.

还原器还可以返回一些Effect :效果是允许应用与外界交互的逻辑部分,例如与某些远程API或电话传感器进行交互。 effect可以通过向减速器中发出另一个动作来结束其处理。 我们不会在今天的文章中使用Effects ,但是它们是可组合体系结构的重要概念,我们将在以后的文章中进行探讨。

The body of the reducer is basically a switch over the possible actions. So the final code has this shape:

减速器的主体基本上是对可能动作的switch 。 因此,最终代码具有以下形状:

extension Reducer where State == AppState, Action == AppAction, Environment == Void {
  static var snakeReducer = Reducer { (state: inout AppState, action: AppAction, _: Void) in
    switch action {
    case .move(let direction):
      let newHeadLocation = state.snake.newHeadLocation(in: direction)
      let hasEaten = state.mouse == newHeadLocation
      let newSnake = state.snake.moving(direction: direction, hasEaten: hasEaten)
      state.snake = newSnake
      if hasEaten {
        state.mouse = AppState.randomLocation(with: newSnake)
      }
      return .none
    }
  }
}

This code is exactly the moveSnake method implemented last week. The peculiarity here is how the snakeReducer is declared: to simplify its usage, we declared it as a static variable in the Reducer type. The variable is not polluting the whole Reducer namespace: we are allowed to use it only when the State is an AppState, the Action is an AppAction and the Envoironment is Void. This is possible thanks to the conditional clause where.

这段代码正是上周实现的moveSnake方法。 这是snakeReducer声明方式的snakeReducer处:为了简化其用法,我们在Reducer类型中将其声明为静态变量。 该变量不会污染整个Reducer命名空间:仅当StateAppStateActionAppActionEnvoironmentVoid时,才允许使用该变量。 这要归功于条件子句where

创建商店 (Creating the Store)

Finally, we can create the Store, the manager of all the State, the Action, and the Reducer. The final piece of code that will make all this machinery work.

最后,我们可以创建Store ,所有State的管理者, ActionReducer 。 使所有这些机器正常工作的最后一段代码。

We will leverage the Store generic class that is defined in the Composable Architecture package. Let’s open the SnakeApp.swift file and, let’s add the following snippet into the SnakeApp struct:

我们将利用Composable Architecture包中定义的Store通用类。 我们打开SnakeApp.swift文件,然后将以下代码段添加到SnakeApp结构中:

let store: Store<AppState, AppAction> = .init(
  initialState: AppState(
    snake: Snake(headPosition: Location(5, 5), of: 5)
  ),
  reducer: Reducer.snakeReducer,
  environment: ()
)

Notice how, to initialize the Store, we need to provide the initial state of our app and the reducer the app will use.

请注意,要初始化Store ,我们需要提供应用程序的初始状态以及应用程序将使用的reducer。

放在一起 (Putting Everything Together)

Now the funny part: let’s put everything together and see if this works. Spoiler alert, it does!

现在有趣的部分:让我们将所有内容放在一起,看看是否可行。 扰流板警报,确实如此!

As the first step, let’s go to the ContentView.swift and replace the @ObservedObject var gameState: GameEngine variable for a@ObservedObject var viewStore: ViewStore<AppState, AppAction> one.

第一步,我们转到ContentView.swift并将@ObservedObject var gameState: GameEngine变量替换为@ObservedObject var viewStore: ViewStore<AppState, AppAction>

A ViewStore is basically a specialized Store that allows the views to operate on a subset of actions and on a slice of the state. Think of it as an implementation of the VM in the classic MVVM pattern. This is the main abstraction for which is so easy to develop an app in modules with SCA.

ViewStore基本上是一个专门的Store ,它允许视图在操作的子集和状态的一部分上进行操作。 将其视为经典MVVM模式中VM的实现。 这是主要的抽象,因此使用SCA在模块中开发应用程序非常容易。

Of course, now, nothing is compiling: we need to replace all the self.gameEngine references with the new self.viewStore ones. So… Let’s do it.

当然,现在什么都没有编译:我们需要用新的self.viewStore替换所有self.gameEngine引用。 所以……做吧。

Now, in the button function, we do not have a moveSnake anymore. That function has been replaced by the reducer. Therefore, we can send an action to the viewStore and let the reducer update the state for us. This can look like a small change, but it makes a world of difference! Instead of actually performing the change, we are declaring what or how we want the state to change. It is a complete paradigm shift, from the imperative world to the declarative one!

现在,在button功能中,我们不再有moveSnake了。 该功能已由减速器代替。 因此,我们可以send一个动作sendviewStore并让reducer为我们更新状态。 这看似很小的变化,但却带来了与众不同的世界! 而不是实际执行更改,我们声明要更改状态的方式或方式。 从命令式世界到声明世界,这是一个完整的范式转换!

Here is the final code for our view:

这是我们视图的最终代码:

struct ContentView: View {
  @ObservedObject var viewStore: ViewStore<AppState, AppAction>


  var body: some View {
    VStack {
      self.gameField()
      Spacer()
      self.commands()
    }
  }
}


extension ContentView {


  func cell(for location: Location) -> some View {
    let text = self.viewStore.mouse == location ? "🐭" :
      self.viewStore.snake.headPosition == location ? "🟢" :
      self.viewStore.snake.bodyPositions.contains(location)
      ? "⚫️"
      : " "


    return Text(text)
      .font(Font(UIFont(name: "Courier", size: 35)!))
      .frame(width: 35, height: 35, alignment: .center)
  }


  func column(for row: Int) -> some View {
    return HStack(alignment: .center, spacing: 0) {
      ForEach(0..<11) { col in
        let testLocation = Location(row, col)
        cell(for: testLocation)
      }
    }
  }


  func gameField() -> some View {
    return VStack(alignment: .center, spacing: 0) {
      ForEach(0..<11) { row in
        return column(for: row)
      }
    }
    .padding(5)
    .border(Color.black, width: 2)
  }


  func button(with text: String, for direction: Direction) -> some View {
    return Button(action: {
      self.viewStore.send(.move(direction))
    }) {
      Text(text)
        .font(Font(UIFont(name: "Courier", size: 60)!))
    }
  }


  func commands() -> some View {
    return VStack {
      self.button(with: "⬆️", for: .up)
      HStack {
        self.button(with: "⬅️", for: .left)
        self.button(with: "⬇️", for: .down)
        self.button(with: "➡️", for: .right)
      }.padding(10)
    }
  }
}

Almost there! The last small step is to update all the initializers for our content view. We only have two of them to update: one is in the very same ContentView.swift file and it is the ContentView’s preview; the second one is in the SnakeApp.swift file.

差不多好了! 最后的小步骤是更新内容视图的所有初始化程序。 我们只有两个要更新:一个在同一个ContentView.swift文件中,它是ContentView的预览; 第二个在SnakeApp.swift文件中。

import SwiftUI
import ComposableArchitecture


@main
struct SnakeApp: App {


  let store: Store<AppState, AppAction> = .init(
    initialState: AppState(
      snake: Snake(headPosition: Location(5, 5), of: 5)
    ),
    reducer: Reducer.snakeReducer,
    environment: ()
  )
  var body: some Scene {


    WindowGroup {
      ContentView(viewStore: ViewStore(store))
    }
  }
}

We just have to create the ViewStore by passing the original Store, and SCA will do the rest.

我们只需要通过传递原始Store来创建ViewStore ,其余的工作将由SCA完成。

If we now click on Play the app should start and it should behave exactly like last week.

如果现在单击“ Play该应用程序应启动,并且其行为应与上周完全相同。

结论 (Conclusions)

In today's article, we explored the main concepts of the Swift Composable Architecture and we converted the logic of our small Snake app to those concepts. This allowed us:

在今天的文章中,我们探讨了Swift Composable Architecture的主要概念,并将小型Snake应用程序的逻辑转换为这些概念。 这使我们能够:

  • to decouple the logic from the models

    使逻辑与模型脱钩
  • to isolate the logic in its own reducer

    在自己的减速器中隔离逻辑
  • to decouple the UI from the logic.

    使UI与逻辑脱钩。

The UI, in fact only needs to know that the viewStore accepts some actions of a given type but it has not to know the whole logic. This shifts the paradigm from imperative programming to a declarative approach.

实际上,UI只需要知道viewStore可以接受给定类型的某些actions ,而不必知道整个逻辑。 这将范式从命令式编程转换为声明式方法。

With this architecture is fairly easy to swap different logics (by changing the reducers) and also to scope what is available to each view, enabling modularization in a simple and effective way.

使用这种架构,可以很容易地交换不同的逻辑(通过更改化简器),还可以确定每个视图可用的范围,从而以简单有效的方式实现模块化。

This is just the tip of the iceberg of the SCA architecture. There is a huge world related to Effects and testing that we will explore in the next weeks.

这只是SCA体系结构的冰山一角。 在接下来的几周中,我们将探索与Effects和测试相关的巨大世界。

翻译自: https://medium.com/swlh/an-app-with-sca-logic-e4ec5458bdc4

apache sca

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值