امتلك الحالة المشتركة القابلة للملاحظة في تطبيق SwiftUI App، ثم مرّر النسخة نفسها إلى كلا المشهدين. أبقِ مسودة كل نافذة داخل العرض الجذري الخاص بها. يتيح ذلك لعناصر التحكم في شريط القوائم تحديث كل النوافذ، مع احتفاظ كل نافذة بنصها غير المكتمل.

يبني هذا البرنامج التعليمي هذا الترتيب لمحرر تسميات صغير على Mac: تعرض نوافذ متعددة تسمية مشتركة واحدة، ولكل نافذة مسودة مستقلة، ويمكن للوحة في شريط القوائم تعديل التسمية المشتركة أو فتح نافذة أخرى. تنتمي جميع النصوص الواردة في الأمثلة إلى التطبيق وتستخدم تسميات اصطناعية.

حدّد التغييرات التي ينبغي أن تصل إلى كل نافذة

ابدأ بتحديد السلوك المتوقع لكل قيمة. ينبغي أن تتغير التسمية المشتركة في كل مكان عند تعديلها. أما المسودة غير المكتملة، فينبغي أن تتغير فقط في النافذة التي يكتب فيها المستخدم. تحدد هذه المتطلبات موضع كل قيمة.

توثّق Apple أن لكل نافذة ينشئها WindowGroup تخزينًا مستقلًا للحالة التي تُنشأ داخل تسلسل عروضها. وعلى النقيض، تعرض إرشادات Apple بشأن بيانات النموذج امتلاك مرجع قابل للملاحظة في المستوى الأعلى من App وتمريره إلى تسلسل العروض. ويجمع هذا المثال بين النمطين لوضع خطة الملكية هذه. WindowGroup، إدارة بيانات النموذج في تطبيقك.

القيمة المالك في هذا المثال السلوك المقصود
التسمية المشتركة App، باستخدام @State مع نموذج @Observable تعدّل كل نافذة ولوحة شريط القوائم القيمة نفسها
المسودة غير المكتملة العرض الجذري لكل نافذة، باستخدام @State تبقى الكتابة ضمن تلك النافذة
عناصر التحكم في شريط القوائم تتلقى النموذج الذي يملكه التطبيق تصل التغييرات إلى كل مستهلكي هذه النسخة
إجراء فتح نافذة يُقرأ من بيئة عرض شريط القوائم يفتح نافذة من المجموعة المحددة

التمييز هنا هو مالك النسخة. إن إعطاء كائنين أُنشئا كلٌّ على حدة اسم الفئة نفسه لا يجعل محتوياتهما مشتركة. وبالمثل، فإن وضع مرجع إلى كائن واحد يملكه التطبيق داخل عدة نوافذ لا ينشئ نسخًا مستقلة من ذلك الكائن. ولهذا ينشئ المثال نموذجه المشترك مرة واحدة على مستوى التطبيق.

اضبط الحد الأدنى لإصدار macOS قبل إضافة المشاهد

استخدم مشروع تطبيق SwiftUI لـ macOS مع تحديد macOS 14 أو إصدار أحدث كحد أدنى للنشر، وسلسلة أدوات Xcode تدعم Observation، مثل Xcode 15 أو إصدار أحدث. يبدأ تكامل SwiftUI مع Observation في macOS 14. تتوفر MenuBarExtra ونمط النافذة الخاص بها بدءًا من macOS 13، لذا يفرض Observation الحد الأدنى الأعلى في هذا التطبيق. إدارة بيانات النموذج في تطبيقك، MenuBarExtra، window.

تستخدم لوحة شريط القوائم .menuBarExtraStyle(.window) لأنها تحتوي على حقول نصية وتخطيط مخصص. تصف Apple هذا النمط بأنه نافذة تشبه النافذة المنبثقة، بعناصر تحكم ذات نمط مماثل لتلك الموجودة في نافذة عادية. وهي مشهد منفصل عن WindowGroup الخاص بالمحرر. window.

يعزل المثال أيضًا صراحةً نموذجه وأنواع واجهته ضمن @MainActor. وتعرّف Apple الممثل الرئيسي بأن منفّذه مكافئ لطابور الإرسال الرئيسي. هنا، تنشأ كل التعديلات من عناصر تحكم واجهة المستخدم، ولا حاجة إلى عمل في الخلفية. MainActor.

أنشئ النموذج المشترك ومسودات النوافذ المستقلة

استبدل نقطة دخول التطبيق التي أنشأها المشروع تلقائيًا بالمثال الكامل التالي في ملف واحد. أبقِ تصريحًا واحدًا فقط لـ @main في الهدف. يتضمن النموذج الاستيرادات والنموذج والعروض والنص الأولي. وقد روجع وفق واجهات البرمجة الموثقة، لكن تعذر تجميعه في هذه البيئة لعدم توفر سلسلة أدوات Swift.

import SwiftUI
import Observation

@MainActor
@Observable
final class LabelStore {
    var sharedLabel = "Sample Alpha"
}

@main
@MainActor
struct LabelScopeApp: App {
    @State private var store = LabelStore()

    var body: some Scene {
        WindowGroup("Label Editor", id: "label-editor") {
            EditorWindow()
                .environment(store)
        }

        MenuBarExtra("Label Controls", systemImage: "square.grid.2x2") {
            LabelMenuPanel()
                .environment(store)
        }
        .menuBarExtraStyle(.window)
    }
}

@MainActor
struct EditorWindow: View {
    @Environment(LabelStore.self) private var store
    @State private var draft = "Sample Draft"

    var body: some View {
        VStack(alignment: .leading, spacing: 16) {
            SharedLabelControls()

            Divider()

            TextField("This window's draft", text: $draft)
                .textFieldStyle(.roundedBorder)

            Button("Use Draft as Shared Label") {
                store.sharedLabel = draft
            }

            Text("Publishing the draft keeps its local text intact.")
                .font(.caption)
        }
        .padding(20)
        .frame(minWidth: 420, minHeight: 220)
    }
}

@MainActor
struct SharedLabelControls: View {
    @Environment(LabelStore.self) private var store

    var body: some View {
        @Bindable var editableStore = store

        VStack(alignment: .leading, spacing: 8) {
            Text("Shared label: \(store.sharedLabel)")

            TextField("Shared label", text: $editableStore.sharedLabel)
                .textFieldStyle(.roundedBorder)
        }
    }
}

@MainActor
struct LabelMenuPanel: View {
    @Environment(\.openWindow) private var openWindow

    var body: some View {
        VStack(alignment: .leading, spacing: 16) {
            SharedLabelControls()

            Button("Open Another Editor") {
                openWindow(id: "label-editor")
            }
        }
        .padding(16)
        .frame(width: 320)
    }
}

هناك حقنان صريحان للبيئة: أحدهما لمحتوى النافذة والآخر لمحتوى شريط القوائم. ويتلقى كلاهما store. لا تتوقع أن يوفّر مُعدِّل بيئة مُرفق بمحتوى نافذة اعتمادًا لمشهد شقيق. توثّق Apple مشاركة البيئة عبر تسلسل العروض؛ ويجعل الحقنان اعتماد كل تسلسل صريحًا. إدارة بيانات النموذج في تطبيقك.

استخدم @Bindable عندما يحتاج عنصر التحكم إلى ربط

يقرأ SharedLabelControls النموذج عبر @Environment(LabelStore.self). ويوفّر المتغير المحلي @Bindable داخل جسم العرض الربط المُسقَط الذي يتطلبه TextField. وهو يشير إلى الكائن القابل للملاحظة الموجود أصلًا ولا ينشئ مخزنًا آخر. توثّق Apple هذا النمط الدقيق للتحويل من البيئة إلى الربط. Bindable.

أما حقل المسودة فيستخدم $draft، لأن قيمته مملوكة أصلًا لـ @State في عرض النافذة. ينسخ الزر المسودة الحالية إلى التسمية المشتركة. وهذا إجراء صريح: لا تعيد التغييرات اللاحقة على التسمية المشتركة كتابة مسودة أي من النافذتين. هذه هي دلالات التحرير التي اختيرت لهذا المثال.

طابق معرّف مجموعة النوافذ بدقة

يحصل زر شريط القوائم على openWindow من بيئته الخاصة ويستدعيه باستخدام \"label-editor\"، مطابقًا تصريح المجموعة. توثّق Apple هذا الأسلوب المعتمد على المعرّف لإنشاء نافذة من مجموعة. الغرض من الزر هو فتح محرر آخر، لذا لا يَعِد هذا المثال بالعثور على نافذة موجودة بعينها وإعادة استخدامها. WindowGroup.

تحقّق من النطاق بتجربة نافذتين

التسلسل التالي اختبار للسلوك المتوقع في المثال. وهو يميّز هوية النموذج المشترك عن التخزين المستقل للمسودات من دون الحاجة إلى أدوات قياس إضافية.

  1. شغّل التطبيق. يعرض المحرر الأول التسمية المشتركة Sample Alpha والمسودة Sample Draft.
  2. افتح لوحة شريط القوائم واختر Open Another Editor.
  3. غيّر مسودة النافذة الأولى إلى Draft One. اترك مسودة النافذة الثانية كما هي.
  4. في لوحة شريط القوائم، غيّر التسمية المشتركة إلى Sample Beta. ينبغي أن تعرض نافذتا المحرر تلك التسمية المشتركة، مع الاحتفاظ بقيمتي المسودتين المختلفتين.
  5. في النافذة الأولى، اختر Use Draft as Shared Label. تصبح التسمية المشتركة Draft One في كلتا النافذتين ولوحة شريط القوائم. وتبقى مسودة النافذة الثانية Sample Draft.

يفحص هذا التسلسل ثلاثة مسارات متميزة: الكتابة المحلية، والتعديل من شريط القوائم، والنسخ الصريح من قيمة محلية إلى مشتركة. إذا غيّرت الكتابة المحلية مسودة النافذة الأخرى، فتحقّق مما إذا كانت المسودة قد نُقلت إلى مخزن يملكه التطبيق. وإذا لم تصل تعديلات شريط القوائم إلى النوافذ، فتحقّق مما إذا كان قد أُنشئ LabelStore() آخر في تسلسل عروض شريط القوائم.

العَرَض أول ما ينبغي التحقق منه
اختلاف التسمية بين النافذة وشريط القوائم تهيئات نموذج منفصلة بدلًا من نسخة مشتركة واحدة
تغيّر كل المسودات معًا وضع تخزين المسودات في نموذج مشترك
تعذّر على عرض قراءة نموذجه غياب .environment(store) من ذلك التسلسل
إعادة ضبط المحرر على نحو غير متوقع تغييرات في هوية العرض المالك أو استبدال نموذجه
تعذّر فتح محرر آخر عدم تطابق معرّف المشهد المُعلَن مع المعرّف المطلوب

لحالة غياب البيئة نتيجة موثّقة: قراءة كائن قابل للملاحظة غير اختياري حسب نوعه تفترض أن أحد الأسلاف قد زوّده؛ وتقول Apple إن غياب الكائن يتسبب في استثناء. وفّر الاعتماد أيضًا في المعاينات ونقاط الدخول البديلة. إدارة بيانات النموذج في تطبيقك.

يخزّن المثال كل شيء في الذاكرة ولا يتضمن تنفيذًا للحفظ أو الاستعادة. لذلك ينبغي اعتبار إغلاق نافذة إتلافًا لمسودتها غير المكتملة، وإنهاء التطبيق إتلافًا للتسمية المشتركة. إذا كان مطلوبًا بقاء المسودات بعد هذه الإجراءات، فحدّد متطلبات التخزين الدائم بشكل منفصل قبل توسيع هذا المثال.

أبقِ الأوامر المركّزة منفصلة عن عناصر تحكم شريط القوائم المشتركة

