<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Carolane Lefebvre</title>
    <description>The latest articles on DEV Community by Carolane Lefebvre (@carolanelfbv).</description>
    <link>https://dev.to/carolanelfbv</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4174901%2Ff6be1bf8-3447-4340-92b2-c530ea6ce30d.png</url>
      <title>DEV Community: Carolane Lefebvre</title>
      <link>https://dev.to/carolanelfbv</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/carolanelfbv"/>
    <language>en</language>
    <item>
      <title>Understanding iOS Crashes in Production: A Sentry Guide for your app</title>
      <dc:creator>Carolane Lefebvre</dc:creator>
      <pubDate>Mon, 12 Oct 2026 07:00:00 +0000</pubDate>
      <link>https://dev.to/carolanelfbv/understanding-ios-crashes-in-production-a-sentry-guide-for-your-app-1n10</link>
      <guid>https://dev.to/carolanelfbv/understanding-ios-crashes-in-production-a-sentry-guide-for-your-app-1n10</guid>
      <description>&lt;p&gt;Your app is on the App Store. You check the reports: zero crashes. You tell yourself everything is fine, your app is perfect!&lt;br&gt;
But does that mean it never crashes… or simply that you cannot see the crashes?&lt;/p&gt;

&lt;p&gt;Imagine it crashes three times a day on your users' devices. You see nothing, so you fix nothing. And the person using it will not necessarily send you an email. They try again, it crashes again, and eventually they uninstall it.&lt;/p&gt;

&lt;p&gt;Today, we will look at how to collect this information with Sentry, integrate it into a real app, and, above all, learn to read a report to understand what crashed and where to look in your code.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhfgnfyzlxl921t4pg05e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhfgnfyzlxl921t4pg05e.png" alt="Screen Capture of Issues reported with Sentry (2 issues reported in a week)" width="800" height="429"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When crashes happen on a user's device, the developer needs visibility.&lt;/p&gt;


&lt;h2&gt;
  
  
  How does that&amp;nbsp;work?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvnts6vsgm5ng2qbrqkzt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvnts6vsgm5ng2qbrqkzt.png" alt="A crash is captured by the Sentry SDK and saved on the user's iPhone. On the next app launch, the report is uploaded to Sentry when possible. The developer can view it online and receive an email alert if configured." width="800" height="251"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When your app crashes during development, you have Xcode right in front of you. You know what you just did, you can look at where things went wrong, and you can try to reproduce the problem.&lt;br&gt;
But once your app is on someone else's device, you are no longer there to see what happens.&lt;/p&gt;

&lt;p&gt;They might tell you, "It crashes when I open this page." OK. But which app version? On which iPhone? And, above all, what crashed in the code? Because when you open the same page on your own device, everything works.&lt;/p&gt;

&lt;p&gt;What we need, then, is evidence we can use to investigate, even when we were not there when the crash happened.&lt;/p&gt;

&lt;p&gt;That is where Sentry comes in. I already use it in Keepio, but today we will add it together to Caffio, another of my iOS apps.&lt;/p&gt;

&lt;p&gt;The goal is not just to install it and say, "Well, that's done." We will trigger a test crash, see what gets reported, and learn to read the report. Having an error in a dashboard is one thing. &lt;br&gt;
Understanding what it tells us so we can fix it is what matters.&lt;/p&gt;
&lt;h2&gt;
  
  
  What a crash report gives&amp;nbsp;you
&lt;/h2&gt;

&lt;p&gt;A crash report provides evidence from a process you were not watching: the failure, its stack trace, the app build, and device context. Depending on the configuration, breadcrumbs can also show some of the actions leading up to it.&lt;/p&gt;

&lt;p&gt;An event is one occurrence. An issue groups related events. Ten events do not necessarily mean ten different bugs, and an event count is not the same thing as a count of affected people. &lt;/p&gt;

&lt;p&gt;That distinction matters when deciding what to fix first. A launch crash affecting new users can be more urgent than a larger number of development-only errors.&lt;/p&gt;

&lt;p&gt;Reporting also has limits. The SDK must be initialized, the event must be captured and delivered, and the project must accept it. An empty dashboard is not proof that every user had a crash-free experience.&lt;/p&gt;
&lt;h2&gt;
  
  
  Create a project for the&amp;nbsp;app
&lt;/h2&gt;

