تكرار تهيئة صفوف SwiftUI لا يثبت أن الصف اختفى أو فقد حالته. لتشخيص واجهة Mac قابلة للتمرير، تتبّع الإنشاء والحالة المحتفَظ بها واستدعاءات الظهور وظهور الصف ضمن منفذ العرض كلًّا على حدة.
هذا الدليل مخصص لمطوّر يبحث في سجلات تكرار الصفوف داخل تطبيق macOS الخاص به. تتمثل المهمة المحددة في معرفة ما إذا كان التحديث ينشئ قيمة عرض أخرى فحسب، أو يعيد ضبط حالة الصف، أو يغيّر الصفوف التي تشغل منفذ عرض التمرير. يستخدم المثال تسميات اصطناعية وأزرارًا عادية.
ما الذي تخبرك به كل إشارة من إشارات دورة الحياة؟
يستخدم صف SwiftUI عادةً بنية، وهي نوع قيمة في Swift. ويختلف إنشاء قيمة بنية أخرى عن إنشاء كائن مرجعي دائم. توضح وثائق Swift هذا الاختلاف في أنواع القيم والأنواع المرجعية في Swift.
تضيف SwiftUI نموذج الهوية الخاص بها. توضح Apple أن قيم العرض المؤقتة يمكن أن تمثل الهوية نفسها عبر التحديثات، وأن عمر العرض يتبع تلك الهوية. لذلك، لا يمكن لسجل المُهيّئ إثبات انتهاء عمر الصف السابق. راجع إزالة الغموض عن SwiftUI.
استخدم هذه الإشارات للإجابة عن أسئلة مختلفة:
| الإشارة | السؤال الذي تجيب عنه | حدود التفسير |
|---|---|---|
| مُهيّئ الصف | هل أُنشئت قيمة صف أخرى؟ | لا يثبت الظهور أو استبدال الحالة. |
تقييم body |
هل جرى تقييم وصف هذا العرض؟ | لا يحصي الإطارات المعروضة. |
| حالة الصف المحتفَظ بها | هل بقيت هذه القيمة التشخيصية بعد التحديث؟ | تصف استمرارية تلك الحالة، لا إشغال منفذ العرض. |
onAppear وonDisappear |
هل أبلغت SwiftUI عن انتقال في الظهور؟ | يعتمد التوقيت على نوع العرض. |
onScrollVisibilityChange |
هل تجاوز الصف عتبة منفذ العرض المختارة؟ | تقيس شرط ظهور التمرير الموثّق. |
توثّق Apple أن onAppear يكتمل قبل الإطار الأول المعروض، مع اعتماد التوقيت الدقيق على نوع العرض. ويُنفّذ onDisappear بعد الاختفاء، ويعتمد توقيته أيضًا على نوع العرض. لا توثّق أي من الطريقتين إلغاء تخصيص الحالة. أبقِ أحداث استدعاء هذه الدوال منفصلة عن الاستنتاجات المتعلقة بعمر التخزين. راجع onAppear(perform:) وonDisappear(perform:).
لطرح سؤال عن منفذ العرض، اختر عتبة صراحةً. في macOS 15 والإصدارات الأحدث، تُبلغ onScrollVisibilityChange(threshold:_:) عما إذا كان العرض يتجاوز عتبة الظهور المحددة؛ والقيمة الافتراضية هي 0.5. وتوثّق Apple أيضًا استدعاءً أوليًا عندما يظهر العرض وقد بلغ العتبة بالفعل. راجع onScrollVisibilityChange(threshold:_:).
في هذه المقالة، يعني «ظاهر ضمن منفذ العرض» استيفاء عتبة التمرير هذه. ولا يعني قياس ما إذا كان أحدهم قد نظر إلى الصف.
أنشئ تجربة صغيرة تفصل بين الإشارات
أنشئ تطبيق SwiftUI لنظام macOS باستخدام SDK يتضمن واجهات macOS 15، واضبط هدف النشر على macOS 15 أو أحدث. استبدل تصريحات التطبيق وعرض المحتوى التي أنشأها المشروع بالبرنامج الكامل أدناه. استخدم نقطة دخول واحدة فقط تحمل @main.
يستخدم المثال ScrollView وLazyVStack لتوضيح سلوك منفذ العرض. تصف Apple LazyVStack بأنها تنشئ العناصر عند الحاجة. ولا تفترض هذه التجربة أن سلوك التحميل أو الاحتفاظ في List مماثل. راجع LazyVStack.
import SwiftUI
import Foundation
@main
struct RowLifetimeLabApp: App {
var body: some Scene {
WindowGroup("Row Lifetime Lab") {
ProbeView()
}
}
}
struct ProbeView: View {
@State private var revision = 0
@State private var generation = 0
var body: some View {
VStack(spacing: 12) {
HStack {
Button("Update rows") {
revision += 1
}
Button("Reset identity") {
generation += 1
}
Text("Revision \(revision)")
}
ScrollView {
LazyVStack(spacing: 8) {
ForEach(1...60, id: \.self) { rowID in
ProbeRow(rowID: rowID, revision: revision)
}
}
.padding()
}
.id(generation)
}
.padding()
.frame(minWidth: 560, minHeight: 360)
}
}
struct ProbeRow: View {
let rowID: Int
let revision: Int
private let constructionID: UUID
@State private var appearanceToken: UUID? = nil
@State private var count = 0
init(rowID: Int, revision: Int) {
self.rowID = rowID
self.revision = revision
self.constructionID = UUID()
print("init row=\(rowID) value=\(constructionID)")
}
var body: some View {
let _ = print("body row=\(rowID) value=\(constructionID)")
HStack {
VStack(alignment: .leading) {
Text("Sample row \(rowID)")
Text("Revision \(revision)")
Text(appearanceToken?.uuidString ?? "Awaiting appearance")
.font(.caption.monospaced())
}
Spacer()
Button("Count \(count)") {
count += 1
}
}
.padding(12)
.frame(height: 80)
.background(Color.gray.opacity(0.12))
.onAppear {
if appearanceToken == nil {
appearanceToken = UUID()
}
print("appear row=\(rowID) token=\(tokenDescription)")
}
.onDisappear {
print("disappear row=\(rowID) token=\(tokenDescription)")
}
.onScrollVisibilityChange(threshold: 0.5) { visible in
print("viewport row=\(rowID) visible=\(visible)")
}
}
private var tokenDescription: String {
appearanceToken?.uuidString ?? "unassigned"
}
}
يخدم معرّفا UUID غرضين مختلفين. يضع constructionID تسمية لكل استدعاء للمُهيّئ. ويبدأ appearanceToken فارغًا، ثم يُعيّن عند أول ظهور مرصود لحالة الصف المحتفَظ بها. ولا يضع طابعًا زمنيًا لتخصيص الحالة. ويمنحك العدد قيمة مرئية أخرى يمكنك فحص استمراريتها.
توثّق Apple أن SwiftUI تدير تخزين State وتحدّث محتوى الواجهة المعتمد عندما تتغير قيمته. لذلك يحتفظ التشخيص برمزه وعدده في حالة خاصة، بينما تبقى معلومات الإنشاء خاصية مخزّنة عادية. راجع State.
فُحص البرنامج بمقارنته مع الواجهات الموثّقة، لكن لم يُترجم أو يُشغّل هنا لعدم توفر سلسلة أدوات Swift. وتشرح أمثلة السجل أدناه كيفية تفسير تشغيلك؛ وهي ليست نتائج مسجلة.
نفّذ ثلاثة فحوص مضبوطة وفسّر النتائج
1. حدّث مُدخلات الصفوف من دون تغيير الهوية
شغّل التطبيق في Xcode وأبقِ وحدة تصحيح الأخطاء مفتوحة. انقر زر Count في صف ظاهر مرتين، وسجّل رمزه، ثم انقر Update rows. تتغير المراجعة المُمررة إلى الصفوف، بينما تبقى المعرّفات الصحيحة الثابتة كما هي.
إذا ظهرت رسائل مُهيّئ مرة أخرى مع احتفاظ الصف المحدد برمزه وعدده، فهذا دليل على تكرار إنشاء القيم مع بقاء الحالة التشخيصية. لا تصنّف رسائل المُهيّئ هذه على أنها أعمار منفصلة للظهور على الشاشة. وتؤكد المراجعة المعروضة أن الصف تلقى المُدخل الجديد.
يُعيّن رمز الظهور عبر الحالة، وقد يؤدي ذلك بحد ذاته إلى تحديث آخر. وهذا سلوك تشخيصي مقصود. لا تتوقع عددًا محددًا من رسائل المُهيّئ أو body، وأزل هذه المجسات بعد اكتمال التحقيق.
2. مرّر صفًا إلى خارج منفذ العرض ثم أعده
مرّر ببطء حتى يتجاوز الصف المحدد عتبة 50%. قارن رسائل viewport برسائل الظهور، ثم عُد إلى الصف. افحص رمزه وعدده مرة أخرى.
يشير بقاء الرمز بعد العودة إلى استمرارية هذه الحالة التشخيصية طوال تلك الرحلة. أما اختلاف الرمز فيعني أن الرمز السابق لم يُحتفَظ به. ولتفسير إعادة الضبط في تطبيق فعلي، تحقّق من تغييرات الهوية والإزالة الشرطية وسلوك الاحتفاظ في الحاوية بدل استنتاج السبب من التمرير وحده.
توضح إرشادات Apple الحالية أن الحاويات المكدسة الكسولة قد تجهّز جسم العرض وتخطيطه قبل ظهوره. وإذا انعكس اتجاه التمرير، فقد لا يتلقى العرض الذي جرى تجهيزه مسبقًا onAppear مطلقًا. وتوضح Apple أيضًا أن حالة العناصر خارج الشاشة في الحاويات المكدسة الكسولة قد تُهمَل في نهاية المطاف، وتوصي بالاحتفاظ بالبيانات التي يجب أن تصمد أمام التمرير في نموذج أو عرض خارجي. راجع تعمّق في الحاويات المكدسة الكسولة والتمرير باستخدام SwiftUI.
لذلك يفيد العدد المحلي للصف في المثال بوصفه مجسًا. أما في واجهة تحرير فعلية، فخزّن التغييرات التي يجب أن تصمد أمام إزالة الصف في مالك ذي عمر أطول، مع ربطها بالمعرّف الثابت للعنصر.
3. أعد ضبط الهوية عمدًا
انقر Reset identity. يغيّر البرنامج الهوية المرتبطة بعرض التمرير بأكمله، ما يتيح مقارنة مقصودة بتحديث المراجعة العادي. توثّق Apple صراحةً أن تغيير القيمة المقدمة إلى id(_:) يعيد ضبط هوية العرض، بما يشمل حالته. راجع id(_:).
افحص الصفوف التي تظهر حديثًا بحثًا عن رموز جديدة وأعداد ابتدائية. ولأن إعادة الضبط تطبّق على شجرة العرض الفرعية لعرض التمرير، فلا تستخدمها اختبارًا مضبوطًا للحفاظ على موضع التمرير.
استخدم هذا الدليل عند مراجعة سجلك:
| الملاحظة | الاستنتاج المدعوم | الفحص التالي |
|---|---|---|
| رسائل مُهيّئ جديدة؛ والرمز والعدد مستمران | حدث إنشاء مع استمرار الحالة التشخيصية. | افحص تكلفة المُهيّئ إذا كانت التحديثات بطيئة. |
| تكرار استدعاءات الظهور؛ والرمز مستمر | لم يؤدِ تكرار الظهور إلى استبدال ذلك الرمز. | استخدم استدعاءات منفذ العرض للسلوك المعتمد على العتبة. |
| إعادة ضبط الرمز والعدد بعد تحديث عادي | لم تبقَ حالة الصف على النحو المتوقع. | افحص معرّفات البيانات ومعدّلات id المتغيرة والفروع الشرطية. |
| تهيئة صف من دون الإبلاغ عن ظهوره مطلقًا | لم يثبت الإنشاء وحده حدوث الظهور. | ضع في الحسبان التجهيز الكسول وتغيّرات التمرير اللاحقة. |
| يصبح ظهور الصف ضمن منفذ العرض غير محقق | لم تعد عتبة التمرير المختارة مستوفاة. | قرر ما إذا كانت هذه العتبة تلائم السلوك المقصود. |
في البيانات الديناميكية، أبقِ المعرّفات مرتبطة بالعناصر الأساسية خلال التحديثات. إنشاء معرّف جديد عند كل قراءة يمنع الاستمرارية. وافحص أيضًا هويات الأسلاف: فلا يمكن لمعرّفات الصفوف الثابتة الحفاظ على صف داخل شجرة فرعية تستبدل هويتها عمدًا. تناقش Apple معرّفات البيانات الثابتة والهوية البنيوية في إزالة الغموض عن SwiftUI.
ما المسار المدعوم في إصدارات macOS الأقدم؟
يتطلب معدّل ظهور التمرير في المثال macOS 15. ويتطلب خفض هدف النشر فرعًا للتحقق من التوفر مع تنفيذ منفصل؛ فـonAppear ليس قياسًا مكافئًا للعتبة.
إذا كان تطبيقك يملك بالفعل واجهة تمرير من AppKit، فإن NSScrollView.documentVisibleRect يوفّر الجزء من عرض المستند الظاهر عبر عرض المحتوى، معبرًا عنه بإحداثيات عرض المستند. ومن البدائل القائمة على الهندسة مقارنة هذا المستطيل بمستطيلات الصفوف الخاصة بك في نظام الإحداثيات نفسه، وإعادة الحساب بعد التمرير وتغييرات التخطيط. راجع documentVisibleRect.
على سبيل المثال، يمكن لقاعدة تختارها صراحةً تصنيف الصف على أنه ظاهر عندما يتقاطع نصف مساحته المستطيلة على الأقل مع المستطيل المرئي للمستند. هذه قاعدة تطبيق مقترحة، وليست ادعاءً بأنها تطابق تنفيذ SwiftUI. عالج المستطيلات الفارغة وتحويل الإحداثيات صراحةً.
توفر SwiftUI واجهة NSViewRepresentable لدمج عرض AppKit ينشئه تطبيقك ويديره. وتوفر طرائق الإنشاء والتحديث والتنسيق وإنهاء العمل الموثّقة جسرًا مدعومًا. احترم تحكم SwiftUI في تخطيط العرض المُمثّل؛ إذ تحذر Apple من تغيير إطار ذلك العرض أو حدوده مباشرةً. راجع NSViewRepresentable.
يتعلق هذا البديل بعرض مستند AppKit الذي تملكه. ولا يتطلب افتراضات بشأن العروض الخلفية الخاصة لقائمة SwiftUI من نوع List.
هل يثبت تخطيط TextKit أن النص ظاهر على الشاشة؟
يحتاج TextKit إلى الفصل نفسه بين التجهيز وهندسة منفذ العرض. ويهم ذلك إذا كان صفك يحتوي على عرض نص AppKit وأصبح السؤال هو أي مقاطع النص تتقاطع مع المنطقة المرئية.
يدير NSTextViewportLayoutController في TextKit 2، المتاح على macOS 12 والإصدارات الأحدث، منطقة تخطيط نشطة تتضمن عمومًا المنطقة المرئية بالإضافة إلى منطقة تمرير زائد. ويتضمن viewportBounds منطقة مرسومة خارج حدودها، ويصف viewportRange الاختياري تخطيط منفذ العرض الحالي. ولا ينبغي إعادة وصف أي منهما على أنه مجموعة دقيقة من المحارف المرئية. راجع NSTextViewportLayoutController.
في تنفيذ TextKit 2، استخدم هندسة تخطيط النص بدلًا من أحداث التخصيص. يستطيع NSTextLayoutManager تعداد مقاطع النص لنطاق نصي مُعطى. قارن مستطيلات المقاطع الناتجة بمستطيل القص الفعلي بعد وضعهما في نظام الإحداثيات نفسه. خذ textContainerOrigin الخاص بعرض النص في الحسبان؛ ولا تفترض أن إحداثيات حاوية النص تساوي إحداثيات عرض المستند. راجع enumerateTextSegments(in:type:options:using:) وtextContainerOrigin.
في تنفيذ قائم على TextKit 1، تقبل NSLayoutManager.glyphRange(forBoundingRect:in:) مستطيلًا بإحداثيات حاوية النص. وتذكر Apple أن النطاق المتصل المُعاد قد يتضمن محارف رسومية خارج ذلك المستطيل، لذا فهو نطاق مرشح لمزيد من الفحص وليس قائمة دقيقة بالعناصر المرئية. راجع glyphRange(forBoundingRect:in:).
حافظ على محرك النص أثناء التشخيص. توثّق Apple أن الوصول إلى خاصية layoutManager في NSTextView أو حاوية نص على macOS 12 والإصدارات الأحدث يؤدي إلى الانتقال إلى وضع التوافق باستخدام NSLayoutManager. ولاتباع مسار TextKit 2، استخدم textLayoutManager وواجهاته. راجع NSTextView.
اختر الإشارة التي تجيب عن سؤال الواجهة: سجلات المُهيّئ للإنشاء، والحالة المحتفَظ بها للاستمرارية، واستدعاءات الظهور لانتقالات الظهور، وهندسة منفذ العرض للرؤية. وتجعل تجربة باستخدام معرّف ثابت وإعادة ضبط واحدة متعمدة للهوية هذه الفروق قابلة للملاحظة من دون التخمين استنادًا إلى كثافة مخرجات وحدة التحكم.
المصادر
- أنواع القيم والأنواع المرجعية في Swift — Swift.org
- إزالة الغموض عن SwiftUI — Apple
onAppear(perform:)— AppleonDisappear(perform:)— AppleonScrollVisibilityChange(threshold:_:)— AppleLazyVStack— AppleState— Apple- تعمّق في الحاويات المكدسة الكسولة والتمرير باستخدام SwiftUI — Apple
id(_:)— AppledocumentVisibleRect— AppleNSViewRepresentable— AppleNSTextViewportLayoutController— AppleenumerateTextSegments(in:type:options:using:)— AppletextContainerOrigin— AppleglyphRange(forBoundingRect:in:)— AppleNSTextView— Apple