Engineering

29 March, 2021

An example of SwiftUI + The Composable Architecture

An example case on our to structure your SwiftUI projects using The Composable Architecture.

Daniel Almeida

Software Engineer

An example of SwiftUI + The Composable Architecture - Coletiv Blog

Context

Our journey began when SwiftUI was launched. After years of UIKit, finally, Apple's official Declarative UI framework for iOS was out. Fortunately, at Coletiv one of our clients wanted to do a proof of concept for an amazing business concept in an app and they wanted the project to be built using SwitfUI 🎉

We've tried to find the best way to accommodate all these new concepts in our code. @State, @Bindings, @ObservedObject, all these new @'s became our first concern when dealing with the View state changes and where, in the code, we should perform it. Some of the problems are very well described in these videos.

Finally after some time investigating, André showed me these videos provided by pointfree.co. They describe how their proposal for a SwiftUI architecture is. Their explanations are very practical and clear. Their solutions are always iterated until the final one is achieved.

The Composable Architecture (TCA) was adopted in the mentioned projects and it was a charm to work with, although we had some struggles to really integrate this new approach. Their example project is where you can check lots of use cases that can be used as an inspiration to build your products. Check them out and, if you feel like it, contribute 🙌.

Since then we decided to do a small example project to serve as a base on how we tackle small/medium SwiftUI projects using TCA at Coletiv. Next, we will try to sum up the structure and assumptions from our example project. Please note that the goal of this article is not to explain the TCA, we think the author's explanation is better than ours 😅

Pokemon TGC 👾

Pokemon TCG - Cards Screen Pokemon TCG - Cards Screen

Our example project is an app to show Pokemon Cards. We are using the V1 for Pokemon TGC API to retrieve the Pokemon Cards info. In the Cards tab we are retrieving the information from the server and showing it in an infinite scroll list.

The API documentation was fully open source, but now requires a login to access it. The new V2 version also requires an API Key to be generated. Either way it's free for now.

Pokemon TCG - Card Detail Screen Pokemon TCG - Card Detail Screen

Also, we have a detail screen where the card image is being shown with a larger size. Here the user can toggle it as favorite.

Finally, the Favorites tab is showing up the favorite cards list with the same aspect as the Cards tab.

Xcode Project 🛠

We always try to do a native approach as much as possible, avoiding unnecessary dependencies to just do small stuff. Of course we are not trying to reinvent the wheel but we like to not rely too much on something that can be deprecated too quickly, usually caused by lack of support.

Another key point is simplification, we always try to simplify our solutions. Begin with a simple basis and evolve from there, if we are not building a spaceship then why do we need the rocket boosters? 🚀

Dependencies 🕸

We use the Swift Package Manager (SPM) to manage the dependencies, most of the widely used deps are now also available using the SPM, so we thought that there's no reason to not use the vanilla dependency manager for Swift. We should pay our respects 🤲🏻 to CocoaPods and Carthage as they played a crucial role in the deps problem for years until the SPM came out. 🚀 Check the example README for more details.

Folder Structure 📁

In this section let's resume a bit what's inside the main folders:

Folder Structure Overview Folder Structure Overview

Data 💿

Inside the data folder you will find everything related with the data itself, models, web service clients, CoreData and other services you may need to handle the data models.

Client Folder Client Folder

The CardsClient and FavoriteCardsClient clients should be the only way the TCA Reducers access and change data. Those clients follow a pattern well described in this video where there's always a live and a mock version of the client. This improves a lot on testability allowing our code to be fully "mockable". This will remove the tests dependency from external data providers (ex. API calls).

The Provider holds all the code to interact with the services that provide the data. This includes request/response serialisation, URLRequest, CoreData queries, etc...

Design System 👨‍🎨

This is where all the design components are. We usually add here typography, buttons, table cells, fields, modals, etc. Usually, this is the first place to have some UI code. Breaking the UI in reusable components helps on code clarity. Main views will have just the necessary code to glue all these components, creating complex UI's with simple code.

Application 📱

The Application folder contains the app screen main views, in our Pokemon example it includes the cards list (CardsView), the favorite cards list (FavoritesView) and the card detail screen (CardDetailView). All the main views have its own "Core" file, this file is where the TCA elements are. In almost every example project on the TCA repository the View and the Core are in the same file. We think this separation improves code readability but there's a discussion here about the Core entities separation and the creation of a generator for those.

Digging up into some code, there are some highlights I would like to share:

Main Reducer

