<?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: Yoshiaki Hirokawa</title>
    <description>The latest articles on DEV Community by Yoshiaki Hirokawa (@netflowers).</description>
    <link>https://dev.to/netflowers</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%2F4004792%2Fbe5f2040-425a-4708-89e6-124c3069d8b3.png</url>
      <title>DEV Community: Yoshiaki Hirokawa</title>
      <link>https://dev.to/netflowers</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/netflowers"/>
    <language>en</language>
    <item>
      <title>Slope &amp; Fall Level — Set any grade, not just level</title>
      <dc:creator>Yoshiaki Hirokawa</dc:creator>
      <pubDate>Sat, 19 Sep 2026 01:41:58 +0000</pubDate>
      <link>https://dev.to/netflowers/slope-fall-level-set-any-grade-not-just-level-4igj</link>
      <guid>https://dev.to/netflowers/slope-fall-level-set-any-grade-not-just-level-4igj</guid>
      <description>&lt;p&gt;Most levels only know flat. Set the grade you are after in percent, degrees, a ratio or mm/m, rest the phone on the work, and chase the reading until it lands. No ads.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Set any grade, not just level&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Most spirit levels only tell you one thing: whether something is flat. But&lt;br&gt;
a drain has to fall, a ramp has to climb, and a deck has to shed water.&lt;br&gt;
Slope &amp;amp; Fall Level is built for the times the answer is not zero.&lt;/p&gt;

&lt;p&gt;Enter the grade you want in whatever notation your trade uses — a&lt;br&gt;
percentage, an angle, a ratio like 1:12, or a fall in millimetres per&lt;br&gt;
metre. The app converts between all of them, so a drawing marked 1 in 80&lt;br&gt;
and a spec written as 1.25% stop being two different problems.&lt;/p&gt;

&lt;p&gt;Rest the phone on the pipe, the board or the screed rail, and the display&lt;br&gt;
shows the difference between where you are and where you want to be. Get&lt;br&gt;
it right and the app sounds a tone and gives a short buzz, so you can keep&lt;br&gt;
your eyes on the work instead of the screen.&lt;/p&gt;

&lt;p&gt;Calibrate against a surface you trust to cancel out the case and any&lt;br&gt;
bench that is not quite true. Save the grades you use often, so the next&lt;br&gt;
run of pipe starts where the last one left off.&lt;/p&gt;

&lt;p&gt;Readings come from the phone's own motion sensors, and accuracy depends on&lt;br&gt;
the phone lying flat and still on a straight surface. It works alongside&lt;br&gt;
your level and your laser, not in place of them, and any grade that has to&lt;br&gt;
meet a code or a drawing should be confirmed with the instrument that&lt;br&gt;
drawing was written for.&lt;/p&gt;

&lt;h3&gt;
  
  
  Set the grade you actually need
&lt;/h3&gt;

&lt;p&gt;Most spirit levels only tell you whether something is flat. A drain has to fall, a ramp has to climb and a deck has to shed water. Enter the grade you want and the app shows the difference between where you are and where you want to be.&lt;/p&gt;

&lt;h3&gt;
  
  
  Four notations, one job
&lt;/h3&gt;

&lt;p&gt;Percentage, angle, a ratio like 1:80, or a fall in millimetres per metre. The app converts between all of them, so a drawing marked 1 in 80 and a spec written as 1.25% stop being two different problems.&lt;/p&gt;

&lt;h3&gt;
  
  
  It tells you which way to move
&lt;/h3&gt;

&lt;p&gt;Not just how far off you are, but whether to ease the slope off or steepen it, and by how much.&lt;/p&gt;

&lt;h3&gt;
  
  
  A tone when you reach it
&lt;/h3&gt;

&lt;p&gt;Get the grade right and the app sounds a tone and gives a short buzz, so you can keep your eyes on the work instead of the screen.&lt;/p&gt;

&lt;h3&gt;
  
  
  Calibrate against a surface you trust
&lt;/h3&gt;

&lt;p&gt;Zeroing on a known-true surface cancels out the phone case and any bench that is not quite flat.&lt;/p&gt;

&lt;h3&gt;
  
  
  Save the grades you use often
&lt;/h3&gt;

&lt;p&gt;Drains, ramps, gutters and decks are already there as presets, and you can store your own so the next run of pipe starts where the last one left off.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How accurate is it?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Readings come from the phone's own motion sensors, so accuracy depends on the phone lying flat and still on a straight surface. Calibrating against a surface you trust removes the offset from the case and the bench. It works alongside a spirit level or a laser, not instead of one — for anything a code or drawing depends on, confirm it with the instrument that drawing assumes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Which notations does it handle?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Percentage, degrees, ratio (like 1:12 or 1 in 80) and fall per metre in millimetres. Imperial units are supported throughout, including inches per foot. You can enter a grade in one notation and read it in any of the others.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do I need a network connection?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No. Everything is calculated on the device from its own sensors. Your saved grades and settings stay on your phone.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I use it on a trailer, a shelf or a solar panel?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes. Anything where you need a specific angle rather than dead level. The presets cover the common trades, but you can set and save any grade you like.&lt;/p&gt;




&lt;p&gt;&lt;a href="https://www.netflowers.co.jp/ios/pitch-level/" rel="noopener noreferrer"&gt;www.netflowers.co.jp&lt;/a&gt;&lt;/p&gt;

</description>
      <category>slope</category>
      <category>level</category>
      <category>grade</category>
      <category>fall</category>
    </item>
    <item>
      <title>Laundry Weather: Rain Alert — Live radar tracks storms ahead</title>
      <dc:creator>Yoshiaki Hirokawa</dc:creator>
      <pubDate>Mon, 14 Sep 2026 01:41:58 +0000</pubDate>
      <link>https://dev.to/netflowers/laundry-weather-rain-alert-live-radar-tracks-storms-ahead-56nm</link>
      <guid>https://dev.to/netflowers/laundry-weather-rain-alert-live-radar-tracks-storms-ahead-56nm</guid>
      <description>&lt;p&gt;Summer storm season is here. Hang out your laundry, tap once, and SentakuGuard watches the sky — rain alerts on your Lock Screen before the first drop falls.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Live radar tracks storms ahead&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Hang your laundry, tap one button, and let SentakuGuard stand watch.&lt;br&gt;
It keeps an eye on approaching rain clouds and sudden downpours, shows&lt;br&gt;
the risk right on your Lock Screen and Dynamic Island as a Live Activity,&lt;br&gt;
and warns you before the rain reaches your laundry — so you can bring&lt;br&gt;
it in dry. One purpose, done well: from "Hung out" to "Brought in".&lt;/p&gt;

&lt;h3&gt;
  
  
  One tap to start the watch
&lt;/h3&gt;

&lt;p&gt;Hang your laundry, tap "Hung out!", and SentakuGuard starts watching the sky for you until you bring it in.&lt;/p&gt;

&lt;h3&gt;
  
  
  Live Activity on your Lock Screen
&lt;/h3&gt;

&lt;p&gt;The current rain risk stays visible on your Lock Screen and in the Dynamic Island — no need to open the app.&lt;/p&gt;

&lt;h3&gt;
  
  
  Warned before the rain arrives
&lt;/h3&gt;

&lt;p&gt;SentakuGuard tracks approaching rain clouds and sudden downpours, and alerts you minutes before rain reaches your laundry.&lt;/p&gt;

&lt;h3&gt;
  
  
  12-hour rain risk at a glance
&lt;/h3&gt;

&lt;p&gt;A simple timeline shows when rain is expected over the next 12 hours, so you can pick the best time to hang and to bring in.&lt;/p&gt;

&lt;h3&gt;
  
  
  A simple log of every watch
&lt;/h3&gt;

&lt;p&gt;Each "Hung out" to "Brought in" session is recorded, so you can look back on how your laundry days went.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where does the weather data come from?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Rain forecasts come from Apple Weather (WeatherKit), using your device's location. Forecast accuracy and coverage depend on your region.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why don't I see the Live Activity on my Lock Screen?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Make sure Live Activities are enabled in Settings &amp;gt; SentakuGuard, and start a watch with the "Hung out!" button. The Dynamic Island is available on supported iPhone models.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does the app work in the background?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes. While a watch is active, SentakuGuard updates the rain risk periodically and sends an alert when rain is approaching. Keeping notifications enabled is recommended.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is my location sent anywhere?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Your location is used only on your device to fetch the weather for your area. It is never stored on our servers or shared with anyone.&lt;/p&gt;




