今日なに着る?
設定難易度

「今日は半袖で大丈夫?」「夜は上着がいる?」
そんな毎日の服装選びを、天気予報からサッと判断できるScriptableウィジェットです。
設定した地域の気温・体感温度・降水確率・風・寒暖差をもとに、その日におすすめの服装を表示します。
朝・昼・夜それぞれの服装も確認できるので、昼間は暖かくても夜に冷え込む日など、一日の気温変化に合わせた服装選びにも便利です。

ウィジェットの表示内容

郵便番号の地域の予報から、体感温度や天気に合わせた服装の目安を作ります。

サイズごとの表示

  • 小:おすすめの服装と気温・体感温度、ひとことを表示します。
  • 中:朝・昼・夜の体感温度と服装の目安を表示します。
  • 大:雨・風・寒暖差なども含めて詳しく表示します。

設定手順

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

  1. Scriptableを用意する

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

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

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

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

    // Variables used by Scriptable.
    // These must be at the very top of the file. Do not edit.
    // icon-color: blue; icon-glyph: t-shirt;
    // Version: 1.0.1
    
    // =====================================================
    // 今日なに着る? for Scriptable V2
    // =====================================================
    //
    // Parameterに郵便番号を入力
    // 例:1000001
    //
    // 100-0001 のようなハイフンありでもOK
    //
    // ・APIキー不要
    // ・位置情報許可不要
    // ・朝 / 昼 / 夜の気温・体感温度を取得
    // ・体感温度、風、雨から服装を判定
    // ・寒暖差も考慮
    // ・体感温度に応じて気温表示の色が変化
    // ・小 / 中 / 大サイズ対応
    // ・郵便番号 → 位置情報をキャッシュ
    // ・天気データをキャッシュ
    // ・通信失敗時は前回データを使用
    // ・約60分ごとに更新
    //
    // 地点取得:HeartRails Geo API
    // 天気予報:Open-Meteo
    //
    // =====================================================
    
    
    // =====================================================
    // 設定
    // =====================================================
    
    const BG_COLOR =
      new Color("#101216");
    
    const CARD_COLOR =
      new Color("#FFFFFF", 0.07);
    
    const TEXT_COLOR =
      Color.white();
    
    const SUB_COLOR =
      new Color("#AEB4BE");
    
    const DIM_COLOR =
      new Color("#777D87");
    
    const REFRESH_MINUTES = 60;
    
    const CACHE_FOLDER =
      "what_to_wear_v2";
    
    
    // =====================================================
    // FileManager
    // =====================================================
    
    const fm =
      FileManager.local();
    
    const cacheDir =
      fm.joinPath(
        fm.documentsDirectory(),
        CACHE_FOLDER
      );
    
    if (!fm.fileExists(cacheDir)) {
    
      fm.createDirectory(
        cacheDir,
        true
      );
    }
    
    
    // =====================================================
    // Parameter
    // =====================================================
    
    const rawParameter =
      typeof args !== "undefined" &&
      args.widgetParameter
        ? args.widgetParameter.trim()
        : "";
    
    const postalCode =
      rawParameter.replace(
        /[^0-9]/g,
        ""
      );
    
    
    // =====================================================
    // メイン
    // =====================================================
    
    async function main() {
    
      if (!/^\d{7}$/.test(postalCode)) {
    
        const widget =
          createErrorWidget(
            "郵便番号を設定してください",
            "例:1000001"
          );
    
        setRefresh(widget);
    
        Script.setWidget(widget);
    
        if (!config.runsInWidget) {
          await widget.presentSmall();
        }
    
        Script.complete();
        return;
      }
    
    
      try {
    
        // -------------------------------------------------
        // 位置情報
        // -------------------------------------------------
    
        const location =
          await getLocationCached(
            postalCode
          );
    
    
        // -------------------------------------------------
        // 天気
        // -------------------------------------------------
    
        const weatherResult =
          await getWeatherCached(
            location.latitude,
            location.longitude,
            postalCode
          );
    
    
        const data =
          analyzeWeather(
            weatherResult.weather
          );
    
    
        data.isCached =
          weatherResult.isCached;
    
    
        const widget =
          createWidget(
            data,
            location
          );
    
    
        setRefresh(widget);
    
        Script.setWidget(widget);
    
    
        if (!config.runsInWidget) {
    
          if (
            config.widgetFamily === "large"
          ) {
    
            await widget.presentLarge();
    
          } else if (
            config.widgetFamily === "medium"
          ) {
    
            await widget.presentMedium();
    
          } else {
    
            await widget.presentSmall();
          }
        }
    
    
        Script.complete();
    
    
      } catch (error) {
    
        console.error(error);
    
    
        const widget =
          createErrorWidget(
            "データを取得できませんでした",
            String(error)
          );
    
    
        setRefresh(widget);
    
        Script.setWidget(widget);
    
    
        if (!config.runsInWidget) {
          await widget.presentSmall();
        }
    
    
        Script.complete();
      }
    }
    
    
    // =====================================================
    // 更新時間
    // =====================================================
    
    function setRefresh(widget) {
    
      widget.refreshAfterDate =
        new Date(
          Date.now() +
          REFRESH_MINUTES *
          60 *
          1000
        );
    }
    
    
    // =====================================================
    // 郵便番号 → 住所・緯度経度
    // キャッシュ対応
    // =====================================================
    
    async function getLocationCached(zip) {
    
      const path =
        fm.joinPath(
          cacheDir,
          `location_${zip}.json`
        );
    
    
      if (fm.fileExists(path)) {
    
        try {
    
          const cached =
            JSON.parse(
              fm.readString(path)
            );
    
    
          if (
            cached &&
            Number.isFinite(
              Number(cached.latitude)
            ) &&
            Number.isFinite(
              Number(cached.longitude)
            )
          ) {
    
            console.log(
              "位置情報キャッシュを使用"
            );
    
            return cached;
          }
    
        } catch (e) {
    
          console.log(
            "位置情報キャッシュ読込失敗"
          );
        }
      }
    
    
      const location =
        await getLocation(zip);
    
    
      try {
    
        fm.writeString(
          path,
          JSON.stringify(location)
        );
    
      } catch (e) {
    
        console.log(
          "位置情報キャッシュ保存失敗"
        );
      }
    
    
      return location;
    }
    
    
    // =====================================================
    // HeartRails Geo API
    // =====================================================
    
    async function getLocation(zip) {
    
      const url =
        "https://geoapi.heartrails.com/api/json" +
        "?method=searchByPostal" +
        "&postal=" +
        encodeURIComponent(zip);
    
    
      const request =
        new Request(url);
    
      request.timeoutInterval = 15;
    
    
      const json =
        await request.loadJSON();
    
    
      if (
        !json ||
        !json.response ||
        !json.response.location ||
        json.response.location.length === 0
      ) {
    
        throw new Error(
          "郵便番号が見つかりません"
        );
      }
    
    
      const item =
        json.response.location[0];
    
    
      const latitude =
        Number(item.y);
    
      const longitude =
        Number(item.x);
    
    
      if (
        !Number.isFinite(latitude) ||
        !Number.isFinite(longitude)
      ) {
    
        throw new Error(
          "緯度経度を取得できません"
        );
      }
    
    
      return {
    
        prefecture:
          item.prefecture || "",
    
        city:
          item.city || "",
    
        town:
          item.town || "",
    
        postal:
          item.postal || zip,
    
        latitude,
        longitude
      };
    }
    
    
    // =====================================================
    // 天気取得
    // キャッシュ対応
    // =====================================================
    
    async function getWeatherCached(
      latitude,
      longitude,
      zip
    ) {
    
      const path =
        fm.joinPath(
          cacheDir,
          `weather_${zip}.json`
        );
    
    
      try {
    
        const weather =
          await getWeather(
            latitude,
            longitude
          );
    
    
        try {
    
          fm.writeString(
            path,
            JSON.stringify({
              savedAt:
                new Date().toISOString(),
    
              weather
            })
          );
    
        } catch (e) {
    
          console.log(
            "天気キャッシュ保存失敗"
          );
        }
    
    
        return {
    
          weather,
          isCached: false
        };
    
    
      } catch (error) {
    
        console.log(
          "最新天気取得失敗:" +
          error
        );
    
    
        if (fm.fileExists(path)) {
    
          try {
    
            const cached =
              JSON.parse(
                fm.readString(path)
              );
    
    
            if (
              cached &&
              cached.weather
            ) {
    
              console.log(
                "前回の天気キャッシュを使用"
              );
    
    
              return {
    
                weather:
                  cached.weather,
    
                isCached: true
              };
            }
    
          } catch (e) {
    
            console.log(
              "天気キャッシュ読込失敗"
            );
          }
        }
    
    
        throw error;
      }
    }
    
    
    // =====================================================
    // Open-Meteo
    // =====================================================
    
    async function getWeather(
      latitude,
      longitude
    ) {
    
      const hourly = [
        "temperature_2m",
        "apparent_temperature",
        "precipitation_probability",
        "weather_code",
        "wind_speed_10m"
      ].join(",");
    
    
      const daily = [
        "weather_code",
        "temperature_2m_max",
        "temperature_2m_min",
        "apparent_temperature_max",
        "apparent_temperature_min",
        "precipitation_probability_max",
        "wind_speed_10m_max"
      ].join(",");
    
    
      const url =
        "https://api.open-meteo.com/v1/forecast" +
        "?latitude=" +
        encodeURIComponent(latitude) +
        "&longitude=" +
        encodeURIComponent(longitude) +
        "&hourly=" +
        encodeURIComponent(hourly) +
        "&daily=" +
        encodeURIComponent(daily) +
        "&timezone=Asia%2FTokyo" +
        "&wind_speed_unit=ms" +
        "&forecast_days=1";
    
    
      const request =
        new Request(url);
    
      request.timeoutInterval = 15;
    
    
      const json =
        await request.loadJSON();
    
    
      if (
        !json ||
        !json.hourly ||
        !json.daily
      ) {
    
        throw new Error(
          "天気予報を取得できません"
        );
      }
    
    
      return json;
    }
    
    
    // =====================================================
    // 天気解析
    // =====================================================
    
    function analyzeWeather(weather) {
    
      const morning =
        getPeriodData(
          weather.hourly,
          7,
          9
        );
    
    
      const daytime =
        getPeriodData(
          weather.hourly,
          11,
          15
        );
    
    
      const evening =
        getPeriodData(
          weather.hourly,
          18,
          21
        );
    
    
      const maxTemp =
        Number(
          weather.daily
            .temperature_2m_max[0]
        );
    
    
      const minTemp =
        Number(
          weather.daily
            .temperature_2m_min[0]
        );
    
    
      const maxApparent =
        Number(
          weather.daily
            .apparent_temperature_max[0]
        );
    
    
      const minApparent =
        Number(
          weather.daily
            .apparent_temperature_min[0]
        );
    
    
      const rain =
        Number(
          weather.daily
            .precipitation_probability_max[0] ??
          0
        );
    
    
      const wind =
        Number(
          weather.daily
            .wind_speed_10m_max[0] ??
          0
        );
    
    
      const weatherCode =
        Number(
          weather.daily
            .weather_code[0]
        );
    
    
      const tempDiff =
        maxTemp -
        minTemp;
    
    
      const result = {
    
        morning,
        daytime,
        evening,
    
        maxTemp,
        minTemp,
    
        maxApparent,
        minApparent,
    
        tempDiff,
        rain,
        wind,
    
        weather:
          getWeatherInfo(
            weatherCode
          )
      };
    
    
      result.advice =
        createAdvice(result);
    
    
      return result;
    }
    
    
    // =====================================================
    // 時間帯データ
    // =====================================================
    
    function getPeriodData(
      hourly,
      startHour,
      endHour
    ) {
    
      const indexes = [];
    
    
      for (
        let i = 0;
        i < hourly.time.length;
        i++
      ) {
    
        const hour =
          Number(
            hourly.time[i]
              .slice(11, 13)
          );
    
    
        if (
          hour >= startHour &&
          hour <= endHour
        ) {
    
          indexes.push(i);
        }
      }
    
    
      function average(key) {
    
        const values =
          indexes
            .map(
              i =>
                Number(
                  hourly[key][i]
                )
            )
            .filter(
              value =>
                Number.isFinite(value)
            );
    
    
        if (!values.length) {
          return 0;
        }
    
    
        return (
          values.reduce(
            (a, b) =>
              a + b,
            0
          ) /
          values.length
        );
      }
    
    
      function maximum(key) {
    
        const values =
          indexes
            .map(
              i =>
                Number(
                  hourly[key][i]
                )
            )
            .filter(
              value =>
                Number.isFinite(value)
            );
    
    
        if (!values.length) {
          return 0;
        }
    
    
        return Math.max(
          ...values
        );
      }
    
    
      const temp =
        average(
          "temperature_2m"
        );
    
    
      const apparent =
        average(
          "apparent_temperature"
        );
    
    
      const rain =
        maximum(
          "precipitation_probability"
        );
    
    
      const wind =
        maximum(
          "wind_speed_10m"
        );
    
    
      let weatherCode = 0;
    
    
      for (
        const index of indexes
      ) {
    
        const code =
          Number(
            hourly.weather_code[index]
          );
    
    
        if (
          weatherSeverity(code) >
          weatherSeverity(weatherCode)
        ) {
    
          weatherCode = code;
        }
      }
    
    
      return {
    
        temp,
        apparent,
        rain,
        wind,
    
        weather:
          getWeatherInfo(
            weatherCode
          ),
    
        outfit:
          getOutfit(
            apparent,
            wind,
            rain
          )
      };
    }
    
    
    // =====================================================
    // 服装判定
    // =====================================================
    
    function getOutfit(
      apparentTemp,
      wind,
      rain
    ) {
    
      let icon;
      let label;
      let shortLabel;
      let detail;
    
    
      if (apparentTemp >= 30) {
    
        icon = "👕";
        label = "半袖";
        shortLabel = "半袖";
    
        detail =
          "暑さ対策を。涼しい服装がおすすめ";
    
    
      } else if (
        apparentTemp >= 25
      ) {
    
        icon = "👕";
        label = "半袖";
        shortLabel = "半袖";
    
        detail =
          "半袖1枚で快適に過ごせそう";
    
    
      } else if (
        apparentTemp >= 21
      ) {
    
        icon = "👕";
        label = "半袖+羽織り";
        shortLabel = "半袖+羽織";
    
        detail =
          "半袖でもOK。薄手の羽織りがあると安心";
    
    
      } else if (
        apparentTemp >= 17
      ) {
    
        icon = "👔";
        label = "長袖";
        shortLabel = "長袖";
    
        detail =
          "薄手の長袖がちょうどよさそう";
    
    
      } else if (
        apparentTemp >= 13
      ) {
    
        icon = "🧶";
        label = "ニット・羽織り";
        shortLabel = "ニット";
    
        detail =
          "ニットやカーディガンがおすすめ";
    
    
      } else if (
        apparentTemp >= 8
      ) {
    
        icon = "🧥";
        label = "ジャケット";
        shortLabel = "上着";
    
        detail =
          "ジャケットやコートが欲しい気温";
    
    
      } else if (
        apparentTemp >= 3
      ) {
    
        icon = "🧥";
        label = "冬物コート";
        shortLabel = "コート";
    
        detail =
          "冬物のコートでしっかり防寒を";
    
    
      } else {
    
        icon = "🧣";
        label = "しっかり防寒";
        shortLabel = "防寒";
    
        detail =
          "厚手のコートと防寒小物がおすすめ";
      }
    
    
      // ---------------------------------------------------
      // 強風補正
      // 8.3m/s ≒ 30km/h
      // ---------------------------------------------------
    
      if (
        wind >= 8.3 &&
        apparentTemp < 21
      ) {
    
        detail +=
          "。風を通しにくい上着もおすすめ";
      }
    
    
      return {
    
        icon,
        label,
        shortLabel,
        detail
      };
    }
    
    
    // =====================================================
    // 体感温度カラー
    // =====================================================
    
    function temperatureColor(
      apparentTemp
    ) {
    
      if (
        apparentTemp >= 30
      ) {
    
        // 暑い
        return new Color(
          "#FF6B6B"
        );
      }
    
    
      if (
        apparentTemp >= 25
      ) {
    
        // 暖かい
        return new Color(
          "#FF9F5A"
        );
      }
    
    
      if (
        apparentTemp >= 21
      ) {
    
        // やや暖かい
        return new Color(
          "#FFD166"
        );
      }
    
    
      if (
        apparentTemp >= 17
      ) {
    
        // 快適
        return new Color(
          "#7EDC8B"
        );
      }
    
    
      if (
        apparentTemp >= 13
      ) {
    
        // やや涼しい
        return new Color(
          "#70D6D1"
        );
      }
    
    
      if (
        apparentTemp >= 8
      ) {
    
        // 寒い
        return new Color(
          "#69AFFF"
        );
      }
    
    
      // とても寒い
      return new Color(
        "#A99BFF"
      );
    }
    
    
    // =====================================================
    // 朝・昼・夜カラー
    // =====================================================
    
    function periodColor(
      title
    ) {
    
      if (
        title === "朝"
      ) {
    
        return new Color(
          "#FFB45C"
        );
      }
    
    
      if (
        title === "昼"
      ) {
    
        return new Color(
          "#6EC8FF"
        );
      }
    
    
      if (
        title === "夜"
      ) {
    
        return new Color(
          "#A99BFF"
        );
      }
    
    
      return SUB_COLOR;
    }
    
    
    // =====================================================
    // 総合アドバイス
    // =====================================================
    
    function createAdvice(data) {
    
      const result = [];
    
    
      const morning =
        data.morning.apparent;
    
    
      const daytime =
        data.daytime.apparent;
    
    
      const evening =
        data.evening.apparent;
    
    
      if (
        daytime >= 25 &&
        evening < 21
      ) {
    
        result.push(
          "日中は半袖でOK。夜は薄手の羽織りがあると安心です。"
        );
    
    
      } else if (
        daytime >= 21 &&
        daytime < 25
      ) {
    
        result.push(
          "日中は半袖でもOK。薄手の羽織りがあると安心です。"
        );
    
    
      } else if (
        morning < 17 &&
        daytime >= 22
      ) {
    
        result.push(
          "朝はひんやり。脱ぎ着しやすい服装がおすすめです。"
        );
    
    
      } else if (
        morning >= 25 &&
        daytime >= 25 &&
        evening >= 25
      ) {
    
        result.push(
          "一日を通して半袖で快適に過ごせそうです。"
        );
    
    
      } else {
    
        result.push(
          data.daytime.outfit.detail +
          "。"
        );
      }
    
    
      if (
        data.tempDiff >= 10
      ) {
    
        result.push(
          "朝晩との寒暖差に注意。"
        );
      }
    
    
      if (
        data.rain >= 70
      ) {
    
        result.push(
          "傘を忘れずに。"
        );
    
    
      } else if (
        data.rain >= 40
      ) {
    
        result.push(
          "折りたたみ傘があると安心です。"
        );
      }
    
    
      if (
        data.wind >= 9.7
      ) {
    
        result.push(
          "風が強くなるので体感温度にも注意。"
        );
      }
    
    
      return result.join(" ");
    }
    
    
    // =====================================================
    // Widget
    // =====================================================
    
    function createWidget(
      data,
      location
    ) {
    
      const widget =
        new ListWidget();
    
    
      widget.backgroundColor =
        BG_COLOR;
    
    
      const family =
        config.widgetFamily ||
        "small";
    
    
      if (
        family === "medium"
      ) {
    
        createMedium(
          widget,
          data,
          location
        );
    
    
      } else if (
        family === "large"
      ) {
    
        createLarge(
          widget,
          data,
          location
        );
    
    
      } else {
    
        createSmall(
          widget,
          data,
          location
        );
      }
    
    
      return widget;
    }
    
    
    // =====================================================
    // 小サイズ
    // =====================================================
    
    function createSmall(
      widget,
      data,
      location
    ) {
    
      widget.setPadding(
        14,
        14,
        13,
        14
      );
    
    
      const title =
        widget.addText(
          "今日なに着る?"
        );
    
    
      title.font =
        Font.boldSystemFont(13);
    
    
      title.textColor =
        TEXT_COLOR;
    
    
      widget.addSpacer(6);
    
    
      // ---------------------------------------------------
      // メイン服装
      // ---------------------------------------------------
    
      const outfitStack =
        widget.addStack();
    
    
      outfitStack.centerAlignContent();
    
    
      const outfitIcon =
        outfitStack.addText(
          data.daytime.outfit.icon
        );
    
    
      outfitIcon.font =
        Font.systemFont(20);
    
    
      outfitStack.addSpacer(5);
    
    
      const outfit =
        outfitStack.addText(
          data.daytime.outfit.label
        );
    
    
      outfit.font =
        Font.boldSystemFont(21);
    
    
      outfit.textColor =
        TEXT_COLOR;
    
    
      outfit.lineLimit = 1;
    
    
      outfit.minimumScaleFactor =
        0.60;
    
    
      widget.addSpacer(3);
    
    
      // ---------------------------------------------------
      // 気温
      // 体感温度で色が変化
      // ---------------------------------------------------
    
      const temp =
        widget.addText(
          Math.round(
            data.daytime.temp
          ) +
          "℃  /  体感 " +
          Math.round(
            data.daytime.apparent
          ) +
          "℃"
        );
    
    
      temp.font =
        Font.mediumSystemFont(10);
    
    
      temp.textColor =
        temperatureColor(
          data.daytime.apparent
        );
    
    
      temp.lineLimit = 1;
    
    
      widget.addSpacer(6);
    
    
      // ---------------------------------------------------
      // 最高 / 最低
      // ---------------------------------------------------
    
      const range =
        widget.addStack();
    
    
      range.centerAlignContent();
    
    
      const high =
        range.addText(
          "↑ " +
          Math.round(
            data.maxTemp
          ) +
          "℃"
        );
    
    
      high.font =
        Font.mediumSystemFont(9);
    
    
      high.textColor =
        TEXT_COLOR;
    
    
      range.addSpacer(10);
    
    
      const low =
        range.addText(
          "↓ " +
          Math.round(
            data.minTemp
          ) +
          "℃"
        );
    
    
      low.font =
        Font.mediumSystemFont(9);
    
    
      low.textColor =
        SUB_COLOR;
    
    
      widget.addSpacer(6);
    
    
      // ---------------------------------------------------
      // 服装アドバイス
      // ---------------------------------------------------
    
      const clothingAdvice =
        widget.addText(
          getSmallClothingAdvice(
            data
          )
        );
    
    
      clothingAdvice.font =
        Font.mediumSystemFont(9);
    
    
      clothingAdvice.textColor =
        SUB_COLOR;
    
    
      clothingAdvice.lineLimit = 2;
    
    
      clothingAdvice.minimumScaleFactor =
        0.65;
    
    
      // ---------------------------------------------------
      // 天気注意
      // ---------------------------------------------------
    
      const weatherAdvice =
        getSmallWeatherAdvice(
          data
        );
    
    
      if (
        weatherAdvice
      ) {
    
        widget.addSpacer(2);
    
    
        const warning =
          widget.addText(
            weatherAdvice
          );
    
    
        warning.font =
          Font.mediumSystemFont(8);
    
    
        warning.textColor =
          SUB_COLOR;
    
    
        warning.lineLimit = 1;
    
    
        warning.minimumScaleFactor =
          0.65;
      }
    
    
      widget.addSpacer();
    
    
      // ---------------------------------------------------
      // 地域
      // 📍なし
      // ---------------------------------------------------
    
      const place =
        widget.addText(
          location.city
        );
    
    
      place.font =
        Font.mediumSystemFont(8);
    
    
      place.textColor =
        DIM_COLOR;
    
    
      place.lineLimit = 1;
    
    
      // ---------------------------------------------------
      // キャッシュ表示
      // ---------------------------------------------------
    
      if (
        data.isCached
      ) {
    
        const cached =
          widget.addText(
            "前回データ"
          );
    
    
        cached.font =
          Font.systemFont(7);
    
    
        cached.textColor =
          DIM_COLOR;
      }
    }
    
    
    // =====================================================
    // 小サイズ:服装アドバイス
    // =====================================================
    
    function getSmallClothingAdvice(
      data
    ) {
    
      if (
        data.daytime.apparent >= 25 &&
        data.evening.apparent < 21
      ) {
    
        return "夜は薄手の羽織りを";
      }
    
    
      if (
        data.daytime.apparent >= 21 &&
        data.daytime.apparent < 25
      ) {
    
        return "薄手の羽織りがあると安心";
      }
    
    
      if (
        data.morning.apparent < 17 &&
        data.daytime.apparent >= 21
      ) {
    
        return "朝は羽織りがあると安心";
      }
    
    
      if (
        data.tempDiff >= 10
      ) {
    
        return "脱ぎ着しやすい服装を";
      }
    
    
      return data.daytime.outfit.detail;
    }
    
    
    // =====================================================
    // 小サイズ:天気注意
    // =====================================================
    
    function getSmallWeatherAdvice(
      data
    ) {
    
      if (
        data.rain >= 70
      ) {
    
        return "☔ 傘を忘れずに";
      }
    
    
      if (
        data.rain >= 40
      ) {
    
        return "🌂 折りたたみ傘を";
      }
    
    
      if (
        data.wind >= 9.7
      ) {
    
        return "💨 風が強め";
      }
    
    
      return "";
    }
    
    
    // =====================================================
    // 中サイズ
    // =====================================================
    
    function createMedium(
      widget,
      data,
      location
    ) {
    
      widget.setPadding(
        13,
        17,
        12,
        17
      );
    
    
      // ---------------------------------------------------
      // ヘッダー
      // ---------------------------------------------------
    
      const top =
        widget.addStack();
    
    
      top.centerAlignContent();
    
    
      const title =
        top.addText(
          "今日なに着る?"
        );
    
    
      title.font =
        Font.boldSystemFont(13);
    
    
      title.textColor =
        TEXT_COLOR;
    
    
      top.addSpacer();
    
    
      const weather =
        top.addText(
          data.weather.icon +
          " " +
          Math.round(
            data.maxTemp
          ) +
          "° / " +
          Math.round(
            data.minTemp
          ) +
          "°"
        );
    
    
      weather.font =
        Font.mediumSystemFont(11);
    
    
      weather.textColor =
        SUB_COLOR;
    
    
      widget.addSpacer(8);
    
    
      // ---------------------------------------------------
      // 朝・昼・夜
      // ---------------------------------------------------
    
      const row =
        widget.addStack();
    
    
      row.centerAlignContent();
    
    
      addPeriodCard(
        row,
        "朝",
        data.morning
      );
    
    
      row.addSpacer();
    
    
      addPeriodCard(
        row,
        "昼",
        data.daytime
      );
    
    
      row.addSpacer();
    
    
      addPeriodCard(
        row,
        "夜",
        data.evening
      );
    
    
      widget.addSpacer(8);
    
    
      // ---------------------------------------------------
      // アドバイス
      // ---------------------------------------------------
    
      const adviceBox =
        widget.addStack();
    
    
      adviceBox.backgroundColor =
        CARD_COLOR;
    
    
      adviceBox.cornerRadius = 8;
    
    
      adviceBox.setPadding(
        6,
        8,
        6,
        8
      );
    
    
      const advice =
        adviceBox.addText(
          getMediumAdvice(
            data
          )
        );
    
    
      advice.font =
        Font.mediumSystemFont(9);
    
    
      advice.textColor =
        TEXT_COLOR;
    
    
      advice.lineLimit = 2;
    
    
      advice.minimumScaleFactor =
        0.7;
    
    
      widget.addSpacer(6);
    
    
      // ---------------------------------------------------
      // 下段
      // ---------------------------------------------------
    
      const bottom =
        widget.addStack();
    
    
      bottom.centerAlignContent();
    
    
      const rain =
        bottom.addText(
          "☔ " +
          data.rain +
          "%"
        );
    
    
      rain.font =
        Font.mediumSystemFont(8);
    
    
      rain.textColor =
        SUB_COLOR;
    
    
      bottom.addSpacer();
    
    
      // 📍なし
      const place =
        bottom.addText(
          location.city
        );
    
    
      place.font =
        Font.mediumSystemFont(8);
    
    
      place.textColor =
        DIM_COLOR;
    }
    
    
    // =====================================================
    // 中サイズ:時間帯
    // =====================================================
    
    function addPeriodCard(
      stack,
      titleText,
      data
    ) {
    
      const item =
        stack.addStack();
    
    
      item.layoutVertically();
    
    
      item.centerAlignContent();
    
    
      // ---------------------------------------------------
      // 朝 / 昼 / 夜
      // ---------------------------------------------------
    
      const title =
        item.addText(
          titleText
        );
    
    
      title.font =
        Font.boldSystemFont(9);
    
    
      title.textColor =
        periodColor(
          titleText
        );
    
    
      title.centerAlignText();
    
    
      item.addSpacer(3);
    
    
      // ---------------------------------------------------
      // 服装
      // ---------------------------------------------------
    
      const outfit =
        item.addText(
          data.outfit.icon +
          " " +
          data.outfit.shortLabel
        );
    
    
      outfit.font =
        Font.boldSystemFont(11);
    
    
      outfit.textColor =
        TEXT_COLOR;
    
    
      outfit.centerAlignText();
    
    
      outfit.lineLimit = 1;
    
    
      outfit.minimumScaleFactor =
        0.65;
    
    
      item.addSpacer(2);
    
    
      // ---------------------------------------------------
      // 気温
      // ---------------------------------------------------
    
      const temps =
        item.addText(
          Math.round(
            data.temp
          ) +
          "℃  体感" +
          Math.round(
            data.apparent
          ) +
          "℃"
        );
    
    
      temps.font =
        Font.mediumSystemFont(8);
    
    
      temps.textColor =
        temperatureColor(
          data.apparent
        );
    
    
      temps.centerAlignText();
    }
    
    
    // =====================================================
    // 中サイズアドバイス
    // =====================================================
    
    function getMediumAdvice(
      data
    ) {
    
      let text;
    
    
      if (
        data.daytime.apparent >= 25 &&
        data.evening.apparent < 21
      ) {
    
        text =
          "日中は半袖でOK。帰りが遅いなら薄手の羽織りを。";
    
    
      } else if (
        data.daytime.apparent >= 21 &&
        data.daytime.apparent < 25
      ) {
    
        text =
          "半袖でもOK。薄手の羽織りがあると安心。";
    
    
      } else if (
        data.tempDiff >= 10
      ) {
    
        text =
          "寒暖差あり。脱ぎ着しやすい服装がおすすめ。";
    
    
      } else {
    
        text =
          data.daytime.outfit.detail;
      }
    
    
      if (
        data.rain >= 70
      ) {
    
        text +=
          " 傘を忘れずに。";
    
    
      } else if (
        data.rain >= 40
      ) {
    
        text +=
          " 折りたたみ傘も。";
      }
    
    
      return text;
    }
    
    
    // =====================================================
    // 大サイズ
    // =====================================================
    
    function createLarge(
      widget,
      data,
      location
    ) {
    
      widget.setPadding(
        16,
        18,
        14,
        18
      );
    
    
      // ---------------------------------------------------
      // ヘッダー
      // ---------------------------------------------------
    
      const top =
        widget.addStack();
    
    
      top.centerAlignContent();
    
    
      const title =
        top.addText(
          "今日なに着る?"
        );
    
    
      title.font =
        Font.boldSystemFont(15);
    
    
      title.textColor =
        TEXT_COLOR;
    
    
      top.addSpacer();
    
    
      // 📍なし
      const place =
        top.addText(
          location.prefecture +
          location.city
        );
    
    
      place.font =
        Font.mediumSystemFont(9);
    
    
      place.textColor =
        DIM_COLOR;
    
    
      widget.addSpacer(5);
    
    
      // ---------------------------------------------------
      // 天気
      // ---------------------------------------------------
    
      const weather =
        widget.addText(
          data.weather.icon +
          " " +
          data.weather.label +
          " 最高 " +
          Math.round(
            data.maxTemp
          ) +
          "℃ 最低 " +
          Math.round(
            data.minTemp
          ) +
          "℃"
        );
    
    
      weather.font =
        Font.mediumSystemFont(11);
    
    
      weather.textColor =
        SUB_COLOR;
    
    
      widget.addSpacer(12);
    
    
      // ---------------------------------------------------
      // 今日のおすすめ
      // ---------------------------------------------------
    
      const main =
        widget.addStack();
    
    
      main.centerAlignContent();
    
    
      const mainIcon =
        main.addText(
          data.daytime.outfit.icon
        );
    
    
      mainIcon.font =
        Font.systemFont(32);
    
    
      main.addSpacer(10);
    
    
      const mainText =
        main.addStack();
    
    
      mainText.layoutVertically();
    
    
      const mainLabel =
        mainText.addText(
          data.daytime.outfit.label
        );
    
    
      mainLabel.font =
        Font.boldSystemFont(23);
    
    
      mainLabel.textColor =
        TEXT_COLOR;
    
    
      mainText.addSpacer(2);
    
    
      const mainDetail =
        mainText.addText(
          data.daytime.outfit.detail
        );
    
    
      mainDetail.font =
        Font.mediumSystemFont(10);
    
    
      mainDetail.textColor =
        SUB_COLOR;
    
    
      mainDetail.lineLimit = 2;
    
    
      widget.addSpacer(13);
    
    
      // ---------------------------------------------------
      // 朝 / 昼 / 夜
      // ---------------------------------------------------
    
      addLargePeriod(
        widget,
        "朝",
        data.morning
      );
    
    
      widget.addSpacer(7);
    
    
      addLargePeriod(
        widget,
        "昼",
        data.daytime
      );
    
    
      widget.addSpacer(7);
    
    
      addLargePeriod(
        widget,
        "夜",
        data.evening
      );
    
    
      widget.addSpacer(13);
    
    
      // ---------------------------------------------------
      // 今日のポイント
      // ---------------------------------------------------
    
      const pointTitle =
        widget.addText(
          "今日のポイント"
        );
    
    
      pointTitle.font =
        Font.boldSystemFont(10);
    
    
      pointTitle.textColor =
        SUB_COLOR;
    
    
      widget.addSpacer(5);
    
    
      const adviceBox =
        widget.addStack();
    
    
      adviceBox.backgroundColor =
        CARD_COLOR;
    
    
      adviceBox.cornerRadius = 10;
    
    
      adviceBox.setPadding(
        9,
        11,
        9,
        11
      );
    
    
      const advice =
        adviceBox.addText(
          data.advice
        );
    
    
      advice.font =
        Font.mediumSystemFont(10);
    
    
      advice.textColor =
        TEXT_COLOR;
    
    
      advice.lineLimit = 3;
    
    
      advice.minimumScaleFactor =
        0.75;
    
    
      widget.addSpacer(12);
    
    
      // ---------------------------------------------------
      // 今日のコンディション
      // ---------------------------------------------------
    
      const conditionTitle =
        widget.addText(
          "今日のコンディション"
        );
    
    
      conditionTitle.font =
        Font.boldSystemFont(10);
    
    
      conditionTitle.textColor =
        SUB_COLOR;
    
    
      widget.addSpacer(6);
    
    
      const conditions =
        widget.addStack();
    
    
      conditions.centerAlignContent();
    
    
      addCondition(
        conditions,
        "☔",
        "降水",
        data.rain + "%"
      );
    
    
      conditions.addSpacer();
    
    
      addCondition(
        conditions,
        "💨",
        "風",
        formatWind(
          data.wind
        )
      );
    
    
      conditions.addSpacer();
    
    
      addCondition(
        conditions,
        "↕️",
        "寒暖差",
        Math.round(
          data.tempDiff
        ) +
        "℃"
      );
    
    
      // ---------------------------------------------------
      // キャッシュ表示
      // ---------------------------------------------------
    
      if (
        data.isCached
      ) {
    
        widget.addSpacer(8);
    
    
        const cached =
          widget.addText(
            "前回取得した天気データを表示しています"
          );
    
    
        cached.font =
          Font.systemFont(8);
    
    
        cached.textColor =
          DIM_COLOR;
    
    
        cached.centerAlignText();
      }
    }
    
    
    // =====================================================
    // 大サイズ:時間帯
    // =====================================================
    
    function addLargePeriod(
      widget,
      titleText,
      data
    ) {
    
      const row =
        widget.addStack();
    
    
      row.centerAlignContent();
    
    
      // ---------------------------------------------------
      // 朝 / 昼 / 夜
      // ---------------------------------------------------
    
      const time =
        row.addText(
          titleText
        );
    
    
      time.font =
        Font.boldSystemFont(11);
    
    
      time.textColor =
        periodColor(
          titleText
        );
    
    
      time.lineLimit = 1;
    
    
      row.addSpacer(18);
    
    
      // ---------------------------------------------------
      // 服装
      // ---------------------------------------------------
    
      const outfit =
        row.addText(
          data.outfit.icon +
          " " +
          data.outfit.label
        );
    
    
      outfit.font =
        Font.boldSystemFont(12);
    
    
      outfit.textColor =
        TEXT_COLOR;
    
    
      outfit.lineLimit = 1;
    
    
      outfit.minimumScaleFactor =
        0.75;
    
    
      row.addSpacer();
    
    
      // ---------------------------------------------------
      // 天気
      // ---------------------------------------------------
    
      const weather =
        row.addText(
          data.weather.icon
        );
    
    
      weather.font =
        Font.systemFont(12);
    
    
      row.addSpacer(7);
    
    
      // ---------------------------------------------------
      // 気温
      // ---------------------------------------------------
    
      const temp =
        row.addText(
          Math.round(
            data.temp
          ) +
          "℃"
        );
    
    
      temp.font =
        Font.boldSystemFont(13);
    
    
      temp.textColor =
        temperatureColor(
          data.apparent
        );
    
    
      row.addSpacer(7);
    
    
      const apparent =
        row.addText(
          "体感 " +
          Math.round(
            data.apparent
          ) +
          "℃"
        );
    
    
      apparent.font =
        Font.mediumSystemFont(9);
    
    
      apparent.textColor =
        temperatureColor(
          data.apparent
        );
    }
    
    
    // =====================================================
    // コンディション
    // =====================================================
    
    function addCondition(
      stack,
      icon,
      label,
      value
    ) {
    
      const item =
        stack.addStack();
    
    
      item.layoutVertically();
    
    
      item.centerAlignContent();
    
    
      const top =
        item.addText(
          icon +
          " " +
          label
        );
    
    
      top.font =
        Font.mediumSystemFont(8);
    
    
      top.textColor =
        DIM_COLOR;
    
    
      top.centerAlignText();
    
    
      item.addSpacer(2);
    
    
      const bottom =
        item.addText(
          value
        );
    
    
      bottom.font =
        Font.boldSystemFont(11);
    
    
      bottom.textColor =
        TEXT_COLOR;
    
    
      bottom.centerAlignText();
    }
    
    
    // =====================================================
    // 天気の重要度
    // =====================================================
    
    function weatherSeverity(
      code
    ) {
    
      if (
        code >= 95
      ) {
        return 7;
      }
    
    
      if (
        code >= 71 &&
        code <= 77
      ) {
        return 6;
      }
    
    
      if (
        code >= 85 &&
        code <= 86
      ) {
        return 6;
      }
    
    
      if (
        code >= 61 &&
        code <= 67
      ) {
        return 5;
      }
    
    
      if (
        code >= 80 &&
        code <= 82
      ) {
        return 5;
      }
    
    
      if (
        code >= 51 &&
        code <= 57
      ) {
        return 4;
      }
    
    
      if (
        code === 45 ||
        code === 48
      ) {
        return 3;
      }
    
    
      if (
        code === 3
      ) {
        return 2;
      }
    
    
      if (
        code === 1 ||
        code === 2
      ) {
        return 1;
      }
    
    
      return 0;
    }
    
    
    // =====================================================
    // 天気情報
    // =====================================================
    
    function getWeatherInfo(
      code
    ) {
    
      if (
        code === 0
      ) {
    
        return {
          icon: "☀️",
          label: "晴れ"
        };
      }
    
    
      if (
        code === 1 ||
        code === 2
      ) {
    
        return {
          icon: "🌤️",
          label: "晴れ時々曇り"
        };
      }
    
    
      if (
        code === 3
      ) {
    
        return {
          icon: "☁️",
          label: "曇り"
        };
      }
    
    
      if (
        code === 45 ||
        code === 48
      ) {
    
        return {
          icon: "🌫️",
          label: "霧"
        };
      }
    
    
      if (
        code >= 51 &&
        code <= 57
      ) {
    
        return {
          icon: "🌦️",
          label: "小雨"
        };
      }
    
    
      if (
        code >= 61 &&
        code <= 67
      ) {
    
        return {
          icon: "🌧️",
          label: "雨"
        };
      }
    
    
      if (
        code >= 71 &&
        code <= 77
      ) {
    
        return {
          icon: "🌨️",
          label: "雪"
        };
      }
    
    
      if (
        code >= 80 &&
        code <= 82
      ) {
    
        return {
          icon: "🌦️",
          label: "にわか雨"
        };
      }
    
    
      if (
        code >= 85 &&
        code <= 86
      ) {
    
        return {
          icon: "🌨️",
          label: "にわか雪"
        };
      }
    
    
      if (
        code >= 95
      ) {
    
        return {
          icon: "⛈️",
          label: "雷雨"
        };
      }
    
    
      return {
        icon: "☁️",
        label: "曇り"
      };
    }
    
    
    // =====================================================
    // 風速
    // Open-Meteoからm/sで取得
    // =====================================================
    
    function formatWind(
      speed
    ) {
    
      return (
        Number(speed).toFixed(1) +
        "m/s"
      );
    }
    
    
    // =====================================================
    // エラーWidget
    // =====================================================
    
    function createErrorWidget(
      titleText,
      message
    ) {
    
      const widget =
        new ListWidget();
    
    
      widget.backgroundColor =
        BG_COLOR;
    
    
      widget.setPadding(
        16,
        16,
        16,
        16
      );
    
    
      const title =
        widget.addText(
          "今日なに着る?"
        );
    
    
      title.font =
        Font.boldSystemFont(14);
    
    
      title.textColor =
        TEXT_COLOR;
    
    
      widget.addSpacer(10);
    
    
      const text =
        widget.addText(
          titleText
        );
    
    
      text.font =
        Font.boldSystemFont(13);
    
    
      text.textColor =
        TEXT_COLOR;
    
    
      widget.addSpacer(5);
    
    
      const sub =
        widget.addText(
          message
        );
    
    
      sub.font =
        Font.systemFont(9);
    
    
      sub.textColor =
        SUB_COLOR;
    
    
      sub.lineLimit = 4;
    
    
      return widget;
    }
    
    
    // =====================================================
    // 実行
    // =====================================================
    
    await main();
  3. ホーム画面に追加して設定する

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

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

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

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

