<?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: ios</title>
    <description>The latest articles tagged 'ios' on DEV Community.</description>
    <link>https://dev.to/t/ios</link>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tag/ios"/>
    <language>en</language>
    <item>
      <title>Where the Money Is in AI Apps in 2026: A Niche Map</title>
      <dc:creator>Kirill Lukyanov</dc:creator>
      <pubDate>Wed, 21 Oct 2026 11:23:25 +0000</pubDate>
      <link>https://dev.to/klukyanov/where-the-money-is-in-ai-apps-in-2026-a-niche-map-343m</link>
      <guid>https://dev.to/klukyanov/where-the-money-is-in-ai-apps-in-2026-a-niche-map-343m</guid>
      <description>&lt;p&gt;In 2025, people spent more money in regular apps than in games for the first time. Generative AI is the main reason. But if you're thinking of building "an AI app", the money is not where it looks like it is. Chat assistants are taken. Photo, video, voice, and AI tucked inside a daily habit are where people still pay. I build iOS apps, so I pulled together the numbers by niche and wrote down where a small team can still get in.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Short version:&lt;/strong&gt; don't build another assistant. Build something that produces a file people keep, or remove the boring step from a task people already do every day. The second one is the realistic bet for a solo developer.&lt;/p&gt;

&lt;p&gt;This is part 1 of a short series on how people make money with AI right now. This one is about mobile apps; part 2 covers websites and web services.&lt;/p&gt;

&lt;h2&gt;
  
  
  The market: there is money
&lt;/h2&gt;

&lt;p&gt;According to &lt;a href="https://sensortower.com/blog/state-of-mobile-2026" rel="noopener noreferrer"&gt;Sensor Tower's State of Mobile 2026&lt;/a&gt;, consumers spent $167B in apps in 2025, up 10.6%. Non-game app spending grew 21% and passed games, which grew 1.3%. Generative AI apps were downloaded 3.8B times (double the year before), and their in-app revenue nearly tripled to over $5B.&lt;/p&gt;

&lt;p&gt;Growth hasn't stopped. &lt;a href="https://sensortower.com/press/sensor-tower-state-of-ai-2026-report-global-time-spent-on-generative-ai-apps-projected-to-more-than-double-year-over-year" rel="noopener noreferrer"&gt;Sensor Tower projects&lt;/a&gt; AI apps will pass $4B in H1 2026, 36% more than H2 2025. Time spent doubled year over year, from 17.2B to 36B hours. More than 200,000 apps now mention AI in their descriptions.&lt;/p&gt;

&lt;p&gt;That last number matters most. With 200,000 apps saying "AI", the label on its own sells nothing. What sells is the job it does.&lt;/p&gt;

&lt;h2&gt;
  
  
  Chat assistants: the field is taken
&lt;/h2&gt;

&lt;p&gt;In Q1 2026, ChatGPT, DeepSeek and Google Gemini held almost 90% of time spent in AI assistant apps (Sensor Tower). &lt;a href="https://appfigures.com/resources/insights/20251121/amp?f=3" rel="noopener noreferrer"&gt;Appfigures estimates&lt;/a&gt; ChatGPT made $187M net on mobile in October 2025, with Grok at $8.7M, Claude at $5M and Perplexity at $3.1M. Even companies with their own frontier models are 20–40x behind.&lt;/p&gt;

&lt;p&gt;It wasn't always like this. In 2023–2024, thin wrappers did well: &lt;a href="https://appfigures.com/resources/insights/20250131/3-we-ranked-every-ai-app-and-here-are-the-highest-earners" rel="noopener noreferrer"&gt;Appfigures&lt;/a&gt; puts ChatOn at $135M and Chat &amp;amp; Ask AI at $109M over two years. That was before OpenAI and Google shipped good apps of their own, when people searched the App Store for "ChatGPT" and installed whatever came up first. Today the free original beats any wrapper. That's my read, not a finding from the reports.&lt;/p&gt;

&lt;h2&gt;
  
  
  The niche map
&lt;/h2&gt;

&lt;p&gt;Figures come from different sources and aren't directly comparable: some are net of store fees, some gross, some are company claims.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Niche&lt;/th&gt;
&lt;th&gt;Who earns&lt;/th&gt;
&lt;th&gt;Numbers&lt;/th&gt;
&lt;th&gt;Barrier for a small team&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;General-purpose assistants&lt;/td&gt;
&lt;td&gt;ChatGPT, Claude, Grok, Perplexity&lt;/td&gt;
&lt;td&gt;ChatGPT: $187M net in Oct 2025 (Appfigures)&lt;/td&gt;
&lt;td&gt;Closed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Video generation &amp;amp; avatars&lt;/td&gt;
&lt;td&gt;Higgsfield, HeyGen, Runway, Kling, Synthesia&lt;/td&gt;
&lt;td&gt;Higgsfield claims $1B annualized revenue (company claim)&lt;/td&gt;
&lt;td&gt;High: generation is expensive, margins thin&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Photo: editing, retouching, product shots&lt;/td&gt;
&lt;td&gt;Photoroom, Evoto, FaceApp, Reface, Topaz&lt;/td&gt;
&lt;td&gt;About ten photo tools in a16z's top 50 by spend&lt;/td&gt;
&lt;td&gt;Medium: a narrow job works&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Audio: music, voice, read-aloud&lt;/td&gt;
&lt;td&gt;Suno, ElevenLabs, Speechify, Moises&lt;/td&gt;
&lt;td&gt;Suno is #7 in a16z's top 50&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Productivity: meetings, notes, slides&lt;/td&gt;
&lt;td&gt;Otter, Plaud, Fireflies, Gamma, Notion&lt;/td&gt;
&lt;td&gt;15 of 50 spots in a16z's top 50&lt;/td&gt;
&lt;td&gt;Medium: higher prices, companies pay&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Companions &amp;amp; roleplay&lt;/td&gt;
&lt;td&gt;Zeta, Tipsy Chat, Character.AI&lt;/td&gt;
&lt;td&gt;$165M general + $163M romance in H1 2026, gross (Appfigures via Decrypt)&lt;/td&gt;
&lt;td&gt;Low technically, high on moderation and reputation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Homework &amp;amp; study&lt;/td&gt;
&lt;td&gt;Gauth (ByteDance), Question.AI&lt;/td&gt;
&lt;td&gt;~$2.5M net combined in 2024 on 15M installs (Appfigures)&lt;/td&gt;
&lt;td&gt;High: big players plus free ChatGPT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AI inside a daily habit: food, health, fitness&lt;/td&gt;
&lt;td&gt;Cal AI and narrow trackers&lt;/td&gt;
&lt;td&gt;Cal AI: $40M+ trailing-12-month sales, acquired by MyFitnessPal in March 2026&lt;/td&gt;
&lt;td&gt;Low–medium: best shot for a solo dev&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The a16z ranking is the &lt;a href="https://officechai.com/ai/top-consumer-apps-by-revenue/" rel="noopener noreferrer"&gt;top 50 consumer AI products&lt;/a&gt; by US consumer spend (October 2026, YipitData), web and mobile combined. More than half of the list is photo, video and audio.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why people pay for photo, video and voice
&lt;/h2&gt;

&lt;p&gt;My interpretation, but it fits the numbers.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The result is a file.&lt;/strong&gt; You read a chatbot answer and close it. A video, an edited photo or a voiceover gets saved, posted, sent to a client. It's easier to pay for something you can take away.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Free ChatGPT is worse at it.&lt;/strong&gt; A general assistant can draw a picture. It won't batch-remove backgrounds from forty product shots at marketplace size.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Professionals pay.&lt;/strong&gt; Marketplace sellers, photographers, creators and marketers treat these as work tools that pay for themselves.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The flip side is cost. Every video generation costs money, and an all-you-can-eat plan will put you in the red. Credits instead of unlimited, and expensive models only on the paid tier.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where a solo developer fits: AI inside a habit
&lt;/h2&gt;

&lt;p&gt;The most telling story of the year is &lt;strong&gt;Cal AI&lt;/strong&gt;. Two high schoolers built a calorie counter by photo: snap your plate, get calories and protein. In under two years it passed 15M downloads, did over $40M in sales in the trailing 12 months, and MyFitnessPal bought it in March 2026 (terms undisclosed).&lt;/p&gt;

&lt;p&gt;The model inside isn't theirs, and food recognition isn't unique. What worked: AI removed the most tedious step of a task people already wanted to do. Counting calories was always the goal; typing in every spoonful was the problem.&lt;/p&gt;

&lt;p&gt;The recipe:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Pick a task people do regularly, daily or weekly.&lt;/li&gt;
&lt;li&gt;Find the most tedious step: data entry, transcription, counting.&lt;/li&gt;
&lt;li&gt;Let AI remove that step, and keep the rest a good ordinary app.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I do this in my own fasting app: calories from a photo are part of the daily log, and recognition runs on the phone through Apple Foundation Models, offline and with no API bill. When a request costs nothing, a free tier and a long trial don't turn into losses.&lt;/p&gt;

&lt;p&gt;A habit also fights the other AI-app problem, churn. People leave AI apps faster than regular ones, and what keeps them is the recurring task, not the model. I covered that in &lt;a href="https://dev.to/klukyanov/ai-apps-earn-more-but-lose-subscribers-faster-3d6l"&gt;an earlier breakdown of RevenueCat's data&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What can break it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The platform eats your feature.&lt;/strong&gt; If your product is one button, it'll show up in ChatGPT, Google Photos or iOS itself. Data, history and a good workflow are what protect you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Store rules.&lt;/strong&gt; In April 2026 Apple briefly pulled Cal AI: it took payments via Stripe outside in-app purchase and showed the weekly price more prominently than the actual charge. It came back after fixes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost.&lt;/strong&gt; A cloud model is a variable cost per user. Do the math before launch, not after the first invoice.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Companions are their own thing.&lt;/strong&gt; Hundreds of millions in revenue, but the top 10% of apps take 89% of it (Appfigures, 2025), and moderation and reputational risk are the highest anywhere.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I'd pick
&lt;/h2&gt;

&lt;p&gt;Not an assistant, and not a video generator: the first is taken, the second needs model budgets a solo developer doesn't have. I'd take a boring recurring task in a niche where people already pay for apps (food, fitness, health, bookkeeping, photos for sellers), have AI remove the most tedious step, on-device where possible, and sell an annual plan with a long trial.&lt;/p&gt;

&lt;p&gt;Less exciting than "my own ChatGPT". But it's what the one real success story of the year looks like.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://klukyanov.ru/notes/ai-apps-money-map/" rel="noopener noreferrer"&gt;klukyanov.ru&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Shorter weekly write-ups (in Russian) — on &lt;a href="https://t.me/+KSAy8mfFOsgwYWMy" rel="noopener noreferrer"&gt;Telegram&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>mobile</category>
      <category>startup</category>
      <category>ios</category>
    </item>
    <item>
      <title>The JustWatch app has acquired its own movie viewing platform</title>
      <dc:creator>Hacks.gr</dc:creator>
      <pubDate>Wed, 21 Oct 2026 10:20:25 +0000</pubDate>
      <link>https://dev.to/hacksgr/the-justwatch-app-has-acquired-its-own-movie-viewing-platform-38kp</link>
      <guid>https://dev.to/hacksgr/the-justwatch-app-has-acquired-its-own-movie-viewing-platform-38kp</guid>
      <description>&lt;p&gt;JustWatch has launched JustWatch TV in the US, expanding its platform into a direct streaming service with free ad-supported viewing, rentals, and a monthly ad-free subscription tier available across mobile apps, smart TVs, and web browsers.&lt;/p&gt;




&lt;h2&gt;
  
  
  What does the new service offer?
&lt;/h2&gt;

&lt;p&gt;The service launched in the United States on Tuesday, offering access to 344 titles at launch. Among the films available for free are &lt;em&gt;A Different Man&lt;/em&gt;, &lt;em&gt;Aftersun&lt;/em&gt;, &lt;em&gt;Captain Fantastic&lt;/em&gt;, &lt;em&gt;Green Room&lt;/em&gt;, &lt;em&gt;Locke&lt;/em&gt;, &lt;em&gt;Red Rocket&lt;/em&gt;, &lt;em&gt;Sing Sing&lt;/em&gt;, &lt;em&gt;Stop Making Sense&lt;/em&gt;, &lt;em&gt;Swiss Army Man&lt;/em&gt;, &lt;em&gt;Under the Skin&lt;/em&gt; and &lt;em&gt;We Need to Talk About Kevin&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Users can watch content without signing in to an account, but a login is required if they want to track their progress. Viewing is available through the company's mobile apps, on select TVs, and on a computer via a browser.&lt;/p&gt;

&lt;h3&gt;
  
  
  Subscription and rental options
&lt;/h3&gt;

&lt;p&gt;Those who prefer to watch movies without ads can opt for the JustWatch TV Plus subscription, which costs $3,99 per month and includes 234 titles at launch. There’s also a lifetime subscription available for $49,99, though it’s unclear if that option will remain available permanently. The subscription also offers a 10% discount on select rentals and unlocks additional features, such as advanced search filters.&lt;/p&gt;

&lt;p&gt;READ ALSO&lt;/p&gt;

&lt;p&gt;Subaru to phase out gasoline cars in Europe by 2027&lt;/p&gt;

&lt;p&gt;In addition to free content, the platform allows rentals of 348 titles, including &lt;em&gt;Arrival&lt;/em&gt;, &lt;em&gt;Friday the 13th&lt;/em&gt;, &lt;em&gt;Good Will Hunting&lt;/em&gt;, &lt;em&gt;The Godfather&lt;/em&gt; and &lt;em&gt;Top Gun: Maverick&lt;/em&gt;. Most rentals cost between $3,99 and $5,99 in high definition, with no option to view in even higher resolution. Movies still showing in theaters, such as &lt;em&gt;Paw Patrol: The Dino Movie&lt;/em&gt;, cost $19,99 to rent.&lt;/p&gt;

&lt;h3&gt;
  
  
  Objectives and content suggestions
&lt;/h3&gt;

&lt;p&gt;The company's head of global content strategy, Quentin Carbonell, said the service aims to reach 100 million monthly users in the coming years. According to the company, data from users' searches may be used to better recommend titles across the platform's entire range of services.&lt;/p&gt;

&lt;p&gt;G&lt;/p&gt;

&lt;p&gt;Hacks.gr on Google&lt;/p&gt;

&lt;p&gt;Set it as a preferred source for cybersecurity updates.&lt;/p&gt;

&lt;p&gt;Preferred source&lt;/p&gt;




&lt;p&gt;&lt;a href="https://en.hacks.gr/i-efarmogi-justwatch-apektise-ti-diki-tis-platforma-provolis-tainion/" rel="noopener noreferrer"&gt;Read the original English article on Hacks.gr&lt;/a&gt;&lt;/p&gt;

</description>
      <category>justwatch</category>
      <category>streaming</category>
      <category>android</category>
      <category>ios</category>
    </item>
    <item>
      <title>Fajr is the easiest prayer to sleep through, so we turned it into a real alarm</title>
      <dc:creator>Ahtisham Hasan Khan </dc:creator>
      <pubDate>Wed, 21 Oct 2026 09:03:10 +0000</pubDate>
      <link>https://dev.to/iahtisham/fajr-is-the-easiest-prayer-to-sleep-through-so-we-turned-it-into-a-real-alarm-5a2o</link>
      <guid>https://dev.to/iahtisham/fajr-is-the-easiest-prayer-to-sleep-through-so-we-turned-it-into-a-real-alarm-5a2o</guid>
      <description>&lt;p&gt;Fajr, the dawn prayer, is the one most people miss. Not because they don't care. Because they're asleep.&lt;/p&gt;

&lt;p&gt;For years prayer apps have handled it the same way: a notification at prayer time. It lights up the screen for a second, plays a short sound, and it's gone. If your phone is on Silent, you get nothing. If you're in a Sleep Focus, it waits quietly in Notification Center until you wake up, which is exactly the problem.&lt;/p&gt;

&lt;p&gt;When Osama Fahim and I started building Nur, a free prayer app with no ads, this was the part I cared about most. I wanted the adhan for Fajr to wake you up the way your morning alarm does.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why notifications were never going to work
&lt;/h2&gt;

&lt;p&gt;iOS treats notifications as things that can wait. That's the right call for almost every app. A prayer time can't wait, but a third-party app had no honest way to say so. The workarounds were all bad: long notification sounds that get cut off, asking people to turn off Silent mode, or telling them to set their own alarm in the Clock app every night, by hand, for a time that changes every day.&lt;/p&gt;

&lt;p&gt;Then Apple shipped AlarmKit in iOS 26. It lets an app schedule a real alarm, the same kind the Clock app makes. It rings through Silent mode and Focus, and you stop it the same way you stop any alarm. I'd already used it as a contractor on Timey, a shareable alarm app, so I knew what it could do. Fajr was the obvious place to use it next.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tricky part: prayer times move
&lt;/h2&gt;

&lt;p&gt;A morning alarm is simple. It's 7:00 every day.&lt;/p&gt;

&lt;p&gt;Prayer times are not. Fajr shifts a little almost every day through the year, all five prayers depend on where you are, and they change when you travel. So Nur can't set one repeating alarm and walk away.&lt;/p&gt;

&lt;p&gt;What it does instead is keep a rolling week armed. Nur works out the prayer times for the next seven days, for wherever you are, and schedules a separate alarm for every prayer you've turned on. Sunrise is skipped, since it isn't a prayer. Fajr and Maghrib are on by default and you can switch on the others.&lt;/p&gt;

&lt;p&gt;Every time the app refreshes, it checks that week again and tops it up. You turn the alarm on once and it keeps moving with Fajr by itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keeping a week of alarms in sync
&lt;/h2&gt;

&lt;p&gt;Rebuilding every alarm on every refresh would be wasteful, and it can go wrong while an alarm is actually ringing. So each alarm gets an ID built from the prayer and its exact date and minute. Fajr on a given Tuesday at 4:15 always gets the same ID. When Nur refreshes, it can tell straight away which alarms already exist, which ones are still correct, and which ones changed.&lt;/p&gt;

&lt;p&gt;Alarms that are already right are left alone. Alarms for prayers you've switched off are removed. And an alarm that is ringing right now is never touched, whatever else changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Things AlarmKit taught us the hard way
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;You can't schedule over an alarm that already exists.&lt;/strong&gt; When someone changed their adhan recording, every alarm in the week needed the new sound. AlarmKit refused to replace them and gave back an error that didn't explain much. The fix was to cancel the old alarm first and then schedule the new one, for each prayer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;There's a limit to how many alarms an app can hold.&lt;/strong&gt; If Nur hits it, it stops, keeps everything that was confirmed, and records how far ahead you're actually covered. That date shows in the app, so you know the alarms are set up to that day.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One failure shouldn't silence the whole week.&lt;/strong&gt; If one prayer can't be scheduled, Nur carries on with the rest and tells you which one failed, instead of quietly giving up.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Never swap the adhan for a beep.&lt;/strong&gt; AlarmKit plays short sounds bundled with the app, so every adhan recording is checked before it's used: it has to load and be under 30 seconds. If a recording is broken, Nur doesn't schedule a generic system tone in its place. A random beep at 4 a.m. is not what someone asked for.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Time zones will surprise you.&lt;/strong&gt; Nur lets you follow a mosque's timetable, and someone in Karachi following a London mosque heard Isha around midnight. The app was ringing at London's moment instead of at the time shown on the screen. Now the times keep their clock reading on your phone, so the Home screen, the widgets, the Live Activity and the alarms all agree.&lt;/p&gt;

&lt;h2&gt;
  
  
  Staying armed when the app isn't open
&lt;/h2&gt;

&lt;p&gt;A rolling week only works if the app gets to top it up. Nur asks iOS for a background refresh every few hours, but iOS decides whether that actually happens, based on how you use the app. So we don't count on it. Opening the app always refreshes the week, background refresh is a bonus, and the "covered until" date in the app tells you the truth either way.&lt;/p&gt;

&lt;p&gt;There's also a test button in Settings that rings the adhan ten seconds later, so you can hear exactly what Fajr will sound like before you rely on it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd tell another iOS developer
&lt;/h2&gt;

&lt;p&gt;If your app has a moment that really can't be missed, AlarmKit is worth a look. Treat it as a small scheduler you have to keep honest. Give alarms stable IDs, cancel before you replace, expect a limit, never fake the sound, and show people how far ahead they're covered.&lt;/p&gt;

&lt;p&gt;And test it with Silent on. That's the whole point.&lt;/p&gt;

&lt;p&gt;Nur is free, with no ads, on the App Store as "NUR Prayer Times, Quran, Qibla". If you try the Fajr alarm, I'd love to hear how the first morning went.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Nur is built by me and Osama Fahim.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.iahtisham.com/blog/fajr-alarmkit" rel="noopener noreferrer"&gt;iahtisham.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I'm Ahtisham, an independent iOS developer building native Swift apps.&lt;/strong&gt; More from me:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🌐 Portfolio and client work: &lt;a href="https://www.iahtisham.com" rel="noopener noreferrer"&gt;iahtisham.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;💸 &lt;a href="https://apppricekit.iahtisham.com" rel="noopener noreferrer"&gt;AppPriceKit&lt;/a&gt;: free App Store subscription prices by country&lt;/li&gt;
&lt;li&gt;🧾 &lt;a href="https://justinvoice.prismcodelabs.com" rel="noopener noreferrer"&gt;JustInvoice&lt;/a&gt;: invoice maker for iPhone&lt;/li&gt;
&lt;li&gt;📄 &lt;a href="https://scanit.prismcodelabs.com" rel="noopener noreferrer"&gt;ScanIt&lt;/a&gt;: document scanner and PDF for iPhone&lt;/li&gt;
&lt;li&gt;🌙 &lt;a href="https://nur.prismcodelabs.com" rel="noopener noreferrer"&gt;NUR&lt;/a&gt;: prayer times, offline Qur'an and adhkar, free with no ads&lt;/li&gt;
&lt;li&gt;🏢 &lt;a href="https://prismcodelabs.com" rel="noopener noreferrer"&gt;PrismCode Labs&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Find me on &lt;a href="https://x.com/iAhtishamDev" rel="noopener noreferrer"&gt;X @iAhtishamDev&lt;/a&gt; · &lt;a href="https://github.com/iahtisham" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; · &lt;a href="https://www.linkedin.com/in/ahtishamhasankhan" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt; · &lt;a href="https://upwork.com/freelancers/iahtisham" rel="noopener noreferrer"&gt;Upwork&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ios</category>
      <category>swift</category>
      <category>mobile</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Mobile App Development Services: How to Build an App Users Love on Every Phone</title>
      <dc:creator>Appagentix India</dc:creator>
      <pubDate>Wed, 21 Oct 2026 07:53:44 +0000</pubDate>
      <link>https://dev.to/appagentixindia/mobile-app-development-services-how-to-build-an-app-users-love-on-every-phone-1k6</link>
      <guid>https://dev.to/appagentixindia/mobile-app-development-services-how-to-build-an-app-users-love-on-every-phone-1k6</guid>
      <description>&lt;p&gt;Your customers spend most of their screen time inside apps. If your business isn't there, or your app is slow and confusing, they'll pick someone else.&lt;/p&gt;

&lt;p&gt;Building a good app isn't just about writing code. It takes the right plan, the right technology and a team that thinks about real users.&lt;/p&gt;

&lt;p&gt;This guide explains how professional mobile app development services work and what to look for.&lt;/p&gt;

&lt;p&gt;What Do Mobile App Development Services Include?&lt;br&gt;
&lt;a href="https://appagentix.com/locations/app-development-india/" rel="noopener noreferrer"&gt;Mobile app development&lt;/a&gt; services cover everything needed to take an idea to the app stores and keep it running well.&lt;/p&gt;

&lt;p&gt;A complete service usually includes:&lt;/p&gt;

&lt;p&gt;Discovery and planning: understanding your users, goals and must-have features&lt;br&gt;
UI/UX design: wireframes, screens and a simple user flow&lt;br&gt;
Development: building the app and its backend&lt;br&gt;
Testing: checking performance, security and usability on real devices&lt;br&gt;
Launch: publishing on the Apple App Store and Google Play&lt;br&gt;
Support: updates, fixes and new features after launch&lt;br&gt;
Skipping any of these steps usually shows up later as bugs, poor reviews or costly rework.&lt;/p&gt;

&lt;p&gt;Native or Cross-Platform: Which Is Right for My App?&lt;br&gt;
This is one of the first decisions in any mobile app project.&lt;/p&gt;

&lt;p&gt;Native app development means building separately for each platform: Swift for iOS and Kotlin for Android. It gives the best performance and full access to device features. It's ideal for apps with heavy graphics, complex animations or deep hardware use.&lt;/p&gt;

&lt;p&gt;Cross-platform app development uses one codebase for both platforms. The two most popular options are:&lt;/p&gt;

&lt;p&gt;React Native: great for teams with JavaScript skills and apps that need fast updates&lt;br&gt;
Flutter: known for smooth, consistent UI and strong performance&lt;br&gt;
For most business apps and startups, cross-platform saves time and cost while still giving a near-native experience. Native makes sense when performance or platform-specific features are critical.&lt;/p&gt;

&lt;p&gt;What Makes a Mobile App Successful?&lt;br&gt;
A successful app isn't the one with the most features. It's the one people open again and again.&lt;/p&gt;

&lt;p&gt;Here's what matters most:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Speed. Users expect apps to open in seconds. Slow screens lead to uninstalls.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Simple design. Every extra tap is a reason to leave. Clean layouts and clear buttons win.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Works on real phones and networks. Many users have budget devices and patchy internet. Good apps are tested on these conditions, not just on the latest flagship.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Security. Protect user data with secure login, encrypted storage and safe APIs.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Regular updates. Fixing issues quickly and improving features keeps ratings high.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;How Does the App Development Process Work?&lt;br&gt;
A professional team follows a clear process so there are no surprises:&lt;/p&gt;

&lt;p&gt;Workshop: define the problem, users and success goals&lt;br&gt;
Scope: list features and decide what goes into version one&lt;br&gt;
Design: create screens and test the flow with real users&lt;br&gt;
Build: develop in short cycles with regular demos&lt;br&gt;
Test: check every feature on multiple devices&lt;br&gt;
Launch: submit to the app stores and monitor early feedback&lt;br&gt;
Regular demos keep you in control. You see progress every week or two, not just at the end.&lt;/p&gt;

&lt;p&gt;Why Should Startups Begin With an MVP?&lt;br&gt;
For startups, the biggest risk isn't bad code. It's building something nobody wants.&lt;/p&gt;

&lt;p&gt;MVP development services solve this. An MVP (Minimum Viable Product) is a simple first version with only the core features. It helps you:&lt;/p&gt;

&lt;p&gt;Test your idea with real users quickly&lt;br&gt;
Spend less before you have proof&lt;br&gt;
Show traction to investors&lt;br&gt;
Learn what to build next from real feedback&lt;br&gt;
A good MVP focuses on one core problem and does it well. Once users respond, you can expand with confidence.&lt;/p&gt;

&lt;p&gt;When Do You Need Custom App Development?&lt;br&gt;
Off-the-shelf apps work for common needs. But many businesses have workflows that ready-made tools can't handle.&lt;/p&gt;

&lt;p&gt;Custom app development services build software around how your business actually works. This is useful when you need to:&lt;/p&gt;

&lt;p&gt;Connect with existing systems like CRM, ERP or inventory tools&lt;br&gt;
Support field teams, bookings, deliveries or internal approvals&lt;br&gt;
Own your data and avoid paying for features you don't use&lt;br&gt;
A custom app grows with your business instead of forcing your team to work around its limits.&lt;/p&gt;

&lt;p&gt;Can AI Features Be Added to a Mobile App?&lt;br&gt;
Yes, and you don't need to rebuild your app to do it.&lt;/p&gt;

&lt;p&gt;AI app solutions can add features like smart search, chat assistants, personalized recommendations and automatic data entry from photos or documents. Most of these connect through a secure backend, so they can be added to new or existing apps step by step.&lt;/p&gt;

&lt;p&gt;The key is to start with one AI feature that solves a real user problem.&lt;/p&gt;

&lt;p&gt;FAQs&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;How long does it take to build a mobile app?&lt;br&gt;
It depends on features and complexity. A focused MVP takes much less time than a full-featured app with many integrations.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Should I build for iOS or Android first?&lt;br&gt;
Look at where your users are. If you're unsure, cross-platform development with React Native or Flutter lets you launch on both.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Is React Native or Flutter better?&lt;br&gt;
Both are excellent. React Native suits teams with JavaScript skills, while Flutter is known for consistent UI. The right choice depends on your project.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;How much do mobile app development services cost?&lt;br&gt;
Cost depends on features, design, platforms and integrations. Starting with an MVP keeps the first investment lower.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Can you update my existing app instead of building a new one?&lt;br&gt;
Yes. Many apps can be improved, redesigned or extended with new features without starting over.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Will my app work well on budget phones?&lt;br&gt;
It should, if it's built and tested for them. Testing on real low-end devices is part of a good development process.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Can AI be added to my app later?&lt;br&gt;
Yes. AI features can be added after launch through APIs and a secure backend.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Conclusion&lt;br&gt;
Great apps come from clear planning, the right technology and a focus on real users. Whether you need a native or cross-platform app, a quick MVP or a fully custom solution, the process matters as much as the code.&lt;/p&gt;

&lt;p&gt;If you're planning your next app, &lt;a href="https://appagentix.com/contact-us/" rel="noopener noreferrer"&gt;Appagentix&lt;/a&gt; offers mobile app development services for iOS, Android, React Native and Flutter, from first idea to launch and beyond.&lt;/p&gt;

</description>
      <category>mobileapplicationdevelopment</category>
      <category>mobile</category>
      <category>androidapplicationdevelopment</category>
      <category>ios</category>
    </item>
    <item>
      <title>Drawing SwiftUI previews in a browser when SwiftUI only runs on Apple's systems</title>
      <dc:creator>SwiftQuest</dc:creator>
      <pubDate>Wed, 21 Oct 2026 07:34:44 +0000</pubDate>
      <link>https://dev.to/swiftquest/drawing-swiftui-previews-in-a-browser-when-swiftui-only-runs-on-apples-systems-5n5</link>
      <guid>https://dev.to/swiftquest/drawing-swiftui-previews-in-a-browser-when-swiftui-only-runs-on-apples-systems-5n5</guid>
      <description>&lt;p&gt;I've been building a SwiftUI course that runs in the browser, and the hard part was obvious from day one: Apple's SwiftUI only exists on Apple platforms, and the learner's code compiles and runs on Linux servers. Here's how it ended up working, in case it's useful to anyone or someone spots a hole in it.&lt;/p&gt;

&lt;h2&gt;
  
  
  A stand-in SwiftUI module
&lt;/h2&gt;

&lt;p&gt;There's a stand-in &lt;code&gt;SwiftUI&lt;/code&gt; module with the same names and signatures as Apple's. So the learner's file is the file they'd write in Xcode, &lt;code&gt;import SwiftUI&lt;/code&gt; and all, and their mistakes get the same compiler errors.&lt;/p&gt;

&lt;p&gt;The module draws nothing. A run turns the views into a tree of plain nodes, with each modifier wrapping what it modifies, in order. That order matters for the same reason it matters in real SwiftUI: &lt;code&gt;.padding().background(.blue)&lt;/code&gt; and &lt;code&gt;.background(.blue).padding()&lt;/code&gt; are different trees.&lt;/p&gt;

&lt;h2&gt;
  
  
  #Preview is what gets drawn
&lt;/h2&gt;

&lt;p&gt;A macro can't name something a host program can find. So before compiling, the &lt;code&gt;#Preview&lt;/code&gt; line gets rewritten to a plain &lt;code&gt;let&lt;/code&gt; (line numbers kept, so compiler errors still point at the right line), and a generated &lt;code&gt;main.swift&lt;/code&gt; hands every preview to the host.&lt;/p&gt;

&lt;h2&gt;
  
  
  One run explores the whole screen
&lt;/h2&gt;

&lt;p&gt;After rendering, the host presses every control once and keeps each screen that follows, up to 160 screens within two seconds. When you tap a button in the browser, it just follows a path that was already explored, so there's no second run. A tap that crashes gets pinned to its button instead of killing the whole preview.&lt;/p&gt;

&lt;h2&gt;
  
  
  The browser does the layout
&lt;/h2&gt;

&lt;p&gt;The browser reimplements SwiftUI's proposal and response rules: stacks sharing space by flexibility, frames and padding proposing and reporting sizes, text wrapping to what it's offered. The constants came from frames the iOS simulator printed: line heights, spacing, button padding, symbol sizes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checking it against the real thing
&lt;/h2&gt;

&lt;p&gt;Every example is also drawn with real SwiftUI on the iOS simulator, and the two pictures are compared. The weakest spot is SF Symbols, which are stand-in icons because their licence keeps them on Apple platforms. It's close to Xcode, not identical, and you still finish real apps in Xcode on a Mac.&lt;/p&gt;




&lt;p&gt;This is the SwiftUI part of SwiftQuest, a course I'm building. Every exercise runs on a real Swift compiler, each run in its own throwaway gVisor container (I wrote about that sandbox &lt;a href="https://dev.to/swiftquest/running-strangers-swift-code-one-gvisor-container-per-run-52mf"&gt;here&lt;/a&gt;). The SwiftUI course has 8 destinations and 24 quests, from your first view to one app of your own built in three milestones, and it's open to beta testers now next to the Swift course. There's no deadline, you can be approved the same day, and testers get Pro for life: &lt;a href="https://swiftquest.dev/beta?ref=devto" rel="noopener noreferrer"&gt;https://swiftquest.dev/beta?ref=devto&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Happy to go deeper on any of the above in the comments.&lt;/p&gt;

</description>
      <category>swift</category>
      <category>swiftui</category>
      <category>ios</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I measured what Safari actually gives your web page on iPhone Duo</title>
      <dc:creator>tyoukyou23</dc:creator>
      <pubDate>Wed, 21 Oct 2026 06:02:49 +0000</pubDate>
      <link>https://dev.to/tyoukyou23/i-measured-what-safari-actually-gives-your-web-page-on-iphone-duo-8jp</link>
      <guid>https://dev.to/tyoukyou23/i-measured-what-safari-actually-gives-your-web-page-on-iphone-duo-8jp</guid>
      <description>&lt;p&gt;iPhone Duo ships on October 23. Most "is my site ready?" guides quote viewport sizes worked out from the spec sheet — screen pixels divided by three: 466 × 678 folded, 626 × 890 or 890 × 626 unfolded.&lt;/p&gt;

&lt;p&gt;That isn't what your page gets. I opened a probe page in Safari on the iPhone Duo simulator (Xcode 27.1) and logged what the page actually receives in each pose. A few results surprised me, and two of them I haven't seen written down anywhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  The numbers
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pose&lt;/th&gt;
&lt;th&gt;What the page gets (&lt;code&gt;innerWidth × innerHeight&lt;/code&gt;)&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Folded&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;382 × 580&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Safari puts its status bar and buttons in a column on the &lt;strong&gt;right&lt;/strong&gt;. The screen is 466 wide; your page is 84 px narrower.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unfolded, upright&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;669 × 825&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Between Tailwind's &lt;code&gt;sm&lt;/code&gt; (640) and &lt;code&gt;md&lt;/code&gt; (768) — a band few sites test.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unfolded, sideways&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;951 × 543&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Desktop width. Still a finger. Only 543 px tall.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Folded, sideways&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;678 × 340&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The shortest pose of all.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The inner display itself is 2853 × 2007 px = &lt;strong&gt;951 × 669 pt&lt;/strong&gt; in the simulator, so even the "626 × 890 screen" figure going around doesn't match.&lt;/p&gt;

&lt;p&gt;Every pose reports &lt;code&gt;(pointer: coarse)&lt;/code&gt; and no hover. Folding or unfolding fires &lt;code&gt;resize&lt;/code&gt;; the page is not reloaded. &lt;code&gt;screen.width&lt;/code&gt; keeps the folded value (466) after you unfold, so don't size anything from &lt;code&gt;screen&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Wide doesn't mean mouse
&lt;/h2&gt;

&lt;p&gt;At 951 px most sites switch to their desktop layout. That's where this pattern lives:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- hidden on "desktop", shown on hover --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"opacity-100 sm:opacity-0 group-hover:opacity-100"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Edit&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On an unfolded Duo — or any iPad — that button is never visible, because a finger has no hover. Gate it on the input, not the width:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="k"&gt;@media&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hover&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;hover&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;and&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;pointer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;fine&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nc"&gt;.row&lt;/span&gt; &lt;span class="nc"&gt;.edit&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;opacity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nc"&gt;.row&lt;/span&gt;&lt;span class="nd"&gt;:hover&lt;/span&gt; &lt;span class="nc"&gt;.edit&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;opacity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&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;h2&gt;
  
  
  2. Half-folding is invisible to the web
&lt;/h2&gt;

&lt;p&gt;I expected the half-open "tabletop" pose to show up somewhere — Viewport Segments, Device Posture, a media query. Nothing. When you half-fold the phone, Safari moves to the outer screen and the page gets exactly the folded viewport, 382 × 580. No posture event, no segments, all posture media queries false.&lt;/p&gt;

&lt;p&gt;So on iPhone Duo there are only two states the web can see: folded and open. You don't need a tabletop layout — and you can't build one.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. One fold fires 2–3 resizes, through a size that belongs to no pose
&lt;/h2&gt;

&lt;p&gt;I logged every &lt;code&gt;resize&lt;/code&gt; while folding and unfolding:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;resize → 466 × 498     ← transient: no pose has this size
orientation → landscape
resize → 951 × 543     ← open
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The 466 × 498 step happens mid-swing, while the screens are switching over. If your code re-lays out on every &lt;code&gt;resize&lt;/code&gt;, it does that work for a frame nobody sees. Worse, a list–detail layout that docks the detail beside the list above some width will flip to a drawer and back in one gesture.&lt;/p&gt;

&lt;p&gt;Wait for the size to settle (~150 ms of no resizes) before switching layouts. If you measure with &lt;code&gt;ResizeObserver&lt;/code&gt;, debounce the &lt;em&gt;decision&lt;/em&gt;, not the measurement.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. &lt;code&gt;100vh&lt;/code&gt; is taller than the page
&lt;/h2&gt;

&lt;p&gt;Folded, Safari gives the page 580 px of height:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;100vh&lt;/code&gt; / &lt;code&gt;100lvh&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;598 — 18 px too tall&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;100dvh&lt;/code&gt; / &lt;code&gt;100svh&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;580 — correct at rest&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A full-height sheet sized with &lt;code&gt;vh&lt;/code&gt; hides its bottom button. &lt;code&gt;dvh&lt;/code&gt; is right — until the keyboard opens, at which point no viewport unit follows it. Read &lt;code&gt;visualViewport.height&lt;/code&gt; for that.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Safari's right-hand column ignores &lt;code&gt;theme-color&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Folded, Safari's controls live in a column to the right of your page, and they don't use &lt;code&gt;theme-color&lt;/code&gt;. Safari samples the page edge instead, and only from fixed or sticky elements. People have tried widening the page to the edge to colour it, which causes horizontal scrolling.&lt;/p&gt;

&lt;p&gt;What worked in the simulator: an 8 px &lt;code&gt;position: fixed&lt;/code&gt; strip on the right edge. The whole column takes its colour, and &lt;code&gt;scrollWidth&lt;/code&gt; stays at 382 — no horizontal scroll.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.toolbar-tint&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;fixed&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;top&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;right&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;bottom&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#1e3a8a&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;pointer-events&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;none&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;Give it the colour of whatever is next to it, so it reads as part of the page.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd check before Thursday
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Search for width-gated hover: &lt;code&gt;sm:opacity-0&lt;/code&gt;, &lt;code&gt;md:hidden&lt;/code&gt; on controls, &lt;code&gt;:hover&lt;/code&gt; rules outside &lt;code&gt;(hover: hover)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Open your site at &lt;strong&gt;669 px wide&lt;/strong&gt; — between your breakpoints.&lt;/li&gt;
&lt;li&gt;Look at your layout at &lt;strong&gt;951 × 543&lt;/strong&gt; with touch emulation on. Is the header eating half the height?&lt;/li&gt;
&lt;li&gt;Replace &lt;code&gt;100vh&lt;/code&gt; with &lt;code&gt;100dvh&lt;/code&gt; (keep &lt;code&gt;vh&lt;/code&gt; as a fallback).&lt;/li&gt;
&lt;li&gt;Delete user-agent checks — the Duo says plain &lt;code&gt;iPhone&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Don't debounce resizing in general, but do let a fold settle before swapping layouts.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  I packaged this up
&lt;/h2&gt;

