使用 NSAttributedString 的最快组件?

Fastest component that works with NSAttributedString?

看起来 NSTextField 对于大型属性文本的处理速度太慢了。

1000 rows with 18 symbols each are slow on M1 processor;
3000 rows slow on macbook pro 2015

是否存在一些组件可以足够快地使用 NSAttributedString?

我需要的组件将是:

PS:SwiftUI 的 TextAttributedStringNSTextFieldNSAttributedString

慢得多

NSTextField

性能测试申请
@main
struct TestAppApp: App {
    var body: some Scene {
        WindowGroup {
            AttrTest()
        }
    }
}

struct AttrTest: View {
    @State var nsString: NSAttributedString = generateText(rows: 1000)
    var body: some View{
        VStack {
            HStack{
                Button("1000") {
                    nsString = generateText(rows: 1000)
                }
                Button("2000") {
                    nsString = generateText(rows: 2000)
                }
                Button("5000") {
                    nsString = generateText(rows: 5000)
                }
                Button("7000") {
                    nsString = generateText(rows: 7000)
                }
                Button("9000") {
                    nsString = generateText(rows: 9000)
                }
            }  
            TabView {
                VStack{
                    AttributedText(attributedString: $nsString, selectable: false)   
                }
                .tabItem {
                    Text("NSTextField")
                }
                AttributedText(attributedString: $nsString, selectable: false)
                    .padding(.leading, 80)
                    .background(Color.green)
                    .tabItem {
                        Text("Other")
                    }
            }
        }
    }
}

func generateText(rows: Int) -> NSMutableAttributedString {
    let attrs: [[NSAttributedString.Key : Any]] = [
        [.foregroundColor: NSColor.red],
        [.backgroundColor: NSColor.blue],
        [.strokeColor: NSColor.blue],
        [.strokeColor: NSColor.green],
        [.underlineColor: NSColor.green],
        [.underlineColor: NSColor.yellow],
        [.underlineColor: NSColor.gray],
        [.backgroundColor: NSColor.yellow],
        [.backgroundColor: NSColor.green],
        [.backgroundColor: NSColor.magenta]
    ]
    let str = NSMutableAttributedString(string: "")
    for _ in 0...rows {
        let strNew = NSMutableAttributedString(string: "fox jumps over the lazy dog\n")
        strNew.setAttributes(attrs.randomElement(), range: NSRange(location: 0, length: strNew.length) ) 
        str.append(strNew)
    }
    return str
}

@available(OSX 11.0, *)
public struct AttributedText: NSViewRepresentable {
    @Binding var text: NSAttributedString
    private let selectable: Bool
    public init(attributedString: Binding<NSAttributedString>, selectable: Bool = true) {
        _text = attributedString
        self.selectable = selectable
    }
    public func makeNSView(context: Context) -> NSTextField {
        let textField = NSTextField(labelWithAttributedString: text)
        textField.preferredMaxLayoutWidth = textField.frame.width
        textField.allowsEditingTextAttributes = true // Fix of clear of styles on click
        textField.isSelectable = selectable
        return textField
    }
    public func updateNSView(_ textField: NSTextField, context: Context) {
        textField.attributedStringValue = $text.wrappedValue
    }
}

通常大文本存储在 NSTextView 中,而不是 NSTextField 中。但是对于特殊用途,在 Core Text 中构建您自己的解决方案是很常见的。

代码基于 Rob Napier 的回答:

import SwiftUI
import Cocoa

@available(OSX 11.0, *)
public struct AttributedText: View {
    @Binding var text: NSAttributedString
    
    public init(attributedString: Binding<NSAttributedString>) {
        _text = attributedString
    }
    
    public var body: some View {
        AttributedTextInternal(attributedString: $text)
            .frame(minWidth: $text.wrappedValue.size().width + 350, minHeight: $text.wrappedValue.size().height )
    }
}

@available(OSX 11.0, *)
public struct AttributedTextInternal: NSViewRepresentable {
    @Binding var text: NSAttributedString
    
    public init(attributedString: Binding<NSAttributedString>) {
        _text = attributedString
    }
    
    public func makeNSView(context: Context) -> NSTextView {
        let textView = NSTextView()
        textView.isRichText = true
        textView.isSelectable = true
        
        textView.setContent(text: text, makeNotEditable: true)
        
        textView.textStorage
        
        return textView
    }
    
    public func updateNSView(_ textView: NSTextView, context: Context) {
        textView.setContent(text: text, makeNotEditable: true)
    }
}

extension NSTextView {
    func setContent(text: NSAttributedString, makeNotEditable: Bool) {
        self.isEditable = true
        
        self.selectAll(nil)
        self.insertText(text, replacementRange: self.selectedRange())
        
        self.isEditable = !makeNotEditable
    }
}