QUEST LOG · June 2, 2026· UPDATED October 29, 2026
如何優雅的過橋,放棄 SwiftUI,擁抱 Expo
原本一直以為跨平台一定會卡、一定會慢,最近認真用了 Expo 跟它生態上的工具,發現事實不是如此。
我做 Solid(ar)ity(一個自己掌握鑰匙的身份頁跟名片 app)的時候,第一版是用 SwiftUI 寫的。2025 年 9 月開始寫,原生、漂亮、Apple 親生的,那時候覺得這就是正確答案。
結果 2026 年 5 月底開了 Expo 的專案,6 月底就把整個 SwiftUI app、Xcode 專案跟 Swift 設定全部踢掉了。一個月,整個過橋(雖然大量的功勞可能是 Claude)。
這篇記錄一下為什麼要過、怎麼過,還有過完之後發現的事。
# 為什麼要過橋
最直接的理由是 Android。名片跟身份這種東西,對方用什麼手機你管不了,只做 iOS 等於一半的人掃不到你的卡。用 SwiftUI 就代表 Android 要整個重寫一份,兩份 code 要同步兩份加密邏輯,光想到就頭痛。
而且我們核心機制 airdrop 突然被 pixel 支援了,只追新的好奇寶寶我就直接把所有功能做上去看是不是真的能支援 (後面我朋友拿 pixel 的人說支援了 XD)
第二個理由是迭代速度。SwiftUI 的 Preview 時好時壞,改個 UI 等編譯等到懷疑人生;Expo 改完存檔,畫面就變了。每次變動 UI 根本在坐牢,而且 swiftUI 甚至沒比較快,一堆地方要用回 UIKit ..
# 過橋的恐懼:以為跨平台會遇到的問題
以前對 React Native 的印象大概停在幾年前:
- bridge 很慢,動畫會卡
- 碰到原生功能(相機、Keychain、Face ID)就要自己寫 native module,寫到哭 (好險現在我有 AI 哈哈)
- 加密、零知識證明這種重運算,JS 一定撐不住,何況我還搞 ZK
結果實際用下來,這幾個問題現在都有解。
# 實際上的解法,吃我 Nitro 吧小子
1. 原生的東西,用 Nitro Modules 寫
需要碰原生的地方,像是護照 NFC 驗證、Keychain、Apple Vision 的 OCR,我們都寫成 Nitro Module。TypeScript 寫規格,codegen 產生 C++/Swift/Kotlin 的骨架,再填實作。JS 呼叫原生就像呼叫一般函式,沒有以前 bridge 那種序列化的開銷。
2. 相機用 VisionCamera v5
QR 掃描、護照 MRZ 辨識都靠 VisionCamera 的 frame processor。這部分踩的坑多到可以另外寫一篇(真的另外寫了,〈地獄難度:手機相機效能優化〉)。
3. 重運算放在原生,JS 只負責流程
ZK 證明用 Mopro 編好的 xcframework,在原生端跑,JS 只負責把結果接回來。效能跟純原生一樣,因為它本來就是原生。
# 怎麼過橋不掉下去
最怕的是:舊版 app 的使用者資料怎麼辦?加密格式不一樣,備份就全部報銷了。
我們的做法是先寫對拍測試。在 Swift 版加一個 FixtureExporter,把 AES-GCM 加密的結果、名片的資料格式匯出成測試資料,然後 Expo 版用 bun test 跑同一份資料,確保兩邊加密出來的東西一模一樣、互相解得開。對拍全部通過之後,才敢把 Swift 版刪掉。
後來也真的有用到:從 Swift 時代留下來的備份檔,格式跟新版不太一樣,還是補了一個修正讓新版讀得懂舊備份。如果沒有先做對拍,這種問題大概是上線後才會被使用者罵出來。
# 過完橋之後
現在 Solid(ar)ity 2.0 是 Expo SDK 57、React Native 0.86,iOS 跟 Android 一份 code。發現幾件事:
- 跨平台不等於慢。慢的通常是你把重的東西放在 JS 跑。原生該做的事交給原生,JS 只管流程跟 UI,體感跟原生 app 差不多
- 但原生知識沒有省掉。Xcode Cloud 的建置、Metal shader 預編譯、Android 的 minSdk,這些坑一個都沒少,只是從「整個 app 都是坑」變成「邊界上有坑」
- SwiftUI 沒有不好。只做 iOS、而且很吃 Apple 新 API 的話,它還是最好的選擇。我只是剛好不是那種 app
所以標題說「優雅的過橋」,其實一點都不優雅,就是先把兩岸的東西對齊,確定踩得過去,再把舊橋拆掉。能優雅,是因為先做了很不優雅的對拍。
話說 Solid(ar)ity 裡面還很不優雅的塞著很多 Swift 檔案呢 XD
