<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
<channel>
<title>Blog — FlineDev</title>
<description>Blog, apps, and open source Swift packages by indie developer Cihat Gündüz. Topics: SwiftUI, visionOS, error handling, localization, and more.</description>
<link>https://fline.dev/de/blog/</link>
<language>de</language>
<atom:link href="https://fline.dev/de/blog/feed.xml" rel="self" type="application/rss+xml"/>
<item>
<title>Pair Programming für Claude und Codex — ohne Copy-Paste</title>
<link>https://fline.dev/de/blog/tandemkit-pair-programming-for-ai-agents/</link>
<guid isPermaLink="true">https://fline.dev/de/blog/tandemkit-pair-programming-for-ai-agents/</guid>
<pubDate>Tue, 14 Apr 2026 00:00:00 +0000</pubDate>
<description><![CDATA[Ich hab monatelang Claude und Codex parallel genutzt — Ergebnisse per Hand hin- und herkopiert, Erkenntnisse manuell weitergegeben. TandemKit automatisiert genau diesen Workflow.]]></description>
<content:encoded><![CDATA[<p>In den letzten Monaten sah mein Entwicklungsworkflow so aus: Claude Code und Codex liefen nebeneinander. Gleicher Prompt, gleiches Ziel, beide erkundeten die Codebase unabhängig voneinander. Wenn es ans Planen ging, entwarf Claude den Plan und ich kopierte ihn rüber zu Codex zum Review. Codex fand Dinge, die Claude übersehen hatte — Edge Cases, falsche Annahmen, übersehene Dateien. Die Ergebnisse trug ich zurück, ließ Claude überarbeiten, kopierte die Überarbeitung wieder zu Codex, iterierte, bis sich beide einig waren. Manchmal stagedte ich Änderungen aus der Vorrunde, damit Codex die Diffs besser sehen konnte. Dann schaute ich mir das Ergebnis selbst an.</p><p>Das hat richtig gut funktioniert. Die zwei Modelle finden unterschiedliche Dinge. Claude ist besser in Synthese und Kommunikation. Codex liest genauer — es verfolgt Code-Pfade, prüft Edge Cases, erklärt nicht zu früh „fertig”. Sie ermitteln unabhängig voneinander, und genau das macht die Ergebnisse besser.</p><p>Aber das Hin-und-Her-Kopieren war furchtbar.</p><p>Links gingen verloren, Formatierungen zerfielen, aber das war nicht das eigentliche Problem. Das eigentliche Problem war, dass jede Übergabe von mir abhing. Output kopieren, in die andere Session einfügen, warten, zurückkopieren, wieder einfügen. Meine Aufmerksamkeit steckte in dieser Schleife fest, obwohl sie woanders viel nützlicher gewesen wäre. Das war der echte Schmerzpunkt — ein Workflow, der ohne mich nicht weiterlief.</p><p>Ich wollte den Workflow selbst gar nicht ändern. Ich wollte nur aufhören, der menschliche Nachrichtenbus zu sein.</p><h2 id="der-harness-artikel-der-alles-bestätigt-hat">Der Harness-Artikel, der alles bestätigt hat</h2><p>Dann bin ich auf Anthropics <a href="https://www.anthropic.com/engineering/harness-design-long-running-apps"><em>Harness design for long-running application development</em></a> gestoßen, erwähnt in einem YouTube-Video. Ich hab den ganzen Artikel gelesen, und er hat zwei Dinge gleichzeitig bewirkt.</p><p>Erstens hat er bestätigt, was ich schon erlebt hatte: eine separate Session die Arbeit unabhängig prüfen zu lassen — ohne den Anchoring Bias, der entsteht, wenn man selbst gebaut hat — liefert bessere Ergebnisse. Genau das hatte ich mit Claude und Codex gesehen.</p><p>Zweitens hat er diese Erfahrung in ein klareres System überführt. Ich hatte mich hauptsächlich auf den „anderes Modell”-Aspekt konzentriert. Der Artikel machte den „unabhängiger Evaluator”-Teil viel expliziter. Sogar eine andere <em>Claude</em>-Session hilft, wenn sie unabhängig genug ist. Die Trennung an sich ist das Entscheidende.</p><p>Und ehrlich gesagt ergibt das ja auch Sinn: Wenn die ursprüngliche Session den Code für falsch gehalten hätte, hätte sie ihn nicht so geschrieben. Es ist derselbe Vorteil, den Pair Programming schon immer hatte — ein zweites Augenpaar fängt auf, was das Gehirn des Autors ausblendet. Da unterscheiden sich diese LLMs gar nicht so sehr von uns.</p><p>Diese Kombination — bestätigt durch Erfahrung, dann geschärft durch den Artikel — war der Moment, in dem es Klick gemacht hat. Das war nicht nur eine persönliche Eigenart in meiner Arbeitsweise. Es schien allgemein nützlich zu sein. Also hab ich den Workflow in ein richtiges Plugin verwandelt und es <strong>TandemKit</strong> genannt.</p><p><img src="/assets/images/blog/tandemkit-pair-programming-for-ai-agents/logo.webp" alt="TandemKit logo" loading="lazy" /></p><h2 id="das-koordinationsproblem-das-wirklich-gelöst-werden-musste">Das Koordinationsproblem, das wirklich gelöst werden musste</h2><p>Mein erster Entwurf hatte vier Top-Level-Sessions: Planner, Generator und zwei Evaluators — einer Claude, einer Codex. Sie koordinierten sich über einfache Textdateien. Wenn eine Session fertig war, schrieb sie eine Sync-Datei, und die andere wartete im Hintergrund, bis sich diese Datei änderte. In Claude hat das einwandfrei funktioniert. Aber Codex wollte einfach nicht zuverlässig warten. Egal was ich versuchte — verschiedene File-Watching-Ansätze, verschiedene Signaling-Mechanismen — Codex hat entweder verpasst, dass es dran war, oder das Warten komplett übersprungen.</p><p>Ich war beim fünften oder sechsten Workaround dafür, als ich entdeckt habe, dass OpenAI gerade ein offizielles Codex-Plugin für Claude Code veröffentlicht hatte — <a href="https://github.com/openai/codex-plugin-cc"><code>codex-plugin-cc</code></a>. Claude konnte Codex jetzt intern aufrufen, die Antwort sehen und dieselbe Codex-Session später fortsetzen. Genau das, was ich brauchte.</p><p>Das hat den Claude-Codex-Austausch aber nicht unsichtbar gemacht. Alles wird weiterhin in Markdown-Dateien unter <code>TandemKit/</code> geschrieben, eine Datei pro Runde. Die komplette Mission-History bleibt auf der Festplatte — jede Recherche, jeder Convergence-Austausch, jedes Evaluationsergebnis.</p><p>Dieses Archiv ist auch nützlich. Wenn Wochen später etwas Merkwürdiges auftaucht, liefert die Git-History nicht nur eine Commit-Message — sie liefert die ganze Konversation hinter dem Commit, sodass man tatsächlich nachvollziehen kann, warum eine Entscheidung getroffen wurde. Es ist auch super, um den Workflow selbst zu verbessern: Alte Missions durchzulesen ist oft der schnellste Weg zu erkennen, dass eine Regel in <code>AGENTS.md</code> oder einen lokalen Skill gehört.</p><p>Was sich geändert hat, war die Infrastruktur. Statt ein fragiles viertes Terminal zu jonglieren, ruft TandemKit jetzt einen persistenten Codex-Subagent on demand über <code>codex-plugin-cc</code> auf und setzt ihn fort, wann immer es einen weiteren unabhängigen Durchlauf braucht.</p><h2 id="was-eine-mission-eigentlich-ist">Was eine Mission eigentlich ist</h2><p>Ich nutze KI-Agenten jetzt seit fast einem Jahr täglich, und eine Mission ist die Arbeitsgröße, bei der ich immer wieder lande: groß genug, um von separater Planung, Generierung und Evaluation zu profitieren, aber klein genug, dass das Ganze innerhalb eines Satzes von Sessions noch implementiert <em>und</em> vollständig verifiziert werden kann.</p><p>Wenn die Arbeit kleiner ist — ein schneller Einzeiler-Fix, ein kleines Refactoring, ein simples Umbenennen — nutze ich einfach direkt Claude Code. TandemKits Multi-Session-Schleife verbraucht mehr Tokens und mehr Aufwand, als so eine Änderung verdient.</p><p>Wenn die Arbeit größer ist, teile ich sie vorher auf. Da kommt <a href="https://github.com/FlineDev/PlanKit">PlanKit</a> ganz natürlich ins Spiel: Es führt Ideen durch <strong>Ideas → Roadmap → Features → Missions</strong>. Wenn etwas den Status „Mission” erreicht, ist es kein vager Feature-Topf mehr — es ist bereits in ein session-großes Arbeitsstück zugeschnitten.</p><h2 id="eine-aktuelle-mission-in-der-praxis">Eine aktuelle Mission in der Praxis</h2><p>Ich wollte App Store Connect Localization-Support zu <a href="https://translatekit.app">TranslateKit</a> hinzufügen, meinem KI-gestützten Tool für App-Lokalisierung. Die Idee war simpel: Statt App-Metadaten manuell in App Store Connect zu pflegen, könnten Entwickler Namen, Untertitel, Beschreibungen und Keywords für alle ihre Lokalisierungen direkt in TranslateKit bearbeiten — synchronisiert über Apples API.</p><p>Das war zu groß für eine Mission. Also hab ich es in zwei aufgeteilt: eine Mission für die App Store Connect API-Anbindung — JWT-Authentifizierung, Credential-Handling, Metadaten abrufen, Metadaten aktualisieren — und eine Mission für die UI-Änderungen, um das Ganze in der App sichtbar zu machen. Datenschicht zuerst, UX-Schicht danach.</p><p>Beim Planen haben Claude und Codex Edge Cases aufgedeckt, die ich nicht durchdacht hatte. Zum Beispiel: Was passiert, wenn der User eine neue Sprache hinzufügen will, aber es noch keine neue App Store Connect Version gibt? Bereits veröffentlichte Versionen lassen sich nicht bearbeiten, also muss das System entscheiden — Änderungen lokal cachen, den User auffordern zuerst eine Version zu erstellen, oder anbieten plattformübergreifend automatisch eine zu erstellen.</p><p>Und wenn TandemKit eine neue Version anlegt, welche Nummer soll sie bekommen? In die History schauen und raten? Den User bitten eine einzutippen? Gängige Nächste-Version-Optionen zur Auswahl anbieten? Das sind Produktentscheidungen, keine Implementierungsdetails, und die gehören in die Spec, bevor auch nur eine Zeile Code existiert.</p><p>Bei der Evaluation der API-Mission hat Claude das Feature zunächst als bestanden markiert, weil der Happy Path funktionierte und die Tests für existierende Versionen grün waren. Codex verfolgte den weniger offensichtlichen Branch und fand die eigentliche Lücke: Wenn keine editierbare App Store Connect Version existierte, erstellte der Code zwar den neuen Version-Record, versuchte aber nie den Localization-Write im selben Flow erneut. Der „neue Sprache”-Pfad sah also erfolgreich aus, tat aber im Stillen nichts, bis der User nochmal Sync auslöste — genau die Art von Code-Path-Bug, die ein zweites Modell findet.</p><p>Der Fix war klein: den Write nach der Version-Erstellung erneut versuchen, Tests für diesen Branch hinzufügen. Aber ohne den zweiten Durchlauf wäre er durchgerutscht.</p><h2 id="drei-sessions-autonome-schleife">Drei Sessions, autonome Schleife</h2><pre><code>DU  -- Planung
  |
  `--&gt; [1] Planner Session
             Claude ---------&gt; Codex (Hintergrund)
              |    &lt;- Findings - |
              `---- konvergieren ┘
                         |
                      Spec.md  &lt;-- du genehmigst, bevor es weitergeht

DU  -- beide Sessions starten, dann zurücklehnen
  |
  |--&gt; [2] Generator Session
  |          implementiert gegen Spec.md
  |          committet bei Meilensteinen
  |
  `--&gt; [3] Evaluator Session
             Claude ---------&gt; Codex (Hintergrund)
              |    &lt;- Findings - |
              `---- konvergieren ┘
                         |
                 FAIL -&gt; Generator behebt -&gt; Schleife
                 PASS -&gt; Review Briefing -&gt; du</code></pre><p>Der Planner ist der einzige interaktive Schritt. Sobald du <code>Spec.md</code> freigibst, laufen Generator und Evaluator eigenständig, bis du entweder ein FAIL zum Fixen oder ein PASS mit Review Briefing bekommst.</p><h2 id="wie-claude-und-codex-sich-tatsächlich-einigen">Wie Claude und Codex sich tatsächlich einigen</h2><p>Der naheliegende Ansatz wäre Scoring: Beide Modelle bewerten die Arbeit, man mittelt die Scores, bestanden ab einem bestimmten Schwellenwert. Aber Scores verstecken Fehler. Eine 8/10 kann bedeuten, dass zwei kritische Kriterien komplett durchgefallen sind und der Rest okay war.</p><p>Deshalb evaluiert TandemKit Kriterium für Kriterium. Jedes Finding bekommt zwei Dimensionen:</p><ul><li><p><strong>Agreement Level</strong>: agreed, partially agreed oder disputed</p></li><li><p><strong>Severity Level</strong>: <strong>HIGH</strong>, <strong>MEDIUM</strong> oder <strong>LOW</strong></p></li></ul><p>Claude und Codex ermitteln unabhängig und schreiben ihre Findings in Dateien. Dann liest Claude, was Codex gefunden hat, erstellt eine zusammengeführte Evaluation — behält bei, wo es übereinstimmt, erklärt wo es anderer Meinung ist — und Codex reviewt diesen Merge. Bei jeder Uneinigkeit werden die tatsächlichen Quelldateien nochmal gelesen, statt aus dem Gedächtnis zu argumentieren.</p><p>Die Convergence-Regel ist simpel: Die Schleife läuft weiter, bis keine <strong>HIGH</strong>- oder <strong>MEDIUM</strong>-Findings mehr in den Buckets „partially agreed” oder „disputed” übrig sind. Wenn dieselbe Meinungsverschiedenheit drei Runden überlebt, hört TandemKit auf zu iterieren und präsentiert dir beide Positionen.</p><p>Normalerweise 2–4 Runden.</p><h2 id="warum-nicht-agent-teams">Warum nicht Agent Teams</h2><p>Du fragst dich vielleicht: Hat Claude Code nicht Agent Teams für genau das? Doch, aber Agent Teams braucht API-Billing — nicht in Claude Max enthalten — und kann kein Codex einbinden.</p><p>Wenn Claude Max dich schon 100+ $/Monat kostet, sind 20 $/Monat für ChatGPT Plus ein vernünftiger Aufschlag — du bekommst ein unabhängiges zweites Modell, plus Bildgenerierung für UI-Mockups, Icons und andere Grafiken, die Claude nicht erzeugt.</p><h2 id="alles-ist-plain-text">Alles ist Plain Text</h2><p>Jede Recherche, jeder Convergence-Austausch, jede Evaluationsrunde wird als lesbare Datei in deinem Projekt gespeichert:</p><pre><code>TandemKit/001-ConnectAPIClient/
├── Spec.md
├── Planner-Discussion/
│   ├── Claude-01.md    ← Claudes Recherche
│   ├── Codex-01.md     ← Codex' unabhängige Findings
│   └── Claude-02.md    ← konvergierter Plan
├── Generator/
│   └── Round-01.md
└── Evaluator/
    ├── Round-01.md     ← FAIL: Version erstellt, Lokalisierungs-Write nicht wiederholt
    └── Round-02.md     ← PASS</code></pre><p>Einfach die Dateien öffnen und du kannst die gesamte Argumentationskette nachvollziehen.</p><h2 id="erste-schritte">Erste Schritte</h2><p>Die <a href="https://github.com/FlineDev/TandemKit">README auf GitHub</a> beschreibt den kompletten Setup-Ablauf und zeigt, wie die Sessions einander übergeben. Wenn das nach dem Workflow klingt, den du bisher von Hand zusammengestückelt hast, ist das der richtige Startpunkt:</p><a class="sk-link-card" href="https://github.com/FlineDev/TandemKit"><span class="sk-link-card-source"><svg viewBox="0 0 16 16" width="16" height="16" fill="currentColor" aria-hidden="true"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27s1.36.09 2 .27c1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0016 8c0-4.42-3.58-8-8-8z"/></svg> GitHub</span><span class="sk-link-card-title">FlineDev/TandemKit</span><span class="sk-link-card-description">Pair programming for AI agents — a Claude Code plugin that coordinates Claude and Codex across planning, generation, and evaluation.</span></a>]]></content:encoded>
</item>
<item>
<title>Warum ich Xcode 26s KI-Chat-Integration nicht nutze (und was meine Meinung ändern könnte)</title>
<link>https://fline.dev/de/blog/why-im-not-using-xcode-26s-ai-chat-integration-and-what-could-change-my-mind/</link>
<guid isPermaLink="true">https://fline.dev/de/blog/why-im-not-using-xcode-26s-ai-chat-integration-and-what-could-change-my-mind/</guid>
<pubDate>Sun, 17 Aug 2025 00:00:00 +0000</pubDate>
<description><![CDATA[7 fehlende Features, die mich von Xcodes KI fernhalten, plus mein 5-Release-Fahrplan, damit Apple zu Claude Code und Cursor aufschließen kann.]]></description>
<content:encoded><![CDATA[<p>Ich war bei KI-Tools bisher eher zurückhaltend und habe Cursor nicht ausprobiert, als es gehypt wurde, weil mir unwohl dabei war, einer KI vollen Projektzugriff zu geben. Ich habe im Grunde auf Apples datenschutzfreundlichere Lösung gewartet. Aber als sich herausstellte, dass es sich einfach nur um ChatGPT (oder andere Modelle) handelt, dachte ich mir, dann kann ich genauso gut Drittanbieter-Tools ausprobieren. Also teste ich seit der WWDC Xcode 26 (Beta 5) gegen Cursor und Claude Code, um zu sehen, welches Tool mich produktiver macht.</p><p>Und ich bin ehrlich froh, dass Apple auf serverbasierte LLMs umgeschwenkt ist, statt nur auf lokale Modelle zu setzen. Nachdem ich inzwischen gesehen habe, wie großartig Claude Code mit dem richtigen Context Engineering sein kann, weiß ich, dass eine rein lokale Lösung Jahre gebraucht hätte, um brauchbare Qualität zu erreichen. Apple hat hier richtig entschieden, die ursprüngliche Swift-Assist-Idee zu verwerfen, aber sie hatten offensichtlich nicht genug Zeit, eine vollständige Lösung für das erste Release zu bauen. Als jemand, der dem Apple-Ökosystem für die Entwicklung treu ist, ist es frustrierend, solche offensichtlichen Lücken in einem potenziell großartigen Tool zu sehen. Zum ersten Mal in meiner 14-jährigen Entwicklerkarriere nutze ich Xcode also nicht mehr als meinen täglichen Begleiter.</p><p><img src="/assets/images/blog/why-im-not-using-xcode-26s-ai-chat-integration-and-what-could-change-my-mind/claude-code-in-cursor.webp" alt="Claude Code in Cursor summarizes what context it has thanks to Context Engineering." loading="lazy" />
<em>Claude Code in Cursor fasst zusammen, welchen Kontext es dank Context Engineering hat.</em></p><p>Ich bin stattdessen dazu übergegangen, Claude Code in Cursors Terminal laufen zu lassen – so bekomme ich Cursors Editor-Awareness zusammen mit Claude Codes überlegenen Tools wie Websuche, Planungsmodus und dem großzügigen 5-Stunden-Nutzungsfenster. Ich setze voll auf das Chat-Interface und versuche, der KI durch umfangreiches Context Engineering (Richtlinien, Referenzdokumentation) beizubringen, wie sie guten Code schreibt. Xcodes KI-Integration hingegen – obwohl vielversprechend durch die native IDE-Position – fehlen grundlegende Features, die diese Art von KI-getriebenem Entwickeln produktiv machen.</p><p>Hier ist, was mich davon abhält, zurück zu Xcode zu wechseln.</p><h2 id="die-7-fehlenden-features-in-xcode-ai">Die 7 fehlenden Features in Xcode AI</h2><p><strong>1. Request-Queuing</strong> war die erste Einschränkung, die mir in Xcode sofort aufgefallen ist. Beim Entwickeln wechseln die Gedanken und Fragen schnell. Auf jede Antwort warten zu müssen, unterbricht meinen Rhythmus komplett. Eine der besten Möglichkeiten, bei der Arbeit mit KI Zeit zu sparen, ist vorauszudenken, während man wartet. Sowohl Cursor als auch Claude Code lassen mich nahtlos Anfragen anreihen und halten mich im Flow. Xcode blockiert einfach jede Eingabe.</p><p><img src="/assets/images/blog/why-im-not-using-xcode-26s-ai-chat-integration-and-what-could-change-my-mind/xcode-26-blocks-new-chat.webp" alt="Xcode 26 blocks new chat input while processing the previous one." loading="lazy" />
<em>Xcode 26 blockiert neue Chat-Eingaben, während die vorherige verarbeitet wird.</em></p><p><strong>2. Context-Engineering-Unterstützung</strong> ist der Bereich, in dem Xcode komplett versagt. Es gibt keine Unterstützung für Kontextdateien wie <code>.cursorrules</code> oder <code>CLAUDE.md</code>. Ohne automatisches Laden von Kontext ist all die Arbeit, die ich in Context Engineering gesteckt habe – der KI meine Coding-Standards, Architekturmuster, Fehlerbehandlungsansätze und mehr beizubringen – in Xcode schlicht nicht möglich. Ich muss meine Richtlinien in jeder Unterhaltung aufs Neue erklären. Claude Code und Cursor laden automatisch meine übergeordneten Richtlinien und verstehen dann, wann welche detailliertere Richtlinie gelesen werden muss. Das verwandelt KI von einem generischen Code-Generator in einen tatsächlich nützlichen Assistenten. In Xcode leider nicht.</p><p><img src="/assets/images/blog/why-im-not-using-xcode-26s-ai-chat-integration-and-what-could-change-my-mind/the-coding-assistant.webp" alt="The Coding Assistant telling me it has no way to teach it about my project or guidelines." loading="lazy" />
<em>Der Coding Assistant teilt mir mit, dass es keine Möglichkeit gibt, ihm etwas über mein Projekt oder meine Richtlinien beizubringen.</em></p><p><strong>3. Build-Validierung</strong> zeigt, dass Xcode nicht für KI-getriebene Entwicklung konzipiert wurde. Die KI kann ihre eigenen Codeänderungen nicht durch Builds validieren und hat nicht mal Zugriff auf die Build-Ausgabe, wenn ich sie selbst starte. Sie kann nicht mal die Konsolenausgabe lesen, wenn ich sie explizit darum bitte. Klar, Xcode erlaubt es, nach dem Build einen Fehler auszuwählen und die KI um eine Korrektur zu bitten – aber das ist für einen anderen Workflow gedacht. Es ist nur nützlich, wenn man selbst Code schreibt und einen Fehler macht, was selten vorkommt. Aber wenn die KI Code für mich schreibt? Build-Fehler treten ständig auf. Selbst bauen und dann manuell auf “Beheben” klicken zu müssen, ist super nervig. Die KI sollte einfach selbst bauen und die Fehler sehen können. Mit Claude Code lasse ich es einfach <code>xcodebuild</code> ausführen und die KI sieht alle Fehler sofort. Sie fragt mich zwar um Erlaubnis, aber wenn ich zustimme, kann sie iterieren, beheben und neu bauen, bis alles kompiliert – ohne manuelles Eingreifen.</p><p><img src="/assets/images/blog/why-im-not-using-xcode-26s-ai-chat-integration-and-what-could-change-my-mind/the-coding-assistant-2.webp" alt="The Coding Assistant telling me it can’t read the console output." loading="lazy" />
<em>Der Coding Assistant teilt mir mit, dass er die Konsolenausgabe nicht lesen kann.</em></p><p><strong>4. Git-Integration</strong> fehlt komplett – kein Durchsuchen der History, kein Vergleichen von Versionen, keine automatisierten Commits nach meinen Richtlinien. Ich kann der KI nicht sagen, sie soll meinen aktuellen Code mit einer früheren Version vergleichen, um Dokumentationsdateien basierend auf den letzten Änderungen zu aktualisieren, oder funktionierenden Code aus früheren Commits zurückholen. Claude Code kann meine Git-History durchsuchen, funktionierenden Code aus früheren Commits wiederherstellen und ordentlich formatierte Commits erstellen, indem es die tatsächlichen Änderungen analysiert und eine passende Nachricht findet, die meinen Commit-Richtlinien folgt. Es kann sogar helfen, Dokumentation zu aktualisieren, indem es vergleicht, was sich seit der letzten Version geändert hat.</p><p><img src="/assets/images/blog/why-im-not-using-xcode-26s-ai-chat-integration-and-what-could-change-my-mind/the-coding-assistant-3.webp" alt="The Coding Assistant telling me it can’t access git history." loading="lazy" />
<em>Der Coding Assistant teilt mir mit, dass er nicht auf die Git-History zugreifen kann.</em></p><p><strong>5. Terminal- und CLI-Zugriff</strong> ist die größte Lücke. Kein Kommandozeilenzugriff bedeutet kein <code>swift-format</code>, keine eigenen Skripte, keine Automatisierung. Als Indie-Entwickler, der viele Aufgaben gleichzeitig jongliert, ist das eine ernsthafte Einschränkung. Ich kann der KI nicht beibringen, vor einem Commit einen Code-Formatter auszuführen, kann sie nicht meine Test-Suites starten lassen und keine der eigenen Skripte ausführen, die meinen Entwicklungsprozess beschleunigen. Claude Code führt jeden Terminal-Befehl aus, den ich brauche. Es formatiert Code vor Commits, führt meine Test-Suites aus, startet Deployment-Skripte, verwaltet Abhängigkeiten und übernimmt meinen gesamten Automatisierungs-Workflow. Ein einziges Terminal-Tool gibt Zugriff auf alles – <code>git</code>, <code>xcodebuild</code>, Paketmanager, was auch immer. Moderne KI kann das!</p><blockquote><p>❇️ Wenn Apple allein den Terminal-Zugriff hinzufügen würde, wären die obigen Punkte 3 und 4 ebenfalls gelöst! Natürlich ist voller Kommandozeilenzugriff riskant. Aber Claude Code löst das elegant, indem es beim ersten Mal um Erlaubnis fragt und eine Allow- &amp; Deny-Liste in einer <a href="https://docs.anthropic.com/en/docs/claude-code/settings">einfachen Konfigurationsdatei</a> unterstützt.</p></blockquote><p><strong>6. Projektdatei-Einschränkungen</strong> zeigen, dass Xcode nicht für moderne KI-Context-Engineering-Workflows ausgelegt ist. Viele Projekte erstrecken sich über mehrere Repositories – wie <a href="https://translatekit.app/">TranslateKit</a> mit seiner App, dem Server und den <a href="https://github.com/FlineDev/TranslateKitSDK">Open-Source-Paket</a>-Komponenten. Meine Context-Engineering-Richtlinien liegen im übergeordneten Ordner, der kein Xcode-Projekt enthält, und trotzdem brauche ich KI-Zugriff darauf über alle Komponenten hinweg. Wenn ich einen Ordner in Xcode ziehe, um ihn im Editor zu öffnen, bekomme ich nur einen Fehler. Xcode kann nur einzelne Textdateien, Xcode-Projekte oder Swift-Package-Manifestdateien öffnen – aber keine beliebigen Ordner. Außerdem werden Dotfiles versteckt, sodass ich Dinge wie GitHub Actions Workflows nicht sehen oder bearbeiten kann. Cursor dagegen öffnet jeden Ordner, zeigt versteckte Dateien an und funktioniert über meine gesamte Multi-Repo-Entwicklungsumgebung hinweg. Das sollte Xcode auch können!</p><p><img src="/assets/images/blog/why-im-not-using-xcode-26s-ai-chat-integration-and-what-could-change-my-mind/the-error-dialog-when-i.webp" alt="The error dialog when I try to open an arbitrary folder in Xcode." loading="lazy" />
<em>Der Fehlerdialog, wenn ich versuche, einen beliebigen Ordner in Xcode zu öffnen.</em></p><p><strong>7. Websuche und Dokumentationszugriff</strong> fehlen ebenfalls komplett. Die KI kann nicht mal in Xcodes eigenen heruntergeladenen Dokumentationsdateien suchen. Wenn ich aktuelle Informationen oder API-Referenzen brauche, bin ich auf mich allein gestellt. Claude Code hat eine eingebaute Websuche. Wenn ich nach den neuesten Swift-Evolution-Proposals oder neuen APIs frage, die auf der WWDC25 vorgestellt wurden, findet es einfach die Antwort. In Xcode 26 nicht.</p><p><img src="/assets/images/blog/why-im-not-using-xcode-26s-ai-chat-integration-and-what-could-change-my-mind/asked-about-the-new.webp" alt="Asked about the new frameworks introduced by Apple in 2025, it 100% hallucinates." loading="lazy" />
<em>Nach den neuen Frameworks gefragt, die Apple 2025 eingeführt hat, halluziniert es zu 100 %.</em></p><p>Diese sieben Einschränkungen ergeben zusammen ein grundlegendes Problem: Xcodes KI fühlt sich an wie eine Tech-Demo und nicht wie ein Produktivitäts-Tool. Jedes fehlende Puzzleteil zwingt mich zurück in manuelle Workflows, die Claude Code nahtlos erledigt.</p><h2 id="mein-fahrplan-für-apple-5-release-meilensteine">Mein Fahrplan für Apple: 5 Release-Meilensteine</h2><p>So könnte Apple diese Lücken schließen und mich 2026 zurück zu Xcode bringen:</p><p><img src="/assets/images/blog/why-im-not-using-xcode-26s-ai-chat-integration-and-what-could-change-my-mind/my-roadmap-for-apple-5.webp" alt="My roadmap for apple 5" loading="lazy" /></p><p><strong>Xcode 26.1 (Oktober 2025):</strong>
Fügt Request-Queuing und Kontextdatei-Unterstützung hinzu (z. B. <code>Xcode.md</code>). Diese Features sollten einfach zu implementieren sein und kein Risiko unerwünschter Nebeneffekte haben.</p><p><strong>Xcode 26.2 (Dezember 2025):</strong>
Spezifische Tool-Integrationen für <code>git</code>, <code>xcodebuild</code> und <code>swift-format</code>. Da diese alle in Xcode integriert oder Apple-Technologien sind, können Apple-Ingenieure ihre bestehende Erfahrung nutzen, um diese Tools der KI mit sicheren, konservativen Aktionen bereitzustellen.</p><p><strong>Xcode 26.3 (März 2026):</strong>
Websuche, Integration der neuesten Dokumentation und Öffnen beliebiger Ordner mit Zugriff auf versteckte Dateien. Diese Integrationen sind alle risikoarm, brauchen aber Zeit, um sie richtig umzusetzen.</p><p><strong>Xcode 26.4 (Mai 2026):</strong>
Voller Terminal-Zugriff mit befehlsspezifischen Allow-/Deny-Listen.</p><p>Ich verstehe Apples Sicherheitsbedenken, aber Entwickler können mit der Verantwortung umgehen. Vertraut uns die Tools an, die wir brauchen, Apple, bitte!</p><p><strong>Xcode 27 (September 2026, angekündigt auf der WWDC 26):</strong>
Features, “die nur Apple bieten kann”, wie tiefe Simulator-Integration (stell dir vor, die KI navigiert durch deine App, um zu testen, ob ihre Änderungen wie erwartet funktioniert haben), oder SwiftUI-Preview-Zugriff (damit die KI sehen kann, wie ihr UI-Code aussieht). Vielleicht sogar ein “App Builder”-Modus, der SwiftUI zu einem sekundären Artefakt macht und App-Entwicklung einem ganz neuen Publikum zugänglich macht. Hier könnte Apple uns mit etwas überraschen, das wir nicht haben kommen sehen. Nicht nur “aufholen”, sondern die Konkurrenz wirklich übertreffen.</p><h2 id="das-fazit">Das Fazit</h2><p>Ich habe Xcode immer geliebt und möchte es ehrlich wieder exklusiv nutzen. Apple hat einzigartige Vorteile, die kein Wettbewerber bieten kann – nahtlose IDE-Integration, Simulator-Steuerung, SwiftUI-Preview-Funktionen. Aber im Moment ist Claude Code dramatisch leistungsfähiger, mit einem 5-fachen Produktivitätsunterschied. Aufgaben, die in Xcode Stunden dauern, erledigt Claude Code in Minuten – hauptsächlich weil es für einfache Dinge wie “Build drücken” nicht von mir abhängt.</p><p>Apple muss sich beeilen. Mit jedem Tag, der vergeht, etablieren mehr Entwickler tiefe Workflows mit anderen Tools. Ich werde jedes Point-Release gespannt verfolgen, und sobald Xcodes KI konkurrenzfähig wird, bin ich der Erste, der zurückkehrt. Bis dahin halte ich Claude Code in meinem Terminal offen und träume von dem Tag, an dem Xcode mich wieder umhauen wird und den Wandel zu einer AI-first-IDE schafft.</p><p><em>Wie sind eure Erfahrungen mit Xcodes KI-Integration? Habt ihr Workarounds für diese Einschränkungen gefunden, oder nutzt ihr auch Alternativen?</em></p>]]></content:encoded>
</item>
<item>
<title>Top 10 Developer-Tools, die Apple auf der WWDC25 vorgestellt hat</title>
<link>https://fline.dev/de/blog/top-10-developer-tools-apple-introduced-at-wwdc25/</link>
<guid isPermaLink="true">https://fline.dev/de/blog/top-10-developer-tools-apple-introduced-at-wwdc25/</guid>
<pubDate>Mon, 23 Jun 2025 00:00:00 +0000</pubDate>
<description><![CDATA[Entdecke bahnbrechende Features wie Foundation Models für On-Device-KI, ChatGPT-Integration in Xcode, AlarmKit für echte Wecker-Apps und große Verbesserungen bei räumlichen visionOS-Erlebnissen.]]></description>
<content:encoded><![CDATA[<p>Die WWDC25 ist vorbei, und nachdem ich über 50 Sessions geschaut und an Labs teilgenommen habe, habe ich die spannendsten entwicklerrelevanten Ankündigungen der diesjährigen Konferenz zusammengestellt. Hier sind die 10 herausragenden Features, die unsere Art, Apps für Apple-Plattformen zu bauen, verändern werden.</p><h2 id="1-foundation-models-on-device-ki">1. Foundation Models: On-Device-KI</h2><p>Der größte Gamechanger dieses Jahr ist <strong>Foundation Models</strong> – Apples On-Device-KI-Framework, das leistungsfähige 3-Milliarden-Parameter-Modelle direkt in deine Apps bringt. Was macht das so besonders?</p><ul><li><p><strong>Privacy-first</strong>: Alles läuft auf dem Gerät, ohne Server-Verzögerungen</p></li><li><p><strong>Strukturierter Output</strong>: Mit dem <code>@Generable</code>-Macro lassen sich typisierte Antworten garantieren</p></li><li><p><strong>Tool Calling</strong>: Die KI kann automatisch mit den Funktionen deiner App interagieren</p></li><li><p><strong>Teilergebnisse</strong>: Aktualisiere deine UI, während Antworten in Echtzeit generiert werden</p></li></ul><p><img src="/assets/images/blog/top-10-developer-tools-apple-introduced-at-wwdc25/best-of-wwdc25.webp" alt="Best of wwdc25" loading="lazy" /></p><h3 id="verwandte-sessions">Verwandte Sessions</h3><ul><li><p><a href="https://wwdcnotes.com/documentation/wwdcnotes/wwdc25-286-meet-the-foundation-models-framework">Meet the Foundation Models framework</a></p></li><li><p><a href="https://wwdcnotes.com/documentation/wwdcnotes/wwdc25-301-deep-dive-into-the-foundation-models-framework/">Deep dive into the Foundation Models framework</a></p></li><li><p><a href="https://wwdcnotes.com/documentation/wwdcnotes/wwdc25-248-explore-prompt-design-and-safety-for-ondevice-foundation-models/">Explore prompt design &amp; safety for on-device foundation models</a></p></li><li><p><a href="https://wwdcnotes.com/documentation/wwdcnotes/wwdc25-259-codealong-bring-ondevice-ai-to-your-app-using-the-foundation-models-framework/">Bring on-device AI to your app using Foundation Models</a></p></li></ul><h2 id="2-xcode-bekommt-chatgpt-integration">2. Xcode bekommt ChatGPT-Integration</h2><p>Apple hat alle überrascht, indem sie <strong>ChatGPT direkt in Xcode</strong> integriert haben – statt des angekündigten Swift Assist. Das bringt:</p><ul><li><p>Native KI-Unterstützung mit Code-Generierung und Dokumentation</p></li><li><p>Unterstützung für mehrere KI-Anbieter (ChatGPT, Claude, lokale Modelle)</p></li><li><p>Git-ähnliche History für KI-Änderungen, um Änderungen einfach rückgängig zu machen</p></li><li><p>Kontextbezogene Code-Vorschläge (wie Dokumentieren, Erklären usw.)</p></li></ul><p>Das <code>#Playground</code><strong>-Macro</strong> revolutioniert außerdem das Debugging – erstelle SwiftUI-Preview-artige Playgrounds für jeden Datentyp, nicht nur für Views. Ideal für Prompt Engineering!</p><p><img src="/assets/images/blog/top-10-developer-tools-apple-introduced-at-wwdc25/best-of-wwdc25-2.webp" alt="Best of wwdc25 2" loading="lazy" /></p><h3 id="verwandte-sessions">Verwandte Sessions</h3><ul><li><p><a href="https://wwdcnotes.com/documentation/wwdcnotes/wwdc25-247-whats-new-in-xcode">What’s new in Xcode</a></p></li><li><p><a href="https://wwdcnotes.com/documentation/wwdcnotes/wwdc25-306-optimize-swiftui-performance-with-instruments/">Optimize SwiftUI performance with Instruments</a></p></li><li><p><a href="https://wwdcnotes.com/documentation/wwdcnotes/wwdc25-344-record-replay-and-review-ui-automation-with-xcode/">Record, replay, and review: UI automation with Xcode</a></p></li></ul><h2 id="3-swiftui-webview-ist-endlich-da">3. SwiftUI WebView ist endlich da</h2><p>Nach Jahren voller Wünsche ist <strong>WebView jetzt nativ in SwiftUI</strong> verfügbar:</p><pre><code class="language-swift">WebView(url: URL(string: &quot;https://swift.org&quot;)!)</code></pre><p>Zusammen mit dem neuen <strong>WebPage</strong>-Typ für erweiterte Steuerung – einschließlich JavaScript-Ausführung und Scroll-Synchronisation. Das eröffnet hybride App-Erlebnisse, die vorher komplex umzusetzen waren und UIKit/AppKit-Bridging erforderten.</p><p><img src="/assets/images/blog/top-10-developer-tools-apple-introduced-at-wwdc25/best-of-wwdc25-3.webp" alt="Best of wwdc25 3" loading="lazy" /></p><h3 id="verwandte-sessions">Verwandte Sessions</h3><ul><li><p><a href="https://wwdcnotes.com/documentation/wwdcnotes/wwdc25-231-meet-webkit-for-swiftui">Meet WebKit for SwiftUI</a></p></li><li><p><a href="https://wwdcnotes.com/documentation/wwdcnotes/wwdc25-256-whats-new-in-swiftui/">What’s new in SwiftUI</a></p></li><li><p><a href="https://wwdcnotes.com/documentation/wwdcnotes/wwdc25-233-whats-new-in-safari-and-webkit/">What’s new in Safari and WebKit</a></p></li><li><p><a href="https://wwdcnotes.com/documentation/wwdcnotes/wwdc25-237-whats-new-for-the-spatial-web/">What’s new for the spatial web</a></p></li></ul><h2 id="4-alarmkit-drittanbieter-wecker-apps">4. AlarmKit: Drittanbieter-Wecker-Apps</h2><p><strong>AlarmKit</strong> durchbricht Fokus-Modi und Systemeinschränkungen und ermöglicht:</p><ul><li><p>Echte Wecker-Apps, die Nutzer tatsächlich aufwecken können</p></li><li><p>Live-Activities-Integration (erforderlich) für Schlummerfunktionalität</p></li><li><p>Eigene Wecktöne und individuelle Erlebnisse</p></li><li><p>Perfekt für Timer-, Wecker- und Gebets-/Medikamenten-Erinnerungs-Apps</p></li></ul><p><img src="/assets/images/blog/top-10-developer-tools-apple-introduced-at-wwdc25/best-of-wwdc25-4.webp" alt="Best of wwdc25 4" loading="lazy" /></p><h3 id="verwandte-session">Verwandte Session</h3><ul><li><p><a href="https://wwdcnotes.com/documentation/wwdcnotes/wwdc25-230-wake-up-to-the-alarmkit-api">Wake up to the AlarmKit API</a></p></li></ul><h2 id="5-app-store-connect-analytics-rundumerneuerung">5. App Store Connect: Analytics-Rundumerneuerung</h2><p>App Store Connect bekommt ein großes Upgrade:</p><ul><li><p><strong>Monthly Recurring Revenue (MRR)</strong> endlich verfügbar</p></li><li><p>Neue Analytics-APIs für Drittanbieter-Tools</p></li><li><p><strong>Angebotscodes für Nicht-Abo-Produkte</strong> (auf allen Plattformen)</p></li><li><p>Verbesserte Navigation – Analytics ist jetzt in die einzelnen Apps integriert</p></li></ul><p><img src="/assets/images/blog/top-10-developer-tools-apple-introduced-at-wwdc25/best-of-wwdc25-0006.webp" alt="Best of wwdc25 0006" loading="lazy" /></p><h3 id="verwandte-sessions">Verwandte Sessions</h3><ul><li><p><a href="https://wwdcnotes.com/documentation/wwdcnotes/wwdc25-328-whats-new-in-app-store-connect">What’s new in App Store Connect</a></p></li><li><p><a href="https://wwdcnotes.com/documentation/wwdcnotes/wwdc25-252-optimize-your-monetization-with-app-analytics/">Optimize your monetization with App Analytics</a></p></li><li><p><a href="https://wwdcnotes.com/documentation/wwdcnotes/wwdc25-241-whats-new-in-storekit-and-inapp-purchase/">What’s new in StoreKit and In-App Purchase</a></p></li><li><p><a href="https://wwdcnotes.com/documentation/wwdcnotes/wwdc25-324-automate-your-development-process-with-the-app-store-connect-api/">Automate your development process with the Connect API</a></p></li></ul><h2 id="6-visionos-räumliche-revolution">6. visionOS: Räumliche Revolution</h2><p>Große Verbesserungen bei visionOS:</p><ul><li><p><strong>Persistenz</strong>: Widgets, Fenster und Volumes bleiben zwischen Sitzungen angeheftet</p></li><li><p><strong>Nearby Sharing</strong>: Geteilte Multi-User-Erlebnisse im selben Raum</p></li><li><p><strong>APMP-Unterstützung</strong>: Support für 180°-, 360°- und Wide-FOV-Spatial-Video</p></li><li><p>Verbesserte SwiftUI-3D-Layout-Tools, die die Entwicklung erleichtern</p></li><li><p><strong>Swift Charts 3D</strong>: Dreidimensionale Datenvisualisierung für räumliche Apps</p></li></ul><p><img src="/assets/images/blog/top-10-developer-tools-apple-introduced-at-wwdc25/best-of-wwdc25-5.webp" alt="Best of wwdc25 5" loading="lazy" /></p><h3 id="verwandte-sessions">Verwandte Sessions</h3><ul><li><p><a href="https://wwdcnotes.com/documentation/wwdcnotes/wwdc25-278-whats-new-in-widgets">What’s new in widgets</a></p></li><li><p><a href="https://wwdcnotes.com/documentation/wwdcnotes/wwdc25-317-whats-new-in-visionos-26/">What’s new in visionOS 26</a></p></li><li><p><a href="https://wwdcnotes.com/documentation/wwdcnotes/wwdc25-304-explore-video-experiences-for-visionos">Explore video experiences for visionOS</a></p></li><li><p><a href="https://wwdcnotes.com/documentation/wwdcnotes/wwdc25-313-bring-swift-charts-to-the-third-dimension">Bring Swift Charts to the third dimension</a></p></li></ul><h2 id="7-swift-6-concurrency-wird-zugänglich">7. Swift 6: Concurrency wird zugänglich</h2><p><strong>Approachable Concurrency</strong> löst die Adoptionsprobleme von Swift 6:</p><ul><li><p>Standard-<code>@MainActor</code>-Isolation reduziert Warnungen</p></li><li><p>Schrittweises Opt-in zu Concurrency mit <code>@concurrent</code></p></li><li><p>Macht die Swift-6-Migration für bestehende App-Projekte endlich realistisch</p></li></ul><p><img src="/assets/images/blog/top-10-developer-tools-apple-introduced-at-wwdc25/best-of-wwdc25-6.webp" alt="Best of wwdc25 6" loading="lazy" /></p><h3 id="verwandte-sessions">Verwandte Sessions</h3><ul><li><p><a href="https://wwdcnotes.com/documentation/wwdcnotes/wwdc25-245-whats-new-in-swift">What’s new in Swift</a></p></li><li><p><a href="https://wwdcnotes.com/documentation/wwdcnotes/wwdc25-268-embracing-swift-concurrency/">Embracing Swift concurrency</a></p></li><li><p><a href="https://wwdcnotes.com/documentation/wwdcnotes/wwdc25-270-codealong-elevate-an-app-with-swift-concurrency/">Elevate an app with Swift concurrency</a></p></li></ul><h2 id="8-wi-fi-aware-mehr-als-bluetooth">8. Wi-Fi Aware: Mehr als Bluetooth</h2><p><strong>Wi-Fi Aware</strong> ermöglicht Erlebnisse wie AirPlay/AirDrop:</p><ul><li><p>Leistungsstarke lokale Kommunikation</p></li><li><p>Größere Reichweite als Bluetooth, plattformübergreifender Standard</p></li><li><p>Unterstützung für mehr gleichzeitige Verbindungen (ähnlich wie bei AirPods)</p></li><li><p>Perfekt für Media-Streaming und Multiplayer-Erlebnisse</p></li></ul><p><img src="/assets/images/blog/top-10-developer-tools-apple-introduced-at-wwdc25/best-of-wwdc25-7.webp" alt="Best of wwdc25 7" loading="lazy" /></p><h3 id="verwandte-session">Verwandte Session</h3><ul><li><p><a href="https://wwdcnotes.com/documentation/wwdcnotes/wwdc25-228-supercharge-device-connectivity-with-wifi-aware">Supercharge device connectivity with Wi-Fi Aware</a></p></li></ul><h2 id="9-string-catalog-verbesserungen">9. String Catalog-Verbesserungen</h2><p>Lokalisierung wird genauer und flexibler:</p><ul><li><p><strong>KI-generierte Kommentare</strong> für Übersetzungskontext</p></li><li><p><strong>String Symbols</strong> mit Auto-Completion (für manuelle Strings)</p></li><li><p><strong>Mehrfachauswahl</strong> für Massenaktualisierungen (einschließlich neuer Refactoring-Funktion)</p></li></ul><p><img src="/assets/images/blog/top-10-developer-tools-apple-introduced-at-wwdc25/best-of-wwdc25-8.webp" alt="Best of wwdc25 8" loading="lazy" /></p><h3 id="verwandte-sessions">Verwandte Sessions</h3><ul><li><p><a href="https://wwdcnotes.com/documentation/wwdcnotes/wwdc25-225-codealong-explore-localization-with-xcode">Explore localization with Xcode</a></p></li></ul><h2 id="10-icon-composer-layered-icons">10. Icon Composer &amp; Layered Icons</h2><p>Die neue <strong>Icon Composer</strong>-App erstellt:</p><ul><li><p>Geschichtete Icons mit bis zu 4 Ebenen und eingebauten Effekten</p></li><li><p>Vorschau aller Icon-Stile (einschließlich des umstrittenen “Clear”-Stils)</p></li><li><p>Einheitliches <code>.icon</code>-Dateiformat zum Ablegen in Xcode</p></li><li><p>Kann auch flache Bilder für Marketingzwecke exportieren</p></li></ul><p><img src="/assets/images/blog/top-10-developer-tools-apple-introduced-at-wwdc25/best-of-wwdc25-9.webp" alt="Best of wwdc25 9" loading="lazy" /></p><h3 id="verwandte-sessions">Verwandte Sessions</h3><ul><li><p><a href="https://wwdcnotes.com/documentation/wwdcnotes/wwdc25-361-create-icons-with-icon-composer">Create icons with Icon Composer</a></p></li><li><p><a href="https://wwdcnotes.com/documentation/wwdcnotes/wwdc25-220-say-hello-to-the-new-look-of-app-icons/">Say hello to the new look of app icons</a></p></li><li><p><a href="https://wwdcnotes.com/documentation/wwdcnotes/wwdc25-219-meet-liquid-glass/">Meet Liquid Glass</a></p></li><li><p><a href="https://wwdcnotes.com/documentation/wwdcnotes/wwdc25-356-get-to-know-the-new-design-system/">Get to know the new design system</a></p></li></ul><hr /><h2 id="schau-dir-die-vollständige-analyse-an">Schau dir die vollständige Analyse an</h2><p>Dieser Artikel deckt nur einen kleinen Teil der neuen APIs ab, aber es gibt noch so viel mehr in meiner umfassenden Video-Analyse zu entdecken. Ich gehe alles Interessante durch und teile meine Einschätzung, was für deine Apps am wichtigsten sein wird:</p><iframe width="200" height="113" src="https://www.youtube.com/embed/w3yfBjFFxAI?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen="" title="WWDC 2025 Developer Breakdown: 50+ New APIs, Foundation Models, AlarmKit &amp; Platform Updates"></iframe>
<p><em>Was wollt ihr am liebsten in euren Apps umsetzen? Lasst es mich in den Kommentaren auf YouTube oder auf anderen Kanälen wissen (Links unten)!</em></p>]]></content:encoded>
</item>
<item>
<title>Swift-Fehlermeldungen menschenfreundlich gestalten – gemeinsam</title>
<link>https://fline.dev/de/blog/making-swift-error-messages-human-friendly-together/</link>
<guid isPermaLink="true">https://fline.dev/de/blog/making-swift-error-messages-human-friendly-together/</guid>
<pubDate>Mon, 12 May 2025 00:00:00 +0000</pubDate>
<description><![CDATA[Swifts Fehlermeldungen können kryptisch sein, aber als Community können wir sie verständlicher machen. Hilf anderen (und deinem zukünftigen Ich), indem du bessere Erklärungen beisteuert.]]></description>
<content:encoded><![CDATA[<p>Trotz Swifts elegantem Design sind System-Fehlermeldungen oft kryptisch und wenig hilfreich. Statt mich nur zu beschweren, habe ich <a href="https://github.com/FlineDev/ErrorKit">ErrorKit</a> entwickelt – ein Tool, das diese Meldungen auf menschenfreundliche Beschreibungen abbildet. Aber Apples gesamtes Ökosystem abzudecken, ist eine zu große Aufgabe für eine einzelne Person – das muss ein Community-Projekt werden.</p><p>In diesem Beitrag teile ich die Grundlage, die ich für verbesserte Fehlerbeschreibungen geschaffen habe, und lade dich ein, gemeinsam mit mir ein umfassendes Wörterbuch benutzerfreundlicher Fehlermeldungen für Swift-Entwickler zu erstellen.</p><h2 id="das-problem-mit-system-fehlermeldungen">Das Problem mit System-Fehlermeldungen</h2><p>Schauen wir uns ein paar reale Beispiele unhilfreicher System-Fehlermeldungen an:</p><pre><code class="language-swift">// Dateisystem-Fehler
&quot;The file couldn't be opened because it doesn't exist.&quot;
// Bessere Meldung: &quot;The file 'report.pdf' could not be found. Please verify the file name and location.&quot;

// Core Data-Fehler
&quot;The operation couldn't be completed. (Cocoa error 133000.)&quot;
// Bessere Meldung: &quot;The database has a validation error. One or more required fields are empty or have invalid values.&quot;</code></pre><p>Diese Standard-Meldungen haben mehrere Probleme:</p><ol><li><p>Sie sind oft <strong>zu technisch</strong> für Endnutzer</p></li><li><p>Ihnen fehlt der <strong>Kontext</strong>, was gerade versucht wurde</p></li><li><p>Sie bieten selten <strong>Vorschläge</strong>, wie sich das Problem lösen lässt</p></li><li><p>Sie sind manchmal schlicht <strong>falsch</strong> oder irreführend</p></li><li><p>Sie können <strong>Implementierungsdetails</strong> preisgeben, die Nutzer gar nicht sehen sollten</p></li></ol><p>Apple hat tausende Fehlercodes über Dutzende Frameworks verteilt, und viele davon existieren seit Jahrzehnten. Neuere APIs wurden zwar verbessert, aber viele ältere Frameworks liefern nach wie vor Meldungen, die für Entwickler gedacht sind – nicht für Nutzer.</p><h2 id="die-lösung-community-kuratierte-fehlerzuordnungen">Die Lösung: Community-kuratierte Fehlerzuordnungen</h2><p>ErrorKit stellt eine Funktion namens <code>userFriendlyMessage(for:)</code> bereit, die System-Fehler auf hilfreichere Meldungen abbildet:</p><pre><code class="language-swift">do {
    let data = try Data(contentsOf: url)
    // Daten verarbeiten...
} catch {
    // Statt die Standard-Meldung anzuzeigen
    // showAlert(error.localizedDescription)

    // Eine verbesserte Meldung anzeigen
    showAlert(ErrorKit.userFriendlyMessage(for: error))
}</code></pre><p>Unter der Haube analysiert diese Funktion den Fehler und gibt eine bessere Beschreibung zurück, basierend auf einer wachsenden Sammlung bekannter Fehlertypen:</p><pre><code class="language-swift">// Beispiel aus ErrorKits Implementierung
enum FoundationErrorMapper: ErrorMapper {
    static func userFriendlyMessage(for error: Error) -&gt; String? {
        let nsError = error as NSError

        // URL-Ladefehler
        if nsError.domain == NSURLErrorDomain {
            switch nsError.code {
            case NSURLErrorNotConnectedToInternet:
                return String(localized: &quot;You are not connected to the Internet. Please check your connection.&quot;)
            case NSURLErrorTimedOut:
                return String(localized: &quot;The request timed out. Please try again later.&quot;)
            // Viele weitere Fälle...
            }
        }

        // Dateisystem-Fehler
        if nsError.domain == NSCocoaErrorDomain {
            switch nsError.code {
            case NSFileNoSuchFileError:
                return String(localized: &quot;The file could not be found.&quot;)
            // Viele weitere Fälle...
            }
        }

        // Weitere Domains und Fehlercodes...

        return nil // Fallback auf Standard-Behandlung
    }
}</code></pre><h2 id="aktuelle-abdeckung-und-beispiele">Aktuelle Abdeckung und Beispiele</h2><p>ErrorKit enthält Zuordnungen für häufige Fehler in wichtigen Apple-Frameworks:</p><h3 id="foundation">Foundation</h3><ul><li><p>Netzwerk: <code>NSURLErrorNotConnectedToInternet</code> → “Your device isn’t connected to the internet. Please check your connection and try again.”</p></li><li><p>Dateisystem: <code>NSFileNoSuchFileError</code> → “The file ‘report.pdf’ could not be found. Please verify the file name and location.”</p></li></ul><h3 id="core-data">Core Data</h3><ul><li><p>Validierung: <code>NSValidationErrorMinimum</code> → “The database failed validation. Please check your inputs and try again.”</p></li><li><p>Store-Verwaltung: <code>NSPersistentStoreCoordinatorError</code> → “Database error. This might be due to a recent app update or file corruption.”</p></li></ul><h3 id="mapkit">MapKit</h3><ul><li><p>Wegbeschreibungen: <code>MKErrorDirectionsNotFound</code> → “No directions found for the requested route.”</p></li><li><p>Standort: <code>MKErrorLocationUnknown</code> → “Current location unavailable. Please check location permissions.”</p></li></ul><h2 id="warum-wir-eine-gemeinschaftliche-antwort-brauchen">Warum wir eine gemeinschaftliche Antwort brauchen</h2><p>In einer idealen Welt würde Apple all seine verwirrenden Fehlermeldungen fixen. Und fairerweise muss man sagen: Sie haben einiges verbessert – neuere Frameworks wie SwiftUI sind deutlich klarer. Aber Jahrzehnte von Legacy-APIs liefern eben immer noch kryptische, technische Meldungen, die wohl nie wieder angefasst werden.</p><p>Genau hier kann unsere Community einen echten Unterschied machen. Kein einzelner Entwickler hat jeden obskuren Fehler gesehen – aber zusammen haben wir die meisten davon erlebt. Indem wir teilen, was wir bereits herausgefunden haben, und diese Meldungen in verständliche Sprache umschreiben, können wir eine Ressource schaffen, die allen Zeit spart und die Erfahrung für Entwickler und Nutzer gleichermaßen verbessert.</p><h2 id="so-kannst-du-beitragen">So kannst du beitragen</h2><p>Wenn du schon mal eine System-Fehlermeldung umgeschrieben hast, um sie hilfreicher zu machen, machst du die Arbeit ja bereits – jetzt kannst du sie teilen. Hier sind die 3 grundlegenden Schritte:</p><ol><li><p><strong>Eine schlechte Meldung entdecken</strong>
Notiere die Error-Domain und den Code, die Operation, die den Fehler ausgelöst hat, sowie nützliche Infos aus dem <code>userInfo</code>-Dictionary.</p></li><li><p><strong>Eine bessere Version schreiben</strong>
Verwende verständliche Sprache. Sei konkret. Biete einen hilfreichen nächsten Schritt an, wenn es Sinn ergibt.</p></li><li><p><strong>Auf GitHub einreichen</strong>
Eröffne einen Pull Request oder ein Issue im <a href="https://github.com/FlineDev/ErrorKit">ErrorKit-Repo</a> mit deiner verbesserten Meldung, dem ursprünglichen Kontext und eventuellen Anmerkungen.</p></li></ol><p>Selbst kleine Beiträge können tausenden Entwicklern helfen und die Nutzererfahrung für Millionen verbessern!</p><h2 id="über-apple-frameworks-hinaus-fehler-beliebiger-libraries-mappen">Über Apple-Frameworks hinaus: Fehler beliebiger Libraries mappen</h2><p>Während Apple-Frameworks der Hauptfokus sind, funktioniert ErrorKits Fehlerzuordnungssystem mit jedem Fehlertyp. Das <code>ErrorMapper</code>-Protocol ermöglicht es Entwicklern, eigene Zuordnungen für Third-Party-Libraries zu erstellen:</p><pre><code class="language-swift">// Beispiel-Mapper für Alamofire-Netzwerkfehler
enum AlamofireErrorMapper: ErrorMapper {
    static func userFriendlyMessage(for error: Error) -&gt; String? {
        switch error {
        case let afError as Alamofire.AFError:
            switch afError {
            case .sessionTaskFailed(let underlying):
                if let urlError = underlying as? URLError {
                    switch urlError.code {
                    case .notConnectedToInternet:
                        return String(localized: &quot;Your device isn't connected to the internet. Please check your connection and try again.&quot;)
                    case .timedOut:
                        return String(localized: &quot;The server took too long to respond. Please try again later.&quot;)
                    default:
                        return nil
                    }
                }
                return nil
            case .responseValidationFailed(let reason):
                if case .unacceptableStatusCode(let code) = reason {
                    return String(localized: &quot;The server returned error \(code). Please check your request or try again later.&quot;)
                }
                return nil
            default:
                return nil
            }
        default:
            return nil
        }
    }
}

// Für sofortige Verwendung registrieren
ErrorKit.registerMapper(AlamofireErrorMapper.self)</code></pre><p>Diese Erweiterbarkeit eröffnet Möglichkeiten für:</p><ul><li><p>Eigene Zuordnungen in deiner App für beliebte Libraries wie Alamofire</p></li><li><p>Mapper-Packages für Closed-Source-SDKs wie Stripe, Admob usw.</p></li><li><p>Teilen von Fehlerzuordnungen innerhalb von Teams oder Organisationen</p></li></ul><h2 id="fazit">Fazit</h2><p>Fehlermeldungen haben einen erheblichen Einfluss auf die Nutzererfahrung, werden in der Entwicklung aber oft übersehen. ErrorKits <code>userFriendlyMessage(for:)</code>-Funktion schafft einen Rahmen, um das durch Community-Zusammenarbeit zu ändern – eine Zusammenarbeit, bei der Entwickler ihr Wissen bündeln, um das gesamte Swift-Ökosystem benutzerfreundlicher zu machen.</p><p>Wenn du schon mal kryptische Fehlermeldungen entschlüsselt hast, ist deine Erfahrung wertvoll. Indem du zu ErrorKit beiträgst, kannst du diese Probleme nicht nur für dich selbst lösen, sondern für jeden Swift-Entwickler, der vor denselben Herausforderungen steht. Schau dir ErrorKit an und teile deine Lösungen, um diese Community-Ressource aufzubauen:</p><p><a class="sk-link-card sk-link-card-github" href="https://github.com/FlineDev/ErrorKit?ref=fline.dev"><span class="sk-link-card-source"><svg viewBox="0 0 16 16" width="16" height="16" fill="currentColor"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/></svg><span>github.com</span></span><span class="sk-link-card-title">FlineDev / ErrorKit</span><span class="sk-link-card-description">Simplified error handling with built-in user-friendly messages for common errors. Fully localized. Community-driven</span></a></p><p>Seid ihr schon auf kryptische Fehlermeldungen in Apple-Frameworks gestoßen? Habt ihr sie für euch hilfreicher gemacht? Schreibt mir auf den sozialen Kanälen (Links unten)!</p><h3 id="vorherige-artikel-in-dieser-serie">Vorherige Artikel in dieser Serie:</h3><ol><li><p><a href="https://www.fline.dev/swift-error-handling-done-right-overcoming-the-objective-c-error-legacy/">Swift Error Handling Done Right: Overcoming the ObjC Legacy</a></p></li><li><p><a href="https://www.fline.dev/swift-6-typed-throws-error-chains/">Unlocking the Power of Swift 6’s Typed Throws with Error Chains</a></p></li><li><p><a href="https://www.fline.dev/better-error-reporting-in-swift-apps-automatic-logs-analytics/">Better Error Reporting in Swift Apps: Automatic Logs + Analytics</a></p></li></ol>]]></content:encoded>
</item>
<item>
<title>Besseres Error-Reporting in Swift-Apps: Automatische Logs + Analytics</title>
<link>https://fline.dev/de/blog/better-error-reporting-in-swift-apps-automatic-logs-analytics/</link>
<guid isPermaLink="true">https://fline.dev/de/blog/better-error-reporting-in-swift-apps-automatic-logs-analytics/</guid>
<pubDate>Mon, 05 May 2025 00:00:00 +0000</pubDate>
<description><![CDATA[Genervt von vagen Bugreports wie "es geht nicht"? In diesem Beitrag lernst du, wie du automatische Logs sammelst und reale Fehler in deinen Swift-Apps trackst – mit nur wenigen Zeilen Code.]]></description>
<content:encoded><![CDATA[<p>“Es geht nicht.”</p><p>Wenn du schon mal eine iOS-App supportet hast, kennst du dieses frustrierend vage Nutzerfeedback. Keine Schritte zum Reproduzieren, keine Fehlermeldung, kein Kontext – nur das gefürchtete “geht nicht”, das dich mit mehr Fragen als Antworten zurücklässt.</p><p>Selbst die detailorientiertesten Nutzer wissen selten, welche Informationen du zur Diagnose brauchst. Und wenn sie doch versuchen zu helfen, fehlt ihnen oft das technische Wissen, um die richtigen Details zu liefern. Diese Diskrepanz erzeugt eine frustrierende Erfahrung für alle Beteiligten.</p><p>In diesem Beitrag stelle ich zwei praktische Ansätze vor, die ich in <a href="https://github.com/FlineDev/ErrorKit">ErrorKit</a> implementiert habe, um diese Lücke zu schließen: einen einfachen Feedback-Button, der automatisch Diagnose-Logs sammelt, und einen strukturierten Ansatz für Error-Analytics, der dir hilft, Muster zu erkennen – auch ohne direkte Nutzerberichte.</p><h2 id="das-problem-des-fehlenden-kontexts">Das Problem des fehlenden Kontexts</h2><p>Wenn Nutzer auf Probleme stoßen, erschweren mehrere Herausforderungen die Diagnose:</p><ol><li><p>Sie wissen nicht, <strong>welche Informationen</strong> du brauchst</p></li><li><p>Sie können nicht einfach auf <strong>System-Logs</strong> zugreifen</p></li><li><p>Sie können sich nur schwer an die <strong>genauen Schritte erinnern</strong> und diese formulieren</p></li><li><p>Komplexe Probleme können <strong>mehrere Komponenten</strong> betreffen</p></li><li><p>Sporadische Probleme sind <strong>schwer auf Abruf reproduzierbar</strong></p></li></ol><p>Ohne richtigen Kontext wird Debugging zum Ratespiel. Du verbringst womöglich Stunden mit dem Reproduzieren eines Problems, das mit den richtigen Informationen in Minuten gelöst wäre.</p><h2 id="lösung-1-feedback-button-mit-angehängten-logs">Lösung 1: Feedback-Button mit angehängten Logs</h2><p>Die erste Lösung macht es Nutzern extrem einfach, dir vollständige Informationen zu schicken. ErrorKit bietet einen SwiftUI-Modifier, der einen Mail-Composer mit automatischer Log-Sammlung hinzufügt:</p><pre><code class="language-swift">struct ContentView: View {
    @State private var showMailComposer = false

    var body: some View {
        VStack {
            // Dein App-Inhalt

            Button(&quot;Report a Problem&quot;) {
                showMailComposer = true
            }
            .mailComposer(
                isPresented: $showMailComposer,
                recipient: &quot;support@yourapp.com&quot;,
                subject: &quot;YourApp Bug Report&quot;,
                messageBody: &quot;&quot;&quot;
                   Please describe what happened:



                   ----------------------------------
                   Device: \(UIDevice.current.model)
                   iOS: \(UIDevice.current.systemVersion)
                   App version: \(Bundle.main.infoDictionary?[&quot;CFBundleShortVersionString&quot;] as? String ?? &quot;Unknown&quot;)
                   &quot;&quot;&quot;,
                attachments: [
                    try? ErrorKit.logAttachment(ofLast: .minutes(30))
                ]
            )
        }
    }
}</code></pre><p>Das erstellt einen einfachen “Problem melden”-Button, der:</p><ol><li><p>Einen <strong>vorausgefüllten</strong> E-Mail-Composer öffnet</p></li><li><p><strong>Geräte- und App-Informationen</strong> enthält</p></li><li><p>Automatisch aktuelle <strong>System-Logs</strong> anhängt</p></li><li><p><strong>Platz für den Nutzer</strong> bietet, das Problem zu beschreiben</p></li></ol><p>Der Log-Anhang ist hier das Geheimrezept. Wenn der Nutzer diesen Button nach einem Problem antippt, bekommst du ein umfassendes Bild davon, was in und um deine App passiert ist, als das Problem auftrat.</p><h2 id="apples-unified-logging-system-nutzen">Apples Unified Logging System nutzen</h2><p>ErrorKit verwendet Apples Unified Logging System (<code>OSLog</code>/<code>Logger</code>), um Diagnose-Informationen zu sammeln. Falls du noch kein strukturiertes Logging nutzt, hier eine kurze Einführung:</p><pre><code class="language-swift">import OSLog

// Logger erstellen
let logger = Logger()
// oder mit Subsystem und Kategorie
let networkLogger = Logger(subsystem: &quot;com.yourapp&quot;, category: &quot;networking&quot;)

// Auf passenden Ebenen loggen
logger.debug(&quot;Detailed connection info&quot;)      // Entwickler-Debugging
logger.info(&quot;User tapped submit button&quot;)      // Allgemeine Informationen
logger.notice(&quot;Profile successfully loaded&quot;)   // Wichtige Ereignisse
logger.error(&quot;Failed to load user data&quot;)      // Fehler, die behoben werden sollten
logger.fault(&quot;Database corruption detected&quot;)   // Systemausfälle

// Werte formatieren und Datenschutz steuern
logger.info(&quot;User \(userId, privacy: .private) logged in from \(ipAddress, privacy: .public)&quot;)</code></pre><p>Das Unified Logging System bietet mehrere Vorteile gegenüber <code>print()</code>-Anweisungen:</p><ul><li><p>Log-Level zum Filtern von Informationen</p></li><li><p>Datenschutzkontrollen für sensible Daten</p></li><li><p>Effiziente Performance mit minimalem Overhead</p></li><li><p>Persistenz über App-Neustarts hinweg</p></li></ul><h2 id="umfassende-log-sammlung">Umfassende Log-Sammlung</h2><p>Ein zentraler Vorteil von ErrorKits Ansatz ist, dass nicht nur die Logs deiner App erfasst werden, sondern auch relevante Logs von:</p><ol><li><p><strong>Third-Party-Frameworks</strong>, die Apples Unified Logging System nutzen</p></li><li><p><strong>Systemkomponenten</strong>, mit denen deine App interagiert (Netzwerk, Dateisystem usw.)</p></li><li><p><strong>Hintergrundprozessen</strong>, die mit der Funktionalität deiner App zusammenhängen</p></li></ol><p>Das gibt dir ein vollständiges Bild davon, was in und um deine App passiert ist, als das Problem auftrat – nicht nur die Logs, die du explizit hinzugefügt hast.</p><h2 id="log-sammlung-steuern">Log-Sammlung steuern</h2><p>Du kannst die Log-Sammlung anpassen, um Detail und Datenschutz auszubalancieren:</p><pre><code class="language-swift">// Logs der letzten 30 Minuten ab Notice-Level sammeln (Standard)
try ErrorKit.logAttachment(ofLast: .minutes(30), minLevel: .notice)

// Logs der letzten Stunde ab Error-Level sammeln (weniger ausführlich)
try ErrorKit.logAttachment(ofLast: .hours(1), minLevel: .error)

// Logs der letzten 5 Minuten ab Debug-Level sammeln (sehr detailliert)
try ErrorKit.logAttachment(ofLast: .minutes(5), minLevel: .debug)</code></pre><p>Der <code>minLevel</code>-Parameter filtert Logs nach Wichtigkeit:</p><ul><li><p><code>.debug</code>: Alle Logs (sehr ausführlich)</p></li><li><p><code>.info</code>: Informative Logs und darüber</p></li><li><p><code>.notice</code>: Bemerkenswerte Ereignisse (Standard)</p></li><li><p><code>.error</code>: Nur Fehler und Ausfälle</p></li><li><p><code>.fault</code>: Nur kritische Fehler</p></li></ul><p>Das gibt dir Kontrolle darüber, wie viel Information du sammelst, während du trotzdem den nötigen Kontext für die Diagnose erhältst.</p><h2 id="alternative-methoden-für-mehr-kontrolle">Alternative Methoden für mehr Kontrolle</h2><p>Wenn du mehr Kontrolle über die Log-Verarbeitung brauchst, bietet ErrorKit zusätzliche Ansätze:</p><h3 id="log-daten-direkt-abrufen">Log-Daten direkt abrufen</h3><p>Um Logs an dein eigenes Backend zu senden oder sie in der App zu verarbeiten, verwende <code>loggedData</code>:</p><pre><code class="language-swift">let logData = try ErrorKit.loggedData(
    ofLast: .minutes(10),
    minLevel: .notice
)

// Die Daten mit deinem eigenen Reporting-System verwenden
analyticsService.sendLogs(data: logData)</code></pre><h3 id="in-eine-temporäre-datei-exportieren">In eine temporäre Datei exportieren</h3><p>Um Logs über andere Mechanismen zu teilen, verwende <code>exportLogFile</code>:</p><pre><code class="language-swift">let logFileURL = try ErrorKit.exportLogFile(
    ofLast: .hours(1),
    minLevel: .error
)

// Die Log-Datei teilen
let activityVC = UIActivityViewController(
    activityItems: [logFileURL],
    applicationActivities: nil
)
present(activityVC, animated: true)</code></pre><h2 id="lösung-2-smarte-error-analytics-mit-gruppierungs-ids">Lösung 2: Smarte Error-Analytics mit Gruppierungs-IDs</h2><p>Während der Feedback-Button Nutzern hilft, bemerkte Probleme zu melden, bleiben viele Probleme ungemeldet. Nutzer stoßen vielleicht auf einen Fehler, zucken die Schultern und versuchen es erneut – ohne dir je davon zu erzählen. Genau hier kommen Error-Analytics ins Spiel.</p><p>ErrorKit bietet Tools, um Fehler automatisch zu tracken und intelligent zu gruppieren:</p><pre><code class="language-swift">func handleError(_ error: Error) {
    // Eine stabile ID abrufen, die dynamische Parameter ignoriert
    let groupID = ErrorKit.groupingID(for: error)

    // Die vollständige Fehlerketten-Beschreibung abrufen
    let errorDetails = ErrorKit.errorChainDescription(for: error)

    // An dein Analytics-System senden
    Analytics.track(
        event: &quot;error_occurred&quot;,
        properties: [
            &quot;error_group&quot;: groupID,
            &quot;error_details&quot;: errorDetails,
            &quot;user_id&quot;: currentUser.id
        ]
    )

    // Dem Nutzer eine passende UI anzeigen
    showErrorAlert(message: ErrorKit.userFriendlyMessage(for: error))
}</code></pre><p><em>Beispiel einer globalen Fehlerbehandlungsfunktion für deine App.</em></p><p>Die Magie steckt hier in der <code>groupingID(for:)</code>-Funktion. Sie generiert einen stabilen Bezeichner basierend auf der Typstruktur des Fehlers und den Enum-Cases, wobei dynamische Parameter und lokalisierte Meldungen ignoriert werden.</p><p>Das bedeutet, dass Fehler mit der gleichen Ursache dieselbe Gruppierungs-ID haben, auch wenn konkrete Details (wie Dateipfade oder Nutzer-IDs) unterschiedlich sind:</p><pre><code>// Beide erzeugen dieselbe groupID: &quot;3f9d2a&quot;
ProfileError
└─ DatabaseError
   └─ FileError.notFound(path: &quot;/Users/john/data.db&quot;)

ProfileError
└─ DatabaseError
   └─ FileError.notFound(path: &quot;/Users/jane/backup.db&quot;)</code></pre><p>Dieser Ansatz bietet mehrere Vorteile:</p><ol><li><p><strong>Häufige Probleme identifizieren</strong>: Sieh, welche Fehler am häufigsten auftreten</p></li><li><p><strong>Fixes priorisieren</strong>: Konzentriere dich zuerst auf Probleme mit großer Auswirkung</p></li><li><p><strong>Behebung nachverfolgen</strong>: Beobachte, ob Fehlerraten nach Fixes sinken</p></li><li><p><strong>Neue Probleme erkennen</strong>: Identifiziere schnell neue Fehlermuster nach Releases</p></li><li><p><strong>Mit Nutzersegmenten korrelieren</strong>: Sieh, ob bestimmte Fehler spezifische Nutzer betreffen</p></li></ol><h2 id="beide-ansätze-für-maximale-einblicke-kombinieren">Beide Ansätze für maximale Einblicke kombinieren</h2><p>Ein wirkungsvoller Ansatz ist es, automatische Analytics mit nutzerinitiiertem Feedback zu kombinieren, also etwa so:</p><pre><code class="language-swift">func handleError(_ error: Error) {
    // Immer für Analytics tracken
    trackErrorAnalytics(error)

    // Bei schwerwiegenden oder unerwarteten Fehlern zum Feedback auffordern
    if isSerious(error) {
        showErrorAlert(
            message: ErrorKit.userFriendlyMessage(for: error),
            feedbackOption: true
        )
    } else {
        // Bei kleineren Problemen einfach eine Meldung anzeigen
        showErrorAlert(message: ErrorKit.userFriendlyMessage(for: error))
    }
}

func showErrorAlert(message: String, feedbackOption: Bool = false) {
    // Implementierung eines Alerts, der optional einen
    // &quot;Feedback senden&quot;-Button enthält, der den Mail-Composer mit Logs öffnet
}</code></pre><p>Das ergibt ein umfassendes System, in dem:</p><ol><li><p>Alle Fehler für Analytics getrackt werden und dir breite Muster liefern</p></li><li><p>Schwerwiegende Fehler Nutzer zu detailliertem Feedback mit Logs auffordern</p></li><li><p>Nutzer jederzeit Feedback für Probleme initiieren können, die du womöglich nicht trackst</p></li></ol><h2 id="best-practices-für-logging">Best Practices für Logging</h2><p>Um den Wert der Log-Sammlung zu maximieren, beachte diese Best Practices:</p><h3 id="1-logs-mit-kontext-strukturieren">1. Logs mit Kontext strukturieren</h3><p>Liefere genug Kontext in deinen Logs, um zu verstehen, was passiert ist:</p><pre><code class="language-swift">// Statt:
Logger().error(&quot;Failed to load&quot;)

// Verwende:
Logger().error(&quot;Failed to load document \(documentId): \(ErrorKit.errorChainDescription(for: error))&quot;)</code></pre><h3 id="2-passende-log-level-wählen">2. Passende Log-Level wählen</h3><p>Setze Log-Level strategisch ein, um die Ausführlichkeit zu steuern:</p><ul><li><p><code>.debug</code> für Entwicklerdetails, die nur während der Entwicklung gebraucht werden</p></li><li><p><code>.info</code> zum Nachverfolgen des normalen App-Ablaufs</p></li><li><p><code>.notice</code> für wichtige Ereignisse, die Nutzer interessieren würden</p></li><li><p><code>.error</code> für Probleme, die behoben werden müssen, aber die Kernfunktionalität nicht verhindern</p></li><li><p><code>.fault</code> für kritische Probleme, die die Kernfunktionalität beeinträchtigen</p></li></ul><h3 id="3-sensible-informationen-schützen">3. Sensible Informationen schützen</h3><p>Verwende Privacy-Modifier, um Nutzerdaten zu schützen:</p><pre><code class="language-swift">Logger().info(&quot;Processing payment for user \(userId, privacy: .private)&quot;)</code></pre><h3 id="4-wichtige-nutzeraktionen-loggen">4. Wichtige Nutzeraktionen loggen</h3><p>Erstelle Breadcrumbs der Nutzeraktivität, um den Weg zu Fehlern nachzuvollziehen:</p><pre><code class="language-swift">Logger().notice(&quot;User navigated to profile screen&quot;)
Logger().info(&quot;User tapped edit button&quot;)
Logger().notice(&quot;User saved profile changes&quot;)</code></pre><h3 id="5-start-und-abschluss-wichtiger-operationen-loggen">5. Start und Abschluss wichtiger Operationen loggen</h3><p>Klammere bedeutende Operationen ein, um unvollständige Aufgaben zu identifizieren:</p><pre><code class="language-swift">Logger().notice(&quot;Starting data sync&quot;)
// ... Sync-Implementierung
Logger().notice(&quot;Completed data sync&quot;)</code></pre><h2 id="die-auswirkung-auf-support-und-entwicklung">Die Auswirkung auf Support und Entwicklung</h2><p>Die Implementierung dieser Tools kann sowohl die Nutzererfahrung als auch die Entwicklungsabläufe transformieren:</p><h3 id="für-nutzer">Für Nutzer:</h3><ul><li><p><strong>Vereinfachtes Melden</strong>: Feedback mit einem einzigen Tipp absenden</p></li><li><p><strong>Keine technischen Fragen</strong>: Frustrierendes Hin-und-her-Kommunizieren vermeiden</p></li><li><p><strong>Schnellere Lösung</strong>: Probleme können schneller diagnostiziert und behoben werden</p></li><li><p><strong>Bessere Erfahrung</strong>: Zeigt Nutzern, dass ihre Probleme ernst genommen werden</p></li></ul><h3 id="für-entwickler">Für Entwickler:</h3><ul><li><p><strong>Vollständiger Kontext</strong>: Sieh genau, was passiert ist, als Probleme auftraten</p></li><li><p><strong>Reduzierter Support-Aufwand</strong>: Weniger Zeit für Nachfragen nach zusätzlichen Informationen</p></li><li><p><strong>Bessere Reproduktion</strong>: Zuverlässigere Reproduktionsschritte basierend auf Log-Daten</p></li><li><p><strong>Effizientes Debugging</strong>: Muster in Fehlerberichten schnell erkennen</p></li><li><p><strong>Datengetriebene Prioritäten</strong>: Zuerst die häufigsten Probleme beheben</p></li></ul><h2 id="fazit">Fazit</h2><p>ErrorKits Ansatz überbrückt die frustrierende Lücke zwischen einem Nutzer, der “es geht nicht” sagt, und dem tatsächlichen Wissen, was passiert ist. Ich habe festgestellt, dass automatische Log-Sammlung kombiniert mit smarter Error-Analytics eine Feedback-Schleife erzeugt, die tatsächlich funktioniert.</p><p>Wirklich mächtig wird es, wenn du detaillierte Logs bekommst, sobald Nutzer sich entscheiden, ein Problem zu melden, und gleichzeitig die Probleme auffängst, die sie nie erwähnen. Dieser duale Ansatz hat grundlegend verändert, wie ich Probleme in meinen Apps verstehe und behebe. Wenn du es satt hast, Probleme im Blindflug zu debuggen, enthält ErrorKit all diese Logging-Tools und Verbesserungen für die Fehlerbehandlung – Werkzeuge, die ich gebaut habe, weil ich sie selbst brauchte:</p><p><a class="sk-link-card sk-link-card-github" href="https://github.com/FlineDev/ErrorKit?ref=fline.dev"><span class="sk-link-card-source"><svg viewBox="0 0 16 16" width="16" height="16" fill="currentColor"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/></svg><span>github.com</span></span><span class="sk-link-card-title">FlineDev / ErrorKit</span><span class="sk-link-card-description">Simplified error handling with built-in user-friendly messages for common errors. Fully localized. Community-driven</span></a></p><p>Wie gehst du mit Nutzerfeedback und Error-Reporting um? Hast du andere effektive Techniken gefunden, die wirklich helfen? Schreib mir auf den sozialen Kanälen (Links unten)!</p><h3 id="vorherige-artikel-in-dieser-serie">Vorherige Artikel in dieser Serie:</h3><ol><li><p><a href="https://www.fline.dev/swift-error-handling-done-right-overcoming-the-objective-c-error-legacy/">Swift Error Handling Done Right: Overcoming the ObjC Legacy</a></p></li><li><p><a href="https://www.fline.dev/swift-6-typed-throws-error-chains/">Unlocking the Power of Swift 6’s Typed Throws with Error Chains</a></p></li></ol><h3 id="folgender-artikel-in-dieser-serie">Folgender Artikel in dieser Serie:</h3><ol><li><p><a href="https://www.fline.dev/making-swift-error-messages-human-friendly-together/">Making Swift Error Messages Human-Friendly—Together</a></p></li></ol>]]></content:encoded>
</item>
<item>
<title>Die wahre Stärke von Swift 6&apos;s Typed Throws mit Fehlerketten entfesseln</title>
<link>https://fline.dev/de/blog/swift-6-typed-throws-error-chains/</link>
<guid isPermaLink="true">https://fline.dev/de/blog/swift-6-typed-throws-error-chains/</guid>
<pubDate>Mon, 28 Apr 2025 00:00:00 +0000</pubDate>
<description><![CDATA[Erfahre, wie du Typed Throws vom Kopfzerbrechen zur Superkraft machst – mit sauberer Fehlerbehandlung und mächtigen Debugging-Einblicken.]]></description>
<content:encoded><![CDATA[<p>Swift 6 hat endlich eines der meistgewünschten Features in Swift eingeführt: Typed Throws. Diese Verbesserung erlaubt es dir, genau anzugeben, welche Fehlertypen eine Funktion werfen kann, und bringt damit Swifts Typsicherheit in die Fehlerbehandlung. Aber mit dieser Mächtigkeit kommt eine neue Herausforderung, die ich “Verschachtelungshölle” nennen würde – ein Problem, das beeinflusst, wie Fehler sich durch die Schichten deiner Anwendung verbreiten.</p><p>In diesem Beitrag erkläre ich das Verschachtelungsproblem und zeige dir, wie ich es in <a href="https://github.com/FlineDev/ErrorKit">ErrorKit</a> mit einem einfachen Protocol gelöst habe, das Typed Throws ohne Boilerplate praxistauglich macht. Als Bonus siehst du, wie richtige Fehlerketten dein Debugging-Erlebnis dramatisch verbessern können.</p><h2 id="typed-throws-das-versprechen-und-das-problem">Typed Throws: Das Versprechen und das Problem</h2><p>Schauen wir uns zuerst an, was uns Typed Throws in Swift 6 bietet:</p><pre><code class="language-swift">// Statt nur 'throws' können wir den Fehlertyp angeben
func processFile() throws(FileError) {
    if !fileExists {
        throw FileError.fileNotFound(fileName: &quot;config.json&quot;)
    }
    // Implementierung...
}</code></pre><p>Das ermöglicht eine bessere Fehlerbehandlung an der Aufrufstelle:</p><pre><code class="language-swift">do {
    try processFile()
} catch FileError.fileNotFound(let fileName) {
    print(&quot;Could not find file: \(fileName)&quot;)
} catch FileError.readFailed {
    print(&quot;Could not read file&quot;)
}
// Kein generischer catch nötig, wenn wir alle möglichen FileError-Fälle behandelt haben!</code></pre><p>Die Vorteile liegen auf der Hand:</p><ul><li><p>Compile-time-Verifizierung der Fehlerbehandlung</p></li><li><p>Kein Type-Casting mit <code>as?</code> in Catch-Blöcken nötig</p></li><li><p>Selbstdokumentierende API, die Aufrufern genau sagt, was schiefgehen kann</p></li><li><p>IDE-Autovervollständigung für Fehlerfälle</p></li></ul><h2 id="das-verschachtelungshölle-problem">Das Verschachtelungshölle-Problem</h2><p>Das Problem entsteht bei der Arbeit mit mehrschichtigen Anwendungen. Sieh dir das an:</p><pre><code class="language-swift">// Datenbankschicht wirft DatabaseError
func fetchUser(id: String) throws(DatabaseError) {
    // Datenbankoperationen...
}

// Profilschicht muss die Datenbankschicht aufrufen
func loadUserProfile(id: String) throws(ProfileError) {
    do {
        // Problem: Dies wirft DatabaseError, nicht ProfileError
        let user = try fetchUser(id: id)
    } catch {
        // Manuelle Fehlerkonvertierung nötig
        switch error {
        case DatabaseError.recordNotFound:
            throw ProfileError.userNotFound
        default:
            throw ProfileError.databaseError(error) // Ein Wrapper-Case wird benötigt
        }
    }
}</code></pre><p>Das erzeugt mehrere Probleme:</p><ol><li><p><strong>Explosion von Wrapper-Cases</strong>:
Jeder Fehlertyp braucht Wrapper-Cases für alle möglichen Kind-Fehler</p></li><li><p><strong>Manuelles Error-Mapping</strong>:
Repetitive Do-Catch-Blöcke mit expliziter Fehlerkonvertierung</p></li><li><p><strong>Typen-Inflation</strong>:
Fehlertypen wachsen mit jeder Schicht und werden schwerer zu pflegen</p></li><li><p><strong>Verlorener Kontext</strong>:
Details über den ursprünglichen Fehler gehen bei der Übersetzung oft verloren</p></li></ol><p>Für kleine Apps mag das handhabbar sein. Für größere Apps mit vielen Schichten wird es schnell zu dem, was man als “Verschachtelungshölle” bezeichnen kann.</p><h2 id="die-lösung-das-catching-protocol">Die Lösung: Das Catching-Protocol</h2><p>ErrorKit löst das mit einem einfachen Protocol namens <code>Catching</code>:</p><pre><code class="language-swift">public protocol Catching {
    static func caught(_ error: Error) -&gt; Self
}</code></pre><p>Dieses Protocol erfordert einen einzelnen Enum-Case namens <code>caught</code>, der jeden Fehler in deinen Typ einwickelt. So verwendest du es:</p><pre><code class="language-swift">enum ProfileError: Throwable, Catching {
    case userNotFound
    case invalidProfile
    case caught(Error) // Ein einziger Case für alle anderen Fehler

    var userFriendlyMessage: String {
        switch self {
        case .userNotFound:
            return &quot;User not found.&quot;
        case .invalidProfile:
            return &quot;Profile data is invalid.&quot;
        case .caught(let error):
            // Die Meldung des eingewickelten Fehlers verwenden
            return ErrorKit.userFriendlyMessage(for: error)
        }
    }
}</code></pre><p><em>Beachte, dass <code>Throwable</code> ein Drop-in-Ersatz für <code>Error</code> ist (siehe <a href="https://www.fline.dev/swift-error-handling-done-right-overcoming-the-objective-c-error-legacy/">vorheriger Beitrag</a>).</em></p><p>Jetzt passiert die Magie mit der <code>catch</code>-Funktion, die mit dem Protocol mitgeliefert wird:</p><pre><code class="language-swift">func loadUserProfile(id: String) throws(ProfileError) {
    // Bei bekannten Fehlern direkt werfen
    guard isValidID(id) else {
        throw ProfileError.invalidInput
    }

    // Für Operationen, die andere Fehlertypen werfen können, die catch-Funktion verwenden
    let user = try ProfileError.catch {
        // Jeder hier geworfene Fehler wird automatisch
        // in ProfileError.caught(error) eingewickelt
        return try fetchUser(id: id)
    }

    // Rest der Implementierung...
}</code></pre><p><em>Beachte, dass die <code>catch</code>-Funktion zurückgibt, was du in der Closure zurückgibst.</em></p><p>Die <code>catch</code>-Funktion wickelt automatisch alle Fehler, die in ihrer Closure geworfen werden, in deinen Fehlertyp ein. Keine manuellen Do-Catch-Blöcke, kein explizites Error-Mapping – es funktioniert einfach. Sogar mehrere <code>try</code>-Ausdrücke sind möglich.</p><h2 id="das-geheimrezept-der-catch-funktion">Das Geheimrezept der catch-Funktion</h2><p>Die <code>catch</code>-Funktion ist elegant einfach:</p><pre><code class="language-swift">extension Catching {
    public static func `catch`&lt;ReturnType&gt;(
        _ operation: () throws -&gt; ReturnType
    ) throws(Self) -&gt; ReturnType {
        do {
            return try operation()
        } catch {
            throw Self.caught(error)
        }
    }
}</code></pre><p>Diese Funktion:</p><ol><li><p>Nimmt eine werfende Closure entgegen</p></li><li><p>Versucht, sie auszuführen</p></li><li><p>Gibt das Ergebnis bei Erfolg zurück</p></li><li><p>Wickelt jeden geworfenen Fehler automatisch über deinen <code>caught</code>-Case ein</p></li><li><p>Behält den Rückgabetyp der Operation bei</p></li></ol><p>Das Beste daran? Es funktioniert nahtlos mit Swift 6’s Typed Throws, erhält die Typsicherheit und eliminiert gleichzeitig Boilerplate.</p><h2 id="die-fehlerkette-für-debugging-erhalten">Die Fehlerkette für Debugging erhalten</h2><p>Einer der größten Vorteile dieses Ansatzes ist, dass er die vollständige Fehlerkette erhält. Statt Kontext zu verlieren, wenn Fehler Schichtgrenzen überschreiten, fügt jede Schicht Informationen hinzu und behält den ursprünglichen Fehler intakt.</p><p>ErrorKit nutzt das für mächtiges Debugging mit der <code>errorChainDescription(for:)</code>-Funktion:</p><pre><code class="language-swift">do {
    try await updateUserProfile()
} catch {
    print(ErrorKit.errorChainDescription(for: error))

    // Ausgabe zeigt die vollständige Kette:
    // AppError
    // └─ ProfileError
    //    └─ DatabaseError
    //       └─ FileError.notFound(path: &quot;/Users/data.db&quot;)
    //          └─ userFriendlyMessage: &quot;Could not find database file.&quot;
}</code></pre><p>Diese hierarchische Ansicht zeigt dir:</p><ol><li><p>Wo der Fehler entstanden ist (FileError)</p></li><li><p>Den genauen Weg durch deine Anwendung (FileError -&gt; DatabaseError -&gt; ProfileError -&gt; AppError)</p></li><li><p>Die konkreten Details, was schiefgelaufen ist (Datei nicht gefunden, mit dem Pfad)</p></li><li><p>Die benutzerfreundliche Meldung, die dem Nutzer angezeigt würde</p></li></ol><p>Dieses Maß an Einblick ist beim Debugging unbezahlbar, besonders bei komplexen Anwendungen, in denen Fehler tief im Call Stack entstehen können.</p><h2 id="strukturierte-fehlerketten-ausgabe">Strukturierte Fehlerketten-Ausgabe</h2><p>Die Fehlerketten-Beschreibung funktioniert durch rekursives Inspizieren der Fehlerstruktur:</p><pre><code class="language-swift">static func errorChainDescription(for error: Error) -&gt; String {
    // Rekursive Implementierung, die eine hierarchische Beschreibung aufbaut
    Self.chainDescription(for: error, enclosingType: type(of: error))
}</code></pre><p><em>Siehe <a href="https://github.com/FlineDev/ErrorKit/blob/7e37b3f788ef9bc419819f7872f23395762ce822/Sources/ErrorKit/ErrorKit.swift#L227">hier</a> für die vollständige Implementierung von <code>chainDescription</code> in ErrorKit.</em></p><p>Die Funktion nutzt Swifts Reflection-Fähigkeiten, um:</p><ol><li><p>Den Fehler über die Mirror-API zu inspizieren</p></li><li><p>Bei Fehlern, die <code>Catching</code> konformieren, den eingewickelten Fehler zu extrahieren</p></li><li><p>Bei Enum-Fehlern Case-Namen und zugehörige Werte zu erfassen</p></li><li><p>Bei Struct- oder Class-Fehlern Typ-Metadaten einzubeziehen</p></li><li><p>Alles in einer hierarchischen Baumstruktur zu formatieren</p></li></ol><p>Das liefert weit mehr Informationen als Standard-Error-Logging, besonders bei komplexen Fehlerhierarchien.</p><h2 id="eingebaute-unterstützung-in-errorkit">Eingebaute Unterstützung in ErrorKit</h2><p>Alle <a href="https://swiftpackageindex.com/FlineDev/ErrorKit/documentation/errorkit/built-in-error-types">eingebauten Fehlertypen</a> von ErrorKit (wie <code>FileError</code> oder <code>NetworkError</code>) konformieren bereits zu <code>Catching</code>, sodass du sie sofort verwenden kannst:</p><pre><code class="language-swift">func saveUserData() throws(DatabaseError) {
    // Wickelt automatisch SQLite-Fehler, Dateisystem-Fehler usw. ein
    try DatabaseError.catch {
        try database.beginTransaction()
        try database.execute(query)
        try database.commit()
    }
}</code></pre><h2 id="praxisbeispiel-eine-typische-anwendung">Praxisbeispiel: Eine typische Anwendung</h2><p>Schauen wir uns an, wie das in einem vollständigeren Beispiel funktioniert:</p><pre><code class="language-swift">// Datenzugriffsschicht
func fetchUserData(id: String) throws(DatabaseError) {
    guard database.isConnected else {
        throw DatabaseError.connectionFailed
    }

    // Hier könnten Dateisystem-Fehler auftreten
    try DatabaseError.catch {
        let query = try QueryBuilder.build(for: id)
        return try database.execute(query)
    }
}

// Geschäftslogik-Schicht
func processUserProfile(id: String) throws(ProfileError) {
    guard isValidID(id) else {
        throw ProfileError.invalidInput
    }

    // Wickelt automatisch DatabaseError ein
    let userData = try ProfileError.catch {
        return try fetchUserData(id: id)
    }

    // Nutzerdaten verarbeiten...
}

// Präsentationsschicht
func displayUserProfile(id: String) throws(UIError) {
    // Wickelt automatisch ProfileError ein (das möglicherweise DatabaseError enthält)
    let profile = try UIError.catch {
        return try processUserProfile(id: id)
    }

    // Profil anzeigen...
}</code></pre><p>Wenn eine Datenbankverbindung fehlschlägt, siehst du Folgendes in der Fehlerkette:</p><pre><code>UIError
└─ ProfileError
   └─ DatabaseError.connectionFailed
      └─ userFriendlyMessage: &quot;Unable to establish a connection to the database. Check your network settings and try again.&quot;</code></pre><p>Das zeigt dir genau, was passiert ist und wo der Fehler entstanden ist, was das Debugging erheblich erleichtert. Der zusätzliche Kontext kann dir den entscheidenden Hinweis geben, um das Problem zu beheben!</p><h2 id="fazit">Fazit</h2><p>Swift 6’s Typed Throws ist eine mächtige Ergänzung der Sprache, bringt aber Herausforderungen bei der Fehlerweiterleitung über Schichten hinweg mit sich. Das <code>Catching</code>-Protocol bietet eine einfache, elegante Lösung, die Typsicherheit erhält und gleichzeitig Boilerplate eliminiert.</p><p>Kombiniert mit ErrorKits <code>errorChainDescription</code>-Funktion wird Fehlerbehandlung zu einem mächtigen Debugging-Werkzeug. Nutze ErrorKit jetzt und profitiere von vielen weiteren Verbesserungen, die Fehlerbehandlung in Swift in realen Apps nützlicher machen:</p><p><a class="sk-link-card sk-link-card-github" href="https://github.com/FlineDev/ErrorKit?ref=fline.dev"><span class="sk-link-card-source"><svg viewBox="0 0 16 16" width="16" height="16" fill="currentColor"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/></svg><span>github.com</span></span><span class="sk-link-card-title">FlineDev / ErrorKit</span><span class="sk-link-card-description">Simplified error handling with built-in user-friendly messages for common errors. Fully localized. Community-driven</span></a></p><p>Hast du schon angefangen, Swift 6’s Typed Throws zu nutzen? Wie gehst du mit der Fehlerweiterleitung über Schichten in deinen Apps um? Schreib mir auf den sozialen Kanälen (Links unten)!</p><h3 id="vorheriger-artikel-in-dieser-serie">Vorheriger Artikel in dieser Serie:</h3><ol><li><p><a href="https://www.fline.dev/swift-error-handling-done-right-overcoming-the-objective-c-error-legacy/">Swift Error Handling Done Right: Overcoming the ObjC Legacy</a></p></li></ol><h3 id="folgende-artikel-in-dieser-serie">Folgende Artikel in dieser Serie:</h3><ol><li><p><a href="https://www.fline.dev/better-error-reporting-in-swift-apps-automatic-logs-analytics/">Better Error Reporting in Swift Apps: Automatic Logs + Analytics</a></p></li><li><p><a href="https://www.fline.dev/making-swift-error-messages-human-friendly-together/">Making Swift Error Messages Human-Friendly—Together</a></p></li></ol>]]></content:encoded>
</item>
<item>
<title>Swift Error Handling richtig gemacht: Das Objective-C-Erbe überwinden</title>
<link>https://fline.dev/de/blog/swift-error-handling-done-right-overcoming-the-objective-c-error-legacy/</link>
<guid isPermaLink="true">https://fline.dev/de/blog/swift-error-handling-done-right-overcoming-the-objective-c-error-legacy/</guid>
<pubDate>Mon, 21 Apr 2025 00:00:00 +0000</pubDate>
<description><![CDATA[Genervt von kryptischen Swift-Fehlermeldungen wie '(YourError error 0)'? So behebst du das Problem ein für alle Mal – mit Klarheit und Eleganz.]]></description>
<content:encoded><![CDATA[<p>Hast du schon mal sorgfältig Fehlermeldungen in deiner Swift-App formuliert, nur um festzustellen, dass sie nie wirklich angezeigt werden? Stattdessen sehen deine Nutzer (oder du beim Debuggen) kryptische Meldungen wie:</p><blockquote><p>“The operation couldn’t be completed. (YourApp.YourError error 0.)”</p></blockquote><p>Dann bist du nicht allein. Dieses verwirrende Verhalten bringt Swift-Entwickler – von Anfängern bis zu Experten – seit der Einführung der Sprache zur Verzweiflung. Heute möchte ich erklären, warum das passiert, und eine Lösung vorstellen, die Swift-Fehlerbehandlung intuitiver macht.</p><h2 id="das-überraschende-verhalten-von-swifts-error-protocol">Das überraschende Verhalten von Swifts Error-Protocol</h2><p>Schauen wir uns ein einfaches Beispiel an, das das Problem demonstriert:</p><pre><code class="language-swift">enum NetworkError: Error {
   case noConnectionToServer
   case parsingFailed

   var localizedDescription: String {
      switch self {
      case .noConnectionToServer:
         return &quot;No connection to the server.&quot;
      case .parsingFailed:
         return &quot;Data parsing failed.&quot;
      }
   }
}

// Verwendung des Fehlers
do {
   throw NetworkError.noConnectionToServer
} catch {
   print(&quot;Error message: \(error.localizedDescription)&quot;)
   // Erwartet: &quot;No connection to the server.&quot;
   // Tatsächlich: &quot;The operation couldn't be completed. (AppName.NetworkError error 0.)&quot;
}</code></pre><p>Was ist schiefgelaufen? Wir haben eine klare Fehlermeldung definiert, aber Swift hat sie komplett ignoriert!</p><h2 id="warum-das-passiert-die-nserror-bridge">Warum das passiert: Die NSError-Bridge</h2><p>Dieses verwirrende Verhalten entsteht, weil Swifts <code>Error</code>-Protocol unter der Haube auf Objective-Cs <code>NSError</code>-Klasse gemappt wird. Wenn du auf <code>localizedDescription</code> zugreifst, verwendet Swift nicht deine Property – es erstellt ein <code>NSError</code> mit einer Domain (deinem Modulnamen), einem Code (dem Integer-Wert des Enum-Case) und einer Standardmeldung.</p><p>Dieses Design mag für Objective-C-Interoperabilität sinnvoll sein, aber es erzeugt eine schreckliche Entwicklererfahrung, besonders für Swift-Neulinge.</p><h2 id="die-offizielle-lösung-localizederror">Die “offizielle” Lösung: LocalizedError</h2><p>Swift bietet doch eine offizielle Lösung an: das <code>LocalizedError</code>-Protocol. So soll man es verwenden:</p><pre><code class="language-swift">enum NetworkError: LocalizedError {
   case noConnectionToServer
   case parsingFailed

   var errorDescription: String? { // Hinweis: Optionaler String
      switch self {
      case .noConnectionToServer:
         return &quot;No connection to the server.&quot;
      case .parsingFailed:
         return &quot;Data parsing failed.&quot;
      }
   }

   // Es gibt auch diese optionalen Properties, die selten genutzt werden
   var failureReason: String? { return nil }
   var recoverySuggestion: String? { return nil }
   var helpAnchor: String? { return nil }
}</code></pre><p>Das funktioniert zwar, hat aber mehrere Probleme:</p><ul><li><p>Alle Properties sind <strong>optional</strong> (<code>String?</code>), also hilft dir der Compiler nicht, wenn du einen Fall vergisst</p></li><li><p>Nur <code>errorDescription</code> beeinflusst <code>localizedDescription</code>; die anderen Properties werden oft ignoriert</p></li><li><p>Die Benennung macht nicht klar, welche Property die angezeigte Meldung beeinflusst</p></li><li><p>Es nutzt immer noch einen Legacy-Ansatz basierend auf Cocoa-Fehlerbehandlungsmustern</p></li></ul><h2 id="eine-bessere-lösung-das-throwable-protocol">Eine bessere Lösung: Das Throwable-Protocol</h2><p>Nachdem mich diese Frustration zu oft getroffen hat, habe ich als Teil von <a href="https://github.com/FlineDev/ErrorKit?ref=fline.dev">ErrorKit</a> eine einfachere Lösung geschaffen – ein Protocol namens <code>Throwable</code>:</p><pre><code class="language-swift">public protocol Throwable: LocalizedError {
   var userFriendlyMessage: String { get }
}</code></pre><p>Dieses Protocol hat mehrere Vorteile:</p><ul><li><p>Es hat eine <strong>einzige, nicht-optionale Anforderung</strong> – kein Vergessen von Fällen mehr</p></li><li><p>Der Name <code>userFriendlyMessage</code> drückt die Absicht klar aus</p></li><li><p>Es erweitert <code>LocalizedError</code> für Kompatibilität (kein Mehraufwand für dich!)</p></li><li><p>Es folgt Swifts Namenskonventionen mit dem <code>-able</code>-Suffix</p></li></ul><p>So verwendest du es:</p><pre><code class="language-swift">enum NetworkError: Throwable {
   case noConnectionToServer
   case parsingFailed

   var userFriendlyMessage: String {
      switch self {
      case .noConnectionToServer:
         return &quot;Unable to connect to the server.&quot;
      case .parsingFailed:
         return &quot;Data parsing failed.&quot;
      }
   }
}

// Verwendung des Fehlers
do {
   throw NetworkError.noConnectionToServer
} catch {
   print(&quot;Error message: \(error.localizedDescription)&quot;)
   // Zeigt jetzt korrekt: &quot;Unable to connect to the server.&quot;
}</code></pre><p>Mit <code>Throwable</code> bekommst du genau das, was du erwartest – deine Fehlermeldungen erscheinen exakt wie beabsichtigt, ohne Überraschungen.</p><h2 id="schnelle-entwicklung-mit-string-raw-values">Schnelle Entwicklung mit String Raw Values</h2><p>Für schnelles Prototyping funktioniert <code>Throwable</code> auch nahtlos mit String Raw Values:</p><pre><code class="language-swift">enum NetworkError: String, Throwable {
   case noConnectionToServer = &quot;Unable to connect to the server.&quot;
   case parsingFailed = &quot;Data parsing failed.&quot;
}

// Das war's! Keine zusätzliche Implementierung nötig</code></pre><p>Die Raw-String-Werte werden automatisch zu deinen Fehlermeldungen – ganz ohne Boilerplate während der frühen Entwicklung. Später, wenn du bereit für richtige Lokalisierung bist, kannst du auf <code>String(localized:)</code> in einer vollständigen Implementierung von <code>userFriendlyMessage</code> umsteigen.</p><h2 id="fertige-fehlertypen">Fertige Fehlertypen</h2><p>Um noch mehr Boilerplate zu vermeiden, enthält ErrorKit vordefinierte Fehlertypen für häufige Szenarien:</p><pre><code class="language-swift">func fetchData() async throws {
    guard isNetworkAvailable else {
        throw NetworkError.noInternet
    }

    guard let url = URL(string: path) else {
        throw ValidationError.invalidInput(field: &quot;URL path&quot;)
    }

    // Weitere Implementierung...
}</code></pre><p>Diese eingebauten Typen umfassen:</p><ul><li><p><code>NetworkError</code> für Konnektivitäts- und API-Probleme</p></li><li><p><code>FileError</code> für Dateisystem-Operationen</p></li><li><p><code>DatabaseError</code> für Datenpersistenz-Probleme</p></li><li><p><code>ValidationError</code> für Eingabevalidierung</p></li><li><p><code>PermissionError</code> für Autorisierungsprobleme</p></li><li><p>Und einige mehr…</p></li></ul><p>Jeder eingebaute Typ konformiert bereits zu <code>Throwable</code> und liefert lokalisierte, benutzerfreundliche Meldungen direkt mit – das spart dir Zeit bei gleichzeitiger Klarheit.</p><h2 id="schnelle-einmal-fehler-mit-genericerror">Schnelle Einmal-Fehler mit GenericError</h2><p>Für Situationen, in denen du eine individuelle Meldung brauchst, ohne gleich einen neuen Fehlertyp zu definieren, bietet ErrorKit <code>GenericError</code>:</p><pre><code class="language-swift">func quickOperation() throws {
    guard condition else {
        throw GenericError(userFriendlyMessage: &quot;The operation couldn't be completed because a specific condition wasn't met.&quot;)
    }

    // Weitere Implementierung...
}</code></pre><p>Das ist perfekt für die frühe Entwicklung oder einzigartige Fehlerfälle, die keinen eigenen Fehlertyp rechtfertigen.</p><h2 id="vorteile-über-bessere-meldungen-hinaus">Vorteile über bessere Meldungen hinaus</h2><p>Die Nutzung von <code>Throwable</code> behebt nicht nur Fehlermeldungen – es bringt mehrere zusätzliche Vorteile:</p><ol><li><p><strong>Klarheit für neue Entwickler</strong>:
Das Protocol zeigt klar, wie Fehlermeldungen definiert werden</p></li><li><p><strong>Compile-time-Sicherheit</strong>:
Die nicht-optionale Anforderung stellt sicher, dass alle Fälle Meldungen haben</p></li><li><p><strong>Lokalisierungssupport</strong>:
Funktioniert perfekt mit <code>String(localized:)</code> für Internationalisierung</p></li><li><p><strong>Weniger Boilerplate</strong>:
Besonders mit Raw-String-Werten und eingebauten Typen</p></li><li><p><strong>Verbesserte Nutzererfahrung</strong>:
Klare Fehlermeldungen helfen Nutzern zu verstehen, was schiefgelaufen ist</p></li><li><p><strong>Besseres Debugging</strong>:
Aussagekräftige Fehlermeldungen machen das Debuggen schneller</p></li></ol><h2 id="der-umstieg">Der Umstieg</h2><p>Das Beste daran? <code>Throwable</code> ist ein Drop-in-Ersatz für <code>Error</code>:</p><pre><code class="language-swift">// Vorher
enum AppError: Error {
    case configurationFailed
}

// Nachher
enum AppError: Throwable {
    case configurationFailed

    var userFriendlyMessage: String {
        switch self {
        case .configurationFailed:
           return &quot;Failed to load configuration.&quot;
        }
    }
}</code></pre><p>Bestehender Code mit <code>throws</code>, <code>do</code>/<code>catch</code> und anderen Fehlerbehandlungsmustern funktioniert exakt gleich – der einzige Unterschied ist, dass deine Fehlermeldungen jetzt tatsächlich wie beabsichtigt angezeigt werden.</p><h2 id="fazit">Fazit</h2><p>Swifts Fehlerbehandlung ist leistungsstark, aber die Meldungsverarbeitung ist viel zu lange ein verwirrender Schmerzpunkt gewesen. Das <code>Throwable</code>-Protocol bietet eine einfache, intuitive Lösung, die mit Swifts Designprinzipien übereinstimmt und gleichzeitig ein langjähriges Problem behebt.</p><p>Indem du <code>Throwable</code> für deine Fehlertypen verwendest, bekommst du klarere Fehlermeldungen, weniger Boilerplate und eine intuitivere Entwicklererfahrung. Zusammen mit den eingebauten Fehlertypen und dem <code>GenericError</code>-Fallback ergibt das einen umfassenden Ansatz für Fehlerbehandlung, der so funktioniert, wie du es erwarten würdest.</p><p>Wenn du diesen Ansatz in deinen eigenen Projekten ausprobieren möchtest, schau dir ErrorKit an, das das <code>Throwable</code>-Protocol, eingebaute Fehlertypen und viele weitere Verbesserungen für die Swift-Fehlerbehandlung enthält:</p><p><a class="sk-link-card sk-link-card-github" href="https://github.com/FlineDev/ErrorKit?ref=fline.dev"><span class="sk-link-card-source"><svg viewBox="0 0 16 16" width="16" height="16" fill="currentColor"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/></svg><span>github.com</span></span><span class="sk-link-card-title">FlineDev / ErrorKit</span><span class="sk-link-card-description">Simplified error handling with built-in user-friendly messages for common errors. Fully localized. Community-driven</span></a></p><p>Bist du auch schon auf diese Verwirrung mit Fehlermeldungen in deiner Swift-Entwicklung gestoßen? Wie hast du das gelöst? Schreib mir auf den sozialen Kanälen (Links unten)!</p><h3 id="folgende-artikel-in-dieser-serie">Folgende Artikel in dieser Serie:</h3><ol><li><p><a href="https://www.fline.dev/swift-6-typed-throws-error-chains/">Unlocking the Power of Swift 6’s Typed Throws with Error Chains</a></p></li><li><p><a href="https://www.fline.dev/better-error-reporting-in-swift-apps-automatic-logs-analytics/">Better Error Reporting in Swift Apps: Automatic Logs + Analytics</a></p></li><li><p><a href="https://www.fline.dev/making-swift-error-messages-human-friendly-together/">Making Swift Error Messages Human-Friendly—Together</a></p></li></ol>]]></content:encoded>
</item>
<item>
<title>Swift-Macro-Vertrauensprobleme in Xcode Cloud Builds lösen</title>
<link>https://fline.dev/de/blog/solving-swift-macro-trust-issues-in-xcode-cloud-builds/</link>
<guid isPermaLink="true">https://fline.dev/de/blog/solving-swift-macro-trust-issues-in-xcode-cloud-builds/</guid>
<pubDate>Thu, 20 Mar 2025 00:00:00 +0000</pubDate>
<description><![CDATA[Swift Macros sind mächtig, können aber deine CI-Pipeline mit Vertrauensfehlern lahmlegen. Erfahre, wie du mit einem einfachen Post-Clone-Skript den Fehler "Target must be enabled" in Xcode Cloud ein für alle Mal löst.]]></description>
<content:encoded><![CDATA[<h2 id="das-problem-mit-swift-macros-in-ci-umgebungen">Das Problem mit Swift Macros in CI-Umgebungen</h2><p>Swift Macros, eingeführt in Xcode 15 mit Swift 5.9, sind ein mächtiges Feature, das Code-Generierung und Metaprogrammierung zur Compile-Zeit ermöglicht. Während sie hervorragende Produktivitätsvorteile bieten, haben sie eine neue Herausforderung für CI/CD-Pipelines geschaffen – insbesondere in Xcode Cloud.</p><p>Wenn du ein Macro zum ersten Mal lokal verwendest, zeigt Xcode einen Dialog an, der dich auffordert, dem Package des Macros zu vertrauen, bevor es ausgeführt werden kann. Das funktioniert auf deinem Entwicklungsrechner problemlos, wird aber in automatisierten Umgebungen wie Xcode Cloud zum Problem, wo es eben keine Möglichkeit gibt, auf “OK” in einem Dialogfenster zu klicken.</p><p>Wenn du schon mal versucht hast, ein Projekt mit Swift Macros in Xcode Cloud zu bauen, bist du wahrscheinlich auf diesen frustrierenden Fehler gestoßen:</p><pre><code>Target must be enabled before it can be used.</code></pre><h2 id="die-vollständige-lösung">Die vollständige Lösung</h2><p>Nach einiger Recherche habe ich eine zuverlässige Lösung gefunden, die bei Xcode Cloud Builds mit Swift Macros konsistent funktioniert. So implementierst du sie:</p><h3 id="schritt-1-einen-ci-scripts-ordner-erstellen">Schritt 1: Einen CI-Scripts-Ordner erstellen</h3><p>Erstelle zunächst einen eigenen Ordner für unsere CI-Skripte:</p><ol><li><p>Rechtsklick auf dein Projekt in Xcode</p></li><li><p>Wähle “New Group”</p></li><li><p>Benenne ihn <code>ci_scripts</code></p></li></ol><h3 id="schritt-2-das-post-clone-skript-erstellen">Schritt 2: Das Post-Clone-Skript erstellen</h3><p>Als Nächstes erstellen wir ein Post-Clone-Skript, das Xcode Cloud automatisch nach dem Klonen deines Repositories ausführt:</p><ol><li><p>Rechtsklick auf den Ordner <code>ci_scripts</code></p></li><li><p>Wähle “New File” -&gt; “Empty File”</p></li><li><p>Benenne es <code>ci_post_clone.sh</code></p></li></ol><h3 id="schritt-3-den-skriptinhalt-hinzufügen">Schritt 3: Den Skriptinhalt hinzufügen</h3><p>Kopiere den folgenden Code in deine neue Datei <code>ci_post_clone.sh</code>:</p><pre><code class="language-shell">#!/bin/sh

# Bei Fehler abbrechen (-e), undefinierte Variablen (-u) und Pipeline-Fehler (-o pipefail)
set -euo pipefail

# Xcode-Macro-Fingerprint-Validierung deaktivieren, um fehlerhafte Build-Fehler zu vermeiden
defaults write com.apple.dt.Xcode IDESkipMacroFingerprintValidation -bool YES</code></pre><p>Dieses Skript tut eine entscheidende Sache: Es deaktiviert Xcodes Macro-Fingerprint-Validierung – also genau das, was den Vertrauensdialog überhaupt auslöst.</p><h3 id="schritt-4-das-skript-ausführbar-machen">Schritt 4: Das Skript ausführbar machen</h3><p>Öffne das Terminal, navigiere zu deinem Projektordner und führe aus:</p><pre><code class="language-bash">chmod +x ci_scripts/ci_post_clone.sh</code></pre><p>Dieser Befehl macht dein Skript ausführbar, was erforderlich ist, damit Xcode Cloud es korrekt ausführen kann.</p><h3 id="schritt-5-committen-und-einen-build-auslösen">Schritt 5: Committen und einen Build auslösen</h3><p>Committe die neue Datei in dein Repository und pushe die Änderungen. Das löst einen neuen Build in Xcode Cloud aus, der jetzt ohne den Macro-Vertrauensfehler erfolgreich durchlaufen sollte.</p><h2 id="so-sieht-es-aus">So sieht es aus</h2><p>Wenn alles korrekt eingerichtet ist, sollte deine Projektstruktur den CI-Scripts-Ordner mit dem Post-Clone-Skript enthalten:</p><p><img src="/assets/images/blog/solving-swift-macro-trust-issues-in-xcode-cloud-builds/3x-hu-f-yl-d.webp" alt="3x hu f yl d" loading="lazy" /></p><h2 id="warum-das-funktioniert">Warum das funktioniert</h2><p>Xcode Cloud führt automatisch jedes Skript mit dem Namen <code>ci_post_clone.sh</code> aus, das sich in einem <code>ci_scripts</code>-Verzeichnis im Root deines Projekts befindet. Unser Skript ändert die Xcode-Einstellungen, um die Macro-Fingerprint-Validierung zu deaktivieren und so die Vertrauensanforderung zu umgehen.</p><p>Der Befehl <code>defaults write com.apple.dt.Xcode IDESkipMacroFingerprintValidation -bool YES</code> ändert eine spezifische Xcode-Einstellung, die steuert, ob Macros explizit vertraut werden muss, bevor sie verwendet werden können.</p><h2 id="sicherheitsaspekte">Sicherheitsaspekte</h2><p>Manche Entwickler schlagen vor, die Datei <code>macros.json</code> (die vertrauenswürdige Macro-Fingerprints enthält) von deinem lokalen Rechner in die CI-Umgebung zu kopieren. Ich bin allerdings der Meinung, dass dieser Ansatz nicht unbedingt sicherer ist als die Validierung einfach zu deaktivieren.</p><p>Der entscheidende Punkt ist hier der Kontext: Xcode Cloud läuft in einer Sandbox-Umgebung, die speziell zum Bauen deiner Anwendung dient. Es führt keinen beliebigen Code auf Produktionssystemen aus. Wenn du deinem Entwicklungsteam und deinem Release-Workflow nicht vertrauen kannst, hast du wahrscheinlich schwerwiegendere Sicherheitsprobleme als Macros, die in einer CI-Umgebung laufen.</p><h2 id="die-wachsende-bedeutung-dieser-lösung">Die wachsende Bedeutung dieser Lösung</h2><p>Da Swift sich stetig weiterentwickelt, implementieren immer mehr Third-Party-Packages Macros, um leistungsstarke Features mit minimalem Boilerplate zu bieten. Zum Beispiel:</p><ul><li><p><a href="https://github.com/FlineDev/TranslateKit"><strong>TranslateKit SDK</strong></a> bietet Lokalisierungs-Macros</p></li><li><p>Verschiedene Logging- und Debugging-Bibliotheken setzen auf Macros</p></li><li><p>UI-Frameworks nutzen Macros, um View-Deklarationen zu vereinfachen</p></li></ul><p>Dieser Trend wird sich nur beschleunigen, da Entwickler immer neue Einsatzmöglichkeiten für Compile-Zeit-Metaprogrammierung entdecken. Eine zuverlässige Lösung für den Umgang mit Macros in CI-Pipelines wird für Swift-Projekte zunehmend wichtig.</p><p>Jetzt, da du das Macro-Vertrauensproblem gelöst hast, kannst du die Möglichkeiten von Xcode Cloud voll ausschöpfen. Ich werde in meinem nächsten Artikel und dem dazugehörigen Video zeigen, wie man Xcode Cloud für App-Store-Deployments nutzt. Dieser Ansatz kann dir wertvolle Zeit beim Warten auf Builds sparen, besonders wenn du wie ich auf mehreren Apple-Plattformen (iOS, macOS, visionOS) veröffentlichst. Bleib dran und viel Spaß beim Coden!</p>]]></content:encoded>
</item>
<item>
<title>Wenn Gesherlocked-Werden zu etwas Besserem führt: Die TranslateKit-Reise</title>
<link>https://fline.dev/de/blog/sherlocked-to-success/</link>
<guid isPermaLink="true">https://fline.dev/de/blog/sherlocked-to-success/</guid>
<pubDate>Wed, 19 Feb 2025 00:00:00 +0000</pubDate>
<description><![CDATA[Eine ehrliche Geschichte über Widerstandsfähigkeit in der Indie-App-Entwicklung – und warum der schlimmste Tag deines Entwicklerlebens manchmal dazu führen kann, etwas Besseres zu bauen.]]></description>
<content:encoded><![CDATA[<p>Als Apple auf der WWDC 2023 String Catalogs vorstellte, sank mir das Herz. Ich hatte gerade ein ganzes Jahr damit verbracht, RemafoX zu bauen, eine Xcode-Extension für App-Lokalisierung, nur um zu sehen, wie ein Großteil der Funktionalität Teil von Xcode selbst wurde. Als Indie-Entwickler war das ein erheblicher Rückschlag – RemafoX sollte meine Flaggschiff-App werden, nachdem ich 2022 den Schritt in die Vollzeit-Selbstständigkeit gewagt hatte.</p><p>Doch als ich mich tiefer in String Catalogs einarbeitete, fiel mir etwas Interessantes auf: Apples tiefe SwiftUI-Integration eröffnete Möglichkeiten, die über Xcode-Extensions nicht erreichbar waren. Anstatt gegen den Strom zu schwimmen, entschied ich mich, ihn zu nutzen. An einem einzigen Wochenende baute ich ein einfaches Tool, das String Catalogs parsen und maschinelle Übersetzungen durchführen konnte. Dieses Tool wurde TranslateKit.</p><h2 id="eine-unerwartete-erfolgsgeschichte">Eine unerwartete Erfolgsgeschichte</h2><p>Was als Wochenendprojekt für den Eigenbedarf begann, wurde schnell zu meiner bisher erfolgreichsten App. Entwickler weltweit schätzten TranslateKits Einfachheit und Effizienz. Das Feedback übertraf meine Erwartungen für eine so schnell geschriebene App, und damit kamen wertvolle Erkenntnisse darüber, was Entwickler von einem Lokalisierungstool wirklich brauchen. Erst kürzlich teilte ein Indie-Entwicklerkollege auf Bluesky:</p><blockquote class="bluesky-embed" data-bluesky-uri="at://did:plc:fsec6poqfwy2eammskvojusd/app.bsky.feed.post/3lhbuy4wqzc2j" data-bluesky-cid="bafyreiftdfsq2brqeyku66zlpf3ut3tk674gh6wt32v63fq5gze4vqkcgi"><p lang="en">1/ I've always wanted to localize @glusight.app, but I initially thought it'd require significant refactoring, which I wasn't prepared for. However, my new project, setup with localization from the start made it clear how easy Apple and tools like @translatekit.app make the process.
<p>#BuildInPublic</p>— <a href="https://bsky.app/profile/did:plc:fsec6poqfwy2eammskvojusd?ref_src=embed&ref=fline.dev">slowbrewed.studio (@creativewith.in)</a> <a href="https://bsky.app/profile/did:plc:fsec6poqfwy2eammskvojusd/post/3lhbuy4wqzc2j?ref_src=embed&ref=fline.dev">2025-02-03T15:42:08.641Z</a></blockquote><script async="" src="https://embed.bsky.app/static/embed.js" charset="utf-8"></script></p><p>Das hat mich sehr angesprochen. Viele Entwickler <strong>überschätzen den Aufwand, der nötig ist, um ihre SwiftUI-Apps zu lokalisieren</strong>. Mit den heutigen Tools ist es überraschend unkompliziert, auf 90 % Lokalisierungsabdeckung zu kommen, und die letzten 10 % werden einfacher denn je.</p><p>Bedenke mal: Etwa <strong>80 % der Weltbevölkerung sprechen kein Englisch</strong>. Fehlende Lokalisierung ist statistisch gesehen weltweit der Grund Nummer eins, warum eine App für Nutzer nicht zugänglich ist. Jede App sollte lokalisiert werden – es geht nicht nur darum, mehr Nutzer zu erreichen, sondern darum, deine App wirklich zugänglich zu machen. Und ich habe es mir zur Mission gemacht, das für Indie-Entwickler so einfach wie möglich zu machen.</p><h2 id="die-weiterentwicklung-zu-translatekit-3">Die Weiterentwicklung zu TranslateKit 3</h2><p>Nach einem Jahr des Lernens aus Nutzerfeedback wurde mir klar, dass ich es viel besser machen könnte. Der größte Schmerzpunkt? Das Einrichten von API-Keys. Entwickler mussten sich bei verschiedenen Übersetzungsdiensten registrieren, mit Kreditkarten hantieren und API-Kontingente verwalten. Das war <strong>zu viel Reibung</strong> für etwas, das ein einfacher Prozess sein sollte.</p><p>Dann gab es die Probleme mit der <strong>Übersetzungsqualität</strong>. Meine Integrationen für maschinelle Übersetzung waren zwar gut, aber sie verfehlten oft den entscheidenden Kontext, weil sie jeden String isoliert übersetzten. Ein Button, der auf Englisch perfekt Sinn ergab, konnte in anderen Sprachen umständlich lang oder völlig am Kontext vorbei übersetzt werden. Durch die Verarbeitung von Strings in Batches und die Bereitstellung des vollständigen App-Kontexts für die KI gewährleistet TranslateKit 3 Konsistenz und einen angemessenen Ton in der gesamten App.</p><p>Projektweites Management war eine weitere wichtige Verbesserung. Zuvor mussten Entwickler daran denken, ihre <code>InfoPlist.xcstrings</code>-Datei separat per Drag-and-Drop hinzuzufügen – was dazu führte, dass Apps zwar lokalisiert waren, aber Berechtigungsdialoge immer noch auf Englisch erschienen. Jetzt kümmert sich TranslateKit um dein gesamtes Projekt auf einmal und stellt sicher, dass <strong>nichts übersehen wird</strong>.</p><p>Das führte zu einem kompletten Neuaufbau, fokussiert auf das, was Entwicklern am wichtigsten ist:</p><ol><li><p><strong>Kein Setup</strong> nötig – keine API-Keys oder Service-Registrierungen mehr</p></li><li><p><strong>Kontextbewusste</strong> KI-Übersetzungen, die den Zweck deiner App verstehen</p></li><li><p><strong>Projektweite</strong> Lokalisierung, die alles erfasst, einschließlich Berechtigungstexte</p></li><li><p>Intelligenter Umgang mit <strong>Markenbegriffen und Terminologie</strong> deiner App</p></li><li><p>Unterstützung von <strong>sprachspezifischen Nuancen</strong> wie Formalität und kulturellem Kontext</p></li></ol><p>Die Ergebnisse? Übersetzungsfehler wurden im Vergleich zu herkömmlichen Diensten um etwa 90 % reduziert. Darüber hinaus führt TranslateKit 3 KI-<strong>Korrekturlesen</strong> ein – ein einzigartiges Feature, das bestehende Übersetzungen verbessern kann, egal ob sie von einer älteren Version von TranslateKit oder einer anderen Quelle stammen. Wähle einfach die zu prüfenden Sprachen aus und lass die KI deine Übersetzungen für noch bessere Genauigkeit feintunen.</p><p>Überzeuge dich selbst davon, was Noah, der Entwickler hinter <a href="https://proxyman.com/">Proxyman</a>, sagt:</p><blockquote class="twitter-tweet"><p lang="en" dir="ltr">love this feature. <br><br>Meanwhile, Google Translate is so stupid, it tried to translate universal terms like HEAD, GET, Proxy, REST ... to Chinese, which is completely wrong <a href="https://t.co/YaxCan3kUH?ref=fline.dev">pic.twitter.com/YaxCan3kUH</a></p>— Noah Tran (@_nghiatran) <a href="https://twitter.com/_nghiatran/status/1891439992251007310?ref_src=twsrc%5Etfw&ref=fline.dev">February 17, 2025</a></blockquote>
<script async="" src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
<p><em>Noah über die Lokalisierung seiner sehr großen App Proxyman.</em></p><p>Und mit dem neuen Pay-what-you-need-Preismodell ist Lokalisierung endlich für <em>alle</em> Indie-Entwickler erschwinglich. Du kannst das Wasser testen, indem du eine einzelne Sprache für nur 1 $ hinzufügst (mit einem kostenlosen ersten Monat zum Ausprobieren), oder gleich voll einsteigen und 5x mehr Nutzer erreichen, indem du die Top 10 Sprachen einer durchschnittlich großen App in nur 4 Minuten hinzufügst – für weniger als 5 $. Ich hoffe, das macht App-Lokalisierung so erschwinglich wie möglich und beseitigt eine weitere Hürde für Entwickler, die darüber nachdenken.</p><h2 id="über-ios-hinausdenken">Über iOS hinausdenken</h2><p>Aber dabei habe ich nicht aufgehört. Wie ich in meinem <a href="https://www.fline.dev/swift-localization-in-2025-best-practices-you-couldnt-use-before/">kürzlichen Beitrag</a> über das <a href="https://github.com/FlineDev/TranslateKit">TranslateKit SDK</a> geteilt habe, können SwiftUI-Entwickler jetzt von automatischer Key-Generierung mit dem <code>#tk</code>-Macro profitieren und auf über 2.000 vorlokalisierte gängige UI-Texte zugreifen – mit einem Aufruf wie <code>TK.Action.cancel</code>. Beides wird die Genauigkeit der Lokalisierung weiter verbessern. Und obwohl iOS-Entwicklung immer mein Hauptfokus bleiben wird, habe ich TranslateKits Kern-Übersetzungssystem plattformunabhängig konzipiert und damit den Weg für die Unterstützung anderer App-Plattformen geebnet – Android und Flutter als Erste.</p><p>Ich arbeite gerade an den letzten Details eines umfassenden Video-Guides, der zeigen wird, wie unkompliziert App-Lokalisierung inzwischen geworden ist. Denn manchmal hilft eben nur Sehen, um zu glauben – und ich möchte, dass jeder Entwickler weiß, dass er seine App global machen kann, ohne die Kopfschmerzen, die er vielleicht erwartet. Vorerst muss dieses 50-Sekunden-Video genügen:</p><div class="kg-card kg-video-card kg-width-wide kg-card-hascaption" data-kg-thumbnail="https://www.fline.dev/content/media/2025/02/TranslateKit-3-Trailer_thumb.webp" data-kg-custom-thumbnail="">
<div class="kg-video-container">
<video src="/assets/images/blog/sherlocked-to-success/translate-kit-3-trailer.mp4" poster="https://img.spacergif.org/v1/1280x720/0a/spacer.webp" width="1280" height="720" loop="" autoplay="" muted="" playsinline="" preload="metadata" style="background: transparent url('/assets/images/blog/sherlocked-to-success/trailer-thumb.webp') 50% 50% / cover no-repeat;"></video>
<div class="kg-video-overlay">
<button class="kg-video-large-play-icon" aria-label="Play video">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M23.14 10.608 2.253.164A1.559 1.559 0 0 0 0 1.557v20.887a1.558 1.558 0 0 0 2.253 1.392L23.14 13.393a1.557 1.557 0 0 0 0-2.785Z"></path>
</svg>
</button>
</div>
<div class="kg-video-player-container kg-video-hide">
<div class="kg-video-player">
<button class="kg-video-play-icon" aria-label="Play video">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M23.14 10.608 2.253.164A1.559 1.559 0 0 0 0 1.557v20.887a1.558 1.558 0 0 0 2.253 1.392L23.14 13.393a1.557 1.557 0 0 0 0-2.785Z"></path>
</svg>
</button>
<button class="kg-video-pause-icon kg-video-hide" aria-label="Pause video">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<rect x="3" y="1" width="7" height="22" rx="1.5" ry="1.5"></rect>
<rect x="14" y="1" width="7" height="22" rx="1.5" ry="1.5"></rect>
</svg>
</button>
<span class="kg-video-current-time">0:00</span>
<div class="kg-video-time">
/<span class="kg-video-duration">0:52</span>
</div>
<input type="range" class="kg-video-seek-slider" max="100" value="0">
<button class="kg-video-playback-rate" aria-label="Adjust playback speed">1x</button>
<button class="kg-video-unmute-icon" aria-label="Unmute">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M15.189 2.021a9.728 9.728 0 0 0-7.924 4.85.249.249 0 0 1-.221.133H5.25a3 3 0 0 0-3 3v2a3 3 0 0 0 3 3h1.794a.249.249 0 0 1 .221.133 9.73 9.73 0 0 0 7.924 4.85h.06a1 1 0 0 0 1-1V3.02a1 1 0 0 0-1.06-.998Z"></path>
</svg>
</button>
<button class="kg-video-mute-icon kg-video-hide" aria-label="Mute">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M16.177 4.3a.248.248 0 0 0 .073-.176v-1.1a1 1 0 0 0-1.061-1 9.728 9.728 0 0 0-7.924 4.85.249.249 0 0 1-.221.133H5.25a3 3 0 0 0-3 3v2a3 3 0 0 0 3 3h.114a.251.251 0 0 0 .177-.073ZM23.707 1.706A1 1 0 0 0 22.293.292l-22 22a1 1 0 0 0 0 1.414l.009.009a1 1 0 0 0 1.405-.009l6.63-6.631A.251.251 0 0 1 8.515 17a.245.245 0 0 1 .177.075 10.081 10.081 0 0 0 6.5 2.92 1 1 0 0 0 1.061-1V9.266a.247.247 0 0 1 .073-.176Z"></path>
</svg>
</button>
<input type="range" class="kg-video-volume-slider" max="100" value="100">
</div>
</div>
</div>
<figcaption><p>Kurze Demo der Lokalisierung einer App mit TranslateKit 3</p></figcaption>
</div>
<h2 id="probier-es-selbst-aus">Probier es selbst aus</h2><p>Der beste Weg zu verstehen, wie einfach Lokalisierung inzwischen geworden ist, ist es selbst auszuprobieren. Füge einen String Catalog zu deinem Projekt hinzu, baue deine App, und du wirst vielleicht überrascht sein, dass sie bereits die meisten Einträge enthält, die lokalisiert werden müssen. Mit <a href="https://translatekit.app/">TranslateKit 3</a> kannst du sie mit kontextbewusster KI übersetzen, Konsistenz über dein gesamtes Projekt sicherstellen und Nutzer weltweit schneller denn je erreichen.</p><p>Die Reise von RemafoX zu TranslateKit hat mir gezeigt, dass etwas, das wie ein Rückschlag aussieht, manchmal dazu führen kann, etwas noch Besseres zu bauen. Indem ich neue Technologien angenommen und wirklich auf die Bedürfnisse der Entwickler gehört habe, konnte ich ein Tool schaffen, das App-Lokalisierung einfacher und besser macht. Geduld zahlt sich am Ende eben immer aus!</p>]]></content:encoded>
</item>
<item>
<title>Swift-Lokalisierung in 2025: Best Practices, die vorher nicht möglich waren</title>
<link>https://fline.dev/de/blog/swift-localization-in-2025-best-practices-you-couldnt-use-before/</link>
<guid isPermaLink="true">https://fline.dev/de/blog/swift-localization-in-2025-best-practices-you-couldnt-use-before/</guid>
<pubDate>Wed, 12 Feb 2025 00:00:00 +0000</pubDate>
<description><![CDATA[String Catalogs haben die Lokalisierung verbessert, aber auch neue Herausforderungen mit sich gebracht. Dieser Artikel zeigt, wie du mit modernen Best Practices und einem neuen Open-Source-Tool Struktur und Effizienz zurückgewinnst – und vielleicht die Art, wie du lokalisierst, grundlegend veränderst.]]></description>
<content:encoded><![CDATA[<h2 id="die-entwicklung-der-ios-lokalisierung">Die Entwicklung der iOS-Lokalisierung</h2><p>Um zu verstehen, wie weit die Lokalisierung gekommen ist – und wo sie noch Schwächen hat – werfen wir einen kurzen Blick zurück: In der Ära vor den String Catalogs verließen sich Entwickler auf eine Kombination aus <code>.strings</code>- und <code>.stringsdict</code>-Dateien, organisiert in sprachspezifischen Ordnern wie <code>en.lproj</code>. Dieses System war zwar funktional, hatte aber einige Nachteile:</p><ol><li><p>Fehlende <strong>Sicherheitsprüfungen</strong> für ungenutzte oder fehlende Übersetzungen</p></li><li><p><strong>Manuelle Synchronisierung</strong> zwischen verschiedenen Sprachdateien nötig</p></li><li><p><strong>Keine</strong> eingebaute Unterstützung für die <strong>Extraktion von Strings</strong> aus dem Code</p></li></ol><p>Tools wie <a href="https://github.com/SwiftGen/SwiftGen">SwiftGen</a> und <a href="https://github.com/FlineDev/BartyCrouch">BartyCrouch</a> entstanden, um diese Probleme zu lösen, und boten Typsicherheit und automatisierte Extraktion. Und die Community etablierte Best Practices rund um semantische Keys (z. B. <code>&quot;Onboarding.Page1.title&quot;</code>), um Kontext für Übersetzer zu liefern und verwandte Strings zu gruppieren.</p><h2 id="string-catalogs-ein-gamechanger-mit-kompromissen">String Catalogs: Ein Gamechanger mit Kompromissen</h2><p>String Catalogs, 2023 von Apple eingeführt, lösten mehrere langjährige Probleme:</p><p>✅  <strong>Automatische</strong> Key-<strong>Extraktion</strong> aus dem Code
✅  Eingebaute <strong>Sicherheitsprüfungen</strong> für fehlende Übersetzungen
✅  Visuelles <strong>Fortschritts</strong>-Tracking in Xcode
✅  <strong>Einheitliches Dateiformat</strong> für alle Sprachen
✅  <strong>Abwärtskompatibilität</strong> mit älteren iOS-Versionen</p><p>Allerdings brachte Apples empfohlener Ansatz, englische Strings als Keys zu verwenden – obwohl er die Lesbarkeit im Code verbessert – neue Herausforderungen mit sich:</p><p>❌  Zusammengehörige Strings sind alphabetisch <strong>verstreut</strong>
❌  Weniger <strong>Kontext</strong> für Übersetzer
❌  Keine <strong>Gruppierung</strong> nach Feature oder Screen</p><p><img src="/assets/images/blog/swift-localization-in-2025-best-practices-you-couldnt-use-before/the-lack-of-grouping-in.webp" alt="Das Fehlen von Gruppierung in String Catalogs ist beim Übersetzen nicht hilfreich." loading="lazy" />
<em>Das Fehlen von Gruppierung in String Catalogs ist beim Übersetzen nicht hilfreich.</em></p><p>Ebenso wichtig: Es gibt eine erhebliche Lücke im iOS-Entwicklungs-Ökosystem rund um Lokalisierung, die der Situation bei Icons vor SF Symbols ähnelt. Vor SF Symbols musste jeder Entwickler eigene Icons erstellen oder beschaffen, was zu inkonsistenten Erlebnissen über Apps hinweg führte. SF Symbols löste das durch einen standardisierten, umfassenden Satz an Icons, die Konsistenz sicherstellen und enorm viel Entwicklungszeit sparen.</p><p>Der Lokalisierungsbereich braucht dringend eine ähnliche Lösung. <strong>Jede App braucht gängige UI-Strings</strong> wie “Speichern”, “Abbrechen”, “Fertig”, “Datenschutzrichtlinie” oder “Allgemeine Geschäftsbedingungen” – und diese sollten über Apps hinweg genauso konsistent sein wie Icons. Nutzer profitieren von dieser Konsistenz in gleicher Weise wie von konsistenter Ikonografie. Doch aktuell muss jeder Entwickler diese gängigen UI-Elemente unabhängig übersetzen, was zu unterschiedlichen Übersetzungen für die gleichen Konzepte in verschiedenen Apps und Sprachen führt.</p><h2 id="translatekit-das-fehlende-puzzlestück">TranslateKit: Das fehlende Puzzlestück</h2><p>Das Open-Source-<a href="https://github.com/FlineDev/TranslateKit">TranslateKit SDK</a> schließt diese Lücken und bietet eine umfassende Lösung, die Best Practices beibehält und gleichzeitig die Vorteile von String Catalogs voll ausschöpft. So wie SF Symbols die Icon-Nutzung in iOS-Apps revolutioniert hat, will dieses neue Swift-Package dasselbe für die Lokalisierung tun.</p><h3 id="1-semantische-key-generierung-ohne-aufwand">1. Semantische Key-Generierung ohne Aufwand</h3><p>Das <code>#tk</code>-Macro bringt semantische Keys zurück und hält den Code dabei sauber und lesbar:</p><pre><code class="language-swift">struct OnboardingView: View {
  var body: some View {
    // Generates key: OnboardingView.Body.welcomeToMyApp
    Text(#tk(&quot;Welcome to MyApp&quot;))
  }
}</code></pre><p>Dieser Ansatz:</p><ul><li><p>Generiert automatisch <strong>semantische Keys</strong> basierend auf dem Code-Kontext</p></li><li><p>Bewahrt die <strong>Lesbarkeit</strong> des Codes</p></li><li><p>Liefert wichtigen <strong>Kontext für Übersetzer</strong></p></li><li><p><strong>Gruppiert zusammengehörige Strings</strong> im String Catalog</p></li></ul><p><img src="/assets/images/blog/swift-localization-in-2025-best-practices-you-couldnt-use-before/auto-generated-semantic.webp" alt="Automatisch generierte semantische Keys, die zusammengehörige Strings natürlich gruppieren." loading="lazy" />
<em>Automatisch generierte semantische Keys, die zusammengehörige Strings natürlich gruppieren.</em></p><p>Und alles, was du schreiben musst, ist <code>#tk()</code> um deine String-Literale. Super einfach!</p><h3 id="2-vorlokalisierte-gängige-ui-elemente">2. Vorlokalisierte gängige UI-Elemente</h3><p>So wie SF Symbols die Icon-Nutzung standardisiert hat, brauchen wir Konsistenz bei UI-Texten. TranslateKit bietet über 2.000 Strings, die in alle 40 von Apple im iOS-Betriebssystem unterstützten Sprachen vorlokalisiert sind.</p><p>Sie sind in <strong>4 Kategorien</strong> unterteilt, damit du leicht findest, was du brauchst:</p><pre><code class="language-swift">// Actions
Button(TK.Action.save) { saveData() }
// =&gt; en: &quot;Save&quot;, de: &quot;Sichern&quot;, etc.

// Labels
Text(TK.Label.privacyPolicy)
// =&gt; en: &quot;Privacy Policy&quot;, de: &quot;Sichern&quot;, etc.

// Messages
Text(TK.Message.areYouSure)
// =&gt; en: &quot;Privacy Policy&quot;, de: &quot;Datenschutzerklärung&quot;, etc.

// Placeholders
ProgressView(TK.Placeholder.loadingDots)
// =&gt; en: &quot;Loading…&quot;, de: &quot;Laden…&quot;, etc.</code></pre><p>Vorteile:</p><ul><li><p>Passt zu System-UI-Übersetzungen von Apple (<strong>Konsistenz</strong>)</p></li><li><p>Garantiert professionelle Qualität für gängige UI-Elemente (<strong>Genauigkeit</strong>)</p></li><li><p>Reduziert den Lokalisierungsaufwand (spart <strong>Zeit</strong> &amp; <strong>Geld</strong>)</p></li></ul><h3 id="3-kontextbewusste-übersetzungen">3. Kontextbewusste Übersetzungen</h3><p>Moderne Lokalisierung bedeutet nicht nur, Wörter zu übersetzen – es geht darum, den Kontext zu verstehen. Während die automatische Key-Generierung in 90 % der Fälle dabei hilft, musst du in manchen Fällen zusätzlich den optionalen <code>c</code>-Parameter angeben:</p><pre><code class="language-swift">struct DocumentView: View {
    let fileName: String

    var body: some View {
        Button(#tk(&quot;Delete '\(fileName)'?&quot;,
                   c: &quot;Example: Delete 'MyStats.csv'?&quot;)) {
            handleDelete()
        }
    }
}</code></pre><p>Dieser Kommentar-Parameter wird am häufigsten benötigt, wenn du <strong>dynamische Daten</strong> in deinem String hast, wie im obigen Beispiel. Es ist wichtig, immer typische Beispieldaten im Kommentar anzugeben, damit es klar wird. Sonst wissen Übersetzer möglicherweise nicht, wie sie korrekt übersetzen sollen.</p><h2 id="was-kommt-als-nächstes">Was kommt als Nächstes?</h2><p>Dieser Artikel hat gezeigt, wie das <a href="https://github.com/FlineDev/TranslateKit">TranslateKit SDK</a> den grundlegenden Lokalisierungs-Workflow modernisiert. Wenn du dich für fortgeschrittenere Themen wie Pluralisierung, Formatter und mehr interessierst, abonniere meinen YouTube-Kanal, um dein Interesse an detaillierteren Videos zu signalisieren:</p><p><a href="https://www.youtube.com/c/FlineDev">FlineDev</a></p><blockquote><p>✨ TranslateKit SDK funktioniert perfekt zusammen mit <a href="https://translatekit.app/">TranslateKit for Mac</a>, einem präzisen, KI-gestützten Übersetzungstool, um <strong>den Rest deiner App zu lokalisieren</strong>! Es ist superschnell und erschwinglich, <a href="https://translatekit.app/"><strong>probiere es kostenlos</strong></a> aus, um mehr Nutzer zu erreichen.</p></blockquote>]]></content:encoded>
</item>
<item>
<title>HandySwiftUI Styles: SwiftUIs Standard-Views aufwerten</title>
<link>https://fline.dev/de/blog/handyswiftui-styles/</link>
<guid isPermaLink="true">https://fline.dev/de/blog/handyswiftui-styles/</guid>
<pubDate>Thu, 07 Nov 2024 00:00:00 +0000</pubDate>
<description><![CDATA[Von aufmerksamkeitsstarken pulsierenden Buttons über vielseitige Label-Layouts bis hin zu plattformübergreifenden Checkboxen und vertikalen Formular-Styles – entdecke die SwiftUI-Styles, die deinen Apps den letzten Schliff und Konsistenz verleihen. Diese praxiserprobten Styles bilden die UI-Grundlage von 10 Produktiv-Apps und es werden mehr.]]></description>
<content:encoded><![CDATA[<p>Nach 4 Jahren Feinschliff an diesen APIs in meinen eigenen Apps freue ich mich, das erste getaggte Release von <a href="https://github.com/FlineDev/HandySwiftUI">HandySwiftUI</a> zu teilen. Dieses Paket enthält verschiedene Hilfsfunktionen und Convenience-APIs, die mir enorm dabei geholfen haben, allein im letzten Jahr 10 Apps auszuliefern. Es bietet Komfortfunktionen für die SwiftUI-Entwicklung, ähnlich wie mein <a href="https://github.com/FlineDev/HandySwift">HandySwift</a>-Paket für Foundation.</p><p>In diesem Artikel stelle ich eine Auswahl der <em>Styles</em> vor, die sich in meiner täglichen Arbeit an Apps wie <a href="https://translatekit.app/">TranslateKit</a>, <a href="https://freemiumkit.app/">FreemiumKit</a> und <a href="https://crosscraft.app/">CrossCraft</a> am meisten bewährt haben. HandySwiftUI enthält zwar noch viele weitere Hilfsfunktionen, aber diese Styles haben sich in der Praxis immer wieder als besonders wertvoll erwiesen und könnten auch für deine SwiftUI-Projekte nützlich sein.</p><h3 id="primary--secondary--und-pulsating-buttons">Primary-, Secondary- und Pulsating-Buttons</h3><p>Erstelle visuell ansprechende Buttons mit vorgefertigten Styles für verschiedene Anwendungsfälle:</p><pre><code class="language-swift">struct ButtonShowcase: View {
   var body: some View {
       VStack(spacing: 20) {
           // Primary Button mit auffälligem Hintergrund
           Button(&quot;Get Started&quot;) {}
               .buttonStyle(.primary())

           // Secondary Button mit Rahmen
           Button(&quot;Learn More&quot;) {}
               .buttonStyle(.secondary())

           // Aufmerksamkeitsstarker pulsierender Button
           Button {} label: {
              Label(&quot;Updates&quot;, systemImage: &quot;bell.fill&quot;)
                 .padding(15)
           }
           .buttonStyle(.pulsating(color: .blue, cornerRadius: 20, glowRadius: 8, duration: 2))
       }
   }
}</code></pre><p><img src="/assets/images/blog/handyswiftui-styles/button-styles.gif" alt="Button styles" loading="lazy" /></p><h3 id="horizontale-vertikale-und-icon-width-fixierte-labels">Horizontale, vertikale und Icon-Width-fixierte Labels</h3><p>Mehrere Label-Styles für verschiedene Layout-Anforderungen:</p><pre><code class="language-swift">struct LabelShowcase: View {
   var body: some View {
       VStack(spacing: 20) {
           // Horizontales Layout mit Icon rechts
           Label(&quot;Settings&quot;, systemImage: &quot;gear&quot;)
               .labelStyle(.horizontal(spacing: 8, iconIsTrailing: true, iconColor: .blue))

           // Feste Icon-Breite für Ausrichtung
           Label(&quot;Profile&quot;, systemImage: &quot;person&quot;)
               .labelStyle(.fixedIconWidth(30, iconColor: .green, titleColor: .primary))

           // Vertikales Stapel-Layout
           Label(&quot;Messages&quot;, systemImage: &quot;message.fill&quot;)
               .labelStyle(.vertical(spacing: 8, iconColor: .blue, iconFont: .title))
       }
   }
}</code></pre><p>Alle Parameter sind optional mit sinnvollen Standardwerten, du kannst sie also einfach wie <code>.vertical()</code> verwenden. Du musst nur angeben, was du anpassen möchtest.</p><h3 id="vertikal-beschriftete-inhalte">Vertikal beschriftete Inhalte</h3><p>Strukturierte Formulareingaben mit vertikalen Labels, wie sie in der API-Konfiguration von <a href="https://freemiumkit.app/">FreemiumKit</a> verwendet werden:</p><pre><code class="language-swift">struct APIConfigView: View {
    @State private var keyID = &quot;&quot;
    @State private var apiKey = &quot;&quot;

    var body: some View {
        Form {
            HStack {
                VStack {
                    LabeledContent(&quot;Key ID&quot;) {
                        TextField(&quot;e.g. 2X9R4HXF34&quot;, text: $keyID)
                            .textFieldStyle(.roundedBorder)
                    }
                    .labeledContentStyle(.vertical())

                    LabeledContent(&quot;API Key&quot;) {
                        TextEditor(text: $apiKey)
                            .frame(height: 80)
                            .textFieldStyle(.roundedBorder)
                    }
                    .labeledContentStyle(.vertical())
                }
            }
        }
    }
}</code></pre><p><img src="/assets/images/blog/handyswiftui-styles/vertical-labeled-content.webp" alt="Vertical labeled content" loading="lazy" /></p><p>Der <code>.vertical</code>-Style ermöglicht die Anpassung von Ausrichtung (Standard: <code>leading</code>) und Abstand (Standard: 4). Übergib <code>muteLabel: false</code>, wenn du einen eigenen Label-Style verwendest, da Labels standardmäßig automatisch kleiner und grau dargestellt werden.</p><p>Zum Beispiel möchte ich im Feature-Lokalisierungsformular von <a href="https://freemiumkit.app/">FreemiumKit</a>, dass das vertikale Label eine größere Schrift hat:</p><pre><code class="language-swift">LabeledContent {
   LimitedTextField(
      &quot;English \(self.title)&quot;,
      text: self.$localizedString.fallback,
      characterLimit: self.characterLimit
   )
   .textFieldStyle(.roundedBorder)
} label: {
   Text(&quot;English \(self.title) (\(self.isRequired ? &quot;Required&quot; : &quot;Optional&quot;))&quot;)
      .font(.title3)
}
.labeledContentStyle(.vertical(muteLabel: false))</code></pre><p><img src="/assets/images/blog/handyswiftui-styles/mute-label-false.webp" alt="Mute label false" loading="lazy" /></p><h3 id="multi-platform-toggle-style">Multi-Platform Toggle-Style</h3><p>Während SwiftUI einen <code>.checkbox</code>-Toggle-Style bietet, ist dieser nur auf macOS verfügbar. HandySwiftUI ergänzt <code>.checkboxUniversal</code>, das Checkbox-artige Toggles auf allen Plattformen ermöglicht (und auf macOS als <code>.checkbox</code> gerendert wird):</p><pre><code class="language-swift">struct ProductRow: View {
    @State private var isEnabled: Bool = true

    var body: some View {
       HStack {
           Toggle(&quot;&quot;, isOn: $isEnabled)
              .toggleStyle(.checkboxUniversal)

           Text(&quot;Pro Monthly&quot;)

           Spacer()
       }
    }
}</code></pre><p><img src="/assets/images/blog/handyswiftui-styles/checkbox-universal.webp" alt="Checkbox universal" loading="lazy" /></p><p>Das Beispiel stammt aus dem Produkte-Bildschirm von <a href="https://freemiumkit.app/">FreemiumKit</a>, der für macOS optimiert ist, aber auch andere Plattformen unterstützt.</p><h2 id="leg-noch-heute-los">Leg noch heute los</h2><p>Ich hoffe, du findest diese Styles genauso nützlich in deinen Projekten wie ich in meinen. Wenn du Ideen für Verbesserungen oder zusätzliche Styles hast, die der SwiftUI-Community zugutekommen könnten, trage gerne auf GitHub bei:</p><p><a class="sk-link-card sk-link-card-github" href="https://github.com/FlineDev/HandySwiftUI?ref=fline.dev"><span class="sk-link-card-source"><svg viewBox="0 0 16 16" width="16" height="16" fill="currentColor"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/></svg><span>github.com</span></span><span class="sk-link-card-title">FlineDev / HandySwiftUI</span><span class="sk-link-card-description">Handy SwiftUI features that didn’t make it into SwiftUI (yet)</span></a></p><p>Dies ist der letzte von vier Artikeln, die die Features von HandySwiftUI erkunden. Schau dir die vorherigen Artikel über <a href="https://www.fline.dev/handyswiftui-new-types/">Neue Typen</a>, <a href="https://www.fline.dev/handyswiftui-view-modifiers/">View Modifier</a> und <a href="https://www.fline.dev/handyswiftui-extensions/">Extensions</a> an, falls du sie noch nicht gelesen hast!</p>]]></content:encoded>
</item>
<item>
<title>HandySwiftUI Extensions: SwiftUI-Entwicklung noch komfortabler</title>
<link>https://fline.dev/de/blog/handyswiftui-extensions/</link>
<guid isPermaLink="true">https://fline.dev/de/blog/handyswiftui-extensions/</guid>
<pubDate>Mon, 04 Nov 2024 00:00:00 +0000</pubDate>
<description><![CDATA[Entdecke leistungsstarke SwiftUI-Extensions für saubere optionale Bindings, intuitive Farbverwaltung, XML-basierte Textformatierung und mehr. Diese praxiserprobten Hilfsfunktionen helfen dir, eleganteren SwiftUI-Code zu schreiben und Boilerplate in deinen Apps zu reduzieren.]]></description>
<content:encoded><![CDATA[<p>Nach 4 Jahren Feinschliff an diesen APIs in meinen eigenen Apps freue ich mich, das erste getaggte Release von <a href="https://github.com/FlineDev/HandySwiftUI">HandySwiftUI</a> zu teilen. Dieses Paket enthält verschiedene Hilfsfunktionen und Convenience-APIs, die mir enorm dabei geholfen haben, allein im letzten Jahr 10 Apps auszuliefern. Es bietet Komfortfunktionen für die SwiftUI-Entwicklung, ähnlich wie mein <a href="https://github.com/FlineDev/HandySwift">HandySwift</a>-Paket für Foundation.</p><p>In diesem Artikel stelle ich eine Auswahl der <em>Extensions</em> vor, die sich in meiner täglichen Arbeit an Apps wie <a href="https://translatekit.app/">TranslateKit</a>, <a href="https://freemiumkit.app/">FreemiumKit</a> und <a href="https://crosscraft.app/">CrossCraft</a> am meisten bewährt haben. HandySwiftUI enthält zwar noch viele weitere Hilfsfunktionen, aber diese Extensions haben sich in der Praxis immer wieder als besonders wertvoll erwiesen und könnten auch für deine SwiftUI-Projekte nützlich sein.</p><h3 id="komfortfunktionen-für-optionale-bindings">Komfortfunktionen für optionale Bindings</h3><p>Die Operatoren <code>??</code> und <code>!</code> sowie der <code>isPresent</code>-Modifier vereinfachen die Arbeit mit optionalen Werten in Bindings:</p><pre><code class="language-swift">struct EditableProfile: View {
   @State private var profile: Profile?
   @State private var showAdvanced = false

   var body: some View {
       Form {
           // Standardwert für optionales Binding über den `??`-Operator
           TextField(&quot;Name&quot;, text: $profile?.name ?? &quot;Anonymous&quot;)

           // Binding-Wert mit `!`-Operator negieren
           Toggle(&quot;Hide Details&quot;, isOn: !$showAdvanced)
       }
       // Optionales Binding für Sheet-Präsentation nutzen
       .sheet(isPresented: $profile.isPresent(wrappedType: Profile.self)) {
           ProfileEditor(profile: $profile)
       }
   }
}</code></pre><p>Die Operatoren sind in allen möglichen Views nützlich, wenn du zum Beispiel mit optionalen Daten in Models arbeitest.</p><h3 id="farbverwaltung">Farbverwaltung</h3><p>Die umfassenden Color-Extensions bieten leistungsstarke Werkzeuge zur Farbmanipulation und Nutzung von Systemfarben:</p><pre><code class="language-swift">struct ColorfulView: View {
   @State private var baseColor = Color.blue

   var body: some View {
       VStack {
           // Variationen der Basisfarbe erzeugen
           Rectangle()
               .fill(baseColor.change(.luminance, by: -0.2))
           Rectangle()
               .fill(baseColor)
           Rectangle()
               .fill(baseColor.change(.luminance, by: 0.2))

           // Mit Hex-Farben arbeiten
           Circle()
               .fill(Color(hex: &quot;#FF5733&quot;))

           // Farbkomponenten verwenden
           Text(&quot;HSB: \(baseColor.hsbo.hue), \(baseColor.hsbo.saturation), \(baseColor.hsbo.brightness)&quot;)
           Text(&quot;RGB: \(baseColor.rgbo.red), \(baseColor.rgbo.green), \(baseColor.rgbo.blue)&quot;)
       }
       .padding()
       // Semantische Systemfarben für eigene systemähnliche Komponenten nutzen
       .background(Color.systemBackground)
   }
}</code></pre><p><img src="/assets/images/blog/handyswiftui-extensions/colorful-view.webp" alt="Colorful view" loading="lazy" /></p><p>Wenn du die Helligkeit einer Farbe anpasst, verwende <code>.luminance</code> statt <code>.brightness</code> aus dem HSB-Farbsystem. Luminanz bildet besser ab, wie Menschen Licht und Dunkelheit wahrnehmen – deshalb unterstützt HandySwiftUI auch den HLC-Farbraum.</p><h3 id="rich-text-formatierung">Rich-Text-Formatierung</h3><p>Die Textformatierungs-Extensions bieten eine praktische Möglichkeit, Rich Text mit gemischten Styles zu erstellen, inspiriert von XML-artigen Tags:</p><pre><code class="language-swift">struct FormattedText: View {
   var body: some View {
       Text(
           format: &quot;A &lt;b&gt;bold&lt;/b&gt; new way to &lt;i&gt;style&lt;/i&gt; your text with &lt;star.fill/&gt; and &lt;b&gt;mixed&lt;/b&gt; &lt;red&gt;formatting&lt;/red&gt;.&quot;,
           partialStyling: Dictionary.htmlLike.merging([
               &quot;red&quot;: { $0.foregroundColor(.red) },
               &quot;star.fill&quot;: { $0.foregroundColor(.yellow) }
           ]) { $1 }  // $1 zurückgeben (statt $0) bedeutet, dass hinzugefügte Keys bestehende überschreiben
       )
   }
}</code></pre><p><img src="/assets/images/blog/handyswiftui-extensions/formatted-text.webp" alt="Formatted text" loading="lazy" /></p><p>Im obigen Beispiel wird das mitgelieferte <code>.htmlLike</code>-Styling von HandySwiftUI mit eigenen Tags kombiniert. Beachte, dass <code>.htmlLike</code> einfach Folgendes zurückgibt:</p><pre><code class="language-swift">[
   &quot;b&quot;: { $0.bold() },
   &quot;sb&quot;: { $0.fontWeight(.semibold) },
   &quot;i&quot;: { $0.italic() },
   &quot;bi&quot;: { $0.bold().italic() },
   &quot;sbi&quot;: { $0.fontWeight(.semibold).italic() },
   &quot;del&quot;: { $0.strikethrough() },
   &quot;ins&quot;: { $0.underline() },
   &quot;sub&quot;: { $0.baselineOffset(-4) },
   &quot;sup&quot;: { $0.baselineOffset(6) },
]</code></pre><p>Alle XML-artigen Einträge, die mit <code>/&gt;</code> enden, wie <code>&lt;star.fill/&gt;</code> im Beispiel oben, werden als SFSymbol gerendert. So kannst du SFSymbols ganz einfach direkt in deinem Text verwenden.</p><h3 id="bildverarbeitung">Bildverarbeitung</h3><p>Einheitliche Extensions für die Bildverarbeitung mit <code>UIImage</code> und <code>NSImage</code>:</p><pre><code class="language-swift">class ImageProcessor {
   func processImage(_ image: UIImage) {
       // Bild unter Beibehaltung des Seitenverhältnisses skalieren
       let resized = image.resized(maxWidth: 800, maxHeight: 600)

       // In verschiedene Formate konvertieren
       let pngData = image.webpData()
       let jpegData = image.webpData(compressionQuality: 0.8)
       let heicData = image.heicData(compressionQuality: 0.8)
   }
}</code></pre><p>Beachte, dass all diese APIs optionale Werte zurückgeben – für Grenzfälle wie extrem niedrigen Speicher – aber in den meisten Fällen erfolgreich durchlaufen.</p><h3 id="komfortable-model-zu-view-konvertierungen">Komfortable Model-zu-View-Konvertierungen</h3><p>HandySwiftUI bietet Initializer-Komfortfunktionen, die es einfach machen, deine Model-Typen direkt in SwiftUI-Views darzustellen:</p><pre><code class="language-swift">enum Tab: CustomLabelConvertible {
    case home, profile, settings

    var description: String {
        switch self {
        case .home: &quot;Home&quot;
        case .profile: &quot;Profile&quot;
        case .settings: &quot;Settings&quot;
        }
    }

    var symbolName: String {
        switch self {
        case .home: &quot;house.fill&quot;
        case .profile: &quot;person.circle&quot;
        case .settings: &quot;gear&quot;
        }
    }
}

struct ContentView: View {
    @State private var selectedTab: Tab = .home

    var body: some View {
        TabView(selection: $selectedTab) {
            HomeView()
                // Tab-Item direkt aus Enum-Case erstellen
                .tabItem { Label(convertible: Tab.home) }
                .tag(Tab.home)

            ProfileView()
                .tabItem { Label(convertible: Tab.profile) }
                .tag(Tab.profile)

            SettingsView()
                .tabItem { Label(convertible: Tab.settings) }
                .tag(Tab.settings)
        }

        // Funktioniert auch mit Text- und Image-Views
        Text(convertible: selectedTab)  // Zeigt den Tab-Namen
        Image(convertible: selectedTab) // Zeigt das Tab-Icon
    }
}</code></pre><p>Statt manuell Strings und Symbolnamen aus deinen Models zu extrahieren, kannst du sie auf <code>CustomStringConvertible</code> für Text, <code>CustomSymbolConvertible</code> für SF Symbols oder <code>CustomLabelConvertible</code> für beides konformieren. Dann nutze die komfortablen Initializer, um SwiftUI-Views direkt zu erstellen:</p><ul><li><p><code>Text(convertible:)</code> – Erstellt Text aus jedem <code>CustomStringConvertible</code></p></li><li><p><code>Image(convertible:)</code> – Erstellt SF-Symbol-Bilder aus jedem <code>CustomSymbolConvertible</code></p></li><li><p><code>Label(convertible:)</code> – Erstellt Text+Icon-Labels aus jedem <code>CustomLabelConvertible</code></p></li></ul><p>Dieses Muster funktioniert besonders gut mit Enums, die UI-Zustände, Menüoptionen oder Tabs repräsentieren, wie im Beispiel oben gezeigt.</p><h3 id="suchprefix-hervorhebung">Suchprefix-Hervorhebung</h3><p>HandySwiftUI bietet eine elegante Möglichkeit, übereinstimmenden Text in Suchergebnissen hervorzuheben, damit Nutzer sofort sehen, welche Teile des Textes zu ihrer Suchanfrage passen:</p><pre><code class="language-swift">struct SearchResultsView: View {
    @State private var searchText = &quot;&quot;
    let translations = [
        &quot;Good morning!&quot;,
        &quot;Good evening!&quot;,
        &quot;How are you?&quot;,
        &quot;Thank you very much!&quot;
    ]

    var body: some View {
        List {
            ForEach(translations.filtered(by: searchText), id: \.self) { translation in
                // Bei Suche nach &quot;go mo&quot; wird &quot;Go mo&quot; in &quot;Good morning!&quot; hervorgehoben
                Text(translation.highlightMatchingTokenizedPrefixes(in: searchText))
            }
        }
        .searchable(text: $searchText)
    }
}

extension [String] {
    func filtered(by searchText: String) -&gt; [String] {
        guard !searchText.isEmpty else { return Array(self) }
        return filter { $0.localizedCaseInsensitiveContains(searchText) }
    }
}</code></pre><p><img src="/assets/images/blog/handyswiftui-extensions/common-translations.webp" alt="Common translations" loading="lazy" /></p><p>Diese Hervorhebungsfunktion wurde ursprünglich für das „Common Translations”-Feature in der Menüleiste von <a href="https://translatekit.app/">TranslateKit</a> entwickelt, wo sie Nutzern hilft, passende Phrasen in bestätigten Übersetzungen schnell zu erkennen. Die Funktion zerlegt den Suchtext in Tokens und hebt jeden passenden Prefix hervor – perfekt für:</p><ul><li><p>Hervorhebung von Suchergebnissen in Listen oder Menüs</p></li><li><p>Autocomplete-Vorschläge mit visuellem Feedback</p></li><li><p>Filtern durch Textsammlungen mit sichtbarem Trefferkontext</p></li><li><p>Bessere Sichtbarkeit von Suchtreffern in Dokumentvorschauen</p></li></ul><p>Die Hervorhebung ist standardmäßig case-insensitive und diakritik-insensitive, aber du kannst die Locale und den Font für die Hervorhebung anpassen. Das macht sie zu einem vielseitigen Werkzeug für jede Suchoberfläche, in der du übereinstimmende Textabschnitte betonen möchtest.</p><h2 id="leg-noch-heute-los">Leg noch heute los</h2><p>Ich hoffe, du findest diese Extensions genauso nützlich in deinen Projekten wie ich in meinen. Wenn du Ideen für Verbesserungen oder zusätzliche Extensions hast, die der SwiftUI-Community zugutekommen könnten, trage gerne auf GitHub bei:</p><p><a class="sk-link-card sk-link-card-github" href="https://github.com/FlineDev/HandySwiftUI?ref=fline.dev"><span class="sk-link-card-source"><svg viewBox="0 0 16 16" width="16" height="16" fill="currentColor"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/></svg><span>github.com</span></span><span class="sk-link-card-title">FlineDev / HandySwiftUI</span><span class="sk-link-card-description">Handy SwiftUI features that didn’t make it into SwiftUI (yet)</span></a></p><p>Dies ist der dritte von vier Artikeln, die die Features von HandySwiftUI erkunden. Schau dir die vorherigen Artikel über <a href="https://www.fline.dev/handyswiftui-new-types/">Neue Typen</a> und <a href="https://www.fline.dev/handyswiftui-view-modifiers/">View Modifier</a> an, falls du sie noch nicht gelesen hast, und bleib dran für den letzten Beitrag über <a href="https://www.fline.dev/handyswiftui-styles/">Styles</a>!</p>]]></content:encoded>
</item>
<item>
<title>HandySwiftUI View Modifier: Dein SwiftUI-Code wird schlanker</title>
<link>https://fline.dev/de/blog/handyswiftui-view-modifiers/</link>
<guid isPermaLink="true">https://fline.dev/de/blog/handyswiftui-view-modifiers/</guid>
<pubDate>Fri, 01 Nov 2024 00:00:00 +0000</pubDate>
<description><![CDATA[Von intelligentem Farbkontrast über vereinfachtes Error-Handling bis hin zu Lösch-Workflows und plattformspezifischem Styling – entdecke die SwiftUI-Modifier, die gängigen Boilerplate-Code eliminieren und dir helfen, wartbarere Apps zu entwickeln.]]></description>
<content:encoded><![CDATA[<p>Nach 4 Jahren Feinschliff an diesen APIs in meinen eigenen Apps freue ich mich, das erste getaggte Release von <a href="https://github.com/FlineDev/HandySwiftUI">HandySwiftUI</a> zu teilen. Dieses Paket enthält verschiedene Hilfsfunktionen und Convenience-APIs, die mir enorm dabei geholfen haben, allein im letzten Jahr 10 Apps auszuliefern. Es bietet Komfortfunktionen für die SwiftUI-Entwicklung, ähnlich wie mein <a href="https://github.com/FlineDev/HandySwift">HandySwift</a>-Paket für Foundation.</p><p>In diesem Artikel stelle ich eine Auswahl der <em>View Modifier</em> vor, die sich in meiner täglichen Arbeit an Apps wie <a href="https://translatekit.app/">TranslateKit</a>, <a href="https://freemiumkit.app/">FreemiumKit</a> und <a href="https://crosscraft.app/">CrossCraft</a> am meisten bewährt haben. HandySwiftUI enthält zwar noch viele weitere Hilfsfunktionen, aber diese Modifier haben sich in der Praxis immer wieder als besonders wertvoll erwiesen und könnten auch für deine SwiftUI-Projekte nützlich sein.</p><h3 id="intelligenter-farbkontrast">Intelligenter Farbkontrast</h3><p>Der <code>foregroundStyle(_:minContrast:)</code>-Modifier sorgt dafür, dass Text lesbar bleibt, indem er den Farbkontrast automatisch anpasst. Das ist nützlich für dynamische Farben oder Systemfarben wie <code>.yellow</code>, die in bestimmten Farbschemata schlechten Kontrast haben können:</p><pre><code class="language-swift">struct AdaptiveText: View {
    @State private var dynamicColor: Color = .yellow

    var body: some View {
        HStack {
            // Ohne Kontrastanpassung
            Text(&quot;Maybe hard to read&quot;)
                .foregroundStyle(dynamicColor)

            // Mit automatischer Kontrastanpassung
            Text(&quot;Always readable&quot;)
                .foregroundStyle(dynamicColor, minContrast: 0.5)
        }
    }
}</code></pre><p><img src="/assets/images/blog/handyswiftui-view-modifiers/yellow-with-contrast.webp" alt="Yellow with contrast" loading="lazy" /></p><blockquote><p>Dieser Warnungsindikator in <a href="https://translatekit.app/">TranslateKit</a> nutzt <code>.yellow</code>, stellt aber einen guten Kontrast auch im Light Mode sicher, um lesbar zu bleiben. Im Dark Mode bleibt es gelb.</p></blockquote><p>Der <code>minContrast</code>-Parameter (Bereich von 0 bis 1) bestimmt das minimale Kontrastverhältnis gegenüber Weiß (im Light Mode) oder Schwarz (im Dark Mode) anhand des Luminanzwerts (wahrgenommene Helligkeit). So bleibt Text unabhängig vom aktuellen Farbschema lesbar.</p><h3 id="tasks-mit-error-handling">Tasks mit Error-Handling</h3><p>Der <code>throwingTask</code>-Modifier vereinfacht das asynchrone Error-Handling in SwiftUI-Views. Anders als SwiftUIs eingebauter <code>.task</code>-Modifier, der manuelle <code>do-catch</code>-Blöcke erfordert, bietet <code>throwingTask</code> einen dedizierten Error-Handler-Closure:</p><pre><code class="language-swift">struct DataView: View {
    @State private var error: Error?

    var body: some View {
        ContentView()
            .throwingTask {
                try await loadData()
            } catchError: { error in
                self.error = error
            }
    }
}</code></pre><p>Der Task verhält sich ähnlich wie <code>.task</code> – er startet, wenn die View erscheint, und wird abgebrochen, wenn sie verschwindet. Der <code>catchError</code>-Closure ist optional, du kannst ihn also weglassen, wenn du Fehler nicht behandeln musst – und füllst damit genau die Lücke, die der <code>task</code>-Modifier offengelassen hat.</p><h3 id="plattformspezifisches-styling">Plattformspezifisches Styling</h3><p>Ein vollständiger Satz plattformspezifischer Modifier ermöglicht präzise Kontrolle über Multi-Platform-UI:</p><pre><code class="language-swift">struct AdaptiveInterface: View {
    var body: some View {
        ContentView()
            // Padding nur auf macOS hinzufügen
            .macOSOnlyPadding(.all, 20)
            // Plattformspezifische Styles
            .macOSOnly { $0.frame(minWidth: 800) }
            .iOSOnly { $0.navigationViewStyle(.stack) }
    }
}</code></pre><p>Das Beispiel zeigt Modifier für plattformspezifisches Styling:</p><ul><li><p><code>.macOSOnlyPadding</code> fügt Padding nur auf macOS hinzu, wo Container wie <code>Form</code> kein Standard-Padding haben</p></li><li><p><code>.macOSOnlyFrame</code> setzt die auf macOS nötigen Mindestfenstergrößen</p></li><li><p>Plattform-Modifier (<code>.iOSOnly</code>, <code>.macOSOnly</code>, <code>.iOSExcluded</code>, etc.) sind für iOS, macOS, tvOS, visionOS und watchOS verfügbar und erlauben die gezielte Anwendung von View-Modifikationen auf bestimmten Plattformen</p></li></ul><p>Diese Modifier helfen dir, plattformgerechte Oberflächen zu erstellen und dabei den Code sauber und wartbar zu halten.</p><h3 id="border-mit-eckenradius">Border mit Eckenradius</h3><p>SwiftUI bietet keinen einfachen Weg, einer View eine Border mit Eckenradius hinzuzufügen. Der Standardansatz erfordert umständlichen Overlay-Code, den man sich schwer merken kann:</p><pre><code class="language-swift">Text(&quot;Without HandySwiftUI&quot;)
    .padding()
    .overlay(
        RoundedRectangle(cornerRadius: 12)
            .strokeBorder(.blue, lineWidth: 2)
    )</code></pre><p>HandySwiftUI vereinfacht das mit einem praktischen Border-Modifier:</p><pre><code class="language-swift">Text(&quot;With HandySwiftUI&quot;)
    .padding()
    .roundedRectangleBorder(.blue, cornerRadius: 12, lineWidth: 2)</code></pre><p><img src="/assets/images/blog/handyswiftui-view-modifiers/state-badges.webp" alt="State badges" loading="lazy" /></p><blockquote><p>Badges in <a href="https://translatekit.app/">TranslateKit</a> nutzen das zum Beispiel für abgerundete Rahmen.</p></blockquote><h3 id="bedingte-modifier">Bedingte Modifier</h3><p>Eine Reihe von Modifiern für den sauberen Umgang mit bedingten View-Anpassungen:</p><pre><code class="language-swift">struct DynamicContent: View {
    @State private var isEditMode = false
    @State private var accentColor: Color?

    var body: some View {
        ContentView()
            // Unterschiedliche Modifier je nach Bedingung anwenden
            .applyIf(isEditMode) {
                $0.overlay(EditingTools())
            } else: {
                $0.overlay(ViewingTools())
            }

            // Modifier nur anwenden, wenn Optional einen Wert hat
            .ifLet(accentColor) { view, color in
                view.tint(color)
            }
    }
}</code></pre><p>Das Beispiel zeigt <code>.applyIf</code>, das verschiedene View-Modifikationen basierend auf einer Boolean-Bedingung anwendet, und <code>.ifLet</code>, das wie Swifts <code>if let</code>-Statement funktioniert – es bietet nicht-optionalen Zugriff auf optionale Werte innerhalb des Closures. Beide Modifier helfen, Boilerplate-Code in SwiftUI-Views zu reduzieren.</p><h3 id="app-lifecycle-handling">App-Lifecycle-Handling</h3><p>Reagiere elegant auf App-Zustandsänderungen:</p><pre><code class="language-swift">struct MediaPlayerView: View {
    @StateObject private var player = VideoPlayer()

    var body: some View {
        PlayerContent(player: player)
            .onAppResignActive {
                // Wiedergabe pausieren, wenn die App in den Hintergrund geht
                player.pause()
            }
            .onAppBecomeActive {
                // Zustand wiederherstellen, wenn die App aktiv wird
                player.checkPlaybackState()
            }
    }
}</code></pre><p>Diese Modifier arbeiten zusammen, um eine flüssigere und wartbarere SwiftUI-Entwicklung zu ermöglichen, indem sie Boilerplate-Code reduzieren und gleichzeitig Qualität und Konsistenz deiner Benutzeroberfläche verbessern.</p><h3 id="löschbestätigungs-dialoge">Löschbestätigungs-Dialoge</h3><p>SwiftUIs Bestätigungsdialoge erfordern bei Löschaktionen immer wieder den gleichen Boilerplate-Code, besonders beim Löschen von Elementen aus einer Liste:</p><pre><code class="language-swift">struct TodoView: View {
    @State private var showDeleteConfirmation = false
    @State private var todos = [&quot;Buy milk&quot;, &quot;Walk dog&quot;]
    @State private var todoToDelete: String?

    var body: some View {
        List {
            ForEach(todos, id: \.self) { todo in
                Text(todo)
                    .swipeActions {
                        Button(&quot;Delete&quot;, role: .destructive) {
                            todoToDelete = todo
                            showDeleteConfirmation = true
                        }
                    }
            }
        }
        .confirmationDialog(&quot;Are you sure?&quot;, isPresented: $showDeleteConfirmation) {
            Button(&quot;Delete&quot;, role: .destructive) {
                if let todo = todoToDelete {
                    todos.removeAll { $0 == todo }
                    todoToDelete = nil
                }
            }
            Button(&quot;Cancel&quot;, role: .cancel) {
                todoToDelete = nil
            }
        } message: {
            Text(&quot;This delete action cannot be undone. Continue?&quot;)
        }
    }
}</code></pre><p>HandySwiftUI vereinfacht das mit einem dedizierten Modifier:</p><pre><code class="language-swift">struct TodoView: View {
    @State private var todoToDelete: String?
    @State private var todos = [&quot;Buy milk&quot;, &quot;Walk dog&quot;]

    var body: some View {
        List {
            ForEach(todos, id: \.self) { todo in
                Text(todo)
                    .swipeActions {
                        Button(&quot;Delete&quot;, role: .destructive) {
                            todoToDelete = todo
                        }
                    }
            }
        }
        .confirmDeleteDialog(item: $todoToDelete) { item in
            todos.removeAll { $0 == item }
        }
    }
}</code></pre><p><img src="/assets/images/blog/handyswiftui-view-modifiers/confirm-delete.webp" alt="Confirm delete" loading="lazy" /></p><blockquote><p>Puzzle-Löschung in <a href="https://crosscraft.app/">CrossCraft</a> mit einem Bestätigungsdialog, um versehentliches Löschen zu vermeiden.</p></blockquote><p>Das Beispiel zeigt, wie <code>.confirmDeleteDialog</code> den gesamten Lösch-Workflow – von der Bestätigung bis zur Ausführung – mit einem einzigen Modifier abwickelt. Der Dialog wird automatisch in ca. 40 Sprachen lokalisiert und folgt den Plattform-Designrichtlinien. Du kannst einen optionalen <code>message</code>-Parameter angeben, falls du eine andere Nachricht anzeigen möchtest. Es gibt auch eine Overload-Variante, die einen Boolean für Situationen nimmt, in denen keine Liste involviert ist.</p><h2 id="leg-noch-heute-los">Leg noch heute los</h2><p>Ich hoffe, du findest diese Modifier genauso nützlich in deinen Projekten wie ich in meinen. Wenn du Ideen für Verbesserungen oder zusätzliche Modifier hast, die der SwiftUI-Community zugutekommen könnten, trage gerne auf GitHub bei:</p><p><a class="sk-link-card sk-link-card-github" href="https://github.com/FlineDev/HandySwiftUI?ref=fline.dev"><span class="sk-link-card-source"><svg viewBox="0 0 16 16" width="16" height="16" fill="currentColor"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/></svg><span>github.com</span></span><span class="sk-link-card-title">FlineDev / HandySwiftUI</span><span class="sk-link-card-description">Handy SwiftUI features that didn’t make it into SwiftUI (yet)</span></a></p><p>Dies ist der zweite von vier Artikeln, die die Features von HandySwiftUI erkunden. Schau dir den vorherigen Artikel über <a href="https://www.fline.dev/handyswiftui-new-types/">Neue Typen</a> an, falls du ihn noch nicht gelesen hast, und bleib dran für die kommenden Beiträge über <a href="https://www.fline.dev/handyswiftui-extensions/">Extensions</a> und <a href="https://www.fline.dev/handyswiftui-styles/">Styles</a>!</p>]]></content:encoded>
</item>
<item>
<title>HandySwiftUI – Neue Typen: Unverzichtbare Views und Typen für die SwiftUI-Entwicklung</title>
<link>https://fline.dev/de/blog/handyswiftui-new-types/</link>
<guid isPermaLink="true">https://fline.dev/de/blog/handyswiftui-new-types/</guid>
<pubDate>Wed, 30 Oct 2024 00:00:00 +0000</pubDate>
<description><![CDATA[Von plattformspezifischen Werten ohne #if-Abfragen über ausgefeilte Auswahl-Controls bis hin zu Async-State-Management – entdecke die unverzichtbaren SwiftUI-Typen, die mir geholfen haben, Apps schneller auszuliefern. Diese praxiserprobten Views und Typen füllen häufige Lücken in der SwiftUI-Entwicklung.]]></description>
<content:encoded><![CDATA[<p>Nach 4 Jahren Feinschliff an diesen APIs in meinen eigenen Apps freue ich mich, das erste getaggte Release von <a href="https://github.com/FlineDev/HandySwiftUI">HandySwiftUI</a> zu teilen. Dieses Paket enthält verschiedene Hilfsfunktionen und Convenience-APIs, die mir enorm dabei geholfen haben, allein im letzten Jahr 10 Apps auszuliefern. Es bietet Komfortfunktionen für die SwiftUI-Entwicklung, ähnlich wie mein <a href="https://github.com/FlineDev/HandySwift">HandySwift</a>-Paket für Foundation.</p><p>In diesem Artikel stelle ich eine Auswahl der <em>neuen Typen</em> vor, die sich in meiner täglichen Arbeit an Apps wie <a href="https://translatekit.app/">TranslateKit</a>, <a href="https://freemiumkit.app/">FreemiumKit</a> und <a href="https://crosscraft.app/">CrossCraft</a> am meisten bewährt haben. HandySwiftUI enthält zwar noch viele weitere Hilfsfunktionen, aber diese Typen haben sich in der Praxis immer wieder als besonders wertvoll erwiesen und könnten auch für deine SwiftUI-Projekte nützlich sein.</p><h3 id="plattformspezifische-werte">Plattformspezifische Werte</h3><p>HandySwiftUI bietet eine elegante Möglichkeit, plattformspezifische Werte zu handhaben:</p><pre><code class="language-swift">struct AdaptiveView: View {
    enum TextStyle {
        case compact, regular, expanded
    }

    var body: some View {
        VStack {
            // Unterschiedliche Zahlenwerte pro Plattform
            Text(&quot;Welcome&quot;)
                .padding(Platform.value(default: 20.0, phone: 12.0))

            // Unterschiedliche Farben pro Plattform
            Circle()
                .fill(Platform.value(default: .blue, mac: .indigo, pad: .purple, vision: .cyan))
        }
    }
}</code></pre><p><img src="/assets/images/blog/handyswiftui-new-types/last30days.webp" alt="Last30days" loading="lazy" /></p><blockquote><p>Ein ähnliches Erscheinungsbild über Plattformen hinweg für einen Titel in <a href="https://freemiumkit.app/">FreemiumKit</a> via <code>.font(Platform.value(default: .title2, phone: .headline))</code>.</p></blockquote><p><code>Platform.value</code> funktioniert mit jedem Typ – von einfachen Zahlen über Farben und Fonts bis hin zu eigenen Typen. Du gibst einfach einen Standardwert an und überschreibst bei Bedarf bestimmte Plattformen. Das kann enorm nützlich sein, vor allem weil es sogar einen eigenen Case für iPad namens <code>pad</code> gibt – so kannst du Phones und Tablets separat ansprechen.</p><p>Das ist mit Abstand mein meistgenutzter HandySwiftUI-Helfer und spart mir massenhaft Boilerplate durch <code>#if</code>-Abfragen. Einfach, aber extrem wirkungsvoll!</p><h3 id="lesbare-preview-erkennung">Lesbare Preview-Erkennung</h3><p>Stelle Fake-Daten bereit und simuliere Ladezustände während der Entwicklung:</p><pre><code class="language-swift">Task {
   loadState = .inProgress

   if Xcode.isRunningForPreviews {
       // Netzwerkverzögerung in Previews simulieren
       try await Task.sleep(for: .seconds(1))
       self.data = Data()
       loadState = .successful
   } else {
       do {
           self.data = try await loadFromAPI()
           loadState = .successful
       } catch {
           loadState = .failed(error: error.localizedDescription)
       }
   }
}</code></pre><p><code>Xcode.isRunningForPreviews</code> ermöglicht es dir, echte Netzwerk-Requests zu umgehen und stattdessen sofortige oder verzögerte Fake-Antworten nur in SwiftUI-Previews bereitzustellen – perfekt fürs Prototyping und die UI-Entwicklung. Es ist auch nützlich, um begrenzte Ressourcen während der Entwicklung zu schonen, etwa API-Rate-Limits, Analytics-Events, die Statistiken verzerren könnten, oder Services, die pro Request abrechnen – packe diese einfach in eine <code>if !Xcode.isRunningForPreviews</code>-Abfrage.</p><h3 id="effizientes-laden-von-bildern">Effizientes Laden von Bildern</h3><p><code>CachedAsyncImage</code> bietet effizientes Laden von Bildern mit eingebautem Caching:</p><pre><code class="language-swift">struct ProductView: View {
    let product: Product

    var body: some View {
        VStack {
            CachedAsyncImage(url: product.imageURL)
                .frame(width: 200, height: 200)
                .clipShape(RoundedRectangle(cornerRadius: 10))

            Text(product.name)
                .font(.headline)
        }
    }
}</code></pre><p>Beachte, dass <code>.resizable()</code> und <code>.aspectRatio(contentMode: .fill)</code> bereits auf die <code>Image</code>-View im Inneren angewandt werden.</p><h3 id="erweiterte-auswahl-controls">Erweiterte Auswahl-Controls</h3><p>Mehrere ausgefeilte Picker-Typen für verschiedene Anwendungsfälle:</p><pre><code class="language-swift">struct SettingsView: View {
    @State private var selectedMood: Mood?
    @State private var selectedColors: Set&lt;Color&gt; = []
    @State private var selectedEmoji: Emoji?

    var body: some View {
        Form {
            // Vertikaler Option-Picker mit Icons
            VPicker(&quot;Select Mood&quot;, selection: $selectedMood)

            // Horizontaler Picker mit individuellem Styling
            HPicker(&quot;Rate your experience&quot;, selection: $selectedMood)

            // Mehrfachauswahl mit plattformadaptiver UI
            MultiSelector(
                label: { Text(&quot;Colors&quot;) },
                optionsTitle: &quot;Select Colors&quot;,
                options: [.red, .blue, .green],
                selected: $selectedColors,
                optionToString: \.description
            )

            // Durchsuchbarer Grid-Picker für Emoji- oder SF-Symbol-Auswahl
            SearchableGridPicker(
                title: &quot;Choose Emoji&quot;,
                options: Emoji.allCases,
                selection: $selectedEmoji
            )
        }
    }
}</code></pre><p><img src="/assets/images/blog/handyswiftui-new-types/settings-view.gif" alt="Settings view" loading="lazy" /></p><p>HandySwiftUI enthält <code>Emoji</code>- und <code>SFSymbol</code>-Enums mit gängigen Emojis und Symbolen. Du kannst auch eigene Enums erstellen, indem du <code>SearchableOption</code> konformierst und <code>searchTerms</code> für jeden Case angibst, um die Suchfunktion zu unterstützen.</p><h3 id="async-state-management">Async-State-Management</h3><p>Verfolge asynchrone Operationen mit typsicherem State-Handling über <code>ProgressState</code>:</p><pre><code class="language-swift">struct DocumentView: View {
    @State private var loadState: ProgressState&lt;String&gt; = .notStarted

    var body: some View {
        Group {
            switch loadState {
            case .notStarted:
                AsyncButton(&quot;Load Document&quot;) {
                    loadState = .inProgress
                    try await loadDocument()
                    loadState = .successful
                } catchError: { error in
                    loadState = .failed(error: error.localizedDescription)
                }

            case .inProgress:
                ProgressView(&quot;Loading document...&quot;)

            case .failed(let errorMessage):
                VStack {
                    Text(&quot;Failed to load document:&quot;)
                        .foregroundStyle(.secondary)
                    Text(errorMessage)
                        .foregroundStyle(.red)

                  AsyncButton(&quot;Try Again&quot;) {
                      loadState = .inProgress
                      try await loadDocument()
                      loadState = .successful
                  } catchError: { error in
                      loadState = .failed(error: error.localizedDescription)
                  }
                }

            case .successful:
                VStack {
                    DocumentContent()
                }
            }
        }
    }
}</code></pre><p>Das Beispiel zeigt, wie alle Zustände typsicher behandelt werden:</p><ul><li><p><code>.notStarted</code> zeigt den initialen Lade-Button</p></li><li><p><code>.inProgress</code> zeigt einen Ladeindikator</p></li><li><p><code>.failed</code> zeigt den Fehler mit einer Wiederholungsoption</p></li><li><p><code>.successful</code> zeigt den geladenen Inhalt</p></li></ul><h3 id="nsopenpanel-in-swiftui-nutzen"><code>NSOpenPanel</code> in SwiftUI nutzen</h3><p>Eine Brücke von nativem macOS-Dateizugriff zu SwiftUI, besonders nützlich für den Umgang mit sicherheitsbezogenen Ressourcen:</p><pre><code class="language-swift">struct SecureFileLoader {
    @State private var apiKey = &quot;&quot;

    func loadKeyFile(at fileURL: URL) async {
        #if os(macOS)
        // Auf macOS brauchen wir die Zustimmung des Nutzers für den Dateizugriff
        let panel = OpenPanel(
            filesWithMessage: &quot;Provide access to read key file&quot;,
            buttonTitle: &quot;Allow Access&quot;,
            contentType: .data,
            initialDirectoryUrl: fileURL
        )
        guard let url = await panel.showAndAwaitSingleSelection() else { return }
        #else
        let url = fileURL
        #endif

        guard url.startAccessingSecurityScopedResource() else { return }
        defer { url.stopAccessingSecurityScopedResource() }

        do {
            apiKey = try String(contentsOf: url)
        } catch {
            print(&quot;Failed to load file: \(error.localizedDescription)&quot;)
        }
    }
}</code></pre><p><img src="/assets/images/blog/handyswiftui-new-types/open-panel.webp" alt="Open panel" loading="lazy" /></p><p>Das Beispiel direkt aus <a href="https://freemiumkit.app/">FreemiumKit</a> zeigt, wie <code>OpenPanel</code> den Umgang mit sicherheitsbezogenem Dateizugriff für gezogene Elemente auf macOS vereinfacht und dabei die plattformübergreifende Kompatibilität beibehält.</p><h3 id="vertikale-tab-navigation">Vertikale Tab-Navigation</h3><p>Eine Alternative zu SwiftUIs <code>TabView</code>, die eine Sidebar-Navigation implementiert, wie sie häufig in macOS- und iPadOS-Apps zu sehen ist:</p><pre><code class="language-swift">struct MainView: View {
    enum Tab: String, CaseIterable, Identifiable, CustomLabelConvertible {
        case documents, recents, settings

        var id: Self { self }
        var description: String {
            rawValue.capitalized
        }
        var symbolName: String {
            switch self {
            case .documents: &quot;folder&quot;
            case .recents: &quot;clock&quot;
            case .settings: &quot;gear&quot;
            }
        }
    }

    @State private var selectedTab: Tab = .documents

    var body: some View {
        SideTabView(
            selection: $selectedTab,
            bottomAlignedTabs: 1  // Platziert Einstellungen unten
        ) { tab in
            switch tab {
            case .documents:
                DocumentList()
            case .recents:
                RecentsList()
            case .settings:
                SettingsView()
            }
        }
    }
}</code></pre><p><img src="/assets/images/blog/handyswiftui-new-types/side-tab-view.webp" alt="Side tab view" loading="lazy" /></p><p><code>SideTabView</code> bietet eine vertikale Sidebar mit Icons und Labels, optimiert für größere Bildschirme mit Unterstützung für am unteren Rand angeordnete Tabs. Die View übernimmt automatisch das plattformspezifische Styling und Hover-Effekte.</p><h2 id="leg-noch-heute-los">Leg noch heute los</h2><p>Ich hoffe, du findest diese Typen genauso nützlich in deinen Projekten wie ich in meinen. Wenn du Ideen für Verbesserungen oder zusätzliche Typen hast, die der SwiftUI-Community zugutekommen könnten, trage gerne auf GitHub bei:</p><p><a class="sk-link-card sk-link-card-github" href="https://github.com/FlineDev/HandySwiftUI?ref=fline.dev"><span class="sk-link-card-source"><svg viewBox="0 0 16 16" width="16" height="16" fill="currentColor"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/></svg><span>github.com</span></span><span class="sk-link-card-title">FlineDev / HandySwiftUI</span><span class="sk-link-card-description">Handy SwiftUI features that didn’t make it into SwiftUI (yet)</span></a></p><p>Dies ist der erste von vier Artikeln, die die Features von HandySwiftUI erkunden. Bleib dran für die kommenden Beiträge über <a href="https://www.fline.dev/handyswiftui-view-modifiers/">View Modifier</a>, <a href="https://www.fline.dev/handyswiftui-extensions/">Extensions</a> und <a href="https://www.fline.dev/handyswiftui-styles/">Styles</a>!</p>]]></content:encoded>
</item>
<item>
<title>Swift Packages auf Linux-Kompatibilität testen – direkt vom Mac</title>
<link>https://fline.dev/de/blog/test-your-swift-packages-linux-compatibility-on-mac/</link>
<guid isPermaLink="true">https://fline.dev/de/blog/test-your-swift-packages-linux-compatibility-on-mac/</guid>
<pubDate>Tue, 22 Oct 2024 00:00:00 +0000</pubDate>
<description><![CDATA[Du fragst dich, wie du die Linux-Kompatibilität deines Swift-Codes vom Mac aus testen kannst, ohne dich in Docker einarbeiten zu müssen? In diesem Artikel zeige ich dir einen einfachen Befehl, der den Prozess mühelos macht!]]></description>
<content:encoded><![CDATA[<p>Vor Kurzem stand ich vor der Situation, dass ich Swift-Netzwerkcode aus einer meiner Apps extrahieren musste, um ihn auf einem Vapor-Server wiederzuverwenden. Während der Code auf meinem Mac einwandfrei baute, stieß ich beim Deployment auf meinen Server auf mehrere Fehler. Das hat mich dazu gebracht, nach einer Möglichkeit zu suchen, die Linux-Kompatibilität des Codes einfach zu testen – denn Linux ist das Betriebssystem, das typischerweise auf Servern läuft. Zum Glück fand ich <a href="https://oleb.net/2020/swift-docker-linux/">diesen Artikel</a> von Ole Begemann aus dem Jahr 2020, der mir erspart hat, Docker von Grund auf lernen zu müssen.</p><p>In seinem Artikel beschreibt Ole einen unkomplizierten Ansatz, Swift-Code in einer Linux-Umgebung mit einem einzigen Befehl auszuführen. Du musst dafür lediglich die kostenlose <a href="https://www.docker.com/products/docker-desktop/">Docker Desktop App</a> auf deinem Mac installieren. Aber der Befehl schien mir recht lang und schwer zu merken, also wollte ich das Ganze noch weiter vereinfachen. Am Ende hatte ich einen Ansatz, bei dem ich mir nur <code>swift-linux</code> merken musste. So geht’s:</p><h2 id="den-docker-befehl-vereinfachen">Den Docker-Befehl vereinfachen</h2><p>Zunächst habe ich Oles Befehl für Kürze weiter vereinfacht:</p><pre><code class="language-zsh">docker run --rm -it -v &quot;$(pwd):/src&quot; -w &quot;/src&quot; swift</code></pre><p><strong>Erklärung des Befehls (falls es dich interessiert):</strong></p><ul><li><p><code>docker run</code>: Dieser Befehl erstellt und startet einen Container.</p></li><li><p><code>--rm</code>: Entfernt den Container automatisch, wenn er beendet wird.</p></li><li><p><code>-it</code>: Führt den Container im interaktiven Modus mit angeschlossenem Terminal aus.</p></li><li><p><code>-v &quot;$(pwd):/src&quot;</code>: Bindet das aktuelle Verzeichnis (<code>$(pwd)</code>) an <code>/src</code> im Container ein, sodass du auf deine Swift-Dateien zugreifen kannst.</p></li><li><p><code>-w &quot;/src&quot;</code>: Setzt das Arbeitsverzeichnis im Container auf <code>/src</code>.</p></li><li><p><code>swift</code>: Gibt das zu verwendende Swift-Docker-Image an.</p></li></ul><p>Beachte, dass ich auch die Option <code>--privileged</code> aus Oles Befehl aus Sicherheitsgründen entfernt habe, da sie zum Testen von Swift Packages selten benötigt wird.</p><h2 id="einen-praktischen-alias-hinzufügen">Einen praktischen Alias hinzufügen</h2><p>Um das Ausführen dieses Befehls einfacher zu machen, habe ich einen Alias zu meiner <code>~/.zshrc</code>-Datei hinzugefügt (z.B. über <code>touch ~/.zshrc</code>). Das ist eine Konfigurationsdatei für die Zsh-Shell, die heutzutage die Standard-Shell auf macOS ist. So geht’s:</p><ul><li><p><strong>Terminal öffnen</strong></p></li><li><p><strong>Die <code>~/.zshrc</code>-Datei bearbeiten</strong>
Du kannst TextEdit verwenden, den vorinstallierten Texteditor auf macOS.
Führe einfach aus: <code>open ~/.zshrc</code></p></li><li><p><strong>Den Alias hinzufügen</strong>
Scrolle zum Ende der Datei und füge die folgende Zeile hinzu:</p></li></ul><pre><code class="language-bash">alias swift-linux='docker run --rm -it -v &quot;$(pwd):/src&quot; -w &quot;/src&quot; swift'</code></pre><ul><li><p><strong>Datei speichern und schließen</strong></p></li><li><p><strong>Änderungen anwenden</strong>
Führe den folgenden Befehl aus, um deine Konfigurationsdatei neu zu laden: <code>source ~/.zshrc</code></p></li></ul><h2 id="befehle-in-einer-linux-umgebung-ausführen">Befehle in einer Linux-Umgebung ausführen</h2><p>Jetzt, da der Alias eingerichtet ist, kannst du deinen Swift-Code ganz einfach direkt von deinem Mac aus in einer Linux-Umgebung testen. Navigiere einfach im Terminal zu deinem Projektverzeichnis und führe aus:</p><pre><code class="language-bash">swift-linux</code></pre><blockquote><p>Beim ersten Mal dauert der Download des Linux-Containers etwas.</p></blockquote><p>Dieser Befehl versetzt dich in eine Linux-Umgebung, in der du je nach Bedarf <code>swift build</code> oder <code>swift test</code> ausführen kannst. Tippe einfach <code>exit</code> ein, um zu deinem Mac zurückzukehren. So kannst du schnell überprüfen, ob alles wie erwartet funktioniert, und Fehler erkennen, bevor du auf deinen Server deployest.</p><p>Und alles, was du dir merken musst, ist <code>swift-linux</code>!</p>]]></content:encoded>
</item>
<item>
<title>LinksKit: App-Links für Swift-Entwickler vereinfacht</title>
<link>https://fline.dev/de/blog/introducing-linkskit-simplifying-app-links-for-swift-developers/</link>
<guid isPermaLink="true">https://fline.dev/de/blog/introducing-linkskit-simplifying-app-links-for-swift-developers/</guid>
<pubDate>Wed, 09 Oct 2024 00:00:00 +0000</pubDate>
<description><![CDATA[Keine Lust mehr, immer wieder die gleichen Links in deinen Apps einzubauen? LinksKit ist das Swift Package, das alles abdeckt – von rechtlichen Anforderungen bis Cross-Promotion – und dir Zeit spart und die Sichtbarkeit deiner Apps steigert.]]></description>
<content:encoded><![CDATA[<p>Als Indie-Entwickler, der in den letzten zwei Jahren neun Apps veröffentlicht hat, bin ich immer wieder auf dieselbe Herausforderung gestoßen: die Implementierung grundlegender Links in jeder App. Diese repetitive Aufgabe hat mich dazu gebracht, LinksKit zu entwickeln – ein Swift Package, das den Prozess des Hinzufügens wichtiger Links zu deinen iOS-, macOS- und visionOS-Apps vereinfacht.</p><h2 id="warum-ich-linkskit-gebaut-habe">Warum ich LinksKit gebaut habe</h2><p>Nach dem Launch mehrerer Apps auf verschiedenen Apple-Plattformen ist mir ein Muster aufgefallen. Jede App brauchte ähnliche Link-Bereiche:</p><ol><li><p>Rechtliche Links (Datenschutzrichtlinie, Nutzungsbedingungen)</p></li><li><p>Support-Links (FAQs, Kontakt-E-Mail)</p></li><li><p>App-Review-Deep-Link</p></li><li><p>Social-Media-Links</p></li><li><p>Cross-Promotion für meine anderen Apps</p></li></ol><p>Diese Links zu implementieren wurde zu einem zeitaufwändigen und repetitiven Prozess. Und auf dem Mac war alles ganz anders, was zusätzliche Arbeit für Multi-Plattform-Apps bedeutete. Da wurde mir klar, dass ich eine wiederverwendbare Lösung brauchte, und <a href="https://github.com/FlineDev/LinksKit">LinksKit</a> war geboren.</p><h2 id="warum-jede-neue-app-linkskit-nutzen-sollte">Warum jede neue App LinksKit nutzen sollte</h2><ol><li><p><strong>Zeitersparnis</strong>: LinksKit bietet eine sofort einsetzbare Lösung für gängige Link-Anforderungen, sodass sich Entwickler auf die Kernfunktionen der App konzentrieren können.</p></li><li><p><strong>Compliance</strong>: Es stellt sicher, dass deine App alle notwendigen rechtlichen Links enthält und hilft dir, die App-Store-Richtlinien mühelos einzuhalten.</p></li><li><p><strong>Cross-Promotion</strong>: LinksKit macht es einfach, deine anderen Apps zu präsentieren und so die Sichtbarkeit deines gesamten App-Portfolios zu steigern.</p></li><li><p><strong>Networking-Möglichkeiten</strong>: Das Package ermöglicht es dir, Apps von befreundeten Entwicklern zu bewerben, was eine unterstützende Community und potenzielle Cross-Promotion-Partnerschaften fördert.</p></li><li><p><strong>Anpassbarkeit</strong>: Neben den vorgefertigten Lösungen bietet LinksKit auch volle Anpassungsmöglichkeiten für deine spezifischen Bedürfnisse.</p></li><li><p><strong>Plattform-Anpassung</strong>: Es funktioniert nahtlos auf iOS, macOS und visionOS und passt sich den UI-Konventionen jeder Plattform an.</p></li></ol><h2 id="so-verwendest-du-linkskit">So verwendest du LinksKit</h2><p>Der Einstieg mit LinksKit ist unkompliziert, aber bevor wir in den Code eintauchen, besprechen wir ein wichtiges Konzept: den <code>providerToken</code>.</p><h3 id="den-providertoken-verstehen">Den <code>providerToken</code> verstehen</h3><p>Der <code>providerToken</code> ist eine entscheidende Komponente in App-Store-Marketingkampagnen. Es handelt sich um eine eindeutige Kennung für dein Entwicklerkonto, die dabei hilft, die Wirksamkeit deiner Marketingmaßnahmen zu verfolgen. Wenn Nutzer auf Links mit deinem <code>providerToken</code> klicken, kann Apple diese Klicks deinen Kampagnen zuordnen und wertvolle Einblicke in die Nutzergewinnung deiner App liefern. Das Wort “Kampagne” klingt übertrieben kompliziert, aber es ist eigentlich nur ein Parameter, der angibt, woher die Nutzer kommen.</p><h4 id="so-findest-du-deinen-providertoken">So findest du deinen <code>providerToken</code></h4><p>Den <code>providerToken</code> zu finden ist nicht sofort offensichtlich, aber so geht’s:</p><ol><li><p>Melde dich bei App Store Connect an</p></li><li><p>Navigiere zu Analytics &gt; Acquisition &gt; Campaigns</p></li><li><p>Klicke auf “Create Campaign Link”</p></li><li><p>Gib einen beliebigen Text in das Formular ein (du musst nicht wirklich eine Kampagne erstellen)</p></li><li><p>In der Kampagnen-Link-Vorschau suchst du den Parameter <code>pt</code> – der Wert nach <code>pt=</code> ist dein <code>providerToken</code></p></li></ol><p>Merke dir: Dieser Token ist für alle deine Apps gleich, du musst ihn also nur einmal nachschlagen.</p><h3 id="grundkonfiguration">Grundkonfiguration</h3><p>Jetzt, da du den <code>providerToken</code> verstehst, richten wir LinksKit in deiner App ein:</p><pre><code class="language-swift">LinksKit.configure(
   providerToken: &quot;123456&quot;,
   linkSections: [
      .helpLinks(appID: &quot;123456789&quot;, supportEmail: &quot;support@example.com&quot;),
      .legalLinks(privacyURL: URL(string: &quot;https://example.com/privacy&quot;)!)
   ]
)</code></pre><p>Diese grundlegende Einrichtung fügt deiner App die wesentlichen Hilfe- und Rechts-Links hinzu, einschließlich der Nutzungsbedingungen.</p><h3 id="umfassendes-beispiel">Umfassendes Beispiel</h3><p>Schauen wir uns ein umfassenderes Beispiel an, das alle integrierten Features von LinksKit zeigt – mit Social-Links und Links zu <em>deinen</em> Apps und den Apps <em>deiner Freunde</em>:</p><pre><code class="language-swift">// App-Links
let ownApps = LinkSection(entries: [
   .link(.ownApp(id: &quot;6502914189&quot;, name: &quot;FreemiumKit&quot;, systemImage: &quot;cart&quot;)),
   .link(.ownApp(id: &quot;6480134993&quot;, name: &quot;FreelanceKit&quot;, systemImage: &quot;timer&quot;)),
   .link(.ownApp(id: &quot;6472669260&quot;, name: &quot;CrossCraft&quot;, systemImage: &quot;puzzlepiece&quot;)),
   .link(.ownApp(id: &quot;6477829138&quot;, name: &quot;FocusBeats&quot;, systemImage: &quot;music.note&quot;)),
])

let friendsApps = LinkSection(entries: [
   .link(.friendsApp(id: &quot;1249686798&quot;, name: &quot;NFC.cool Tools&quot;, systemImage: &quot;tag&quot;, providerToken: &quot;106913804&quot;)),
   .link(.friendsApp(id: &quot;6503256642&quot;, name: &quot;App Exhibit&quot;, systemImage: &quot;square.grid.3x3.fill.square&quot;)),
])

// LinksKit konfigurieren
LinksKit.configure(
   providerToken: &quot;549314&quot;,
   linkSections: [
      .helpLinks(
         appID: &quot;6476773066&quot;,
         faqURL: URL(string: &quot;https://translatekit.app/#faq&quot;)!,
         supportEmail: &quot;translatekit@fline.dev&quot;
      ),
      .socialMenus(
         appLinks: .appSocialLinks(
            platforms: [.twitter, .mastodon(instance: &quot;mastodon.social&quot;), .threads],
            handle: &quot;TranslateKit&quot;,
            handleOverrides: [.twitter: &quot;TranslateKitApp&quot;]
         ),
         developerLinks: .developerSocialLinks(
            platforms: [.twitter, .mastodon(instance: &quot;iosdev.space&quot;), .threads],
            handle: &quot;Jeehut&quot;
         )
      ),
      .appMenus(ownAppLinks: [ownApps], friendsAppLinks: [friendsApps]),
      .legalLinks(privacyURL: Constants.legalPrivacyPolicyURL)
   ]
)</code></pre><p>Die obige Konfiguration habe ich (vereinfacht) aus meiner App <a href="https://translatekit.app/">TranslateKit</a> übernommen:</p><ol><li><p>Richtet Hilfe-Links mit FAQ und Support-E-Mail ein</p></li><li><p>Fügt Social-Media-Links sowohl für die App als auch den Entwickler hinzu</p></li><li><p>Erstellt ein Menü zur Bewerbung meiner anderen Apps</p></li><li><p>Enthält einen Bereich für die Apps von Freunden (großartig fürs Networking!)</p></li><li><p>Stellt sicher, dass alle notwendigen rechtlichen Links vorhanden sind</p></li></ol><p>Um diese konfigurierten Links zu verwenden, füge einfach <code>LinksView()</code> zu deinem Einstellungsbildschirm hinzu:</p><pre><code class="language-swift">Form {
   // Andere Einstellungen...
   LinksView()
}</code></pre><p>Das Ergebnis sieht auf iOS etwa so aus:</p><p><img src="/assets/images/blog/introducing-linkskit-simplifying-app-links-for-swift-developers/phone-settings.webp" alt="Telefon-Einstellungen" loading="lazy" /></p><p>Auf dem Mac ist es üblicher, Links im Hilfe-Menü zu platzieren. So geht’s:</p><pre><code class="language-swift">WindowGroup {
   // dein UI-Code
}
.commands {
   CommandGroup(replacing: .help) {
      LinksView()
         .labelStyle(.titleAndIcon)
   }
}</code></pre><p>Es erscheint dann so im Menü:</p><p><img src="/assets/images/blog/introducing-linkskit-simplifying-app-links-for-swift-developers/mac-help-menu.webp" alt="Mac-Hilfe-Menü" loading="lazy" /></p><h2 id="eigene-link-bereiche">Eigene Link-Bereiche</h2><p>Neben den vorgefertigten Bereichen kannst du mit <code>LinkSection</code> auch komplett eigene Bereiche erstellen. Diese Flexibilität ermöglicht es dir, beliebige Link-Typen oder verschachtelte Menüstrukturen hinzuzufügen.</p><p>Hier ein Beispiel für einen eigenen Bereich:</p><pre><code class="language-swift">let customSection = LinkSection(
    title: &quot;Custom Links&quot;,
    entries: [
        .link(Link(title: &quot;Our Website&quot;, systemImage: &quot;globe&quot;, url: URL(string: &quot;https://www.example.com&quot;)!)),
        .menu(LinkMenu(
            title: &quot;Social Media&quot;,
            systemImage: &quot;network&quot;,
            linkSections: [
                LinkSection(entries: [
                    .link(Link.followUsOn(socialPlatform: .twitter, handle: &quot;YourAppHandle&quot;)),
                    .link(Link.followUsOn(socialPlatform: .instagram, handle: &quot;YourAppHandle&quot;))
                ])
            ]
        ))
    ]
)</code></pre><p>Dieses Beispiel zeigt, wie du Menüs verschachteln und eine Hierarchie von Links erstellen kannst, die zu den Anforderungen deiner App passt.</p><h2 id="eigene-label-styles">Eigene Label-Styles</h2><p>LinksKit bietet Flexibilität nicht nur beim Inhalt, sondern auch bei der Darstellung. Du kannst das Erscheinungsbild deiner Links ganz einfach mit SwiftUIs <code>labelStyle</code>-Modifier anpassen. LinksKit stellt drei eigene Label-Styles bereit, um die visuelle Gestaltung deiner Links zu verbessern:</p><ol><li><p><code>.titleAndTrailingIcon</code>: Ähnlich dem Standard-<code>.titleAndIcon</code>-Style, aber platziert das Icon am Ende des Labels.</p></li><li><p><code>.titleAndIconBadge(color:)</code>: Imitiert den Style von Apples Einstellungen-App, indem den führenden Icons ein farbiger Hintergrund hinzugefügt wird.</p></li><li><p><code>.titleAndTrailingIconBadge(color:)</code>: Kombiniert die Icon-Platzierung am Ende mit dem farbigen Hintergrund.</p></li></ol><p>Um diese Styles anzuwenden, füge einfach den <code>labelStyle</code>-Modifier zu deiner <code>LinksView</code> hinzu:</p><pre><code class="language-swift">LinksView()
    .labelStyle(.titleAndIconBadge(color: .blue))</code></pre><p>Diese einfache Anpassung kann das Erscheinungsbild deiner Links deutlich verändern und dir ermöglichen, sie an die Design-Sprache deiner App anzupassen oder eine klare visuelle Hierarchie zu schaffen. Hier ein visueller Vergleich dieser Styles:</p><p><img src="/assets/images/blog/introducing-linkskit-simplifying-app-links-for-swift-developers/links-kit.webp" alt="LinksKit" loading="lazy" /></p><p>Durch den Einsatz eigener Label-Styles kannst du einen einzigartigen und ausgefeilten Look für den Link-Bereich deiner App schaffen und so die gesamte Nutzererfahrung verbessern.</p><h2 id="fazit">Fazit</h2><p>LinksKit ist mehr als nur ein zeitsparendes Tool – es ist eine umfassende Lösung für die Verwaltung von App-Links, die Einhaltung rechtlicher Vorgaben und die Förderung von Entwickler-Networking. Indem es alles von rechtlichen Anforderungen bis hin zu Cross-Promotion abdeckt, ermöglicht dir LinksKit, dich auf das zu konzentrieren, was wirklich zählt – großartige Apps zu entwickeln.</p><p>Egal ob du ein erfahrener Entwickler mit mehreren Apps bist oder gerade dein erstes Projekt startest, LinksKit kann deinen Entwicklungsprozess erheblich vereinfachen. Probiere es in deinem nächsten Projekt aus und erlebe die Vorteile eines vereinfachten Link-Managements auf allen Apple-Plattformen! Ich hoffe, es hilft dir weiter.</p><p><a class="sk-link-card sk-link-card-github" href="https://github.com/FlineDev/LinksKit?ref=fline.dev"><span class="sk-link-card-source"><svg viewBox="0 0 16 16" width="16" height="16" fill="currentColor"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/></svg><span>github.com</span></span><span class="sk-link-card-title">FlineDev / LinksKit</span><span class="sk-link-card-description">SwiftUI convenience view to show common links in apps settings/help menu</span></a></p>]]></content:encoded>
</item>
<item>
<title>Warum ich aufgehört habe, für visionOS zu entwickeln (und was mich zurückbringen könnte)</title>
<link>https://fline.dev/de/blog/why-i-stopped-building-for-visionos-and-what-could-bring-me-back/</link>
<guid isPermaLink="true">https://fline.dev/de/blog/why-i-stopped-building-for-visionos-and-what-could-bring-me-back/</guid>
<pubDate>Mon, 07 Oct 2024 00:00:00 +0000</pubDate>
<description><![CDATA[Entdecke die Einschränkungen, die die Vision Pro daran hindern, ihr volles Potenzial zu entfalten. Dieser Artikel beleuchtet die fehlenden APIs, die nötig sind, um sie in eine echte Mixed-Reality-Plattform zu verwandeln, und diskutiert, was sich dafür ändern muss.]]></description>
<content:encoded><![CDATA[<p>Als Apple die Vision Pro erstmals vorstellte, war ich unglaublich begeistert. Das Potenzial der Plattform schien grenzenlos, und als Entwickler wirkte es wie eine neue Welt, die es zu erkunden galt. Ich bin sofort voll eingestiegen und sah eine seltene Chance für Experimente und Innovation. Nachdem ich jedoch einige Apps veröffentlicht und intensiv mit visionOS gearbeitet hatte, wurde mir klar, dass ich auf Einschränkungen stieß, die es so nicht geben sollte.</p><p>Ich hatte mehrere einzigartige App-Ideen, aber mir fehlten die APIs, die diese Konzepte überhaupt erst umsetzbar gemacht hätten. Ich hätte mehr Apps veröffentlicht, wenn visionOS die nötigen Werkzeuge für die AR/VR-Erlebnisse geliefert hätte, die das Gerät verspricht. Es geht mir dabei nicht um eine offenere Plattform wie macOS – ich verstehe den Wert der Sicherheit und Einfachheit im iOS/iPadOS-Ökosystem. Doch visionOS fühlt sich an wie iPadOS mit anderen Eingabemethoden. Was fehlt, sind einzigartige APIs, die speziell dafür entwickelt wurden, die immersiven Erlebnisse voll auszuschöpfen, die die Vision Pro bieten kann.</p><p>Hier sind die fünf APIs, die meiner Meinung nach visionOS von einem vielversprechenden Experiment in eine Plattform verwandeln könnten, für die es sich wirklich lohnt zu entwickeln:</p><h3 id="1-magnetisches-anheften-von-fenstern-und-objekten-an-oberflächen">1. Magnetisches Anheften von Fenstern und Objekten an Oberflächen</h3><p><img src="/assets/images/blog/why-i-stopped-building-for-visionos-and-what-could-bring-me-back/kristyna-squared-one.webp" alt="Eine Frauenhand hält einen Magneten an einem Kühlschrank" loading="lazy" />
<em>Foto von <a href="https://unsplash.com/@squared_one1">Kristyna Squared.one</a> / <a href="https://unsplash.com/?utm_source=ghost&utm_medium=referral&utm_campaign=api-credit">Unsplash</a></em></p><p>Stell dir vor, du könntest Fenster oder 3D-Objekte an Wänden oder Möbeln in einem Raum anheften – dauerhaft. Dieses Feature würde es Nutzern ermöglichen, persistente Setups in ihrer Umgebung zu erstellen, und Entwicklern die Möglichkeit geben, die Anheftungsfunktion je nach Kontext ihres Erlebnisses ein- und auszuschalten. Zum Beispiel würde meine <a href="https://apps.apple.com/app/apple-store/id6478062053?pt=549314&ct=fline.dev&mt=8">Posters</a>-App dann endlich Sinn ergeben! Noch wichtiger: Diese angehefteten Objekte sollten auch nach einem Systemneustart an Ort und Stelle bleiben, ähnlich wie Desktop-Umgebungen oder Fokusmodi. Diese Funktionalität sollte sich auch über verschiedene Nutzer hinweg erstrecken – beim Wechsel in den Gastmodus sollten alle angehefteten Elemente genau dort bleiben, wo sie platziert wurden.</p><p>Derzeit fühlt sich die Erweiterung der Realität auf der Vision Pro vorübergehend an, weil diese API fehlt, was sie eher zu einem reinen VR-Gerät macht – und ich glaube nicht, dass das Apples Vision für die Plattform ist. Diese Einschränkung begrenzt das volle Potenzial von Mixed-Reality-Erlebnissen, da Nutzer sich nicht darauf verlassen können, dass ihre digitalen Objekte an ihrem Platz im physischen Raum bleiben. Die Möglichkeit, Fenster und Objekte magnetisch anzuheften, ist daher die wichtigste API, die nötig ist, um die Vision Pro über ein VR-Erlebnis hinauszuheben und ihr wahres Mixed-Reality-Potenzial zu verwirklichen.</p><h3 id="2-erweitertes-raum-scanning-mit-editierbaren-3d-modellen">2. Erweitertes Raum-Scanning mit editierbaren 3D-Modellen</h3><p><img src="/assets/images/blog/why-i-stopped-building-for-visionos-and-what-could-bring-me-back/dynamic-wang-unsplash.webp" alt="Ein Raum mit einem großen Ball" loading="lazy" />
<em>Foto von <a href="https://unsplash.com/@dynamicwang">Dynamic Wang</a> / <a href="https://unsplash.com/?utm_source=ghost&utm_medium=referral&utm_campaign=api-credit">Unsplash</a></em></p><p>Eine hybride API, die die Leistung von RoomPlan und dem auf iOS verfügbaren 3D-Scanner kombiniert, könnte die Möglichkeiten von Entwicklern zur Erstellung immersiver Inhalte revolutionieren. Während das Scannen eines Raums derzeit möglich ist, fehlt die nötige Tiefe für vollständig interaktive Räume. Der nächste Schritt sollte es ermöglichen, nicht nur die Abmessungen, sondern auch die Farben und Texturen zu erfassen, um hochdetaillierte 3D-Modelle zu erzeugen.</p><p>Warum sind diese Scanning-APIs nicht auf der Apple Vision Pro verfügbar? Sie benötigen einen LiDAR-Sensor, den die Vision Pro hat. Die Erstellung auf der Vision Pro wäre komfortabler und würde es Nutzern ermöglichen, die Ergebnisse sofort in 3D zu sehen. Idealerweise könnte Apple auch eine App für Entwickler bereitstellen, um diese Umgebungen direkt in der Vision Pro mit Handgesten zu bearbeiten. Wenn ich zum Beispiel einen Tisch verschiebe und den Raum erneut scanne, sollte das System die Änderung erkennen und den Tisch als bewegliches Objekt behandeln. Apples KI-Tools könnten Lücken mit realistischen Texturen und Objekten füllen, die 3D-Umgebungserstellung intuitiver machen und den Bedarf an komplexen CAD-Tools reduzieren.</p><h3 id="3-eine-skelett-erkennungs-api-für-spaßige-ar-interaktionen">3. Eine Skelett-Erkennungs-API für spaßige AR-Interaktionen</h3><p><img src="/assets/images/blog/why-i-stopped-building-for-visionos-and-what-could-bring-me-back/dalle-skeletal-recognition.webp" alt="DALL-E Illustration der Skelett-Erkennung" loading="lazy" /></p><p>Eine meiner liebsten App-Ideen beinhaltete die Erkennung von Körperteilen mithilfe von Apples AR-Frameworks, um ein bisschen <em>Die Sims</em> in die reale Welt zu bringen. Stell dir vor, du gehst herum und siehst einen schwebenden grünen Diamanten (wie den Plumbob aus den Sims) über jeder Person, der es dir ermöglicht, auf einzigartige und spielerische Weise mit ihnen zu interagieren. Um das möglich zu machen, wäre eine einfache Skelett-Tracking-API ein Gamechanger, die es Entwicklern ermöglichen würde, Körperbewegungen und Gesten zu erkennen. Selbst eine grobe Schätzung der Kopfposition oder Armbewegung könnte Features wie interaktive Sprechblasen ermöglichen, basierend auf dem, was jemand sagt oder fühlt. Fortgeschrittene APIs könnten Gesichtsausdrücke erkennen und so die Möglichkeit für schwebende Emotions-Icons oder Interaktionsvorschläge schaffen, ähnlich wie in <em>Die Sims</em>.</p><p>Ich verstehe, dass Apple keinen vollen Kamerazugriff gewähren möchte – und ehrlich gesagt würde ich die Vision Pro auch nicht nutzen, wenn sie es täten. Da die Kamera aber bereits an ist, könnte das System erkannte Weltinformationen teilen, die die Privatsphäre der Nutzer respektieren und gleichzeitig solche Erlebnisse ermöglichen. Es gibt noch viel ungenutztes Potenzial, das AR-Interaktionen deutlich verbessern könnte.</p><h3 id="4-interaktive-räumliche-360--und-180-videoelemente">4. Interaktive räumliche 360°- und 180°-Videoelemente</h3><p><img src="/assets/images/blog/why-i-stopped-building-for-visionos-and-what-could-bring-me-back/roger-ce-unsplash.webp" alt="Ein Spielzeugauto mit offener Motorhaube auf dem Boden" loading="lazy" />
<em>Foto von <a href="https://unsplash.com/@roger_ce77">Roger Ce</a> / <a href="https://unsplash.com/?utm_source=ghost&utm_medium=referral&utm_campaign=api-credit">Unsplash</a></em></p><p>Während die Vision Pro immersive Video-Erlebnisse verspricht, ist es derzeit unmöglich, interaktive Buttons oder Objekte in diese räumlichen 180°/360°-Videos zu integrieren. Entwickler sollten in der Lage sein, klickbare Elemente im Raum zu platzieren, während man sich in einem Video befindet – stell dir interaktive Touren oder geführte Erlebnisse vor, bei denen Nutzer auf Objekte klicken können, um mehr Informationen zu erhalten oder zwischen Videos zu wechseln, die verschiedene Zeiten oder Perspektiven zeigen.</p><p>Dieses Feature könnte es Nutzern auch ermöglichen, innerhalb eines Ortes zu „Zeitreisen”, indem sie nahtlos zwischen Videoaufnahmen desselben Ortes zu verschiedenen Zeiten wechseln. Derzeit erfordern solche Interaktionen das mühsame Erstellen von 3D-Umgebungen von Grund auf. Eine einfache API zum Platzieren interaktiver Elemente in Videos würde den Prozess enorm vereinfachen und endlose Möglichkeiten für interaktives Storytelling, Bildungstools und mehr eröffnen. Aktuell ist viel Aufwand nötig, um auch nur annähernd ein solches Erlebnis zu schaffen, einschließlich des Baus eines eigenen Videoplayers in RealityKit mit verschiedenen Bildern für jedes Auge. Solche Hacks sollten nicht nötig sein.</p><h3 id="5-die-welt-entdecken-die-zukunft-von-apple-maps">5. Die Welt entdecken: Die Zukunft von Apple Maps</h3><p><img src="/assets/images/blog/why-i-stopped-building-for-visionos-and-what-could-bring-me-back/jezael-melgoza-unsplash.webp" alt="Menschen gehen auf einer Straße in der Nähe beleuchteter Gebäude" loading="lazy" />
<em>Foto von <a href="https://unsplash.com/@jezar">Jezael Melgoza</a> / <a href="https://unsplash.com/?utm_source=ghost&utm_medium=referral&utm_campaign=api-credit">Unsplash</a></em></p><p>Eine der überraschendsten Lücken in visionOS ist das Fehlen einer nativen Maps-App, die für die Vision Pro optimiert ist. Während <em>Umsehen</em> auf anderen Geräten immersive Straßenansichten bietet, könnte die Vision Pro dieses Erlebnis auf ein neues Level heben, indem Nutzer direkt in 3D-Umgebungen versetzt werden, in denen sie in Echtzeit mit ihrer Umgebung interagieren können. Apples KI-Technologie, die Fotos in 3D-Szenen verwandelt, könnte genutzt werden, um die bestehenden <em>Umsehen</em>-Bilder in interaktive Umgebungen umzuwandeln und neue Möglichkeiten für immersive, standortbasierte Apps zu erschließen.</p><p>Eine richtige Vision Pro Maps-API könnte dieses Erlebnis komplett verändern. Stell dir vor, du gehst (oder besser gesagt beamst dich) durch eine Stadt und interagierst mit virtuellen Objekten, die an bestimmte Orte gebunden sind – sei es für ein Spiel, zu Bildungszwecken oder sogar einen immersiven Reiseplaner. Entwickler könnten Erlebnisse schaffen, bei denen Nutzer durch historische Rekonstruktionen navigieren, zukünftige Stadtpläne erkunden oder mit dynamischen Inhalten interagieren, während sie durch die Straßen streifen.</p><h2 id="der-schlüssel-zum-erfolg-der-vision-pro">Der Schlüssel zum Erfolg der Vision Pro</h2><p><img src="/assets/images/blog/why-i-stopped-building-for-visionos-and-what-could-bring-me-back/razvan-chisu-unsplash.webp" alt="Froschperspektive eines Mannes zwischen Gebäuden" loading="lazy" />
<em>Foto von <a href="https://unsplash.com/@nullplus">Razvan Chisu</a> / <a href="https://unsplash.com/?utm_source=ghost&utm_medium=referral&utm_campaign=api-credit">Unsplash</a></em></p><p>Das sind genau die Art von APIs, die nicht nur Entwickler wie mich bei visionOS halten würden, sondern auch die Erstellung immersiverer und fesselnderer Inhalte vorantreiben würden. Im Moment fehlen der Vision Pro die Killer-Apps, die Nutzer zum Kauf bewegen, und ohne genügend Nutzer zögern Entwickler, signifikant Zeit in die Plattform zu investieren. Es ist ein Kreislauf, der durchbrochen werden muss, und die Lösung liegt auf der Hand – Apple muss die Inhaltserstellung für Entwickler so einfach wie möglich machen.</p><p>Ich verlange kein offenes System oder realitätsferne Features – ich verlange APIs, die das echte Potenzial der Vision Pro für Entwickler und Nutzer gleichermaßen entfesseln können. Ohne sie ist es schwer zu sehen, wie die Plattform die nötige Dynamik gewinnen soll. Aber wenn Apple auch nur einige dieser APIs einführt, wäre ich sofort bereit, wieder einzusteigen und Apps zu bauen, die die Grenzen von AR und VR verschieben könnten – und die Vision Pro letztlich zu dem bahnbrechenden Gerät machen, das sie sein sollte.</p>]]></content:encoded>
</item>
<item>
<title>Gastbeitrag: Warum ich FreemiumKit statt RevenueCat für meine App gewählt habe</title>
<link>https://fline.dev/de/blog/why-i-chose-freemiumkit-over-revenuecat-for-my-diabetes-management-app-guest-post/</link>
<guid isPermaLink="true">https://fline.dev/de/blog/why-i-chose-freemiumkit-over-revenuecat-for-my-diabetes-management-app-guest-post/</guid>
<pubDate>Sat, 24 Aug 2024 00:00:00 +0000</pubDate>
<author>Guest Author</author>
<description><![CDATA[Hast du Schwierigkeiten mit der Integration von In-App-Abos? Erfahre, wie FreemiumKit meinen Entwicklungsprozess transformiert hat, mir half, Herausforderungen mit RevenueCat zu überwinden, und den Launch meiner App beschleunigt hat.]]></description>
<content:encoded><![CDATA[<h2 id="die-bisherige-reise">Die bisherige Reise</h2><p>Ich arbeite seit etwa anderthalb Jahren an einer Diabetes-Management-App, die ich <em>Glu Sight</em> genannt habe. Als der Veröffentlichungstermin näher rückte, musste ich anfangen, Features zu reduzieren, lose Enden zusammenzuführen und mich endlich auf das Release vorzubereiten. Nicht alles muss vom ersten Tag an fertig sein, erinnerte ich mich. Manche Features können warten, aber mit einem soliden Fundament zu launchen war essentiell. Und genau hier kam mein Entscheidungsprozess bezüglich der Handhabung von In-App-Abos und -Käufen ins Spiel.</p><h2 id="revenuecat-ein-schwieriger-start">RevenueCat: Ein schwieriger Start</h2><p>Als ich an dem Punkt war, In-App-Käufe zu integrieren, hörte ich massenhaft Positives über RevenueCat. Es schien die Lösung schlechthin für App-Entwickler zu sein, besonders mit seinen leistungsstarken Features und seinem Ruf in der Community. Also zog ich natürlich in Betracht, RevenueCat oder die StoreKit-2-API direkt zu verwenden. Ich wandte mich an andere, die RevenueCat benutzt hatten, um Tipps für den Einstieg zu bekommen.</p><p>Was ich allerdings schnell feststellte, war, dass der anfängliche Einrichtungsprozess für RevenueCat deutlich herausfordernder war als erwartet. Die Abläufe waren komplex, und um die Sache noch schlimmer zu machen, war die Dokumentation genau dort lückenhaft, wo ich am dringendsten Hilfe brauchte. Die Grundeinrichtung kostete Zeit und fühlte sich wie ein Kampf an, besonders weil ich schon unter Druck stand, meine App startfertig zu machen. Ich habe es zwar irgendwie durch die Einrichtung geschafft, aber die angenehmste Erfahrung war es nicht.</p><h2 id="freemiumkit-entdecken-eine-erleichterung">FreemiumKit entdecken: Eine Erleichterung</h2><p>Dann stieß ich auf FreemiumKit. Nachdem ich einige positive Bewertungen gelesen hatte, beschloss ich, es auszuprobieren. Von Anfang an fiel mir ein deutlicher Unterschied auf. Die Einrichtung mit FreemiumKit war unglaublich einfach. Ich konnte es viel schneller in meine App integrieren als jede andere Lösung, die ich ausprobiert hatte. Die SDK-Integrationsdokumentation war glasklar – einmal lesen und ich war startklar. Es fühlte sich an, als würde einfach alles zusammenpassen.</p><p><img src="/assets/images/blog/why-i-chose-freemiumkit-over-revenuecat-for-my-diabetes-management-app-guest-post/discovering-freemiumkit.webp" alt="FreemiumKit entdecken" loading="lazy" /></p><p>FreemiumKit machte nicht nur den Einrichtungsprozess einfach – es kam auch vollgepackt mit smarter Automatisierung und Standardwerten, die sofort mit App Store Connect funktionierten. Und trotzdem bot es die Flexibilität, alles an meine speziellen Bedürfnisse anzupassen. Das Pricing stimmte auch, besonders für einen App-Entwickler wie mich, der gerade erst anfängt. Der Support von den Entwicklern? Herausragend. Ich hatte einen Call mit dem Entwickler, der sich als eine großartige Erfahrung herausstellte. Er führte mich durch den Prozess, und innerhalb von 45 Minuten hatte ich Klarheit über Aspekte von In-App-Käufen, die selbst Apples Dokumentation nicht im Detail abdeckte.</p><h2 id="der-einfluss-auf-meinen-entwicklungsprozess">Der Einfluss auf meinen Entwicklungsprozess</h2><p>Der Wechsel zu FreemiumKit hatte einen unglaublichen Einfluss auf meinen Entwicklungsprozess. Ich konnte eine erhebliche Menge an Code aufräumen und Extra-Klassen sowie unnötige Komplexität entfernen, die RevenueCat erfordert hatte. Dieses Aufräumen war nicht nur kosmetisch – es machte meine App effizienter und einfacher zu verwalten. Das ganze RevenueCat-Zeug war weg, ersetzt durch FreemiumKit, und ich war begeistert.</p><p>Eingebaute SDK-Komponenten wie <code>PaidFeatureView</code> und <code>PaidStatusView</code> waren unglaublich anpassbar und ermöglichten es mir, mich auf die User Experience zu konzentrieren, ohne mir über die technischen Details den Kopf zu zerbrechen. Statt ein ganzes ViewModel für die Handhabung von In-App-Käufen schreiben zu müssen, konnte ich einen Einzeiler von FreemiumKit verwenden. Das gab mir die Freiheit, mich auf das zu konzentrieren, was wirklich zählt: eine großartige App zu bauen.</p><p>Darüber hinaus machte FreemiumKit die einschüchternden Aspekte von StoreKit2 handhabbar. Durch das Experimentieren mit FreemiumKit gewann ich ein besseres Verständnis von StoreKit2, was ein enormer Mehrwert war. Es verwandelte etwas, das wie eine überwältigende Aufgabe schien, in etwas Angenehmes und sehr Lehrreiches.</p><h2 id="ein-klarer-weg-zum-launch">Ein klarer Weg zum Launch</h2><p>Rückblickend war der Hauptgrund, warum ich meine App nicht früher gelauncht hatte, der überwältigende Aufwand zur Integration von RevenueCat. Ich hatte angefangen, die Paywall zu bauen und alles zu programmieren, aber die Komplexität hatte mich aufgerieben. Am Ende prokrastinierte ich, weil es sich nach zu viel Arbeit anfühlte, diesen Teil der App noch mal anzufassen.</p><p>FreemiumKit änderte das alles. Es ermöglichte mir, mich sowohl auf die Entwicklung neuer Features zu konzentrieren als auch die Abo-Verwaltung ohne Angst zu integrieren. Ich schaffte es sogar, Änderungen an meinen Testzeiträumen in App Store Connect vorzunehmen, und FreemiumKit übernahm alles nahtlos. Die Angst und die „Entwicklerblockade” waren verschwunden, und ich freute mich wieder darauf, meinen Launch-Tag zu erreichen.</p><p><img src="/assets/images/blog/why-i-chose-freemiumkit-over-revenuecat-for-my-diabetes-management-app-guest-post/screenshot.webp" alt="Screenshot" loading="lazy" /></p><h2 id="abschließende-gedanken">Abschließende Gedanken</h2><p>FreemiumKit war ein Gamechanger für mich. Es hat nicht nur meinen Entwicklungsprozess vereinfacht, sondern auch meine Leidenschaft für den Launch von <em>Glu Sight</em> neu entfacht. Die Einrichtung war reibungslos, die Dokumentation erstklassig und der Support von den Entwicklern unglaublich. Für alle, die vor ähnlichen Herausforderungen bei der In-App-Kauf-Verwaltung stehen – ich kann FreemiumKit nur wärmstens empfehlen. Es hat mir geholfen, die Hindernisse zu überwinden, die RevenueCat mir in den Weg gelegt hatte, und mich darauf zu konzentrieren, ein Produkt abzuliefern, auf das ich stolz bin.</p><p>Während ich mich dem Launch im September 2024 nähere, bin ich aufgeregt und bereit – dank FreemiumKit. Ich habe auf dem Weg neue Freunde gewonnen, eine Menge gelernt und – am wichtigsten – ich bin endlich auf dem Weg, anderen zu helfen, ihr Diabetes einfacher zu managen.</p><p>Bleibt dran für den Launch!</p><blockquote><p>👨‍💻 <strong>Verpasse nicht den Launch-Beitrag!</strong>
Folge mir auf <a href="https://www.threads.net/@slowbrewed.studio">Threads</a> und <a href="https://iosdev.space/@Jeehut">Mastodon</a>.</p></blockquote><blockquote><p>💁 <strong>Hat dir dieser Artikel gefallen? Schau dir FreemiumKit an!</strong>
Einfache In-App-Käufe mit Paywalls, A/B-Tests und Live-Push!
<a href="https://freemiumkit.app/"><strong>Jetzt holen</strong></a> oder das <a href="https://www.youtube.com/watch?v=6JxwA3WieHs">Video-Setup-Tutorial</a> ansehen, um es in Aktion zu sehen.</p></blockquote>]]></content:encoded>
</item>
<item>
<title>Vorstellung von Pleydia Organizer: Die ultimative native Mac-App zum Umbenennen von TV- &amp; Filmdateien</title>
<link>https://fline.dev/de/blog/introducing-pleydia-organizer/</link>
<guid isPermaLink="true">https://fline.dev/de/blog/introducing-pleydia-organizer/</guid>
<pubDate>Mon, 05 Aug 2024 00:00:00 +0000</pubDate>
<description><![CDATA[Organisiere deine Medienbibliothek mühelos mit Pleydia Organizer – einer automatisierten App, die das Umbenennen von TV- und Filmdateien vereinfacht. Entdecke unübertroffene Geschwindigkeit, Genauigkeit und Komfort bei der Verwaltung deiner Mediensammlung.]]></description>
<content:encoded><![CDATA[<p>Ich freue mich, die Veröffentlichung von Pleydia Organizer bekannt zu geben – einer hochmodernen nativen Mac-App, die ich speziell für alle entwickelt habe, die ihre Mediendateien auf dem Mac gerne organisiert halten. Egal ob du einen Mac mini als Medienserver oder ein anderes Mac-Gerät nutzt – Pleydia Organizer setzt auf Apples neueste Technologien, um das beste Erlebnis beim Umbenennen und Organisieren deiner TV- und Filmdateien zu bieten.</p><p><img src="/assets/images/blog/introducing-pleydia-organizer/pleydia-organizer.webp" alt="Pleydia Organizer ordnet Filme nach Drag &amp; Drop automatisch zu" loading="lazy" />
<em>Pleydia Organizer ordnet Filme nach Drag &amp; Drop automatisch zu</em></p><h3 id="mühelose-organisation-mit-drag-drop">Mühelose Organisation mit Drag &amp; Drop</h3><p>Deine Medienbibliothek zu organisieren war noch nie so einfach. Mit Pleydia Organizer ziehst du deine TV- oder Filmdateien einfach per Drag &amp; Drop in die App, und sie durchsucht automatisch The Movie Database (TMDb), um deine Dateien korrekt zu identifizieren und zuzuordnen. Ein weiterer Klick, und alle deine Dateien werden in ordentliche, standardisierte Dateinamen umbenannt – ohne seltsame Zeichen oder Formatierungsprobleme.</p><p><img src="/assets/images/blog/introducing-pleydia-organizer/pleydia-organizer-after.webp" alt="Pleydia Organizer nach dem Umbenennen zugeordneter TV-Episoden-Dateien" loading="lazy" />
<em>Pleydia Organizer nach dem Umbenennen zugeordneter TV-Episoden-Dateien</em></p><h3 id="warum-pleydia-organizer-anderen-tools-vorziehen">Warum Pleydia Organizer anderen Tools vorziehen?</h3><p>Pleydia Organizer hebt sich von anderen Umbenennungs-Tools wie FileBot ab:</p><ul><li><p><strong>Blitzschneller App-Start:</strong> Pleydia Organizer startet fast sofort, während FileBot auf einem M1 Pro Mac etwa 10 Sekunden braucht.</p></li><li><p><strong>Genaue Film-Zuordnung:</strong> Pleydia Organizer glänzt bei der Film-Zuordnung, besonders wenn internationale Erscheinungsjahre von denen in deinem Land abweichen.</p></li><li><p><strong>Präzise TV-Episoden-Zuordnung:</strong> Die App behandelt Sonderfälle bei der Staffelgruppierung von Serien besser, etwa bei Animes wie One Piece, wo Episoden wöchentlich erscheinen – ein Bereich, in dem FileBot Schwierigkeiten hat.</p></li><li><p><strong>Gespeicherte TV-Serien-Auswahl:</strong> Pleydia Organizer merkt sich deine Staffelgruppierungswahl für Serien und erspart dir wiederholte Auswahl bei jeder Nutzung.</p></li><li><p><strong>Kostenloses Umbenennen:</strong> Du kannst einzelne Dateien kostenlos umbenennen, was die App für Gelegenheitsnutzer zugänglich macht. FileBot erfordert selbst für eine einzige Datei einen Kauf.</p></li><li><p><strong>Günstiger Multi-Datei-Support:</strong> Wenn du Multi-Datei-Support brauchst, kostet er die Hälfte von FileBot – eine wirtschaftliche Wahl für größere Bibliotheken.</p></li><li><p><strong>Drag &amp; Drop-Einfachheit:</strong> Anstatt dass du die App erst erlernen und viele Buttons klicken musst, ist Pleydia Organizer super einfach zu bedienen.</p></li></ul><p>Schau dir an, wie einfach es in Aktion ist, in diesem GIF:</p><p><img src="/assets/images/blog/introducing-pleydia-organizer/just-drag-drop-your.gif" alt="Ziehe deine Dateien einfach per Drag &amp; Drop in Pleydia Organizer, und die Zuordnung erfolgt automatisch." loading="lazy" />
<em>Ziehe deine Dateien einfach per Drag &amp; Drop in Pleydia Organizer, und die Zuordnung erfolgt automatisch.</em></p><p>Wenn du deinen eigenen Plex-, Kodi-, Jellyfin- oder Infuse-Medienserver hast, ist Pleydia Organizer definitiv einen Versuch wert. Lade es sicher aus dem Mac App Store herunter:</p><p><a href="https://apps.apple.com/app/apple-store/id6587583340?pt=549314&ct=fline.dev&mt=8">Pleydia Organizer: File Rename</a></p><p>Ich freue mich über jedes Feedback – <a href="mailto:pleydia@fline.dev">lass mich doch wissen</a>, wenn du auf Probleme stößt!</p>]]></content:encoded>
</item>
<item>
<title>Auf Preis-Gegenwind reagieren: Gelernte Lektionen</title>
<link>https://fline.dev/de/blog/reacting-to-pricing-backlash-lessons-learned/</link>
<guid isPermaLink="true">https://fline.dev/de/blog/reacting-to-pricing-backlash-lessons-learned/</guid>
<pubDate>Tue, 16 Apr 2024 00:00:00 +0000</pubDate>
<description><![CDATA[Meine Sicht darauf, wie man den richtigen Preis findet – und wie ich reagiert habe, als ein Nutzer meine App als „überteuert“ bezeichnet hat. Lerne aus meinen Fehlern und vermeide schlechte Bewertungen.]]></description>
<content:encoded><![CDATA[<p>Preisgestaltung ist schwer. Man kann es nie allen recht machen. Das weiß ich alles. Aber es deprimiert mich trotzdem länger, als ich zugeben möchte, wenn ich eine Bewertung wie diese lese:</p><p><img src="/assets/images/blog/reacting-to-pricing-backlash-lessons-learned/a-user-expressing-their.webp" alt="Ein Nutzer, der seine Gefühle in einer negativen App-Store-Bewertung ausdrückt." loading="lazy" />
<em>Ein Nutzer, der seine Gefühle in einer negativen App-Store-Bewertung ausdrückt.</em></p><p>Es ist wirklich schwer, der Versuchung zu widerstehen, einfach ihrer Forderung nachzugeben und den Preis zu senken. Und ich konnte nicht immer widerstehen, wie du hier sehen kannst:</p><p><img src="/assets/images/blog/reacting-to-pricing-backlash-lessons-learned/a-user-on-reddit.webp" alt="Ein Nutzer auf Reddit beschwert sich über den Preis einer Einmalkauf-App." loading="lazy" />
<em>Ein Nutzer auf Reddit beschwert sich über den Preis einer Einmalkauf-App.</em></p><p>Ich bin eben auch nur ein Mensch und ich möchte, dass den Leuten gefällt, was ich mache. Aber ich muss als Indie-Entwickler auch Geld verdienen, sonst kann ich die App nicht weiter verbessern. Es liegt also sogar im Interesse der Nutzer, dass ich einen fairen Preis ansetze – nicht nur in meinem. Aber was ist <em>fair</em>?</p><h2 id="faire-preisgestaltung">„Faire” Preisgestaltung</h2><p>Man kann das aus vielen verschiedenen Perspektiven betrachten.</p><p>Aus wirtschaftlicher Sicht sollte mein Ziel sein, meine Einnahmen zu maximieren. Wenn du nach einem Artikel über verschiedene Strategien dafür suchst, liest du besser einen Artikel wie <a href="https://www.appsflyer.com/blog/tips-strategy/app-pricing-strategies/">diesen hier</a>. Für mich ist Profit nur ein Mittel, um mich motiviert zu halten, weiter Apps zu bauen. Er ist nicht das Ziel. Die User Experience ist alles, was zählt. Und Probleme lösen. Ein fairer Preis gehört für mich zu einer guten UX dazu.</p><p>Eine reine Entwickler-Perspektive könnte sein, eine App nach dem Aufwand zu bepreisen, der nötig war, um sie zu bauen. Klingt fair, oder? Aber das funktioniert nicht in einem freien Markt. Und das aus gutem Grund! Was, wenn du etwas super Komplexes, aber total Irrelevantes gebaut hast? Warum sollte jemand dafür einen hohen Preis zahlen? Würdest du das?</p><p>Das bringt mich dazu, wie ich meine Anfangspreise meistens wähle: Was würde ich zahlen, wenn diese App von jemand anderem gebaut wäre und ich sie einfach nutzen wollte? Da ich ausschließlich Apps baue, die ich selbst brauche, bin ich auch Kunde! Es ist also quasi eine Ein-Personen-Kundenrecherche, auf der meine anfängliche Preisgestaltung basiert. Aber ist das eine gute Idee?</p><p>Zumindest nicht, wenn ich schlechte Bewertungen wie die 2-Sterne-Bewertung oben vermeiden will. Warum? Weil ich nicht nur Kunde bin, sondern auch genau weiß, was meine App bietet. Die Nutzer aber nicht. Sie wissen nur das, was ich ihnen auf der App-Store-Seite und in meinem Onboarding präsentiert habe. Und wahrscheinlich nicht mal alles davon.</p><h2 id="die-nutzer-perspektive">Die Nutzer-Perspektive</h2><p>Was sich für einen Nutzer fair anfühlt, hängt also stark davon ab, wie der Nutzer den Wert einer App wahrnimmt. Zum Beispiel die schlechte Bewertung oben, die ich für <a href="https://translatekit.app/">TranslateKit</a> bekommen habe. Mein Vorschauvideo auf der App-Store-Seite kommuniziert die einfache Bedienbarkeit des Drag-&amp;-Drop-Workflows zum Übersetzen von Apps. Aber der Nutzer hat sich beschwert, weil er sich bei einem Drittanbieter-Übersetzungsdienst registrieren musste, um die beworbenen Funktionen zu nutzen.</p><p><img src="/assets/images/blog/reacting-to-pricing-backlash-lessons-learned/all-3rd-party-services.webp" alt="Alle Drittanbieter-Dienste ausgeklappt mit Links zur Registrierung &amp; API-Key-Dokumentation." loading="lazy" /></p><p>Natürlich erwähne ich diese Anforderung in meiner App-Store-Beschreibung. Aber wer liest Beschreibungen? Niemand! Ich könnte tun, was der Nutzer gefordert hat, und eingebaute Unterstützung für diese Übersetzungsdienste anbieten. Aber dann müsste ich für diese Dienste bezahlen und den Preis darauf aufschlagen. Und ich müsste zusätzlich einen Proxy-Server betreiben, um sicherzustellen, dass meine API-Keys nicht offengelegt werden, was weitere Kosten verursacht.</p><p>Das ist für die meisten meiner anderen Kunden aber nicht gut. Schließlich hat jeder Übersetzungsdienst, den ich unterstütze, ein großzügiges kostenloses Kontingent, das mehr als ausreicht, um eine App beliebiger Größe in viele Sprachen zu lokalisieren! Das einzige Problem ist die zusätzliche Hürde, ein kostenloses Konto zu erstellen und die API-Keys zu holen. Ich versuche zu helfen, indem ich auf die richtigen Seiten verlinke:</p><p><img src="/assets/images/blog/reacting-to-pricing-backlash-lessons-learned/all-3rd-party-services-2.webp" alt="Alle Drittanbieter-Dienste ausgeklappt mit Links zur Registrierung &amp; API-Key-Dokumentation." loading="lazy" />
<em>Alle Drittanbieter-Dienste ausgeklappt mit Links zur Registrierung &amp; API-Key-Dokumentation.</em></p><p>Diese Hürde hat wahrscheinlich beim Nutzer Frust verursacht, der erwartet hatte, seinen String Catalog nach dem Herunterladen der App einfach „per Drag &amp; Drop” reinzuziehen, wie im Vorschauvideo gezeigt. Aber das ging nicht sofort. Und als Konsequenz drückte er seine Gefühle aus, indem er das Einzige kritisierte, worüber er tatsächlich Bescheid wusste: den Preis. Denn den sieht man, wenn man im Hauptfenster auf „Upgrade” klickt.</p><p><img src="/assets/images/blog/reacting-to-pricing-backlash-lessons-learned/the-top-section-of-the.webp" alt="Der obere Bereich des Hauptfensters in TranslateKit." loading="lazy" />
<em>Der obere Bereich des Hauptfensters in TranslateKit.</em></p><h2 id="die-beschwerde-adressieren">Die Beschwerde adressieren</h2><p>Das Einfachste, was ich tun könnte, um Beschwerden über den Preis zu vermeiden, wäre, die Information über die kostenpflichtige Stufe zu verstecken, bis der Nutzer das kostenlose Limit überschreitet. Aber das betrachte ich als Dark Pattern, also würde ich das nie tun. Und selbst wenn ich es täte, würden sie sich wahrscheinlich einfach über etwas anderes beschweren – es löst das Problem nicht.</p><p>Viel nützlicher, um schlechte Bewertungen zu verhindern, wäre es, wenn ich mehr Anleitung und Hilfe beim Erstellen dieser Konten bieten würde, vielleicht mit Schritt-für-Schritt-Anleitungen oder Videos. Aber das senkt nur den Aufwand – es könnte für manche immer noch zu viel sein!</p><p>Die obige Analyse bringt mich zu dem Schluss, dass es eigentlich nicht der Preis meiner App war, der zur schlechten Bewertung geführt hat, sondern meine Unfähigkeit, ihren Wert zu kommunizieren. Nutzer können nicht wissen, dass meine App die String Catalogs nicht nur mit Übersetzungsdiensten verbindet, sondern auch unter der Haube allerlei Cleveres tut, um sicherzustellen, dass die Übersetzungen akkurat sind. Ein einfacher Weg, das zu kommunizieren, ist ein Beispiel:</p><p><img src="/assets/images/blog/reacting-to-pricing-backlash-lessons-learned/new-window-shown-on.webp" alt="Neues Fenster beim ersten App-Start zur Kommunikation des App-Werts." loading="lazy" />
<em>Neues Fenster beim ersten App-Start zur Kommunikation des App-Werts.</em></p><p>Indem ich die verschiedenen Stufen zeige, die eine Übersetzung beim App-Start durchlaufen kann, erkläre ich meinen Nutzern, welchen zusätzlichen Mehrwert meine App bietet – über das hinaus, was schon auf der Store-Seite offensichtlich ist. Zusätzlich sollte ich diese Stufen &amp; Anpassungen wahrscheinlich auch in der Übersetzungs-UI meiner App sichtbar machen, was ich in einem zukünftigen Update tun könnte. Aber es hätte die schlechte Bewertung sowieso nicht verhindert, da man die Übersetzungs-UI erst sieht, nachdem man mindestens einen API-Key eingerichtet hat.</p><h2 id="fazit">Fazit</h2><p>Preisgestaltung ist tatsächlich schwer und ich bin mir immer noch nicht zu 100 % sicher, ob das, was ich tue, richtig ist. Und das werde ich wahrscheinlich nie sein. Aber zumindest fühle ich mich nicht schlecht wegen meines Preises, da meine App <a href="https://x.com/KSlazinski/status/1774075673742901683?s=20">viele Stunden Zeit spart</a> und deutlich weniger kostet als der durchschnittliche Stundensatz. Und ich weiß, dass Leute die App abonnieren – das sehe ich an den Zahlen in App Store Connect. Jedes gekaufte Abo ist jemand, der mir sagt: „Der Preis ist <em>nicht</em> zu hoch.”</p><p>Nur weil ein Kunde sich über den Preis beschwert hat, heißt das nicht, dass er falsch ist. Die deprimierendsten Bewertungen können manchmal die nützlichsten Einblicke in die Schwächen deines App-Onboardings liefern. Ignoriere sie nicht einfach. Versuche zuerst zu verstehen, woher deine Nutzer kommen könnten. Und kommuniziere den Wert deiner App!</p>]]></content:encoded>
</item>
<item>
<title>2 neue Vision Pro Apps: &quot;Guided Guest Mode&quot; &amp; &quot;FocusBeats: Pomodoro + Music&quot;</title>
<link>https://fline.dev/de/blog/2-new-vision-pro-apps-guided-guest-mode-focusbeats-pomodoro-music/</link>
<guid isPermaLink="true">https://fline.dev/de/blog/2-new-vision-pro-apps-guided-guest-mode-focusbeats-pomodoro-music/</guid>
<pubDate>Mon, 15 Apr 2024 00:00:00 +0000</pubDate>
<description><![CDATA["Guided Guest Mode" wertet Apple Vision Pro-Demos auf – mit leicht verständlichen Anleitungen für eine immersive Einführung in Spatial Computing. "FocusBeats: Pomodoro + Music" kombiniert die produktivitätssteigernde Pomodoro-Technik mit thematischer Musik, um den Fokus während Arbeitssitzungen zu verbessern.]]></description>
<content:encoded><![CDATA[<h2 id="guided-guest-mode">Guided Guest Mode</h2><p>Du besitzt eine Apple Vision Pro? Mach deine Demos noch besser! Freunde durch die Funktionen zu navigieren kann schwierig sein, und man vergisst schnell mal, einige der besten Aspekte zu zeigen. Aber damit ist jetzt Schluss!</p><p><img src="/assets/images/blog/2-new-vision-pro-apps-guided-guest-mode-focusbeats-pomodoro-music/a-short-demo-video-of-2.webp" alt="Ein kurzes Demo-Video von Guided Guest Mode." loading="lazy" /></p><p><strong>Guided Guest Mode</strong> ist dafür gemacht, deinen Freunden &amp; deiner Familie eine reibungslose Einführung in die Zukunft von Spatial Computing zu bieten. Nutze die eingebauten Anleitungen – inspiriert von Apples eigenen Demos – um die besten Erlebnisse zu präsentieren, oder erstelle deine eigenen Guides. Eine riesige Zeitersparnis!</p><iframe width="200" height="113" src="https://www.youtube.com/embed/mRf523FXIBU?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen="" title="&quot;Guided Guest Mode&quot; now on Apple Vision Pro!"></iframe>
<p><em>Ein kurzes Demo-Video von Guided Guest Mode.</em></p><p><strong>Jetzt herunterladen:</strong></p><p><a href="https://apps.apple.com/app/apple-store/id6479207869?pt=549314&ct=fline.dev&mt=8">Guided Guest Mode</a></p><p><a href="https://github.com/FlineDev/CrossCraft-LandingPage/raw/main/downloads/GuidedGuestMode-PressKit.zip">Press Kit herunterladen</a></p><h2 id="focusbeats-pomodoro-music">FocusBeats: Pomodoro + Music</h2><p><em>Auch verfügbar auf iPhone, iPad und Mac</em></p><p>Du arbeitest gerne mit Hintergrundmusik? Und du möchtest deine Produktivität mit der Pomodoro-Technik (25 Min./5 Min.) verbessern? Dann ist das die App, auf die du gewartet hast: Sie bietet ausgewählte Musik-Themen wie Filmmusik oder klassische Musik. Und spielt die Musik während der Fokus- &amp; Pausenzeiten automatisch ab &amp; pausiert sie. Entdecke Fokus-Musik für jede Stimmung!</p><p><img src="/assets/images/blog/2-new-vision-pro-apps-guided-guest-mode-focusbeats-pomodoro-music/a-short-demo-video-of.webp" alt="Ein kurzes Demo-Video von FocusBeats auf Vision Pro." loading="lazy" /></p><p>Für die automatische Musikwiedergabe benötigt diese App ein Apple Music-Abo. Ohne Abo ist sie trotzdem einer der besten Pomodoro-Timer da draußen. Das Standard-25/5-Pomodoro-System und ausgewählte Musik-Themen sind kostenlos nutzbar.</p><iframe width="200" height="113" src="https://www.youtube.com/embed/MVKfBCAOAqE?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen="" title="&quot;FocusBeats: Pomodoro + Music&quot; now on Apple Vision Pro! (+ iPhone, iPad, and Mac)"></iframe>
<p><em>Ein kurzes Demo-Video von FocusBeats auf Vision Pro.</em></p><p><strong>Jetzt kostenlos testen:</strong></p><p><a href="https://apps.apple.com/app/apple-store/id6477829138?pt=549314&ct=fline.dev&mt=8">FocusBeats: Pomodoro + Music</a></p><p><a href="https://github.com/FlineDev/CrossCraft-LandingPage/raw/main/downloads/FocusBeats-PressKit.zip">Press Kit herunterladen</a></p>]]></content:encoded>
</item>
<item>
<title>Vorstellung von FreelanceKit: Zeiterfassung für alle Plattformen!</title>
<link>https://fline.dev/de/blog/introducing-freelancekit/</link>
<guid isPermaLink="true">https://fline.dev/de/blog/introducing-freelancekit/</guid>
<pubDate>Tue, 09 Apr 2024 00:00:00 +0000</pubDate>
<description><![CDATA[Günstige und native Zeiterfassung, die sich über iPhone, iPad, Mac und Vision synchronisiert. Sieh zu, wie sich dein verdientes Geld live aktualisiert. Export als CSV. Und vieles mehr!]]></description>
<content:encoded><![CDATA[<h3 id="verfügbar-auf-iphone-ipad-mac-und-apple-vision">Verfügbar auf iPhone, iPad, Mac und Apple Vision!</h3><p>FreelanceKit bietet eine günstige, einfach zu bedienende Zeiterfassungslösung, die sich auf deinen Apple-Geräten wie zu Hause anfühlt. Mit SwiftUI entwickelt, ist die App intuitiv gestaltet und bietet ein reibungsloses, natives Erlebnis auf allen Apple-Plattformen.</p><p><img src="/assets/images/blog/introducing-freelancekit/freelancekit-on-mac.gif" alt="FreelanceKit auf dem Mac." loading="lazy" />
<em>FreelanceKit auf dem Mac.</em></p><h3 id="wichtigste-features">Wichtigste Features:</h3><ul><li><p><strong>Nahtlose Synchronisierung</strong>: Starte die Zeiterfassung auf dem Mac und stoppe sie auf dem iPhone. iCloud-Sync stellt sicher, dass deine Daten auf allen Geräten verfügbar sind.</p></li><li><p><strong>Mini-Timer-Modus</strong>: Ein ablenkungsfreier Timer, der das Pausieren und Fortsetzen der Arbeit mühelos macht, ohne zu viel Bildschirmplatz einzunehmen.</p></li><li><p><strong>Mehrsprachige Unterstützung</strong>: Verfügbar in über 30 Sprachen, damit sie für alle zugänglich ist.</p></li><li><p><strong>Live-Verdienst-Tracking</strong>: Gib deinen Stundensatz ein, um deinen Verdienst in Echtzeit zu sehen – das motiviert!</p></li><li><p><strong>CSV-Export</strong>: Exportiere deine erfasste Zeit, um sie in anderen Tools mit allen erwartbaren Daten zu öffnen. Du kannst sogar das Datumsformat und das Trennzeichen anpassen!</p></li></ul><p><img src="/assets/images/blog/introducing-freelancekit/freelancekit-on-apple.gif" alt="FreelanceKit auf Apple Vision." loading="lazy" />
<em>FreelanceKit auf Apple Vision.</em></p><h3 id="perfekt-für-alle">Perfekt für alle:</h3><p>Ob du freiberuflich arbeitest, studierst oder Projekte verwaltest – FreelanceKit ist für alle gemacht, die ihre Zeit in den Griff bekommen wollen. Die Einfachheit und geräteübergreifende Kompatibilität sorgen dafür, dass du dich auf das Wesentliche konzentrieren kannst, ohne den Überblick über deine Zeit zu verlieren. Lade FreelanceKit jetzt herunter und entfalte dein Produktivitätspotenzial. Manage deine Projekte und deine Zeit effizienter!</p><h3 id="jetzt-holen">Jetzt holen:</h3><p><a href="https://apps.apple.com/app/apple-store/id6480134993?pt=549314&ct=fline.dev&mt=8">FreelanceKit: Time Tracking</a></p><p>Lade FreelanceKit jetzt herunter und entfalte dein Produktivitätspotenzial. Manage deine Projekte und deine Zeit effizienter!</p>]]></content:encoded>
</item>
<item>
<title>Meine Top 10 Wünsche für die WWDC24</title>
<link>https://fline.dev/de/blog/my-top-10-wishes-for-wwdc24/</link>
<guid isPermaLink="true">https://fline.dev/de/blog/my-top-10-wishes-for-wwdc24/</guid>
<pubDate>Fri, 05 Apr 2024 00:00:00 +0000</pubDate>
<description><![CDATA[Von einer SportsKit-API und einem .zoom-Modifier in SwiftUI, über verbessertes SwiftData und Source Control in Xcode, bis hin zu meinen größten Schmerzpunkten bei tvOS und visionOS – und vielem mehr! Eine Mischung aus langjährigen Wünschen und frischen Ideen.]]></description>
<content:encoded><![CDATA[<p>Es ist mittlerweile schon ein bisschen zur Gewohnheit geworden, dass ich kurz nachdem Apple die Termine für die nächste WWDC bekannt gibt, eine Liste der Dinge zusammenstelle, die mir in Apples Tools und Frameworks am meisten fehlen. Ich hoffe, dass diese Übersichtsartikel für Apple-Ingenieure hilfreich sind, um intern Prioritäten zu bestätigen oder anzupassen. Und bisher wurde tatsächlich jedes Jahr 33 % von dem, was ich mir gewünscht habe, Realität! Mag Zufall sein, aber ich finde es trotzdem gut.</p><p>Einige dieser Ideen habe ich schon vor langer Zeit als Radar eingereicht. Aber oft bringt es neue Dinge zum Vorschein, die mir im Arbeitsalltag nicht begegnen, wenn ich mir die Zeit nehme, über die Dinge nachzudenken, die mir am meisten fehlen. Du wirst in dieser Liste also ein paar offensichtliche Sachen finden, aber ich bin mir sicher, dass auch Dinge dabei sind, an die du nie gedacht hast. Ich würde gerne hören, was du denkst – kommentiere doch auf Social Media und erwähne meinen Handle @Jeehut.</p><blockquote><p>ℹ️ Es gibt noch 3 Wünsche aus meinem <a href="https://www.fline.dev/my-top-5-wishes-for-wwdc-2023/">letztjährigen Artikel</a>, die noch nicht umgesetzt wurden, und ich bin weiterhin der Meinung, dass wir sie brauchen:</p><ol><li><p><strong>Einfache App-Modularisierung</strong> in Xcode ohne den ganzen Aufwand</p></li><li><p>Tortendiagramme gibt es jetzt, aber <strong>Spinnendiagramme</strong> fehlen noch!</p></li><li><p><strong>Streamer-Modus</strong> in Xcode, um sensiblen Code in Calls/Aufnahmen zu verbergen</p></li></ol></blockquote><h2 id="1-eine-weatherkit-ähnliche-api-für-apple-sports">#1 – Eine WeatherKit-ähnliche API für Apple Sports</h2><p>Ich war überrascht, als Apple <a href="https://developer.apple.com/weatherkit/">WeatherKit</a> 2022 vorstellte. Es war das erste System-Framework (das ich kenne), das für Entwickler nicht kostenlos nutzbar war – aber doch 500.000 Aufrufe pro Monat gratis mitbrachte. Das war großzügig genug, damit Indie-Entwickler interessante neue Apps darauf aufbauen konnten. Apple hatte im Grunde den Datenteil einer System-App extrahiert und Entwicklern zugänglich gemacht. Und nicht nur das – sie machten es auch für Bootstrapped-Indies erschwinglich!</p><p><img src="/assets/images/blog/my-top-10-wishes-for-wwdc24/the-new-apple-sports-app.webp" alt="Die neue Apple Sports App." loading="lazy" /></p><p><em>Die neue Apple Sports App.</em></p><p>Die Einführung der <a href="https://apps.apple.com/us/app/apple-sports/id6446788829">Apple Sports</a> App in den USA brachte mich auf eine Idee: Was wäre, wenn Apple dasselbe mit den Daten dieser App machen würde? Ich hatte über die Jahre mehrere App-Ideen, die ich als Prototyp gebaut, aber nicht veröffentlichen konnte, weil es keine bezahlbare Live-Sportdaten-API gab. Deren Preise richten sich an große Konzerne (wie es bei vielen APIs der Fall ist). Aber angesichts der Tatsache, dass die beste Art, ein Sportevent heutzutage zu erleben, die Vision Pro ist, wäre es meiner Meinung nach sehr sinnvoll, wenn Apple es Indies so einfach wie möglich macht, neue Erlebnisse rund um Sportevents zu entwickeln.</p><blockquote><p>Ich wünsche mir eine neue „SportsKit”-API mit einem großzügigen Gratis-Kontingent wie bei WeatherKit!</p></blockquote><h2 id="2-einen-zoom-modifier-für-scrollview-in-swiftui">#2 – Einen .zoom-Modifier für ScrollView in SwiftUI</h2><p>Eines der ersten Feature-Wünsche für den Spielbildschirm meiner Kreuzworträtsel-App <a href="https://apps.apple.com/app/apple-store/id6472669260?pt=549314&ct=fline.dev&mt=8">CrossCraft</a> war, hinein- und herauszuzoomen, um einen besseren Überblick über die zusammenhängenden Buchstaben zu bekommen. Aber als ich <code>.zoom</code> tippte, um den entsprechenden Modifier zu meiner <code>ScrollView</code> in SwiftUI hinzuzufügen, war ich schockiert festzustellen, dass es ihn gar nicht gibt! 😱</p><p>Es gibt zwar Workarounds mit einer benutzerdefinierten <code>MagnifyGesture</code>, einem <code>GeometryReader</code> basierend auf dem <code>.scaleEffect</code>-Modifier mit eigenen <code>@State</code>-Properties, aber das funktioniert nur für eingeschränkte Anwendungsfälle wie das Zoomen in Bilder. Meine View ist aber eine <code>ScrollView</code>, die sich ganz anders verhält als eine normale View. Ich habe mehrere Ansätze ausprobiert, konnte es aber nicht so zum Laufen bringen, wie man es erwarten würde. Und selbst wenn ich es geschafft hätte – den Inhalt einer ScrollView zu zoomen ist ein so häufiger Anwendungsfall, dass es wirklich in <code>ScrollView</code> eingebaut und einfach nutzbar sein sollte, ohne den ganzen Aufwand.</p><blockquote><p>Ich wünsche mir einen <code>.zoom(scale:offset:)</code>-Modifier für <code>ScrollView</code> in SwiftUI!</p></blockquote><h2 id="3-swiftdata-limitierungen-mit-cloudkit-beheben">#3 – SwiftData-Limitierungen mit CloudKit beheben</h2><p>Paul Hudson hat es perfekt in seinem großartigen <a href="https://www.hackingwithswift.com/quick-start/swiftdata/how-to-sync-swiftdata-with-icloud">SwiftData + CloudKit Artikel</a> formuliert:</p><blockquote><p>SwiftData with iCloud has a requirement that local SwiftData does not: all properties must be optional or have default values, and all relationships must be optional. The first of those is a small annoyance, but the second is a much bigger annoyance – it can be quite disruptive for your code.</p></blockquote><p>Dem ist wirklich nicht viel hinzuzufügen, außer dass ich eine Menge Computed Properties in allen meinen Modellen schreiben musste, um Optionals auf Non-Optionals zu mappen. Es ist sicher, meine Apps stürzen nicht ab. Warum ist das also nicht in irgendeinem SwiftData-Macro eingebaut?</p><blockquote><p>Ich wünsche mir, dass Non-Optional-Typen in SwiftData-Modellen mit CloudKit funktionieren.</p></blockquote><h2 id="4-persistente-windows-und-volumes-in-visionos">#4 – Persistente Windows und Volumes in visionOS</h2><p>Ich habe so viele App-Ideen für visionOS, die nur dann Sinn ergeben, wenn die Position von Windows oder Volumes über App-Neustarts und sogar Systemneustarts oder Updates hinweg gespeichert werden könnte. Ich habe zum Beispiel eine App namens <a href="https://apps.apple.com/app/apple-store/id6478062053?pt=549314&ct=fline.dev&mt=8">Posters</a> veröffentlicht, mit der man seine Wände mit automatisch aktualisierenden, interaktiven Filmpostern dekorieren kann – aber wenn du das Gerät neustartest, sind sie weg. Die Erweiterung der Realität ist nur vorübergehend.</p><p><img src="/assets/images/blog/my-top-10-wishes-for-wwdc24/decorating-walls-with.webp" alt="Wände dekorieren mit Posters in visionOS." loading="lazy" /></p><p><em>Wände dekorieren mit <a href="https://apps.apple.com/app/apple-store/id6478062053?pt=549314&ct=fline.dev&mt=8">Posters</a> in visionOS.</em></p><p>Ich verstehe, dass Apple uns im Shared Space keinen vollen ARKit-Zugriff geben möchte. Es ist nicht möglich, gute Performance zu haben, wenn alle Apps gleichzeitig ARKit nutzen. Das wünsche ich mir auch nicht. Aber das System trackt die Fensterpositionen bereits.</p><blockquote><p>Ich wünsche mir eine (vom Nutzer genehmigte) Option, um Window-/Volume-Positionen zu speichern.</p></blockquote><h2 id="5-modale-texteingabe-auf-tvos-wie-bei-playstation">#5 – Modale Texteingabe auf tvOS (wie bei PlayStation)</h2><p>Auf iOS, iPadOS und visionOS bekommen wir QWERTY-Tastaturen, weil sie bekannt, schnell und einfach zu tippen sind. Ausgerechnet auf tvOS hat man sich entschieden, die Tasten in eine Zeile zu setzen. Die Folge: Um zu einer bestimmten Taste zu gelangen, braucht man viel länger als bei einem QWERTY-ähnlichen Rastersystem. Außerdem nimmt die Texteingabe die volle Breite des TV-Bildschirms ein und wird oben platziert, sodass sie zum Hauptfokus wird. Warum das Ganze? Schau dir einfach mal diese viel bessere modale Tastatur-UI von Sony aus dem Jahr 2013 an:</p><p><img src="/assets/images/blog/my-top-10-wishes-for-wwdc24/virtual-keyboard-on-ps4.webp" alt="Virtuelle Tastatur auf der PS4." loading="lazy" /></p><p><em>Virtuelle Tastatur auf der PS4.</em></p><p>Das Fehlen einer solchen Lösung macht Erlebnisse unmöglich, bei denen die Tastatur nur ein Zubehör zu einer ansonsten interessanteren View ist. Weil sie fehlt, habe ich meine ursprünglichen Pläne verworfen, <a href="https://apps.apple.com/app/apple-store/id6472669260?pt=549314&ct=fline.dev&mt=8">CrossCraft</a> auch auf tvOS zu bringen. Ich hätte es geliebt, Kreuzworträtsel auf dem Fernseher zu generieren und zu lösen. Das könnte ein tolles Familienerlebnis sein. Aber mit der aktuellen Tastatur ist es ein UX-Albtraum!</p><blockquote><p>Ich wünsche mir eine schmalere, QWERTY-ähnliche Zusatztastatur auf tvOS.</p></blockquote><h2 id="6-den-string-catalog-editor-nützlicher-machen">#6 – Den String Catalog Editor nützlicher machen</h2><p>Ja, String Catalogs sind neu. Deshalb ist es zu erwarten, dass sie noch nicht vollständig ausgereift sind. Aber selbst einige der grundlegendsten Dinge funktionieren aktuell nicht richtig. Einen Eintrag per Rechtsklick als „Reviewed” zu markieren, ist möglich. Aber mehrere gleichzeitig auswählen und markieren? Geht nicht. Eine neue Sprache mit dem Plus-Button unten hinzufügen? Geht. Aber eine mit dem Minus-Button entfernen? Geht nicht.</p><p>Abgesehen von den kaputten Grundlagen habe ich noch einen Feature-Wunsch, der absolut sinnvoll wäre: Die Oberfläche markiert Parameter bereits blau, es gibt also eine Erkennung. Aber wenn einer in einer anderen Sprache fehlt, gibt es keine Warnung. Das sollte es aber!</p><blockquote><p>Ich wünsche mir, dass der String Catalog Editor in Xcode nützlicher wird.</p></blockquote><blockquote><p>💁‍♂️ UPDATE: Ich habe diese Verbesserungen selbst umgesetzt! 😍 Ich habe im Grunde Xcodes String Catalog Editor nachgebaut und als komplett kostenloses Feature in meine App <a href="https://translatekit.app/">TranslateKit</a> integriert. Schau mal rein! 🌐 👍</p></blockquote><h2 id="7-neue-create-llm-app-wie-create-ml">#7 – Neue „Create LLM”-App wie „Create ML”</h2><p>Hast du mal <a href="https://developer.apple.com/machine-learning/create-ml/">Create ML</a> ausprobiert? Es ist ein Entwicklertool, das Apple bereits 2019 eingeführt hat und das meiner Meinung nach von vielen Entwicklern übersehen wird. Es ist ein KI-Tool, das das Problem der Klassifikation ziemlich gut löst. Schau dir einfach mal die verfügbaren Optionen an:</p><p><img src="/assets/images/blog/my-top-10-wishes-for-wwdc24/create-ml-project.webp" alt="Create ML Projektvorlagen" loading="lazy" /></p><p><em>Create ML Projektvorlagen</em></p><p>Aber wir alle wissen, dass generative KI basierend auf LLMs der neue Trend ist. Und es gibt sogar Belege dafür, dass Apple <a href="https://www.macrumors.com/2023/12/21/apple-ai-researchers-run-llms-iphones/">aktiv daran arbeitet</a>, in naher Zukunft etwas zu veröffentlichen. Angenommen, sie schaffen es, dieses Jahr etwas für Konsumenten auf den Markt zu bringen – was ich mir als Entwickler wünsche, ist ein Tool ähnlich wie Create ML, aber für LLMs. Stell dir vor, du könntest Apples Core-LLM-Modell mit deinen spezifischen Fachgebietsdaten und vielleicht sogar mit persönlichen Nutzerdaten anpassen, alles lokal auf dem Gerät gespeichert. Das würde erstaunliche neue App-Erlebnisse ermöglichen, und als Entwickler würden wir es kostenlos bekommen!</p><blockquote><p>Ich wünsche mir eine einfache Möglichkeit, fachspezifische und personalisierte LLMs zu erstellen.</p></blockquote><h2 id="8-verbesserte-source-control-ux-in-xcode">#8 – Verbesserte Source-Control-UX in Xcode</h2><p>Jahr für Jahr wird die Integration von Git in Xcode besser und besser. Letztes Jahr hat es einen Punkt erreicht, an dem ich beschlossen habe, Xcode als meine Standard-Git-UI zu verwenden. Vorher nutzte ich <a href="https://git-fork.com/">Git-Fork</a>, aber die volle Integration in Xcode ist unschlagbar!</p><p><img src="/assets/images/blog/my-top-10-wishes-for-wwdc24/xcode-source-editor.webp" alt="Xcode Source Editor Screenshot" loading="lazy" /></p><p>Allerdings fehlen mir ein paar Dinge. Erstens: Obwohl Xcode eine Code-Diff-Ansicht hat, gibt es keine Möglichkeit, die Diffs vergangener Commits zu sehen – nur „Uncommitted Changes”. Zweitens: Die Diff-Ansicht selbst hat wirklich seltsame Farbwahlen, die es schwer machen zu verstehen, welcher Code entfernt und welcher hinzugefügt wurde. Es ist üblich, dass Tools Rot und Grün für klare Bedeutungen verwenden, was ich der aktuellen Lösung deutlich vorziehen würde. Drittens: Um in einem Schritt zu committen und zu pushen, muss man auf das Pfeil-nach-unten-Icon rechts neben dem „Commit”-Button klicken und „Commit and Push…” wählen. Ich würde eine Checkbox bevorzugen, die ihren Zustand behält, damit ich nicht jedes Mal mehrere Klicks machen muss. Und schließlich: Manchmal tippe ich eine Commit-Nachricht und finde beim Überprüfen der Änderungen etwas, das nicht stimmt. Dann wechsle ich zur Datei, nehme Anpassungen vor, und wenn ich zur Diff-Ansicht zurückkehre, ist meine Commit-Nachricht weg! Sie sollte noch da sein.</p><blockquote><p>Ich wünsche mir eine verbesserte Source-Control-UX und vernünftige Git-Funktionalität in Xcode.</p></blockquote><h2 id="9-swiftui-previews-zuverlässig-zum-laufen-bringen">#9 – SwiftUI Previews zuverlässig zum Laufen bringen</h2><p>Das ist ein Ärgernis seit den allerersten Tagen von SwiftUI. Das Designen in SwiftUI könnte so nützlich und schnell sein. Wenn doch nur die Previews funktionieren würden. Aber bei mir funktionieren die Previews 90 % der Zeit nicht. Und ich habe die gängigen Lösungen schon ausprobiert. Manchmal haben sie geholfen, manchmal nicht. Aber es ist nervig, Workarounds machen zu müssen, nur damit SwiftUI Previews funktionieren.</p><blockquote><p>Ich wünsche mir, dass SwiftUI Previews jedes Mal funktionieren, wenn eine App für den Simulator baut.</p></blockquote><h2 id="10-screenshots-über-swiftui-previews">#10 – Screenshots über SwiftUI Previews</h2><p>App Store Screenshots zu erstellen ist schwierig. Man kann sie manuell auf 2–3 Geräten in einer Sprache machen. Aber sobald man sie lokalisieren möchte, wird der Aufwand unerträglich. Wenn ich raten müsste, würde ich sagen, dass 99 % der Screenshots im App Store veraltet sind. Aber das muss nicht so sein.</p><p>Wir brauchen etwas Hilfe von Apple, um das zu lösen. Und ich rede nicht von UI Tests. Die sind langsam. Die sind Extra-Aufwand. Die sind unzuverlässig. Ich schreibe heutzutage keine UI Tests mehr, weil es SwiftUI Previews gibt. Ja, ich habe oben geschrieben, dass sie nicht zuverlässig sind – das stimmt. Aber was wäre, wenn Apple das verbessern würde? Das <code>#Preview</code>-Macro macht es wirklich einfach, mehrere Previews in einer einzigen View zu erstellen. Und es ist relativ einfach, über die Initialisierung einen State an deine Views zu übergeben.</p><blockquote><p>Ich wünsche mir eine API zum Erstellen lokalisierter Screenshots mit SwiftUI Previews.</p></blockquote><h2 id="fazit">Fazit</h2><p>Das sind <em>meine</em> Top 10 Wünsche für die WWDC24. Stimmst du zu? Was habe ich vergessen?</p>]]></content:encoded>
</item>
<item>
<title>HandySwift 4.0 – Das große Update</title>
<link>https://fline.dev/de/blog/introducing-handyswift-4/</link>
<guid isPermaLink="true">https://fline.dev/de/blog/introducing-handyswift-4/</guid>
<pubDate>Sun, 24 Mar 2024 00:00:00 +0000</pubDate>
<description><![CDATA[Wieder Zeit in Open Source investiert: Komplette Überarbeitung von HandySwift mit deutlich verbesserter Dokumentation und vielen praktischen Features aus meinen Apps. Lies weiter, um zu erfahren, welche Helfer ich am häufigsten nutze!]]></description>
<content:encoded><![CDATA[<p>Es ist eine Weile her, seit ich zuletzt an Open-Source-Projekten gearbeitet habe. Ich habe mich darauf konzentriert, neue Apps zu veröffentlichen, um sicherzustellen, dass ich meine Indie-Karriere langfristig fortführen kann. Nachdem ich <a href="https://x.com/Jeehut/status/1767534092516491338?s=20">6 Apps</a> innerhalb von 3 Monaten gelauncht hatte, dachte ich, es wäre an der Zeit, die am häufigsten wiederverwendeten Code-Teile zu teilen. Dafür habe ich bereits eine Open-Source-Bibliothek: <a href="https://github.com/FlineDev/HandySwift">HandySwift</a>.</p><p>Aber es waren mehr als 2 Jahre seit dem letzten Release vergangen. Natürlich hatte ich über die Zeit hinweg einige Funktionalitäten zum <code>main</code>-Branch hinzugefügt, damit ich sie leicht in meinen Apps wiederverwenden konnte. Doch undokumentierte Features, die nicht Teil eines neuen Releases sind, kann man als “intern” betrachten, selbst wenn die APIs selbst als <code>public</code> markiert sind.</p><p>Also habe ich mir in den letzten Tagen die Zeit genommen, den gesamten Code aufzuräumen, alles an die neuesten Swift-Erweiterungen anzupassen, ungenutzten Code zu entfernen, <code>@available</code>-Attribute für Umbenennungen hinzuzufügen (damit Xcode Fix-its anbieten kann) und eine ganze Reihe neuer APIs zu dokumentieren. Ich habe sogar ein komplett neues Logo entworfen!</p><p>Zusätzlich habe ich mich entschieden, <a href="https://github.com/apple/swift-docc">Swift-DocC</a> zu nutzen, wodurch ich meine README-Datei auf das Minimum reduzieren und stattdessen meine Dokumentation auf der <a href="https://swiftpackageindex.com/">Swift Package Index</a>-Seite hosten konnte. Mit etwas Hilfe von ChatGPT konnte ich sogar die bestehende Dokumentation ausbauen, was zur am besten dokumentierten Bibliothek führte, die ich je veröffentlicht habe!</p><p><img src="/assets/images/blog/introducing-handyswift-4/logo-update.webp" alt="Logo-Update" loading="lazy" /></p><p>Ich werde einen eigenen Beitrag über die Details der Migration schreiben. Aber weil ich noch nie über HandySwift geschrieben habe, möchte ich einige der Annehmlichkeiten erklären, die du bei der Nutzung bekommst. Ich empfehle, es jedem Projekt hinzuzufügen. Es hat keine Abhängigkeiten, ist selbst leichtgewichtig, unterstützt alle Plattformen (einschließlich Linux und visionOS) und die Plattformunterstützung reicht bis iOS 12 zurück. Ein absoluter No-Brainer.</p><hr /><h2 id="extensions">Extensions</h2><p>Einige Highlights der über 100 Funktionen und Properties, die bestehenden Typen hinzugefügt werden – jeweils mit einem praktischen Anwendungsfall direkt aus einer meiner Apps:</p><h4 id="sicherer-index-zugriff">Sicherer Index-Zugriff</h4><p><img src="/assets/images/blog/introducing-handyswift-4/music-player.webp" alt="Music Player" loading="lazy" /></p><p>In <a href="https://apps.apple.com/app/apple-store/id6477829138?pt=549314&ct=fline.dev&mt=8">FocusBeats</a> greife ich über einen Index auf ein Array von Musiktiteln zu. Mit <a href="https://swiftpackageindex.com/flinedev/handyswift/main/documentation/handyswift/swift/collection/subscript(safe:)"><code>subscript(safe:)</code></a> vermeide ich Out-of-Bounds-Crashes:</p><pre><code>var nextEntry: ApplicationMusicPlayer.Queue.Entry? {
   guard let nextEntry = playerQueue.entries[safe: currentEntryIndex + 1] else { return nil }
   return nextEntry
}</code></pre><p>Du kannst es bei jedem Typ verwenden, der <code>Collection</code> konform ist, einschließlich <code>Array</code>, <code>Dictionary</code> und <code>String</code>. Statt den Subscript <code>array[index]</code> aufzurufen, der ein Non-Optional zurückgibt, aber bei einem ungültigen Index abstürzt, nutze das sicherere <code>array[safe: index]</code>, das in solchen Fällen stattdessen <code>nil</code> zurückgibt.</p><h4 id="leere-strings-vs-blank-strings">Leere Strings vs. Blank Strings</h4><p><img src="/assets/images/blog/introducing-handyswift-4/api-keys.webp" alt="API Keys" loading="lazy" /></p><p>Ein häufiges Problem bei Textfeldern, die nicht leer sein dürfen: Nutzer tippen versehentlich ein Leerzeichen oder Zeilenumbruch ein und bemerken es nicht. Wenn der Validierungscode nur <code>.isEmpty</code> prüft, bleibt das Problem unbemerkt. Deshalb stelle ich in <a href="https://translatekit.app/">TranslateKit</a> bei der Eingabe eines API-Keys sicher, dass zuerst alle Zeilenumbrüche und Leerzeichen am Anfang und Ende des Strings entfernt werden, bevor die <code>.isEmpty</code>-Prüfung erfolgt. Und weil ich das sehr oft an vielen Stellen mache, habe ich einen Helfer geschrieben:</p><pre><code>Image(systemName: self.deepLAuthKey.isBlank ? &quot;xmark.circle&quot; : &quot;checkmark.circle&quot;)
   .foregroundStyle(self.deepLAuthKey.isBlank ? .red : .green)</code></pre><p>Verwende einfach <a href="https://swiftpackageindex.com/flinedev/handyswift/main/documentation/handyswift/swift/string/isblank"><code>isBlank</code></a> statt <code>isEmpty</code> für das gleiche Verhalten!</p><h4 id="lesbare-zeitintervalle">Lesbare Zeitintervalle</h4><p><img src="/assets/images/blog/introducing-handyswift-4/premium-plan-expires.webp" alt="Premium-Plan läuft ab" loading="lazy" /></p><p>Immer wenn ich eine API verwendet habe, die ein <code>TimeInterval</code> erwartet (was nur ein Typealias für <code>Double</code> ist), fehlte mir die Einheit, was zu weniger lesbarem Code führte, weil man sich aktiv daran erinnern muss, dass die Einheit “Sekunden” ist. Und wenn ich eine andere Einheit wie Minuten oder Stunden brauchte, musste ich die Berechnung manuell durchführen. Nicht so mit HandySwift!</p><p>Statt einen einfachen <code>Double</code>-Wert wie <code>60 * 5</code> zu übergeben, kannst du einfach <code>.minutes(5)</code> schreiben. Zum Beispiel nutze ich in <a href="https://translatekit.app/">TranslateKit</a> für die Vorschau der Ansicht bei einem abgelaufenen Abo Folgendes:</p><pre><code>#Preview(&quot;Expiring&quot;) {
   ContentView(
      hasPremiumAccess: true,
      premiumExpiresAt: Date.now.addingTimeInterval(.days(3))
   )
}</code></pre><p>Du kannst sogar mehrere Einheiten mit einem <code>+</code>-Zeichen verketten, um eine Uhrzeit wie “09:41 Uhr” zu erstellen:</p><pre><code>let startOfDay = Calendar.current.startOfDay(for: Date.now)
let iPhoneRevealedAt = startOfDay.addingTimeInterval(.hours(9) + .minutes(41))</code></pre><p>Beachte, dass dieses API-Design im Einklang mit <code>Duration</code> und <code>DispatchTimeInterval</code> steht, die beide bereits Dinge wie <code>.milliseconds(250)</code> unterstützen. Aber sie hören bei der Sekunden-Ebene auf, sie gehen nicht höher. HandySwift fügt für diese Typen auch Minuten, Stunden, Tage und sogar Wochen hinzu. So kannst du zum Beispiel Folgendes schreiben:</p><pre><code>try await Task.sleep(for: .minutes(5))</code></pre><blockquote><p>Achtung: Das Voranschreiten der Zeit durch Intervalle berücksichtigt keine Komplexitäten wie die Sommerzeit. Verwende dafür einen <code>Calendar</code>.</p></blockquote><h4 id="durchschnitte-berechnen">Durchschnitte berechnen</h4><p><img src="/assets/images/blog/introducing-handyswift-4/crossword-generation.webp" alt="Kreuzworträtsel-Generierung" loading="lazy" /></p><p>Im Kreuzworträtsel-Generierungsalgorithmus von <a href="https://apps.apple.com/app/apple-store/id6472669260?pt=549314&ct=swiftpackageindex.com&mt=8">CrossCraft</a> habe ich eine Health-Funktion für jede Iteration, die die Gesamtqualität des Rätsels berechnet. Zwei verschiedene Aspekte werden berücksichtigt:</p><pre><code>/// Ein Wert zwischen 0 und 1.
func calculateQuality() -&gt; Double {
   let fieldCoverage = Double(solutionBoard.fields) / Double(maxFillableFields)
   let intersectionsCoverage = Double(solutionBoard.intersections) / Double(maxIntersections)
   return [fieldCoverage, intersectionsCoverage].average()
}</code></pre><p>In früheren Versionen habe ich mit verschiedenen Gewichtungen experimentiert, zum Beispiel Kreuzungen doppelt so stark gewichtet wie die Feldabdeckung. Das ließe sich immer noch mit <a href="https://swiftpackageindex.com/flinedev/handyswift/main/documentation/handyswift/swift/collection/average()-3g44u"><code>average()</code></a> erreichen – einfach so in der letzten Zeile:</p><pre><code>return [fieldCoverage, intersectionsCoverage, intersectionsCoverage].average()</code></pre><h4 id="fließkommazahlen-runden">Fließkommazahlen runden</h4><p><img src="/assets/images/blog/introducing-handyswift-4/progress-bar.webp" alt="Fortschrittsbalken" loading="lazy" /></p><p>Beim Lösen eines Rätsels in <a href="https://apps.apple.com/app/apple-store/id6472669260?pt=549314&ct=swiftpackageindex.com&mt=8">CrossCraft</a> siehst du deinen aktuellen Fortschritt oben auf dem Bildschirm. Ich nutze den eingebauten Prozent-Formatter (<code>.formatted(.percent)</code>) für numerische Werte, aber er erwartet ein <code>Double</code> mit einem Wert zwischen 0 und 1 (1 = 100%). Ein <code>Int</code> wie <code>12</code> zu übergeben, rendert unerwartet als <code>0%</code>, also kann ich nicht einfach Folgendes machen:</p><pre><code>Int(fractionCompleted * 100).formatted(.percent)  // =&gt; &quot;0%&quot; bis &quot;100%&quot;</code></pre><p>Und einfach <code>fractionCompleted.formatted(.percent)</code> zu verwenden, ergibt manchmal sehr langen Text wie <code>&quot;0.1428571429&quot;</code>.</p><p>Stattdessen nutze ich <a href="https://swiftpackageindex.com/flinedev/handyswift/main/documentation/handyswift/swift/double/rounded(fractiondigits:rule:)"><code>rounded(fractionDigits:rule:)</code></a>, um den <code>Double</code>-Wert auf 2 signifikante Stellen zu runden:</p><pre><code>Text(fractionCompleted.rounded(fractionDigits: 2).formatted(.percent))</code></pre><blockquote><p>Es gibt auch eine mutierende <a href="https://swiftpackageindex.com/flinedev/handyswift/main/documentation/handyswift/swift/double/round(fractiondigits:rule:)"><code>round(fractionDigits:rule:)</code></a>-Funktion, wenn du eine Variable direkt ändern möchtest.</p></blockquote><h4 id="symmetrische-datenverschlüsselung">Symmetrische Datenverschlüsselung</h4><p><img src="/assets/images/blog/introducing-handyswift-4/share-puzzle.webp" alt="Rätsel teilen" loading="lazy" /></p><p>Bevor ich ein Kreuzworträtsel in <a href="https://apps.apple.com/app/apple-store/id6472669260?pt=549314&ct=fline.dev&mt=8">CrossCraft</a> hochlade, stelle ich sicher, dass es verschlüsselt wird, damit technisch versierte Leute die Antworten nicht einfach aus dem JSON auslesen können:</p><pre><code>func upload(puzzle: Puzzle) async throws {
   let key = SymmetricKey(base64Encoded: &quot;&lt;base-64 encoded secret&gt;&quot;)!
   let plainData = try JSONEncoder().encode(puzzle)
   let encryptedData = try plainData.encrypted(key: key)

   // Upload-Logik
}</code></pre><p>Beachte, dass der obige Code zwei Extensions nutzt: Zuerst wird <a href="https://swiftpackageindex.com/flinedev/handyswift/main/documentation/handyswift/cryptokit/symmetrickey/init(base64encoded:)"><code>init(base64Encoded:)</code></a> verwendet, um den Schlüssel zu initialisieren, dann verschlüsselt <a href="https://swiftpackageindex.com/flinedev/handyswift/main/documentation/handyswift/foundation/data/encrypted(key:)"><code>encrypted(key:)</code></a> die Daten unter Verwendung sicherer <a href="https://swiftpackageindex.com/flinedev/handyswift/main/documentation/handyswift/cryptokit">CryptoKit</a>-APIs unter der Haube, mit denen du dich nicht beschäftigen musst.</p><p>Wenn ein anderer Nutzer dasselbe Rätsel herunterlädt, entschlüssele ich es mit <a href="https://swiftpackageindex.com/flinedev/handyswift/main/documentation/handyswift/foundation/data/decrypted(key:)"><code>decrypted(key:)</code></a>:</p><pre><code>func downloadPuzzle(from url: URL) async throws -&gt; Puzzle {
   let encryptedData = // Download-Logik

   let key = SymmetricKey(base64Encoded: &quot;&lt;base-64 encoded secret&gt;&quot;)!
   let plainData = try encryptedPuzzleData.decrypted(key: symmetricKey)
   return try JSONDecoder().decode(Puzzle.self, from: plainData)
}</code></pre><blockquote><p>HandySwift liefert außerdem praktischerweise <a href="https://swiftpackageindex.com/flinedev/handyswift/main/documentation/handyswift/swift/string/encrypted(key:)"><code>encrypted(key:)</code></a>- und <a href="https://swiftpackageindex.com/flinedev/handyswift/main/documentation/handyswift/swift/string/decrypted(key:)"><code>decrypted(key:)</code></a>-Funktionen für <code>String</code>, die eine Base-64-kodierte String-Repräsentation der verschlüsselten Daten zurückgeben. Verwende sie, wenn du mit String-APIs arbeitest.</p></blockquote><hr /><h2 id="neue-typen">Neue Typen</h2><p>Neben der Erweiterung bestehender Typen führt HandySwift auch 7 neue Typen und 2 globale Funktionen ein. Hier sind die, die ich in nahezu jeder einzelnen App verwende:</p><h3 id="gregorian-day-und-time">Gregorian Day und Time</h3><p>Du möchtest ein <code>Date</code> aus Jahr, Monat und Tag konstruieren? Ganz einfach:</p><pre><code>GregorianDay(year: 1960, month: 11, day: 01).startOfDay() // =&gt; Date</code></pre><p>Du hast ein <code>Date</code> und möchtest nur den Datumsteil ohne die Uhrzeit speichern? Verwende einfach <a href="https://swiftpackageindex.com/flinedev/handyswift/main/documentation/handyswift/gregorianday"><code>GregorianDay</code></a> in deinem Model:</p><pre><code>struct User {
   let birthday: GregorianDay
}


let selectedDate = // aus dem DatePicker
let timCook = User(birthday: GregorianDay(date: selectedDate))
print(timCook.birthday.iso8601Formatted)  // =&gt; &quot;1960-11-01&quot;</code></pre><p>Du möchtest einfach das heutige Datum ohne Uhrzeit?</p><pre><code>GregorianDay.today</code></pre><p>Funktioniert auch mit <code>.yesterday</code> und <code>.tomorrow</code>. Für mehr einfach aufrufen:</p><pre><code>let todayNextWeek = GregorianDay.today.advanced(by: 7)</code></pre><blockquote><p><code>GregorianDay</code> konformiert zu allen Protocols, die du erwarten würdest, wie <code>Codable</code>, <code>Hashable</code> und <code>Comparable</code>. Für Encoding/Decoding wird das ISO-Format wie “2014-07-13” verwendet.</p></blockquote><p><a href="https://swiftpackageindex.com/flinedev/handyswift/main/documentation/handyswift/gregoriantimeofday"><code>GregorianTimeOfDay</code></a> ist das Gegenstück:</p><pre><code>let iPhoneAnnounceTime = GregorianTimeOfDay(hour: 09, minute: 41)
let anHourFromNow = GregorianTimeOfDay.now.advanced(by: .hours(1))


let date = iPhoneAnnounceTime.date(day: GregorianDay.today)  // =&gt; Date</code></pre><h3 id="delay-und-debounce">Delay und Debounce</h3><p>Wolltest du schon mal Code verzögert ausführen und fandest diese API umständlich zu merken und einzutippen?</p><pre><code>DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + .milliseconds(250)) {
   // dein Code
}</code></pre><p>HandySwift bietet eine kürzere Version, die leichter zu merken ist:</p><pre><code>delay(by: .milliseconds(250)) {
   // dein Code
}</code></pre><p>Es unterstützt auch verschiedene Quality-of-Service-Klassen wie <code>DispatchQueue</code> (Standard ist die Main Queue):</p><pre><code>delay(by: .milliseconds(250), qosClass: .background) {
   // dein Code
}</code></pre><p>Während Verzögerungen großartig für einmalige Aufgaben sind, gibt es manchmal schnelle Eingaben, die Performance- oder Skalierungsprobleme verursachen. Ein Nutzer könnte zum Beispiel schnell in ein Suchfeld tippen. Es ist gängige Praxis, die Aktualisierung der Suchergebnisse zu verzögern und zusätzlich ältere Eingaben zu verwerfen, sobald der Nutzer eine neue macht. Diese Praxis nennt sich “Debouncing”. Und mit HandySwift ist es ganz einfach:</p><pre><code>@State private var searchText = &quot;&quot;
let debouncer = Debouncer()


var body: some View {
    List(filteredItems) { item in
        Text(item.title)
    }
    .searchable(text: self.$searchText)
    .onChange(of: self.searchText) { newValue in
        self.debouncer.delay(for: .milliseconds(500)) {
            // Suchvorgang mit dem aktualisierten Suchtext nach 500 Millisekunden Nutzer-Inaktivität ausführen
            self.performSearch(with: newValue)
        }
    }
    .onDisappear {
        debouncer.cancelAll()
    }
}</code></pre><p>Beachte, dass der <a href="https://swiftpackageindex.com/flinedev/handyswift/main/documentation/handyswift/debouncer"><code>Debouncer</code></a> in einer Property gespeichert wurde, damit <a href="https://swiftpackageindex.com/flinedev/handyswift/main/documentation/handyswift/debouncer/cancelall()"><code>cancelAll()</code></a> beim Verschwinden für die Bereinigung aufgerufen werden kann. Aber <a href="https://swiftpackageindex.com/flinedev/handyswift/main/documentation/handyswift/debouncer/delay(for:id:operation:)-83bbm"><code>delay(for:id:operation:)</code></a> ist der Ort, an dem die Magie passiert – und du musst dich nicht um die Details kümmern!</p><hr />]]></content:encoded>
</item>
<item>
<title>Migration meiner SwiftUI-App nach VisionOS in 2 Stunden</title>
<link>https://fline.dev/de/blog/migrating-my-swiftui-app-to-visionos/</link>
<guid isPermaLink="true">https://fline.dev/de/blog/migrating-my-swiftui-app-to-visionos/</guid>
<pubDate>Sat, 02 Mar 2024 00:00:00 +0000</pubDate>
<description><![CDATA[Wie ich meine SwiftUI-App CrossCraft für visionOS portiert habe – pünktlich zum Day-1-Release der Apple Vision Pro. Es hat effektiv nur etwa 2 Stunden gedauert. Dieser Artikel fasst meine wichtigsten Erkenntnisse zusammen.]]></description>
<content:encoded><![CDATA[<p>Vor wenigen Monaten habe ich <a href="https://apps.apple.com/app/apple-store/id6472669260?pt=549314&ct=fline.dev&mt=8">CrossCraft: Custom Crosswords</a> veröffentlicht – eine komplett in SwiftUI geschriebene App, verfügbar auf iOS, iPadOS und macOS. Für den Launch der Vision Pro habe ich mir die Herausforderung gesetzt, sie auf die neue visionOS-Plattform zu portieren – allerdings habe ich mit der Migration erst 3 Tage vor dem Launch begonnen!</p><p>Die Frage war also, ob ich das in so kurzer Zeit schaffen würde. Aber zum Glück stellte sich heraus, dass es einfach genug war, und meine App war am Tag 1 bereit!</p><p><img src="/assets/images/blog/migrating-my-swiftui-app-to-visionos/the-official-email-from.webp" alt="Die offizielle E-Mail von Apple, in der sie sich bei Day-1-App-Entwicklern bedanken." loading="lazy" /></p><p><em>Die offizielle E-Mail von Apple, in der sie sich bei Day-1-App-Entwicklern bedanken.</em></p><p>Im Folgenden findest du alle meine Erkenntnisse, die dir bei der Migration deiner eigenen Apps helfen können!</p><h2 id="drittanbieter-frameworks">Drittanbieter-Frameworks</h2><p>Nachdem ich das „Apple Vision”-Ziel zu meinem Projekt hinzugefügt hatte, war mein erster Schritt, den „Apple Vision Pro”-Simulator auszuwählen und einen Build zu starten.</p><p><img src="/assets/images/blog/migrating-my-swiftui-app-to-visionos/step-2-adjusting-the.webp" alt="Schritt 2: Die Anpassung der Package.swift-Datei ist der wichtigste Schritt." loading="lazy" /></p><p>Wie erwartet schlug der Build fehl – denn nicht alle Frameworks unterstützen die visionOS-Plattform bereits. Aber grundlegende Unterstützung hinzuzufügen war einfach. Hier sind die 4 Schritte:</p><p><img src="/assets/images/blog/migrating-my-swiftui-app-to-visionos/step-2-adjusting-the-2.webp" alt="Schritt 2: Die Anpassung der Package.swift-Datei ist der wichtigste Schritt." loading="lazy" /></p><p><em>Schritt 2: Die Anpassung der Package.swift-Datei ist der wichtigste Schritt.</em></p><ol><li><p>Forke die Dependency, entferne sie aus deinem Projekt und füge deinen Fork mit dem <code>main</code>-Branch stattdessen hinzu.</p></li><li><p>Öffne die <code>Package.swift</code>-Datei im Fork, erhöhe die Swift-Tools-Version oben in der Datei auf <code>5.9</code> und füge <code>.visionOS(.v1)</code> zum <code>platforms</code>-Array hinzu.</p></li><li><p>Suche nach Vorkommen von <code>#if os(iOS)</code> und ändere sie zu <code>#if os(iOS) || os(visionOS)</code>, um den macOS-Pfad zu vermeiden und den iOS-Pfad zu bevorzugen.</p></li><li><p>Wähle den „Apple Vision Pro”-Simulator und baue das Projekt, um es zu bestätigen.</p></li></ol><p>Wenn du einen Fehler wegen fehlender APIs bekommst, stelle sicher, dass du <code>#if !os(visionOS)</code>-Checks an den richtigen Stellen einfügst. Die meisten APIs sollten aber verfügbar sein, da visionOS ein Fork von iPadOS ist, wie Apple offiziell bestätigt hat. Wenn ein Feature nicht vorhanden ist, wird es entweder bald kommen oder es macht auf der Plattform eben keinen Sinn.</p><p>In meinem Fall musste ich nur bei meiner eigenen <a href="https://github.com/FlineDev/ReviewKit">ReviewKit</a>-Bibliothek etwas Code rund um <code>SKReviewController</code> deaktivieren, der auf visionOS nicht verfügbar ist. Meine Bibliothek macht also effektiv nichts beim Build für <code>visionOS</code>, aber meine iOS- &amp; Mac-Apps fordern weiterhin Nutzer auf, die App zu bewerten. Ich hätte das mit einer eigenen UI lösen können, aber ich habe mich entschieden, erstmal die diesjährige WWDC abzuwarten – in der Hoffnung, dass es dort schon verfügbar wird.</p><p>Vergiss nicht, einen Pull Request an das Original-Repo zu stellen, wenn du es geforkt hast, damit auch andere in der Community von deinem Fix profitieren können. Je mehr Leute das machen, desto weniger Dependencies musst du selbst mit Plattform-Support versehen.</p><h2 id="testen-meiner-app-im-simulator">Testen meiner App im Simulator</h2><p>Nachdem ich alle Dependencies behoben hatte, baute ich meine App und es funktionierte!</p><p><img src="/assets/images/blog/migrating-my-swiftui-app-to-visionos/xcodes-preview-of-your.webp" alt="Xcodes Vorschau deines App-Icons." loading="lazy" /></p><p>Leider war ich noch nicht fertig. Zunächst fiel mir auf, dass beim Start der App kein App-Icon angezeigt wurde, obwohl ich eines im Projekt habe. Außerdem entdeckte ich nach dem Start sofort eine Reihe weiterer Probleme. Hier eine Übersicht:</p><ul><li><p>Das <strong>App-Icon</strong> fehlte</p></li><li><p>Beim Bewegen des Cursors (= Blick) war die <strong>Hover-Form</strong> an manchen Stellen falsch</p></li><li><p>Das <strong>Layout &amp; die Größen</strong> vieler Fenster, Modals und meiner UI-Elemente stimmten nicht</p></li><li><p>Meine <strong>Accent Color</strong> hatte keinen lesbaren Kontrast zum gläsernen Hintergrund</p></li></ul><p>Alle diese Punkte betreffen jede einzelne App, die nach visionOS migriert wird. Für mich halfen kleine Anpassungen, um sie zu beheben. Lass mich meine Erkenntnisse nacheinander teilen.</p><h2 id="app-icon">App-Icon</h2><p>Es stellt sich heraus, dass visionOS seinen eigenen App-Icon-Stil hat. Sie sind rund wie auf watchOS, bestehen aber aus mehreren Ebenen, um einen Tiefeneindruck zu erzeugen, wie auf tvOS. Du fügst ein visionOS-App-Icon hinzu, indem du auf den +-Button drückst und „visionOS App Icon” auswählst. Dann musst du mindestens ein „Front”- und ein „Back”-Ebenenbild in der Größe 1024 x 1024 bereitstellen. Die „Middle”-Ebene ist optional.</p><p><img src="/assets/images/blog/migrating-my-swiftui-app-to-visionos/xcodes-preview-of-your-2.webp" alt="Xcodes Vorschau deines App-Icons." loading="lazy" /></p><p>In meinem Fall bestand mein App-Icon bereits aus einer Hintergrundebene und einem Icon im Vordergrund, also war es kein großes Problem, sie separat zu exportieren. Ich musste nur die „Middle”-Ebene im rechten Panel entfernen. Aber weil mein Vordergrund-Icon einen Schatten hatte und die <a href="https://developer.apple.com/design/human-interface-guidelines/app-icons#Platform-considerations">Human Interface Guidelines</a> besagen, dass man „weiche oder ausgeblendete Kanten” bei Nicht-Hintergrund-Ebenen vermeiden soll, musste ich den Schatten entfernen. Das System fügt beim Hover automatisch einen leichten Schatten hinzu.</p><p>Apropos Hover: Xcode bietet oben eine Vorschau, wie dein App-Icon aussehen wird, und wenn du mit der Maus darüber fährst, simuliert es den 3D-Hover-Effekt, den Nutzer sehen, wenn sie auf der Vision Pro auf dein App-Icon schauen – richtig praktisch!</p><p><img src="/assets/images/blog/migrating-my-swiftui-app-to-visionos/xcodes-preview-of-your.gif" alt="Xcodes Vorschau deines App-Icons." loading="lazy" /></p><p><em>Xcodes Vorschau deines App-Icons.</em></p><h2 id="hover-effekte">Hover-Effekte</h2><p>In visionOS gehören die richtigen Hover-Effekte zu den Dingen, die man im Simulator leicht übersieht, die aber beim tatsächlichen Verwenden des Geräts extrem wichtig sind. Da man Elemente auf dem Gerät mit den Augen auswählt, ist es wichtig, dass deine App Feedback darüber gibt, welches Element gerade ausgewählt ist. Das funktioniert bei String-basierten Control-APIs in SwiftUI wie <code>Button(&quot;Click me&quot;) { ... }</code> von allein.</p><p>Aber sobald du einen eigenen <code>label</code>-Parameter für einen Button bereitstellst oder sogar komplett eigene Controls hast, musst du dem System die genaue Form deines Controls mitteilen. Zum Beispiel verwende ich ein eigenes Control namens <a href="https://github.com/FlineDev/HandySwiftUI/blob/main/Sources/HandySwiftUI/Views/HPicker.swift"><code>HPicker</code></a>, das ich anstelle des Standard-Dropdown-<code>Picker</code> nutze, wenn ich nur 2–4 Optionen zur Auswahl habe. Es sah beim Hovern über eine Option so aus:</p><p><img src="/assets/images/blog/migrating-my-swiftui-app-to-visionos/my-custom-hpicker-view.webp" alt="Mein eigener HPicker-View ohne jegliche Hover-Effekt-Anpassungen." loading="lazy" /></p><p><em>Mein eigener HPicker-View ohne jegliche Hover-Effekt-Anpassungen.</em></p><p>Die Anpassung, damit er der Form der Optionen folgt, war einfach genug:</p><pre><code class="language-Swift">Button {
   // ...
} label: {
   Label(option.description, systemImage: option.symbolSystemName)
      // ...
      .clipShape(.rect(cornerRadius: 12.5))
      #if !os(macOS)
      .contentShape(.hoverEffect, .rect(cornerRadius: 12.5))
      .hoverEffect()
      #endif
}</code></pre><p><em>Vereinfachte Version der Buttons in meinem <a href="https://github.com/FlineDev/HandySwiftUI/blob/main/Sources/HandySwiftUI/Views/HPicker.swift">HPicker</a>-Komponenten.</em></p><p>Die <code>.contentShape</code>- und <code>.hoverEffect</code>-Modifier sind das, was ich für einen korrekten Hover-Effekt hinzugefügt habe. Ersetze <code>.rect(cornerRadius: 12.5)</code> durch die jeweilige Form deines eigenen Controls. Beachte, dass ich sie in einen <code>#if !os(macOS)</code>-Check eingeschlossen habe, da meine App macOS unterstützt, <code>.hoverEffect</code> dort aber nicht verfügbar ist. Außerdem wichtig: Diese Modifier außerhalb des <code>Button</code> zu platzieren hat bei mir nicht funktioniert – sie müssen innerhalb der Label-Definition stehen, um richtig zu funktionieren.</p><p>In manchen Situationen fällt dir vielleicht auf, dass du einen Hover-Effekt hast, wo du keinen erwartest. Bei mir war das der Fall, wenn ich einen <code>Button</code> innerhalb einer anderen View platziert hatte, die selbst schon als Control erkannt wird, wie diese <code>DisclosureGroup</code>:</p><p><img src="/assets/images/blog/migrating-my-swiftui-app-to-visionos/the-entire-show-clues.webp" alt="Die gesamte " loading="lazy" /></p><p><em>Die gesamte „Show Clues”-Zeile ist ein Button, aber der Button darin ist ein weiterer Button.</em></p><p>Du kannst den Hover-Effekt abschalten, indem du einfach den <code>.hoverEffectDisabled()</code>-Modifier hinzufügst. In meinem Fall oben war der innere Button nur für macOS vorgesehen (weil eine <code>DisclosureGroup</code> auf dieser Plattform beim Klicken auf das Label nicht umschaltet). Meine Lösung war, nur auf macOS einen <code>Button</code> darin zu verwenden und sonst ein einfaches <code>Label</code>.</p><p>Alle Controls mit einem korrekten Hover-Effekt auszustatten war tatsächlich die zeitaufwendigste Aufgabe der Migration und dauerte etwa 40 Minuten. Es wäre wahrscheinlich viel schneller gegangen, wenn SwiftUI-Previews in meinem Projekt funktioniert hätten, aber aus irgendeinem Grund ließen sie sich bei mir nicht bauen, und wenn ich es versuchte, fing mein Mac an zu hängen.</p><h2 id="layoutsystem">Layoutsystem</h2><p>Obwohl visionOS auf iPadOS basiert und daher Dinge wie <code>Form</code>-Views ähnlich wie auf dem iPad darstellt, ist es wichtig zu verstehen, dass es einen entscheidenden Unterschied beim Layoutsystem im Vergleich zu iOS/iPadOS gibt:</p><p><img src="/assets/images/blog/migrating-my-swiftui-app-to-visionos/a-person-s-field-of-view.webp" alt="Das Sichtfeld einer Person in der Apple Vision Pro. Quelle: HIG" loading="lazy" /></p><p><em>Das Sichtfeld einer Person in der Apple Vision Pro. <a href="https://developer.apple.com/design/human-interface-guidelines/spatial-layout">Quelle: HIG</a></em></p><p>Auf Apple Vision werden Apps auf einer unendlichen Leinwand geöffnet – es gibt keine feste Bildschirmbreite oder -höhe, von der deine Views ihre Größe ableiten können. Das ist ein wesentlicher Unterschied, den du verstehen musst. Wenn du bereits Apps für macOS entwickelt hast, ist dir dieser Unterschied schon bekannt. In vieler Hinsicht ist das Layoutsystem dem von macOS viel ähnlicher, wo Monitore ebenfalls unterschiedliche Größen haben können und Fenster nur sehr selten im Vollbildmodus geöffnet werden, wie es auf iOS &amp; iPadOS üblich ist.</p><p>Wenn deine App also bereits macOS unterstützt, kannst du einfach die <code>#if os(macOS)</code>-Branches nutzen, die du wahrscheinlich schon zahlreich hast, wenn es um Größenanpassungen oder Fensterverwaltung geht. Ersetze sie einfach durch <code>#if os(macOS) || os(visionOS)</code>.</p><p>Der Hauptunterschied selbst zu macOS ist, dass Fenster abgerundete Ecken mit einem großen Corner Radius haben. Ich musste daher zusätzliches Padding oben und unten bei meinen Fenster-Root-Views hinzufügen, z. B. mit <code>.padding(.vertical, 10)</code>.</p><p>Wenn deine App noch nicht für macOS optimiert ist, hier einige wichtige Erkenntnisse:</p><ul><li><p>Du musst überall <code>.frame(minWidth: 400, minHeight: 300)</code> für deine Views angeben, sonst könnten deine Fenster oder Modals Größen haben, die für deine UI nicht funktionieren. Prüfe sie alle und gib passende Werte an.</p></li><li><p>Obwohl du <code>minWidth</code> und <code>minHeight</code> für deine Views angeben solltest, damit Nutzer sie nicht zu klein für deinen Inhalt machen können, möchtest du zusätzlich eine größere <code>.defaultSize(width: 800, height: 600)</code> auf deiner <code>WindowGroup</code>-Scene angeben, um standardmäßig eine größere Größe als das Minimum zu verwenden.</p></li><li><p>Wenn du modale Views hast, die deinen gesamten Bildschirm abdecken und auch diesen Platz brauchen, solltest du erwägen, diese Modals in eigene Fenster auszulagern. Nutze die <code>@Environment(\.openWindow) var openWindow</code>-Property, um neue Fenster auf visionOS (und macOS) zu öffnen, und definiere zusätzliche <code>WindowGroup</code>-Views. Lies meinen Artikel über <a href="https://www.fline.dev/window-management-on-macos-with-swiftui-4/">Fensterverwaltung in SwiftUI 4</a>, um mehr zu erfahren.</p></li><li><p>Du kannst dich bei der ersten Migration auch dafür entscheiden, die Modals beizubehalten, anstatt externe Fenster zu verwenden, was die sauberere Lösung wäre. Beachte aber, dass im Gegensatz zu macOS modale Sheets in visionOS nicht in der Größe veränderbar sind. Stelle also zumindest sicher, dass du eine Größe angibst, die gut für dein Sheet funktioniert – für alle Arten von möglicherweise dynamischen Daten, die im Modal angezeigt werden. Genau das habe ich für das „Puzzle spielen” in CrossCraft gemacht.</p></li></ul><h2 id="farben">Farben</h2><p>Beachte, dass Controls mit weißem Hintergrund nicht gut mit dem Hover-Effekt harmonieren, weil der Effekt ein weißes Overlay verwendet. Weiß auf Weiß ist eben nicht sichtbar. Dieses Problem trat bei meinem Kreuzworträtsel-Spielmodus auf, wo Nutzer auf Kacheln drücken, um Buchstaben einzugeben. Hier ist der Cursor auf einer Kachel, aber der Hover ist nicht sichtbar:</p><p><img src="/assets/images/blog/migrating-my-swiftui-app-to-visionos/hovers-are-not-visible.webp" alt="Hovers sind auf weißen Hintergrund-Buttons nicht sichtbar." loading="lazy" /></p><p><em>Hovers sind auf weißen Hintergrund-Buttons nicht sichtbar.</em></p><p>Meine schnelle Lösung war, den Modifier <code>.opacity(0.85)</code> hinzuzufügen, um meine weißen Hintergründe 15 % transparent zu machen, was schon geholfen hat. Mehr wäre besser, aber Weiß ist eben eine erwartete „Kreuzworträtsel”-Farbe, also habe ich versucht, es so weiß wie möglich zu halten.</p><p><img src="/assets/images/blog/migrating-my-swiftui-app-to-visionos/colors-that-are-legible.webp" alt="Farben, die auf iOS lesbar sind, funktionieren auf visionOS möglicherweise nicht." loading="lazy" /></p><p><em>Farben, die auf iOS lesbar sind, funktionieren auf visionOS möglicherweise nicht.</em></p><p>Mir ist auch aufgefallen, dass viele Farben mit mittlerem Kontrast, einschließlich der Standard-„Blue”-Accent Color, einen wirklich schlechten Kontrast auf dem Standard-Glas-Hintergrund des Fensters haben. Stelle sicher, dass du diese Farben für visionOS heller machst. Du kannst eine spezifische Variante für „Apple Vision” über den Attributes Inspector hinzufügen, wenn eine Farbe ausgewählt ist.</p><p><img src="/assets/images/blog/migrating-my-swiftui-app-to-visionos/colors.webp" alt="Colors" loading="lazy" /></p><blockquote><p>Um eine Farbe im HSB-System heller zu machen, musst du die Sättigung leicht verringern und die Helligkeit deutlich erhöhen. Zusätzlich kannst du den Farbton ein wenig in Richtung des nächsten hellen RGB-Werts verschieben. Lies <a href="https://www.learnui.design/blog/color-in-ui-design-a-practical-framework.html">diesen Artikel</a>, um mehr darüber zu erfahren, wie man Farben mit HSB richtig heller/dunkler macht.</p></blockquote><h2 id="fazit">Fazit</h2><p>Wenn du eine App hast, die bereits auf iPadOS &amp; macOS läuft, bist du in einer sehr guten Ausgangslage, um visionOS-Unterstützung hinzuzufügen. Du kannst deinen gesamten SwiftUI-Code wiederverwenden. Bei der Fensterverwaltung solltest du die macOS-Variante wählen. Für alles andere die iPadOS-Variante. Stelle dann sicher, dass alle deine eigenen Controls einen korrekten Hover-Effekt haben. Nimm einige Layout- &amp; UI-Anpassungen vor, wie Padding hinzufügen, Farben heller machen oder dein App-Icon in Front- und Back-Ebene aufteilen.</p><p>Der gesamte Prozess hat bei mir effektiv 2 Stunden gedauert. Ich habe den gesamten Vorgang live gestreamt – du findest meine Aufnahmen (ohne die „Warten auf Build”- und „Chat”-Phasen) in den folgenden zwei YouTube-Videos, jedes etwa eine Stunde lang. Ich habe Zeitmarken für die verschiedenen oben beschriebenen Schritte hinzugefügt, damit du gezielt in Einzelheiten eintauchen kannst:</p><iframe width="200" height="113" src="https://www.youtube.com/embed/snLZXfMQJic?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen="" title="Migrating my SwiftUI App to VisionOS in 2 hours – Part 1  |  Indie Apps for Apple Vision Pro"></iframe>
<iframe width="200" height="113" src="https://www.youtube.com/embed/zSe-zkAZQs8?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen="" title="Migrating my SwiftUI App to VisionOS in 2 hours – Part 2  |  Indie Apps for Apple Vision Pro"></iframe>
]]></content:encoded>
</item>
<item>
<title>Vorstellung von &quot;Posters&quot; – Meine erste Spatial-first App für Vision Pro</title>
<link>https://fline.dev/de/blog/introducing-posters-my-first-spatial-first-app-for-vision-pro/</link>
<guid isPermaLink="true">https://fline.dev/de/blog/introducing-posters-my-first-spatial-first-app-for-vision-pro/</guid>
<pubDate>Fri, 23 Feb 2024 00:00:00 +0000</pubDate>
<description><![CDATA[Hauche deinem Zuhause Leben ein mit automatisch aktualisierten & interaktiven Postern der neuesten angesagten Filme & Serien. Tippe auf ein Poster, um den Trailer anzusehen, herauszufinden wo es gerade gestreamt wird, oder ein Kino in deiner Nähe zu finden. Die Zukunft ist da!]]></description>
<content:encoded><![CDATA[<p>Als großer Filmfan dekoriere ich meine Wände schon seit langem mit Filmpostern. Aber statische Poster können mit der Zeit langweilig werden. Als Apple also die Vision Pro auf den Markt brachte und Reviewer bestätigten, dass die Fensterpositionen sehr genau sind, dachte ich sofort daran, <strong>eine App zu bauen, um meine Wände zu dekorieren</strong> – mit automatisch aktualisierten Postern.</p><p>Als ich meiner Frau einen Prototyp dieser Idee zeigte, fragte sie sofort, ob man die Poster anklicken kann, um einen Trailer anzusehen. Und so war die Idee geboren, sie interaktiv zu machen! In der gerade veröffentlichten App findet man neben Trailern auch Spielzeiten und Direktlinks zu Streaming-Diensten. Hier ein Demo-Video:</p><figure class="kg-card kg-video-card kg-width-regular" data-kg-thumbnail="https://www.fline.dev/content/media/2024/02/Posters-Demo-720p-silent_thumb.webp" data-kg-custom-thumbnail="">
<div class="kg-video-container">
<video src="/assets/images/blog/introducing-posters-my-first-spatial-first-app-for-vision-pro/posters-demo-720p-silent.mp4" poster="https://img.spacergif.org/v1/1280x720/0a/spacer.webp" width="1280" height="720" playsinline="" preload="metadata" style="background: transparent url('/assets/images/blog/introducing-posters-my-first-spatial-first-app-for-vision-pro/demo-thumb.webp') 50% 50% / cover no-repeat;"></video>
<div class="kg-video-overlay">
<button class="kg-video-large-play-icon" aria-label="Play video">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M23.14 10.608 2.253.164A1.559 1.559 0 0 0 0 1.557v20.887a1.558 1.558 0 0 0 2.253 1.392L23.14 13.393a1.557 1.557 0 0 0 0-2.785Z"></path>
</svg>
</button>
</div>
<div class="kg-video-player-container">
<div class="kg-video-player">
<button class="kg-video-play-icon" aria-label="Play video">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M23.14 10.608 2.253.164A1.559 1.559 0 0 0 0 1.557v20.887a1.558 1.558 0 0 0 2.253 1.392L23.14 13.393a1.557 1.557 0 0 0 0-2.785Z"></path>
</svg>
</button>
<button class="kg-video-pause-icon kg-video-hide" aria-label="Pause video">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<rect x="3" y="1" width="7" height="22" rx="1.5" ry="1.5"></rect>
<rect x="14" y="1" width="7" height="22" rx="1.5" ry="1.5"></rect>
</svg>
</button>
<span class="kg-video-current-time">0:00</span>
<div class="kg-video-time">
/<span class="kg-video-duration">0:30</span>
</div>
<input type="range" class="kg-video-seek-slider" max="100" value="0">
<button class="kg-video-playback-rate" aria-label="Adjust playback speed">1x</button>
<button class="kg-video-unmute-icon" aria-label="Unmute">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M15.189 2.021a9.728 9.728 0 0 0-7.924 4.85.249.249 0 0 1-.221.133H5.25a3 3 0 0 0-3 3v2a3 3 0 0 0 3 3h1.794a.249.249 0 0 1 .221.133 9.73 9.73 0 0 0 7.924 4.85h.06a1 1 0 0 0 1-1V3.02a1 1 0 0 0-1.06-.998Z"></path>
</svg>
</button>
<button class="kg-video-mute-icon kg-video-hide" aria-label="Mute">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M16.177 4.3a.248.248 0 0 0 .073-.176v-1.1a1 1 0 0 0-1.061-1 9.728 9.728 0 0 0-7.924 4.85.249.249 0 0 1-.221.133H5.25a3 3 0 0 0-3 3v2a3 3 0 0 0 3 3h.114a.251.251 0 0 0 .177-.073ZM23.707 1.706A1 1 0 0 0 22.293.292l-22 22a1 1 0 0 0 0 1.414l.009.009a1 1 0 0 0 1.405-.009l6.63-6.631A.251.251 0 0 1 8.515 17a.245.245 0 0 1 .177.075 10.081 10.081 0 0 0 6.5 2.92 1 1 0 0 0 1.061-1V9.266a.247.247 0 0 1 .073-.176Z"></path>
</svg>
</button>
<input type="range" class="kg-video-volume-slider" max="100" value="100">
</div>
</div>
</div>
</figure>
<p>Um sicherzustellen, dass alles wie erwartet funktioniert, habe ich Anfang dieser Woche Apples Developer-Labs in München besucht, da ich das Gerät hier in Deutschland nicht selbst kaufen kann, um es zu testen. Leider bestätigten die Apple-Ingenieure, dass visionOS das Speichern der Fensterpositionen über App-Neustarts hinweg noch nicht unterstützt. Vorerst muss man das Gerät also am Strom lassen, um die Poster-Positionen über mehrere Tage zu behalten. Aber es ist einfach, sie zu platzieren, also habe ich mich entschieden, die App trotzdem zu veröffentlichen. Und ich habe diesen Mangel auf allen offiziellen Wegen an Apple gemeldet, auch direkt an die Ingenieure im Lab. Ich habe noch mehr Ideen als nur diese, die dieses Feature brauchen. Ich bin überzeugt, dass es das Nummer-1-Feature ist, das uns Entwickler in visionOS 2.0 dringend brauchen.</p><p>Lange Rede, kurzer Sinn – ich habe gerade die „Posters”-App veröffentlicht und würde mich freuen, wenn du sie mal ausprobierst:</p><p><a href="https://apps.apple.com/app/apple-store/id6478062053?pt=549314&ct=fline.dev&mt=8">‎Posters: Discover Movies @Home</a></p><p>Lass mich bitte wissen, wenn du auf Probleme stößt. Ich bin wirklich begeistert davon, weitere Apps auf dieser neuen Plattform zu veröffentlichen. Mehrere Apps sind in Arbeit, von denen 2 vielleicht schon nächste Woche fertig werden könnten. Das liegt an meiner Entscheidung, mit meinen zukünftigen Apps auf Spatial-first zu setzen (statt Mobile-first). Immerhin wurden mehr als 70 % meines Umsatzes im letzten Monat auf Apple Vision erzielt. Und das, obwohl das Gerät noch nicht mal einen vollen Monat auf dem Markt ist! Aufregende Zeiten für Indies.</p><p><img src="/assets/images/blog/introducing-posters-my-first-spatial-first-app-for-vision-pro/vision-pro-revenue.webp" alt="Vision pro revenue" loading="lazy" /></p><p>Wenn du meine zukünftigen Apps nicht verpassen möchtest, folge mir doch einfach!</p>]]></content:encoded>
</item>
<item>
<title>RIESIGES CrossCraft 2.0 Update: Sieben große neue Features!</title>
<link>https://fline.dev/de/blog/huge-crosscraft-update-seven-major-features/</link>
<guid isPermaLink="true">https://fline.dev/de/blog/huge-crosscraft-update-seven-major-features/</guid>
<pubDate>Tue, 30 Jan 2024 00:00:00 +0000</pubDate>
<description><![CDATA[Dieses Update bringt wichtige Verbesserungen wie das Speichern und Synchronisieren von Kreuzworträtseln, erweiterte Inhalte mit 30 neuen Themen und ein verbessertes Nutzungserlebnis mit Features wie Rätsel-Tipps, einer nativen Mac-App, einer nativen Vision-Pro-App und Teiloptionen für kompetitives Spielen.]]></description>
<content:encoded><![CDATA[<p>CrossCraft 2.0 ist da – ein riesiges Update für die App, mit der du unendlich viele Kreuzworträtsel zu verschiedenen Themen erstellen und sogar eigene Fragen hinzufügen kannst, um Freunde, Familie oder dein Publikum mit einem personalisierten Rätsel zu überraschen.</p><p>In den 6 Wochen seit dem <a href="https://www.fline.dev/introducing-crosscraft/">ersten Release</a> habe ich unermüdlich daran gearbeitet, die am meisten gewünschten Features umzusetzen, und ich freue mich, dir 7 große Verbesserungen vorzustellen:</p><h3 id="1-kreuzworträtsel-speichern-und-synchronisieren">#1: Kreuzworträtsel speichern und synchronisieren</h3><p>Du kannst Kreuzworträtsel jetzt speichern, um sie später zu lösen! Und dein Fortschritt wird über iCloud auch geräteübergreifend synchronisiert. Das war das am meisten gewünschte Feature!</p><p><img src="/assets/images/blog/huge-crosscraft-update-seven-major-features/my-account-is-assigned-4.webp" alt="Mein Account ist dem deutschen App Store zugeordnet, daher sind meine Screenshots auf Deutsch." loading="lazy" /></p><h3 id="2-5-neue-kategorien-und-30-neue-themen">#2: 5 neue Kategorien und 30 neue Themen</h3><p>Vor diesem Update wurden 22 Themen in 5 Kategorien unterstützt. Dieses Update verdoppelt die Anzahl der Kategorien und mehr als verdoppelt die Anzahl der Themen! Und alle sind in allen 7 Sprachen verfügbar (EN, FR, DE, IT, PT, ES, TR).</p><p>Hier ist eine vollständige Liste aller neuen Themen:</p><ul><li><p><strong><em>Allgemeinwissen</em>:</strong>
Anime, Bollywood, Kultur &amp; Religion</p></li><li><p><strong><em>TV-Serien (neu)</em>:</strong>
Friends, Game of Thrones, SpongeBob, Die Simpsons</p></li><li><p><strong><em>Videospiele (neu)</em>:</strong>
Grand Theft Auto, Minecraft, Pokemon, Die Sims</p></li><li><p><strong><em>Sport</em>:</strong>
Karate, Taekwondo</p></li><li><p><strong><em>Sprachenlernen</em>:</strong>
Italienisch, Portugiesisch (Brasilien), Türkisch</p></li><li><p><strong><em>Kultur &amp; Religion</em> (neu):</strong>
Altes Ägypten, Christentum, Griechische Mythologie, Islam, Judentum</p></li><li><p><strong><em>Wissenschaft (neu)</em>:</strong>
Astronomie, Biologie, Informatik, Wirtschaft, Medizin</p></li><li><p><strong><em>Technologie (neu)</em>:</strong>
Apple, Autos &amp; Motoren, Google, Microsoft</p></li></ul><p>Weitere Themen sind bereits in Arbeit und werden in zukünftigen Updates hinzugefügt.</p><p><img src="/assets/images/blog/huge-crosscraft-update-seven-major-features/my-account-is-assigned.webp" alt="Mein Account ist dem deutschen App Store zugeordnet, daher sind meine Screenshots auf Deutsch." loading="lazy" /></p><h3 id="3-begrenzte-tipps-erhalten">#3: (Begrenzte) Tipps erhalten</h3><p>In jedem Rätsel gibt es diese paar Wörter, die man einfach nicht weiß oder sich nicht daran erinnert. Du kannst jetzt eine begrenzte Anzahl von Feldern aufdecken, damit du nicht bei einem zu 90 % gelösten Rätsel feststeckst, nur wegen eines seltsamen Hinweises – das kann echt nervig sein!</p><p><img src="/assets/images/blog/huge-crosscraft-update-seven-major-features/my-account-is-assigned-3.webp" alt="Mein Account ist dem deutschen App Store zugeordnet, daher sind meine Screenshots auf Deutsch." loading="lazy" /></p><h3 id="4-game-center-unterstützung">#4: Game Center Unterstützung</h3><p>Verdiene Erfolge für die Nutzung verschiedener Features der App und tritt gegen Freunde und andere in globalen Bestenlisten an. Jede Kategorie hat ihre eigene Bestenliste – also zeig, was du draufhast, und beweise dein Können in deinem Lieblingsthema!</p><p><img src="/assets/images/blog/huge-crosscraft-update-seven-major-features/my-account-is-assigned-2.webp" alt="Mein Account ist dem deutschen App Store zugeordnet, daher sind meine Screenshots auf Deutsch." loading="lazy" /></p><p><em>Mein Account ist dem deutschen App Store zugeordnet, daher sind meine Screenshots auf Deutsch.</em></p><h3 id="5-native-app-für-den-mac">#5: Native App für den Mac</h3><p>Während die iPad-Version von CrossCraft von Anfang an auf Apple-Silicon-Macs verfügbar war, habe ich mir die zusätzliche Mühe gemacht, eine native Mac-App mit einem passenderen Look &amp; Feel zu erstellen. Das bedeutet: CrossCraft ist jetzt zum ersten Mal auch auf Intel-Macs verfügbar!</p><p><img src="/assets/images/blog/huge-crosscraft-update-seven-major-features/native-mac-app-kopie.webp" alt="Native Mac-App Kopie" loading="lazy" /></p><h3 id="6-native-app-für-die-neue-apple-vision-pro">#6: Native App für die neue Apple Vision Pro</h3><p>Zusätzlich habe ich CrossCraft auf visionOS migriert, um ein natives Erlebnis zu bieten, und den gesamten Prozess sogar <a href="https://www.twitch.tv/Jeehut">live gestreamt</a>. Räumliches Kreuzworträtsel-Lösen ist da!</p><p><img src="/assets/images/blog/huge-crosscraft-update-seven-major-features/native-vision-os-app.webp" alt="Native visionOS-App" loading="lazy" /></p><h3 id="7-dasselbe-rätsel-teilen-und-spielen">#7: Dasselbe Rätsel teilen und spielen</h3><p>Teile dein Rätsel mit einem einfachen Link oder einem gedruckten QR-Code. So kannst du ein Rätsel erstellen und einen Freund herausfordern, dasselbe Rätsel zu lösen – wer ist schneller?</p><p>Wenn du ein Publikum von Lesern hast, teile ein Bild eines eigens vorbereiteten Rätsels, um sie herauszufordern, und füge einen QR-Code hinzu, der das Rätsel direkt in CrossCraft öffnet – für ein reibungsloses Lösen direkt in der App.</p><p>Ich habe Rätsel in 7 Sprachen für dich erstellt, damit du das neue Teilen-Feature ausprobieren kannst:</p><ul><li><p><em>Anime-Rätsel</em>: <a href="https://play.crosscraft.app/puzzle/en/3FECEA6F-C05D-45BF-A2F8-A64142483E59">EN</a> | <a href="https://play.crosscraft.app/puzzle/fr/10B32327-3244-42E8-8DB1-58FEDC89EE3B">FR</a> | <a href="https://play.crosscraft.app/puzzle/de/D7632F91-F2C1-4D6C-B472-DDF061A33A1A">DE</a> | <a href="https://play.crosscraft.app/puzzle/it/D2CE35FC-9900-474F-BA39-5EEC59F40918">IT</a> | <a href="https://play.crosscraft.app/puzzle/pt-BR/81DE5464-0461-41F0-97AE-E92034230B20">PT</a> | <a href="https://play.crosscraft.app/puzzle/es/5E9FC723-6E4E-40E0-8C40-FC02DD4F49BB">ES</a> | <a href="https://play.crosscraft.app/puzzle/tr/E086D5F6-E436-4450-ADBE-62ADE1D8E680">TR</a></p></li><li><p>*Apple-Rätsel: *<a href="https://play.crosscraft.app/puzzle/en/84F7B9F7-5075-4E0D-8351-1B27037204D7">EN</a> | <a href="https://play.crosscraft.app/puzzle/fr/06CB25A5-89CD-4DF2-8ABF-59F5A531F030">FR</a> | <a href="https://play.crosscraft.app/puzzle/de/ABCEAC51-DCEA-4D62-8419-E5AD969FF117">DE</a> | <a href="https://play.crosscraft.app/puzzle/it/3C688B45-4112-4F94-89D5-5C0C92D3CF12">IT</a> | <a href="https://play.crosscraft.app/puzzle/pt-BR/96DA57BA-B3ED-4ADB-9AEF-D19BD7B986CA">PT</a> | <a href="https://play.crosscraft.app/puzzle/es/7DD54D02-872B-44A9-B41E-69F5F34FA438">ES</a> | <a href="https://play.crosscraft.app/puzzle/tr/106096FF-D253-4D07-8F7D-9BE8846A60A0">TR</a></p></li><li><p>*Informatik-Rätsel: *<a href="https://play.crosscraft.app/puzzle/en/2CDB3AD1-4A0C-4A2E-9BF0-E09EE02A6234">EN</a> | <a href="https://play.crosscraft.app/puzzle/fr/ED043A8F-3F86-4272-A200-429E3FAE0E21">FR</a> | <a href="https://play.crosscraft.app/puzzle/de/0C7A9454-5E00-41F9-93D5-02F09236361F">DE</a> | <a href="https://play.crosscraft.app/puzzle/it/AB3D4E71-2F75-4182-9BA7-FC6F57EC339D">IT</a> | <a href="https://play.crosscraft.app/puzzle/pt-BR/5CAEB43A-9166-480E-989C-012012C03550">PT</a> | <a href="https://play.crosscraft.app/puzzle/es/75B85338-4503-4DE3-AF50-96542704EB08">ES</a> | <a href="https://play.crosscraft.app/puzzle/tr/D78D5A03-04C3-4ED8-AF91-33C8166BE79D">TR</a></p></li><li><p>*Wirtschaft-Rätsel: *<a href="https://play.crosscraft.app/puzzle/en/240AD409-091E-4405-BDCE-F6FF54AEF40B">EN</a> | <a href="https://play.crosscraft.app/puzzle/fr/DB3D5F50-C36F-4782-81C1-9163F70F9236">FR</a> | <a href="https://play.crosscraft.app/puzzle/de/D6E94DE9-6109-4274-B090-68256DC21DB0">DE</a> | <a href="https://play.crosscraft.app/puzzle/it/5096B962-36D1-4ADD-9260-10FFCECCEC46">IT</a> | <a href="https://play.crosscraft.app/puzzle/pt-BR/346695F4-0493-42A6-86E6-0AA639E2CCAC">PT</a> | <a href="https://play.crosscraft.app/puzzle/es/BCE62711-27AF-4CC9-83B2-D658F6928B5E">ES</a> | <a href="https://play.crosscraft.app/puzzle/tr/1F02E249-3413-4B58-B88B-5291BCDCCF94">TR</a></p></li></ul><p><img src="/assets/images/blog/huge-crosscraft-update-seven-major-features/share-play-the-same-puzzle-kopie.webp" alt="Teilen und dasselbe Rätsel spielen Kopie" loading="lazy" /></p><p>Das waren die am meisten gewünschten Features – ich hoffe, dir gefällt, wie sie geworden sind!</p><p>Hol dir das Update jetzt:</p><p><a href="https://apps.apple.com/app/apple-store/id6472669260?pt=549314&ct=fline.dev&mt=8">‎CrossCraft: Custom Crosswords</a></p><p>Falls du noch weitere Features vermisst, <a href="mailto:crosscraft@fline.dev">schreib mir gerne eine E-Mail</a>.</p>]]></content:encoded>
</item>
<item>
<title>In 8 einfachen Schritten Kreuzworträtsel zu jedem Thema mit ChatGPT erstellen</title>
<link>https://fline.dev/de/blog/8-steps-to-create-crosswords-with-chatgpt/</link>
<guid isPermaLink="true">https://fline.dev/de/blog/8-steps-to-create-crosswords-with-chatgpt/</guid>
<pubDate>Mon, 18 Dec 2023 00:00:00 +0000</pubDate>
<description><![CDATA[Entdecke, wie einfach es ist, personalisierte Kreuzworträtsel zu jedem Thema mit der vollen Kraft von ChatGPT zu erstellen. Diese Anleitung führt dich in acht einfachen Schritten durch den Prozess – vom Generieren der Hinweis-Antwort-Paare bis zum Gestalten und Anpassen deines Rätsels mit der innovativen CrossCraft-App.]]></description>
<content:encoded><![CDATA[<h3 id="schritt-1-hinweis-antwort-paare-mit-chatgpt-generieren">Schritt 1: Hinweis-Antwort-Paare mit ChatGPT generieren</h3><p>Gib ChatGPT den folgenden Prompt ein, aber vergiss nicht, <code>THEMA</code> durch das Thema zu ersetzen, zu dem du ein Kreuzworträtsel erstellen möchtest, und <code>SPRACHE</code> durch die Sprache, in der die Fragen und Antworten sein sollen:</p><blockquote><p>Erstelle 50 Hinweis-Antwort-Paare für ein Kreuzworträtsel zum Thema “<em><strong>THEMA</strong></em>” für ein Publikum von Fans des Themas. Biete eine gute Mischung aus leichteren und schwierigeren Fragen. Biete eine gute Mischung aus historischen und aktuelleren Beispielen.
Gib Hinweise und Antworten auf <em><strong>SPRACHE</strong></em> an.
Vermeide Paare, bei denen die Antwort eine Zahl enthält. Vermeide Hinweise, die (Teile der) Antwort enthalten. Halte sowohl Hinweise als auch Antworten kurz, z.B. vermeide unnötige Artikel. Ersetze Sonderzeichen wie “+” in der Antwort durch das ausgeschriebene Wort, z.B. “plus”.
Formatiere die Hinweis-Antwort-Paare als einen einzelnen Codeblock mit kommasepariertem CSV wie folgt:</p><pre><code class="language-csv">&quot;Dies ist der erste Hinweis&quot;,&quot;Antwort&quot;
&quot;Dies ist der zweite Hinweis&quot;,&quot;Antwort&quot;</code></pre></blockquote><p>Das <code>THEMA</code> kann durch alles Mögliche ersetzt werden. Eine bestimmte Technologie, der Name eines Franchises, eine Sportart, die du magst, oder die URL einer Medienwebsite, die du täglich besuchst. Ich bin iOS-Entwickler und interessiere mich für die neuesten Nachrichten über Apple-Produkte. Also kann ich <code>THEMA</code> einfach durch <code>Apple News</code> und <code>SPRACHE</code> durch <code>Englisch</code> ersetzen. ChatGPT sollte dann etwa so antworten:</p><p><img src="/assets/images/blog/8-steps-to-create-crosswords-with-chatgpt/chatgpt-saves-a-lot-of.webp" alt="ChatGPT spart viel Zeit beim Erstellen passender Hinweise und Antworten." loading="lazy" /></p><p><em>ChatGPT spart viel Zeit beim Erstellen passender Hinweise und Antworten.</em></p><blockquote><p>Wenn du nicht sofort eine solche Antwort bekommst, versuch es einfach noch mal, indem du den 🔄 Wiederholen-Button unter der Antwort drückst.</p></blockquote><h3 id="schritt-2-den-csv-text-in-eine-datei-kopieren">Schritt 2: Den CSV-Text in eine Datei kopieren</h3><p><img src="/assets/images/blog/8-steps-to-create-crosswords-with-chatgpt/a-selection-of-topics-2.webp" alt="Eine Auswahl an Themen, die du in der CrossCraft-App findest." loading="lazy" /></p><p>Drücke oben rechts im Codeblock auf „Copy code”, erstelle eine neue Datei mit einem Klartexteditor deiner Wahl (z.B. <a href="https://www.sublimetext.com/download">SublimeText</a>) und füge den Inhalt in die neue Datei ein. Achte darauf, die Datei im Klartextformat mit der Endung <code>.csv</code> zu speichern.</p><blockquote><p>Dokumenteditoren wie Word oder TextEdit speichern ihre Dateien in speziellen Markup-Formaten wie <code>.docx</code> oder <code>.rtf</code>. Das sind keine Klartextdateien.</p></blockquote><h3 id="schritt-3-die-gültigkeit-der-generierten-paare-prüfen">Schritt 3: Die Gültigkeit der generierten Paare prüfen</h3><p>Obwohl der Prompt bereits vorgibt, keine Zahlenwerte in den Antworten zu verwenden und die Antworten nicht in den Fragen zu verraten, macht ChatGPT Fehler. Prüfe also nochmal, ob alles korrekt aussieht. Du kannst auch eigene Einträge mit eigenen Fragen hinzufügen, um das Ganze noch etwas aufzupeppen.</p><h3 id="schritt-4-ein-passendes-fallback-thema-in-crosscraft-auswählen">Schritt 4: Ein passendes Fallback-Thema in CrossCraft auswählen</h3><p><img src="/assets/images/blog/8-steps-to-create-crosswords-with-chatgpt/a-selection-of-topics.webp" alt="Eine Auswahl an Themen, die du in der CrossCraft-App findest." loading="lazy" /></p><p><em>Eine Auswahl an Themen, die du in der CrossCraft-App findest.</em></p><p>Stelle sicher, dass du CrossCraft kostenlos auf deinem iPhone, iPad oder Mac installiert hast:</p><p><a href="https://apps.apple.com/app/apple-store/id6472669260?pt=549314&ct=fline.dev&mt=8">‎CrossCraft: Custom Crosswords</a></p><p>Öffne die App und wähle eines der vielen Themen aus, die die App standardmäßig mitliefert – am besten das, das deinem Thema am nächsten kommt. Falls ein passendes Thema noch nicht vorhanden ist, drücke unbedingt „Thema vorschlagen” in der App. Du kannst auch oben „Ohne Thema, nur eigene Fragen” wählen, wenn du die Lücken nicht mit Fragen eines Fallback-Themas füllen möchtest. Für vollere Rätsel kannst du immer Allgemeinwissen-Themen wie „Berühmte Persönlichkeiten” oder „Popkultur” wählen, da diese weithin bekannt sind. Ich habe „Technologie” ausgewählt.</p><h3 id="schritt-5-die-csv-datei-in-crosscraft-importieren">Schritt 5: Die CSV-Datei in CrossCraft importieren</h3><p><img src="/assets/images/blog/8-steps-to-create-crosswords-with-chatgpt/importing-csv-files-is-a.webp" alt="Das Importieren von CSV-Dateien ist ein Premium-Feature." loading="lazy" /></p><p><em>Das Importieren von CSV-Dateien ist ein Premium-Feature.</em></p><p>Stelle sicher, dass du Zugriff auf die <code>.csv</code>-Datei von dem Gerät hast, auf dem du CrossCraft verwendest – z.B. indem du die Datei in iCloud Drive speicherst, um sie auf deinem iPhone oder iPad zu öffnen. Dann drücke im Assistenten, in dem du die Größe und Schwierigkeit des Kreuzworträtsels wählen kannst, im letzten Schritt auf „Aus Datei importieren…” und wähle <code>CSV</code> im Dialog.</p><h3 id="schritt-6-kreuzworträtsel-neu-generieren-bis-es-passt">Schritt 6: Kreuzworträtsel (neu) generieren, bis es passt</h3><p><img src="/assets/images/blog/8-steps-to-create-crosswords-with-chatgpt/the-resulting-exported.webp" alt="Das resultierende exportierte personalisierte Kreuzworträtsel für MacRumors.com" loading="lazy" /></p><p>Drücke den Button „Kreuzworträtsel erstellen” unten rechts, um dein erstes individuelles Kreuzworträtsel zu generieren. Die Generierung stoppt automatisch, wenn der aktuelle Zustand des Rätsels nicht mehr verbessert werden kann. Unterhalb der Kreuzworträtsel-Vorschau siehst du Werte, die die Qualität des generierten Kreuzworträtsels anzeigen – also wie „gefüllt” es ist und wie viele Kreuzungen es hat. Der Qualitätsprozentsatz fasst diese Aspekte in einem einzigen Wert zusammen. Jeder Prozentsatz über 50 % kann als „gut gefülltes” Kreuzworträtsel betrachtet werden.</p><p>Auf demselben Bildschirm findest du einen „Neu generieren”-Button, um den zufälligen Generierungsalgorithmus neu zu starten, falls du mit der Qualität des Kreuzworträtsels oder der Auswahl der Hinweise unzufrieden bist. Die Hinweise kannst du dir über „Hinweise anzeigen” ansehen.</p><h3 id="schritt-7-dein-kreuzworträtsel-als-bild-exportieren">Schritt 7: Dein Kreuzworträtsel als Bild exportieren</h3><p><img src="/assets/images/blog/8-steps-to-create-crosswords-with-chatgpt/crossword-export-share-options.webp" alt="Der Teilen-Dialog in CrossCraft zeigt die Exportoptionen." loading="lazy" /></p><p>Auf iPhone oder iPad empfehle ich, „Teilen…” zu drücken und dann „Bild sichern” auszuwählen, um das Kreuzworträtsel in der Fotos-App zu speichern. Von dort kannst du es bei Bedarf mit anderen Geräten teilen. So wird sichergestellt, dass das Bild nicht zu einem JPEG ohne Transparenz herunterkonvertiert wird. Alternativ kannst du „In Datei sichern” verwenden.</p><p>Du wirst gefragt, welchen Teil du teilen möchtest. Wähle „Kreuzworträtsel + Hinweise” für ein kombiniertes Einzelbild. Alternativ kannst du das Rätsel und die Hinweise separat exportieren, falls du sie zum Beispiel auf 2 separaten Seiten ausdrucken möchtest.</p><h3 id="schritt-8-optional-ein-hintergrundbild-deiner-wahl-hinzufügen">Schritt 8: (Optional) Ein Hintergrundbild deiner Wahl hinzufügen</h3><p><img src="/assets/images/blog/8-steps-to-create-crosswords-with-chatgpt/the-resulting-exported-2.webp" alt="Das resultierende exportierte personalisierte Kreuzworträtsel für MacRumors.com" loading="lazy" /></p><p><em>Das resultierende exportierte personalisierte Kreuzworträtsel für MacRumors.com</em></p><p>Für eine besonders schöne Optik such dir ein passendes Hintergrundbild zu deinem Thema und platziere das exportierte PNG-Bild darüber – mit einem Designtool deiner Wahl (z.B. <a href="https://www.figma.com/login">Figma</a>). Du könntest sogar KI-Bildgeneratoren verwenden und sie bitten, eine „Szenerie” zu erstellen, die zu deinem Thema passt. Genau das habe ich mit DALL-E in ChatGPT Plus gemacht. Mein genauer Prompt für das obige Hintergrundbild war:</p><blockquote><p>Erstelle eine Szenerie, die zum Thema „Apple News” passt, im Hochformat. Die untere Hälfte sollte eine einheitliche Farbe/Schattierung sein. Halte es insgesamt schlicht.</p></blockquote><p>Das exportierte Bild hat praktischerweise eine eingebaute Transparenz von 5 % in den weißen Hintergründen der Felder und Hinweise, sodass etwas von deinem Hintergrundbild durchscheint. Das Endergebnis siehst du oben. Mit mehr Zeit und Kreativität kannst du sicher noch etwas Besseres erstellen!</p><p>Probier es selbst aus und überrasche jemanden, den du magst. Es ist das perfekte Geschenk!</p><p><a href="https://apps.apple.com/app/apple-store/id6472669260?pt=549314&ct=fline.dev&mt=8">‎CrossCraft: Custom Crosswords</a></p>]]></content:encoded>
</item>
<item>
<title>Vorstellung von CrossCraft: Individuelle Kreuzworträtsel</title>
<link>https://fline.dev/de/blog/introducing-crosscraft/</link>
<guid isPermaLink="true">https://fline.dev/de/blog/introducing-crosscraft/</guid>
<pubDate>Fri, 15 Dec 2023 00:00:00 +0000</pubDate>
<description><![CDATA[Erstelle thematische und personalisierte Kreuzworträtsel mit Leichtigkeit und spiele sie selbst oder überrasche deine Liebsten mit einem maßgeschneiderten Geschenk. Perfekt auch, um spielerisch Vokabeln zu lernen oder deine Schüler oder Freunde mit einem unterhaltsamen Quiz herauszufordern.]]></description>
<content:encoded><![CDATA[<p>Bist du ein Kreuzworträtsel-Fan, der sich schon immer ein persönlicheres Rätsel-Erlebnis gewünscht hat? Oder ein großer Fan von Franchises wie Harry Potter oder Herr der Ringe auf der Suche nach der nächsten Herausforderung, um dein tiefes Wissen zu beweisen? Oder vielleicht eine Lehrkraft, die nach einer spannenden Möglichkeit sucht, Vokabeln mit Schülern zu festigen? Vielleicht suchst du auch einfach nach einer einzigartigen und unterhaltsamen Geschenkidee für das nächste Familienevent. Was auch immer deine Motivation ist – <a href="https://crosscraft.app/">CrossCraft</a> verwandelt deine Ideen in Sekundenschnelle in spaßige Kreuzworträtsel. Und das alles kostenlos!</p><p><img src="/assets/images/blog/introducing-crosscraft/created-with-crosscraft.webp" alt="Erstellt mit CrossCraft. Thema: Popkultur. Die letzte Frage ist personalisiert." loading="lazy" /></p><p><em>Erstellt mit CrossCraft. Thema: Popkultur. Die letzte Frage ist personalisiert.</em></p><h2 id="volle-personalisierung-möglich">Volle Personalisierung möglich</h2><p>CrossCraft ist nicht einfach nur eine weitere Kreuzworträtsel-App. Es ist ein Werkzeug, mit dem du eine <strong>unbegrenzte Anzahl</strong> abwechslungsreicher Kreuzworträtsel zu einem ausgewählten Thema erstellen kannst. Darüber hinaus kannst du eigene Hinweis-Antwort-Paare hinzufügen, um das Kreuzworträtsel zu <strong>personalisieren</strong> oder gezieltere Fragen für dein Publikum zu stellen. Die App platziert deine eigenen Fragen immer zuerst im Rätsel, bevor sie die Lücken mit dem ausgewählten Thema füllt. Das bedeutet: Wenn du genug eigene Fragen mitbringst, kannst du sogar komplett individuelle Kreuzworträtsel erstellen – fast ohne Füllwörter aus dem gewählten Thema. Perfekt für Lehrkräfte, die eine unterhaltsame Herausforderung für ihre Schüler vorbereiten.</p><p>Um umständliches Tippen auf dem Handy oder Tablet zu vermeiden, kann die App Listen von Hinweis-Antwort-Paaren aus <strong>CSV- oder JSON-Dateien</strong> <strong>importieren</strong>. So kannst du deine eigenen Inhalte bequem am Computer erstellen – zum Beispiel mit Google Sheets, das sogar die Zusammenarbeit mit anderen ermöglicht, um schneller mehr Inhalte zu erstellen. Wenn alles bereit ist, speichere das Sheet einfach als CSV-Datei und erstelle dein komplett individuelles Kreuzworträtsel.</p><h2 id="viele-themen-zur-auswahl">Viele Themen zur Auswahl</h2><p>Eine ausreichend große Sammlung von Hinweis-Antwort-Paaren zu erstellen, kann eine mühsame und zeitaufwändige Aufgabe sein. Für ein gut gefülltes mittelgroßes Kreuzworträtsel können je nach Länge der Antworten und ihrer Buchstaben Hunderte von Paaren nötig sein. Deshalb liefert CrossCraft eine große Auswahl an Themen mit, die du als Fallback verwenden kannst, um die Lücken zu füllen. Oder um einfach ein thematisches Kreuzworträtsel ganz ohne eigene Fragen zu erstellen. Der Personalisierungsschritt ist nämlich komplett optional!</p><p><img src="/assets/images/blog/introducing-crosscraft/topics.webp" alt="Themen" loading="lazy" /></p><p>Im ersten Release werden 20 verschiedene Themen aus 5 verschiedenen Kategorien unterstützt:</p><ul><li><p><strong>Allgemeinwissen:</strong> Berühmte Persönlichkeiten, Geografie, Geschichte, Popkultur</p></li><li><p><strong>Film-Franchises:</strong> Harry Potter, Herr der Ringe, Marvel (MCU), Star Wars</p></li><li><p><strong>Sport:</strong> American Football, Basketball, Formel 1, Fußball</p></li><li><p><strong>Sprachenlernen:</strong> Englisch, Französisch, Japanisch, Spanisch</p></li><li><p><strong>Programmierung:</strong> Android-Entw., Apple-Plattf.-Entw., Flutter, Ruby on Rails</p></li></ul><p>Mit jedem Update kommen weitere Themen in weiteren Kategorien hinzu. Einige der nächsten Themen auf meiner Roadmap sind Filmzitate, Game of Thrones, Anime, weitere Sprachen wie Deutsch oder Italienisch und viele mehr in neuen Kategorien wie Musik, Kunst, Literatur, Wissenschaft und Technologie.</p><p>Welche Themen als Nächstes kommen, hängt ganz davon ab, was sich die Nutzer am meisten wünschen: In der App gibt es einen „Thema vorschlagen”-Button – nutz ihn unbedingt für dein Lieblingsthema!</p><p>Weitere Features wie die Möglichkeit, ein Lösungswort zu definieren, sind bereits geplant.</p><h2 id="in-allen-formen-und-größen">In allen Formen und Größen</h2><p>Beim Erstellen eines Kreuzworträtsels kannst du aus 5 verschiedenen Größen, 3 verschiedenen Formen und 3 verschiedenen Schwierigkeitsgraden wählen. Das Thema „Spanisch” enthält zum Beispiel die 600 häufigsten Wörter auf Spanisch. Wenn der leichteste Schwierigkeitsgrad ausgewählt wird, werden nur die 200 häufigsten Wörter verwendet, 400 beim mittleren und alle 600 beim schwierigsten Level. So kannst du einfach anfangen und die Schwierigkeit steigern, wenn du die leichten Antworten schon alle kennst.</p><p><img src="/assets/images/blog/introducing-crosscraft/app-preview-6.7.gif" alt="App-Vorschau 6.7" loading="lazy" /></p><p>Für Lehrkräfte kann es wichtig sein, ein ganzseitiges Kreuzworträtsel auszudrucken. Die Formen „Querformat” und „Hochformat” sind so dimensioniert, dass sie eine ganze Seite perfekt ausfüllen. Beim Teilen oder Drucken eines erstellten Kreuzworträtsels bietet CrossCraft die Möglichkeit, das Rätsel und die Hinweise separat zu exportieren. So kann das Rätsel auf einer Seite und die Hinweise auf einer zweiten gedruckt werden, was die Lesbarkeit besonders bei größeren Kreuzworträtseln verbessert.</p><h2 id="für-alle-und-überall">Für alle und überall</h2><p>Sowohl die App als auch alle Themeninhalte sind in <strong>7 Sprachen</strong> verfügbar: Englisch, Französisch, Deutsch, Italienisch, Portugiesisch (Brasilien), Spanisch und Türkisch. Die Sprache für die Hinweise kann unabhängig von der App-Sprache gewählt werden, sodass du ganz einfach Kreuzworträtsel für verschiedene Sprachzielgruppen erstellen kannst. Wo es sinnvoll ist, wurden die Themeninhalte an das Publikum der jeweiligen Sprache angepasst. Wenn zum Beispiel nach berühmten Moderatoren gefragt wird, sind das auf Englisch „Oprah Winfrey” oder „David Letterman”, während auf Deutsch eher Namen wie „Günther Jauch” und „Anne Will” passender sind. Diese Anpassungen wurden für alle Sprachen vorgenommen.</p><p>Außerdem ist beim Erstellen und Spielen der Kreuzworträtsel kein Server beteiligt. Alles passiert auf dem Gerät, was bedeutet, dass die App auch komplett offline funktioniert. Perfekt zum Spielen im Zug oder im Flugzeug ohne Internet.</p><h2 id="worauf-wartest-du-noch-hol-sie-dir-jetzt">Worauf wartest du noch? Hol sie dir jetzt!</h2><p>Unendlich viele personalisierte Kreuzworträtsel erstellen, spielen und teilen – komplett kostenlos. Keine Werbung. Kein Tracking. Keine Fallen. Nur einige Optionen und der Datei-Import sind „Pro-Features” und können für einen kleinen Betrag freigeschaltet werden.</p><h2 id="zusammenfassung">Zusammenfassung</h2><ul><li><p><strong>Einfach anpassen</strong>
Wähle aus vielfältigen Themen. Füge eigene Fragen hinzu. Importiere sie, um Zeit zu sparen.</p></li><li><p><strong>Lehrreich und unterhaltsam</strong>
Ideal für Lehrkräfte und Lernende. Mit druckfreundlichen Exportgrößen.</p></li><li><p><strong>Mehrsprachig und offline</strong>
Verfügbar in 7 Sprachen – erstellen und spielen auch ohne Internetverbindung.</p></li></ul><h2 id="herunterladen-und-entdecken">Herunterladen und entdecken</h2><p>CrossCraft ist verfügbar auf iPhone, iPad und Apple-Silicon-Macs. Eine native Mac-App für alle Macs ist fast fertig und folgt in Kürze.</p><p>Hol dir die App jetzt:</p><p><a href="https://apps.apple.com/app/apple-store/id6472669260?pt=549314&ct=fline.dev&mt=8">‎CrossCraft: Custom Crosswords</a></p><p>Für Journalisten und Blogger ist ein PressKit <a href="https://github.com/FlineDev/CrossCraft-LandingPage/raw/main/downloads/PressKit-v1.0.zip">hier</a> verfügbar.</p>]]></content:encoded>
</item>
<item>
<title>Erkenntnisse aus der Analyse von 20 erfolgreichen mobilen Paywalls</title>
<link>https://fline.dev/de/blog/freemiumkit-learnings-from-analyzing-mobile-paywalls/</link>
<guid isPermaLink="true">https://fline.dev/de/blog/freemiumkit-learnings-from-analyzing-mobile-paywalls/</guid>
<pubDate>Sun, 10 Sep 2023 00:00:00 +0000</pubDate>
<description><![CDATA[Erfahre, wie FreemiumKit, eine benutzerfreundliche Open-Source-Paywall-Bibliothek, das Erstellen erfolgreicher Paywalls vereinfacht und A/B-Tests optimiert. Die hochgradig anpassbaren UI-Komponenten basieren auf meiner tiefgehenden Analyse gängiger Paywall-Designs.]]></description>
<content:encoded><![CDATA[<p>Nachdem ich RevenueCat ausprobiert und (<a href="https://twitter.com/jeehut/status/1658394554037424128?s=61&t=JK1yW_OmTUCtNsXRmpB8FA">öffentlich</a>) festgestellt hatte, dass es meine (zugegebenermaßen sehr hohen) Erwartungen an die Benutzerfreundlichkeit nicht erfüllt, beschloss ich, das Problem selbst anzugehen und begann mit der Arbeit an der Open-Source-Bibliothek <a href="https://github.com/FlineDev/FreemiumKit">FreemiumKit</a>. Für mich bedeutet „Benutzerfreundlichkeit” bei einem Framework, das bei In-App-Käufen hilft, drei Dinge:</p><ol><li><p>Eine klare Schritt-für-Schritt-Anleitung zum Einstieg – einschließlich App Store Connect.</p></li><li><p>Ein einfaches, aber flexibles Berechtigungssystem basierend auf den Käufen des aktuellen Nutzers.</p></li><li><p>Ein einheitliches Paywall-Design-Framework für wiederverwendbaren, gemeinsam genutzten Paywall-UI-Code.</p></li></ol><p>Die ersten beiden plane ich in der README der Bibliothek abzudecken. Dieser Artikel konzentriert sich auf den dritten Aspekt: Wie hilft dir FreemiumKit, schnell eine erfolgreiche Paywall zu bauen? Und wie kann es dir bei A/B-Tests helfen?</p><p><img src="/assets/images/blog/freemiumkit-learnings-from-analyzing-mobile-paywalls/freemiumkit-makes.webp" alt="FreemiumKit macht die StoreKit-2-Integration kinderleicht. Mit eingebauten Paywalls &amp; Berechtigungen." loading="lazy" /></p><p><em>FreemiumKit macht die StoreKit-2-Integration kinderleicht. Mit eingebauten Paywalls &amp; Berechtigungen.</em></p><h2 id="grundidee">Grundidee</h2><p>Eines der Dinge, die ich von RevenueCat erwartet hatte – einfach weil die Leute immer sooo positiv darüber gesprochen haben – war, dass sie fertige UI-Komponenten in ihrem Swift SDK mitliefern. Tun sie aber nicht. (UPDATE: Nach meinen Beschwerden haben sie damit <a href="https://twitter.com/RevenueCat/status/1697253094520967183">angefangen</a>!) Stattdessen integrieren sie sich mit einem anderen Dienst, der sich auf Paywalls spezialisiert hat und Nutzern hilft, ihre Paywalls durch Ausprobieren verschiedener Designs zu optimieren: <a href="https://superwall.com/">Superwall</a>. Falls du übrigens nicht weißt, was A/B-Tests sind – verschiedene Designs auszuprobieren und die Daten auszuwerten, um zu sehen, was am besten funktioniert, ist so ziemlich die Definition davon. Eine Paywall ist wahrscheinlich der wichtigste Screen, den man A/B-testen sollte. Die Idee des Dienstes gefällt mir also sehr, aber das Pricing skaliert nicht so gut nach unten: Sie verlangen 0,20 $ pro Kauf, was bei einem monatlichen Abo von 1 $ ganze 20 % deiner Einnahmen sind – das ist sogar höher als die 15 %, die Apple bei Small Businesses einbehält!</p><p>Außerdem bin ich persönlich kein Fan davon, viele Drittanbieter-Dienste zu nutzen. Ich halte meine App-Datenschutzerklärungen gerne kurz und einfach, was mit jedem neuen Dienst schwieriger wird. Daher bevorzuge ich es, einen eher „universellen” Analytics-Dienst zu wählen, der unter anderem A/B-Testing unterstützt, und diesen sorgfältig auszuwählen, statt viele Microservices zu integrieren. Aber das ist natürlich nur mein persönlicher Geschmack und du kannst dich anders entscheiden. Eine Sache gibt es allerdings, die ich an Superwall wirklich liebe: Sie betreiben die Website <a href="https://www.paywallscreens.com/">PaywallScreens</a>, auf der du eine schöne Übersicht von Tausenden realer Paywalls findest, sortiert nach dem geschätzten Umsatz jeder App – aktuell angeführt von YouTube, TikTok und Disney+.</p><hr /><p>Beim ersten Release von FreemiumKit wollte ich alles mitliefern, was man braucht, um schnell eine erfolgreiche Paywall-UI zu bauen. Also scrollte ich durch alle 312 Paywall-Screens von Apps mit einem geschätzten Umsatz von über 500.000 $ pro Monat, wählte die 20 Screens aus, die ich am einladendsten und aufgeräumtesten fand, und analysierte sie, um Gemeinsamkeiten zu finden. Basierend auf meinen Erkenntnissen entwickelte ich dann 2 verschiedene und hochgradig anpassbare UI-Komponenten, mit denen du die meisten der analysierten Varianten erstellen kannst!</p><p>Hier sind die 20 ausgewählten Screens in absteigender Umsatzreihenfolge:</p><p><img src="/assets/images/blog/freemiumkit-learnings-from-analyzing-mobile-paywalls/paywallscreens-com.webp" alt="Quelle: paywallscreens.com" loading="lazy" /></p><p><em>Quelle: <a href="https://www.paywallscreens.com/">paywallscreens.com</a></em></p><p><img src="/assets/images/blog/freemiumkit-learnings-from-analyzing-mobile-paywalls/paywallscreens-com-2.webp" alt="Quelle: paywallscreens.com" loading="lazy" /></p><p><em>Quelle: <a href="https://www.paywallscreens.com/">paywallscreens.com</a></em></p><p><img src="/assets/images/blog/freemiumkit-learnings-from-analyzing-mobile-paywalls/paywallscreens-com-3.webp" alt="Quelle: paywallscreens.com" loading="lazy" /></p><p><em>Quelle: <a href="https://www.paywallscreens.com/">paywallscreens.com</a></em></p><p><img src="/assets/images/blog/freemiumkit-learnings-from-analyzing-mobile-paywalls/paywallscreens-com-4.webp" alt="Quelle: paywallscreens.com" loading="lazy" /></p><p><em>Quelle: <a href="https://www.paywallscreens.com/">paywallscreens.com</a></em></p><h2 id="häufige-design-entscheidungen">Häufige Design-Entscheidungen</h2><p>Alles, was mir aufgefallen ist und was mindestens 50 % (einer Art) gemeinsam haben:</p><ol><li><p>100 % bieten entweder einen monatlichen oder wöchentlichen „Kurzzeit”-Plan an.</p></li><li><p>100 % bieten entweder einen jährlichen oder lebenslangen „Langzeit”-Plan an.</p></li><li><p>100 % verwenden schlichten weißen oder schwarzen Text für die Pläne oder die Liste freigeschalteter Features.</p></li><li><p>95 % füllen den gesamten Bildschirm ohne zusammenhanglose Navigationselemente.</p></li><li><p>95 % bringen alle wichtigen Informationen auf einen Bildschirm, ohne dass gescrollt werden muss.</p></li><li><p>90 % verwenden eine Hintergrundfarbe, um ihren Haupt-Call-to-Action-Button hervorzuheben.</p></li><li><p>85 % haben die Preispläne in der unteren Bildschirmhälfte aufgelistet.</p></li><li><p>80 % verwenden einen abgerundeten Rahmen, um die aktuell ausgewählte Option hervorzuheben.</p></li><li><p>80 % haben entweder einen Zurück-Pfeil oder einen „X”-Button oben zum Schließen.</p></li><li><p>61 % der „X”-Buttons befinden sich in der linken Ecke (schwerer erreichbar für die meisten).</p></li><li><p>80 % haben einen deutlich hervorgehobenen Call-to-Action-Button ganz unten.</p></li><li><p>75 % der Call-to-Action-Buttons sind kapselförmig (Enden zu 100 % abgerundet).</p></li><li><p>56 % der Call-to-Action-Buttons verwenden exakt denselben Titel: „Continue”</p></li><li><p>70 % verwenden eine vertikale Liste von Buttons für die verschiedenen Planoptionen.</p></li><li><p>70 % derjenigen mit Vorauswahl entscheiden sich für die langfristige wiederkehrende Option.</p></li><li><p>60 % verwenden einen weißen Hintergrund hinter der Planliste zur Auswahl.</p></li><li><p>55 % erwähnen den App-Namen irgendwo in der oberen Hälfte.</p></li><li><p>65 % bieten entweder eine Liste, ein Grid oder eine Seitenansicht mit 2–6 (durchschn. 4) freigeschalteten Features.</p></li><li><p>77 % derjenigen mit Liste verwenden ein Häkchen-Symbol vor jedem Feature.</p></li><li><p>55 % verwenden ein Hintergrundbild, das sich bis zum oberen Bildschirmrand erstreckt.</p></li><li><p>50 % bieten eine kostenlose Testversion für mindestens einen bezahlten Plan an.</p></li><li><p>50 % erwähnen einen Rabattprozentsatz im Vergleich zu anderen Optionen.</p></li></ol><h2 id="weniger-häufige-design-entscheidungen">Weniger häufige Design-Entscheidungen</h2><p>Dinge, die mir bei einigen Paywalls aufgefallen sind, die aber die Mehrheit nicht macht:</p><ol><li><p>45 % verwenden eine andere Hintergrundfarbe für den aktuell ausgewählten Plan.</p></li><li><p>35 % haben ein „Beliebt”- / „Empfohlen”-Tag bei einer der Kaufoptionen.</p></li><li><p>30 % haben einen Radio-Button wie einen Punkt oder eine Checkbox, um die Auswahl hervorzuheben.</p></li><li><p>30 % verwenden eine horizontale Liste von Buttons für die verschiedenen Planoptionen.</p></li><li><p>30 % bieten einen (weniger hervorgehobenen) „Wiederherstellen”-Button innerhalb der Paywall.</p></li><li><p>30 % bieten einen (weniger hervorgehobenen) Link zu ihren Nutzungsbedingungen und ihrer Datenschutzerklärung.</p></li><li><p>15 % verwenden eine Seitenansicht, um die durch den Kauf freigeschalteten Features zu präsentieren.</p></li><li><p>10 % bieten ein Segmented Control zum Wechseln zwischen Monatlich/Jährlich usw.</p></li><li><p>5 % haben eine Animation auf ihrem Call-to-Action-Button (oder anderswo).</p></li></ol><h2 id="der-paywall-bauplan">Der Paywall-Bauplan</h2><p>Mit den obigen Erkenntnissen habe ich einen „Bauplan” erstellt, der sie alle einbezieht:</p><p><img src="/assets/images/blog/freemiumkit-learnings-from-analyzing-mobile-paywalls/this-is-the-only-part-in-2.webp" alt="Das ist der einzige Teil im Paywall-Bauplan mit komplexer Logik." loading="lazy" /></p><p>Ich bin kein professioneller Designer, aber ich denke, es ist gut genug für das erste öffentliche Release von FreemiumKit. Bessere Designer als ich in der Community sind eingeladen, (ausgefallene) Verbesserungen oder ganz andere Designs beizusteuern. Die README hat einen <a href="https://github.com/FlineDev/FreemiumKit#implementing-a-custom-ui">eigenen Abschnitt</a>, der beschreibt, wie man eigene Designs baut. Wenn man sich anschaut, wie dieser Screen insgesamt aussieht und die 20 Paywall-Designs betrachtet, die ich geprüft habe, denke ich, es ist klug für das Framework, sich auf den Teil mit der meisten Logik zu konzentrieren. Die gesamte obere Hälfte des Screens ist in SwiftUI sehr einfach umzusetzen (nur ein <code>Image</code> mit einem <code>.overlay</code>, ein <code>Button</code> und eine <code>List</code> von <code>Text</code>-Views). Und dasselbe gilt für die weniger hervorgehobenen „Nutzungsbedingungen”-, „Wiederherstellen”- und „Datenschutzerklärung”-Buttons ganz unten.</p><p><img src="/assets/images/blog/freemiumkit-learnings-from-analyzing-mobile-paywalls/this-is-the-only-part-in.webp" alt="Das ist der einzige Teil im Paywall-Bauplan mit komplexer Logik." loading="lazy" /></p><p><em>Das ist der einzige Teil im Paywall-Bauplan mit komplexer Logik.</em></p><p>Konzentrieren wir uns also auf den Teil, der die verfügbaren Produkte lädt und auflistet, die aktuelle Auswahl hervorhebt, potenziell verfügbare Testzeiträume anzeigt, Langzeitplan-Rabatte, das Preisschild, ein zweites Monatspreisschild für besseren Vergleich, ein „Bester Wert”- oder „Beliebteste”-Badge, die aktuelle Planauswahl handhabt und die Logik für die Anzeige &amp; Deaktivierung des „Weiter”-Buttons bei Bedarf. Wie du sehen kannst, ist dieser Teil ziemlich komplex, um ihn richtig hinzubekommen, und weil (fast) all diese Informationen der App tatsächlich von <code>StoreKit</code> bereitgestellt werden, ist es eine großartige Gelegenheit, die Dinge zu vereinfachen. Indem der Rest des Screens dem Entwickler/Designer überlassen wird – mit all den Erkenntnissen und dem Bauplan von oben – sollten Nutzer der Bibliothek volle Freiheit &amp; Flexibilität haben, ein einzigartiges Aussehen für die Paywall ihrer App zu gestalten.</p><p>Um die Sache etwas spaßiger zu machen und A/B-Tests schon im ersten Release von FreemiumKit zu ermöglichen, erstellen wir auch eine horizontale Listenvariante wie diese:</p><p><img src="/assets/images/blog/freemiumkit-learnings-from-analyzing-mobile-paywalls/the-vertical-left-and.webp" alt="Die vertikale (links) und horizontale (rechts) Paywall-Baupläne nebeneinander." loading="lazy" /></p><p>Beide nebeneinander in ihrer Vollbild-Ausdehnung sehen so aus:</p><p><img src="/assets/images/blog/freemiumkit-learnings-from-analyzing-mobile-paywalls/the-vertical-left-and-2.webp" alt="Die vertikale (links) und horizontale (rechts) Paywall-Baupläne nebeneinander." loading="lazy" /></p><p><em>Die vertikale (links) und horizontale (rechts) Paywall-Baupläne nebeneinander.</em></p><h2 id="vertikaler-horizontaler-produkt-stil">Vertikaler &amp; Horizontaler Produkt-Stil</h2><p>FreemiumKit liefert eine SwiftUI-View namens <code>AsyncProducts</code>, die ganz ähnlich wie <code>AsyncImage</code> funktioniert: Du gibst die Produkt-IDs an, die du in deiner Paywall anzeigen willst (wie du <code>AsyncImage</code> eine URL deines Bildes gibst), und <code>AsyncProducts</code> kümmert sich um das Abrufen der Produkte aus dem App Store (wie <code>AsyncImage</code> die Bilder aus dem Web abruft), zeigt einen Platzhalter während des Ladens an und zeigt sogar eine Fehlermeldung mit einem Neu-laden-Button an, falls es Netzwerkprobleme gibt. Mit anderen Worten: Es erledigt die ganze schwere Arbeit – du musst nur entscheiden, wo auf deiner Paywall du es platzieren willst und welche Größe am besten passt:</p><pre><code class="language-Swift">AsyncProducts(
   style: PlainProductsStyle(),
   productIDs: ProductID.allCases,
   inAppPurchase: AppDelegate.inAppPurchase
)</code></pre><p><em>Beispielverwendung von <code>AsyncProducts</code>.</em></p><p>Die Parameter <code>productIDs</code> &amp; <code>inAppPurchase</code> werden in der Schritt-für-Schritt-Anleitung <a href="https://github.com/FlineDev/FreemiumKit#getting-started">Getting Started</a> in der README erklärt. Was uns in diesem Artikel interessiert, ist der <code>style</code>-Parameter, der es dir ermöglicht, verschiedene Designs für die UI-Komponente zu übergeben. Der <code>PlainProductsStyle</code> ist nur ein Demo-Stil, der die minimale Implementierung eines Stils zeigt, für diejenigen, die eigene Stile erstellen wollen. Er ist nicht für die direkte Verwendung gedacht. Die zwei echten Stile, die FreemiumKit in <code>1.0</code> mitliefert, sind:</p><ul><li><p><code>VerticalPickerProductsStyle</code> für das linke Design von oben</p></li><li><p><code>HorizontalPickerProductsStyle</code> für das rechte Design von oben (in Arbeit)</p></li></ul><p>Außerdem plane ich, mindestens einen weiteren Stil hinzuzufügen, der etwa <code>HorizontalButtonsProductStyle</code> heißen könnte und ein UI ohne „Weiter”-Button implementiert, wie es in der Paywall von Apples neuester App <a href="https://apps.apple.com/de/app/final-cut-pro-f%C3%BCr-das-ipad/id1631624924">Final Cut Pro für iPad</a> verwendet wird:</p><p><img src="/assets/images/blog/freemiumkit-learnings-from-analyzing-mobile-paywalls/paywall-in-apple-s-new.webp" alt="Paywall in Apples neuer Final-Cut-Pro-App für iPad." loading="lazy" /></p><p><em>Paywall in Apples neuer Final-Cut-Pro-App für iPad.</em></p><p>Aber konzentrieren wir uns auf die heute verfügbaren Stile und verwenden den vertikalen Picker:</p><pre><code class="language-Swift">AsyncProducts(
   style: VerticalPickerProductsStyle(
      preselectedProductID: ProductID.proYearly,
      tintColor: .blue
   ),
   ...
)</code></pre><p>Er nimmt zwei Argumente entgegen: Eines, mit dem du das Produkt angeben kannst, das als Vorauswahl hervorgehoben werden soll. Erkenntnis Nr. 15 aus der obigen Analyse sagt uns, dass 70 % die langfristige wiederkehrende Option vorauswählen. Das andere Argument lässt dich die Akzentfarbe wählen, die sowohl für den „Weiter”-Button-Hintergrund als auch zur Hervorhebung der aktuellen Auswahl des Nutzers verwendet wird. Achte darauf, eine Farbe zu wählen, die gut mit Weiß kontrastiert, denn das ist die Textfarbe des Weiter-Buttons.</p><p>Wenn du verschiedene Paywall-Designs testen willst, ist es sehr einfach, den Stil für A/B-Tests auszutauschen: Ersetze einfach <code>VerticalPickerProductsStyle</code> durch <code>HorizontalPickerProductsStyle</code> und alles sollte einfach funktionieren. Diese beiden Stile nehmen sogar dieselben Argumente entgegen, es ist also wirklich einfach. Andere Stile wie der geplante <code>HorizontalButtonsProductsStyle</code> werden andere Argumente haben, da es bei einem reinen Button-Stil ohne Auswahl kein Konzept von „Selection” gibt – aber es sollte trotzdem einfach genug sein, das zu ändern. Alle Stile konformieren zum <code>AsyncProductsStyle</code>-Protokoll. Wenn du also eine Variable erstellen willst, der du je nach A/B-Test-Gruppe verschiedene Stile zuweisen kannst, kannst du <code>any AsyncProductsStyle</code> als Typ verwenden.</p><h2 id="fazit">Fazit</h2><p>Das waren meine Erkenntnisse aus der Analyse von 20 erfolgreichen Paywalls. Ich habe alle Erkenntnisse in den <code>VerticalPickerProductsStyle</code> eingebaut, der in FreemiumKit mitgeliefert wird. So kannst du einfach die <code>AsyncProducts</code>-View in SwiftUI nutzen, statt dich mit komplexer Logik herumzuschlagen. Du musst dir nur merken, die weniger komplexen Erkenntnisse anzuwenden – wie <code>AsyncProducts</code> in der unteren Bildschirmhälfte zu platzieren, sowohl ein Kurzzeit- als auch ein Langzeit-Abo anzubieten oder eine Liste der freigeschalteten Features in der oberen Hälfte bereitzustellen.</p>]]></content:encoded>
</item>
<item>
<title>Die fehlende String-Catalogs-FAQ für Lokalisierung in Xcode 15</title>
<link>https://fline.dev/de/blog/the-missing-string-catalogs-faq-for-xcode-15/</link>
<guid isPermaLink="true">https://fline.dev/de/blog/the-missing-string-catalogs-faq-for-xcode-15/</guid>
<pubDate>Tue, 04 Jul 2023 00:00:00 +0000</pubDate>
<description><![CDATA[Entdecke die bahnbrechenden Auswirkungen von Apples neuem Feature String Catalogs, das traditionelle Lokalisierungsdateien ersetzt und den Lokalisierungsprozess deutlich vereinfacht. Von automatischer Key-Extraktion bis zu Sicherheitsprüfungen – erfahre, warum Entwickler sich über dieses mächtige Tool in Xcode 15 freuen sollten.]]></description>
<content:encoded><![CDATA[<p>Auf der WWDC23 hat Apple ein neues Feature für Xcode vorgestellt, das große Teile meiner <a href="https://remafox.app/">RemafoX</a>-App gesherlocked hat. Natürlich habe ich das neue Feature ausführlich getestet und all meine Fragen an das Team gestellt, das es gebaut hat – in der zugehörigen Slack-Aktivität. Sie haben mit String Catalogs wirklich gute Arbeit geleistet, so gut sogar, dass ich meine App komplett neu denken werde, da sie den Großteil der Low-Level-Arbeit übernommen haben, die ich bisher in meiner App erledigt habe.</p><p>Aber die meisten Leute, mit denen ich seit der Dub Dub gesprochen habe, sind sich der Auswirkungen von String Catalogs auf ihre Projekte noch gar nicht bewusst. Also dachte ich, ich sollte die häufigsten Fragen beantworten, um klarer zu machen, wie großartig String Catalogs wirklich sind.</p><h3 id="was-sind-string-catalogs-was-ist-mit-stringsdict-dateien">Was sind String Catalogs? Was ist mit Strings(dict)-Dateien?</h3><p>String Catalogs sind Dateien mit der Endung <code>.xcstrings</code>, die ihren Inhalt in einem eigenen JSON-Format speichern. Das genaue Format ist nicht dokumentiert (Feedback eingereicht, um das zu ändern: FB12264877), und als Entwickler wirst du nie mit JSON-Code herumhantieren müssen, denn Xcode 15 liefert einen visuellen Editor mit:</p><p><img src="/assets/images/blog/the-missing-string-catalogs-faq-for-xcode-15/the-lack-of-a-green.webp" alt="Das Fehlen eines grünen Häkchens bei der deutschen Sprache zeigt an, dass die Übersetzung unvollständig ist." loading="lazy" /></p><p>String Catalogs ersetzen sowohl <code>.strings</code>- als auch <code>.stringsdict</code>-Dateien und unterstützen daher Pluralisierung von Haus aus. Anders als <code>.strings(dict)</code>-Dateien, die in sprachspezifischen Ordnern wie <code>en.lproj</code> abgelegt werden, kapseln String Catalogs die Übersetzungen aller unterstützten Sprachen in einer einzigen Datei. Das ermöglicht Sicherheitsprüfungen wie die Anzeige des Übersetzungsfortschritts direkt in Xcode, damit du über fehlende Übersetzungen informiert bist (ersetzt den RemafoX-Linter). Ebenso werden neue Keys automatisch für alle Sprachen hinzugefügt, was dir viel Zeit spart (ersetzt den RemafoX-Normalizer). Und in zukünftigen Updates könnten weitere Features folgen, wie eine Prüfung, ob deine Lokalisierungen die gleichen Parameter (z. B. <code>%@</code>) wie die Ausgangssprache haben (ein Feature, das ich für RemafoX geplant hatte, Feedback eingereicht: FB12264614).</p><h3 id="ich-habe-ein-projekt-mit-stringsdict-dateien-ist-die-migration-einfach">Ich habe ein Projekt mit Strings(dict)-Dateien. Ist die Migration einfach?</h3><p>Ja, absolut! Die Migration zu String Catalogs ist kinderleicht: Klicke einfach mit der rechten Maustaste auf eine deiner <code>.strings(dict)</code>-Dateien und wähle “Migrate to String Catalog…”. Das öffnet ein Modal mit einer Liste all deiner <code>.strings(dict)</code>-Dateien im Projekt, aus denen du auswählen kannst, welche zu einem String Catalog zusammengefasst werden sollen. Wenn du separate Strings-Dateien für verschiedene Teile deines Projekts hast, kannst du auch separate String Catalogs behalten – es ist nicht nötig, alles in einen String Catalog zu packen.</p><h3 id="aber-mein-projekt-unterstützt-ältere-os-versionen-kann-ich-trotzdem-migrieren">Aber mein Projekt unterstützt ältere OS-Versionen. Kann ich trotzdem migrieren?</h3><p>Ja, absolut! Die Apple-Ingenieure haben hier etwas sehr Cleveres gemacht: Während wir als Entwickler alle Features von String Catalogs voll nutzen können, bekommt unsere App kein neues Dateiformat wie <code>.xcstrings</code> zu sehen. Stattdessen konvertiert Xcode den String Catalog während des Build-Prozesses zurück in die guten alten <code>.strings</code>- und <code>.stringsdict</code>-Dateien, was die Unterstützung aller OS-Versionen sicherstellt, die du potenziell anvisieren kannst. Das bedeutet: Sobald du für dein Projekt auf Xcode 15 wechseln kannst, kannst du String Catalogs uneingeschränkt nutzen, ohne jemals zurückblicken zu müssen!</p><blockquote><p>💁‍♂️ Wenn du dir schon mal eine App wie SF Symbols gewünscht hast, aber für lokalisierte Strings im Code mit offiziellen Übersetzungen für alle Sprachen, in denen iOS verfügbar ist: Genau das habe ich gebaut, und das Feature ist komplett kostenlos!
Lade einfach <a href="https://translatekit.app/">TranslateKit</a> herunter und schau in deine Menüleiste. 🌐 👍</p></blockquote><h3 id="ich-nutze-swiftgenremafox-für-sichere-lokalisierungs-key-referenzen-mit-compiler-checks-verliere-ich-diese-sicherheit">Ich nutze SwiftGen/RemafoX für sichere Lokalisierungs-Key-Referenzen mit Compiler-Checks. Verliere ich diese Sicherheit?</h3><p>Nein, überhaupt nicht. Xcode führt nicht nur ein neues Dateiformat für deine Lokalisierungen ein, sondern bringt auch Tools mit, die automatisch neue Lokalisierungs-Keys aus deinem Projekt extrahieren. Wenn du eine von SwiftUIs Lokalisierungs-APIs mit <code>LocalizedStringKey</code> oder <code>LocalizedStringResource</code> verwendest, werden sie automatisch erkannt und deinem String Catalog hinzugefügt. Aber das beschränkt sich nicht auf SwiftUI – es funktioniert auch mit reinen Swift-String-APIs wie <code>String(localized:)</code>, klassischen Obj-C-Style-APIs wie <code>NSLocalizedString()</code> (sogar mit eigenen Namen) und sogar mit Interface-Builder-Dateien wie <code>.storyboard</code> und <code>.xib</code>. Für <code>Info.plist</code>-Dateien musst du einen eigenen String Catalog namens <code>InfoPlist.xcstrings</code> erstellen, und die Extraktion funktioniert auch dort automatisch.</p><p>Beachte, dass du keine Auto-Completion für deine Keys im Code bekommst, wie du sie jetzt für Bilder und Farben in deinen Asset Catalogs mit Xcode 15 erhältst. Das liegt daran, dass bei Asset Catalogs die Source of Truth im Catalog selbst liegt, wo deine Assets platziert werden. Weil Xcode aber automatisch alle hinzugefügten Lokalisierungen aus deinem Quellcode extrahiert, ist die Source of Truth für deine Lokalisierungen umgekehrt und liegt in deinem Code. Daher ist es unmöglich, einen Text in deinem Code hinzuzufügen, für den es kein Gegenstück in einer Strings-Datei gibt (wie es vorher möglich war und zu defekten Übersetzungen führte). Stattdessen erstellt Xcode jedes Mal, wenn du einen lokalisierten String in deinem Projekt hinzufügst, automatisch einen neuen Übersetzungs-Key für alle unterstützten Sprachen, und du siehst im String Catalog, dass deine Übersetzungen unvollständig sind.</p><p><img src="/assets/images/blog/the-missing-string-catalogs-faq-for-xcode-15/the-lack-of-a-green-2.webp" alt="Das Fehlen eines grünen Häkchens bei der deutschen Sprache zeigt an, dass die Übersetzung unvollständig ist." loading="lazy" /></p><p><em>Das Fehlen eines grünen Häkchens bei der deutschen Sprache zeigt an, dass die Übersetzung unvollständig ist.</em></p><h3 id="was-ist-mit-tippfehlern-kann-ich-den-key-während-der-entwicklung-ändern">Was ist mit Tippfehlern? Kann ich den Key während der Entwicklung ändern?</h3><p>Xcode extrahiert nicht nur neue Keys und fügt sie automatisch deinem String Catalog hinzu, sondern stellt auch sicher, dass Keys in deinem Catalog, die nicht mehr in deinem Projekt referenziert werden, gelöscht werden. Das geschieht auf sichere Weise: Wenn du einen Key hast, für den du noch keine Übersetzungen in irgendeiner Sprache bereitgestellt hast, wird er automatisch gelöscht, sobald er nicht mehr in deinem Projekt gefunden wird. Das passiert typischerweise, wenn du einen Tippfehler in deinem Key hast oder den Key während der Entwicklung einfach verbessern und ändern möchtest. Aber wenn du bereits einige Übersetzungen hast, markiert Xcode den Key im String Catalog stattdessen als “stale” mit einem gelben Warnsymbol. So lassen sich Keys, die nicht mehr referenziert werden, leicht erkennen, ihre Übersetzungen bei Bedarf zum neuen Key kopieren und danach löschen.</p><p><img src="/assets/images/blog/the-missing-string-catalogs-faq-for-xcode-15/what-about-typos-can-i.webp" alt="What about typos can i" loading="lazy" /></p><h3 id="was-wenn-ich-einen-bestimmten-text-in-codeib-dateien-nicht-lokalisieren-möchte">Was, wenn ich einen bestimmten Text in Code/IB-Dateien nicht lokalisieren möchte?</h3><p>Derzeit scheint es keinen direkten Weg zu geben, die Extraktion aus der Source of Truth zu steuern. Ich habe ein Feedback speziell für IB-Dateien eingereicht (FB12264777), weil meine Tools RemafoX (und sein Vorgänger <a href="https://github.com/FlineDev/BartyCrouch">BartyCrouch</a>) dort das Ausschließen unterstützen – der Wechsel zu String Catalogs wäre daher für Nutzer dieser Tools nur möglich, wenn Apple eine Lösung anbieten würde. Einen expliziten Weg, einen String im Code als “nicht übersetzbar” zu markieren, konnte ich auch nicht finden, aber in den meisten SwiftUI-Views findest du eine Überladung der gleichen API, die statt eines Strings ein <code>Text</code>-View entgegennimmt. Für Views, bei denen das zutrifft, kannst du <code>Text(verbatim: &quot;Your Text&quot;)</code> verwenden, um ein <code>Text</code>-View zu erstellen, dessen Text nicht extrahiert wird, weil das Schlüsselwort <code>verbatim</code> ihn als nicht übersetzbar kennzeichnet. Da aber nicht immer eine Überladung mit <code>Text</code> existiert, habe ich ein Feedback für einen direkteren Weg zur Steuerung der Extraktion eingereicht (FB12469163). Vorläufig habe ich einen Workaround gefunden, indem ich einfach einen String-Initializer in SwiftUI-APIs verwende, die die LocalizedStringKey-Überladung bevorzugen – übergib einfach etwas wie <code>String(&quot;Your Text&quot;)</code>.</p><h2 id="fazit">Fazit</h2><p>Die Einführung von String Catalogs in Xcode 15 bringt erhebliche Verbesserungen für den Lokalisierungs-Workflow, indem sie die Verwaltung von Übersetzungsdateien vereinfacht. Entwickler können ihre Projekte einfach zu String Catalogs migrieren und die Sicherheit von Lokalisierungs-Key-Referenzen beibehalten, während sie weiterhin ältere OS-Versionen unterstützen und Kontrolle über Key-Änderungen und Tippfehler haben. Obwohl es einige Einschränkungen und Verbesserungsmöglichkeiten gibt, sind String Catalogs ein riesiger Schritt nach vorne und haben gegenüber dem alten Strings-Dateisystem keine echten Nachteile. Jetzt migrieren!</p>]]></content:encoded>
</item>
<item>
<title>ReviewKit: Verbessere dein App-Store-Rating ganz einfach</title>
<link>https://fline.dev/de/blog/introducing-reviewkit/</link>
<guid isPermaLink="true">https://fline.dev/de/blog/introducing-reviewkit/</guid>
<pubDate>Thu, 22 Jun 2023 00:00:00 +0000</pubDate>
<description><![CDATA[ReviewKit: Erhalte App-Bewertungen von zufriedenen Nutzern zum richtigen Zeitpunkt. Schluss mit aufdringlichen Aufforderungen – optimiere deinen Review-Prozess.]]></description>
<content:encoded><![CDATA[<p>Als App-Entwickler weißt du, wie wichtig Nutzerbewertungen für den Erfolg und die Glaubwürdigkeit deiner App sind. Positive Bewertungen ziehen nicht nur mehr Nutzer an, sondern tragen auch zu besseren Rankings im App Store bei. Allerdings kann es zu Frustration und negativem Feedback führen, wenn man Nutzer zum falschen Zeitpunkt oder nach einer schlechten Erfahrung um eine Bewertung bittet. Genau hier kommt ReviewKit ins Spiel – ein leistungsstarkes Open-Source-Tool, das App-Reviews nur von Nutzern anfragt, die kürzlich positive Aktivitäten gezeigt haben, und das auch nur zu passenden Zeitpunkten.</p><h2 id="das-problem-und-die-lösung">Das Problem und die Lösung</h2><p>Traditionell haben App-Entwickler einfache Aufforderungen verwendet, um Nutzer um Bewertungen zu bitten – oft direkt nach dem Öffnen der App oder in zufälligen Abständen. Dieser Ansatz kann aufdringlich und nervig sein, was dazu führt, dass Nutzer negative Bewertungen hinterlassen oder die App sogar deinstallieren.</p><p>Ich hatte eine ziemlich komplexe Logik entwickelt, um den richtigen Zeitpunkt für Review-Anfragen in meiner ersten App <a href="https://remafox.app/">RemafoX</a> zu bestimmen. Für meine neueste App (<a href="https://twoot-it.app/">Twoot it!</a>) habe ich dann auf das Wesentliche reduziert und die Logik noch weiter vereinfacht. ReviewKit ist das Ergebnis dieses Prozesses.</p><p><img src="/assets/images/blog/introducing-reviewkit/sample-usage-of.webp" alt="Beispielhafte Nutzung von ReviewKit in der Twoot it! App." loading="lazy" /></p><p>ReviewKit löst dieses Problem, indem es intelligent entscheidet, wann App-Reviews basierend auf der jüngsten positiven Aktivität des Nutzers angefragt werden. Es stellt sicher, dass nur Nutzer, die eine zufriedenstellende Erfahrung mit deiner App gemacht und bestimmte Aktivitäten durchgeführt haben, zur Abgabe einer Bewertung aufgefordert werden. Dadurch erhöht ReviewKit die Wahrscheinlichkeit positiver Bewertungen und minimiert gleichzeitig die Belästigung der Nutzer.</p><p>Natürlich ist jede App anders, daher musst du selbst festlegen, was eine “positive Aktivität” in deiner App bedeutet. Aber um den Rest kümmert sich ReviewKit – und es ist auch anpassbar!</p><h2 id="reviewkit-einrichten">ReviewKit einrichten</h2><p><img src="/assets/images/blog/introducing-reviewkit/reviewkit-logo.webp" alt="ReviewKit Logo" loading="lazy" /></p><p>Der Einstieg mit ReviewKit ist einfach. Folge den Anweisungen unten, um es in deine iOS- oder macOS-App zu integrieren – das Deployment Target kann bis iOS 11 oder macOS 10.14 zurückreichen, was selbst die meisten Unternehmens-Apps abdecken sollte:</p><h3 id="schritt-1-reviewkit-zu-deiner-app-hinzufügen">Schritt 1: ReviewKit zu deiner App hinzufügen</h3><p>Um ReviewKit zu deinem Projekt hinzuzufügen, verwende den Swift Package Manager (SPM). Navigiere in Xcode zu deinem Projekt und gehe zum Tab “Swift Packages”. Klicke auf den “+”-Button und gib die ReviewKit-Repository-URL ein:</p><pre><code>https://github.com/FlineDev/ReviewKit.git</code></pre><p>Stelle abschließend sicher, dass du dein App-Target auswählst, um <code>ReviewKit</code> zu verlinken.</p><h3 id="schritt-2-review-kriterien-anpassen-optional">Schritt 2: Review-Kriterien anpassen (optional)</h3><p>ReviewKit bietet Standardkriterien für die Anfrage von App-Reviews: Es erfordert mindestens 3 positive Events innerhalb der letzten 14 Tage. Wenn du diese anpassen möchtest, kannst du die Kriterien über <code>ReviewCriteria</code> wie folgt konfigurieren:</p><pre><code class="language-swift">ReviewKit.criteria = ReviewCriteria(
   minPositiveEventsWeight: 5,
   eventsExpireAfterDays: 30
)</code></pre><p>Im obigen Beispiel wurden die Kriterien so angepasst, dass Reviews erst angefragt werden, wenn Nutzer mindestens 5 positive Events haben, und die Events nach 30 Tagen ablaufen. Das gilt, wenn du das Standard-<code>weight</code> von 1 für die folgenden Aufrufe verwendest.</p><h3 id="schritt-3-positive-events-aufzeichnen-und-review-anfragen">Schritt 3: Positive Events aufzeichnen und Review anfragen</h3><p>Um die App-Review-Anfrage auszulösen, wenn Nutzer bestimmte Workflows oder Aktivitäten abschließen, musst du positive Events mit ReviewKit aufzeichnen. Rufe die folgende Methode auf, wann immer ein Nutzer eine dieser Aktivitäten abschließt:</p><pre><code class="language-swift">ReviewKit.recordPositiveEventAndRequestReviewIfCriteriaMet()</code></pre><p>Diese Methode bestimmt automatisch, ob der Nutzer die Kriterien für eine App-Review-Anfrage basierend auf seiner jüngsten positiven Aktivität erfüllt. Wenn die Kriterien erfüllt sind, wird die Review-Aufforderung angezeigt.</p><h3 id="schritt-4-weitere-positive-aktivitäten-aufzeichnen-optional">Schritt 4: Weitere positive Aktivitäten aufzeichnen (optional)</h3><p>Neben den primären Workflows ist es wichtig, auch zusätzliche Aktivitäten zu berücksichtigen, die auf positive Nutzererfahrungen hindeuten. Allerdings könnte eine Review-Anfrage in diesen Momenten Nutzer unterbrechen, die gerade in einem Workflow sind, was potenziell zu Verärgerung und geringerer Bereitschaft führt, eine Bewertung abzugeben – oder sogar das Rating negativ beeinflusst. Um diese Events zu erfassen, kannst du die Funktion <code>recordPositiveEvent()</code> verwenden:</p><pre><code class="language-swift">// Optional kannst du einen eigenen `weight`-Parameter übergeben (Standard ist 1)
ReviewKit.recordPositiveEvent()</code></pre><p>Beachte, dass beide oben genannten Methoden optional einen <code>weight: Int</code>-Parameter akzeptieren, mit dem du die Kriterien für Review-Anfragen feinjustieren kannst. Wenn deine App zum Beispiel verschiedene Engagement-Stufen hat, könntest du ein Weight von <code>3</code> für die höchste Aktivitätsstufe festlegen und <code>minPositiveEventsWeight</code> auf etwas wie <code>10</code> setzen. Das Standard-<code>weight</code> für positive Events ist <code>1</code>.</p><hr /><h2 id="beispielhafte-verwendung-in-einer-ios-app">Beispielhafte Verwendung in einer iOS-App</h2><p>Zum besseren Verständnis betrachten wir eine Beispiel-iOS-App und zeigen, wie ReviewKit effektiv eingesetzt werden kann. Stell dir vor, du hast eine Social-Media-App, in der Nutzer Beiträge posten und die Beiträge anderer liken können. Hier ein Beispiel:</p><pre><code class="language-swift">import ReviewKit

func sendPost() {
  // ...

  // Positives Event nach dem Senden eines Beitrags aufzeichnen
  ReviewKit.recordPositiveEventAndRequestReviewIfCriteriaMet(weight: 3)
}

func handlePostLike() {
  // ...

  // Positives Event für das Liken eines Beitrags unauffällig aufzeichnen
  ReviewKit.recordPositiveEvent()
}</code></pre><p>Im obigen Beispiel zeigen die Methoden <code>sendPost()</code> und <code>handlePostLike()</code>, wie positive Events für verschiedene Aktivitäten aufgezeichnet werden. Wir rufen <code>recordPositiveEventAndRequestReviewIfCriteriaMet()</code> auf, nachdem ein Beitrag gesendet wurde, da dies das Ende eines Workflows markiert, den der Nutzer in unserer App durchgeführt hat – ein guter Zeitpunkt für eine Review-Anfrage.</p><p>Wenn Nutzer einen Beitrag liken, befinden sie sich noch mitten im Anwendungsfall des Inhalte-Konsumierens, was zwar eine positive Aktivität ist, die wir aufzeichnen sollten, aber es wäre aufdringlich, zu diesem Zeitpunkt nach einer Bewertung zu fragen. Deshalb rufen wir einfach <code>recordPositiveEvent()</code> auf.</p><p><img src="/assets/images/blog/introducing-reviewkit/sample-usage-of.gif" alt="Beispielhafte Nutzung von ReviewKit in der Twoot it! App." loading="lazy" /></p><p><em>Beispielhafte Nutzung von ReviewKit in der <a href="https://twoot-it.app/">Twoot it!</a> App.</em></p><h2 id="fazit">Fazit</h2><p><a href="https://github.com/FlineDev/ReviewKit">ReviewKit</a> bietet eine einfache und doch effektive Lösung, um Nutzer um eine Bewertung deiner App zu bitten. Durch die intelligente Bestimmung des richtigen Zeitpunkts basierend auf jüngster positiver Aktivität erhöht ReviewKit die Chancen auf positive Bewertungen und hilft deiner App beim Wachstum. Mit einfacher Integration und anpassbaren Kriterien ist ReviewKit ein wertvolles Werkzeug für jeden iOS-Entwickler und jedes Team.</p>]]></content:encoded>
</item>
<item>
<title>RemafoX Sale: 50 % Rabatt auf alle Abo-Pläne während der WWDC-Woche!</title>
<link>https://fline.dev/de/blog/remafox-wwdc-sale/</link>
<guid isPermaLink="true">https://fline.dev/de/blog/remafox-wwdc-sale/</guid>
<pubDate>Sun, 04 Jun 2023 00:00:00 +0000</pubDate>
<description><![CDATA[3 neue Features, die jedem Swift-Entwickler Zeit sparen, und reduzierte Preise für alle Abos zur WWDC-Woche – mit einem langfristigen Twist, den du nicht verpassen willst!]]></description>
<content:encoded><![CDATA[<p>Die WWDC steht vor der Tür, und ich freue mich, einen großen Sale auf alle RemafoX-Abo-Pläne ankündigen zu können. Aber das ist noch nicht alles – ich habe auch drei großartige neue Features in RemafoX 1.6 hinzugefügt und sie zur Feier der WWDC kostenlos für alle verfügbar gemacht. Lies weiter, um alle Details zu erfahren und wie du diese tollen Angebote nutzen kannst, um deine Produktivität in Xcode zu steigern.</p><h2 id="großer-sale-50-rabatt-auf-alle-abo-pläne">Großer Sale: 50 % Rabatt auf alle Abo-Pläne!</h2><p>Um die Begeisterung rund um die WWDC noch zu steigern, habe ich mich entschieden, einen zeitlich begrenzten Sale auf alle RemafoX-Abo-Pläne anzubieten. Ab sofort und bis zum Ende der WWDC-Woche gibt es satte 50 % Rabatt auf alle Abo-Preise. Das ist eine seltene Gelegenheit, das volle Potenzial von RemafoX zu einem unschlagbaren Preis zu nutzen. Aber hier kommt das Beste: Wenn du dich während dieser Woche abonnierst, behältst du den reduzierten Preis für immer – auch nach Ende des Sales. Lass dir diesen unglaublichen Deal nicht entgehen, um deine Entwicklerkarriere anzukurbeln.</p><p><a href="https://github.com/FlineDev/RemafoX/issues?q=is%3Aopen+is%3Aissue+label%3A%22Feature+Request%22+sort%3Areactions-%2B1-desc">Neue Features</a> werden jeden Monat zu RemafoX hinzugefügt – ohne zusätzliche Kosten! Als Nächstes kommt Version 2.0 mit einer einfachen Möglichkeit, deine Nutzer einzuladen, bei der Übersetzung deiner App zu helfen, indem sie ein paar maschinell übersetzte Texte direkt in deiner App überprüfen. Und du hast die volle Kontrolle darüber, wie du deine Nutzer ansprechen möchtest – oder vielleicht willst du einfach ein paar Freunde um Hilfe beim Übersetzen bitten? All das wird bald mit einem aktiven Abo möglich sein. ✨</p><h2 id="update-16-spannende-neue-features-zur-wwdc">Update 1.6: Spannende neue Features zur WWDC!</h2><p>Zusätzlich zum ganz besonderen Sale habe ich RemafoX 1.6 mit drei brandneuen Features ausgestattet, die dein Coding-Erlebnis verbessern sollen. Diese Features sind komplett kostenlos für alle und sind meine Art, die WWDC mit euch zu feiern.</p><p>Schauen wir sie uns genauer an:</p><p><img src="/assets/images/blog/remafox-wwdc-sale/demo.gif" alt="" loading="lazy" /></p><h3 id="feature-1-sort-selection-organisiere-deinen-code-mit-leichtigkeit">Feature 1: “Sort Selection” – Organisiere deinen Code mit Leichtigkeit</h3><p>Ich habe ein schmerzlich vermisstes Feature zu Xcode hinzugefügt: Einen “Sort Selection”-Button. Jetzt kannst du ganz einfach jeden markierten Code alphabetisch sortieren – das spart dir wertvolle Zeit und Mühe. Erlebe den Komfort von organisiertem Code und optimiere deinen Entwicklungsprozess.</p><h3 id="feature-2-multi-line-code-vereinfache-komplexe-collections">Feature 2: “Multi-Line Code” – Vereinfache komplexe Collections</h3><p>Komplexe Collections in deinem Code zu handhaben ist jetzt ein Kinderspiel. Mit dem “Multi-Line Code”-Button in RemafoX erkennt Xcode Collections wie Parameterlisten und formatiert sie automatisch mehrzeilig. Genieße bessere Übersichtlichkeit und Effizienz beim Umgang mit komplexen Code-Strukturen.</p><h3 id="feature-3-one-line-code-code-sofort-verdichten">Feature 3: “One-Line Code” – Code sofort verdichten</h3><p>Wenn du mehrzeiligen Code in eine einzige Zeile verdichten musst, kommt der “One-Line Code”-Button zur Rettung. Reduziere visuelles Rauschen und verbessere die Code-Lesbarkeit mit einem einzigen Klick. Perfekt zum Teilen von Snippets oder zum Erstellen kompakter Darstellungen von länglichem Code. Oder um einfache Enums lesbarer zu machen.</p><h2 id="so-startest-du-kostenlos-mit-remafox">So startest du kostenlos mit RemafoX</h2><p><img src="/assets/images/blog/remafox-wwdc-sale/setup.gif" alt="" loading="lazy" /></p><ol><li><p><strong>Lade RemafoX herunter und starte die App:</strong> Lade RemafoX <a href="https://apps.apple.com/app/apple-store/id1605635026?pt=549314&ct=fline.dev&mt=8">hier aus dem Mac App Store</a> herunter, um vom Sale und den neuen Features von RemafoX 1.6 zu profitieren. Nach dem Download starte die App einmal, damit sich die Xcode-Extension im System registriert. Du kannst die App direkt wieder schließen, wenn du die Übersetzungs-Workflow-Verbesserungen nicht nutzen möchtest.</p></li><li><p><strong>Aktiviere die Xcode Source Editor Extension:</strong> Um RemafoX nahtlos in Xcode zu integrieren, öffne die Einstellungen-App deines Geräts und suche nach “Extensions”. Aktiviere den Eintrag für RemafoX im Modal der Xcode Source Editor Extension, um die RemafoX-Buttons in Xcode zu aktivieren.</p></li><li><p><strong>Richte Shortcuts in den Xcode-Einstellungen ein:</strong> Öffne Xcode und navigiere zum Einstellungen-Menü. Im Bereich Key Bindings richtest du eigene Shortcuts für die Buttons “Sort Selection”, “Multi-Line Code” und “One-Line Code” ein, die du einfach findest, indem du “remafox” in die Suchleiste eingibst. So kannst du die Möglichkeiten von RemafoX schnell und mühelos in Xcode nutzen.
Wenn du unsicher bist, welche Shortcuts du verwenden sollst, hier sind meine:
⌥⌘S  für “Sort Selection”
⌥⌘⬇  für “Multi-Line Code”
⌥⌘⬆  für “One-Line Code”</p></li></ol><hr /><blockquote><p>✨ Want to see your ad here? Contact me at <a href="mailto:ads@fline.dev">ads@fline.dev</a> to get in touch.</p></blockquote><hr /><h2 id="fazit">Fazit</h2><p>Hol dir RemafoX – entweder wegen der tollen kostenlosen Features oder um dir ein Abo zum reduzierten Preis zu sichern und von allen zukünftigen Updates zu profitieren. Steigere jetzt deine Produktivität! 🚀</p><p><a href="https://apps.apple.com/app/apple-store/id1605635026?pt=549314&ct=fline.dev&mt=8">‎RemafoX: Easy App Localization</a></p>]]></content:encoded>
</item>
<item>
<title>WWDC Notes übernehmen und ihre Zukunft gestalten</title>
<link>https://fline.dev/de/blog/taking-over-wwdc-notes-and-its-future/</link>
<guid isPermaLink="true">https://fline.dev/de/blog/taking-over-wwdc-notes-and-its-future/</guid>
<pubDate>Thu, 25 May 2023 00:00:00 +0000</pubDate>
<description><![CDATA[Das Open-Source-Projekt weiterentwickeln: Werde Teil der Community-Aktion und gestalte die Zukunft mit, wie wir WWDC-Sessions effektiv entdecken und daraus lernen!]]></description>
<content:encoded><![CDATA[<p>Vor gerade mal 10 Tagen hat mich <a href="https://twitter.com/zntfdr">Federico Zanetello</a>, der Initiator und Maintainer von <a href="https://wwdcnotes.com/">WWDC Notes</a>, kontaktiert und gefragt, ob ich helfen möchte, das Projekt am Leben zu halten. Nach einigen Nachrichten und einem Videocall stellte sich heraus, dass er gar nicht mehr an dem Projekt arbeiten konnte, und ich habe zugestimmt, es von ihm zu übernehmen. Die schlechte Nachricht ist (wie du dir denken kannst), dass der Videocall nur 2 Wochen vor der Dub Dub stattfand. 😱</p><p>Aber die gute Nachricht ist, dass Federico die Seite in so gut wie allen Aspekten automatisiert hat, sodass sie größtenteils „einfach läuft”. Hut ab vor ihm, dass er die Dinge so gestaltet hat, dass es so einfach wie möglich ist, das Projekt gesund zu halten. 💯👏 Außerdem hat er zum Glück Swift für im Grunde alles verwendet, einschließlich des <a href="https://github.com/JohnSundell/Publish">Publish</a>ens der Website, oder sogar für Hilfsprogramme wie das automatische Twittern neuer Zusammenfassungen. Für einen Swift-Enthusiasten wie mich, der sogar einen <a href="https://swiftevolution.substack.com/">Newsletter über Swift Evolution</a> betreibt, war das eine riesige Erleichterung. Es sieht so aus, als müsste ich dieses Jahr nur 2 Dinge tun:</p><p>Erstens: Die grundlegenden Informationen zu jeder Session (wie Links zum Video oder Apples Session-Beschreibung) ins Projekt eintragen, nachdem die Platforms State of the Union (auch bekannt als die „Developer Keynote”) stattgefunden hat. Denn dann kündigt Apple die Session-Details für den Rest der Woche an.</p><p>Zweitens: PRs mergen, die Community-Mitglieder für die Sessions <a href="https://wwdcnotes.com/what-s-missing/">hier aufgelistet</a> erstellt haben, die noch keine Zusammenfassung haben. Du kannst die Liste jetzt schon besuchen und wirst ca. 120 Sessions allein für das Jahr 2022 finden, für die noch niemand Notizen beigetragen hat – verglichen mit ca. 60 Sessions, die <em>bereits</em> Notizen für 2022 haben.</p><p>Obwohl es meine Hauptaufgabe für dieses Jahr ist, diese 2 Dinge zu tun, weil die Zeit für mehr Vorbereitung zu knapp ist, habe ich weitere Pläne für die Zukunft des Projekts. Und weil dies ein Community-Projekt ist, möchte ich meine Pläne mit euch teilen.</p><h2 id="80-session-abdeckung-bis-ende-der-wwdc-woche">80 % Session-Abdeckung bis Ende der WWDC-Woche</h2><p><img src="/assets/images/blog/taking-over-wwdc-notes-and-its-future/tuyen-vo-unsplash.webp" alt="Tuyen vo unsplash" loading="lazy" /></p><p><em>Foto von <a href="https://unsplash.com/@bitu2104?utm_source=ghost&utm_medium=referral&utm_campaign=api-credit">Tuyen Vo</a> / <a href="https://unsplash.com/?utm_source=ghost&utm_medium=referral&utm_campaign=api-credit">Unsplash</a></em></p><p>Wenn ich eine Lektion im Leben gelernt habe, dann diese: Es ist egal, was du tust – solange du aktiv bleibst, wirst du immer etwas lernen! Warum ich das erwähne? Nun, während meines Calls mit Federico hatte ich eine Idee für dieses Projekt, die auf einer Erfahrung beruhte, die ich gemacht habe, weil ich ein großer Fan der Harry-Potter-Bücher bin. (Keine Sorge, es hat nichts direkt mit Rowlings Schreiben zu tun.) Für einen Fan wie mich war es nämlich unerträglich zu wissen, dass ein neues Buch erschienen ist, aber noch 2,5 Monate auf die deutsche Übersetzung warten zu müssen. Ich war gerade 14 geworden, als <a href="https://harrypotter.fandom.com/wiki/Harry_Potter_and_the_Half-Blood_Prince">Der Halbblutprinz</a> erschien, und obwohl ich Englisch bis zu einem gewissen Grad verstand, reichte es nicht aus, um ein ganzes Buch durchzuarbeiten.</p><p>Zum Glück hörte ich in einem der Fan-Foren (wahrscheinlich <a href="http://forum.harrypotter-xperts.de/index.php?sid=791eea64f14bc76b4cd4b8f2913fb2d7">diesem hier</a>) von <a href="https://web.archive.org/web/20071021030543/http://www.harry-auf-deutsch.de/HaD/index.php">einem Projekt</a>, bei dem sich Leute organisierten, um das gesamte Buch in nur 48 Stunden zu übersetzen. Ich war zunächst skeptisch. Aber der Plan war einfach: Die eine Hälfte der Teilnehmer übersetzte jeweils ungefähr eine Seite des Buches innerhalb der ersten 24 Stunden. Dann erhielt die andere Hälfte jeweils die Übersetzung eines Teilnehmers und überprüfte sie als Lektor, um die Qualität zu verbessern. Am Ende fügten die Organisatoren alles zusammen und schickten das Endergebnis als PDF-Datei an alle Teilnehmer. Es klang zu schön, um wahr zu sein. Aber es hat tatsächlich funktioniert: Ich nahm als Erstübersetzer teil und hatte das vollständig übersetzte Buch am Sonntagabend. Es fühlte sich damals wie Magie an. Und ich lernte meine Lektion über die Kraft der Masse und des gemeinsamen Einsatzes.</p><p>Diese transformative Erfahrung hat bei mir nachgewirkt und mich davon überzeugt, dass wir mit dem WWDC Notes-Projekt etwas ebenso Beeindruckendes erreichen können. Genauso wie das Harry-Potter-Übersetzungsprojekt die Kraft der Community-Zusammenarbeit nutzte, bin ich zuversichtlich, dass wir mit eurer Hilfe ein ambitioniertes Ziel anstreben können: 100 % Abdeckung aller WWDC-Sessions mit zumindest grundlegenden Notizen innerhalb der ersten Woche! Indem wir das kollektive Fachwissen und die Begeisterung der Community nutzen, können wir sicherstellen, dass wertvolle Einblicke und Zusammenfassungen schnell verfügbar sind und Entwickler weltweit unterstützen.</p><p>Okay, du hast den Abschnittstitel gelesen – warum strebe ich dann nur 80 % an? Erstens gibt es einige Sessions zu Nischenthemen, die für ein breiteres Entwicklerpublikum nicht von Interesse sein dürften, wie „<a href="https://developer.apple.com/videos/play/wwdc2022/10149">What’s new in AVQT</a>”. Ich denke nicht, dass diese innerhalb der ersten Tage verfügbar sein müssen – also reduzieren wir mal auf 90 %.</p><p>Zweitens ist eine der zentralen Erkenntnisse bei <a href="https://testing.googleblog.com/2020/08/code-coverage-best-practices.html">Best Practices für Code Coverage</a>: „Wir sollten uns nicht darauf versteifen, von 90 % Code Coverage auf 95 % zu kommen. Die Gewinne durch eine Erhöhung der Code Coverage über einen bestimmten Punkt hinaus sind logarithmisch.” Ich glaube, etwas Ähnliches gilt für WWDC-Sessions: Sobald 80 % der Sessions abgedeckt sind, sind die fehlenden 10 % relevanter Themen wahrscheinlich solche, die niemand anschauen möchte, weil sie zu langweilig oder kompliziert klingen (auch wenn sie es nicht sind). Es kann sich anfühlen wie „der <em>Aufwand</em> zur Erhöhung der <em>Session</em>-Abdeckung über einen bestimmten Punkt hinaus ist logarithmisch.” Daher strebe ich 80 % Abdeckung bis Sonntag an. Passt auch zum <a href="https://en.wikipedia.org/wiki/Pareto_principle">Pareto-Prinzip</a>.</p><p>Aber das alles kann nur mit deiner Hilfe funktionieren. Ja, deiner. Du schreibst nie Session-Notizen? Oder du machst es, aber nur spärlich, und teilst sie deshalb nie öffentlich? Dann ist das deine Chance, aktiver zu werden und nebenbei etwas Neues zu lernen! Es gibt keine Anforderungen an Länge oder Format der Notizen, alles hilft. Du kannst auch nur als Lektor beitragen, wenn du möchtest – das hilft ebenfalls!</p><p>Damit ich das organisieren kann, kontaktiere bitte @WWDCNotes auf <a href="https://twitter.com/WWDCNotes">Twitter</a> oder <a href="https://iosdev.space/@WWDCNotes">Mastodon</a> mit der Nachricht: „Ich melde mich freiwillig, um Notizen beizutragen und zu überprüfen. Mich interessieren am meisten die Themen” und nenne dann einige Themen von dieser <a href="https://developer.apple.com/videos/topics/">offiziellen Liste</a>. Wenn du nur beitragen oder nur reviewen möchtest, passe den Text entsprechend an.</p><p>Ich melde mich dann am zweiten Tag der WWDC-Woche mit Vorschlägen, bei welchen Sessions du helfen könntest, sodass du nur für diese Notizen machen musst. Vielen Dank im Voraus für eure Hilfe! 🙏 Wenn jeder ein bisschen mithilft, profitieren wir alle gemeinsam.</p><hr /><h2 id="kompakte-takeaways-für-twitter-und-mastodon">Kompakte Takeaways für Twitter und Mastodon</h2><p><img src="/assets/images/blog/taking-over-wwdc-notes-and-its-future/khamkhor-unsplash.webp" alt="Khamkhor unsplash" loading="lazy" /></p><p><em>Foto von <a href="https://unsplash.com/@khamkhor?utm_source=ghost&utm_medium=referral&utm_campaign=api-credit">Khamkhor</a> / <a href="https://unsplash.com/?utm_source=ghost&utm_medium=referral&utm_campaign=api-credit">Unsplash</a></em></p><p>Auch wenn es großartig ist, Zusammenfassungen für jede WWDC-Session zu haben – es gibt jedes Jahr immer noch über 100 Sessions, und es kann wirklich schwer sein zu entscheiden, welche Session deine Zeit wert ist, nicht nur für das Video, sondern sogar fürs Lesen oder Überfliegen der Notizen, was auch zeitaufwendig sein kann. Genau deshalb habe ich versucht, meine wichtigsten Takeaways aus jeder der 21 Sessions, die ich während der WWDC-Woche geschaut und zusammengefasst habe, auf das Wesentliche zu reduzieren und sie alle in <a href="https://twitter.com/jeehut/status/1536077070958317568?s=61&t=mFz0Zzcht6SMwjCcQUaZBA">einen einzigen Twitter-Thread</a> zu packen – jede Session in einem einzigen Tweet mit weniger als 250 Zeichen (280 minus „More: <link>”).</p><p>Die Community schien das zu lieben – der Thread wurde 48 Mal retweetet (das sind mit Abstand meine meisten Retweets bisher) und in mehreren Newslettern erwähnt. Ich denke, das ist ein Beleg dafür, dass die Community genau solche kompakten Zusammenfassungen braucht, und deshalb plane ich, das Ganze dieses Jahr auf ein neues Level zu heben:</p><p>Natürlich werde ich, wie jedes Jahr, alle Sessions anschauen, die mich persönlich interessieren, und Notizen schreiben, damit ich später meine Erkenntnisse durchgehen kann. Selbstverständlich werde ich meine Notizen zum Projekt beitragen. Aber ich werde in der ersten Woche wahrscheinlich wieder nur ca. 20 Sessions abdecken können, was gerade mal etwa 10 % aller Sessions ist. Mit der Hilfe der Community möchte ich den Nutzen des #SessionSummary-Threads für #WWDC23 steigern, indem wir die Abdeckung erhöhen!</p><p>Ich werde also versuchen, jede bei WWDC Notes beigetragene Notiz auf eine Liste der wichtigsten Takeaways herunterzubrechen, und ich lade die Community-Beitragenden ein, mir dabei zu helfen. Wie diese Hilfe genau aussehen kann, werde ich vor der WWDC klären und auf den @WWDCNotes-Accounts auf <a href="https://twitter.com/wwdcnotes?s=21&t=mFz0Zzcht6SMwjCcQUaZBA">Twitter</a> und <a href="https://iosdev.space/@WWDCNotes">Mastodon</a> verkünden. Folge ihnen, um die Ankündigung nicht zu verpassen!</p><h2 id="die-website-open-source-machen">Die Website Open Source machen</h2><p>Aktuell besteht das WWDC Notes-Projekt aus 4 GitHub-Repositories: Content, Website, TwitterBot und SocialImages. Nur Content ist derzeit Open Source:</p><p><img src="/assets/images/blog/taking-over-wwdc-notes-and-its-future/the-wwdcnotes.webp" alt="Screenshot der Repositories der WWDCNotes-Organisation." loading="lazy" /></p><p><em>Screenshot der Repositories der WWDCNotes-Organisation.</em></p><p>Federico sagte mir, für die Zukunft des Projekts wollte er alles Open Source machen, ähnlich wie das <a href="https://github.com/SwiftPackageIndex/SwiftPackageIndex-Server">Swift Package Index</a>-Projekt öffentlich arbeitet. Obwohl er nicht bei diesem Ziel helfen kann, stimme ich zu 100 % damit überein, also werde ich versuchen, die Repositories so umzustrukturieren, dass ich alles als Open Source veröffentlichen kann – und vielleicht sogar alles in einem einzigen Repository halten, wenn das sinnvoll ist. Ich plane, dieses Thema später im Jahr anzugehen, wenn der WWDC-Trubel sich gelegt hat. Ich halte euch auf Twitter und Mastodon auf dem Laufenden, wenn es so weit ist.</p><h2 id="fazit">Fazit</h2><p>Das sind meine anfänglichen Ziele für dieses Projekt. Zusammengefasst:</p><ol><li><p>80 % aller WWDC-Sessions bis Sonntag mit Notizen abdecken</p></li><li><p>Twitter/Mastodon-Thread mit kompakten Takeaways für alle Sessions</p></li><li><p>Das komplette Projekt als Open Source veröffentlichen, inklusive Website und Social Bots (langfristig)</p></li></ol><p>Was denkst du? Gefällt dir die Richtung, die das nimmt?
Oder hast du Ideen, die du mit mir teilen möchtest? Lass es mich wissen!</p>]]></content:encoded>
</item>
<item>
<title>Fensterverwaltung mit SwiftUI 4</title>
<link>https://fline.dev/de/blog/window-management-on-macos-with-swiftui-4/</link>
<guid isPermaLink="true">https://fline.dev/de/blog/window-management-on-macos-with-swiftui-4/</guid>
<pubDate>Mon, 08 May 2023 00:00:00 +0000</pubDate>
<description><![CDATA[Erkenntnisse aus der Modernisierung der Fensterverwaltung meiner Mac-App nach dem Upgrade auf SwiftUI 4. Erklaerung von `\.openWindow`, `.windowResizability` und mehr.]]></description>
<content:encoded><![CDATA[<p>SwiftUI wird jedes Jahr deutlich besser. Letztes Jahr (2022) haben wir nicht nur verbesserte <a href="https://developer.apple.com/documentation/swiftui/navigation">Navigations-APIs</a> bekommen. Apple hat auch die Unterstuetzung fuer macOS erheblich verbessert – ich wuerde behaupten, dass die SwiftUI-APIs, die wir fuer die Mac-App-Entwicklung in SwiftUI 4 erhalten haben, auf einem <code>1.0</code>-Niveau sind und es endlich ermoeglichen, alle moeglichen Dinge in SwiftUI zu tun, ohne fuer einige der gaengigsten Aufgaben auf <code>AppKit</code> zurueckgreifen zu muessen. Ich habe SwiftUI auf dem Mac erlebt, waehrend ich an <a href="https://remafox.app/">RemafoX</a> mit SwiftUI 3 gearbeitet habe, und in einigen Teilen war es wirklich ein Albtraum.</p><p>Als iOS-Entwickler hatte ich gehofft, nicht alle Details von <code>AppKit</code> lernen zu muessen. Aber ich musste alle moeglichen Hacks schreiben, um die einfachsten Dinge zu tun – wie ein Fenster zu schliessen. Oder den Vollbild-Button an einem Fenster zu deaktivieren. Aber es gibt gute Neuigkeiten: Durch das Anheben meines App-Targets auf macOS 13.0 konnte ich die Fensterverwaltung endlich ueber SwiftUI erledigen und alle Hacks aus meiner App entfernen.</p><p>Endlich fuehlt sich meine App wirklich zu 100% SwiftUI-getrieben an. Und hier sind alle neuen APIs, die ich nutzen konnte – gruppiert und betitelt nach der Aufgabe, die ich erledigen wollte. Da die Fensterverwaltung wahrscheinlich der groesste Unterschied zwischen iOS- und macOS-Entwicklung in SwiftUI-Zeiten ist, koennte dieser Artikel auch jedem helfen, der von iOS zu macOS wechselt, um zu verstehen, wie Fensterverwaltung auf dem Mac funktioniert.</p><h2 id="ein-fenster-oeffnen">Ein Fenster oeffnen</h2><p>Wenn du eine <code>WindowGroup</code> verwendest (die der einzige Fenstertyp in SwiftUI 3 war), hast du mit SwiftUI 4 zwei Optionen: Die erste, die auch vorher schon unterstuetzt wurde, ist die Methode <a href="https://developer.apple.com/documentation/swiftui/windowgroup/handlesexternalevents(matching:)"><code>handlesExternalEvents</code></a>:</p><pre><code class="language-Swift">enum Window: String, Identifiable {
   case paywall
   // ...
   var id: String { self.rawValue }
}

@main
struct AppView: App {
   var body: some Scene {
      // ...
      WindowGroup(&quot;Plan Chooser&quot;) { ... }
         .handlesExternalEvents(matching: [Window.paywall.id])
      // ...
   }
}</code></pre><p>Wenn du dieses Fenster dann oeffnen moechtest, muesstest du eine URL oeffnen – so wie du jede externe URL oeffnen kannst, aber mit einem <a href="https://developer.apple.com/documentation/xcode/defining-a-custom-url-scheme-for-your-app#Register-your-URL-scheme">Custom URL Scheme</a>. Zum Beispiel:</p><pre><code class="language-Swift">@main
struct AppView: App {
   @Environment(\.openURL)
   var openURL

   var body: some Scene {
      // ...
      WindowGroup(...) { ... }
         .commands {
            CommandGroup(after: .windowArrangement) {
               Button(&quot;Show Plan Chooser&quot;) {
                  self.openURL(URL(string: &quot;remafox://\(Window.paywall.id)&quot;)!)
               }
               .keyboardShortcut(&quot;1&quot;)
            }
         }
   }
}</code></pre><p>Diese Methode funktioniert allerdings nicht mit dem neuen <code>Window</code>-Typ, obwohl sie dort vollstaendig verfuegbar ist. Die <a href="https://developer.apple.com/documentation/swiftui/window/handlesexternalevents(matching:)#discussion">Dokumentation</a> ist eindeutig:</p><blockquote><p>This modifier is only supported for WindowGroup Scene types.</p></blockquote><p>Aber die zweite Methode funktioniert sowohl fuer <code>WindowGroup</code> als auch <code>Window</code>: Der neue Environment-Wert <a href="https://developer.apple.com/documentation/swiftui/environmentvalues/openwindow/"><code>\.openWindow</code></a>! Zuerst definieren wir eine <code>id</code> im Initializer:</p><pre><code class="language-Swift">enum Window: String, Identifiable {
   case paywall
   // ...
   var id: String { self.rawValue }
}

@main
struct AppView: App {
   var body: some Scene {
      // ...
      WindowGroup(&quot;Plan Chooser&quot;, id: Window.paywall.id) { ... }
      // ...
   }
}</code></pre><p>Dann uebergeben wir diese <code>id</code> einfach an <code>openWindow</code>, um die Praesentation manuell auszuloesen:</p><pre><code class="language-Swift">@main
struct AppView: App {
   @Environment(\.openWindow)
   var openWindow

   var body: some Scene {
      // ...
      WindowGroup(...) { ... }
         .commands {
            CommandGroup(after: .windowArrangement) {
               Button(&quot;Show Plan Chooser&quot;) {
                  self.openWindow(id: Window.paywall.id)
               }
               .keyboardShortcut(&quot;1&quot;)
            }
         }
      // ...
   }
}</code></pre><p>Das ist viel schoener! Beachte, dass es auch <a href="https://developer.apple.com/documentation/swiftui/environmentvalues/opendocument/"><code>\.openDocument</code></a> fuer <code>DocumentGroup</code> gibt.</p><h2 id="doppelte-fenster-verhindern">Doppelte Fenster verhindern</h2><p>Die Verwendung einer <code>id</code> fuer ein Fenster verhindert nicht, dass mehrere davon erscheinen:</p><p><img src="/assets/images/blog/window-management-on-macos-with-swiftui-4/prevent-duplicate-windows.webp" alt="Prevent duplicate windows" loading="lazy" /></p><p>Zumindest nicht bei <code>WindowGroup</code> – du kannst aber einfach <code>Window</code> verwenden, um sicherzustellen, dass nie mehrere Fenster mit derselben <code>id</code> erstellt werden! Aber das ist nicht immer eine Option.</p><p>Ein <code>Window</code> ist in verschiedener Hinsicht viel eingeschraenkter als eine <code>WindowGroup</code>. Zum Beispiel kann man nicht <code>.commands</code> darauf aufrufen, wie ich es oben getan habe, um Buttons im Hauptmenue der App zu setzen. Ausserdem ist <code>Window</code> eine reine macOS-API – du kannst den Code also nicht auf iPadOS wiederverwenden. Der Grund, warum <em>ich</em> es fuer RemafoX nicht verwenden konnte, ist die Einschraenkung, die ich bereits erwaehnt habe: <code>Window</code> unterstuetzt kein <code>handlesExternalEvents</code>. Aber ich brauche diese API, weil RemafoX tief in Xcode integriert ist – sowohl ueber eine Extension als auch ein CLI-Tool –, und diese koennen <code>\.openWindow</code> nicht nutzen, weil sie einfach nicht Teil desselben Targets sind. Aber sie koennen trotzdem “externe” URLs oeffnen!</p><p>Was auch immer <em>dein</em> Grund sein mag, um Duplikate bei <code>WindowGroup</code> zu verhindern – mach Folgendes:</p><pre><code class="language-Swift">WindowGroup(&quot;Plan Chooser&quot;, id: Window.paywall.id, for: String.self) { _ in
   // ...
} defaultValue: {
   Window.paywall.id
}</code></pre><p>Hier wird eine <a href="https://developer.apple.com/documentation/swiftui/windowgroup/init(_:id:for:content:defaultvalue:)-95crv">ueberladene Version</a> des <code>WindowGroup</code>-Initializers verwendet, die zusaetzliche <code>for type</code>- und <code>defaultValue</code>-Argumente akzeptiert. Sie kann genutzt werden, um ein bestimmtes Fenster fuer einen beliebigen Datentyp zu oeffnen (wie einen <code>Profile</code>-Typ), aber ich verwende hier einfach die <code>id</code> vom Typ <code>String</code>, um das Fenster eindeutig zu machen.</p><p>In meinem Fall musste ich das an mehreren Stellen tun, also habe ich diese Extension erstellt:</p><pre><code class="language-Swift">extension WindowGroup {
   init&lt;W: Identifiable, C: View&gt;(_ titleKey: LocalizedStringKey, uniqueWindow: W, @ViewBuilder content: @escaping () -&gt; C)
   where W.ID == String, Content == PresentedWindowContent&lt;String, C&gt; {
      self.init(titleKey, id: uniqueWindow.id, for: String.self) { _ in
         content()
      } defaultValue: {
         uniqueWindow.id
      }
   }
}</code></pre><p>Damit wurde der obige Aufruf mit zwei <code>Window.paywall.id</code>-Aufrufen einfach zu:</p><pre><code class="language-Swift">WindowGroup(&quot;Plan Chooser&quot;, uniqueWindow: Window.paywall) {
   // ...
}</code></pre><p>Zum Oeffnen eines Fensters uebergeben wir zusaetzlich den <code>value</code>-Parameter an <code>openWindow</code>:</p><pre><code class="language-Swift">Button(&quot;Show Plan Chooser&quot;) {
   self.openWindow(id: Window.paywall.id, value: Window.paywall.id)
}</code></pre><p>Wieder haben mich die mehrfachen Aufrufe von <code>Window.paywall.id</code> gestoert, also habe ich einen Helper erstellt:</p><pre><code class="language-Swift">extension OpenWindowAction {
   func callAsFunction&lt;W: Identifiable&gt;(_ window: W) where W.ID == String {
      self.callAsFunction(id: window.id, value: window.id)
   }
}</code></pre><p>Jetzt kann ich einfach aufrufen – sogar ohne das <code>.id</code>-Suffix:</p><pre><code class="language-Swift">self.openWindow(Window.paywall)</code></pre><h2 id="ein-fenster-schliessen">Ein Fenster schliessen</h2><p>Jetzt, wo wir ein Fenster (eindeutig) oeffnen koennen, schliessen wir es auch. Und hier war die Situation vorher viel schlimmer als bei <code>WindowGroup</code>, wo wir zumindest einen Workaround innerhalb von SwiftUI hatten. Es gab einfach keine Moeglichkeit, ein Fenster direkt in SwiftUI zu schliessen. Ich musste <a href="https://onmyway133.com/posts/how-to-manage-windowgroup-in-swiftui-for-macos/#access-underlying-nswindow">diesen Hack</a> mit <code>AppKit</code> implementieren:</p><pre><code class="language-Swift">struct WindowAccessor: NSViewRepresentable {
   @Binding
   var window: NSWindow?

   func makeNSView(context: Context) -&gt; NSView {
      let view = NSView()
      DispatchQueue.main.async {
         self.window = view.window
      }
      return view
   }

   func updateNSView(_ nsView: NSView, context: Context) {}
}

@main
struct AppView: App {
   @State
   private var window: NSWindow?

   var body: some Scene {
      WindowGroup(...) {
         SomeView(...)
            .background(WindowAccessor(window: self.$window))
      }
   }
}</code></pre><p>Wenn ich dann das Fenster schliessen wollte, rief ich <code>self.window.close()</code> auf.</p><p>2021 wurde der Environment-Wert <a href="https://developer.apple.com/documentation/swiftui/environmentvalues/dismiss/"><code>\.dismiss</code></a> hinzugefuegt, mit dem praesentierte Views wie <code>sheet</code>, <code>popover</code> oder <code>fullScreenCover</code> direkt von innen heraus geschlossen werden konnten. Ich bin mir nicht sicher, ob dieses Verhalten damals schon verfuegbar war oder 2022 hinzugefuegt wurde, aber heute steht in der Dokumentation zusaetzlich, dass die <code>dismiss</code>-Aktion genutzt werden kann, um:</p><blockquote><p>Close a window that you create with <a href="https://developer.apple.com/documentation/swiftui/windowgroup"><code>WindowGroup</code></a> or <a href="https://developer.apple.com/documentation/swiftui/window"><code>Window</code></a>.</p></blockquote><p>Das funktioniert natuerlich nur, wenn gerade keine modale View innerhalb des betreffenden Fensters praesentiert wird. Aber dann funktioniert es einwandfrei – wir koennen einfach schreiben:</p><pre><code class="language-Swift">@main
struct AppView: App {
   @Environment(\.dismiss)
   var dismiss

   var body: some Scene {
      WindowGroup(...) {
         // ...
         Button(&quot;Close&quot;) {
            self.dismiss()  // &lt;= this closes the window if no modal
         }
      }
   }
}</code></pre><h2 id="den-vollbild-button-deaktivieren">Den Vollbild-Button deaktivieren</h2><p><img src="/assets/images/blog/window-management-on-macos-with-swiftui-4/disabling-full-screen.webp" alt="Disabling full screen" loading="lazy" /></p><p>Zuvor habe ich denselben oben erwahnten Hack verwendet, der mir Zugriff auf ein <code>NSWindow</code> gab, um weitere Konfigurationen vorzunehmen – wie das Deaktivieren des Vollbild-Buttons fuer Views mit fester Groesse, etwa mein Willkommensfenster oder mein About-Fenster. Aber jetzt haben wir den neuen Modifier <a href="https://developer.apple.com/documentation/swiftui/windowresizability/"><code>windowResizability</code></a>, der es uns erlaubt, den Vollbild-Button indirekt zu deaktivieren. Standardmaessig ist er auf <code>contentMinSize</code> gesetzt fuer alle Fenster ausser <a href="https://developer.apple.com/documentation/swiftui/settings"><code>Settings</code></a> (das ein <code>Scene</code>-Typ wie <code>WindowGroup</code> ist). Aber wir koennen jetzt Folgendes tun:</p><pre><code class="language-Swift">@main
struct AppView: App {
   var body: some Scene {
      WindowGroup(...) {
         SomeView(...)
            .frame(maxWidth: 400, maxHeight: 400)
         }
         .windowResizability(.contentSize)
      }
   }
}</code></pre><p>Indem wir <code>windowResizability</code> auf <code>.contentSize</code> setzen, sagen wir SwiftUI, den Groessen, die wir im <code>frame</code>-Modifier angeben, strenger zu folgen. Als logische Konsequenz: Wenn die von der View angegebene Maximalgroesse kleiner ist als die aktuelle Bildschirmgroesse des Nutzers, deaktiviert SwiftUI automatisch den Vollbild-Button fuer uns! Das ist keine besonders offensichtliche oder direkte API, aber es ergibt Sinn. Effektiv sollte der Button fuer die meisten Nutzer deaktiviert sein, wenn wir Werte unter 1366 mal 768 angeben – das ist die native Groesse eines 11-Zoll MacBook Air (von 2015).</p><h2 id="tca-extras">TCA-Extras</h2><p>Wenn du wie ich <a href="https://github.com/pointfreeco/swift-composable-architecture">The Composable Architecture</a> (TCA) fuer deine Apps verwendest, fragst du dich vielleicht, wie du diese neuen Environment-Werte am besten an deine Reducer weiterleiten kannst, da Logik wie das Oeffnen/Schliessen von Fenstern dort stattfinden sollte. Die grossartige Community rund um TCA hat mir geholfen, das elegant zu loesen – insbesondere <a href="https://github.com/tgrapperon">Thomas Grapperon</a> hat einen Typ bereitgestellt, den ich in <code>OnChange</code> umbenannt habe und den du einfach per Copy &amp; Paste in deine Projekte uebernehmen kannst – <a href="https://github.com/pointfreeco/swift-composable-architecture/discussions/1683#discussioncomment-5539006">von hier</a>. Dann haenge in deiner View den <code>.onChange</code>-Modifier an und uebergib ihm einen beliebigen Wert, der an den Reducer weitergeleitet werden soll:</p><pre><code class="language-Swift">@Environment(\.openWindow)
var openWindow

var body: some View {
   WithViewStore(...) { viewStore in
      SomeView(...)
         .onChange(of: \.$openWindow, store: self.store) { window in
            self.openWindow(window)
         }
   }
}</code></pre><p>Beachte, dass sich <code>\.$openWindow</code> auf ein Feld im <code>State</code> bezieht, also muessen wir es definieren:</p><pre><code class="language-Swift">struct SomeState {
   @OnChange
   var openWindow: Window?
}</code></pre><p>Jetzt koennen wir in unseren Reducern einfach den State-Wert auf einen <code>Window</code>-Enum-Case setzen, und die View leitet die Aenderung automatisch an den <code>@Environment</code>-Wert weiter. Das funktioniert auch mit jedem anderen SwiftUI-Attribut – ich habe es auch fuer <code>@FocusState</code> verwendet!</p><p>Uebrigens: Obwohl TCA mit einer <code>\.dismiss</code>-Dependency ausgeliefert wird, wird der Aufruf von <code>await self.dismiss()</code> im Reducer sich (<a href="https://github.com/pointfreeco/swift-composable-architecture/discussions/1944#discussioncomment-5549895">derzeit</a>) nicht wie der SwiftUI-<code>\.dismiss</code>-Environment-Wert verhalten und das Fenster schliessen. Stattdessen passiert nichts und es wird sogar eine Warnung erzeugt. Als Workaround habe ich eine Dependency implementiert, die AppKit-APIs nutzt, indem sie die offenen Fenster durchlaeuft, den passenden Match findet und diesen schliesst. Du kannst das Gist <a href="https://gist.github.com/Jeehut/7601bdbce1af1f848b1ea98f697a0f95">von hier</a> per Copy &amp; Paste uebernehmen. Die Verwendung sieht so aus:</p><pre><code class="language-Swift">// add the dependency to your reducer
@Dependency(\.closeWindow)
var closeWindow

// close a window in a `run` effect
return .run { _ in await self.closeWindow(Window.paywall) }</code></pre><p>Und das war alles, was ich heute ueber Fensterverwaltung in SwiftUI 4 zu teilen hatte!</p><blockquote><p><strong>Du fandest diesen Artikel hilfreich? Hol dir meinen Expertenrat!</strong></p></blockquote>]]></content:encoded>
</item>
<item>
<title>Meine Top 5 Wünsche für die WWDC 2023</title>
<link>https://fline.dev/de/blog/my-top-5-wishes-for-wwdc-2023/</link>
<guid isPermaLink="true">https://fline.dev/de/blog/my-top-5-wishes-for-wwdc-2023/</guid>
<pubDate>Thu, 27 Apr 2023 00:00:00 +0000</pubDate>
<description><![CDATA[Die WWDC ist nur noch wenige Wochen entfernt, also wird es Zeit, meine Wunschliste zu aktualisieren. Ein Wunsch wurde letztes Jahr erfüllt – wie viele werden es 2023?]]></description>
<content:encoded><![CDATA[<p>Ich habe letztes Jahr bereits einen Artikel mit genau dem gleichen Zweck für die WWDC 2022 geschrieben und <a href="https://www.fline.dev/my-top-3-wishes-for-wwdc-2022/">meine Top 3 Wünsche</a> aufgelistet. Zum Glück wurde einer davon (Wunsch Nr. 3) tatsächlich erfüllt – die <a href="https://developer.apple.com/documentation/xcode-release-notes/xcode-14-release-notes">Xcode 14 Release Notes</a> beschreiben das zugehörige neue Feature so:</p><blockquote><p>Simplify an app icon with a single 1024x1024 image that is automatically resized for its target. Choose the Single Size option in the app icon’s Attributes inspector in the asset catalog. You can still override individual sizes with the All Sizes option. (18475136) (FB5503050)</p></blockquote><p>Meine anderen beiden Wünsche gelten weiterhin und bleiben ganz oben auf meiner Wunschliste:</p><p>#1: Ein <strong>neues, reines Swift-Datenbank-Framework</strong>, das CoreData langfristig ersetzen soll. Wie ich mir so ein Framework vorstelle, habe ich <a href="https://www.fline.dev/my-top-3-wishes-for-wwdc-2022/#1-new-swift-only-database-framework">hier</a> beschrieben – lies dort für Details nach.</p><p>#2: <strong>Modularisierungs-Unterstützung</strong> für Apps in Xcode. Aktuell muss ich manuell eine lange <code>Package.swift</code>-Datei pflegen, um meine App zu modularisieren – mehr dazu <a href="https://www.fline.dev/my-top-3-wishes-for-wwdc-2022/#2-app-modularization-support-in-xcode">hier</a>.</p><p>Nachdem wir das geklärt haben, kommen hier 3 neue Wünsche, die ich für die diesjährige WWDC habe.</p><h2 id="3-kreisdiagramme-in-der-swift-charts-bibliothek">#3: Kreisdiagramme in der Swift Charts-Bibliothek</h2><p>Am Ende meines letztjährigen Artikels schrieb ich:</p><blockquote><p>In der Vergangenheit wurde ich immer von mindestens ein oder zwei Frameworks komplett überrascht, wie <a href="https://developer.apple.com/videos/play/wwdc2019/216/">SwiftUI</a> 2019, <a href="https://developer.apple.com/videos/play/wwdc2020/10028/">WidgetKit</a> 2020 und <a href="https://developer.apple.com/videos/play/wwdc2021/10166/">DocC</a> 2021. Was wird es dieses Jahr sein? Ich kann es kaum erwarten, es herauszufinden!</p></blockquote><p>Nun, es war definitiv <a href="https://developer.apple.com/videos/play/wwdc2022/10136/">Swift Charts</a>! Die neue, glänzende Bibliothek, die vielleicht nicht jede App sofort braucht, aber ich bin sicher, dass sie viele Entwickler inspirieren wird, Daten in ihren Apps zu visualisieren, um Nutzern einen besseren Überblick über ihre gesammelten Daten zu geben. Sie ist SwiftUI-only, also hatten noch nicht alle Teams die Möglichkeit, sie einzusetzen.</p><p>Die erste Version der <a href="https://developer.apple.com/documentation/Charts">Charts-Bibliothek</a> wurde mit einer guten Auswahl an Diagrammtypen ausgeliefert, die alle gemeinsam haben, dass sie nur ein oder zwei gerade Achsen benötigen. Du kannst zum Beispiel bereits Liniendiagramme, Balkendiagramme und sogar Heatmaps zeichnen (und anpassen!). <a href="https://twitter.com/jordibruin">Jordi Bruin</a> hat <a href="https://github.com/jordibruin/Swift-Charts-Examples">ein GitHub-Repo zusammengestellt</a> mit Screenshots und Quellcode, das dir einen guten Überblick gibt, was heute schon möglich ist.</p><p>Aber als ich die Swift Charts-Bibliothek selbst für meine Open-Source-App <a href="https://github.com/FlineDevPublic/OpenFocusTimer">Open Focus Timer</a> verwenden wollte, die ich während <a href="https://www.youtube.com/playlist?list=PLvkAveYAfY4TVdM3Lc52SJTkuGB85I5uw">meiner Livestreams</a> auf <a href="https://www.twitch.tv/Jeehut">Twitch</a> entwickle, wollte ich den Nutzern ein Tortendiagramm zeigen, wie sich ihre Arbeitszeit auf verschiedene Projekte verteilt. Aber Tortendiagramme werden von der Charts-Bibliothek aktuell nicht unterstützt, genauso wenig wie Spinnendiagramme, Donut-Diagramme oder Sunburst-Diagramme. Ich finde aber, sie sind so verbreitet, dass sie alle in einer zweiten Version der Charts-Bibliothek dieses Jahr hinzugefügt werden sollten.</p><p>Und nächstes Jahr könnten vielleicht auch baumartige Diagramme hinzugefügt werden, die helfen könnten, professionelle Tools zu erstellen – z. B. zum Zeichnen eines <a href="https://en.wikipedia.org/wiki/Unified_Modeling_Language">UML</a>-Diagramms der Model-Schicht einer App, zum Visualisieren von Datenstrukturen wie <a href="https://en.wikipedia.org/wiki/B-tree">B-Bäumen</a> oder zum Bauen interaktiver <a href="https://en.wikipedia.org/wiki/Finite-state_machine">Zustandsmaschinen</a>, die helfen könnten, Features auf einer abstrakteren Ebene zu verstehen. Ich habe genug Ideen, die solche Diagramme gut nutzen könnten!</p><h2 id="4-streamer-modus-in-xcode-zum-schwärzen-von-code">#4: Streamer-Modus in Xcode zum Schwärzen von Code</h2><p>Inhalte zu teilen ist eine tolle Sache. Es hilft nicht nur anderen, die den geteilten Inhalt konsumieren, etwas Neues zu lernen oder sich inspirieren zu lassen. Das Feedback kann auch dem Content Creator helfen, neue Aspekte zu entdecken, an die er noch nicht gedacht hat, wie subtile Bugs, UX-Probleme oder fehlende Barrierefreiheit. Aber wenn der zu teilende Inhalt mit einem Coding-Projekt zusammenhängt, kommen Sicherheits- und Geheimhaltungsbedenken ins Spiel.</p><p>Wenn man zum Beispiel ein Open-Source-Framework mit der Community teilt, das sich mit Drittanbieter-Services integriert, muss man sicherstellen, dass nie Test-Zugangsdaten ins Repository committed werden, da sie sonst leaken und missbraucht werden könnten. Genau in diese Situation bin ich mit SwiftPM für mein Open-Source-Tool <a href="https://github.com/FlineDev/BartyCrouch">BartyCrouch</a> geraten – wie ich es gelöst habe, erkläre ich in <a href="https://www.fline.dev/hiding-secrets-from-git-in-swiftpm/">diesem Artikel</a>.</p><p>Und Secrets sind nicht der einzige Teil, den wir vor anderen verbergen wollen: Kein Unternehmen möchte, dass der Kernwert seiner Produkte, der viel Aufwand gekostet hat, an einen potenziellen Konkurrenten durchsickert. Nachahmer sind ein ernstes Problem, das Unternehmen zerstören kann. Die Lösung für Git-Repositories ist einfach genug: Wertvoller Code, der nicht leaken darf, muss Closed Source bleiben und nie mit Außenstehenden geteilt werden.</p><p><img src="/assets/images/blog/my-top-5-wishes-for-wwdc-2023/kristina-flour-unsplash.webp" alt="Secret" loading="lazy" /></p><p><em>Foto von <a href="https://unsplash.com/@tinaflour?utm_source=ghost&utm_medium=referral&utm_campaign=api-credit">Kristina Flour</a> / <a href="https://unsplash.com/?utm_source=ghost&utm_medium=referral&utm_campaign=api-credit">Unsplash</a></em></p><p>Aber Secrets aus einem Git-Repository zu verbergen oder Teile der Codebasis privat zu halten, ist nur ein Teil der Geschichte. Was ist mit Videocalls mit Freunden oder Fremden, die uns helfen wollen? Was ist mit Indie-Entwicklern (wie mir), die so viel wie möglich von ihrer Arbeit live streamen wollen? Es gibt so viele Situationen, in denen ich einfach meinen Bildschirm teilen und ein bestimmtes Problem oder eine Lösung direkt im Kontext meiner echten Anwendung zeigen möchte – aber ich tue es nicht. Das Risiko, versehentlich sensiblen Code zu leaken, ist für mich einfach zu hoch. Das führt zu vielen verpassten Möglichkeiten:</p><ul><li><p>Als Indie-Entwickler würde ich gerne sogar die Entwicklung von Updates für meine Closed-Source-Apps live streamen, aber es gibt Teile, die ich nicht leaken möchte. Also streame ich nur meine Open-Source-Arbeit. Das ist echt schade.</p></li><li><p>Als Framework-Nutzer würde ich gerne Bugs oder Feature-Wünsche melden, indem ich schnell meinen Bildschirm aufnehme und ein Video der Nutzung im Kontext teile. Aktuell verzichte ich oft darauf, überhaupt etwas zu melden, weil mir die Vorbereitung einer Demo zu umständlich ist. Oder ich kopiere Teile des Codes aus dem Kontext und muss dann mehrere Fragen beantworten, um zu erklären, warum ich es genau so mache.</p></li><li><p>Als Teilnehmer eines wöchentlichen Entwickler-Austauschs spreche ich lieber auf einer abstrakten Ebene über Code, wenn ich auf Probleme stoße, anstatt meinen Bildschirm zu teilen und es im Detail zu zeigen. Und selbst wenn ich meinen Bildschirm <em>doch</em> teile, würde ich meinem Gegenüber niemals erlauben, meinen Bildschirm zu <em>steuern</em> (z. B. um mir schnell etwas zu zeigen). Ich weiß, ein Teil des Problems ist, dass ich Vertrauensprobleme habe – aber damit bin ich nicht allein.</p></li></ul><p>Was ich mir also wünsche, ist eine Funktion in Xcode, um bestimmte Teile meiner Codebasis als „vertraulich” zu markieren, und wenn ich meinen Bildschirm teile, werden alle vertraulichen Teile auf dem geteilten Bildschirm ausgeblendet. So eine Funktion wird in Apps wie <a href="https://support.discord.com/hc/en-us/articles/218485407-Streamer-Mode-101">Discord</a> üblicherweise „Streamer-Modus” genannt. Sie könnte auf verschiedene Weisen implementiert werden – so stelle ich mir das vor:</p><ul><li><p>Die vertraulichen Inhalte könnten für den Streamer sichtbar bleiben, aber von Xcode hervorgehoben werden, damit der Streamer weiß, dass sie in geteilten Inhalten nicht erscheinen.</p></li><li><p>Das Markieren von Inhalten als „vertraulich” könnte auf verschiedenen Ebenen erfolgen, z. B. auf Dateiebene (alle Inhalte einer Datei werden geschwärzt), auf Typ-Ebene, auf Funktionsebene oder auf Variablenebene. Im letzteren Fall würden die gesamten Zeilen geschwärzt.</p></li><li><p>Die Namen der geschwärzten Inhalte könnten weiterhin sichtbar sein (Dateiname / Typname / Funktionsdeklaration / Variablenname) und nur ihre Bodies/Werte geschwärzt werden.</p></li><li><p>Die Namen aller als „vertraulich” markierten Inhalte könnten vor Bearbeitung gesperrt werden, um sicherzustellen, dass ein Umbenennen nicht vorübergehend deren Inhalte leakt.</p></li><li><p>Die Vertraulichkeitsmarkierungen könnten in einer Datei gespeichert werden, die in Git committed werden kann, sodass andere Entwickler im Team sie in ihren externen Calls ebenfalls nutzen könnten.</p></li><li><p>Der Streamer-Modus könnte automatisch aktiviert werden, wenn eine App gerade die <a href="https://developer.apple.com/documentation/screencapturekit">Screen-Capturing-APIs</a> nutzt, sodass der Entwickler nicht daran denken muss, den „Streamer-Modus” manuell einzuschalten. Das sollte sowohl für Videocalls (in FaceTime/Zoom etc.) als auch für Bildschirmaufnahmen (in OBS/QuickTime etc.) funktionieren.</p></li></ul><p>Ich bin nicht sehr optimistisch, dass so eine Funktion es in Xcode schafft, da ich das Gefühl habe, dass das kein Problem ist, das von vielen Entwicklern häufig angesprochen wird. Es fühlt sich wie ein Nischenthema an. Aber meiner Meinung nach ist es eines dieser Features, von denen man nicht weiß, dass man sie vermisst hat, bis man sich daran gewöhnt hat – und dann möchte man sie nie wieder missen.</p><h2 id="5-arvr-os-entwicklung-mit-einem-iphone">#5: AR/VR OS-Entwicklung mit einem iPhone</h2><p>Tim Cook spricht öffentlich über das Potenzial von AR seit <a href="https://www.theverge.com/21077484/apple-tim-cook-ar-augmented-reality">mindestens 2016</a>. Unter der Annahme, dass er nicht über etwas sprechen würde, an dem Apple nicht schon mindestens ein ganzes Jahr geforscht hat, können wir sicher sagen, dass Apple seit mindestens 8 Jahren an AR-Produkten arbeitet. Das erste iPhone brauchte <a href="https://www.history.com/news/iphone-original-size-invention-steve-jobs">zweieinhalb Jahre</a> Entwicklungszeit. Die erste Apple Watch brauchte <a href="https://www.businessinsider.com/tim-cook-full-interview-with-charlie-rose-with-transcript-2014-9">drei Jahre</a> Entwicklung. Während man also fragen könnte, <em>warum</em> wir Apples erstes AR-Gerät immer noch nicht bekommen haben, gehe ich einfach mal davon aus, dass wir dieses Jahr eines bekommen. Und ich nehme auch an, dass dieses neue Gerät mit einem eigenen Betriebssystem ausgeliefert wird – nennen wir es „AR/VR OS”.</p><p>Ich kann schon jetzt ein Problem für Entwickler absehen, wenn so ein Gerät angekündigt wird: Nicht jeder Entwickler wird sich sofort ein neues Gerät kaufen können. Manche wegen finanzieller Einschränkungen, manche weil Produkte der ersten Generation meist nicht gleich in allen Ländern verfügbar sind. In der Vergangenheit war das kein großes Problem. Es gibt Simulatoren für das iPhone, das iPad, die Apple Watch und sogar den Apple TV, die mit Xcode ausgeliefert werden. Zwar gibt es Einschränkungen (wie keine Kameraunterstützung), aber die meisten Apps können vollständig entwickelt und größtenteils auch getestet werden, ohne Probleme. Das liegt daran, dass all diese Geräte eines mit unserem Entwicklungsgerät, dem Mac, gemeinsam haben: Sie alle rendern eine virtuelle Oberfläche auf einem flachen Bildschirm.</p><p>Aber ein AR-Gerät ist anders: Per Definition „erweitert” es die Realität, was bedeutet, dass wir für die Entwicklung und das Testen einer nützlichen App Zugang zu einem Kamerafeed brauchen, damit wir etwas zum „Erweitern” haben. Apple könnte zwar einige virtuelle Beispielumgebungen zum Testen in einem AR/VR-Simulator bereitstellen, wie sie es bei der Standortsimulation getan haben, aber das wäre für viele Anwendungsfälle sehr einschränkend und kann auch nervig sein.</p><p><img src="/assets/images/blog/my-top-5-wishes-for-wwdc-2023/arvr-dev-environment.webp" alt="AR/VR development environment screenshot" loading="lazy" /></p><p>Was ich mir stattdessen wünsche, ist, dass Apple Entwicklern eine Möglichkeit bietet, die AR/VR OS-Entwicklung mit dem Kamerafeed eines verbundenen iPhones oder iPads zu testen. Das könnte auf Geräte mit einem <a href="https://en.wikipedia.org/wiki/Lidar">LiDAR</a>-Scanner beschränkt werden, wenn das eine technische Voraussetzung ist. Aber es sollte auch drahtlos funktionieren, da wir damit herumlaufen wollen, um unsere Features in verschiedenen Umgebungen zu testen. Die grundlegende Technologie dafür wurde bereits in Form von <a href="https://support.apple.com/en-us/HT213244">Continuity Camera</a> ausgeliefert, das es erlaubt, ein iPhone als Webcam zu verwenden. Vielleicht war dieses Feature ja nur ein Nebenprodukt besagter Testfunktionen, die ursprünglich für das interne Team gebaut wurden, das am AR-Produkt arbeitete. Wer weiß? Das könnte ein Zeichen sein, dass es kommen wird.</p><p>Aber noch wichtiger: Warum ich optimistisch bin, dass wir <em>irgendeine</em> Möglichkeit zum Testen für AR/VR OS ohne das echte Gerät bekommen werden, ist, dass Apple ein Interesse daran hat, dass so viele Apps wie möglich das Gerät unterstützen. Und das bedeutet, es muss so einfach wie möglich sein, Anwendungen dafür zu entwickeln. Schließlich ist die Verfügbarkeit von (einzigartigen) Apps ein wichtiges Verkaufsargument jeder neuen Softwareplattform.</p><h2 id="wie-steht-es-um-den-ki-hype">Wie steht es um den KI-Hype?</h2><p>Ich wünsche mir zwar eine bessere Auto-Completion-Unterstützung in Xcode oder sogar einen virtuellen Coding-Assistenten, dem ich sagen kann, was ich möchte, und er schreibt den Code, damit ich nicht alles selbst tippen muss – aber ich glaube, wir sind noch nicht so weit. Ich habe mit ChatGPT experimentiert, aber es lag in 80 % der Fälle daneben, wenn ich nach Code fragte. Es verwendete nicht nur ständig veraltete APIs, sondern produzierte auch Code, der nicht kompilierte. Oft verstand es nicht einmal, was ich wollte.</p><p>Obwohl ein dediziertes Modell fürs Programmieren von Apple bessere Ergebnisse liefern könnte, glaube ich nicht, dass es etwas erreichen wird, das ich als „zuverlässig” bezeichnen würde. Und ich bin sicher, Apple weiß das. Sie werden so eine Funktion vielleicht in der Zukunft erforschen, aber ich hoffe, sie springen nicht auf den aktuellen Hype-Zug auf und bauen etwas Halbfertiges in Xcode ein. Ich bevorzuge Tools, auf die ich mich verlassen kann und die vorhersehbar sind. Ich glaube, das sieht Apple genauso. Aber die Technologie ist noch nicht so weit. Ich erwarte etwas Großes in diese Richtung frühestens nächstes Jahr, dieses Jahr vielleicht etwas Kleines, wenn überhaupt.</p><h2 id="fazit">Fazit</h2><p>Das sind also <em>meine</em> Top 5 Wünsche für die WWDC 2023. Stimmst du mir zu? Und was sind deine? Lass es mich wissen, indem du auf Twitter <a href="https://twitter.com/jeehut/status/1651733420240666625?s=61&t=3mfZyJ0MLsW7Lpqz13mreg">hier</a> oder auf Mastodon <a href="https://iosdev.space/@Jeehut/110273426937982763">dort</a> kommentierst.</p>]]></content:encoded>
</item>
<item>
<title>Meine App fuer Swift 6 vorbereiten</title>
<link>https://fline.dev/de/blog/preparing-for-swift-6/</link>
<guid isPermaLink="true">https://fline.dev/de/blog/preparing-for-swift-6/</guid>
<pubDate>Tue, 18 Apr 2023 00:00:00 +0000</pubDate>
<description><![CDATA[Wie du den Swift-6-Modus fuer deine Xcode-Projekte und SwiftPM-Module schon heute aktivieren kannst. Und wie die Migrationserfahrung so ist.]]></description>
<content:encoded><![CDATA[<h2 id="was-ist-der-swift-6-modus">Was ist der “Swift-6-Modus”?</h2><p>Swift 6 wird nicht mehr 2023 veroeffentlicht, das wurde von Doug Gregor aus der Swift Language Workgroup <a href="https://forums.swift.org/t/design-priorities-for-the-swift-6-language-mode/62408/15">klargestellt</a>. Aber wusstest du, dass Apple bereits Teile von Swift 6 in Version 5.8 ausgeliefert hat? Ja, das stimmt. Einige Teile von Swift, die mit Xcode 14.3 vor ein paar Wochen ausgeliefert wurden, sind <strong>standardmaessig deaktiviert</strong>. Sie werden eingeschaltet, sobald Swift 6 erscheint, was noch ein Jahr dauern koennte – oder sogar laenger.</p><p>Diese Features fuehren einige Breaking Changes in Swift ein, zum Beispiel durch Umbenennung bekannter APIs, Anpassung ihres Verhaltens oder neue Sicherheitspruefungen im Compiler. Aber sie werden alle irgendwann aktiviert, um unsere Codebasen zu verbessern. Und wir werden unsere Projekte entsprechend anpassen muessen.</p><p>Ich dachte mir, es waere eine gute Idee, alle Features in meinen Projekten zu aktivieren, um zu sehen, ob es Breaking Changes fuer meine Codebasis gibt, die ich kennen sollte. Und natuerlich koennte ich auch einige neue Features nutzen, wie <code>BareSlashRegexLiterals</code>, das die <code>/.../</code>-Regex-Literal-Syntax fuer kompakte Regex-Initialisierung verfuegbar macht.</p><p>Gluecklicherweise gibt es mit Swift 5.8 jetzt einen einheitlichen Weg, diese Optionen zu aktivieren: Wir muessen einfach <code>-enable-upcoming-feature</code> an Swift uebergeben, indem wir es in den <code>OTHER_SWIFT_FLAGS</code> in den Build Settings unseres Projekts in Xcode angeben. Aber wir muessen auch wissen, welche Features verfuegbar sind, und ich konnte keinen Ort mit einer guten Uebersicht finden (<a href="https://github.com/apple/swift-org-website/pull/284">noch nicht</a>). Der Vorschlag, der diese einheitliche Option eingefuehrt hat, enthaelt <a href="https://github.com/apple/swift-evolution/blob/main/proposals/0362-piecemeal-future-features.md#proposals-define-their-own-feature-identifier">eine solche Liste</a>, aber sie wird nicht mit neueren Optionen aktualisiert, die spaeter hinzugefuegt werden, wie die von <a href="https://github.com/apple/swift-evolution/blob/main/proposals/0384-importing-forward-declared-objc-interfaces-and-protocols.md">SE-0384</a>. Wo koennen wir also derzeit zuverlaessig eine Liste aller unterstuetzten Optionen bekommen?</p><blockquote><p>UPDATE: Du kannst jetzt direkt auf Swift.org <a href="https://www.swift.org/swift-evolution/#?upcoming=true">nach Upcoming Flags filtern</a>.</p></blockquote><p>Swift ist Open Source, also scheint der zuverlaessigste Ort das Swift-GitHub-Repository zu sein! Es enthaelt eine <code>Features.def</code>-Datei, die <a href="https://github.com/apple/swift/blob/release/5.8/include/swift/Basic/Features.def#L96-L99">Eintraege</a> (Link zum <code>release/5.8</code>-Branch) namens <code>UPCOMING_FEATURE</code> beinhaltet, einschliesslich der zugehoerigen Swift-Evolution-Vorschlagsnummer und der Swift-Version, in der sie aktiviert werden:</p><pre><code class="language-Swift">UPCOMING_FEATURE(ConciseMagicFile, 274, 6)
UPCOMING_FEATURE(ForwardTrailingClosures, 286, 6)
UPCOMING_FEATURE(BareSlashRegexLiterals, 354, 6)
UPCOMING_FEATURE(ExistentialAny, 335, 6)</code></pre><p>Hier sind die Optionen mit einer kurzen Beschreibung, was sie tun:</p><ul><li><p><a href="https://github.com/apple/swift-evolution/blob/main/proposals/0274-magic-file.md"><strong><code>ConciseMagicFile</code></strong></a><strong>:</strong>
Aendert <code>#file</code>, sodass es <code>#fileID</code> statt <code>#filePath</code> bedeutet.</p></li><li><p><a href="https://github.com/apple/swift-evolution/blob/main/proposals/0286-forward-scan-trailing-closures.md"><strong><code>ForwardTrailingClosures</code></strong></a><strong>:</strong>
Entfernt die Backward-Scan-Matching-Regel.</p></li><li><p><a href="https://github.com/apple/swift-evolution/blob/main/proposals/0335-existential-any.md"><strong><code>ExistentialAny</code></strong></a><strong>:</strong>
Verlangt <code>any</code> fuer existenzielle Typen.</p></li><li><p><a href="https://github.com/apple/swift-evolution/blob/main/proposals/0354-regex-literals.md"><strong><code>BareSlashRegexLiterals</code></strong></a><strong>:</strong>
Macht die <code>/.../</code>-Regex-Literal-Syntax verfuegbar.</p></li></ul><p>Aus irgendeinem Grund scheinen zwei Optionen dort nicht aufgefuehrt zu sein (ich <a href="https://forums.swift.org/t/design-priorities-for-the-swift-6-language-mode/62408/80">untersuche es</a>):</p><ul><li><p><a href="https://github.com/apple/swift-evolution/blob/main/proposals/0337-support-incremental-migration-to-concurrency-checking.md"><strong><code>StrictConcurrency</code></strong></a><strong>:</strong>
Fuehrt vollstaendige Concurrency-Pruefung durch.</p></li><li><p><a href="https://github.com/apple/swift-evolution/blob/main/proposals/0352-implicit-open-existentials.md"><strong><code>ImplicitOpenExistentials</code></strong></a><strong>:</strong>
Fuehrt implizites Oeffnen in zusaetzlichen Faellen durch.</p></li></ul><p>Weitere Optionen werden mit spaeteren Versionen ausgeliefert, z. B. <a href="https://github.com/apple/swift-evolution/blob/main/proposals/0384-importing-forward-declared-objc-interfaces-and-protocols.md"><code>ImportObjcForwardDeclarations</code></a>.</p><p>Um meinen Code zusaetzlich auf korrekte Concurrency-Unterstuetzung zu pruefen, habe ich mich entschieden, auch <code>-warn-concurrency</code> (das sollte eigentlich dasselbe wie <code>StrictConcurrency</code> sein, falls das tatsaechlich funktioniert) und <code>-enable-actor-data-race-checks</code> zu uebergeben.</p><h2 id="mein-projekt-migrieren">Mein Projekt migrieren</h2><p>Wenn du wie ich alle <code>5.8</code>-Optionen in deinem Projekt aktivieren moechtest, kopiere (Cmd+C) den folgenden Textblock, gehe dann zum Tab “Build Settings” deines Xcode-Projekts, suche nach “Other Swift Flags”, waehle diese Option im Editor aus und fuege sie ein (Cmd+V):</p><pre><code class="language-bash">//:configuration = Debug
OTHER_SWIFT_FLAGS = -enable-upcoming-feature BareSlashRegexLiterals -enable-upcoming-feature ConciseMagicFile -enable-upcoming-feature ExistentialAny -enable-upcoming-feature ForwardTrailingClosures -enable-upcoming-feature ImplicitOpenExistentials -enable-upcoming-feature StrictConcurrency -warn-concurrency -enable-actor-data-race-checks

//:configuration = Release
OTHER_SWIFT_FLAGS = -enable-upcoming-feature BareSlashRegexLiterals -enable-upcoming-feature ConciseMagicFile -enable-upcoming-feature ExistentialAny -enable-upcoming-feature ForwardTrailingClosures -enable-upcoming-feature ImplicitOpenExistentials -enable-upcoming-feature StrictConcurrency -warn-concurrency -enable-actor-data-race-checks

//:completeSettings = some
OTHER_SWIFT_FLAGS</code></pre><p><img src="/assets/images/blog/preparing-for-swift-6/enableupcomingfeature-is.gif" alt=".enableUpcomingFeature is only available from Swift 5.8" loading="lazy" /></p><p>Wenn du wie ich eine mit SwiftPM modularisierte App verwendest oder an einem Swift-Package arbeitest, musst du zusaetzlich ein Array von <code>.enableUpcomingFeature</code>s an jedes Target ueber <code>swiftSettings</code> uebergeben:</p><pre><code class="language-Swift">let swiftSettings: [SwiftSetting] = [
   .enableUpcomingFeature(&quot;BareSlashRegexLiterals&quot;),
   .enableUpcomingFeature(&quot;ConciseMagicFile&quot;),
   .enableUpcomingFeature(&quot;ExistentialAny&quot;),
   .enableUpcomingFeature(&quot;ForwardTrailingClosures&quot;),
   .enableUpcomingFeature(&quot;ImplicitOpenExistentials&quot;),
   .enableUpcomingFeature(&quot;StrictConcurrency&quot;),
   .unsafeFlags([&quot;-warn-concurrency&quot;, &quot;-enable-actor-data-race-checks&quot;]),
]

let package = Package(
   // ...
   targets: [
      // ...
      .target(
         name: &quot;MyTarget&quot;,
         dependencies: [/* ... */],
         swiftSettings: swiftSettings
      ),
      // ...
   ]</code></pre><p>Vergiss nicht, deine Tools-Version am Anfang der Datei auf <code>5.8</code> zu aktualisieren:</p><pre><code class="language-Swift">// swift-tools-version:5.8</code></pre><p><em><code>.enableUpcomingFeature</code> ist erst ab Swift <code>5.8</code> verfuegbar</em></p><p>Das liegt daran, dass Swift die Optionen, die du fuer dein Projekt-Target angegeben hast, nicht automatisch an importierte Module weitergibt. Und das sind gute Neuigkeiten, denn so muessen Swift-Packages, die du moeglicherweise in dein Projekt einbindest, nicht angepasst werden, und du kannst diese Features trotzdem fuer deinen eigenen App-Code nutzen. Und umgekehrt: Package-Autoren koennen diese Features fuer ihre Projekte aktivieren, ohne den Code in Projekten zu beeinflussen, in die sie eingebunden werden.</p><p>Nachdem ich alles aktiviert und gebaut hatte, stiess ich auf vier Arten von Problemen:</p><ol><li><p>Ich musste an mehreren Stellen das <code>any</code>-Keyword hinzufuegen – Xcode half mit einem Fix-It:</p></li></ol><p><img src="/assets/images/blog/preparing-for-swift-6/migrating-my-project.webp" alt="Migrating my project" loading="lazy" /></p><p><img src="/assets/images/blog/preparing-for-swift-6/migrating-my-project-2.webp" alt="Migrating my project 2" loading="lazy" /></p><ol><li><p>Aus irgendeinem Grund erhielt ich viele Fehler bei Views mit einem <code>.sheet</code>-Modifier. Die Fehlerpaare besagten: “Generic parameter ‘Content’ could not be inferred” und “Missing argument for parameter ‘content’ in call”. Diese Meldungen waren aber nicht sehr hilfreich, also versuchte ich zuerst, den <code>.sheet</code>-Inhalt durch eine einfache <code>Text</code>-View zu ersetzen – aber das half nicht. Also habe ich die Optionen einzeln deaktiviert und die Fehler verschwanden, als ich <code>ForwardTrailingClosures</code> ausschaltete – also liess ich es deaktiviert. Ich hoffe, dass zukuenftige Swift-Versionen eine bessere Fehlermeldung erzeugen, um die Fehler spaeter zu beheben. Es eilt nicht. Ich kann es mit Swift 5.9 erneut versuchen. Ich hatte jetzt keine Zeit zum Untersuchen.</p></li></ol><p><img src="/assets/images/blog/preparing-for-swift-6/migrating-my-project-3.webp" alt="Migrating my project 3" loading="lazy" /></p><ol><li><p>Ich musste einige meiner Funktionen, die einen <a href="https://github.com/pointfreeco/swift-composable-architecture">TCA</a>-<code>WithViewStore</code> zurueckgeben, mit dem <code>@MainActor</code>-Attribut markieren – aber auch hier half Xcode mit Fix-Its.</p></li></ol><p><img src="/assets/images/blog/preparing-for-swift-6/migrating-my-project-4.webp" alt="Migrating my project 4" loading="lazy" /></p><ol><li><p>An vielen Stellen wurden Warnungen angezeigt: “Non-sendable type ‘…’ passed in call to main actor-isolated function cannot cross actor boundary”. Also habe ich diese Typen dem <code>Sendable</code>-Protokoll konform gemacht (mehr ueber <code>Sendable</code> <a href="https://www.hackingwithswift.com/swift/5.5/sendable">hier</a>).</p></li></ol><p>Alles andere schien fuer meine App <a href="https://remafox.app/">RemafoX</a> mit ~35.000 Zeilen Swift-Code problemlos zu bauen. Der gesamte Prozess hat mich weniger als 3 Stunden gekostet.</p><p>Mein Projekt ist jetzt bereit fuer die Zukunft von Swift und ich kann das neue Regex-Literal-Feature nutzen (<code>let regex = /.*@.*/</code>). Ausserdem kann ich keinen <em>neuen</em> Code einfuehren, den ich spaeter zu Swift 6 migrieren muesste, da ich sofort Fehler erhalte.</p><p>Wie sieht es bei deinem Projekt aus? Welche Upcoming Features moechtest du migrieren?</p>]]></content:encoded>
</item>
<item>
<title>Binding: Equatable vs EquatableBinding</title>
<link>https://fline.dev/de/blog/binding-equatable-vs-equatablebinding/</link>
<guid isPermaLink="true">https://fline.dev/de/blog/binding-equatable-vs-equatablebinding/</guid>
<pubDate>Thu, 13 Apr 2023 00:00:00 +0000</pubDate>
<description><![CDATA[Wie ich einen subtilen Bug in SwiftUI-Pickern in meiner App behoben habe, indem ich einen Property Wrapper statt einer Equatable-Konformitaet fuer Binding verwendet habe.]]></description>
<content:encoded><![CDATA[<p>Es klingt vielleicht kontraintuitiv, warum ich das getan habe, aber in meiner App <a href="https://remafox.app/">RemafoX</a> musste ich den <code>Binding</code>-Typ, der mit SwiftUI ausgeliefert wird, um Aenderungen in Views an Daten zu binden, dem <code>Equatable</code>-Protokoll konform machen, damit ich ein Binding-Objekt in meiner Datenschicht herumreichen konnte. Die Implementierung der Extension sah so aus:</p><pre><code class="language-Swift">extension Binding: Equatable where Value: Equatable {
   static func == (left: Binding&lt;Value&gt;, right: Binding&lt;Value&gt;) -&gt; Bool {
      left.wrappedValue == right.wrappedValue
   }
}</code></pre><p>Obwohl ich mit dieser Loesung nicht zu 100% zufrieden war, funktionierte sie einwandfrei, als ich sie urspruenglich entwickelte – also habe ich sie ausgeliefert. Aber dann begann mit irgendeinem macOS-Update ein Bug in alle Picker-Views meiner App einzuschleichen. Die Picker funktionierten zwar die meiste Zeit noch, verhielten sich aber manchmal seltsam. Das einzige Verhalten, das ich immer reproduzieren konnte, war: Wenn ich einen ausgewaehlten Wert programmatisch auf das Binding setzte und der Nutzer ihn dann spaeter auf einen anderen Wert aenderte, wurden beide Werte mit einem Haekchen im Picker-Dropdown angezeigt – was wahrscheinlich ein Bug irgendwo in SwiftUI ist:</p><p><img src="/assets/images/blog/binding-equatable-vs-equatablebinding/picker-double-checkmark.gif" alt="" loading="lazy" /></p><p>Es hat mich Stunden des Code-Auskommentierens gekostet, um die Ursache zu finden, und es stellte sich heraus, dass es die oben erwaehnte <code>Binding</code>-Extension war. Irgendwie scheint ihre Definition in meiner App die interne Implementierung der <code>Picker</code>-View in SwiftUI beeinflusst zu haben. Und wenn man das weiss – wer weiss, welche anderen Seiteneffekte sie moeglicherweise verursacht hat oder in zukuenftigen System-Updates verursachen koennte? Ich musste also eindeutig eine bessere Loesung finden, die das Verhalten in SwiftUI-Views nicht beeinflussen sollte.</p><p>Der Grund, warum ich <code>Binding</code> <code>Equatable</code>-konform machen wollte, war, dass ich Anforderungen an meine Datenschicht hatte – naemlich bestimmte <code>State</code>-Typen in der <a href="https://github.com/pointfreeco/swift-composable-architecture">TCA-Architektur</a> –, die verlangten, dass alle Daten, die ich darin speichere, ebenfalls <code>Equatable</code> konformieren. Etwas wie:</p><pre><code class="language-Swift">struct AppState: Equatable {
   // other properties

   var configFile: Binding&lt;ConfigFile&gt;
}</code></pre><hr /><blockquote><p>Moechtest du hier deine Werbung sehen? Kontaktiere mich unter <a href="mailto:ads@fline.dev">ads@fline.dev</a>.</p></blockquote><hr /><p>Die Loesung, die ich gefunden habe, ist recht einfach, auch wenn ich erst “lernen” musste, wie man einen Property Wrapper schreibt, da es das erste Mal war, dass ich einen eigenen brauchte. Aber es war unkompliziert – ich glaube, du wirst es verstehen, auch wenn du noch nie einen geschrieben hast:</p><pre><code class="language-Swift">@propertyWrapper
public struct EquatableBinding&lt;Wrapped: Equatable&gt;: Equatable {
   public var wrappedValue: Binding&lt;Wrapped&gt;

   public init(wrappedValue: Binding&lt;Wrapped&gt;) {
      self.wrappedValue = wrappedValue
   }

   public static func == (left: EquatableBinding&lt;Wrapped&gt;, right: EquatableBinding&lt;Wrapped&gt;) -&gt; Bool {
      left.wrappedValue.wrappedValue == right.wrappedValue.wrappedValue
   }
}</code></pre><p>Die einzige Anforderung des <code>@propertyWrapper</code> ist die <code>wrappedValue</code>-Property, und weil ich diesen Wrapper in meiner gesamten modularisierten Anwendung teilen wollte, musste ich auch einen public Initializer schreiben – aber alles ist unkompliziert. Die <code>==</code>-Funktion ist die einzige Anforderung des <code>Equatable</code>-Protokolls und auch sie ist unkompliziert.</p><p>Damit kann ich jetzt alle meine <code>Binding</code>-Properties mit <code>@EquatableBinding</code> markieren:</p><pre><code class="language-Swift">struct AppState: Equatable {
   // other properties

   @EquatableBinding&lt;ConfigFile&gt;
   var configFile: Binding&lt;ConfigFile&gt;
}</code></pre><p>Und das war’s – der Typ <code>AppState</code> ist jetzt vollstaendig <code>Equatable</code>, das seltsame Dropdown-Problem ist geloest, und es besteht kein Risiko fuer Seiteneffekte, weil ich keine bestehenden Typen aus anderen Frameworks mit neuen Protokollen konform mache. Stattdessen habe ich einfach einen <em>neuen</em> Typ eingefuehrt, von dem andere Frameworks nicht einmal wissen – sie koennen also nicht davon betroffen sein.</p><p>Die Lektion daraus: Erweitere nie Typen, die du nicht besitzt, um Protokolle, die du nicht besitzt. Es gibt sogar einen <a href="https://www.fline.dev/swift-evolution-monthly-july-22/#se-0364-warning-for-retroactive-conformances-of-external-types">Swift-Vorschlag</a>, der das zu einer Compiler-Warnung machen soll. Beachte, dass die Loesung nicht <em>immer</em> ein Property Wrapper ist. Es gibt viele Moeglichkeiten, eigene Typen einzubeziehen, wenn man Protokolle konformiert – vergiss einfach nicht, es zu tun.</p><blockquote><p><strong>Was ist RemafoX?</strong>
Eine native Mac-App, die sich in Xcode integriert, um beim Uebersetzen <em>deiner</em> App zu helfen.
<a href="https://apps.apple.com/app/apple-store/id1605635026?pt=549314&ct=fline.dev&mt=8"><strong>Jetzt herunterladen</strong></a>, um beim Entwickeln Zeit zu sparen und Lokalisierung einfach zu machen.</p></blockquote>]]></content:encoded>
</item>
<item>
<title>Migration auf The Composable Architecture (TCA) 1.0</title>
<link>https://fline.dev/de/blog/migrating-to-tca-1-0/</link>
<guid isPermaLink="true">https://fline.dev/de/blog/migrating-to-tca-1-0/</guid>
<pubDate>Mon, 03 Apr 2023 00:00:00 +0000</pubDate>
<description><![CDATA[Meine Erkenntnisse und meine Code-Struktur nach der Migration meiner App auf die grundlegend modernisierten APIs von TCA 1.0.]]></description>
<content:encoded><![CDATA[<h2 id="intro-und-ergebnisse">Intro und Ergebnisse</h2><p>Ich habe gerade meine App <a href="https://remafox.app/">RemafoX</a> migriert, die auf <a href="https://github.com/pointfreeco/swift-composable-architecture">The Composable Architecture</a> (TCA) Version <code>0.35.0</code> aufgebaut war, auf den neuen API-Stil von <code>1.0</code>. Obwohl zwischen dem Release von <code>0.35.0</code> und der aktuellen Beta von <code>1.0</code> weniger als ein Jahr liegt, ist es wichtig zu beachten, dass in diesem kurzen Zeitraum nicht weniger als <a href="https://github.com/pointfreeco/swift-composable-architecture/releases">27 Feature-Releases</a> mit teilweise erheblichen Aenderungen veroeffentlicht wurden. Das Point-Free-Team arbeitet wirklich auf Hochtouren daran, die App-Entwicklung fuer Swift-Entwickler zu verbessern, und TCA ist die Kulmination des Grossteils ihrer Arbeit. Nahezu jedes Thema, das sie in ihrer <a href="https://www.pointfree.co/">grossartigen Advanced-Swift-Videoreihe</a> besprechen, hat Auswirkungen auf TCA. Obwohl sie es geschafft haben, alle Aenderungen bis zur aktuellen Version <code>0.52.0</code> weitgehend quellcode-kompatibel zu halten, wird das <code>1.0</code>-Release <a href="https://www.pointfree.co/blog/posts/103-composable-architecture-1-0-preview">viele aeltere APIs entfernen</a>, die schon seit einiger Zeit als deprecated markiert sind.</p><p>Weil ich es sehr ineffizient finde, staendig jede Entscheidung bezueglich der Architektur oder der Konventionen meines App-Codes zu hinterfragen, habe ich in den letzten Monaten nicht viel Zeit investiert, um alle Verbesserungen an TCA nachzuholen – auch wenn ich immer ein Auge darauf hatte, um die allgemeine Richtung mitzubekommen. Aber das nahende Release der Meilenstein-Version <code>1.0</code> der Library und die Tatsache, dass ich als Naechstes an einigen <a href="https://github.com/FlineDev/RemafoX/issues/13">groesseren</a> <a href="https://github.com/FlineDev/RemafoX/issues/22">Features</a> fuer RemafoX arbeiten will, machen es zu einem guten Zeitpunkt, um neu zu ueberlegen und zu lernen, wie ich meine Apps kuenftig am besten strukturiere.</p><p>Gluecklicherweise hat sich das grundlegende Konzept von TCA ueberhaupt nicht geaendert. Aber die APIs zur Beschreibung, wie Features verbunden werden, wie Navigation funktionieren soll, wie asynchrone Arbeit deklariert wird und sogar wie Dependencies weitergegeben werden – all das hat seitdem erhebliche Aenderungen erfahren, alles zum Besseren, unter Nutzung der neuesten Swift-Features. Es gab also viel herauszufinden und zu migrieren, und ich habe alle diese Aenderungsbereiche auf einmal angegangen. Um es aber handhabbar zu halten, habe ich die Aenderungen Modul fuer Modul auf alle 33 UI-Features meiner <a href="https://www.pointfree.co/episodes/ep171-modularization-part-1">mit SwiftPM modularisierten</a> App angewendet.</p><p>Hier sind die wichtigsten Erkenntnisse des Migrationsprozesses vorab:</p><ol><li><p>Es hat mich eine <strong>volle Arbeitswoche</strong> (~5 Tage) gekostet, die Migration abzuschliessen.</p></li><li><p>Meine Codebasis ist um <strong>2.500 Zeilen Code geschrumpft</strong>, was einer Reduktion von ~7% entspricht.</p></li><li><p>Einige Navigations-Bugs, Threading-Probleme und SwiftUI-Glitches sind jetzt <strong>behoben</strong>.</p></li><li><p>Mein <strong>Code ist deutlich einfacher</strong> zu verstehen, zu navigieren und nachzuvollziehen.</p></li></ol><p>Was die Teststory meiner App angeht: Alle meine Tests bestehen tatsaechlich weiterhin und ich musste keinerlei Aenderungen am Testcode vornehmen. Der Grund dafuer ist, dass ich derzeit nur Tests fuer Nicht-UI-Features habe, wie das Parsen von Daten, das Suchen nach Dateien oder das Vornehmen von Aenderungen an Strings-Dateien – und in einigen Teilen recht umfangreiche Tests. Als ich aber auch Tests fuer mein UI in Betracht zog, lag ich bereits Monate hinter meiner urspruenglichen Timeline fuer das App-Release, und zusaetzlich war TCA noch einige Wochen davon entfernt, <a href="https://www.pointfree.co/blog/posts/83-non-exhaustive-testing-in-the-composable-architecture">nicht-erschoepfendes Testen</a> zu unterstuetzen. Also entschied ich mich gegen das Hinzufuegen von UI-Tests, da ich nicht wirklich zufrieden damit war, wie oft man Tests aendern musste, nur wegen eines Refactorings auf der UI-Ebene, das das allgemeine Verhalten eigentlich nicht aenderte, aber gefuehlt ein Umschreiben der zugehoerigen Tests erforderte, weil sie so erschoepfend waren. Aber mit dem jetzt verfuegbaren nicht-erschoepfenden Testen plane ich, Schritt fuer Schritt UI-Tests fuer meine App zu schreiben, angefangen mit den wichtigsten: Mein Feature mit der meisten Business-Logik und alle meine Onboarding-Features. Moeglicherweise schreibe ich darueber in einem zukuenftigen Artikel.</p><p>Aber fuer jetzt konzentrieren wir uns darauf, wie ich die Migration meiner App-Codebasis angegangen bin.</p><h2 id="vor-der-migration-praxisbeispiel">Vor der Migration (Praxisbeispiel)</h2><p>Ich denke, der beste Weg zu erklaeren, welche Aenderungen noetig waren und welche weiteren Aenderungen ich zur Vereinfachung vorgenommen habe, ist, echten Code zu zeigen. Im Folgenden zeige ich dir also, wie der tatsaechliche Code des einfachsten Features meiner App vor der Migration aussah und wie ich ihn zum neuen TCA-<code>1.0</code>-Stil weiterentwickelt habe.</p><p>Das Feature heisst in meiner Codebasis <code>AppInfo</code> und sieht in der App so aus:</p><p><img src="/assets/images/blog/migrating-to-tca-1-0/the-about-remafox-screen.webp" alt="The " loading="lazy" /></p><p><em>Der “About RemafoX”-Screen (Cmd+I).</em></p><p>Vor der Migration war der Feature-Code auf 7 verschiedene Dateien aufgeteilt:</p><p><img src="/assets/images/blog/migrating-to-tca-1-0/feature-parts-action.webp" alt="Feature parts: Action, ActionHandler, Error, Event, Reducer, State, and View." loading="lazy" /></p><p><em>Feature-Teile: <code>Action</code>, <code>ActionHandler</code>, <code>Error</code>, <code>Event</code>, <code>Reducer</code>, <code>State</code> und <code>View</code>.</em></p><p><code>AppInfoState</code> und <code>AppInfoAction</code> definieren die Daten und moeglichen Interaktionen:</p><pre><code class="language-Swift">import AppFoundation
import AppUI

public struct AppInfoState: Equatable {
   public typealias Action = AppInfoAction
   public typealias Error = AppInfoError

   @BindingState
   var showEnvInfoCopiedToClipboard: Bool = false
   var selectedAppIcon: AppIcon

   var errorHandlingState: ErrorHandlingState?

   public init() {
      self.selectedAppIcon = Defaults[.selectedAppIcon]
   }
}</code></pre><p><em>AppInfoState.swift</em></p><pre><code class="language-Swift">import AppFoundation
import AppUI

public enum AppInfoAction: Equatable, BindableAction {
   public typealias State = AppInfoState
   public typealias Error = AppInfoError

   case onAppear
   case onDisappear
   case selectedAppIconChanged
   case copyEnvironmentInfoPressed

   case binding(BindingAction&lt;State&gt;)

   case errorOccurred(error: Error)
   case setErrorHandling(isPresented: Bool)
   case errorHandling(action: ErrorHandlingAction)
}</code></pre><p><em>AppInfoAction.swift</em></p><p>Beachte, dass ich immer Typealiases fuer verwandte Teile des Features definiert habe, auf die ich irgendwo innerhalb der Typen verweisen koennte – auch wenn ich sie nicht wirklich genutzt habe. Ausserdem hatte ich eine zusaetzliche Action <code>set&lt;Name des Childs&gt;(isPresented:)</code>, wann immer ich eine Child-View hatte, die ich spaeter per Sheet praesentieren wollte. Falls du dich fragst, was diese Imports von <code>AppFoundation</code> und <code>AppUI</code> sind – das habe ich in <a href="https://www.fline.dev/organizing-my-swiftpm-modules/">diesem Artikel</a> erklaert. Sie helfen, die Anzahl der Imports in meiner App zu reduzieren.</p><p>Als Naechstes schauen wir uns an, wie die <code>AppInfoView</code>-Datei aussieht:</p><pre><code class="language-Swift">import AppFoundation
import AppUI

public struct AppInfoView: View {
   public typealias State = AppInfoState
   public typealias Action = AppInfoAction

   let store: Store&lt;State, Action&gt;

   public init(store: Store&lt;State, Action&gt;) {
      self.store = store
   }

   public var body: some View {
      WithViewStore(self.store) { viewStore in
         VStack(alignment: .leading, spacing: 20) {
            VStack(alignment: .center, spacing: 10) {
               viewStore.selectedAppIcon.image
                  .resizable()
                  .aspectRatio(contentMode: .fit)
                  .frame(width: 128, height: 128)
                  .onChange(of: Defaults[.selectedAppIcon]) { newValue in
                     viewStore.send(.selectedAppIconChanged)
                  }

               Text(Constants.appDisplayName)
                  .font(.system(size: 33, weight: .light, design: .rounded))

               Text(&quot;Copyright © 2022 Cihat Gündüz&quot;)
                  .font(.footnote)
                  .foregroundColor(.secondary)
            }
            .frame(maxWidth: .infinity)

            Divider()

            VStack(alignment: .center, spacing: 10) {
               Text(&quot;Environment Info&quot;)
                  .font(.headline)

               Text(&quot;Provide these info when reporting bugs or use Help menu.&quot;)
                  .frame(maxWidth: .infinity, alignment: .leading)
                  .font(.subheadline)
                  .padding(.bottom, 5)

               HStack {
                  Text(&quot;App Version:&quot;).foregroundColor(.secondary)
                  Spacer()
                  Text(Bundle.main.versionInfo)
               }

               HStack {
                  Text(&quot;System Version:&quot;).foregroundColor(.secondary)
                  Spacer()
                  Text(ProcessInfo.processInfo.operatingSystemVersionString.replacingOccurrences(of: &quot;Version &quot;, with: &quot;&quot;))
               }

               HStack {
                  Text(&quot;System CPU:&quot;).foregroundColor(.secondary)
                  Spacer()
                  Text(KernelState.getStringValue(for: .cpuBrandString))
               }

               HStack {
                  Text(&quot;Tier:&quot;).foregroundColor(.secondary)
                  Spacer()
                  Text(Plan.loadCurrent().tier.displayName)
               }

               Button {
                  viewStore.send(.copyEnvironmentInfoPressed)
               } label: {
                  Label(&quot;Copy&quot;, systemSymbol: .docOnClipboard)
               }
               .padding(.top, 10)
               .popover(isPresented: viewStore.binding(\.$showEnvInfoCopiedToClipboard), arrowEdge: Edge.top) {
                  Text(&quot;Copied!&quot;).padding(10)
               }
            }
         }
         .frame(width: 320)
         .padding()
         .onAppear { viewStore.send(.onAppear) }
         .onDisappear { viewStore.send(.onDisappear) }
         .sheet(
            isPresented: viewStore.binding(
               get: { $0.errorHandlingState != nil },
               send: Action.setErrorHandling(isPresented:)
            )
         ) {
            IfLetStore(
               self.store.scope(state: \State.errorHandlingState, action: Action.errorHandling(action:)),
               then: ErrorHandlingView.init(store:)
            )
         }
      }
   }
}

#if DEBUG
   struct AppInfoView_Previews: PreviewProvider {
      static let store = Store(
         initialState: .init(),
         reducer: appInfoReducer,
         environment: .mocked
      )

      static var previews: some View {
         AppInfoView(store: self.store)
      }
   }
#endif</code></pre><p><em>AppInfoView.swift</em></p><p>Beachte, dass ich fuer das Praesentieren eines Sheets nicht weniger als 11 Zeilen Code brauche und die Action <code>setErrorHandling(isPresented:)</code> manuell zurueck ins System senden muss. Erfahrene Entwickler werden auch bemerken, dass ich tatsaechlich globale Dependencies in meinem View-Code verwende, zum Beispiel mit <code>Plan.loadCurrent()</code>, was meinen UI-Code nicht besonders testbar macht. Aber ich werde sie als richtige Dependencies einfuehren, sobald ich mit dem Schreiben von UI-Tests beginne – ignorieren wir das fuer jetzt also.</p><p>Das letzte fehlende Puzzleteil eines Features in TCA ist der <code>AppInfoReducer</code>:</p><pre><code class="language-Swift">import AppFoundation
import AppUI

public let appInfoReducer = AnyReducer.combine(
   errorHandlingReducer
      .optional()
      .pullback(
         state: \AppInfoState.errorHandlingState,
         action: /AppInfoAction.errorHandling(action:),
         environment: { $0 }
      ),
   AnyReducer&lt;AppInfoState, AppInfoAction, AppEnv&gt; { state, action, env in
      let actionHandler = AppInfoActionHandler(env: env)

      switch action {
      case .onAppear, .onDisappear:
         return .none  // for analytics only

      case .selectedAppIconChanged:
         return actionHandler.selectedAppIconChanged(state: &amp;state)

      case .copyEnvironmentInfoPressed:
         return actionHandler.copyEnvironmentInfoPressed(state: &amp;state)

      case .binding:
         return .none  // assignment handled by `.binding()` below

      case .errorOccurred, .setErrorHandling, .errorHandling:
         return actionHandler.handleErrorAction(state: &amp;state, action: action)
      }
   }
   .binding()
   .recordAnalyticsEvents(eventType: AppInfoEvent.self) { state, action, env in
      switch action {
      case .onAppear:
         return .init(event: .onAppear)

      case .onDisappear:
         return .init(event: .onDisappear)

      case .copyEnvironmentInfoPressed:
         return .init(event: .copyEnvironmentInfoPressed)

      case .errorOccurred(let error):
         return .init(event: .errorOccurred, attributes: [&quot;errorCode&quot;: error.errorCode])

      case .binding, .setErrorHandling, .errorHandling, .selectedAppIconChanged:
         return nil
      }
   }
)</code></pre><p>Beachte zuerst, dass der <code>appInfoReducer</code> auf globaler Ebene definiert ist, was sich schon falsch anfuehlt. Dann werden 7 Zeilen benoetigt, um das Child-Feature <code>ErrorHandling</code> mit diesem Feature zu verbinden. Und dir wird auffallen, dass ich einen weiteren Typ namens <code>AppInfoActionHandler</code> eingefuehrt habe, der die eigentliche Logik des Reducers enthaelt. Der Grund dafuer ist, dass ein Teil meiner Reducer-Logik recht lang ist und wenn ich die gesamte Logik im <code>switch-case</code> behalten wuerde, haette ich viele Cases mit viel Code darin. Xcode bietet aber keine Features, um innerhalb von <code>switch</code>-Cases zu navigieren. Also habe ich die Logik in Funktionen eines anderen Typs ausgelagert. Schliesslich wirst du bemerken, dass ich eine Extension auf dem <code>AnyReducer</code>-Typ selbst fuer Analytics-Zwecke definiert habe:</p><pre><code class="language-Swift">import ComposableArchitecture

extension AnyReducer {
   /// Returns a `Result` where each action coming to the store first attempts to record an analytics event.
   /// In the implementation, switch over `action` and return an ``Analytics.AttributedEvent`` if the action should be recorded, or else return `nil`.
   public func recordAnalyticsEvents&lt;Event: AnalyticsEvent&gt;(
      eventType: Event.Type,
      event toAttributedEvent: @escaping (State, Action, Environment) -&gt; Analytics.AttributedEvent&lt;Event&gt;?
   ) -&gt; Self {
      .init { state, action, env in
         guard let attributedEvent = toAttributedEvent(state, action, env) else { return self.run(&amp;state, action, env) }

         return .concatenate(
            .fireAndForget { Analytics.shared.record(attributedEvent: attributedEvent) },
            self.run(&amp;state, action, env)
         )
      }
   }
}</code></pre><p><em>AnyReducerExt.swift</em></p><p>Das Einzige, was das tut, ist ein Event in meiner Analytics-Engine zu erfassen, die von <a href="https://telemetrydeck.com/?source=fline.dev">TelemetryDeck</a> angetrieben wird, fuer die Actions, die ich aufzeichnen moechte. Ich finde das sehr nuetzlich als Erinnerung, bei jeder neuen Action, die ich zum <code>AppInfoAction</code>-Enum hinzufuege, immer zu ueberlegen, ob ich das neue Event auf voellig anonymisierte Weise analysieren moechte. Damit das richtig funktioniert, muss ich auch fuer jedes Feature einen weiteren Typ definieren, hier <code>AppInfoEvent</code>:</p><pre><code class="language-Swift">import AppFoundation

enum AppInfoEvent: String {
   case onAppear
   case onDisappear
   case copyEnvironmentInfoPressed
   case errorOccurred
}

extension AppInfoEvent: AnalyticsEvent {
   var idComponents: [String] {
      [&quot;AppInfo&quot;, self.rawValue]
   }
}</code></pre><p><em>AppInfoEvent.swift</em></p><p>Dieses Enum definiert alle Events, die ich erfassen moechte, und die <code>idComponents</code>-Property hilft beim automatischen Erstellen eines Strings, wenn ein Event-Name an meinen Analytics-Anbieter uebergeben wird. Das <code>AnalyticsEvent</code>-Protokoll ist etwas am Thema vorbei, aber falls es dich interessiert – es ist einfach das hier:</p><pre><code class="language-Swift">import Foundation

public protocol AnalyticsEvent: Identifiable where ID == String {
   var idComponents: [String] { get }
}

extension AnalyticsEvent {
   public var id: String {
      self.idComponents.joined(separator: &quot;.&quot;)
   }
}</code></pre><p><em>AnalyticsEvent.swift (Teil eines Hilfsmoduls namens <code>Analytics</code>)</em></p><p>Dir ist vielleicht auch ein <code>AppEnv</code>-Typ aufgefallen, den ich fuer die Environment verwende. Das ist eigentlich ein gemeinsamer Typ, den ich ueberall wiederverwendet habe, wo ich nur einen einfachen Environment-Typ mit einer <code>mainQueue</code> brauchte und der ueberall in meiner Anwendung herumgereicht wird:</p><pre><code class="language-Swift">import CombineSchedulers
import Defaults
import Foundation

public struct AppEnv {
   public let mainQueue: AnySchedulerOf&lt;DispatchQueue&gt;

   public init(mainQueue: AnySchedulerOf&lt;DispatchQueue&gt;) {
      self.mainQueue = mainQueue
   }
}

#if DEBUG
   extension AppEnv {
      public static var mocked: AppEnv {
         .init(mainQueue: DispatchQueue.main.eraseToAnyScheduler())
      }
   }
#endif</code></pre><p>Nun, die letzte der 7 Dateien ist <code>AppInfoError</code> und dieser Typ ist fuer dieses sehr einfache Feature tatsaechlich leer. Aber ich werde seinen Zweck in einem spaeteren Artikel erklaeren, in dem ich meinen Ansatz zur Fehlerbehandlung im Detail beschreibe. Alles, was du fuer diesen Artikel wissen musst: Wenn etwas Unerwartetes passiert, moechte ich ein Sheet mit hilfreichen Informationen direkt im Kontext eines Features anzeigen.</p><p>Point-Free neigt dazu, alle ihre Typen in einer einzigen Datei zu behalten, was fuer ein kleines Feature wie <code>AppInfo</code> funktionieren mag. Aber ein typisches Feature von mir umfasst etwa 500 bis 1.500 Zeilen Code mit allen Typen zusammen. Ich neige dazu, meine Dateien klein zu halten, mit einem Soft Cap von 400 Zeilen und einem Hard Cap von 1.000 Zeilen (siehe <a href="https://realm.github.io/SwiftLint/file_length.html">SwiftLint-Regelstandards</a>). Mit 314 Zeilen wuerde selbst dieses sehr einfache Feature schon nahe an das Soft Cap kommen, und einige meiner Features wuerden sogar ueber das Hard Cap hinausgehen. Alles in einer Datei zu halten ist also fuer mich ein No-Go. Deshalb habe ich stattdessen jeden Typ in eine eigene Datei gelegt. Aber ich war auch nie 100% zufrieden damit, weil alles etwas verstreut wirkte. Im Idealfall waere zusammengehoeriger Code immer noch beieinander, aber das Feature waere trotzdem gleichmaessig auf weniger als 7 Dateien verteilt. Schauen wir also, wie die Dinge nach der Migration aussehen.</p><hr /><blockquote><p>Moechtest du hier deine Werbung sehen? Kontaktiere mich unter <a href="mailto:ads@fline.dev">ads@fline.dev</a>.</p></blockquote><hr /><h2 id="nach-der-migration-praxisbeispiel">Nach der Migration (Praxisbeispiel)</h2><p>In der TCA-<code>1.0</code>-Beta hat Point-Free das Konzept eingefuehrt, ein spezielles <code>struct</code> zu erstellen, das als Scope oder Namespace fuer ein Feature dient, und Hilfstypen wie <code>State</code> und <code>Action</code> als Subtypen in diesen Namespace-Typ zu legen. Sie haben diesem Namespace den Namen <code>Feature</code> gegeben und ihn <code>Reducer</code>-konform gemacht, was in etwa so aussieht:</p><pre><code class="language-Swift">struct Feature: Reducer {
  struct State: Equatable { … }
  enum Action: Equatable { … }

  func reduce(into state: inout State, action: Action) -&gt; Effect&lt;Action&gt; { … }
}</code></pre><p>Fuer mich ist diese Struktur aber aus zwei Gruenden verwirrend:</p><ol><li><p>Den Namespace mit dem Suffix <code>Feature</code> zu benennen, waehrend er <code>Reducer</code>-konform ist, erscheint mir unstimmig. Ueberall, wo ein <code>reducer</code>-Parameter uebergeben werden muss, wuerden wir ein <code>Feature</code> uebergeben – was verwirrend waere. Der Namespace muesste dann eher <code>Reducer</code> heissen, aber dann haetten wir Subtypen ueber <code>Reducer.State</code> und <code>Reducer.Action</code>, was auch nicht korrekt ist.</p></li><li><p>Wenn man die Idee eines Feature-Namespaces voll annimmt, wuerde ich einen Typ <code>Reducer</code> innerhalb des <code>Feature</code>-Typs der Konsistenz halber erwarten.</p></li></ol><p>Stattdessen habe ich mich dafuer entschieden, das <code>Feature</code> tatsaechlich als Namespace zu verwenden und auch einen <code>Reducer</code>-Subtyp hineinzusetzen, der <code>Reducer</code>-konform ist. Nun, das wuerde zu <code>struct Reducer: Reducer</code> fuehren – ein Namenskonflikt. Loesen wir das mit einem Typealias:</p><pre><code class="language-Swift">import ComposableArchitecture

public typealias FeatureReducer: Reducer</code></pre><p>Jetzt koennten wir einen <code>Reducer: FeatureReducer</code>-Subtyp in unserem <code>Feature</code>-Namespace definieren. Und wo wir schon dabei sind – ich vergesse tatsaechlich oft, einen public Initializer fuer meine Reducer zu schreiben (was noetig ist, da die App modularisiert ist), also definieren wir stattdessen ein neues oeffentliches Protokoll, das einen public Initializer verlangt:</p><pre><code class="language-Swift">import ComposableArchitecture

public protocol FeatureReducer: Reducer {
   init()
}</code></pre><p>Tatsaechlich gibt es noch mehr Dinge, die ich bei anderen TCA-Feature-Typen gerne vergesse. Machen wir das alles zu einer klaren Anforderung, indem wir Protokolle wie <code>FeatureReducer</code> fuer alle Arten von Subtypen innerhalb eines <code>Feature</code> implementieren:</p><pre><code class="language-Swift">import Analytics
import ComposableArchitecture
import ErrorHandling
import SwiftUI

public protocol FeatureState: Equatable {
   var childErrorHandling: ErrorHandlingFeature.State? { get set }
}

public protocol FeatureAction: Equatable {
   associatedtype ErrorType: FeatureError

   static func errorOccurred(_ error: ErrorType) -&gt; Self
   static func childErrorHandling(_ action: PresentationAction&lt;ErrorHandlingFeature.Action&gt;) -&gt; Self
}

public protocol FeatureEvent: AnalyticsEvent {}

public protocol FeatureError: HelpfulError {}

public protocol FeatureReducer: Reducer {
   init()
}

public protocol FeatureView: View {
   associatedtype Action: FeatureAction
}</code></pre><p><em>Auszug aus Feature.swift (Teil eines Hilfsmoduls)</em></p><p>Beachte, dass ich verlange, dass <code>FeatureState</code> und <code>FeatureAction</code> <code>Equatable</code> sind, was in TCA immer eine gute Idee ist, um sie testbar zu machen – und alle meine State- und Action-Typen konformieren bereits dazu. Zusaetzlich habe ich <code>FeatureView</code> entsprechend definiert, plus die zwei zusaetzlichen Typen, die ich fuer meine Analytics- und Error-Handling-Beduerfnisse brauche. Beachte auch, dass ich mich entschieden habe, statt dem Suffix <code>State</code> bei allen Child-Features (wie bei <code>errorHandlingState</code> im Feature zuvor), das Praefix <code>child</code> zu verwenden – wie in <code>childErrorHandling</code>. Das erleichtert das Finden von Child-Features beim Scannen der Attribute von oben nach unten.</p><p>Mit diesen Protokollen koennen wir dem Compiler jetzt sogar beibringen, was ein “Feature” eigentlich ist, indem wir ein weiteres Protokoll definieren, das alle Subtypen verlangt:</p><pre><code class="language-Swift">/// A namespace for a TCA feature with extra requirements for Analytics and Error Handling.
public protocol Feature {
   associatedtype State: FeatureState
   associatedtype Action: FeatureAction
   associatedtype Event: FeatureEvent
   associatedtype Error: FeatureError
   associatedtype Reducer: FeatureReducer
   associatedtype View: FeatureView
}

/// A helper to declare a `Store` of a `Feature` type.
public typealias FeatureStore&lt;F: Feature&gt; = Store&lt;F.State, F.Action&gt;</code></pre><p><em>Auszug aus Feature.swift (Teil eines Hilfsmoduls)</em></p><p>Ich habe auch einen Typealias zum Definieren des <code>store</code> in unseren Views erstellt, aehnlich dem neuen <a href="https://github.com/pointfreeco/swift-composable-architecture/blob/prerelease/1.0/Sources/ComposableArchitecture/Store.swift#L615"><code>StoreOf</code>-Typealias</a> von Point-Free, aber spezifisch fuer ein <code>Feature</code>.</p><p>Gut, mit dieser Vorarbeit schauen wir uns an, wie das migrierte <code>Feature</code> aussieht:</p><pre><code class="language-Swift">import AppFoundation
import AppUI

public enum AppInfoFeature: Feature {
   public struct State: FeatureState {
      // see below
   }

   public enum Action: FeatureAction, BindableAction {
      // see below
   }

   public enum Event: String, FeatureEvent {
      // see below
   }

   public enum Error: FeatureError {
      // ...
   }

   public struct Reducer: FeatureReducer {
      // see below
   }

   public struct View: FeatureView {
      // see below
   }
}

extension AppInfoFeature.Event: AnalyticsEvent {
   public var idComponents: [String] {
      [&quot;AppInfo&quot;, self.rawValue]
   }
}</code></pre><p><em>Ueberblick ueber AppInfoFeature.swift</em></p><p>Beachte, dass ich ein <code>enum</code> statt eines <code>struct</code> fuer das <code>Feature</code> definiert habe, um zu verdeutlichen, dass es sich lediglich um einen Namespace handelt. Ausserdem konformieren alle Subtypen genau zu dem, was ihr Name ist, mit <code>Feature</code> als Praefix – z. B. <code>State: FeatureState</code>. Das macht es wirklich einfach, sich zu merken, wozu man konformieren muss, und den Code konsistenter.</p><p>Hier ist der <code>State</code>-Body, den ich im obigen Code-Beispiel fuer einen besseren Ueberblick ausgelassen habe:</p><pre><code class="language-Swift">   public struct State: FeatureState {
      @BindingState
      var showEnvInfoCopiedToClipboard: Bool = false
      var selectedAppIcon: AppIcon

      @PresentationState
      public var childErrorHandling: ErrorHandlingFeature.State?

      public init() {
         self.selectedAppIcon = Defaults[.selectedAppIcon]
      }
   }</code></pre><p>Das sieht dem urspruenglichen <code>AppInfoState</code> recht aehnlich, aber diesmal ist das Child von <code>errorHandlingFeature</code> in <code>childErrorHandling</code> umbenannt. Und weil ich auch das Child-Feature selbst migriert habe, hat sich der Typ von <code>ErrorHandlingState?</code> zu <code>ErrorHandlingFeature.State?</code> geaendert. Ausserdem habe ich das <code>@PresentationState</code>-Attribut fuer den neuen Navigationsstil in TCA <code>1.0</code> hinzugefuegt, der Dismissal von innerhalb des Childs unterstuetzt, ueber <code>@Dependency(\.dismiss)</code> und den Aufruf von <code>self.dismiss()</code> im <code>Reducer</code>.</p><p>Als Naechstes schauen wir uns unseren <code>Action</code>-Subtyp an:</p><pre><code class="language-Swift">   public enum Action: FeatureAction, BindableAction {
      case onAppear
      case onDisappear
      case selectedAppIconChanged
      case copyEnvironmentInfoPressed

      case binding(BindingAction&lt;State&gt;)

      case errorOccurred(Error)
      case childErrorHandling(PresentationAction&lt;ErrorHandlingFeature.Action&gt;)
   }</code></pre><p>Das ist auch weitgehend eine Kopie der urspruenglichen <code>AppInfoAction</code>, aber beachte, dass die Child-Action jetzt einen anderen Typ hat. Sie hat sich von <code>HelpfulErrorAction</code> zu <code>PresentationAction&lt;HelpfulErrorFeature.Action&gt;</code> geaendert – ein Wrapper, der alle Child-Actions in einen Case namens <code>.presented</code> legt. Der andere Case <code>.dismiss</code> meldet zurueck, dass das Child geschlossen wurde, falls das Parent darauf reagieren muss. Dank <code>PresentationAction</code> konnte ich die Action <code>setErrorHandling(isPresented:)</code> komplett entfernen, da dies jetzt in TCA-eigenen Typen gekapselt ist.</p><p>Schauen wir uns jetzt an, wie unsere <code>View</code> aussieht:</p><pre><code class="language-Swift">   public struct View: FeatureView {
      let store: FeatureStore&lt;AppInfoFeature&gt;

      public init(store: FeatureStore&lt;AppInfoFeature&gt;) {
         self.store = store
      }
   }</code></pre><p>Wie du siehst, verwende ich den <code>FeatureStore</code>-Typealias anstelle des alten Stils <code>Store&lt;State, Action&gt;</code> oder des TCA-<code>1.0</code>-Stils <code>StoreOf&lt;Feature&gt;</code>. Aber wo ist alles andere, das eine SwiftUI-<code>View</code> definiert, wie die <code>body</code>-Property? Nun, die Implementierung einer View ist typischerweise einer der laengsten Teile eines Features, also habe ich mich dafuer entschieden, die strukturellen Teile aller Subtypen an einem Ort zu behalten, waehrend Konformitaeten zu Protokollen, die viel Code erfordern, in Extension-Dateien ausgelagert werden.</p><p>Die Implementierung der <code>View</code> ist als Extension in einer separaten Datei:</p><pre><code class="language-Swift">import AppFoundation
import AppUI

extension AppInfoFeature.View: View {
   public typealias Action = AppInfoFeature.Action

   public var body: some View {
      WithViewStore(self.store, observe: { $0 }) { viewStore in
         VStack(alignment: .leading, spacing: 20) {
            // same code as before
         }
         .frame(width: 320)
         .padding()
         .onAppear { viewStore.send(.onAppear) }
         .onDisappear { viewStore.send(.onDisappear) }
         .sheet(store: self.store.scope(state: \.$childErrorHandling, action: Action.childErrorHandling)) { childStore in
            HelpfulErrorFeature.View(store: childStore)
         }
      }
   }
}

#if DEBUG
   struct AppInfoView_Previews: PreviewProvider {
      static let store = Store(initialState: AppInfoFeature.State(), reducer: AppInfoFeature.Reducer())

      static var previews: some View {
         AppInfoFeature.View(store: self.store).previewVariants()
      }
   }
#endif</code></pre><p><em>AppInfoFeature</em>View.swift*</p><p>Die Implementierung der <code>body</code>-Property ist so ziemlich die gleiche wie vorher. Aber beachte, dass der 11-zeilige <code>.sheet</code>-Modifier auf nur 3 Zeilen geschrumpft ist. Das verdanken wir den neuen Navigationstools mit <code>@PresentationState</code> und <code>PresentationAction</code>. Eine weitere Aenderung ist beim <code>static let store</code> im <code>PreviewProvider</code> passiert: Es gibt keinen Environment-Parameter mehr, der an den Store uebergeben werden muss!</p><p>Schauen wir uns den <code>Reducer</code>-Subtyp an, um zu erfahren warum:</p><pre><code class="language-Swift">   public struct Reducer: FeatureReducer {
      @Dependency(\.mainQueue)
      var mainQueue

      @Dependency(\.continuousClock)
      var clock

      public init() {}
   }</code></pre><p>Beachte die Verwendung des <code>@Dependency</code>-Attributs. Es erinnert dich vielleicht an das <code>@Environment</code>-Attribut in SwiftUI, und es funktioniert tatsaechlich genauso. Dieses neue Attribut ist der Grund, warum in TCA <code>1.0</code> kein <code>Environment</code>-Typ mehr benoetigt wird. Stattdessen werden alle Dependencies mit dem <code>@Dependency</code>-Attribut deklariert. Das erlaubt mir, den <code>AppEnv</code>-Typ, den ich vorher herumgereicht habe, komplett zu entfernen.</p><p>Auch hier vermisst du vielleicht die eigentliche Implementierung des <code>Reducer</code>-Protokolls. Nun, die Implementierung des Protokolls ist der zweite Teil eines Features, der recht lang werden kann, also habe ich auch das in eine eigene Datei ausgelagert:</p><pre><code class="language-Swift">import AppFoundation
import AppUI

extension AppInfoFeature.Reducer: Reducer {
   public typealias State = AppInfoFeature.State
   public typealias Action = AppInfoFeature.Action

   enum ShowEnvInfoCopiedId {}

   public var body: some ReducerOf&lt;Self&gt; {
      AnalyticsEventRecorderOf&lt;AppInfoFeature&gt; { state, action in
         switch action {
         case .onAppear:
            return .init(event: .onAppear)

         case .onDisappear:
            return .init(event: .onDisappear)

         case .copyEnvironmentInfoPressed:
            return .init(event: .copyEnvironmentInfoPressed)

         case .errorOccurred(let error):
            return .init(event: .errorOccurred, attributes: [&quot;errorCode&quot;: error.errorCode])

         case .binding, .childErrorHandling, .selectedAppIconChanged:
            return nil
         }
      }

      BindingReducer()

      Reduce&lt;State, Action&gt; { state, action in
         switch action {
         case .onAppear, .onDisappear:
         return .none  // for analytics only

      case .selectedAppIconChanged:
         return self.selectedAppIconChanged(state: &amp;state)

      case .copyEnvironmentInfoPressed:
         return self.copyEnvironmentInfoPressed(state: &amp;state)

      case .binding:
         return .none  // assignment handled by `BindingReducer()` above

      case .errorOccurred, .childErrorHandling:
         return self.handleHelpfulErrorAction(state: &amp;state, action: action)
         }
      }
      .ifLet(\.$childErrorHandling, action: /Action.childErrorHandling) {
         HelpfulErrorFeature.Reducer()
      }
   }

   private func selectedAppIconChanged(...)

   private func copyEnvironmentInfoPressed(state: inout State) -&gt; Effect&lt;Action&gt; {
      Pasteboard.string = Constants.GitHub.environmentInfo
      state.showEnvInfoCopiedToClipboard = true

      return .run { send in
         try await self.clock.sleep(for: Constants.toastMessageDuration)
         try Task.checkCancellation()
         await send(.set(\.$showEnvInfoCopiedToClipboard, false))
      }
      .cancellable(id: ShowEnvInfoCopiedId.self, cancelInFlight: true)
   }

   private func handleHelpfulErrorAction(...)
}</code></pre><p><em>AppInfoFeature</em>Reducer.swift*</p><p>Beachte zuerst die voellig andere Struktur. Es werden keine globalen <code>reducer</code>-Variablen mehr benoetigt. Stattdessen wird eine <code>body</code>-Property implementiert, ganz aehnlich wie beim <code>View</code>-Protokoll in SwiftUI. Und die Analogie endet nicht dort – die Struktur ist auch sehr SwiftUI-aehnlich, mit einer einfachen Liste verschiedener Reducer, die zusammen den <code>AppInfoFeature.Reducer</code> bilden, einschliesslich eines namens <code>BindingReducer()</code>, der <code>.binding()</code> ersetzt. Beachte auch, dass die 7 Zeilen Code fuer die Verbindung des Child-Features auf nur 3 Zeilen mit der neuen <code>.ifLet</code>-API geschrumpft sind. Zusaetzlich konnte ich, anstatt einen eigenen <code>ActionHandler</code>-Typ definieren zu muessen, in dem ich die Logik fuer die Reaktion auf Actions untergebracht habe, diese Funktionen einfach in den <code>Reducer</code>-Typ selbst verschieben – weil wir uns jetzt in einem Typ befinden und nicht auf globaler Ebene. Ausserdem nutzt die Implementierung von <code>copyEnvironmentInfoPressed</code> die neuen <code>async</code>-APIs. Vorher war sie im weniger lesbaren <code>Combine</code>-Stil implementiert:</p><pre><code class="language-Swift">      Pasteboard.string = Constants.GitHub.environmentInfo
      state.showEnvInfoCopiedToClipboard = true

      return .init(value: .set(\.$showEnvInfoCopiedToClipboard, false))
         .delay(for: Constants.toastMessageDuration, scheduler: env.mainQueue)
         .eraseToEffect()
         .cancellable(id: ShowEnvInfoCopiedId.self, cancelInFlight: true)</code></pre><p><em>Auszug aus dem alten AppInfoActionHandler.swift</em></p><p>Schliesslich musste ich durch den neuen SwiftUI-artigen Function-Builder-Stil meine <code>AnyReducer</code>-Extension-Funktion <code>recordAnalyticsEvents</code> in einen einfachen <code>Reducer</code> umwandeln, der die Ausfuehrungslogik als Property speichert:</p><pre><code class="language-Swift">import ComposableArchitecture
import Foundation

/// Returns a `Reducer` where each action coming to the store attempts to record an analytics event.
public struct AnalyticsEventRecorder&lt;State, Action, Event: AnalyticsEvent&gt;: Reducer {
   let toAttributedEvent: (State, Action) -&gt; Analytics.AttributedEvent&lt;Event&gt;?

   /// In the event closure, switch over `action` and return an ``Analytics.AttributedEvent`` if the action should be recorded, or else return `nil`.
   public init(event toAttributedEvent: @escaping (State, Action) -&gt; Analytics.AttributedEvent&lt;Event&gt;?) {
      self.toAttributedEvent = toAttributedEvent
   }

   public func reduce(into state: inout State, action: Action) -&gt; Effect&lt;Action&gt; {
      if let attributedEvent = self.toAttributedEvent(state, action) {
         Analytics.shared.record(attributedEvent: attributedEvent)
      }

      return .none
   }
}

/// Convenient way to declare an `AnalyticsEventRecorder`, but requires `Reducer` to conform to `Feature`.
public typealias AnalyticsEventRecorderOf&lt;F: Feature&gt; = AnalyticsEventRecorder&lt;F.State, F.Action, F.Event&gt;</code></pre><p><em>AnalyticsEventRecorder.swift (aus einem Hilfsmodul)</em></p><p>Der Body des Analytics-Helpers im Reducer oben hat sich ueberhaupt nicht geaendert – ich habe ihn einfach von der vorherigen Hilfsfunktion in den Initializer dieses neuen Reducers kopiert.</p><p>Und das war’s – das gesamte <code>AppInfo</code>-Feature ist auf TCA <code>1.0</code> migriert. Die gesamte Dateistruktur sieht jetzt so aus, mit nur 3 Dateien statt 7:</p><p><img src="/assets/images/blog/migrating-to-tca-1-0/after-the-migration-case.webp" alt="After the migration case" loading="lazy" /></p><p>Beachte, dass ich <code>*</code> als Trennzeichen verwende, um zu signalisieren, dass die Datei den Hauptteil eines <em>Sub</em>typs enthaelt. Natuerlich koennten wir <code>.</code> als Trennzeichen verwenden, sodass der Name sich wie <code>AppInfoFeature.Reducer.swift</code> liest. Aber weil <code>.R</code> von <code>.Reducer</code> vor <code>.s</code> von <code>.swift</code> sortiert wird, wuerden die Subtyp-Dateien ueber der Haupt-Feature-Datei <code>AppInfoFeature.swift</code> erscheinen – also habe ich mich fuer ein Trennzeichen entschieden, das aehnlich wie ein Punkt aussieht, aber niedrigere Prioritaet als <code>.</code> hat, was zu <code>*</code> fuehrte.</p><p>Die <a href="https://realm.github.io/SwiftLint/file_name.html">SwiftLint-Regel <code>file_name</code></a>, die ich aktiviert habe, zeigte mir mit diesem Benennungsstil eine Warnung. Aber ich konnte das einfach anpassen, indem ich dies zur Konfigurationsdatei hinzufuegte:</p><pre><code class="language-yaml">file_name:
   nested_type_separator: '*'</code></pre><h2 id="fazit">Fazit</h2><p>Die Migration meiner mittelgrossen App auf den neuen API-Stil von TCA <code>1.0</code> war viel Arbeit, aber der Grossteil bestand aus dem Einrichten von Dateistrukturen, Suchen und Ersetzen und dem Verschieben von bestehendem Code an andere Stellen. Und ich habe einige Zeit investiert, um eine gute Struktur herauszufinden, die mir gefaellt. Ich denke, wenn ich es nochmal fuer eine andere App mit meinen Erkenntnissen machen muesste, waere ich wahrscheinlich in 2–3 Tagen statt 5 fertig.</p><p>Nur an wenigen Stellen musste ich tatsaechlich Code anpassen, hauptsaechlich bei der Migration von Combine-artigem Effect-Code in meinen Reducern zu async/await-artigem Code. Aber dank grossartiger Dokumentation und Warnungen war immer ziemlich klar, was zu tun ist. Fuer alle, die eine aehnliche Migration durchfuehren, hier sind die 3 Links, die ich am nuetzlichsten fand:</p><ol><li><p><a href="https://github.com/pointfreeco/swift-composable-architecture/discussions/1186">Concurrency Beta</a></p></li><li><p><a href="https://github.com/pointfreeco/swift-composable-architecture/blob/main/Sources/ComposableArchitecture/Documentation.docc/Articles/MigratingToTheReducerProtocol.md">Migrating to the Reducer protocol</a></p></li><li><p><a href="https://github.com/pointfreeco/swift-composable-architecture/discussions/1944">Composable Navigation Beta</a></p></li></ol><p>Und wenn es eine Episode gibt, die einen guten Ueberblick ueber die Fortschritte in TCA <code>1.0</code> bietet, dann sind es die ersten ~35 Minuten von <a href="https://www.pointfree.co/episodes/ep222-composable-navigation-tabs">Episode #222</a>, die Composable Navigation einfuehrt. Schau sie dir an, um schnell eine Idee davon zu bekommen, wie sich die Dinge im letzten Jahr veraendert haben.</p><blockquote><p><strong>Du fandest diesen Artikel hilfreich? Hol dir meinen Expertenrat!</strong></p></blockquote>]]></content:encoded>
</item>
<item>
<title>2.000 Imports: Meine SwiftPM-Module organisieren</title>
<link>https://fline.dev/de/blog/organizing-my-swiftpm-modules/</link>
<guid isPermaLink="true">https://fline.dev/de/blog/organizing-my-swiftpm-modules/</guid>
<pubDate>Thu, 23 Mar 2023 00:00:00 +0000</pubDate>
<description><![CDATA[Wie du die Swift-Module deiner Apps für Klarheit und Komfort organisieren kannst – mithilfe eines versteckten (inoffiziellen) Swift-Features. Eine praktische Lösung für kleine bis mittelgroße Apps.]]></description>
<content:encoded><![CDATA[<h2 id="das-problem">Das Problem</h2><p>Ich habe mich kürzlich entschieden, am <a href="https://github.com/FlineDev/RemafoX/issues/13">größten Feature</a> für RemafoX bisher zu arbeiten, und während ich überlegte, wo ich anfangen soll, fand ich mich in über 70 Targets wieder – bei einem weniger als ein Jahr alten Projekt. Ich modularisiere meine App für klare Code-Trennung und schnellere Build-Zeiten (= schnellere SwiftUI Previews, Tests und mehr) mit dem klassischen SwiftPM-basierten Ansatz, den Point-Free in <a href="https://www.pointfree.co/episodes/ep171-modularization-part-1">dieser kostenlosen Episode</a> vorgestellt hat. Da ich vorhabe, über Jahre an dieser App zu arbeiten (die <a href="https://github.com/FlineDev/RemafoX/issues?q=is%3Aopen+is%3Aissue+label%3A%22Feature+Request%22&ref=fline.dev">27 Features</a> online sind nur die Spitze des Eisbergs, ich habe noch viel mehr Ideen), habe ich mich entschlossen, erst mal aufzuräumen. Denn eine Runde Refactoring zwischen Features hält die Codebasis sauber und macht den Entwickler glücklich! 😇</p><p>Ich erinnerte mich daran, dass ich <a href="https://github.com/apple/swift/blob/main/docs/ReferenceGuides/UnderscoredAttributes.md#_exported">das <code>@_exported</code>-Attribut</a> entdeckt hatte, als ich Swift-Evolution-Threads durchlas, um eine der Ausgaben <a href="https://swiftevolution.substack.com/">meines zugehörigen Newsletters</a> vorzubereiten. Obwohl es nicht empfohlen wird, APIs mit Unterstrich zu verwenden, da sich ihr Verhalten ändern oder sie sogar komplett entfernt werden könnten, fehlten mir Alternativen für mein Ziel, die vielen unorganisierten Targets aufzuräumen. Genau deshalb habe ich mich davon überzeugt, dass die Chancen, dass dieses Attribut komplett entfernt wird, relativ gering sind. Wenn überhaupt, glaube ich, dass der <a href="https://forums.swift.org/t/exported-and-fixing-import-visibility/9415">zugehörige Pitch</a> irgendwann aufgegriffen wird und seinen Weg ins offizielle Swift findet, sodass wir <code>@_exported</code> mit dem dann gewählten Namen ersetzen können. Außerdem habe ich herausgefunden, dass Point-Free in <a href="https://github.com/pointfreeco/swift-composable-architecture">The Composable Architecture</a> ebenfalls <a href="https://github.com/pointfreeco/swift-composable-architecture/blob/32dafefb1746af47fd0010637221415ac6828b08/Sources/ComposableArchitecture/Internal/Exports.swift">auf dieses Attribut setzt</a>, einem Framework, von dem meine App bereits stark abhängt. Warum also nicht voll darauf setzen?</p><p>Kurz gesagt hilft das Attribut bei Folgendem: Stell dir vor, du hast 10 Feature-Module und 5 Helfer-Module. In jeder Datei der 10 Features importiere ich tendenziell alle (oder die meisten) dieser 5 Helfer-Module, was zu so etwas führt:</p><pre><code class="language-Swift">import Assets
import Analytics
import ComposableArchitecture
import Constants
import Defaults
import HandySwift
import HelpfulErrorUI
import ReusableUI
import SFSafeSymbols
import SwiftUI
import Utility</code></pre><p>Und das wiederholt sich immer wieder. Ok, es stimmt, dass nicht alle in <em>jeder</em> einzelnen Datei eines Targets gebraucht werden, aber die Wahrheit ist auch, dass Xcode das gesamte Modul ohnehin linkt, sobald eine einzige Datei im Modul es importiert – sie also in allen Dateien zu importieren, würde die Build-Zeiten nicht beeinflussen (soweit ich weiß). Mit <code>@_exported import</code> können wir all diese Imports kombinieren, indem wir ein neues Target erstellen, es z.B. <code>CoreDependencies</code> nennen und eine Swift-Datei darin mit folgendem Inhalt anlegen:</p><pre><code class="language-Swift">@_exported import Assets
@_exported import Analytics
@_exported import ComposableArchitecture
@_exported import Constants
@_exported import Defaults
@_exported import HandySwift
@_exported import HelpfulErrorUI
@_exported import ReusableUI
@_exported import SFSafeSymbols
@_exported import SwiftUI
@_exported import Utility</code></pre><p>Jetzt importiert <code>import CoreDependencies</code> automatisch auch alle anderen Module!</p><p>Aber ist es wirklich die richtige Lösung, alles in eine Gruppe namens <code>CoreDependencies</code> zu packen? Ein weiteres Problem neben den zu oft wiederholten Imports ist, dass ich aktuell all diese 70 Module alphabetisch sortiere, weil mir eine andere Art von Gruppierung oder Struktur fehlt. Dieses Fehlen einer Gruppierung macht es nicht nur schwieriger, das richtige Modul zu finden, wenn ich ungefähr weiß, was ich suche, aber den genauen Namen nicht mehr kenne. Es kann auch zu <a href="https://en.wikipedia.org/wiki/Circular_dependency">zirkulären Abhängigkeiten</a> führen, wenn man an Features arbeitet und dabei möglichst viel Code wiederverwenden will. Es erfordert strategische Planung, was wohin gehört, um Code wiederzuverwenden und gleichzeitig zyklische Abhängigkeiten zu vermeiden, die zu Kompilierfehlern führen.</p><h2 id="die-lösung">Die Lösung</h2><blockquote><p>✨ UPDATE: Für neue/kleinere Apps verwende ich eine vereinfachte Version dessen, was ich unten im Detail beschreibe. Siehe <a href="https://github.com/FlineDev/Foundation">mein FlineDevKit-Repository</a> für mehr.</p></blockquote><p>Der beste Weg, eine praktische Lösung für ein Problem zu finden, ist, sich ein reales Beispiel anzuschauen. Hier also eine Auswahl von Modulen, die ich tatsächlich in <a href="https://remafox.app/">RemafoX</a> verwende:</p><pre><code class="language-Swift">Analytics
Assets
BetterCodable
CommandLineSetup
ComposableArchitecture
Constants
FilesSearch
Foundation
HandySwift
HelpfulErrorUI
MachineTranslation
Paywall
ProjectsBrowser
ReusableUI
SFSafeSymbols
Settings
SwiftUI
Utility</code></pre><p>Ja, ich habe auch <code>Foundation</code> und <code>SwiftUI</code> in der Liste oben aufgeführt. Warum? Weil sie am Ende des Tages eben auch Dependencies sind, die importiert werden müssen, genau wie jede andere Dependency, die wir importieren, ob extern oder intern. Ich betrachte sie als eingebaute externe Dependencies. Du bist es vielleicht gewohnt, mindestens <code>import Foundation</code> in jeder Swift-Datei zu haben, aber tatsächlich kannst du Swift-Code auch ohne <code>Foundation</code> schreiben – du hast dann eben nur die grundlegenden Swift-Features inklusive allem, was in der <a href="https://developer.apple.com/documentation/swift/swift-standard-library">Swift Standard Library</a> enthalten ist. Das funktioniert!</p><p>Und diese beiden Imports, die wir alle so oft machen, repräsentieren tatsächlich eine Apple-interne Gruppierung von Features/Helfern: Apple bündelt eine ganze Menge Funktionalität hinter <code>Foundation</code>, und das Gleiche macht Apple mit <code>SwiftUI</code> oder <code>UIKit</code>/<code>AppKit</code>. Der entscheidende Faktor scheint zu sein, dass alles, was eine Art UI darstellt oder direkt mit UI zusammenhängt, in eine Gruppe gehört, und alles, was keine UI darstellt oder nicht direkt mit UI zusammenhängt, in eine andere. Das Naheliegendste wäre also, ihrem Beispiel zu folgen – wir könnten sogar ihre Benennung übernehmen, indem wir <code>Foundation</code> für die Nicht-UI-Gruppe und <code>UI</code> (das sowohl in <code>SwiftUI</code> als auch in <code>UIKit</code> vorkommt) für die UI-Gruppe verwenden. Da unsere Gruppen spezifisch für eine App-Domäne sind, wären die resultierenden Namen für unsere Gruppen: <code>AppFoundation</code> und <code>AppUI</code>.</p><p>Wenden wir das mal auf die obige Modulliste an:</p><pre><code class="language-Swift">// AppFoundation
Analytics
BetterCodable
CommandLineSetup
Constants
FilesSearch
Foundation
HandySwift
MachineTranslation
Utility

// AppUI
Assets
ComposableArchitecture
HelpfulErrorUI
Paywall
ProjectsBrowser
SFSafeSymbols
ReusableUI
Settings
SwiftUI</code></pre><p>Das sieht schon besser aus. Aber es gibt noch etwas, das wir von Apples Framework-Struktur lernen können: Apple linkt nicht jedes Nicht-UI-Feature als Teil von <code>Foundation</code>, und sie liefern auch nicht allen SwiftUI-bezogenen Code als Teil von <code>SwiftUI</code> aus. <code>Combine</code> und <code>Charts</code> sind zwei Frameworks, die wir separat importieren müssen. Warum werden sie nicht als Teil von <code>Foundation</code> und <code>SwiftUI</code> ausgeliefert? Weil sie nur in bestimmten Domänen nützlich sind und möglicherweise nicht in einem globaleren Kontext gebraucht werden.</p><p>Wenn du dich an das ursprüngliche Problem erinnerst: Ich hatte einen Satz von Modulen, die ich immer und immer wieder an vielen Stellen importiert habe, weil sie global nützliche Helfer waren, und nicht nur in bestimmten Domänen. Es ergibt also Sinn, sie unter einem einheitlichen Gruppennamen zu importieren. Aber was genau ist ein Helfer? Was unterscheidet ihn von einem domänenspezifischeren Feature?</p><p>Ich persönlich nenne ein Feature dann einen “Helfer” oder ein “Utility”-Feature, wenn seine <strong>globale Verfügbarkeit viel nützlicher ist, als sie dem Entwicklungsprozess schadet</strong>. Das ist natürlich etwas subjektiv, aber als Faustregel mache ich es so: Wenn ich das Feature bereits in mehreren verschiedenen Teilen meiner App verwende und mir zusätzlich 2 oder 3 potenzielle neue Features vorstelle, die ich irgendwann in Zukunft hinzufügen könnte, und mindestens eines davon es ebenfalls nutzen könnte – dann ist es wahrscheinlich global sehr nützlich.</p><p>Konkreter gesagt würde ich die obige Modulliste so aufteilen:</p><pre><code class="language-Swift">// (global nützliche) Helfer
Analytics
Assets
BetterCodable
ComposableArchitecture
Constants
Foundation
HandySwift
HelpfulErrorUI
ReusableUI
SFSafeSymbols
SwiftUI
Utility

// (domänenspezifische) Features
CommandLineSetup
FilesSearch
MachineTranslation
Paywall
ProjectsBrowser
Settings</code></pre><p>Wenn wir nun die beiden Trennungsdimensionen kombinieren, erhalten wir etwas wie den folgenden Graphen mit 4 Quadranten und Abhängigkeiten dazwischen:</p><p><img src="/assets/images/blog/organizing-my-swiftpm-modules/11-imports-were-reduced.webp" alt="11 Imports wurden dank des @_exported-Attributs auf nur 2 reduziert." loading="lazy" /></p><p>Hier ein paar wichtige Dinge zu beachten:</p><ol><li><p>Die ⬆️ obere “Feature”-Hälfte baut auf der unteren “Helfer”-Hälfte auf, deshalb:</p></li><li><p>Die ↖️ grünen “Nicht-UI-Feature”-Module können <code>import AppFoundation</code> verwenden.</p></li><li><p>Die ↗️ roten “UI-Feature”-Module können beides importieren, <code>AppFoundation</code> und <code>AppUI</code>.</p></li><li><p>Innerhalb einer Gruppe (eines Quadranten) können Module voneinander abhängen (Zyklen vermeiden!).</p></li><li><p>➡️ “UI”-Module können von ⬅️ “Nicht-UI”-Modulen oder von <code>AppFoundation</code> abhängen.</p></li><li><p>Die ⬇️ unteren “Helfer” dürfen niemals von den “Features” oben importieren!</p></li><li><p>Externe Module können auch “Features” sein (siehe ↖️, aktuell habe ich keine in ↗️)</p></li></ol><p>Um diese Struktur umzusetzen, habe ich einfach ein neues Modul namens <code>AppFoundation</code> erstellt sowie eine neue Swift-Datei darin namens <code>AppFoundation.swift</code> mit folgendem Inhalt:</p><pre><code class="language-Swift">// System
@_exported import Foundation

// Internal
@_exported import Analytics
@_exported import Constants
@_exported import Utility

// External
@_exported import BetterCodable
@_exported import HandySwift</code></pre><p>Ich habe auch ein Modul <code>AppUI</code> mit folgendem Inhalt für <code>AppUI.swift</code> darin erstellt:</p><pre><code class="language-Swift">// System
@_exported import SwiftUI

// Internal
@_exported import Assets
@_exported import HelpfulErrorUI
@_exported import ReusableUI

// External
@_exported import ComposableArchitecture
@_exported import SFSafeSymbols</code></pre><p>Jetzt kann ich die 11 Imports aus dem Anfangsbeispiel dieses Artikels, die ich aus einer Datei innerhalb eines UI-Feature-Moduls entnommen habe, durch nur diese 2 Zeilen ersetzen:</p><pre><code class="language-Swift">import AppFoundation
import AppUI</code></pre><p><em>11 Imports wurden dank des @_exported-Attributs auf nur 2 reduziert.</em></p><p>Beachte, dass ich nicht mal <code>Foundation</code> oder <code>SwiftUI</code> importieren musste. Und für jedes Nicht-UI-Feature brauche ich sogar nur eine einzige Zeile mit <code>import AppFoundation</code>!</p><p>Natürlich bedeutet das nicht, dass ich nie wieder etwas anderes importieren werde. Ich werde weiterhin Imports auf der vertikalen Achse haben, wo ein bestimmtes Modul ein anderes bestimmtes Modul <em>innerhalb</em> einer Gruppe importiert, z.B. ein <code>ConfigFile</code>-UI-Feature, das Kindkomponenten wie <code>ConfigFileLinter</code> und <code>ConfigFileNormalizer</code> importiert. Aber das sind domänenspezifische Imports, die nicht zu vielen sich wiederholenden Imports führen.</p><p>Als Letztes habe ich meine Products, Dependencies und Targets in meiner <code>Package.swift</code>-Datei nach diesen 4 Quadranten gruppiert. Dafür habe ich Pragma-Marks wie <code>// MARK: - Non-UI Features</code> in allen Abschnitten hinzugefügt und die zugehörigen Anweisungen alphabetisch sortiert hineingesetzt. Mein resultierendes Manifest sieht jetzt ungefähr so aus:</p><pre><code class="language-Swift">import PackageDescription

let package = Package(
   name: &quot;RemafoX&quot;,
   platforms: [.macOS(.v12)],

   // MARK: - Products
   products: [
      // MARK: - Grouping Products
      .library(name: &quot;AppFoundation&quot;, targets: [&quot;AppFoundation&quot;]),
      .library(name: &quot;AppUI&quot;, targets: [&quot;AppUI&quot;]),
      .library(name: &quot;AppTest&quot;, targets: [&quot;AppTest&quot;]),

      // MARK: - Non-UI Helper Products (AppFoundation)
      .library(name: &quot;Analytics&quot;, targets: [&quot;Analytics&quot;]),
      .library(name: &quot;Constants&quot;, targets: [&quot;Constants&quot;]),
      .library(name: &quot;Utility&quot;, targets: [&quot;Utility&quot;]),

      // MARK: - UI Helper Products (AppUI)
      .library(name: &quot;Assets&quot;, targets: [&quot;Assets&quot;]),
      .library(name: &quot;HelpfulErrorUI&quot;, targets: [&quot;HelpfulErrorUI&quot;]),
      .library(name: &quot;ReusableUI&quot;, targets: [&quot;ReusableUI&quot;]),

      // MARK: - Test Helper Products (AppTest)
      .library(name: &quot;TestResources&quot;, targets: [&quot;TestResources&quot;]),

      // MARK: - Non-UI Feature Products
      .library(name: &quot;CommandLineSetup&quot;, targets: [&quot;CommandLineSetup&quot;]),
      .library(name: &quot;FilesSearch&quot;, targets: [&quot;FilesSearch&quot;]),
      .library(name: &quot;MachineTranslation&quot;, targets: [&quot;MachineTranslation&quot;]),

      // MARK: - UI Feature Products
      .library(name: &quot;Paywall&quot;, targets: [&quot;Paywall&quot;]),
      .library(name: &quot;ProjectsBrowser&quot;, targets: [&quot;ProjectsBrowser&quot;]),
      .library(name: &quot;Settings&quot;, targets: [&quot;Settings&quot;]),
   ],

   // MARK: - Dependencies
   dependencies: [
      // MARK: - Non-UI Helper Dependencies (AppFoundation)
      .package(url: &quot;https://github.com/marksands/BetterCodable.git&quot;, from: &quot;0.4.0&quot;),
      .package(url: &quot;https://github.com/sindresorhus/Defaults&quot;, from: &quot;6.3.0&quot;),
      .package(url: &quot;https://github.com/FlineDev/HandySwift&quot;, branch: &quot;main&quot;),

      // MARK: - UI Helper Dependencies (AppUI)
      .package(url: &quot;https://github.com/SFSafeSymbols/SFSafeSymbols&quot;, from: &quot;3.3.0&quot;),
      .package(url: &quot;https://github.com/pointfreeco/swift-composable-architecture&quot;, from: &quot;0.40.2&quot;),

      // MARK: - Test Helper Dependencies (AppTest)
      .package(url: &quot;https://github.com/pointfreeco/swift-custom-dump&quot;, from: &quot;0.3.0&quot;),

      // MARK: - Non-UI Feature Dependencies
      .package(url: &quot;https://github.com/FlineDev/Microya&quot;, branch: &quot;main&quot;),
      .package(url: &quot;https://github.com/JohnSundell/Splash.git&quot;, from: &quot;0.16.0&quot;),
      .package(url: &quot;https://github.com/jakeheis/SwiftCLI.git&quot;, from: &quot;6.0.3&quot;),
      .package(url: &quot;https://github.com/TelemetryDeck/SwiftClient&quot;, branch: &quot;main&quot;),

      // MARK: - UI Feature Dependencies
   ],

   // MARK: - Targets
   targets: [
      // MARK: - Grouping Targets
      .target(
         name: &quot;AppFoundation&quot;,
         dependencies: [
            // Internal
            &quot;Analytics&quot;,
            &quot;Constants&quot;,
            &quot;Utility&quot;,

            // External
            .product(name: &quot;BetterCodable&quot;, package: &quot;BetterCodable&quot;),
            .product(name: &quot;HandySwift&quot;, package: &quot;HandySwift&quot;),
         ]
      ),
      .target(
         name: &quot;AppUI&quot;,
         dependencies: [
            // Internal
            &quot;Assets&quot;,
            &quot;HelpfulErrorUI&quot;,
            &quot;ReusableUI&quot;,

            // External
            .product(name: &quot;SFSafeSymbols&quot;, package: &quot;SFSafeSymbols&quot;),
            .product(name: &quot;ComposableArchitecture&quot;, package: &quot;swift-composable-architecture&quot;),
         ]
      ),
      .target(
         name: &quot;AppTest&quot;,
         dependencies: [
            // Internal
            &quot;TestResources&quot;,

            // External
            .product(name: &quot;CustomDump&quot;, package: &quot;swift-custom-dump&quot;),
         ]
      ),

      // MARK: - Non-UI Helper Targets (AppFoundation)
      .target(
         name: &quot;Analytics&quot;,
         dependencies: [
            // Internal
            &quot;Constants&quot;,

            // External
            .product(name: &quot;HandySwift&quot;, package: &quot;HandySwift&quot;),
            .product(name: &quot;TelemetryClient&quot;, package: &quot;SwiftClient&quot;),
         ]
      ),
      .testTarget(name: &quot;AnalyticsTests&quot;, dependencies: [&quot;AppTest&quot;, &quot;Analytics&quot;]),
      .target(
         name: &quot;Constants&quot;,
         dependencies: [
            .product(name: &quot;Defaults&quot;, package: &quot;Defaults&quot;),
            .product(name: &quot;HandySwift&quot;, package: &quot;HandySwift&quot;),
         ]
      ),
      .target(
         name: &quot;Utility&quot;,
         dependencies: [
            // Internal
            &quot;Analytics&quot;,
            &quot;Constants&quot;,

            // External
            .product(name: &quot;HandySwift&quot;, package: &quot;HandySwift&quot;),
            .product(name: &quot;Defaults&quot;, package: &quot;Defaults&quot;),
         ]
      ),
      .testTarget(name: &quot;UtilityTests&quot;, dependencies: [&quot;AppTest&quot;, &quot;Utility&quot;]),

      // MARK: - UI Helper Targets (AppUI)
      .target(
         name: &quot;Assets&quot;,
         dependencies: [.product(name: &quot;Defaults&quot;, package: &quot;Defaults&quot;)],
         resources: [
            .process(&quot;Colors.xcassets&quot;),
            .process(&quot;Images.xcassets&quot;),
            .copy(&quot;Sounds&quot;),
         ]
      ),
      .target(
         name: &quot;HelpfulErrorUI&quot;,
         dependencies: [
            // Internal
            &quot;AppFoundation&quot;,
            &quot;Assets&quot;,
            &quot;ReusableUI&quot;,
         ]
      ),
      .target(
         name: &quot;ReusableUI&quot;,
         dependencies: [
            // Internal
            &quot;AppFoundation&quot;,
            &quot;Assets&quot;,

            // External
            .product(name: &quot;ComposableArchitecture&quot;, package: &quot;swift-composable-architecture&quot;),
            .product(name: &quot;Splash&quot;, package: &quot;Splash&quot;),
            .product(name: &quot;SFSafeSymbols&quot;, package: &quot;SFSafeSymbols&quot;),
         ]
      ),

      // MARK: - Test Helper Targets (AppTest)
      .target(
         name: &quot;TestResources&quot;,
         dependencies: [],
         path: &quot;TestResources&quot;,
         exclude: [&quot;Package.swift&quot;],
         sources: [&quot;TestResources.swift&quot;],
         resources: [
            .copy(&quot;CustomSample&quot;),
            .copy(&quot;EmptyFileStructureSamples&quot;),
            .copy(&quot;GitHubSampleProjects&quot;),
         ]
      ),

      // MARK: - Non-UI Feature Targets
      .target(name: &quot;CommandLineSetup&quot;, dependencies: [&quot;AppFoundation&quot;]),
      .target(
         name: &quot;MachineTranslation&quot;,
         dependencies: [
            // Internal
            &quot;AppFoundation&quot;,

            // External
            .product(name: &quot;Microya&quot;, package: &quot;Microya&quot;),
         ]
      ),
      .testTarget(
         name: &quot;MachineTranslationTests&quot;,
         dependencies: [&quot;AppFoundation&quot;, &quot;AppTest&quot;, &quot;MachineTranslation&quot;],
         exclude: [&quot;Resources/secrets.json.sample&quot;],
         resources: [.copy(&quot;Resources/secrets.json&quot;)]
      ),

      // MARK: - UI Feature Targets
      .target(name: &quot;Paywall&quot;, dependencies: [&quot;AppFoundation&quot;, &quot;AppUI&quot;]),
      .target(
         name: &quot;ProjectSetup&quot;,
         dependencies: [
            &quot;AppFoundation&quot;,
            &quot;AppUI&quot;,
            &quot;ProjectDragAndDrop&quot;,
            &quot;ProjectAnalyzer&quot;,
         ]
      ),
      .target(
         name: &quot;Settings&quot;,
         dependencies: [
            &quot;AppFoundation&quot;,
            &quot;AppUI&quot;,
            &quot;SettingsTabCurrentPlan&quot;,
            &quot;SettingsTabGeneral&quot;,
            &quot;SettingsTabMachineTranslation&quot;,
         ]
      ),
      // ... many more features related to Project, Settings etc.
   ]
)</code></pre><blockquote><p>☑️ Ähnlich wie <code>AppFoundation</code> und <code>AppUI</code> habe ich auch ein <code>AppTest</code>-Gruppierungs-Target in meiner App eingeführt, das ich verwende, um Imports von <code>XCTest</code> und Dependencies/Helfern wie <a href="https://github.com/pointfreeco/swift-custom-dump"><code>CustomDump</code></a> (sehr empfehlenswert!) zu vereinheitlichen.</p></blockquote><p>Um alle relevanten Imports durch <code>AppFoundation</code>/<code>AppUI</code> zu ersetzen, habe ich diesen Trick angewandt:</p><ol><li><p>Zuerst habe ich Xcodes <a href="https://developer.apple.com/documentation/xcode/finding-and-replacing-content-in-a-project">Suchen &amp; Ersetzen</a> für jede <code>@_exported</code>-Library verwendet und alle Imports durch <code>AppFoundation</code> ersetzt, sodass ich am Ende viele Dateien mit mehrfachen Imports von <code>AppFoundation</code> hatte.</p></li><li><p>Dann habe ich die <a href="https://github.com/realm/SwiftLint">SwiftLint</a>-Regel <a href="https://realm.github.io/SwiftLint/duplicate_imports.html"><code>duplicate_imports</code></a> verwendet, die Autokorrektur unterstützt. Installiere es über <code>brew install swiftlint</code> und führe dann diese 3 Zeilen aus:</p></li></ol><pre><code class="language-bash">echo &quot;only_rules: [duplicate_imports]&quot; &gt; temp_swiftlint.yml
swiftlint lint --config temp_swiftlint.yml --path Sources --autocorrect
rm temp_swiftlint.yml</code></pre><p><em>Passe den Parameter bei –path an, falls deiner anders als Sources ist.</em></p><ol><li><p>Zum Schluss habe ich die Änderungen für Dateien, die in Modulen liegen, die selbst Teil von <code>AppFoundation</code> sind, mit Git zurückgesetzt, ebenso die <code>AppFoundation.swift</code> selbst.</p></li><li><p>Dann habe ich die obigen Schritte für <code>AppUI</code> wiederholt. Das Ganze hat weniger als 10 Minuten gedauert!</p></li></ol><p>Das war’s! Wie du siehst, hatte ich vor dem Aufräumen ca. 2.000 Imports in meinem Projekt:</p><p><img src="/assets/images/blog/organizing-my-swiftpm-modules/the-solution.webp" alt="Die Lösung" loading="lazy" /></p><p>Nach dem Aufräumen habe ich jetzt nur noch 1.200 Imports, also rund 40 % weniger als vorher!</p><p><img src="/assets/images/blog/organizing-my-swiftpm-modules/the-solution-2.webp" alt="Die Lösung 2" loading="lazy" /></p><p>Außerdem ist meine <code>Package.swift</code>-Manifestdatei viel kürzer geworden, von 827 Zeilen auf 575 Zeilen – das ist ungefähr ein Drittel weniger. Und alles ist so viel strukturierter, ich bin begeistert! 😍</p><h2 id="fazit">Fazit</h2><p>Dank <code>@_exported import</code> und der Aufteilung von Modulen in vier Gruppen – durch die Fragen, ob sie (A) “UI-bezogen” oder “Nicht-UI-bezogen” und (B) eher “global nützlich” oder eher “domänenspezifisch” sind – kann ich jetzt eine beliebige Anzahl von “Helfer”-Modulen mit nur ein oder zwei <code>import</code>-Zeilen in meine “Feature”-Module importieren! Nicht nur das – diese Gruppen mit ihren Import-Regeln dienen auch als Leitfaden, um meinen Code im richtigen Modul zu platzieren und zirkuläre Abhängigkeiten zu vermeiden.</p><p>Das Ergebnis: Weniger Code schreiben, weniger Chancen für Build-Fehler – eine Win-win-Situation!</p>]]></content:encoded>
</item>
<item>
<title>Hardware-Anforderungen für iOS-Entwicklung (Mai 2025)</title>
<link>https://fline.dev/de/blog/hardware-requirements-for-ios-development/</link>
<guid isPermaLink="true">https://fline.dev/de/blog/hardware-requirements-for-ios-development/</guid>
<pubDate>Fri, 02 Dec 2022 00:00:00 +0000</pubDate>
<description><![CDATA[Von der günstigsten brauchbaren Option bis zum besten Preis-Leistungs-Mac für iOS-Entwickler.]]></description>
<content:encoded><![CDATA[<p>Ich sehe ziemlich oft Leute, die fragen, welche Hardware sie für die iOS-Entwicklung kaufen sollen – besonders Anfänger, die gerade erst loslegen, aber manchmal auch erfahrenere Entwickler, die bei ihrem aktuellen Mac auf Performance-Probleme stoßen. Als ehemaliger iOS-Teamleiter in zwei Unternehmen, wo ich fundierte Entscheidungen darüber treffen musste, welche Hardware für welches Entwickler-Level angeschafft wird, und wo ich verschiedene Macs an unterschiedlich großen Projekten testen konnte, hier meine aktuellen Empfehlungen.</p><p>Am Ende beantworte ich auch einige häufig gestellte Fragen:</p><ul><li><p><a href="https://jeehut.medium.com/hardware-requirements-for-ios-development-may-2022-ac0234cfc879?source=your_stories_page-------------------------------------#3b3d">Apple Silicon oder Intel?</a></p></li><li><p><a href="https://jeehut.medium.com/hardware-requirements-for-ios-development-may-2022-ac0234cfc879?source=your_stories_page-------------------------------------#54d7">Wie viel Speicherplatz brauche ich?</a></p></li><li><p><a href="https://jeehut.medium.com/hardware-requirements-for-ios-development-may-2022-ac0234cfc879?source=your_stories_page-------------------------------------#10d8">Wie viel RAM brauche ich?</a></p></li><li><p><a href="https://jeehut.medium.com/hardware-requirements-for-ios-development-may-2022-ac0234cfc879?source=your_stories_page-------------------------------------#5f2b">Was ist mit dem Mac Studio?</a></p></li></ul><blockquote><p><em>Dieser Artikel wurde <strong>zuletzt im Dezember 2024 aktualisiert</strong>. Wenn seitdem 6 Monate vergangen sind, <a href="https://bsky.app/profile/jeehut.bsky.social"><em>schreib mir auf Bluesky</em></a></em> und erinnere mich an ein Update.*</p></blockquote><h2 id="der-günstigste-mac-für-entwickler-ab-750">Der günstigste Mac für Entwickler (ab 750 $)</h2><p><img src="/assets/images/blog/hardware-requirements-for-ios-development/outdated-screenshot-of.webp" alt="Veralteter Screenshot dieser XcodeBenchmark-Repository-Tabelle." loading="lazy" /></p><h3 id="hardware">Hardware:</h3><p><a href="https://www.apple.com/shop/buy-mac/mac-mini/apple-m4-chip-with-10-core-cpu-and-10-core-gpu-16gb-memory-256gb#"><strong>M4 Mac Mini (16 GB RAM &amp; 256 GB SSD)</strong></a> für <strong>600 $</strong></p><ul><li><p>Monitor (mindestens <strong>120 $</strong>)</p></li><li><p>Kabellose Maus &amp; Tastatur (mindestens <strong>30 $</strong>)</p></li></ul><h3 id="vorteile">Vorteile:</h3><ul><li><p>Schnelle Build-Zeiten mit zukunftssicherem M4-Apple-Silicon-Prozessor</p></li><li><p>Schnelle SSD, um Xcode-Projekte schnell zu öffnen (viele kleine Dateien)</p></li><li><p>Insgesamt günstigste Option mit einem Gesamtpreis (inkl. Peripherie) von ca. 750 $</p></li><li><p>Eingebaute Anschlüsse, kein Adapter nötig (5x USB-C, HDMI, LAN, 3,5 mm)</p></li></ul><h3 id="nachteile">Nachteile:</h3><ul><li><p>Nicht mobil (kein Laptop)</p></li><li><p>Erfordert zusätzliche Peripherie (Maus, Tastatur, Monitor)</p></li><li><p>256 GB SSD reicht gerade so für die Entwicklung, kein Platz für andere Medien</p></li></ul><h3 id="upgrade-optionen">Upgrade-Optionen</h3><ul><li><p>512 GB SSD, wenn du den Mac auch für andere Zwecke nutzen willst (<strong>+200 $</strong>)</p></li><li><p>ODER eine externe 2-TB-SSD <a href="https://www.amazon.com/SAMSUNG-Inch-Internal-MZ-77E4T0B-AM/dp/B08QB93S6R">für einen etwas niedrigeren Preis</a> kaufen (nur für Medien)</p></li><li><p>Den <a href="https://www.apple.com/shop/buy-mac/imac/blue-24-inch-8-core-cpu-7-core-gpu-8gb-memory-256gb">24” iMac</a> für einen 4,5K-Monitor, Apple-Maus &amp; -Tastatur holen (<strong>+550 $</strong>)</p></li></ul><h3 id="empfohlene-zielgruppe">Empfohlene Zielgruppe</h3><p>Wenn Geld das Hauptkriterium ist, z. B. weil es nur ein Hobby für dich ist, <strong>und</strong> du weißt, dass du von einem festen Platz aus coden wirst. <strong>Oder</strong> wenn du den Mac auch als Medienserver für dein Zuhause nutzen möchtest (mit einer externen Festplatte).</p><h2 id="der-günstigste-mobile-mac-für-entwickler-ab-1000">Der günstigste <em>mobile</em> Mac für Entwickler (ab 1.000 $)</h2><p><img src="/assets/images/blog/hardware-requirements-for-ios-development/outdated-screenshot-of-4.webp" alt="Veralteter Screenshot dieser XcodeBenchmark-Repository-Tabelle." loading="lazy" /></p><h3 id="hardware">Hardware:</h3><p><a href="https://www.apple.com/shop/buy-mac/macbook-air/13-inch-m2"><strong>13” M4 MacBook Air (16 GB RAM &amp; 256 GB SSD)</strong></a> für <strong>1.000 $</strong></p><h3 id="vorteile">Vorteile:</h3><ul><li><p>Schnelle Build-Zeiten mit ausreichendem M4-Apple-Silicon-Prozessor</p></li><li><p>Schnelle SSD, um Xcode-Projekte schnell zu öffnen (viele kleine Dateien)</p></li><li><p>Mobil (Laptop mit 13” eingebautem Bildschirm, Tastatur &amp; Trackpad)</p></li></ul><h3 id="nachteile">Nachteile:</h3><ul><li><p>Teurer als der Mac mini (selbst mit inkludierter Peripherie)</p></li><li><p>Eine externe Festplatte statt SSD-Upgrade zu nutzen ist nicht praktikabel</p></li><li><p>Du musst einen USB-C-Adapter kaufen und mitnehmen für die Kompatibilität</p></li><li><p>256 GB SSD reicht gerade so für die Entwicklung, kein Platz für andere Medien</p></li></ul><h3 id="upgrade-optionen">Upgrade-Optionen</h3><ul><li><p>512 GB SSD, wenn du den Mac auch für andere Zwecke nutzen willst (<strong>+200 $</strong>)</p></li></ul><h3 id="empfohlene-zielgruppe">Empfohlene Zielgruppe</h3><p>Wenn Geld ein Thema ist, du aber sowieso einen Laptop brauchst, z. B. weil du Student bist, <strong>und</strong> du flexibel sein und ihn immer mitnehmen willst.</p><h2 id="der-beste-preis-leistungs-mac-für-entwickler-ab-2500">Der beste Preis-Leistungs-Mac für Entwickler (ab 2.500 $)</h2><p><img src="/assets/images/blog/hardware-requirements-for-ios-development/outdated-screenshot-of-3.webp" alt="Veralteter Screenshot dieser XcodeBenchmark-Repository-Tabelle." loading="lazy" /></p><h3 id="hardware">Hardware:</h3><p><a href="https://www.apple.com/shop/buy-mac/macbook-pro/16-inch-space-black-standard-display-apple-m4-pro-with-14-core-cpu-and-20-core-gpu-24gb-memory-512gb#"><strong>16” M4 Pro MacBook Pro (24 GB &amp; 512 GB)</strong></a> für <strong>2.500 $</strong></p><h3 id="vorteile">Vorteile:</h3><ul><li><p>14-Kern-M4-Pro mit nochmal ca. 50 % schnelleren Build-Zeiten (im Vergleich zum M4)</p></li><li><p>Größerer &amp; hochwertigerer 4K-, 120-Hz-, HDR-Bildschirm (im Vergleich zum Air)</p></li><li><p>Längste Akkulaufzeit aller bisherigen Macs (länger als M4 Max oder 14”-Varianten)</p></li><li><p>Eingebauter Kartenleser und HDMI-Anschluss &amp; 1 zusätzlicher USB-C (im Vergleich zum Air)</p></li><li><p>Schnelle 512-GB-SSD, um Xcode-Projekte schnell zu öffnen (viele kleine Dateien)</p></li><li><p>8 GB zusätzlicher RAM-Puffer für fortgeschrittenere Entwickler-Workflows</p></li><li><p>Mobil (Laptop mit 16” eingebautem Bildschirm, Tastatur &amp; Trackpad)</p></li></ul><h3 id="nachteile">Nachteile:</h3><ul><li><p>Ziemlich teuer (gesparte Build-Zeit lohnt sich, wenn man professionell programmiert)</p></li></ul><h3 id="upgrade-optionen">Upgrade-Optionen</h3><ul><li><p>1 TB SSD, wenn du etwas extra Puffer haben willst (<strong>+200 $</strong>)</p></li><li><p><a href="https://www.apple.com/shop/buy-mac/macbook-pro/16-inch-space-black-standard-display-apple-m4-max-with-16-core-cpu-and-40-core-gpu-48gb-memory-1tb#">M4 Max</a> für 20 % schnellere CPU, 48 GB RAM, 1 TB SSD, schnellere GPU (<strong>+1.500 $</strong>)</p></li></ul><h3 id="empfohlene-zielgruppe">Empfohlene Zielgruppe</h3><p>Wenn <strong>Build-Performance</strong> dein Hauptkriterium ist (= professionelle Entwickler) und du kein Geld für kleine Verbesserungen verschwenden willst, nimm diesen.</p><p>Wenn du den <a href="https://www.apple.com/shop/buy-mac/macbook-pro/14-inch-space-black-standard-display-apple-m4-pro-chip-with-12-core-cpu-16-core-gpu-24gb-memory-512gb#">14”-Formfaktor</a> für 2.200 $ bevorzugst (vergiss nicht, auf die 14-Kern-CPU aufzurüsten!), ist das ähnlich empfehlenswert, kommt aber mit einigen kleineren Nachteilen wie einer kürzeren (aber trotzdem großartigen) Akkulaufzeit und lauteren (aber trotzdem sehr leisen) Lüftern bei nur 100 $ Ersparnis. Dann nimm lieber das <a href="https://www.apple.com/shop/buy-mac/macbook-pro/14-inch-space-black-standard-display-apple-m4-pro-chip-with-12-core-cpu-16-core-gpu-24gb-memory-512gb#">Basismodell des 14”</a>, das 500 $ weniger kostet (2.000 $), aber bedenke, dass es mit einer 10 % langsameren 12-Kern-CPU kommt.</p><h2 id="häufig-gestellte-fragen">Häufig gestellte Fragen</h2><p>Einige häufig gestellte Hardware-Fragen und meine persönliche Einschätzung dazu.</p><h3 id="apple-silicon-oder-intel">Apple Silicon oder Intel?</h3><p><img src="/assets/images/blog/hardware-requirements-for-ios-development/outdated-screenshot-of-2.webp" alt="Veralteter Screenshot dieser XcodeBenchmark-Repository-Tabelle." loading="lazy" /></p><p>Eine realistischere Mindestberechnung ist daher:</p><ul><li><p>40 GB für macOS</p></li><li><p>80 GB für 2 gleichzeitig installierte Xcode-Versionen</p></li><li><p>20 GB für 4 gleichzeitig installierte SDK-Versionen</p></li><li><p>30 GB für Build-Artefakte von 3 verschiedenen Projekten gleichzeitig</p></li><li><p>20 GB für andere entwicklungsbezogene Apps und Tools</p></li><li><p>50 GB für andere Apps (Pages, Numbers, Notion, Slack, Zoom usw.)</p></li></ul><p>Das ergibt zusammen 240 GB, also reicht eine <strong>256-GB-SSD gerade so</strong> für ernsthafte iOS-Entwicklung. Aber das funktioniert nur, wenn du <em>ausschließlich</em> iOS-Entwicklung auf diesem Mac machst und ihn nicht z. B. zum Synchronisieren deiner iPhone-Fotos oder -Videos nutzt. Wenn du den Mac auch für andere Zwecke verwenden willst, nimm <strong>mindestens 512 GB SSD</strong>. Das gibt dir auch den Spielraum, andere Technologien auszuprobieren, wie etwa <a href="https://developer.android.com/studio/">Android Studio</a> zu installieren, um auch mal Android-Entwicklung zu testen.</p><h3 id="wie-viel-ram-brauche-ich">Wie viel RAM brauche ich?</h3><p><img src="/assets/images/blog/hardware-requirements-for-ios-development/how-much-ram-do-i-need.webp" alt="How much ram do i need" loading="lazy" /></p><p><strong>Kurze Antwort:</strong> Mindestens <em>16 GB</em>, besser <em>24 GB+</em>.</p><p><strong>Lange Antwort:</strong>
8 GB sind ein guter Start, wenn du schon einen Mac besitzt, und du solltest damit problemlos einen einzelnen Simulator plus Safari mit vielen Tabs nutzen können, ohne große Ruckler. Aber sobald dein Projekt größer wird und du anfängst, zwei Dinge gleichzeitig zu machen, und vielleicht sogar einen Server oder eine VM im Hintergrund laufen hast, wirst du durch den RAM limitiert.</p><p>Und falls du auf dem Gerät auch mal Android-Entwicklung betreiben willst, brauchst du definitiv die 16-GB-Option, weil (anders als iOS-Simulatoren) der Android-Emulator den RAM nicht effizient mit dem Host-System teilen kann.</p><p>Während der Unterschied von 8 GB zu 16 GB sehr spürbar ist, werden die Unterschiede von 16 GB zu 24 GB oder höher deutlich kleiner. Matt Gallagher hat dazu kürzlich einen <a href="https://twitter.com/cocoawithlove/status/1517736163318009856?s=21&t=kBvPG1DmkGdh7PHiJWsEdw&ref=fline.dev">guten Vergleich auf Twitter</a> geschrieben, mit dem abschließenden Hinweis: „Wähle nicht RAM statt schnellerer CPU.” Dem stimme ich voll zu.</p><p>Erfreulicherweise haben seit 2024 alle neuen Macs mindestens 16 GB, du solltest also auf der sicheren Seite sein.</p><h3 id="was-ist-mit-dem-mac-studio">Was ist mit dem Mac Studio?</h3><p><img src="/assets/images/blog/hardware-requirements-for-ios-development/what-about-the-mac-studio.webp" alt="What about the mac studio" loading="lazy" /></p><p><strong>Kurze Antwort:</strong> Warte auf den M4-Ultra-Mac-Studio, wenn du ihn dir leisten kannst. Der ist ein Biest.</p><p><strong>Lange Antwort:</strong>
Die aktuellen Mac-Studio-Geräte sind in einer seltsamen Position – sie laufen noch mit M2-Max- und M2-Ultra-Chips. Der aktuelle M4 Pro ist ca. 33 % schneller als der M2 Max, und der M4 Max ist ca. 7 % schneller als der M4 Ultra bei Entwicklungsaufgaben. Es ist also derzeit keine gute Idee, einen Mac Studio zu kaufen. Wenn dir der Formfaktor gefällt, nimm stattdessen einfach einen <a href="https://www.apple.com/shop/buy-mac/mac-mini/apple-m4-pro-chip-with-12-core-cpu-16-core-gpu-24gb-memory-512gb#">M4-Pro-Mac-Mini</a> (stelle sicher, dass du auf die 14-Kern-CPU aufrüstest!) für 1.600 $ (400 $ weniger als der M2-Max-Mac-<em>Studio</em>).</p><p>Der <a href="https://www.apple.com/shop/buy-mac/mac-studio/10-core-cpu-24-core-gpu-16-core-neural-engine-32gb-memory-512gb#">Basis-Mac-Studio</a> für 2.000 $ hat eine 33 % langsamere 12-Kern-CPU als das oben empfohlene Best-Value-MacBook-Pro. Du kannst ihn natürlich trotzdem kaufen, um 500 $ zu sparen, wenn du den tollen Bildschirm, das Lautsprechersystem und die Mobilität des MacBooks nicht brauchst. Aber dann musst du für einen Monitor, Maus &amp; Tastatur bezahlen, was (auf diesem professionellen Niveau) ungefähr genauso viel kosten kann – es sei denn, du hast sie schon.</p><p>Die <a href="https://www.apple.com/shop/buy-mac/mac-studio/24-core-cpu-60-core-gpu-32-core-neural-engine-64gb-memory-1tb#">M2-Ultra-Variante</a> für 4.000 $ war hier früher das interessantere Gerät, weil sie doppelt so viele CPU-Kerne hatte und nochmal schnellere Build-Zeiten im Vergleich zu den Max-Chips bot. Wenn dir Geld egal ist, aber Performance nicht, kannst du gerne auf den M4 Ultra warten und ihn dir für die schnellstmöglichen Builds zulegen.</p>]]></content:encoded>
</item>
<item>
<title>Vorstellung von RemafoX: Einfache App-Lokalisierung</title>
<link>https://fline.dev/de/blog/introducing-remafox-easy-app-localization/</link>
<guid isPermaLink="true">https://fline.dev/de/blog/introducing-remafox-easy-app-localization/</guid>
<pubDate>Thu, 13 Oct 2022 00:00:00 +0000</pubDate>
<description><![CDATA[Sag hallo zu RemafoX, der App mit der Mission, das Entwicklerleben zu vereinfachen – mit neuen Workflows für die Lokalisierung bei der Arbeit mit Xcode.]]></description>
<content:encoded><![CDATA[<p>Nach 90 Tagen Beta-Testing ist es Zeit für das <a href="https://apps.apple.com/app/apple-store/id1605635026?pt=549314&ct=fline.dev.ReMafoX&mt=8&ref=fline.dev">offizielle Release im Mac App Store</a>!</p><h2 id="features">Features</h2><p>Dieses erste Release ist bereits <strong>vollgepackt mit vielen Features</strong>, die dir helfen:</p><ul><li><p><strong>Fokus:</strong> Verlasse nie den Kontext deiner Swift-Datei – programmier einfach weiter</p></li><li><p><strong>Schneller sein:</strong> Keine manuelle Bearbeitung von Strings-Dateien mehr beim Hinzufügen neuer Keys</p></li><li><p><strong>Automatisieren:</strong> Richte DeepL oder Microsoft Translator ein, um deine App zu übersetzen</p></li><li><p><strong>Linten:</strong> Erhalte Warnungen in Xcode bei leeren Übersetzungen oder doppelten Keys</p></li><li><p><strong>Normalisieren:</strong> Strings-Dateien alphabetisch sortiert, damit du Übersetzungen leichter findest</p></li><li><p><strong>Synchronisieren:</strong> Automatische Aktualisierung der Strings-Dateien bei Änderungen an Storyboard/XIB-Dateien</p></li><li><p><strong>Lernen:</strong> Viele Erklärungen, Schritt-für-Schritt-Anleitungen und sogar Videos</p></li><li><p><strong>Pluralisieren:</strong> Automatische Erkennung und sprachbewusste Formen für einfache Pluralisierung</p></li></ul><h2 id="roadmap">Roadmap</h2><p>Aber das ist erst der Anfang. <strong>Jeden Monat kommt ein neues Feature</strong> hinzu, zum Beispiel:</p><ul><li><p><strong><a href="https://github.com/FlineDev/ReMafoX/issues/14">Fastlane-Support</a>:</strong> App-Store-Beschreibung und “What’s New” übersetzen</p></li><li><p><strong><a href="https://github.com/FlineDev/ReMafoX/issues/13">Verifizierung</a>:</strong> Einfache Möglichkeit, andere einzuladen, Übersetzungen zu prüfen oder beizusteuern</p></li><li><p><strong><a href="https://github.com/FlineDev/ReMafoX/issues/12">Strings-Editor</a>:</strong> Eine spezielle Oberfläche für angenehmeres Bearbeiten von Strings(dict)-Dateien</p></li><li><p><strong><a href="https://github.com/FlineDev/ReMafoX/issues/11">Google Translate</a> &amp; <a href="https://github.com/FlineDev/ReMafoX/issues/4">Yandex</a>:</strong> Unterstützung weiterer Übersetzungsdienste</p></li><li><p><a href="https://github.com/FlineDev/ReMafoX/issues/10"><strong>Open Source</strong></a><strong>:</strong> Die Community unterstützen, indem Pro-Features kostenlos werden</p></li><li><p>… und <strong>vieles mehr</strong> – [<a href="https://github.com/FlineDev/ReMafoX/issues?q=is%3Aopen+is%3Aissue+label%3A%22Feature+Request%22+sort%3Areactions-%2B1-desc&ref=fline.dev">erkunde und stimme für Features ab</a> oder schlage eigene vor](https://github.com/FlineDev/ReMafoX/issues?q=is%3Aopen+is%3Aissue+label%3A%22Feature+Request%22+sort%3Areactions-%2B1-desc&amp;ref=fline.dev)!</p></li></ul><h2 id="vorschau">Vorschau</h2><p>Noch nicht überzeugt? Dann schau dir die folgenden 3 GIFs an, die RemafoX in Aktion zeigen:</p><p><strong>Füge eine neue Übersetzung</strong> zu einem Projekt in vielen Sprachen mit <strong>nur einem Schritt</strong> hinzu:</p><p><img src="/assets/images/blog/introducing-remafox-easy-app-localization/add-translation-edit-with-audio.gif" alt="" loading="lazy" /></p><p>Finde <strong>leere Übersetzungen</strong> in Strings-Dateien und nutze <strong>maschinelle Übersetzung</strong>:</p><p><img src="/assets/images/blog/introducing-remafox-easy-app-localization/previews.gif" alt="" loading="lazy" /></p><p><strong>Richte ein Projekt</strong> in RemafoX ein und <strong>passe es an</strong> mit dokumentierten Konfigurationsoptionen:</p><p><img src="/assets/images/blog/introducing-remafox-easy-app-localization/project-setup-edit-with-audio.gif" alt="" loading="lazy" /></p><hr /><blockquote><p>✨ Want to see your ad here? Contact me at <a href="mailto:ads@fline.dev">ads@fline.dev</a> to get in touch.</p></blockquote><hr /><h2 id="sonderangebot">Sonderangebot</h2><p>Kaufe noch heute ein Lifetime-Abo und <strong>spare ca. 30 %</strong> mit dem Early-Bird-Rabatt, der bis Ende Oktober läuft. <strong>Monats- oder Jahresabonnements</strong> unterstützen die Weiterentwicklung dieses Projekts und kommen alle mit einer kostenlosen Testphase. So kannst du selbst sehen, wie RemafoX <em>deinen</em> Entwickler-Workflow verbessert.</p><p>Aber es gibt auch ein <strong>kostenloses Tier</strong> für kleinere Projekte, das alle oben genannten Features außer Pluralisierung umfasst. Ich verspreche, dass ich nie Features aus dem kostenlosen Tier entfernen werde – wenn es heute für dich funktioniert, wird es das auch immer tun! Und die meisten der geplanten Features werden auch dem kostenlosen Tier zugutekommen.</p><h2 id="jetzt-loslegen">Jetzt loslegen!</h2><p>Es gibt also wirklich <strong>keinen Grund, RemafoX <em>nicht</em> zu verwenden</strong>! Hol es dir jetzt:</p><p><a href="https://apps.apple.com/app/apple-store/id1605635026?pt=549314&ct=fline.dev&mt=8&ref=fline.dev">‎ReMafoX: Easy App Localization</a></p><p>Wenn du auf Probleme stößt oder Fragen hast, schau bitte im Menü ‘Help’ nach.</p>]]></content:encoded>
</item>
<item>
<title>Das Beste aus der WWDC 2022 herausholen</title>
<link>https://fline.dev/de/blog/making-the-most-of-wwdc-2022/</link>
<guid isPermaLink="true">https://fline.dev/de/blog/making-the-most-of-wwdc-2022/</guid>
<pubDate>Mon, 30 May 2022 00:00:00 +0000</pubDate>
<description><![CDATA[Wie du beide Keynotes gemeinsam mit anderen Entwicklern (remote) genießen und deine Lernerfahrungen während der Woche maximieren kannst – wenn du die Zeit investieren kannst.]]></description>
<content:encoded><![CDATA[<p>Apple veranstaltet seit 1983 jedes Jahr eine Entwicklerkonferenz (nächstes Jahr wäre also technisch gesehen das 40-jährige Jubiläum!), auch wenn sie offiziell erst <a href="https://apple.fandom.com/wiki/Worldwide_Developers_Conference#History">seit 1990</a> den Namen „World Wide Developer Conference” oder kurz „WWDC” trägt. Fun Fact: Die allererste Konferenz 1983 wurde <a href="https://apple.fandom.com/wiki/Apple_Independent_Software_Developers_Conference_1983">in Monterey abgehalten</a> – der Name der <a href="https://www.apple.com/macos/monterey/">aktuellen macOS</a>-Version.</p><p>Während die erste „Dub Dub” – so nennen die meisten, die mal persönlich dabei waren, das Event gerne – eigentlich eine geschlossene Veranstaltung war, bei der ein NDA unterschrieben werden musste, weil vorab ein neues Produkt gezeigt wurde, öffnete sie sich im Folgejahr und ist seitdem für alle Entwickler zugänglich. Zumindest für diejenigen, die eine Gebühr von ca. 1.600 $ zahlen, sich eine einwöchige Reise nach Kalifornien leisten konnten und das Glück hatten, bei der Lotterie zu gewinnen, da die Tickets begrenzt waren.</p><p>Aber weil solche großen Konferenzen 2020 wegen der <a href="https://en.wikipedia.org/wiki/COVID-19_pandemic">COVID-19-Pandemie</a> undenkbar waren, musste Apple etwas anderes machen, und seitdem wird die WWDC ihrem Namen tatsächlich gerecht und ist wirklich für alle Entwickler weltweit zugänglich – kostenlos! Das ist natürlich die positive Seite, aber eine Online-Konferenz ist eben ein anderes Erlebnis als eine Vor-Ort-Veranstaltung. Deshalb versuchen sie dieses Jahr einen gemischten Ansatz mit einem <a href="https://developer.apple.com/wwdc22/special-day/">Special Event im Apple Park</a> für einige Glückliche, aber die Konferenz selbst bleibt weiterhin weltweit offen für alle.</p><p>Angesichts dieser Veränderungen und der Möglichkeit, dass die jährliche Konferenz in diesem Online-Format bleibt, schauen wir uns die drei zentralen Aspekte der Konferenz an und wie wir das Beste daraus machen können, wenn wir die Zeit aufbringen:</p><ul><li><p><strong>Lernen</strong> über die neuesten Apple-Technologien</p></li><li><p><strong>Vernetzen</strong> mit anderen Entwicklern und eine gute Zeit haben</p></li><li><p><strong>Diskutieren</strong> über die Auswirkungen neuer Technologien, Features und Produkte</p></li></ul><h2 id="lernen">Lernen</h2><p><img src="/assets/images/blog/making-the-most-of-wwdc-2022/learning.webp" alt="" loading="lazy" /></p><h3 id="apple">Apple</h3><p>Welche Lernmöglichkeiten Apple offiziell während der Woche anbietet.</p><p><strong>Keynote &amp; Platforms State of the Union</strong>
Die <a href="https://www.youtube.com/watch?v=0TD96VTf0Xs&ref=fline.dev">WWDC Keynote</a> ist der Moment, in dem Apple der Öffentlichkeit die neuesten Software- und Produkt-Updates präsentiert. Sie richtet sich nicht speziell an Entwickler, enthält aber gegen Ende einen <a href="https://www.youtube.com/watch?v=0TD96VTf0Xs&t=5598s&ref=fline.dev">kurzen Entwickler-Abschnitt</a>, und große Ankündigungen wie <a href="https://www.youtube.com/watch?v=w87fOAG8fjk&t=6232s&ref=fline.dev">Swift 2014</a> oder <a href="https://www.youtube.com/watch?v=psL_5RIBqnY&t=7591s&ref=fline.dev">SwiftUI 2019</a> wurden alle hier enthüllt (schau dir die verlinkten Abschnitte an und hör auf die Menge, um dich auf die diesjährige Keynote einzustimmen!).</p><p>Die <a href="https://developer.apple.com/videos/play/wwdc2021/102/">Platforms State of the Union</a> wird auch „Developer Keynote” genannt – und das aus gutem Grund. Sie enthüllt und demonstriert im Grunde alle neuen, tollen APIs, so wie die Keynote die neuen Produkte und Services vorstellt. Sie richtet sich gezielt an Entwickler, sodass mehr ins Detail gegangen werden kann, und wenn du während der gesamten WWDC nur Zeit für ein einziges Video hast, dann würde ich dieses empfehlen, weil es quasi eine Zusammenfassung der restlichen Woche ist. Es ist auch eine tolle Möglichkeit, Themen zu finden, die dich interessieren. Alle hier erwähnten Technologien haben eigene Sessions im Laufe der Woche.</p><p><strong>Sessions</strong>
Die <a href="https://developer.apple.com/videos/wwdc2021/">über 200 Session-Videos</a> bilden den Kern der WWDC und sind der beste Ort, um die neuesten Technologien im Detail kennenzulernen. Ein guter Ort zum Anschauen ist die spezielle <a href="https://apps.apple.com/us/app/apple-developer/id640199958">Developer App</a>, die kürzlich auch Unterstützung für <a href="https://developer.apple.com/shareplay/">SharePlay</a> bekommen hat – so kannst du sogar eine Session gemeinsam mit jemandem remote schauen und direkt darüber diskutieren. Apple gruppiert die Sessions nach Themen, sodass du die Videos in der App einfach <strong>filtern</strong> kannst. Du kannst Sessions auch als <strong>Lesezeichen</strong> speichern, um sie später anzuschauen. Das mache ich direkt nach der Developer Keynote.</p><p><img src="/assets/images/blog/making-the-most-of-wwdc-2022/apple.webp" alt="" loading="lazy" /></p><p>Falls dich die schiere Menge an Sessions überwältigt: Ein guter Einstieg ist, nach Sessions mit dem Titel „What’s new in …” oder „Meet …” Ausschau zu halten, da diese Zusammenfassungen für ein bestimmtes Thema oder Framework sind. In diesen Sessions werden dann andere Sessions erwähnt, um bei Bedarf tiefer in bestimmte Details einzutauchen.</p><p>Um das Meiste aus den Sessions herauszuholen, mache ich mir gerne Notizen, damit ich etwas Interessantes später leicht wiederfinden kann, und es hilft auch, die Aufmerksamkeit aufrechtzuerhalten. Hier sind die Notizen, die ich während der <a href="https://www.notion.so/Cihat-WWDC-2021-Notes-6cae8d046c17426f8dafddc00abdae29">WWDC 2021</a> und <a href="https://www.notion.so/Cihat-WWDC-2020-Notes-5891eff2d250446f914110f8f008925d">WWDC 2020</a> gemacht habe. Ich empfehle dir, das auch zu tun, und wenn du es machst, überlege doch, deine Notizen zum Community-Projekt <a href="https://www.wwdcnotes.com/">WWDC Notes</a> beizutragen, um anderen Entwicklern in der Zukunft zu helfen.</p><p><strong>Labs</strong>
Wenn du Probleme mit einer Apple-Technologie hast, etwa bei der Integration ins System oder mit Xcode, oder wenn du einfach nicht verstehst, wie du ein Framework für einen bestimmten Anwendungsfall nutzen könntest, weil die Dokumentation fehlt, dann sind die <a href="https://developer.apple.com/wwdc22/labs/">Labs</a> eine großartige Gelegenheit, mit einem Apple-Ingenieur zu sprechen und direktes Feedback von den Leuten zu bekommen, die diese Dinge implementiert haben und alle Details kennen. Es gibt auch ein Lab für Hilfe beim App Review und eines für Design-Feedback zu deiner App.</p><p>Du kannst in der Developer App einen Termin anfragen, wenn du mit einer Apple ID eingeloggt bist, die Teil des kostenpflichtigen Apple Developer Program ist, oder wenn du diesjähriger Student Challenge-Gewinner bist. Frage frühzeitig an und beachte:</p><blockquote><p><strong>Da die Verfügbarkeit begrenzt ist, werden Anfragen geprüft und du erhältst am Vorabend deines Labs um 22:00 Uhr PT eine E-Mail mit deinem Status.</strong></p></blockquote><p><strong>Challenges</strong>
Apple hat bei der WWDC 2021 etwas Neues ausprobiert und 25 „Challenges” angeboten; auf der offiziellen WWDC22-Seite werden „tägliche Coding- und Design-Challenges” erwähnt, es wird sie also wieder geben. Die meisten Entwickler scheinen sie letztes Jahr verpasst zu haben (und nein, ich meine nicht die Swift Student Challenge!).</p><p>Apple macht es unnötig schwer, sie zu erkunden – ich konnte online keinen guten Überblick finden, den ich verlinken könnte. Ich fand nur Links zu denen, die ein <a href="https://developer.apple.com/documentation/realitykit/wwdc21_challenge_framework_freestyle">begleitendes Beispielprojekt</a> haben, weil die Downloadseite dann zu einem <a href="https://developer.apple.com/news/?id=zpb2xcfr&ref=fline.dev">News-Artikel</a> verlinkt. Der einfachste Weg, sie alle zu finden, war für mich die Suche nach „Challenge” in der Developer App:</p><p><img src="/assets/images/blog/making-the-most-of-wwdc-2022/apple-2.webp" alt="" loading="lazy" /></p><p>Ich bin mir nicht sicher, ob viele Leute an diesen Challenges teilgenommen haben – das Developer Forum hat nur <a href="https://developer.apple.com/forums/tags/wwdc21-challenges">8 Threads mit dem offiziellen Tag</a> vom letzten Jahr. Aber wenn du genug Zeit hast und eher der „Learning by Doing”-Typ bist, schau dir die diesjährigen Challenges an!</p><h3 id="community">Community</h3><p>Welche Lernmöglichkeiten andere aus der Community während der Woche anbieten.</p><p><strong>WWDC Notes</strong>
<a href="https://www.wwdcnotes.com/">Dieses tolle Community-Projekt</a>, organisiert von <a href="https://www.twitter.com/zntfdr">Federico Zanetello</a>, ist eine großartige Ressource, um zu erfahren, was die verschiedenen Sessions beinhalten, ohne sie alle ansehen zu müssen. Zwar sind noch nicht alle Sessions abgedeckt, aber wie oben erwähnt – wenn wir alle unsere Notizen dort zusammentragen, können wir das dieses Jahr leicht ändern. Es dient auch als Archiv für WWDC-Inhalte, die bis zur <a href="https://www.wwdcnotes.com/events/wwdc10/">WWDC 2010</a> zurückreichen. Apple bietet derzeit nur Videos bis zur <a href="https://developer.apple.com/videos/all-videos/?q=WWDC+2014&ref=fline.dev">WWDC 2014</a> an, aber sie entfernen jedes Jahr stillschweigend einige ältere Videos, und bei weitem nicht alle Inhalte von 2014 sind noch verfügbar.</p><p><strong>Artikel, Podcasts und mehr</strong>
Natürlich werden alle iOS-Dev-Content-Creator die Inhalte von Apple nicht nur <strong>konsumieren</strong>, sondern auch darüber schreiben, sprechen oder streamen. Ich plane sogar, die ganze Woche live zu streamen, während ich die Sessions schaue, die mich interessieren, und Notizen mache – ihr könnt gerne <a href="https://www.twitch.tv/Jeehut">auf Twitch dazukommen</a>, um neue APIs im Chat zu diskutieren!</p><p><a href="https://twitter.com/johnsundell">John Sundell</a> deckt normalerweise WWDC-Inhalte ab, sowohl in seinem <a href="https://swiftbysundell.com/podcast/">Podcast</a> als auch <a href="https://swiftbysundell.com/articles/">Blog</a>. <a href="https://twitter.com/twostraws">Paul Hudson</a> schreibt die ganze Woche über tolle Zusammenfassungen <a href="https://www.hackingwithswift.com/articles">in seinem Blog</a>. In den letzten zwei Jahren hat er auch eine schöne Übersicht aller WWDC-bezogenen Inhalte in <a href="https://github.com/twostraws/wwdc">diesem Repository</a> zusammengestellt – vielleicht macht er das dieses Jahr wieder? Falls nicht, schau dir <a href="https://iosdevblogs.com/">diesen iOS-Dev-Feed-Aggregator</a> von <a href="https://twitter.com/ay8s">Andrew Yates</a> an, der auf dem <a href="https://iosdevdirectory.com/">iOS Dev Directory</a> von <a href="https://twitter.com/daveverwer">Dave Verwer</a> basiert – ich bin sicher, viele davon werden WWDC-Inhalte während der Woche abdecken.</p><p><strong>Dub Dub Series</strong>
Ähnlich wie die oben erwähnten „Challenges” von Apple hat <a href="https://twitter.com/jordibruin">Jordi Bruin</a> kürzlich eine Reihe von Coding-Challenges namens <a href="https://www.swiftuiseries.com/">SwiftUI Series</a> organisiert. Im Gegensatz zu Apples Challenges hatten diese Community-getriebenen Challenges 3 Juroren pro Thema, die sich das Projekt anschauten und in einem Livestream-Video Feedback gaben. Und Jordi plant, dasselbe für den 10. Juni zu organisieren, direkt nach dem Ende der WWDC, mit der <a href="https://www.swiftuiseries.com/dubdubseries">Dub Dub Series</a>. Details gibt es noch nicht, aber wenn es auch nur annähernd so wird wie die SwiftUI Series, wird es ziemlich cool – und diesmal auf die <strong>neuen</strong> APIs fokussiert.</p><h2 id="vernetzen">Vernetzen</h2><p><img src="/assets/images/blog/making-the-most-of-wwdc-2022/connecting.webp" alt="" loading="lazy" /></p><h3 id="apple">Apple</h3><p><strong>Special Event im Apple Park</strong>
Wie oben erwähnt, veranstaltet Apple am ersten Tag der WWDC-Woche ein <a href="https://developer.apple.com/wwdc22/special-day/">Special Event im Apple Park</a>. Anmeldungen sind bereits geschlossen – wer es nicht geschafft hat, hat leider Pech gehabt. Aber für die wenigen Glücklichen gibt es die Möglichkeit, andere Entwickler persönlich zu treffen und die Keynotes gemeinsam zu erleben. <a href="https://twitter.com/twostraws/status/1529467321420349441?s=20&t=Nf85FLBFL-iL-pyXjDAA1g&ref=fline.dev">Apple bietet viele Gelegenheiten</a> dafür im Laufe des Tages, inklusive Frühstück, Mittagessen und sogar Führungen durch den Apple Park.</p><h3 id="community">Community</h3><p><strong>WWDC22 Discord</strong>
Aktive Community-Mitglieder wie <a href="https://twitter.com/mikaela__caron">Mikaela Caron</a> haben einen „WWDC22”-Space in <a href="https://discord.com/">Discord</a> erstellt, dem du über <a href="https://discord.com/invite/6XWE2SGZ">diesen Einladungslink</a> beitreten kannst, um Treffen rund um die Bay Area während der WWDC-Woche zu organisieren – zum Beispiel ein <a href="https://twitter.com/jordibruin/status/1526953936409833472">Sonntagsabendessen</a>, organisiert von <a href="https://twitter.com/jordibruin">Jordi Bruin</a>. Falls du Discord nicht kennst: Es ist im Grunde wie Slack, aber mit einer stärkeren Gaming- und Audio-Call-Geschichte. Deshalb eignet sich der Discord-Space auch gut für Diskussionen! Schau doch während der WWDC mal bei Discord vorbei, um Entwickler zu treffen und neue APIs zu diskutieren! Der Discord-Space hatte zum Zeitpunkt dieses Artikels ca. 300 Mitglieder.</p><p><strong>iOS Developers Slack</strong>
Schon vor einiger Zeit hat die Community einen <a href="https://slack.com/">Slack</a>-Space für iOS-Entwickler gestartet, um miteinander in Kontakt zu bleiben. Du kannst über <a href="https://ios-developers.io/">diese Website</a> beitreten. Mit über 22.000 Mitgliedern bin ich sicher, dass während der Woche viele Leute im dedizierten <code>#wwdc</code>-Channel sein werden, um die neuesten APIs zu diskutieren.</p><p><strong>WWDC Community Week</strong>
<a href="https://wwdc.community/">Diese spezielle Website</a> versucht, die Community während der WWDC-Woche zusammenzubringen, indem sie <a href="https://wwdcwatch.party/">Keynote Watch Parties</a> und andere Events organisiert und auflistet, wie Twitter „Spaces” (live, interaktive Audio-Diskussionen) – zum Beispiel den <a href="https://twitter.com/stefanjblos/status/1529475899736965128">Mega-Pre-WWDC Twitter Space</a> vor der WWDC oder die <a href="https://twitter.com/iosdevhappyhour/status/1529920449500434432">iOS Dev Happy Hour</a> während der Woche. Sie organisieren auch Treffen (vor Ort und online), einen Community-Hackathon und sammeln denkwürdige Momente der Community auf einem Mural. Außerdem haben sie gerade ihren eigenen Discord-Server eingeführt – du kannst <a href="https://discord.com/invite/3P94atxcV5">hier beitreten</a>.</p><hr /><blockquote><p>✨ Möchtest du hier deine Anzeige sehen? Kontaktiere mich unter <a href="mailto:ads@fline.dev">ads@fline.dev</a>.</p></blockquote><hr /><h2 id="diskutieren">Diskutieren</h2><p><img src="/assets/images/blog/making-the-most-of-wwdc-2022/discussing.webp" alt="" loading="lazy" /></p><h3 id="apple">Apple</h3><p><strong>Digital Lounges</strong>
Wie <a href="https://developer.apple.com/news/?id=a5aw05t9&ref=fline.dev">letztes Jahr</a> wird Apple auch dieses Jahr wieder <a href="https://developer.apple.com/wwdc22/digital-lounges/">Digital Lounges</a> anbieten. Das waren im Grunde kontrollierte Slack-Channels, die nur zu bestimmten Zeiten geöffnet sind und für die man sich vorab registrieren muss – die Registrierung öffnet am 31. Mai und erfordert eine Apple Developer Membership oder den Gewinn der Student Challenge.</p><p><strong>Forums</strong>
Die <a href="https://developers.apple.com/forums/">Apple Developer Forums</a> bekommen ebenfalls <a href="https://developer.apple.com/wwdc22/forums/">4 dedizierte Tags</a>, um neue APIs zu diskutieren und Fragen dazu zu stellen, mit der Chance, direkt von Apple-Ingenieuren eine Antwort zu bekommen. Ich bevorzuge zwar die <a href="https://www.discourse.org/">Forum-Technologie</a>, die Apple in den <a href="https://forums.swift.org/">Swift Forums</a> verwendet, gegenüber dieser eigenen Implementierung, aber manche der kniffligeren Fragen werden nur hier beantwortet, also kann es manchmal ein Lebensretter sein!</p><h3 id="community">Community</h3><p>Natürlich kannst du neue APIs auch in einem der oben unter „Vernetzen” erwähnten Discord-Server oder dem Slack-Server diskutieren. Hier sind noch weitere Optionen:</p><p><strong>Dub Dub Together</strong>
<a href="https://wwdctogether.com/">Diese Website</a>, erstellt von <a href="https://twitter.com/onmyway133">Khoa</a>, ist ein Ort, an dem du beide Keynotes anschauen und live mit anderen Entwicklern darüber chatten kannst – alles auf einem Bildschirm. Theoretisch könntest du die erste Keynote auch auf YouTube schauen und dort chatten, aber für die Developer Keynote geht das nicht, und im YouTube-Chat schreiben auch viele Nicht-Entwickler mit. Also durchaus eine Überlegung wert!</p><p><strong>Livestreams</strong>
Einige bekannte Entwickler-Seiten wie <a href="https://www.raywenderlich.com/34291068-don-t-miss-our-wwdc-2022-livecast-june-6-9pm-edt">RayWenderlich</a> werden das Event live streamen und die APIs besprechen, während sie vorgestellt werden. Ich habe schon erwähnt, dass ich auch streamen werde, und du findest vielleicht auch andere <a href="https://iosdevdirectory.com/#twitch-en">Twitch-Streamer</a>, die dasselbe machen. Ich habe sogar einige kontaktiert, um gemeinsam in unseren Streams über APIs zu diskutieren. Bitte beachte, dass Apple es nicht erlaubt, die Keynote oder Sessions weiterzuverbreiten – du musst also die Inhalte von Apple auf einem zweiten Gerät öffnen, nur als Hinweis.</p><p>Ich hoffe, dass dir all diese Informationen helfen, eine tolle WWDC 2022 zu erleben. Hoffen wir, dass alle <a href="https://www.fline.dev/my-top-3-wishes-for-wwdc-2022/">unsere Wünsche</a> in Erfüllung gehen!</p><blockquote><p>💁🏻‍♂️ <strong>Hat dir dieser Artikel gefallen? Schau dir meine App <strong>RemafoX</strong> an!</strong>
Eine native Mac-App, die sich in Xcode integriert und dir hilft, <strong>deine</strong> App zu übersetzen.
<a href="https://apps.apple.com/app/apple-store/id1605635026?pt=549314&ct=fline.dev&mt=8&ref=fline.dev"><strong>Jetzt herunterladen</strong></a>, um Zeit bei der Entwicklung zu sparen und die Lokalisierung einfach zu machen.</p></blockquote>]]></content:encoded>
</item>
<item>
<title>Open-Source-Entwicklung auf Twitch streamen – Teil 2</title>
<link>https://fline.dev/de/blog/streaming-open-source-development-on-twitch-part-2/</link>
<guid isPermaLink="true">https://fline.dev/de/blog/streaming-open-source-development-on-twitch-part-2/</guid>
<pubDate>Fri, 15 Apr 2022 00:00:00 +0000</pubDate>
<description><![CDATA[Mein Software-Setup und genutzte Drittanbieter-Dienste.]]></description>
<content:encoded><![CDATA[<h2 id="tldr">TL;DR</h2><p>**Software: **<a href="https://streamelements.com/obslive">Broadcasting-Software</a>, <a href="https://github.com/keycastr/keycastr">Tastenkuerzel-Tool</a>
**Drittanbieter-Dienste: **<a href="https://streamelements.com/">Community-Aufbau</a>, <a href="https://rogueamoeba.com/loopback/">Audio-Routing</a>, <a href="https://www.pretzel.rocks/">Hintergrundmusik</a></p><h2 id="software-setup">Software-Setup</h2><p>Schau dir <a href="https://jeehut.medium.com/streaming-open-source-development-on-twitch-part-1-1926b9b7e051?sk=c828dafc91b82fc902819cb69d447cd7&ref=fline.dev">den ersten Teil</a> dieser Serie an, um zu erfahren, welche Hardware ich verwende. Hier geht es jetzt um die Software-Seite – die wichtigsten Streaming-relevanten Apps inklusive Preis, meinen Einstellungen und einigen Alternativen.</p><h3 id="broadcasting-software-selive-obs-mit-plugins">Broadcasting-Software: <a href="https://streamelements.com/obslive">SE.Live (OBS mit Plugins)</a></h3><p><img src="/assets/images/blog/streaming-open-source-development-on-twitch-part-2/juan-gomez-10.webp" alt="" loading="lazy" /></p><p><strong>Beschreibung:</strong>
Das Herzstueck jedes Streams ist die Broadcasting-Software. Sie erzeugt das Live-Video und sendet es als Kombination vieler verschiedener Quellen an Twitch (oder andere Plattformen wie YouTube). Ich nutze sie, um meinen Bildschirm und meine Webcam aufzunehmen, einen Green-Screen-Filter anzuwenden, mein Mikrofon-Audio aufzuzeichnen, Filter auf das Audio anzuwenden, Hintergrundmusik abzuspielen, Stream-bezogene Alerts wie Follows anzuzeigen, Sound-Alerts bereitzustellen und zwischen verschiedenen Szenen zu wechseln – etwa wenn ich den Stream starte, beende oder eine Pause mache.</p><p>Das ist ziemlich viel fuer eine einzelne Software, daher ist es wirklich wichtig, hier das richtige Tool zu waehlen. Die gute Nachricht: Es gibt ein Open-Source-Projekt dafuer namens „Open Broadcaster Software”, kurz „OBS”, mit <a href="https://github.com/obsproject/obs-studio">37k Sternen auf GitHub</a> und vielen Features, auf denen andere Unternehmen aufbauen koennen. Ich nutze die StreamElements-Variante, weil sie dem originalen OBS Studio sehr aehnlich ist, aber zusaetzliche Plugins fuer den Community-Aufbau mitbringt.</p><p><strong>Preis:</strong> Kostenlos (die OBS-Basis ist vollstaendig <a href="https://github.com/obsproject/obs-studio">Open Source</a>, die Plugins nicht)</p><p><strong>Meine Einstellungen:</strong>
Ich habe im Grunde zwei Arten von Szenen eingerichtet: Wenn ich nicht sichtbar bin und wenn ich es bin. Fuer Ersteres habe ich ein schoenes Hintergrund-Video von <a href="https://pixabay.com/videos/">Pixabay</a> heruntergeladen und eine abgedunkelte Ebene darueber gelegt, damit der Bildschirm lebendig wirkt, auch wenn ich nicht da bin. Ich habe 3 Szenen erstellt (vorher/Pause/nachher) mit im Grunde dem gleichen Aufbau, aber unterschiedlichem Text:</p><p><img src="/assets/images/blog/streaming-open-source-development-on-twitch-part-2/juan-gomez-7.webp" alt="" loading="lazy" /></p><p>Fuer die Zeiten, in denen ich streame, habe ich ebenfalls 3 Szenen: Eine mit einem Pomodoro-Timer (aktuell nutze ich die <a href="https://apps.apple.com/us/app/flow-focus-pomodoro-timer/id1423210932">Flow</a>-App, werde aber meinen eigenen <a href="https://github.com/FlineDevPublic/OpenFocusTimer">Open Focus Timer</a> verwenden, sobald er fertig ist), eine ohne Timer (z. B. am Ende des Streams) und eine mit zensiertem Bildschirm (z. B. wenn ich mich irgendwo einloggen muss). Nach vielen Versuchen habe ich mich entschieden, mich unten rechts im Bild zu platzieren (aber nicht ganz am Rand):</p><p><img src="/assets/images/blog/streaming-open-source-development-on-twitch-part-2/juan-gomez-3.webp" alt="" loading="lazy" /></p><p>Fuer meine Webcam habe ich einen „<a href="https://obsproject.com/cs/kb/chroma-key-filter">Chroma Key</a>”-Filter fuer den Green Screen eingerichtet:</p><p><img src="/assets/images/blog/streaming-open-source-development-on-twitch-part-2/juan-gomez.webp" alt="" loading="lazy" /></p><p>Meine Audio-Einstellungen sind etwas fortgeschrittener. Zunaechst meine Mikrofon-Einstellungen. Ich verwende 4 Filter fuer mein Mikrofon, die alle von Audio-Profis empfohlen werden:</p><p><img src="/assets/images/blog/streaming-open-source-development-on-twitch-part-2/juan-gomez-9.webp" alt="" loading="lazy" /></p><p>„<a href="https://obsproject.com/cs/kb/noise-suppression-filter">Noise Suppression</a>” hilft, Hintergrundgeraeusche wie laute Luefter herauszufiltern.</p><p><img src="/assets/images/blog/streaming-open-source-development-on-twitch-part-2/juan-gomez-5.webp" alt="" loading="lazy" /></p><p>Mit dem „<a href="https://obsproject.com/cs/kb/noise-gate-filter">Noise Gate</a>” kann ich das Mikrofon komplett „ausschalten”, wenn ein bestimmter Lautstaerke-Schwellenwert nicht erreicht wird. Das hilft, Tastaturtippen zu entfernen, wenn ich nicht spreche.</p><p><img src="/assets/images/blog/streaming-open-source-development-on-twitch-part-2/juan-gomez-6.webp" alt="" loading="lazy" /></p><p>Manchmal koennen Leute sehr laut werden, wenn sie von etwas ueberrascht werden. Stell dir vor, ich besuche eine Website und es gibt einen Jump Scare. Um meine Zuschauer nicht mit einem ploetzlichen lauten Geraeusch von mir zu belaestigen, habe ich einen „<a href="https://obsproject.com/ko/kb/compressor-filter">Compressor</a>” eingerichtet, der jede Lautstaerke ueber einem bestimmten Schwellenwert um den Faktor 10 „komprimiert”.</p><p><img src="/assets/images/blog/streaming-open-source-development-on-twitch-part-2/juan-gomez-4.webp" alt="" loading="lazy" /></p><p>Schliesslich macht der „<a href="https://obsproject.com/fr/kb/limiter-filter">Limiter</a>” etwas sehr Aehnliches wie ein Compressor – anstatt das ueberschuessige Audio mit einem anpassbaren Faktor zu „komprimieren”, verwendet er einfach immer den Faktor „unendlich” und begrenzt die Lautstaerke damit auf ein vorgegebenes Maximum.</p><p>Weiter zu anderen Audio-Einstellungen. Zwei Dinge sollten hier erwaehnt werden: Erstens gibt es ein Konzept namens „Monitor”. Ein Monitor ist das Audio, das du als Streamer zurueckhoerst. Wenn du beispielsweise den „Monitor” fuer dein Mikrofon einschaltest, hoerst du dich selbst ueber deine Kopfhoerer. Ich habe ihn fuer alles ausser dem Mikrofon aktiviert, weil es sich seltsam anfuehlt, sich selbst (mit etwas Verzoegerung) zu hoeren. Zweitens kannst du fuer jede Audioquelle eine Audiospur-Nummer festlegen. Das ist nuetzlich, wenn du deine Videos spaeter bearbeiten moechtest, z. B. um fokussiertere YouTube-Videos zu produzieren (was ich vorhabe). Ich lege zum Beispiel meine Stimme auf eine eigene Spur, die Hintergrundmusik auf eine andere und alle Sound-Alerts auf wieder eine andere Spur. So kann ich die Videoquelle schneiden und Teile meiner Stimme behalten, ohne dass die Hintergrundmusik „springt”. Und ich kann Alert-Sounds komplett entfernen oder ihre Lautstaerke unabhaengig aendern, wenn noetig. Hier siehst du das:</p><p><img src="/assets/images/blog/streaming-open-source-development-on-twitch-part-2/juan-gomez-8.webp" alt="" loading="lazy" /></p><p>Zusaetzlich habe ich eingestellt, dass alle meine Streams automatisch als <code>.mkv</code>-Datei mit allen Audiospuren von 1 bis 5 aufgezeichnet werden. Hier siehst du das:</p><p><img src="/assets/images/blog/streaming-open-source-development-on-twitch-part-2/juan-gomez-2.webp" alt="" loading="lazy" /></p><p>Das war eine Auswahl meiner OBS-Einstellungen, aber nicht alle. Mit OBS kann man so viel machen, dass es einschuechtend wirken kann. Zum Beispiel habe ich <a href="https://obsproject.com/eu/kb/browser-source">Browser-Quellen</a> uebersprungen.</p><p><strong>Alternativen:</strong>
– <a href="https://streamlabs.com/">Streamlabs Desktop</a>: Kostenlos (<a href="https://github.com/stream-labs/desktop">Open Source</a>), eigene OBS-Oberflaeche (Geschmackssache)
– <a href="https://www.ecamm.com/mac/ecammlive/">Ecamm Live</a>: 400 $ pro Jahr, native Mac-App, einfach, aber eingeschraenkte Flexibilitaet</p><h3 id="tastenkuerzel-tool-keycastr">Tastenkuerzel-Tool: <a href="https://github.com/keycastr/keycastr">KeyCastr</a></h3><p><img src="/assets/images/blog/streaming-open-source-development-on-twitch-part-2/juan-gomez.webp" alt="" loading="lazy" /></p><hr /><blockquote><p>Want to see your ad here? Contact me at <a href="mailto:ads@fline.dev">ads@fline.dev</a> to get in touch.</p></blockquote><hr /><h2 id="drittanbieter-dienste">Drittanbieter-Dienste</h2><p>Das sind die Drittanbieter-Dienste, die ich nutze und die jeder Streamer in Betracht ziehen sollte.</p><h3 id="community-aufbau-streamelements">Community-Aufbau: <a href="https://streamelements.com/">StreamElements</a></h3><p><img src="/assets/images/blog/streaming-open-source-development-on-twitch-part-2/how-to-add-an-alertbox.webp" alt="" loading="lazy" /></p><p><strong>Beschreibung:</strong>
Was Streaming richtig spassig macht, ist die Interaktion mit deinen Zuschauern. Waehrend Twitch als Streaming-Plattform einen integrierten Chat bietet, gibt es noch viel mehr Moeglichkeiten, mit deiner Community zu interagieren. Du moechtest vielleicht in deinem Stream einen Alert anzeigen, wenn dir jemand folgt, damit sich die Zuschauer wirklich als Teil des Streams fuehlen. Oder du moechtest mit einer Bildschirm-Animation feiern, wenn ein anderer Streamer dich raidet. Und du hast wahrscheinlich einige Erinnerungen wie Links zu deinen Social-Media-Profilen, die du mit deinen staendig wechselnden Zuschauern teilen moechtest. Diese und weitere Features werden von Drittanbieter-Diensten bereitgestellt, und ich empfehle dir dringend, mindestens einen davon zu nutzen, um mit deinen Zuschauern verbunden zu bleiben und ueber die Zeit eine Fan-Community aufzubauen.</p><p><strong>Preis:</strong> Kostenlos (sie nehmen einen Anteil, wenn du ihren <a href="https://streamelements.com/merch">Merch Store</a> oder <a href="https://blog.streamelements.com/series-a-and-brand-partnerships-3f08f2b7c314">Partnerschaften</a> nutzt)</p><p><strong>Meine Einstellungen:</strong>
StreamElements bietet viele Features, aber ich nutze hauptsaechlich 2 davon:</p><p>Erstens nutze ich ihre Alerts- und Overlays-Funktion mit einem benutzerdefinierten Overlay. Die zwei wichtigsten Ebenen, die ich eingerichtet habe, sind die Alert Box und Kappagen:</p><p><img src="/assets/images/blog/streaming-open-source-development-on-twitch-part-2/how-to-add-an-alertbox-2.webp" alt="" loading="lazy" /></p><p><strong>Beschreibung:</strong>
Apple sichert macOS auf vielen Ebenen ab. Eine davon ist der Schutz von System-Audio: Standardmaessig gibt es keine Moeglichkeit, die Audio-Ausgabe anderer Apps in OBS zu leiten, damit deine Zuschauer dasselbe hoeren wie du. Das kann nuetzlich sein, z. B. wenn du Hintergrundmusik in einer separaten Musik-App abspielen oder ein Tutorial-Video anschauen und live kommentieren moechtest. Es gibt zwar eine Moeglichkeit, einige Sicherheitsfunktionen zu deaktivieren und Apps Zugriff auf System-Audio zu geben, aber OBS unterstuetzt das (noch) nicht. Du brauchst also zusaetzliche Software, die Audio von anderen Apps oder deinem System in ein virtuelles „Fake-Mikrofon” leitet, das du dann in OBS als Quelle wie jedes andere Mikrofon hinzufuegen kannst. Eine schoene detaillierte Slideshow zur Einrichtung des Tools auf Intel- und M-Chip-Macs findest du <a href="https://rogueamoeba.com/support/knowledgebase/?showArticle=ACE-StepByStep&product=Loopback&ref=fline.dev">hier</a>.</p><p><strong>Preis:</strong> 99 $ (Einmalzahlung)</p><p><strong>Meine Einstellungen:</strong>
Die wichtigste Einstellung hier ist das „Video Content”-Geraet, weil es mir das Audio von Videos liefert, die ich mit QuickTime oder Safari abspielen koennte.</p><p><img src="/assets/images/blog/streaming-open-source-development-on-twitch-part-2/my-preferred-pretzel-4.webp" alt="" loading="lazy" /></p><p><strong>Alternativen:</strong>
– <a href="https://github.com/ExistentialAudio/BlackHole">Blackhole</a>: Kostenlos (Open Source) – habe es nicht ausprobiert, sieht aber sehr aehnlich aus</p><h3 id="hintergrundmusik-pretzel">Hintergrundmusik: <a href="https://www.pretzel.rocks/">Pretzel</a></h3><p><img src="/assets/images/blog/streaming-open-source-development-on-twitch-part-2/my-preferred-pretzel.webp" alt="" loading="lazy" /></p><p><strong>Beschreibung:</strong>
Eine Coding-Session ohne entspannende Hintergrundmusik ist fuer mich sehr ermuedend. Ich finde, es ist sogar noch wichtiger, gute Hintergrundmusik zu haben, wenn man live streamt, da man wahrscheinlich nicht die ganze Zeit redet und die Pausen als Zuschauer seltsam wirken koennen, wenn es gar kein Geraeusch gibt. Gleichzeitig ist die Lizenzierung von Musik ein kompliziertes Thema, und du darfst nicht alles auf Twitch streamen. In meinem Fall lade ich meine Streams auch auf YouTube hoch, und die Regeln unterscheiden sich zwischen den Plattformen. Generell ist auf Twitch mehr erlaubt, und noch mehr ist erlaubt, wenn du die „Video-on-Demand”-Funktion auf Twitch deaktivierst, sodass Zuschauer die Musik nur live hoeren koennen und es keine Moeglichkeit gibt, sie spaeter anzuschauen.</p><p>Wenn du aber Aufzeichnungen deiner Streams langfristig bereitstellen moechtest, musst du „lizenzfreie” Musik finden oder zumindest eine Lizenz fuer die Musik haben, die du nutzt – sonst riskierst du einen <a href="https://www.dmca.com/FAQ/What-is-DMCA">DMCA</a>-Takedown von (Teilen) deiner Videos. Wenn du das auf YouTube oder Twitch wiederholt tust, kann dein Kanal sogar gesperrt werden. Geh kein Risiko ein und stelle sicher, dass du dich an die Regeln haeltst.</p><p><strong>Preis:</strong> Freemium (15 $/Monat, um den Chatbot zu entfernen, der jeden Song postet)</p><p><strong>Meine Einstellungen:</strong>
Ich habe immer „YouTube Safe” aktiviert. Frueher hatte ich „Instrumental” aktiviert und habe die Sender „LoFi” und „Chill” viel gehoert. Aber in letzter Zeit habe ich angefangen, auch den „Rock”-Sender zu hoeren, und mit diesem Filter gab es keine Songs, also lasse ich den „Instrumental”-Filter jetzt deaktiviert.</p><p><img src="/assets/images/blog/streaming-open-source-development-on-twitch-part-2/my-preferred-pretzel-3.webp" alt="" loading="lazy" /></p><p><em>Meine bevorzugten Pretzel-Einstellungen.</em></p><p><img src="/assets/images/blog/streaming-open-source-development-on-twitch-part-2/lofi-chill-and-rock-are.webp" alt="" loading="lazy" /></p><p><em>LoFi, Chill und Rock gehoeren zu meinen Lieblingssendern.</em></p><p><strong>Alternativen:</strong>
– <a href="https://www.monstercat.com/gold">Monstercat Gold</a>: 7,50 $/Monat, unterstuetzt Musik-Download
– Playlists auf Apple Music / Spotify / Amazon Music, z. B. von <a href="https://anjunabeats.lnk.to/Twitch">Anjunabeats</a>
– Hier ist eine <a href="https://streamermusic.com/dmca-safe-music/">Liste weiterer Alternativen</a></p><h2 id="was-kommt-als-naechstes">Was kommt als Naechstes</h2><p>Das war’s mit der Software, die ich nutze, und wie ich sie konfiguriert habe – das sollte dich einen weiteren Schritt naeher an deine eigenen Livestreams bringen. In Teil 3 dieser Serie werde ich meine Systemeinstellungen und einige weitere Tipps behandeln, die du beim Streamen von Softwareentwicklung auf Twitch beachten solltest. Folge mir, um nichts zu verpassen!</p><blockquote><p><strong>Dir hat dieser Artikel gefallen? Schau dir meine App RemafoX an!</strong>
Eine native Mac-App, die sich in Xcode integriert und dir hilft, <strong>deine</strong> App zu uebersetzen.
<a href="https://apps.apple.com/app/apple-store/id1605635026?pt=549314&ct=fline.dev&mt=8&ref=fline.dev"><strong>Jetzt herunterladen</strong></a>, um bei der Entwicklung Zeit zu sparen und Lokalisierung einfach zu machen.</p></blockquote>]]></content:encoded>
</item>
<item>
<title>Meine Top 3 Wünsche für die WWDC 2022</title>
<link>https://fline.dev/de/blog/my-top-3-wishes-for-wwdc-2022/</link>
<guid isPermaLink="true">https://fline.dev/de/blog/my-top-3-wishes-for-wwdc-2022/</guid>
<pubDate>Wed, 06 Apr 2022 00:00:00 +0000</pubDate>
<description><![CDATA[Apple hat die WWDC-Woche für den 6.–10. Juni angekündigt – schauen wir uns mal an, welche neuen Frameworks, APIs und Tools ich mir erhoffe und wie ihre Nutzung aussehen könnte, inklusive Beispielen.]]></description>
<content:encoded><![CDATA[<blockquote><p>✨ 2023 Update: Ich habe 3 weitere Wünsche in <a href="https://www.fline.dev/my-top-5-wishes-for-wwdc-2023/">diesem Nachfolgeartikel</a> ergänzt.</p></blockquote><p>Jedes Jahr gibt es diese ganz besondere Zeit, in der eine bestimmte Gruppe von Menschen Wünsche äußert und sich Hoffnungen auf alles Mögliche macht. Manche <a href="https://github.com/bhansmeyer/WWDC-2021-Community-Wishlist">teilen ihre Wünsche</a> mit anderen, manche behalten sie lieber für sich, um nicht zu enttäuscht zu werden, wenn sie nicht in Erfüllung gehen. Ich gehörte bisher zur letzteren Gruppe, aber diesmal möchte ich meine Wünsche teilen, um die Wahrscheinlichkeit zu erhöhen, dass sie wahr werden – wenn nicht dieses Jahr, dann vielleicht nächstes. Schließlich hört der Weihnachtsmann vielleicht zu.</p><p>Ich werde die offensichtlichen Themen überspringen, die auf so gut wie <em>jeder</em> iOS-Entwickler-Liste ganz oben stehen, wie ein stabileres Xcode, ein fehlerfreieres Swift, ein vollständigeres SwiftUI oder zuverlässigere SwiftUI Previews. Los geht’s!</p><h2 id="3-app-icons-in-allen-größen-aus-einem-einzigen-bild-importieren">#3: App-Icons in allen Größen aus einem einzigen Bild importieren</h2><h3 id="problem">Problem</h3><p>Jede App braucht ein App-Icon. Xcode verlangt von uns, das App-Icon in Dutzenden verschiedener Größen bereitzustellen, ohne dabei Unterstützung beim Skalieren zu bieten. Es gibt zwar viele Apps und Tools, die dabei helfen, aber nur wenige davon unterstützen die neuesten Größen, da Apple gerne neue dazufügt. Das ist ein unnötiges Hindernis für neue Entwickler, die gerade erst anfangen.</p><p><img src="/assets/images/blog/my-top-3-wishes-for-wwdc-2022/the-temporary-app-icon.webp" alt="Das vorläufige App-Icon-Set meiner Open Focus Timer App" loading="lazy" /></p><p>Aber sie werden im Swift-Code sowieso alle als <code>Optional</code>-Typen generiert. Das und das gesamte <code>NSManagedObjectContext</code>-API-Design fühlt sich ziemlich veraltet und nicht besonders „<a href="https://www.swift.org/about/">Swifty</a>” an (also „nicht sicher”). Es wird Zeit für etwas Neues!</p><h3 id="lösung">Lösung</h3><p>Apple könnte ein neues, reines Swift-Framework einführen (wie <code>SwiftUI</code>), das vielleicht <code>SwiftData</code> heißt und eine High-Level-API zum Definieren und Verwalten persistierbarer Modelle bietet. Ein Modell zu definieren könnte dann so aussehen:</p><pre><code class="language-Swift">import SwiftData

actor Category: PersistableObject {
  @Persisted
  var colorHexCode: String

  @Persisted
  var iconSymbolName: String

  @Persisted
  var name: String

  @PersistedRelation(inverse: \CategoryGroup.categories)
  var group: CategoryGroup
}</code></pre><p>Für Modelle, die in iCloud gespeichert werden sollen, müsste man <code>distributed</code> vor <code>actor</code> setzen. Normalerweise würde der Zugriff auf eine Property eines <code>actor</code> das Schlüsselwort <code>await</code> erfordern, aber spezielle Property Wrapper könnten das vereinfachen zu:</p><pre><code class="language-Swift">import SwiftUI
import SwiftData

struct CategoryView: View {
  @PersistedObject
  var category: Category

  var body: some View {
    Label(
      self.category.name,
      systemImage: self.category.iconSymbolName
    )
    .foregroundColor(
      Color(hex: self.category.colorHexCode)
    )
  }
}</code></pre><p>Und das Schreiben auf eine <code>actor</code>-Property ist von außen nicht möglich, aber der <code>@Persisted</code>-Property-Wrapper könnte etwas <code>Binding</code>-Magie mitbringen, um das zu ermöglichen:</p><pre><code class="language-Swift">import SwiftUI
import SwiftData

struct CategoryView: View {
  @PersistedObject
  var category: Category

  var body: some View {
    TextField(&quot;Name&quot;, self.category.$name.bind())
  }
}</code></pre><p>Ich muss zugeben, ich habe Actors in der Praxis noch nicht verwendet, also verzeiht mir, wenn einige der obigen Beispiele keinen Sinn ergeben. Aber ich habe das Gefühl, dass Actors eine wichtige Rolle in einem <code>SwiftData</code>-Framework für sicheren Zugriff spielen könnten.</p><p>Zusätzlich könnte Xcode eine Oberfläche mitbringen, die es einfach macht, Datenmodelle zu versionieren, und ein grafisches Migrationstool bieten, das in deklarativer Swift-Syntax geschrieben und als UML-Diagramm auf der rechten Seite angezeigt werden könnte (wie SwiftUI Previews). Aber vielleicht habe ich hier angefangen, zu groß zu träumen …</p><h3 id="wahrscheinlichkeit">Wahrscheinlichkeit</h3><p>Viele haben das schon in den letzten zwei Jahren erwartet, weil es der logische nächste Schritt nach SwiftUI ist. Aber dieses Jahr, mit <a href="https://github.com/apple/swift-evolution/blob/main/proposals/0306-actors.md">Actors</a>, die bereits in Swift 5.5 ausgeliefert wurden, und <a href="https://github.com/apple/swift-evolution/blob/main/proposals/0336-distributed-actor-isolation.md">Distributed</a> <a href="https://github.com/apple/swift-evolution/blob/main/proposals/0344-distributed-actor-runtime.md">Actors</a> (für iCloud-Unterstützung), die erst kürzlich akzeptiert wurden, könnte die Technologie gerade bereit sein, die erste Version bis September auszuliefern.</p><h2 id="fazit">Fazit</h2><p>Es gibt vieles, das Apple im Juni ankündigen könnte, und die obigen Punkte sind nur meine persönlichen Wünsche. Aber in der Vergangenheit wurde ich immer von mindestens ein oder zwei Frameworks komplett überrascht, wie <a href="https://developer.apple.com/videos/play/wwdc2019/216/">SwiftUI</a> 2019, <a href="https://developer.apple.com/videos/play/wwdc2020/10028/">WidgetKit</a> 2020 und <a href="https://developer.apple.com/videos/play/wwdc2021/10166/">DocC</a> 2021. Was wird es dieses Jahr sein? Ich kann es kaum erwarten, es herauszufinden!</p>]]></content:encoded>
</item>
<item>
<title>Open-Source-Entwicklung auf Twitch streamen – Teil 1</title>
<link>https://fline.dev/de/blog/streaming-open-source-development-on-twitch-part-1/</link>
<guid isPermaLink="true">https://fline.dev/de/blog/streaming-open-source-development-on-twitch-part-1/</guid>
<pubDate>Thu, 31 Mar 2022 00:00:00 +0000</pubDate>
<description><![CDATA[Meine Streaming-Motivation und mein Hardware-Setup mit Bewertungen.]]></description>
<content:encoded><![CDATA[<h2 id="tldr">TL;DR</h2><p><strong>Motivation:</strong> Teilen mit der Community / Etwas zurueckgeben, Zeiteffizienz
<strong>Hardware-Setup:</strong> <a href="https://www.apple.com/shop/buy-mac/macbook-pro/16-inch-space-gray-10-core-cpu-16-core-gpu-1tb#">Mac</a>, <a href="https://www.apple.com/shop/buy-airpods/airpods-max/sky-blue">Headset</a>, <a href="https://www.logitech.com/en-us/products/webcams/c920s-pro-hd-webcam.960-001257.html">Webcam</a>, <a href="https://www.elgato.com/en/green-screen">Green Screen</a>, <a href="https://www.bluemic.com/en-us/products/yeti/">Mikrofon</a>, <a href="https://www.amazon.de/gp/product/B088WTSFS9/?language=en_US&ref=fline.dev">Licht</a></p><h2 id="motivation">Motivation</h2><p><img src="/assets/images/blog/streaming-open-source-development-on-twitch-part-1/tegan-mierle.webp" alt="" loading="lazy" /></p><p>**Preis: **~2.700 $</p><p><strong>Vorteile:</strong>
– M1 Pro baut 40 % schneller als M1, nur 2 % langsamer als M1 Max (<a href="https://github.com/devMEremenko/XcodeBenchmark#xcode-130-or-above">Quelle</a>)
– Doppelte Akkulaufzeit im Vergleich zu Intel-MacBooks (auch <a href="https://www.reddit.com/r/macbookpro/comments/qogsov/battery_life_comparison_between_m1_pro_and_m1_max/">~25 % laenger als M1 Max</a>)
– 1 TB bietet reichlich Platz fuer mehrere Xcodes, iOS-Simulatoren und 1080p-Streams</p><p><strong>Nachteile:</strong>
– Mehr als doppelt so teuer wie ein <a href="https://www.apple.com/shop/buy-mac/mac-mini/apple-m1-chip-with-8-core-cpu-and-8-core-gpu-512gb#">M1 Mac Mini</a> mit 16 GB + 1 TB – die 40 % kuerzere Build-Zeit kombiniert mit dem tollen Display und der Mobilitaet ist es aber wert
– Kein Spielraum mit nur 16 GB RAM (die 32-GB-Option kostet 400 $ extra!)</p><p><strong>Empfohlen?</strong> <strong>Oh ja.</strong> Der <a href="https://www.apple.com/shop/buy-mac/mac-studio/20-core-cpu-48-core-gpu-32-core-neural-engine-64gb-memory-1tb">M1 Ultra</a> baut allerdings nochmal 37 % schneller – ueberleg dir das mal!</p><p><strong>Alternativen:</strong>
– <a href="https://www.apple.com/shop/buy-mac/mac-mini/apple-m1-chip-with-8-core-cpu-and-8-core-gpu-512gb">M1 Mac mini</a> mit 16 GB RAM + 1 TB (~1.300 $)
– <a href="https://www.apple.com/shop/buy-mac/macbook-air/space-gray-apple-m1-chip-with-8-core-cpu-and-8-core-gpu-512gb">M1 MacBook Air</a> mit 16 GB RAM + 1 TB (~1.650 $)
– <a href="https://www.apple.com/shop/buy-mac/mac-studio/20-core-cpu-48-core-gpu-32-core-neural-engine-64gb-memory-1tb#">M1 Ultra Mac Studio</a> mit 64 GB RAM + 1 TB (~4.000 $)</p><h3 id="headset-airpods-max">Headset: <a href="https://www.apple.com/shop/buy-airpods/airpods-max/sky-blue">AirPods Max</a></h3><p><img src="/assets/images/blog/streaming-open-source-development-on-twitch-part-1/headset-airpods-max.webp" alt="" loading="lazy" /></p><p><strong>Preis:</strong> ~300 EUR (~335 $) – Hinweis: Ich habe sie <strong>gebraucht</strong> gekauft.</p><p><strong>Vorteile:</strong>
– Einfache Verbindung mit Apple-Geraeten
– Sehr lange Akkulaufzeit (ich muss kaum laden)
– Bequem auch nach stundenlangem Streamen</p><p><strong>Nachteile:</strong>
– Teuer im Neukauf (~550 $)
– Der Kabelmodus erfordert ein zusaetzliches <a href="https://www.apple.com/shop/product/MR2C2AM/A/lightning-to-35mm-audio-cable-12m">Lightning-auf-3,5-mm-Kabel</a> (~35 $)</p><p><strong>Empfohlen?</strong> Wenn du weitere Apple-Geraete besitzt: Ja. Sonst: Nein.</p><p><strong>Alternativen:</strong>
– <a href="https://www.logitechg.com/en-us/products/gaming-audio/pro-x-gaming-headset-blue-voice-mic-tech.981-000817.html">Logitech Pro X</a> (~90 $), gutes abnehmbares Mikrofon
– <a href="https://hyperx.com/products/hyperx-cloud-ii">HyperX Cloud II</a> (~70 $), ebenfalls sehr beliebt bei Gamern
– Jeder Kopfhoerer, den du bereits besitzt (wenn du dir sowieso ein externes Mikrofon holst)</p><h3 id="webcam-logitech-c920s-pro">Webcam: <a href="https://www.logitech.com/en-us/products/webcams/c920s-pro-hd-webcam.960-001257.html">Logitech C920S Pro</a></h3><p><img src="/assets/images/blog/streaming-open-source-development-on-twitch-part-1/webcam-logitech-c920s-pro.webp" alt="" loading="lazy" /></p><p>**Preis: **~70 $</p><p><strong>Vorteile:</strong>
– Zuverlaessig (ich sehe in Streams oefter, dass Kameras sich zufaellig abschalten – bei mir ist das nie passiert)
– Gute 1080p-Qualitaet und natuerliche Farben
– Sichtschutzblende (immerhin hat uns Edward Snowden <a href="https://www.imdb.com/title/tt4044364/">erzaehlt</a>, dass die NSA zuschaut)
– Autofokus und automatische Beleuchtungsanpassung, konfigurierbar ueber die Logitech-Software</p><p><strong>Nachteile:</strong>
– Eingebautes Mikrofon, das sich nicht physisch ausschalten laesst (NSA, du erinnerst dich?)</p><p><strong>Empfohlen?</strong> Ja!</p><h3 id="green-screen-elgato-collapsible-chroma-key-panel">Green Screen: <a href="https://www.elgato.com/en/green-screen">Elgato Collapsible Chroma Key Panel</a></h3><p><img src="/assets/images/blog/streaming-open-source-development-on-twitch-part-1/green-screen-elgato.webp" alt="" loading="lazy" /></p><p><strong>Preis:</strong> ~120 $</p><p><strong>Vorteile:</strong>
– Einfach und schnell aufzubauen und wieder wegzuraeumen
– Stabiler Staender
– Gute Hoehe</p><p><strong>Nachteile:</strong>
– Koennte noch etwas breiter sein (ich muss die Seiten meines Webcam-Bildes abschneiden)</p><p><strong>Empfohlen?</strong> Ja, wirklich praktisch, wenn du einen Green Screen brauchst.</p><p><strong>Alternativen:</strong>
– Richte dir einen huebschen Hintergrund fuer deinen Arbeitsplatz ein, wenn du den Platz dafuer hast</p><hr /><blockquote><p>Want to see your ad here? Contact me at <a href="mailto:ads@fline.dev">ads@fline.dev</a> to get in touch.</p></blockquote><hr /><h3 id="mikrofon-blue-yeti">Mikrofon: <a href="https://www.bluemic.com/en-us/products/yeti/">Blue Yeti</a></h3><p><img src="/assets/images/blog/streaming-open-source-development-on-twitch-part-1/mic-blue-yeti.webp" alt="" loading="lazy" /></p><p><strong>Preis:</strong> ~100 $</p><p><strong>Vorteile:</strong>
– Flexible Modi, die auch Interviews und mehr ermoeglichen
– Toller Klang direkt aus der Verpackung, ganz ohne Filter
– Logitech-Software liefert mehrere Filter-Presets mit</p><p><strong>Nachteile:</strong>
– Standard-Staender nicht gut genug gegen Vibrationen (Tastatur) geschuetzt
– Der Mute-Button erzeugt beim Klicken ein zu lautes Geraeusch, um ihn waehrend des Streams zu nutzen</p><p><strong>Empfohlen?</strong> Ja, dieses Mikrofon wird tatsaechlich von vielen fuer den Einstieg empfohlen.</p><p><strong>Alternativen:</strong>
– <a href="https://www.hyperxgaming.com/us/microphone/quadcast-gaming-microphone">HyperX Quadcast</a>, praktischer Tap-to-Mute-Sensor (~110 $)
– <a href="https://rode.com/en/microphones/usb/nt-usb">Rode NT-USB</a>, wird mit Popschutz geliefert (~170 $)
– Wenn du ein Headset mit gutem Mikrofon hast, nimm das (z. B. <a href="https://www.logitechg.com/en-us/products/gaming-audio/pro-x-gaming-headset-blue-voice-mic-tech.981-000817.html">Logitech Pro X</a>)</p><h3 id="licht-lavkow-10-inches-rgb-selfie-ring-light-us-alternative">Licht: <a href="https://www.amazon.de/gp/product/B088WTSFS9/?language=en_US&ref=fline.dev">LAVKOW 10 Inches RGB Selfie Ring Light</a> (<a href="https://www.amazon.com/UBeesize-Dimmable-Desktop-Streaming-Compatible/dp/B08HVH8FPF">US-Alternative</a>.)</h3><p><img src="/assets/images/blog/streaming-open-source-development-on-twitch-part-1/light-lavkow-10-inches.webp" alt="" loading="lazy" /></p><p><strong>Preis:</strong> ~30 $</p><p><strong>Vorteile:</strong>
– Guenstig
– Unterstuetzt verschiedene Farben</p><p><strong>Nachteile:</strong>
– Nicht besonders hell, aber ausreichend fuers Streamen, wenn man nah dran sitzt</p><p><strong>Empfohlen?</strong> Um Geld zu sparen: Ja, ein Selfie-Licht tut’s. Fuer Qualitaet: Nein.</p><p><strong>Alternativen:</strong>
– <a href="https://www.elgato.com/en/key-light">Elgato Key Light</a> (~200 $)</p><blockquote><p><strong>Schau dir auch</strong> Twitchs <strong><a href="https://www.twitch.tv/creatorcamp/en/setting-up-your-stream/hardware-recommendations/">Hardware-Empfehlungen</a></strong> an fuer weitere Alternativen.</p></blockquote><h2 id="weiteres-zubehoer-das-ich-nutze">Weiteres Zubehoer, das ich nutze</h2><p>Nicht direkt mit Streaming verbunden, aber falls du dich dafuer interessierst, was ich sonst noch auf meinem Schreibtisch stehen habe – hier eine kurze kommentierte Liste:</p><ul><li><p><a href="https://www.amazon.com/Apple-Wireless-Keyboard-Silver-MLA22LL/dp/B01NABDNPH/">Apple Wireless Magic Keyboard 2</a> (65 $):
Mittlerweile wuerde ich das <a href="https://www.logitech.com/en-us/products/keyboards/k860-split-ergonomic.920-009166.html">Logitech ERGO K860</a> bevorzugen.</p></li><li><p><a href="https://www.logitech.com/en-us/products/mice/mx-vertical-ergonomic-mouse.910-005447.html">Logitech MX Vertical</a> (80 $):
Ein Retter fuer mein Handgelenk – moechte nicht mehr zurueck.</p></li><li><p><a href="https://www.inateck.com/products/usb-c-hub-with-3-type-a-ports-1-pd-port-and-1-hdmi-port-hb2021">Inateck HB2021 5-in-1 Adapter</a> (35 $):
Nur ein Kabel anschliessen, wie eine Dockingstation.</p></li><li><p><a href="https://www.iboyata.com/laptop-stands/boyata-adjustable-laptop-riser-with-slide-proof-silicone-and-protective-hooks/">Boyata Laptop Stand</a> (30 $):
Schoene Verarbeitung, sehr stabil und rutschfest.</p></li><li><p><a href="https://www.lamicall.com/product/cell-phone-stand-for-desk-s1/">Lamicall Phone Stand S1</a> (10 $):
Gut genug, Oeffnung fuers Kabel. Erfuellt seinen Zweck.</p></li><li><p><a href="https://www.maxlvl.de/products/sidorenko-gaming-mauspads-im-schwarzen-design-vernahte-kanten?variant=31935806668886&ref=fline.dev">MAXLVL Gaming Mauspad XL</a> (15 $):
Die Groesse von 90 cm x 40 cm ist perfekt fuer mich.</p></li></ul><p>Das war’s zu meiner Motivation und meinem Hardware-Setup. Ich hoffe, das hilft zukuenftigen Streamern beim Einstieg. Der <a href="https://www.fline.dev/streaming-open-source-development-on-twitch-part-2/">naechste Teil</a> behandelt die Software-Seite, die meiner Meinung nach noch interessanter ist: Alle Tools, die ich nutze, und wie ich sie zusammen mit meiner Hardware eingerichtet habe. Folge mir, um nichts zu verpassen!</p><blockquote><p><strong>Dir hat dieser Artikel gefallen? Schau dir meine App <strong>RemafoX</strong> an!</strong>
Eine native Mac-App, die sich in Xcode integriert und dir hilft, <strong>deine</strong> App zu uebersetzen.
<a href="https://apps.apple.com/app/apple-store/id1605635026?pt=549314&ct=fline.dev&mt=8&ref=fline.dev"><strong>Jetzt herunterladen</strong></a>, um bei der Entwicklung Zeit zu sparen und Lokalisierung einfach zu machen.</p></blockquote>]]></content:encoded>
</item>
<item>
<title>SwiftPM + CoreData: SwiftUI Previews funktionieren nicht? Hier sind 5 Tipps zur Lösung</title>
<link>https://fline.dev/de/blog/swiftpm-coredata-failing-swiftui-previews-here-are-5-tips-to-fix/</link>
<guid isPermaLink="true">https://fline.dev/de/blog/swiftpm-coredata-failing-swiftui-previews-here-are-5-tips-to-fix/</guid>
<pubDate>Wed, 09 Mar 2022 00:00:00 +0000</pubDate>
<description><![CDATA[Xcode-Bugs beheben, die dazu führen, dass SwiftUI Previews in Apps fehlschlagen, die mit SwiftPM modularisiert sind und CoreData verwenden.]]></description>
<content:encoded><![CDATA[<p>Meine SwiftUI Previews funktionierten nicht richtig, seitdem ich das Projekt für den <a href="https://github.com/FlineDevPublic/OpenFocusTimer">Open Focus Timer</a> in Xcode mit Point-Frees <a href="https://www.pointfree.co/episodes/ep171-modularization-part-1">Modularisierungsansatz</a> eingerichtet hatte – mit aktivierter CoreData-Checkbox, um einen guten Ausgangspunkt für meine Modellschicht zu bekommen. Das war ziemlich nervig, denn schnellere Builds und damit zuverlässigere SwiftUI Previews waren einer der Hauptgründe, warum ich mich überhaupt dafür entschieden hatte, meine App in kleine Stücke zu modularisieren.</p><p>Also habe ich in <a href="https://youtu.be/OMhzx3zdrJw?t=6415&ref=fline.dev">einem meiner Streams</a> (das ist eine Open-Source-App, die ich komplett öffentlich entwickle und dabei <a href="https://www.twitch.tv/Jeehut">live auf Twitch streame</a>) beschlossen, dieses Problem ein für alle Mal zu lösen. Und bin gescheitert:</p><blockquote class="twitter-tweet"><p lang="en" dir="ltr">Just failed at fixing a <a href="https://twitter.com/hashtag/SwiftUI?src=hash&ref_src=twsrc%5Etfw&ref=fline.dev">#SwiftUI</a> preview error during my <a href="https://twitter.com/hashtag/livestream?src=hash&ref_src=twsrc%5Etfw&ref=fline.dev">#livestream</a>. Could fix one issue, but then got stuck at "MessageError: Connection interrupted". Any ideas? The project is open source:<a href="https://t.co/ppevrcRMtK?ref=fline.dev">https://t.co/ppevrcRMtK</a><br><br>I started getting this error here:<a href="https://t.co/AQz2l7vnTv?ref=fline.dev">https://t.co/AQz2l7vnTv</a> <a href="https://t.co/aTYQy5gzHi?ref=fline.dev">pic.twitter.com/aTYQy5gzHi</a></p>— Cihat Gündüz (@Jeehut) <a href="https://twitter.com/Jeehut/status/1499135821756129285?ref_src=twsrc%5Etfw&ref=fline.dev">March 2, 2022</a></blockquote>
<script async="" src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
<p>Dank der Hilfe der großartigen Swift-Community auf Twitter konnte ich die Ursache des Problems herausfinden: <strong>SwiftUI Previews bekommen Probleme, wenn CoreData-Modelle in ihnen referenziert werden.</strong></p><p>Aber während ich dachte, es sei nur ein Pfadproblem, das sich mit einem einfachen Workaround beheben ließe, war es doch nicht so einfach. Ja, es gibt ein Pfadproblem, aber beim Lösen der Previews bin ich auf mehrere Fehlerebenen gestoßen. Und ich habe dabei gelernt, wie man SwiftUI Previews debuggt. Lass mich meine Erkenntnisse teilen …</p><h3 id="1-explizite-dependencies-im-package-manifest">#1: Explizite Dependencies im Package-Manifest</h3><p>Das Wichtigste zuerst. Point-Frees Modularisierungsansatz zu verwenden bedeutet, dass du eine <code>Package.swift</code>-Datei manuell pflegen musst. Für jedes Modul fügst du einen <code>target</code>-, einen <code>testTarget</code>- und einen <code>library</code>-Eintrag hinzu und für jedes Target musst du die Dependencies angeben. Xcode hilft hier in keiner Weise, außer dass es die Änderungen erkennt, die du in dieser Datei vornimmst. Bei vielen Packages kann die Manifestdatei erheblich wachsen, und es gibt derzeit keine Hilfe, die mir bekannt wäre, um das einfacher zu machen. So sieht mein Manifest aktuell aus:</p><pre><code class="language-Swift">// swift-tools-version:5.5
import PackageDescription

let package = Package(
  name: &quot;OpenFocusTimer&quot;,
  defaultLocalization: &quot;en&quot;,
  platforms: [.macOS(.v12), .iOS(.v15)],
  products: [
    .library(name: &quot;AppEntryPoint&quot;, targets: [&quot;AppEntryPoint&quot;]),
    .library(name: &quot;Model&quot;, targets: [&quot;Model&quot;]),
    .library(name: &quot;TimerFeature&quot;, targets: [&quot;TimerFeature&quot;]),
    .library(name: &quot;ReflectionFeature&quot;, targets: [&quot;ReflectionFeature&quot;]),
    .library(name: &quot;Resources&quot;, targets: [&quot;Resources&quot;]),
  ],
  dependencies: [
    // Commonly used data structures for Swift
    .package(url: &quot;https://github.com/apple/swift-collections&quot;, from: &quot;1.0.2&quot;),

    // Handy Swift features that didn't make it into the Swift standard library.
    .package(url: &quot;https://github.com/Flinesoft/HandySwift&quot;, from: &quot;3.4.0&quot;),

    // Handy SwiftUI features that didn't make it into the SwiftUI (yet).
    .package(url: &quot;https://github.com/Flinesoft/HandySwiftUI&quot;, .branch(&quot;main&quot;)),

    // ⏰ A few schedulers that make working with Combine more testable and more versatile.
    .package(url: &quot;https://github.com/pointfreeco/combine-schedulers&quot;, from: &quot;0.5.3&quot;),

    // A library for building applications in a consistent and understandable way, with composition, testing, and ergonomics in mind.
    .package(url: &quot;https://github.com/pointfreeco/swift-composable-architecture&quot;, from: &quot;0.33.1&quot;),

    // Safely access Apple's SF Symbols using static typing Topics
    .package(url: &quot;https://github.com/SFSafeSymbols/SFSafeSymbols&quot;, from: &quot;2.1.3&quot;),
  ],
  targets: [
    .target(
      name: &quot;AppEntryPoint&quot;,
      dependencies: [
        .product(name: &quot;ComposableArchitecture&quot;, package: &quot;swift-composable-architecture&quot;),
        .product(name: &quot;HandySwift&quot;, package: &quot;HandySwift&quot;),
        .product(name: &quot;HandySwiftUI&quot;, package: &quot;HandySwiftUI&quot;),
        &quot;Model&quot;,
        &quot;ReflectionFeature&quot;,
        &quot;TimerFeature&quot;,
        &quot;Utility&quot;,
      ]
    ),
    .target(
      name: &quot;Model&quot;,
      dependencies: [
        .product(name: &quot;OrderedCollections&quot;, package: &quot;swift-collections&quot;),
        .product(name: &quot;HandySwift&quot;, package: &quot;HandySwift&quot;),
        .product(name: &quot;SFSafeSymbols&quot;, package: &quot;SFSafeSymbols&quot;),
      ],
      resources: [
        .process(&quot;Model.xcdatamodeld&quot;)
      ]
    ),
    .target(
      name: &quot;TimerFeature&quot;,
      dependencies: [
        .product(name: &quot;HandySwift&quot;, package: &quot;HandySwift&quot;),
        .product(name: &quot;ComposableArchitecture&quot;, package: &quot;swift-composable-architecture&quot;),
        &quot;Model&quot;,
        &quot;ReflectionFeature&quot;,
        &quot;Resources&quot;,
        .product(name: &quot;SFSafeSymbols&quot;, package: &quot;SFSafeSymbols&quot;),
        &quot;Utility&quot;,
      ]
    ),
    .target(
      name: &quot;ReflectionFeature&quot;,
      dependencies: [
        .product(name: &quot;ComposableArchitecture&quot;, package: &quot;swift-composable-architecture&quot;),
        .product(name: &quot;HandySwift&quot;, package: &quot;HandySwift&quot;),
        &quot;Model&quot;,
        &quot;Resources&quot;,
        &quot;Utility&quot;,
      ]
    ),
    .target(
      name: &quot;Resources&quot;,
      resources: [
        .process(&quot;Localizable&quot;)
      ]
    ),
    .target(
      name: &quot;Utility&quot;,
      dependencies: [
        .product(name: &quot;CombineSchedulers&quot;, package: &quot;combine-schedulers&quot;),
        &quot;Model&quot;,
      ]
    ),
    .testTarget(
      name: &quot;ModelTests&quot;,
      dependencies: [&quot;Model&quot;]
    ),
    .testTarget(
      name: &quot;TimerFeatureTests&quot;,
      dependencies: [
        .product(name: &quot;ComposableArchitecture&quot;, package: &quot;swift-composable-architecture&quot;),
        &quot;TimerFeature&quot;,
      ]
    ),
  ]
)</code></pre><p>Das Problem bei der manuellen Pflege dieser Datei ist nicht nur die manuelle Arbeit. Xcode verhält sich anscheinend inkonsistent bezüglich der Dependencies: Wenn du zum Beispiel einen normalen Build für den Simulator machst, scheint eine Dependency einer Dependency automatisch zu deinem Target gelinkt zu werden. Wenn also mein <code>TimerFeature</code> zum Beispiel <code>Utility</code> importiert, es aber nicht als Dependency unter dem <code>TimerFeature</code>-Target aufgelistet ist, kann Xcode trotzdem ohne Fehler kompilieren, wenn eine andere Dependency, z.B. <code>Model</code>, ebenfalls von <code>Utility</code> abhängt – so kann Xcode indirekt auf <code>Utility</code> innerhalb von <code>TimerFeature</code> zugreifen, weil <code>TimerFeature</code> <code>Model</code> als Dependency gelistet hat.</p><p>Obwohl das sehr nützlich klingt, kann es ziemlich frustrierend werden, weil SwiftUI Previews anders funktionieren. Für sie funktioniert diese transitive Art von impliziten Imports – soweit ich das beurteilen kann – nicht. Das Gleiche scheint auch für das Ausführen von Tests zu gelten (zumindest manchmal). Anders gesagt: Es ist wichtig, die <code>dependencies</code> für jedes Target immer doppelt zu prüfen und nicht zu vergessen, jeden <code>import</code>, den du in einem Target verwendest, auch zum entsprechenden Target in deiner <code>Package.swift</code>-Manifestdatei hinzuzufügen.</p><p>Vielleicht schreibt ja jemand in Zukunft ein Tool, das das einfacher macht. 🤞</p><h2 id="2-generierter-code-wird-von-xcode-nicht-zuverlässig-erkannt">#2: Generierter Code wird von Xcode nicht zuverlässig erkannt</h2><p>Ein weiteres Problem, auf das ich gestoßen bin: Selbst wenn meine Builds erfolgreich waren, zeigte Xcode (nachdem es mir den “Build succeeded”-Dialog gezeigt hatte) einen Fehler im Editor innerhalb des <code>PreviewProvider</code> an, der besagte, dass <code>FocusTimer</code> nicht gefunden werden könne:</p><p><img src="/assets/images/blog/swiftpm-coredata-failing-swiftui-previews-here-are-5-tips-to-fix/error-stating-focustimer.webp" alt="Fehler, dass <code>FocusTimer</code> im Scope nicht gefunden werden kann, trotz <code>import Model</code> und erfolgreichem Build." loading="lazy" /></p><p>Beachte, dass du diese generierten Dateien jedes Mal löschen und neu erstellen musst, wenn du eine Änderung am Modell vornimmst (was du ohnehin selten machen solltest, um Datenbank-Migrationsprobleme zu vermeiden). Wähle außerdem das Modell in Xcode aus und setze <code>Codegen</code> auf <code>Manual/None</code>.</p><p><img src="/assets/images/blog/swiftpm-coredata-failing-swiftui-previews-here-are-5-tips-to-fix/clicking-on-diagnostics-2.webp" alt="Klick auf “Diagnostics” zeigt nur “MessageError: Connection interrupted”." loading="lazy" /></p><p>Damit zeigt der Editor keinen Fehler mehr an.</p><h3 id="3-swiftui-diagnostics-swiftui-crash-reports">#3: SwiftUI Diagnostics != SwiftUI Crash Reports</h3><p>Hier ein Tipp für alle (wie mich), die sich fragen, wie man mit Fehlern wie diesem umgeht, wenn man nach dem Fehlschlagen der SwiftUI Previews auf den <code>Diagnostics</code>-Button drückt:</p><p><img src="/assets/images/blog/swiftpm-coredata-failing-swiftui-previews-here-are-5-tips-to-fix/clicking-on-diagnostics-3.webp" alt="Klick auf “Diagnostics” zeigt nur “MessageError: Connection interrupted”." loading="lazy" /></p><p>Wenn du den Inhalt des hervorgehobenen Ordners anschaust, findest du viele Dateien mit verschiedenen Details über den SwiftUI-Preview-Build. Die nützlichste Datei zum Debuggen liegt im Ordner <code>CrashLogs</code>, wo du eine oder mehrere <code>.ips</code>-Dateien findest, die wir einfach per Doppelklick in Xcode öffnen können:</p><p><img src="/assets/images/blog/swiftpm-coredata-failing-swiftui-previews-here-are-5-tips-to-fix/contents-of-xcode.webp" alt="Inhalt der Xcode-Previews-.ips-Datei mit einer aussagekräftigen Fehlerkonsolenausgabe." loading="lazy" /></p><p>Zum Glück half hier der bereits erwähnte <a href="https://twitter.com/NiklasBuelow/status/1499160862220857349?s=20&ref=fline.dev">Hinweis eines hilfsbereiten Entwicklers</a> aus der Swift-Community auf Twitter, der mich auf einen Thread mit <a href="https://stackoverflow.com/a/65789298/3451975">dieser Antwort</a> auf StackOverflow verwies.</p><p>Es besagt im Wesentlichen, dass es derzeit einen Bug in Xcode (oder SwiftPM?) gibt, der dazu führt, dass <code>Bundle.module</code> in SwiftUI Previews auf den falschen Pfad zeigt. Zur Lösung schlagen sie vor, eine <code>Bundle</code>-Extension mit einer benutzerdefinierten Suche hinzuzufügen. Hier der vollständige Code, leicht angepasst an meinen Coding- und Kommentarstil:</p><pre><code class="language-Swift">import Foundation

extension Foundation.Bundle {
  /// Workaround for making `Bundle.module` work in SwiftUI previews. See: https://stackoverflow.com/a/65789298
  ///
  /// - Returns: The bundle of the target with a path that works in SwiftUI previews, too.
  static var swiftUIPreviewsCompatibleModule: Bundle {
    #if DEBUG
      // adjust these for each module
      let packageName = &quot;OpenFocusTimer&quot;
      let targetName = &quot;Model&quot;

      final class ModuleToken {}

      let candidateUrls: [URL?] = [
        // Bundle should be present here when the package is linked into an App.
        Bundle.main.resourceURL,

        // Bundle should be present here when the package is linked into a framework.
        Bundle(for: ModuleToken.self).resourceURL,

        // For command-line tools.
        Bundle.main.bundleURL,

        // Bundle should be present here when running previews from a different package (this is the path to &quot;…/Debug-iphonesimulator/&quot;).
        Bundle(for: ModuleToken.self).resourceURL?.deletingLastPathComponent().deletingLastPathComponent()
          .deletingLastPathComponent(),
        Bundle(for: ModuleToken.self).resourceURL?.deletingLastPathComponent().deletingLastPathComponent(),
      ]

      // The name of your local package, prepended by &quot;LocalPackages_&quot; for iOS and &quot;PackageName_&quot; for macOS.
      let bundleNameCandidates = [&quot;\(packageName)_\(targetName)&quot;, &quot;LocalPackages_\(targetName)&quot;]

      for bundleNameCandidate in bundleNameCandidates {
        for candidateUrl in candidateUrls where candidateUrl != nil {
          let bundlePath: URL = candidateUrl!.appendingPathComponent(bundleNameCandidate)
            .appendingPathExtension(&quot;bundle&quot;)
          if let bundle = Bundle(url: bundlePath) { return bundle }
        }
      }

      return Bundle.module
    #else
      return Bundle.module
    #endif
  }
}</code></pre><blockquote><p><em>Wenn du diesen Code kopierst und einfügst, stelle sicher, dass du die Variablen <code>packageName</code> und <code>targetName</code> an dein Package und deine Target-Namen anpasst.</em></p></blockquote><p>Beachte, dass ich den Workaround in ein <code>#if DEBUG</code> eingewickelt habe, um sicherzustellen, dass mein Produktionscode nicht versehentlich diese Pfadsuche verwendet, sondern sich auf das offizielle <code>Bundle.module</code> verlässt. Außerdem habe ich den <code>fatalError</code> aus dem StackOverflow-Workaround-Code entfernt, sodass er, falls er kein Bundle in den benutzerdefinierten Suchpfaden findet, nicht abstürzt, sondern stattdessen <code>Bundle.module</code> als Fallback zurückgibt. Das soll den Code robuster machen und auch dann weiterhin funktionieren, wenn dieser Bug in einem zukünftigen Xcode-Release behoben wird, die benutzerdefinierten Suchpfade aber möglicherweise nicht mehr funktionieren.</p><p>Nun war die letzte Änderung, die ich im <code>PersistenceController</code> vornehmen musste, den Aufruf von <code>Bundle.module</code> durch einen Aufruf des neuen <code>Bundle.swiftUIPreviewsCompatibleModule</code> zu ersetzen:</p><pre><code class="language-Swift">let modelUrl = Bundle.swiftUIPreviewsCompatibleModule.url(forResource: &quot;Model&quot;, withExtension: &quot;momd&quot;)!
let managedObjectModel = NSManagedObjectModel(contentsOf: modelUrl)!
container = NSPersistentContainer(name: &quot;Model&quot;, managedObjectModel: managedObjectModel)</code></pre><p>Und endlich funktionierten meine SwiftUI Previews wieder!</p>]]></content:encoded>
</item>
<item>
<title>Multi Selector in SwiftUI</title>
<link>https://fline.dev/de/blog/multi-selector-in-swiftui/</link>
<guid isPermaLink="true">https://fline.dev/de/blog/multi-selector-in-swiftui/</guid>
<pubDate>Thu, 03 Mar 2022 00:00:00 +0000</pubDate>
<description><![CDATA[Eine fehlende SwiftUI-Komponente fuer Prototyping-Zwecke ergaenzen.]]></description>
<content:encoded><![CDATA[<p>Waehrend ich meine erste ernsthafte App mit SwiftUI entwickelte, war ich immer wieder beeindruckt, wie schnell die UI-Entwicklung mit SwiftUI geworden war – vor allem, wenn die mitgelieferten Views den eigenen Anwendungsfall bereits abdecken. Und obwohl wir fuer jede Art von individuellem UI natuerlich weiterhin eigene Views schreiben muessen, indem wir bestehende kombinieren und mit Modifiers anpassen, wuerde ich erwarten, dass SwiftUI zumindest die gaengigsten Views unterstuetzt, die Entwickler brauchen koennten, um Daten darzustellen und Eingaben von Nutzern entgegenzunehmen.</p><p>Waere das der Fall, koennte SwiftUI sogar fuer Prototyping genutzt werden, bei dem eine “funktionierende, aber nicht huebsche” Version einer App-Idee schnell gebaut und Nutzern gezeigt werden koennte, um zu pruefen, ob die App-Idee Erfolgschancen hat. Ausserdem koennte man so auch schnell Feedback sammeln, welche Teile ein deutlich verstaendlicheres UI brauchen (die noch nicht gut verstandenen Teile) und welche groesstenteils bei den Standard-Komponenten mit einigen visuellen Anpassungen bleiben koennten.</p><p>Mit anderen Worten: SwiftUI hat meiner Meinung nach das Potenzial, <a href="https://en.wikipedia.org/wiki/Minimum_viable_product">MVP</a>-getriebene Produktentwicklung fuer deutlich mehr Entwickler interessant zu machen – was definitiv eine gute Sache ist, da es viel Zeit spart, die sonst in Dinge investiert wuerde, die sich letztlich als Fehlschlag herausstellen. Das passt zur <a href="https://en.wikipedia.org/wiki/Lean_startup">Lean-Startup-Methodik</a>, die ich fuer einen grossartigen Ansatz halte, um jede Art von neuem Produkt anzugehen.</p><h3 id="der-aktuelle-stand-von-swiftui">Der aktuelle Stand von SwiftUI</h3><p>Damit das moeglich waere, wuerde ich erwarten, dass SwiftUI bereits alle gaengigen Eingabetypen abdeckt, die in Formularen benoetigt werden koennten – etwa fuer die Nutzerregistrierung oder andere Arten von Daten – denn viele Arten von Apps sind letztlich nichts anderes als ein Formular, das Eingabedaten entgegennimmt, sie auf irgendeine Weise transformiert und Daten auf eine besondere Art oder zu einem bestimmten Zeitpunkt zurueckgibt. Leider ist SwiftUI da noch nicht ganz angekommen.</p><p>Apples Ansatz bei SwiftUI scheint zu sein, jedes Jahr zu ueberlegen, welche Komponenten am meisten fehlen, und einige davon hinzuzufuegen. Zum Beispiel wurden auf der WWDC 2020 <a href="https://developer.apple.com/documentation/swiftui/progressview">ProgressView</a>, <a href="https://developer.apple.com/documentation/swiftui/gauge">Gauge</a>, <code>Image</code>-Unterstuetzung innerhalb von <code>Text</code> und viele <a href="https://developer.apple.com/videos/play/wwdc2020/10041/">andere Details bestehender Views</a> verbessert, sowohl in Bezug auf Performance als auch Flexibilitaet. Auf der WWDC 2021 wurden mehrere <code>async/await</code>-bezogene APIs hinzugefuegt, wie <a href="https://developer.apple.com/documentation/swiftui/asyncimage">AsyncImage</a> oder die View-Modifier <a href="https://developer.apple.com/documentation/swiftui/label/refreshable%28action:%29">.refreshable</a> und <a href="https://developer.apple.com/documentation/swiftui/circle/task%28priority:_:%29">.task</a>, neben <a href="https://medium.com/@anithawritings/whats-new-in-swiftui-wwdc2021-aadbdd8d34de">anderen Verbesserungen und Ergaenzungen</a>.</p><p>Der Vorteil dieses Ansatzes ist: Sobald etwas zum Framework hinzugefuegt wird, kann man erwarten, dass es lange Zeit existiert und auf die gleiche Weise funktioniert – grosse Codeaenderungen sind also nicht bei jedem Release noetig (wie es bei Swift als Sprache vor Swift 4 der Fall war). Der Nachteil ist, dass noch viele Komponenten fehlen. Und genau da kann die Community einspringen, um temporaere Loesungen bereitzustellen, die spaeter leicht durch offizielle Komponenten von Apple ersetzt werden koennen.</p><h3 id="eine-multi-selection-view-komponente-implementieren">Eine Multi-Selection-View-Komponente implementieren</h3><p>In diesem Beitrag moechte ich mich auf eine solche Komponente konzentrieren und meine erste Loesung dafuer vorstellen: Einen Multi-Selector, um mehrere Optionen aus einer gegebenen Menge auszuwaehlen. Derzeit bietet Apple zwar einen <a href="https://developer.apple.com/documentation/swiftui/picker">Picker</a>, aber der unterstuetzt keine Mehrfachauswahl und verlaesst sogar automatisch den Listen-Screen, sobald eine einzelne Auswahl getroffen wurde. Also packen wir es direkt an!</p><p>Welche Art von Datenstruktur koennte einen Multi-Selector erfordern? Schauen wir uns dieses Beispiel an:</p><pre><code class="language-Swift">struct Goal: Hashable, Identifiable {
    var name: String
    var id: String { name }
}

struct Task {
    var name: String
    var servingGoals: Set&lt;Goal&gt;
}</code></pre><p>Also haben wir in unserer App im Grunde eine Sammlung von Zielen und eine Sammlung von Aufgaben. Und wir moechten die Beziehung modellieren, welche Ziele jede Aufgabe bedient. Beim Erstellen oder Bearbeiten einer <code>Task</code> wollen wir auswaehlen, welche Ziele die Aufgabe bedient. Hier ist der SwiftUI-Code fuer eine <code>TaskEditView</code>:</p><pre><code class="language-Swift">import SwiftUI

struct TaskEditView: View {
    @State
    var task = Task(name: &quot;&quot;, servingGoals: [])

    var body: some View {
        Form {
            Section(header: Text(&quot;Name&quot;)) {
                TextField(&quot;e.g. Find a good Japanese textbook&quot;, text: $task.name)
            }

            Section(header: Text(&quot;Relationships&quot;)) {
                Text(&quot;TODO: add multi selector here&quot;)
            }
        }.navigationTitle(&quot;Edit Task&quot;)
    }
}

struct TaskEditView_Previews: PreviewProvider {
    static var previews: some View {
        NavigationView {
            TaskEditView()
        }
    }
}</code></pre><p>Der obige Code rendert zu dieser Vorschau:</p><p><img src="/assets/images/blog/multi-selector-in-swiftui/implementing-a-multi-3.webp" alt="Implementing a multi 3" loading="lazy" /></p><p>Um zu zeigen, wie es funktionieren wuerde, wenn wir nur ein Ziel haetten, koennten wir unseren TODO-<code>Text</code>-Eintrag einfach durch einen <code>Picker</code> ersetzen:</p><pre><code class="language-Swift">// mock data:
let allGoals: [Goal] = [
    Goal(name: &quot;Learn Japanese&quot;),
    Goal(name: &quot;Learn SwiftUI&quot;),
    Goal(name: &quot;Learn Serverless with Swift&quot;)
]

Picker(&quot;Serving Goal&quot;, selection: $task.servingGoal) {
    ForEach(allGoals) {
        Text($0.name).tag($0 as Goal)
    }
}</code></pre><p>So sieht die <code>TaskEditView</code> jetzt aus:</p><p><img src="/assets/images/blog/multi-selector-in-swiftui/implementing-a-multi-6.webp" alt="Implementing a multi 6" loading="lazy" /></p><p>Und beim Klicken auf den Picker erscheint diese Detailansicht:</p><p><img src="/assets/images/blog/multi-selector-in-swiftui/implementing-a-multi-5.webp" alt="Implementing a multi 5" loading="lazy" /></p><p>Ziemlich unkompliziert. Beachte, dass <code>Goal</code> <code>Identifiable</code> sein muss, damit das funktioniert – deshalb habe ich <code>var id: String { name }</code> von Anfang an hinzugefuegt. Fuer unseren Multi-Selector soll das UI eigentlich ziemlich gleich aussehen, aber statt einer moechten wir mehrere Eintraege auswaehlen koennen.</p><p>Zuerst muessen wir den Eintrag in der <code>TaskEditView</code> neu erstellen. Ich habe <code>MultiSelector</code> als Ersatz-Typnamen fuer <code>Picker</code> gewaehlt. Hier ist die Implementierung:</p><pre><code class="language-Swift">import SwiftUI

struct MultiSelector&lt;LabelView: View, Selectable: Identifiable &amp; Hashable&gt;: View {
    let label: LabelView
    let options: [Selectable]
    let optionToString: (Selectable) -&gt; String
    var selected: Binding&lt;Set&lt;Selectable&gt;&gt;

    private var formattedSelectedListString: String {
        ListFormatter.localizedString(byJoining: selected.wrappedValue.map { optionToString($0) })
    }

    var body: some View {
        NavigationLink(destination: multiSelectionView()) {
            HStack {
                label
                Spacer()
                Text(formattedSelectedListString)
                    .foregroundColor(.gray)
                    .multilineTextAlignment(.trailing)
            }
        }
    }

    private func multiSelectionView() -&gt; some View {
        Text(&quot;TODO: add multi selection detail view here&quot;)
    }
}</code></pre><p>Beachte, dass ich mich entschieden habe, jeden Eintrag mit einem <code>String</code> darzustellen – daher wird die <code>optionToString</code>-Closure benoetigt, die die <code>String</code>-Darstellung des Options-Typs liefert.</p><p>Der Aufruf von <code>ListFormatter.localizedString</code> stellt sicher, dass wir eine Liste ausgewaehlter Optionen im korrekten Lokalisierungsformat zusammenfuegen (z. B. wird <code>[&quot;A&quot;, &quot;B&quot;, &quot;C&quot;]</code> im Englischen zu “A, B and C”).</p><p>Das ist der Preview-Code, den ich fuer die View verwendet habe:</p><pre><code class="language-Swift">struct MultiSelector_Previews: PreviewProvider {
    struct IdentifiableString: Identifiable, Hashable {
        let string: String
        var id: String { string }
    }

    @State
    static var selected: Set&lt;IdentifiableString&gt; = Set([&quot;A&quot;, &quot;C&quot;].map { IdentifiableString(string: $0) })

    static var previews: some View {
        NavigationView {
            Form {
                MultiSelector&lt;Text, IdentifiableString&gt;(
                    label: Text(&quot;Multiselect&quot;),
                    options: [&quot;A&quot;, &quot;B&quot;, &quot;C&quot;, &quot;D&quot;].map { IdentifiableString(string: $0) },
                    optionToString: { $0.string },
                    selected: $selected
                )
            }.navigationTitle(&quot;Title&quot;)
        }
    }
}</code></pre><p>Beachte, dass ich statt <code>Goal</code> einen internen Typ verwendet habe, um die Vorschau unabhaengig von meinem konkreten Projekt zu machen. So sieht die Vorschau aus:</p><p><img src="/assets/images/blog/multi-selector-in-swiftui/implementing-a-multi.webp" alt="Implementing a multi" loading="lazy" /></p><p>Setzen wir das in unsere <code>TaskEditView</code> ein und schauen, wie es in diesem Kontext aussieht, indem wir den TODO-<code>Text</code>-Aufruf ersetzen durch:</p><pre><code class="language-Swift">MultiSelector(
    label: Text(&quot;Serving Goals&quot;),
    options: allGoals,
    optionToString: { $0.name },
    selected: $task.servingGoals
)</code></pre><p>Die Vorschau aendert sich jetzt zu diesem Ergebnis, das genau so aussieht wie erwartet:</p><p><img src="/assets/images/blog/multi-selector-in-swiftui/implementing-a-multi-4.webp" alt="Implementing a multi 4" loading="lazy" /></p><p>Aber wenn man darauf klickt, sieht man das hier – das ist noch nicht richtig:</p><p><img src="/assets/images/blog/multi-selector-in-swiftui/implementing-a-multi-2.webp" alt="Implementing a multi 2" loading="lazy" /></p><p>Implementieren wir also die Detailansicht. Ich habe den Typnamen <code>MultiSelectionView</code> fuer die Detailansicht gewaehlt, und hier ist der Code:</p><pre><code class="language-Swift">import SwiftUI

struct MultiSelectionView&lt;Selectable: Identifiable &amp; Hashable&gt;: View {
    let options: [Selectable]
    let optionToString: (Selectable) -&gt; String

    @Binding
    var selected: Set&lt;Selectable&gt;

    var body: some View {
        List {
            ForEach(options) { selectable in
                Button(action: { toggleSelection(selectable: selectable) }) {
                    HStack {
                        Text(optionToString(selectable)).foregroundColor(.black)

                        Spacer()

                        if selected.contains { $0.id == selectable.id } {
                            Image(systemName: &quot;checkmark&quot;).foregroundColor(.accentColor)
                        }
                    }
                }.tag(selectable.id)
            }
        }.listStyle(GroupedListStyle())
    }

    private func toggleSelection(selectable: Selectable) {
        if let existingIndex = selected.firstIndex(where: { $0.id == selectable.id }) {
            selected.remove(at: existingIndex)
        } else {
            selected.insert(selectable)
        }
    }
}</code></pre><p>Abgesehen von <code>label</code> hat diese View im Grunde die gleichen Properties. Aber diesmal werden sie tatsaechlich genutzt – zum Beispiel, wenn geprueft wird, ob das Haekchen angezeigt werden soll, indem <code>contains</code> auf der <code>selected</code>-Sammlung aufgerufen wird.</p><p>Wenn einer der Eintraege angeklickt wird, wird <code>toggleSelection</code> auf den Eintrag angewendet, um ihn aus der <code>selected</code>-Property zu entfernen oder einzufuegen. Fuer das Haekchen verwende ich das SF Symbol “checkmark”, das genauso aussieht wie das Haekchen-Icon des <code>Picker</code>.</p><p>Das ist der Preview-Code, den ich fuer die Detailansicht eingerichtet habe – beachte, dass er im Wesentlichen eine Kopie der <code>MultiSelector</code>-Vorschau ist:</p><pre><code class="language-Swift">struct MultiSelectionView_Previews: PreviewProvider {
    struct IdentifiableString: Identifiable, Hashable {
        let string: String
        var id: String { string }
    }

    @State
    static var selected: Set&lt;IdentifiableString&gt; = Set([&quot;A&quot;, &quot;C&quot;].map { IdentifiableString(string: $0) })

    static var previews: some View {
        NavigationView {
            MultiSelectionView(
                options: [&quot;A&quot;, &quot;B&quot;, &quot;C&quot;, &quot;D&quot;].map { IdentifiableString(string: $0) },
                optionToString: { $0.string },
                selected: $selected
            )
        }
    }
}</code></pre><p>So sieht es in der Xcode-Vorschau aus:</p><p><img src="/assets/images/blog/multi-selector-in-swiftui/implementing-a-multi-7.webp" alt="Implementing a multi 7" loading="lazy" /></p><p>Jetzt integrieren wir schliesslich unsere <code>MultiSelectionView</code> in unseren <code>MultiSelector</code>, indem wir den TODO-<code>Text</code>-Eintrag ersetzen durch:</p><pre><code class="language-Swift">MultiSelectionView(
    options: options,
    optionToString: optionToString,
    selected: selected
)</code></pre><p>Im Grunde geben wir einfach die Daten an die Detailansicht weiter. Aber schauen wir uns an, wie unsere App jetzt in diesem animierten GIF aussieht, das ich aus dem Simulator aufgenommen habe:</p><p><img src="/assets/images/blog/multi-selector-in-swiftui/implementing-a-multi.gif" alt="Implementing a multi" loading="lazy" /></p><p>Super, es funktioniert!</p><p>Ich habe das Demo-Projekt <a href="https://github.com/Jeehut/MultiSelectorDemo">auf GitHub hochgeladen</a>. Wenn du einfach den Inhalt von <code>MultiSelector</code> und <code>MultiSelectionView</code> kopieren moechtest, findest du sie in <a href="https://github.com/Jeehut/MultiSelectorDemo/tree/main/Shared/MultiSelector">diesem Ordner</a>.</p><blockquote><p><strong>Du fandest diesen Artikel hilfreich? Hol dir meinen Expertenrat!</strong></p></blockquote>]]></content:encoded>
</item>
<item>
<title>Secrets vor Git verbergen in SwiftPM</title>
<link>https://fline.dev/de/blog/hiding-secrets-from-git-in-swiftpm/</link>
<guid isPermaLink="true">https://fline.dev/de/blog/hiding-secrets-from-git-in-swiftpm/</guid>
<pubDate>Sun, 20 Feb 2022 00:00:00 +0000</pubDate>
<description><![CDATA[Eine Schritt-für-Schritt-Anleitung, wie du verhinderst, dass deine Secrets für Drittanbieter-Dienste in Git landen, wenn du Apps verwendest, die mit SwiftPM modularisiert sind.]]></description>
<content:encoded><![CDATA[<p>Du kennst vielleicht einige <a href="https://nshipster.com/secrets/">traditionelle Methoden</a>, um Secrets wie einen API-Key oder ein Token für Drittanbieter-Dienste zu verstecken, das du für deine App brauchst. Aber heutzutage werden Ansätze zur <strong>Modularisierung deiner App</strong> mit SwiftPM immer beliebter.</p><p>Point-Free hat zum Beispiel eine großartige <a href="https://www.pointfree.co/episodes/ep171-modularization-part-1">kostenlose Episode</a> zu diesem Thema, und Majid Jabrayilov hat kürzlich eine 4-teilige Serie über “Microapps Architecture” geschrieben (Teile <a href="https://swiftwithmajid.com/2022/01/12/microapps-architecture-in-swift-spm-basics/">1</a>, <a href="https://swiftwithmajid.com/2022/01/19/microapps-architecture-in-swift-feature-modules/">2</a>, <a href="https://swiftwithmajid.com/2022/01/26/microapps-architecture-in-swift-resources-and-localization/">3</a>, <a href="https://swiftwithmajid.com/2022/02/02/microapps-architecture-in-swift-dependency-injection/">4</a>), die ich beide als Einstieg empfehlen kann.</p><p>Außerdem möchtest du vielleicht Secrets auch in öffentlichen Open-Source-Bibliotheken verstecken, z.B. in den Unit-Tests einer Drittanbieter-Service-Integration, wo Nutzer der Bibliothek ihr eigenes Token bereitstellen, du aber möchtest, dass <a href="https://github.com/Flinesoft/BartyCrouch/blob/baece7f4786bc805358f35ba5fd60d6259d5c8b9/Tests/BartyCrouchTranslatorTests/MicrosoftTranslatorApiTests.swift#L8">deine Tests mit deinem eigenen laufen</a>.</p><p>Was all diese Situationen gemeinsam haben: Sie basieren auf einer manuell gepflegten <code>Package.swift</code>-Datei – nicht der, die Xcode für dich verwaltet, wenn du einfach eine Abhängigkeit zu einem App-Projekt hinzufügst. Die App oder das Projekt ist in viele kleine Module aufgeteilt, ohne eine zugehörige <code>.xcodeproj</code>-Datei – Xcode öffnet einfach direkt die <code>Package.swift</code>-Datei, ganz ohne ein Projekt.</p><p>Das bedeutet auch, dass es für die einzelnen Module keine Möglichkeit gibt, Build-Einstellungen oder Build-Skripte innerhalb von Xcode festzulegen – alles muss direkt in der <code>Package.swift</code>-Manifestdatei gemacht werden.</p><p>Obwohl immer mehr solcher Features zu SwiftPM hinzugefügt werden (wie <a href="https://github.com/apple/swift-evolution/blob/main/proposals/0303-swiftpm-extensible-build-tools.md">SE-303</a>, <a href="https://github.com/apple/swift-evolution/blob/main/proposals/0325-swiftpm-additional-plugin-apis.md">SE-325</a>, <a href="https://github.com/apple/swift-evolution/blob/main/proposals/0332-swiftpm-command-plugins.md">SE-332</a>) in zukünftigen Releases, gibt es keine Anzeichen dafür, dass sie Xcode-spezifische Features wie <code>.xcconfig</code>-Dateien unterstützen werden.</p><p>Wie können wir also Secrets <em>heute</em> davon abhalten, in Git zu landen, damit sie nicht an unseren Git-Provider oder jemanden mit Zugriff auf unser Repository gelangen?</p><h1 id="swiftpm-ressourcen-jsondecoder">SwiftPM-Ressourcen &amp; JSONDecoder</h1><p>Ich bin sicher, es gibt nicht die eine “beste” Antwort darauf und andere haben vielleicht cleverere Ideen als ich. Aber ich halte die Dinge gerne <a href="https://en.wikipedia.org/wiki/KISS_principle">einfach</a> und verwende gerne grundlegende Features, weil ich sie gut kenne und erwarte, dass andere Entwickler sie bei Bedarf schnell verstehen. Außerdem kann ich mir sicher sein, dass sie zukunftssicher sind.</p><p>Der Ansatz, den ich verwenden möchte, ist der <a href="https://github.com/bkeepers/dotenv">klassische </a><a href="https://github.com/bkeepers/dotenv"><code>.env</code></a><a href="https://github.com/bkeepers/dotenv">-Datei-Ansatz</a>, der in der Webentwicklung verbreitet ist. Aber statt einer speziell formatierten <code>.env</code>-Datei möchte ich einfach eine <code>.json</code>-Datei mit meinen Secrets verwenden, weil JSON-Dateien vielen iOS-Entwicklern vertraut sind und wir dank <a href="https://developer.apple.com/documentation/foundation/jsondecoder"><code>JSONDecoder</code></a> in Swift bereits eingebaute Unterstützung fürs Parsen haben. Das Laden von Dateien oder allgemeiner “Ressourcen” wird ebenfalls seit Swift 5.3 von SwiftPM unterstützt (<a href="https://github.com/apple/swift-evolution/blob/main/proposals/0271-package-manager-resources.md">SE-271</a>).</p><p>Hier ist die Grundidee, wie ich Secrets vor Git verbergen möchte:</p><ol><li><p>Eine <code>secrets.json.sample</code>-Datei mit den Keys, aber ohne Werte, in Git einchecken</p></li><li><p>Entwickler duplizieren sie, entfernen die <code>.sample</code>-Endung und tragen die Werte ein</p></li><li><p>Die <code>secrets.json</code>-Datei über <code>.gitignore</code> ignorieren, damit sie nie eingecheckt wird</p></li><li><p>Ein einfaches <code>struct</code> bereitstellen, das <code>Decodable</code> implementiert, um die Secrets auszulesen</p></li></ol><p>Der Rest dieses Artikels ist eine Schritt-für-Schritt-Anleitung, wie du diesen Ansatz anwendest. Ich werde als Beispiel die Unit-Tests meines Open-Source-Übersetzungstools <a href="https://github.com/Flinesoft/BartyCrouch">BartyCrouch</a> verwenden, das zwei Drittanbieter-Übersetzungsdienste integriert.</p><blockquote><p><em>⚠️ Beachte bitte, dass du bei der Anwendung dieses Ansatzes auf ein <strong>App-Target</strong>, das du an Nutzer ausliefern willst, wahrscheinlich auf dasselbe Problem stößt, das beim <code>.xcconfig</code>-Ansatz in <em><a href="https://nshipster.com/secrets/#big-brain-store-secrets-in-xcode-configuration-and-infoplist"><em>diesem NSHipster-Artikel</em></a></em> beschrieben wird. Meine Methode hilft nur beim Verbergen der Secrets vor Git – du brauchst zusätzliche Verschleierung, wenn du sie an Nutzer ausliefern willst.</em></p></blockquote><h1 id="die-secretsjson-ressourcendatei-hinzufügen">Die <code>secrets.json</code>-Ressourcendatei hinzufügen</h1><p>Zuerst fügen wir die <code>secrets.json</code>-Datei zu unserem Projekt hinzu. Da es eine zugehörige <code>secrets.json.sample</code> und eine <code>Secrets.swift</code>-Datei geben wird, erstelle ich zunächst einen Ordner <code>Secrets</code>, dann eine leere Datei namens <code>secrets.json</code>, und füge eine einfache JSON-Dictionary-Struktur mit zwei Keys hinzu:</p><p><img src="/assets/images/blog/hiding-secrets-from-git-in-swiftpm/the-secrets-json-file-2.webp" alt="Die <code>secrets.json</code>-Datei mit zwei echten Secrets, dem Projekt hinzugefügt." loading="lazy" /></p><p>Und so ist auch meine CI eingerichtet, um sicher auf meine Secrets zuzugreifen, ohne sie preiszugeben.</p>]]></content:encoded>
</item>
<item>
<title>Laser-Focus-Priorisierungsstrategie</title>
<link>https://fline.dev/de/blog/laser-focus-priority-strategy/</link>
<guid isPermaLink="true">https://fline.dev/de/blog/laser-focus-priority-strategy/</guid>
<pubDate>Mon, 27 Sep 2021 00:00:00 +0000</pubDate>
<description><![CDATA[Eine einfache, aber effektive Priorisierungstechnik, die dir helfen kann, den Umfang deiner App zu verschlanken und dir mehr Sicherheit zu geben – mit verschiedenen Stufen, die sich auf Alpha, Beta & Release abbilden lassen.]]></description>
<content:encoded><![CDATA[<p>Es gibt zahlreiche Priorisierungstechniken, die unterschiedliche Probleme lösen sollen. Wahrscheinlich hast du schon irgendeine Form der Wert-vs.-Aufwand-Priorisierung verwendet, zum Beispiel <a href="https://www.productplan.com/glossary/rice-scoring-model/">RICE</a>. Vielleicht hast du sogar mal deine Zielgruppe mit einer gezielt konzipierten Umfrage befragt, etwa mit dem <a href="https://en.wikipedia.org/wiki/Kano_model">KANO-Modell</a>. Jede Priorisierungstechnik hat ihre Anwendungsfälle und möglicherweise haben sie dir schon bei vielen nützlichen Entscheidungen geholfen.</p><p>Aber diese Strategien sind für eine <strong>übergeordnete</strong> Art der <strong>Priorisierung</strong> gedacht – also um zu entscheiden, ob du zuerst Feature A oder Feature B umsetzen solltest oder ob Feature C überhaupt in die nächste Version gehört. Sie <strong>skalieren aber nicht</strong> auf Aufgaben oder gar Unteraufgaben deiner Features herunter, sodass man bei einem bestimmten Feature durchaus zu viel tun kann. Außerdem helfen sie nicht bei der Frage, wann du das Feature in die Hände der Nutzer geben kannst, um frühes Feedback zu sammeln und nutzerzentrierte Ansätze wie die <a href="https://en.wikipedia.org/wiki/Lean_startup">Lean-Startup</a>-Methodik anzuwenden. Man könnte natürlich eine skalenunabhängige Methode wählen, wie die <a href="https://en.wikipedia.org/wiki/MoSCoW_method">MoSCoW-Methode</a>, aber deren Kategorien wären nicht einfach einzuordnen, weil sie so abstrakt sind, dass verschiedene Personen unterschiedliche Erwartungen an jede Kategorie hätten.</p><p>Das Ziel der Laser-Focus-Priorisierungsstrategie ist es, klare Bewertungskategorien bereitzustellen, beim Aufgabenumfang zu helfen und eine einfach anwendbare Interpretationsmethode zu bieten. Alle drei Aspekte zusammen helfen dir, den Fokus zu behalten.</p><h3 id="laser-focus-kategorien">Laser-Focus-Kategorien</h3><p>Es gibt drei Ziele, die wir mit unseren Kategorien erreichen wollen:</p><ol><li><p>Entscheiden, welche Aufgaben <strong>im Umfang</strong> des aktuell geplanten Releases liegen.</p></li><li><p>Aufgaben, die für eine Alpha- oder Beta-Version nötig sind, <strong>höher priorisieren</strong> als andere.</p></li><li><p>Die Kategorienamen sollen eine handlungsorientierte, eigenständige <strong>Bedeutung</strong> haben.</p></li></ol><p>Wir schlagen folgende Kategorien vor, die alle Anforderungen erfüllen:</p><p><img src="/assets/images/blog/laser-focus-priority-strategy/laser-focus-categories.webp" alt="Laser focus categories" loading="lazy" /></p><h3 id="vital">Vital</h3><p><strong>Absolutes Minimum, das für die erste Testrunde nötig ist. Darf hässlich sein.</strong></p><p>Damit kann ein Produkt mit nur den „Vital”-Features oder -Aufgaben an eine kleine Gruppe von Testern ausgeliefert werden, um frühes Feedback zu bekommen. Natürlich sollte der Umfang dieses Alpha-Tests klar kommuniziert werden – also welche grundlegenden Features oder Aufgaben noch fehlen, damit sie nicht unnötig von den Testern gemeldet werden. Aber die vitalen Teile des Produkts oder Features können schon getestet werden und wir bekommen die erste Runde Feedback, die zeigt, ob wir in die richtige Richtung gehen.</p><h3 id="essential">Essential</h3><p><strong>Kernaspekte, die für die grundlegende Funktionalität nötig sind. Darf raue Kanten haben.</strong></p><p>Eine zweite, größere Testrunde kann gestartet werden, sobald alle „Essential”-Features oder -Aufgaben umgesetzt sind. Auf dieser Stufe muss kein konkreter Testumfang kommuniziert werden – es reicht, die Version als „Beta” zu bezeichnen, bei der die Grundfunktionen verfügbar sind, aber noch vieles fehlt oder unvollständig ist.</p><h3 id="completing">Completing</h3><p><strong>Raue Kanten glätten und Funktionalitäten vervollständigen.</strong></p><p>Die „Completing”-Stufe definiert den Umfang, bei dem das finale Produkt bereit für das Release ist. In manchen Situationen, z. B. wenn eine neue Version für ein bestimmtes Datum angekündigt wurde, kann das Produkt auch veröffentlicht werden, während noch einige „Completing”-Aufgaben offen sind – dann sollte es aber öffentlich als „Beta” gekennzeichnet werden. Typischerweise umfasst diese Stufe alle Features oder Aufgaben, die für eine größere Nutzerbasis wichtig sind, aber für die Bewertung des Kerns des Produkts nicht relevant sind.</p><h3 id="optional">Optional</h3><p><strong>Nice-to-haves, die auf spätere Versionen verschoben oder ganz weggelassen werden können.</strong></p><p>Die „Optional”-Stufe drückt aus, dass die so bewerteten Features oder Aufgaben zwar gewünscht sind, aber in keiner Weise nötig, um eine fertige Version eines Produkts zu veröffentlichen – auch langfristig nicht. Daher können sie bei Bedarf je nach Ressourcen des Teams einfach verschoben oder gestrichen werden.</p><h3 id="retracting">Retracting</h3><p><strong>Nice-to-haves (auf den ersten Blick), die (potenziell) mehr schaden als nützen können.</strong></p><p>Anders als „Optional” sollten Features oder Aufgaben, die als „Retracting” bewertet werden, <em>aktiv vermieden</em> werden. Das heißt, es kann sinnvoll sein, sie inklusive der Begründung, warum sie vermieden werden sollten, für langfristige Entscheidungsfindung zu dokumentieren. Das spart Zeit, wenn dieselbe Idee irgendwann in der Zukunft wieder aufkommt. Außerdem kann es bei der Bewertung durch mehrere Personen helfen, die Aufgaben zu identifizieren, bei denen eine Diskussion nötig ist, um die Auswirkung einer Aufgabe auf das Produkt zu klären.</p><h3 id="laser-focus-matrix">Laser-Focus-Matrix</h3><p>Die zweite Säule der Laser-Focus-Strategie ist ihre <strong>mehrdimensionale Skalierbarkeit</strong>. Um zu erklären, was das bedeutet und warum es wichtig ist, wenden wir die bisherigen Kategorien an einem Beispiel an: Lass uns eine Stoppuhr-App entwickeln, die die Zeit für verschiedene Tätigkeiten über den Tag hinweg erfasst. Das ist die anfängliche Liste der Feature-Ideen, bewertet mit den Laser-Focus-Kategorien:</p><ol><li><p>Projekte erstellen
→ <strong>Essential</strong> (essentiell für die App, aber vorgefüllte Projekte reichen für den ersten Test)</p></li><li><p>Projekte bearbeiten
→ <strong>Completing</strong> (für Testzwecke nicht nötig, aber für das finale Release)</p></li><li><p>Projekte löschen
→ <strong>Completing</strong> (Aufräumaufgabe, für Tests nicht nötig, aber für das finale Release)</p></li><li><p>Timer starten/stoppen
→ <strong>Vital</strong> (Kernidee der App, vitaler Teil)</p></li><li><p>Ein Projekt für den Timer auswählen
→ <strong>Vital</strong> (ohne Projektauswahl ist die App-Idee nicht erfüllt)</p></li><li><p>Vergangene erfasste Zeiten bearbeiten
→ <strong>Retracting</strong> (V2 mit Wettbewerbs-Feature geplant, Risiko von Manipulation)</p></li><li><p>Vergangene erfasste Zeiten löschen
→ <strong>Optional</strong> (nice to have, kein Manipulationsrisiko, da keine Zeit hinzugefügt wird)</p></li><li><p>Historische erfasste Zeit für ein ausgewähltes Projekt anzeigen
→ <strong>Essential</strong> (Kernanwendungsfall der App)</p></li><li><p>Projekte mit der meisten erfassten Zeit anzeigen
→ <strong>Essential</strong> (Kernanwendungsfall der App)</p></li></ol><p>Dank der Kategorisierung können wir schon zwei Features aus dem ersten Release ausschließen und haben sogar ein Feature erkannt, das wir wahrscheinlich nie umsetzen sollten (6) und das dauerhaft dokumentiert werden sollte. Aber noch wichtiger: Wir wissen jetzt, dass 4 und 5 die „Vital”-Features sind, die zuerst umgesetzt werden müssen.</p><p>Fangen wir an, an ihren Unteraufgaben zu arbeiten:</p><p><strong>4. Timer starten/stoppen:</strong> (Vital)</p><p>a.	Start/Stop-Button-Layout designen (Low Fidelity)
b.	Start/Stop-Button-Farben &amp; Icons designen (High Fidelity)
c.	Start/Stop-Button pulsierenden Schatteneffekt designen (Animationen)
d.	Start/Stop-Button-Layout implementieren (Low Fidelity)
e.	Start/Stop-Button-Farben &amp; Icons implementieren (High Fidelity)
f.	Start/Stop-Button pulsierenden Schatteneffekt implementieren (Animationen)
g.	Grundlegende Datenbankmodelle für erfasste Zeiten aufsetzen
h.	Start/Stop-Aktionen in der Datenbank persistieren</p><p><strong>5. Ein Projekt für den Timer auswählen:</strong> (Vital)</p><p>a.	Projektauswahl-Navigation &amp; Layout designen (Low Fidelity)
b.	Projektauswahl-Formen, Farben &amp; Icons designen (High Fidelity)
c. 	Projektauswahl-Navigation &amp; Layout implementieren (Low Fidelity)
d.	Projektauswahl-Formen, Farben &amp; Icons implementieren (High Fidelity)
e.	Ausgewähltes Projekt im Datenbankmodell für erfasste Zeiten persistieren</p><p>Alles klar, legen wir los, oder? <em>Oder?</em></p><p>Nein. Du hast es sicher schon beim Lesen bemerkt. Es gibt ein Problem. Wir haben die Features priorisiert und darüber nachgedacht, was wirklich nötig ist, um testbar zu sein, um die App in die Hände der Nutzer zu geben. Aber jetzt haben wir dasselbe Problem wieder, nur auf einer anderen Ebene. Diese Aufgaben (und potenziell auch ihre Unteraufgaben) sind nicht alle „Vital” für unsere allererste Version, die wir in die Hände der Nutzer geben wollen. Wie können wir das lösen? Sollten wir eine weitere Bewertung für die Aufgaben durchführen?</p><p>Ja, unbedingt! Das ist sogar eine Anforderung der Laser-Focus-Strategie: Wende die Bewertung auf <strong>allen Ebenen</strong> nach unten hin an! Nicht unbedingt auf höheren Ebenen, wo du jede beliebige alternative Priorisierungstechnik verwenden darfst. Aber die tieferen Ebenen ab dem Punkt, an dem du anfangen willst, sollten alle so bewertet werden.</p><p>Weisen wir den Aufgaben also auch die Laser-Focus-Kategorien zu und schauen dann, was das für die Gesamtpriorität bedeutet:</p><p><strong>4. Timer starten/stoppen:</strong> (Vital)</p><p>a.	Start/Stop-Button-Layout designen (Low Fidelity)
→ <strong>Vital</strong>
b.	Start/Stop-Button-Farben &amp; Icons designen (High Fidelity)
→ <strong>Completing</strong>
c.	Start/Stop-Button pulsierenden Schatteneffekt designen (Animationen)
→ <strong>Optional</strong>
d.	Start/Stop-Button-Layout implementieren (Low Fidelity)
→ <strong>Vital</strong>
e.	Start/Stop-Button-Farben &amp; Icons implementieren (High Fidelity)
→ <strong>Completing</strong>
f.	Start/Stop-Button pulsierenden Schatteneffekt implementieren (Animationen)
→ <strong>Optional</strong>
g.	Grundlegende Datenbankmodelle für erfasste Zeiten aufsetzen
→ <strong>Essential</strong>
h.	Start/Stop-Aktionen in der Datenbank persistieren
→ <strong>Essential</strong></p><p><strong>5. Ein Projekt für den Timer auswählen:</strong> (Vital)</p><p>a.	Projektauswahl-Navigation &amp; Layout designen (Low Fidelity)
→ <strong>Vital</strong>
b.	Projektauswahl-Formen, Farben &amp; Icons designen (High Fidelity)
→ <strong>Completing</strong>
c. 	Projektauswahl-Navigation &amp; Layout implementieren (Low Fidelity)
→ <strong>Vital</strong>
d.	Projektauswahl-Formen, Farben &amp; Icons implementieren (High Fidelity)
→ <strong>Completing</strong>
e.	Ausgewähltes Projekt im Datenbankmodell für erfasste Zeiten persistieren
→ <strong>Essential</strong></p><p>Wichtig: Der Bezugswert für die Bewertung der Aufgaben war das Feature, weil es das direkte Elternelement ist. Das heißt, ich habe mir die Frage gestellt: „Ist das Persistieren von Start/Stop-Aktionen in der Datenbank vital oder essential <em>für das Feature</em> Timer starten/stoppen?” – und nicht für die App oder irgendetwas anderes. Das macht die Beantwortung der Fragen deutlich einfacher.</p><p>Visualisieren wir diese zwei verschiedenen Bewertungsebenen mit einer einfachen Matrix. Auf der X-Achse sind die Bewertungen der Features. Auf der Y-Achse die Bewertungen der Aufgaben. Die Kreise stellen die Aufgaben dar:</p><p><img src="/assets/images/blog/laser-focus-priority-strategy/laser-focus-matrix.webp" alt="Laser focus matrix" loading="lazy" /></p><p>Wie du sehen kannst, befinden sich die Aufgaben 4a, 4d, 5a und 5c im Feld unten links, dem „Vital-Vital”-Feld, kurz „VV”. Der Hintergrund des Feldes ist rot eingefärbt. Es enthält alle Aufgaben, auf die man sich zuerst konzentrieren sollte. Sobald sie alle umgesetzt sind, kann die allererste Testrunde beginnen und die Alpha-Phase startet.</p><p>Die Aufgaben 4g, 4h und 5e im gelb eingefärbten Feld „Vital-Essential” oder kurz „VE” sollten als Nächstes angegangen werden. Sobald alle Aufgaben in allen drei gelb eingefärbten Feldern (VV, VE, EV) abgeschlossen sind, beginnt die Beta-Phase.</p><p>Das „VC”-Feld mit seinen „Completing”-Aufgaben für die „Vital”-Features sollte unter den bisher definierten Aufgaben zuletzt angegangen werden. Sobald alle Aufgaben in allen grün eingefärbten Feldern (VC, CV, EC, CE, CC) erledigt sind, ist es Release-Zeit.</p><p>Im obigen Beispiel haben wir die Aufgaben für alle nicht-vitalen Features übersprungen. Hätten wir sie auch bewertet, hätte die vollständige Matrix etwa so aussehen können, einschließlich der „Retracting”-Bewertung:</p><p><img src="/assets/images/blog/laser-focus-priority-strategy/laser-focus-matrix-2.webp" alt="Laser focus matrix 2" loading="lazy" /></p><p>Wir können sehen, wie die Alpha-, Beta- und Release-Aufgaben kreisförmig um den Ursprungspunkt (unten links) geschichtet sind und uns visuell eine Priorität für jede Aufgabe basierend auf ihrem Abstand zum Ursprung geben. Das skaliert problemlos auf eine dritte Achse, wenn zum Beispiel Unteraufgaben zu jeder Aufgabe hinzukommen. Formal gesprochen skaliert das auf beliebig viele Dimensionen. Um die Gesamtkategorie eines bestimmten Elements zu berechnen, schau dir einfach alle Vorfahren an und wähle die niedrigste Priorität als Gesamtkategorie des „atomaren” (untersten) Elements. Stell dir zum Beispiel eine Unteraufgabe mit der Kategoriebewertung „Essential” vor, einer übergeordneten Aufgabe mit „Vital” und deren übergeordnetem Feature mit „Completing”. Insgesamt ist die niedrigste Priorität „Completing”, also ist das die Gesamtkategorie der Unteraufgabe.</p><p>Die Berechnung der Gesamtkategorie allein kann dazu führen, dass viele Aufgaben auf derselben Stufe landen, besonders bei „Completing”, wo wir 5 verschiedene Felder haben. Eine Möglichkeit, Features oder Aufgaben innerhalb derselben Kategorie zu priorisieren, ist die Berechnung des Durchschnitts aus der eigenen Kategorie und der aller Vorfahren-Kategorien. Dazu weisen wir jeder Kategorie eine Zahl zu (von 1 „Vital” bis 5 „Retracting”), die unterste Ebene (z. B. eine Unteraufgabe) kann dann als Tupel dargestellt werden, z. B. <code>(2, 1, 3)</code> im obigen Beispiel. Der Durchschnitt dieser Zahlen wird einfach berechnet: <code>(2 + 1 + 3) / 3 = 2.0</code>. Eine andere Aufgabe mit mehr Vorfahren und derselben Gesamtkategorie „Completing” könnte als <code>(3, 2, 3, 1)</code> bewertet sein und hätte daher einen Durchschnitt von <code>(3 + 2 + 3 + 1) / 4 = 2.25</code>, sollte also niedriger priorisiert werden. Je höher der Gesamtdurchschnitt, desto niedriger die Priorität – das ergibt viel Sinn, da die Durchschnittszahl in etwa den Abstand zum Ursprung widerspiegelt – der höchstmöglichen Priorität.</p><p>Aber keine Sorge, du musst diese Durchschnitte nicht tatsächlich berechnen. Es gibt einen einfacheren Weg basierend auf der Matrix, die wir oben gesehen haben, mit ausreichender Genauigkeit:</p><p><img src="/assets/images/blog/laser-focus-priority-strategy/priority-order-matrix.webp" alt="Laser Focus priority order matrix showing field processing sequence" loading="lazy" /></p><p>Das obige Diagramm zeigt, in welcher Reihenfolge die Felder bearbeitet werden sollten, basierend auf dem Abstand zum Ursprung. Beachte, dass es jeweils zwei Felder auf Platz 2, 4 und 5 gibt. Für diese Felder muss je nach Situation eine Entscheidung getroffen werden: Sollten wir uns mehr darauf konzentrieren, weitere Features hinzuzufügen? Oder sollten wir uns mehr darauf konzentrieren, die bereits begonnenen Features zu verbessern? Für eine Feature-Erweiterung zuerst solltest du in Richtung der „Feature-Kategorie” weitermachen, z. B. „EV” vor „VE”. Für die Verbesserung bestehender Features zuerst sollte es andersherum sein.</p><h3 id="laser-focus-aufschlüsselung">Laser-Focus-Aufschlüsselung</h3><p>Im obigen Abschnitt haben wir gelernt, dass die Kategorisierung auf mehreren Ebenen der Schlüssel zum Laser-Focus-Konzept ist. Wenn du das direkt auf dein Projekt anwenden willst, wirst du vielleicht feststellen, dass viele oder sogar alle deine Features oder Aufgaben für dich eigentlich „Vital” oder „Essential” sind. Wenn das der Fall ist, dann ist es ein Zeichen dafür, dass du deine Aufgaben wahrscheinlich noch nicht effizient aufgeteilt hast.</p><p>Deshalb ist es wichtig, deine Aufgaben richtig herunterzubrechen, bevor du sie kategorisierst. Die Leitfrage, die du dir beim Aufteilen von Features in Aufgaben oder Aufgaben in Unteraufgaben stellen solltest, sollte nicht nur sein: „Welche Schritte muss ich machen, um es fertigzustellen?” Du solltest auch den Aufwand für jeden Schritt bedenken, und wenn der Aufwand nicht vernachlässigbar klein ist, solltest du in Betracht ziehen, ihn abzutrennen. Manchmal mag das schwierig erscheinen, aber meistens ist es eine gute Idee, dem Ansatz „erst zum Laufen bringen, dann verbessern” beim Aufteilen der Aufgaben zu folgen.</p><p>Beim obigen Feature „Timer starten/stoppen” hätten wir zum Beispiel in 3 Aufgaben aufteilen können: „Start/Stop-Buttons designen”, „Start/Stop-Buttons implementieren” und „Daten persistieren”. Das Problem dabei ist, dass es keine verschiedenen Fertigstellungsstufen gibt. Es ist besser, noch weiter herunterzubrechen. Natürlich könnten wir das als Unteraufgaben unter diesen Aufgaben tun, aber um die Prioritätsberechnung einfacher zu machen, empfiehlt es sich, es auf weniger Ebenen zu tun. Also haben wir uns stattdessen für „Start/Stop-Button-<em>Layout</em> designen”, „Start/Stop-Button-<em>Layout</em> implementieren” entschieden und dieselben zwei Aufgaben auch für „… Farben &amp; Icons” und „… pulsierender Schatteneffekt”.</p><p>Frag dich, welche Teile ihren eigenen Aufwand haben, und teile sie so auf, dass jede Aufgabe es wert ist, basierend auf dem nötigen Aufwand priorisiert zu werden. Trenne keine Mikroaufgaben ab – es lohnt sich nicht, so kleine Aufgaben zu priorisieren; behalte sie einfach als Teil einer anderen Aufgabe.</p><p>Eine ordentliche Aufschlüsselung ist sehr wichtig, damit die Laser-Focus-Strategie effektiv ist.</p><h3 id="zusammenfassung">Zusammenfassung</h3><p>Fassen wir die Laser-Focus-Priorisierungsstrategie in der richtigen Reihenfolge zusammen:</p><ol><li><p><strong>Brich</strong> deine Features und Aufgaben in kleinere Schritte mit verschiedenen Fertigstellungsstufen <strong>herunter</strong></p></li><li><p><strong>Bewerte</strong> sie auf jeder Ebene mit „Vital”, „Essential”, „Completing”, „Optional” oder „Retracting”</p></li><li><p><strong>Visualisiere oder berechne</strong> die Gesamtpriorität für die unterste Ebene unter Berücksichtigung aller Vorfahren</p></li></ol><p>Wende diese Schritte zu jedem beliebigen Zeitpunkt in deinem Projekt an und sie werden dir helfen, dich auf die wichtigen Dinge zu konzentrieren und deine In-Arbeit-Versionen so früh wie sinnvoll in die Hände der Nutzer zu geben.</p>]]></content:encoded>
</item>
<item>
<title>Git Merge vs Rebase</title>
<link>https://fline.dev/de/blog/git-merge-vs-rebase/</link>
<guid isPermaLink="true">https://fline.dev/de/blog/git-merge-vs-rebase/</guid>
<pubDate>Thu, 01 Jul 2021 00:00:00 +0000</pubDate>
<description><![CDATA[Eine FAQ, die erklärt, wann man was verwenden sollte – und warum.]]></description>
<content:encoded><![CDATA[<p>Unter Entwicklern gibt es eine häufige Diskussion darüber, wie Teams <a href="https://git-scm.com/">Git</a> verwenden sollten, um sicherzustellen, dass alle immer auf dem neuesten Stand mit den letzten Änderungen im <code>main</code>-Branch sind. Die typische Situation, in der diese Frage aufkommt, ist wenn jemand auf einem neuen Branch gearbeitet hat und dann, sobald die Arbeit fertig und bereit zum Mergen ist, sich der Main-Branch zwischenzeitlich so verändert hat, dass der Arbeits-Branch veraltet ist und nun <strong>Merge-Konflikte</strong> hat.</p><p>Natürlich müssen diese gelöst werden, bevor der Arbeits-Branch gemergt werden kann. Aber die Frage ist: <em>Wie</em> sollte diese Situation gelöst werden? Sollten wir den Main-Branch in den Arbeits-Branch <em><strong>mergen</strong></em>? Oder sollten wir den Arbeits-Branch auf den neuesten Main-Branch <em><strong>rebasen</strong></em>?</p><p>Meiner Meinung nach gibt es nur eine richtige Antwort auf diese Frage. Aus meiner Erfahrung ist der Hauptgrund, warum so viele Diskussionen rund um dieses Thema entstehen, dass es viele Missverständnisse darüber gibt, wie sich Merge und Rebase in diesem Kontext unterscheiden, und ein generelles Verständnisdefizit, was ein Rebase überhaupt ist.</p><p>Deshalb habe ich eine FAQ für mein Team erstellt, die versucht, die Dinge klarzustellen. Die möchte ich hier teilen:</p><h4 id="was-ist-ein-merge">Was ist ein Merge?</h4><p>Ein Commit, der alle Änderungen eines anderen Branches in den aktuellen kombiniert.</p><h4 id="was-ist-ein-rebase">Was ist ein Rebase?</h4><p>Alle Commits des aktuellen Branches auf einen anderen Basis-Commit neu anzuwenden.</p><h4 id="was-sind-die-hauptunterschiede-zwischen-merge-und-rebase">Was sind die Hauptunterschiede zwischen Merge und Rebase?</h4><ol><li><p><code>merge</code> führt nur <strong>einen</strong> neuen Commit aus. <code>rebase</code> führt typischerweise <strong>mehrere</strong> aus (Anzahl der Commits im aktuellen Branch).</p></li><li><p><code>merge</code> erzeugt einen <strong>neuen</strong> generierten Commit (den sogenannten Merge-Commit). <code>rebase</code> verschiebt nur <strong>bestehende</strong> Commits.</p></li></ol><h4 id="in-welchen-situationen-sollte-man-merge-verwenden">In welchen Situationen sollte man <code>merge</code> verwenden?</h4><p>Verwende <code>merge</code>, wenn du Änderungen eines abgezweigten Branches <strong>zurück</strong> in den Basis-Branch übernehmen willst.</p><p>Typischerweise macht man das über den “Merge”-Button bei Pull/Merge Requests, z.B. auf GitHub.</p><h4 id="in-welchen-situationen-sollte-man-rebase-verwenden">In welchen Situationen sollte man <code>rebase</code> verwenden?</h4><p>Verwende <code>rebase</code>, wenn du <strong>Änderungen eines Basis-Branches</strong> in einen abgezweigten Branch übernehmen willst.</p><p>Typischerweise macht man das in <code>work</code>-Branches, wenn es eine Änderung im <code>main</code>-Branch gibt.</p><h4 id="warum-nicht-merge-verwenden-um-änderungen-vom-basis-branch-in-einen-arbeits-branch-zu-mergen">Warum nicht <code>merge</code> verwenden, um Änderungen vom Basis-Branch in einen Arbeits-Branch zu mergen?</h4><ol><li><p>Die Git-History wird viele <strong>unnötige Merge-Commits</strong> enthalten. Wenn mehrere Merges in einem Arbeits-Branch nötig waren, könnte der Arbeits-Branch sogar mehr Merge-Commits als eigentliche Commits enthalten!</p></li><li><p>Das erzeugt eine Schleife, die <strong>das mentale Modell zerstört, für das Git entworfen wurde</strong>, was zu Problemen bei jeder Visualisierung der Git-History führt.</p></li></ol><p>Stell dir vor, es gibt einen Fluss (z.B. den “Nil”). Wasser fließt in eine Richtung (Zeitrichtung in der Git-History). Hin und wieder gibt es Abzweigungen von diesem Fluss und die meisten davon fließen irgendwann wieder in den Fluss zurück. So könnte der natürliche Flussverlauf aussehen. Das ergibt Sinn.</p><p>Aber dann stell dir mal vor, es gibt eine kleine Abzweigung dieses Flusses. Dann, aus irgendeinem Grund, <strong>fließt der Fluss in die Abzweigung</strong> und die Abzweigung geht von dort aus weiter. Der Fluss ist nun technisch gesehen verschwunden, er befindet sich jetzt in der Abzweigung. Aber dann, auf magische Weise, wird diese Abzweigung wieder zurück in den Fluss geführt. In welchen Fluss fragst du? Keine Ahnung. Der Fluss sollte eigentlich in der Abzweigung sein, aber irgendwie existiert er noch weiter und ich kann die Abzweigung zurück in den Fluss führen. Also ist der Fluss im Fluss. Irgendwie ergibt das keinen Sinn.</p><p>Genau das passiert, wenn du den Basis-Branch in einen <code>work</code>-Branch <code>merge</code>st und dann, wenn der <code>work</code>-Branch fertig ist, diesen wieder zurück in den Basis-Branch mergst. Das mentale Modell ist kaputt. Und deswegen hat man am Ende eine Branch-Visualisierung, die nicht besonders hilfreich ist.</p><h4 id="beispiel-einer-git-history-bei-verwendung-von-merge">Beispiel einer Git-History bei Verwendung von <code>merge</code>:</h4><p><img src="/assets/images/blog/git-merge-vs-rebase/example-git-history-when.webp" alt="Beispiel einer Git-History bei Verwendung von Merge" loading="lazy" /></p><p><em>Beispiel einer Git-History bei Verwendung von <code>merge</code></em></p><p>Beachte die vielen Commits, die mit <code>Merge branch 'main' into …</code> beginnen (mit gelben Kästchen markiert). Die existieren bei einem Rebase gar nicht (dort hat man nur Pull-Request-Merge-Commits). Beachte auch die vielen visuellen Branch-Merge-Schleifen (<code>main</code> in <code>work</code> in <code>main</code>).</p><h4 id="beispiel-einer-git-history-bei-verwendung-von-rebase">Beispiel einer Git-History bei Verwendung von <code>rebase</code>:</h4><p><img src="/assets/images/blog/git-merge-vs-rebase/example-git-history-when-2.webp" alt="Beispiel einer Git-History bei Verwendung von Rebase" loading="lazy" /></p><p><em>Beispiel einer Git-History bei Verwendung von <code>rebase</code></em></p><p>Viel sauberere Git-History mit deutlich weniger Merge-Commits und keinerlei unübersichtlichen visuellen Branch-Merge-Schleifen.</p><h4 id="gibt-es-nachteile-fallstricke-bei-rebase">Gibt es Nachteile / Fallstricke bei <code>rebase</code>?</h4><p>Ja:</p><ol><li><p>Da ein <code>rebase</code> Commits verschiebt (technisch gesehen neu ausführt), wird das Commit-Datum aller verschobenen Commits auf den Zeitpunkt des Rebase gesetzt und die <strong>Git-History verliert den ursprünglichen Commit-Zeitpunkt</strong>. Wenn also das genaue Datum eines Commits aus irgendeinem Grund wichtig ist, dann ist <code>merge</code> die bessere Option. Aber typischerweise ist eine saubere Git-History viel nützlicher als exakte Commit-Daten.</p></li><li><p>Wenn der gerebaste Branch mehrere Commits hat, die dieselbe Zeile ändern, und diese Zeile auch im Basis-Branch geändert wurde, musst du möglicherweise Merge-Konflikte für dieselbe Zeile mehrmals lösen, was beim Mergen nie nötig ist. Im Durchschnitt gibt es also mehr Merge-Konflikte zu lösen.</p></li></ol><h4 id="tipps-zur-reduzierung-von-merge-konflikten-bei-verwendung-von-rebase">Tipps zur Reduzierung von Merge-Konflikten bei Verwendung von <code>rebase</code>:</h4><ol><li><p><strong>Häufig rebasen.</strong> Ich empfehle, es mindestens einmal täglich zu machen.</p></li><li><p>Versuche, <strong>Änderungen an derselben Zeile</strong> so weit wie möglich in einen Commit zusammenzufassen (squashen).</p></li></ol><p>Ich hoffe, diese FAQ hilft dem einen oder anderen Team da draußen.</p>]]></content:encoded>
</item>
<item>
<title>Einführung in reguläre Ausdrücke</title>
<link>https://fline.dev/de/blog/primer-on-regular-expressions/</link>
<guid isPermaLink="true">https://fline.dev/de/blog/primer-on-regular-expressions/</guid>
<pubDate>Thu, 06 May 2021 00:00:00 +0000</pubDate>
<description><![CDATA[In diesem Beitrag versuche ich, dir einen praktischen Überblick über reguläre Ausdrücke zu geben – was sie sind, wofür man sie verwenden kann und wie du schnell damit loslegen kannst.]]></description>
<content:encoded><![CDATA[<h3 id="was-sind-reguläre-ausdrücke-eigentlich">Was sind reguläre Ausdrücke eigentlich?</h3><p>Reguläre Ausdrücke (kurz Regexes) sind Strings, die als DSL (Domain-Specific Language, also eine domänenspezifische Sprache) funktionieren, um bestimmte häufige Aufgaben in anderen Strings zu erledigen. Eine DSL kann man auch als “eine Programmiersprache innerhalb einer Programmiersprache” beschreiben.</p><p>Im Fall von Regexes kann die äußere Programmiersprache jede beliebige Sprache sein, die den Typ <code>String</code> unterstützt – sie muss eben nur Regexes unterstützen. Nahezu alle populären Programmiersprachen unterstützen Regexes, was sie so nützlich macht. Die innere Sprache der Regexes besteht nur aus <code>String</code>, wobei bestimmte Zeichen eine besondere Bedeutung haben.</p><p>Zum Beispiel bedeutet im String <code>&quot;.*@.*\\.com&quot;</code> der <code>.</code> “ein beliebiges Zeichen”, das <code>*</code> bedeutet “beliebig oft <was davor steht>”, zusammen bedeutet <code>.*</code> also “beliebig viele beliebige Zeichen”. Dann haben wir ein nicht-spezielles Zeichen <code>@</code>, dann wieder <code>.*</code> gefolgt von <code>\\</code>, was “das nächste Zeichen escapen und als nicht-spezielles Zeichen behandeln” bedeutet – <code>\\.</code> zusammen liest sich also wie ein normaler <code>.</code> ohne die besondere Bedeutung “beliebiges Zeichen”. Zum Schluss kommt <code>com</code>, was einfach eine Reihe von Zeichen ohne besondere Bedeutung ist. Insgesamt ist diese Regex ein einfacher Matcher für jede E-Mail-Adresse, die mit <code>.com</code> endet und irgendwo ein <code>@</code> enthält.</p><h3 id="was-kann-ich-mit-der-regex-dsl-machen">Was kann ich mit der “Regex-DSL” machen?</h3><blockquote><p>ℹ️ Wenn du <a href="https://www.ruby-lang.org/en/">Ruby</a> installiert hast (auf Macs ist es vorinstalliert), kannst du <code>irb</code> eingeben, um eine <strong>i</strong>nteraktive <strong>R</strong>u<strong>b</strong>y-Shell zu starten und mit den folgenden Beispielen herumzuspielen.</p></blockquote><p>Es gibt drei Hauptfunktionen, mit denen jeder Regex-String verwendet werden kann:</p><ol><li><p><code>matches</code>: Gegeben eine Regex und einen anderen String, prüft diese Funktion, ob der gegebene String zur Regex “passt”. Das heißt, wenn es “irgendeinen” Teil im gegebenen String gibt, der zur angegebenen Regex passt, gibt sie <code>true</code> zurück, sonst <code>false</code>. Zum Beispiel in Ruby (wo <code>matches</code> als <code>match?</code> heißt – das <code>?</code> ist Teil des Funktionsnamens):</p></li></ol><pre><code class="language-Ruby">/.*@.*\\.com/.match?('harry.potter@hogwarts.co.uk') # =&gt; false /.*@.*\\.com/.match?('queenie.goldstein@ilvermorny.com') # =&gt; true</code></pre><ol><li><p><code>captures</code>: Gegeben eine Regex und einen anderen String, kann diese Funktion Teilstrings aus dem gegebenen Text auslesen, die zu markierten Teilen der gegebenen Regex passen. Die Teile in der Regex werden mit <code>(</code> und <code>)</code> markiert. Sie werden “Capture Groups” (Erfassungsgruppen) genannt. Zum Beispiel in Ruby (wo auf <code>captures</code> über das <code>Match</code>-Objekt zugegriffen wird):</p></li></ol><pre><code class="language-Ruby">/(.*)@(.*)\\.com/.match('queenie.goldstein@ilvermorny.com').captures # =&gt; [&quot;queenie.goldstein&quot;, &quot;ilvermorny&quot;]</code></pre><ol><li><p><code>replace</code>: Gegeben eine Regex und ein Template-String, kann diese Funktion Treffer automatisch mit einem gegebenen String ersetzen, wobei sogar die Capture Groups über <code>$1</code>, <code>$2</code> oder in manchen Sprachen auch <code>\\1</code>, <code>\\2</code> usw. referenziert werden können. Zum Beispiel in Ruby (wo <code>replace</code> als <code>gsub</code> heißt):</p></li></ol><pre><code class="language-Ruby">'queenie.goldstein@ilvermorny.com'.gsub(/(.*)@(.*)\\.com/, '\\1@\\2.org') # =&gt; &quot;queenie.goldstein@ilvermorny.org&quot;</code></pre><h3 id="wie-sieht-die-regex-dsl-aus">Wie sieht die “Regex-DSL” aus?</h3><p>Es gibt jede Menge nützliche “Cheat Sheets” mit tollen Beispielen dafür:</p><ul><li><p><a href="https://www.rexegg.com/regex-quickstart.html#chars">https://www.rexegg.com/regex-quickstart.html#chars</a></p></li><li><p><a href="https://www.regular-expressions.info/examples.html">Regular Expression Examples</a></p></li></ul><p>Grundsätzlich gibt es 5 verschiedene Arten von DSL-Komponenten zu verstehen:</p><h4 id="1-zeichen-gruppenmodifikatoren-zb">1. Zeichen-/Gruppenmodifikatoren (z.B. <code>*</code>, <code>+</code>, <code>{,}</code>, <code>?</code>)</h4><p>Der Standard-“Baustein” von Regexes sind Zeichen. Nach jedem Zeichen kannst du einen Modifikator schreiben, der angibt, wie oft das vorangehende Zeichen gematcht wird. Folgende Modifikatoren stehen zur Verfügung:</p><p>**<code>0</code> oder <code>1</code> Mal: **
<code>?</code> (Beispiel: <code>a?b?c?</code> matcht <code>a</code>, <code>ab</code>, <code>abc</code>, <code>bc</code>, <code>c</code>)</p><p>**Genau <code>1</code> Mal: **
Kein Modifikator (Standard)</p><p>**<code>0</code> bis ♾️ Mal: **
<code>*</code> (Beispiel: <code>a*bc</code> matcht <code>bc</code>, <code>abc</code>, <code>aaabc</code>)</p><p><strong><code>1</code> bis ♾️ Mal:</strong>
<code>+</code> (Beispiel: <code>a+bc</code> matcht <code>abc</code>, <code>aaabc</code>, aber nicht <code>bc</code>)</p><p><strong>Genau <code>X</code> Mal:</strong>
<code>{X}</code> (Beispiel: <code>a{3}bc</code> matcht <code>aaabc</code>, aber nicht <code>aabc</code>, <code>aaaabc</code>)</p><p><strong><code>X</code> bis <code>Y</code> Mal:</strong>
<code>{X,Y}</code> (Beispiel: <code>a{2,5}bc</code> matcht <code>aaaaabc</code>, aber nicht <code>abc</code>)</p><p><strong><code>X</code> bis ♾️ Mal:</strong>
<code>{X,}</code> (Beispiel: <code>a{2,}bc</code> matcht <code>aaaaaaaabc</code>, aber nicht <code>abc</code>)</p><p>Die gleichen Modifikatoren funktionieren auch auf Gruppen (z.B. <code>(abc)+</code>) (siehe unten bei Gruppen).</p><h4 id="benutzerdefinierte-zeichensätze-erstellt-mit-und">Benutzerdefinierte Zeichensätze (erstellt mit <code>[</code> und <code>]</code>)</h4><p>Du kannst eigene Zeichensätze definieren, indem du die Zeichen ohne Trennzeichen in eckigen Klammern auflistest, z.B. für einen Satz der Zeichen a, b, c und der Zahlen 1, 2, 3 schreibst du <code>[abc123]</code>. Dies wird dann als “ein Zeichen aus diesem Satz” betrachtet, und um mehrere davon zu matchen, braucht man Zeichenmodifikatoren wie <code>[abc123]*</code> oder <code>[abc123]{2,5}</code>.</p><p>Du kannst auch <code>^</code> am Anfang eines benutzerdefinierten Zeichensatzes verwenden, um anzugeben, dass du jedes Zeichen akzeptierst, außer denen, die du in den Klammern angegeben hast, z.B. <code>[^\\n]</code> um jedes Zeichen außer einem Zeilenumbruch zu akzeptieren.</p><p>Bei Zeichen, von denen du weißt, dass sie direkt aufeinanderfolgen – wie Zahlen oder das Alphabet – kannst du auch Bereiche verwenden, indem du ein <code>-</code> dazwischensetzt, z.B. <code>[a-zA-Z0-9]</code>.</p><p><code>[abc123]{3,}</code> würde nicht <code>a</code>, <code>b</code>, <code>c</code>, <code>ab</code> matchen, aber <code>111</code>, <code>abc</code> schon.</p><h4 id="vordefinierte-zeichensätze-s-s-d-d-w-w">Vordefinierte Zeichensätze (<code>\\s</code>, <code>\\S</code>, <code>\\d</code>, <code>\\D</code>, <code>\\w</code>, <code>\\W</code>)</h4><p>Die folgenden Zeichensätze (vereinfacht) sind bereits vordefiniert und können direkt verwendet werden:</p><ul><li><p><code>\\s</code> entspricht im Wesentlichen <code>[ \\t\\n]</code>, liest sich als “ein beliebiges Leerzeichen”</p></li><li><p><code>\\S</code> entspricht im Wesentlichen <code>[^ \\t\\n]</code>, liest sich als “ein beliebiges Nicht-Leerzeichen”</p></li><li><p><code>\\d</code> entspricht im Wesentlichen <code>[0-9]</code>, liest sich als “eine beliebige Ziffer”</p></li><li><p><code>\\D</code> entspricht im Wesentlichen <code>[^0-9]</code>, liest sich als “eine beliebige Nicht-Ziffer”</p></li><li><p><code>\\w</code> ähnlich wie <code>[a-zA-Z_0-9]</code> (einschließlich Umlaute usw.), liest sich als “ein beliebiges Wortzeichen”</p></li><li><p><code>\\W</code> ähnlich wie <code>[^a-zA-Z_0-9]</code>, liest sich als “ein beliebiges Nicht-Wortzeichen”</p></li></ul><h4 id="gruppen-zb-und-name-und">Gruppen (z.B. <code>(</code> und <code>)</code>, <code>(?&lt;name&gt;</code> und <code>)</code>)</h4><p>Gruppen kann man sich wie “Wörter” oder “Sätze” vorstellen – sie ändern den Standard-Baustein, der “Zeichen” ist, für jeden Modifikator auf einen Satz von Zeichen, also eine “Gruppe”. Zum Beispiel liest sich <code>abc*</code> als “einmal a, einmal b und beliebig oft c”. Wenn du “beliebig oft abc” schreiben willst, machst du es so: <code>(abc)*</code>. Das <code>abc</code> wird dann als eine Gruppe betrachtet und die Regex würde den gesamten String <code>abcabcabc</code> matchen.</p><p>Gruppen erlauben es auch, verschiedene Optionen zur Auswahl anzugeben. Dafür schreibst du eine Gruppe und trennst die verschiedenen Wörter mit einem <code>|</code>, so: <code>(abc|def)</code> – das liest sich als “entweder abc oder def” und würde sowohl <code>123abc123</code> als auch <code>456def456</code> matchen, aber nicht <code>adbecf</code>.</p><p>Diese erfassen bestimmte Teilabschnitte einer Regex und weisen ihnen eine Nummer oder einen Namen zu, der dann im Code oder in Ersetzungs-Templates referenziert werden kann. Typischerweise werden Capture Groups wie in <code>(.*)@(.*).com</code> dann über <code>\\1@\\2.com</code> oder <code>$1@$2.com</code> (je nach Sprache) zurückreferenziert.</p><p>Es ist auch möglich, den Gruppen Namen zu geben, z.B. <code>(?&lt;user&gt;.*)@(?&lt;domain&gt;.*).com</code>, um sie dann wie in <code>${user}@${domain}.com</code> zu referenzieren, aber das sind fortgeschrittene Features, die in verschiedenen Sprachen unterschiedlich implementiert sind (und in manchen fehlen).</p><h4 id="match-modifikatoren-zb-a-z-lookaheads-und-lookbehinds">Match-Modifikatoren (z.B. <code>\\A</code>, <code>\\z</code>, <code>^</code>, <code>$</code>, Lookaheads und Lookbehinds)</h4><p>Standardmäßig wird ein Match für eine Regex wie <code>abc</code> wie eine <code>contains</code>-Methode ausgeführt. Du kannst aber auch angeben, dass der String <code>abc</code> am Anfang oder Ende eines gegebenen Strings oder einer Zeile stehen muss. Zum Beispiel stellt das <code>^</code> in <code>^abc</code> sicher, dass nur Strings matchen, bei denen <code>abc</code> am Anfang einer neuen Zeile steht. Das würde <code>def\\nabc</code> matchen, aber nicht <code>defabc</code>. Das <code>$</code> in <code>abc$</code> stellt sicher, dass nach <code>abc</code> ein Zeilenende kommt. Verwende <code>\\A</code> und <code>\\z</code>, um über den gesamten String zu matchen (über mehrere Zeilen hinweg).</p><p>Lookaheads und Lookbehinds sind eher ein fortgeschrittenes Thema und besonders dann nützlich, wenn du matchen willst, dass der Anfang oder das Ende deiner Regex NICHT auf eine bestimmte Regex passt. In den meisten Fällen können Regexes mit Lookaheads und Lookbehinds auch mit Capture Groups umgeschrieben werden, also solltest du sie als Capture Groups zu schreiben versuchen und dich nur über Lookarounds informieren, wenn die anderen Optionen nicht funktionieren – denn Lookarounds sind CPU-intensive Operationen und auch etwas eingeschränkt (z.B. unterstützen sie die meisten Modifikatoren nicht).</p><p>Hier ist ein guter Ort, um mehr darüber zu erfahren:</p><h3 id="häufige-eigenheiten-und-validierung-neuer-regexes">Häufige Eigenheiten und Validierung neuer Regexes</h3><p>Eine häufige Sache, die man beachten sollte: Der <code>.</code> matcht in den meisten Sprachen standardmäßig <strong>nicht den Zeilenumbruch</strong>. Aber das lässt sich typischerweise mit einer Option aktivieren – in Ruby z.B. durch Angabe von <code>/m</code> am Ende, was für “make dot match newlines” steht.</p><p>Beachte auch, dass es in jeder Sprache verschiedene Zeichen gibt, die <strong>reserviert</strong> sind, je nachdem wie Strings in der jeweiligen Sprache funktionieren. In Ruby z.B. muss <code>/</code> mit <code>\\/</code> escaped werden, in Swift ist dieses Escaping nicht nötig, aber dafür muss man <code>{</code> und <code>}</code> mit <code>\\{</code> und <code>\\}</code> escapen. Diese Eigenheiten sind wichtig, wenn man Regexes aus anderen Sprachen kopiert und einfügt.</p><p>Generell empfehle ich beim Schreiben einer neuen Regex, eine Website oder ein Tool mit diesen 3 Features zu verwenden:</p><ol><li><p>Eine Option, einen Beispiel-String zum Abgleichen hinzuzufügen.</p></li><li><p>Ein Regex-Cheat-Sheet direkt auf dem Bildschirm zum Nachschlagen.</p></li><li><p>Ein Live-Matcher für die Regex, die du schreibst, basierend auf dem gegebenen Beispiel-String.</p></li></ol><p>Die Seite, die ich dafür verwende, ist diese (sie läuft mit Ruby im Hintergrund):</p><p><a href="https://rubular.com/">Rubular</a></p><h3 id="wie-kann-ich-regexes-heute-schon-in-meinen-projekten-nutzen">Wie kann ich Regexes <em>heute</em> schon in meinen Projekten nutzen?</h3><p>Du musst nicht warten, bis sich eine gute Gelegenheit ergibt, Regexes einzusetzen – du kannst deine Projekte einfach mit regulären Ausdrücken linten (inklusive Autokorrektur-Unterstützung) über AnyLint:</p><p><a class="sk-link-card sk-link-card-github" href="https://github.com/FlineDev/AnyLint?ref=fline.dev"><span class="sk-link-card-source"><svg viewBox="0 0 16 16" width="16" height="16" fill="currentColor"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/></svg><span>github.com</span></span><span class="sk-link-card-title">FlineDev / AnyLint</span><span class="sk-link-card-description">Lint anything by combining the power of scripts &amp; regular expressions</span></a></p>]]></content:encoded>
</item>
</channel>
</rss>