Search code examples
iosswiftuiuikituilabel

Fill remaining whitespace in text line with dots (multiline text) iOS


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

    }
}

Solution

  • 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