&lt;p&gt;All of the above came out of adapting a real app (a school management system our teachers use on PCs, iPads and phones), so I pulled the reusable parts into a small React library, &lt;strong&gt;fold-ready&lt;/strong&gt; — zero dependencies, SSR-safe, React 17+:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;SplitHost&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Pane&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useSelection&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;KeepPlace&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ToolbarTint&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fold-ready&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Inbox&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;mails&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;sel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setSel&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useSelection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sel&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;           &lt;span class="c1"&gt;// ?sel=42, survives reload&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;open&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;mails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;sel&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="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;SplitHost&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;                                         &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* docks side by side for wide touch screens */&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;KeepPlace&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;                                       &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* same row on top after a fold */&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;MailList&lt;/span&gt; &lt;span class="na"&gt;mails&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;mails&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;onOpen&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setSel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;KeepPlace&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;open&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Pane&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Message"&lt;/span&gt; &lt;span class="na"&gt;onClose&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setSel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;…&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Pane&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;SplitHost&lt;/span&gt;&lt;span class="p"&gt;&amp;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;ul&gt;
&lt;li&gt;
&lt;code&gt;useInputMode()&lt;/code&gt; → &lt;code&gt;{ touch, hover, short }&lt;/code&gt;, plus Tailwind variants &lt;code&gt;can-hover:&lt;/code&gt; &lt;code&gt;touch:&lt;/code&gt; &lt;code&gt;short:&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;SplitHost&lt;/code&gt; + &lt;code&gt;Pane&lt;/code&gt;: detail docks beside the list when there's room on a touch screen, drawer otherwise; waits for a fold to settle&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;KeepPlace&lt;/code&gt;: keeps the same item at the top (and your caret) when the width changes&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ToolbarTint&lt;/code&gt;, &lt;code&gt;useVisibleViewport&lt;/code&gt;, &lt;code&gt;useSettledViewport&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;FoldReadyInspector&lt;/code&gt;: a dev overlay that finds hover-only controls on the current page&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Mouse users keep the behaviour they had unless you opt in.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Duo Lab&lt;/strong&gt; shows any URL in the measured poses and plays the fold step by step — including that 466 × 498 frame: &lt;a href="https://tyoukyou23.github.io/fold-ready/#/lab" rel="noopener noreferrer"&gt;https://tyoukyou23.github.io/fold-ready/#/lab&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Code: &lt;a href="https://github.com/tyoukyou23/fold-ready" rel="noopener noreferrer"&gt;https://github.com/tyoukyou23/fold-ready&lt;/a&gt; · npm: &lt;code&gt;fold-ready&lt;/code&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Caveat
&lt;/h2&gt;

&lt;p&gt;These are simulator numbers. I'll check them on hardware when the phone arrives. If you have a Duo before I do, the demo's &lt;strong&gt;Measure&lt;/strong&gt; page prints what your Safari reports and copies it as JSON — I'd love to compare: &lt;a href="https://tyoukyou23.github.io/fold-ready/#/measure" rel="noopener noreferrer"&gt;https://tyoukyou23.github.io/fold-ready/#/measure&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>css</category>
      <category>ios</category>
      <category>react</category>
    </item>
    <item>
      <title>Etisalat Recharge: Fastest Ways to Top Up Your e&amp; Line</title>
      <dc:creator>Telecomdesk</dc:creator>
      <pubDate>Wed, 21 Oct 2026 06:00:00 +0000</pubDate>
      <link>https://dev.to/telecomdesk/etisalat-recharge-fastest-ways-to-top-up-your-e-line-3kmn</link>
      <guid>https://dev.to/telecomdesk/etisalat-recharge-fastest-ways-to-top-up-your-e-line-3kmn</guid>
      <description>&lt;p&gt;An e&amp;amp; (Etisalat) prepaid line gives you four realistic recharge routes, and they are not equally good. The difference between the best and the worst is about fifteen minutes and one avoidable queue. Here is the ranking, with the details that trip people up.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The My Etisalat app - the permanent fix
&lt;/h2&gt;

&lt;p&gt;Card registered in the app, top-up in seconds, balance confirmation immediate. The app route also shows what the recharge bought you: credit, bonus data, or a promotional top-up bundle - information the scratch-card route never gives you. If your line tops up twice a month or more, register the card once and never think about it again. The &lt;a href="https://telecomdeskuae.com/quick-pay/etisalat-mobile-recharge" rel="noopener noreferrer"&gt;Etisalat quick-pay page&lt;/a&gt; mirrors the flow on the web for anyone without the app.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Web quick-pay - the somebody-else's-line route
&lt;/h2&gt;

&lt;p&gt;The web payment page tops up any e&amp;amp; number with any card. This is the route for topping up a parent's line, a domestic worker's line, or a visitor's line from your phone - no app on their device required, receipt lands with you.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Scratch cards and ATM codes - the cash route
&lt;/h2&gt;

&lt;p&gt;Physical recharge cards are sold at every supermarket and petrol station; ATMs issue codes directly to registered accounts. Each card's scratch code is loaded by dialing the recharge sequence - the current one is documented in our &lt;a href="https://telecomdeskuae.com/blog/etisalat-recharge-10-aed.html" rel="noopener noreferrer"&gt;Etisalat recharge guide&lt;/a&gt;. Watch the card's expiry date: old stock in some shops has voided codes, which the counter should replace on the spot.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Counters and kiosks - cash, no rush
&lt;/h2&gt;

&lt;p&gt;Full-price, queue-included, but human: the right route when a dispute needs resolving in person anyway. For pure recharges it is strictly the slow option.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two things worth knowing once
&lt;/h2&gt;

&lt;p&gt;Small top-ups (the AED 10 class) sometimes carry a shorter validity window than larger ones - check what your amount buys. And promotional top-up bonuses rotate monthly; before a large top-up, glance at the app's offers screen, because the same AED 100 occasionally buys credit plus data depending on the week.&lt;/p&gt;

&lt;p&gt;The fastest way to see every guide we maintain is the &lt;a href="https://telecomdeskuae.com/sitemap.xml" rel="noopener noreferrer"&gt;full sitemap&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>howto</category>
      <category>mobile</category>
      <category>productivity</category>
      <category>ios</category>
    </item>
    <item>
      <title>5 timing bugs I hit building timed blocks with Screen Time's DeviceActivity (iOS)</title>
      <dc:creator>ayumu kitabayashi</dc:creator>
      <pubDate>Wed, 21 Oct 2026 05:19:54 +0000</pubDate>
      <link>https://dev.to/ayumu_kitabayashi_8e0de96/5-timing-bugs-i-hit-building-timed-blocks-with-screen-times-deviceactivity-ios-5dnc</link>
      <guid>https://dev.to/ayumu_kitabayashi_8e0de96/5-timing-bugs-i-hit-building-timed-blocks-with-screen-times-deviceactivity-ios-5dnc</guid>
      <description>&lt;p&gt;I build minulo, an iPhone app that hides Reels and Shorts. One feature is a timed block: lock the official Instagram app with Screen Time for, say, 30 minutes, and have it unlock on its own even if minulo isn't running. That part runs on DeviceActivity (DeviceActivityCenter.startMonitoring + a DeviceActivitySchedule, with the unlock happening in a DeviceActivityMonitor extension's intervalDidEnd).&lt;/p&gt;

&lt;p&gt;It took five rounds of bugs to get the timing right. All five are about how the schedule's start and end are interpreted, so they apply whether you write Swift directly or go through a wrapper like react-native-device-activity (which I use, with my own additions).&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Windows shorter than 15 minutes are rejected
&lt;/h2&gt;

&lt;p&gt;Pass a 5-minute window to startMonitoring and you get an error. In my testing on device, anything under 15 minutes was refused.&lt;/p&gt;

&lt;p&gt;My first workaround was "now + 15 minutes", which meant a 5-minute break stayed unlocked for 15. The fix was to keep the end fixed and move the start back instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// keep the end; pull the start back so the window is 15 minutes&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;shortWindow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;untilMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ceil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&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="nx"&gt;untilMs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getTime&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;60000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;60000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getTime&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getTime&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60000&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="na"&gt;s&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;dated&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getTime&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="c1"&gt;// if the start falls on the previous day, include the date (see #3)&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;dated&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getDate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getDate&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;A start in the past is fine; intervalDidEnd still fires at the end.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Same hour and minute for start and end is rejected
&lt;/h2&gt;

&lt;p&gt;With hour/minute-only DateComponents, a 24-hour block (13:05 today to 13:05 tomorrow) reads as start == end and is refused. I shift the start one minute &lt;em&gt;later&lt;/em&gt;, giving a 23h59m window. Shifting it earlier would make a 1-minute window, which runs into #1.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getHours&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getHours&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getMinutes&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getMinutes&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getTime&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;60000&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;h2&gt;
  
  
  3. Just after midnight, hour/minute isn't enough
&lt;/h2&gt;

&lt;p&gt;Because of #1, a break ending at 00:05 starts at 23:50. Sent as hour and minute only, that was read as 23:50 &lt;em&gt;today&lt;/em&gt;, and the window broke: a 5-minute break after midnight stayed open for 15. When the start falls on the previous day, I now include year, month and day:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;winEdge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;dated&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;dated&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;year&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getFullYear&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="na"&gt;month&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getMonth&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;day&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getDate&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="na"&gt;hour&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getHours&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="na"&gt;minute&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getMinutes&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="na"&gt;second&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="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;hour&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getHours&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="na"&gt;minute&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getMinutes&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="na"&gt;second&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  4. The number of monitored activities is limited
&lt;/h2&gt;

&lt;p&gt;Registering one activity per weekday for each schedule (bedtime, class hours…) adds up fast, and the number of monitored activities is reportedly capped at around 20. Past that, startMonitoring fails, and the block simply never unlocks. Two changes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;daily schedules are a single activity without a weekday&lt;/li&gt;
&lt;li&gt;failures are stored instead of swallowed, so the app can show that something is wrong
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;DA&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startMonitoring&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;FOCUS_ACTIVITY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;intervalStart&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;intervalEnd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;repeats&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&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;catch&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;focus:nativeErr&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;slice&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;120&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  5. "Unblock everything" also unblocks things that should stay blocked
&lt;/h2&gt;

&lt;p&gt;Clearing all shields in intervalDidEnd also removed other blocks that should still be active at that moment, like a bedtime schedule or an app whose daily limit was used up. A manual block ending in the middle of the night took the bedtime block down with it until morning.&lt;/p&gt;

&lt;p&gt;Now the end action re-applies whatever should still be active at that time, computed when the block starts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;DA&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;configureActions&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;activityName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FOCUS_ACTIVITY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;callbackName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;intervalDidEnd&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;actions&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="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;resetBlocks&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="c1"&gt;// re-apply schedules that are still in their window at the end time&lt;/span&gt;
  &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;during&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;blockSelection&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;familyActivitySelectionId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;id&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 extension also drops re-applies that belong to schedules already finished, so a repeating registration doesn't carry them into the next day.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Windows under 15 minutes (on device) are refused: fix the end, move the start back.&lt;/li&gt;
