<?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: Rene Larsen</title>
    <description>The latest articles on DEV Community by Rene Larsen (@renelarsen).</description>
    <link>https://dev.to/renelarsen</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%2F4066154%2F8e756800-5a20-4194-adda-814e1b9d83a4.png</url>
      <title>DEV Community: Rene Larsen</title>
      <link>https://dev.to/renelarsen</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/renelarsen"/>
    <language>en</language>
    <item>
      <title>Build map guidance that follows the user without blocking pinch-to-zoom</title>
      <dc:creator>Rene Larsen</dc:creator>
      <pubDate>Sat, 08 Aug 2026 18:23:17 +0000</pubDate>
      <link>https://dev.to/renelarsen/build-map-guidance-that-follows-the-user-without-blocking-pinch-to-zoom-5h2b</link>
      <guid>https://dev.to/renelarsen/build-map-guidance-that-follows-the-user-without-blocking-pinch-to-zoom-5h2b</guid>
      <description>&lt;p&gt;A navigation map should help the user move through the world, not fight every gesture they make.&lt;/p&gt;

&lt;p&gt;I recently hit a deceptively simple bug while building field guidance in a React Native / Expo app: the route rendered correctly and the camera followed the current position, but users could not meaningfully zoom or pan while walking. They could pinch the map, but the next location update snapped the camera back to a fixed zoom.&lt;/p&gt;

&lt;p&gt;The map looked active. The experience felt broken.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cause: two camera owners
&lt;/h2&gt;

&lt;p&gt;The implementation combined two useful features:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;followsUserLocation={true} on the native map.&lt;/li&gt;
&lt;li&gt;animateCamera(...) after every location update, using a fixed walking zoom and pitch.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each feature was reasonable on its own. Together, they gave the camera two automatic owners and the user none.&lt;/p&gt;

&lt;p&gt;A pinch gesture changed the zoom for a fraction of a second. Then a GPS update arrived and our effect applied the navigation camera again. On iOS, native user-follow behavior added another layer of camera control.&lt;/p&gt;

&lt;h2&gt;
  
  
  A better model: follow mode and explore mode
&lt;/h2&gt;

&lt;p&gt;The fix was not to stop navigation. Route progress, distance, bearing, breadcrumb recording and off-route detection should all continue regardless of what the user does with the map.&lt;/p&gt;

&lt;p&gt;Only the camera behavior should change.&lt;/p&gt;

&lt;p&gt;We now keep a small piece of local UI state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;cameraFollowing&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setCameraFollowing&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;navigationActive&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;navigationActive&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;cameraFollowing&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;bearing&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nx"&gt;mapRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;animateCamera&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nf"&gt;walkingCamera&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;currentCoordinate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bearing&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;480&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="nx"&gt;currentCoordinate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bearing&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;navigationActive&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cameraFollowing&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The native follow prop uses the same state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;MapView&lt;/span&gt;
  &lt;span class="na"&gt;showsUserLocation&lt;/span&gt;
  &lt;span class="na"&gt;followsUserLocation&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;navigationActive&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;cameraFollowing&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;onTouchStart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;navigationActive&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;setCameraFollowing&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As soon as the user touches the map, the camera enters explore mode. Pinch, pan and rotation work normally. The active route and navigation calculations continue in the background.&lt;/p&gt;

&lt;p&gt;A large location button restores follow mode:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;recenterAndFollow&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;setCameraFollowing&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;mapRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;animateCamera&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nf"&gt;walkingCamera&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;currentCoordinate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bearing&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;420&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This creates a familiar contract:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;start guidance: follow automatically;&lt;/li&gt;
&lt;li&gt;touch the map: inspect freely;&lt;/li&gt;
&lt;li&gt;tap the location arrow: resume following;&lt;/li&gt;
&lt;li&gt;stop guidance: return to the ordinary overview map.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why onTouchStart?
&lt;/h2&gt;

&lt;p&gt;react-native-maps exposes gesture details on region-change events, but isGesture is not equally available across providers. Relying only on that detail can produce different behavior between Google Maps and Apple Maps.&lt;/p&gt;