&lt;p&gt;&lt;a href="https://www.netflowers.co.jp/ios/sentakuguard/" rel="noopener noreferrer"&gt;www.netflowers.co.jp&lt;/a&gt;&lt;/p&gt;

</description>
      <category>clothesline</category>
      <category>drying</category>
      <category>downpour</category>
      <category>shower</category>
    </item>
    <item>
      <title>SeasonWood: Firewood Dryer — Know which cord is ready</title>
      <dc:creator>Yoshiaki Hirokawa</dc:creator>
      <pubDate>Tue, 08 Sep 2026 01:42:23 +0000</pubDate>
      <link>https://dev.to/netflowers/seasonwood-firewood-dryer-know-which-cord-is-ready-3i4o</link>
      <guid>https://dev.to/netflowers/seasonwood-firewood-dryer-know-which-cord-is-ready-3i4o</guid>
      <description>&lt;p&gt;Stop guessing which stack is ready. SeasonWood watches your local climate and tells you when each cord of firewood is done seasoning.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Know which cord is ready&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Firewood takes months to become firewood. You split it, you stack it, and then you wait: six months for pine, a year for ash, two or three years for oak. Come winter you are guessing which pile is ready, and burning wet wood means a smoky fire, wasted heat and a dirty chimney.&lt;/p&gt;

&lt;p&gt;SeasonWood keeps track of the waiting. Log each stack the day you build it, and the app tells you when that wood should be ready to burn, and which pile to reach for first.&lt;/p&gt;

&lt;p&gt;BUILT AROUND YOUR CLIMATE, NOT A GENERIC CHART&lt;br&gt;
A cord of oak dries very differently in Arizona than it does in Maine. SeasonWood pulls several years of weather history for your location, works out how much drying actually happens in each month of the year, and stretches or shortens the estimate to match. Wood stacked in November in a cold, damp climate barely moves until spring, and the app knows that.&lt;/p&gt;

&lt;p&gt;EVERY STACK, EVERY DETAIL&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Species, from fast-drying pine and poplar to slow, dense oak and hickory&lt;/li&gt;
&lt;li&gt;The date you split it, and how big you split it&lt;/li&gt;
&lt;li&gt;Covered on top, wrapped, or open to the weather&lt;/li&gt;
&lt;li&gt;Single row, double row or a heap, because airflow changes everything&lt;/li&gt;
&lt;li&gt;Sun or shade, up on pallets or down on the dirt&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each of these shifts the estimate, and the app shows you exactly how. Open any stack and you see the whole calculation: base time for the species, then the adjustment for your climate, your stacking and your split size. No black box.&lt;/p&gt;

&lt;p&gt;IF YOU OWN A MOISTURE METER&lt;br&gt;
Take a reading whenever you like and log it. After a couple of readings SeasonWood learns how fast that particular stack is really drying and corrects its own forecast. If a reading already shows the wood is dry, the measurement wins, because a number beats a prediction. And if you do not own a meter, everything still works.&lt;/p&gt;

&lt;p&gt;WHAT YOU GET&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Progress ring and estimated ready date for every stack&lt;/li&gt;
&lt;li&gt;Sorted so the pile that becomes usable first is on top&lt;/li&gt;
&lt;li&gt;A drying curve showing estimated moisture over time, with your readings marked&lt;/li&gt;
&lt;li&gt;Home Screen widgets: how many stacks are ready right now&lt;/li&gt;
&lt;li&gt;Optional reminders when a stack comes ready, and before heating season&lt;/li&gt;
&lt;li&gt;Volume in cords, face cords, cubic meters or stere&lt;/li&gt;
&lt;li&gt;Works offline once your climate data is stored&lt;/li&gt;
&lt;li&gt;No account, no sign-up. Your stacks stay on your device&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;FOR ANYONE WHO BURNS WOOD&lt;br&gt;
Wood stove and fireplace owners planning the winter. Homesteaders rotating three years of oak. Anyone running an outdoor boiler, a fire pit, a pizza oven or a smoker who wants dry wood instead of a smoldering one.&lt;/p&gt;

&lt;p&gt;These are estimates for planning, based on published seasoning times and local climate data. Real stacks vary with how they are built, where they sit, the weather that actually shows up and the wood itself. SeasonWood is not a safety instrument and does not certify that wood is safe to burn. The 20% figure is a widely used guideline, and a moisture meter is the way to confirm it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Every stack, from the day you split it
&lt;/h3&gt;

&lt;p&gt;Log a pile the day you build it: species from 23 built in, split date, split size, whether it is covered on top, wrapped or open to the weather, single row, double row or a heap, sun or shade, up on pallets or down on the dirt. Every one of those shifts the estimate.&lt;/p&gt;

&lt;h3&gt;
  
  
  Your climate, not a generic chart
&lt;/h3&gt;

&lt;p&gt;A cord of oak dries very differently in Arizona than it does in Maine. SeasonWood pulls several years of weather history for your location and works out how much drying actually happens in each month of the year. Wood stacked in November barely moves until spring, and the app knows that.&lt;/p&gt;

&lt;h3&gt;
  
  
  The whole calculation, in the open
&lt;/h3&gt;

&lt;p&gt;Open any stack and you see how the number was reached: base seasoning time for the species, then the adjustment for your climate, your stacking and your split size. No black box, nothing hidden behind a single date.&lt;/p&gt;

&lt;h3&gt;
  
  
  Moisture meter readings sharpen it
&lt;/h3&gt;

&lt;p&gt;Log readings whenever you take them and SeasonWood corrects its forecast for that particular pile. A reading at or below your target always beats the model, because a number beats a prediction. No meter? Everything still works.&lt;/p&gt;

&lt;h3&gt;
  
  
  Widgets, reminders and offline use
&lt;/h3&gt;

&lt;p&gt;Small and medium Home Screen widgets show how many stacks are ready right now. Optional notifications tell you when a pile comes ready and before heating season starts. Volume in cords, face cords, cubic meters or stere, and it all works offline once your climate data is stored.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do I need a moisture meter?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No. SeasonWood works fine without one and the estimate is built entirely from species, climate, stacking and split size. If you do own a meter, logging a few readings lets the app learn how fast that specific pile is really drying and correct itself, and a reading at or below your target is taken over the prediction. A meter is also the only way to actually confirm the wood is dry, which is worth doing before you burn it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why does it say longer than the chart I read online?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Those charts assume good conditions and rarely say where. SeasonWood starts from published seasoning times for the species, then adjusts for the weather history at your location, how you stacked it and how big you split it. Big rounds in a shaded heap on the ground in a damp climate can easily take twice as long as the same species split small, single row, up on pallets and in the sun.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why does my climate slow things down so much?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Drying happens when the air is warm and dry enough to pull moisture out of the wood, and that varies enormously by month. A pile stacked in late fall in a cold, wet region does almost nothing until spring, so the app spreads the remaining drying across the months that will actually do the work rather than counting calendar days evenly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I use it for a pizza oven, smoker or fire pit?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes, and plenty of people do. The tracking is the same whether the wood ends up in a stove, an outdoor boiler, a fire pit, a wood-fired oven or a smoker. Dry wood is what separates a clean fire from a smoldering one, so the same target applies.&lt;/p&gt;




&lt;p&gt;&lt;a href="https://www.netflowers.co.jp/ios/seasonwood/" rel="noopener noreferrer"&gt;www.netflowers.co.jp&lt;/a&gt;&lt;/p&gt;

</description>
      <category>seasoning</category>
      <category>wood</category>
      <category>stack</category>
      <category>rack</category>
    </item>
    <item>
      <title>SealantYield: Caulk Calculator — Caulk, backer rod &amp; cartridges</title>
      <dc:creator>Yoshiaki Hirokawa</dc:creator>
      <pubDate>Tue, 08 Sep 2026 01:41:51 +0000</pubDate>
      <link>https://dev.to/netflowers/sealantyield-caulk-calculator-caulk-backer-rod-cartridges-4pdb</link>
      <guid>https://dev.to/netflowers/sealantyield-caulk-calculator-caulk-backer-rod-cartridges-4pdb</guid>
      <description>&lt;p&gt;Add up every joint on the job before you round to whole cartridges. Backer rod, sausage packs and leftover-tube tracking included. No ads, no account, fully offline.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Caulk, backer rod &amp;amp; cartridges&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Enter the length, width and depth of your joint and SealantYield tells you&lt;br&gt;
