لإبقاء نافذة SwiftUI المنبثقة مفتوحة أثناء التحديثات، حافظ على هوية مضيف العرض التقديمي وأبقِ رابط العرض التقديمي نشطًا. أرفق النافذة بحاوية مستقرة كي يتسنى تحديث محتوى العناصر الفرعية المتغيّر من دون استبدال العنصر المقدِّم.
هذا الدليل موجّه إلى مطوّر ينشئ تطبيقًا لنظام Mac يتضمن نافذة تحرير منبثقة يُفترض أن تظل ظاهرة بينما تتغير الواجهة الموجودة خلفها. يستخدم المثال التطبيقي نصًا محليًا اصطناعيًا، ويفصل بين ثلاثة أمور يمكنك فحص كل منها على حدة: حالة العرض التقديمي، وعمر العرض المقدِّم، ومحتوى المحرر.
لماذا قد يؤدي تحديث العرض إلى اختفاء نافذة منبثقة؟
قد تنشئ SwiftUI قيم عرض جديدة عند تقييم body، مع الحفاظ على هوية العرض الظاهر. تستمر الحالة طوال عمر تلك الهوية. تؤدي إزالة العرض أو تغيير هويته إلى إنهاء ذلك العمر؛ كما أن التبديل بين الفروع الشرطية قد ينشئ هويات متميزة، حتى عندما يحتوي الفرعان على نوع العرض نفسه. تشرح Apple هذه العلاقات في إزالة الغموض عن SwiftUI.
بالنسبة إلى نافذة منبثقة يتحكم بها متغير منطقي، فإن أداة التحكم الموثقة في العرض التقديمي هي الرابط الممرر إلى sheet(isPresented:onDismiss:content:): فالقيمة true تطلب العرض. عند التحقق من سبب اختفاء نافذة منبثقة، افحص هذا الرابط وتأكد أيضًا من استمرار وجود العرض الذي يحمل المعدِّل. إن تخزين القيمة true في مكان آخر لا يثبت أن مضيف العرض التقديمي الأصلي ظل موجودًا. راجع توثيق النافذة المنبثقة ذات المتغير المنطقي من Apple.
ومن الأسباب المباشرة على نحو خاص تغيّر المعرّف الصريح. توثّق Apple أن تغيير القيمة المقدّمة إلى id(_:) يعيد ضبط هوية العرض، بما في ذلك حالته. افحص العرض المقدِّم والعروض الأسلاف بحثًا عن معرّفات مشتقة من عدّادات المراجعات أو النصوص القابلة للتحرير أو القيم المُنشأة حديثًا. أزل هذه المعرّفات عندما يكون سلوك إعادة الضبط غير مقصود. راجع id(_:).
والاستنتاج العملي من التصميم هو وضع معدِّل النافذة المنبثقة خارج الجزء المتغيّر من الواجهة. إذا أمكن اختفاء عرض تحميل أو صف مفلتر أو تخطيط تفاصيل بديل، فعادةً ما يكون أقرب عنصر سلف مناسب ودائم مالكًا أفضل للعرض التقديمي.
أنشئ مالكًا مستقرًا للنافذة المنبثقة باستخدام مثال كامل لنظام Mac
أنشئ تطبيق SwiftUI لنظام macOS في Xcode، واضبط هدف النشر على macOS 12 أو أحدث، واستبدل تعريفَي التطبيق وعرض المحتوى اللذين أنشأهما Xcode بالملف الواحد التالي. أبقِ على تعريف واحد فقط للتطبيق يحمل @main. يتيح الحد الأدنى macOS 12 استخدام إجراء البيئة dismiss في المثال؛ أما TextEditor نفسه فمتاح بدءًا من macOS 11. راجع توثيق الإغلاق من Apple وتوثيق TextEditor.
يملك العرض الجذري للنافذة متغير العرض التقديمي، والنص القابل للتحرير، وعداد المراجعات، ومتغير التخطيط. ويحمل VStack الخارجي معدِّل النافذة المنبثقة. أما المحتوى داخل هذه الحاوية وحده فيتغير بين الفروع. توصي Apple بتعريف الحالة على نحو خاص في أعلى عرض يحتاج إليها، وتمرير الروابط إلى العناصر الفرعية التي تحتاج إلى تغييرها. راجع State.
import SwiftUI
@main
struct StableSheetDemoApp: App {
var body: some Scene {
WindowGroup {
SheetOwnerView()
}
}
}
struct SheetOwnerView: View {
@State private var isEditorPresented = false
@State private var note = "Sample note Alpha"
@State private var revision = 0
@State private var compactLayout = false
var body: some View {
VStack(alignment: .leading, spacing: 16) {
Text("Local Notes")
.font(.title)
if compactLayout {
Text("Compact summary: revision \(revision)")
} else {
VStack(alignment: .leading, spacing: 8) {
Text("Expanded summary")
Text("Revision \(revision)")
Text(note)
}
}
Button("Edit Note") {
isEditorPresented = true
}
}
.padding(24)
.frame(minWidth: 440, minHeight: 260)
.sheet(isPresented: $isEditorPresented) {
NoteEditorSheet(
note: $note,
revision: $revision,
compactLayout: $compactLayout
)
}
}
}
struct NoteEditorSheet: View {
@Binding var note: String
@Binding var revision: Int
@Binding var compactLayout: Bool
@Environment(\.dismiss) private var dismiss
var body: some View {
VStack(alignment: .leading, spacing: 16) {
Text("Edit Sample Note")
.font(.headline)
TextEditor(text: $note)
.frame(minHeight: 160)
Text("Owner revision: \(revision)")
Button("Update Owner") {
revision += 1
compactLayout.toggle()
}
HStack {
Spacer()
Button("Done") {
dismiss()
}
}
}
.padding(24)
.frame(width: 460, height: 340)
}
}
هذا تطبيق توضيحي كامل، وقد جرت مراجعته مقابل واجهات API الموثقة. لم يُجمّع أو يُشغّل هنا، إذ لا تتوفر في هذه البيئة أدوات Swift أو بيئة تشغيل macOS.
كيفية تجربة التحديث من دون إغلاق النافذة المنبثقة
- شغّل التطبيق وانقر على
Edit Note. - استبدل النص النموذجي بـ
Sample note Beta. - انقر على
Update Ownerعدة مرات. تؤدي كل نقرة إلى زيادة حالة المالك والتبديل بين المحتويات الشرطية للمالك. - تأكد من بقاء المحرر ظاهرًا، وبقاء نصه
Sample note Beta، وتقدّم رقم المراجعة المعروض. - انقر على
Done. تأكد من أن النافذة تعرض النص المحرر في التخطيط الموسّع عندما يكون ذلك التخطيط نشطًا. - أعد فتح المحرر وتأكد من بقاء النص المحرر متاحًا.
يوجد زر التحديث داخل النافذة المنبثقة، لذا لا يعتمد الاختبار على النقر على النافذة الموجودة خلفها. توثّق AppKit النوافذ المنبثقة على أنها تحجب المستند: أثناء ظهور النافذة المنبثقة، يُحظر معظم الأحداث الموجّهة إلى نافذتها الأصلية. ويتيح تحديث المالك عبر رابط لهذا المثال تجربة تغيير الحالة مباشرةً. راجع beginSheet(_:completionHandler:).
حدّد معنى إغلاق المحرر
يحرر هذا المثال نص المالك مباشرةً. يؤدي Done إلى إغلاق المحرر؛ ولا ينفّذ عملية حفظ منفصلة. تظل السلسلة النصية محفوظة في الحالة المحلية للنافذة، ولا يوفّر المثال حفظًا للبيانات بين مرات تشغيل التطبيق. يتبع هذا السلوك تصميم الرابط المشترك الموضح في State.
إذا كانت مهمتك تتطلب مسودة قابلة للإلغاء، فاستخدم قيمة مسودة منفصلة وانسخها إلى المستند فقط عند إجراء تأكيد صريح. حدّد هذا السلوك قبل تنفيذ الإغلاق: فالبت في اعتماد المسودة أو تجاهلها يختلف عن تحديد العرض الذي يملك النافذة المنبثقة.
شخّص سبب الإغلاق بثلاثة فحوص مستقلة
اتبع التسلسل التالي في تطبيقك. فهو يحوّل قواعد الهوية العامة إلى تحقق مركّز، من دون افتراض أن كل نافذة منبثقة تختفي للسبب نفسه.
| الفحص | ما ينبغي فحصه | التصحيح العملي |
|---|---|---|
| قيمة العرض التقديمي | كل موضع يضبط فيه المتغير المنطقي على false أو يُمسح فيه عنصر العرض التقديمي الاختياري |
أبقِ عمليات الإغلاق ضمن إجراءات إغلاق مقصودة |
| هوية العنصر المقدِّم | الفروع الشرطية، والصفوف المُزالة، ومعرّفات العناصر الأسلاف المتغيرة | انقل حالة العرض التقديمي والمعدِّل معًا إلى عنصر سلف مناسب ومستقر |
| عمر المحتوى | الحالة المحلية للنافذة المنبثقة، ومعرّفات المحتوى المتغيرة، ومزامنة المحرر | حافظ على جلسة التحرير المقصودة وحدّث المحتوى الموجود عمدًا |
أولًا، ابحث في مشروعك عن عمليات إسناد إلى خاصية العرض التقديمي واستدعاءات dismiss(). فقد يكون رد نداء التحديث يغلق النافذة المنبثقة صراحةً. اقرأ dismiss داخل عرض النافذة المنبثقة نفسه، كما في المثال: توثّق Apple أن الإجراء ينطبق على البيئة التي جرى الحصول عليه منها. وقد تستهدف قراءة الإجراء في العرض الجذري للنافذة تلك النافذة بدلًا من ذلك. راجع dismiss.
ثانيًا، افحص التسلسل الهرمي فوق المعدِّل. إذا أُرفقت النافذة المنبثقة بعرض تفاصيل يزيله التحديث، فانقلها إلى أعلى نقطة تتجاوز ذلك الحد الفاصل للاستبدال. وانقل كذلك حالة العرض التقديمي المملوكة محليًا. ووفقًا لشرح Apple لعمر العرض، فإن إبقاء المتغير في عنصر فرعي تنتهي هويته يعرّض تخزينه للاستبدال.
ثالثًا، ميّز بين إغلاق العرض التقديمي وبقاء المحرر ظاهرًا مع فقدان مسودته. ابدأ بمثال TextEditor البسيط، ثم أعد إضافة المحتوى المخصص على مراحل. هذه طريقة تشخيصية تساعد على تحديد ما إذا كان العطل مرتبطًا بملكية العرض التقديمي أو بمنطق تحديث المحتوى لديك.
هل ينبغي التبديل إلى نافذة منبثقة تعتمد على عنصر؟
استخدم sheet(item:onDismiss:content:) عندما يكون كيان محدد مختار هو مصدر العرض التقديمي. توثّق Apple عنصرًا اختياريًا يطابق Identifiable، وتمرر إلى إغلاق المحتوى عنصرًا غير nil، وتشرح استبدال المحتوى عند تغيّر العنصر. التبديل بين نسختَي واجهة API لا يحافظ بحد ذاته على عنصر مقدِّم انتهى عمره. راجع النوافذ المنبثقة المعتمدة على عنصر.
اختر معرّفًا يمثّل الكيان طوال جلسة التحرير المقصودة. يعبّر بروتوكول Swift Identifiable عن الهوية المستقرة، وتترك وثائقه صراحةً للأنواع المشاركة تحديد المدة والنطاق اللازمين. ولا يمكن لمعرّف جديد في كل مرة تُقرأ فيها خاصية أن يمثّل كيانًا مستمرًا. راجع Identifiable.
في محرر بسيط مثل المثال التوضيحي، يسهّل متغير عرض تقديمي منطقي مع روابط نصية منفصلة فحص العلاقة المقصودة. أما في محرر يعتمد على عنصر مختار، فأبقِ هوية التحديد منفصلة عن الحقول التي تتغير كثيرًا. تعامل مع تحديد كيان آخر على أنه قرار عرض تقديمي مقصود، وحدّد أولًا ما ينبغي أن يحدث للمسودة الحالية.
هل سيُبقي تعطيل الإغلاق التفاعلي النافذة المنبثقة مفتوحة؟
يتحكم interactiveDismissDisabled(_:) في الإغلاق غير البرمجي. وتصرّح Apple بوضوح بأنه لا يمنع الإغلاق الناتج عن تغيير رابط العرض التقديمي أو استدعاء dismiss. لذلك لا يوفّر دوره الموثق علاجًا لاستبدال الهوية. راجع interactiveDismissDisabled(_:).
طبّق المعدِّل داخل التسلسل الهرمي للنافذة المنبثقة فقط عندما تتطلب واجهتك سلوك الإغلاق هذا، ووفّر مسارًا صريحًا للإكمال. في هذه المهمة، ابدأ بالحفاظ على المضيف وحالته. قد يؤدي إضافة قيد على الإغلاق قبل تحديد السبب إلى بقاء مشكلة الملكية الأصلية بلا حل.
ما الذي يتغير عندما تحتوي النافذة المنبثقة على محتوى AppKit أو TextKit؟
عند تضمين NSTextView باستخدام NSViewRepresentable، افصل الإنشاء عن التحديث. تعرّف Apple makeNSView(context:) بوصفه خطوة الإنشاء، وupdateNSView(_:context:) بوصفه الموضع الذي تُطبّق فيه معلومات SwiftUI اللاحقة. وينقل المنسّق تغييرات المفوّض أو إجراءات الهدف إلى SwiftUI. راجع NSViewRepresentable وmakeNSView(context:).
من القواعد المفيدة المستنبطة من دورة الحياة هذه تجنب إعادة إنشاء المحرر لمجرد تغيّر حالة مالك لا صلة لها به. قبل استبدال محتواه، حدّد ما إذا كان نص النموذج الوارد قد تغيّر فعلًا وكيفية التعامل مع التعديلات المحلية المتزامنة. فالنافذة المنبثقة المستقرة لا تحل وحدها مشكلة الكتابة المتضاربة إلى نصها.
يوفّر TextKit تخزين النص وتخطيطه وعرضه. ويجمع NSTextView في AppKit هذه الإمكانات ويتيح محركات التخطيط الحديثة والقديمة. تتعلق هذه الآليات بمحتوى المحرر؛ أما ملكية العرض التقديمي فتحتاج إلى عمر مستقر خاص بها. استخدم TextKit عندما تتطلبه مهمة التحرير، وابحث في سلوك تحديث النص بمعزل عن إغلاق النافذة المنبثقة. راجع نظرة عامة على TextKit من Apple.
متى يكون العرض التقديمي على مستوى النافذة باستخدام AppKit مناسبًا؟
إذا كان تطبيقك الحالي يدير دورة حياة نوافذه بالفعل في AppKit، فإن macOS 26 يقدّم NSWindow.beginSheet(content:completionHandler:) لعرض محتوى SwiftUI مباشرةً على تلك النافذة. توثّق Apple دعم إجراء بيئة الإغلاق للنافذة المنبثقة، وتغيير الحجم، وأشرطة الأدوات، والتحكم في الإغلاق التفاعلي. استخدم SDK لنظام macOS 26، وطبّق حد التوفر لنظام macOS 26 قبل اعتماد هذه الإمكانية. راجع beginSheet(content:completionHandler:).
يمكن لـ NSWindow.HostingSheetRepresentation المُعاد تغيير العرض الجذري المعروض أو دعم الإغلاق البرمجي من AppKit. وهو خيار موثق عندما تناسب الملكية على مستوى النافذة بنية التطبيق. لكنه غير ضروري لمثال حاوية SwiftUI المستقرة أعلاه. راجع NSWindow.HostingSheetRepresentation.
في إصدارات macOS الأقدم، تقبل beginSheet(_:completionHandler:) الحالية في AppKit نافذة NSWindow منبثقة وتوثّق وضع العرض في قائمة انتظار. لا تفترض توفر النسخة الأحدث التي تعرض محتوى SwiftUI على هذه الأنظمة. راجع beginSheet(_:completionHandler:).
أبقِ المالك مستقرًا، وحافظ على نشاط قيمة العرض التقديمي، وصن مصدر الحقيقة المقصود للمحرر. تحقّق من هذه الشروط باستخدام إجراء تحديث محلي صغير قبل إعادة المحتوى الأكثر تعقيدًا. يمنحك ذلك طريقة ملموسة لمواصلة التحرير أثناء تحديث الواجهة، ضمن حدود العمر وواجهات API التي توثقها Apple.
المصادر
- إزالة الغموض عن SwiftUI — Apple
- sheet(isPresented:onDismiss:content:) — Apple
- id(_:) — Apple
- State — Apple
- dismiss — Apple
- TextEditor — Apple
- sheet(item:onDismiss:content:) — Apple
- Identifiable — Apple
- interactiveDismissDisabled(_:) — Apple
- NSViewRepresentable — Apple
- makeNSView(context:) — Apple
- TextKit — Apple
- beginSheet(content:completionHandler:) — Apple
- NSWindow.HostingSheetRepresentation — Apple
- beginSheet(_:completionHandler:) — Apple