
自分の人生は、今どのくらいまで進んでいるんだろう?
「人生カウント」は、生年月日と自分で決めた想定寿命から、人生の経過率を表示するScriptableウィジェットです。
「今年は何%?」を作っているときに、これを1年じゃなく人生そのものでやったらどうなるんだろう?と思ったのが、このウィジェットを作ったきっかけです。
人生を「46歳」のような年齢ではなく「%」で見てみると、同じ時間でもちょっと違った見え方になります。
ホーム画面を見るだけで、現在の人生が何%まで進んでいるのかを確認できます。
想定寿命は80歳・85歳・90歳・100歳など、好きな年齢に設定できます。
ウィジェットの表示内容
生年月日と自分で決めた想定寿命をもとに、経過した期間の割合を計算します。
サイズごとの表示
- 小:設定した人生の経過率を表示します。
- 中:生きた日数や残り年数も表示します。
- 大:残り日数を含め、設定した期間の進み具合を表示します。
設定手順
上から順に進めてください。コードの保存と設定のコピーに必要な操作欄は、それぞれの手順の中にあります。
- Scriptableを用意する
App StoreでScriptableを開き、iPhoneにインストールします。すでに入っている場合は次へ進んでください。
- コードをコピーして保存する
下のコード欄の「コピー」を押します。Scriptableへ移動し、「+」から新しいスクリプトを作ってコード全文を貼り付け、人生の経過は何%?などの名前で保存してください。
保存したらこのページに戻り、次の手順へ進みます。
// Version: 1.0.0 // ===================================================== // 人生カウント - Scriptable Widget // ===================================================== // // Parameter: // 生年月日,想定寿命 // // 例: // 1979-05-10,85 // // 小・中・大ウィジェット対応 // ===================================================== // ===================================================== // Parameter // ===================================================== const rawParam = typeof args !== "undefined" && args.widgetParameter ? args.widgetParameter.trim() : ""; let birthString = "1979-01-01"; let lifeExpectancy = 85; if (rawParam) { const params = rawParam.split(","); if (params[0]) { birthString = params[0].trim(); } if (params[1]) { const value = Number(params[1]); if (!isNaN(value) && value > 0) { lifeExpectancy = value; } } } // ===================================================== // 日付処理 // ===================================================== function parseDate(str) { const parts = str.split("-").map(Number); if (parts.length !== 3) { return null; } const [year, month, day] = parts; const date = new Date( year, month - 1, day, 12, 0, 0 ); if ( date.getFullYear() !== year || date.getMonth() !== month - 1 || date.getDate() !== day ) { return null; } return date; } const birthDate = parseDate(birthString); if (!birthDate) { throw new Error( "生年月日は YYYY-MM-DD の形式で入力してください。" ); } const today = new Date(); // ===================================================== // 想定寿命の日付 // ===================================================== const endDate = new Date(birthDate); endDate.setFullYear( birthDate.getFullYear() + lifeExpectancy ); // ===================================================== // 人生の経過率 // ===================================================== const totalLife = endDate.getTime() - birthDate.getTime(); const elapsed = today.getTime() - birthDate.getTime(); let percentage = (elapsed / totalLife) * 100; percentage = Math.max( 0, Math.min(100, percentage) ); const percentageText = percentage.toFixed(1); // ===================================================== // 日数 // ===================================================== const DAY = 1000 * 60 * 60 * 24; const livedDays = Math.max( 0, Math.floor(elapsed / DAY) ); const remainingDays = Math.max( 0, Math.ceil( (endDate.getTime() - today.getTime()) / DAY ) ); // ===================================================== // 残り年数 // ===================================================== let remainingYears = endDate.getFullYear() - today.getFullYear(); const birthdayThisYear = new Date( today.getFullYear(), endDate.getMonth(), endDate.getDate() ); if (today >= birthdayThisYear) { remainingYears--; } remainingYears = Math.max(0, remainingYears); // ===================================================== // 背景 // ===================================================== function createGradient() { const gradient = new LinearGradient(); gradient.locations = [0, 1]; gradient.colors = [ new Color("#171A21"), new Color("#292E38") ]; return gradient; } // ===================================================== // テキスト追加 // ===================================================== function addText( stack, text, size, weight = "regular", opacity = 1 ) { const label = stack.addText(text); if (weight === "bold") { label.font = Font.boldSystemFont(size); } else if (weight === "medium") { label.font = Font.mediumSystemFont(size); } else { label.font = Font.systemFont(size); } label.textColor = new Color( "#FFFFFF", opacity ); return label; } // ===================================================== // 円グラフ // ===================================================== function createRingWithText( text, percentage, size, lineWidth, fontSize ) { const ctx = new DrawContext(); ctx.size = new Size(size, size); ctx.opaque = false; ctx.respectScreenScale = true; const centerX = size / 2; const centerY = size / 2; const radius = (size - lineWidth) / 2; // --------------------------------------------------- // 背景リング // --------------------------------------------------- ctx.setStrokeColor( new Color( "#FFFFFF", 0.14 ) ); ctx.setLineWidth(lineWidth); ctx.strokeEllipse( new Rect( lineWidth / 2, lineWidth / 2, size - lineWidth, size - lineWidth ) ); // --------------------------------------------------- // 進捗リング // --------------------------------------------------- ctx.setStrokeColor( new Color("#FFFFFF") ); ctx.setLineWidth(lineWidth); const startAngle = -Math.PI / 2; const endAngle = startAngle + Math.PI * 2 * (percentage / 100); const path = new Path(); const steps = Math.max( 1, Math.ceil( 200 * percentage / 100 ) ); for ( let i = 0; i <= steps; i++ ) { const progress = i / steps; const angle = startAngle + ( endAngle - startAngle ) * progress; const x = centerX + radius * Math.cos(angle); const y = centerY + radius * Math.sin(angle); if (i === 0) { path.move( new Point(x, y) ); } else { path.addLine( new Point(x, y) ); } } ctx.addPath(path); ctx.strokePath(); // --------------------------------------------------- // 円の先端を丸くする // --------------------------------------------------- // Scriptableには setLineCap() がないため、 // 先端に小さな円を描いて丸く見せる if ( percentage > 0 && percentage < 100 ) { const dotRadius = lineWidth / 2; // 開始位置 const startX = centerX + radius * Math.cos(startAngle); const startY = centerY + radius * Math.sin(startAngle); ctx.setFillColor( new Color("#FFFFFF") ); ctx.fillEllipse( new Rect( startX - dotRadius, startY - dotRadius, lineWidth, lineWidth ) ); // 終了位置 const endX = centerX + radius * Math.cos(endAngle); const endY = centerY + radius * Math.sin(endAngle); ctx.fillEllipse( new Rect( endX - dotRadius, endY - dotRadius, lineWidth, lineWidth ) ); } // --------------------------------------------------- // 中央 % // --------------------------------------------------- ctx.setTextColor( new Color("#FFFFFF") ); ctx.setFont( Font.boldSystemFont( fontSize ) ); ctx.setTextAlignedCenter(); ctx.drawTextInRect( `${text}%`, new Rect( 0, size / 2 - fontSize * 0.65, size, fontSize * 1.5 ) ); return ctx.getImage(); } // ===================================================== // Widget // ===================================================== const widget = new ListWidget(); widget.backgroundGradient = createGradient(); widget.refreshAfterDate = new Date( Date.now() + 60 * 60 * 1000 ); // ===================================================== // SMALL // ===================================================== function createSmall() { widget.setPadding( 14, 14, 14, 14 ); addText( widget, "人生", 13, "bold", 0.65 ); widget.addSpacer(7); const ringSize = 88; const ringStack = widget.addStack(); ringStack.addSpacer(); const image = ringStack.addImage( createRingWithText( percentageText, percentage, ringSize, 8, 21 ) ); image.imageSize = new Size( ringSize, ringSize ); ringStack.addSpacer(); widget.addSpacer(); addText( widget, `${lifeExpectancy}歳まで あと${remainingYears}年`, 10, "medium", 0.55 ); } // ===================================================== // MEDIUM // ===================================================== function createMedium() { widget.setPadding( 18, 20, 18, 20 ); const main = widget.addStack(); main.centerAlignContent(); // --------------------------------------------------- // 左側 // --------------------------------------------------- const left = main.addStack(); left.layoutVertically(); addText( left, "人生", 14, "bold", 0.6 ); left.addSpacer(8); const ringSize = 105; const image = left.addImage( createRingWithText( percentageText, percentage, ringSize, 9, 24 ) ); image.imageSize = new Size( ringSize, ringSize ); main.addSpacer(22); // --------------------------------------------------- // 右側 // --------------------------------------------------- const right = main.addStack(); right.layoutVertically(); addText( right, "今日まで", 11, "medium", 0.5 ); right.addSpacer(2); addText( right, `${livedDays.toLocaleString()}日`, 20, "bold" ); right.addSpacer(13); addText( right, `${lifeExpectancy}歳まで`, 11, "medium", 0.5 ); right.addSpacer(2); addText( right, `あと ${remainingYears}年`, 20, "bold" ); } // ===================================================== // LARGE // ===================================================== function createLarge() { widget.setPadding( 24, 24, 24, 24 ); addText( widget, "人生カウント", 17, "bold", 0.65 ); widget.addSpacer(20); const ringSize = 165; const ring = widget.addStack(); ring.addSpacer(); const image = ring.addImage( createRingWithText( percentageText, percentage, ringSize, 12, 34 ) ); image.imageSize = new Size( ringSize, ringSize ); ring.addSpacer(); widget.addSpacer(22); const stats = widget.addStack(); stats.centerAlignContent(); // 今日まで const lived = stats.addStack(); lived.layoutVertically(); lived.centerAlignContent(); addText( lived, "今日まで", 12, "medium", 0.5 ); lived.addSpacer(4); addText( lived, `${livedDays.toLocaleString()}日`, 21, "bold" ); stats.addSpacer(30); // 残り const remain = stats.addStack(); remain.layoutVertically(); remain.centerAlignContent(); addText( remain, `${lifeExpectancy}歳まで`, 12, "medium", 0.5 ); remain.addSpacer(4); addText( remain, `あと ${remainingYears}年`, 21, "bold" ); widget.addSpacer(); const bottom = widget.addStack(); bottom.addSpacer(); addText( bottom, `残り ${remainingDays.toLocaleString()}日`, 11, "medium", 0.45 ); bottom.addSpacer(); } // ===================================================== // サイズ判定 // ===================================================== const family = config.widgetFamily || "medium"; if (family === "small") { createSmall(); } else if (family === "large") { createLarge(); } else { createMedium(); } // ===================================================== // 表示 // ===================================================== if (config.runsInWidget) { Script.setWidget(widget); } else { if (family === "small") { await widget.presentSmall(); } else if (family === "large") { await widget.presentLarge(); } else { await widget.presentMedium(); } } Script.complete(); - ウィジェットの設定を作る
下の入力欄で自分用の設定を作り、生成されたパラメーターをコピーします。詳しい設定仕様は、設定手順の下でも確認できます。
人生カウント パラメーター作成
生年月日と想定寿命を入力すると、ウィジェット用のパラメーターを自動で作成します。
生年月日
想定寿命
歳想定寿命はあとからいつでも変更できます。
生成されたパラメーター
生年月日を入力してくださいコピーできたら、次の手順でホーム画面のウィジェットへ貼り付けます。
- ホーム画面に追加して設定する
iPhoneのホーム画面を長押しし、ウィジェット追加画面からScriptableを選びます。この記事の対応サイズの説明に合わせてサイズを選んで追加してください。
追加したウィジェットを長押しして「ウィジェットを編集」を開き、「Script」に保存したスクリプトを選びます。
「Parameter」に直前の手順でコピーした値を貼り付けます。設定を変えるときも、同じ入力欄で作り直して貼り替えてください。
編集画面を閉じて表示を確認します。ホーム画面への反映には少し時間がかかる場合があります。
パラメータ・カスタマイズ
パラメーター の設定
このウィジェットでは、Parameter(パラメータ)に生年月日と想定寿命を設定します。
ウィジェットを長押しして、
「ウィジェットを編集」→「Parameter」
に、下記の形式で生年月日と想定寿命を入力してください。
例:1979-05-10,85
と入力した場合は、
1979年5月10日生まれ・想定寿命85歳
として人生の経過率を計算します。
想定寿命はいつでも変更できます。
想定寿命を変更するために、スクリプトを書き換える必要はありません。
ホーム画面でウィジェットを長押しして、
「ウィジェットを編集」→「Parameter」
を開き、想定寿命の数字だけ変更してください。
例:1979-05-10,99
とすれば 想定寿命99歳 の計算で表示されます。
設定手順内のジェネレーターでパラメーターを生成してコピーすると簡単です。
人生の経過率について
このウィジェットは、生年月日から現在までの日数と、設定した想定寿命までの期間をもとに、人生が現在何%まで経過しているかを計算します。
表示される想定寿命は平均寿命などから自動的に決められるものではありません。
80歳、85歳、90歳、100歳など、自分で好きな年齢を設定できます。
あくまで設定した年齢を100%とした場合の目安として楽しんでください。
動作確認・使用結果
| 確認した端末 | iPhone 17 ProMAX |
|---|---|
| iOS | iOS27 |
| Scriptable | 1.7.19 |
| 確認したサイズ | 小・中・大 |
| バージョン | 1.0.0 |
| 動作確認日 | 2026-09-18 |
表示・使用時のポイント
生年月日と自分で決めた想定寿命をもとに、経過した期間の割合を計算します。想定寿命を変えると割合や残り期間も変わります。
更新履歴
1.0.0
- 初回リリース
よくあるエラーと対処
- 確認すること
- 表示されない場合は、ウィジェット編集画面の「Script」に保存したスクリプトが選ばれているかも確認してください。コードを貼り直すときは全文を置き換えます。
- 割合や年齢が合わない
- 生年月日と想定寿命を確認し、フォームで作ったParameterを貼り直してください。
- 残り期間が少ない・マイナスになる
- 設定した想定寿命を超えている可能性があります。自分の目的に合う値へ変更してください。
注意事項・制限
- 想定寿命は自分で設定する値です。余命の予測や医学的な評価ではありません。
- 端末の日付に基づく計算のため、日付設定を変えると表示も変わります。
- ホーム画面の更新タイミングはiOSが管理するため、操作やデータの変更がすぐに反映されない場合があります。
データ提供・参考資料
このウィジェットにはデータ提供や参考資料は特にありません。
SHARE

