🪶

Writing code with intelligence in Xcode

に公開

Xcode26.0からAI Agentが使える

皆さんこんにちわJboyです。
Appleが、Xcode26.0からCoding Agentなるものを搭載しました。これはどんなものかと言うと、Vscode/Cursor/Windsurfのエージェントモードと同じように、プロンプトを入力したり画像のuploadをすると、コードを自動生成してくれる機能です。

こちらが公式のリンク👇
https://developer.apple.com/documentation/Xcode/writing-code-with-intelligence-in-xcode

使用方法

ChatGPT DesktopかClaude Desktopが必要です。有料ツールを使うのでアカウントも作成しておいてください。

iOSDCに行った日に、MacOS26までバージョン上げないと使用できないそうで、Mac miniのOSのバージョンを上げました。そしたら画面左に、AIのアイコンが表示されたので使えるようになりました!

AIツールはこちらを使用します
https://chatgpt.com/ja-JP/features/desktop/
https://claude.ai/download

実際に動かして動画も撮ってみました。ChatDesktopを使用しております。

https://youtu.be/1QrA62rPxpI?si=ITERnVtSqugLhghT
https://youtu.be/7T8THYYee6Y?si=80iK8iM9kGNmHHWj

電卓とストップウォッチのソースコードを生成させてみました。使ってみた感じ失敗することもあったが、修正すると精度の高いものに仕上がりました!

ストップウォッチ
import SwiftUI

struct StopWatchView: View {
    // MARK: - State
    @State private var isRunning: Bool = false
    @State private var baseElapsed: TimeInterval = 0 // 累積経過時間(停止中も保持)
    @State private var startDate: Date? = nil        // 計測再開時の開始時刻
    @State private var tick: Date = Date()           // 画面更新トリガー

    // MARK: - Body
    var body: some View {
        VStack(spacing: 24) {
            Spacer()

            // Time Display
            Text(formattedElapsed)
                .font(.system(size: 64, weight: .semibold, design: .rounded))
                .monospacedDigit()
                .minimumScaleFactor(0.5)
                .lineLimit(1)
                .padding(.horizontal)
                .accessibilityLabel("経過時間")
                .accessibilityValue(formattedElapsed)

            // Controls
            HStack(spacing: 16) {
                Button("スタート", action: start)
                    .buttonStyle(.borderedProminent)
                    .tint(.green)
                    .disabled(isRunning)

                Button("ストップ", action: stop)
                    .buttonStyle(.borderedProminent)
                    .tint(.red)
                    .disabled(!isRunning)

                Button("リセット", action: reset)
                    .buttonStyle(.bordered)
                    .tint(.gray)
                    .disabled(!canReset)
            }
            .padding(.horizontal)

            Spacer()
        }
        .task(id: isRunning) {
            guard isRunning else { return }
            // スタート直後の即時更新
            await MainActor.run { tick = Date() }
            // 10ms 間隔で tick を更新(タスクがキャンセルされるまで)
            while !Task.isCancelled {
                try? await Task.sleep(nanoseconds: 10_000_000)
                await MainActor.run { tick = Date() }
            }
        }
        .navigationTitle("ストップウォッチ")
    }

    // MARK: - Derived
    private var currentElapsed: TimeInterval {
        if isRunning, let startDate {
            return baseElapsed + tick.timeIntervalSince(startDate)
        } else {
            return baseElapsed
        }
    }

    private var formattedElapsed: String { format(currentElapsed) }

    private var canReset: Bool {
        // 計測中は常にリセット可能、停止中は経過があるときのみ
        isRunning || currentElapsed > 0.0
    }

    // MARK: - Actions
    private func start() {
        guard !isRunning else { return }
        let now = Date()
        startDate = now
        tick = now
        isRunning = true
    }

    private func stop() {
        guard isRunning else { return }
        let now = Date()
        if let startDate {
            baseElapsed += now.timeIntervalSince(startDate)
        }
        isRunning = false
        self.startDate = nil
        tick = now
    }

