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

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

ما الذي يجعل التصفية تتحرك على النحو الصحيح؟

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

امنح كل سجل معرّفًا مخزنًا لا يتغير عند التصفية. يمثّل بروتوكول Swift المسمى Identifiable هوية الكيان المستقرة؛ ويقبل ForEach عناصر قابلة للتعرّف أو مسار مفتاح هوية صريحًا. في هذا المثال، تحدد أعداد صحيحة ثابتة أربعة سجلات اصطناعية. Identifiable، ForEach

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

استخدم VStack عاديًا داخل ScrollView لهذا المثال الصغير. توصي Apple باستخدام مكدس عادي لعدد قليل من العروض الفرعية؛ إذ يعرضها كلها معًا، بينما ينشئ المكدس الكسول العروض عند الحاجة. وهذا يجعل فحص مثال ذي أربعة صفوف أسهل قبل إدخال العرض المؤجل. VStack، ScrollView

أنشئ مثال Mac كاملًا

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

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

import SwiftUI

@main
struct FilteredCollectionDemoApp: App {
    var body: some Scene {
        WindowGroup {
            FilteredCollectionView()
        }
    }
}

private struct DemoItem: Identifiable {
    let id: Int
    let title: String
    let isTile: Bool
}

private struct FilteredCollectionView: View {
    @Environment(\.accessibilityReduceMotion)
    private var reduceMotion

    @State private var query = ""
    @State private var tilesOnly = false

    private let items: [DemoItem] = [
        DemoItem(id: 1, title: "Tile A", isTile: true),
        DemoItem(id: 2, title: "Dot A", isTile: false),
        DemoItem(id: 3, title: "Tile B", isTile: true),
        DemoItem(id: 4, title: "Dot B", isTile: false)
    ]

    private var visibleItems: [DemoItem] {
        let needle = query.lowercased()
        return items.filter { item in
            let matchesText = needle.isEmpty
                || item.title.lowercased().contains(needle)
            let matchesCategory = !tilesOnly || item.isTile
            return matchesText && matchesCategory
        }
    }

    private var rowTransition: AnyTransition {
        .asymmetric(
            insertion: AnyTransition.offset(x: 0, y: 8)
                .combined(with: .opacity),
            removal: .opacity
        )
    }

    var body: some View {
        let displayedItems = visibleItems
        let displayedIDs = displayedItems.map(\.id)

        VStack(alignment: .leading, spacing: 12) {
            Text("Label Collection")
                .font(.title2)

            TextField("Filter labels", text: $query)
                .textFieldStyle(.roundedBorder)

            Toggle("Tiles only", isOn: $tilesOnly)

            Text("\(displayedItems.count) of \(items.count) labels")
                .font(.caption)

            ScrollView {
                VStack(alignment: .leading, spacing: 8) {
                    ForEach(displayedItems) { item in
                        HStack {
                            Text(item.title)
                            Spacer()
                            Text(item.isTile ? "Tile" : "Dot")
                                .foregroundStyle(.secondary)
                        }
                        .padding(12)
                        .frame(maxWidth: .infinity, alignment: .leading)
                        .background(
                            RoundedRectangle(cornerRadius: 8)
                                .fill(Color.blue.opacity(0.10))
                        )
                        .transition(rowTransition)
                    }

                    if displayedItems.isEmpty {
                        Text("No matching labels")
                            .foregroundStyle(.secondary)
                            .padding(12)
                            .transition(.opacity)
                    }
                }
                .padding(12)
                .animation(
                    reduceMotion
                        ? nil
                        : .easeInOut(duration: 0.22),
                    value: displayedIDs
                )
            }
            .frame(height: 300)
        }
        .padding(20)
        .frame(minWidth: 360)
    }
}

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

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

لماذا نراقب المعرّفات الظاهرة بدلًا من عدد النتائج؟

يراقب مُعدِّل الرسوم المتحركة displayedIDs، وهي مصفوفة مرتبة من المعرّفات المستقرة. يطبّق animation(_:value:) لدى Apple الرسوم المتحركة المحددة عند تغيّر القيمة المكافئة التي يراقبها. وتمثل هذه القيمة هنا عضوية المجموعة المعروضة وترتيبها. animation(_:value:)

إن مراقبة العدد وحده تفوّت حالة مفيدة: فتغيير الاستعلام من a إلى b يستبدل صفين بصفين مختلفين. يظل العدد اثنين، لكن المعرّفات تتغير من [1, 2] إلى [3, 4]. كما ترصد مراقبة المعرّفات السجلات التي يعاد ترتيبها إذا أُضيف الفرز لاحقًا. وعلى العكس، فإن تغيير الاستعلام الذي يُبقي المعرّفات المرتبة نفسها ظاهرة لا يشغّل هذا المُعدِّل.

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

يُطبّق انتقال على كل صف كامل بعد الحشو وتنسيق الخلفية. يجمع الإدراج بين إزاحة رأسية قصيرة وعتامة، بينما تستخدم الإزالة العتامة وحدها. توثّق Apple أن combined(with:) يطبّق كلا الانتقالين، وأن asymmetric(insertion:removal:) يختار انتقالين مختلفين للاتجاهين. AnyTransition، asymmetric(insertion:removal:)

