Unlucky Number 13: How iOS 13 was a step backwards for VoIP apps
Written by Jeremy Norman on 22nd July 2020
At Apple’s WWDC 2019, in a talk ironically (at least for us) named “Advances in App Background Execution”, Apple announced a major change to how VoIP push notifications must be handled in apps targeting iOS 13. These changes were introduced because apps were abusing these high priority VoIP notifications to guarantee they would be woken up, lowering the amount of control Apple has to preserve battery life. Unfortunately for us, as a legitimate user of these VoIP notifications, this small change would cause us months worth of stress, struggles and customer complaints.
Regressions in App Background Execution
To understand this change and why it causes issues for not just our VoIP app (Vialer) but for most VoIP apps, it is necessary to understand how exactly incoming VoIP calls are implemented by most app developers.
Unlike a traditional VoIP phone which would maintain an open connection (via REGISTER requests) to a VoIP platform, mobile apps typically maintain this communication via push notifications. This means that Google’s FCM or Apple’s APNS sits between a VoIP platform and a mobile app. When there is no active call, the app can be completely dead but when a call is incoming, the VoIP platform will notify the Push Notification service, which will then wake up the phone and the app, at which point it can behave like a normal VoIP client. It would initialize SIP, register with the VoIP platform, receive an incoming call and then alert the user by ringing/vibrating the phone and displaying a call screen.
For apps targeting iOS 13 Apple decided that the final step of this process, alerting the user, had to occur immediately and for every single notification received. Failing to comply with this would result in apps essentially being shadow-banned from VoIP notifications. Phones would stop receiving them without any more information given about how long they would be stopped for, how they could get them back, or really any information at all honestly.
A push notification is not a call
There exists a concept called “Optimistic UI” (there are similar paradigms in many areas of computing) in which, when a user makes a change that must be submitted to a server, the UI will be updated immediately, optimistically assuming that nothing will go wrong when it is submitted.
This is essentially what Apple has forced apps to implement, but instead of seeing a small UI glitch if an issue does occur, you have a phone ringing, a user having to take their phone out of their pocket, only to find there is no call on the other end.
At the time the push message is received we do not know if there is a real call available because the app is just waking up, and given just a second of delay it’s possible that:
- The caller cancelled the call
- Another user picked up the call (if in a call group)
- The user lost their connection
In the 0-2 seconds it takes to initialize SIP and register, we will find out if any of the above have occurred but we have already had to inform and bother the user for a call that simply will not exist by the time the user has their phone in their hand.
The application could also use this as an opportunity to decide if it wanted to handle the call. For example, it could check if the current network connection was strong enough to support a VoIP call. This and any other similar checks can no longer be implemented without bothering the user.
The final problem is that a push message is not a call, it is just a payload containing small amounts of meta-data about the call (e.g. the number of the person calling). A user cannot “answer” a push message, we need to have established an actual SIP call for this to be possible. However, Apple demands that the user interface must be presented immediately and therefore present the option to answer/decline a call that doesn’t actually exist yet. This is relevant even in perfect conditions (although admittedly would require some fast fingers) but given a bit of latency such as on a slow 3G connection and it could easily affect normal users.
Our iOS 13 nightmare
Adapting our app to support these changes has been a very difficult experience for us. Some of the blame can fall at Apple’s feet for their handling of this but there were many factors at play.
Lack of updates in existing APIs
The main reason to fault Apple is their complete lack of updates to their existing APIs to support this. They could build features into their CallKit framework that would help developers work with these changes and more importantly would serve to point them in the right direction while adapting the changes. Instead their advice only comes in the form of a forum post by a member of the Developer Technical Support team. While this post served to be very helpful, these are issues that all developers will face implementing VoIP and should be reflected in the framework and not just in vague instructions.
How long is too long?
Another problem we encountered is what would happen if you did not comply with these guidelines. If you did not present the call UI, the app would crash (this was clear and easy to see), taking too long to present the UI would supposedly cause notifications to no longer be delivered to the app. This, however, is completely invisible and we were never sure how much time we were actually allowed and if we were ever suspended from receiving notifications.
A knowledge gap appeared
However, we had internal issues too. Firstly that we had a clear knowledge gap, the original developer that worked on the calling part of the application no longer works at the company. While we have developers with fantastic general iOS knowledge and other developers with strong knowledge of how calling works on mobile platforms, this knowledge was not put together until many months after work had been started.
Testing incoming calls is hard
Calling in-general, but in particular incoming calls are very difficult to test. This not only made us hesitant to make large changes but it would also take a long time to receive feedback regarding changes that were made. While we had QA investing just as many hours into testing the app as developers did developing the changes, it could pass QA with flying colors, only to fail for a significant percentage of users in real-world situations.
The end of life is near
The final problem, which is purely an internal one, is that by the time this issue came to light, we had already decided that we were approaching the end of life of our iOS app. The mobile apps team was already working on a replacement that aligns the development of both the iOS and Android app of Vialer. This made us focus on the essential work that was needed for iOS 13, instead of having the time and capacity for a complete overhaul.
What we did to make incoming calls work again
After many different iterations, all of which seemed to experience various issues, the experience gained by working on those finally made it clear how we could implement the advice in the forum thread from Apple that was mentioned earlier.
- The first change had to occur server-side. To ensure that the push messages reached the phone, we would send multiple of them and simply ignore any of them on the phone aside from the first one. This would mean if a push message failed (which does happen) that the following may still reach the phone and the call would still be successful. However, as we were not responding to these subsequent messages the app would crash, so we now only send a single push message to the phone and hope that message reaches the phone successfully.
- When that single push message comes in, we immediately start alerting the user, as required. At the same time we begin booting SIP, then register and let the platform know that we are ready to receive the call. If any of these processes failed, we would stop the phone from ringing. The most important thing to prevent ghost calls involves a flag that is set to true when a call becomes a “real” SIP call, if this hasn’t happened within 2 seconds after responding to the server, we would consider the call dead and stop the ringing.
- The final change to be made, as suggested by Apple, was to make the accept/decline buttons hang until we had a “real” call. Making a UI hang is pretty much the antithesis of what you try to achieve when creating a UI but we will accept that if Apple is suggesting it. There are no better options.
While the actual changes that are in-place do not seem too complicated, the amount of knowledge that had to be gained to properly rip out the existing method of handling incoming calls and replace it, without breaking anything else, was the time consuming part.
In the end: a worse app and annoyed users
The most infuriating part about the many, many months we have spent trying to support these changes by Apple is that, through all the stress and hard work, we now have a worse product than we did last year.
Assuming our implementation is how it should be and there are no bugs or problems caused by what we have done, Apple’s changes have made it a worse experience for our users. There will be situations in which a user runs across the room to grab their phone to answer a call only to find the call is already dead, there will be situations where a user taps the answer button multiple times wondering why it’s not doing anything, only for it to flicker back to the app momentarily before moving to the call screen. These situations aren’t the end of the world, but they are not the user experience that we want to (and before iOS 13 could!) offer our users. But a project that takes the better part of 6 months with the only tangible result being that it might cause some annoyance amongst our user-base isn’t exactly a satisfying one to finish.
How do you feel about iOS 13?
While it’s something we already knew, the changes in iOS 13 for us served as a stark reminder that when developing for such a closed platform, your product exists completely at the mercy of the company running it.
What’s your experience with iOS 13 and VoIP apps? Do our struggles sound familiar or not at all? Let us know in the comments below.