SwiftUI series 10: Building a Better Fruit Search UI in SwiftUI: Custom Components and Search Implementation
In this tutorial, we'll explore how to create a polished search interface with custom components and smooth interactions. We'll break down the implementation into three main parts: FruitRow, CategoryButton, and the main BetterSearchView.
1. FruitRow Component
First, let's look at our custom row component for displaying fruit information:
struct FruitRow: View {
let fruit: Fruit
var body: some View {
HStack(spacing: 16) {
Text(fruit.emoji)
.font(.title)
.frame(width: 50, height: 50)
.background(Color.gray.opacity(0.1))
.clipShape(Circle())
VStack(alignment: .leading, spacing: 4) {
Text(fruit.name)
.font(.headline)
HStack {
Text(fruit.category)
.font(.caption)
.foregroundColor(.secondary)
Text("•")
.foregroundStyle(.secondary)
Text("Vitamins: \(fruit.vitamins)")
.font(.caption)
.foregroundStyle(.secondary)
}
}
}
.padding(.vertical, 4)
}
}
Key Features:
- Circular emoji container with gray background
- Clean layout with fruit name as headline
- Secondary information displayed with proper spacing
- Consistent padding for optimal appearance
2. CategoryButton Component
Next, let's examine our custom category filter button:
struct CategoryButton: View {
let title: String
let isSelected: Bool
let action: () -> Void
var body: some View {
Button(action: action) {
Text(title)
.font(.subheadline)
.fontWeight(.medium)
.padding(.horizontal, 16)
.padding(.vertical, 8)
.background(isSelected ? Color.accentColor : Color.gray.opacity(0.1))
.foregroundStyle(isSelected ? .white : .primary)
.clipShape(Capsule())
}
}
}
Key Features:
- Capsule-shaped button design
- Visual feedback for selected state
- Consistent padding and spacing
- Reusable component design
3. BetterSearchView Implementation
Finally, let's look at how we bring it all together:
struct BetterSearchView: View {
@StateObject private var viewModel = FruitViewModel()
var body: some View {
NavigationStack {
VStack(spacing: 1) {
// Category Filter ScrollView
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 12) {
CategoryButton(title: "All", isSelected: viewModel.selectedCategory == nil) {
viewModel.selectedCategory = nil
}
ForEach(viewModel.categories, id: \.self) { category in
CategoryButton(title: category,
isSelected: viewModel.selectedCategory == category) {
viewModel.selectedCategory = category
}
}
}
.padding(.horizontal)
}
.padding(.vertical, 8)
// Fruit List
List {
ForEach(viewModel.filteredFruits) { fruit in
FruitRow(fruit: fruit)
}
}
.listStyle(.inset)
}
.searchable(text: $viewModel.searchText,
placement: .navigationBarDrawer(displayMode: .always),
prompt: "Search The Name Or Select Category...")
.navigationTitle("A Better Fruit Search")
.navigationBarTitleDisplayMode(.large)
}
}
}
Key Features:
-
Navigation and Search
- Large navigation title
- Persistent search bar
- Custom search prompt
-
Category Filtering
- Horizontal scrolling category buttons
- "All" option to clear filters
- Visual feedback for selected category
-
List Implementation
- Clean, inset list style
- Custom row components
- Smooth scrolling experience
Putting It All Together
The view hierarchy creates a seamless user experience:
- Search bar at the top
- Horizontal scrolling categories
- Filtered list of fruits
- Custom-styled rows for each fruit
Best Practices Implemented
-
Component Separation
- Reusable components (FruitRow, CategoryButton)
- Clean, maintainable code structure
-
User Experience
- Smooth animations
- Clear visual hierarchy
- Intuitive filtering system
-
Performance
- Efficient list rendering
- Minimal view updates
- Smooth scrolling