How to Use CenteredHorizontalCollection framework in Your SwiftUI Project
The CenteredHorizontalCollection is a powerful SwiftUI component that provides a horizontally scrolling collection with automatic centering, smooth scrolling physics, and enhanced user experience. This article will guide you through implementing and customizing this component in your SwiftUI applications, using the techniques demonstrated in the DemoView implementation.
Table of Contents
1.Basic Implementation
2.Customizing Item Appearance
3.Handling Selection
4.Configuring Collection Behavior
5.Advanced Theming and Visual Styles
6.Performance Considerations
7.Complete Implementation Example
Basic Implementation
To get started with the CenteredHorizontalCollection, first import the package and create some data to display:
import SwiftUI
import CenteredHorizontalCollection
struct MyView: View {
// Create some sample items
let items: [Item] = (1...10).map { Item(id: $0, color: .blue) }
// Track the selected item
@State private var selectedID = 1
var body: some View {
CenteredHorizontalCollection(items: items) { item, isSelected in
// Custom item view
RoundedRectangle(cornerRadius: 8)
.fill(item.color)
.frame(width: 60, height: 60)
.overlay(
Text("\(item.id)")
.foregroundColor(.white)
)
.scaleEffect(isSelected ? 1.1 : 0.9)
}
.selection($selectedID)
}
}
This code creates a horizontal collection of colored rectangles with numbers, where the selected item appears slightly larger.
Customizing Item Appearance
The power of CenteredHorizontalCollection lies in the view builder closure that lets you fully customize how each item appears:
CenteredHorizontalCollection(items: items) { item, isSelected in
// Custom item view
ZStack {
Circle()
.fill(item.color)
.frame(width: 60, height: 60)
Text("\(item.id)")
.font(.headline)
.foregroundColor(.white)
}
.shadow(radius: isSelected ? 6 : 2)
.scaleEffect(isSelected ? 1.1 : 0.9)
.animation(.spring(response: 0.3, dampingFraction: 0.7), value: isSelected)
}
The closure provides two parameters:
item: The data model for this item
isSelected: A boolean indicating whether this item is currently selected
Use these parameters to customize the appearance based on the selection state. Adding animations creates a more polished experience.
Handling Selection
To track which item is selected, bind a state variable to the collection:
@State private var selectedID = 1
var body: some View {
VStack {
// Display the selected item
Text("Selected Item: \(selectedID)")
CenteredHorizontalCollection(items: items) { item, isSelected in
// Item view builder
}
.selection($selectedID)
// Navigation buttons
HStack {
Button("Previous") {
if selectedID > 1 {
selectedID -= 1
}
}
.disabled(selectedID <= 1)
Button("Next") {
if selectedID < items.count {
selectedID += 1
}
}
.disabled(selectedID >= items.count)
}
}
}
The .selection() modifier binds your state variable to the collection, allowing two-way updates:
When the user selects an item in the collection, your variable updates
When you change the variable programmatically, the collection scrolls to show that item
Configuring Collection Behavior
The CenteredHorizontalCollection uses an actor-based configuration system that provides thread-safe access to all settings. Configure these at the beginning of your view's lifecycle:
.task {
// Configure the collection
await HorizontalCollectionConstants.configure(
itemSize: 60, // Size of each item
itemSpacing: 16, // Spacing between items
debugMode: false, // Enable/disable debug logging
scrollBehaviorMode: .targetContentOffset // Enhanced scrolling physics
)
}
The component supports two scrolling behavior modes:
.standard: Simple selection and centering
.targetContentOffset: Enhanced physics similar to UICollectionView with momentum-based targeting
You can also create UI controls to let users adjust these settings:
@State private var itemSize: Double = 60
@State private var itemSpacing: Double = 16
@State private var debugMode = false
@State private var scrollBehaviorMode = 1 // 0 for standard, 1 for targetContentOffset
// In your view body:
HStack {
Text("Item Size:")
Slider(value: $itemSize, in: 40...120, step: 5)
.onChange(of: itemSize) { value in
Task {
await HorizontalCollectionConstants.configure(
itemSize: value
)
}
}
Text("\(Int(itemSize))")
}
Toggle("Debug Mode", isOn: $debugMode)
.onChange(of: debugMode) { value in
Task {
await HorizontalCollectionConstants.configure(debugMode: value)
}
}
Advanced Theming and Visual Styles
You can create different visual themes for your collection by implementing a theming system:
@State private var currentTheme = 0
let themes = ["Standard", "Minimalist", "Vibrant", "3D Effect"]
// In your view body:
Picker("Display Style", selection: $currentTheme) {
ForEach(0..<themes.count, id: \.self) { index in
Text(themes[index]).tag(index)
}
}
.pickerStyle(SegmentedPickerStyle())
// Then use the selected theme in your item builder:
CenteredHorizontalCollection(items: items) { item, isSelected in
itemView(for: item, isSelected: isSelected)
}
// Define a helper method to create themed item views:
private func itemView(for item: Item, isSelected: Bool) -> some View {
switch currentTheme {
case 0: // Standard
return RoundedRectangle(cornerRadius: 8)
.fill(item.color)
.overlay(Text("\(item.id)").foregroundColor(.white))
.scaleEffect(isSelected ? 1.1 : 0.9)
case 1: // Minimalist
return Circle()
.stroke(item.color, lineWidth: isSelected ? 3 : 1)
.background(Circle().fill(Color.white))
.overlay(Text("\(item.id)").foregroundColor(item.color))
// Add more themes as needed
default:
return RoundedRectangle(cornerRadius: 8)
.fill(item.color)
.overlay(Text("\(item.id)").foregroundColor(.white))
}
}
This approach allows you to offer multiple visual styles without cluttering your main view code.
Performance Considerations
For optimal performance with CenteredHorizontalCollection:
Configure item size appropriately for your design and device size
Use efficient item views that don’t perform expensive rendering operations
Apply animations carefully — use them for selection state but avoid excessive animations
Turn off debug mode in production builds
Complete Implementation Example
Here’s a complete example of a view using CenteredHorizontalCollection with all the features discussed:
import SwiftUI
import CenteredHorizontalCollection
struct CollectionDemoView: View {
// Sample data
let items: [Item] = (1...20).map { Item(id: $0, color: Color(
red: Double.random(in: 0...1),
green: Double.random(in: 0...1),
blue: Double.random(in: 0...1)))
}
// State variables
@State private var selectedID = 1
@State private var itemSize: Double = 60
@State private var itemSpacing: Double = 16
@State private var debugMode = false
@State private var scrollBehaviorMode = 1 // Enhanced mode
@State private var currentTheme = 0
// Theme options
let themes = ["Standard", "Minimal", "Vibrant"]
var body: some View {
VStack(spacing: 16) {
// Header
Text("Collection Demo")
.font(.headline)
// Selected item display
Text("Selected: \(selectedID)")
.padding(.bottom, 8)
// The collection
CenteredHorizontalCollection(items: items) { item, isSelected in
itemView(for: item, isSelected: isSelected)
}
.selection($selectedID)
// Navigation
HStack(spacing: 20) {
Button(action: {
if selectedID > 1 {
selectedID -= 1
}
}) {
Label("Previous", systemImage: "chevron.left")
.padding(.horizontal, 10)
.padding(.vertical, 5)
.background(Color.blue.opacity(0.1))
.cornerRadius(8)
}
.disabled(selectedID <= 1)
Button(action: {
if selectedID < items.count {
selectedID += 1
}
}) {
Label("Next", systemImage: "chevron.right")
.padding(.horizontal, 10)
.padding(.vertical, 5)
.background(Color.blue.opacity(0.1))
.cornerRadius(8)
}
.disabled(selectedID >= items.count)
}
.padding()
// Theme selection
Picker("Style", selection: $currentTheme) {
ForEach(0..<themes.count, id: \.self) { index in
Text(themes[index]).tag(index)
}
}
.pickerStyle(SegmentedPickerStyle())
.padding(.horizontal)
// Configuration controls
VStack(alignment: .leading) {
Text("Configuration")
.font(.subheadline)
.fontWeight(.bold)
// Size and spacing sliders
HStack {
Text("Size:")
Slider(value: $itemSize, in: 40...120, step: 5)
.onChange(of: itemSize) { value in
Task {
await HorizontalCollectionConstants.configure(
itemSize: value
)
}
}
Text("\(Int(itemSize))")
.frame(width: 30)
}
HStack {
Text("Spacing:")
Slider(value: $itemSpacing, in: 4...40, step: 2)
.onChange(of: itemSpacing) { value in
Task {
await HorizontalCollectionConstants.configure(
itemSpacing: value
)
}
}
Text("\(Int(itemSpacing))")
.frame(width: 30)
}
// Mode toggles
Toggle("Debug Mode", isOn: $debugMode)
.onChange(of: debugMode) { value in
Task {
await HorizontalCollectionConstants.configure(debugMode: value)
}
}
Picker("Scroll Behavior", selection: $scrollBehaviorMode) {
Text("Standard").tag(0)
Text("Enhanced").tag(1)
}
.pickerStyle(SegmentedPickerStyle())
.onChange(of: scrollBehaviorMode) { value in
Task {
await HorizontalCollectionConstants.configure(
scrollBehaviorMode: value == 0 ? .standard : .targetContentOffset
)
}
}
// Reset button
Button("Reset Settings") {
resetToDefaults()
}
.padding(.top, 8)
}
.padding()
.background(Color.gray.opacity(0.1))
.cornerRadius(12)
.padding(.horizontal)
}
.padding()
.task {
// Initial configuration
await HorizontalCollectionConstants.configure(
itemSize: itemSize,
itemSpacing: itemSpacing,
debugMode: debugMode,
scrollBehaviorMode: scrollBehaviorMode == 0 ? .standard : .targetContentOffset
)
}
}
// Helper method for themed item views
private func itemView(for item: Item, isSelected: Bool) -> some View {
Group {
switch currentTheme {
case 0: // Standard
RoundedRectangle(cornerRadius: 8)
.fill(item.color)
.frame(width: itemSize, height: itemSize)
.overlay(
Text("\(item.id)")
.foregroundColor(.white)
.fontWeight(.bold)
)
.shadow(radius: isSelected ? 6 : 2)
.scaleEffect(isSelected ? 1.1 : 0.9)
case 1: // Minimalist
Circle()
.stroke(item.color, lineWidth: isSelected ? 3 : 1)
.background(Circle().fill(Color.white))
.frame(width: itemSize, height: itemSize)
.overlay(
Text("\(item.id)")
.foregroundColor(item.color)
.fontWeight(isSelected ? .bold : .regular)
)
.scaleEffect(isSelected ? 1.05 : 1.0)
case 2: // Vibrant
ZStack {
Circle()
.fill(item.color)
Circle()
.fill(item.color.opacity(0.7))
.scaleEffect(0.8)
Circle()
.fill(Color.white.opacity(0.9))
.scaleEffect(0.5)
Text("\(item.id)")
.font(.system(size: itemSize * 0.3))
.fontWeight(.black)
.foregroundColor(item.color)
}
.frame(width: itemSize, height: itemSize)
.shadow(color: item.color.opacity(0.5), radius: isSelected ? 10 : 2)
.scaleEffect(isSelected ? 1.15 : 0.95)
default:
RoundedRectangle(cornerRadius: 8)
.fill(item.color)
.frame(width: itemSize, height: itemSize)
.overlay(Text("\(item.id)").foregroundColor(.white))
}
}
.animation(.spring(response: 0.3, dampingFraction: 0.7), value: isSelected)
}
// Reset to default settings
private func resetToDefaults() {
itemSize = 60
itemSpacing = 16
debugMode = false
scrollBehaviorMode = 1 // Enhanced
currentTheme = 0
Task {
await HorizontalCollectionConstants.configure(
itemSize: 60,
itemSpacing: 16,
debugMode: false,
scrollBehaviorMode: .targetContentOffset
)
}
}
}
Conclusion
The CenteredHorizontalCollection component provides a powerful way to create horizontally scrolling collections with professional-quality behavior in SwiftUI. By leveraging its customization options and configuration system, you can create collections that perfectly match your design needs while providing a smooth, polished user experience.
Key benefits of using this component include:
Enhanced scrolling physics similar to UICollectionView
Complete customization of item appearance
Thread-safe configuration via Swift actors
Support for multiple visual themes
Easy integration with existing SwiftUI code
Start by implementing the basic collection, then gradually add customizations to create exactly the experience you want for your users.
The git you can find here: https://github.com/Maxnxi/CenteredHorizontalCollection_SwiftUI
and DemoView: https://github.com/Maxnxi/CenteredHorizontalCollection_SwiftUI/tree/main/Examples
Top comments (0)