← Notes

We rewrote the half you look at

August 6, 2026 · Foldic development notes

Foldic for Mac and Foldic for iPhone were both submitted today, rebuilt as native Swift. The part that decides what happens to your photos is the same code it was yesterday.

Six versions on a webview

Until today, Foldic's interface was a web page inside a window. That is not a confession — it shipped six versions that way, scanned real libraries, and did its job. But every album row was a document being re-rendered, and the parts of the system an app like this lives on — the menu bar, a window that remembers itself, the photo library — could only be reached through a bridge we wrote and maintained ourselves, one call at a time.

It worked. "Works" is not the bar we want to clear.

What native feels like

The interface is SwiftUI now, drawn by macOS and by iOS themselves. Most of the difference is not a feature you can list — it is a hundred small things that stop being almost right. Lists scroll the way the system's own lists scroll. The window remembers where you left it. Switching languages applies on the spot. The app opens in a blink, because there is no page to load.

And some of it is a feature. On the Mac, Foldic now lives in the menu bar: close the window and a running backup keeps going, the icon turning until it is done. A web page in a window cannot do that. This rewrite happened so that things like that stop being impossible.

The Mac app after the rewrite — the album list is the system's own, down to the scrollbar.
The Mac app after the rewrite — the album list is the system's own, down to the scrollbar.

How Swift talks to Rust

The two languages are closer than they look. The engine compiles to a plain library — one for the Mac, one for the iPhone — that gets baked into the app the way any system library would be. On top of it, a tool called UniFFI reads the engine's interface and writes the Swift side of it for us. From SwiftUI's point of view there is no seam: calling the engine is calling a Swift function, in the same process, at function-call speed. No server, no messages, nothing serialized.

The interesting part is not the plumbing but the discipline about what crosses it. Rust answers questions: which photos still need copying, what this file should be named, whether two folders overlap. Swift owns everything that belongs to the platform — the photo library, the file pickers, the permissions — and it owns every loop. When a download needs pacing, Rust hands back a number of milliseconds and Swift sleeps a task it can cancel. A cancellation flag never crosses the boundary in either direction. We learned to want that rule the hard way: the one flag that used to cross is the one that once broke every sync.

The half we refused to rewrite

The engine — the part that decides which photos still need copying, what every file is named, which folder belongs to which album — is written in Rust, and it did not change today. That is deliberate. The code that touches your files should be the most boring, most heavily tested code in the app.

Why is that half Rust and not Swift too? Three reasons, in the order they matter. It is the one place where a memory mistake would be aimed at your photos, and Rust removes that class of mistake at the language level rather than by our vigilance. It is the code we most need to test relentlessly — and a dependency-free Rust crate runs its whole test suite in a blink, on any machine, no simulator required. And it had already survived six shipped versions; a rewrite of the interface that also rewrote the engine would have put the trustworthy half at risk to improve the cosmetic one.

It is also the same engine on the Mac and on the iPhone. Both write to the same drive, so both must agree, always, about which files there are yours — and code that exists once cannot disagree with itself.

We rewrote the half you look at precisely so we would never have to rewrite the half you trust.

Update: both versions passed review — 48 hours, no rejections — and are live now. The Mac's 0.7.0 replaces the webview build directly. The iPhone's 0.7.0 jumps over two versions that never made it to the store, so user-triggered scanning, cancellable scans, readable file names, a power-saving screen and an in-app log all arrive at once. Get Foldic on the App Store.

Foldic is $29.99 once, for Mac and iPhone together.

← 開發筆記

我們重寫了你看得見的那一半

2026 年 8 月 6 日 · Foldic 開發筆記

Foldic 的 Mac 版和 iPhone 版今天都送審了,兩邊都改寫成原生 Swift。而決定你的照片會發生什麼事的那一部分,和昨天是同一份程式碼。

在 webview 上出了六個版本

今天之前,Foldic 的介面是一個放在視窗裡的網頁。這不是自首——它就這樣出了六個版本,掃過真實的圖庫,把工作做完了。但每一列相簿都是一份被重新算繪的文件,而這種 App 真正依賴的那些系統部件——選單列、記得自己位置的視窗、照片圖庫——都只能透過我們自己寫、自己維護的橋接去碰,一次一個呼叫。

它能用。但「能用」不是我們想守住的標準。

原生的手感

介面現在是 SwiftUI,由 macOS 和 iOS 自己繪製。大部分的差別不是一條條列得出來的功能——是上百件「差一點點」的小事,從此不再差那一點。清單捲動起來和系統自己的清單一樣。視窗記得你上次把它放在哪。切換語言當場生效。App 一眨眼就開好,因為沒有網頁要載入。

也有真正的功能。Mac 版現在常駐選單列:關掉視窗,進行中的備份會繼續跑,圖示會一直轉到做完為止。放在視窗裡的網頁做不到這件事。這次重寫,就是為了讓這一類的事不再是不可能。

