Dexcom G7をMacで常時表示!SwiftUIとSwift Chartsで血糖値アプリ自作録
作成日:
更新日:

Dexcom G7をMacで常時表示!SwiftUIとSwift Chartsで血糖値アプリ自作録


🔧 論旨

持続グルコース測定器(CGM:『Dexcom G7』や『FreeStyle リブレ 2』)を着けてデスクワークをしていると、昼食後に数値がじわじわ上がってきたとき「ちょっと歩きに行ったほうがいいのは分かっているけれど、今キリがいいところだから座ったままでいいか…」と腰が重くなりがちです。純正スマホアプリは「今の数値」は教えてくれても、「座りっぱなしと 20 分の散歩で、平常値に戻るまでの時間がどれだけ変わるか」 までは見せてくれません。

結論からお伝えすると、Mac の大画面上で現在のグルコース値を 5 分おきに自動取得しつつ、「安静(1 MET)・ウォーキング(4 METs)・ハイキング(6 METs)」の 3 パターンで目標値までの予測到達時間を折れ線グラフ化する SwiftUI 製アプリ『GlucoseTracker』 を AI と自作したことで、食後にスッと立ち上がって歩く習慣が劇的に定着しました。本記事では、個人情報を完全に除外した汎用コードをもとに、日本国内版 Dexcom Share 連携の技術的な急所と Swift Charts によるグラフ描画ノウハウを公開します。

🔧 合わせて読みたい深まる関連情報
🔧 対象となる方
  • 『Dexcom G7』や『FreeStyle リブレ 2』を活用しており、食後の軽いウォーキングがどれだけ血糖値コントロールに効くかを Mac 上で直感的に可視化したい 方

  • 日本国内アカウントの Dexcom Share サービスへ接続する際、海外製ツールの初期設定のままではログインに失敗する原因と対処法 を知りたい方

  • macOS 標準の Swift Charts フレームワークや Grid レイアウトを使った実用ダッシュボード開発 に挑戦してみたい方

なぜMacデスクトップに血糖消費シミュレーターが必要か

食後に上がった血糖値を穏やかに下げる最強の方法は、インスリンだけに頼らず下半身の大きな筋肉を動かして糖を取り込ませる「食後 15〜30 分以内の軽いウォーキング」です。

しかし人間の脳は正直なもので、ただスマホ画面で「175 mg/dL ↗」という数字を見ただけではなかなか重い腰が上がりません。ところが同じ数値が Dexcom G7 から自動反映(または手入力)された瞬間に、目の前の Mac 画面で 「座ったままなら下がるまであと約 70 分、今すぐ 4 METs で歩けば約 18 分!」 とグラフの傾きで見せられると、「よし、ちょっと外を一周歩いてこよう!」と自然に行動スイッチが入ります。

実際に AI と一緒に構築して手元の Mac で稼働させている『GlucoseTracker v2』の画面がこちらです(プロフィール欄は体重 70.0kg・標準 BMI 22.0 の汎用サンプル値を入力しています)。

SwiftUIとSwift Chartsで自作したMacアプリ『GlucoseTracker v2』の実機画面。左サイドバーに体重70.0kg・BMI 22.0・現在のグルコース値138 mg/dL・日本版Dexcom連携の『接続済み』ステータスと次回更新カウントダウン表示、右メイン画面に安静1 MET・ウォーキング4 METs・ハイキング6 METsの消費ペースと目標110 mg/dLまでの所要時間カード(27分・6分・4分)が並んでいる

▲ 稼働中の自作 Mac アプリ『GlucoseTracker v2』:ログイン後は左ペインが「✅ 接続済み」と 5 分周期のカウントダウン表示にスッキリ切り替わり、右ペインで運動強度別の消費ペース(秒・分・時)と目標 110 mg/dL までの所要時間を即座に並列比較できます

このアプリの最大の快適さは、一度 Dexcom 連携にログインしてしまえば、あとは完全に全自動で連動し続ける点 です。ログインに成功した瞬間にまず今『Dexcom G7』が表示している最新の血糖値(上図では 138 mg/dL)が左側の「現在の状態」欄へ即座に表示され、以降は放置しておくだけで 5 分(300 秒)ごとに最新値へ自動更新 されていきます。そして血糖値が更新されるたびに、右画面の 「目標値までに必要な運動時間(安静・ウォーキング・ハイキング)」と「予測推移グラフ」もリアルタイムで自動再計算 されます。

