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:

🔗 Scriptable im App Store


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:

  1. Tippe oben rechts auf das Plus-Symbol (+), um ein neues Skript zu erstellen.
  2. Vergib einen Namen wie z. B. „petuja Finanz-Widget“.
  3. Füge den kopierten Code in das Editor-Fenster ein.
  4. 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:

  1. Langes Tippen auf eine freie Stelle auf dem Homescreen → Apps beginnen zu wackeln.
  2. Tippe auf das Plus-Symbol (+) oben links.
  3. Suche nach Scriptable und wähle das Widget aus.
  4. Wähle die Größe „Mittel“ oder „Groß“ (klein ist nicht geeignet).
  5. Nach dem Hinzufügen: Widget antippen und bearbeiten
  6. 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!