<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">

  <title><![CDATA[肇鑫的技术博客]]></title>
  <link href="https://zhaoxin.pro/technology/atom.xml" rel="self"/>
  <link href="https://zhaoxin.pro/technology/"/>
  <updated>2026-05-05T11:51:50+08:00</updated>
  <id>https://zhaoxin.pro/technology/</id>
  <author>
    <name><![CDATA[]]></name>
    
  </author>
  <generator uri="http://www.coderforart.com/">CoderForArt</generator>

  
  <entry>
    <title type="html"><![CDATA[处理copilot update更新缓慢问题获得的两个意外收获]]></title>
    <link href="https://zhaoxin.pro/technology/17784526707310.html"/>
    <updated>2026-05-11T06:37:50+08:00</updated>
    <id>https://zhaoxin.pro/technology/17784526707310.html</id>
    <content type="html"><![CDATA[
<p>今早发现copilot update更新的时候突然变慢了，速度只有10KB/s，于是跑去找ChatGPT问了问，为什么我终端已经设置了all_proxy，但是copilot update的时候却没有使用？</p>
<p>ChatGPT和我说，这是因为copilot这种有时候只会使用环境变量，我需要设置的是http_proxy和https_proxy。我设置好之后，更新了copilot，这下有500KB/s的速度了。</p>
<h2><a id="%E4%B8%A4%E4%B8%AA%E6%84%8F%E5%A4%96%E6%94%B6%E8%8E%B7" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>两个意外收获</h2>
<p>再次打开copilot，对话后我发现，默认的模型变成sonnet-4.6了。我很意外！因为我是中国用户的原因，很久以前就没法使用任何Claude的模型，只能使用OpenAI的。之前我虽然听说如果VPN开启全局模式也能做到，但是我嫌麻烦，就没有弄。</p>
<p>此外，我还发现，现在copilot响应速度快了很多。之前我就觉得copilot的gpt-5.4比OpenAI Codex app里的慢很多。我还以为是copilot的问题。因为我问ChatGPT为啥copilot比Codex的慢，它还给我解释的头头是道，什么copilot搞了负载均衡，额外会加载其他内容，还有Codex可能是独享的，copilot的是共享的。不过从这次改变可以知道，其实原因并不是ChatGPT说的那样，就是因为原本的代理设置不被copilot支持导致的。我们应该警惕大模型的回复，有的时候它说的也不一定对。</p>

]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[中国开发者测试Google Play应用内购买的正确方法]]></title>
    <link href="https://zhaoxin.pro/technology/17769895238695.html"/>
    <updated>2026-04-24T08:12:03+08:00</updated>
    <id>https://zhaoxin.pro/technology/17769895238695.html</id>
    <content type="html"><![CDATA[
<p>作为一名苦逼的中国开发者，我们要想测试发布到Google Play的商店，会收到中美双方的排挤。本文是我通过调查资料，最终总结的一个我认为最适合的方式，希望后来者可以少走弯路。</p>
<h1><a id="%E5%BC%80%E5%8F%91%E8%80%85%E8%B4%A6%E5%8F%B7" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>开发者账号</h1>
<p>你需要一个开发者账户，一个测试账号。这是因为作为收钱的开发者账户，我们需要使用自己的真实信息，这样才能通过Google的开发者账户的验证。因此，开发者账户是中国账户，加上单Visa的信用卡即可，我使用的是招商银行的单Visa的全币卡。你还需要找银行要一下对账单，准备好身份证，这些都是Google验证时需要的。</p>
<h1><a id="%E6%B5%8B%E8%AF%95%E8%B4%A6%E6%88%B7" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>测试账户</h1>
<p>测试账户需要新建一个美国账户。感谢AI，ChatGPT告诉我，Google账户是根据你注册时的IP来判断你属于哪个国家的。所以，我们主要开启一个美国的VPN，然后注册就可以。特别的，注册时会要求手机号来验证。很多人担心能否用中国的手机号来验证，没问题的。因为这个只是验证你是真人，不作为判断国别的依据。因此，只要你注册时的IP是美国，以后也一直使用这个IP，就没问题。</p>
<p>此外，由于我们的测试账户只是为了测试。并且我们也没有美国的信用卡用来支付，所以我们的账户将不绑定任何信用卡。以免因为绑定了中国的信用卡而导致账户被风控。</p>
<h1><a id="%E6%B5%8B%E8%AF%95%E5%BA%94%E7%94%A8" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>测试应用</h1>
<ol>
<li>上传应用到Google Play</li>
<li>选择内部测试</li>
<li>然后在内部测试中，创建一个新的组，将测试账户的邮箱添加上去</li>
<li>复制定下的链接，用手机登录测试账户，打开这个链接</li>
<li>安装测试应用，就可以测试了。</li>
</ol>
<h1><a id="%E5%85%B6%E4%BB%96%E9%97%AE%E9%A2%98" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>其他问题</h1>
<p>如果测试时，在选择“快速卡，一直通过”之后遇到问题，那是因为你的VPN选择的是规则，导致Play商店和Google服务的IP不一致。解决办法就是关掉应用，然后将VPN的规则改为全局，然后重新测试。这样就可以了。</p>
<h1><a id="%E6%9C%80%E5%90%8E" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>最后</h1>
<p>我们采用这种方式，是没办法的的办法。因为我们中国的开发者账户直接用来测试，经常会遇到“设备或者账户不支持支付”这类的问题。那样的话，我们就没有办法测试应用内购买是否正确了。</p>

]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[第三方应用使用DocumentGroup时，模仿Pages的工具栏]]></title>
    <link href="https://zhaoxin.pro/technology/17756134186479.html"/>
    <updated>2026-04-08T09:56:58+08:00</updated>
    <id>https://zhaoxin.pro/technology/17756134186479.html</id>
    <content type="html"><![CDATA[
<p>最近，我在开发iOS的Markdown应用的时候遇到一个问题，应用是使用DocumentGroup开发的，但是默认会显示标题，从而导致留给工具栏的空间太小，工具栏的图标经常被折叠起来。</p>
<p><img src="media/17756134186479/IMG_1181.jpeg" alt="IMG_1181" /></p>
<p>我一开始只是想把后退去掉，换成三个点的菜单，然后去掉标题。结果发现后退去不掉。于是我就想看看苹果自己是如何做的。于是我打开了苹果自己的Notes和Numbers。不过Notes其实是有内部库的，于是主要参考的Numbers。</p>
<p><img src="media/17756134186479/IMG_1184.jpeg" alt="IMG_1184" /></p>
<p>我把Numbers截图给AI，说想要弄一个这个风格的。结果AI搞不定。它虽然设置了标题为空，但是标题始终显示。于是我有把这个描述给Grok，Grok说，这是因为DocumentGroup自动包含了一层Navigation，你需要将它禁用。结果我禁用了，还是不行。又去问Grok，它又说，这是因为iOS的某些版本，有bug，不执行这个操作。但是网友总结了三种办法，1，2，3，然后挨个试。结果第一种就可以了。</p>
<p>这个是我应用最终的效果。</p>
<p><img src="media/17756134186479/IMG_1205.jpeg" alt="IMG_1205" /></p>
<p>最核心的代码只需要如下的部分。</p>
<p><img src="media/17756134186479/%E6%88%AA%E5%B1%8F2026-04-12%2010.50.33.png" alt="截屏2026-04-12 10.50.33" /></p>

]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[Swift Package应用签名与公证实战总结]]></title>
    <link href="https://zhaoxin.pro/technology/17716310437233.html"/>
    <updated>2026-02-21T07:44:03+08:00</updated>
    <id>https://zhaoxin.pro/technology/17716310437233.html</id>
    <content type="html"><![CDATA[
<h2><a id="%E9%97%AE%E9%A2%98%E8%83%8C%E6%99%AF" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>问题背景</h2>
<p>fork 了一个第三方 macOS 应用，原 release 版本未签名，但提供了源码，为Swift Package格式，需要重新签名并公证后分发。</p>
<hr />
<h2><a id="%E9%81%87%E5%88%B0%E7%9A%84%E5%9D%91" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>遇到的坑</h2>
<h3><a id="1-swift-package-manager%E6%97%A0%E6%B3%95%E5%90%AF%E7%94%A8-hardened-runtime" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>1. Swift Package Manager 无法启用 Hardened Runtime</h3>
<p><strong>问题</strong>：使用 <code>swift build</code> 构建的可执行文件，无法通过签名添加 hardened runtime。公证时一直报错：</p>
<pre class="line-numbers"><code class="language-plain_text">The executable does not have the hardened runtime enabled.
</code></pre>
<p><strong>原因</strong>：Swift Package Manager 构建时不会启用 hardened runtime，签名时添加也不被认可。</p>
<p><strong>解决</strong>：使用 XcodeGen 生成 Xcode 项目，在项目配置中启用。</p>
<h3><a id="2-apple-development%E8%AF%81%E4%B9%A6%E6%97%A0%E6%B3%95%E5%85%AC%E8%AF%81" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>2. Apple Development 证书无法公证</h3>
<p><strong>问题</strong>：最初只有 Apple Development 证书（用于开发调试），公证时报错：</p>
<pre class="line-numbers"><code class="language-plain_text">The binary is not signed with a valid Developer ID certificate.
</code></pre>
<p><strong>原因</strong>：公证必须使用 <strong>Developer ID Application</strong> 证书，不能用 Apple Development。</p>
<p><strong>解决</strong>：在 Apple Developer 账号中添加 Developer ID Application 证书。</p>
<h3><a id="3%E6%97%B6%E9%97%B4%E6%88%B3%E9%97%AE%E9%A2%98" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>3. 时间戳问题</h3>
<p><strong>问题</strong>：签名缺少安全时间戳：</p>
<pre class="line-numbers"><code class="language-plain_text">The signature does not include a secure timestamp.
</code></pre>
<p><strong>解决</strong>：在签名配置中添加 <code>--timestamp</code> 参数。</p>
<h3><a id="4-get-task-allow-entitlement%E9%97%AE%E9%A2%98" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>4. get-task-allow entitlement 问题</h3>
<p><strong>问题</strong>：分发版本包含开发用 entitlement：</p>
<pre class="line-numbers"><code class="language-plain_text">The executable requests the com.apple.security.get-task-allow entitlement.
</code></pre>
<p><strong>解决</strong>：在 entitlements 配置中明确设置 <code>com.apple.security.get-task-allow: false</code>。</p>
<hr />
<h2><a id="%E7%BB%8F%E9%AA%8C%E6%95%99%E8%AE%AD" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>经验教训</h2>
<h3><a id="%E8%AF%81%E4%B9%A6%E7%B1%BB%E5%9E%8B" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>证书类型</h3>
<table>
<thead>
<tr>
<th>证书类型</th>
<th>用途</th>
</tr>
</thead>
<tbody>
<tr>
<td>Apple Development</td>
<td>开发调试，不能公证</td>
</tr>
<tr>
<td>Developer ID Application</td>
<td>分发和公证 ✅</td>
</tr>
</tbody>
</table>
<h3><a id="%E7%AD%BE%E5%90%8D%E9%85%8D%E7%BD%AE%E8%A6%81%E7%82%B9" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>签名配置要点</h3>
<ol>
<li><strong>Hardened Runtime</strong> - 必须启用</li>
<li><strong>时间戳</strong> - 必须添加</li>
<li><strong>entitlements</strong> - 分发版本禁止包含 get-task-allow</li>
<li><strong>使用 Manual 签名</strong> - 避免自动签名冲突</li>
</ol>
<hr />
<h2><a id="%E6%9C%80%E4%BD%B3%E5%AE%9E%E8%B7%B5%EF%BC%88%E7%AE%80%E5%8D%95%E6%9C%89%E6%95%88%EF%BC%89" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>最佳实践（简单有效）</h2>
<h3><a id="1%E4%BD%BF%E7%94%A8-xcode%E9%A1%B9%E7%9B%AE%E8%80%8C%E9%9D%9E-spm" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>1. 使用 Xcode 项目而非 SPM</h3>
<pre class="line-numbers"><code class="language-bash"># 安装 XcodeGen
brew install xcodegen

# 配置 project.yml（关键设置）
targets:
  YourApp:
    settings:
      base:
        ENABLE_HARDENED_RUNTIME: YES
        CODE_SIGN_STYLE: Manual
        CODE_SIGN_IDENTITY: &quot;Developer ID Application: Your Name (TEAM_ID)&quot;
        OTHER_CODE_SIGN_FLAGS: &quot;--timestamp&quot;
    entitlements:
      properties:
        com.apple.security.get-task-allow: false

# 生成项目
xcodegen generate

# 构建
xcodebuild -project YourApp.xcodeproj -scheme YourApp -configuration Release build
</code></pre>
<h3><a id="2%E9%AA%8C%E8%AF%81%E7%AD%BE%E5%90%8D" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>2. 验证签名</h3>
<pre class="line-numbers"><code class="language-bash">codesign -dvv YourApp.app/Contents/MacOS/YourApp
</code></pre>
<p>确认输出包含：</p>
<ul>
<li><code>flags=0x10000(runtime)</code> - hardened runtime 已启用</li>
<li><code>Timestamp=...</code> - 有时间戳</li>
</ul>
<h3><a id="3%E5%85%AC%E8%AF%81%E6%B5%81%E7%A8%8B" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>3. 公证流程</h3>
<pre class="line-numbers"><code class="language-bash"># 1. 创建 zip
zip -r YourApp.zip YourApp.app

# 2. 提交公证（需要 App 专用密码）
xcrun notarytool submit YourApp.zip \
  --apple-id &quot;your@email.com&quot; \
  --password &quot;app-specific-password&quot; \
  --team-id &quot;TEAM_ID&quot;

# 3. 等待完成
xcrun notarytool wait &lt;submission-id&gt; --apple-id &quot;your@email.com&quot; --password &quot;password&quot; --team-id &quot;TEAM_ID&quot;

# 4. 附加票据
xcrun stapler staple YourApp.app
</code></pre>
<hr />
<h2><a id="%E6%80%BB%E7%BB%93" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>总结</h2>
<p><strong>最简单有效的方式</strong>：</p>
<ol>
<li>用 <strong>XcodeGen</strong> 生成 Xcode 项目（不要用纯 SPM）</li>
<li>在 <code>project.yml</code> 中配置好签名和 hardened runtime</li>
<li>用 <strong>xcodebuild</strong> 构建</li>
<li>用 <strong>notarytool</strong> 公证</li>
</ol>
<p>不要试图用 <code>swift build</code> + 手动签名的方式来处理需要公证的分发版本，SPM 的构建产物不兼容。</p>

]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[NavigationSplitView的一些奇技淫巧]]></title>
    <link href="https://zhaoxin.pro/technology/17614393407502.html"/>
    <updated>2025-10-26T08:42:20+08:00</updated>
    <id>https://zhaoxin.pro/technology/17614393407502.html</id>
    <content type="html"><![CDATA[
<h2><a id="sidebar%E6%8C%89%E9%92%AE%E9%9A%90%E8%97%8F%E8%B5%B7%E6%9D%A5%E7%9A%84%E6%AD%A3%E7%A1%AE%E5%81%9A%E6%B3%95" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>Sidebar按钮隐藏起来的正确做法</h2>
<p>使用</p>
<pre class="line-numbers"><code class="language-swift">.toolbar(removing: .sidebarToggle)
</code></pre>
<p>可以隐藏侧边栏切换按钮。但是注意，这个不是放在作用在<code>NavigationSplitView</code>上面的。而不是作用在sidebar的视图上面。即必须按照如下的方式使用：</p>
<pre class="line-numbers"><code class="language-swift">NavigationSplitView(columnVisibility: $columnVisibility) {
    sidebar
        .toolbar(removing: .sidebarToggle)
    } content: {
    content
    } detail: {
    detail
}
</code></pre>
<h2><a id="%E9%9A%90%E8%97%8Fsidebar%E7%9A%84%E6%AD%A3%E7%A1%AE%E5%81%9A%E6%B3%95" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>隐藏Sidebar的正确做法</h2>
<p><code>NavigationSplitView</code>是三栏的，但是如果你要使用两栏，则有两种方式。</p>
<h3><a id="%E4%BD%BF%E7%94%A8sidebar" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>使用sidebar</h3>
<p>使用sidebar的好处是，sidebar可以隐藏，也可以显示。<br />
使用sidebar+detail的方式。</p>
<h3><a id="%E4%B8%8D%E4%BD%BF%E7%94%A8sidebar" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>不使用sidebar</h3>
<p>如果你希望永远显示为两栏，不希望隐藏任何一个栏隐藏，那么使用此方式。</p>
<pre class="line-numbers"><code class="language-swift">NavigationSplitView(columnVisibility: .constant(.doubleColumn)) {
    EmptyView()
        .toolbar(removing: .sidebarToggle)
    } content: {
    content
    } detail: {
    detail
}
</code></pre>
<p>这个的重点在于，通过sidebar设置隐藏了切换sidebar按钮。同时，选择两栏模式，默认不显示sidebar。这样sidebar就无法被调用出来了。并且一直是两栏。</p>
<h2><a id="%E9%85%8D%E5%90%88settings%E4%BD%BF%E7%94%A8" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>配合Settings使用</h2>
<p>在使用Settings时候，包含sidebar的<code>NavigationSplitView</code>的sidebar按钮会导致出现问题。因此，必须使用上面的第二种方式，隐藏sidebar，并且使用一直存在的两栏。</p>

]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[macOS在工具栏上切换页面，TabView和Picker怎么选？]]></title>
    <link href="https://zhaoxin.pro/technology/17598944718603.html"/>
    <updated>2025-10-08T11:34:31+08:00</updated>
    <id>https://zhaoxin.pro/technology/17598944718603.html</id>
    <content type="html"><![CDATA[
<p>目前的SwiftUI，如果你希望通过工具栏的按钮直接切换标签，可以使用TabView，也可以使用Picker，不过在细节上，二者有所不同。</p>
<h2><a id="tabview" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>TabView</h2>
<p>TabView是最简单的方式，只要你应用的根视图为TabView，那么你的系统架构会自动转成Navigation Tab Bar的方式，自动在工具栏显示Tab的标签。</p>
<p>不过这个方式有一种缺陷，就是应用的标题不会在工具栏上显示。如果你想强制限制，必须引入AppKit然后，覆盖Window的相应设置。</p>
<h2><a id="picker" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>Picker</h2>
<p>使用Picker的方式则更为友好。之需要在toolbar里添加Picker，然后使用segment样式，就可以获得同样的显示效果。然后使用Switch切换视图即可。</p>
<p>这么做不如直接使用TabView简单，但是可定制化强，并且不会影响标题的显示。</p>
<h2><a id="%E7%BB%93%E8%AE%BA" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>结论</h2>
<p>如果不需要设置标题栏，使用TabView更为简便。否则就使用Picker，这样效果更好。</p>

]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[macOS系统菜单栏显示多图标的两种方式]]></title>
    <link href="https://zhaoxin.pro/technology/17591091125785.html"/>
    <updated>2025-09-29T09:25:12+08:00</updated>
    <id>https://zhaoxin.pro/technology/17591091125785.html</id>
    <content type="html"><![CDATA[
<p>一直以来都是使用的菜单栏单图标的方式。今天心血来潮，想在Focus原本的图标旁边新增一个刷新的图标，可以用来快速重置计时器。</p>
<p>把任务分配给AI，AI很快完成了第一版。我运行一看，怎么没看到新图标，再仔细一找，的确是两个图标了，但是两个图标是各自独立的，彼此之间还间隔了几个其它的图标。这和我看到的不一样，我其实希望的是像音乐播放器那样的，几个图标一体的方式。于是我和AI说明了要一体的。AI表示了解，然后重新生成了代码。AI特意解释说，一体之后，点击时就需要用点击的位置进行二次判断，还确定用户点击的是哪个图标。</p>
<p>最终结果我实验了一下，的确和音乐播放器的一样。看来他们也是这么做的。</p>

]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[SwiftUI应用伴随系统登录自动启动后显示macOS应用的窗口的办法]]></title>
    <link href="https://zhaoxin.pro/technology/17591090521053.html"/>
    <updated>2025-09-29T09:24:12+08:00</updated>
    <id>https://zhaoxin.pro/technology/17591090521053.html</id>
    <content type="html"><![CDATA[
<p>我们知道SwiftUI应用本身没有应用窗口的概念，它是使用WindowGroup来自动管理窗口的。这在用户手动启动应用的时候没有问题。但是如果你设置了应用伴随系统登录后自动启动后，SwiftUI的应用会存在一些问题。</p>
<p>这是因为这种方式启动应用后，SwiftUI的应用不会主动创建窗口，视图会在用户手动点击应用之后才创建。而这可能不是我们所需要的。因为如果我们有使用onAppear来执行一些代码。我们实际上是希望代码可以在应用启动时就运行，而这个机制会导致执行会推迟到用户点击时。</p>
<p>因此，我们需要保证即便SwiftUI没有生成Window，也要自己主动来生成Window。</p>
<h2><a id="%E9%97%AE%E9%A2%98%E5%88%86%E6%9E%90" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>问题分析</h2>
<p>要解决这个问题，我们首先要了解这个启动的整个过程，然后才能知道如何来改进。具体调试的过程我就不讲了，最终我确认启动的过程是这样的：</p>
<ol>
<li>用户登录。</li>
<li>应用启动。</li>
<li>应用在后台启动，但是没有主窗口，因此无法自动切换到前台。</li>
</ol>
<p>我们可以使用<code>NSApplication.shared.windows.count</code>来判断。如果是用户手动打开的应用，SwiftUI会创建SwiftUI.AppKitWindow的窗口。而如果是跟随系统登录后后台启动，则不会有这个窗口。</p>
<p>我使用的判断函数是这个：</p>
<pre class="line-numbers"><code class="language-swift">func isLaunchedAtLogin() -&gt; Bool {
  NSApplication.shared.windows.count &lt; 2
}
</code></pre>
<p>之所以用2，而不是1判断。是因为我还使用了菜单栏图标，菜单栏图标使用的是NSStatusItem，因此还会包含一个叫NSStatusWindow的窗口。</p>
<h2><a id="%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>解决方案</h2>
<pre class="line-numbers"><code class="language-swift">func applicationDidFinishLaunching(_ notification: Notification) {
    createContentViewWindow()
}

/// 用户启动时SwiftUI的窗口会先创建，因此window不会为nil，但是有可能存在延迟，因为是通过.updateWindow还获取的。
/// 所以这里使用其他的方式进行判断，而不是使用window是否为nil
private func createContentViewWindow() {
  /// 若是用户点击，会有两个窗口，一个是SwiftUI创建的主窗口，一个status窗口。后者应该是对应菜单栏图标的。
  /// 如果是伴随系统启动，则SwiftUI创建的主窗口不存在，只有status的窗口。
  func isLaunchedAtLogin() -&gt; Bool {
    NSApplication.shared.windows.count &lt; 2
  }

  if isLaunchedAtLogin() {
    let contentView = ContentView()
      .environment(\.managedObjectContext, ModelProvider.shared.container.viewContext)
    let window = NSWindow(
      contentRect: NSRect(x: 0, y: 0, width: 400, height: 240),
      styleMask: [.titled, .closable, .miniaturizable, .resizable],
      backing: .buffered, defer: false)

    window.setFrameAutosaveName(&quot;Main Window&quot;)
    window.contentView = NSHostingView(rootView: contentView)
    window.center()
    window.makeKeyAndOrderFront(nil)
    self.window = window
  }
}
</code></pre>
<h2><a id="%E5%B0%8F%E7%BB%93" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>小结</h2>
<p>传统上，如果我们使用loginItem来实现应用伴随系统启动，那么为了区分是用户手动启动，还是伴随系统启动，需要使用传递参数的方式。但是传递参数，就需要使用额外的launcher辅助应用。</p>
<p>设置辅助应用的步骤是很复杂的。因此，我们现在大多都是直接使用下面的代码来直接使用自动伴随系统登录启动。</p>
<pre class="line-numbers"><code class="language-swift">try SMAppService.mainApp.register()
</code></pre>
<p>这个办法虽然大大简化了设置系统启动后启动应用的步骤。但是这么做之后，由于没有辅助应用，也就没法使用传递参数的办法了。</p>
<p>本文给出的使用<code>NSApplication.shared.windows.count</code>，用窗口数量来间接判断的方式，利用了SwiftUI在后台启动后，不会主动创建窗口的特性，解决了这个问题。</p>

]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[解决Xcode Cloud无法enable Swift Package包中的宏的问题]]></title>
    <link href="https://zhaoxin.pro/technology/17590454903830.html"/>
    <updated>2025-09-28T15:44:50+08:00</updated>
    <id>https://zhaoxin.pro/technology/17590454903830.html</id>
    <content type="html"><![CDATA[
<p>最近我在做应用适配iOS/macOS 26的特性。今天在Xcode Cloud打包的时候遇到打包失败的错误。</p>
<blockquote>
<p>Macro “DefaultsMacrosDeclarations” from package “Defaults” must be enabled before it can be used.</p>
</blockquote>
<p>这个问题是我应用所使用的第三方的库 “Defaults”在其内部使用了宏。这个宏在Xcode本地编译时，需要用户手动点击确认才能继续。但是Xcode Cloud中，没有点击确认的位置。因此，就无法完成打包应用的过程。</p>
<h2><a id="%E8%A7%A3%E5%86%B3%E5%8A%9E%E6%B3%95" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>解决办法</h2>
<p>通过运行脚本的方式，在克隆完文件夹之后，运行脚本，规避掉对于宏的验证。</p>
<pre class="line-numbers"><code class="language-sh">#!/bin/sh 
defaults write com.apple.dt.Xcode IDESkipMacroFingerprintValidation -bool YES
</code></pre>
<p>必须在Xcode中的根部位置，创建一个新组，命名为ci_scripts，然后在这个组中创建ci_post_clone.sh，内容是上面的内容。</p>
<blockquote>
<p>必须在Xcode的根部位置创建组，并且命名也不能错。</p>
</blockquote>
<h3><a id="%E5%B0%8F%E6%8F%92%E6%9B%B2" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>小插曲</h3>
<p>我其实最开始是像GPT 4.1提出了这个问题。GPT 4.1的解答只对了一半。它提出了创建文件夹和脚本，文件夹是正确的，脚本名字是错误的。并且它也没有告诉需要在Xcode中创建组，而只是说在项目的根目录创建就可以。最后，它创建的脚本内容不完全正确。</p>
<p>之后我使用了Google搜索。Google搜索默认的AI总结的是正确的，但应该就是从stackoverflow里的答案总结的。我最后是看的SO里的回答，进行的总结。</p>
<p>另外，我建议你完整阅读下面的第一个引用。我使用了里面最为简便的方案。而非最安全的。也许你看了之后，会选择一条不同的手段。</p>
<h2><a id="%E5%BC%95%E7%94%A8%E6%96%87%E7%8C%AE" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>引用文献</h2>
<p><a href="https://stackoverflow.com/questions/77267883/how-do-i-trust-a-swift-macro-target-for-xcode-cloud-builds/77312559#77312559">How do I trust a swift macro target for Xcode Cloud builds?</a></p>
<p><a href="https://developer.apple.com/documentation/xcode/writing-custom-build-scripts">Writing custom build scripts</a></p>

]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[什么？AccentColor又闹幺蛾子了？]]></title>
    <link href="https://zhaoxin.pro/technology/17590279465918.html"/>
    <updated>2025-09-28T10:52:26+08:00</updated>
    <id>https://zhaoxin.pro/technology/17590279465918.html</id>
    <content type="html"><![CDATA[
<p>一年以前，我就踩过一次AccentColor的坑。没想到，一年之后我又掉进来了。</p>
<blockquote>
<p><a href="17256661738520.html">SwiftUI下，TextField诡异失去Focus下样式的问题</a></p>
</blockquote>
<p>事情的起因是这样的。因为苹果的新系统发布了嘛。我不能免俗的也要改进我之前的应用，添加对于新系统特性的一些支持之类的。</p>
<p>但是我在修改代码后测试时发现，当使用ZStack模拟弹窗之后，弹窗后面的视图的颜色会出错。我一开始以为是ZStack的问题，于是将模拟弹窗改成了.fullScreenCover的方式。这个问题在当时看起来时解决了。但是今天我在使用中发现，这个问题又重新出现了。</p>
<p>于是我在Google上搜索了一下，没想到这还是一个SwiftUI长期存在的一个问题。</p>
<blockquote>
<p><a href="https://stackoverflow.com/questions/77550885/how-do-i-stop-the-accentcolor-from-turning-gray-when-a-sheet-is-being-presented/78128099#78128099">How do I stop the AccentColor from turning Gray when a sheet is being presented?</a></p>
</blockquote>
<p>原来在SwiftUI中实际使用时。原本应该一致的Color.accentColor和Color(&quot;AccentColor&quot;)在实际使用中是不一致的。说得更具体些，就是Color.accentColor会使用应用设置和用户的系统设置。而Color(&quot;AccentColor&quot;)则是将AccentColor作为颜色资源从Asset文件夹直接读取。因此，虽然它的名字也叫&quot;AccentColor&quot;，但是实际上它只是名字叫&quot;AccentColor&quot;的一个颜色，你改成别的名字，比如&quot;MyAppColor&quot;也是一样的。虽然这样会失去Color.accentColor一些独特的个性，但是能保证颜色的一致，即颜色不会莫名其妙的改变。</p>
<p>系统弹sheet的时候，Color.accentColor会改变的问题，应该就是sheet本身可能存在某种机制，将应用内设置的Color.accentColor从Asset文件夹设置的内容，改成了系统默认设置的内容。比如下图红色圈起来的部分，就是系统允许用户自定义用户AccentColor偏好的地方。</p>
<p><img src="media/17590279465918/accent_color.jpg" alt="accent_color" /></p>

]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[大模型又暴露了……]]></title>
    <link href="https://zhaoxin.pro/technology/17572405061386.html"/>
    <updated>2025-09-07T18:21:46+08:00</updated>
    <id>https://zhaoxin.pro/technology/17572405061386.html</id>
    <content type="html"><![CDATA[
<p>这真的不是危言耸听，这是真的骇人听闻。盘点一下我遇到大模型的智障行为，本文不定期更新。</p>
<h2><a id="%E5%A4%A7%E6%A8%A1%E5%9E%8B%E8%87%B3%E4%BB%8A%E4%B8%8D%E4%BA%86%E8%A7%A3swiftui%E7%9A%84%E6%9B%B4%E6%96%B0%E6%9C%BA%E5%88%B6%EF%BC%8C%E5%BF%85%E9%A1%BB%E8%A6%81%E4%B8%A5%E6%A0%BC%E7%BA%A6%E6%9D%9F%EF%BC%8C%E5%B0%8F%E5%BF%83%E4%BD%BF%E7%94%A8" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>大模型至今不了解SwiftUI的更新机制，必须要严格约束，小心使用</h2>
<p>今天遇到了一个sheet弹窗之后，视图更新不同步的问题。由于我已经在提示词中，告诉了我倾向使用@Observable，而不是旧版的ObservableObject，所以一开始大模型创建了正确的@Observable class，然后在视图中使用@State创建了这个model的实例。</p>
<p>但是为什么还是出错了呢？那就要看使用时更具体的实现了。在开启sheet之前，大模型调用了一个准备函数，在这个函数中，对于model进行了一系列的初始化。具体的步骤是这样的，先新建一个model，然后修改它，然后用这个修改完成的model，替换为系统@State里的那个model。</p>
<p>我首先将问题描述给大模型，sheet首次打开的时候，显示的界面并不符合预期，应该有值的地方，实际上为空。但是如果关掉，再次打开，就又正确了。</p>
<p>大模型思考了一番，说这可能是sheet的机制造成，sheet有时会提前锁定一些值，这会造成打开视图时的不同步。它的建议是，在sheet打开的视图内部的onAppear中新增一个fallback的调用，重新检查并赋值。然后还说，如果这样还不行，可以考虑使用GCD延迟的方式，也就是使用DispatchQueme.main.async调用来实现改动。</p>
<p>试了，onAppear的确不行。但是我没有继续使用它建议的GCD延迟的方式。我问它是否还有其它的办法。</p>
<p>大模型想了想，说可以考虑将@Observable的class改成纯粹的struct的方式。然后就其次咔嚓地修改起来，我一看改动后的代码，妈呀！连mutating都搞出来了。SwiftUI的代码哪有这么写的啊？我果断点了Undo，取消了这次修改。</p>
<h3><a id="%E6%AD%A3%E7%A1%AE%E7%9A%84%E6%96%B9%E5%BC%8F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>正确的方式</h3>
<p>我再次看了一下代码，突然我发现了问题的所在。前面提到，在准备函数中，大模型的改动方式是“先新建一个model，然后修改它，然后用这个修改完成model，替换为系统@State里的那个model。”这个方式在SwiftUI中显然是错误的。SwfitUI中@State的class类型，你不能替换它，因为一替换之前的跟踪就被中断了。</p>
<p>所以我和大模型说，我们不应该“新建model，修改，然后替换@State”，而是应该直接修改@State中的变量。</p>
<p>大模型想了想，觉得我说得有道理，就修改了。然后还跟我说，如果我确定这个好用，那么onAppear那里的代码，就可以删掉了。</p>
<p>于是我先注释掉onAppear那里的代码，然后运行应用，果然一切都正常了。</p>
<h3><a id="%E5%B0%8F%E7%BB%93" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>小结</h3>
<p>大模型并没有真正了解SwiftUI的更新机制，这部分的代码写的时候，它更像是一个拙略的模仿者。虽然这部分，我之前的领悟也不怎么深刻，但是我随着持续不断地学习和使用SwiftUI，现在在这方面，我有信心可以说，我可比大模型强多了。</p>
<h2><a id="%E5%A4%A7%E6%A8%A1%E5%9E%8B%E4%B8%8D%E6%93%85%E9%95%BF%E9%87%8D%E6%9E%84" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>大模型不擅长重构</h2>
<p>这个说法听起来是反直觉的。大模型相比于人类，应该更擅长重构吧。为什么说它不擅长呢？</p>
<p>事实如此。昨天我想重构一个900多行的SwiftUI的文件，里面包含一个主视图，主视图下的视图组件，以及一个弹出视图。如果是开发者来重构这个文件，之需要新建文件，然后复制，粘贴，再删掉旧的就可以了。可以很快就完成。</p>
<p>但是我用大模型来重构。却接连几次失败。过程一般是这样。一开始大模型计划的挺好。</p>
<ul>
<li>我将会将文件拆分为以下几个文件。</li>
<li>然后开始拆分，但是执行到5、6个的时候。</li>
<li>弹出出错提示，上下文已经耗尽了。</li>
</ul>
<p>我为了节省上下文，又单独增加了对于上下文节省的办法。每次重构一个文件，复制、粘贴、删除完了，验证有没有错误。有错误修复，没有再继续。并且可以抛弃掉使用过的上下文。</p>
<p>再次运行，稍微好了些，多实现几个文件，但是最后还是因为上下文不够而失败了。</p>
<h3><a id="%E5%B0%8F%E7%BB%93" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>小结</h3>
<p>大模型不擅长重构，不是因为它无法规划重构。而是因为大模型重构时需要考虑大量的上下文信息，最终会因为上下文耗尽而无法继续。</p>

]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[苹果表无法解锁macOS问题的一个奇葩的解决办法]]></title>
    <link href="https://zhaoxin.pro/technology/17557696976538.html"/>
    <updated>2025-08-21T17:48:17+08:00</updated>
    <id>https://zhaoxin.pro/technology/17557696976538.html</id>
    <content type="html"><![CDATA[
<p>我实在无法找到更适当的词，只能用奇葩来形容这个解决办法。</p>
<p>我开始使用macOS 26 beta系列也有一段时间了，目前在使用的最新版本是beta 7。长久以来，我一直遇到一个有些奇怪的问题，就是每次系统睡眠之后唤醒，苹果表的解锁总是失败。但是进入系统之后，在需要苹果表解锁的其它情况下，比如打开密码应用或者钥匙串，双击苹果表侧键解锁的这个功能却总是能成功。</p>
<p>不过苹果表解锁也不总是失败。我有两台Mac mini，一台是M1，一台是M4，M1的解锁就总是成功，而M4这台就总是失败。</p>
<h2><a id="%E5%B0%9D%E8%AF%95%E8%A7%A3%E5%86%B3%E8%BF%99%E4%B8%AA%E9%97%AE%E9%A2%98" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>尝试解决这个问题</h2>
<p>今天我突发奇想，想要在AI的辅助下解决这个问题。我首先怀疑的时候VPN软件。因为更新到最新的macOS 26 beta之后，原来使用Clash X Pro不好用了，我不得不换成了Clash Verge rev。我问AI有没有可能是VPN软件造成的。AI说有可能。让我关了它再试。我试了。还是一样。</p>
<p>之后我又和AI一起开启了macOS控制台应用，想尝试通过读取日志来找到问题的解决方案。最后发现了一个loginwindow的一条故障日志，故障（红色）是比错误（黄色）等级更高的错误。它说在创建main文件夹的某个临时文件夹时出错，可能是沙盒的问题。我当时也信了，因为我的M4是256G的，为了节省磁盘空间，我将home文件夹设置到了外置的SSD上。我想这种比较罕见的设置，可能是苹果没有考虑过。我甚至还差一点儿就去跟苹果反馈这个问题。但是后来我放弃了。因为我觉得每次输入密码也还好，不算很麻烦，就没有反馈。同时我在想，以后我再买新电脑，一定要多花些钱，买个大一些的硬盘。</p>
<h2><a id="%E5%B1%B1%E9%87%8D%E6%B0%B4%E5%A4%8D%E7%96%91%E6%97%A0%E8%B7%AF" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>山重水复疑无路</h2>
<p>的确。我没能解决这个问题。我只是放下了它。然后，我开始着手解决我的另外一个开源应用的小问题。</p>
<p>App Helper，应用助手。是我开源在GitHub上的一个macOS的助手应用。它的其中一个功能，是一键切换HDR模式，即在开启和关闭之间切换。不过，我发现，我的显示器，虽然支持HDR，但是使用不同的连接方式它对于HDR的支持不同。比如用USB-C线直接连接，就不支持HDR，用HDMI或者DisplayPort线连接，就支持。</p>
<p>所以，我的目标是通过系统API检测，在不支持HDR的时候，隐藏这个一键切换的按钮。因为我当前是USB-C的连接，不支持HDR，所以我首先完成了这部分的代码。</p>
<p>因为我还需要测试支持HDR下的部分。于是我同时使用HDMI进行连接。macOS有一个问题，就是你用两根不同的线连接同一个显示器，但是在macOS看来你就是在使用双显示器。它的显示器设置中，同时显示出两台显示器，并且不能设置禁用其中的一个。这样就比较麻烦了。因为我虽然可以通过显示器上的信号源菜单来切换到不同的接口，但是存在一个主副窗口的问题。</p>
<p>最终没办法，我只能将Mac mini上的USB-C连接显示器的那根拔了下来。将HDMI的连接作为唯一的连接，这样可以方便我进行调试。</p>
<h2><a id="%E6%9F%B3%E6%9A%97%E8%8A%B1%E6%98%8E%E5%8F%88%E4%B8%80%E6%9D%91" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>柳暗花明又一村</h2>
<p>这次HDR的选项还是不能显示。经过调试我发现，必须显示器先打开HDR模式，苹果的API才能检测出显示器支持HDR，如果没有开启，那就检测不出来。</p>
<p>那也无妨，大不了我就跟原来一样，不检测了。我又测试一键切换的功能。结果这个功能也不好用。这就比较郁闷了。因为苹果本身并没有提供切换的HDR切换的API，我使用的是脚本调用控件的方式，这个方法生效的前提是控件的位置必须固定。我管它叫数格子，脚本的写法类似，找到xx组的xx格子，然后把它上面的开关打开/关闭。现在系统升级了，位置变了。苹果💊。</p>
<p>算了，心累。我打算彻底去掉这个功能。毕竟，苹果动动手指，我就得重新数格子，并且还需要考虑不同版本macOS的兼容性，实在得不偿失。</p>
<h3><a id="%E4%BC%91%E6%81%AF%EF%BC%8C%E4%BC%91%E6%81%AF%E4%B8%80%E4%BC%9A%E5%84%BF" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>休息，休息一会儿</h3>
<p>休息结束之后，重新唤醒已经睡眠的Mac，手表传来熟悉的解锁声。我居然成功解锁了这台M4。又试了几次，无论是睡眠之后立即解锁，还是睡眠了几小时之后再解锁，都是次次成功。</p>
<p>“这是为什么呢？”（蔡明）如是说。“排除了一切不可能，那么剩下的那个无论多么不合理，就是唯一的可能。”——福尔摩斯。</p>
<p>我做的最大改动只有一点，使用HDMI连接显示器，并且拿掉了USB-C的连线。所以，这个问题的原因就是使用USB-C连接显示器，会导致苹果表无法解锁macOS。这谁能想到啊，你说是不是奇葩的解决办法？</p>
<blockquote>
<p>特别说明，这个USB-C连接显示器，就是两边都是USB-C接口。而HDMI连接，则是用Mac mini独有的HDMI接口连接显示器的HDMI接口。</p>
<p>理论上，这个USB-C除了传递数据，还能同时获得显示器传来最大90瓦的电量。不过由于Mac mini的USB-C不支持反向供电，所以没啥用。如果比MacBook，是可以同时供电的。但是不知道是不是这一点影响了苹果表的解锁。我不是硬件工程师，不敢妄言。但是觉得这个值得提一下。</p>
</blockquote>

]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[macOS 26 Beta版至今发现的兼容性问题及解决方案]]></title>
    <link href="https://zhaoxin.pro/technology/17535688834291.html"/>
    <updated>2025-07-27T06:28:03+08:00</updated>
    <id>https://zhaoxin.pro/technology/17535688834291.html</id>
    <content type="html"><![CDATA[
<ul>
<li><a href="#beta-4">Beta 4</a>
<ul>
<li><a href="#%E5%91%BD%E4%BB%A4system-profiler%E7%9A%84%E5%8F%82%E6%95%B0%E5%8F%91%E7%94%9F%E6%94%B9%E5%8F%98">命令system_profiler的参数发生改变</a></li>
<li><a href="#%E5%BA%94%E7%94%A8%E4%BC%B4%E9%9A%8F%E7%B3%BB%E7%BB%9F%E5%90%AF%E5%8A%A8%E7%9A%84%E6%96%B9%E5%BC%8F%EF%BC%8C%E4%B8%8D%E5%90%8C%E7%9A%84%E6%96%B9%E6%B3%95%E5%9C%A8%E8%AE%BE%E7%BD%AE%E4%B8%AD%E6%98%BE%E7%A4%BA%E4%B8%8D%E5%90%8C">应用伴随系统启动的方式，不同的方法在设置中显示不同</a></li>
<li><a href="#%E7%B3%BB%E7%BB%9F%E5%B8%B8%E9%A9%BB%E8%8F%9C%E5%8D%95%E6%A0%8F%E5%9B%BE%E6%A0%87%E6%B6%88%E5%A4%B1%E9%97%AE%E9%A2%98%E7%9A%84%E8%A7%A3%E5%86%B3%EF%BC%88%E4%B8%8B%E6%96%B92025%E5%B9%B47%E6%9C%8829%E6%97%A5%E6%9C%89%E6%9B%B4%E6%96%B0%EF%BC%89">系统常驻菜单栏图标消失问题的解决（下方2025年7月29日有更新）</a></li>
</ul>
</li>
<li><a href="#beta-4-2025%E5%B9%B47%E6%9C%8829%E6%97%A5%E6%9B%B4%E6%96%B0">Beta 4 2025年7月29日更新</a>
<ul>
<li><a href="#%E7%B3%BB%E7%BB%9F%E5%B8%B8%E9%A9%BB%E8%8F%9C%E5%8D%95%E6%A0%8F%E5%9B%BE%E6%A0%87%E6%B6%88%E5%A4%B1%E9%97%AE%E9%A2%98%E7%9A%84%E8%A7%A3%E5%86%B3%EF%BC%88%E6%9B%B4%E6%96%B0%E7%89%88%EF%BC%89">系统常驻菜单栏图标消失问题的解决（更新版）</a></li>
</ul>
</li>
</ul>
<p>我是从beta 4开始当成主力机使用的，因此从beta 4开始记录。之后会不定期更新。</p>
<h2><a id="beta-4" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>Beta 4</h2>
<h3><a id="%E5%91%BD%E4%BB%A4system-profiler%E7%9A%84%E5%8F%82%E6%95%B0%E5%8F%91%E7%94%9F%E6%94%B9%E5%8F%98" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>命令system_profiler的参数发生改变</h3>
<p>之前要查看USB设备，需要使用</p>
<pre class="line-numbers"><code class="language-bash">system_profiler SPUSBDataType
</code></pre>
<p>但是新系统中，参数改变了，变成了<code>SPUSBHostDataType</code>。并且输出的结果中的属性也有了变化，需要进行对应的修改。</p>
<h3><a id="%E5%BA%94%E7%94%A8%E4%BC%B4%E9%9A%8F%E7%B3%BB%E7%BB%9F%E5%90%AF%E5%8A%A8%E7%9A%84%E6%96%B9%E5%BC%8F%EF%BC%8C%E4%B8%8D%E5%90%8C%E7%9A%84%E6%96%B9%E6%B3%95%E5%9C%A8%E8%AE%BE%E7%BD%AE%E4%B8%AD%E6%98%BE%E7%A4%BA%E4%B8%8D%E5%90%8C" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>应用伴随系统启动的方式，不同的方法在设置中显示不同</h3>
<p>如果要应用伴随系统启动，现在有两种方式：</p>
<ol>
<li>使用Login Item应用，注册一个Login Item辅助程序，系统启动它之后，由它来调用主应用。</li>
<li>直接在主应用中调用SMAppService.mainApp.register()。</li>
</ol>
<p>方法1是很早就有的方式。方法2是后来新增的方式。不过在新系统中，如果你使用的是方法1，那么在设置的启动项中，只有后台有主应用的名字，系统自动其中中不会有。如果是使用的方法2，那么启动项和后台中都会有主应用的名字。</p>
<p>两种方式都可以成功伴随系统启动。</p>
<h3><a id="%E7%B3%BB%E7%BB%9F%E5%B8%B8%E9%A9%BB%E8%8F%9C%E5%8D%95%E6%A0%8F%E5%9B%BE%E6%A0%87%E6%B6%88%E5%A4%B1%E9%97%AE%E9%A2%98%E7%9A%84%E8%A7%A3%E5%86%B3%EF%BC%88%E4%B8%8B%E6%96%B92025%E5%B9%B47%E6%9C%8829%E6%97%A5%E6%9C%89%E6%9B%B4%E6%96%B0%EF%BC%89" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>系统常驻菜单栏图标消失问题的解决（下方2025年7月29日有更新）</h3>
<p>新系统中，我发现有的应用的菜单栏常驻图标会消失。进一步调查我发现，如果你的菜单栏常驻图标中使用attributedTitle，并且重复设置了相同的值，常驻图标就会消失。</p>
<blockquote>
<p>临时解决方案是，每次更改之前比较值是否发生了改变，变了再重新设置。</p>
</blockquote>
<h2><a id="beta-4-2025%E5%B9%B47%E6%9C%8829%E6%97%A5%E6%9B%B4%E6%96%B0" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>Beta 4 2025年7月29日更新</h2>
<h3><a id="%E7%B3%BB%E7%BB%9F%E5%B8%B8%E9%A9%BB%E8%8F%9C%E5%8D%95%E6%A0%8F%E5%9B%BE%E6%A0%87%E6%B6%88%E5%A4%B1%E9%97%AE%E9%A2%98%E7%9A%84%E8%A7%A3%E5%86%B3%EF%BC%88%E6%9B%B4%E6%96%B0%E7%89%88%EF%BC%89" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>系统常驻菜单栏图标消失问题的解决（更新版）</h3>
<p>上面的临时解决方案虽然在系统运行时可以解决问题，但是一旦系统睡眠，唤醒之后还是可能出现同样的问题。经过进一步研究，我发现了更好的解决方案：</p>
<p>假设你的代码是类似这种</p>
<pre class="line-numbers"><code class="language-swift">class AppDelegate: NSObject, NSApplicationDelegate {
  private var statusItem: NSStatusItem?

  private func setupMenubarTray() {
    let statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
    self.statusItem = statusItem

    guard let button = statusItem.button else {
      fatalError()
    }
    
    // 其他代码
  }
}
</code></pre>
<p>那么将代码改成</p>
<pre class="line-numbers"><code class="language-swift">class AppDelegate: NSObject, NSApplicationDelegate {
  private var statusItem: NSStatusItem?

  private func setupMenubarTray() {
    if self.statusItem == nil {
      self.statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
    }

    guard let button = self.statusItem?.button else {
      fatalError()
    }
    
     // 其他代码
  }
}
</code></pre>
<blockquote>
<p>我想问题应该是出在<code>NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)</code>这个函数。它可能没有正确的释放。导致同时存在多个实例了。</p>
</blockquote>
<h2><a id="beta-5-2025%E5%B9%B48%E6%9C%887%E6%97%A5%E6%9B%B4%E6%96%B0" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>Beta 5 2025年8月7日更新</h2>
<p>Beta 5修正了MacCatalyst下.fullScreenCover的显示问题。上一版，会透出后面视图的内容。</p>

]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[不要使用NSDecimalNumber.intValue]]></title>
    <link href="https://zhaoxin.pro/technology/17502372934060.html"/>
    <updated>2025-06-18T17:01:33+08:00</updated>
    <id>https://zhaoxin.pro/technology/17502372934060.html</id>
    <content type="html"><![CDATA[
<p>今天遇到了一件怪事。函数返回值始终是0，但是中间计算过程又没啥问题。</p>
<p>最终判定，就是在计算百分比时将Decimal计算的百分比，通过（decimal as NSDecimalNumber).intValue进行输出时出错的。</p>
<p>深入分析具体的原因，是因为Int的精度不足以转换NSDecimalNumber，所以就自动设置为0了。这个对于普通人来说，可能很难理解。毕竟结果也才46，怎么说Int的精度不足呢？但是事实就是如此，你可以简单地认为就是46后面的小数点之后的位数过多了。</p>
<p>解决的办法有有两个，一种是直接使用.doubleValue，然后取整。</p>
<pre class="line-numbers"><code class="language-swift">let intValue = Int(nsDecimalNumber.doubleValue)
</code></pre>
<p>另一种则比较复杂，使用了专门的rounding函数。</p>
<pre class="line-numbers"><code class="language-swift">let rounded = nsDecimalNumber.rounding(accordingToBehavior: NSDecimalNumberHandler(
    roundingMode: .plain,
    scale: 0,
    raiseOnExactness: false,
    raiseOnOverflow: false,
    raiseOnUnderflow: false,
    raiseOnDivideByZero: true
))
let intValue = rounded.intValue
</code></pre>

]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[SwiftUI重复刷新的问题可能找到了]]></title>
    <link href="https://zhaoxin.pro/technology/17489028926615.html"/>
    <updated>2025-06-03T06:21:32+08:00</updated>
    <id>https://zhaoxin.pro/technology/17489028926615.html</id>
    <content type="html"><![CDATA[
<p>我：<br />
按照SwiftUI的刷新方式，如果有两个@State，然后在一个函数中分别为这两个@State对应的属性赋值，SwiftUI对应的View是会自动刷新1次，还是两次？</p>
<p>Claude Sonnet 4（C）：<br />
在SwiftUI中，如果你在一个函数中同时修改两个@State属性，View只会刷新1次，而不是两次。</p>
<p>这是因为SwiftUI有一个重要的优化机制：批量更新（Batch Updates）。当多个状态变化发生在同一个运行循环中时，SwiftUI会将这些变化合并成一次更新。</p>
<h2><a id="%E7%AC%AC%E4%B8%80%E8%BD%AE" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>第一轮</h2>
<p>让我用一个例子来说明：</p>
<pre class="line-numbers"><code class="language-swift">struct ContentView: View {
    @State private var count1 = 0
    @State private var count2 = 0
    
    var body: some View {
        VStack {
            Text(&quot;Count1: \(count1)&quot;)
            Text(&quot;Count2: \(count2)&quot;)
            
            Button(&quot;同时更新两个值&quot;) {
                // 在同一个函数中修改两个@State
                count1 += 1
                count2 += 1
                
                print(&quot;函数执行完毕&quot;) // 这会在View刷新之前打印
            }
        }
    }
}
</code></pre>
<p>在这个例子中，点击按钮时：</p>
<pre class="line-numbers"><code class="language-plain_text">1.	count1和count2都会被修改
2.	SwiftUI会将这两个状态变化合并
3.	View只会重新渲染1次，同时反映两个值的变化
</code></pre>
<p>这种机制的好处是：</p>
<pre class="line-numbers"><code class="language-plain_text">•	性能优化：避免不必要的多次渲染
•	视觉连贯性：用户看到的是一次性的状态更新，而不是闪烁的分步更新
•	逻辑一致性：相关的状态变化作为一个整体呈现
</code></pre>
<p>如果你想要强制分开更新，可以使用DispatchQueue：</p>
<pre class="line-numbers"><code class="language-swift">Button(&quot;分开更新&quot;) {
    count1 += 1
    
    DispatchQueue.main.async {
        count2 += 1  // 这会导致第二次刷新
    }
}
</code></pre>
<p>但在绝大多数情况下，SwiftUI的默认批量更新机制是最佳选择。</p>
<h2><a id="%E7%AC%AC%E4%BA%8C%E8%BD%AE" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>第二轮</h2>
<p>我：<br />
如果是@Observable这种，也会是批量刷新吗？</p>
<p>C：<br />
是的，**@Observable也会进行批量刷新！**这是SwiftUI观察系统的一个核心特性，不仅适用于@State，也适用于@Observable。</p>
<p>让我用例子来演示：</p>
<pre class="line-numbers"><code class="language-swift">@Observable
class UserData {
    var name = &quot;&quot;
    var age = 0
    var email = &quot;&quot;
}

struct ContentView: View {
    @State private var userData = UserData()
    
    var body: some View {
        VStack {
            Text(&quot;姓名: \(userData.name)&quot;)
            Text(&quot;年龄: \(userData.age)&quot;)
            Text(&quot;邮箱: \(userData.email)&quot;)
            
            Button(&quot;同时更新多个属性&quot;) {
                // 在同一个函数中修改多个@Observable属性
                userData.name = &quot;张三&quot;
                userData.age = 25
                userData.email = &quot;zhangsan@example.com&quot;
                
                print(&quot;所有属性更新完毕&quot;) // 这会在View刷新之前打印
            }
        }
    }
}
</code></pre>
<p>在这个例子中，虽然修改了3个不同的属性，但<strong>View只会刷新1次</strong>。</p>
<h3><a id="observable%E7%9A%84%E6%89%B9%E9%87%8F%E6%9B%B4%E6%96%B0%E6%9C%BA%E5%88%B6" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>@Observable的批量更新机制</h3>
<p>@Observable使用了Swift的新观察系统（Observation framework），它具有以下特点：</p>
<pre class="line-numbers"><code class="language-plain_text">1.	**事务性更新：**多个属性变化会被合并成一个更新事务
2.	**智能依赖追踪：**只有被View实际使用的属性变化才会触发刷新
3.	**性能优化：**避免了不必要的多次渲染
</code></pre>
<h3><a id="%E6%9B%B4%E9%AB%98%E7%BA%A7%E7%9A%84%E4%BE%8B%E5%AD%90" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>更高级的例子</h3>
<pre class="line-numbers"><code class="language-swift">@Observable
class ShoppingCart {
    var items: [String] = []
    var totalPrice: Double = 0.0
    var discountApplied: Bool = false
    
    func addItem(_ item: String, price: Double) {
        // 这些修改会被批量处理
        items.append(item)
        totalPrice += price
        
        if totalPrice &gt; 100 {
            discountApplied = true
            totalPrice *= 0.9 // 应用10%折扣
        }
    }
}

struct ShoppingView: View {
    @State private var cart = ShoppingCart()
    
    var body: some View {
        VStack {
            Text(&quot;商品数量: \(cart.items.count)&quot;)
            Text(&quot;总价: \(cart.totalPrice, specifier: &quot;%.2f&quot;)&quot;)
            Text(&quot;折扣: \(cart.discountApplied ? &quot;已应用&quot; : &quot;未应用&quot;)&quot;)
            
            Button(&quot;添加昂贵商品&quot;) {
                cart.addItem(&quot;iPhone&quot;, price: 120.0)
                // 即使函数内部修改了多个属性，View也只刷新1次
            }
        }
    }
}
</code></pre>
<h3><a id="%E5%85%B3%E9%94%AE%E8%A6%81%E7%82%B9" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>关键要点</h3>
<pre class="line-numbers"><code class="language-plain_text">•	**批量更新是自动的：**无需特殊配置
•	**跨属性合并：**同一对象的多个属性变化会合并
•	**运行循环边界：**更新会在当前运行循环结束时统一执行
•	**性能友好：**这种机制显著提升了UI响应性能
</code></pre>
<p>所以无论是@State、@Observable，还是其他SwiftUI的状态管理方式，都会享受到这种批量刷新的优化机制。</p>
<h2><a id="%E7%AC%AC%E4%B8%89%E8%BD%AE" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>第三轮</h2>
<p>是的，@ObservedObject确实在性能上相对较差，主要原因是它基于较老的ObservableObject协议和Combine框架。关于批量刷新，情况比较复杂：</p>
<h3><a id="observedobject%E7%9A%84%E6%89%B9%E9%87%8F%E5%88%B7%E6%96%B0%E8%A1%8C%E4%B8%BA" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>@ObservedObject的批量刷新行为</h3>
<p>@ObservedObject<strong>理论上也支持批量刷新</strong>，但实际效果取决于具体实现：</p>
<pre class="line-numbers"><code class="language-swift">class UserDataOld: ObservableObject {
    @Published var name = &quot;&quot;
    @Published var age = 0
    @Published var email = &quot;&quot;
}

struct ContentView: View {
    @ObservedObject var userData = UserDataOld()
    
    var body: some View {
        VStack {
            Text(&quot;姓名: \(userData.name)&quot;)
            Text(&quot;年龄: \(userData.age)&quot;)
            Text(&quot;邮箱: \(userData.email)&quot;)
            
            Button(&quot;同时更新多个属性&quot;) {
                userData.name = &quot;李四&quot;
                userData.age = 30
                userData.email = &quot;lisi@example.com&quot;
                // 这通常会触发1次刷新，但不如@Observable稳定
            }
        }
    }
</code></pre>
<h3><a id="observedobject%E7%9A%84%E6%80%A7%E8%83%BD%E9%97%AE%E9%A2%98" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>@ObservedObject的性能问题</h3>
<h4><a id="1%E8%BF%87%E5%BA%A6%E5%88%B7%E6%96%B0%E9%97%AE%E9%A2%98" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>1. 过度刷新问题</h4>
<pre class="line-numbers"><code class="language-swift">class ProblematicModel: ObservableObject {
    @Published var importantData = &quot;&quot;
    @Published var trivialData = &quot;&quot; // 即使只是这个改变
    
    func updateTrivialData() {
        trivialData = &quot;无关紧要的数据&quot;
        // 整个View都会刷新，即使View可能不关心这个属性
    }
}
</code></pre>
<h4><a id="2%E6%97%A0%E6%B3%95%E7%B2%BE%E7%A1%AE%E8%BF%BD%E8%B8%AA%E4%BE%9D%E8%B5%96" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>2. 无法精确追踪依赖</h4>
<pre class="line-numbers"><code class="language-swift">struct InefficientView: View {
    @ObservedObject var model = ProblematicModel()
    
    var body: some View {
        VStack {
            Text(&quot;重要数据: \(model.importantData)&quot;)
            // 即使这个View只使用importantData
            // 但model中任何@Published属性变化都会导致刷新
        }
    }
}
</code></pre>
<h3><a id="%E4%B8%8Eobservable%E7%9A%84%E5%AF%B9%E6%AF%94" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>与@Observable的对比</h3>
<pre class="line-numbers"><code class="language-swift">// 老式方式 - @ObservedObject
class OldUserData: ObservableObject {
    @Published var profile = &quot;&quot;
    @Published var settings = &quot;&quot;
    @Published var cache = &quot;&quot; // 改变这个会刷新整个View
}

// 新式方式 - @Observable  
@Observable
class NewUserData {
    var profile = &quot;&quot;
    var settings = &quot;&quot;
    var cache = &quot;&quot; // 只有被使用的属性改变才会刷新View
}

struct ComparisonView: View {
    @ObservedObject var oldData = OldUserData()
    @State private var newData = NewUserData()
    
    var body: some View {
        VStack {
            // 使用oldData时，任何@Published属性变化都会刷新
            Text(&quot;老数据: \(oldData.profile)&quot;)
            
            // 使用newData时，只有profile变化才会刷新这个Text
            Text(&quot;新数据: \(newData.profile)&quot;)
            
            Button(&quot;更新缓存&quot;) {
                oldData.cache = &quot;新缓存&quot; // 会刷新整个View
                newData.cache = &quot;新缓存&quot; // 不会刷新View，因为没有Text使用cache
            }
        }
    }
}
</code></pre>
<h3><a id="%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96%E5%BB%BA%E8%AE%AE" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>性能优化建议</h3>
<h4><a id="1%E8%BF%81%E7%A7%BB%E5%88%B0-observable" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>1. 迁移到@Observable</h4>
<pre class="line-numbers"><code class="language-swift">// 推荐的现代方式
@Observable
class ModernUserData {
    var name = &quot;&quot;
    var age = 0
    var email = &quot;&quot;
}
</code></pre>
<h4><a id="2%E5%A6%82%E6%9E%9C%E5%BF%85%E9%A1%BB%E4%BD%BF%E7%94%A8-observedobject%EF%BC%8C%E6%89%8B%E5%8A%A8%E6%8E%A7%E5%88%B6%E6%9B%B4%E6%96%B0" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>2. 如果必须使用@ObservedObject，手动控制更新</h4>
<pre class="line-numbers"><code class="language-swift">class OptimizedModel: ObservableObject {
    // 只对重要数据使用@Published
    @Published var criticalData = &quot;&quot;
    
    // 不重要的数据不使用@Published
    private var _cacheData = &quot;&quot;
    var cacheData: String {
        get { _cacheData }
        set { 
            _cacheData = newValue
            // 不会触发View刷新
        }
    }
}
</code></pre>
<h3><a id="%E6%80%BB%E7%BB%93" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>总结</h3>
<pre class="line-numbers"><code class="language-plain_text">•	**@ObservedObject：**支持批量刷新，但容易过度刷新，性能较差
•	**@Observable：**精确的依赖追踪，更好的批量刷新，性能优秀
•	**建议：**新项目优先使用@Observable，老项目逐步迁移
</code></pre>
<p>@Observable是SwiftUI未来的方向，它解决了@ObservedObject的大部分性能问题。</p>
<h2><a id="%E7%AC%AC%E5%9B%9B%E8%BD%AE" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>第四轮</h2>
<p>我：<br />
我注意到，你在使用@Observable的类时，使用了@State，@State private var newData = NewUserData()，而不是let或者var，这是为什么呢？</p>
<p>C：<br />
这是一个非常好的观察！你提到的这个细节很重要，让我详细解释一下：</p>
<h3><a id="%E4%B8%BA%E4%BB%80%E4%B9%88%E4%BD%BF%E7%94%A8state%E8%80%8C%E4%B8%8D%E6%98%AFlet%E6%88%96var" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>为什么使用@State而不是let或var</h3>
<h4><a id="1-observable%E9%9C%80%E8%A6%81%E8%A2%ABswiftui%E8%A7%82%E5%AF%9F%E7%B3%BB%E7%BB%9F%E8%BF%BD%E8%B8%AA" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>1. @Observable需要被SwiftUI观察系统追踪</h4>
<pre class="line-numbers"><code class="language-swift">@Observable
class UserData {
    var name = &quot;&quot;
    var age = 0
}

struct ContentView: View {
    // ❌ 错误方式 - 不会触发View更新
    let userData = UserData()
    
    // ❌ 错误方式 - 也不会触发View更新  
    var userData = UserData()
    
    // ✅ 正确方式 - 会触发View更新
    @State private var userData = UserData()
    
    var body: some View {
        VStack {
            Text(&quot;姓名: \(userData.name)&quot;)
            
            Button(&quot;更新姓名&quot;) {
                userData.name = &quot;张三&quot;
                // 只有使用@State时，这个变化才会触发View刷新
            }
        }
    }
}
</code></pre>
<h4><a id="2-swiftui%E7%9A%84%E8%A7%82%E5%AF%9F%E6%9C%BA%E5%88%B6%E9%9C%80%E8%A6%81%E5%8C%85%E8%A3%85%E5%99%A8" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>2. SwiftUI的观察机制需要包装器</h4>
<p>@Observable虽然让类具备了被观察的能力，但在SwiftUI中，<strong>View仍然需要通过属性包装器来建立观察关系：</strong></p>
<pre class="line-numbers"><code class="language-swift">@Observable
class Counter {
    var value = 0
    
    func increment() {
        value += 1
    }
}

struct CounterView: View {
    // @State告诉SwiftUI：&quot;请观察这个对象的变化&quot;
    @State private var counter = Counter()
    
    var body: some View {
        VStack {
            Text(&quot;计数: \(counter.value)&quot;)
            
            Button(&quot;增加&quot;) {
                counter.increment()
                // 因为用了@State，View会自动刷新
            }
        }
    }
}
</code></pre>
<h4><a id="3%E4%B8%8D%E5%90%8C%E5%9C%BA%E6%99%AF%E4%B8%8B%E7%9A%84%E7%94%A8%E6%B3%95%E5%AF%B9%E6%AF%94" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>3. 不同场景下的用法对比</h4>
<pre class="line-numbers"><code class="language-swift">@Observable
class AppState {
    var isLoggedIn = false
    var username = &quot;&quot;
}

struct LoginView: View {
    // 场景1: View拥有并管理对象的生命周期
    @State private var appState = AppState()
    
    var body: some View {
        VStack {
            if appState.isLoggedIn {
                Text(&quot;欢迎, \(appState.username)&quot;)
            } else {
                loginForm
            }
        }
    }
    
    var loginForm: some View {
        Button(&quot;登录&quot;) {
            appState.username = &quot;用户&quot;
            appState.isLoggedIn = true
        }
    }
}

struct SharedDataView: View {
    // 场景2: 对象从外部传入
    let sharedAppState: AppState
    
    var body: some View {
        VStack {
            Text(&quot;共享状态: \(sharedAppState.username)&quot;)
            // 注意：这里不会自动刷新，因为没有使用属性包装器
        }
    }
}
</code></pre>
<h4><a id="4%E6%AD%A3%E7%A1%AE%E7%9A%84%E5%85%B1%E4%BA%AB%E6%95%B0%E6%8D%AE%E6%96%B9%E5%BC%8F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>4. 正确的共享数据方式</h4>
<p>如果需要在多个View之间共享@Observable对象：</p>
<pre class="line-numbers"><code class="language-swift">@Observable
class SharedAppState {
    var theme = &quot;light&quot;
    var language = &quot;zh&quot;
}

struct RootView: View {
    @State private var appState = SharedAppState()
    
    var body: some View {
        NavigationView {
            VStack {
                SettingsView(appState: appState)
                ProfileView(appState: appState)
            }
        }
    }
}

struct SettingsView: View {
    @Bindable var appState: SharedAppState  // 用于接收外部传入的@Observable对象
    
    var body: some View {
        VStack {
            Text(&quot;当前主题: \(appState.theme)&quot;)
            Button(&quot;切换主题&quot;) {
                appState.theme = appState.theme == &quot;light&quot; ? &quot;dark&quot; : &quot;light&quot;
            }
        }
    }
}
</code></pre>
<h3><a id="%E6%80%BB%E7%BB%93" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>总结</h3>
<pre class="line-numbers"><code class="language-plain_text">•	**@State：**告诉SwiftUI”我拥有这个对象，请观察它的变化”
•	**@Bindable：**告诉SwiftUI”这个对象是从外部传入的，请观察它的变化”
•	**let/var：**普通属性，SwiftUI不会观察其变化
</code></pre>
<p>使用@State是为了让SwiftUI知道需要观察这个@Observable对象，从而在对象属性发生变化时自动更新View。这是SwiftUI观察系统的要求，而不是@Observable本身的限制。</p>
<h2><a id="%E7%AC%AC%E4%BA%94%E8%BD%AE" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>第五轮</h2>
<p>我：<br />
你上面的说法有一部分并不准确。在苹果的官方文档中，<a href="https://developer.apple.com/documentation/swiftui/migrating-from-the-observable-object-protocol-to-the-observable-macro/%EF%BC%8C%E8%8B%B9%E6%9E%9C%E7%94%A8%E6%9D%A5%E6%9B%BF%E4%BB%A3@StateObject%E7%9A%84%E6%98%AF@State%EF%BC%8C%E7%94%A8%E6%9D%A5%E6%9B%BF%E4%BB%A3@ObservedObject%E7%9A%84%EF%BC%8C%E5%B0%B1%E6%98%AFvar%E3%80%82">https://developer.apple.com/documentation/swiftui/migrating-from-the-observable-object-protocol-to-the-observable-macro/，苹果用来替代@StateObject的是@State，用来替代@ObservedObject的，就是var。</a></p>
<p>C：<br />
你说得完全正确！我之前的解释确实有不准确的地方。感谢你提供的官方文档链接，让我重新准确地解释：</p>
<h3><a id="%E6%AD%A3%E7%A1%AE%E7%9A%84%E8%BF%81%E7%A7%BB%E5%AF%B9%E5%BA%94%E5%85%B3%E7%B3%BB" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>正确的迁移对应关系</h3>
<p>根据苹果官方文档，从ObservableObject迁移到@Observable的对应关系是：</p>
<pre class="line-numbers"><code class="language-swift">// 旧的ObservableObject方式
class OldDataModel: ObservableObject {
    @Published var name = &quot;&quot;
}

struct OldView: View {
    @StateObject private var model = OldDataModel()  // 创建并拥有
    @ObservedObject var shared: OldDataModel        // 接收外部传入
}

// 新的@Observable方式  
@Observable
class NewDataModel {
    var name = &quot;&quot;
}

struct NewView: View {
    @State private var model = NewDataModel()  // ✅ 替代@StateObject
    var shared: NewDataModel                   // ✅ 替代@ObservedObject (普通var!)
}
</code></pre>
<h3><a id="%E5%85%B3%E9%94%AE%E5%8C%BA%E5%88%AB%E8%A7%A3%E9%87%8A" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>关键区别解释</h3>
<h4><a id="1-state%E6%9B%BF%E4%BB%A3-stateobject" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>1. @State替代@StateObject</h4>
<pre class="line-numbers"><code class="language-swift">struct OwnerView: View {
    // 创建并拥有对象的生命周期
    @State private var userData = UserData()
    
    var body: some View {
        UserDetailView(user: userData)
    }
}
</code></pre>
<h4><a id="2%E6%99%AE%E9%80%9A-var%E6%9B%BF%E4%BB%A3-observedobject" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>2. 普通var替代@ObservedObject</h4>
<pre class="line-numbers"><code class="language-swift">struct UserDetailView: View {
    // 接收外部传入的@Observable对象 - 就是普通var!
    var user: UserData
    
    var body: some View {
        VStack {
            Text(user.name)
            Button(&quot;更新&quot;) {
                user.name = &quot;新名字&quot;  // 这会自动触发View更新!
            }
        }
    }
}
</code></pre>
<h3><a id="%E4%B8%BA%E4%BB%80%E4%B9%88%E6%99%AE%E9%80%9Avar%E5%B0%B1%E8%83%BD%E5%B7%A5%E4%BD%9C%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>为什么普通var就能工作？</h3>
<p>这是因为@Observable宏为类生成了观察机制，SwiftUI会自动检测到对@Observable对象的访问和修改：</p>
<pre class="line-numbers"><code class="language-swift">@Observable
class UserData {
    var name = &quot;&quot;
    var age = 0
}

struct DetailView: View {
    var userData: UserData  // 普通属性，无需@ObservedObject
    
    var body: some View {
        VStack {
            Text(userData.name)     // SwiftUI自动追踪这个访问
            Text(&quot;\(userData.age)&quot;)  // SwiftUI自动追踪这个访问
            
            Button(&quot;更新&quot;) {
                userData.name = &quot;新名字&quot;  // 自动触发刷新
                userData.age += 1        // 自动触发刷新
            }
        }
    }
}
</code></pre>
<h3><a id="%E5%AE%8C%E6%95%B4%E7%9A%84%E8%BF%81%E7%A7%BB%E7%A4%BA%E4%BE%8B" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>完整的迁移示例</h3>
<pre class="line-numbers"><code class="language-swift">// === 旧方式 ===
class OldShoppingCart: ObservableObject {
    @Published var items: [String] = []
    @Published var total: Double = 0
}

struct OldShoppingView: View {
    @StateObject private var cart = OldShoppingCart()
    
    var body: some View {
        OldCartDetailView(cart: cart)
    }
}

struct OldCartDetailView: View {
    @ObservedObject var cart: OldShoppingCart  // 需要@ObservedObject
    
    var body: some View {
        VStack {
            Text(&quot;商品数: \(cart.items.count)&quot;)
            Text(&quot;总价: \(cart.total)&quot;)
        }
    }
}

// === 新方式 ===
@Observable
class ShoppingCart {
    var items: [String] = []
    var total: Double = 0
}

struct ShoppingView: View {
    @State private var cart = ShoppingCart()  // @State替代@StateObject
    
    var body: some View {
        CartDetailView(cart: cart)
    }
}

struct CartDetailView: View {
    var cart: ShoppingCart  // 普通var替代@ObservedObject!
    
    var body: some View {
        VStack {
            Text(&quot;商品数: \(cart.items.count)&quot;)
            Text(&quot;总价: \(cart.total)&quot;)
            
            Button(&quot;添加商品&quot;) {
                cart.items.append(&quot;新商品&quot;)  // 自动触发更新
                cart.total += 10.0          // 自动触发更新
            }
        }
    }
}
</code></pre>
<p>感谢你的纠正！@Observable的强大之处就在于，接收外部传入的对象时只需要普通的var属性，SwiftUI会自动处理观察关系。</p>
<h2><a id="%E7%AC%AC%E5%85%AD%E8%BD%AE" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>第六轮</h2>
<p>我：<br />
我有一些疑惑，@StateObject和@ObservedObject似乎没有什么区别啊。</p>
<p>C：<br />
你的疑惑很正常！从表面上看，@StateObject和@ObservedObject确实很相似，但它们有一个<strong>关键的区别：对象的生命周期管理</strong>。</p>
<h3><a id="%E6%A0%B8%E5%BF%83%E5%8C%BA%E5%88%AB%EF%BC%9A%E8%B0%81%E6%8B%A5%E6%9C%89%E5%AF%B9%E8%B1%A1%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>核心区别：谁拥有对象？</h3>
<p>@StateObject - “我创建并拥有这个对象”</p>
<pre class="line-numbers"><code class="language-swift">struct ParentView: View {
    @State private var counter = 0
    
    var body: some View {
        VStack {
            Text(&quot;父视图计数: \(counter)&quot;)
            
            // 每次counter变化，ChildView都会重新创建
            ChildView(parentCounter: counter)
            
            Button(&quot;增加父计数&quot;) {
                counter += 1  // 这会导致ChildView重建！
            }
        }
    }
}

class DataModel: ObservableObject {
    @Published var value = 0
    
    init() {
        print(&quot;DataModel创建了！&quot;)
    }
    
    deinit {
        print(&quot;DataModel销毁了！&quot;)
    }
}

struct ChildView: View {
    let parentCounter: Int
    
    // ❌ 错误用法 - 每次父视图刷新都会重新创建DataModel
    @ObservedObject private var wrongModel = DataModel()
    
    // ✅ 正确用法 - 无论父视图如何刷新，DataModel只创建一次
    @StateObject private var correctModel = DataModel()
    
    var body: some View {
        VStack {
            Text(&quot;父传入的值: \(parentCounter)&quot;)
            Text(&quot;子视图数据: \(correctModel.value)&quot;)
            
            Button(&quot;增加子数据&quot;) {
                correctModel.value += 1
            }
        }
    }
}
</code></pre>
<h3><a id="%E5%AE%9E%E9%99%85%E9%97%AE%E9%A2%98%E6%BC%94%E7%A4%BA" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>实际问题演示</h3>
<p>运行上面的代码，你会发现：</p>
<h4><a id="%E4%BD%BF%E7%94%A8observedobject%E7%9A%84%E9%97%AE%E9%A2%98%EF%BC%9A" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>使用@ObservedObject的问题：</h4>
<pre class="line-numbers"><code class="language-swift">struct ProblematicChildView: View {
    let parentCounter: Int
    @ObservedObject private var model = DataModel()  // ❌ 问题代码
    
    var body: some View {
        VStack {
            Text(&quot;数据: \(model.value)&quot;)
            Button(&quot;增加&quot;) {
                model.value += 1
            }
        }
    }
}
</code></pre>
<p>**问题：**每当父视图刷新时，DataModel()会被重新执行，创建新对象，之前的数据丢失！</p>
<h4><a id="%E4%BD%BF%E7%94%A8stateobject%E7%9A%84%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88%EF%BC%9A" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>使用@StateObject的解决方案：</h4>
<pre class="line-numbers"><code class="language-swift">struct CorrectChildView: View {
    let parentCounter: Int
    @StateObject private var model = DataModel()  // ✅ 正确代码
    
    var body: some View {
        VStack {
            Text(&quot;数据: \(model.value)&quot;)
            Button(&quot;增加&quot;) {
                model.value += 1  // 数据会被保持，不会因父视图刷新而丢失
            }
        }
    }
}
</code></pre>
<h3><a id="%E5%AE%8C%E6%95%B4%E7%9A%84%E5%AF%B9%E6%AF%94%E7%A4%BA%E4%BE%8B" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>完整的对比示例</h3>
<pre class="line-numbers"><code class="language-swift">class Timer: ObservableObject {
    @Published var count = 0
    private var timer: Foundation.Timer?
    
    init() {
        print(&quot;⚠️ Timer对象创建&quot;)
        startTimer()
    }
    
    deinit {
        print(&quot;💥 Timer对象销毁&quot;)
        timer?.invalidate()
    }
    
    private func startTimer() {
        timer = Foundation.Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in
            self.count += 1
        }
    }
}

struct MainView: View {
    @State private var refreshTrigger = 0
    
    var body: some View {
        VStack(spacing: 20) {
            Text(&quot;主视图刷新触发器: \(refreshTrigger)&quot;)
            
            // 每次点击都会导致子视图重建
            BadTimerView(trigger: refreshTrigger)
            GoodTimerView(trigger: refreshTrigger)
            
            Button(&quot;触发父视图刷新&quot;) {
                refreshTrigger += 1
            }
        }
    }
}

struct BadTimerView: View {
    let trigger: Int
    @ObservedObject private var timer = Timer()  // ❌ 每次父视图刷新都重新创建
    
    var body: some View {
        VStack {
            Text(&quot;错误方式 - 计时: \(timer.count)&quot;)
            Text(&quot;触发值: \(trigger)&quot;)
        }
        .border(Color.red)
    }
}

struct GoodTimerView: View {
    let trigger: Int
    @StateObject private var timer = Timer()  // ✅ 只创建一次，状态保持
    
    var body: some View {
        VStack {
            Text(&quot;正确方式 - 计时: \(timer.count)&quot;)
            Text(&quot;触发值: \(trigger)&quot;)
        }
        .border(Color.green)
    }
}
</code></pre>
<h3><a id="%E4%BD%BF%E7%94%A8%E8%A7%84%E5%88%99%E6%80%BB%E7%BB%93" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>使用规则总结</h3>
<pre class="line-numbers"><code class="language-swift">struct MyView: View {
    // ✅ 我创建并拥有这个对象
    @StateObject private var myData = MyDataModel()
    
    // ✅ 这个对象是从外部传入的
    @ObservedObject var sharedData: SharedDataModel
    
    var body: some View {
        ChildView(data: myData)  // 传递给子视图
    }
}

struct ChildView: View {
    // ✅ 接收从父视图传来的对象
    @ObservedObject var data: MyDataModel
    
    var body: some View {
        Text(&quot;\(data.value)&quot;)
    }
}
</code></pre>
<h4><a id="%E7%AE%80%E5%8D%95%E8%AE%B0%E5%BF%86%E6%B3%95%EF%BC%9A" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>简单记忆法：</h4>
<pre class="line-numbers"><code class="language-plain_text">•	**@StateObject：**我创建 = 我负责
•	**@ObservedObject：**别人创建 = 我只观察
</code></pre>
<p>这就是为什么在迁移到@Observable时，@StateObject对应@State（创建并拥有），@ObservedObject对应普通var（接收外部对象）。</p>

]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[@State在初始化函数中的特殊性]]></title>
    <link href="https://zhaoxin.pro/technology/17416521091912.html"/>
    <updated>2025-03-11T08:15:09+08:00</updated>
    <id>https://zhaoxin.pro/technology/17416521091912.html</id>
    <content type="html"><![CDATA[
<p>在<a href="17416513703708.html">原来Swift的Sheet里还不能这么做</a>中，提到也可以使用初始化函数。但是我在修改使用为初始化函数之后，代码运行却不符合预期。于是我询问AI：</p>
<blockquote>
<p>使用初始化函数的方式之后，始终显示的是xtTransUnits.isEmpty == true的分支，这是为什么呢？</p>
</blockquote>
<p>#AI回答<br />
问题出在初始化器的实现方式上。当前的实现使用了立即执行的闭包来设置 xtTransUnits，但因为 @State 属性在初始化器中的特殊性，这种方式并不会正确地设置初始值。</p>
<p>修改建议如下：</p>
<pre class="line-numbers"><code class="language-swift">struct AITranslationView: View {
    // ... 其他属性 ...
    
    init(trees: [Tree&lt;IDTree&gt;]) {
        self.trees = trees
        let allUnits = Xliff_Tool.getXTTransUnits(from: trees)
        _xtTransUnits = State(initialValue: allUnits.filter { $0.target == nil || $0.target?.isEmpty == true })
    }
}
</code></pre>
<p>关键修改点：</p>
<ol>
<li>使用 _xtTransUnits = State(initialValue: ...) 来正确初始化 @State 属性</li>
<li>移除了立即执行闭包，改为直接赋值</li>
<li>保持过滤逻辑不变</li>
</ol>
<p>这样修改后，xtTransUnits 将被正确初始化，视图也会显示正确的分支。</p>

]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[原来Swift的Sheet里还不能这么做]]></title>
    <link href="https://zhaoxin.pro/technology/17416513703708.html"/>
    <updated>2025-03-11T08:02:50+08:00</updated>
    <id>https://zhaoxin.pro/technology/17416513703708.html</id>
    <content type="html"><![CDATA[
<p>今天在调试一个应用崩溃的问题的时候，发现有个sheet弹出后，被反复执行了多次。于是询问AI怎么回事。</p>
<p>原始的代码是这样的</p>
<pre class="line-numbers"><code class="language-swift">.sheet(isPresented: $showAITranslationGuideView) {
  let xtTransUnits: [XTTransUnit] = {
    let xtTransUnits = Xliff_Tool.getXTTransUnits(from: trees)
    return xtTransUnits.filter { $0.target == nil || $0.target?.isEmpty == true }
  }()

  AITranslationView(xtTransUnits: xtTransUnits)
}
</code></pre>
<p>询问AI啥原因，AI是这么回答的。</p>
<blockquote>
<p>.sheet(isPresented: $showAITranslationGuideView)为什么会多次执行？</p>
</blockquote>
<p>在 .sheet(isPresented: $showAITranslationGuideView) 代码块中，每次 showAITranslationGuideView 状态变化时都会重新执行闭包内的代码。这里的问题是 xtTransUnits 的计算被放在了视图构建的闭包中，而不是在实际需要时才执行。</p>
<p>建议将这些逻辑移到 AITranslationView 的初始化或 onAppear 中。以下是修改建议：</p>
<pre class="line-numbers"><code class="language-swift">.sheet(isPresented: $showAITranslationGuideView) {
    AITranslationView(trees: trees)
}
</code></pre>
<p>然后在 AITranslationView 中处理数据：</p>
<pre class="line-numbers"><code class="language-swift">struct AITranslationView: View {
    let trees: [Tree&lt;IDTree&gt;]
    @State private var xtTransUnits: [XTTransUnit] = []
    
    var body: some View {
        // 你的视图内容
        .onAppear {
            xtTransUnits = getXTTransUnits(from: trees)
                .filter { $0.target == nil || $0.target?.isEmpty == true }
        }
    }
}
</code></pre>
<p>这样可以确保计算只在实际需要时执行一次。</p>
<blockquote>
<p>需要注意的是，AI的解释是对的，但是不是真正的原因。但是解法又同样是对的。只要移出这个初始化的代码之后，的确就不会重复计算了。</p>
<p>我认为真正原因是这样的。像.sheet(isPresented: $showAITranslationGuideView)的sheet，是在View初始化时就会被生成的。它是不变的。这就是为什么还有一个同名的但是使用Optional的sheet，那个才是动态的。但是我这里面的代码，在sheet使用前重新进行了计算。这导致原本应该只能是静态的sheet，变成了动态的，从而导致它从新被加载，而这种情形，其实是为定义的，因为这里不应该使用动态的。这个才是出错的真正原因。</p>
</blockquote>

]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[苹果的老六行为]]></title>
    <link href="https://zhaoxin.pro/technology/17414288047864.html"/>
    <updated>2025-03-08T18:13:24+08:00</updated>
    <id>https://zhaoxin.pro/technology/17414288047864.html</id>
    <content type="html"><![CDATA[
<p>苹果有时候还是很老六的。比如在系统声音设置中，有一个声音叫“Heroine”，也就是女英雄。</p>
<p><img src="media/17414288047864/lao6.jpg" alt="lao6" style="width:860px;" /></p>
<p>那么如果你想在自己的应用中使用它，可以通过NSSound调用</p>
<pre class="line-numbers"><code class="language-swift">class Foo {
    private var sound: NSSound?
    
    func playSound() {
        sound = NSSound(named: &quot;Heroine&quot;)
        sound?.play()
    }
}
</code></pre>
<p>但是这个声音应用运行时并不会发出。这是因为传统上，这段声音的名字叫“Hero”，大概是出于平权的考虑，苹果将它改名为“Heroine”了。但是程序代码里的，没改，还是叫“Hero”。所以，想在自己的应用里使用它，必须使用</p>
<pre class="line-numbers"><code class="language-swift"> sound = NSSound(named: &quot;Hero&quot;)
</code></pre>
<p>这可是连文档里都没有的。只能自己搜索加猜测才能摸索到。</p>
<blockquote>
<p>苹果还真是个老六。如果你真想平权，还想保证兼容性。那你让&quot;Hero&quot;和&quot;Heroine&quot;可以同时使用就可以了啊。这么做很难吗？</p>
</blockquote>

]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[TabView自动跳转Tab问题的解决]]></title>
    <link href="https://zhaoxin.pro/technology/17414080735085.html"/>
    <updated>2025-03-08T12:27:53+08:00</updated>
    <id>https://zhaoxin.pro/technology/17414080735085.html</id>
    <content type="html"><![CDATA[
<p>今天我在开发应用的时候遇到了一个奇怪的问题。当TabView的最后一个tab被拖动图片之后，应用会自动跳到第一个tab去。我很奇怪为什么有这个行为，于是拿来问AI。没想到很快就解决了。</p>
<blockquote>
<p>提示词：阅读代码，尝试解决问题。当最后一个tab被拖动图片后，它会自动跳到第一个tab，我不明白这是什么原因造成的。</p>
</blockquote>
<h1><a id="ai%E5%9B%9E%E5%A4%8D" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>AI回复</h1>
<h2><a id="%E9%97%AE%E9%A2%98%E5%88%86%E6%9E%90" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>问题分析</h2>
<p>我已经查看了您的代码，找到了为什么当最后一个watchOSTabView被拖动图片后会自动跳回第一个标签页的问题。</p>
<p>这是由于在TabView中没有设置选中的标签页状态，所以当watchOSTabView中的State变量更新时，视图会重新绘制，导致TabView回到默认的第一个标签页。</p>
<h2><a id="%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>解决方案</h2>
<p>我们需要在MainView中添加一个State变量来跟踪当前选中的标签页，并将其绑定到TabView。这样，即使视图重新绘制，也会保持在当前选中的标签页上。</p>
<pre class="line-numbers"><code class="language-swift">//
//  MainView.swift
//  My Icon Gear
//
//  Created by zhaoxin on 2025/3/5.
//

import SwiftUI

struct MainView: View {
  // 添加一个状态变量来跟踪选中的标签页
  @State private var selectedTab = 0
  private let platforms = [&quot;iOS&quot;, &quot;macOS&quot;, &quot;watchOS&quot;]

  var body: some View {
    TabView(selection: $selectedTab) {
      macOSTabView()
        .tag(0)
      iOSTabView()
        .tag(1)
      watchOSTabView()
        .tag(2)
    }
    .toolbar(content: {
      Button {

      } label: {
        Image(systemName: &quot;square.and.arrow.up&quot;)
        Text(&quot;Export to Folder&quot;)
      }
      .bold()
    })
    .padding()
  }
}

#Preview {
  MainView()
}
</code></pre>
<h2><a id="%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88%E8%A7%A3%E9%87%8A" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>解决方案解释</h2>
<p>我对MainView.swift文件进行了以下修改：</p>
<ol>
<li>
<p>添加了一个<code>@State private var selectedTab = 0</code>状态变量来跟踪当前选中的标签页。</p>
</li>
<li>
<p>修改了TabView，使用<code>TabView(selection: $selectedTab)</code>将选中状态绑定到TabView。</p>
</li>
<li>
<p>为每个TabView的子视图添加了<code>.tag()</code>修饰符，分别标记为0、1、2，这样系统就能知道哪个标签页被选中。</p>
</li>
</ol>
<p>这样修改后，当您在watchOSTabView（最后一个标签页）上拖放图片时，即使视图状态发生变化并重新绘制，TabView也会保持在当前选中的标签页（即tag为2的watchOSTabView），而不会跳回第一个标签页。</p>
<p>这是因为现在TabView的选择状态由<code>selectedTab</code>变量控制，而不是由系统自动决定。无论是哪个子视图更新状态并触发重绘，TabView都会保持在用户上次选择的标签页。</p>

]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[Runloop的各种模式的区别]]></title>
    <link href="https://zhaoxin.pro/technology/17405324842496.html"/>
    <updated>2025-02-26T09:14:44+08:00</updated>
    <id>https://zhaoxin.pro/technology/17405324842496.html</id>
    <content type="html"><![CDATA[
<p>薅到AI的羊毛了。👀今天AI在编写Timer相关的代码时，手动添加了loop，让我看不懂，于是我询问AI，它的解释让我学到了。</p>
<blockquote>
<p>能否解释一下“确保计时器在RunLoop中运行”的必要性这断代码的必要性。</p>
</blockquote>
<h2><a id="%E5%85%B3%E4%BA%8Erunloop%E4%B8%AD%E8%BF%90%E8%A1%8Ctimer%E7%9A%84%E5%BF%85%E8%A6%81%E6%80%A7" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>关于RunLoop中运行Timer的必要性</h2>
<p>在Swift中，关于这段代码的必要性：</p>
<pre class="line-numbers"><code class="language-swift">if let timer = autoCheckTimer {
  RunLoop.main.add(timer, forMode: .common)
}
</code></pre>
<p>这段代码确实非常重要，让我解释一下为什么：</p>
<h3><a id="%E4%B8%BA%E4%BB%80%E4%B9%88%E9%9C%80%E8%A6%81%E5%B0%86timer%E6%B7%BB%E5%8A%A0%E5%88%B0runloop%E4%B8%AD" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>为什么需要将Timer添加到RunLoop中</h3>
<p>当你使用<code>Timer.scheduledTimer</code>创建计时器时，它会自动添加到当前线程的RunLoop中，但仅使用默认模式（<code>.default</code>）。这在大多数情况下已经足够，但在某些特定情况下可能会导致计时器无法按预期工作：</p>
<ol>
<li>
<p><strong>滚动期间计时器暂停</strong>：当用户在UI上执行滚动操作时，RunLoop会切换到<code>.tracking</code>模式，这时默认模式下的计时器会暂时停止触发。</p>
</li>
<li>
<p><strong>模态操作期间计时器暂停</strong>：某些模态操作（如拖拽）也会导致RunLoop切换模式，影响计时器的正常运行。</p>
</li>
<li>
<p><strong>确保可靠性</strong>：显式添加计时器到RunLoop可以确保在不同的应用状态下计时器都能正常工作。</p>
</li>
</ol>
<p>通过将计时器添加到<code>.common</code>模式，我们确保它在多种常见的RunLoop模式下都能正常触发，提高了计时器的可靠性和稳定性。</p>
<h3><a id="%E6%9C%80%E4%BD%B3%E5%AE%9E%E8%B7%B5" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>最佳实践</h3>
<p>尽管<code>scheduledTimer</code>方法会自动将计时器添加到当前RunLoop，但明确地将计时器添加到RunLoop的<code>.common</code>模式是一种最佳实践，可以避免在复杂应用场景下出现计时器不按预期工作的问题。</p>
<p>特别是对于像自动检查更新这样需要在后台可靠运行的功能，确保计时器在各种应用状态下都能正常触发是非常重要的。</p>

]]></content>
  </entry>
  
</feed>
