Getting Started
SwiftUI Setup
// Create a new SwiftUI project in Xcode:
// 1. File → New → Project
// 2. Select iOS/macOS tab
// 3. Choose App template
// 4. Select SwiftUI for Interface
// Basic SwiftUI App structure
import SwiftUI
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
// Basic View structure
struct ContentView: View {
var body: some View {
Text("Hello, SwiftUI!")
.padding()
}
}
// Preview provider
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Note: SwiftUI requires Xcode 11+ and iOS 13+ or macOS 10.15+. The @main attribute identifies the app's entry point.
Swift Basics
// Variables and constants
var mutableVariable = "This can change" // Mutable
let constantValue = "This cannot change" // Immutable
// Type annotations
var name: String = "John"
var age: Int = 30
var price: Double = 19.99
var isEnabled: Bool = true
// Optionals (can be nil)
var optionalName: String? = nil
var unwrappedName = optionalName ?? "Default" // Nil coalescing
// Control flow
if age > 18 {
print("Adult")
} else {
print("Minor")
}
// Switch statement
switch age {
case 0..18:
print("Minor")
case 18...65:
print("Adult")
default:
print("Senior")
}
// Arrays and dictionaries
var names: [String] = ["Alice", "Bob", "Charlie"]
var person: [String: Any] = [
"name": "John",
"age": 30,
"isStudent": false
]
Note: Swift is a type-safe language with type inference. Use 'let' for constants and 'var' for variables. Optionals handle the absence of a value.
Views & Layouts
Basic Views
// Text view
Text("Hello, World!")
.font(.title)
.foregroundColor(.blue)
.bold()
// Image view
Image(systemName: "heart.fill") // SF Symbols
.font(.largeTitle)
.foregroundColor(.red)
Image("myImage") // Asset catalog
.resizable()
.scaledToFit()
.frame(width: 100, height: 100)
// Button
Button(action: {
print("Button tapped")
}) {
Text("Tap me")
.padding()
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(10)
}
// TextField
@State private var username = ""
TextField("Enter username", text: $username)
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding()
// Toggle
@State private var isOn = false
Toggle(isOn: $isOn) {
Text("Enable feature")
}
Note: SwiftUI views are value types that conform to the View protocol. Modifiers are used to customize their appearance and behavior.
Layout Containers
// VStack - Vertical stack
VStack(alignment: .leading, spacing: 10) {
Text("First item")
Text("Second item")
Text("Third item")
}
.padding()
// HStack - Horizontal stack
HStack(spacing: 20) {
Image(systemName: "star.fill")
Text("Rating: 4.5")
Spacer() // Pushes content to the left
}
// ZStack - Overlapping views
ZStack {
Circle()
.fill(Color.blue)
.frame(width: 100, height: 100)
Text("Center")
.foregroundColor(.white)
.font(.headline)
}
// List
List {
Section(header: Text("Section 1")) {
Text("Item 1")
Text("Item 2")
}
Section(header: Text("Section 2")) {
ForEach(1...5, id: \.self) { number in
Text("Item \(number)")
}
}
}
.listStyle(GroupedListStyle())
// ScrollView
ScrollView {
VStack {
ForEach(1...50, id: \.self) { index in
Text("Item \(index)")
.padding()
}
}
}
Note: Stacks are fundamental layout containers in SwiftUI. VStack arranges vertically, HStack horizontally, and ZStack overlays views.
Modifiers
Layout Modifiers
// Frame modifier
Text("Hello")
.frame(width: 200, height: 100)
.frame(maxWidth: .infinity, maxHeight: .infinity) // Expand to fill available space
// Padding
Text("Padded text")
.padding() // All sides with default padding
.padding(10) // All sides with specific padding
.padding(.horizontal, 20) // Horizontal only
.padding([.top, .bottom], 10) // Top and bottom
// Position and offset
Text("Offset text")
.offset(x: 10, y: 5) // Relative offset
Text("Positioned text")
.position(x: 100, y: 100) // Absolute position in parent
// Alignment guides
VStack(alignment: .leading) {
Text("First")
.alignmentGuide(.leading) { d in d[.leading] - 10 }
Text("Second")
}
Note: Modifiers return new views rather than modifying existing ones. The order of modifiers matters as each one wraps the previous view.
Appearance Modifiers
// Color modifiers
Text("Colored text")
.foregroundColor(.blue) // Text color
.background(Color.yellow) // Background color
.accentColor(.green) // Accent color for interactive elements
// Font modifiers
Text("Styled text")
.font(.title) // System font
.fontWeight(.bold) // Font weight
.font(.system(size: 24, weight: .heavy, design: .rounded))
// Shape and border
Text("Rounded background")
.padding()
.background(Color.blue)
.cornerRadius(10) // Rounded corners
Circle()
.stroke(Color.red, lineWidth: 2) // Border
.fill(Color.blue) // Fill color
.frame(width: 100, height: 100)
// Shadow and overlay
Text("Shadow text")
.shadow(color: .gray, radius: 2, x: 2, y: 2)
Rectangle()
.fill(Color.blue)
.frame(width: 100, height: 100)
.overlay(
Text("Overlay")
.foregroundColor(.white)
)
.background(
Circle()
.fill(Color.red)
.frame(width: 120, height: 120)
)
Note: Overlay adds views on top of the current view, while background adds views behind it. The order of these modifiers affects the final result.
State Management
Property Wrappers
// @State - For local view state
struct CounterView: View {
@State private var count = 0
var body: some View {
VStack {
Text("Count: \(count)")
Button("Increment") {
count += 1
}
}
}
}
// @Binding - For sharing state with parent views
struct ToggleView: View {
@Binding var isOn: Bool
var body: some View {
Toggle(isOn: $isOn) {
Text(isOn ? "On" : "Off")
}
}
}
// @ObservedObject - For external reference types
class UserSettings: ObservableObject {
@Published var username = "Anonymous"
}
struct SettingsView: View {
@ObservedObject var settings: UserSettings
var body: some View {
TextField("Username", text: $settings.username)
}
}
// @EnvironmentObject - For shared data across views
class AppData: ObservableObject {
@Published var userIsLoggedIn = false
}
struct ContentView: View {
@EnvironmentObject var appData: AppData
var body: some View {
if appData.userIsLoggedIn {
Text("Welcome!")
} else {
LoginView()
}
}
}
// In the app entry point
ContentView()
.environmentObject(AppData())
Note: @State is for value types owned by the view. @ObservedObject is for reference types that conform to ObservableObject. @EnvironmentObject is for shared data across the view hierarchy.
More State Management
// @StateObject - For creating & owning observable objects
struct TimerView: View {
@StateObject private var timer = TimerManager()
var body: some View {
Text("Time: \(timer.time)")
}
}
class TimerManager: ObservableObject {
@Published var time = 0
private var timer: Timer?
init() {
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in
self?.time += 1
}
}
}
// @Environment - For accessing system-wide settings
struct ThemeView: View {
@Environment(\.colorScheme) var colorScheme
var body: some View {
Text(colorScheme == .dark ? "Dark Mode" : "Light Mode")
}
}
// @AppStorage - For UserDefaults integration
struct SettingsView: View {
@AppStorage("username") var username = "Anonymous"
@AppStorage("isDarkMode") var isDarkMode = false
var body: some View {
Form {
TextField("Username", text: $username)
Toggle("Dark Mode", isOn: $isDarkMode)
}
}
}
// @SceneStorage - For scene state restoration
struct ContentView: View {
@SceneStorage("selectedTab") var selectedTab = "home"
var body: some View {
TabView(selection: $selectedTab) {
HomeView()
.tabItem { Label("Home", systemImage: "house") }
.tag("home")
SettingsView()
.tabItem { Label("Settings", systemImage: "gear") }
.tag("settings")
}
}
}
Note: @StateObject is similar to @ObservedObject but ensures the object isn't recreated during view updates. @AppStorage provides easy access to UserDefaults, while @SceneStorage helps with state restoration.
Animation
Implicit Animation
// Implicit animation with .animation() modifier
struct AnimatedCircle: View {
@State private var isTapped = false
var body: some View {
Circle()
.fill(isTapped ? .red : .blue)
.frame(
width: isTapped ? 100 : 50,
height: isTapped ? 100 : 50
)
.animation(.easeInOut(duration: 0.3), value: isTapped)
.onTapGesture {
isTapped.toggle()
}
}
}
// Spring animation
Circle()
.scaleEffect(isTapped ? 1.5 : 1.0)
.animation(
.spring(response: 0.3, dampingFraction: 0.2),
value: isTapped
)
// Multiple animations with different timing
Rectangle()
.fill(isTapped ? .green : .yellow)
.animation(.easeInOut(duration: 0.5), value: isTapped)
.rotationEffect(.degrees(isTapped ? 45 : 0))
.animation(.easeInOut(duration: 1.0), value: isTapped)
// Animation with delay
Text("Hello")
.opacity(isVisible ? 1.0 : 0.0)
.animation(
.easeInOut(duration: 0.5)
.delay(0.2),
value: isVisible
)
Note: Implicit animations automatically animate all animatable properties that change within the view. The animation is triggered when the specified value changes.
Explicit Animation
// Explicit animation with withAnimation
struct AnimatedButton: View {
@State private var isPressed = false
var body: some View {
Button(action: {
withAnimation(.easeInOut(duration: 0.3)) {
isPressed.toggle()
}
}) {
Text("Press Me")
.padding()
.background(isPressed ? .red : .blue)
.foregroundColor(.white)
.cornerRadius(10)
.scaleEffect(isPressed ? 0.9 : 1.0)
}
}
}
// Complex animation sequence
Button("Animate") {
withAnimation(.easeInOut(duration: 0.3)) {
phase1.toggle()
}
withAnimation(.easeInOut(duration: 0.6).delay(0.3)) {
phase2.toggle()
}
}
// Custom animation
withAnimation(
.timingCurve(0.68, -0.6, 0.32, 1.6, duration: 0.8)
) {
customValue.toggle()
}
// Transaction for more control
var transaction = Transaction()
transaction.animation = .easeInOut(duration: 0.5)
transaction.disablesAnimations = false
withTransaction(transaction) {
animatedValue = newValue
}
Note: Explicit animations using withAnimation give you more control over when and how animations occur. You can animate specific state changes and chain multiple animations together.
Advanced Topics
View Composition
// Extract subviews for better organization
struct ProfileView: View {
var body: some View {
VStack {
ProfileHeader()
ProfileDetails()
ProfileActions()
}
}
}
struct ProfileHeader: View {
var body: some View {
HStack {
Image("avatar")
.resizable()
.frame(width: 60, height: 60)
.clipShape(Circle())
VStack(alignment: .leading) {
Text("John Doe")
.font(.title2)
Text("iOS Developer")
.foregroundColor(.secondary)
}
Spacer()
}
.padding()
}
}
// ViewBuilder for complex conditional views
struct ConditionalView: View {
var condition: Bool
var body: some View {
VStack {
if condition {
Text("Condition is true")
Image(systemName: "checkmark.circle.fill")
.foregroundColor(.green)
} else {
Text("Condition is false")
Image(systemName: "xmark.circle.fill")
.foregroundColor(.red)
}
}
}
}
// Custom view modifier
struct CardModifier: ViewModifier {
func body(content: Content) -> some View {
content
.padding()
.background(Color.white)
.cornerRadius(10)
.shadow(color: .black.opacity(0.1), radius: 5, x: 0, y: 2)
}
}
// Extension for easier use
extension View {
func cardStyle() -> some View {
modifier(CardModifier())
}
}
// Usage
Text("Card Content")
.cardStyle()
Note: Breaking complex views into smaller subviews improves readability and maintainability. Custom view modifiers help apply consistent styling across your app.
Performance & Advanced
// LazyVStack and LazyHStack for efficient scrolling
ScrollView {
LazyVStack {
ForEach(1...1000, id: \.self) { index in
RowView(index: index)
.onAppear {
// Load more data when approaching end
if index > items.count - 5 {
loadMoreData()
}
}
}
}
}
// EquatableView for preventing unnecessary redraws
struct UserView: View, Equatable {
let user: User
var body: some View {
Text(user.name)
}
static func == (lhs: UserView, rhs: UserView) -> Bool {
lhs.user.id == rhs.user.id
}
}
// Using .equatable()
UserView(user: currentUser)
.equatable()
// GeometryReader for responsive layouts
struct ResponsiveView: View {
var body: some View {
GeometryReader { geometry in
if geometry.size.width > 600 {
HStack {
Sidebar()
MainContent()
}
} else {
VStack {
MainContent()
Sidebar()
}
}
}
}
}
// PreferenceKey for communicating size information
struct SizePreferenceKey: PreferenceKey {
static var defaultValue: CGSize = .zero
static func reduce(value: inout CGSize, nextValue: () -> CGSize) {
value = nextValue()
}
}
struct SizeReader: View {
var body: some View {
GeometryReader { geometry in
Color.clear
.preference(key: SizePreferenceKey.self, value: geometry.size)
}
}
}
Note: Use Lazy stacks for better performance with large datasets. EquatableView prevents unnecessary view updates. GeometryReader helps create responsive layouts that adapt to different screen sizes.
Quick Reference
Property Wrappers Reference
| Wrapper | Purpose | When to Use |
|---|---|---|
@State |
Local view state | Simple value types owned by the view |
@Binding |
Two-way connection | Sharing state with parent views |
@ObservedObject |
External reference type | Complex data owned elsewhere |
@StateObject |
Owned observable object | Creating and owning observable objects |
@EnvironmentObject |
Shared data | Data used across many views |
@Environment |
System settings | Accessing system values |
@AppStorage |
UserDefaults | Simple persistence |
@SceneStorage |
Scene state restoration | Preserving scene state |
Layout Reference
| Container | Purpose | Direction |
|---|---|---|
VStack |
Vertical arrangement | Top to bottom |
HStack |
Horizontal arrangement | Leading to trailing |
ZStack |
Overlapping views | Back to front |
LazyVStack |
Efficient vertical scrolling | Top to bottom |
LazyHStack |
Efficient horizontal scrolling | Leading to trailing |
List |
Scrollable list of items | Vertical (typically) |
ScrollView |
Custom scrollable content | Any direction |
Form |
Grouped input controls | Vertical (typically) |
Ready to build amazing apps with SwiftUI?
Practice these concepts and explore the official SwiftUI documentation for more advanced techniques.