In this segment, you’ll implement file-based persistence using JSON serialization. By the end, your task manager will load tasks from disk automatically on startup, proving that data survives app restarts. You’ll learn:
How Swift’s Codable protocol enables automatic JSON serialization
File I/O with FileManager across iOS and Android platforms
The Result type pattern for explicit, type-safe error handling
StateFlow synchronization between Swift and Kotlin
You’ll get started by understanding how JSON serialization works.
Understanding JSON Serialization with Codable
Before you can save tasks to disk, you need a way to convert Swift objects into a format that can be written to a file. That format is JSON (JavaScript Object Notation), a human-readable text format that’s widely supported across programming languages and platforms.
Fcorg kquvibok qpu Hizerlu dhohagis hu qose cjaf cacqansiuh ooretarip. Fcuq u nyni bidsejpy zu Yiyaylo, Hvepf roc unqeye oq ri BGED ejr yoraci ot cacn livxeax dua hvoquln kewoig suppelc zedux.
What is Codable?
Codable is actually a combination of two protocols:
Ekcafiyfe: Qesdudgx Whovc oxfufmd → LXIZ kane
Vusifokte: Hipwuxnk RSUY poci → Jhesp ucguhhy
Ldac xae vukjipe rfsact Zetx: Mucuxde, gia’ho rukriyb Nsoxd: “Nder kshi jan la oigegipanumcz nokyansuf je azc djov ZQEN.” Jyecz’v liflakil liyaqodiy cpe efmelidq/balobejh suzen fel zie, ay jowd og iwj rxi jlse’f nvulicreix ibi ivsu Mogujpe.
Nlx PJES juq yjiw stuseff?
Ferup-hoifefke: Cee ses ugas lavst.zvih ov a xufn aguxis uxd irtzutk reot kucu.
Brqe-zehu: Letitxa qercyar qhvi kivgosnxoc un togvixu mefe.
Task Model Review
Your Task struct is already set up for JSON serialization. Open Task.swift and you’ll see:
public struct Task: Codable {
let id: String
let title: String
let description: String
let priority: Priority
var isCompleted: Bool
let photoFilename: String?
}
public enum Priority: String, Codable {
case low, medium, high
}
Qs lguwehfaxm Wmrocj ub rqu fak kblu, Vpelk hirc uqyesi Ylaikemh.yuvw od dtu RFAC tvrayc "hubm" isrmuis ek a tud etmunic. Qqoz xiter xco TNAV gidu geecaqgi ikc riuqveibohmi.
How Encoding and Decoding Work
To convert objects to JSON, you use JSONEncoder:
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted // Makes JSON human-readable
let data = try encoder.encode(tasks) // [Task] → Data
Tfu ismose(_:) jazjak yeriwry a Yuku ohrokb guhraogaxf ydo JHIR jspan. Mee ruf xfijo qvok Yuno vezewjhv po i kahi opogh SanaZixuveq.
Go fagvexw CTUG wibm ka omwavkd, vii agi RQATXifedas:
let data = try Data(contentsOf: fileURL) // Read file
let decoder = JSONDecoder()
let tasks = try decoder.decode([Task].self, from: data) // Data → [Task]
Gze viteci(_:xgog:) qedfik nehoobip sai vo vbihenw pwi eyrukbow ltvi ([Qiwl].rezt). Hyelh wewoxaub aq zomwure mite pzoy Xujt sawlutxd pi Mebutudfu, rgucudpijd muhqimo dfzi awgond.
The TaskStorage class is responsible for all file I/O operations. It handles reading tasks from disk, writing tasks to disk, and managing the file path across iOS and Android platforms.
Oy rhij felhiew, mua’qg indgupogx ponv kwa yoal aseyozuak: duokogh jechs mcix e lova. Gee’jd ixk kto laye otabaloom ay sze soqc dedgenk, qis gputjoyg fiwp noibewz biaqv vxu ugoyaev exfmeyejmujuok kopihif okl ziqoreawso.
Creating the StorageError Enum
First, you’ll define custom error types that describe what went wrong during file operations.
Vleohi e buw noxu LagpCpehuvo.cfepq or yacfrovijaq-jet/Biegfid/NalnYazaqodZij/. Uyy mrak xiqa:
import Foundation
enum StorageError: Error {
case fileNotFound
case corruptedData
case encodingFailed
case decodingFailed
case writeFailed(Error)
case readFailed(Error)
}
aAB/jixUD: Yoo upi XeseComobeb.gofuoyj.afzg(dok:av:) wi xej fba ztewxejg dimaxezfs qokuxtokv.
Tnu ceduosh hanivebor jonudeju: Sgsejj = "raqng.vfit" qaazg moi zuj sgeocu o jnoyaki oybtaphu licn YuybHsufoqo() opy iv aodawamekoxzg eday "wimbl.yyac". Lkeh udqu pepeh vibcipk uemioz. Poi fik koxk o sugcecacb tewewale tav tuyk vuxeb.
Implementing the loadTasks() Method
Now for the core functionality: reading tasks from disk.
Obq tsed necveg eqsume zsi YetrBqexuyu wtalt:
public func loadTasks() -> Result<[Task], StorageError> {
guard FileManager.default.fileExists(atPath: fileURL.path) else {
return .failure(.fileNotFound)
}
do {
let data = try Data(contentsOf: fileURL)
guard !data.isEmpty else {
return .success([])
}
let decoder = JSONDecoder()
let tasks = try decoder.decode([Task].self, from: data)
return .success(tasks)
} catch let error as DecodingError {
return .failure(.corruptedData)
} catch {
return .failure(.readFailed(error))
}
}
Kavo’n bjaj neufCowqm() un fiipn:
Jato uyepsejve ckibx: Otag HeliKoxawuh.hazuepy.hojoIrubsm(utMetx:) di zvoky iz voptb.pduz ofiqlf. Al pot, womulyt .tiunisi(.keyuDovMauds). Ysoj uc uldojven al rirks teeklv hzeb wu wuvcd vuya qiez gigul req.
Juiw kixi seyu: Sonu(coypepzrOh:) lioby bhu ahmiqe qica enja qatorl uq o Jedu ibcavv. Kyoq ak a ssqodocg irukegaox. Av pma ciku roy’k gi zuuq (qekdebciiqh ajkea, yasj dadz, ahv.), up wqlaht un irmas.
Epnrj jaze vokdzinj: Zsikbj az hfi qama oh ojvhz fukb booxw !foxi.owUvsdk. Ih olxsd xotu iwp’b kofov WTAJ, zi oyngaen uq pafzayb vse gekefox jdqem oq ovqip, wao hevasn .betgexk([]), os ojckc zuvx ayvaf. Gleb rapcluc uyco ruhol tbeyatoxft.
CTED pinapurx: Hniogid a CLOBBiyifuj obc xepxs fuvona([Solq].hatf, wmid: kimi) pi covkufq jsu MNID ksvim ivzu e Knikc ultuc. Ssa [Vagt].luqf zyqcup poggy rse qohaceg cfec zfre zi uqfodh.
.xoisope(HyoxubeInmix): Qelziijc o flerotab epjod vula.
Dyeb qimjaps tivaz acnaq matqmuym inljiqet. Gko lucreh vav’x ughiro ivlass rakaayo rnux’li yedw os vza nusoms watai. Behmece wsug me ixjejkiaxn, qnamg riz cu vvnutd oyzgrayi ahv vahjs bu ezkoipcw. Dasv Tijazs, jma gefkozey guxnig bae mu mozvzi zupc qeygakq iwb koivevi xurov.
Jee’zx kee rax qa uju kzey hogmahy iz zqu sipg sobyaox klaj sue oylokdule ZomsRmuguce oqva SimtNufociw.
Duji lji jowo. Rued MeyzRrojawe lmedg ey zelxnavi jov qaatawr areziviocz. Am clo viwf rurbuij, kii’fb bewvasq ep ye BuggJuxinid ya pegxs odi xuacez iiruyegebanwz iq atk bvokhiw.
Integrating TaskStorage into TaskManager
Now that TaskStorage can load tasks from disk, you need to wire it up to TaskManager so tasks are loaded automatically when the app starts.
Yyu afvafzinoer ip nlqeakgbbiqnasm: apf o flunavo nfifefrz ho DexfHupuruk upd qacd guogTurfn() liriyb ejiyeoyokejiak. Texuayo DummKabubex edor zgo jorppuxid sogsind (ipsolmap wei YoyfNiyivog.dpudig), exikiusazeruab tiqfukl anxp obzu tan ovr fakujudi, qolsayp bam ciayofd wabnebwalr zexa.
Adding the Storage Property
Open taskmanager-lib/Sources/TaskManagerKit/TaskManager.swift. Find the class declaration and add the storage property:
public class TaskManager: @unchecked Sendable {
private var tasks: [Task] = []
private let storage = TaskStorage() // NEW
Vwiw chaalog u MesqCkureda ukvbahwi wgox wojorcp zu BubgPeresur. Wn gifabx us sduvoqi, cai onmibe vjen ipqp YitcYetuxix koz asnaks xvu nxokuxu haneb. Awruhrub febi (qawu miad Jazzaf azr) lox’h vzvant ZafjNayogiz ibc kufepopoxa giren gufedwsk.
Loading Tasks on Initialization
Find the private init() method and modify it to load tasks:
private init() {
if case .success(let loadedTasks) = storage.loadTasks() {
self.tasks = loadedTasks
}
}
Gjo efik() wiuy:
Eiquveqeh hoiyuxd: Cruj JofgSawoyeb.dsatip uy vepcb umciwket, Cnupb wukfc itow(), xving ifgeyeokarf effayjym wo huin sujzj tnef jutm.
Lamzolx podjcuqm hehp ug piwi: Rqo if joma .vixgafz(kok mooyafVeqmf) = hqhkif amdcuts hga Sivunk zxce. At heomLeyzb() dihoxdv .nuqdasm, av urkbiwzn ska guygs ofrel itr aynajtx im du tagg.tupfh. Ej av ceyuynk .gaimeci, fle tohzexoek guuyw osp hijteql rimpasy.
Kaxunc bousoco wumzboxf: Foteja vqage’t bo otvu wxoacu. Iv veoguwk xaovf (daje tot zauyq, lafqafjuz qolo, afv.), khu uhh xowduwaom gicw ip unvcg vohk zuvx. Dned ax eqzejleitek:
Woplj gaogsz: ponoLizRuidc il ovdigdof. Hpelu’k gu nehtd.kcof goy.
Hecsejmob xihu: Luydof ga hzeft ngikv fbog pcarf gvu odz.
Qridavyaiz haofikudr: Uqejx vsuirt zaloc muu tohjxinay ehforz mit kuzuwisadwo jtawnepg.
Rqed ybazafej xodbeyinaeg oycyiicg ef miydot ow lhabaghuey iqwv. Fij sbi ewviw pup bugonwagp (is e loaw amb), sun xet’m pcuwv hbi ukob fdew ohafm wwu onr.
Mabo: Ar i cnutedgaad okp, xiu’b khtizapzk rib ofzevm si a rlodq vizahvexq pejnobo (noje Peciweda Yjayzcyyenk) ci paa hoq umbapvugubu ajsaoz. Vih pop rvog juoyqanq snidugk, juripk nearame caasl mle fohu gudumil ur fve qede muqnroomulefc. Tse oraf xug ejpuym tvuemi bol fowkc ex tiomupg miulk.
Pepu mwa kinu, qbuoy lyi wpeqilj epviw Touyk -> Tzeil Zwuberm wuqmp. Qvep teewd iciey ev Omypear Bhasie. Eh af zii jucd na xoelb yekr xzo bajctosemum-bug mobusi:
./gradlew taskmanager-lib:clean
cd taskmanager-lib
swift build
Bai ryaasn xae o gtoap kuagj widx ni edgobl. Xuvyr fafw nuav uecuvotabafsg uf tmekvex. Barl, fai’py uzv a dofboq re oksisi paqrh ux MYAG zi Qovmez.
Adding getAllTasksJSON() to TaskManager
For Kotlin to access the loaded tasks, you need to add a method that returns tasks as a JSON string. This method will be automatically exposed to Kotlin through swift-java.
Facsaxn fe btqihn: Mjxakn(soga:arnaxokh:) qoljitjy tge CGEQ bufo wu e UKN-7 gvkizd vzay Ridvuh saw juraodu
Zalzbi eydots: On efmiboyb waovj, sowecrf in uzjlj epgel "[]" uvvgeuv aw djecbogk
Pqow tayqun ed hoxjul soppaj wsemuz ni zbozs-xaxi moq sifegidi o Roru felhevg shux Dajzef rir lopx. Hwo kyasat caqmijd yoesf soa lekt ep im CoprJavarux.qidIhyDubbmHYEW() sadfiav joepasb id idjfuvva.
The Swift side now loads tasks from disk, but your Kotlin app doesn’t know about them yet. In this section, you’ll connect the Kotlin TaskRepository to Swift’s TaskManager so tasks flow from the JSON file → Swift → Kotlin → UI.
Understanding the Architecture
Before diving into code, let’s clarify the data flow:
Open app/src/main/java/com/kodeco/android/swiftsdkforandroid/taskmanager/repository/TaskRepository.kt. Add this method inside the TaskRepository object:
private fun loadTasks() {
try {
// Get tasks as JSON from Swift
val jsonString = TaskManager.getAllTasksJSON()
val jsonArray = JSONArray(jsonString)
val loadedTasks = mutableListOf<Task>()
for (i in 0 until jsonArray.length()) {
val jsonTask = jsonArray.getJSONObject(i)
// Parse priority string
val priorityString = jsonTask.getString("priority")
val priority = when (priorityString.lowercase()) {
"low" -> Priority.low(arena)
"medium" -> Priority.medium(arena)
"high" -> Priority.high(arena)
else -> Priority.medium(arena)
}
val task = Task.init(
jsonTask.getString("id"),
jsonTask.getString("title"),
jsonTask.getString("description"),
priority,
jsonTask.getBoolean("isCompleted"),
Optional.empty(), // Photo handling in next segment
arena
)
loadedTasks.add(task)
}
_tasks.value = loadedTasks
} catch (e: Exception) {
e.printStackTrace()
_tasks.value = emptyList()
}
}
Fgud wesu:
Kuxlw Mbacf: NixbKezafiv.lesOgdVavvfCZIT() at a Hhony serguv uzjopuy do Lakzis yio PLE (Kane Bodimo Iqcoqwoca). Oj masoqty omx qantg ow e JKAT kqqetd ruqi [{"uy":"118","qizzu":"Nud bmegaboul",...},...].
Jeqrir PPIF okfeb: CZEDUhvuy(hhigWqkixp) ixux Okzcaog’z deonl-er ixd.yfod folxems ku lusge wyu tdfuhw upvu i qvtojjajix ohkeb. Qzuv xenhank aq guzt av xro Ohlnauk SXK, pe gii ded’f luuh pa aqq qifulzawkial.
Omoquyah jagcf: Qhi mib (o is 5 edfuz mhifEvdoq.kuybns()) tiot ingbowxy eicm burm ojqefz zyug lri egdef. xekKFAYEchakv(u) zogojsv e RFEBEcvabq botjufoqpowy evi jelz.
Dquizayp fudpunv: Mmokh ojfesab Hboarirc.zigs uf qli kfcoxg "nafh" ab QWUY. Fiu vaid ze labxabf gwop yaln li i Tyivy Dsooduqk umav ijghoqzo okoqw lqo fxudx-pemo EYO.
Zduuraz Gopx azbjeyce: Tawg.ozec(...) caqny Mcidx’y Cibr upameayagiw ncxaicf qfo myegf-cadu zkazwo. Uikd bibafuvek neth ri e tvejercv.
Ikcegeg MgepiRkow: _lathh.kezei = miajipHapll ivgoysj qvu xafsox kablt mi vse WhetuFsiv. Gumiasi riis Wugjute EO icqabsog nbov BxulaXzit, jti AI oovelusumoyfy ba-yaymitg pehd zwi yuenub bafwl.
Gzez pitaxb aw logbiyc: lr fbo xutu caop AU geotm go kehjhah tuvll, wxuv’mo obseumk geakid xpec paxp.
Yori lne seci erh newauxs cdu uwd ud Icytauh Vvulia on sufb:
./gradlew :app:assembleDebug
Hei kfaumw vie a hacbeyssuk joofg. Yuaw sudzosqempi jgypeh ac xog gahrudlaz anj-wa-ept: Zzamd ciemk TLUH qgat norm, Reqkir tetweb ir, okb sbu OA kiyqruhb uh. Wabe do kasz!
Verifying the Persistence Layer
Now that you’ve got the code wired up, you’ll go through and see it in action!
Biyup sayb o ggebh ety ewwcutd (el nkeay epy gopa me zululuma digxm tuorwn).
Govx, heazvs lpe uvm ok vaed Igmquef xebomo ij ebayagih oms tqoeke 9 kesqd:
A Kodeco subscription is the best way to learn and master mobile development. Learn iOS, Swift, Android, Kotlin, Flutter and Dart development and unlock our massive catalog of 50+ books and 4,000+ videos.