Opus 4.7とGX10 QwenのSwiftUIボタン幅制約のコード比較

Article

同じ電卓アプリのソースを読む: Opus 4.7とGX10 Qwenの違い

2026年5月26日
8 min read
ローカルAI
#Claude Code#GX10 Qwen#SwiftUI#コードレビュー#ローカルAI

前回の記事では、Claude Codeに同じようなSwiftUI電卓アプリを作らせた。

Opus 4.7は、Simulator上で見た目まで成立する電卓を作った。GX10 QwenもビルドとSimulator起動までは到達したが、最終UIはボタン幅と配置が崩れていた。

今回は、その差がソースコードにどう出ていたのかを見る。

Opus 4.7とGX10 Qwenのボタン幅制約のコード比較

結論から書くと、Opusのコードはあまり凝っていない。単一の ContentView.swift にビューと簡単なViewModelをまとめた、かなり素朴なSwiftUIコードだ。

一方で、GX10 Qwenのコードは見た目には設計されている。CalculatorLogic.swiftCalculatorView.swift を分け、@Observable を使い、ButtonItemButtonRow という構造も作っている。

ただし、アプリとして成立していたのはOpus側だった。

ここが面白い。コードの見た目の設計感と、実際にユーザー経路がつながっているかは別の話だった。

ファイル構成の違い

Opus 4.7が作った主なソースは、ほぼ単一ファイルだった。

CalculatorApp.swift
ContentView.swift

ContentView.swift の中に、演算子 enum、ViewModel、ボタンView、画面全体が入っている。きれいに分割された設計というより、まず動くものを作る構成だ。

GX10 Qwenは、もう少し分割していた。

CalculatorApp.swift
CalculatorLogic.swift
CalculatorView.swift

計算ロジックとビューが分かれているので、ぱっと見ではQwenのほうが「ちゃんと設計している」ように見える。

ソース構成と実用上の差の比較

しかし、今回のような小さなUIアプリでは、分割そのものはゴールではない。ユーザーが押すボタン、表示、演算、エラー処理までがつながって初めて意味がある。

決定的だったのはボタン幅

最終スクリーンショットの差は、レイアウトコードを読むとかなり直接的に説明できる。

Opus側のボタンは、普通に横へ広がる。

Text(label)
    .font(.system(size: 32, weight: .medium))
    .foregroundColor(foreground)
    .frame(maxWidth: .infinity, maxHeight: .infinity)

.frame(height: 72)
.background(background)
.clipShape(Capsule())

重要なのは .frame(maxWidth: .infinity, maxHeight: .infinity) だ。4つのボタンを HStack に並べたとき、各ボタンが横方向に広がる。結果として、普通の4列グリッドになる。

Qwen側は、ButtonItemButtonRowcolumnSpan まで用意していた。

ButtonItem(title: "0", style: .darkGray, action: { logic.inputDigit("0") }, columnSpan: 2)

ところが、実際の幅指定はこうなっていた。

.frame(maxWidth: columnSpan > 1 ? .infinity : .none)
.frame(height: 80)

通常ボタンは columnSpan が1なので、maxWidth: .none になる。つまり、横に広がらない。テキストの幅に近い細いボタンになる。

一方で、0ボタンだけは columnSpan: 2 なので .infinity で横に伸びる。

Opus 4.7とGX10 Qwenのボタン幅制約

これが、Qwen版の画面で細長い通常ボタンと、巨大な0ボタンが同時に出た理由だ。

GX10 Qwen版UI崩れの注釈

ここで起きていたのは、SwiftUIのコンパイルエラーではない。抽象化の名前と、実際のレイアウト制約が一致していなかった。

columnSpan という名前はグリッドを想起させる。しかし実装は、4列の幅配分を作っていない。結果として、コードは構造化されているのにUIは崩れた。

計算ロジックにも差があった

画面だけでなく、計算ロジックにも差があった。

Opus側は、次の演算子を選ぶ前にpending operationを解決する。

if pendingOp != nil && isTypingNumber {
    accumulator = compute(lhs: accumulator, rhs: current, op: pendingOp!)
    display = format(accumulator)
} else {
    accumulator = current
}
pendingOp = op

これは完全な式パーサではないが、普通の電卓らしい左結合の動きに近い。たとえば 2 + 3 × と押したとき、まず 2 + 3 を解決してから次の演算に進む。

