Category Archives: Development

The Dangers of Mixing Locks with Core Data

Codebases accumulate patterns over time that don’t match up with current best practices. These patterns might have made sense when they were written, or they might just reflect how our understanding has evolved. Before you can address  these patterns, you need to spot them and understand why they’re risky.

One particularly dangerous combination in iOS codebases is mixing locks (@synchronized, NSLock, etc.) with Core Data’s performAndWait. These patterns were used to maintain synchronous operation patterns, but together they create hidden cross-thread dependencies that lead to deadlocks, freezing your app.

This shows exactly how these deadlocks occur, so you can recognize and avoid them in your own code.

A Simple Shared Class

Let’s start with a basic class that manages some shared state. This shows a common pattern from before Swift concurrency – using dispatch queues to manage background work. This class might be accessed from multiple threads:

  • Main thread: reads the operation status description
  • Background thread: Starts a background operation
class DataProcessor {
    var currentOperationIdentifier: String = ""
    var currentOperationStatus: String = ""

    // Called from main thread
    func getDescription() -> String {
        return "Operation \(currentOperationIdentifier) has status: \(currentOperationStatus)"
    }
    
   // Called from background thread
  func startBackgroundOperation() {
      currentOperationIdentifier = "DataSync"
      currentOperationStatus = "Processing"
      // Do processing
  }
}

The Problem – Race Conditions

When dealing with multiple threads, execution can interleave unpredictably. One thread executes some code, then another thread slips in and executes its code, then back to the first thread – you have no way of knowing the order.

Here’s what can happen:

Background ThreadMain Thread
currentOperationIdentifier = “DataSync”
(about to update status…)
getDescription()
reads identifier → “DataSync” ✓
reads status → “Idle” ❌ (old value!)
currentOperationStatus = “Processing”
❌ too late – main thread already read old value

The main thread ends up with the new identifier but the old status – a mismatch that leads to inconsistent data.

There are better solutions to this problem – like bundling related state in one immutable structure, or using actors in modern Swift. But in legacy codebases, synchronous locks were a common strategy to protect shared state.

Adding Locks for Thread Safety

The lock creates “critical sections” – ensuring we either write to both properties OR read from both without other threads interfering.

class DataProcessor {
    private let lock = NSLock()
    var currentOperationIdentifier: String = ""
    var currentOperationStatus: String = ""

    func getDescription() -> String {
        lock.lock()
        defer { lock.unlock() }
        
        return "Operation \(currentOperationIdentifier) has status: \(currentOperationStatus)"
    }

    func startBackgroundOperation() {
        lock.lock()
        defer { lock.unlock() }

        currentOperationIdentifier = "DataSync"
        currentOperationStatus = "Processing"
    }
}

So far, this works fine. The locks protect our shared state, and both threads can safely access the properties.

The Deadlock – When Locks Meet Core Data

Now let’s assume we want to store this data to Core Data. This is where things get interesting.

When sharing Core Data across threads, you can run into race conditions just like we had earlier. So you need to use the right APIs to protect the critical sections too.

Your go-to is performBlock – it asynchronously performs the work safely. However, there are cases in legacy code where the caller needs to do something synchronously using performAndWait. When you call performAndWait on a main queue context, it blocks the calling thread until the block executes on the main thread. Think of waiting on the main queue as our “lock”.

Let’s assume some developer in the past (who definitely isn’t you) decided to use performAndWait here:

func startBackgroundOperation(with context: NSManagedObjectContext) {
    lock.lock()
    defer { lock.unlock() }
    
    // Assume the main thread tries to call
    // getDescription() at this point. 
    // It is blocked as we are holding the lock

    currentOperationIdentifier = "DataSync"
    currentOperationStatus = "Processing"

    // 💀 DEADLOCK HAPPENS HERE
    context.performAndWait {
        saveDataToStore(context: context)
    }
}

Why Does This Deadlock?

There’s a problem:

  • performAndWait needs the MAIN THREAD to execute this block
  • The MAIN THREAD is blocked waiting for our lock (in getDescription)
  • We’re holding that lock and won’t release until performAndWait completes

CIRCULAR WAIT = DEADLOCK

Timeline of the Deadlock

