<?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: Manuel Escrig</title>
    <description>The latest articles on DEV Community by Manuel Escrig (@manuelescrig).</description>
    <link>https://dev.to/manuelescrig</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%2F3560317%2Fdfea59ea-52dc-4aeb-bce8-4c0816d80628.jpg</url>
      <title>DEV Community: Manuel Escrig</title>
      <link>https://dev.to/manuelescrig</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/manuelescrig"/>
    <language>en</language>
    <item>
      <title>Sharing private photos with people who will never install your app</title>
      <dc:creator>Manuel Escrig</dc:creator>
      <pubDate>Tue, 10 Nov 2026 15:42:35 +0000</pubDate>
      <link>https://dev.to/manuelescrig/sharing-private-photos-with-people-who-will-never-install-your-app-iii</link>
      <guid>https://dev.to/manuelescrig/sharing-private-photos-with-people-who-will-never-install-your-app-iii</guid>
      <description>&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%2Fliyvjhbehnx2fvt55v2k.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%2Fliyvjhbehnx2fvt55v2k.png" alt="Our Family Album app icon" width="798" height="173"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Our Family Album is a private photo album for families. Parents post a moment once, and the people they invited see it right away. It launched in July 2026. And the main engineering problem turned out to be one sentence I kept hearing from users: &lt;em&gt;"My parents won't install another app."&lt;/em&gt; 😅&lt;/p&gt;

&lt;h2&gt;
  
  
  Sharing without an app
&lt;/h2&gt;

&lt;p&gt;Version 1.5 added &lt;strong&gt;Share without an app&lt;/strong&gt;. From the invitation screen or an album's menu you can do two things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Create a browser link&lt;/strong&gt; for one person. It opens the album read-only on any device, with no account. Each link is made for a single person, can expire after 30 days, and can be revoked at any time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Invite someone by email.&lt;/strong&gt; Each photo email includes a link to the full album, so relatives can catch up from their inbox.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Browser access is read-only on purpose. Uploading, commenting and reacting stay inside the app, where people have accounts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Permissions belong on the server
&lt;/h2&gt;

&lt;p&gt;The first versions stored some album settings in &lt;code&gt;UserDefaults&lt;/code&gt; on the device. That breaks as soon as two family members change settings on two different phones. So now:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Album permissions come from the server, not from local storage.&lt;/li&gt;
&lt;li&gt;A save waits for the server to confirm it. If it fails, the app restores the previous state and shows the error.&lt;/li&gt;
&lt;li&gt;Uploads, comments, reactions, sharing and exports all check the server's flags.&lt;/li&gt;
&lt;li&gt;Notification settings live on the account, and the server filters what gets delivered. Email is opt-in.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Screenshot protection
&lt;/h2&gt;

&lt;p&gt;An album can block screenshots. When it does, its photos and videos show up black in screenshots and screen recordings, everywhere they appear: the photo viewer, the family feed and the album grid.&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%2Fzvxin5x04rwgfjzud128.jpeg" 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%2Fzvxin5x04rwgfjzud128.jpeg" alt="Our Family Album App Store screenshots" width="799" height="434"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Privacy in analytics
&lt;/h2&gt;

&lt;p&gt;My product analytics follow one rule across my apps: send ids, counts, settings and fixed codes. Never names, emails, typed text or photos. For a family photo app, that rule isn't optional.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Design sharing for the least technical person in the family.&lt;/li&gt;
&lt;li&gt;Every shareable link should be personal, expirable and revocable.&lt;/li&gt;
&lt;li&gt;If several devices can change a setting, the server owns it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://apps.apple.com/us/app/our-family-album/id6790241302" rel="noopener noreferrer"&gt;Get Our Family Album on the App Store →&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Website: &lt;a href="https://ourfamilyalbum.com" rel="noopener noreferrer"&gt;ourfamilyalbum.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Thanks for reading! How do you handle sharing with non-technical users in your apps? I'd love to know 🙂&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Why I built a private family album, as a dad of three, is a story on my newsletter: &lt;a href="https://manuelescrig.substack.com" rel="noopener noreferrer"&gt;manuelescrig.substack.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Manuel&lt;/p&gt;

</description>
      <category>ios</category>
      <category>privacy</category>
      <category>swift</category>
      <category>mobile</category>
    </item>
    <item>
      <title>Making a UIKit app feel like an arcade game</title>
      <dc:creator>Manuel Escrig</dc:creator>
      <pubDate>Tue, 10 Nov 2026 15:42:14 +0000</pubDate>
      <link>https://dev.to/manuelescrig/making-a-uikit-app-feel-like-an-arcade-game-30ee</link>
      <guid>https://dev.to/manuelescrig/making-a-uikit-app-feel-like-an-arcade-game-30ee</guid>
      <description>&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%2Fpggxeac5cs7uzgbto7mq.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%2Fpggxeac5cs7uzgbto7mq.png" alt="Geo Adventures app icon" width="798" height="173"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Geo Adventures is a geography game: you're dropped into a real street view somewhere in the world, you look for clues, and you pin your guess on a map. It launched in August 2026. It's built with plain UIKit and Auto Layout in code. The challenge was making it &lt;em&gt;not feel&lt;/em&gt; like UIKit at all 🎮&lt;/p&gt;

&lt;h2&gt;
  
  
  Replace the system chrome
&lt;/h2&gt;

&lt;p&gt;Games don't have navigation bars and standard alerts, so I built my own:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;GameButton&lt;/code&gt;: chunky 3D "arcade pill" buttons with a pressed state.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GameHeaderView&lt;/code&gt; instead of &lt;code&gt;UINavigationBar&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GameAlertController&lt;/code&gt; instead of &lt;code&gt;UIAlertController&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;SpringModalTransition&lt;/code&gt; for bouncy modal presentations.&lt;/li&gt;
&lt;li&gt;A starfield background, confetti, Core Haptics patterns and system sounds for feedback.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A &lt;code&gt;ThemeManager&lt;/code&gt; (the same pattern I use in Email Me) switches between four themes: Midnight, Ocean, Sunset and Daylight.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two map SDKs, each where it fits
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Google Maps SDK&lt;/strong&gt; shows the Street View panoramas.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MapKit&lt;/strong&gt; handles the guess map and the result map, so those screens don't need a Google key.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Play first, sign up later
&lt;/h2&gt;

&lt;p&gt;You can play right away as a guest, even offline. Accounts are optional, and they're built behind a small &lt;code&gt;AuthProvider&lt;/code&gt; protocol, with Sign in with Apple and Google Sign-In. Either way, the app exchanges the login for a bearer token from the game's own API. Scores sync to the server, and Game Center keeps a leaderboard for each difficulty.&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%2Fzu7ljwukzpzfjcik3y6f.jpeg" 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%2Fzu7ljwukzpzfjcik3y6f.jpeg" alt="Geo Adventures App Store screenshots: home, a street view round, the result map and multiplayer" width="799" height="434"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Fairness is a backend problem
&lt;/h2&gt;

&lt;p&gt;The Daily Challenge gives everyone the same rounds and a shared leaderboard. That brought edge cases I hadn't planned for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A player quits halfway. Now leaving early ends today's run with the rounds already played.&lt;/li&gt;
&lt;li&gt;A player closes the app or switches devices in the middle of a run. Now rounds are saved as you play.&lt;/li&gt;
&lt;li&gt;In multiplayer, one player leaves. Now the match ends for everyone, instead of leaving a frozen lobby.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Localization from day one
&lt;/h2&gt;

&lt;p&gt;Every string ships in 10 languages, including Chinese, Japanese, Korean and Brazilian Portuguese.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Custom components are cheaper than fighting the system ones until they look like a game.&lt;/li&gt;
&lt;li&gt;Make accounts optional. Friction before the first round kills a game.&lt;/li&gt;
&lt;li&gt;Write down what "fair" means before you build a leaderboard.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://apps.apple.com/us/app/geo-adventures-geography-game/id6787150687" rel="noopener noreferrer"&gt;Get Geo Adventures on the App Store →&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Website: &lt;a href="https://geoguesserapp.com" rel="noopener noreferrer"&gt;geoguesserapp.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Thanks for reading! And if you play, tell me your Daily Challenge score in the comments 😄&lt;/p&gt;

&lt;p&gt;&lt;em&gt;I write about the other side of building apps, the stories and decisions, on my newsletter: &lt;a href="https://manuelescrig.substack.com" rel="noopener noreferrer"&gt;manuelescrig.substack.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Manuel&lt;/p&gt;

</description>
      <category>ios</category>
      <category>swift</category>
      <category>gamedev</category>
      <category>uikit</category>
    </item>
    <item>
      <title>Normalizing 77 years of MotoGP results into SQLite</title>
      <dc:creator>Manuel Escrig</dc:creator>
      <pubDate>Tue, 10 Nov 2026 15:41:53 +0000</pubDate>
      <link>https://dev.to/manuelescrig/normalizing-77-years-of-motogp-results-into-sqlite-1cg1</link>
      <guid>https://dev.to/manuelescrig/normalizing-77-years-of-motogp-results-into-sqlite-1cg1</guid>
      <description>&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%2Fszptt6t901py9es1k4w8.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%2Fszptt6t901py9es1k4w8.png" alt="Race Results app icon" width="798" height="173"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Race Results is my MotoGP, Moto2 and Moto3 companion app for iOS. It has a live countdown, session times in your own time zone, standings, widgets and Live Activities. But the part I enjoyed building the most is the History tab: every season since 1949 🏍️&lt;/p&gt;

&lt;h2&gt;
  
  
  Don't trust the upstream API to stay around
&lt;/h2&gt;

&lt;p&gt;The app reads from a public MotoGP results feed, but it doesn't depend on it for history. A small PHP backend normalizes the feed into &lt;strong&gt;SQLite&lt;/strong&gt;, so finished seasons stay fast and keep working even if the provider changes something.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Lazy fill:&lt;/strong&gt; when someone opens an old season, Grand Prix or session, the backend fetches it and stores it right then.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Full backfill:&lt;/strong&gt; a resumable importer can load the whole premier-class archive, from 1949 until today, in one run.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Championships get renamed
&lt;/h2&gt;

&lt;p&gt;The hardest part of the data model is that the classes changed names over the years. 500cc became MotoGP, 250cc became Moto2 and 125cc became Moto3. The archive tracks each class as one &lt;strong&gt;lineage&lt;/strong&gt;, so records, champions and head-to-head comparisons work across eras. Records only count seasons where every finished race has structured results. For older sessions without data, the app links to the official PDF instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Live data on the Lock Screen
&lt;/h2&gt;

&lt;p&gt;Live Activities brought their own bugs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The Lock Screen leaderboard has to keep updating after the app restarts.&lt;/li&gt;
&lt;li&gt;Lapped riders need to stay behind the lead lap in the live order.&lt;/li&gt;
&lt;li&gt;The race Live Activity shows a lap counter instead of a clock, because races are measured in laps.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And time zones, of course 😅 For a while the Apple Watch showed session times shifted by the circuit's time difference.&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%2Fwithbeyhoc8b8wxii8xi.jpeg" 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%2Fwithbeyhoc8b8wxii8xi.jpeg" alt="Race Results App Store screenshots: schedule, race weekend, rider profile and news" width="799" height="434"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Startup performance
&lt;/h2&gt;

&lt;p&gt;Widget refreshes and other background work used to run during launch. Now they wait until the first screen is visible, and the Standings tab no longer rebuilds itself twice when you come back to the app.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bonus: an MCP server
&lt;/h2&gt;

&lt;p&gt;The same data also powers &lt;a href="https://github.com/manuelescrig/raceresults-mcp" rel="noopener noreferrer"&gt;raceresults-mcp&lt;/a&gt;, a free hosted MCP server. It gives AI assistants tools like &lt;code&gt;get_standings&lt;/code&gt;, &lt;code&gt;get_calendar&lt;/code&gt;, &lt;code&gt;get_next_race&lt;/code&gt;, &lt;code&gt;get_race_results&lt;/code&gt;, &lt;code&gt;get_rider&lt;/code&gt;, &lt;code&gt;get_live_timing&lt;/code&gt; and &lt;code&gt;get_news&lt;/code&gt;. If you already have clean data behind your app, exposing it to AI tools takes very little extra work. Give it a try!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://apps.apple.com/us/app/race-results-motogp-standings/id6785385300" rel="noopener noreferrer"&gt;Get Race Results on the App Store →&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Website: &lt;a href="https://raceresultsapp.com" rel="noopener noreferrer"&gt;raceresultsapp.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Thanks for reading! Any MotoGP fans here? Tell me in the comments 🙂&lt;/p&gt;

&lt;p&gt;&lt;em&gt;More stories about the apps I build go out on my newsletter: &lt;a href="https://manuelescrig.substack.com" rel="noopener noreferrer"&gt;manuelescrig.substack.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Manuel&lt;/p&gt;

</description>
      <category>sqlite</category>
      <category>ios</category>
      <category>mcp</category>
      <category>php</category>
    </item>
    <item>
      <title>Building a photo booth app that never loses a photo</title>
      <dc:creator>Manuel Escrig</dc:creator>
      <pubDate>Tue, 10 Nov 2026 15:41:33 +0000</pubDate>
      <link>https://dev.to/manuelescrig/building-a-photo-booth-app-that-never-loses-a-photo-3ej2</link>
      <guid>https://dev.to/manuelescrig/building-a-photo-booth-app-that-never-loses-a-photo-3ej2</guid>
      <description>&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%2Fcmrzmhzjc6hbogv7142m.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%2Fcmrzmhzjc6hbogv7142m.png" alt="Photo Booth App icon" width="798" height="173"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Photo Booth App turns an iPhone or iPad into a self-service photo booth for weddings, parties and company events. Version 1.0 went live in March 2026, and I've shipped more than 20 updates since. An event app doesn't forgive mistakes: if a photo gets lost at a wedding, there's no retake next week.&lt;/p&gt;

&lt;h2&gt;
  
  
  Offline first
&lt;/h2&gt;

&lt;p&gt;Venue Wi-Fi is usually bad, so the booth never depends on it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Photos and GIFs are captured and saved on the device first.&lt;/li&gt;
&lt;li&gt;Uploads to the online gallery run in the background and keep going while guests keep using the booth.&lt;/li&gt;
&lt;li&gt;Finished photos survive the booth restarting or moving on to the next guest.&lt;/li&gt;
&lt;li&gt;When an event opens, the app checks the gallery and recovers any saved photos that are missing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Sharing by QR code, email or text needs a connection. Taking photos never does.&lt;/p&gt;

&lt;h2&gt;
  
  
  Printing is harder than it looks
&lt;/h2&gt;

&lt;p&gt;AirPrint is easy to add and hard to get right:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A 4×6 template used to print in the corner of a full sheet 🤦 Now the paper size is matched to the template size.&lt;/li&gt;
&lt;li&gt;Imported artwork snaps to the nearest print size. If it needs more than a small trim, the app tells you how much, and offers to crop, use a custom size, or pick another image.&lt;/li&gt;
&lt;li&gt;A quick double tap on Print used to send two jobs. Now the button ignores the second tap.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The camera has to follow the device
&lt;/h2&gt;

&lt;p&gt;When the booth runs in landscape, the live preview and the photos have to be landscape too. Now the camera rotates with the device. The mirror-style preview is a setting called "Mirror Live Preview", with a short explanation of what it does, because people kept turning it on by accident.&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%2Fm5h0z1966dua4r02x2tc.jpeg" 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%2Fm5h0z1966dua4r02x2tc.jpeg" alt="Photo Booth App Store screenshots" width="799" height="434"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Unattended by design
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Guided Access&lt;/strong&gt; and an exit passcode keep guests inside the booth.&lt;/li&gt;
&lt;li&gt;An &lt;strong&gt;Apple Watch remote&lt;/strong&gt; lets the host restart or finish a guest's session from across the room.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;web dashboard&lt;/strong&gt; manages events, templates and galleries. You sign in to it from the app with a one-time Safari link, or by scanning a code on your computer and approving the browser.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Lessons
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Design for the worst venue: no Wi-Fi, a cheap printer, and a guest who taps everything twice.&lt;/li&gt;
&lt;li&gt;Make data recovery automatic. Nobody checks a gallery in the middle of a party.&lt;/li&gt;
&lt;li&gt;Most of these fixes came from hosts who wrote to me after their event. Make that easy for them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://apps.apple.com/us/app/photo-booth-app-for-events/id6758273611" rel="noopener noreferrer"&gt;Get Photo Booth App on the App Store →&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Website: &lt;a href="https://thephotoboothapp.com" rel="noopener noreferrer"&gt;thephotoboothapp.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Thanks for reading! Have you built anything offline-first on iOS? Let me know in the comments 🙂&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The story of how I tested it at my own wedding is on my newsletter: &lt;a href="https://manuelescrig.substack.com" rel="noopener noreferrer"&gt;manuelescrig.substack.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Manuel&lt;/p&gt;

</description>
      <category>ios</category>
      <category>swift</category>
      <category>mobile</category>
      <category>indiedev</category>
    </item>
    <item>
      <title>One photo frame codebase for iPhone, iPad, Mac and Apple TV, back to iOS 12</title>
      <dc:creator>Manuel Escrig</dc:creator>
      <pubDate>Tue, 10 Nov 2026 15:41:12 +0000</pubDate>
      <link>https://dev.to/manuelescrig/one-photo-frame-codebase-for-iphone-ipad-mac-and-apple-tv-back-to-ios-12-51a2</link>
      <guid>https://dev.to/manuelescrig/one-photo-frame-codebase-for-iphone-ipad-mac-and-apple-tv-back-to-ios-12-51a2</guid>
      <description>&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%2Fm9sza2lamhx0xot2ews0.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%2Fm9sza2lamhx0xot2ews0.png" alt="Digital Photo Frame app icon" width="798" height="173"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Digital Photo Frame turns an Apple device into a picture frame. It's been on the App Store since March 2017, and it has shipped more than 160 releases. The interesting engineering isn't the slideshow itself. It's keeping it running, alone, for weeks.&lt;/p&gt;

&lt;h2&gt;
  
  
  One project, four platforms
&lt;/h2&gt;

&lt;p&gt;The app is one Xcode project with an iOS target, a Mac Catalyst build, a tvOS target and a WidgetKit extension. It's UIKit, with MVVM for the main screens, coordinators for navigation, and a service layer for every photo source:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Apple Photos and iCloud shared albums&lt;/li&gt;
&lt;li&gt;Google Photos&lt;/li&gt;
&lt;li&gt;Self-hosted servers through Immich or WebDAV/NAS&lt;/li&gt;
&lt;li&gt;Flickr and Unsplash&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On the Mac, a small AppKit plugin bundle covers what Catalyst can't do.&lt;/p&gt;

&lt;h2&gt;
  
  
  Supporting old iPads has a price
&lt;/h2&gt;

&lt;p&gt;The deployment target is still &lt;strong&gt;iOS 12&lt;/strong&gt;. A lot of people run the app on an old iPad that can't update anymore, and that's exactly the point of the product. The cost shows up in the dependencies. RevenueCat 5 needs iOS 13, so I vendor a patched copy of RevenueCat 4.43.4 inside the repo instead of pulling it through Swift Package Manager. Not pretty, but it works.&lt;/p&gt;

&lt;h2&gt;
  
  
  Audio is the hardest part of a slideshow
&lt;/h2&gt;

&lt;p&gt;A slideshow with background music and videos has to manage &lt;code&gt;AVAudioSession&lt;/code&gt; like a real media app:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Lower the music while a video with sound plays, and only bring it back when every video has finished.&lt;/li&gt;
&lt;li&gt;Resume after a phone call, Siri or an alarm, and restart if iOS stops playback without telling you.&lt;/li&gt;
&lt;li&gt;Keep a paused slideshow paused after a call ends.&lt;/li&gt;
&lt;li&gt;On Apple TV, skip Apple Music songs that can't play there, instead of stopping the whole playlist.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each one of those was a separate bug report, usually from someone whose frame had been running for days. Big thanks to them 🙏&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%2Fz90tnj8krdzbxxdyfsmu.jpeg" 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%2Fz90tnj8krdzbxxdyfsmu.jpeg" alt="Digital Photo Frame App Store screenshots" width="799" height="434"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Small details on tvOS
&lt;/h2&gt;

&lt;p&gt;The Apple TV remote is a terrible keyboard. Setting up Immich, a NAS or a weather location now uses the standard tvOS keyboard, so you can type or paste from the Apple TV Remote on your iPhone, or just use dictation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;An "always on" app needs a test plan for interruptions, not just for features.&lt;/li&gt;
&lt;li&gt;Supporting old OS versions is a product decision. Budget for it in your dependencies.&lt;/li&gt;
&lt;li&gt;Thank bug reporters by name in your release notes. They keep reporting.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://apps.apple.com/us/app/digital-photo-frame-with-music/id1219786089" rel="noopener noreferrer"&gt;Get Digital Photo Frame on the App Store →&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Website: &lt;a href="https://digitalphotoframeapp.com" rel="noopener noreferrer"&gt;digitalphotoframeapp.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Thanks for reading! Questions about Catalyst or tvOS? Ask me in the comments 🙂&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Why I built this app in the first place, for my parents and my twins, is a story on my newsletter: &lt;a href="https://manuelescrig.substack.com" rel="noopener noreferrer"&gt;manuelescrig.substack.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Manuel&lt;/p&gt;

</description>
      <category>ios</category>
      <category>swift</category>
      <category>tvos</category>
      <category>indiedev</category>
    </item>
    <item>
      <title>Hour-long voice notes don't fit in an email, so I stopped sending them</title>
      <dc:creator>Manuel Escrig</dc:creator>
      <pubDate>Tue, 10 Nov 2026 15:40:51 +0000</pubDate>
      <link>https://dev.to/manuelescrig/hour-long-voice-notes-dont-fit-in-an-email-so-i-stopped-sending-them-2gg7</link>
      <guid>https://dev.to/manuelescrig/hour-long-voice-notes-dont-fit-in-an-email-so-i-stopped-sending-them-2gg7</guid>
      <description>&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%2F9z3znkj5zw7cs27li4s8.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%2F9z3znkj5zw7cs27li4s8.png" alt="Email Me app icon" width="798" height="173"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Email Me does one thing: it sends a note to your own inbox. For years, a voice note was simply an audio file attached to that email. And that worked fine, until people started asking me to record meetings and lectures 😅&lt;/p&gt;

&lt;h2&gt;
  
  
  The 25 MB wall
&lt;/h2&gt;

&lt;p&gt;Most email providers cap attachments at around 25 MB, and some of them strip or bounce big ones. An hour of speech at the app's old "high" setting (AAC mono at 22 kHz) is about 22 MB. That's already at the limit before any other attachment. On top of that, the recorder had a hard 5-minute cap in &lt;code&gt;AVAudioRecorder&lt;/code&gt;, on both iPhone and Apple Watch.&lt;/p&gt;

&lt;p&gt;A bigger attachment wasn't the answer. The real fix was to separate the audio from the email.&lt;/p&gt;

&lt;h2&gt;
  
  
  Audio as an asset, email as a notification
&lt;/h2&gt;

&lt;p&gt;So I changed the rule: &lt;strong&gt;a recording is a durable file on the server, and the email just tells you about it.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The app uploads the audio to the server and gets back a URL. The link can be password-protected.&lt;/li&gt;
&lt;li&gt;The email carries that link and the transcript, instead of the file.&lt;/li&gt;
&lt;li&gt;Recordings also stay on the device, in a Core Data library with a full player: waveform, scrubbing and 0.75–2× speed.&lt;/li&gt;
&lt;li&gt;The Apple Watch uploads through a background &lt;code&gt;URLSession&lt;/code&gt;, so a long recording still arrives after you lower your wrist.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Two transcription engines
&lt;/h2&gt;

&lt;p&gt;The transcripts come from two places:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;On-device, live:&lt;/strong&gt; &lt;code&gt;SpeechAnalyzer&lt;/code&gt; on iOS 26, and &lt;code&gt;SFSpeechRecognizer&lt;/code&gt; on older versions, so you see the words while you record.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Server-side, final:&lt;/strong&gt; Whisper produces the transcript that goes into the email. Hour-long recordings usually finish in about 30 seconds.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Funny enough, the trickiest bugs were about language, not audio. The transcript language you pick in the settings had to match on iPhone, Apple Watch and CarPlay. Now the Watch gets the language from the iPhone automatically, so a setting you chose months ago still applies.&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%2F9pnfwkwbhexhie3dul8x.jpeg" 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%2F9pnfwkwbhexhie3dul8x.jpeg" alt="Email Me App Store screenshots" width="799" height="434"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd tell my past self
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Treat email as a notification channel, never as storage.&lt;/li&gt;
&lt;li&gt;Give long-running work its own background path from day one, especially on watchOS.&lt;/li&gt;
&lt;li&gt;Put the transcription result in the email itself. When something fails, users can tell you about it instead of guessing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Email Me is on version 7.15 now, ten years after version 1.0. Still going 💪&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://apps.apple.com/us/app/audio-notes-email-me/id1090744587" rel="noopener noreferrer"&gt;Get Email Me on the App Store →&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Website: &lt;a href="https://emailmeapp.net" rel="noopener noreferrer"&gt;emailmeapp.net&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Thanks for reading! If you've dealt with long audio on iOS, I'd love to hear how you solved it. Drop a comment 🙂&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The personal story behind this app, and why I built it in the first place, is on my newsletter: &lt;a href="https://manuelescrig.substack.com" rel="noopener noreferrer"&gt;manuelescrig.substack.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Manuel&lt;/p&gt;

</description>
      <category>ios</category>
      <category>swift</category>
      <category>indiedev</category>
      <category>ai</category>
    </item>
    <item>
      <title>Six apps, one developer: how I run an indie iOS portfolio</title>
      <dc:creator>Manuel Escrig</dc:creator>
      <pubDate>Tue, 10 Nov 2026 15:40:32 +0000</pubDate>
      <link>https://dev.to/manuelescrig/six-apps-one-developer-how-i-run-an-indie-ios-portfolio-3i2o</link>
      <guid>https://dev.to/manuelescrig/six-apps-one-developer-how-i-run-an-indie-ios-portfolio-3i2o</guid>
      <description>&lt;p&gt;Hey everyone 👋 I'm Manuel, an indie iOS developer from Spain. I build and maintain six apps on the App Store, each with its own website and a small backend. There's no team, it's just me. In this blog I want to share the engineering side: how the apps are built and what broke along the way (spoiler: a lot 😅).&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%2Fiz05xrsyix866fwxqgiv.jpeg" 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%2Fiz05xrsyix866fwxqgiv.jpeg" alt="The six apps: Email Me, Digital Photo Frame, Photo Booth, Race Results, Geo Adventures and Our Family Album" width="800" height="240"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  One structure for every app
&lt;/h2&gt;

&lt;p&gt;All six apps use programmatic UIKit with Auto Layout in code. No storyboards, apart from the launch screens. When I start a new app, I copy the structure of an existing one: Geo Adventures started from Email Me's layout, and Our Family Album from Digital Photo Frame's. So whenever I open any repo, I know exactly where everything is: &lt;code&gt;App/&lt;/code&gt;, &lt;code&gt;Configuration/&lt;/code&gt;, &lt;code&gt;Controllers/&lt;/code&gt;, &lt;code&gt;Models/&lt;/code&gt;, &lt;code&gt;Persistence/&lt;/code&gt;, &lt;code&gt;Views/&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Being the only developer, this saves me a lot of time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Shared building blocks
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;RevenueCat&lt;/strong&gt; for subscriptions and purchases, and &lt;strong&gt;Bugsnag&lt;/strong&gt; for crash reports.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;ThemeManager&lt;/strong&gt; pattern for app themes, reused from app to app.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Analytics with one convention&lt;/strong&gt; in four of the apps: Title Case events, &lt;code&gt;[Screen] Name&lt;/code&gt; for screens, and the same base properties on every event. The privacy rule is strict: ids, counts and settings only. Never names, emails, typed text or photos.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Every app has a website and a backend
&lt;/h2&gt;

