Swift & SwiftUI in 2026
Swift is a fast, type-safe, memory-safe language, and SwiftUI is Apple's declarative UI framework for building apps across iOS, iPadOS, macOS, watchOS, and visionOS. Instead of imperatively mutating views, you describe what the UI should look like for a given state, and the framework diffs and re-renders when that state changes.
Modern Swift 6 enforces strict data-race safety at compile time, and SwiftUI has adopted the Observation framework (the @Observable macro) as the standard way to model reference-type state. This guide covers the language essentials, then builds up to a small networked MVVM app.
Swift Language Basics
Use let for constants and var for variables. Prefer let by default — immutability makes code safer and lets the compiler optimize. Types are inferred but can be annotated explicitly.
let appName = "Contentin" // inferred String, immutable
var counter: Int = 0 // explicit type, mutable
counter += 1
let pi = 3.14159 // Double
let isReady = true // Bool
let scores: [Int] = [10, 20, 30] // Array<Int>
let user: [String: Int] = ["age": 30] // Dictionary
// String interpolation
print("\(appName) has \(scores.count) scores")
Optionals
An optional (T?) either holds a value or is nil. This is Swift's answer to null-pointer bugs: you must explicitly unwrap before use. Use if let / guard let for safe unwrapping and ?? to supply a default.
var nickname: String? = nil
// Optional binding (shorthand: same-name unwrap)
if let nickname { print(nickname) } else { print("no nickname") }
// guard: exit early if nil, keeps happy path flat
func greet(_ name: String?) {
guard let name else { return }
print("Hi, \(name)")
}
// Nil-coalescing and optional chaining
let display = nickname ?? "Guest"
let count = nickname?.count ?? 0
Structs, Enums & Protocols
Structs are value types (copied on assignment) and are the default modeling tool in Swift. Enums model a fixed set of cases and can carry associated values. Protocols define capabilities that types conform to — the basis for Swift's protocol-oriented design.
struct Task: Identifiable, Codable {
let id: UUID
var title: String
var isDone: Bool = false
}
enum Priority: String, CaseIterable {
case low, medium, high
}
enum LoadState {
case idle
case loading
case loaded([Task])
case failed(Error) // associated value
}
protocol Repository {
func fetch() async throws -> [Task]
}
Closures
Closures are self-contained blocks of functionality that can be passed around. Trailing-closure syntax makes higher-order functions read cleanly.
let names = ["Zoe", "Amir", "Kai"]
let sorted = names.sorted { $0 < $1 } // shorthand args
let upper = names.map { $0.uppercased() }
let short = names.filter { $0.count < 4 }
let total = [1, 2, 3].reduce(0, +)
Declarative Views
A SwiftUI view is a struct conforming to the View protocol with a computed body. Views are cheap, disposable value types — the framework recreates them freely and only re-renders what changed.
import SwiftUI
struct WelcomeView: View {
var body: some View {
VStack(spacing: 12) {
Text("Welcome")
.font(.largeTitle)
.fontWeight(.bold)
Text("Let's build something.")
.foregroundStyle(.secondary)
HStack {
Image(systemName: "swift")
Text("Swift 6")
}
}
.padding()
}
}
#Preview { WelcomeView() }
Xcode Previews
The #Preview macro renders your view live in the canvas without launching the simulator. Add several previews with different data or a .preferredColorScheme(.dark) modifier to check states side by side.
State & Property Wrappers
Because views are recreated on every render, ordinary stored properties can't hold mutable UI state. SwiftUI provides property wrappers that give state a durable home the framework manages for you.
| Wrapper | Use for |
|---|---|
| @State | View-local value owned by this view |
| @Binding | Two-way reference to state owned elsewhere |
| @Observable | A reference-type model class (Observation) |
| @Environment | Shared values injected down the tree |
struct CounterView: View {
@State private var count = 0
var body: some View {
VStack {
Text("Count: \(count)")
Button("Increment") { count += 1 }
Toggle("Enabled", isOn: $isOn) // $ makes a Binding
}
}
@State private var isOn = false
}
// A child view receives a two-way Binding
struct StepperRow: View {
@Binding var value: Int
var body: some View {
Stepper("Value: \(value)", value: $value)
}
}
The @Observable Model
For shared, reference-type state, the modern @Observable macro replaces the older ObservableObject / @Published pattern. SwiftUI automatically tracks exactly which properties a view reads and re-renders only when those change.
import Observation
@Observable
final class Settings {
var username = "guest"
var notifications = true
}
struct ProfileView: View {
@State private var settings = Settings() // own the object
var body: some View {
Form {
TextField("Name", text: $settings.username)
Toggle("Notify", isOn: $settings.notifications)
}
}
}
The @Observable macro
No more marking every field with @Published. Every stored property of an @Observable class is tracked automatically, and views subscribe only to the properties they actually read — cutting unnecessary re-renders.
Lists & ForEach
List renders scrolling rows efficiently. Pair it with ForEach over an array of Identifiable data. Swipe actions and deletion are built in.
struct TaskList: View {
@State private var tasks: [Task] = []
var body: some View {
List {
ForEach(tasks) { task in
HStack {
Image(systemName: task.isDone ? "checkmark.circle.fill" : "circle")
Text(task.title)
}
}
.onDelete { tasks.remove(atOffsets: $0) }
}
}
}
Navigation
NavigationStack is the modern container for push-style navigation. Use NavigationLink with a value plus a navigationDestination for type-driven, data-based routing.
struct RootView: View {
let tasks: [Task]
var body: some View {
NavigationStack {
List(tasks) { task in
NavigationLink(task.title, value: task)
}
.navigationTitle("Tasks")
.navigationDestination(for: Task.self) { task in
DetailView(task: task)
}
}
}
}
Swift Concurrency: async/await
Swift's structured concurrency replaces nested completion handlers with linear async / await code. Suspension points are explicit, and the compiler enforces that UI updates happen on the main actor. Kick off async work from a view with the .task modifier, which auto-cancels when the view disappears.
func loadNumbers() async throws -> [Int] {
try await Task.sleep(for: .seconds(1))
return [1, 2, 3]
}
// Run two calls concurrently with async let
func loadAll() async throws {
async let a = loadNumbers()
async let b = loadNumbers()
let combined = try await a + b
print(combined)
}
Fetching Data: URLSession + Codable
Decode JSON into Swift types by conforming them to Codable. The async URLSession.data(from:) API returns bytes and a response you can decode directly.
struct Post: Codable, Identifiable {
let id: Int
let title: String
let body: String
}
func fetchPosts() async throws -> [Post] {
let url = URL(string: "https://api.example.com/posts")!
let (data, response) = try await URLSession.shared.data(from: url)
guard let http = response as? HTTPURLResponse,
http.statusCode == 200 else {
throw URLError(.badServerResponse)
}
return try JSONDecoder().decode([Post].self, from: data)
}
Forms
Form groups inputs into platform-styled sections. Bind each control to state with $.
struct NewTaskForm: View {
@State private var title = ""
@State private var priority: Priority = .medium
var body: some View {
Form {
Section("Details") {
TextField("Title", text: $title)
Picker("Priority", selection: $priority) {
ForEach(Priority.allCases, id: \.self) { p in
Text(p.rawValue.capitalized).tag(p)
}
}
}
Button("Save") { /* persist */ }
.disabled(title.isEmpty)
}
}
}
MVVM Architecture
Model–View–ViewModel keeps views thin. The view renders state and forwards intents; an @Observable view model holds state and business logic. Marking the view model @MainActor guarantees its mutations run on the main thread — safe for UI.
@MainActor
@Observable
final class PostsViewModel {
var posts: [Post] = []
var isLoading = false
var errorMessage: String?
func load() async {
isLoading = true
defer { isLoading = false }
do {
posts = try await fetchPosts()
} catch {
errorMessage = error.localizedDescription
}
}
}
struct PostsView: View {
@State private var model = PostsViewModel()
var body: some View {
NavigationStack {
List(model.posts) { Text($0.title) }
.overlay { if model.isLoading { ProgressView() } }
.navigationTitle("Posts")
.task { await model.load() } // runs on appear
}
}
}
SwiftUI vs UIKit
| Aspect | SwiftUI | UIKit |
|---|---|---|
| Paradigm | Declarative | Imperative |
| Building block | View struct (value) | UIView / UIViewController (class) |
| State to UI | Automatic diffing | Manual updates |
| Layout | Stacks & modifiers | Auto Layout constraints |
| Previews | Live #Preview canvas | Run in simulator |
| Maturity | Modern, evolving | Battle-tested, huge API |
New apps should start in SwiftUI. When you need a control SwiftUI doesn't expose yet, wrap a UIKit view with UIViewRepresentable — the two frameworks interoperate cleanly.
Practice Exercises
- Build a
Taskstruct that isIdentifiableandCodable, then render aListof tasks with a toggle to mark each one done. - Add a
NavigationStackwith a detail screen, passing the selected task vianavigationDestination(for:). - Create an
@Observableview model that fetches JSON from a public API using asyncURLSessionandJSONDecoder, exposing loading and error states. - Wire the view model into a view with
.task, showing aProgressViewwhile loading and an error banner on failure. - Build a
Formto add a new task, disabling the Save button until the title field is non-empty, and validate with aguard. - Refactor a child row into its own view that receives an
@Binding, and verify edits propagate back to the parent's state.