&lt;p&gt;In Sentry, create a project for your app (mine's gonna be Caffio) and select iOS as the platform. Choose the team that should have access and configure the notifications you want to receive.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Open the project&amp;nbsp;list&lt;/strong&gt;&lt;br&gt;
Open Settings → Projects, or use your organization's menu to reach Projects.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbn8uhpdo8gflbkm3jpqm.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbn8uhpdo8gflbkm3jpqm.webp" alt="Sentry navigation showing how to access the Projects section through Settings." width="800" height="523"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;From the project list, choose Create Project.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxucddqa3xj88nmq5joo8.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxucddqa3xj88nmq5joo8.webp" alt="Sentry's project list, with the Create Project button used to add your project." width="800" height="511"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Choose the platform and project&amp;nbsp;settings&lt;/strong&gt;&lt;br&gt;
Connecting a repository is optional for this setup.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvku830jumccj1e2bz28w.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvku830jumccj1e2bz28w.webp" alt="The optional repository connection step in Sentry's project creation form." width="798" height="114"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Select iOS as the plateform&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhr9e9tj8u82dkfrf70qt.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhr9e9tj8u82dkfrf70qt.webp" alt="The platform selection step, with iOS selected for the my project." width="799" height="121"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The products panel describes the available features. In these screenshots, error monitoring is marked Always on; this panel does not require selecting a checkbox.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F48knmsi3lcnbubf3r5ws.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F48knmsi3lcnbubf3r5ws.webp" alt="Sentry's products overview, showing Error Monitoring as Always on alongside other available features" width="799" height="363"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Name the project with your app name and choose the team that should have access.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwz14h0ja72k1u8gffl4q.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwz14h0ja72k1u8gffl4q.webp" alt="Project settings showing the name of the project and the selected team, Okorp." width="799" height="204"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Configure alerts and their delivery channel. The source walkthrough uses High Priority Only and Email; choose the settings that suit your project, then create it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1fa72vryniavvl279ztc.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1fa72vryniavvl279ztc.webp" alt="Alert settings configured to send high-priority notifications by email." width="800" height="389"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Open manual configuration and find the&amp;nbsp;DSN&lt;/strong&gt;&lt;br&gt;
Sentry offers a setup wizard. For this walkthrough, open Manual Configuration so we can add the SDK and configure it ourselves.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F92rc8mpvqx1px5iz78sd.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F92rc8mpvqx1px5iz78sd.webp" alt="Web page to configure iOS SDK with Sentry" width="800" height="523"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The exact onboarding labels can change. The important outputs are the project and its DSN, which tells the SDK where to send events. Copy the DSN from your project's setup instructions or Client Keys settings rather than constructing it yourself.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgj9tty19ej3ah5o9749z.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgj9tty19ej3ah5o9749z.webp" alt="Sentry's setup screen offering the setup wizard and the Manual Configuration option." width="800" height="150"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Keep the distinction between capture and notification in mind: an event appearing in Sentry and an email being sent are separate steps. Alert rules can filter or delay notifications.&lt;/p&gt;

&lt;p&gt;We will use a DSN in the app and an authentication token for uploading debug symbols. They serve different purposes; the upload token belongs in your developer or CI environment, not in the application binary.&lt;/p&gt;

&lt;p&gt;Okay, now that we have our project, let's get back to Xcode.&lt;/p&gt;


&lt;h2&gt;
  
  
  Add the Apple SDK through Swift Package&amp;nbsp;Manager
&lt;/h2&gt;

&lt;p&gt;In Xcode, open File → Add Package Dependencies.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkb5w6kgj5ssxvfrun2e3.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkb5w6kgj5ssxvfrun2e3.webp" alt="Xcode's File menu showing the Add Package Dependencies command." width="800" height="409"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Enter the official package URL: &lt;a href="https://github.com/getsentry/sentry-cocoa.git" rel="noopener noreferrer"&gt;https://github.com/getsentry/sentry-cocoa.git&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Add the &lt;strong&gt;Sentry&lt;/strong&gt; product to &lt;strong&gt;the main application target&lt;/strong&gt;. Adding it to the app does not automatically instrument a widget or another extension.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7xanceifhyg4e52moeu3.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7xanceifhyg4e52moeu3.webp" alt="Xcode's package setup screen for adding the Sentry SDK to the application target" width="800" height="523"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Check the SDK's compatibility with your deployment target before selecting a version. Sentry's manual setup currently documents iOS 15 or later for Cocoa SDK 9.x, and SDK 8.x for iOS 11–14.&lt;/p&gt;
&lt;h2&gt;
  
  
  Initialize it&amp;nbsp;early
&lt;/h2&gt;

&lt;p&gt;My app initializer creates a SwiftData ModelContainer. If the cloud-backed configuration fails, it attempts a local configuration. If both attempts fail, the current code calls fatalError.&lt;/p&gt;

&lt;p&gt;That is a useful reminder about initialization order: starting crash reporting when the first view appears would miss the opportunity to observe failures earlier in startup.&lt;/p&gt;

&lt;p&gt;The following is a partial initializer. Add it at the beginning of the existing app initialization and preserve the rest of your setup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;import&lt;/span&gt; &lt;span class="kt"&gt;Sentry&lt;/span&gt;

&lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;SentrySDK&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
        &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dsn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"YOUR_PROJECT_DSN"&lt;/span&gt;
        &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sendDefaultPii&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;

        &lt;span class="cp"&gt;#if DEBUG&lt;/span&gt;
        &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"development"&lt;/span&gt;
        &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;debug&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
        &lt;span class="cp"&gt;#else&lt;/span&gt;
        &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"production"&lt;/span&gt;
        &lt;span class="cp"&gt;#endif&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Continue the existing notification and ModelContainer setup here.&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Initialize on the main thread as early as practical. In a SwiftUI app, Sentry documents using the App initializer. Its debug option prints SDK diagnostics; it does not switch crash reporting on. &lt;/p&gt;

&lt;p&gt;The environment names above are a simple starting point. A Release build can also be a staging or TestFlight build, so use an explicit build configuration if those need separate environments.&lt;/p&gt;

&lt;p&gt;Performance tracing and session replay are separate features. This guide focuses on crash reporting, so it does not enable them as part of the setup.&lt;/p&gt;




&lt;h2&gt;
  
  
  Give Sentry the matching debug&amp;nbsp;symbols
&lt;/h2&gt;

&lt;p&gt;Compiled code does not give you the source-level view you expect in Xcode. Debug symbols allow a crash address to be mapped back to a function and, when available, a file and line. This process is called symbolication.&lt;/p&gt;

&lt;p&gt;The binary and its companion dSYM must have matching build UUIDs. The same app version number is not enough. The main executable, frameworks, and extensions can each have their own symbols.&lt;/p&gt;

&lt;p&gt;In the target's Build Settings, check &lt;strong&gt;Debug Information Format&lt;/strong&gt;. For the configuration you are testing or distributing, use &lt;strong&gt;DWARF with dSYM File&lt;/strong&gt; if you need a separate dSYM to upload. Debug normally places symbols in the binary; Release normally produces the companion file.&lt;/p&gt;

&lt;p&gt;Your app target: Build Settings → Debug Information Format&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj44i6uajpxeq2hrj4xmr.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj44i6uajpxeq2hrj4xmr.webp" alt="Your app's Build Settings in Xcode, showing Debug Information Format set to DWARF for Debug and DWARF with dSYM File for Release" width="800" height="127"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Add the temporary crash button from the next section before building, so its code belongs to the binary whose symbols you upload.&lt;/p&gt;

&lt;p&gt;For a development build, use &lt;strong&gt;Product → Build&lt;/strong&gt;, find the app in Products, and choose &lt;strong&gt;Show in Finder&lt;/strong&gt;. Locate its companion dSYM in the build products. For a distribution build, retain the Xcode archive and its dSYMs.&lt;/p&gt;

&lt;p&gt;Install the symbol-upload utility and authenticate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;brew install getsentry/tools/sentry-cli
sentry-cli login
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These commands use sentry-cli, the debug-file upload utility, rather than the separate sentry developer CLI.&lt;/p&gt;

&lt;p&gt;Upload the symbols for that exact build:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sentry-cli debug-files upload \
  --org YOUR_ORGANIZATION_SLUG \
  --project YOUR_PROJECT\
  "/path/to/YOUR_APP.app.dSYM"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use the actual organization and project slugs. Check the command result and the project's Debug Files settings. Do not assume that generating a dSYM also uploaded it. &lt;/p&gt;

&lt;p&gt;If a report stays partially symbolicated, compare the required UUID with the dSYM you uploaded:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;dwarfdump --uuid "/path/to/YOUR_APP.app.dSYM"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Apple explains how to locate and compare the corresponding binary UUID in its &lt;a href="https://medium.com/r?url=https%3A%2F%2Fdeveloper.apple.com%2Fdocumentation%2Fxcode%2Flocating-a-missing-debug-symbol-file" rel="noopener noreferrer"&gt;missing-symbol guide&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;For ongoing releases, automate symbol uploads in CI or your build workflow and retain the archives. Rebuilding the app can produce a different binary and matching dSYM pair.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verify capture with a deliberate crash
&lt;/h2&gt;

&lt;p&gt;Adding the package and getting a successful build are not evidence that a crash reaches the dashboard.&lt;/p&gt;

&lt;p&gt;Temporarily place this button in a development-only screen that is reachable after launch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="cp"&gt;#if DEBUG&lt;/span&gt;
&lt;span class="kt"&gt;Button&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Test crash reporting"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;SentrySDK&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;crash&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="cp"&gt;#endif&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The screen also needs the usual SwiftUI and Sentry imports. Keep the crash behind a deliberate action. Putting it in the app initializer creates a launch crash loop and can prevent the next launch from delivering the saved report.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Build and install the app with the temporary button included.&lt;/li&gt;
&lt;li&gt;Stop the Xcode run session.&lt;/li&gt;
&lt;li&gt;Open the installed app from the device's home screen, without the debugger attached.&lt;/li&gt;
&lt;li&gt;Tap the button and let the app terminate.&lt;/li&gt;
&lt;li&gt;Reopen the app so the SDK can send the saved crash report.&lt;/li&gt;
&lt;li&gt;Check the correct project and environment in Sentry and allow time for processing.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Sentry specifically requires testing crashes without an attached debugger. A captured message or handled error can test event delivery, but it does not prove that the crash handler works. &lt;/p&gt;

&lt;p&gt;Remove the temporary button when finished. It has no place in a shipping build.&lt;/p&gt;

&lt;h2&gt;
  
  
  Read the report as an investigation
&lt;/h2&gt;

&lt;p&gt;Start with the event's context: the release and build, environment, device, operating system, and when it happened. Confirm that you are inspecting your test rather than an unrelated event.&lt;/p&gt;

&lt;p&gt;Then look at the failure and crashed thread. Find the first relevant frame in your own code and follow the callers around it. A system frame at the top does not automatically mean the operating system caused the bug.&lt;/p&gt;

&lt;p&gt;Use breadcrumbs as supporting context when available. They are a partial trail of recorded activity, not a complete recording of the user's session.&lt;/p&gt;

&lt;p&gt;Finally, look across events in the issue. Is it limited to a release or OS version? Does it affect a particular path? The stack trace helps locate the failure; the surrounding evidence helps formulate a reproduction.&lt;/p&gt;

&lt;p&gt;If the report is unreadable, fix symbolication first. Guessing from hexadecimal addresses is a poor substitute for supplying the matching symbols.&lt;/p&gt;




&lt;h2&gt;
  
  
  Use an agent to help with the investigation
&lt;/h2&gt;

&lt;p&gt;Once an issue exists, an agent can help gather the evidence and inspect the relevant code. Sentry's hosted MCP server supports OAuth and can be scoped to a project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;https://mcp.sentry.dev/mcp/YOUR_ORGANIZATION_SLUG/YOUR_APP&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add it through your client's supported MCP settings and complete authentication. Follow the current client instructions rather than guessing an endpoint from the app's DSN region. Official Sentry MCP setup&lt;/p&gt;

&lt;p&gt;For example, ask the agent:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Inspect this  issue. Identify the release, failure, and relevant application frames. Compare the report with the source code, propose a reproduction, and explain the smallest correction. Separate observed facts from hypotheses.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A Notion connection can help record the issue URL, reproduction, diagnosis, fix, and validation. &lt;strong&gt;Access to Sentry alone does not give the agent access to Notion (and that's only an advice I like to give, since it's my workflow!)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Review the proposed change and test the reproduction. Resolving a ticket or issue changes its tracking state; it does not deploy a corrected app to users.&lt;/p&gt;




&lt;h2&gt;
  
  
  Keep crash context useful and&amp;nbsp;limited
&lt;/h2&gt;

&lt;p&gt;A crash report should help diagnose a failure without becoming a copy of a your users data.&lt;/p&gt;

&lt;p&gt;Disabling default PII collection is a starting point, not a guarantee that custom breadcrumbs or error messages contain no sensitive data. Review what you attach. Sentry supports beforeSend and beforeBreadcrumb hooks for filtering event and breadcrumb data. &lt;/p&gt;

&lt;p&gt;Useful context can often be expressed as a state such as "empty summary" rather than raw records. If you want a reliable affected-user count, define an appropriate identification strategy; do not invent a user count from the number of events.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make the workflow repeatable
&lt;/h2&gt;

&lt;p&gt;For each release, keep the chain intact: initialize reporting early, identify the environment and build, upload matching symbols, and verify the event path. When an issue arrives, use its evidence to reproduce, fix, test, and follow the result after the updated app is released.&lt;/p&gt;

&lt;p&gt;Sentry does not correct the bug for you. It gives you a better starting point than "it crashed on someone else's phone."&lt;br&gt;
That is the part I want from crash reporting: enough context to turn a vague symptom into a concrete investigation.&lt;/p&gt;

&lt;p&gt;That's it for now, have fun debugging!&lt;/p&gt;

</description>
      <category>programming</category>
      <category>softwaredevelopment</category>
      <category>ios</category>
      <category>swift</category>
    </item>
    <item>
      <title>HandKit: Controlling My HomeKit Lights With Hand Gestures on iOS</title>
      <dc:creator>Carolane Lefebvre</dc:creator>
      <pubDate>Sat, 10 Oct 2026 09:22:24 +0000</pubDate>
      <link>https://dev.to/carolanelfbv/handkit-controlling-my-homekit-lights-with-hand-gestures-on-ios-2fk8</link>
      <guid>https://dev.to/carolanelfbv/handkit-controlling-my-homekit-lights-with-hand-gestures-on-ios-2fk8</guid>
      <description>&lt;p&gt;&lt;a href="https://github.com/CarolaneLFBV/handkit" rel="noopener noreferrer"&gt;Open sourced project here&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It started with a question that was probably not worth spending two days on:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Could I use my fingers to control the lights in my room from a distance?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I didn't know much about computer vision. I had barely touched AVFoundation, Vision, or Core Media. I had never built a gesture recognition system.&lt;br&gt;
Which, naturally, made me want to try it.&lt;/p&gt;

&lt;p&gt;The idea behind HandKit was simple: point my iPhone's front camera at myself, recognize a few hand gestures, and translate them into actions in my HomeKit home.&lt;/p&gt;

&lt;p&gt;The first two interactions I wanted were: pinch my thumb and index finger together to toggle a light. Move my index finger horizontally to control brightness from 0 to 100%.&lt;/p&gt;

&lt;p&gt;The final interaction looks almost trivial. A pinch. A lamp turns on. But getting from a raw camera feed to an intentional, reliable gesture turned out to involve camera pipelines, pixel buffers, coordinate systems, noisy measurements, a little geometry, signal filtering, state machines, hysteresis, Swift Concurrency, and finally HomeKit.&lt;/p&gt;

&lt;p&gt;This article is a walkthrough of how I built it, but more importantly, why the architecture and algorithms evolved the way they did.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://youtu.be/iRq3DwV4OeA?si=yYxLRlBt8HiPX_32" rel="noopener noreferrer"&gt;The result&lt;/a&gt;&lt;/p&gt;



&lt;p&gt;Starting with the camera&lt;/p&gt;

&lt;p&gt;Before detecting a hand, I needed access to something much simpler: images.&lt;/p&gt;

&lt;p&gt;On iOS, that starts with AVFoundation.&lt;br&gt;
The capture pipeline can be simplified to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AVCaptureDevice
        ↓
AVCaptureDeviceInput
        ↓
AVCaptureSession
        │
        ├────────→ AVCaptureVideoPreviewLayer
        │                   ↓
        │                 Preview
        │
        └────────→ AVCaptureVideoDataOutput
                            ↓
                       Video frames
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each component has a different responsibility.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;AVCaptureDevice&lt;/code&gt; represents the physical capture device. In my case, the iPhone's front-facing camera.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;AVCaptureDeviceInput&lt;/code&gt; turns that device into an input that can be attached to a capture session. &lt;/p&gt;

&lt;p&gt;&lt;code&gt;AVCaptureSession&lt;/code&gt; coordinates the capture pipeline.&lt;/p&gt;

&lt;p&gt;From there, I needed the video in two different places. The first path goes to an &lt;code&gt;AVCaptureVideoPreviewLayer&lt;/code&gt;, so I can see the camera feed on screen. The second goes to an &lt;code&gt;AVCaptureVideoDataOutput&lt;/code&gt;, because displaying the video isn't enough. I need access to the individual frames if I want to analyze what my hand is doing.&lt;/p&gt;

&lt;p&gt;That distinction became the foundation of HandKit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Camera
  │
  ├──→ human sees the preview
  │
  └──→ application analyzes the frames
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Managing the capture session with Swift Concurrency
&lt;/h2&gt;

&lt;p&gt;I didn't want camera lifecycle operations scattered throughout the UI, so I introduced a &lt;code&gt;CameraManager&lt;/code&gt;. It owns the capture session and is responsible for configuring, starting, and stopping it.&lt;br&gt;
I implemented it as an actor.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;actor&lt;/span&gt; &lt;span class="kt"&gt;CameraManager&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;AVCaptureSession&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Actors are useful here because they provide isolation around mutable state.&lt;br&gt;
One thing I had to properly understand while building this was that an actor is not a thread.&lt;br&gt;
An actor defines an isolation domain. Its executor determines where jobs isolated to that actor execute.&lt;br&gt;
For the capture session, I wanted serialized execution on a dedicated queue, so I gave &lt;code&gt;CameraManager&lt;/code&gt; a custom serial executor.&lt;br&gt;
Conceptually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CameraManager actor
        ↓
Custom SerialExecutor
        ↓
camera.session queue
        ↓
AVCaptureSession
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A simplified version looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;actor&lt;/span&gt; &lt;span class="kt"&gt;CameraManager&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;sessionQueue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
        &lt;span class="kt"&gt;DispatchSerialQueue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"camera.session"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="kd"&gt;nonisolated&lt;/span&gt; &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;unownedExecutor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;UnownedSerialExecutor&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;sessionQueue&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;asUnownedSerialExecutor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gave the camera pipeline a clear execution context without turning the &lt;code&gt;MainActor&lt;/code&gt; into the place where capture work happens.&lt;br&gt;
I also kept configuration inside the manager.&lt;/p&gt;

&lt;p&gt;From outside, starting the camera shouldn't require knowing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;configure first&lt;/li&gt;
&lt;li&gt;then start&lt;/li&gt;
&lt;li&gt;but don't configure twice&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is an internal invariant.&lt;br&gt;
So &lt;code&gt;CameraManager&lt;/code&gt; tracks whether the session has already been configured and exposes a higher-level &lt;code&gt;start()&lt;/code&gt; operation.&lt;br&gt;
This was one of the first architectural principles that kept appearing throughout the project:&lt;br&gt;
The caller should express an intention, not reproduce the internal procedure required to achieve it.&lt;/p&gt;
&lt;h2&gt;
  
  
  Keeping session management and frame processing separate
&lt;/h2&gt;

&lt;p&gt;The next step was receiving frames from &lt;code&gt;AVCaptureVideoDataOutput&lt;/code&gt;.&lt;br&gt;
AVFoundation provides them through &lt;code&gt;AVCaptureVideoDataOutputSampleBufferDelegate&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I initially considered making &lt;code&gt;CameraManager&lt;/code&gt; the delegate too, but there was both a technical and architectural reason not to.&lt;br&gt;
The delegate participates in Objective-C's &lt;code&gt;NSObjectProtocol&lt;/code&gt;, while my camera manager is an actor. More importantly, managing the lifecycle of the camera and processing every video frame are different responsibilities.&lt;/p&gt;

&lt;p&gt;So I introduced &lt;code&gt;VideoOutputDelegate&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CameraManager
│
├── configure / start / stop
├── AVCaptureSession
│
└── VideoOutputDelegate
          ↓
       frames
          ↓
        Vision
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I also separated their execution queues.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;camera.session
├── configure
├── start
└── stop

frame.session
├── frame
├── frame
├── frame
└── Vision processing
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This matters because analyzing an image can take time. I don't want that work unnecessarily blocking operations on the capture session itself.&lt;/p&gt;

&lt;p&gt;It also gave the code a much cleaner conceptual boundary:&lt;br&gt;
&lt;code&gt;CameraManager&lt;/code&gt; manages capture. &lt;code&gt;VideoOutputDelegate&lt;/code&gt; interprets what was captured.&lt;/p&gt;

&lt;p&gt;A video is just a lot of images arriving very&amp;nbsp;quickly&lt;br&gt;
The delegate eventually receives:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;captureOutput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="nv"&gt;output&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;AVCaptureOutput&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;didOutput&lt;/span&gt; &lt;span class="nv"&gt;sampleBuffer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;CMSampleBuffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;from&lt;/span&gt; &lt;span class="nv"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;AVCaptureConnection&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important value here is the &lt;code&gt;CMSampleBuffer&lt;/code&gt;.&lt;br&gt;
I initially thought of a camera frame as essentially an image, but the media pipeline is richer than that.&lt;/p&gt;

&lt;p&gt;A &lt;code&gt;CMSampleBuffer&lt;/code&gt; is a container for a media sample. For video, it carries the sample and associated information such as timing and format metadata.&lt;/p&gt;

&lt;p&gt;For Vision, what I really needed was the image buffer inside it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="k"&gt;guard&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;pixelBuffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="kt"&gt;CMSampleBufferGetImageBuffer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sampleBuffer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That gives me a &lt;code&gt;CVPixelBuffer&lt;/code&gt;. The pipeline had now become:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Camera
  ↓
AVCaptureVideoDataOutput
  ↓
CMSampleBuffer
  ↓
CVPixelBuffer
  ↓
Vision
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is no need to turn every frame into a &lt;code&gt;UIImage&lt;/code&gt;. The pixel buffer can go directly into Vision.&lt;/p&gt;

&lt;h2&gt;
  
  
  Asking Vision to find a&amp;nbsp;hand
&lt;/h2&gt;

&lt;p&gt;Apple's Vision framework provides &lt;code&gt;VNDetectHumanHandPoseRequest&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Instead of recreating the request for every frame, I keep one instance around and ask it to detect at most one hand.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;handPoseRequest&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;VNDetectHumanHandPoseRequest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;request&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;VNDetectHumanHandPoseRequest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;maximumHandCount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;
&lt;span class="p"&gt;}()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For each pixel buffer, I create a request handler:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;handler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;VNImageRequestHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nv"&gt;cvPixelBuffer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pixelBuffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nv"&gt;orientation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="k"&gt;right&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nv"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[:]&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perform&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;handPoseRequest&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once Vision detects a hand, it returns a &lt;code&gt;VNHumanHandPoseObservation&lt;/code&gt;. From that observation, I can request specific landmarks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;indexTip&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="n"&gt;hand&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;recognizedPoint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;indexTip&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;thumbTip&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="n"&gt;hand&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;recognizedPoint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;thumbTip&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Vision doesn't return screen pixels. The locations are normalized coordinates, which makes them independent of a specific image resolution. So now I had something incredibly useful:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;index fingertip → (x, y)
thumb fingertip → (x, y)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That sounds like enough to build gestures. Well it wasn't lol&lt;/p&gt;