スマホ純正CGMアプリと『自作Macシミュレーター』の役割の違い
行動変容の差
過去〜現在の記録
スマホの純正CGMアプリ単体
  • 仕事中に毎回iPhoneを手に取るとSNSや通知に気を取られやすい
  • 「今いくつか」は分かるが、運動強度ごとの回復ペース比較はない
これからの行動支援
自作Macアプリ(GlucoseTracker)
  • 5分ごとの自動更新(またはリブレ2等の手入力)で作業を止めず一瞥確認
  • 1 MET・4 METs・6 METsの所要時間差をSwift Chartsで即座に並列描画
Dexcom (デクスコム) G7 持続グルコース測定システム センサー(Garmin公式アプリ&Apple Watch直接接続対応)

Dexcom (デクスコム) G7 持続グルコース測定システム センサー(Garmin公式アプリ&Apple Watch直接接続対応)

アボット (Abbott) FreeStyle リブレ 2 センサー(1分間隔リアルタイム計測・14日間装着モデル)

アボット (Abbott) FreeStyle リブレ 2 センサー(1分間隔リアルタイム計測・14日間装着モデル)


日本版Dexcom Share連携と自動再認証の実装

日本専用エンドポイントと2段階セッション取得

『Dexcom G7』には、スマホの純正アプリで「Share(フォロー共有)」機能を有効にしておくと、クラウド経由で最新の測定値を参照できる仕組みがあります。ただし、海外のオープンソース記事(米国向け share2.dexcom.com や欧州等向け shareous1.dexcom.com)のコードをそのまま持ってきても、日本で登録したアカウントでは認証エラー(ログイン失敗) になります。

