<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>I know one thing; that I know nothing</title>
    <description>Haozes</description>
    <link>http://blog.yaoyaojumprope.com/</link>
    <atom:link href="http://blog.yaoyaojumprope.com/feed.xml" rel="self" type="application/rss+xml" />
    <pubDate>Sat, 25 Jul 2026 20:50:31 +0800</pubDate>
    <lastBuildDate>Sat, 25 Jul 2026 20:50:31 +0800</lastBuildDate>
    <generator>Jekyll v3.10.0</generator>
    
      <item>
        <title>Tooboo Turns Apple Watch Ultra Into a Standalone Offline Outdoor Navigator</title>
        <description>&lt;h1 id=&quot;tooboo-turns-apple-watch-ultra-into-a-standalone-offline-outdoor-navigator&quot;&gt;Tooboo Turns Apple Watch Ultra Into a Standalone Offline Outdoor Navigator&lt;/h1&gt;

&lt;h2 id=&quot;can-apple-watch-ultra-handle-long-distance-outdoor-navigation&quot;&gt;Can Apple Watch Ultra Handle Long-Distance Outdoor Navigation?&lt;/h2&gt;

&lt;p&gt;Apple Watch Ultra has capable GPS and workout hardware, but long-distance
outdoor navigation still presents two challenges: operating independently
of an iPhone and preserving enough battery for an extended activity.&lt;/p&gt;

&lt;p&gt;Tooboo addresses both. Developed by JiaHao, it provides offline navigation
for hiking, trail running and cycling, with routes prepared on iPhone and
followed directly from Apple Watch.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prepare the route on iPhone. Navigate from the wrist.&lt;/strong&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;what-is-tooboo&quot;&gt;What Is Tooboo?&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt;&lt;br /&gt;
Tooboo is an offline outdoor navigation app that lets users download
maps and routes before an activity, then navigate directly from Apple
Watch without requiring continuous iPhone connectivity.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/post/260723/tooboo1.jpg&quot; alt=&quot;tooboo-1&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Tooboo supports:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Hiking&lt;/li&gt;
  &lt;li&gt;Running&lt;/li&gt;
  &lt;li&gt;Cycling&lt;/li&gt;
  &lt;li&gt;Walking&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Key features include:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Offline vector maps&lt;/li&gt;
  &lt;li&gt;GPX, KML and KMZ route import&lt;/li&gt;
  &lt;li&gt;Route following&lt;/li&gt;
  &lt;li&gt;Off-route alerts&lt;/li&gt;
  &lt;li&gt;Turn guidance&lt;/li&gt;
  &lt;li&gt;Elevation information&lt;/li&gt;
  &lt;li&gt;Waypoints&lt;/li&gt;
  &lt;li&gt;Backtracking&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src=&quot;/img/post/260723/tooboo-outline.jpeg&quot; alt=&quot;tooboo-outline&quot; /&gt;
Unlike many navigation apps that treat Apple Watch as only a companion
display, Tooboo is designed around a watch-first outdoor experience.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;standalone-apple-watch-navigation-without-an-iphone&quot;&gt;Standalone Apple Watch Navigation Without an iPhone&lt;/h2&gt;

&lt;p&gt;&lt;img src=&quot;/img/post/260723/tooboo4.jpg&quot; alt=&quot;tooboo-outline&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Before an activity, users can prepare everything on iPhone or iPad:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;import GPX routes;&lt;/li&gt;
  &lt;li&gt;download offline maps;&lt;/li&gt;
  &lt;li&gt;review route details.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;During the activity, Apple Watch provides:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;offline map display;&lt;/li&gt;
  &lt;li&gt;route following;&lt;/li&gt;
  &lt;li&gt;GPS track recording;&lt;/li&gt;
  &lt;li&gt;off-route notifications;&lt;/li&gt;
  &lt;li&gt;voice and haptic guidance.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For hikers who keep their phone inside a backpack, the watch can remain
the primary navigation device.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;expedition-mode-extending-apple-watch-ultra-battery-life&quot;&gt;Expedition Mode: Extending Apple Watch Ultra Battery Life&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt;&lt;br /&gt;
Expedition Mode reduces non-essential processing while keeping the
navigation features most important for long outdoor adventures.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/post/260723/tooboo3.jpg&quot; alt=&quot;tooboo&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Battery life remains one of the biggest limitations when using any
smartwatch for long-distance navigation.&lt;/p&gt;

&lt;p&gt;Tooboo Expedition Mode was designed specifically for extended outdoor
activities.&lt;/p&gt;

&lt;p&gt;It reduces:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;heart rate sensor sampling;&lt;/li&gt;
  &lt;li&gt;background processing that is less important for navigation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;While keeping:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;GPS track recording;&lt;/li&gt;
  &lt;li&gt;offline maps;&lt;/li&gt;
  &lt;li&gt;route navigation;&lt;/li&gt;
  &lt;li&gt;off-route alerts;&lt;/li&gt;
  &lt;li&gt;distance and elevation information;&lt;/li&gt;
  &lt;li&gt;voice and haptic guidance;&lt;/li&gt;
  &lt;li&gt;workout recording.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In developer testing on Apple Watch Ultra 2, Tooboo measured
approximately:&lt;/p&gt;

&lt;p&gt;Mode                         Battery consumption
  —————————- ———————
  Standard mode                ~7% per hour
  Tooboo Expedition Mode       ~4–5% per hour&lt;/p&gt;

&lt;p&gt;Actual battery performance depends on screen usage, GPS reception,
temperature, alert frequency and user interaction.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;real-world-test-maclehose-trail-in-hong-kong&quot;&gt;Real-World Test: MacLehose Trail in Hong Kong&lt;/h2&gt;

&lt;p&gt;&lt;img src=&quot;/img/post/260723/tooboo2.jpg&quot; alt=&quot;tooboo&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Tooboo was tested in a real long-distance hiking environment on the
MacLehose Trail in Hong Kong.&lt;/p&gt;

&lt;p&gt;The MacLehose Trail is one of Hong Kong’s most iconic long-distance
hiking routes, crossing diverse terrain including mountains, reservoirs,
valleys and coastal landscapes.&lt;/p&gt;

&lt;p&gt;This type of environment represents the scenario where standalone
navigation and battery efficiency matter most:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;long continuous activity time;&lt;/li&gt;
  &lt;li&gt;changing elevation;&lt;/li&gt;
  &lt;li&gt;limited opportunities to check a phone;&lt;/li&gt;
  &lt;li&gt;the need for reliable route guidance.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;test-setup&quot;&gt;Test setup&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Route:&lt;/strong&gt;&lt;br /&gt;
MacLehose Trail, Hong Kong&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Device:&lt;/strong&gt;&lt;br /&gt;
Apple Watch Ultra 2&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Navigation mode:&lt;/strong&gt;&lt;br /&gt;
Tooboo Expedition Mode&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/post/260723/tooboo6.jpg&quot; alt=&quot;tooboo&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;test-results&quot;&gt;Test results&lt;/h3&gt;

&lt;ul&gt;
  &lt;li&gt;Distance: &lt;strong&gt;20 km&lt;/strong&gt;&lt;/li&gt;
  &lt;li&gt;Elevation gain: &lt;strong&gt;840 m&lt;/strong&gt;&lt;/li&gt;
  &lt;li&gt;Duration: &lt;strong&gt;11 hours&lt;/strong&gt;&lt;/li&gt;
  &lt;li&gt;Starting battery: &lt;strong&gt;95%&lt;/strong&gt;&lt;/li&gt;
  &lt;li&gt;Remaining battery: &lt;strong&gt;49%&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;During the 11-hour hike, Apple Watch Ultra 2 consumed approximately
&lt;strong&gt;46% battery&lt;/strong&gt;, averaging around &lt;strong&gt;4.2% per hour&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Offline navigation, route following, GPS recording and off-route alerts
remained available throughout the activity. The measured battery use
also fell within the developer estimate of approximately &lt;strong&gt;4–5% per
hour&lt;/strong&gt; for Expedition Mode.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;integrations-and-pricing&quot;&gt;Integrations and Pricing&lt;/h2&gt;

&lt;p&gt;&lt;img src=&quot;/img/post/260723/tooboo5.jpg&quot; alt=&quot;tooboo&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;integrations&quot;&gt;Integrations&lt;/h3&gt;

&lt;p&gt;Tooboo integrates with:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;HealthKit&lt;/li&gt;
  &lt;li&gt;Apple Workout&lt;/li&gt;
  &lt;li&gt;Bluetooth sensors&lt;/li&gt;
  &lt;li&gt;Strava&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;pricing&quot;&gt;Pricing&lt;/h3&gt;

&lt;p&gt;Tooboo is free to download.&lt;/p&gt;