&lt;p&gt;For this field use case, a touch is a good expression of intent: the user wants control of the map. The action is reversible with one obvious button.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep navigation state separate from presentation state
&lt;/h2&gt;

&lt;p&gt;The most important architectural lesson is that camera state is not navigation state.&lt;/p&gt;

&lt;p&gt;These should continue while the camera is detached:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;current-position updates;&lt;/li&gt;
&lt;li&gt;remaining route distance;&lt;/li&gt;
&lt;li&gt;bearing and direction;&lt;/li&gt;
&lt;li&gt;off-route detection;&lt;/li&gt;
&lt;li&gt;route recalculation;&lt;/li&gt;
&lt;li&gt;local breadcrumb recording.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Only camera animations are suspended. That separation prevents a UI gesture from accidentally stopping the trip or discarding route progress.&lt;/p&gt;

&lt;h2&gt;
  
  
  Outdoor UX details that matter
&lt;/h2&gt;

&lt;p&gt;Field navigation is used with cold fingers, sunlight, rain and intermittent attention. A few details made the interaction clearer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a 52-point recenter button with a generous hit area;&lt;/li&gt;
&lt;li&gt;high-contrast route colors;&lt;/li&gt;
&lt;li&gt;route status outside the map, so it is still readable while zooming;&lt;/li&gt;
&lt;li&gt;no hidden gesture required to resume following;&lt;/li&gt;
&lt;li&gt;tests that assert both map surfaces use the same follow/explore contract.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We applied this pattern to both live trip guidance and navigation back to a privately saved place. Consistency matters: users should not have to learn two camera behaviors inside the same app.&lt;/p&gt;

&lt;h2&gt;
  
  
  The product context
&lt;/h2&gt;

&lt;p&gt;I found this issue while developing &lt;a href="https://mycoverse.app/en/" rel="noopener noreferrer"&gt;Mycoverse&lt;/a&gt;, an iPhone app for planning mushroom trips with weather, habitat and seasonal signals, plus a private field journal. Exact saved places are private by default. The app does not identify mushrooms, guarantee a find or provide food-safety advice.&lt;/p&gt;

&lt;p&gt;The bug came from real field feedback, which is exactly the kind of feedback a simulator rarely produces. If your app guides people outdoors, test it while walking, locking the phone, putting it in a pocket, resuming it and trying to inspect the map mid-route. That is where the real interface begins.&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>ux</category>
      <category>mobile</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>What I learned building a privacy-first mushroom trip planner with open data</title>
      <dc:creator>Rene Larsen</dc:creator>
      <pubDate>Thu, 06 Aug 2026 15:42:16 +0000</pubDate>
      <link>https://dev.to/renelarsen/what-i-learned-building-a-privacy-first-mushroom-trip-planner-with-open-data-6if</link>
      <guid>https://dev.to/renelarsen/what-i-learned-building-a-privacy-first-mushroom-trip-planner-with-open-data-6if</guid>
      <description>&lt;p&gt;Most mushroom apps focus on the moment after someone has found a specimen. I wanted to build for the harder part before that moment: planning a useful field trip without turning private mushroom locations into a public heat map.&lt;/p&gt;

&lt;p&gt;The result is &lt;strong&gt;Mycoverse&lt;/strong&gt;, an Expo/React Native iPhone app that combines weather, habitat, terrain, season and a private field journal. Building it exposed a few product and engineering lessons that apply to many location-aware apps.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. A model score is not the product
&lt;/h2&gt;

&lt;p&gt;It is easy to calculate a number from rain, temperature, soil signals and forest data. It is much harder to explain what that number actually means.&lt;/p&gt;

&lt;p&gt;An early version showed scores such as 99/100. Testers correctly asked: &lt;em&gt;99 for what?&lt;/em&gt; The precision implied certainty that the underlying data did not support.&lt;/p&gt;

