I am writing an macOS app in SwiftUI with an NSTextView
, so I am using NSViewRepresentable
. This all works fine.
In the text view, I am trying to add a gap after every 10 characters. The text uses a monospaced font with letters from the alphabet only, so each glyph/character has the same width.
I could add a space after every 10th character, but that would mess up my data model.
So I am playing with exclusion paths, and I got far. But what I see now is instead of adding the gap after every 10th character, it is added after every 7th character:
Note that I am also adding a wider margin on the left.
What am I missing, my code is below.
struct TestView: View {
var string = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum"
var body: some View {
MyTextView(text: string)
}
}
struct MyTextView: NSViewRepresentable {
var text: String
final class Coordinator: NSObject, NSTextViewDelegate {
var parent: MyTextView
unowned var textView: NSTextView!
init(parent: MyTextView) {
self.parent = parent
}
func textDidChange(_ notification: Notification) {
self.parent.text = textView.string
}
}
func makeCoordinator() -> Coordinator {
Coordinator(parent: self)
}
func makeNSView(context: Context) -> NSTextView {
let t = NSTextView()
context.coordinator.textView = t
t.delegate = context.coordinator
return t
}
func updateNSView(_ t: NSTextView, context: Context) {
t.textStorage?.setAttributedString(NSAttributedString(string: text
.filter { $0.isLetter } // only keep letters
.uppercased()) // make text uppercase
)
t.font = NSFont.monospacedSystemFont(ofSize: 12.0, weight: NSFont.Weight.regular)
t.textContainer?.exclusionPaths = exclusionPaths(t)
}
func exclusionPaths(_ textView: NSTextView) -> [NSBezierPath] {
var exclusionPaths = [NSBezierPath]()
let leftMargin = CGRect(x: 0, y: 0, width: 30, height: CGFloat.greatestFiniteMagnitude)
exclusionPaths.append(NSBezierPath(rect: leftMargin))
guard let tc = textView.textContainer, let lm = tc.layoutManager else { return exclusionPaths }
var currentX = 0.0
let gapWidth = 10.0
var index = 0
repeat {
let glyphRect = lm.boundingRect(forGlyphRange: NSMakeRange(index, 10), in: tc) 77 get bounding rect for 10 glyphs/characters
currentX = leftMargin.width + glyphRect.origin.x
let columnRect = CGRect(x: currentX, y: 0, width: gapWidth, height: CGFloat.greatestFiniteMagnitude)
exclusionPaths.append(NSBezierPath(rect: columnRect))
index += 10 // skip 10 characters
}
while currentX < 500 // this should be the width of the textView
return exclusionPaths
}
}
Based on the comment by @Willeke, I was able to get it to work with the following adjustment:
func exclusionPaths(_ textView: NSTextView) -> [NSBezierPath] {
var exclusionPaths = [NSBezierPath]()
guard let tc = textView.textContainer, let lm = tc.layoutManager else { return exclusionPaths }
let leftMargin = CGRect(x: 0, y: 0, width: 30, height: CGFloat.greatestFiniteMagnitude)
exclusionPaths.append(NSBezierPath(rect: leftMargin))
// this is always the same size for monospaced font, so don't need to recalculate it in the loop.
let glyphRect = lm.boundingRect(forGlyphRange: NSMakeRange(0, 10), in: tc)
var currentX = 0.0
let gapWidth = 5.0
var index = 0
repeat {
currentX = leftMargin.width + Double(index) * (glyphRect.width + 3 * gapWidth)
let gap = CGRect(x: currentX, y: 0, width: gapWidth, height: CGFloat.greatestFiniteMagnitude)
exclusionPaths.append(NSBezierPath(rect: gap))
index += 1
}
while currentX < 500 // this should be the width of the textView
return exclusionPaths
}
Result, and it also allows selection through the gaps: