2

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 have

What I need:

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

    }
}
Claudiu
  • 117
  • 1
  • 12

2 Answers2

2

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

    }
}
Before After
Dots at overlapping last line of text Dots stopping at last line of text
aheze
  • 6,076
  • 3
  • 6
  • 40
1

You can do that with UIKit:

  1. Find the text in the last line using this approach: https://stackoverflow.com/a/14413484/2051369
  2. Get the size of last line using let size = lastLineText.size(withAttributes: [.font: label.font]) ?? .zero
  3. let gap = label.frame.width - size.width
  4. let dotsCount = gap / dotWidth
  5. let resultText = sourceText + String(repeating: " .", count: dotsCount)