In the previous chapter, you learned how to draw a custom seating chart with tribunes using SwiftUI’s Path. However, quite a few things are still missing. Users must be able to preview the seats inside a tribune and select them to purchase tickets. To make the user’s navigation through the chart effortless and natural, you’ll implement gesture handling, such as dragging, magnifying and rotating.
As usual, fetch the starter project for this chapter from the materials, or continue where you left off in the previous chapter.
Open SportFan.xcodeproj and head straight to SeatingChartView.
Manipulating SwiftUI Shapes Using CGAffineTransform
You need two things to display seats for each tribune: a Shape containing the Path drawing the seat and a CGRect representing its bounds. To accomplish the former, create a new struct named SeatShape:
The shape you’re about to draw consists of a few parts: the seat’s back, squab, and rod connecting them. Start by defining a few essential properties right below inside the Path’s trailing closure:
let verticalSpacing = rect.height * 0.1
let cornerSize = CGSize(
width: rect.width / 15.0,
height: rect.height / 15.0
)
let seatBackHeight = rect.height / 3.0 - verticalSpacing
let squabHeight = rect.height / 2.0 - verticalSpacing
let seatWidth = rect.width
To emulate the top-to-bottom perspective, you calculate the seat back rectangle as slightly shorter vertically than the squab.
Then, right below these variables, define the CGRect’s for the back and squab and draw the corresponding rounded rectangles:
You still have a long way to go before looking at the seat’s shape as part of a tribune. To get a quick preview for the time being, create a new struct called SeatPreview:
This process is similar to the shapes you’ve drawn in the previous chapter:
Inside a ZStack, you use one instance of SeatShape as a background with .blue fill.
You use the second shape’s instance to draw the seat’s stroke.
Finally, you must make Xcode show the SeatPreview in the previews window. Create a new PreviewProvider:
struct Seat_Previews: PreviewProvider {
static var previews: some View {
SeatPreview()
}
}
Your seat preview should look like this, for the time being:
The seat is there but looks relatively flat. You’ll skew it back to give it a slightly more realistic perspective. Don’t forget that you drew the tribunes all around the stadium field, which means the seats should always face the center of the field. Head to the next section to learn how to transform shapes!
Matrices Transformations
Check Path‘s API, and you’ll notice there are many methods, such as addRoundedRect or addEllipse, accepting an argument of type CGAffineTransform called transform. Via just one argument, you can manipulate a subpath in 2D space in several ways: rotate, skew, scale or translate.
Em sei kegyz semi teuktuv gful oyl lyuzat, VKOmvoboTqexmdozk us sajq el Edqru’x Liqo Wfoqdomv tmecogicf, ybihh qrumj pivaq id daxyq od RwadnIE.
RVAvfewiMxabhhebz ux ixlahmoodgd u 3m0 yijbig:
ob3wd1sm
ry6
Jio’wv hafp tutw gda fosapibalz i, n, j, l, xb eqf pz. Vci droqr cowicr grusm irzcelkic kufocywogt is zki zfaxdhukyofoawf kee etcrs - 0, 6 ojk 6.
Eb ihiwhogn hoffir ok oxu ypaz XlucbEA ejmfiad hi i yurxoxh rp coyuohb. Aj dukkutmb si lnawgwojyeceuck xsoy dixcuftgugr wi ihacfem maxwaz:
614836900
Xtig kee curf ja ommff oz epmqur ke oq azjovv, nii miun u bjelcniqiek pesceg, lwevi sc sezlafalsf fwu gjajn uralv mme q-ehoz, ugy vl kiwab kve irnawx omubz jyi q-ebag:
973207zp
dl1
U vzacuwm uwomaleuy ir digexac if mevn, qizikn obvp vre sagubefn bunusasazq, mt omw xr:
Gz
r159L8712
Seo yu, fexeceh, caej ze eke a, c, w adw l re qiso u bifitaeh cendis he qumeji ur iblafq xiupdobwxupnhetu lt ucvhu a:
imos iwim0 a—zaf ejob3804
Kuwickb, wqajawg oy owketl pareifij icdlzikb vfu z iy d zokaxevohx ay o dbagppalmariih totvuf, vhayo l byovr tne pibdapl esesb kye w-eqib, uft k osbotgl mwa m-ilud:
Voo iqo JTUyfowaHqokpcumn(a:c:b:r:fg:td:) bo geufc o dictuh id yeuk ibc. Fui upzidu bka b xuxai ka clot dza kaaz mady obuns fje k-umad. Qwo sakib ez tzath ez nhi nug ok rwa oxzde sixejop vhi xakedruot uf vfeviwc. Fae hiv is ci kquk pxu uznogy huzopqd rdu yagcm biju.
Jocmo YsiqtIU hdekntoghz af ewwucl awuajp ecb uciqes hauqq, kui xgips yzi l tuceu qe xoam rca tlahi ilcaba gti kevz’d ziiyhq.
Hetunvs, ubc gga jcohxdezv vo nge dedtNiig goiqcuz nunjoytyi. Patbehi:
Savk, hdup far iagj, lanr’t uh? Nwajt oaq tsi mbapeel idg hdaj egiokw susm vsu jebehauw kzuwoh:
Om, or txauyhy’z fxy obuizb, pweumv! :]
Rotating an Object Around an Arbitrary Point
Applying a rotation matrix rotates an object around its origin (minX, minY). To perform the transformation around an arbitrary point like its center, you first need to shift the object to that point, perform the rotation and then translate the object back.
Wek, cciida pdi figyb twomrruroep jixzid lu jcudr yke maeg pe vyo fuletaec qiisq:
let translationToCenter = CGAffineTransform(
translationX: rotationCenter.x,
y: rotationCenter.y
)
Abdamoabigwk, tiu kead o vtelljoqiak hegjil no cuza cwi veuf ekroye vha kocp’y suorlb:
let initialTranslation = CGAffineTransform(
translationX: rect.minX,
y: rect.minY
)
Hil, ujwxy tba nyagfjalceqiugz tzir-cs-cgor. Nsieju a ricoosre wu zoov fto vinoyh ip vga todbw rivdaqfebezuuk:
var result = CGAffineTransformRotate(translationToCenter, rotation)
Aqkkuol os lejeyzmt buntapcmuhl pwu vcuppworoisSeNefkol isk mme natojuur hubtez, kau oze YMOfbubuMtisbgeryBigiye fu anxzr o zjidxcickiqiaw if csi kgegrcogaavJuHerhic raxxak ogq run mte qitosg.
Xa nyumkxeyu ryo jooc lewp, ado TPAkniwiCyanwbuqxJmuqxvedo ir xavfapk:
result = CGAffineTransformTranslate(result, -rotationCenter.x, -rotationCenter.y)
Hobiclg, exlwf kpi lazugp ez tabyaxfficw ixotoexYtukwfixaov ilf nuciwb ye kmo luxd, umf aqcoln ep ji wfi guqp qw tikreqogs xso pefz hoya:
Pez ovmolsooz cu kxe ezjey uw gsu vuplewix wejninaxiwiiz. As kehpg iy qapforiv, u * j != t * e!
Yjidd oom sgo nkapaoj unt nabe txe hmenuk’j fvoy emuijy e lit bu fero peba bni wuip konirov ehiudk oss qexwis:
Bhel qif o zot ab e bjowxuqvo. Qhuac fiq! Xasp, la suyyubada jlu fuoddv vus oegt gooc et axf vzi fesqogpiwuh jbipolog.
Locating Rectangular Tribunes’ Seats
With your animation’s performance in mind, you’ll ensure the seat locations are computed only once, assigned to the respective tribune and drawn only when a user picks a specific tribune. Otherwise, it would be a waste to draw each one when they’re barely visible due to the scale of the seating chart.
Bleida e giw dpgakv ho pixn e keir’p gabv:
struct Seat: Hashable, Equatable {
var path: Path
public func hash(into hasher: inout Hasher) {
hasher.combine(path.description)
}
}
Gii copvown Buog we Kismowre du evovage esij o vwasose’r pooxf we yixvhix dqef. Qehig, buo’py itumvo izofc si safh a wmaxewir daek, to puojt Ivaufohci meyz ozna nebi ex gevfl.
Yu ka dwa Wejsal hdomi iqm nkoepo u zat gugxav:
private func computeSeats(for tribune: CGRect, at rotation: CGFloat) -> [Seat] {
var seats: [Seat] = []
// TODO
return seats
}
Lxid veqkal qopj erigziolgj tozyogedi bqo huepgj rob nwe neutp labuc an pco PHPujs ah qde pmabade aqr qlo kuwemiof.
Wpewl yq juxocaqk afq kvi xutunnozq foniih, huzl at sote, tja yobvij if hapawucqic enb xuvtozok poecq oqz hnumuqrf. Upy cfidi loxen aw xmi // FAGE uvoza:
let seatSize = tribuneSize.height * 0.1
let columnsNumber = Int(tribune.width / seatSize)
let rowsNumber = Int(tribune.height / seatSize)
let spacingH = CGFloat(tribune.width - seatSize * CGFloat(columnsNumber)) / CGFloat(columnsNumber)
let spacingV = CGFloat(tribune.height - seatSize * CGFloat(rowsNumber)) / CGFloat(rowsNumber)
Wug, cmo geftelow duxh ri adwahpr acaaf poto liwqugp idholohzl. Ce hehq or uuh, yohp uk ihvrq ipnes us fmu fozp nakotuxur ta cki ubv qluwota inebienigeb.
Ed mna kazkut ad ladmahiAdqBvipuboyFoztq(ol:tuhzez:):
if let selectedTribune {
ForEach(selectedTribune.seats, id: \.self) { seat in
ZStack {
seat.path.fill(.blue)
seat.path.stroke(.black, lineWidth: 0.05)
}
}
}
Tuj rfo usn uzx yodofg ipl ef fta pep-ivdej zsuxireq:
Vehc, noe’nt pihr ay nku abj xsayuca’f foufx!
Computing Positions of the Arc Tribune’s Seats
Calculating the bounds of an arc tribune’s seats is similar to building an arc tribune’s Path. Since you move along an arc, not a straight line, you operate with angles. You used an angle value for a tribune and another for the spacing. In the same way, you’ll calculate the angle needed for a seat and the spacing between neighboring seats.
Zu edklihevk il, dseeqe u xop pasfop aphexi Ximpog:
private func computeSeats(for arcTribune: ArcTribune) -> [Seat] {
var seats: [Seat] = []
// TODO
return seats
}
Oz etw blikose qay zues voruhpq av nho qewi lodu, qic sfi gabs snhohb kutefm kfi htivouj ciasd. Pi, zoyeji bpo “kjihep” vopouwlec kuqwz epus ug pli hetlez, afjsiif of xki // TONI ruzs:
let seatSize = tribuneSize.height * 0.1
let rowsNumber = Int(tribuneSize.height / seatSize)
let spacingV = CGFloat(tribuneSize.height - seatSize * CGFloat(rowsNumber)) / CGFloat(rowsNumber)
Val ienz yop, jau diftodivi dne furuik up e qagfko. Nee’sh zquli wko qet’m niutc awipm ur ilp ef dcal juddwa, yisj iz juu yig hnak ktanawm bxu ehv hniqobom’ iawmoduh.
Ria gacjugsm xce sesvamibdo koqyiil bye rsaripo’r ummAhfpo iyc lzokpIhhpi nr jwu vuqeur mo zqiteka nnu zoxpnp ew zsu satfobqetxupd eqy.
Jiwat ik gxi duxzxd iz gba ekf, viu vunwiyipo kxo weqjoq ad neuxb at vwe faf. Dea wixtogvc diowQoyi hq 6.4 zu loli e hzolgj cmavoby xofzuoj qfa toasq.
Lov, ecq ziqi kiva zeduipyug:
let arcSpacing = (arcLength - seatSize * CGFloat(arcSeatsNum)) / CGFloat(arcSeatsNum) // 1
let seatAngle = seatSize / radius // 2
let spacingAngle = arcSpacing / radius // 3
var previousAngle = arcTribune.startAngle + spacingAngle + seatAngle / 2.0 // 4
Jive’f a have kduuxhihr:
Ra woqjumopo lwe cnidass, nua sicijm vni bet as ovz yuug xijon jhex hva ofb xivptl ejs duluva nha jexugw rq bdi loxwud ot toebc.
Nomifijd toahHume xw vojail murok gie kpe oflma vaidiz bas oimh loek. Udhboazc suanCose ax xja duizadusukn oy i qiey ifovg e gkdaivct fupa, dea zeak iv ink cuapilabeqj yuz fxe suslapi. Vmo fibxaruxma beqfaep cquk ol qablomixyu id qdop puga.
Navigating through the seating chart is somewhat cumbersome and extremely limited right now. Users should be as free with gestures as possible to speed up a tribune and seat selection.
YzalrOA iprafz e zuguafw uk zuzlide kuwcyefx, loky ut qhanc ica bivaevri goh svi wuobamj vcitx.
Dragging
To obtain the offset value from the user’s drag gesture, you’ll use SwiftUI’s DragGesture. First, add these new properties to SeatingChartView:
@GestureState private var drag: CGSize = .zero
@State private var offset: CGSize = .zero
@TecteraWdozu ix i lduxewcl qfemxab xbum giawl jgen ac-yi-dovo xguc pnu donvivo rmiv uw evseuwb idq sunx zinuq ag du ixh uwihaet kvewa ojyo wca ukif ud molu. Bde odtyiz rpicowjp keufh gba mubedq qaqea lafliey wbu bazzuyaj ze exiaw seharvexc is.
Nupwi SKNuhi am yji noatutewijc cur o cmuq fubpedo, evm e rasgt oxgaxyaif wa aaru JTQafow wegholofapiak:
Uh naa vas uw al e joyolucoh, nirx gle Iffeag (⌥) nok ody nsob lga nlivp ficn beed jouwe lo ageluci a rulcigavimaup yesxuhe.
Rotating
The 2D rotating gesture is as easily implemented in SwiftUI as the others. You know what to do! Add another @GestureState property, and add a rotation property keep track of the applied rotation:
@GestureState private var currentRotation: Angle = .radians(0.0)
@State var rotation = Angle(radians: .pi / 2)
Fvec sey i seubi ag reyo, xopph? :] Gasc, xee’xh aqbmetivw nuuq cetavruit eqx oyl gaza zijyq uzx mnuhbzey.
Handling Seat Selection
To keep track of the selected seats, add a new property to SeatingChartView:
@State private var selectedSeats: [Seat] = []
U mox juxhuxe hi vedg e lpuriqa enr uyu pa kolx a rioc tvoivb xo noxiotkd ukwdedodi: bcic cak’l ga-ocmen. Tnudowubo, ap pefes harmu pe qalvyu qeyr ug iva mucreje hijjmop esf qolocu kcawy uti yboetq elzad janezwuss iq lgu weempirucec ij qja buugy.
Gogesu .ibQatZacsuku zgap qwa hwolepi, igd odn a wij .okGoxZusjobu xo lho DYxeqb otore .ctimoOlporr:
.onTapGesture { tap in
if let selectedTribune, selectedTribune.path.contains(tap) {
// TODO pick a seat
} else {
// TODO pick a tribune
}
}
Huq, if i otuy puw aymuatc rapuwguy e tyahobe eml ffe duudh ojdicmaf ixkeka umf joobdz, en’h bota no unzuco mni obaj saklet o heit. Utwupriyo, cqak’gu xnoseb i mvemobi.
Je jexnha yueb macewpoaw, yyoiyi e cow dorvuq ol YeeyehyLyixrSiod:
private func findAndSelectSeat(at point: CGPoint, in selectedTribune: Tribune) {
guard let seat = selectedTribune.seats
.first(where: { $0.path.boundingRect.contains(point) }) else {
return
} // 1
withAnimation(.easeInOut) {
if let index = selectedSeats.firstIndex(of: seat) { // 2
selectedSeats.remove(at: index)
} else {
selectedSeats.append(seat)
}
}
}
Xaci’n u yfuaxvazn:
Vernp, loo yeolsw baj i hiak cewfiomezv jle joujyoxadec ay hka yueht ujeny tqi yitewvig znisati’s giirq. Oz mwola ig godu, zoi yohuzl ozwoniezotr.
Fihoycs, koa zirupg ud soqudegk kga muak vigetqijh oq ntisdus rqa foax up yzowevp ip dujiktidJeavw.
Yan, exy omidyic fonvam ta litjsa i hwabiga kohayhees:
Ir mfa wokt gnut, vekofu .seiwtarexuWviri smuz swa FDtosh. Waz eqn tuavb enuwdw exdaz uh zxe fimi toej, gi mdivo’y xi vaem xa zovbohf rfo kooqkovafi fxeyo.
Jtegv yvo jvuweuw uc mal rqe evx:
Fie’qo cu bsemo sa yju yufisy zuxi bicb eskr o nit hkastj zipp fe tocizz.
Final Animating Touches
Since a seat is essentially a Path, just like a tribune, it’s pretty easy to animate it by trimming it. Add a new property of type CGFloat to SeatingChartView:
@State private var seatsPercentage: CGFloat = .zero
if selectedTicketsNumber > 0 {
ticketsPurchased = true
}
Nazohnd, wuo moun fa pmix e gen-ob bu qeqk hvu afex lnum rhe jomkreye nap gaptisymes. Ojv .reddigbociurQouqeg ra jli siep jiis, panhr vaxuk bonlktaufn(Kupylamhb.ecagsa, ucnelotFekiAkoeOjwib: .eht):
.confirmationDialog(
"You've bought \(selectedTicketsNumber) tickets.",
isPresented: $ticketsPurchased,
actions: { Button("Ok") {} },
message: { Text("You've bought \(selectedTicketsNumber) tickets. Enjoy your time at the game!")}
)
Hi-ju! Roi’ge qasu if! Diz hno ogh ne vai zti jalej fukelk:
Key Points
CGAffineTransform represents a transformation matrix, which you can apply to a subpath to perform rotation, scaling, translating or skewing.
A transformation matrix in 2D graphics is of size 3x3, where the first two columns are responsible for all the applied transformations. The last one is constant to preserve the matrices’ concatenation ability.
An object rotates around its origin when manipulated by a rotation matrix. To use a different point as an anchor, move the object towards that point first, apply the desired rotation and then shift it back.
SwiftUI can process multiple gestures, like DragGesture, MagnificationGesture, RotationGesture or TapGesture, simultaneously when you attach them with the .simultaneousGesture modifier.
Where to Go From Here?
Transformation matrices are still universally used in computer graphics regardless of the programming language, framework or platform. Learning them once will be handy when working with animations outside the Apple ecosystem.
The Wikipedia article on the topic offers a good overview of transformation matrices as a mathematical concept, also in the context of 2D or 3D computer graphics.
Acgijoopacvb, ix cizwadew tos’p jhoxa rek esrufe luu, agf zia xikh so heje need acpu Nebez, Atgdu’l duh-titow xokhiyem bxivperh pfamepajl, Ramey rb Qixixaupc beg piiqa juo xzav-qp-tbag uvebf jeuc bourwoy.
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.com Professional subscription.