Building a Beautiful and Responsive Detail View in SwiftUI

Darshana Jaisingh
Swift Universe
Published in
6 min readSep 7, 2024

--

Have you ever wondered how to build a responsive product detail screen in SwiftUI that looks great across devices? Maybe you’re working on an e-commerce app, and you need a sleek product page with images, seller information, and a sticky “Add to Cart” button that always stays visible. This article will guide you through building such a screen step by step, starting from the layout basics to implementing features like a scrollable product description and a price section that sticks to the bottom.

Let’s dive in and see how we can achieve this!

Hello Readers!👋 Welcome to my blog, where we turn complex programming concepts into easy and understandable stories. If you’re just beginning your journey or seeking to expand your skills, I’m here to help guide you through. Let’s learn and grow together, one line of code at a time. Enjoy the read! 🌟

Imagine you’re working on a flower shop app, and you want to display the details of each flower — showing an image, the seller’s details, and of course, a button to add the flower to the cart. But there’s a catch: you need to make it look good on all screen sizes, and ensure that the price and “Add to Cart” button are always visible at the bottom, even when the user scrolls.

Sounds like something you’ve run into? Well, that’s exactly what we’ll build today.

Before we start breaking down the code, let’s discuss some key SwiftUI concepts you’ll encounter:

  • VStack & HStack: These allow you to arrange views vertically or horizontally.
  • GeometryReader: A powerful view that gives you access to the parent container’s size, enabling flexible layouts. You can read in further in detail about Geometry Reader here.
  • ScrollView: To make content scrollable when there’s more information than what fits on the screen.
  • State: To manage whether the product description is expanded or collapsed.
  • Modifiers: Customizing views with properties like padding, background, and more.
  • ZStack: Overlap views, like putting an image behind a transparent overlay.

Step-by-Step Code Breakdown

Step 1: Setting up the Image View

We want to display a flower image with a subtle transparent background. This can be done using a combination of a ZStack, a Rectangle with a material effect, and a resizable Image.

ZStack {
Rectangle()
.fill(.thinMaterial)
.frame(height: geometry.size.height * 0.55)
Image("purpleFlower")
.resizable()
.frame(height: geometry.size.height * 0.55)
}
  • Rectangle: Filling the rectangle with the thin material effect makes the background subtle.
  • Image: Resizing the flower image to fit a specific height based on the screen size. This kind of height configuration will ensure that the UI remains consistent in devices of different sizes, like the iphone and ipad.

Step 2: Product Information and Seller Details

Next, we add the product name, rating, and seller information using a VStack for vertical alignment. This also includes a custom SellerView that displays the seller’s name and occupation.

VStack(alignment: .leading) {
HStack {
Text(flowerItem.category)
.font(.subheadline)
.foregroundStyle(.gray)
Spacer()
Image(systemName: "star.fill")
.resizable()
.frame(width: 20, height: 20)
.foregroundColor(.yellow)
Text(String(flowerItem.rating))
.font(.subheadline)
.foregroundStyle(.gray)
}
.padding(.bottom, 5)

Text(flowerItem.itemName)
.font(.title2)
.fontWeight(.medium)
.padding(.bottom)

// Seller information
SellerView_(sellerInfo: flowerItem.sellerInfo)
}
  • HStack: For the product category and rating, arranged horizontally.
  • SellerView: A reusable component that shows the seller’s name and occupation along with buttons for messaging and calling.

Step 3: Expanding and Collapsing Product Description

Now, let’s add an expandable product description. This is done using SwiftUI’s @State to track whether the description is expanded or not. We’ll also use a button to toggle the expansion.

Text(flowerItem.itemDescription)
.foregroundStyle(.gray)
.lineLimit(isExpanded ? nil : 2)

Button(action: {
withAnimation {
isExpanded.toggle()
}
}, label: {
Text(isExpanded ? "Read Less" : "Read More")
.fontWeight(.medium)
.underline()
})
  • lineLimit: Controls how many lines of text are shown. We toggle between 2 lines and unlimited text.
  • Button: Changes the state between expanded and collapsed, with a smooth animation.

Step 4: Sticky Price and Add to Cart Button

Lastly, we want the price and “Add to Cart” button to always stay visible at the bottom. We achieve this using a custom StickyBottomView that displays the total price and a button.

StickyBottomView(price: flowerItem.price)
.edgesIgnoringSafeArea(.bottom)

Here’s what the StickyBottomView looks like:

HStack {
VStack(alignment: .leading) {
Text("Total Price")
.font(.title3)
.foregroundStyle(.gray)
Text("$\(String(price))")
.font(.title2)
.fontWeight(.medium)
}
Spacer()
Button(action: {
print("Add to cart clicked")
}, label: {
HStack(spacing: 12) {
Image(systemName: "handbag.fill")
.resizable()
.frame(width: 20, height: 20)
.foregroundColor(.white)
Text("Add to Cart")
.fontWeight(.medium)
.font(.title2)
.foregroundStyle(.white)
}
.padding(.vertical, 15)
.padding(.horizontal, 30)
.background(.white)
})
}
  • HStack: For laying out the price on the left and the button on the right.
  • Button: With custom styling and padding to make it prominent.

