avatarEd

Summary

The provided web content is a comprehensive guide on implementing syntax highlighting for source code in SwiftUI applications.

Abstract

The article discusses the process of displaying source code with syntax highlighting in SwiftUI for macOS, iOS, and iPadOS applications. It begins by illustrating the importance of syntax highlighting and the transition from plain text to colorful, distinguishable code blocks. The author details the creation of a SwiftUI SampleCodeView and the evolution of styling code with monospaced fonts, specifically Menlo, and the use of background colors and borders to distinguish code blocks. The article delves into the technicalities of handling long lines of code, implementing scrollable views, and refining the appearance of code blocks. It also covers the usage of regular expressions to identify and style different elements of the code, such as keywords, class names, and property wrappers. The author provides insights into the differences between SwiftUI's Regex and NSRegularExpression, and how to handle syntax highlighting in earlier versions of iOS that do not support the latest SwiftUI features. Additionally, the article explores the enhancement of the user interface with line numbering and the ability to highlight specific lines of code. The author concludes by discussing the RegexBuilder framework and provides references to further documentation and resources.

Opinions

  • The author believes that syntax highlighting is essential for clearly presenting source code within applications.
  • There is a preference for using regular expressions to efficiently apply styles to code elements, although the author acknowledges the complexity involved.
  • The article suggests that using SwiftUI's native capabilities, such as Regex and NSRegularExpression, provides a more streamlined and efficient approach to syntax highlighting compared to traditional methods.
  • The author emphasizes the importance of adapting code to be compatible with earlier iOS versions, ensuring a broader range of application support.
  • The inclusion of line numbering and the ability to highlight specific lines of code is presented as a valuable feature for code readability and navigation.
  • The author encourages readers to engage with the content, inviting comments and suggestions, indicating a commitment to community feedback and continuous improvement.

Syntax Highlighting in SwiftUI

If you ever need to show source code in your Mac, iOS, or iPad OS application, this might be useful for you.

Photo by Markus Spiske on Unsplash

I have written several articles so far. There are a lot of code blocks in those articles. In the earlier days, only keywords are set to heavier font-weight (bold). Later on, it is changed to a colourful code block.

I’m wondering how would I display code block in an iOS or macOS application.

The quick and dirty way is setting the font to Courier or other monospaced fonts against a Text or UILabel element. Yes, it works but it is like a bowl of alphabet soup.

The next step is syntax highlighting!

So I start creating a SwiftUI reference library application. (Some people call it a kitchen sink application.) The application demonstrates various SwiftUI features with sample source code.

Here is a screenshot of the application that I have been working on.

On the left, it shows some UI. Click on the info icon and a panel pops up. The panel contains some information with syntax highlighted source code.

The First Cut

First, I’m going to create a SwiftUI View called SampleCodeView.

struct SampleCodeView: View {
    var sampleCode: String
    
    var body: some View {
        Text(sampleCode)
    }
}