    private func reset() {
        baseElapsed = 0
        let now = Date()
        if isRunning {
            // 計測中にリセットした場合は 0 から継続
            startDate = now
            tick = now
        } else {
            startDate = nil
            tick = now
        }
    }

    // MARK: - Formatting
    private func format(_ t: TimeInterval) -> String {
        let totalCentiseconds = Int((t * 100).rounded())
        let cs = totalCentiseconds % 100
        let totalSeconds = totalCentiseconds / 100
        let s = totalSeconds % 60
        let totalMinutes = totalSeconds / 60
        let m = totalMinutes % 60
        let h = totalMinutes / 60

        if h > 0 {
            return String(format: "%d:%02d:%02d.%02d", h, m, s, cs)
        } else {
            return String(format: "%02d:%02d.%02d", m, s, cs)
        }
    }
}

#Preview {
    StopWatchView()
}

電卓
import SwiftUI

struct ContentView: View {
    // MARK: - Calculator State
    @State private var display: String = "0"
    @State private var accumulator: Double? = nil
    @State private var pendingOperation: Operation? = nil
    @State private var currentInput: String = ""
    @State private var isError: Bool = false

    enum Operation {
        case add, subtract, multiply, divide
    }

    // MARK: - Derived Strings
    private var expression: String {
        if isError { return "" }
        if let acc = accumulator, let op = pendingOperation {
            let left = format(acc)
            let sym = symbol(for: op)
            if currentInput.isEmpty {
                return "\(left) \(sym)"
            } else {
                return "\(left) \(sym) \(currentInput)"
            }
        } else {
            return currentInput.isEmpty ? "" : currentInput
        }
    }

    private func symbol(for op: Operation) -> String {
        switch op {
        case .add: return "+"
        case .subtract: return "−"
        case .multiply: return "×"
        case .divide: return "÷"
        }
    }

    // MARK: - UI
    var body: some View {
        VStack(spacing: 16) {
            Spacer(minLength: 0)

            // Display
            HStack {
                Spacer()
                VStack(alignment: .trailing, spacing: 4) {
                    Text(expression)
                        .font(.system(size: 20, weight: .regular, design: .rounded))
                        .foregroundStyle(.secondary)
                        .lineLimit(1)
                        .minimumScaleFactor(0.5)
                        .opacity(expression.isEmpty ? 0 : 1)
                    Text(display)
                        .font(.system(size: 64, weight: .light, design: .rounded))
                        .lineLimit(1)
                        .minimumScaleFactor(0.3)
                        .monospacedDigit()
                }
                .padding(.horizontal)
            }

            // Buttons Grid
            Grid(horizontalSpacing: 12, verticalSpacing: 12) {
                GridRow {
                    calcButton(title: "AC", bg: .gray.opacity(0.25), fg: .primary) { allClear() }
                        .gridCellColumns(3)
                    opButton("÷", op: .divide)
                }

                GridRow {
                    digitButton("7")
                    digitButton("8")
                    digitButton("9")
                    opButton("×", op: .multiply)
                }

                GridRow {
                    digitButton("4")
                    digitButton("5")
                    digitButton("6")
                    opButton("−", op: .subtract)
                }

                GridRow {
                    digitButton("1")
                    digitButton("2")
                    digitButton("3")
                    opButton("+", op: .add)
                }

                GridRow {
                    digitButton("0")
                        .gridCellColumns(2)
                    decimalButton()
                    equalsButton()
                }
            }
            .padding(.horizontal)
            .padding(.bottom)
        }
        .padding(.top)
    }

    // MARK: - Button Builders
    private func digitButton(_ title: String) -> some View {
        calcButton(title: title, bg: .gray.opacity(0.15)) {
            appendDigit(title)
        }
    }

    private func decimalButton() -> some View {
        calcButton(title: ".", bg: .gray.opacity(0.15)) {
            appendDecimal()
        }
    }