&lt;h2&gt;
  
  
  Vision gives estimates, not&amp;nbsp;truth tho
&lt;/h2&gt;

&lt;p&gt;Each recognized point also has a confidence value. Very quickly, I discovered why.&lt;br&gt;
I printed the index coordinates while moving my finger around. When Vision was confident, I would get values like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;x: 0.578
y: 0.300
confidence: 0.872

x: 0.577
y: 0.311
confidence: 0.867

x: 0.575
y: 0.328
confidence: 0.855
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But occasionally the tracking would produce a large jump with very low confidence.&lt;/p&gt;

&lt;p&gt;If I interpreted every coordinate as equally trustworthy, a tracking mistake could look like an extremely fast gesture.&lt;br&gt;
My first filter therefore became:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="k"&gt;guard&lt;/span&gt; &lt;span class="n"&gt;indexTip&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;confidence&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.7&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;0.7 isn't a universal Vision constant. It was an experimental starting point based on the data I was observing.&lt;/p&gt;

&lt;p&gt;That distinction matters throughout HandKit: most thresholds aren't presented as magic values that should work for everybody. They're values I measured and tuned while experimenting with my own setup.&lt;br&gt;
But confidence filtering wasn't enough.&lt;/p&gt;

&lt;p&gt;Even when Vision was confident and my finger looked completely still to me, its coordinates continued to move slightly.&lt;/p&gt;

