Overview
Over the years, our team of engineers have gathered valuable experience from developing a wide variety of iOS projects. Here are some insights into how we kickstart our iOS projects, along with some proven approaches to our project foundations:
- Project setup
- Targeting iOS versions
- Managing environments
- Modularizing projects
- Testing processes
- Using pull requests (PRs)
- Deployment strategies
These foundations allow us to create a smoother development process and get a project pipeline up and running very quickly, potentially saving days of development work. While every project is unique, these principles can help to give your apps a strong foundation, enabling you to adapt and grow with confidence.
The Team
At the time of writing we are a team of six in-house iOS engineers (majority at Senior level), and normally we also have one iOS intern at any given time. This team will be split amongst projects but we normally allocate at least two iOS engineers to any project and increase this as required.
Boilerplate Project Template
Why Use a Template?
At ustwo, as a product-focused agency, we’re constantly developing new iOS projects for our clients. To streamline this process, we’ve developed a project template to not only speed up product setup but also provide a strong foundation for:
- Project organization
- Reusable foundation code
- Pre-configured testing suites
- CI/ CD scripts for various tasks
It ensures consistency and allows the team to hit the ground running with every new project we undertake.
Our iOS Project Template
We are continuously updating our iOS project template–as this sits in a source repository we are able to make ongoing changes to it as tools, techniques and the team's knowledge develops.
This currently includes:
- An Xcode project file including a basic modularised app structure including some foundation modules, a basic app structure (a simple two screen flow), test suites and project targets including their entitlements and configurations.
- Various scripts for our CI/CD pipelines (including GitHub workflows)
- Repository
ReadMe
- Templates for PRs, RFC + ADRs
- A
CODEOWNERS
file to allow us to easily target reviewers for PRs
You may notice that we don’t mention any architectural frameworks in this template, this is for many reasons including:
- Our projects vary widely in scale and scope
- We don't want to lock our engineers into historic technical decisions
- Architectural frameworks can sometimes lose favour in the community and may require additional support to implement updates
- We want our teams to have the opportunity to discuss, decide, implement and adapt the architectures that best suits their needs and skillsets
- Our clients may have their own preferences (we may be working alongside their engineering team or be handing over to a client team)
- We want to allow our developers to consider and potentially adopt new technology
Xcode Project Setup
The Groundwork
Engineers should be able to take the Xcode project file (.xcodeproj) and get up and running pretty rapidly (with pipelines needing some configuration). The key for us here is to get developers from 0 to writing useful code quite quickly, and as easily as possible.
To help achieve this, we’ve handled much of the initial project setup, including:
- Development teams
- Environments
- Targets
- Custom target assets (e.g. app icons per environment target)
- Removing project specific naming conventions in targets, the main project file and folders, in favour of more general terms such as
App
,Main
andiOS
- Custom file templates using text macros
Target iOS Versions
When it comes to setting up target versions for a project, with iOS we will normally work to n-1 where n is the most recently publicly available version of iOS. For larger projects with a longer development timeline we will sometimes work to the latest or upcoming version as by the time we are due to launch the project this could have become n-1 or is not a huge concern for the client. This not only saves us backwards compatibility issues but also greatly increases our velocity.
We appreciate this is a huge luxury and are aware of the concerns with potentially lower user adoption in relation to using to the latest OS. This conversation will always take place between our project and client teams, and will involve studying client (if available) and market data at the time but generally we find this isn’t a concern.
Environments
Once our project is set up, we can consider our workspace and CI/CD pipelines (we use GitHub and self hosted runners). In terms of environments we have our build configuration files setup for an engineer's local development environment, alongside our staging and production enviroments.
It's important we have documentation on how to update the project and pipelines to support these targets, with bundle IDs, urls, keys, etc. for all our build targets via xcconfig files and repository secrets.
On this topic, we will normally make builds available via TestFlight to pre-defined groups. The staging target is available to the project team, whilst production builds will be available to the wider client team and closed beta groups. This does require a bit of admin and there are ways to make some of this easier with enterprise distribution certificates but we find it's better to hold the reins with pre-defined testers than to allow anyone with a link to download a build.
Modules
Splitting your code into modules will keep things organized and easier to maintain whilst improving build times, testing, and developer collaboration. For our Xcode project structure, we have a number of core modules for foundation elements, including (but not limited to):
- Design system
- Services
- View components
- Analytics
- Foundation extensions
- Testing foundation modules (e.g. snapshot test libraries, local proxy libraries and general utils).
We will then house all of the features of the project within their own modules. Assets (i.e. Swift files, localisations, image assets etc) unique to specific features will be stored within the module and moved up to the core modules as they bridge over into other features. We consider modularizing a Swift project an ongoing and iterative process.
Testing
We take testing very seriously at ustwo with automated and manual testing processes.
At a code level, we utilise unit tests, snapshot tests and UI tests. We also reference these in our PR templates to reinforce this amongst the team. Within a project we will always allocate a member of our QA team to the project.
Our tests sit throughout the modules, where we will house our unit tests (including snapshot) whilst UI tests will sit at the top level of the project. All of our tests are handled by test plans which allow us to easily design test suites for various purposes (smoke tests, full test suite, run all tests etc) as well as setup tests for locales. We will normally cover a mobile and tablet device, both under light mode and dark mode, locales will be dependent upon project requirements.
In the interest of flagging emergent issues as soon as possible, we advocate running the full test suite against each PR. We’re fortunate to have our own in-house runners which allow us to do this without racking up the fees of a 3rd party provider. In cases where client security policies have restricted our use of local runners, we have also integrated with cloud-hosted providers (like HyperExecute) to ensure our tests are run on real devices. Discussions around scalability (of running the full suite on each PR) will usually come at a later project stage but, due to the foresight of using test plans alongside our scripts, this can easily be configured to meet the requirements of the time.
PR Process
Setting Up a PR
Once a PR is ready for review the developer will update the status from draft to Ready for Review
. We’re keen for developers to push PRs early, even if only in draft form, to give the team better visibility on the code they are working on.
We have custom GitHub workflows set up to run a number of tasks when any PR is created or updated. These tasks ensure the integrity and code cleanliness of a project. They are run in the following order so the lightest tasks will fail sooner and leave any heavy lifting till the end (this helps with potential queues on a runner):
- We check the PR title conforms to Conventional Commits
- We check code is linted to our specification
- We check the project folders and all files are sorted within the project file
- Run unit tests
- Run snapshot tests
- Run UI tests
- Upload test results and artifacts
PR Templates
PR templates provide us a predefined and common template for our developers to complete before requesting a review of their PR. PR templates may vary from project to project, but as a starting point we will always include:
- A descriptive overview of the work completed
- Link to the ticket whether that be on the repo or another project management platform (e.g. Linear, Jira etc)
- Screenshots (if applicable - to help the reviewer quickly identify the section of the project that has been updated). Sometimes this can be a GIF or video if such detail is needed.
- Any special testing steps - these could be how to replicate a bug or how to get the app into a certain state.
- Accessibility considerations
- A short check list of items such as linting, a design review, and manual testing in both portrait and landscape orientations
Here is an example of how such a PR template may look:
# Pull request
## Ticket
<!--What ticket does this PR reference?-->
## Description
<!--A description of the work completed. Could be long-form, a list, etc.-->
## Testing steps
<!--Optional list of project specific testing (directed at QA)-->
## Checklist:
- [ ] Tested on a real device
- [ ] Code has been linted via `swiftlint --fix`
- [ ] Tested in landscape and portrait oreintations
- [ ] Design review has been carried out
- [ ] Accessibility has been tested
## Screen shots
<!--Optional screen shots-->
| Optional Title | Optional Other Image |
|---|---|
| | |
## Appendix
<!--links to documentation etc -->
Reviewing a PR
Once the PR has been reviewed we will update the tag (we're big fans of custom tags) of the PR and move it across the project board where a QA will come in and manually test it before giving it the final sign off. Now this isn’t to say we will do this on all projects. Depending on the release process we’ve adopted and the volume of PRs being submitted, we may cut a scheduled build for the QA to test, which will include a number of PRs to be tested at the same time.
Deployment
Historically, we have not used Fastlane for deployments, instead relying on our own custom scripts. These scripts manage the deployment process, including scheduling, which varies across projects. Some teams may prefer a regular daily build (Monday to Friday), while others opt for a per-PR release approach–both of which can be customised as needed.
When a release is triggered, our scripts automatically update the CHANGELOG, associating the merged PRs since the last release.
Releasing to the public is typically a shared responsibility. Our team sets up the release, while the client usually handles the final approval and submission. Responsibility for marketing content required for App Store distribution varies by project and may be handled either by us or the client’s team.
The cadence of releases differs between projects. In most cases, a release train runs every two weeks, aligning with sprint cycles. However, releases can also occur more or less frequently, depending on when new features are ready for deployment.
What Next?
Hopefully, this has given you a bit of a helpful insight into how we approach our greenfield iOS projects. By incorporating some of these practices into your workflow, hopefully you can create more efficient, scalable, and polished iOS projects.
We’d love to dive deeper into some of these topics in future posts and maybe broader topics so let us know if there’s anything you’d like us to explore further!