Modular Apps with a Tuist — Part 3 — Backpack UI

Ronan O Ciosoig
12 min readJun 21, 2021

--

Connecting modules

A step-by-step code walkthrough converting an existing app to a modularised app using tuist.

In the previous article I started to dig into the modularisation process by separating the network layer into a separate project. In this article I will go through the steps of refactoring the UI layer into separate projects — effectively hyper-modularisation with (almost) each screen in a separate module. In the process of doing this I also have to refactor the core of the project to create a shared framework to make it work with the help of a scaffold template. I cover all the mistakes, crashes and errors that I encountered along the way, and provide a solution with a link to the commit in each case. It’s a long one this time, but bare with me. The following and final article is far shorter as it builds upon the topics presented here.

Note that to fully understand this article I recommend reading the previous 2 articles in this series, looking at the repo, and specifically at the commits on this branch that are linked in the text.

What’s In A Name?

As mentioned in the last article there is a different approach to modularisation known as micro-features that is recommended, and I will follow up this exercise with this alternative. But for now, let’s keep going.

The process of making another project follows what was done in the last article for Network, namely:

  • Make a new folder: Backpack in this case.
  • Init tuist in that folder.
  • Edit the project file and project template and copy & paste from the main Pokedex project and template swift file.
  • Delete the BackpackKit target folder.

There were a few errors on running tuist generate. The main issues were copying over the project template and specifying the resources when none exist. The glob pattern used to refine the resource paths needs to find at least one file matching the pattern for it to compile without emitting warnings.

The UI has a dependency on Haneke (image loading and cache) so this must be included in the project dependencies.

On running the generate command I ran into this error that had me stumped for quite a while:

ronan.ociosig@BCN-MAC Backpack % tuist generate
/Library/Caches/com.apple.xbs/Binaries/SwiftPrebuiltSDKModules_macOS/install/TempContent/Objects/EmbeddedProjects/CrossTrain_macOS_SDK/macOS_SDK/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/lib/swift/Swift.swiftmodule/x86_64.swiftinterface:11105: Fatal error: Duplicate values for key: ‘BackpackUITests’

The issue here is the conflict of naming between the Backpack example target which has a UITest folder, and a BackpackUITests source file clashing with the BackpackUI target which contains a Tests folder which in turn contains a BackpackUITests source file.

The origin of the problem is that I added a UITests target to the main app in the project template and this creates the name clash when the target for the BackPackUI framework is generated because this has a Tests target. The solution is to change the name slightly, like add an underscore, but an underscore cannot be added into the bundle identifier, so this can be a hyphen. The first commit here completes these steps with the fixes in place in the project template.

Oh Lint!

Now when I run the compile once again it fails with

Error: No lintable files found at paths: ‘Targets/Pokedex/Sources’

This is to do with the way the path was defined in the scripts folder in the first article and clearly the problem is that the project name is hard-coded in the path. The fix for this is simple: Use an argument in the script.

Fixing the swift lint command is surprisingly easy (to me at least). First update the TargetAction by passing $TARGETNAME in an array as an argument like so:

TargetAction.post(path: “../scripts/swiftlint.sh”, arguments: [“$TARGETNAME”], name: “SwiftLint”)

Then the sources path is updated with an argument variable here. With that fixed, I also fix the same target action in the main Pokedex project.

Building Some Scaffolding

Next step is to move the 2 scenes: Backpack and Pokemon Detail.

On copying these over to the BackpackUI target, the compiler cannot find a number of files as one can expect. Since it isn’t possible to import the main project into this framework (it makes a cyclical dependency), the only other option is to make a new common shared framework. This can be done in at least 2 different ways, namely creating yet another project, or adding another target to the main project, the latter being preferable in this case since it isn’t going to be so many files.

A quick look at the file structure of a framework and it is clear that there really isn’t much to it. It requires 2 folders: Sources and Tests, and a source file in each with some boilerplate code. Although this can be just added manually, it would be better to take a dive into how templates work using stencil files, and generating the framework using tuist scaffold.

But before that I take note of the compile issues so that I can decide which sources need to be in the common framework:

  • Core — most of it
  • Extensions
  • Model
  • Utils
  • Views

So basically just about everything that isn’t under scenes or services which are already refactored out should be in a common framework.

I revert the change and take a look at how this template is going to work.

According to the documentation I have to add a nested folder called Templates under Tuist and then create another one named after the template name: In the case:

<ProjectRoot>/Pokedex/Tuist/Template/framework

Then inside this folder I create framework.swift which contains the actual template. The stencil syntax is very simple and here I only use one parameter: name. In this commit you can see the double curly brackets used replacing the name in the stencil files.