&lt;p&gt;That led to the first genuinely interesting algorithmic problem.&lt;/p&gt;
&lt;h2&gt;
  
  
  How do you measure movement when "not moving" still&amp;nbsp;moves?
&lt;/h2&gt;

&lt;p&gt;To know whether my finger is moving, I need more than its current position. I need its previous one. For two consecutive measurements:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;previous = (x₁, y₁)
current  = (x₂, y₂)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I calculate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Δ x = x_2 - x_1
Δ y = y_2 - y_1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But I also wanted one value representing how much the point moved regardless of direction.&lt;/p&gt;

&lt;p&gt;That's just the hypotenuse of the triangle formed by the two deltas:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;d = sqrt((Δ x)^2 + (Δ y)^2)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Swift already gives me exactly that operation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;movementDistance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;hypot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;deltaX&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;deltaY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This was my first little return to the Pythagorean theorem for a project involving smart lights. The interesting discovery came when I kept my index as still as possible. movementDistance wasn't zero.&lt;/p&gt;

&lt;p&gt;I saw values around:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;0.001
0.003
0.002
0.005
0.001
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and occasional larger spikes. The system needed to distinguish actual intentional movement from natural tracking jitter.&lt;/p&gt;

&lt;p&gt;A single frame wasn't enough to make that decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  Filtering the tracking&amp;nbsp;signal
&lt;/h2&gt;