struct SampleCodeView_Previews: PreviewProvider {
    static let smallBlock = #"""
if true {
   Text("Hello World")
}
"""#
    
    static let sampleSourceCode = smallBlock
    
    static var previews: some View {
        ScrollView {
            SampleCodeView(sampleCode: sampleSourceCode)
        }
    }
}

I’m going to apply a monospaced font to make it a bowl of alphabet soup. 🙂

Instead of going for the traditional Courier or “Courier New” font, I decided to use a font called Menlo.

Screenshot from Font Book

BTW, there are different sets of fonts that are preloaded in iOS and Mac. You should know which font is available in which environment. You can refer to this Apple document for available fonts.

After applying a new font and changing the sample code with a bigger block of code, this is what it looks like in the simulator.

Long lines wrap around in the view.

Let’s “unwrap” the code and see what will happen.

Text(sampleCode)
    .fixedSize(horizontal: true, vertical: true)
    .font(.custom("Menlo", size: 15))

It unwraps the long lines but only shows the code's middle part.

The .fixedSize() function indicates that the current UI element will use the “ideal” size instead of the size suggested by its parent element. (Apple’s documentation has a better explanation.)

In other words, the application will show the middle portion of the code block. The rest of the code block goes beyond the screen.

The code block cannot scroll/slide left and right across the view.

The Text element needs to wrap inside a ScrollView component. The ScrollView needs to be set for horizontal scrolling.

ScrollView(.horizontal) {
    Text(swiftCode)
        .fixedSize(horizontal: true, vertical: true)
        .font(.custom("Menlo", size: 15))
}

Functionally, it is good. I can scroll left and right to view the complete code.

Boxing Day

The code block should stand out from the rest of the content on the page. So, I’m going to mimic the appearance of a code block in this article.

ScrollView(.horizontal) {
    Text(swiftCode)
        .fixedSize(horizontal: true, vertical: false)
        .font(.custom("Menlo", size: 15))
}
.padding(10)
.background(.yellow)
.border(.red, width: 2)
.cornerRadius(10)

For the time being, let’s make the background yellow and the border red with a corner radius of 10 points. It is a bit easier to see the result.

Instead of hard coding a specific color, you should use a named color. That way you can support dark mode in your application.

It seems like the border is broken at all four corners.

It seems like the .cornerRadius() is simply removing or clipping the pixels from the underlining element. Well, it actually makes sense since it is a chain of UI operations.

If you move the .cornerRadius() modifier ahead of both .background() and .border() modifiers, you won’t see the rounded corner effect.

Instead of invoking the .border() modifier, perhaps we can overlay a RoundedRectangle on top of the ScrollView.

ScrollView(.horizontal) {
    Text(swiftCode)
        .fixedSize(horizontal: true, vertical: false)
        .font(.custom("Menlo", size: 15))
}
.padding(10)
.background(.yellow)
.cornerRadius(10)
.overlay(
    RoundedRectangle(cornerRadius: 10)
        .stroke(.red, lineWidth: 2)
)

Vola! It works.

I also found another way to implement the rounded corner box. It uses a ZStack.

ZStack {
    RoundedRectangle(cornerRadius: 10, style: .continuous)
        .fill(.yellow)
    RoundedRectangle(cornerRadius: 10, style: .continuous)
        .stroke(.red, lineWidth: 2)
    ScrollView(.horizontal) {
        Text(swiftCode)
            .fixedSize(horizontal: true, vertical: false)
            .font(.custom("Menlo", size: 15))
    }
    .padding(10)
}

It composes of three layers of objects: A rounded rectangle with background fill, a rounded rectangle with a line stroke (border), and the ScrollView with the source code.

There might be other and better ways to implement this. Feel free to share it in the comment!

Syntax Highlighting Requirements

The solution that I’m going to implement will highlight the following:

  • Swift keywords
  • Class and struct name
  • Property wrappers

Swift keywords can be found in the language reference manual. It is a finite set of words.

As for class and struct name, it is usually in pascal-case formattings, such as AttributedString, and NSMutableAttributedString.

Property wrappers are also in pascal-case format. It starts with an “@” character.

This is only a small set of requirements. You can add operators, quotation marks, brace brackets, and so on.

Thinking Process

What is it like to format some code in SwiftUI?

Technically, I should break down the code into words and spaces. Analyze each word and apply an appropriate style.

👉 A sample code snippet at Gist

Writing a parser is a bit overkill, I think.

Instead, I’m going to use regular expressions to search and apply a style.

Initialization

Define Regular Expression

Based on the criteria that I defined above, I have two regular expressions:

  1. Struct and class name, and property wrapper
  2. Swift keywords

The first one is easy. It is any word-bounded text with an optional leading “@” character.

/\u0040?\b[A-Z]w+\b/

The \u0040 is a Unicode for the “@” character.

For Swift keywords, I started by iterating through each keyword, compiling a regular expression for each keyword, and executing against the source code. Apply style against each matching result.

That doesn’t sound very efficient. There are about 100 keywords in Swift. So it needs to iterate through 100 times.

A better solution is combining all keywords into a single regular expression.

/\bassociatedtype\b|\bclass\b|\bdeinit\b|..../

The “\b” is a word-boundary token/metacharacter.

\b explanation from MDN

I copied all the keywords from Apple’s documentation, and I placed them in separate arrays based on the category specified in the documentation. (Keywords are under the “Keywords and Punctuation” section of the documentation.)

In order to create a string of regular expression pattern, we need to transform, map(), and concatenate, joined(), all the keywords together.

For simplicity, the sample code only supports Swift keywords. You can extend it to support other programming languages.

Search Set

A search set defines what the code should look for in the source code, and how to style the matches.

The structure contains an optional array of words to be sought, an uncompiled regular expression, and a (foreground) colour.

struct SearchSet {
    let words: [String]?
    let regexPattern: (String) -> String
    let color: Color
    
    init(words: [String]? = nil,
         regexPattern: @escaping (String) -> String,
         color: Color) {
        self.words = words ?? [""]
        self.regexPattern = regexPattern
        self.color = color
    }
}

You can modify it to support font weight and other font formatting features. Replace the color with one of the following declarations:

// For AttributedString
let style: AttributeContainer

// For NSAttributedString
let style: [NSAttributedString.Key: Any]

If you do so, you also need to update the functions below.

The words property comes from the original implementation. It can still be used to highlight anything else that you like. Here is an example:

SearchSet(words: ["apple", "orange", "rasin"],
          regexPattern: { word in "\\b\(word)\\b" },
          color: Color.purple)

Another example would be highlighting operators:

SearchSet(regexPattern: { word in "\\s[\\+\\-\\*\\/]\\s|\\s->\\s" },
          color: Color.orange)

👉 In the playground project, there is a SearchSetData protocol. It contains syntax highlighting SearchSet for Swift programming language.

Define CodeBlockView

This is where we define the output layout and the syntax highlighting logic.

Here is the first incarnation of the CodeBlockView init() function. It has a code argument. This is where the application passes the raw source code.

The init() function also defines the search sets.

struct CodeBlockView: View {
  private let code: String
  private var searchSets: [SearchSet] = []

  init(code: String) {
    self.code = code

    // Keywords array (too long to show)

    // Define search sets
    self.searchSets = [
        SearchSet(regexPattern: { _ in "\\u0040?\\b[A-Z]\\w+\\b"},
                  color: Color(red: 0.196, green: 0.392, blue: 0.659)),
        SearchSet(regexPattern: { _ in "\(swiftKeywords)" },
                  color: Color(red: 0.615, green: 0.196, blue: 0.659))
    ]
  }

  var body: some View {
    Text("TBD")
  }
}

In the playground project, the searchSets is refactored out into SearchSetData. That makes the init() cleaner and able to support other programming languages in the future.

Define a MasterPageView to use the CodeBlockView.

struct Sample {
  private static let dataset1 = #"""
struct SearchSet {
    let words: [String]?
    let regexPattern: (String) -> String
    let color: Color
    
