Recently, I was asked to help develop a proof of concept (POC) iOS app after having been away from iOS coding for a while. When I looked at the features included in MacOS 26 and the latest Xcode beta product, I was happy to see the inclusion of two (albeit related) things. The first was Apple’s new FoundationModels large language model (LLM) on device. This allows apps to include ChatGP like chat bots without the need to pump conversations back and forth to the cloud. The second, in Xcode, was the inclusion of what has become a big top in programming circles, these days, “Vibe Coding”. For the uninitiated, this means you have a chat with ChatGPT or Claude, and it handles a lot of the work of generating code. How good the code is, or how much it can do for you is currently up for debate, but I’ll throw my two cents worth in.
On-Device LLMs
First, let’s take a look at having an LLM on-device… to set the scene a bit, I use the free version of ChatGPT in their app on iOS and it does, generally speaking, a pretty reasonable job. It is easy trip up, and writing good prompts is key to getting the best out of it. As an example, I tried this prompt based on a news article I read somewhere (if I can remember the source, I’ll attribute it here):
Me >
“Was 1995 thirty years ago?”
ChatGPT >
“No, 1995 was 30 years ago in 2025.”
Me >
“What year is it?”
ChatGPT >
“2025”
To see if a small change to the prompt would solicit a correct answer, I tried this:
Me >
“Was 1995 thirty years ago today?”
ChatGPT >
“Yes”
The lesson learned here is that sometimes, the prompt needs more information than you might think. But I digress. Having an LLM on device can be very important for the use cases where the main concerns are speed, reliability and security. Not having to send the prompt to the cloud and waiting for the response to come back from the cloud, removes a lot the latency involved in implementing cloud-based LLMs in mobile apps. That’s not to say that responses will be immediate… there is still the processing and response generation happening and sometimes the devices don’t have the processing power of a cloud setup. Still though, whenever you can shorten the path from the prompt to the response, it’s a good thing.
Also, having the LLM on-device, means that there are less points of failure between the prompt and the response. By this I mean, less hardware that could fail before the path is fully traveled. By being on-device, you don’t need a radio to package up the data and send it to the cloud. You don’t need a receiver to receive the data package and put it onto a wire, where it gets sent through untold number of servers in untold number of locations in the world, then processed in a server farm somewhere with its own network of hardware and software to move and process data, only to turn it all around to send the generated response back. Generally, all of that is reasonably fast and reliable, when it fails, you really feel it. So, I’ll take on-device any day.
The proof of concept app that I am working on includes a feature where the user can have a conversation with the app and the app can use components of that conversation to find outside content that may be of interest to the user. So, chat itself happens all on-device (which so far in beta is pretty good speed-wise) and produces reasonable responses. This helps a lot with user engagement, so, a good thing. Also, the app can tap into the conversation and the response and use another artificial intelligence (AI) feature called Named Entity Recognition, to identify things of interest to the app. These identified things are used to find available content that matches with the text of the conversations and surface that new content to the user of the app. Done properly, that can look pretty impressive to the user and also supports more engagement with the user, in the long run.
On-device LLMs then, seem like a pretty good idea to me, especially for the use case I am working on currently. To help me work on that POC app, I thought I would use the feature in Xcode that is our second point, having an LLM to generate code. I’d never done that before, and I’m always up for any way to make my work easier and faster.
Vibe Coding
When I downloaded the latest beta of Xcode, I noticed that it featured a coding assistant using ChatGPT (or Claude) to help with the generation of code and to check for and correct coding errors. It works just like any other chatbot… you enter a prompt asking for some feature to be added to some existing code, or for some code to be refactored, or to fix an error. It responds with it’s reasoning on how to solve the problem, and can (if you like) implement the proposed changes right in your code. If you don’t like what it did, you can tell it revert the changes (although in the beta I’m using this is surprisingly hit or miss).
Since the POC needed to include the on-device chatbot, I looked to the internet to see what someone might have already done. Usually someone does the small app to show off a new feature and are kind enough to provide all the code they wrote. A good place to start, I thought and it didn’t take to long to find a good starting point. Let me give a shout out to “SwiftUI Cheatsheet” on Patreon for the excellent starting point. Here is a link to the article I used:
Using that as a building block, I spent some time building the POC to include their chatbot, with a few changes to suit my purposes. I mostly built the rest of the app simply by prompting the coding assistant (in my case, ChatGTP 4.1 and 5.0) and getting it to do most of the work. I was, with very little touching up of the code, able to put together my POC in a seriously short amount of time. What would normally take me days, only took hours. Here is where I started after getting the basic chatbot in place.I wanted to be able to have the chatbot talk the response rather then just spit out the text of it:
Me >
“when the ai is finished generating a message, have the message output to audio as voice”
Xcode >
“To implement your request, I’ll add speech synthesis so that after the AI finishes generating a message, the message is spoken aloud as voice.
I’ll use the AVFoundation framework’s AVSpeechSynthesizer to perform the speech. This will require:
• Importing AVFoundation
• Initializing an AVSpeechSynthesizer in ChatViewModel
• Invoking the synth whenever an AI message completes (after aiMessage.isPartial = false and is saved)
Let’s make these changes to your ChatViewModel.swift now.”
— a picture of the proposed change was shown —
Xcode >
“Here’s what I’ve changed:
• Imported AVFoundation at the top of the file.
• Added a private AVSpeechSynthesizer property to ChatViewModel.
• After the AI finishes generating a message (when marking aiMessage.isPartial = false and saving), the message is spoken aloud using AVSpeechUtterance and the synthesizer.
This ensures that only complete AI messages (not partial responses or errors) are voiced, as you requested. If you’d like any adjustments (such as custom voice, rate, or language), let me know!
Me >
“add a toggle to the chatviewmodel to allow the turning on and off of the voice output”
Xcode >
“To fulfill your request, I’ll add a @Published property (e.g., isVoiceOutputEnabled: Bool = true) to the ChatViewModel. I’ll then update the logic so that the AI message is only spoken if this toggle is enabled.
This allows any SwiftUI view observing the view model to bind to this property and provide a user-facing toggle.
Let’s implement this now.
— a picture of the proposed change was shown —
“I’ve made the following changes:
• Added a @Published var isVoiceOutputEnabled: Bool = true property to ChatViewModel. This property can be bound to a toggle in your UI.
• Updated the AI response logic: The AI message is now only spoken if isVoiceOutputEnabled is true.
You can now easily wire this property to a control in your UI, such as a SwiftUI Toggle, to allow users to turn voice output on or off as they wish. Let me know if you need help adding the toggle to your user interface!”
Me >
“add a button to the navigationbar to toggle the voice output of the chatviewmodel”
Xcode >
“To fulfill your request, I’ll add a button to the navigationBar(viewModel:) within ChatView.swift. This button will toggle viewModel.isVoiceOutputEnabled and update its icon to indicate whether voice output is currently on or off.
You’ll see a button with a speaker icon (speaker.wave.2.fill for on, speaker.slash.fill for off) in the navigation bar.
Let’s implement this now.
— a picture of the proposed change was shown —
I’ve added a button to your navigation bar in ChatView. This new button appears before the clear conversation (trash) button and toggles viewModel.isVoiceOutputEnabled. Its icon visually switches between a speaker (speaker.wave.2.fill) when voice is enabled and a muted speaker (speaker.slash.fill) when disabled. The button is also accessible with appropriate labels.
You can now easily toggle voice output directly from the navigation bar! If you’d like further customization, just ask.”
The code changes the coding assistant put in more or less worked without changes. So far, so good. I was a little impressed. I continued to implement other things simply by asking…
When I ran into a problem, I could just ask the coding assistant to fix it:
Me >
“Fix error in ChatViewModel.swift on line 90:
@error:ConvosBot/ViewModels/ChatViewModel.swift:90:Cannot assign value of type 'String' to type 'AVSpeechSynthesisVoice'
“
Xcode >
“The error occurs because AVSpeechUtterance.voice expects an instance of AVSpeechSynthesisVoice, but the code is passing a string identifier (AVSpeechSynthesisVoiceIdentifierAlex).
To fix this, you must use the initializer AVSpeechSynthesisVoice(identifier:) to create the correct object.
I will update the code so that the line
— a picture of the proposed change was shown —
I updated the code so that utterance.voice is now set using AVSpeechSynthesisVoice(identifier: AVSpeechSynthesisVoiceIdentifierAlex). This creates the correct type and resolves the error.
Why this fixes the issue:
Previously, the code tried to assign a String directly to utterance.voice, which expects an AVSpeechSynthesisVoice?. Now, it constructs the voice object with the identifier, which matches the expected type.
Let me know if you have any other issues or need further adjustments!”
This fix worked just fine. Instead of me having to search for an answer in the documentation or on some of the websites that have answers, and having to figure out exactly what to, Xcode did it all for me in a few seconds. Win!
I also asked Xcode to create all new part of the app:
Me >
“make a new view that has two main sections in a vertical structure, and then a tab bar at the bottom with 4 tabs (Main, Profile, Shared, Convos)”
Xcode >
“You want to add a new view with a vertical structure (two main sections) and a tab bar at the bottom with four tabs: Main, Profile, Shared, and Convos.
Here’s the plan:
1. I will create a new SwiftUI view (let’s call it MainTabView) that implements this design.
2. The view will use a TabView with four tabs. Each tab will show a placeholder for now.
3. The Main tab will be structured with two vertical sections (using VStack), each using a placeholder.
4. I’ll create the new view in a separate file for clarity.
Let’s start by creating the new view file: MainTabView.swift.
— a picture of the proposed change was shown —
I’ve created a new SwiftUI view called MainTabView that structures its content in two main vertical sections and includes a tab bar at the bottom with four tabs: Main, Profile, Shared, and Convos. The Main tab uses a vertical stack for the two sections as requested.
Would you like to see how to integrate this new view into your app, or customize the contents of the two main sections or any of the tabs?”
Me >
“MainTabView should be the main view of the app and ChatView should be the view when the Convos tab is selected”
Xcode >
“Here’s the plan:
1. Update ConvosBotApp.swift so that MainTabView is the main/root view of the app, instead of ChatView.
2. Update MainTabView.swift so the Convos tab displays ChatView (not just a placeholder).
First, I’ll update ConvosBotApp.swift to launch with MainTabView.
— a picture of the proposed change was shown —
The following changes were made:
1. ConvosBotApp.swift now uses MainTabView() as the main/root view instead of ChatView().
2. MainTabView.swift was updated so that the “Convos” tab displays ChatView() instead of a placeholder.
Your app will now show the tabbed interface as the entry point, with the Convos tab showing the chat interface. Let me know if you’d like to further customize any of the sections or tab contents!”
Over the period of a few days I was able to put together a reasonably complete standalone app suitable as a proof of concept. I estimate that Xcode wrote about 90% of the code and was able to fix almost all errors. Some of the errors were due to ChatGPT not having up-to-the-second knowledge of the most recent changes in Swift, SwiftUI and how Xcode works. Never-the-less, I was able to in a few days what would have taken me weeks, given that I have been away from iOS app coding for a couple of years.
Vibe Coding Lessons learned
I am a vibe-coding convert. I will continue to use vibe-coding whenever I can and I expect that going forward, it will become even better, faster and more accurate. Vibe-coding still doesn’t do the best of code block optimization, but I found that you could ask Xcode to do something particular that would optimize your code structure and it would happily and accurately do it. I am hooked!
In Conclusion
I love having LLMs on-device. They will only get better and more entrenched in more apps going forward so it’s nice to see that Apple (and I’m sure others) are working hard to make this happen.
And vibe-coding… yes feakin’ please! Anything to make my life easier is a win. I still get to flex my dev experience to put together a world-class app with top-notch architecture, but its nice to be able to focus on the fun stuff like that and let coding assistants lie the one in Xcode do all the grunt work for me.
Let’s keep this conversation going in the comments, as this is one topic that is going to change a lot and often in the very near future.