How to dismantle a Massive Singleton iOS App
At least once in a lifetime any iOS developer has inherited a legacy project from a younger himself or from someone else who coded when iOS development was a gold race for indie developers.
At that times only a few of us cared about tests, testability, architecture or patterns. There was only massive view controllers, care for limiting number of crashes, care for a unlimited number of features, and singletons.
There was a lot of singletons.
Singletons are very easy to implement with just 1 line of code in Swift and 5 in Objective C, are easy to invoke from everywhere in your application and are the best to mess up your code preventing Unit and UI tests.
The purpose of this article is to give you an effective and fast tool to untangle any hank of singletons, no matter how many they are.
Act 0: Current situation
Im going to use a code snippet to give and example of what I’ve described above, simulating 3 singleton services and an example view controller.
This snippet is a real mess but is not an unusual situation for many iOS codebases. As you can see we have all 3 singletons used the example view controller, and some singletons are invoking the others, this situation makes our code untestable because we can’t mock anything.
Act 1: Introducing a Service Provider
First step to reduce singleton impact is the introduction of a Service Provider. To implement this pattern i’ll use Swift Protocol Extension, and thanks to this technique i’ll gain testability.
I’m writing a protocol with 3 services as variables and providing singleton instances in extension as default values.
Now any class or struct can just implement this protocol to get access to the services without invoking directly the shared singletons. We have just introduced a decoupling layer between our code and the singletons.
Here’s an example of how use it on the example view controller:
As you can imagine, now we can test the view controller simply adding an extension in our test target where a service var is pointing to a mock service:
Please remind that this kind of override works only if your singleton is a class because you can’t subclass a struct. If you are using struct singletons you’ll need to create generalize them with protocols.
Act 3: removing singleton static properties
At last we want to remove any reference to shared or sharedInstance static variables from our code.
To achieve this I’ll use a static factory that will return the same instance of the service class, removing any reference to singleton pattern from our code.
I also prefer to have this class with fileprivate access control inside Service Provider file, to prevent any access except service provider implementation.
Now you should have no shared static properties in your code and a strong decoupling layer between you services and the rest of your code.
- Decouple your singleton each others
- Decouple your singletons and other components
- Using protocol extension you’ll provide a default value that you can override in tests or in particular cases
- Avoid exposing shared or sharedInstance global vars to avoid the temptation of using singletons again
Here’s some hint to expand this approach and improve your solution:
- Use protocol composition to have more specific service providers:
- Have protocol extension in files with app specific target and UI tests protocol extension files with UI tests target because you want to mock any service and have a static data behavior during UI tests
- Stop writing singletons unless your really need them
Found this article useful? Follow me (Giuseppe Travasoni) on Medium and check out my most popular articles below! Please 👏 this article to share it!