&lt;li&gt;Identical start and end hour:minute are refused: move the start one minute later.&lt;/li&gt;
&lt;li&gt;After midnight the start lands on the previous day: include the date only then.&lt;/li&gt;
&lt;li&gt;Monitored activities are capped: merge daily schedules and record failures.&lt;/li&gt;
&lt;li&gt;Clearing everything clears too much: re-apply what should stay, at the end time.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Related: &lt;a href="https://minulo.hashnode.dev/opening-your-own-app-from-a-screen-time-block-screen-on-ios-shieldaction-can-t-so-use-a-notification" rel="noopener noreferrer"&gt;opening your own app from a Screen Time block screen&lt;/a&gt; (ShieldAction can't open URLs, so I go through a notification).&lt;/p&gt;

&lt;p&gt;minulo (hiding Reels and Shorts is free): &lt;a href="https://minulo.app/en/?ref=devto-1021b" rel="noopener noreferrer"&gt;https://minulo.app/en/?ref=devto-1021b&lt;/a&gt;&lt;br&gt;
List of apps and methods for hiding Reels and Shorts on iPhone: &lt;a href="https://github.com/ayumu-kitabayashi/awesome-reels-shorts-blockers" rel="noopener noreferrer"&gt;https://github.com/ayumu-kitabayashi/awesome-reels-shorts-blockers&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ios</category>
      <category>swift</category>
      <category>reactnative</category>
      <category>mobile</category>
    </item>
    <item>
      <title>Unexpected behaviour of Expo Router deep links in production</title>
      <dc:creator>Amit chakraborty</dc:creator>
      <pubDate>Wed, 21 Oct 2026 04:30:06 +0000</pubDate>
      <link>https://dev.to/techamit95ch/unexpected-behaviour-of-expo-router-deep-links-in-production-56p1</link>
      <guid>https://dev.to/techamit95ch/unexpected-behaviour-of-expo-router-deep-links-in-production-56p1</guid>
      <description>&lt;h2&gt;
  
  
  The situation
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;You are a mobile engineer on a two‑week sprint.&lt;/em&gt; &lt;br&gt;
Your team has added a new “Invite” screen that must be reachable from an email link (&lt;code&gt;myapp://invite/abc123&lt;/code&gt;). You follow the Expo Router tutorial, add a file &lt;code&gt;app/invite/[code].tsx&lt;/code&gt;, run the app in the Expo Go client, tap the link on a device and the screen appears instantly. &lt;/p&gt;

&lt;p&gt;During the nightly build, the QA team reports that the same link opened the app on Android but stayed on a blank white screen on iOS, and on Android it sometimes opened the home screen instead of the invite screen. The failure only appears in the production build generated by EAS. &lt;/p&gt;

&lt;p&gt;Cost: the release is blocked for another day, the QA cycle is delayed, and the product manager has to explain the missing feature to stakeholders.&lt;/p&gt;

&lt;p&gt;The rest of this guide shows why the tutorial‑level configuration breaks under production conditions, and how to make deep links reliable with Expo Router.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why it happens
&lt;/h2&gt;

&lt;p&gt;Expo Router relies on two mechanisms:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Native intent/association handling&lt;/strong&gt; – Android &lt;code&gt;intent-filter&lt;/code&gt; and iOS &lt;em&gt;Associated Domains&lt;/em&gt; tell the OS that a URL scheme or universal link belongs to the app. &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JS‑side route resolution&lt;/strong&gt; – &lt;code&gt;expo-router&lt;/code&gt; parses the incoming URL, matches it against the file‑system based route tree, and then renders the matched screen.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The tutorial usually covers only the first step for the &lt;em&gt;development&lt;/em&gt; client:&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="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"expo"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"scheme"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"myapp"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&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 a production build the following hidden constraints appear:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Constraint&lt;/th&gt;
&lt;th&gt;How it manifests&lt;/th&gt;
&lt;th&gt;Why it matters&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Scheme registration on Android&lt;/strong&gt; – The generated &lt;code&gt;AndroidManifest.xml&lt;/code&gt; only contains a &lt;code&gt;&amp;lt;data android:scheme="myapp"/&amp;gt;&lt;/code&gt; if &lt;code&gt;expo.android.scheme&lt;/code&gt; is set &lt;strong&gt;and&lt;/strong&gt; the &lt;code&gt;expo-router&lt;/code&gt; plugin is applied. Without the plugin the manifest lacks the &lt;code&gt;&amp;lt;intent-filter&amp;gt;&lt;/code&gt; block, so Android falls back to the default activity.&lt;/td&gt;
&lt;td&gt;Links open the app but land on the default (&lt;code&gt;Home&lt;/code&gt;) screen.&lt;/td&gt;
&lt;td&gt;The router receives &lt;code&gt;null&lt;/code&gt; for &lt;code&gt;initialURL&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Associated Domains on iOS&lt;/strong&gt; – The iOS project must have an &lt;code&gt;entitlements&lt;/code&gt; file with &lt;code&gt;applinks:myapp.com&lt;/code&gt; and a matching Apple App Site Association (AASA) file on the domain. The tutorial omits this because Expo Go uses the custom scheme only.&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;myapp://invite/abc123&lt;/code&gt; works in development, but a universal link &lt;code&gt;https://myapp.com/invite/abc123&lt;/code&gt; opened from Mail shows a white screen.&lt;/td&gt;
&lt;td&gt;The native bridge receives the URL after the JS runtime has already rendered the home screen, causing a race where the router never processes the deep link.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Bundle‑time route hashing&lt;/strong&gt; – Expo Router generates a route manifest (&lt;code&gt;router-manifest.json&lt;/code&gt;) at build time. If a file is added after the manifest is generated (e.g., a new &lt;code&gt;[code].tsx&lt;/code&gt; added in a later commit without a clean rebuild), the manifest on the device does not contain the new route.&lt;/td&gt;
&lt;td&gt;The app logs &lt;code&gt;Unable to find route for path "/invite/abc123"&lt;/code&gt; and stays on a blank screen.&lt;/td&gt;
&lt;td&gt;The router cannot match the incoming URL.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;JS runtime readiness&lt;/strong&gt; – In production the JavaScript bundle may take 500‑800 ms to evaluate on low‑end devices. If the native side forwards the deep link before &lt;code&gt;router.isReady&lt;/code&gt; resolves, the router drops the URL.&lt;/td&gt;
&lt;td&gt;Intermittent failures on Android 6‑7 devices, where the link sometimes works and sometimes does not.&lt;/td&gt;
&lt;td&gt;No deterministic behaviour; users perceive the feature as flaky.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Expo Updates caching&lt;/strong&gt; – When using &lt;code&gt;expo-updates&lt;/code&gt;, a cached bundle may be served that does not contain the latest route files. The deep link will resolve to a missing route.&lt;/td&gt;
&lt;td&gt;QA sees the same link working on a fresh install but failing after a hot‑update.&lt;/td&gt;
&lt;td&gt;The update process does not invalidate the route manifest.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These constraints are not highlighted in the basic tutorial because they only surface when the app is compiled, signed, and run without the Expo Go wrapper.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix, step by step
&lt;/h2&gt;

&lt;p&gt;The following procedure assumes you are using &lt;strong&gt;Expo SDK 48&lt;/strong&gt;, &lt;strong&gt;expo-router 2.0.0&lt;/strong&gt;, &lt;strong&gt;React Native 0.71.3&lt;/strong&gt;, and building with &lt;strong&gt;EAS Build&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Add the router plugin to &lt;code&gt;app.json&lt;/code&gt;
&lt;/h3&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="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"expo"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"MyApp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"slug"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"myapp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"scheme"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"myapp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"plugins"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"expo-router"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"origin"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"expo-router"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&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;&lt;em&gt;Why&lt;/em&gt;: The plugin injects the required &lt;code&gt;&amp;lt;intent-filter&amp;gt;&lt;/code&gt; on Android and ensures the route manifest is regenerated on every build.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Verify&lt;/em&gt;: Run &lt;code&gt;eas build --profile preview --platform android&lt;/code&gt;. After the build finishes, download the &lt;code&gt;apk&lt;/code&gt; and unzip it (&lt;code&gt;unzip myapp.apk&lt;/code&gt;). Open &lt;code&gt;AndroidManifest.xml&lt;/code&gt; and confirm the presence of:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;intent-filter&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;action&lt;/span&gt; &lt;span class="na"&gt;android:name=&lt;/span&gt;&lt;span class="s"&gt;"android.intent.action.VIEW"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;category&lt;/span&gt; &lt;span class="na"&gt;android:name=&lt;/span&gt;&lt;span class="s"&gt;"android.intent.category.DEFAULT"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;category&lt;/span&gt; &lt;span class="na"&gt;android:name=&lt;/span&gt;&lt;span class="s"&gt;"android.intent.category.BROWSABLE"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;data&lt;/span&gt; &lt;span class="na"&gt;android:scheme=&lt;/span&gt;&lt;span class="s"&gt;"myapp"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/intent-filter&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On iOS, open the generated Xcode project (&lt;code&gt;ios/MyApp.xcodeproj&lt;/code&gt;) and check that &lt;code&gt;MyApp/Info.plist&lt;/code&gt; contains the &lt;code&gt;CFBundleURLTypes&lt;/code&gt; entry for &lt;code&gt;myapp&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Configure Associated Domains for iOS
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;In the Apple Developer portal, enable &lt;strong&gt;Associated Domains&lt;/strong&gt; for the app identifier. &lt;/li&gt;
&lt;li&gt;Add a domain entry &lt;code&gt;applinks:myapp.com&lt;/code&gt;. &lt;/li&gt;
&lt;li&gt;Host an &lt;code&gt;apple-app-site-association&lt;/code&gt; file at &lt;code&gt;https://myapp.com/.well-known/apple-app-site-association&lt;/code&gt; with the following JSON (no &lt;code&gt;.json&lt;/code&gt; extension, &lt;code&gt;application/json&lt;/code&gt; MIME type):
&lt;/li&gt;
&lt;/ol&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="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"applinks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"apps"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"details"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"appID"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;TEAM_ID&amp;gt;.myapp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"paths"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/invite/*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/profile/*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/*"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&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;ol&gt;
&lt;li&gt;In Xcode, edit the &lt;strong&gt;Signing &amp;amp; Capabilities&lt;/strong&gt; tab, add &lt;strong&gt;Associated Domains&lt;/strong&gt;, and paste &lt;code&gt;applinks:myapp.com&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;em&gt;Why&lt;/em&gt;: Without this, universal links (&lt;code&gt;https://myapp.com/...&lt;/code&gt;) are not handed to the app, and the router receives the URL too late.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Verify&lt;/em&gt;: Run the app on a physical iOS device, then execute:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;xcrun simctl openurl booted &lt;span class="s2"&gt;"https://myapp.com/invite/abc123"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The console should show:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[expo-router] Initial URL: https://myapp.com/invite/abc123
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and the Invite screen should render.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Regenerate the route manifest after every change
&lt;/h3&gt;

&lt;p&gt;Whenever you add, rename, or delete a file in the &lt;code&gt;app/&lt;/code&gt; folder, run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;expo prebuild
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or, if you rely on EAS, simply trigger a new build. The &lt;code&gt;expo-router&lt;/code&gt; plugin automatically runs &lt;code&gt;expo-router generate&lt;/code&gt; during the prebuild step.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Why&lt;/em&gt;: The manifest is a static snapshot; stale manifests cause missing‑route errors.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Verify&lt;/em&gt;: Open the built bundle (&lt;code&gt;.js&lt;/code&gt; file) and locate &lt;code&gt;router-manifest.json&lt;/code&gt;. Ensure the new route appears, e.g.:&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="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"/invite/[code]"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"file"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"app/invite/[code].tsx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"page"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&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;h3&gt;
  
  
  4. Delay deep‑link processing until the router is ready
&lt;/h3&gt;

&lt;p&gt;Create a small wrapper component (&lt;code&gt;app/_layout.tsx&lt;/code&gt;) that waits for &lt;code&gt;router.isReady&lt;/code&gt; before handling the initial URL.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useEffect&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useRouter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;usePathname&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;expo-router&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;Linking&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;expo-linking&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;RootLayout&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;children&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;children&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ReactNode&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;router&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useRouter&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isReady&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;Linking&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getInitialURL&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Let expo-router parse it; it will navigate automatically.&lt;/span&gt;
        &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[expo-router] Initial URL:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;url&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="nf"&gt;handle&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="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isReady&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;&amp;lt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&amp;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;em&gt;Why&lt;/em&gt;: Guarantees the deep link is processed after the JS bundle has mounted the router.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Verify&lt;/em&gt;: On a low‑end Android device, attach &lt;code&gt;adb logcat&lt;/code&gt; and look for the log line above when opening a link via:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;adb shell am start &lt;span class="nt"&gt;-W&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt; android.intent.action.VIEW &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"myapp://invite/abc123"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see the log after a short delay, and the Invite screen should appear.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Align Expo Updates caching with the route manifest
&lt;/h3&gt;

&lt;p&gt;Add the following to &lt;code&gt;app.json&lt;/code&gt; to force a fresh manifest after each OTA update:&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="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"expo"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"updates"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"fallbackToCacheTimeout"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"checkAutomatically"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ON_LOAD"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&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;Then, in &lt;code&gt;app/_layout.tsx&lt;/code&gt;, clear the router cache when a new update is detected:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;Updates&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;expo-updates&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useRouter&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;expo-router&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useEffect&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Layout&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;children&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;router&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useRouter&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;subscription&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Updates&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;Updates&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;UpdateEventType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;UPDATE_AVAILABLE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;Updates&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reloadAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// forces a fresh bundle with the correct manifest&lt;/span&gt;
      &lt;span class="p"&gt;}&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;subscription&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remove&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;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&amp;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;em&gt;Why&lt;/em&gt;: Guarantees that a newly downloaded bundle also contains the latest route manifest, preventing “missing route” errors after OTA updates.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Verify&lt;/em&gt;: Publish an OTA update that adds a new deep‑linkable screen, then install the previous version from the store, open the new link, and confirm the app reloads and displays the screen correctly.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it costs, and when not to do it
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;th&gt;Detail&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Bundle size&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Adding the &lt;code&gt;expo-router&lt;/code&gt; plugin and the associated native code adds ~30 KB to the Android APK and ~25 KB to the iOS IPA (measured with &lt;code&gt;apktool&lt;/code&gt; on SDK 48).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Build time&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The pre‑build step now runs &lt;code&gt;expo-router generate&lt;/code&gt;, adding ~10 seconds to &lt;code&gt;eas build&lt;/code&gt; on CI.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Maintenance&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;You must keep the scheme unique across all apps in the Play Store and App Store, and update the AASA file whenever the domain changes.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Runtime latency&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;On low‑end devices the extra &lt;code&gt;Linking.getInitialURL&lt;/code&gt; call adds ~100 ms before the first screen renders. This is usually invisible but can be noticeable on Android 5 devices.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Complexity&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The need to coordinate Android intent filters, iOS associated domains, and the JS manifest introduces three separate failure surfaces.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  When not to use Expo Router deep links
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;If your app only needs &lt;strong&gt;web‑only&lt;/strong&gt; navigation (e.g., a PWA), skip native intent configuration. &lt;/li&gt;
&lt;li&gt;If you already use a &lt;strong&gt;custom native navigation stack&lt;/strong&gt; (e.g., &lt;code&gt;react-navigation&lt;/code&gt; with native screens) and you want fine‑grained control over intent handling, implement the native side yourself rather than relying on the router plugin. &lt;/li&gt;
&lt;li&gt;If you must support &lt;strong&gt;legacy Android versions (&amp;lt; 6.0)&lt;/strong&gt; where the intent‑filter schema is unreliable, consider a fallback to a custom scheme handled by a minimal native module.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  At your level
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Student / first‑year engineer&lt;/strong&gt; – Verify that the scheme appears in &lt;code&gt;AndroidManifest.xml&lt;/code&gt; and that the iOS entitlements file contains the domain before testing any link. &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Working engineer&lt;/strong&gt; – Add the router plugin, regenerate the manifest after each route change, and wrap deep‑link handling in a &lt;code&gt;router.isReady&lt;/code&gt; guard. &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Senior / staff&lt;/strong&gt; – Document the three‑point contract (scheme, associated domain, manifest regeneration) in the repo’s README, and set up a CI lint step that fails if &lt;code&gt;router-manifest.json&lt;/code&gt; is out of sync with the file system. &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lead / director&lt;/strong&gt; – Allocate budget for the extra build time and for maintaining the AASA file; treat deep‑link reliability as a non‑functional requirement in the product roadmap.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  In the interview
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Question (as asked by an interviewer):&lt;/strong&gt; &lt;br&gt;
“Explain how you would implement reliable deep links in an Expo Router‑based React Native app that is built with EAS. What hidden pitfalls would you look for in production?”&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Weak answer:&lt;/strong&gt; &lt;br&gt;
“Just add a scheme to &lt;code&gt;app.json&lt;/code&gt; and use &lt;code&gt;Linking.openURL&lt;/code&gt;. It works in development, so it should be fine.” &lt;br&gt;
&lt;em&gt;Why it’s weak:&lt;/em&gt; It mentions only the obvious step, ignores native intent configuration, the route manifest, and runtime timing issues. No trade‑offs or failure modes are identified.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Strong answer:&lt;/strong&gt; &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;“First, I would add the &lt;code&gt;expo-router&lt;/code&gt; plugin to &lt;code&gt;app.json&lt;/code&gt; so that the Android manifest receives the proper `&amp;lt;intent&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;&lt;em&gt;Amit Chakraborty is a founding engineer and senior architect — React Native, AI/RAG systems and production architecture. Portfolio: &lt;a href="https://www.amitchakraborty.dev?utm_source=article&amp;amp;utm_medium=content&amp;amp;utm_campaign=expo-router-deep-links-the-trade-offs-nobody-documents" rel="noopener noreferrer"&gt;www.amitchakraborty.dev&lt;/a&gt; · &lt;a href="https://linkedin.com/in/devamitch" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt; · &lt;a href="https://github.com/devamitch" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;. Open to senior and founding engineering roles, remote worldwide.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>expo</category>
      <category>android</category>
      <category>ios</category>
    </item>
    <item>
      <title>Blocking apps failed for me three times. A daily budget plus a counter of every override has held for 21 days.</title>
      <dc:creator>Martin Ma</dc:creator>
      <pubDate>Wed, 21 Oct 2026 02:51:37 +0000</pubDate>
      <link>https://dev.to/jumpingma/blocking-apps-failed-for-me-three-times-a-daily-budget-plus-a-counter-of-every-override-has-held-neg</link>
      <guid>https://dev.to/jumpingma/blocking-apps-failed-for-me-three-times-a-daily-budget-plus-a-counter-of-every-override-has-held-neg</guid>
      <description>&lt;p&gt;Every screen-time app I tried in 2024 assumed the problem was access. Remove access, problem solved. It is not, and I have the logs to prove it on myself.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three attempts that failed
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Full block, 9 days.&lt;/strong&gt; I blocked the four apps I used most. Day four I started using the mobile web versions. Day nine I had reinstalled two of them and turned the blocker off "temporarily", which lasted three weeks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Accountability partner, 5 weeks.&lt;/strong&gt; A friend held the passcode. I asked for it three times with increasingly creative reasons, got it twice, and stopped asking because asking felt worse than the habit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cold turkey, 12 days.&lt;/strong&gt; Deleted everything, kept it up for twelve days, then reinstalled five apps in one evening after a bad day.&lt;/p&gt;

&lt;p&gt;The pattern: every method removed the &lt;em&gt;access&lt;/em&gt; and left the &lt;em&gt;reason&lt;/em&gt; untouched. A phone is also my map, my messages, my train ticket. "Just don't use it" is not available to me.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually changed: budget + counter
&lt;/h2&gt;

&lt;p&gt;Two mechanics replaced blocking.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A per-app daily budget.&lt;/strong&gt; Games get 20 minutes, short video 20 minutes, messaging and maps unlimited. Not one global hour, which runs out by noon and then gets ignored for the rest of the day.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A counter of overrides.&lt;/strong&gt; When a budget runs out, the app does not lock. It asks once: continue now, or do the thing you said you wanted to do first. If I continue, it records that. No punishment, no streak to protect, no guilt screen. Just a number that goes up.&lt;/p&gt;

&lt;p&gt;The counter is the part that worked, and it is the part every blocker I tried was designed &lt;em&gt;not&lt;/em&gt; to have. Blocking is binary: you are compliant or you have failed. A counter is a measurement, and measurements are harder to lie to.&lt;/p&gt;

&lt;h2&gt;
  
  
  21 days of data
&lt;/h2&gt;

&lt;p&gt;I logged every open and every override from 1 to 21 October.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sleep-adjacent phone use: 90 min/night to 12 min/night&lt;/li&gt;
&lt;li&gt;Opens per day: 41 to 19&lt;/li&gt;
&lt;li&gt;Overrides per week: 23 to 6&lt;/li&gt;
&lt;li&gt;Apps uninstalled: 0&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The 23-to-6 drop is the interesting number. I was not overriding less because I had more willpower. I was overriding less because overriding had become &lt;em&gt;visible&lt;/em&gt;. Seeing "23" next to a week made the next override feel like a choice rather than a reflex.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the "no stopping cues" framing matters
&lt;/h2&gt;

&lt;p&gt;Short video, games and social feeds share one structure: no natural end. A season never ends, a feed never refreshes to a bottom, an autoplay queue always has a next item. So the useful question is not "how do I stop" but "where do I put the stopping cue".&lt;/p&gt;

&lt;p&gt;Three cues that survived contact with a bad day:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A budget boundary&lt;/strong&gt; — an external, pre-committed end that is not a judgement about me.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A single question at the boundary&lt;/strong&gt; — one prompt, not a wall of options.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A visible count of crossings&lt;/strong&gt; — the only feedback that changed behaviour without shame.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What I built, and what I left out
&lt;/h2&gt;

&lt;p&gt;I got tired of stitching this together across three apps, so I built it as one: per-app budgets, one prompt at the boundary, and a plain counter of overrides, all stored on the device. It is called ChoiceField, iOS only, free.&lt;/p&gt;

&lt;p&gt;Two deliberate omissions, because I think they matter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No blocking mode.&lt;/strong&gt; A block that can be bypassed teaches you that rules are optional; a budget that reports does not.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No rankings or streaks.&lt;/strong&gt; Streaks punish a bad day twice: once when it happens, once when it resets.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you want to look at it: &lt;a href="https://apps.apple.com/us/app/id6769946195" rel="noopener noreferrer"&gt;App Store (US)&lt;/a&gt; or &lt;a href="https://apps.apple.com/cn/app/id6769946195" rel="noopener noreferrer"&gt;App Store (China)&lt;/a&gt;.&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%2Fhs3ma46t3b38ozisx9vp.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%2Fhs3ma46t3b38ozisx9vp.png" alt="ChoiceField download card" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you only take one thing from this: stop trying to remove access, and start making the override visible. You can do that with a notebook for a week before you install anything.&lt;/p&gt;

</description>
      <category>productivity</category>
      <category>health</category>
      <category>ios</category>
      <category>discuss</category>
    </item>
    <item>
      <title>Apple Watch Ultra 4: Everything the orange action button can do</title>
      <dc:creator>Hacks.gr</dc:creator>
      <pubDate>Wed, 21 Oct 2026 02:35:32 +0000</pubDate>
      <link>https://dev.to/hacksgr/apple-watch-ultra-4-everything-the-orange-action-button-can-do-1g5m</link>
      <guid>https://dev.to/hacksgr/apple-watch-ultra-4-everything-the-orange-action-button-can-do-1g5m</guid>
      <description>&lt;p&gt;The Apple Watch Ultra 4 expands its hardware utility with the introduction of the Action Button, allowing users to bind custom commands to physical input.&lt;/p&gt;

&lt;p&gt;The button integrates with the iOS Shortcuts framework, enabling complex automation sequences.&lt;/p&gt;

&lt;p&gt;Additionally, the device leverages watchOS 27 to allow shortcut creation through descriptive input and introduces Audio Intelligence for music recognition, providing a more robust interface for wrist-based computing.&lt;/p&gt;




&lt;h2&gt;
  
  
  How the action button works
&lt;/h2&gt;

&lt;p&gt;You can set the button to perform a specific action with a single press, such as opening the flashlight, taking a voice note, translating a phrase, or recognizing a piece of music with the watch's instant audio recognition feature. If you hold the button down, a menu appears to quickly change its function.&lt;/p&gt;

&lt;p&gt;For those who play sports or go outdoors, the button instantly controls workouts, timers and dives. It also supports navigation functions, such as recording specific points and returning to the route. In case of emergency, the same button can activate a siren to attract attention.&lt;/p&gt;

&lt;h3&gt;
  
  
  One-touch automations
&lt;/h3&gt;

&lt;p&gt;The action button can be linked to the Shortcuts app on your iPhone. This lets you perform automated commands, such as controlling smart home devices, with a single tap on your watch.&lt;/p&gt;

&lt;p&gt;READ ALSO&lt;/p&gt;

&lt;p&gt;Fire TV, Apple TV, Google TV and more: what each TV system offers&lt;/p&gt;

&lt;p&gt;With the latest updates, creating these shortcuts has become easier. You can define a new command by simply describing in words what you want to happen, without any technical setup required.&lt;/p&gt;

&lt;p&gt;G&lt;/p&gt;

&lt;p&gt;Hacks.gr on Google&lt;/p&gt;

&lt;p&gt;Set it as a preferred source for cybersecurity updates.&lt;/p&gt;

&lt;p&gt;Preferred source&lt;/p&gt;




&lt;p&gt;&lt;a href="https://en.hacks.gr/apple-watch-ultra-4-ola-osa-mporei-na-kanei-to-portokali-koympi-drasis/" rel="noopener noreferrer"&gt;Read the original English article on Hacks.gr&lt;/a&gt;&lt;/p&gt;

</description>
      <category>applewatch</category>
      <category>ios</category>
      <category>automation</category>
      <category>shortcuts</category>
    </item>
    <item>
      <title>AI Questions Were Scattered Everywhere, So I Built Approval Box to Collect Approvals on My Phone</title>
      <dc:creator>Quo</dc:creator>
      <pubDate>Wed, 21 Oct 2026 02:15:34 +0000</pubDate>
      <link>https://dev.to/quolu/ai-questions-were-scattered-everywhere-so-i-built-approval-box-to-collect-approvals-on-my-phone-4ig4</link>
      <guid>https://dev.to/quolu/ai-questions-were-scattered-everywhere-so-i-built-approval-box-to-collect-approvals-on-my-phone-4ig4</guid>
      <description>&lt;p&gt;!&lt;/p&gt;

&lt;p&gt;This article is a repost from &lt;a href="https://kitepon.dev/blog/" rel="noopener noreferrer"&gt;I Started Using Claude Code&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I released an iPhone app called &lt;a href="https://kitepon.dev/products/approval-box/" rel="noopener noreferrer"&gt;Approval Box&lt;/a&gt; on the &lt;a href="https://apps.apple.com/jp/app/approval-box/id6818175213" rel="noopener noreferrer"&gt;App Store&lt;/a&gt;. It's an app that lets you receive consultations coming from AI running on your PC on your phone, and answer them on the spot.&lt;/p&gt;

&lt;h2&gt;
  
  
  Questions get scattered all over the place
&lt;/h2&gt;

&lt;p&gt;When you work with AI, you end up opening a lot of sessions. I also use multiple tools (harnesses) to run AI. As development goes on—and not just development, but any kind of work—the AI in each session starts asking questions. The questions get scattered all over the place, and answering them becomes a chore.&lt;/p&gt;

&lt;p&gt;I often step away from my computer. If the AI asks a question while I'm away, that session stops and waits for an answer. Before I know it, the work has ground to a halt.&lt;/p&gt;

&lt;p&gt;Approval Box was made to solve that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Approvals from many sessions gather on your phone
&lt;/h2&gt;

&lt;p&gt;When AI needs a human decision or approval, it submits a request to Approval Box. Requests gather in the phone app, and the subject arrives as a push notification. The app lists which AI it came from, what work it's about, whether it's urgent, and when it's due.&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%2Ff8d3hxpuvuclxt35gt99.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%2Ff8d3hxpuvuclxt35gt99.png" alt="The inbox screen. Consultations from Claude Code, Codex, and Grok are lined up in a single list." width="800" height="1738"&gt;&lt;/a&gt;&lt;br&gt;&lt;br&gt;
&lt;em&gt;The inbox screen. Consultations from Claude Code, Codex, and Grok are lined up in a single list.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;When you open a request, the background and options written by the AI appear. The AI's recommendation is marked, so you can choose while looking at it and send your answer. Besides the options, you can also write instructions in text, or attach images and documents in your reply. If you can't decide right away, you can put it on hold and answer later.&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%2Fmfc2w0ec14inidnqrwjy.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%2Fmfc2w0ec14inidnqrwjy.png" alt="The consultation screen. Read the background, look at the AI's recommendation, choose an option, and send your answer." width="800" height="1738"&gt;&lt;/a&gt;&lt;br&gt;&lt;br&gt;
&lt;em&gt;The consultation screen. Read the background, look at the AI's recommendation, choose an option, and send your answer.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  When you answer, even sleeping sessions wake up
&lt;/h2&gt;

&lt;p&gt;The amazing thing about this app is that once you send an answer, even sleeping sessions wake up. The answer goes straight back into the conversation of the AI that submitted the request. If the AI is working, it reaches the current turn; if it's stopped, it arrives as a new turn, and the AI continues the work from there. Answer from your phone, and the work resumes on the spot.&lt;/p&gt;

&lt;p&gt;Another feature: the AI itself can withdraw or update an approval request. If circumstances change after submitting a request, the AI can revise the content or withdraw a request that's no longer needed. The app reflects this directly, so the list keeps only the consultations that need an answer now.&lt;/p&gt;

&lt;p&gt;In the history, you can check whether your answer reached the AI. I also added a feature to copy a "nudge to resume" for when you want to continue the conversation.&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%2Ff6lb9olopu47tq5nkvu2.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%2Ff6lb9olopu47tq5nkvu2.png" alt="The history screen. Answered consultations and whether they reached the AI are lined up." width="800" height="1738"&gt;&lt;/a&gt;&lt;br&gt;&lt;br&gt;
&lt;em&gt;The history screen. Answered consultations and whether they reached the AI are lined up.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  It's easy to use
&lt;/h2&gt;

&lt;p&gt;Run the following command on your PC.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npx -y approval-box@latest setup
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It will find the AI on your PC and register Approval Box for you. It shows you the files it will modify first, keeps a backup, and then makes the changes. After that, just scan the QR code shown on screen with the app, and your PC and phone are connected.&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%2F2kmokda4397xx0tg4j8h.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%2F2kmokda4397xx0tg4j8h.png" alt="The add device screen. Run the command on your PC and scan the QR code with your phone." width="800" height="1738"&gt;&lt;/a&gt;&lt;br&gt;&lt;br&gt;
&lt;em&gt;The add device screen. Run the command on your PC and scan the QR code with your phone.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Finally, ask the AI to "run Approval Box's setup_test." A test request will arrive on your phone, so answer it, and if the answer reaches the AI, you're all set.&lt;/p&gt;

&lt;p&gt;The supported AIs are Claude Code, Codex, Cursor, and Grok, and the PC can run macOS, Windows, or Linux. The PC side requires Node.js 20 or higher.&lt;/p&gt;

&lt;p&gt;The app can be downloaded for free, and the official service's monthly plan is 300 yen in Japan. A separate contract for the AI itself is required. There's also a &lt;a href="https://approval-box.kitepon.dev/" rel="noopener noreferrer"&gt;web version&lt;/a&gt; you can use in a browser, and usage instructions are compiled on the &lt;a href="https://approval-box.kitepon.dev/support" rel="noopener noreferrer"&gt;support page&lt;/a&gt;. The server and web version code are published on &lt;a href="https://github.com/kitepon/approval-box" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;, so you can also host the server yourself.&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>ai</category>
      <category>ios</category>
      <category>mcp</category>
    </item>
  </channel>
</rss>