&lt;p&gt;Instead of considering one movement measurement at a time, I started keeping a sliding window containing the five most recent distances.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="err"&gt;d&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;d&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;d&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;d&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;d&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When a sixth arrives:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="err"&gt;d&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;d&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;d&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;d&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;d&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;d&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I remove the oldest:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="err"&gt;d&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;d&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;d&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;d&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;d&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In Swift:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="n"&gt;recentMovementDistances&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;movementDistance&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;recentMovementDistances&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;recentMovementDistances&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeFirst&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then I calculate the median. For five sorted values:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[0.001, 0.002, 0.003, 0.005, 0.050]
                ↑
              median
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That was useful because an isolated bad measurement such as 0.050 doesn't drag the result upwards as strongly as it would affect an average.&lt;/p&gt;

&lt;p&gt;This is deliberately a very small and simple filter. HandKit isn't trying to be a sophisticated signal-processing library.&lt;/p&gt;

&lt;p&gt;But it was enough to introduce an important idea: &lt;strong&gt;a noisy measurement becomes much more useful when you reason about it over time instead of treating every sample independently.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And that observation led directly to state machines.&lt;/p&gt;

&lt;h2&gt;
  
  
  Detecting movement with hysteresis
&lt;/h2&gt;

&lt;p&gt;My first instinct could have been:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;movement &amp;gt; threshold → moving
movement &amp;lt; threshold → idle
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But imagine the signal oscillating around that threshold:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;0.0069
0.0071
0.0068
0.0072
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The state could rapidly become:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;IDLE
MOVING
IDLE
MOVING
IDLE
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead, I introduced two different thresholds. From my experiments, I started with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;median &amp;gt; 0.01  → start moving
median &amp;lt; 0.005 → become idle
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Between them, I keep the current state.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                 &amp;gt; 0.01
        ┌───────────────────────┐
        │                       ↓
      IDLE                   MOVING
        ↑                       │
        └───────────────────────┘
                 &amp;lt; 0.005
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is hysteresis. The gap between the two thresholds prevents the state from constantly flipping when the signal sits near a boundary.&lt;br&gt;
In code, I represent the state explicitly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;enum&lt;/span&gt; &lt;span class="kt"&gt;MovementState&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;idle&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;moving&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That small choice became useful because I wasn't really trying to answer:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Is there movement in this exact frame?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I was answering:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Has an intentional movement started, and are we currently inside it?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is a temporal question.&lt;/p&gt;

&lt;h2&gt;
  
  
  A gesture is something that happens over&amp;nbsp;time
&lt;/h2&gt;

&lt;p&gt;Originally, I started thinking about recognizing a swipe. A single position obviously can't represent a swipe.Even two consecutive positions aren't enough. A gesture has a beginning, a trajectory, and an end.&lt;/p&gt;

&lt;p&gt;So when transitioning from&amp;nbsp;&lt;code&gt;.idle&lt;/code&gt; to&amp;nbsp;&lt;code&gt;.moving&lt;/code&gt;, I store:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;startPosition
startTime
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;using a &lt;code&gt;ContinuousClock&lt;/code&gt; for elapsed time measurement.&lt;br&gt;
When movement eventually ends, I can compare:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;startPosition → endPosition
startTime     → endTime
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and derive:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;totalDeltaX = endX - startX
totalDeltaY = endY - start
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;plus the gesture duration.&lt;/p&gt;

&lt;p&gt;This gave me the information required to reason about a swipe:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Did it travel far enough horizontally?&lt;/li&gt;
&lt;li&gt;Did it remain reasonably horizontal?&lt;/li&gt;
&lt;li&gt;Did it happen quickly enough?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But testing this exposed a completely different bug. &lt;/p&gt;

&lt;h2&gt;
  
  
  The horizontal gesture that somehow moved vertically
&lt;/h2&gt;

&lt;p&gt;I performed horizontal movements and looked at my data.&lt;/p&gt;

&lt;p&gt;Instead of getting:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;|Δx| &amp;gt;&amp;gt; |Δy|
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I repeatedly got large changes on Y. Then I performed an intentionally vertical gesture. This time X changed dramatically. My coordinate system was effectively rotated relative to what I was seeing.&lt;br&gt;
The culprit was this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;orientation&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.up&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I had initially told Vision to interpret the pixel buffer as if it were already oriented upright.&lt;br&gt;
The preview looked fine, which made this easy to miss.&lt;br&gt;
But the orientation of what the user sees and the orientation in which Vision interprets the raw image buffer aren't necessarily the same thing.&lt;br&gt;
I tested&amp;nbsp;&lt;code&gt;.right&lt;/code&gt;. Then repeated the horizontal gestures. This time I got measurements such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Δx = -0.217
Δy = +0.013

Δx = +0.263
Δy = -0.040

Δx = -0.278
Δy = +0.018
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Exactly what I wanted:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;|Δx| &amp;gt;&amp;gt; |Δy|
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I also explicitly tested direction with the front-facing camera.&lt;br&gt;
In my current configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Δx &amp;gt; 0 → visual movement towards the left
Δx &amp;lt; 0 → visual movement towards the right
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Again, I preferred measuring the behavior rather than assuming what mirroring would do.&lt;/p&gt;

&lt;h2&gt;
  
  
  Realizing I didn't actually want a&amp;nbsp;swipe (yes, kinda late to think that)
&lt;/h2&gt;

&lt;p&gt;While designing the swipe, I realized something else.&lt;br&gt;
For brightness control, a swipe is the wrong interaction.&lt;br&gt;
A swipe is a discrete gesture:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;gesture
   ↓
recognized
   ↓
perform action once
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's great for something like &lt;code&gt;swipe → next track&lt;/code&gt; &lt;br&gt;
Brightness is continuous.&lt;br&gt;
What I actually wanted was a gesture-controlled slider:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;finger starts moving
        ↓
keep tracking horizontal position
        ↓
brightness changes continuously
        ↓
finger stops
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So the same&amp;nbsp;&lt;code&gt;.idle /&amp;nbsp;.moving&lt;/code&gt; state machine remained useful, but the meaning of the moving state changed.&lt;br&gt;
Instead of waiting for the gesture to end before doing something, I calculate brightness continuously while movement is active.&lt;br&gt;
There's another subtle decision here.&lt;/p&gt;