// Main Reducer let mainReducer: Reducer<MainState, MainAction, MainEnvironment> = .combine( cardsReducer.pullback( state: \MainState.cardsState, action: /MainAction.cards, environment: { environment in CardsEnvironment( cardsClient: environment.cardsClient, favoriteCardsClient: environment.favoriteCardsClient, mainQueue: environment.mainQueue, uuid: environment.uuid ) } ), favoritesReducer.pullback( state: \MainState.favoritesState, action: /MainAction.favorites, environment: { environment in FavoritesEnvironment( favoriteCardsClient: environment.favoriteCardsClient, mainQueue: environment.mainQueue, uuid: environment.uuid ) } ), .init { state, action, environment in switch action { // Update favorites on Cards State case .cards(.card(id: _, action: .toggleFavoriteResponse(.success(let favorites)))): state.favoritesState.cards = .init( favorites.map { CardDetailState( id: environment.uuid(), card: $0 ) } ) return .none case .cards: return .none // Update favorites on Favorites State case .favorites(.card(id: _, action: .toggleFavoriteResponse(.success(let favorites)))): state.cardsState.favorites = favorites return .none case .favorites: return .none case .selectedTabChange(let selectedTab): state.selectedTab = selectedTab return .none } } )

Note that the mainReducer combines the cardsReducer and the favoritesReducer . This allows the mainReducer to handle all the actions that are also handled by the cardsReducer and favoritesReducer. This is useful, for example, to update the main state so that the favorites are the same across all the screens when we perform an add/remove favorite action.

Cancel Effects

Cancelling the Effects is very important, specially for network related tasks.

Here's a quick example from the favoritesReducer:

// Favorites Reducer switch action { case .onAppear: guard state.cards.isEmpty else { return .none } return .init(value: .retrieveFavorites) case .retrieveFavorites: return environment.favoriteCardsClient .all() .receive(on: environment.mainQueue) .catchToEffect() .map(FavoritesAction.favoritesResponse) .cancellable(id: FavoritesCancelId()) case .favoritesResponse(.success(let favorites)): state.cards = .init( favorites.map { CardDetailState( id: environment.uuid(), card: $0 ) } ) return .none case .card(id: _, action: .onDisappear): return .init(value: .retrieveFavorites) case .card(id: _, action: _): return .none case .onDisappear: return .cancel(id: FavoritesCancelId()) } // Favorites view struct FavoritesView: View { var store: Store<FavoritesState, FavoritesAction> var body: some View { WithViewStore(store) { viewStore in NavigationView { ScrollView { itemsList(viewStore) .padding() } .edgesIgnoringSafeArea(.bottom) .navigationBarTitle(Localization.Cards.title) } .onAppear { viewStore.send(.onAppear) } .onDisappear { viewStore.send(.onDisappear) } } } }

The first time the view appears it will trigger the .retrieveFavorites action. This will trigger the fetch request for the favorite cards that will be later returned to the .favoritesResponse action. Notice that a cancel id is being assigned to this Effect. If the user dismisses this screen/view this id will allow any other action to cancel an in progress effect that does not need to be running anymore.

Testing ✅

One of the reasons we adopted TCA is testability. TCA definitely changed the way we approach testing.

In our PokemonCards example we have some tests to ensure the reducers business logic is well implemented.

We also want to highlight the SnapshotTesting package that also fits in the TCA. You can check some simple examples in PokemonCardsSnapshotTests.swift file.

Conclusion

We think SwiftUI is one of the biggest changes in the latest years and it will definitely improve the code quality and overall development speed in the next years. This is just the beginning, new architectures will emerge and we should always keep an eye on updates and support these kind of community projects.

In the end it's a relief to have the technology to automatically do our manual testing, helping the developers to build better products.

Next Steps 🔮

  • Update the example to the V2 of the Pokémon TCG API
  • We are also hoping to update this example with more complex use cases
  • We definitely want to test this in huge projects, applying for example this awesome modular architecture

Source Code

https://github.com/coletiv/coletiv-ios-swiftui-tca-example

SwitfUI

iOS

Architecture

Join our newsletter

Be part of our community and stay up to date with the latest blog posts.

Subscribe

Join our newsletter

Be part of our community and stay up to date with the latest blog posts.

Subscribe

You might also like...

Go back to blogNext
How to support a list of uploads as input with Absinthe GraphQL

Engineering

26 July, 2022

How to support a list of uploads as input with Absinthe GraphQL

As you might guess, in our day-to-day, we write GraphQL queries and mutations for Phoenix applications using Absinthe to be able to create, read, update and delete records.

Nuno Marinho

Software Engineer

Flutter Navigator 2.0 Made Easy with Auto Router - Coletiv Blog

Engineering

04 January, 2022

Flutter Navigator 2.0 Made Easy with Auto Router

If you are a Flutter developer you might have heard about or even tried the “new” way of navigating with Navigator 2.0, which might be one of the most controversial APIs I have seen.

António Valente

Software Engineer

Enabling PostgreSQL cron jobs on AWS RDS - Coletiv Blog

Engineering

04 November, 2021

Enabling PostgreSQL cron jobs on AWS RDS

A database cron job is a process for scheduling a procedure or command on your database to automate repetitive tasks. By default, cron jobs are disabled on PostgreSQL instances. Here is how you can enable them on Amazon Web Services (AWS) RDS console.

Nuno Marinho

Software Engineer

Go back to blogNext