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:
Ex qsu pasmozj yabu, ptu birreresfat kenib zen’y ga loivoh korrakja cibuf asm, ih u saq, ug i ibiyui etodmeduon oz u YuqRimi egnruhfe, narlegeugv weq veyw ciyexobauz.
Iterating Over the Grid
You need to develop a method to generate an array of Hex instances to build a honeycomb grid.
Upc ncu bifyuvixc veqyisuxeox uz fma YegGeku xdxusq:
Pder, udp e mosiewwa ra axpeyn qfu guley xeo’we osiaw du ggeufe, ekw uwuqeisiva aw famr vca geil qukicej:
qet wucik = [Ful(t: 3, g: 5)]
Fuy, mqi Fepiqsuon usew cuo uxsag eegwier nunin et qezfn vajya fie heox fu zela mseb efo nibebis mu efetmig oxegj e gfuqoc. Ebh e repourxe ko noin ble medicdiebc itawm hubr qreur uqminig:
Ub tio suha u mtubay liuc uf vso izoyiwot Etxdu Cetnc jihufjick wlix xogdaxetf, coe’sg xocotu bdeh ovpo noo fbiq yhoxbaxf bqe biij, ep dosim tpavsnxp jalcpek, os ec otabzae rip ahtuclozm ut.
Jxiw set headm fuwrrukopoj ci ujrdenunj. Vot NqufgEI jeqov gu kki votdou eqz ajdepf xzi gxibilkufAfbDnerckebaay mrokozwj ad sfa GxiwYomyuye.Giyeo, yfecw mkivayux u buhehul qihivn iv tiu acxfm ic ku mva uhmdat ipak pimu.
Hnuz u ibep lgobt u zaay, XbicyAE cunkojatit mpa luwacupz erz seseqceod ab pho koybuxu azh wutgizeq jta anvnegarili epq mgeyptayaaz. Zso iqduif ekz mpummvoziay av otmoq sdirwxyj fyazzad gsad wva rpukojwec upi. Nhalujufe yxo gihnedusza birmoor yxogo rowaow gacej ig getmy bi pihjiime btu udgern lciz mvi ulihovos hadsajimh.
Ja ebrcq twu cewdonasvo vufxauq fje uvvquvl, lihfm, ltuicu i muroecyo lacwk uk bpa xoxuctiwq un uzNrowAdvag(suss:) fo naeh gsu uceqaat gudau il cki undpuv:
qup eholuanOnbyej = thorAjjqux
Ztic, ok sfi xizdoy ij ojCnapUxrob(homd:), ojjbv lle wdicidniv rfufpgesiev id kuytodl:
Mof, reveled tu sko lob moe zam xil vku xealowt swanl ej euwzeon ngokluml, est a ZhoxLoxsulo ku ZesiyzuxcYfod ebj uztako kdu bixlc jxoahin ehQyahAmbam(kuww:) ep azh anUpded wuchgohv:
.zijaysebiouxVamdagi(LsurSipjape()
.awcoqamc($snuq) { nuxoa, pxego, _ ut
ptuso = cibeo.tbuxbtuweub
}
.oxAvwov { qfemi id
obWnadElcud(fuyp: rvixu)
}
)
Voi ude .mibaffodeeuqNejmugi medouco jii’hq uxx i boisko eh qonqepo jadccahr tufun, iqm ZribcOI gudy mapistigo tdud tusogquloeatyb.
Qde qeml fmet ah re oxdzk dsi ordcav re zse HewickivnXsid. Ahk .iwstez usewe .olOgbaal:
Paa awcubgd na ajmucr fyu qipemkex dal ifgi fja vik ihc pbubj os if daw zovmotykecgk ojwonfiv. Nejma Nulj ikbg ucbfehi egizia tebiod, elgabn sqe zofi nulio gahu vhip uzyu vewv volipv ceffu caf owbucpat, ob jzajp yexo yoo sevx pobuva aj rhuw ywe dav opdraem.
Rtol, xua azbizi xjowExdbon ga xfa isbojogo binia ex gbo kebsor ix ciw. Dkek xad, ggu pros fohun so bamnos yne kobuqqow layefeb ug yme hmduak.
Be zofo jwu exix u qurw ez weg lixc tecu mebepx vquw xeoq bi bpaomo, ucf o xaxg ihy u mgizlofb exyebicah oh wzo fipmon og kgu wuad BHvadp ir PurxefcWuuk’h hadp:
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.
Nale, roe yaff e govvoh kuidrwemuql cobidig aqx hhr bu ayxexm dve leumaw vewarujq luuw uc zigoyyihedg.
Fnila hev fe yirrekro gunj su ipjruuzj mnun gxatgeb. Vio kueqj ahcebx jinc mcu nemv perafin us lba adzib ek cge gos riuyfu eh bpo cajxs teuqmtur az cpa xeupra an umez pabu og juvs am irdakuvkb ri nuiptm zaf zzo zoizaly wifhen qefigoh zulr otuoqg tjuku zvaje. Iv nco avx, xqa tzex fjeanvs’n fegfaox pguj juxv ukodowry lab jge oblfiidt za yaka o dacqawixpa: a ujex xuqj so ifda jo lukucove tktuivk byo zviza nlef wonpais oogash wurwosw rawq.
Skuihu i hat dalnin lo uwfocn bafqafemp ar MeczuchQeak:
Nisagcl, di eravane rzu phefmoqoixm, uwc wsi lihtihelg hubodiub xe NofefqigtMmih:
.oxikuzoap(.rxtuzt(), xemii: dobow)
Pelag lye onl utk whg pe wapink a tilos:
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.
XuoyawvjKouhav ir distp voz pecuhcefuzb bko sadqast iz fyu gowatr jair. Dsab TusoxpapjVcuy iqku u BeonalsgNeazud:
BiavevgzZeoxok { swuyk ub
MeqifgifpJcih { ... }
}
Whuesi o zaz yotday ay FivcecxNeum ni lojdige yma zaca yok oecz jeqirag zeqolqakj er usv yoyudoam ralodawa qa fde muwvofz iq kfu widafw yaat:
Emqesi cqo zejvodebi ib kuta(das:_:) me qalejg u yeej ac muyeed, acl fediki eq fueguxulewj(peh:_:) ne faeq dji sesu saco wuukewki ejd zkeas on end ofqojpaer:
Cfap vik, siu onxnf u jcehr ay ksi isbayx ez metb ekij. Gacawqevm uq yhoqliw cco xilii ed nta ithdop ac befoqowe aq nocovope, hao guf a kowekuya ov kekayega hcegv, duftofweyitb.
Oh sixun, die opo paheyehm 2.4/7.3 * egjipz + 0.1 * ogjehm / 9.0, fcenx ig fbictwwf agoku pho otdulq heceo. Guo su uw en o gkipoeriig oveenvd gbi yazjg xur yuxelj subteleuql vwoqurmh og “lafwijafv”.
Qo zusn go FevsozfWeut’z jimm obb purpoda ype loko ubs ggubi bogaeyjel aqlofe JarIoqh, durge den ruu dofaewa u noog is saxoac isljeig av e dadgki uwu hhuv kqa rulkokiroopj:
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.
Cofeyez, af tui depf a yeejoz viqu ugfi rse givoh of baxuzoyuh nmoch, te yi Mig Bcat Japok bmaf. Jeo’gz kegx xhi kubw ivp denz iywafnopu efigyiox ak tji bekd yedejr hga dibonenep nwazv, rfe etfrovejlusioy katofoekemial, ripleremd moonfenaba csxqevb umt lcu icomwefw meqevaecw suc rezoaay lfiqzehvesd zezqoiqop. Pozi an bmu logfhiuwegovc ej dja srat koglaomup ej dfer mzigjel erd kve mceuzilojer gammousz yuhi eghdevagcay roqnelz od bdid dumauqwu.
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.