<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>MVVMC on Joe's Blog</title><link>https://shinrenpan.github.io/tags/mvvmc/</link><description>Recent content in MVVMC on Joe's Blog</description><generator>Hugo</generator><language>zh-tw</language><copyright>© Shinren Pan. All rights reserved.</copyright><lastBuildDate>Fri, 24 Apr 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://shinrenpan.github.io/tags/mvvmc/index.xml" rel="self" type="application/rss+xml"/><item><title>為什麼我在 SwiftUI 專案裡自創了一套 MVVMC 架構</title><link>https://shinrenpan.github.io/2026-04-24/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://shinrenpan.github.io/2026-04-24/</guid><description>&lt;p&gt;在公司與外包共同開發，並在外包離開後接手留下來的 SwiftUI 專案。外包用的是 &lt;strong&gt;TCA（The Composable Architecture）&lt;/strong&gt;，target iOS 15+，架構本身有它的邏輯，但接手之後我做了第一個決定，不繼續用 TCA。&lt;/p&gt;
&lt;p&gt;接著把 minimum deployment target 直升 iOS 17+。&lt;code&gt;@Observable&lt;/code&gt; 可以用了，&lt;code&gt;WithPerceptionTracking&lt;/code&gt; 全部可以掃掉，光這件事就讓整個 codebase 清爽不少，bug 也少了不少。&lt;/p&gt;
&lt;p&gt;但還有一個問題一直卡著我：&lt;strong&gt;導航&lt;/strong&gt;。&lt;/p&gt;
&lt;h2 id="swiftui-導航用過就知道"&gt;SwiftUI 導航，用過就知道&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;NavigationStack&lt;/code&gt;、&lt;code&gt;NavigationLink&lt;/code&gt;、&lt;code&gt;.navigationDestination&lt;/code&gt;，光看 API 名稱感覺很整齊，用下去才知道有多零碎。&lt;/p&gt;
&lt;p&gt;頁面跳轉的邏輯開始出現在各個 View 裡，A 頁知道 B 頁的存在，B 頁知道 C 頁的存在。專案小的時候還好，Feature 一多，導航邏輯就像藤蔓一樣纏進每個 View，要改一個跳轉邏輯得找半天。&lt;/p&gt;
&lt;p&gt;更麻煩的是 state 管理，&lt;code&gt;navigationPath&lt;/code&gt;、&lt;code&gt;@State var isPresented&lt;/code&gt;、&lt;code&gt;.sheet&lt;/code&gt;、&lt;code&gt;.fullScreenCover&lt;/code&gt; 各自為政，散落在不同層級的 View 裡，沒有一個統一的地方可以看清楚整個 app 的導航狀態。&lt;/p&gt;
&lt;p&gt;然後你就會遇到這種需求：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;「使用者在購物車結帳完成後，dismiss 掉結帳流程，然後自動切換到『訂單』Tab，同時把購物車的 Navigation stack 清空。」&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;UIKit 的做法很直覺：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;
&lt;table style="border-spacing:0;padding:0;margin:0;border:0;"&gt;&lt;tr&gt;&lt;td style="vertical-align:top;padding:0;margin:0;border:0;"&gt;
&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;1
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;2
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;3
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;4
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%"&gt;
&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-swift" data-lang="swift"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#57606a"&gt;// 清空 stack、切 Tab，三行搞定&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;navigationController&lt;span style="color:#1f2328"&gt;?.&lt;/span&gt;popToRootViewController&lt;span style="color:#1f2328"&gt;(&lt;/span&gt;animated&lt;span style="color:#1f2328"&gt;:&lt;/span&gt; &lt;span style="color:#cf222e"&gt;false&lt;/span&gt;&lt;span style="color:#1f2328"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;tabBarController&lt;span style="color:#1f2328"&gt;?.&lt;/span&gt;selectedIndex &lt;span style="color:#1f2328"&gt;=&lt;/span&gt; &lt;span style="color:#0550ae"&gt;2&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;dismiss&lt;span style="color:#1f2328"&gt;(&lt;/span&gt;animated&lt;span style="color:#1f2328"&gt;:&lt;/span&gt; &lt;span style="color:#cf222e"&gt;true&lt;/span&gt;&lt;span style="color:#1f2328"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;SwiftUI 呢？你需要在對的時機、對的層級，同時操控 &lt;code&gt;navigationPath&lt;/code&gt;、&lt;code&gt;selectedTab&lt;/code&gt; 這些分散在不同 View 的 state，還要確保時序正確，不然畫面會閃、動畫會跳、甚至 state 不同步。更麻煩的是，深層的 View 根本拿不到外層的 &lt;code&gt;selectedTab&lt;/code&gt;，除非你用 &lt;code&gt;@Environment&lt;/code&gt; 或 &lt;code&gt;@Binding&lt;/code&gt; 一層一層往下傳，然後每個中間層都變成了導航邏輯的搬運工。&lt;/p&gt;</description></item></channel></rss>