Kamakailan ko lang na-ship ang offline downloads para sa iOS app ng DIALØGUE — yung mismong parte na sa nakaraang post ko ay binanggit ko lang nang pasaglit bilang "resilient offline" pero hindi ko naman talaga ipinaliwanag kung ano ibig sabihin noon. Itong post na ito yung paliwanag. At nagsisimula ito sa muntik ko nang paggawa ng maling bagay.
Eto yung confession. Ang DIALØGUE ay isang AI podcast app: bibigyan mo ito ng topic o ng PDF, tapos gagawa ito ng usapan ng dalawang host. Kaya nung sinimulan ko yung offline mode, diretso agad ang utak ko sa kung anong AI-shaped na bagay. Baka ang ibig sabihin ng "offline" ay mag-pre-generate ng audio sa device mismo. Baka mag-cache ng output ng model, o kung anong clever na pipeline na kayang i-rebuild ang isang episode kahit walang network. May buong architecture nang nabubuo sa isip ko. Muntik ko nang isulat ang design doc nang ganoon.
Tapos huminto ako at nagtanong ng mas nakakabagot na tanong: ano na ba ang binibigay sa akin ng iOS para dito?
Lumabas na ang sagot ay "halos lahat." At ang tunay na trabaho sa feature na ito ay hindi ang gumawa ng matalino. Ang trabaho ay pigilan ang gana na gawin yun.
Yung nakakabagot na tanong ang nagligtas sa akin ng isang buwan
Eto yung bagay na paulit-ulit kong natututunan, post pagkatapos ng post: hindi mo kaibigan ang complexity. Lalo na kung mag-isa ka lang na nagtatrabaho tuwing gabi at weekend.
Ang isang AI podcast, kapag nagawa na, ay isa lang namang audio file na nakatambay sa isang URL. Ang "offline" ay hindi nangangahulugang gawin ito ulit. Ibig sabihin nito ay i-download yung file at pakinggan mamaya. Hindi yun AI na problema. Problema yun na nilulutas na ng Apple bago pa man umiral ang App Store.
Kaya sa halip na clever na pipeline, hinawakan ko yung pinaka-nakakabagot at pinaka-subok na tool sa kahon: isang background URLSession. Kung hindi mo pa ito nakilala, simple lang ang idea — ibibigay mo yung download sa operating system, at tutuloy-tuloy ito kahit nasa background na ang app mo o napatay na, tapos ire-relaunch nito ang app mo para iabot sa iyo yung tapos nang file.
let config = URLSessionConfiguration.background(withIdentifier: "com.yourapp.downloads")
config.sessionSendsLaunchEvents = true
Mga dalawang linya, at ngayon ang operating system na ang may hawak sa mga tunay na mahihirap na parte. Hindi ako gumawa ng download engine. Hiniram ko yung kay Apple.
Paulit-ulit ang parehong kwento saan man ako tumingin:
- Naputol ang download sa gitna? Bibigyan ka ng iOS ng "resume data" — isang maliit na blob na nagpapahintulot sa download na magpatuloy kung saan ito huminto sa halip na magsimula mula sa wala. Isinusulat ko lang ito sa disk (sa
Cachesfolder, dahil pwede naman itong itapon) tapos ibinabalik ko sa susunod. - Ayaw mong ubusin ang cellular data ng user? Isang linya:
request.allowsCellularAccess = !wifiOnly. Pagkatapos, ang operating system na mismo ang naghihintay ng WiFi. Hindi ako nagmo-monitor ng koneksyon. Wala akong pino-poll na kahit ano. - Kailangan mong ipakita kung gaano kalaking espasyo ang nilalaman ng mga download? Ang
ByteCountFormatterang nagpapalit ng byte count tungo sa "23.4 MB" na may tamang localized units. Hindi ko rin yun sinulat.
Gusto kong maging totoo kung gaano kasarap ang pakiramdam nito, kasi salungat ito sa instinct. Pagbalik-tanaw, ang tamang galaw ay hindi mas clever na code. Ito ay ang pag-amin na isa na itong solved na problema at ang paghiram sa trabaho sa halip na subukang daigin ito sa talino.
Pero ang "nakakabagot" ay hindi nangangahulugang "madali"
Ngayon kailangan kong umamin ng isang bagay, kasi kung sasabihin ko sa iyo na ginawang trivial ng standard practices ito, magsisinungaling ako — at mahuhuli ako ng kahit sinong iOS developer na nagbabasa nito sa loob ng mga sampung segundo.
Ang standard ay hindi pareho sa trivial. Nakakabagot ang mga boring na tool kasi gasgas na sila, hindi kasi walang hirap. Eto yung mga bagay na talagang umubos ng oras ko, baka makatipid ka sa listahan:
1. Nawawala ang temp file habang tinitingnan mo pa lang. Kapag natapos ang isang background download, tatawagan ka ng iOS pabalik at ituturo nito yung file sa isang temporary na lokasyon. Yung catch na walang nagsasabi: bubura yung file na yun sa mismong sandali na nag-return ang callback mo. Kailangan mong ilipat ito sa permanenteng lugar nang synchronously, doon mismo sa function, bago ka gumawa ng kahit anong iba. Mas nagiging awkward pa, kasi ang callback na yun ay tumatakbo sa labas ng main thread at ang alam lang nito ay aling download ang natapos, hindi aling episode ito sa salita ng app ko. Kaya gumagawa ang code ng isang pangit na two-step: ilipat agad yung file sa temporary na pangalan, tapos lumukso sa main thread para i-rename ito kapag alam na kung ano ito. Hindi maganda. Pero ang alternatibo ay isang file na wala na bago mo pa ito ma-save.
2. Tahimik na walang ginagawa ang background sessions sa simulator. May nakakahiyang yugto akong ginugol na kumbinsido na sira ang code ko. Hindi pala. Ang background URLSession ay basta hindi lang umaasal nang maaasahan sa iOS Simulator — walang error, walang crash, walang nado-download. Pangit ang ayos at aaminin ko:
#if DEBUG
// Use a default session in the simulator — background sessions can silently fail
session = URLSession(configuration: .default, delegate: self, delegateQueue: nil)
#else
session = URLSession(configuration: .background(withIdentifier: identifier), delegate: self, delegateQueue: nil)
#endif
Hindi elegante yung #if DEBUG na yan. Pero ang magpanggap na ang simulator ay umaasal na parang tunay na device ay mas magmamahal sa akin kaysa sa pag-amin na hindi ito ganoon.
3. Pinilit ako ng Swift 6 na sabihin nang eksakto kung sino ang tumatakbo sa aling thread. Dumarating ang download callbacks sa isang background thread. Ang UI state ko — yung maliliit na progress ring, yung berdeng "Downloaded" na checkmark — ay kailangang mag-update sa main thread. Ang mas mahigpit na concurrency checking ng Swift 6 ay tumangging hayaan akong maging sloppy sa hangganan na yun. Napunta ako sa isang cleanup commit na literal na pinamagatang "resolve Swift 6 actor-isolation warnings." Nakakainis nung sandaling yun, tama pagbalik-tanaw. Tama ang compiler at mali ako.
4. Hindi laging malinis ang pagpangalan ng server sa file. Bumabalik ang audio minsan bilang audio/mpeg, minsan bilang m4a, paminsan-minsan ay iba. Kaya may isang maliit at walang-gana na function na humuhulo sa file extension mula sa content type ng response, bumabagsak sa URL kung sakali, at nagde-default sa .mp3. Walang glamour. Kailangan.
Wala sa mga ito ang AI problema. Bawat isa ay problema ng "paano talaga gumagana ang mga computer." Yun ang buwis ng nakakabagot na daan. Totoo ito — pero babayaran ko ito sa bawat pagkakataon kaysa sa gastos ng pagpapanatili ng kung anong clever na bagay na ako mismo ang umimbento.
Ang sadyang HINDI ko ginawa
Sa tingin ko, ang pinaka-kapaki-pakinabang na parte ng buong feature na ito ay ang listahan ng mga bagay na hindi ko ginawa:
- Hindi ako sumulat ng download engine. (Background
URLSession.) - Hindi ako sumulat ng connectivity monitor. (
allowsCellularAccess.) - Hindi ako umimbento ng resume protocol. (Resume data ng OS.)
- Hindi ako gumawa ng kahit anong AI-specific. (File lang ito. Nado-download ito.)
Ang talagang pinaglaanan ko ng ingat ay maliit at walang-glamour: isang maliit na queue para hindi hihigit sa tatlo ang sabay na tumatakbong download at maghihintay ang iba ng turn nila; pag-persist sa mga title ng episode para magpakita yung storage screen ng totoong pangalan pagkatapos ng cold launch sa halip na "Episode, Episode, Episode"; pag-sort ng screen na yun mula sa pinakamalaking file, para kapag sinusubukan mong magbakante ng espasyo, ang pinakamabibigat na salarin ang nasa itaas. At hinila ko yung mga tunay na desisyon — ilang slot ang bakante, paano i-sort ang listahan — palabas sa simpleng functions na walang kahit anong nakadikit na URLSession, para ma-unit-test ko sila nang maayos. Nakakabagot. Testable. Nakakabagot dahil testable.
Ang aral, sa mga ikasandaang beses na
Sa puntong ito ay nakasulat na ako ng kung anong bersyon ng post na ito tungkol sa muling pagbuo ng website ko, tungkol sa isang $54 na API bill, tungkol sa muling pagbuo ng mismong app na ito. Pareho pa rin ang aral, at paulit-ulit ko itong kailangang matutunan ulit: ang clever na solusyon ay kadalasang ang mahal na solusyon. "Ano na ba ang binibigay sa akin ng platform?" — yan ang pinaka-mahalagang tanong na maitatanong ko bago sumulat ng kahit isang linya ng code.
Lalo na para sa isang solo builder: bawat clever na bagay na iniimbento mo ay isang bagay na ikaw lang mag-isa ang magma-maintain kapag nasira ito sa pinakamasamang posibleng oras. Bawat nakakabagot at standard na bagay na hiniram mo ay minimaintina ng isang kompanya na may libu-libong engineer sa likod. Sa karanasan ko, halos lagi namang sulit ang trade na yun — at, totoo lang, ang disiplina na gawin ito ay mas mahirap kaysa sa talinong pinapalitan nito. Pero baka mali ako kung saan eksaktong nakatakda ang linya. Sigurado may mga problema kung saan walang ibinibigay ang platform sa iyo at kailangan mong gawin mismo ang mahirap na parte — ang nadi-discover ko lang paulit-ulit ay mas bihira ito kaysa sa iginigiit ng instinct ko.
Ilan sa mga taong gumagamit ng DIALØGUE ay pwede nang mag-download ng episode, sumakay sa eroplano, at makinig nang walang signal. Pakiramdam, isa itong feature na may kahulugan. Ang code sa likod nito ay halos sobrang ordinaryo. Nakipagkasundo na ako doon. Sa tingin ko, yun mismo ang trabaho.
Ginagawa mo rin ba ito — nahuhuli mo ba ang sarili mong inaabot yung clever na bersyon habang yung nakakabagot ay nasa tabi mo lang pala? O ako lang ba ang mabagal, paulit-ulit na natututo ng parehong aral? Gustong-gusto kong malaman kung paano nilalabanan ng iba ang instinct na ito.
Maraming salamat, Chandler