admaDIC App Development & IT Solutions

Local LLM based AI-Assisted Code Review Comments in Xcode - Part 2

by Annett Schwarze | 2026-04-24

In this series an Xcode Source Editor Extension is built, that analyzes Swift code using a locally running language model (via llama.cpp) and inserts generated review comments directly into the source code as inline annotations.

In the second part the Xcode Source Editor extension is implemented.

Create Host App and Extension

Xcode Source Editor Extensions must be bundled within a macOS app. Follow these steps to create the app project:

  1. Open Xcode
  2. Select the menu item File > New > Project
  3. Select macOS > App
  4. Name it "CodeReviewAssistant"
  5. Interface: SwiftUI or Storyboard (doesn't matter, it's just a container)
  6. Language: Swift
  7. Click Create

The project will look like the screenshot below:

App Target

The extension is created in the app. Follow the steps below to add the extension target:

  1. In Xcode, select your project in the navigator (top item with blue icon)
  2. At the bottom of the targets list, click the + button (it says "Add Target")
  3. In the template chooser that appears:
    • Platform filter: Make sure "macOS" is selected at the top
    • Search box: Type "source editor" or scroll down to find it
    • Template: Look for "Xcode Source Editor Extension" (it has an Xcode icon)
    • Note that it must not be "App Extension" or "System Extension"
  4. Click Next
  5. Product Name: "CodeReviewExtension"
  6. Language: Swift
  7. Click Finish
  8. When Xcode asks to activate the scheme, click Activate

The is now an extension target in your project and Xcode should look like the screenshot below:

Extension Target

Configure the Build Settings

The extension needs to be signed for development. It may already be set up that way, but follow the steps below to ensure, that it is:

Repeat for the CodeReviewAssistant target.

Implement the Extension

Two files need to be implemented: `SourceEditorCommand.swift` and `SourceEditorExtension.swift`.

The `SourceEditorCommand` is already created by Xcode but needs to be changed:

        
import Foundation
import XcodeKit

class SourceEditorCommand: NSObject, XCSourceEditorCommand {
    func perform(with invocation: XCSourceEditorCommandInvocation, completionHandler: @escaping (Error?) -> Void ) -> Void {
        Task {
            do {
                guard let selection = invocation.buffer.selections.firstObject as? XCSourceTextRange else {
                    throw NSError(
                        domain: "CodeReview",
                        code: 1,
                        userInfo: [NSLocalizedDescriptionKey: "No code selected"])
                }
                let selectedCode = extractCode(from: invocation.buffer, range: selection)
                let suggestions = try await analyzeCode(selectedCode)
                insertReviewComments(suggestions, into: invocation.buffer, at: selection.start.line)

                completionHandler(nil)
            } catch {
                completionHandler(error)
            }
        }
    }

    private func extractCode(from buffer: XCSourceTextBuffer, range: XCSourceTextRange) -> String {
        let startLine = range.start.line
        var endLine = range.end.line
        if endLine == buffer.lines.count {
            endLine = endLine - 1
        }
        let lines = buffer.lines.compactMap { $0 as? String }
        guard startLine <= endLine && endLine < lines.count else {
            return ""
        }
        return lines[startLine...endLine].joined()
    }

    private func analyzeCode(_ code: String) async throws -> [String] {
        let analyzer = CodeAnalyzer()
        return try await analyzer.getReviewSuggestions(for: code)
    }

    private func insertReviewComments(_ suggestions: [String],
                                     into buffer: XCSourceTextBuffer,
                                     at line: Int) {
        var currentLine = line
        for suggestion in suggestions {
            let comment = "// 🤖 AI Review: \(suggestion)\n"
            buffer.lines.insert(comment, at: currentLine)
            currentLine += 1
        }
    }
}
    

The `SourceEditorExtension` is already created by Xcode and it can be left unchanged:

        
import Foundation
import XcodeKit

class SourceEditorExtension: NSObject, XCSourceEditorExtension {
    /*
    func extensionDidFinishLaunching() {
        // If your extension needs to do any work at launch, implement this optional method.
    }
    */

    /*
    var commandDefinitions: [[XCSourceEditorCommandDefinitionKey: Any]] {
        // If your extension needs to return a collection of command definitions that differs from those in its Info.plist, implement this optional property getter.
        return []
    }
    */
}
    

The main work is done in the `CodeAnalyzer` class, which is shown below and which you need to add to your extension:

        
import Foundation

class CodeAnalyzer {
    private let serverURL: URL

    init() {
        self.serverURL = URL(string: "http://localhost:8000")!
    }

