洗濯指数ウィジェット
設定難易度

「今日は外に洗濯物を干して大丈夫?」
「ちゃんと乾きそう?」
「部屋干しにした方がいい?」

そんな毎日のちょっとした判断を、ホーム画面ですぐ確認できるScriptable用ウィジェットです。

天気予報を見れば気温や降水確率は分かりますが、知りたいのは結局「今日は洗濯物が乾くのか?」だったりします。
それだけをパッと確認できるものが欲しくて、気温・湿度・降水確率・風速・天候から、洗濯物の乾きやすさを独自に計算するウィジェットを作りました。

結果は0〜100の「洗濯指数」で表示。天気予報アプリを開かなくても、iPhoneのホーム画面を見るだけで今日が洗濯日和かどうかを確認できます。

※表示される洗濯指数は気象データをもとに独自計算した参考値です。

ウィジェットの表示内容

郵便番号から地域を特定し、予報の気温・湿度・降水確率・風を使って独自の洗濯指数を計算します。

サイズごとの表示

  • 小:今日の洗濯指数と乾きやすさの目安を表示します。
  • 中:気温・湿度・降水確率・風速も確認できます。
  • 大:午前8〜12時・午後12〜16時・夕方16〜19時の目安と日中の天気情報を表示します。

設定手順

