swiftperformanceswiftuinsattributedstring

Fastest component that works with NSAttributedString?


Looks like NSTextField is too slow to work with large attributed texts.

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

Is there some component that works fast enough with NSAttributedString?

I need a component that will be:

PS: SwiftUI's Text with AttributedString is much slower than NSTextField with NSAttributedString.


Application for testing performance of 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
    }
}

Solution

  • Typically large text is stored in an NSTextView, not an NSTextField. But for specialized uses, it's quite common to build your own solutions in Core Text.