&lt;p&gt;Each app has a small PHP site behind it, with the landing pages, blog, changelog, feature requests and the app's API. Some of them use SQLite, which is more than enough at this scale. The backends do the work that doesn't belong on a phone: audio transcription for Email Me, the MotoGP archive for Race Results, accounts and scores for Geo Adventures, and share links for Our Family Album.&lt;/p&gt;

&lt;h2&gt;
  
  
  Localized from day one
&lt;/h2&gt;

&lt;p&gt;Most of the apps ship in 8 or more languages. Translating early is much cheaper than translating a finished app, trust me.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ship small, ship often
&lt;/h2&gt;

&lt;p&gt;Email Me is on version 7.15 after ten years, and Digital Photo Frame has shipped more than 160 releases. Every changelog thanks the users who reported a bug by name. As a solo developer, that's the best feedback loop I've found. People keep writing to you when they see you listen.&lt;/p&gt;

&lt;h2&gt;
  
  
  The deep dives
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://manuelescrig.hashnode.dev/hour-long-voice-notes-don-t-fit-in-an-email-so-i-stopped-sending-them" rel="noopener noreferrer"&gt;Hour-long voice notes don't fit in an email, so I stopped sending them&lt;/a&gt; (Email Me)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://manuelescrig.hashnode.dev/one-photo-frame-codebase-for-iphone-ipad-mac-and-apple-tv-back-to-ios-12" rel="noopener noreferrer"&gt;One photo frame codebase for iPhone, iPad, Mac and Apple TV, back to iOS 12&lt;/a&gt; (Digital Photo Frame)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://manuelescrig.hashnode.dev/building-a-photo-booth-app-that-never-loses-a-photo" rel="noopener noreferrer"&gt;Building a photo booth app that never loses a photo&lt;/a&gt; (Photo Booth)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://manuelescrig.hashnode.dev/normalizing-77-years-of-motogp-results-into-sqlite" rel="noopener noreferrer"&gt;Normalizing 77 years of MotoGP results into SQLite&lt;/a&gt; (Race Results)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://manuelescrig.hashnode.dev/making-a-uikit-app-feel-like-an-arcade-game" rel="noopener noreferrer"&gt;Making a UIKit app feel like an arcade game&lt;/a&gt; (Geo Adventures)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://manuelescrig.hashnode.dev/sharing-private-photos-with-people-who-will-never-install-your-app" rel="noopener noreferrer"&gt;Sharing private photos with people who will never install your app&lt;/a&gt; (Our Family Album)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The apps:&lt;/strong&gt; &lt;a href="https://emailmeapp.net" rel="noopener noreferrer"&gt;Email Me&lt;/a&gt; · &lt;a href="https://digitalphotoframeapp.com" rel="noopener noreferrer"&gt;Digital Photo Frame&lt;/a&gt; · &lt;a href="https://thephotoboothapp.com" rel="noopener noreferrer"&gt;Photo Booth&lt;/a&gt; · &lt;a href="https://raceresultsapp.com" rel="noopener noreferrer"&gt;Race Results&lt;/a&gt; · &lt;a href="https://geoguesserapp.com" rel="noopener noreferrer"&gt;Geo Adventures&lt;/a&gt; · &lt;a href="https://ourfamilyalbum.com" rel="noopener noreferrer"&gt;Our Family Album&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Thanks for reading! If you have any questions, drop them in the comments, I'm happy to help 🙂&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The personal stories behind each app, why I built them and for whom, go out every week on my newsletter: &lt;a href="https://manuelescrig.substack.com" rel="noopener noreferrer"&gt;manuelescrig.substack.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Manuel&lt;/p&gt;

</description>
      <category>ios</category>
      <category>swift</category>
      <category>indiedev</category>
      <category>mobile</category>
    </item>
  </channel>
</rss>
