12. Jun 2023
iOSExploring our iOS toolbox - How we make reactive applications in Swift?
Reactive programming has gained significant popularity in recent years, and with the introduction of frameworks such as Combine it has become an essential tool for developing modern iOS applications. However, as applications become more complex, reactive programming can become challenging to manage. This is where the GoodReactor package comes in, providing a simple, yet powerful approach to managing reactive code.
How to make reactive applications in Swift
GoodReactor uses a protocol-based architecture to create a reactive component that can be easily reused throughout an application. Its protocol-based approach and integration with Combine make it an excellent tool for managing complex, reactive code.
By utilising GoodReactor, developers can write clean, maintainable, and testable code that is both efficient and easy to manage.
Create BaseViewController
Let’s say we want to build a simple counter application using GoodReactor.
Firstly we can start by defying our BaseViewController
. This Controller will serve as a base class for other viewControllers in our iOS app.
It takes a generic type T
, which is expected to be a viewModel that provides the data and business logic for the view. This means that any subclass of BaseViewController<T>
must pass in a view model when initialising the superclass.
The class also includes a cancellables
property, which is a set of AnyCancellable
objects. This property is used to keep track of any publishers or subscriptions that the viewController creates, so they can be cancelled when the viewController is deallocated.
class BaseViewController<T>: UIViewController {
let viewModel: T
var cancellables = Set<AnyCancellable>()
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
required init(viewModel: T) {
self.viewModel = viewModel
super.init(nibName: nil, bundle: nil)
}
}
Create ViewModel
Our CounterViewModel
is an example of how to implement the GoodReactor
protocol in Swift using the Combine framework.
import Combine
import GoodReactor
final class CounterViewModel: GoodReactor {
// State represents every state of data we want to use
struct State {
var counterValue: Int
}
// Action represents every user action
enum Action {
case increaseCounterValue
}
// Mutation represents state changes
enum Mutation {
case counterValueUpdated(Int)
}
internal let initialState: State
internal let coordinator: GoodCoordinator<AppStep>
init(coordinator: Coordinator<AppStep>) {
self.coordinator = coordinator
initialState = State(counterValue: 0)
}
}
The State
struct represents the current state of the view. The state contains a single property counterValue
which is initialised with initialState
to 0.
Action
enum represents user actions. In this case, we defined increaseCounterValue
action, which will be triggered when the user taps a button to increase the value of the counter.
Mutation
enum represents state changes that will be triggered in response to user actions. In this example, the only mutation is counterValueUpdated
, which will update the value of the counterValue
property in the state.
Finally, the class also defines an init
method which takes a Coordinator
parameter, which is used to initialise the coordinator
property. This property is used to manage navigation in the app, and is required by the GoodReactor
protocol. More info here.
Now continue with adding necessary methods within our CounterViewModel:
func navigate(action: Action) -> AppStep? {
return nil
}
func mutate(action: Action) -> AnyPublisher<Mutation, Never> {
switch action {
case .increaseCounterValue(let mode):
let increasedValue = currentState.counterValue + 1
return Just(.counterValueUpdated(increasedValue)).eraseToAnyPublisher()
default:
return Empty().eraseToAnyPublisher()
}
}
func reduce(state: State, mutation: Mutation) -> State {
var state = state
switch mutation {
case .counterValueUpdated(let newValue):
state.counterValue = newValue
}
return state
}
The navigate
method is used to handle navigation or screen transitions between different parts of the app. The navigate method is triggered whenever an action is received. The navigation is handled by a GoodCoordinator
object, which will be responsible for managing the flow between different parts of the app. The AppStep
type represents a specific step within the app's navigation hierarchy. More info about Coordinators here.
The mutate
method is called whenever an action is received. In this case, the method returns a counterValueUpdated
mutation with a new value for the counterValue
property.
The reduce
method is called whenever a mutation is committed. In this case, the method returns a new state with the updated counterValue
property.
Create ViewController
Now we define our CounterViewController
which will be responsible for displaying and interacting with the data provided by the CounterViewModel
instance.
import UIKit
import Combine
final class CounterViewController: BaseViewController<CounterViewModel> {
// Label showing actual counter value
private let counterValueLabel = UILabel()
// Button responsible for increasing the counter value
private let increasingButton = UIButton()
override func viewDidLoad() {
super.viewDidLoad()
setupLayout()
bindState(reactor: viewModel)
bindActions()
}
}
In the viewDidLoad
method, the setupLayout
method is called, which sets up the layout of the UI components in the view. After that, the **bindState
**and bindActions
methods are called, which are used to establish a two-way communication between the CounterViewModel
instance and the view controller.
Now add the following functions within the CounterViewController
:
// Sets up the layout of the UI components in the view
func setupLayout() {
[counterValueLabel, increasingButton].forEach { view.addSubview($0) }
NSLayoutConstraint.activate([
counterValueLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
counterValueLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor),
increasingButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
increasingButton.topAnchor.constraint(equalTo: counterValueLabel.bottomAnchor, constant: 16)
])
}
// binds viewModel state to UIComponents
func bindState(reactor: CounterViewModel) {
reactor.state
.map { String($0.counterValue) }
.removeDuplicates()
.assign(to: \\.text, on: counterValueLabel, ownership: .weak)
.store(in: &cancellables)
}
// bind user actions to the viewModel
func bindActions() {
increasingButton.addTarget(self, action: #selector(increasingButtonPressed(_ :)), for: .touchUpInside)
}
@objc func increasingButtonPressed(_ sender: UIButton) {
viewModel.send(event: .increaseCounterValue)
}
The bindState
method takes the viewModel
property of the viewController and binds it to the UI components. This means that whenever the state of the CounterViewModel
changes, the UI components will be updated accordingly.
The bindActions
method is used to bind user actions to the CounterViewModel
. This means that whenever the user interacts with the UI components, the corresponding action will be sent to the CounterViewModel
, which will then update its state accordingly.
Congrats!
You have gone through the process of making reactive app using GoodReactor package.
Now you may be wondering how it works under the hood inside the app. Luckily, the package comes with a sample that you can explore to gain a deeper understanding of its functionality.
If you found this package useful, be sure to check out our other packages. Who knows, you might just find another gem that can help take your app to the next level!