how many cartridges to buy. Not a single-shot web calculator: add every run&lt;br&gt;
on the job, keep them in one project, and the totals add up before they are&lt;br&gt;
rounded to whole tubes, so you do not over-order.&lt;/p&gt;

&lt;p&gt;Handles 290, 300 and 310 mL cartridges, 400 and 600 mL sausage packs, quarts&lt;br&gt;
and gallons, or any custom size you type in. Metric and imperial both work.&lt;/p&gt;

&lt;p&gt;Backer rod is counted too: pick the diameter your spec calls for and get the&lt;br&gt;
length, the number of rolls and the cost alongside the sealant.&lt;/p&gt;

&lt;p&gt;Half-used cartridge in the van? Log what is left and the app works backwards&lt;br&gt;
to tell you how many more metres it will run at your joint size.&lt;/p&gt;

&lt;p&gt;Export a clean estimate as a PDF and share it on the spot. No account, no&lt;br&gt;
ads, no data collection, and it works fully offline.&lt;/p&gt;

&lt;p&gt;SealantYield is an estimating aid based on the numbers you enter. Always&lt;br&gt;
check the product label and the manufacturer's instructions before you buy&lt;br&gt;
or apply.&lt;/p&gt;

&lt;h3&gt;
  
  
  Multi-joint calculation
&lt;/h3&gt;

&lt;p&gt;Add every joint on the project to one space and get total cartridges before rounding, so you don't over-order&lt;/p&gt;

&lt;h3&gt;
  
  
  Multiple size support
&lt;/h3&gt;

&lt;p&gt;Handles 290, 300 and 310 mL cartridges, 400 and 600 mL sausage packs, or any custom size&lt;/p&gt;

&lt;h3&gt;
  
  
  Backer rod calculation
&lt;/h3&gt;

&lt;p&gt;Choose the diameter of backer rod and get length, number of rolls and cost&lt;/p&gt;

&lt;h3&gt;
  
  
  Metric and imperial
&lt;/h3&gt;

&lt;p&gt;Works in both metric and imperial systems&lt;/p&gt;

&lt;h3&gt;
  
  
  Remainder calculation
&lt;/h3&gt;

&lt;p&gt;Log what's left of a half-used cartridge and the app calculates how many more meters it will cover&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does SealantYield collect personal data?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No, SealantYield doesn't collect any personal data. All calculations are done locally on your device.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does it work without internet?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes, the app works completely offline.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I export calculations?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes, you can export clear estimates as PDF files and share them instantly.&lt;/p&gt;




&lt;p&gt;&lt;a href="https://www.netflowers.co.jp/ios/sealantyield/" rel="noopener noreferrer"&gt;www.netflowers.co.jp&lt;/a&gt;&lt;/p&gt;

</description>
      <category>sealant</category>
      <category>joint</category>
      <category>sausage</category>
      <category>silicone</category>
    </item>
    <item>
      <title>PetShot: Pet Vaccine Reminder — Never miss a dose or a shot</title>
      <dc:creator>Yoshiaki Hirokawa</dc:creator>
      <pubDate>Mon, 07 Sep 2026 01:42:21 +0000</pubDate>
      <link>https://dev.to/netflowers/petshot-pet-vaccine-reminder-never-miss-a-dose-or-a-shot-31m9</link>
      <guid>https://dev.to/netflowers/petshot-pet-vaccine-reminder-never-miss-a-dose-or-a-shot-31m9</guid>
      <description>&lt;p&gt;Set the interval once. PetShot recalculates the next due date from the day you actually treated your pet, and reminds you before it's due.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Never miss a dose or a shot&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Set the interval once — monthly, every three months, once a year — and PetShot works out the next due date from the day you actually treated your pet, not the day it was originally scheduled. Late by three days? The next one starts three days late too, automatically.&lt;/p&gt;

&lt;p&gt;ONE SCREEN, EVERY ANIMAL&lt;br&gt;
The home screen lists what is due next across all of your pets, closest first. Overdue is red, due this week is amber, everything else waits its turn. Tap an entry when it is done.&lt;/p&gt;

&lt;p&gt;YOU DECIDE THE SCHEDULE&lt;br&gt;
Common intervals are offered as a starting point to save typing — vaccination, worming, flea and tick treatment, check-ups. You can change any of them. PetShot does not tell you what your animal needs; your vet does.&lt;/p&gt;

&lt;p&gt;REMINDERS THAT ARRIVE IN TIME&lt;br&gt;
Choose how many days ahead you want to be warned, and at what time. Set several reminders for the same entry if one is not enough. All of it happens on your device — no account, no sign-up, nothing sent anywhere.&lt;/p&gt;

&lt;p&gt;ON YOUR HOME SCREEN&lt;br&gt;
A widget shows the next thing due and how many days are left, so you see it without opening anything.&lt;/p&gt;

&lt;p&gt;WHAT YOU CAN KEEP&lt;br&gt;
• Several pets — dogs, cats, rabbits, ferrets, birds, reptiles&lt;br&gt;
• Vaccinations, worming, flea and tick treatment, check-ups, grooming&lt;br&gt;
• Notes for the product name, the batch, the clinic&lt;br&gt;
• A full history of what was done and when, exportable as CSV&lt;br&gt;
• Weight and birthday, if you want them&lt;/p&gt;

&lt;p&gt;PAID ONCE, THAT IS ALL&lt;br&gt;
No subscription, no adverts, no account. Everything stays on your device. Available in many languages.&lt;/p&gt;

&lt;p&gt;PetShot is a reminder book, not a medical device. It does not give veterinary advice and it does not decide what treatment your animal needs — always follow your vet.&lt;/p&gt;

&lt;h3&gt;
  
  
  Central Management
&lt;/h3&gt;

&lt;p&gt;Track all pet care schedules in one convenient place&lt;/p&gt;

&lt;h3&gt;
  
  
  Smart Reminders
&lt;/h3&gt;

&lt;p&gt;Get notified before appointments and preventive care are due&lt;/p&gt;

&lt;h3&gt;
  
  
  Home Screen Widget
&lt;/h3&gt;

&lt;p&gt;See your next scheduled task at a glance with our Home Screen widget&lt;/p&gt;

&lt;h3&gt;
  
  
  No Network or Accounts
&lt;/h3&gt;

&lt;p&gt;Everything stays on your device — no internet, no accounts, no tracking&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Notifications Not Arriving?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you're not receiving reminders, check: (1) Notifications Permission — Go to Settings &amp;gt; PetShot and ensure 'Notifications' is enabled; (2) Focus Mode or Do Not Disturb — These can silence notifications; (3) System Time — Ensure your device time is set correctly; (4) Multiple Reminders — You can set several reminders for the same entry if one notification isn't enough.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to Add a Widget&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;PetShot includes Home Screen widgets to see your next due item at a glance: (1) Long-press your Home Screen and tap the '+' button; (2) Search for and select 'PetShot'; (3) Choose the Small widget (next 1 item) or Medium widget (next 3 items); (4) Tap 'Add Widget' and choose your Home Screen location. The widget updates daily and shows how many days remain until each task is due.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is PetShot a Medical Device?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No. PetShot is a reminder app, not a medical device or veterinary consultation tool. It does not provide medical advice or diagnoses. Always follow your veterinarian's guidance for your pet's health care.&lt;/p&gt;




&lt;p&gt;&lt;a href="https://www.netflowers.co.jp/ios/petshot/" rel="noopener noreferrer"&gt;www.netflowers.co.jp&lt;/a&gt;&lt;/p&gt;

</description>
      <category>pet</category>
      <category>dog</category>
      <category>cat</category>
      <category>vaccine</category>
    </item>
    <item>
      <title>FertClock: Fertilizer Timing — Rate, area &amp; when to reapply</title>
      <dc:creator>Yoshiaki Hirokawa</dc:creator>
      <pubDate>Mon, 07 Sep 2026 01:41:50 +0000</pubDate>
      <link>https://dev.to/netflowers/fertclock-fertilizer-timing-rate-area-when-to-reapply-40mj</link>
      <guid>https://dev.to/netflowers/fertclock-fertilizer-timing-rate-area-when-to-reapply-40mj</guid>
      <description>&lt;p&gt;The bag says 70 days — but that assumes 25°C. FertClock uses real temperatures to show how much fertilizer is left and when to feed again. Every formula shown.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rate, area &amp;amp; when to reapply&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Fertilizer does not run out on a calendar. It runs out on temperature.&lt;/p&gt;