日本国内アカウントで接続するには、日本リージョン専用のエンドポイント(https://share.dexcom.jp/ShareWebServices/Services) を選択できるようにした上で、以下の 2 段階セッション認証フローを順番に実行する必要があります。

Dexcom Share Web Services の2段階セッション確立と自動リトライの流れ
Step 1
アカウントIDの取得

AuthenticatePublisherAccount へアカウント名・パスワード・地域別applicationIdをPOSTします。

Step 2
セッションIDの発行

取得したaccountIdを使って LoginPublisherAccountById を呼び出し、有効なsessionIdを受け取ります。

Step 3
初回即時反映+5分ごとに自動連動更新

ログイン直後に ReadPublisherLatestGlucoseValues(maxCount=1)で現在のG7血糖値を即時表示し、以後300秒ごとに自動取得して右画面の必要な運動時間・グラフを連動更新します(期限切れ時は1回だけ自動再認証)。

センサーの更新周期に合わせ、ポーリング間隔は必ず5分(300秒)以上空けてサーバー負荷を抑えます

実装時に特に気をつけたエッジケースが 2 つ あります。1 つ目は、受け取った sessionId の前後にダブルクォーテーションや改行空白が混じっていると URL 生成に失敗するため、必ず replacingOccurrences(of: "\"", with: "") と trimmingCharacters(in: .whitespacesAndNewlines) でクレンジングすること。2 つ目は、長時間起動していて SessionIdNotFound が返った際、無限再帰ループを起こさないよう isRetry: Bool フラグで自動再ログインを 1 回だけに制限する ことです。

5分周期ポーリングとKeychain保護の鉄則

また、自分専用アプリであってもセキュリティの基本として 「パスワードを @AppStorage(平文の UserDefaults)やローカルのテキストログへ書き出さないこと」 が鉄則です。メモリ上の @State で保持するか、永続化したい場合は macOS 標準の Security フレームワーク(Keychain Services:キーチェーン) に暗号化保存しましょう。あわせて認証成功後は入力欄を隠して 緑色の「✅ 接続済み」ステータスと「次回更新まで: 4分52秒」といった 300 秒周期の残り時間カウントダウン(および左下のリビジョン番号)に切り替わる UI にしておくと、画面上に認証情報を露出させず、バックグラウンド同期が生きているかも一目で分かります。

以下は個人情報・ローカル固有パスを一切含まない、クリーンな通信サービスクラスのコア実装例です。

// DexcomService.swift(汎用クリーン版:日本リージョン対応&1回限りの安全な再認証リトライ)
import Foundation

@MainActor
final class DexcomService: ObservableObject {
    @Published var isLoggedIn = false
    @Published var errorMessage: String?
    @Published var latestGlucose: Double?

    private var sessionId: String?

    enum Region: String, CaseIterable {
        case japan = "Japan (share.dexcom.jp)"
        case us = "US (share2.dexcom.com)"
        case ous = "Outside US (shareous1.dexcom.com)"

        var baseUrl: String {
            switch self {
            case .japan: return "https://share.dexcom.jp/ShareWebServices/Services"
            case .us:    return "https://share2.dexcom.com/ShareWebServices/Services"
            case .ous:   return "https://shareous1.dexcom.com/ShareWebServices/Services"
            }
        }

        var applicationId: String {
            switch self {
            case .japan:    return "d8665ade-9673-4e27-9ff6-92db4ce13d13"
            case .us, .ous: return "d89443d2-327c-4a6f-89e5-496bbb0317db"
            }
        }
    }

    func authenticate(username: String, password: String, region: Region) async -> Bool {
        guard let accountUrl = URL(string: "\(region.baseUrl)/General/AuthenticatePublisherAccount") else { return false }
        var req = URLRequest(url: accountUrl)
        req.httpMethod = "POST"
        req.setValue("application/json", forHTTPHeaderField: "Content-Type")
        req.setValue("application/json", forHTTPHeaderField: "Accept")

        let body: [String: String] = [
            "accountName": username,
            "password": password,
            "applicationId": region.applicationId
        ]

        do {
            req.httpBody = try JSONEncoder().encode(body)
            let (accData, accResp) = try await URLSession.shared.data(for: req)
            guard (accResp as? HTTPURLResponse)?.statusCode == 200 else {
                errorMessage = "認証サーバーへの接続に失敗しました"
                isLoggedIn = false
                return false
            }

            let accountId = (String(data: accData, encoding: .utf8) ?? "")
                .replacingOccurrences(of: "\"", with: "")
                .trimmingCharacters(in: .whitespacesAndNewlines)
            guard !accountId.isEmpty, accountId != "00000000-0000-0000-0000-000000000000" else {
                errorMessage = "アカウント名またはパスワードを確認してください"
                isLoggedIn = false
                return false
            }

            guard let sessionUrl = URL(string: "\(region.baseUrl)/General/LoginPublisherAccountById") else { return false }
            var sReq = URLRequest(url: sessionUrl)
            sReq.httpMethod = "POST"
            sReq.setValue("application/json", forHTTPHeaderField: "Content-Type")
            sReq.setValue("application/json", forHTTPHeaderField: "Accept")
            sReq.httpBody = try JSONEncoder().encode([
                "accountId": accountId,
                "password": password,
                "applicationId": region.applicationId
            ])

            let (sesData, sesResp) = try await URLSession.shared.data(for: sReq)
            guard (sesResp as? HTTPURLResponse)?.statusCode == 200 else { return false }

            let sid = (String(data: sesData, encoding: .utf8) ?? "")
                .replacingOccurrences(of: "\"", with: "")
                .trimmingCharacters(in: .whitespacesAndNewlines)
            guard !sid.isEmpty, sid != "00000000-0000-0000-0000-000000000000" else { return false }

            self.sessionId = sid
            self.isLoggedIn = true
            self.errorMessage = nil
            // ログイン成功直後に現在のG7血糖値を初回取得して画面へ即時反映(isRetry: trueで無限再帰を防止)
            await fetchLatestGlucose(username: username, password: password, region: region, isRetry: true)
            return true
        } catch {
            self.errorMessage = "通信エラーが発生しました"
            self.isLoggedIn = false
            return false
        }
    }

    private struct GlucoseEntry: Decodable {
        let Value: Double
    }

    // 初回ログイン直後および5分(300秒)タイマーごとに呼び出して最新グルコース値を自動更新
    func fetchLatestGlucose(username: String, password: String, region: Region, isRetry: Bool = false) async {
        guard let sid = sessionId,
              var comps = URLComponents(string: "\(region.baseUrl)/Publisher/ReadPublisherLatestGlucoseValues") else { return }
        comps.queryItems = [
            URLQueryItem(name: "sessionId", value: sid),
            URLQueryItem(name: "minutes", value: "1440"),
            URLQueryItem(name: "maxCount", value: "1")
        ]
        guard let fetchUrl = comps.url else { return }

        var req = URLRequest(url: fetchUrl)
        req.httpMethod = "POST"
        req.setValue("application/json", forHTTPHeaderField: "Accept")
        req.setValue("0", forHTTPHeaderField: "Content-Length")

        do {
            let (data, resp) = try await URLSession.shared.data(for: req)
            let code = (resp as? HTTPURLResponse)?.statusCode ?? 0
            if code != 200 {
                // セッション期限切れ(SessionIdNotFound等)の場合は1回だけ自動再ログインして再取得
                if !isRetry, await authenticate(username: username, password: password, region: region) {
                    return
                }
                return
            }
            let entries = try JSONDecoder().decode([GlucoseEntry].self, from: data)
            if let latest = entries.first, latest.Value.isFinite, latest.Value > 0 {
                self.latestGlucose = latest.Value
            }
        } catch {
            errorMessage = "最新値の取得に失敗しました"
        }
    }
}

このように DexcomService 側の @Published var latestGlucose が 5 分おきに書き換わるため、画面(ContentView)側では .onChange(of: dexcom.latestGlucose) で「現在の状態」テキストを更新するだけで、SwiftUI の自動再描画によって 右ペインの消費ペース・目標値までの所要時間(分)・Swift Charts の予測折れ線グラフがすべて一斉に最新値へ連動して切り替わる 仕組みです。

Code500と再接続ループを潰した3つの急所

実際の開発中には、Clarity(Web 分析サービス)と同じ正しいメールアドレス・パスワードを入れているのに Code: 500 やセッション切れ無限ループ が連発する壁にぶつかりました。過去のデバッグ履歴から判明した「日本国内で Dexcom Share 連携を安定稼働させる 3 つの実戦ポイント」を整理しておきます。

🔧 🛠️ 日本版Dexcom Shareでハマりやすい3つの罠と解決策
  1. スマホ純正アプリ側で「最低 1 人のフォロワー登録」が必須 : たとえ自分ひとりで Mac から見るだけでも、iPhone の『Dexcom G7』純正アプリ内の「接続 > Share」を有効にし、最低 1 人(自分自身のサブメールアドレス等でも可)をフォロワーとして招待・承認 しておかないと、クラウド側に共有ストリーム自体が生成されず Code: 500 で弾かれます(※Mac アプリ側に入力する認証情報はフォロワー側ではなく センサーを着けている親機本人のアカウント です)。

  2. 日本サーバー専用の applicationId 分離(海外共通 ID の拒絶) : 接続先 URL を share.dexcom.jp に切り替えた後、「applicationId は世界共通の最新 ID(d89443d2-...)に統一しよう」と書き換えた途端、日本のサーバーだけが ApplicationNotAuthenticated エラーで通信を拒否 しました。上記のコード通り、.japan 選択時は必ず日本リージョン専用の固定 UUID(d8665ade-9673-4e27-9ff6-92db4ce13d13)を使い分ける必要があります。

  3. 「一瞬で消えるエラー」の可視化と未接続時の自動復帰タイマー : 開発初期に第 1 段階の accountId と第 2 段階の sessionId を取り違えた際、成功 → セッション切れ再接続中... という表示が 1 秒間に何十回も入れ替わり、画面上では何が起きているか人間には判読不能になりました。そこで ステータス変化と HTTP 応答コードだけをタイムスタンプ付きの一時テキストへ書き出させて AI に読ませた ところ、非同期競合と ID 取り違えを一発で特定できました(※パスワードやトークンは絶対にログ出力しないこと)。あわせて Wi-Fi 瞬断等で一度「未接続」に落ちても 5 分タイマー自体は止めず、次回カウントダウンが 0 秒になった時点で自動的に再ログインを試みる設計 にしたことで、数日間起動しっぱなしでも途切れない安定性を獲得できました。

質問するおじさん

なるほどな〜!アメリカ用のアドレスやアプリ ID のままだと日本のアカウントは弾かれてしまうし、最初にスマホ側で自分をフォロワー登録しておくひと手間がいるんじゃな。しかもリブレ 2 を使っとる人でも、左側の入力欄に数字を手打ちすればそのままシミュレーターとして使えるんか!

解説ナビゲーター

そうなんです!左サイドバーの「現在の状態」テキスト欄は自動取得にも手入力にも連動しているので、CGM の機種を問わず、気になった瞬間にサッと数字を入れて運動時間の目安を比べられますよ!


Swift Chartsで運動METs別の所要時間を可視化

安静・歩行・登山の比較カードと推移グラフ実装

右側のメイン画面では、厚生労働省の身体活動ガイドでも用いられている標準式(エネルギー消費量 [kcal/時] = 1.05 × METs × 体重 [kg])をベースに、「安静(1.0 MET)」「ウォーキング(4.0 METs)」「ハイキング・階段昇降(6.0 METs)」 の 3 段階で消費ペースを計算し、macOS 13 以降標準の import Charts で一気に折れ線グラフと 10 mg/dL 刻みの所要時間テーブルを描画しています。

なお、画面上部に明記している 1 kcal 消費 = 約 0.833 mg/dL 低下(約 1.2 kcal 消費あたり 1 mg/dL 低下)という係数は、中強度運動における糖質燃焼比率を約 50%(0.5 kcal = グルコース 125 mg 相当)と仮定し、成人の細胞外液グルコース分布容積の目安(約 15 L = 150 dL)で割った 125 ÷ 150 ≒ 0.833 mg/dL の線形換算モデル(推論)です。先ほどの実機スクリーンショット(体重 70.0 kg・現在値 138 mg/dL から目標 110 mg/dL までのシミュレーション例)で見比べると、行動変容へのインパクトが一目瞭然になります。

🔧 📊 実機ダッシュボード(GlucoseTracker v2)の3段比較ビュー
  1. 血糖消費ペース(秒・分・時の 3 単位カード) : 安静(1 MET)では 0.017 mg/dL / 秒(1.02 mg/dL / 分・61.2 mg/dL / 時) ずつしか進まない理論消費ペースが、ウォーキング(4 METs)なら 4 倍の 4.08 mg/dL / 分(244.9 mg/dL / 時)、ハイキングや階段昇降(6 METs)なら 6 倍の 6.12 mg/dL / 分(367.4 mg/dL / 時) へ跳ね上がる様子を秒単位から実感できます。

  2. 目標(110 mg/dL)までの所要時間カード : たとえば食後に少し上がった 138 mg/dL から目標 110 mg/dL(-28 mg/dL)へ戻す理論目安で比べた場合、座りっぱなしの安静(1 MET)では 「27 分(合計 27.4 分)」 かかるところが、ウォーキング(4 METs)に出れば 「6 分(合計 6.9 分)」、ハイキング・階段昇降(6 METs)ならわずか 「4 分(合計 4.6 分)」 と、数分体を動かすだけで 20 分以上も早く平常圏へ戻れる計算が青字で大きく強調されます。

  3. 時間経過に伴う予測血糖値の推移グラフ(Swift Charts) : カードの下部には時間経過に対する 3 本の予測ラインを並列プロットし、視覚的な傾きの差で「今から 10 分だけ歩いてこよう!」と背中を押してくれます。

外部のグラフライブラリを 1 つもインストールせずに、Chart ブロックの中に LineMark を数行書くだけで、凡例・目盛り・色分けシンボル付きの美しいネイティブグラフが完成するのは SwiftUI 最大の魅力です。

// ContentView.swift(抜粋:全角半角正規化+無限大クラッシュ防止ガード付き Swift Charts 描画)
import SwiftUI
import Charts

// 全角数字・ピリオドや誤入力記号を安全に吸収し、有限かつ0以上のDoubleへ変換
func parseSafePositiveDouble(_ raw: String, maxLimit: Double) -> Double {
    let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines)
    guard !trimmed.isEmpty else { return 0 }
    let halfWidth = trimmed.applyingTransform(.fullwidthToHalfwidth, reverse: false) ?? trimmed
    let cleaned = halfWidth.replacingOccurrences(of: ",", with: "")
    guard let val = Double(cleaned), val.isFinite, val >= 0 else { return 0 }
    return min(val, maxLimit)
}

