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

لماذا يترك عرض التمرير القصير مساحة فارغة؟

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

بالنسبة إلى لوحة مكتبية صغيرة، حدّد قاعدة صريحة لتغيير الحجم: viewportHeight = min(contentHeight, maximumHeight). قِس المحتوى عند العرض الفعلي الذي يتلقاه، بما في ذلك أي حشوة ينبغي أن تتحرك معه أثناء التمرير. طبّق النتيجة على عرض التمرير الخارجي. تعيد min(_:_:) في Swift القيمة الأصغر، بينما تحدد frame(width:height:alignment:) في SwiftUI ارتفاعًا ثابتًا عند توفير وسيط الارتفاع. Swift min(_:_:)، الإطار الثابت في SwiftUI.

للتوضيح، افترض أن الحد الأقصى هو 240 نقطة. ينتج عن محتوى ارتفاعه 86 نقطة منفذ عرض بارتفاع 86 نقطة؛ أما المحتوى الذي يبلغ ارتفاعه 410 نقاط فينتج عن منفذ عرض بارتفاع 240 نقطة، مع إتاحة الوصول إلى بقية المحتوى بالتمرير. هذه مدخلات توضيحية وليست قياسات مأخوذة من المثال أدناه.

التغيير ما الذي يتحكم فيه هل يحدد الارتفاع الصغير المطلوب؟
ارتفاع المحتوى المقاس بعد تحديد سقفه وتطبيقه على عرض التمرير ارتفاع منفذ العرض نعم، في التخطيط المحدود الموضح أدناه
.scrollBounceBehavior(.basedOnSize) سلوك الارتداد بالنسبة إلى حجم المحتوى لا
.fixedSize(horizontal: false, vertical: true) على التسميات الحجم الرأسي المثالي للمحتوى يساعد في القياس؛ لكن منفذ العرض الخارجي لا يزال بحاجة إلى قاعدة تحدد ارتفاعه

توثّق Apple أن .basedOnSize إعداد لسلوك الارتداد، وأن fixedSize(horizontal:vertical:) يثبّت العرض على حجمه المثالي على امتداد الأبعاد المحددة. ولا يحدد أي منهما ارتفاعًا لمنفذ العرض مشتقًا من المحتوى. سلوك ارتداد التمرير، الحجم الثابت حسب المحور.

إنشاء لوحة صغيرة بحد أقصى لارتفاع التمرير

استخدم إصدارًا حاليًا من Xcode يوفّر حزمة تطوير macOS التي تحتوي على التحميل الزائد ذي القيمة الواحدة لـ onGeometryChange(for:of:action:). توثّق Apple حاليًا أن هذا التحميل الزائد متاح بدءًا من macOS 13.0. يستهدف المثال الكامل أدناه macOS 13.3 أو أحدث، لأنه يستخدم أيضًا scrollBounceBehavior(_:axes:)، المتاح بدءًا من macOS 13.3. استدعاء تغيير هندسة العرض، توافر مُعدِّل الارتداد.

أنشئ مشروع تطبيق macOS باستخدام SwiftUI. استبدل تعريف التطبيق وعرض المحتوى اللذين أُنشئا تلقائيًا بهذا البرنامج ذي الملف الواحد، مع الإبقاء على تعريف واحد فقط لـ @main في الهدف. جميع التسميات نصوص محلية اصطناعية. يبدّل مفتاح Show more labels بين المحتوى القصير والطويل.

import SwiftUI

@main
struct CompactLabelsApp: App {
    var body: some Scene {
        WindowGroup {
            CompactLabelsView()
        }
    }
}

struct CompactLabelsView: View {
    @State private var showMore = false
    @State private var viewportHeight: CGFloat = 240

    private var labels: [String] {
        if showMore {
            return (1...18).map { number in
                "Sample label \(number): a longer description that can wrap when the panel becomes narrower."
            }
        }
        return ["Sample label A", "Sample label B"]
    }

    var body: some View {
        VStack(alignment: .leading, spacing: 12) {
            Toggle("Show more labels", isOn: $showMore)

            ScrollView(.vertical) {
                VStack(alignment: .leading, spacing: 8) {
                    if labels.isEmpty {
                        Text("No labels to display")
                    } else {
                        ForEach(labels.indices, id: \.self) { index in
                            Text(labels[index])
                        }
                    }
                }
                .font(.body)
                .fixedSize(horizontal: false, vertical: true)
                .frame(maxWidth: .infinity, alignment: .leading)
                .padding(12)
                .onGeometryChange(for: CGFloat.self) { proxy in
                    min(proxy.size.height.rounded(.up), 240)
                } action: { newHeight in
                    viewportHeight = newHeight
                }
            }
            .frame(height: viewportHeight)
            .scrollBounceBehavior(.basedOnSize, axes: .vertical)
            .border(Color.secondary.opacity(0.4))

            Text("The label panel grows up to 240 points.")
                .font(.caption)
        }
        .padding(20)
        .frame(
            maxWidth: .infinity,
            maxHeight: .infinity,
            alignment: .topLeading
        )
        .frame(minWidth: 300, minHeight: 360)
    }
}

