<?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>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/</link>
<language>en</language>
<atom:link href="https://fline.dev/feed.xml" rel="self" type="application/rss+xml"/>
<item>
<title>Pair Programming for Claude and Codex, Without the Copy-Paste</title>
<link>https://fline.dev/blog/tandemkit-pair-programming-for-ai-agents/</link>
<guid isPermaLink="true">https://fline.dev/blog/tandemkit-pair-programming-for-ai-agents/</guid>
<pubDate>Tue, 14 Apr 2026 00:00:00 +0000</pubDate>
<description><![CDATA[I'd been running Claude and Codex in parallel manually for months — copy-pasting between sessions, passing findings back and forth. TandemKit automates that workflow.]]></description>
<content:encoded><![CDATA[<p>For the past several months, my development workflow looked like this: I’d run Claude Code and Codex side by side. Same prompt, same goal, both investigating the codebase independently. When it was time to plan, Claude would draft the plan and I’d copy it over to Codex for review. Codex would spot things Claude had missed — edge cases, wrong assumptions, overlooked files. I’d relay the findings back, let Claude revise, copy the revision back to Codex, iterate until both agreed. Sometimes I’d stage changes from a previous round so Codex could see the diffs more clearly. Then I’d look over the result myself.</p><p>It worked really well. The two models catch different things. Claude is better at synthesis and communication. Codex reads more carefully — it traces code paths, checks edge cases, doesn’t declare done too early. They investigate independently, and that’s what makes the results better.</p><p>But the relay was miserable.</p><p>Links got stripped, formatting degraded, but those weren’t the real problem. The real problem was that every handoff still depended on me. Copy the output, paste it into the other session, wait, copy back, paste again. My attention stayed trapped inside the loop when it could’ve been somewhere more useful. That was the actual pain — a workflow that wouldn’t move without me.</p><p>I didn’t want to change the workflow itself. I just wanted to stop being the human message bus.</p><h2 id="the-harness-article-that-confirmed-everything">The Harness Article That Confirmed Everything</h2><p>Then I came across Anthropic’s <a href="https://www.anthropic.com/engineering/harness-design-long-running-apps"><em>Harness design for long-running application development</em></a>, mentioned in a YouTube video. I read the whole thing and it did two things at once.</p><p>First, it confirmed what I’d already been experiencing: having a separate session verify work independently — without the anchoring bias of having built it — produces better results. That was exactly what I’d been seeing with Claude and Codex.</p><p>Second, it turned that experience into a clearer system. I’d been focused mostly on the “different model” part. The article made the “separate evaluator” part much more explicit. Even another <em>Claude</em> session helps if it’s independent enough. The separation itself is load-bearing.</p><p>And honestly, it makes sense: if the original session thought the code was wrong, it wouldn’t have written it that way. It’s the same benefit pair programming has always had — a second pair of eyes catches what the author’s brain filters out. These LLMs aren’t that different from us here.</p><p>That combination — confirmed by experience, then clarified by research — was the moment it clicked. This wasn’t just some personal quirk in how I like to work. It seemed generally useful. So I turned the workflow into a proper plugin and called it <strong>TandemKit</strong>.</p><p><img src="/assets/images/blog/tandemkit-pair-programming-for-ai-agents/logo.webp" alt="TandemKit logo" loading="lazy" /></p><h2 id="the-coordination-problem-that-actually-needed-solving">The Coordination Problem That Actually Needed Solving</h2><p>My first design had four top-level sessions: Planner, Generator, and two Evaluators — one Claude, one Codex. They coordinated through plain text files. When one session was done, it would write a sync file, and the other would wait in the background until that file changed. It worked perfectly in Claude. But Codex just wouldn’t wait reliably. No matter what I tried — different file-watching approaches, different signaling mechanisms — Codex would either miss that it was its turn or skip the wait entirely.</p><p>I was on my fifth or sixth workaround for this when I discovered that OpenAI had just released an official Codex plugin for Claude Code — <a href="https://github.com/openai/codex-plugin-cc"><code>codex-plugin-cc</code></a>. Claude could now invoke Codex internally, see its response, and resume the same Codex session later. Exactly what I needed.</p><p>That didn’t make the Claude-Codex exchange invisible. Everything is still written to markdown files under <code>TandemKit/</code>, one file per round. The full mission history stays on disk — every investigation, every convergence exchange, every evaluation verdict.</p><p>That archive is useful, too. When something weird surfaces weeks later, git history doesn’t just give you a commit message — it gives you the whole conversation behind the commit, so you can actually dig into why a decision was made. It’s also great for improving the workflow itself: reading old missions is often the fastest way to notice that a rule belongs in <code>AGENTS.md</code> or a local skill.</p><p>What changed was the plumbing. Instead of juggling a fragile fourth terminal, TandemKit now calls one persistent Codex subagent on demand through <code>codex-plugin-cc</code> and resumes it whenever it needs another independent pass.</p><h2 id="what-a-mission-actually-is">What a Mission Actually Is</h2><p>I’ve been using AI agents every day for close to a year now, and a mission is the size of work I keep landing on: big enough to benefit from separate planning, generation, and evaluation, but small enough that the whole thing can still be implemented <em>and</em> fully verified within one set of sessions.</p><p>If the work is smaller than that — a quick one-file fix, a tiny refactor, a straightforward rename — I just use Claude Code directly. TandemKit’s multi-session loop uses more tokens and more ceremony than that kind of change deserves.</p><p>If the work is larger than that, I split it before starting. That’s where <a href="https://github.com/FlineDev/PlanKit">PlanKit</a> fits in naturally: it takes ideas through <strong>Ideas → Roadmap → Features → Missions</strong>. By the time something reaches “mission,” it’s not a vague feature bucket anymore — it’s already shaped into a session-sized piece of work.</p><h2 id="a-recent-mission-in-practice">A Recent Mission in Practice</h2><p>I wanted to add App Store Connect localization support to <a href="https://translatekit.app">TranslateKit</a>, my AI-powered app localization tool. The idea was simple: instead of manually managing app metadata in App Store Connect, developers could edit names, subtitles, descriptions, and keywords for all their localizations directly inside TranslateKit — synced via Apple’s API.</p><p>That was too big for one mission. So I split it in two: one mission for the App Store Connect API connection — JWT authentication, credential handling, fetching metadata, updating metadata — and one mission for the UI changes needed to surface all of that in the app. Data layer first, UX layer second.</p><p>During planning, Claude and Codex surfaced edge cases I hadn’t thought through. For example: what happens if the user wants to add a new language, but there’s no new App Store Connect version yet? Already-released versions can’t be edited, so the system has to decide — cache the changes locally, prompt the user to create a version first, or offer to auto-create one across platforms.</p><p>And if TandemKit creates a new version, what number should it use? Look at the history and guess? Ask the user to type one in? Offer common next-version options to pick from? Those are product decisions, not implementation details, and they belong in the spec before any code exists.</p><p>During evaluation of the API mission, Claude initially marked the feature as passing because the happy path worked and the existing-version tests were green. Codex traced the less obvious branch and found the real gap: when no editable App Store Connect version existed yet, the code created the new version record but never retried the localization write in the same flow. So the “new language” path looked successful while silently doing nothing until the user ran sync again — exactly the kind of code-path bug a second model catches.</p><p>The fix was small: retry the write after version creation, add tests for that branch. But without the second pass, it would’ve slipped through.</p><h2 id="three-sessions-autonomous-loop">Three Sessions, Autonomous Loop</h2><pre><code>YOU  -- planning
  |
  `--&gt; [1] Planner Session
             Claude ---------&gt; Codex (background)
              |    &lt;- findings - |
              `---- converge ----'
                         |
                      Spec.md  &lt;-- you approve before continuing

YOU  -- start both sessions, then step away
  |
  |--&gt; [2] Generator Session
  |          implements against Spec.md
  |          commits at milestones
  |
  `--&gt; [3] Evaluator Session
             Claude ---------&gt; Codex (background)
              |    &lt;- findings - |
              `---- converge ----'
                         |
                 FAIL -&gt; Generator fixes -&gt; loop
                 PASS -&gt; Review Briefing -&gt; you</code></pre><p>The Planner is the only interactive step. Once you approve <code>Spec.md</code>, the Generator and Evaluator run on their own until you get either a FAIL to fix or a PASS with a review briefing.</p><h2 id="how-claude-and-codex-actually-agree">How Claude and Codex Actually Agree</h2><p>The obvious move here is scoring: both models rate the work, average the scores, pass if it’s above some threshold. But scores hide failures. An 8/10 can still mean two critical criteria completely failed and everything else was fine.</p><p>So TandemKit evaluates criterion by criterion. Each finding gets two dimensions attached to it:</p><ul><li><p><strong>Agreement level</strong>: agreed, partially agreed, or disputed</p></li><li><p><strong>Severity level</strong>: <strong>HIGH</strong>, <strong>MEDIUM</strong>, or <strong>LOW</strong></p></li></ul><p>Claude and Codex investigate independently and write their findings to files. Then Claude reads what Codex found, creates a merged evaluation — keeping what it agrees with, explaining where it differs — and Codex reviews that merge. Any disagreement means re-reading the actual source files, not arguing from memory.</p><p>The convergence rule is simple: the loop continues until no <strong>HIGH</strong> or <strong>MEDIUM</strong> findings remain in the partially agreed or disputed buckets. If the same disagreement survives three rounds, TandemKit stops iterating and presents both positions to you.</p><p>Usually 2-4 rounds.</p><h2 id="why-not-agent-teams">Why Not Agent Teams</h2><p>You might wonder: doesn’t Claude Code have Agent Teams for exactly this? It does, but Agent Teams requires API billing — not included in Claude Max — and can’t integrate with Codex.</p><p>If Claude Max already costs you $100+/month, adding $20/month for ChatGPT Plus is a reasonable add — you get an independent second model, plus image generation for UI mockups, icons, and other visuals Claude doesn’t produce.</p><h2 id="everything-is-plain-text">Everything Is Plain Text</h2><p>Every investigation, every convergence exchange, every evaluation round is stored as readable files in your project:</p><pre><code>TandemKit/001-ConnectAPIClient/
├── Spec.md
├── Planner-Discussion/
│   ├── Claude-01.md    ← Claude's investigation
│   ├── Codex-01.md     ← Codex's independent findings
│   └── Claude-02.md    ← converged plan
├── Generator/
│   └── Round-01.md
└── Evaluator/
    ├── Round-01.md     ← FAIL: version created, localization write not retried
    └── Round-02.md     ← PASS</code></pre><p>Open the files and you can trace the full reasoning trail.</p><h2 id="getting-started">Getting Started</h2><p>The <a href="https://github.com/FlineDev/TandemKit">README on GitHub</a> has the full setup flow and shows how the sessions hand off to each other. If this sounds like the workflow you’ve been stitching together by hand, that’s the place to start:</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>Vapor&apos;s maximumActiveConnections Is Per Event Loop, Not Per Worker</title>
<link>https://fline.dev/snippets/vapor-maximum-active-connections/</link>
<guid isPermaLink="true">https://fline.dev/snippets/vapor-maximum-active-connections/</guid>
<pubDate>Mon, 29 Dec 2025 00:00:00 +0000</pubDate>
<description><![CDATA[A non-obvious Vapor configuration detail that can cause intermittent 500 errors when your actual connection count exceeds what you think you configured.]]></description>
<content:encoded><![CDATA[<h2 id="the-misleading-configuration">The Misleading Configuration</h2><p>When configuring connection pools in Vapor – whether for Redis, PostgreSQL, or other databases – you encounter a parameter called <code>maximumActiveConnections</code>. The natural assumption is that this sets the total maximum connections for your application. It does not. It sets the maximum per NIO <code>EventLoop</code>.</p><p>This distinction matters enormously in production.</p><h2 id="the-math-that-surprised-me">The Math That Surprised Me</h2><p>On a typical server, Swift NIO creates one <code>EventLoop</code> per CPU core. So the actual maximum connection count is:</p><pre><code>actual max = maximumActiveConnections * CPU cores * number of dynos/instances</code></pre><p>I had configured <code>maximumActiveConnections</code> to 8, thinking I was capping my Redis connections at a reasonable 16 across two dynos. The real number:</p><pre><code>8 (per event loop) * 8 (cores) * 2 (dynos) = 128 potential connections</code></pre><p>That is an order of magnitude more than intended, and it was exceeding my Redis provider’s connection limit during traffic spikes.</p><p><img src="/assets/images/snippets/vapor-maximum-active-connections/code-screenshot.webp" alt="Code showing the connection pool configuration" loading="lazy" /></p><h2 id="the-symptoms">The Symptoms</h2><p>The failure mode is particularly insidious: intermittent 500 errors that only appear under load. During normal traffic, you never hit the real connection limit, so everything works fine. During spikes, connections pile up across all event loops simultaneously, exceeding the provider’s limit. The errors look random and are difficult to reproduce locally because development machines typically have fewer cores.</p><h2 id="how-to-calculate-the-right-value">How to Calculate the Right Value</h2><p>Divide your provider’s connection limit by the total number of event loops across all instances:</p><pre><code class="language-swift">// Provider limit: 40 connections
// 2 dynos * 8 cores = 16 event loops total
// 40 / 16 = 2.5, round down to 2

app.redis.configuration = .init(
   pool: .init(maximumActiveConnections: 2)
)</code></pre><p>This conservative setting finally fixed the longstanding rare 500 errors in TranslateKit that had been puzzling me for months. Always check your cloud provider’s connection limits and do the multiplication before deploying.</p>]]></content:encoded>
</item>
<item>
<title>Why I&apos;m Not Using Xcode 26&apos;s AI Chat Integration (And What Could Change My Mind)</title>
<link>https://fline.dev/blog/why-im-not-using-xcode-26s-ai-chat-integration-and-what-could-change-my-mind/</link>
<guid isPermaLink="true">https://fline.dev/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 missing features keeping me from using Xcode's AI, plus my 5-release roadmap for Apple to catch up with Claude Code and Cursor.]]></description>
<content:encoded><![CDATA[<p>I’ve been a bit more conservative when it comes to AI tools, not having tried Cursor when it was being hyped as I was cautious with giving AI access to an entire project. I was basically waiting for Apple’s more privacy-focused solution. But when that turned out to just be ChatGPT (or other models), I figured I could also just try third-party tools. So I’ve been testing Xcode 26 (beta 5) against both Cursor and Claude Code since WWDC to see which can make me more efficient.</p><p>And I’m genuinely glad Apple pivoted to server-based LLMs instead of going local-only. Having seen by now how amazing Claude Code can be with the right context engineering, I know a local-only solution would have taken years to reach usable quality. Apple made the right call here to pivot from the original Swift Assist idea, but they clearly didn’t have time to build a complete solution for the initial release. As someone loyal to the Apple ecosystem for development, it’s frustrating to see such obvious gaps in what could be an incredible tool. So for the first time in my 14 years developer career, I’m no longer using Xcode as my daily driver.</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 summarizes what context it has thanks to Context Engineering.</em></p><p>I ended up running Claude Code in Cursor’s terminal instead – getting Cursor’s editor awareness with Claude Code’s superior tools like web search, planning mode, and the generous 5-hour usage window. I’ve been fully embracing the chat interface and trying to teach AI to understand how to write good code through extended context engineering (guidelines, reference documentation). Meanwhile, Xcode’s AI integration – while promising due to its native IDE position – lacks fundamental features that make this kind of AI-driven development productive.</p><p>Here’s what’s keeping me from making the switch back to Xcode.</p><h2 id="the-7-missing-features-in-xcode-ai">The 7 Missing Features in Xcode AI</h2><p><strong>1. Request queuing</strong> was the first limitation I noticed in Xcode right away. When I’m developing, thoughts and questions come fast. Having to wait for each response breaks my rhythm completely. Thinking ahead while waiting is one of the biggest time savers when working with AI. Both Cursor and Claude Code let me queue requests seamlessly, keeping me in the flow. Xcode just blocks all input.</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 blocks new chat input while processing the previous one.</em></p><p><strong>2. Context engineering support</strong> is where Xcode completely falls short. There’s no support for context files like <code>.cursorrules</code> or <code>CLAUDE.md</code>. Without automatic context loading, all the work I’ve done on context engineering – teaching the AI my coding standards, architectural patterns, error handling approaches, and more – simply isn’t possible in Xcode. I have to repeatedly explain my guidelines in every conversation. Claude Code and Cursor both automatically load my lead guideline, then understanding when to read which more detailed guideline. This transforms AI from a generic code generator into an actually useful assistant. Not in Xcode.</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>The Coding Assistant telling me it has no way to teach it about my project or guidelines.</em></p><p><strong>3. Build validation</strong> shows Xcode wasn’t designed for AI-driven development. The AI can’t validate its own code changes by running builds or even access build output when I run them. It can’t even read console logs when I explicitly ask it to. Sure, Xcode lets you select an error after building and ask the AI to fix it – but that’s designed for a different workflow. It’s only useful when you manually write code and make a mistake, which rarely happens. But when AI writes code for me? Build errors occur constantly. Having to build myself and then manually press to fix it is super annoying. The AI should just be able to build and look at the errors itself. With Claude Code, I simply let it run <code>xcodebuild</code> and the AI sees all errors immediately. It does ask me for permission, but when granted, it can iterate, fix, and rebuild until everything compiles – no manual intervention needed.</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>The Coding Assistant telling me it can’t read the console output.</em></p><p><strong>4. Git integration</strong> is completely absent – no history searching, no comparing versions, no automated commits following my guidelines. I can’t tell it to compare my current code with a previous version to update documentation files based on recent changes, or bring back some working code from previous commits. Claude Code can search my git history, bring back working code from previous commits, and create properly formatted commits by analyzing the actual changes and finding a good message that follows my commit guidelines. It can even help update documentation by comparing what changed since the last version.</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>The Coding Assistant telling me it can’t access git history.</em></p><p><strong>5. Terminal and CLI access</strong> is the biggest gap. No command line access means no <code>swift-format</code>, no custom scripts, no automation. As an indie developer juggling multiple responsibilities, this is a serious limitation. I can’t teach it to run a code formatter before I make a commit, can’t have it execute my test suites, can’t run any of the custom scripts that streamline my development process. Claude Code runs any terminal command I need. It formats code before commits, runs my test suites, executes deployment scripts, manages dependencies, and handles my entire automation workflow. One terminal tool gives access to everything – <code>git</code>, <code>xcodebuild</code>, package managers, you name it. Modern AI is good in this!</p><blockquote><p>❇️ If Apple just added terminal access alone, above points 3 and 4 would be resolved as well! Of course giving full command line access is risky. But Claude Code solves this nicely by asking for permission the first time and supporting an allow &amp; deny list in a <a href="https://docs.anthropic.com/en/docs/claude-code/settings">simple config file</a>.</p></blockquote><p><strong>6. Project file limitations</strong> show how Xcode wasn’t designed for modern AI context engineering workflows. Many projects span multiple repositories – like <a href="https://translatekit.app/">TranslateKit</a> with its app, server, and <a href="https://github.com/FlineDev/TranslateKitSDK">open source package</a> components. My context engineering guidelines live in the parent folder containing no Xcode project, yet I need AI access to them across all components. When I drag a folder to Xcode to open it in the editor, I just get an error. It can only open single text files, Xcode projects, or Swift Package manifest files – but not arbitrary folders. It also hides dotfiles, so I can’t see or edit things like GitHub Actions workflows. Cursor on the other hand opens any folder, shows hidden files, and works across my entire multi-repo development environment. Xcode should support this, too!</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>The error dialog when I try to open an arbitrary folder in Xcode.</em></p><p><strong>7. Web search and documentation access</strong> are also missing entirely. The AI can’t even search within Xcode’s own downloaded documentation files. When I need current information or API references, I’m on my own. Claude Code has built-in web search. When I ask about the latest Swift evolution proposals or new APIs introduced at WWDC25, it just finds the answer. Not in Xcode 26.</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>Asked about the new frameworks introduced by Apple in 2025, it 100% hallucinates.</em></p><p>These seven limitations add up to a fundamental problem: Xcode’s AI feels like a tech demo rather than a productivity tool. Each missing piece forces me back to manual workflows that Claude Code handles seamlessly.</p><h2 id="my-roadmap-for-apple-5-release-milestones">My Roadmap for Apple: 5 Release Milestones</h2><p>Here’s how Apple could address these gaps and bring me back to Xcode in 2026:</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 (October 2025):</strong>
Adds request queuing and context file support (e.g. <code>Xcode.md</code>). These features should be easy to implement and have no risk of unnecessary side effects.</p><p><strong>Xcode 26.2 (December 2025):</strong>
Specific tool integrations for <code>git</code>, <code>xcodebuild</code>, and <code>swift-format</code>. Since these are all built-in to Xcode or Apple technologies, I can see Apple engineers use their existing experience to provide these tools to AI with safe conservative actions.</p><p><strong>Xcode 26.3 (March 2026):</strong>
Web search, latest documentation integration, and opening arbitrary folders with hidden file access. These integrations are all low-risk, but take time to get right.</p><p><strong>Xcode 26.4 (May 2026):</strong>
Full terminal access with command-specific allow/deny lists.</p><p>I understand Apple’s security concerns, but developers can handle the responsibility. Trust us with the tools we need, Apple, please!</p><p><strong>Xcode 27 (September 2026, announced at WWDC 26):</strong>
Features “only Apple can do”, such as deep Simulator integration (imagine AI navigating your app to test if its changes worked as expected), or SwiftUI preview access (so AI can see what it’s UI code looks like). Maybe even an “app builder” mode that makes SwiftUI a secondary artifact, bringing app development to a whole new audience. This is where Apple could surprise us with something we didn’t see coming. Not just “catching up” but truly surpassing the competition.</p><h2 id="the-bottom-line">The Bottom Line</h2><p>I’ve always loved Xcode and genuinely want to use it exclusively again. Apple has unique advantages no competitor can match – seamless IDE integration, Simulator control, SwiftUI preview capabilities. But right now, Claude Code is dramatically more capable with a 5x productivity difference. Tasks that take hours in Xcode happen in minutes with Claude Code, mainly because it doesn’t depend on me for simple tasks like pressing “build”.</p><p>Apple needs to move fast. Every day that passes, more developers are establishing deep workflows with other tools. I’ll be watching each point release eagerly, and the moment Xcode’s AI becomes competitive, I’ll be first in line to return. Until then, I’ll keep Claude Code open in my terminal, dreaming of the day when Xcode will truly blow my mind again and make the shift to becoming an AI-first IDE.</p><p><em>What’s your experience with Xcode’s AI integration? Have you found workarounds for these limitations, or are you also using alternatives?</em></p>]]></content:encoded>
</item>
<item>
<title>Top 10 Developer Tools Apple introduced at WWDC25</title>
<link>https://fline.dev/blog/top-10-developer-tools-apple-introduced-at-wwdc25/</link>
<guid isPermaLink="true">https://fline.dev/blog/top-10-developer-tools-apple-introduced-at-wwdc25/</guid>
<pubDate>Mon, 23 Jun 2025 00:00:00 +0000</pubDate>
<description><![CDATA[Explore breakthrough features like Foundation Models bringing on-device AI, ChatGPT integration in Xcode, AlarmKit enabling true alarm apps, and major improvements to visionOS spatial experiences.]]></description>
<content:encoded><![CDATA[<p>WWDC25 has wrapped up, and after watching over 50 sessions and participating in labs, I’ve compiled the most exciting developer-focused announcements from this year’s conference. Here are the 10 standout features that will transform how we build apps for Apple platforms.</p><h2 id="1-foundation-models-on-device-ai">1. Foundation Models: On-Device AI</h2><p>The biggest game-changer this year is <strong>Foundation Models</strong> – Apple’s on-device AI framework that brings powerful 3-billion parameter models directly to your apps. What makes this special?</p><ul><li><p><strong>Privacy-first</strong>: Everything runs on-device with no server delays</p></li><li><p><strong>Structured output</strong>: Use the <code>@Generable</code> macro to guarantee typed responses</p></li><li><p><strong>Tool calling</strong>: AI can interact with your app’s functions automatically</p></li><li><p><strong>Partial results</strong>: Update your UI as responses generate in real-time</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="related-sessions">Related 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-gets-chatgpt-integration">2. Xcode Gets ChatGPT Integration</h2><p>Apple surprised everyone by integrating <strong>ChatGPT directly into Xcode</strong> instead of their promised Swift Assist. This brings:</p><ul><li><p>Native AI assistance with code generation and documentation</p></li><li><p>Support for multiple AI providers (ChatGPT, Claude, local models)</p></li><li><p>Git-like history for AI changes to revert changes easily</p></li><li><p>Context-aware code suggestions (like document, explain, etc.)</p></li></ul><p>The <code>#Playground</code> <strong>macro</strong> also transforms debugging – create SwiftUI preview-style playgrounds for any data type, not just views. Great for 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="related-sessions">Related 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-finally-arrives">3. SwiftUI WebView Finally Arrives</h2><p>After years of requests, <strong>WebView is now native in SwiftUI</strong>:</p><pre><code class="language-swift">WebView(url: URL(string: &quot;https://swift.org&quot;)!)</code></pre><p>Combined with the new <strong>WebPage</strong> type for advanced control, including JavaScript execution and scroll synchronization. This opens up hybrid app experiences that were previously complex to implement – and required UIKit/AppKit bridging.</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="related-sessions">Related 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-third-party-alarm-apps">4. AlarmKit: Third-Party Alarm Apps</h2><p><strong>AlarmKit</strong> breaks through focus modes and system restrictions, enabling:</p><ul><li><p>True alarm apps that can wake users</p></li><li><p>Live Activities integration (required) for snooze functionality</p></li><li><p>Custom alarm sounds and experiences possible</p></li><li><p>Perfect for timers, wake up alarms, and prayer/medication reminder 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="related-session">Related 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-overhaul">5. App Store Connect Analytics Overhaul</h2><p>App Store Connect gets a major upgrade:</p><ul><li><p><strong>Monthly Recurring Revenue (MRR)</strong> finally available</p></li><li><p>New Analytics APIs for third-party tools</p></li><li><p><strong>Offer codes for non-subscription products</strong> (on all platforms)</p></li><li><p>Improved navigation with analytics moved into individual apps</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="related-sessions">Related 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-spatial-revolution">6. visionOS Spatial Revolution</h2><p>Major visionOS improvements include:</p><ul><li><p><strong>Persistence</strong>: Widgets, windows, and volumes stay pinned between sessions</p></li><li><p><strong>Nearby sharing</strong>: Multi-user shared experiences in the same room</p></li><li><p><strong>APMP support</strong>: 180°, 360°, and Wide FOV spatial video support</p></li><li><p>Enhanced SwiftUI 3D layout tools making development easier</p></li><li><p><strong>Swift Charts 3D</strong>: Bring dimensional data visualization to spatial 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="related-sessions">Related 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-made-approachable">7. Swift 6 Concurrency Made Approachable</h2><p><strong>Approachable concurrency</strong> solves Swift 6 adoption issues:</p><ul><li><p>Default <code>@MainActor</code> isolation reduces warnings</p></li><li><p>Gradual opt-in to concurrency with <code>@concurrent</code></p></li><li><p>Finally makes Swift 6 migration realistic for existing app projects</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="related-sessions">Related 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-beyond-bluetooth">8. Wi-Fi Aware: Beyond Bluetooth</h2><p><strong>Wi-Fi Aware</strong> enables experiences like AirPlay/AirDrop:</p><ul><li><p>High-performance local communication</p></li><li><p>Greater range than Bluetooth, cross-platform standard</p></li><li><p>Support for more simultaneous connections (AirPods-like)</p></li><li><p>Perfect for media streaming and multiplayer experiences</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="related-session">Related 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-enhancements">9. String Catalog Enhancements</h2><p>Localization gets more accurate &amp; more flexible:</p><ul><li><p><strong>AI-generated comments</strong> for translation context</p></li><li><p><strong>String symbols</strong> with auto-completion (for manual strings)</p></li><li><p><strong>Multi-select</strong> operations for bulk updates (including new refactor)</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="related-sessions">Related 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>The new <strong>Icon Composer</strong> app creates:</p><ul><li><p>Layered icons with up to 4 layers with built-in effects</p></li><li><p>Preview all icon styles (including controversial “clear” style)</p></li><li><p>Unified <code>.icon</code> file format to drop to Xcode</p></li><li><p>Can also export flat images for marketing purposes</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="related-sessions">Related 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="watch-the-full-breakdown">Watch the Full Breakdown</h2><p>This article covers only a small fraction of new APIs, but there’s so much more to explore in my comprehensive video breakdown. I go through everything new that’s interesting and share my perspective on what will matter most for your apps:</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>What are you most excited to implement in your apps? Let me know in the comments on YouTube or on other socials (links below)!</em></p>]]></content:encoded>
</item>
<item>
<title>Apple Implements Feedback Requests -- Filing Reports Works</title>
<link>https://fline.dev/snippets/apple-implements-feedback-requests/</link>
<guid isPermaLink="true">https://fline.dev/snippets/apple-implements-feedback-requests/</guid>
<pubDate>Tue, 10 Jun 2025 00:00:00 +0000</pubDate>
<description><![CDATA[A personal experience of having a Feedback Assistant request implemented in Xcode 26, and tips for writing effective feedback reports.]]></description>
<content:encoded><![CDATA[<h2 id="your-feedback-actually-gets-read">Your Feedback Actually Gets Read</h2><p>There is a common belief in the developer community that filing Feedback Assistant reports (the successor to Radar) is pointless – that reports disappear into a void and nothing ever happens. I had a feature request implemented in Xcode 26, which proves otherwise.</p><p><img src="/assets/images/snippets/apple-implements-feedback-requests/feedback-implemented.webp" alt="Screenshot showing the implemented feedback request" loading="lazy" /></p><p>Seeing a feature you requested show up in a keynote or release notes is a satisfying validation. But more importantly, it demonstrates that Apple’s engineering teams do review and prioritize community feedback, even when they never respond to the report directly.</p><h2 id="tips-for-writing-effective-feedback">Tips for Writing Effective Feedback</h2><p>Not all reports are created equal. Here is what I have found increases the chances of a report being actionable:</p><p><strong>Be specific about the problem.</strong> “Xcode is slow” is not useful. “Xcode’s SwiftUI preview takes 12 seconds to refresh when the file contains more than 3 <code>#Preview</code> blocks” gives engineers something to investigate.</p><p><strong>Include a sample project.</strong> A minimal reproduction project is the single most valuable thing you can attach. If an engineer can build and run your project to see the issue, you have removed the biggest barrier to them investigating it.</p><p><strong>Describe the use case, not just the solution.</strong> Instead of “add a button that does X,” explain why you need it: “When working with large SPM graphs, I need to quickly identify which target a file belongs to because…” This gives the team context to design the right solution, which might be different from what you imagined.</p><p><strong>File during beta season.</strong> The weeks after WWDC are when Apple’s teams are most actively collecting feedback on new features. Reports filed during this window get significantly more attention than those filed in the middle of a release cycle.</p><h2 id="make-it-a-habit">Make It a Habit</h2><p>Every WWDC beta season, set aside time to test your apps against the new OS and Xcode betas. File reports for every issue and every missing feature. Most will not get a response, but some will quietly influence future releases.</p>]]></content:encoded>
</item>
<item>
<title>Making Swift Error Messages Human-Friendly—Together</title>
<link>https://fline.dev/blog/making-swift-error-messages-human-friendly-together/</link>
<guid isPermaLink="true">https://fline.dev/blog/making-swift-error-messages-human-friendly-together/</guid>
<pubDate>Mon, 12 May 2025 00:00:00 +0000</pubDate>
<description><![CDATA[Swift error messages can be cryptic, but as a community, we can make them clearer. Help others (and your future self) by contributing better explanations.]]></description>
<content:encoded><![CDATA[<p>Despite Swift’s elegant design, system error messages are often cryptic and unhelpful. Instead of just complaining, I created <a href="https://github.com/FlineDev/ErrorKit?ref=fline.dev">ErrorKit</a>, a tool that maps these messages to human-friendly descriptions. But mapping Apple’s entire ecosystem is too big a task for one person—this needs to be a community effort.</p><p>In this post, I’ll share the foundation I’ve built for enhanced error descriptions and invite you to join me in creating a comprehensive dictionary of user-friendly error messages for Swift developers.</p><h2 id="the-problem-with-system-error-messages">The Problem with System Error Messages</h2><p>Let’s look at some real examples of unhelpful system error messages:</p><pre><code class="language-swift">// File system error
&quot;The file couldn't be opened because it doesn't exist.&quot;
// Better message: &quot;The file 'report.pdf' could not be found. Please verify the file name and location.&quot;

// Core Data error
&quot;The operation couldn't be completed. (Cocoa error 133000.)&quot;
// Better message: &quot;The database has a validation error. One or more required fields are empty or have invalid values.&quot;</code></pre><p>These default messages have several problems:</p><ol><li><p>They’re often <strong>too technical</strong> for end users</p></li><li><p>They lack <strong>context</strong> about what was being attempted</p></li><li><p>They rarely offer <strong>suggestions</strong> for how to resolve the issue</p></li><li><p>They’re sometimes just plain <strong>wrong</strong> or misleading</p></li><li><p>They may expose <strong>implementation details</strong> that users shouldn’t see</p></li></ol><p>Apple has thousands of error codes across dozens of frameworks, and many of these have existed for decades. While they’ve improved newer APIs, many older frameworks still return messages designed for developers, not users.</p><h2 id="the-solution-community-curated-error-mappings">The Solution: Community-Curated Error Mappings</h2><p>ErrorKit provides a function called <code>userFriendlyMessage(for:)</code> that maps system errors to more helpful messages:</p><pre><code class="language-swift">do {
    let data = try Data(contentsOf: url)
    // Process data...
} catch {
    // Instead of showing the default message
    // showAlert(error.localizedDescription)

    // Show an enhanced message
    showAlert(ErrorKit.userFriendlyMessage(for: error))
}</code></pre><p>Behind the scenes, this function analyzes the error and returns a better description based on a growing set of known error types:</p><pre><code class="language-swift">// Example from ErrorKit's implementation
enum FoundationErrorMapper: ErrorMapper {
    static func userFriendlyMessage(for error: Error) -&gt; String? {
        let nsError = error as NSError

        // URL loading errors
        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;)
            // Many more cases...
            }
        }

        // File system errors
        if nsError.domain == NSCocoaErrorDomain {
            switch nsError.code {
            case NSFileNoSuchFileError:
                return String(localized: &quot;The file could not be found.&quot;)
            // Many more cases...
            }
        }

        // More domains and error codes...

        return nil // Fall back to default handling
    }
}</code></pre><h2 id="current-coverage-and-examples"><strong>Current Coverage and Examples</strong></h2><p>ErrorKit includes mappings for common errors across key Apple frameworks:</p><h3 id="foundation">Foundation</h3><ul><li><p>Network: <code>NSURLErrorNotConnectedToInternet</code> → “Your device isn’t connected to the internet. Please check your connection and try again.”</p></li><li><p>File System: <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>Validation: <code>NSValidationErrorMinimum</code> → “The database failed validation. Please check your inputs and try again.”</p></li><li><p>Store Management: <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>Directions: <code>MKErrorDirectionsNotFound</code> → “No directions found for the requested route.”</p></li><li><p>Location: <code>MKErrorLocationUnknown</code> → “Current location unavailable. Please check location permissions.”</p></li></ul><h2 id="why-we-need-a-community-response">Why We Need a Community Response</h2><p>In an ideal world, Apple would fix all its confusing error messages. And to be fair, they’ve improved things—newer frameworks like SwiftUI are much clearer. But decades of legacy APIs still return cryptic, technical messages that may never be touched again.</p><p>This is where our community can make a real impact. No single developer has seen every obscure error—but together, we’ve seen most of them. By sharing what we’ve already figured out and rewriting these messages in plain language, we can create a resource that saves everyone time and improves the experience for developers and users alike.</p><h2 id="how-you-can-contribute">How You Can Contribute</h2><p>If you’ve rewritten a system error message to make it more helpful, you’re already doing the work—now you can share it. Here are the 3 general steps:</p><ol><li><p><strong>Spot a bad message</strong><br>Note the error domain and code, the operation that triggered it, and any useful info from the <code>userInfo</code> dictionary.</p></li><li><p><strong>Write a better version</strong><br>Use plain language. Be specific. Offer a helpful next step when it makes sense.</p></li><li><p><strong>Submit it on GitHub</strong><br>Open a pull request or issue on the <a href="https://github.com/FlineDev/ErrorKit?ref=fline.dev">ErrorKit repo</a> with your improved message, the original context, and any notes.</p></li></ol><p>Even small contributions can help thousands of developers and improve the user experience for millions!</p><h2 id="beyond-apple-frameworks-mapping-any-librarys-errors">Beyond Apple Frameworks: Mapping Any Library’s Errors</h2><p>While Apple frameworks are the primary focus, ErrorKit’s error mapping system works with any error type. The <code>ErrorMapper</code> protocol allows developers to create custom mappings for third-party libraries:</p><pre><code class="language-swift">// Example mapper for Alamofire networking errors
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
        }
    }
}

// Register for immediate use
ErrorKit.registerMapper(AlamofireErrorMapper.self)</code></pre><p>This extensibility opens up possibilities for:</p><ul><li><p>Adding custom mappings in your app for popular libraries like Alamofire</p></li><li><p>Creating mapper packages for closed-source SDKs like Stripe, Admob, etc.</p></li><li><p>Sharing error mappings within teams or organizations</p></li></ul><h2 id="conclusion">Conclusion</h2><p>Error messages significantly impact user experience yet often go overlooked in development. ErrorKit’s <code>userFriendlyMessage(for:)</code> function creates a framework for transforming this through community collaboration—one where developers pool their knowledge to make the entire Swift ecosystem more user-friendly.</p><p>If you’ve decoded cryptic error messages, your experience is valuable. By contributing to ErrorKit, you can fix these issues not just for yourself, but for every Swift developer who faces the same challenges. Check out ErrorKit and share your solutions to build this community resource:</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>Have you encountered cryptic error messages in Apple frameworks? Have you made them more helpful for yourself? Let me know on socials (links below)!</p><h3 id="previous-articles-in-this-series">Previous articles in this series:</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>Better Error Reporting in Swift Apps: Automatic Logs + Analytics</title>
<link>https://fline.dev/blog/better-error-reporting-in-swift-apps-automatic-logs-analytics/</link>
<guid isPermaLink="true">https://fline.dev/blog/better-error-reporting-in-swift-apps-automatic-logs-analytics/</guid>
<pubDate>Mon, 05 May 2025 00:00:00 +0000</pubDate>
<description><![CDATA[Tired of vague bug reports like "it doesn't work"? In this post, you'll learn how to collect automatic logs and track real-world errors in your Swift apps—with just a few lines of code.]]></description>
<content:encoded><![CDATA[<p>“It doesn’t work.”</p><p>If you’ve ever supported an iOS app, you’ve received this frustratingly vague user feedback. No steps to reproduce, no error message, no context—just the dreaded “doesn’t work” report that leaves you with more questions than answers.</p><p>Even the most detail-oriented users rarely know what information you need to diagnose issues. And when they do try to help, they might not have the technical knowledge to provide the right details. This disconnect creates a frustrating experience for everyone involved.</p><p>In this post, I’ll share two practical approaches I’ve implemented in <a href="https://github.com/FlineDev/ErrorKit">ErrorKit</a> to bridge this gap: a simple feedback button that automatically collects diagnostic logs, and a structured approach to error analytics that helps you identify patterns even without direct user reports.</p><h2 id="the-missing-context-problem">The Missing Context Problem</h2><p>When users encounter issues, several challenges make diagnosis difficult:</p><ol><li><p>They don’t know <strong>what information</strong> you need</p></li><li><p>They can’t easily access <strong>system logs</strong></p></li><li><p>They struggle to <strong>remember</strong> and articulate <strong>exact steps</strong></p></li><li><p>Complex issues may involve <strong>multiple components</strong></p></li><li><p>Intermittent issues are <strong>hard to reproduce</strong> on demand</p></li></ol><p>Without proper context, debugging becomes a guessing game. You might spend hours trying to reproduce an issue that could be solved in minutes with the right information.</p><h2 id="solution-1-feedback-button-with-logs-attached">Solution 1: Feedback Button with Logs Attached</h2><p>The first solution is to make it ridiculously easy for users to send you complete information. ErrorKit provides a SwiftUI modifier that adds a mail composer with automatic log collection:</p><pre><code class="language-swift">struct ContentView: View {
    @State private var showMailComposer = false

    var body: some View {
        VStack {
            // Your app content

            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>This creates a simple “Report a Problem” button that:</p><ol><li><p>Opens a <strong>pre-filled</strong> email composer</p></li><li><p>Includes <strong>device and app information</strong></p></li><li><p>Automatically attaches recent <strong>system logs</strong></p></li><li><p>Provides <strong>space for the user</strong> to describe the issue</p></li></ol><p>The log attachment is the secret sauce here. When the user taps this button after encountering an issue, you get a comprehensive picture of what was happening in and around your app when the problem occurred.</p><h2 id="leveraging-apples-unified-logging-system">Leveraging Apple’s Unified Logging System</h2><p>ErrorKit uses Apple’s unified logging system (<code>OSLog</code>/<code>Logger</code>) to collect diagnostic information. If you’re not already using structured logging, here’s a quick intro:</p><pre><code class="language-swift">import OSLog

// Create loggers
let logger = Logger()
// or with subsystem and category
let networkLogger = Logger(subsystem: &quot;com.yourapp&quot;, category: &quot;networking&quot;)

// Log at appropriate levels
logger.debug(&quot;Detailed connection info&quot;)      // Development debugging
logger.info(&quot;User tapped submit button&quot;)      // General information
logger.notice(&quot;Profile successfully loaded&quot;)   // Important events
logger.error(&quot;Failed to load user data&quot;)      // Errors that should be fixed
logger.fault(&quot;Database corruption detected&quot;)   // System failures

// Format values and control privacy
logger.info(&quot;User \(userId, privacy: .private) logged in from \(ipAddress, privacy: .public)&quot;)</code></pre><p>The unified logging system provides several advantages over <code>print()</code> statements:</p><ul><li><p>Log levels for filtering information</p></li><li><p>Privacy controls for sensitive data</p></li><li><p>Efficient performance with minimal overhead</p></li><li><p>Persistence across app launches</p></li></ul><h2 id="comprehensive-log-collection">Comprehensive Log Collection</h2><p>A key advantage of ErrorKit’s approach is that it captures not just your app’s logs, but also relevant logs from:</p><ol><li><p><strong>Third-party frameworks</strong> that use Apple’s unified logging system</p></li><li><p><strong>System components</strong> your app interacts with (networking, file system, etc.)</p></li><li><p><strong>Background processes</strong> related to your app’s functionality</p></li></ol><p>This gives you a complete picture of what was happening in and around your app when the issue occurred—not just the logs you explicitly added.</p><h2 id="controlling-log-collection">Controlling Log Collection</h2><p>You can customize log collection to balance detail and privacy:</p><pre><code class="language-swift">// Collect logs from last 30 minutes with notice level or higher (default)
try ErrorKit.logAttachment(ofLast: .minutes(30), minLevel: .notice)

// Collect logs from last hour with error level or higher (less verbose)
try ErrorKit.logAttachment(ofLast: .hours(1), minLevel: .error)

// Collect logs from last 5 minutes with debug level (very detailed)
try ErrorKit.logAttachment(ofLast: .minutes(5), minLevel: .debug)</code></pre><p>The <code>minLevel</code> parameter filters logs by importance:</p><ul><li><p><code>.debug</code>: All logs (very verbose)</p></li><li><p><code>.info</code>: Informational logs and above</p></li><li><p><code>.notice</code>: Notable events (default)</p></li><li><p><code>.error</code>: Only errors and faults</p></li><li><p><code>.fault</code>: Only critical errors</p></li></ul><p>This gives you control over how much information you collect while still providing the context you need for diagnosis.</p><h2 id="alternative-methods-for-more-control">Alternative Methods for More Control</h2><p>If you need more control over log handling, ErrorKit offers additional approaches:</p><h3 id="getting-log-data-directly">Getting Log Data Directly</h3><p>For sending logs to your own backend or processing them in-app, use <code>loggedData</code>:</p><pre><code class="language-swift">let logData = try ErrorKit.loggedData(
    ofLast: .minutes(10),
    minLevel: .notice
)

// Use the data with your custom reporting system
analyticsService.sendLogs(data: logData)</code></pre><h3 id="exporting-to-a-temporary-file">Exporting to a Temporary File</h3><p>For sharing logs via other mechanisms, use <code>exportLogFile</code>:</p><pre><code class="language-swift">let logFileURL = try ErrorKit.exportLogFile(
    ofLast: .hours(1),
    minLevel: .error
)

// Share the log file
let activityVC = UIActivityViewController(
    activityItems: [logFileURL],
    applicationActivities: nil
)
present(activityVC, animated: true)</code></pre><h2 id="solution-2-smart-error-analytics-with-grouping-ids">Solution 2: Smart Error Analytics with Grouping IDs</h2><p>While the feedback button helps users report issues they notice, many problems go unreported. Users might encounter an error, shrug, and try again—never telling you about it. That’s where error analytics comes in.</p><p>ErrorKit provides tools to automatically track errors and group them intelligently:</p><pre><code class="language-swift">func handleError(_ error: Error) {
    // Get a stable ID that ignores dynamic parameters
    let groupID = ErrorKit.groupingID(for: error)

    // Get the full error chain description
    let errorDetails = ErrorKit.errorChainDescription(for: error)

    // Send to your analytics system
    Analytics.track(
        event: &quot;error_occurred&quot;,
        properties: [
            &quot;error_group&quot;: groupID,
            &quot;error_details&quot;: errorDetails,
            &quot;user_id&quot;: currentUser.id
        ]
    )

    // Show appropriate UI to the user
    showErrorAlert(message: ErrorKit.userFriendlyMessage(for: error))
}</code></pre><p><em>Sample global error handling function to add to your app.</em></p><p>The magic here is in the <code>groupingID(for:)</code> function. It generates a stable identifier based on the error’s type structure and enum cases, ignoring dynamic parameters and localized messages.</p><p>This means that errors with the same underlying cause will have the same grouping ID, even if specific details (like file paths or user IDs) differ:</p><pre><code>// Both generate the same 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>This approach provides several benefits:</p><ol><li><p><strong>Identify common issues</strong>: See which errors occur most frequently</p></li><li><p><strong>Prioritize fixes</strong>: Focus on high-impact problems first</p></li><li><p><strong>Track resolution</strong>: Monitor if error rates decrease after fixes</p></li><li><p><strong>Detect new issues</strong>: Quickly identify new error patterns after releases</p></li><li><p><strong>Correlate with user segments</strong>: See if some errors affect specific users</p></li></ol><h2 id="combine-both-approaches-for-max-insight">Combine Both Approaches for Max Insight</h2><p>A powerful approach is to combine automatic analytics with user-initiated feedback, so you might want to do something like this:</p><pre><code class="language-swift">func handleError(_ error: Error) {
    // Always track for analytics
    trackErrorAnalytics(error)

    // For serious or unexpected errors, prompt for feedback
    if isSerious(error) {
        showErrorAlert(
            message: ErrorKit.userFriendlyMessage(for: error),
            feedbackOption: true
        )
    } else {
        // For minor issues, just show a message
        showErrorAlert(message: ErrorKit.userFriendlyMessage(for: error))
    }
}

func showErrorAlert(message: String, feedbackOption: Bool = false) {
    // Implementation of an alert that optionally includes a
    // &quot;Send Feedback&quot; button that opens the mail composer with logs
}</code></pre><p>This creates a comprehensive system where:</p><ol><li><p>All errors are tracked for analytics, giving you broad patterns</p></li><li><p>Serious errors prompt users for detailed feedback with logs</p></li><li><p>Users can always initiate feedback for issues you might not track</p></li></ol><h2 id="best-practices-for-logging">Best Practices for Logging</h2><p>To maximize the value of log collection, consider these best practices:</p><h3 id="1-structure-logs-for-context">1. Structure Logs for Context</h3><p>Provide enough context in your logs to understand what was happening:</p><pre><code class="language-swift">// Instead of:
Logger().error(&quot;Failed to load&quot;)

// Use:
Logger().error(&quot;Failed to load document \(documentId): \(ErrorKit.errorChainDescription(for: error))&quot;)</code></pre><h3 id="2-choose-appropriate-log-levels">2. Choose Appropriate Log Levels</h3><p>Use log levels strategically to control verbosity:</p><ul><li><p><code>.debug</code> for developer details only needed during development</p></li><li><p><code>.info</code> for tracking normal app flow</p></li><li><p><code>.notice</code> for important events users would care about</p></li><li><p><code>.error</code> for problems that need fixing but don’t prevent core functionality</p></li><li><p><code>.fault</code> for critical issues that break core functionality</p></li></ul><h3 id="3-protect-sensitive-information">3. Protect Sensitive Information</h3><p>Use privacy modifiers to protect user data:</p><pre><code class="language-swift">Logger().info(&quot;Processing payment for user \(userId, privacy: .private)&quot;)</code></pre><h3 id="4-log-key-user-actions">4. Log Key User Actions</h3><p>Create breadcrumbs of user activity to understand the path to errors:</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-log-start-and-completion-of-important-operations">5. Log Start and Completion of Important Operations</h3><p>Bracket significant operations to identify incomplete tasks:</p><pre><code class="language-swift">Logger().notice(&quot;Starting data sync&quot;)
// ... sync implementation
Logger().notice(&quot;Completed data sync&quot;)</code></pre><h2 id="the-impact-on-support-and-development">The Impact on Support and Development</h2><p>Implementing these tools can transform both user experience and development workflows:</p><h3 id="for-users">For Users:</h3><ul><li><p><strong>Simplified Reporting</strong>: Submit feedback with a single tap</p></li><li><p><strong>No Technical Questions</strong>: Avoid frustrating back-and-forth communications</p></li><li><p><strong>Faster Resolution</strong>: Issues can be diagnosed and fixed more quickly</p></li><li><p><strong>Better Experience</strong>: Shows users you take their problems seriously</p></li></ul><h3 id="for-developers">For Developers:</h3><ul><li><p><strong>Complete Context</strong>: See exactly what was happening when issues occurred</p></li><li><p><strong>Reduced Support Time</strong>: Less time spent asking for additional information</p></li><li><p><strong>Better Reproduction</strong>: More reliable reproduction steps based on log data</p></li><li><p><strong>Efficient Debugging</strong>: Quickly identify patterns in error reports</p></li><li><p><strong>Data-Driven Priorities</strong>: Focus on fixing the most common issues first</p></li></ul><h2 id="conclusion">Conclusion</h2><p>ErrorKit’s approach bridges that frustrating gap between a user saying “it doesn’t work” and actually knowing what happened. I’ve found that automatic log collection combined with smart error analytics creates a feedback loop that actually works.</p><p>What’s really powerful is getting detailed logs when users choose to report problems while also catching the issues they never mention. This dual approach has transformed how I understand and fix problems in my apps. If you’re tired of debugging issues blindfolded, ErrorKit includes all these logging tools and error handling improvements—tools I built because I needed them myself:</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>How do you handle user feedback and error reporting? Have you found other effective techniques that actually help? Tell me on socials (links below)!</p><h3 id="previous-articles-in-this-series">Previous articles in this series:</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="following-article-in-this-series">Following article in this series:</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>Unlocking the Real Power of Swift 6&apos;s Typed Throws with Error Chains</title>
<link>https://fline.dev/blog/swift-6-typed-throws-error-chains/</link>
<guid isPermaLink="true">https://fline.dev/blog/swift-6-typed-throws-error-chains/</guid>
<pubDate>Mon, 28 Apr 2025 00:00:00 +0000</pubDate>
<description><![CDATA[Discover how to turn Typed Throws from a headache into a superpower — with clean error handling and powerful debugging insights.]]></description>
<content:encoded><![CDATA[<p>Swift 6 finally introduced one of the most requested features to Swift: Typed Throws. This improvement allows you to specify exactly which error types a function can throw, bringing Swift’s type safety to error handling. But with this power comes a new challenge I would call “nesting hell” — a problem that affects how errors propagate across layers of your application.</p><p>In this post, I’ll explain the nesting problem and show you how I’ve solved it in <a href="https://github.com/FlineDev/ErrorKit">ErrorKit</a> with a simple protocol that makes typed throws practical without boilerplate. As a bonus, you’ll see how proper error chaining can dramatically improve your debugging experience.</p><h2 id="typed-throws-the-promise-and-the-problem">Typed Throws: The Promise and the Problem</h2><p>First, let’s look at what Typed Throws gives us in Swift 6:</p><pre><code class="language-swift">// Instead of just 'throws', we can specify the error type
func processFile() throws(FileError) {
    if !fileExists {
        throw FileError.fileNotFound(fileName: &quot;config.json&quot;)
    }
    // Implementation...
}</code></pre><p>This enables better error handling at the call site:</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;)
}
// No generic catch needed if we've handled all possible FileError cases!</code></pre><p>The benefits are clear:</p><ul><li><p>Compile-time verification of error handling</p></li><li><p>No need for type casting with <code>as?</code> in catch blocks</p></li><li><p>Self-documenting API that tells callers exactly what can go wrong</p></li><li><p>IDE autocompletion for error cases</p></li></ul><h2 id="the-nesting-hell-problem">The Nesting Hell Problem</h2><p>The problem arises when working with multi-layered applications. Consider this:</p><pre><code class="language-swift">// Database layer throws DatabaseError
func fetchUser(id: String) throws(DatabaseError) {
    // Database operations...
}

// Profile layer needs to call the database layer
func loadUserProfile(id: String) throws(ProfileError) {
    do {
        // ⚠️ Problem: This throws DatabaseError, not ProfileError
        let user = try fetchUser(id: id)
    } catch {
        // Manual error conversion needed
        switch error {
        case DatabaseError.recordNotFound:
            throw ProfileError.userNotFound
        default:
            throw ProfileError.databaseError(error) // Need a wrapper case
        }
    }
}</code></pre><p>This creates several problems:</p><ol><li><p><strong>Wrapper Cases Explosion</strong>:
Every error type needs wrapper cases for all possible child errors</p></li><li><p><strong>Manual Error Mapping</strong>:
Repetitive do-catch blocks with explicit error conversion</p></li><li><p><strong>Type Proliferation</strong>:
Error types grow with each layer, becoming harder to maintain</p></li><li><p><strong>Lost Context</strong>:
Details about the original error often get lost in translation</p></li></ol><p>For small apps, this might be manageable. For larger apps with many layers, it quickly becomes what can be described as “nesting hell”.</p><h2 id="the-solution-the-catching-protocol">The Solution: The Catching Protocol</h2><p>ErrorKit solves this with a simple protocol called <code>Catching</code>:</p><pre><code class="language-swift">public protocol Catching {
    static func caught(_ error: Error) -&gt; Self
}</code></pre><p>This protocol requires a single enum case named <code>caught</code> that wraps any error into your type. Here’s how you use it:</p><pre><code class="language-swift">enum ProfileError: Throwable, Catching {
    case userNotFound
    case invalidProfile
    case caught(Error) // Single case for all other errors

    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):
            // Use the wrapped error's message
            return ErrorKit.userFriendlyMessage(for: error)
        }
    }
}</code></pre><p><em>Note that <code>Throwable</code> is a drop-in replacement for <code>Error</code> (see <a href="https://www.fline.dev/swift-error-handling-done-right-overcoming-the-objective-c-error-legacy/">previous post</a>).</em></p><p>Now, the magic happens with the <code>catch</code> function that comes with the protocol:</p><pre><code class="language-swift">func loadUserProfile(id: String) throws(ProfileError) {
    // For known errors, throw them directly
    guard isValidID(id) else {
        throw ProfileError.invalidInput
    }

    // For operations that may throw other error types, use the catch function
    let user = try ProfileError.catch {
        // Any error thrown here will be automatically wrapped
        // into ProfileError.caught(error)
        return try fetchUser(id: id)
    }

    // Rest of implementation...
}</code></pre><p><em>Note that the <code>catch</code> function returns whatever you return in the closure.</em></p><p>The <code>catch</code> function automatically wraps any errors thrown in its closure into your error type. No manual do-catch blocks, no explicit error mapping—it just works. Even multiple <code>try</code> expressions are possible.</p><h2 id="the-catch-functions-secret-sauce">The catch Function’s Secret Sauce</h2><p>The <code>catch</code> function is elegantly simple:</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>This function:</p><ol><li><p>Takes a throwing closure</p></li><li><p>Tries to execute it</p></li><li><p>Returns the result if successful</p></li><li><p>Automatically wraps any thrown error using your <code>caught</code> case</p></li><li><p>Preserves the return type of the operation</p></li></ol><p>The best part? It works seamlessly with Swift 6’s typed throws, maintaining type safety while eliminating boilerplate.</p><h2 id="preserving-the-error-chain-for-debugging">Preserving the Error Chain for Debugging</h2><p>One of the biggest benefits of this approach is that it preserves the complete error chain. Instead of losing context when errors cross boundaries, each layer adds information while keeping the original error intact.</p><p>ErrorKit leverages this to provide powerful debugging with the <code>errorChainDescription(for:)</code> function:</p><pre><code class="language-swift">do {
    try await updateUserProfile()
} catch {
    print(ErrorKit.errorChainDescription(for: error))

    // Output shows the complete chain:
    // AppError
    // └─ ProfileError
    //    └─ DatabaseError
    //       └─ FileError.notFound(path: &quot;/Users/data.db&quot;)
    //          └─ userFriendlyMessage: &quot;Could not find database file.&quot;
}</code></pre><p>This hierarchical view tells you:</p><ol><li><p>Where the error originated (FileError)</p></li><li><p>The exact path it took through your application (FileError → DatabaseError → ProfileError → AppError)</p></li><li><p>The specific details of what went wrong (file not found, with the path)</p></li><li><p>The user-friendly message that would be shown to users</p></li></ol><p>This level of insight is invaluable during debugging, especially for complex applications where errors might originate deep in the call stack.</p><h2 id="structured-error-chain-output">Structured Error Chain Output</h2><p>The error chain description works by recursively inspecting the error structure:</p><pre><code class="language-swift">static func errorChainDescription(for error: Error) -&gt; String {
    // Recursive implementation that builds a hierarchical description
    Self.chainDescription(for: error, enclosingType: type(of: error))
}</code></pre><p><em>See <a href="https://github.com/FlineDev/ErrorKit/blob/7e37b3f788ef9bc419819f7872f23395762ce822/Sources/ErrorKit/ErrorKit.swift#L227">here</a> for the full implementation of <code>chainDescription</code> inside ErrorKit.</em></p><p>The function uses Swift’s reflection capabilities to:</p><ol><li><p>Inspect the error using the Mirror API</p></li><li><p>For errors conforming to <code>Catching</code>, extract the wrapped error</p></li><li><p>For enum errors, capture case names and associated values</p></li><li><p>For struct or class errors, include type metadata</p></li><li><p>Format everything in a hierarchical tree structure</p></li></ol><p>This provides far more information than standard error logging, particularly for complex error hierarchies.</p><h2 id="built-in-support-in-errorkit">Built-in Support in ErrorKit</h2><p>All of ErrorKit’s <a href="https://swiftpackageindex.com/FlineDev/ErrorKit/documentation/errorkit/built-in-error-types">built-in error types</a> (like <code>FileError</code> or <code>NetworkError</code>) already conform to <code>Catching</code>, so you can use them right away:</p><pre><code class="language-swift">func saveUserData() throws(DatabaseError) {
    // Automatically wraps SQLite errors, file system errors, etc.
    try DatabaseError.catch {
        try database.beginTransaction()
        try database.execute(query)
        try database.commit()
    }
}</code></pre><h2 id="real-world-example-a-typical-application">Real-World Example: A Typical Application</h2><p>Let’s see how this works in a more complete example:</p><pre><code class="language-swift">// Data Access Layer
func fetchUserData(id: String) throws(DatabaseError) {
    guard database.isConnected else {
        throw DatabaseError.connectionFailed
    }

    // This could throw file system errors
    try DatabaseError.catch {
        let query = try QueryBuilder.build(for: id)
        return try database.execute(query)
    }
}

// Business Logic Layer
func processUserProfile(id: String) throws(ProfileError) {
    guard isValidID(id) else {
        throw ProfileError.invalidInput
    }

    // This automatically wraps DatabaseError
    let userData = try ProfileError.catch {
        return try fetchUserData(id: id)
    }

    // Process the user data...
}

// Presentation Layer
func displayUserProfile(id: String) throws(UIError) {
    // This automatically wraps ProfileError (which might contain DatabaseError)
    let profile = try UIError.catch {
        return try processUserProfile(id: id)
    }

    // Display the profile...
}</code></pre><p>If a database connection fails, here’s what you’ll see in the error chain:</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>This tells you exactly what happened and where the error originated, making debugging much easier. The added context can give you the right hint to fix the issue!</p><h2 id="conclusion">Conclusion</h2><p>Swift 6’s typed throws is a powerful addition to the language, but it introduces challenges for error propagation across layers. The <code>Catching</code> protocol offers a simple, elegant solution that maintains type safety while eliminating boilerplate.</p><p>Combined with ErrorKit’s <code>errorChainDescription</code> function, error handling becomes a powerful debugging tool. Use ErrorKit now and profit from many other improvements that make error handling in Swift more useful in real-world apps:</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>Have you started using Swift 6’s typed throws? How are you handling error propagation across layers in your apps? Let me know on socials (links below)!</p><h3 id="previous-article-in-this-series">Previous article in this series:</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="following-articles-in-this-series">Following articles in this series:</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 Done Right: Overcoming the Objective-C Error Legacy</title>
<link>https://fline.dev/blog/swift-error-handling-done-right-overcoming-the-objective-c-error-legacy/</link>
<guid isPermaLink="true">https://fline.dev/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[Tired of cryptic Swift error messages like '(YourError error 0)'? Here's how to fix them for good—with clarity and elegance.]]></description>
<content:encoded><![CDATA[<p>Have you ever spent time carefully crafting error messages in your Swift app, only to find they never actually appear? Instead, your users (or you during debugging) see cryptic messages like:</p><blockquote><p>“The operation couldn’t be completed. (YourApp.YourError error 0.)”</p></blockquote><p>If so, you’re not alone. This confusing behavior has been tripping up Swift developers—from beginners to experts—since the language was introduced. Today, I want to explain why this happens and present a solution that makes Swift error handling more intuitive.</p><h2 id="the-surprising-behavior-of-swifts-error-protocol">The Surprising Behavior of Swift’s Error Protocol</h2><p>Let’s look at a simple example that demonstrates the problem:</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;
      }
   }
}

// Using the error
do {
   throw NetworkError.noConnectionToServer
} catch {
   print(&quot;Error message: \(error.localizedDescription)&quot;)
   // Expected: &quot;No connection to the server.&quot;
   // Actual: &quot;The operation couldn't be completed. (AppName.NetworkError error 0.)&quot;
}</code></pre><p>What went wrong? We defined a clear error message, but Swift ignored it completely!</p><h2 id="why-this-happens-the-nserror-bridge">Why This Happens: The NSError Bridge</h2><p>This confusing behavior occurs because Swift’s <code>Error</code> protocol is bridged to Objective-C’s <code>NSError</code> class behind the scenes. When you access <code>localizedDescription</code>, Swift doesn’t use your property—it creates an <code>NSError</code> with a domain (your module name), a code (the enum case’s integer value), and a default message.</p><p>This design might make sense for Objective-C interoperability, but it creates a terrible developer experience, especially for those new to Swift.</p><h2 id="the-official-solution-localizederror">The “Official” Solution: LocalizedError</h2><p>Swift does provide an official solution: the <code>LocalizedError</code> protocol. Here’s how you’re supposed to use it:</p><pre><code class="language-swift">enum NetworkError: LocalizedError {
   case noConnectionToServer
   case parsingFailed

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

   // There are also these optional properties that are rarely used
   var failureReason: String? { return nil }
   var recoverySuggestion: String? { return nil }
   var helpAnchor: String? { return nil }
}</code></pre><p>While this works, it has several problems:</p><ul><li><p>All properties are <strong>optional</strong> (<code>String?</code>), so the compiler won’t help you if you forget to handle a case</p></li><li><p>Only <code>errorDescription</code> affects <code>localizedDescription</code>; the other properties are often ignored</p></li><li><p>The naming doesn’t clearly indicate which property affects the displayed message</p></li><li><p>It still uses a legacy approach based on Cocoa error handling patterns</p></li></ul><h2 id="a-better-solution-the-throwable-protocol">A Better Solution: The Throwable Protocol</h2><p>After experiencing this frustration too many times, I created a simpler solution as part of <a href="https://github.com/FlineDev/ErrorKit?ref=fline.dev">ErrorKit</a>—a protocol called <code>Throwable</code>:</p><pre><code class="language-swift">public protocol Throwable: LocalizedError {
   var userFriendlyMessage: String { get }
}</code></pre><p>This protocol has several advantages:</p><ul><li><p>It has a <strong>single, non-optional requirement</strong>—no more forgetting cases</p></li><li><p>The name <code>userFriendlyMessage</code> clearly expresses intent</p></li><li><p>It extends <code>LocalizedError</code> for compatibility (no extra work for you!)</p></li><li><p>It follows Swift naming conventions with the <code>-able</code> suffix</p></li></ul><p>Here’s how you use it:</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;
      }
   }
}

// Using the error
do {
   throw NetworkError.noConnectionToServer
} catch {
   print(&quot;Error message: \(error.localizedDescription)&quot;)
   // Now correctly shows: &quot;Unable to connect to the server.&quot; 🎉
}</code></pre><p>With <code>Throwable</code>, what you see is what you get—your error messages appear exactly as intended, with no surprises.</p><h2 id="quick-development-with-string-raw-values">Quick Development with String Raw Values</h2><p>For rapid prototyping, <code>Throwable</code> also works seamlessly with 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;
}

// That's it! No extra implementation needed</code></pre><p>The raw string values automatically become your error messages, eliminating boilerplate during early development. Later, when you’re ready for proper localization, you can switch to using <code>String(localized:)</code> in a full implementation of <code>userFriendlyMessage</code>.</p><h2 id="ready-to-use-error-types">Ready-To-Use Error Types</h2><p>To further reduce boilerplate, ErrorKit includes pre-defined error types for common scenarios:</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;)
    }

    // More implementation...
}</code></pre><p>These built-in types include:</p><ul><li><p><code>NetworkError</code> for connectivity and API issues</p></li><li><p><code>FileError</code> for file system operations</p></li><li><p><code>DatabaseError</code> for data persistence issues</p></li><li><p><code>ValidationError</code> for input validation</p></li><li><p><code>PermissionError</code> for authorization issues</p></li><li><p>And several more…</p></li></ul><p>Each built-in type already conforms to <code>Throwable</code> and provides localized user-friendly messages out of the box, saving you time while maintaining clarity.</p><h2 id="quick-one-off-errors-with-genericerror">Quick One-Off Errors with GenericError</h2><p>For those situations where you need a custom message without defining a whole new error type, ErrorKit provides <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;)
    }

    // More implementation...
}</code></pre><p>This is perfect for early development or unique error cases that don’t warrant a dedicated error type.</p><h2 id="benefits-beyond-better-messages">Benefits Beyond Better Messages</h2><p>Adopting <code>Throwable</code> doesn’t just fix error messages—it brings several additional benefits:</p><ol><li><p><strong>Clarity for new developers</strong>:
The protocol clearly indicates how to define error messages</p></li><li><p><strong>Compile-time safety</strong>:
The non-optional requirement ensures all cases have messages</p></li><li><p><strong>Localization support</strong>:
Works perfectly with <code>String(localized:)</code> for internationalization</p></li><li><p><strong>Reduced boilerplate</strong>:
Especially with raw string values and built-in types</p></li><li><p><strong>Improved user experience</strong>:
Clear error messages help users understand what went wrong</p></li><li><p><strong>Better debugging</strong>:
Meaningful error messages make debugging faster</p></li></ol><h2 id="making-the-transition">Making the Transition</h2><p>The best part? <code>Throwable</code> is a drop-in replacement for <code>Error</code>:</p><pre><code class="language-swift">// Before
enum AppError: Error {
    case configurationFailed
}

// After
enum AppError: Throwable {
    case configurationFailed

    var userFriendlyMessage: String {
        switch self {
        case .configurationFailed:
           return &quot;Failed to load configuration.&quot;
        }
    }
}</code></pre><p>Existing code using <code>throws</code>, <code>do</code>/<code>catch</code>, and other error handling patterns work exactly the same—the only difference is that now your error messages actually appear as intended.</p><h2 id="conclusion">Conclusion</h2><p>Swift’s error handling is powerful, but its message handling has been a confusing pain point for too long. The <code>Throwable</code> protocol provides a simple, intuitive solution that aligns with Swift’s design principles while fixing a longstanding issue.</p><p>By adopting <code>Throwable</code> for your error types, you get clearer error messages, reduced boilerplate, and a more intuitive developer experience. Combined with built-in error types and the <code>GenericError</code> fallback, it creates a comprehensive approach to error handling that works the way you’d expect.</p><p>If you want to try this approach in your own projects, check out ErrorKit, which includes the <code>Throwable</code> protocol, built-in error types, and many other error handling improvements for Swift:</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>Have you encountered this error message confusion in your Swift development? How have you addressed it? Let me know on social media (links below)!</p><h3 id="following-articles-in-this-series">Following articles in this series:</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>Solving Swift Macro Trust Issues in Xcode Cloud Builds</title>
<link>https://fline.dev/blog/solving-swift-macro-trust-issues-in-xcode-cloud-builds/</link>
<guid isPermaLink="true">https://fline.dev/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 are powerful but can break your CI pipeline with trust errors. Learn how to implement a simple post-clone script that solves the "Target must be enabled" error in Xcode Cloud once and for all.]]></description>
<content:encoded><![CDATA[<h2 id="the-problem-with-swift-macros-in-ci-environments">The Problem with Swift Macros in CI Environments</h2><p>Swift macros, introduced in Xcode 15 with Swift 5.9, are a powerful feature that enables code generation and compile-time metaprogramming. While they offer excellent developer productivity benefits, they’ve introduced a new challenge for CI/CD pipelines, particularly in Xcode Cloud.</p><p>When you first use a macro locally, Xcode displays a dialog asking you to trust the macro’s package before it can be executed. This works fine on your development machine, but becomes problematic in automated environments like Xcode Cloud where there’s no way to click “OK” on a dialog box.</p><p>If you’ve tried to build a project using Swift macros in Xcode Cloud, you’ve likely encountered this frustrating error:</p><pre><code>Target must be enabled before it can be used.</code></pre><h2 id="a-complete-solution">A Complete Solution</h2><p>After some research, I’ve found a reliable solution that works consistently for Xcode Cloud builds using Swift macros. Here’s how to implement it:</p><h3 id="step-1-create-a-ci-scripts-folder">Step 1: Create a CI Scripts Folder</h3><p>First, let’s create a dedicated folder for our CI scripts:</p><ol><li><p>Right-click your project in Xcode</p></li><li><p>Select “New Group”</p></li><li><p>Name it <code>ci_scripts</code></p></li></ol><h3 id="step-2-create-the-post-clone-script">Step 2: Create the Post-Clone Script</h3><p>Next, we’ll create a post-clone script that Xcode Cloud will automatically execute after cloning your repository:</p><ol><li><p>Right-click the <code>ci_scripts</code> folder</p></li><li><p>Select “New File” → “Empty File”</p></li><li><p>Name it <code>ci_post_clone.sh</code></p></li></ol><h3 id="step-3-add-the-script-content">Step 3: Add the Script Content</h3><p>Copy and paste the following code into your new <code>ci_post_clone.sh</code> file:</p><pre><code class="language-shell">#!/bin/sh

# Exit on error (-e), undefined vars (-u), and pipeline failures (-o pipefail)
set -euo pipefail

# Disable Xcode macro fingerprint validation to prevent spurious build errors
defaults write com.apple.dt.Xcode IDESkipMacroFingerprintValidation -bool YES</code></pre><p>This script does one crucial thing: it disables Xcode’s macro fingerprint validation, which is what triggers the trust dialog in the first place.</p><h3 id="step-4-make-the-script-executable">Step 4: Make the Script Executable</h3><p>Open Terminal, navigate to your project folder, and run:</p><pre><code class="language-bash">chmod +x ci_scripts/ci_post_clone.sh</code></pre><p>This command makes your script executable, which is required for Xcode Cloud to run it properly.</p><h3 id="step-5-commit-and-trigger-a-build">Step 5: Commit and Trigger a Build</h3><p>Commit the new file to your repository and push the changes. This will trigger a new build in Xcode Cloud, which should now complete successfully without the macro trust error.</p><h2 id="how-it-looks">How It Looks</h2><p>When properly set up, your project structure should include the CI scripts folder with the post-clone script:</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="why-this-works">Why This Works</h2><p>Xcode Cloud automatically executes any script named <code>ci_post_clone.sh</code> located in a <code>ci_scripts</code> directory at the root of your project. Our script modifies the Xcode preferences to disable macro fingerprint validation, effectively bypassing the trust requirement.</p><p>The command <code>defaults write com.apple.dt.Xcode IDESkipMacroFingerprintValidation -bool YES</code> changes a specific Xcode preference that controls whether macros need to be explicitly trusted before they can be used.</p><h2 id="security-considerations">Security Considerations</h2><p>Some developers suggest copying the <code>macros.json</code> file (which contains trusted macro fingerprints) from your local machine to the CI environment. However, I believe this approach isn’t necessarily more secure than simply disabling validation.</p><p>The key consideration here is context: Xcode Cloud runs in a sandboxed environment specifically for building your application. It’s not executing arbitrary code on production systems. If you can’t trust your development team and your release workflow, you likely have more significant security concerns than macros running in a CI environment.</p><h2 id="the-growing-importance-of-this-solution">The Growing Importance of This Solution</h2><p>As Swift continues to evolve, more and more third-party packages are implementing macros to provide powerful features with minimal boilerplate. For example:</p><ul><li><p><a href="https://github.com/FlineDev/TranslateKit"><strong>TranslateKit SDK</strong></a> offers localization macros</p></li><li><p>Various logging and debugging libraries are adopting macros</p></li><li><p>UI frameworks are using macros to simplify view declarations</p></li></ul><p>This trend will only accelerate as developers discover new use cases for compile-time metaprogramming. Having a reliable solution for handling macros in CI pipelines will become increasingly important for Swift projects.</p><p>Now that you’ve solved the macro trust issue, you can take full advantage of Xcode Cloud’s capabilities. In fact, I’ll be exploring how to utilize Xcode Cloud for App Store deployments in my next article and accompanying video. This approach can save you valuable time waiting for builds, especially if you ship on multiple Apple platforms (iOS, macOS, visionOS) like I do. Stay tuned for that and happy coding!</p>]]></content:encoded>
</item>
<item>
<title>When Getting Sherlocked Leads to Something Better: The TranslateKit Journey</title>
<link>https://fline.dev/blog/sherlocked-to-success/</link>
<guid isPermaLink="true">https://fline.dev/blog/sherlocked-to-success/</guid>
<pubDate>Wed, 19 Feb 2025 00:00:00 +0000</pubDate>
<description><![CDATA[A candid story about resilience in indie app development—and how the worst day of your developer life can sometimes lead to building something better.]]></description>
<content:encoded><![CDATA[<p>When Apple introduced String Catalogs at WWDC 2023, I felt my heart sink. I had just spent a full year building RemafoX, an Xcode extension for app localization, only to see much of its functionality become part of Xcode itself. As an indie developer, this was a significant setback – RemafoX was meant to be my flagship app after going indie full-time in 2022.</p><p>Yet, as I dug deeper into String Catalogs, I realized something interesting: Apple’s deep SwiftUI integration offered possibilities that weren’t achievable through Xcode extensions. Instead of fighting the tide, I decided to embrace it. Over a single weekend, I built a simple tool that could parse String Catalogs and handle machine translations. That tool became TranslateKit.</p><h2 id="an-unexpected-success-story">An Unexpected Success Story</h2><p>What started as a weekend project for personal use quickly became my most successful app to date. Developers worldwide embraced TranslateKit’s simplicity and efficiency. The feedback exceeded my expectations for such a quickly written app, and with it came valuable insights about what developers really needed from a localization tool. Just recently, a fellow indie developer shared on 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>This resonated deeply with me. Many developers <strong>overestimate the effort required to localize their SwiftUI apps</strong>. Getting to 90% localization coverage is surprisingly straightforward with today’s tools, and reaching that last 10% is becoming easier than ever.</p><p>Consider this: About <strong>80% of the world’s population doesn’t speak English</strong>. Lack of localization is statistically the number one reason globally for an app being inaccessible to users. Every app should be localized – it’s not just about reaching more users, it’s about making your app truly accessible. And I’ve made it my mission to make this as easy as possible for Indie developers.</p><h2 id="the-evolution-to-translatekit-3">The Evolution to TranslateKit 3</h2><p>After a year of learning from user feedback, I realized I could do much better. The biggest pain point? Setting up API keys. Developers found themselves registering for various translation services, dealing with credit cards, and managing API quotas. It was <strong>too much friction</strong> for what should be a simple process.</p><p>Then there were the <strong>translation quality</strong> issues. While my machine translation integrations were good, they often missed crucial context by translating each string in isolation. A button that made perfect sense in English could become awkwardly verbose or totally miss the context in other languages. By processing strings in batches and providing full app context to the AI, TranslateKit 3 maintains consistency and an appropriate tone across your entire app.</p><p>Project-wide management was another crucial improvement. Previously, developers had to remember to drag and drop their <code>InfoPlist.xcstrings</code> file separately – leading to situations where apps would be localized but permission dialogs would still appear in English. Now, TranslateKit handles your entire project at once, ensuring <strong>nothing gets missed</strong>.</p><p>This led to a complete rebuild, focusing on what matters most to developers:</p><ol><li><p><strong>Zero setup</strong> required - no more API keys or service registrations</p></li><li><p><strong>Context-aware</strong> AI translations that understand your app’s purpose</p></li><li><p><strong>Project-wide</strong> localization that catches everything, including permission texts</p></li><li><p>Intelligent handling of your app’s <strong>brand terms and terminology</strong></p></li><li><p>Support for <strong>language-specific nuances</strong> like formality and cultural context</p></li></ol><p>The results? Translation errors reduced by about 90% compared to traditional services. On top of that, TranslateKit 3 introduces AI <strong>proofreading</strong>, a unique feature that can improve any existing translations, whether they’re from an older version of TranslateKit or any other source. Just select the languages to check and let the AI fine-tune your translations for even better accuracy.</p><p>See for yourself what Noah, the developer behind <a href="https://proxyman.com/">Proxyman</a>, is saying:</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 about the localization of his very large app Proxyman.</em></p><p>And with the new pay-what-you-need pricing approach, localization is finally affordable for <em>all</em> indie developers. You can test the waters by adding a single language for just $1 (with a free first month to try it out), or go all in and reach 5x more users by adding the top 10 languages to an average-sized app in just 4 minutes, for less than $5. I hope this makes app localization as affordable as possible and removes one more barrier for developers considering it.</p><h2 id="looking-beyond-ios">Looking Beyond iOS</h2><p>But I didn’t stop there. As I shared in my <a href="https://www.fline.dev/swift-localization-in-2025-best-practices-you-couldnt-use-before/">recent post</a> about the <a href="https://github.com/FlineDev/TranslateKit">TranslateKit SDK</a>, SwiftUI developers can now benefit from automatic key generation with the <code>#tk</code> macro and access over 2,000 pre-translated common UI texts with a call like <code>TK.Action.cancel</code>. Both will help further improve the accuracy of localization. And while iOS development will always be my primary focus, I’ve designed TranslateKit’s core translation system to be platform-agnostic, paving the way for supporting other app platforms in the future, Android and Flutter being the first.</p><p>I’m putting the finishing touches on a comprehensive video guide that will show just how straightforward app localization has become. Because sometimes, seeing is believing – and I want every developer to know they can make their app global without the headaches they might expect. For now, this 50 sec video must suffice:</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>Quick Demo of Localizing an app with TranslateKit 3</p></figcaption>
</div>
<h2 id="try-it-yourself">Try It Yourself</h2><p>The best way to understand how easy localization has become is to try it. Add a String Catalog to your project, build your app, and you might be surprised to find it already mostly has all the entries to be localized. With <a href="https://translatekit.app/">TranslateKit 3</a>, you can translate them with context-aware AI, ensure consistency across your entire project, and reach users worldwide faster than ever.</p><p>The journey from RemafoX to TranslateKit has taught me that sometimes, what seems like a setback can lead to building something even better. By embracing new technologies and really listening to developer needs, I’ve created a tool that makes app localization easier &amp; better. Patience always pays off in the end!</p>]]></content:encoded>
</item>
<item>
<title>Swift Localization in 2025: Best Practices You Couldn&apos;t Use Before</title>
<link>https://fline.dev/blog/swift-localization-in-2025-best-practices-you-couldnt-use-before/</link>
<guid isPermaLink="true">https://fline.dev/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 improved localization but introduced new challenges. This article explores how to regain structure and efficiency with modern best practices and a new open-source tool that could change the way you localize.]]></description>
<content:encoded><![CDATA[<h2 id="the-evolution-of-ios-localization">The Evolution of iOS Localization</h2><p>To understand how far localization has come—and where it still falls short—let’s take a quick look back: At the pre-String Catalogs era, developers relied on a combination of <code>.strings</code> and <code>.stringsdict</code> files, organized in language-specific folders like <code>en.lproj</code>. This system, while functional, had several drawbacks:</p><ol><li><p><strong>Missing safety</strong> checks for unused or missing translations</p></li><li><p><strong>Manual synchronization</strong> needed between different language files</p></li><li><p><strong>No</strong> built-in support for <strong>extracting strings</strong> from code</p></li></ol><p>Tools like <a href="https://github.com/SwiftGen/SwiftGen">SwiftGen</a> and <a href="https://github.com/FlineDev/BartyCrouch">BartyCrouch</a> emerged to address these issues, providing type safety and automated extraction. And the community established best practices around using semantic keys (e.g., <code>&quot;Onboarding.Page1.title&quot;</code>) to provide context for translators and group related strings together.</p><h2 id="string-catalogs-a-game-changer-with-trade-offs">String Catalogs: A Game-Changer with Trade-offs</h2><p>String Catalogs introduced by Apple in 2023 solved several long-standing issues:</p><p>✅  <strong>Automatic</strong> key <strong>extraction</strong> from code
✅  Built-in <strong>safety checks</strong> for missing translations
✅  Visual <strong>progress</strong> tracking in Xcode
✅  <strong>Unified file</strong> format for all languages
✅  <strong>Backward compatibility</strong> with older iOS versions</p><p>However, Apple’s recommended approach of using English strings as keys, while improving readability in code, introduced new challenges:</p><p>❌  Related strings are <strong>scattered</strong> alphabetically
❌  Reduced <strong>context</strong> for translators
❌  No <strong>grouping</strong> by feature or 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="The lack of grouping in String Catalogs is not helpful while translating." loading="lazy" />
<em>The lack of grouping in String Catalogs is not helpful while translating.</em></p><p>Just as importantly, there’s a significant gap in the iOS development ecosystem around localization that mirrors where we were with icons before SF Symbols. Before SF Symbols, every developer had to create or source their own icons, leading to inconsistent experiences across apps. SF Symbols solved this by providing a standardized, comprehensive set of icons that ensure consistency and save tremendous development time.</p><p>The localization space desperately needs a similar solution. <strong>Every app needs common UI strings</strong> like “Save,” “Cancel,” “Done,” “Privacy Policy,” or “Terms and Conditions” – and these should be consistent across apps just like icons are. Users benefit from this consistency in the same way they benefit from consistent iconography. Yet currently, each developer must translate these common UI elements independently, leading to varying translations for the same concepts across different apps in different languages.</p><h2 id="enter-translatekit-the-missing-piece">Enter TranslateKit: The Missing Piece</h2><p>The open-source <a href="https://github.com/FlineDev/TranslateKit">TranslateKit SDK</a> bridges these gaps, offering a comprehensive solution that maintains best practices while fully embracing the benefits of String Catalogs. Just as SF Symbols revolutionized icon usage in iOS apps, this new Swift package aims to do the same for localization.</p><h3 id="1-semantic-key-generation-without-the-hassle">1. Semantic Key Generation Without the Hassle</h3><p>The <code>#tk</code> macro brings back semantic keys while keeping code clean and readable:</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>This approach:</p><ul><li><p>Automatically generates <strong>semantic keys</strong> based on code context</p></li><li><p>Preserves code <strong>readability</strong></p></li><li><p>Provides essential <strong>context for translators</strong></p></li><li><p><strong>Groups related strings</strong> together in String Catalogs</p></li></ul><p><img src="/assets/images/blog/swift-localization-in-2025-best-practices-you-couldnt-use-before/auto-generated-semantic.webp" alt="Auto-generated semantic keys, naturally grouping related strings together." loading="lazy" />
<em>Auto-generated semantic keys, naturally grouping related strings together.</em></p><p>And all you need to write is <code>#tk()</code> around your String literals. Super simple!</p><h3 id="2-pre-localized-common-ui-elements">2. Pre-localized Common UI Elements</h3><p>Just as SF Symbols standardized icon usage, we need consistency in UI text. TranslateKit provides over 2,000 strings pre-localized to all 40 languages supported by Apple in the iOS operating system.</p><p>They are divided into <strong>4 categories</strong>, making it easy to discover what you need:</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>Benefits:</p><ul><li><p>Matches system UI translations from Apple (<strong>Consistency</strong>)</p></li><li><p>Ensures professional quality for common UI elements (<strong>Accuracy</strong>)</p></li><li><p>Reduces localization overhead (save <strong>Time</strong> &amp; <strong>Money</strong>)</p></li></ul><h3 id="3-context-aware-translations">3. Context-Aware Translations</h3><p>Modern localization isn’t just about translating words—it’s about understanding context. While automatic key generation helps with this in 90% of the cases, in some cases you might need to additionally specify the optional <code>c</code> parameter:</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>This comment parameter is most commonly needed when you have <strong>dynamic data</strong> in your string like in the example above. It’s important to always provide typical sample data in the comment to make it clear. Otherwise translators might not know how to translate properly.</p><h2 id="whats-next">What’s Next?</h2><p>This article covered how <a href="https://github.com/FlineDev/TranslateKit">TranslateKit SDK</a> modernizes the core localization workflow. If you’re interested in more advanced topics like pluralization, formatters &amp; more, subscribe to my YouTube channel to signal your interest in more detailed videos:</p><p><a href="https://www.youtube.com/c/FlineDev">FlineDev</a></p><blockquote><p>✨ TranslateKit SDK works perfectly together with <a href="https://translatekit.app/">TranslateKit for Mac</a>, an accurate, AI-powered translation tool to <strong>localize the rest of your app</strong>! It’s super fast &amp; affordable, <a href="https://translatekit.app/"><strong>try free</strong></a> now to reach more users.</p></blockquote>]]></content:encoded>
</item>
<item>
<title>Use .labelStyle(.iconOnly) Instead of Nesting Image in Button</title>
<link>https://fline.dev/snippets/labelstyle-icononly-swiftui/</link>
<guid isPermaLink="true">https://fline.dev/snippets/labelstyle-icononly-swiftui/</guid>
<pubDate>Sun, 12 Jan 2025 00:00:00 +0000</pubDate>
<description><![CDATA[The proper SwiftUI pattern for icon-only buttons that preserves accessibility without sacrificing readability.]]></description>
<content:encoded><![CDATA[<h2 id="stop-fighting-the-framework">Stop Fighting the Framework</h2><p>A pattern I see constantly in SwiftUI code is manually creating icon-only buttons by putting an <code>Image</code> directly inside a <code>Button</code>‘s label closure. It works visually, but it throws away accessibility information and fights against SwiftUI’s design.</p><h2 id="the-wrong-way">The Wrong Way</h2><pre><code class="language-swift">Button {
   toggleSidebar()
} label: {
   Image(systemName: &quot;sidebar.left&quot;)
}</code></pre><p>This renders a tappable icon, but VoiceOver has no meaningful label to announce. The user hears something like “button” or the raw SF Symbol name, which is not helpful.</p><h2 id="the-right-way">The Right Way</h2><pre><code class="language-swift">Button(&quot;Toggle Sidebar&quot;, systemImage: &quot;sidebar.left&quot;) {
   toggleSidebar()
}
.labelStyle(.iconOnly)</code></pre><p>Or using <code>Label</code> explicitly:</p><pre><code class="language-swift">Button(action: toggleSidebar) {
   Label(&quot;Toggle Sidebar&quot;, systemImage: &quot;sidebar.left&quot;)
}
.labelStyle(.iconOnly)</code></pre><p><img src="/assets/images/snippets/labelstyle-icononly-swiftui/code-comparison.webp" alt="Code comparison showing the wrong way versus the right way" loading="lazy" /></p><h2 id="why-this-matters">Why This Matters</h2><p>The <code>Label</code> view carries both a title and an icon. When you apply <code>.labelStyle(.iconOnly)</code>, SwiftUI hides the title visually but preserves it in the accessibility tree. VoiceOver will announce “Toggle Sidebar, button” – exactly what the user needs to hear.</p><p>This pattern also makes your code more adaptable. If you later decide to show text alongside the icon (for example, in a toolbar on iPad), you just change the label style to <code>.titleAndIcon</code>. No restructuring needed.</p><h2 id="beyond-buttons">Beyond Buttons</h2><p>The same principle applies to any view that accepts a <code>Label</code>: <code>NavigationLink</code>, <code>Toggle</code>, <code>Picker</code>, menu items. Whenever you are tempted to use a bare <code>Image</code>, ask yourself whether a <code>Label</code> with a style modifier would be more appropriate. In almost every case, it is.</p>]]></content:encoded>
</item>
<item>
<title>EditorConfig for Every SwiftPM Package</title>
<link>https://fline.dev/snippets/editorconfig-swiftpm-package/</link>
<guid isPermaLink="true">https://fline.dev/snippets/editorconfig-swiftpm-package/</guid>
<pubDate>Tue, 03 Dec 2024 00:00:00 +0000</pubDate>
<description><![CDATA[Why every Swift package should include an .editorconfig file to enforce consistent indentation across all contributors.]]></description>
<content:encoded><![CDATA[<h2 id="the-invisible-formatting-problem">The Invisible Formatting Problem</h2><p>When multiple people contribute to a Swift package, indentation inconsistencies creep in. One contributor uses 4 spaces, another uses tabs, a third uses 2 spaces. Pull requests become noisy with whitespace-only changes, and the codebase drifts toward an inconsistent mess. The fix is a single file that takes 30 seconds to add.</p><h2 id="the-editorconfig-standard">The .editorconfig Standard</h2><p>EditorConfig is a widely supported standard for defining coding style settings per project. Xcode respects <code>.editorconfig</code> files, so contributors automatically get the correct indentation settings when they open the project – no manual configuration, no documentation to read.</p><p>Here is the <code>.editorconfig</code> I recommend for every SwiftPM package:</p><pre><code>root = true

[*.swift]
indent_style = space
indent_size = 3

[*.{yml,yaml,json}]
indent_style = space
indent_size = 2</code></pre><p>The 3-space indent for Swift is an intentional choice. It is less common than 2 or 4 spaces, but it hits a sweet spot: more readable than 2 spaces for nested closures, less wasteful of horizontal space than 4 spaces. Once you try it, the standard choices start to feel either too cramped or too spread out.</p><h2 id="why-this-beats-global-settings">Why This Beats Global Settings</h2><p>Every developer has their own Xcode indentation preferences configured globally. Without <code>.editorconfig</code>, those global settings apply to every project they open. This means a contributor with 4-space tabs will silently reformat code when they edit a file, even if the project convention is different.</p><p>With <code>.editorconfig</code> in the repository root, Xcode overrides global settings with the project-specific ones. Contributors do not need to change anything – it just works.</p><h2 id="adoption">Adoption</h2><p>Drop this file in your package root and commit it. That is all. There is no build step, no dependency, no configuration beyond the file itself. Most modern editors (VS Code, Vim, Sublime Text) also support EditorConfig, so non-Xcode contributors benefit too.</p>]]></content:encoded>
</item>
<item>
<title>Push Notifications for App Store Reviews</title>
<link>https://fline.dev/snippets/push-notifications-app-store-reviews/</link>
<guid isPermaLink="true">https://fline.dev/snippets/push-notifications-app-store-reviews/</guid>
<pubDate>Thu, 28 Nov 2024 00:00:00 +0000</pubDate>
<description><![CDATA[How to enable push notifications for new App Store reviews in the App Store Connect app so you can respond to user feedback quickly.]]></description>
<content:encoded><![CDATA[<h2 id="a-hidden-feature-in-app-store-connect">A Hidden Feature in App Store Connect</h2><p>The App Store Connect iOS app has a feature that most developers never discover: push notifications for new user reviews. It is turned off by default, and you have to enable it separately for each app, which is probably why so few people know about it.</p><h2 id="how-to-enable-it">How to Enable It</h2><p>Open the App Store Connect app on your iPhone, then navigate to:</p><ol><li><p>Tap your profile icon or go to <strong>Settings</strong></p></li><li><p>Select <strong>Notifications</strong></p></li><li><p>You will see a list of all your apps</p></li><li><p>For each app, toggle on <strong>Customer Reviews</strong></p></li></ol><p>That is it. From now on, you will get a push notification whenever someone leaves a new review for that app.</p><p><video src="/assets/images/snippets/push-notifications-app-store-reviews/demo.mp4" controls muted playsinline></video></p><h2 id="why-this-matters-for-indie-developers">Why This Matters for Indie Developers</h2><p>Responding to App Store reviews quickly has a tangible impact. When a user leaves a negative review about a bug, a fast reply acknowledging the issue (or pointing them to a fix) can lead them to update their rating. Positive reviews also deserve acknowledgment – it encourages users to keep providing feedback.</p><p>Without notifications, most developers only check reviews when they remember to, which might be days or weeks later. By then, the user has moved on, and your response feels like an afterthought.</p><h2 id="a-word-of-caution">A Word of Caution</h2><p>If you have multiple apps with high review volume, enabling this for all of them could become noisy. Start with your most important apps and adjust based on how many notifications you actually receive. For most indie developers with a handful of apps, the volume is perfectly manageable and the benefits are immediate.</p>]]></content:encoded>
</item>
<item>
<title>HandySwiftUI Styles: Enhancing SwiftUI&apos;s Standard Views</title>
<link>https://fline.dev/blog/handyswiftui-styles/</link>
<guid isPermaLink="true">https://fline.dev/blog/handyswiftui-styles/</guid>
<pubDate>Thu, 07 Nov 2024 00:00:00 +0000</pubDate>
<description><![CDATA[From attention-grabbing pulsating buttons and versatile label layouts to cross-platform checkboxes and vertical form styles - discover the SwiftUI styles that bring polish and consistency to your apps. These battle-tested styles power the UI of 10 production apps and counting.]]></description>
<content:encoded><![CDATA[<p>After 4 years of iterating on these APIs in my own apps, I’m happy to share the first tagged release of <a href="https://github.com/FlineDev/HandySwiftUI">HandySwiftUI</a>. This package contains various utilities and convenience APIs that were essential in helping me ship 10 apps in the past year alone. It provides conveniences for SwiftUI development similar to how my <a href="https://github.com/FlineDev/HandySwift">HandySwift</a> package does for Foundation.</p><p>In this article, I’ll share a selection of the <em>styles</em> I’ve found most valuable in my daily development work across apps like <a href="https://translatekit.app/">TranslateKit</a>, <a href="https://freemiumkit.app/">FreemiumKit</a>, and <a href="https://crosscraft.app/">CrossCraft</a>. While HandySwiftUI contains many more utilities, these particular styles have proven their worth time and time again in real-world applications and could be helpful for your SwiftUI projects as well.</p><h3 id="primary-secondary-and-pulsating-buttons">Primary, Secondary, and Pulsating Buttons</h3><p>Create visually appealing buttons with pre-made styles for different use cases:</p><pre><code class="language-swift">struct ButtonShowcase: View {
   var body: some View {
       VStack(spacing: 20) {
           // Primary button with prominent background
           Button(&quot;Get Started&quot;) {}
               .buttonStyle(.primary())

           // Secondary button with border
           Button(&quot;Learn More&quot;) {}
               .buttonStyle(.secondary())

           // Attention-grabbing pulsating 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="horizontal-vertical-fixed-icon-width-labels">Horizontal, Vertical, Fixed Icon-Width Labels</h3><p>Multiple label styles for different layout needs:</p><pre><code class="language-swift">struct LabelShowcase: View {
   var body: some View {
       VStack(spacing: 20) {
           // Horizontal layout with trailing icon
           Label(&quot;Settings&quot;, systemImage: &quot;gear&quot;)
               .labelStyle(.horizontal(spacing: 8, iconIsTrailing: true, iconColor: .blue))

           // Fixed-width icon for alignment
           Label(&quot;Profile&quot;, systemImage: &quot;person&quot;)
               .labelStyle(.fixedIconWidth(30, iconColor: .green, titleColor: .primary))

           // Vertical stack layout
           Label(&quot;Messages&quot;, systemImage: &quot;message.fill&quot;)
               .labelStyle(.vertical(spacing: 8, iconColor: .blue, iconFont: .title))
       }
   }
}</code></pre><p>All parameters are optional with sensible defaults, so you can use them like <code>.vertical()</code>. You only need to specify what you want to customize.</p><h3 id="vertically-labeled-contents">Vertically Labeled Contents</h3><p>Structured form inputs with vertical labels, as used in <a href="https://freemiumkit.app/">FreemiumKit</a>’s API configuration:</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>The <code>.vertical</code> style allows customizing alignment (defaults to <code>leading</code>) and spacing (defaults to 4). Pass <code>muteLabel: false</code> if you’re providing a custom label style, as by default labels are automatically styled smaller and grayed out.</p><p>For example, in <a href="https://freemiumkit.app/">FreemiumKit</a>’s feature localization form, I want the vertical label to have a larger font:</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>While SwiftUI provides a <code>.checkbox</code> toggle style, it’s only available on macOS. HandySwiftUI adds <code>.checkboxUniversal</code> that brings checkbox-style toggles to all platforms (rendering as <code>.checkbox</code> on macOS):</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>The example is extracted from <a href="https://freemiumkit.app/">FreemiumKit</a>’s products screen, which is optimized for macOS but also supports other platforms.</p><h2 id="get-started-today">Get Started Today</h2><p>I hope you find these styles as useful in your projects as I do in mine. If you have ideas for improvements or additional styles that could benefit the SwiftUI community, please feel free to contribute on GitHub:</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>This is the final article in a series of four exploring HandySwiftUI’s features. Check out the previous articles about <a href="https://www.fline.dev/handyswiftui-new-types/">New Types</a>, <a href="https://www.fline.dev/handyswiftui-view-modifiers/">View Modifiers</a>, and <a href="https://www.fline.dev/handyswiftui-extensions/">Extensions</a> if you haven’t already!</p>]]></content:encoded>
</item>
<item>
<title>HandySwiftUI Extensions: Making SwiftUI Development More Convenient</title>
<link>https://fline.dev/blog/handyswiftui-extensions/</link>
<guid isPermaLink="true">https://fline.dev/blog/handyswiftui-extensions/</guid>
<pubDate>Mon, 04 Nov 2024 00:00:00 +0000</pubDate>
<description><![CDATA[Discover powerful SwiftUI extensions for clean optional bindings, intuitive color management, XML-style text formatting, and more. These battle-tested utilities will help you write more elegant SwiftUI code while reducing boilerplate in your apps.]]></description>
<content:encoded><![CDATA[<p>After 4 years of iterating on these APIs in my own apps, I’m happy to share the first tagged release of <a href="https://github.com/FlineDev/HandySwiftUI">HandySwiftUI</a>. This package contains various utilities and convenience APIs that were essential in helping me ship 10 apps in the past year alone. It provides conveniences for SwiftUI development similar to how my <a href="https://github.com/FlineDev/HandySwift">HandySwift</a> package does for Foundation.</p><p>In this article, I’ll share a selection of the <em>extensions</em> I’ve found most valuable in my daily development work across apps like <a href="https://translatekit.app/">TranslateKit</a>, <a href="https://freemiumkit.app/">FreemiumKit</a>, and <a href="https://crosscraft.app/">CrossCraft</a>. While HandySwiftUI contains many more utilities, these particular extensions have proven their worth time and time again in real-world applications and could be helpful for your SwiftUI projects as well.</p><h3 id="optional-binding-conveniences">Optional Binding Conveniences</h3><p>The <code>??</code> and <code>!</code> operators and the <code>isPresent</code> modifier simplify working with optional values 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 {
           // Provide default value for optional binding using the `??` operator
           TextField(&quot;Name&quot;, text: $profile?.name ?? &quot;Anonymous&quot;)

           // Negate binding value using `!` operator
           Toggle(&quot;Hide Details&quot;, isOn: !$showAdvanced)
       }
       // Use optional binding for sheet presentation
       .sheet(isPresented: $profile.isPresent(wrappedType: Profile.self)) {
           ProfileEditor(profile: $profile)
       }
   }
}</code></pre><p>The operators are useful in all kinds of views, when working with optional data in models, for example.</p><h3 id="color-management">Color Management</h3><p>The comprehensive color extensions provide powerful tools for color manipulation and system color adoption:</p><pre><code class="language-swift">struct ColorfulView: View {
   @State private var baseColor = Color.blue

   var body: some View {
       VStack {
           // Create variations of the base color
           Rectangle()
               .fill(baseColor.change(.luminance, by: -0.2))
           Rectangle()
               .fill(baseColor)
           Rectangle()
               .fill(baseColor.change(.luminance, by: 0.2))

           // Work with hex colors
           Circle()
               .fill(Color(hex: &quot;#FF5733&quot;))

           // Use color components
           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()
       // Use semantic system colors for custom system-like components
       .background(Color.systemBackground)
   }
}</code></pre><p><img src="/assets/images/blog/handyswiftui-extensions/colorful-view.webp" alt="Colorful view" loading="lazy" /></p><p>When adjusting color brightness, use .luminance instead of .brightness from the HSB color system. Luminance better represents how humans perceive light and dark, which is why HandySwiftUI includes support for the HLC color space.</p><h3 id="rich-text-formatting">Rich Text Formatting</h3><p>The text formatting extensions provide a convenient way to create rich text with mixed styles inspired by XML-style 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 }  // returning $1 (instead of $0) means added keys override (potentially) existing keys
       )
   }
}</code></pre><p><img src="/assets/images/blog/handyswiftui-extensions/formatted-text.webp" alt="Formatted text" loading="lazy" /></p><p>In the above example, the built-in <code>.htmlLike</code> styling that ships with HandySwiftUI is combined with custom tags. Note that <code>.htmlLike</code> simply returns this:</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>Any XML-like entries that end with a <code>/&gt;</code> such as <code>&lt;star.fill/&gt;</code> from the example above get rendered as an SFSymbol. This way, you can easily use SFSymbols right within your text.</p><h3 id="image-handling">Image Handling</h3><p>Unified extensions for image processing for <code>UIImage</code> and <code>NSImage</code>:</p><pre><code class="language-swift">class ImageProcessor {
   func processImage(_ image: UIImage) {
       // Resize image while maintaining aspect ratio
       let resized = image.resized(maxWidth: 800, maxHeight: 600)

       // Convert to different formats
       let pngData = image.webpData()
       let jpegData = image.webpData(compressionQuality: 0.8)
       let heicData = image.heicData(compressionQuality: 0.8)
   }
}</code></pre><p>Note that all these APIs return optional values for edge cases like when the system is extremely low on memory, but should succeed most of the time.</p><h3 id="convenient-model-to-view-conversions">Convenient Model-to-View Conversions</h3><p>HandySwiftUI provides initializer conveniences that make it easy to display your model types directly in SwiftUI views:</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()
                // Create tab item directly from enum case
                .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)
        }

        // Works with Text and Image views too
        Text(convertible: selectedTab)  // Shows tab name
        Image(convertible: selectedTab) // Shows tab icon
    }
}</code></pre><p>Instead of manually extracting strings and symbol names from your models, you can conform them to <code>CustomStringConvertible</code> for text, <code>CustomSymbolConvertible</code> for SF Symbols, or <code>CustomLabelConvertible</code> for both. Then use the convenient initializers to create SwiftUI views directly:</p><ul><li><p><code>Text(convertible:)</code> - Creates text from any <code>CustomStringConvertible</code></p></li><li><p><code>Image(convertible:)</code> - Creates SF Symbol images from any <code>CustomSymbolConvertible</code></p></li><li><p><code>Label(convertible:)</code> - Creates text+icon labels from any <code>CustomLabelConvertible</code></p></li></ul><p>This pattern works especially well with enums representing UI states, menu options, or tabs, as shown in the example above.</p><h3 id="search-prefix-highlighting">Search Prefix Highlighting</h3><p>HandySwiftUI provides an elegant way to highlight matching text in search results, making it easy to show users exactly what parts of the text matched their search query:</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
                // When searching for &quot;go mo&quot;, highlights &quot;Go mo&quot; in &quot;Good morning!&quot;
                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>This highlighting feature was originally developed for <a href="https://translatekit.app/">TranslateKit</a>’s menu bar “Common Translations” feature, where it helps users quickly spot matching phrases in confirmed translations. The function breaks down the search text into tokens and highlights each matching prefix, making it perfect for:</p><ul><li><p>Search result highlighting in lists or menus</p></li><li><p>Autocomplete suggestions with visual feedback</p></li><li><p>Filtering through text collections while showing match context</p></li><li><p>Making search matches more visible in document previews</p></li></ul><p>The highlighting is case-insensitive and diacritic-insensitive by default, but you can customize the locale and font used for highlighting. This makes it a versatile tool for any search interface where you want to emphasize matching portions of text.</p><h2 id="get-started-today">Get Started Today</h2><p>I hope you find these extensions as useful in your projects as I do in mine. If you have ideas for improvements or additional extensions that could benefit the SwiftUI community, please feel free to contribute on GitHub:</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>This is the third in a series of four articles exploring HandySwiftUI’s features. Check out the previous articles about <a href="https://www.fline.dev/handyswiftui-new-types/">New Types</a> and <a href="https://www.fline.dev/handyswiftui-view-modifiers/">View Modifiers</a> if you haven’t already, and stay tuned for the final post about <a href="https://www.fline.dev/handyswiftui-styles/">Styles</a>!</p>]]></content:encoded>
</item>
<item>
<title>HandySwiftUI View Modifiers: Streamlining Your SwiftUI Code</title>
<link>https://fline.dev/blog/handyswiftui-view-modifiers/</link>
<guid isPermaLink="true">https://fline.dev/blog/handyswiftui-view-modifiers/</guid>
<pubDate>Fri, 01 Nov 2024 00:00:00 +0000</pubDate>
<description><![CDATA[From smart color contrast and streamlined error handling to simplified deletion flows and platform-specific styling - discover the SwiftUI modifiers that eliminate common boilerplate code and help create more maintainable apps.]]></description>
<content:encoded><![CDATA[<p>After 4 years of iterating on these APIs in my own apps, I’m happy to share the first tagged release of <a href="https://github.com/FlineDev/HandySwiftUI">HandySwiftUI</a>. This package contains various utilities and convenience APIs that were essential in helping me ship 10 apps in the past year alone. It provides conveniences for SwiftUI development similar to how my <a href="https://github.com/FlineDev/HandySwift">HandySwift</a> package does for Foundation.</p><p>In this article, I’ll share a selection of the <em>view modifiers</em> I’ve found most valuable in my daily development work across apps like <a href="https://translatekit.app/">TranslateKit</a>, <a href="https://freemiumkit.app/">FreemiumKit</a>, and <a href="https://crosscraft.app/">CrossCraft</a>. While HandySwiftUI contains many more utilities, these particular modifiers have proven their worth time and time again in real-world applications and could be helpful for your SwiftUI projects as well.</p><h3 id="smart-color-contrast">Smart Color Contrast</h3><p>The <code>foregroundStyle(_:minContrast:)</code> modifier ensures text remains readable by automatically adjusting color contrast. This is useful for dynamic colors or system colors like <code>.yellow</code> that might have poor contrast in certain color schemes:</p><pre><code class="language-swift">struct AdaptiveText: View {
    @State private var dynamicColor: Color = .yellow

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

            // With automatic contrast adjustment
            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>This warning indicator in <a href="https://translatekit.app/">TranslateKit</a> uses <code>.yellow</code> but ensures a good contrast even in light mode to be legible. It stays yellow in dark mode.</p></blockquote><p>The <code>minContrast</code> parameter (ranging from 0 to 1) determines the minimum contrast ratio against either white (in light mode) or black (in dark mode) using the luminance value (perceived brightness). This ensures text stays readable regardless of the current color scheme.</p><h3 id="error-handling-tasks">Error-Handling Tasks</h3><p>The <code>throwingTask</code> modifier streamlines async error handling in SwiftUI views. Unlike SwiftUI’s built-in <code>.task</code> modifier which requires manual <code>do-catch</code> blocks, <code>throwingTask</code> provides a dedicated 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>The task behaves similarly to <code>.task</code> – starting when the view appears and canceling when it disappears. The <code>catchError</code> closure is optional, so you can omit it if you don’t need to handle errors, filling the gap the <code>task</code> modifier left open.</p><h3 id="platform-specific-styling">Platform-Specific Styling</h3><p>A full set of platform modifiers enables precise control over multi-platform UI:</p><pre><code class="language-swift">struct AdaptiveInterface: View {
    var body: some View {
        ContentView()
            // Add padding only on macOS
            .macOSOnlyPadding(.all, 20)
            // Platform-specific styles
            .macOSOnly { $0.frame(minWidth: 800) }
            .iOSOnly { $0.navigationViewStyle(.stack) }
    }
}</code></pre><p>The example showcases modifiers for platform-specific styling:</p><ul><li><p><code>.macOSOnlyPadding</code> adds padding only on macOS where containers like <code>Form</code> lack default padding</p></li><li><p><code>.macOSOnlyFrame</code> sets minimum window sizes needed on macOS</p></li><li><p>Platform modifiers (<code>.iOSOnly</code>, <code>.macOSOnly</code>, <code>.iOSExcluded</code>, etc.) available for iOS, macOS, tvOS, visionOS, and watchOS allow selective application of view modifications on specific platforms</p></li></ul><p>These modifiers help create platform-appropriate interfaces while keeping the code clean and maintainable.</p><h3 id="border-with-corner-radius">Border with Corner Radius</h3><p>SwiftUI doesn’t provide a straightforward way to add a border to a view with corner radius. The standard approach requires verbose overlay code that is hard to remember:</p><pre><code class="language-swift">Text(&quot;Without HandySwiftUI&quot;)
    .padding()
    .overlay(
        RoundedRectangle(cornerRadius: 12)
            .strokeBorder(.blue, lineWidth: 2)
    )</code></pre><p>HandySwiftUI simplifies this with a convenient 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> use this for rounded borders, for example.</p></blockquote><h3 id="conditional-modifiers">Conditional Modifiers</h3><p>A suite of modifiers for handling conditional view modifications cleanly:</p><pre><code class="language-swift">struct DynamicContent: View {
    @State private var isEditMode = false
    @State private var accentColor: Color?

    var body: some View {
        ContentView()
            // Apply different modifiers based on condition
            .applyIf(isEditMode) {
                $0.overlay(EditingTools())
            } else: {
                $0.overlay(ViewingTools())
            }

            // Apply modifier only if optional exists
            .ifLet(accentColor) { view, color in
                view.tint(color)
            }
    }
}</code></pre><p>The example demonstrates <code>.applyIf</code> which applies different view modifications based on a boolean condition, and <code>.ifLet</code> which works like Swift’s <code>if let</code> statement – providing non-optional access to optional values inside its closure. Both modifiers help reduce boilerplate code in SwiftUI views.</p><h3 id="app-lifecycle-handling">App Lifecycle Handling</h3><p>Respond to app state changes elegantly:</p><pre><code class="language-swift">struct MediaPlayerView: View {
    @StateObject private var player = VideoPlayer()

    var body: some View {
        PlayerContent(player: player)
            .onAppResignActive {
                // Pause playback when app goes to background
                player.pause()
            }
            .onAppBecomeActive {
                // Resume state when app becomes active
                player.checkPlaybackState()
            }
    }
}</code></pre><p>These modifiers work together to create a more fluid and maintainable SwiftUI development experience, reducing boilerplate code while enhancing the quality and consistency of your user interface.</p><h3 id="delete-confirmation-dialogs">Delete Confirmation Dialogs</h3><p>SwiftUI’s confirmation dialogs require repetitive boilerplate code for delete actions, especially when deleting items from a list:</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 simplifies this with a dedicated 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 deletion in <a href="https://crosscraft.app/">CrossCraft</a> with a confirmation dialog to avoid accidental deletes.</p></blockquote><p>The example shows how <code>.confirmDeleteDialog</code> handles the entire deletion flow – from confirmation to execution – with a single modifier. The dialog is automatically localized in ~40 languages and follows platform design guidelines. You can provide an optional <code>message</code> parameter in case you need to provide a different message. There’s also an overload that takes a boolean for situations where no list is involved.</p><h2 id="get-started-today">Get Started Today</h2><p>I hope you find these modifiers as useful in your projects as I do in mine. If you have ideas for improvements or additional modifiers that could benefit the SwiftUI community, please feel free to contribute on GitHub:</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>This is the second in a series of four articles exploring HandySwiftUI’s features. Check out the previous article about <a href="https://www.fline.dev/handyswiftui-new-types/">New Types</a> if you haven’t already, and stay tuned for upcoming posts about <a href="https://www.fline.dev/handyswiftui-extensions/">Extensions</a> and <a href="https://www.fline.dev/handyswiftui-styles/">Styles</a>!</p>]]></content:encoded>
</item>
<item>
<title>HandySwiftUI New Types: Essential Views and Types for SwiftUI Development</title>
<link>https://fline.dev/blog/handyswiftui-new-types/</link>
<guid isPermaLink="true">https://fline.dev/blog/handyswiftui-new-types/</guid>
<pubDate>Wed, 30 Oct 2024 00:00:00 +0000</pubDate>
<description><![CDATA[From platform-specific values without #if checks to sophisticated selection controls and async state management - discover the essential SwiftUI types that helped ship apps faster. These battle-tested views and types fill common gaps in SwiftUI development.]]></description>
<content:encoded><![CDATA[<p>After 4 years of iterating on these APIs in my own apps, I’m happy to share the first tagged release of <a href="https://github.com/FlineDev/HandySwiftUI">HandySwiftUI</a>. This package contains various utilities and convenience APIs that were essential in helping me ship 10 apps in the past year alone. It provides conveniences for SwiftUI development similar to how my <a href="https://github.com/FlineDev/HandySwift">HandySwift</a> package does for Foundation.</p><p>In this article, I’ll share a selection of the <em>new types</em> I’ve found most valuable in my daily development work across apps like <a href="https://translatekit.app/">TranslateKit</a>, <a href="https://freemiumkit.app/">FreemiumKit</a>, and <a href="https://crosscraft.app/">CrossCraft</a>. While HandySwiftUI contains many more utilities, these particular types have proven their worth time and time again in real-world applications and could be helpful for your SwiftUI projects as well.</p><h3 id="platform-specific-values">Platform-Specific Values</h3><p>HandySwiftUI provides an elegant way to handle platform-specific values:</p><pre><code class="language-swift">struct AdaptiveView: View {
    enum TextStyle {
        case compact, regular, expanded
    }

    var body: some View {
        VStack {
            // Different number values per platform
            Text(&quot;Welcome&quot;)
                .padding(Platform.value(default: 20.0, phone: 12.0))

            // Different colors per platform
            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>Getting a similar look across platforms for a title 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> works with any type - from simple numbers to colors, fonts, or your own custom types. Just provide a default and override specific platforms as needed. This can be enormously useful, especially given that it even has a specific case for iPad named <code>pad</code>, so you can even address phones and tablets separately.</p><p>This is by far my most-used HandySwiftUI helper saving me a ton of boilerplate <code>#if</code> checks. It’s simple but so powerful!</p><h3 id="readable-preview-detection">Readable Preview Detection</h3><p>Provide fake data and simulate loading states during development:</p><pre><code class="language-swift">Task {
   loadState = .inProgress

   if Xcode.isRunningForPreviews {
       // Simulate network delay in previews
       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> allows you to bypass actual network requests and instead provide instant or delayed fake responses in SwiftUI previews only, making it perfect for prototyping and UI development. It’s also useful to avoid consuming limited resources during development, such as API rate limits, analytics events that could distort statistics, or services that charge per request – just wrap these in a <code>if !Xcode.isRunningForPreviews</code> check.</p><h3 id="efficient-image-loading">Efficient Image Loading</h3><p><code>CachedAsyncImage</code> provides efficient image loading with built-in 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>Note that <code>.resizable()</code> and <code>.aspectRatio(contentMode: .fill)</code> are already applied to the <code>Image</code> view inside it.</p><h3 id="enhanced-selection-controls">Enhanced Selection Controls</h3><p>Multiple sophisticated picker types for different use cases:</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 {
            // Vertical option picker with icons
            VPicker(&quot;Select Mood&quot;, selection: $selectedMood)

            // Horizontal picker with custom styling
            HPicker(&quot;Rate your experience&quot;, selection: $selectedMood)

            // Multi-selection with platform-adaptive UI
            MultiSelector(
                label: { Text(&quot;Colors&quot;) },
                optionsTitle: &quot;Select Colors&quot;,
                options: [.red, .blue, .green],
                selected: $selectedColors,
                optionToString: \.description
            )

            // Searchable grid picker for emoji or SF Symbol selection
            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 includes <code>Emoji</code> and <code>SFSymbol</code> enums that contain common emoji and symbols. You can also create custom enums by conforming to <code>SearchableOption</code> and providing <code>searchTerms</code> for each case to power the search functionality.</p><h3 id="async-state-management">Async State Management</h3><p>Track async operations with type-safe state handling using <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>The example demonstrates handling all states in a type-safe way:</p><ul><li><p><code>.notStarted</code> shows the initial load button</p></li><li><p><code>.inProgress</code> displays a loading indicator</p></li><li><p><code>.failed</code> shows the error with a retry option</p></li><li><p><code>.successful</code> presents the loaded content</p></li></ul><h3 id="bring-nsopenpanel-to-swiftui">Bring <code>NSOpenPanel</code> to SwiftUI</h3><p>Bridging native macOS file access into SwiftUI, particularly useful for handling security-scoped resources:</p><pre><code class="language-swift">struct SecureFileLoader {
    @State private var apiKey = &quot;&quot;

    func loadKeyFile(at fileURL: URL) async {
        #if os(macOS)
        // On macOS, we need user consent to access the file
        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>The example taken right out of <a href="https://freemiumkit.app/">FreemiumKit</a> demonstrates how <code>OpenPanel</code> simplifies handling security-scoped file access for dragged items on macOS while maintaining cross-platform compatibility.</p><h3 id="vertical-tab-navigation">Vertical Tab Navigation</h3><p>An alternative to SwiftUI’s <code>TabView</code> that implements sidebar-style navigation commonly seen in macOS and iPadOS apps:</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  // Places settings at the bottom
        ) { 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> provides a vertical sidebar with icons and labels, optimized for larger screens with support for bottom-aligned tabs. The view automatically handles platform-specific styling and hover effects.</p><h2 id="get-started-today">Get Started Today</h2><p>I hope you find these types as useful in your projects as I do in mine. If you have ideas for improvements or additional types that could benefit the SwiftUI community, please feel free to contribute on GitHub:</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>This is the first in a series of four articles exploring HandySwiftUI’s features. Stay tuned for upcoming posts about <a href="https://www.fline.dev/handyswiftui-view-modifiers/">View Modifiers</a>, <a href="https://www.fline.dev/handyswiftui-extensions/">Extensions</a>, and <a href="https://www.fline.dev/handyswiftui-styles/">Styles</a>!</p>]]></content:encoded>
</item>
<item>
<title>Test your Swift Packages Linux Compatibility on Mac</title>
<link>https://fline.dev/blog/test-your-swift-packages-linux-compatibility-on-mac/</link>
<guid isPermaLink="true">https://fline.dev/blog/test-your-swift-packages-linux-compatibility-on-mac/</guid>
<pubDate>Tue, 22 Oct 2024 00:00:00 +0000</pubDate>
<description><![CDATA[Ever wondered how to test your Swift code's compatibility with Linux from your Mac without diving into Docker? In this article, I'll share a simple command that makes the process effortless!]]></description>
<content:encoded><![CDATA[<p>Recently, I encountered a situation where I needed to extract Swift networking code from one of my applications for reuse on a Vapor server. While the code built perfectly on my Mac, I ran into multiple errors when deploying it to my server. This prompted me to look for a way to easily test the code’s compatibility with Linux, which is the OS typically running on servers. Luckily, I found <a href="https://oleb.net/2020/swift-docker-linux/">this article</a> by Ole Begemann from 2020 that saved me from having to learn Docker.</p><p>In his article, Ole provides a straightforward approach to running Swift code in a Linux environment with a single command. All you need to do is to install the free <a href="https://www.docker.com/products/docker-desktop/">Docker Desktop App</a> on your Mac. But the command seemed quite lengthy and hard to remember, so I wanted to simplify things even further. I ended up with an approach where all I need to remember was <code>swift-linux</code>. Here’s how:</p><h2 id="simplifying-the-docker-command">Simplifying the Docker Command</h2><p>First, I decided to further simplify Ole’s command for brevity:</p><pre><code class="language-zsh">docker run --rm -it -v &quot;$(pwd):/src&quot; -w &quot;/src&quot; swift</code></pre><p><strong>Explanation of the Command (in case you’re interested):</strong></p><ul><li><p><code>docker run</code>: This command creates and starts a container.</p></li><li><p><code>--rm</code>: Automatically removes the container when it exits.</p></li><li><p><code>-it</code>: Runs the container in interactive mode with a terminal attached.</p></li><li><p><code>-v &quot;$(pwd):/src&quot;</code>: Mounts the current directory (<code>$(pwd)</code>) to <code>/src</code> in the container, allowing you to access your Swift files.</p></li><li><p><code>-w &quot;/src&quot;</code>: Sets the working directory in the container to <code>/src</code>.</p></li><li><p><code>swift</code>: Specifies the Swift Docker image to use.</p></li></ul><p>Note that I also removed the <code>--privileged</code> option from Ole’s command for security reasons since it is rarely needed to test Swift packages.</p><h2 id="adding-a-convenient-alias">Adding a Convenient Alias</h2><p>To make running this command easier, I added an alias to my <code>~/.zshrc</code> file (e.g. via <code>touch ~/.zshrc</code>). This is a configuration file for the Zsh shell, which is the default shell on macOS nowadays. Here’s how you can do it:</p><ul><li><p><strong>Open the Terminal</strong></p></li><li><p><strong>Edit the <code>~/.zshrc</code> file</strong>
You can use TextEdit, which is the pre-installed text editor on macOS.
Simply run: <code>open ~/.zshrc</code></p></li><li><p><strong>Add the alias</strong>
Scroll to the bottom of the file and add the following line:</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>Save and close the file</strong></p></li><li><p><strong>Apply the changes</strong>
Run the following command to reload your config file: <code>source ~/.zshrc</code></p></li></ul><h2 id="running-commands-in-a-linux-environment">Running Commands in a Linux Environment</h2><p>Now that the alias is set up, you can easily test your Swift code in a Linux environment directly from your Mac. Simply navigate to your project directory in the terminal and run:</p><pre><code class="language-bash">swift-linux</code></pre><blockquote><p>☕ On first run, the download of the Linux container will take some time.</p></blockquote><p>This command will drop you into a Linux environment where you can execute <code>swift build</code> or <code>swift test</code> depending on your needs. Just type <code>exit</code> to return back to your Mac. This allows you to quickly see if everything works as expected and to catch any errors before deploying to your server.</p><p>And all you need to remember is <code>swift-linux</code>!</p>]]></content:encoded>
</item>
<item>
<title>Introducing LinksKit: Simplifying App Links for Swift Developers</title>
<link>https://fline.dev/blog/introducing-linkskit-simplifying-app-links-for-swift-developers/</link>
<guid isPermaLink="true">https://fline.dev/blog/introducing-linkskit-simplifying-app-links-for-swift-developers/</guid>
<pubDate>Wed, 09 Oct 2024 00:00:00 +0000</pubDate>
<description><![CDATA[Tired of repeatedly implementing essential links in your apps? LinksKit is the Swift package that handles it all—from legal requirements to cross-promotion—saving you time and boosting your app's visibility.]]></description>
<content:encoded><![CDATA[<p>As an indie developer who has released nine apps in the past two years, I’ve encountered a recurring challenge: implementing essential links in each app. This repetitive task led me to create LinksKit, a Swift package designed to streamline the process of adding crucial links to your iOS, macOS, and visionOS apps.</p><h2 id="why-i-built-linkskit">Why I Built LinksKit</h2><p>After launching multiple apps across various Apple platforms, I noticed a pattern. Each app required similar link sections:</p><ol><li><p>Legal links (privacy policy, terms of use)</p></li><li><p>Support links (FAQs, contact email)</p></li><li><p>App review deep link</p></li><li><p>Social media links</p></li><li><p>Cross-promotion for my other apps</p></li></ol><p>Implementing these links became a time-consuming and repetitive process. And it was very different on the Mac, causing extra work for multi-platform apps. That’s when I realized the need for a reusable solution, and <a href="https://github.com/FlineDev/LinksKit">LinksKit</a> was born.</p><h2 id="why-every-new-app-should-use-linkskit">Why Every New App Should Use LinksKit</h2><ol><li><p><strong>Time-Saving</strong>: LinksKit provides a ready-to-use solution for common link requirements, allowing developers to focus on core app features.</p></li><li><p><strong>Compliance</strong>: It ensures your app includes essential legal links, helping you meet App Store guidelines effortlessly.</p></li><li><p><strong>Cross-Promotion</strong>: LinksKit makes it easy to showcase your other apps, potentially increasing your overall app portfolio’s visibility.</p></li><li><p><strong>Networking Opportunities</strong>: The package allows you to promote apps from fellow developers, fostering a supportive community and potential cross-promotion partnerships.</p></li><li><p><strong>Customization</strong>: While offering pre-built solutions, LinksKit also allows for full customization to fit your specific needs.</p></li><li><p><strong>Platform Adaptability</strong>: It works seamlessly across iOS, macOS, and visionOS, adapting to each platform’s UI conventions.</p></li></ol><h2 id="how-to-use-linkskit">How to Use LinksKit</h2><p>Getting started with LinksKit is straightforward, but before we dive into the code, let’s discuss an important concept: the <code>providerToken</code>.</p><h3 id="understanding-the-providertoken">Understanding the <code>providerToken</code></h3><p>The <code>providerToken</code> is a crucial component in App Store marketing campaigns. It’s a unique identifier for your developer account that helps track the effectiveness of your marketing efforts. When users click on links with your <code>providerToken</code>, Apple can attribute those clicks to your campaigns, providing valuable insights into your app’s user acquisition. The word “campaign” makes it sound overly complicated, but it’s really just a parameter indicating where users are coming from.</p><h4 id="how-to-obtain-your-providertoken">How to Obtain Your <code>providerToken</code></h4><p>Finding your <code>providerToken</code> isn’t immediately obvious, but here’s how to do it:</p><ol><li><p>Log in to App Store Connect</p></li><li><p>Navigate to Analytics &gt; Acquisition &gt; Campaigns</p></li><li><p>Click on “Create Campaign Link”</p></li><li><p>Enter any text in the form (you don’t need to actually create a campaign)</p></li><li><p>In the Campaign Link preview, look for the <code>pt</code> parameter - the value after <code>pt=</code> is your <code>providerToken</code></p></li></ol><p>Remember, this token is the same for all your apps, so you only need to look it up once.</p><h3 id="basic-configuration">Basic Configuration</h3><p>Now that you understand the <code>providerToken</code>, let’s set up LinksKit in your app:</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>This basic setup adds essential help and legal links to your app, including terms.</p><h3 id="comprehensive-example">Comprehensive Example</h3><p>Let’s explore a more comprehensive example that showcases all built-in features LinksKit offers with social links, and links to <em>your</em> apps and your <em>friends</em> apps:</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;)),
])

// Configure LinksKit
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>The above configuration I took (and simplified) from my app <a href="https://translatekit.app/">TranslateKit</a>:</p><ol><li><p>Sets up help links with an FAQ and support email</p></li><li><p>Adds social media links for both the app and the developer</p></li><li><p>Creates a menu to promote my other apps</p></li><li><p>Includes a section to promote friends’ apps (great for networking!)</p></li><li><p>Ensures all necessary legal links are present</p></li></ol><p>To use these configured links, simply add <code>LinksView()</code> to your settings screen:</p><pre><code class="language-swift">Form {
   // Other settings...
   LinksView()
}</code></pre><p>The result looks something like this on iOS:</p><p><img src="/assets/images/blog/introducing-linkskit-simplifying-app-links-for-swift-developers/phone-settings.webp" alt="Phone settings" loading="lazy" /></p><p>On the Mac, it is more common to place links into the help menu. Do it like this:</p><pre><code class="language-swift">WindowGroup {
   // your UI code
}
.commands {
   CommandGroup(replacing: .help) {
      LinksView()
         .labelStyle(.titleAndIcon)
   }
}</code></pre><p>It will appear like this in the menu:</p><p><img src="/assets/images/blog/introducing-linkskit-simplifying-app-links-for-swift-developers/mac-help-menu.webp" alt="Mac help menu" loading="lazy" /></p><h2 id="custom-link-sections">Custom Link Sections</h2><p>While LinksKit provides pre-built sections, you can also create entirely custom sections using <code>LinkSection</code>. This flexibility allows you to add any type of link or nested menu structure you need.</p><p>Here’s an example of a custom section:</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>This example demonstrates how you can nest menus and create a hierarchy of links to suit your app’s needs.</p><h2 id="custom-label-styles">Custom Label Styles</h2><p>LinksKit offers flexibility not just in content, but also in presentation. You can easily customize the appearance of your links using SwiftUI’s <code>labelStyle</code> modifier. LinksKit provides three custom label styles to enhance the visual appeal of your links:</p><ol><li><p><code>.titleAndTrailingIcon</code>: Similar to the default <code>.titleAndIcon</code> style, but places the icon at the trailing end of the label.</p></li><li><p><code>.titleAndIconBadge(color:)</code>: Mimics Apple’s Settings app style by adding a colored background to the leading icons.</p></li><li><p><code>.titleAndTrailingIconBadge(color:)</code>: Combines the trailing icon placement with the colored background.</p></li></ol><p>To apply these styles, simply add the <code>labelStyle</code> modifier to your <code>LinksView</code>:</p><pre><code class="language-swift">LinksView()
    .labelStyle(.titleAndIconBadge(color: .blue))</code></pre><p>This simple modification can significantly alter the look and feel of your links, allowing you to match your app’s design language or create a distinct visual hierarchy. Here’s a visual comparison of these styles:</p><p><img src="/assets/images/blog/introducing-linkskit-simplifying-app-links-for-swift-developers/links-kit.webp" alt="Links kit" loading="lazy" /></p><p>By leveraging custom label styles, you can create a unique and polished look for your app’s link section, enhancing the overall user experience.</p><h2 id="conclusion">Conclusion</h2><p>LinksKit is more than just a time-saving tool; it’s a comprehensive solution for managing app links, ensuring compliance, and fostering developer networking. By handling everything from legal requirements to cross-promotion, LinksKit allows you to focus on what truly matters – creating great apps.</p><p>Whether you’re a seasoned developer with multiple apps or just starting your first project, LinksKit can significantly streamline your development process. Give it a try in your next project and experience the benefits of simplified link management across all Apple platforms! I hope it helps.</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>Why I Stopped Building for visionOS (And What Could Bring Me Back)</title>
<link>https://fline.dev/blog/why-i-stopped-building-for-visionos-and-what-could-bring-me-back/</link>
<guid isPermaLink="true">https://fline.dev/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[Explore the limitations hindering the Vision Pro from reaching its full potential. This article highlights the missing APIs essential for transforming it into a true mixed-reality platform and discusses what needs to change for that to happen.]]></description>
<content:encoded><![CDATA[<p>When Apple first introduced the Vision Pro, I was incredibly excited. The platform’s potential felt boundless, and as a developer, it seemed like a new frontier to explore. I jumped in headfirst, seeing a rare opportunity for experimentation and innovation. However, after releasing a few apps and working extensively with visionOS, I realized I was facing limitations that shouldn’t have existed.</p><p>I had several unique app ideas, but I found myself restricted by a lack of APIs that would make those concepts feasible. I would have released more apps if visionOS had delivered the tools necessary for creating the AR/VR experiences the device promised. This isn’t about wanting a more open platform like macOS; I understand the value of the security and simplicity in the iOS/iPadOS ecosystem. Yet, visionOS feels like just iPadOS with different input options. What’s missing are unique APIs designed to fully leverage the immersive experiences that the Vision Pro can offer.</p><p>Here are the five APIs that I believe could transform visionOS from a promising experiment into a platform truly worth developing for:</p><h3 id="1-magnetically-pinning-windows-and-objects-to-surfaces">1. Magnetically Pinning Windows and Objects to Surfaces</h3><p><img src="/assets/images/blog/why-i-stopped-building-for-visionos-and-what-could-bring-me-back/kristyna-squared-one.webp" alt="a woman’s hand is holding a magnet on a refrigerator" loading="lazy" />
<em>Photo by <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>Imagine being able to pin windows or 3D objects to walls or furniture in a room—permanently. This feature would allow users to create persistent setups in their environment, enabling developers to toggle the pinning feature based on the context of their experience. For instance, my <a href="https://apps.apple.com/app/apple-store/id6478062053?pt=549314&ct=fline.dev&mt=8">Posters</a> app would finally make sense! More importantly, these pinned objects should remain in place even after restarting the system, similar to desktop environments or focus modes. This functionality should also extend across different users—when switching to guest mode, all pinned items should stay exactly where they were placed.</p><p>Currently, augmenting reality feels temporary on the Vision Pro due to the lack of this API, making it more of a VR-only device, which I don’t believe aligns with Apple’s vision for the platform. This limitation restricts the full potential of mixed-reality experiences, as users cannot rely on their digital objects staying put in their physical space. Therefore, the ability to magnetically pin windows and objects is the most crucial API needed to elevate the Vision Pro beyond a VR experience and realize its true mixed-reality potential.</p><h3 id="2-advanced-room-scanning-with-editable-3d-models">2. Advanced Room Scanning with Editable 3D Models</h3><p><img src="/assets/images/blog/why-i-stopped-building-for-visionos-and-what-could-bring-me-back/dynamic-wang-unsplash.webp" alt="a room with a large ball" loading="lazy" />
<em>Photo by <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>A hybrid API combining the power of RoomPlan and the 3D scanner available on iOS could revolutionize developers’ ability to create immersive content. While scanning a room is currently possible, it lacks the depth required for fully interactive spaces. The next step should allow developers to capture not only the dimensions but also the colors and textures, producing high-fidelity 3D models.</p><p>Why aren’t these scanning APIs available on the Apple Vision Pro? They require a LiDAR sensor, which the Vision Pro has. Creating them on the Vision Pro would be more comfortable, allowing users to see the results in 3D immediately. Ideally, Apple could also ship an app for developers to edit these environments directly within the Vision Pro using hand gestures. For instance, if I move a table and rescan the room, the system should recognize the change and treat the table as a movable object. Apple’s AI tools could fill in gaps with realistic textures and objects, making 3D environment creation more intuitive and reducing the need for complex CAD tools.</p><h3 id="3-a-skeletal-recognition-api-for-fun-ar-interactions">3. A Skeletal Recognition API for Fun AR Interactions</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 of skeletal recognition" loading="lazy" /></p><p>One of my favorite app ideas involved body part detection using Apple’s AR frameworks to bring a bit of <em>The Sims</em> into the real world. Imagine walking around and seeing a floating green diamond (like the Sims’ plumbob) above each person, allowing you to interact with them in unique and playful ways. To make this possible, a simple skeleton tracking API would be a game-changer, enabling developers to recognize body movements and gestures. Even a rough estimation of head position or arm movement could unlock features like interactive speech bubbles based on what someone is saying or feeling. Advanced APIs could detect facial expressions, creating opportunities for floating emotion icons or interaction suggestions akin to those in <em>The Sims</em>.</p><p>I understand that Apple doesn’t want to give full camera access—and honestly, I wouldn’t use the Vision Pro if they did. However, since the camera is already on, the system could share detected world information that respects user privacy while enabling these kinds of experiences. There’s still a lot of untapped potential that could enhance AR interactions significantly.</p><h3 id="4-interactive-spatial-360-and-180-video-elements">4. Interactive Spatial 360° and 180° Video Elements</h3><p><img src="/assets/images/blog/why-i-stopped-building-for-visionos-and-what-could-bring-me-back/roger-ce-unsplash.webp" alt="a toy car with its hood open sitting on the ground" loading="lazy" />
<em>Photo by <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>While the Vision Pro promises immersive video experiences, it’s currently impossible to integrate interactive buttons or objects into these spatial 180°/360° videos. Developers should be able to place clickable elements within space while inside a video—imagine offering interactive tours or guided experiences where users can click on objects to receive more information or switch between videos showing different times or perspectives.</p><p>This feature could also enable users to “time travel” within a location by seamlessly switching video recordings of the same place at different times. Right now, such interactions require painstakingly building 3D environments from scratch. A simple API for placing interactive elements within videos would vastly simplify the process and open up endless possibilities for interactive storytelling, educational tools, and more. Currently, a lot of effort is needed to come even close to such an experience, and includes building a custom video player inside RealityKit with different images for each eye. Hacks like this shouldn’t be needed.</p><h3 id="5-discovering-the-world-the-future-of-apple-maps">5. Discovering the World: The Future of 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="people walking on road near well-lit buildings" loading="lazy" />
<em>Photo by <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>One of the most surprising gaps in visionOS is the absence of a native Maps app optimized for the Vision Pro. While <em>Look Around</em> offers immersive street-level views on other devices, the Vision Pro could elevate this experience by placing users directly into 3D environments where they can interact with their surroundings in real-time. Apple’s AI technology, which turns photos into 3D scenes, could be used to convert <em>Look Around</em>’s existing imagery into interactive environments, unlocking new possibilities for immersive, location-based apps.</p><p>A proper Vision Pro Maps API could transform this experience entirely. Imagine walking (or rather beaming yourself) through a city, interacting with virtual objects tied to specific locations—whether for a game, educational purposes, or even an immersive travel planner. Developers could create experiences where users navigate through historical reconstructions, explore future city plans, or engage with dynamic content as they roam the streets.</p><h2 id="the-key-to-vision-pros-success">The Key to Vision Pro’s Success</h2><p><img src="/assets/images/blog/why-i-stopped-building-for-visionos-and-what-could-bring-me-back/razvan-chisu-unsplash.webp" alt="low-angle photography of man in the middle of buidligns" loading="lazy" />
<em>Photo by <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>These are the kinds of APIs that would not only keep developers like me invested in visionOS but also drive the creation of more immersive and compelling content. Right now, the Vision Pro lacks the killer apps that make users want to buy it, and without enough users, developers are hesitant to invest significant time in the platform. It’s a cycle that needs to be broken, and the solution is clear—Apple must make content creation as simple as possible for developers.</p><p>I’m not asking for an open system or far-fetched features—I’m asking for APIs that can unleash the real potential of the Vision Pro for developers and users alike. Without them, it’s hard to see how the platform will gain the momentum it needs. But if Apple introduces even just a few of these APIs, I’d be ready to jump back in and build apps that could push the boundaries of AR and VR, ultimately making the Vision Pro the breakthrough device it was meant to be.</p>]]></content:encoded>
</item>
<item>
<title>Guest Post: Why I Chose FreemiumKit Over RevenueCat for My App</title>
<link>https://fline.dev/blog/why-i-chose-freemiumkit-over-revenuecat-for-my-diabetes-management-app-guest-post/</link>
<guid isPermaLink="true">https://fline.dev/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[Struggling with in-app subscription integration? Discover how FreemiumKit transformed my development process, helping me overcome challenges with RevenueCat and fast-tracking my app's launch.]]></description>
<content:encoded><![CDATA[<h2 id="the-journey-so-far">The Journey So Far</h2><p>I’ve been working on a Diabetes management app, which I’ve named <em>Glu Sight</em>, for about a year and half now. As the launch date approached, I found myself needing to cut down on features, tie up the loose ends, and finally prepare for release. Not everything has to be ready from Day One, I reminded myself. Some features can wait, but launching with a solid foundation was essential. This is where my decision-making process about handling in-app subscriptions and purchases came in.</p><h2 id="revenuecat-a-tough-start">RevenueCat: A Tough Start</h2><p>As I approached the point of integrating in-app purchases, I started hearing tons of great things about RevenueCat. It seemed like the go-to solution for app developers, especially with its powerful features and reputation in the community. So, naturally, I considered using RevenueCat or the StoreKit 2 API directly. I reached out to others who had used RevenueCat, seeking tips on getting started.</p><p>However, what I soon discovered was that the initial setup process for RevenueCat was a lot more challenging than I expected. The procedures were complex, and to make matters worse, the documentation was lacking in areas where I needed guidance the most. Setting up the basics took time and felt like a struggle, especially when I was already under pressure to get my app ready for launch. Though I did manage to get through the setup, it wasn’t the most pleasant experience.</p><h2 id="discovering-freemiumkit-a-breath-of-fresh-air">Discovering FreemiumKit: A Breath of Fresh Air</h2><p>Then I stumbled upon FreemiumKit. After reading some positive reviews, I decided to give it a try. From the get-go, I noticed a stark difference. The setup with FreemiumKit was unbelievably simple. I was able to integrate it into my app much faster than any other solution I had tried. The SDK integration documentation was crystal clear—one read and I was good to go. It felt like everything just clicked.</p><p><img src="/assets/images/blog/why-i-chose-freemiumkit-over-revenuecat-for-my-diabetes-management-app-guest-post/discovering-freemiumkit.webp" alt="Discovering freemiumkit" loading="lazy" /></p><p>FreemiumKit didn’t just make the setup process easy; it also came packed with smart automation and defaults that worked flawlessly out of the box with App Store Connect. And yet, it still provided the flexibility to tailor things to my specific needs. The pricing was also spot on, especially for an app developer like me who’s just starting out. The support from the developers? Stellar. I had a call with the developer, which turned out to be an amazing experience. He walked me through the process, and within 45 minutes, I had clarity on aspects of in-app purchases that even Apple’s documentation didn’t cover in detail.</p><h2 id="the-impact-on-my-development-process">The Impact on My Development Process</h2><p>Switching to FreemiumKit had an incredible impact on my development process. I was able to clean up a significant amount of code, removing extra classes and unnecessary complexity that RevenueCat required. This cleanup wasn’t just about aesthetics—it made my app more efficient and easier to manage. All the RevenueCat stuff was gone, replaced with FreemiumKit, and I was loving it.</p><p>Built-in SDK components like <code>PaidFeatureView</code> and <code>PaidStatusView</code> were incredibly customizable, allowing me to focus on the user experience without worrying about the technical nitty-gritty. Instead of having to write an entire ViewModel for handling in-app purchases, I could use a one-liner from FreemiumKit. This freed me to concentrate on what mattered most: building a great app.</p><p>Moreover, FreemiumKit made the daunting aspects of StoreKit2 feel manageable. By playing around with FreemiumKit, I gained a better understanding of StoreKit2, which was a huge value add. It turned what seemed like an overwhelming task into something enjoyable and highly educational.</p><h2 id="a-clear-path-to-launch">A Clear Path to Launch</h2><p>Looking back, the major reason I hadn’t launched my app earlier was the overwhelming work required to integrate RevenueCat. I had started building the paywall and coding everything, but the complexity wore me down. I ended up procrastinating because it felt like too much work to revisit that part of the app.</p><p>FreemiumKit changed all of that. It allowed me to both focus on developing new features and integrate subscription management without anxiety. I even managed to make changes on App Store Connect to my trial periods, and FreemiumKit picked it all up seamlessly. The anxiety and “developer block” were gone, and I was back to feeling excited about hitting my launch day.</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="final-thoughts">Final Thoughts</h2><p>FreemiumKit has been a game-changer for me. Not only did it simplify my development process, but it also reinvigorated my passion for launching <em>Glu Sight</em>. The setup was smooth, the documentation was top-notch, and the support from the developers was incredible. For anyone facing similar challenges with in-app purchase management, I can’t recommend FreemiumKit enough. It’s helped me move past the obstacles that RevenueCat presented and focus on delivering a product I’m proud of.</p><p>As I inch closer to launching in September 2024, I’m excited and ready, thanks to FreemiumKit. I’ve made new friends along the way, learned a ton, and most importantly, I’m finally on the path to helping others manage their diabetes with ease.</p><p>Stay tuned for the launch!</p><blockquote><p>👨‍💻 <strong>Don’t miss the launch post!</strong>
Follow me on <a href="https://www.threads.net/@slowbrewed.studio">Threads</a> and <a href="https://iosdev.space/@Jeehut">Mastodon</a>.</p></blockquote><blockquote><p>💁 <strong>Enjoyed this article? Check out FreemiumKit!</strong>
Simple in-app purchases with paywalls, A/B testing, and live push!
<a href="https://freemiumkit.app/"><strong>Get it now</strong></a> or watch the <a href="https://www.youtube.com/watch?v=6JxwA3WieHs">video setup guide</a> to see it in action.</p></blockquote>]]></content:encoded>
</item>
<item>
<title>Introducing Pleydia Organizer: The Ultimate Native Mac App for Renaming TV &amp; Movie Files</title>
<link>https://fline.dev/blog/introducing-pleydia-organizer/</link>
<guid isPermaLink="true">https://fline.dev/blog/introducing-pleydia-organizer/</guid>
<pubDate>Mon, 05 Aug 2024 00:00:00 +0000</pubDate>
<description><![CDATA[Effortlessly organize your media library with Pleydia Organizer, an automated app that simplifies renaming TV and movie files. Discover unmatched speed, accuracy, and convenience in managing your media collection.]]></description>
<content:encoded><![CDATA[<p>I’m excited to announce the release of Pleydia Organizer, a cutting-edge native Mac app that I’ve developed specifically for anyone who loves to keep their media files organized on a Mac. Whether you’re using a Mac mini as your media server or any other Mac device, Pleydia Organizer leverages Apple’s latest technologies to deliver the best experience in renaming and organizing your TV and movie files.</p><p><img src="/assets/images/blog/introducing-pleydia-organizer/pleydia-organizer.webp" alt="Pleydia Organizer automatically matching Movies after drag &amp; drop" loading="lazy" />
<em>Pleydia Organizer automatically matching Movies after drag &amp; drop</em></p><h3 id="effortless-organization-with-drag-drop-simplicity">Effortless Organization with Drag &amp; Drop Simplicity</h3><p>Organizing your media library has never been easier. With Pleydia Organizer, simply drag and drop your TV or movie files into the app, and it will automatically search The Movie Database (TMDb) to identify and match your files accurately. One more click, and all your files are renamed to tidy, standardized filenames without any weird characters or formatting issues.</p><p><img src="/assets/images/blog/introducing-pleydia-organizer/pleydia-organizer-after.webp" alt="Pleydia Organizer after renaming matched TV episode files" loading="lazy" />
<em>Pleydia Organizer after renaming matched TV episode files</em></p><h3 id="why-choose-pleydia-organizer-over-other-tools">Why Choose Pleydia Organizer Over Other Tools?</h3><p>Pleydia Organizer stands out from other renaming tools like FileBot:</p><ul><li><p><strong>Super-Fast App Start:</strong> Pleydia Organizer launches almost instantly, while FileBot can take around 10 seconds on an M1 Pro Mac.</p></li><li><p><strong>Accurate Movie Matching:</strong> Pleydia Organizer excels in movie matching, especially when international release years differ from those in your country.</p></li><li><p><strong>Precise TV Episode Matching:</strong> The app handles edge cases in TV show season grouping better, such as with Animes like One Piece where episodes are released each week, which FileBot struggles with.</p></li><li><p><strong>Persistent TV Show Choices:</strong> Pleydia Organizer remembers your season grouping choices for TV shows, eliminating the need for repetitive selections every time you use the app.</p></li><li><p><strong>Free Renaming Options:</strong> You can rename one file at a time for free, making it accessible for light users. FileBot requires a purchase even for a single file.</p></li><li><p><strong>Affordable Multi-File Support:</strong> If you need multi-file support, it’s half the price of FileBot, making it an economical choice for managing larger libraries.</p></li><li><p><strong>Drag &amp; Drop Simplicity:</strong> Rather than you having to learn the app and having to click a lot of buttons, Pleydia Organizer is super easy to use.</p></li></ul><p>See how easy it is in in action with this GIF:</p><p><img src="/assets/images/blog/introducing-pleydia-organizer/just-drag-drop-your.gif" alt="Just drag &amp; drop your files into Pleydia Organizer, and it matches automatically." loading="lazy" />
<em>Just drag &amp; drop your files into Pleydia Organizer, and it matches automatically.</em></p><p>If you have your own Plex, Kodi, Jellyfin, or Infuse media server, Pleydia Organizer is definitely worth a try. Download it safely from the Mac App Store:</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>I appreciate any feedback, so please <a href="mailto:pleydia@fline.dev">let me know</a> if you run into any issues!</p>]]></content:encoded>
</item>
<item>
<title>Videos and Tabs in DocC Documentation</title>
<link>https://fline.dev/snippets/docc-videos-tabs-documentation/</link>
<guid isPermaLink="true">https://fline.dev/snippets/docc-videos-tabs-documentation/</guid>
<pubDate>Thu, 20 Jun 2024 00:00:00 +0000</pubDate>
<description><![CDATA[Two lesser-known DocC features that make your documentation more interactive: embedded videos and tabbed content navigation.]]></description>
<content:encoded><![CDATA[<h2 id="beyond-basic-markdown-in-docc">Beyond Basic Markdown in DocC</h2><p>Most developers know DocC supports standard markdown, but there are two powerful directives that are surprisingly underused: video embedding and tabbed content. Both work in Xcode’s documentation viewer and on hosted DocC websites.</p><h2 id="embedding-videos">Embedding Videos</h2><p>Adding a video to your documentation is a single directive:</p><pre><code>@Video(source: &quot;setup-walkthrough.mp4&quot;)</code></pre><p>Place the video file in your documentation catalog’s Resources folder. This renders an inline video player directly in the documentation, which is far more effective than linking to an external URL or describing a visual process in text. It works well for setup guides, UI walkthroughs, or demonstrating animations.</p><h2 id="tabbed-content-with-tabnavigator">Tabbed Content with TabNavigator</h2><p>When you need to show alternative approaches or platform-specific instructions, tabs keep things organized without overwhelming the reader:</p><pre><code>@TabNavigator {
   @Tab(&quot;SwiftUI&quot;) {
      Use the `.environment` modifier to inject dependencies.
   }
   @Tab(&quot;UIKit&quot;) {
      Override `viewDidLoad` and configure your dependencies there.
   }
}</code></pre><p>This renders as a proper tabbed interface where readers can switch between sections. It is especially useful for documentation that covers multiple platforms, API versions, or configuration approaches.</p><p><video src="/assets/images/snippets/docc-videos-tabs-documentation/demo.mp4" controls muted playsinline></video></p><h2 id="practical-advice">Practical Advice</h2><p>I updated my Contributing guide to use both of these features, and the result is noticeably more approachable than a wall of text. The video shows the setup process that would take paragraphs to describe, and the tabs separate platform-specific steps cleanly.</p><p>These directives are documented in Apple’s <a href="https://www.swift.org/documentation/docc/video">DocC documentation</a> but rarely mentioned in tutorials. If you maintain an open-source Swift package, consider adding them to your documentation catalog – they make a real difference in how people experience your docs.</p>]]></content:encoded>
</item>
<item>
<title>Reacting to Pricing Backlash: Lessons Learned</title>
<link>https://fline.dev/blog/reacting-to-pricing-backlash-lessons-learned/</link>
<guid isPermaLink="true">https://fline.dev/blog/reacting-to-pricing-backlash-lessons-learned/</guid>
<pubDate>Tue, 16 Apr 2024 00:00:00 +0000</pubDate>
<description><![CDATA[My take on how to find the right pricing and how I reacted to a user calling my app "overpriced". Learn from my mistakes and avoid bad reviews.]]></description>
<content:encoded><![CDATA[<p>Pricing is hard. You can never make everyone happy. I know all this. But it still depresses me for longer than I want to admit when I read a review like this:</p><p><img src="/assets/images/blog/reacting-to-pricing-backlash-lessons-learned/a-user-expressing-their.webp" alt="A user expressing their feelings with a negative App Store review." loading="lazy" />
<em>A user expressing their feelings with a negative App Store review.</em></p><p>It’s really tough to withstand the temptation to just follow their request to lower the price. And I haven’t always been able to withstand it as you can see here:</p><p><img src="/assets/images/blog/reacting-to-pricing-backlash-lessons-learned/a-user-on-reddit.webp" alt="A user on Reddit complaining about the price of an up-front paid app." loading="lazy" />
<em>A user on Reddit complaining about the price of an up-front paid app.</em></p><p>I’m a human after all and I want people to like what I do. But I also have to make money as an Indie developer or I can’t continue improving the app. So, it’s even in the interest of users that I put a fair price on it, not just mine. But what is <em>fair</em>?</p><h2 id="fair-pricing">“Fair” Pricing</h2><p>One could look at this from many different perspectives.</p><p>From an economical point of view, my goal should be to maximize my earnings. If you’re looking for an article about different strategies to do that, you’re better off reading an article like <a href="https://www.appsflyer.com/blog/tips-strategy/app-pricing-strategies/">this one</a>. To me, profit is just a means to keep me motivated to continue building apps. It’s not the goal. The user experience is all that matters. And solving problems. A fair price is part of a good UX to me.</p><p>A pure developer point of view could be to price an app based on the effort it required to make it. Sounds fair, right? But it doesn’t work in a free market. And for good reason! What if you’ve built something super complex but totally irrelevant? Why should anyone pay a high price for that? Would you?</p><p>That brings me to how I always tend to choose my initial prices. What would I pay if this app was built by someone else and I just wanted to use it? Because I exclusively build apps that I need myself, I’m also a customer! So it’s like a one-person customer research my initial pricing is based on. But is this a good idea?</p><p>At least it’s not if I want to avoid bad ratings like the 2-star one above. Why? Because I’m not only a customer, but I also know exactly what my app offers. But users don’t. They will only know what I’ve presented to them on the App Store page and in my Onboarding. And probably not even all of what I present there.</p><h2 id="the-users-perspective">The User’s Perspective</h2><p>So, what feels fair to a user is very much dependent on how the user perceives the value of an app. For example, the above bad rating I received for <a href="https://translatekit.app/">TranslateKit</a>. My preview video on the App Store page communicates the ease of use of the drag &amp; drop workflow to translate apps. But the user complained because they had to register for a 3rd-party translation service to use the marketed features.</p><p><img src="/assets/images/blog/reacting-to-pricing-backlash-lessons-learned/all-3rd-party-services.webp" alt="All 3rd-party services expanded linking to registration &amp; API key docs." loading="lazy" /></p><p>Of course, I mention that requirement in my App Store description. But who reads descriptions? Nobody! I could do what the user requested and provide built-in support for those translation services. But then I would have to pay for those services and add that price on top. And I would additionally have to host a proxy server to make sure my API keys don’t get exposed, adding even more costs.</p><p>That’s not good for most of my other customers though. After all, each translation service I support has a huge free tier that is more than enough to localize any size app to many languages! The only problem is the extra hurdle of registering a free account &amp; getting the API keys. I try to help by linking to the right pages:</p><p><img src="/assets/images/blog/reacting-to-pricing-backlash-lessons-learned/all-3rd-party-services-2.webp" alt="All 3rd-party services expanded linking to registration &amp; API key docs." loading="lazy" />
<em>All 3rd-party services expanded linking to registration &amp; API key docs.</em></p><p>That hurdle probably caused frustration on the users end who expected to simply “drag &amp; drop” their String Catalog after downloading the app like shown in the preview. But they couldn’t do it right away. And as a consequence, they expressed their feelings by criticizing the only thing they have actually knowledge about: The pricing. Because you can see it by clicking on “Upgrade” on the main window.</p><p><img src="/assets/images/blog/reacting-to-pricing-backlash-lessons-learned/the-top-section-of-the.webp" alt="The top section of the main window in TranslateKit." loading="lazy" />
<em>The top section of the main window in TranslateKit.</em></p><h2 id="addressing-the-complaint">Addressing the Complaint</h2><p>The easiest thing I could do to prevent users complaining about the pricing would be to hide the information about the paid tier until the user exceeds the Free limit. But I consider this a dark pattern, so I wouldn’t ever do that. And even if I did, they would probably just complain about something else, it doesn’t solve the issue.</p><p>Much more useful to prevent bad ratings would be if I provided more guidance and help to create those accounts, including maybe step-by-step guides or videos. But that’s just a matter of lowering the effort, it could still be too much for some!</p><p>The analysis above leads me to think that it’s actually not the pricing of my app that lead to the bad rating, but my inability to communicate its value. Users can’t know that my app not just connects String Catalogs with translation services, but that it also does all kinds of smart stuff behind the scenes to ensure the resulting translations are accurate. A simple way to communicate this is with an example:</p><p><img src="/assets/images/blog/reacting-to-pricing-backlash-lessons-learned/new-window-shown-on.webp" alt="New window shown on first app start to communicate app’s value." loading="lazy" />
<em>New window shown on first app start to communicate app’s value.</em></p><p>By showing the different stages a translation can walk through on app start, I’m explaining to my users what additional value my app brings on top of what’s already obvious from the Store page. Additionally, I should probably make these stages &amp; adjustments clear in my apps translation UI, which I might do in a future update. But it wouldn’t have helped to address the bad rating anyway since you would only ever see the translation UI after setting up at least one API key.</p><h2 id="conclusion">Conclusion</h2><p>Pricing is indeed hard and I’m still not 100% sure what I’m doing is right. And I probably never will. But at least I don’t feel bad about my price as my app <a href="https://x.com/KSlazinski/status/1774075673742901683?s=20">saves many hours of time</a> and costs much less than the average hourly rate. And I know people subscribe to the app from the numbers on App Store Connect. Each purchased subscription is someone telling me “the price is <em>not</em> too high”.</p><p>Just because a customer complained about the price, it doesn’t mean it’s wrong. The most depressing reviews can sometimes provide the most useful insights into the flaws in your apps Onboarding. Don’t ignore them outright. Try to understand where your users might be coming from first. And communicate your app’s value!</p>]]></content:encoded>
</item>
<item>
<title>2 New Vision Pro Apps: &quot;Guided Guest Mode&quot; &amp; &quot;FocusBeats: Pomodoro + Music&quot;</title>
<link>https://fline.dev/blog/2-new-vision-pro-apps-guided-guest-mode-focusbeats-pomodoro-music/</link>
<guid isPermaLink="true">https://fline.dev/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" elevates Apple Vision Pro demos with easy-to-follow guides for an immersive introduction to spatial computing. "FocusBeats: Pomodoro + Music" combines productivity-enhancing Pomodoro technique with themed music to boost focus during work sessions.]]></description>
<content:encoded><![CDATA[<h2 id="guided-guest-mode">Guided Guest Mode</h2><p>Own an Apple Vision Pro? Enhance your demo experience! Navigating friends through its features can be tricky &amp; it’s easy to miss out on showing some of its best aspects. But no more!</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="A short Demo video of Guided Guest Mode." loading="lazy" /></p><p><strong>Guided Guest Mode</strong> is designed to offer your friends &amp; family a smooth introduction to the future of spatial computing. Use the built-in guides inspired by Apples own demos to showcase the best experiences or create your own guides. A huge time-saver!</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>A short Demo video of Guided Guest Mode.</em></p><p><strong>Get it now:</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">Download Press Kit</a></p><h2 id="focusbeats-pomodoro-music">FocusBeats: Pomodoro + Music</h2><p><em>Also available on iPhone, iPad, and Mac</em></p><p>You like to work with background music? And you want to improve your productivity with the Pomodoro (25m / 5m) technique? Then this is the app you’ve been waiting for: It provides selected themes for music like movie soundtracks or classical music. And automatically plays &amp; pauses the music during focus &amp; break periods. Discover focus music for every mood!</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="A short Demo video of FocusBeats on Vision Pro." loading="lazy" /></p><p>To auto-play music, this app requires an Apple Music subscription. If you don’t have one, it’s serves as one of the best Pomodoro timers out there. It’s free to use for the default 25/5 Pomodoro system and select music themes.</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>A short Demo video of FocusBeats on Vision Pro.</em></p><p><strong>Try it Free:</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">Download Press Kit</a></p>]]></content:encoded>
</item>
<item>
<title>Introducing FreelanceKit: Time Tracking for all  Platforms!</title>
<link>https://fline.dev/blog/introducing-freelancekit/</link>
<guid isPermaLink="true">https://fline.dev/blog/introducing-freelancekit/</guid>
<pubDate>Tue, 09 Apr 2024 00:00:00 +0000</pubDate>
<description><![CDATA[Affordable & native time tracking that syncs across iPhone, iPad, Mac, and Vision. Watch your earned money update live. Export to CSV. And much more!]]></description>
<content:encoded><![CDATA[<h3 id="available-on-iphone-ipad-mac-and-apple-vision">Available on iPhone, iPad, Mac, and Apple Vision!</h3><p>FreelanceKit offers an affordable, easy-to-use time tracking solution that feels right at home on your Apple devices. Built with SwiftUI, this app is intuitively designed to ensure a smooth, native experience on all Apple platforms.</p><p><img src="/assets/images/blog/introducing-freelancekit/freelancekit-on-mac.gif" alt="FreelanceKit on Mac." loading="lazy" />
<em>FreelanceKit on Mac.</em></p><h3 id="key-features">Key Features:</h3><ul><li><p><strong>Seamless Synchronization</strong>: Start tracking on a Mac and stop on your iPhone. iCloud sync ensures your data is available across all devices.</p></li><li><p><strong>Mini Timer Mode</strong>: A distraction-free timer that makes pausing and resuming work effortless without taking too much of your screen.</p></li><li><p><strong>Multilingual Support</strong>: Available in over 30 languages, making it universally accessible.</p></li><li><p><strong>Live Earnings Tracking</strong>: Enter your hourly rate to see your earnings update in real-time to stay motivated!</p></li><li><p><strong>CSV Export</strong>: Export your tracked time to open in other tools with all the data you would expect. You can even customize the date format &amp; separator!</p></li></ul><p><img src="/assets/images/blog/introducing-freelancekit/freelancekit-on-apple.gif" alt="FreelanceKit on Apple Vision." loading="lazy" />
<em>FreelanceKit on Apple Vision.</em></p><h3 id="perfect-for-everyone">Perfect for Everyone:</h3><p>Whether you’re freelancing, studying, or managing projects, FreelanceKit is tailored to anyone looking to take control of their time. Its simplicity and cross-device compatibility mean you can focus on what matters most without losing track of time. Download FreelanceKit now to unlock your productivity potential. Start managing your projects and time more efficiently!</p><h3 id="get-it-now">Get it now:</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>Download FreelanceKit now to unlock your productivity potential. Start managing your projects and time more efficiently!</p>]]></content:encoded>
</item>
<item>
<title>My Top 10 Wishes for WWDC24</title>
<link>https://fline.dev/blog/my-top-10-wishes-for-wwdc24/</link>
<guid isPermaLink="true">https://fline.dev/blog/my-top-10-wishes-for-wwdc24/</guid>
<pubDate>Fri, 05 Apr 2024 00:00:00 +0000</pubDate>
<description><![CDATA[From a SportsKit API and .zoom modifier in SwiftUI, over improved SwiftData and source control in Xcode, to my biggest pain points in tvOS and visionOS, and much more! Blending long-standing requests with fresh ideas.]]></description>
<content:encoded><![CDATA[<p>It’s become a bit of a habit for me that shortly after Apple announces the dates for the next WWDC, I gather a list of the top things I’m missing in Apples tools and frameworks. My hope is that these summary articles are helpful for Apple engineers to internally confirm or adjust priorities. And so far each year 33% of what I wished for actually came true! Might be a coincidence, but I like it still.</p><p>Some of these ideas I filed a radar long ago. But more often than not, taking the time to reflect on the things I’m missing most brings up new things that I don’t come across in day-to-day work. So you might find a few obvious things in this list, but I’m sure I also have things you never thought about. I’d love to hear what you think, make sure to comment on socials and mention my handle @Jeehut.</p><blockquote><p>ℹ️ There are still 3 wishes from my <a href="https://www.fline.dev/my-top-5-wishes-for-wwdc-2023/">last years’ article</a> that were not shipped yet, and I still think we need them:</p><ol><li><p><strong>Easy App Modularization</strong> in Xcode without the hassle</p></li><li><p>Pie Charts are here, but <strong>Spider Charts</strong> are still missing!</p></li><li><p><strong>Streamer Mode</strong> in Xcode to hide sensitive code in calls/recordings</p></li></ol></blockquote><h2 id="1-a-weatherkit-like-api-for-apple-sports">#1 – A WeatherKit-like API for Apple Sports</h2><p>I was surprised when Apple introduced <a href="https://developer.apple.com/weatherkit/">WeatherKit</a> in 2022. It was the first system framework (I know of) that was not free for developers to use – but it still came with 500,000 calls per month for free. This was generous enough for Indie developers to write interesting new apps based on it. Apple had basically extracted the data portion of a system app and made it available to developers. And not just that, they also made it affordable enough for bootstrapped Indies!</p><p><img src="/assets/images/blog/my-top-10-wishes-for-wwdc24/the-new-apple-sports-app.webp" alt="The new Apple Sports app." loading="lazy" /></p><p><em>The new Apple Sports app.</em></p><p>The introduction of the <a href="https://apps.apple.com/us/app/apple-sports/id6446788829">Apple Sports</a> app in the US made me think: What if Apple did the same with the data of that app? I had multiple app ideas over the years that I prototyped but couldn’t release because of the lack of an affordable Live Sports Data API. Their pricing is targeted toward large corporations (like many APIs are). But given that the best way to experience a sports event nowadays is by using the Vision Pro, I think it would make a lot of sense for Apple to make it as easy as possible for Indies to explore new experiences in sports events.</p><blockquote><p>I wish for a new “SportsKit” API with a generous free tier like WeatherKit!</p></blockquote><h2 id="2-add-zoom-modifier-to-scrollview-in-swiftui">#2 – Add .zoom modifier to ScrollView in SwiftUI</h2><p>One of the first features requested for the playing screen of my crossword puzzle app <a href="https://apps.apple.com/app/apple-store/id6472669260?pt=549314&ct=fline.dev&mt=8">CrossCraft</a> was to zoom in and out to get a better overview of the related characters. But when I typed <code>.zoom</code> to add the related modifier to my <code>ScrollView</code> in SwiftUI, I was shocked to see that it’s not available! 😱</p><p>While there are workarounds using a custom <code>MagnifyGesture</code>, a <code>GeometryReader</code> based on the <code>.scaleEffect</code> modifier with custom <code>@State</code> modifiers, it only works for limited use cases such as zooming into images. But my view is a <code>ScrollView</code> which behaves very different compared to a regular view. I’ve tried multiple approaches, but couldn’t get it to work the way one would expect. And even if I did, zooming the contents of a scroll view is such a common use case that it should really be built-in to <code>ScrollView</code> and easy to use without all the hassle.</p><blockquote><p>I wish for a <code>.zoom(scale:offset:)</code> modifier for <code>ScrollView</code> in SwiftUI!</p></blockquote><h2 id="3-fix-swiftdata-limitations-on-cloudkit">#3 – Fix SwiftData limitations on CloudKit</h2><p>Paul Hudson has put it perfectly in his great <a href="https://www.hackingwithswift.com/quick-start/swiftdata/how-to-sync-swiftdata-with-icloud">SwiftData + CloudKit article</a>:</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>There’s really not much to add to this other than that I had to write a lot of computed properties in all of my models to map Optionals to non-Optionals. It’s safe, my apps aren’t crashing. So why isn’t it built-in to some SwiftData macro?</p><blockquote><p>I wish for non-Optional types in SwiftData models to work with CloudKit.</p></blockquote><h2 id="4-persistent-windows-volumes-in-visionos">#4 – Persistent Windows &amp; Volumes in VisionOS</h2><p>I have so many app ideas for VisionOS that only make sense when the position of windows or volumes could be persisted across app restarts and even system restarts or updates. For example, I released an app called <a href="https://apps.apple.com/app/apple-store/id6478062053?pt=549314&ct=fline.dev&mt=8">Posters</a> where you can decorate your walls with auto-updating interactive film posters – but when you restart the device they’re gone. Augmenting reality is only temporary.</p><p><img src="/assets/images/blog/my-top-10-wishes-for-wwdc24/decorating-walls-with.webp" alt="Decorating walls with Posters in VisionOS." loading="lazy" /></p><p><em>Decorating walls with <a href="https://apps.apple.com/app/apple-store/id6478062053?pt=549314&ct=fline.dev&mt=8">Posters</a> in VisionOS.</em></p><p>I understand that Apple doesn’t want to give us full ARKit access in Shared Space. It’s not possible to have good performance with all apps using ARKit at the same time. I’m not wishing for that. But the system already tracks window positions.</p><blockquote><p>I wish for a (user-approved) option to persists window/volume positions.</p></blockquote><h2 id="5-modal-text-input-on-tvos-like-playstation">#5 – Modal Text Input on tvOS (like Playstation)</h2><p>On iOS, iPadOS, and visionOS we get QWERTY keyboards because they are known, fast, and easy to type. On tvOS of all platforms, they decided to put the keys into a line. As a consequence, getting to a specific key takes much longer than it would if there was just a QWERTY-like grid system. Also, the text input needs the full width of the TV screen and is placed at the top, making it the main focus. Why all this? Just look at this much better modal keyboard UI from 2013 by Sony:</p><p><img src="/assets/images/blog/my-top-10-wishes-for-wwdc24/virtual-keyboard-on-ps4.webp" alt="Virtual Keyboard on PS4." loading="lazy" /></p><p><em>Virtual Keyboard on PS4.</em></p><p>The lack of something like this makes experiences impossible where the keyboard is just an accessory to an otherwise more interesting view. Because it’s missing, I ditched my initial plans to bring <a href="https://apps.apple.com/app/apple-store/id6472669260?pt=549314&ct=fline.dev&mt=8">CrossCraft</a> to tvOS as well. I would have loved to generate &amp; solve crossword puzzles on the TV. It could make for a fun family experience. But with the current keyboard, it’s a UX nightmare!</p><blockquote><p>I wish for a narrower, QWERTY-like accessory keyboard input on tvOS.</p></blockquote><h2 id="6-make-string-catalog-editor-more-useful">#6 – Make String Catalog editor more useful</h2><p>Yes, String Catalogs are new. So it is to be expected that they’re not fully evolved yet. But even some of the most basic things are broken right now. Right-clicking an entry and choosing something like “Mark as Reviewed” is possible. But multi-selecting to mark multiple at once? Isn’t. Adding a new language with a plus button at the bottom is possible. But removing one with the minus button? Isn’t.</p><p>Apart from the broken basics, I have one more feature request that makes so much sense: The UI marks parameters in blue already, so there is detection. But when one is missing in another language, there’s no warning. There should be!</p><blockquote><p>I wish for the String Catalog editor UI in Xcode to become more useful.</p></blockquote><blockquote><p>💁‍♂️ UPDATE: I implemented these improvements myself! 😍 I basically rebuilt Xcode’s String Catalog Editor and added it as a completely free feature to my app <a href="https://translatekit.app/">TranslateKit</a>. Check it out! 🌐 👍</p></blockquote><h2 id="7-new-create-llm-app-like-create-ml">#7 – New “Create LLM” app like “Create ML”</h2><p>Have you ever tried <a href="https://developer.apple.com/machine-learning/create-ml/">Create ML</a>? It’s a developer tool Apple introduced far back in 2019 which I feel gets overlooked by many developers. It’s an AI tool that solves the problem of classification quite well. Just look at all the available options:</p><p><img src="/assets/images/blog/my-top-10-wishes-for-wwdc24/create-ml-project.webp" alt="Create ML Project Templates" loading="lazy" /></p><p><em>Create ML Project Templates</em></p><p>But we all know that generative AI based on LLMs is the new trend. And there’s even proof that Apple is <a href="https://www.macrumors.com/2023/12/21/apple-ai-researchers-run-llms-iphones/">actively working</a> on something they will ship in the near future. Assuming that they will be able to publish something this year to consumers, what I want as a developer is a tool akin to Create ML but for LLMs. Imagine you could customize Apple’s Core LLM model with your specific domain data and maybe even with personal user data all stored on-device. This would allow for amazing new app experiences and as developers, we would get it for free!</p><blockquote><p>I wish for an easy way to create domain-specific &amp; personalized LLMs.</p></blockquote><h2 id="8-improved-source-control-ux-in-xcode">#8 – Improved Source Control UX in Xcode</h2><p>Year over year the integration of Git into Xcode is getting better and better. Last year, it reached a point that I decided to switch to Xcode as my default Git UI. Previously I was using <a href="https://git-fork.com/">Git-Fork</a>, but full integration into Xcode is unbeatable!</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>I am missing a few things though. Firstly, although Xcode has a code diffing view, there’s no way to see the diffs of past commits, only for “Uncommited changes”. Secondly, the diffing view itself has really weird color choices that make it hard to understand what code was removed and what was added. It’s common that tools use red and green for clear meanings which I would prefer a lot over the current. Thirdly, to commit and push in one step, you have to press the arrow-down icon to the right of the “Commit” button and choose “Commit and Push…”. I would prefer a checkbox that keeps its state so I don’t have to do multiple clicks each time. And lastly, sometimes I type a commit message and while reviewing the changes I find something that’s off. Then I switch to the file, make some adjustments and when I return to the diffing view, my commit message is gone! It should still be there.</p><blockquote><p>I wish for improved source control UX &amp; proper Git functionality in Xcode.</p></blockquote><h2 id="9-make-swiftui-previews-work-reliably">#9 – Make SwiftUI Previews work Reliably</h2><p>This has been an annoyance from the very first days of SwiftUI. Designing in SwiftUI could be so useful and fast. If only the previews worked. But 90% of the time the previews are not working for me. And I’ve tried the common solutions already. Sometimes they helped, sometimes they didn’t. But it’s annoying to have to do workarounds just to get SwiftUI previews working.</p><blockquote><p>I wish that SwiftUI previews work every time an app builds for Simulator.</p></blockquote><h2 id="10-screenshots-for-swiftui-previews">#10 – Screenshots for SwiftUI Previews</h2><p>Creating App Store screenshots is hard. You can do them manually on 2-3 devices in one language. But as soon as you want to localize them, the effort becomes unbearable. If I had to guess, I would say that 99% of the screenshots you can see on the App Store are outdated. But it doesn’t have to be this way.</p><p>We need some help on Apple’s end to fix it. And I’m not talking about UI Tests. They are slow. They are extra effort. They are unreliable. I don’t write UI Tests nowadays anymore, because we have SwiftUI previews. Well, I wrote above that they are not reliable, that’s true. But what if they  improved that? The <code>#Preview</code> macro makes it really easy to create multiple previews in a single view. And it’s relatively easy to pass in some state upon initialization to your views.</p><blockquote><p>I wish for an API to create localized screenshots using SwiftUI previews.</p></blockquote><h2 id="conclusion">Conclusion</h2><p>These are <em>my</em> top 10 wishes for WWDC24. Do you agree? What did I miss?</p>]]></content:encoded>
</item>
<item>
<title>Convert Paid Apps to Freemium Without Affecting Existing Users</title>
<link>https://fline.dev/snippets/convert-paid-apps-freemium/</link>
<guid isPermaLink="true">https://fline.dev/snippets/convert-paid-apps-freemium/</guid>
<pubDate>Thu, 28 Mar 2024 00:00:00 +0000</pubDate>
<description><![CDATA[How to use StoreKit's AppTransaction API to transition from paid-up-front to freemium while preserving access for users who already paid.]]></description>
<content:encoded><![CDATA[<h2 id="the-paid-to-freemium-transition-problem">The Paid-to-Freemium Transition Problem</h2><p>Switching a paid app to freemium is a common business decision, but it comes with a fairness challenge: users who already paid for the app should not suddenly lose features or be asked to pay again. StoreKit’s <code>AppTransaction</code> API solves this cleanly.</p><h2 id="using-apptransaction-to-check-purchase-history">Using AppTransaction to Check Purchase History</h2><p>The key is <code>AppTransaction.shared</code>, which provides information about the original transaction for your app. Specifically, <code>originalAppVersion</code> tells you which version of the app the user originally downloaded. If that version was a paid version, you know the user already paid.</p><pre><code class="language-swift">import StoreKit

func shouldGrantFullAccess() async -&gt; Bool {
   do {
      let result = try await AppTransaction.shared
      if case .verified(let transaction) = result {
         let originalVersion = transaction.originalAppVersion
         // Version &quot;2.0&quot; was when the app went freemium
         if originalVersion.compare(&quot;2.0&quot;, options: .numeric) == .orderedAscending {
            return true // User purchased before freemium transition
         }
      }
   } catch {
      // Handle verification failure
   }
   return false
}</code></pre><p>The logic is straightforward: compare the user’s <code>originalAppVersion</code> against the version where you made the freemium switch. If their original version predates the change, grant them full access automatically.</p><h2 id="important-details">Important Details</h2><p>The <code>originalAppVersion</code> corresponds to the <code>CFBundleShortVersionString</code> value at the time of the original purchase (or download for free apps). Make sure you know exactly which version number marks your transition point.</p><p>This approach requires no server infrastructure and no migration code. StoreKit handles verification through the App Store receipt chain, so the check is tamper-resistant. Apple documents this pattern in their <a href="https://developer.apple.com/documentation/storekit/apptransaction/3954437-originalappversion">original API for business model migration</a>.</p><p>For TestFlight and simulator testing, <code>originalAppVersion</code> returns the <code>CFBundleVersion</code> (build number) instead, so plan your test cases accordingly.</p>]]></content:encoded>
</item>
<item>
<title>Xcode Quick Help in the Sidebar</title>
<link>https://fline.dev/snippets/xcode-quick-help-sidebar/</link>
<guid isPermaLink="true">https://fline.dev/snippets/xcode-quick-help-sidebar/</guid>
<pubDate>Mon, 25 Mar 2024 00:00:00 +0000</pubDate>
<description><![CDATA[The Quick Help inspector in Xcode's sidebar auto-updates documentation as your cursor moves, removing the need to Cmd-click for docs.]]></description>
<content:encoded><![CDATA[<h2 id="an-overlooked-feature">An Overlooked Feature</h2><p>For years, my workflow for checking documentation in Xcode was the same: Cmd-click a symbol, select “Show Quick Help” from the context menu, read the popup, then dismiss it and continue coding. It works, but it is interruptive – each lookup requires three actions and breaks your editing flow.</p><p>Then I accidentally discovered the Quick Help inspector panel in the right sidebar.</p><p><video src="/assets/images/snippets/xcode-quick-help-sidebar/demo.mp4" controls muted playsinline></video></p><h2 id="how-it-works">How It Works</h2><p>Open the Quick Help inspector with Option+Cmd+3, or go to View &gt; Inspectors &gt; Quick Help. This opens a panel in the right sidebar that displays documentation for whatever symbol your cursor is currently on. As you move your cursor through your code – clicking on a method, arrowing through parameters, selecting a type – the sidebar updates automatically.</p><p>There is no clicking, no popup to dismiss, no interruption. You just keep coding and the relevant documentation follows along.</p><h2 id="why-it-beats-cmd-click">Why It Beats Cmd-Click</h2><p>The Cmd-click approach has a specific cost: it requires you to decide “I need docs for this” before you look. The sidebar inverts that. Because it is always visible, you absorb documentation passively. You notice parameter descriptions, return types, deprecation warnings, and availability annotations without making a conscious effort to look them up.</p><p>This is particularly valuable when working with unfamiliar APIs. Instead of Cmd-clicking every other symbol, you simply move your cursor through the code and read the sidebar as you go. It turns documentation lookup from a discrete action into a continuous stream.</p><p>The panel shows the same content as the Quick Help popup: declaration, description, parameters, return value, availability, and related symbols. The only difference is that it persists and updates in place rather than appearing and disappearing.</p><p>If you have the screen space for the right sidebar, keeping Quick Help open while coding is one of those small workflow changes that compounds over time.</p>]]></content:encoded>
</item>
<item>
<title>Introducing HandySwift 4.0</title>
<link>https://fline.dev/blog/introducing-handyswift-4/</link>
<guid isPermaLink="true">https://fline.dev/blog/introducing-handyswift-4/</guid>
<pubDate>Sun, 24 Mar 2024 00:00:00 +0000</pubDate>
<description><![CDATA[Investing time in Open Source again: Complete revamp of HandySwift with vastly improved documentation and lots of added handy features extracted from my apps. Read on to learn which helpers I use most often!]]></description>
<content:encoded><![CDATA[<p>It’s been a while since I last did open-source work. I’ve been focusing on shipping new apps lately to make sure I can continue my Indie career long-term. After launching <a href="https://x.com/Jeehut/status/1767534092516491338?s=20">6 apps</a> within 3 months, I thought it’s about time to share the most reused parts of code. I already have an open-source library for that: <a href="https://github.com/FlineDev/HandySwift">HandySwift</a>.</p><p>But it’s been more than 2 years since I made a new release. Of course, I was adding some functionality over time to the <code>main</code> branch so I could easily reuse them across my apps. But undocumented features that aren’t part of a new release can be considered “internal” even if the APIs themselves are marked <code>public</code>.</p><p>So I took the time in the last few days to clean up all the code, making sure everything is consistent with the latest additions to Swift, removing unused code, adding <code>@available</code> attributes for renames (so Xcode can provide fix-its), and documenting a bunch of new APIs. I even designed an entirely new logo!</p><p>Additionally, I decided to embrace <a href="https://github.com/apple/swift-docc">Swift-DocC</a> which means that I could shrink my README file to the bare minimum and instead host my documentation on the <a href="https://swiftpackageindex.com/">Swift Package Index</a> site. With some help of ChatGPT I could elaborate even on my existing documentation, leading to the best documented library I ever released!</p><p><img src="/assets/images/blog/introducing-handyswift-4/logo-update.webp" alt="Logo update" loading="lazy" /></p><p>I’ll be writing a dedicated post about the details of the migration later. But because I’ve never written about HandySwift before, let me explain some of the conveniences you get when using it. I recommend adding it to every project. It has no dependencies, is lightweight itself, supports all platforms (including Linux &amp; visionOS) and the platform support goes back to iOS 12. A no-brainer IMO. 💯</p><hr /><h2 id="extensions">Extensions</h2><p>Some highlights of the &gt;100 functions &amp; properties added to existing types, each with a practical use case directly from one of my apps:</p><h4 id="safe-index-access">Safe Index Access</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> I’m accessing an array of music tracks using an index. With <a href="https://swiftpackageindex.com/flinedev/handyswift/main/documentation/handyswift/swift/collection/subscript(safe:)"><code>subscript(safe:)</code></a> I avoid 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>You can use it on every type that conforms to <code>Collection</code> including <code>Array</code>, <code>Dictionary</code>, and <code>String</code>. Instead of calling the subscript <code>array[index]</code> which returns a non-Optional but crashes when the index is out of bounds, use the safer <code>array[safe: index]</code> which returns <code>nil</code> instead of crashing in those cases.</p><h4 id="blank-strings-vs-empty-strings">Blank Strings vs Empty Strings</h4><p><img src="/assets/images/blog/introducing-handyswift-4/api-keys.webp" alt="Api keys" loading="lazy" /></p><p>A common issue with text fields that are required to be non-empty is that users accidentally type a whitespace or newline character and don’t recognize it. If the validation code just checks for <code>.isEmpty</code> the problem will go unnoticed. That’s why in <a href="https://translatekit.app/">TranslateKit</a> when users enter an API key I make sure to first strip away any newlines and whitespaces from the beginning &amp; end of the String before doing the <code>.isEmpty</code> check. And because this is something I do very often in many places, I wrote a helper:</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>Just use <a href="https://swiftpackageindex.com/flinedev/handyswift/main/documentation/handyswift/swift/string/isblank"><code>isBlank</code></a> instead of <code>isEmpty</code> to get the same behavior!</p><h4 id="readable-time-intervals">Readable Time Intervals</h4><p><img src="/assets/images/blog/introducing-handyswift-4/premium-plan-expires.webp" alt="Premium plan expires" loading="lazy" /></p><p>Whenever I used an API that expects a <code>TimeInterval</code> (which is just a typealias for <code>Double</code>), I missed the unit which lead to less readable code because you have to actively remember that the unit is “seconds”. Also, when I needed a different unit like minutes or hours, I had to do the calculation manually. Not with HandySwift!</p><p>Intead of passing a plain <code>Double</code> value like <code>60 * 5</code>, you can just pass <code>.minutes(5)</code>. For example in <a href="https://translatekit.app/">TranslateKit</a> to preview the view when a user unsubscribed I use this:</p><pre><code>#Preview(&quot;Expiring&quot;) {
   ContentView(
      hasPremiumAccess: true, 
      premiumExpiresAt: Date.now.addingTimeInterval(.days(3))
   )
}</code></pre><p>You can even chain multiple units with a <code>+</code> sign to create a day in time like “09:41 AM”:</p><pre><code>let startOfDay = Calendar.current.startOfDay(for: Date.now)
let iPhoneRevealedAt = startOfDay.addingTimeInterval(.hours(9) + .minutes(41))</code></pre><p>Note that this API design is in line with <code>Duration</code> and <code>DispatchTimeInterval</code> which both already support things like <code>.milliseconds(250)</code>. But they stop at the seconds level, they don’t go higher. HandySwift adds minutes, hours, days, and even weeks for those types, too. So you can write something like this:</p><pre><code>try await Task.sleep(for: .minutes(5))</code></pre><blockquote><p>⚠️ Advancing time by intervals does not take into account complexities like daylight saving time. Use a <code>Calendar</code> for that.</p></blockquote><h4 id="calculate-averages">Calculate Averages</h4><p><img src="/assets/images/blog/introducing-handyswift-4/crossword-generation.webp" alt="Crossword generation" loading="lazy" /></p><p>In the crossword generation algorithm within <a href="https://apps.apple.com/app/apple-store/id6472669260?pt=549314&ct=swiftpackageindex.com&mt=8">CrossCraft</a> I have a health function on every iteration that calculates the overall quality of the puzzle. Two different aspects are taken into consideration:</p><pre><code>/// A value between 0 and 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 previous versions I played around with different weights, for example giving intersections double the weight compared to field coverage. I could still achieve this using <a href="https://swiftpackageindex.com/flinedev/handyswift/main/documentation/handyswift/swift/collection/average()-3g44u"><code>average()</code></a> like this in the last line:</p><pre><code>return [fieldCoverage, intersectionsCoverage, intersectionsCoverage].average()</code></pre><h4 id="round-floating-point-numbers">Round Floating-Point Numbers</h4><p><img src="/assets/images/blog/introducing-handyswift-4/progress-bar.webp" alt="Progress bar" loading="lazy" /></p><p>When solving a puzzle in <a href="https://apps.apple.com/app/apple-store/id6472669260?pt=549314&ct=swiftpackageindex.com&mt=8">CrossCraft</a> you can see your current progress at the top of the screen. I use the built-in percent formatter (<code>.formatted(.percent)</code>) for numerics, but it requires a <code>Double</code> with a value between 0 and 1 (1 = 100%). Passing an <code>Int</code> like <code>12</code> unexpectedly renders as <code>0%</code>, so I can’t simply do this:</p><pre><code>Int(fractionCompleted * 100).formatted(.percent)  // =&gt; &quot;0%&quot; until &quot;100%&quot;</code></pre><p>And just doing <code>fractionCompleted.formatted(.percent)</code> results in sometimes very long text such as <code>&quot;0.1428571429&quot;</code>.</p><p>Instead, I make use of <a href="https://swiftpackageindex.com/flinedev/handyswift/main/documentation/handyswift/swift/double/rounded(fractiondigits:rule:)"><code>rounded(fractionDigits:rule:)</code></a> to round the <code>Double</code> to 2 significant digits like so:</p><pre><code>Text(fractionCompleted.rounded(fractionDigits: 2).formatted(.percent))</code></pre><blockquote><p>ℹ️ There’s also a mutating <a href="https://swiftpackageindex.com/flinedev/handyswift/main/documentation/handyswift/swift/double/round(fractiondigits:rule:)"><code>round(fractionDigits:rule:)</code></a> function if you want to change a variable in-place.</p></blockquote><h4 id="symmetric-data-cryptography">Symmetric Data Cryptography</h4><p><img src="/assets/images/blog/introducing-handyswift-4/share-puzzle.webp" alt="Share puzzle" loading="lazy" /></p><p>Before uploading a crossword puzzle in <a href="https://apps.apple.com/app/apple-store/id6472669260?pt=549314&ct=fline.dev&mt=8">CrossCraft</a> I make sure to encrypt it so tech-savvy people can’t easily sniff the answers from the JSON like so:</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 logic
}</code></pre><p>Note that the above code makes use of two extensions, first <a href="https://swiftpackageindex.com/flinedev/handyswift/main/documentation/handyswift/cryptokit/symmetrickey/init(base64encoded:)"><code>init(base64Encoded:)</code></a> is used to initialize the key, then <a href="https://swiftpackageindex.com/flinedev/handyswift/main/documentation/handyswift/foundation/data/encrypted(key:)"><code>encrypted(key:)</code></a> encrypts the data using safe <a href="https://swiftpackageindex.com/flinedev/handyswift/main/documentation/handyswift/cryptokit">CryptoKit</a> APIs internally you don’t need to deal with.</p><p>When another user downloads the same puzzle, I decrypt it with <a href="https://swiftpackageindex.com/flinedev/handyswift/main/documentation/handyswift/foundation/data/decrypted(key:)"><code>decrypted(key:)</code></a> like so:</p><pre><code>func downloadPuzzle(from url: URL) async throws -&gt; Puzzle {
   let encryptedData = // download logic
   
   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 also conveniently ships with <a href="https://swiftpackageindex.com/flinedev/handyswift/main/documentation/handyswift/swift/string/encrypted(key:)"><code>encrypted(key:)</code></a> and <a href="https://swiftpackageindex.com/flinedev/handyswift/main/documentation/handyswift/swift/string/decrypted(key:)"><code>decrypted(key:)</code></a> functions for <code>String</code> which return a base-64 encoded String representation of the encrypted data. Use it when you’re dealing with String APIs.</p></blockquote><hr /><h2 id="new-types">New Types</h2><p>Besides extending existing types, HandySwift also introduces 7 new types and 2 global functions. Here are the ones I use in nearly every single app:</p><h3 id="gregorian-day-time">Gregorian Day &amp; Time</h3><p>You want to construct a <code>Date</code> from year, month, and day? Easy:</p><pre><code>GregorianDay(year: 1960, month: 11, day: 01).startOfDay() // =&gt; Date </code></pre><p>You have a <code>Date</code> and want to store just the day part of the date, not the time? Just use <a href="https://swiftpackageindex.com/flinedev/handyswift/main/documentation/handyswift/gregorianday"><code>GregorianDay</code></a> in your model:</p><pre><code>struct User {
   let birthday: GregorianDay
}


let selectedDate = // coming from DatePicker
let timCook = User(birthday: GregorianDay(date: selectedDate))
print(timCook.birthday.iso8601Formatted)  // =&gt; &quot;1960-11-01&quot;</code></pre><p>You just want today’s date without time?</p><pre><code>GregorianDay.today</code></pre><p>Works also with <code>.yesterday</code> and <code>.tomorrow</code>. For more, just call:</p><pre><code>let todayNextWeek = GregorianDay.today.advanced(by: 7)</code></pre><blockquote><p>ℹ️ <code>GregorianDay</code> conforms to all the protocols you would expect, such as <code>Codable</code>, <code>Hashable</code>, and <code>Comparable</code>. For encoding/decoding, it uses the ISO format as in “2014-07-13”.</p></blockquote><p><a href="https://swiftpackageindex.com/flinedev/handyswift/main/documentation/handyswift/gregoriantimeofday"><code>GregorianTimeOfDay</code></a> is the counterpart:</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-debounce">Delay &amp; Debounce</h3><p>Have you ever wanted to delay some code and found this API annoying to remember &amp; type out?</p><pre><code>DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + .milliseconds(250)) {
   // your code
}</code></pre><p>HandySwift introduces a shorter version that’s easier to remember:</p><pre><code>delay(by: .milliseconds(250)) {
   // your code
}</code></pre><p>It also supports different Quality of Service classes like <code>DispatchQueue</code> (default is main queue):</p><pre><code>delay(by: .milliseconds(250), qosClass: .background) {
   // your code
}</code></pre><p>While delaying is great for one-off tasks, sometimes there’s fast input that causes performance or scalability issues. For example, a user might type fast in a search field. It’s common practice to delay updating the search results and additionally cancelling any older inputs once the user makes a new one. This practice is called “Debouncing”. And it’s easy with HandySwift:</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)) {
            // Perform search operation with the updated search text after 500 milliseconds of user inactivity
            self.performSearch(with: newValue)
        }
    }
    .onDisappear {
        debouncer.cancelAll()
    }
}</code></pre><p>Note that the <a href="https://swiftpackageindex.com/flinedev/handyswift/main/documentation/handyswift/debouncer"><code>Debouncer</code></a> was stored in a property so <a href="https://swiftpackageindex.com/flinedev/handyswift/main/documentation/handyswift/debouncer/cancelall()"><code>cancelAll()</code></a> could be called on disappear for cleanup. But the <a href="https://swiftpackageindex.com/flinedev/handyswift/main/documentation/handyswift/debouncer/delay(for:id:operation:)-83bbm"><code>delay(for:id:operation:)</code></a> is where the magic happens – and you don’t have to deal with the details!</p><hr />]]></content:encoded>
</item>
<item>
<title>Migrating my SwiftUI App to VisionOS in 2 Hours</title>
<link>https://fline.dev/blog/migrating-my-swiftui-app-to-visionos/</link>
<guid isPermaLink="true">https://fline.dev/blog/migrating-my-swiftui-app-to-visionos/</guid>
<pubDate>Sat, 02 Mar 2024 00:00:00 +0000</pubDate>
<description><![CDATA[How I migrated my SwiftUI app CrossCraft to support visionOS for the Day 1 Release of the Apple Vision Pro. It took effectively about 2 hours in total, this article summarizes my key learnings along the way.]]></description>
<content:encoded><![CDATA[<p>Just a few months ago, I released <a href="https://apps.apple.com/app/apple-store/id6472669260?pt=549314&ct=fline.dev&mt=8">CrossCraft: Custom Crosswords</a>, an app written entirely in SwiftUI and available on iOS, iPadOS, and macOS. For the launch of the Vision Pro, I set myself a challenge to migrate it to the new visionOS platform – but I started the migration just 3 days before launch day!</p><p>So the question was if I would be able to pull it off in this short amount of time. But luckily it turned out to be easy enough, so my app was ready on Day 1! 👇</p><p><img src="/assets/images/blog/migrating-my-swiftui-app-to-visionos/the-official-email-from.webp" alt="The official email from Apple, thanking Day 1 app developers." loading="lazy" /></p><p><em>The official email from Apple, thanking Day 1 app developers.</em></p><p>The following are all of my learnings that could help you migrate your apps, too!</p><h2 id="3rd-party-frameworks">3rd-Party Frameworks</h2><p>After adding the “Apple Vision” destination to my project, the first thing I did was selecting the “Apple Vision Pro” simulator and starting a build.</p><p><img src="/assets/images/blog/migrating-my-swiftui-app-to-visionos/step-2-adjusting-the.webp" alt="Step 2: Adjusting the Package.swift file is the most important step." loading="lazy" /></p><p>As I was expecting, the build failed. Because not all frameworks support the visionOS platform yet. But adding basic support was easy. Here are the 4 steps:</p><p><img src="/assets/images/blog/migrating-my-swiftui-app-to-visionos/step-2-adjusting-the-2.webp" alt="Step 2: Adjusting the Package.swift file is the most important step." loading="lazy" /></p><p><em>Step 2: Adjusting the Package.swift file is the most important step.</em></p><ol><li><p>Fork the dependency, remove it from your project &amp; add your fork with the <code>main</code> branch instead.</p></li><li><p>Open the <code>Package.swift</code> file in the fork, bump the Swift tools version at the top of the file to <code>5.9</code> and add <code>.visionOS(.v1)</code> to the supported <code>platforms</code> array.</p></li><li><p>Search for any mentions of <code>#if os(iOS)</code> and change them to <code>#if os(iOS) || os(visionOS)</code> to avoid building the macOS path, preferring the iOS path.</p></li><li><p>Select the “Apple Vision Pro” simulator and build the project to confirm.</p></li></ol><p>If you get an error due to missing APIs, make sure to add <code>#if !os(visionOS)</code> checks at the right places. Most APIs should be available though, as visionOS is a fork of iPadOS as Apple officially confirmed. If a feature is not there, it will either come soon or it doesn’t make sense on the platform anyway.</p><p>In my case, only for my own <a href="https://github.com/FlineDev/ReviewKit">ReviewKit</a> library I had to disable some code around <code>SKReviewController</code> which isn’t available on visionOS yet. So my library effectively does nothing when building for <code>visionOS</code>, but my iOS &amp; Mac apps will continue asking users to the rate the app. I could have fixed that with a custom UI, but I decided to wait for this years WWDC first, hoping we’ll get it there already.</p><p>Don’t forget to post a Pull Request to the original repo if you forked it, so others in the community can profit from your fix as well. The more people do this, the less dependencies you have to add platform support yourself. 💪</p><h2 id="testing-my-app-in-the-simulator">Testing my App in the Simulator</h2><p>After fixing all the dependencies, I built my app and it succeeded! 🥳</p><p><img src="/assets/images/blog/migrating-my-swiftui-app-to-visionos/xcodes-preview-of-your.webp" alt="Xcodes preview of your app icon." loading="lazy" /></p><p>Unfortunately, I was not done yet. First off, while the app was launching, I saw that there was no App Icon shown although I have one in my project. Also, after it launched, I immediately discovered a bunch of other issues. Here’s an overview:</p><ul><li><p>The <strong>App Icon</strong> was missing</p></li><li><p>When moving the cursor (= gaze), the <strong>Hover Shape</strong> was off in some places</p></li><li><p>The <strong>Layout &amp; Sizes</strong> of many windows, modals, and my UI elements were off</p></li><li><p>My <strong>Accent Color</strong> did not have a legible contrast to the glassy background</p></li></ul><p>All of these points will affect every single app migrating to visionOS. For me, small adjustments helped fix them though. Let me share my learnings one by one.</p><h2 id="app-icon">App Icon</h2><p>It turns out, visionOS has its own App Icon style. They are circular like on watchOS, but they consist of multiple layers to create a sense of depth, like on tvOS. You add a visionOS app icon by pressing the + button and choosing “visionOS App Icon”. Then, you need to provide at least a “Front” and “Back” layer image of size 1024 x 1024. The “Middle” layer is optional.</p><p><img src="/assets/images/blog/migrating-my-swiftui-app-to-visionos/xcodes-preview-of-your-2.webp" alt="Xcodes preview of your app icon." loading="lazy" /></p><p>In my case, my app icon already consisted of a background layer and an icon in the foreground, so it was not a big deal to export them separately. I just had to remove the “Middle” layer in the right pane. But because I had a shadow applied to my foreground icon, and the <a href="https://developer.apple.com/design/human-interface-guidelines/app-icons#Platform-considerations">Human Interface Guidelines</a> state that we should “avoid using soft or feathered edges” for the non-background layers, I had to remove the shadow. The system will add a slight shadow on hover automatically.</p><p>Speaking of hover, Xcode provides a preview of how your app icon will look like at the top, and when you hover your mouse over it, it simulates the 3D hover effect when users will look at your app icon on the Vision Pro, which is really handy!</p><p><img src="/assets/images/blog/migrating-my-swiftui-app-to-visionos/xcodes-preview-of-your.gif" alt="Xcodes preview of your app icon." loading="lazy" /></p><p><em>Xcodes preview of your app icon.</em></p><h2 id="hover-effects">Hover Effects</h2><p>In visionOS, one of the things that are easy to miss in the Simulator but extremely important when actually using the device are proper hover effects. As you select elements on the device with your eyes, it is important your app gives feedback about which element is currently selected. This works great out of the box with Stringly-based control APIs in SwiftUI, such as <code>Button(&quot;Click me&quot;) { ... }</code>.</p><p>But as soon as you provide a custom <code>label</code> parameter to a button, or even have your entirely custom controls, you will need to provide the exact shape of your control to the system. For example, I’m using a custom control I call <a href="https://github.com/FlineDev/HandySwiftUI/blob/main/Sources/HandySwiftUI/Views/HPicker.swift"><code>HPicker</code></a> which I use instead of the default drop-down <code>Picker</code> when I have only 2-4 options to choose from. It ended up looking like this when hovering over an option:</p><p><img src="/assets/images/blog/migrating-my-swiftui-app-to-visionos/my-custom-hpicker-view.webp" alt="My custom HPicker view without any Hover Effect adjustments." loading="lazy" /></p><p><em>My custom HPicker view without any Hover Effect adjustments.</em></p><p>Adjusting it to follow the shape of the options was easy enough:</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>Simplified version of the buttons inside my <a href="https://github.com/FlineDev/HandySwiftUI/blob/main/Sources/HandySwiftUI/Views/HPicker.swift">HPicker</a> component.</em></p><p>The <code>.contentShape</code> and <code>.hoverEffect</code> modifiers are what I added for a proper hover effect. Replace <code>.rect(cornerRadius: 12.5)</code> with whatever the shape of your custom control is. Note that I wrapped them in a <code>#if !os(macOS)</code> check as my app supports macOS, but <code>.hoverEffect</code> is not available on it. Also, note that placing these modifiers outside the <code>Button</code> did not work for me, they have to be placed inside the label definition to work properly.</p><p>In some situations, you might notice that you have a hover effect where you don’t expect one. For me, this was the case when I provided a <code>Button</code> inside another view that already is recognized as a control, like this <code>DisclosureGroup</code>:</p><p><img src="/assets/images/blog/migrating-my-swiftui-app-to-visionos/the-entire-show-clues.webp" alt="The entire " loading="lazy" /></p><p><em>The entire “Show Clues” row is a button, but the button inside is another button.</em></p><p>You can turn off the hover effect by just adding the <code>.hoverEffectDisabled()</code> modifier. In my case above, the inner button was only added for macOS (because a <code>DisclosureGroup</code> doesn’t toggle when pressing the label on that platform). So my fix was to only use a <code>Button</code> inside on macOS and to use a simple <code>Label</code> else.</p><p>Making all controls have a proper hover effect was actually the most time-consuming task of the migration and took ~40 minutes. It would probably have been much faster if SwiftUI previews worked in my project, but for some reason they wouldn’t build for me, and when I tried, my Mac would start hanging. 🤷‍♂️</p><h2 id="layout-system">Layout System</h2><p>While visionOS is based on iPadOS and therefore renders things like <code>Form</code> views similar to the iPad, it’s important to understand that there’s actually a key difference when it comes to the layout system compared to iOS/iPadOS:</p><p><img src="/assets/images/blog/migrating-my-swiftui-app-to-visionos/a-person-s-field-of-view.webp" alt="A person’s field of view inside the Apple Vision Pro. Source: HIG" loading="lazy" /></p><p><em>A person’s field of view inside the Apple Vision Pro. <a href="https://developer.apple.com/design/human-interface-guidelines/spatial-layout">Source: HIG</a></em></p><p>On Apple Vision apps are opened in an infinite canvas, there’s no fixed screen width or height your views can derive their size from. This is a key difference you need to understand. If you have developed apps for macOS, you will already be familiar with this difference. In many ways, the layout system is much closer to that of macOS where monitors can also have differing sizes and windows are very rarely opened using the full-screen space like they do on iOS &amp; iPadOS.</p><p>So, if your app already supports macOS, you can simply opt for the <code>#if os(macOS)</code> branches that you will most probably have many of already when it comes to sizing or window management. Just replace with <code>#if os(macOS) || os(visionOS)</code>.</p><p>The main difference even to macOS is that windows have rounded corners with a large corner radius. So I found I had to add extra padding to the top &amp; bottom of my window root views, e.g. using <code>.padding(.vertical, 10)</code>.</p><p>If you don’t have your app optimized for macOS yet, here are some key learnings:</p><ul><li><p>You need to provide <code>.frame(minWidth: 400, minHeight: 300)</code> for your views all over the place, otherwise your windows or modals might have sizes that don’t work for your UI. Make sure to check them all and provide proper values.</p></li><li><p>While you should specify <code>minWidth</code> and <code>minHeight</code> for your views so users can’t resize them to become too small for your content, you might additionally want to provide a larger <code>.defaultSize(width: 800, height: 600)</code> on your <code>WindowGroup</code> scene to default to a larger size than the minimum.</p></li><li><p>If you have modal views that cover your entire screen and also need that space, you will want to consider moving these modals to their own windows instead. Utilize the <code>@Environment(\.openWindow) var openWindow</code> property to open new windows on visionOS (and macOS) and specify additional <code>WindowGroup</code> views. See my article about <a href="https://www.fline.dev/window-management-on-macos-with-swiftui-4/">window management in SwiftUI 4</a> to learn more.</p></li><li><p>You can decide to keep the modals for your initial migration instead of using external windows, which would be the proper solution. But note that unlike on macOS, modal sheets in visionOS are not resizable. So at least make sure to provide a size that works well for your sheet for all kinds of potentially dynamic data shown in the modal. That’s what I did for “playing a puzzle” in CrossCraft.</p></li></ul><h2 id="colors">Colors</h2><p>Note that any controls with a white background will not play well with the hover effect, because the effect uses a white overlay. White on top of white isn’t visible. I ran into this for my crossword puzzle game mode, where users press on tiles to enter characters. Note how the cursor is on a tile but the hover isn’t visible:</p><p><img src="/assets/images/blog/migrating-my-swiftui-app-to-visionos/hovers-are-not-visible.webp" alt="Hovers are not visible on white background buttons." loading="lazy" /></p><p><em>Hovers are not visible on white background buttons.</em></p><p>My quick fix was to add the modifier <code>.opacity(0.85)</code> making my white backgrounds 15% transparent, which helped already. More would be better, but white is an expected “crossword” color, so I tried to keep it as white as possible.</p><p><img src="/assets/images/blog/migrating-my-swiftui-app-to-visionos/colors-that-are-legible.webp" alt="Colors that are legible on iOS might not work on visionOS." loading="lazy" /></p><p><em>Colors that are legible on iOS might not work on visionOS.</em></p><p>I also noticed that many mid-contrast colors, including the system default “blue” accent color, have a really bad contrast on the default window glass background. Make sure to make these colors brighter for visionOS. You can add a specific variant for “Apple Vision” using the Attributes inspector with a color selected.</p><p><img src="/assets/images/blog/migrating-my-swiftui-app-to-visionos/colors.webp" alt="Colors" loading="lazy" /></p><blockquote><p>💡 To make a color brighter using the HSB system, you need to decrease the saturation slightly and increase the brightness significantly. You can additionally move the hue a little bit towards the closest bright RGB value. Refer to <a href="https://www.learnui.design/blog/color-in-ui-design-a-practical-framework.html">this article</a> to learn more about how to make colors brighter/darker properly utilizing HSB.</p></blockquote><h2 id="conclusion">Conclusion</h2><p>If you have an app that’s already on iPadOS &amp; macOS, you’re in a very good spot to add support for visionOS. You will be able to reuse all your SwiftUI code. When it comes to window management, you should opt for the macOS version. For everything else, opt for the iPadOS version. Then, ensure all your custom controls have a proper hover effect. Make some layout &amp; UI adjustments like adding padding, making colors brighter, or splitting your app icon to a front &amp; back part.</p><p>The entire process took effectively 2 hours for me. I live-streamed the entire process, you can find my recordings with the “wait for build” &amp; “chat” removed in the following two YouTube videos, each roughly an hour long. Note that I added time codes for the different steps outlined above so you can dive into specifics:</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>Introducing &quot;Posters&quot; – My first Spatial-first app for Vision Pro</title>
<link>https://fline.dev/blog/introducing-posters-my-first-spatial-first-app-for-vision-pro/</link>
<guid isPermaLink="true">https://fline.dev/blog/introducing-posters-my-first-spatial-first-app-for-vision-pro/</guid>
<pubDate>Fri, 23 Feb 2024 00:00:00 +0000</pubDate>
<description><![CDATA[Breath life into your home with auto-updating & interactive posters of the latest trending movies & TV shows. Tap on a poster to unveil the trailer, find out where it's currently streaming, or locate a cinema showing near you. The future is here!]]></description>
<content:encoded><![CDATA[<p>As a huge film fan, I’ve been decorating my walls with film posters for a long time. But static posters can get boring over time. So when Apple launched the Vision Pro and reviewers stated that the window positions were very accurate, I immediately thought I could <strong>build an app to decorate my walls</strong> with auto-updating posters.</p><p>When I showed a prototype of this idea to my wife, she immediately asked if she can click the posters to watch a trailer. And born was the idea to make them interactive! In the just published app, besides Trailers, you can also find Showtimes, and Direct Links to Streaming services. Here’s a 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>To make sure everything worked as expected, I visited Apples Developers labs in Munich earlier this week since I can’t buy the device myself here in Germany to test it. Unfortunately, Apple engineers confirmed that visionOS does not yet support persisting the positions of windows across app restarts. So for now, one has to keep the device connected to battery to retain the poster positions for multiple days. But it’s easy to place them, so I decided to publish the app anyways. And I made sure to report this shortcoming to Apple in all the official ways I could, including directly to the engineers in the lab. I have more ideas than just this that need that feature. I believe it is the #1 thing missing right now that we developers desperately need in visionOS 2.0.</p><p>Long story short, I just released the “Posters” app and would love you to try it:</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>Please let me know if you run into any issues. I’m really excited about publishing more apps on this new platform. Multiple apps are in the works, 2 of which might get ready for release as soon as next week. 🤞 This is due to my decision to go Spatial-first (rather than Mobile-first) with my future apps. After all, more than 70% of my last months revenue was made on Apple Vision. And that even though the device isn’t even out for a full month yet! Exciting times for 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>If you don’t want to miss my future apps, make sure to follow me! 👇</p>]]></content:encoded>
</item>
<item>
<title>HUGE CrossCraft 2.0 Update: Seven Major New Features!</title>
<link>https://fline.dev/blog/huge-crosscraft-update-seven-major-features/</link>
<guid isPermaLink="true">https://fline.dev/blog/huge-crosscraft-update-seven-major-features/</guid>
<pubDate>Tue, 30 Jan 2024 00:00:00 +0000</pubDate>
<description><![CDATA[This update brings key improvements like saving and syncing crosswords, expanded content with 30 new topics, and enhanced user experience with features like puzzle tips, a native Mac app, a native Vision Pro app & sharing options for competitive play.]]></description>
<content:encoded><![CDATA[<p>CrossCraft 2.0 is here – a huge update to the app that can create an infinite amount of crossword puzzles for various topics and even allows adding your own questions to surprise friends &amp; family or your audience with a personalized puzzle.</p><p>In the 6 weeks since the app’s <a href="https://www.fline.dev/introducing-crosscraft/">initial release</a>, I worked tirelessly to get the most requested features out, and I’m happy to introduce you to 7 major improvements:</p><h3 id="1-save-sync-crosswords">#1: Save &amp; Sync Crosswords</h3><p>You can now save crosswords to solve them later! And your progress will also sync across devices through iCloud. This was the most requested feature!</p><p><img src="/assets/images/blog/huge-crosscraft-update-seven-major-features/my-account-is-assigned-4.webp" alt="My account is assigned to the German App Store, so my screenshots are in German." loading="lazy" /></p><h3 id="2-5-new-categories-30-new-topics">#2: 5 New Categories &amp; 30 New Topics</h3><p>Before this update, 22 topics in 5 categories were supported. This update doubles the number of categories and more than doubles the number of topics! And all of them are available in all 7 languages (EN, FR, DE, IT, PT, ES, TR).</p><p>Here’s a full list of all the new topics:</p><ul><li><p><strong><em>General Knowledge</em>:</strong><br />Anime, Bollywood, Culture &amp; Religion</p></li><li><p><strong><em>TV Shows (new)</em>:</strong><br />Friends, Game of Thrones, SpongeBob, The Simpsons</p></li><li><p><strong><em>Video games (new)</em>:</strong><br />Grand Theft Auto, Minecraft, Pokémon, The Sims</p></li><li><p><strong><em>Sports</em>:</strong><br />Karate, Taekwondo</p></li><li><p><strong><em>Language Learning</em>:</strong><br />Italian, Portuguese (Brazil), Turkish</p></li><li><p><strong><em>Culture &amp; Religion</em> (new):</strong><br />Ancient Egypt, Christianity, Greek Mythology, Islam, Judaism</p></li><li><p><strong><em>Science (new)</em>:</strong><br />Astronomy, Biology, Computer Science, Economy, Medicine</p></li><li><p><strong><em>Technology (new)</em>:</strong><br />Apple, Cars &amp; Engines, Google, Microsoft</p></li></ul><p>More topics are already in the works and will be added in future updates.</p><p><img src="/assets/images/blog/huge-crosscraft-update-seven-major-features/my-account-is-assigned.webp" alt="My account is assigned to the German App Store, so my screenshots are in German." loading="lazy" /></p><h3 id="3-get-limited-tips">#3: Get (Limited) Tips</h3><p>Every puzzle has those few words that you just don’t know or don’t remember. You can now uncover a limited amount of fields so you don’t get stuck at a 90% completed puzzle just because of a weird hint, which can be really annoying!</p><p><img src="/assets/images/blog/huge-crosscraft-update-seven-major-features/my-account-is-assigned-3.webp" alt="My account is assigned to the German App Store, so my screenshots are in German." loading="lazy" /></p><h3 id="4-game-center-support">#4: Game Center Support</h3><p>Earn Achievements for using various features of the app and compete with friends &amp; others in global Leaderboards. Each category has its own leaderboard, so it’s your time to shine and show off your skills in your favorite topic!</p><p><img src="/assets/images/blog/huge-crosscraft-update-seven-major-features/my-account-is-assigned-2.webp" alt="My account is assigned to the German App Store, so my screenshots are in German." loading="lazy" /></p><p><em>My account is assigned to the German App Store, so my screenshots are in German.</em></p><h3 id="5-native-app-for-the-mac">#5: Native app for the Mac</h3><p>While the iPad version of CrossCraft was available on Apple Silicon Macs from day one, I took the extra effort to prepare a native Mac app with a more fitting look &amp; feel. This means, CrossCraft is available on Intel Macs for the first time now!</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-for-the-new-apple-vision-pro">#6: Native app for the new Apple Vision Pro</h3><p>Additionally, I migrated CrossCraft to visionOS for a native experience and even <a href="https://www.twitch.tv/Jeehut">live-streamed</a> the entire process. Spatial Crossword Puzzling is here! ✨</p><p><img src="/assets/images/blog/huge-crosscraft-update-seven-major-features/native-vision-os-app.webp" alt="Native vision os app" loading="lazy" /></p><h3 id="7-share-play-the-same-puzzle">#7: Share &amp; Play the Same Puzzle</h3><p>Share your puzzle with a simple link or a printed QR code. This way you can create a puzzle &amp; challenge a friend to solve the same puzzle – who’s faster?</p><p>If you have an audience of readers, share an image of a custom-prepared puzzle to challenge them and add a QR Code that opens the puzzle right within CrossCraft for a smoother solving experience right within the app.</p><p>I created puzzles in 7 languages for you to experience the new sharing feature:</p><ul><li><p><em>Anime Puzzle</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 Puzzle: *<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>*Computer Science Puzzle: *<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>*Economy Puzzle: *<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="Share play the same puzzle kopie" loading="lazy" /></p><p>These features were requested most, I hope you like how they turned out!</p><p>Get the update now: 👇👇👇</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>If there are any more features you’re missing, please <a href="mailto:crosscraft@fline.dev">drop an email</a>.</p>]]></content:encoded>
</item>
<item>
<title>8 Simple Steps to Create Crosswords on Any Topic in Minutes Using ChatGPT</title>
<link>https://fline.dev/blog/8-steps-to-create-crosswords-with-chatgpt/</link>
<guid isPermaLink="true">https://fline.dev/blog/8-steps-to-create-crosswords-with-chatgpt/</guid>
<pubDate>Mon, 18 Dec 2023 00:00:00 +0000</pubDate>
<description><![CDATA[Discover the ease of creating personalized crossword puzzles on any topic harnessing the full power of ChatGPT. This guide walks you through eight simple steps, from generating clue-answer pairs to crafting and customizing your puzzle with the innovative CrossCraft app.]]></description>
<content:encoded><![CDATA[<h3 id="step-1-produce-clue-answer-pairs-with-chatgpt">Step 1: Produce Clue-Answer Pairs with ChatGPT</h3><p>Feed ChatGPT with the following prompt, but don’t forget to replace <code>TOPIC</code> with the topic you want to create a crossword puzzle about, and <code>LANGUAGE</code> with the language the questions and answers should be provided in:</p><blockquote><p>💬 Create 50 clue-answer pairs for a crossword puzzle about the topic “<em><strong>TOPIC</strong></em>” for an audience of fans of the topic. Provide a good mix of easier and harder questions. Provide a good mix of historical and more recent examples.
Provide clues and answers in <em><strong>LANGUAGE</strong></em>.
Avoid pairs where the answer contains a number. Avoid clues that contain (parts of) the answer. Keep both clues and answers short, e.g. avoid unnecessary articles. Replace special characters like “+” in the answer with its written-out word, like “plus”.
Format the clue-answer pairs as a single code block of comma-separated CSV like so:</p><pre><code class="language-csv">&quot;This is the first clue&quot;,&quot;Answer&quot;
&quot;This is the second clue&quot;,&quot;Answer&quot;</code></pre></blockquote><p>The <code>TOPIC</code> can be replaced with anything. A specific kind of technology, the name of a franchise, a sport you like, or a media website’s URL you visit every day. I’m an iOS developer and I like to know the latest news about Apple products. So I can simply replace <code>TOPIC</code> with <code>Apple News</code> and <code>LANGUAGE</code> with <code>English</code>. ChatGPT should then respond with something like this:</p><p><img src="/assets/images/blog/8-steps-to-create-crosswords-with-chatgpt/chatgpt-saves-a-lot-of.webp" alt="ChatGPT saves a lot of time in coming up with related clues &amp; answers." loading="lazy" /></p><p><em>ChatGPT saves a lot of time in coming up with related clues &amp; answers.</em></p><blockquote><p>ℹ️ If you don’t get an answer like this right away, just try again by pressing the 🔄 retry button below the answer.</p></blockquote><h3 id="step-2-copy-the-csv-text-into-a-file">Step 2: Copy the CSV Text into a File</h3><p><img src="/assets/images/blog/8-steps-to-create-crosswords-with-chatgpt/a-selection-of-topics-2.webp" alt="A selection of topics you can find in the CrossCraft app." loading="lazy" /></p><p>Press “Copy code” on the top right of the code block, create a new file with your plain text editor of choice (e.g. <a href="https://www.sublimetext.com/download">SublimeText</a>) and paste the contents into your new file. Make sure to save your file in plain text format with the ending <code>.csv</code>.</p><blockquote><p>ℹ️ Document editors like Word or TextEdit save their files in special markup formats like <code>.docx</code> or <code>.rtf</code>. These are not plain-text files.</p></blockquote><h3 id="step-3-check-the-validity-of-the-produced-pairs">Step 3: Check the Validity of the Produced Pairs</h3><p>While the prompt already specifies not to use numeric values in the answers and not to spoil the answers in the questions, ChatGPT makes mistakes. So double-check that everything looks right. You can also add new entries with your own questions to spice it up a bit more if you like.</p><h3 id="step-4-select-a-related-fallback-topic-in-crosscraft">Step 4: Select a related Fallback Topic in CrossCraft</h3><p><img src="/assets/images/blog/8-steps-to-create-crosswords-with-chatgpt/a-selection-of-topics.webp" alt="A selection of topics you can find in the CrossCraft app." loading="lazy" /></p><p><em>A selection of topics you can find in the CrossCraft app.</em></p><p>Make sure you have installed CrossCraft for free on your iPhone, iPad, or Mac:</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>Open the app and select one of the many topics the app ships with by default, choosing the one that is most related to <em>your</em> topic. If a related topic isn’t there yet, make sure to press “Suggest Topic” inside the app and make your wish. You can also choose “Without topic, just your own questions” at the top if you don’t want to fill the gaps with questions of a fallback topic. For fuller puzzles, you can always select general knowledge topics like “Famous People” or “Pop Culture” as these are widely known. I’ve selected “Technology”.</p><h3 id="step-5-import-the-csv-file-to-crosscraft">Step 5: Import the CSV file to CrossCraft</h3><p><img src="/assets/images/blog/8-steps-to-create-crosswords-with-chatgpt/importing-csv-files-is-a.webp" alt="Importing CSV files is a Premium feature." loading="lazy" /></p><p><em>Importing CSV files is a Premium feature.</em></p><p>Make sure you have access to the <code>.csv</code> file from the device you run CrossCraft in, e.g. by saving the file to your iCloud Drive to access it on your iPhone or iPad. Then, in the wizard where you can choose the size &amp; difficulty of the crossword, press the button “Import from file…” in the last step and select <code>CSV</code> in the dialog.</p><h3 id="step-6-re-generate-a-crossword-until-youre-happy">Step 6: (Re-)Generate a Crossword Until You’re Happy</h3><p><img src="/assets/images/blog/8-steps-to-create-crosswords-with-chatgpt/the-resulting-exported.webp" alt="The resulting exported personalized crossword for MacRumors.com" loading="lazy" /></p><p>Press the “Create Crossword” button in the bottom right to start generating your first custom crossword. The generation will stop automatically when it can no longer improve the current state of the puzzle. Below the crossword preview, you can see values that indicate the quality of the generated crossword, as in how “filled” it is and how many intersections it has. The quality percentage combines these aspects into one single value. Any percentage higher than 50% can be considered a “well-filled” crossword.</p><p>On the same screen, you can find a “Regenerate” button to restart the random generation algorithm if you are unhappy with the quality of the crossword or the selection of clues, which you can preview by pressing “Show Clues”.</p><h3 id="step-7-export-your-crossword-as-an-image">Step 7: Export your Crossword as an Image</h3><p><img src="/assets/images/blog/8-steps-to-create-crosswords-with-chatgpt/crossword-export-share-options.webp" alt="The share dialog in CrossCraft lets you choose what to export." loading="lazy" /></p><p>On iPhone or iPad, I recommend pressing “Share…” and then selecting “Save Image” to store the crossword puzzle in the Photos app, from where you can share it with other devices if needed. This ensures the image does not get downgraded to a JPEG without transparency for compatibility. Alternatively, use “Save to File”.</p><p>You will be asked what part you want to share. Choose “Crossword + Clues” for a combined single image. Alternatively, you can export the puzzle &amp; clues separately in case you want to print them out on 2 separate pages, for example.</p><h3 id="step-8-optional-add-a-background-image-of-your-liking">Step 8: (Optional) Add a Background Image of your Liking</h3><p><img src="/assets/images/blog/8-steps-to-create-crosswords-with-chatgpt/the-resulting-exported-2.webp" alt="The resulting exported personalized crossword with background for MacRumors.com" loading="lazy" /></p><p><em>The resulting exported personalized crossword for MacRumors.com</em></p><p>For some extra nice looks, search for a nice background image that fits your topic and place the exported PNG image on top of it with your design tool of choice (e.g. <a href="https://www.figma.com/login">Figma</a>). You could even use AI image generators and ask them to create a “scenery” that fits your topic, which I did using DALL·E in ChatGPT Plus. My exact prompt for the above background image was:</p><blockquote><p>💬 Create a scenery that fits the topic “Apple News” in portrait format. The bottom half should be a plain color/shade. Keep it simple overall.</p></blockquote><p>The exported image conveniently has 5% transparency in its white backgrounds of the fields and clues built-in, letting some of your background image shine through. You see the final result above. With more time and creativity, I’m sure you can create something even better!</p><p>Try it for yourself and surprise someone you love. It’s the perfect gift! 🎁</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>Introducing CrossCraft: Custom Crosswords</title>
<link>https://fline.dev/blog/introducing-crosscraft/</link>
<guid isPermaLink="true">https://fline.dev/blog/introducing-crosscraft/</guid>
<pubDate>Fri, 15 Dec 2023 00:00:00 +0000</pubDate>
<description><![CDATA[Create themed & personalized crossword puzzles with ease and either play yourself or surprise your loved ones with a custom-tailored special gift. Perfect also to playfully learn vocabulary or challenge your students or friends with a fun quiz.]]></description>
<content:encoded><![CDATA[<p>Are you a crossword enthusiast who’s ever wished for a more personalized puzzle experience? Or a big fan of franchises like Harry Potter or Lord of the Rings in search of the next challenge to prove your deep knowledge? Or perhaps a teacher looking for an engaging way to reinforce vocabulary with your students? Maybe you’re just searching for a unique and engaging gift idea for your next family event. Whatever your motivation, <a href="https://crosscraft.app/">CrossCraft</a> is here to transform your ideas into fun crossword puzzles in a matter of seconds. And all that for free!</p><p><img src="/assets/images/blog/introducing-crosscraft/created-with-crosscraft.webp" alt="Created with CrossCraft. Topic: Pop Culture. The last question is personalized." loading="lazy" /></p><p><em>Created with CrossCraft. Topic: Pop Culture. The last question is personalized.</em></p><h2 id="full-personalization-possible">Full Personalization possible</h2><p>CrossCraft isn’t just another crossword puzzle app. It’s a tool that allows you to create an <strong>infinite number</strong> of varied crosswords on a selected topic. On top of that, you can add your own clue-answer pairs to <strong>personalize</strong> the crossword or to ask more specific questions targeted to your audience. The app will always place your custom questions into the puzzle first before filling the gaps with the selected topic. That means, if you provide enough custom questions, you can even create entirely custom crosswords with nearly no fill-words from the selected topic. Perfect for teachers preparing a fun challenge for their students.</p><p>To prevent inconvenient typing on a phone or tablet, the app can <strong>import</strong> lists of clue-answer pairs from <strong>CSV or JSON</strong> files. This way, you can easily create your custom content on your computer using tools like Google Sheets, which even allows collaboration with others to get more content faster. When you’re ready, save the sheet as a CSV file and create your entirely custom crossword.</p><h2 id="lots-of-topics-to-choose-from">Lots of Topics to Choose from</h2><p>Creating a large enough set of clue-answer pairs can be a tedious and time-consuming task. For a well-filled medium-sized crossword, hundreds of pairs could be needed, depending on the length of the answers and their characters. That’s why CrossCraft ships with a wide variety of topics that you can use as a fallback to fill in the gaps. Or to simply create a crossword that is themed without any custom questions. The personalization step is entirely optional!</p><p><img src="/assets/images/blog/introducing-crosscraft/topics.webp" alt="Topics" loading="lazy" /></p><p>In the first release, 20 different topics from 5 different categories are supported:</p><ul><li><p><strong>General Knowledge:</strong> Famous People, Geography, History, Pop Culture</p></li><li><p><strong>Movie Franchise:</strong> Harry Potter, Lord of the Rings, Marvel (MCU), Star Wars</p></li><li><p><strong>Sports:</strong> American Football, Basketball, Formula 1, Soccer</p></li><li><p><strong>Language Learning:</strong> English, French, Japanese, Spanish</p></li><li><p><strong>Programming:</strong> Android Dev., Apple Platf. Dev., Flutter, Ruby on Rails</p></li></ul><p>More topics in more categories will be added with each update. Some of the next topics I have on my roadmap are Movie Quotes, Game of Thrones, Anime, more languages like German or Italian, and many more in new categories like music, art, literature, science, and technology.</p><p>Which topics I include next entirely depends on what users want most: Inside the app, there’s a “Suggest Topic” button – make sure to use it for your favorite topic!</p><p>Other features like the ability to define a solution word are already planned.</p><h2 id="in-all-shapes-and-sizes">In all Shapes and Sizes</h2><p>When creating a crossword puzzle, you can choose from 5 different sizes, 3 different shapes, and 3 different difficulty levels. For example, the topic “Spanish” contains the 600 most frequent words in Spanish. If the easiest difficulty level is selected, only the 200 most frequent words are used, 400 in the mid-level, and all 600 in the hardest level. This way you can start easy and make it harder when you tend to know all the easy answers.</p><p><img src="/assets/images/blog/introducing-crosscraft/app-preview-6.7.gif" alt="App preview 6.7" loading="lazy" /></p><p>For educators, it can be important to print a full-page crossword. The shapes “landscape” and “portrait” are sized to perfectly fill a full page. When sharing or printing a created crossword, CrossCraft provides the option to export the puzzle and the clue parts separately. This way, the puzzle can be printed on one page, and the clues on a second, improving readability, especially for larger crosswords.</p><h2 id="for-everyone-and-everywhere">For Everyone and Everywhere</h2><p>Both the app and all topic contents are available in <strong>7 languages</strong>: English, French, German, Italian, Portuguese (Brazil), Spanish, and Turkish. The language for the clues can be selected independently from the app’s interface language, allowing it to easily create crosswords for different language audiences. Where appropriate, the topic contents were adjusted for the audience of the language. For example, when asking for famous TV hosts, in English these could be “Oprah Winfrey” or “David Letterman”, whereas in German other names like “Günther Jauch” and “Anne Will” are more appropriate. These adjustments were made for all languages.</p><p>Also, note that no server-side is involved when creating the crosswords or playing them. Everything happens on-device, which means that the app also fully works offline. This is great for playing on the train or an airplane with no internet.</p><h2 id="what-are-you-waiting-for-get-it-now">What are you waiting for? Get it now!</h2><p>Creating an infinite amount of personalized crosswords, plus playing and sharing them is completely free. No ads. No tracking. No traps. Only some options and the file import are considered “pro features” and can be enabled for a small fee.</p><h2 id="in-summary">In Summary</h2><ul><li><p><strong>Customize with Ease</strong><br />Choose from diverse topics. Add your own questions. Import them to save time.</p></li><li><p><strong>Educational and Fun</strong><br />Ideal for teachers and learners. With print-friendly sized exports.</p></li><li><p><strong>Multilingual and Offline</strong><br />Enjoy in 7 languages and create &amp; play even with no internet connection.</p></li></ul><h2 id="download-and-explore">Download and Explore</h2><p>CrossCraft is available on iPhone, iPad, and Apple Silicon Macs. A native Mac app for all Macs is nearing completion and will follow soon.</p><p>Get the app now: 👇👇👇</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>For journalists and bloggers, a PressKit is available <a href="https://github.com/FlineDev/CrossCraft-LandingPage/raw/main/downloads/PressKit-v1.0.zip">here</a>.</p>]]></content:encoded>
</item>
<item>
<title>Building an AsyncButton in SwiftUI</title>
<link>https://fline.dev/snippets/asyncbutton-swiftui-progress-status/</link>
<guid isPermaLink="true">https://fline.dev/snippets/asyncbutton-swiftui-progress-status/</guid>
<pubDate>Wed, 27 Sep 2023 00:00:00 +0000</pubDate>
<description><![CDATA[A reusable button component that handles async actions with automatic loading state, disabling, and success/failure indication.]]></description>
<content:encoded><![CDATA[<h2 id="the-need-for-asyncbutton">The Need for AsyncButton</h2><p>Standard SwiftUI <code>Button</code> actions are synchronous. When you need to perform an async operation – a network request, a database write, a StoreKit purchase – you end up manually managing a <code>Task</code>, tracking loading state, disabling the button, and handling errors. This boilerplate repeats across every async button in your app.</p><p>I built an <code>AsyncButton</code> component that wraps all of this into a single reusable view.</p><p><video src="/assets/images/snippets/asyncbutton-swiftui-progress-status/demo.mp4" controls muted playsinline></video></p><h2 id="a-simplified-implementation">A Simplified Implementation</h2><p>Here is the core idea:</p><pre><code class="language-swift">struct AsyncButton&lt;Label: View&gt;: View {
   let action: () async throws -&gt; Void
   let label: () -&gt; Label

   @State private var isRunning = false
   @State private var result: Result&lt;Void, Error&gt;?

   var body: some View {
      Button {
         isRunning = true
         Task {
            do {
               try await action()
               result = .success(())
            } catch {
               result = .failure(error)
            }
            isRunning = false
         }
      } label: {
         HStack(spacing: 8) {
            label()
            if isRunning {
               ProgressView()
            }
         }
      }
      .disabled(isRunning)
   }
}</code></pre><p>The button creates a <code>Task</code> internally, so callers can use <code>await</code> directly in the action closure. While the task runs, a <code>ProgressView</code> appears next to the label and the button is disabled to prevent duplicate submissions. The <code>result</code> state can drive success or failure indicators – a checkmark, a color flash, or a shake animation.</p><h2 id="usage">Usage</h2><p>Using it feels natural:</p><pre><code class="language-swift">AsyncButton {
   try await viewModel.submitOrder()
} label: {
   Text(&quot;Place Order&quot;)
}</code></pre><p>No manual state management, no <code>Task</code> creation at the call site. The full implementation in <a href="https://github.com/FlineDev/HandySwiftUI">HandySwiftUI</a> adds configurable success/failure animations, customizable progress indicators, and support for button styles. But the core pattern above covers the most common case and is straightforward to adapt to your own projects.</p>]]></content:encoded>
</item>
<item>
<title>ImageRenderer Cannot Export UIKit-Backed Views</title>
<link>https://fline.dev/snippets/imagerenderer-uikit-backed-views/</link>
<guid isPermaLink="true">https://fline.dev/snippets/imagerenderer-uikit-backed-views/</guid>
<pubDate>Tue, 19 Sep 2023 00:00:00 +0000</pubDate>
<description><![CDATA[SwiftUI's ImageRenderer silently fails on views that use UIKit or AppKit under the hood, like List and ScrollView.]]></description>
<content:encoded><![CDATA[<h2 id="the-limitation">The Limitation</h2><p>SwiftUI’s <code>ImageRenderer</code> lets you render any SwiftUI view into a <code>UIImage</code> or <code>CGImage</code>. It works well for pure SwiftUI views, but silently fails – producing a blank or incomplete image – when the view tree contains components backed by UIKit or AppKit. This includes several commonly used views:</p><ul><li><p><code>List</code> (wraps UITableView / NSTableView)</p></li><li><p><code>ScrollView</code> (wraps UIScrollView / NSScrollView)</p></li><li><p><code>TextEditor</code> (wraps UITextView / NSTextView)</p></li><li><p><code>Map</code> (wraps MKMapView)</p></li></ul><p>There is no compiler warning or runtime error. The renderer simply does not capture those portions of the view hierarchy.</p><h2 id="the-workaround">The Workaround</h2><p>When I needed to export a list-like layout as an image, the solution was to replace <code>List</code> with a pure SwiftUI equivalent built from <code>VStack</code> and manual styling:</p><pre><code class="language-swift">let exportView = VStack(spacing: 0) {
   ForEach(items) { item in
      HStack {
         Text(item.name)
         Spacer()
         Text(item.value)
            .foregroundStyle(.secondary)
      }
      .padding(.horizontal, 16)
      .padding(.vertical, 12)

      if item.id != items.last?.id {
         Divider()
      }
   }
}
.background(.white)
.frame(width: 390)

let renderer = ImageRenderer(content: exportView)
renderer.scale = UIScreen.main.scale

if let image = renderer.uiImage {
   // Use the rendered image
}</code></pre><p>The <code>VStack</code> with <code>ForEach</code> replicates the visual structure of a <code>List</code> without relying on any UIKit-backed views. Adding dividers, padding, and a background produces a result that looks close enough to a standard list for export purposes.</p><h2 id="practical-advice">Practical Advice</h2><p>If you plan to use <code>ImageRenderer</code> in your app, design your exportable views with this constraint in mind from the start. Build them from basic SwiftUI primitives: stacks, text, shapes, and images. Avoid any view that you know wraps a platform-specific control. Testing the render output early saves the frustration of discovering blank regions later.</p>]]></content:encoded>
</item>
<item>
<title>Combine Swift Imports with a Wrapper Module</title>
<link>https://fline.dev/snippets/combine-swift-imports-wrapper-module/</link>
<guid isPermaLink="true">https://fline.dev/snippets/combine-swift-imports-wrapper-module/</guid>
<pubDate>Wed, 13 Sep 2023 00:00:00 +0000</pubDate>
<description><![CDATA[Create a single import that re-exports all your commonly used frameworks using @_exported import.]]></description>
<content:encoded><![CDATA[<h2 id="the-repetitive-import-problem">The Repetitive Import Problem</h2><p>In any Swift project of moderate size, you end up with the same set of imports at the top of nearly every file. Foundation, SwiftUI, OSLog, maybe Observation – the list grows as you adopt new frameworks. With the move to structured logging via OSLog, I found myself adding <code>import OSLog</code> to almost every file alongside the usual suspects.</p><p>The solution is a wrapper module that re-exports everything you commonly need through a single import.</p><h2 id="setting-up-the-wrapper-module">Setting Up the Wrapper Module</h2><p>Create a new target in your Swift package or Xcode project. In my case, I called it <code>AppFoundation</code>. The entire module consists of a single file:</p><pre><code class="language-swift">// Sources/AppFoundation/Exports.swift
@_exported import Foundation
@_exported import SwiftUI
@_exported import OSLog
@_exported import Observation</code></pre><p>The <code>@_exported</code> attribute makes all public symbols from each framework available to any file that imports <code>AppFoundation</code>. Now, every file in your app just needs:</p><pre><code class="language-swift">import AppFoundation</code></pre><p>In your <code>Package.swift</code>, the target has no source code of its own beyond the exports file. Your app target declares a dependency on <code>AppFoundation</code>, and all the re-exported frameworks become available everywhere.</p><h2 id="trade-offs-to-consider">Trade-Offs to Consider</h2><p>This approach has clear benefits: less boilerplate, fewer merge conflicts in import sections, and a single place to add new framework imports. But there are trade-offs.</p><p>First, <code>@_exported</code> is an underscored attribute, meaning it is not officially part of the stable Swift API. In practice, it has been stable for years and is used widely in the ecosystem, but it carries no formal guarantee.</p><p>Second, implicit dependencies can make it harder to understand what a file actually uses. When every file has access to everything, you lose the documentation value of explicit imports. If you later extract a module into a standalone package, you will need to add the explicit imports back.</p><p>For app targets where convenience matters more than strict modularity, the wrapper module pattern saves real time. For reusable libraries, explicit imports remain the better choice.</p>]]></content:encoded>
</item>
<item>
<title>Learnings from Analyzing 20 Successful Mobile Paywalls</title>
<link>https://fline.dev/blog/freemiumkit-learnings-from-analyzing-mobile-paywalls/</link>
<guid isPermaLink="true">https://fline.dev/blog/freemiumkit-learnings-from-analyzing-mobile-paywalls/</guid>
<pubDate>Sun, 10 Sep 2023 00:00:00 +0000</pubDate>
<description><![CDATA[Dive into how FreemiumKit, a user-friendly paywall open-source library, simplifies the creation of successful paywalls and streamlines A/B testing. Its highly customizable UI components are based on my deep analysis of common paywall designs.]]></description>
<content:encoded><![CDATA[<p>After trying out RevenueCat and finding out (<a href="https://twitter.com/jeehut/status/1658394554037424128?s=61&t=JK1yW_OmTUCtNsXRmpB8FA">publicly</a>) that it didn’t meet my (admittedly very high) expectations for ease of use, I decided to tackle the problem myself and started working on the open-source library <a href="https://github.com/FlineDev/FreemiumKit">FreemiumKit</a>. To me, “ease of use” for a framework that helps with In-App Purchases means 3 things:</p><ol><li><p>A clear step-by-step guide on how to get started – including App Store Connect.</p></li><li><p>A simple but flexible permissions system based on the current user’s purchases.</p></li><li><p>A unified paywall design framework for reusable shared paywall UI code.</p></li></ol><p>The first two, I plan to cover in the libraries’ README. This article focuses on the third aspect: How does FreemiumKit help you build a successful paywall quickly? And how can it help you with A/B testing?</p><p><img src="/assets/images/blog/freemiumkit-learnings-from-analyzing-mobile-paywalls/freemiumkit-makes.webp" alt="FreemiumKit makes StoreKit 2 integration easy as pie. With built-in paywalls &amp; permissions." loading="lazy" /></p><p><em>FreemiumKit makes StoreKit 2 integration easy as pie. With built-in paywalls &amp; permissions.</em></p><h2 id="basic-idea">Basic Idea</h2><p>One of the things I had expected from RevenueCat, simply because people were soo positive about it all the time, was that they provide ready-to-use UI components in their Swift SDK. But they don’t. (UPDATE: After my complaints, they started <a href="https://twitter.com/RevenueCat/status/1697253094520967183">working on it</a>!) All they do is integrate with another service that specializes in providing paywalls and helping users optimize their paywalls by trying out different designs: <a href="https://superwall.com/">Superwall</a>. By the way, if you don’t know what A/B testing is – trying out different designs and assessing the data to see what works best is pretty much the definition. A paywall is probably the most important screen to A/B test. So while I really like the idea of the service, its pricing doesn’t scale down very well: They require $0.20 for each purchase, which for a monthly subscription of $1 is 20% of your income – that’s even higher than the 15% Apple keeps for Small Businesses!</p><p>Also, I’m personally not a fan of using many 3rd party services, I like to keep my app’s privacy policies short and simple, which becomes harder to do with every new service added. So I rather prefer to select a more “general purpose” analytics service that comes with A/B testing support (among other things) and choose that wisely than integrating with many microservices. But of course, that’s just my personal taste and you might choose differently. There’s certainly one thing I really love about Superwall though: They run the <a href="https://www.paywallscreens.com/">PaywallScreens</a> website, where you can find a nice overview of thousands of real-world paywalls sorted by the estimated income that each app generates, currently led by YouTube, TikTok, and Disney+.</p><hr /><p>In the first release of FreemiumKit, I wanted to ship everything needed to build a successful paywall UI quickly, so I scrolled through all 312 paywall screens of apps with a higher estimate than $500,000 income per month, picked the 20 screens I found most inviting &amp; clean and analyzed them to find what they have in common. Then, based on my learnings, I developed 2 different and highly customizable UI components that allow you to create most of the variants I analyzed!</p><p>Here are the 20 screens I selected in falling grossing order:</p><p><img src="/assets/images/blog/freemiumkit-learnings-from-analyzing-mobile-paywalls/paywallscreens-com.webp" alt="Source: paywallscreens.com" loading="lazy" /></p><p><em>Source: <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="Source: paywallscreens.com" loading="lazy" /></p><p><em>Source: <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="Source: paywallscreens.com" loading="lazy" /></p><p><em>Source: <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="Source: paywallscreens.com" loading="lazy" /></p><p><em>Source: <a href="https://www.paywallscreens.com/">paywallscreens.com</a></em></p><h2 id="common-design-choices">Common Design Choices</h2><p>Everything I recognized which at least 50% (of a kind) have in common:</p><ol><li><p>100% offer either a Monthly or a Weekly “short-term” plan.</p></li><li><p>100% offer either a Yearly or Lifetime “long-term” plan.</p></li><li><p>100% use plain white or black text for the plans or unlocked features list.</p></li><li><p>95% cover the entire screen without any unrelated navigational elements.</p></li><li><p>95% fit all important information into one screen without the need to scroll.</p></li><li><p>90% use a background color to highlight their main call-to-action button.</p></li><li><p>85% have the pricing plans listed on the bottom half of the screen.</p></li><li><p>80% use a rounded border outline to highlight the currently selected option.</p></li><li><p>80% have either a back arrow or an “X” button at the top to leave.</p></li><li><p>61% of the “X” buttons are put on the left corner (harder to reach for most).</p></li><li><p>80% have a clearly highlighted call-to-action button at the very bottom.</p></li><li><p>75% of the call-to-action buttons are capsule-shaped (ends 100% rounded).</p></li><li><p>56% of the call-to-action buttons use the exact same title: “Continue”</p></li><li><p>70% use a vertical list of buttons for the different plan options.</p></li><li><p>70% of those with a preselection opt for the long-term recurring option.</p></li><li><p>60% use a white background behind the list of plans to select from.</p></li><li><p>55% mention the name of the app somewhere in the top half.</p></li><li><p>65% offer either a list, grid, or page view with 2-6 (avg. 4) features unlocked.</p></li><li><p>77% of those offering a list use a checkmark icon in front of each feature.</p></li><li><p>55% use a background image stretching all to the top edge of the screen.</p></li><li><p>50% offer a free trial for at least one paid plan.</p></li><li><p>50% mention a discount percentage compared to other options.</p></li></ol><h2 id="less-common-design-choices">Less Common Design Choices</h2><p>Things that I noticed some paywalls do, but the majority of them do not:</p><ol><li><p>45% use a different background color for the currently selected plan.</p></li><li><p>35% have a “Popular” / “Recommended” tag on one of the purchase options.</p></li><li><p>30% have a radio button like a dot or checkbox to highlight the selection.</p></li><li><p>30% use a horizontal list of buttons for the different plan options.</p></li><li><p>30% provide a (less highlighted) “Restore” button within the paywall.</p></li><li><p>30% provide a (less highlighted) link to their terms and privacy policy.</p></li><li><p>15% use a page view to show off the features unlocked by the purchase.</p></li><li><p>10% provide a segmented control to switch between Monthly/Yearly etc.</p></li><li><p>5% have an animation on their call-to-action button (or anywhere else).</p></li></ol><h2 id="the-paywall-blueprint">The Paywall Blueprint</h2><p>With the learnings from above, I built a “blueprint” that incorporates them all:</p><p><img src="/assets/images/blog/freemiumkit-learnings-from-analyzing-mobile-paywalls/this-is-the-only-part-in-2.webp" alt="This is the only part in the paywall blueprint with complex logic." loading="lazy" /></p><p>I’m not a professional designer, but I think it’s good enough for the first public release of FreemiumKit. Better designers than me in the community are invited to contribute (fancy) improvements or entirely different designs. The README has a <a href="https://github.com/FlineDev/FreemiumKit#implementing-a-custom-ui">dedicated section</a> describing how to build your own designs. From what this screen looks like overall and looking at the 20 paywall designs I checked, I think it’s wise for the framework to focus on the part that involves more logic. The entire top half of the screen is very simple to do in SwiftUI (just an <code>Image</code> with an <code>.overlay</code>, a <code>Button</code> and a <code>List</code> of <code>Text</code> views). And the same is true for the less highlighted “Terms of Use”, “Restore”, and “Privacy Policy” buttons at the very bottom.</p><p><img src="/assets/images/blog/freemiumkit-learnings-from-analyzing-mobile-paywalls/this-is-the-only-part-in.webp" alt="This is the only part in the paywall blueprint with complex logic." loading="lazy" /></p><p><em>This is the only part in the paywall blueprint with complex logic.</em></p><p>So let’s focus on the part that loads and lists the available products, highlights the current selection, shows potentially available trial periods, long-term plan discounts, the price tag, a second monthly price tag for better comparison, a “Best Value” or “Most Popular” badge, handles the current plan selection &amp; the logic to show &amp; disable the “Continue” button when needed. As you can see, this part is quite complex to get right, and because (nearly) all that information is actually provided to the app by <code>StoreKit</code>, it’s a great opportunity to simplify things. By leaving the rest of the screen to the developer/designer with all the learnings and the blueprint shared above, users of the library should have full freedom &amp; flexibility to create a unique look for their app’s paywall.</p><p>To make things more fun and to make A/B testing possible even in the initial release of FreemiumKit, let’s also create a horizontal list variant like this:</p><p><img src="/assets/images/blog/freemiumkit-learnings-from-analyzing-mobile-paywalls/the-vertical-left-and.webp" alt="The vertical (left) and horizontal (right) paywall blueprints, side-by-side." loading="lazy" /></p><p>Putting them both side-by-side in their full-screen extent, they compare like this:</p><p><img src="/assets/images/blog/freemiumkit-learnings-from-analyzing-mobile-paywalls/the-vertical-left-and-2.webp" alt="The vertical (left) and horizontal (right) paywall blueprints, side-by-side." loading="lazy" /></p><p><em>The vertical (left) and horizontal (right) paywall blueprints, side-by-side.</em></p><h2 id="vertical-horizontal-products-style">Vertical &amp; Horizontal Products Style</h2><p>FreemiumKit ships with a SwiftUI view named <code>AsyncProducts</code>, which works quite like <code>AsyncImage</code>: You provide it with the product identifiers you want to show in your paywall (like you provide <code>AsyncImage</code> with a URL of your image), and <code>AsyncProducts</code> takes care of fetching the products from the App Store (like <code>AsyncImage</code> fetches the images from the web), shows a placeholder while loading the data, and even presents an error message with a reload button if there are network issues. In other words: It does all the hard work, you just need to decide where on your paywall you want to place it and what size works best for you:</p><pre><code class="language-Swift">AsyncProducts(
   style: PlainProductsStyle(), 
   productIDs: ProductID.allCases, 
   inAppPurchase: AppDelegate.inAppPurchase
)</code></pre><p><em>Sample usage of <code>AsyncProducts</code>.</em></p><p>The <code>productIDs</code> &amp; <code>inAppPurchase</code> parameters are explained in the step-by-step <a href="https://github.com/FlineDev/FreemiumKit#getting-started">Getting Started guide</a> within the README. What we care about in this article is the <code>style</code> parameter that allows you to pass in different designs for the UI component. The <code>PlainProductsStyle</code> is just a demo style that shows the minimal implementation of a style for those who want to create their own styles. It’s not intended for direct use. The two real styles FreemiumKit ships with in <code>1.0</code> are:</p><ul><li><p><code>VerticalPickerProductsStyle</code> for the left design from above</p></li><li><p><code>HorizontalPickerProductsStyle</code> for the right design from above (WIP)</p></li></ul><p>Also, I plan on adding at least one more style named something like <code>HorizontalButtonsProductStyle</code> which implements a UI without a “Continue” button like used in the paywall of Apple’s newest app <a href="https://apps.apple.com/de/app/final-cut-pro-f%C3%BCr-das-ipad/id1631624924">Final Cut Pro for iPad</a>:</p><p><img src="/assets/images/blog/freemiumkit-learnings-from-analyzing-mobile-paywalls/paywall-in-apple-s-new.webp" alt="Paywall in Apple’s new Final Cut Pro app for iPad." loading="lazy" /></p><p><em>Paywall in Apple’s new Final Cut Pro app for iPad.</em></p><p>But let’s focus on the styles available today and use the vertical picker instead:</p><pre><code class="language-Swift">AsyncProducts(
   style: VerticalPickerProductsStyle(
      preselectedProductID: ProductID.proYearly,
      tintColor: .blue
   ), 
   ...
)</code></pre><p>It takes two arguments, one which lets you specify the product which you want to highlight as a pre-selection. Learning #15 from the above analysis tells us that 70% preselect the long-term recurring option. The other argument lets you choose the tint color, which is used for both the “Continue” button background and to highlight the user’s current selection. Just make sure to select a color that contrasts nicely with white because that’s the continue button text color.</p><p>If you want to test out different paywall designs, it’s very easy to switch out the style for A/B testing: Just replace <code>VerticalPickerProductsStyle</code> with <code>HorizontalPickerProductsStyle</code> and all should just work. These two styles even take the same arguments, so it’s really easy. Other styles like the planned <code>HorizontalButtonsProductsStyle</code> will have other arguments, cause there’s no notion of “selection” for a direct button-only style like that, but it should still be easy enough to change that up. All styles conform to the <code>AsyncProductsStyle</code> protocol, so if you want to create a variable to which you can assign different styles based on your A/B test group, you can use <code>any AsyncProductsStyle</code> as its type.</p><h2 id="conclusion">Conclusion</h2><p>And these are my learnings from analyzing 20 successful paywalls. I put all my learnings into the <code>VerticalPickerProductsStyle</code> which ships in FreemiumKit, so you can easily use <code>AsyncProducts</code> view in SwiftUI instead of dealing with complex logic. You just need to remember to apply the less complex learnings like placing <code>AsyncProducts</code> in the bottom half of your screen, providing both a short-term and long-term subscription, or providing a list of features to unlock in the top half.</p>]]></content:encoded>
</item>
<item>
<title>SwiftUI Navigation: Present Data, Not Views</title>
<link>https://fline.dev/snippets/swiftui-navigation-present-data/</link>
<guid isPermaLink="true">https://fline.dev/snippets/swiftui-navigation-present-data/</guid>
<pubDate>Thu, 27 Jul 2023 00:00:00 +0000</pubDate>
<description><![CDATA[Understanding the mental model shift from imperative navigation in UIKit to data-driven navigation in SwiftUI.]]></description>
<content:encoded><![CDATA[<h2 id="the-mental-model-shift">The Mental Model Shift</h2><p>In UIKit, navigation is imperative. You tell the system exactly what to do:</p><pre><code class="language-swift">let detailVC = DetailViewController()
detailVC.item = selectedItem
navigationController?.pushViewController(detailVC, animated: true)</code></pre><p>You create a view controller, configure it, and push it onto the stack. You are in control of the action.</p><p>SwiftUI works differently. You do not navigate – you present data in new views. Everything is data-driven. You do not control views. You control data.</p><h2 id="data-driven-navigation-in-practice">Data-Driven Navigation in Practice</h2><p>With <code>NavigationStack</code>, navigation is driven by state. You declare what data maps to what view, and SwiftUI handles the transitions:</p><pre><code class="language-swift">struct ContentView: View {
   @State private var path: [Item] = []

   var body: some View {
      NavigationStack(path: $path) {
         List(items) { item in
            Button(item.name) {
               path.append(item)  // Modify data, not views
            }
         }
         .navigationDestination(for: Item.self) { item in
            DetailView(item: item)
         }
      }
   }
}</code></pre><p>The key line is <code>path.append(item)</code>. You are not pushing a view. You are adding data to an array. SwiftUI observes the change and presents the corresponding view automatically.</p><h2 id="why-this-matters">Why This Matters</h2><p>This distinction is not just philosophical – it has practical consequences. Because navigation is state, you get deep linking for free by constructing the right path array. You can persist and restore navigation state by saving the path. You can programmatically navigate to any depth by appending multiple items at once.</p><p>It also means dismissal is just data removal. Calling <code>path.removeLast()</code> pops the top view. Clearing the array returns to root. No need to track view controller references or walk the navigation hierarchy.</p><p>The shift takes time to internalize, especially if you have years of UIKit experience. But once it clicks, SwiftUI navigation becomes far more predictable. Your views become pure functions of your data, and navigation is just another piece of that data.</p>]]></content:encoded>
</item>
<item>
<title>AsyncImage Does Not Support .resizable()</title>
<link>https://fline.dev/snippets/asyncimage-resizable-modifier/</link>
<guid isPermaLink="true">https://fline.dev/snippets/asyncimage-resizable-modifier/</guid>
<pubDate>Wed, 26 Jul 2023 00:00:00 +0000</pubDate>
<description><![CDATA[SwiftUI's AsyncImage does not allow the .resizable() modifier, requiring a phase-based workaround.]]></description>
<content:encoded><![CDATA[<h2 id="the-problem">The Problem</h2><p>SwiftUI’s <code>AsyncImage</code> is convenient for loading remote images, but it has a surprising limitation: you cannot apply the <code>.resizable()</code> modifier to it. This code compiles but does not behave as expected:</p><pre><code class="language-swift">// This does NOT work as intended
AsyncImage(url: imageURL)
   .resizable()  // Has no effect -- AsyncImage is not an Image
   .aspectRatio(contentMode: .fill)
   .frame(width: 200, height: 200)</code></pre><p>The reason is that <code>AsyncImage</code> is not an <code>Image</code> – it is a container view that manages loading state. The <code>.resizable()</code> modifier is defined only on <code>Image</code>, so applying it to <code>AsyncImage</code> just calls the generic <code>View</code> version, which does nothing useful.</p><h2 id="the-solution">The Solution</h2><p>The fix is to use the phase-based initializer, which gives you direct access to the underlying <code>Image</code> value once loading completes:</p><pre><code class="language-swift">AsyncImage(url: imageURL) { phase in
   switch phase {
   case .success(let image):
      image
         .resizable()
         .aspectRatio(contentMode: .fill)
   case .failure:
      Image(systemName: &quot;photo&quot;)
         .foregroundStyle(.secondary)
   case .empty:
      ProgressView()
   @unknown default:
      EmptyView()
   }
}
.frame(width: 200, height: 200)
.clipped()</code></pre><p>Inside the <code>.success</code> case, <code>image</code> is a real <code>Image</code> value, so <code>.resizable()</code> works correctly. This also gives you control over the loading and error states, which is better practice anyway.</p><h2 id="when-to-load-manually">When to Load Manually</h2><p>If you need the raw image data – for example, to cache it, inspect its size, or create a <code>UIImage</code> – you may want to skip <code>AsyncImage</code> entirely and load with <code>URLSession</code>. But for most display-only cases, the phase-based initializer covers the need without extra complexity.</p><p>This is one of those SwiftUI APIs where the simple initializer looks appealing but falls short in practice. Default to the phase-based version whenever you need any image-specific modifiers.</p>]]></content:encoded>
</item>
<item>
<title>Multi-Line Code with Ctrl+M in Xcode 15</title>
<link>https://fline.dev/snippets/multi-line-code-ctrl-m-xcode-15/</link>
<guid isPermaLink="true">https://fline.dev/snippets/multi-line-code-ctrl-m-xcode-15/</guid>
<pubDate>Tue, 25 Jul 2023 00:00:00 +0000</pubDate>
<description><![CDATA[Xcode 15 introduces a Ctrl+M shortcut to expand function calls and parameters across multiple lines.]]></description>
<content:encoded><![CDATA[<h2 id="expanding-code-to-multiple-lines">Expanding Code to Multiple Lines</h2><p>Xcode 15 introduced a small but impactful editing shortcut: Ctrl+M. Place your cursor on a function call, initializer, or any comma-separated parameter list, press Ctrl+M, and Xcode automatically expands it across multiple lines – one parameter per line, properly indented.</p><p><img src="/assets/images/snippets/multi-line-code-ctrl-m-xcode-15/ctrl-m-shortcut.webp" alt="Ctrl+M shortcut" loading="lazy" /></p><p>Before this shortcut, reformatting a long function call meant manually adding line breaks and fixing indentation. Consider a call like this:</p><pre><code class="language-swift">let label = UILabel(frame: .zero, font: .systemFont(ofSize: 14), textColor: .label, numberOfLines: 0)</code></pre><p>After pressing Ctrl+M with the cursor on that line, Xcode reformats it to:</p><pre><code class="language-swift">let label = UILabel(
   frame: .zero,
   font: .systemFont(ofSize: 14),
   textColor: .label,
   numberOfLines: 0
)</code></pre><h2 id="when-to-use-it">When to Use It</h2><p>This shortcut is most valuable when you are writing SwiftUI view modifiers or initializers that accumulate parameters over time. A view that starts with two parameters often grows to five or six as you add configuration. Rather than reformatting manually each time, Ctrl+M handles it in one keystroke.</p><p>It also works in reverse – if your parameters are already on separate lines, pressing Ctrl+M collapses them back into a single line. This toggle behavior makes it easy to switch between compact and expanded formats depending on readability needs.</p><p>One thing to note: the shortcut operates on the innermost parameter list at the cursor position. If you have nested function calls, position your cursor carefully to expand the right one.</p>]]></content:encoded>
</item>
<item>
<title>The Missing String Catalogs FAQ for Localization in Xcode 15</title>
<link>https://fline.dev/blog/the-missing-string-catalogs-faq-for-xcode-15/</link>
<guid isPermaLink="true">https://fline.dev/blog/the-missing-string-catalogs-faq-for-xcode-15/</guid>
<pubDate>Tue, 04 Jul 2023 00:00:00 +0000</pubDate>
<description><![CDATA[Discover the game-changing implications of Apple's new feature, String Catalogs, which replaces traditional localization files and streamlines the localization process. From automatic key extraction to safety checks, find out why developers should be excited about this powerful tool in Xcode 15.]]></description>
<content:encoded><![CDATA[<p>In WWDC23 Apple introduced a new feature to Xcode that sherlocked large parts of my <a href="https://remafox.app/">RemafoX</a> app. Of course, I tested the new feature in detail and asked all my questions to the team who built it in the related Slack activity. They actually did a great job with String Catalogs, so much so that I will reimagine my app since they’ve taken away most of the low-level work that I did in my app in the past.</p><p>But most people I talked to since Dub Dub are still unaware of the implications String Catalogs have on their projects. So I figured I should answer the most frequent questions to make it more clear how amazing String Catalogs really are.</p><h3 id="what-are-string-catalogs-what-about-stringsdict-files">What are String Catalogs? What about Strings(dict) files?</h3><p>String Catalogs are files with the ending <code>.xcstrings</code> and store their content in a custom JSON format. The exact format is not documented (filed feedback to change that: FB12264877) and as a developer, you won’t ever need to fiddle with any JSON code because Xcode 15 ships with a visual editor:</p><p><img src="/assets/images/blog/the-missing-string-catalogs-faq-for-xcode-15/the-lack-of-a-green.webp" alt="The lack of a green checkmark for the German language indicates that the translation is incomplete." loading="lazy" /></p><p>String Catalogs replace both <code>.strings</code> and <code>.stringsdict</code> files and therefore support pluralization out-of-the-box. Unlike <code>.strings(dict)</code> files that are placed under locale-specific folders like <code>en.lproj</code>, String Catalogs encapsulate the translations of all supported languages in one file. This allows for safety checks like showing the progress of specific translations right in Xcode so you are aware of any translations missing (replacing the RemafoX Linter). Likewise, it will also add new keys to all languages automatically for you, saving you a lot of time (replacing the RemafoX Normalizer). And more features could come in future updates, like a check if your localizations have the same parameters (e.g. <code>%@</code>) as your source language (a feature I had planned for RemafoX, filed feedback: FB12264614).</p><h3 id="i-have-a-project-with-stringsdict-files-is-migration-easy">I have a project with Strings(dict) files. Is migration easy?</h3><p>Yes, totally! Migrating to String Catalogs is easy as pie: Simply right-click one of your <code>.strings(dict)</code> files and press “Migrate to String Catalog…”. This will open a modal with a list of all your <code>.strings(dict)</code> files in the project so you can choose which ones should be combined into one String Catalog file. If you have separate Strings files for different parts of your project, you can keep separate String Catalogs for them, there’s no need to put everything into one String Catalog.</p><h3 id="but-my-project-supports-older-os-versions-can-i-still-migrate">But my project supports older OS versions. Can I still migrate?</h3><p>Yes, totally! Apple engineers have done something very smart here: While we as developers can make full use of all String Catalog’s features, our apps won’t see any new file format like <code>.xcstrings</code>. Instead, during the build process Xcode will convert the String Catalog back to plain old <code>.strings</code> and <code>.stringsdict</code> files, thus ensuring support for all OS versions you can potentially target. This means, once you can switch to Xcode 15 for your project, you can make full use of String Catalogs without ever looking back!</p><blockquote><p>💁‍♂️ If you have been wishing for an SF Symbols-like app but for Localized Strings in code with official translations for all languages iOS is available in: I built exactly that and the feature is completely free!
Just <a href="https://translatekit.app/">download TranslateKit</a> and check your menu bar. 🌐 👍</p></blockquote><h3 id="im-using-swiftgenremafox-for-safe-localization-key-references-with-compiler-checks-will-i-lose-this-safety">I’m using SwiftGen/RemafoX for safe Localization key references with compiler checks. Will I lose this safety?</h3><p>No, not at all. Xcode not only introduces a new file format for your localizations, but it also comes with tools to automatically extract new localization keys from your project. If you use any of SwiftUI’s localization APIs with <code>LocalizedStringKey</code> or <code>LocalizedStringResource</code>, they automatically get detected and added to your String Catalog. But this doesn’t end with SwiftUI, it also works with plain Swift String APIs like <code>String(localized:)</code>, plain Obj-C-style APIs like <code>NSLocalizedString()</code> (even custom names supported), and even Interface Builder files like <code>.storyboard</code> and <code>.xib</code> files. For <code>Info.plist</code> files you need to create a dedicated String Catalog named <code>InfoPlist.xcstrings</code> and extraction will even work there automatically.</p><p>Note that you won’t get auto-completion for your keys in code like you now get for images and colors in your Asset Catalogs with Xcode 15. That’s because in Asset Catalogs the source of truth lies in the catalog itself, where your assets are placed. But because Xcode automatically extracts any added localizations from your source code, the source of truth for your localizations is reversed here and lies in your code. Therefore it’s impossible that you add a text to your code that lacks a counterpart in a Strings file (like it was possible before, leading to broken translations). Instead, whenever you add a localized String in your project, Xcode automatically creates a new translation key for all supported languages and you’ll see in the Strings Catalog that your translations are incomplete.</p><p><img src="/assets/images/blog/the-missing-string-catalogs-faq-for-xcode-15/the-lack-of-a-green-2.webp" alt="The lack of a green checkmark for the German language indicates that the translation is incomplete." loading="lazy" /></p><p><em>The lack of a green checkmark for the German language indicates that the translation is incomplete.</em></p><h3 id="what-about-typos-can-i-change-the-key-in-development">What about typos? Can I change the key in development?</h3><p>Xcode not only extracts new keys and adds them automatically to your Strings Catalog, but it will also make sure that any keys in your catalog that are no longer referenced in your project get deleted. It does this safely: If you have a key that you have not provided any translations for in any languages yet, then it will automatically delete the key if it no longer finds it in your project. This is what will typically happen when you have a typo in your key or simply want to improve the key during development and change it up. But if you do have some translations already, Xcode will instead mark the key in the String Catalog as “stale” with a yellow warning symbol. This makes it easy to spot keys no longer referenced, copy over their translations to the new key if needed and delete them afterward.</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="what-if-i-dont-want-to-localize-a-specific-text-in-codeib-file">What if I don’t want to localize a specific text in Code/IB file?</h3><p>Currently, there seems to be no direct way to control the extraction from the source of truth. I filed a feedback for IB files in particular (FB12264777) because my tools RemafoX (and its predecessor <a href="https://github.com/FlineDev/BartyCrouch">BartyCrouch</a>) support excluding there, therefore switching to String Catalogs might only be possible for people using those if there was some solution provided by Apple. While I also couldn’t find an explicit way to mark a String in code as “non-translatable”, in most SwiftUI views you will find an overload of the same API which takes a <code>Text</code> view instead of a String. For views where this applies, you can use <code>Text(verbatim: &quot;Your Text&quot;)</code> to create a <code>Text</code> view whose text won’t get extracted because the keyword <code>verbatim</code> marks it as non-translatable. But because an overload with a <code>Text</code> does not always exist, I filed a feedback for a more direct way to control extraction (FB12469163). For now, I found a workaround by simply using a String initializer in SwiftUI APIs that prefer the LocalizedStringKey overload, just pass something like <code>String(&quot;Your Text&quot;)</code>.</p><h2 id="conclusion">Conclusion</h2><p>The introduction of String Catalogs in Xcode 15 brings significant improvements to the localization workflow by simplifying the management of translation files. Developers can easily migrate their projects to String Catalogs and maintain the safety of localization key references, while still supporting older OS versions and having control over key changes and typos. Although there are some limitations and areas for improvement, String Catalogs are a huge step forward and have no real downsides compared to the old Strings file system. Migrate now!</p>]]></content:encoded>
</item>
<item>
<title>Introducing ReviewKit: Improve your App Store Rating with Ease</title>
<link>https://fline.dev/blog/introducing-reviewkit/</link>
<guid isPermaLink="true">https://fline.dev/blog/introducing-reviewkit/</guid>
<pubDate>Thu, 22 Jun 2023 00:00:00 +0000</pubDate>
<description><![CDATA[ReviewKit: Get app reviews from satisfied users at the right time. Say goodbye to intrusive prompts and optimize your app review process.]]></description>
<content:encoded><![CDATA[<p>As an app developer, you know how important user reviews are for the success and credibility of your application. Positive reviews not only attract more users but also contribute to higher rankings in the App Store. However, asking for reviews at the wrong time or from users who haven’t had a positive experience can lead to frustration and negative feedback. That’s where ReviewKit comes in – a powerful open-source tool that helps you request app reviews only from users who have had recent positive activity and at appropriate times.</p><h2 id="the-problem-and-solution">The Problem and Solution</h2><p>Traditionally, app developers have relied on simple prompts to ask users for reviews, often presented immediately after opening the app or at random intervals. This approach can be intrusive and irritating, resulting in users leaving negative reviews or even uninstalling the app.</p><p>I developed a fairly complex logic to determine when to ask for reviews in my first app, <a href="https://remafox.app/">RemafoX</a>. But for my most recent app (<a href="https://twoot-it.app/">Twoot it!</a>) I broke down what’s really important and further simplified the logic. ReviewKit is the result of this process.</p><p><img src="/assets/images/blog/introducing-reviewkit/sample-usage-of.webp" alt="Sample usage of ReviewKit in the Twoot it! app." loading="lazy" /></p><p>ReviewKit solves this problem by intelligently determining when to request app reviews based on the user’s recent positive activity. It ensures that only users who have had a satisfactory experience with your app and have engaged in certain activities are prompted to leave a review. By doing so, ReviewKit increases the likelihood of receiving positive reviews while minimizing user annoyance.</p><p>Of course, each app is different, so you need to specify what a “positive activity” is for your app. But ReviewKit takes care of the rest and is customizable, too!</p><h2 id="setting-up-reviewkit">Setting Up ReviewKit</h2><p><img src="/assets/images/blog/introducing-reviewkit/reviewkit-logo.webp" alt="ReviewKit Logo" loading="lazy" /></p><p>Getting started with ReviewKit is easy. Follow the instructions below to integrate it into your iOS or macOS app – your deployment target can be as old as iOS 11 or macOS 10.14, which should cover even most corporate apps:</p><h3 id="step-1-add-reviewkit-to-your-app">Step 1: Add ReviewKit to Your App</h3><p>To add ReviewKit to your project, use Swift Package Manager (SPM). In Xcode, navigate to your project and go to the “Swift Packages” tab. Click the “+” button and enter the ReviewKit repository URL:</p><pre><code>https://github.com/FlineDev/ReviewKit.git</code></pre><p>Lastly, make sure to select your app target to link <code>ReviewKit</code> to.</p><h3 id="step-2-adjust-review-criteria-optional">Step 2: Adjust Review Criteria (Optional)</h3><p>ReviewKit provides default criteria for requesting app reviews: It requires a minimum of 3 positive events within the last 14 days. If you want to customize these, you can adjust the criteria using <code>ReviewCriteria</code> like so:</p><pre><code class="language-swift">ReviewKit.criteria = ReviewCriteria(
   minPositiveEventsWeight: 5, 
   eventsExpireAfterDays: 30
)</code></pre><p>In the example above, the criteria have been modified to request reviews only when users have a minimum of 5 positive events, and the events expire after 30 days. That’s when you use the default <code>weight</code> of 1 for the calls down below.</p><h3 id="step-3-record-positive-events-request-a-review">Step 3: Record Positive Events &amp; Request a Review</h3><p>To trigger the app review request when users complete specific workflows or activities, you need to record positive events using ReviewKit. Call the following method whenever a user completes one of these activities:</p><pre><code class="language-swift">ReviewKit.recordPositiveEventAndRequestReviewIfCriteriaMet()</code></pre><p>This method will automatically determine if the user meets the criteria for requesting an app review based on their recent positive activity. If the criteria are met, the review prompt will be displayed.</p><h3 id="step-4-record-other-positive-activities-optional">Step 4: Record Other Positive Activities (Optional)</h3><p>Apart from the primary workflows, it is essential to take into account additional activities that signify positive user experiences. However, requesting reviews during these moments could interrupt users who are already engaged in a workflow, potentially leading to annoyance and decreased likelihood of leaving a review or even impacting their rating negatively. To monitor and capture these events, you can utilize the <code>recordPositiveEvent()</code> function:</p><pre><code class="language-swift">// Optionally, you can pass a custom `weight` parameter (defaults to 1)
ReviewKit.recordPositiveEvent()</code></pre><p>Note that both of the above methods optionally accept a <code>weight: Int</code> parameter, which you can use to fine-tune the criteria for requesting app reviews. For example, if your app had different levels of engagement, you could specify a weight of <code>3</code> for your highest-level activity and set the <code>minPositiveEventsWeight</code> to something like <code>10</code>. The default <code>weight</code> for positive events is <code>1</code>.</p><hr /><h2 id="example-usage-in-an-ios-application">Example Usage in an iOS Application</h2><p>To provide a better understanding, let’s consider a sample iOS application and demonstrate how ReviewKit can be used effectively. Suppose you have a social media app where users can post and like others’ posts. Here’s an example:</p><pre><code class="language-swift">import ReviewKit

func sendPost() {
  // ...
    
  // Record a positive event after sending a post
  ReviewKit.recordPositiveEventAndRequestReviewIfCriteriaMet(weight: 3)
}

func handlePostLike() {
  // ...
    
  // Record a positive event for liking a post unintrusively
  ReviewKit.recordPositiveEvent()
}</code></pre><p>In the example above, the <code>sendPost()</code> and <code>handlePostLike()</code> methods demonstrate how to record positive events for different activities. We call <code>recordPositiveEventAndRequestReviewIfCriteriaMet()</code> after a post was sent as this signifies the end of a workflow the user did in our app, which is a good time to request a review.</p><p>When users like a post, they’re still in the middle of the use case of consuming content, which is still a positive activity that we should record, but it could be intrusive to ask for a review at this time, so we just call <code>recordPositiveEvent()</code>.</p><p><img src="/assets/images/blog/introducing-reviewkit/sample-usage-of.gif" alt="Sample usage of ReviewKit in the Twoot it! app." loading="lazy" /></p><p><em>Sample usage of ReviewKit in the <a href="https://twoot-it.app/">Twoot it!</a> app.</em></p><h2 id="conclusion">Conclusion</h2><p><a href="https://github.com/FlineDev/ReviewKit">ReviewKit</a> offers a simple yet effective solution for asking users to review your app. By intelligently determining when to request app reviews based on recent positive activity, ReviewKit increases the chances of receiving positive reviews and helps your app grow. With easy integration and customizable criteria, ReviewKit is a valuable tool for any iOS developer or team.</p>]]></content:encoded>
</item>
<item>
<title>Search Apple Documentation with Shift+Cmd+O</title>
<link>https://fline.dev/snippets/search-apple-documentation-shift-cmd-o/</link>
<guid isPermaLink="true">https://fline.dev/snippets/search-apple-documentation-shift-cmd-o/</guid>
<pubDate>Sat, 10 Jun 2023 00:00:00 +0000</pubDate>
<description><![CDATA[Use the same Open Quickly shortcut to search Apple's developer documentation directly on the web.]]></description>
<content:encoded><![CDATA[<h2 id="open-quickly-for-documentation">Open Quickly for Documentation</h2><p>Most Xcode users are familiar with Shift+Cmd+O – the “Open Quickly” shortcut that lets you jump to any file, symbol, or type in your project. It is one of those shortcuts that becomes second nature within days of using Xcode. What I did not realize until recently is that the same shortcut now works on the Apple Developer documentation website, powered by DocC.</p><p><img src="/assets/images/snippets/search-apple-documentation-shift-cmd-o/search-documentation.webp" alt="Search documentation" loading="lazy" /></p><p>When browsing <a href="https://developer.apple.com/documentation">developer.apple.com/documentation</a>, pressing Shift+Cmd+O opens a search overlay that behaves just like Xcode’s Open Quickly dialog. You can type a framework name, a class, a method, or even a partial match, and results appear instantly. Selecting a result navigates directly to that documentation page.</p><h2 id="why-this-matters">Why This Matters</h2><p>Before this feature, searching Apple’s documentation meant scrolling through the sidebar hierarchy or using the general search bar, which often returned a mix of articles, tutorials, and API references. The Open Quickly overlay filters results to API symbols and pages, making it far more precise.</p><p>This is especially useful when you are in a browser reading a WWDC article or forum thread and need to quickly check the signature of a related API. Instead of switching back to Xcode, you stay in context and look it up directly.</p><p>The feature is part of Apple’s broader investment in DocC as a documentation platform. Since DocC powers both Xcode’s documentation viewer and the web-based documentation, it makes sense that the same interaction patterns carry over. If you spend time reading Apple docs in the browser, building this shortcut into your muscle memory is worth the effort.</p>]]></content:encoded>
</item>
<item>
<title>Xcode 15 Brings Type-Safe Asset Catalog Access</title>
<link>https://fline.dev/snippets/xcode-15-type-safe-asset-catalogs/</link>
<guid isPermaLink="true">https://fline.dev/snippets/xcode-15-type-safe-asset-catalogs/</guid>
<pubDate>Tue, 06 Jun 2023 00:00:00 +0000</pubDate>
<description><![CDATA[Xcode 15 generates type-safe Swift accessors for images and colors in asset catalogs, replacing the need for SwiftGen.]]></description>
<content:encoded><![CDATA[<h2 id="the-end-of-string-based-asset-references">The End of String-Based Asset References</h2><p>One of the quieter but impactful changes in Xcode 15 is built-in type-safe access to asset catalogs. Previously, referencing an image or color from your asset catalog required a string literal:</p><pre><code class="language-swift">// Before Xcode 15
Image(&quot;custom-header-icon&quot;)
Color(&quot;primaryBrand&quot;)</code></pre><p>This was fragile. Rename an asset and your code compiles fine but crashes or shows nothing at runtime. Tools like SwiftGen existed specifically to solve this by generating type-safe constants from your asset catalogs.</p><h2 id="what-changed">What Changed</h2><p>Xcode 15 now generates Swift accessors for every image and color in your asset catalog automatically. You access them through the resource initializers:</p><pre><code class="language-swift">// Xcode 15+
Image(.customHeaderIcon)
Color(.primaryBrand)</code></pre><p><img src="/assets/images/snippets/xcode-15-type-safe-asset-catalogs/asset-catalog-autocomplete.webp" alt="Xcode showing autocomplete suggestions for asset catalog resources" loading="lazy" /></p><p>The compiler knows about your assets. You get full autocomplete, and if you delete or rename an asset, you get a compile-time error instead of a silent runtime failure.</p><h2 id="apple-sherlocked-swiftgen">Apple Sherlocked SwiftGen</h2><p>This is effectively Apple integrating what SwiftGen has provided for years. For teams already using SwiftGen, this is a good time to evaluate whether you still need it. The built-in solution covers the two most common use cases – images and colors – without any build phase scripts or code generation steps.</p><p>There are still reasons to keep SwiftGen if you use it for fonts, localized strings, or other resource types. But for asset catalogs specifically, the native solution is now good enough for most projects, and it works out of the box with zero configuration.</p>]]></content:encoded>
</item>
<item>
<title>Xcode 15 String Catalogs Replace .strings and .stringsdict</title>
<link>https://fline.dev/snippets/xcode-15-string-catalogs/</link>
<guid isPermaLink="true">https://fline.dev/snippets/xcode-15-string-catalogs/</guid>
<pubDate>Tue, 06 Jun 2023 00:00:00 +0000</pubDate>
<description><![CDATA[Xcode 15 introduces String Catalogs, a visual editor for managing localized strings that replaces legacy .strings and .stringsdict files.]]></description>
<content:encoded><![CDATA[<h2 id="the-legacy-localization-workflow">The Legacy Localization Workflow</h2><p>Working with <code>.strings</code> and <code>.stringsdict</code> files has always been one of the rougher edges of Apple development. Plain text key-value files are easy to get wrong – missing semicolons, mismatched keys across languages, no way to see translation progress at a glance. And <code>.stringsdict</code> files, used for pluralization rules, are XML-based plist files that are notoriously difficult to author by hand.</p><h2 id="what-string-catalogs-bring">What String Catalogs Bring</h2><p>Xcode 15 introduces a new <code>.xcstrings</code> file format called String Catalogs. It replaces both <code>.strings</code> and <code>.stringsdict</code> with a single file that comes with a dedicated visual editor.</p><p><img src="/assets/images/snippets/xcode-15-string-catalogs/string-catalog-editor.webp" alt="The String Catalog editor in Xcode 15 showing translations across multiple languages" loading="lazy" /></p><p>The key improvements are substantial:</p><p><strong>Visual editor</strong> – All localizable strings are displayed in a table with columns for each language. You can see and edit translations inline without switching between files or using external tools.</p><p><strong>Translation progress tracking</strong> – Each language shows a completion percentage. At a glance, you can tell which languages need attention and which strings are still untranslated.</p><p><strong>Automatic string extraction</strong> – Xcode scans your Swift code and automatically discovers strings that need localization. New strings appear in the catalog without any manual registration step.</p><p><strong>Built-in migration</strong> – There is a migration path from existing <code>.strings</code> and <code>.stringsdict</code> files. Right-click your existing localization files and Xcode offers to convert them to the new format.</p><h2 id="practical-impact">Practical Impact</h2><p>For projects with even a handful of supported languages, this is a significant quality-of-life improvement. The visual editor alone eliminates an entire class of formatting errors. Combined with automatic extraction, it means fewer forgotten strings and a clearer picture of your localization status across the entire project.</p><p>If you are starting a new project on Xcode 15, String Catalogs are the default. For existing projects, the migration is straightforward and well worth doing.</p>]]></content:encoded>
</item>
<item>
<title>Xcode 15&apos;s Format to Multiple Lines Feature</title>
<link>https://fline.dev/snippets/xcode-15-format-parameters-multiple-lines/</link>
<guid isPermaLink="true">https://fline.dev/snippets/xcode-15-format-parameters-multiple-lines/</guid>
<pubDate>Tue, 06 Jun 2023 00:00:00 +0000</pubDate>
<description><![CDATA[Xcode 15 adds a built-in action to reformat long function parameter lists from a single line to multiple lines.]]></description>
<content:encoded><![CDATA[<h2 id="the-formatting-problem">The Formatting Problem</h2><p>Long function calls and declarations with many parameters are one of the most common readability issues in Swift code. You end up with lines that stretch well past any reasonable column limit:</p><pre><code class="language-swift">func configureView(title: String, subtitle: String, icon: Image, backgroundColor: Color, isEnabled: Bool, action: @escaping () -&gt; Void) {</code></pre><p>Manually breaking this into multiple lines is tedious. You have to position the cursor, add line breaks, indent each parameter, and make sure the trailing parenthesis lines up correctly.</p><h2 id="the-new-action-in-xcode-15">The New Action in Xcode 15</h2><p>Xcode 15 introduces a “Format to Multiple Lines” action that does this automatically. Place your cursor on a function call or declaration with multiple parameters, and Xcode offers to reformat it:</p><p><img src="/assets/images/snippets/xcode-15-format-parameters-multiple-lines/format-to-multiple-lines.webp" alt="The Format to Multiple Lines option in Xcode 15" loading="lazy" /></p><p>The result is cleanly formatted with one parameter per line:</p><pre><code class="language-swift">func configureView(
   title: String,
   subtitle: String,
   icon: Image,
   backgroundColor: Color,
   isEnabled: Bool,
   action: @escaping () -&gt; Void
) {</code></pre><p>You can find this action by right-clicking on the function signature and looking under <strong>Refactor</strong>, or by using the <strong>Editor</strong> menu. It works on both function declarations and call sites.</p><h2 id="when-to-use-it">When to Use It</h2><p>This is most useful right after writing a new function or adding parameters to an existing one. Instead of manually formatting as you go, you can write everything on one line and then apply the formatter in a single action. It also helps when reviewing code where someone else left long single-line signatures – select and reformat without manual editing.</p><p>The reverse operation (collapsing multiple lines back to one) is not currently available, but that direction is less commonly needed.</p>]]></content:encoded>
</item>
<item>
<title>RemafoX Sale: Get 50% Off on all Subscription Plans during WWDC Week!</title>
<link>https://fline.dev/blog/remafox-wwdc-sale/</link>
<guid isPermaLink="true">https://fline.dev/blog/remafox-wwdc-sale/</guid>
<pubDate>Sun, 04 Jun 2023 00:00:00 +0000</pubDate>
<description><![CDATA[Shipping 3 new features that help save time for every Swift developer and reducing the price of all subscriptions for WWDC week with a long-term twist you don't want to miss!]]></description>
<content:encoded><![CDATA[<p>As WWDC is approaching, I am thrilled to announce a massive sale on RemafoX subscription plans. Not only that, but I also added three incredible new features in RemafoX 1.6, and I made them available for Free to everyone to celebrate WWDC. Read on to find out all the details and how you can take advantage of these amazing offerings to boost your productivity in Xcode.</p><h2 id="massive-sale-50-off-on-all-subscription-plans">Massive Sale: 50% off on all Subscription Plans!</h2><p>To add to the excitement of WWDC, I decided to offer a limited-time sale on all RemafoX subscription plans. Starting now and until the end of WWDC week, you can enjoy a staggering 50% discount on all subscription prices. This is a rare opportunity to access the full potential of RemafoX at an unbeatable price. But here’s the best part: if you subscribe during this week, you will keep the lower price forever, even after the sale ends. Don’t miss out on this incredible deal to supercharge your development journey.</p><p><a href="https://github.com/FlineDev/RemafoX/issues?q=is%3Aopen+is%3Aissue+label%3A%22Feature+Request%22+sort%3Areactions-%2B1-desc">New features</a> are added to RemafoX every month – at no additional cost! Next up is version 2.0 with an easy way to invite your users to help translate your app by reviewing a few machine-translated texts without leaving your app. And giving you full control over how you want to approach your users – or maybe you just want to ask a few friends to help translate? All possible soon with an active subscription. ✨</p><h2 id="update-16-exciting-new-features-for-wwdc">Update 1.6: Exciting New Features for WWDC!</h2><p>In addition to the very special sale, I have packed RemafoX 1.6 with three brand-new features designed to enhance your coding experience. These features are completely free for everyone and are my way of celebrating WWDC with you.</p><p>Let’s take a closer look:</p><p><img src="/assets/images/blog/remafox-wwdc-sale/demo.gif" alt="" loading="lazy" /></p><h3 id="feature-1-sort-selection-organize-your-code-with-ease">Feature 1: “Sort Selection” – Organize Your Code with Ease</h3><p>I’ve added a sorely missed feature to Xcode: A “Sort Selection” button. Now you can easily sort any selected code alphabetically, saving you valuable time and effort. Experience the convenience of organized code and streamline your development process.</p><h3 id="feature-2-multi-line-code-simplify-complex-collections">Feature 2: “Multi-Line Code” – Simplify Complex Collections</h3><p>Tackling complex collections in your code is now a breeze. With the “Multi-Line Code” button in RemafoX, Xcode detects collections like parameter lists and automatically multi-lines them. Enjoy improved visibility and efficiency while handling intricate code structures.</p><h3 id="feature-3-one-line-code-condense-code-instantly">Feature 3: “One-Line Code” – Condense Code Instantly</h3><p>When you need to condense multi-line code into a single line, the “One-Line Code” button comes to the rescue. Reduce clutter and enhance code readability with a single click. Perfect for sharing snippets or creating concise representations of lengthy code. Or for making simple enums more readable.</p><h2 id="how-to-get-started-with-remafox-for-free">How to Get Started with RemafoX for Free</h2><p><img src="/assets/images/blog/remafox-wwdc-sale/setup.gif" alt="" loading="lazy" /></p><ol><li><p><strong>Download and Start RemafoX:</strong> Download RemafoX <a href="https://apps.apple.com/app/apple-store/id1605635026?pt=549314&ct=fline.dev&mt=8">here from the Mac App Store</a> to enjoy the benefits of the sale and the new features of RemafoX 1.6. Once downloaded, launch the app once for the Xcode extension to propagate itself to the system. You can close the app right away if you don’t want to use any of its translation workflow enhancements.</p></li><li><p><strong>Enable the Xcode Source Editor Extension:</strong> To integrate RemafoX seamlessly with Xcode, go to your device’s Settings app and search for “Extensions”. Enable the entry for RemafoX inside the Xcode Source Editor Extension modal to activate the RemafoX buttons within Xcode.</p></li><li><p><strong>Set Up Shortcuts in Xcode Settings:</strong> Open Xcode and navigate to the Preferences menu. In the Key Bindings section, set up custom shortcuts for the “Sort Selection,” “Multi-Line Code,” and “One-Line Code” buttons you can easily find by typing “remafox” into the search bar. This will allow you to harness the power of RemafoX quickly and effortlessly within Xcode.<br />If you’re not sure which shortcuts to use, here are the ones I use:<br />⌥⌘S  for “Sort Selection”<br />⌥⌘⬇  for “Multi-Line Code”<br />⌥⌘⬆  for “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="conclusion">Conclusion</h2><p>Get RemafoX, either for its awesome Free features or to secure a subscription at a reduced price and profit from all future updates. Boost your productivity now! 🚀</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>Previewing Loading States in SwiftUI Without Changing Production Code</title>
<link>https://fline.dev/snippets/preview-loading-states-swiftui/</link>
<guid isPermaLink="true">https://fline.dev/snippets/preview-loading-states-swiftui/</guid>
<pubDate>Wed, 31 May 2023 00:00:00 +0000</pubDate>
<description><![CDATA[A preview-only helper that simulates network delays so you can see loading states in SwiftUI previews.]]></description>
<content:encoded><![CDATA[<h2 id="the-challenge">The Challenge</h2><p>When building views that depend on asynchronous data, you typically have a loading state that shows a spinner or placeholder. In production, this state appears briefly while data loads from the network or database. But in SwiftUI previews, your mock data is available instantly, so the loading state flashes by too fast to inspect – or never appears at all.</p><h2 id="a-preview-only-delay-helper">A Preview-Only Delay Helper</h2><p>The solution is a small helper that introduces an artificial delay, but only in preview or debug contexts. Here is the pattern:</p><pre><code class="language-swift">struct DelayedStatePreview&lt;Content: View&gt;: View {
   @State private var isLoaded = false
   let delay: Duration
   let content: (Bool) -&gt; Content

   init(
      delay: Duration = .seconds(2),
      @ViewBuilder content: @escaping (Bool) -&gt; Content
   ) {
      self.delay = delay
      self.content = content
   }

   var body: some View {
      content(isLoaded)
         .task {
            try? await Task.sleep(for: delay)
            isLoaded = true
         }
   }
}</code></pre><p>You use it in a preview like this:</p><pre><code class="language-swift">#Preview {
   DelayedStatePreview { isLoaded in
      if isLoaded {
         ArticleListView(articles: mockArticles)
      } else {
         LoadingView()
      }
   }
}</code></pre><p><img src="/assets/images/snippets/preview-loading-states-swiftui/code-example.webp" alt="Code example showing the preview helper in context" loading="lazy" /></p><h2 id="why-this-matters">Why This Matters</h2><p>The key benefit is that your production code stays clean. You are not adding artificial delays or debug flags to your actual views. The helper exists purely at the preview layer, giving you a way to visually verify that your loading states look correct, that transitions animate smoothly, and that layout does not jump when data arrives.</p><p>This is especially useful for views with skeleton loaders or shimmer effects where the visual quality of the loading state is part of the user experience.</p>]]></content:encoded>
</item>
<item>
<title>Taking over WWDC Notes &amp; Envisioning its Future</title>
<link>https://fline.dev/blog/taking-over-wwdc-notes-and-its-future/</link>
<guid isPermaLink="true">https://fline.dev/blog/taking-over-wwdc-notes-and-its-future/</guid>
<pubDate>Thu, 25 May 2023 00:00:00 +0000</pubDate>
<description><![CDATA[Evolving the Open-Source Project: Join the Community effort and shape the Future of how we discover and learn from WWDC Sessions effectively!]]></description>
<content:encoded><![CDATA[<p>Just 10 days ago, <a href="https://twitter.com/zntfdr">Federico Zanetello</a>, the initiator and maintainer of <a href="https://wwdcnotes.com/">WWDC Notes</a> contacted me and asked me if I’d want to help keep the project alive. After some messaging and a video call, it turned out that he couldn’t work on the project anymore at all and I agreed to take it over from him. The bad news is (as you might guess) that the video call was just 2 weeks before Dub Dub. 😱</p><p>But the good news is that Federico automated the site in pretty much all aspects, so it “just works” for the most part. Kudos to him for designing things in a way that makes keeping the project healthy as easy as possible. 💯👏 Also, thankfully he used Swift for basically everything, including <a href="https://github.com/JohnSundell/Publish">publish</a>ing the website, or even for helper tools like sending automated tweets for new summaries on Twitter. For a Swift enthusiast like me, who even runs a <a href="https://swiftevolution.substack.com/">newsletter about Swift Evolution</a>, this was a huge relief. It looks like that for this year, all I need to do is 2 things:</p><p>First: Dump the basic information about each session (like the links to the video or Apple’s session description) into the project after the Platforms State of the Union (also known as the “Developer Keynote”) has taken place. Because that’s when Apple announces the session details for the rest of the week.</p><p>Second: Merge PRs that members of the community created for the sessions <a href="https://wwdcnotes.com/what-s-missing/">listed here</a> which don’t have a summary yet. In fact, you could actually visit the list now and will find ~120 sessions for just the year 2022 nobody contributed notes for yet, compared to ~60 sessions that <em>do</em> have session notes for 2022.</p><p>While it’s my main job for this year to do these 2 things due to the restricted time to prepare anything else, I have more things planned for the future of the project. And because this is a community project, I want to share my plans with you.</p><h2 id="80-session-coverage-by-end-of-wwdc-week">80% Session Coverage by End of WWDC Week</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>Photo by <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>If I have learned one lesson in life, it’s this: It doesn’t matter what you do – as long as you stay active, you will always learn something! Why I’m bringing it up? Well, during my call with Federico I had an idea for this project that was rooted in an experience I’ve had because I’m a big fan of the Harry Potter books. (Don’t worry, it has nothing to do with Rowlings writing directly.) You see, for a fan like me, it was unbearable to know that a new book has been published, but having to wait another 2.5 months for the German translation to be available. I had just turned 14 when <a href="https://harrypotter.fandom.com/wiki/Harry_Potter_and_the_Half-Blood_Prince">The Half-Blood Prince</a> was released, so while I could understand English to some extent, it was not enough to work through an entire book.</p><p>Thankfully, in one of the fan forums (probably <a href="http://forum.harrypotter-xperts.de/index.php?sid=791eea64f14bc76b4cd4b8f2913fb2d7">this one</a>) I heard of <a href="https://web.archive.org/web/20071021030543/http://www.harry-auf-deutsch.de/HaD/index.php">a project</a> where people organized to translate the entire book in just 48 hours. I was skeptical at first. But the plan was simple: Half of the participants would translate roughly one page of the book within the first 24 hours. Then the other half would each receive one participant’s translation and review it, like an editor, to improve the quality. In the end, the organizers would stitch everything together and send the end result out as a PDF file to all participants. It sounded too good to be true. But it totally worked: I participated as an initial translator and had the fully translated book by Sunday evening. It felt like magic back then. And I learned my lesson about the power of crowds and collective effort.</p><p>This transformative experience resonated with me and led me to believe that we can achieve something equally incredible with the WWDC Notes project. Just as the Harry Potter translation project harnessed the power of community collaboration, I am confident that, with your help, we can strive for an ambitious goal: achieving 100% coverage of all WWDC sessions with at least some basic notes within the first week! By leveraging the collective expertise and enthusiasm of the community, we can ensure that valuable insights and summaries are available promptly, empowering developers worldwide.</p><p>Okay, you’ve read the section title, so why do I aim for just 80% then? First off, there are some sessions that are about niche topics and might not be of interest to a broader developer audience, like “<a href="https://developer.apple.com/videos/play/wwdc2022/10149">What’s new in AVQT</a>”. I don’t think there’s a need for those to be available within the first few days, so let’s reduce to 90%.</p><p>Secondly, one of the key learnings for <a href="https://testing.googleblog.com/2020/08/code-coverage-best-practices.html">code coverage best practices</a> is that “we should not be obsessing on how to get from 90% code coverage to 95%. The gains of increasing code coverage beyond a certain point are logarithmic.” I think, something similar is true for WWDC sessions: Once 80% of sessions are covered, it’s likely that the missing 10% of relevant topics are ones nobody wants to watch because they sound too boring or complicated (even if they are not). It can feel like “the <em>pains</em> of increasing <em>session</em> coverage beyond a certain point are logarithmic.” Hence, I aim for 80% coverage by Sunday. Fits also the <a href="https://en.wikipedia.org/wiki/Pareto_principle">Pareto principle</a>.</p><p>But this all can only work with your help. Yes, yours. You never write session notes? Or you do, but only scarce, and therefore you never share them publicly? Then this is your chance to be more active and learn something new along the way! There are no requirements for the length or format of notes, anything helps. You can even choose to only contribute as an editor if you like, it also helps!</p><p>In order for me to organize, please contact @WWDCNotes on <a href="https://twitter.com/WWDCNotes">Twitter</a> or <a href="https://iosdev.space/@WWDCNotes">Mastodon</a> with the message: “I volunteer to contribute &amp; review notes. I’m most interested in the topics” and then mention some topics from this <a href="https://developer.apple.com/videos/topics/">official list</a>. If you want to only contribute or only review, adjust the text accordingly.</p><p>I will then get back to you on day 2 of WWDC week with suggestions on which sessions you could help with, so you only need to take notes for those. Thank you in advance for your help! 🙏 If everyone helps out a bit, we all profit together.</p><hr /><h2 id="bite-sized-takeaways-for-twitter-mastodon">Bite-Sized Takeaways for Twitter &amp; 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>Photo by <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>While it’s great to have summaries for every WWDC session, there still are well over 100 sessions each year, and it can be really hard to decide which session is worth your time, let alone for the video but even for reading or skimming through the notes, which can also be time-consuming. This is exactly the reason why I’ve tried to strip down my key takeaways from each of the 21 sessions I watched &amp; summarized last year during WWDC week and put them all into <a href="https://twitter.com/jeehut/status/1536077070958317568?s=61&t=mFz0Zzcht6SMwjCcQUaZBA">a single Twitter thread</a>, summarizing each session in a single tweet in less than 250 characters each (280 minus “More: <link>”).</p><p>The community seemed to have loved this, the thread was retweeted 48 times (this is by far my most retweets so far) and got mentioned in several newsletters. I think that’s evidence that these kind of byte-sized summaries are something the community needs, and therefore I’m planning to up the game this year:</p><p>Of course, like every year, I will be watching all the sessions I’m personally interested in and I will be writing notes so I can skim through my learnings later on. Obviously, I will contribute my notes to the project. But I will probably only be able to cover ~20 sessions in the first week again, which is just about 10% of them all. With the help of the community, I’d like to grow the usefulness of the #SessionSummary thread for #WWDC23 by increasing the coverage!</p><p>So, I will try to break down every note contributed to WWDC Notes to a list of key takeaways and I’m inviting the community contributors to help me with that. How this help can look exactly, I will figure out before WWDC and announce on the @WWDCNotes <a href="https://twitter.com/wwdcnotes?s=21&t=mFz0Zzcht6SMwjCcQUaZBA">Twitter</a> and the new <a href="https://iosdev.space/@WWDCNotes">Mastodon</a> accounts. Make sure to follow them to not miss the announcement!</p><h2 id="open-sourcing-the-website">Open-Sourcing the Website</h2><p>Currently, the WWDC Notes project consists of 4 GitHub repositories: Content, Website, TwitterBot, and SocialImages. Only Content is currently open-source:</p><p><img src="/assets/images/blog/taking-over-wwdc-notes-and-its-future/the-wwdcnotes.webp" alt="Screenshot of the WWDCNotes organization’s repositories." loading="lazy" /></p><p><em>Screenshot of the WWDCNotes organization’s repositories.</em></p><p>Federico told me, for the future of the project he wanted to open-source everything, similar to how the <a href="https://github.com/SwiftPackageIndex/SwiftPackageIndex-Server">Swift Package Index</a> project operates in the open. While he won’t be able to help with this goal, I 100% agree with it so I will try to restructure the repositories in a way that I can open-source everything, and maybe even keep everything in a single repository, if that makes sense. I plan on tackling this topic later this year once the WWDC buzz has settled down. I’ll keep you posted about any changes on Twitter &amp; Mastodon when the time comes.</p><h2 id="conclusion">Conclusion</h2><p>And those are my initial goals for this project. To summarize:</p><ol><li><p>Get 80% of all WWDC sessions covered with notes by Sunday</p></li><li><p>Twitter/Mastodon thread with bite-sized key takeaways for all sessions</p></li><li><p>Open-Source the full project, including website &amp; social bots (long-term)</p></li></ol><p>What do you think? Do you like the direction this is taking?<br />Or do you have ideas you would like to share with me? Let me know!</p>]]></content:encoded>
</item>
<item>
<title>Speed Up Xcode Launches by Disabling Debug Executable</title>
<link>https://fline.dev/snippets/speed-up-xcode-disable-debug-executable/</link>
<guid isPermaLink="true">https://fline.dev/snippets/speed-up-xcode-disable-debug-executable/</guid>
<pubDate>Wed, 10 May 2023 00:00:00 +0000</pubDate>
<description><![CDATA[A hidden Xcode scheme setting that can significantly reduce app launch times during development.]]></description>
<content:encoded><![CDATA[<h2 id="the-blank-screen-problem">The Blank Screen Problem</h2><p>If you have ever noticed a delay – sometimes several seconds – between pressing Run in Xcode and your app actually appearing, the culprit is often the LLDB debugger attaching to your process. During this time, the Simulator shows a blank screen while the debugger initializes.</p><p><img src="/assets/images/snippets/speed-up-xcode-disable-debug-executable/comparison.webp" alt="Comparison of launch times with and without debug executable enabled" loading="lazy" /></p><h2 id="where-to-find-the-setting">Where to Find the Setting</h2><p>The setting lives in your scheme configuration:</p><ol><li><p>Go to <strong>Product &gt; Scheme &gt; Edit Scheme</strong> (or press Cmd+Shift+&lt;)</p></li><li><p>Select the <strong>Run</strong> action on the left</p></li><li><p>Switch to the <strong>Info</strong> tab</p></li><li><p>Uncheck <strong>Debug executable</strong></p></li></ol><p><img src="/assets/images/snippets/speed-up-xcode-disable-debug-executable/setting-before.webp" alt="The setting before the change" loading="lazy" /></p><p><img src="/assets/images/snippets/speed-up-xcode-disable-debug-executable/setting-after.webp" alt="The setting after unchecking Debug executable" loading="lazy" /></p><h2 id="what-this-does">What This Does</h2><p>When “Debug executable” is enabled (the default), Xcode attaches the LLDB debugger to your app process at launch. This is what enables breakpoints, the debug memory graph, view hierarchy debugger, and <code>po</code> commands in the console.</p><p>Disabling it skips the debugger attachment entirely. Your app launches noticeably faster – in my experience, the difference can be 2 to 5 seconds on larger projects. Console output via <code>print()</code> and <code>os_log</code> still works normally, so you can still use logging for debugging.</p><h2 id="the-trade-off">The Trade-Off</h2><p>Without the debugger attached, you lose:</p><ul><li><p>Breakpoints (they will not trigger)</p></li><li><p><code>po</code> and <code>expression</code> commands in the console</p></li><li><p>Memory graph and view hierarchy debugging tools</p></li></ul><p>This makes the setting ideal for UI iteration work, where you are making visual tweaks and re-running frequently. When you need to investigate a specific bug with breakpoints, just re-enable the checkbox temporarily. I keep it off most of the time and only toggle it on when I need to step through code.</p>]]></content:encoded>
</item>
<item>
<title>Window Management with SwiftUI 4</title>
<link>https://fline.dev/blog/window-management-on-macos-with-swiftui-4/</link>
<guid isPermaLink="true">https://fline.dev/blog/window-management-on-macos-with-swiftui-4/</guid>
<pubDate>Mon, 08 May 2023 00:00:00 +0000</pubDate>
<description><![CDATA[Learnings from modernizing the window handling of my Mac app after upgrading to SwiftUI 4. Explaining `\.openWindow`, `.windowResizability` & more.]]></description>
<content:encoded><![CDATA[<p>SwiftUI is getting significantly better every year. Last year (in 2022), we not only received improved <a href="https://developer.apple.com/documentation/swiftui/navigation">navigation APIs</a>. Apple also greatly improved the support for macOS – I would argue that the SwiftUI APIs we received for Mac app development in SwiftUI 4 are on a <code>1.0</code> level and finally allow for doing all sorts of things within SwiftUI without having to resort to <code>AppKit</code> for some of the most common tasks. I’ve experienced SwiftUI on the Mac while working on <a href="https://remafox.app/">RemafoX</a> using SwiftUI 3 and in some parts, it really was a nightmare.</p><p>Coming from iOS development, I was hoping not to have to learn all details of <code>AppKit</code>. But I had to write all sorts of hacky code to do the simplest things, like closing a window. Or disabling the full-screen button on a window. But there’s good news: By increasing my app target to macOS 13.0, I could finally do window management through SwiftUI and eliminate all the hacks I’ve had in my app.</p><p>Finally, my app feels really 100% SwiftUI-driven. And here are all the new APIs I could make use of grouped and titled by the task I wanted to achieve. As window management is probably the biggest difference between iOS and macOS development in SwiftUI times, this article could also help anyone switching from iOS to macOS to understand how window management is done on the Mac.</p><h2 id="opening-a-window">Opening a Window</h2><p>If you are using a <code>WindowGroup</code> (which was the only type of window available on SwiftUI 3), with SwiftUI 4 you have two options here: The first, which was already supported before, is to use the <a href="https://developer.apple.com/documentation/swiftui/windowgroup/handlesexternalevents(matching:)"><code>handlesExternalEvents</code></a> method like so:</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>Then, when you want to open this window, you would need to open a URL like you can open any external URL but with a <a href="https://developer.apple.com/documentation/xcode/defining-a-custom-url-scheme-for-your-app#Register-your-URL-scheme">custom URL scheme</a>. For example:</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>This method doesn’t work on the new <code>Window</code> type though, although it’s fully available there, too. The <a href="https://developer.apple.com/documentation/swiftui/window/handlesexternalevents(matching:)#discussion">documentation</a> is clear about it:</p><blockquote><p>This modifier is only supported for WindowGroup Scene types.</p></blockquote><p>But the second method works for both <code>WindowGroup</code> &amp; <code>Window</code>: The new <a href="https://developer.apple.com/documentation/swiftui/environmentvalues/openwindow/"><code>\.openWindow</code></a> environment value! First, we define an <code>id</code> in the 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>Then, we simply pass that <code>id</code> to <code>openWindow</code> to trigger presentation manually:</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>This is much nicer! Note that there’s also <a href="https://developer.apple.com/documentation/swiftui/environmentvalues/opendocument/"><code>\.openDocument</code></a> for <code>DocumentGroup</code>.</p><h2 id="prevent-duplicate-windows">Prevent Duplicate Windows</h2><p>Using <code>id</code> for a window does not prevent multiples of it from appearing:</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>At least not for <code>WindowGroup</code>, but you can simply use <code>Window</code> to ensure multiples of a window with the same <code>id</code> are never created! But that’s not always an option.</p><p>A <code>Window</code> is much more restricted than a <code>WindowGroup</code> in various ways. For example, you can’t call <code>.commands</code> on it like I’ve done above to set some buttons in the app’s main menu. Also, <code>Window</code> is a macOS-only API, therefore you can’t reuse the code on iPadOS. The reason <em>I</em> couldn’t use it for RemafoX is the restriction I already mentioned before: <code>Window</code> does not support <code>handlesExternalEvents</code>. But I need that API because RemafoX is deeply integrated with Xcode through both an extension and a CLI tool, and those can’t make use of <code>\.openWindow</code> because they simply are not part of the same target. But they can still open “external” URLs!</p><p>Whatever reason <em>you</em> might have to prevent duplicates of <code>WindowGroup</code>, do this:</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>It’s using an <a href="https://developer.apple.com/documentation/swiftui/windowgroup/init(_:id:for:content:defaultvalue:)-95crv">overloaded version</a> of the <code>WindowGroup</code> initializer which takes additional <code>for type</code> and <code>defaultValue</code> arguments. It can be used to open a specific window related to any data type you want (like a <code>Profile</code> type), but I’m simply reusing the <code>id</code> of type <code>String</code> here to make the window unique.</p><p>In my case, I had to do this in several places, so I created this extension:</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>With that, the above call with two <code>Window.paywall.id</code> calls became just this:</p><pre><code class="language-Swift">WindowGroup(&quot;Plan Chooser&quot;, uniqueWindow: Window.paywall) {
   // ...
}</code></pre><p>For opening a window, we additionally pass the <code>value</code> param to <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>Again, the multiple calls to <code>Window.paywall.id</code> bugged me, so I created a helper:</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>Now I can simply call, even getting rid of the <code>.id</code> suffix:</p><pre><code class="language-Swift">self.openWindow(Window.paywall)</code></pre><h2 id="closing-a-window">Closing a Window</h2><p>Now that we can (uniquely) open a window, let’s also close it. And here, the situation before was much worse than with <code>WindowGroup</code>, where we had some workaround within SwiftUI. There simply was no way of closing a window in SwiftUI directly. I had to implement <a href="https://onmyway133.com/posts/how-to-manage-windowgroup-in-swiftui-for-macos/#access-underlying-nswindow">this hack</a> using <code>AppKit</code>:</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>Then, when I wanted to close the window, I would call <code>self.window.close()</code>.</p><p>In 2021, the <a href="https://developer.apple.com/documentation/swiftui/environmentvalues/dismiss/"><code>\.dismiss</code></a> environment value was added that allowed to dismiss presented views like <code>sheet</code>, <code>popover</code> or <code>fullScreenCover</code> right from within them. I’m not sure if this behavior was already available back then or added in 2022, but today the docs additionally state that the <code>dismiss</code> action can be used to:</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>This certainly only works if there’s currently no modal view presented within the window in question. But then it works like a charm, we can just write this:</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="disabling-full-screen-button">Disabling Full-Screen Button</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>Previously, I used the same hack mentioned above which gave me access to an <code>NSWindow</code> to do more configuration like disabling the full-screen button for views with a fixed size, like my welcome window or my about window. But now we have the new modifier <a href="https://developer.apple.com/documentation/swiftui/windowresizability/"><code>windowResizability</code></a> which allows us to disable the full-screen button indirectly. By default, it’s set to <code>contentMinSize</code> for all windows except <a href="https://developer.apple.com/documentation/swiftui/settings"><code>Settings</code></a> (which is a <code>Scene</code> type like <code>WindowGroup</code>). But we can do this now:</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>By setting the <code>windowResizability</code> to <code>.contentSize</code>, we tell SwiftUI to more strictly follow the sizes we provide in the <code>frame</code> modifier. As a logical consequence, if the maximum size specified by the view is smaller than the users current screen size, then SwiftUI will automatically disable the full-screen button for us! 🪄 It’s not a very obvious or direct API, but it makes sense. Effectively, if we provide any values below 1366 by 768, which is the native size of an 11-inch MacBook Air (from 2015), we should have disabled the button for most users.</p><h2 id="tca-extras">TCA Extras</h2><p>If you are using <a href="https://github.com/pointfreeco/swift-composable-architecture">The Composable Architecture</a> (TCA) for your apps like me, you might ask yourself how you can best forward these new environment values to your reducers, as logic such as opening/closing windows should happen in those. The great community around TCA helped me solve this elegantly, in particular, <a href="https://github.com/tgrapperon">Thomas Grapperon</a> provided a type that I renamed to <code>OnChange</code> which you can simply copy &amp; paste into your projects <a href="https://github.com/pointfreeco/swift-composable-architecture/discussions/1683#discussioncomment-5539006">from here</a>. Then, in your view, attach the <code>.onChange</code> modifier passing it any value to forward to the reducer like this:</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>Note that the <code>\.$openWindow</code> refers to a field in the <code>State</code>, so we need to define it:</p><pre><code class="language-Swift">struct SomeState {
   @OnChange
   var openWindow: Window?
}</code></pre><p>Now, in our reducers, we can simply set the state value to a <code>Window</code> enum case and the view will automatically forward the change to the <code>@Environment</code> value. This also works with any other SwiftUI attribute, I also used it for <code>@FocusState</code>!</p><p>By the way, while TCA ships with a <code>\.dismiss</code> dependency, calling <code>await self.dismiss()</code> in the reducer will (<a href="https://github.com/pointfreeco/swift-composable-architecture/discussions/1944#discussioncomment-5549895">currently</a>) not behave like the SwiftUI <code>\.dismiss</code> environment value and close the window. Instead, it will do nothing and even produce a warning. As a workaround, I implemented a dependency that makes use of <code>AppKit</code> APIs by traversing the open windows, finding the match, and closing that. You can copy &amp; paste the Gist <a href="https://gist.github.com/Jeehut/7601bdbce1af1f848b1ea98f697a0f95">from here</a>, usage looks like this:</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>And that’s all I had to share about window management in SwiftUI 4 today!</p>]]></content:encoded>
</item>
<item>
<title>Xcode Code Snippets for Developer Warnings</title>
<link>https://fline.dev/snippets/xcode-snippets-developer-warnings/</link>
<guid isPermaLink="true">https://fline.dev/snippets/xcode-snippets-developer-warnings/</guid>
<pubDate>Sun, 07 May 2023 00:00:00 +0000</pubDate>
<description><![CDATA[Using Xcode code snippets with #warning to leave actionable reminders in your codebase.]]></description>
<content:encoded><![CDATA[<h2 id="why-warning-matters-during-development">Why #warning Matters During Development</h2><p>Swift’s <code>#warning</code> directive generates a compiler warning with a custom message. Unlike comments, these show up in the Issue Navigator and in the build output, making them impossible to miss. I use them as hard reminders for things that must be addressed before shipping.</p><h2 id="two-snippets-i-use-daily">Two Snippets I Use Daily</h2><p>I have two Xcode code snippets configured for this purpose.</p><p><strong>“nyi” – Not Yet Implemented:</strong></p><pre><code class="language-swift">#warning(&quot;Not yet implemented!&quot;)</code></pre><p>I type <code>nyi</code> and hit Enter whenever I stub out a function or skip a code path during prototyping. It compiles fine but the warning ensures I come back to finish the work.</p><p><strong>“dw” – Developer Warning:</strong></p><pre><code class="language-swift">#warning(&quot;&lt;#message#&gt;&quot;)</code></pre><p>This one uses a placeholder token so that after typing <code>dw</code> and pressing Enter, the cursor lands inside the message and I can type a custom note. I use this for things like <code>#warning(&quot;Handle error case for offline mode&quot;)</code> or <code>#warning(&quot;Remove before release&quot;)</code>.</p><h2 id="how-to-create-xcode-snippets">How to Create Xcode Snippets</h2><p>Setting these up takes about 30 seconds each:</p><ol><li><p>Type the code you want as a snippet in any Swift file</p></li><li><p>Select the code</p></li><li><p>Right-click and choose <strong>Create Code Snippet</strong></p></li><li><p>Give it a title (e.g., “Developer Warning”)</p></li><li><p>Set the <strong>Completion</strong> shortcut (e.g., <code>dw</code>)</p></li><li><p>Set <strong>Availability</strong> to “All” or “Swift” scopes</p></li><li><p>Click Done</p></li></ol><p>From that point on, typing the shortcut in any Swift file offers the snippet via autocomplete.</p><p>The key advantage over plain comments is visibility. A <code>// TODO:</code> comment is easy to ignore and hard to search for consistently. A <code>#warning</code> forces the compiler to surface it every single build, keeping your unfinished work front and center until you resolve it.</p>]]></content:encoded>
</item>
<item>
<title>Quick Access to Swift Evolution Proposal Summaries on GitHub</title>
<link>https://fline.dev/snippets/swift-evolution-proposal-summaries-github/</link>
<guid isPermaLink="true">https://fline.dev/snippets/swift-evolution-proposal-summaries-github/</guid>
<pubDate>Tue, 02 May 2023 00:00:00 +0000</pubDate>
<description><![CDATA[A simple URL trick to read summarized versions of Swift Evolution proposals on GitHub.]]></description>
<content:encoded><![CDATA[<h2 id="the-problem-with-proposal-documents">The Problem with Proposal Documents</h2><p>Swift Evolution proposals are thorough by design. They cover motivation, detailed design, alternatives considered, ABI implications, and more. That thoroughness is essential for the review process, but when you just want to understand what a proposal does and why, reading through thousands of words can be a lot.</p><h2 id="the-url-trick">The URL Trick</h2><p>There is a simple way to get a summarized version of any Swift Evolution proposal on GitHub. When you are viewing a proposal at a URL like:</p><pre><code>https://github.com/apple/swift-evolution/blob/main/proposals/0390-noncopyable-structs-and-enums.md</code></pre><p>Replace <code>apple</code> with <code>FlineDev</code>:</p><pre><code>https://github.com/FlineDev/swift-evolution/blob/main/proposals/0390-noncopyable-structs-and-enums.md</code></pre><p>That takes you to a fork of the swift-evolution repository where proposals have been augmented with AI-generated summaries at the top. Each summary distills the key points – what the proposal introduces, why it matters, and the basic syntax – into a few paragraphs.</p><h2 id="when-this-helps">When This Helps</h2><p>This is particularly useful when you see a proposal mentioned in release notes or on social media and want to quickly understand the gist. Instead of spending 15 minutes reading the full proposal, you get the essential information in a couple of minutes.</p><p>The summaries cover most recently accepted proposals. For older proposals that predate the fork, you will still see the original text. But for anything from the last few years of Swift Evolution, the summarized version is a real time-saver.</p><p>I built this fork because I found myself repeatedly skimming proposals for just the core idea, and figured other developers might benefit from the same shortcut.</p>]]></content:encoded>
</item>
<item>
<title>My Top 5 Wishes for WWDC 2023</title>
<link>https://fline.dev/blog/my-top-5-wishes-for-wwdc-2023/</link>
<guid isPermaLink="true">https://fline.dev/blog/my-top-5-wishes-for-wwdc-2023/</guid>
<pubDate>Thu, 27 Apr 2023 00:00:00 +0000</pubDate>
<description><![CDATA[WWDC is only weeks away, so it's time for me to update my wishlist. One wish came true last year, how many will it be in 2023?]]></description>
<content:encoded><![CDATA[<p>I wrote an article with the exact same purpose last year already for WWDC 2022, listing <a href="https://www.fline.dev/my-top-3-wishes-for-wwdc-2022/">my top 3 wishes</a>. Thankfully, one of them (wish #3) was actually fulfilled, the <a href="https://developer.apple.com/documentation/xcode-release-notes/xcode-14-release-notes">Xcode 14 Release Notes</a> explain the related new feature like this:</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>My other two wishes still stand true and remain at the top of my wishlist:</p><p>#1: A <strong>new Swift-only database framework</strong> to replace CoreData in the future. I outlined how I imagine such a framework could work <a href="https://www.fline.dev/my-top-3-wishes-for-wwdc-2022/#1-new-swift-only-database-framework">here</a>, read that for details.</p><p>#2: App **modularization support **within Xcode. Currently, I have to maintain a long <code>Package.swift</code> file manually to modularize my app, read <a href="https://www.fline.dev/my-top-3-wishes-for-wwdc-2022/#2-app-modularization-support-in-xcode">this</a> for more.</p><p>With these out of the way, here are 3 new wishes I have for this year’s WWDC.</p><h2 id="3-circular-charts-in-the-swift-charts-library">#3: Circular charts in the Swift Charts library</h2><p>At the end of my last year’s article, I stated:</p><blockquote><p>In the past, I was always surprised by at least one or two frameworks entirely, like <a href="https://developer.apple.com/videos/play/wwdc2019/216/">SwiftUI</a> in 2019, <a href="https://developer.apple.com/videos/play/wwdc2020/10028/">WidgetKit</a> in 2020, and <a href="https://developer.apple.com/videos/play/wwdc2021/10166/">DocC</a> in 2021. What will it be this year? I can’t wait to find out!</p></blockquote><p>Well, it was <a href="https://developer.apple.com/videos/play/wwdc2022/10136/">Swift Charts</a>, for sure! It’s the new shiny library that maybe not every app needs immediately, but I’m sure will inspire many developers to visualize some of their app’s data to give users a better overview of their contributed data. It’s SwiftUI-only, so not all teams have had the opportunity to make use of it yet.</p><p>The first version of the <a href="https://developer.apple.com/documentation/Charts">Charts library</a> shipped with a good amount of chart types that all have in common that they need only one or two straight axes. For example, you can already draw (and customize!) line charts, bar charts, and even heat maps. <a href="https://twitter.com/jordibruin">Jordi Bruin</a> has <a href="https://github.com/jordibruin/Swift-Charts-Examples">put together a GitHub repo</a> with screenshots &amp; source code that can give you a good overview of what’s already possible today.</p><p>But when I tried using the Swift Charts library myself for my open-source app <a href="https://github.com/FlineDevPublic/OpenFocusTimer">Open Focus Timer</a> that I develop during <a href="https://www.youtube.com/playlist?list=PLvkAveYAfY4TVdM3Lc52SJTkuGB85I5uw">my live streams</a> on <a href="https://www.twitch.tv/Jeehut">Twitch</a>, I wanted to show users a pie chart of how their work time is split across different projects. But pie charts are currently not supported by the Charts library, nor are spider charts, doughnut charts or sunburst charts. But I think they’re so common, that they really should all be added to a second version of the Charts library this year.</p><p>And next year, maybe tree-like charts (or diagrams) could be added as well, which could help create professional tools, e.g. for drawing a <a href="https://en.wikipedia.org/wiki/Unified_Modeling_Language">UML</a> diagram of an apps model layer, for visualizing data structures like <a href="https://en.wikipedia.org/wiki/B-tree">B-trees</a>, or for building interactive <a href="https://en.wikipedia.org/wiki/Finite-state_machine">state machines</a> that could help understand features on a more abstract level. I have enough ideas that could make great use of such charts or diagrams!</p><h2 id="4-streamer-mode-in-xcode-to-redact-code">#4: Streamer Mode in Xcode to redact code</h2><p><img src="/assets/images/blog/my-top-5-wishes-for-wwdc-2023/kristina-flour-unsplash.webp" alt="Kristina flour unsplash" loading="lazy" /></p><p>Sharing content is a great thing. It not only helps others who consume your shared content to learn something new or to get inspired by it. Their feedback can also help the content creator to discover new aspects they haven’t thought about yet, such as subtle bugs, UX issues, or lack of accessibility support. But if the content to be shared is related to a coding project, security &amp; secrecy concerns come into play.</p><p>For example, when sharing an open-source framework with the community that integrates with 3rd party services, we have to ensure no testing credentials are ever committed to the repository or else they might leak and get abused. In fact, that example is exactly the situation I ran into with SwiftPM for my open-source tool <a href="https://github.com/FlineDev/BartyCrouch">BartyCrouch</a>, I explained how I solved it in <a href="https://www.fline.dev/hiding-secrets-from-git-in-swiftpm/">this article</a>.</p><p>And secrets aren’t the only part we want to hide from others: No company would want the core value of their products that took a lot of effort to figure out to leak to a potential competitor. Copycats are a serious problem that can destroy businesses. The solution for Git repositories is simple enough: Valuable code that shouldn’t leak needs to be kept closed-source and never shared with outsiders.</p><p>But hiding secrets from a Git repository or keeping parts of the codebase private is only part of the story. What about video calls with friends or strangers that want to help us? What about Indie developers (like me) who want to live stream their work as much as possible? There are so many situations where I would love to simply share my screen and show a particular problem or solution right within the context of my real-world application, but I don’t. The risk of accidentally leaking sensitive code is just too high for me. This leads to many missed potentials:</p><ul><li><p>As an Indie developer, I would love to even live-stream the development of updates to my closed-source apps, but there are parts I don’t want to leak. So, all I do is to live-stream my open-source work. That’s a big bummer.</p></li><li><p>As a framework user, I would love to report bugs or feature requests by quickly recording my screen and sharing a video of the in-context usage. Currently, I often times skip reporting anything because I find it too cumbersome to prepare a demo. Or I copy parts of code from the context and then have to answer several questions to explain why I’m doing it exactly the way I do.</p></li><li><p>As a participant in a weekly developer exchange call, I prefer to just talk about code on an abstract level if I run into problems rather than sharing my screen and showing it in detail. And even if I <em>do</em> share my screen, I would never allow my counterpart to <em>control</em> my screen (e.g. to quickly show me something). I know, part of the problem is that I’m having trust issues, but I’m not alone.</p></li></ul><p>So, what I wish for is a feature within Xcode to mark specific parts of my codebase as “confidential” and when I share my screen, all confidential parts would be hidden from the shared screen. Such a feature is commonly referred to as “Streamer mode” in apps like <a href="https://support.discord.com/hc/en-us/articles/218485407-Streamer-Mode-101">Discord</a>. And it could be implemented in a variety of ways, here’s how I imagine it to work:</p><ul><li><p>The confidential content could stay visible for the streamer but be highlighted by Xcode so the streamer is aware that it won’t appear in any shared content.</p></li><li><p>The marking of content as “confidential” could happen on different levels, such as on a file level (all contents of a file are redacted), on a type level, on a function level, or on a variable level. In the latter, the entire lines would be redacted.</p></li><li><p>The names of redacted content could be still shown (file name / type name / function declaration / variable name) and only their bodies/values redacted.</p></li><li><p>The names of all contents marked as “confidential” could be locked from editing to ensure that renaming them doesn’t temporarily make their contents leak.</p></li><li><p>The confidential markings could be stored in a file that can be committed to Git so other developers in a team could profit from them in their outside calls, too.</p></li><li><p>Streamer mode could be automatically enabled when any app is currently making use of the <a href="https://developer.apple.com/documentation/screencapturekit">screen-capturing APIs</a> so the developer wouldn’t have to remember to turn on “Streamer mode” manually. This should make it work for both video calls (in FaceTime/Zoom etc.) and screen recordings (in OBS/QuickTime etc.).</p></li></ul><p>I’m not very optimistic about such a feature making its way into Xcode as I feel like this is not a problem commonly stated by many developers. It feels like a niche issue. But in my opinion, it’s one of those features you don’t know you were missing until you got used to them and then you’d never want to miss them again.</p><h2 id="5-arvr-os-development-with-an-iphone">#5: AR/VR OS development with an iPhone</h2><p>Tim Cook has been publicly talking about the potential of AR since <a href="https://www.theverge.com/21077484/apple-tim-cook-ar-augmented-reality">as early as 2016</a>. Assuming he wouldn’t talk about something Apple hasn’t researched at least for a whole year already, we can safely say that Apple is working on AR products for at least 8 years. The first iPhone took <a href="https://www.history.com/news/iphone-original-size-invention-steve-jobs">two and a half years</a> to develop. The first Apple Watch took <a href="https://www.businessinsider.com/tim-cook-full-interview-with-charlie-rose-with-transcript-2014-9">three years</a> of development. So while one could ask, <em>why</em> we still haven’t gotten Apple’s first AR device then, I’m going to simply assume we will get one this year. And I will also assume that this new device will ship with its own operating system, let’s call it “AR/VR OS”.</p><p>I can already foresee a problem for developers when such a device gets announced: Not every developer will be able to purchase a new device right away. Some due to financial restrictions, some because first gen products tend to not be available in all countries all at once. In the past, this was not a big issue. There are simulators for the iPhone, the iPad, the Apple Watch, and even the Apple TV that ship with Xcode. While there are some restrictions (like no camera support), most apps can be fully developed and for the most part, even tested on these without any issues. That’s because all these devices have one thing in common with our development device, the Mac: They all render a virtual interface on a flat screen.</p><p>But an AR device is different: By its very definition, it “augments reality”, which means that in order to develop and test any useful app properly, we need to have access to a camera feed so we have something to “augment”. While Apple could provide some sample virtual environments for testing stuff in an AR/VR simulator, as they did with the location simulation, this would be very limiting for many use cases of apps Apple didn’t cover and can also be annoying to work with.</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>What I wish for instead is for Apple to provide developers with a way of testing AR/VR OS development using the camera feed of a connected iPhone or iPad. This could be restricted to devices with a <a href="https://en.wikipedia.org/wiki/Lidar">LiDAR</a> scanner if that’s a technical requirement. But it should also work wirelessly as we will want to walk around with it to test our features in different areas of the world. The basic technology for that was already shipped in the form of <a href="https://support.apple.com/en-us/HT213244">Continuity Camera</a> which allows using an iPhone as a webcam. Maybe that feature was just a by-product of said testing functionalities originally built for the internal team that was working on the AR product in the first place. Who knows? This could be a sign that it will come.</p><p>But more importantly, why I’m optimistic that we will get <em>some</em> way of testing for AR/VR OS without the real device is that Apple has an interest in as many apps as possible to support the device. And that means it has to be as easy as possible to develop applications for it. After all, the availability of (unique) apps is an important selling point of any new software platform.</p><h2 id="what-about-the-ai-hype">What about the AI hype?</h2><p>While I do wish for better auto-completion support in Xcode or even a virtual coding assistant of some sort that I can tell what I want and it writes the code to meet my criteria so I don’t have to type out all the code myself, I don’t think we’re there yet. I did play around with ChatGPT, but it got things wrong 80% of the time when I asked for code. It not only used deprecated APIs all the time, but it also produced code that didn’t build. It often didn’t even understand what I wanted.</p><p>While a dedicated model for coding from Apple might lead to better results, I don’t think it will come close to what I would consider “reliable” yet. And I’m sure Apple knows this. They might explore such a feature in the future, but I hope they won’t jump on the current hype train and try to build something half-baked into Xcode. I prefer tools that I can rely on, and that are predictable. I think that Apple does that, too. But the technology isn’t there yet. I expect something big in this direction next year at the earliest, but only something tiny if anything at all this year.</p><h2 id="conclusion">Conclusion</h2><p>So, these are <em>my</em> top 5 wishes for WWDC 2023. Do you agree with me? And what are yours? Let me know by commenting on Twitter <a href="https://twitter.com/jeehut/status/1651733420240666625?s=61&t=3mfZyJ0MLsW7Lpqz13mreg">here</a> or on Mastodon <a href="https://iosdev.space/@Jeehut/110273426937982763">there</a>.</p>]]></content:encoded>
</item>
<item>
<title>Preparing My App for Swift 6</title>
<link>https://fline.dev/blog/preparing-for-swift-6/</link>
<guid isPermaLink="true">https://fline.dev/blog/preparing-for-swift-6/</guid>
<pubDate>Tue, 18 Apr 2023 00:00:00 +0000</pubDate>
<description><![CDATA[How to enable Swift 6 mode for your Xcode projects and for your SwiftPM modules today. And what the migration experience is like.]]></description>
<content:encoded><![CDATA[<h2 id="what-is-swift-6-mode">What is “Swift 6 mode”?</h2><p>Swift 6 won’t be released in 2023 anymore, that has been <a href="https://forums.swift.org/t/design-priorities-for-the-swift-6-language-mode/62408/15">made clear</a> by Doug Gregor from the Swift Language Workgroup. But did you know that Apple has already shipped parts of Swift 6 in 5.8? Yes, it’s true. Some parts of Swift that shipped with Xcode 14.3 a few weeks ago are <strong>turned off by default</strong>. They will be turned on once Swift 6 comes out, which might take another year, or even longer.</p><p>These features introduce some breaking changes to Swift, for example by renaming known APIs, adjusting their behavior, or adding new safety checks to the compiler. But they will all get turned on at some point to help improve our code bases. And we will have to update our projects to play nicely with those changes.</p><p>I figured it’s a good idea to turn on all features in my projects to see if there’s any breaking change for my codebase that I should know of. And of course, I could also make use of some new features, such as <code>BareSlashRegexLiterals</code>, which makes the <code>/.../</code> regex literal syntax available for concise Regex initialization.</p><p>Thankfully, with Swift 5.8 there’s now a unified way of enabling these options: We just need to pass <code>-enable-upcoming-feature</code> to Swift by providing it in the <code>OTHER_SWIFT_FLAGS</code> in our projects build settings in Xcode. But we need to know also what features are available, and I couldn’t find any place with a good overview (<a href="https://github.com/apple/swift-org-website/pull/284">yet</a>). The proposal that introduced this unified option does <a href="https://github.com/apple/swift-evolution/blob/main/proposals/0362-piecemeal-future-features.md#proposals-define-their-own-feature-identifier">contain such a list</a>, but it doesn’t get updated with newer options added later on, such as the one from <a href="https://github.com/apple/swift-evolution/blob/main/proposals/0384-importing-forward-declared-objc-interfaces-and-protocols.md">SE-0384</a>. So, where can we currently reliably get a list of all supported options?</p><blockquote><p>✨ UPDATE: You can now <a href="https://www.swift.org/swift-evolution/#?upcoming=true">filter by upcoming flags</a> directly on Swift.org.</p></blockquote><p>Swift is open source, so the most reliable place seems the Swift GitHub repository! It includes a <code>Features.def</code> file which <a href="https://github.com/apple/swift/blob/release/5.8/include/swift/Basic/Features.def#L96-L99">contains entries</a> (link to <code>release/5.8</code> branch) named <code>UPCOMING_FEATURE</code> including both the related Swift Evolution proposal number and the Swift version they will be turned on:</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>Here are the options with a short description of what they do:</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><br />Changes <code>#file</code> to mean <code>#fileID</code> rather than <code>#filePath</code>.</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><br />Removes the backward-scan matching rule.</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><br />Requires <code>any</code> for existential types.</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><br />Makes the <code>/.../</code> regex literal syntax available.</p></li></ul><p>For some reason, two options seem not to be listed there (I’m <a href="https://forums.swift.org/t/design-priorities-for-the-swift-6-language-mode/62408/80">investigating</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><br />Performing complete concurrency checking.</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><br />Performs implicit opening in additional cases.</p></li></ul><p>More options will ship with later versions, e.g. <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>To additionally check my code for proper concurrency support, I chose to also pass <code>-warn-concurrency</code> (this should actually be the same as <code>StrictConcurrency</code> if that actually works) and <code>-enable-actor-data-race-checks</code>.</p><h2 id="migrating-my-project">Migrating my project</h2><p>If you also want to enable all <code>5.8</code> options in your project, like me, copy (<strong>⌘</strong>C) the following text block, then head to your Xcode projects “Build Settings” tab, search for “Other Swift Flags”, select that option in the editor and paste (<strong>⌘</strong>V) it in:</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>If you are using a SwiftPM-modularized app like me, or if you’re working on a Swift package, you will need to additionally pass an array of <code>.enableUpcomingFeature</code>’s to each target via <code>swiftSettings</code>:</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>Don’t forget to upgrade your tools version at the top of the file to <code>5.8</code>:</p><pre><code class="language-Swift">// swift-tools-version:5.8</code></pre><p><em><code>.enableUpcomingFeature</code> is only available from Swift <code>5.8</code></em></p><p>That’s because Swift doesn’t pass along the options you specified for your project target automatically to any modules you import into your project. And that’s good news, because that way Swift packages you might be including into your project don’t need any adjustments and you can still use these features for your own app code. And vice versa: Package authors can turn these features on for their projects without affecting the code in projects they get consumed in.</p><p>After turning these on and building, I ran into four kinds of issues:</p><ol><li><p>I had to add the <code>any</code> keyword in several places – Xcode helped with a 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>For some reason I received a lot of errors for views with a <code>.sheet</code> modifier. The error pairs stated: “Generic parameter ‘Content’ could not be inferred” and “Missing argument for parameter ‘content’ in call”. These messages weren’t very helpful though, so I first tried to replace the <code>.sheet</code> content with just a <code>Text</code> view, but it didn’t help. So I’ve tried turning off the options one by one and the errors disappeared when I turned off <code>ForwardTrailingClosures</code>, so I kept it turned off. I’m hoping that a better error message will be produced by future versions of Swift to fix them later. There’s no hurry, after all. I can retry on Swift 5.9. I didn’t have time to investigate now.</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>I had to mark some of my functions returning a <a href="https://github.com/pointfreeco/swift-composable-architecture">TCA</a> <code>WithViewStore</code> with the <code>@MainActor</code> attribute, but again, Xcode helped with 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>In many places, warnings were shown stating: “Non-sendable type ‘…’ passed in call to main actor-isolated function cannot cross actor boundary”. So I made these types conform to the <code>Sendable</code> protocol (learn about <code>Sendable</code> <a href="https://www.hackingwithswift.com/swift/5.5/sendable">here</a>).</p></li></ol><p>All else seemed to build fine for my app <a href="https://remafox.app/">RemafoX</a> with ~35k lines of Swift code. The whole process took less than 3 hours of my time.</p><p>My project is now ready for the future of Swift 🎉 and I can use the new Regex literal feature (<code>let regex = /.*@.*/</code>). Also, I can’t introduce <em>new</em> code that I have to migrate to Swift 6 later, cause I will receive errors right away. 💯</p><p>How about your project? Which upcoming features do you want to migrate to?</p>]]></content:encoded>
</item>
<item>
<title>Binding: Equatable vs EquatableBinding</title>
<link>https://fline.dev/blog/binding-equatable-vs-equatablebinding/</link>
<guid isPermaLink="true">https://fline.dev/blog/binding-equatable-vs-equatablebinding/</guid>
<pubDate>Thu, 13 Apr 2023 00:00:00 +0000</pubDate>
<description><![CDATA[How I fixed a subtle bug in SwiftUI Pickers in my app by using a Property Wrapper instead of conforming Binding to Equatable.]]></description>
<content:encoded><![CDATA[<p>It might sound counter-intuitive why I did that, but in my app <a href="https://remafox.app/">RemafoX</a> I needed to conform the <code>Binding</code> type, which is shipped with SwiftUI to bind changes in views to data, to the <code>Equatable</code> protocol to be able to pass a Binding object around in my apps data layer. The implementation of the extension looked like this:</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>While I wasn’t 100% happy with this solution, it worked fine when I first developed it so I shipped it. But then, with some macOS update a bug started to creep into all Picker views in my app. The pickers still worked most of the time, but sometimes they would behave strangely. The only behavior I could always reproduce was that when I set a selected value to the binding programmatically, and then later the user changed it to another value, it would show both values with a checkmark in the Picker dropdown, which is probably a bug somewhere in SwiftUI:</p><p><img src="/assets/images/blog/binding-equatable-vs-equatablebinding/picker-double-checkmark.gif" alt="" loading="lazy" /></p><p>It took me hours of commenting out code to figure out the root cause, and it turned out to be the <code>Binding</code> extension I mentioned above. Somehow, defining it in my app seems to have influenced the internal implementation of the <code>Picker</code> view in SwiftUI. And knowing that, who knows what other side effects it might have caused or potentially cause in future system updates? So I clearly needed to find a better solution that shouldn’t affect behavior in SwiftUI views.</p><p>The reason I wanted to make <code>Binding</code> conform to <code>Equatable</code> was that I had requirements on my data layer, namely some <code>State</code> types in the <a href="https://github.com/pointfreeco/swift-composable-architecture">TCA architecture</a>, that required all data I stored in it to also conform to <code>Equatable</code>. Something like:</p><pre><code class="language-Swift">struct AppState: Equatable {
   // other properties

   var configFile: Binding&lt;ConfigFile&gt;
}</code></pre><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 /><p>The solution I found is quite simple, although I had to “learn” how to write a property wrapper as it was the first time I needed my own. But it was straight-forward, I believe you’ll understand it even if you haven’t ever written one:</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>The only requirement of the <code>@propertyWrapper</code> is the <code>wrappedValue</code> property, and because I wanted to share this wrapper in my whole modularized application, I also had to write a public initializer, but it’s all straightforward. The <code>==</code> function is the only requirement of the <code>Equatable</code> protocol and it’s also straightforward.</p><p>With this, I can now mark all my <code>Binding</code> properties with <code>@EquatableBinding</code>:</p><pre><code class="language-Swift">struct AppState: Equatable {
   // other properties
   
   @EquatableBinding&lt;ConfigFile&gt;
   var configFile: Binding&lt;ConfigFile&gt;
}</code></pre><p>And that’s it, the type <code>AppState</code> now is fully <code>Equatable</code>, the weird dropdown issue is solved, and there’s no risk for any side effects because I’m not conforming any existing types from other frameworks to new protocols. Instead, I just introduced a <em>new</em> type that other frameworks don’t even know about, so they can’t be affected. 🚀</p><p>The lesson to learn is to never extend types that you don’t own with protocols that you don’t own. There’s even a <a href="https://www.fline.dev/swift-evolution-monthly-july-22/#se-0364-warning-for-retroactive-conformances-of-external-types">Swift proposal</a> to make this a compiler warning. Note that the solution isn’t <em>always</em> a property wrapper. There are many ways to include your own types when conforming to protocols, just don’t forget to do it.</p><blockquote><p>💁🏻‍♂️ <strong>What is RemafoX?</strong>
A native Mac app that integrates with Xcode to help translate <em>your</em> app.
<a href="https://apps.apple.com/app/apple-store/id1605635026?pt=549314&ct=fline.dev&mt=8"><strong>Get it now</strong></a> to save time during development &amp; make localization easy.</p></blockquote>]]></content:encoded>
</item>
<item>
<title>Pulsating Button Animation in SwiftUI</title>
<link>https://fline.dev/snippets/pulsating-button-animation-swiftui/</link>
<guid isPermaLink="true">https://fline.dev/snippets/pulsating-button-animation-swiftui/</guid>
<pubDate>Fri, 07 Apr 2023 00:00:00 +0000</pubDate>
<description><![CDATA[How to create a pulsating button effect in SwiftUI to guide users during onboarding.]]></description>
<content:encoded><![CDATA[<h2 id="guiding-users-with-a-pulsating-button">Guiding Users with a Pulsating Button</h2><p>When building an onboarding flow, one challenge is directing the user’s attention to the next action they should take. A subtle pulsating animation on buttons draws the eye without being intrusive. I implemented this in SwiftUI using a combination of <code>scaleEffect</code> and <code>opacity</code> modifiers tied to a repeating animation.</p><p>Here is the core approach:</p><pre><code class="language-swift">struct PulsatingButtonStyle: ButtonStyle {
   @State private var isPulsating = false

   func makeBody(configuration: Configuration) -&gt; some View {
      configuration.label
         .scaleEffect(isPulsating ? 1.06 : 1.0)
         .opacity(isPulsating ? 0.8 : 1.0)
         .animation(
            .easeInOut(duration: 0.8)
               .repeatForever(autoreverses: true),
            value: isPulsating
         )
         .onAppear {
            isPulsating = true
         }
   }
}</code></pre><p>The trick is combining two animations – scale and opacity – that run in sync. The <code>repeatForever(autoreverses: true)</code> modifier makes the animation bounce back and forth continuously. By toggling <code>isPulsating</code> on appear, the animation starts immediately when the view is displayed.</p><p><video src="/assets/images/snippets/pulsating-button-animation-swiftui/demo.mp4" controls muted playsinline></video></p><p>The scale factor of 1.06 is intentionally subtle. Going much higher (like 1.2) makes the animation feel aggressive and distracting. The slight opacity shift adds depth to the pulse without making the button hard to read.</p><p>This pattern works well for onboarding screens where you want to highlight a “Continue” or “Get Started” button. Once the user has moved past onboarding, the animation is no longer shown, so it does not become annoying over time.</p><p>You can apply it to any button with <code>.buttonStyle(PulsatingButtonStyle())</code>, keeping the animation logic cleanly separated from your button content.</p>]]></content:encoded>
</item>
<item>
<title>Migrating to The Composable Architecture (TCA) 1.0</title>
<link>https://fline.dev/blog/migrating-to-tca-1-0/</link>
<guid isPermaLink="true">https://fline.dev/blog/migrating-to-tca-1-0/</guid>
<pubDate>Mon, 03 Apr 2023 00:00:00 +0000</pubDate>
<description><![CDATA[Sharing my learnings and my code structure after migrating my app to the vastly modernized APIs of TCA 1.0.]]></description>
<content:encoded><![CDATA[<h2 id="intro-results">Intro &amp; Results</h2><p>I just migrated my app <a href="https://remafox.app/">RemafoX</a> which was built upon <a href="https://github.com/pointfreeco/swift-composable-architecture">The Composable Architecture</a> (TCA) version <code>0.35.0</code> to the new <code>1.0</code> style of APIs. While the time between the release of <code>0.35.0</code> and the current beta of <code>1.0</code> spans less than a year, it is important to note that no less than <a href="https://github.com/pointfreeco/swift-composable-architecture/releases">27 feature releases</a> with sometimes significant changes were made in that short period of time. The Point-Free team really is working in full swing on improving app development for Swift developers, and TCA is the culmination of most of their work. And nearly every topic they discuss in their <a href="https://www.pointfree.co/">great advanced Swift video series</a> has some effect on TCA. While they managed to keep all changes pretty much source-compatible up until the current version <code>0.52.0</code>, the <code>1.0</code> release is going to <a href="https://www.pointfree.co/blog/posts/103-composable-architecture-1-0-preview">get rid of a lot of older style APIs</a> that were marked as deprecated for some time already.</p><p>Because I find it very inefficient to constantly question every decision I made regarding the architecture or conventions of my apps code, I didn’t invest much time in catching up with all the improvements they made to TCA in recent months, although I did keep one eye open to ensure I’m aware of the general direction. But the nearing release of the milestone version <code>1.0</code> of the library and the fact that I’m planning to work on some <a href="https://github.com/FlineDev/RemafoX/issues/13">bigger</a> <a href="https://github.com/FlineDev/RemafoX/issues/22">features</a>  for RemafoX next make it a good time to reconsider and learn about how to best structure my apps going forward.</p><p>Thankfully, the general concept of TCA has not changed at all. But the APIs to describe how features are connected, how navigation should work, how asynchronous work is declared, and even how dependencies are passed along have received significant changes since, all for the better, using the latest Swift features. So there was a lot to figure out and migrate for me and I tackled all of these areas of change at once, but to keep things manageable, I applied the changes module for module to all of the 33 UI features of my app <a href="https://www.pointfree.co/episodes/ep171-modularization-part-1">modularized using SwiftPM</a>.</p><p>Here are the main takeaways of the migration process up-front:</p><ol><li><p>It took me a <strong>full work week</strong> (~5 days) to complete the migration.</p></li><li><p>My code base has <strong>shrunk by 2,500 lines of code</strong>, which is a ~7% reduction.</p></li><li><p>A few navigation bugs, threading issues, and SwiftUI glitches are <strong>fixed</strong> now.</p></li><li><p>My <strong>code is much easier</strong> to understand, navigate, and reason about.</p></li></ol><p>As for the testing story of my app, all of my tests are actually still passing and I did not have to make any changes to the test code. The reason for that is that I currently only have tests for non-UI features like parsing data, searching for files, or making changes to Strings files – and quite extensive tests in some parts here. But when I considered also writing tests for my UI, I was already months behind my initial timeline of releasing the app and additionally, TCA was still a few weeks away from supporting <a href="https://www.pointfree.co/blog/posts/83-non-exhaustive-testing-in-the-composable-architecture">non-exhaustive testing</a>. So I decided against adding UI tests as I wasn’t really happy with how often one had to make changes to tests just because of some refactoring on the UI layer that didn’t really change the general behavior but required what felt like a rewrite of the related tests because of their exhaustive nature. But with non-exhaustive testing available now, I’m planning on writing UI tests for my app step by step, beginning with the most important ones: My most business-logic-heavy feature, and all my Onboarding features. I might write about this in a future article.</p><p>But for now, let’s focus on how I tackled the migration of my app’s code base.</p><h2 id="before-the-migration-case-example">Before the Migration (Case Example)</h2><p>I think the best way to explain what changes were necessary and also what other changes I did to further streamline things is to show some real-world code. So in the following I will show you how the actual code of my app’s simplest feature looked before the migration and how I evolved it to the new TCA <code>1.0</code> style.</p><p>The feature is named <code>AppInfo</code> in my code base and looks like this in the app:</p><p><img src="/assets/images/blog/migrating-to-tca-1-0/the-about-remafox-screen.webp" alt="The " loading="lazy" /></p><p><em>The “About RemafoX” screen (Cmd+I).</em></p><p>Before the migration, the features code was split up to 7 different files:</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 parts: <code>Action</code>, <code>ActionHandler</code>, <code>Error</code>, <code>Event</code>, <code>Reducer</code>, <code>State</code>, and <code>View</code>.</em></p><p>The <code>AppInfoState</code> and <code>AppInfoAction</code> define the data and interactions possible:</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>Note that I had always defined typealiases for related parts of the feature I might reference somewhere within the types for convenience, even if I had not actually used them. Also, I had an extra action <code>set&lt;name of child&gt;(isPresented:)</code> whenever I had a child view that I wanted to present via a sheet sometime later. If you’re wondering what those imports of <code>AppFoundation</code> and <code>AppUI</code> are, I’ve explained them in <a href="https://www.fline.dev/organizing-my-swiftpm-modules/">this article</a>. They help reduce the number of imports in my app.</p><p>Next, here’s what <code>AppInfoView</code> file looks like:</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>Note that for presenting a sheet I have to write no less than 11 lines of code and send the action <code>setErrorHandling(isPresented:)</code> back into the system manually. Also, experienced developers might notice that I’m actually using global dependencies in my view code, such as with <code>Plan.loadCurrent()</code>, which doesn’t make my UI code very testable. But I will introduce them as proper dependencies once I start writing tests for UI, so let’s ignore these for now.</p><p>The last missing piece of the puzzle for a feature in TCA is the <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>First, note how the <code>appInfoReducer</code> is defined on a global level, which feels wrong already. Next, 7 lines are required to connect the child feature <code>ErrorHandling</code> to this feature. And you’ll notice that I have introduced yet another type named <code>AppInfoActionHandler</code> which seems to hold the actual logic of the reducer. The reason for that is that some of my reducer logic is quite long and if I kept all logic inside the <code>switch-case</code>, I’d have a lot of cases with a lot of code inside. But Xcode doesn’t provide any features to help find and navigate between <code>switch</code> cases. So I’ve extracted that logic to functions in another type. Lastly, you will notice that I have defined an extension to the <code>AnyReducer</code> type itself for analytics purposes:</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>All this does is record an event in my Analytics engine powered by <a href="https://telemetrydeck.com/?source=fline.dev">TelemetryDeck</a> for the actions that I want to record. I find this very useful as a reminder to always consider for each new action I add to the <code>AppInfoAction</code> enum if I may want to analyze the new event in a fully anonymized way. To make this work properly, I also have to define another type for each feature, here <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>This enum defines all events I want to collect, and the <code>idComponents</code> property helps auto-create a String when passing an event name to my Analytics provider. The <code>AnalyticsEvent</code> protocol is a bit off-topic, but if you’re interested it’s just this:</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 (part of a helper module named <code>Analytics</code>)</em></p><p>You might have also spotted an <code>AppEnv</code> type that I use for the environment. This is actually a shared type which I reuse wherever I just need a basic environment type with a <code>mainQueue</code> and which is passed around all over my application:</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>Now, the last of the 7 files is <code>AppInfoError</code> and that type is actually empty for this very simple feature. But I will explain its purpose in a later article where I will cover my error-handling approach in great detail. All you need to know for this article is that when something unexpected happens, I want to show a sheet with some helpful information right in the context of a feature.</p><p>Point-Free tends to keep all their types in a single file, which might work for a small feature like <code>AppInfo</code>. But a typical feature of mine takes about 500 to 1,500 lines of code with all types combined. I typically tend to keep my files small with a soft cap of 400 lines and a hard cap of 1,000 lines (see <a href="https://realm.github.io/SwiftLint/file_length.html">SwiftLint rule defaults</a>). With 314 lines even this very simple feature already would come close to the soft cap and some of my features might even get above the hard cap. So keeping it all in one file is a no-go for me. Thus I decided to put each type in its own file instead. But I also never was 100% happy with that, as things seem very all over the place. In the best case, related code would still be together but the feature would still be evenly distributed to fewer files than 7. So, let’s see how things look after the migration.</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="after-the-migration-case-example">After the Migration (Case Example)</h2><p>In the TCA <code>1.0</code> beta, Point-Free introduced the concept of creating a special <code>struct</code> that serves as the scope or namespace for a feature and they put helping types like <code>State</code> and <code>Action</code> as subtypes into that namespace type. They gave this namespace the name <code>Feature</code> and made it conform to <code>Reducer</code>, which looks something like this:</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>To me, this structure is confusing though for two reasons:</p><ol><li><p>Naming the namespace with the suffix <code>Feature</code> while making it conform to <code>Reducer</code> seems off to me. Anywhere a <code>reducer</code> parameter need to be passed, we’d pass a <code>Feature</code> which seems confusing. The namespace then should be named <code>Reducer</code> in the first place, but then, we’d have subtypes accessed through <code>Reducer.State</code> and <code>Reducer.Action</code>, which also isn’t correct.</p></li><li><p>Fully embracing the idea of a feature namespace, I’d expect a type <code>Reducer</code> inside the <code>Feature</code> type for consistency.</p></li></ol><p>Instead, I opted for actually using the <code>Feature</code> as a namespace and also putting a <code>Reducer</code> subtype in it that conforms to <code>Reducer</code>. Well, that would result in <code>struct Reducer: Reducer</code> which is a name clash, let’s solve that with a typealias:</p><pre><code class="language-Swift">import ComposableArchitecture

public typealias FeatureReducer: Reducer</code></pre><p>Now we could define a <code>Reducer: FeatureReducer</code> subtype in our <code>Feature</code> namespace. And while we’re at it, I actually tend to forget to write a public initializer for my reducers (which is required since the app is modularized), so let’s define a new public protocol instead which requires a public initializer:</p><pre><code class="language-Swift">import ComposableArchitecture

public protocol FeatureReducer: Reducer {
   init()
}</code></pre><p>Actually, there are more things I tend to forget about other TCA feature types. Let’s make it all a clear requirement by implementing protocols like <code>FeatureReducer</code> for all kinds of subtypes within a <code>Feature</code>:</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>Excerpt of Feature.swift (part of a helper module)</em></p><p>Note that I require <code>FeatureState</code> and <code>FeatureAction</code> to be <code>Equatable</code>, which is always a good idea in TCA to make them testable and all my state &amp; action types already conform to it anyways. Additionally, I defined a <code>FeatureView</code> accordingly, plus the two extra types I need for my Analytics and Error Handling needs. Note that I also decided to instead of adding the <code>State</code> suffix to all child features as I did with <code>errorHandlingState</code> in the feature previously, I decided to go for the <code>child</code> prefix instead as in <code>childErrorHandling</code> which makes finding child features easier while scanning the attributes from top to bottom.</p><p>With these protocols in place, we can now even teach the compiler what a “feature” actually is by defining another protocol that requires all the subtypes:</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>Excerpt of Feature.swift (part of a helper module)</em></p><p>I also defined a typealias for defining the <code>store</code> in our views similar to the new <a href="https://github.com/pointfreeco/swift-composable-architecture/blob/prerelease/1.0/Sources/ComposableArchitecture/Store.swift#L615"><code>StoreOf</code> typealias</a> created by Point-Free, but specific to a <code>Feature</code>.</p><p>Alright, with this up-front work, let’s see what the migrated <code>Feature</code> looks like:</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>Overview of AppInfoFeature.swift</em></p><p>Note that I defined an <code>enum</code> instead of a <code>struct</code> for the <code>Feature</code> to signify that this merely represents a namespace. Next, all of the subtypes conform to exactly what their name is with <code>Feature</code> added as a prefix, e.g. <code>State: FeatureState</code>. This makes it really easy to remember what to conform to and the code more consistent.</p><p>Here’s the <code>State</code> body I left out from the code sample above for a better overview:</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>This looks pretty similar to the original <code>AppInfoState</code>, but this time the child is renamed from <code>errorHandlingFeature</code> to <code>childErrorHandling</code>. Also because I also migrated the child feature itself, the type changed from <code>ErrorHandlingState?</code> to <code>ErorHandlingFeature.State?</code>. Also, I added the <code>@PresentationState</code> attribute for the new navigation style in TCA <code>1.0</code> that supports dismissal from within the child using <code>@Dependency(\.dismiss)</code> and calling <code>self.dismiss()</code> in the <code>Reducer</code>.</p><p>Next, let’s take a look at our <code>Action</code> subtype:</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>This is also pretty much a copy of the original <code>AppInfoAction</code>, but note that the child action has now a different type. It changed from <code>HelpfulErrorAction</code> to <code>PresentationAction&lt;HelpfulErrorFeature.Action&gt;</code>, which is a wrapper that puts all child actions into a case named <code>.presented</code> – the other case <code>.dismiss</code> reports back that the child was dismissed in case the parent needs to react to that. Thanks to <code>PresentationAction</code>, I could completely get rid of the action <code>setErrorHandling(isPresented:)</code> as this is now encapsulated in TCA-provided types.</p><p>Let’s now take a look at what our <code>View</code> looks like:</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>As you can see, I’m using the <code>FeatureStore</code> typealias instead of the old style <code>Store&lt;State, Action&gt;</code> or the TCA <code>1.0</code> style <code>StoreOf&lt;Feature&gt;</code>. But where’s everything else that defines a SwiftUI <code>View</code> like the <code>body</code> property? Well, the implementation of a view typically is one of the longest parts of a feature, so while I opted to keep the structural parts of all subtypes in one place, conformances to protocols that require a lot of code I extracted to extension files.</p><p>The implementation of the <code>View</code> is in a separate file as an extension:</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>The implementation of the <code>body</code> property is pretty much the same as before. But note that the 11 lines <code>.sheet</code> modifier has shrunk to just 3 lines. This is thanks to the new navigation tools using <code>@PresentationState</code> and <code>PresentationAction</code>. Another change happened to the <code>static let store</code> inside the <code>PreviewProvider</code>: There’s no environment parameter to pass to the Store anymore!</p><p>Let’s take a look at the <code>Reducer</code> subtype to learn why this is:</p><pre><code class="language-Swift">   public struct Reducer: FeatureReducer {
      @Dependency(\.mainQueue)
      var mainQueue

      @Dependency(\.continuousClock)
      var clock

      public init() {}
   }</code></pre><p>Note the usage of the <code>@Dependency</code> attribute. It might remind you of the <code>@Environment</code> attribute in SwiftUI, and it actually works exactly the same. This new attribute is why there’s no <code>Environment</code> type needed anymore in TCA <code>1.0</code>. Instead, all dependencies are declared using the <code>@Dependency</code> attribute. This allows me to entirely get rid of the <code>AppEnv</code> type I had passed around before.</p><p>Yet again, you might be missing the actual implementation of the <code>Reducer</code> protocol. Well, the implementation of the protocol is the second portion of code in a feature that can get pretty long, so I also opted to extract that to its own file:</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>Note first the entirely different structure. No global <code>reducer</code> variables are needed anymore. Instead, a <code>body</code> property is implemented, very much like with the <code>View</code> protocol in SwiftUI. And the analogy doesn’t end there, the structure is also very SwiftUI-like with a mere list of different reducers that together build the <code>AppInfoFeature.Reducer</code>, including one called <code>BindingReducer()</code> which replaces <code>.binding()</code>. Also note that the 7 lines of code connecting the child feature have shrunk down to just 3 lines using the new <code>.ifLet</code> API. Additionally, instead of having to define a custom <code>ActionHandler</code> type where I put the implementation of the logic to react upon actions, because we are now in a type and not a global level, I could easily move those functions into the <code>Reducer</code> type itself. Also, the implementation of <code>copyEnvironmentInfoPressed</code> is using the new <code>async</code> style APIs. Previously, it was implemented using the less readable <code>Combine</code> style:</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>Excerpt from the old AppInfoActionHandler.swift</em></p><p>Lastly, due to the new SwiftUI-like function builder style, I had to change my <code>AnyReducer</code> extension function <code>recordAnalyticsEvents</code> to simply being a <code>Reducer</code> that stores the execution logic as a property like so:</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 (from a helper module)</em></p><p>The body of the analytics helper in the Reducer above didn’t change at all, I just copied it over from the previous helper function into this new reducers initializer.</p><p>And that’s all, the entire <code>AppInfo</code> feature is migrated over to TCA <code>1.0</code>. The overall file structure now looks like this, with just 3 files instead of 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>Note that I’m using <code>*</code> as a separator for signaling that the file contains the main portion of a <em>sub</em>type. Naturally, we could use  <code>.</code> as a separator making the name read like <code>AppInfoFeature.Reducer.swift</code>. But because <code>.R</code> from <code>.Reducer</code> is sorted above <code>.s</code> from <code>.swift</code>, this would result in the subtypes files appearing above the main feature file <code>AppInfoFeature.swift</code>, so I opted for a separator that looks similar to a dot but has lower precedence than <code>.</code> which lead to <code>*</code>.</p><p>The <a href="https://realm.github.io/SwiftLint/file_name.html">SwiftLint rule <code>file_name</code></a> which I opted in to showed me a warning with this naming style. But I could easily adjust that by adding this to the config file:</p><pre><code class="language-yaml">file_name:
   nested_type_separator: '*'</code></pre><h2 id="conclusion">Conclusion</h2><p>Migrating my medium-sized app to the new TCA <code>1.0</code> style of APIs was a lot of work, but most of it was setting up file structures, doing search &amp; replace, and moving existing code to other places. And I invested quite some of my time into figuring out a good structure that I liked. I think if I had to do it again for another app with my learnings, I’d probably be done in 2-3 days rather than 5.</p><p>Only in a few places I had to actually adjust code, mostly when migrating Combine-style effect code in my reducers to async-await style code. But thanks to great documentation and warnings, it was always pretty clear what to do. For everyone doing a similar migration, here are the 3 links I found most useful:</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>Also, if there’s one episode that gives a good overview of the advancements in TCA <code>1.0</code>, it’s the first ~35 minutes of <a href="https://www.pointfree.co/episodes/ep222-composable-navigation-tabs">episode #222</a> which kicks off Composable Navigation. Watch it to quickly get an idea of how things changed in the past year.</p>]]></content:encoded>
</item>
<item>
<title>2,000 Imports: Organizing my Apps&apos; SwiftPM modules</title>
<link>https://fline.dev/blog/organizing-my-swiftpm-modules/</link>
<guid isPermaLink="true">https://fline.dev/blog/organizing-my-swiftpm-modules/</guid>
<pubDate>Thu, 23 Mar 2023 00:00:00 +0000</pubDate>
<description><![CDATA[How to organize your apps Swift modules for clarity & convenience using a hidden (unofficial) Swift feature. A practical solution for small to medium-sized apps.]]></description>
<content:encoded><![CDATA[<h2 id="the-problem">The Problem</h2><p>I recently decided to work on the <a href="https://github.com/FlineDev/RemafoX/issues/13">biggest feature</a> for RemafoX to date and while I was thinking about where to start, I found myself drowning in over 70 targets for my less-than-one-year-old project. Note that I’m modularizing my app for clear code segregation and faster build times (= faster SwiftUI previews, tests &amp; more) using the vanilla SwiftPM-based method presented by Point-Free in <a href="https://www.pointfree.co/episodes/ep171-modularization-part-1">this free episode</a>. Because I plan on working on this app for years to come (the <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 are just the tip of the iceberg, I have many more ideas), I’ve decided to first clean up this mess. After all, a round of refactoring between features keeps the code base clean and makes the developer happy! 😇</p><p>I remembered that I had discovered <a href="https://github.com/apple/swift/blob/main/docs/ReferenceGuides/UnderscoredAttributes.md#_exported">the <code>@_exported</code> attribute</a> while reading through Swift Evolution threads when preparing one of the issues of <a href="https://swiftevolution.substack.com/">my related newsletter</a>. While it’s not recommended to use underscored APIs as their behavior might change or they might even potentially get entirely removed, I found myself lacking alternatives with my goal of cleaning up the many unorganized targets. For exactly this reason, I convinced myself that the chances of this attribute getting entirely removed were relatively low. If anything, I believe that the <a href="https://forums.swift.org/t/exported-and-fixing-import-visibility/9415">related pitch</a> might get picked up some time and finds its way into official Swift, so we can replace <code>@_exported</code> with whatever it could be named then. Also, I found out that Point-Free is <a href="https://github.com/pointfreeco/swift-composable-architecture/blob/32dafefb1746af47fd0010637221415ac6828b08/Sources/ComposableArchitecture/Internal/Exports.swift">depending on this attribute</a> as well in <a href="https://github.com/pointfreeco/swift-composable-architecture">The Composable Architecture</a> framework, which my app heavily depends on already. So why not go all-in on it?</p><p>In short, what the attribute helps with is this: Imagine you have 10 feature modules and 5 helper modules. In each file of the 10 features, I tend to import all (or most) of these 5 helper modules, which results in something like this:</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>And this gets repeated over and over again. Ok, it’s true that not all of them are needed in <em>every</em> single file of a target, but the truth is also that Xcode is linking the entire module anyway when only a single file in the module imports it, so importing them all in all files wouldn’t hurt build times (AFAIK). Using <code>@_exported import</code> we can combine all these imports by creating a new target, call it something like <code>CoreDependencies</code> and create a Swift file in it with this content:</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>Now, whenever we <code>import CoreDependencies</code>, it will import all other modules, too!</p><p>But is putting everything into one group called <code>CoreDependencies</code> really the right solution? Another problem apart from having to repeat the imports too often is that I’m currently sorting all these 70 modules alphabetically due to the lack of another kind of grouping or structure. This lack of grouping doesn’t only make it harder to find the right module when I roughly know what I’m looking for but don’t remember the exact name of the module. It can also lead to <a href="https://en.wikipedia.org/wiki/Circular_dependency">circular dependencies</a> while working on features and trying to reuse as much code as possible. It requires strategic planning of what belongs where to allow reusing code while preventing cyclic dependencies which lead to compiler errors.</p><h2 id="the-solution">The Solution</h2><blockquote><p>✨ UPDATE: For new/smaller apps, I use a simplified solution of what I describe in detail below. See <a href="https://github.com/FlineDev/Foundation">my FlineDevKit repository</a> for more.</p></blockquote><p>The best way to find a practical solution to a problem is to look at a real-world example. So, here’s a selection of modules I actually use in <a href="https://remafox.app/">RemafoX</a>:</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>Yes, I’ve also listed <code>Foundation</code> and <code>SwiftUI</code> in the list above. Why? Because at the end of the day, they are dependencies that need to be imported as well, just like any other dependency we import, be it an external or internal dependency. I view them as built-in external dependencies. You might be used to at least <code>import Foundation</code> in any Swift file, but actually, you can write Swift code without <code>Foundation</code>, you’ll just have the barebones Swift features then including everything contained in the <a href="https://developer.apple.com/documentation/swift/swift-standard-library">Swift Standard Library</a>. It totally works!</p><p>And actually, these two imports that we all made so often represent an Apple-internal grouping of features/helpers: Apple groups a whole bunch of functionality behind <code>Foundation</code>, and they do the same with <code>SwiftUI</code> or <code>UIKit</code>/<code>AppKit</code>. The deciding factor seems to be that everything that represents some kind of UI or is directly related to UI belongs to one group, and everything that doesn’t represent a UI or isn’t directly related to UI into another group. So, the most natural thing we could do is to follow their lead, we could even copy their naming by using <code>Foundation</code> for the non-UI group and <code>UI</code> (which appears in both <code>SwiftUI</code> and <code>UIKit</code>) for the UI group. Because our groups are specific to an apps domain, the resulting names for our groups would be: <code>AppFoundation</code> and <code>AppUI</code>.</p><p>Let’s apply this to the list of modules above:</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>This already starts to look better. But there’s one more thing we can learn from how Apple structures its frameworks: Apple doesn’t link each and every non-UI feature as part of <code>Foundation</code>, nor do they ship all SwiftUI-related code as part of <code>SwiftUI</code>. <code>Combine</code> and <code>Charts</code> are two frameworks we need to import separately. Why not ship them as part of <code>Foundation</code> and <code>SwiftUI</code>? Because they are useful only in some specific domains and might not be needed in a more global scope.</p><p>If you remember the initial problem, it was that I had a set of modules that I imported over and over again in many places because they were useful helpers in a global manner, rather than being useful only in some specific domains. So it makes sense to import them as part of a unified group name. But what actually is a helper? What differentiates it from a more domain-specific feature?</p><p>I personally call a feature a “helper” or a “utility” feature, when its <strong>global availability is much more useful than it hurts</strong> the development process. Of course, this is somewhat subjective but as a rule of thumb I do this: If I already use the feature in multiple different parts of my app, plus when thinking about 2 or 3 potential new features I might add to my app sometime in the future and at least one of them could also make use of it, then it’s probably very useful globally.</p><p>In more practical terms, I would separate the above list of modules like this:</p><pre><code class="language-Swift">// (globally useful) Helpers
Analytics
Assets
BetterCodable
ComposableArchitecture
Constants
Foundation
HandySwift
HelpfulErrorUI
ReusableUI
SFSafeSymbols
SwiftUI
Utility

// (domain-specific) Features
CommandLineSetup
FilesSearch
MachineTranslation
Paywall
ProjectsBrowser
Settings</code></pre><p>If we now combine the two dimensions of separation, we end up with something like the following graph with 4 quarters &amp; dependencies in between:</p><p><img src="/assets/images/blog/organizing-my-swiftpm-modules/11-imports-were-reduced.webp" alt="11 imports were reduced to just 2 thanks to the @_exported attribute." loading="lazy" /></p><p>Here are a few important things to note:</p><ol><li><p>The ⬆️ top “Feature” half is built on top of the bottom “Helpers” half, thus:</p></li><li><p>The ↖️ green “Non-UI Features” modules can <code>import AppFoundation</code>.</p></li><li><p>The ↗️ red “UI Features” modules can import both, <code>AppFoundation</code> &amp; <code>AppUI</code>.</p></li><li><p>Within a group (quarter), modules can depend on each other (prevent cycles!).</p></li><li><p>➡️ “UI” modules can depend on ⬅️ “Non-UI” modules or on <code>AppFoundation</code>.</p></li><li><p>The ⬇️ bottom “Helpers” are never allowed to import from “Features” above!</p></li><li><p>External modules can also be “Features” (see ↖️, currently I have none in ↗️)</p></li></ol><p>To apply this structure, I just created a new module named <code>AppFoundation</code>, plus a new Swift file in it named <code>AppFoundation.swift</code> with the following contents:</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>I also created a module <code>AppUI</code> with the following contents for <code>AppUI.swift</code> in it:</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>Now I can replace the 11 imports from the initial example at the beginning of this article, that I took from a file inside a UI Feature module, with just these 2 lines:</p><pre><code class="language-Swift">import AppFoundation
import AppUI</code></pre><p><em>11 imports were reduced to just 2 thanks to the @_exported attribute.</em></p><p>Note that I didn’t even have to import <code>Foundation</code> or <code>SwiftUI</code>. And for any Non-UI Feature I even just need a single line stating  <code>import AppFoundation</code>!</p><p>Of course, this doesn’t mean I won’t ever import anything else anymore. I’ll still be having imports on the vertical axis, where a specific module imports another specific module <em>inside</em> a group, e.g. a <code>ConfigFile</code> UI feature importing child components like <code>ConfigFileLinter</code> and <code>ConfigFileNormalizer</code>. But these are domain-specific imports and don’t lead to many repetitive imports.</p><p>The last thing I did was group my products, my dependencies, and targets in my <code>Package.swift</code> file by these 4 quarters. For this, I added pragma marks like <code>// MARK: - Non-UI Features</code> in all sections and put the related statements into them in alphabetic order. My resulting manifest now looks something like this:</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>☑️ Similar to <code>AppFoundation</code> and <code>AppUI</code>, I also introduced an <code>AppTest</code> grouping target to my app which I use to unify imports of <code>XCTest</code> and dependencies/helpers like <a href="https://github.com/pointfreeco/swift-custom-dump"><code>CustomDump</code></a> (highly recommended!).</p></blockquote><p>To replace all relevant imports with <code>AppFoundation</code>/<code>AppUI</code>, I used this trick:</p><ol><li><p>First, I used Xcodes <a href="https://developer.apple.com/documentation/xcode/finding-and-replacing-content-in-a-project">Find &amp; Replace</a> for every <code>@_exported</code> lib and replaced all imports with <code>AppFoundation</code>, so I ended up with a lot of files with multiple imports of <code>AppFoundation</code>.</p></li><li><p>Next, I used the <a href="https://github.com/realm/SwiftLint">SwiftLint</a> rule <a href="https://realm.github.io/SwiftLint/duplicate_imports.html"><code>duplicate_imports</code></a> which supports auto-correction. Install it via <code>brew install swiftlint</code>, then run these 3 lines:</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>Adjust the parameter passed to –path if yours is different than Sources.</em></p><ol><li><p>Lastly, I reverted the changes for files that lie inside modules which are themselves part of <code>AppFoundation</code> using Git, also <code>AppFoundation.swift</code> itself.</p></li><li><p>Then I repeated the above steps for <code>AppUI</code>. It all took less than 10 minutes!</p></li></ol><p>That’s it! As you can see, before cleaning up I had ~2,000 imports in my project:</p><p><img src="/assets/images/blog/organizing-my-swiftpm-modules/the-solution.webp" alt="The solution" loading="lazy" /></p><p>After the cleanup, I have now only 1,200 imports, roughly 40% less than before!</p><p><img src="/assets/images/blog/organizing-my-swiftpm-modules/the-solution-2.webp" alt="The solution 2" loading="lazy" /></p><p>Also, my <code>Package.swift</code> manifest file got a lot shorter, from 827 lines to 575 lines, that’s roughly a third less. And it’s all so much more structured, I’m happy! 😍</p><h2 id="conclusion">Conclusion</h2><p>Thanks to <code>@_exported import</code> and separation of modules into four groups by asking if they are (A) “UI-related” or “Non-UI-related”, and (B) more “globally useful” or more “domain-specific”, I can now import an infinite number of “Helper” modules into my “Feature” modules with just one or two <code>import</code> lines! Not only that, but these groups with their import rules also serve as a guide to easily place my code into the right module to prevent circular dependencies.</p><p>The result: Less code to write, fewer chances for build errors – a win-win!</p>]]></content:encoded>
</item>
<item>
<title>Hardware Requirements for iOS Development (May 2025)</title>
<link>https://fline.dev/blog/hardware-requirements-for-ios-development/</link>
<guid isPermaLink="true">https://fline.dev/blog/hardware-requirements-for-ios-development/</guid>
<pubDate>Fri, 02 Dec 2022 00:00:00 +0000</pubDate>
<description><![CDATA[From the cheapest viable option to the best value Mac for iOS Developers.]]></description>
<content:encoded><![CDATA[<p>I see people asking questions about which hardware to get for iOS development quite often, especially beginners just starting out but also sometimes more experienced developers who start running into performance issues with their current Mac. As a former iOS Team lead in two companies where I had to make informed decisions about which hardware to get for what level of developer and where I could test out different Macs on differently sized projects, here are my current recommendations.</p><p>In the end, I also answer some frequently asked questions:</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 or 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">How much disk space do I need?</a></p></li><li><p><a href="https://jeehut.medium.com/hardware-requirements-for-ios-development-may-2022-ac0234cfc879?source=your_stories_page-------------------------------------#10d8">How much RAM do I need?</a></p></li><li><p><a href="https://jeehut.medium.com/hardware-requirements-for-ios-development-may-2022-ac0234cfc879?source=your_stories_page-------------------------------------#5f2b">What about the Mac Studio?</a></p></li></ul><blockquote><p><em>This article was <strong>last updated in December 2024</strong>. If 6 months have passed since then, <em><a href="https://bsky.app/profile/jeehut.bsky.social"><em>ping me on Bluesky</em></a></em> &amp; remind me to update.</em></p></blockquote><h2 id="the-cheapest-mac-for-developers-750">The Cheapest Mac for Developers ($750+)</h2><p><img src="/assets/images/blog/hardware-requirements-for-ios-development/outdated-screenshot-of.webp" alt="Outdated screenshot of this XcodeBenchmark repository table." 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 (16GB RAM &amp; 256GB SSD)</strong></a> for <strong>$600</strong></p><ul><li><p>monitor (at least <strong>$120</strong>)</p></li><li><p>wireless mouse &amp; keyboard (at least <strong>$30</strong>)</p></li></ul><h3 id="pros">Pros:</h3><ul><li><p>Fast build times with future-proof M4 Apple Silicon processor</p></li><li><p>Fast SSD for opening Xcode projects quickly (many small files)</p></li><li><p>Cheapest overall option with a total price (including periphery) of ~$750</p></li><li><p>Built-in ports, no adapter needed (5x USB-C, HDMI, LAN, 3.5mm)</p></li></ul><h3 id="cons">Cons:</h3><ul><li><p>Immobile (not a Laptop)</p></li><li><p>Requires additional periphery (mouse, keyboard, monitor)</p></li><li><p>256 GB SSD is barely enough for development, no space for other media</p></li></ul><h3 id="upgrade-options">Upgrade Options</h3><ul><li><p>Get 512GB SSD if you plan to use it for other purposes as well (<strong>+$200</strong>)</p></li><li><p>OR buy a 2TB external SSD <a href="https://www.amazon.com/SAMSUNG-Inch-Internal-MZ-77E4T0B-AM/dp/B08QB93S6R">for a slightly lower price</a> (for media only)</p></li><li><p>Get the <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> for a 4.5K monitor, Apple mouse &amp; keyboard (<strong>+$550</strong>)</p></li></ul><h3 id="recommended-group">Recommended Group</h3><p>When money is the main concern, e.g. this is just a hobby for you, <strong>and</strong> you know you’re going to be coding from a fixed place. <strong>Or</strong> if you want to use the Mac as a media server as well for your home (with an external hard drive).</p><h2 id="the-cheapest-mobile-mac-for-devs-1000">The Cheapest <em>Mobile</em> Mac for Devs ($1,000+)</h2><p><img src="/assets/images/blog/hardware-requirements-for-ios-development/outdated-screenshot-of-4.webp" alt="Outdated screenshot of this XcodeBenchmark repository table." 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 (16GB RAM &amp; 256GB SSD)</strong></a> for <strong>$1,000</strong></p><h3 id="pros">Pros:</h3><ul><li><p>Fast build times with good-enough M4 Apple Silicon processor</p></li><li><p>Fast SSD for opening Xcode projects quickly (many small files)</p></li><li><p>Mobile (Laptop with 13” built-in screen, keyboard &amp; trackpad)</p></li></ul><h3 id="cons">Cons:</h3><ul><li><p>More expensive than the Mac mini (even with peripherals included)</p></li><li><p>Using an external hard drive instead of upgrading SSD is not viable</p></li><li><p>You will need to buy and take with you a USB-C adapter for compatibility</p></li><li><p>256 GB SSD is barely enough for development, no space for other media</p></li></ul><h3 id="upgrade-options">Upgrade Options</h3><ul><li><p>Get 512GB SSD if you plan to use it for other purposes as well (<strong>+$200</strong>)</p></li></ul><h3 id="recommended-group">Recommended Group</h3><p>When money is a concern but you need a Laptop anyways, e.g. because you’re a student, <strong>and</strong> you want to be flexible and take it always with you.</p><h2 id="the-best-value-mac-for-developers-2500">The Best Value Mac for Developers ($2,500+)</h2><p><img src="/assets/images/blog/hardware-requirements-for-ios-development/outdated-screenshot-of-3.webp" alt="Outdated screenshot of this XcodeBenchmark repository table." 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 (24GB &amp; 512GB)</strong></a> for <strong>$2,500</strong></p><h3 id="pros">Pros:</h3><ul><li><p>14-core M4 Pro with another ~50% faster build times (compared to M4)</p></li><li><p>Larger &amp; higher quality 4K, 120Hz, HDR screen (compared to Air)</p></li><li><p>Longest battery life in any Mac ever (longer than M4 Max or 14” variants)</p></li><li><p>Built-in card-reader and HDMI port &amp; 1 extra USB-C (compared to Air)</p></li><li><p>Fast 512GB SSD for opening Xcode projects quickly (many small files)</p></li><li><p>Extra 8 GB RAM headroom for more advanced developer workflows</p></li><li><p>Mobile (Laptop with 16” built-in screen, keyboard &amp; trackpad)</p></li></ul><h3 id="cons">Cons:</h3><ul><li><p>Quite expensive (saved built time worth it if coding professionally)</p></li></ul><h3 id="upgrade-options">Upgrade Options</h3><ul><li><p>Get 1TB SSD if you want to have some extra headroom (<strong>+$200</strong>)</p></li><li><p>Get <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> for 20% faster CPU, 48GB RAM, 1TB SSD, faster GPU (<strong>+$1,500</strong>)</p></li></ul><h3 id="recommended-group">Recommended Group</h3><p>When <strong>build performance</strong> is your main concern (= professional developers) and you don’t want to waste any money on small improvements, get this.</p><p>If you prefer the <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” form factor</a> for $2,200 (don’t forget to upgrade to 14-core CPU!), that is similarly recommendable but comes with some smaller drawbacks such as a shorter (but still amazing) battery life and louder (but still very silent) fans for just $100 of savings. Then better choose the <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#">base 14” model</a> which costs $500 less ($2,000) but be aware that this comes with a 10% slower 12-core CPU.</p><h2 id="frequently-asked-questions">Frequently Asked Questions</h2><p>Some frequently asked hardware questions and my personal take on them.</p><h3 id="apple-silicon-or-intel">Apple Silicon or Intel?</h3><p><img src="/assets/images/blog/hardware-requirements-for-ios-development/outdated-screenshot-of-2.webp" alt="Outdated screenshot of this XcodeBenchmark repository table." loading="lazy" /></p><p>A more realistic minimum calculation therefore is:</p><ul><li><p>40 GB for macOS</p></li><li><p>80 GB for 2 versions of Xcode installed at the same time</p></li><li><p>20 GB for having 4 SDK versions installed at the same time</p></li><li><p>30 GB for build artifacts of 3 different projects at the same time</p></li><li><p>20 GB for other development-related apps and tools</p></li><li><p>50 GB for other apps (Pages, Numbers, Notion, Slack, Zoom, etc.)</p></li></ul><p>That adds up to 240 GB, so a <strong>256 GB SSD is just enough</strong> to do serious iOS development. But this only works if you <em>exclusively</em> do iOS development on this Mac and don’t use it to sync your iPhone photos or videos on it, for example. If you want to use the Mac for other uses as well, get <strong>at least 512 GB SSD</strong>. This will also give you the headroom to try out other technologies, like installing <a href="https://developer.android.com/studio/">Android Studio</a> to try out Android development as well.</p><h3 id="how-much-ram-do-i-need">How much RAM do I need?</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>Short answer:</strong> At least <em>16 GB</em>, better <em>24GB+</em>.</p><p><strong>Long answer:</strong><br />8GB is a good start if you already own a Mac and you should be able to run a single Simulator plus Safari with many tabs easily on it without many hiccups. But as soon as your project gets larger and you start doing two things at once and maybe even have a server or VM running in the background, you’ll be limited by the RAM.</p><p>And if you consider ever doing Android development on the machine, too, you definitely need the 16GB option because (unlike iOS Simulators) the Android Emulator can’t efficiently share RAM with the host system.</p><p>While the difference is very noticeable when comparing 8GB to 16GB, the differences are much smaller when going from 16GB to 24GB or higher. Matt Gallagher recently wrote a <a href="https://twitter.com/cocoawithlove/status/1517736163318009856?s=21&t=kBvPG1DmkGdh7PHiJWsEdw&ref=fline.dev">good comparison on Twitter</a>, ending with the final note: “Don’t choose RAM over faster CPU”. I totally agree with that.</p><p>Thankfully, since 2024, all new Macs have at least 16 GB, so you should be good.</p><h3 id="what-about-the-mac-studio">What about the 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>Short answer: W</strong>ait for the M4 Ultra Mac Studio, if you can afford it. It’s a beast.</p><p><strong>Long answer:</strong><br />The current Mac Studio machines are in a weird spot, still running on M2 Max and M2 Ultra chips. The current M4 Pro is ~33% faster than the M2 Max, and the M4 Max is ~7% faster than the M4 Ultra for development tasks. So it’s currently not a good idea to buy any of the Mac Studio machines. Instead, if you like the form factor, just get a <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> (make sure to upgrade to the 14-core CPU!) at a cost of $1,600 ($400 less than the M2 Max Mac <em>Studio</em>).</p><p>The <a href="https://www.apple.com/shop/buy-mac/mac-studio/10-core-cpu-24-core-gpu-16-core-neural-engine-32gb-memory-512gb#">base Mac Studio</a> for $2,000 has a 33% slower 12-core CPU than the best-value MacBook Pro recommended above. You can of course get it to save $500 if you don’t need the MacBook’s amazing screen, speaker system, and mobility. But then you need to pay for a monitor, mouse &amp; keyboard which (as we’re at the professional level here) can cost you about the same unless you have them.</p><p>The <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 variant</a> for $4,000 used to be the more interesting device here because it had twice the CPU cores and came with yet again faster build times compared to the Max chips. If money isn’t a concern to you but performance is, feel free to wait for the M4 Ultra and get it for the fastest possible builds.</p>]]></content:encoded>
</item>
<item>
<title>Introducing RemafoX: Easy App Localization</title>
<link>https://fline.dev/blog/introducing-remafox-easy-app-localization/</link>
<guid isPermaLink="true">https://fline.dev/blog/introducing-remafox-easy-app-localization/</guid>
<pubDate>Thu, 13 Oct 2022 00:00:00 +0000</pubDate>
<description><![CDATA[Say hello to RemafoX, the app on the mission to simplify developer life by providing new workflows for localization when working with Xcode.]]></description>
<content:encoded><![CDATA[<p>After 90 days of Beta-testing, time for the <a href="https://apps.apple.com/app/apple-store/id1605635026?pt=549314&ct=fline.dev.ReMafoX&mt=8&ref=fline.dev">public release on the Mac App Store</a>!</p><h2 id="features">Features</h2><p>This first release is already <strong>jam-packed with many features</strong> that’ll help you:</p><ul><li><p><strong>Focus:</strong> Don’t ever leave the context of your Swift file – keep coding</p></li><li><p><strong>Be Faster:</strong> No longer manually edit Strings files when adding new keys</p></li><li><p><strong>Automate:</strong> Set up DeepL or Microsoft Translator to translate your app</p></li><li><p><strong>Lint:</strong> Get warnings in Xcode for empty translations or duplicate keys</p></li><li><p><strong>Normalize:</strong> Strings files sorted alphabetically to help find translations</p></li><li><p><strong>Synchronize:</strong> Auto-update Strings files on Storyboard/XIB file changes</p></li><li><p><strong>Learn:</strong> Lots of explanations, step-by-step guides, and even videos</p></li><li><p><strong>Pluralize:</strong> Auto-detection &amp; language-aware form for easy pluralization</p></li></ul><h2 id="roadmap">Roadmap</h2><p>But that’s just the beginning. <strong>Each month, a new feature</strong> will be added, like:</p><ul><li><p><strong><a href="https://github.com/FlineDev/ReMafoX/issues/14">Fastlane support</a>:</strong> Translate App Store description &amp; “What’s New”</p></li><li><p><strong><a href="https://github.com/FlineDev/ReMafoX/issues/13">Verification</a>:</strong> Easy way of inviting others to check/provide translations</p></li><li><p><strong><a href="https://github.com/FlineDev/ReMafoX/issues/12">Strings editor</a>:</strong> A special UI to make edits on Strings(dict) files nicer</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> Support for more translation services</p></li><li><p><a href="https://github.com/FlineDev/ReMafoX/issues/10"><strong>Open Source</strong></a><strong>:</strong> Support the community by making Pro features Free</p></li><li><p>… and <strong>much more</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">explore &amp; vote on features</a> or request your own](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="previews">Previews</h2><p>Still not convinced? Then check out the following 3 GIFs showcasing RemafoX:</p><p><strong>Add a new translation</strong> to a project in many languages with <strong>just one step</strong>:</p><p><img src="/assets/images/blog/introducing-remafox-easy-app-localization/add-translation-edit-with-audio.gif" alt="" loading="lazy" /></p><p>Find <strong>empty translations</strong> in Strings files and use <strong>machine translation</strong>:</p><p><img src="/assets/images/blog/introducing-remafox-easy-app-localization/previews.gif" alt="" loading="lazy" /></p><p><strong>Set up a project</strong> in RemafoX and <strong>customize</strong> with documented config options:</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="special-offer">Special Offer</h2><p>Purchase a lifetime subscription today to **save ~30% **with the Early Bird discount running til the end of October. <strong>Monthly or yearly subscriptions</strong> support the continued development of this project and come all with a Free Trial period. So you can see for yourself how ReMafoX improves <em>your</em> developer workflow.</p><p>But there’s also a <strong>Free tier</strong> for smaller projects including all of the above features except pluralization. I promise that I will never remove any features from the Free tier, so if this tier works for you today, it will always do! And most of the planned features will benefit the Free tier as well.</p><h2 id="get-started-now">Get Started Now!</h2><p>So there should really be <strong>no reason <em>not</em> to use RemafoX</strong>! Get it now:</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>If you come across any issues or have questions, please consult the ‘Help’ menu.</p>]]></content:encoded>
</item>
<item>
<title>Making the Most of WWDC 2022</title>
<link>https://fline.dev/blog/making-the-most-of-wwdc-2022/</link>
<guid isPermaLink="true">https://fline.dev/blog/making-the-most-of-wwdc-2022/</guid>
<pubDate>Mon, 30 May 2022 00:00:00 +0000</pubDate>
<description><![CDATA[How to enjoy both Keynotes with other developers (remotely) and how to maximize your learnings throughout the week. If you can invest the time]]></description>
<content:encoded><![CDATA[<p>Apple hosts a developer conference each year since as early as 1983 (so next year will technically be the 40th anniversary!), even if it officially holds the name “World Wide Developer Conference”, or short “WWDC” only <a href="https://apple.fandom.com/wiki/Worldwide_Developers_Conference#History">since 1990</a>. Fun fact: The very first conference in 1983 was <a href="https://apple.fandom.com/wiki/Apple_Independent_Software_Developers_Conference_1983">held in the City of Monterey</a>, the name of <a href="https://www.apple.com/macos/monterey/">the current macOS</a> version.</p><p>While the first “Dub Dub” — that’s how most who have once attended the event in person like to refer to it — was actually a closed event with an NDA to be signed because they showed off a new product up-front to developers, it opened up the following year, and has been open to developers to attend since. At least for those who can pay a fee of ~$1,600, can afford a one-week trip to California, and were lucky enough to win the lottery as tickets were limited.</p><p>But because such large conferences were unthinkable in 2020 due to the <a href="https://en.wikipedia.org/wiki/COVID-19_pandemic">COVID-19 pandemic</a>, Apple had to do something different and since then, WWDC truly holds up to its name and actually is attendable by all developers worldwide — for free! While this is the positive side of things, of course it’s a different experience to attend an online conference than an in-person one. So this year, they are trying a mixed approach with a <a href="https://developer.apple.com/wwdc22/special-day/">Special Event at Apple Park</a> for a lucky few, but the conference itself is open to everyone worldwide still.</p><p>With these changes in place and the yearly conference potentially staying in this online format, let’s take a look at the three key aspects of the conference and how we can make the most of it for each of them if we can offer the time:</p><ul><li><p><strong>Learning</strong> about the newest Apple technologies</p></li><li><p><strong>Connecting</strong> with other Developers &amp; having a good time</p></li><li><p><strong>Discussing</strong> the impact of new technologies, features &amp; products</p></li></ul><h2 id="learning">Learning</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>What learning opportunities Apple offers officially throughout the week.</p><p><strong>Keynote &amp; Platforms State of the Union</strong><br />The <a href="https://www.youtube.com/watch?v=0TD96VTf0Xs&ref=fline.dev">WWDC Keynote</a> is where Apple presents the latest software and product updates to the public. It’s not specifically targeted at developers, but it does include a <a href="https://www.youtube.com/watch?v=0TD96VTf0Xs&t=5598s&ref=fline.dev">short developer section</a> towards the end, and big announcements like <a href="https://www.youtube.com/watch?v=w87fOAG8fjk&t=6232s&ref=fline.dev">Swift in 2014</a> or <a href="https://www.youtube.com/watch?v=psL_5RIBqnY&t=7591s&ref=fline.dev">SwiftUI in 2019</a> have all been unveiled here (watch the linked sections &amp; listen to the crowd to get excited about this year’s keynote!).</p><p>The <a href="https://developer.apple.com/videos/play/wwdc2021/102/">Platforms State of the Union</a> is also called the “Developer Keynote” for a reason. It’s basically unveiling and demoing all the new amazing APIs much like the keynote is unveiling and demoing all the new products &amp; services. It’s targeted specifically at developers so they can go into more detail and if there’s a single video you have time to watch for the whole WWDC, this is the one I would recommend because it’s like a summary of the rest of the week. It’s also a great way to find topics you’re interested in. All technologies mentioned here have dedicated sessions throughout the week.</p><p><strong>Sessions</strong><br />The <a href="https://developer.apple.com/videos/wwdc2021/">more than 200 session videos</a> form the core of WWDC and are the best place to learn about the latest technologies in more detail. A good place to watch them is the dedicated <a href="https://apps.apple.com/us/app/apple-developer/id640199958">Developer app</a>, which recently also got support for <a href="https://developer.apple.com/shareplay/">SharePlay</a> so you can even watch a session together with someone remotely and discuss it right away. Apple groups the sessions by topics so you can easily <strong>filter</strong> the videos inside the app. You can also <strong>bookmark</strong> sessions to watch later. This is what I do right after the developer keynote.</p><p><img src="/assets/images/blog/making-the-most-of-wwdc-2022/apple.webp" alt="" loading="lazy" /></p><p>In case you get overwhelmed by the sheer amount of sessions, a good start is to look out for all sessions titled “What’s new in …” or “Meet …”, as these are summary sessions for a specific topic or framework. Then in these sessions, they mention other sessions to dive deeper into certain details if needed.</p><p>To get the most out of the sessions, I like to take notes so I can easily find something I found interesting later on, and it also helps keep my attention up.Here are the notes I took during <a href="https://www.notion.so/Cihat-WWDC-2021-Notes-6cae8d046c17426f8dafddc00abdae29">WWDC 2021</a> and <a href="https://www.notion.so/Cihat-WWDC-2020-Notes-5891eff2d250446f914110f8f008925d">WWDC 2020</a>, for example. I recommend you do this too, and if you do, consider contributing your notes to the <a href="https://www.wwdcnotes.com/">WWDC Notes</a> community project to help other developers in the future.</p><p><strong>Labs</strong><br />If you are having problems with some Apple technology such as integrating with the system or with Xcode, or if you simply don’t understand how you could use a framework to implement a specific feature due to a lack of documentation for your use case, the <a href="https://developer.apple.com/wwdc22/labs/">Labs</a> are a great opportunity to talk to an Apple engineer and get direct feedback from the people who implemented these things and know all the nitty-gritty details. There’s also a lab for getting help with App Review and one for getting feedback for your app’s Design.</p><p>You will be able to request an appointment inside the Developer app if you’re signed in with an Apple ID that is part of the paid Apple Developer Program or if you’re a this year’s Student Challenge winner. Request early and note that:</p><blockquote><p><strong>Since availability is limited, requests will be reviewed and you’ll receive an email with your status at 10:00 p.m. PT the night before your lab.</strong></p></blockquote><p><strong>Challenges</strong><br />Apple tried something new at WWDC 2021 by providing 25 “Challenges”, on the official WWDC22 page they mention “daily coding and design challenges”, so they will see a return. Most developers seem to have missed them last year (and no, I’m not talking about the Swift Student Challenge!).</p><p>Apple makes them unnecessarily hard to explore, I couldn’t find a good overview of them online that I could link to, I could only find links for those that have an <a href="https://developer.apple.com/documentation/realitykit/wwdc21_challenge_framework_freestyle">accompanying sample project</a>, because then the download page links to a <a href="https://developer.apple.com/news/?id=zpb2xcfr&ref=fline.dev">news article</a>. The easiest way to find them all for me was by searching for “Challenge” within the developer app:</p><p><img src="/assets/images/blog/making-the-most-of-wwdc-2022/apple-2.webp" alt="" loading="lazy" /></p><p>I’m not sure if many people participated in these challenges, the Developer Forum only has <a href="https://developer.apple.com/forums/tags/wwdc21-challenges">8 threads marked with the official tag</a> from last year. But if you have enough time and are more the “learning by doing” type of person, check out this years challenges!</p><h3 id="community">Community</h3><p>What learning opportunities others in the Community offer during the week.</p><p><strong>WWDC Notes</strong><br /><a href="https://www.wwdcnotes.com/">This amazing community project</a> organized by <a href="https://www.twitter.com/zntfdr">Federico Zanetello</a> is a great resource to learn what the different sessions hold without having to watch them all. While not all sessions are covered yet, as mentioned above, if we all put our notes together there, we can easily change that this year. It also serves as an archive for WWDC content dating back as much as <a href="https://www.wwdcnotes.com/events/wwdc10/">WWDC 2010</a>. Apple only offers videos back to <a href="https://developer.apple.com/videos/all-videos/?q=WWDC+2014&ref=fline.dev">WWDC 2014</a> at the moment, but they silently remove some older videos each year and by far not all content from 2014 is available.</p><p><strong>Articles, Podcasts &amp; More</strong><br />Of course, all the iOS Dev content creators won’t just <strong>consume</strong> Apple’s content, but they will also write, talk or stream about it. I’m actually planning to live stream the whole week myself while I watch the sessions I’m interested in &amp; take notes — feel free to <a href="https://www.twitch.tv/Jeehut">join me on Twitch</a> to discuss new APIs in the chat!</p><p><a href="https://twitter.com/johnsundell">John Sundell</a> is usually covering WWDC content, both in his <a href="https://swiftbysundell.com/podcast/">podcast</a> &amp; <a href="https://swiftbysundell.com/articles/">blog</a>. <a href="https://twitter.com/twostraws">Paul Hudson</a> writes great summaries throughout the whole week <a href="https://www.hackingwithswift.com/articles">in his blog</a>. In the last two years, he also put together a nice overview of all the WWDC-related content in <a href="https://github.com/twostraws/wwdc">this repository</a>, maybe he’ll do it again this year? If not, check out <a href="https://iosdevblogs.com/">this iOS Dev feed aggregator</a> by <a href="https://twitter.com/ay8s">Andew Yates</a> based on the <a href="https://iosdevdirectory.com/">iOS Dev Directory</a>maintained by <a href="https://twitter.com/daveverwer">Dave Verwer</a> for all iOS dev content, I’m sure many of them will cover WWDC content throughout the week.</p><p><strong>Dub Dub Series</strong><br />Similar to the “Challenges” from Apple mentioned above, <a href="https://twitter.com/jordibruin">Jordi Bruin</a> recently organized a set of coding challenges called the <a href="https://www.swiftuiseries.com/">SwiftUI Series</a>. Unlike Apples challenges, these community-driven challenges had 3 judges for each topic who looked at the project and gave feedback in a livestream video. And Jordi plans to organize the same thing for June 10th, right after WWDC ended with the <a href="https://www.swiftuiseries.com/dubdubseries">Dub Dub Series</a>. Details aren’t up yet, but if it’s going to be anything like the SwiftUI Series, it’s gonna be pretty cool and focused on the <strong>new</strong> APIs this time.</p><h2 id="connecting">Connecting</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 at Apple Park</strong><br />As mentioned above, Apple is <a href="https://developer.apple.com/wwdc22/special-day/">hosting a Special Event</a> on the first day of the WWDC week at Apple Park. Submissions are closed already, so if you’re not in yet, you’re out of luck. But for those few lucky ones, they’ll be able to meet other developers in person and enjoy the keynotes together, <a href="https://twitter.com/twostraws/status/1529467321420349441?s=20&t=Nf85FLBFL-iL-pyXjDAA1g&ref=fline.dev">Apple offers lots of opportunities</a> for doing that throughout the day, including breakfast, lunch, and even guided tours within Apple Park.</p><h3 id="community">Community</h3><p><strong>WWDC22 Discord</strong><br />Active members in the community, like <a href="https://twitter.com/mikaela__caron">Mikaela Caron</a>, have created a “WWDC22” space in <a href="https://discord.com/">Discord</a> which you can join using <a href="https://discord.com/invite/6XWE2SGZ">this invite link</a>, to organize meetups around the Bay Area around the WWDC week, for example, a <a href="https://twitter.com/jordibruin/status/1526953936409833472">Sunday dinner</a> organized by <a href="https://twitter.com/jordibruin">Jordi Bruin</a>. If you don’t know Discord, it’s pretty much the same as Slack, but with more of gaming &amp; audio-call-focused history. This is why the Discord space could also be used for discussions pretty well! Consider checking the Discord during WWDC to meet developers &amp; discuss new APIs! The Discord space had ~300 members at the time of this writing.</p><p><strong>iOS Developers Slack</strong><br />Already some time ago, the community had started a <a href="https://slack.com/">Slack</a> space for iOS developers to stay in touch with each other, which you can join through <a href="https://ios-developers.io/">this website</a>. With over 22k members, I’m sure there’ll be many people around in the dedicated <code>#wwdc</code> channel to discuss the latest APIs throughout the week.</p><p><strong>WWDC Community Week</strong><br /><a href="https://wwdc.community/">This dedicated website</a> tries to bring together the community during WWDC week by organizing and listing <a href="https://wwdcwatch.party/">Keynote Watch Parties</a> and other events such as Twitter “Spaces” (live, interactive audio-discussions) like the <a href="https://twitter.com/stefanjblos/status/1529475899736965128">Mega-Pre-WWDC Twitter Space</a> before WWDC or the <a href="https://twitter.com/iosdevhappyhour/status/1529920449500434432">iOS Dev Happy Hour</a> during the week. They also organize get-togethers (in-person and online), a community hackathon, and collect memorable moments from the community on a mural. They also just introduced their own Discord server, you can <a href="https://discord.com/invite/3P94atxcV5">join it here</a>.</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="discussing">Discussing</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><br />Like <a href="https://developer.apple.com/news/?id=a5aw05t9&ref=fline.dev">last year</a>, Apple will offer <a href="https://developer.apple.com/wwdc22/digital-lounges/">Digital Lounges</a> again this year, which were basically controlled Slack channels that are open only at certain times and which you need to register for up-front — registration opens May 31st and requires Apple Developer Membership / Student Challenge Winnership.</p><p><strong>Forums</strong><br />The <a href="https://developers.apple.com/forums/">Apple Developer Forums</a> will also get <a href="https://developer.apple.com/wwdc22/forums/">4 dedicated tags</a> to discuss new APIs and ask questions about them with a chance to get answered by Apple Engineers directly. While I prefer the <a href="https://www.discourse.org/">forum technology</a> Apple uses on the <a href="https://forums.swift.org/">Swift Forums</a>over this custom implementation, some of the more tricky questions get only answered here, so it can be a lifesaver sometimes!</p><h3 id="community">Community</h3><p>Of course, you can discuss new APIs in one of the Discord servers or the Slack server mentioned above for “Connecting”. Here are some more options:</p><p><strong>Dub Dub Together</strong><br /><a href="https://wwdctogether.com/">This website</a> created by <a href="https://twitter.com/onmyway133">Khoa</a> is a place you can watch both Keynotes and chat with other developers live about it in one screen. While you could in theory also watch the first keynote on YouTube and chat there, you can’t do this for the developer keynote and you’ll find many non-developers wiring in the chat, too. So definitely worth considering!</p><p><strong>Livestreams</strong><br />Some known developer sites like <a href="https://www.raywenderlich.com/34291068-don-t-miss-our-wwdc-2022-livecast-june-6-9pm-edt">RayWenderlich</a> will be live streaming the event and discussing the APIs as they are presented. I already mentioned that I’ll be streaming, too, and you might find other <a href="https://iosdevdirectory.com/#twitch-en">Twitch streamers</a> doing the same, I’ve even contacted some to discuss APIs together in our streams. Please note that Apple doesn’t allow to re-distribute the Keynote or Sessions, so you’ll have to open Apple’s content on a second device to follow, just a heads up.</p><p>I hope that all this information will help you have an amazing WWDC 2022. Let’s hope that all <a href="https://www.fline.dev/my-top-3-wishes-for-wwdc-2022/">our wishes</a> come true!</p><blockquote><p>💁🏻‍♂️ <strong>Enjoyed this article? Check out my app <strong>RemafoX</strong>!</strong>
A native Mac app that integrates with Xcode to help translate <strong>your</strong> app.
<a href="https://apps.apple.com/app/apple-store/id1605635026?pt=549314&ct=fline.dev&mt=8&ref=fline.dev"><strong>Get it now</strong></a> to save time during development &amp; make localization easy.</p></blockquote>]]></content:encoded>
</item>
<item>
<title>Streaming Open Source Development on Twitch — Part 2</title>
<link>https://fline.dev/blog/streaming-open-source-development-on-twitch-part-2/</link>
<guid isPermaLink="true">https://fline.dev/blog/streaming-open-source-development-on-twitch-part-2/</guid>
<pubDate>Fri, 15 Apr 2022 00:00:00 +0000</pubDate>
<description><![CDATA[My Software Setup & used 3rd Party Services.]]></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">Keyboard Shortcuts Tool</a><br />**3rd Party Services: **<a href="https://streamelements.com/">Community Building</a>, <a href="https://rogueamoeba.com/loopback/">Audio Routing</a>, <a href="https://www.pretzel.rocks/">Background Music</a></p><h2 id="software-setup">Software Setup</h2><p>Check out <a href="https://jeehut.medium.com/streaming-open-source-development-on-twitch-part-1-1926b9b7e051?sk=c828dafc91b82fc902819cb69d447cd7&ref=fline.dev">the first part</a> of this series to learn about which hardware I’m using. But here’s the software side of things, the most important streaming-related apps including price, my settings, and some alternatives to consider.</p><h3 id="broadcasting-software-selive-obs-with-plugins">Broadcasting Software: <a href="https://streamelements.com/obslive">SE.Live (OBS with 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>ℹ Description:</strong><br />The heart of all streams is the braodcasting software. This is what produces the live video &amp; sends it to Twitch (or other platforms like YouTube) as a combination of many different sources. I use it to capture my screen, my webcam, apply a green screen filter, capture my microphone audio, run filters on the audio, run background music, show stream-related alerts like follows, provide sound alerts, switch between different scenes e.g. when I start/stop the stream or have a break.</p><p>This is quite a lot of things in one software, so it’s really important to choose the right tool here. The good news is, there’s an open-source project for this named “Open Broadcaster Software”, short “OBS” with <a href="https://github.com/obsproject/obs-studio">37k starts on GitHub</a> with lots of features that other companies can rely on. I use the StreamElements variant because it’s very close to the original OBS Studio with some extra plugins for community building purposes.</p><p><strong>💰 Price:</strong> Free (the OBS base is fully <a href="https://github.com/obsproject/obs-studio">Open Source</a>, the plugins are not)</p><p><strong>⚙️ My Settings:</strong><br />I basically set up two types of scenes: When I’m not visible and when I am. For the latter, I downloaded a nice background video from <a href="https://pixabay.com/videos/">Pixabay</a> and added a dim layer on top of it so the screen feels alive even if I’m not there. I’ve created 3 scenes (before/break/after) with basically the same setup but different 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>For when I’m streaming, I also have 3 scenes: One with a Pomodoro timer (currently using the <a href="https://apps.apple.com/us/app/flow-focus-pomodoro-timer/id1423210932">Flow</a> app, but gonna use my own <a href="https://github.com/FlineDevPublic/OpenFocusTimer">Open Focus Timer</a> once it’s ready), one without a timer (e.g. at end of stream) and one with screen censored (e.g. when I need to sign in/up somewhere). After many tries, I decided to place myself at the bottom right of the screen (but not the edge):</p><p><img src="/assets/images/blog/streaming-open-source-development-on-twitch-part-2/juan-gomez-3.webp" alt="" loading="lazy" /></p><p>For my webcam, I have set up a “<a href="https://obsproject.com/cs/kb/chroma-key-filter">Chroma Key</a>” filter for the green screen:</p><p><img src="/assets/images/blog/streaming-open-source-development-on-twitch-part-2/juan-gomez.webp" alt="" loading="lazy" /></p><p>My audio settings are a little more advanced. First, my mic settings. I’m using 4 filters for my mic which seem to be all recommended by audio pros:</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>” helps filter out background noise such as loud fans.</p><p><img src="/assets/images/blog/streaming-open-source-development-on-twitch-part-2/juan-gomez-5.webp" alt="" loading="lazy" /></p><p>With “<a href="https://obsproject.com/cs/kb/noise-gate-filter">Noise Gate</a>” I can completely “turn off the mic” if a specified loudness threshold isn’t met. This helps remove keyboard typing when I’m not talking.</p><p><img src="/assets/images/blog/streaming-open-source-development-on-twitch-part-2/juan-gomez-6.webp" alt="" loading="lazy" /></p><p>Sometimes people can get very loud when they are surprised by something. Imagine I’m visiting a website and there’s a jump scare. To not bother my viewers with a sudden loud noise from me, I’ve setup a “<a href="https://obsproject.com/ko/kb/compressor-filter">Compressor</a>” to “compress” any loudness above a given treshold by a factor of 10.</p><p><img src="/assets/images/blog/streaming-open-source-development-on-twitch-part-2/juan-gomez-4.webp" alt="" loading="lazy" /></p><p>Lastly, the “<a href="https://obsproject.com/fr/kb/limiter-filter">Limiter</a>” does a very similar thing to a compressor, but instead of “compressing” the extra audio by a customizable factor, it simply always uses the factor “infinity” and therefore limits the loudness to a given max.</p><p>Let’s move on to other audio settings. Two things should be noted here: First, there’s a concept called a “monitor”. A monitor is the audio you as the streamer will hear back. For example, if you turn the “monitor” on for your mic, you will hear yourself on your headphones. I have it turned on for everything except the mic as it feels weird to hear myself (with some delay). Second, for each audio source, you can specify an audio track number. This is useful if you plan to edit your videos later, e.g. to produce more narrowed-down YouTube videos (like I plan to do). For example, I’m putting my voice on its own track, the background music on another one, and all sound alerts on yet another track. This way I can cut the video source and keep parts of my voice without having the background music “jump”. And I can remove alert sounds entirely or change their volume independently if I need to. See here:</p><p><img src="/assets/images/blog/streaming-open-source-development-on-twitch-part-2/juan-gomez-8.webp" alt="" loading="lazy" /></p><p>Additionally, I set up all my streams to be recorded to a <code>.mkv</code> file automatically with all the audio tracks from 1 to 5. See here:</p><p><img src="/assets/images/blog/streaming-open-source-development-on-twitch-part-2/juan-gomez-2.webp" alt="" loading="lazy" /></p><p>That’s a selection of my OBS settings, but not all. There’s so much you can do with OBS that it can be intimidating. For example, I skipped <a href="https://obsproject.com/eu/kb/browser-source">Browser sources</a>.</p><p><strong>⎇ Alternatives:</strong><br />– <a href="https://streamlabs.com/">Streamlabs Desktop</a>: Free (<a href="https://github.com/stream-labs/desktop">Open Source</a>), custom OBS UI (matter of taste)<br />– <a href="https://www.ecamm.com/mac/ecammlive/">Ecamm Live</a>: $400 per year, native Mac app, easy but limited flexibility</p><h3 id="keyboard-shortcuts-tool-keycastr">Keyboard Shortcuts 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="3rd-party-services">3rd Party Services</h2><p>These are the 3rd party services I’m using and every streamer should consider.</p><h3 id="community-building-streamelements">Community Building: <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>ℹ Description:</strong><br />What makes streaming really fun is the interaction with your viewership. While Twitch as the streaming platform provides a built-in chat, there are so many more ways to interact with your community. For example, you might want to show in your stream an alert when somebody follows you to give the viewer the feeling of truly being part of the stream. Or you want to celebrate with an on-screen animation when another streamer raids you. And you probably have some reminders like links to your socials you want to share with your always changing viewership. These and more features are provided by third-party services which I highly recommend you use at least one of to stay connected with your viewers and build a community of fans over time.</p><p><strong>💰 Price:</strong> Free (they take a cut if you use their <a href="https://streamelements.com/merch">Merch Store</a> or <a href="https://blog.streamelements.com/series-a-and-brand-partnerships-3f08f2b7c314">Partnerships</a>)</p><p><strong>⚙️ My Settings:</strong><br />StreamElements provides many features, but I’m using mostly 2 of them:</p><p>First, I’m using their Alerts &amp; Overlays Feature with a custom overlay. The two most important layers I’ve setup are the Alert Box and the Kappagen ones:</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>ℹ Description:</strong><br />Apple is securing macOS on many levels. One of them is the protection of system audio: By default, there’s no way to fetch audio output from other apps into OBS so your viewers can hear the same as you do. This might be useful e.g. if you want to play background music in a dedicated music app or watch a tutorial video and comment it live. There is a way to turn off some security features and give apps access to system audio, but OBS doesn’t support that (yet). So you’d need extra software that routes audio from other apps or your system into a fake “virtual” microphone, which you can then add to OBS as a source like any other mic. You can find a nice detailed slideshow for how to set up the tool on both Intel &amp; M Chip Macs <a href="https://rogueamoeba.com/support/knowledgebase/?showArticle=ACE-StepByStep&product=Loopback&ref=fline.dev">here</a>.</p><p><strong>💰 Price:</strong> $99 (one-time payment)</p><p><strong>⚙️ My Settings:</strong><br />The most important setting here is the “Video Content” device because it provides me the audio from videos I may play using QuickTime or Safari.</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>⎇ Alternatives:</strong><br />– <a href="https://github.com/ExistentialAudio/BlackHole">Blackhole</a>: Free (Open Source) — haven’t tried it, but looks very similar</p><h3 id="background-music-pretzel">Background Music: <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>ℹ Description:</strong><br />A coding session without some relaxing background music is very tiring to me. I think it is even more important to have some good background music if you are live streaming, as you’re probably not going to talk the whole time and the pauses can feel weird as a viewer if there’s no noise at all. At the same time, licensing music is a complicated topic and you are not allowed to stream everything on Twitch. In my case, I am also uploading my streams to YouTube and the rules are different between these platforms. Generally speaking, more is allowed on Twitch, and even more is allowed if you turn off the “video-on-demand” feature on Twitch, so viewers can only listen to the music while you’re live and there’s no way to watch it later.</p><p>But if you do want to provide recordings of your streams long-term, you have to find some “royalty-free” music or at least have a license for the music you use, otherwise, you might get a <a href="https://www.dmca.com/FAQ/What-is-DMCA">DMCA</a> takedown of (parts of) your videos. If you do this repeatedly on either YouTube or Twitch, your channel might even get banned. Don’t risk it and make sure you conform to the rules.</p><p><strong>💰 Price:</strong> Freemium ($15/month to remove chatbot posting every song)</p><p><strong>⚙️ My Settings:</strong><br />I always have “YouTube Safe” turned on. I used to have “Instrumental” turned on and play the stations “LoFi” and “Chill” a lot. But lately, I have started to listen to the “Rock” station as well, and with that filter, there were no songs, so I keep the “Instrumental” filter off now.</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>My preferred Pretzel settings.</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 and Rock are part of my favorite stations.</em></p><p><strong>⎇ Alternatives:</strong><br />– <a href="https://www.monstercat.com/gold">Monstercat Gold</a>: $7.50/month, supports music download<br />– Playlists on Apple Music / Spotify / Amazon Music, e.g. from <a href="https://anjunabeats.lnk.to/Twitch">Anjunabeats</a><br />– Here’s a <a href="https://streamermusic.com/dmca-safe-music/">list of more alternatives</a></p><h2 id="next-up">Next Up</h2><p>This wraps up the software I use &amp; how I have them configured — it should get you another step closer to your own live streams. In part 3 of this series, I will be covering my system settings and some other tips you should keep in mind while streaming software development on Twitch. Follow me to not miss!</p><blockquote><p>💁🏻‍♂️ <strong>Enjoyed this article? Check out my app RemafoX!</strong>
A native Mac app that integrates with Xcode to help translate <strong>your</strong> app.
<a href="https://apps.apple.com/app/apple-store/id1605635026?pt=549314&ct=fline.dev&mt=8&ref=fline.dev"><strong>Get it now</strong></a> to save time during development &amp; make localization easy.</p></blockquote>]]></content:encoded>
</item>
<item>
<title>My Top 3 Wishes for WWDC 2022</title>
<link>https://fline.dev/blog/my-top-3-wishes-for-wwdc-2022/</link>
<guid isPermaLink="true">https://fline.dev/blog/my-top-3-wishes-for-wwdc-2022/</guid>
<pubDate>Wed, 06 Apr 2022 00:00:00 +0000</pubDate>
<description><![CDATA[With Apple announcing WWDC week for June 6–10 this year, let’s dive into what new frameworks, APIs, and tools I hope to see unveiled and what using them might feel like with examples.]]></description>
<content:encoded><![CDATA[<blockquote><p>✨ 2023 Update: I’ve added 3 more wishes in <a href="https://www.fline.dev/my-top-5-wishes-for-wwdc-2023/">this successor article</a>.</p></blockquote><p>Each year, there’s this very special time, when a specific group of people is making wishes and getting their hopes up for all sorts of things. Some <a href="https://github.com/bhansmeyer/WWDC-2021-Community-Wishlist">share their wishes </a>with others, some just keep them secret in their heads to not get too disappointed if not fulfilled. I used to be in the latter group, but this time I’d like to share my wishes to improve the probability of them coming true — if not this year, then maybe the next. After all, Santa might be listening.</p><p>I’m going to skip any of the obvious topics that are on top of almost <em>every</em> iOS developer’s list, such as a more stable Xcode, a more bug-free Swift, a more complete SwiftUI, or more reliable SwiftUI Previews. Let’s get started!</p><h2 id="3-importing-app-icons-in-all-sizes-from-one-image">#3: Importing App Icons in all sizes from One Image</h2><h3 id="problem">Problem</h3><p>Every app is required to have an app icon. Xcode requires us to provide the app icon in dozens of different sizes though, without any support to resize them. While there are many apps &amp; tools to help get there, only a few of them support the latest set of sizes as Apple likes to add new sizes over time. This is an unnecessary obstacle for new developers just starting.</p><p><img src="/assets/images/blog/my-top-3-wishes-for-wwdc-2022/the-temporary-app-icon.webp" alt="The temporary App Icon set of my Open Focus Timer app" loading="lazy" /></p><p>But they are all generated as <code>Optional</code> types in Swift code anyhow. This and the whole <code>NSManagedObjectContext</code> API design feels quite outdated and not very “<a href="https://www.swift.org/about/">Swifty</a>” (as in “not safe”). It’s time for something new!</p><h3 id="solution">Solution</h3><p>Apple could introduce a new Swift-only framework (like <code>SwiftUI</code>) named something like <code>SwiftData</code> which provides a high-level API to define and manage persistable models. Defining a model could look something like this:</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>For models to store in iCloud, you’d need to prepend <code>distributed</code> in front of <code>actor</code>. Normally accessing any property of an <code>actor</code> would require the <code>await</code>keyword, but some magic property wrappers could simplify this to:</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>And writing to an <code>actor</code> property isn’t possible from outside, but the <code>@Persisted</code>property wrapper might have some <code>Binding</code> magic to allow this:</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>I have to admit, I have not used Actors in practice yet, so forgive me if some of the above examples don’t make any sense. But I have a feeling that Actors could play an important role in a <code>SwiftData</code> framework for safe access.</p><p>Additionally, Xcode could come with a UI that makes it easy to version data models and provide a graphical migration tool that could be written in a declarative Swift syntax and be previewed as a UML diagram on the right (like SwiftUI previews). But maybe I started dreaming too big here …</p><h3 id="probability">Probability</h3><p>Many were expecting this already for the last two years because it’s a logical next step after SwiftUI. But this year, with <a href="https://github.com/apple/swift-evolution/blob/main/proposals/0306-actors.md">Actors</a> already shipped in Swift 5.5 and <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> (for iCloud support) being accepted just recently as well, the technology might just be ready to ship the first version by September.</p><h2 id="conclusion">Conclusion</h2><p>There are lots of things Apple could announce in June, and the above are just my personal wishes. But in the past, I was always surprised by at least one or two frameworks entirely, like <a href="https://developer.apple.com/videos/play/wwdc2019/216/">SwiftUI</a> in 2019, <a href="https://developer.apple.com/videos/play/wwdc2020/10028/">WidgetKit</a> in 2020, and <a href="https://developer.apple.com/videos/play/wwdc2021/10166/">DocC</a> in 2021. What will it be this year? I can’t wait to find out!</p>]]></content:encoded>
</item>
<item>
<title>Streaming Open Source Development on Twitch — Part 1</title>
<link>https://fline.dev/blog/streaming-open-source-development-on-twitch-part-1/</link>
<guid isPermaLink="true">https://fline.dev/blog/streaming-open-source-development-on-twitch-part-1/</guid>
<pubDate>Thu, 31 Mar 2022 00:00:00 +0000</pubDate>
<description><![CDATA[My Streaming Motivation & Hardware Setup with Reviews.]]></description>
<content:encoded><![CDATA[<h2 id="tldr">TL;DR</h2><p><strong>Motivation:</strong> Sharing with/Giving back to Community, Time Efficiency<br /><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/">Mic</a>, <a href="https://www.amazon.de/gp/product/B088WTSFS9/?language=en_US&ref=fline.dev">Light</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>**Price: **~$2,700</p><p><strong>👍 Pros:</strong><br />– M1 Pro builds 40% faster than M1, only 2% slower than M1 Max (<a href="https://github.com/devMEremenko/XcodeBenchmark#xcode-130-or-above">source</a>)<br />– Double the battery life of Intel MacBooks (also <a href="https://www.reddit.com/r/macbookpro/comments/qogsov/battery_life_comparison_between_m1_pro_and_m1_max/">~25 % longer than M1 Max</a>)<br />– 1TB has plenty of space for multiple Xcodes, iOS simulators, 1080p streams</p><p><strong>👎 Cons:</strong><br />– More than double the price of an <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> with 16GB+1TB → the 40% reduced build time combined with the great monitor &amp; mobility is worth it<br />– No headroom with only 16GB of RAM (the 32GB option costs extra $400!)</p><p><strong>Recommended?</strong> **💁‍♂️ **Oh yes. <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> builds yet 37% faster though, consider!</p><p><strong>⎇ Alternatives:</strong><br />– <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> with 16GB RAM + 1TB (<del>$1,300)<br />– <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> with 16 GB RAM + 1 TB (</del>$1,650)<br />– <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> with 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>Price:</strong> <del>300€ (</del>$335) — Note: I bought them <strong>second-hand</strong>.</p><p><strong>Pros: 👍</strong><br />– Easy to connect to Apple devices<br />– Very long battery life (I hardly have to charge)<br />– Comfortable even after hours of stream</p><p><strong>Cons: 👎</strong><br />– Expensive when bought new (<del>$550)<br />– Cable-mode requires extra <a href="https://www.apple.com/shop/product/MR2C2AM/A/lightning-to-35mm-audio-cable-12m">Lightning-to-3.55mm cable</a> (</del>$35)</p><p>**Recommended? 💁‍♂️ **If you own more Apple devices, then: Yes. Else: No.</p><p><strong>⎇ Alternatives:</strong><br />– <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> (<del>$90), good quality detachable mic<br />– <a href="https://hyperx.com/products/hyperx-cloud-ii">HyperX Cloud II</a> (</del>$70), also very popular with gamers<br />– Any headphones you already own (if you get an external mic anyways)</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>**Price: **~$70</p><p><strong>Pros: 👍</strong><br />– Reliable (I see cams in streams turning off randomly, never happened to me)<br />– Good 1080p quality &amp; natural colors<br />– Privacy shutter (after all Edward Snowden <a href="https://www.imdb.com/title/tt4044364/">told us</a> the NSA is watching 👀)<br />– Auto-focus &amp; auto-lighting that can be configured via Logitech software</p><p><strong>Cons: 👎</strong><br />– Built-in mic that can’t be turned off physically (NSA, remember?)</p><p>**Recommended? 💁‍♂️ **Yes!</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>Price:</strong> ~$120</p><p><strong>Pros: 👍</strong><br />– Easy &amp; fast to set up &amp; put away<br />– Stable stand<br />– Good height</p><p><strong>Cons: 👎</strong><br />– Could be even a bit wider (I have to cut the sides of my webcam image)</p><p>**Recommended? 💁‍♂️ **Yes, really convenient if you need a green screen.</p><p><strong>⎇ Alternatives:</strong><br />– Setup a nice looking background for your workspace if you have the space</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="mic-blue-yeti">Mic: <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>Price:</strong> ~$100</p><p><strong>Pros: 👍</strong><br />– Flexible modes that also allow for interviews &amp; more<br />– Great sound out of the box without any filters applied<br />– Logitech software ships with multiple filter presets</p><p><strong>Cons: 👎</strong><br />– Default stand not well enough shielded against vibrations (keyboard)<br />– Clicking mute button creates too loud noise to use during stream</p><p>**Recommended? 💁‍♂️ **Yes, this is actually recommended by many for starters.</p><p><strong>⎇ Alternatives:</strong><br />– <a href="https://www.hyperxgaming.com/us/microphone/quadcast-gaming-microphone">HyperX Quadcast</a>, convenient tap-to-mute sensor (<del>$110)<br />– <a href="https://rode.com/en/microphones/usb/nt-usb">Rode NT-USB</a>, ships with a pop shield (</del>$170)<br />– If you have a good quality headset mic, use that instead (e.g. <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="light-lavkow-10-inches-rgb-selfie-ring-light-us-alternat">Light: <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 alternat</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>Price:</strong> ~$30</p><p><strong>Pros: 👍</strong><br />– Cheap<br />– Supports different colors</p><p><strong>Cons: 👎</strong><br />– Not very bright, but good enough for streaming when sitting close</p><p>**Recommended? 💁‍♂️ **To save money, get a selfie light, Yes. For quality: No.</p><p><strong>⎇ Alternatives:</strong><br />– <a href="https://www.elgato.com/en/key-light">Elgato Key Light</a> (~$200)</p><blockquote><p><strong>See also <strong>Twitch’s <strong><a href="https://www.twitch.tv/creatorcamp/en/setting-up-your-stream/hardware-recommendations/"><strong>hardware recommendations</strong></a></strong> page</strong> for more alternatives.</strong></p></blockquote><h2 id="other-periphery-i-use">Other Periphery I Use</h2><p>Not exactly streaming related, but if you’re interested in what else I have on my desk while streaming, here’s a quick commented list:</p><ul><li><p><a href="https://www.amazon.com/Apple-Wireless-Keyboard-Silver-MLA22LL/dp/B01NABDNPH/">Apple Wireless Magic Keyboard 2</a> ($65):<br />I’d prefer <a href="https://www.logitech.com/en-us/products/keyboards/k860-split-ergonomic.920-009166.html">Logitech ERGO K860</a> now.</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):<br />A savior for my wrist, wouldn’t wanna go back.</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):<br />Only one cable to connect, like a dock.</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):<br />Nice build quality, very stable &amp; grippy. 👍</p></li><li><p><a href="https://www.lamicall.com/product/cell-phone-stand-for-desk-s1/">Lamicall Phone Stand S1</a> ($10):<br />Good enough, hole for cable. Does its job.</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):<br />Size of 90cm x 40cm is perfect for me.</p></li></ul><p>That’s it for my motivation &amp; my hardware setup. I hope this helps future streamers to get started. The <a href="https://www.fline.dev/streaming-open-source-development-on-twitch-part-2/">next part</a> covers the software side which I think is even more interesting: All tools I use and how I have them and my hardware set up to work together. Make sure to follow me to not miss it!</p><blockquote><p>💁🏻‍♂️ <strong>Enjoyed this article? Check out my app <strong>RemafoX</strong>!</strong>
A native Mac app that integrates with Xcode to help translate <strong>your</strong> app.
<a href="https://apps.apple.com/app/apple-store/id1605635026?pt=549314&ct=fline.dev&mt=8&ref=fline.dev"><strong>Get it now</strong></a> to save time during development &amp; make localization easy.</p></blockquote>]]></content:encoded>
</item>
<item>
<title>SwiftPM + CoreData: Failing SwiftUI Previews? Here Are 5 Tips to Fix</title>
<link>https://fline.dev/blog/swiftpm-coredata-failing-swiftui-previews-here-are-5-tips-to-fix/</link>
<guid isPermaLink="true">https://fline.dev/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[Fixing Xcode bugs that make SwiftUI previews fail in apps modularized with SwiftPM and that are using CoreData.]]></description>
<content:encoded><![CDATA[<p>My SwiftUI previews didn’t work properly since the day I had set up the project for the <a href="https://github.com/FlineDevPublic/OpenFocusTimer">Open Focus Timer</a> in Xcode using Point-Free’s <a href="https://www.pointfree.co/episodes/ep171-modularization-part-1">modularization approach</a> — with the CoreData checkbox enabled to get a good starting point for my model layer. This was quite annoying, after all getting faster builds and therefore more reliable SwiftUI previews was one of the main reasons I had opted to modularize my app into small chunks in the first place.</p><p>So in <a href="https://youtu.be/OMhzx3zdrJw?t=6415&ref=fline.dev">one of my streams</a> (this is an open-source app I am developing fully in the open while <a href="https://www.twitch.tv/Jeehut">streaming live on Twitch</a>) I decided to tackle this problem and fix the SwiftUI preview error once and for all. And I failed:</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>Thanks to some help from the great Swift community on Twitter, I could figure out the root cause of the issue: <strong>SwiftUI previews get into trouble when CoreData models are referenced in them</strong>.</p><p>But while I thought that it’s just a path issue that can be fixed with a simple workaround, it was not as simple as that. Yes, there is a path issue involved, but while solving the previews, I came across multiple levels of failure. And I learned how to debug SwiftUI previews along the way. Let me share my learnings…</p><h3 id="1-explicit-dependencies-in-package-manifest">#1: Explicit Dependencies in Package Manifest</h3><p>First things first. Using Point-Free’s modularization approach means you’ll have a <code>Package.swift</code> file to manage manually. For each module, you’ll add a <code>target</code>, a <code>testTarget</code> and a <code>library</code> entry and for each target, you’ll need to specify the dependencies. Xcode does not help here in any way other than recognizing the changes you make in that file. With many packages, the manifest file can grow significantly, and there’s currently no help I’m aware of to make this easier. This is what my manifest looks like right now:</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>The problem with managing this file manually isn’t just the manual work. Xcode seems to behave inconsistently regarding the dependencies: When you do a normal build targeting the Simulator for example, a dependency of a dependency seems to get automatically linked to your target. So if my <code>TimerFeature</code> is importing <code>Utility</code> for example, but it’s not listed as a dependency under the <code>TimerFeature</code> target, Xcode might still be able to compile without errors if another dependency, e.g. <code>Model</code> also depends on <code>Utility</code> so Xcode can indirectly access <code>Utility</code>inside of <code>TimerFeature</code> because <code>TimerFeature</code> is listing <code>Model</code> as its dependency.</p><p>While this sounds very useful, it can become quite frustrating because SwiftUI previews work differently. For them, as far as I can tell, this transitive kind of implicit imports don’t work. The same seems to be true for running tests as well (at least sometimes). In other words: It’s important to always double-check the <code>dependencies</code> for each target and not to forget to add every <code>import</code> you make in a target to the related target in your <code>Package.swift</code> manifest file.</p><p>Maybe, someone will write a tool to help make this easier in the future. 🤞</p><h2 id="2-generated-code-not-reliably-picked-up-by-xcode">#2: Generated Code not reliably picked up by Xcode</h2><p>Another issue I had come across was that even when my builds succeeded, Xcode would (after showing me the “Build succeeded” dialog) show an error in the editor within <code>PreviewProvider</code> stating it can’t find <code>FocusTimer</code>:</p><p><img src="/assets/images/blog/swiftpm-coredata-failing-swiftui-previews-here-are-5-tips-to-fix/error-stating-focustimer.webp" alt="Error stating <code>FocusTimer</code> can’t be found in scope despite <code>import Model</code> &amp; successful build." loading="lazy" /></p><p>Note that you will need to delete and re-create these generated files each time you make a change to the model (which you should do rarely anyways to prevent database migration problems). Additionally, select the model in Xcode and set <code>Codegen</code> to <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="Clicking on “Diagnostics” just stating “MessageError: Connection interrupted”." loading="lazy" /></p><p>With this done, the editor no longer shows an error.</p><h3 id="3-swiftui-diagnostics-swiftui-crash-reports">#3: SwiftUI Diagnostics != SwiftUI Crash Reports</h3><p>Here’s a learning for those (like me) wondering how to make use of errors like this after pressing the <code>Diagnostics</code>button when SwiftUI previews fail:</p><p><img src="/assets/images/blog/swiftpm-coredata-failing-swiftui-previews-here-are-5-tips-to-fix/clicking-on-diagnostics-3.webp" alt="Clicking on “Diagnostics” just stating “MessageError: Connection interrupted”." loading="lazy" /></p><p>Viewing the contents of the highlighted folder will reveal many files that hold different kinds of details about the SwiftUI preview build. The most useful file for debugging lies inside the folder <code>CrashLogs</code> where you can find one or multiple <code>.ips</code> files that we can easily open in Xcode via a double-click:</p><p><img src="/assets/images/blog/swiftpm-coredata-failing-swiftui-previews-here-are-5-tips-to-fix/contents-of-xcode.webp" alt="Contents of Xcode Previews .ips file with a proper error console output." loading="lazy" /></p><p>Thankfully, here the aforementioned <a href="https://twitter.com/NiklasBuelow/status/1499160862220857349?s=20&ref=fline.dev">pointer of a kind developer</a> in the Swift community on Twitter helped, which pointed me to a thread with <a href="https://stackoverflow.com/a/65789298/3451975">this answer</a> on StackOverflow.</p><p>It’s basically saying that there’s currently a bug in Xcode (or SwiftPM?) which makes <code>Bundle.module</code> point to the wrong path in SwiftUI previews. To fix it, they are suggesting to add a <code>Bundle</code> extension with a custom search. Here’s the full code slightly adjusted to fit my coding &amp; commenting style:</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>When copy and pasting this code, make sure to adjust the <code>packageName</code> and <code>targetName</code> variables to your package &amp; target names accordingly.</em></p></blockquote><p>Note that I wrapped the workaround into an <code>#if DEBUG</code> to ensure my production code does not accidentally use this path search and instead relies on the official <code>Bundle.module</code>. Also, I removed the <code>fatalError</code> from the workaround code found on StackOverflow, so in case it can’t find a Bundle in the custom search paths it doesn’t fail but instead I <code>return Bundle.module</code> as a fallback. This is supposed to make the code more resilient and continue to work even when this bug gets fixed in a future Xcode release but the custom search paths may no longer work.</p><p>Now, the last change I had to make in the <code>PersistenceController</code> was to replace the call to <code>Bundle.module</code> with a call to the new <code>Bundle.swiftUIPreviewsCompatibleModule</code>:</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>And finally, my SwiftUI previews started working again!</p>]]></content:encoded>
</item>
<item>
<title>Multi Selector in SwiftUI</title>
<link>https://fline.dev/blog/multi-selector-in-swiftui/</link>
<guid isPermaLink="true">https://fline.dev/blog/multi-selector-in-swiftui/</guid>
<pubDate>Thu, 03 Mar 2022 00:00:00 +0000</pubDate>
<description><![CDATA[Adding a missing SwiftUI component for prototyping purposes.]]></description>
<content:encoded><![CDATA[<p>While developing my first serious app using SwiftUI I was continuously impressed about how fast UI development had become using SwiftUI, especially if the pre-provided views already support your use case. And while of course for any kind of custom UI we will still need to write our custom views, combining the existing ones and adjusting them with modifiers and such, I would expect SwiftUI to support at least the most common views that developers might need to present data and accept input from users.</p><p>If this were the case, SwiftUI could even be used for prototyping where a “working but not beautiful” version of an app idea could be quickly built and shown to users to verify if the app idea has any chances of success. Also, this way one could also quickly gather feedback about which parts actually need a much better understandable UI (the parts not yet understood well) and which could be mostly kept to the default components with some visual adjustments.</p><p>In other words: SwiftUI in my eyes has the potential to make <a href="https://en.wikipedia.org/wiki/Minimum_viable_product">MVP</a>-driven product development much more interesting to many more developers which is definitely a good thing as it saves a lot of time that would otherwise be invested in things that would eventually turn out to fail in one way or another. This goes in line with the <a href="https://en.wikipedia.org/wiki/Lean_startup">Lean Startup methodology</a>which I think is a great way to tackle any kind of new product.</p><h3 id="the-current-state-of-swiftui">The Current State of SwiftUI</h3><p>For this to be possible, I would expect SwiftUI to already cover all common types of input that might be needed in forms, like for user registration or other kinds of data, as many types of apps, in the end, are nothing else than a form that accepts input data, transforms it in some way and presents data back in a special way or time. Unfortunately, SwiftUI isn’t quite there yet.</p><p>The approach Apple seems to be taking with SwiftUI is to consider which are the most missing components in SwiftUI and adding some of them each year. For example, at WWDC 2020 they added <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> support within <code>Text</code> and improved a lot of <a href="https://developer.apple.com/videos/play/wwdc2020/10041/">other details of existing views</a>, both for performance and more flexibility. At WWDC 2021 they’ve added multiple <code>async/await</code> related APIs, such as <a href="https://developer.apple.com/documentation/swiftui/asyncimage">AsyncImage</a> or the <a href="https://developer.apple.com/documentation/swiftui/label/refreshable%28action:%29">.refreshable</a> and <a href="https://developer.apple.com/documentation/swiftui/circle/task%28priority:_:%29">.task</a> view modifiers, amongst <a href="https://medium.com/@anithawritings/whats-new-in-swiftui-wwdc2021-aadbdd8d34de">other improvements &amp; additions</a>.</p><p>The upside of that approach is, once something is added to the framework, one can expect it to exist and work in the same manner for a long time, so no big code changes are needed with every release (like it was for Swift as a language before Swift 4). The downside is that many components are still missing. And that’s where I think the community can jump in to provide temporary solutions that can be easily replaced by official components provided by Apple sometime in the future.</p><h3 id="implementing-a-multi-selection-view-component">Implementing a Multi-Selection View Component</h3><p>In this post, I would like to focus on one such component and provide my initial solution for it: A multi-selector to choose multiple options out of a given set of options. As of now Apple does provide a <a href="https://developer.apple.com/documentation/swiftui/picker">Picker</a>, but it doesn’t support the selection of multiple entries and even automatically leaves the list screen once a single choice was made. So let’s get right to it and fix that!</p><p>What kind of data structure could require a multi-selector? Let’s have a look at this example:</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>So basically, in our app we have a collection of goals and a collection of tasks. And we want to model the relation describing which goals each task serves. When creating or editing a <code>Task</code> we want to select which goals the task is serving. Here’s the SwiftUI code for a <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>The above code renders to this preview:</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>To show off, how things would work if we only had one goal to serve, we could simply replace our TODO <code>Text</code> entry with a <code>Picker</code> like this:</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>This is what the <code>TaskEditView</code> now looks like:</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>And when clicking the picker, this is the detail view:</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>Pretty straight-forward. Note that <code>Goal</code> needs to be <code>Identifiable</code> for this to work, that’s why I added <code>var id: String { name }</code> to it in the first place. For our multi-selector we want the UI to actually look pretty much the same, but instead of one, we would like to be able to choose multiple entries.</p><p>First, we need to re-create the entry in the <code>TaskEditView</code>, I’ve chosen the name <code>MultiSelector</code> as the replacement type name for <code>Picker</code>. Here is it’s implementation:</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>Note that I decided to represent each entry with a <code>String</code>, thus the <code>optionToString</code> closure is needed which will provide the <code>String</code>representation of the options type.</p><p>The call to <code>ListFormatter.localizedString</code> makes sure that we join a list of selected options together in the correct localization format (e.g. <code>[&quot;A&quot;, &quot;B&quot;, &quot;C&quot;]</code>becomes “A, B and C” for English).</p><p>This is the preview code I used for the view:</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>Note that instead of <code>Goal</code> I used an internal type to make the preview independent from my specific project. This is what the preview looks like:</p><p><img src="/assets/images/blog/multi-selector-in-swiftui/implementing-a-multi.webp" alt="Implementing a multi" loading="lazy" /></p><p>Let’s place this into our <code>TaskEditView</code> and see what it looks like in that context by replacing the TODO <code>Text</code> call with:</p><pre><code class="language-Swift">MultiSelector(
    label: Text(&quot;Serving Goals&quot;),
    options: allGoals,
    optionToString: { $0.name },
    selected: $task.servingGoals
)</code></pre><p>The preview now changes to this, which looks just as expected:</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>But when clicking on it, we see this, which is not right yet:</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>Let’s implement the detail view then. I’ve chosen the type name <code>MultiSelectionView</code> for the detail view and this is its 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>Except for <code>label</code>, this has basically the same properties. But this time, they are actually used, for example when checking if the checkmark should actually be shown by calling <code>contains</code> on the <code>selected</code> collection.</p><p>When one of the entries is clicked, <code>toggleSelection</code> is used on the entry to remove or insert it into the <code>selected</code>property. For the checkmark, I’m using the SF Symbol “checkmark” which looks exactly like the checkmark icon of <code>Picker</code>.</p><p>This is the preview code I’ve setup for the detail view, note that it’s pretty much a copy of the <code>MultiSelector</code> preview:</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>This is what it looks like in the Xcode preview:</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>Now finally, let’s integrate our <code>MultiSelectionView</code> to our <code>MultiSelector</code> by replacing the TODO <code>Text</code> entry with:</p><pre><code class="language-Swift">MultiSelectionView(
    options: options,
    optionToString: optionToString,
    selected: selected
)</code></pre><p>Basically, we’re just passing the data onto the detail view. But let’s see what our app looks like now in this animated GIF I recorded from the simulator:</p><p><img src="/assets/images/blog/multi-selector-in-swiftui/implementing-a-multi.gif" alt="Implementing a multi" loading="lazy" /></p><p>Nice, it’s working!</p><p>I’ve uploaded the Demo project <a href="https://github.com/Jeehut/MultiSelectorDemo">to GitHub</a> if you want to just copy the contents of the <code>MultiSelector</code> and <code>MultiSelectionView</code>, you can find them in <a href="https://github.com/Jeehut/MultiSelectorDemo/tree/main/Shared/MultiSelector">this folder</a>.</p>]]></content:encoded>
</item>
<item>
<title>Hiding Secrets From Git in SwiftPM</title>
<link>https://fline.dev/blog/hiding-secrets-from-git-in-swiftpm/</link>
<guid isPermaLink="true">https://fline.dev/blog/hiding-secrets-from-git-in-swiftpm/</guid>
<pubDate>Sun, 20 Feb 2022 00:00:00 +0000</pubDate>
<description><![CDATA[A step-by-step guide on how to prevent your 3rd party service secrets from committing to Git when using apps modularized with SwiftPM.]]></description>
<content:encoded><![CDATA[<p>You may be aware of some <a href="https://nshipster.com/secrets/">traditional methods</a> of hiding secrets like an API key or some 3rd party services’ token you need for your app. But nowadays approaches to <strong>modularize your app</strong> using SwiftPM become more and more popular.</p><p>For example, Point-Free has a great <a href="https://www.pointfree.co/episodes/ep171-modularization-part-1">free episode</a> on this topic and Majid Jabrayilov recently wrote a 4-parts series on “Microapps architecture” (parts <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>) which I can both recommend to get started.</p><p>Also, you might even want to hide secrets in public Open Source libraries, e.g. in the unit tests of some 3rd party service integration where users of the library will provide their own token, but you want <a href="https://github.com/Flinesoft/BartyCrouch/blob/baece7f4786bc805358f35ba5fd60d6259d5c8b9/Tests/BartyCrouchTranslatorTests/MicrosoftTranslatorApiTests.swift#L8">your tests to run with your own</a>.</p><p>What these situations have in common is that they are based on a custom maintained <code>Package.swift</code> file — not the one Xcode maintains for you if you just add a dependency to an app project. The app or project is split into many small modules with no corresponding <code>.xcodeproj</code> file, Xcode just opens the <code>Package.swift</code> file directly, without the need for a project.</p><p>This also means, that for the separate modules, there’s no way to specify any build settings or build scripts within Xcode, all needs to be done right within the <code>Package.swift</code> manifest file.</p><p>While more and more such features are being added to SwiftPM (like <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 future releases, there’s no sign they will support any Xcode-specific features such as <code>.xcconfig</code> files.</p><p>How can we hide secrets from committing to Git to ensure we’re not leaking them to our Git provider or anyone else with access to our repo <em>today</em>?</p><h1 id="swiftpm-resources-jsondecoder">SwiftPM resources &amp; JSONDecoder</h1><p>I’m sure there’s no single “best” answer to this and others may have smarter ideas than mine. But I like to <a href="https://en.wikipedia.org/wiki/KISS_principle">keep things simple</a> and I also like to use basic features because I know them well &amp; I expect other developers to understand them quickly if needed. Plus I can be sure they’re future-proof.</p><p>The approach I want to use is the <a href="https://github.com/bkeepers/dotenv">classical </a><a href="https://github.com/bkeepers/dotenv"><code>.env</code></a><a href="https://github.com/bkeepers/dotenv"> file approach</a> which is common in web development. But instead of a custom-formatted <code>.env</code> file, I want to simply have a <code>.json</code> file with my secrets in it, because JSON files are familiar to many iOS developers and we have built-in support for parsing them in Swift thanks to <a href="https://developer.apple.com/documentation/foundation/jsondecoder"><code>JSONDecoder</code></a>. Loading files or more generally “resources” is also supported by SwiftPM since Swift 5.3 (<a href="https://github.com/apple/swift-evolution/blob/main/proposals/0271-package-manager-resources.md">SE-271</a>).</p><p>Here’s the basic idea of how I want to hide secrets from Git outlined:</p><ol><li><p>Check in a <code>secrets.json.sample</code> file into Git with the keys, but no values</p></li><li><p>Let developers duplicate it , remove the <code>.sample</code> extension &amp; add values</p></li><li><p>Ignore the <code>secrets.json</code> file via <code>.gitignore</code> so it’s never checked in</p></li><li><p>Provide a simple <code>struct</code> conforming to <code>Decodable</code> to read the secrets</p></li></ol><p>The rest of this article is a step-by-step guide on how to apply this approach. I will be using the unit tests of my open source translation tool <a href="https://github.com/Flinesoft/BartyCrouch">BartyCrouch</a> which integrates with two 3rd-party translation services as an example.</p><blockquote><p><em>⚠️ Please note that if you plan to apply this approach to an <strong>app target</strong> which you will ship to users, you will probably run into the same problem as described in the <code>.xcconfig</code> approach in <em><a href="https://nshipster.com/secrets/#big-brain-store-secrets-in-xcode-configuration-and-infoplist"><em>this NSHipster article</em></a></em>. My method only helps hiding the secrets from Git, you will need additional obfuscation if you plan to ship to users.</em></p></blockquote><h1 id="adding-the-secretsjson-resource-file">Adding the <code>secrets.json</code> resource file</h1><p>First, let’s add the <code>secrets.json</code> file to our project. As there’s going to be a corresponding <code>secrets.json.sample</code> and a <code>Secrets.swift</code> file, I opt for creating a folder <code>Secrets</code> first, then I create an empty file which I name <code>secrets.json</code> and I add a simple JSON dictionary structure with two keys:</p><p><img src="/assets/images/blog/hiding-secrets-from-git-in-swiftpm/the-secrets-json-file-2.webp" alt="The <code>secrets.json</code> file with two actual secrets, added to the project." loading="lazy" /></p><p>And so my CI is also set up to access my secrets safely without leaking them.</p>]]></content:encoded>
</item>
<item>
<title>Laser Focus priority strategy</title>
<link>https://fline.dev/blog/laser-focus-priority-strategy/</link>
<guid isPermaLink="true">https://fline.dev/blog/laser-focus-priority-strategy/</guid>
<pubDate>Mon, 27 Sep 2021 00:00:00 +0000</pubDate>
<description><![CDATA[A simple but effective prioritization technique that can help slim down your apps scope and give you more confidence in it with different stages that can be mapped to Alpha, Beta & Release.]]></description>
<content:encoded><![CDATA[<p>There are lots of prioritization techniques that aim to solve different problems. You’ve probably already used some form of value vs. effort-based prioritization techniques, such as <a href="https://www.productplan.com/glossary/rice-scoring-model/">RICE</a>. Maybe you’ve even asked your target audience with a purposefully designed survey to learn from them, e.g. using the <a href="https://en.wikipedia.org/wiki/Kano_model">KANO model</a>. Every prioritization technique has its use cases and maybe they already helped you make a lot of useful decisions.</p><p>But these strategies are designed for a <strong>higher-level</strong> kind of <strong>prioritization</strong>, as in deciding if you should be implementing feature A or feature B first or if feature C is even needed in the next version at all. They **don’t scale down **to tasks or even sub-tasks of your features though, so it’s quite possible to do too much within a specific feature. Also, they don’t help answer when you can start putting the feature in users’ hands for early feedback to apply user-focused approaches, such as the <a href="https://en.wikipedia.org/wiki/Lean_startup">Lean Startup</a> methodology. One could of course opt for a method that is independent of scale, like the <a href="https://en.wikipedia.org/wiki/MoSCoW_method">MoSCoW method</a>, but their categories wouldn’t be easy to rate because they’re so abstract that different people would have different expectations for each category.</p><p>The goal of the Laser Focus prioritization strategy is to provide clear rating categories, help with task scopes and provide an easy-to-apply method for interpretation. All three aspects together help you stay laser-focused.</p><h3 id="laser-focus-categories">Laser Focus categories</h3><p>There are three goals we want to reach with our categories:</p><ol><li><p>Decide which tasks are <strong>in the scope</strong> of the currently planned release.</p></li><li><p><strong>Prioritize</strong> tasks needed for an Alpha or Beta version <strong>higher</strong> than the others.</p></li><li><p>The Categories names should have an actionable, self-contained <strong>meaning</strong>.</p></li></ol><p>We’re suggesting the following categories which fulfill all requirements:</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>Absolute minimum needed for the first round of testing. Can be ugly.</strong></p><p>This allows for shipping a product with just “Vital” features or tasks implemented to a small group of testers to get feedback early. Of course, the scope of this Alpha testing should be made clear stating what basic features or tasks are still missing so they are not unnecessarily reported by the testers. But the vitals of the product or feature can be tested already and we get the first round of feedback showing if we’re headed in the right direction.</p><h3 id="essential">Essential</h3><p><strong>Core aspects required for basic functionality. Can have rough edges.</strong></p><p>A second and bigger round of testing can be started as soon as all “Essential” features or tasks are implemented. At this level, no specific testing scope needs to be communicated, it should be enough to call the version a “Beta” version where the base features are available but still a lot of things are missing or incomplete.</p><h3 id="completing">Completing</h3><p><strong>Ironing out rough edges and completing aspects of functionality.</strong></p><p>The “Completing” level defines the scope where the final product is ready to be released. In some situations, e.g. if a new version was announced for a specific date, the product can also be released while still, some “Completing” tasks are open, but then it should be publicly marked as “Beta”. Typically this level includes all kinds of features or tasks that are important for a bigger customer base but are not relevant to evaluating the core of the product.</p><h3 id="optional">Optional</h3><p><strong>Nice-to-haves that can be delayed to later (versions) or skipped entirely.</strong></p><p>The “Optional” level has the notion that the features or tasks rated as such are wanted things, but that they are in no way necessary to release a finalized version of a product, even long term. Hence they can also be easily delayed or scrapped if needed as per the resources of the team.</p><h3 id="retracting">Retracting</h3><p><strong>Nice-to-haves (at first sight) that can (potentially) cause more harm than improve things.</strong></p><p>Unlike “Optional”, features or tasks rated as “Retracting” should be <em>actively avoided</em>. That means it can make sense to document or keep them somewhere including the rationale why they should be avoided for long-term decision-making. This saves time when the same idea comes up again sometime in the future. Also, if multiple people are involved in the rating, it can help identify the tasks where discussion might be necessary to clarify the effect of a task on the product.</p><h3 id="laser-focus-matrix">Laser Focus matrix</h3><p>The second pillar of the Laser Focus strategy is its <strong>multi-dimensional scalability</strong>. To explain what this means and why it is important, let’s apply the categories we have so far with an example: Let’s develop a stopwatch app to track time for different things done throughout the day. This is the initial list of feature ideas, rated using the Laser Focus categories:</p><ol><li><p>Create projects<br />→ <strong>Essential</strong> (essential to app, but pre-filled projects enough for first test)</p></li><li><p>Edit projects<br />→ <strong>Completing</strong> (not a necessity for testing purposes, but for final release)</p></li><li><p>Delete projects<br />→ <strong>Completing</strong> (cleanup task, not needed for testing purposes, but for final)</p></li><li><p>Start/Stop a timer<br />→ <strong>Vital</strong> (core idea of app, vital part of the app)</p></li><li><p>Select a project for the timer<br />→ <strong>Vital</strong> (without selecting project, app idea not fulfilled)</p></li><li><p>Edit past tracked times<br />→ <strong>Retracting</strong> (V2 planned with competitive feature, risk of cheating)</p></li><li><p>Delete past tracked times<br />→ <strong>Optional</strong> (nice to have, no risk of cheating as no added time)</p></li><li><p>Show historical time tracked on a selected project<br />→ <strong>Essential</strong> (core use case for app)</p></li><li><p>Show projects with the most tracked time<br />→ <strong>Essential</strong> (core use case for app)</p></li></ol><p>Thanks to the categorization, we can already exclude two features from the first release and recognized even a feature we should probably never implement (6) that should be permanently documented. But more importantly, we now know that 4 and 5 are the “Vital” features to implement first.</p><p>Let’s start working on their sub-tasks:</p><p><strong>4. Start/Stop a timer:</strong> (Vital)</p><p>a.	Design Start/Stop button layout (low fidelity)<br />b.	Design Start/Stop button coloring &amp; icons (high fidelity)<br />c.	Design Start/Stop button pulsating shadow effect (animations)<br />d.	Implement Start/Stop button layout (low fidelity)<br />e.	Implement Start/Stop button coloring &amp; icons (high fidelity)<br />f.	Implement Start/Stop button pulsating shadow effect (animations)<br />g.	Setup basic tracked time database models<br />h.	Persist Start/Stop actions into database</p><p><strong>5. Select a project for the timer:</strong> (Vital)</p><p>a.	Design project selector navigation &amp; layout (low fidelity)<br />b.	Design project selector shapes, colors &amp; icons (high fidelity)<br />c. 	Implement project selector navigation &amp; layout (low fidelity)<br />d.	Implement project selector shapes, colors &amp; icons (high fidelity)<br />e.	Persist selected project into tracked time database model</p><p>All clear, let’s get started, right? <em>Right?</em></p><p>No. I’m sure you noticed it already while reading/skimming through them. There’s a problem. We have prioritized the features thinking about what’s really necessary for being testable, for putting the app into users’ hands. But now we have the same problem again, just on a different level. These tasks (and potentially also their sub-tasks) aren’t all “Vital” for our very first version to put in users’ hands. How can we fix this? Should we apply another rating for the tasks, too?</p><p>Yes, absolutely! This is actually a requirement in the Laser Focus strategy: Apply the rating on <strong>all levels</strong> down the road! Not necessarily above levels, where you are allowed to choose any alternative prioritization technique. But the lower levels from wherever you want to start from should all be rated like this.</p><p>Let’s assign the Laser Focus categories to the tasks, too, and then see what this means for overall priority:</p><p><strong>4. Start/Stop a timer:</strong> (Vital)</p><p>a.	Design Start/Stop button layout (low fidelity)<br />→ <strong>Vital</strong><br />b.	Design Start/Stop button coloring &amp; icons (high fidelity)<br />→ <strong>Completing</strong><br />c.	Design Start/Stop button pulsating shadow effect (animations)<br />→ <strong>Optional</strong><br />d.	Implement Start/Stop button layout (low fidelity)<br />→ <strong>Vital</strong><br />e.	Implement Start/Stop button coloring &amp; icons (high fidelity)<br />→ <strong>Completing</strong><br />f.	Implement Start/Stop button pulsating shadow effect (animations)<br />→ <strong>Optional</strong><br />g.	Setup basic tracked time database models<br />→ <strong>Essential</strong><br />h.	Persist Start/Stop actions into database<br />→ <strong>Essential</strong></p><p><strong>5. Select a project for the timer:</strong> (Vital)</p><p>a.	Design project selector navigation &amp; layout (low fidelity)<br />→ <strong>Vital</strong><br />b.	Design project selector shapes, colors &amp; icons (high fidelity)<br />→ <strong>Completing</strong><br />c. 	Implement project selector navigation &amp; layout (low fidelity)<br />→ <strong>Vital</strong><br />d.	Implement project selector shapes, colors &amp; icons (high fidelity)<br />→ <strong>Completing</strong><br />e.	Persist selected project into tracked time database model<br />→ <strong>Essential</strong></p><p>It’s important to note that the reference value for the ratings of the tasks was the feature because it’s the direct parent. This means that I asked myself the question “Is persisting Start/Stop actions into database vital or essential <em>to the feature</em> Start/Stop a timer?” and not to the app or anything else. This makes answering the questions much easier.</p><p>Let’s visualize these two different levels of rating with a simple matrix. On the X-axis we put the ratings of the features. On the Y-axis are the ratings of the tasks. The circles represent the tasks:</p><p><img src="/assets/images/blog/laser-focus-priority-strategy/laser-focus-matrix.webp" alt="Laser focus matrix" loading="lazy" /></p><p>As you can see, tasks 4a, 4d, 5a, and 5c are in the bottom left field, the “Vital-Vital” field, or short “VV”. The field’s background is tinted red. It contains all tasks the focus should be on first. Once they’re all implemented, the very first testing round can begin, and the Alpha phase starts.</p><p>The tasks 4g, 4h, and 5e in the yellow-tinted field “Vital-Essential” or short “VE” should be tackled next. Once all tasks in all three yellow-tinted fields (VV, VE, EV) are completed, the Beta phase starts.</p><p>The “VC” field with its “Completing” tasks for the “Vital” features should be tackled last among the tasks we defined so far. Once all tasks in all green-tinted fields (VC, CV, EC, CE, CC) are done, it’s Release time.</p><p>In the above example, we skipped the tasks for all non-Vital features. If we had rated them also, the full matrix could have looked something like this, including also the “Retracting” rating:</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>We can see how the Alpha, Beta, and Release tasks are circularly layered around the origin point (bottom left corner), visually providing us a priority for each task based on its distance to the origin. This easily scales to a third axis if for example sub-tasks were added to each task. Formally speaking, this scales to any number of dimensions. To calculate the overall category of any given element, just look up all ancestors and just select the lowest priority as the overall category of the “atomic” (lowest level) element. For example, imagine a sub-task with the category rating “Essential”, a parent task rated “Vital” and its parent feature rated “Completing”. Overall, the lowest priority is “Completing”, so this is the overall category of the sub-task.</p><p>Calculating the overall category alone can lead to many tasks being on the same level, especially at “Completing” where we have 5 different fields. A way of prioritizing features or tasks within the same category is by calculating the average of its own category combined with that of all its ancestor’s categories. To do this, let’s assign each category a number (from 1 “Vital” to 5 “Retracting”), the lowest level (e.g. a sub-task) can then be represented by a tuple, e.g. <code>(2, 1, 3)</code> in the above example. The average of these numbers is simply calculated by <code>(2 + 1 + 3) / 3 = 2.0</code>. Another task with more ancestors and the same overall “Completing” category might be rated as <code>(3, 2, 3, 1)</code> and therefore have an average of <code>(3 + 2 + 3 + 1) / 4 = 2.25</code>, so it should be prioritized lower. The higher the overall average, the lower the priority – that makes a lot of sense as the average number roughly resembles the distance to the origin – the highest possible priority.</p><p>But don’t worry, you don’t actually have to calculate these averages, there’s a simpler way based on the matrix we’ve seen above with enough precision:</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>The above diagram shows in which order the fields should be tackled, based on origin distance. Note that there are two fields placed 2nd, 4th, and 5th each. For these fields, there’s a choice to be made that can be different depending on the situation: Should we focus more on adding more features? Or should we focus more on improving the already started features? For a feature focus expansion first, you should continue in direction of the “Feature category”, e.g. “EV” before “VE”. For improving existing features first, it should be the other way around.</p><h3 id="laser-focus-breakdown">Laser Focus breakdown</h3><p>In the above section, we learned that categorization on multiple levels is key to the Laser Focus concept. If you try to apply this to your project right away, you may realize though that many or even all of your features or tasks are actually “Vital” or “Essential” to you. If this is the case, then it’s a sign that you have probably not efficiently split your tasks yet.</p><p>That’s why breaking down your tasks the right way is important before categorizing them. The guiding question you should ask yourself while splitting features into tasks or tasks into sub-tasks should not be restricted to “which steps do I need to make to finalize it”. You should also think about the effort for each step and if the effort isn’t negligibly small, you might want to consider splitting it away. Sometimes it might seem to be hard doing that, but more often than not, it’s a good idea to follow the approach “make it work, then make it better” while splitting the tasks.</p><p>For example, for the above feature “Start/Stop a timer” we could have split it up into 3 tasks: “Design the Start/Stop buttons”, “Implement the Start/Stop buttons” and “Persist data”. The problem with this is that there are no different levels of completion. It’s better to break it down even further. Of course, we could do that as sub-tasks under these tasks, but to make priority calculation easier, it is recommended to do it on fewer levels. So instead we opted for “Design the Start/Stop button <em>layout</em>”, “Implement Start/Stop button <em>layout</em>” and the same two tasks also for “… coloring &amp; icons” and “… pulsating shadow effect”.</p><p>Ask yourself which parts have their own effort and split them so each task is worth being prioritized based on the effort needed. Don’t split micro-tasks away, it’s not worth prioritizing such small tasks, just keep them as part of another task.</p><p>A proper breakdown is very important for the Laser Focus strategy to be effective.</p><h3 id="summary">Summary</h3><p>Let’s sum up the Laser Focus prioritization strategy in the right order:</p><ol><li><p><strong>Break down</strong> your features and tasks into smaller steps of different completion levels</p></li><li><p><strong>Rate them</strong> on each level with “Vital”, “Essential”, “Completing”, “Optional” or “Retracting”</p></li><li><p><strong>Visualize or calculate</strong> the overall priority for the lowest level by considering all ancestors</p></li></ol><p>Apply these steps at any given time for your project and it will help you keep focusing on the important things and confidently put your work-in-progress versions into users’ hands as soon as reasonable.</p>]]></content:encoded>
</item>
<item>
<title>Git Merge vs Rebase</title>
<link>https://fline.dev/blog/git-merge-vs-rebase/</link>
<guid isPermaLink="true">https://fline.dev/blog/git-merge-vs-rebase/</guid>
<pubDate>Thu, 01 Jul 2021 00:00:00 +0000</pubDate>
<description><![CDATA[An FAQ that explains and answers when to use which and why.]]></description>
<content:encoded><![CDATA[<p>There’s a common discussion among developers about how teams should use <a href="https://git-scm.com/">Git</a> to make sure everyone is always up-to-date with the latest changes in the <code>main</code> branch. The typical situation this question arises is when someone worked on a new branch and then once the work is done and ready to be merged, the main branch had changes in the meantime in a way that the work branch is outdated and now has <strong>merge conflicts</strong>.</p><p>Obviously, they need to be resolved before the work branch can be merged. But the question is: <em>How</em> should this situation be resolved? Should we <em><strong>merge</strong></em> the main branch into the work branch? Or should we <em><strong>rebase</strong></em> the work branch onto the latest main branch?</p><p>In my opinion, there’s only one correct answer to this question. From my experience, the main reason why so many discussions arise around this topic is that there’s a lot of misunderstandings out there about how merge and rebase differ from each other in this context and a general lack of understanding, what a rebase even is.</p><p>So I created an FAQ for my team which tries to clarify things. Let me share:</p><h4 id="what-is-a-merge">What is a merge?</h4><p>A commit, that combines all changes of a different branch into the current.</p><h4 id="what-is-a-rebase">What is a rebase?</h4><p>Re-comitting all commits of the current branch onto a different base commit.</p><h4 id="what-are-the-main-differences-between-merge-and-rebase">What are the main differences between merge and rebase?</h4><ol><li><p><code>merge</code> executes only <strong>one</strong> new commit. <code>rebase</code> typically executes <strong>multiple</strong> (number of commits in current branch).</p></li><li><p><code>merge</code> produces a <strong>new</strong> generated commit (the so called merge-commit). <code>rebase</code> only moves <strong>existing</strong> commits.</p></li></ol><h4 id="in-which-situations-should-we-use-a-merge">In which situations should we use a <code>merge</code>?</h4><p>Use <code>merge</code> whenever you want to add changes of a branched out branch <strong>back</strong> into the base branch.</p><p>Typically, you do this by clicking the “Merge” button on Pull/Merge Requests, e.g. on GitHub.</p><h4 id="in-which-situations-should-we-use-a-rebase">In which situations should we use a <code>rebase</code>?</h4><p>Use <code>rebase</code> whenever you want to add <strong>changes of a base branch</strong> back to a branched out branch.</p><p>Typically, you do this in <code>work</code> branches whenever there’s a change in the <code>main</code> branch.</p><h4 id="why-not-use-merge-to-merge-changes-from-the-base-branch-into-a-work-branch">Why not use <code>merge</code> to merge changes from the base branch into a work branch?</h4><ol><li><p>The git history will include many <strong>unnecessary merge commits</strong>. If multiple merges were needed in a work branch, then the work branch might even hold more merge commits than actual commits!</p></li><li><p>This creates a loop which <strong>destroys the mental model that Git was designed by</strong> which causes troubles in any visualization of the Git history.</p></li></ol><p>Imagine there’s a river (e.g. the “Nile”). Water is flowing in one direction (direction of time in Git history). Now and then, imagine there’s a branch to that river and suppose most of those branches merge back into the river. That’s what the flow of a river might look like naturally. It makes sense.</p><p>But then imagine there’s a small branch of that river. Then, for some reason, <strong>the river merges into the branch</strong> and the branch continues from there. The river has now technically disappeared, it’s now in the branch. But then, somehow magically, that branch is merged back into the river. Which river you ask? I don’t know. The river should actually be in the branch now, but somehow it still continues to exist and I can merge the branch back into the river. So, the river is in the river. Kind of doesn’t make sense.</p><p>This is exactly what happens when you <code>merge</code> the base branch into a <code>work</code> branch and then when the <code>work</code> branch is done, you merge that back into the base branch again. The mental model is broken. And because of that, you end up with a branch visualization that’s not very helpful.</p><h4 id="example-git-history-when-using-merge">Example Git History when using <code>merge</code>:</h4><p><img src="/assets/images/blog/git-merge-vs-rebase/example-git-history-when.webp" alt="Example Git History when using merge" loading="lazy" /></p><p><em>Example Git History when using <code>merge</code></em></p><p>Note the many commits starting with <code>Merge branch ‘main’ into …</code> (marked with yellow boxes). They don’t even exist if you rebase (there, you will only have pull request merge commits). Also note the many visual branch merge loops (<code>main</code> into <code>work</code> into <code>main</code>).</p><h4 id="example-git-history-when-using-rebase">Example Git History when using <code>rebase</code>:</h4><p><img src="/assets/images/blog/git-merge-vs-rebase/example-git-history-when-2.webp" alt="Example Git History when using rebase" loading="lazy" /></p><p><em>Example Git History when using <code>rebase</code></em></p><p>Much cleaner Git history with much less merge commits and no cluttered visual branch merge loops whatsoever.</p><h4 id="are-there-any-downsides-pitfalls-with-rebase">Are there any downsides / pitfalls with <code>rebase</code>?</h4><p>Yes:</p><ol><li><p>Because a <code>rebase</code> moves commits (technically re-executes them), the commit date of all moved commits will be the time of the rebase and the <strong>git history loses the initial commit time</strong>. So, if the exact date of a commit is needed for some reason, then <code>merge</code> is the better option. But typically, a clean git history is much more useful than exact commit dates.</p></li><li><p>If the rebased branch has multiple commits that change the same line and that line was also changed in the base branch, you might need to solve merge conflicts for that same line multiple times, which you never need to do when merging. So, on average, there’s more merge conflicts to solve.</p></li></ol><h4 id="tips-to-reduce-merge-conflicts-when-using-rebase">Tips to reduce merge conflicts when using <code>rebase</code>:</h4><ol><li><p><strong>Rebase often</strong>. I typically recommend doing it at least once a day.</p></li><li><p>Try to <strong>squash changes</strong> on the same line into one commit as much as possible.</p></li></ol><p>I hope this FAQ helps some teams out there.</p>]]></content:encoded>
</item>
<item>
<title>Primer on Regular Expressions</title>
<link>https://fline.dev/blog/primer-on-regular-expressions/</link>
<guid isPermaLink="true">https://fline.dev/blog/primer-on-regular-expressions/</guid>
<pubDate>Thu, 06 May 2021 00:00:00 +0000</pubDate>
<description><![CDATA[In this post, I will try to give you a practical overview of Regular Expressions to teach you what they are, what they can be used for and a quick intro to how you can use them.]]></description>
<content:encoded><![CDATA[<h3 id="what-are-regular-expressions-even">What are Regular Expressions even?</h3><p>Regular Expressions (short Regexes) are Strings that work as a DSL (domain-specific language) to do some common tasks within other Strings. A DSL can also be subscribed as “a programming language within a programming language”.</p><p>In the case of Regexes, the outer programming language can be any programming language that supports the <code>String</code>type, it just has to support Regexes. Nearly all popular programming languages support Regexes, which makes Regexes so useful to know. The inner language of Regexes consists of only <code>String</code> with some characters having a special meaning.</p><p>For example in the String <code>&quot;.*@.*\\.com&quot;</code> the <code>.</code> means “any character”, the <code>*</code> means “any amount of <whatever precedes>”, together <code>.*</code> means “any amount of any character”. Then we have a non-special character <code>@</code>, then again <code>.*</code>followed by <code>\\</code> which means “escape the next character and treat like a non-special character” so <code>\\.</code> together reads like a normal <code>.</code> character without the special meaning “any character”. Lastly, there’s <code>com</code> which is just a set of characters without any special meaning. Overall this Regex is a simple matcher for any email address ending with <code>.com</code> and containing a <code>@</code> somewhere.</p><h3 id="what-can-i-do-with-the-regular-expression-dsl">What can I do with the “Regular Expression DSL”?</h3><blockquote><p>ℹ️ With <a href="https://www.ruby-lang.org/en/">Ruby</a> installed (on Macs it’s preinstalled), you can type <code>irb</code> to start an <strong>i</strong>nteractive <strong>R</strong>u<strong>b</strong>y shell to play around with the samples below.</p></blockquote><p>There are three main functions that any Regex string can be used with:</p><ol><li><p><code>matches</code>: Given a Regex and another String, this function checks if the given String “matches” the Regex. This means if there’s “any” part within the given String that matches the specified Regex, it returns <code>true</code>, otherwise, it’s <code>false</code>. For example in Ruby (where <code>matches</code> is called <code>match?</code> – <code>?</code> is part of the function name):</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>: Given a Regex and another String, this function can read substrings out of the given text which matches marked portions of the given Regex. The portions in the Regex can be marked via <code>(</code> and <code>)</code>. They are called “capture groups”. For example in Ruby (where <code>captures</code> can be accessed on the <code>Match</code> object):</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>: Given a Regex and a Template String, this function can automatically replace matches with a given String where even the capture groups can be referenced via <code>$1</code>, <code>$2</code> or in some languages also <code>\\1</code> , <code>\\2</code>, etc. For example in Ruby (where <code>replace</code> is called <code>gsub</code>):</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="what-does-the-regular-expressions-dsl-look-like">What does the “Regular Expressions DSL” look like?</h3><p>There’s plenty of useful “cheat sheets” for this with great examples:</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>Generally, there are 5 different kinds of DSL components to understand:</p><h4 id="1-charactergroup-modifiers-eg">1. Character/Group Modifiers (e.g. <code>*</code>, <code>+</code>, <code>{,}</code>, <code>?</code>)</h4><p>The default “building” block of Regexes are characters. After each character, you can write a modifier that tells how many times the preceding character is matched. The following modifiers are available:</p><p>**<code>0</code> or <code>1</code> times: **<br /><code>?</code> (example: <code>a?b?c?</code> matches all of <code>a</code>, <code>ab</code>, <code>abc</code>, <code>bc</code>, <code>c</code>)</p><p>**<code>1</code> time exactly: **<br />No modifier (default)</p><p>**<code>0</code> to ♾️ times: **<br /><code>*</code> (example: <code>a*bc</code> matches <code>bc</code>, <code>abc</code>, <code>aaabc</code>)</p><p><strong><code>1</code> to ♾️ times:</strong><br /><code>+</code> (example: <code>a+bc</code> matches <code>abc</code>, <code>aaabc</code> but not <code>bc</code>)</p><p><strong><code>X</code> times exactly:</strong><br /><code>{X}</code> (example: <code>a{3}bc</code> matches <code>aaabc</code> but not <code>aabc</code>, <code>aaaabc</code>)</p><p><strong><code>X</code> to <code>Y</code> times:</strong><br /><code>{X,Y}</code> (example: <code>a{2,5}bc</code> matches <code>aaaaabc</code>, but not <code>abc</code>)</p><p><strong><code>X</code> to ♾️ times:</strong><br /><code>{X,}</code> (example: <code>a{2,}bc</code> matches <code>aaaaaaaabc</code> but not <code>abc</code>)</p><p>The same modifiers also work on Groups (e.g. <code>(abc)+</code>) (see below for groups).</p><h4 id="custom-sets-created-with-and">Custom Sets (created with <code>[</code> and <code>]</code>)</h4><p>You can define custom sets of characters by listing them without any separator within brackets, e.g. for a set of the characters a, b, c and numbers 1, 2, 3 we would write <code>[abc123]</code>. This is then considered as “one character of this set”, thus matching multiple of them need character modifiers as in <code>[abc123]*</code> or <code>[abc123]{2,5}</code>.</p><p>You can also use <code>^</code> at the beginning of a custom set to specify that you accept any character except the set you specified in the brackets, e.g. <code>[^\\n]</code> to accept any character except a newline.</p><p>Characters of which you know are ordered right after each other like numbers or the Alphabet you can also use ranges by putting a <code>-</code> in between, e.g. <code>[a-zA-Z0-9]</code>.</p><p><code>[abc123]{3,}</code> would not match <code>a</code>, <code>b</code>, <code>c</code>, <code>ab</code>, but would match <code>111</code>, <code>abc</code></p><h4 id="predefined-sets-s-s-d-d-w-w">Predefined Sets (<code>\\s</code>, <code>\\S</code>, <code>\\d</code>, <code>\\D</code>, <code>\\w</code>, <code>\\W</code>)</h4><p>The following sets (simplified) are already pre-defined and can be used directly:</p><ul><li><p><code>\\s</code> effectively same as <code>[ \\t\\n]</code>, reads “any whitespace character”</p></li><li><p><code>\\S</code> effectively same as <code>[^ \\t\\n]</code>, reads “any non-whitespace character”</p></li><li><p><code>\\d</code> effectively same as <code>[0-9]</code>, reads “any digit”</p></li><li><p><code>\\D</code> effectively same as <code>[^0-9]</code>, reads “any non-digit”</p></li><li><p><code>\\w</code> similar to <code>[a-zA-Z_0-9]</code> (includes Umlauts etc.), reads “any word character”</p></li><li><p><code>\\W</code> similar to <code>[^a-zA-Z_0-9]</code> reads “any non-word character”</p></li></ul><h4 id="groups-eg-and-name-and">Groups (e.g. <code>(</code> and <code>)</code>, <code>(?&lt;name&gt;</code> and <code>)</code>)</h4><p>Groups could be thought of like “words” or “sentences”, they change the default building block, which is “character” for any modifier to a set of characters, or a “group”. For example, writing <code>abc*</code> reads “one time a, one times b and any number of times c”. If you want to write “any number of times abc” you do this: <code>(abc)*</code>. The <code>abc</code> is then considered one group and the regex would match the whole string <code>abcabcabc</code>.</p><p>Groups also allow for specifying different options to choose from. For this, you write a group and separate the different words via a <code>|</code> like so: <code>(abc|def)</code> – this reads “either abc or def” and would match both <code>123abc123</code> and <code>456def456</code> but not <code>adbecf</code>.</p><p>These capture some sub-portions of a Regex and assign them a number or name which then can be used to reference them in code or in replacement template Strings. Typically capture groups like in <code>(.*)@(.*).com</code> are used then referenced back via <code>\\1@\\2.com</code> or <code>$1@$2.com</code> (depending on the language).</p><p>It’s also possible to give the groups names, e.g. <code>(?&lt;user&gt;.*)@(?&lt;domain&gt;.*).com</code> to reference back like in <code>${user}@${domain}.com</code>, but these are advanced features which are implemented differently in different languages (and are missing in some).</p><h4 id="match-modifiers-eg-a-z-lookaheads-and-lookbehinds">Match Modifiers (e.g. <code>\\A</code>, <code>\\z</code>, <code>^</code>, <code>$</code>, Lookaheads and Lookbehinds)</h4><p>By default, a match for a Regex, like <code>abc</code> is done like a <code>contains</code> method. But you can also specify that the <code>abc</code> string needs to be at the beginning or end of a given string or of a line. For example, the <code>^</code> in <code>^abc</code> makes sure only strings with <code>abc</code> at the beginning of a new line match. This will match <code>def\\nabc</code> but not <code>defabc</code>. The <code>$</code> in <code>abc$</code> makes sure there’s a line-end after <code>abc</code>. Use <code>\\A</code> and <code>\\z</code> to match among the entire String (matching multiple lines).</p><p>Lookaheads &amp; Lookbehinds are more of an advanced topic and useful mostly when you want to match that the beginning or end of your Regex does NOT match a given Regex. In most cases, Regexes with Lookaheads &amp; Lookbehinds can be rewritten with Capture Groups, so you should try to write them as Capture Groups instead and only read about these if the other options don’t work as Lookaround are CPU-intensive operations and also kind of restricted (e.g. they don’t support most modifiers).</p><p>Here’s a good place to learn about them:</p><h3 id="common-quirks-and-validating-new-regexes">Common Quirks and validating new Regexes</h3><p>One common thing to consider is that <code>.</code> in most languages does <strong>not match the newline</strong> character by default. But it can typically be turned on with an option, in Ruby by specifying the <code>/m</code> at the end which stands for “make dot match newlines”.</p><p>Also note that in every language there are different characters that are <strong>reserved</strong> due to how Strings work in them, for example in Ruby <code>/</code> needs to be escaped with <code>\\/</code>, in Swift this escape is not needed but there you need to escape <code>{</code> and <code>}</code> with <code>\\{</code> and <code>\\}</code>. These quirks are important to remember when copy &amp; pasting Regexes written for other languages.</p><p>Generally, when writing a new Regex, I recommend using a website or tool with 3 features:</p><ol><li><p>An option to add a sample String to match against.</p></li><li><p>A Regex cheat sheet visible right on the screen to look up things.</p></li><li><p>A live matcher for the regex you write among the given sample String.</p></li></ol><p>The site I’ve come to use here is this (runs Ruby in the background):</p><p><a href="https://rubular.com/">Rubular</a></p><h3 id="how-can-i-use-regexes-in-my-projects-today">How can I use Regexes in my projects <em>today</em>?</h3><p>There’s no need to wait until there’s a good opportunity to use Regexes, you can simply lint your projects using Regular expressions (including Auto-Correction support) via 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>