    init(words: [String]? = nil,
         regexPattern: @escaping (String) -> String,
         color: Color) {
        self.words = words ?? [""]
        self.regexPattern = regexPattern
        self.color = color
    }
}
"""#

  static let data = dataset1
}

struct MasterPageView: View {
    var body: some View {
        VStack(alignment: .leading) {
            CodeBlockView(code: Sample.data)
        }
    }
}

Define The CodeBlockView Body

The CodeBlockView body will use the “box” that we defined earlier in this article.

let borderColor = Color(red: 0.5, green: 0.5, blue: 0.5)
let bgColor = Color(red: 0.9, green: 0.9, blue: 0.9)

var body: some View {
    VStack(alignment: .leading) {
        ScrollView(.horizontal) {
            Text(syntaxHighlight(self.code))
                .fixedSize(horizontal: true, vertical: true)
                .font(.custom("Menlo", size: 12))
        }
        .padding(10)
    }
    .background(bgColor)
    .cornerRadius(5)
    .overlay(
        RoundedRectangle(cornerRadius: 5, style: .continuous)
            .stroke(borderColor, lineWidth: 2)
    )
}

The content of the Text component invokes a function called syntaxHighlight(). It returns an instance of AttributedString.

Highlight The Code

The process is straightforward. Compile and execute each set of a regular expression against the given raw source code. For every matching result, apply style against the attributed source code.

Compile Search Set Regular Expression

/// Syntax highlight the source  code
/// - Parameter code: Source code
/// - Returns: Syntax highlighted source code
func syntaxHighlight(_ code: String) -> AttributedString {
    var attrInText: AttributedString = AttributedString(code)
    
    searchSets.forEach { searchSet in
        searchSet.words?.forEach({ word in
            guard let regex = try? Regex<Substring>(searchSet.regexPattern(word))
            else {
                fatalError("Failed to create regular expession")
            }
            processMatches(attributedText: &attrInText, 
                           regex: regex, 
                           color: searchSet.color)
        })
    }
    
    return attrInText
}

The first loop iterates through the searchSets. The second loop iterates through each word in the searchSet.word.

Remember the init() method sets the self.words = words ?? [“”]. So the second loop is guaranteed to have one iteration.

guard let regex = try? Regex<Substring>(searchSet.regexPattern(word)) 
else {
    fatalError("Failed to create regular expession")
}

The guard let statement tries to compile the regular expression pattern stored in the searchSet. The application will raise an exception and terminate. You can replace it with continue and move on.

There are a lot of online regular expression testers. It is a good idea to test your regular expression before adding it to your code.

Apply Style

This part is a bit tricky.

Now, we have a compiled regular expression. We need to use it to find all occurrences within the source code.

AttributedString doesn’t seem to have a method to perform regular expression operations. (If there is one, please let me know.)

However, String has a matches(of:) instance method that accepts a regular expression and returns an array of matches.

In the original implementation, I pass the source code in both String and AttributedString to the processMatches() function.

private func processMatches(originalText: String,
                            attributedText: inout AttributedString,
                            regex: Regex<Substring>,
                            color: Color) {
    ...
}

The raw String version is used for pattern matching and the AttributedString version is for styling.

Ideally, I should be able to extract the raw text from the attributedText. However, I have a hard time finding an equivalent NSAttributedString.string property in AttributedString. (Again, if there is one, please let me know.)

So the workaround is reconstructing the source code from the attributedText.

var origialText: String = (
    attributedText.characters.compactMap { c in
        String(c)
    } as [String]).joined()

Since I can reconstruct the raw source code therefore I don’t need to pass it from the calling function.

// Revised function signature
private func processMatches(attributedText: inout AttributedString,
                            regex: Regex<Substring>,
                            color: Color) {
    ...
}

Note that the attributedText argument has an inout keyword. That is because the function will directly apply the style in the attributedText.

Now I can search and apply style against the attributedText.

The String.matches(of:) will return an array of matches or an empty array. A forEach loop can iterate through each match.

orignalText.matches(of: regex).forEach { match in
    attributedText[match.range][
        AttributeScopes
            .SwiftUIAttributes.ForegroundColorAttribute.self
        ] = color
}

That causes an exception!

What it means is match.range has the type Range<String.Index> rather than Range<AttributedString.Index>.

So we need to create a new Range based on match.range with respect to attributedText.

if let swiftRange = Range(match.range, in: attributedText) {
    ...
}

Here is the complete processMatches() function:

/// Apply style for each matching word
/// - Parameters:
///   - attributedText: Attributed text
///   - regex: Compiled regular expression
///   - color: Color style
private func processMatches(attributedText: inout AttributedString,
                            regex: Regex<Substring>,
                            color: Color) {
    var origialText: String = (
        attributedText.characters.compactMap { c in
            String(c)
        } as [String]).joined()

    orignalText.matches(of: regex).forEach { match in
        if let swiftRange = Range(match.range, in: attributedText) {
            attributedText[swiftRange][
                AttributeScopes
                    .SwiftUIAttributes.ForegroundColorAttribute.self
            ] = color
        }
    }
}

The foreground colour assignment can shorten to…

attributedText[swiftRange].foregroundColor = UIColor(color)

Apparently, .foregroundColor property and key refer to AttributeScopes.UIKitAttribute.ForegroundColor! So it needs to convert SwiftUI Color to UIColor.

👉 Here is the complete Regex version of the code

Slightly Older Way

So far the code above runs in the latest O/S. i.e. iOS 16

What if I want to run it in iOS 15? It won’t have String.matches(of:) and Regex.

Fortunately, we can use NSRegularExpression.

Let’s go through this simple exercise.

Change Regex to NSRegularExpression

In the syntaxHighlight() function, the guard statement is updated with NSRegularExpression.

// From
guard let regex = try? Regex<Substring>(searchSet.regexPattern(word))
else {
    fatalError("Failed to create regular expession")
}

// To
guard let regex = try? NSRegularExpression(pattern:searchSet.regexPattern(word)) 
else {
    fatalError("Failed to create regular expression: \(word)")
}

The processMatches() function signature needs to be updated as well:

private func processMatches(attributedText: inout AttributedString,
                            regex: NSRegularExpression,
                            color: Color) {
    ...
}

The regex argument data type becomes NSRegularExpression.

We still need to extract the original text from the attributedText.

Regular expression searches with NSRegularExpression is slightly different from the Regex version. The raw source code, originalText, needs to pass into regex.enumerateMatches() function.

let ofRange = NSRange(location: 0, length: originalText.count)
regex.enumerateMatches(in: originalText, range: ofRange) { (match, _, stop) in
    guard let match = match else { return }
    
    if let swiftRange = Range(match.range, in: attributedText) {
        attributedText[swiftRange].foregroundColor = UIColor(color)
    }
}

The .enumerateMatches() function not only needs the source string. It also needs a range, NSRange, within the given text string.

You can also use NSMakeRange or another NSRange initializer to create a NSRange.

let ofRange = NSMakeRange(0, originalText.count)

let ofRange = NSRange(originalText.startIndex..<originalText.endIndex,
                      in: originalText)

The match argument in the closure is nullable. As a result, I need to use a guard statement.

Finally, apply the style against the attributedText. Just like before.

👉 Here is the complete NSRegularExpression version of the code

Two More Things

Highlight Rows

Hypothetically, I can specify a single line or a range of lines. Probably something like this:

var rows = [
    Highlight(2), Highlight(rangeOf: 7...9), Highlight(14)
]

So the Highlight structure needs to be able to initialize with a single value or a range. Here is the implementation:

struct Highlight: Identifiable {
    let id = UUID()
    private var lines: [Int] = []
    
    var values: [Int] {
        lines
    }
    
    init(_ lineNumber: Int) {
        self.lines.append(lineNumber)
    }
    
    init(rangeOf: ClosedRange<Int>) {
        for i in rangeOf {
            self.lines.append(i)
        }
    }
}

Internally, it stores line numbers in an array of Int.

The first init() function stores a single value in the array.

The second init() function accepts a range of integers, in particular, a ClosedRange of Int type, through a range operator. The function iterates through each value and appends to the array.

The CodeBlockView initialization needs to be modified a bit.

In addition to passing the entire code block to the init() function, it needs to know which row or rows to highlight.

struct CodeBlockView: View {
    private let codeLines: Array<Substring>
    private let highlightRows: Set<Int>
    ...

    init(code: String, highlightAt: [Highlight]? = nil) {
        // Split the codes into individual lines
        self.codeLines = code.split(separator: /\n/)
        
        // Collect all highlight row numbers
        var rows: Set<Int> = []
        if let lineNumbers = highlightAt {
            lineNumbers.forEach { group in
                group.values.forEach { row in
                    rows.insert(row)
                }
            }
        }
        self.highlightRows = rows

        ...
    }
    ...
}

The highlightAt argument is an array of Highlight, and it is optional.

Instead of storing the entire code block, the new init() split the code block into individual lines of code.

Apparently, code.split() return Array<Substring> instead of Array<String>

highlightRows is a Set<Int>. It holds row numbers to be highlighted. Why Set<Int>?

A Swift Set is “an unordered collection of unique elements.” It is ideal to store unique row numbers. There is no need to have duplicate row numbers!

The only change left is in the output:

var body: some View {
    VStack(alignment: .leading) {
        ScrollView(.horizontal) {
            VStack(alignment: .leading, spacing: 0) {
                ForEach(codeLines.indices, id: \.self) { idx in
                    Text(syntaxHighlight(String(codeLines[idx])))
                        .frame(maxWidth: .infinity, alignment: .leading)
                        .font(.custom("Menlo", size: 12))
                        .background(highlightRows.contains(idx+1) ? .yellow : Color.clear)
                }
            }
        }
        .padding(10)
    }
    .background(bgColor)
    .cornerRadius(5)
    .overlay(
        RoundedRectangle(cornerRadius: 5, style: .continuous)
            .stroke(borderColor, lineWidth: 2)
    )
}

There are quite a few changes in the body code.

Instead of performing syntax highlighting against the entire code block, it will become a per-line basis.

VStack(spacing: 0) {
    ForEach(codeLines.indices, id: \.self) { idx in
        ...
    }
}

The ForEach loop iterates through the codeLines indices. The index value will be used for row highlighting.

In the earlier implementation, the Text element uses .fixedSize() modifer to force unwrapping the code block. It works great for the entire code block. For the individual lines of code, it doesn’t work perfectly.

To make it easier to understand, I purposefully set a red border for the following screenshots.

.fixedSize() for the entire code block
.fixedSize() for the individual line

.fixedSize() sets the frame width that fits the content. Hence, the ideal width.

.frame(maxWidth: .infinity) for the individual line

.frame(maxWidth: .infinity, alignment: .leading) sets the frame as large as the parent element. (maxWidth: .infinity doesn’t mean it goes to the edge of the universe! 😀) Also, alignment: .leading will be left justified content within the frame.

Lastly, the logic sets the background colour if the index+1 is found in the highlightRows. Otherwise, there is no background colour, Color.clear.

.background(highlightRows.contains(idx+1) ? .yellow : Color.clear)

This is what it looks like:

Line Numbering

Since we are able to highlight rows therefore we can also add line numbers.

First, we are going to add a boolean argument to the CodeBlockView init() function. It will be used to toggle the line number column.

struct CodeBlockView: View {
    private let codeLines: Array<Substring>
    private let highlightRows: Set<Int>
    private let showLineNumber: Bool
    ...

    init(code: String, 
        highlightAt: [Highlight]? = nil, 
        showLineNumbers: Bool = false) {
        ...

        self.showLineNumber = showLineNumbers

        ...
    }
    ...
}

Next, we need to add the line number column.

Here are a couple of layout approaches:

The white text with a blue background Text element is the line number. The other one is the syntax highlighted source code.

Both layout structures have the correct appearance but they have different behaviour.

The line number column in the left layout structure will scroll off the view.

Here is the code snippet:

struct CodeBlockView: View, SearchSetData {
    ....
    private let lineNumberColumnWidth: CGFloat
    ....

    init(....) {
        ....
        self.lineNumberColumnWidth = String(self.codeLines.count)
            .size(withAttributes: [.font: UIFont(name: "Menlo", size: 12)!])
            .width
            .rounded(.up)
    }

    var body = some View {
        VStack(alignment: .leading) {
            ScrollView(.horizontal) {
                VStack(alignment: .leading, spacing: 0) {
                    ForEach(codeLines.indices, id: \.self) { idx in
                        HStack(spacing: 2) {
                            Text("\(idx+1)")
                                .frame(width: self.lineNumberColumnWidth, 
                                       alignment: .trailing)
                                .font(.custom("Menlo", size: 12))
                                .foregroundColor(.white)
                                .padding(EdgeInsets(top: 0, leading: 2, bottom: 0, trailing: 2))
                                .background(.blue)
                            Text(syntaxHighlight(String(codeLines[idx])))
                                .frame(maxWidth: .infinity, alignment: .leading)
                                .font(.custom("Menlo", size: 12))
                                .background(highlightRows.contains(idx+1) ? .yellow : Color.clear)
                        }
                    }
                }
            }
            .padding(10)
        }
        .background(bgColor)
        .cornerRadius(5)
        .overlay(
            RoundedRectangle(cornerRadius: 5, style: .continuous)
                .stroke(borderColor, lineWidth: 2)
        )
    }
}

In order to keep the line number Text element has the same width among all rows, the init() function needs to calculate the width based on the number of lines in the code block.

It is pretty straightforward. First, stringify the number of rows, codeLines.count. Get the size of the string with respect to the targetted font and font size. Finally, get the width from the resulting size modifier. In the code above, there is a .round(.up). It is optional because frame(width) is a CGFloat type.

The second approach is a two-columns layout. The left column is the line numbers, and the right column is the source code. A HStack will hold the columns side-by-side.

Here is the layout code:

var body: some View {
    VStack(alignment: .leading) {
        HStack(spacing: 5) {
            // Row number column
            if (self.showLineNumber) {
                VStack(alignment: .trailing) {
                    ForEach(codeLines.indices, id: \.self) { idx in
                        Text("\(idx+1)")
                            .font(.custom("Menlo", size: 12))
                            .foregroundColor(.white)
                            .padding(EdgeInsets(top: 0, leading: 2, bottom: 0, trailing: 2))
                    }
                }
                .background(.blue)
            }
            // Source code column
            ScrollView(.horizontal) {
                VStack {
                    ForEach(codeLines.indices, id: \.self) { idx in
                        Text(syntaxHighlight(String(codeLines[idx])))
                            .frame(maxWidth: .infinity, alignment: .leading)
                            .font(.custom("Menlo", size: 12))
                            .background(highlightRows.contains(idx+1) ? .yellow : Color.clear)
                    }
                }
            }
        }
        .padding(10)
    }
    .background(bgColor)
    .cornerRadius(5)
    .overlay(
        RoundedRectangle(cornerRadius: 5, style: .continuous)
            .stroke(borderColor, lineWidth: 2)
    )
}

Just to make it a little bit more efficient. If the application only needs to show a syntax-highlighted source code, there is no need to split the raw source code into individual lines.

init(code: String,
     highlightAt: [Highlight]? = nil,
     showLineNumbers: Bool = false) {
    
    ....
    
    if !showLineNumbers && rows.count == 0 {
        // Not going to show line numbers and no row highlighting

        // Create a single element array
        self.codeLines = [Substring(code)]
    } else {
        // Split the codes into individual lines
        self.codeLines = code.split(separator: /\n/)
    }
}

👉 Here is the complete playground project

SwiftUI Regex and NSRegularExpression

NSRegularExpression conform to the International Components for Unicode (ICU) specification for regular expressions.

Although there is no mention of such specifications for Regex, I can’t imagine that they have a different implementation.

With Regex, there is a new framework called RegexBuilder. According to Apple’s documentation, RegexBuilder “Uses an expressive domain-specific language to build regular expressions, for operations like searching and replacing in text.” It is verbose and it is an interesting way to build regular expression patterns.

There is also an online ICU regular expression tester. It might be useful for testing complicated queries.

References

I hope you enjoy this article.

Feel free to leave me comments and suggestions.

Swiftui
Regex
Nsregularexpression
Syntax Highlighting
Swift
Recommended from ReadMedium