لإظهار نقطة الإدراج في NSTextView المُضمَّن في تطبيق Mac، أعد تركيز لوحة المفاتيح واستدعِ scrollRangeToVisible(_:) مع التحديد الحالي. اضبط لون مؤشر الكتابة على نحو منفصل. يتطلب إبقاء المؤشر ضمن الشاشة وإيقاف وميضه إعدادين مختلفين.

ينشئ هذا الدليل محررًا محليًا صغيرًا مع زر Reveal insertion point. وهو يعالج مهمة مكتبية مألوفة: العودة إلى موضع التحرير بعد التمرير إلى مكان آخر في المستند. يحافظ التنفيذ على التحديد الحالي ويترك لـ AppKit إدارة إدخال النص المعتاد ورسم المؤشر.

تحديد سبب اختفاء نقطة الإدراج

ابدأ بما تراه. قد يؤدي التمرير أو التركيز أو التحديد أو حركة المؤشر إلى نتائج متشابهة ظاهريًا، لكن كل حالة تتطلب استجابة مختلفة.

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

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

يتبع التركيز النافذة الحاوية. وتوجّه Apple المتصلين صراحةً إلى استخدام window.makeFirstResponder(textView) بدلًا من استدعاء becomeFirstResponder() لعرض النص مباشرةً. يتبع التنفيذ أدناه هذا المسار ويتحقق من الكائن الذي أصبح المستجيب الأول بالفعل. توثيق Apple لـ NSTextView، توثيق الانتقال إلى المستجيب الأول.

إنشاء محرر محلي بأمر لإظهار الموضع

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

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

إنشاء نافذة SwiftUI

يزيد الزر رقم الطلب. وهذا يمنح الغلاف أمرًا محددًا لمعالجته بدلًا من إعادة تركيز المحرر أو تمريره مع كل تحديث من SwiftUI.

// CaretDemoApp.swift
import SwiftUI

@main
struct CaretDemoApp: App {
    var body: some Scene {
        WindowGroup("Local Editor") {
            EditorScreen()
        }
    }
}

struct EditorScreen: View {
    @State private var revealRequest = 0

    private let sampleText = (1...80)
        .map { "Sample row \($0): blue square, green circle." }
        .joined(separator: "\n")

    var body: some View {
        VStack(alignment: .leading, spacing: 12) {
            Button("Reveal insertion point") {
                revealRequest += 1
            }

            CaretEditor(
                initialText: sampleText,
                revealRequest: revealRequest
            )
        }
        .padding()
        .frame(minWidth: 480, minHeight: 320)
    }
}

تضمين محرر AppKit قابل للتمرير

NSViewRepresentable هو الجسر الذي توفره Apple لإدارة عرض AppKit ضمن تسلسل عروض SwiftUI. هنا، العرض المُمثَّل هو NSScrollView، ويكون NSTextView عرض المستند الخاص به. يتذكر المنسق آخر طلب تمت معالجته. وتوفر Apple دالة إنشاء عرض النص القابل للتمرير المستخدمة أثناء الإنشاء. NSViewRepresentable، scrollableTextView().

// CaretEditor.swift
import AppKit
import SwiftUI

@MainActor
struct CaretEditor: NSViewRepresentable {
    let initialText: String
    let revealRequest: Int

    @MainActor
    final class Coordinator {
        var lastHandledRequest: Int

        init(lastHandledRequest: Int) {
            self.lastHandledRequest = lastHandledRequest
        }
    }

    func makeCoordinator() -> Coordinator {
        Coordinator(lastHandledRequest: revealRequest)
    }

    func makeNSView(context: Context) -> NSScrollView {
        let scrollView = NSTextView.scrollableTextView()
        scrollView.hasVerticalScroller = true

        guard let textView = scrollView.documentView as? NSTextView else {
            preconditionFailure("Expected an NSTextView document view")
        }

        textView.isRichText = false
        textView.isEditable = true
        textView.isSelectable = true
        textView.allowsUndo = true
        textView.drawsBackground = true
        textView.backgroundColor = .textBackgroundColor
        textView.textColor = .textColor
        textView.insertionPointColor = .textColor
        textView.font = .systemFont(ofSize: 15)
        textView.textContainerInset = NSSize(width: 12, height: 12)
        textView.string = initialText

        let end = (initialText as NSString).length
        textView.setSelectedRange(NSRange(location: end, length: 0))
        return scrollView
    }

    func updateNSView(_ scrollView: NSScrollView, context: Context) {
        guard revealRequest != context.coordinator.lastHandledRequest else {
            return
        }
        context.coordinator.lastHandledRequest = revealRequest

        guard let textView = scrollView.documentView as? NSTextView,
              let window = textView.window,
              textView.acceptsFirstResponder else {
            return
        }

        guard window.makeFirstResponder(textView),
              window.firstResponder === textView else {
            return
        }

        textView.scrollRangeToVisible(textView.selectedRange())
    }
}

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

