Forms, User Input & Basic Networking (URLSession, async/await)

iOS Development Fundamentals

Chapter 9 · Forms, User Input & Basic Networking

This chapter closes out the course's own core skills with two real, distinct pieces: gathering structured user input through a real Form, and fetching real data from the internet using Swift's modern async/await concurrency system.

Form and Input Controls

struct NewTaskView: View { @State private var title = "" @State private var isUrgent = false @State private var priority = 1 var body: some View { Form { TextField("Title", text: $title) Toggle("Urgent", isOn: $isUrgent) Stepper("Priority: \(priority)", value: $priority, in: 1...5) } } }
A Real, Familiar Pattern
Every real control above binds to a @State property via the $ projected-value syntax from Chapter 6 — nothing new here syntactically, just three new real, purpose-built controls: TextField for text, Toggle for a boolean switch, and Stepper for a bounded numeric value. Form itself is a real, styled container specifically designed for this kind of grouped input, giving it the native, sectioned look used throughout Settings and similar Apple apps.

Swift Concurrency: async/await

Introduced in Swift 5.5 (iOS 15, 2021), async/await is Swift's real, current way of writing asynchronous code that reads top-to-bottom, like ordinary synchronous code, instead of nesting completion-handler closures.

func fetchGreeting() async -> String { // imagine a real network call happens here return "Hello from the network" } func showGreeting() async { let greeting = await fetchGreeting() print(greeting) }

A function marked async can pause at an await point without blocking the thread it runs on — real, genuine cooperative suspension, not a busy-wait.

Fetching Real Data with URLSession

struct Quote: Decodable { let text: String let author: String } func fetchQuote() async throws -> Quote { let url = URL(string: "https://api.example.com/quote")! let (data, response) = try await URLSession.shared.data(from: url) guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else { throw URLError(.badServerResponse) } return try JSONDecoder().decode(Quote.self, from: data) }
PieceReal Role
URLSession.shared.data(from:)A real, built-in async method that fetches a URL's data, returning (Data, URLResponse).
DecodableA real protocol — conforming lets a type be built directly from JSON by JSONDecoder, matching properties to real JSON keys by name.
try awaitBoth real keywords needed together — data(from:) is genuinely async throws, so calling it needs both.

Calling Async Code from a View: .task

A SwiftUI view's own body can't itself be async.task {} is the real, dedicated modifier that bridges the two, running its own async closure when the view first appears.

struct QuoteView: View { @State private var quote: Quote? var body: some View { Group { if let quote { Text(quote.text) } else { ProgressView() } } .task { do { quote = try await fetchQuote() } catch { print("Failed to load quote: \(error)") } } } }
A Real, Genuinely Useful Lifecycle Guarantee
Work started inside .task {} is automatically cancelled if the view disappears before it finishes — real, built-in behavior, not something written manually. Navigating away from QuoteView mid-fetch doesn't leave a stray network request trying to update a @State property on a view that no longer exists.
A Real, Genuine Requirement
fetchQuote() can genuinely fail — a bad URL, no network, a malformed response — so it's marked throws, and every real call site needs try plus a surrounding do/catch (or a further throws of its own) to handle that. Skipping error handling here isn't optional the way ignoring a return value might be elsewhere.

Hands-On Exercises

Exercise 1

Build a Form with a TextField for an email address, a Toggle for "Subscribe to newsletter," and a Stepper for "Number of guests" ranging 1 to 10, each backed by its own real @State property.

📄 View solution
Exercise 2

Define a struct Joke: Decodable with a real setup: String and punchline: String, write an async throws function fetchJoke() -> Joke following this chapter's own fetchQuote() pattern, and a view using .task to load and display it.

📄 View solution
Exercise 3

Explain, in your own words, why .task {} automatically cancelling its own work when a view disappears is a genuinely important real safety guarantee, connecting your answer to Chapter 6's own @State externalized-storage mechanism.

📄 View solution

Chapter 9 Quick Reference

  • Form + TextField/Toggle/Stepper — grouped, natively-styled user input, bound via the same @State/$ pattern from Chapter 6
  • async/await (Swift 5.5, iOS 15, 2021) — Swift's real, current concurrency system for readable asynchronous code
  • URLSession.shared.data(from:) — real, built-in async method returning (Data, URLResponse)
  • Decodable + JSONDecoder — real, automatic JSON-to-struct decoding
  • .task {} — bridges async code into a view's own lifecycle, automatically cancelling if the view disappears mid-work