इस एपिसोड के बारे में
Make sure to let me know what you think of this episode.
I completely refactored an audio system for a work app, splitting a single AVAudioEngine into separate engines for recording and playback. This architectural change fixed a bizarre bug where the system volume slider moved unexpectedly during audio operations.
• Split AVAudioEngine into separate recording and playback engines
• Fixed the MP Volume View movement issue by unifying audio session management
• Improved background task management for location tracking services
• Removed dead code and deprecated functionality
• Explored solutions for audio session conflicts, threading issues, and memory leaks
• Implemented dedicated dispatch queues for different audio operations
• Created a robust background task management system for location updates
• Added extensive logging to better understand audio session lifecycles
Looking ahead to SwiftUI integration, audio performance optimization, and iOS 26 compatibility testing. Do iOS 2025 is happening November 11-13 at NEMO Science Museum in Amsterdam - check out do-ios.com for more information.
Do iOS: https://do-ios.com
Rate me on Apple Podcasts.
Send feedback on SpeakPipe
Or contact me:
- Mastodon: https://hachyderm.io/@appforce1
- X: https://x.com/appforce1
- BlueSky: https://bsky.app/profile/appforce1.net
- LinkedIN: https://www.linkedin.com/in/leenarts/
Support my podcast with a monthly subscription, it really helps.
My book: Being a Lead Software Developer
नोद्स दिखाएं 🔗
अनुलिपि 🔗
00:00:01.745 --> 00:00:02.649
All right, let's get started.
00:00:02.649 --> 00:00:06.850
Ios Development Worklog, episode 105.
00:00:06.850 --> 00:00:09.929
Audio Engine Refactoring and Background Task Management.
00:00:09.929 --> 00:00:13.369
Welcome to the first episode of my iOS Development Worklog.
00:00:13.369 --> 00:00:19.268
I'm Jeroen Leenarts and this is where I'll share the real work I've been doing, the challenges I've faced and the lessons I've learned.
00:00:19.268 --> 00:00:26.309
No fake demos, no oversimplified examples, just honest insights from building real iOS applications.
00:00:26.309 --> 00:00:27.603
Let's get started.
00:00:28.160 --> 00:00:36.149
The Week in Review this week was all about audio engineering and background task management, two areas that are notoriously tricky in iOS development.
00:00:36.149 --> 00:00:39.328
Let me break down what actually shipped and what didn't.
00:00:39.328 --> 00:00:40.201
What shipped?
00:00:40.201 --> 00:00:51.164
Audio Engine Refactoring I completely refactored the audio system in the app I'm working on for my job, splitting the single AV audio engine into separate engines for recording and playback.
00:00:51.164 --> 00:00:58.890
This was a major architectural change that touched multiple components File files changed, 256 insertions, 44 deletions.
00:00:58.890 --> 00:01:04.067
The refactoring included creating separate recording engines and playback engine.
00:01:04.067 --> 00:01:08.594
Instancesing a unified audio session helper to manage audio session state.
00:01:08.594 --> 00:01:12.811
Fixing the MP Volume View movement issue that was driving users crazy.
00:01:12.811 --> 00:01:16.971
Improving the walkie-talkie service integration with the new audio architecture.
00:01:16.971 --> 00:01:19.365
Mp Volume View fix.
00:01:19.365 --> 00:01:22.009
This was actually the most satisfying fix of the week.
00:01:22.009 --> 00:01:28.379
The system volume slider was moving unexpectedly during audio operations and users were reporting it as a bug.
00:01:28.379 --> 00:01:32.055
The root cause was multiple audio engines fighting over audio session control.
00:01:32.055 --> 00:01:35.608
By unifying the audio session management, I eliminated the conflicts.
00:01:35.608 --> 00:01:45.632
Background task management, improved our location tracking service to properly handle background tasks, preventing iOS from killing our location updates when the app goes to background.
00:01:45.632 --> 00:01:51.692
This involved implementing proper background task lifecycle management and preventing duplicate background tasks.
00:01:52.280 --> 00:01:53.585
And there was also a code cleanup.
00:01:53.585 --> 00:02:07.466
I removed some dead code and deprecated functionality, including an unused MP4M Plus slider view extension, cleaned up deprecated audio session code code and I removed a few unnecessary main actor annotations that were causing threading issues.
00:02:07.466 --> 00:02:09.866
Then there's also some stuff that didn't ship.
00:02:09.866 --> 00:02:12.967
I was working on an audio session optimization.
00:02:12.967 --> 00:02:20.789
I spent considerable time trying to optimize audio session management, but some of the changes introduced new issues, so I had to roll back parts of that work.
00:02:20.789 --> 00:02:23.025
Sometimes the best code is the code you don't write.
00:02:23.025 --> 00:02:28.177
Performance improvements I had some ambitious plans for audio buffer optimization that didn't pan out.
00:02:28.177 --> 00:02:35.526
The current implementation is actually performing better than my optimized version, which is a good reminder that premature optimization is still the root of all evil.
00:02:36.427 --> 00:02:44.443
Swiftui integration I planned to start integrating SwiftUI into the existing UIKit app, but the audio refactoring took priority and consumed most of the week.
00:02:44.443 --> 00:02:50.985
So the honest assessment this week was frustrating in the best way possible.
00:02:50.985 --> 00:02:55.019
Audio on iOS is genuinely hard and every simple fix seems to introduce three new problems.
00:02:55.019 --> 00:02:59.820
But that's exactly why I want to share this with you, because this is what real iOS development looks like.
00:02:59.820 --> 00:03:03.088
The most challenging part was debugging the MPVolumeView issue.
00:03:03.088 --> 00:03:08.688
It's one of those bugs that's hard to reproduce consistently, but when it happens it's immediately obvious to users.
00:03:08.688 --> 00:03:14.509
The fix required understanding the entire audio pipeline and how different components interact with the system audio session.
00:03:14.509 --> 00:03:18.086
So let's dive in Code, deep dive to audio engine split.
00:03:18.086 --> 00:03:25.901
Let me walk you through the biggest technical change I tackled this week splitting the single AV audio engine into separate recording and playback engines.
00:03:25.901 --> 00:03:32.306
The problem the app has a walkie-talkie feature that needs to handle both recording and playback simultaneously.
00:03:32.568 --> 00:03:39.189
The original implementation used a single AV audio engine for both operations, which was causing several issues Audio session conflicts.
00:03:39.189 --> 00:03:41.506
The single engine was fighting with system audio controls.
00:03:41.506 --> 00:03:42.731
Performance issues.
00:03:42.731 --> 00:03:44.802
Recording and playback were interfering with each other.
00:03:44.802 --> 00:03:49.608
Mp volume view movement the system volume slider was moving unexpectedly during operations.
00:03:49.608 --> 00:03:54.308
Threading issues audio operations were happening on different threads without proper coordination.
00:03:54.308 --> 00:03:55.693
Memory management.
00:03:55.693 --> 00:03:58.443
The single engine was creating complex retained cycles.
00:03:58.443 --> 00:04:00.747
The solution architecture.
00:04:00.747 --> 00:04:10.268
So here's how I approached this refactoring Instead of having one audio engine trying to do everything, I created two separate engines One specifically for recording and another for playback.
00:04:10.659 --> 00:04:18.026
In the AudioManager class I now have two private properties RecordingEngine and PlaybackEngine both instances of AVAudioEngine.
00:04:18.026 --> 00:04:21.848
This separation allows each engine to be optimized for a specific purpose.
00:04:21.848 --> 00:04:25.512
For recording, I have properties like IsRec to track data.
00:04:25.512 --> 00:04:31.771
Is tap installed to know if I have setup the audio tap and bus number, which is always zero for the input bus.
00:04:31.771 --> 00:04:43.408
For playback, I have a player node, which is an AV audio player node, a mix node for audio mixing and an input converter for handling different audio formats.
00:04:43.939 --> 00:04:47.379
The key insight here is that each engine can now be configured independently.
00:04:47.379 --> 00:04:52.326
The recording engine can be optimized for low latency input, while the playback engine can be optimized for smooth output.
00:04:52.326 --> 00:04:55.067
This eliminates the conflicts we were seeing before.
00:04:55.067 --> 00:05:01.072
The key insight was when the BreakTool came, when I realized that AV Audio Engine is designed to be specialized.
00:05:01.072 --> 00:05:03.266
Each engine should have a single, clear responsibility.
00:05:03.266 --> 00:05:07.709
By separating recording and playback, each engine could be optimized for its specific use case.
00:05:07.709 --> 00:05:12.711
But here's the crucial part the engines still need to share the same audio session.
00:05:12.711 --> 00:05:17.130
This is where the complexity lies and why the MP Volume Fuel was moving unexpectedly.
00:05:17.579 --> 00:05:18.723
The implementation details.
00:05:18.723 --> 00:05:21.850
I will now walk you through how this actually works in practice.
00:05:21.850 --> 00:05:32.209
For the recording engine, when we start recording, the start recording function first checks if you have already recorded, if you're already recording to prevent duplicate operations.
00:05:32.209 --> 00:05:39.949
Then it calls setup recording engine, which configures the engine for offline rendering with a 4096 byte frame buffer.
00:05:39.949 --> 00:05:43.524
That gives us enough good performance without overwhelming the system.
00:05:43.524 --> 00:05:48.351
And the key part is the install recording tab, which sets up a tab on the input node.
00:05:48.351 --> 00:05:56.435
This tab captures audio data as it flows through the engine and calls our process recording buffer function each time with each audio buffer.
00:05:56.435 --> 00:05:59.730
Think of it like installing a microphone at distance on the audio stream.
00:05:59.730 --> 00:06:07.002
For the playback engine, the set the player function creates an AV audio player node, attaches it to the playback engine and connects.
00:06:07.002 --> 00:06:08.949
Setupplayer function creates an AVAudioPlayer node, attaches it to the playback engine and connects it to the main mixer node.
00:06:08.949 --> 00:06:11.572
This creates the audio graph that allows us to play audio.
00:06:11.572 --> 00:06:23.184
The playback engine is configured for real-time rendering with a smaller 1024 frame buffer, which is perfect for smooth playback without the latency concerns we have with recording.
00:06:23.184 --> 00:06:29.425
The beautiful thing about the separation is that each engine can be started, stopped and configured independently.
00:06:29.425 --> 00:06:34.723
We can be recording on one engine while playing back on the other, and they won't interfere with each other.
00:06:35.300 --> 00:06:41.098
The debugging process to get to this conclusion this wasn't a smooth implementation because there went a lot of things wrong.
00:06:41.098 --> 00:06:42.163
And this is how I debugged it.
00:06:42.163 --> 00:06:44.668
So first of all, there were audio session conflicts.
00:06:44.668 --> 00:06:47.149
The engines were fighting over audio session control.
00:06:47.149 --> 00:06:48.826
Second of all, there were some threading issues.
00:06:48.826 --> 00:06:51.267
Recording and playback were happening on different threads.
00:06:51.267 --> 00:06:52.444
Then there were some memory leaks.
00:06:52.444 --> 00:07:02.254
Of course the separate engines were creating retain cycles and of course we had this visual issue with the MP volume view tab moving if we started recording.
00:07:02.254 --> 00:07:10.494
So the debugging strategy that I used was to use AV audio engines built-in logging to track engine state.
00:07:10.494 --> 00:07:13.202
I added extensive logging to understand the audio session lifecycle.
00:07:13.202 --> 00:07:16.992
I used instruments to profile memory uses and identify leaks.
00:07:16.992 --> 00:07:20.591
I created a test harness to reproduce the MP volume view issue consistently.
00:07:22.221 --> 00:07:24.747
So let's dive into this MP volume view mystery.
00:07:24.747 --> 00:07:27.783
This was the most frustrating part of all the MP volume view.
00:07:27.783 --> 00:07:31.583
The system volume slider was moving unexpectedly during audio operations.
00:07:31.583 --> 00:07:33.932
Let me explain a little bit what the MP volume view is.
00:07:33.932 --> 00:07:41.033
It's basically a simple view that you can put on your screen and that attached itself automatically to the system volume.
00:07:41.033 --> 00:07:53.968
But depending on what your output channel is, so that's like the like the, the earpiece of the of the iphone, or the speaker of the of the iphone, or a bluetooth headset or connected headset, they all have different volumes.
00:07:53.968 --> 00:08:06.930
So if you change something that adjusts the channel that the output is on, the mp volume view will follow that and clearly indicate what volume the playback channel that you currently have selected is on.
00:08:06.930 --> 00:08:13.482
And that was causing the jumping around because we were not being consistent with the output channel that we were choosing.
00:08:13.482 --> 00:08:20.002
So it took me a few hours actually to be able to explain this to you in a few sentences.
00:08:20.002 --> 00:08:22.728
So that was a bit of a challenge.
00:08:22.968 --> 00:08:27.024
So multiple engines were audio engines were trying to control the audio session.
00:08:27.024 --> 00:08:35.220
Each engine had settings that were different and were using different audio session properties, and the system was interpreting these changes as user input.
00:08:35.220 --> 00:08:39.855
The fix was to ensure that only one component manages the audio session state.
00:08:39.855 --> 00:08:45.027
So the final solution that I came up with was when I had this breakthrough of understanding.
00:08:45.027 --> 00:08:48.913
So when I unified, the audio session manager called the audio session helper.
00:08:48.913 --> 00:08:56.059
It is a singleton yes, I know that has both of the engines and uses it to manage a shared audio session.
00:08:56.059 --> 00:09:03.447
And here's the key insight Instead of each engine trying to configure the audio session independently, they all could go through the central manager.
00:09:03.447 --> 00:09:10.027
The manager tracks the current state, what category is set, what options are configured, what sample rate is being used and whether the session is active.
00:09:10.629 --> 00:09:14.224
The critical part is in the setup walkie-talkie audio session function.
00:09:14.224 --> 00:09:24.423
Before making any change to the audio session, it checks if the setup is already in progress to prevent race conditions and then it only reconfigures the session if something has actually changed.
00:09:24.423 --> 00:09:27.509
This is what fixed the MP Volume Slider issue.
00:09:27.509 --> 00:09:36.328
The problem was that multiple engines were constantly reconfiguring the audio engine even when nothing had changed.
00:09:36.328 --> 00:09:38.399
Ios was interpreting these unnecessary changes as user input, which caused the volume slider to move.
00:09:38.399 --> 00:09:44.524
Now the session only gets reconfigured when it actually needs to be and both engines share the same session state.
00:09:44.524 --> 00:09:52.130
The walkie-talkie option includes things like mixing with other audio, defaulting to speaker and allowing Bluetooth connections All the settings we need for a walkie-talkie app.
00:09:52.130 --> 00:09:54.927
So the walkie-talkie service integration.
00:09:55.288 --> 00:10:00.991
The walkie-talkie service also needed some updates to work with a new audio architecture for Push to Talk.
00:10:00.991 --> 00:10:04.764
This service acts as the coordinator between the audio manager and the rest of the app.
00:10:04.764 --> 00:10:11.123
One of the most important changes was creating separate dispatch queues for different audio operations.
00:10:11.123 --> 00:10:17.203
I have three dedicated queues one for receiving audio data, one for sending audio data and one for playing audio.
00:10:17.203 --> 00:10:23.307
Each queue uses user-initiated as the quality of service, which gives audio operations priority over other background tasks.
00:10:23.307 --> 00:10:26.232
This is crucial because audio is very time sensitive.
00:10:26.232 --> 00:10:30.307
If audio processing gets delayed or interrupted, you get clipping, stuttering or dropped audio.
00:10:30.307 --> 00:10:35.147
By giving audio operations their own high priority cues, we ensure smooth performance.
00:10:35.147 --> 00:10:40.784
The services begin audio recording and end audio recording functions are now much simpler.
00:10:40.784 --> 00:10:45.024
They just call the corresponding methods on the audio manager and update the walkie-talkie state.
00:10:45.024 --> 00:10:56.423
The complexity is hidden in the audio manager and this is an architectural principle to hide the complexity, which makes the service easier to test and also easier to maintain.
00:10:56.423 --> 00:11:02.543
And the key insight here is that the audio operations need dedicated cues to prevent clipping and ensure smooth operation.
00:11:02.543 --> 00:11:08.014
Using user-initiated quality of service ensures that audio operations get priority over background tasks.
00:11:10.039 --> 00:11:14.683
Then there was also something that I did, that what I like to call the tool talk background task management.
00:11:14.683 --> 00:11:19.543
This week I also dove into background task managers for the location tracking that the app does.
00:11:19.543 --> 00:11:27.892
This is one of those iOS topics that seems simple, but it's actually quite complex, especially when you're dealing with location services that need to run continuously.
00:11:27.892 --> 00:11:35.250
So the challenge was that our app needs to track location even when it's in the background, but iOS is very aggressive about killing background tasks.
00:11:35.250 --> 00:11:41.169
The challenge is managing the lifecycle of background tasks properly while ensuring that location updates continue to work reliably.
00:11:41.169 --> 00:11:49.591
The specific issues we were facing we had some duplicate background tasks, multiple location updates for starting new background tasks without ending previous ones.
00:11:49.591 --> 00:11:51.868
We had task expiration.
00:11:51.868 --> 00:12:03.671
Ios was killing our background tasks before location updates were completed, memory leaks, background tasks weren't being properly cleaned up and there was also a performance impact because too many background tasks were impacting the app performance.
00:12:03.671 --> 00:12:12.730
So the solution that came up is to create a robust background task management system with proper lifecycle management in a class we call the MBLocationService.
00:12:13.139 --> 00:12:19.543
The key is having a single background task idea property that tracks whether we have an active background task when we need to do location work.
00:12:19.543 --> 00:12:23.192
The startBackgroundTask function first checks if there's already a task running.
00:12:23.192 --> 00:12:25.224
If there is, it skips creating a new one.
00:12:25.224 --> 00:12:26.727
If we need to start a new task.
00:12:26.727 --> 00:12:33.253
It calls your application shared beginBackgroundTask, with a descriptive name, location update and an expiration handler.
00:12:33.253 --> 00:12:35.207
This expiration handler is crucial.
00:12:35.207 --> 00:12:42.432
It gets called if iOS decides to kill the background task before you're done and it ensures you can actually still clean up everything properly.
00:12:42.432 --> 00:12:45.288
The endBackgroundT task function is equally important.
00:12:45.288 --> 00:12:51.985
It checks if we have a valid task ID, calls end background task to tell iOS we're done and resets the task ID to invalid.
00:12:51.985 --> 00:12:58.394
The update location function ties it all together Start the background task, do the location work, then end the background task.
00:12:58.394 --> 00:13:03.851
This ensures that location updates can complete even when the app is in the background, but we're not hogging system resources.
00:13:05.504 --> 00:13:07.375
So why were we doing this background task work?
00:13:07.375 --> 00:13:08.399
What was actually happening?
00:13:08.399 --> 00:13:11.908
We were getting a background push notification with a location update.
00:13:11.908 --> 00:13:37.683
So that's usually when you enter a geographic region or you exit the geographic region, and one of the things that this implementation was doing was actually ask the system for an exact gps location and then, because of the way this ap works, the processing of the function that gets called is done, because you ask the system to ping the GPS and get you a location, but you get the information through a callback.
00:13:37.683 --> 00:13:45.523
So what we did was, before we return from this push notification callback, we actually start the background task.
00:13:45.523 --> 00:13:49.764
Then we end this function, then we end the push notification handling.
00:13:49.764 --> 00:13:53.023
We have asked the system for a GPS update.
00:13:53.023 --> 00:13:57.283
We return Then because the background task is active.
00:13:57.534 --> 00:14:00.337
The operating system keeps the app in memory.
00:14:00.337 --> 00:14:07.054
The GPS gets the location and calls back into our application to report the location.
00:14:07.054 --> 00:14:16.275
Through these GPS callback functions we get the GPS, we process that, then we enter task and then return.
00:14:16.275 --> 00:14:21.748
What now happens is that the app gets started through one push notification call.
00:14:21.748 --> 00:14:23.475
A background task is started.
00:14:23.475 --> 00:14:24.841
This keeps the app alive.
00:14:24.841 --> 00:14:28.065
Something happens in the background while we already return from the first call.
00:14:28.065 --> 00:14:40.447
Then another callbacks get called and that actually clears the background task out of memory so that the operating system now knows okay, now it's safe to shut down the app again so we can start a proper shutdown of the application.
00:14:42.217 --> 00:14:50.683
So we needed to make sure that we only started a single background task and not create a new background task if there was already a GPS ping running.
00:14:50.683 --> 00:14:55.363
So the startBackgroundTask function uses a guard statement to check if we already have an active task.
00:14:55.363 --> 00:14:58.360
If we do, it logs a message and skips creating a new one.
00:14:58.360 --> 00:15:02.328
This prevents duplicate task creation and avoids a big headache for us.
00:15:02.328 --> 00:15:04.138
So and then the cleanup.
00:15:04.138 --> 00:15:06.163
The end background task function is defensive.
00:15:06.163 --> 00:15:09.399
It checks if we have a valid task ID before trying to end it.
00:15:09.399 --> 00:15:15.522
It logs the task ID for debugging, calls the system's background task method and resets the internal state.
00:15:16.423 --> 00:15:18.028
And, of course, there's the expiration handling.
00:15:18.028 --> 00:15:20.823
The expiration callback is where the magic happened.
00:15:20.823 --> 00:15:25.866
If iOS decides a background task has run for too long, it calls this callback.
00:15:25.866 --> 00:15:32.101
We use a weak self-reference to avoid retained cycles and we ensure cleanup happens even if the task gets killed unexpectedly.
00:15:32.101 --> 00:15:40.929
So this three-part approach start work and ensures that the background tasks are managed properly throughout the entire lifecycle.
00:15:41.255 --> 00:15:43.062
I hope this was an understandable explanation.
00:15:43.062 --> 00:15:53.514
So the reason this approach works is that we have a single background task that keeps the app alive while the GPS ping is running, and it also prevents the app from being terminated.
00:15:53.514 --> 00:15:56.004
Of course we are neat citizens on iOS.
00:15:56.004 --> 00:15:59.817
We want to do proper cleanup when we have the opportunity.
00:15:59.817 --> 00:16:05.129
This prevents memory leaks and resources being retained for too long.
00:16:05.129 --> 00:16:07.899
This also happens with the expiration handling.
00:16:07.899 --> 00:16:12.559
We've added extensive logging because we want to actually, because it's happening in the background.
00:16:12.600 --> 00:16:15.216
You can't really see and tell in the app what is happening.
00:16:15.216 --> 00:16:26.461
So you have to base your conclusions and your observations on what's appearing in log files and, where appropriate, we used weak reference enclosures to prevent retained cycles.
00:16:26.461 --> 00:16:30.735
So the tool that we used was the UI application background task.
00:16:30.735 --> 00:16:36.596
The key tool was the UI application shared object and then the begin background task function on that.
00:16:36.596 --> 00:16:41.769
This gives you a limited amount of time, usually about 30 seconds, to complete a background piece of work.
00:16:41.769 --> 00:16:46.307
So some best practices here always end background tasks when you're done.
00:16:46.307 --> 00:16:49.019
Don't start multiple background tasks if you can help it.
00:16:49.019 --> 00:16:55.379
Handle the expiration callback properly and use descriptive names where possible, because this really aids in debugging.
00:16:55.379 --> 00:16:58.408
And make sure that you do proper memory management.
00:16:58.554 --> 00:17:07.284
So weak references where appropriate and make sure that you clean things up and log background task lifecycle for debugging so that you can actually see what is happening.
00:17:07.284 --> 00:17:13.044
So this background task management system integrates seamlessly with the location service.
00:17:13.044 --> 00:17:21.827
When the CL location manager calls did update locations, we extract the most recent locations, start a background task process, the location update and then end the background task.
00:17:21.827 --> 00:17:29.638
The processLocationUpdate function does three things it updates an internal last known location property.
00:17:29.638 --> 00:17:39.919
Notifies iDelegate about the new location so it can be sent to the server and it updates the location tracking state server and it updates the location tracking state.
00:17:39.919 --> 00:17:44.397
This pattern ensures that location updates can complete the work even when the app is backgrounded, but we're not leaving background tasks hanging indefinitely.
00:17:45.401 --> 00:17:47.646
There are some performance considerations you need to be aware of.
00:17:47.646 --> 00:17:52.507
Background tasks have a performance impact, so it's important to minimize background task duration.
00:17:52.507 --> 00:17:54.880
Keep background tasks as short as possible.
00:17:54.880 --> 00:18:00.442
You want to try and batch operations, so if you have related operations, group them together.
00:18:00.442 --> 00:18:21.663
Make sure that you monitor your task count, because you know how many background tasks you start, so you need to make sure that you don't start too many and use an appropriate quality of service if you are using any queues, because that gives you the right priority on the background and gives the system a better chance of giving you some CPU time at an appropriate time.
00:18:21.663 --> 00:18:53.678
So the result of this is, after implementing this background task management, that the location updates now continue to work reliably in the background, because it used to be that once the GPS callback into our application was finished and we did our fetching of a location on the on the seal location manager, that we we finished our processing, processing, we returned out of the callback function and then the system thought okay, we're done processing, no background task active, kill the application because work is done.
00:18:53.678 --> 00:18:58.185
Um, so we've.
00:18:58.185 --> 00:19:05.403
We've been able to have better quality in our location updates in the background by implementing this background task mechanism.
00:19:05.403 --> 00:19:12.022
We made sure that we didn't have duplicate background tasks and we do proper cleanup to avoid memory leaks.
00:19:12.022 --> 00:19:20.367
And we had better performance now because the tasks were very efficient and because of the logging it was it was, it was much easier to debug.
00:19:20.367 --> 00:19:24.296
So lessons learned of all the things that I just mentioned.
00:19:24.476 --> 00:19:27.020
So both the audio engine and the gps stuff.
00:19:27.020 --> 00:19:31.959
Um, audio engineering is hard, so this rig really reinforced that.
00:19:31.959 --> 00:19:33.945
Audio on ios is genuinely difficult.
00:19:33.945 --> 00:19:39.928
The combination of audio sessions, av audio engine and system audio controls creates complex interaction that is easy to break.
00:19:39.928 --> 00:19:49.263
So what I do differently the next time is start with a simpler audio architecture and add complexity gradually instead of like doing it one big move in one go.
00:19:49.263 --> 00:19:58.729
The single engine approach was actually working fine for the use case, but the refactoring was necessary to fix the m MP volume issues because it was a very visual thing that was annoying users.
00:19:58.729 --> 00:20:01.223
But technically nothing was really wrong.
00:20:01.223 --> 00:20:09.615
But we could have approached this issue and avoided this issue probably if we approached it a little bit more incrementally.
00:20:09.615 --> 00:20:14.287
So the real insight is that audio on iOS is not just about code.
00:20:14.287 --> 00:20:16.262
It's about understanding how the system works.
00:20:16.262 --> 00:20:23.961
So the MP volume viewumeView issue taught me that the iOS interprets audio session changes as user input, which is why the volume slider was moving.
00:20:25.515 --> 00:20:28.781
And background task that's the second lesson needs very careful management.
00:20:28.781 --> 00:20:32.164
Background task management in iOS requires discipline.
00:20:32.164 --> 00:20:42.701
It's easy to forget to end background tasks, which can lead to memory leaks and poor performance, and what I do different next time is create a dedicated background task manager class that handles the lifecycle automatically.
00:20:42.701 --> 00:20:48.685
This would be a reusable component that tracks multiple named background tasks so that we have some control over that.
00:20:48.685 --> 00:20:57.237
The manager would have a dictionary mapping task names to their identifiers and it would provide start task and end task methods that take a name parameter.
00:20:57.237 --> 00:21:01.227
This would make it easy to have multiple background tasks running simultaneously without conflicts.
00:21:01.227 --> 00:21:09.335
The start task method would check if a task with that name is already running and, if not, it would create a new background task with the provided expiration handler.
00:21:09.335 --> 00:21:13.366
The end task method would look up the task by name and clean it up appropriately.
00:21:13.366 --> 00:21:22.349
This approach would allow me for a much easier time creating a scalable and reusable system that works across different parts of the app that need background task management.
00:21:22.349 --> 00:21:26.832
And then there's also a third lesson Sometimes the best code is the code you don't write.
00:21:27.134 --> 00:21:32.798
I spent hours trying to optimize audio buffer management, only to discover that the current implementation was already performing well.
00:21:32.798 --> 00:21:40.467
This is a good reminder that premature optimization is still the root of all evil, and the real lesson here is that performance optimization should be data-driven.
00:21:40.467 --> 00:21:44.246
I was optimizing based on assumptions rather than actual performance measurements.
00:21:44.246 --> 00:21:49.106
The current implementation was already efficient and my optimizations actually made things worse.
00:21:49.106 --> 00:22:08.201
So what I do differently next time is to measure before I get started, and once I have measurements, do small increments and see if those actually generate an improvement in the processing that we were already having, the quality of processing that we were already having, and you can really use instruments here to profile the actual performance.
00:22:08.201 --> 00:22:11.509
And then you can optimize based on real data.
00:22:11.509 --> 00:22:14.980
And there's a fourth lesson here Logging is your best friend.
00:22:15.234 --> 00:22:18.022
Audio debugging is nearly impossible without extensive logging.
00:22:18.022 --> 00:22:22.134
I added logging at every step of the audio pipeline, which made debugging much simpler.
00:22:22.134 --> 00:22:27.536
So, and a pro tip is to use emojis in your log messages to have to make them easy to spot in the console.
00:22:27.536 --> 00:22:36.259
So a microphone emoji for recording, a speaker emoji for playback and one of these map pins if you want to log something about locations.
00:22:36.259 --> 00:22:41.000
And the real insight here is that logging isn't just for debugging, it's also for understanding.
00:22:41.000 --> 00:22:46.526
By logging the audio session lifecycle, I was able to exactly see when and why the MP volume view was moving.
00:22:46.526 --> 00:22:51.701
And then there's even a fifth lesson Threading is critical for audio.
00:22:52.154 --> 00:22:55.961
Audio operations need dedicated threads to prevent clipping and ensure smooth performance.
00:22:55.961 --> 00:23:03.163
The walkie-talkie service uses three separate dispatch queues one for receiving audio data, one for sending audio data and one for playing back audio.
00:23:03.163 --> 00:23:06.997
Each queue has a descriptive label which aids in logging and debugging.
00:23:06.997 --> 00:23:10.586
That includes our app's bundle identifier and the specific purpose of the queue.
00:23:10.586 --> 00:23:21.269
They all use user-initiated quality of service which gives audio operations priority over background tasks and concurrent attributes to allow multiple operations to run simultaneously.
00:23:21.269 --> 00:23:30.940
This separation is crucial because audio is time sensitive, as I already mentioned, If audio is not processed in time or interrupted, you get clipping, stuttering and dropped audio.
00:23:30.940 --> 00:23:35.941
And by giving each type of audio operation its own high priority queue, we ensure smooth operation.
00:23:35.941 --> 00:23:47.267
So audio operations are time crucial and if you use user initiated quality of service, that ensures that audio operations get the exact priority that it needs.
00:23:50.559 --> 00:23:56.226
And then there's a bonus tip, and that's a bonus lesson, I should say, and that is number six code cleanup is worth the time.
00:23:56.226 --> 00:23:57.436
Removing dead code and deprecated functionality might seem like busy work.
00:23:57.436 --> 00:23:58.111
Bonus lesson I should say, and that is number six code cleanup is worth the time.
00:23:58.111 --> 00:23:59.457
Removing that code and deprecated functionality.
00:23:59.457 --> 00:24:04.057
Functionality might seem like busy work but it's actually crucial for maintainability.
00:24:04.057 --> 00:24:12.865
This week I removed an unused mpvue slider view extension, a bunch of deprecated audio session code and some unnecessary add main actor annotations.
00:24:12.865 --> 00:24:17.578
That code creates confusion and makes debugging harder.
00:24:17.578 --> 00:24:24.401
By removing unused code, I made the code base cleaner and easier to understand, and user experience matters.
00:24:24.401 --> 00:24:27.282
That was the whole purpose of diving into this MP Volume View.
00:24:27.282 --> 00:24:28.205
This is Lesson 7.
00:24:28.205 --> 00:24:57.384
The MP Volume View was driving users crazy, even though it wasn't technically a bug in the app and users don't care about the technical details, they just want the app to work as expected and a volume slider moving around on its own is not really expected behavior and user experience bugs are just as important as functional bugs, sometimes the most satisfying fixes are the ones that improve user experience, even if they're not technically, technically complex, while in this case, getting it right was actually quite complex.
00:24:58.628 --> 00:25:26.022
So, looking ahead a little bit, I'll probably be working on some more audio performance optimization, some more background task monitoring and some error handling to improve things, and I want to get started on the switch ui integration and I want to do some combined reactor refactoring, because there's like a mix of reactive code that uses, delegates and combine and we need to standardize on one approach for better consistency and easier testing.
00:25:26.022 --> 00:25:29.421
So what I'm really excited about is the SwiftUI integration.
00:25:29.421 --> 00:25:32.942
I'm planning to start integrating SwiftUI in the existing UIKit app.
00:25:32.942 --> 00:25:45.035
This will be a gradual migration, of course, starting with new functionality and features, and I'm particularly excited about using SwiftUI for the walkie talkie interface or some new component that we might be adding.
00:25:45.035 --> 00:25:54.836
I also want to improve the testing infrastructure because, especially for audio input components, audio testing is notoriously difficult, but I think we can create some good test harnesses there.
00:25:57.737 --> 00:26:04.000
Do iOS 2025 is happening and I'm really excited about the upcoming event in Amsterdam this November.
00:26:04.000 --> 00:26:12.722
It's on November 11th and 13th at NEMU Science Museum and it's going to be an incredible opportunity to connect with fellow iOS developers and learn about the latest trends in iOS development.
00:26:12.722 --> 00:26:19.743
The conference features workshops on topics like building connected devices with embedded Swift, plus two days of inspiring talks from industry leaders.
00:26:19.743 --> 00:26:24.143
If you're interested in iOS development, I highly recommend checking out doioscom.
00:26:24.143 --> 00:26:27.128
So that's do-ioscom link in the show notes.
00:26:27.128 --> 00:26:32.964
It's going to be a great way to stay current with the latest iOS technologies and network with other developers facing similar challenges.
00:26:32.964 --> 00:26:37.185
So what I'm dreading is iOS 26 compatibility.
00:26:37.556 --> 00:26:43.815
Ios 26 is now released and we really need to start to test our audio implementation and see if there's other issues.
00:26:43.815 --> 00:26:53.963
I did cursory tests and it all seems to work out fine, but there's always this thing that you didn't look at or that you forgot about and that we really need to get right.
00:26:53.963 --> 00:26:58.507
And I also want to look at memory management in the app.
00:26:58.507 --> 00:27:02.838
There are some strange things that are noticed and we need to be very careful when dealing with memory management in the app.
00:27:02.838 --> 00:27:11.261
There are some strange things that are noticed and we need to be very careful when dealing with memory management, and there are some complex retain cycles and we need to ensure that everything is like properly cleaned up.
00:27:11.261 --> 00:27:16.922
So I really think in the big picture of things this week was very foundational.
00:27:16.922 --> 00:27:25.486
I've established a solid audio architecture that should serve well going forward, and the next phase is about optimizing and integration.
00:27:25.634 --> 00:27:31.184
I want to make sure that the audio system is not just functional, but also that the performance is good and that it stays maintainable.
00:27:31.184 --> 00:27:37.288
And the most important thing that I learned this week is that audio on iOS is a system-level concern.
00:27:37.288 --> 00:27:38.701
It's not just about writing code.
00:27:38.701 --> 00:27:42.665
It's about understanding how the system works and how different components interact.
00:27:42.665 --> 00:27:46.846
This understanding will be crucial as we continue to build and optimize the audio features.
00:27:46.846 --> 00:27:51.486
So I want to hear from you about your experience with audio on iOS, of course.
00:27:52.194 --> 00:27:54.163
So some questions for you.
00:27:54.163 --> 00:27:58.486
If you care, use one of the channels that I'm available on to answer those.
00:27:58.486 --> 00:28:00.358
And audio architecture.
00:28:00.358 --> 00:28:01.343
That's the first question.
00:28:01.343 --> 00:28:03.664
How do you architecture audio components in your apps?
00:28:03.664 --> 00:28:03.765
Apps?
00:28:03.765 --> 00:28:05.954
Do you use separate engines for recording and playback?
00:28:06.896 --> 00:28:08.381
Second of all is background task.
00:28:08.381 --> 00:28:10.365
What is your approach to background task management?
00:28:10.365 --> 00:28:12.501
Have you found any patterns that work well for you?
00:28:12.501 --> 00:28:16.315
Uh, testing audio how do you test audio functionality?
00:28:16.315 --> 00:28:18.219
What tools and techniques have worked for you?
00:28:18.219 --> 00:28:21.807
Um, performance, I mentioned instruments.
00:28:21.807 --> 00:28:30.244
Are there any ways that you test performance, or are there different tools that you use and what specific metrics are you mostly interested about?
00:28:30.244 --> 00:28:33.301
And then SwiftUI and audio.
00:28:33.301 --> 00:28:36.144
That's probably something that I need to do at some point as well.
00:28:36.144 --> 00:28:40.961
Anybody integrate the SwiftUI with audio components and were there any specific challenges that you faced?
00:28:40.961 --> 00:28:43.307
And another?
00:28:43.394 --> 00:28:48.403
Another final question is do iOS conference Are you planning to end to attend do iOS in Amsterdam?
00:28:48.403 --> 00:28:53.865
And, if you look at the speaker list and their topics, what are the topics that you're most interested about?
00:28:53.865 --> 00:29:05.266
As always, you can reach me on Twitter at app force one that's app force and then the numeral one on LinkedIn Mastodon, blue Sky, anywhere.
00:29:05.266 --> 00:29:07.282
I will make sure that those links are in the show notes.
00:29:07.282 --> 00:29:11.826
Make sure to reach out and you can always use the text.
00:29:11.826 --> 00:29:19.762
The show feature of my podcast and your input will really help me shape future episodes and directions of this work log.
00:29:19.762 --> 00:29:27.125
Also, if you're planning to attend DoIOS, I'd really love for you to connect with me there.
00:29:27.125 --> 00:29:40.804
It's always great to meet fellow iOS developers in person and share experiences, and you can find me at the conference, of course, because I'm organized and I'll be sharing some insights from this work log and audio engineering challenges with anyone who's interested while we're there.
00:29:41.935 --> 00:29:50.606
So this week was all about audio engineering and background task management, two areas that are notoriously tricky in iOS development, and this is what we covered.
00:29:50.606 --> 00:29:54.746
So the key achievements were that the audio engine refactoring was successful.
00:29:54.746 --> 00:29:58.184
I split the AV audio engine into separate recording and playback engines.
00:29:58.184 --> 00:30:03.646
The MP volume view issue was fixed, so this thing is not moving around anymore.
00:30:03.646 --> 00:30:04.347
Unexpectedly.
00:30:04.347 --> 00:30:15.263
We fixed the background task management, so we implemented proper lifecycle management for location tracking and I did a lot of code cleanup by removing dead code and deprecated functionality.
00:30:15.263 --> 00:30:18.805
We did a technical deep dive on the audio architecture.
00:30:18.805 --> 00:30:34.181
I tried to do a detailed walkthrough of the audio engine split, how we managed the audio sessions so a unified audio session helper to prevent conflicts and making sure that we don't set audio session properties if they're already set so that you don't set the same values twice.
00:30:34.181 --> 00:30:42.307
Background task lifecycle so proper management of iOS background tasks, some threading issues and there was also some lessons learned.
00:30:42.307 --> 00:30:47.362
In recap, audio on iOS is genuinely difficult and requires system-level understanding.
00:30:47.362 --> 00:30:49.702
Background tasks need careful lifecycle management.
00:30:49.702 --> 00:30:52.242
Sometimes the best code is the code that you don't write.
00:30:52.775 --> 00:30:54.481
Logging is essential for audio debugging.
00:30:54.481 --> 00:30:57.002
Threading is critical for audio performance.
00:30:57.002 --> 00:30:58.621
Code cleanup is worth the time.
00:30:58.621 --> 00:31:00.157
User experience.
00:31:00.157 --> 00:31:21.443
Bugs are just as important as functional bugs and, looking ahead, I'm going to be working on audio performance optimization, swiftui integration, combined refactorings, ios 26, fixed compatibility testing and verification, some legacy code cleanup, because we're probably going to deprecate the support for older iOS versions because we're now supporting back to iOS 15.
00:31:21.443 --> 00:31:24.442
I'm aiming for at at a minimum like iOS 17.
00:31:24.442 --> 00:31:34.221
And I'm hoping that this episode demonstrated what real iOS development looks like the challenges, the debugging process and the satisfaction of solving complex problems.
00:31:34.221 --> 00:31:38.560
And next time we'll dive into the things that I'll be working on this week.
00:31:38.560 --> 00:31:42.154
Keep building amazing iOS apps, and that's it for this week's Work Log, and I'll keep you on this week.
00:31:42.154 --> 00:31:48.261
Keep building amazing iOS apps, and that's it for this week's work log, and I'll keep you posted on any developments and make sure to check the links in the show notes.