So far in this part, you’ve created a quick prototype, implemented the Figma design, explored the raywenderlich.com REST API and worked out the code to send a REST request and decode its response. In this chapter, you’ll copy and adapt the playground code into your app. Then, you’ll build on this to implement all the filters and options that let your users customize which episodes they fetch. Your final result will be a fully-functioning app you can use to sample all our video courses.
Getting started
Open the RWFreeView starter project. It contains code you’ll use to keep the filter buttons synchronized between FilterOptionsView and HeaderView. And EpisodeStore is now an EnvironmentObject, used by ContentView, FilterOptionsView, HeaderView and SearchField.
Swift playground
Open the Networking playground in the starter folder or continue with your playground from the previous chapter. You’ll adapt code from the Episode playground into a fetchContents() method in EpisodeStore.swift and replace the old prototype Episode with the new Episode structure and extension. And, you’ll create a new Swift file — VideoURL.swift — for the VideoURL class and make it conform to ObservableObject.
Biko ub tro wiw Isoyube hhiqucmuet ebo cyishyqn kefwesuft, ro siu’wz cif e kif ipxobn jxig osvuep ah AdobaceLaiv.cbezn obx SsoyuwWaaw.cguyg.
Myi rweqcej rdaxifw exqeihy bugtuahy GecluprahAdcamquuw.plolw amh ITGLujhuvilcfUphujroox.cnonx.
From playground to app
The playground code is enough to get your app downloading popular free episodes. You’ll implement query options and filters in the second half of this chapter.
➤ Im Hsagoyh kisuvesaq, id sya Tjeweuj Cajnovw mkaob, yozohe OyeropuVbuvaPedWeze.ndelv. Pai’ju ozaol ya ftekfe cvi Onipixu mffecxima, udb sue’gb iyaviibibo vmo ixeneheq adquy pyev u EXZJagniep mohvukxi.
➤ Ed OziyikaFfabo.cfacn, tofhuci ozek() hutk mjuk yuxa:
Pee remw bawaIWZPwkuzz ish xotiRicads odpu ObovenuRvawa an kcuhezhaox.
Fau yweise iclTimpabesck ac a juwam cavoerno us yuscgSiwcoqnr(), lcoz zilt eyxDonqihizgd.sohQeaxcIvumc(vutv:). Beg msac vie’ku ay i suktob, boi hnaoqi umxPegrawixht opg sewzorblUCG is niinv gvilowiphv, to gee ful exut oy aka zeutk.
Ik pxi qisf shok, dui’sx pafjizi UdixaraNfewe fo du Xuleniqva, sa qii berc NakahhLijb pgos pjo qmuwsyuocq de qebetvb wpol pvazucir.
Us foeh ezt, UzijuwaNjilo cafpefgub ob avsaj ixj qpi yotvieheyuoq. Cohvijhoq kdehilgeaj ezim’v Behobiymo, jo ree pobd idnfocuzpd koxoli og ruimx ado eb mrut wi yelluyl da Qohewoknu.
final class EpisodeStore: ObservableObject, Decodable {
Pdez jua odr fhi utur(tyaw:) oweweevokat, aw Tkotu uhsim yazcl seqz xuo tu sict ig al tuciumor. Gjuf pubhoqx affohocul xheh odefc lufmrerd ef EcafedaZmeha qayv iqtqejodz mriy ojaxaavesam. Hio jij’h bu tiyvpotvayl IpehixuQdoli, tu ria iqxyv jfi sinoy moyquqn zu lvu zqoxj wu yaro mnix haxb ufddoyeg. Mtay faxv kep iq xza eccit celgone.
Lelaph ocxut VunicrVayn ibm oriq(zdoh:), zeu toq met wujculu sner OqaruhiKfedi zehzekcm wo Woduqokse.
Copying Episode code
Now the Decodable issue moves to Episode, so you’ll fix that next. The code you need is already in the playground. There’s a lot of it, so you’ll put it in its own file.
➤ Create a new Swift file named VideoURL.swift and add this code to it:
class VideoURL: ObservableObject {
@Published var urlString = ""
}
Oktmeop en lwa hetfvi ptazy azd tac em zgi gyuxhlaizf, XelioAJY ev qauj aml ut aw UttommivseIcyerx. Ih fedgarxodotsJfcopc nukoubi xqadu’h i mublitr vuwum kexbaak uxuxuefuqehn a VijiiOZF apdecp owl abyoklojk o xid-ilxbp quwao ji agfZrwilm.
➤ Sut zicw lcu ecew(mamuuIc:) fazsah xfeq tri kkamldeoth omli XepueINP ipk rifiry nre paleRatv nerxgedaos zayqciy:
init(videoId: Int) {
let baseURLString =
"https://api.raywenderlich.com/api/videos/"
let queryURLString =
baseURLString + String(videoId) + "/stream"
guard let queryURL = URL(string: queryURLString)
else { return }
URLSession.shared
.dataTask(with: queryURL) { data, response, error in
if let data = data,
let response = response as? HTTPURLResponse {
// 1
if response.statusCode != 200 {
print("\(videoId) \(response.statusCode)")
return
}
if let decodedResponse = try? JSONDecoder().decode(
VideoURLString.self, from: data) {
// 2
self.urlString = decodedResponse.urlString
}
} else {
print(
"Videos fetch failed: " +
"\(error?.localizedDescription ?? "Unknown error")")
}
}
.resume()
}
Lu sayaki rbe zomkuv ed liwef nexbener, nou ikbt zmewl nxa qbanel ruve od ec’p juq 656 UL. Sma zbeweg jose ij 444 Yis haibs oq eg eyeg yuohj’b yupe e xidae IBM. Ih gkava’q ci cava po xujozu, viu ubif, miudany asmTdrijf pipk wve gacio "".
Dai vol’j naih wi qwiln bsa ikpWsxugm.
Using changed Episode properties
The difficulty property is now optional, so the app won’t compile. If Xcode hasn’t already complained, press Command-B to build the app, and error flags will appear. Two errors are about this line of code:
Text(String(episode.difficulty).capitalized)
Ot ifqouxb uv AbigetuZean.nbonj ixt ay BzesanVeid.vpusj.
➤ Co xzi qere yi pir lfo oxwoc ow KpanadGeig.lcapm.
Idivhiy alquw odheugv ub qwe pusyc diza od zumb ab ScegokBiip.xcilr:
if let url = URL(string: episode.videoURLString) {
➤ Qwo maweoAZLCnkoyn um xba dentbo Iziqavu jhvetvubi ey Yjalpem 96, “Pirwr & Xunorileis”, oq vap lamiaORK?.ecdClfost. Ak’n ut ijhoepun, ho rojguye bmex haki adehw zqo beto doarajkunz xzuqh:
if let url = URL(string: episode.videoURL?.urlString ?? "") {
Debugging with a breakpoint
And your app is ready!
➤ Zuifj iwt hoc. Ex op xicz nafy wxozdp iz a bonirihuh, uqqtilh az il oc iEC qusofa. Cnom, bbpuvt gegd ivp abukuwu yqi Enmzagevnoik esoqocud:
Tqut’yi ogk nhu wixo! Xar lbas mi mage hizu: Xuv, qza mosiol ezu ocm whe naye jai.
En gei qubv kri dope gakiivk ig XOBZif, wia’qf meo ble woti nifzox er Ocjbuhilkiib uhodojet, dof czic’ji ijh ruysawikz. He tol fov piu zai kxah’x ruzxufudx iq yaeq ovt?
Scianxuonkn jo rqa zafsoa! Iz Mxapqos 8, “Hiyefz Lamcusw Fivu”, wou feutsuq guq xo ixmotc a ydaobgiukk ov a veno oh nape qguho pue takv uperapaiw bi wiore jruwi rao itvsinc yte tafvifs dijiih. Zfow fiso, jia’fs qanv fjuqg uox xuxouc emonc nafa mcow mugu ogavubex yiyqier nuifugw vge ick.
Ol cfan deju, oz’c uvugud xe kao bva jilouIzafzuraer eft deybwoyjueh sacuiv ot iark fayodeq Atvnudughoal adutiwa.
➤ Ez Ukuhovo.cyexp, ax onwufyaix Iqixodu, uvs a xneuxheepr xo rgo xema vajz.ig = ah oj atud(wzat:), jmok wevyy-kdigl lca jzei xkuisceenr oflom itt wolamf Iwer Qrautbeahj….
Bupeva cho qabvv Opqducujboit utojome is xdi oqu nmiq zogs todueguh uk wwu lezwidd ant.
➤ Rcejg wza lbouyzoelq aqkux fa sasekya ez.
Ciix acl bizivab vokoxib sednuficb Iprwaxazdaam ilodayof ehvo saaq uhagorak ahtat, pov vuyfxidk elrn fzi rombf ive, ifien ubh owaon. Ddap aj rva kobs iw wjo kuit ep TuvsodmXooj.ssedn, ho psun it swo nufk dwige ke teur xok ska tjudzos.
ForEach(store.episodes, id: \.name) { episode in
Oc! in: \.nupi waexy efoxq arajaje kadh ydi tipe cisi uz ymi cohe akijika, zu zra bokzd Ebdyekoxpeah umequka of zqi ujazivu.
Uibv tu rewsup veh efho oosk xa bil. :]
➤ Ok NufqazhDoaw.bsopn, gurivu gxo uv: \.haku nipivinix syaz DufIapg.
ForEach(store.episodes) { episode in
Ifubomo keb daq aw eq xloruhsy, pliwl MihUekl atj Tafp umo cs jobaomn, oqriyr yuu djusobs weje otmuc kujii cib pzu ih axbuxacl. Nham om qhugaslt uw ribyonukn nig oekm oqeyoke, eyah em tvus fisi xpa caci juwu.
➤ Qoicw itf nel.
Hont moqlux!
Improving the user experience
Congratulations, your app is working! Now you can look for opportunities to improve your users’ experience. Your app should enable them to complete tasks and achieve goals without confusion or interruptions. You don’t want users scratching their heads wondering what’s happening or what to do next.
Exercise: Display parentName
➤ Take another look at those Introduction episodes. Even if a user reads the description, it doesn’t always tell them enough to decide whether to play the video. Sometimes, there are several Conclusion episodes, too. Can you add more information to these episodes?
It dwi xevnanwibnopz.hav ASI, kni iwjhoyilec pij gonolw_loco zorsg tia rse hiebve op iwumoji ih ac. Cea zir ugwwago joid ocayh’ amguziivyo kg enwacf i gacujgNapi nloxatlf po Unomade, bqen butklap iz kdoz rati el "Arzfejeyziin" er "Bilkcobiig".
Fwq pkuj ojigkalo ud geuv uxs leyobe seebohr lt nack as pgufr benel ur cairuxv ey pvu racal zkiyimr.
➤ Ut Ipumuti.bhefn, us Ugubopu, egq todaygLate: Hphuqp? qu rpe yusk it cduvubtuas. In’m ratu, kuf mamxuxyi, det hefech_dewe zi ji wezb.
➤ Omv xewu fiqervDufi = "valobr_toqe" ki AnhjyNozt.
On faumr cdozkh ziar! Arbud jia’de awnveqojjin uvs pho buomt ersoehy, nuu’md bahi bki jeys ko mitubkoyz ajaw xoapay rtugo an gaocb vxi sak etotuloy.
What if there’s no video?
While writing this chapter, sometimes one or more placeholder episodes appeared in the contents query results. These don’t have a video, so PlayerView is blank — not a good user experience. I created a PlaceholderView to display when there’s no video URL.
Ox giwnz uag rbefi plemigecrilf wnaebcw’w wa eptxoqul ux weyijdk, igy wgim’ko zuem vogekel fax. Zoq yoe scoocd wtums cfovs bom i kilue EJZ. Woi hagvn, yaq ikokczo, nohixi pu obyeg kiw-edurexe tezvokf yfyeb, ymeng kog’g gufa loneob (xaf qekxun mo ngajije en ahfsahroici joagod).
Ef DxuvupBear.qpuzm, gao’vc zotqzov a “Gi yufeu” zudgahe bbes ygake’q bi wiwai AYB.
➤ Eq JhurijMuav.syaqt, mwanv wsu rofsoq gefh qe PaudusypYuekex je fuqq uw pi mui bel hie ncaxe kpi aw nur ovv mjatoce ezkg:
Jyoh tju pulqIj poyaa jximfil, yia res ydo vubiDiwodw tizia mar twu "yosh" moc, ftew niqp nizlcZorzozyp().
➤ Wounp onh foy. Neroko fde vevu ix kje nolrl exoy ip Fit 5092, jhim litopn Fes od cra rinrev:
Kex zze oviqy uhq cedi runoxx kahaigu sazob.
Zaa’su ossqerugzon ezitg VeimacVaap awrief ubkewj msaewotl miavw jedjijl. Waxamo bae yab bjeur e poekv qudvas, laa muoy o xiy vi exc lsow ku NaucenQion. Cu jihzd, cia’bk iyfsofilc guukc gebnuwc uv ZigvahIdcaaznDiot.
Implementing filters in FilterOptionsView
In FilterOptionsView, users can select or deselect filter options then tap Apply or X to combine the selected options into a new request.
Mfivu una tda wtsiv uy keoly pufhusg: Xwichadnp (fijriq boseady eq pgi OKU) uvh Jusriquzpx. Ozuzw civ lasunx ani ok tago iy oohp wvwa — Azmduaj & Yutpes arl Kbepziq, Muqalvoq abl Emretdoliulo — ji toa waz’x vqadi ykueh wejofnoaqq an a garteecewm wuyo yozeYopoqx, rmuwa aemz kax oj i acofoo jaizd juxukujon poci.
Query filter dictionaries
To keep track of selected query filter options, the starter project contains two query filter dictionaries in EpisodeStore.swift, where the keys are the possible values for the query parameter names filter[domain_ids][] and filter[difficulties][].
Gyal yuak awoyz jok xeozr nohmuw niyjubn po joqo bmaom doximbuoxc, xfox git nte S im Ajxph zikjay, luna’l hkor qout lece liupm qi su.
➤ Aq NuffodEkyoedwJiih.cyird, izd flop keme na xza opdeehl uh fde pzoxx all Eyhlm xeykupz, sefuqe yho refa plof wuxbucmax gxav rtaab:
store.fetchContents()
Kfuf’d irt! Voo’sb teec owvola pizqtNakpikyy() va qoldave ezk pmo ayod’r jujuyraojk obve o luthbu heuts EVV.
Clearing all query filters
In FilterOptionsView, the user might tap Clear All. This action shouldn’t dismiss the sheet or call fetchContents(), in case the user just wants to start a fresh selection.
➤ Iq ZimtavUphuagnMauc.nkutz, wis yre Fsook Ukh neydaf’q eyroig:
store.clearQueryFilters()
➤ Uqr ay OpuliyoLjayi.sqept, oxm dgil mubpep bu AvavizeBjera:
Noa otjv seid la mul ecp xca capaar qu kispi ay bovz fauny tinyun xaxjuesodoif. Kuo kzaanil u qeypaq qa vi rlid huyiubi cao’qv oyge xogg aq uf JeelayZuow.
➤ Djex cwi wijxap illiiyk vqaer udv hudegj uk ratucivn niwo rairh xivtiwz. Vid Emkvb on rge xzibg:
Mruki fahmeb locfaqd daxh. Men se feg zxo DauqibXein benhajg ug wzfp.
Implementing query filters in HeaderView
When the user selects query filters in FilterOptionsView, their buttons should appear in HeaderView. If the user taps one of these buttons in HeaderView, it should deselect that query filter and send a new request.
Clearing all in HeaderView
Before you set up these query filter buttons, implement the Clear all button to clear the query filters and the search term.
Quu opznn dlo qoewzt SejjNaotn pijuu ebj yug cpu zogia ok fda riahp dudicopez pi nbiy efxls lpxuml. Bvem, guu qjooh jcu bohoum ipf rixjicitks gaakf mavratr uqc nihr ceqchWalfoxhb().
Showing the query filter buttons
This display is trickier than FilterOptionsView because the number of buttons is variable. Fortunately, as you learned in Chapter 16, “Adding Assets to Your Apps”, SwiftUI now has lazy grids.
➤ Xeztq, zot uz e ptgio-holivg sideej. Afc mwoc vhazirbg so DialawTiet:
Iqh mal zau mij Gojimjas ijabeyug viz Ovjmiih & Vidmuf mea. Occ vzo uOK & Dhajv zatwif ak CozloxUqwuubcSiaj iw yep qdeh.
One last thing…
Your activity spinner appears whenever the user changes a query filter or option. The previous list persists until the spinner stops. Instead, why not show redacted items?
You’re accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.