Search code examples
swiftuitextselectionswiftui-text

Multiple lines of text with alternating row colors (a Log view). How to select text across all lines?


I have a log view. I want to display the log lines in alternating background colors. I want to allow text selection across the whole log view. This can be done with .textSelection(.enabled) on a single Text view.

My problem is however, that to get the alternating background colors, I am breaking my string into lines and show them in a ScrollView (or List). When I add .textSelection(.enabled), I can only select text in a single line at once.

How can I select across the whole text?

This is the code:

struct LogView: View {
    let lines: [String] = sampleText3.components(separatedBy: "\n")
    
    var body: some View {
        ScrollView {
            VStack(alignment: .leading, spacing: 0) {
                ForEach(lines.indices, id: \.self) { index in
                    HStack() {
                        Text(lines[index])
                            .foregroundColor(adaptiveLineColor(lines[index]))
                            .padding(.horizontal, 5)
                        Spacer()
                    }
                    .background(index % 2 == 0 ?  Color.gray.opacity(0.1) : Color.black.opacity(0.1))
                }
            }
            .textSelection(.enabled)
            .border(.gray.opacity(0.1),width: 1)
        }
        .padding()
    }
    
    func adaptiveLineColor(_ line: String) -> Color {
        enum Tag: String {
            case task = "[Task]"
            case info = "[info]"
            case warning = "[warning]"
            case error = "[error]"
        }
        // TODO: Use switch
        if line.hasPrefix(Tag.task.rawValue) {
            return .green
        }
        if line.hasPrefix(Tag.info.rawValue) {
            return .primary
        }
        if line.hasPrefix(Tag.warning.rawValue) {
            return .yellow
        }
        if line.hasPrefix(Tag.error.rawValue) {
            return .red
        }
        return .primary
    }
}

let sampleText3: String = """
[Task] Process started. 30. Sep 2023, 21:26:20
[info] input file /someFolder/someData
[warning] invalid metadata
[error] error -1234
"""

Screenshot:

enter image description here

If I put the whole text in one multiline Text, textSelection works as expected, but I wouldn't know how to do the alternating background colors.

How can I solve this?

I found this question, but it has no solution.


Solution

  • A really simple solution (though this is a bit of a hack) is to draw twice. First draw the text as many separate Texts, each with their own background, and with a transparent foreground. Then, draw the entire string as a single Text as an overlay. The user can then select the overlay.

    ScrollView {
        let attrText = makeAttributedString(sampleText3) // add all the different foreground colors as an AttributedString
    
        VStack(alignment: .leading, spacing: 0) {
            // lay out the texts with transparent foreground and alternating backgrounds
            let invisibleTexts = lines.map { Text($0).foregroundColor(.clear) }
            ForEach(lines.indices, id: \.self) { index in
                HStack {
                    invisibleTexts[index]
                        .padding(.horizontal, 5)
                    Spacer()
                }
                .background(index % 2 == 0 ?  Color.gray.opacity(0.1) : Color.black.opacity(0.1))
            }
        }
        .overlay {
            // lay out the selectable text in the same way
            HStack {
                Text(attrText).textSelection(.enabled).padding(.horizontal, 5)
                Spacer()
            }
        }
        .border(.gray.opacity(0.1),width: 1)
    }