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和逻辑的第一个版本。 逻辑与ContentView
的body
字段交织在一起:这不是一件好事! 在编写应用程序时,我们希望将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. Thestate
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
action
s, that are all and the only operations to update the state.一组
action
,它们是更新状态的全部且唯一的操作。One or more
reducer
s, 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
,保持在两参考state
和reducer
,它是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.
它的几乎所有构造都来自功能编程世界。 它们是创建者在其上构建了许多有用操作的简单单元: map
, flatMap
, zip
, pullback
。 所有这些操作都允许将基本工作单元组合成更复杂的功能:例如,我们可以使用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中。 因此,我们可以:
Click on
File
>Swift Packages
>Add Package Dependency
单击
File
>Swift Packages
>Add Package Dependency
Insert the URL
https://github.com/pointfreeco/swift-composable-architecture
插入URL
https://github.com/pointfreeco/swift-composable-architecture
Click on
Next
and wait for Xcode to download all the dependencies单击
Next
,等待Xcode下载所有依赖项Click on
Next
again to use the latest available version再次单击
Next
以使用最新的可用版本- Tick the Composable Architecture checkbox勾选可组合架构复选框
Click on
Finish
点击
Finish
In the Project navigator, we will see three new packages appear, below our project.
在项目导航器中,我们将在项目下方看到三个新的软件包。
定义国家 (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 Location
s 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
。
rename the
GameEngine
intoAppState
将
GameEngine
重命名为AppState
change it from being a
final class
to be astruct
将其从
final class
转变为struct
Remove the
@Published
property wrappers and the conformance toObservableObject
删除
@Published
属性包装和对ObservableObject
的一致性Remove the
moveSnake
method (we will have a reducer for that)删除
moveSnake
方法(moveSnake
我们将有一个moveSnake
器)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
命名空间:仅当State
为AppState
, Action
为AppAction
且Envoironment
为Void
时,才允许使用该变量。 这要归功于条件子句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
的管理者, Action
和Reducer
。 使所有这些机器正常工作的最后一段代码。
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
一个动作send
到viewStore
并让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