服装はどうやって決めている?

単純に最高気温だけを見るのではなく、体感温度を中心に、時間帯・風・雨・一日の寒暖差も考慮して服装を判定しています。
例えば日中は半袖で過ごせる気温でも、夜に気温が下がる日は「半袖+羽織り」のように表示します。
雨や風が強い日は、傘や風を通しにくい上着などのアドバイスも追加されます。

毎朝天気予報を見て、

「で、今日は何着たらいいん?」

となる人向けのウィジェットです。
ホーム画面を見るだけで、その日の服装をサッと決められます。

動作確認・使用結果

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

表示・使用時のポイント

郵便番号の地域の予報から、体感温度や天気に合わせた服装の目安を作ります。朝・昼・夜で気温が違う日は、時間帯ごとの案内を確認できます。

更新履歴

1.0.1

  • 天気アイコンの表示を改善
  • にわか雨の天気判定を修正
  • 風速データを m/s に統一し、判定精度を改善
  • 郵便番号から取得した位置情報をキャッシュするように変更
  • 天気データをキャッシュし、通信エラー時は前回のデータを表示するように改善
  • ウィジェットの更新間隔を約60分に設定
  • その他、内部処理と安定性を改善

1.0.0

  • 初回リリース

よくあるエラーと対処

確認すること
表示されない場合は、ウィジェット編集画面の「Script」に保存したスクリプトが選ばれているかも確認してください。コードを貼り直すときは全文を置き換えます。
自分の地域ではない
Parameterの郵便番号を確認してください。
昨日の案内が残る
通信や更新の都合で保存済み予報が使われる場合があります。Scriptableで実行してください。
提案の服装では寒い・暑い
個人差や室内環境は反映していません。自分の体感に合わせて調整してください。

注意事項・制限

  • 服装は予報を使った一般的な目安です。
  • 移動先や室内の空調、日なた・日陰など局所的な条件はすべて反映していません。
  • ホーム画面の更新タイミングはiOSが管理するため、操作やデータの変更がすぐに反映されない場合があります。

データ提供・参考資料

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

SHARE

この記事をシェア