Как получить выделенный текст внутри TextEditor в SwiftUI на macOS

TextEditor в SwiftUI все еще не имеет API для получения выделения пользователя. Но так как внутри используется NSTextView, мы можем подписаться на его уведомления.

Вот пример того, как получить подстроку из пользовательского выделения:

До macOS 15:

import SwiftUI

struct ContentView: View {
    @State var myString = "Привет, мир"
    @State var selectedString = ""

    var body: some View {
        HStack {
            Text("Выделение:")
                .foregroundStyle(.secondary)
            Text("\"\(selectedString)\"")
        }.padding()

        TextEditor(text: $myString)
            .font(.title)
            .onReceive(NotificationCenter.default.publisher(
                for: NSTextView.didChangeSelectionNotification
            ), perform: { notification in
                guard let textView = notification.object as? NSTextView else { return }

                // Проверяем, тот ли это текст, который нам нужен
                guard textView.string == myString else { return }

                let selectionRage = textView.selectedRange()

                let start = String.Index(
                    utf16Offset: selectionRage.lowerBound, in: myString
                )
                let end = String.Index(
                    utf16Offset: selectionRage.upperBound, in: myString
                )
                let subStringRange: Range<String.Index> = start..<end

                selectedString = String(myString[subStringRange])
        })
    }
}

На macOS 15+ появилась новая структура TextSelection, которую можно использовать в подписчике .onChange:

import SwiftUI

struct ContentView: View {
    @State var myString = "Привет, мир"
    @SSState var selectedString = ""
    @State private var selection: TextSelection?

    var body: some View {
        HStack {
            Text("Выделение:")
                .foregroundStyle(.secondary)
            Text("\"\(selectedString)\"")
        }.padding()

        TextEditor(text: $myString, selection: $selection)
            .font(.title)
            .onChange(of: selection) {
                if let selection = selection.take() {
                    if case let .selection(range) = selection.indices {
                        selectedString = String(myString[range])
                    }
                }
            }
    }
}