Every developer has to start at the beginning. We all go through stages of growth, and inevitably we make mistakes. It’s all part of the process, and it can be a valuable, if sometimes painful, way to learn.This is why we have senior developers and the code lifecycle, to make sure our mistakes don’t make it into the final version of our code.

This article won’t be a deep dive into code. I won’t be giving specific examples of proper APIs and syntax or telling you which architecture is better than the others. This article is far more personal. These are lessons I have learned by making mistakes (sometimes big ones) and applying my retrospective to see how that happened and how to avoid doing it again. Some of these are lessons I learned developing for iOS, but some are things I learned from my time in the military that carry over well.

You’ll have to make your own mistakes, of course, but I hope this article can help even one developer learn from my mistakes.

Attention to Detail

This is a mantra that was absolutely drilled into me (literally—by drill sergeants) in the army. “Attention to Detail, Motivation is Key.” It seems fairly self-explanatory, but the application eludes most people and varies depending on the context. In the army, attention to detail might be checking a coordinate for a third time, making sure your map is oriented correctly, ensuring you know where all of your equipment is, etc.

In coding, it’s a bit different. Let’s say you’re working on a bug in an application. You’ve found the cause and even identified the individual line causing the issue. Good work, but slow your roll for a moment. Before you go, “OK, I see the problem, and the fix is obvious!”, why don’t we take a look at the call site? Why is it executed? What does it do? More importantly, what is it supposed to do? What about the file or view it’s nested inside; what does it do? Is it doing what it’s supposed to? What will your “fix” do to all of this?

This is a lesson I’ve “learned” repeatedly. You get a bug report, it’s 4:30 PM on a Friday, and all you want to do is fix the bug and wash your hands of it. But be careful down that road; down there, there be dragons. There’s nothing more embarrassing as an iOS engineer than tagging your boss and saying “fixed the issue for you,” then getting messages two hours later that you broke a key feature or maybe even the whole app with your “fix.” That extra 20 minutes to truly understand the change you’re making can save you embarrassment and potentially hours of time hotfixing your mistake.

Slow is Smooth; Smooth is Fast

Once again, this is a military mantra. It’s something heavily ingrained in anyone who has spent significant time training in difficult or complex tasks. But I’ve found that this mantra applies to every aspect of my life, from the army to childcare to Brazilian Jiu-Jitsu and even to coding.

The concept is simple enough: Take your time and do each task as slowly as is needed to do it correctly. If you’re making mistakes, typos, logic errors, or overlooking potential issues, you’re going too fast. This generally applies best to learning new skills, but it can also be applied when you’re retraining yourself out of bad habits. By taking your time and focusing on the right way to perform a task, you’ll train your muscle memory on the correct way to do things.

At first, when something is new to you, you have to think about the individual pieces of the task. What is the correct syntax? What about placement, best practices, thread safety, etc.? Over time, the more you repeat a common task slowly and correctly, the faster you’ll be able to execute it with the same skill level. Those details that you used to have to think about are now intuitive. Eventually, even complex tasks can seem like you’re doing them in a rush, but you’re actually just well trained.

“Take your time” might seem counterintuitive in iOS engineering, since deadlines are a very real thing. But I don’t mean “work slowly on purpose,” and I definitely don’t mean “slack off and watch TV or take a nap.” In this context, “take your time” means don’t rush your work. Take the time to understand the context you’re working in, and when it’s time to write your code take a moment to think about the best way to do it. Sometimes a five-minute pause to consider the whole workflow can help you decide “Oh, this approach is much better than my first idea.” Or sometimes you write half of your solution, then realize “I really should have done it this way instead.” Go back and do it that way. Don’t pawn it off as technical debt and move forward. If it’s an extra 20 minutes or even an hour to do something right, rather than a patch job, it’s absolutely worth it.

But Don’t Get Trapped in the Planning Loop

I suspect (or maybe just hope) every engineer has fallen into this trap. It looks something like this:

  1. Receive a new large feature request.
  2. Identify where this feature would need to go in the code.
  3. Read through the existing code to familiarize yourself.
  4. Think about how best to make the new feature.
  5. Think of some issue with your first idea.
  6. Repeat steps 3 - 5.
  7. Repeat steps 3 - 6.

Before you know it, you’re at the end of a work day and you haven’t actually written any code, and you don’t feel any closer to a decision. Not ideal, but we don’t want to rush things right? Sure, but you do have to start writing at some point.