تم فحص البرنامج، لكن لم يُترجم أو يُشغّل هنا لأن هذه البيئة لا تحتوي على مترجم Swift أو بيئة تشغيل macOS. أنشئه في Xcode ونفّذ الفحوصات أدناه قبل دمجه في واجهة قائمة.

لماذا ينبغي إجراء القياس داخل عرض التمرير

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

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

يؤدي التقريب إلى الأعلى إلى تجنب تحديد منفذ عرض أقصر عمدًا من المحتوى المقاس ولو بجزء من نقطة. توفّر حالة 240 نقطة الأولية منفذ عرض صالحًا للاستخدام قبل وصول القياس؛ وهي قيمة بدء وليست ارتفاعًا مقاسًا للمحتوى. ويستبدلها الاستدعاء لاحقًا.

لماذا يهم العرض وترتيب المُعدِّلات

أبقِ تغيير الحجم الأفقي مرنًا. يستخدم المثال .fixedSize(horizontal: false, vertical: true) كي تتمكن التسميات من استخدام العرض المتاح مع الاحتفاظ بحجمها الرأسي المثالي. تحذّر Apple من أن تثبيت بُعد مثالي قد يجعل العرض يتجاوز حدود العنصر الأب؛ لذلك فإن تثبيت البُعدين ليس خيارًا افتراضيًا مناسبًا للتسميات التي تلتف في لوحة ضيقة. توثيق Apple لـ fixedSize(horizontal:vertical:).

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

التحقق من المحتوى القصير والتفاف النص وتقلّص اللوحة

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

  1. شغّل التطبيق مع إيقاف Show more labels. ينبغي أن يحيط الإطار بالتسميتين وحشوتهما، وأن يظهر التعليق أسفلهما مباشرة.
  2. فعّل Show more labels. ينبغي أن يكبر حجم اللوحة حتى حدها الأقصى، وهو 240 نقطة. مرّر للتأكد من إمكانية الوصول إلى التسمية الأخيرة.
  3. ضيّق النافذة ووسّعها. ينبغي أن يتغير ارتفاع التسميات الملتفة دون فقدان أسطرها الأخيرة.
  4. أوقف المفتاح مجددًا. ينبغي أن يتقلص حجم اللوحة ليعود إلى ارتفاع المحتوى القصير.
  5. غيّر المصفوفة القصيرة مؤقتًا إلى []. ينبغي أن تحدد الرسالة الصريحة No labels to display ارتفاع المحتوى.
  6. كرر ذلك باستخدام أنماط النص وعناصر التحكم المحيطة المستخدمة في لوحتك الفعلية.

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

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

تشخيص الفجوات المتبقية أو التسميات المقصوصة

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

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

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

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

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

متى تنطبق قياسات AppKit وTextKit؟

بالنسبة إلى لوحة التسميات في SwiftUI، قِس المجموعة الفرعية المعروضة في SwiftUI. إذ إن قياس النص باستخدام AppKit سيضيف إعداد تخطيط آخر يجب أن يطابق التسميات المعروضة.

إذا كان تطبيقك يملك بالفعل NSScrollView، فحافظ على التمييز نفسه بين المستند ومنفذ العرض. تعرّف Apple documentView بأنه العرض الجاري تمريره، بينما contentSize هو حجم عرض المحتوى في عرض التمرير. ورغم اسمها، فإن contentSize لا تقيس المستند كاملًا. AppKit documentView، AppKit contentSize.

في تخطيط TextKit قائم يستخدم NSLayoutManager، تعرض usedRect(for:) المستطيل المحيط بالمحارف التي وُزّعت بالفعل داخل حاوية نصية؛ وتوضح Apple صراحةً أن الاستعلام عنها لا ينشئ محارف ولا ينفّذ التخطيط. وعندما يلزم تخطيط الحاوية بالكامل، فإن ensureLayout(for:) هي الطريقة الموثّقة لفرض هذا التخطيط أولًا. TextKit usedRect(for:)، TextKit ensureLayout(for:).

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

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

المصادر