Background ThreadMain Thread
lock.lock() ✅
Updates propertiesCalls getDescription()
Still holding lock…             lock.lock() ❌ WAITING…
Waiting on performAndWait() (needs main thread)Can’t process – stuck waiting on lock!
  • Main thread: stuck in lock.lock() waiting for background thread
  • Background thread: stuck in performAndWait waiting for main thread

 How to Fix This Deadlock

The best solution is to eliminate performAndWait entirely and use the asynchronous perform instead. This breaks the circular dependency because the background thread no longer waits for the main thread:

func startBackgroundOperation(with context: NSManagedObjectContext) {
    lock.lock()
    defer { lock.unlock() }

    currentOperationIdentifier = "DataSync"
    currentOperationStatus = "Processing"

    // ✅ No deadlock
    // doesn't block waiting for main thread
    context.perform {
        self.saveDataToStore(context: context)
    }
}

If you absolutely cannot eliminate performAndWait, you’ll need to carefully analyze all lock dependencies, but this is error-prone and hard to maintain. The real fix is embracing asynchronous patterns.

What We Learned

In this article, we’ve seen how mixing locks with Core Data’s performAndWait creates a classic deadlock scenario:

  1. Race conditions can occur when multiple threads access shared mutable state
  2. Locks were traditionally used to protect this shared state with critical sections
  3. performAndWait works like a lock but requiring the main thread to execute
  4. When a background thread holds a lock and calls performAndWait, while the main thread is waiting for that same lock, we get a circular dependency – neither thread can proceed

Coming Up



Future articles will explore other ways you can hit or avoid these deadlocks:

  • Child contexts with read operations – Why using a child context doesn’t save you from deadlocks during fetch operations
  • Child contexts with write operations – How save operations on child contexts create the same circular dependencies
  • Private Contexts – Why private contexts with direct store connections are less likely to lock up

How I Use Voice and AI to Turn Messy Thoughts Into Clear Plans

When I was a teenager, I got really into philosophy. I’d sit at my desk with blank paper (this was before smartphones), scribbling down every half-baked thought about existence and consciousness. Whatever rabbit hole I’d fallen into that week.

I realized that brainstorming on paper forced me to actually think. All those “profound” ideas bouncing around my head? Half of them were nonsense after I’d written them down. The other half started making more sense than I expected.

But I kept trying to organize my thoughts while brainstorming, which defeated the whole purpose. I needed that messy exploration phase, but the structure kept getting in the way.

So I started talking through ideas out loud. I could work through ideas while biking or driving, no structure needed. Just raw thoughts. No stopping to fix sentences, no fiddling with formatting.

Problem was, what do I do with 30 minutes of rambling? Record, listen back and take notes? Those recordings just sat there, full of a few good ideas I never actually used.

Then transcription and AI came along.

Now I can have the same stream-of-consciousness voice sessions, dump the transcript into Claude or ChatGPT, and get a structured plan back. Talk freely, get organized output.

How I Actually Do It

Here’s what I do when I need to work through something:

  1. Hit record and brain dump: Apple’s voice recorder, a few minutes but sometimes as long as 1 hour. Start with the problem, then just go. Questions, angles, contradictions, all of it.
  2. Let it wander: I start talking about some ideas and often end up somewhere unexpected. Ideas build on each other. What starts as chaos usually ends with clarity.
  3. Feed the transcript to AI: Apple transcribes it, I give it to Claude or ChatGPT. The AI follows my rambling and pulls out what matters.
  4. Quick cleanup: Sometimes I’ll record myself reviewing the output with changes. Or just make a few quick edits. Usually minimal.

Team Brainstorming Gets Crazy Good

This gets even better with teams. Record a team brainstorming session (with permission, obviously). Not for meeting notes, but for AI to turn the raw thoughts into a comprehensive plan.

Weird thing happens when everyone knows AI will form the first draft of the plan: people actually explain their thinking. We spell out assumptions. We say why we’re making decisions. Someone will literally say “Hey AI, make sure you catch this part…” and we all laugh, but then we realize we should be this clear all the time.

No one’s frantically taking notes. No one’s trying to remember who said what. We just talk, explore tangents, disagree, figure things out. The AI sorts it out later.

Where It Gets Wild: Voice-to-Code

Real example: On an open source project recently, we were discussing background processing in iOS. Background tasks? Silent push? Background fetch? Everyone’s got ideas, no one actually knows. Usually this ends with “let’s spike on it” and one week later, we’ve explored one or two of the concepts, we’re already committed to the first or second idea and not really sure.

