
「習慣トラッカー」は、毎日の習慣を続けるためのScriptableウィジェットです。
読書、筋トレ、ストレッチ、勉強など、続けたい習慣を自由に設定できます。 ウィジェットをタップして「達成した!」を選ぶだけで、その日の記録が保存されます。
連続達成日数や今月の達成日数をひと目で確認でき、中サイズでは直近7日間、大サイズでは今月の達成状況をカレンダーで確認できます。
習慣ごとに記録は別々に保存されるため、「読書」「筋トレ」「ストレッチ」など複数の習慣をそれぞれ管理することもできます。
下の「パラメーター作成」で習慣名・絵文字・テーマカラーを設定すれば、ウィジェット用のパラメーターを簡単に作成できます。
ウィジェットの表示内容
ウィジェットをタップして今日の達成・取り消しを選びます。
サイズごとの表示
- 小:連続達成日数と今日の実施状況を表示します。
- 中:今月の達成数や直近7日間の状況も表示します。
- 大:月間カレンダーで記録を確認できます。
設定手順
上から順に進めてください。コードの保存と設定のコピーに必要な操作欄は、それぞれの手順の中にあります。
- Scriptableを用意する
App StoreでScriptableを開き、iPhoneにインストールします。すでに入っている場合は次へ進んでください。
- コードをコピーして保存する
下のコード欄の「コピー」を押します。Scriptableへ移動し、「+」から新しいスクリプトを作ってコード全文を貼り付け、習慣トラッカーなどの名前で保存してください。
保存したらこのページに戻り、次の手順へ進みます。
// Variables used by Scriptable. // These must be at the very top of the file. Do not edit. // icon-color: green; icon-glyph: check-circle; // Version: 1.0.0 // ===================================================== // 習慣トラッカー - Scriptable Widget // // Parameter: // 習慣名,絵文字,テーマカラー // // 例: // 読書,📖,#37D67A // 筋トレ,💪,#FF8A65 // ストレッチ,🧘,#64B5F6 // // 小:今日の状態・連続達成 // 中:連続達成・今月・直近7日 // 大:連続達成・今月・直近7日・月間カレンダー // // ウィジェットをタップして今日の達成を記録 // 習慣ごとに記録を個別保存 // ===================================================== // ===================================================== // デフォルト設定 // ===================================================== const DEFAULT_NAME = "毎日の習慣"; const DEFAULT_ICON = "✓"; const DEFAULT_COLOR = "#37D67A"; // ===================================================== // パラメーター取得 // ===================================================== const rawWidgetParam = (args.widgetParameter || "").trim(); const widgetParams = rawWidgetParam ? rawWidgetParam.split(",").map(v => v.trim()) : []; const query = args.queryParameters || {}; // URLパラメーターを優先 const HABIT_NAME = query.name || widgetParams[0] || DEFAULT_NAME; const HABIT_ICON = query.icon || widgetParams[1] || DEFAULT_ICON; const THEME_COLOR = normalizeHex( query.color || widgetParams[2] || DEFAULT_COLOR ); // ===================================================== // 保存設定 // ===================================================== const fm = FileManager.local(); const DATA_DIR = fm.joinPath( fm.documentsDirectory(), "GEBOKU_HabitTracker" ); if (!fm.fileExists(DATA_DIR)) { fm.createDirectory(DATA_DIR, true); } // 習慣ごとに別ファイル const habitId = safeFileName( `${HABIT_NAME}_${HABIT_ICON}` ); const DATA_FILE = fm.joinPath( DATA_DIR, `${habitId}.json` ); // ===================================================== // データ読み込み // ===================================================== let data = loadData(); // ===================================================== // Scriptable URL // ===================================================== const scriptName = Script.name(); const baseURL = `scriptable:///run?scriptName=${encodeURIComponent(scriptName)}`; const actionURL = baseURL + `&action=check` + `&name=${encodeURIComponent(HABIT_NAME)}` + `&icon=${encodeURIComponent(HABIT_ICON)}` + `&color=${encodeURIComponent(THEME_COLOR)}`; // ===================================================== // Scriptableアプリ内 // ===================================================== if (!config.runsInWidget) { await showHabitAction(); Script.complete(); return; } // ===================================================== // ウィジェット表示 // ===================================================== const widget = await createWidget(); widget.url = actionURL; Script.setWidget(widget); Script.complete(); // ===================================================== // ウィジェット作成 // ===================================================== async function createWidget() { const widget = new ListWidget(); widget.backgroundColor = new Color("#111614"); widget.setPadding( 16, 16, 16, 16 ); const today = dateKey(new Date()); const completedToday = data.completed.includes(today); const streak = calculateStreak(); const monthCount = calculateMonthCount(); // =================================================== // 小サイズ // =================================================== if (config.widgetFamily === "small") { addHeader( widget, completedToday ); widget.addSpacer(10); const label = widget.addText("連続"); label.font = Font.mediumSystemFont(11); label.textColor = new Color("#8A9892"); widget.addSpacer(1); const value = widget.addText(`${streak}日`); value.font = Font.boldSystemFont(34); value.textColor = new Color(THEME_COLOR); value.minimumScaleFactor = 0.7; widget.addSpacer(); const status = widget.addText( completedToday ? "✓ 今日も達成!" : "○ 今日はまだ" ); status.font = Font.semiboldSystemFont(11); status.textColor = completedToday ? new Color(THEME_COLOR) : new Color("#A4AEA9"); return widget; } // =================================================== // 中サイズ // =================================================== if (config.widgetFamily === "medium") { addHeader( widget, completedToday ); widget.addSpacer(9); // ----------------------------------------------- // 統計 // ----------------------------------------------- const body = widget.addStack(); body.layoutHorizontally(); const left = body.addStack(); left.layoutVertically(); const streakLabel = left.addText("連続達成"); streakLabel.font = Font.mediumSystemFont(10); streakLabel.textColor = new Color("#8A9892"); left.addSpacer(1); const streakText = left.addText(`${streak}日`); streakText.font = Font.boldSystemFont(29); streakText.textColor = new Color(THEME_COLOR); body.addSpacer(); const right = body.addStack(); right.layoutVertically(); const monthLabel = right.addText("今月"); monthLabel.font = Font.mediumSystemFont(10); monthLabel.textColor = new Color("#8A9892"); right.addSpacer(1); const monthText = right.addText(`${monthCount}日`); monthText.font = Font.boldSystemFont(22); monthText.textColor = Color.white(); // ----------------------------------------------- // 直近7日 // ----------------------------------------------- widget.addSpacer(8); addWeekRow( widget, false ); widget.addSpacer(); const status = widget.addText( completedToday ? "✓ 今日の習慣は達成済み" : "タップして今日の達成を記録" ); status.font = Font.semiboldSystemFont(10); status.textColor = completedToday ? new Color(THEME_COLOR) : new Color("#A4AEA9"); return widget; } // =================================================== // 大サイズ // =================================================== addHeader( widget, completedToday ); widget.addSpacer(10); // --------------------------------------------------- // 統計 // --------------------------------------------------- const stats = widget.addStack(); stats.layoutHorizontally(); const streakBox = stats.addStack(); streakBox.layoutVertically(); const streakLabel = streakBox.addText("連続達成"); streakLabel.font = Font.mediumSystemFont(10); streakLabel.textColor = new Color("#8A9892"); streakBox.addSpacer(1); const streakText = streakBox.addText(`${streak}日`); streakText.font = Font.boldSystemFont(31); streakText.textColor = new Color(THEME_COLOR); stats.addSpacer(); const monthBox = stats.addStack(); monthBox.layoutVertically(); const monthLabel = monthBox.addText("今月の達成"); monthLabel.font = Font.mediumSystemFont(10); monthLabel.textColor = new Color("#8A9892"); monthBox.addSpacer(1); const monthText = monthBox.addText(`${monthCount}日`); monthText.font = Font.boldSystemFont(25); monthText.textColor = Color.white(); // --------------------------------------------------- // 直近7日 // --------------------------------------------------- widget.addSpacer(11); addWeekRow( widget, true ); // --------------------------------------------------- // 月間カレンダー // --------------------------------------------------- widget.addSpacer(12); addMonthCalendar(widget); widget.addSpacer(); // --------------------------------------------------- // 状態 // --------------------------------------------------- const status = widget.addText( completedToday ? "✓ 今日の習慣は達成済み" : "タップして今日の達成を記録" ); status.font = Font.semiboldSystemFont(10); status.textColor = completedToday ? new Color(THEME_COLOR) : new Color("#A4AEA9"); return widget; } // ===================================================== // ヘッダー // ===================================================== function addHeader( widget, completedToday ) { const header = widget.addStack(); header.layoutHorizontally(); header.centerAlignContent(); const icon = header.addText(HABIT_ICON); icon.font = Font.systemFont(16); header.addSpacer(6); const title = header.addText(HABIT_NAME); title.font = Font.boldSystemFont(14); title.textColor = Color.white(); title.lineLimit = 1; title.minimumScaleFactor = 0.7; header.addSpacer(); if (completedToday) { const check = header.addText("✓"); check.font = Font.boldSystemFont(15); check.textColor = new Color(THEME_COLOR); } } // ===================================================== // 直近7日 // ===================================================== function addWeekRow( widget, showTitle = true ) { if (showTitle) { const weekTitle = widget.addText("直近7日"); weekTitle.font = Font.semiboldSystemFont(10); weekTitle.textColor = new Color("#8A9892"); widget.addSpacer(5); } const weekStack = widget.addStack(); weekStack.layoutHorizontally(); const days = getLastDays(7); days.forEach( (date, index) => { const dayBox = weekStack.addStack(); dayBox.layoutVertically(); dayBox.centerAlignContent(); const weekday = dayBox.addText( getWeekday(date) ); weekday.font = Font.mediumSystemFont(9); weekday.textColor = new Color("#7E8984"); weekday.centerAlignText(); dayBox.addSpacer(3); const key = dateKey(date); const done = data.completed.includes(key); const mark = dayBox.addText( done ? "●" : "○" ); mark.font = Font.boldSystemFont(17); mark.textColor = done ? new Color(THEME_COLOR) : new Color("#45504B"); mark.centerAlignText(); if ( index < days.length - 1 ) { weekStack.addSpacer(); } } ); } // ===================================================== // 月間カレンダー // ===================================================== function addMonthCalendar(widget) { const now = new Date(); const year = now.getFullYear(); const month = now.getMonth(); const today = now.getDate(); // --------------------------------------------------- // 月タイトル // --------------------------------------------------- const monthTitle = widget.addText( `${month + 1}月` ); monthTitle.font = Font.semiboldSystemFont(10); monthTitle.textColor = new Color("#8A9892"); widget.addSpacer(5); // --------------------------------------------------- // 曜日 // --------------------------------------------------- const weekdays = ["日", "月", "火", "水", "木", "金", "土"]; const weekdayRow = widget.addStack(); weekdayRow.layoutHorizontally(); weekdays.forEach( (day, index) => { const box = weekdayRow.addStack(); box.size = new Size(30, 12); box.centerAlignContent(); const text = box.addText(day); text.font = Font.mediumSystemFont(8); text.textColor = new Color("#68736E"); text.centerAlignText(); if (index < 6) { weekdayRow.addSpacer(); } } ); widget.addSpacer(3); // --------------------------------------------------- // 日付計算 // --------------------------------------------------- const firstDay = new Date( year, month, 1 ).getDay(); const lastDate = new Date( year, month + 1, 0 ).getDate(); let day = 1; // 最大6週 for ( let week = 0; week < 6; week++ ) { if (day > lastDate) { break; } const row = widget.addStack(); row.layoutHorizontally(); for ( let weekday = 0; weekday < 7; weekday++ ) { const cell = row.addStack(); cell.size = new Size(30, 15); cell.centerAlignContent(); // 月初前の空欄 if ( week === 0 && weekday < firstDay ) { if (weekday < 6) { row.addSpacer(); } continue; } if (day <= lastDate) { const date = new Date( year, month, day ); const key = dateKey(date); const done = data.completed.includes(key); const isToday = day === today; let displayText; if (done) { displayText = "●"; } else if (isToday) { displayText = "◎"; } else { displayText = String(day); } const text = cell.addText( displayText ); // --------------------------------------------- // 達成日 // --------------------------------------------- if (done) { text.font = Font.boldSystemFont(11); text.textColor = new Color(THEME_COLOR); // --------------------------------------------- // 今日・未達成 // --------------------------------------------- } else if (isToday) { text.font = Font.boldSystemFont(11); text.textColor = new Color("#FFFFFF"); // --------------------------------------------- // 通常日 // --------------------------------------------- } else { text.font = Font.mediumSystemFont(8); text.textColor = new Color("#66716C"); } text.centerAlignText(); day++; } if (weekday < 6) { row.addSpacer(); } } if (day <= lastDate) { widget.addSpacer(2); } } } // ===================================================== // 記録操作 // ===================================================== async function showHabitAction() { const today = dateKey(new Date()); const completed = data.completed.includes(today); // --------------------------------------------------- // 達成済み // --------------------------------------------------- if (completed) { const alert = new Alert(); alert.title = `${HABIT_ICON} ${HABIT_NAME}`; alert.message = "今日は達成済みです ✓"; alert.addAction("OK"); alert.addDestructiveAction( "今日の記録を取り消す" ); const result = await alert.presentAlert(); if (result === 1) { data.completed = data.completed.filter( date => date !== today ); saveData(); await showMessage( "記録を取り消しました", `「${HABIT_NAME}」の今日の記録を削除しました。` ); } return; } // --------------------------------------------------- // 未達成 // --------------------------------------------------- const alert = new Alert(); alert.title = `${HABIT_ICON} ${HABIT_NAME}`; alert.message = `今日の「${HABIT_NAME}」を達成しましたか?`; alert.addAction( "達成した!" ); alert.addCancelAction( "まだ" ); const result = await alert.presentAlert(); if (result === 0) { data.completed.push(today); data.completed = [...new Set(data.completed)] .sort(); saveData(); const streak = calculateStreak(); await showMessage( `${HABIT_ICON} 今日も達成!`, streak > 1 ? `「${HABIT_NAME}」\n🔥 ${streak}日連続達成です!` : `「${HABIT_NAME}」の今日の達成を記録しました。` ); } } // ===================================================== // メッセージ // ===================================================== async function showMessage( title, message ) { const alert = new Alert(); alert.title = title; alert.message = message; alert.addAction("OK"); await alert.presentAlert(); } // ===================================================== // 連続達成日数 // ===================================================== function calculateStreak() { if ( !data.completed || data.completed.length === 0 ) { return 0; } let streak = 0; let date = new Date(); date.setHours( 12, 0, 0, 0 ); const todayKey = dateKey(date); // 今日が未達成なら昨日から数える if ( !data.completed.includes(todayKey) ) { date.setDate( date.getDate() - 1 ); } while (true) { const key = dateKey(date); if ( data.completed.includes(key) ) { streak++; date.setDate( date.getDate() - 1 ); } else { break; } } return streak; } // ===================================================== // 今月の達成数 // ===================================================== function calculateMonthCount() { const now = new Date(); const year = now.getFullYear(); const month = now.getMonth() + 1; const prefix = `${year}-${String(month).padStart(2, "0")}-`; return data.completed.filter( date => date.startsWith(prefix) ).length; } // ===================================================== // 直近の日付 // ===================================================== function getLastDays(count) { const dates = []; const today = new Date(); today.setHours( 12, 0, 0, 0 ); for ( let i = count - 1; i >= 0; i-- ) { const date = new Date(today); date.setDate( today.getDate() - i ); dates.push(date); } return dates; } // ===================================================== // 曜日 // ===================================================== function getWeekday(date) { const weekdays = [ "日", "月", "火", "水", "木", "金", "土" ]; return weekdays[ date.getDay() ]; } // ===================================================== // 日付キー // ===================================================== function dateKey(date) { const y = date.getFullYear(); const m = String( date.getMonth() + 1 ).padStart(2, "0"); const d = String( date.getDate() ).padStart(2, "0"); return `${y}-${m}-${d}`; } // ===================================================== // データ読み込み // ===================================================== function loadData() { if ( !fm.fileExists(DATA_FILE) ) { return { completed: [] }; } try { const json = fm.readString(DATA_FILE); const parsed = JSON.parse(json); if ( !Array.isArray( parsed.completed ) ) { parsed.completed = []; } return parsed; } catch (e) { return { completed: [] }; } } // ===================================================== // 保存 // ===================================================== function saveData() { fm.writeString( DATA_FILE, JSON.stringify( data, null, 2 ) ); } // ===================================================== // ファイル名用 // ===================================================== function safeFileName(value) { return String(value) .replace( /[\\/:*?"<>|]/g, "_" ) .replace( /\s+/g, "_" ); } // ===================================================== // HEXカラー補正 // ===================================================== function normalizeHex(value) { let hex = String(value || "") .trim(); if ( !hex.startsWith("#") ) { hex = "#" + hex; } if ( /^#[0-9A-Fa-f]{6}$/.test(hex) ) { return hex; } return DEFAULT_COLOR; } - ウィジェットの設定を作る
下の入力欄で自分用の設定を作り、生成されたパラメーターをコピーします。詳しい設定仕様は、設定手順の下でも確認できます。
習慣トラッカー パラメーター作成
習慣名・絵文字・テーマカラーを入力すると、ウィジェット用のパラメーターを自動で作成します。
習慣の設定
生成されたパラメーター
コピーできたら、次の手順でホーム画面のウィジェットへ貼り付けます。
- ホーム画面に追加して設定する
iPhoneのホーム画面を長押しし、ウィジェット追加画面からScriptableを選びます。この記事の対応サイズの説明に合わせてサイズを選んで追加してください。
追加したウィジェットを長押しして「ウィジェットを編集」を開き、「Script」に保存したスクリプトを選びます。
「Parameter」に直前の手順でコピーした値を貼り付けます。設定を変えるときも、同じ入力欄で作り直して貼り替えてください。
編集画面を閉じて表示を確認します。ホーム画面への反映には少し時間がかかる場合があります。
- 今日の習慣を記録
習慣を達成したらウィジェットをタップします。 Scriptableが開き、「今日の習慣を達成しましたか?」と表示されるので「達成した!」をタップしてください。
間違えて記録した場合は、もう一度ウィジェットをタップすると「今日の記録を取り消す」こともできます。
パラメータ・カスタマイズ
パラメーターの設定
下記の「習慣トラッカー パラメーター作成」を使って必要な項目を入力すると、画面下に「生成されたパラメーター」が自動で表示されます。
「パラメーターをコピー」を押してください。
そのままiPhoneのウィジェット設定の「parameter」枠に貼り付けたら設定完了です。
複数の習慣を管理する場合
同じスクリプトを使って、複数のウィジェットを設置できます。
たとえば「読書」「筋トレ」「ストレッチ」それぞれのパラメーターを作成して各ウィジェットに設定すれば、達成記録は習慣ごとに個別に保存されます。
動作確認・使用結果
| 確認した端末 | iPhone 17 ProMAX |
|---|---|
| iOS | iOS27 |
| Scriptable | 1.7.19 |
| 確認したサイズ | 小・中・大 |
| バージョン | 1.0.0 |
| 動作確認日 | 2026-09-25 |
表示・使用時のポイント
ウィジェットをタップして今日の達成・取り消しを選びます。同じ習慣名の記録を継続し、習慣ごとに別のウィジェットを置けます。
更新履歴
1.0.0
- 初回リリース
よくあるエラーと対処
- 確認すること
- 表示されない場合は、ウィジェット編集画面の「Script」に保存したスクリプトが選ばれているかも確認してください。コードを貼り直すときは全文を置き換えます。
- タップしても達成にならない
- 開いたメニューで達成の操作を選んでください。タップだけでは記録しません。
- 記録が別のものになった
- 習慣名を変えると別の記録として扱われます。以前と同じ名前か確認してください。
- 記録後にホーム画面が変わらない
- 保存後、ウィジェットの再表示や次の更新を待ってください。
注意事項・制限
- 行動を自動検知する機能はなく、自分で記録する方式です。
- 記録は端末側に保存されます。アプリや保存データを削除すると引き継げない場合があります。
- ホーム画面の更新タイミングはiOSが管理するため、操作やデータの変更がすぐに反映されない場合があります。
データ提供・参考資料
このウィジェットにはデータ提供や参考資料は特にありません。
SHARE

