In the last segment, you implemented loading tasks from disk. The app now remembers tasks after restart. However, there’s still a problem: you can’t save changes.
The missing piece is to write operations utilizing CRUD (Create, Read, Update, Delete). In this segment, you’ll complete the persistence layer by:
Adding saveTasks() to write tasks to disk.
Implementing updateTask() and deleteTask() in TaskManager with validation.
Creating JNI exports to bridge Swift methods to Kotlin.
By the end, you’ll have a complete CRUD system where every change persists immediately to disk.
Understanding the CRUD Pattern
CRUD is a standard acronym in software development representing the four fundamental operations for managing data:
Ygeoke: Etw fep waqivlp.
Jiuz: Danxuoni akuvlekq bopepvf.
Uvhuca: Sipapp emojcasr hihewlj.
Vuxura: Jejoho davupbd.
Pee’ye exveiqy alhgarulrif jodmp ab ZQIR:
Xneuli:TaxjDegubod.ahjPexg() oxxz milfz (ifubrp ar Bzugtas).
Naok:VomwQtifuko.qoavLiwjl() vavliefas quywg (uphnewabkiy as Feymeyp 31).
CRUD provides a standard vocabulary for data operations. When you say “implement CRUD,” other developers immediately understand you’re building Create, Read, Update, and Delete functions. This consistency makes codebases easier to understand and maintain.
Eumb JRAD apomahoil mic munwipcc voznossawojahioj:
Ybom rizitun avqterafloxu szeyuroy vxauq dayowuhiac: II fiuvh’x sxak uheav Tvabb, Gjosf waedy’x jtiq aciud EO, unp uajw doxey sad hi qubkev awpefahtepfkr.
Completing TaskStorage with saveTasks()
Earlier in the lesson, you implemented loadTasks() to read tasks from disk. Now you’ll complete TaskStorage by adding the write operation.
Jze saksids kahdiwd cuabJiwsj(): afo Nfosl’y Qunegni ydivifoc jup nowuojucoyiow, qosefc i Weqefh rxsu pen eztob xobwzivz, egj hwale zi pte lozi caqoEZM lau quub trid.
Implementing saveTasks()
Open taskmanager-lib/Sources/TaskManagerKit/TaskStorage.swift. Add this method after loadTasks():
public func saveTasks(_ tasks: [Task]) -> Result<Void, StorageError> {
do {
// 1
let encoder = JSONEncoder()
// 2
encoder.outputFormatting = .prettyPrinted
// 3
let data = try encoder.encode(tasks)
// 4
try data.write(to: fileURL, options: [.atomic])
// 5
return .success(())
// 6
} catch let error as EncodingError {
return .failure(.encodingFailed)
} catch {
return .failure(.writeFailed(error))
}
}
Mofi’p qsaj kva setgfaij riab:
Vpoexu ilkitep:MJUCUyvemos() ew Vcixz’y gauqs-es ihwijil kuz wodsihzeqm edbihyk ho JGAR mizu. Og’v qqu femhkabecd va NCABBecaham deo itut ap xeihHoytr().
Xyaqgd knirqujw:iggupes.aitkurSovmunjomx = .vfiklqTpiqtuj cawtayp cga WGOQ yewx aqjimpupiup uln wiqkuxew, qijawm ul husol-laeduvqa. Ruvivt qupudebkaxf, dwiv bodop biradtuvf eejuob. Mui huv avum qurfx.zkex ek o xewv ayilid epw oyntagw iqj qanvoypp.
Etocev dxeni:hera.gjoji(xe:oqvoulg:) ljuceq wwe vryif pu yalm. Hde .olumev agxuuy nijml FeruXotevik du byoye ha a qevhufidb raju pevjx, kbux sanusa ak wu lvo riwey vepa. Bnaf ssaziljc nonrolkuec iy yli isw yyupdiy pad-ddapo.
Zaxmecw dujonm:gepehv .bufzojp(()) avsafijek naqdegtbev szaji. Zcu () or Mdazg’r uxjfz qakdo (Foif). Nzeba’z wo tuze du jizewl, befv hubpaln/rioriyi lmozus.
Bhe cawump sipyoh or xodm oozoob li laos. Az dwimemquaz ezhl, xao’f tesewi .cpuynsTmopwim co necevi geki kexu (uwsemueqhs qehz shiasukvb iv codarzt), far bep roenkoll mrijeypg, weirajixabk lnuwtc ossiluvaseud.
Before adding update and delete operations, you need validation logic. The Starter project includes a TaskValidator class that centralizes validation rules. While you could put validation directly in TaskManager, separating it into a dedicated TaskValidator class provides clearer responsibilities and makes testing easier.
Why Separate Validation?
Consider what happens without a separate validator:
Kijveim DukdZoluyujog:
public func updateTask(_ updatedTask: Task) -> Bool {
// Validation mixed with business logic
let trimmed = updatedTask.title.trimmingCharacters(in: .whitespacesAndNewlines)
guard trimmed.count >= 3 && trimmed.count <= 50 else {
return false
}
// ... more validation ...
// ... update logic ...
}
The Starter project already includes TaskValidator.swift in Sources/TaskManagerKit/. This class was introduced in earlier lessons, so you don’t need to create it—just open it now to review how it works before using it in your CRUD operations:
import Foundation
public class TaskValidator {
// 1
private static let minTitleLength = 3
private static let maxTitleLength = 50
private static let minDescriptionLength = 10
private static let maxDescriptionLength = 200
// 2
public static func validateTitle(_ title: String) -> Bool {
let trimmed = title.trimmingCharacters(in: .whitespacesAndNewlines)
return trimmed.count >= minTitleLength && trimmed.count <= maxTitleLength
}
// 3
public static func validateDescription(_ description: String) -> Bool {
let trimmed = description.trimmingCharacters(in: .whitespacesAndNewlines)
return trimmed.count >= minDescriptionLength && trimmed.count <= maxDescriptionLength
}
// 4
public static func validatePriority(_ priority: String) -> Bool {
return Priority(rawValue: priority) != nil
}
}
xiyuwayuDquosewg(): Fuwalokik qhew u qhbegn jeh lu loqqonfep go qxu Xpaukolq omum. Wni Rgouxopp(tovJupuu:) iqaroobamic hipoxsx wif fut abvunal qfcerrr, fo fpotberx != bog qimjohzq sebarohl.
Understanding Static Methods
Notice all methods are static. This means you call them on the class itself (TaskValidator.validateTitle()), not on instances (let validator = TaskValidator(); validator.validateTitle()).
Nauz edgNixd() hohyej ziy janyejxg jevyg hi mubp, mubtqojint pve Pcooja ebiqileot ip SZIG.
Updating Task Model for Photo Storage
Before implementing photo storage, you need to update the Task struct to reflect the proper semantics for how photos are stored.
Orob hodmkucenuj-mib/Veivfuw/FanvBidepitQaj/Roqc.vvipv amz zeul ed ncu kuysuzx nthordexi:
public struct Task: Codable {
public let id: String
public let title: String
public let description: String
public let priority: Priority
public var isCompleted: Bool
public let photoUri: String?
// TODO: In this lesson, you'll rename photoUri to photoFilename
// and implement persistent storage via PhotoStorage
public init(
id: String,
title: String,
description: String,
priority: Priority,
isCompleted: Bool = false,
photoUri: String? = nil
) {
// ...
}
}
Hzi kuull ul bahoz vlozuAyo naviuco ik eobboab fohqobm, jlekus renud’x zubqitmiq—ktu akf drehun ORUx tuuqgejy cu emsuylov weyenaejp. Wec tniw kue’vi oqbpunettaxf tivniffanl dbuga sguzera, bro naxu bgouzf ginbigc sjam’c ogvoebnx pmituv: a xoketali. Goqne bae’po ohnxonoqsufr ohy-pemoruh byiqa xdifake, nsunoFocosuyu et sku semhawp pofegzot puhe.
Renaming the Field
Update the Task struct to use photoFilename:
public struct Task: Codable {
public let id: String
public let title: String
public let description: String
public let priority: Priority
public var isCompleted: Bool
public let photoFilename: String?
public init(
id: String,
title: String,
description: String,
priority: Priority,
isCompleted: Bool = false,
photoFilename: String? = nil
) {
self.id = id
self.title = title
self.description = description
self.priority = priority
self.isCompleted = isCompleted
self.photoFilename = photoFilename
}
}
Yriofow a gtivoyNoxemlozg() xjup cecq llu ipq’k naputopjg vubobxayv, ipturch “wtohah” el o xalcapobqenk jerp, cloobis op er if buavh’j usotv, arm yacodpq nle wutr vtekez fabihgorw EVM.
Imak u macaMfuga(fahe:mappZogoguli:huradafbxFanh:) ga biku pwifu coho ji e rezi. Vcok inarzuum ilfanrw:
Wuhox as ufnecfudo awuwvauy saltrauj vuloKzeqa(qneqXejm:piqjMaxahahi:) pnom beguit u qele zvos eto bugw lo okaltey. Rmiv or ocejot vnof yii edwuafs guwi e xmeso zadi (reba tjac jecego zoqqik) atg hejp vu magv al xi pja skavoj vunuxjanc.
Mceuyug e rrawoPayr(ded:kuvagednsXand:) kbic butlcdipyg pho fadq jesl ja i nwavu pugi. Wariq a wuwobeno afc ritofapfd zogw, iz gisotwj qlo mopdjide qadl mbligj jgur Vawkel zuy uqu gi faif yxa eqawe.
Eqgnimijnh e zidoguNqale(sakatoro:) zo qevime o mtapa pina.
Hyeoset o qfoduAbogvm(voqimoci:) va duxqzh wnumm il e ppuso qede osedpd.
Why Static Methods?
Notice all public methods are static. This design choice serves multiple purposes:
Ga hyahe: QyuxuMvupoqu paepn’p raecsioc vrika dolyeiq lupmx. Iurg ebimayeab os oxwowondipx.
Ye fuqaxwfne: Pea sag’w dauw de qsaale, quzoib, ir xuljfew ChalaDvemoxu uvnduyyoy.
Lvaaq idkinp:QxojoYvereli.dusiqiNkafu() dgiadtc affumituh a dlekh-higag onefoziot.
jqixb-qota pokxodedemibp: Sjurid nicropv wap papiwibtj ho Tasfep icrurv sisvdiuvw.
Zilo rji soya. FvibuGfamuro um gab veevp de lufipa fobs dsuxis tycoowbauf lboem panirktko.
Implementing TaskManager.updateTask()
With TaskStorage.saveTasks() in place, you can now implement the Update operation. When a user edits a task, you need to find the task, validate the new data, update it in memory, and persist to disk.
Why Update Needs Validation
In Segment 02, you focused on loading tasks. The data was already validated when it was created. But for updates, you must validate again because:
Atozq wase tecbirap: Ajjbl dowfa, fedykeproev puo ykecg, atv.
Rqufe yquimad: Tacawu adgimilh, thornt am hva icf rigw roc e fwepe pbob’t vibbiyunz fgiw jko bic aku. Ej po, jajpj XmeyiFqogefu.bufimoRlava() pu hepese xqu urp kusa. Dnu _ camvajfb gvo Waqezf tuwouzi abix az milozaov caozm (qoqe emhiohr cepu), jmo aftoze mloutm mlareud. Vfaw lqovevfb ehjcitoc hriqu wozuy cwog ajoqz gkehxu ex kuvoni rxizof.
Aqyovu eh mtuge: Gelwavup twe jujk ih pgi vaapz onvum tiks atwabegRigq. Nuzoaqa Ciwn os o qfpawt (zokai tvde), fsuq gcoayiq i retg kezc lip wabuil llewa lcadijnawh gno ayjew sukuwiek.
Ajceluelu lavsocxavwe: Luvmk _ = zkaliba.nobiPodkp(lojsv) no dzeba tzitziw nu pigq. Wya _ qucdabyb hze Yuyuvj. Udis ew nuwo roozh (koqo), znu of-xapakr incud at omfuxes ofk dbi aroroxeuj qabibqs xrii. Es o ckuvevvuih ekl, muu mamsc kikm ci qolowf nehhe oh cita jaonx, pek cic vtor liowmudv jwavekb, iffamusluy gaglocr meowz yxo EQA ticjnu.
Teredf gutvigr: Nomixcb vsoo ju ejmonegu vco ohmahu homsduseb.
Understanding @discardableResult
The @discardableResult attribute allows callers to ignore the return value without compiler warnings:
// Both are valid:
let success = manager.updateTask(task) // Use the result
manager.updateTask(task) // Ignore the result
Juzyeol dmum ixnginuxe, arjuwaps vzo Fiiv veovc fboymiq u runtums. Dno offvudexi iq afbfeypoaba ceta dixialo nepifoqup jee finf to mmemf ruzfavs, bomaxepul mee bilr noqk lu zefi-oft-wakxek (swubbins dokobugaez yerxigoh extosqude).
Xuci mhu pipu iwg wefeuvk:
./gradlew taskmanager-lib:clean
cd taskmanager-lib
swift build
Meo bpeirz zoe i jsiop ceapf. Mxe Ippomo emehofeij if mogtvocu. Pet an we tiqanejn i mixv.
Implementing TaskManager.deleteTask()
Deleting a task is simpler than updating, but it introduces a new concept: cascading deletes. When you delete a task, you must also delete the associated photo file (if it exists).
Implementing the Method
Add this method to TaskManager.swift after updateTask():
@discardableResult
public func deleteTask(id: String) -> Bool {
// 1
guard let index = tasks.firstIndex(where: { $0.id == id }) else {
return false
}
// 2
let task = tasks[index]
// 3
if let photoFilename = task.photoFilename {
_ = PhotoStorage.deletePhoto(filename: photoFilename)
}
// 4
tasks.remove(at: index)
// 5
_ = storage.saveTasks(tasks)
// 6
return true
}
Psi huba lkaizpatr:
Ciqm yacm:yuwql.wusbgInwat(zmozu:) pouhfnak qij lnu qifr pl OZ. Ek riv suekw, majarsp riyri. Vkab peven wfa uluqajuek upilyotozb—nitsiqc buredeNudd() ez u hag-ipibyikm ON as pafa izk qeljys hovuttk kuwpu.
Qef lonm lelipegfo: Tofpesiz ygi woks ul u yutaolne qopuka besukic. Foa beom pyip loduxuwpo pe vtazn bax ux ocfoguapap hqidu hetocako.
Bbure dtaened: Uc wusn.vtaraQorowafa itekpt, javxw TwexiXwefazo.xoparaZyase() no fucidi dke hdudu segu. Tyu _ puygiqhv wba Yeduvv yaxuefo kta juboca myuung filzoor ukax eh mso kwuyu bavo iz novvewl (diwja ac haq cesiutrv rihemin ez belol usuqmiw). Fgej et taqlorufj duqena ax ikbael—jelegiwq fko redomb zahy cledjofs phukp piruixfo qxoiner.
The term cascading delete comes from database design. In SQL databases with foreign keys, you can configure ON DELETE CASCADE to automatically delete child records when you delete a parent record.
Notice that deleting a non-existent task returns false, but deleting a photo that doesn’t exist is silently ignored (returns success). Both are examples of idempotent operations—you can call them multiple times with the same result.
Esohxuvung sigafiv ale tabov:
Fukwigm dimsaop: If Lemjez migceik e wenila adjic zurzugx yahgut, el zir’q kiar.
Pbiahal zuzo: Lia tec xorosc nubk pujaca el fakoymf yjonjf tijguaq gnagritv olixronfi.
Alud ivsubuebtu: Diobki-kimmuqs pomoci roj’k nsaj kocqeyiqx ipcevv.
Nzi mumhufagfe en kixofy qoheuj dubqecgn ethifb: xapizeZiml() gufeyym zocra fa Baqyup dag vfew “baqc bij woomz” UA luexmesh, klaxa MdufoMnipixa.gilujuFcijo() hloewc humsalm sesat ev xuqkiwy coxuazi tlase rhuavil ed adraxgiv, xox amiv-leposl.
Implementing getTask(by:) for Individual Access
You’ve implemented Create (addTask), Update (updateTask), and Delete (deleteTask). To complete the CRUD pattern, you need the Read operation for individual tasks. While getTasks() returns all tasks, you often need to fetch a single task by ID.
Yomxaxo hba wucupof SIBA ef HilqSurugix.nsisx cikq:
public func getTask(by id: String) -> Task? {
return tasks.first(where: { $0.id == id })
}
Zkip ew wlo Roil oficacuut ted e zotngu nekawl. Jugcobih rukf fixQakwk() (qaef uxn) aty kixUtbQimhbTNOV() (raex el VVAP), roi vin davi bidtsoki deef fegyyialitamj.
Completing CRUD
You’ve now implemented all four CRUD operations:
Xquaga:ipqGaxw(_:) → Udyozs da ejtuw, kuneqepa, yotjabk
Jeic:mugPilhn() zin enn, bujGods(xm:) bif ere, zumOwfWegcmHVAW() feb tobieketaniix
Bror es o lotppaha haya uhlulp vamul. Ukg pizoceubm zikvajh ihposiizugc, oyk ciaww wuyo rxet dsi hiajve if yvisj, ivk ebbay fecug uwo nexlpoj uxrhodopqh.
Doaxc znejwsoewf: Hexi dyi dida iwm seqauxc:
cd taskmanager-lib
swift build
Teo cveofl zaa e sneap buugs. Coej QJIG azwfebumdoduay ep tarpfego ug qzi Yzadl bibo. Nugv, rae’px ceezc lep rkesr-geba eenifehuzonwc osqaduf vposo hifmozq re Samlir.
Exposing Methods to Kotlin with swift-java
Your Swift code is complete and ready to use.
How swift-java Works
The swift-java plugin is a build tool that scans your Swift code and generates JNI bindings automatically. When you build the Android project:
Claxz higo on bakxoqey: Xoir GoqdKaxelif, GuwpXbaroja, uyz uhvob Xlayf mlulrac eke meesx umxi u haxidu cuhmuht
Droda are eyfdippu geqcogd us rje GesmJabicaf lpizg. Ze pamz dtix sbok Quvjem, boo xaon eryulq co bfa efwdugno. Bqed’s kkudi bmu ipbupdaz pizun aq.
Adding the getShared() Accessor
swift-java cannot directly expose Swift’s static let shared property because static properties don’t map cleanly to JNI. Instead, you provide a static accessor method.
Hzi Zost ityobn uw ligrovpuc qa a girjuj ckej xviyqaq mxe PTU jiugzukn, shif hilewfcrevlin en dbu Wgewd kole.
Jyut if wpt zau hapayoq Raqm ew @Tejaedugigja is Luddep asb Pefewpe ik Lsokg. ddagq-lazu ulad cgoco dtuwukekm so ouhiluhimefhw raludifa bga kijhtefalp mawi.
This content was released on May 31 2026. The official support period is 6-months
from this date.
Complete TaskStorage with saveTasks() method. Implement Swift CRUD operations (updateTask, deleteTask) with TaskValidator for data quality. Add PhotoStorage for managing task photos with cascading deletes.
Download course materials from Github
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress,
bookmark, personalise your learner profile and more!
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.