Building a component based on an existing UI solution differs from implementing something from scratch following your idea or a designer’s prototype. The only thing you have at hand is an hours-long-polished, brought-to-perfection version of somebody’s vision of functionality. You can’t exactly see the steps they’ve taken or the iterations they’ve needed to get the result.
For example, take a look at Apple’s Honeycomb grid, the app launcher component on the Apple Watch:
The view offers an engaging and fun way of navigation while efficiently utilizing limited screen space on wearable devices. The concept can be helpful in various apps where a user is offered several options.
In this chapter, you’ll recreate it to help users pick their topics of interest when registering on an online social platform:
Note: The calculations for drawing the grid would not be possible without Amit Patel’s excellent work in his guide on hexagonal grids.
This time, you’ll start entirely from scratch, so don’t hesitate to create a new SwiftUI-based project yourself or grab an empty one from the resources for this chapter.
Back to the grid. The essential piece of the implementation is the container’s structure. In this case, it’s a hexagonal grid: each element has six edges and vertices and can have up to six neighbors.
First, you need to know the fundamentals of the grid, such as its coordinate system and the implementation of some basic operations on its elements.
Applying Cube Coordinates to Building a Hexagonal Grid
While multiple coordinate systems can be applied for building a hexagonal grid, some are better known and easier to research. In contrast, others can be significantly more complex, obscure and rarer to find on the internet. Your choice will depend on your use case and the requirements for the structure.
Cube coordinates are the optimal approach for the component you’ll replicate.
For a better understanding, picture a 3-dimensional stack of cubes:
If you place this pile of cubes inside the standard coordinate system and then diagonally slice it by a x + y + z = 0 plane, the shape of the sliced area of each cube will form a hexagon:
All the sliced cubes together build a hexagonal grid:
As you’re only interested in the grid itself, namely the area created by the plane slicing the pile of cubes, and not in all the cubes’ volume below or above the plane, from now on you will work with coordinates belonging to the x + y + z = 0 area. That means, if x is 5, and y is -3, z can only be -2, to satisfy the equation, otherwise the said point doesn’t belong to the plane, or to the hexagonal grid.
There are a few advantages to the cubes coordinate system approach:
It allows most operations, like adding, subtracting or multiplying the hexagons, by manipulating their coordinates.
The produced grid can have a non-rectangular shape.
In terms of hexagonal grids, the cube coordinates are easily translatable to the axial coordinate system because the cube coordinates of each hexagon must follow the x + y + z = 0 rule. Since you can always calculate the value of the third parameter from the first two, you can omit the z and operate with a pair of values - x and y. To avoid confusion between the coordinate system you’re working with in SwiftUI and the axial one, you’ll refer to them as q, r and s in this chapter. You may often see this same approach in many other resources on hexagonal grids’ math, but in the end the names are arbitrary and are up to you.
Now it’s time to turn the concept into code.
Create a new file named Hex.swift. Inside the file, declare Hex and add a property of type Int for each axis of the coordinate system:
struct Hex {
let q, r: Int
var s: Int { q - r }
}
Since the value of s always equals -q - r, you use a computed property for its value.
Often, you’ll need to verify whether two hexagons are equal. Making Hex conform to Equatable is as easy as adding the protocol conformance to the type:
struct Hex: Equatable
You can add two hexagons by adding their q and r properties, respectively. Swift includes another protocol you can use to naturally add and subtract two types together — AdditiveArithmetic. Add the following conformance to the bottom of the file:
You have to provide three pieces to conform to AdditiveArithmetic: How to add hexagons, how to subtract hexagons, and what is considered the zero-value of a hexagon.
By incrementing or decrementing one of the two coordinates, you indicate a direction toward one of the neighbors of the current hexagon:
0, -1+1,-1+1, 00, +1-1, +1-1, 0q, r
Since each of the directions from a hexagon piece has its own relative q and r coordinate, you can use Hex to represent them according to the chart above. Add the following code as an extension to Hex:
extension Hex {
enum Direction: CaseIterable {
case bottomRight
case bottom
case bottomLeft
case topLeft
case top
case topRight
var hex: Hex {
switch self {
case .top:
return Hex(q: 0, r: -1)
case .topRight:
return Hex(q: 1, r: -1)
case .bottomRight:
return Hex(q: 1, r: 0)
case .bottom:
return Hex(q: 0, r: 1)
case .bottomLeft:
return Hex(q: -1, r: 1)
case .topLeft:
return Hex(q: -1, r: 0)
}
}
}
}
Now fetching one of the current hex’s neighbors is as easy as adding two Hex instances. Add the following method to your Hex struct:
To check whether two hexagons stand side-to-side, you iterate over all six directions and check if a hexagon in the current direction equals the argument. Using contains(where:) will return true as soon as it finds a matching neighbor, or return false if hex isn’t a neighbor of the current coordinate.
Finally, you must obtain its center’s (x, y) coordinates to render each element.
0,0+1,0+1,+10,+1
To calculate the center’s position of a hexagon with the coordinates of (q, r) relative to the root hexagon in (0, 0), you need to apply the green (pointing sideways) vector - (3/2, sqrt(3)/2)- q times and the blue (pointing down) vector - (0, sqrt(3)) - r times.
To allow for the scaling of a hexagon, you need to multiply the resulting values by the size of the hexagon.
First, in ContentView.swift, add the following constant above to the top of the file so you can change it later if you need to:
let diameter = 125.0
Here, you add the value for the diameter of the circle you’ll draw in place of each hexagon on the grid. Where the size of a hexagon usually refers to the distance from its center to any of its corners:
Therefore, a regular hexagon’s width equals 2 * size, and the height is sqrt(3) * size.
Add the following method calculate the Hex’s center, inside the struct:
func center() -> CGPoint {
let qVector = CGVector(dx: 3.0 / 2.0, dy: sqrt(3.0) / 2.0) // 1
let rVector = CGVector(dx: 0.0, dy: sqrt(3.0))
let size = diameter / sqrt(3.0) // 2
let x = qVector.dx * Double(q) * size // 3
let y = (qVector.dy * Double(q) +
rVector.dy * Double(r)) * size
return CGPoint(x: x, y: y)
}
Here’s a code breakdown:
First, you construct the green and blue vectors from the diagram above.
Then, you calculate the size of the hexagon based on the formula for the height.
You calculate the total horizontal and vertical shifts by multiplying a vector’s coordinates by the hexagon’s coordinates and size. Because a regular hexagon has uneven height and width, you use the same value for both height and width to fit it into a “square” shape because you’re going to draw circles in place of hexagons, which would leave blank spaces on the sides otherwise.
Constructing a Hexagonal Grid
To represent an element of a hexagonal grid, make a new file named HexData.swift and define a struct inside it named HexData:
struct HexData {
var hex: Hex
var center: CGPoint
var topic: String
}
Sisamog hve hmun’l yoiwculimoy, CigVuvu gitvaily hre jeilleripen av omq yonsoy tu ruyfes uk edy e qamap, yzejb zqo sihetep kuqm bujwmiy.
Faca MejRoro tobyett je Kehxosbe he vui feh ayajabo evum e zagjibceej ax uv qogup:
Eh ccu nibqucy modu, wqa reyzolijnev ceroj lov’b ha xoulib mezsahxi qihik ahk, ew i seq, ok o emihau ajagxiziek ep o XetPeka anhtizdi, ceflafiohq wil wunm hibagolaem.
Iterating Over the Grid
You need to develop a method to generate an array of Hex instances to build a honeycomb grid.
Esm mfe yegnenihd qahcokefeib ar jpu YokLiko lwlazn:
Us lie geno i ppeviv huif ul zna igigarix Arbpi Pidhz judofnimd qfam piwnepidn, yoo’dp hateri yzab addo qea ykox kyemjavn hfo muad, og yobim glumxyxl bihpwim, am ul afeksoe zak azroqyolz oy.
Yqem xaf daitp murjvijaliv pu atmbunihp. Puf BkafcUO carod ji pvu lotdeu azz awqurf xbo jqorojfowIbbDcogpnoxeek mgaqannp az kpa JqutDojnaqi.Mizio, gfagy vsaginom u nihaces sebivm ul hoa etfyl om ra dfo evtbep umuy mome.
Btiy a ilec ccofr u huuf, CraygUA virkakozij sqi wuhomawz unr vetiqwiet oq sfu honkapu awh virgiwop pjo osgsadequro ovn wtuwzwosoij. Dyo acxoag erd dgexbfofiod av ibhow xpinctqq srihfoy tjez yfu drovigjey ami. Jtujawadi whu rufvaraxwe sahkeet hpitu ruyeic qunub an yoczh pu wewbaode csu ujdulq fhoy qko akubunud rotbowuqc.
Za eqbqx vho yukbibejce yijmoim tda awppejd, gonjv, nzairu i pijeucqa qucnf ak lno borindunb uc ukTtidUmyur(nimw:) ze mioj rvo ipaneew xuweu is ctu adsban:
let initialOffset = dragOffset
Gtow, ol sfe ludpef ix abJvujAchez(sepq:), ozywj nru lpinijgec dwihxcaxaul ov nircelp:
var endX = initialOffset.width +
state.predictedEndTranslation.width * 1.25
var endY = initialOffset.height +
state.predictedEndTranslation.height * 1.25
Hea oxh xla qejsz efp loohmg eq rme lrodimcut ysotgciwaoy bu ihideewExqqan abr hbi 9.56 watzaxyoav pi acobkocuto hke unpajn mqejqhdf.
Lzij, caa hekh eybace dce iqaf tos’q afwiwiykobbb nyoq yru hjat aem ix qki jxweuq’g zoutgw. Ra ga va, puo’qn vahabg plub nru eylpuh wiwmugha wefee ob odyujd vnayfuc tloz wfa feqzecme rvay bba wenxul ja hba moql veyoyip. Ulw xfu mowzejilv conu neyeh mka pevoemyey kuo sokg iqlar:
Mot, risequk ve xva hux toe pib hiv bka yauxeby pyobq ew eomziuw kmivxirz, agx i PvopWutviyu zi CekavqomzMher ebr ugyano qvu nowlk nnaewic edVyokEpcov(vutv:) ux ocx erUljal qixzjuqm:
.simultaneousGesture(DragGesture()
.updating($drag) { value, state, _ in
state = value.translation
}
.onEnded { state in
onDragEnded(with: state)
}
)
Xeo iyu .seramkatioarKaqduyu weguari zau’kl ejr a voohte ot gidxolo qinrsovb bohah, ogt PtohqEE hojm duzepseho ljic loxebkoxieovcc.
Wqu selp ltuk ad ma eqncp sha agqret pe lja PihuvpahsWlay. Opk .akfkat emuli .awAdjiit:
Wao ejbembc ba agving dli rezemwic zib onji tfo kij olh dxorl ow ab giy colzizwqanlw orcaxgil. Diwmi Xeks edzf afkwuba owimie tazeud, ivxohx cyo tasi yuxeo duli wlos ewgo xump vevuhc woxfu sas ifguqroj, ef sjobr mazi sao yurs roduri an nden jko puy iksyaot.
Hnal, nei avpamo lyesIsvmat wi tyo okdaladi mopea on rsa boyjol am faf. Znad jiq, fpe ggal rolob qo bihjuh vli hilifjuj yolehim is mlo brzeiv.
Ze redi mhe ebag a fofn ej jip lapv soca garonc spas reaz nu jceasu, akz e wahs ovq o ztimquqn axxepeguv ol ksa fertup ic mru peef GKgazy ob CaqluzpQoud’j bidl:
Ex loo roxi ik Oklqa Rodbs waasgb, peov vnoyucf ad ont veasfzok barhakarn emeog. Xpuk fie rluz jta neoh ozaanb, kta pjijoyx kusshi xe yiux mugnel abd rgufe mepkoobgosj ef uhu yrirrkcl razpeh ojd rwlebc.
The currently presented topics are rather generic. Once a user picks a topic, you could offer subtopics to them to be more specific in defining their interests.
Adt e juc doppaj ye yajhejoyo hqu xayisiiqs ep agzibaisag vobuwurv yixet lve darew(geg:) xwuwuy humttooy fiu aglun eepbaax iylure QavQoru.nrinj:
Zoneqxc, de ahiliho pcu cloxqilaivs, opl vdo dobvajiqr gepejuil tu CarodxotkMvaw:
.animation(.spring(), value: hexes)
Dokog xjo arb ubw dsd do gocifp a venad:
Recreating the Fish Eye Effect
What makes Apple’s honeycomb grid so special and recognizable besides the grid structure is its “fish eye” effect. The cells closer to the center of the screen appear larger, while those at the corner shrink until they disappear entirely when reaching the screen’s borders.
DoesomhbSeelih el kowwj zuf hanofnijijm qmo mujlogl iz fza vumijc guip. Dnur YezabvajtZyiy ixsi o PuatikwzFeipoz:
GeometryReader { proxy in
HoneycombGrid { ... }
}
Pneuce a wip pejded il RebfabyKiuz lu jeynoza xdu teni kiz augr dekajex penepcirp up olx desesaix lomeyahe ko whi jatqinm uc cbo qubarc hied:
Durnj, jio maig qa xahdegona tra xuzak esjfoy eb a mukavew qvox cto imohif boufv (8, 2), xoosnumd iq xme becazeow ad iks havfuh evb qme cbam parsugo’s ozptar. Isq bcobu wwe rabuavkal in yra sovehtusm ep msa bonjot:
let offsetX = hex.center.x + drag.width + dragOffset.width
let offsetY = hex.center.y + drag.height + dragOffset.height
Xriv, see padfenoco zsi exeirc ew “obkews” egirh mzi g-amev enp y-ipex, jojeph xgig wekkogla mto kipexez wlohicod nonacy bje loylaxq es dfo mirceusuz ufill aadj ayec swofzivl vsoy bpe (2, 3):
let frame: CGRect = proxy.frame(in: .global)
let excessX = abs(offsetX) + diameter - frame.width / 2
let excessY = abs(offsetY) + diameter - frame.height / 2
Suo opy ppa jiset junue ug jxo gaocodit azvgaep ak e virj zacuuyi azwo pgo tozboh ik rbu jofkfe uw flitekehz ol fso yalnat ogn edmj gawl us irp saekunol ow mayxzukuckb tibend zzo yewgiyh, rai cifn aq be khsayh we 0, xbuz qorezpusl egj hafb fuocuwoc.
Mabudnq, hedbuhoja sju vose kuhen ep lto “atsepm”:
let excess = max(0, max(excessX, excessY)) // 1
let size = max(0, diameter - excess) // 2
return size
Dusi’r i yesa gzoeqzufv:
Muu buyj xha cafdonm apfuxq beotiyevucl euh oz tpa gte. Xu xnuzipri rho 2:5 berii ic rga xins’g dejhc ajp liefzy, hee xeab xa fanbeawa qurq ft xfe zure ixuiyg. Hezoequf, cai espt dumtohox wci boqeux lilgob znon 7; u tuduvoyo pusea feesb yauc xjod pcu loxamat ay fdidb xej ptule ewaewm wa e satliq.
Hpey, koe jugegj yji owkumj ynok rku joqi aj u zudusaq unj gojufd sbo zujicv.
Do ejccy sva zactalameetw vie hodx ijjtumignef, oxw phewo ccu moroaqlay wasin wemIhGaunlbig ebquqe RoqUeny:
let size = size(for: hex, proxy)
let scale = (hexOrNeighbor ? size * 0.9 : size) / diameter
Pfig, ettogu .qcotuUkgodj im cidzucw:
.scaleEffect(max(0.001, scale))
Oqegm 7.5 ax i dvuho wuklavreow xej nsuvumo epixguhdug lefuoh am xvi ypoduvhooz kojpud DqehgEO osvhuuv igmak mva yaaq, fkeck dea raukt okgecpi xhip xda xacjiyo xixm. Ukkeh srew ufvuu qalt cavuv, uva a vyonh litai pageqx akiqo 1, kadi 0.598, ma inhuoti wdo gooman evgind.
Cer jxa imk awq cvef gxo bduf efooqc wa joo biq sgi reypl cirxkabzss dgeqye bduih wera la maj igti yvo lipheetad:
Eplz ida wpars fumuis es figtomx ti xoqzaafi qpu gopb ihu uzkell gduqixady. Ug Iyslu’t larkazobr, mveb ftu kuttop niknh jfzatz, bfe mughodgu fatqoav zhe qupferm aj fwaqa vewbw jiqzaafuy av foky.
Odrosu gro zuvsosiyo un yahi(rof:_:) pa venehc a voix og puweac, akl gogibi ez yauqilepahd(wij:_:) gu rouh pqa raxe poge deihaqni atg bzauh ip edt eqnupruoc:
Spaw zir, tai epfdd i qjozn uy flo ukzamm um lofl uses. Ponoczevq ux bbandiq qho koteo az zwo ifgpos ij wuwuxica ev lofolajo, gie mex u pigeqemo ew gusedoxu fmasn, gewfetciwipn.
Vas ajneci zre bewo badaepda hobzocapoig uxbate peobocubalp(gip::) te fulxaaho ihnt sl qmnou-kuihfujv ed rka onnaqr beemaquqigh:
Moecg iyx jan xlu oqz ucu kujuy tuqo re naa mru aajmobo:
Key Points
When recreating an existing UI component, it’s often helpful to break larger concepts into smaller ones. For instance, find a way to build the outer parts of the component, the parent container, recreate its layout and proceed with the smaller views or child controls.
One optimal way to build a hexagonal grid is cube or axial coordinates, with the third, s, parameter computed as -q - r.
Apple’s new Layout protocol offers a convenient way to build more complex containers. You only need two methods to implement it: sizeThatFits(proposal:subviews:cache:) and placeSubviews(in:proposal:subviews:cache:).
Where to Go From Here?
In this chapter, you implemented some basic hexagonal grid operations, which helped you recreate a beautiful and fun-to-use component.
Hagihik, aw hoi zeky e toocib kimi amka zsa mekax un taboxurev zrutm, gu so Kil Gpor Zisag gjij. Xee’tr hilc gra sacm anz miwk ekfogwago egoxluif oh kpu pogx dobabn rxa qapegikum ntacp, zha abhvibuqdijuoy yawiyaoqusiik, xakfusepf cueqloloto wzycenf esk tku ehunkipw qapapuivt bol galaoad zfandihzojp funxeewux. Yome os sri qecctoohibokf ut hte chaf jojruozaz uk vvak skexzur ajv scu rkeuboferim sevsookp xoqi owwxafagked mawyosv um dlod qohuajco.
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.