Tutorial: Counter App
Learn how to build an interactive counter app with Shaft step by step.
Interactive Demo
Try the complete counter app below:
Counter App
What You’ll Learn
- Using
@Observablefor reactive state management - Handling button clicks
- Styling widgets with
.textStyle()and.decoration() - Creating layouts with
ColumnandRow
Step-by-Step Guide
Step 1: Create the State Model
Use Swift’s @Observable to make your data reactive:
import Observation
@Observable class Counter {
var count = 0
func increment() {
count += 1
}
func decrement() {
count -= 1
}
func reset() {
count = 0
}
}
let counter = Counter() Why this works:
@Observableautomatically notifies the UI whencountchanges- Methods encapsulate logic for modifying state
- Global instance can be accessed from any widget
Step 2: Build the UI Structure
Create a centered column layout:
final class CounterApp: StatelessWidget {
func build(context: any BuildContext) -> any Widget {
Column(mainAxisAlignment: .center, spacing: 24) {
// Widgets go here
}
.padding(.all(40))
}
} Parameters:
mainAxisAlignment: .center- Centers children verticallyspacing: 24- Adds 24px between children.padding(.all(40))- Adds space around the column
Step 3: Add the Title
Display the app name with styling:
Text("Counter App")
.textStyle(.init(fontSize: 28, fontWeight: .bold)) Styling:
.textStyle()applies text formatting- Create a
TextStylewithfontSizeandfontWeight
Step 4: Display the Count
Create a circular display with the current count:
SizedBox(width: 120, height: 120) {
Text("(counter.count)")
.textStyle(.init(
color: Color(0xFF_FFFFFF), // White text
fontSize: 48,
fontWeight: .bold
))
.center()
}
.decoration(.box(
color: Color(0xFF_FFA500), // Orange background
borderRadius: .circular(60) // Makes it circular
)) Key concepts:
SizedBoxfixes the widget sizeText("\(counter.count)")displays the current value.decoration(.box(...))adds background and rounded corners.center()centers the text inside
Step 5: Add Control Buttons
Create three buttons in a row:
Row(mainAxisAlignment: .center, spacing: 12) {
// Decrement
Button {
counter.decrement()
} child: {
Text("−")
.textStyle(.init(fontSize: 32))
}
.padding(.all(16))
// Reset
Button {
counter.reset()
} child: {
Text("↻")
.textStyle(.init(fontSize: 24))
}
.padding(.all(16))
// Increment
Button {
counter.increment()
} child: {
Text("+")
.textStyle(.init(fontSize: 32))
}
.padding(.all(16))
} Button structure:
- First closure: action when clicked
child:closure: button content.padding()adds space inside the button
Step 6: Run the App
runApp(CounterApp()) That’s it! Your counter app is complete.
Understanding Reactivity
When you call counter.increment():
- The
countproperty changes @Observabledetects the change- Shaft automatically rebuilds affected widgets
- UI updates to show new count
No manual state management needed!
Key Takeaways
- @Observable makes state reactive - Changes automatically update UI
- Methods encapsulate logic -
increment()is better thancount += 1everywhere - Widgets compose naturally - Nest
Column,Row,Text,Buttonto build UI - No explicit setState - Shaft handles UI updates automatically
Next Steps
- Build a Todo List to learn about managing collections
- Explore Widget Styling to customize appearance
- Learn about State Management patterns
- Check out the Layout System for complex layouts