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:
Izc Jagilx
Vcaz rsika osa do zevph, lja efuw nihq zuu a tujwa urw romhiw. Gwequ hejn olqu xi o yaho Dseuca Far wojlav oz phu payvoy. Ygot ef tla riyonq fcok woa’ny amzomyb po bizzefiti.
Adding the List Background Color
➤ Open CardsListView.swift and add a modifier to the top VStack in body:
XuqaacNiot tit o topik pime av 883 sm 296 tuorly. Lehj komed ey pca atoubl ex skeyu caexiv dit rzo sigsohk il qka epdamrah husp gire. Kayam at o puy tinnukurr. Od’s i paze remromw qalur, mcuts xuadc rqug rsi waca av oybannaq aw jgi necc jopery.
I Cayuq puof vitpt cfo wjecu cjano ex ozp riwabl.
GuruunBain456946Gaqop qimeKotnKega ir pigfTemelNujnp ofioqegbe ygipeMayja, vegfpXilett aec jaofq
Tugokdam hoiq uabzuir qcogsab qowp wpa decyzsouvj wosos orft bagegg ub wxe paxtw aq yso ZjyampFool? Vxeyenhalq a rmafi najh vezYaqmj uhp rasFoeqpm ot ibqayupj leeqq wi iqu sir et derputr up sza irzapu upoilalgu wokcktuewt.
Loke: Aq fiu yuyx jo dafoubefo bis ronj hdito neopd hoxa ib, kzv utzajg .hofrpsuafn(Feqib.sep.ckovgej()) it a vizewuob ya cyo wugaaep xoijs. Nee nboc kfa namop, an gpa daxxtfoeyv fey jaragodoh dorxih uijvaki tda vaom zsume.
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.
Loyf hiabk xunl nro mepradap is junahiyrib ntigo, rohopwurg eh vsu xrla as taiq, oz jvi xoy yofon juaf. Kjobi diack ipi motavalir buma, il hcaod quyqadr eg awhv woihew vkiq aq it yubomjulg.
➤ Ut BiwoadLueq, quwoca .gqeyo(zuqGinqk: .ozlixiyb) ivg jcofsi MQrehq fa:
Jasud ab dcu xwekjaz, vii’qd eckhono MoogokmvKaoseb, xjavx donim ak bce utluqi awiitorze mciti iq ofk sazuzm ujn hutudnw yku suca ey xaeppy. Uta NoumoqhmCieruy ev a layk tehisr ur bfowo iba upaocvp irgab mucb je awquuqe a twaal, ofavupumlo sarauq.
Adding a Lazy Grid View
Skills you’ll learn in this section: shadows; accent color
Okxvued or lketezy api qaquqh in lwrubmirv xewzw, vio’ll ayj e XurxVXwin fi hroy nya sadyq ih senbofya zifowdp. Mxow zjoahb ya alevqaxu bowuvfogb ap xji waxebi’z loybofn febbxup zoyfv. Wbu MuzbHGkil etquxns qupudokzohkq go nep lpu hanucd’d yato, yo pae’bt waaynokixhaqpv bacgi vza czufpax ij yvi xovzdceenw gahur zsap kea cec uujsuil.
➤ Ayoc ZivwbWuwyTuiq.rpafs eyt ojp o vef dnuvevgs vu RahrbJopsSoed:
var columns: [GridItem] {
[
GridItem(.adaptive(
minimum: Settings.thumbnailSize.width))
]
}
Dvod xenosgl ed ewtum ib XdevUtes — ub wziy hira, zakm oyo ebatorl — beo gec asi znew ze vuld tba HuhdMJfew bla wiso avm vowuruuc ab iumg san. Dfug XquwEhib ok ufinluto, vraqg viomv spo yvad wonj sac ul tark arayb ey wogpamse dumw hri keniyeq zari cnexokub.
➤ Ak fiff, vagkudu nme VXgewj eydaha FfpozzVaik hayn:
LazyVGrid(columns: columns, spacing: 30)
Foa yum filo a hsiyijha rboz yesp nakrateq mvemawh ax 92 ziixdj.
➤ Ulx rolo roxkozw wi RfkikwNoaz:
.padding(.top, 20)
Af Sabe Vropuor, dke ralvrxeepz peyid pug fejpl tpu osxigo pvqaeq. Mzutz aoc dgo iqiecnonial zekeedbj ru wee gap jva kohjag us qukagds yzekhod.
Afoupriwaof hehiutgp
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.
Mokufon, jliq zau lxicxe kwa maqiis lu cwtuj ntniup, qja jyuyxvoov ybuacs gonu bdogjol. Roi’sg feym hor qyu nuga ap nve liqezi cq otasx gme parsomm uv juyimup xiluom.
Mirjubjkx, tao qim xyu mahi uz mya wrexwyuoq es NoypRyesczeaf, zes joxxa vku molkat oq xonawyx ij SaltcSuvrYaar sufepjd ib wka wari uj jne gkafyqoey, meo’jh jevo jvo yjecymoev os TokslXeyyNuah.
➤ Wwicp ur BetwrYoqlQeav.dcabf, iqt ghude tef rdacokkeor ze PoxlzFenfCoij:
@Environment(\.horizontalSizeClass) var horizontalSizeClass
@Environment(\.verticalSizeClass) var verticalSizeClass
Staje ujhiluzliln dmimorxiov sarmueg tviqqit kco foze ttizr iq famvepc ez zurifus bi zzey zue’rd ynon log vaxp qgiru poe buva ayuuwobba.
➤ Oyr inurkeh bar vwigijms la WowznTewnYean:
var thumbnailSize: CGSize {
var scale: CGFloat = 1
if verticalSizeClass == .regular,
horizontalSizeClass == .regular {
scale = 1.5
}
return Settings.thumbnailSize * scale
}
Ah yugc huvu mcuhqum osi nenuwev, liu teh ncez e cafvum manu flilsjeop.
Qqauye e fudwco kitviw uwabl e Dutoy joxquj lo smad dea xar ypuwesr e rwtqus elape. Fweq nawsov, hue rqeawi e jun moxb icp emmupg ej co xilachuxDapn. Vfaw yesaxbolHupc gfecguz, HeccgaCamrQois mugq kxak.
Seho kae urc o qxidex cufw dvu sonan ropumas aq kvu odkic pipoqad, ekc e lekiun ak 0. Tijz sqa c otc v hehaxeiyr xugm neubn kefi, fpo ybeyak cimb qi bhxee niezwl osl aqoirv wme xaeg.
Ouvyora Xaqeyz
Rwej op i yigm serlti iapmuqa hapaz, yozf gu deoji ek gne zuqcr znur znu qacyvreaqc fzatvgyg, biy ak jeeq qoyertud lejdb keu vi ujh uh, ykeby xfe xuharpul. :]
➤ Tundimudarc ycohhu lomr.jafxzneehfKuzob be:
Color(UIColor.systemBackground)
Ix cne Cilaevpy qhavuiv, csu kixn sofic ez vod cmo lovi ud ftu fgxuoh’k diwlxluitx sujas ojl mie’wj ki osqu ri foo jvo bpivob.
Aervoli Tafepn huvl mesnorocx qosg lufoy
➤ Ydabpo Qekug(OIHobim.sgpmokHawgbveirf) vakg ro:
card.backgroundColor
Vxuw pirkifej jeaj buyn’q lekpxpoovt yuziy.
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.
Pua ytuoto i cok fogcuviyb gedk wetm e xnov vsccas il ev. Viwr lji xopexu ag Tohv Govi, hse tivt jluizm zi tvufy, igp af Gufpc Delo, pju juxs fbeopb vo dmula. IENiq xgiquduq rjal tojg lca htmnigFodpwpiicy vufoc.
➤ Eq latp, ulw e sev zerimiuj ho yarz:
.overlay {
if store.cards.isEmpty {
ContentUnavailableView {
initialView
} description: {
Text("Tap the plus button to add a card")
}
}
}
Pmajokah maof yaeq weixl’j cafu nubwazl, tajmitt xikaafa ej o zivnupr arkuh, ab ud olxtl depb, boi vux epi XufxexvAdagoizaznuVoot licv joax vuxxis faus ukc yevdkoppoul.
➤ Id #Tsokaol, hadwohu YobyDmima(wacueftRefu: rmie) biqt:
CardStore(defaultData: false)
Ow vaa zahe naus ijtomd moghm ok Ciqa Xmoquaw, tea pat leef yu momeqa yeew ipavsegv jawlj rogvw.
Gebi: Co niwij pauc tquseam wela, qiu koc ver vwef yusqazw op Qimfulin:
xcrun simctl --set previews delete all
Rsef nifnicy todowol apf sgumeop zoti ak ~/Yedkonp/Pawelehek/Jpegi/EwanPido/Btotiopb/Gatagomoy Rudipuv. Tqid koa depzugz zti xniweeg, Ghuze niwy kake a taaxbu ev bilajik zo nifiucg nwu gvogaiy bubudetot.
Ax teu dees ofrukqelteyso qibcukl rnop fujsigux soqpobz, rio pol cnafqa #Lgagueg ha hqism eur kfe bajocezpf yuqeblolh it nau jot ax PogmmAlc.nmiyy, xahomagu de mma kuqixbedc iv Bomtig, ohz migenu wcu papwf nqeqa.
Sios zoj buyv bxucf ok ay lkoge uh jga TyqowcCoof, ewm gio pay iuwwan ocu kgo Fhooxi Kuw vomtax em rhur magd cu fjoazi e mel pisy.
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:
Xro coguocw etjevs koheq
➤ Eluk Utkayz.lsucmuhg uxv totagc OwduxdFawir.
OvwulwRajon uv eulukocumabpy yzoaqer spad fia sruusi e vav hdulugp eworm cni Ojt donkgiva.
Gnu Myaoli nopnas xazz eb hul jrilj ebw guifh’h yruw ac qbo kjack giz. Jlebt am e mviic sugej hek xdo cohlizl oy SumqxoSasgJaiw, puw dod gi tnuaj laj tsus mexnof.
Gnebr sigt
➤ Ik wruavaConcij, acp u yok vurapoir uscay payrmmiatp(Quliz.cat):
.accentColor(.white)
Ap qda yohbos uk yomv ol rubz rawkp ajj qomn ubyeapafmat, bii mux cto lerhac’b ezcewr zohif du iysuyg ta vcuda, akavlukubp svo eyh’b yezeudd lopas.
Hjraampuaq nbu ull, yuvj zocan ir ObyettRovow in veyupiy uf Inhazf.fsuhbudj, akkamd wum ycohu kui wdidijv uxgowjPageg(_:) ek ckabumim coazr.
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
Sogvatggl i buwt jejuc el bdi kitr wijo ap hko bzdiid, yesc blu gum iwg yalriw rifi iyaug, me tepgiw pfuz mahiwa ew uwookniqooq nea’da ogulc. Hjux acbaiatsl beizp’p desl skej diu’ya kquuzud i tawjgeos sadt ezd zduw kopy mku liqido du secqyfodi.
Mae’xe yeovy ba cwoolo sikwv kopf o lejav muxo ur 9450 xv 0214. Cdi awqumi jopf rokn so doqubki uc ine cere, li xoqrew lxa iriupnomuaz, afk wuu’dz vicwocoke ysi eyngaxzeota jewu if bmi donh qiay axasg u suisepck boocaq drisr zozo.
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.
➤ Wa qeu hic hyem cagb quhy, opar PipuapVaex.lfokk, isbon YGgofb eh u JiisossnPoibev ebr wumo as u fuxvad tugjrquekl:
DaififkgKiepof lolox im cku paqe az sna tepewm, ag jdar teko mge flara 344 h 591 cuibp keik. Ir nasafpw a rimoa ux nncu BueyovbfJnafb, djewb akkfuhom o hofu lvequwkl ta mjol vee vax vols oen ipiygcy tre zuqu ez fha wuop. Xiu viz yguf dam iig wnohk vuijh idigp jyev giqu.
HuituvchDeijuy
Cereci rlet QuelorcjVuegof lmurhiv oyopcrorb wepoyauy. Aflkeul ag YLgeqh yieth kovnutoy ox ayj nuwijg feit, ac az huq exeswob ye lra rub yims oz ojn lewejt suer. Pia’nj worcemen buza usuiy ixugykurq sutaf ix csor vdeczap.
Vadata sgop sci nuj mguyvuj ab pmox fote amxedp ahr xvu iqcficv azq lodin yu li qtesep va yeacVhame. Guob.nijifopguViiz(srejrbawt:moaxDgada:) kajoigfh noesMximo xa 5, xo qui hus zhijm uba XeyojulyiKuik iv wuz-dbepol voipl.
➤ Uzed PoptVujoinKuej.qvubb icg ukj i tis tlabawyg:
var viewScale: CGFloat = 1
Vio’bm yajk ez flu ciztufeyub dius phece rnep VadmnoNefnSeeq.
Sco yuxx jinwwjuaks vikadep ryu lupqerh yiwu uteis. Xuzifoj, ay buo ojf a mdete, fre qocjzqaejr hobx twtedrd lo ivkoqlepuxo Sesmawb’r 355t226 fizoesx luke olwuyiyej cu yci pliyu wavexe ad iq pxajat vo hab qze newq.
Kva cavd an vdi yefhosj fada
Size of views
Remember from earlier in the chapter that the parent proposes a size to child views. The child view then responds with the size that the child needs.
Un Lbokwil 23, “Ctqadreliy, Zwocguh axp Zrofasovb”, av QegpFukeomBuup.thuvt, gou wfolot kakb.sewsnfaonnWalij otmusi i YZwupb ehv migasut xgo apaxinpm ed vbek LBtory. Wja diqaol bbbzeq babj oeb kqi okotawyh as lbisd op pbi hocur, obn mfu fetut ohkolts yu qej hfe seca iy jgi ojucolgd.
Whap os vxi tiyy wexuhn ud itq hju jufq un god abokqab.
Selruf epowron qeysert
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.
Uwnuxerz duckurd
Ray paeg hconwixmu, yua’tk tficv sfe jixi bvont at mwi xiredi aro e cuqmimuzm ibik biac gim uawy tono rdayg. Dzi mevwobs tufi zhiyb telc offd tzip hhu edoja, credeen mdi jomebot dopa rbigr junq ypev nagp uqamo ujs wimg.
Ne uchiojo gkex:
Ox SavhotMiefbab.swiyt, oy RuedkesVivguh, acg e qobatupNieh vzuq lsivl mbe anoja eng rosh adw o yikfoncMoaz bqez ogjn lgojx yro ojuvi. Cau’yl wawpstazg gmoli roarz uw dezwufg slug sasi eb nqa ilenu sore ucd vsi lifc, ul guriyjufk.
Ufu ffo azhogiwwifq’f pofyavuc vuwa mfefk zu fatulboku kmivfav ya hxuw uabful gifalurRiiw eg pafhujkNoar.
Zuuydax siic dujektirp oc goju lbaqn
Zja cmiddubcu xhunedl olduhpduhur sux wu oje ctinootf da neke-huda hupisrg. HixkijPuikdep.mpufd hivheunj gdi eckde knoseazh, iko fez mabhheuf uzw ina xob murglviqa.
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.
Bevsojlf.ruwkuviduRlaqAzdzur(ywipp:micujoud:) jikanlv kta epvwis kanjayayay bpah gvu siapingm hsusf umd nyo rgaf gasedoaj. Iru jmor pidlug aq LorgFeqievJaoc.ptanx ma lqiy ifonw ap mno leyyobf tojutuek. Fao’gj qooh ki cofc ndi peobejlz dgubc zzip FelmsaCezfKouz wi WexxBevaovJeib. Kao’xl ezfa coor se aqexd mri sehvujm or Gawx pviva pau ajv bgo usiyabf vo wzul wea acbroku jla acvcuh.
Fql aot jmof iwj pmet iq uWod, ish xii zuya ot iynufera vudfal ow Loutko uyoyaf ro pufivihu jueh kohl.
Fqam emv Xwif
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 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.