&lt;p&gt;The bag says "70 day type" — but that number assumes 25°C. In a cool spring the same product lasts 117 days. In midsummer it is gone in 58. FertClock uses the actual temperatures at your plot to tell you how much is still working and when to feed again.&lt;/p&gt;

&lt;p&gt;WORK OUT THE RATE&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Enter your area, or measure it: tap the corners on the satellite map, or walk the bed with AR&lt;/li&gt;
&lt;li&gt;Pick a blend — 10-10-10, 8-8-8, 46-0-0 and more — or type your own N-P-K&lt;/li&gt;
&lt;li&gt;Get the total to spread AND the amount per square metre, side by side&lt;/li&gt;
&lt;li&gt;Bags to buy, with the leftover shown&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;WATCH IT RUN DOWN&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A graph shows the nitrogen still in the soil, day by day&lt;/li&gt;
&lt;li&gt;Solid line where real temperatures were measured, dashed where forecast&lt;/li&gt;
&lt;li&gt;Base dressing and top dressing stack up on the same chart&lt;/li&gt;
&lt;li&gt;The next feeding date, with the days remaining&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;EVERY NUMBER SHOWS ITS WORK&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tap any result to see the formula, the values put into it, and the answer&lt;/li&gt;
&lt;li&gt;Degree-days are listed one day at a time, so the running total adds up&lt;/li&gt;
&lt;li&gt;Constants are printed in full — ln(5) = 1.609438 — so you can check it on a pocket calculator&lt;/li&gt;
&lt;li&gt;No hidden model. This is not a paid feature; it is how the app works&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;GARDEN, LAWN, FIELD&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Kitchen garden: g/m², planter counts, per-planter amounts&lt;/li&gt;
&lt;li&gt;Lawn: annual nitrogen budget, warm- and cool-season, lb/1000 sq ft&lt;/li&gt;
&lt;li&gt;Field: kg/10a, ares and hectares, base and top dressing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;QUIET BY DESIGN&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No account, no ads, no subscription — one price, yours forever&lt;/li&gt;
&lt;li&gt;Works offline. Only the weather comes from the network&lt;/li&gt;
&lt;li&gt;Location is optional: type temperatures in by hand if you prefer&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;FertClock estimates from what you enter and from weather data. The figures are a guide — the product label, your soil, and local guidance come first. It does not select products, diagnose soil, or predict yield, and it does not guarantee results.&lt;/p&gt;

&lt;h3&gt;
  
  
  Work out the rate
&lt;/h3&gt;

&lt;p&gt;Enter your area, or measure it with a satellite map or AR. Pick a blend like 10-10-10 or enter your own N-P-K. Get total to spread and per square metre side by side.&lt;/p&gt;

&lt;h3&gt;
  
  
  Watch it run down
&lt;/h3&gt;

&lt;p&gt;A graph shows the nitrogen still in the soil day by day. Solid line for real temperatures, dashed for forecast. Base and top dressing stack on the same chart.&lt;/p&gt;

&lt;h3&gt;
  
  
  Every number shows its work
&lt;/h3&gt;

&lt;p&gt;Tap any result to see the formula, the values put into it, and the answer. Degree-days listed one day at a time. Constants printed in full so you can check on a calculator.&lt;/p&gt;

&lt;h3&gt;
  
  
  Three modes
&lt;/h3&gt;

&lt;p&gt;Kitchen garden: g/m² and planter counts. Lawn: annual nitrogen budget, warm or cool season. Field: kg/10a and bag counts.&lt;/p&gt;

&lt;h3&gt;
  
  
  Quiet by design
&lt;/h3&gt;

&lt;p&gt;No account, no ads, no subscription. Works offline except for weather. Location is optional.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why use temperature instead of days?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The 70-day type on a fertilizer bag assumes 25°C. In a cool spring the same product lasts 117 days. In midsummer it is gone in 58. FertClock uses your actual temperatures to show how much fertilizer is left.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does FertClock collect personal data?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No. All calculations happen on your device. Location is optional and used only to fetch weather, not stored.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does it work without internet?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes, except for weather forecast. You can type temperatures in by hand if you prefer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What does FertClock not do?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It does not select products, diagnose soil, or predict yield. The figures are a guide — your product label and local guidance come first.&lt;/p&gt;




&lt;p&gt;&lt;a href="https://www.netflowers.co.jp/ios/fertclock/" rel="noopener noreferrer"&gt;www.netflowers.co.jp&lt;/a&gt;&lt;/p&gt;

</description>
      <category>fertiliser</category>
      <category>npk</category>
      <category>lawn</category>
      <category>garden</category>
    </item>
    <item>
      <title>LureColorCam: Read the Water — Point, read, pick your color</title>
      <dc:creator>Yoshiaki Hirokawa</dc:creator>
      <pubDate>Sun, 06 Sep 2026 01:42:24 +0000</pubDate>
      <link>https://dev.to/netflowers/lurecolorcam-read-the-water-point-read-pick-your-color-1k0n</link>
      <guid>https://dev.to/netflowers/lurecolorcam-read-the-water-point-read-pick-your-color-1k0n</guid>
      <description>&lt;p&gt;Point your camera at the water, get lure color suggestions matched to clarity and light. Keep a log of every reading.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Point, read, pick your color&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Every angler knows the rule of thumb: bright day, natural bait;&lt;br&gt;
stained water, chartreuse. LureColorCam turns that folklore into a&lt;br&gt;
ten-second habit. Point your camera at the water, tap once, and get&lt;br&gt;
lure color suggestions matched to what the water actually looks like&lt;br&gt;
right now.&lt;/p&gt;

&lt;p&gt;READ THE WATER&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The camera reads hue, saturation and brightness in a fixed spot&lt;/li&gt;
&lt;li&gt;Exposure locks with a tap, so glare doesn't fool the reading&lt;/li&gt;
&lt;li&gt;Clear, stained or muddy — plus bright or low light&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;GET A COLOR, NOT A LECTURE&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Three suggested lure colors, biggest first&lt;/li&gt;
&lt;li&gt;One plain-language line explaining why&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;KEEP A WATER LOG&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Save each reading with a place note and target fish&lt;/li&gt;
&lt;li&gt;Spot patterns: "this lake runs stained after rain"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;LEARN THE CHART&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The full water-by-light color matrix, readable offline&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Suggestions are rules of thumb built from common angling practice —&lt;br&gt;
a way to choose with intent, never a guarantee of catches.&lt;br&gt;
No account, no ads, no tracking. Everything stays on your phone.&lt;/p&gt;

&lt;h3&gt;
  
  
  Read the water in seconds
&lt;/h3&gt;

&lt;p&gt;Point your camera at the water, tap once, and get a reading of water clarity (clear, stained, or muddy) and light level. Exposure locks with a tap so glare doesn't fool the camera.&lt;/p&gt;

&lt;h3&gt;
  
  
  Three suggested lure colors, explained
&lt;/h3&gt;

&lt;p&gt;See the top three color picks ranked by likelihood. Each suggestion comes with one plain-language line explaining why—so you choose with intent, not guessing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Learn the full color chart
&lt;/h3&gt;

&lt;p&gt;Water clarity by light level: a 3×2 matrix showing the recommended colors for every combination. Tap each square to see the reasoning behind the pick.&lt;/p&gt;

&lt;h3&gt;
  
  
  Keep a water log
&lt;/h3&gt;

&lt;p&gt;Save each reading with a place note and target fish species. Spot patterns: 'this lake always runs stained after rain.' Review your history and get smarter about where to fish.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rules of thumb, never a promise
&lt;/h3&gt;

&lt;p&gt;Suggested colors are based on common angling experience—a framework for choosing intentionally. No online tracking, no account required. Everything stays on your phone.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why might the suggested colors not work?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The suggested colors are rules of thumb built from general angling experience, not a guarantee. Water conditions vary by season, region, and time of day. Fish behavior depends on many factors beyond lure color—feed times, forage species, and water temperature all matter. The app is a framework for choosing intentionally, not a promise of catches.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does the app send my camera data anywhere?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No. The app analyzes camera frames on your device only. The app does not store or send your camera footage, photos, location, or any readings to the developer or any third party. Everything stays on your phone.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I use this app without an internet connection?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes. LureColorCam works fully offline. The app does not require a network connection to read water color, show suggestions, or keep your log. It also does not use GPS, weather APIs, or any online services.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is all my data kept on my device only?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes. All readings, place notes, target fish records, and photos are stored only on your device using SwiftData. Nothing is synced to the cloud, backed up externally, or sent anywhere. Your data stays yours.&lt;/p&gt;




