<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Tai Phan</title>
    <link>https://taiphanvan.dev</link>
    <description>Canvas cá nhân của Tai — engineer ở Việt Nam, làm sản phẩm. Tech, AI, phim, sách, ảnh, game, bất cứ gì muốn ghi.</description>
    <language>vi-VN</language>
    <lastBuildDate>Sat, 23 May 2026 00:00:00 GMT</lastBuildDate>
    <atom:link href="https://taiphanvan.dev/rss.xml" rel="self" type="application/rss+xml" />
    
    <item>
      <title>QuickSpend now really quick, finally</title>
      <link>https://taiphanvan.dev/vi/blog/quickspend-now-really-quick-finally</link>
      <guid isPermaLink="true">https://taiphanvan.dev/vi/blog/quickspend-now-really-quick-finally</guid>
      <pubDate>Sat, 23 May 2026 00:00:00 GMT</pubDate>
      <description>QuickSpend 3.0 thêm voice-first flow qua Siri + Shortcuts — chi tiêu nhanh đến mức không cần mở app. Bonus: cuộc chiến nắm tay Siri.</description>
      <content:encoded><![CDATA[<p><a href="https://apps.apple.com/us/app/quickspend-instant-tracker/id6754815176">QuickSpend</a> là app iOS mình build để log chi tiêu nhanh bằng câu nói tự nhiên — và phiên bản 3.0 sắp ship, cuối cùng cũng đúng cái tên <code>Quick</code>. Đặt tên <code>Quick</code> từ v1, sau hai phiên bản mới thấy hơi sai sự thật, giờ thì thấy hợp lí rồi =)).</p>
