لمطابقة عناصر التحكم الجانبية لمعاينة SwiftUI ذات نسبة أبعاد، احسب حجم المعاينة الملائم من مساحة النافذة المتاحة، ثم امنح عناصر التحكم الارتفاع نفسه. يحافظ هذا المثال على macOS على تناسب معاينة اصطناعية ويجعل عناصر التحكم الزائدة قابلة للتمرير.

المطلوب هو محرر سطح مكتب قابل لتغيير الحجم، يضم معاينة على اليسار ولوحة عناصر تحكم بعرض ثابت على اليمين. المقصود بـ«المعاينة» هنا هو المحتوى داخل تطبيقك قيد التشغيل. الهدف هو محاذاة حافتي التخطيط العلوية والسفلية للوحتيْن عند تغيير حجم النافذة.

ما الارتفاع الذي ينبغي أن تتبعه عناصر التحكم؟

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

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

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

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

احسب المستطيل الملائم مرة واحدة

لنفترض أن W وH هما أبعاد المحتوى المتاحة بعد الهوامش الخارجية، وأن C هو عرض عناصر التحكم، وG هو الفجوة، وR هي نسبة عرض المعاينة إلى ارتفاعها. يكون الحساب كما يلي:

remainingWidth = max(0, W - C - G)
previewWidth = min(remainingWidth, H * R)
previewHeight = previewWidth / R
controlsHeight = previewHeight

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

على سبيل المثال، لنفترض أن مساحة المحتوى قبل الهوامش تبلغ 960 × 600 نقطة. بعد إضافة هوامش قدرها 16 نقطة عند كل حافة، ولوحة عناصر تحكم بعرض 220 نقطة، وفجوة قدرها 16 نقطة، ومعاينة بنسبة 16:9، تصبح المساحة المتاحة 928 × 568 نقطة. ويبلغ عرض المعاينة المتبقي 692 نقطة، ما ينتج عنه ارتفاع للمعاينة قدره 389.25 نقطة. لذلك تصبح مساحة عرض عناصر التحكم 220 × 389.25 نقطة.

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

استخدم CGFloat لأبعاد التخطيط ونسبة موجبة ومحدودة. في Swift، تجعل let ratio: CGFloat = 16.0 / 9.0 عملية الحساب العشري المقصودة صريحة. إذا أتاح تطبيقك لاحقًا نسبًا قابلة للضبط، فتحقق من صلاحيتها قبل القسمة.

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

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

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

import SwiftUI

@main
struct MatchedPreviewApp: App {
    var body: some Scene {
        WindowGroup {
            PreviewEditor()
                .frame(minWidth: 640, minHeight: 360)
        }
        .windowResizability(.contentMinSize)
    }
}

struct PreviewEditor: View {
    @State private var intensity: Double = 0.5
    @State private var showsLabel = true

    private let ratio: CGFloat = 16.0 / 9.0
    private let controlsWidth: CGFloat = 220
    private let gap: CGFloat = 16

    var body: some View {
        GeometryReader { geometry in
            let availableHeight = max(0, geometry.size.height)
            let remainingWidth = max(
                0,
                geometry.size.width - controlsWidth - gap
            )
            let previewWidth = min(
                remainingWidth,
                availableHeight * ratio
            )
            let previewHeight = previewWidth / ratio

            HStack(alignment: .top, spacing: gap) {
                RoundedRectangle(cornerRadius: 12)
                    .fill(Color.blue.opacity(0.2 + intensity * 0.7))
                    .aspectRatio(ratio, contentMode: .fit)
                    .overlay {
                        Text("Sample composition")
                            .font(.headline)
                            .opacity(showsLabel ? 1 : 0)
                    }
                    .frame(
                        width: previewWidth,
                        height: previewHeight
                    )

                ScrollView(.vertical) {
                    VStack(alignment: .leading, spacing: 16) {
                        Text("Preview controls")
                            .font(.headline)

                        Toggle("Show label", isOn: $showsLabel)

                        VStack(alignment: .leading, spacing: 8) {
                            Text("Color intensity")
                            Slider(value: $intensity, in: 0...1)
                        }

                        ForEach(1...8, id: \.self) { index in
                            Text("Sample setting \(index)")
                                .font(.caption)
                        }
                    }
                    .padding(16)
                    .frame(maxWidth: .infinity, alignment: .leading)
                }
                .frame(
                    width: controlsWidth,
                    height: previewHeight
                )
                .background(Color.gray.opacity(0.08))
            }
            .frame(
                width: geometry.size.width,
                height: geometry.size.height,
                alignment: .center
            )
        }
        .padding(16)
    }
}

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

ينشئ الإطاران الثابتان ارتفاعًا مشتركًا. توثّق Apple أن frame(width:height:alignment:) ينشئ إطارًا غير مرئي بالأبعاد المحددة. تأتي خلفية عناصر التحكم بعد هذا الإطار، فتغطي مساحة عرض عناصر التحكم، بما في ذلك أي مساحة أسفل قائمة قصيرة. وثائق Apple حول الإطار الثابت.

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

حافظ على الحالة وسلوك دورة الحياة أثناء تغيير الحجم

أبقِ القيم القابلة للتعديل من المستخدم منفصلة عن الهندسة المحسوبة. في المثال، intensity وshowsLabel هما حالتان، بينما عرض المعاينة وارتفاعها قيمتان محليتان مشتقتان من الحجم المتاح حاليًا.

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

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

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

تحقق من تغيير الحجم والتجاوز وترتيب المعدّلات

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

مساحة المحتوى حجم المعاينة مساحة عرض عناصر التحكم
960 × 600 نقطة 692 × 389.25 220 × 389.25
640 × 360 نقطة 372 × 209.25 220 × 209.25
1200 × 400 نقطة 654.22 × 368 220 × 368
700 × 900 نقطة 432 × 243 220 × 243

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

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

إذا ظلت المحاذاة مختلفة بعد تكييف المثال، فتحقق من الحالات المحددة التالية:

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

متى ينبغي استخدام تخطيط مخصص بدلًا من ذلك؟

يناسب هذا الأسلوب محررًا يتلقى منطقة نافذة محدودة ويستخدم نسبة أبعاد معروفة. سياسة تحديد الحجم فيه صريحة ولا تعتمد على قياس المحتوى الداخلي للمعاينة.

إذا كان على مكوّنك القابل لإعادة الاستخدام تحديد حجمه المفضل انطلاقًا من العناصر الفرعية، ففكّر في استخدام Layout مخصص. يفصل العقد الذي توثّقه Apple بين الإبلاغ عن الحجم المركب عبر sizeThatFits(proposal:subviews:cache:) وتحديد مواضع العناصر الفرعية عبر placeSubviews(in:proposal:subviews:cache:). وتوفر وكلاء العروض عمليات للقياس وتحديد المواضع. وثائق Apple حول التخطيط.

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

المصادر