    func getReviewSuggestions(for code: String) async throws -> [String] {
        let prompt = buildPrompt(for: code)
        do {
            let response = try await runLlama(prompt: prompt)
            return parseResponse(response)
        } catch {
            return ["Please start your local LLM for code review assistance"]
        }
    }

    private func buildPrompt(for code: String) -> String {
        """
        You are a senior Swift developer. Analyze this code and provide 2 to 3 comments regarding readability and Swift best practices.

        Code to review:
        ```swift
        \(code)
        ```

        Provide each comment on a new line, starting with a dash (-).
        """
    }

    private func runLlama(prompt: String) async throws -> String {
        let requestBody: [String: Any] = [
            "prompt": prompt,
            "n_predict": 256,      // Max tokens
            "temperature": 0.2,    // Low temperature for consistent output
            "top_p": 0.9,
            "top_k": 40
        ]

        let jsonData = try JSONSerialization.data(withJSONObject: requestBody)

        var request = URLRequest(url: serverURL.appendingPathComponent("/completion"))
        request.httpMethod = "POST"
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        request.httpBody = jsonData

        let (data, response) = try await URLSession.shared.data(for: request)
        guard let httpResponse = response as? HTTPURLResponse,
              (200...299).contains(httpResponse.statusCode) else {
            throw NSError(domain: "CodeAnalyzer", code: -1, userInfo: [NSLocalizedDescriptionKey: "Failed to get response from llama server"])
        }

        if let jsonResponse = try JSONSerialization.jsonObject(with: data) as? [String: Any],
           let content = jsonResponse["content"] as? String {
            return content
        }

        return ""
    }

    private func parseResponse(_ response: String) -> [String] {
        // Extract suggestions from LLM output
        let lines = response.components(separatedBy: .newlines)
        return lines
            .filter { $0.hasPrefix("-") || $0.hasPrefix("*") }
            .map { $0.trimmingCharacters(in: CharacterSet(charactersIn: "-* ")) }
            .filter { !$0.isEmpty }
    }
}
    

Just to check that everything is set up correctly, this is how the `Info.plist` of the extension should look like:

        
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
    <dict>
        <key>NSExtension</key>
        <dict>
            <key>NSExtensionAttributes</key>
            <dict>
                <key>XCSourceEditorCommandDefinitions</key>
                <array>
                    <dict>
                        <key>XCSourceEditorCommandClassName</key>
                        <string>$(PRODUCT_MODULE_NAME).SourceEditorCommand</string>
                        <key>XCSourceEditorCommandIdentifier</key>
                        <string>$(PRODUCT_BUNDLE_IDENTIFIER).SourceEditorCommand</string>
                        <key>XCSourceEditorCommandName</key>
                        <string>AI Code Review</string>
                    </dict>
                </array>
                <key>XCSourceEditorExtensionPrincipalClass</key>
                <string>$(PRODUCT_MODULE_NAME).SourceEditorExtension</string>
            </dict>
            <key>NSExtensionPointIdentifier</key>
            <string>com.apple.dt.Xcode.extension.source-editor</string>
        </dict>
    </dict>
</plist>
    

Preparing Testing

To test the extension, ensure that the framework settings are configured correctly. Select the project in the Project Navigator, then choose the extension target. Open the "General" tab and scroll down to "Frameworks and Libraries". Set the embed option for "XcodeKit.framework" to "Embed & Sign".

Framework

When testing the extension, it is launched within Xcode. To configure this, edit the scheme for the extension target. In the Run section, open the Info tab. For the Executable setting, choose Other… and select Xcode from the Applications folder. This ensures that the extension runs within Xcode during testing.

Schema Settings

The signing settings should be configured for automatic signing using a development certificate. This is typically the default configuration, but it can be verified in the extension target’s Signing & Capabilities settings.

Sandbox Info

Smoke Test

You are now ready to test the extension. Ensure that the extension’s scheme is selected, then click the Run button. A new instance of Xcode will launch, labeled “Inferior,” indicating that it is a dedicated session for extension testing.

Open a suitable project and select a code snippet within a Swift file. In Xcode’s Editor menu, locate the CodeAnalyzer entry and choose AI Code Review from its submenu. The extension will then insert a line above the selected code in the editor.

First run

The Xcode Source Editor extension is now prepared and ready to run. You can set breakpoints within your extension’s code to monitor its behavior.

Since the local LLM server has not yet been started, the extension will display a message indicating that it could not find the LLM server. The next part will cover how to start the LLM server and install the extension for general use.

 

www.admadic.de | webmaster@admadic.de | Legal Notice and Trademarks | Privacy
© 2005-2007 - admaDIC | All Rights Reserved
All other trademarks and/or registered trademarks are the property of their respective owners
Last Change: Fri Apr 24 06:42:29 2026 GMT