{"id":"l-sw-4-3","courseId":"swift-essentials","lang":"ar","kind":"lesson","summary":"حوّل Expense إلى نموذج في SwiftData، وجهّز الـ model container، واقرأ بـ @Query، وأضف واحذف عبر الـ model context، وعدّل الكائنات المحفوظة بـ @Bindable.","html":"<p>أضف خمسة مصاريف، وأغلق Pocket Budget، ثم افتحه مجدّدًا، وستجد القائمة فارغة. كل شيء كان يعيش في الذاكرة. يتوقّع المستخدمون أن تصمد بياناتهم أمام إعادة التشغيل والتحديثات وهاتف نفدت بطاريته أثناء الحفظ. <strong>SwiftData</strong> هو إطار الحفظ الدائم (persistence) من Apple المصمّم لـ Swift: تصف نموذجك في الكود، ويتولّى هو قاعدة البيانات والمخطّط وإبقاء SwiftUI متزامنًا.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">تحويل Expense إلى نموذج</h2>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"swift\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">Expense.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 style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> SwiftData</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">@Model</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\"> Expense</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\"> title: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">String</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    var</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> amount: Decimal</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    var</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> category: Category</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    var</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> date: Date</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    var</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> note: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">String</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> \"\"</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    init</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">title</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:#8250DF;--shiki-dark:#D2A8FF\">amount</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: Decimal, </span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">category</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: Category, </span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">date</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: Date </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> .now) {</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">        self</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.title </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> title</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">        self</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.amount </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> amount</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">        self</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.category </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> category</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">        self</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.date </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> date</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>تغيّر أمران عن الـ struct الذي تستخدمه منذ قسم التصميم. أولًا، <code dir=\"ltr\">@Model</code> يتطلّب <strong>class</strong>. فالمصروف المحفوظ له هوية واحدة: يجب أن ترى القائمة وشاشة التفاصيل ونموذج التعديل كلها السجلّ نفسه، وهذا هو سلوك المرجع الذي تعلّمت أن تلجأ إليه حين تكون البيانات مشتركة. ثانيًا، تكتب المُهيِّئ بنفسك، لأن الـ classes لا تحصل على memberwise initializer.</p>\n<p>يجعل <code dir=\"ltr\">@Model</code> الـ class يتوافق مع <code dir=\"ltr\">PersistentModel</code>، الذي يجلب معه <code dir=\"ltr\">Identifiable</code> و<code dir=\"ltr\">Hashable</code>، فيستمر <code dir=\"ltr\">ForEach</code> و<code dir=\"ltr\">NavigationLink(value:)</code> في العمل دون تغيير. ويحتاج الـ enum المسمّى <code dir=\"ltr\">Category</code> إلى التوافق مع <code dir=\"ltr\">Codable</code> حتى يُخزَّن؛ أضفه إلى تعريفه. ويمكنك حذف خاصية <code dir=\"ltr\">id</code>: يعطي SwiftData كل نموذج معرّفًا دائمًا.</p>\n<h2 id=\"sec-2\" tabindex=\"-1\">container واحد للتطبيق</h2>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"swift\" data-index=\"1\"><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\">    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>\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\">modelContainer</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">for</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: Expense.</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">self</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>الـ <strong>model container</strong> (حاوية النماذج) يملك ملف قاعدة البيانات والمخطّط. وإنشاؤه مرة واحدة على الـ scene يضع أيضًا <strong>model context</strong> (سياق النماذج) في الـ environment. الـ context هو مساحة عملك: يتتبّع النماذج المضافة والمعدّلة والمحذوفة ويكتبها على القرص. والـ context الرئيسي يحفظ تلقائيًا، فمعظم التطبيقات لا تستدعي <code dir=\"ltr\">save()</code> يدويًا أبدًا.</p>\n<p>أنشئ الـ container في مكان واحد بالضبط. الـ <code dir=\"ltr\">ModelContainer</code> المبني داخل <code dir=\"ltr\">body</code> أو مُهيِّئ أحد الـ views يُعاد إنشاؤه كلما أعاد SwiftUI بناء ذلك الـ view، وكل نسخة تفتح المخزن من جديد. ضعه على الـ scene كما هنا، وسيتشاركه كل view تحته.</p>\n<figure class=\"diagram\"><svg class=\"diagram-svg\" focusable=\"false\" viewBox=\"0 0 700 220\" role=\"img\" aria-labelledby=\"l-sw-4-3-t1\"><defs><marker id=\"arrow-l-sw-4-3-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-3-t1\">الـ ModelContainer يملك المخزن على القرص. والـ ModelContext الرئيسي التابع له موجود في الـ environment الخاص بـ SwiftUI. تقرأ الـ views عبر Query وتكتب عبر الـ context باستخدام insert وdelete؛ ويحفظ الـ context تلقائيًا في المخزن فتتحدّث نتائج Query.</title>\n  <rect class=\"d-box\" x=\"20\" y=\"70\" width=\"150\" height=\"80\" rx=\"10\"/>\n  <text class=\"d-label-strong\" x=\"95\" y=\"105\" text-anchor=\"middle\">المخزن على القرص</text>\n  <text class=\"d-label-muted\" x=\"95\" y=\"128\" text-anchor=\"middle\">ملف SQLite</text>\n  <rect class=\"d-box-primary\" x=\"215\" y=\"70\" width=\"170\" height=\"80\" rx=\"10\"/>\n  <text class=\"d-code\" x=\"300\" y=\"105\" text-anchor=\"middle\">ModelContext</text>\n  <text class=\"d-label-muted\" x=\"300\" y=\"128\" text-anchor=\"middle\">يحفظ تلقائيًا</text>\n  <path class=\"d-arrow\" d=\"M215 110 L172 110\" marker-end=\"url(#arrow-l-sw-4-3-1)\"/>\n  <rect class=\"d-box-success\" x=\"460\" y=\"20\" width=\"220\" height=\"70\" rx=\"10\"/>\n  <text class=\"d-code\" x=\"570\" y=\"50\" text-anchor=\"middle\">@Query expenses</text>\n  <text class=\"d-label-muted\" x=\"570\" y=\"72\" text-anchor=\"middle\">يقرأ ويبقى محدّثًا</text>\n  <rect class=\"d-box-accent\" x=\"460\" y=\"130\" width=\"220\" height=\"70\" rx=\"10\"/>\n  <text class=\"d-code\" x=\"570\" y=\"160\" text-anchor=\"middle\">context.insert / delete</text>\n  <text class=\"d-label-muted\" x=\"570\" y=\"182\" text-anchor=\"middle\">يكتب</text>\n  <path class=\"d-arrow\" d=\"M385 95 L458 60\" marker-end=\"url(#arrow-l-sw-4-3-1)\"/>\n  <path class=\"d-arrow\" d=\"M458 165 L387 130\" marker-end=\"url(#arrow-l-sw-4-3-1)\"/>\n  <text class=\"d-label-muted\" x=\"300\" y=\"200\" text-anchor=\"middle\">يُنشأ مرة واحدة عبر .modelContainer(for:)</text>\n</svg><figcaption>الـ container والـ context والـ queries</figcaption></figure>\n<h2 id=\"sec-3\" tabindex=\"-1\">القراءة بـ @Query، والكتابة عبر الـ context</h2>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"swift\" data-index=\"2\"><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\">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\">    @Query</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(sort</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> \\Expense.date, order</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> .reverse) </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\"> expenses: [Expense]</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    @Environment</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(\\.modelContext) </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\"> context</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\">        NavigationStack</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</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:#0550AE;--shiki-dark:#79C0FF\">                ForEach</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(expenses) { expense </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">in</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">                    NavigationLink</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\">: expense) {</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">                        Text</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(expense.title)</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\">onDelete</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { offsets </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">in</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">                    for</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> index </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">in</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> offsets {</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">                        context.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">delete</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(expenses[index])</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>\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\">\"Expenses\"</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\">navigationDestination</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">for</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: Expense.</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">self</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) { expense </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">in</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">                EditExpenseView</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">expense</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>\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\">@Query</code> كل <code dir=\"ltr\">Expense</code>، الأحدث أولًا، ويعيد التنفيذ كلما تغيّرت البيانات المخزّنة، فأي حذف أو إضافة في أي مكان في التطبيق يحدّث هذه القائمة من تلقاء نفسها. نتيجة الـ query للقراءة فقط: أنت تغيّر البيانات عبر الـ context. وفي نموذج الإضافة من قسم SwiftUI، صار زر Save يستدعي <code dir=\"ltr\">context.insert(Expense(title: title, amount: amount, category: category, date: date))</code> بدل <code dir=\"ltr\">store.add(...)</code>. لم تعد المصاريف تعيش في <code dir=\"ltr\">BudgetStore</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>تعديل نتيجة الـ query</span></p>\n<p><code dir=\"ltr\">expenses.remove(atOffsets: offsets)</code> لا يُترجَم، لأن <code dir=\"ltr\">@Query</code> يكشف مصفوفة للقراءة فقط. وحذف عناصر من نسخة لن يحذف شيئًا من قاعدة البيانات على أي حال. مُرّ دائمًا عبر <code dir=\"ltr\">context.delete</code>، وسيلحق الـ query بالتغيير.</p>\n</aside>\n<h2 id=\"sec-4\" tabindex=\"-1\">التعديل بـ @Bindable</h2>\n<p>النموذج class مراقَب، فيعطيك <code dir=\"ltr\">@Bindable</code> bindings مباشرة إلى داخله:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"swift\" data-index=\"3\"><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\"> EditExpenseView</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\">    @Bindable</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> var</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> expense: Expense</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\">        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\">\"Title\"</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\">: $expense.title)</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\">\"Amount\"</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\">: $expense.amount, </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:#0550AE;--shiki-dark:#79C0FF\">            DatePicker</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"Date\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">selection</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: $expense.date, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">displayedComponents</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: .date)</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\">(expense.title)</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>لا يوجد زر Save. الكتابة تغيّر النموذج، فيلاحظ الـ context ذلك ويحفظ تلقائيًا، وتتحدّث القائمة الموجودة خلف الشاشة أيضًا. وإن أردت سلوك Cancel بدلًا من ذلك، فانسخ القيم إلى مسوّدات <code dir=\"ltr\">@State</code> كما يفعل نموذج الإضافة، واكتبها رجوعًا عند Save.</p>\n<h2 id=\"sec-5\" tabindex=\"-1\">التصفية</h2>\n<p>مرّر predicate (شرط تصفية) إلى <code dir=\"ltr\">@Query</code> لجلب مجموعة جزئية. وحين يعتمد الفلتر على قيمة يستقبلها الـ view، ابنِ الـ query داخل <code dir=\"ltr\">init</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\"> RecentExpensesView</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\">    @Query</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\"> recent: [Expense]</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    init</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">since</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> start: Date) {</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">        _recent </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> Query</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">filter</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">#Predicate</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">&#x3C;</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">Expense</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">></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\">.date </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">>=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> start },</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">                        sort</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: \\.date, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">order</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: .reverse)</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\"> 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\">(recent) { </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">Text</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\">.title) }</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\">#Predicate</code> وقت الترجمة ويُترجم إلى query لقاعدة البيانات، فتحدث التصفية في التخزين لا في حلقة Swift. اقتصر في الـ predicates على الخصائص المخزّنة البسيطة مثل التواريخ والأرقام والنصوص. فالتصفية على خصائص من نوع enum داخل predicate لم تكن موثوقة تاريخيًا، لذلك صفِّ التصنيفات في Swift بعد الجلب، أو خزّن القيمة الخام للتصنيف كـ <code dir=\"ltr\">String</code> إن احتجت إلى الاستعلام بها.</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>الـ previews والاختبارات تحصل على مخزنها الخاص</span></p>\n<p><code dir=\"ltr\">.modelContainer(for: Expense.self, inMemory: true)</code> داخل <code dir=\"ltr\">#Preview</code> يعطيك قاعدة بيانات مؤقتة، فتستطيع إضافة مصاريف تجريبية دون تلويث البيانات الحقيقية. والاختبارات تفعل الشيء نفسه عبر <code dir=\"ltr\">ModelContainer(for: Expense.self, configurations: ModelConfiguration(isStoredInMemoryOnly: true))</code>.</p>\n</aside>\n<p>إضافة خاصية جديدة لاحقًا، مثل <code dir=\"ltr\">note</code> أعلاه، تعمل دون كود إضافي حين يكون لها قيمة افتراضية: ينقل SwiftData المخزن تلقائيًا إلى الشكل الجديد. أما إعادة التسمية فتحتاج إلى تلميح، <code dir=\"ltr\">@Attribute(originalName: &quot;oldName&quot;)</code>، حتى ينتقل العمود القديم إلى الاسم الجديد؛ وتغيير نوع خاصية يحتاج إلى مخطّط بإصدارات (versioned schema) وخطة ترحيل (migration plan)، ويمكنك تأجيل ذلك حتى يكون لديك مستخدمون حقيقيون ببيانات حقيقية.</p>\n<p>صار Pocket Budget الآن يصمد أمام إعادة التشغيل. والدرس الأخير يضمن أنه يستمر في العمل وأنت تغيّره، باستخدام Swift Testing، ويوصله إلى هواتف الآخرين عبر TestFlight.</p>\n","toc":[{"id":"sec-1","text":"تحويل Expense إلى نموذج","level":2},{"id":"sec-2","text":"container واحد للتطبيق","level":2},{"id":"sec-3","text":"القراءة بـ @Query، والكتابة عبر الـ context","level":2},{"id":"sec-4","text":"التعديل بـ @Bindable","level":2},{"id":"sec-5","text":"التصفية","level":2}],"takeaways":["<code dir=\"ltr\">@Model</code> يحوّل الـ class إلى بيانات محفوظة بشكل دائم؛ ويبني SwiftData مخطّط التخزين من خصائصه المخزّنة.","<code dir=\"ltr\">.modelContainer(for:)</code> على الـ scene الخاص بالتطبيق ينشئ قاعدة البيانات مرة واحدة ويضع model context في الـ environment.","<code dir=\"ltr\">@Query</code> يجلب النماذج ويرتّبها ويصفّيها للـ view، ويُبقي النتيجة محدّثة كلما تغيّرت البيانات.","أضف واحذف عبر <code dir=\"ltr\">modelContext</code>؛ أما التغييرات على خصائص النموذج فتُحفظ تلقائيًا، بما في ذلك التعديلات المربوطة عبر <code dir=\"ltr\">@Bindable</code>.","استخدم container في الذاكرة (<code dir=\"ltr\">inMemory: true</code> أو <code dir=\"ltr\">isStoredInMemoryOnly</code>) للـ previews والاختبارات حتى لا تمسّ البيانات الحقيقية أبدًا."],"further":[{"title":"SwiftData","url":"https://developer.apple.com/documentation/swiftdata"},{"title":"Preserving your app's model data across launches","url":"https://developer.apple.com/documentation/swiftdata/preserving-your-apps-model-data-across-launches"},{"title":"Query","url":"https://developer.apple.com/documentation/swiftdata/query"}],"quiz":[{"q":"<p>لماذا يتحوّل <code dir=\"ltr\">Expense</code> من struct إلى <code dir=\"ltr\">final class</code> حين يصبح نموذجًا في SwiftData؟</p>","options":[{"text":"الـ classes أسرع في الحفظ من الـ structs.","why":"السرعة ليست السبب. المسألة مسألة هوية."},{"text":"<code dir=\"ltr\">@Model</code> يتطلّب class، لأن السجلّ المحفوظ له هوية واحدة تراقبها كل شاشة وتعدّلها.","why":"صحيح. عدّل مصروفًا في view واحد وسيرى التغيير كل view يعرضه، وهذا هو سلوك المرجع."},{"text":"الـ structs لا يمكن أن تحتوي خصائص من النوعين <code dir=\"ltr\">Decimal</code> أو <code dir=\"ltr\">Date</code>.","why":"الـ structs تحمل هذين النوعين طوال الوقت؛ والدروس السابقة استخدمتهما."}],"answer":1},{"q":"<p>تستخدم قائمة <code dir=\"ltr\">@Query private var expenses: [Expense]</code>. كيف تحذف مصروفًا سحبه المستخدم جانبًا؟</p>","options":[{"text":"<code dir=\"ltr\">expenses.remove(at: index)</code>","why":"نتيجة الـ query للقراءة فقط؛ إنها تعكس المخزن ولا تتحكّم فيه."},{"text":"اضبط خصائص المصروف على قيم فارغة.","why":"هذا يحفظ مصروفًا فارغًا؛ ولا يحذف شيئًا."},{"text":"<code dir=\"ltr\">context.delete(expenses[index])</code>، باستخدام الـ model context من الـ environment.","why":"صحيح. التغييرات تمرّ عبر الـ context، والـ query يحدّث نفسه ليطابقها."}],"answer":2},{"q":"<p>تربط شاشة تعديل حقل <code dir=\"ltr\">TextField</code> بـ <code dir=\"ltr\">$expense.title</code> عبر <code dir=\"ltr\">@Bindable var expense: Expense</code>. متى يُحفظ التغيير؟</p>","options":[{"text":"تلقائيًا؛ يتتبّع الـ context التغيير ويحفظه تلقائيًا.","why":"صحيح. الـ context الرئيسي في SwiftData يحفظ تلقائيًا، فتعديل النموذج يكفي في معظم الشاشات."},{"text":"فقط بعد أن تستدعي <code dir=\"ltr\">context.insert(expense)</code> مرة أخرى.","why":"المصروف موجود في الـ context أصلًا؛ والإضافة مجدّدًا ليست طريقة عمل التحديثات."},{"text":"أبدًا، لأن الـ bindings تنشئ نسخة من النموذج.","why":"النموذج class، والـ binding يكتب مباشرة في الـ instance المشترك."}],"answer":0},{"q":"<p>يجب أن تعرض الـ preview الخاصة بقائمة المصاريف بيانات تجريبية دون أن تمسّ قاعدة البيانات الحقيقية. ماذا تستخدم؟</p>","options":[{"text":"الـ <code dir=\"ltr\">.modelContainer(for: Expense.self)</code> العادي الخاص بالتطبيق.","why":"هذا يشير إلى المخزن نفسه على القرص الذي يستخدمه التطبيق؛ فتقرأ الـ previews بيانات حقيقية وتكتب فيها."},{"text":"‏<code dir=\"ltr\">.modelContainer(for: Expense.self, inMemory: true)</code>، ثم تضيف مصاريف تجريبية.","why":"صحيح. البيانات تعيش في الذاكرة فقط وتختفي حين تنتهي الـ preview."},{"text":"مصفوفة عادية بدل <code dir=\"ltr\">@Query</code>.","why":"هذا يعاين view مختلفًا عن الذي ستطلقه، فيُفسد الغرض كله."}],"answer":1}],"exercise":{"kind":"fill","language":"swift","template":"@{{0}}\nfinal class Expense {\n    var title: String\n    var amount: Decimal\n    var date: Date\n\n    init(title: String, amount: Decimal, date: Date = .now) {\n        self.title = title\n        self.amount = amount\n        self.date = date\n    }\n}\n\nstruct ExpensesList: View {\n    @{{1}}(sort: \\Expense.date, order: .reverse) private var expenses: [Expense]\n    @Environment(\\.modelContext) private var context\n\n    var body: some View {\n        List {\n            ForEach(expenses) { expense in\n                Text(expense.title)\n            }\n            .onDelete { offsets in\n                for index in offsets {\n                    context.{{2}}(expenses[index])\n                }\n            }\n        }\n        .toolbar {\n            Button(\"Add coffee\") {\n                context.{{3}}(Expense(title: \"Coffee\", amount: 4.5))\n            }\n        }\n    }\n}","blanks":[{"answers":["Model"]},{"answers":["Query"]},{"answers":["delete"]},{"answers":["insert"]}],"title":"احفظ المصاريف بـ SwiftData","prompt":"<p>اجعل <code dir=\"ltr\">Expense</code> نموذجًا في SwiftData وأكمل القائمة: يجب أن تجلب المصاريف الأحدث أولًا، وتحذف ما يسحبه المستخدم جانبًا، وتضيف مصروف قهوة عند النقر على الزر. الـ scene الخاص بالتطبيق فيه <code dir=\"ltr\">.modelContainer(for: Expense.self)</code> أصلًا.</p>\n","hints":["الماكرو الذي يجعل الـ class محفوظًا بشكل دائم مسمّى باسم ما يصفه.","الـ property wrapper الذي يجلب النماذج للـ view هو أيضًا اسم عملية قراءة من قاعدة البيانات.","كل عمليات الكتابة تمرّ عبر الـ model context: method تضيف نموذجًا جديدًا، وأخرى تحذف نموذجًا موجودًا."],"explanation":"<p>يحوّل <code dir=\"ltr\">@Model</code> الـ class إلى نموذج SwiftData محفوظ بشكل دائم. ويجلب <code dir=\"ltr\">@Query(sort: \\Expense.date, order: .reverse)</code> كل المصاريف الأحدث أولًا ويتحدّث حين تتغيّر البيانات. وعمليات الكتابة تمرّ عبر الـ context القادم من الـ environment: <code dir=\"ltr\">context.delete(_:)</code> يحذف نموذجًا و<code dir=\"ltr\">context.insert(_:)</code> يضيف واحدًا. الـ context الرئيسي يحفظ تلقائيًا، والـ query يحدّث القائمة من تلقاء نفسه، فلا توجد مصفوفة عليك إبقاؤها متزامنة يدويًا.</p>\n","checkLabels":{}},"words":645,"readMinutes":4}