SwiftBloc 1.0.3

SwiftBloc 1.0.3

kronvel2014 维护。



SwiftBloc 1.0.3

  • 作者:
  • VictorKachalov

SwiftBloc

Version Platform

关于

受到一个非常出色的 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,则也需要创建一个子类。通用类型 EventState 可以是符合 Equitable 协议的任何类型。

您可以使用超构造函数提供初始状态。

CubitBloc 之间的区别,尽管 BlocCubit 的子类,但 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 文件。

赞助

如果您认为我的仓库帮助您解决了难题,请不要犹豫,请赞助!:-)