After doing this too many times, I’ve started breaking the cycle after step 4. Even if I immediately start seeing potential flaws in my plan, that’s OK. Maybe in the process of writing the code, I’ll stumble on the solution. I certainly won’t fix code I never wrote in the first place. So here’s what my revised steps tend to look like:

  1. Receive a new large feature request.
  2. Identify where this feature would need to go in the actual code.
  3. Read through the code that already exists to familiarize myself.
  4. Think about how best to make the new feature.
  5. Start writing.
  6. Keep writing until the feature works.
  7. Test the feature.
  8. Clean up the code.
  9. Push up my initial version to my branch. (It’s finished and working, so I keep a record of it.)
  10. Go back through the code and consider whether there are better approaches:
    1. Maybe a class would work better here than a struct?
    2. I don’t like how the data flow is managed here.
    3. Maybe I could use an Environment variable here instead of passing the data down through three layers of views.
    4. Etc.
  11. Make changes that will improve the code.
  12. Repeat steps 7 - 11 as needed.

In some ways, code can be very similar to art. My brother is an artist, and he is absolutely never happy with a piece. He will modify and tweak it until it barely resembles the original version. Engineers fight the same battle. We finish a feature and immediately think of things we could have done better, ways we could improve efficiency or cleanliness. The best we can do is to go through a few iterations within the reasonable timeframe we gave our client and have a working, tested, and verified solution when deadlines approach.

We will always have some level of technical debt. We are constantly improving (hopefully), and our understanding evolves with time. That’s OK, but we can’t get lost in the rabbit hole trying to invent the perfect solution or the perfect feature. That leads not only to failure but also to missed deadlines, because perfection is intimidating and you will second-guess everything you write. Instead, aim for functional, clean, reusable code that accomplishes the task you set out to do, and if it’s really important you can revisit it later. An opportunity will almost always present itself when some new feature gets added in that same code. Now’s your chance to make those fixes! But don’t get lost in the loop.

“Fight for the Users” (Be Customer-Experience Driven)

This is one that I still struggle with, and it’s a newer concept for me if I’m being completely honest. Obviously, we all know that we’re making our apps for users. But that’s not the same as approaching your architecture and feature implementation with that in mind.

So how do we apply this to our code? I’m not a designer. (I can’t even see colors, for crying out loud.) But it’s simpler than you might think. Run your code, but don’t just confirm that it works. When you make a feature or fix a bug, check that it works, yes—and then play with it. Try to think about it as a user, not an engineer, especially for new features. How does it feel? Would you be able to find it if you hadn’t written the code? Does the experience make sense? Would a less tech-savvy user be able to figure it out? And, most important of all, is this something people will actually use? Has any user expressed an interest in this?

We often get ideas from the top asking for features, and our first response should be “who asked for that?” or “who is going to use this?” If the answer to these questions ever appears to be “no one,” reach out to the people who requested the feature. Give them your feedback in a constructive way, and try to work directly with the design team and business analysts to come up with experiences that actually make sense. All of us—coders, designers, and business analysts—can get lost in the “idea-driven” trap where we come up with new, awesome ideas and want them thrown into our apps. But if users don’t want it and won’t use it, it’s just wasted code and unneeded clutter in the app. Keep your apps user-driven.

It’s important to differentiate this from pushing back on things you don’t want to do for whatever reason. But for any feature we’re making, we should always know its intended purpose and its target audience. This knowledge will inform your decisions on how to build it, and that can make a massive difference. As Captain Francis Hooke once said, “A word to the wise is enough. The old proverb is, forewarned, forearmed.”

It’s also important to understand that this isn’t a replacement for testing. Unit tests and QA are more thorough processes, with their own place in the lifecycle of code development. Speaking of which…

Live the Code Lifecycle

We’re all familiar with the code lifecycle:

  1. Plan
  2. Design
  3. Implement/Develop
  4. Test
  5. Deploy
  6. Maintain

Straightforward enough, right? But the simple presentation of these six steps can lead to some common mistakes based on a couple easy misunderstandings.

For one thing, you always see this cycle presented as though development moves in only one direction: from planning to designing to implementing, and so on. But when something goes wrong, you often need to bump back to the step before the one you’re in.

For example, you can be on the Develop step, only to realize you’re missing designs for some of the views. Bump it back, reach out to your design team and let them know. Or you could be in the Test stage and discover your feature doesn’t work or introduces a new regression. Looks like you broke something that already worked. Back to the Develop stage. Don’t be afraid to move backward in the cycle if that’s what your project needs.

The other misunderstanding I’ve come across is the belief that all testing happens in the Test step. Makes sense, right? Wrong. If you have a QA team, it’s not their job to confirm that your code actually works. It’s not your code reviewer’s job, either. That’s your job. Testing by the developer is actually part of the Develop stage. I have been guilty of forgetting this many times, and it has never failed to punish me.

Before you submit a pull request for review, run your code on your simulator. Test your feature, and then (this is key) test all the features that had already existed before you added yours. You don’t have to do a full QA test cycle, but play with the app and make sure that things that worked before still work. Then do it on your real device. I know it seems like overkill, but there have been many times where code that worked perfectly on the sim crashed the app on a real device.

