In this installment of our SwiftUI tutorial series, we’ll build out our Categories view.
Posts in this series:
- Part 1: Project Setup
- Part 2: Category Card View
- Part 3: Categories List
- Part 4: Dynamic Categories & Navigation
- Part 5: Profile View
Here’s what our prototype will look like at the end of this section.
Now let’s get down to business.
:)
To get started, we’ll create a view that contains each row of categories and
call it CategoryRow
. Inside this view, we’ll include two instances of
CategoryCard
within an HStack
.
struct CategoryRow: View {
var body: some View {
HStack { // Like ZStack, but positions views horizontally
CategoryCard()
CategoryCard()
}
}
}
Now we can use our CategoryRow
view in our Categories view. Let’s add four
instances of it within a VStack
.
struct Categories: View {
var body: some View {
VStack { // Like HStack, but positions views vertically
CategoryRow()
CategoryRow()
CategoryRow()
CategoryRow()
}
}
}
Now we should have four rows of Category Cards visible.
We want to be able to scroll through this list, so let’s put our VStack
within
a ScrollView
. While we’re at it, we can add some default padding to our
VStack
to give our content some room to breathe.
struct Categories: View {
var body: some View {
ScrollView {
VStack {
CategoryRow()
CategoryRow()
CategoryRow()
CategoryRow()
}
.padding()
}
}
}
We should now have a scrollable list of Category Cards.
This is looking great already, but we want each Category Card to have a rectangular shape. You might think we could just change the hardcoded height and width arguments in the frame method on our Image. However, this solution wouldn’t account for various device sizes.
In this case, we want our width and height values to be dynamic, so we’ll use
GeometryReader
. GeometryReader
is a container view which provides access to
its own size and coordinate space. We’ll use a fraction of the width component
of this size to set the width and height of each Category Card.
We also we need to instantiate GeometryReader
outside our ScrollView
or it
won’t behave properly.
Let’s start by doing that.
struct Categories: View {
var body: some View {
GeometryReader { geometry in // Add GeometryReader
ScrollView {
VStack {
CategoryRow()
CategoryRow()
CategoryRow()
}
.padding()
}
}
}
}
Great! Now we have access to our screen size in our Categories
view via
geometry
. Let’s pass geometry
down to our CategoryRow
view.
struct Categories: View {
var body: some View {
GeometryReader { geometry in
ScrollView {
VStack {
CategoryRow(geometry: geometry) // Pass geometry to CategoryRow
CategoryRow(geometry: geometry)
CategoryRow(geometry: geometry)
CategoryRow(geometry: geometry)
}
.padding()
}
}
}
}
Since CategoryRow doesn’t take any arguments yet, we’ll have to add a parameter
geometry
of type GeometryProxy
.
struct CategoryRow: View {
let geometry: GeometryProxy // Add geometry parameter to CategoryRow
var body: some View {
HStack {
CategoryCard()
CategoryCard()
}
}
}
Now we have access to geometry
in CategoryRow. We’ll repeat this process to
pass geometry
down to our CategoryCard.
struct CategoryRow: View {
let geometry: GeometryProxy
var body: some View {
HStack {
CategoryCard(geometry: geometry) // Pass geometry to CategoryCard
CategoryCard(geometry: geometry)
}
}
}
struct CategoryCard: View {
let geometry: GeometryProxy // Add geometry parameter to CategoryCard
var body: some View {
ZStack(alignment: .bottomTrailing) {
Image("business")
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 200, height: 200)
Now, we can finally use geometry
in place of our static values for our frame
width and height.
Image("business")
.resizable()
.aspectRatio(contentMode: .fill)
.frame(
width: geometry.size.width * 0.45, // Set width and height to a fraction of view width
geometry.size.width * 0.55)
Nice! Now we have a list of Category Cards. See you for Part Four, in which we’ll make our categories list dynamic and add some navigation.