1 Swift Basics
Swift is a strongly-typed, safety-focused language. You declare variables with var (mutable) or let (constant), and Swift infers types automatically so you rarely need to write them explicitly.
var name = "Ada" // inferred as String
let birthYear = 1815 // inferred as Int, immutable
var score: Double = 92.5 // explicit type annotation
name += " Lovelace"
print("\(name) was born in \(birthYear), score: \(score)")
// Constants can't be reassigned:
// birthYear = 1816 // compile-time error
Prefer let over var whenever a value won't change — it makes intent clear and prevents bugs.
2 Optionals & Optional Binding
An optional (Type?) represents a value that might be missing. Swift forces you to unwrap it safely before use, most commonly with if let, guard let, or the nil-coalescing operator ??.
var middleName: String? = nil
if let name = middleName {
print("Middle name: \(name)")
} else {
print("No middle name")
}
func greet(_ name: String?) {
guard let name = name else {
print("No name given")
return
}
print("Hello, \(name)!")
}
let display = middleName ?? "N/A"
Force-unwrapping with ! crashes if the value is nil — avoid it unless you're certain the value exists.
3 UIKit vs SwiftUI
UIKit is Apple's original imperative UI framework, built around view controllers and manual layout with Auto Layout constraints. SwiftUI, introduced in 2019, is a declarative framework where you describe what the UI should look like for a given state, and the framework updates the screen automatically when that state changes.
// UIKit (imperative): you manually create and configure views
let label = UILabel()
label.text = "Hello"
label.textColor = .systemBlue
view.addSubview(label)
// SwiftUI (declarative): you describe the result
struct ContentView: View {
var body: some View {
Text("Hello")
.foregroundColor(.blue)
}
}
New Apple projects typically default to SwiftUI, but many production apps still mix both frameworks.
4 SwiftUI Views & Modifiers
In SwiftUI, everything on screen is a View — a lightweight struct describing content. Modifiers like .padding() or .background() return a new, wrapped view, so they chain together to build up styling and layout.
struct GreetingView: View {
var body: some View {
Text("Welcome to Bryn Flow")
.font(.title2)
.fontWeight(.bold)
.foregroundColor(.white)
.padding()
.background(Color.blue)
.cornerRadius(12)
}
}
Modifier order matters — .padding().background(...) pads first then colors the padded area, while reversing them colors only the original text.
5 State & Binding
@State marks a view's own local, mutable data — SwiftUI re-renders the view whenever it changes. @Binding lets a child view read and write a value owned by a parent, creating a two-way connection.
struct CounterView: View {
@State private var count = 0
var body: some View {
VStack {
Text("Count: \(count)")
ToggleSwitch(isOn: $isEnabled)
}
}
@State private var isEnabled = false
}
struct ToggleSwitch: View {
@Binding var isOn: Bool
var body: some View {
Toggle("Enabled", isOn: $isOn)
}
}
The $ prefix creates a binding from a @State property to pass down to a child view.
6 ObservableObject & @Published
When state needs to be shared across multiple views, use a class conforming to ObservableObject. Properties marked @Published automatically notify any view observing the object whenever they change.
class UserSettings: ObservableObject {
@Published var username: String = "Guest"
@Published var isPremium: Bool = false
}
struct ProfileView: View {
@StateObject private var settings = UserSettings()
var body: some View {
VStack {
Text("User: \(settings.username)")
Button("Upgrade") {
settings.isPremium = true
}
}
}
}
Use @StateObject to create and own the object, and @ObservedObject when receiving one from a parent.
8 Lists & ForEach
List renders a scrollable, styled collection of rows, and ForEach generates a view for each item in a collection. Together they're the standard way to display dynamic data in SwiftUI.
struct Fruit: Identifiable {
let id = UUID()
let name: String
}
struct FruitListView: View {
let fruits = [Fruit(name: "Apple"), Fruit(name: "Banana"), Fruit(name: "Cherry")]
var body: some View {
List {
ForEach(fruits) { fruit in
Text(fruit.name)
}
}
}
}
Items must be Identifiable (or you supply an id: key path) so SwiftUI can track them efficiently across updates.
9 Networking with URLSession & async/await
URLSession is Apple's built-in HTTP client. Combined with async/await, fetching data reads like straight-line code instead of nested completion handlers.
struct Post: Decodable {
let id: Int
let title: 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)
}
Call this from a Task { } or an async view lifecycle method — you can't call await from ordinary synchronous code.
10 Codable & JSON Parsing
Codable (a combination of Encodable and Decodable) lets Swift automatically convert your structs to and from JSON, with no manual parsing code required for straightforward shapes.
struct User: Codable {
let id: Int
let name: String
let email: String
}
let json = """
{"id": 1, "name": "Grace", "email": "grace@example.com"}
""".data(using: .utf8)!
let user = try JSONDecoder().decode(User.self, from: json)
print(user.name) // "Grace"
let encoded = try JSONEncoder().encode(user)
Use CodingKeys enums when your JSON field names (like snake_case) don't match your Swift property names.
11 Core Data Basics
Core Data is Apple's on-device persistence framework. You define an entity model, then use a managed object context to create, save, and fetch objects that are automatically stored to disk.
// Assuming a "Task" entity with a "title" attribute in the .xcdatamodeld
let context = PersistenceController.shared.container.viewContext
// Create and save
let task = Task(context: context)
task.title = "Buy groceries"
try context.save()
// Fetch
let request = Task.fetchRequest()
request.predicate = NSPredicate(format: "title CONTAINS %@", "groceries")
let results = try context.fetch(request)
In SwiftUI, @FetchRequest can bind Core Data results directly to a view so it updates automatically when the store changes.
12 Async/Await & Structured Concurrency
Swift's structured concurrency lets you run async work safely with Task, and run multiple operations in parallel with async let or task groups, while the compiler tracks cancellation and errors for you.
func loadDashboard() async throws -> (String, Int) {
async let profile = fetchProfileName() // starts concurrently
async let unread = fetchUnreadCount() // starts concurrently
return try await (profile, unread)
}
struct DashboardView: View {
@State private var title = "Loading..."
var body: some View {
Text(title)
.task {
if let (name, _) = try? await loadDashboard() {
title = name
}
}
}
}
async let runs tasks concurrently, while sequential await calls run one after another.
13 View Lifecycle
SwiftUI views don't have lifecycle methods like UIKit's viewDidLoad. Instead, .onAppear runs code when a view appears on screen, and .task runs an async operation tied to the view's lifetime, automatically cancelling it if the view disappears.
struct ArticleView: View {
@State private var article: String?
var body: some View {
Group {
if let article {
Text(article)
} else {
ProgressView()
}
}
.onAppear {
print("View appeared")
}
.task {
article = try? await fetchArticle()
}
}
}
Prefer .task over .onAppear for async work — it cancels automatically, preventing wasted network calls.
14 Combine Basics
Combine is Apple's reactive framework for handling streams of asynchronous values over time. A Publisher emits values, and a Subscriber receives them — useful for things like debouncing search text or observing timers.
import Combine
class SearchViewModel: ObservableObject {
@Published var query = ""
@Published private(set) var results: [String] = []
private var cancellable: AnyCancellable?
init() {
cancellable = $query
.debounce(for: .milliseconds(300), scheduler: RunLoop.main)
.sink { [weak self] value in
self?.results = ["Result for \(value)"]
}
}
}
Since iOS 15, many simple Combine use cases (like debounced search) can also be written with async sequences instead.
Practice Quiz
Test what you just learned about iOS development.