Bir SwiftUI menü komutunu etkin NSTextView’a göndermek için, komut çalıştığında ana pencerenin ilk yanıtlayıcısını bulun ve ardından eylemi bu görünüme gönderin. Bu öğreticide, yalnızca odaklanmış düzenleyiciyi etkileyen Select First Line komutuna sahip iki düzenleyicili bir örnek oluşturuluyor.

Görev, kendi macOS uygulamanızdaki özel bir menü eylemidir. Örnekte sentetik metin, uygulamaya ait bir NSTextView alt sınıfı ve SwiftUI ile AppKit’in herkese açık API’leri kullanılır. Temel tasarım kararı, komutun AppKit’in yanıtlayıcı zincirini izlemesi mi yoksa belirli bir etkin düzenleyiciyi zorunlu kılması mı gerektiğidir.

“Etkin metin görünümünün” ne anlama geldiğini belirleyin

Bu örnekte etkin düzenleyici, o anda ana pencerenin ilk yanıtlayıcısı olan SampleTextView örneğidir. Apple, ana pencereyi klavye olaylarını alan pencere olarak tanımlar; pencerenin firstResponder özelliği, normalde olayları ve eylemleri ilk alan yanıtlayıcıyı belirtir. Bu özellikleri çağrı anında okumak, komutun güncel hedefini belirler. Apple’ın keyWindow belgeleri, Apple’ın firstResponder belgeleri.

AppKit ayrıca hedef belirtilmeyen eylemleri de destekler: nil değerini NSApplication.sendAction(_:to:from:) yöntemine hedef olarak iletin; uygulama da seçiciyi uygulayan bir yanıtlayıcı arar. Bu arama ana pencerenin ilk yanıtlayıcısından başlar, ancak uygulama düzeyindeki işleyicilere ulaşmadan önce temsilcilere ve ana pencereye kadar devam edebilir. Dolayısıyla hedef belirtilmeyen bir eylem, “yalnızca bu ana pencerenin etkin metin görünümünde çalıştır” anlamına gelmez. Apple’ın eylem yönlendirme belgeleri.

Gerekli davranış Yönlendirme tercihi Sonuç
AppKit’in uygun bir işleyici bulmasına izin verin Eylemi to: nil ile gönderin Yanıtlayıcı zincirinin olağan yedek davranışı uygulanır.
Yalnızca uygulamaya ait etkin düzenleyiciyi etkileyin İlk yanıtlayıcıyı bulun ve türünü doğrulayın, ardından hedef olarak doğrudan onu belirtin Bu düzenleyici etkin değilse komut durur.
Odaktan bağımsız olarak belirli bir düzenleyicide işlem yapın O düzenleyiciyi hedef olarak belirtin Bu, etkin düzenleyici komutundan farklı bir etkileşimdir.

Aşağıdaki uygulama ikinci davranışı seçer. Özel alt sınıfı denetlemek, ilgisiz bir metin alanının düzenleyicisinin yalnızca bir metin görünümü olduğu için hedef hâline gelmesini de önler.

SwiftUI ve AppKit örneğinin tamamını oluşturun

SwiftUI kullanan bir macOS uygulama projesi oluşturun ve bu örnek için dağıtım hedefini macOS 13 veya üstü olarak ayarlayın. Oluşturulan uygulama giriş noktasını aşağıdaki tek Swift dosyasıyla değiştirin; diğer tüm @main tanımlarını kaldırın. Program her iki düzenleyiciyi, başlangıç metinlerini, menüyü ve eylem işleyicisini sağlar.

NSViewRepresentable, AppKit görünümünü gömmek için SwiftUI tarafından desteklenen köprüdür. Sahne komutları menü içeriğini sağlar, CommandMenu ise macOS menü çubuğunda uygulamaya özgü bir menü oluşturur. Apple’ın NSViewRepresentable belgeleri, sahne komutları, komut menüleri.

import SwiftUI
import AppKit
import Foundation

@main
@MainActor
struct RoutingDemoApp: App {
    var body: some Scene {
        WindowGroup("Routing Demo") {
            ContentView()
        }
        .commands {
            CommandMenu("Sample Actions") {
                Button("Select First Line") {
                    Self.routeSelectFirstLine()
                }
            }
        }
    }

