Last updated 2 min read

Leveraging Composable Architecture at Scale by Krzysztof Zabłocki. Swift Heroes 2023 Talk

Related: Krzysztof Zablocki, iOS App Development, Software Engineering


Testability

TCA has been pushing Exhaustive testing from the very beginning. Exhaustivity makes a lot of sense in the development of isolated libraries.

However, that's not always the case. It often leads to test duplication and their fragility which makes them hard to maintain.

It's also overwhelming. Devs tend to copy broken tests' "expectations" without figuring out what really happened.

What we want is to have a test that is:

  • Small and easy to understand
  • Verifies only one piece of functionality at a time
  • Doesn't break due to internal implementation changes
  • Verifies only public interface/behavior

This allows devs to be confident when changing implementation details.

We would prefer to have 10 tests with a single assert each other than 1 test with 10 asserts. This way we can easily reason about what exactly got broken.

Maintainability of Codebase

Boundaries. Actions implemented as enum does not provide enough isolation of details of features implementation.

We can categorize them and cover them with a protocol:


protocol TCAFeatureAction {
	associatedtype ViewAction
	associatedtype DelegateAction
	associatedtype LocalAction

	static func view(_: ViewAction) -> Self
	static func delegate(_: DelegateAction) -> Self
	static func local(_: LocalAction) -> Self

}

Then implement actions like this:


public enum MyFeatureAction: TCAFeatureAction {

	enum ViewAction: Equatable {
	    case disappear 
	    case dismissed
	}

	enum LocalAction: Equatable {
		case listResult(Result<[Todo], TodoError>)
	}

	enum DelegateAction: Equatable {
		case userLoggedin(User)
	}
	
	case view (ViewAction)
	case local(LocalAction)
	case delegate(DelegateAction)
}

This categorization allows us to write our own linter rules to ensure certain actions are not used where they shouldn't.

Performance

If the app has too many actions sent to the system TCA does not perform well.

The whole set of optimizations that comes to mind:

  • Stores updates deduplication
  • Multiples stores instead of a single one (also good from an arhitecture decomposition standpoint)
  • Throttle/Debounce on actions producing and state consumption levels

What didn't work out:

  • Scoping Stores deduplication with explicitly defined paths of the state. It's kinda wastes the whole UDF idea
  • Caching/memoization turned out to be too much fragile. Cache missing, etc.

Tooling

The biggest promise of UDF/MVI architectures like TCA is to make it clear what's happening in the application. In large applications built with TCA, it doesn't happen by itself. But the tooling may help.

One of the big benefits of TCA as a consistent architecture is the ability to build tooling around it.

  • Performance dashboard with all actions and state processing measurements
  • State tree visualization
  • Dashboard with actions logging and filtering
  • Object lifetime tracker to spot memory leaks

References

Krzysztof Zabłocki - Leveraging Composable Architecture at Scale | Swift Heroes 2023 Talk - YouTube