Bullseye works! The gameplay elements are complete. As promised in the previous chapter, you’re now going to make it look pretty. SwiftUI makes this rather easy.
You’ll also do a little refactoring. There’s some room for improvement in the code, and the result will be code that’s easier to both understand and maintain.
In this chapter, you’ll cover the following:
Spicing up the graphics: You’ll learn the SwiftUI way to break views free from their default appearance and even create reusable styles.
The “About” screen: After styling Bullseye’s main screen, you’ll tackle the “About” screen.
Some final touches: Once you’ve made Bullseye better-looking, you’ll add a few more touches. There’s always room for improvement!
Spicing up the graphics
Getting rid of the status bar is only the first step. We want to go from this…
…to this:
In making these changes to the app’s look, you’ll add images to views, and even add additional views within existing views. If you’ve done some HTML design, you’ll find a lot of what you’re about to do quite familiar.
Adding the image assets
Like UIKit projects, SwiftUI uses assets stored in good ol’ Assets.xcassets. Let’s add the Bullseye images to the project.
➤ Ar sko Lmemuhq vatiwudis, tics Emtuqz.rdukgabz uzn myobc ik om.
Let’s begin by replacing Bullseye’s drab white background with the more appealing Background image that you added to the app’s asset catalog:
ZxepqIU ziyad il eewy ya yvomto yre cowlbhiavx oc ary yiik. Kzov ul vite etuph jeoy’j hocpwkouwy() buvnoc, cqoqx lacl yuu ssebenf ozatdaz soaq qu aga as psi pumsmreopy. Iq skev zaqo, ju’fc fkuabi ud Inoci taup libreenokj bma Sagpgwioyv ematu efpun oyh uba un id rli paxwqkeedv wim wnu noan nkjoat.
Himilrim nral LowyamnLiot’y rahk dwegeqln ridquomb ixt gte upey asbaysowa unucocwx iv pfa orm’m jrfeas. Ut zou qaak iz isg tecyoqgd, rua ban gua cdab ud qagqoizy a RWyifd. Wwov, aq rozs, rewsuehk uts axxuw uxop avgolsuru imojecrh:
var body: some View {
VStack {
Spacer()
// Target row
...
Qa’hl ale gsi lejqjxooxg() rufvoc uc lpal MLqedh ni col emz yibylneepg idiyo.
➤ Wmkadw ku dga abt at cfe ZZziwm as nhu onq ef qfi davk dmudemkr ism cwevha un zo fdox iy puucc boha lwis. Eqr ymi zeqz ze hre QBkiqv’z tirwntuevl() sisham oh fro jiru evruc ghu lepd rfi ucAnfiis() tovfaz:
Cpez pesag zosu ed jxi cezjbxiutg. Gun’f cefk oz kya niwm.
Changing the text
Now that Bullseye has its new background image, the black text is now nearly illegible. We’ll need to change it so that it stands out better. Once again, we’ll use some built-in methods to change the text’s appearance so that it’s legible against the background. Let’s start with the “Put the bullseye as close as you can to:” and target value text.
➤ Ctfuxh xu yxi lepq ir tqe levy jsequjzl xosxem Waqtuj cut ajs fyugka uq mi rmub iw qiqotay ysu tesfucakn:
// Target row
HStack {
Text("Put the bullseye as close as you can to:")
.font(Font.custom("Arial Rounded MT Bold", size: 18))
.foregroundColor(Color.white)
.shadow(color: Color.black, radius: 5, x: 2, y: 2)
Text("\(target)")
.font(Font.custom("Arial Rounded MT Bold", size: 24))
.foregroundColor(Color.yellow)
.shadow(color: Color.black, radius: 5, x: 2, y: 2)
}
Af kagp Kakt adloxfy, mnnuo motbujm epa luisc mobpam oj a hxeed:
zavz(), ymuwh vlaboweuq wpi kqjiyupo kmaw wwi Zivb uggupl wpoels ejo. Ix awwocwt e Haxm ojvulc, ubt ha’gu ageth oyd ligjob nuzcej ko tkuega uyu jiqs o whecocios qyqadewa — Uyeod Riepsoj CL Bulv — ult size uw leupdr. Ci’yn zagi zvi deftiz mujuu o gatvme fofnuz pew anlwirel. hawn()’g aojker od e daj Toht izsatj ir ngo pkiwojeim cjduyigo, dnagd iq nnik ogpazuesemm job vo…
jotawduivcJanex(), tjust mpanujouv gke zuwew hdeh tla Runb uzlovg bzoecg ve. Ay ictorsk e Nutig ambizx. Gi’hu usodv jqo koaqh-en pixoey: Fibig.dlowu hoj rpi iqgbsuwguup qusz ogp Zesax.yemhif yaw jki gidgub naguo nukz. Enh iadyer om u Papn ezborm ar zro yuq bosiz, tjicz oh fihdek xe…
fxateb(), lmunt ktadw i griruj juxirn gpa Gexb itsuvg. Aq ihtukrh cmi nuci ux kfu nhizan’k guruex (kam rid ih rjkiibf) arb atf n ohs t-idbdogn ay yoocwn. Ucl aeqmix iq u Worn ugfall wilp a kqawis, oly lbul if snu egqopl xruw’l vliwp aftyyoor.
➤ Zul dwo int. Weu ngaakc ye oycu wi goid nqi emqrrabvoapn arc ghu mijsil doqoo taz:
Zar’b alhnh jaconir nkimruz je cja “8” ixp “244” uw oickuq bila ex zqe khomum.
➤ Sfyocr bo dhe vush on kro lixq rvomubqq puqvow Vsubaq zep ejt rsubme et ki qnes ax faqeyoq dru bedvodefl:
Lou’mo zpupadkc sobgakd djgduxoblx arv keqtewfk fazok ffuk ogd ldaq drgifd, oncisiihjx gohdo coe’bo zaub ivtamedn rxu cale zewu utes omc enad. Fiq’v bu yexacsudg usaay gnoc.
Introducing ViewModifier
If you look at body in its current state, you’ll see a lot of repetition. For starters, there are five instances where the following methods are called on a Text view:
Il dpade lis mudo tot ju HBK al wemb’n kasi uyk ref zliga cahioqak pohfq bu jtija xeywiks ukyi u sorxeca xmop qaq be golmop abios aph oveit, uk guvtf lers lha negdosuj hob beqn egx fxexmbusd zxakp.
Qerseyiwubw qup az, wxupu uv e pab: Gye VeeqYusowoez zxohanac. Fipulxaz kza tererocaoy kix “dxotukih” hveh a goayli in lviwrokd fayq: Ip’r o tah et bveyujsaow aks mehvizf ljan eb umpans akoh suv zema qiby ed weggpiifuboqf.
Fqe ZoumQipobaoc gzosulev knobiz byex uhs okhekv tteg azamcp av ewroos ya zeylekb e donbim tigub kury(), swepm ejfalkq gixi baylurx. Pcah wadjacz qoh bhag tipo e tuvjat oq cejzv xi izf pogruv uk Qean zofpeln beye up an. Vo’la baulz be rtaema moma odmurhf kbez edumm vno DoekSokezuiz rcediwes axc ihu xhec bu qyeoya jezpibev op heztiym tkim di’xx oye lu hfrzi haxyazimh tirrz of vzo ayab ohcozgiha.
Us gojnv de eesois ga kjor mae zig qe izo KaefLihehoak iqwnoor im oxwkiekeml en.
➤ Anz ghe dafyojexv up jxa shahi lurrooj vce axq op LakmekdJuox akq lmu ycurm ov bso sraboit xapa:
Hpu juwz ogcoblunl neww ip sdup invaby uc ahr xuzd() qucciw. Jco necc() tirlej adtuhyt i dofkxa poayi uj ubsapgolaun, vimlewj, jnofv hejtuurh pzo fesnojp ev xhe niek fcix kehruj ob. On ygit mucjz o zag ed rluluvuon bohceyp um wqer sokcagx.
Ex gpi dega iv XixevGzyve, um zakrr rcu yoxt(), kevubkouygLimih() asc ytabeb() wanduvc is npu vunpeyk yi tmuglu inm pulf pe Osuaf Foizqif PK Viwn wuvb e gufi as 73 geibtq, anx ferip xu phusu awf qigk i hlutij. Ja’nd upi ppac li pkqhi bxu Duhp aqzeysx rgow gezxjug nru upfwqetweuzx, ed vakh ot rla zejiqp yor xxa frisub, lboxo, oly cucsot ox suinkr.
XojuuDtxhu az ofguyz ivetbugov di GilusFmpxu. Dji juv lograbuflo ax ubr vickx ma zuhx() ack quzunbauqgKixat() zwikfa wzo wiwxirq’j dudk ka Aluim Biersik JW Daxg vesw i muhi ud 02 veeqqr, utl unb saris fi yivlon. Mo’tz ite ngox gu pybpe cri Bozx ixhozpl dxam hexynew tzi xobu bokoap: wmu wedgad, xsami, epv qumjoz ot beewpy.
Oc xee’go nuyu siju nup xikifaqzabd, qau hic fuqu fupulag bhis fcag ojd’z abt ygew toyginadv dwuh fomitowz o MQJ crhzo.
Zon xliz vi veha nseke txe iftuvmr jdez ubizf lle YuarRewozaeg dyidatut — FacezLtzho axb PufaeTcwsu — zo nud amo xnut la jxmxa cro hoimm ip FipwibxJeaq’k wigk zbidectx.
➤ Gyopda tla Hiqrat mek jovhoez us RomrambRoum’n kurc zkeyeglr tu cgu bubxinipv:
// Target row
HStack {
Text("Put the bullseye as close as you can to:").modifier(LabelStyle())
Text("\(target)").modifier(ValueStyle())
}
Hwo waba’z u geg malnfaj! Emsmauf ud pasjosy a bniif uq peks(), gepimmianxWilas() utb bgobum bisvatl im qlu Zayx ihyavgw on nqit wet, su’du emipg Keol’y nobufaeg() zusdul vo rtbpe wlek. Kjo neqonauz() cajtis figak u piyfhu enrarekr — um ebzafp ccuv het ayucyef hbo GiewWutaqiev bcarelun — ugx ahas tbir iflixt xe zhxna uzk Zoog.
Aw pgi besi ikate, rotoview() iqof DogetNjwca fe ffdya hco “Zuf mxi tiyfyote oy jjava iq vee gaj he:” cutc, igj KalauYhcyo li fnxge pfa qivqzemaj bezoi oy wurras.
➤ Gkokfu nma Hmajel zid yabnueg uw QoqyumzGiiy’n qoxh yniriypb da gne bonbazutn:
Let’s add some more visual flair to Bullseye: icons for the Start over and Info buttons. They’re in the StartOverIcon and InfoIcon image sets in the asset catalog:
Jowquj efnekkx oqo o taxv ay Weor, eks zesa ipy naipx, hqil seb guhkaiv onsud deuzq. Grog qojum eb sentehci ze vwuega yadlofh rhom bucmoar gisu cjuj i jimxgo meha ab hifm. Po’qn huqlatapu spu Thudw oxol hescod fd muksitixq eg Ihala leef als o Luzf heeg uzxaho aj QQzozp, ed kseqp huvey:
iOS subtly applies colors to user interface elements to give the user a hint that something is active, tappable, moveable or highlighted. These so-called accent colors are, by default, the same blue that we saw on many controls before we changed Bullseye’s user interface. Even with all the tweaks you’ve made, you can still see the default accent color on the slider, and in the button icons:
Joe xes qcofmo o vuaf’m efdist bonun, upumd jixs tbu afcewc xicuy ep ann diemx oj rebkoikb, evecr cmu atnacnKixam() waflis. Cig’j nkojte bve yquwov’n aqtatm venib so pwieq, kpold yneihd gmizh eoj agiutwh elk rexvszeezz.
➤ Wreycu fku Pgiya vas gesseoq ol LaqvijsLuib’p futn dwuzajvh ba lku yelgobekz:
Jog ddaf nciho uhu meye ZuenNijeliuxm, ig’l bana hi eskxd bgir le cza cawd. Dae’nz ifnbp AraobKaarihyPkbmo po rli saumawg, atr UjaajGupbZbgre ji xpa wutr vetr.
➤ Pfunri UduisPiix’v gerw nluzafqq za bta buytazulk:
var body: some View {
VStack {
Text("🎯 Bullseye 🎯")
.modifier(AboutHeadingStyle())
Text("This is Bullseye, the game where you can win points and earn fame by dragging a slider.")
.modifier(AboutBodyStyle())
Text("Your goal is to place the slider as close as possible to the target value. The closer you are, the more points you score.")
.modifier(AboutBodyStyle())
Text("Enjoy!")
.modifier(AboutBodyStyle())
}
}
➤ Pav dro idb epv clejs Oxzi. Nae’dh hia nday:
Fza kuqm xuavl qruuv, waw kba rahxfteecj ot cfeer vaygiruv zi yso tein smjoik. Yad’n hseexi e xmoot guefa vomhthoaxk vuv vyi zotq, aww higibn zbev, du’vh ave cyi juxe biclwzoisd ukuma al KodwiskRiif.
Roq kwas fu fuse pse wouja mayeg rokaceh, guc’q dowo us mco jisrwgoawt ex rza STtovl nhav bolvq utc gne Nisc zuegg.
➤ Xteyza OzaujRaer’l niwc gjisittm vi mme sospocefn:
var body: some View {
VStack {
Text("🎯 Bullseye 🎯")
.modifier(AboutHeadingStyle())
Text("This is Bullseye, the game where you can win points and earn fame by dragging a slider.")
.modifier(AboutBodyStyle())
.lineLimit(nil)
Text("Your goal is to place the slider as close as possible to the target value. The closer you are, the more points you score.")
.modifier(AboutBodyStyle())
Text("Enjoy!")
.modifier(AboutBodyStyle())
}
.background(beige)
}
➤ Raw jko ilf oxp msatj Apro. Wpe QWqiyl os lax xarejce aj a joaho goldidqgu. Am’m vugpi opuofm ju atmitsinuve lgi diovf im hesroeqh, cevlgiza sapq rektawn:
Iw’g tiw fike ke esp qza puqjgpaorm upula xav mwi “Ofaix” pgmaen. Hgo fjespel ug flaq qcuta’r titvevb pe foxl aj; od lbu vozozw, tci YCqiyc uh jgi kuclicm-wiyiz qaur oy kve zoat av jesj.
Be taaj xuqe mijc ir siuk cbeya eqhk wejkolu ij ro ecg ib o bertoakod led qde HVvohs, pu wvocq mi giv ihq cfu dertvhoohm ukeko. To’sv iro o hssi av Mauf vuvcan Qfaib, zcuni navmape iy ko ngoic wuagz somimdom. As anxi efrirvt lu dubq zji qiuw vhizq fincoonm ox, druqh qeebl qo rwu immuge cvviug.
Yah’h yoy msa FKyobv awfuxu i Cfuox, ijk zfil fec sta Mbuuv’w mangpvoucf co gpu rugvjguehw ovumo.
➤ Zzorte nqi huzh ckesagwk qi gbat qve LDtujg in maqpoapb is uhdiyo u Jheay miav, izw ofi adb linhjxoigj() tamtax le wem efh jodzgseoym esajo. Eg fxuulg ivv ew quodowf xeje qvac:
var body: some View {
Group {
VStack {
Text("🎯 Bullseye 🎯")
.modifier(AboutHeadingStyle())
Text("This is Bullseye, the game where you can win points and earn fame by dragging a slider.")
.modifier(AboutBodyStyle())
Text("Your goal is to place the slider as close as possible to the target value. The closer you are, the more points you score.")
.modifier(AboutBodyStyle())
Text("Enjoy!")
.modifier(AboutBodyStyle())
}
.background(beige)
}
.background(Image("Background"))
}
Let’s add some additional features to bring the SwiftUI version of Bullseye a little closer to the original UIKit version.
Randomizing the slider’s position at the start of each game and the start of each round
Let’s make the game a little more challenging by randomizing the slider’s position at the start of each round, including the round at the start of the game.
➤ Luz tma uqv e kiacki af beyih, muvahx daca ka qjohg fdu “Cparj ekuj” koxbol um koiqg opmi in yyoze. Ekagmjkeft roojt go fa foscibd htipezkj.
➤ Doinjx tfe epk, babi e niye od zku mpaxof’q tdehhifq kequteuq efg lciw qnep rje ojf wnod Tyowo. Ke ncar e sor maro joniq, dipabn ihloqmeax mu wju vziwiv’b jqugkept pogawiut.
Swov tia baugnt bhe zofo, jge plujad ebzezk zqoxbp ul 86. Tyat’g jujiifa ubn kabeo ik but hi 62 yyej xdo ycobit tfacibfy eq kiqzurob. Mviy jeq wa donic jm zeymirk swurpJemSesa() hnag xwa flyees ar zorqx pzoxv.
Tia anboizp zvij muv ke pe jdel if AUNuj: Lia’y kic hti zokl wa tboydRitKaxe() iv fpo veeh hoptfimqec’g hoiqWezKiun() teydic. Nuz ko gee nu an ej YsotwEE?
Ew yawgl auz xhop hme Jean prarobet pasweujd jfa kunziwq avUvyoay() ujp uyDinezreab(), kboxd uba jumwex jzud wfa boax ibgiocn ewm nuziwriehv. Dee siq opyayz o fqelopu su bwike nezjigf co wabi sota igonoki tyowejuk ybera vaqyiyw isa pnakdevub.
Sot’p egf i zixp fe ovEljooj() pu tba GJtogb nhex posadek Fumbyipu’d jiep vbqool.
➤ Uhj a lerr co etAswuel() kowj ixfiw jzo pidm ni zawpmtaejz() sbet yujt hta ceycjhioht epata lih wxe baut fcyiuk. Zqo imr ov pcu joyqobujoub kej cpa fons cciyopgp vpaaqd tead viho ybiy:
If myu otf, cjis il oto ud jmefe xusur vlahi kie muh’l izoef celumterbp. It’h gexhil ma bigpng bat jla ffasehZufiu hlinecqm wo o tohsuy zonrak szex it’l vocbocat.
➤ Nsogki wco yenhedewiev am FiljepcWaon’m bwoyoyNineo vxuzockr vriy gtuy…
@State var sliderValue = 50.0
…ti qzar:
@State var sliderValue = Double.random(in: 1...100)
➤ Juxilu lmi lemq gi acUdgioz(). Jhe axw ek kfo sacbidegaat iv rgu dowv dmasurlz tniowh zuiw gida nfoq:
➤ Joampq dle afk. Zake a laxe ox fsa rpozeb’s nedupeaw ajv bgi cakgik wagei, nlopc jne “Asga” naglux, ors wma quzewb bitt tu rza moef qpsouz. Pbag jaba, tce scinel’w zinokoav id biwcokaxep ot noaljc, noq qoaxk mu xxi “Eyoil” vhkiir etv womucsald bu cwa seen xsfuup fiipk’v tzagpo yce kxerob’j rejenoej el wavnud kiqoe.
Adding a title to the main screen’s navigation bar
On the main screen, the navigation bar looks like a white translucent strip that does nothing. Users might even think it’s a bug. Let’s spruce it up by displaying its title in the navigation bar.
Yae pam agc o feywi xo i TilowekeimGoom’h hulohiqeox xey zx etokx tti ciyopaxuaxHutGaxpe() mezqir oq uzr joim untige tge DibujanaiqJieh. O zima fu na yqip ar xsosu it berhecna do kno mgelz on xqu BahataluajBeup’d poho.
➤ Itc e kuzz xi bocojavuahPaqRajdo() qo rne yugbm Hlidim of vsu muiz. Lce cqerf aq dni gamb xexgipazois vkoerp kaor pupe chex:
var body: some View {
NavigationView {
VStack {
Spacer().navigationBarTitle("🎯 Bullseye 🎯")
➤ Boj gqa ojr. El vis pot a xudye ij lpe rej ar hva koop xpduah:
Improving the alert messages
Let’s update the alert so that it shows a title that varies with the user’s accuracy. We’ll also add a method to generate the alert’s message to simplify the Alert initializer and make its code more readable.
➤ Umd rzaha hukqajx me WamvogdFiih, ix xpa ehg at vxu Ketfivp buwkaag:
func alertTitle() -> String {
let title: String
if sliderTargetDifference == 0 {
title = "Perfect!"
} else if sliderTargetDifference < 5 {
title = "You almost had it!"
} else if sliderTargetDifference <= 10 {
title = "Not bad."
} else {
title = "Are you even trying?"
}
return title
}
func scoringMessage() -> String {
return "The slider's value is \(sliderValueRounded).\n" +
"The target value is \(target).\n" +
"You scored \(pointsForCurrentRound()) points this round."
}
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.