Developers make great software for us to use and enjoy. Line by line, they craft a growing product with tons of new and exciting features. Unfortunately, something else is hiding in all those lines of code, something we don’t enjoy as much: Bugs. Here’s a quick guide to how we go about iOS bug fixing.
Bugs in software are flaws that can cause unexpected behavior or even errors and application crashes. Bugs can destroy a product if they are not fixed before releasing. They break the user experience and can even make a user stop using a product or in the worst case, even incentivize other users to stop using the product by spreading bad reviews.
As a developer, our goal is to release a product with all the expected features with the minimum number of bugs as possible. There are three steps to remove bugs from software: Detect, Reproduce and Fix.
Step One: Detect
Identifying a bug is not as simple as one would think. Most of the bugs are not visible at plain sight and require careful testing and observation. A Quality Assurance (QA) team usually help identify incorrect behavior, missing functionality and bugs. They can help by creating tickets on the discovered issues that usually include description and reproduction steps.
Common places for bugs are:
- Third-party frameworks
- Untested code
- Complex logic
Step Two: Reproduce
If you are lucky enough to have a ticket with reproduction steps, you are done for this step and can continue to Fix it. If the ticket doesn’t include reproduction steps, you are in for a nice challenge. It is time for what I call: Detective mode.
Understanding the problem is the main objective. Know why it’s happening and you can head in the right direction and fix it.
- Get as much information as you can from the person who identified the problem
- Check for related issues
- Debug the code
- Return to a previous version of the code and check for differences
An important thing to mention is that without reproduction steps one should not attempt to fix a bug. It is tempting to do some work related to the issue, but if you don’t have the correct reproduction steps it will be impossible to verify the fix.
No reproduction steps = Do not attempt to fix
Obtaining reproduction steps can be very challenging, but knowing tools and where to start can make a difference in both time and quality. The next tips are exemplified using iOS but they can be applied to other platforms.
A complex issue is usually caused by a single condition that went wrong and crashed the application. Just like a snowball that became an avalanche. While it may not be easy to reproduce a bug as a user, as a developer you can make changes on individual pieces of code to modify a value when the application is running and force the issue to appear.
For example, a crash could only happen if a value is outside a certain range. Change the value and observe if that triggers the problem. Once it is reproduced artificially, it will be easier to check where as a user you can create that same problem.
In Xcode the debugger uses LLDB which allows changing values in real-time.
Another alternative is to change values using breakpoints by adding a Debugger Command
By using breakpoints you can run a debugger command multiple times without having to stop to change your values and speed up the whole process.
Use older devices
It is not uncommon to have everything work perfectly in the latest hardware and operating system, but fail in older devices. In iOS, older models run slower and animations and actions can perform slower. In the latest OS, a user may not be able to press a button repeated times, but with an older model it may take longer to complete an operation and it could be possible.
Printing output to the console is the usual way to get information, but what if you are running a real-time application that dumps thousands of lines. Use breakpoints instead and assign a sound. You can even assign different sounds to each breakpoint and check the Automatically continue option.
It is incredibly helpful for tracking connections/disconnections or methods that should only be called once. If you hear the frog twice, you know the problem.
With a complex codebase you may sometimes find conditions that should never happen, but if they do, it could lead you to failure. Drop a breakpoint in the problematic condition and if it is triggered, you will hit the breakpoint and will have a stack trace to follow and possibly fix.
Increasing output speed
Once I worked with an audio real-time application and having breakpoints was not an option. Using NSLogs for output was also causing problems and the solution was the old printf. It has almost the same printing capabilities, but is much faster.
This is the one breakpoint I run with about 99% of the time. When debugging, it will force you to stop the app at the exact time you crashed, and it will give you a chance to check the stack trace and view the state of the program.
Step Three: Fix
Having the correct reproduction steps are most of the challenge. You can now go into the code section that is causing the problem and fix it. Use your reproduction steps to verify that after the change, the bug no longer happens.
While coding without introducing bugs is complicated, it is possible to reduce their quantity and complexity.
The best way to avoid bugs is to create unit tests at least for the most complex pieces with failing and success cases. By having unit tests once you get the reproduction steps, you can modify your unit tests to handle the problematic case. Your unit tests will become more robust each time.
Smart people make smart bugs
Don’t be too clever when coding. Writing code is a lot easier than reading something you or someone else wrote several weeks ago. Give descriptive names to variables and methods. Add comments when required. Add code reviews to your development process and if something is not clear to the reviewer, improve it.