Bryn Flow

🍎 Learn iOS (Swift)

14 core topics for building native iOS apps, from Swift fundamentals through SwiftUI, networking, and Core Data. Each topic includes a short explanation and a working code sample.

← All Tracks

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.

Ready for another track?

Explore Android, React Native, Flutter, or Dart next.

Back to All Tracks