&lt;p&gt;&lt;a href="https://www.netflowers.co.jp/ios/lurecolorcam/" rel="noopener noreferrer"&gt;www.netflowers.co.jp&lt;/a&gt;&lt;/p&gt;

</description>
      <category>lure</category>
      <category>color</category>
      <category>fishing</category>
      <category>water</category>
    </item>
    <item>
      <title>Axle Load Calculator — Load position to axle weight</title>
      <dc:creator>Yoshiaki Hirokawa</dc:creator>
      <pubDate>Sun, 06 Sep 2026 01:41:52 +0000</pubDate>
      <link>https://dev.to/netflowers/axle-load-calculator-load-position-to-axle-weight-2o30</link>
      <guid>https://dev.to/netflowers/axle-load-calculator-load-position-to-axle-weight-2o30</guid>
      <description>&lt;p&gt;Overloaded on the rear axle? It works out how far the load has to move to bring both axles inside your limits.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Load position to axle weight&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Where you put the weight matters as much as how much you carry. Axle Load Calculator works out how a load sitting on your bed or trailer divides between the axles, so you can see an overload before the weighbridge does.&lt;/p&gt;

&lt;p&gt;SET THE VEHICLE UP ONCE&lt;br&gt;
Enter the wheelbase, and the weight each axle already carries when the vehicle is empty. Add the limits from your vehicle plate. That is the whole setup, and it is ready for every load after that.&lt;/p&gt;

&lt;p&gt;ADD THE LOAD BY WEIGHT AND POSITION&lt;br&gt;
Each item goes in as a weight and a distance. The moment you place it, you see how much of it lands on the front axle and how much on the rear, and every bar fills toward the limit you entered. Green while there is room, red once there is not.&lt;/p&gt;

&lt;p&gt;THE PART THAT SAVES THE TRIP&lt;br&gt;
When an axle goes over, the app does not stop at telling you so. It solves for how far the load has to move — forward or back — to bring every axle inside its limit, and shows the distance in centimetres or inches. One tap shifts the whole load by that amount so you can check the result. If the total is simply too heavy to fix by moving it, the app says that instead, and works out roughly how much has to come off.&lt;/p&gt;

&lt;p&gt;TONGUE AND HITCH LOADS&lt;br&gt;
A trailer is the same lever, measured from the coupling instead of the front axle. The app shows the share landing on the hitch both in weight and as a percentage of trailer weight. Loaded too far back, a trailer lifts the coupling and starts to sway. Too far forward, it presses down on the tow vehicle's rear axle. Pick your tow vehicle and you can see exactly what the trailer does to its axles.&lt;/p&gt;

&lt;p&gt;BUILT ON ARITHMETIC, NOT GUESSWORK&lt;br&gt;
An item over the front axle is carried entirely by the front axle. Over the rear axle, entirely by the rear. Anywhere between, it splits in proportion to distance — the same balance of moments that decides a see-saw, and the same one a weighbridge measures. There are no invented correction factors anywhere in this app.&lt;/p&gt;

&lt;p&gt;LIMITS ARE YOURS TO ENTER&lt;br&gt;
Axle limits are written into your vehicle plate and into the road rules where you drive, and they differ from country to country. This app will never tell you what your legal limit is. You enter it, and the app tells you whether the load fits.&lt;/p&gt;

&lt;p&gt;ALSO IN THE APP&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Kilograms and metres, or pounds and feet&lt;/li&gt;
&lt;li&gt;Several vehicles and trailers, saved side by side&lt;/li&gt;
&lt;li&gt;Turn any item off to test loading without it&lt;/li&gt;
&lt;li&gt;Combined centre of gravity marked on the side view&lt;/li&gt;
&lt;li&gt;Works entirely offline. No account, no tracking, no network access at all — everything you enter stays on your device&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A PLANNING TOOL, NOT A CERTIFICATE&lt;br&gt;
This app estimates from the numbers you type in. Real axle weights also depend on the true weight of each item, how the load settles, the road surface and the state of the suspension. It is a way to load thoughtfully and to know when a weighbridge trip is worth it — never a substitute for the certificate one gives you.&lt;/p&gt;

&lt;p&gt;One purchase. No subscription, no adverts, no in-app purchases.&lt;/p&gt;

&lt;h3&gt;
  
  
  Set the vehicle up once
&lt;/h3&gt;

&lt;p&gt;Enter the wheelbase, the empty axle weights and the limits from your vehicle plate. That setup is then ready for every load after it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Add items by weight and position
&lt;/h3&gt;

&lt;p&gt;Each item goes in as a weight and a distance. You immediately see how much lands on the front axle and how much on the rear, with every bar filling toward the limit you entered.&lt;/p&gt;

&lt;h3&gt;
  
  
  It tells you how far to move the load
&lt;/h3&gt;

&lt;p&gt;When an axle goes over, the app solves for the distance the load has to shift — forward or back — to bring every axle inside its limit. If the total is simply too heavy, it says so and works out roughly how much has to come off.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tongue and hitch loads
&lt;/h3&gt;

&lt;p&gt;For trailers, the share landing on the coupling is shown both in weight and as a percentage of trailer weight. Pick your tow vehicle to see what the trailer does to its axles.&lt;/p&gt;

&lt;h3&gt;
  
  
  Arithmetic, not invented factors
&lt;/h3&gt;

&lt;p&gt;An item over an axle is carried by that axle; anywhere between, it splits in proportion to distance — the same balance of moments a weighbridge measures. There are no correction factors anywhere in this app.&lt;/p&gt;

&lt;h3&gt;
  
  
  Works entirely offline
&lt;/h3&gt;

&lt;p&gt;Kilograms and metres or pounds and feet, several vehicles and trailers saved side by side, and no network access at all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does the app know my legal axle limits?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No, and it never will. Axle limits come from your vehicle plate and from the road rules where you drive, and they differ from country to country. You enter them, and the app tells you whether the load fits.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I use this instead of a weighbridge?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No. This is a planning tool that estimates from the numbers you type in. Real axle weights also depend on the true weight of each item, how the load settles, the road surface and the state of the suspension. Use it to load thoughtfully and to know when a weighbridge trip is worth it — never as a substitute for the certificate one gives you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How does it decide the split between axles?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;By the balance of moments. An item sitting over the front axle is carried entirely by the front axle, one over the rear entirely by the rear, and anywhere between it divides in proportion to distance — the same lever arithmetic that decides a see-saw.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does it work without a network connection?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes. The app works entirely offline. There is no account, no tracking and no network access at all.&lt;/p&gt;




&lt;p&gt;&lt;a href="https://www.netflowers.co.jp/ios/load-share/" rel="noopener noreferrer"&gt;www.netflowers.co.jp&lt;/a&gt;&lt;/p&gt;

</description>
      <category>axle</category>
      <category>load</category>
      <category>weight</category>
      <category>trailer</category>
    </item>
    <item>
      <title>FlockLedger: Egg Tracker — Daily Eggs &amp; Flock Records</title>
      <dc:creator>Yoshiaki Hirokawa</dc:creator>
      <pubDate>Sat, 05 Sep 2026 13:19:19 +0000</pubDate>
      <link>https://dev.to/netflowers/flockledger-egg-tracker-daily-eggs-flock-records-pln</link>
      <guid>https://dev.to/netflowers/flockledger-egg-tracker-daily-eggs-flock-records-pln</guid>
      <description>&lt;p&gt;Tap the big egg button as you collect. See today's count, this week's and month's totals, and a calendar that glows yellow on your best laying days.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Daily Eggs &amp;amp; Flock Records&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Count this morning's eggs before you're back inside. FlockLedger is&lt;br&gt;
a daily egg counter and flock record book for backyard chicken&lt;br&gt;
keepers — and nothing else.&lt;/p&gt;

&lt;p&gt;Tap the big egg button (or anywhere on the screen) to add each egg&lt;br&gt;
as you collect. Today's count fills the screen in huge digits, with&lt;br&gt;
a ring that shows how your hens are laying against your flock size,&lt;br&gt;
plus this week's and this month's totals at a glance.&lt;/p&gt;

