Cursorは個人開発者を救う。不慣れな分野でも生成AIの力でやりたいことをやり切る方法

Cursorは個人開発者を救う。不慣れな分野でも生成AIの力でやりたいことをやり切る方法
Photo by Aerps.com / Unsplash

生成AIの進化は凄まじく、毎日新しい話題がつきません。
この度、CursorやChatGPTを使って、個人開発のアプリの新機能を実装したんですが、とても体験が良かったので共有したくてブログにまとめす。
少し前なら、構想だけで終わった内容が、実際に開発をして、アプリをリリースするところまでできたことに驚きを隠せません。
生成AIを利用した一体験を共有して、現状の、特にiOS開発でどう使えるのかをお伝えしたいと思います。

今回使ったツールはこちらです。

  • Cursor v0.50
    • モデル: Claude 3.7 Sonnet
  • ChatGPT
    • 4o, o3など
  • Xcode 16.2

⚠️使いにくいUI

私は風水のアプリを開発しています。

LPImage-4.png

風水は住居の方角によって吉凶を占います。
機能の一つに図面を取り込み、その住居を占う機能があります。引っ越し先の住居を占うことを想定しています。
図面を風水で診断するにあたって、偏角の入力が必要になります。
偏角とは真北と磁北の差の角度です。
図面の北は真北を指していますが、風水ではコンパスを使うのでコンパスが指す北、磁北で判定をしなければいけません。
この偏角、東京で2020年では大体7.4度、2025年では7.9度ずれているので、計算しないと不正確な情報で診断することになります。

開発時点のアプリでは下記のフローに沿って入力をさせていました。

  • 偏角の設定画面に国土地理院の地図サイトへのリンクを置く。
  • お客さまはアプリのボタンをタップして国土地理院の地図ページを表示し、自分が引っ越す場所の偏角を自分で探す。
  • 偏角設定画面に戻ってその値を自分で入力する

ステップが多くてわかりにくい状態でした。
何をすればいいのかのお問い合わせも実際にあり、お客さまに操作を迷わせるUIになっていると感じていました。
なんとか使いやすいUIに変えたいと思いました。

🔨開発のきっかけ

偏角の計算方法を調べていく中で、NOAA(米国海洋大気庁)が提供するIGRF(International Geomagnetic Reference Field)という地磁気モデルを使えば、プログラムで計算できることがわかりました。

🌎IGRFとは?

IGRF(International Geomagnetic Reference Field)は、地球の主磁場(主に地球内部の外核で発生する磁場)を数学的に表現した標準モデルです。

このモデルは、地球深部の構造、地殻、電離圏、磁気圏の研究に広く利用されています。
衛星の姿勢制御や航法システム、地磁気偏角(磁北と真北の差)の計算にも利用されます。

地球の主磁場を球面調和関数の級数展開で表現することで、地球の中心からの距離、緯度、経度、時間を変数とするスカラー磁場ポテンシャル関数を用いて、地磁気ベクトル場を計算します。

地磁気ポテンシャルが何かというと「どのくらい磁場が強いか」の表すものです。
コンパスの磁石が北や東のどちらの方向を向くか、どれくらい地球中心に引っぱられるか、その力はどれくらい強いかを表します。

この関数は、ガウス係数と呼ばれる一連の係数を用いて、球面調和関数の形で表現されます。最新の係数は2024年11月に公開された第14世代(IGRF-14)です。1900年から2030年までの地球の主磁場を表現します。

噛み砕いて説明すると、「IGRFモデルという地球の磁場を計算する式があり、最新版のIGRF-14世代のモデルを利用すると1900年から2030年までの磁場を計算できる」ということですね。

数式はこちらで表されます。

$$ V(r, \theta, \lambda, t) = a \sum_{n=1}^{N} \sum_{m=0}^{n} \left( \frac{a}{r} \right)^{n+1} \left[ g_n^m(t) \cos(m\lambda) + h_n^m(t) \sin(m\lambda) \right] P_n^m(\cos\theta) $$

