لجعل نص SwiftUI يلتف داخل خلية NSTableView، امنح عرضًا محددًا لعرض الاستضافة، واربط ارتفاعه بالخلية باستخدام Auto Layout، وفعّل ارتفاعات الصفوف التلقائية. يشرح هذا الدليل إنشاء قائمة ملصقات صغيرة لنظام Mac بهذا الترتيب.
المهمة محددة: عرض ملصقات اصطناعية متفاوتة الأطوال في تطبيقك، مع صفوف بارتفاع يكفي لعرض محتواها عند تغيّر العرض المتاح. يستهدف المثال macOS 13 أو أحدث، ويستخدم جدولًا قائمًا على العروض.
كيف يتشارك SwiftUI وNSTableView في تحديد الحجم؟
تشارك ثلاث طبقات. يخصص الجدول مستطيل الخلية. ويربط Auto Layout الخلية بـ NSHostingView المضمّن فيها. ويحدد SwiftUI كيفية ملاءمة الملصق للمساحة المقترحة له. تصف Apple NSHostingView بأنها الجسر الذي يمرر تفضيلات حجم محتوى SwiftUI إلى AppKit أثناء التخطيط. NSHostingView
في النص القابل للالتفاف، يجب تحديد العرض قبل أن يصبح الارتفاع ذا معنى. فقد يحتاج الملصق إلى سطر واحد في عمود عريض وعدة أسطر في عمود ضيق. ولا يؤدي ضبط الإطار الأولي لعرض الاستضافة إلى إنشاء تخطيط دائم لخلية الجدول؛ إذ توضح Apple أن الجدول يضبط إطار العرض الذي تعيده مفوضية الخلية. tableView(_:viewFor:row:)
لذلك يتبع التنفيذ أدناه ترتيبًا واحدًا لتحديد الحجم: يوفر الجدول المساحة الأفقية، ويُقيَّد إطار الاستضافة بحواف الخلية مع هوامش أفقية، ويحافظ ملصق SwiftUI على حجمه الرأسي المثالي مع قبول العرض المقترح. ويستخدم الجدول Auto Layout لحساب ارتفاعات الصفوف عند تفعيل usesAutomaticRowHeights. usesAutomaticRowHeights
لا تتعامل مع intrinsicContentSize وfittingSize على أنهما قياسان متبادلان. تعرّف Apple fittingSize بأنه أصغر حجم يفي بقيود العرض وقيود العروض الفرعية. لذلك فإن الاستعلام عنه من دون قيود العرض المقصودة لا يجيب عن سؤال ارتفاع الخلية المناسب عند عرض العمود الحالي. fittingSize
أنشئ جدول ملصقات متكاملًا لـ macOS 13 والإصدارات الأحدث
أنشئ مشروع تطبيق macOS في Xcode باستخدام واجهة SwiftUI، وحدد macOS 13 أو إصدارًا أحدث كهدف للنشر. استبدل تعريف التطبيق الذي أنشأه Xcode بالملف الأول أدناه، ثم أضف ملفي Swift الآخرين إلى هدف التطبيق نفسه. تشكل هذه الملفات الثلاثة معًا برنامجًا متكاملًا؛ أبقِ تعريفًا واحدًا فقط لـ @main.
يأتي شرط macOS 13 من الاستخدام الصريح لـ NSHostingView.sizingOptions. يتوفر عرض الاستضافة نفسه منذ macOS 10.15، كما تتوفر ارتفاعات صفوف الجدول التلقائية منذ macOS 10.13. يتطلب هدف نشر أقدم إحاطة sizingOptions بفحص التوافر أو استخدام نسخة من التنفيذ تحذف هذا التخصيص. NSHostingView، sizingOptions، usesAutomaticRowHeights
أنشئ التطبيق وبيانات عينة اصطناعية
يقدم الملف الأول، LabelSizingApp.swift، ثلاثة ملصقات وزرًا يغيّر الملصق الأخير. يتيح لك هذا التحقق من التخطيط الأولي ومن تحديث المحتوى.
import SwiftUI
@main
struct LabelSizingApp: App {
@State private var expanded = false
private var labels: [String] {
[
"Amber tile",
"Blue tile with a longer descriptive label that can wrap.",
expanded
? "Green tile with several descriptive words. This longer label makes it easier to inspect wrapping when the window becomes narrower."
: "Green tile"
]
}
var body: some Scene {
WindowGroup {
VStack(alignment: .leading, spacing: 12) {
Button("Toggle longer label") {
expanded.toggle()
}
LabelTable(labels: labels)
}
.padding()
.frame(minWidth: 300, minHeight: 240)
}
}
}
اربط عرض الاستضافة بالخلية
في HostedLabelCell.swift، يقبل محتوى SwiftUI العرض المتاح ويطلب ارتفاعه المثالي. يُعد المعدّل المحوري fixedSize مهمًا: فتثبيت المحور الأفقي يطلب من النص الحفاظ على عرضه المثالي، وقد يجعله يتجاوز حدود العنصر الأب. fixedSize(horizontal:vertical:)
import AppKit
import SwiftUI
struct RowLabel: View {
let text: String
var body: some View {
Text(verbatim: text)
.font(.system(size: 14))
.fixedSize(horizontal: false, vertical: true)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.vertical, 8)
}
}
@MainActor
final class HostedLabelCell: NSTableCellView {
private let host = NSHostingView(
rootView: RowLabel(text: "")
)
override init(frame frameRect: NSRect) {
super.init(frame: frameRect)
host.translatesAutoresizingMaskIntoConstraints = false
host.sizingOptions = [.intrinsicContentSize]
addSubview(host)
NSLayoutConstraint.activate([
host.leadingAnchor.constraint(
equalTo: leadingAnchor, constant: 12
),
host.trailingAnchor.constraint(
equalTo: trailingAnchor, constant: -12
),
host.topAnchor.constraint(equalTo: topAnchor),
host.bottomAnchor.constraint(equalTo: bottomAnchor)
])
}
required init?(coder: NSCoder) {
fatalError("This sample creates cells programmatically.")
}
func configure(text: String) {
host.rootView = RowLabel(text: text)
}
}
تنشئ قيود الحواف الأربع الربط بين إطار الاستضافة والخلية. يتيح تعطيل ترجمة قناع تغيير الحجم التلقائي على إطار الاستضافة المضمّن للقيود الصريحة أن تتحكم فيه، كما توصي Apple للعروض التي تتطلب قيودها تعديلًا ديناميكيًا. ويظل الجدول مسؤولًا عن تحديد موضع الخلية الخارجية. translatesAutoresizingMaskIntoConstraints
هنا، تحتفظ sizingOptions بتحديد الحجم اعتمادًا على المحتوى الجوهري من دون طلب قيود الحدين الأدنى والأقصى لحجم إطار الاستضافة. توثق Apple هذين السلوكين كخيارين منفصلين؛ ويؤدي ضبط الخاصية على مجموعة خيارات فارغة إلى عدم إنشاء أي من هذه القيود المشتقة من المحتوى. الاحتفاظ بتحديد الحجم الجوهري خيار تنفيذي لهذه الخلية المعتمدة على المحتوى، وليس إعدادًا عامًا لكل عروض الاستضافة. sizingOptions
فعّل ارتفاعات الصفوف التلقائية واضبط الخلايا المعاد استخدامها
أضف LabelTable.swift لإنشاء عرض التمرير والجدول والمفوض. يستخدم البرنامج تحديد الحجم التلقائي عمدًا، من دون تنفيذ مفوض لارتفاع الصف يدويًا.
import AppKit
import SwiftUI
@MainActor
struct LabelTable: NSViewRepresentable {
let labels: [String]
func makeCoordinator() -> Coordinator {
Coordinator(labels: labels)
}
func makeNSView(context: Context) -> NSScrollView {
let table = NSTableView()
let column = NSTableColumn(
identifier: Coordinator.cellID
)
column.title = "Label"
column.width = 360
column.minWidth = 120
column.resizingMask = .autoresizingMask
table.addTableColumn(column)
table.headerView = nil
table.columnAutoresizingStyle = .lastColumnOnlyAutoresizingStyle
table.intercellSpacing = NSSize(width: 0, height: 0)
table.usesAutomaticRowHeights = true
table.dataSource = context.coordinator
table.delegate = context.coordinator
let scrollView = NSScrollView()
scrollView.hasVerticalScroller = true
scrollView.documentView = table
return scrollView
}
func updateNSView(
_ scrollView: NSScrollView,
context: Context
) {
guard let table = scrollView.documentView as? NSTableView,
context.coordinator.labels != labels else {
return
}
context.coordinator.labels = labels
table.reloadData()
}
@MainActor
final class Coordinator: NSObject,
NSTableViewDataSource, NSTableViewDelegate {
static let cellID = NSUserInterfaceItemIdentifier("LabelCell")
var labels: [String]
init(labels: [String]) {
self.labels = labels
super.init()
}
func numberOfRows(in tableView: NSTableView) -> Int {
labels.count
}
func tableView(
_ tableView: NSTableView,
viewFor tableColumn: NSTableColumn?,
row: Int
) -> NSView? {
let cell = (
tableView.makeView(
withIdentifier: Self.cellID,
owner: self
) as? HostedLabelCell
) ?? HostedLabelCell(frame: .zero)
cell.identifier = Self.cellID
cell.configure(text: labels[row])
return cell
}
}
}
يحاول المفوض إعادة الاستخدام قبل إنشاء خلية، ثم يعيّن الملصق الحالي في كل مرة. يتبع هذا التسلسل الذي توصي به Apple لإنشاء عروض الخلايا. يمكن أن تعيد makeView(withIdentifier:owner:) عرضًا معاد الاستخدام، أو تعيد nil إذا لم يتوفر عرض مطابق. tableView(_:viewFor:row:)، makeView(withIdentifier:owner:)
يعيد النموذج تحميل الجدول فقط عندما تتغير مصفوفة الإدخال. يسهل تتبع إعادة التحميل الكاملة لثلاثة صفوف، لكن Apple توضح أنها تُسقط عروض الصفوف والخلايا المرئية ثم تعيد الحصول عليها. في تطبيق أكبر، اختر استراتيجية تحديث أضيق نطاقًا عند ملاءمتها. أنشئ عروض AppKit وحدّثها على الممثل الرئيسي؛ وتوثق Swift أن MainActor يستخدم منفّذًا مكافئًا لطابور الإرسال الرئيسي. reloadData()، MainActor
كيف تشخّص اقتطاع النص أو زيادة ارتفاع الصف؟
ابدأ بالعرض، ثم افحص انتقال الارتفاع، وبعدها افحص التحديثات. يتبع هذا الترتيب تبعيات التخطيط ويساعد على تجنب تغيير إعدادات متعددة غير مرتبطة دفعة واحدة.
| العَرَض | أول ما ينبغي التحقق منه | التصحيح المفيد |
|---|---|---|
| يمتد النص الطويل أفقيًا إلى ما وراء الخلية | هل فُعّل تحديد الحجم الأفقي المثالي؟ | استخدم fixedSize(horizontal: false, vertical: true) لهذا الملصق القابل للالتفاف. |
| يلتف النص لكن تُقتطع أسطره الأخيرة | هل يرتبط إطار الاستضافة بحافتي الخلية الرأسيتين؟ | أعد قيدي الأعلى والأسفل وأزل الارتفاعات الثابتة المتعارضة. |
| يظل ارتفاع جميع الصفوف متساويًا | هل فُعّل تحديد الحجم التلقائي، وهل يوجد تنفيذ يدوي للارتفاع؟ | تحقق من إعداد الجدول واستخدم استراتيجية واحدة مقصودة لتحديد الحجم. |
| يظهر النص القصير في خلية أطول من اللازم | هل يحتوي المحتوى على ارتفاع ثابت أو مساحة رأسية إضافية؟ | افحص تسلسل عروض SwiftUI والحشو قبل تعديل الجدول. |
| يعرض ملصق تغيّر محتواه النص القديم | هل أُعيد ضبط الخلية بعد تغيّر النموذج؟ | حدّث العرض الجذري المعروض أو أعد تحميل المحتوى المتأثر. |
استخدم ميزانية للعرض لجعل الفحص ملموسًا. إذا كان عرض الخلية، على سبيل المثال، 320 نقطة، فإن الهامشين الأفقيين في المثال، وكل منهما 12 نقطة، يتركان 296 نقطة لإطار الاستضافة. ويضيف الحشو الرأسي 16 نقطة إجمالًا. أما الارتفاع المطلوب المتبقي فيعتمد على تخطيط النص الفعلي؛ ولا تتنبأ هذه الأرقام بعدد الأسطر التي سيشغلها ملصق بعينه.
ينشئ إطار SwiftUI الممتد محاذاة إلى الحافة اليسرى ضمن المساحة الأفقية المتاحة. ولا يعيّن عرضًا ماديًا غير محدود؛ إذ توضح وثائق Apple الخاصة بالإطار أن الإطار يشارك في المقترحات الواردة من العنصر الأب ويطبّق قيود الحجم المحددة. frame(minWidth:idealWidth:maxWidth:minHeight:idealHeight:maxHeight:alignment:)
شغّل النموذج مع الملصق الطويل في نافذة عريضة وأخرى ضيقة، ثم بدّله إلى الملصق القصير. تحقق من بقاء النص كاملًا ومن إمكانية تقليص ارتفاع الصف مجددًا. هذه اختبارات قبول ينبغي إجراؤها على إصدارات macOS التي يدعمها تطبيقك. فُحصت الشيفرة بمقارنتها بوثائق واجهات البرمجة المسترجعة؛ ولم تُترجم أو تُشغّل هنا لعدم توفر مترجم Swift أو بيئة تشغيل macOS في هذه البيئة.
ما الذي يتغير عند حساب ارتفاعات الصفوف يدويًا؟
يُعد تحديد الحجم يدويًا مسار تنفيذ منفصلًا. إذا احتجت إليه، فاحسب الارتفاعات للمحتوى والعرض المقصودين خارج مفوض الارتفاع، واجعل المفوض يعيد النتيجة المعدة. ويتضمن مفتاح التخزين المؤقت المفيد العنصر والعرض المتاح وأي مدخلات عرض تغيّر التخطيط، مثل الخط أو الحشو.
تشترط Apple أن يكون ارتفاع الصف المُعاد يدويًا أكبر من الصفر، وألا يشمل المسافة بين الخلايا. كما تمنع وثائقها الحصول على عروض الخلايا أو الصفوف داخل tableView(_:heightOfRow:)، وتحذر من استعلامات الهندسة التي قد تؤدي إلى تخطيط الجدول بشكل متكرر. لذا فإن قياس خلية مرئية من ذلك الاستدعاء ليس أساسًا مناسبًا لتنفيذ يدوي. tableView(_:heightOfRow:)
عندما تتغير الارتفاعات المعدة، أبلغ الجدول باستخدام noteHeightOfRows(withIndexesChanged:). توضح Apple أن هذا يعيد تبليط الجدول فورًا باستخدام ارتفاعات المفوض إذا كانت طريقة المفوض هذه منفذة. ويجب أيضًا إبطال القياسات المعتمدة على العرض عند تغيير حجم العمود؛ فـ columnDidResizeNotification هو الإشعار الموثق لهذا الحدث. وهذه اعتبارات ضرورية إذا استبدلت النهج التلقائي بذاكرة تخزين مؤقت يدوية. noteHeightOfRows(withIndexesChanged:)، columnDidResizeNotification
هل ينبغي أن يقيس TextKit ملصق SwiftUI؟
يفيد TextKit عندما ينسّق تطبيقك النص فعليًا عبر حاوية TextKit. لكن نطاق القياس الموثق أضيق من خلية SwiftUI كاملة: إذ تُبلغ NSLayoutManager.usedRect(for:) عن حدود المحارف الرسومية التي سبق تنسيقها داخل الحاوية المحددة، ولا تنشئ المحارف الرسومية بنفسها ولا تنفذ التخطيط. استدعِ ensureLayout(for:) قبل قراءة هذا القياس إذا لم يكتمل التخطيط. usedRect(for:)، ensureLayout(for:)
خذ إعداد الحاوية في الحسبان أيضًا. توثق Apple أن القيمة الافتراضية لـ lineFragmentPadding هي 5 نقاط عند كل طرف من أطراف مقطع السطر، ما يؤثر في عرض التخطيط المتاح. وتوضح أيضًا أن هذه الخاصية غير مخصصة للتعبير عن هوامش النص. lineFragmentPadding
والاستنتاج العملي هو أن قياسًا منفصلًا باستخدام TextKit لا يستطيع، بمفرده، تحديد ارتفاع تسلسل عروض SwiftUI هذا. فالخلية تتضمن أيضًا سلوك تخطيط SwiftUI والحشو. في جدول الملصقات هذا، أبقِ مسار تحديد الحجم عبر عرض الاستضافة وAuto Layout، ثم تحقّق من التفاف النص وتغيّر المحتوى وتقليص الارتفاع عند العروض التي يدعمها تطبيقك.
المصادر
- NSHostingView — Apple
- sizingOptions — Apple
- usesAutomaticRowHeights — Apple
- tableView(_:viewFor:row:) — Apple
- fittingSize — Apple
- fixedSize(horizontal:vertical:) — Apple
- translatesAutoresizingMaskIntoConstraints — Apple
- makeView(withIdentifier:owner:) — Apple
- reloadData() — Apple
- MainActor — Apple
- frame(minWidth:idealWidth:maxWidth:minHeight:idealHeight:maxHeight:alignment:) — Apple
- tableView(_:heightOfRow:) — Apple
- noteHeightOfRows(withIndexesChanged:) — Apple
- columnDidResizeNotification — Apple
- usedRect(for:) — Apple
- ensureLayout(for:) — Apple
- lineFragmentPadding — Apple