Setting context: Adoption of SwiftUI by PhonePe
Since PhonePe’s inception in 2015, its mobile ecosystem has primarily consisted of the PhonePe app, the PhonePe Business app, and a payment gateway SDK for third-party apps. Over the years, we have managed to grow the PhonePe app to almost ~1.2M lines of code.
During this time, the global mobile ecosystem also evolved and the general trend that caught on was to build and publish apps using declarative frameworks. By 2019, Apple also hopped on to the trend, following declarative frameworks such as Litho and Epoxy, and released its own declarative framework – SwiftUI.
Fast forward to 2022, PhonePe started brainstorming on creating a standalone shopping app which lets users buy food, grocery, medicines and more, from nearby stores. The plan was to build over the Government’s ONDC initiative – an open-network that connects hyper-local supply to demand.
SwiftUI became a favorable choice over the more traditional UIKit to build the Pincode iOS app. Among other benefits, SwiftUI offered the facility to maintain a high product velocity by enabling faster development cycles – a crucial feature for a new product. SwiftUI’s declarative syntax was also very similar to its Android counterpart – Compose (launched in 2019), which made it easier to reason about a common architecture pattern for both platforms. With all these considerations in mind, and a long discussion later, we decided to place our bets on SwiftUI and we couldn’t wait to get started!
At PhonePe, we’ve been using Swift and UIKit as the core technologies for our app for nearly a decade. During this time, we’ve put in a lot of hard work to create a full ecosystem, complete with in-house tools and supporting libraries to power our application.
One of our main concerns during this transition was making sure that all the libraries we had built would smoothly integrate with SwiftUI. We didn’t want to start from scratch.
What is SwiftUI?
Although it’s common knowledge, for those who are new to the ecosystem – SwiftUI simplifies iOS development by treating UI as a function of app state. It employs a declarative syntax to describe UI based on the current state, ensuring automatic updates when data changes. That means no more `reloadTableViews()`, `layoutSubviews()` etc. This also makes one more part of everyone’s lives easier – Animations. Since everything works on state, it only takes a few minutes to tweak and craft beautiful animations.
New Frameworks, New problems
Although SwiftUI sounds like a new and shiny solution to all our problems, it’s not as workable in the real world! SwiftUI can be resource-intensive, especially for complex layouts, which demands careful memory management. Interoperability challenges can arise when combining SwiftUI with UIKit, which can be a bummer for some since not every component has been translated 1-1 in SwiftUI.
Evaluation of Design Patterns
With new languages come new design patterns. After reading various blogs and conducting a thorough research on design patterns, we found that MV (Model-View) was considered the most suitable choice.
However, upon examining the requirements of the Pincode project, we encountered a few drawbacks associated with the MV pattern. MV lacks the clear separation of concerns as compared to, say, MVVM, which can lead to code that is harder to maintain & test as the app grows, and as complexity increases, it could become challenging to reuse the components and maintain clear separation.
Given these considerations, we found MVVM to be a more suitable solution for the long term, combined with the usage of `ObservableObject` and `@Published`.
The Navigation Problem
SwiftUI, as is, on iOS 14 was nowhere near production ready, and we expected so. There were some known issues and limitations, especially in NavigationLink – there were bugs with using Lists, back buttons were not working, and many other issues.
Considering SwiftUI is evolving and Apple often addresses these issues, it’s also important to note that they don’t always do it on time, and time was a luxury we did not have.
So we had to develop an in-house solution with the combination of UIHostingViewController (thanks Apple) and UINavigationController (We missed you, friend.)
The Crash Problem
We were using Firebase as our crash reporting tool, where we basically upload debug symbols for every release version. But here’s the fun part – Apple provides symbols for most system frameworks (UIKit, AVFoundation, etc.), but others like SwiftUI, Combine and Metal Performance Shaders Graph are exceptions — no symbols are provided. So basically whenever you try to view crashes, the tool itself is blindsided because it has improper files.
Things we learned
- Custom modifiers: Modifiers are one thing that made our lives 10x easier. Modifiers are simple methods to modify given values in various ways. For eg: Padding is a modifier, which adds padding to a view. We have made tons of custom modifiers which helped us avoid boilerplate and keep the app consistent.
- Combine with MVVM: Combine with MVVM is a legit workhorse, and we ensured it was. We used it to its full potential and made sure we were writing some good code.
- Nested LazyVStack: In SwiftUI, `LazyVStack` is designed to be efficient in terms of memory usage and performance, as it only renders views that are currently visible on the screen, similar to a UITableView in UIKit. While using nested Lazy Stacks is necessary to achieve complex layouts, it sometimes leads to unexpected layouting and performance issues. So we as a team decided NOT to nest two Lazy Stacks.
- Design System: Since we were coming from imperative programming, we constructed our Design System in such a way that it used ViewModels for everything, whether it was colours, or attributes such as strikethrough. We regretted this later down the line, but as soon as the first version was released, we instantly jumped back on to this problem to make the components in a more declarative way, inspired by SwiftUI itself.
- Ease of onboarding into the codebase: SwiftUI is a paradigm shift in mobile app development, and can take some time getting used to. Developers, especially those coming from a UIKit world need to reason about their logic and their definition of UI in a different way. That being said, the learning curve for SwiftUI is still relatively straightforward. SwiftUI is intuitive, and works the way one would expect. With some effort, one can learn the SwiftUI style of development and leverage it to build apps in a much faster and efficient way.
The moment we worked so hard for finally arrived!
It was finally time, and we released the app to the world on 4th April, 2023, undergoing numerous iterations within the team. The world was excited about ONDC so they latched on to the application as soon as they got their hands on it. It was phenomenal, watching a project go from 0 lines to someone’s mobile device. We gained approximately 10,000 users on iOS during the first month, raising our anxiety levels proportionally.
Building an entire app in SwiftUI was a challenging yet rewarding experience. Not only did we build an app with SwiftUI, but also expanded the horizons of the mobile ecosystem at PhonePe, foraying into the modern landscape of declarative programming. We also established trust in SwiftUI as a framework for end-to-end development among fellow developers, who were otherwise doubtful about its production readiness.
While we still have far to go, we are proud of what we have been able to achieve in a small amount of time. We have an exciting roadmap ahead of us. SwiftUI is the future of application development on iOS, and we plan to introduce it in the PhonePe app as well, and we can’t wait to get started!
We went through multiple internal launches of the Pincode app. The entire journey felt like a thrilling roller-coaster ride for the team. As of today, Pincode has more than 1,000,000+ downloads on Play Store and the App Store.