// 簡易目安モデル:1時間あたりの消費エネルギー(1.05 * METs * 体重kg)に理論換算係数を乗じた参考値
func estimatedDropPerMinute(mets: Double, weightKg: Double) -> Double {
    guard mets > 0, weightKg > 0 else { return 0 }
    let kcalPerHour = 1.05 * mets * weightKg
    return (kcalPerHour * 0.833) / 60.0
}

func minutesToTarget(current: Double, target: Double, mets: Double, weightKg: Double) -> Double {
    let diff = current - target
    guard diff > 0 else { return 0 }
    let rate = estimatedDropPerMinute(mets: mets, weightKg: weightKg)
    guard rate > 0 else { return .infinity }
    return diff / rate
}

// Swift Charts 描画パート(weight > 0 かつ mins.isFinite で保護し、無限大渡しクラッシュを防止)
Chart {
    if weight > 0 && currentGlucose > targetGlucose {
        ForEach(metsOptions, id: \.name) { option in
            ForEach(Array(stride(from: currentGlucose, through: targetGlucose, by: -10.0)), id: \.self) { gVal in
                let mins = minutesToTarget(current: currentGlucose, target: gVal, mets: option.value, weightKg: weight)
                if mins.isFinite {
                    LineMark(
                        x: .value("所要時間 (分)", mins),
                        y: .value("目安グルコース値 (mg/dL)", gVal)
                    )
                    .foregroundStyle(by: .value("運動強度", option.name))
                    .symbol(by: .value("運動強度", option.name))
                }
            }
        }
    }
}
.frame(height: 320)

