Einleitung
Mit unserem neuen Scriptable-Widget für iOS möchten wir eine einfache Möglichkeit bieten, die aktuellen Kurse von Aktien, ETFs und Kryptowährungen direkt auf dem Homescreen im Blick zu behalten. Das Widget ist leichtgewichtig, übersichtlich und vollständig anpassbar – ideal für alle, die ihre Assets regelmäßig verfolgen möchten, ohne extra eine App zu öffnen.
Dank der Integration mit der Yahoo Finance API lassen sich beliebige Symbole anzeigen, inklusive deren Kursverlauf der letzten Tage. Die Anzeige passt sich automatisch an das gewählte Format (mittel oder groß) an und kann sowohl im Dark- als auch im Light-Mode verwendet werden. Besonders praktisch: Das Widget erkennt automatisch die passende Währung und zeigt sie direkt mit dem jeweiligen Symbol an.
In diesem Beitrag zeigen wir, was das Widget kann, wie man es installiert und nach den eigenen Wünschen anpasst – inklusive Beispielbildern, Anleitung und dem vollständigen Code zum Download.
Funktionen im Überblick – Was kann das Widget?
Das Widget ist bewusst schlank gehalten, bietet aber eine ganze Reihe durchdachter Funktionen:
- 📈 Aktuelle Kurse auf einen Blick
Zeigt den aktuellen Preis beliebiger Assets an – z. B. Aktien, ETFs, Kryptowährungen u. v. m. - ⏱️ Prozentuale Kursveränderungen
Zwei individuell einstellbare Spalten zeigen Kursveränderungen über definierte Zeiträume (z. B. 1D & 7D). - 🌍 Automatische Währungserkennung
Die Währung wird direkt aus den Yahoo-Finance-Daten übernommen (z. B. €, $, ¥) – keine manuelle Angabe nötig. - 🎨 Dark Mode / Light Mode umschaltbar
Das Widget passt sich per Einstellung an deinen bevorzugten Darstellungsmodus an. - 🌐 Mehrsprachige Spaltenüberschriften
Unterstützt Deutsch, Englisch, Französisch und Spanisch für internationale Nutzung. - 🛠️ Einfach anpassbar über Konfigurationsbereich
Alle Einstellungen (Titel, Sprache, Zeiträume, Symbole etc.) sind ganz oben im Script übersichtlich einstellbar. - 🕓 Letzte Aktualisierung
Die letzte Aktualisierungszeit wird links unten angezeigt
Screenshots