&lt;p&gt;Users can choose:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Pro subscription: 10$/year&lt;/li&gt;
  &lt;li&gt;lifetime purchase option: 20$&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;summary&quot;&gt;Summary&lt;/h2&gt;

&lt;p&gt;Tooboo combines offline navigation with battery optimization designed
for longer Apple Watch activities. In the 11-hour MacLehose Trail test,
core navigation remained active while Apple Watch Ultra 2 retained 49%
battery, showing the practical value of its watch-first approach.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Navigation, directly from the wrist.&lt;/strong&gt;&lt;/p&gt;
</description>
        <pubDate>Thu, 23 Jul 2026 00:00:00 +0800</pubDate>
        <link>http://blog.yaoyaojumprope.com/2026/07/23/tooboo-apple-watch-ultra-offline-navigation-expedition-mode/</link>
        <guid isPermaLink="true">http://blog.yaoyaojumprope.com/2026/07/23/tooboo-apple-watch-ultra-offline-navigation-expedition-mode/</guid>
        
        <category>Apple Watch</category>
        
        <category>Apple Watch Ultra</category>
        
        <category>hiking</category>
        
        <category>trail running</category>
        
        <category>cycling</category>
        
        <category>GPS navigation</category>
        
        <category>offline maps</category>
        
        <category>outdoor apps</category>
        
        
      </item>
    
      <item>
        <title>一次很典型的 SwiftUI 性能坑</title>
        <description>&lt;h1 id=&quot;一次很典型的-swiftui-性能坑&quot;&gt;一次很典型的 SwiftUI 性能坑&lt;/h1&gt;

&lt;p&gt;在 Tooboo 实际 SwiftUI页面里，运动会话页静止不动时，只有计时数字在变化，但 CPU 仍然有 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;60%~70%&lt;/code&gt;。优化后降到了大约 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;10%&lt;/code&gt;。问题的根源是：父视图读了高频变化的 observable 属性，导致整页都跟着失效，连地图这种重组件也被拖着每秒重算。&lt;/p&gt;

&lt;h2 id=&quot;先说结论&quot;&gt;先说结论&lt;/h2&gt;

&lt;p&gt;SwiftUI Observation 下有一句很重要的话：&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;谁在 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;body&lt;/code&gt; 里读取 observable 属性，谁就会订阅它。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;所以性能优化的关键原则是：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;高频变化的数据，不要在大容器视图里读&lt;/li&gt;
  &lt;li&gt;谁需要，谁在最小子树里读&lt;/li&gt;
  &lt;li&gt;地图、列表、图表这类重组件，只依赖自己真正关心的状态&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;错误写法父视图提前读取高频状态&quot;&gt;错误写法：父视图提前读取高频状态&lt;/h2&gt;

&lt;p&gt;下面这个写法非常常见，也非常容易出问题：&lt;/p&gt;

&lt;div class=&quot;language-swift highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;SwiftUI&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;Observation&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;CoreLocation&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;WorkoutData&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;elapsedSec&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;Int&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;nearIdx&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;Int&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;nil&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;state&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;WorkoutState&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;running&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;enum&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;WorkoutState&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;running&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;paused&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;@Observable&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;final&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;SessionViewModel&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;workoutData&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;WorkoutData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;followingPoints&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;CLLocation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[]&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;WorkoutSessionView&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;View&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;viewModel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;SessionViewModel&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;body&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;some&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;View&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;kt&quot;&gt;ZStack&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;kt&quot;&gt;MapView&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
                &lt;span class=&quot;nv&quot;&gt;followingPoints&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;viewModel&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;followingPoints&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                &lt;span class=&quot;nv&quot;&gt;routeGuideNearestIndex&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;viewModel&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;workoutData&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;nearIdx&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

            &lt;span class=&quot;kt&quot;&gt;DataPanelContainer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
                &lt;span class=&quot;nv&quot;&gt;workoutData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;viewModel&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;workoutData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                &lt;span class=&quot;nv&quot;&gt;workoutState&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;viewModel&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;workoutData&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;state&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;看起来只是把数据传给子视图，但真正的问题是：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;WorkoutSessionView.body&lt;/code&gt; 里读了 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;viewModel.workoutData&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;elapsedSec&lt;/code&gt; 每秒变化一次&lt;/li&gt;
  &lt;li&gt;于是整个 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;WorkoutSessionView&lt;/code&gt; 每秒重算一次&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;MapView&lt;/code&gt; 也被迫重新求值&lt;/li&gt;
  &lt;li&gt;地图内部的 annotation 过滤、可见区域计算、路径处理也都被重新执行&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;这就是“明明只有计时器在变，为什么地图也在刷”的原因。&lt;/p&gt;

&lt;h2 id=&quot;为什么这很贵&quot;&gt;为什么这很贵&lt;/h2&gt;

&lt;p&gt;很多人以为 SwiftUI 重算 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;body&lt;/code&gt; 只是“轻量声明式更新”。这话只说对了一半。&lt;/p&gt;

&lt;p&gt;如果你的子树里有这些东西，重算就不轻了：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;地图标注过滤&lt;/li&gt;
  &lt;li&gt;路线点聚合&lt;/li&gt;
  &lt;li&gt;图表数据映射&lt;/li&gt;
  &lt;li&gt;图片查找&lt;/li&gt;
  &lt;li&gt;几何计算&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;onChange&lt;/code&gt; / &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;onReceive&lt;/code&gt; 里的附带逻辑&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;也就是说，问题不只是“视图重建”，而是“重建触发了一整条昂贵计算链”。&lt;/p&gt;

&lt;h2 id=&quot;优化写法把订阅边界收缩到最小子树&quot;&gt;优化写法：把订阅边界收缩到最小子树&lt;/h2&gt;

&lt;p&gt;正确做法不是把所有数据都拆碎，而是让父视图只读地图真正需要的状态。&lt;/p&gt;

&lt;div class=&quot;language-swift highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;SwiftUI&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;Observation&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;CoreLocation&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;WorkoutData&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;elapsedSec&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;Int&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;state&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;WorkoutState&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;running&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;enum&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;WorkoutState&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;running&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;paused&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;@Observable&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;final&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;SessionViewModel&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;workoutData&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;WorkoutData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;followingPoints&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;CLLocation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[]&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;routeGuideNearestIndex&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;Int&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;nil&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;WorkoutSessionView&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;View&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;viewModel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;SessionViewModel&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;body&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;some&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;View&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;kt&quot;&gt;ZStack&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;kt&quot;&gt;MapView&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
                &lt;span class=&quot;nv&quot;&gt;followingPoints&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;viewModel&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;followingPoints&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                &lt;span class=&quot;nv&quot;&gt;routeGuideNearestIndex&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;viewModel&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;routeGuideNearestIndex&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

            &lt;span class=&quot;kt&quot;&gt;DataPanelContainer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;viewModel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;viewModel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;DataPanelContainer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;View&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;viewModel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;SessionViewModel&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;body&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;some&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;View&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;kt&quot;&gt;DataPanelView&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
            &lt;span class=&quot;nv&quot;&gt;workoutData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;viewModel&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;workoutData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;nv&quot;&gt;workoutState&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;viewModel&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;workoutData&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;state&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;这时订阅关系变成了：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;WorkoutSessionView&lt;/code&gt; 只订阅 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;followingPoints&lt;/code&gt; 和 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;routeGuideNearestIndex&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DataPanelContainer&lt;/code&gt; 才订阅 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;workoutData&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;elapsedSec&lt;/code&gt; 每秒变化时，只刷新面板，不刷新地图&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;这就是这次优化真正生效的原因。&lt;/p&gt;

&lt;h2 id=&quot;很多人会困惑的一点&quot;&gt;很多人会困惑的一点&lt;/h2&gt;

&lt;p&gt;“我把 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;workoutData&lt;/code&gt; 改成 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;viewModel&lt;/code&gt; 传给子视图，子视图不还是会失效吗？”&lt;/p&gt;

&lt;p&gt;对，子视图还是会失效。&lt;/p&gt;

&lt;p&gt;但这正是我们想要的结果。&lt;/p&gt;

&lt;p&gt;重点不是“完全不要失效”，而是“只让需要更新的那一小块失效”。计时面板每秒刷新是合理的，地图每秒跟着刷新才是不合理的。&lt;/p&gt;

&lt;h2 id=&quot;第二个关键点把高频字段和低频字段解耦&quot;&gt;第二个关键点：把高频字段和低频字段解耦&lt;/h2&gt;

&lt;p&gt;还有一个容易忽略的坑：&lt;/p&gt;

&lt;div class=&quot;language-swift highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kt&quot;&gt;MapView&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;nv&quot;&gt;followingPoints&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;viewModel&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;followingPoints&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;nv&quot;&gt;routeGuideNearestIndex&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;viewModel&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;workoutData&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;nearIdx&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;如果地图依赖的导航索引也藏在 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;workoutData&lt;/code&gt; 里，那父视图为了拿这个值，依然会订阅整个 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;workoutData&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;更稳妥的做法是把它单独拆出来：&lt;/p&gt;