ここで実運用の中で実際に踏んだ SwiftUI 特有の重大な落とし穴 も共有しておきます。@AppStorage(UserDefaults 自動保存)に紐づけた体重欄をバックスペースで空欄にしたり、誤って記号や全角数字を入力したりした瞬間、weight == 0 となって計算結果が Double.infinity(無限大) になります。これをカード表示の Int(mins) や Chart にそのまま渡すと Swift のランタイムエラー(EXC_BAD_INSTRUCTION)で即クラッシュし、さらに UserDefaults に不正文字が残ることで 次回から起動 0.2 秒で毎回落ち続ける起動不能ループ に陥ります。上記のように applyingTransform による全角→半角正規化と mins.isFinite ガード を必ずセットで入れておきましょう。

簡易理論モデルの限界と医療上の安全ガイドライン

ここで技術・生理学の両面から、必ず押さえておくべき 「簡易直線モデルの限界と安全境界」 を明記しておきます。

🔧 ⚠️ 必ず知っておきたい生体反応の複雑さと医療免責事項
  1. 実際の血糖曲線は「消化吸収の波」と「ホルモン応答」で変動する(推論モデルの限界) : 上記の計算式は「消費エネルギーに比例して一定のペースで血中グルコースが消費されたら何分かかるか」を直感的に比較するための 簡易理論モデル(推論目安) です。実際の生体内では、胃腸からの糖吸収がまだ続いている食後 30〜60 分は運動中も緩やかに横ばいになりやすく、逆に全力疾走のような無酸素に近い高強度運動を行うとアドレナリン等の働きで肝臓から糖が放出され、一時的に血糖値が上がることもあります。

  2. 医療機器プログラム(SaMD)や診断・投薬指示の代替ではない : 本記事で紹介している自作ツールは、あくまで健康な方や境界型の方が「座りっぱなしをやめて食後に軽く歩こう」と行動変容するための個人用学習・可視化メモであり、疾病の診断・治療・予防を目的とした医療機器プログラムではありません。

  3. 薬物療法・インスリン使用中の方は低血糖に厳重注意 : インスリン注射や血糖降下薬を使用されている方が自己判断で運動量を増やすと、重篤な低血糖(および数時間後の遅発性低血糖)を招く危険があります。治療中の方は必ず主治医の指導範囲内で管理を行ってください。