重寫後的 Mac 版——相簿清單是系統自己的,連捲軸都是。
重寫後的 Mac 版——相簿清單是系統自己的,連捲軸都是。

Swift 怎麼跟 Rust 對話

這兩個語言比看起來更近。引擎編譯成一個普通的程式庫——Mac 一份、iPhone 一份——像任何系統程式庫一樣被打進 App 裡。在它上面,一個叫 UniFFI 的工具讀取引擎的介面,替我們把 Swift 那一側自動寫出來。從 SwiftUI 的角度看不到接縫:呼叫引擎就是呼叫一個 Swift 函式,在同一個行程裡,以函式呼叫的速度。沒有伺服器、沒有訊息傳遞、沒有任何東西要序列化。

有意思的不是管線,而是「什麼可以過橋」的紀律。Rust 負責回答問題:哪些照片還沒複製、這個檔案該叫什麼、兩個資料夾有沒有重疊。Swift 擁有一切屬於平台的東西——照片圖庫、選資料夾的視窗、權限——而且擁有每一個迴圈。下載需要節流的時候,Rust 交回一個毫秒數,Swift 去睡一個它可以取消的 task。取消旗標永遠不過橋,兩個方向都不。這條規則是付過學費才學會要的:當年唯一一個過橋的旗標,就是把每一次同步都弄壞的那一個。

我們拒絕重寫的那一半

引擎——決定哪些照片還沒複製、每個檔案叫什麼名字、哪個相簿對應哪個資料夾的那部分——是用 Rust 寫的,而它今天沒有變。這是刻意的。碰你檔案的程式碼,應該是整個 App 裡最無聊、被測得最徹底的程式碼。

為什麼那一半是 Rust,而不是也用 Swift?三個理由,按重要性排。第一,那是唯一一個記憶體錯誤會指到你照片上的地方,而 Rust 在語言層面消滅了這一整類錯誤,不用靠我們自己小心。第二,那是我們最需要反覆測試的程式碼——一個零依賴的 Rust crate,整套測試在任何機器上一眨眼跑完,不需要模擬器。第三,它已經活過六個上架的版本;如果重寫介面時連引擎一起重寫,等於拿值得信任的那一半去冒險,換好看的那一半。

而且 Mac 和 iPhone 用的是同一個引擎。兩邊寫的是同一顆硬碟,所以兩邊對「那裡哪些檔案是你的」必須永遠意見一致——只存在一份的程式碼,不可能跟自己意見不合。

我們重寫你看得見的那一半,正是為了永遠不必重寫你信任的那一半。

更新:兩個版本都過審了——48 小時、零退件——現在已經上架。Mac 的 0.7.0 直接取代 webview 版;iPhone 的 0.7.0 跨過了兩個從未上架的版本,所以手動觸發掃描、可取消的掃描、可讀的檔名、省電畫面和 App 內日誌一次全部到位。到 App Store 取得 Foldic

Foldic 一次購買 $29.99,Mac 與 iPhone 通用

← 開発ノート

見える側だけ、作り直した

2026 年 8 月 6 日 · Foldic 開発ノート

Foldic の Mac 版と iPhone 版を、今日どちらもネイティブ Swift として申請しました。写真に何が起きるかを決める部分は、昨日と同じコードです。

WebView のまま 6 バージョン

今日まで、Foldic のインターフェースはウインドウの中の Web ページでした。これは告白ではありません——その形で 6 つのバージョンを出し、実際のライブラリをスキャンし、仕事を果たしてきました。ただ、アルバムの 1 行 1 行は再描画されるドキュメントであり、この種のアプリが寄って立つシステムの部品——メニューバー、自分の位置を覚えるウインドウ、写真ライブラリ——には、自分たちで書き、自分たちで維持するブリッジを通してしか、1 呼び出しずつしか触れませんでした。

動いてはいました。でも「動く」は、私たちが守りたい水準ではありません。

ネイティブの手ざわり

インターフェースはいま SwiftUI で、macOS と iOS 自身が描画しています。違いの大半は、機能として列挙できるものではありません——「あと少しで正しい」だった百の小さなことが、その「あと少し」でなくなることです。リストはシステム自身のリストと同じようにスクロールします。ウインドウは前回の位置を覚えています。言語の切り替えはその場で反映されます。読み込むページがないので、アプリは一瞬で開きます。

そして、機能そのものもあります。Mac 版はメニューバーに常駐するようになりました。ウインドウを閉じても実行中のバックアップは続き、終わるまでアイコンが回り続けます。ウインドウの中の Web ページには、これはできません。この書き直しは、こういうことが「不可能」でなくなるために行いました。

書き直し後の Mac 版——アルバム一覧はスクロールバーまでシステム標準のものです。
書き直し後の Mac 版——アルバム一覧はスクロールバーまでシステム標準のものです。