Once you’ve done that testing yourself, put your code up for code review, the final stage of the Develop step. Code review is essential and should never be skipped or rubber-stamped. Write a real description: Put what you changed and why you changed it, make a relevant checklist to show you tested those things, and include a screenshot or a video if appropriate. This is maybe 10 minutes of extra work, but it will help your reviewers do their job. More importantly, when you submit your review request you can walk away confident that you did everything you could to ensure success. You’ve done your due diligence, and you’re much less likely to get a message at 8pm saying your changes broke production.

IPC Skills are Necessary

Another key skill set I developed outside of software engineering is interpersonal communication (IPC) skills. Many people don’t understand just how much goes into good communication. In my time as a police officer, I took (and later taught) entire 8-hour courses on communication skills. Despite that, there are some lessons I, with dozens of hours of training and real-world experience, have learned the hard way as an iOS engineer.

Those of us who have chosen a career where most of our communication is with a computer, terminal, IDE, or maybe even AI are not, traditionally, a social bunch. That doesn’t change reality, sadly. You need to be able to communicate effectively with design teams, business analysts, managers, supervisors, and maybe even executives or clients. So here are a few tips.

First, a piece of advice that I was given by my squad leader while serving in Iraq: “Every time you have something you want to say, stop for just three seconds and think about it. If you still want to say it after three seconds, go for it.” At first, this seems silly, but I find that often, after those three seconds, I say nothing at all. Just as often, I completely rephrase my intended message. It might seem disconnected, but this is a practice I use in my daily work even now. It can be something as simple as looking at a message or email a few seconds before you hit send and re-reading it. It’s often the case that what we think does not always translate well into what we say or write. Re-reading your messages and emails can keep you from inadvertently coming off with the wrong tone or communicating incorrect information—and in some cases, you might realize the answer to the question you were about to ask.

Second, understand that good communication is less about being a raconteur and more about being able to put ideas into a simple, easy to understand form. With that in mind, as iOS engineers, keep the technical out of it as much as possible when you’re working with your non-technical clients and colleagues. Non-coders don’t want to hear a technical manual. If you’re good at analogies, try to relate the idea you want to get across to generic knowledge your colleague will be familiar with. If analogies aren’t your thing, that’s OK—try using simple, short phrases. Instead of telling your business analyst about race conditions or recursive issues, let them know “There’s a problem using this new feature. It’s conflicting with existing features and causing unexpected problems.” Then, and here’s the important part, let them know what this issue means to them. “This has delayed development a bit, so it will take an extra week to make sure the new code is functional, safe, and free of regressions.”

Finally, plan ahead. It’s pretty rare for developers to suddenly and unexpectedly need to discuss our work with someone not in our dev team. Most often, we get advance notice of a meeting or a call. So use that time. Make yourself notes. There’s an app for that. Write a couple key points down, then read through them and consider, “Would a non-engineer understand this?” If the answer to that is “no,” reword it.

And by the way, if IPC skills are something you struggle with, there are courses you can take to improve them. It’s worth it. Good communication is essential for success in all aspects of life.

When All Else Fails, Start at the Beginning

Alright, we’ve learned our lessons, and we’re ready to get out there, forearmed with knowledge. But sometimes, despite all your skills, you run into an issue you just can’t suss out.

You’ve used every debugging trick you know, and you’re still stumped. What do you do? Ask your senior? Sure, you can and should do that if you’re stuck. But they can’t always immediately swap over to your issue. What can you do in the meantime? Catch up on YouTube? Probably not the best choice.

The answer is to re-evaluate your assumptions and start at the beginning. This was a lesson I learned fairly recently. Sometimes, when you’ve checked data flow, race conditions, and logic errors, and you’ve debugPrinted everything you can think of and tried every other trick in your toolbox, you’re still stumped. This is when it helps to go back to step 1: “What’s the most fundamental piece of this code?”

Let’s say your view isn’t updating in real time. Well, then step 1 is that the data you’re looking at is observed in some way. If you want changes to reflect in real time, the view needs to know it changed. Swift has a bunch of tools to implement this, from Combine to ObservableObject to the new Observable macro and its family of tools. So which one are you using? OK, let’s confirm that the observer is actually working. Put in some checks to see if changes are actually sending the onChange callbacks…

You get the idea: If you’ve eliminated everything else, start looking at things you just assumed are working. Apple bugs happen. Logic that had been working before could actually be faulty, bad logic just waiting for the right use case to trigger its flaws. Don’t take anything for granted. No code is perfect, no tool beyond reproach. And no developer, either—maybe you assumed your senior developer’s code works correctly and didn’t check it for the source of your bug. Check it. Everyone can make mistakes, no matter how senior.

I’m not done making mistakes. Every one of us learns something new every day. It’s a constant journey of self-improvement and better understanding. But I hope that these insights I have gleaned from my past mistakes can help others avoid a hard lesson or two.

Jeremy Fitzpatrick

Senior iOS Developer

MartianCraft is a US-based mobile software development agency. For nearly two decades, we have been building world-class and award-winning mobile apps for all types of businesses. We would love to create a custom software solution that meets your specific needs. Let's get in touch.