The scaffold command is the recommended way to create new modules for project, and you should create your own templates for each type you need. Experiment for a while with the tuist command to get a feel for it before starting on a new project.

Then the command (from inside the Targets folder) is

tuist scaffold framework -name PokedexCommon

Next, I edit the project manifest and add PokedexComon as a LocalFramework dependency to the main Pokedex target, check that it worked with tuist generate, and then open the project. That worked.

Adding Dependencies The Wrong Way

When I compiled the project after fixing all the issues (the next 7 commits) about accessibility (mostly adding public in a lot of places in the code) and importing PokedexCommon, it now fails because it doesn’t have the NetworkKit framework added as a dependency.

When I add this dependency in the project file I now get this error:

The ‘dwarfdump’ command exited with error code 1 and message:
error: /Users/ronan.ociosoig/Projects/Tuist-Pokedex/Network/Targets/NetworkKit/NetworkKit: No such file or directory

It seems that there is something wrong with the path I defined here:

frameworkDependancies: [.framework(path: “../Network/Targets/NetworkKit”)]

This is the first time a framework is depending on another framework in this repo, so this is an interesting problem to solve. So how? It’s a TargetDependancy type which is an enumeration. Looking at the docs on it perhaps I should use .project(target: path:) instead — Yes, that seems to work. But…

No files found at: /Users/ronan.ociosig/Projects/Tuist-Pokedex/Pokedex/Targets/Pokedex/Sources/**/*.jsonThe bundle identifier ‘com.sonomos.NetworkKit’ is being used by multiple targets: NetworkKit and NetworkKit.

Solving these error messages: The first is simple — the project template has a glob that is expecting a JSON resource file when there isn’t one there. The second can be fixed by changing the bundle identifier defined in the NetworkKit target. But there is something else going on — the framework dependency still can’t be found when building the project, so that clearly isn’t the correct approach.

The solution turns out to be adding the framework as a target. 🎉

A Bundle of Trouble

But there is one more issue — the app terminates on trying to display the Pokemon view — the error message is: Could not load NIB in bundle. I forgot to add the path of a NIB to the resources of PokedexCommon.

In the main target NIB files are loaded like so:

resources: [
“Targets/\(name)/Sources/**/*.xib”,
… other resources ]

I add Targets/\(name)Common/Sources/**/*.xib as a resources path and try again.

This time another crash with a different issue:

[Storyboard] Unknown class _TtC7Pokedex11PokemonView in Interface Builder file.2021–05–24 17:25:17.152479+0200 Pokedex[25639:4017170] *** Terminating app due to uncaught exception ‘NSUnknownKeyException’, reason: ‘[<UIView 0x7fc3887105a0> setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key date.’

The problem here is that the main target knows where to find the NIB, but the NIB can’t find the custom class. Thus I conclude that the LocalFramework structure needs another property to pass in this information.

Adding a property resources as an array of type String to LocalFramework works, but there was one small issue to fix in the Loadable extension: The bundle has a default value of main bundle when nil is passed in as a parameter but since the view and xib are in a custom framework this now should be Bundle(for: Self.self).

With those fixes in place, the app is working again. Hurrah!

Too Much Baggage

And now, back to what I was intending to do in the first place — separate out the 2 scenes into a dedicated framework.

But hang on just a second! Look at those dependencies. Does it make sense to make that a UI framework should be dependent on a common framework, which in turn depends on a networking framework? The PokedexCommon framework needs another refactor as it would be so much nicer to remove the NetworkKit as a dependency.

Looking at the code in PokedexCommon, the DataProvider class is the only point that imports NetworkKit, and the search function is the only function dependent on it. The init function uses dependency injection but there are other ways to do this.

Instead of injecting the dependency in the constructor, it can be done in the function where it is needed. By extracting that function using an extension, and moving that extension into a separate source file in the main application, the NetworkKit dependency can be removed from PokedexCommon.

Starting The Right Way

Now I feel confident that moving the scenes into the Backpack module will work well. I add the Backpack module to the main project and generate it. Fails! The problem is that the path for the storyboards and nib files is defined relative to the main target. It’s a simple fix though.

By using the fact that the module is being defined in the project manifest as a LocalFramework and that it has both a path and name property, then the relative path to resources can be computed quite easily:

let resourceFilePaths = resources.map { ResourceFileElement.glob(pattern: Path(“../\(localFramework.path)/” + $0), tags: [])}

With that fix in place the project generates correctly. Running the application crashes however because it uses some code tricks in extensions that do not use the Bundle correctly, but that is simple to fix here.

The project works again and the first part of creating the Backpack module is done, but it should ideally also work as a stand-alone mini project.

The Backpack App

For the project to be more independent a quick look at the structure of the initial state of the app is needed. The original app had 4 scenes, with 2 of them now placed in the BackpackUI module. The only interaction was to tap on the backpack button on the home scene to open the backpack. So the first step is to copy over the home scene sources into the Backpack app, and then simplify them a little by removing the extra button, and some repositioning. I also copy over the coordinator and AppController and simplify.

The project and template definition needs to be updated to handle injection of the resource paths so that the BackpackUI module can load the xibs and storyboards, so that is done here.

Then I fix an issue with the scope of the AppData init to make the app work. At this point it all compiles and runs, but there is just one thing missing — data. The BackpackUI module is read-only so I need to inject some mock data into the app for it to show anything.

Mocking It

Although the Backpack app works as intended it doesn’t show anything, so time to inject some mock data into it. This can be done by simply adding a MockDataFactory.

When the app runs there is a small error displayed in the console:

Backpack[23598:4173330] Could not load the “PokemonPlaceholder” image referenced from a nib in the bundle with identifier “com.sonomos.BackpackUI”

The whole app works as expected and this error doesn’t seem to be affecting anything, however I would still like to fix it. I disable the code for assigning the image in the view, and the error still happens, so that suggests it isn’t in the code. Then I check the storyboard — there might be some issue there. And yes! This is exactly the problem. The default image specified in the storyboard should come from the image assets of the local bundle and not the sample app so the fix for this is to add the image assets to Resources of the BackpackUI target, and then update the project manifest so that the resources paths to include these asses like this:

“Targets/\(name)/Resources/**”

When I run the app it crashed.

*** Terminating app due to uncaught exception ‘NSInvalidArgumentException’, reason: ‘Could not find a storyboard named ‘PokemonDetailViewController’ in bundle NSBundle

This is very similar to a previous crash relating to how the bundle is defined, or not defined, as is the case here. In the PokemonDetailWireframe the code to load the storyboard is:

let storyboard = UIStoryboard.init(name: “PokemonDetailViewController”, bundle: nil)

Clearly the issue here is that the bundle is nil, and should be that of the view controller class — a trivial fix.

Running it again and on opening the detail view it crashes once again:

*** Terminating app due to uncaught exception ‘NSInternalInconsistencyException’, reason: ‘Could not load NIB in bundle: ‘NSBundle </Users/ronan.ociosig/Projects/Tuist-Pokedex/Backpack/build/Backpack/Build/Products/Debug-iphonesimulator/PokedexCommon.framework> (loaded)’ with name ‘PokemonView’’

This is crashing because the xib is not loading from the PokemonCommon framework. Once again, the fix is simple and was done previously: Add the relative path to the resources definition and it will load correctly. The Backpack app is now completed and working.

Backpack sample app

Test And Test Again

The unit tests were fixed in this commit, but the UI tests fail. For them to work, the application must load the sample data from the PokemonSearchEndpoint which is now in the NetworkKit module, therefore this JSON file needs to be in this module and for the resources path to find it. Then the resources path needs to be added into the NetworkKit target in the project manifest. That fix is here. At this point the main project works, the new Backpack project works, and all the unit and UI tests are passing. Mission accomplished.

Graphing Progress

The final step for this article is to generate the graph once again and compare it with the previous one (using tuist graph).

Since the Backpack module is effectively a separate tuist project it merits its own graph.

This clearly shows that the BackpackUI only depends on PokedexCommon, which was the only desired dependency. The sample Backpack project imports Haneke to load the images, and injects in the mock data.

The overall Pokedex project now looks like this:

Comparing this graph with what was done in the last article, it has the extra branch for BackpackUI and PokedexCommon. I have almost completed what I initially set out to achieve. What remains is another UI refactor for the Home and Catch scenes, but that must wait for another article.

Recap

  • Build a PokedexCommon framework and refactor the code.
  • Build a BackPackUI framework and create an application to validate it.
  • Looked at a simple way to use scaffold to create a template for a framework.
  • Fixed a number of issues related to resources (storyboards, xibs and JSON files), bundles and image assets.

In the next and final article of this series, I will finish the UI refactoring. Please follow to receive all the updates as they are published.

The completed code for this article is here.

If you find this useful, please follow to get the next updates.

Part 4 — Catch and Home UI Refactor

--

--

Ronan O Ciosoig
Ronan O Ciosoig

Written by Ronan O Ciosoig

iOS software architect eDreams ODIGEO and electronic music fan

No responses yet