上から順に進めてください。コードを保存したら、ホーム画面に追加して郵便番号を入力します。

  1. Scriptableを用意する

    App StoreでScriptableを開き、iPhoneにインストールします。すでに入っている場合は次へ進んでください。

  2. コードをコピーして保存する

    下のコード欄の「コピー」を押します。Scriptableへ移動し、「+」から新しいスクリプトを作ってコード全文を貼り付け、洗濯指数などの名前で保存してください。

    保存したらこのページに戻り、次の手順へ進みます。

    // Version: 1.0.1
    // =====================================================
    // 洗濯指数ウィジェット for Scriptable
    // =====================================================
    //
    // ウィジェットのParameterに郵便番号を入力してください。
    // 例:671-2576
    //
    // 位置情報の許可:不要
    // APIキー:不要
    //
    // 使用データ
    // ・HeartRails Geo API:郵便番号 → 緯度経度
    // ・Open-Meteo:天気予報
    //
    // 洗濯指数は気温・湿度・降水確率・風速・天候から
    // 独自に算出した参考値です。
    // =====================================================
    
    
    // =====================================================
    // 基本設定
    // =====================================================
    
    const BG_COLOR = new Color("#F5F5F3");
    const TEXT_COLOR = new Color("#1C1C1E");
    const SUB_COLOR = new Color("#777777");
    const LINE_COLOR = new Color("#DDDDDA");
    
    const ACCENT_GOOD = new Color("#F5A623");
    const ACCENT_NORMAL = new Color("#4A90E2");
    const ACCENT_BAD = new Color("#8E8E93");
    
    
    // =====================================================
    // 郵便番号取得
    // =====================================================
    
    let postal = args.widgetParameter
      ? args.widgetParameter.trim()
      : "";
    
    postal = postal.replace(/[^0-9]/g, "");
    
    if (!postal) {
      const widget = createErrorWidget(
        "郵便番号を設定してください",
        "ウィジェットを長押し → 編集 → Parameter"
      );
    
      Script.setWidget(widget);
      Script.complete();
    
      if (!config.runsInWidget) {
        await widget.presentSmall();
      }
    
      return;
    }
    
    if (postal.length !== 7) {
      const widget = createErrorWidget(
        "郵便番号が正しくありません",
        "7桁の郵便番号を入力してください"
      );
    
      Script.setWidget(widget);
      Script.complete();
    
      if (!config.runsInWidget) {
        await widget.presentSmall();
      }
    
      return;
    }
    
    
    // =====================================================
    // 郵便番号 → 緯度経度
    // =====================================================
    
    let locationData;
    
    try {
    
      const geoURL =
        "https://geoapi.heartrails.com/api/json" +
        "?method=searchByPostal" +
        "&postal=" + encodeURIComponent(postal);
    
      const req = new Request(geoURL);
      const json = await req.loadJSON();
    
      if (
        !json.response ||
        !json.response.location ||
        json.response.location.length === 0
      ) {
        throw new Error("住所が見つかりません");
      }
    
      const loc = json.response.location[0];
    
      locationData = {
        latitude: Number(loc.y),
        longitude: Number(loc.x),
        prefecture: loc.prefecture || "",
        city: loc.city || "",
        town: loc.town || ""
      };
    
    } catch (e) {
    
      const widget = createErrorWidget(
        "地域を取得できませんでした",
        "郵便番号を確認してください"
      );
    
      Script.setWidget(widget);
      Script.complete();
    
      if (!config.runsInWidget) {
        await widget.presentSmall();
      }
    
      return;
    }
    
    
    // =====================================================
    // 天気データ取得
    // =====================================================
    
    let weather;
    
    try {
    
      const lat = locationData.latitude;
      const lon = locationData.longitude;
    
      const weatherURL =
        "https://api.open-meteo.com/v1/forecast" +
        "?latitude=" + lat +
        "&longitude=" + lon +
        "&hourly=" +
        "temperature_2m," +
        "relative_humidity_2m," +
        "precipitation_probability," +
        "weather_code," +
        "wind_speed_10m" +
        "&forecast_days=1" +
        "&timezone=Asia%2FTokyo" +
        "&wind_speed_unit=ms";
    
      const req = new Request(weatherURL);
      weather = await req.loadJSON();
    
    } catch (e) {
    
      const widget = createErrorWidget(
        "天気情報を取得できませんでした",
        "しばらくしてから再度お試しください"
      );
    
      Script.setWidget(widget);
      Script.complete();
    
      if (!config.runsInWidget) {
        await widget.presentSmall();
      }
    
      return;
    }
    
    
    // =====================================================
    // 時間別データ作成
    // =====================================================
    
    const hourly = [];
    
    for (let i = 0; i < weather.hourly.time.length; i++) {
    
      const time = new Date(weather.hourly.time[i]);
    
      hourly.push({
        time: time,
        hour: time.getHours(),
        temp: weather.hourly.temperature_2m[i],
        humidity: weather.hourly.relative_humidity_2m[i],
        rain: weather.hourly.precipitation_probability[i] ?? 0,
        wind: weather.hourly.wind_speed_10m[i],
        code: weather.hourly.weather_code[i]
      });
    }
    
    
    // =====================================================
    // 洗濯指数計算
    // =====================================================
    
    function calcLaundryIndex(data) {
    
      let score = 50;
    
      // ---------------------------------------------------
      // 気温
      // ---------------------------------------------------
    
      if (data.temp >= 30) {
        score += 25;
      } else if (data.temp >= 25) {
        score += 22;
      } else if (data.temp >= 20) {
        score += 18;
      } else if (data.temp >= 15) {
        score += 12;
      } else if (data.temp >= 10) {
        score += 5;
      } else {
        score -= 5;
      }
    
    
      // ---------------------------------------------------
      // 湿度
      // ---------------------------------------------------
    
      if (data.humidity <= 40) {
        score += 25;
      } else if (data.humidity <= 50) {
        score += 20;
      } else if (data.humidity <= 60) {
        score += 12;
      } else if (data.humidity <= 70) {
        score += 5;
      } else if (data.humidity <= 80) {
        score -= 10;
      } else {
        score -= 25;
      }
    
    
      // ---------------------------------------------------
      // 風
      // ---------------------------------------------------
    
      if (data.wind >= 3 && data.wind <= 7) {
        score += 10;
      } else if (data.wind >= 1.5) {
        score += 5;
      } else if (data.wind < 0.5) {
        score -= 5;
      }
    
    
      // ---------------------------------------------------
      // 降水確率
      // ---------------------------------------------------
    
      if (data.rain >= 80) {
        score -= 60;
      } else if (data.rain >= 60) {
        score -= 45;
      } else if (data.rain >= 40) {
        score -= 30;
      } else if (data.rain >= 20) {
        score -= 15;
      }
    
    
      // ---------------------------------------------------
      // 天候コード
      // Open-Meteo WMO Weather Code
      // ---------------------------------------------------
    
      const code = data.code;
    
      // 雨
      if (
        (code >= 51 && code <= 67) ||
        (code >= 80 && code <= 82)
      ) {
        score -= 40;
      }
    
      // 雷雨
      if (code >= 95) {
        score -= 60;
      }
    
      // 雪
      if (
        (code >= 71 && code <= 77) ||
        (code >= 85 && code <= 86)
      ) {
        score -= 50;
      }
    
    
      return Math.max(0, Math.min(100, Math.round(score)));
    }
    
    
    // =====================================================
    // 時間帯
    // =====================================================
    
    function getPeriod(startHour, endHour) {
    
      const list = hourly.filter(item =>
        item.hour >= startHour &&
        item.hour < endHour
      );
    
      if (!list.length) return null;
    
      const avg = key =>
        list.reduce((sum, item) => sum + item[key], 0) / list.length;
    
      const max = key =>
        Math.max(...list.map(item => item[key]));
    
      const representative = {
        temp: avg("temp"),
        humidity: avg("humidity"),
        rain: max("rain"),
        wind: avg("wind"),
        code: getWorstWeatherCode(list)
      };
    
      return {
        index: calcLaundryIndex(representative),
        temp: representative.temp,
        humidity: representative.humidity,
        rain: representative.rain,
        wind: representative.wind
      };
    }
    
    
    // =====================================================
    // 悪天候優先
    // =====================================================
    
    function getWorstWeatherCode(list) {
    
      const codes = list.map(item => item.code);
    
      if (codes.some(c => c >= 95)) return 95;
    
      if (
        codes.some(c =>
          (c >= 71 && c <= 77) ||
          (c >= 85 && c <= 86)
        )
      ) return 71;
    
      if (
        codes.some(c =>
          (c >= 51 && c <= 67) ||
          (c >= 80 && c <= 82)
        )
      ) return 61;
    
      if (codes.some(c => c >= 3)) return 3;
    
      if (codes.some(c => c >= 1)) return 1;
    
      return 0;
    }
    
    
    // =====================================================
    // 今日の時間帯
    // =====================================================
    
    const morning = getPeriod(8, 12);
    const afternoon = getPeriod(12, 16);
    const evening = getPeriod(16, 19);
    
    
    // =====================================================
    // 今日全体
    // 8:00〜17:00を洗濯時間として判定
    // =====================================================
    
    const daytime = getPeriod(8, 17);
    
    const laundryIndex = daytime
      ? daytime.index
      : 0;
    
    
    // =====================================================
    // 判定
    // =====================================================
    
    function getLaundryStatus(index) {
    
      if (index >= 80) {
        return {
          label: "よく乾きます",
          message: "今日は外干し日和!",
          emoji: "☀️",
          color: ACCENT_GOOD
        };
      }
    
      if (index >= 60) {
        return {
          label: "乾きやすい",
          message: "外干しがおすすめ",
          emoji: "🌤",
          color: ACCENT_GOOD
        };
      }
    
      if (index >= 40) {
        return {
          label: "やや乾きにくい",
          message: "早めの洗濯がおすすめ",
          emoji: "☁️",
          color: ACCENT_NORMAL
        };
      }
    
      if (index >= 20) {
        return {
          label: "乾きにくい",
          message: "部屋干しも検討して",
          emoji: "🌧",
          color: ACCENT_BAD
        };
      }
    
      return {
        label: "部屋干し推奨",
        message: "今日は室内が安心",
        emoji: "🏠",
        color: ACCENT_BAD
      };
    }
    
    const status = getLaundryStatus(laundryIndex);
    
    
    // =====================================================
    // Widget
    // =====================================================
    
    const widget = new ListWidget();
    
    widget.backgroundColor = BG_COLOR;
    
    const family = config.widgetFamily || "small";
    
    if (family === "small") {
      createSmallWidget(widget);
    }
    
    else if (family === "medium") {
      createMediumWidget(widget);
    }
    
    else {
      createLargeWidget(widget);
    }
    
    
    // =====================================================
    // SMALL
    // =====================================================
    
    function createSmallWidget(widget) {
    
      widget.setPadding(15, 15, 14, 15);
    
      const title = widget.addText("🧺 洗濯指数");
      title.font = Font.boldSystemFont(13);
      title.textColor = TEXT_COLOR;
    
      widget.addSpacer(7);
    
      const score = widget.addText(String(laundryIndex));
      score.font = Font.boldSystemFont(43);
      score.textColor = status.color;
      score.minimumScaleFactor = 0.8;
    
      widget.addSpacer(1);
    
      const label = widget.addText(status.label);
      label.font = Font.boldSystemFont(14);
      label.textColor = TEXT_COLOR;
    
      widget.addSpacer(5);
    
      const message = widget.addText(status.message);
      message.font = Font.systemFont(11);
      message.textColor = SUB_COLOR;
      message.lineLimit = 1;
      message.minimumScaleFactor = 0.75;
    
      widget.addSpacer();
    
      const city = widget.addText(locationData.city);
      city.font = Font.systemFont(9);
      city.textColor = SUB_COLOR;
    }
    
    
    // =====================================================
    // MEDIUM
    // =====================================================
    
    function createMediumWidget(widget) {
    
      widget.setPadding(15, 17, 13, 17);
    
      const top = widget.addStack();
      top.centerAlignContent();
    
      const left = top.addStack();
      left.layoutVertically();
    
      const title = left.addText("🧺 洗濯指数");
      title.font = Font.boldSystemFont(13);
      title.textColor = TEXT_COLOR;
    
      left.addSpacer(4);
    
      const location = left.addText(
        locationData.prefecture + " " + locationData.city
      );
    
      location.font = Font.systemFont(10);
      location.textColor = SUB_COLOR;
    
      top.addSpacer();
    
      const right = top.addStack();
      right.centerAlignContent();
    
      const score = right.addText(String(laundryIndex));
      score.font = Font.boldSystemFont(37);
      score.textColor = status.color;
    
      right.addSpacer(6);
    
      const statusStack = right.addStack();
      statusStack.layoutVertically();
    
      const label = statusStack.addText(status.label);
      label.font = Font.boldSystemFont(13);
      label.textColor = TEXT_COLOR;
    
      const message = statusStack.addText(status.message);
      message.font = Font.systemFont(9);
      message.textColor = SUB_COLOR;
    
      widget.addSpacer(10);
    
      const line = widget.addStack();
      line.backgroundColor = LINE_COLOR;
      line.size = new Size(0, 1);
    
      widget.addSpacer(10);
    
      if (daytime) {
    
        const info = widget.addStack();
        info.centerAlignContent();
    
        addWeatherInfo(
          info,
          "🌡",
          Math.round(daytime.temp) + "℃"
        );
    
        info.addSpacer();
    
        addWeatherInfo(
          info,
          "💧",
          Math.round(daytime.humidity) + "%"
        );
    
        info.addSpacer();
    
        addWeatherInfo(
          info,
          "☔",
          Math.round(daytime.rain) + "%"
        );
    
        info.addSpacer();
    
        addWeatherInfo(
          info,
          "💨",
          daytime.wind.toFixed(1) + "m/s"
        );
      }
    }
    
    
    // =====================================================
    // LARGE
    // =====================================================
    
    function createLargeWidget(widget) {
    
      widget.setPadding(18, 18, 14, 18);
    
      const header = widget.addStack();
      header.centerAlignContent();
    
      const titleStack = header.addStack();
      titleStack.layoutVertically();
    
      const title = titleStack.addText("🧺 洗濯指数");
      title.font = Font.boldSystemFont(15);
      title.textColor = TEXT_COLOR;
    
      titleStack.addSpacer(3);
    
      const location = titleStack.addText(
        locationData.prefecture +
        " " +
        locationData.city +
        " " +
        locationData.town
      );
    
      location.font = Font.systemFont(10);
      location.textColor = SUB_COLOR;
      location.lineLimit = 1;
    
      header.addSpacer();
    
      const score = header.addText(String(laundryIndex));
      score.font = Font.boldSystemFont(45);
      score.textColor = status.color;
    
      widget.addSpacer(8);
    
      const statusText = widget.addText(
        status.emoji +
        " " +
        status.label +
        " " +
        status.message
      );
    
      statusText.font = Font.boldSystemFont(14);
      statusText.textColor = TEXT_COLOR;
    
      widget.addSpacer(13);
    
      const line1 = widget.addStack();
      line1.backgroundColor = LINE_COLOR;
      line1.size = new Size(0, 1);
    
      widget.addSpacer(13);
    
    
      // ---------------------------------------------------
      // 時間帯
      // ---------------------------------------------------
    
      addPeriodRow(widget, "午前", "8〜12時", morning);
    
      widget.addSpacer(10);
    
      addPeriodRow(widget, "午後", "12〜16時", afternoon);
    
      widget.addSpacer(10);
    
      addPeriodRow(widget, "夕方", "16〜19時", evening);
    
    
      widget.addSpacer(14);
    
      const line2 = widget.addStack();
      line2.backgroundColor = LINE_COLOR;
      line2.size = new Size(0, 1);
    
      widget.addSpacer(10);
    
    
      // ---------------------------------------------------
      // 今日の平均
      // ---------------------------------------------------
    
      if (daytime) {
    
        const info = widget.addStack();
    
        addWeatherInfo(
          info,
          "🌡",
          Math.round(daytime.temp) + "℃"
        );
    
        info.addSpacer();
    
        addWeatherInfo(
          info,
          "💧",
          Math.round(daytime.humidity) + "%"
        );
    
        info.addSpacer();
    
        addWeatherInfo(
          info,
          "☔",
          Math.round(daytime.rain) + "%"
        );
    
        info.addSpacer();
    
        addWeatherInfo(
          info,
          "💨",
          daytime.wind.toFixed(1) + "m/s"
        );
      }
    
      widget.addSpacer();
    
      const credit = widget.addText(
        "Weather: Open-Meteo  /  Geo: HeartRails"
      );
    
      credit.font = Font.systemFont(7);
      credit.textColor = new Color("#AAAAAA");
    }
    
    
    // =====================================================
    // 時間帯表示
    // =====================================================
    
    function addPeriodRow(widget, label, time, data) {
    
      if (!data) return;
    
      const row = widget.addStack();
      row.centerAlignContent();
    
      const nameStack = row.addStack();
      nameStack.layoutVertically();
    
      const name = nameStack.addText(label);
      name.font = Font.boldSystemFont(13);
      name.textColor = TEXT_COLOR;
    
      const timeText = nameStack.addText(time);
      timeText.font = Font.systemFont(9);
      timeText.textColor = SUB_COLOR;
    
      row.addSpacer();
    
      const weather = row.addText(
        "☔ " +
        Math.round(data.rain) +
        "% 💧 " +
        Math.round(data.humidity) +
        "%"
      );
    
      weather.font = Font.systemFont(10);
      weather.textColor = SUB_COLOR;
    
      row.addSpacer(12);
    
      const index = row.addText(String(data.index));
      index.font = Font.boldSystemFont(22);
      index.textColor =
        getLaundryStatus(data.index).color;
    }
    
    
    // =====================================================
    // 天気情報
    // =====================================================
    
    function addWeatherInfo(stack, icon, value) {
    
      const item = stack.addStack();
      item.centerAlignContent();
    
      const text = item.addText(icon + " " + value);
      text.font = Font.systemFont(10);
      text.textColor = TEXT_COLOR;
    }
    
    
    // =====================================================
    // エラーWidget
    // =====================================================
    
    function createErrorWidget(titleText, messageText) {
    
      const widget = new ListWidget();
    
      widget.backgroundColor = BG_COLOR;
      widget.setPadding(16, 16, 16, 16);
    
      const icon = widget.addText("🧺");
      icon.font = Font.systemFont(28);
    
      widget.addSpacer(8);
    
      const title = widget.addText(titleText);
      title.font = Font.boldSystemFont(14);
      title.textColor = TEXT_COLOR;
    
      widget.addSpacer(5);
    
      const message = widget.addText(messageText);
      message.font = Font.systemFont(11);
      message.textColor = SUB_COLOR;
    
      return widget;
    }
    
    
    // =====================================================
    // 表示
    // =====================================================
    
    widget.refreshAfterDate =
      new Date(Date.now() + 30 * 60 * 1000);
    
    Script.setWidget(widget);
    Script.complete();
    
    
    // Scriptable内で実行した場合はプレビュー
    if (!config.runsInWidget) {
    
      if (family === "medium") {
        await widget.presentMedium();
      }
    
      else if (family === "large") {
        await widget.presentLarge();
    
    
    
      }
      else {
        await widget.presentSmall();
      }
    }
    
  3. ホーム画面に追加して設定する

    iPhoneのホーム画面を長押しし、ウィジェット追加画面からScriptableを選びます。この記事の対応サイズの説明に合わせてサイズを選んで追加してください。

    追加したウィジェットを長押しして「ウィジェットを編集」を開き、「Script」に保存したスクリプトを選びます。

    「Parameter」に確認したい地域の7桁の郵便番号を入力します。例:1000001。ハイフンあり・なしのどちらでも使えます。

    編集画面を閉じて表示を確認します。ホーム画面への反映には少し時間がかかる場合があります。

