Overview
SwiftUI is Apple's modern declarative UI framework introduced at WWDC 2019. It represents a paradigm shift from UIKit's imperative approach, enabling developers to build user interfaces across all Apple platforms with less code and a more intuitive syntax. SwiftUI leverages Swift's power to provide a declarative, functional, and reactive approach to UI development.
Why SwiftUI is Revolutionary
- Declarative Syntax: Describe what your UI should look like, not how to build it
- Single Source of Truth: Automatic UI updates when data changes
- Cross-Platform: One codebase for iOS, macOS, watchOS, tvOS
- Live Preview: See changes instantly without building
- Built-in Animations: Smooth transitions with minimal code
- Accessibility: Automatic support for VoiceOver, Dynamic Type
- Dark Mode: Automatic adaptation to user preferences
- Less Code: 10x less code than UIKit for many interfaces
Basic App Structure
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
Basic View
struct ContentView: View {
var body: some View {
Text("Hello, SwiftUI!")
}
}
Views and Modifiers
Common Views
// Text
Text("Hello").font(.title).foregroundColor(.blue)
// Image
Image(systemName: "star.fill")
Image("photo").resizable().aspectRatio(contentMode: .fit)
// Button
Button("Tap me") { print("Tapped") }
Button(action: { }) { Text("Custom") }
// TextField
TextField("Enter name", text: $name)
// Toggle
Toggle("Enable", isOn: $isEnabled)
// Slider
Slider(value: $value, in: 0...100)
// Picker
Picker("Select", selection: $selected) {
Text("Option 1").tag(1)
Text("Option 2").tag(2)
}
Common Modifiers
.padding()
.background(Color.blue)
.foregroundColor(.white)
.font(.headline)
.frame(width: 200, height: 100)
.cornerRadius(10)
.shadow(radius: 5)
.opacity(0.8)
.scaleEffect(1.2)
.rotationEffect(.degrees(45))
.overlay(Text("Overlay"))
.border(Color.red, width: 2)
Layout System
Stack Views
// Vertical Stack
VStack(alignment: .leading, spacing: 10) {
Text("Item 1")
Text("Item 2")
}
// Horizontal Stack
HStack(spacing: 20) {
Image(systemName: "star")
Text("Rating")
}
// Z-Stack (Depth)
ZStack {
Rectangle().fill(Color.blue)
Text("On Top")
}
Spacer and Divider
HStack {
Text("Left")
Spacer()
Text("Right")
}
VStack {
Text("Top")
Divider()
Text("Bottom")
}
GeometryReader
GeometryReader { geometry in
Text("Width: \(geometry.size.width)")
.frame(width: geometry.size.width / 2)
}
LazyStacks (iOS 14+)
ScrollView {
LazyVStack {
ForEach(0..<1000) { i in
Text("Row \(i)")
}
}
}
State Management
@State
- Local state for value types
- View owns the data
struct CounterView: View {
@State private var count = 0
var body: some View {
Button("Count: \(count)") {
count += 1
}
}
}
@Binding
- Two-way connection to state
- Child views modify parent's state
struct ChildView: View {
@Binding var isOn: Bool
var body: some View {
Toggle("Switch", isOn: $isOn)
}
}
struct ParentView: View {
@State private var isEnabled = false
var body: some View {
ChildView(isOn: $isEnabled)
}
}
@StateObject (iOS 14+)
- Reference type state
- View creates and owns the object
class ViewModel: ObservableObject {
@Published var text = "Hello"
}
struct ContentView: View {
@StateObject private var viewModel = ViewModel()
var body: some View {
Text(viewModel.text)
}
}
@ObservedObject
- Reference type state
- View observes but doesn't own
struct DetailView: View {
@ObservedObject var viewModel: ViewModel
var body: some View {
Text(viewModel.text)
}
}
@EnvironmentObject
- Shared data across view hierarchy
// Set in parent
ContentView().environmentObject(UserSettings())
// Use in any child
struct ChildView: View {
@EnvironmentObject var settings: UserSettings
var body: some View {
Text(settings.username)
}
}
@Environment
- System values and settings
struct ContentView: View {
@Environment(\.colorScheme) var colorScheme
@Environment(\.presentationMode) var presentationMode
var body: some View {
Text(colorScheme == .dark ? "Dark" : "Light")
}
}
Lists and Collections
List
// Static
List {
Text("Row 1")
Text("Row 2")
}
// Dynamic
List(items) { item in
Text(item.name)
}
// With sections
List {
Section("Header") {
ForEach(items) { item in
Text(item.name)
}
}
}
ForEach
ForEach(0..<5) { index in
Text("Item \(index)")
}
ForEach(items, id: \.self) { item in
Text(item)
}
List Operations
List {
ForEach(items) { item in
Text(item.name)
}
.onDelete(perform: deleteItems)
.onMove(perform: moveItems)
}
ScrollView
ScrollView(.horizontal, showsIndicators: false) {
HStack {
ForEach(items) { item in
ItemView(item: item)
}
}
}
Gestures and Animations
Basic Animations
@State private var scale = 1.0
Text("Tap me")
.scaleEffect(scale)
.onTapGesture {
withAnimation(.spring()) {
scale = scale == 1.0 ? 1.5 : 1.0
}
}
Animation Types
.animation(.default, value: scale)
.animation(.linear(duration: 1), value: offset)
.animation(.easeInOut, value: rotation)
.animation(.spring(), value: position)
Transitions
if showView {
Text("Hello")
.transition(.slide)
.transition(.scale)
.transition(.opacity)
.transition(.move(edge: .leading))
}
Gesture Recognizers
// Tap
.onTapGesture { print("Tapped") }
// Long Press
.onLongPressGesture { print("Long pressed") }
// Drag
.gesture(
DragGesture()
.onChanged { value in
offset = value.translation
}
)
// Magnification
.gesture(
MagnificationGesture()
.onChanged { value in
scale = value
}
)
Data Flow
ObservableObject Protocol
class DataModel: ObservableObject {
@Published var items = [Item]()
func addItem(_ item: Item) {
items.append(item)
}
}
Combine Integration
class ViewModel: ObservableObject {
@Published var searchText = ""
@Published var results = [String]()
private var cancellables = Set<AnyCancellable>()
init() {
$searchText
.debounce(for: .seconds(0.5), scheduler: RunLoop.main)
.sink { [weak self] text in
self?.performSearch(text)
}
.store(in: &cancellables)
}
}
Lifecycle Methods
View Lifecycle
struct ContentView: View {
var body: some View {
Text("Hello")
.onAppear {
print("View appeared")
}
.onDisappear {
print("View disappeared")
}
.task {
// Async work when view appears
await loadData()
}
}
}
Scene Phase
@Environment(\.scenePhase) var scenePhase
var body: some View {
ContentView()
.onChange(of: scenePhase) { newPhase in
switch newPhase {
case .active:
print("App is active")
case .inactive:
print("App is inactive")
case .background:
print("App is in background")
@unknown default:
break
}
}
}
Performance Optimization
Best Practices
- Use
LazyVStack/LazyHStackfor large lists - Avoid complex calculations in body
- Use
@StateObjectfor view-owned objects - Minimize view updates with proper state management
- Use
equatable()for expensive views
View Identity
// Stable identity
ForEach(items, id: \.id) { item in
ItemView(item: item)
}
// Explicit identity
Text("Hello")
.id(UUID())
Lazy Loading
LazyVGrid(columns: [GridItem(.adaptive(minimum: 100))]) {
ForEach(items) { item in
ItemView(item: item)
}
}
Advanced Topics
Custom ViewModifier
struct TitleModifier: ViewModifier {
func body(content: Content) -> some View {
content
.font(.largeTitle)
.foregroundColor(.blue)
.padding()
}
}
extension View {
func titleStyle() -> some View {
modifier(TitleModifier())
}
}
PreferenceKey
struct SizePreferenceKey: PreferenceKey {
static var defaultValue: CGSize = .zero
static func reduce(value: inout CGSize, nextValue: () -> CGSize) {
value = nextValue()
}
}
// Usage
.background(GeometryReader { geo in
Color.clear.preference(key: SizePreferenceKey.self, value: geo.size)
})
.onPreferenceChange(SizePreferenceKey.self) { size in
print("Size: \(size)")
}
Custom Container Views
struct Card<Content: View>: View {
let content: Content
init(@ViewBuilder content: () -> Content) {
self.content = content()
}
var body: some View {
content
.padding()
.background(Color.white)
.cornerRadius(10)
.shadow(radius: 5)
}
}
Environment Values
private struct ThemeKey: EnvironmentKey {
static let defaultValue = Theme.light
}
extension EnvironmentValues {
var theme: Theme {
get { self[ThemeKey.self] }
set { self[ThemeKey.self] = newValue }
}
}
Common Interview Questions
1. What's the difference between @State and @StateObject?
@State: For value types, view owns the data@StateObject: For reference types (ObservableObject), view creates and owns the object
2. When to use @ObservedObject vs @StateObject?
@StateObject: When the view creates the object@ObservedObject: When the object is passed in from parent
3. How does SwiftUI update views?
- SwiftUI uses a diffing algorithm
- Only updates views whose state has changed
- Structural identity helps optimize updates
4. What is @ViewBuilder?
- Function builder that constructs views from closures
- Allows multiple child views without explicit container
- Used in custom container views
5. How to pass data between views?
- @Binding: Parent to child, two-way
- @EnvironmentObject: Across view hierarchy
- @Environment: System values
- Initializer: Direct passing
6. What are Property Wrappers in SwiftUI?
- Special attributes that add behavior to properties
- Examples: @State, @Binding, @Published, @ObservedObject
7. How to handle async operations?
.task {
await fetchData()
}
.refreshable {
await refreshData()
}
8. Key Performance Tips:
- Use lazy stacks for large lists
- Implement Equatable for complex views
- Avoid heavy computations in body
- Use @StateObject correctly
- Profile with Instruments
Quick Reference
State Management Decision Tree
Is it a value type? → @State
Is it passed from parent? → @Binding
Is it a reference type I create? → @StateObject
Is it a reference type passed in? → @ObservedObject
Is it shared across many views? → @EnvironmentObject
Is it a system value? → @Environment
Common Patterns
// Conditional Views
if condition {
TrueView()
} else {
FalseView()
}
// Optional Binding
if let value = optionalValue {
Text(value)
}
// Switch Statement
switch value {
case .option1:
View1()
case .option2:
View2()
}
Debugging Tips
- Use
.border(Color.red)to visualize frames - Print in
.onAppearto track view lifecycle - Use
Random colorsfor layout debugging - Check Xcode Memory Graph for retain cycles
- Use SwiftUI Preview variants
Found this useful? Pass it on.