iT邦幫忙

2026 iThome 鐵人賽

DAY 12
0
Vibe Coding

神隊友.swift:30 天 Vibe Coding 打造育兒 iOS App 系列 第 12 篇

[Day 12] Share Extension 接進主 App:兩個程序怎麼共用收件匣,和第一版為什麼改方向

  • 分享至 

  • xImage
  •  

Day 12

文章同步發表在我的個人 Blog

Share Extension 接進主 App:兩個程序怎麼共用收件匣,和第一版為什麼改方向

你有沒有遇過這種狀況:太太在 LINE 傳了一個課程連結,你回了「好」,三天後兩個人都忘了那件事?

昨天一句話可以存進手機了,今天做 Day 10 排的 M2:交接卡分享出去、別的 App 分享進來,再把一句一筆改成一段話拆成多筆。225 個單元測試、三個 UI 測試流程都通過。

先講結果:功能都在,但第一版要解決的事改了。Day 9 定的是記錄孩子的近況,現在改成記下兩個人之間「還沒有結論的事」。分類、拆句、確認卡拿掉,分享進來的入口留下。這篇前半講為什麼改,後半講 Share Extension 和 App Group 怎麼接,也就是 Day 7 學習地圖的第四層「平台」。

1. 我自己都沒有打開它

今天早上有一堆事要記,我最後還是打開 LINE 用語音一口氣講完,沒有打開 Teammate。

原因不難找。App 現在是一句一筆,每一筆都要看一次確認卡。今天把它改成一段話拆成多筆、一個畫面確認,畫面長這樣:

批次確認畫面

確認從一筆一次變成一次全部,但每一筆還是有分類、內容、時間三個欄位要看,急的時候沒有人會這樣做。

回頭看我們家的狀況,全部長這樣:

  • 太太白天在顧小朋友,傳課程、活動、想去的地方過來,我在忙沒回,晚上就忘了
  • 需要討論的事一直插進工作,當下沒辦法停下來談
  • 晚上想討論的時候,要往上滑找 LINE,中間夾雜幾十則別的訊息

缺口在這裡:LINE 記得下來,但它沒有「這件事還沒有結論」這個狀態,所以訊息會滑走。 這件事育兒 App 不做,LINE 也不做。

所以第一版改成:記下兩個人之間「還沒有結論的事」。只需要三個動作:

動作 怎麼做
丟進去 打一段話送出,整段存一筆;或從別的 App 分享進來
看還沒結論的 首頁就是這份清單,跨日累積
關掉 討論完按「有結論了」

2. 拿掉什麼,留下什麼

拿掉 留下
三條關鍵字解析規則 輸入與寫入架構
確認卡 從別的 App 分享進來的入口
一段話拆成多筆與批次確認 資料模型與狀態欄位
四種分類 交接卡的產生與分享

拿掉的理由有兩個。第一,現在沒有 AI 在判斷任何東西,解析只有三條關鍵字規則決定分類,沒有模型改寫內容、沒有抽取劑量、沒有推算時間,確認卡實際保護的只是「分類可能猜錯」,而分類錯了點一下就能改。第二,首頁依分類分區,分類猜錯的後果不只是標籤錯,是那件事從首頁不見了。換手的時候需要看到的資訊,反而容易掉出畫面。

首頁三區

里程碑也重排了:同步從第四個移到第二個。這個 App 的前提是兩個人都在裡面丟東西、看同一份清單,沒有同步就不成立。

後面三節講留下來的那一塊:分享進來的入口是怎麼做的。

3. Share Extension 是另一個程式

Share Extension 是 iOS 分享表單裡那一排 App 圖示背後的東西。使用者在 Safari 按分享、點你的 App,系統會啟動一個獨立的小程式,不是你的主 App。

用 Laravel 來對照,它比較像 queue worker:跟 web 是兩個程序,各自啟動、各自有記憶體上限,只是這裡兩邊不共用資料庫。

主 App Share Extension
誰啟動 使用者點圖示 系統從分享表單啟動
沙盒 自己一份 自己一份
記憶體 一般 上限很低
生命週期 使用者關掉才結束 投遞完成就被系統結束

兩個程序各有各的沙盒,要交換資料,需要 Day 10 提到的 App Group:兩個 target 宣告同一個 group ID,系統就給一個共用的資料夾。這個專案的 group ID 是 group.com.gooliya.Teammate,寫在兩個 target 的 entitlements 裡。

架構文件寫的是「Extension 只投進收件匣,主 App 才正式匯入」,做出來就是這樣:

Share Extension 投遞、主 App 匯入的流程

Extension 不碰 SwiftData,原因有兩個。第一,Extension 的記憶體上限很低,開一個資料庫不划算。第二,兩個程序同時寫同一個資料庫,衝突處理會很麻煩,而且那是同步才要解的題目。

4. 收件匣只有四個欄位

每一筆分享是一個獨立的 JSON 檔,用原子寫入:先寫暫存檔再改名,主 App 只會看到寫完整的檔案。檔案格式只有四個欄位:

{"schemaVersion":1,"id":"<UUID>","receivedAt":"2026-09-21T14:13:20Z","text":"週六去公園嗎?"}

Extension 接受兩種東西:純文字,或 http(s) 開頭的網址。圖片、PDF、多個附件、超過 1000 字,都直接拒絕,不截斷、不挑第一個。

主 App 匯入的順序是:一次一個檔,存進資料庫成功之後立刻刪掉來源檔,再處理下一個。刪不掉就顯示錯誤、檔案留著,下次開 App 會再匯入一次,多一筆重複的資料。這裡刻意不做去重,那是同步才需要的機制。