    private func opButton(_ symbol: String, op: Operation) -> some View {
        calcButton(title: symbol, bg: .orange, fg: .white) {
            selectOperation(op)
        }
    }

    private func equalsButton() -> some View {
        calcButton(title: "=", bg: .orange, fg: .white) {
            equals()
        }
    }

    private func calcButton(title: String, bg: Color, fg: Color = .primary, action: @escaping () -> Void) -> some View {
        Button(action: action) {
            Text(title)
                .font(.system(size: 28, weight: .semibold, design: .rounded))
                .frame(maxWidth: .infinity, minHeight: 64)
                .contentShape(RoundedRectangle(cornerRadius: 16, style: .continuous))
        }
        .buttonStyle(.plain)
        .foregroundStyle(fg)
        .background(bg, in: RoundedRectangle(cornerRadius: 16, style: .continuous))
    }

    // MARK: - Logic
    private func appendDigit(_ d: String) {
        if isError { allClear() }
        // Limit length to avoid overflow
        guard currentInput.count < 12 else { return }

        if currentInput == "0" {
            // Replace leading zero unless adding decimal later
            currentInput = d
        } else {
            currentInput.append(d)
        }
        display = currentInput
    }

    private func appendDecimal() {
        if isError { allClear() }
        if currentInput.isEmpty { currentInput = "0" }
        guard !currentInput.contains(".") else { return }
        currentInput.append(".")
        display = currentInput
    }

    private func selectOperation(_ op: Operation) {
        if isError { allClear() }
        if let value = Double(currentInput) {
            if let acc = accumulator, let pending = pendingOperation {
                // Compute chained operation first
                if let result = compute(lhs: acc, op: pending, rhs: value) {
                    accumulator = result
                    display = format(result)
                } else {
                    showError()
                    return
                }
            } else {
                accumulator = value
            }
            currentInput = ""
        }
        // If no current input but we have an accumulator, just change the pending op
        pendingOperation = op
    }

    private func equals() {
        if isError { allClear(); return }
        guard let acc = accumulator, let pending = pendingOperation, let value = Double(currentInput) else { return }
        if let result = compute(lhs: acc, op: pending, rhs: value) {
            display = format(result)
            accumulator = result
            pendingOperation = nil
            currentInput = ""
        } else {
            showError()
        }
    }

    private func allClear() {
        accumulator = nil
        pendingOperation = nil
        currentInput = ""
        isError = false
        display = "0"
    }

    private func compute(lhs: Double, op: Operation, rhs: Double) -> Double? {
        switch op {
        case .add:      return lhs + rhs
        case .subtract: return lhs - rhs
        case .multiply: return lhs * rhs
        case .divide:
            if rhs == 0 { return nil }
            return lhs / rhs
        }
    }

    private func showError() {
        display = "Error"
        isError = true
        accumulator = nil
        pendingOperation = nil
        currentInput = ""
    }

    private func format(_ x: Double) -> String {
        if x.isNaN || x.isInfinite { return "Error" }
        let roundedToInt = x.rounded()
        if abs(roundedToInt - x) < 1e-10 {
            return String(Int(roundedToInt))
        }
        var s = String(format: "%.8f", x)
        // Trim trailing zeros
        while s.contains(".") && s.last == "0" { s.removeLast() }
        if s.last == "." { s.removeLast() }
        return s
    }
}

#Preview {
    ContentView()
}

最後に

使ってみた感想ですが、GitHub Copilot For Xcodeと比較すると劣ってはいないように思えました。違いがあるとしたら、質問機能のaskのpull downがないことでしょうか。プロンプトをコンテキストを読み込んで質問をすれば回答はしてくれるので、ここは慣れかなと思います。

ファイル名を指定して質問を投げるとこんな感じで回答が返ってきます。これがあれば多分Cursorはいらなくなるだろうと思いました。できれば、同じIDEで操作は完結したいので💦

ご興味ある方は試してみてください!

Discussion