This time we recorded the whole messy discussion. All our dumb questions: How often does BGAppRefreshTask actually fire? What’s the real time limit? Does anything work when the app’s killed?

Fed the transcript to Claude asking for a demo app covering everything we discussed plus anything we missed. The idea was to create a demo that confirms assumptions. We really don’t care what the AI’s opinion is of how things may work – give us something real we can confirm it with.

An hour later we had a working sample app. Each tab demonstrating a different approach with detailed event logging in the UI. We install it, we watch what actually happens.

After a few hours experimenting with the app and reading the code, we understood how these APIs actually work, their limitations, and which approach made sense.

Why This Works so Well

I get clarity this way that doesn’t happen otherwise. Talking forces me to think linearly but lets ideas evolve. AI adds structure without killing the exploration.

Might work if you:

  • Get ideas while walking or driving
  • Find talking easier than writing
  • Edit while writing kills your flow
  • Need to explore without committing

A Simple Checklist for Debugging Regressions

I’ve been thinking about a process I’ve used for resolving regressions that may be useful to share. I’ve never explicitly written the steps down before, but figured it was worth capturing—both for myself and others.

When a regression shows up, there are three questions that I’ve found you have to answer. Skipping any of them usually leads to confusion, wasted time, or a fix that doesn’t actually solve the real problem. But if you take the time to work through them, you can usually land on the right answer with confidence.

1. Can you reproduce the problem?

A lot of engineers want to jump straight into the code. That’s the fun part, right? Digging through logic, inspecting diffs, reasoning your way to a fix. But if you can’t reliably reproduce the issue, studying the code is usually a waste of time. You don’t even know where to look yet.

Reproducing the problem is the first real step. It’s not glamorous, and it can feel a little silly—especially when you’re trying the same steps over and over with tiny variations. But this is one of the most valuable things you can do when a bug shows up.

As engineers, we have a special vantage point. We know how the code works, and we often have a gut instinct about what kinds of conditions might trigger strange behavior. That gives us a real edge in uncovering subtle issues—so don’t think you’re above tapping on an iPad for hours or running the same test over and over. It’s our duty to chase it down.

Once you have a reliable repro, everything gets easier. You can try fixes, stress other paths, and most importantly, build real confidence that your solution works.

Some useful tricks:

  • Adjust timing, inputs, or state to help provoke the bug
  • Script setup steps or test data to save time
  • Loop the behavior or stress threads to make edge cases more likely

2. What changed?

This step is often skipped. People jump into debugging without first understanding what changed. But the fastest way to track down a regression is to compare working code to broken code and see what’s different.

This question can feel sensitive. It gets close to specific contributions that may have introduced instability. I’ve seen plenty of cases where the discussion gets deflected into vague root causes or long-term issues—anything but the specific change. That’s understandable. We’ve all been there. But avoiding the question doesn’t help. It puts the fix at risk and slows everyone down.

Some go-to techniques:

  • Review pull requests and diffs
  • Trace from crash logs or error messages to recently changed code
  • Use git bisect to find the breaking commit
  • Try reverting the suspected change and see if the issue disappears

Once you find the change, test your theory. If undoing it makes the problem go away, you’re on the right track.

3. Why does it happen?

Knowing what changed isn’t enough. You need to understand why that change caused the issue. Otherwise, you’re just fixing symptoms, and might miss deeper problems.

This is where the real problem solving happens:

  • Read documentation for the APIs or system behavior involved
  • Think through the interaction between components or timing
  • Build a mental model and prove it with experiments or targeted tests

You don’t want to ship a fix that works by accident. You want one that works because you actually understand the problem. That’s what prevents repeat issues and edge cases slipping through.

Wrapping up

These three questions — Can you reproduce it? What changed? Why does it happen? — have helped me find and fix bugs more reliably than anything else.

It’s easy to skip them under pressure. It’s tempting to merge the first thing that seems to work. But without answering all three, you’re flying blind. You might get lucky. Or you might end up wasting hours chasing your tail or shipping the wrong fix.

Take Control of Your Window AC Unit

While there are plenty of smart devices to control central heat and air conditioning, these will not usually work with portable units. Using a Raspberry Pi with a few simple electronics, I was able to upgrade the control of my window A/C unit.