    private static func routeSelectFirstLine() {
        let application = NSApplication.shared
        guard let editor = application.keyWindow?.firstResponder
                as? SampleTextView else {
            return
        }

        _ = application.sendAction(
            #selector(SampleTextView.selectFirstSampleLine(_:)),
            to: editor,
            from: nil
        )
    }
}

@MainActor
struct ContentView: View {
    @State private var windowLabel = "Sample Window"

    var body: some View {
        VStack(alignment: .leading, spacing: 12) {
            TextField("Window label", text: $windowLabel)

            HStack(spacing: 12) {
                SampleEditor(initialText: "Sample Alpha\nAlpha details")
                SampleEditor(initialText: "Sample Beta\nBeta details")
            }

            Text("Click an editor, then use Sample Actions.")
                .font(.caption)
        }
        .padding()
        .frame(minWidth: 640, minHeight: 320)
    }
}

@MainActor
final class SampleTextView: NSTextView {
    @objc func selectFirstSampleLine(_ sender: Any?) {
        guard isSelectable, !hasMarkedText() else { return }

        let contents = string as NSString
        guard contents.length > 0 else { return }

        let firstLine = contents.lineRange(
            for: NSRange(location: 0, length: 0)
        )
        setSelectedRange(firstLine)
        scrollRangeToVisible(firstLine)
    }
}

@MainActor
struct SampleEditor: NSViewRepresentable {
    let initialText: String

    func makeNSView(context: Context) -> NSScrollView {
        let scrollView = NSScrollView()
        scrollView.hasVerticalScroller = true
        scrollView.hasHorizontalScroller = false
        scrollView.borderType = .bezelBorder

        let editor = SampleTextView(
            frame: NSRect(x: 0, y: 0, width: 300, height: 240)
        )
        editor.isEditable = true
        editor.isSelectable = true
        editor.isRichText = false
        editor.allowsUndo = true
        editor.font = NSFont.monospacedSystemFont(
            ofSize: 14, weight: .regular
        )
        editor.minSize = .zero
        editor.maxSize = NSSize(
            width: CGFloat.greatestFiniteMagnitude,
            height: CGFloat.greatestFiniteMagnitude
        )
        editor.isVerticallyResizable = true
        editor.isHorizontallyResizable = false
        editor.autoresizingMask = [.width]
        editor.textContainer?.containerSize = NSSize(
            width: 300,
            height: CGFloat.greatestFiniteMagnitude
        )
        editor.textContainer?.widthTracksTextView = true
        editor.string = initialText

        scrollView.documentView = editor
        return scrollView
    }

    func updateNSView(_ nsView: NSScrollView, context: Context) {
        // AppKit owns the editable contents in this example.
    }
}

Bu, bellek içi düzenleyici içeriklerini kullanan eksiksiz bir uygulama örneğidir. Alınan API belgelerine göre incelendi; mevcut ortam Linux olduğundan ve Swift araç zinciri bulunmadığından burada derlenmedi veya çalıştırılmadı.

Seçici neden amaçlanan düzenleyiciye ulaşır?

Yönlendirme işlevi, gönderimden hemen önce ilk yanıtlayıcıyı alır. Yalnızca SampleTextView türünü kabul eder ve tam olarak bu nesneyi eylemin hedefi olarak belirtir. Böylece menü, daha önce tıklanmış bir düzenleyiciye bağlı kalmaz ve tür denetimi başarısız olduğunda başka bir pencerede arama yapmaz.

Eylem yöntemi @objc kullanır; çünkü hedef-eylem gönderimi Objective-C seçicilerini kullanır. #selector ifadesi, elle yazılmış bir seçici dizesi yerine tanımlanmış yönteme başvurur. Apple, Swift yöntemlerini Objective-C hedef-eylem API’leri üzerinden çağırmak için bu birleşimi belgeliyor. Swift’te Objective-C çalışma zamanı özelliklerini kullanma.

