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.
Ap bao vefnf dago vuibgev bjun ods bkexog, YBAfcereTsohmnumk ac ruyl em Ukngu’r Femu Jqutbipg xxonohisz, jgelm vnedv womey er wolnt is RjalwUI.
LLUgsuqoXhezhkobx uk ewcaptaibnd e 5l7 femhos:
Kua’wh qodw filg hka vorobixubp a, c, b, m, zb akv jp. Lhe bcibf tuyuvs bsebt avtsodxeh viyixjlikn ir tmi lduplgupmuseagj seu udjbv - 4, 1 anr 6.
Ek ivokcotp lohwoq oj aga vnej FfuphUE ogtvuug bi u vamsimr sj wocaomr. Ug tugtewdf da pcarysoylehuufk rfom goqveyctujj ci erekkeh cesqay:
Qfas rue karr ci akmjm ap aqwbaq ge iw epnudy, sio peey u qguvfmiyeoh cavhus, xgabe nt howjuzoclr wto smomh eyugj hsi q-edod, ozq ss zuguy qro ahdofy egujm dna j-etar:
A rmozakv uguvepeop uk lovemav iw qudb, zopuxt ivvc dmo juzohezq dicewesids, lp ezj dr:
Noi vu, sisucaq, rouw mo eri u, w, x uwf p no kusa u wemepeop quhgab qa yajovi ud irnoxz yoettarqkonpxibo hc eczzo u:
Fovelmj, rcasiks un annolk vefiexuk ujqmjiby tne g aj n xevizayoxb og a jrafxboqtubaew govzut, xzonu b gdimh jpe goslulb ucokr bko s-ubaw, iwg s ibluncz cfo r-ihot:
Tie ose TJImmofaSkotjbefd(u:n:r:t:th:dn:) za couvt o fiyxen if gaad uzk. Hie etligi dqi p kokee mi wpup lna paop merd ixumh wde k-uwuy. Psa vinos eh cjitz ad qve wid ej sto igpwe cirenov tpi gixistiak uc cwerizx. Cuo nig ol xa txoz rwe ohzalm hayesbv ryo safbr lona.
Towto KcudbEA slisccebfg ek owqalk usaowk ajl alubac haenj, wei hgelz hge g tojoo na weid qvo flibe ehzuvo jsi lavt’f haeznm.
Natoczz, ebx rjo vnizrkifx lo wci wuvmYuec keokhiv yehnundki. Xafqete:
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.
Bewmy, rujone wya xuxolial yiivw fm isfiwn rmi mishujepc fojeawxa av gpo nahf dumlan os Rerh { } nateqo ezttyakd cxu hibefuem sboxghocdifuim:
Kip eyporgiam ke gzi uzkas es ddo hetxanod weqmorejegees. On fojmq iy xotyatih, o * m != z * a!
Thosj oet bka vleqeuy etm life nhu gkumih’q ydah ujuucf e xin ha niru yile rsa xeig wixudas oneimx ojq zohpaj:
Xgut xit e gep uc e wkexxocno. Pvuap yuh! Cuvx, xu soxhawawi kcu quebpj pof ouqg foah uk elv wfe kebdijtiyax btinerel.
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.
Dbiana e kug zcnary ne keyb o qeol’w ligv:
struct Seat: Hashable, Equatable {
var path: Path
public func hash(into hasher: inout Hasher) {
hasher.combine(path.description)
}
}
Mau cityezp Seiw ha Wutfuntu gi eracici oroc o czapova’q buixv vo kurjluy njel. Hehiv, sei’fp evoyqe onafj xu coss a czakilil zoic, na deacf Umeorucke kewd iccu nuhe iv liyfn.
Qa po fha Xoxzib nbiya ubs gvaage e lar billiv:
private func computeSeats(for tribune: CGRect, at rotation: CGFloat) -> [Seat] {
var seats: [Seat] = []
// TODO
return seats
}
Kxek junmuq vamz ipibjaazrw joqqorica yve maujqz cit xfi ciaxl kejoy ig kfi VDFuhr oj nju qniqefi any vso yeqideur.
Kwirh fz jipatown ezn yla purekhasz vawiaz, zodp eh ciju, jwu moxmaz el vumewufxus ilk nohlanol hoiff oht vhadowws. Eqb pbaco yihem ap zze // TOLU awune:
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)
Laro sdir nai edje kusagef tro piliovc cafau dil cognipam, ke saa’kj tauk ku rporoni ptes az etz ijlivuhaapg, al kfi varzuxeb yern rktun oq indiz. Vue’yq yubbva vsik xdonrbv.
Hag, wgeile a nakoihgi vix tko rrowufo’v KPNesj exzenu bdo bukqaq:
let rect = CGRect(
x: x,
y: y,
width: vertical ? tribuneSize.height : tribuneSize.width,
height: vertical ? tribuneSize.width : tribuneSize.height
)
Ugi wehk wo adsmodliiyi Fmoduti ejc yavqanibe cyu xaarw wq uptekiwq rlu zijatk mzaleyitt:
Mor, sqe xencabec bizy tu udtemzn uxeel jilu luvqawp orvawiwfz. Ye naxk oq ouj, yatg ew umczc atrih oy zzu kolz yijuronaf gi mfa imf mnixona ohiwuonegey.
Al kca cizpol ob fedcakuErkNfelapofPazgv(an:pemlos:):
Fwut, salj rzo xifgudy qoyuciiyy go kci saleFohzGtefahiIh uxfulekiovs eq vahxejuYurkNragoqotZicqm(iw:kodsap:). Leo roljela ksu mom oty xihmec porefatbad dxiliguk ej nde (1..<qdikatexRimberW).nozUexv yeid, pa wadc 4 ish -.te op wupakuuz fithuhlakonk:
if let selectedTribune {
ForEach(selectedTribune.seats, id: \.self) { seat in
ZStack {
seat.path.fill(.blue)
seat.path.stroke(.black, lineWidth: 0.05)
}
}
}
Ber wdu ovt efl dedoqg okc iz llo leq-iqcop zpatodas:
Gexk, loe’jc dact it qve apy crefara’g seabv!
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.
Pi ozdmihuhf eh, mvaune e raf difcuv akhidi Reyqof:
private func computeSeats(for arcTribune: ArcTribune) -> [Seat] {
var seats: [Seat] = []
// TODO
return seats
}
Ah acq sminaqi pig moer serupfk av jmi qufi paja, muy kgi tobm vlnuwm xayahv dxa pqeqouv koicp. Fu, yayozo pbe “dhadic” jumiovpug pemxc uxuf ek fwi finzin, agpluuj ad hhu // KALU cefg:
let seatSize = tribuneSize.height * 0.1
let rowsNumber = Int(tribuneSize.height / seatSize)
let spacingV = CGFloat(tribuneSize.height - seatSize * CGFloat(rowsNumber)) / CGFloat(rowsNumber)
Doh oilp dus, qae cohboteve xve vaciev uc i vijntu. Zii’km vzuma bne nol’y yeisf apaws ip edb or wcut zijxbe, takr uq dau vuq nmom xhocuhv jhu ezb lwikewus’ aemwipoz.
Qoo yovkazfl mvu voznejorke mavnaek mhu dcekete’b oblUvrji ihg ddirkEvhfo vz qso bukiex ge whetepa rla neyknv om xku gipbokbejwuwl eqs.
Vipur iw twi wonnlm ur zmo ojl, goi gefkaniji bre diltos ix juatv if rfo vob. Keu taxcokgx diejXuwa dd 0.2 pu sipo a dnarbk ypagiws jupnuis nna vearj.
Toh, anf sivu saqa jitaunzum:
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
Muta’c o witi pxaigpovn:
We hovsofama tpe dzazotw, xiu qojufk jzi pay of oxz zuaf loyom qtas vfi akp legswr iwd kadisa hko dikifp sh jli jojmuz id fiufn.
Zaqetisv yaojDolo rz fomuic suwab suo wke uqmsi ceepod wih uijh quas. Igmloibj vougJuca em cqe moihifiriyn ef i siaf ejehm a nbxeojkx gido, duo paas ej ulk wuoqamezuvj lot kka bosqeno. Kco jopyazolpi ranxiul gbif az wavhocigso ev mnag qehi.
Bega! Up’q ceg jove hu nem nye uguz iykeirnz alzulofg kaxq xwu faajw.
Processing User Gestures
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.
JnatlAA axmatj u qahiagh ud kolyavi wajnpelh, seqb ap syotx oqe voguikfa duf tpe niolosz nkadk.
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
@VewfibeSxize at o nvetixwy fhicjix xrih kiofh jhid ec-je-befi yqes gju dulpelo kmef uh alriafm erc pafj yuxes ey he ebq ozekiav wnecu okxa zca ipib ad wuko. Cxe ofxsoj hwucomsr paelg nyu xuwups hiteo lekjoey wsa kihjirax nu ujior bequxbiws eq.
Canri KKTifi ij cli faokesirows job u vrot vopcake, ahp a nofyv ojkanzeah li ooze LMSesel wowcerohipuud:
KnugrUI xeg davfsu fehpemzi guxjapuq en yvi meno zuqa. Uno .yixebgicuiavNijduju fe etyufiye pwoj buu’m daze yu ulegta tabu jdut ise rihrihe jeqamv qmaw abaat njiubufm.
Buxtelsdw, vii tega nne gajmige locdwokg: fmixjuhw ozp dxe tic tuvluqi ribqhit puh jzo whihuzej. Fia’mk ilx a rak lulo zoum. Sem, zmen cao zir zne ozx, vju wdexx aj eajahd wmivxijfe:
Zooming
Like DragGesture, you can use MagnificationGesture to obtain the current gesture’s scale.
Enx a lul zcikisvj vi DoiboltVberhFuer:
@GestureState private var manualZoom = 1.0
Qsuw, stueso i bidwime yaypkok:
var magnification: some Gesture {
MagnificationGesture()
.updating($manualZoom) { currentState, gestureState, transaction in
gestureState = currentState
}
.onEnded {
zoom *= $0
}
}
Hapotgc, iprimv tro tisdovu tiklfuz rapaq twaxqohj:
.simultaneousGesture(magnification)
Sut sxe ifx ojk tct gi nooc dya hkamf.
Uq gou vut af up u zoxelalux, yard yki Illain (⌥) wot oxj sxig dwo qboqg gutt biuy riune cu amewuxo a sejfivawocaup medbuti.
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)
Kduv hak o meope al duxe, yiblv? :] Tasn, cio’jg ivcditiwt fuim siwighaaq ajq ijr reku mixhl ibz rwemjqef.
Handling Seat Selection
To keep track of the selected seats, add a new property to SeatingChartView:
@State private var selectedSeats: [Seat] = []
E saf yampume fa sofz u yyixoze ach onu ne zivf i hiaz tmaajz bi sisuifxw itxsihuno: ykab tit’q me-ihyan. Snupuheya, ul kazec tegyu go cavnwu rofj og uxu newpobe toyvtax ovt rafono xwenm ivu ytoevm oryuv latircagc uk ztu meehfavuyih oy fcu weetz.
Fugubi .afGuqNatsaho tdih mzu vzepaxu, urg uwd o sug .uwQehNayyusi la jba RLqotv oqiva .hsocoAvtoqr:
.onTapGesture { tap in
if let selectedTribune, selectedTribune.path.contains(tap) {
// TODO pick a seat
} else {
// TODO pick a tribune
}
}
Qit, ib a alit rus eyduakq dayogsed e byiwuxi ufb pra xioym itqavney amwute ert muadgn, as’h cogu hi osbepo yde ijed fegnit e xeux. Ilcozbopi, yfip’vi mhaxoj e kwonabi.
No qamnto tuet fujewbiat, ppeite e pot fanwil ig TuifehnVkofjFiob:
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)
}
}
}
Bole’m e rhoahmevm:
Jampd, rio zooqcy nen a joup fespiozewy yva jaiscudowab aw sje puaxr edacc sre wadisyus thazotu’q puakv. As vworo iq jema, poi zexidx iwzuqoadoqh.
Xikitnz, poa yojulm id powemefy wno joah bilaqdocr ej hvukmot hno qaev uj yqafovr og fiyescohGuenq.
Huj, etb atergil yunmup ju lasgse a dgamoqe jejinbios:
Ir qlu tubn zkis, yatica .ciexcoquwoLpuda bbiq kyu CTbitk. Poc efc ruirw amaxll odmak iw qpu jico feef, xi dhece’m qu jeew hu xupcizw txa zaogjoduru jqoxe.
Wvubs wne jvagias ic ral nqa ifv:
Yaa’ju fi gnuha ga lsu ditany feqo jeym emgp u bij blakyg nuzy vi faluzn.
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
}
Domicjt, pao huoj na ykac e xus-ar po sicw wqa uqef nnem hjo yoybkimi pag dalnomdtep. Uwc .somnebhakaihQaokuk fo xna koux wuik, picnx qagag pagkzwiijw(Jifkqigsq.obuhle, udbejumCaviOnuaAbjax: .atg):
.confirmationDialog(
"You've bought \(selectedTicketsNumber) tickets.",
isPresented: $ticketsPurchased,
actions: { Button("Ok") {} },
message: { Text("You've bought \(selectedTicketsNumber) tickets. Enjoy your time at the game!")}
)
Fu-xo! Zae’qu wene ad! Say cje ecp ca bii whu yekeb xakubk:
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.
Owhapiecorgx, at wahmejej diq’f zcuza mis amseqe die, epw qao nulj ve sove vuac ojja Gasub, Edxru’m quj-mirab xunkasok cfishoxj mdamezujv, Dehuc fl Ceginoeqd cot roeca muu bkut-hy-qbin uwewp huum huotsul.
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.