主食を『きらりもち』のもち麦ごはんに切り替えて「入口の急上昇」を抑えつつ、少し食べすぎた日は Mac のグラフを見て「食後に 15〜20 分だけ 4 METs で近所を歩く」という 食事×軽い有酸素運動の二段構え にすると、驚くほど穏やかな 1 日を過ごせるようになります。

ノルウェージャンフォレストキャット

「頑張って運動しなきゃ…」と思うとしんどいけど、グラフで「20 分歩けばこんなに早くスッキリするでち!」と目で見えると、お散歩に行くのが楽しみになるでちね!

解説ナビゲーター

まさにそこが自作ツールの醍醐味なんです!自分を責めるための監視ツールではなく、「今ちょっと歩けばこれだけ得をする!」と背中を優しく押してくれる自分専用の計器盤 を作れるのが SwiftUI 個人開発の面白さですね。

国産 もち麦『きらりもち』(農研機構育成・水溶性食物繊維β-グルカン高含有・白米なし10割炊きでも美味しい品種)

国産 もち麦『きらりもち』(農研機構育成・水溶性食物繊維β-グルカン高含有・白米なし10割炊きでも美味しい品種)


まとめ:数字を行動に変える自分専用ダッシュボード

既製品のアプリに欲しい画面がなければ、SwiftPM と Swift Charts を使って自分のデスク環境ぴったりのダッシュボードを AI と一緒に組み立てることができます。「5 分おきの自動確認でスマホ依存を断ち切り、運動強度別のグラフで食後の 15 分ウォーキングを習慣化する」——この小さな仕組みづくりが、毎日の集中力と体調管理を大きく底上げしてくれます。