&lt;p&gt;The calendar paints every laying day in egg-yolk yellow — two shades,&lt;br&gt;
so your best days stand out — and a tap on any date lets you fix a&lt;br&gt;
day you forgot to log. Charts show your week, month and year as&lt;br&gt;
simple bars, so you can spot the seasonal ebb and flow.&lt;/p&gt;

&lt;p&gt;Your hens get their own ledger: name, breed and hatch date, with age&lt;br&gt;
computed automatically. No feed costs, no breeding tables, no&lt;br&gt;
spreadsheets — just eggs and the hens who lay them.&lt;/p&gt;

&lt;p&gt;Everything stays on your device: no account, no ads, no internet&lt;br&gt;
required. One price, all features.&lt;/p&gt;

&lt;h3&gt;
  
  
  One-Tap Egg Counting
&lt;/h3&gt;

&lt;p&gt;Tap the big egg button — or anywhere on the screen — to add each egg as you collect. Today's count fills the screen in huge digits.&lt;/p&gt;

&lt;h3&gt;
  
  
  Laying Ring &amp;amp; Totals
&lt;/h3&gt;

&lt;p&gt;A ring shows how your hens are laying against your flock size, with this week's and this month's totals at a glance.&lt;/p&gt;

&lt;h3&gt;
  
  
  Egg-Yolk Calendar
&lt;/h3&gt;

&lt;p&gt;Every laying day is painted in egg-yolk yellow, in two shades so your best days stand out. Tap any date to fix a day you forgot to log.&lt;/p&gt;

&lt;h3&gt;
  
  
  Weekly, Monthly &amp;amp; Yearly Charts
&lt;/h3&gt;

&lt;p&gt;Simple bar charts show your week, month and year, so you can spot the seasonal ebb and flow of your flock's laying.&lt;/p&gt;

&lt;h3&gt;
  
  
  Your Own Flock Ledger
&lt;/h3&gt;

&lt;p&gt;Give each hen her own record — name, breed and hatch date — with age computed automatically. No feed costs, no breeding tables, just eggs and the hens who lay them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where is my egg-counting data stored?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Everything you log stays on your device. FlockLedger has no account and no server, so your records never leave your iPhone unless you back it up yourself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What happens to my records if I switch phones?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you use iCloud backup or Migration Assistant for your device, your FlockLedger data moves with it like any other app. There's no separate account to sign back into.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is FlockLedger a one-time purchase?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes. FlockLedger is paid once with no subscription. Unlimited hens, egg logs and history are included from the start.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I log an egg count for a day I forgot?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes. Tap any date on the calendar to add or correct that day's egg count, so your records stay accurate even if you miss a day.&lt;/p&gt;




&lt;p&gt;&lt;a href="https://www.netflowers.co.jp/ios/flockledger/" rel="noopener noreferrer"&gt;www.netflowers.co.jp&lt;/a&gt;&lt;/p&gt;

</description>
      <category>chicken</category>
      <category>egg</category>
      <category>tracker</category>
      <category>coop</category>
    </item>
    <item>
      <title>FishRuler: Fish Size Measure — AR tape measure for anglers</title>
      <dc:creator>Yoshiaki Hirokawa</dc:creator>
      <pubDate>Sat, 05 Sep 2026 13:18:47 +0000</pubDate>
      <link>https://dev.to/netflowers/fishruler-fish-size-measure-ar-tape-measure-for-anglers-4677</link>
      <guid>https://dev.to/netflowers/fishruler-fish-size-measure-ar-tape-measure-for-anglers-4677</guid>
      <description>&lt;p&gt;Point your iPhone at your catch, tap nose and tail, read the length in cm. New: save catches to your log and see your personal best per species. Uses LiDAR on Pro models.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AR tape measure for anglers&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You landed the fish. Now how long is it — and will you remember by next season?&lt;/p&gt;

&lt;p&gt;Reaching for a tape measure with wet hands, holding a thrashing fish flat, keeping the tape straight — it never works the way it should, and the fish is losing time out of the water while you fumble.&lt;/p&gt;

&lt;p&gt;FishRuler turns your iPhone into the measuring tool you actually have with you. Point the camera at your catch, tap the nose, tap the tail, and the length appears on screen in centimeters. Save it to your log with one tap, and it's never lost again.&lt;/p&gt;

&lt;p&gt;HOW IT WORKS&lt;br&gt;
• Tap two points — nose and tail — and read the distance between them&lt;br&gt;
• Missed the spot? Drag either marker to fine-tune without starting over&lt;br&gt;
• A line is drawn between the two points so you can see exactly what was measured&lt;br&gt;
• Save the catch with a photo, species, spot and date, or hit Reset and move on&lt;/p&gt;

&lt;p&gt;YOUR CATCH LOG&lt;br&gt;
• Every saved catch lives in a scrollable log — photo, species, length, spot and date, all in one card&lt;br&gt;
• FishRuler tracks your personal best per species automatically. Beat your record and the app tells you right away&lt;br&gt;
• Best-ever catches stand out in the log so your top fish are always easy to find&lt;br&gt;
• Turn any catch card into a single image, ready to share&lt;/p&gt;

&lt;p&gt;WHY ANGLERS LIKE IT&lt;br&gt;
• Nothing to unroll, nothing to hold flat, nothing to lose overboard&lt;br&gt;
• Fast enough to measure and release quickly&lt;br&gt;
• Works fully offline — no signal needed at the lake, river or jetty&lt;br&gt;
• No account, no sign-up. Your log stays on your device&lt;br&gt;
• Useful beyond fish, too: measure gear, coolers, boat fittings or anything else in front of you&lt;/p&gt;

&lt;p&gt;BEFORE YOU BUY — DEVICE REQUIREMENT&lt;br&gt;
FishRuler measures using the LiDAR scanner, so it needs an iPhone that has one — iPhone 12 Pro / Pro Max or later Pro models. On a device without a LiDAR scanner the app will tell you it cannot measure. It also requires iOS 17 or later, so please check both your model and iOS version before purchasing.&lt;/p&gt;

&lt;p&gt;ABOUT ACCURACY&lt;br&gt;
Readings are real-world estimates produced by the device's depth sensor. Good light, a steady hand and a fish lying flat give the best results. Treat the number as a practical field measurement — for tournament rules, legal size limits or official record submissions, always confirm with the measuring device that the rules require.&lt;/p&gt;

&lt;p&gt;ABOUT THE SIZE REFERENCE NOTE&lt;br&gt;
For a small number of species and regions, FishRuler can show a reference note if a catch falls outside the usual take-home size range. This is for your information only — it is not a full or authoritative list of regulations, and it does not replace checking current rules with your local fisheries authority before you keep a fish.&lt;/p&gt;

&lt;p&gt;One purchase. No subscription, no ads.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tap the nose, tap the tail
&lt;/h3&gt;

&lt;p&gt;Aim the camera at your fish, tap once at the nose and once at the tail. The distance between the two points shows up on screen in centimeters. No rewinding a tape, no fighting to keep it straight.&lt;/p&gt;

&lt;h3&gt;
  
  
  Off by a bit? Just drag it
&lt;/h3&gt;

&lt;p&gt;If a tap lands slightly off, drag the marker to nudge it into place — no need to start over. A line connects the two points, so you can see exactly what you measured at a glance.&lt;/p&gt;

&lt;h3&gt;
  
  
  Measure and release fast
&lt;/h3&gt;

&lt;p&gt;Hit Reset and the measurement clears, ready for the next fish. Since you're not wrestling the fish flat against a board, it spends less time out of the water.&lt;/p&gt;

&lt;h3&gt;
  
  
  Keep a logbook of every catch
&lt;/h3&gt;

&lt;p&gt;Save the species, length, a photo, where you caught it, and when — all in a logbook that lives on your phone. Look back on the season, or just one great day, whenever you want.&lt;/p&gt;

&lt;h3&gt;
  
  
  See your personal bests
&lt;/h3&gt;

&lt;p&gt;FishRuler checks each new catch against your logbook and flags it when it's your biggest yet for that species. A quiet way to watch your personal records add up over time.&lt;/p&gt;

&lt;h3&gt;
  
  
  Turn a catch into a shareable card
&lt;/h3&gt;

