
世界や日本で、iPhoneがどれくらい使われているのかをチェックできるウィジェットです。
日本と世界のiPhoneシェアを大きく表示し、主要国のシェアランキングも確認できます。
データは Statcounter Global Stats の「Mobile Vendor Market Share」を使用しています。
日本はランキング内でもハイライト表示されるので、世界の主要国と比べてどのくらいiPhoneのシェアが高いのかがひと目で分かります。
※ここで表示する数値は「人口に対するiPhone所有率」ではなく、Statcounterが集計しているモバイル端末のベンダーシェアです。
ウィジェットの表示内容
Statcounterのモバイル端末ベンダー統計からAppleの値を取得します。
サイズごとの表示
- 小:日本と世界のAppleのシェアを表示します。
- 中:主要国のランキングを比較できます。
- 大:主要7か国を広いレイアウトで表示します。
設定手順
上から順に進めてください。コードの保存と設定のコピーに必要な操作欄は、それぞれの手順の中にあります。
- Scriptableを用意する
App StoreでScriptableを開き、iPhoneにインストールします。すでに入っている場合は次へ進んでください。
- コードをコピーして保存する
下のコード欄の「コピー」を押します。Scriptableへ移動し、「+」から新しいスクリプトを作ってコード全文を貼り付け、世界のiPhoneシェア率などの名前で保存してください。
保存したらこのページに戻り、次の手順へ進みます。
// Variables used by Scriptable. // These must be at the very top of the file. Do not edit. // icon-color: red; icon-glyph: mobile-alt; // Version: 1.0.0 // ===================================================== // 世界のiPhoneシェア - Scriptable Widget // // データ:Statcounter Global Stats // Mobile Vendor Market Share 公式ページ // ・月ごとの値を端末内へ保存 // ・保存済み前月データから前月比を計算 // ・日本をランキング内でハイライト // // 小:日本を大きく+世界 // 中:日本 / 世界を左右に大きく+ランキングTOP4 // 大:日本 / 世界を左右に大きく+主要7か国 // // ※「人口に対するiPhone所有率」ではありません。 // Statcounterが観測したMobile Vendor Market Shareです。 // ===================================================== // ===================================================== // 設定 // ===================================================== const CONFIG = { BASE: "https://gs.statcounter.com/vendor-market-share/mobile", HISTORY_FILE: "iphone_share_history_v1.json", CACHE_FILE: "iphone_share_html_cache_v1.json", CACHE_HOURS: 12, COUNTRIES: [ { key: "JP", name: "日本", flag: "🇯🇵", slug: "japan" }, { key: "US", name: "アメリカ", flag: "🇺🇸", slug: "united-states-of-america" }, { key: "CA", name: "カナダ", flag: "🇨🇦", slug: "canada" }, { key: "GB", name: "イギリス", flag: "🇬🇧", slug: "united-kingdom" }, { key: "AU", name: "豪州", flag: "🇦🇺", slug: "australia" }, { key: "CN", name: "中国", flag: "🇨🇳", slug: "china" }, { key: "IN", name: "インド", flag: "🇮🇳", slug: "india" } ] }; // ===================================================== // カラー // ===================================================== const COLORS = { bg: new Color("#0B0B0D"), text: Color.white(), sub: new Color("#8E8E93"), up: new Color("#30D158"), down: new Color("#FF453A"), flat: new Color("#8E8E93"), japanBg: new Color("#FFFFFF", 0.10) }; // ===================================================== // FileManager // ===================================================== const fm = FileManager.local(); const docs = fm.documentsDirectory(); const historyPath = fm.joinPath( docs, CONFIG.HISTORY_FILE ); const cachePath = fm.joinPath( docs, CONFIG.CACHE_FILE ); // ===================================================== // JSON // ===================================================== function readJSON(path) { if (!fm.fileExists(path)) { return null; } try { return JSON.parse( fm.readString(path) ); } catch (e) { return null; } } function writeJSON( path, data ) { try { fm.writeString( path, JSON.stringify( data, null, 2 ) ); } catch (e) { console.log( "保存エラー: " + e ); } } // ===================================================== // HTML → テキスト // ===================================================== function decodeHTML(text) { return text .replace(/ /gi, " ") .replace(/&/gi, "&") .replace(/"/gi, "\"") .replace(/'/gi, "'") .replace(/</gi, "<") .replace(/>/gi, ">"); } function htmlToText(html) { return decodeHTML( html .replace( /<script[\s\S]*?<\/script>/gi, " " ) .replace( /<style[\s\S]*?<\/style>/gi, " " ) .replace( /<[^>]+>/g, " " ) .replace( /\s+/g, " " ) ).trim(); } // ===================================================== // Statcounter公式ページ解析 // ===================================================== function parseStatcounter(html) { const text = htmlToText(html); const months = { January: "01", February: "02", March: "03", April: "04", May: "05", June: "06", July: "07", August: "08", September: "09", October: "10", November: "11", December: "12" }; let period = null; const periodMatch = text.match( /Mobile Vendor Market Share.*?-\s*(January|February|March|April|May|June|July|August|September|October|November|December)\s+(\d{4})/i ); if (periodMatch) { const monthName = periodMatch[1] .charAt(0) .toUpperCase() + periodMatch[1] .slice(1) .toLowerCase(); period = periodMatch[2] + "-" + months[monthName]; } const appleMatch = text.match( /\bApple\s+([0-9]+(?:\.[0-9]+)?)\s*%/i ); if (!appleMatch) { console.log( "Apple値を取得できません" ); return null; } const value = parseFloat( appleMatch[1] ); if ( !Number.isFinite(value) || value < 0 || value > 100 ) { return null; } return { value, period }; } // ===================================================== // 公式ページ取得 // ===================================================== async function fetchPage(url) { const req = new Request(url); req.timeoutInterval = 20; req.headers = { "User-Agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X) AppleWebKit/605.1.15", "Accept": "text/html,application/xhtml+xml" }; return await req.loadString(); } // ===================================================== // 地域取得 // ===================================================== async function fetchRegion( key, slug ) { const url = slug ? CONFIG.BASE + "/" + slug : CONFIG.BASE; try { const html = await fetchPage(url); const parsed = parseStatcounter(html); if (!parsed) { throw new Error( "解析失敗" ); } console.log( key + " : " + parsed.value + "% / " + parsed.period ); return parsed; } catch (e) { console.log( key + " ERROR : " + e ); return null; } } // ===================================================== // 全地域取得 // ===================================================== async function fetchAll() { const targets = [ { key: "WORLD", slug: null }, ...CONFIG.COUNTRIES.map( country => ({ key: country.key, slug: country.slug }) ) ]; const results = await Promise.all( targets.map( target => fetchRegion( target.key, target.slug ) ) ); const raw = {}; targets.forEach( (target, index) => { if (results[index]) { raw[target.key] = results[index]; } } ); return raw; } // ===================================================== // キャッシュ // ===================================================== function loadCache() { const cache = readJSON( cachePath ); if (!cache) { return null; } const age = Date.now() - cache.savedAt; const maxAge = CONFIG.CACHE_HOURS * 60 * 60 * 1000; if (age > maxAge) { return null; } return cache.data; } function loadOldCache() { const cache = readJSON( cachePath ); return cache?.data || null; } function saveCache(data) { writeJSON( cachePath, { savedAt: Date.now(), data } ); } // ===================================================== // 履歴 // ===================================================== function loadHistory() { return ( readJSON( historyPath ) || {} ); } function saveHistory(history) { writeJSON( historyPath, history ); } // ===================================================== // 前月 // ===================================================== function previousPeriod( period ) { if (!period) { return null; } const parts = period.split("-"); let year = Number(parts[0]); let month = Number(parts[1]); month--; if (month === 0) { month = 12; year--; } return ( year + "-" + String(month) .padStart(2, "0") ); } // ===================================================== // 履歴保存 // ===================================================== function updateHistory( history, raw ) { for ( const [key, item] of Object.entries(raw) ) { if ( !item || !item.period || item.value === null ) { continue; } if (!history[item.period]) { history[item.period] = {}; } history[item.period][key] = item.value; } const periods = Object.keys(history) .sort(); while ( periods.length > 24 ) { const oldest = periods.shift(); delete history[ oldest ]; } saveHistory(history); return history; } // ===================================================== // 表示データ生成 // ===================================================== function buildData( raw, history ) { const period = raw.JP?.period || raw.WORLD?.period || Object .values(raw) .find( item => item?.period ) ?.period || null; const prev = previousPeriod( period ); const result = { period, world: null, countries: {} }; function makeItem(key) { const current = raw[key]?.value; if ( current === null || current === undefined ) { return { value: null, previous: null, diff: null }; } const previous = prev && history[prev] && history[prev][key] !== undefined ? history[prev][key] : null; return { value: current, previous, diff: previous !== null ? current - previous : null }; } result.world = makeItem( "WORLD" ); CONFIG.COUNTRIES.forEach( country => { result.countries[ country.key ] = makeItem( country.key ); } ); return result; } // ===================================================== // データ取得 // ===================================================== async function getData() { let raw = loadCache(); if (!raw) { try { raw = await fetchAll(); if ( !raw || Object.keys(raw) .length === 0 ) { throw new Error( "データなし" ); } saveCache(raw); } catch (e) { console.log( "取得失敗: " + e ); raw = loadOldCache(); if (!raw) { raw = {}; } } } let history = loadHistory(); history = updateHistory( history, raw ); return buildData( raw, history ); } // ===================================================== // 表示フォーマット // ===================================================== function pct(value) { if ( value === null || value === undefined ) { return "--.-%"; } return ( Number(value) .toFixed(1) + "%" ); } function diffText(diff) { if ( diff === null || diff === undefined ) { return "—"; } if ( Math.abs(diff) < 0.05 ) { return "→ 0.0pt"; } if (diff > 0) { return ( "↑ +" + diff.toFixed(1) + "pt" ); } return ( "↓ " + diff.toFixed(1) + "pt" ); } function diffColor(diff) { if ( diff === null || diff === undefined ) { return COLORS.flat; } if (diff > 0.05) { return COLORS.up; } if (diff < -0.05) { return COLORS.down; } return COLORS.flat; } function displayPeriod( period ) { if (!period) { return ""; } return period.replace( "-", "." ); } // ===================================================== // タイトル // ===================================================== function addTitle(widget) { const row = widget.addStack(); row.centerAlignContent(); const symbol = SFSymbol.named( "apple.logo" ); if (symbol) { symbol.applyFont( Font.systemFont(14) ); const image = row.addImage( symbol.image ); image.imageSize = new Size( 14, 14 ); image.tintColor = COLORS.text; row.addSpacer(7); } const title = row.addText( "iPhoneシェア率" ); title.textColor = COLORS.text; title.font = Font.boldSystemFont(14); } // ===================================================== // 前月比 // ===================================================== function addDiff( parent, diff, size = 10 ) { const text = parent.addText( diffText(diff) ); text.textColor = diffColor(diff); text.font = Font.mediumSystemFont( size ); text.lineLimit = 1; } // ===================================================== // ランキング // ===================================================== function getRanking(data) { return CONFIG.COUNTRIES .map( country => ({ country, data: data.countries[ country.key ] }) ) .filter( item => item.data?.value !== null && item.data?.value !== undefined ) .sort( (a, b) => b.data.value - a.data.value ); } // ===================================================== // ランキング行 // // 日本は背景だけハイライト。 // paddingを日本だけに入れないことで // 他の行と文字位置を完全に統一。 // ===================================================== function addRankingRow( parent, rank, country, data, fontSize = 11 ) { const isJapan = country.key === "JP"; const row = parent.addStack(); row.centerAlignContent(); // --------------------------------------------- // 日本:背景のみハイライト // --------------------------------------------- if (isJapan) { row.backgroundColor = COLORS.japanBg; row.cornerRadius = 6; } // --------------------------------------------- // 順位 // --------------------------------------------- const rankBox = row.addStack(); rankBox.size = new Size( 16, 0 ); const rankText = rankBox.addText( String(rank) ); rankText.textColor = isJapan ? COLORS.text : COLORS.sub; rankText.font = isJapan ? Font.boldSystemFont(9) : Font.mediumSystemFont(9); rankText.rightAlignText(); row.addSpacer(5); // --------------------------------------------- // 国旗 // --------------------------------------------- const flagBox = row.addStack(); flagBox.size = new Size( 20, 0 ); const flag = flagBox.addText( country.flag ); flag.font = Font.systemFont( fontSize ); row.addSpacer(5); // --------------------------------------------- // 国名 // --------------------------------------------- const name = row.addText( country.name ); name.textColor = COLORS.text; name.font = isJapan ? Font.boldSystemFont( fontSize ) : Font.systemFont( fontSize ); // --------------------------------------------- // シェア // --------------------------------------------- row.addSpacer(); const value = row.addText( pct( data?.value ) ); value.textColor = COLORS.text; value.font = Font.boldSystemFont( fontSize ); row.addSpacer(8); // --------------------------------------------- // 前月比 // --------------------------------------------- addDiff( row, data?.diff, fontSize - 2 ); } // ===================================================== // 日本 / 世界 HERO // ===================================================== function addHeroArea( widget, data, large = false ) { const hero = widget.addStack(); // --------------------------------------------- // 日本 // --------------------------------------------- const left = hero.addStack(); left.layoutVertically(); left.size = new Size( large ? 145 : 135, 0 ); const jpLabel = left.addText( "🇯🇵 日本" ); jpLabel.textColor = COLORS.sub; jpLabel.font = Font.mediumSystemFont( large ? 11 : 10 ); left.addSpacer(2); const jpValue = left.addText( pct( data.countries.JP?.value ) ); jpValue.textColor = COLORS.text; jpValue.font = Font.boldSystemFont( large ? 28 : 22 ); addDiff( left, data.countries.JP?.diff, large ? 10 : 8 ); hero.addSpacer(); // --------------------------------------------- // 世界 // --------------------------------------------- const right = hero.addStack(); right.layoutVertically(); const worldLabel = right.addText( "🌍 世界" ); worldLabel.textColor = COLORS.sub; worldLabel.font = Font.mediumSystemFont( large ? 11 : 10 ); right.addSpacer(2); const worldValue = right.addText( pct( data.world?.value ) ); worldValue.textColor = COLORS.text; worldValue.font = Font.boldSystemFont( large ? 28 : 22 ); addDiff( right, data.world?.diff, large ? 10 : 8 ); } // ===================================================== // 小サイズ // ===================================================== function createSmall(data) { const w = new ListWidget(); w.backgroundColor = COLORS.bg; w.setPadding( 14, 14, 12, 14 ); addTitle(w); w.addSpacer(); // --------------------------------------------- // 日本 // --------------------------------------------- const label = w.addText( "🇯🇵 日本" ); label.textColor = COLORS.sub; label.font = Font.mediumSystemFont(11); w.addSpacer(1); const value = w.addText( pct( data.countries.JP?.value ) ); value.textColor = COLORS.text; value.font = Font.boldSystemFont(28); value.minimumScaleFactor = 0.7; addDiff( w, data.countries.JP?.diff, 9 ); w.addSpacer(); // --------------------------------------------- // 世界 / 日付 // --------------------------------------------- const bottom = w.addStack(); bottom.centerAlignContent(); const world = bottom.addText( "🌍 " + pct( data.world?.value ) ); world.textColor = COLORS.sub; world.font = Font.mediumSystemFont(9); bottom.addSpacer(4); addDiff( bottom, data.world?.diff, 8 ); bottom.addSpacer(); const date = bottom.addText( displayPeriod( data.period ) ); date.textColor = COLORS.sub; date.font = Font.systemFont(8); return w; } // ===================================================== // 中サイズ // ===================================================== function createMedium(data) { const w = new ListWidget(); w.backgroundColor = COLORS.bg; w.setPadding( 14, 16, 11, 16 ); addTitle(w); w.addSpacer(7); // 日本 / 世界 addHeroArea( w, data, false ); w.addSpacer(8); // --------------------------------------------- // ランキング TOP4 // --------------------------------------------- const ranking = getRanking(data) .slice(0, 4); ranking.forEach( (item, index) => { addRankingRow( w, index + 1, item.country, item.data, 10 ); if ( index < ranking.length - 1 ) { w.addSpacer(3); } } ); w.addSpacer(); // --------------------------------------------- // 日付 // --------------------------------------------- const date = w.addText( displayPeriod( data.period ) ); date.textColor = COLORS.sub; date.font = Font.systemFont(8); date.rightAlignText(); return w; } // ===================================================== // 大サイズ // ===================================================== function createLarge(data) { const w = new ListWidget(); w.backgroundColor = COLORS.bg; w.setPadding( 18, 18, 14, 18 ); addTitle(w); w.addSpacer(14); // --------------------------------------------- // 日本 / 世界 // --------------------------------------------- addHeroArea( w, data, true ); w.addSpacer(20); // --------------------------------------------- // ランキングタイトル // --------------------------------------------- const heading = w.addText( "主要国ランキング" ); heading.textColor = COLORS.sub; heading.font = Font.mediumSystemFont(10); w.addSpacer(8); // --------------------------------------------- // 主要7か国ランキング // --------------------------------------------- const ranking = getRanking(data); ranking.forEach( (item, index) => { addRankingRow( w, index + 1, item.country, item.data, 12 ); if ( index < ranking.length - 1 ) { w.addSpacer(6); } } ); w.addSpacer(); // --------------------------------------------- // フッター // --------------------------------------------- const footer = w.addStack(); const source = footer.addText( "Statcounter Global Stats" ); source.textColor = COLORS.sub; source.font = Font.systemFont(8); footer.addSpacer(); const date = footer.addText( displayPeriod( data.period ) ); date.textColor = COLORS.sub; date.font = Font.systemFont(8); return w; } // ===================================================== // 実行 // ===================================================== const data = await getData(); let widget; if ( config.widgetFamily === "small" ) { widget = createSmall(data); } else if ( config.widgetFamily === "large" ) { widget = createLarge(data); } else { widget = createMedium(data); } // ===================================================== // 更新 // ===================================================== widget.refreshAfterDate = new Date( Date.now() + 6 * 60 * 60 * 1000 ); if (config.runsInWidget) { Script.setWidget(widget); } else { await widget.presentMedium(); } Script.complete(); - ホーム画面に追加して設定する
iPhoneのホーム画面を長押しし、ウィジェット追加画面からScriptableを選びます。この記事の対応サイズの説明に合わせてサイズを選んで追加してください。
追加したウィジェットを長押しして「ウィジェットを編集」を開き、「Script」に保存したスクリプトを選びます。
「Parameter」は空欄で構いません。
編集画面を閉じて表示を確認します。ホーム画面への反映には少し時間がかかる場合があります。
前月比について
シェアデータは月ごとに端末内へ保存されます。
前月のデータが保存されている場合は、
↑ +1.2pt:前月より上昇↓ -0.8pt:前月より低下→ 0.0pt:ほぼ変化なし
のように前月比を表示します。
初めて使用する月は前月データがないため「—」と表示されます。
翌月以降は、保存されたデータをもとに自動で前月比が表示されます。
動作確認・使用結果
| 確認した端末 | iPhone 17 ProMAX |
|---|---|
| iOS | iOS27 |
| Scriptable | 1.7.19 |
| 確認したサイズ | 小・中・大 |
| バージョン | 1.0.0 |
| 動作確認日 | 2026-10-02 |
表示・使用時のポイント
Statcounterのモバイル端末ベンダー統計からAppleの値を取得します。前月比は比較用の保存データがあるときに表示します。
更新履歴
1.0.0
- 初回リリース
よくあるエラーと対処
- 確認すること
- 表示されない場合は、ウィジェット編集画面の「Script」に保存したスクリプトが選ばれているかも確認してください。コードを貼り直すときは全文を置き換えます。
- 前月比が「—」になる
- 比較用の前月データがまだ保存されていない場合があります。
- 値が毎日変わらない
- 月次の集計データです。対象月を確認してください。
- Appleの値が取得できない
- 配信元のページ構造変更や公開状況による可能性があります。
注意事項・制限
- Statcounterの統計は、同社が計測しているWebアクセスデータをもとに算出された市場シェアです。
そのため、Appleの販売台数や各国の人口から算出した「実際のiPhone所有率」とは異なります。
また、Statcounter側の仕様変更や通信状況によって、一時的にデータを取得できなくなる場合があります。 - Webアクセスに基づくモバイル端末ベンダーのシェアです。販売台数や全人口の所有率ではありません。
- 集計方法や対象期間が違う統計とは値が一致しない場合があります。
- ホーム画面の更新タイミングはiOSが管理するため、操作やデータの変更がすぐに反映されない場合があります。
データ提供・参考資料
Statcounter GlobalStats Mobile Vendor Market Share:モバイル端末ベンダー別のアクセスシェア。
SHARE