ご自身の Mac でも運動強度別の消費時間比較ダッシュボード(手入力モード標準搭載)を試してみたい方は、以下のコピペ用プロンプトを最上位 AI モデルへ渡して構築してみてください。

🔧 🤖 『Swift Charts 運動METsシミュレーター』をAIに作らせるコピペ用プロンプト

※非同期通信の排他制御や macOS Keychain 保護、Swift Charts の型推論エラーを防ぐため、必ず 『Gemini 4 Argon』(または同等以上の最上位推論モデル)を選択して実行してください。

以下の記事の設計を参考に、Swift Package Manager(Package.swift / macOS 13以降対象)で動くSwiftUI+Swift Charts製の個人用健康行動支援Macアプリ『GlucoseTracker』を作成してください。
参照URL: https://lab.chihouzakki.com/blog/review/swiftui-cgm-glucose-tracker-mac-app-diy/

【実装ルールと安全要件(厳守)】
1. 左サイドバーにプロフィール入力(体重kg・身長cm・BMI自動算出)と現在のグルコース値(mg/dL)の手入力フォームを設け、右メイン画面には安静(1.0 MET)・ウォーキング(4.0 METs)・ハイキング(6.0 METs)の3段階で「血糖消費ペース(mg/dLの秒・分・時3単位表示カード)」「目標値(例:110 mg/dL)までの所要時間カード(時間・分および合計分)」「時間経過に伴う予測推移のSwift Charts折れ線グラフ」を並列表示してください。
2. 画面上部に「※ 1kcal消費 = 約0.833mg/dL低下として計算した行動支援用の理論値であり、医療上の診断・治療判断には使用しないでください」という注記ラベルを明示し、左サイドバー下部にリビジョン番号を表示してください。
3. もし外部API(日本版Dexcom Share等)連携機能を追加する場合は、地域選択(Japan: share.dexcom.jp 等)と地域別applicationId(日本は d8665ade-9673-4e27-9ff6-92db4ce13d13)を分離し、ログイン成功直後にDexcom G7の現在の血糖値を即時取得して「現在の状態」へ反映するとともに、入力欄を隠して「✅ 接続済み」ステータスと「次回更新まで: X分XX秒」のカウントダウン表示に切り替えてください。以後は300秒(5分)ごとに最新値を自動更新し、通信断で一時的に未接続になっても300秒タイマーを止めず次回0秒で自動再ログインを試み、値が変化するたびに右画面の必要な運動時間カードと予測推移グラフも自動で再計算・再描画されるよう連動させてください(パスワードは絶対に @AppStorage 平文やログへ書き出さず、macOS標準の Keychain Services に保存すること)。
4. Appのinit()に NSApplication.shared.setActivationPolicy(.regular) を設定し、SwiftPM単体起動でもキーボード入力が確実に動作するようにしてください。
5. 体重や血糖値の入力欄に空欄・記号・全角数字が入っても @AppStorage 経由で起動クラッシュしないよう、全角半角正規化(applyingTransform)と Double.isFinite ガード(Int変換およびChart描画前の無限大・NaN除外)、異常桁数の上限クランプを必ず実装してください。

テクノロジーを味方につけて、デスクワークの合間に気持ちよく体を動かす健康リズムを作っていきましょう!

窓の外のお散歩コースをじっと見つめるノルウェージャンフォレストキャット

▲ 食後の15分ウォーキングで頭も体もシャキッとリフレッシュ!