sendAction yönteminin dönüş değeri iletinin gönderilip gönderilmediğini bildirir. İşleyicinin seçimi değiştirip değiştirmediğini bildirmez: işleyici iletiyi alıp içerik boşsa veya işaretli metin varsa erken dönebilir. Hedef ve yöntem zaten belli olduğundan, bu örnek gönderim sonucunu bilerek yok sayar. Apple’ın sendAction dönüş değeri belgeleri.

Seçim komutunun gerçekte seçtiği şey

İşleyici, dizenin başlangıcını içeren mantıksal satırı bulmak için NSString.lineRange(for:) yöntemini çağırır. Varsa bu aralığa satır sonu karakteri de dahildir. Böylece ilk satır ekranda birden fazla satıra kaydırılmış olsa bile, satır sonu karakterleriyle ayrılmış ilk satır seçilir. Apple’ın satır aralığı belgeleri.

Sağlanan metinde sol düzenleyici Sample Alpha metnini ve ardından gelen satır sonunu, sağ düzenleyici ise Sample Beta metnini ve ardından gelen satır sonunu seçer. Komut önceki seçimin yerini alır. Metni değiştirmez.

setSelectedRange(_:) seçimi uygular, scrollRangeToVisible(_:) ise seçimin ilk karakterlerini görünür alana getirir. Boş içerik denetimi, metin yokken seçim işlemi yapılmasını önler. İşaretli metin denetimi açıkça belirtilmiş bir örnek politikadır: giriş birleştirme işlemi etkin olduğunda bu özel seçim eylemini erteler. Seçim aralıkları, bir aralığı görünür alana kaydırma, işaretli metin.

Görünüm oluşturmayı, düzenlemeyi ve odağı ayrı tutun

Temsil edilebilir görünüm bir NSScrollView oluşturur ve özel metin görünümünü belge görünümü olarak kurar. Düzenleyici dikey yönde büyürken metin kapsayıcısı genişliğini izler. Bu, Apple’ın belgelerinde açıklanan kaydırılabilir metin görünümü düzenini izler. Bu düzen, Apple’ın arşivlenmiş AppKit kılavuzunda yer alır; burada alıntılanan SwiftUI köprüsü ve eylem API’leri güncel belgeleri kullanır. NSScrollView içine NSTextView nesnesi yerleştirme.

SwiftUI, temsil edilen kaydırma görünümünün yerleşimini denetler. Kod, iç içe geçmiş belge görünümünün başlangıç geometrisini ve AppKit yeniden boyutlandırma davranışını yapılandırır; temsil edilen kaydırma görünümünün çerçevesini tekrar tekrar atamaz. Apple, temsil edilebilir görünüm için SwiftUI’nin yönettiği yerleşim özelliklerini değiştirmemek konusunda özellikle uyarır. Apple’ın temsil edilebilir görünüm yerleşimi hakkındaki yönergeleri.

Örnek, initialText değerini yalnızca oluşturma sırasında atar. Boş güncelleme yöntemi, sonraki yazma işlemlerini bilerek AppKit’in denetiminde bırakır. Uygulamanızın bir SwiftUI belge modeline ihtiyacı varsa, bir koordinatör ve temsilci tabanlı eşitleme ekleyin; düzenleme sırasında gelen model değişiklikleri için de belirli bir politika tanımlayın. Her güncellemede başlangıç dizesini yeniden atamak, kullanıcının o anki içeriğini değiştirir. Apple, koordinatörlerin AppKit etkileşimlerini SwiftUI’ye ileten köprü olduğunu açıklıyor. Apple’ın temsil edilebilir görünüm koordinasyonu hakkındaki yönergeleri.

Bu anlatımda odağı düzenleyiciye tıklayarak belirlersiniz. Uygulamanız odağı programlı olarak taşımak istiyorsa, görünüm pencereye eklendikten sonra kapsayıcı pencerenin makeFirstResponder(_:) özelliğini kullanın. Apple, metin görünümünün becomeFirstResponder() yöntemini doğrudan çağırmamanız gerektiğini açıkça belirtir. Her SwiftUI güncellemesinde odak istemekten kaçının; aksi takdirde kullanıcının düzenleme hedefi tekrar tekrar değiştirilir. Apple’ın metin görünümü odağı hakkındaki yönergeleri.

Mevcut TextKit yapılandırmanızı koruyun