Installationsanleitung
1. Scriptable installieren
Lade die App „Scriptable“ aus dem App Store herunter:
2. Code kopieren
Kopiere den Code, damit du ihn, wie in Schritt 3 erklärt wird, im Skript einfügen kannst. Den Code findest du ganz unten auf der Webseite.
3. Neues Script erstellen
Öffne Scriptable und gehe wie folgt vor:
- Tippe oben rechts auf das Plus-Symbol (+), um ein neues Skript zu erstellen.
- Vergib einen Namen wie z. B. „petuja Finanz-Widget“.
- Füge den kopierten Code in das Editor-Fenster ein.
- Tippe oben rechts auf „Done“ (Fertig), um zu speichern.
4. Widget auf dem Homescreen einfügen
Nun kannst du das Widget zu deinem Homescreen hinzufügen:
- Langes Tippen auf eine freie Stelle auf dem Homescreen → Apps beginnen zu wackeln.
- Tippe auf das Plus-Symbol (+) oben links.
- Suche nach Scriptable und wähle das Widget aus.
- Wähle die Größe „Mittel“ oder „Groß“ (klein ist nicht geeignet).
- Nach dem Hinzufügen: Widget antippen und bearbeiten
- Wähle unter „Script“ dein zuvor erstelltes Widget-Script aus (z. B. „FinanzWidget“).
5. Anzeigen lassen – fertig!
Das Widget lädt nun automatisch die Kurse deiner konfigurierten Assets und zeigt sie an – inklusive Preis, Veränderung und Zeitstempel der letzten Aktualisierung.
Code
Klicke im Code-Fenster rechts oben auf das Icon, um den Code zu kopieren.
// ============================
// == KONFIGURATION / CONFIG ==
// ============================
// Titel des Widgets / Title of the widget
const WIDGET_TITLE = "Tech & Krypto"
// Dark Mode aktivieren / Enable dark mode
const USE_DARK_MODE = true
// Sprache für Spaltenüberschriften / Column header language
// Optionen / Options: "DE", "EN", "FR", "ES"
const LANGUAGE = "DE"
// Spalte 1 aktivieren (z. B. Tagesveränderung) / Enable column 1 (e.g. daily change)
const ENABLE_CHANGE_COL_1 = true
// Zeitraum in Tagen für Spalte 1 / Range in days for column 1
const RANGE_DAYS_COL_1 = 1
// Spalte 2 aktivieren (z. B. Wochenveränderung) / Enable column 2 (e.g. weekly change)
const ENABLE_CHANGE_COL_2 = true
// Zeitraum in Tagen für Spalte 2 / Range in days for column 2
const RANGE_DAYS_COL_2 = 7
// Liste der Assets / List of assets
// BTC & MATIC in EUR to demonstrate auto currency detection
const SYMBOLS = [
{ symbol: "NVDA", label: "NVIDIA" },
{ symbol: "AAPL", label: "Apple" },
{ symbol: "MSFT", label: "Microsoft" },
{ symbol: "BTC-EUR", label: "Bitcoin" },
{ symbol: "MATIC-EUR", label: "Matic" }
]
// ============================
// == SCRIPTABLE WIDGET CODE ==
// ============================
// Return localized column headers
function getColumnLabels(lang) {
switch (lang) {
case "DE": return { label1: "Bezeichnung", label2: "Preis" }
case "FR": return { label1: "Actif", label2: "Prix" }
case "ES": return { label1: "Activo", label2: "Precio" }
default: return { label1: "Asset", label2: "Price" }
}
}
// Convert currency code to a readable symbol
function currencySymbol(code) {
const map = {
USD: "$", EUR: "€", GBP: "£", CHF: "Fr.",
JPY: "¥", AUD: "A$", CAD: "C$", INR: "₹"
}
return map[code] || code
}
// Fetch data for a single asset from Yahoo Finance
async function fetchSymbolData(symbol) {
const maxRange = Math.max(RANGE_DAYS_COL_1, RANGE_DAYS_COL_2)
const url = `https://query1.finance.yahoo.com/v8/finance/chart/${symbol}?interval=1d&range=${maxRange + 1}d`
try {
const data = await new Request(url).loadJSON()
const result = data.chart.result[0]
const close = result.indicators.quote[0].close
const current = result.meta.regularMarketPrice
const currency = currencySymbol(result.meta.currency)
const change = (days) => {
if (close.length <= days) return null
const past = close[close.length - 1 - days]
return past ? ((current - past) / past) * 100 : null
}
return {
price: current,
currency,
change1: ENABLE_CHANGE_COL_1 ? change(RANGE_DAYS_COL_1) : null,
change2: ENABLE_CHANGE_COL_2 ? change(RANGE_DAYS_COL_2) : null
}
} catch (e) {
console.error(`Error fetching ${symbol}:`, e)
return null
}
}
// Format a percentage change with up/down arrow
function formatChange(val) {
if (val === null) return "-"
const arrow = val >= 0 ? "↑" : "↓"
return `${arrow} ${Math.abs(val).toFixed(2)}%`
}
// Return color depending on value direction
function getColor(val, gray) {
if (val === null) return gray
return val >= 0 ? Color.green() : Color.red()
}
// Define fixed layout width (medium and large share same width)
function getAdjustedWidgetWidth() {
switch (config.widgetFamily) {
case "small": return 0
case "medium":
case "large":
default:
return 300 // consistent layout width
}
}
// Create and return the widget
async function createWidget() {
const BG = USE_DARK_MODE ? new Color("#1C1C1E") : Color.white()
const TEXT = USE_DARK_MODE ? Color.white() : Color.black()
const GRAY = USE_DARK_MODE ? Color.gray() : new Color("#444")
const HEADER = TEXT
const { label1, label2 } = getColumnLabels(LANGUAGE)
const widget = new ListWidget()
widget.setPadding(12, 15, 12, 15)
widget.backgroundColor = BG
// Header with title and icon
const titleStack = widget.addStack()
titleStack.centerAlignContent()
const icon = SFSymbol.named("chart.bar.fill")
const iconImg = titleStack.addImage(icon.image)
iconImg.imageSize = new Size(16, 16)
iconImg.tintColor = TEXT
titleStack.addSpacer(6)
const title = titleStack.addText(WIDGET_TITLE)
title.textColor = TEXT
title.font = Font.boldSystemFont(14)
widget.addSpacer(8)
// Show message if widget is too small
if (config.widgetFamily === "small") {
const msgStack = widget.addStack()
msgStack.addSpacer()
const msg = msgStack.addText("Please use at least\na medium-sized widget.")
msg.font = Font.systemFont(12)
msg.textColor = GRAY
msg.centerAlignText()
msgStack.addSpacer()
widget.addSpacer()
const now = new Date()
const time = widget.addText(now.toLocaleTimeString("de-DE", { hour: "2-digit", minute: "2-digit" }))
time.font = Font.systemFont(10)
time.textColor = GRAY
time.leftAlignText()
return widget
}
// Fetch and prepare price data
const prices = []
for (const asset of SYMBOLS) {
const data = await fetchSymbolData(asset.symbol)
if (!data) continue
prices.push({
label: asset.label,
price: `${data.price.toFixed(2)} ${data.currency}`,
change1: data.change1,
change2: data.change2
})
}
// Build column data
const col1 = [label1, ...prices.map(p => p.label)]
const col2 = [label2, ...prices.map(p => p.price)]
const col3 = ENABLE_CHANGE_COL_1 ? [`${RANGE_DAYS_COL_1}D`, ...prices.map(p => formatChange(p.change1))] : []
const col4 = ENABLE_CHANGE_COL_2 ? [`${RANGE_DAYS_COL_2}D`, ...prices.map(p => formatChange(p.change2))] : []
const colors3 = ENABLE_CHANGE_COL_1 ? [HEADER, ...prices.map(p => getColor(p.change1, GRAY))] : []
const colors4 = ENABLE_CHANGE_COL_2 ? [HEADER, ...prices.map(p => getColor(p.change2, GRAY))] : []
const columns = [col1, col2]
const colorSets = [[], []]
if (ENABLE_CHANGE_COL_1) { columns.push(col3); colorSets.push(colors3) }
if (ENABLE_CHANGE_COL_2) { columns.push(col4); colorSets.push(colors4) }
// Calculate spacing dynamically
const colWidth = 70
const available = getAdjustedWidgetWidth()
const spacing = Math.max(4, Math.floor((available - (colWidth * columns.length)) / (columns.length - 1)))
// Render the main table
const rowStack = widget.addStack()
rowStack.layoutHorizontally()
rowStack.spacing = spacing
for (let i = 0; i < columns.length; i++) {
const colStack = rowStack.addStack()
colStack.layoutVertically()
colStack.spacing = 4
colStack.size = new Size(colWidth, 0)
for (let j = 0; j < columns[i].length; j++) {
const txt = colStack.addText(columns[i][j])
txt.font = j === 0 ? Font.mediumSystemFont(12) : Font.systemFont(12)
txt.textColor = colorSets[i]?.[j] || (j === 0 ? HEADER : GRAY)
txt.leftAlignText()
txt.minimumScaleFactor = 0.7
txt.lineLimit = 1
}
}
widget.addSpacer()
// Footer row with time and petuja.net
const footerStack = widget.addStack()
footerStack.layoutHorizontally()
const now = new Date()
const time = footerStack.addText(now.toLocaleTimeString("de-DE", { hour: "2-digit", minute: "2-digit" }))
time.font = Font.systemFont(10)
time.textColor = GRAY
time.leftAlignText()
footerStack.addSpacer()
const site = footerStack.addText("petuja.net")
site.font = Font.systemFont(10)
site.textColor = GRAY
site.rightAlignText()
return widget
}
// Run widget
if (config.runsInWidget) {
const w = await createWidget()
Script.setWidget(w)
} else {
const w = await createWidget()
await w.presentMedium()
}
Script.complete()
Das wars – Danke!