{"id":"l-sw-3-2","courseId":"swift-essentials","lang":"ar","kind":"lesson","summary":"اجعل لكل قطعة بيانات مصدر حقيقة واحدًا، باستخدام @State للقيم التي يملكها الـ view، و@Binding لمشاركة صلاحية الكتابة، وclasses من نوع @Observable مع @Bindable والـ environment لبيانات التطبيق.","html":"<p>الـ view في SwiftUI هو struct ينشئه SwiftUI ويقرؤه ويتخلّص منه متى شاء. فأين تعيش معلومة «فعّل المستخدم فلتر المصاريف الكبيرة فقط»؟ ليس في خاصية عادية: فالـ struct الذي يحملها قد يختفي بعد لحظة، ومن داخل <code dir=\"ltr\">body</code> لا تستطيع حتى تغييرها. جواب SwiftUI مجموعة صغيرة من الـ property wrappers (أغلفة الخصائص)، كلٌّ منها يجيب عن سؤال واحد: <strong>من يملك هذه البيانات؟</strong></p>\n<h2 id=\"sec-1\" tabindex=\"-1\">@State: بيانات يملكها الـ view</h2>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"swift\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">ExpensesScreen.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\"> SwiftUI</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\"> ExpensesScreen</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\"> showLargeOnly </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> false</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\">        Toggle</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"Large expenses only\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">isOn</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: $showLargeOnly)</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\">@State</code> إطار SwiftUI أن يحتفظ بالقيمة في تخزين يديره هو، مرتبط بموقع هذا الـ view في الهرمية. يمكن إعادة إنشاء الـ struct مئة مرة وتبقى القيمة. وحين تتغيّر القيمة، يعيد SwiftUI قراءة <code dir=\"ltr\">body</code> ويحدّث الشاشة.</p>\n<p>العلامة <code dir=\"ltr\">$</code> في <code dir=\"ltr\">$showLargeOnly</code> تطلب <strong>binding</strong> (ربطًا): اتصالًا في الاتجاهين مع الـ state. الـ <code dir=\"ltr\">Toggle</code> لا يملك قيمة التشغيل والإيقاف؛ بل يستقبل binding ليقرأ القيمة الحالية ويكتب القيمة الجديدة عند النقر. وكل عنصر إدخال (<code dir=\"ltr\">TextField</code> و<code dir=\"ltr\">Toggle</code> و<code dir=\"ltr\">Picker</code> و<code dir=\"ltr\">DatePicker</code> و<code dir=\"ltr\">Slider</code>) يعمل بهذه الطريقة.</p>\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>نسيان @State</span></p>\n<p>اكتب <code dir=\"ltr\">var isExpanded = false</code> ثم <code dir=\"ltr\">isExpanded.toggle()</code> داخل زر، وستفشل الترجمة مع “cannot use mutating member on immutable value: ‘self’ is immutable”. الحل ليس <code dir=\"ltr\">mutating</code>؛ فالـ views لا تستطيع ذلك. أضف <code dir=\"ltr\">@State private</code> ليملك SwiftUI التخزين.</p>\n</aside>\n<h2 id=\"sec-2\" tabindex=\"-1\">@Binding: استعارة state يملكها غيرك</h2>\n<p>استخرج الـ toggle إلى view مستقل، وسيحتاج الابن إلى تغيير state يملكها الأب:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"swift\" data-index=\"1\"><div class=\"code-head\"><span class=\"code-title\">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\"> FilterBar</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\">    @Binding</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> var</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> showLargeOnly: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">Bool</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\">        Toggle</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"Large expenses only\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">isOn</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: $showLargeOnly)</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>\n<span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// In ExpensesScreen's body:</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">FilterBar</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">showLargeOnly</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: $showLargeOnly)</span></span></code></pre></div>\n<p>يبقى الأب <strong>مصدر الحقيقة الوحيد</strong> (single source of truth)؛ ويحصل الابن على binding إليه. لو أعلن الابن <code dir=\"ltr\">@State</code> بدلًا من ذلك، لحمل نسخة منفصلة تبدأ بقيمة الأب ثم تبتعد عنها. هذا هو الخطأ الكامن وراء معظم أسئلة «الـ toggle عندي لا يفعل شيئًا».</p>\n<figure class=\"diagram\"><svg class=\"diagram-svg\" focusable=\"false\" viewBox=\"0 0 680 240\" role=\"img\" aria-labelledby=\"l-sw-3-2-t1\"><defs><marker id=\"arrow-l-sw-3-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-3-2-t1\">يملك ExpensesScreen القيمة showLargeOnly عبر State. ويمرّر binding إلى FilterBar، الذي يكتب الـ Toggle فيه عبر الـ binding رجوعًا إلى الـ state. أما المخزن، وهو class من نوع Observable يملكه التطبيق، فيقرأ منه ExpensesScreen وSettingsScreen كلاهما.</title>\n  <rect class=\"d-box-primary\" x=\"30\" y=\"30\" width=\"260\" height=\"70\" rx=\"10\"/>\n  <text class=\"d-code\" x=\"160\" y=\"58\" text-anchor=\"middle\">ExpensesScreen</text>\n  <text class=\"d-label-muted\" x=\"160\" y=\"82\" text-anchor=\"middle\">@State showLargeOnly</text>\n  <rect class=\"d-box\" x=\"30\" y=\"150\" width=\"260\" height=\"70\" rx=\"10\"/>\n  <text class=\"d-code\" x=\"160\" y=\"178\" text-anchor=\"middle\">FilterBar</text>\n  <text class=\"d-label-muted\" x=\"160\" y=\"202\" text-anchor=\"middle\">@Binding showLargeOnly</text>\n  <path class=\"d-arrow\" d=\"M130 100 L130 148\" marker-end=\"url(#arrow-l-sw-3-2-1)\"/>\n  <text class=\"d-label\" x=\"100\" y=\"130\" text-anchor=\"end\">$binding</text>\n  <path class=\"d-arrow d-dashed\" d=\"M190 150 L190 102\" marker-end=\"url(#arrow-l-sw-3-2-1)\"/>\n  <text class=\"d-label\" x=\"200\" y=\"130\">يكتب رجوعًا</text>\n  <rect class=\"d-box-success\" x=\"400\" y=\"90\" width=\"250\" height=\"70\" rx=\"10\"/>\n  <text class=\"d-code\" x=\"525\" y=\"118\" text-anchor=\"middle\">@Observable BudgetStore</text>\n  <text class=\"d-label-muted\" x=\"525\" y=\"142\" text-anchor=\"middle\">يُملَك مرة واحدة عبر @State</text>\n  <path class=\"d-arrow\" d=\"M400 110 L292 70\" marker-end=\"url(#arrow-l-sw-3-2-1)\"/>\n  <text class=\"d-label-muted\" x=\"525\" y=\"200\" text-anchor=\"middle\">الـ views التي تقرأ خاصية</text>\n  <text class=\"d-label-muted\" x=\"525\" y=\"220\" text-anchor=\"middle\">تتحدّث حين تتغيّر</text>\n</svg><figcaption>مصدر حقيقة واحد، يُشارَك عبر الـ bindings</figcaption></figure>\n<h2 id=\"sec-3\" tabindex=\"-1\">@Observable: بيانات التطبيق المشتركة</h2>\n<p>مفتاح الفلتر ينتمي إلى شاشة واحدة. أما قائمة المصاريف فتنتمي إلى التطبيق كله: القائمة تعرضها، والملخّص يجمعها، ونموذج الإضافة يضيف إليها. هذه هوية مشتركة، وقد قال درس الـ structs إنها تستدعي class. علّمه بالماكرو <code dir=\"ltr\">@Observable</code> من إطار Observation:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"swift\" data-index=\"2\"><div class=\"code-head\"><span class=\"code-title\">BudgetStore.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\"> Observation</span></span>\n<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\">@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\"> BudgetStore</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    var</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> expenses: [Expense] </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> []</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    var</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> monthlyLimit: Decimal </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 1500</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\"> totalSpent: Decimal {</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">        expenses.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">reduce</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) { </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">$0</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> +</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> $1</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.amount }</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\"> remaining: Decimal {</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">        monthlyLimit </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">-</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> totalSpent</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\">    func</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> add</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">_</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> expense: Expense) {</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">        expenses.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">append</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(expense)</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>لا property wrappers على الخصائص، ولا publishers. يعيد الماكرو كتابة كل خاصية مخزّنة حتى يستطيع SwiftUI تسجيل أيّها يقرأ الـ <code dir=\"ltr\">body</code>. الـ view الذي يعرض <code dir=\"ltr\">store.remaining</code> يقرأ <code dir=\"ltr\">monthlyLimit</code> و<code dir=\"ltr\">expenses</code> من خلالها، فيتحدّث حين تتغيّر أيٌّ منهما؛ أما الـ view الذي يعرض الحدّ فقط فيتجاهل المصاريف الجديدة.</p>\n<p>لاحظ أن <code dir=\"ltr\">totalSpent</code> و<code dir=\"ltr\">remaining</code> محسوبتان لا مخزّنتان. من المغري أن تحتفظ بـ <code dir=\"ltr\">var total</code> وتحدّثها في <code dir=\"ltr\">add</code>، لكن عندئذٍ يجب على كل method مستقبلية تمسّ <code dir=\"ltr\">expenses</code> (الحذف، التعديل، الاستيراد) أن تتذكّر تحديثها أيضًا، وذات يوم ستنسى إحداها. خزّن الحدّ الأدنى من الحقائق واشتقّ كل ما عداها. يتتبّع Observation الخصائص المحسوبة عبر الخصائص المخزّنة التي تقرؤها، فتبقى الشاشة صحيحة دون أي جهد.</p>\n<p>أنشئ المخزن مرة واحدة، قرب قمّة التطبيق، وامتلكه عبر <code dir=\"ltr\">@State</code>:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"swift\" data-index=\"3\"><div class=\"code-head\"><span class=\"code-title\">PocketBudgetApp.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\">@main</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">struct</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> PocketBudgetApp</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">App </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\"> store </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> BudgetStore</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\"> Scene {</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">        WindowGroup</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">            ExpensesScreen</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">store</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: store)   </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// ExpensesScreen now has `let store: BudgetStore`</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">                .</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">environment</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(store)</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<p>تستطيع الـ views الأبناء أن تأخذ المخزن كخاصية عادية <code dir=\"ltr\">let store: BudgetStore</code> وتستدعي <code dir=\"ltr\">store.add(…)</code>؛ لا حاجة إلى أي wrapper للقراءة أو لاستدعاء الـ methods. وللشاشات العميقة في الهرمية، يجعله <code dir=\"ltr\">.environment(store)</code> متاحًا دون تمريره عبر كل مُهيِّئ، ويلتقطه الـ view عبر <code dir=\"ltr\">@Environment(BudgetStore.self) private var store</code>.</p>\n<h2 id=\"sec-4\" tabindex=\"-1\">@Bindable: bindings إلى داخل كائن مراقَب</h2>\n<p>حقل <code dir=\"ltr\">TextField</code> الذي يعدّل الحدّ الشهري يحتاج إلى binding إلى <code dir=\"ltr\">store.monthlyLimit</code>. و<code dir=\"ltr\">@Bindable</code> يوفّره:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"swift\" data-index=\"4\"><div class=\"code-head\"><span class=\"code-title\">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\"> SettingsScreen</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\">    @Environment</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(BudgetStore.</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">self</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </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\"> store</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:#CF222E;--shiki-dark:#FF7B72\">        @Bindable</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> var</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> store </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> store</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">        Form</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">            TextField</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"Monthly limit\"</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\">: $store.monthlyLimit, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">format</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: .</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">currency</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">code</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\">                .</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">keyboardType</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(.decimalPad)</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<p>حين يصل المخزن كخاصية، اكتب <code dir=\"ltr\">@Bindable var store: BudgetStore</code> بدلًا من ذلك؛ أما الصيغة المحلية المعروضة هنا فهي للكائنات القادمة من الـ environment.</p>\n<aside class=\"callout callout-note\" 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\"><circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"M12 16v-4\"/><path d=\"M12 8h.01\"/></svg><span><span class=\"callout-kind\">ملاحظة</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>الكود الأقدم: ObservableObject</span></p>\n<p>قبل iOS 17، كانت المهمة نفسها تستخدم <code dir=\"ltr\">ObservableObject</code> و<code dir=\"ltr\">@Published</code> و<code dir=\"ltr\">@StateObject</code> و<code dir=\"ltr\">@ObservedObject</code>. ستقابلها في الشروحات وقواعد الكود الأقدم. أما الكود الجديد فيجب أن يستخدم <code dir=\"ltr\">@Observable</code>: كود تمهيدي أقل، والـ views تتحدّث فقط للخصائص التي تقرؤها.</p>\n</aside>\n<p>لتقرّر أيّ wrapper تستخدم، اسأل من يملك البيانات. هل يملك هذا الـ view قيمة بسيطة؟ <code dir=\"ltr\">@State</code>. هل يحتاج إلى تغيير قيمة يملكها غيره؟ <code dir=\"ltr\">@Binding</code>. هل هي بيانات تطبيق مشتركة؟ class من نوع <code dir=\"ltr\">@Observable</code>، يُملَك مرة واحدة عبر <code dir=\"ltr\">@State</code> ويُمرَّر إلى الأسفل. الدرس التالي يشغّل المخزن في قائمة حقيقية مع تنقّل ونموذج لإضافة مصروف.</p>\n","toc":[{"id":"sec-1","text":"@State: بيانات يملكها الـ view","level":2},{"id":"sec-2","text":"@Binding: استعارة state يملكها غيرك","level":2},{"id":"sec-3","text":"@Observable: بيانات التطبيق المشتركة","level":2},{"id":"sec-4","text":"@Bindable: bindings إلى داخل كائن مراقَب","level":2}],"takeaways":["<code dir=\"ltr\">@State</code> يخزّن القيمة خارج الـ struct الخاص بالـ view فتبقى بعد كل re-render؛ علّمها بـ <code dir=\"ltr\">private</code>، لأن الـ view هو من يملكها.","البادئة <code dir=\"ltr\">$</code> تعطيك <code dir=\"ltr\">Binding</code>: صلاحية قراءة وكتابة على مصدر حقيقة يملكه غيرك، وهذا ما تحتاجه عناصر التحكّم مثل <code dir=\"ltr\">Toggle</code> و<code dir=\"ltr\">TextField</code>.","علّم الـ class الخاص بالنموذج المشترك بـ <code dir=\"ltr\">@Observable</code>، فيعيد SwiftUI رسم الـ views التي قرأ الـ <code dir=\"ltr\">body</code> الخاص بها خاصية تغيّرت فقط.","امتلك كائن <code dir=\"ltr\">@Observable</code> عبر <code dir=\"ltr\">@State</code> في الأعلى، ثم مرّره إلى الأسفل مباشرةً أو عبر <code dir=\"ltr\">.environment(_:)</code>.","استخدم <code dir=\"ltr\">@Bindable</code> حين تحتاج bindings إلى خصائص كائن <code dir=\"ltr\">@Observable</code>، مثلًا لتعديلها في نموذج."],"further":[{"title":"Managing model data in your app","url":"https://developer.apple.com/documentation/swiftui/managing-model-data-in-your-app"},{"title":"State","url":"https://developer.apple.com/documentation/swiftui/state"},{"title":"Observation","url":"https://developer.apple.com/documentation/observation"}],"quiz":[{"q":"<p>يعلن view عن <code dir=\"ltr\">var count = 0</code> وينفّذ زرٌّ <code dir=\"ltr\">count += 1</code>. ماذا يحدث؟</p>","options":[{"text":"يُترجَم الكود، لكن الشاشة لا تتحدّث أبدًا.","why":"لا يصل الأمر إلى هذا الحدّ. الـ <code dir=\"ltr\">body</code> لا يستطيع تعديل خاصية مخزّنة عادية، فتفشل الترجمة."},{"text":"لا يُترجَم، لأن <code dir=\"ltr\">self</code> غير قابل للتغيير داخل <code dir=\"ltr\">body</code>؛ الخاصية تحتاج إلى <code dir=\"ltr\">@State</code>.","why":"صحيح. <code dir=\"ltr\">@State</code> ينقل التخزين من الـ struct إلى SwiftUI، فيجعلها قابلة للكتابة ومراقَبة."},{"text":"يعمل، لأن الـ views في SwiftUI هي classes.","why":"الـ views هي structs، وهذا بالضبط سبب عدم قدرة الخصائص العادية على التغيّر من داخل <code dir=\"ltr\">body</code>."}],"answer":1},{"q":"<p>يملك الأب <code dir=\"ltr\">@State private var showLargeOnly = false</code>. ويجب على الابن <code dir=\"ltr\">FilterBar</code> أن يبدّل قيمتها. ماذا يعلن الابن، وماذا يمرّر الأب؟</p>","options":[{"text":"الابن: <code dir=\"ltr\">@Binding var showLargeOnly: Bool</code>. والأب يمرّر <code dir=\"ltr\">$showLargeOnly</code>.","why":"صحيح. الـ binding يعطي الابن صلاحية القراءة والكتابة، بينما يبقى الأب مصدر الحقيقة الوحيد."},{"text":"الابن: <code dir=\"ltr\">@State var showLargeOnly: Bool</code>. والأب يمرّر <code dir=\"ltr\">showLargeOnly</code>.","why":"<code dir=\"ltr\">@State</code> سينشئ نسخة ثانية مستقلّة؛ ولن يرى الأب التغيير أبدًا."},{"text":"الابن: <code dir=\"ltr\">var showLargeOnly: Bool</code>. والأب يمرّر <code dir=\"ltr\">$showLargeOnly</code>.","why":"الخاصية العادية لا تستطيع استقبال <code dir=\"ltr\">Binding</code>، وحتى لو تطابق النوع لما استطاعت الكتابة رجوعًا."}],"answer":0},{"q":"<p>أيّ الـ views يُعاد رسمها حين تتغيّر <code dir=\"ltr\">store.monthlyLimit</code> في مخزن من نوع <code dir=\"ltr\">@Observable</code>؟</p>","options":[{"text":"كل view في التطبيق.","why":"يتتبّع Observation الوصول لكل خاصية على حدة، فتُترك الـ views غير المعنية وشأنها."},{"text":"كل view يحمل مرجعًا إلى المخزن.","why":"حمل المرجع لا يكفي؛ لا يُتتبَّع الـ view إلا للخصائص التي قرأها الـ <code dir=\"ltr\">body</code> الخاص به فعلًا."},{"text":"فقط الـ views التي قرأ الـ <code dir=\"ltr\">body</code> الخاص بها <code dir=\"ltr\">monthlyLimit</code>، أو خاصية محسوبة تستخدمها.","why":"صحيح. هذا التتبّع الدقيق هو التحسين الأساسي مقارنةً بـ <code dir=\"ltr\">ObservableObject</code> الأقدم."}],"answer":2},{"q":"<p>تحصل شاشة الإعدادات على المخزن عبر <code dir=\"ltr\">@Environment(BudgetStore.self) private var store</code> وتحتاج إلى <code dir=\"ltr\">$store.monthlyLimit</code> لحقل <code dir=\"ltr\">TextField</code>. ماذا تضيف؟</p>","options":[{"text":"تغيّر الـ property wrapper إلى <code dir=\"ltr\">@State</code>.","why":"<code dir=\"ltr\">@State</code> سينشئ مخزنًا جديدًا تملكه هذه الشاشة بدل استخدام المخزن المشترك."},{"text":"‏<code dir=\"ltr\">@Bindable var store = store</code> في أعلى <code dir=\"ltr\">body</code>.","why":"صحيح. <code dir=\"ltr\">@Bindable</code> ينشئ bindings إلى خصائص كائن <code dir=\"ltr\">@Observable</code>، ويمكن إعلانه كمتغيّر محلي داخل <code dir=\"ltr\">body</code>."},{"text":"تعلّم <code dir=\"ltr\">monthlyLimit</code> بـ <code dir=\"ltr\">@Published</code>.","why":"<code dir=\"ltr\">@Published</code> ينتمي إلى نظام <code dir=\"ltr\">ObservableObject</code> الأقدم ولا ينشئ bindings بنفسه."}],"answer":1}],"exercise":{"kind":"spot-bug","language":"swift","code":"import SwiftUI\n\nstruct ExpenseCard: View {\n    let expense: Expense\n    var isExpanded = false\n\n    var body: some View {\n        VStack(alignment: .leading, spacing: 8) {\n            Text(expense.title)\n                .font(.headline)\n            if isExpanded {\n                Text(expense.date, format: .dateTime.weekday().day().month())\n                Text(expense.category.title)\n            }\n            Button(isExpanded ? \"Less\" : \"More\") {\n                isExpanded.toggle()\n            }\n        }\n    }\n}","bugLines":[5],"title":"البطاقة التي لا تتوسّع","prompt":"<p>يجب أن تعرض هذه البطاقة التاريخ والتصنيف حين ينقر المستخدم More. لكنها لا تُبنى: يُبلغ Xcode عن “cannot use mutating member on immutable value: ‘self’ is immutable” عند استدعاء <code dir=\"ltr\">toggle()</code>. اختر السطر الذي يجب تغييره لإصلاح المشكلة.</p>\n","hints":["يظهر الخطأ في سطر الـ toggle، لكن هذا السطر مجرّد عَرَض. أين أُعلن <code dir=\"ltr\">isExpanded</code>؟","الـ structs الخاصة بالـ views لا تستطيع تغيير خصائصها المخزّنة العادية من داخل <code dir=\"ltr\">body</code>.","أيّ property wrapper يخبر SwiftUI أن يملك قيمة ويخزّنها لهذا الـ view؟"],"explanation":"<p>السطر 5 يعلن <code dir=\"ltr\">isExpanded</code> كخاصية مخزّنة عادية في struct، فلا شيء داخل <code dir=\"ltr\">body</code> يستطيع تغييرها، ولن يعرف SwiftUI أن عليه إعادة الرسم حتى لو استطاع. أعلنها كـ state يملكها الـ view:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"swift\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">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\">@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\"> isExpanded </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> false</span></span></code></pre></div>\n<p>الآن يخزّن SwiftUI القيمة خارج الـ struct، ويُترجَم <code dir=\"ltr\">toggle()</code>، وكل نقرة تعيد رسم البطاقة. وتشير <code dir=\"ltr\">private</code> إلى أن هذا الـ view وحده يملك القيمة؛ أما الأب الذي يحتاج إلى التحكّم بها فسيمرّر <code dir=\"ltr\">@Binding</code> بدلًا من ذلك.</p>\n","checkLabels":{}},"words":606,"readMinutes":4}