← All articles
iOS Development7 min read

From Idea to the App Store 2: Technical Decisions and Implementation

How I made development choices based on the product's needs.

This is the second article in a series about my process of developing Volei+. If you have not read the first one, start here. In this part, we will take a closer look at the technical decisions behind the app’s development.

Of all these decisions, one would shape the path of everything that followed: which platform should I start with?

Why did I choose iOS as the initial platform?

The first decision was which platform to use for development, and I chose native iOS. This decision was based on the expertise of the technical team, which, in this case, consisted only of me, in developing native iOS applications with Swift. I also considered building the app with Flutter, a framework I already had experience with, but the technical knowledge I had accumulated in the Apple ecosystem would allow me to move faster with native development.

Therefore, development time and product quality were both taken into account. Given my expertise, I knew I could build an application from scratch much faster using native technologies. This allowed me to validate the product more quickly and decide to continue its development. At this point, my career interests also played an important role in the choice. I wanted to use the application as a laboratory for exploring Swift technologies I had not worked with as much in my day-to-day job, such as Core Data and SwiftUI.

This allowed me to gain early experience with these features, or capabilities, and make a more meaningful impact when I needed to use them at work. In summary, the platform choice was influenced much more by the development team’s capabilities and future interests than by the audience itself at that moment.

However, as the app was validated and became more relevant, the need to expand to Android became clear. I have already decided that the native app will be replaced by a Flutter implementation that supports both iOS and Android, but I have not yet decided when I will begin that development.

That expansion will come later. To get the first native version working, there was a more immediate decision to make: should I build the interface with UIKit or SwiftUI?

Why did I choose SwiftUI instead of UIKit?

Because this was a new project targeting recent versions of iOS, I did not need to maintain legacy components or support older versions of the operating system. This removed one of the main reasons to choose UIKit and allowed me to evaluate both frameworks based on the product’s technical needs.

Volei+ has a state-driven interface: lists of players, teams, matches, loading states, and error messages need to react constantly to changes made by the user. SwiftUI’s declarative approach fit this scenario well. Views describe how the interface should appear for each state, while ViewModels publish the required changes. When the data is updated, the framework re-renders the affected parts, reducing the need to manually synchronize the interface with the model.

SwiftUI also integrated naturally with the project’s other architectural decisions. NavigationStack works with AppRouter to centralize routes, while shared objects, such as the user session and dependency container, can be made available throughout the screen hierarchy. In addition, previews, visual representations of the interface inside the development environment, allowed me to create different screen states and quickly validate components, visual changes, and animations without running the app’s entire flow.

UIKit would still be a relevant option if the product required highly specific components, detailed control over the interface lifecycle, or functionality that remained limited in SwiftUI. Because those requirements were not part of Volei+’s initial scope, the productivity gains and integration with a reactive architecture carried more weight in the decision. Even so, if a specific need arises, the two frameworks can coexist through mechanisms such as UIHostingController.

Choosing the framework answered how the screens would be built, but it raised a bigger question: how could I organize state, navigation, and data access without concentrating too many responsibilities in each View?

How did I architect everything together?

MVVM, MVVM-C, VIPER, and MVP are valid and widely used architectures in iOS development. I needed to choose one that could take advantage of SwiftUI’s reactivity while also supporting the app’s growth. MVVM met the first requirement well because ViewModels expose the states observed by Views. To handle navigation as well, I chose MVVM-C.

In this model, the Coordinator, represented in Volei+ by AppRouter, centralizes the routes and prevents each View from taking responsibility for defining the app’s flows. This additional layer made the initial implementation slightly more involved, but it made adding new screens and changing the navigation easier as the product evolved.

However, organizing only the interface and navigation would not be enough. To avoid god components, structures that concentrate too many responsibilities and change for several different reasons, I also needed to separate presentation from data access and persistence.

For this reason, MVVM-C became responsible for the interface layer, while principles from Clean Architecture, an approach that separates system responsibilities into layers and reduces coupling between them, guided the organization of the app as a whole. In practice, Views display the interface state, while ViewModels coordinate user actions and communicate with repositories through protocols without directly accessing the persistence mechanism.

Repositories act as a bridge between ViewModels and Data Sources, which concentrate the details of accessing and converting data stored in Core Data. AppContainer, in turn, centralizes the creation of these dependencies and injects them into the ViewModels. This organization also makes testing easier because a real implementation can be replaced by a mock, an object that simulates a dependency, without changing the interface layer.

This separation will be especially important in the product’s next stage. Once the backend is available, I will be able to create a new data source responsible for Networking, the process through which the app communicates with external services, and connect it to the same repositories. As a result, the interface and much of the existing logic will not need to know the API details or be rewritten because the data source has changed.

But before preparing the app to retrieve information from a server, I needed to solve a problem that had existed since the first version: where and how should the data be stored?

What about storage?

As I explained in the previous article, the app is used at volleyball venues and in outdoor environments. Therefore, users may not have internet access or may experience significant connection instability.

Combining this requirement with my technical curiosity, I chose local storage. For this purpose, I decided to use Core Data, an Apple framework designed for offline storage that also provides robust relationship management. Among the available options, it seemed the most suitable because it is a native Apple framework, which reduces the reliance on third-party packages, and because it supports more complex relationships. I needed models such as Player, Pickup Game, Teams, and Championships, with several relationships among these entities.

This was definitely the most complex part of the app’s development. I needed to model the data carefully and analyze its relationships to achieve a satisfactory result, after a few attempts involving amusing bugs such as disappearing players and teams being disbanded.

As the app grows, I see creating a backend and transferring the local data to that service as the next steps. The application will then follow an online-first model, an approach in which server data is the primary source while offline support remains available when there is no connection. This will allow me to create several new features for interaction among users and manage the data more effectively.

Platform, interface, architecture, and storage solved different problems. The final question is what these choices, considered together, represent for the product’s evolution and for my work as a developer.

Conclusion

Developing an app alone involves several technical decisions that also represent daily challenges at large companies. In a small app developed for learning and experimentation, we have the opportunity to make choices that prioritize learning and to take more risks. In an app built for a large company, however, we cannot take those same risks: decisions need to be carefully researched and well-founded.

Building Volei+ allowed me to experience these decisions at every stage, from choosing the platform to preparing the architecture for the product’s next steps. This is the practical experience I bring to other projects. If you are also making decisions about architecture, scalability, or user experience, contact me and tell me about your project. I can help you find the most appropriate path for building a scalable app and providing a good experience for your customers.

About the author

Danilo Lira

An iOS engineer experienced in building products across retail, financial services, healthcare, and independent ventures. I write about the technical decisions behind better products.