Qwen側は、演算子選択がかなり単純だった。

func selectOperation(_ op: String) {
    operation = op
    shouldResetOperand = true
    previousOperand = currentOperand
}

この実装だと、次の演算子を押したときに、未解決の演算を先に処理しない。演算子と前値を上書きするだけになる。つまり、連続演算への対応は、ターミナル上の説明ほど実装されていない。

Opus 4.7とGX10 Qwenの連続演算処理の違い

ここでも、Opusは地味だがユーザーの操作経路に近い。Qwenは設計っぽい形を作るが、状態遷移の詰めが甘い。

Qwenが勝っていた部分もある

ただし、Opusがすべて上という話ではない。

ゼロ除算の扱いは、Qwenのほうが自然だった。

Opusは、割る数が0のときに0を返していた。

case .div: return rhs == 0 ? 0 : lhs / rhs

これは電卓としてはかなり微妙だ。0除算を0として表示すると、ユーザーには何が起きたのか分からない。

Qwenは "Error" を表示し、pending stateもクリアしていた。

if current == 0 {
    currentOperand = "Error"
    previousOperand = nil
    operation = nil
    return
}

この判断はQwenのほうがよい。

また、ロジックとビューを分けたこと自体も悪くない。ボタン定義を ButtonItem として扱おうとしたのも、方向性としては分かる。

問題は、その構造が最後の画面と操作に結びついていなかったことだ。

使われない機能もあった

Qwenの CalculatorLogic には backspace() がある。

func backspace() {
    guard !currentOperand.isEmpty, currentOperand != "Error" else {
        currentOperand = "0"
        return
    }
    currentOperand.removeLast()
    if currentOperand.isEmpty || currentOperand == "-" {
        currentOperand = "0"
    }
}

しかし、見えているUIにはバックスペースボタンがない。

これはコーディングエージェントでよく見るパターンだと思う。内部関数を生成した時点で、モデルはそれを「機能」として数えがちだ。しかし、ユーザーが触れる経路につながっていなければ、プロダクトの機能とは言いにくい。

Observationの選択も差として出た

Opusは、古いが安定したSwiftUIの状態管理を使っていた。

final class CalculatorViewModel: ObservableObject
@Published var display: String = "0"
@StateObject private var vm = CalculatorViewModel()

Qwenは新しいObservationを使っていた。

@Observable
class CalculatorLogic
@State private var logic = CalculatorLogic()

これは現代的で、間違いではない。ただ、対応するdeployment targetやビルド設定の面倒も増える。実際、Qwen側のツールループでは、この周辺の設定を調整する場面があった。

小さな電卓アプリでは、Opusの退屈な選択が強かった。

評価すべきはファイル数ではない

今回の比較でいちばん面白かったのは、Qwenのコードが一見するとOpusより整って見えることだ。

分割されている。Observationを使っている。ボタンの構造体もある。ゼロ除算の扱いもよい。

それでも、触れるアプリとしてはOpusのほうが成立していた。

Opus 4.7が作った電卓UI

GX10 Qwenが作った崩れた電卓UI

ローカルLLMの評価は、生成されたファイル数や抽象化の数ではなく、ユーザーが触れる経路が最後までつながっているかで見るべきだと思う。

ビルド成功はチェックポイントであって、ゴールではない。

特にUIアプリでは、最後にスクリーンショットを見ることがほぼテストになる。今回のQwen版は、コンパイルと起動までは通った。しかし、画面を見た瞬間に「まだ完成ではない」と分かる。

次にやるなら

次に同じ実験をするなら、Qwenには最初からこう指示したい。

  • XcodeGenプロジェクトを最初から作る
  • 4列グリッドの全ボタンを等幅にする
  • 0ボタンだけ2列分にする場合も、明示的なgrid幅計算を入れる
  • ビルド後にSimulatorスクリーンショットを撮る
  • スクリーンショットを見て、UIが崩れていたら自分で修正する

ローカルQwenは、安く何度も回せる。だからこそ、1回で完成させるよりも、スクリーンショット確認と修正ループを前提にしたほうがよさそうだ。

今回の実験で見えたのは、GX10 QwenがClaude Codeのツールループに入れるということ。そして、そこから実用的なコーディングエージェントにするには、ビルド、画面、ソースレビューを分けて見なければいけないということだった。