Menü yönlendirmesi, metin düzeni altyapısını değiştirmeyi gerektirmez. Bu örnek NSTextView(frame:) kullanır; Apple, bu başlatıcının varsayılan olarak NSLayoutManager kullandığını belgeliyor. NSTextLayoutManager ile açıkça oluşturma macOS 12’den itibaren desteklenir. Apple’ın metin görünümü başlatma belgeleri.

Mevcut bir TextKit 2 düzenleyicisi için mevcut oluşturma yöntemini koruyup eylemi alt sınıfa ekleyin. Düzenleyiciyi bulmak veya ona yönlendirme yapmak amacıyla yalnızca layoutManager özelliğini okumaktan kaçının: Apple, TextKit 2 metin görünümünde bu özelliğe erişmenin görünümü uyumluluk kipine geçirebileceğini belgeliyor. Yönlendirme kodunun yalnızca yanıtlayıcıya ve eylem yöntemine ihtiyacı vardır. Apple’ın NSTextView uyumluluk kipi belgeleri.

Yönlendirmeyi doğrulayın ve kullanılamayan komutları yönetin

macOS’ta amaçlanan davranışı denetlemek için şu adımları izleyin. Bunlar, uygulamadan elde edilmiş sonuçlar değil, uygulamanın davranışına ilişkin beklenen sonuçlardır.

  1. Sol düzenleyiciye tıklayın, ardından Sample Actions → Select First Line komutunu seçin. Sol düzenleyicinin ilk mantıksal satırı seçilmelidir.
  2. Sağ düzenleyiciye tıklayın ve aynı komutu çalıştırın. Yalnızca sağ düzenleyicinin seçimi değişmelidir.
  3. Window label alanına tıklayın ve komutu çalıştırın. İlk yanıtlayıcı alt sınıf denetimini geçemediği için iki özel düzenleyici de değişmeden kalmalıdır.
  4. Bir düzenleyicinin tüm içeriğini silin ve komutu orada çalıştırın. Boş içerik denetimi, metni değiştirmeden dönmelidir.
  5. Birkaç satır ekleyin, aşağı kaydırın ve komutu çalıştırın. İlk satır seçilmeli ve görünür alana kaydırılmalıdır.

Bu basit örnekte komut, odak başka yerdeyken bile etkin kalır. Eylemin çalışıp çalışmayacağını çağrı anındaki denetim belirler. SwiftUI .disabled ifadesinde keyWindow?.firstResponder değerini okumak, AppKit odağı değiştiğinde bu ifadenin güncellenmesini tek başına sağlamaz.

Daha büyük bir SwiftUI arayüzünde, uygun komut yeteneğini odağı izleyen durum aracılığıyla yayımlayın, @FocusedValue ile gözlemleyin ve düğmenin .disabled koşulunu bu duruma göre belirleyin. AppKit köprüsü, yeteneği gerçek düzenleyici odağıyla eşzamanlı tutmalıdır. Çağrı anındaki denetimi de koruyun. Apple, FocusedValue’nun odaklanmış görünümden veya onun üst öğelerinden gelen değerleri gözlemlediğini belgeliyor. Odaklanmış değerler, etkileşimi devre dışı bırakma.

Bunun yerine hedefi düzenleyici olan bir AppKit menü öğesi oluşturursanız, validateMenuItem(_:) AppKit’in doğrulama kancasını sağlar. Bu yöntemi bu alt sınıfa eklemek tek başına SwiftUI düğmesini doğrulamaz: düğmenin eylemi, sonrasında başka bir eylemi gönderen bir closure’dır. Apple, doğrulama nesnesinin menü öğesinin hedefi olmasını şart koşar. Apple’ın menü öğesi doğrulama belgeleri.

Daha fazla komut eklemeden önce hedef politikasını seçin. Yanıtlayıcı zincirinin yedek davranışı istenen davranışın bir parçasıysa hedef belirtilmeyen bir eylem kullanın; işlem yalnızca etkin düzenleyiciye aitse ilk yanıtlayıcıyı bulun ve hedef olarak belirtin. Bu tercih, kullanıcılar metin görünümleri ve diğer denetimler arasında geçiş yaparken aynı menünün öngörülebilir kalmasını sağlar.

Kaynaklar