{"id":"l-sw-4-2","courseId":"swift-essentials","lang":"ar","kind":"lesson","summary":"اجلب JSON عبر الـ API غير المتزامن في URLSession، وافحص حالة HTTP، وفكّ ترميزه إلى structs من نوع Codable، واعرض حالات التحميل والنجاح والفشل في SwiftUI.","html":"<p>يسجّل Pocket Budget المصاريف بالدولار، لكنك تنفق في إجازتك باليورو والجنيه المصري. وللتحويل، يحتاج التطبيق إلى أسعار الصرف لهذا اليوم من خادم. يستبدل هذا الدرس <code dir=\"ltr\">Task.sleep</code> التمثيلية من الدرس السابق بطلب حقيقي: ابنِ URL، واجلب البيانات، وافحص الاستجابة، وفكّ ترميز الـ JSON إلى أنواع Swift، ثم اعرضها. هذه الخطوات هي نفسها مع معظم الـ APIs التي ستستدعيها في حياتك.</p>\n<p>تستخدم الأمثلة endpoint بديلًا، <code dir=\"ltr\">https://api.example.com/v1/latest?base=USD</code>. ضع مكانه مزوّدًا حقيقيًا لأسعار الصرف (لمعظمهم باقة مجانية) وعدّل الـ struct ليطابق الـ JSON الخاص به. تبدو الاستجابة هكذا:</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=\"نسخ الكود\"><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>نسخ</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\">اعكس شكل الـ JSON باستخدام 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=\"نسخ الكود\"><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>نسخ</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> من الـ compiler أن يولّد الكود الذي يقرأ هذا النوع من صيغة خارجية. يجب أن تطابق أسماء الخصائص مفاتيح الـ JSON وأن تناسب الأنواع: كائن JSON يصبح قاموسًا، والرقم يمكن فكّ ترميزه إلى <code dir=\"ltr\">Decimal</code>، والمفتاح الغائب أو النوع الخاطئ يرمي <code dir=\"ltr\">DecodingError</code> يسمّي المشكلة. استخدم <code dir=\"ltr\">Codable</code> (أي <code dir=\"ltr\">Decodable</code> مع <code dir=\"ltr\">Encodable</code>) حين ترسل النوع أيضًا. والـ struct مبني من أجزاء <code dir=\"ltr\">Sendable</code>، فهو <code dir=\"ltr\">Sendable</code> أيضًا ويستطيع الانتقال بأمان من كود الشبكة إلى الـ main actor.</p>\n<p>الـ JSON يقول <code dir=\"ltr\">updated_at</code>، وأسلوب Swift يقول <code dir=\"ltr\">updatedAt</code>. بدل إعادة تسمية الخصائص، اضبط الـ decoder، كما يفعل العميل أدناه.</p>\n<h2 id=\"sec-2\" tabindex=\"-1\">اجلب، افحص، فكّ الترميز</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=\"نسخ الكود\"><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>نسخ</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>امشِ عليه بالترتيب. تبني <code dir=\"ltr\">appending(queryItems:)</code> العنوان <code dir=\"ltr\">...latest?base=USD</code> وتطبّق الـ percent-encoding على القيم تلقائيًا، وهذا أكثر أمانًا من لصق النصوص ببعضها. والفكّ القسري على <code dir=\"ltr\">URL(string:)</code> من النوع المقبول الذي رأيته في درس الـ optionals: قيمة كتبتها بنفسك، تعني <code dir=\"ltr\">nil</code> فيها خطأً إملائيًا.</p>\n<p><code dir=\"ltr\">URLSession.shared.data(from:)</code> هو الـ API غير المتزامن: يعلّق التنفيذ حتى تصل الاستجابة كاملة، ويعيد بايتات الجسم مع <code dir=\"ltr\">URLResponse</code>. يرمي خطأً عند فشل النقل، مثل انعدام الاتصال أو انتهاء المهلة أو الإلغاء، لكنه <strong>لا</strong> يرمي عند أخطاء HTTP. ولهذا وُجدت جملة <code dir=\"ltr\">guard</code>.</p>\n<p>يربط <code dir=\"ltr\">.convertFromSnakeCase</code> المفتاح <code dir=\"ltr\">updated_at</code> بـ <code dir=\"ltr\">updatedAt</code> لكل المفاتيح، ويقرأ <code dir=\"ltr\">.iso8601</code> صيغة الطابع الزمني التي تستخدمها معظم الـ APIs. ثم إما أن تعيد <code dir=\"ltr\">decode</code> كائن <code dir=\"ltr\">RatesResponse</code> مكتمل البناء أو ترمي خطأً.</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\">خمس خطوات من اليسار إلى اليمين: ابنِ الـ URL، واجلب باستخدام data(from:)، وافحص رمز الحالة، وفكّ ترميز الـ JSON إلى RatesResponse، ثم اضبط الـ state على الـ main actor فيتحدّث الـ view.</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\">ابنِ الـ 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\">اجلب</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\">افحص</text>\n  <text class=\"d-label-muted\" x=\"350\" y=\"99\" text-anchor=\"middle\">الحالة 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\">فكّ الترميز</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\">اعرض</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\">أي خطوة قد ترمي خطأً؛ وcatch واحدة في النموذج تحوّله إلى حالة الفشل</text>\n</svg><figcaption>من الـ URL إلى البكسلات</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\">خطأ شائع</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>فكّ الترميز دون فحص الحالة</span></p>\n<p>تخطَّ فحص الحالة، وستُرسل استجابة 500 صفحة خطأ بصيغة HTML إلى <code dir=\"ltr\">JSONDecoder</code>، الذي يرمي رسالة محيّرة: “data couldn’t be read because it isn’t in the correct format”. تقضي ساعة في تتبّع خطأ في الـ decoder بينما أخبرك الخادم بالضبط بما حدث. افحص الحالة أولًا، وضمّنها في الخطأ الخاص بك.</p>\n</aside>\n<p>وليست كل حالات الفشل تستحق الرسالة نفسها. فالـ <code dir=\"ltr\">URLError</code> الذي يُرمى يحمل <code dir=\"ltr\">code</code>، فتستطيع التمييز بين «غير متصل» (<code dir=\"ltr\">.notConnectedToInternet</code>) و«انتهت المهلة» (<code dir=\"ltr\">.timedOut</code>)، وتقول “You’re offline. Rates will refresh when you reconnect” بدل اعتذار عام. المستخدمون يسامحون التطبيق الذي يشرح نفسه.</p>\n<h2 id=\"sec-3\" tabindex=\"-1\">حالات تحميل يثق بها الـ view</h2>\n<p>فكرة الـ enum من درس الـ enums تؤتي ثمارها الآن. نموذج على الـ main actor يملك الـ state، والـ view يكتب switch عليها:</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=\"نسخ الكود\"><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>نسخ</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=\"نسخ الكود\"><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>نسخ</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> البيانات مرة واحدة حين تظهر الشاشة، ويضيف <code dir=\"ltr\">.refreshable</code> السحب للتحديث بالدالة نفسها. ولأن <code dir=\"ltr\">RatesModel</code> معلّم بـ <code dir=\"ltr\">@MainActor</code>، يكون إسناد <code dir=\"ltr\">state</code> آمنًا دائمًا للواجهة، وتتحقّق Swift 6 من ذلك عنك. وكل مسار خطأ ينتهي في <code dir=\"ltr\">.failed</code>، فلا يحدّق المستخدم أبدًا في مؤشّر تحميل لن يتوقّف. ومفاتيح القاموس لا ترتيب لها، فيرتّبها الـ view ليحصل على قائمة ثابتة.</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\">نصيحة</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>أين يُنفَّذ فكّ الترميز</span></p>\n<p>مع العزل الافتراضي على الـ main actor، يعمل <code dir=\"ltr\">RateClient</code> على الـ main actor أيضًا. الطلب نفسه لا يحجبه؛ فـ <code dir=\"ltr\">await</code> تعلّق التنفيذ أثناء وصول البايتات. وفكّ ترميز بضع مئات من البايتات على الـ main thread لا بأس به. أما للاستجابات التي تبلغ عدة ميغابايتات، فانقل فكّ الترميز إلى دالة <code dir=\"ltr\">async</code> معلّمة بـ <code dir=\"ltr\">@concurrent</code> لتعمل في الخلفية، وأعلن النموذج بالصيغة <code dir=\"ltr\">nonisolated struct RatesResponse</code>؛ وإلا فسيكون توافقه مع <code dir=\"ltr\">Decodable</code> معزولًا على الـ main actor أيضًا، ولن يسمح الـ compiler للكود العامل في الخلفية باستخدامه.</p>\n</aside>\n<p>يشترط iOS أيضًا HTTPS افتراضيًا عبر App Transport Security، فيفشل الـ URL الذي يبدأ بـ <code dir=\"ltr\">http://</code> العادي ما لم تُضِف استثناءً. استخدم HTTPS ولن تحتاج إلى استثناء أبدًا.</p>\n<p>صار Pocket Budget قادرًا على التحدّث مع العالم الخارجي. لكنه ما زال ينسى مصاريفه حين تغلقه، لذلك ستحفظها بعد ذلك على الجهاز باستخدام SwiftData.</p>\n","toc":[{"id":"sec-1","text":"اعكس شكل الـ JSON باستخدام Codable","level":2},{"id":"sec-2","text":"اجلب، افحص، فكّ الترميز","level":2},{"id":"sec-3","text":"حالات تحميل يثق بها الـ view","level":2}],"takeaways":["<code dir=\"ltr\">try await URLSession.shared.data(from: url)</code> يعيد جسم الاستجابة والاستجابة نفسها؛ ويرمي خطأً عند فشل الشبكة، لكن ليس عند أخطاء HTTP مثل 404 أو 500.","افحص دائمًا رمز الحالة في <code dir=\"ltr\">HTTPURLResponse</code> قبل فكّ الترميز، وحوّل الحالة السيئة إلى خطأ خاص بك.","اعكس شكل الـ JSON في structs من نوع <code dir=\"ltr\">Decodable</code>؛ و<code dir=\"ltr\">keyDecodingStrategy</code> و<code dir=\"ltr\">dateDecodingStrategy</code> يتعاملان مع المفاتيح بصيغة snake_case والتواريخ بصيغة ISO 8601.","صِف دورة حياة الشاشة كـ enum اسمه <code dir=\"ltr\">LoadState</code>، فلا يمكن أبدًا عرض التحميل والبيانات والأخطاء في الوقت نفسه."],"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>يجيب الخادم على طلب بالحالة HTTP 500 وصفحة خطأ بصيغة HTML. ماذا يفعل <code dir=\"ltr\">try await URLSession.shared.data(from: url)</code>؟</p>","options":[{"text":"يرمي <code dir=\"ltr\">URLError</code> يحمل رمز الحالة.","why":"لا يرمي URLSession خطأً إلا حين يتعذّر إكمال الطلب. والحالة 500 استجابة مكتملة."},{"text":"يعيد الـ HTML على أنه <code dir=\"ltr\">data</code> واستجابة قيمة <code dir=\"ltr\">statusCode</code> فيها 500.","why":"صحيح. لهذا تفحص الحالة بنفسك قبل فكّ الترميز."},{"text":"يعيد المحاولة تلقائيًا حتى يحصل على 200.","why":"لا يعيد URLSession المحاولة عند أخطاء HTTP؛ سياسة إعادة المحاولة متروكة لك."}],"answer":1},{"q":"<p>في الـ JSON مفتاح اسمه <code dir=\"ltr\">updated_at</code>، وفي الـ struct الخاص بك <code dir=\"ltr\">let updatedAt: Date</code>. يفشل فكّ الترميز مع <code dir=\"ltr\">keyNotFound</code>. ما أصغر إصلاح؟</p>","options":[{"text":"أعد تسمية الخاصية إلى <code dir=\"ltr\">updated_at</code>.","why":"يعمل، لكنه يجرّ أسلوب التسمية الخاص بالخادم إلى كود Swift الخاص بك."},{"text":"اجعل الخاصية optional: <code dir=\"ltr\">let updatedAt: Date?</code>.","why":"يختفي الخطأ، لكن التاريخ يصبح <code dir=\"ltr\">nil</code> دائمًا لأن المفتاح ما زال لا يتطابق."},{"text":"اضبط <code dir=\"ltr\">decoder.keyDecodingStrategy = .convertFromSnakeCase</code>.","why":"صحيح. يربط الـ decoder المفتاح <code dir=\"ltr\">updated_at</code> بـ <code dir=\"ltr\">updatedAt</code> لكل المفاتيح، فتبقى أسماؤك في Swift على أسلوبها المعتاد."}],"answer":2},{"q":"<p>لماذا تستخدم شاشة الأسعار enum واحدًا اسمه <code dir=\"ltr\">LoadState</code> بدل الخصائص <code dir=\"ltr\">isLoading</code> و<code dir=\"ltr\">rates</code> و<code dir=\"ltr\">errorMessage</code>؟</p>","options":[{"text":"الخصائص المنفصلة تسمح بتركيبات مستحيلة، مثل التحميل والفشل معًا؛ أما الـ enum فيسمح بحالة واحدة بالضبط.","why":"صحيح. يكتب الـ view جملة switch على الـ enum ويعالج كل حالة مرة واحدة بالضبط."},{"text":"لا يستطيع SwiftUI مراقبة أكثر من خاصية واحدة في كل class.","why":"يتتبّع Observation كل خاصية يقرؤها الـ body؛ ليس هذا هو السبب."},{"text":"الـ enums تجعل طلبات الشبكة أسرع.","why":"الـ enum يغيّر طريقة وصف الـ state، لا سرعة تنفيذ الطلب."}],"answer":0}],"exercise":{"kind":"order","items":["start-task","set-loading","build-url","fetch","check-status","decode","set-loaded"],"title":"من ظهور الشاشة إلى الأسعار على الشاشة","prompt":"<p>رتّب خطوات شاشة أسعار الصرف في Pocket Budget بالترتيب الذي تحدث به وقت التشغيل، من لحظة ظهور الشاشة إلى لحظة ظهور الأسعار.</p>\n","hints":["لا شيء يحدث حتى يبدأ SwiftUI العمل غير المتزامن، ويجب أن يرى المستخدم مؤشّر تحميل قبل أن يبدأ أي استدعاء للشبكة.","لا يمكنك الجلب قبل أن يكون لديك URL، ولا يجب أن تفكّ الترميز قبل أن تعرف أن الاستجابة ناجحة.","الخطوة الأخيرة هي التي تجعل SwiftUI يعيد الرسم."],"explanation":"<p>تبدأ <code dir=\"ltr\">.task</code> الدالة <code dir=\"ltr\">model.load()</code> حين يظهر الـ view. يضبط النموذج <code dir=\"ltr\">.loading</code> أولًا فيظهر مؤشّر التحميل فورًا. يبني العميل الـ URL مع الـ query items، ثم يعلّق <code dir=\"ltr\">URLSession.shared.data(from:)</code> التنفيذ حتى تصل الاستجابة. يأتي فحص الحالة قبل فكّ الترميز، لأن URLSession لا يرمي خطأً عند 404 أو 500، وصفحة الخطأ ستفشل في فكّ الترميز برسالة مضلّلة. ينتج فكّ الترميز <code dir=\"ltr\">RatesResponse</code>، وإسناد <code dir=\"ltr\">.loaded(response)</code> على الـ main actor هو ما يجعل SwiftUI يعيد رسم القائمة.</p>\n<p>إن رمت أي خطوة بعد <code dir=\"ltr\">set-loading</code> خطأً، تضبط <code dir=\"ltr\">catch</code> الوحيدة في النموذج الحالة <code dir=\"ltr\">.failed(message)</code> بدلًا من ذلك.</p>\n","checkLabels":{},"itemText":{"start-task":"ينفّذ SwiftUI ‏<code dir=\"ltr\">.task { await model.load() }</code> عند ظهور الـ view","set-loading":"يضبط النموذج <code dir=\"ltr\">state = .loading</code>، فيعرض الـ view ‏<code dir=\"ltr\">ProgressView</code>","build-url":"ابنِ الـ 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":"افحص <code dir=\"ltr\">(response as? HTTPURLResponse)?.statusCode == 200</code>، وإلا فارمِ <code dir=\"ltr\">RateError.badStatus</code>","decode":"<code dir=\"ltr\">try decoder.decode(RatesResponse.self, from: data)</code> مع استراتيجيتَي snake_case وISO 8601","set-loaded":"أسند <code dir=\"ltr\">state = .loaded(response)</code> على الـ main actor، فيعيد SwiftUI رسم القائمة"}},"words":635,"readMinutes":4}