AppForce1 Worklog

AppForce1 Worklog: Refactoring an 8-Year-Old iOS App is Like Restoring a Classic Car

Jeroen Leenarts Episode 106

Send me a text

Jeroen shares his real-world iOS development journey working on a legacy app at Dawn Technologies. He details his systematic approach to modernizing an 8-year-old codebase that serves as a critical tool for companies.

• Breaking down a monolithic App Delegate into dedicated managers with single responsibilities
• Leveraging the existing feature flag system to safely deploy new implementations
• Refactoring the walkie-talkie functionality with real-time audio streaming over WebSockets
• Completely rewriting the chat system to use a modern service-based architecture
• Overhauling the location tracking system to use iOS 17's new async location tracking APIs
• Implementing WiFi settings fixes for iOS 16 compatibility using modern APIs
• Maintaining a cleanup branch to remove deprecated APIs and fix compiler warnings

Check out Do iOS, the iOS development conference I'm organizing later this year. Visit do-ios.com for more information and tickets - link in the show notes.


Join me in Amsterdam for Do iOS 2025, tickets and details available now.

Lead Software Developer 
Learn best practices for being a great lead software developer.

Lead Software Developer
Learn best practices for being a great lead software developer.

Disclaimer: This post contains affiliate links. If you make a purchase, I may receive a commission at no extra cost to you.

Support the show

Do iOS: https://do-ios.com


Rate me on Apple Podcasts.

Send feedback on SpeakPipe
Or contact me:

Support my podcast with a monthly subscription, it really helps.

My book: Being a Lead Software Developer

SPEAKER_00:

Welcome to the second episode of the App Force One Worklock, where I share my real world iOS development journey as I work on my day-to-day iOS projects. Hey there, iOS developers, welcome to the second worklock episode of App Force One. I'm Juden Leinhardt and I'm excited to share with you what I've been working on over the past few weeks. As I mentioned in the intro episode, I'm now back to building iOS apps full time at Dawn Technologies. Working on my day-to-day projects, which include critical image response apps for companies. Think of it like a digital first aid kit that companies use when something goes wrong. This isn't just any app, it's one of that could literally save lives, which makes the work feel incredibly meaningful. But here's the thing this code base is like a classic car that's been sitting in a garage for years. It's over eight years old, which in iOS development terms is ancient. It was built before Swiss UI existed, so it's full on all UI kit. It's using patterns and practices that were cutting edge in 2016 but are now considered outdated. We're talking about manual view controller management, old networking patterns and architectural decisions that made sense at the time but are now technical debt. And I love it. I absolutely love it because this is exactly the kind of challenge that gets me excited about iOS development. It's like being handed a classic car that needs a complete restoration. Sure, it's going to be a lot of work, but the end result is going to be amazing. So my weekend review. So what I actually have been working on since September 25th, let me break it down into major themes. So those are the app delicate cleanup and modern architecture, working with the existing feature flag system, walkie-talkie and PTT integration, chat system refactoring, location tracking modernization. Okay, let's get started. First one. App Delicate Cleanup and Modern Architecture. One of the biggest refactoring efforts I've been leading is breaking down the monolithic app delicate. You know how it is. Over the years, the app delicate becomes this massive file that handles everything from authentication to notifications to background tasks. It's like that one friend who always volunteers to organize everything at a party. They end up doing everything and eventually they burn out. I've been extracting concerns into dedicated managers like hiring a team of specialists instead of relying on one overworked generalist. An app lifecycle manager handles foreground background transitions like a bouncer at a club. Background task manager manages background task registration and scheduling, it's more like a project manager. Authentication coordinator handles all biometric and authentication logic, it's the security card. Crash Reporting Manager manages Sentry app and uses tracking like detective collecting evidence. The key insight here is that we've been moving from a single massive class to a collection of focused testable components. Each manager has a single responsibility, making the code much cleaner to understand and maintain. It's like going from a Swiss Army knife to a proper toolkit. So the second thing I've been dealing with is working with the existing feature flag system. The app already had a massive feature flag system in place, and I've been leveraging it extensively for my refactoring work. Think of it like having a dimmer switch for your code. You can turn features up or down without rewiring the entire house. This is crucial when you're dealing with a critical app that you can't afford to have any downtime on. It's like having an emergency break on the train. You hope you never need it, but when you do, you'll be really glad it's there. The feature flags I've been working on were the following one: Refactor Alert Detail Screen for the new alert detail implementation, a refactor alert statistic screen for the statistics screen rewrite, and a refactor flick buttons for the flick management refactor. And then there's the async location tracking for the new iOS 17 and 18 location tracking. This allows me to test new implementations alongside the old ones, ensuring I can roll back quickly if something goes wrong. It's like having a safety net when you're walking a tightrope, really. And the third thing I've been working on is the walkie-talkie and push-to-talk integration. One of the most complex features I've been working on is the push-to-talk integration for the walkie-talkie functionality. This involves real-time audio streaming over WebSockets, which is incredibly challenging to get right. It's like trying to have a conversation through a walkie-talkie while riding a roller coaster, everything is moving, everything is changing, and you need to keep the connection stable. Let me tell you about the debugging nightmare I had last week. I was getting reports that the walkie-talkie was cutting out mid-conversation, but only on certain devices. After hours of debugging, I discovered that the audio engine was setting up multiple times, causing conflicts. And it also caused the MP volume fuel to literally move around on the screen because of this. That was the issue I talked about earlier. It was like having a volume slider that had a mind of its own. And the key challenges I've been solving is the audio streaming, encoding and decoding audio in real time, like trying to translate a conversation while it's happening, web socket management, handling connection states, reconnection logic, and retry mechanisms, like having a backup plan for every possible scenario, state management, tracking recording state, connection state, and audio playback, like keeping track of who's talking, who's listening, and what's happening. And then of course there's thread safety, ensuring that audio processes happen on the right threads, like making sure that the right people are in the right rooms at the right time. I've consolidated the audio engine setup to solve this MP volume view movement issues I had and implemented the retry mechanism that attempts reconnecting up to five times instead of just two. The audio buffering system now runs in separate threads to prevent clipping and ensure smooth playback. It's like having a backup sound system that kicks in when the main one fails. Another thing I've been working on was a chat system refactoring. I've been completely rewriting the chat system to use a modern service-based architecture. The old system uses a singleton MB chat manager that was tightly coupled to the UI. It's like having a chat system that was permanently glued to the screen. The new chat service is much more modular and testable, like having a chat system that can be plugged into any device. And the key improvements that I've been working on are an async await support, modernizing the matrix SDK integration and better error handling so that proper error types and recovery mechanisms are in place, and thread safety, again thread safety, so to make sure that the matrix SDK that we rely on is used safely across threads and making sure that everyone follows the same traffic rules there. And I also wanted to introduce some more separation of concerns. The service handles all the matrix logics while the UI focuses on presentations, like having a chef who cooks and a waiter who serves. Everybody has their own specialized task and we don't walk in front of each other. The location tracking system has been completely overhauled to use iOS 17 and 18's new async location tracking APIs. This is a perfect example of how iOS evolves and how you need to adapt your code. It's like upgrading from a paper map to GPS. The old system worked, but the new one is much better. The new system includes geofencing, processing, using the new CL monitor API for better performance, like having a security card who never sleeps, and background task manager management, so background task management, proper background task handling for location updates like having a reliable assistant who always remembers to check in. Debouncing, preventing to prevail prevent excessive server calls when driving, because otherwise we get like an update every five meters, and especially if you're at speed, that's a bit much. And distance filtering so that we only send location updates when the user has moved significantly. So that's just to make sure that we don't uh saturate the server. Alright, let's dive into the details a little bit more because this is like a high-level overview. Code Deep Dive, the App Delicate Refactor. Uh let me show you a specific example of the refactoring work that I've been doing. This is the kind of real-world problem that every iOS developer faces when working with legacy codes. So the problem here. The original app delicate was over 500 lines long and handled everything. It was like a Swiss armor knife that had grown into a full toolbox. Imagine if your kitchen had one giant appliance that was supposed to be your refrigerator, your stuff, your dishwasher, your microwave, and your coffee make all rolled into one. That's what our app delicate had become. I remember the first time I opened the file, I was like, nope, what the hell is this? It was handling authentication and biometric login, push notification registration, background task management, deep linking, security checks, feature flag refreshing, and then some other things. This made it incredibly difficult to test, understand, and maintain. It was like trying to fix a car when all the parts were welded together. You couldn't work on one thing without affecting everything else. Every time I made a change, I was holding my breath, wondering if I might have broken something on the other side of the whole machine. So the solution that I came up with was that I broke everything down into focus managers. The new app lifecycle manager is much simpler, it just coordinates between the other services. When the app comes to the foreground, it refreshes feature flags, syncs device info, and updates the session manager if needed. When it becomes active, it posts notifications and runs security checks. The key insight is that each manager has a single clear responsibility. The app lifecycle manager doesn't know how to handle background tasks, it just knows when to tell the other services to do their job. It's like having a conductor who doesn't play any instruments but knows when each section should come in. The benefits of this is that first of all we have testability. Each manager can be tested in isolation, like having separate test tracks for each car component. Maintainability, changes to one concern don't affect others, like having separate rooms in a house. Readability, the code is self-documenting, like having clear labels on everything, and there's some more reusability. Managers can be used in different contexts, like having modular furniture that works in any room. So the challenges that ever was facing was that the biggest challenges was ensuring that the refractor didn't break existing functionality. This is where the feature flag system became crucial. I could test the new implementation alongside the old one, ensuring a smooth transition. It's like having uh renovation in your house while you're still living in it. You need to make sure that the electricity and the water still work while you're replacing the plumbing. Alright, so let's dive into feature flags a little bit because I've mentioned them a full few times over. Um the feature flag system that's already in place in this code base, it's like a tool that becomes essential for modern iOS development, especially when dealing with legacy code. Think of it like having a remote control for your code. You can turn features on and off without touching the code itself. The code base already had a well-designed feature flag system in place. It's built around an enum that defines all the different features with names like refactor alert detail screen and async location tracking. Each feature has a unique identifier and the default value. So, how did I use it? In the UI code, I can conditionally use new implementations. For example, when someone taps on alert, I check if the new alert detail screen feature is enabled. If it is, I create a new view controller with the modern MVM architecture. If not, I fall back to the old storyboard-based implementation. This pattern is repeated throughout the app. Every time I want to use a refacted component, I first check the feature flag. It's like having a switch that determines which version of the code gets executed. So why does this matter? Feature flags allows you to deploy safely, test new code in production without affecting all users, like having a test kitchen in a restaurant, and you can roll back quickly. If something goes wrong, I can disable the feature instantly, like having an emergency stop button. Also, it allows for A-B testing, so you can compare the old versus the new implementations, like having two different recipes and seeing which of the which of these the customers prefer, and it allows for gradual rollouts, so enabling features for a subset of users first, like having a soft opening before the grant opening. This is especially important for critical apps like the ones I'm working on, where downtime could literally cost lives. It's like having a backup generator for a hospital. You hope you never need it, but when you do, it's the difference between life and death. The existing system has been a lifesaver for my refactoring work. Instead of having to deploy everything at once and hope for the best, I can gradually migrate users to the new implementation while keeping the old code as a safety net. So the lessons that I learned, um, legacy code is a gift. That's the first lesson. Working with legacy code isn't a burden, it's an opportunity to learn. Every piece of technical debt tells a story about how IRS development has evolved. The old patterns made sense at the time, and understanding why they were chosen helps me make better decisions today. It's like reading a history book, you can see how we got to where we are now, and it helps you understand where you're going to need to be going. I've actually started to appreciate the craftsmanship that went into this code, even if it's outdated. The developers who built this eight years ago were working with the tools and patterns they had available. They made the best decisions they could with the information they had back at the time. That's something I try to remember when I refactored their work. Refactoring also requires patience, that's the second lesson. You can't refactor everything at once. The key is to identify the most critical areas and tackle them systematically. The app delicate refactor took well over a week, but each step made the code base a little better. It's like renovating an old house, you can't tear it all down at once, you have to do it room by room. I had to resist the urge to just rewrite everything from scratch. That could have been faster in the short term, but it would have been significantly more risky for a critical app like this one. Instead, I took the slow and steady approach, making small incremental improvements that I could test and validate at each step. So the third lesson: testing is everything. When refactoring critical functionality, testing becomes your safety net. The feature flex system allows me to test new implementations in production without risking the entire app. It's like having a safety harness when you're rock climbing. You hope you never need it, but when you do, it's the difference between a minor slip and a major fall. I learned this the hard way early on. I made a change to the authentication flow without properly testing it, and it broke login for a subset of users. That was a wake-up call. Now I test everything extensively. And I use the feature flag system to gradually roll out changes to a small percentage of users first. Then the fourth lesson is modern iOS apps are worth the effort. Sorry, I need to say modern iOS APIs are worth the effort. The new location tracking API in iOS 17 or 18 is significantly better than the old ones. Yes, it requires rewriting existing code, but the performance and reliability improvements are worth it. It's like upgrading from a flip phone to a smartphone. Sure, you have to learn new ways of doing things, but the capabilities are so much better that it's worth the effort. The old location tracking code was a mass of callbacks and delicate methods. The new async await APIs are so much cleaner and easier to reason about. It took me a while to get used to the new patterns though, but now I can't imagine going back to the old way of doing things. Okay, so let's look ahead a little bit. So what's coming up in the next two weeks? I'm not exactly sure, but I expect it will be something to do with the chat system. I'm refactoring it and I'm now 80% done. The remaining work involves migrating the remaining UI components to the new chat service and in comprehensive error handling, implementing proper offline support. It's like finishing the last few rooms in the house renovation. The foundation is solid, but there's some finishing work to do. The chat system is one of the most complex parts of the app, so I'm taking my time to get it right. I also want to finish the alert detail screen refactor. The new alert detail screen is much more complete. It also uses a modern MVVM architecture with proper separation of concerns. The remaining work is mostly UI polish and testing. It's like putting the final coat of paint on a car. The engine is running great, but you want to make sure that it definitely looks good. This screen is critical because it's where users get detailed information about emergency alerts. The old version was clunky and hard to navigate, especially in high stress situations. The new version is much more intuitive and responsive. I'm also going to continue the location tracking modernization. The new location tracking system is working well, but I want to add more sophisticated UFencing logic and improve the background task management. It's like having a GPS that works, but now I want to make sure that it's smart enough to avoid traffic jams. So location tracking is crucial for this app because it's used to determine who's in the area when an emergency happens. The more accurate and reliable it is, the better the emergency response can be. And then of course there's my cleanup branch, because I've been doing this on the side. I've also been working on a massive cleanup branch that's been running in parallel. That's the kind of work that doesn't get much attention but is absolutely crucial for maintaining a healthy code base. It's like doing uh spring cleaning, not glamorous but necessary. I actually enjoy this kind of work. There's something satisfying about removing that code and fixing warnings. It's like cleaning up your workspace. You feel more productive when everything is organized and tidy. This cleanup work includes removing deprecated APIs, getting rid of the old API that we don't longer want to support. I've been just fixing warnings because there were like hundreds of warnings. So cleaning up compiler warnings and deprecation notices, removing that code, eliminating code that's no longer used anywhere anywhere. So there were large chunks of code that were not being called into. And I wanted to modernize a few of the patterns that were used. So updating old patterns to use modern Swift UI features and Swift features. You don't notice it when it's working, but when you but you'll definitely notice when it's not. So there was also a bug fix I needed to do, uh Wi-Fi settings related. I've been working on this bug fix for the Wi-Fi settings not being configurable. Uh so it used to be that this worked, but now with iOS 26 it doesn't. So this is a perfect example of how legacy code can have subtle issues that only serves when you're doing other refacting work. The Wi-Fi management system was using deprecated APIs and had some threading issues that were preventing users from properly configuring their Wi-Fi networks for presence tracking. So here's the story. I got a support ticket from a user who couldn't configure their Wi-Fi network. They were on iOS 26, and the Wi-Fi setting screen was completely broken. At first I thought it was a UI issue, but after digging deeper, I discovered something much more interesting. The issue was that the app was still using the old Wi-Fi API that was deprecated since iOS 14. Specifically, it was using CN copy current network info from the system configuration framework, which was deprecated in iOS 14. And here's the kicker. It seems like this API stopped working completely in iOS 26, so users on the latest iOS versions couldn't configure their Wi-Fi networks at all, which is pretty critical for an emerging response app that relies on presence tracking, right? So the fix involved completely modernizing the Wi-Fi validation system, moving away from the deprecated C and copy current network info API to the modern NEHotspot Network.fetchCurrent API that's been available since iOS 14. This new API uses uh well uses uh completion handling, but I wrapped it in an async await pattern, which is much more reliable. I also had to ensure that the UI updates happen on the main thread and fix some race conditions in the validation logic. The key change was replacing the old synchronous API call with a new async version and wrapping it in a proper continuation to handle the completion-based API. It's a perfect example of how Apple's API evolution requires us to constantly update our codes to code to stay compatible. It's like fixing a squeaky door. It's not the most exciting work, but it makes a huge difference in the user experience, and it's a great reminder of why keeping up with API deprecations is so important, especially when you're dealing with critical functionality. So, and that's a wrap on the second App Force One worklock episode. I hope this gives you a real sense of what it's like to work on a complex legacy iOS app in 2025. The key takeaways is that uh refactoring legacy code isn't just about making it modern, it's about understanding the business context, the technical the technical constraints and the user impact. Every decision I make was has real consequences for people who depend on this app in emergency situations. It's like being a surgeon, you can't just focus on the technical aspects, you have to remember that there's a real person on the operating table. In the next episode, I'll dive deeper into the walkie-talkie audio streaming most likely, and share some of the debugging challenges I am definitely going to be facing in the next week. I'll also talk perhaps about the Matrix SDK integration and how I'm handling real-time management messaging. Think of it like a behind-the-scenes look on how the magic happens. Before I wrap up, I want to mention that I'm organizing a conference called Do iOS, which is happening later this year. It's going to be an amazing event focused on iOS development, which talks uh with talks from some of the best developers in the community. If you're interested in learning more about iOS development, networking with other developers, and getting inspired by the latest trends and techniques, definitely check out do iOS.com for more information on the tickets. Make sure to check the link in the show notes. So, until next time, keep building amazing apps and remember every expert was once a beginner. The key is to keep learning, keep building, and keep sharing what you learn with the community. Thank you for listening, and I'll see you next week for the next worklock episode. I'm really excited to share more of this journey with you.

People on this episode