{"id":"l-sw-4-2","courseId":"swift-essentials","lang":"en","kind":"lesson","summary":"Fetch JSON with URLSession’s async API, check the HTTP status, decode it into Codable structs, and show loading, loaded and failed states in SwiftUI.","html":"<p>Pocket Budget records expenses in dollars, but you spend money on holiday in euros and Egyptian pounds. To convert, the app needs today’s exchange rates from a server. This lesson replaces the pretend <code dir=\"ltr\">Task.sleep</code> from the last lesson with a real request: build a URL, fetch the data, check the response, decode the JSON into Swift types and show it. Those steps are the same for nearly every API you will ever call.</p>\n<p>The examples use a stand-in endpoint, <code dir=\"ltr\">https://api.example.com/v1/latest?base=USD</code>. Swap in a real exchange-rate provider (most have a free tier) and adjust the struct to match its JSON. The response looks like this:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"json\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">JSON</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">{</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  \"base\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"USD\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  \"updated_at\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"2026-10-02T06:00:00Z\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  \"rates\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: { </span><span style=\"color:#116329;--shiki-dark:#7EE787\">\"EUR\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0.92</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#116329;--shiki-dark:#7EE787\">\"GBP\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0.79</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#116329;--shiki-dark:#7EE787\">\"EGP\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">48.31</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> }</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<h2 id=\"sec-1\" tabindex=\"-1\">Mirror the JSON with Codable</h2>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"swift\" data-index=\"1\"><div class=\"code-head\"><span class=\"code-title\">RatesResponse.swift</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Foundation</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">struct</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> RatesResponse</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">Decodable </span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">{</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    let</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> base: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">String</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    let</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> updatedAt: Date</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    let</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> rates: [</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">String</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: Decimal]</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p><code dir=\"ltr\">Decodable</code> asks the compiler to generate the code that reads this type from an external format. Property names must match the JSON keys and the types must fit: a JSON object becomes a dictionary, a number can decode into <code dir=\"ltr\">Decimal</code>, and a missing key or wrong type throws a <code dir=\"ltr\">DecodingError</code> that names the problem. Use <code dir=\"ltr\">Codable</code> (<code dir=\"ltr\">Decodable</code> plus <code dir=\"ltr\">Encodable</code>) when you also send the type back. The struct is made of <code dir=\"ltr\">Sendable</code> parts, so it’s <code dir=\"ltr\">Sendable</code> too and can travel safely from the network code to the main actor.</p>\n<p>The JSON says <code dir=\"ltr\">updated_at</code>; Swift style says <code dir=\"ltr\">updatedAt</code>. Rather than renaming properties, configure the decoder, as the client below does.</p>\n<h2 id=\"sec-2\" tabindex=\"-1\">Fetch, check, decode</h2>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"swift\" data-index=\"2\"><div class=\"code-head\"><span class=\"code-title\">RateClient.swift</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">enum</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> RateError</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">Error </span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">{</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    case</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> badStatus</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">Int</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">struct</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> RateClient</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    let</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> endpoint </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> URL</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">string</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"https://api.example.com/v1/latest\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">!</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    func</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> latest</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">base</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">String</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">async</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> throws</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> -></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> RatesResponse {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">        let</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> url </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> endpoint.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">appending</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">queryItems</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: [</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">URLQueryItem</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">name</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"base\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">value</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: base)])</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">        let</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (data, response) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> try</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> await</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> URLSession.shared.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">data</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">from</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: url)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">        let</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> status </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (response </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">as?</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> HTTPURLResponse)</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">?</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.statusCode </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">??</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 0</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">        guard</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> status </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">==</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 200</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> else</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">            throw</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> RateError.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">badStatus</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(status)</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">        }</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">        let</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> decoder </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> JSONDecoder</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">()</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">        decoder.keyDecodingStrategy </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> .convertFromSnakeCase</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">        decoder.dateDecodingStrategy </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> .iso8601</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">        return</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> try</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> decoder.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">decode</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(RatesResponse.</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">self</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">from</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: data)</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    }</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p>Walk through it in order. <code dir=\"ltr\">appending(queryItems:)</code> builds <code dir=\"ltr\">...latest?base=USD</code> and percent-encodes values for you, which is safer than gluing strings together. The force unwrap on <code dir=\"ltr\">URL(string:)</code> is the acceptable kind from the optionals lesson: a literal you wrote, where <code dir=\"ltr\">nil</code> would mean a typo.</p>\n<p><code dir=\"ltr\">URLSession.shared.data(from:)</code> is the async API: it suspends until the whole response has arrived and returns the body bytes plus a <code dir=\"ltr\">URLResponse</code>. It throws for transport failures, such as no connection, a timeout or cancellation, but it does <strong>not</strong> throw for HTTP errors. That’s what the <code dir=\"ltr\">guard</code> is for.</p>\n<p><code dir=\"ltr\">.convertFromSnakeCase</code> maps <code dir=\"ltr\">updated_at</code> to <code dir=\"ltr\">updatedAt</code> for every key, and <code dir=\"ltr\">.iso8601</code> reads the timestamp format most APIs use. Then <code dir=\"ltr\">decode</code> either returns a fully formed <code dir=\"ltr\">RatesResponse</code> or throws.</p>\n<figure class=\"diagram\"><svg class=\"diagram-svg\" focusable=\"false\" viewBox=\"0 0 700 170\" role=\"img\" aria-labelledby=\"l-sw-4-2-t1\"><defs><marker id=\"arrow-l-sw-4-2-1\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"7\" markerHeight=\"7\" orient=\"auto-start-reverse\"><path d=\"M0 0 L10 5 L0 10 z\" class=\"d-arrowhead\"/></marker></defs>\n  <title id=\"l-sw-4-2-t1\">Five steps left to right: build the URL, fetch with data(from:), check the status code, decode JSON into RatesResponse, then set state on the main actor so the view updates.</title>\n  <rect class=\"d-box\" x=\"5\" y=\"50\" width=\"120\" height=\"64\" rx=\"10\"/>\n  <text class=\"d-label-strong\" x=\"65\" y=\"78\" text-anchor=\"middle\">Build URL</text>\n  <text class=\"d-label-muted\" x=\"65\" y=\"99\" text-anchor=\"middle\">query items</text>\n  <path class=\"d-arrow\" d=\"M125 82 L143 82\" marker-end=\"url(#arrow-l-sw-4-2-1)\"/>\n  <rect class=\"d-box-accent\" x=\"145\" y=\"50\" width=\"125\" height=\"64\" rx=\"10\"/>\n  <text class=\"d-label-strong\" x=\"207\" y=\"78\" text-anchor=\"middle\">Fetch</text>\n  <text class=\"d-code\" x=\"207\" y=\"99\" text-anchor=\"middle\">data(from:)</text>\n  <path class=\"d-arrow\" d=\"M270 82 L288 82\" marker-end=\"url(#arrow-l-sw-4-2-1)\"/>\n  <rect class=\"d-box-warn\" x=\"290\" y=\"50\" width=\"120\" height=\"64\" rx=\"10\"/>\n  <text class=\"d-label-strong\" x=\"350\" y=\"78\" text-anchor=\"middle\">Check</text>\n  <text class=\"d-label-muted\" x=\"350\" y=\"99\" text-anchor=\"middle\">status 200</text>\n  <path class=\"d-arrow\" d=\"M410 82 L428 82\" marker-end=\"url(#arrow-l-sw-4-2-1)\"/>\n  <rect class=\"d-box-primary\" x=\"430\" y=\"50\" width=\"120\" height=\"64\" rx=\"10\"/>\n  <text class=\"d-label-strong\" x=\"490\" y=\"78\" text-anchor=\"middle\">Decode</text>\n  <text class=\"d-label-muted\" x=\"490\" y=\"99\" text-anchor=\"middle\">JSONDecoder</text>\n  <path class=\"d-arrow\" d=\"M550 82 L568 82\" marker-end=\"url(#arrow-l-sw-4-2-1)\"/>\n  <rect class=\"d-box-success\" x=\"570\" y=\"50\" width=\"125\" height=\"64\" rx=\"10\"/>\n  <text class=\"d-label-strong\" x=\"632\" y=\"78\" text-anchor=\"middle\">Show</text>\n  <text class=\"d-label-muted\" x=\"632\" y=\"99\" text-anchor=\"middle\">main actor</text>\n  <text class=\"d-label-muted\" x=\"350\" y=\"150\" text-anchor=\"middle\">Any step can throw; one catch in the model turns it into the failed state</text>\n</svg><figcaption>From URL to pixels</figcaption></figure>\n<aside class=\"callout callout-mistake\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3\"/><path d=\"M12 9v4\"/><path d=\"M12 17h.01\"/></svg><span><span class=\"callout-kind\">Common mistake</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>Decoding without checking the status</span></p>\n<p>Skip the status check and a 500 response sends an HTML error page into <code dir=\"ltr\">JSONDecoder</code>, which throws a confusing “data couldn’t be read because it isn’t in the correct format”. You spend an hour debugging the decoder when the server told you exactly what went wrong. Check the status first, and include it in your error.</p>\n</aside>\n<p>Not every failure deserves the same message, either. A thrown <code dir=\"ltr\">URLError</code> carries a <code dir=\"ltr\">code</code>, so you can tell “offline” (<code dir=\"ltr\">.notConnectedToInternet</code>) apart from “timed out” (<code dir=\"ltr\">.timedOut</code>) and say “You’re offline. Rates will refresh when you reconnect” instead of a generic apology. Users forgive an app that explains itself.</p>\n<h2 id=\"sec-3\" tabindex=\"-1\">Loading states the view can trust</h2>\n<p>The enum idea from the enums lesson now pays off. A model on the main actor owns the state, and the view switches over it:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"swift\" data-index=\"3\"><div class=\"code-head\"><span class=\"code-title\">RatesModel.swift</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">@MainActor</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">@Observable</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">final</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> class</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> RatesModel</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    enum</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> LoadState</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">        case</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> idle</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">        case</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> loading</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">        case</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> loaded</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(RatesResponse)</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">        case</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> failed</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">String</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    }</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    var</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> state: LoadState </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> .idle</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    private</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> let</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> client </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> RateClient</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">()</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    func</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> load</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">() </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">async</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">        state </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> .loading</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">        do</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">            let</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> response </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> try</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> await</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> client.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">latest</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">base</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"USD\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">            state </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> .</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">loaded</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(response)</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">        } </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">catch</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">            state </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> .</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">failed</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(error.localizedDescription)</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">        }</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    }</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"swift\" data-index=\"4\"><div class=\"code-head\"><span class=\"code-title\">RatesView.swift</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">struct</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> RatesView</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">View </span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">{</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    @State</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> private</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> var</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> model </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> RatesModel</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">()</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    var</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> body: </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">some</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> View {</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">        List</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">            switch</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> model.state {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">            case</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> .idle, .loading</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">                ProgressView</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">()</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">            case</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> .</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">loaded</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">let</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> response)</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">                ForEach</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(response.rates.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">keys</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">sorted</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(), </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">id</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: \\.</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">self</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) { code </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">in</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">                    LabeledContent</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(code, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">value</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: response.rates[code] </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">??</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 0</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">                                   format</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: .number.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">precision</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">fractionLength</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">2</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">...</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">4</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)))</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">                }</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">            case</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> .</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">failed</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">let</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> message)</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">                ContentUnavailableView</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"Couldn't load rates\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">                                       systemImage</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"wifi.exclamationmark\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">                                       description</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">Text</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(message))</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">            }</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">        }</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">        .</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">navigationTitle</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"Exchange Rates\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">        .</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">task</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">await</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> model.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">load</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">() }</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">        .</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">refreshable</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">await</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> model.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">load</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">() }</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    }</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p><code dir=\"ltr\">.task</code> loads once when the screen appears, and <code dir=\"ltr\">.refreshable</code> adds pull-to-refresh with the same function. Because <code dir=\"ltr\">RatesModel</code> is <code dir=\"ltr\">@MainActor</code>, assigning <code dir=\"ltr\">state</code> is always safe for the UI, and Swift 6 checks that for you. Every error path ends in <code dir=\"ltr\">.failed</code>, so the user never stares at a spinner that will never stop. Dictionary keys have no order, so the view sorts them for a stable list.</p>\n<aside class=\"callout callout-tip\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5\"/><path d=\"M9 18h6\"/><path d=\"M10 22h4\"/></svg><span><span class=\"callout-kind\">Tip</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>Where the decoding runs</span></p>\n<p>With main-actor default isolation, <code dir=\"ltr\">RateClient</code> runs on the main actor too. The request itself doesn’t block it; <code dir=\"ltr\">await</code> suspends while the bytes arrive. Decoding a few hundred bytes on the main thread is fine. For multi-megabyte responses, move decoding into an <code dir=\"ltr\">async</code> function marked <code dir=\"ltr\">@concurrent</code> so it runs in the background, and declare the model <code dir=\"ltr\">nonisolated struct RatesResponse</code>; otherwise its <code dir=\"ltr\">Decodable</code> conformance is main-actor isolated too and the compiler won’t let background code use it.</p>\n</aside>\n<p>iOS also requires HTTPS by default through App Transport Security, so a plain <code dir=\"ltr\">http://</code> URL fails unless you add an exception. Use HTTPS and you’ll never need one.</p>\n<p>Pocket Budget can now talk to the outside world. It still forgets its own expenses when you quit, so next you’ll save them on the device with SwiftData.</p>\n","toc":[{"id":"sec-1","text":"Mirror the JSON with Codable","level":2},{"id":"sec-2","text":"Fetch, check, decode","level":2},{"id":"sec-3","text":"Loading states the view can trust","level":2}],"takeaways":["<code dir=\"ltr\">try await URLSession.shared.data(from: url)</code> returns the body and the response; it throws for network failures but not for HTTP errors like 404 or 500.","Always check the <code dir=\"ltr\">HTTPURLResponse</code> status code before decoding, and turn a bad status into your own error.","Mirror the JSON with <code dir=\"ltr\">Decodable</code> structs; <code dir=\"ltr\">keyDecodingStrategy</code> and <code dir=\"ltr\">dateDecodingStrategy</code> handle snake_case keys and ISO 8601 dates.","Model the screen’s lifecycle as a <code dir=\"ltr\">LoadState</code> enum so loading, data and errors can never be shown at the same time."],"further":[{"title":"Fetching website data into memory","url":"https://developer.apple.com/documentation/foundation/fetching-website-data-into-memory"},{"title":"Encoding and Decoding Custom Types","url":"https://developer.apple.com/documentation/foundation/encoding-and-decoding-custom-types"},{"title":"URLSession","url":"https://developer.apple.com/documentation/foundation/urlsession"}],"quiz":[{"q":"<p>The server answers a request with HTTP 500 and an HTML error page. What does <code dir=\"ltr\">try await URLSession.shared.data(from: url)</code> do?</p>","options":[{"text":"It throws a <code dir=\"ltr\">URLError</code> with the status code.","why":"URLSession only throws when the request couldn’t complete. A 500 is a complete response."},{"text":"It returns the HTML as <code dir=\"ltr\">data</code> and a response whose <code dir=\"ltr\">statusCode</code> is 500.","why":"Correct. That’s why you check the status yourself before decoding."},{"text":"It retries automatically until it gets a 200.","why":"URLSession doesn’t retry on HTTP errors; retry policy is up to you."}],"answer":1},{"q":"<p>The JSON has a key <code dir=\"ltr\">updated_at</code>, and your struct has <code dir=\"ltr\">let updatedAt: Date</code>. Decoding fails with <code dir=\"ltr\">keyNotFound</code>. What’s the smallest fix?</p>","options":[{"text":"Rename the property to <code dir=\"ltr\">updated_at</code>.","why":"It works, but it drags the server’s naming style into your Swift code."},{"text":"Make the property optional: <code dir=\"ltr\">let updatedAt: Date?</code>.","why":"The error disappears, but the date is now always <code dir=\"ltr\">nil</code> because the key still doesn’t match."},{"text":"Set <code dir=\"ltr\">decoder.keyDecodingStrategy = .convertFromSnakeCase</code>.","why":"Correct. The decoder maps <code dir=\"ltr\">updated_at</code> to <code dir=\"ltr\">updatedAt</code> for every key, so your Swift names stay idiomatic."}],"answer":2},{"q":"<p>Why does the rates screen use one <code dir=\"ltr\">LoadState</code> enum instead of <code dir=\"ltr\">isLoading</code>, <code dir=\"ltr\">rates</code> and <code dir=\"ltr\">errorMessage</code> properties?</p>","options":[{"text":"Separate properties allow impossible combinations, such as loading and failed at once; the enum allows exactly one state.","why":"Correct. The view switches over the enum and handles each state exactly once."},{"text":"SwiftUI can only observe one property per class.","why":"Observation tracks every property a body reads; that’s not the reason."},{"text":"Enums make network requests faster.","why":"The enum changes how state is modelled, not how fast the request runs."}],"answer":0}],"exercise":{"kind":"order","items":["start-task","set-loading","build-url","fetch","check-status","decode","set-loaded"],"title":"From screen appearance to rates on screen","prompt":"<p>Put the steps of Pocket Budget’s exchange-rate screen in the order they happen at runtime, from the moment the screen appears to the moment the rates are visible.</p>\n","hints":["Nothing happens until SwiftUI starts the async work, and the user should see a spinner before any network call begins.","You can’t fetch before you have a URL, and you shouldn’t decode before you know the response is a success.","The last step is the one that makes SwiftUI redraw."],"explanation":"<p><code dir=\"ltr\">.task</code> starts <code dir=\"ltr\">model.load()</code> when the view appears. The model sets <code dir=\"ltr\">.loading</code> first so the spinner shows immediately. The client builds the URL with its query items, then <code dir=\"ltr\">URLSession.shared.data(from:)</code> suspends until the response arrives. The status check comes before decoding, because URLSession doesn’t throw for a 404 or 500 and an error page would fail to decode with a misleading message. Decoding produces a <code dir=\"ltr\">RatesResponse</code>, and assigning <code dir=\"ltr\">.loaded(response)</code> on the main actor is what makes SwiftUI re-render the list.</p>\n<p>If any step after <code dir=\"ltr\">set-loading</code> throws, the model’s single <code dir=\"ltr\">catch</code> sets <code dir=\"ltr\">.failed(message)</code> instead.</p>\n","checkLabels":{},"itemText":{"start-task":"SwiftUI runs <code dir=\"ltr\">.task { await model.load() }</code> as the view appears","set-loading":"The model sets <code dir=\"ltr\">state = .loading</code>, so the view shows a <code dir=\"ltr\">ProgressView</code>","build-url":"Build the URL: <code dir=\"ltr\">endpoint.appending(queryItems: [URLQueryItem(name: &quot;base&quot;, value: &quot;USD&quot;)])</code>","fetch":"<code dir=\"ltr\">let (data, response) = try await URLSession.shared.data(from: url)</code>","check-status":"Check <code dir=\"ltr\">(response as? HTTPURLResponse)?.statusCode == 200</code>, otherwise throw <code dir=\"ltr\">RateError.badStatus</code>","decode":"<code dir=\"ltr\">try decoder.decode(RatesResponse.self, from: data)</code> with snake_case and ISO 8601 strategies","set-loaded":"Assign <code dir=\"ltr\">state = .loaded(response)</code> on the main actor, and SwiftUI redraws the list"}},"words":696,"readMinutes":3}