<?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: BeforeGo</title>
    <description>The latest articles on DEV Community by BeforeGo (@_8f5abeda89bd0a842a1d0).</description>
    <link>https://dev.to/_8f5abeda89bd0a842a1d0</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4183580%2F061301d4-6365-4890-8d9d-2640a47977af.jpg</url>
      <title>DEV Community: BeforeGo</title>
      <link>https://dev.to/_8f5abeda89bd0a842a1d0</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/_8f5abeda89bd0a842a1d0"/>
    <language>en</language>
    <item>
      <title>Fewest transfers was correct. It also told a stranger to pay my friend.</title>
      <dc:creator>BeforeGo</dc:creator>
      <pubDate>Sun, 01 Nov 2026 14:52:18 +0000</pubDate>
      <link>https://dev.to/_8f5abeda89bd0a842a1d0/fewest-transfers-was-correct-it-also-told-a-stranger-to-pay-my-friend-1ipb</link>
      <guid>https://dev.to/_8f5abeda89bd0a842a1d0/fewest-transfers-was-correct-it-also-told-a-stranger-to-pay-my-friend-1ipb</guid>
      <description>&lt;p&gt;The plan said &lt;strong&gt;Coco pays Mia $75.20&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The arithmetic was right. Coco and Mia have never met. Coco wasn't even on the trip.&lt;/p&gt;

&lt;p&gt;This is about the settle-up screen in the expense splitting I built for my travel app (BeforeGo, iOS). The problem got caught in a worked example before it shipped, and the fix is small. I still think it's worth writing up, because the textbook algorithm does exactly what it promises, and that turned out to be the problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two kinds of people on a trip's bills
&lt;/h2&gt;

&lt;p&gt;Most splitting apps assume everyone on a bill is in the group. On a real trip, that's not quite true:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Buddies&lt;/strong&gt;: the people on the trip. They pay for things and split things with each other.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Others&lt;/strong&gt;: friends back home you bought things for. If you've been to a Japanese drugstore with a shopping list from three friends, you know.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Others show up on exactly one kind of bill: one you paid on their behalf. They don't owe the group anything. They owe you.&lt;/p&gt;

&lt;h2&gt;
  
  
  The textbook settle-up
&lt;/h2&gt;

&lt;p&gt;The usual way to settle a group's debts:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Work out each person's &lt;strong&gt;net&lt;/strong&gt;: what they paid minus what they owe. Positive means they're owed money.&lt;/li&gt;
&lt;li&gt;Pair the largest creditor with the largest debtor, transfer as much as possible, and repeat until everyone's at zero.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each step zeroes at least one person, so with &lt;em&gt;n&lt;/em&gt; people you never need more than &lt;em&gt;n − 1&lt;/em&gt; transfers. It isn't guaranteed to find the true minimum (that problem is NP-hard: it comes down to finding as many separate zero-sum groups as possible), but it's simple, it's predictable, and on a trip-sized group it's hard to beat.&lt;/p&gt;

&lt;p&gt;Here's the version in the app, trimmed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="c1"&gt;/// Greedy: the largest creditor takes from the largest debtor until one of them is square.&lt;/span&gt;
&lt;span class="c1"&gt;/// Scanning in roster order and replacing only on a strictly larger amount makes ties go to&lt;/span&gt;
&lt;span class="c1"&gt;/// whoever comes first on the roster.&lt;/span&gt;
&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;fewestTransfers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="nv"&gt;net&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;UUID&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nv"&gt;roster&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;UUID&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="kt"&gt;SettlementTransfer&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;credits&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="nv"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;UUID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;roster&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;compactMap&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
        &lt;span class="k"&gt;guard&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;net&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="nf"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;debts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="nv"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;UUID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;roster&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;compactMap&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
        &lt;span class="k"&gt;guard&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;net&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="nf"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// largest(_:) returns the index of the biggest amount; the first one wins a tie.&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;transfers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;SettlementTransfer&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;credits&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isEmpty&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;debts&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isEmpty&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;largest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;credits&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;largest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;debts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;amount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;credits&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;debts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;transfers&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;SettlementTransfer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;fromId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;debts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;toId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;credits&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="n"&gt;credits&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;
        &lt;span class="n"&gt;debts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;credits&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;credits&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;debts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;debts&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;d&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="n"&gt;transfers&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The worked example
&lt;/h2&gt;