&lt;div class=&quot;language-swift highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;@Observable&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;final&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;SessionViewModel&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;workoutData&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;WorkoutData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;routeGuideNearestIndex&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;Int&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;nil&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;这样地图依赖的导航状态，就和每秒变化的计时、心率、卡路里脱钩了。&lt;/p&gt;

&lt;h2 id=&quot;用一句话理解这次优化&quot;&gt;用一句话理解这次优化&lt;/h2&gt;

&lt;p&gt;旧写法：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;父视图先读了 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;viewModel.workoutData&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;所以父视图订阅了高频变化状态&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;elapsedSec&lt;/code&gt; 每秒变化&lt;/li&gt;
  &lt;li&gt;整个页面跟着重算&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;新写法：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;父视图不再读 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;viewModel.workoutData&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;只有面板子视图在读&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;elapsedSec&lt;/code&gt; 每秒变化&lt;/li&gt;
  &lt;li&gt;只有面板子树重算，地图保持稳定&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;一个更完整的最小-demo&quot;&gt;一个更完整的最小 Demo&lt;/h2&gt;

&lt;p&gt;下面这个 Demo 能更直观说明问题。&lt;/p&gt;

&lt;h3 id=&quot;优化前&quot;&gt;优化前&lt;/h3&gt;

&lt;div class=&quot;language-swift highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;SwiftUI&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;Observation&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;@Observable&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;final&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;DemoViewModel&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;elapsedSec&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;Int&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;points&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;Int&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;Array&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;..&amp;lt;&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;10_000&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;BadDashboardView&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;View&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;viewModel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;DemoViewModel&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;body&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;some&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;View&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;kt&quot;&gt;VStack&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;kt&quot;&gt;ExpensiveChart&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;points&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;viewModel&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;points&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;kt&quot;&gt;Text&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Time: &lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;viewModel&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;elapsedSec&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;这里 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;BadDashboardView&lt;/code&gt; 同时读取了：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;points&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;elapsedSec&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;只要 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;elapsedSec&lt;/code&gt; 每秒变化一次，整个 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;VStack&lt;/code&gt; 都会重新计算，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ExpensiveChart&lt;/code&gt; 也会被拖进去。&lt;/p&gt;

&lt;h3 id=&quot;优化后&quot;&gt;优化后&lt;/h3&gt;

&lt;div class=&quot;language-swift highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;SwiftUI&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;Observation&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;@Observable&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;final&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;DemoViewModel&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;elapsedSec&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;Int&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;points&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;Int&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;Array&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;..&amp;lt;&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;10_000&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;GoodDashboardView&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;View&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;viewModel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;DemoViewModel&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;body&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;some&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;View&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;kt&quot;&gt;VStack&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;kt&quot;&gt;ExpensiveChart&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;points&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;viewModel&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;points&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;kt&quot;&gt;TimerText&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;viewModel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;viewModel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;TimerText&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;View&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;viewModel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;DemoViewModel&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;body&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;some&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;View&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;kt&quot;&gt;Text&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Time: &lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;viewModel&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;elapsedSec&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;这次变化很小，但效果很大：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;GoodDashboardView&lt;/code&gt; 不再读取 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;elapsedSec&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;计时变化时，只重算 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;TimerText&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ExpensiveChart&lt;/code&gt; 不会每秒跟着刷新&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;可以直接复用的判断标准&quot;&gt;可以直接复用的判断标准&lt;/h2&gt;

&lt;p&gt;遇到 SwiftUI 页面性能异常时，先问自己这几个问题：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;父视图是不是读了高频变化的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@Observable&lt;/code&gt; 属性？&lt;/li&gt;
  &lt;li&gt;地图、图表、列表这些重组件，是不是被放在这个父视图下面？&lt;/li&gt;
  &lt;li&gt;子视图真正需要的只是一个字段，我是不是却把整个大对象传上去先读了一遍？&lt;/li&gt;
  &lt;li&gt;有没有“为了方便取值”，把多个更新频率完全不同的字段塞进同一个 observable 对象里？&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;只要这几个问题里有两个答“是”，大概率就已经踩坑了。&lt;/p&gt;

&lt;h2 id=&quot;一条最好记的经验&quot;&gt;一条最好记的经验&lt;/h2&gt;

&lt;p&gt;不要让高频变化的数据穿过大容器视图。&lt;/p&gt;

&lt;p&gt;换句话说：&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;谁显示，谁读取；谁不关心，谁就不要订阅。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;这类优化通常不需要改算法，不需要上多线程，也不需要做复杂缓存。很多时候，单纯把 Observation 的依赖边界切对，性能就会立刻下来。&lt;/p&gt;

&lt;h2 id=&quot;结尾&quot;&gt;结尾&lt;/h2&gt;

&lt;p&gt;这次案例非常典型，因为它看上去像“地图性能差”，但真正的问题并不在地图本身，而在 SwiftUI 的状态订阅范围。&lt;/p&gt;

&lt;p&gt;如果你的页面里有这些组合，就值得优先检查一遍：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@Observable&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;高频变化字段&lt;/li&gt;
  &lt;li&gt;大容器视图&lt;/li&gt;
  &lt;li&gt;地图、图表、列表等重子树&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;很多性能问题，最终不是算法问题，而是依赖关系问题。&lt;/p&gt;
</description>
        <pubDate>Wed, 18 Mar 2026 00:00:00 +0800</pubDate>
        <link>http://blog.yaoyaojumprope.com/blog/2026/03/18/swift-permormance-case/</link>
        <guid isPermaLink="true">http://blog.yaoyaojumprope.com/blog/2026/03/18/swift-permormance-case/</guid>
        
        <category>Coding</category>
        
        
        <category>blog</category>
        
      </item>
    
      <item>
        <title>watchOS 开发点滴</title>
        <description>&lt;p&gt;作为 &lt;a href=&quot;https://apps.apple.com/us/app/yaoyao-jump-rope/id1179393901&quot;&gt;YaoYao 跳绳&lt;/a&gt;、&lt;a href=&quot;https://apps.apple.com/us/app/tooboo-hiking-trail-guides/id6736378337&quot;&gt;Tooboo（徒步骑行&lt;/a&gt;）、&lt;a href=&quot;https://apps.apple.com/us/app/dundun-squat-counter/id1348285355&quot;&gt;DunDun（深蹲）&lt;/a&gt;的开发者，我开发的这些运动健身类产品，基本是以 Apple Watch app 为核心产品形态，而不是 iPhone app，以下我这些年来一些 watchOS 零零散散的开发经验，也希望对你有用。&lt;/p&gt;

&lt;h1 id=&quot;watch-app-和-ios-app-协同问题&quot;&gt;watch app 和 iOS app 协同问题&lt;/h1&gt;

&lt;p&gt;如果 watch app 和 iOS app 需要协同，手机和手表系统版本不一致，或两边 app 版本不一致都有可能导致很多问题。&lt;/p&gt;

&lt;h2 id=&quot;watchos-和-ios-版本不一致的问题&quot;&gt;watchOS 和 iOS 版本不一致的问题&lt;/h2&gt;

&lt;p&gt;比如用户的 iOS 版本是 26.1，watchOS 版本是 26.0，这个可能会导致一系列问题：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;手表端 app 无法安装：
新用户在安装手机 app 后，手表 app 未能同步自动安装&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Watch 端 HKWorkoutSession 结束后，手机端“健身” app 中看不到数据，健身圆环无法填充。
如果是健身 app，非常依赖 HealthKit 存储，这个问题相当普遍。&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Watch Connectivity 通信失败，手表和手机 app 无法使用 WCSession 通信。 &lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;watch-app-和-ios-app-版本不一致的问题&quot;&gt;watch app 和 iOS app 版本不一致的问题&lt;/h2&gt;

&lt;p&gt;比如用户的 iOS app 是 1.1，watch app 是 1.0，&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Watch Connectivity 通信可能失败，WCSession 相关方法会失效。 &lt;/li&gt;
&lt;/ul&gt;

&lt;h1 id=&quot;watch-app-和-ios-app-互相唤起&quot;&gt;watch app 和 iOS app 互相唤起&lt;/h1&gt;

&lt;h2 id=&quot;watch-app-唤起-ios-app&quot;&gt;watch app 唤起 iOS app&lt;/h2&gt;

&lt;p&gt;使用 watch 端  WCSession sendMessage 给 iOS app 发消息可直接唤起 iOS app。&lt;/p&gt;