المدة التوضيحية هي 0.22 ثانية. وهي خيار للضبط وليست قيمة مثلى موثقة. يوضح تحديد easeInOut(duration:) التوقيت المقصود؛ وتعرّف Apple معامل المدة فيه بالثواني. easeInOut(duration:)

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

تحقّق من السلوك باستخدام تسلسل تصفية متوقع

استخدم التسلسل التالي في التطبيق قيد التشغيل. اشتُقت مصفوفات المعرّفات المتوقعة من بيانات العينة ودالة التصفية؛ وهي ليست نتائج تنفيذ مُبلّغًا عنها.

الإجراء المعرّفات الظاهرة المتوقعة ما ينبغي فحصه
ابدأ بحقل فارغ ومفتاح التبديل متوقف [1, 2, 3, 4] تظهر العناوين الأربعة كلها.
أدخل a [1, 2] يغادر العنوانان اللذان ينتهيان بـ B.
استبدل a بـ b [3, 4] تتغير العضوية رغم بقاء العدد اثنين.
أفرغ الحقل، ثم فعّل «المربعات فقط» [1, 3] تغادر صفوف النقاط وتبقى صفوف المربعات.
أدخل z [] تغادر الصفوف المتبقية وتظهر عبارة «لا توجد عناوين مطابقة».
أفرغ الحقل وأوقف مفتاح التبديل [1, 2, 3, 4] تعود السجلات كلها بترتيبها الأصلي.

يقرأ المثال accessibilityReduceMotion ويستخدم nil بدلًا من الرسوم المتحركة عند تفعيل هذا التفضيل. وتقول Apple إن الواجهات ينبغي أن تتجنب الرسوم المتحركة الكبيرة عندما تكون هذه القيمة صحيحة؛ وتعطيل حركة هذه المجموعة هو خيار التنفيذ في المثال. يوثّق مُعدِّل الرسوم المتحركة أن الرسوم المتحركة من نوع nil لا تحرّك العرض. accessibilityReduceMotion، animation(_:value:)

تشخيص الانتقالات المفقودة أو غير المتوقعة

تحقق من هذه الطبقات بالترتيب لتساعدك كل ملاحظة على تضييق نطاق السبب:

  1. افحص المعرّفات المُصفّاة. تحقق من أن دالة التصفية تنتج السجلات المتوقعة قبل تعديل إعدادات الرسوم المتحركة.
  2. افحص ثبات الهوية وتفرّدها. يجب أن يحتفظ السجل المستبقى بمعرّفه، وأن تكون لكل سجل مختلف معرّفات مختلفة. أزل المعرّفات العشوائية المحسوبة والمعرّفات المعتمدة على الموضع من المجموعة المتغيرة.
  3. افحص مشغّل الرسوم المتحركة. تأكد من أن القيمة المراقبة تتغير عند التحديث قيد الاختبار. تساوي الأعداد لا يعني تساوي العضوية.
  4. افحص موضع الانتقال. أرفق الانتقال بالصف الكامل الذي ينشئه ForEach، لا بنصه الداخلي وحده.
  5. افحص نطاق الحركة. أبقِ المكدس الأب موجودًا عبر تغييرات المرشّح. تجنب جعل هويته تعتمد على الاستعلام.
  6. افحص التجاوزات. تحقق من إعداد تقليل الحركة ومن أي إعدادات محيطة للمعاملة أو الرسوم المتحركة قبل الاستنتاج أن الانتقال لا يعمل.

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

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

متى ينبغي استخدام المكدسات الكسولة أو AppKit أو TextKit؟

للمجموعات الأكبر، فكّر في LazyVStack، الذي ينشئ العناصر الفرعية عند الحاجة لعرضها فقط. واصل تصفية البيانات قبل تمريرها إلى ForEach؛ فهذا ينتج صفًا واحدًا لكل عنصر مُمرّر. تشرح وثائق Apple لـ ForEach أن الحاويات الكسولة تستفيد عندما ينتج كل عنصر عددًا ثابتًا من العروض. أعد التحقق من التمرير والانتقالات بعد تغيير الحاويات. LazyVStack، ForEach

يمكن لتطبيق AppKit قائم تضمين واجهة SwiftUI هذه باستخدام NSHostingView، الموثق بوصفه عرض AppKit يستضيف تسلسل عروض SwiftUI. ويمكن أن تظل الهوية والتصفية والانتقالات الخاصة بالمجموعة في العرض الجذري لـ SwiftUI. يدمج الاستضافة الواجهة في تسلسل عروض التطبيق نفسه. NSHostingView

يخدم TextKit مهمة مختلفة: إدارة تخزين النصوص وتخطيطها المخصص، بما في ذلك عبر NSTextView في AppKit. لا يتطلب شيء في مثال تصفية العناوين هذا محرك تخطيط نصوص مخصصًا. استخدم TextKit عندما تستدعي متطلبات تحرير نص الصف أو تخطيطه ذلك؛ وأبقِ عضوية المجموعة وانتقالات الصفوف في واجهة SwiftUI المحيطة. TextKit

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

المصادر