&lt;p&gt;I could update brightness using each frame's delta:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;brightness&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;currentDelta&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But then tracking errors can accumulate over time.&lt;br&gt;
Instead, at the beginning of the gesture I store:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;startPosition
startBrightness
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every subsequent value is calculated relative to those fixed references:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Δ x = currentX - startX
brightness = startBrightness + f(Δ x)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This means that if I move away and then return exactly to my starting position, I should return to the original brightness regardless of the intermediate path.&lt;br&gt;
That avoids cumulative drift.&lt;/p&gt;

&lt;p&gt;The current prototype maps the normalized horizontal displacement to a value between 0 and 100 and clamps the result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;newBrightness&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;startBrightness&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;brightnessDelta&lt;/span&gt;
&lt;span class="n"&gt;brightness&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;newBrightness&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Detecting a pinch with&amp;nbsp;geometry
&lt;/h2&gt;

&lt;p&gt;The second gesture was simpler conceptually. I already had the index fingertip. Vision can also give me the thumb tip.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;indexTip&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="n"&gt;hand&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;recognizedPoint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;indexTip&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;thumbTip&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="n"&gt;hand&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;recognizedPoint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;thumbTip&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After applying the same confidence filtering, I have two normalized points.&lt;br&gt;
So detecting how close the fingers are is again a geometry problem:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Δ x = indexX - thumbX
Δ y = indexY - thumbY
distance = sqrt((Δ x)^2 + (Δ y)^2)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;distance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;hypot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;deltaX&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;deltaY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The confidence check needs to cover both points. A reliable index position doesn't help if the thumb position is uncertain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="k"&gt;guard&lt;/span&gt; &lt;span class="n"&gt;indexTip&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;confidence&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;thumbTip&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;confidence&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.7&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;deltaX&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;indexTip&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;location&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;thumbTip&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;location&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;deltaY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;indexTip&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;location&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;thumbTip&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;location&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;distance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;hypot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;deltaX&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;deltaY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This time, the distance compares two landmarks in the same frame. Earlier, I used the same calculation to compare one landmark across consecutive frames. The geometry is identical; the question is different.&lt;/p&gt;

&lt;p&gt;Before deciding what counted as a pinch, I measured the distance while opening and closing my fingers. In my setup, the observations were roughly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Fingers apart:     0.15 to 0.21
Fingers touching:  0.005 to 0.015
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Contact didn't produce a perfect zero. Vision estimates the locations of landmarks in a two-dimensional image, and those estimates still contain noise when the fingers touch.&lt;/p&gt;

&lt;p&gt;These are distances in normalized image coordinates, not centimeters or measurements of physical contact. Normalizing the coordinates removes the dependence on pixel resolution; it doesn't make the measurement independent of hand size, camera distance, or image geometry. Moving my hand farther from the camera changes its apparent size and therefore the distance between the detected points.&lt;/p&gt;

&lt;p&gt;The measurements were enough to establish a useful threshold for this prototype, but they weren't a general calibration for every hand and camera setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  A pinch needs memory&amp;nbsp;too
&lt;/h2&gt;

&lt;p&gt;The movement detector had already taught me what happens when a noisy measurement sits near one threshold.&lt;br&gt;
For the pinch, I reused hysteresis with a separate state machine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;enum&lt;/span&gt; &lt;span class="kt"&gt;PinchState&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="kd"&gt;open&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;pinched&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The rules became:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;OPEN    -- distance &amp;lt; 0.015 --&amp;gt; PINCHED
PINCHED -- distance &amp;gt; 0.03  --&amp;gt; OPEN
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Between the two thresholds, I preserve the current state. At either exact boundary, the strict comparisons also leave the state unchanged.&lt;/p&gt;

&lt;p&gt;That gap matters. After recognizing a pinch, I don't want a small increase from 0.014 to 0.016 to mean that the hand has opened again. The fingers have to separate more clearly before another pinch can begin.&lt;/p&gt;

&lt;p&gt;But there was one more distinction to make: the state and the event are different things.&lt;br&gt;
If I called the toggle action on every frame where the distance was small enough, holding my fingers together would repeatedly switch the lamp on and off.&lt;br&gt;
The event I care about is the transition:&lt;br&gt;
&lt;code&gt;OPEN -&amp;gt; PINCHED -&amp;gt; emit one toggle&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The core logic can be expressed as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="k"&gt;switch&lt;/span&gt; &lt;span class="n"&gt;pinchState&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;open&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;distance&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;0.015&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;pinchState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pinched&lt;/span&gt;
        &lt;span class="nf"&gt;onToggle&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;pinched&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;distance&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;0.03&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;pinchState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;open&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once the state becomes&amp;nbsp;&lt;code&gt;.pinched&lt;/code&gt;, subsequent frames stay in that branch until the fingers reopen. Releasing the pinch rearms the interaction; it doesn't toggle the light.&lt;/p&gt;

&lt;p&gt;This gave me a specific behavior to test: pinch, hold, release, pinch again. Holding should produce no additional action, and the second pinch should produce exactly one new action.&lt;/p&gt;

&lt;p&gt;Hysteresis stabilizes the transition, but it doesn't solve every tracking problem. For example, losing the hand while it is pinched raises a separate question about when to reset or rearm the recognizer. That is part of making the experiment more robust, beyond this first working interaction.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding the actual light with&amp;nbsp;HomeKit
&lt;/h2&gt;

&lt;p&gt;At this point, the gesture pipeline could produce a toggle event. I still needed to connect that event to something outside the phone.&lt;br&gt;
&lt;code&gt;HomeKit&lt;/code&gt; introduced another hierarchy to understand:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;HMHomeManager
  |
  +-- HMHome
        |
        +-- HMAccessory
              |
              +-- HMService (Lightbulb)
                    |
                    +-- HMCharacteristic (PowerState)
                    +-- HMCharacteristic (Brightness)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;HMHomeManager&lt;/code&gt; provides access to the configured homes. Each home contains accessories, and each accessory exposes services. The values I can inspect or change belong to the characteristics of those services. &lt;/p&gt;

&lt;p&gt;Apple's &lt;code&gt;HMCharacteristic&lt;/code&gt; documentation describes that final layer.&lt;/p&gt;

&lt;p&gt;I began by inspecting what my own home actually exposed.&lt;br&gt;
Within the service, the two characteristic types I needed were:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;HMCharacteristicTypePowerState&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;HMCharacteristicTypeBrightness&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;PowerState&lt;/code&gt; represents whether the light is on. &lt;code&gt;Brightness&lt;/code&gt; represents its brightness as an integer percentage of the maximum, as defined by Apple's brightness characteristic documentation.&lt;/p&gt;

&lt;p&gt;Finding those characteristics didn't yet prove that I could control the lamp. I first asked HomeKit to read their values.&lt;br&gt;
The actual output was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;BRIGHTNESS: 100
POWER: 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now I knew that I had found the intended accessory and could read its state: the lamp was on, with brightness at 100%.&lt;br&gt;
Testing one layer at a&amp;nbsp;time&lt;/p&gt;

&lt;p&gt;It was tempting to connect the pinch immediately. Instead, I worked through four small steps:&lt;br&gt;
&lt;code&gt;Read state -&amp;gt; Manual write -&amp;gt; Temporary button -&amp;gt; Gesture&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The first read established that discovery worked.&lt;/p&gt;
&lt;h2&gt;
  
  
  Giving HomeKit its own controller
&lt;/h2&gt;

