Uncategorized

iOS App Codebase Best Practices

A client recently had me prepare a document containing suggestions on how to bring their app in line with current best practices as they continue to iterate and grow their team. Below I’ve listed some of those best practices, as I see them, and some suggestions on how to get there on a ‘lazy’ basis – that is, without sidelining new feature development for months while refactoring the whole app.

Generally, these suggestions break down into three larger principles: improving code quality, enforcing code quality, and speeding up development. We’ll refer to these in the titles of the sections with (I), (E), and (S), respectively.

First, though, a note on the concept of the lazy refactor, if you haven’t heard of it – most of the other suggestions more or less require it, so good to get it out of the way now.

Lazy refactoring

The concept is straightforward enough: rather than refactoring your entire app to a new pattern all at once, you do it slowly, over time, by just refactoring the files you touch for any given feature.

So, for instance, if you want to move to MVVM from MVC and your project has 50 controllers, you might initially be overwhelmed with the scale of the refactor. 50 is a lot of classes to refactor! That’s where the lazy refactor comes in. For the next feature you work on, you might touch one or two controllers; when you do, move them to MVVM. The feature after that, you might be fixing a bug in another controller – great time to move it, in turn, to MVVM. And so on from there, until your whole app uses MVVM.

Other great candidates for lazy refactors include: moving to Swift from Obj-C; increasing test coverage; and making large architecture changes.

Break out modules (I) (S)

Rather than having all your code sit in your app target, break out related files into their own module. This will do a few things.

First, it will make you think more deliberately about how you want your code to be organized. What parts rely on other parts? Should they? How can you abstract out those pieces to be more loosely connected and flexible? What parts should be public to other modules, and what is internal or private?

Second, it allows you to use that code in Playgrounds, something I cover in more detail below.

Finally, it moves you towards easily reusing that code in other projects. Getting good at keeping code general while maintaining an easy to use API for accessing that code is a balance, but one step is certainly to silo it from other code. If you do this consistently, you’ll soon have a library of components you can use over and over, which is amazing for your productivity. The next step after separating code into separate modules/frameworks is to:

Use internal Cocoapods (I) (S)

Cocoapods is just a convenient way to point to code files you want to include in your project. One way you can point to that code is to hand Cocoapods a repo to which you have access and which has a valid .podspec file.

The key phrase there is “a repo to which you have access.” It doesn’t have to be a public repo, it just has to be one the person running pod install can pull down.

So, you could conceivably break each screen into its own private repo containing a Cocoapod, and have your main app repo rely upon these and combine them to create the actual product. And there are companies that have done that (though, they don’t go quite so far anymore).

For fast moving products that are quickly trying to iterate on new features, this can be very advantageous. First, it helps you avoid merge conflicts in your pbx file, as you’re adding/removing files to a different project altogether. You can also test the code and ensure its functionality in isolation, incorporating new versions relatively easily. Finally, if you’re doing this on a UI level, it makes it easy to test a single feature with users during usability testing.

There are downsides to this though. Breaking changes that affect the API for working with the feature can take more time to resolve, simply because you have to update the pod and fix it in several (many?) different other pods. It’s also another layer of overhead: rather than maintaining one repo, you’re maintaining many and then integrating them all together.

My recommendation tends to be to break out small Cocoapods of discrete functionality that could be reused in other apps, or screens that have simple (or better, no) project specific dependencies. Otherwise, you can achieve similar results by just doing as the last point suggested, and maybe add a separate target for testing purposes.

Regardless, making a Cocoapod is pretty easy, so give it a shot.

Coordinators (I)

One way to make both of the former points even more convenient is to use the flow coordinator pattern. Much has been written about flow coordinators, and many talks have been given on the subject. They’re one of the most universally agreed upon, Neutral Good things you can do for your app. Suffice to say, they offer some huge advantages, including:

  • ease of testing – if you’re configuring a VC with a coordinator, in your tests you can make that VC dance like a marionette;
  • ease of refactoring – if your VC only knows about its view, and nothing about navigation, it is way easier to move it to a new spot in your app;
  • abstracts macro-context – on an iPad, you might want to configure your VC differently, or you may want to display the same screen with slightly different text depending on where it is in the flow;
  • less VC code – we’ve all heard of Massive View Controller, and moving nav code and configuration out of your VCs can dramatically improve this.

