If you are a part of an established product company, you don't usually have the luxury of starting a new project from scratch very often. Facebook migrated its hybrid app to native Objective-C a while ago. Lyft, Uber did rewrite their old Objective-C apps to modern Swift language. Few others dared to do the same, and understandably so - because of prohibitive cost and risks of doing the full re-write of the milking-cow, core business app. I was privileged to experience what Uber's rewrite felt like, I saw what a tremendous effort it was and the level of commitment it required from everybody. If I could, I would love to roll the time back and experience it again.
The most important takeaway for me, as an engineer and an aspiring technical lead, was that the choice of commonly accepted practices across the development teams, as well as the underlying tooling and the choice of programming paradigms, has a dramatic effect on the success of the software project. With the right decisions, made early and communicated well, the team of developers could gain tremendous speed and sustain it for a long time, not piling up technical debt, continuing to keep application architecture clean and maintainable, and not stepping on each other's toes.
Abstract
I've started a few new iOS app projects recently. The biggest, which took me two years to complete, was a full-blown native iOS messaging app. When working on it, I was able to try out multiple approaches and architectural patterns and see how well they play along. The biggest takeaway from this experience, which 100% matches my experience working in a huge engineering organization, is that the entire application's architecture needs to be aligned. You cannot effectively combine MVC in the onboarding part with Redux for the settings, with VIPER (or RIBs for that matter) somewhere else.
The most effective way for me to gain enough speed at the beginning of the project, and to maintain it throughout the project lifespan, was to use a set of 3rd-party frameworks, that give enough high-level abstraction power, and to follow a single carefully chosen architectural principle, not mixing abstraction levels in different parts of the application. The best thing you can do as a team lead, is to see that principles you set up early are followed by all team members.
So, cutting it short, here's the list of principles and frameworks that I would use again for every new iOS application I'm starting.
The principles
-
RIBs architecture (for a typical UI-oriented app):
- Because it allows the application's business logic, and not the UI tree, to drive the application's architecture;
- Because it allows achieving the code coverage and dependency injection targets set below;
- Because it's such a simple concept, that to use it you don't really need a framework, you can start using it by adopting a pattern consisting of only 4-5 protocols;
- Initializer-based static dependency injection:
- Because it allows making your code 100% unit-testable;
- Reactive (RxSwift) data/event streams for passing the data:
- Because thinking of your app in terms of data and event flows simplifies its architecture;
- Unit tests covering 100% of the business and presentation/navigation logic:
- Business logic classes decide what screen the application should navigate to, what network requests it should make and what data to store to the local database;
- The navigaiton logic is hard to unit-test in a classic MVC app, and that's where RIBs are shining;
- Snapshot tests covering 100% of the UI:
- Because it's cool that you could adopt unit-testing principles to testing the UI (e.g., making sure that long labels nicely wrap and don't break the rest of the screen in every supported language);
- UI Automation tests covering key end-to-end user journeys:
- Creating and maintaining them is hard, and requires some advanced tooling, but it automates the end-to-end Q&A, and pays off dramatically in the end;
- Code-generation for such things as:
- Mock classes, so that you have test doubles at your disposal in unit-tests;
- Network services and backend models are all code-generated from the singe cross-platform IDL file, shared across systems and teams, because it guarantees these systems can reliably talk to each other;
- Localization strings, images, and other resources, so that there's a process to keep your strings and resources organized, type-safe and that garbage is removed automatically;
- Multithreading, timers, operation queues are only allowed using RxSwift schedulers:
- So that the code is unit-testable (and not just in principle, but in a reliable, fast, and controllable manner), and unit-tests are not flaky;
- All UI is created programmatically using specialized domain-specific language (so no XIBs/Storyboards!):
- So that every and all code changes can be code-reviewed;
- So that the UI logic is separated from the business logic and the Massive View Controller pattern is avoided;
- So that an initializer-based dependency injection can be used to instantiate view controllers and views (and avoid optional unwrapping);
- So that UI snapshot testing can be used;
- Automated translation process:
- So you don't have to upload your strings for translation manually;
- Every significant code change, initiative, down to the detailed feature implementation detail and rollout plan is communicated to all team members:
- So that the knowledge is shared;
- The feedback loop is established which makes the development team more stable and efficient;
- Because this is the only process I know of that scales well from a team of 2 to a team of 1000+;
- Did I mention obligatory pre-commit peer code-reviewing, with the areas of responsibility between teams clearly defined, and rules for automatically assigning reviewers from corresponding teams established:
- So that the knowledge is shared and the coding discipline is maintained.
Having defined the principles, what minimum set of frameworks will most closely get us there?
3rd-party frameworks
- For the main app target:
- RxSwift, RxCocoa as the most established and supported reactive library in Swift language;
- Alamofire for the network abstraction layer;
- Realm as a mobile database;
- SnapKit as a domain-specific language to help building UI without using XIBs;
- RIBs as a programming pattern and a strongly opinionated application's business logic building framwork;
- Envelope to provide protocol-based abstractions over UIKit, Network, Realm (working on it currently, extracting a reusable and fine-grained framework from different projects);
- For the test target:
- Sourcery and SwiftMockTemplates code-generation templates to generate mock classes for unit-tests;
- Swiftgen to generate strings and image assets;
- Twine to automate translation process;
- Quick and Nimble for unit-testing as a replacement for plain old XCTest;
- iOSSnapshotTestCase for snapshot testing.
Conclusion
The list above seems to include frameworks from very different domains. That's on purpose. I want frameworks to provide high-level abstraction concepts, not the concrete building blocks, that's why I would not use a 3rd-party image picker implementation, but happily use RIBs and RxSwift with Alamofire and Realm. The former adds a concrete specialization to the application, and a dependency on a (likely) not well-maintained component, while the latter adds generic programming concepts, that play well together and are well-supported.
As usual, I would appreciate any feedback or questions.
Thanks for reading!
Top comments (0)