&lt;p&gt;I put &lt;code&gt;HomeKit&lt;/code&gt; discovery and control in &lt;code&gt;LightController&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Its job was to find Lamp, retain the characteristics the app needed, and expose an operation such as toggle() to the rest of the application.&lt;/p&gt;

&lt;p&gt;The references begin as optional values because HomeKit discovery is asynchronous:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;powerCharacteristic&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;HMCharacteristic&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;
&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;brightnessCharacteristic&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;HMCharacteristic&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;They refer to live &lt;code&gt;HomeKit&lt;/code&gt; objects. They aren't preferences to persist in UserDefaults, and keeping them avoids traversing the entire home hierarchy on every gesture.&lt;br&gt;
Once discovery had found the power characteristic, toggle() followed this sequence:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Power characteristic available?
          |
          v
Read current value
          |
          v
Convert to Bool and invert
          |
          v
Write new value
          |
          v
Check completion for an error
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I read the value before toggling because the lamp could also be changed from the Home app or another controller. A local &lt;code&gt;Boolean&lt;/code&gt; would only describe what HandKit last believed, which might no longer match the accessory.&lt;/p&gt;

&lt;p&gt;There was also an Objective-C interoperability detail: &lt;code&gt;HMCharacteristic.value&lt;/code&gt; is exposed as &lt;code&gt;Any?&lt;/code&gt;. In my tests, the power value arrived as an &lt;code&gt;NSNumber&lt;/code&gt;, so I used its boolValue before inverting it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="k"&gt;guard&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;powerCharacteristic&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="k"&gt;as?&lt;/span&gt; &lt;span class="kt"&gt;NSNumber&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;newState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;boolValue&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A read followed by a write still isn't an atomic toggle. Another command could arrive between them, and rapid gestures could overlap requests. Serializing device commands is a further robustness step; the working demo doesn't establish that all concurrent-control cases are handled.&lt;/p&gt;

&lt;p&gt;At this snapshot, retaining the brightness characteristic prepared the second interaction. It did not mean that the gesture-controlled brightness value was already being written to the lamp.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keeping recognition independent from the&amp;nbsp;action
&lt;/h2&gt;

&lt;p&gt;The frame-processing code knows about landmarks, distances, and gesture states. It doesn't need to know what a HomeKit accessory is.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;VideoOutputDelegate&lt;/code&gt; communicates through two callbacks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;onToggle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kd"&gt;@Sendable&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;Void&lt;/span&gt;
&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;onBrightnessChanged&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kd"&gt;@Sendable&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;CGFloat&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;Void&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One emits a discrete event. The other emits a continuously updated value.&lt;br&gt;
&lt;code&gt;CameraManager&lt;/code&gt; receives the toggle callback from the application and passes it to the delegate. The application decides that this event should call &lt;code&gt;LightController.toggle()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That boundary means I can inspect or log the gesture output without a connected lamp. It also means the same pinch recognizer could eventually trigger something else without changing the geometry or thresholds.&lt;/p&gt;

&lt;p&gt;There is still a deliberate prototype-level coupling here: &lt;code&gt;onBrightnessChanged&lt;/code&gt; names the value in terms of the current interaction. It abstracts away &lt;code&gt;HomeKit&lt;/code&gt;, but it isn't a completely generic gesture API. If I extracted this into a reusable component, a normalized slider value or displacement event might be a better contract.&lt;/p&gt;

&lt;p&gt;For HandKit, the useful separation was already clear:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Recognition: What did the hand do?&lt;/li&gt;
&lt;li&gt;Application: What should that mean?&lt;/li&gt;
&lt;li&gt;HomeKit:     How do I ask the lamp to do it?&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Crossing from the frame queue to the &lt;code&gt;MainActor&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;These callbacks also exposed a concurrency boundary. Frames arrive on &lt;code&gt;frameQueue&lt;/code&gt;. The UI-facing model and, in this project, &lt;code&gt;LightController&lt;/code&gt; are &lt;strong&gt;isolated&lt;/strong&gt; to the &lt;code&gt;MainActor&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Calling &lt;code&gt;lightController.toggle()&lt;/code&gt; directly from the synchronous toggle callback produced an isolation error. &lt;br&gt;
The callback needed an explicit handoff:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="nv"&gt;onToggle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;Task&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="kd"&gt;@MainActor&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
        &lt;span class="n"&gt;lightController&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toggle&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The brightness callback uses the same pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="nv"&gt;onBrightnessChanged&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;newBrightness&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
    &lt;span class="kt"&gt;Task&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="kd"&gt;@MainActor&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
        &lt;span class="n"&gt;viewModel&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;brightness&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;newBrightness&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;@Sendable&lt;/code&gt; describes a closure's ability to cross concurrency boundaries with safe captures. It doesn't dispatch the closure onto the main actor. The &lt;code&gt;Task { @MainActor in&amp;nbsp;... }&lt;/code&gt; body establishes that execution context explicitly. &lt;br&gt;
The distinction follows Swift's concurrency and isolation model.&lt;/p&gt;

&lt;p&gt;The values crossing this boundary are small: an event or a brightness number. The pixel buffers, Vision request, and recognition state remain in the frame-processing path.&lt;/p&gt;

&lt;p&gt;Moving the callback onto the main actor also doesn't make the physical device operation synchronous. HomeKit still completes its read and write asynchronously. Actor isolation protects access to application state; it doesn't guarantee that the lamp has already reacted.&lt;/p&gt;

&lt;p&gt;For this prototype, the callback handoff made the two paths explicit. If updates become more frequent than the UI or accessory can use, the next step is to coalesce them rather than create an ever-growing stream of pending work. Independent tasks also shouldn't be used as a guarantee of event ordering.&lt;/p&gt;
&lt;h2&gt;
  
  
  One shared model for the camera and the interface
&lt;/h2&gt;

&lt;p&gt;The on-screen brightness indicator reads from a &lt;code&gt;CameraViewModel&lt;/code&gt; that is both observable and main-actor isolated:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;@MainActor&lt;/span&gt; &lt;span class="kd"&gt;@Observable&lt;/span&gt;
&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="kt"&gt;CameraViewModel&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;brightness&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;CGFloat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This reduced example shows the two separate responsibilities. &lt;br&gt;
&lt;code&gt;@Observable&lt;/code&gt;lets SwiftUI track changes to the state it reads. &lt;code&gt;@MainActor&lt;/code&gt; defines where that mutable state is accessed. Observation alone doesn't establish concurrency isolation.&lt;/p&gt;

&lt;p&gt;There was an equally important ownership detail: ContentView and CameraManager needed the same model instance.&lt;/p&gt;

&lt;p&gt;If each created its own &lt;code&gt;CameraViewModel&lt;/code&gt;, the camera callback could update one object while SwiftUI observed another. The gesture calculations would be correct, but the indicator would never reflect them.&lt;/p&gt;

&lt;p&gt;In the view's initialization, the wiring therefore follows this shape:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;viewModel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;CameraViewModel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;lightController&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;LightController&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_viewModel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;State&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;initialValue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;viewModel&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lightController&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lightController&lt;/span&gt;

    &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cameraManager&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;CameraManager&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nv"&gt;viewModel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;viewModel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nv"&gt;onToggle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kt"&gt;Task&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="kd"&gt;@MainActor&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
                &lt;span class="n"&gt;lightController&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toggle&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is an excerpt of the wiring, with the surrounding stored properties and view body omitted. The important part is that the local viewModel passed to &lt;code&gt;CameraManager&lt;/code&gt; is the same reference retained for the view's state. Inside the manager, the brightness callback updates that reference on the main actor.&lt;/p&gt;

