Tag Archives: AI

AI for Developers: Pitfalls and Patterns For Production Code

AI is a genuine force multiplier for developers, as I’ve written about before. It can help you communicate, manage your codebase, and ship things faster.

But there’s not really any training on how to use it well, and it’s easy to pick up habits that are counterproductive. Here are some practices and pitfalls I’ve encountered, mostly from doing them myself.

AI Development

1. Your Content Should Still Be Your Own

Let AI edit your thoughts, refine your phrasing, help you research, help you brainstorm. But what you produce should still be your content.

Our expertise as developers is in the choices: design choices, technology choices, API decisions. AI is an input. It gives you options. The expectation is that you iterate with it, consider alternatives, and evaluate tradeoffs. AI may point out something you didn’t think of or present other options, but in the end, you chose that solution over the alternatives, and that’s what matters.

Practically, that means you own the code line by line. If AI inserts something you don’t understand, go look it up. Validate why it did that, whether it’s correct. Either you learn something new, or you find out the AI was wrong. Either way, the expectation is you chose the solution and are ready to defend it.

2. Give AI Context About Your Codebase

AI knows nothing about your code standards unless you tell it. Without documented conventions, styles, or architectural guidance, it’s going to look at whatever patterns already exist and repeat them, whether you like those patterns or not.

Create Agent skills. Write markdown files that describe how you want things done. Without that, every session starts from scratch: AI guesses, you correct, repeat. This is also why I find it less interesting when someone points a model at a cold codebase and demos a feature. That’s neat, but a codebase without documented skills and conventions isn’t really ready for AI yet. The interesting thing to me is what happens when the AI has real context to work with.

Without architectural direction, AI solves the immediate problem by extending whatever’s already there. A 2,000-line class becomes 2,500. It’s not thinking about structure, just shoehorning in the feature. This is how weird patterns start emerging that nobody asked for. Without skills pointing AI in the right direction, it’ll grow a messy architecture faster than a human ever could.

3. Build Feedback Loops for AI

AI should be able to verify whether it got the right result. Unit tests are great for this. But it can also mean a CLI interface that lets AI access your app’s internals, running the same algorithms from the command line. This is especially useful for UI developers who don’t have that by default.

The more ways AI can check its own work, the less you have to catch manually.

Communication

4. Never Answer Someone’s Question With AI

If someone asks you a question, never regurgitate an answer from AI that clearly isn’t your own thinking. That’s a starting point for an answer, maybe, but it shouldn’t be the answer. We’re still humans and we expect to talk to humans. If you don’t have time to answer their question, just send them the prompt. It’s the new “let me Google that for you.” 🙂

5. Leverage AI for Your Writing

If you have a 10-paragraph document that could be 3 concise paragraphs, and you haven’t used AI for that, you’re missing out. If you avoid writing because you hate it, you’re missing out even more. AI can take your complex thoughts and make them coherent with minimal effort.

Use it on Slack posts before sending them to a large channel. Use it to polish technical docs. My first drafts are never good. They always need to be tightened up and made more concise, and that’s where AI shines. Think of it as a free editor that’s always available. Just make sure to ask it to remove those em dashes 🙂

6. Clean Up Your AI-Generated Pull Request Summaries

A pull request summary is used by other developers to understand what your change actually does. If you’re not careful, AI will just spit out what looks like a commit log, and it’s obvious. When that happens, reviewers can’t tell what the true intent is. Start with some background on what the thing does and why, then get into the details. It’s fine to use AI for this, just make sure it tells the real story.

7. Ship Something Real

In the early days of AI, everyone was posting demos. “I built this awesome weather app in 20 minutes.” “I stood up an entire AWS backend in an afternoon.” And for a while, that was genuinely exciting. We were all figuring out what was possible. But people see through that now. We all know you can generate something that looks impressive in a demo but is non-production slop under the hood. The novelty has worn off.

The bar has shifted. What’s impressive now isn’t how fast you built it. It’s whether it actually ships. Whether someone’s using it. Whether you’re using it. Even if the audience is just yourself, putting something into production is a fundamentally different thing than spinning up a demo. I have dozens of unshipped projects myself, and I’ve had to catch myself more than once. Weeks spent on something that never saw the light of day. Your time isn’t free just because AI moves fast.

That’s not to say stop sharing what you’re building. Keep doing that. But lean into sharing what you shipped rather than what you scaffolded.

CLI-Driven Development: Building AI-Friendly iOS and Mac Apps

I’ve been using Claude Code for several months now on many personal projects, and I’ve worked out some practices that work really well for me as an iOS and Mac developer. This CLI-driven development approach has fundamentally changed how I build applications with AI assistance.

The Problem with GUI-Based Development

One of the most powerful aspects of AI coding assistants is their ability to receive feedback and iterate on solutions. Give an AI a goal, and it can refine and improve until it gets there. However, if you’re an iOS or Mac developer, you’ve likely hit a wall: GUI interfaces are opaque to AI systems.

You could set up tooling to capture simulator screenshots, but this approach is slow and error-prone. By the time the AI gets a screenshot, analyzes it, and suggests changes, you’ve lost the rapid iteration cycle that makes AI assistants so valuable in the first place.

The other option is writing comprehensive unit tests. If you’re not doing test-driven development with AI yet, CLI-driven development is a nice stepping stone toward that goal. It has the added flexibility of interacting with real data—somewhat like an end-to-end test. Tests are still important, but this is another tool in your toolbelt for those not ready to go full TDD.

The goal is to give the AI the full context of your running application so it can fully interact with it.

Note: See the caveats section below regarding respecting user privacy and security when giving AI access to application data.


The Solution: CLI-Driven Development