Swift は Rust とどう話すか

この 2 つの言語は、見た目より近くにいます。エンジンは普通のライブラリにコンパイルされ——Mac 用に 1 つ、iPhone 用に 1 つ——システムのライブラリと同じようにアプリに組み込まれます。その上で UniFFI というツールがエンジンのインターフェースを読み、Swift 側のコードを私たちの代わりに書き出します。SwiftUI から見れば継ぎ目はありません。エンジンを呼ぶことは Swift の関数を呼ぶことで、同じプロセスの中、関数呼び出しの速度です。サーバーもメッセージもシリアライズもありません。

面白いのは配管ではなく、「何が境界を越えてよいか」という規律のほうです。Rust は問いに答えます。どの写真がまだか、このファイルを何と名付けるか、2 つのフォルダは重なっていないか。Swift はプラットフォームに属するすべて——写真ライブラリ、フォルダ選択、権限——を持ち、そしてすべてのループを持ちます。ダウンロードに間隔が必要なとき、Rust はミリ秒数を返し、Swift はキャンセルできるタスクとして待ちます。キャンセルのフラグは、どちらの方向にも決して境界を越えません。この規則の必要性は授業料を払って学びました。かつて境界を越えていた唯一のフラグが、すべての同期を壊したあのフラグです。

書き直すことを拒んだ半分

エンジン——どの写真がまだコピーされていないか、各ファイルを何と名付けるか、どのアルバムがどのフォルダに対応するかを決める部分——は Rust で書かれていて、今日も変わっていません。これは意図的です。あなたのファイルに触れるコードは、アプリの中でいちばん退屈で、いちばん徹底的にテストされたコードであるべきです。

なぜその半分は Rust で、Swift ではないのか。理由は 3 つ、重要な順に。第一に、メモリの間違いがあなたの写真に向かう唯一の場所であり、Rust はその種類の間違いを私たちの注意力ではなく言語のレベルで消し去ります。第二に、もっとも執拗にテストすべきコードであり——依存ゼロの Rust クレートはテスト一式がどんなマシンでも一瞬で走り、シミュレータも要りません。第三に、すでに出荷された 6 つのバージョンを生き延びたコードだからです。インターフェースの書き直しと一緒にエンジンまで書き直すのは、信頼できる半分を危険にさらして見た目の半分を良くすることになります。

しかも Mac と iPhone で同じエンジンです。どちらも同じドライブに書き込む以上、「そこにあるどのファイルがあなたのものか」について両者は常に一致していなければなりません——一度しか存在しないコードは、自分自身と食い違いようがありません。

見える側の半分を書き直したのは、あなたが信頼する側の半分を書き直さずに済ませるためです。

追記:両バージョンとも審査を通過し——48 時間、リジェクトなし——公開されました。Mac の 0.7.0 は WebView 版をそのまま置き換えます。iPhone の 0.7.0 はストアに出なかった 2 つのバージョンを飛び越えるので、手動でのスキャン開始、中止できるスキャン、読める書き出しファイル名、省電力画面、アプリ内ログが一度に届きます。App Store で Foldic を入手

Foldic は一度の購入で $29.99、Mac と iPhone の両方に対応します。

← 개발 노트

보이는 쪽 절반만 다시 썼다

2026년 8월 6일 · Foldic 개발 노트

Foldic의 Mac 버전과 iPhone 버전을 오늘 모두 네이티브 Swift로 다시 만들어 제출했습니다. 사진에 무슨 일이 일어날지 정하는 부분은 어제와 같은 코드입니다.

웹뷰인 채로 여섯 버전

오늘까지 Foldic의 인터페이스는 창 안의 웹 페이지였습니다. 이것은 고백이 아닙니다 — 그 형태로 여섯 개의 버전을 냈고, 실제 라이브러리를 스캔했고, 제 몫을 해냈습니다. 다만 앨범의 한 행 한 행이 다시 렌더링되는 문서였고, 이런 앱이 기대어 사는 시스템의 부품들 — 메뉴 바, 자기 위치를 기억하는 창, 사진 라이브러리 — 에는 우리가 직접 쓰고 직접 유지하는 브리지를 통해서만, 호출 하나씩만 닿을 수 있었습니다.

동작은 했습니다. 하지만 "동작한다"는 우리가 지키고 싶은 기준이 아닙니다.

네이티브의 감촉

인터페이스는 이제 SwiftUI이고, macOS와 iOS가 직접 그립니다. 차이의 대부분은 나열할 수 있는 기능이 아닙니다 — "거의 맞는" 상태였던 백 가지 작은 것들이 더는 그 "거의"가 아니게 되는 것입니다. 목록은 시스템 자신의 목록처럼 스크롤됩니다. 창은 지난번 위치를 기억합니다. 언어 전환은 그 자리에서 적용됩니다. 불러올 페이지가 없으니 앱은 눈 깜짝할 사이에 열립니다.