لا تحتاج التسمية المشتركة إلى تحديد النافذة النشطة: يتلقى كل مستهلك النموذج نفسه. أما الإجراء الذي يعدّل مسودة النافذة المركّز عليها حاليًا، فله متطلب مختلف لأنه يحتاج إلى تحديد نافذة بعينها.

بالنسبة إلى أوامر قوائم التطبيق القياسية، توثّق Apple نشر نموذج مملوك لنافذة باستخدام focusedSceneValue واستهلاكه عبر @FocusedValue في تطبيق Commands. ويعطّل مثالها الأمر عند عدم وجود نموذج مركّز عليه. وهذا مسار مدعوم للإجراءات التي يتبع هدفها المشهد النشط. إنشاء شريط القوائم وتخصيصه باستخدام SwiftUI.

لا تستنتج من مثال الأوامر هذا أن فتح MenuBarExtra يمنحه مرجعًا موثوقًا إلى المحرر الذي كان نشطًا سابقًا. تعمل هذه اللوحة عمدًا على مستوى حالة التطبيق كلها. إذا كان ينبغي للوحة استهداف محرر بعينه، فأضف تصميمًا صريحًا لاختيار الهدف، مثل قائمة بمعرّفات المحررات التي يملكها التطبيق، بدلًا من افتراض النافذة التي ينبغي أن تتلقى الإجراء.

أضف AppKit أو TextKit فقط عندما يتطلبهما المحرر

لا يحتاج محرر التسميات إلا إلى عناصر تحكم SwiftUI. إذا توسعت متطلباته لتشمل عرض نص مخصصًا من AppKit، فإن NSViewRepresentable هو الجسر المدعوم من Apple لإنشاء NSView وتحديثه وإنهاء استخدامه ضمن تسلسل SwiftUI. كما تصف Apple استخدام منسّق لتمرير رسائل المفوّض وإجراءات الهدف إلى SwiftUI. NSViewRepresentable.

حافظ على خطة الملكية عند إضافة هذا الجسر: أبقِ نسخة العرض وحالة التحرير لكل محرر مرتبطتين بنافذته، ومرّر المحتوى المشترك عبر نموذج التطبيق فقط عندما يكون المقصود مشاركته. ينبغي للمنسّق تمرير التعديلات عبر ربط أو إجراء في النموذج. وكإجراء احترازي في التنفيذ، قارن المحتوى الوارد قبل استبدال محتوى عرض النص، حتى لا تؤدي التحديثات المشتركة غير المرتبطة إلى إعادة ضبط تحرير جارٍ مرارًا.

لتنفيذ TextKit 2، تحدد Apple textLayoutManager كنقطة الوصول الحديثة لمحرك التخطيط. وتشرح جلسة TextKit أن الوصول إلى layoutManager القديمة على عرض نص يستخدم TextKit 2 قد يؤدي إلى تفعيل وضع التوافق، فيحوّل ذلك العرض إلى TextKit 1 من دون تبديل تلقائي للعودة. تجنب استخدام هذه الخاصية القديمة لمجرد فحص عرض نص. TextKit، الجديد في TextKit وعروض النص.

إذا كان دعم macOS 13 مطلوبًا، فأبقِ ترتيب الملكية نفسه واستخدم مسار الرصد الأقدم: ObservableObject مع @Published، و@StateObject للمالك، و.environmentObject مع خصائص @EnvironmentObject المستقبِلة. يشرح دليل الترحيل من Apple كيفية تقابل هذه الآليات مع نهج Observation الأحدث. ينبغي أن يحافظ تغيير أنظمة الرصد على حدود المشاركة المقصودة. الترحيل من بروتوكول Observable Object إلى ماكرو Observable.

اختر الملكية وفق السلوك الذي يتوقعه المستخدم من الواجهة: نسخة واحدة يملكها التطبيق للتسمية المشتركة، وقيمة تملكها كل نافذة لكل مسودة غير مكتملة، وإجراء صريح يربط بينهما. احتفظ بتسلسل النافذتين كاختبار ارتدادي كلما توسعت لوحة شريط القوائم أو المحرر.

المصادر