SwiftBloc
关于
受到一个非常出色的 Flutter 包 flutter_bloc 的启发,这个 SwiftUI 库通过 BloC (业务逻辑组件) 方法将状态管理引入分隔视图和业务逻辑。借助 Apple 的库 "Combine",状态管理采用响应式方法处理。
需要 iOS 13.0+ 和 MacOS 10.15+
开始
首先,您需要决定哪种方法更适合您的应用程序。
如果您更喜欢制作简单的内容并独立于当前事件进行更改,则可以使用类的 Cubit 创建子 cubit 并在那里处理状态。
如果您需要更复杂的实现来跟踪事件并将它们映射到状态,那么 Bloc 类是您的选择。
在两种情况下,您可能还需要在自定义的 View 结构中创建一个 BlocView 实例,该实例将在初始化器中接受您新建的 cubit/bloc,还要求提供一个作为参数提供的 @ViewBuilder 构建函数。的理念 BlocView 是根据当前状态在 builder 回调中处理重建视图。每当状态发生变化时,视图都会重新构建。
您可以随意操作状态。此外,您还可以向 BlocView 构造函数提供一个自定义的 action 回调,这使您能够添加一些副作用逻辑或视图行为而不是返回任何符合 View 的对象。
默认情况下,所有更改都会由一个共享的 BlocObserver 实例跟踪,并且目前仅进行控制台打印。您始终可以为共享实例设置一个自定义观察器,并按需覆盖开放方法。
为了方便,您可以使用 ruby 脚本 bloc_template.rb。您需要提供额外参数来执行此脚本。
- 路径
- 类名
- 类型
示例
# creates a cubit
ruby bloc_template /MY_PROJECT/MY_CUBIT_FOLDER Counter cubit
# creates a bloc
ruby bloc_template /MY_PROJECT/MY_BLOC_FOLDER Counter bloc
Cubit
如果您首先选择 Cubit,则需要创建一个子类。通用类型 State 可以是符合 Equitable 协议的任何类型。
您可以使用超构造函数提供初始状态。
要发出新的状态,您需要使用 emit(state:) 方法
作为简单示例,CounterCubit(Flutter 新项目的标准应用之一)
import SwiftBloc
class CounterCubit: Cubit<Int> {
init() {
super.init(state: 0)
}
func increment() {
emit(state: state + 1)
}
func decrement() {
emit(state: state - 1)
}
}
接下来,您需要在使用您的视图。
由于我们的 State 通用状态是 Int,我们可以直接访问整数属性 state 并在文本中显示。
接下来,在您的 body 属性中创建一个包含一些内容的 BlocView
var body: some View {
BlocView(builder: { (cubit) in
VStack {
Button(action: {
cubit.increment()
}, label: {
Text("Increment")
})
Button(action: {
cubit.decrement()
}, label: {
Text("Decrement")
})
Text("Count: \(cubit.state)")
}
}, cubit: CounterCubit())
}
在这种情况下,您可以直接从 builder 中使用 cubit。此外,这种方法将允许您将您的 cubit 实例用作 @EnvironmentObject,这样就无需在整个视图树中“钻探”即可在每个 builder 函数内部的儿童 view 中获取 cubit 的实例。
Bloc
如果您首先选择 Bloc,则也需要创建一个子类。通用类型 Event 和 State 可以是符合 Equitable 协议的任何类型。
您可以使用超构造函数提供初始状态。
Cubit 和 Bloc 之间的区别,尽管 Bloc 是 Cubit 的子类,但 Bloc 使用事件将它们映射到状态。
以简单示例 CounterBloc
import SwiftBloc
enum CounterEvent {
case increment
case decrement
}
struct CounterState: Equatable {
let count: Int
func copyWith(count: Int?) -> CounterState {
CounterState(count: count ?? self.count)
}
}
class CounterBloc: Bloc<CounterEvent, CounterState> {
init() {
super.init(initialState: CounterState(count: 0))
}
override func mapEventToState(event: CounterEvent) -> CounterState {
switch event {
case .increment:
return state.copyWith(count: state.count + 1)
case .decrement:
return state.copyWith(count: state.count - 1)
}
}
}
想法是,应用中发生的所有事情都是事件。基于此,您可以根据需要调用您的事件,例如在此示例中使用枚举(但也可以使用类,但请注意,要符合 Equatable 协议)。
如果存在事件,则应该有某些东西可以告诉适当事件之后的应用状态。
您需要指定您的状态模型以实现此目的。您还可以使用具有继承的类,特别是在您有一些复杂状态的情况下(并记住 Equatable)。
现在是时候实现子 Bloc 类了。
您需要有一个构造函数,在该构造函数中您指定初始 State 值。
这里是重要的部分 - 重写 mapEventToState(event:) 方法。没有它,什么都不会工作...
此方法的目标是将事件转换为状态。
根据传入的事件,生成状态。您可以使用 CounterState 创建新的状态对象,或者创建一个方法来复制当前状态并提供对其的更改。
然后,新状态将被发送到您的 BlocView,并且您的视图将自动重建!
现在让我们看看视图的样式
import SwiftBloc
struct BlocContentView: View {
var body: some View {
NavigationView {
BlocView(builder: { (bloc) in
let isPresented = Binding.constant(bloc.state.count < -6)
CounterView()
.alert(isPresented: isPresented) {
Alert(
title: Text("Hi"),
message: Text("Message"),
dismissButton: .cancel {
for _ in 0..<6 {
bloc.add(event: .increment)
}
}
)
}
}, action: { (bloc) in
print(bloc.state.count)
}, base: CounterBloc())
.navigationBarTitle(Text("Bloc"), displayMode: .inline)
}
}
}
struct CounterView: View {
@EnvironmentObject var bloc: CounterBloc
var body: some View {
if bloc.state.count > 5 {
LimitView()
} else {
OperationView()
}
}
}
struct LimitView: View {
@EnvironmentObject var bloc: CounterBloc
var body: some View {
VStack {
Text("Hooora")
Button(action: {
for _ in 0..<6 {
bloc.add(event: .decrement)
}
}, label: {
Text("Reset")
})
}
}
}
struct OperationView: View {
@EnvironmentObject var bloc: CounterBloc
var body: some View {
VStack {
Button(action: {
bloc.add(event: .increment)
}, label: {
Text("Send Increment event")
})
Button(action: {
bloc.add(event: .decrement)
}, label: {
Text("Send Decrement event")
})
Text("Count: \(bloc.state.count)")
}
}
}
Bloc 类通过 @PublishedSubject 属性包装器监控 event 属性的变化。默认情况下,该 bloc 实例被设置为 @EnvironmentObject,并在需要时对所有子视图可用。
BlocTest
您可能想测试您的 Blocs,以确保适当的传入事件确实会发生预期的状态。
BlocTest.execute 期望有四个回调
- build(在闭包内创建一个 Bloc 实例)
- act(提供一个事件序列)
- expect(设置期待的状态)
- verify(处理预期状态和实际状态相等的消息)
final class SwiftBlocTests: XCTestCase {
func testCounterBlocInitial() {
BlocTest.execute(build: {
MockCounterBloc()
}, act: { (_) in
// DO NOTHING
}, expect: {
[
MockCounterState(count: 0)
]
}, verify: { areEqual, message in
XCTAssert(areEqual, message)
})
}
func testCounterBlocIncrement() {
BlocTest.execute(build: {
MockCounterBloc()
}, act: { (bloc) in
bloc.add(event: .increment)
bloc.add(event: .increment)
}, expect: {
[
MockCounterState(count: 0),
MockCounterState(count: 1),
MockCounterState(count: 2)
]
}, verify: { areEqual, message in
XCTAssert(areEqual, message)
})
}
func testCounterBlocDecrement() {
BlocTest.execute(build: {
MockCounterBloc()
}, act: { (bloc) in
bloc.add(event: .decrement)
bloc.add(event: .decrement)
}, wait: 3.0, expect: {
[
MockCounterState(count: 0),
MockCounterState(count: -1),
MockCounterState(count: -2)
]
}, verify: { areEqual, message in
XCTAssert(areEqual, message)
})
}
}
示例
要运行示例项目,请克隆存储库,然后从 Example 目录运行 pod install
。
要求
该库支持 iOS 13 及以上版本。
安装
Cocoapods
HealthKitReporter 通过 CocoaPods 提供。要安装它,只需在您的 Podfile 中添加以下行
pod 'SwiftBloc'
或者
pod 'SwiftBloc', '~> 1.0.3'
Swift Package Manager
要安装它,只需将以下行添加到您的 Package.swift 文件中(或直接使用 XCode 中的包管理器并参考此仓库)
dependencies: [
.package(url: "https://github.com/VictorKachalov/SwiftBloc.git", from: "1.0.3")
]
Carthage
在您的 Cartfile 中添加此行
github "VictorKachalov/SwiftBloc" "1.0.3"
作者
Victor Kachalov,[email protected]
许可
SwiftBloc 在 MIT 许可下提供。有关更多信息,请参阅 LICENSE 文件。
赞助
如果您认为我的仓库帮助您解决了难题,请不要犹豫,请赞助!:-)