CLI-driven development means architecting your application so that every use case accessible via the UI is also accessible via a command-line interface. The UI and CLI becomes access points to these use cases, rather than containing the business logic itself.

This isn’t a new idea. We’ve been told for years not to put business logic in view controllers or SwiftUI views. When working with AI, this separation becomes critical.

Benefits

  1. Better Architecture: Enforces separation between UI and business logic
  2. Faster Debugging: AI can identify and fix issues more quickly
  3. Faster Feature Development: AI has a way to give itself feedback
  4. Improved Testability: These principles make your app more unit-testable too
  5. Easier Data Migrations: AI can access and transform your real data

Architecture: Three Targets

I’m vastly oversimplifying the architectural requirements for a real application here. Folks will have their choice of patterns, frameworks, and approaches. I’m focusing on the minimal Swift package setup that will allow for this flow to work.

Think of your application as having three distinct targets within a Swift package:

  1. UI Target: Contains your SwiftUI views, view controllers, and UI interactions
  2. CLI Target: Handles command-line input/output
  3. Core Target: Contains all business logic, services, data interactions, and workflows

Both the UI and CLI targets are thin layers that simply pass data to the Core. When a user taps a button, the UI sends data to a service. When you run a CLI command, it does the same thing.

Shared Services in Core

This approach requires discipline you probably want anyway: every new feature or interaction needs a single method that can be called with all necessary parameters, with all the important logic happening outside your views. These shared services live in your Core target.

Here’s a brief example of a service to add a TODO:

Core Target – TodoService.swift:

public class TodoService {
    public func addTodo(listName: String, item: String) throws {
        // All business logic lives here
        let list = try getOrCreateList(name: listName)
        let todo = Todo(title: item, completed: false)
        list.todos.append(todo)
        try persistenceManager.save(list)
    }
}

CLI Target – main.swift:

// Parse command: ./MyAppCLI add-todo --list "Grocery" --item "Buy milk"
let service = TodoService()
try service.addTodo(listName: listName, item: item)
print("Added: \(item)")

Mac App Target – AddTodoView.swift:

struct AddTodoView: View {
    @State private var itemName = ""
    let listName: String
    let service = TodoService()

    var body: some View {
        TextField("Item", text: $itemName)
        Button("Add") {
            try? service.addTodo(listName: listName, item: itemName)
        }
    }
}

Both the CLI and Mac app call the exact same TodoService.addTodo() method. The business logic is shared, and both interfaces benefit from any improvements or fixes.


What Can You Do With This?

Debugging with AI

Let’s say you see an issue when adding a TODO to your Grocery list in the Mac app. Here’s how the conversation might go:

> I see a bug when adding a TODO to my Grocery list. Check it out.

Let me test this using the CLI:

$ ./MyAppCLI add-todo --list "Grocery" --item "Buy milk"

$ ./MyAppCLI list-todos --list "Grocery"
Grocery List:
  - Buy milk
  - Buy milk

I see the issue - the TODO is being added twice. Let me check the add-todo implementation...

[AI fixes the duplicate insertion bug]

$ ./MyAppCLI list-todos --list "Grocery"
Grocery List:
  - Buy milk

Fixed! The issue was in the data persistence layer where we were saving twice

The AI can use the CLI to reproduce the issue, see what’s happening, and iterate on fixes until it’s working correctly.

Building Features Faster

When building a feature, you can give the AI requirements and have it verify its work. Here’s an example:

> Add a feature to mark TODOs as complete. When I mark a TODO as complete,
> it should show with a checkmark when I list them. Verify it with the CLI
> and make sure it works before you're done.

I'll implement the complete-todo feature. Let me build it and test it:

[AI implements the feature in Core target]
[AI adds CLI command for marking complete]
[AI updates list display to show completion status]

Now let me test it:

$ ./MyAppCLI add-todo --list "Grocery" --item "Buy milk"
Added: Buy milk

$ ./MyAppCLI add-todo --list "Grocery" --item "Buy eggs"
Added: Buy eggs

$ ./MyAppCLI list-todos --list "Grocery"
Grocery List:
  - [ ] Buy milk
  - [ ] Buy eggs

$ ./MyAppCLI complete-todo --list "Grocery" --item "Buy milk"
Marked complete: Buy milk

$ ./MyAppCLI list-todos --list "Grocery"
Grocery List:
  - [] Buy milk
  - [ ] Buy eggs

Perfect! The feature is working. TODOs now track completion status and
display with checkmarks when completed.

The AI builds the feature and verifies it works before reporting back. In theory, you’ll see the same results in the UI application.

Data Migration and Transformation

During early iterations, you frequently need to migrate data when properties change. With a CLI, you can ask the AI to transform all your proof-of-concept data because it has access to all the APIs necessary to read and write data.

> Migrate my TODO data from the old JSON format to the new format.
> Make a backup first, then verify the results match exactly.

$ ./MyAppCLI export-data --output todos_backup.json
Exported 127 TODOs to todos_backup.json

[AI implements migration from old format to new nested format with IDs]

$ ./MyAppCLI migrate-data --from todos_backup.json --to-new-format
Migrating 127 TODOs...
Migration complete.

$ ./MyAppCLI verify-migration --old todos_backup.json --new current
 All 127 TODOs migrated successfully
 Data integrity verified

Migration complete. Backup saved at todos_backup.json.

Important Caveats

This is Not a Replacement for Unit Tests

Unit testing leads you toward similar practices of keeping services separate from UI. Following CLI-driven development actually makes your app more testable. I suggest using this approach for both unit testing and CLI-driven development. Don’t focus on CLI-driven development alone.

Security and Privacy Considerations

You must respect your customers’ privacy and security. Be mindful when giving AI access to data and understand your privacy requirements. The CLI should be treated as a development tool with the same security considerations as direct database access.

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