&lt;p&gt;Before writing any of the money code, the design doc had one sample trip worked out with exact fractions: five bills in Tokyo, three buddies (Me, Mia, Leo) and two friends back home (Coco, Ray). Rate: 1 JPY = 0.0067 USD.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Bill&lt;/th&gt;
&lt;th&gt;Paid by&lt;/th&gt;
&lt;th&gt;Amount&lt;/th&gt;
&lt;th&gt;Split&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Taxi&lt;/td&gt;
&lt;td&gt;Leo&lt;/td&gt;
&lt;td&gt;¥3,900 ($26.13)&lt;/td&gt;
&lt;td&gt;Me, Mia, Leo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hotel, 5 nights&lt;/td&gt;
&lt;td&gt;Mia&lt;/td&gt;
&lt;td&gt;¥96,000 ($643.20)&lt;/td&gt;
&lt;td&gt;Me, Mia, Leo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dinner&lt;/td&gt;
&lt;td&gt;Me&lt;/td&gt;
&lt;td&gt;¥12,480 ($83.62)&lt;/td&gt;
&lt;td&gt;Me, Mia, Leo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;teamLab tickets&lt;/td&gt;
&lt;td&gt;Mia&lt;/td&gt;
&lt;td&gt;¥11,400 ($76.38)&lt;/td&gt;
&lt;td&gt;Me, Mia, Leo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Drugstore&lt;/td&gt;
&lt;td&gt;Me&lt;/td&gt;
&lt;td&gt;¥19,830 ($132.86)&lt;/td&gt;
&lt;td&gt;line by line: Coco, Ray, and a bit for me, plus a 10% shopping fee for the friends&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Put everyone in one pool and the nets are:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Person&lt;/th&gt;
&lt;th&gt;Net&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Mia&lt;/td&gt;
&lt;td&gt;+443.14&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Leo&lt;/td&gt;
&lt;td&gt;−250.31&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Me&lt;/td&gt;
&lt;td&gt;−80.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Coco&lt;/td&gt;
&lt;td&gt;−75.20&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ray&lt;/td&gt;
&lt;td&gt;−37.63&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;They add up to zero, as they should. Run the greedy on that pool and you get:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Leo → Mia $250.31&lt;/li&gt;
&lt;li&gt;Me → Mia $80.00&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Coco → Mia $75.20&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Ray → Mia $37.63&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Four transfers, the minimum for this set (there's only one creditor, so each of the four debtors has to pay someone). Every number checks out.&lt;/p&gt;

&lt;p&gt;And it's absurd. Coco would be sending money to someone she's never met, for sunscreen I bought her, including my 10% shopping fee. Meanwhile I'd pay Mia $80 instead of the $192.83 I actually owe her, because my friends back home would be covering the rest of what I owe her.&lt;/p&gt;

&lt;p&gt;The algorithm minimised the number of transfers. Nobody had told it that money only moves between people who know each other.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix: two ledgers, not one
&lt;/h2&gt;

&lt;p&gt;Buddies net against each other and get the fewest-transfers plan. Anyone else settles one-to-one with whoever paid their bills, and never joins the buddies' pool.&lt;/p&gt;

&lt;p&gt;In code, it's one branch where each debt gets recorded:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;owe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="nv"&gt;debtor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;UUID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="nv"&gt;creditor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;UUID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="nv"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;guard&lt;/span&gt; &lt;span class="n"&gt;debtor&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;creditor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;buddies&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;debtor&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;buddies&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;creditor&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;buddyNet&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;debtor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;default&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="o"&gt;-=&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;
        &lt;span class="n"&gt;buddyNet&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;creditor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;default&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="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;debtor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uuidString&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;creditor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uuidString&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Unordered pair → balance; positive means `low` owes `high`.&lt;/span&gt;
        &lt;span class="n"&gt;pairs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;PairKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;low&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;debtor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;high&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;creditor&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="k"&gt;default&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="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;pairs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;PairKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;low&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;creditor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;high&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;debtor&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="k"&gt;default&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="o"&gt;-=&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;buddyNet&lt;/code&gt; goes through &lt;code&gt;fewestTransfers&lt;/code&gt;. &lt;code&gt;pairs&lt;/code&gt; turns straight into one transfer per pair. A recorded payment ("Mark paid") goes through the same &lt;code&gt;owe&lt;/code&gt; in the opposite direction, so it nets exactly like a bill.&lt;/p&gt;

&lt;p&gt;Same sample, new plan:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Leo → Mia $250.31&lt;/li&gt;
&lt;li&gt;Me → Mia $192.83&lt;/li&gt;
&lt;li&gt;Coco → Me $75.20&lt;/li&gt;
&lt;li&gt;Ray → Me $37.63&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Still four transfers. In this example the sensible plan costs nothing extra. In general it can cost a few: if Coco owes me and I owe Mia, the pooled version can route Coco straight to Mia and save a transfer. That's a transfer worth paying for.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pinning it down with invariants
&lt;/h2&gt;

&lt;p&gt;The sample is pinned in the tests twice, settled in USD and in CNY, because every number on every mock-up came from it. On top of that, a handful of properties have to hold for any input:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;assertInvariants&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="nv"&gt;bills&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;ExpenseSnapshot&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="nv"&gt;settlements&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;SettlementSnapshot&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
                              &lt;span class="nv"&gt;ledger&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;TripLedger&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;bill&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;bills&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;breakdown&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;ExpenseMath&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;breakdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;of&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;bill&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;ledger&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ledger&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="kt"&gt;XCTAssertEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;breakdown&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shares&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reduce&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="o"&gt;+&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;breakdown&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;convertedTotal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                       &lt;span class="s"&gt;"shares must add up to the bill"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;plan&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;ExpenseMath&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;settle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bills&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;settlements&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;settlements&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;ledger&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ledger&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="kt"&gt;XCTAssertEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;plan&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;buddyNet&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reduce&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="o"&gt;+&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="s"&gt;"buddy nets must add up to 0"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="kt"&gt;XCTAssertEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;plan&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;net&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reduce&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="o"&gt;+&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="s"&gt;"everyone's nets must add up to 0"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;outsiders&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;plan&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;net&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subtracting&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ledger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;buddyIds&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;transfer&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;plan&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;transfers&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;XCTAssertFalse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;outsiders&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;transfer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fromId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;outsiders&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;transfer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;toId&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                       &lt;span class="s"&gt;"an outsider in the buddies' plan"&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 first one is the one rounding tries hardest to break. Each share is converted and rounded half-up to the settlement currency's smallest unit, and whatever the rounded shares miss of the rounded total goes to the payer. That's why the dinner splits as $27.88 for me and $27.87 each for Mia and Leo: I paid, so I carry the extra cent.&lt;/p&gt;

&lt;p&gt;The same invariants run against the sample at rates from 0.0068 up to 149.37, and in KRW, a currency with no minor unit. The last check in that function is the Coco-and-Mia rule, written so it can't come back.&lt;/p&gt;

&lt;p&gt;Two smaller rules came out of the same sample:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ties go to roster order.&lt;/strong&gt; Two people owed the same amount always come out in the same order, and shuffling the bills gives the same plan. The plan is shown on screen, exported as an image and recomputed whenever a bill changes, so it shouldn't reshuffle when nothing has changed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No rate, no conversion.&lt;/strong&gt; A bill in a currency with no exchange rate yet is left out of the totals and flagged. It doesn't get treated as 1:1.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I took from it
&lt;/h2&gt;

&lt;p&gt;The greedy didn't have a bug. It answered the question I asked: how do these balances clear in as few payments as possible? The question I should have asked was: who would actually send money to whom? Who knows whom is part of the input, and no amount of testing the algorithm would have shown me that.&lt;/p&gt;

&lt;p&gt;What caught it was a worked example with real-looking names, done before the code existed. "Coco pays Mia" looks fine in a unit test full of UUIDs. Next to the sentence "Coco wasn't on the trip," it's obviously wrong.&lt;/p&gt;

&lt;p&gt;If you've built splitting logic: how do you handle people who aren't part of the group? I'd like to know whether anyone has found a cleaner model than two ledgers.&lt;/p&gt;

&lt;p&gt;If you want to try a group split without installing anything, BeforeGo has a &lt;a href="https://beforegotech.com/tools/trip-expense-splitter/?ref=devto-fewest-transfers" rel="noopener noreferrer"&gt;free trip expense splitter&lt;/a&gt; that runs in the browser for one travel group.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This post was written by an AI agent (Claude) working from BeforeGo's code, design notes and tests, at my request. Every number and code excerpt in it was checked against them before publishing.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>algorithms</category>
      <category>swift</category>
      <category>testing</category>
      <category>ios</category>
    </item>
    <item>
      <title>Building a baby memory book with SwiftUI</title>
      <dc:creator>BeforeGo</dc:creator>
      <pubDate>Wed, 28 Oct 2026 07:47:41 +0000</pubDate>
      <link>https://dev.to/_8f5abeda89bd0a842a1d0/-building-a-baby-memory-book-with-swiftui-3di9</link>
      <guid>https://dev.to/_8f5abeda89bd0a842a1d0/-building-a-baby-memory-book-with-swiftui-3di9</guid>
      <description>&lt;p&gt;A photo can show a baby's first smile. A caption can preserve what happened just before it.&lt;/p&gt;

&lt;p&gt;That is the kind of detail I wanted Pagewren to make room for: a picture, a few words, and a page someone might want to return to years later.&lt;/p&gt;

&lt;p&gt;I'm the developer of Pagewren, a baby memory book for iPhone. The central design decision was to put the book itself on the home screen. Add a memory, and it becomes part of the book you're already reading.&lt;/p&gt;

&lt;p&gt;That decision also shaped some interesting engineering work.&lt;/p&gt;

&lt;h3&gt;
  
  
  Making each entry feel finished
&lt;/h3&gt;

&lt;p&gt;The capture flow accepts photos, words, or both. Pagewren arranges the content into a page, with chapters organized around the child's age.&lt;/p&gt;

&lt;p&gt;For a keepsake app, that immediate result matters. If saving a memory also leaves you with a layout task, there is more work waiting the next time you open it. I wanted the output of a small entry to feel complete enough to keep.&lt;/p&gt;

&lt;p&gt;There are no daily streaks. Parents can add an ordinary moment, leave a gap, and come back later. Batch photo import helps them bring older memories into the book too.&lt;/p&gt;

&lt;h3&gt;
  
  
  Sharing the page renderer
&lt;/h3&gt;

&lt;p&gt;Pagewren is built with SwiftUI. Its book pages use a shared &lt;code&gt;BookPageView&lt;/code&gt; that receives the page content, the selected style, and the rendering width.&lt;/p&gt;

&lt;p&gt;The PDF exporter uses that same view through &lt;code&gt;ImageRenderer&lt;/code&gt;, drawing it into a PDF graphics context. Page dimensions and typography scale from the page width.&lt;/p&gt;

&lt;p&gt;This keeps layout decisions in one place. Changing how a theme places a photograph does not require a separate implementation of that layout for export.&lt;/p&gt;

&lt;p&gt;The rendering context still needs a few differences. A printed page has square corners. Interactive controls belong in the app. But the composition comes from the same component.&lt;/p&gt;

&lt;p&gt;One detail that matters here is image loading. Export does not wait for an asynchronous image request to finish. The page renderer therefore receives a synchronous image source, so the photos are available when the page is drawn.&lt;/p&gt;

&lt;p&gt;It is a small interface choice with a visible consequence: a page exported with its pictures in place.&lt;/p&gt;

&lt;h3&gt;
  
  
  Measuring words before splitting pages
&lt;/h3&gt;

&lt;p&gt;Pagination becomes more involved once a memory can include a short caption, a long story, or a list.&lt;/p&gt;

&lt;p&gt;Pagewren measures text using TextKit and the font and available text area for the chosen layout. A photo page has less space for words than a text page. Different themes also leave different amounts of room.&lt;/p&gt;

&lt;p&gt;The page break follows the laid-out lines. List items and embedded elements need their own handling so they do not get split arbitrarily across pages.&lt;/p&gt;

&lt;p&gt;For this app, measuring the actual text is much more useful than treating a page as a fixed number of characters. The visible line breaks are what determine whether the words fit.&lt;/p&gt;

&lt;h3&gt;
  
  
  Keeping the book on the device
&lt;/h3&gt;

&lt;p&gt;The persistence layer uses Core Data, with optional CloudKit mirroring for Pro users.&lt;/p&gt;

&lt;p&gt;The app can open the local store without enabling sync. When CloudKit cannot be initialized, the persistence controller attempts to open the same store locally. That fallback keeps the storage path centered on the book already on the phone.&lt;/p&gt;

&lt;p&gt;There is no Pagewren account to create. Book content stays on the iPhone and, when sync is enabled, in the user's private iCloud database. Usage analytics and crash reporting are separate from the photos and writing in the book.&lt;/p&gt;

&lt;p&gt;For the public details, see the app's &lt;a href="https://apps.apple.com/us/app/pagewren-baby-memory-book/id6823661619#app-privacy" rel="noopener noreferrer"&gt;privacy information on the App Store&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Letting people take the book with them
&lt;/h3&gt;

&lt;p&gt;PDF export is a useful part of the product: it gives the parent a file they can save or share. Individual pages can also be shared as images.&lt;/p&gt;

&lt;p&gt;That requirement makes the shared renderer more than a code organization preference. Exported pages are part of the experience people keep outside the app.&lt;/p&gt;

&lt;p&gt;Pagewren is &lt;a href="https://apps.apple.com/us/app/pagewren-baby-memory-book/id6823661619" rel="noopener noreferrer"&gt;available on the App Store&lt;/a&gt;. It is free to download and use with the Classic theme. Pro adds themes, iCloud sync, and Privacy Lock, with monthly, yearly, and lifetime purchase options. The &lt;a href="https://pagewrenbaby.com/?ref=devto-swiftui" rel="noopener noreferrer"&gt;Pagewren website&lt;/a&gt; also has a free printable baby milestone card maker that runs in the browser.&lt;/p&gt;

&lt;p&gt;I'd be interested to hear from other developers building journals or tools for personal archives: how do you keep the capture flow small while giving people something satisfying to come back to?&lt;/p&gt;

</description>
      <category>buildinpublic</category>
      <category>ios</category>
      <category>mobile</category>
      <category>swift</category>
    </item>
    <item>
      <title>Using your own photo as an iPhone countdown widget background: what works, and what iOS won't let you do</title>
      <dc:creator>BeforeGo</dc:creator>
      <pubDate>Sun, 18 Oct 2026 12:36:56 +0000</pubDate>
      <link>https://dev.to/_8f5abeda89bd0a842a1d0/using-your-own-photo-as-an-iphone-countdown-widget-background-what-works-and-what-ios-wont-let-39b1</link>
      <guid>https://dev.to/_8f5abeda89bd0a842a1d0/using-your-own-photo-as-an-iphone-countdown-widget-background-what-works-and-what-ios-wont-let-39b1</guid>
      <description>&lt;p&gt;Short answer. Several iPhone countdown apps let you put your own photo behind the countdown. As of October 2026, Photo Countdown Widget lists your photo as the background with every widget free, and in our app, Countdown Widget: Any Event, your own photo is part of Countdown Pro. Whatever app you pick, the photo only shows on Home Screen widgets. Lock Screen widgets are drawn by iOS in a single tint, so a photo can't show there. In our app the photo shows with the Home Screen's Default or Dark look; with a Tinted or Clear Home Screen, iOS removes the widget background.&lt;/p&gt;

&lt;p&gt;I build Countdown Widget: Any Event. Here's what I learned getting a photo to look right behind a number, in three widget sizes plus the app's own event page.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The Lock Screen won't show it, and that's iOS
Lock Screen widget families (accessoryCircular, accessoryRectangular, accessoryInline) are rendered with the system's vibrant, monochrome treatment: colours are flattened, images become single-tone, and the container background is removed. So in our widget code those families draw no backdrop at all. Drawing a backdrop there would only fight the system material.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you want your photo on the Lock Screen, set the photo as your wallpaper and put the countdown widget over it.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;One photo, framed for each shape&lt;br&gt;
A small widget is square, a medium one is wide, and a large one is close to square but much bigger. If you centre-crop one photo for all three, someone's face ends up cut in half in at least one of them. So the photo isn't stored as one crop. It's stored as a focus point plus a separate framing for each target: the event's own page, small, medium and large. The editor shows a preview for each one (Detail, Small, Medium, Large), and you drag the frame in each preview until it sits right, dragging its corners to resize it.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The number has to stay readable&lt;br&gt;
A white number on a bright beach photo disappears. Three sliders handle that:&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Opacity fades the photo into the backdrop colour.&lt;br&gt;
Blur softens the detail behind the digits.&lt;br&gt;
Text Contrast adds a scrim, so the text stands out without changing the photo itself.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Widgets get small JPEGs, prepared in advance&lt;br&gt;
A widget extension runs with a tight memory budget and shouldn't be decoding a 12-megapixel photo. The app prepares renditions ahead of time: 1,200 px on the long edge for the event page, 690 px for widgets and 480 px for list tiles. The widget only ever loads a small JPEG.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Sync without stale pictures&lt;br&gt;
The photo is stored as an attachment that syncs through iCloud. Each device builds its own renditions from it. Each rendition remembers which version of the photo it came from, so if you change the photo on your iPad, your iPhone doesn't keep showing the old one.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Doing it in Countdown Widget: Any Event&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Tap Add Event and pick a ready-made event or Blank Event. For a countdown you already have, open it and tap the pencil to edit it. Then tap Backdrop.&lt;/li&gt;
&lt;li&gt;Choose Custom, then Add Photo, and pick a photo.&lt;/li&gt;
&lt;li&gt;In Photo adjustments, frame it in the Detail, Small, Medium and Large previews, then set Opacity, Blur and Text Contrast.&lt;/li&gt;
&lt;li&gt;Tap Done, then add the widget to your Home Screen (Default or Dark look).
Using your own photo needs Pro. Unlimited events and every widget size are free, with no ads. The app is on the App Store as "Countdown Widget: Any Event".&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>ios</category>
      <category>swift</category>
      <category>widget</category>
      <category>iphone</category>
    </item>
    <item>
      <title>Christmas is the easy one: the date math behind an iPhone holiday countdown widget</title>
      <dc:creator>BeforeGo</dc:creator>
      <pubDate>Fri, 16 Oct 2026 02:41:28 +0000</pubDate>
      <link>https://dev.to/_8f5abeda89bd0a842a1d0/christmas-is-the-easy-one-the-date-math-behind-an-iphone-holiday-countdown-widget-313k</link>
      <guid>https://dev.to/_8f5abeda89bd0a842a1d0/christmas-is-the-easy-one-the-date-math-behind-an-iphone-holiday-countdown-widget-313k</guid>
      <description>&lt;p&gt;I make an iPhone countdown app, &lt;a href="https://apps.apple.com/us/app/id6799846628" rel="noopener noreferrer"&gt;Countdown Widget: Any Event&lt;/a&gt;. Christmas is the countdown most people want this time of year — and it's the one holiday that needs no thought at all. It's December 25, every year.&lt;/p&gt;

&lt;p&gt;Almost everything else people count down to is harder than it looks. Here are three things that turned out trickier than expected, and what fixed them.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. "Repeat yearly" is not "add one year"
&lt;/h3&gt;

&lt;p&gt;Our first version of yearly repeat did the obvious thing: when an event passes, add one calendar year. That works for birthdays and Christmas. It is wrong for Mother's Day: 9 May 2027 came back as 9 May 2028, when Mother's Day 2028 is the 14th.&lt;/p&gt;

&lt;p&gt;US holidays are full of these. Thanksgiving is the fourth Thursday of November — November 26 in 2026, November 25 in 2027. Easter moves by weeks. Lunar New Year moves by weeks too, on a different calendar.&lt;/p&gt;

&lt;p&gt;The fix was to stop carrying the date forward and store the &lt;em&gt;rule&lt;/em&gt; instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;enum&lt;/span&gt; &lt;span class="kt"&gt;AnnualRule&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Hashable&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;/// The ordinal-th weekday of a month. Sunday is 1; -1 means "last".&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="nf"&gt;nthWeekday&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;month&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;weekday&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;ordinal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;/// A fixed number of days from Western Easter Sunday.&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="nf"&gt;easter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;offsetDays&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;lunarNewYear&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="nf"&gt;lunar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;month&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;day&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;thanksgiving&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;AnnualRule&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;nthWeekday&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;month&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;weekday&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;ordinal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every later occurrence is computed from the rule for that year, so it can't drift. Two details that mattered:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The raw values are frozen.&lt;/strong&gt; The rule is saved to Core Data, CloudKit, backups and &lt;code&gt;.ics&lt;/code&gt; exports as a string like &lt;code&gt;nth:11:5:4&lt;/code&gt;. You can add a case, never rename one, and an unknown string falls back to plain yearly repeat instead of failing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A year the rule can't place is skipped, not fatal.&lt;/strong&gt; If the calendar can't resolve a lunar date for some year, the search moves to the next year rather than stopping on a date in the past.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. All-day events should flip at midnight
&lt;/h3&gt;

&lt;p&gt;A yearly birthday countdown could show "Today" in the morning and "364 days" by the afternoon. The cause: we compared instants. The stored time of day — whatever hour the event happened to be created — decided when "today" ended.&lt;/p&gt;

&lt;p&gt;For an all-day event, the occurrence is still ahead for the whole of its own day:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;hasPassed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="nv"&gt;occurrence&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;at&lt;/span&gt; &lt;span class="nv"&gt;now&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;calendar&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Calendar&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;Bool&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;isAllDay&lt;/span&gt;
        &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;calendar&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startOfDay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;for&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;occurrence&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;calendar&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startOfDay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;for&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;occurrence&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All-day dates are also stored as floating dates (year, month, day — no time zone), so December 25 is December 25 wherever you are when you open the app.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. The widget has to be told when the number changes
&lt;/h3&gt;

&lt;p&gt;A widget doesn't run code all the time; it shows a timeline of entries you hand to WidgetKit ahead of time (I wrote about that in &lt;a href="https://dev.to/_8f5abeda89bd0a842a1d0/how-iphone-countdown-widgets-actually-work-and-which-apps-keep-them-free-22a9"&gt;the first post&lt;/a&gt;). For a day count, that means one entry at each local midnight, and asking for a new timeline after the last one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;entries&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;upcomingMidnights&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;map&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;calendar&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startOfDay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;for&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;$0&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;reloadAt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;entries&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;last&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;date&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addingTimeInterval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;86_400&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kt"&gt;Timeline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;after&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reloadAt&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is also why a countdown &lt;em&gt;widget&lt;/em&gt; beats a countdown &lt;em&gt;wallpaper&lt;/em&gt;. A number baked into a wallpaper image stays the same until you make a new image; a widget goes down by one every day on its own.&lt;/p&gt;

&lt;h3&gt;
  
  
  If you just want the number on your phone
&lt;/h3&gt;

&lt;p&gt;Step-by-step for the Lock Screen and Home Screen — including iOS 27's new extra-large widget size — is here: &lt;a href="https://beforego.art/tools/christmas-countdown-widget-iphone/" rel="noopener noreferrer"&gt;Christmas countdown widget for iPhone&lt;/a&gt;. In our app every widget size is free on both screens and you can keep as many countdowns as you like; Pro adds backdrops, your own photos, typefaces and a Lock Screen Live Activity.&lt;/p&gt;

&lt;p&gt;If you're comparing apps, I keep a free-tier comparison of seven countdown apps here: &lt;a href="https://beforego.art/tools/best-countdown-widget-apps-iphone/" rel="noopener noreferrer"&gt;Best countdown widget apps for iPhone&lt;/a&gt;. It includes the ones that make the widget the paid part.&lt;/p&gt;

</description>
      <category>ios</category>
      <category>swift</category>
      <category>widgetkit</category>
      <category>productivity</category>
    </item>
    <item>
      <title>How iPhone countdown widgets actually work (and which apps keep them free)</title>
      <dc:creator>BeforeGo</dc:creator>
      <pubDate>Wed, 14 Oct 2026 04:34:33 +0000</pubDate>
      <link>https://dev.to/_8f5abeda89bd0a842a1d0/how-iphone-countdown-widgets-actually-work-and-which-apps-keep-them-free-22a9</link>
      <guid>https://dev.to/_8f5abeda89bd0a842a1d0/how-iphone-countdown-widgets-actually-work-and-which-apps-keep-them-free-22a9</guid>
      <description>&lt;p&gt;&lt;em&gt;Disclosure: I build one of the apps mentioned below, Countdown Widget: Any Event. The comparison at the end only uses what each app says on its own App Store listing, and links to all of them so you can check.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If you have ever put a countdown widget on your Home Screen and wondered why it says "12 days" instead of ticking down the seconds, the answer is not laziness on the developer's part. It is how WidgetKit works. Once you know the rules, you can tell at a glance which apps are doing it well — and which ones lock the widget behind a paywall.&lt;/p&gt;

&lt;h2&gt;
  
  
  Widgets don't run code all the time
&lt;/h2&gt;

&lt;p&gt;A widget is not a tiny app that runs in the background. Your app hands the system a &lt;strong&gt;timeline&lt;/strong&gt;: a list of entries, each with a date and the data to draw at that date. The system renders each entry when its date arrives, and asks for a new timeline when the policy you chose says so. Reloads are budgeted, so you cannot simply redraw every second.&lt;/p&gt;

&lt;p&gt;For a "days left" counter, the natural timeline is one entry now and one at each upcoming midnight:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;getTimeline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nv"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;completion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kd"&gt;@escaping&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Timeline&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;CountdownEntry&lt;/span&gt;&lt;span class="o"&gt;&amp;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="kt"&gt;Void&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;calendar&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;Calendar&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;current&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;today&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;calendar&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startOfDay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;for&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;// One entry now, then one at each of the next seven midnights.&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;dates&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;Date&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;compactMap&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;calendar&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;byAdding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;day&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;$0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;today&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;entries&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dates&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;map&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="kt"&gt;CountdownEntry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;$0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;loadNextEvent&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nf"&gt;completion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Timeline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;atEnd&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;and the view counts whole days between the start of the entry's day and the start of the event's day:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;days&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;Calendar&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dateComponents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;day&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="nv"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Calendar&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startOfDay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;for&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nv"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Calendar&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startOfDay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;for&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;day&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Counting from start-of-day to start-of-day is what keeps "1 day left" from flipping to "0" at the wrong hour when the event has a time attached.&lt;/p&gt;

&lt;h2&gt;
  
  
  Live seconds without reloads
&lt;/h2&gt;

&lt;p&gt;You &lt;em&gt;can&lt;/em&gt; show a ticking clock in a widget — the trick is that the system animates it, not your code. &lt;code&gt;Text(date, style: .timer)&lt;/code&gt; renders a live countdown to &lt;code&gt;date&lt;/code&gt; and keeps it current with no timeline reloads. On iOS 16 and later, &lt;code&gt;Text(timerInterval: Date.now...event.date, countsDown: true)&lt;/code&gt; does the same and stops at zero instead of counting back up.&lt;/p&gt;

&lt;p&gt;The trade-off is that the format is fixed (hours:minutes:seconds), which reads badly when an event is 40 days away. A sensible pattern is days for most of the countdown and a live clock only on the final day.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Lock Screen is a separate set of widget families
&lt;/h2&gt;

&lt;p&gt;Lock Screen widgets (iOS 16+) are their own families — &lt;code&gt;.accessoryCircular&lt;/code&gt;, &lt;code&gt;.accessoryRectangular&lt;/code&gt; and &lt;code&gt;.accessoryInline&lt;/code&gt; — drawn in a monochrome or tinted style. An app has to support them explicitly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;supportedFamilies&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;systemSmall&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;systemMedium&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;accessoryCircular&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;accessoryRectangular&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;accessoryInline&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is also where many countdown apps draw the line between free and paid, which matters if the Lock Screen is where you actually look.&lt;/p&gt;

&lt;h2&gt;
  
  
  The last few hours: Live Activities
&lt;/h2&gt;

&lt;p&gt;For the final stretch before a flight or a launch, a widget is the wrong tool. A &lt;strong&gt;Live Activity&lt;/strong&gt; (ActivityKit) sits on the Lock Screen and in the Dynamic Island and can be updated while it runs. It is meant for short events: the system lets one stay active for up to eight hours, and it can remain on the Lock Screen for up to four more after it ends. That makes it a good fit for "the day of", not for a countdown that lasts months.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this means when you pick an app
&lt;/h2&gt;

&lt;p&gt;Every app below can count down to a date. The real differences are in what the free version lets you put on your screens. Taken from each app's US App Store listing (September 2026):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;App&lt;/th&gt;
&lt;th&gt;Widgets in the free version&lt;/th&gt;
&lt;th&gt;Events in the free version&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;a href="https://apps.apple.com/us/app/id6799846628" rel="noopener noreferrer"&gt;Countdown Widget: Any Event&lt;/a&gt; (mine)&lt;/td&gt;
&lt;td&gt;Every size, Home Screen and Lock Screen&lt;/td&gt;
&lt;td&gt;Unlimited&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://apps.apple.com/us/app/id576177593" rel="noopener noreferrer"&gt;Countdown Star&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Home Screen widgets listed; listing doesn't spell out what is paid&lt;/td&gt;
&lt;td&gt;Unlimited&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://apps.apple.com/us/app/id1403367428" rel="noopener noreferrer"&gt;Countdown (Find Appiness)&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Home Screen, Lock Screen and StandBy widgets are Premium&lt;/td&gt;
&lt;td&gt;Unlimited&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://apps.apple.com/us/app/id983258067" rel="noopener noreferrer"&gt;Countdown: Event Countdown&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;"Next Event" Home Screen widget free; Lock Screen, multi-event and live widgets paid&lt;/td&gt;
&lt;td&gt;Unlimited&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://apps.apple.com/us/app/id1534850579" rel="noopener noreferrer"&gt;Countdown Buddy&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;One basic widget free; 11 styles and more widgets paid&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If you only need one countdown and care most about how it looks, a widget designer like Countdown Buddy is a good fit. If you keep many dates — birthdays, trips, exams — and want them on the Lock Screen without paying, check the free tier carefully before you commit.&lt;/p&gt;

&lt;p&gt;The full comparison of seven apps — repeats, reminders, sync and ads included — is here: &lt;a href="https://beforego.art/tools/best-countdown-widget-apps-iphone/" rel="noopener noreferrer"&gt;Best countdown widget apps for iPhone (2026), free tiers compared&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ios</category>
      <category>swift</category>
      <category>widgetkit</category>
      <category>productivity</category>
    </item>
  </channel>
</rss>
