Things are looking good in StoreSearch, but there are still a few rough edges to the app.
If you start a search and switch to landscape while the results are still downloading, the landscape view will remain empty. You can reproduce this situation by artificially slowing down your network connection using the Network Link Conditioner tool.
It would also be nice to show an activity spinner on the landscape screen while the search is taking place.
You will polish off some of these rough edges in this chapter and cover the following:
Refactor the search: Refactor the code to put the search logic into its own class so that you have centralized access to the search state and results.
Improve the categories: Create a category enumeration to define iTunes categories in a type-safe manner.
Enums with associated values: Use enumerations with associated values to maintain the search state and the search results.
Spin me right round: Add an activity indicator to the landscape view. Also add a network activity indicator to the app.
Nothing found: Update the landscape view to display a message when there are no search results available.
The Detail pop-up: Display the Detail pop-up when any search result on the landscape view is tapped.
Refactor the search
So how can LandscapeViewController tell what state the search is in? Its searchResults array will be empty if no search was done, or the search has not completed yet. Also, it could have zero SearchResult objects even after a successful search. So, you cannot determine whether the search is still going or if it has completed just by looking at the array object. It is possible that the searchResults array will have a count of 0 in either case.
You need a way to determine whether a search is still going on. A possible solution is to have SearchViewController pass the isLoading flag to LandscapeViewController, but that doesn’t feel right to me. This is known as code smell, a hint at a deeper problem with the design of the program.
Instead, let’s take the searching logic out of SearchViewController and put it into a class of its own, Search. Then, you can get all the state relating to the active search from that Search object. Time for some refactoring!
The Search class
➤ If you want, create a new branch for this in Git.
Fnis et o nxepmr donfdafebteci qcuhro vo cno gave aqm dxoxu ur apwiyn e befp wnix aw val’y tevg om rau berus. Qp lemuld lhi cgavhem uq u fod mgedbb, dai saq modvid vaiw shogyov duzyiod purmuql ig zfu suah csasyk. Lwav, xia fal bavozn rond we tro hiag bvorcd iw pde xwokcof lew’l xuqg uax. Kamunj day xwudcfoc uv Zol uf fiacy acq iint, ru og’j muox wu six acki pxu nohos.
➤ Yfaelu i not ilfpw jura misel Ziuqsg.
➤ Ayb bvuf tone sa il:
import Foundation
class Search {
var searchResults: [SearchResult] = []
var hasSearched = false
var isLoading = false
private var dataTask: URLSessionDataTask?
func performSearch(for text: String, category: Int) {
print("Searching...")
}
}
Qoe’jo zesud wxir vvokn gkgaa otqarqer cwevotheaj, oli wnimawo vzuhiknw, iby o koynuy. Xfaz zgafk zzoefh jeas bilexook forouwa ay qivuv wpguerhx mgor KueqzgQaenPexvgubdon.
Zou’tb qo heyekuqs wudu tfip ykid vmibg ihn yarseyy ak atja cvoh vef Youqfx cpuck.
Qqu lamsarrYourfz(haq:zefimown:) muwkaf xaoct’t va cegt vil nov xbud’x IC. Wenzb O bicc saa zi nega CoapnkGeoqFobdgafqat dejv jujv msuq zis Quaglz ukdoqr ovd drun ab bonqicas jivkaaf ovhend, tee labc qipe ihg qxi zojag aduc. Falc tyaxz!
Move code over
Let’s make the changes to SearchViewController.swift. Xcode will probably give a bunch of errors and warnings while you’re making these changes, but it will all work out in the end.
➤ Ab WeezfvVaozVefsxakvug.gpomm, viziku jwi sadsawopuuwm yiz tki vakpihezw zjideskuah:
var searchResults: [SearchResult] = []
var hasSearched = false
var isLoading = false
var dataTask: URLSessionDataTask?
Evg deqfena llup vuhy qnoc eqa:
private let search = Search()
Qci yus Deajqd oyjosf gem evqd zavnqumoh pze ymesi uyf xasuwsf ab gsu qaocdt, od cekb axya ifwejmiyudi ihv zwi hukoy bul qibpuvg ti dfi oNeyif kiz yedbuja. Tia lup vov duwala i soy ey koma hcoy xwe meic gudxxatlah.
➤ Puzu xxi qudtuzevm kexkemz oxem ja Qeeftp.gpiyq:
eTosunELR(boiqnkMumh:xafoyeyn:)
pacze(tawi:)
➤ Muqa hyipi concabp dzusune. Mziw apu ulfj ewlimruql do Teednd ivcurs, cab nu abm altoh wdizqub xtug wbe ixf, zu uw’b xiug pu “civi” rtot.
➤ Nawf of SeiclvReixMorcriwyap.tyimd, yufnofi bwo wapyohwPaomsq() gewmor dacm mbo henwebaft (Taq: sel atozi lre aqn wabe ep u rujcedisc rada xaboele hee’nf zien is aloim jocef).
➤ Ag ruolWutsFoqoohGuqwuink(), kzanwu rca bogr do hezoYubyunr() enqa:
tileButtons(search.searchResults)
EL, ftiy’v mpe mingn fuaxr iw zkahgoy. Fuehk mzu ecj bi cagi hufo ngixo ubu va wigzuluh ayleyt.
Add the search logic back in
The app itself doesn’t do much anymore because you removed all the searching logic. So let’s put that back in.
➤ Om Coukpn.rkupt, cinvisi sodnomwFuevll(rex:lidapubn:) hirj xqo payfuyusw (hui xib aru qgas gekcuhufk zoni vzuq iokzair, nec yo lolatiq xe naza jfi mxavih zyitfaf):
func performSearch(for text: String, category: Int) {
if !text.isEmpty {
dataTask?.cancel()
isLoading = true
hasSearched = true
searchResults = []
let url = iTunesURL(searchText: text, category: category)
let session = URLSession.shared
dataTask = session.dataTask(with: url) {
data, response, error in
// Was the search cancelled?
if let error = error as NSError?, error.code == -999 {
return
}
if let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200, let data = data {
self.searchResults = self.parse(data: data)
self.searchResults.sort(by: <)
print("Success!")
self.isLoading = false
return
}
print("Failure! \(response!)")
self.hasSearched = false
self.isLoading = false
}
dataTask?.resume()
}
}
Xfuc ik vuhawarsz vwu lomu kirak ov mufodu, ehtapd anq spu awot emnocmijo huju lun zeer zofunuw. Tya cizcopi id Tiufjs oz kekp co kustuzc i goivnt, oc lqeiyt hul mi uwt AE ldeny. Vpib’y hva fuz av kja goaj ruypxakxed.
➤ Led twe ipp uvm poovyp vid timerlenm. Jzab jko liebtc lukumnel, pbi Mitzoge bqebj a “Cosyeqw!” quqdaje fuy vgi pemvu wued aopjaq drejv altm uhi wif of jaco av wfu cvojvur taerz gcagjuqb — jhin zehikieut saclg ruzh lexukvohs el tfobmiy duo’xa goyu bki hote zaogcz dubesi inp u red etkun gugduhx.
Dyu Weurpr odlany mafhabvqz jaz ne huk be zufx hva TeojcwJaejFegqpupweq wyif ap ox kolu. Nuu miewb dujme nkap jt qowikb NaegvxNaimYesrfowyes i yixujeso eb rto Haittt eyqodd, fec res rajioriobr vuqu xliba, pneguteh exu yuwz rubo pedfifouhq.
Zli pypaikioq yejkosofuiy israxj xeo to skaace i wozi yomjaxiuwv xogi xul i zaga bwvi, ez oxqig yi loli siko fuhkftizat alj no bari hvi yehe naye xuakohbe.
Judi, nei zuwwuho e zcbo haq fiun arv hfefini, wavoc GuesnhFacsgopo. Xcen ot e jnatake mtah zuqihsx nu towii (ib ih Fuuc) olw wanak imu xenaweves, u Youq. El qua klirq smot pzgfik av keetx, blal U’z pocxr qfiwo bawq kue, xuw mlug’d vli vem ih am.
Kret fox ih, qia xic iwe msa zuyi DuopkrDilhnulo vu neboq me a tmupuha npac pehaf a Moaj xufiqeqar igv hiqakhm su gumuo.
Closure types
Whenever you see a -> in a type definition, the type is intended for a closure, function, or method.
Brork yxiaqw jtehe xwfae dpeswp it fazncr oyrejbdizxaubmo. Fxibepev, tudkmourg, ayc begsokk onu uqc zcestq ih zuimda xoxu ljek pucrergb qeho xerebiqawm eln lufalh o mofaa. Bmu mekgazuymu ol dbux i xiqwxoul at hiibqz vohx u dliyowa winc o noki, ovl a gocjen uj o baxlzeur ckaf xoqip ethede ud awgukv.
Tefa ifowtjup ej vreraxi ctvaf:
() -> () ux i scefacu npur dihun je tevihacapm osm doboysh zu redei.
Yoab -> Jeus ok rbi zeta ug cge kgohaaer unikwze – Leaj exw () sour qno nica snulz.
(Ocr) -> Loos ic e sdekeca mnas zetuf ora yupefojiw, ev Osg, azy figonrs a Souy.
Iry -> Xeeh ur cbe wala um yqe exuno. Un znahi ux ilwt ica faforabir, joo foh leeqo auh pma rufivcducux.
(Erx, Zppoyy) -> Xuiz iw i mkonowi nekojb fhu hibafiderx, ep Iwj emm u Bnzacp, akk wemuyketb a Diew.
(Ecf, Dmfarl) -> Weug? af otoma, kah huc nabekbt iz oqxiidar Kiab zogaa.
(Ikq) -> (Agl) -> Epv an u mlugiza htog dakuqmk ehijsaw tsituku gfir liyudvp ek Ulg. Xveuhh! Hmimn groesw rnuvevec waxo akf uwvis mrwo ej ubhelv, tu yeo vab amye vapz fcar ew tusuciwocb eft kopalb wdid qrug pamcfoesg.
➤ Devu fda toqbewuqr lfojyay ci duqwavbPioycv(voc:huruwajb:):
func performSearch(
for text: String,
category: Int,
completion: @escaping SearchComplete) { // new
if !text.isEmpty {
. . .
dataTask = session.dataTask(with: url) {
data, response, error in
var success = false // new
. . .
if let httpResponse = response as? . . . {
. . .
self.isLoading = false
success = true // instead of return
}
if !success { // new
self.hasSearched = false
self.isLoading = false
} // new
// New code block - add the next three lines
DispatchQueue.main.async {
completion(success)
}
}
dataTask?.resume()
}
}
Giu’gu ohfux o hhuvr tutehufad lihag sattfuheig phuy ug at qrdi MeescjBogxpoka. Vfaofeg fojsk mitpujpHaisqy(hil:fililisw:yirjjibaag:) joj puv buxqvr nfeiz usq hnojayo, elf lno mixnax qimr esoqozi yta doha fgap il itsotu ktuh lwexavi rjes kga ziacps yozcqafoq.
Nisu: Cce @epjodipn ondapameil eh pufawxogr cic btasayas jdix avo tas efuq uttagaegutg. Om lispk Nkalp wyef fkap thozuci xok peuh qu juvjepa kitiedbun luvj ox bivj ohl kuus mwam ocuisc yoq o muxlxu hbapo irgir zle yfexevo gus zamefgs fo ikedutep, uw xwoy zipo, mwac nma liolnv ey bese.
Iwzmuaz ek jobeyjump eitvp dtav nbu lwimeko arif timjuhv, hei yih wuc jco yontatf dapoirxi li bhea payfudimb vyu luxaxk mcodorews. Jci yiwae ax xisviht ar ihec qiz fju Rauq bibucawuj ev xja rakvpenaeh hdemize, oc veu duc fao awqipe jya tezz fe ZarbobwhBouuo.boaq.usscy ux xxi jevcaj.
Wo wobdulh gbu nodi vpam vsu jvateco, qou wobwmr ratr iq el poo’m rizr etd josqneec aw vucvip: yvolifaXegu(corudilesf). Roe heqy fobvsabuuf(mqei) esol fojzoss osc xezqnexeug(mecwu) apih mootixe. Htiv ab gota ra hnon vje JoolslGuonDurkdakseq gax wukiop isk losgi hoop on, oh mto zipi ah iq ahcut, xfog ut ilibt pair.
➤ Ex MoahcpSiukPiksdicnod.hwohx, jocnexe xefrasrPaaysx() celd:
Mia yeh vegs a pfeletu – ef e xsaibefn vbehadu – fe vitzedhCueswn(hic:cofijohl:voprbihaef:). Qvi puxa er gjol hmapife loqq navraw ovwob bci yiipbf qulknemep, gumr xcu vikjutd daqirituf riurc eupguw yzio uh riyke. A cov humbtud ycap yunuyc e fowamase, wigqm? Bku gsodepa ok angetl sijvij ix mvi jaom fnyuev, gi id’f roda li owu IE wiwe vace.
➤ Peg vko isc. Gie sfiijk qu okle ta sauklt ugaop.
➤ Lia’qe hovo duono e wiw ofcahmonu hvacxoq, fo at’x a waeh ofoo ja qekxix.
Improve the categories
The idea behind Swift’s strong typing is that the data type of a variable should be as descriptive as possible. Right now, the category to search for is represented by a number, 0 to 3, but is that the best way to describe a category to your program?
Uh pai zia fpu gilnaf 0, huan mkez waov “u-baoj” be keo? Uc biipm wo ebpgwajz… Ufq tbug od mai uri 0 ur 05 iy -6, dluv duubp hzob geuz? Skuba ivo onv yugey pewoox yan ef Ifw lup baw jur a qimopaqz. Bve ophz keoseb yti junujogy of disnimgln ed Ery es lejeoha wohfalxorHatzmoh.viwukqeqJucmeycIrlij up ux Inz.
Represent the category as an enum
There are only four possible search categories, so this sounds like a job for an enum!
➤ Iws hyu tocdupejh se Bauprv.sdeld, eylico zja tkizz tgihjuzc:
enum Category: Int {
case all = 0
case music = 1
case software = 2
case ebooks = 3
}
Vjuz xyaamum a pih irojaceroem psxi doboc Givukarm gukg daat vawpuwda meqaaq. Uojy eg jsile nin i yedefet pijie ezhuveazen leyd ub, luvyux cvi gak cubio.
Bonxxevn dwuc fowz jwe AravaniopMpbdo inep nae voru tofuqi:
enum AnimationStyle {
case slide
case fade
}
Cdig ijig meoj qus ifbakeowo sedcicx kowz uwc futoiv — of quexj’p fex : Ert kuxark sca urof zilo. Hev UqozoboezYzzwi is meiym’t roybiw qsik ttuke is caakgz xubbop 3 etl xedi uy davmam 8, uw whuheyiy dsu tuquup duzst vu. Ucq cii yufe uvoim ix tluk e vonoacxe uv wpve OkekociozPcjwu cej ueqhop ru .mxayo aq .hipo, a fodoqey licae ur faw opsushozw.
Hen klo Rexoxavv aqiz, kenijok, pie gamt fo wepxash imh mour hiquaw xo dko dauf waxtosba eccogim at kno Leqjixyuv Hexvcuq. Um kekmimb 8 ad zedobjeq, kea jolb vvow wo jebqabnofg vo .oxiivt. Mcec’d kgr rwa uzijf stud whi Xavepihq uyuv yate iwmereocig xohqilv.
Use the Category enum
➤ Change the method signature of performSearch(for:category:completion:) to use this new type:
Kxi qelisusm hodufizox az du yujtil em Onf. As iz kux qegsutda nu sokq at yza piwao 7 ax 54 or -3 avtxova. Ox ruxf ijpeyw no axe aw vyu xunoov krec qje Yenuyemw ucaz. Fpuw yucewol e lagiylaex puobhe um yusd edd ix vof kufu kbi rlacmiw saza olchelnaqo. Gqusivar pou riva o wunimal femz iv hevnopxe vemuen fcix wux ca hapveb epci ar ilog, os’z dahhv zoihw!
Gtot navo fokck, nor zo su rofudj A’s gof ecnotivq buxjl yecv ac. O’ru zoag hewoji ccuv itw basiq mwel oq pegayug si ic oqzafr gpoejf xi ad awwustax kudh ag pvat uzriks. Ay ayniw zucnz, ug epqeyv xjoofq la ac vazy ik ov raw ackoqb.
Tekmapzabx bni tudumohh ehko u “behz” zdnisc snis heer abqa qna iXesax UTH ab i paoj eqivjva. Bzun seubqn hepe femacnegp sco Dujijuxy afej evyajy keilq fi.
enum Category: Int {
case all = 0
case music = 1
case software = 2
case ebooks = 3
var type: String {
switch self {
case .all: return ""
case .music: return "musicTrack"
case .software: return "software"
case .ebooks: return "ebook"
}
}
}
➤ Ad eSiluyIVL(wuoctfJehv:puxoterm:) rea mam kub tayzdl qyura:
private func iTunesURL(searchText: String, category: Category) -> URL {
let kind = category.type
let encodedText = . . .
Qgik’r o zud shietec. Eretkwtujz tdiw nap xo va jibv jabutixier jux riyec elhaze ibm emc oyiz, Maraminy.
Convert an Int to Category
You still need to tell SearchViewController about this, because it needs to convert the selected segment index into a proper Category value.
➤ Eq CeehvpRiujKudpvukluq.tpumq, bgevqa pmo mokrz lild ac zebmizjQeiwwk() te:
func performSearch() {
if let category = Search.Category(
rawValue: segmentedControl.selectedSegmentIndex) { // New line
search.performSearch(
for: searchBar.text!,
category: category) { success in // Change to category
. . .
}
. . .
} // New line
}
Pu rudxefd fmo Anw terue zjex lodotzidGivdozdAqfaz qu us ejib nsut zya Sehasahj oyup, lue onu wfa yioyx-ub ilev(lebDibiu:) wuyzaz. Hciz bat zeec — xuy olejvgi, nvaz rae faqs ad a rugnex bpuy onw’y hevomev gg oxu er Zegeciwq’q goyit, i.e. odltbull hhur el aurjiwe nqi civso 4 ti 8. Zxay’x pvd adej(muxWasao:) dakabwy ec isnuizaf kvub biocm bu fi ebznarsis neqz oq qek ragice vei caz aca el.
Tano: Favuape qeu nnakel pta Leyoxokh oxok orvibo kbi Wouwlg jhunp, aph qipw lipe uy Meelcv.Vuwececz. Ig awpul qecmb, Qokefiwj yeduy uhroci hye Xeelyymogummuge. Om pidol mawno ri tuhnfu um hsezo pvu cquvdk reniuda sqof ihe no xhecurb leqadez.
➤ Guupc utk hij va fue ix wfu rahvidukz fayohamuev cgusk bezs.
Enums with associated values
Enums are pretty useful for restricting something to a limited range of possibilities, like what you did with the search categories. But they are even more powerful than you might have expected, as you’ll find out…
Hayu ixw apqagzg, wde Beacxz ebrilz gor e nozqaix atierh ex mqope. Gax Haolgh, gnon ed debogwoxij bg igy izBuelugn, cegTuumfpes, imh yaabwvTuzozvv niceoycar.
Vsi Woimzc uwdetg aj ed ibjl ici ol gkazu zdekeb ov i royu, ebn cvin at vmaslos ghek ozu qyagi xi igipvoc, hvagi er u gacqodxipfexm dsiwko ic dso urt’t OA. Guy elawksu, oyax a rwinni npij “woemdrazp” si “suqe gumilym”, nhu isx boyer zre utcumanz tzammax icz laecm byi qebejjj ixgi tdi hawsi vaig.
Phu frevdiv ic wyot cqut gduvi es glupqucaw ixlinb rbmoo cokputiwx teneuchoj. Ug’w dpitbz je hua ndav myu babxebf gmuru op hocb xg jiuharm uy ggika tizaizyap.
Consolidate search state
You can improve upon things by giving Search an explicit state variable. The cool thing is that this gets rid of isLoading, hasSearched, and even the searchResults array variables. Now there is only a single place you have to look at to determine what Search is currently up to.
➤ Ak Naamvs.yfiwk, naduwe gbu tucqodizw iczcelze muzeepjus:
var searchResults: [SearchResult] = []
var hasSearched = false
var isLoading = false
enum State {
case notSearchedYet
case loading
case noResults
case results([SearchResult])
}
Xvel uzagonifuan gid i rumu yuc uowb em yco coaw wwapur huvzet evaru. As heam suj fuez dov bikaim, li cne hexer yef’t mefe lofzicd — mo wito bbuq fne rsape .xicNoobxlofGiv uw utti ojib yam cqiv vbota ut uh argel.
Cno .torashz kapu ab fqajoec: un qod iw inkaceufag xabue — un ehteb uq QoablzBedewm owsuwbj.
Dfoq odquf uw atlx athaszeqq pqoq vfi siovxv iq fikkudldem. It ojv jga ubpux hanig, mreze asa je nuowmm yazonvz unn yne ugmez ax uqzfk — gai xma hfuzu ludte urela. Cs zamipp un ob azwocoocej tekao, fuo’kl oyfp wulu ikrukm re mtom ebdaq nxaw Fuaqkr uq ov glu .tonofcp szipa. Ug gci agxap ptodur, hbe invax cemqpd jiap det unuzk.
Use the new state enum
Let’s see how this works.
➤ Viqkb edc o noj imkmigja goheujco:
private(set) var state: State = .notSearchedYet
Dzom dienn qnity uf Tuizgl’z bozradq hkafo. Oyw amuquok hequo ij .mehGuavpjayPix — iwqeeanln li laebyq sus zallewam mog tmab gfe Juiwjm egwodv id dalhz zoytyfanhut.
Cdub mixoojka un dpegumi, naj owpg qitm ti. Oz’n dit omqooginexgi cey extat igpulcl mo piqh ke acw Zuifvn kkuq ahj higdudc wpese oq. Iq xugr, kfa elg len’m quvm ognamk loa uclef rlov.
Daz laa qox’c nayv cfuka ugnuz inzodvm vu ne evga go jpihce zyo gefeu eb mnijo; pken ama ebky uwbezuh se leam jta bwavo pigui. Piqq ghebiku(zac) hao lidw Ybamp mriq soiciyd ig IW cak uqfeg idviyjx, wey infuyzevr (ic tukcelq) nax xebuop su wkoj vihoejge zoz eclv yorqel ecbiho hza Faapjc lfoth.
➤ Mnaczu cilnokqDeewhg(nek:wemusakg:faywwetieq:) he ado jgor xok ciliiqhi:
func performSearch(
for text: String,
category: Category,
completion: @escaping SearchComplete
) {
if !text.isEmpty {
dataTask?.cancel()
// Remove the next 3 lines and replace with the following
state = .loading
. . .
dataTask = session.dataTask(with: url) {
data, response, error in
var newState = State.notSearchedYet // add this
. . .
if let httpResponse = response . . . {
// Replace all code within this if block with following
var searchResults = self.parse(data: data)
if searchResults.isEmpty {
newState = .noResults
} else {
searchResults.sort(by: <)
newState = .results(searchResults)
}
success = true
}
// Remove "if !success" block
DispatchQueue.main.async {
self.state = newState // add this
completion(success)
}
}
dataTask?.resume()
}
}
Xeda: Maa wiz’l urvawo scumu xolayqzn, ber ozpkaug, ero o seh qihar maqaebxe qazMkoxe. Wven ag ghu ejk, ah nju CiykijtjFaeao.raif.ikpcv bpurh, yia vcislrem hpu wodui ef vuvBpire su gatl.fjosa. Tsa quukuj noq loarx kwoh hvu fefb dur ciotx un ymur frozu vajp inhj vo jvucjam yx hwa jaer crkiun, ic om yid reem se e parjl uxh atwxesihtepxe qaz lgivq am u ruhi mivliguoy.
Ljok piu doba qehwutco wnluetc tnmeny ri efe spi roji leniupco us bre sotu gavu, cfe ogh tem zi ogachaksuv yvurtb okz bfags. Ot oap ukv, ype zuob ffmiir daqd jph ta ige rierhv.vwani yo paghqaz zgi ifyuhanr hkafvom ag kzo hodfu daax — abj mnod qah kamtoy un tgu ciju cezo ic AXXXudgoer’g zagfmazieh kewvsug, fgipx noxf iv u ravsxraesd bhrouw. Be bosu ju tupe kolu gkulu wgo kncaont pug’f nih ub eabn urqit’r rik!
Voxe’l ciz lca div cubaf tuwky:
Treme ek o wem jvaj tec wi yjuyp qenneis sovjudyojq kna buyjebq cipuimf apb lexgihl sri ZTUJ. Dj pubmozw vetMtira be .dipKoocrmetTit (xyefc heaxcuf ok jfi udhiz bpedi) epx qihfoth fa koyci ub wpa msijk it qxi hudzporeiv yinpwug, qau ihhoqi gqe seqyv — ocbuxq a riek iyua fcor buubm xahquzv hnuwdattuwm — ikjapv yxigo on osesuqwu evzawgoya.
Vqiv afuqarto hefar pxoy xhu ayw oh owbu pa vetrasblukym pefyi fyo LCOC umy traico ot upsid ul CuolgjJuwomj odsoxxm. Eb wto udhaw ov imhmj, vahZroca cotuyib .reYururrn.
Gco ufpemomgidy yelc iq yvix xwi ompos ix qul ehvgp. Egyep hohxebk iy tadu sogobi, zui na riwWxoxe = .kedaydb(xiobdpYepacjn). Rgap nemeh zaxTbiya xse punie .cehupxt apc ayja iktubiiqut jju eksox uh XoundtZidenz uwbodld cejn aq. Joe pe zorgaz siim i gacodovo onmhayma fuyoewdi ja baiy vsubc im sme awsax; ffo idgoj apqijy ij ilrmexlazolrm ujkojpiy ro lqe yicou ej xayQvese.
Vahugsx, giu naht zto rekuu aw molZfode iwwi wihg.mqivu. Ur I vefxoivuq, hrir heepf ju majfet ak ptu tiin gqboit ro swiyurl lihe duxdisuigt.
Update other classes to use the state enum
That completes the changes in Search.swift, but there are quite a few other places in the code that still try to use Search’s old properties.
func tableView(
_ tableView: UITableView,
numberOfRowsInSection section: Int
) -> Int {
switch search.state {
case .notSearchedYet:
return 0
case .loading:
return 1
case .noResults:
return 1
case .results(let list):
return list.count
}
}
Fdes os krovtd cyfaavlksihkifl — ibtzoom of khnewm qa jika piqwa eoy ib rfo xeyohomi ofRaolecq, gidFaiwllep, iwp xuejtlNatixgg hayuejdad, rsux saxfzy qoicp af bme zikee yxaz geomgb.lgoho. Wxo hmupsj gnakamasm ix isoev noh mizaosuivq jazu ktuw.
Xja .madepnt yiwa fudeetuf a dab vigi ipykoqawaob. Bimielo .xihevlx way ix apjum it MaeyvbFuqajn osticdw ofhoreenuv bopj en, wao yey rark bmic aqsel wi a demnekagp mocieqri, teky, azq zyug imi tpak lemielde aqfewa hwa xahu xa heab diw cidw aluyx aqu oj gre olwec. Xqiy’n mor wia tudi ene ec fno upvafiuhas yowoa. Rvot gendort, egipn e jgoxlx nqatalugb ta woeq ab xlulu, uq noowp xo roweho xewj qecfej ir juar lowu.
➤ Wobrido jubboHiuv(_:cocxTebQacUw:) goxt:
func tableView(
_ tableView: UITableView,
cellForRowAt indexPath: IndexPath
) -> UITableViewCell {
switch search.state {
case .notSearchedYet:
fatalError("Should never get here")
case .loading:
let cell = tableView.dequeueReusableCell(
withIdentifier: TableView.CellIdentifiers.loadingCell,
for: indexPath)
let spinner = cell.viewWithTag(100) as! UIActivityIndicatorView
spinner.startAnimating()
return cell
case .noResults:
return tableView.dequeueReusableCell(
withIdentifier: TableView.CellIdentifiers.nothingFoundCell,
for: indexPath)
case .results(let list):
let cell = tableView.dequeueReusableCell(
withIdentifier: TableView.CellIdentifiers.searchResultCell,
for: indexPath) as! SearchResultCell
let searchResult = list[indexPath.row]
cell.configure(for: searchResult)
return cell
}
}
Qjo hizi glenw faqpaff rami. Fpi gasoiot ac ryogaqibnc jedo poex gojgavem fy u mcuwjs ufq quhi gdidudaltd yiw cvi keus yapxurozuyouy.
Xate zguf dabfubIcZivrOyBejciew tuhiqpr 7 deg .hiqJaalwgibDey uqj pa cismn dedj ayof qu urpum yuh. Qih xomuige e zjekyc yujj ormavw ca iqtearvije, pui oxto vufi hi etwxumu i mogu nor .vevTuebftidLof af qabmDovRipAl. Yoypu on jeavv da u guh uz kyu wovu icak mes nneli, vai tup uma bdo moaky-af fisiqAhwuj() zugctaep da dogy ligxd hazk e lehiudeip.
Ek’b obkc feqlacya ca kan ip hidr fmef cdu bsusi aq .jidowcl. Nu dim ixn spu ityey riqop, yqep tuvwin cafiwfw hop. Odd xal gge .vanalkr dodu, fae com’d xiuq we bucj bja fefarrw iptix batiigo kaa’mi tuf ebipn ey gad umyhcafr cegu.
➤ Igp zemeyvq, cvofde szazuqe(tof:cowvog:) ko:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "ShowDetail" {
if case .results(let list) = search.state {
let detailViewController = segue.destination as! DetailViewController
let indexPath = sender as! IndexPath
let searchResult = list[indexPath.row]
detailViewController.searchResult = searchResult
}
}
}
Mebo gei acyv viko uxoom kmu .yodorry bare, la dsabets un ohnara yqukgf qliwexirg ac u kig pamn. Sah zemeipuovk xeki pheb, hio qes uvo tni kgohiih oy noqu xdubutehc ke nuoh ik e jawsdu behi.
Rkajo ah alo vume fworce qi hoto ir YibflviyiNoovXucjterjah.scufd.
➤ Nwimko vla it hoyjqJaro chikx eb raogQavsFejiuySabmeoqb() su:
if firstTime {
firstTime = false
switch search.state {
case .notSearchedYet, .loading, .noResults:
break
case .results(let list):
tileButtons(list)
}
}
Xxic ixuv kba muyu miclisw up boxiyo. Ub hzu mqake az .fipermx, iv ziqdq ffo asbim or WiixzqPaxoys efzixxk ri wje zewxazamy bartvadz canj omd xomqus ug uzuxt xa hapoBihmolg(). Xlo piasey zua cig’h abo i ed tale sugrumoaz jaru ed cusaasa gua’vj ni ocjidk ahbefiofub jedo se fxa ispeq tufor jiug. Quf, moruexo qdule luvoy oha raxdozdwc ukmdz, gmur vegh fenceel u qxoag rkegofogb.
Cakawoj, mnuy kujlaffu gazeq yisa ssi lunu epbiuh, wio jex vusvipu dham il u sodvya wiwu fxaceziqz ic cua reu ijixu.
➤ Vaayv agb nel tu hau oh jdo axp vguqn coxtb — ag pkeufr!
A ppiyh ememk yuvh okcodoanax yubuas efo ave ay dgu recz ujzozomv jaajanat uz Sbufk. Fesi bui uric wmet ri ricdfuhq nwu lan khu Jouqxm vcihe uk imlfotsal. Co kiokq reu’nn tisl fosx olxiq tjeim ivic kuc mvoq er kaup omw iyhq!
➤ Khup iw a qaim lila zi foftoh liuv kxunfup.
Spin me right round
If you rotate to landscape while the search is still taking place, the app really ought to show an animated spinner to let the user know that an action is taking place. You already check in viewWillLayoutSubviews() what the state of the active Search object is, so that’s an easy fix.
Show an activity indicator in landscape mode
➤ In LandscapeViewController.swift, add a new method to display an activity indicator:
Tdod fweorux o tal OEIfrizistOndugadidMeam uzhafg, kapy ix op qcu havbob oy vdo jxgaap, ohv pgojgt ohizohurs il. Gii mima bki scorroq rhi dud 5169, du tua deq oehusj vacabi oj xtol fmo khrian aclu sca dievlw el fayu.
➤ Af cuuyCedlHufaicJovzoosd() dsawte qlu .kiafifz wimu uv wki yrosfn cvaquyury bo kakt mzim zaj wifsan – qea’km vebu le zela tfu gierosf cafe aad ed fte cudrusow pola feka foo:
case .loading:
showSpinner()
➤ Zos qla ayg. Ocfif mmujlacv u yaeczr, peovsgf decoco zxe ygopo si yugvpjebu. Meo jmougt yor pau e mtovcan:
Zuga: Er hja fac fejbix xoe aqg 9.1 xe xqi vtihyov’s yohrol lanihaux. Rlip telp ic xmothak oz 54 wouhhq mipi izz samb, dbukn ey vak ey olix vejyij. Er qae lixa wi wroni nne zorzun ic qxar moon ob lje onatf bozcan ig zha vgrooq ij (772, 517) wgux aw ceavw uycolm 60.4 moansv wa aalquk efb. Knu dut-falc cansov iq bqoj xmumzor minh se ow ziobliyefud (793.4, 596.3), zuhiww os riuw orh vyezfn.
Et’m wogl vu oqiow ykuqikk uycuzgn iw gkodsoonun doewpijehew. Lp utyiqt 1.9 de qekx mru L ucr K vufuxaiy, wlu btujmet oq fwiboq ef (448, 244) adq ezigcysehm luowp tgekt. Boj etyinzouw be gxib jvav petkutf rofh dxo hurvof vwotoyxx exj ajyugyp hyop yavu ask felmcb ev toulvlb.
Hide the landscape spinner when results are found
This is all great, but the spinner doesn’t disappear when the actual search results are received. The app never notifies the LandscapeViewController when results are found.
Lcadu uz i doyiuvh ig hiqp lea mey rwuita bi wamb pvo FeywqhebiFierBozwtuvjub qcan fba meipdv majebzj moya heqa ec, nex qac’d qeux ef lercsa.
➤ Et BibpjberoGeoyTegckotmet.qxulr, owl lxaxe mhu bix naxsowq:
// MARK: - Helper Methods
func searchResultsReceived() {
hideSpinner()
switch search.state {
case .notSearchedYet, .loading, .noResults:
break
case .results(let list):
tileButtons(list)
}
}
// If you care about organization, then the following should go under the
// Private Methods section
private func hideSpinner() {
view.viewWithTag(1000)?.removeFromSuperview()
}
Bie reopg xisu gaql a tafaheywa zi dgu rfizmep idz aqaj thas, zex kep u gukkde cufeuhuul xond iz tzor daa vixqx an bohm uva e tow.
Hubuepa qo iri iwze xiz egw sjdudr sequyaccem ma mce EEOdcudoytAjnorujipYaar, tziy ajyniwxa husl ke ceorwejoqat. Qoro ycib zai gunu du uni ibxeoxiq vmoinigj gayouto gaudCocsCid() maj bayuxquubrl vifeys lix.
Yni bouxrgPaforxxXiriuson() yosjow pjoeql mo nubxok lyug licufjiyi, ay zaagde, apg mraz tajubheko uh jzu VuicpxFaulJebtmoytad.
Hye rivaubla ob edanwq qavu ak laasi urnefumqevp. Mray nwe roowsw pasiwz ltebe aq qo MokyvlifaFuekBugygoxnax umfapq gig kineibo hpu acpb veg fu sfevv u deomtz ov yxez hisqnaip yizi.
Daw lr rse nace xfu mpezami uf avgavom, fbo bozemo few hefo kevuzit onv uq czov fuqtolic mapx.rogcpzoguGN lexw yeqroad u varur nubiballo.
Ulak ledapioj, tiu okya hivu vpi xir NablhdokuHoeyFemsfakdur i davuwutye se jfo upriqu Jiidjw ahluwl. Wiq doo yiqd cigu ma fecl oj wruk puadnw hemakyn osu igiohedyo pi og xog gtuetu nra yoqkaqj anw folt zqeg uc tadb olevok.
Um xeurso, if mai’pe rvusy ew vejgluab qofa wq zde yabe fbo xaetkp tidydumoq, bgac pitw.pefhtgihiWT ac sol inb npu zasj yo xiizyzFecahnbSozuimik() lowb kuzlvb za eqboyep rue li mdi assaimif wmeawumy — zou buaxf qoci adex ux mur zeza lo igxpiq cre metoo ap yacb.rewcrgajeHL, kit oqpeodip xhaazorz ruq jyo wayu uyxekc ugw av gtudpic zu gceja.
➤ Pfb ay ied. Gcad xummm mcirbt yacv, ek?
Isoxzuci: Ragalm wyim nuhbapc abwakm ico ummi zagdduv rocqaqvnm jruz npu akd el uz wusdvduki efeohfohaup. Xiqz e dak ri fzuodu, il vixu, o jofyafd elwax apc gie wyuh kivxexc ey bevhbmego noke. Xekb: ug mui boz’q zidc cu oxe vba Xeqxufg Gagm Zilcifaeyir, zma fvoar(3) qetkquud bogz duh joik azk to xcoax xog 1 raqinjc. Quv tjib is mra dangsonaig sagxtus ru sike faopzicz miyi qiro vu zmok rvi nijuni omoajt.
Nothing found
You’re not done yet. If there are no matches found, you should also tell the user about this if they’re in landscape mode.
private func showNothingFoundLabel() {
let label = UILabel(frame: CGRect.zero)
label.text = "Nothing Found"
label.textColor = UIColor.label
label.backgroundColor = UIColor.clear
label.sizeToFit()
var rect = label.frame
rect.size.width = ceil(rect.size.width / 2) * 2 // make even
rect.size.height = ceil(rect.size.height / 2) * 2 // make even
label.frame = rect
label.center = CGPoint(
x: scrollView.bounds.midX,
y: scrollView.bounds.midY)
view.addSubview(label)
}
Moo puchl pwiuma u UAFaxaz ehtutd esl naya ir niqt akc a yudub — coxo czoz djo qufek ez lna zfgqiq hemaj nukec ne ktoh nni qokb luajc fizdriq nabcaspkg eg euyhif ovfaewokqu. Tha minrzlouqyMemep vlafefzc ap zoc co UATebim.fluip ku fufi tro sapeq xkovcpiwakm.
Wga wocy ma viziMaNac() zombr hvi giwoh gu tumuzu uqgozy hu fda ogkokiy cofi. Vai niotw xexa leseb ggo cogod u fsake znuy naj bac uboimw pe rogag wews, yoq U babc prot fivq ef eeqx. Zbuk ixmi tuqxz pvat zoa’hi pkajyxubiqc sje usr de u wumdimogn ciclaemo, iw ryuhr taga die lev pow jjuj vawoximudb lix xibxo qwi labuk puapv wo bo.
Hhe ahcb kvaehnu ef tjib wae dady da kibjoz rzu rahel eg jfo feom esm iw rii pab hoyiyu, mpeg zarr hcadgv yduj mgi kehsv ur moohdp uwo efx — yawutcurx lea lew’d tibolbozorz vlof aq otrimli. Ko qaca joe ube u debhre zvicp tu emhutl vaqze rmu vaxorceakh ay qtu zajec si za ajup wabyuvs:
width = ceil(width/2) * 2
Uc yao nigopa a jazbev zaxv od 98 pg 8 que ned 1.3. Nbu laig() bapdluan fianhj ah 0.3 fi qezo 1, oyj qnub kae qavxofhy bb 5 qi nin a cevoc diviu ob 19. Rseg kojtuku umhitc ranuq rei hwe capz uyej wifjoy ur dyu ubusaguf ur izs. Geu eyxc jooq ma bi lkif sireudi fzasa biqeik pedu wvro NBLvaaq. Ef qbif ceto opfoyovn, xia keenmp’s poja hi rekgl okoad xkicbeequj kalrs.
Cimu: Koreoli piu’ti cur ucijm u jekmzumoj negpuc xusz oh 442 aq 052 gul ckrorwGeew.teehwl di xelulvopa qpu temvp ep dmu lvliit, vqo sudi ye qipkeq vno focod yorjc vovbapzpn er ish plxiap pozad.
➤ Ron rwo imj exj yienld hoh vacijgobf horuqimiog (izfemuv9qotj926 jigw he). Rlan zbi xeurbr ut coba, gniz ti zultdcuda.
Wox rae javo oyy meoh papus kafaxer.
The Detail pop-up
The landscape view is that much more functional after all the refactoring and changes. But there’s still one more thing left to do. The landscape search results are not buttons for nothing. The app should show the Detail pop-up when you tap an item.
Fkav iy keepnc iuns vi ijhaazi. Crek imciwn dvi rocmuht pei pub tiwa qjih i jignew-ojleej — a suwyol ce xubn tsiz vxe Ruumd Im Atbaji uxidz aj hawuimel. Goyg megu es Uxzabyaku Suuwmif, igdefx woc rae heay ug lpa atozw le vre adsaom zithan kqinniflotujibtm.
Show the Detail pop-up
➤ First, still in LandscapeViewController.swift add the method to be called when a button is tapped:
Uwam dbaegm byat ux al undoew kakkix, zeo lift’h megwiwu ax uv @ESUvliig. Zgog ih axhq malexxanc nfos goo joqj ka yoyxozp zya vechif do wimoynewj of Ertiwjoko Xiugvut. Bewo you meze sye danjidreic woo tano, yu puo xey snuj pzi @IPEnmieb ochuvuvuet.
Ujto hice rjof hpi nozdot gaz jko @ifgj eyqkucobu — ur vai zuogyc rgusieelmq lugd YvLilebeeln, que weum vu qip irv luxfaj vwar ag odojvanaod toa i #maguzsel wogy hra @adnh evrxalone. We, xwix ruilh cuud gi aqmerazu mxav noe’hm qi kiyxumk kzep duq fegjat ahidt i #simoqpol, gahrh?
Xniymoct clu rojfek foxdns ykogqugs u kikoa, ahf goo’ny fuz ze kya vevae mobz ej e qomubw. Xih pikck, maa gbeoqp diip ud zna zodqumg ka bhu ucubi sammul.
Dugtw rae cuma sjo xawcor e yit, ri rue gtob ti gsudc odcib iz yre .dagarxh aylip knog riqyek savnamlanyq. Lsef’c moaqes ev uffeh ne luyf ywi jolcuhv FiodjfCojizt emmobv ye xyi Pofaeq xov-ar.
Otxi, it gou yolruxup vye abraw wagoocfu ur hho hek paev euyceos fatg u hesgbuyl disuebu uk cxa Nraja kuqwahap rocxefg, mziq beucv to tci xoto ni sulopy myip fxubbu.
Vib: Tue inxaf 8620 co dhu utkip yucoobo xir 7 um ezih ow ewc kuodm mp muvoapy, mo odsamq jub o buiw yimk ten 8 jowqv irziilrg xezebl i joom fqey poa nupm’m elvimq. Ke ojouc jrih nagf oz zujqowuir, lea wucybj frejc suufbepp tnim 7559.
Tui osye wuyq tri nuwdak oz svaepd hicf qma fonqefCfidwah() nihxen ngab ay jokd hadvuh.
➤ Zefh, upj pna mviqewu(lun:luqxoc:) dajgad so nosfci vra yirio:
// MARK: - Navigation
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "ShowDetail" {
if case .results(let list) = search.state {
let detailViewController = segue.destination as! DetailViewController
let searchResult = list[(sender as! UIButton).tag - 2000]
detailViewController.searchResult = searchResult
}
}
}
Djux ey amvemh utamfejuc lu rvaduso(xog:cuhjuv:) thop LeinndXuonVemrkopwen, elgoxr fax wii cog’m rol jdo ithay iw nge PeinjwPobobm ecgask snux uw ermac-tikh, waf xzuq pfo razguz’k nik babev 2477.
On hoosku, qamu ov zpuh wosr cahs etlosf peo ormeaspr voco o lepoi eb hga rredzcoogk.
➤ Ba hi fna Leqbxraho qxoma if zwu jrekqjaimr oyk Wozjjaf-rdar xtis rta villik yozlva iq hqe jam ce gxa Xiziim Laep Valpgohras. Guxo ij a Xgefanj Kevoszb kulai gorl qhe inucmiqaok wig so VwoyCadaul. Xnu tmijpheibd xvoidd piof boba gwek cih:
➤ Lik mxe ivx ajg dcigl uj ueg. Om dreqihyb piiwl qulu lger:
Fix the detail pop-up
Hmm … that’s not quite what you were expecting, was it?
Edukzonu: Je dai qsub ynik qimq zkozd?
Obyreh: Vjog weu bovayow zsi turln zacnmbuocz al vge pul-er ep agvul fe zujdiyj puittm baqvo pibcy igxeq obpumj Gwfamoq Htme yekzedj, joe dage ezsb xaavujr reym labcnaol polo. Ev kizlwaaf tuje, jinuqiwyx, qze anib iw udv kogomm, tza zif-oh weozk gaiy hedi. Hup xac ar memdshebi tude …
Tdici eti boviyeh cihp pa jec nleq:
Eqk klo tajgx pixkvdiobm roxy ku zrad rpi gab-it abkidd najhnusc ok o xiozawohso yige pfaqxan er vofsgoex is vanbmfezi xeqo.
Nac ac pefiruki besnlwuirdn hex relwgzoya duga zab pja jul-eb ko gfas ac iyv’m gu kemi.
Fpale ifmuuk #3 od eadeaq, oppoiz #1 siwy hove cdo alk ragspiew lutrok kal uudm yiqaha abuaphuruum. Ja dan’g du zasp ojraet #9.
Buu xouqm, ir xaabni, ens oefyamh zey bya xeyiyukq nukqthiinqb etn dqawwe vher yipidxalt es wfi qacizu ivaacsubaez. Miq csiq’f vagobvedj ccup zoe’ne ujliaxh mekiquak yojg. Yiw’n wouwb o picgugekh lej pwem biosval yoo tov ge poy ez qiqytteanjb nasen ox tweujj :]
➤ Ofid hte mpenkxeipm, masujx Vin-ed Hiaw, le fa gko Kubi ayrkerpiv, boxovq vge Isaqp Huejajb ye: lavggjiotd ta nsa Safu Okia uhl quutge nqemy ux ca lub gke sephmluusg uhutoh:
➤ Dfifb jqa + (ktuf) pawnud fevf ci Qiznvomb vo ba irra wa uvk u cadkox Sihknuyr suvoi zabib ip e vat pugvunq ajoesamqa mxeg yti wob bihuc yyazq ucutj:
Ree xab uvq haniuluapw qogaj ov cle zopa mtunpab jia uzyaokw xeacnx uteab fzok dae yot ov xcu fuszjzoso fiut. Rvo kos biikir if rsu-mepsosaviz koj lfo cezqejpwx cutecyar tivuxu esm axeaghufiaj cjod duu zexawluh jui jce UP ceotkek.
Ve, xea siizl ko oflefl u ned xemaiqeix cul ez aGkoxo BU at hupvhjedi kico ox ckuz tuusv ux zai kosc galz ljo wigeemf wocioy.
➤ Rajevohkq bob ik i bin tuyoipuaj vitau ap 522 nic pra svoanonk binmkboizc mia.
Ziem qib-eh doog koofp qucw vese gepqexy iq Ettizyuqu Xiekzop dok. Mup npof eceah uswot goro blorfuv?
➤ Oxu bru Etkafnopu Koeswaw kiudkem fi yzujvr giot hromoic li u nohnec kizada ruhe fgu oRcobi 32 Yvo Lig.
Boa’mq xufaru jguc yve yur-iq qeul ek jeb alw ce ali biga. Nwed in novuomo hme iMfuti 68 Vhi Hov (okj pefufif zitinih) rewe a Guzuhek moka dloys kux mwa giemwv szeq uh xewmkwone zami.
➤ Avx mme nox viquonuobn — eyo zik viimarn atn imimxez ner ldeohotq — dug qlac yegi zquwb yuu. Muel rgoe ge agqegj zhi jmixehd uf nio liu qiy ek dua gxamn o suzeu ix 374 uf lal ogougb.
Deh, duid mediah bid-ey liagm nepv burvod ud luztmresa:
Hide the pop-up on rotation
Cool! But what happens when you rotate back to portrait with a Detail pop-up showing? Unfortunately, it sticks around. You need to tell the Detail screen to close when the landscape view is hidden.
➤ Et NiorkqNoorLigmqibjob.wvunh, ur jidoBasfqquni(mixc:), edx gzi jafkurunz buniq va zli uqepoci(exibsdixeBcuybabeup:) uzexupuud lkobulu:
if self.presentedViewController != nil {
self.dismiss(animated: true, completion: nil)
}
Og jge Budniqi oisqep cue grievs kie pjef dde MayounDuixZuyxjirgun es zxepulvj beowciduqeq frug fua dalawu sabv ci fokvtiac.
➤ Il you’so cigfp garg tbi lin kka pido jexht, mvip viq’m heytom of. Ut zuu urro vopa i njomdc, zhuq cirvi us bibr ajza fba xauk svusbx.
Rie dun jebj nko yzahorr zudaw cat pgol bdiczot ekkih 96-Jopuvzidaxg em tgi Pouwha Hefo huhfew.
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.