그리고 기능 그 자체도 있습니다. Mac 버전은 이제 메뉴 바에 상주합니다. 창을 닫아도 진행 중인 백업은 계속되고, 끝날 때까지 아이콘이 돌아갑니다. 창 안의 웹 페이지는 이것을 할 수 없습니다. 이번 재작성은 이런 일들이 더는 불가능이 아니게 하려고 한 것입니다.

다시 쓴 뒤의 Mac 버전 — 앨범 목록은 스크롤 바까지 시스템의 것입니다.
다시 쓴 뒤의 Mac 버전 — 앨범 목록은 스크롤 바까지 시스템의 것입니다.

Swift는 Rust와 어떻게 대화하나

이 두 언어는 보기보다 가깝습니다. 엔진은 평범한 라이브러리로 컴파일되어 — Mac용 하나, iPhone용 하나 — 여느 시스템 라이브러리처럼 앱에 구워집니다. 그 위에서 UniFFI라는 도구가 엔진의 인터페이스를 읽고 Swift 쪽 코드를 대신 써 줍니다. SwiftUI에서 보면 이음새가 없습니다. 엔진을 부르는 것은 Swift 함수를 부르는 것이고, 같은 프로세스 안에서, 함수 호출의 속도입니다. 서버도, 메시지도, 직렬화할 것도 없습니다.

흥미로운 것은 배관이 아니라 "무엇이 경계를 넘어도 되는가"라는 규율입니다. Rust는 질문에 답합니다. 어떤 사진이 아직인지, 이 파일의 이름을 무엇으로 할지, 두 폴더가 겹치는지. Swift는 플랫폼에 속한 모든 것 — 사진 라이브러리, 폴더 선택, 권한 — 을 갖고, 모든 루프를 갖습니다. 다운로드에 간격이 필요하면 Rust는 밀리초 수를 돌려주고 Swift는 취소할 수 있는 작업으로 기다립니다. 취소 플래그는 어느 방향으로도 경계를 넘지 않습니다. 이 규칙의 필요성은 수업료를 내고 배웠습니다. 한때 경계를 넘던 유일한 플래그가, 모든 동기화를 망가뜨렸던 바로 그 플래그입니다.

다시 쓰기를 거부한 절반

엔진 — 어떤 사진이 아직 복사되지 않았는지, 각 파일의 이름을 무엇으로 할지, 어떤 앨범이 어떤 폴더에 해당하는지 정하는 부분 — 은 Rust로 쓰여 있고, 오늘도 변하지 않았습니다. 이것은 의도적입니다. 당신의 파일에 닿는 코드는 앱에서 가장 지루하고 가장 철저히 테스트된 코드여야 합니다.

왜 그 절반은 Rust이고 Swift가 아닌가. 이유는 셋, 중요한 순서대로. 첫째, 메모리 실수가 당신의 사진을 향하게 되는 유일한 곳이고, Rust는 그 종류의 실수를 우리의 주의력이 아니라 언어 차원에서 없앱니다. 둘째, 가장 집요하게 테스트해야 하는 코드인데 — 의존성 없는 Rust 크레이트는 전체 테스트가 어떤 머신에서든 눈 깜짝할 사이에 돌고, 시뮬레이터도 필요 없습니다. 셋째, 이미 출시된 여섯 버전을 살아남은 코드이기 때문입니다. 인터페이스를 다시 쓰면서 엔진까지 다시 쓰는 것은, 믿을 수 있는 절반을 위험에 빠뜨려 겉모습의 절반을 좋게 만드는 일입니다.

게다가 Mac과 iPhone이 같은 엔진을 씁니다. 둘 다 같은 드라이브에 쓰는 이상, "거기 있는 어떤 파일이 당신 것인가"에 대해 둘은 언제나 일치해야 합니다 — 한 번만 존재하는 코드는 자기 자신과 어긋날 수 없습니다.

보이는 쪽 절반을 다시 쓴 것은, 당신이 신뢰하는 쪽 절반을 영원히 다시 쓰지 않기 위해서입니다.

업데이트: 두 버전 모두 심사를 통과했고 — 48시간, 리젝트 없음 — 지금 출시되었습니다. Mac의 0.7.0은 웹뷰 빌드를 그대로 대체합니다. iPhone의 0.7.0은 스토어에 나오지 못한 두 버전을 뛰어넘기 때문에, 직접 시작하는 스캔, 취소할 수 있는 스캔, 읽을 수 있는 파일 이름, 절전 화면, 앱 내 로그가 한꺼번에 도착합니다. App Store에서 Foldic 받기.

Foldic은 한 번 구매로 $29.99이며, Mac과 iPhone 모두에서 사용할 수 있습니다.