&lt;p&gt;The current UI leads with qualitative conditions and keeps technical scores in the details. It also exposes missing signals instead of silently treating them as zero. This makes the app less flashy, but much more honest.&lt;/p&gt;

&lt;p&gt;The principle I now use is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A model should help a person make a decision, not perform confidence theater.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  2. Exact locations need a different privacy model
&lt;/h2&gt;

&lt;p&gt;A mushroom patch is not an ordinary social check-in. A productive place can matter for years, and publishing precise coordinates can damage both trust and the place itself.&lt;/p&gt;

&lt;p&gt;Mycoverse therefore stores findings and exact positions as private by default. Community activity never requires exposing the coordinate behind a photo. Sharing must be deliberate, and public discovery views should not accidentally reveal location metadata.&lt;/p&gt;

&lt;p&gt;That decision affects architecture as much as copy. Location data, photos, sharing state and moderation need separate boundaries so a future social feature cannot casually inherit access to private coordinates.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Graceful degradation matters outdoors
&lt;/h2&gt;

&lt;p&gt;Field apps run where connectivity is unreliable. A successful request on a desk is not enough.&lt;/p&gt;

&lt;p&gt;The app now supports saved field briefs, local trip state and private finding data that can survive poor connectivity. Weather and habitat information shows freshness and data quality, rather than pretending a stale response is live.&lt;/p&gt;

&lt;p&gt;A useful offline state is not just a loading spinner with nicer wording. It must preserve the next action the user needs in the field.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Map routing is a safety feature
&lt;/h2&gt;

&lt;p&gt;A generic walking route can be technically valid and still be a bad field route. During physical testing, a route crossed private gardens because the underlying map data described a path.&lt;/p&gt;

&lt;p&gt;We added stricter routing logic, public-access checks and clear fallback behavior. The app distinguishes between a calculated trail route and a direct compass bearing, and it explains which one is being shown.&lt;/p&gt;

&lt;p&gt;This is still an imperfect global-data problem, but the product should never disguise that uncertainty.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Global taxonomy and local language are separate problems
&lt;/h2&gt;

&lt;p&gt;Scientific names provide a stable backbone, but foragers search in their own language. Mycoverse uses global taxonomy for classification while prioritizing regional common names where they are available.&lt;/p&gt;

&lt;p&gt;The catalogue can show licensed images, synonyms and sources, but it deliberately avoids presenting a search result or manual observation key as a confirmed identification. Planning a trip and approving food safety are different jobs.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Physical field testing beats a perfect simulator
&lt;/h2&gt;

&lt;p&gt;Some of the most valuable feedback has been simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the trip disappeared when the phone was locked&lt;/li&gt;
&lt;li&gt;the route existed, but the map did not behave like navigation&lt;/li&gt;
&lt;li&gt;a saved finding needed a direct link back to the map&lt;/li&gt;
&lt;li&gt;a marker needed the mushroom name or illustration, not just a pin&lt;/li&gt;
&lt;li&gt;map logic that worked in Norway had to degrade correctly in Poland, Sweden and the US&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these issues looked dramatic in unit tests. Together they determined whether the product felt trustworthy outdoors.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Mycoverse does now
&lt;/h2&gt;

&lt;p&gt;The current iPhone app includes seven-day trip planning, qualitative SoppRadar conditions, map-based candidate areas with several relevant species, seasonal watchlists, private findings with photos, offline field briefs, routes back to saved places and a global field handbook.&lt;/p&gt;

&lt;p&gt;It is a planning and documentation tool. It does not guarantee a find, identify a mushroom with certainty or approve anything as safe to eat.&lt;/p&gt;

&lt;p&gt;If you build location-aware or outdoor software, I would love to hear how you test unreliable data, offline state and privacy in real-world use.&lt;/p&gt;

&lt;p&gt;Mycoverse on the App Store: &lt;a href="https://apps.apple.com/app/mycoverse/id6793912021" rel="noopener noreferrer"&gt;https://apps.apple.com/app/mycoverse/id6793912021&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ios</category>
      <category>reactnative</category>
      <category>mobile</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