この数式は「ある時刻tにおいて、地球のある地点(𝑟,𝜃,𝜆における地磁気のポテンシャルVを計算する」数式です。

  • 入力
    • r: 観測点の地心距離(地球中心からの距離)
      • 単位:km
    • θ: 地心余緯度(co-latitude:天頂角)
      • 単位: ラジアン
    • λ: 経度(longitude)
      • 単位: ラジアン
    • t: 時刻(観測時刻またはモデル計算時刻)
      • 単位: 年(小数年)
  • 出力
    • V: 地磁気ポテンシャル(スカラー場)

ある時刻tと地球のある地点(𝑟,𝜃,𝜆)を入れるとV(地磁気ポテンシャル)がわかるという仕組みです。
さらに、地磁気ポテンシャルVがわかると磁場ベクトル成分を導けて、磁場ベクトル成分から直交座標系X(北向き成分),Y(東向き成分),Z(鉛直成分)へ変換し、XとYから偏角Dが導けるというわけです。

磁場ベクトル成分の導出すると

$$ \begin{aligned} B_r &= -\frac{\partial V}{\partial r} && \text{(半径方向:地心直下)} \\ B_\theta &= -\frac{1}{r} \frac{\partial V}{\partial \theta} && \text{(緯度方向)} \\ B_\lambda &= -\frac{1}{r \sin \theta} \frac{\partial V}{\partial \lambda} && \text{(経度方向)} \end{aligned} $$

そこから偏角Dを計算

$$ D = \tan^{-1} \left( \frac{Y}{X} \right) $$

このように最終的にはある地点、ある時刻からその場所の偏角が計算できるのです。
これはアプリ開発者としてはだいぶ嬉しいことです。
アプリのローカルで計算ができるので、計算プログラムをSwiftで実現できれば、それで済むからです。別途サーバー立てたりしなくていいのは楽ですね。

入力値は緯度経度と時刻なのでMapKitを使って地図上で引っ越し先住居を選ぶようにすれば、風水アプリでも偏角の計算がスムーズになりそうと確信を持ちました。

🐍IGRFのPythonプログラムを動かしてみる

International Geomagnetic Reference Field (IGRF)をよく見ると、Python 3.7 Packageのリンクがあり、PythonのCLIツールが公開されていました。

スクリーンショット 2025-06-08 19.33.59.png

なのでPythonコードをSwiftに変換すれば風水アプリで偏角を導き出せそうです。

Cursorのask機能でPythonコードの動かし方を把握

ただし私はPython自体も不慣れでどう動かすかも検討つかない状態でした。
Cursorのaskの機能でコードの理解を進めました。

  • requirements.txtで依存ライブラリーをする
  • main関数が書かれているpythonをターミナルで実行

CLIツールを実行すると、このようなプロンプトが現れます。

******************************************************
*              IGRF SYNTHESIS PROGRAM                *
*                                                    *
* A program for the computation of geomagnetic       *
* field elements from the International Geomagnetic  *
* Reference Field (14th generation) as revised in    *
* December 2024 by the IAGA Working Group V-MOD.     *
*                                                    *
* It is valid for dates from 1900.0 to 2030.0;       *
* values up to 2035.0 will be computed with          *
* reduced accuracy. Values for dates before 1945.0   *
* and after 2020.0 are non-definitive, otherwise     *
* the values are definitive.                         *
*                                                    *
*                                                    *
*            (on behalf of) IAGA Working Group V-MOD *
******************************************************

地球磁場を導くにはターミナルに追加で入力が必要です。

例えば、

  • IGRFのモデルの世代(デフォルトは最新の14)
  • 出力するファイル名
  • 緯度経度の形式、座標入力の形式
  • 緯度経度、高度、年代の入力

すべて入力すると結果が出力されます。
次は東京駅周辺の2025年1月1日の入力結果です。

Geomagnetic field values at:  35.6812° / 139.7671248°, at altitude 0.0 for 2025.0 using IGRF-14
Declination (D): -7.852 ° // 偏角
Inclination (I):  49.502 °
Horizontal intensity (H):  30397.1 nT
Total intensity (F)     :  46806.1 nT
North component (X)     :  30112.1 nT // 磁場ベクトル成分X
East component (Y)      : -4152.5 nT // 磁場ベクトル成分Y
Vertical component (Z)  :  35592.6 nT // 磁場ベクトル成Z
Declination SV (D): -2.40 arcmin/yr
Inclination SV (I):  1.25 arcmin/yr
Horizontal SV (H):  6.5 nT/yr
Total SV (F)     :  29.9 nT/yr
North SV (X)     :  3.5 nT/yr
East SV (Y)      : -21.9 nT/yr
Vertical SV (Z)  :  33.8 nT/yr

Declination (D): -7.852 °と書かれている通り、偏角は-7.852度ということがわかります。偏角はマイナス値ですと磁北が真北よりも西側にずれていることを意味します。

CLIツールとしての動かし方を理解したのでコードを読みながらSwiftに変換していきました。
動かしながらも慣れないPythonコードと初めての分野の処理で戸惑いはありました。
しかし、理解できないコードを書くのはリスクです。
わからない処理がある度にCursorに「この関数がやっていることは?」「この出力はどういう意味?」「この変数の役割は?」と1行ずつCursorに聞いて理解を進めました。環境構築も含めて、やり方をいちいちAIに聞いて一歩ずつ進んでいった感じです。

🍏IGRFのPythonプログラムをSwiftに変換

Pythonのプログラムを読んでみると、下記のようなステップで計算をしていました。

  1. 入力値からIGRFモデルの係数ファイルを読み込む
  2. オプションを選択させる。オプション1は一座標、一時刻の値、オプション2は一座標で複数年指定、オプション3は一時刻に対して緯度経度をグリッド上で計算
  3. 各オプションに従って緯度経度、高度、時刻を入力
  4. 結果を出力

Swiftコードへの変換はこのステップを頭から進めました。
SwiftのプロジェクトはSwiftPMで行い、Cursorでコマンド教えてもらいながら、下記のコマンドで初期化をして進めています。

swift package init

基本的にはまずは愚直にPythonコードをSwiftコードに変換して、その後Swiftらしいコードになるようにリファクタリングをしました。

例えば、オプション1のPythonのコードの定義はこうなっています。

def option1():
    // 緯度経度などの入力値を変数に代入
    return date, alt, lat, colat, lon, itype, sd, cd

Swiftコードでは8つも要素があるタプルを実装するのは読みにくいので避けるべきですが、一旦割り切って型の設計はせずに進めました。

struct IOOptions {
    static func option1() -> (
        date: Double, alt: Double, lat: Double, colat: Double, lon: Double, itype: Int, sd: Double,
        cd: Double
    ) {...}

その後ある程度実装できたら型を設計するリファクタリングをしました。

protocol UserInputProtocol {
    associatedtype InputType: InputResultProtocol
    func input() -> InputType
}

struct SinglePointTime: UserInputProtocol {
    typealias InputType = GeomagneticInput // 8つのタプルを型にまとめる

    // オプション1
    func input() -> InputType {...}

テストで品質担保

変換は基本的にCursorのAgentモードでPythonコードをコピペして「Swiftのコードに変換して」などのプロンプトでSwiftコードを生成していました。

ここで気にあるのは「生成されたコードが正しいかどうか」です。

そこで、品質の担保をするためにテストをたくさん書きました。

幸い、正しいとわかっているPythonコードがあるので、テストはかけそうです。
入力値と出力値をprint文で出力して、Swiftでそれを期待値となるテストコードを書いていきました。

例えば地球磁場の成分を計算をするsynth_valuesのテストを作るには、まずPythonコードで入力と出力を確認しました。

   // 入力値を確認
   print(f"coeffs: {coeffs}") 
    print(f"coeffs.T: {coeffs.T}")
    print(f"alt: {alt}")
    print(f"colat: {colat}")
    print(f"lon: {lon}")
    
    # Compute the main field B_r, B_theta and B_phi value for the location(s) 
    Br, Bt, Bp = iut.synth_values(coeffs.T, alt, colat, lon,
                              igrf.parameters['nmax'])

    // 出力値を確認
    print(f"Br: {Br}")
    print(f"Bt: {Bt}")
    print(f"Bp: {Bp}")

その後、Swiftでテストを書きます。
テストの作成もテストファイルで⌘+kでPythonの出力をコピペしつつ「〇〇メソッドのテストコードをPythonの出力をもとにつくって」的なことを入力するとかなり精度よいコードが出力されて楽でした。

こんな感じですね。

   func testSynthValues() {
        // 入力値の準備
        let coeffsT: [Double] = [...] // 100数十ある要素の配列
        let alt: Double = 6370.910156840483
        let colat: Double = 54.52408663919481
        let lon: Double = 139.7016
        let nmax: Int = 13

        // 期待される出力値
        let expectedBr: Double = -35691.608290802906
        let expectedBt: Double = -30013.23988558015
        let expectedBp: Double = -4161.5644558610265

        let magneticField = IGRFUtils.synthValues(
            coeffs: coeffsT,
            radius: alt,
            theta: colat,
            phi: lon,
            nmax: nmax
        )
        XCTAssertEqual(magneticField.radial, expectedBr, accuracy: 0.001)
        XCTAssertEqual(magneticField.theta, expectedBt, accuracy: 0.001)
        XCTAssertEqual(magneticField.phi, expectedBp, accuracy: 0.001)

Pythonの配列をprint文で標準出力するとSwiftと異なり要素がスペース区切り([1 2 3...]など)だったんですが、いい感じにSwiftのリテラル配列に出力されました。要素が100個を超える配列がありましたが、一発で変換されてとても便利でした。

🐛つまづいたところ

テストを書いていたところ、どうしてもテストが通らないコードがありました。
Fatal error: Range requires lower Bound <= upperBoundのランタイムエラーが発生しテストが完了せず、このエラーをCursorのプロンプトに貼り付けて修正を試みるも今度は期待結果を満たさないという状態です。

これが起こったのは、ルジャンドル多項式という計算をするメソッドで、二重配列に対して二重にループを回す計算があるコードでした。

Cursorが修正したコードは雑にループ回数削ったりして、修正すればするほどPythonコード処理とは離れてしまい、ドツボにハマってしまっていました。

最終的には私が力技で、Pythonのコードを一行づつ見て、ループが何回回っているか、その時の変数は何なのかをprintデバックしたところ、Swiftのコードでループの回数が異なることを見つけて、修正にすることができました。

Cursorの出力コードがうまくいかない場合は、地道にバグを見つけるしかないです。

Builderパターンでアプリからの呼び出し

Swiftによる、IGRFモデルのプログラムがある程度できた後は、アプリからその処理を呼び出すインターフェイスを作る必要があります。

この設計もCursorに作ってもらうことにしました。

IGRFモデルは入力に「緯度経度、高度、時刻」の入力が必要です。外からこれらの値をセットする必要があります。

はじめに自分が思いついたのは「Managerパターン」。IGRFManager的なクラスを作ってプロパティをセットして最後にIGRFモデルの計算メソッドを叩く設計です。

let manager = IGRFManager()
manager.set(latitude: 39, longitude: 139.0) // 緯度経度をセット
manager.set(alt: 0) // 高度をセット
manager.set(date: Date()) // 時刻をセット
manager.synthesize() // IGRFモデルの計算

いやーだめな設計ですね。

synthesizeメソッドは入力値がセットされた後に呼んでほしいですが、それがコンパイルで保証されておらず呼び出し側の責任になってしまってます。
またIGRFManagerの内部でプロパティを更新していく設計なのでイミュータブルな設計になっておらず、意図しないところで値が変更されるなどバグが起こりそうです。

なのでCursorのプロンプトに上記の内容を全部プロンプトに書いていい設計がないか聞いてみました。

  • IGRFモデルのプログラムを外から叩く設計がしたい
  • 要件は、必要な入力をまず行い、最後にsynthesizeを呼び出すことを保証したい
  • 自分はまずManagerパターンを思いついたが、これでは最後にsynthesizeを外から呼ぶ保証がない。なにかいい方法ないか?

するとBuilderパターンで作るとよいという回答を得られました。
入力のsetメソッドごとに返す型を変えることで、最終的にsynthesizeメソッドを最後に呼び出す保証ができる設計でした。

例えばこのような形です。

let result = try IGRFClient.create(igrfGen: .igrf14)
        .set(system: .geodetic)
        .set(
            inputLocation: .decimalDegrees(latitude: 35.6762, longitude: 139.6503)
            )
        .set(alt: 0)
        .set(date: Date())
        .synthesize()

要件と考慮したい点を伝えると設計もうまくできるのかと感心したできごとです。

💪OSSを公開

IGRFモデルのSwiftコードは、GitHubでみても誰もやってなさそうなので、せっかくなのでOSSで公開することにしました。

公開もCursorにREADMEを作成依頼し、ロゴはCharGPTを使うことでスムーズに公開ができました。

📲アプリに組み込む

IGRFのSwiftコードができたので、風水アプリに組み込んでいきます。
せっかくなので、画面デザインを作ることにしました。

画像を作成してもらいたいので、ChatGPT(o3, 4o)でデザイン作成お願いしました。

現状の画面デザインを添付して、こんなような、現状のデザインの言語化と要件をまとめたプロンプトを送ります。

偏角を計算する画面をデザインしたいです

現状のデザインを添付します

・設定項目(西偏か東偏か)、実際の偏角数が上部に来ています。
・結果を下部に表示しています。
・設定したら右上のボタンで次に進みます

このデザインをこのように変えたいです

・結果(偏角)は画面上部に表示したままにします
・必要な設定やボタンは画面下部に表示します。
・ボタンを押すと詳細設定画面がモーダルで現れて細かな入力値を入れられます
・入力値は・緯度経度、高度。MapKitの地図を表示し、ピンを立てることでユーザーは緯度経度を選択できます。MapKitに高度を取得できるメソッドがあるかは分からないですが、取得できるなら地図のみを表示します。設定が終わったらモーダルを閉じます。元の画面では偏角が計算できています。ユーザーが地図で選んだ情報、緯度経度や高度も表示されます

すると、SwiftUIのコードや、デザインの要点を返してくれたのでそれをもとに、ChatGPTに追加でやり取りをしてデザインを作っていきました。
このボタンはもっと下に配置したいとか、入力前のEmpty画面を作りたいとかですね。

ただ、出力されたコードがiOS 17.0では非推奨になったものがありワーニングが出ていました。
しかし自分はMapKitの知識が全然ない状態でした。
ChatGPTの出力結果を判断できないのはまずいと思い、WWDC23のMapKitのビデオを見て、使い方を学んだりしました。

そのあと実際に実装する段階になったらCursorを使っていました。

  • 要件定義やデザインはChatGPT
    • 画像を出力できるのがありがたい
  • 実装はCursor

これがやりやすかったです。

IGRFコードを組み込んだUI

そしてできた新しい偏角設定画面がこちらです。

ユーザーフローはこちらです。

  • 偏角を設定する前はEmpty画面を表示して、住所場所の設定を促す
  • 地図を表示して住居の選択をさせる
    • 住所はテキストフィールドでも検索できるし、右上の矢印アイコンで現在地に飛ぶこともできる。初期値は東京駅
  • 場所を設定すると偏角の計算結果が表示される。画像では-7.90°

以前よりも格段に使いやすくなっています。

🙌リリース

この修正が入った風水アプリは6/2に無事リリースされました。

この修正を思い立ったのが3/30ぐらいからだったのでまるまる2ヶ月かかったことになります。

生成AIの評判で「こんなプロ級の品質が一発で出力された!」と喧伝されることはよくありますが、まったくの未経験の分野をアプリに組み込んで納得のいく品質を保つには2ヶ月は妥当かなと思います。

🚀生成AIでやりたいことがやれる時代

今回は簡単に言えばPythonコードをSwiftに変換するのが肝でしたが、それをやりきれたことに私は感動しています。
まったく不慣れな分野の数式、しかも実装経験がないPythonコードから、最終的に自分が実現したいUIを最後までリリースできたことは、生成AIが登場するまでは考えられないことでした。

驚いたことは普通に使っても7割の品質のコードを出力することです。
ある程度プロンプトは詳しく書く必要がありますが、rulesなどCursorの設定をこらなくても開発が進められました。

また今回でAIエージェントと開発をうまく進めるコツはこのように観点があると感じました。

  • 出力されたコードの妥当性はテストで担保
    • Pythonプログラムというお手本がある状態であったので進めやすかった
  • コードの責任はあくまで自分が持つ。それはコードの理解も含む。AIエージェントに聞きまくって理解していく過程は必要

今回の開発を通し、実現したいことが明確にあればあるほど、それを実際に実現できるツールがすでにそろっていることを痛感しました。
iOS開発の分野でも、プログラマーにとって生成AIやAIエージェントはなくてはならないツールになっていくのを感じました。