يستخدم الموضع الختامي NSString.length، التي تعد وحدات شيفرة UTF-16. يتطابق عرض Swift لـ UTF-16 مع واجهات NSString التي تعتمد على الفهرسة، بينما تحسب String.count في Swift عدد المحارف. عند إنشاء نطاقات نصية لـ AppKit، حافظ على اتساق هذه التمثيلات. تجنّب قراءة selectedRange() حساب إزاحة لأمر الإظهار نفسه. NSString length، Swift String.UTF16View، Swift String.count.

الحفاظ على حالة التحرير أثناء تحديثات SwiftUI

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

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

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

كما أن حواجز التركيز مهمة. قد يفشل makeFirstResponder(_:) عندما يرفض المستجيب الحالي التخلي عن دوره. وتشير Apple أيضًا إلى أن نجاح الإرجاع قد يترك النافذة نفسها مستجيبًا أول إذا لم يقبل المستجيب المطلوب التركيز. يتحقق فحص window.firstResponder === textView من النتيجة الفعلية قبل التمرير. قيم الإرجاع وسلوك الانتقال إلى المستجيب الأول.

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

ضبط اللون من دون استبدال رسم المؤشر

يعيّن المثال .textColor لكل من النص ونقطة الإدراج، ويعيّن .textBackgroundColor لخلفية المحرر. وهذه ألوان دلالية موثّقة للنص والمنطقة خلفه. أما insertionPointColor فهي الخاصية الموثّقة لضبط لون المؤشر. تحقّق من المظهر الناتج على كل إصدار من macOS ومظهر يدعمهما تطبيقك. لون المؤشر، لون النص، لون خلفية النص.

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

يتجنب المثال أيضًا الوصول إلى layoutManager لحساب هندسة المؤشر. توثّق Apple أن الوصول إلى هذه الخاصية في عرض نص أو حاوية نص قد يحوّل العرض إلى وضع التوافق الذي يستخدم NSLayoutManager في macOS 12 والإصدارات الأحدث. ويجنّب استدعاء واجهة التمرير عالية المستوى إدخال هذا التغيير في توافق TextKit لمجرد إظهار التحديد. سلوك توافق TextKit.

فهم حدود إبقاء المؤشر ظاهرًا دائمًا

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

في macOS 14 والإصدارات الأحدث، توفر NSTextInsertionIndicator أوضاع العرض .automatic و.visible و.hidden. توثّق Apple استخدام .visible لعرض المؤشر باستمرار مع تعطيل الحركات، مثل الإشارة إلى وجهة السحب. تستخدم عروض النص القياسية في AppKit واجهة مؤشر النظام تلقائيًا. دليل اعتماد مؤشر نص النظام، NSTextInsertionIndicator.

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

لا توفر واجهتا API الأقدم مظهرًا الإعداد الناقص. توثّق shouldDrawInsertionPoint على أنها للقراءة فقط. أما drawInsertionPoint(in:color:turnedOn:) فترسم المؤشر أو تمحوه، وتقول Apple إن المتصلين لا ينبغي أن يحتاجوا إلى استدعائها مباشرةً. ولا توثّق أي منهما بوصفها مفتاحًا لإبقائه ظاهرًا دائمًا. حالة رسم نقطة الإدراج، طريقة رسم نقطة الإدراج.

في مهمة المحرر العادية التي تتناولها هذه المقالة، أبقِ حركة مؤشر AppKit كما هي، ووفّر طريقة موثوقة للعودة إلى الموضع الحالي. ويتطلب تنفيذ مؤشر مخصص تصميمًا خاصًا به، وحدًا لتوفر macOS 14 عند استخدام NSTextInsertionIndicator.

التحقق من السلوك في تطبيقك

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

  1. شغّل التطبيق واضغط على Reveal insertion point. ينبغي أن يتلقى المحرر التركيز وأن يُظهر موضع نهاية النص.
  2. انقر داخل صف أسبق لنقل المؤشر. مرّر بعيدًا من دون النقر على موضع نص آخر، ثم اضغط الزر. ينبغي أن يُظهر ذلك الموضع الأسبق.
  3. حدّد عدة كلمات واضغط الزر. ينبغي أن يبقى التحديد كما هو؛ فالأمر لا يدمجه ليصبح مؤشرًا.
  4. اكتب تسمية مميزة مثل orange triangle، ثم اضغط الزر عدة مرات. ينبغي أن يبقى المحتوى المعدّل سليمًا.
  5. غيّر حجم النافذة وكرر عملية الإظهار. ينبغي أن يظل الأمر يستخدم النطاق الحالي بدلًا من إحداثيات شاشة قديمة.
  6. تحقّق من المظهرين الفاتح والداكن على إصدارات macOS المدعومة. تأكد من أن ألوان النص والخلفية والمؤشر متناسقة وقابلة للاستخدام.

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

المصادر