&lt;p&gt;Export any logged catch as a clean image card with the photo, species, and length, then share it however you like using your phone's own share sheet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Which iPhones does it work on?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;FishRuler measures with the LiDAR scanner, so it needs an iPhone that has one. That means the Pro models: iPhone 12 Pro / Pro Max and later. On a device without LiDAR you'll see "This device does not support LiDAR" and measuring won't be available. Please check your model before buying.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do I need to update iOS to use it?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes — FishRuler requires iOS 17 or later, since the logbook is built on Apple's newer on-device storage framework. If your iPhone can't update to iOS 17, the app won't install. Most LiDAR-capable iPhones can run iOS 17 without issue, but it's worth checking your software version before buying.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I use it?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Point the camera at the fish, tap once at the nose and once at the tail. A line is drawn between the two points and the distance appears in centimeters. If a tap is off, drag the marker to fine-tune it, and press Reset to clear the measurement for the next fish.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Are my measurements saved? Can I look back at old catches?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes. When you save a measurement, it goes into your logbook along with a photo, species, location name, and date, so you can browse past catches any time. Nothing is saved automatically — you choose which measurements to log.&lt;/p&gt;




&lt;p&gt;&lt;a href="https://www.netflowers.co.jp/ios/fishruler/" rel="noopener noreferrer"&gt;www.netflowers.co.jp&lt;/a&gt;&lt;/p&gt;

</description>
      <category>fishing</category>
      <category>angler</category>
      <category>length</category>
      <category>ruler</category>
    </item>
    <item>
      <title>StoreKit 2 subscriptions + a screenshot mode that bypasses purchases</title>
      <dc:creator>Yoshiaki Hirokawa</dc:creator>
      <pubDate>Sat, 27 Jun 2026 09:00:46 +0000</pubDate>
      <link>https://dev.to/netflowers/storekit-2-subscriptions-a-screenshot-mode-that-bypasses-purchases-105</link>
      <guid>https://dev.to/netflowers/storekit-2-subscriptions-a-screenshot-mode-that-bypasses-purchases-105</guid>
      <description>&lt;p&gt;Two things every paid iOS app needs but nobody enjoys building:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A correct &lt;strong&gt;subscription manager&lt;/strong&gt; (purchase, restore, entitlement checks, transaction updates).&lt;/li&gt;
&lt;li&gt;A way to &lt;strong&gt;capture App Store screenshots&lt;/strong&gt; of the paid screens — without having a live sandbox purchase every time.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here's how &lt;strong&gt;FishGo&lt;/strong&gt; does both, with the second piece deliberately punching a hole through the first.&lt;/p&gt;

&lt;h2&gt;
  
  
  A compact StoreKit 2 manager
&lt;/h2&gt;

&lt;p&gt;StoreKit 2 is &lt;code&gt;async/await&lt;/code&gt;-native, so the whole manager fits in one observable class. We use &lt;code&gt;@Observable&lt;/code&gt; (iOS 17+) so SwiftUI views react to &lt;code&gt;isPro&lt;/code&gt; changes:&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;@Observable&lt;/span&gt;
&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="kt"&gt;StoreManager&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;shared&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;StoreManager&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="kd"&gt;private(set)&lt;/span&gt; &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;isPro&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
    &lt;span class="kd"&gt;private(set)&lt;/span&gt; &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;products&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;Product&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="kd"&gt;private(set)&lt;/span&gt; &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;purchaseError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;productIDs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"pro_monthly"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"pro_yearly"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;transactionListener&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Task&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;Void&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;Never&lt;/span&gt;&lt;span class="o"&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;h3&gt;
  
  
  Loading products
&lt;/h3&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;loadProducts&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="k"&gt;do&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;storeProducts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="kt"&gt;Product&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;products&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;productIDs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;products&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;storeProducts&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sorted&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;$0&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;price&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="n"&gt;purchaseError&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&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;h3&gt;
  
  
  Purchasing, with verification
&lt;/h3&gt;

&lt;p&gt;The important part of StoreKit 2 is that every result is a &lt;code&gt;VerificationResult&lt;/code&gt; you must check:&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;purchase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="nv"&gt;product&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Product&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="n"&gt;purchaseError&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;nil&lt;/span&gt;
    &lt;span class="k"&gt;do&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;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;purchase&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;switch&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;success&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;verification&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;transaction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="nf"&gt;checkVerified&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;verification&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;transaction&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;finish&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;updatePurchaseStatus&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;userCancelled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;break&lt;/span&gt;
        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;pending&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;purchaseError&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"購入処理が保留中です"&lt;/span&gt;
        &lt;span class="kd"&gt;@unknown&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;break&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="n"&gt;purchaseError&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"購入に失敗しました: &lt;/span&gt;&lt;span class="se"&gt;\(&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;localizedDescription&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;nonisolated&lt;/span&gt; &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="n"&gt;checkVerified&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="nv"&gt;result&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;VerificationResult&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;T&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;throws&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;T&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;switch&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;unverified&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="kt"&gt;StoreError&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;failedVerification&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;verified&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;safe&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;safe&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;h3&gt;
  
  
  Listening for transactions out-of-band
&lt;/h3&gt;

&lt;p&gt;Purchases can arrive outside your purchase flow (Ask to Buy approvals, renewals, another device). A long-lived listener keeps &lt;code&gt;isPro&lt;/code&gt; correct:&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;listenForTransactions&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;Task&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;Void&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;Never&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;Task&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;detached&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;weak&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="kt"&gt;Transaction&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;updates&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;transaction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;checkVerified&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&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="n"&gt;transaction&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;finish&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;updatePurchaseStatus&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the source of truth for entitlement is &lt;code&gt;Transaction.currentEntitlements&lt;/code&gt;:&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;updatePurchaseStatus&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="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;hasEntitlement&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="kt"&gt;Transaction&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;currentEntitlements&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;transaction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nf"&gt;checkVerified&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
           &lt;span class="n"&gt;productIDs&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;transaction&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;productID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;hasEntitlement&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
            &lt;span class="k"&gt;break&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;isPro&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hasEntitlement&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The hole: a screenshot "shot mode"
&lt;/h2&gt;

&lt;p&gt;Now the App Store screenshot problem. The paywall and the Pro-only screens look best when &lt;code&gt;isPro == true&lt;/code&gt;, but you don't want to depend on a sandbox purchase succeeding during an automated screenshot run.&lt;/p&gt;

&lt;p&gt;So &lt;code&gt;StoreManager&lt;/code&gt;'s initializer short-circuits when a launch flag is set:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Shot mode: pin Pro state without touching StoreKit&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="kt"&gt;ShotMode&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isEnabled&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="kt"&gt;ShotMode&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isPro&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;isPro&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;transactionListener&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;listenForTransactions&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="kt"&gt;Task&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;updatePurchaseStatus&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;&lt;code&gt;ShotMode&lt;/code&gt; is just a thin reader over launch arguments / &lt;code&gt;UserDefaults&lt;/code&gt;:&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;ShotMode&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;isEnabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Bool&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="kt"&gt;UserDefaults&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;standard&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;forKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"SHOT_MODE"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;isPro&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Bool&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="kt"&gt;UserDefaults&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;standard&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;forKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"SHOT_PRO"&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;Run the UI test with &lt;code&gt;-SHOT_MODE 1 -SHOT_PRO 1&lt;/code&gt; and the app boots straight into a deterministic Pro state — no network, no store, no flaky purchase. Combined with mocked forecast data, every screenshot is identical run-to-run.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pitfalls
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Always &lt;code&gt;finish()&lt;/code&gt; a verified transaction.&lt;/strong&gt; Unfinished transactions get replayed forever via &lt;code&gt;Transaction.updates&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;@unknown default&lt;/code&gt; is mandatory&lt;/strong&gt; on the purchase-result switch — StoreKit can add cases.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep the shot-mode bypass narrow.&lt;/strong&gt; It only forces &lt;code&gt;isPro&lt;/code&gt;; it never fakes a real &lt;code&gt;Transaction&lt;/code&gt;. The real verification path stays untouched for actual users.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Guard the bypass behind a launch argument, not a build flag you might ship.&lt;/strong&gt; It only activates when explicitly passed at launch.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;StoreKit 2 lets you write a full subscription manager in ~100 lines with &lt;code&gt;async/await&lt;/code&gt; + &lt;code&gt;@Observable&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Check &lt;strong&gt;&lt;code&gt;VerificationResult&lt;/code&gt;&lt;/strong&gt; everywhere; trust nothing unverified.&lt;/li&gt;
&lt;li&gt;Drive entitlement from &lt;strong&gt;&lt;code&gt;Transaction.currentEntitlements&lt;/code&gt;&lt;/strong&gt; and keep a &lt;strong&gt;&lt;code&gt;Transaction.updates&lt;/code&gt;&lt;/strong&gt; listener alive.&lt;/li&gt;
&lt;li&gt;A tiny &lt;strong&gt;launch-flag bypass&lt;/strong&gt; makes paid-screen screenshots deterministic — as long as it stays scoped to UI state, not real receipts.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;FishGo is on the App Store: &lt;a href="https://apps.apple.com/app/id6774428559" rel="noopener noreferrer"&gt;https://apps.apple.com/app/id6774428559&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ios</category>
      <category>swift</category>
      <category>storekit</category>
      <category>swiftui</category>
    </item>
    <item>
      <title>An actor + 1-hour cache: calling weather APIs safely from Swift Concurrency</title>
      <dc:creator>Yoshiaki Hirokawa</dc:creator>
      <pubDate>Sat, 27 Jun 2026 08:32:41 +0000</pubDate>
      <link>https://dev.to/netflowers/an-actor-1-hour-cache-calling-weather-apis-safely-from-swift-concurrency-2ad1</link>
      <guid>https://dev.to/netflowers/an-actor-1-hour-cache-calling-weather-apis-safely-from-swift-concurrency-2ad1</guid>
      <description>&lt;p&gt;When an app calls a remote API from many places — a view appears, the user taps refresh, a background reload kicks in — you face two problems at once:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Concurrency safety&lt;/strong&gt;: multiple tasks touching the same cache can race.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Redundant calls&lt;/strong&gt;: the same coordinate gets fetched over and over.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In &lt;strong&gt;FishGo&lt;/strong&gt; (a fishing-conditions app), the weather layer hits &lt;em&gt;two&lt;/em&gt; Open-Meteo endpoints per location — a regular forecast and a marine forecast. We solved both problems with one tool: a Swift &lt;strong&gt;&lt;code&gt;actor&lt;/code&gt;&lt;/strong&gt; holding a cache.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why an actor