分享進來的內容一律存成「待整理」,不套 Day 11 那套規則。原因是分享進來的是別人講的話。太太傳「這個課程好嗎?」,如果套用「問句結尾是請求」的規則,會變成一筆要對方回覆的請求,可是那是她在問我,語意剛好反過來。改了方向之後,這一類就是「還沒有結論的事」,明天改首頁的時候會直接沿用。

匯入之後,首頁最上面會顯示收進幾筆,內容進「待整理」:

匯入之後

關掉 App 再打開,「已匯入」的提示不再出現,兩筆都還在,沒有重複:

重開之後

5. 交接卡:分享出去也留著

交接卡取裝置本地時區「今天」的紀錄,依時間新到舊排,套一個固定模板:

交接摘要(當天的紀錄,共 3 筆)
• 待整理|https://example.com/
• 待整理|週六去公園嗎?
• 待辦|買尿布(時間:明天下午)

「今天」用日曆加一天算上界,不是固定加 86400 秒,日光節約那天才不會多算或少算一小時。上限 50 筆,超過的整筆省略,結尾標「還有 N 筆未列出」,不會切到半句。

產生之後是一個可以編輯的文字框,補一句再按分享,走系統的分享表單,要傳給誰自己選。取消分享,補的那句還在;關掉編輯頁,草稿就丟掉,下次重新產生。

交接卡編輯

分享進來是入口,交接卡是出口,兩邊都留著。方向改了之後,卡片上列的內容會跟著換,產生和分享的機制不動。

6. 兩個單元測試沒抓到的 bug

單元測試全綠,模擬器上跑端到端卻出現匯入不進來。

第一個:URL.path() 會把空格編成 %20。 App Group 的收件匣路徑裡有 Application Support,程式用 path() 轉成字串去列目錄,找的是一個叫 Application%20Support 的資料夾,當然找不到,回傳空清單,也不報錯。Apple 文件的原型是:

func path(percentEncoded: Bool = true) -> String

預設值就是 true。單元測試用的暫存目錄剛好沒有空格,所以一直是綠的。修法是改成 path(percentEncoded: false),並且補一個用有空格目錄的測試。

第二個:啟動時匯入跑了兩輪,第二輪把第一輪的結果蓋掉。 開 App 時,畫面出現觸發一輪匯入,App 進入前景又觸發一輪。第一輪匯入 2 筆,第二輪什麼都沒有,畫面上「已匯入 2 筆待整理」就被全零的結果蓋掉,看起來像沒匯入。修法是匯入筆數改成本次啟動累計,錯誤數才用最新一輪。

規格寫了「同時最多一輪」,沒寫「連續兩輪的顯示」。這種東西只有實際跑起來才看得到。

對照學習地圖,這兩個 bug 一個在第一層、一個在第二層:path() 是 Foundation 的 API 預設值,去官方文件查原型就有答案;兩輪匯入是 SwiftUI 的生命週期,畫面出現和回到前景是兩個事件。

7. 模擬器的三個坑

寫功能的部分 Claude Code 照規格一路做完,時間反而花在模擬器上。三個坑都不是程式的問題,都是環境的。

第一輪:UI 測試卡住十分鐘。 測試要驗證「拷貝」之後剪貼簿的內容,測試程式直接讀 UIPasteboard。iOS 會跳一個「允許貼上」的詢問框,測試就停在那裡等,當時沒設逾時,只能等到十分鐘上限被強制中斷。所以重新調整:改成在 App 的輸入框長按選「貼上」,比對貼出來的文字,跟交接卡一字不差;UI 測試一律加 300 秒逾時。

貼上的結果

第二輪:iOS 26 的 Safari 找不到分享按鈕。 自動化測試想從 Safari 分享到 Teammate,ShareButton 找不到;改從右下角的「更多」選單進去,還是找不到。試了兩次就停,這一步改成人工操作,Extension 有沒有登錄改用 pluginkit 查:

xcrun simctl spawn <udid> pluginkit -m -i com.gooliya.Teammate.Share
# → com.gooliya.Teammate.Share(1.0)

第三輪:simctl 拿不到 App Group 的路徑。 要往收件匣放測試檔,得先問模擬器那個資料夾在哪。這個指令:

xcrun simctl get_app_container <udid> com.gooliya.Teammate group.com.gooliya.Teammate

在 Xcode 26.6 只會印出用法說明,沒有錯誤訊息。一開始以為是模擬器還沒開好,加了等待和重試,還是一樣。後來發現改用 groups 列出全部容器再取那一行就正常:

xcrun simctl get_app_container <udid> com.gooliya.Teammate groups
# → group.com.gooliya.Teammate	/…/Shared/AppGroup/<UUID>

規格能寫的是功能,寫不出來的是模擬器和系統對話框。這三個坑都在第四層,Day 7 那張圖寫的症狀是「裝不到手機、權限被拒」,今天多了一種:指令不報錯,只是沒有結果。

8. 驗收看了什麼

項目 結果
build(含 Share Extension 內嵌) 成功
單元測試 225 個,全通過
UI 測試 3 個流程(M1 回歸、交接卡、收件匣匯入),全通過
靜態掃描 Extension 編入的檔案沒有 import SwiftData;沒有 print、Logger、URLSession
App Group 兩個 target 的 entitlements 都有 group.com.gooliya.Teammate

還有三項要人工做:Safari 分享、備忘錄分享、開著確認卡切出去分享再切回來。

明天

Day 13 開始改:拿掉解析與確認卡、一段話送出直接存、首頁換成「還沒有結論」的清單、加上每晚的提醒。

參考資源

官方文件


上一篇
[Day 11] 一句話存成一筆紀錄:三條規則的解析,和 migration 要寫幾段
下一篇
[Day 13] 投票第一週結果:說出口還不夠,所以第一版重做
系列文
神隊友.swift:30 天 Vibe Coding 打造育兒 iOS App 共 15 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言