At a minimum, I wanted to turn the A/C on and off from my phone. This is nice at the end of the day when I go to bed and realize I left the A/C running from the other end of the house. A remote control was included with the A/C unit but the range is obviously limiting. By coupling an infrared receiver to my Raspberry Pi, I was able to teach the Pi the infrared signals from the remote using an open source Linux package — LIRC. Then I connected an infrared transmitter to the Raspberry Pi to relay those same signals such as Power, Temperature +/-, Mode, etc… That could allow the unit to be controlled from anywhere.

While sending the remote control’s commands is useful, it also would be helpful to know whether the unit is on or off. For this I attached a vibration sensor that will notify the Pi when any vibration is detected in the last 5 seconds, which will be interpreted as the A/C is running.

Final Hardware Setup

This setup allows the Pi to communicate with the A/C unit, but I needed to communicate with the Pi. I opted for an Apple Shortcut that sends SSH commands to trigger the individual scripts. This is not the most elegant interface but it does the job. 

Why You Should Learn Server-Side Swift

If you’ve been watching the server-side Swift changes this year, you may have noticed the building momentum. The Vapor community reinforced their commitment to server-side Swift by pushing significant updates in Vapor version 4.  That was followed by a new server-side Swift platform — the  Swift AWS Lambda Runtime, complete with a WWDC video. Apple also dropped Vapor’s name during a State of the Union demo. Finally, the Swift Server work group has been expanding, so we should expect to see more server-side Swift features trickling out in the near future.

I hope some of the recent positive developments encourages you to consider why Swift on the server may be a good choice for your next server project. But I’d really like to encourage iOS developers to consider writing Swift server apps, even as experiments, to make better iOS apps. There is a synergy between iOS development and Swift server development that compliment one another and justify the investment.

Modularizing your App

To leverage the benefits of Swift server development, you will likely want to share some existing iOS app code with the server. The server-side solutions are built around the Swift Package Manager. If you are not already using packages to modularize your iOS app, you will need to spend some time moving code into a package. This requires separating the iOS-specific parts (ex: UIKit) from the things you want to reuse on the server. The scope of this effort depends on how intertwined the project code is. But once you move even a portion of your app code to a package, you will likely be thrilled to watch in run on a server. Your code is now more modular, opening up opportunities to extend to even other iOS apps.

Become more proficient with Swift

To learn a new development skill, we often need to experiment with technologies that add little value to our primary skill sets. Take for example my desire to expand to server-side development several years ago. My day job consisted of iOS development but I wanted to learn about the web. So I ventured into the world of Node.js and created a few simple web apps for personal use. While I don’t regret doing that, the results could have been better. These web apps got very little of my attention as I didn’t have a good reason to maintain my Node.js skills and I’d cringe at the idea of jumping back into unfamiliar code. 

I can contrast that experience with the time I took recently to convert those apps to Swift Vapor apps. In this case I’m working with Swift — a language I’m familiar with. If I return to that project in a year, I’ll at least be comfortable with the language. Additionally, each time I work with Swift on the server, there is a chance I will learn something new about Swift. That value will translate into better apps on both sides and help justify the time spent.

Time To Learn Something Really New

As I mentioned, it is compelling to learn a new skill that strengthens your existing skills — like cross-platform Swift. But I think we all crave an unfamiliar challenge too. Apple offers a stream of new APIs to keep us busy. But the environment for server-side Swift is a different animal. Working with technologies like Docker, Linux, AWS and Heroku is unlike anything you will see in the Xcode editor. That shift in paradigms may widen your perspective on development possibilities for your company/app and build some confidence to take on even bolder solutions.

How to Get Started

I suggest starting with small experiments to get comfortable in this space. Maybe write a Hello World app with Vapor 4 and contrast that experience with running Swift code on a server with an AWS Lambda deployment. Once you are comfortable with the basics, consider migrating parts of your app to a dedicated Swift Package that can be used by Vapor or AWS. I think you won’t regret the time spent here and at a minimum will learn some new Swift skills, have a more modular app and will have some fun taking on a new challenge.

Vapor Resources

Vapor 4 Getting Started

Vapor 4 Tutorial – Tim Condon

AWS Lambdas Resources

AWS Swift Lambda Announcement – Tom Doron

WWDC Tutorial – Tom Doron

Getting Started With Swift on AWS Lambda – Fabian Fett

HTTP Endpoint With AWS Lambdas – Fabian Fett

Developer Experience Using AWS Swift Lambdas – Adam Fowler