I want to fill the remaining whitespace from the last line with a dotted line. It should start at the end of the last word and continue until the end of the line. Is this possible with SwiftUI or even UIKit?
What I have:
What I need:
struct ContentView: View {
var body: some View {
let fontSize = UIFont.preferredFont(forTextStyle: .headline).lineHeight
let text = "stackoverflow stackoverflow stackoverflow stackoverflow stackoverflow stackoverflow stackoverflow stackoverflow"
HStack(alignment: .lastTextBaseline, spacing: .zero) {
HStack(alignment: .top, spacing: .zero) {
Circle()
.foregroundColor(.green)
.frame(width: 6, height: 6)
.frame(height: fontSize, alignment: .center)
ZStack(alignment: .bottom) {
HStack(alignment: .lastTextBaseline, spacing: .zero) {
Text("")
.font(.headline)
.padding(.leading, 5)
Spacer(minLength: 10)
.overlay(Line(), alignment: .bottom)
}
HStack(alignment: .lastTextBaseline, spacing: .zero) {
Text(text)
.font(.headline)
.padding(.leading, 5)
Spacer(minLength: 10)
}
}
}
}
}
}
struct Line: View {
var width: CGFloat = 1
var color = Color.gray
var body: some View {
LineShape(width: width)
.stroke(style: StrokeStyle(lineWidth: 3, dash: [3]))
.foregroundColor(color)
.frame(height: width)
}
}
private struct LineShape: Shape {
var width: CGFloat
func path(in rect: CGRect) -> Path {
var path = Path()
path.move(to: CGPoint())
path.addLine(to: CGPoint(x: rect.width, y: .zero))
return path
}
}
Here's my slightly hacky, but more simple, solution: add a white highlight to the text, so it covers the dotted line.
We can add a highlight with NSAttributedString
. SwiftUI doesn't support this by default, so we need to use UIViewRepresentable
. Here it is, based off this answer:
struct HighlightedText: View {
var text: String
@State private var height: CGFloat = .zero
private var fontStyle: UIFont.TextStyle = .body
init(_ text: String) { self.text = text }
var body: some View {
InternalHighlightedText(text: text, dynamicHeight: $height, fontStyle: fontStyle)
.frame(minHeight: height) /// allow text wrapping
.fixedSize(horizontal: false, vertical: true) /// preserve the Text sizing
}
struct InternalHighlightedText: UIViewRepresentable {
var text: String
@Binding var dynamicHeight: CGFloat
var fontStyle: UIFont.TextStyle
func makeUIView(context: Context) -> UILabel {
let label = UILabel()
label.numberOfLines = 0
label.lineBreakMode = .byWordWrapping
label.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
label.font = UIFont.preferredFont(forTextStyle: fontStyle)
return label
}
func updateUIView(_ uiView: UILabel, context: Context) {
let attributedText = NSAttributedString(string: text, attributes: [.backgroundColor: UIColor.systemBackground])
uiView.attributedText = attributedText /// set white background color here
uiView.font = UIFont.preferredFont(forTextStyle: fontStyle)
DispatchQueue.main.async {
dynamicHeight = uiView.sizeThatFits(CGSize(width: uiView.bounds.width, height: CGFloat.greatestFiniteMagnitude)).height
}
}
}
/// enable .font modifier
func font(_ fontStyle: UIFont.TextStyle) -> HighlightedText {
var view = self
view.fontStyle = fontStyle
return view
}
}
Then, just replace Text(text)
with HighlightedText(text)
.
struct ContentView: View {
var body: some View {
let fontSize = UIFont.preferredFont(forTextStyle: .headline).lineHeight
let text = "stackoverflow stackoverflow stackoverflow stackoverflow stackoverflow stackoverflow stackoverflow stackoverflow"
HStack(alignment: .lastTextBaseline, spacing: .zero) {
HStack(alignment: .top, spacing: .zero) {
Circle()
.foregroundColor(.green)
.frame(width: 6, height: 6)
.frame(height: fontSize, alignment: .center)
ZStack(alignment: .bottom) {
HStack(alignment: .lastTextBaseline, spacing: .zero) {
Text("")
.font(.headline)
.padding(.leading, 5)
Spacer(minLength: 10)
.overlay(Line(), alignment: .bottom)
}
HStack(alignment: .lastTextBaseline, spacing: .zero) {
HighlightedText(text) /// here!
.font(.headline)
.padding(.leading, 5)
Spacer(minLength: 10)
}
}
}
}
}
}
struct Line: View {
var width: CGFloat = 1
var color = Color.gray
var body: some View {
LineShape(width: width)
.stroke(style: StrokeStyle(lineWidth: 3, dash: [3]))
.foregroundColor(color)
.frame(height: width)
}
}
private struct LineShape: Shape {
var width: CGFloat
func path(in rect: CGRect) -> Path {
var path = Path()
path.move(to: CGPoint())
path.addLine(to: CGPoint(x: rect.width, y: .zero))
return path
}
}