洗濯指数について

洗濯指数は、気温・湿度・降水確率・風速・天候をもとに独自に計算しています。 
指数は0〜100で表示され、

  • 80〜100:よく乾きます
  • 60〜79:乾きやすい
  • 40〜59:やや乾きにくい
  • 20〜39:乾きにくい
  • 0〜19:部屋干し推奨

の5段階で判定します。 
単純に晴れ・雨だけで判断するのではなく、湿度や風なども含めて「洗濯物が乾きやすいか」を判断します。

※表示される洗濯指数は、気象データをもとに独自計算した参考値です。

動作確認・使用結果

確認した端末 iPhone 17 ProMAX
iOS iOS27
Scriptable 1.7.19
確認したサイズ 小・中・大
バージョン 1.0.1
動作確認日 2026-10-11

表示・使用時のポイント

郵便番号から地域を特定し、予報の気温・湿度・降水確率・風を使って独自の洗濯指数を計算します。大サイズでは干す時間帯を比較できます。

更新履歴

1.0.1

  • 記事内の配布コードで欠けていた末尾を補完。小・中・大サイズのプレビュー分岐を修正。

1.0.0

  • 初回リリース

よくあるエラーと対処

確認すること
表示されない場合は、ウィジェット編集画面の「Script」に保存したスクリプトが選ばれているかも確認してください。コードを貼り直すときは全文を置き換えます。
地域が取得できない
Parameterに対象地域の7桁の郵便番号を入れてください。住所や地名では検索しません。
指数や天気が表示されない
通信を確認し、Scriptableで実行して取得エラーを確認してください。
実際の乾き方と違う
予報と独自の計算による目安です。日当たり・洗濯物の厚さ・干し方は反映していません。

注意事項・制限

  • 公的な洗濯指数ではなく、予報値から計算する独自の指標です。
  • 対象地域の予報を使うため、自宅のベランダなど局所的な条件までは分かりません。
  • ホーム画面の更新タイミングはiOSが管理するため、操作やデータの変更がすぐに反映されない場合があります。

データ提供・参考資料

Open-Meteo:天気予報データ。
HeartRails Geo API:郵便番号から地域と座標を取得します。

SHARE

この記事をシェア