The Full Code

Here’s the complete code for the view:

import SwiftUI

struct FlowerItemDetailView: View {
let flowerItem = SampleData.sampleFlowerItem
@State var isExpanded = false
var body: some View {
GeometryReader(content: { geometry in
VStack {
ScrollView {
VStack {
ZStack{
Rectangle()
.fill(.thinMaterial)
.frame(height: geometry.size.height * 0.55)
Image("purpleFlower")
.resizable()
.frame(height: geometry.size.height * 0.55)
}

VStack(alignment: .leading) {
HStack {
Text(flowerItem.category)
.font(.subheadline)
.foregroundStyle(.gray)
Spacer()
Image(systemName: "star.fill")
.resizable()
.frame(width: 20, height: 20)
.foregroundColor(.yellow)
Text(String(flowerItem.rating))
.font(.subheadline)
.foregroundStyle(.gray)
}
.padding(.bottom, 5)

Text(flowerItem.itemName)
.font(.title2)
.fontWeight(.medium)
.padding(.bottom)
Text("Seller")
.font(.body)
.fontWeight(.medium)
.padding(.bottom, -2)
SellerView_(sellerInfo: flowerItem.sellerInfo)
.padding(.bottom, 15)
Text("Product Details")
.font(.title2)
.fontWeight(.medium)
.padding(.bottom, 5)
Text(flowerItem.itemDescription)
.foregroundStyle(.gray)
.lineLimit(isExpanded ? nil : 2)
Button(action: {
withAnimation {
isExpanded.toggle()
}
}, label: {
Text(isExpanded ? "Read Less" : "Read More")
.fontWeight(.medium)
.underline()
})

}
.padding()
.background(Color("BackgroundColor"))
}
}

StickyBottomView(price: flowerItem.price)
.edgesIgnoringSafeArea(.bottom)
}
})
.edgesIgnoringSafeArea(.top)
}
}

#Preview {
FlowerItemDetailView()
}

struct SellerView_: View {
let sellerInfo: SellerInfo
var body: some View {
HStack {
Image(systemName: "person.circle.fill")
.resizable()
.foregroundColor(.gray)
.frame(width: 40, height: 40)
VStack(alignment: .leading) {
Text(sellerInfo.name)
.font(.body)
.fontWeight(.medium)
Text(sellerInfo.occupation)
.font(.subheadline)
.foregroundStyle(.gray)
}
Spacer()
GradientImageButton_(imageName: "ellipsis.message.fill")
GradientImageButton(imageName: "phone.fill")

}
}
}


struct GradientImageButton_: View {
let imageName: String
var body: some View {
Button(action: {
print("Button Clicked")
}, label: {
Image(systemName: imageName)
.resizable()
.foregroundColor(Color("AccentColor"))
.frame(width: 20, height: 20)
.padding(10)
.background(content: {
Circle()
.fill(
LinearGradient(colors: [Color.gray.opacity(0.3),
Color.gray.opacity(0.1),
Color.gray.opacity(0.3)],
startPoint: .topLeading,
endPoint: .bottomTrailing)
)
})
})
}
}


struct StickyBottomView_: View {
let price: Double
var body: some View {
HStack {
VStack(alignment: .leading) {
Text("Total Price")
.font(.title3)
.foregroundStyle(.gray)
Text("$\(String(price))")
.font(.title2)
.fontWeight(.medium)
}
Spacer()
Button(action: {
print("Add to cart clicked")
}, label: {
HStack(spacing: 12) {
Image(systemName: "handbag.fill")
.resizable()
.frame(width: 20, height: 20)
.foregroundColor(.white)
Text("Add to Cart")
.fontWeight(.medium)
.font(.title2)
.foregroundStyle(.white)
}
.padding(.vertical, 15)
.padding(.horizontal, 30)
.background(.white)
.shadow(color: .black.opacity(0.2), radius: 10, x: 0, y: -5)
})
}
}
}

You can now copy this code into Xcode and experiment with it yourself. Feel free to change the images, tweak the layout, or expand on the seller details.

Note: Set the Accent Color and Background Color (here white) in your assets file and also add the boquet image of your choice in the assets file before pasting the code.

Conclusion

By following this step-by-step guide, you’ve learned how to build a responsive product detail view in SwiftUI. We covered how to use essential SwiftUI components like VStack, HStack, and ZStack, and explored features like scrollable content, state management, and sticky bottom views. Hopefully, this project gives you inspiration for your next SwiftUI app!

Let me know in the comments if you try this out or have any questions — I’d love to hear your feedback!

If you found this article helpful, show your support by giving it a clap 👏 and sharing it with your network. Together, we can learn and grow as developers! Don’t forget to follow me for more such programming insights.

If you’d like to further support my work, consider contributing via Buy Me a Coffee. Your generosity keeps the content coming! ☕️

Happy Coding!

--

--

Darshana Jaisingh
Swift Universe

I'm on a journey to master iOS/macOS development. Let's code together, explore new ideas, and grow together in the world of Apple development!