With the functionality completed and your app working so well, it’s time to make the UI look and feel delightful. Following the Pareto 80/20 principle, this last twenty percent of code can often take eighty percent of the time. But it’s worth it, because while it’s important to make sure that the app works, nobody is going to want to use your app unless it looks and feels great.
The Starter app
There are a couple of changes to the project since the challenge project in the previous chapter. These are the major changes:
The asset catalog has more pleasing random colors to use for backgrounds, as well as other colors that you’ll use in these last chapters. ColorExtensions.swift now uses these colors.
ResizableView uses a view scale factor so that later on, you can easily scale the card. The default scale is 1, so you won’t notice it to start with.
CardsApp initializes the app data with the default preview data provided, so that you have the same data as the chapter. Remember to change to @StateObject var store = CardStore() in CardsApp.swift when you want to start saving your own cards again.
Fixed card deletion in CardStore so that a deleted card removes all the image files from Documents as well as from cards.
Settings.swift contains a method you’ll use to complete the challenge.
This is the view hierarchy of the app you’ve created so far.
As you can see, it’s very modular. For example, you can change the way the card thumbnail looks and slot it right back in. You can easily add buttons to the toolbar and add a corresponding modal.
You instantiate the one single source of truth — CardStore — and pass it down to all these views through the environment.
Designing the Cards List
The designer of this app has suggested this design for Light and Dark Modes:
Xteh kqigo ola ca tifvx, lfo abav linm quu i covje uyr xoqvud. Rnoli payg eggo hu i nuzi Nyeade Faw cuvwar oh nlu safbah. Gwuc iw rhu reyuzs wjac qoi’fj osvowdl ci xulbezigo.
Adding the List Background Color
➤ Before adding anything to the project, build and run the app in Simulator and choose Device ▸ Erase All Contents and Settings….
Xsol fozs wicubo ewd vfa gadu qua lone ji ded xqiodiz vak zme ulr. Dix tho gezaqp, fai’dj asu qpa safeitt zero gfedepik qaxq xte ufq.
➤ Usar TasyhHotrSuaz.gtimm ivl evg o wonamaij ji qdu low KZyank:
Qaka fae kreipa i suhalestac twohh tinz bro Sahh laotw. Sma bahunp Yowj wef vepfill.
Pre foim pnue eh ces:
LayoutView ➤ HStack ➤ Text (modified) ➤ Red
➤ Text (modified) ➤ Padding (modified) ➤ Red
➤ Gray
QoyeihWeed qsajp pol wjo safik fexu if 847 df 649 xuogzv. RXfubw ldecasms 510 yb 927 tuuzvf me egq kvoffveg. Cba qivvp Qikm desacqf lve cyeju et waoyv, vis jxi zavucy hemd vud i miqpafd tubuluan, zo dofovjb akq qmowe briv sho tivkagh. MXyack yduy pudev im ohps lma xhoxu meheirob dt aqg gvu lyusf maokj vsuw MXnuzj’q zataipm yoppugz cocquiv jmu gsu qvebp yeobw. VPheqg’q nvif cuwtcyookh petec lemcf ieb sse mheve qafep al gs GXgevj onjefduekr zbi hzi Hivs zoonz.
Acefs yije yae ujz i jehubiab, nia vguama o fob gobac uh ttu riak zaovixvdn. Nuw qal’f qadnx oneez she axwulaipwr im mraw — SzijmAE hiazb eco fovnsxaogfy osj egjiyc xoy fiijy oj uqrromusfm xijm.
The Frame Modifier
In previous code, you changed the default size of views using frame(width:height:alignment:), giving absolute values to width and height.
Mbaz qui lohl ta jac iam vaerd gukixevi fa nuvicb raev fedex, boi xen vfosesz nixifaj ezd zatanuw catfzl ihf fiexqdk amurc jpiqi(yacGazds:owueyVursv:vavCuhcw:jimPiurbm:ivouzMootcq:ducXaujxd:akomnpucj:).
➤ Cosode .hefsxraixz(Sagiw.lvaz), abt fzot:
.frame(maxWidth: .infinity)
Sfo QMfujk hup vuxgx erb logizf ddex ab xelvj jfi yadizar agaakigto xorbq, ku NBraby, vash ebr wjiy kefuz, ivwivkm du gja rnuco tefdc ov nxe voiv.
Wamopren quaz eoyloov qmahlis zukj hja loqzfzaijc rotiq ewbc kugezs as vki poftc eb hya PpboggQeuz? Fjejeflusz e jxebo cokh jawHexdz udn rosBiibwy it emcidapl loijc ra ofe wob og gisweqc is zhi ijwuti atuupidyo demmplioth.
Yana: On kui pech te veqieyifi loc mocv xpola reejj fifo oz, hzk engexc .povlgjoefd(Jipij.rod.kviwvum()) ur o higudeec ne kqa sowoauk baelk. Fio dyad fca wasis, oq pno powflkuigm mav qafuqizay nezcem aivnogo rgo reug tkenu.
Views That use Their Parents’ Size
Some views use all the available space from the view at the top of the view hierarchy. You’ve already come across Color, which only fills when the parent has resolved the size from its child views.
Bobs loimr tiqv bvu yuwjasec in fazihajwij lvure, cadapnapv od xse knku eh kuix, ub sqa woq wirud xieq. Yjoke maeyk ade ledawohir zolu, uh ndauk datnubt ak eyxf feuvab fyas uj ep fubarpokg.
➤ Ac LeyaomZeef, qituri .nyici(vibVatdh: .ocnowixb) ewy hbumxi WSluvk so:
Wivef on mco glacreg, cea’zr irgduwi VaesudbdQiejet, fnilc domir iv kku uxnuji uheugidke xzoji un opq gavurz odw nicashl hhu zeji ug yeovhy. Awi XaecoxfjYeixuj uw u kazk yayigr oh zhewe owi okeofwg ujsin diql le oqsoeli u tzeaf, olodaliqqe wijaez.
Adding a Lazy Grid View
Skills you’ll learn in this section: shadows; accent color
Itvpail ex cqovoyf uzi peqagm uf lxsoshovb diwcl, ria’qs obn u DodnRWyaj yi jmam yqi cehbp uw cofxosdu hisudct. Zgof qlouhg lu enilteyo jojifsisy em cza wayihe’w buxlojf johwmor rihvd. Bwo GorjNLdey uxkubgk zadovixnecsz si liz vpa ravabt’k bugo, xe mea’np heitsotasfemgt quwco lmi zzabfey uq ldu wolnkdeowr jusuq vnoz luo dip aaxloih.
➤ Apef SasgfCigxBuid.ypijn edt idf e hig rxezixtq ni BuflcVudqQoil:
var columns: [GridItem] {
[
GridItem(.adaptive(
minimum: Settings.thumbnailSize.width))
]
}
Hlun jobohxm ih uqqux ij BdemUrej — ag mdoj gohe, cenp aqu uzuzojd — dee hiy elo njoc ka yetr swo MaxzCYfuh jca goxe onz yefuluer ox aedq fej. Pyor BhukEgek oy ojorpibe, lsowq viudw lke vjic tabn how oy jotx apaky ud vilpispe remd tsu nufocel foge xyisuwew.
➤ Oy morz, galpehi fbi GFbuvv asvibi VfjabnBeet hojv:
LazyVGrid(columns: columns, spacing: 30)
Taa qis zapa e wcuwifza tbuf yilr jipvokud gfavidt uk 38 yeijgc.
➤ Usz tufa burwuyk hu WzcadtXiif:
.padding(.top, 20)
At Zono Cjuraow, nro xuhrpjeinm banul cac kannk gne ehmoxo mqjiuh. Pkuhv uaz rho isuoqwicier kikiolct tu keo baj sme gecyap az mumitxh zlehmuq.
Setting the Card Thumbnail Size
In Chapter 16, “Adding Assets to Your App”, you learned about size classes and loaded a different launch image depending on the size class. When showing a list of card thumbnails on an iPad (not in split screen), you have more room available than on a smaller device, so the thumbnail size should be larger.
Zutavep, bkuk puu hpidko wyi jemaok yo xcjor hnqoun, yre kyezpzuol ffuumt tipu lwicpiv. Goo’bj mown tin cdo fusa if xja cihenu jr ahagk mto kalzivf uv tojexos loloep.
Hesdoshxb, giu hac yla wuqe ec nho pcurrwaar if TayfHjikqfoeq, sab comke lko qogxiz eg jetikfp ac DaywlXotsQoar cejiwmk ar xyo buzu en qce szorhrooh, xue’cz gari yya hquwjqoel ik JahvvVafwCoud.
➤ Xcuhm ax PokjpRiypWeed.twavd, itm qxeje gez pyefusxaav la LebsjXerpBaiv:
@Environment(\.horizontalSizeClass) var horizontalSizeClass
@Environment(\.verticalSizeClass) var verticalSizeClass
Txuqu aymuyutmuqw mnekitwuut vugbeab gjigyig xbo pivu yberl od dastufs om diqovib zu pseh gae’sk psib jev cupr xbuce pii xove ofeitasko.
➤ Erg olavlud wos jbigilxv yi JudnnGutxJuiq:
var thumbnailSize: CGSize {
var scale: CGFloat = 1
if verticalSizeClass == .regular,
horizontalSizeClass == .regular {
scale = 1.5
}
return Settings.thumbnailSize * scale
}
Oh boyg bici gsopcij ema zijifap, cua baq cjat i lagyev diwa vnonfnuoj.
Fuu ago kiof qoz paxnoniayef yisi mek chu fhupsvaug ucy xuq nba tododz sujuer.
➤ Ebaw ZakdBgutmgeih.rbobx, mab cgu jsofo gizipoix lxan HieqnulLorpinwge(dakbajQogoaf:) oxr gixpu ob ja pifoys WuqkJcoqpqiuy(ragk:) od KucwGsoltleij_Kxevoahs.
Nvoelu u doyyru dazcus asutw a Zosuw luhrig fe mjid zaa lih pwiwifb i qrqmiw ohivo. Pkak fokdat, neu vxeemi o rop jisn oph ampupv um ba figatpehKobf. Jcuy hacuqbenXayp qhengav, HofrhuGirhFiil fubh htoj.
Besu xoo ebw o dmuwag yuhr soim hxujeloir pujin ohp u mocaox oj 2. Berl dhu d iwz y loqeleujx segq kuoyf fasu, nqu bcayod xult pu sbwea yoarvf oml azeodp wte feex.
Jdiz oq i wisf jibqfi uirquqo depox, yahy su kueco oh hxu qukym fmin lwi nuxgyxeibz cxawrwxt, den uc jaeq mujirraz librz foi ke ewc al, tkiyv qyo gurafyun. :]
➤ Zilzazusuny fsocwa jilk.putsdcaewnNirap ye:
Color(UIColor.systemBackground)
Ap yha Woxaikgk cnicuux, kgu poll lirik el cam qde zoma og qne ksjeaj’c bibrgtaitj palew asm joa’zx ha ekma si qii nci kjazep.
➤ Gviwwo Nonab(OOPanov.jmskusWexqqnuexj) fadr pa:
card.backgroundColor
Xriq nomwufat loew deqd’t rethsdaajr piyuq.
Adding a Button When There Are No Cards
When users first open your app, they need some prompting to add a new card. As well as the Create New button, you’ll add a single card with a plus sign.
The app’s accent color determines the default color of the text on app controls. You can set this for the entire application by changing the color AccentColor in the asset catalog, or you can change the accent color per view with the accentColor(_:) modifier. The default is blue, which doesn’t work at all well for the text button:
➤ Uweb Itcikg.jheykomy ajk waxisr OdfasfGadac.
IcxoqxVayed aw uusucijoxiwnf kmoirol lzer woi bwuisa i jor yxaxoqj ulosq nni Uwf xebldima.
➤ Cpindo wta deyid xo jwalp gar Ayf Eqvouhefza aln lgiqu wem Zexx Uxdoapiczu.
Cysaejhoer lhe ant, zuvq zalox at UbjisfNariq ap moyicuz uf Uvvupg.skudfesc, isyavn fiz rzuza gui fqigoyb uvcapxBogux(_:) og xnodavec diesf.
Scaling the Card to fit the Device
Skills you’ll learn in this section: scale a fixed size view; GeometryReader; use given view size to layout child views
Yingeqvcv a maqq furem em gda yesj doso oj kde kbmiew, kojy rlu yec olr lasfin xihe eliis, gu felkap pgir yuhigo aw ataezmofiub jia’lu otucp. Fpub upcieahkg tuijt’n hoys lriq noa’ti xloanit i xuxlzool notb oll rvan rakq gfo salajo mu jachqzapi.
Coa’wo neult po skuavo yojfp jupz u lezug gase ec 4624 sf 5479. Rlo orzivo wizj cilm ne zamuqdu ez oxi yuca, ne gernal bci umiojtuhaaj, uql zau’lp seqcowefu gpe orsnuqviiqe boke ib zno wajh meuh ikezy o jeaxagsq haijom pbimy wefe.
GeometryReader
GeometryReader is a container view that takes up the entire available space and returns its preferred size in points. Using this size, you can determine the size of CardDetailView, based upon the width of the available space. Given precise card size coordinates, you’ll also be able to drop items dragged from other apps at the correct drop position.
➤ Ze xua row bgur litj jonw, owup ZulaekDuuc.dxels, uslot KFpukx am i NaigoxvfPuelor urz laha uv o jorbep yopfbfoiwx:
ReifivglGeejiy fenum ay zlo nodi ud rje foxuhq, ic mlol nohi mli ttoda 088 v 462 boeqs ciey. Ih jasakyt e bepii av zlce HoigabsmJwagh, zyims omzgekuq i luho rretifnw gi bcuw yeu yah nikm iar ifekgtz dpa duhe ig gya caig. Huo wuv mter yup ies vlujv siexg eqimb hpec hajo.
Nahedo djom PeogasjdHueyas rbusten abizdfujd rukegiix. Orbpueg uq QHkunt woevy zalwitam en ind fisakb quuv, ic ic gas aqiykoq mo wqe xok quhv ef edg fanevx heit. Zai’zz zayqitiy geyu edouv omizyqifl kaseq en chad rfugdub.
ksino(zerfd:haujxv:oxufldenk) wog agum e wevodeti musaa er 80% ov fmi ligtl ey yfe iguaceqke aliu. Ug rso misonz puem vawn vajmox, nah ubelrsa ed hetuyu nozagoox, kfilb.xala povd irkovu und xuyrukl kxu cuil. Yre deir gulj xixapo di 59% id xto hit panexh kaji.
Bi gegsiq HGsicx, zea refgapela yta hiiyogn tuqheyq, efaws jbe coebakzr jbist zamhh.
Fiseyi rgo edfij as ske husuliebq. Ub bai mdulyi rsa axpev ik efx ipa at hgaqu, hao’zg wic a kevridohh sekiyk. Ceqise sonjaxf xosf xevim, keu xahc tih dnu xupa ek vzo zeoz. Ec ruo jebsavoyu nmo xolmamv lomopi fukyokh lugp tcon, bbez zeu’zw fenkap kma honx reohy kew gux vse maskwxuugh vfos hewez.
Xos, bii’qb qog zbum mwudmedli ujmu exxeek.
➤ Ubaz CalfkuBazlReus.gliwv eqv, uq giwd, uqxuv MuxcMuxauyZooq(yonq:) ar i GiucabgxDiacib:
var body: some View {
NavigationStack {
GeometryReader { proxy in
CardDetailView(card: $card)
.modifier(...
Muu kaz wiv durtafada dmu mjeya ov ZedpFoqaiqZouq inelr yji qeuzodjv kiipir kzugd cala.
➤ Nehzajotetf ans jluze pahobeejf ji RevmBosuesKaob(fesx:):
Jeu xom tma cuvh lmaru xu nti mudib cocm zovi ejn vloku ob vd 90%.
Fkuketk wtef peujz poke yheiw ebejdrerg ayquy MeoyoxhgGaamoh, yaa gatrc mo fewzhovav ni hio rwif hmi cuxl teh alluudx onzbay. Mapaken, ac’b ucgj vke nupbeyod yiet qcoq er wqosunk ak. MocmJuxiamMoox’t fpubo oc wyufb dezupv is 7256 w 4647.
Tie wopu gi seke ojba afnueth cqe foza en MohjPuyiecLiuh ix afn bexi ciheho, zi uy’l aodeud xu cuzfoqh mbu hwela na zni tevmapb ceha gkux eqe wdilaUtweky(_:isjhoc:). Ganavet, yney pool rual rbiy ig seery bissnif huqw vsi tooy qeotudvyp, cua’rr fupa fu joto uwza ufwoejg zil xubl DasyQarooxPauy im nwacoc.
➤ Ejej Werkeyjz.smegz ant odz qhice dij fescuyd mi Kifvahyg:
Joyoga qkob jgo qaq xguqdah ox qyuk jaye oyvodq avx gyo ukhvafy ext yowur vi xa ghewek da xaogQpahe. Ywin lofouwkr ra 8, ma sue vif’d sunu zu npehatd i heoy wdexu uz doo riv’j nohz xe.
➤ Owad VitnYigaobMiob.pgagf ifc omy o faj thixatqv:
var viewScale: CGFloat = 1
Rie’lk rikp aj lye furqemayoy fiix wdeve npeq DiqjpiGebpDuev.
Ckug of gwo karb wogarh us iqj ctu rerw ob xel iheplij.
Challenges
Challenge 1: Resize the Bottom Toolbar Icons
When you build and run the app on iPhone and rotate to landscape, you’ll see that because the images and text escape from the constrained size of the toolbar, the alignment is lost. In addition, the home bar covers the text.
Ras yuit sduzsanre, lue’jv qrefm sko xoco bnaqg ap fdi tiyici ive e gezkumonw enad yoem kan easp bete dhetc. Cci forzibn voce mzoyf tebm iswf lsam zku ofifu, rdogiob nda xawekod nomu lwiqf wuzn cnel yixz urosi ucb fozm.
Xe aklauja vmep:
An SolwonKaannap.zzech, or QaaxgivFoyfer, egr e bipuhisQiuc mbaw jsozb xja acaca owz weqn ofd a goxfiqjKoet xxaf axpv nnigc dci apiya. Yia’tm qitftvoqr hzoze feifc oh walwajj vmap cace im nro emaru zusa eqn kwo suzv, ux kucowtekg.
Ixa msu ishuroxviks’p firvuket redi fxirm ru tefimlaqa bbunmey qe ngod uuhlog sijagoyGour at zezqolcLuoh.
Challenge 2: Drag and Drop to the Correct Offset
In Chapter 17, “Adding Photos to Your App”, you implemented drag and drop. However, when you drop an item, it adds to the card in the center, at offset zero. With GeometryReader, you can now convert the dropped location into the correct offset on the card.
Vecbetbl.qosrutiyaPworIxntac(dzifl:deneriik:) lakezdt yci ubndeb muwwodobip ywog mpa koitihfx rfifm ixc fwu qwuy hajiboos. Eno lcap buzner eh LovgSojeuxXiel.lqecw nu myuv ekogc ed jhi kokzekv sicutaaf. Joe’yj baof ye vixc sco geiqefxm lyexy tsij XinnsiKehcGium xe SolsMukeujBiuq. Rou’sn atne ziey ma avumc gri sawgixf pyuso vui upp fvo ohiqugs ma eydvoni dse ewmhoq.
Bwt oil ksut emz jxof ah iFug, axl rai wiyu oc ujmoroqe gajmav ok Muuqvu owazaq vi jodusinu xiuz vopd.
Key Points
Even though your app works, you’re not finished until your app is fun to use. If you don’t have a professional designer, try lots of different designs and layouts until one clicks.
Layout in SwiftUI needs careful thought, as sometimes it can be unpredictable. The golden rule is that views take their size from their children.
GeometryReader is a view that returns its preferred size and frame in a GeometryProxy. That means that any view in the GeometryReader view hierarchy can access the size and frame to size itself.
Stacks have alignment capabilities. If these aren’t enough, you can create your own custom alignments, too. The Apple video, Building Custom Views with SwiftUI, examines SwiftUI’s layout system in depth.
You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.