12. Jun 2023iOS

Exploring 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.

Marek VricaniOS Developer

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!

Don't forget to check out the rest of our iOS toolbox

iOS5 Mins reading

Exploring our iOS toolbox - How to save values to UserDefaults & KeyChain in Swift

Marek Vrican20 Apr 2023
Marek VricaniOS Developer