I use tuist to build most of my iOS projects nowadays. And like
every good software engineer I test the code that I deliver. I want
to ensure that everything works according to what I expect is coming
to my app.
Xcode and schemes
XCode is a fun IDE, it is slow, has his own temper when it comes to
find references, refactor or even launch app. One of the things that
is really strange when you’re using this IDE for the first time is
that it does only build the active scheme. So, if your project has,
let’s say, 10 schemes, and you refactor a method to add an argument.
You won’t notice you have a build error until your build all
schemes…
Build errors
I tend to have a lot of build errors during refactoring, but this is
globally ok, I use them to pave the way to my final implementation.
One of the thing that bothers me is that I don’t always catch all
errors, and I need the CI to bail out because of a missing argument
/ protocol conformance method… This is not a cool thing in my daily
process, I am interrupted to fix a build error that should never
happen.
Build tests target automatically
As I am a tuist user, I want to be able to build all my tests target
easily to prevent pushing something that does not even build (even
before I can say that my tests are passing).
Command line-fu
At first I tried a basic thing, hoping that it would work (spoiler
alert, it does not):
tuistbuild'*Tests'
Scripting to the rescue
It seems that it is not supported out of the box by tuist, running
schemes by using a wildcard operator to filter them. However, it is
rather easy to do using basic bash scripting, so here is my basic
solution
I save this file under buildTests.sh, run a
chmod +x buildTests.sh and then I can build my tests
targets, in sequence, exiting on first failure. This is perfect for
my use case, whenever I refactor too much, or I want to check that
tests are still building, I can run this.
Downside
Script execution is rather slow, as there is not any parallelism
involved, everything is building from scratch. It might be nice to
run everything in parallel, but so far so good, it is ok for my use
case !
I am using tuist for a while now in my projects. This tool was
initially bring into the project’s scope to ease day to day
developer operations : no more fiddling with conflicts in
xcodeproject files !
Tuist ?
What is tuist by the way ?
Have you ever booted an iOS project, from a few years things have
evolved a bit.
Before 2020 I’d say we were doomed to using xcworkspace for defining
complex projects.
Cocoapod and Carthage were the cool kids on the block, allowing to
download libraries and use them in our beautiful apps.
After 2020, and still now, Swift development have changed to
massively using SPM: the built in dependency management tool baked
in the swift toolchain. The process is straightforward and easy to
set up, a few clicks in XCode and voila! The other way of using SPM
is by using a Package.swift file to describe what you want to
achieve.
The swift file works very well but lacks from a lot of advanced
features available in XCode projects. Or if it is possible, it is
not convenient to do.
This is where tuist fills the gap, like Gradle in the Java ecosystem
did a few years ago. We can finally use Swift to describe our Swift
projects. This means no more copasting things over and over and the
dreaded xcodeproj is no more : you’ll only ever have to merge swift
files !
Code modularity appears !
It also came in handy for bringing modularity into our codebase. We
managed to split dependencies and make modules per feature or
functional scope.
This comes at a cost, having to register our different modules to
our main app. When I’m talking about registering, I’m considering
handling dependencies, routing and navigation. This can represent a
lot of modules, a lot of useless lines in our codebase.
To be clearer, our main app contains authentication logic as well as
basic http interactions, our modules are dumb as they depend on this
external provider to be able to issue any http call.
First implementation
Every of our module was being added to our ResourceLoader class that
would call the necessary initializers to set things in motion like
the following
funcregister(){LoggingModule.register()…}
With our ResourceLoader implementing a protocol exposing everything
that was useful for our submodules, protocol which was made
available through a simple used-everywhere dependency.
Auto-registration for the win !
We can do module autoregistration by scanning embedded frameworks.
As we can do with IoC, the idea is to let every module declare
itself to the main consumer. No more long
register method in our application launch, and new
modules are automatically discovered at launch time !
Let’s illustrate this with some code, we declare a protocol that
will allow consumer to call registration for our module.
publicprotocolModuleRegistration{funcregister()}
This is our base building block for registration, every module
implements ModuleRegistration to contribute to the main
app.
In every module project, we then set a class that will be
responsible for registration. It will implement this specific
protocol and be populated by its full name into
Info.plist.
@objcpublicclassMyFrameworkRegistration:NSObject,ModuleRegistration{publicfuncregister(){// inject into the global context our dependencies// for instance we can do// Container.register(MyLogging.self, MyLoggingImpl.self)}}
Please notice important things in this class. It needs to be
annotated @objc and inherit from
NSObject to be discoverable later on.
The magic being this is that we will declare this class to be our
NSPrincipalClass in the Info.plist of our
framework.
To do so, we add the final touch by enriching the
Info.plist of our module using Tuist project’s
description :
Then in our main app, we scan for NSPrincipalClass in
all of our included bundles , one just need to call the
Modules.autoregister() method in application startup.
publicenumModules{publicstaticfuncautoregister(){Bundle.allFrameworks.compactMap(\.principalClass).forEach{clazzinautoregister(clazz:clazz)}}staticfuncautoregister(clazz:AnyClass){letinstance=clazz.alloc()ifletmodule=instanceas?ModuleRegistration{module.logger.debug("Doing registration for \(clazz)")module.register()}}}
Our main app will then scan all included bundles, look for
NSPrincipalClass, test if it matches the protocol
requirement and then calls the registration function.
The declared dependencies in our frameworks are now properly
registered inside our app context, dependency injection system or
anything you can imagine.
As an ancient Java developer, I’ve learned to use a set of
annotations to bring meta programming in my projects.
Meta programming can be considered as an orthogonal thing to your
code, you can inject code to log things, to wrap execution in a
transaction or simply provide some context to your fellow developer.
One of my favorite context providing annotation at this time was
brought by Google-Annotations package :
@VisibleForTesting
Its goal is rather simple: provide the context that the visibility
of the variable / method is not as restricted as it should be, but
this is for the sake of testing.
When going back to my loved XCode (just kidding), I miss these kind
of useful information.
Of course you can add a comment, that maybe someone will read if he
begins to wonder why the visibility is too important.
// this method should be private but we want to access it from unit test code funcdoStuff(){}
You can also play with the deprecation annotation to trigger a
warning (one more to add and parse with your eyes…)
@available(*,deprecated,message:"This is visible for testing")varmyState:State
But one thing I was really missing is the ability to really set the
proper visibility on my fields while keeping the testability.
Recently,
Swift 5.9 had added Macro support. Macros can be seen as ways to generate code based on specific
instructions (this brings back old Java-apt memories).
Macro types
There are multiples macro types, whether they can attach to fields,
and depending on what they can do:
Providing accessors
Generating code alongside the annotated field
Generating code “in-place”
There are two ways of calling macros :
Attached ones with @MacroName
Freeform ones with #MacroName
I will not enter the details of each type and implementation, you
will see more on this later here and can scout on GitHub
repositories for inspiration.
Attached macros are written with a leading @ and can generate code
alongside some of our declaration. This allowed me to introduce my
own @VisibleForTesting for swift implementation.
The idea behind this is really simple, generate specific code with
public visibility that wraps call to the “non-exposed” real method.
This way we get the best of both worlds, we keep our fields private,
we tell our colleagues that this field is available for testing and
we are able to test it properly.
The same goes for let, func, and
init . The only specific thing to keep in mind that if
you annotate a class initializer, you need to mark it
as required, otherwise the build will fail (but a nice
comment will tell you why).
As you might know, iOS supports Dark mode since iOS 12. It is pretty
straightforward to implement it by using dynamic colors. Either
system provided ones or adjust them using trait collections.
Dynamically switch
At times, we can not rely on default colors but we have to listen
for traitCollection changes in order to do what is
appropriate for adapting the UI to the mode being active.
It is easy to do by checking current traitCollection :
overridefunctraitCollectionDidChange(_previousTraitCollection:UITraitCollection?){super.traitCollectionDidChange(previousTraitCollection)letuserInterfaceStyle=traitCollection.userInterfaceStyle// Either .unspecified, .light, or .dark}
App switcher tricks
One thing that I was not aware is that iOS switches back and forth
between active mode and the complementary when the app goes to the
background :
To have proper representation in application switcher, if the trait
collection is changed, the OS takes a screenshot of your app using
light and dark schemes.
Consequences
This can lead to subtle problems, in my case, I was loading a
specific stylesheet for MapBox, that, when loaded, was registering /
refreshing elements on map. Thus, when the app was put to the
background, a lot of things were happening, without a clear
explanation (and the app was sometimes crashing due to the very fast
switching between map stylesheets).
The workaround is rather simple : if the app is in the background we
prevent loading the stylesheet, the trade-off is acceptable : the
app can display an invalid preview in application switcher, but, it
is nicer on CPU / Network.
If you’re like most developers nowadays you make an extensive usage
of git across a large number of repositories.
The now-classic branching model that consists in using a branch for
every fix / feature / experiment before creating a merge request is
perfect for collaborating efficiently. However it comes with one
downside, local branches can quickly become messy and removing stale
branches is not that easy
Removing remote branches
git is a really well designed tool and removing remote
references from your local repository is easily done by
gitremotepruneorigin
This will remove local references to non existing branches, however
it is not useful to remember this command, we can configure
git so that it prunes automatically on fetching :
gitconfig--globalfetch.prunetrue
Removing local branches
This is where things can be quite messy, as local branches might
have been merged, rebased, or squashed, and “classic” commands
allowing to detected merged branches are not always working as they
ought.
If you search how to clean up local branches, you might find a lot
of commands involving git branch --merged with
grep and xargs all the way.
git-gone is even clever enough to give you the command
to restore a branch if change your mind (as long as you did not
git gc the revision is still available locally, so you
can restore it)