&lt;/h2&gt;

&lt;p&gt;An &lt;code&gt;actor&lt;/code&gt; serializes access to its mutable state. Only one task touches the cache at a time, so we get data-race safety &lt;strong&gt;for free&lt;/strong&gt; — no locks, no queues. Just mark the type &lt;code&gt;actor&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

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

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;WeatherData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;timestamp&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="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[:]&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;cacheExpiry&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;TimeInterval&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt;  &lt;span class="c1"&gt;// 1 hour&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;cache&lt;/code&gt; is isolated to the actor. Any code calling &lt;code&gt;fetchWeather&lt;/code&gt; from outside has to &lt;code&gt;await&lt;/code&gt;, and the runtime guarantees serialized access.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cache key: coordinate + day
&lt;/h2&gt;

&lt;p&gt;Weather changes by location and by date, so the cache key combines both:&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;fetchWeather&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="nv"&gt;coordinate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;CLLocationCoordinate2D&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="kt"&gt;Date&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;throws&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;WeatherData&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;dateKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;Self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dateKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;date&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;cacheKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\(&lt;/span&gt;&lt;span class="n"&gt;coordinate&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;latitude&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="se"&gt;\(&lt;/span&gt;&lt;span class="n"&gt;coordinate&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;longitude&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="s"&gt;_&lt;/span&gt;&lt;span class="se"&gt;\(&lt;/span&gt;&lt;span class="n"&gt;dateKey&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;cached&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;cacheKey&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="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timeIntervalSince&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cached&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;timestamp&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;cacheExpiry&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;cached&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="c1"&gt;// ... fetch, then store&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;dateKey&lt;/code&gt; is a &lt;code&gt;yyyy-MM-dd&lt;/code&gt; string in the Tokyo time zone, so "today's forecast for this spot" maps to one stable key regardless of the exact &lt;code&gt;Date&lt;/code&gt; passed in:&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;static&lt;/span&gt; &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;dateKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="nv"&gt;date&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="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;String&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;f&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;DateFormatter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dateFormat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"yyyy-MM-dd"&lt;/span&gt;
    &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;timeZone&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;TimeZone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;identifier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Asia/Tokyo"&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;f&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&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="n"&gt;date&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;If a fresh entry exists (younger than an hour), we return it immediately and skip the network entirely.&lt;/p&gt;

&lt;h2&gt;
  
  
  Combining two endpoints
&lt;/h2&gt;

&lt;p&gt;On a cache miss, we fetch the forecast and the marine data, plus compute astronomy locally, and merge everything into one &lt;code&gt;WeatherData&lt;/code&gt;:&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;forecast&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetchForecastData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;coordinate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;coordinate&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;marine&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetchMarineForecast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;coordinate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;coordinate&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;astronomyData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;AstronomyCalculator&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;calculate&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;date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;coordinate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;coordinate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open-Meteo returns &lt;em&gt;hourly arrays&lt;/em&gt;, so we pick the index for noon as the representative daytime value:&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;targetHourIndex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="nv"&gt;date&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;times&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;String&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;Int&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;formatter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;DateFormatter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;formatter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dateFormat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"yyyy-MM-dd"&lt;/span&gt;
    &lt;span class="n"&gt;formatter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;timeZone&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;TimeZone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;identifier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Asia/Tokyo"&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;dateStr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;formatter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&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="n"&gt;date&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;target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\(&lt;/span&gt;&lt;span class="n"&gt;dateStr&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="s"&gt;T12:00"&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;index&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;times&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;firstIndex&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;target&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;index&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;index&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;times&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;firstIndex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;$0&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hasPrefix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dateStr&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;index&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  A graceful fallback for the marine API
&lt;/h2&gt;

&lt;p&gt;The marine endpoint is the flakier of the two (not every coordinate has marine coverage). Instead of letting a marine failure sink the whole request, we degrade gracefully:&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;fetchMarineForecast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;coordinate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;CLLocationCoordinate2D&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;throws&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;MarineForecastData&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="kt"&gt;URLSession&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shared&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;data&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="n"&gt;url&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;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="kt"&gt;JSONDecoder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;HourlyMarineResponse&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="k"&gt;self&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="n"&gt;data&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;MarineForecastData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nv"&gt;waveHeights&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hourly&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;waveHeight&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nv"&gt;seaTemperatures&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hourly&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;seaSurfaceTemperature&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="k"&gt;return&lt;/span&gt; &lt;span class="kt"&gt;MarineForecastData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;waveHeights&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="nv"&gt;seaTemperatures&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt;  &lt;span class="c1"&gt;// empty, not fatal&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;Downstream, missing wave/temperature data is handled with sensible defaults via &lt;code&gt;indices.contains&lt;/code&gt;, so the app still produces a usable forecast.&lt;/p&gt;

&lt;p&gt;Finally, store the merged result and return:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;cacheKey&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;data&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="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Pitfalls
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Don't make the actor's methods &lt;code&gt;nonisolated&lt;/code&gt; carelessly&lt;/strong&gt; — that throws away the isolation that's protecting your cache.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Time zones bite.&lt;/strong&gt; Keying by raw &lt;code&gt;Date&lt;/code&gt; would scatter "the same day" across many keys. Normalize to a &lt;code&gt;yyyy-MM-dd&lt;/code&gt; string in a fixed zone.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hourly arrays need bounds checks.&lt;/strong&gt; APIs can return shorter arrays than you expect; we guard every index access with &lt;code&gt;indices.contains&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Decide your failure granularity.&lt;/strong&gt; A secondary data source (marine) failing shouldn't kill the primary result — catch and degrade.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;An &lt;strong&gt;&lt;code&gt;actor&lt;/code&gt;&lt;/strong&gt; gives you a thread-safe cache with zero locking ceremony.&lt;/li&gt;
&lt;li&gt;Key the cache by &lt;strong&gt;what actually varies&lt;/strong&gt; (coordinate + normalized day), not by raw objects.&lt;/li&gt;
&lt;li&gt;Merge multiple endpoints behind one method so callers see a single &lt;code&gt;WeatherData&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Degrade, don't crash&lt;/strong&gt;, when a secondary source fails.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;FishGo is on the App Store: &lt;a href="https://apps.apple.com/app/id6774428559" rel="noopener noreferrer"&gt;https://apps.apple.com/app/id6774428559&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ios</category>
      <category>swift</category>
      <category>swiftui</category>
      <category>async</category>
    </item>
  </channel>
</rss>
