Chat
Ask me anything
Ithy Logo

macOS LLM Chat App in Swift

Building a macOS chat application using LLM API keys with SwiftUI

macos desktop app code

Highlights

  • Integrated SwiftUI Chat Interface: Step-by-step instructions to create an intuitive chat UI.
  • LLM API Integration: Secure integration with large language model APIs using URLSession and custom libraries.
  • Robust Code Structure: Clean and modular code design with secure API key handling and error management.

Introduction

In this guide, we provide an extensive walkthrough for developing a macOS app that communicates with large language model (LLM) APIs for chat functionality. Using Swift and SwiftUI, you can build an application that not only provides a smooth user experience but also integrates secure API calls. We will cover project setup, user interface design, and API integration with clear code examples and detailed explanations.

This sample app leverages common Swift libraries and practices to interact with LLMs such as OpenAI or other similar services. While our example uses URLSession for HTTP requests, you may also consider third-party libraries like Alamofire if you require advanced networking. Let’s dive into building your macOS app.


Project Setup

Prerequisites

Before beginning development, ensure you have the following installed:

  • Xcode (latest version): Xcode provides all necessary tools and simulators for macOS app development.
  • SwiftUI Knowledge: Familiarity with SwiftUI allows you to efficiently build user interfaces.
  • API Keys and Endpoints: Obtain your API keys from your chosen LLM provider (e.g., OpenAI, Anthropic) and ensure you have the proper endpoints.

Creating the Xcode Project

Open Xcode and create a new macOS project using the App template:

  • Select File > New > Project and choose the App template for macOS.
  • Name your application (LLMChatApp can be a sample name).
  • Choose Swift for language and SwiftUI as your interface framework.

Implementation

User Interface Design using SwiftUI

Designing the chat interface is a crucial aspect. In our implementation, the user interface features a text editor for conversation logs and a text field with a send button for user input. This setup ensures that both sending and receiving messages are handled intuitively.

SwiftUI Chat Interface Code


import SwiftUI

struct ContentView: View {
    @State private var userInput: String = ""
    @State private var chatMessages: [String] = []

    var body: some View {
        VStack {
            // Display chat messages in a scrollable view
            ScrollView {
                ForEach(chatMessages, id: \.self) { message in
                    HStack {
                        // Differentiate between messages sent by the user and the LLM
                        if message.hasPrefix("You:") {
                            Text(message)
                                .padding()
                                .background(Color.blue.opacity(0.1))
                                .cornerRadius(8)
                                .frame(maxWidth: .infinity, alignment: .leading)
                                .padding(.horizontal)
                        } else {
                            Text(message)
                                .padding()
                                .background(Color.green.opacity(0.1))
                                .cornerRadius(8)
                                .frame(maxWidth: .infinity, alignment: .trailing)
                                .padding(.horizontal)
                        }
                    }
                    .padding(.vertical, 2)
                }
            }
            .frame(maxHeight: 400)

            // Input field and send button for messaging
            HStack {
                TextField("Type your message...", text: $userInput)
                    .textFieldStyle(RoundedBorderTextFieldStyle())
                Button(action: sendMessage) {
                    Text("Send")
                        .padding(.horizontal)
                        .padding(.vertical, 8)
                        .background(Color.blue)
                        .foregroundColor(.white)
                        .cornerRadius(8)
                }
                .disabled(userInput.isEmpty)
            }
            .padding()
        }
        .padding()
    }
    
