Search code examples
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:

  • Fast
  • Able to select/copy text
  • Compatible with NSAttributedString

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.