&lt;p&gt;如果 iOS app 未启动，会直接被拉起。否则 iOS app 会在后台运行。
这是一个极其强大的功能，比如 watchOS 并不支持 SFSpeechRecognizer，&lt;a href=&quot;https://x.com/haozes/status/1710624500973519325&quot;&gt;甚至可以把语音流发到手机，在手机上完成语音转写，间接实现手表上的实时语音转写效果。&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;YaoYao 和 Tooboo 也是使用这种方法，发送需要语音合成文本给手机 app ，然后手机 app 使用语音合成再在手机上播报语音，这样用户可以在运动时一边在手机上听音乐，一边听运动语音提醒。&lt;/p&gt;

&lt;h2 id=&quot;ios-app-唤起-watch-app&quot;&gt;iOS app 唤起 watch app&lt;/h2&gt;

&lt;p&gt;可使用 HKHealthStore 的 startWatchApp(with:) 唤起 watch app&lt;/p&gt;

&lt;div class=&quot;language-javascript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;  &lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;launchEmptyWatchApp&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;onComplete&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;((&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;Bool&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;Error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;?)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Void&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)?&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;nil&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;){&lt;/span&gt;
        &lt;span class=&quot;kd&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;workoutConfiguration&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;HKWorkoutConfiguration&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
        &lt;span class=&quot;nx&quot;&gt;workoutConfiguration&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;activityType&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;other&lt;/span&gt;
        &lt;span class=&quot;nx&quot;&gt;workoutConfiguration&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;locationType&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;outdoor&lt;/span&gt;
       
        &lt;span class=&quot;kd&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;healthStore&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;HKHealthStore&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
        &lt;span class=&quot;nx&quot;&gt;healthStore&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;startWatchApp&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;with&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;workoutConfiguration&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;success&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;in&lt;/span&gt;
            &lt;span class=&quot;nx&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;gt;&amp;gt;&amp;gt; startWatchApp,launchEmptyWatchApp: &lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;(success),error:&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;(String(describing: error))&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;nx&quot;&gt;onComplete&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;?(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;success&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;在 watch 端：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-plain&amp;#x20;text&quot;&gt; func handle(_ workoutConfiguration: HKWorkoutConfiguration) {
      // 此处不一定非要启动 HKWorkoutSession
 }
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;注：该方法仅可在 app category 为 Healthcare &amp;amp; Fitness 才可用，否则审核很可能驳回。&lt;/p&gt;

&lt;h1 id=&quot;watch-app-和-ios-app-数据同步&quot;&gt;watch app 和 iOS app 数据同步&lt;/h1&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;App Groups&lt;/th&gt;
      &lt;th&gt;X&lt;/th&gt;
      &lt;th&gt;watchOS 从 2.0 开始已经不支持这种方式&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;CloudKit&lt;/td&gt;
      &lt;td&gt;✓&lt;/td&gt;
      &lt;td&gt;支持类似 SwiftData 和老的 CloudKit 相关功能&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;iCloud Document&lt;/td&gt;
      &lt;td&gt;X&lt;/td&gt;
      &lt;td&gt;watchOS 并不支持 iCloud Ubiquitous Containers&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;iCloud key-value&lt;/td&gt;
      &lt;td&gt;✓&lt;/td&gt;
      &lt;td&gt;支持 NSUbiquitousKeyValueStore，并较为推荐&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Watch Connectivity&lt;/td&gt;
      &lt;td&gt;✓&lt;/td&gt;
      &lt;td&gt;使用 WCSession 发送消息或文件是相对可靠稳定即时的方法同步两边的数据，较为推荐。更多参考：https://developer.apple.com/documentation/watchconnectivity/wcsession&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;注：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;WCSession  transferFile 发送文件方法在模拟器不支持，需要真机测试&lt;/li&gt;
&lt;/ul&gt;

&lt;h1 id=&quot;异常重启与恢复&quot;&gt;异常重启与恢复&lt;/h1&gt;

&lt;h2 id=&quot;修改-iphone-app-配置会导致-watch-app-立即重启&quot;&gt;修改 iPhone app 配置会导致 watch app 立即重启&lt;/h2&gt;

&lt;p&gt;在 iPhone 端 app 修改任意隐私权限，如通知，位置，健康隐私权限会直接导致手表端 app 立即被 SIGKILL。&lt;/p&gt;

&lt;p&gt;这个问题在一般情况下问题不大，但当你的 watch app 使用 Watch Connectivity 和手机实时通信时，用户在手机 app 上操作，正好碰到需要授权时，watch app 会被杀掉，会非常影响体验。&lt;/p&gt;

&lt;h2 id=&quot;运动会话-watch-app-的异常重启与恢复&quot;&gt;运动会话 watch app 的异常重启与恢复&lt;/h2&gt;

&lt;p&gt;使用 HKWorkoutSession 的 app，在运动过程中异常崩溃了，可以按照 Apple 的文档通过 WKExtensionDelegate 的 handleActiveWorkoutRecovery 方法重启运动会话，&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;
    &lt;p&gt; handleActiveWorkoutRecovery 方法不会在重启手表时被调用。
但比如像 Tooboo 这样的应用，用户可能一直用到没电，在充满电后，再打开手表，无法确保这个方法被调用。&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;建议在 applicationDidFinishLaunching 中 检测 HKHealthStore().recoverActiveWorkoutSession&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;recoverActiveWorkoutSession 仅能恢复HKWorkoutSession 定义的那些一般数据(时间、距离、卡路里等）app 自己定义统计的指标需要自己备份和还原。&lt;/p&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;h1 id=&quot;内存泄露问题&quot;&gt;内存泄露问题&lt;/h1&gt;

&lt;p&gt;无论是 iOS / watchOS app 在关闭时只是 freeze 而不是真正 restart，这会导致内存泄露问题非常隐蔽。会累积使用很多次后，才会导致 app 因内存使用过大被 watchdog kill。&lt;/p&gt;

&lt;h4 id=&quot;watchos-上tabview-嵌套会导致内存泄露&quot;&gt;watchOS 上TabView 嵌套会导致内存泄露！&lt;/h4&gt;

&lt;div class=&quot;language-swift highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;SwiftUI&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;ContentView&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;View&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;body&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;some&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;View&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;kt&quot;&gt;TabView&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;kt&quot;&gt;TabView&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                &lt;span class=&quot;kt&quot;&gt;Text&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Page V1&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
                &lt;span class=&quot;kt&quot;&gt;Text&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Page V1&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;tabViewStyle&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;verticalPage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;kt&quot;&gt;Text&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Page H2&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;在 watch app 可能你需要类似的架构布局，但目前会导致内存泄露，需要避免嵌套使用 TabView。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/post/251207/image.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;h1 id=&quot;电量-优化&quot;&gt;电量 优化&lt;/h1&gt;

&lt;p&gt;使用 HKWorkoutSession 的 app 可以获得相对其他 app 无法拥有的权限：后台运行。同时 app 的责任也更大，如果 UI 有高频刷新的动画、甚至普通数据驱动的 UI 刷新长时间运行都会消耗大量电量。&lt;/p&gt;

&lt;h4 id=&quot;主要优化思路&quot;&gt;主要优化思路&lt;/h4&gt;

&lt;ol&gt;
  &lt;li&gt;isLuminanceReduced 为 true 时减少刷新&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;用户在使用手表的时候，大部分时间手腕是放下的，不可能一直抬着手。这时候系统会将屏幕变暗，也就是 isLuminanceReduced 值会变为 true，仅在抬腕时高频刷新，其他时候尽可能降低刷新频率。&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;App 不在前台时 减少刷新
监听 NotificationCenter.default.publisher(for: WKApplication.willResignActiveNotification)，当 app 不在 active 状态时减少刷新。&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&quot;tooboo-的性能优化&quot;&gt;
Tooboo 的性能优化&lt;/h4&gt;

&lt;p&gt;在 Tooboo 这种徒步场景下，用户可以连续导航 12 小时(GPS、心率传感始终工作)，低电量模式下可以使用 16 小时（Apple Watch Ultra 2 测试）。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/post/251207/image-1.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Tooboo watch app 主要有3个线程， 地图的渲染， UI 界面的主线程，导航算法。 &lt;/p&gt;

&lt;p&gt;每次 GPS 的刷新会触发导航算法的判断，是否偏离路线或者要不要转弯，但这时候地图不一定会渲染，地图的磁贴也不会下载，地图的下载渲染仅在用户的抬腕时，当在后台或者亮度降低时高昂的 UI 操作都会是非常低频的。&lt;/p&gt;

&lt;p&gt;在 UI 界面渲染方法，Tooboo 并没有使用 SwiftUI 的默认的数据驱动渲染方式，因为在运动过程中，运动的数据指标很多（心率、卡路里、距离、速度、位置等等），任意一个指标变化都会导致 UI 重绘，这个刷新频率会在每秒有几次。 &lt;/p&gt;

&lt;p&gt;Tooboo 使用了 TimelineSchedule 定时刷新的方式，如果抬腕亮屏将 Schedule 频率调高 到 1hz，否则降低到 10-20 秒 刷新一次，这样也会大大减少 CPU 的消耗。        &lt;/p&gt;

&lt;div class=&quot;language-swift highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;MetricsTimelineSchedule&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;TimelineSchedule&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;startDate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;Date&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;isPaused&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;Bool&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;lowInterval&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;Double&lt;/span&gt;

    &lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;init&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;startDate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;Date&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;isPaused&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;Bool&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;lowInterval&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;Double&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;nil&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;startDate&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;startDate&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;isPaused&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;isPaused&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;lowInterval&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;lowInterval&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;??&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;Double&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;AppSetting&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;shared&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;lowRefreshInterval&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;entries&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;startDate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;Date&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;mode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;TimelineScheduleMode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;AnyIterator&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;Date&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;baseSchedule&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;PeriodicTimelineSchedule&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;startDate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;by&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mode&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;lowFrequency&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;lowInterval&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;1.0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;entries&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;startDate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;mode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;AnyIterator&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;Date&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;guard&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;isPaused&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;nil&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
            &lt;span class=&quot;c1&quot;&gt;//print(&quot;MetricsTimelineSchedule next()&quot;)&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;baseSchedule&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;next&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-swift highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;Metric&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;View&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;@EnvironmentObject&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;viewModel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;WorkoutViewModel&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;@Environment&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(\&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;isLuminanceReduced&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;isLuminanceReduced&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;body&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;some&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;View&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;kt&quot;&gt;TimelineView&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;MetricsTimelineSchedule&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;viewModel&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;workoutData&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;startDate&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;??&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;Date&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;isPaused&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;viewModel&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;workoutState&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;paused&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;isLuminanceReduced&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)))&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;timeline&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;in&lt;/span&gt;
            &lt;span class=&quot;kt&quot;&gt;ZStack&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;alignment&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;topLeading&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;){&lt;/span&gt;
                
                &lt;span class=&quot;kt&quot;&gt;VStack&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;alignment&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;leading&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;spacing&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;){&lt;/span&gt;
                    &lt;span class=&quot;kt&quot;&gt;Spacer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
                    &lt;span class=&quot;kt&quot;&gt;Text&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;viewModel&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;elapsedSec&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;shortFormatted&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;foregroundStyle&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;Color&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;yellow&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
                           &lt;span class=&quot;c1&quot;&gt;// other metric data&lt;/span&gt;
                    &lt;span class=&quot;kt&quot;&gt;Spacer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
                &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;frame&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;maxWidth&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;infinity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;alignment&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;leading&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
                &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;ignoresSafeArea&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;edges&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;bottom&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
                &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;scenePadding&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
        
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;比如这里在 app 运动暂停或 isLuminanceReduced 为 true 时，app 是低频刷新的。&lt;/p&gt;

&lt;h1 id=&quot;小结&quot;&gt;小结&lt;/h1&gt;

&lt;ul&gt;
  &lt;li&gt; Apple 从 watchOS 6 时支持了 SwiftUI 来开发 watch app 界面，watchOS 已经没有任何理由使用之前类似于 UIKit 的 watchkit 方式来开发。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;如果你是新产品，建议从 watchOS 9+ 开始支持。&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;相对 iOS app 的调试，watchOS 的 app 调试相对困难，另外使用 Xcode &amp;gt; Window &amp;gt; Organizer &amp;gt; Crash 收集到的 crash 报告会是 iOS 收到的日志的 1/10-1/20 。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Watch app 最好配合使用 app 本地写日志的方式记录问题，通过 WCSession 将日志发送到手机再收集。&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;在产品的部署方面，由于大量用户并不熟悉 watch 的 app 的安装方式，加上系统版本不一致可能导致 app 不能安装，致使这类产品在第一步就会遇到困难。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;让用户方便联系上开发者，提供支持非常有必要。&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;a href=&quot;https://www.yaoyaojumprope.com/about&quot;&gt;Haozes&lt;/a&gt;:  是 &lt;a href=&quot;https://apps.apple.com/us/app/yaoyao-jump-rope/id1179393901&quot;&gt;YaoYao跳绳&lt;/a&gt;、&lt;a href=&quot;https://apps.apple.com/us/app/tooboo-hiking-trail-guides/id6736378337&quot;&gt;Tooboo（徒步骑行&lt;/a&gt;）、&lt;a href=&quot;https://apps.apple.com/us/app/dundun-squat-counter/id1348285355&quot;&gt;DunDun（深蹲）&lt;/a&gt; 等健康健身类 app 的作者，致力于用身体去抵抗这个时代的丧。&lt;/p&gt;

&lt;p&gt;如果你是热爱户外，热爱 Apple Watch 这类的健康可穿戴产品的设计师，欢迎交流。&lt;/p&gt;

</description>
        <pubDate>Sun, 07 Dec 2025 00:00:00 +0800</pubDate>
        <link>http://blog.yaoyaojumprope.com/blog/2025/12/07/watchos-dev/</link>
        <guid isPermaLink="true">http://blog.yaoyaojumprope.com/blog/2025/12/07/watchos-dev/</guid>
        
        <category>Coding</category>
        
        
        <category>blog</category>
        
      </item>
    
      <item>
        <title>我需要向这个世界证明什么吗？--个体户开发者的 5年</title>
        <description>&lt;p&gt;我不爱说什么”独立开发者”这种高大上的词。在推特上，我就叫自己”个体户开发者”。说白了，我和街边卖煎饼果子的大爷也没什么两样，都是靠手艺吃饭的。&lt;/p&gt;

&lt;p&gt;五年前，我开始在家写代码。这五年过得跟做梦似的，我觉得有必要把这段经历写下来。写完了我还在琢磨要不要发出去，怪不好意思的。要说这五年，就像我在水泥地上用指甲划了道印子，也不知道算不算回事。&lt;/p&gt;

&lt;h2 id=&quot;我的那些玩意儿&quot;&gt;我的那些玩意儿&lt;/h2&gt;

&lt;p&gt;我做了6个App，就像养了6个不太听话的孩子。有的跑了，有的变了样（比如YaoYao，还得给它做安卓版和鸿蒙版）。苹果应用商店挺给面子，动不动就把我的App往前台推。YaoYao上了90次，顶瓜瓜16次，其他的也都露过脸。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/post/250101/apps.jpg&quot; alt=&quot;apps&quot; /&gt;&lt;/p&gt;

&lt;p&gt;有一回，苹果还把我写进新闻稿里，类似什么 WWDC的新秀。去年在上海，我还见着了苹果老大 TimCook。说实话，我觉得我做得也一般。&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://www.apple.com.cn/newsroom/2020/06/wwdc-inspires-new-apps-that-create-solutions/&quot;&gt;2020 Apple Newsroom: WWDC 孕育解决问题的新 app&lt;/a&gt;&lt;br /&gt;
&lt;img src=&quot;https://www.apple.com.cn/newsroom/cn/images/live-action/wwdc/WWDC_Wang-communicatng-with-Zheng-over-FaceTime_06192020_big.jpg.medium.jpg&quot; alt=&quot;newsroom&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://weibo.com/5524254784/MzwDgdbkx/&quot;&gt;2023 上海 Apple 设计开发加速器开发者交流会 TimCook 接见&lt;/a&gt;
&lt;img src=&quot;https://yaoyaojumprope.com/img/tim.jpg&quot; alt=&quot;timcook&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;赚了多少钱&quot;&gt;赚了多少钱？&lt;/h2&gt;

&lt;p&gt;说到钱，主要是苹果商店给的。刚开始跟我打工时差不多，后来疫情那会儿涨了不少，大概是以前的三倍。我对做生意一窍不通，全靠苹果推荐。有时候审核卡住了，收入就得掉一半。&lt;/p&gt;

&lt;p&gt;说来好笑，收入多了反而睡不好觉，去年还得了个下巴疼的毛病(颞下颌紊乱)。这让我明白了一个道理：有钱人为啥总觉得钱不够，穷人反而能睡得香。&lt;br /&gt;
&lt;img src=&quot;/img/post/250101/rv.jpg&quot; alt=&quot;Revenue&quot; /&gt;&lt;/p&gt;

&lt;p&gt;不过说实话，赚的钱也没让我的生活有多大变化。该开破车还开破车，该吃泡面还吃泡面。唯一的好处是，我可以随心所欲地写代码，不用担心明天吃什么。&lt;/p&gt;

&lt;h2 id=&quot;社交圈子变了吗&quot;&gt;社交圈子变了吗？&lt;/h2&gt;

&lt;p&gt;微信加了五千多人，建了一堆群，推特上也有上万粉丝。感觉像回到了上大学那会儿混BBS的日子，时不时约个网友出来喝茶。不过我发现自己越来越不会在现实中说话了，只会在推特上骂骂咧咧，吐槽各种平台。
&lt;img src=&quot;/img/post/250101/sns.jpg&quot; alt=&quot;sns&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/post/250101/acc.jpg&quot; alt=&quot;acc&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;一些乱七八糟的想法&quot;&gt;一些乱七八糟的想法&lt;/h2&gt;

&lt;p&gt;要说做产品最重要的是什么？我觉得就是要有趣，要让自己开心。有人想给我投资，让我组建团队做大做强。我想说：算了吧，我可不想再管人了，那太累。卖给你要不要？&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/post/250101/github.jpg&quot; alt=&quot;Github&quot; /&gt;&lt;/p&gt;

&lt;p&gt;我的 Github提交记录看着挺吓人，但跟收入没啥关系。在这个世界上，会说话比会写代码重要多了。&lt;/p&gt;

&lt;p&gt;YaoYao就是我周末随手写的，DunDun是过年无聊搞出来的。它们火不火，完全是看老天爷赏不赏脸。&lt;/p&gt;

&lt;p&gt;在这个工业化的时代，这种单打独斗的码农，多少有一些格格不入。但我觉得挺好，至少我还在做自己喜欢的事。&lt;/p&gt;

&lt;p&gt;最后借用《搏击俱乐部》的话：你不是你的工作，不是你的银行存款，不是你开的破车。你就是这世界上的一坨会唱会跳的废料。&lt;/p&gt;

&lt;p&gt;我写代码不是为了证明什么，就是想看看自己能折腾出什么名堂来。生命在流逝，我只是在探索自己罢了。&lt;/p&gt;

&lt;h2 id=&quot;感谢&quot;&gt;感谢&lt;/h2&gt;
&lt;p&gt;说到最后，我得谈谈那些在网上认识的朋友们。&lt;/p&gt;

&lt;p&gt;有时候我觉得自己像个孤胆英雄，在这数字世界里独自挥舞着键盘。事实上，我身后站着一群网友，大家的留言、吐槽、建议，都在默默支撑着我。他们就像是我的啦啦队，时不时给我加油打气，有时还会提出些馊主意。&lt;/p&gt;

&lt;p&gt;感谢你们，使我免于打工奴役之苦。&lt;/p&gt;
</description>
        <pubDate>Wed, 01 Jan 2025 00:00:00 +0800</pubDate>
        <link>http://blog.yaoyaojumprope.com/blog/2025/01/01/5-years/</link>
        <guid isPermaLink="true">http://blog.yaoyaojumprope.com/blog/2025/01/01/5-years/</guid>
        
        <category>Coding</category>
        
        
        <category>blog</category>
        
      </item>
    
      <item>
        <title>周一到周五跳绳，周末呢？</title>
        <description>&lt;p&gt;&lt;img src=&quot;/img/post/241106/toopoo_banner.png&quot; alt=&quot;Banner&quot; /&gt;
周一到周五跑步跳绳有氧，周末？那我们就徒步吧😃&lt;/p&gt;

&lt;p&gt;我大概是从 16年开始接触户外徒步，之前是和户外俱乐部一起，但疫情后，以前的俱乐部都倒闭，找不到了。&lt;/p&gt;

&lt;p&gt;我开始从小红书发现一些徒步的目的地，然后在两步路 app 里搜索轨迹线路，和三五好友去玩。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/post/241106/climb.jpg&quot; alt=&quot;徒步溯溪&quot; /&gt;&lt;/p&gt;

&lt;p&gt;过去两年的春秋天，天气好，大概每周都会抽一天去走一日行程的线路。从最初的市郊，到后来合肥周边 200 公里内的各种野山，我发现了许多不知名、但又风景如画的地方，甚至对安徽的地理有了更深的理解🗺️。&lt;br /&gt;
和老朋友们一起爬爬山，也是中年生活中少有的乐趣。&lt;/p&gt;

&lt;p&gt;然而两步路之前没有手表版（现在有，但也约等于没有），频繁掏手机查看路线防止走错路实在太麻烦了。使用 Watch 来看线路要好很多，但 Apple Watch 这种 App只有国外有一些，但由于某些国情，国内的轨迹在国外的app上显示的。
&lt;img src=&quot;/img/post/241106/errormap.jpg&quot; alt=&quot;某国外app里轨迹偏移导致穿过了湖中心&quot; /&gt;&lt;/p&gt;

&lt;p&gt;在等了几年 Apple 也没有推出解决方案后，我决定自己动手开发一个应用—— Toopoo 🎉
&lt;img src=&quot;/img/post/241106/1.jpg&quot; alt=&quot;Toopoo&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Toopoo 是一款专为户外运动（徒步、骑行、越野跑）设计的 Apple Watch 应用，让你在户外活动时体验更加自然的轨迹导航。&lt;/p&gt;

&lt;p&gt;它弥补了 Apple Watch 在户外运动中的不足，结合 watchOS 的流畅交互，让你的 Apple Watch 甚至能超越高端的佳明户外手表！⌚️&lt;/p&gt;

&lt;p&gt;你可以从两步路 App 导出徒步或骑行的 gpx 轨迹文件&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/post/241106/importgpx.jpg&quot; alt=&quot;如何从两步路下载 GPX轨迹文件&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;主要功能亮点&quot;&gt;主要功能亮点：&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;/img/post/241106/watch1.jpg&quot; alt=&quot;手表app截图1&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;🏔️ 高程及等高线地形地图&lt;/li&gt;
  &lt;li&gt;📉 上坡下坡一目了然，深红色表示爬坡，深蓝色表示下坡&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src=&quot;/img/post/241106/watch2.jpg&quot; alt=&quot;手表app截图2&quot; /&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;🚩 轨迹导航提醒，支持偏离轨迹、转弯、方向震动及语音导航&lt;/li&gt;
  &lt;li&gt;🔄 地图与数据视图无缝切换，显示距离、心率、速度、海拔等重要信息&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src=&quot;/img/post/241106/watch3.jpg&quot; alt=&quot;手表app截图3&quot; /&gt;&lt;/p&gt;

&lt;p&gt;🌅 熄屏显示日落时间，确保不遗漏下山时机&lt;br /&gt;
📡 支持地图预加载，户外无网络也无压力&lt;/p&gt;

&lt;p&gt;Toopoo 也是我这些年对编程和设计的重新思考的结果，我不再将完成功能作为第一目标，保持代码和功能的decent 并让一个小工具真正融入生活是它的追求。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/post/241106/toopoophone.jpg&quot; alt=&quot;简洁的手机端界面&quot; /&gt;&lt;/p&gt;

&lt;p&gt;在手表上从零实现地图渲染，对于我一个数学不好的人来说是一种挑战，在续航本来就捉襟见肘的 Apple Watch 上让 App 保持GPS和心率连续运行 10小时也是一个巨大的挑战。&lt;/p&gt;

&lt;p&gt;这些工程上带来的折磨和突破的快乐就像在玩魂类的游戏。其次它充满了我的反复的内省，每当我设计了一个前后 callback的东西，都让我兴奋不已，这又像一个解迷游戏。&lt;/p&gt;

&lt;p&gt;也许它依然不会取得“商业”的成功，但它依然是我目前为止最为满意的一次发布。&lt;/p&gt;

&lt;h2 id=&quot;定价&quot;&gt;定价&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;按月 ¥8.00&lt;/li&gt;
  &lt;li&gt;按年 ¥38.00&lt;/li&gt;
  &lt;li&gt;一次买断 ¥48.00&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;目前 App 还不够很完善，早鸟买断价是48，后面随着完善会恢复到 68元买断。 （此类 App 一般国外app 的订阅价在 20美元一年左右），相信这个定价也足够有诚意。&lt;/p&gt;

&lt;p&gt;在最近一个月的公测中，也非常感谢很多用户给予的建议和问题反馈，像 YaoYao 一样，这不是我一个人的智慧，是大家一起创造的。最后也感谢 Logo 的设计者诗雨。&lt;/p&gt;

&lt;h2 id=&quot;免费体验计划&quot;&gt;免费体验计划&lt;/h2&gt;
&lt;p&gt;另外我们也尝试使用小红书来推广我们的产品，你也可以在小红书发图文后，来领取一年的免费订阅兑换码。&lt;br /&gt;
&lt;img src=&quot;/img/post/241106/recruitment.jpg&quot; alt=&quot;免费体验计划&quot; /&gt;&lt;/p&gt;
</description>
        <pubDate>Wed, 06 Nov 2024 00:00:00 +0800</pubDate>
        <link>http://blog.yaoyaojumprope.com/blog/2024/11/06/toopoo-launched/</link>
        <guid isPermaLink="true">http://blog.yaoyaojumprope.com/blog/2024/11/06/toopoo-launched/</guid>
        
        <category>Coding</category>
        
        
        <category>blog</category>
        
      </item>
    
      <item>
        <title>深蹲 App(DunDun) 重制版发布啦</title>
        <description>&lt;p&gt;&lt;img src=&quot;/img/post/240308/dundun_banner.jpg&quot; alt=&quot;Banner&quot; /&gt;&lt;/p&gt;

&lt;p&gt;人体的大部分肌肉都分布在腿部，而深蹲主要锻炼大腿肌肉群，不仅有助于促进新陈代谢，还能改善身体姿势、加强腰椎和膝盖，降低运动受伤的风险。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/post/240308/dundun.gif&quot; alt=&quot;Animation&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;dundun-萌生于某个春节假期&quot;&gt;DunDun 萌生于某个春节假期&lt;/h2&gt;

&lt;p&gt;2018 年春节期间，在老家闲来无事，写了一个可以用手机或 Apple Watch 传感器来自动计算深蹲次数的小应用，然后就放到了App Store 上。没想到一晃，居然已经过了 6 年。老家的老屋也早已荒芜，不能住人。 App 偶尔修葺，却一直维护至今。&lt;/p&gt;

&lt;p&gt;YaoYao 目前已经进入功能相对稳定的状态。今年春节期间，我开始重新装修 DunDun，目前看起来已经有了一些样子，小鹿家族再添一员！&lt;/p&gt;

&lt;h2 id=&quot;dundun-重制版&quot;&gt;DunDun 重制版&lt;/h2&gt;
&lt;p&gt;这次进行了很多功能增加和 UI 的调整，按照游戏行业的说法，可以称之为 DunDun Remaster（重制版）😁。&lt;br /&gt;
很多设计灵感来自 YaoYao，比如：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;惨绝人寰的等级体系&lt;/li&gt;
  &lt;li&gt;滥竽充数的勋章&lt;/li&gt;
  &lt;li&gt;枯燥无味的挑战&lt;/li&gt;
  &lt;li&gt;花式的折磨（多种运动模式）&lt;br /&gt;
….&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src=&quot;/img/post/240308/dd_scr1.jpg&quot; alt=&quot;Screen1&quot; /&gt;
&lt;img src=&quot;/img/post/240308/dd_scr2.jpg&quot; alt=&quot;Screen2&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;女神节特惠活动&quot;&gt;女神节特惠活动&lt;/h2&gt;
&lt;p&gt;在3月8日这个特殊的日子里，YaoYao 和 DunDun 都为你准备了一些优惠活动。&lt;/p&gt;

&lt;h3 id=&quot;dundun-活动&quot;&gt;DunDun 活动&lt;/h3&gt;

&lt;p&gt;🌟 一次性买断价格！
原价：28 元 ➡️ 特惠价：21元！&lt;/p&gt;

&lt;h3 id=&quot;yaoyao-活动&quot;&gt;YaoYao 活动&lt;/h3&gt;

&lt;p&gt;🌟 新用户年度订阅费
原价：25.8元 ➡️ 特惠价：18元！&lt;br /&gt;
🌟 一次性买断价格！
原价：45元 ➡️ 特惠价：38元！&lt;/p&gt;

&lt;h2 id=&quot;赶快下载体验吧&quot;&gt;赶快下载体验吧！&lt;/h2&gt;
&lt;p&gt;DunDun:&lt;br /&gt;
https://apps.apple.com/cn/app/id1348285355&lt;/p&gt;

&lt;p&gt;YaoYao 跳绳:&lt;br /&gt;
https://apps.apple.com/cn/app/id1179393901&lt;/p&gt;
</description>
        <pubDate>Fri, 08 Mar 2024 00:00:00 +0800</pubDate>
        <link>http://blog.yaoyaojumprope.com/blog/2024/03/08/yaoyao-dundun-remaster/</link>
        <guid isPermaLink="true">http://blog.yaoyaojumprope.com/blog/2024/03/08/yaoyao-dundun-remaster/</guid>
        
        <category>Coding</category>
        
        
        <category>blog</category>
        
      </item>
    
      <item>
        <title>用 YaoYao 在手表上给好友发一条语音吧</title>
        <description>&lt;p&gt;&lt;img src=&quot;/img/post/1120/voicemsg_banner.jpg&quot; alt=&quot;Banner&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;设计缘由&quot;&gt;设计缘由&lt;/h2&gt;

&lt;p&gt;YaoYao-跳绳 App 有一个简单的好友系统，它设计的主要目标是面向你的家人或线下认识的好友，可以互相关注对方的运动情况，或在对方运动完，给他点个赞，鼓励一下。&lt;/p&gt;

&lt;p&gt;所以，你不能在排行榜里简单地添加陌生人为你的好友。&lt;/p&gt;

&lt;p&gt;新的语音消息功能，也正是为了这样的关系设计。&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://www.bilibili.com/video/BV1tC4y117wh/?vd_source=2f66e6710d0f4e540d4d3e90ae1fc174&quot; title=&quot;CameraMaster&quot;&gt;&lt;img src=&quot;https://i0.hdslb.com/bfs/archive/6cf5b4539cdd96f9c4eb44a1d718ce209c7f3b60.jpg&quot; alt=&quot;DemoVideo&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&quot;语音消息&quot;&gt;语音消息&lt;/h2&gt;

&lt;p&gt;滑到【好友】列表，点击你的好友，即可给你的好友发送一条语音消息。或者你在手表上添加一个【语音留言】的复杂功能 ，这样可以快速打开，给最后一个联系的好友发送消息。这样的操作比微信还要快捷一些。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/post/1120/msg1.jpg&quot; alt=&quot;VoiceMessage1&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;语音自动转文字&quot;&gt;语音自动转文字&lt;/h3&gt;
&lt;p&gt;如果你的手机在身边，它会协同手机，自动将语音实时转成文字，最终一起发送。 这样你也不用担心文字识别不全对，或者对方必须听语音才能知道消息的内容。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/post/1120/msg2.jpg&quot; alt=&quot;VoiceMessage2&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;语音播放和对讲机模式&quot;&gt;语音播放和对讲机模式&lt;/h3&gt;
&lt;p&gt;默认情况下，对方在收到你的消息后，可以点开通知里的播放按钮，听取语音信息。&lt;/p&gt;

&lt;p&gt;打开对讲机模式后，来信语音会自动播放，无需要点击通知。这个功能非常像对讲机的效果，另外国行的 Apple Watch 阉割的对讲机功能，和这个稍有不同，它需要一直处于连线状态，YaoYao 的对讲机功能，更像语音留言，但是也可以自动播放声音。&lt;/p&gt;

&lt;p&gt;如果你需要屏蔽对方的骚扰，你可以在好友界面【关闭通知】即可。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/post/1120/msg3.jpg&quot; alt=&quot;VoiceMessage3&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;下载&quot;&gt;下载&lt;/h2&gt;
&lt;p&gt;App Store 下载 YaoYao, 或更新 App 到最新版，即可体验该功能。
https://apps.apple.com/cn/app/id1179393901&lt;/p&gt;
</description>
        <pubDate>Mon, 20 Nov 2023 00:00:00 +0800</pubDate>
        <link>http://blog.yaoyaojumprope.com/blog/2023/11/20/yaoyao-voice/</link>
        <guid isPermaLink="true">http://blog.yaoyaojumprope.com/blog/2023/11/20/yaoyao-voice/</guid>
        
        <category>Coding</category>
        
        
        <category>blog</category>
        
      </item>
    
      <item>
        <title>Send a voice to a friend on your watch with YaoYao!</title>
        <description>&lt;p&gt;&lt;img src=&quot;/img/post/1120/voicemsg_banner.jpg&quot; alt=&quot;Banner&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;design-reasons&quot;&gt;Design Reasons&lt;/h2&gt;

&lt;p&gt;YaoYao-Jump Rope App has a simple friend system, which is designed with the main goal of targeting your family members or close friends you know offline to follow each other’s workouts, or to give each other a like and encouragement when they’re done with their workout.&lt;/p&gt;

&lt;p&gt;So you can’t simply add strangers as your friends in the leaderboard.&lt;/p&gt;

&lt;p&gt;The new voice messaging feature is also designed for just such relationships.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://youtu.be/F8PQQmSQDTg&quot; title=&quot;CameraMaster&quot;&gt;&lt;img src=&quot;https://i.ytimg.com/vi/F8PQQmSQDTg/hqdefault.jpg&quot; alt=&quot;DemoVideo&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&quot;voice-message&quot;&gt;Voice Message&lt;/h2&gt;

&lt;p&gt;Slide to the [Friends] list and tap your friend to send a voice message to your friend. Or you add a [voice message] complication to your watch , so you can quickly open it and send a message to the last contacted buddy. This is a little quicker than WeChat.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/post/1120/msg1.jpg&quot; alt=&quot;VoiceMessage1&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;automatic-speech-to-text&quot;&gt;Automatic Speech-to-text&lt;/h3&gt;
&lt;p&gt;If you have your phone with you, it will collaborate with your phone to automatically convert the voice to text in real time and eventually send it together. This way you don’t have to worry about the text not being recognized correctly or the other person having to listen to the voice to know what the message is about.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/post/1120/msg2.jpg&quot; alt=&quot;VoiceMessage2&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;voice-playback-and-walkie-talkie-mode&quot;&gt;Voice playback and Walkie-Talkie mode&lt;/h3&gt;
&lt;p&gt;By default, the other party can tap the play button in the notification to listen to the voice message after receiving your message.&lt;/p&gt;

&lt;p&gt;After opening the walkie-talkie mode, the incoming voice will be played automatically, without the need to tap the notification. This function is very much like the feature of the walkie-talkie,  and this is slightly different, it needs to always be in a state of connectivity, YaoYao’s walkie-talkie function, is more like voice message, but also can automatically play the sound.&lt;/p&gt;

&lt;p&gt;If you need to block each other’s harassment, you can turn off notifications in the friend interface.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/post/1120/msg3.jpg&quot; alt=&quot;VoiceMessage3&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;download&quot;&gt;Download&lt;/h2&gt;
&lt;p&gt;Download YaoYao from the App Store, or update the App to the latest version to experience this feature.
https://apps.apple.com/cn/app/id1179393901&lt;/p&gt;
</description>
        <pubDate>Mon, 20 Nov 2023 00:00:00 +0800</pubDate>
        <link>http://blog.yaoyaojumprope.com/blog/2023/11/20/yaoyao-voice-en/</link>
        <guid isPermaLink="true">http://blog.yaoyaojumprope.com/blog/2023/11/20/yaoyao-voice-en/</guid>
        
        <category>Coding</category>
        
        
        <category>blog</category>
        
      </item>
    
      <item>
        <title>打电脑，也要保持好坐姿(顶瓜瓜现已支持 MacOS)</title>
        <description>&lt;p&gt;去年我发布了一款坐姿提醒的App  – &lt;a href=&quot;https://blog.yaoyaojumprope.com/blog/2022/07/22/headsup/&quot;&gt;顶瓜瓜&lt;/a&gt;，它使用iPhone/iPad 的 TrueDepth 深感相机来检测用户头部位置，来提醒坐姿。&lt;/p&gt;

&lt;p&gt;功能新颖有趣，获得了 《App Store 九月最佳》等多个 App Store 推荐。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://blog.yaoyaojumprope.com/img/post/0720/headsup.gif&quot; alt=&quot;demo&quot; /&gt;&lt;/p&gt;

&lt;p&gt;后来又增加了使用 AirPods 运动传感器来检测的方式。这样可以不用一直亮着屏幕来使用。&lt;/p&gt;

&lt;p&gt;9月27日 Apple 发布了 macOS Sonoma（14.0）, 并向 MacOS 回填了一个移动设备上早已支持的 AirPods 运动传感器接口。&lt;/p&gt;

&lt;p&gt;想起早年智能手机刚出现的前两年，我甚至不屑于使用智能和性能差得发指的智能手机。没想到现在iPhone 已经可以跑 《生化危机4》 这样的 3A 大作，移动设备系统提供的功能也开始反哺桌面设备。&lt;/p&gt;

&lt;p&gt;macOS 版的顶瓜瓜，开始利用 AirPods这个运动传感器接口并已经上线 App Store。&lt;/p&gt;

&lt;h2 id=&quot;一边提醒坐姿一边做蕃茄钟&quot;&gt;一边提醒坐姿，一边做蕃茄钟。&lt;/h2&gt;

&lt;p&gt;相对于 iOS app，针对 macOS 系统，增加了状态栏工具，当计时时，状态栏会显示计时信息。这样，它不仅是一个坐姿提醒 App，还可以可把它当作蕃茄钟计时器。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/post/230927/headsup_mac.gif&quot; alt=&quot;MacHeadsUp&quot; /&gt;&lt;/p&gt;

&lt;p&gt;现在你在工作时，可以一边听歌，一边使用它提醒自己坐姿了，时间到了，活动一下！为祖国健康地工作五十年！&lt;/p&gt;

&lt;h2 id=&quot;下载地址&quot;&gt;下载地址&lt;/h2&gt;

&lt;p&gt;https://apps.apple.com/cn/app/id1629577265&lt;/p&gt;
</description>
        <pubDate>Wed, 27 Sep 2023 00:00:00 +0800</pubDate>
        <link>http://blog.yaoyaojumprope.com/blog/2023/09/27/voicedict/</link>
        <guid isPermaLink="true">http://blog.yaoyaojumprope.com/blog/2023/09/27/voicedict/</guid>
        
        <category>Coding</category>
        
        
        <category>blog</category>
        
      </item>
    
      <item>
        <title>YaoYao Jump Rope now supports iPad!</title>
        <description>&lt;p&gt;&lt;img src=&quot;/img/post/230906/ios17_1.jpg&quot; alt=&quot;Banner&quot; /&gt;&lt;/p&gt;

&lt;p&gt;iOS 17 has arrived. The most noteworthy feature of this year’s iPhone 15 is its new Type-C interface. 😁 However, our primary focus lies in Apple’s updates to the iOS system.&lt;/p&gt;

&lt;h2 id=&quot;the-display-on-the-ipad-is-quite-large&quot;&gt;The display on the iPad is quite large.&lt;/h2&gt;
&lt;p&gt;YaoYao’s fitness data is stored in Apple’s HealthKit, which offers the advantage of enhanced privacy and security. However, there is a drawback in that the iPad has never had an Apple Health App, thus rendering YaoYao incompatible with it. I provided feedback to Apple in the past, and to my surprise, my wish was fulfilled with the introduction of iOS 17 this year. Thumbs up to Apple!&lt;/p&gt;

&lt;p&gt;Although the iPad currently does not support connecting to an Apple Watch, which means that the pairing of an Apple Watch and an iPad is still not feasible, I might have to keep hoping for this feature. Apple’s disappointing decision. However, on a positive note, you can now enjoy the convenience of using AirPods with the iPad. The screen size is sufficient to enhance your viewing experience, even when engaging in activities such as jumping.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/post/230906/ios17_2.jpg&quot; alt=&quot;iPad&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;take-control-of-your-workouts-directly-from-the-lock-screen&quot;&gt;Take control of your workouts directly from the lock screen.&lt;/h2&gt;
&lt;p&gt;In iOS 17, once a workout is initiated on the Apple Watch, YaoYao will automatically show the Dynamic Island and provide real-time activity updates on the phone. Additionally, you have the option to tap “pause/stop” on the real-time activity to manage your training. Apple 👍&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/post/230906/ios17_3.jpg&quot; alt=&quot;LiveActivity&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;standby-display&quot;&gt;Standby Display&lt;/h3&gt;
&lt;p&gt;If the phone is being charged while working out, YaoYao will use the iOS 17 Standby feature to show information.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/post/230906/ios17_4.jpg&quot; alt=&quot;StandBy&quot; /&gt;
I am thoroughly impressed with Apple’s Standby feature, which maximizes the functionality of the iPhone. It reminds me of how the watch conveniently displays the time while being charged, representing a remarkably natural and considerate design. Apple, thumbs up!&lt;/p&gt;

&lt;h2 id=&quot;smartstack-on-watchos&quot;&gt;SmartStack on watchOS&lt;/h2&gt;
&lt;p&gt;In watchOS 10, a new feature was introduced that allows you to swipe up from the bottom of the watch face to display SmartStack, which is similar to the previous Siri watch face. Prior to this update, there were only a limited number of slots available on the watch face for complex functions. However, with this new feature, you can add more functions to it. YaoYao also supports this display option, similar to the example shown above.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/post/230906/watchos10_1.jpg&quot; alt=&quot;watchOS10&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Personally, I prefer to have different apps on separate watch faces in order to address this issue. However, the current method of switching watch faces through a long press is less convenient than it used to be. This aspect of Apple’s design could be improved.&lt;/p&gt;

&lt;h3 id=&quot;go-update-to-ios-17-and-the-app&quot;&gt;Go update to iOS 17 and the app!&lt;/h3&gt;

&lt;p&gt;If you have any other constructive suggestions, please feel free to let me know.&lt;/p&gt;
</description>
        <pubDate>Wed, 06 Sep 2023 00:00:00 +0800</pubDate>
        <link>http://blog.yaoyaojumprope.com/blog/2023/09/06/yaoyao_ios17_en/</link>
        <guid isPermaLink="true">http://blog.yaoyaojumprope.com/blog/2023/09/06/yaoyao_ios17_en/</guid>
        
        <category>Coding</category>
        
        
        <category>blog</category>
        
      </item>
    
  </channel>
</rss>