    // Function to send message via LLM API
    private func sendMessage() {
        let messageToSend = userInput
        chatMessages.append("You: \(messageToSend)")
        
        // Call the LLM API using your API key
        LLMAPI.sendMessage(message: messageToSend, apiKey: "YOUR_API_KEY") { response in
            DispatchQueue.main.async {
                if let response = response {
                    chatMessages.append("LLM: \(response)")
                } else {
                    chatMessages.append("LLM: Sorry, an error occurred.")
                }
                userInput = ""
            }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
  

Integration with LLM API

The core of the chat functionality is in integrating an LLM API. In this implementation, we will use Swift’s URLSession to make HTTP POST requests to your LLM endpoint. This allows the application to send the user's message and retrieve the corresponding response.

LLM API Communication Class


import Foundation

class LLMAPI {
    static func sendMessage(message: String, apiKey: String, completion: @escaping (String?) -> Void) {
        // Replace the URL below with your actual LLM endpoint
        guard let url = URL(string: "https://your-llm-api.com/chat") else {
            completion(nil)
            return
        }
        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        request.setValue(apiKey, forHTTPHeaderField: "Authorization")

        // Create the JSON body for the API request
        let requestBody: [String: Any] = ["prompt": message]
        do {
            request.httpBody = try JSONSerialization.data(withJSONObject: requestBody, options: [])
        } catch {
            print("Error encoding body: \(error)")
            completion(nil)
            return
        }

        // Perform the HTTP request
        URLSession.shared.dataTask(with: request) { data, response, error in
            if let error = error {
                print("Error sending request: \(error)")
                completion(nil)
                return
            }
            guard let data = data else {
                print("No data received")
                completion(nil)
                return
            }
            // Decode the JSON response
            do {
                if let jsonResponse = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any],
                   let chatResponse = jsonResponse["response"] as? String {
                    completion(chatResponse)
                } else {
                    completion(nil)
                }
            } catch {
                print("Decoding error: \(error)")
                completion(nil)
            }
        }.resume()
    }
}
  

It is important to ensure that your API key remains secure. To enhance security, consider using the Keychain to store secrets rather than hardcoding them. Additionally, robust error handling mechanisms can help manage networking errors effectively.


Creating the Main Application Entry Point

After designing the user interface and the API integration, configure the SwiftUI app's entry point. This is done in the main structure of the app.

Main Application Code


import SwiftUI

@main
struct LLMChatApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}
  

With this structure, your macOS application is ready to both send user input to the LLM API and display responses in real-time. Make sure to build and test your application in Xcode, targeting "My Mac" as the run destination.


Advanced Considerations and Enhancements

Handling Streaming Responses

For more advanced implementations, particularly when working with APIs that provide streaming responses, it is beneficial to update your chat interface in real-time as new data becomes available. This might include appending to a response text string during an API stream response.

The asynchronous structure of Swift’s async/await can be leveraged to handle streaming data effectively, ensuring that your user interface remains responsive during heavy network operations.

Supporting Multiple LLM Providers

To make your application flexible, consider supporting interactions with multiple LLM providers. This can be achieved by creating an abstraction layer that handles differences in API endpoints, authentication, and payload designs. By doing so, you provide users with a seamless communication interface regardless of which service they employ.

For example, you might create a protocol called LLMService that defines the required functions, and then implement it for different APIs (OpenAI, Anthropic, local models like Ollama, etc.). This modularity enhances maintainability and adaptability of your application.

Security Considerations

When deploying any application that uses API keys or sensitive data, security is paramount. Here are some best practices:

  • Key Storage: Never hardcode API keys. Use the macOS Keychain or configuration files that are excluded from source control.
  • Networking: Validate all endpoints using HTTPS. Ensure that certificates are properly managed.
  • Error Handling: Implement thorough error checking and response handling to manage unexpected responses or connectivity issues.

Extending the User Interface

To improve user experience, consider additional interface enhancements:

  • Incorporate animations or transitions when new messages are sent or received.
  • Provide options for users to select among different LLM services if multiple are supported.
  • Include settings to manage the storage and history of chats for future reference.

Data Tables and UI Components

Below is an example of a simple table that outlines potential configurations in your application for using various LLM services:

Configuration Description Example Value
API Endpoint The URL to access the LLM service. https://api.openai.com/v1/chat
Authentication Method for secure API key transmission. Bearer API_KEY
Content Type The format of the sent data (e.g., JSON). application/json
Error Handling Fallback options if requests fail. Retry mechanism, user alert

Testing & Debugging

Local Testing in Xcode

During development, testing your application in Xcode with the “My Mac” option is crucial. This will simulate real-world conditions and help you identify issues such as improper API calls, UI glitches, or incorrect JSON parsing.

Utilize breakpoints and console logging to pinpoint issues, and remember to test network calls extensively, simulating both success and failure responses.

Debugging Tips

Consider the following debugging strategies:

  • Logging: Maintain verbose logging for API calls to trace requests and responses.
  • Network Monitoring: Use tools like Charles Proxy to inspect HTTP requests/responses and ensure correct formatting.
  • User Feedback: Implement temporary UI alerts for testing that can highlight network issues during development.

Conclusion and Final Thoughts

In conclusion, this guide provided an in-depth tutorial for building a macOS chat application that integrates with a large language model API using Swift and SwiftUI. We outlined the design of a dynamic user interface coupled with secure API integration. Key aspects include building a solid architecture by separating UI logic, networking, and data handling, and implementing best practices for security and error management.

The provided examples balance simplicity and extensibility, allowing you to create a functional base application that can easily be expanded to include features such as multiple LLM providers, streaming responses, and a more polished UI. Whether you are a seasoned developer or just starting, this comprehensive approach will help you build a versatile and robust macOS application for chatting using LLM APIs.

Happy coding, and enjoy experimenting with integrating modern language models into your applications!


References


Recommended Further Queries

swiftpackageindex.com
LLM – Swift Package Index
jamesrochabrun.medium.com
Run LLM’s locally in Swift

Last updated February 27, 2025
Ask Ithy AI
Download Article
Delete Article