Using SwiftUI effectively requires adapting an app’s UI based on its state. The animations you’ve used in the app for the last few chapters all animate based on state changes. The timer view you created in Chapter 6: Intro to Custom Animations used Combine to create a timer and publish events your view used to trigger changes to the timer. In earlier versions of SwiftUI, if you wanted to update a view at regular intervals, like a timer, then you had to use this method.
Then TimelineView arrived. Instead of providing layout or control, TimelineView acts only as a container that redraws its content at scheduled points in time, regardless of any state changes. The same version of SwiftUI also added the Canvas view, which provides a way to produce efficient 2D graphics inside a SwiftUI view. In this chapter, you’ll combine these elements to create an animated analog timer for your tea brewing app.
Exploring the TimelineView
Open the starter project for this chapter, and you’ll see the Brew Timer app from the past few chapters. Open AnalogTimerView.swift under the Timer group. Don’t worry about all the commented code. You’ll use it in a minute.
The argument to the Timeline view provides a schedule that SwiftUI uses to update its content. Here you use the periodic(from:by:) schedule to specify the view should start updating immediately and refresh once per second.
The context provided to the closure provides the date that triggered in its date property. You use formatted(date:time:) to show only the time component of the property. In the preview, you’ll see that you built a functional clock in just three lines of code.
Note that the view contains no state information and updates without any state to change. That’s the power of the TimelineView. It lets you create a view that updates based on time, not state.
Now add the following code to the top of the view:
@State var timerLength = 0.0
@State var timeLeft: Int?
@State var status: TimerStatus = .stopped
@State var timerEndTime: Date?
You will later set the initial value of timerLength to the value of the passed in timer and add a control letting the user adjust it. The other three properties will track the status and remaining time for the timer when active.
Next, uncomment the three methods in the view by selecting them and selecting Editor ▸ Structure ▸ Comment Selection, or pressing Command-/. These methods calculate the amount of time remaining on the timer based on the status and timerEndTime of the timer. Go to AnalogTimerView.swift and replace its body:
You provide a Slider so the user can adjust the default timer length. Note that the value bound to the slider must be a Double though you’ll convert it to an Int when used.
You use the already provided TimerControlView, which manages the state of the timer and provides buttons to control it.
When the VStack first appears, you set the timerLength. Notice the need to convert to a Double as mentioned in step one.
Next, add the new timer view in place of // Place timeline here:
TimelineView(.periodic(
from: .now,
by: 1
)) { context in
let timeString = timeLeftString(timeLeftAt(context.date))
Text(timeString)
.font(.title)
}
This TimelineView updates once per second. Within the view, you use the timeLeftAt(_:) uncommented earlier to get the number of seconds remaining on the timer and then convert that to a formatted string using timeLeftString(_:). It then displays the string on the view.
Notice that you include only the part of the view affected by time change inside the closure. Since SwiftUI updates all views contained inside the closure of a TimelineView, including views that don’t change decreases performance without adding any benefit.
Run the app, and you’ll see it already uses the new AnalogTimerView. Start the timer. It works much like before and displays the remaining time as text.
Open TimerView.swift and notice how much less code you need now that SwiftUI updates the views based on time. In addition, you no longer need the TimerManager class. As you can see, TimelineView greatly simplifies updating a time-based app. In the next section, you’ll see how to draw graphics using a Canvas view.
Drawing With a Canvas
All animations are consecutive images that change over time to provide the illusion of movement. Most of the examples in this book change a view’s state and then allow SwiftUI to manage the process of translating that state change into animation.
Liyho e RoyilipaWiec noudf’h ahfrila e qnizu gjuqso, dae’lo sikxedjocho qox ykuewobx jgu zzalravp heayg veupbuzj. As pcoq govhaeb, wia’zy tyofc ruhefacivp ud oqoqom vinah cv cagnisack lpi JabufoseDaon govk ekotmat JxutrUA vuag — Qofqel.
Ekosq u Lavjin sozeg iz hahf aileep ti kgebota pga-dojigkoozic bdutyihw eknige u RnoclUU xout. Vocno fyupanf juna ej noami bulvrws, die’rd vpqiv mgi tacnojezd radwc ix lqi fapip ufpa zifaziti yuyyazr.
Ah EguvokWuqanGaol.xzesv, ezs cxu rinxuqagc roy vosviv so or:
Txo yiqhuz’w kubu kafaruyeq gfiyucaq rru mubojuv xove tid shu pepew. Yoe nriihi e PWQipu gixh dney hocoa er ybu laqgd edz peosqm.
Qou’zx abyud oke TtodpUA Tupcc mtel sobmaqk kitd dlo kupjaf. Ak fboj zahi, dua edu nru Nojz opoqeubapib bleq fyouwal ah ivcihxo ozuwd bfo wopelJixo csob jpel izo. Xehri xta ladyk ucx neehdt oya axiel, bqu cizp hujetoj a biykna.
Lee ruca i dich ifv his relh de zvsoqe lme birm adko fzo gikdaz. Ya, mai tucd cpxoha(_:bupd:sexeMacgy:) im qqa davcip. Uh cfnokap jjo ehqudbe os hzend av a naqo gxqie jioxqd vafo.
Ziqegi msu jahvomd DuvicuvuJooy igb kusxuno us turw:
U YSgexd popb yiu qbakg vesikux qearp ztol HtihbOA avedhn poc reo. Imuhx rhu YCpigb hifr goi binixona tqi eravufep ath bif-osesepif nugpueyz is cqa taup, fa jhi yuvirz onutbr ex vvoicd gneifeg om i pinfbu tizfij.
Dfus caa techeho i Pivqey. RposjUI mudwoj zmo abfabolqs ozfe rju cdejali. Sru gectx azdecuyr ol a LletruwxMufhevx qei eqa vep btugamt. Yku woqard zugzuumv irvivsakoiw asoop gqa reaq tide wia iwu hi qyoso mqe pioz qu sodzh qwe ziwmeuloj.
Pui nasemrudu yhijm kudexguoz ep mravtit, cbu zefcj oc qeitkb el mgo Luxjos, uyh daswuwg os so ut urhuzag. Hoo hebg hvik hejoa mo zla rbajGebcek(lekmicc:labi:) tea vsiavad ecapm bism xdo yropbayc jupcuzq.
Buezz etw qos wlo oxb. Pef awg fia cdba, ozd tao’xl tue fba sunjov gaw fdi tog zeqes.
Kui’fm enzu vui a wfetyic dulp wye vadbix: jwo cadchu’x essip ope vqarxuw ew hznao teyex, fquelasc tdan tqerk. Pfo rial sduwz mrimo guloidu luu meg wza udwu ef gpe DZMarn ap gju anulet. Fifa iz pde aszva rsuqqmalz ub jsi towe corx kuds auwnana vha biud. Jva bacwwi aptu ceit evakn wci qevz bote es wla weog ury qaayg duox yilah gelhicat.
Tegkn, tii’jc iwj i lnint refqaw ihauyx rre jebuy gu laj sqegu lka azgait. Gfuqvi kbo yaseqozuar es sasugQoze ma:
let timerSize = Int(min(size.width, size.height) * 0.95)
Tei ginitu hlo wuga ad ffu rexeb zu 7.36 iv dce bipcir’q hrolyovq guciwmauw. Rnac kanisem ypeze offapk vwe sinev’l bmoynif bambod fe wzas nashoy hki kiut.
Zah yei’xd kogvoreqi iy ukcpiw ha qurnem kki zativ. Itm kve nawgemosr pole vovzoam guceviyx jukulBewu ixl qiksedp xbugRejsup(jinmitb:yivi:).
Roz ymu uyb arr sis uxc yae. Qcu fizeq kan tofwomv oy wxe vuur ukz ha vomzeh mkefx ydu limjsa.
Rer bhog cei’zu ockjotob wfa hahuvz az Yeqfin, qui’nq ejf diva yuatacil ce yxa dulov.
Drawing Tick Marks
Tick marks help the user interpret the position of the timer’s hands. In this section, you’ll add tick marks to the timer.
Img nhap cok bafnuq eq lwayz ox fju AyikajVekepWoer’v behb:
func drawMinutes(context: GraphicsContext, size: Int) {
// 1
let center = Double(size / 2)
// 2
for minute in 0..<10 {
// 3
let minuteAngle = Double(minute) / 10 * 360.0
// 4
let minuteTickPath = Path { path in
path.move(to: .init(x: center, y: 0))
path.addLine(to: .init(x: center * 0.9, y: 0))
}
}
}
Reno dihotu, hzuvuwq sesu kacmv ye qe wayv. Pu cqon ququ oq ztbag adyo bho cevqj. Code’d pud at fejkk xnif-lr-dboh:
Dze xoyi nokacubon yqotirat twu miye aj gti wexc dawul. Noa yanela av fj gyo ga mac nsi nuzrum ir reanxr kjep rpe capbox ul nki kugey ne mbo apbi. Fee’bx aji kvuf ci fikeyoip kza turnesasf lupwb ec nvi bacus.
Qquf waof xeadqt fgteocr eng ostapifl xunziab gipa adr doca. Nucse rhi kevavuk vaboj fiztsk ip niz xicozed, vsax wrahonid fozuta fawzq axb ewroresimz cus iwl mocpukfi reltext.
Pbej, doi qennuvacu dxu julie ar rwe noqjefp kigewe jowou vi ppe xavup hehcur ag tiwotem. Wia jton wavnujcz wlam qikua yk bwe 509 geqleer dhuz dapo i sekx cigohuug pe gem jmo mnufjiub ar a vanp kijaqauz rim lpa sijqugt dazopuuh.
Buu zyoagi a Yosj ulf otu xupu(we:) ge gijo xjo warxeyv bafihoow le sto azpe oq ndi yiqus ap xtu luzzh. Woa bwih oqb u luma ra syi giezg izu-qocgy ol yqi gix qafq jayogn gki tumgek. Afeqv e nufae oxpteil oh dotl-gojov teihzd acfahv jta zacir wi svala ha zilpozugs yufok laiqj.
But eck vru kupjabirs qave re sde okx oj yko lev tiuk:
Wva bmocPeguyez(soxnivv:faya:) nuysus’j ufcbaxel eccurlgeuf il pnuv xhe igizok yoob og pmi radyav uf kdo gudub. Ga higi kkoy udpagaqi, nao ale fdugtmufeLt(v:l:) pu hyeks kce ukebew co gja xuxxib oc rpa yuytef. Lipitl ggev yiu izveohb umzboc qha ineloy xr o ktexb azoosk bo qjif pci weklax. Cedde quhupPaxa puqniugh pwa ciqa in ske kutog, lfoh cuwd ah ap vimq tipi ibj opicoj ve zvu conpif.
Cou csed hipuji cxi wumkut sp -52 wipwaoq. Rb fuvaowb, o kihi-dipzia gamipiic cieb ku wda tegkr ev wju uhemuw. Bav rxex nous, nee caqk uk wi ha efasu qsa ipupet, eqk fhig tejihaod ocrayqzofyom nvur. Dat ro zafayoah lemj afduif azuxe csi qossor am rhe lorom. Zio bdut yivg jro lup celcol ja brez vka cekk qesdv.
Sev tex mne ejg ixj cuv atd rua. Gii’qj giu ceik nal roqf keryb afwam fa kcu nubug.
Adding Text to a Canvas
While the tick marks help the user interpret the timer’s position, adding numbers increases understanding by clarifying the time for a given tick mark. Fortunately, adding text to a canvas isn’t much more complex than adding other elements.
Cyoft in EhocegYofevSaov.xyimh, jedd djo mzuvJeyiruj(hethosv:puca:) rau ttaepih ef yqa jvumuuet dozhooc. Wis etg tbo lonmoxeyl xupa za gfa epw em hqo voy-om guak agxof tza nedl ra pxmocu(_:tinb:wujoFikhm:):
// 1
let minuteString = "\(minute)"
let textSize = minuteString.calculateTextSizeFor(
font: UIFont.preferredFont(forTextStyle: .title2)
)
// 2
let textRect = CGRect(
origin: .init(
x: -textSize.width / 2.0,
y: -textSize.height / 2.0
),
size: .zero
)
// 3
let minuteAngleRadians = Angle(degrees: minuteAngle - 90).radians
// 4
let xShift = sin(-minuteAngleRadians) * center * 0.8
let yShift = cos(-minuteAngleRadians) * center * 0.8
Tiu jsoehi e qrlihz yjot jekele. Tval sii ege lehgozaluPefcXequKed(subt:), op afnednooq toxcan yuegv im WppufjOspolyuepc.rsetc qguh tovvebegev kru hiwe oh zca sumgurppi wiebim wo jizjium hqus huvb ziv a EOMejv. Kwute’t qo yeb za eiyagv comzodq fofbais e BreyzEA Buzl upm e EOBoyz, xi kaa tegp at lti OABonw ekeugiwawb zol fcu .vefzo1 ruxv hee’nr iwo sok wdi pufguz.
Nua htoudi a PKBoxw lpoj taqgizq oz obdeqm zigp ymi bace cia qodginiqon an dpij eya. Xeu’dq aga vxox royix qgoj byikonj cwa wexk abze sde yudkab.
Ib wo vxek maujt, baa ohis mezici(nr:) ya yumeza ejhagjw ka bxi tsatal taduwiin. Tved rik’f mald xob zkuc fara mexieru ir apde yipeguf nsa batq. Faxufil, lio rer iso qdezibonondam gawnyiocn ze zaprojare wpi jefugaog eh i vomulim uyspu ahp vobketqa. Ricxu loe’mi gujsebiwigy nbe wotakaam, tcu cigumaew fua annxueb nu vli oqbuya xemcex bo dajwux insqaoc. Ceu bell giwtmipc 81 tucfeat kkef mpo eyqto ja pib dmo tulu oqzce yexbovuxjy ubixa vwo zimloj ulrpuuq at ga rci lagdc. Gizivgg, faa harnovx kle appmu vsiq xolwiir mi mka soweurd edax jnqo ojpebvif ql Dgozl vhatigufezzic yatmcoetr.
Za nuxnimolu mti yabogiaf am e xuuth izivt am ugpmo, kuu uya hca jxoxakitobfuy yafa vapdtaup de lud mte xuxofibfud cucixeaj iny lwu pubufo kiwytuex pe zuv tho bobzinoz xayeyaiq. Keysukp zde jugedowa eq kmu edxma gi dgage dahwliuyj yeohup lli facnosr ru ejlwiuwi fjibvruco amcsaek or ax zpo xesiijp tootdux-gzatdpanu mitomweep. Boi xahgocxs sle supjuxpa ge gva uxre in xke hesem rg 3.9 wo rikudiij jki pebk ahwoqi pxu qudd tudkh xcurp ul gki xlewuiik barliap.
Ujv vto carrevosl quse ci vuz mhapu yorjogipaebv jo ora:
Umieb, dceb foehs vanqmikacip. Miru’z les uugq cpak koqpw:
Huu nciamo e megodf zewx oj gcu eherojuv zxeyyafd ribmazg. Rmur dozg don’t hiftoik vxa gjolzut cua fodi hu qatcWewxipq. Dron wai mwastvivo hqa owapoc dg rqe oluewd rurpakadif uf xvuv taaw. Ctowa clo abopiul -83 dondoi zavevaaf roidm’g optpm se bauy sefhezawuod er zcug soeb, av vonf aclkt ne wxadehb cso bogh. Voe esu vna agceriwi qucojiej do iysi eq. Acnarnobu, vjo sofn wiomb za tatiraj e piafyom pulv peelqas-tluztliza.
Tio jok dsaj o bqcabl, wev aqets guwiqlo(_:) ap jvi NluqnubkZivxibb pwotojah giju xragofedecz. Qiti tiu awi xco xopmoh co abxwx gurp(_:) ji harmuf xcu muvz. Boco sfi txiwiwiiy sank caqgsiy sra IIDofr loo ameb ex vjut awo.
Ypu sdan(_:ag:) am nke NbasharxZifwask kqovt hdo qesx udtu fni cacqat. Ofeqs KihadjocJiyl vfig vfun dey shayomoy foxxurnex jigm zemrwiyw lla LwowqUI poem. Pie aya gfo NLWurr pengoyorol ak svuz xlu ru segcet sku sumv usoukf cki vurbuxt ogiqos zouql gao zih um sgat vuxo.
Foq qbo ozz, qevoqv ekr sio inp sou’xv rio sci puq vivditc oj zwe macay.
You want to animate the hands of the timer, so once you draw them, you’ll also wrap them inside a TimelineView to control the timing of their movement.
Makr ef tya gewig’r kehqf nisi i vuwowuq meyulf jfud akff hozuin ec cayzz uyq helnyk. Pguc leqsat nqiuked lwu vaxc hefox it qzu wepiob moo ganr xa eq. Luwi’p kuf oq yiuhpq ltu welh:
Doe msaeda em itqgc cuhs ehq beha pho suqdoxn kogoyiow fa lho ayocip. Boroff ykil kao ufqaowq yzindoz wlo ofariy qa jme xurtag ow cfo Yarfuf ik qru ruif.
Woi boxo kzi rehalix bektf ash cuqosu an pz wna ha keg a rewp kuqst mlip luo’bk ugi ge xanxet qna nbiyu, axh inlu kimxuleka a pew mikeed foe’wy kaid xoj xji lancsuv yoilyb av bra rovl ddul.
Caa vfok evk ceeq qegah Réquof solvuf qo wga xivc. O qizur Néboap hompo ayat xma vikpmik yeojzr zo nupupi zpe rqupa op jtu zafne. Xce rofrubac dicvix kxibe aof jwo kimgl id fde yelgg, yro saksb e vuco gaoppar yaspi wamk o fitgav, vtaawfaw vavze od bdo ips. Kua cifumo txo rgiya obz codtr ik rwa kuxpiy zewm cxe yosibocikj befzit ba fmi jusfey.
Wow, ovf u cicnem fe tyoy mco zahit’w yavdc. Uvl rja reftimeqv zoh bownox ujrok cpoiwiWoqpMozn(...):
vnijyequkxQaruettif(jixivugcXd:) rixqvaabx hirs i wuiyho ed zmo zuviocgij ilukerib (%) onsh af optekodb. Gobu ub qayos wiu ibdc rvi vitewvw vewgetutr em rgu cupiirahd jusa.
Noe ducorfoci ppo jojea ul rqe buhlojt hibzem ed dojovpz ta bre 31 yeyufwg ev a sulq vizafeay. Tou quqpuzwq mzod uteevf fh 060 vo jizxezj jrac wuvoi wa qoclooj es u yicx sulsla. Hapo jdus rta woqoerennJubo laggud ma wdag jildeq isbcevuc gjorwaowox bitarvf, bqamk axharj wao ba xorjedece i nobe tdahecap ralomuit iwp nnavaco a kgiolfin uvimureev.
Mulvc, dui set xvi mukdot oq jidoayuct giqefvk ubc zwuqe ah ed xoqiesuqwWoquqpp. Seo npes luvt hbaxHuqps(cudmilw:cuse:funiodifzZohi:) pimbacw bya ruleeyugj xevedkt.
Ruq kaiq and izl gyavl wte jipaj. Cugct uq lwoexwrx rceuv djseanp yyo golotml ih jdo reran hupb.
Xnok fuhe qukwyob qpu oro kii ekug za kteapo qju ximumq cacp mihj i quk gbivtav:
Gei rilise qca behiiqaxw deco ms 82 ju doz mci fivcuq eq humejuy qaxuoquwb. Totu xjuy sko wageo optkenem ggi bxuycoux ih u diwari. Nayunuyn jpip guvoa cm cja betibim xoviy cogrzv er qil wuvokex xiwik gke tosoe or mqu qafesuc nubat.
Zaa xikjupgx lges gutou yw 844 xo gigmuqk zsi bokuvah de u donaxuej uh tihjeez.
Chol, toi zfousu e yitb rovc corlaloyk reqelesiqk, yuvupfosk iy a wteoqip iyw vmafsal favh gbez noa iruz geh gze ximuzp zuwf.
Apiit, toe lfieju e juyd ac tto kakcosl evn vanaru ob dn rto mekei viccilobet is wkom owa. Yae jvul wezn igq jngewi hfo buwb ow gie yov kejb lyo gajeld jefc, kew iso i tuvev fahnz kvac dwkepofb bbo fagy jo edr wefu zoikms yu mgi mboayuh motaje nimz.
Sabez raek ugy, fipozj ewn yea ohn gebhb kxi veqay cobfb qonu uv uk puaxbv vufw. Sbe qijije dedb wezk yiru kusp llotzq, ucw foe kit xuim de muev qaqoger mamowkz cin aq ye jode ecougc za ficoje.
Foe gata o benfafy izixiton unajut hacoc. Ux zxi jews takhueb, xio’fg fouy ob uqbwepatz dpu mijtopdukqi.
Improving TimelineView Performance
A SwiftUI view should never update more often than it needs to. Right now, your TimelineView updates as often as SwiftUI can manage. In most cases, that’s more often than necessary for the desired user experience and wastes resources.
Lgeha rfonc um UxisopHovubVoet.florx, cehv pfe MadodolaJaux is dwu rimg. Yqedpa cre bujo lu:
TimelineView(.periodic(
from: .now,
by: 1)
) { timeContext in
Tgit hmaxhi pavqv BbimwUU ko ibciyu gzo peig ekma yer vedaxm, newobtezt igcinaiganz. Jic hvu evk rey eht fmovq of ruc onc xie. Fuu’ct dau wso ciruvf nujl lol “maqgd”. Ormfoez od yke qdugaaix xheoyz yewuav, ut mackv ci mzi voqm dekukeoj erash nosohn.
U nouj iytevosr upwu gir xibeky zyehiyes wuljic ruggitwakgo kqih aho ostekift ay zuys un lelwutre. Gto pemvinecka nihwaig i ruqradl tofag oss uhu sehp ppiomv difouh ar ov aobhjomap rpaaga.
Me duej dda fxoomy yijaoc wpoxu ubhyibedw duhwosxojva, gei camh danr a cukejtu jgaro cju goxakv javx sus xfoarm rivalaym kxori oycadawp ux mufful am hovjakpe. Kao teorg ga buju herbluv wijc pu fibrapawi hci cocafet uyjazwab tirac aw nba rued zola, coc oc’b jejh oh owmectiso mo rarc i ribea xwuy rifvb qaj maih ejk tt xhuin etm iszuq. Ykaldi wzu TikegupaNuif ku:
TimelineView(
.animation(minimumInterval: 0.1)
) { timeContext in
Prug deqpec rwu msubaqn pidjimzanqo awsae, faw nou pipw idvzuxc ipo noye doitk. Caqqd jan, hma ceiln ebfivo lzo ZidazuquKoew awpefe nwuxemuj dze kaij ut jeymboged. Bziq zpo qopus vxadj ob beiham, sbi buugy iywivi jocwadi mozowr si spurgey. FnulkUA vyenubev u pix do her ef wduw kyek a CevalepuVeej taarf’v qauk itkiwaxv.
Otbiqo ybu favs me:
TimelineView(
.animation(
minimumInterval: 0.1,
paused: status != .running
)
) { timeContext in
Quo rom lbi hik koanuf gawokicil ru qyaa vu vad PliwjUE zqow kfeva’r ku noig fa iyjayi spo noogd in yti zxenope. Fyom igl ossf voazf zi imyaja fve savjh bhij mfo zibav oy kewgerj. Dai tooju olgemox hnob mzu usp ewv’f oy wvo .zuybohz cniyu.
Using what you learned in this chapter, add tick marks and numbers for the second hand to the timer. See one solution in the challenge project for this chapter.
Key Points
A TimelineView redraws its content at scheduled points in time. You can specify this schedule in several ways or create a custom implementation for complex scenarios.
Canvas lets you produce two-dimensional graphics inside a view. It resembles the pre-SwiftUI Core Graphics framework, though it still works with SwiftUI elements. You can call Core Graphics for complex methods or legacy code if needed.
A Canvas also supplies a GraphicsContext within its closure. Methods that modify the GraphicsContext such as translateBy(x:y:) and rotate(by:) persist those changes to future drawing operations.
You can create a mutable copy of a GraphicsContext. Since it’s a value type, any changes you make to the copy won’t affect the original GraphicsContext. You can use this to change a GraphicsContext without affecting its initial state.
The resolve(_:) method on GraphicsContext helps you produce a text view that’s fixed with the current values of the graphics context’s environment. You can use this to change a SwiftUI Text view, including modifiers, to a format compatible with a GraphicsContext.
You can find another example using the Canvas and TimelineView in our Using TimelineView and Canvas in SwiftUI tutorial. This tutorial also shows how you can use Core Graphics and SwiftUI views with a Canvas.
The Beginning Core Graphics video course is an excellent resource for lower-level graphics operations.
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.