1. The Problem We Wanted to Solve
2. Controlling Concurrency with Swift 6.2
3. Dependency Injection with Native Macros
4. Design Patterns: The Role of Strategy
5. The Server Manager: Security and Configurability
6. Testability: The Direct Benefit of Decoupling
7. Conclusion
How the O2O iOS team redesigned their architecture from scratch to embrace strict concurrency, SOLID principles, and real scalability.
As SwiftUI became established as the main iOS development framework and major changes were introduced in recent language versions — especially strict concurrency in Swift 6 and Swift 6.2 — we decided to step forward and rethink the architecture from its foundations.
The result is a new architecture, built on six fundamental pillars that guide every technical decision made by the team:
- Compliance with SOLID: Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, and Dependency Inversion.
- Unit tests that validate code behavior from its smallest
- Strict Concurrency implemented with Swift 6.2.
- Async/await for asynchronous tasks with structured concurrency, keeping complex code readable.
- Scalable architecture that enables developing new features without compromising existing ones.
- Minimal third-party library dependencies.
The Problem We Wanted to Solve
One of the most common mistakes in multi-developer projects is each developer having a different interpretation of each layer’s responsibilities. The most frequent mistake: business logic in view models or presenters. Our top priority was precisely to prevent that problem.
The first thing we established were the layers of our architecture: the view with its view model to control presentation logic; the Use Case pattern to encapsulate business logic; and in the data layer, the Repository pattern to obtain information from different sources, accompanied by a server manager to handle API communication.
In the use case layer, we created a parent class with generic functions so that all use cases follow the same methodology: an execute function for use cases that do not need to communicate with the data layer, and another for those that do, switching context to work on a thread other than the main one.
Layer diagram: Use Case
The domain layer works with business objects capable of mapping to Data Transfer Objects, ensuring a clear separation of responsibilities between layers.
Controlling Concurrency with Swift 6.2
Once the layers were established, the next challenge was to properly manage concurrency. Using custom global actors, we established that the data layer would operate in its own execution context, separate from the rest of the application.
Both the view model and use cases implement the async/await pattern in the generic functions of their base classes, which allows maintaining structured concurrency and avoiding data races in the code.
Concurrency flow with global actors and async/await
Dependency Injection with Native Macros
In large projects, over-instantiation of objects during the development of new features is a real problem. To avoid it, we opted for Dependency Injection: dependencies are created at a single point in the application and injected into the corresponding classes via macros — custom annotations built from native iOS macros.
Dependency injection via Swift macros
Design Patterns: The Role of Strategy
During development, we aimed to make the code as abstract as possible for developers, while adhering to our guidelines and keeping the learning curve low. The pattern we use most is Strategy.
This behavioral pattern allows defining a family of algorithms, placing them in separate classes and making their objects interchangeable. It proved especially useful for simplifying the instantiation of third-party library managers and for establishing field validation rules consistently across projects.
Strategy pattern implementation for managers and validations
The Server Manager: Security and Configurability
The last piece in the chain is the server manager, invoked from the repository depending on the data source. These managers are used to obtain information from external APIs and apply a security layer in URL construction — endpoints are retrieved via encryption.
Server manager architecture with encrypted endpoints
This layer is highly configurable. It includes basic CRUD functions, disk cache, in-memory cache or both, interceptors for tokens and token refresh, SSL pinning, request and error logging, automatic retries on failure, and the ability to create custom interceptors and inject them when needed.
Testability: The Direct Benefit of Decoupling
The great advantage of such a decoupled architecture, based on protocols and respectful of SOLID principles, is that it makes creating tests straightforward and fast. Each layer can be tested independently, with clear mocks and no unexpected side effects.
Unit test example on server managers
Conclusion
This architecture has been a game changer in how the O2O iOS team approaches development. The combination of SwiftUI, Swift 6.2, SOLID, well-chosen design patterns, and a secure and configurable network layer has given us the confidence to scale our projects reliably.
References
- SOLID Relevance — Uncle Bob: blog.cleancoder.com
- Strict Concurrency and Swift 6 — O2O: o2ods.com/blog
- Design Patterns — Refactoring Guru: https://refactoring.guru/es/design-patterns