Break down Storyboards (S)

I sometimes think it was a cruel joke on Apple’s part to include a storyboard named “Main” by default in new projects. Almost inevitably, if you start using it, it will balloon into a massive file that takes tens of seconds to load.

Luckily, they’ve made it relatively painless to pull things out of it: select the flow you want to pop out of a storyboard and click Editor > Refactor to Storyboard. Just be careful! If you refer to monolithic storyboard as a string in your project anywhere in order to load some of those (no refactored) VCs, you’ll need to change the reference to the new storyboard.

Use Playgrounds (S)

Much has been written and said about Playground Driven Development. Playgrounds can really help you quickly iterate on features: rather than navigating the whole app, you can focus on the screen in question. However, doing this requires a lot of background, most of which is covered in the previous sections. For instance, your VCs have to live in their own framework (and NOT just the app target) to be used in a Playground; so, if they live in a Cocoapod or in a module you’ve broken out from your app, you get Playground support for free. The coordinator pattern also works wonders for Playgrounds by making it easy to configure the VC with any necessary stubs.

Robust testing (I) (S)

Tests are awesome. They serve so many purposes!

  1. They speed up development of the feature by helping you clarify in your head what you’re trying to build and how people will use it, just from writing the tests in the first place.
  2. They help fix bugs by letting you see the behavior step by step and finding where the thing breaks.
  3. They help make sure bugs stay fixed.
  4. Finally, they show other people how the tested object in question is supposed to work.

One way to make your tests more likely to be written an updated is to co-locate your tests with the objects under test in your project. That way you can’t help but notice the tests when you access the file!

Tests with CI (E)

Tests are great, but they can often be neglected, even when co-located with the code. Help stave that off a bit by running your tests on a CI service like CircleCI or Travis, and require them to pass before merging a PR.

Fastlane (E) (S)

Fastlane is a magical tool, and integrating it with your CI pipeline is a huge win. Fastlane helps automate testing, screenshots, beta/release deployments on TestFlight and the App Store, and code signing. Taking the time (which is not a huge commitment, to be clear) will drastically decrease your release cycle.

Other tips and tricks

PBX merge conflicts (S)

Most of the time, merge conflicts in your xcproj file come from multiple branches adding new files to the project. As a result, mostly you want a union of both branches. Luckily, you can do that quite simply by adding a line to your .gitattributes file:

*.pbxproj merge=union

Environment (I) (S)

Make dependency injection easy by using an Environment class (or struct) that holds all the things you need to mock for testing purposes. Then you can easily create a mock Environment object and configure your objects under test with it. This will greatly decrease a lot of the cruft required with traditional dependency injection techniques using protocols.

Reactive programming (I)

Tools like ReactiveSwift and Combine can help make your code less complicated and reduce the number of layers involved in passing data. It can also be a real brain bender. However, knowing and using the basics can take you a long way, so it’s worth taking a look into.

Functional programming (I)

FP is super powerful, and has really revolutionized the way I write code. It does have downsides though, which you’ll need to manage. I highly recommend getting a subscription to PointFree, as not only do they go into great depth on the ways FP can make your code better, they also cover a lot of advanced Swift features in the process, which greatly expand your toolbox as a developer.

Personally, I tend to try to apply FP where it makes sense and really shines. There are two places it regularly saves me time: UI styling and data pipelines.

By using small, reusable functions for UI styling, you avoid a lot of the pitfalls that regularly come with object oriented approaches (specifically, composition allows you to sidestep the diamond inheritance problem, which is a huge advantage).

Using FP for data pipelines allows you to test each step along the way, as well as integration test the whole thing from back to front. Since the flow from server to UI can easily get messy (and is where so many bugs come from) having that flow be rock solid is a huge boon.

Conclusion

These things make a big difference in my development day to day, hopefully they help you too. Anything I missed, please comment and let me know!

Leave a comment