Complete SwiftUI Tutorial - Build a Modern Productivity App
π Table of Contents
- 1. App Overview
- 2. Architecture & Structure
- 3. Data Models (SwiftData)
- 4. Timer Logic
- 5. UI Components
- 6. Animations & Effects
- 7. Putting It All Together
π± App Overview
FlowBoard is a modern productivity app built with SwiftUI that combines a Pomodoro timer, task management, and focus tracking. It features a beautiful glassmorphism design with smooth animations and persistent data storage using SwiftData.
β±οΈ Pomodoro Timer
Focus sessions (25 min) and break times (5 min) with auto-switching functionality
β Task Management
Create, complete, and delete tasks with priority leβ¦
Complete SwiftUI Tutorial - Build a Modern Productivity App
π Table of Contents
- 1. App Overview
- 2. Architecture & Structure
- 3. Data Models (SwiftData)
- 4. Timer Logic
- 5. UI Components
- 6. Animations & Effects
- 7. Putting It All Together
π± App Overview
FlowBoard is a modern productivity app built with SwiftUI that combines a Pomodoro timer, task management, and focus tracking. It features a beautiful glassmorphism design with smooth animations and persistent data storage using SwiftData.
β±οΈ Pomodoro Timer
Focus sessions (25 min) and break times (5 min) with auto-switching functionality
β Task Management
Create, complete, and delete tasks with priority levels and color coding
π Statistics
Weekly focus time tracking with beautiful charts and insights
π Mood Tracking
Log your mood for each focus session to track productivity patterns
Timer Visualization
25:00
Focus mode with animated progress ring
ποΈ Architecture & Structure
The app follows the MVVM (Model-View-ViewModel) pattern with SwiftUIβs modern architecture principles.
App Architecture Flow
CodeApp (Entry Point)
β
ContentView
β
DashboardView
Project Structure
FlowBoard/
βββ CodeApp.swift // App entry point with SwiftData container
βββ ContentView.swift // Main UI components and logic
βββ Models
β βββ TaskItem // Task data model
β βββ FocusSession // Focus session data model
βββ ViewModels
β βββ TimerViewModel // Timer business logic
βββ Views
βββ DashboardView // Main dashboard
βββ TaskRow // Individual task component
βββ MoodPicker // Mood selection UI
βββ ProgressRing // Circular timer progress
βββ AnimatedBackdrop // Animated background
π― Key Design Patterns
- MVVM: Separation of UI (View) and business logic (ViewModel)
- SwiftData: Modern persistence layer replacing Core Data
- Observable: Reactive state management using @Observable macro
- Composition: Small, reusable UI components
πΎ Data Models with SwiftData
SwiftData is Appleβs modern framework for data persistence. It uses Swift macros to simplify database management and provides automatic schema generation.
1 App Entry Point
The app entry point configures SwiftData for persistent storage:
import SwiftUI
import SwiftData
/// App entry point.
@main
struct CodeApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
// SwiftData container: persistent storage for TaskItem and FocusSession
.modelContainer(for: [TaskItem.self, FocusSession.self])
}
}
π‘ Whatβs Happening Here?
@main: Marks the entry point of your SwiftUI app
.modelContainer(): Sets up SwiftData to persist TaskItem and FocusSession objects automatically
2 TaskItem Model
Represents a single task with priority and color coding:
@Model
final class TaskItem {
var title: String
var createdAt: Date
var isDone: Bool
var priority: Int // 0=Low, 1=Medium, 2=High
var colorHex: String
init(
title: String,
createdAt: Date = .now,
isDone: Bool = false,
priority: Int = 1,
colorHex: String = "#7C3AED"
) {
self.title = title
self.createdAt = createdAt
self.isDone = isDone
self.priority = priority
self.colorHex = colorHex
}
}
π Model Properties Explained:
- title: The task description
- createdAt: Timestamp for sorting
- isDone: Completion status
- priority: 0 (Low), 1 (Medium), 2 (High)
- colorHex: Visual identifier for the task
3 FocusSession Model
Records completed focus sessions with duration and mood:
@Model
final class FocusSession {
var startedAt: Date
var durationSeconds: Int
var mood: String // "π", "π₯", "π΅βπ«" etc
init(startedAt: Date = .now, durationSeconds: Int, mood: String) {
self.startedAt = startedAt
self.durationSeconds = durationSeconds
self.mood = mood
}
}
π― Why Track Focus Sessions?
Recording sessions allows the app to generate statistics and charts showing your productivity patterns over time. The mood tracking helps identify optimal working conditions.
β±οΈ Timer Logic & State Management
The TimerViewModel handles all timer logic using the @Observable macro, which automatically triggers UI updates when properties change.
1 Timer Modes
The timer supports two modes with different durations:
@Observable
final class TimerViewModel {
enum Mode: String, CaseIterable {
case focus = "Focus"
case breakTime = "Break"
var defaultSeconds: Int {
switch self {
case .focus: return 25 * 60 // 25 minutes
case .breakTime: return 5 * 60 // 5 minutes
}
}
}
var mode: Mode = .focus
var isRunning: Bool = false
var totalSeconds: Int = Mode.focus.defaultSeconds
var remainingSeconds: Int = Mode.focus.defaultSeconds
private var timer: Timer?
private var lastTick: Date?
}
Timer State Flow
Idle (25:00)
β
Running
β
Paused
β
Completed
2 Start, Pause, and Stop Methods
func start() {
guard !isRunning else { return }
isRunning = true
lastTick = .now
timer?.invalidate()
timer = Timer.scheduledTimer(withTimeInterval: 0.25, repeats: true) { [weak self] _ in
self?.tick()
}
}
func pause() {
isRunning = false
timer?.invalidate()
timer = nil
lastTick = nil
}
func stop() {
pause()
remainingSeconds = totalSeconds
}
3 Tick Method (Core Timer Logic)
private func tick() {
guard isRunning else { return }
let now = Date()
guard let lastTick else { self.lastTick = now; return }
let delta = now.timeIntervalSince(lastTick)
if delta < 1 { return }
self.lastTick = now
remainingSeconds = max(0, remainingSeconds - Int(delta.rounded(.down)))
if remainingSeconds == 0 {
// Auto switch mode when done
pause()
if mode == .focus {
setMode(.breakTime)
} else {
setMode(.focus)
}
}
}
π How the Tick Method Works:
- Check elapsed time: Calculate seconds since last tick
- Update remaining time: Subtract elapsed seconds
- Auto-switch modes: When timer reaches 0, automatically switch between focus and break
- Timer runs at 0.25s intervals: Provides smooth UI updates
4 Progress and Time Formatting
var progress: Double {
guard totalSeconds > 0 else { return 0 }
return 1.0 - (Double(remainingSeconds) / Double(totalSeconds))
}
var timeString: String {
let m = remainingSeconds / 60
let s = remainingSeconds % 60
return String(format: "%02d:%02d", m, s)
}
π¨ UI Components
The app uses a modular component architecture with glassmorphism design for a modern, elegant appearance.
1 Dashboard Structure
struct DashboardView: View {
@Environment(\.modelContext) private var modelContext
// Fetch tasks newest first
@Query(sort: \TaskItem.createdAt, order: .reverse) private var tasks: [TaskItem]
// Fetch sessions newest first
@Query(sort: \FocusSession.startedAt, order: .reverse) private var sessions: [FocusSession]
@State private var timerVM = TimerViewModel()
@State private var newTaskText: String = ""
@State private var selectedMood: String = "π"
var body: some View {
NavigationStack {
ZStack {
AnimatedBackdrop()
.ignoresSafeArea()
ScrollView(.vertical, showsIndicators: false) {
VStack(spacing: 20) {
header
timerCard
statsCard
tasksCard
}
}
}
}
}
}
π Key SwiftUI Property Wrappers:
- @Environment(.modelContext): Access SwiftDataβs model context for database operations
- @Query: Automatically fetches and observes data from SwiftData
- @State: Stores mutable state for the view
2 Progress Ring Component
A circular progress indicator that animates as the timer counts down:
private struct ProgressRing: View {
var progress: Double
var mode: TimerViewModel.Mode
var body: some View {
ZStack {
// Background ring
Circle()
.stroke(
LinearGradient(
colors: [.white.opacity(0.18), .white.opacity(0.10)],
startPoint: .topLeading,
endPoint: .bottomTrailing
),
lineWidth: 17
)
// Progress ring with gradient
Circle()
.trim(from: 0, to: max(0, min(1, progress)))
.stroke(
AngularGradient(
gradient: Gradient(colors: mode == .focus
? [Color.purple, Color.pink, Color.cyan]
: [Color.cyan, Color.green, Color.mint]),
center: .center
),
style: StrokeStyle(lineWidth: 17, lineCap: .round)
)
.rotationEffect(.degrees(-90))
.animation(.spring(response: 0.6), value: progress)
}
}
}
π¨ Design Techniques:
- Circle().trim(): Creates the partial circle based on progress (0.0 to 1.0)
- AngularGradient: Gradient that follows the circleβs curve
- rotationEffect: Starts progress from the top (-90 degrees)
- StrokeStyle lineCap: Rounded ends for a polished look
3 Task Row Component
private struct TaskRow: View {
let task: TaskItem
let onDelete: () -> Void
var body: some View {
HStack(spacing: 14) {
// Checkbox button
Button {
withAnimation(.spring()) {
task.isDone.toggle()
}
} label: {
ZStack {
// Empty circle when not done
Circle()
.strokeBorder(.white, lineWidth: 2.5)
.opacity(task.isDone ? 0 : 1)
// Filled checkmark when done
Image(systemName: "checkmark.circle.fill")
.foregroundStyle(Color(hex: task.colorHex))
.opacity(task.isDone ? 1 : 0)
.scaleEffect(task.isDone ? 1 : 0.5)
}
}
// Task title with strikethrough when done
Text(task.title)
.strikethrough(task.isDone)
Spacer()
// Color indicator
Circle()
.fill(Color(hex: task.colorHex))
.frame(width: 14, height: 14)
// Delete button
Button(role: .destructive) {
onDelete()
} label: {
Image(systemName: "trash")
}
}
.padding()
.background(.white.opacity(0.08))
.cornerRadius(20)
}
}
4 Glassmorphism Button Styles
private struct PrimaryGlassButtonStyle: ButtonStyle {
func makeBody(configuration: Configuration) -> some View {
configuration.label
.padding(.vertical, 14)
.background(
RoundedRectangle(cornerRadius: 18)
.fill(.white.opacity(configuration.isPressed ? 0.22 : 0.16))
)
.overlay {
RoundedRectangle(cornerRadius: 18)
.strokeBorder(.white.opacity(0.3), lineWidth: 1.5)
}
.scaleEffect(configuration.isPressed ? 0.97 : 1.0)
.animation(.spring(), value: configuration.isPressed)
}
}
β¨ Glassmorphism Effect:
Glassmorphism combines:
- Semi-transparent backgrounds: .opacity() values between 0.08-0.22
- Blur effects: .ultraThinMaterial in the actual code
- Subtle borders: Light strokeBorder with gradients
- Shadows: Depth perception with shadow() modifiers
β¨ Animations & Effects
The app uses SwiftUIβs animation system to create smooth, engaging transitions and effects.
1 Animated Backdrop
Creates a dynamic, animated gradient background using Canvas and TimelineView:
private struct AnimatedBackdrop: View {
@State private var t: CGFloat = 0
var body: some View {
TimelineView(.animation) { _ in
Canvas { ctx, size in
t += 0.004 // Increment time for animation
let center = CGPoint(x: size.width * 0.5, y: size.height * 0.35)
let r1 = min(size.width, size.height) * 0.55
// Animated blob using sin/cos for smooth motion
let blob1 = Path(ellipseIn: CGRect(
x: center.x + sin(t) * 30,
y: center.y + cos(t) * 24,
width: r1,
height: r1 * 0.85
))
ctx.fill(blob1, with: .color(Color.purple.opacity(0.35)))
}
}
}
}
Animation Techniques
TimelineView
β
Canvas Drawing
β
Sin/Cos Motion
β
Smooth Animation
2 Spring Animations
SwiftUIβs spring animations create natural, physics-based motion:
// Card entrance animation
.onAppear {
withAnimation(.spring(response: 0.8, dampingFraction: 0.8).delay(0.1)) {
cardsAppeared = true
}
}
// Button press feedback
.scaleEffect(configuration.isPressed ? 0.97 : 1.0)
.animation(.spring(response: 0.3, dampingFraction: 0.7), value: configuration.isPressed)
// Checkbox toggle animation
withAnimation(.spring(response: 0.3, dampingFraction: 0.7)) {
task.isDone.toggle()
}
π― Animation Parameters:
- response: Duration of the spring animation (lower = faster)
- dampingFraction: How much the spring oscillates (0 = bouncy, 1 = no bounce)
- .delay(): Stagger animations for visual hierarchy
3 Glow Effects
// Pulsing glow behind timer ring
Circle()
.fill(
RadialGradient(
colors: [.purple.opacity(0.6), .pink.opacity(0.3), .clear],
center: .center,
startRadius: 50,
endRadius: 110
)
)
.blur(radius: 25)
.opacity(timerVM.isRunning ? 1.0 : 0.4)
.scaleEffect(timerVM.isRunning ? 1.1 : 1.0)
.animation(
timerVM.isRunning
? .easeInOut(duration: 1.8).repeatForever(autoreverses: true)
: .easeOut(duration: 0.5),
value: timerVM.isRunning
)
4 Haptic Feedback
Tactile feedback enhances the user experience:
// Soft haptic when adding a task
UIImpactFeedbackGenerator(style: .soft).impactOccurred()
// Light haptic when toggling task completion
UIImpactFeedbackGenerator(style: .light).impactOccurred()
// Medium haptic when selecting mood
UIImpactFeedbackGenerator(style: .medium).impactOccurred()
π Putting It All Together
Understanding how all components work together to create a cohesive app experience.
1 Data Flow Diagram
π SwiftData Flow
User Action
β
ModelContext
β
Database
β
@Query Update
β
UI Refresh
2 Adding a Task (Complete Flow)
func addTask() {
let trimmed = newTaskText.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else {
// Show feedback animation if empty
showAddTaskGlow = true
DispatchQueue.main.asyncAfter(deadline: .now() + 0.35) {
showAddTaskGlow = false
}
return
}
// Random color and priority
let palette = ["#7C3AED", "#06B6D4", "#F97316", "#22C55E"]
let color = palette.randomElement() ?? "#7C3AED"
let priority = Int.random(in: 0...2)
// Insert into SwiftData
modelContext.insert(TaskItem(title: trimmed, priority: priority, colorHex: color))
newTaskText = ""
// Haptic feedback
UIImpactFeedbackGenerator(style: .soft).impactOccurred()
}
3 Saving a Focus Session
// When user resets timer, save the session if they made progress
Button {
// Save session only if in focus mode and made progress
if timerVM.mode == .focus, timerVM.progress > 0.05 {
let focusedSeconds = Int(Double(timerVM.totalSeconds) * timerVM.progress)
modelContext.insert(FocusSession(
durationSeconds: focusedSeconds,
mood: selectedMood
))
}
timerVM.stop()
} label: {
Label("Reset", systemImage: "arrow.counterclockwise")
}
4 Weekly Statistics Chart
var weeklyChartData: [DayMinutes] {
let cal = Calendar.current
let now = Date()
// Get last 7 days
let days = (0..<7).compactMap {
cal.date(byAdding: .day, value: -$0, to: now)
}.reversed()
return days.map { day in
let start = cal.startOfDay(for: day)
let end = cal.date(byAdding: .day, value: 1, to: start) ?? start
// Sum all sessions for this day
let seconds = sessions
.filter { $0.startedAt >= start && $0.startedAt < end }
.reduce(0) { $0 + $1.durationSeconds }
let label = DateFormatter.shortDay.string(from: day)
return DayMinutes(dayLabel: label, minutes: seconds / 60)
}
}
π Key Learning Points:
- SwiftData auto-saves: No need to call save() explicitly
- @Query is reactive: UI updates automatically when data changes
- modelContext.insert(): Add new objects to the database
- modelContext.delete(): Remove objects from the database
5 Color Extension Helper
private extension Color {
init(hex: String) {
let hex = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted)
var int: UInt64 = 0
Scanner(string: hex).scanHexInt64(&int)
let r = (int >> 16) & 0xff
let g = (int >> 8) & 0xff
let b = int & 0xff
self.init(
.sRGB,
red: Double(r) / 255,
green: Double(g) / 255,
blue: Double(b) / 255
)
}
}
π¨ How Hex Color Parsing Works:
- Remove non-alphanumeric characters (like "#")
- Parse hex string to UInt64 integer
- Extract RGB components using bit shifting
- Convert to 0-1 range for SwiftUI Color
π Congratulations!
Youβve learned how to build a complete productivity app with SwiftUI featuring:
- β SwiftData for persistent storage
- β MVVM architecture with @Observable
- β Custom animations and transitions
- β Glassmorphism UI design
- β Charts and data visualization
- β Haptic feedback integration
π Next Steps:
- Add notifications when timer completes
- Implement custom timer durations
- Add task categories and filtering
- Export statistics to CSV
- Add widgets for iOS home screen