&lt;p&gt;The resulting feedback path is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Vision -&amp;gt; slider value -&amp;gt; callback -&amp;gt; MainActor
                                      |
                                      v
                              CameraViewModel
                                      |
                                      v
                              SwiftUI indicator
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That shared instance is what connects a correct calculation to something visible on screen.&lt;/p&gt;

&lt;h2&gt;
  
  
  The complete architecture
&lt;/h2&gt;

&lt;p&gt;By the end of this iteration, the system looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                          ContentView
                  creates and connects objects
                              |
                              v
                     CameraManager actor
                              |
                    custom SerialExecutor
                              |
                       sessionQueue
                              |
                      AVCaptureSession
                              |
             +----------------+------------------+
             |                                   |
             v                                   v
  AVCaptureVideoPreviewLayer          AVCaptureVideoDataOutput
             |                                   |
             v                                   v
       Camera preview                        frameQueue
                                                 |
                                                 v
                                        VideoOutputDelegate
                                                 |
                                                 v
                                  CMSampleBuffer -&amp;gt; CVPixelBuffer
                                                 |
                                                 v
                                    Vision hand-pose request
                                                 |
                                                 v
                                     Confidence-filtered points
                                                 |
                              +------------------+----------------+
                              |                                   |
                              v                                   v
                     Movement / slider                      Pinch state
                              |                                   |
                              v                                   v
                   onBrightnessChanged(value)                 onToggle()
                              |                                   |
                              v                                   v
                   Task { @MainActor in ... }       Task { @MainActor in ... }
                              |                                   |
                              v                                   v
                     CameraViewModel                       LightController
                 shared with ContentView                          |
                              |                                   v
                              v                           HomeKit PowerState
                    SwiftUI 0-100 indicator                        |
                                                                  v
                                                            Physical Lamp

Next connection, not yet implemented at this snapshot:
slider value -&amp;gt; HomeKit Brightness -&amp;gt; physical light intensity
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The boundaries each answer a practical question. The camera manager owns capture lifecycle. The delegate processes frames and maintains recognition state on its serial queue. The view model exposes UI state. The light controller handles accessory discovery and commands.&lt;/p&gt;

&lt;p&gt;The architecture also makes the unfinished part visible: the brightness path reaches the interface, while the toggle path reaches the physical lamp.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the experiment stands
&lt;/h2&gt;

&lt;p&gt;The pinch now controls a real HomeKit lamp. Bringing my thumb and index finger together triggers one toggle, and holding the pinch doesn't repeatedly switch it.&lt;/p&gt;

&lt;p&gt;The continuous horizontal gesture also works as an on-screen slider. It produces and displays brightness values from 0 to 100 using a fixed starting position and starting brightness.&lt;/p&gt;

&lt;p&gt;At the snapshot covered by this article, that slider was not yet connected to HomeKit's Brightness characteristic. I had discovered and read the characteristic, but the physical brightness-control path was still unfinished.&lt;/p&gt;

&lt;p&gt;Those are two different milestones: recognizing and displaying the intended value, and successfully applying it to an accessory.&lt;/p&gt;

&lt;p&gt;The video captures the part that a console log couldn't. After spending so much time looking at coordinates and state transitions, I brought two fingers together and something in the room changed.&lt;/p&gt;

&lt;p&gt;I laughed. A lot.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I&amp;nbsp;learned
&lt;/h2&gt;

&lt;p&gt;This project made several concepts concrete for me.&lt;br&gt;
Actor isolation and execution are separate concerns. Making CameraManager an actor gave its mutable state an isolation boundary; choosing a serial executor gave its work a deliberate execution context. Neither choice removed the need to think about delegate callbacks and other queues.&lt;/p&gt;

&lt;p&gt;Confidence filtering, median filtering, and hysteresis solve different problems. Confidence rejects uncertain landmarks. The five-sample median limits the influence of isolated movement spikes. Hysteresis prevents repeated state changes near a boundary. Combining them was useful because no single threshold could do all three jobs.&lt;/p&gt;

&lt;p&gt;Filtering also has a cost. A window of recent measurements adds inertia, and per-frame displacement depends on sampling cadence. The numbers that worked in my tests aren't a substitute for testing different frame rates, camera distances, and lighting conditions.&lt;/p&gt;

&lt;p&gt;Gesture recognition requires memory. The movement path needs a beginning and an evolving trajectory; the pinch path needs to know whether the fingers were already together. Once I modeled those states explicitly, the behavior became easier to reason about.&lt;/p&gt;

&lt;p&gt;The transition can be the useful event. A sustained pinch describes a condition. Entering that condition describes an action the application can respond to once.&lt;/p&gt;

&lt;p&gt;A fixed reference simplifies continuous control. Mapping every position back to startPosition and startBrightness avoids accumulating frame-to-frame errors and gives the interaction a predictable return point.&lt;/p&gt;

&lt;p&gt;Object identity matters as much as data flow. The brightness callback and SwiftUI needed to refer to the same model instance. Correct values sent to an unobserved object are still invisible to the user.&lt;/p&gt;

&lt;p&gt;Finally, separating the layers made debugging manageable. A button let me validate HomeKit without a camera gesture. A visual indicator let me validate the brightness calculation without changing a lamp. The callbacks connected those tested pieces.&lt;/p&gt;

&lt;h2&gt;
  
  
  Learning with AI as a&amp;nbsp;mentor
&lt;/h2&gt;

&lt;p&gt;I used AI, including Codex, throughout the experiment as a mentor: to point me toward documentation, explain unfamiliar concepts, and challenge my reasoning when I couldn't explain what I was seeing.&lt;/p&gt;

&lt;p&gt;The learning rule was to work through the challenges myself. I wanted to inspect the measurements, reason about the algorithm, write and test the behavior, and understand the architectural decisions. Explanations and small examples helped me move forward, but having the challenges completed for me would have removed the part I was doing this project to experience.&lt;/p&gt;

&lt;p&gt;The most useful moments were the questions that made me look again: what is actually inside this buffer? Why does a stationary finger still move? Which coordinate system am I measuring? What should happen on the next frame if the fingers are still touching?&lt;br&gt;
That is how a small experiment with a lamp became an excuse to understand much more of the iOS stack.&lt;/p&gt;

&lt;h2&gt;
  
  
  From a small question to a working&amp;nbsp;lamp
&lt;/h2&gt;

&lt;p&gt;I started HandKit because I wanted to know whether I could control my lights with my fingers. I ended this iteration with a working pinch, a brightness interaction I could see on screen, and a much clearer understanding of everything between a camera frame and a physical action.&lt;/p&gt;

&lt;p&gt;The satisfying part was being able to explain that path. I could point to where the pixels arrived, where the landmarks became measurements, where a state transition became an event, and where that event finally reached HomeKit.&lt;/p&gt;

&lt;p&gt;There is still work left. The brightness path needs its final connection, and the gesture logic needs to survive more than the conditions in which I first tuned it.&lt;/p&gt;

&lt;p&gt;But the moment in that first successful video already says why I wanted to build this. I pinched my fingers, the lamp reacted, and I couldn't stop laughing. A question that had sounded a little ridiculous had led me through unfamiliar frameworks, a return to Pythagoras, and a room that now responded to something I had built.&lt;br&gt;
That was a very good reason to spend two days on it.&lt;/p&gt;

&lt;p&gt;Stay hungry, stay foolish!&lt;/p&gt;

</description>
      <category>swift</category>
      <category>ios</category>
      <category>programming</category>
      <category>computervision</category>
    </item>
  </channel>
</rss>