<p><img src="/blog/quickspend-3/frodo-finally.jpg" alt="it&#x27;s over, finally">
<em>"It's done. It's over." — Frodo, The Lord of the Rings.</em></p>
<h2>Vấn đề: app "quick" nhưng vẫn chưa đủ quick</h2>
<p>QuickSpend ban đầu mình build cho chính mình dùng — eat own dogfood, ít nhất để sản phẩm mọi người dùng được thì mình phải dùng được và thấy ổn trước đã. Dùng vài tháng thì lộ ra một điều khá đau lòng: chính mình vẫn quên ghi chi tiêu đều đặn =)).</p>
<p>Lý do thì quá đơn giản — vấn đề chắc chắn ai cũng gặp: quá lười, quá vội để note lại chi tiêu.</p>
<p>Cuối tháng nhìn lại chart, data thưa như tóc mình sau 30 tuổi. Vậy là vấn đề không nằm ở app, nó nằm ở <strong>bước phải mở app</strong>. Cái cần loại bỏ chính là cái đó. Mình muốn một flow mà user chỉ cần nói "chi tiêu nhanh 50k trà sữa" là xong, không cần tap gì hết, không cần nhìn màn hình, đẹp như mơ.</p>
<h2>Hướng xử lý: đẩy entry point ra khỏi app</h2>
<p>Trên iOS có hai con đường rõ ràng:</p>
<ul>
<li><strong>Nói thẳng với Siri</strong> — kịch bản đẹp nhất, không tap gì hết, sang chảnh như quảng cáo Apple.</li>
<li><strong>Shortcuts</strong> — backup thực dụng cho khi Siri trở chứng. Trigger được bằng tap, widget, hoặc gọi qua Siri đều ngon.</li>
</ul>
<p>Siri thì... vẫn ngu ngu, đôi khi mình hỏi giờ Hà Nội còn trả lời sai múi giờ =)). Nhưng Shortcuts là vũ khí mạnh thật trong hệ sinh thái Apple — một khi setup xong nó chỉ là một nút bấm. Nên mình build cả hai, dùng Shortcuts làm safety net để khi Siri drop ball thì user vẫn không bị chặn.</p>
<h2>Luồng mới: hai kịch bản</h2>
<h3>Kịch bản 1 — qua Shortcut (path luôn work)</h3>
<blockquote>
<p>"Hey Siri, chi tiêu nhanh."</p>
</blockquote>
<p>Siri trigger shortcut → shortcut hỏi lại "nội dung gì?" → user nói câu tự nhiên (ví dụ "50k trà sữa") → loading → AI parser xử lý.</p>
<p>Hai nhánh tuỳ confidence của parser:</p>
<ul>
<li><strong>Confidence cao</strong>: app lưu luôn, Siri thông báo "đã ghi", end flow. Không cần mở app, không cần nhìn màn hình, đẹp như mơ.</li>
<li><strong>Confidence thấp</strong>: app mở ra với UI listing transaction được parse, user confirm lưu hoặc cancel. App không tự tin thì cứ hỏi, hơn là lưu bừa rồi user phải vào sửa.</li>
</ul>
<h3>Kịch bản 2 — gọi thẳng app qua Siri (path đôi khi work)</h3>
<blockquote>
<p>"Hey Siri, dùng Chi tiêu nhanh để thêm chi tiêu 50k uống trà sữa."</p>
</blockquote>
<p>Kịch bản đẹp: Siri hiểu intent, gọi thẳng app, app nhận text Siri đã transcribe sẵn → AI parser xử lý y hệt luồng trên. <strong>Đỡ hẳn một bước</strong> — user không phải đợi Siri hỏi lại "nội dung gì". Sướng phải không nào?</p>
<p>Đời không như là mơ. Mình test khá nhiều lần, kết quả không hề ổn định =)). Mà oan cho Siri một phần — đào sâu vào API mới phát hiện đây không hoàn toàn lỗi của em ấy: <code>AppShortcutsProvider</code> của Apple chỉ cho phép phrase có parameter khi parameter là <code>AppEntity</code> hoặc <code>AppEnum</code>. Với free-form <code>String</code> (như nội dung chi tiêu), phrase <strong>bắt buộc</strong> phải open-ended — Siri trigger intent rồi mới hỏi lại "What expense should I add?". Mình sẽ nói thêm ở phần kỹ thuật bên dưới.</p>
<p>Hệ quả: câu kiểu "Hey Siri, dùng Chi tiêu nhanh thêm 50k trà sữa" về mặt API là không declarable được, Siri đoán mò. Đó là lý do Shortcut variant tồn tại — nó là path mình tin được, khi Siri đang có hứng làm việc thì kịch bản 2 mới đẹp.</p>
<p>Nghe đồn với Apple Intelligence thì Siri sẽ thông minh hơn, hy vọng là vậy để kịch bản 2 ổn định hơn trong tương lai. Hiện tại thì mình chưa test được vì đang chỉ có iPhone 15, bạn nào có iPhone 16 Pro hoặc 17 thử giúp mình xem Apple Intelligence có cứu được kịch bản 2 không nhé =)).</p>
<h2>Mấy vấn đề khi build feature này</h2>
<p><strong>Đồng bộ ngôn ngữ giữa Siri và app</strong>. Trước đó QuickSpend tách ngôn ngữ hiển thị và ngôn ngữ transcribe — user có thể chạy UI tiếng Anh nhưng voice input tiếng Việt, kiểu menu Tây mà order Việt. Khi thêm Siri vào, separation đó vỡ — Siri chỉ nói một ngôn ngữ tại một thời điểm theo system. Mình phải bỏ tách biệt đó, gộp về một locale duy nhất. Trade-off đáng vì consistency quan trọng hơn flexibility mà chắc chỉ có mỗi mình dùng.</p>
<p><strong>Tên app khi Siri gọi</strong>. Như đã thấy ở kịch bản 2 — Siri nhận diện app name không ổn định, đặc biệt khi user trộn Anh–Việt trong câu lệnh. Mình thử nhiều phrasing khác, kết quả tốt nhất hiện tại vẫn là: đừng tin Siri, dùng Shortcut.</p>
<p>Liên quan đến tên app nữa, mình đã thêm tên app theo locale: tiếng Việt là <code>Chi tiêu nhanh</code>, tiếng Anh vẫn là <code>QuickSpend</code>, còn tiếng Nhật, tiếng Tây Ban Nha nữa — không nhớ là gì, dùng translator dịch =)).</p>
<p>Với tên app như vậy, khi trigger bằng Siri sẽ có câu kiểu:</p>
<blockquote>
<p>"Hey Siri, thêm chi tiêu vào Chi tiêu nhanh."</p>
</blockquote>
<p>Nghe nó lủng củng sao sao ấy, nhưng chưa biết làm sao vì để Siri nhận diện thì vẫn phải kèm tên app.</p>
<h2>Phần kỹ thuật (skip OK nếu không quan tâm)</h2>
<p><p>Section này dành cho ai tò mò Apple stack. Không quan trọng để dùng feature — nhảy thẳng xuống
<a href="#t%E1%BB%95ng-k%E1%BA%BFt">phần tổng kết</a> cũng được.</p></p>
<p>Bốn thành phần chính:</p>
<ul>
<li><strong>App Intent (<code>AddExpenseIntent</code>)</strong> — define action với <code>@Parameter</code> text + <code>requestValueDialog</code> ("What expense should I add?"). Return type <code>IntentResult &#x26; ShowsSnippetView &#x26; ProvidesDialog</code> để Siri vừa đọc dialog vừa show snippet card. Đặt <code>openAppWhenRun = false</code> để happy path chạy hoàn toàn ngoài app — không launch UI, Siri đọc kết quả là xong.</li>
<li><strong><code>AppShortcutsProvider</code> + localized phrases</strong> — declare phrases EN với <code>\(.applicationName)</code> interpolation, các locale <code>vi/ja/es</code> seed vào <code>AppShortcuts.xcstrings</code>. <strong>Limitation đã nhắc ở trên</strong>: phrase chỉ accept parameter là <code>AppEntity</code>/<code>AppEnum</code>. Với free-form <code>String</code> thì phrase bắt buộc open-ended, Siri phải hỏi value qua dialog — đây là root cause của kịch bản 2 flaky.</li>
<li><strong>Snippet view + confirm flow</strong> — <code>ParsedExpenseSnippetView</code> là SwiftUI view render inline trong Siri card. Confidence ≥ 0.9 → <code>.result(dialog:)</code> auto-save, Siri đọc xác nhận, end flow. Confidence thấp → <code>requestConfirmation(actionName: .log, dialog:, snippet:)</code> ra card có nút Cancel / Log. Edge case khó chịu: label nút lấy từ system enum <code>ConfirmationActionName</code>, localize theo system locale chứ không theo app language — chưa fix được.</li>
<li><strong>Bundled Shortcut "Quick Expense"</strong> — chain <code>Dictate Text → AddExpenseIntent</code>, giúp user nói liền một mạch thay vì đợi Siri hỏi lại "nội dung gì". Đây là Shortcut cài qua link "Add to Shortcuts" ngay trong app.</li>
</ul>
<p>Code chi tiết hơn để dành bài khác — bài này dài quá thì các bạn lại skip mất.</p>
<h2>Tổng kết</h2>
<p>Sau patch 3.0, QuickSpend cuối cùng cũng đúng cái tên <code>Quick</code>:</p>
<ul>
<li>✅ <strong>Voice-first flow</strong>: log chi tiêu không cần mở app trong happy path.</li>
<li>✅ <strong>Shortcut safety net</strong>: luôn có path ổn định khi Siri trở chứng.</li>
<li>✅ <strong>AI parser auto-save</strong> khi confidence cao — Siri thông báo, không gián đoạn flow của user.</li>
</ul>
<p>Điểm chưa ổn:</p>
<ul>
<li>❌ Kịch bản "gọi thẳng app qua Siri" còn flaky. Mình đổ lỗi cho Siri, không nhận =)).</li>
<li>❌ Chưa test được với Siri có Apple Intelligence vì chưa lên đời máy xịn hơn iPhone 15. Bạn nào có iPhone 16 Pro hoặc 17 thử giúp mình, nếu Apple Intelligence cứu được kịch bản 2 thì mình sẽ rất biết ơn.</li>
</ul>
<p>Feature này thực sự hữu ích với chính mình — ba ngày qua data trong app full hơn cả tháng trước cộng lại. Nếu các bạn cũng dùng QuickSpend, update lên 3.0 khi nó ship và quẩy thử. Feedback gì cứ ib mình hoặc gửi qua form feedback trong app — mình đọc hết.</p>
<p>Còn nhiều thứ phải cải thiện trong luồng này:</p>
<ul>
<li>Cho user edit trực tiếp transaction trong UI confirm khi parser nhầm — thay vì chỉ save / cancel.</li>
<li>Polish prompt để parser thông minh hơn, handle được nhiều cách nói tự nhiên hơn của user.</li>
<li>Tìm thêm hướng tối ưu luồng — bớt step, bớt loading, bớt phụ thuộc Siri nếu có thể.</li>
<li>Khi có điều kiện nâng máy lên dòng có Apple Silicon hỗ trợ Apple Intelligence (iPhone 15 Pro trở lên), mình sẽ research <strong>Foundation Models framework</strong> (WWDC 2025) — Swift API gọi thẳng model ~3B on-device của Apple, có guided generation và tool calling. Hứa hẹn parser chạy hoàn toàn offline, không cần gọi cloud.</li>
</ul>
<p>Hy vọng sẽ có bài kế nữa về QuickSpend — nếu có người dùng thật thì mình mới có động lực update tiếp =)).</p>
<p>Hẹn gặp lại ở bài sau.</p>]]></content:encoded>
      <category>quickspend</category>
      <category>ios</category>
      <category>siri</category>
      <category>indie</category>
    </item>
    <item>
      <title>Hello world: Tai is calling</title>
      <link>https://taiphanvan.dev/vi/blog/hello-world</link>
      <guid isPermaLink="true">https://taiphanvan.dev/vi/blog/hello-world</guid>
      <pubDate>Thu, 07 May 2026 00:00:00 GMT</pubDate>
      <description>Sau nhiều năm lặng lẽ, mình cất tiếng. Canvas cá nhân — không gò 1 niche. Tech, AI, phim, sách, ảnh, game, bất cứ gì muốn ghi.</description>
      <content:encoded><![CDATA[<h2>Intro</h2>
<p>Bắt đầu một thứ luôn là rất khó, duy trì nó lại càng khó hơn. Mình thích làm điều khó nên mình làm trang này =)).</p>
<p>Có thể viết bài với nhiều người là điều bình thường nhưng với mình thì đây là một bước nhảy vọt. Mình sẽ cố gắng duy trì đều đặn, không để quá 2 tuần không có bài mới. Sẽ không hoàn hảo, nhưng ai cũng phải bắt đầu từ đâu đó.</p>
<p>Bắt đầu của mình là từ đây, bài viết đầu tiên, sau nhiều năm lặng lẽ quan sát, lắng nghe, giờ đến lúc cất tiếng, chào thế giới.</p>
<h2>Site này là gì</h2>
<p><code>taiphanvan.dev</code> không phải blog kỹ thuật thuần, cũng không phải portfolio. Đây là <strong>canvas cá nhân</strong> — chỗ mình tô vẽ mọi ý tưởng, ghi mọi thứ mình muốn ghi, làm mọi thứ mình muốn làm.</p>
<p>Lưu ý, mọi bài viết đều đậm tính cá nhân, nếu bạn thấy không hợp, có thể trao đổi với mình, mình luôn sẵn sàng mở rộng góc nhìn, tiếp cận vấn đề theo một góc khác — hoặc không, bạn có thể cứ lặng lẽ rời đi.</p>
<h2>Mình đang chuyển sang làm sản phẩm</h2>
<p>Trước giờ mình quen làm theo spec do người khác viết — thuần engineer. Mobile, web, vài năm chuyển stack, nhưng vai trò không đổi: nhận task, code, ship.</p>
<p>Cái mình đang đổi không phải stack — là <strong>vai trò</strong>. Mình muốn đứng từ đầu chuỗi: nhận diện vấn đề, design giải pháp, kiến trúc hệ thống, rồi mới đến code.</p>
<p>Gọi là gì cũng được — product owner, solution architect, system engineer — quan trọng là không gò bó vào 1 domain nữa. Mobile, web, backend, AI infra, devtools... ngon ở đâu thì làm ở đó. Stack chỉ là công cụ; vấn đề mới là cái cần giải.</p>
<h2>Sẽ viết gì</h2>
<p>Đối tượng đọc: bất cứ ai. Có thể là dev đang search debug log, có thể là bạn vô tình search "review phim X" rồi click vào.</p>
<p>Categories dự kiến:</p>
<ul>
<li><strong>tech</strong> — note từ build production, debug stories, pattern lặp lại</li>
<li><strong>ai</strong> — cách mình dùng LLM trong daily work, thí nghiệm, thất bại</li>
<li><strong>product</strong> — quyết định thiết kế, trade-off giữa "ngon" và "ship được"</li>
<li><strong>film, books</strong> — cái mình vừa xem, vừa đọc, đáng ghi lại</li>
<li><strong>photography</strong> — ảnh chụp + ghi chú thiết bị / setup</li>
<li><strong>games</strong> — game đang cày, vài cảm giác về game design</li>
<li><strong>thoughts</strong> — bất cứ thứ gì còn lại</li>
</ul>
<p>Không ép format. Một bài có thể 200 chữ, có thể 2000. Có quan điểm, không hedge. Sai thì sửa công khai.</p>
<h2>Roadmap</h2>
<p>Chưa có roadmap cụ thể về topic — đặt sẵn cứng dễ thành tự gò bản thân. Nhưng có cam kết về <strong>tần suất</strong>:</p>
<ul>
<li><strong>Target</strong>: 1 tuần 2 bài.</li>
<li><strong>Mandatory</strong>: 1 tuần ít nhất 1 bài.</li>
</ul>
<p>Tuần nào không có bài nào, coi như mình đang lười.</p>
<hr>
<p>Hẹn gặp lại bro ở bài sau.</p>]]></content:encoded>
      <category>meta</category>
      <category>personal</category>
      <category>indie</category>
    </item>
  </channel>
</rss>