I’m doing some tests with rendering dashed / dotted lines like so:
For the dotted lines, as an example; I created them with this function:
func drawDottedLines() -> UIImage { let path = UIBezierPath() path.move(to: CGPoint(x: 10,y: 10)) path.addLine(to: CGPoint(x: 290,y: 10)) path.lineWidth = 8
let dots: [CGFloat] = [
0.01, // dot
path.lineWidth * 2, // gap
]
path.setLineDash(dots, count: dots.count, phase: 0)
path.lineCapStyle = CGLineCap.round
UIGraphicsBeginImageContextWithOptions(CGSize(width:300, height:20), false, 2)
UIColor.white.setFill()
UIGraphicsGetCurrentContext()!.fill(.infinite)
UIColor.black.setStroke()
path.stroke()
let image = UIGraphicsGetImageFromCurrentImageContext() ?? nil
UIGraphicsEndImageContext()
return image!
}
What I need to figure out how to do next, render some text in the middle of the lines, so they render like:
* * * * TEST * * * *
How would I do that?
Not sure why you're generating a UIImage
of the dashed-line, as you could easily use a CAShapeLayer
... but anyway ...
The most straight-forward way to do this would be to overlay a label - slightly wider than the text:
and give it the same background color:
However, as you can see, depending on where your dots/dashes fall, you can get sharp edges.
If that's not acceptable, we can create a UIView
subclass to draw the dashed-line-pattern on the left-side, then mirror it horizontally and draw it on the right side.
Here's a quick example to get you started:
class DashedLineLabelView: UIView {
public var text: String = "" {
didSet {
label.text = text
setNeedsLayout()
}
}
public var pattern: [NSNumber] = [] {
didSet {
setNeedsLayout()
}
}
private let label = UILabel()
private let leftDashLayer = CAShapeLayer()
private let rightDashLayer = CAShapeLayer()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
private func commonInit() {
layer.addSublayer(leftDashLayer)
leftDashLayer.lineWidth = 8.0
leftDashLayer.strokeColor = UIColor.black.cgColor
leftDashLayer.lineCap = .round
layer.addSublayer(rightDashLayer)
rightDashLayer.lineWidth = 8.0
rightDashLayer.strokeColor = UIColor.black.cgColor
rightDashLayer.lineCap = .round
label.translatesAutoresizingMaskIntoConstraints = false
addSubview(label)
NSLayoutConstraint.activate([
label.centerXAnchor.constraint(equalTo: centerXAnchor),
label.centerYAnchor.constraint(equalTo: centerYAnchor),
])
}
override func layoutSubviews() {
super.layoutSubviews()
// we'll add 8-points "space" on each side of the label/text
let paddedW: CGFloat = label.frame.width + 16.0
// width of lines
// we need them inset by one-half the lineWidth
// so they don't extend outside the bounds of the view
let pathW: CGFloat = ((bounds.width - 8.0) - paddedW) * 0.5
let bez = UIBezierPath()
var pt: CGPoint = CGPoint(x: 4.0, y: bounds.midY)
bez.move(to: pt)
pt.x = pathW
bez.addLine(to: pt)
// same path and dash pattern for left and right dash layers
leftDashLayer.path = bez.cgPath
leftDashLayer.lineDashPattern = pattern
rightDashLayer.path = bez.cgPath
rightDashLayer.lineDashPattern = pattern
// we need to flip the right-side dash line horizontally
// so it is a mirror of the left-side
// and move it to the right side
let tr1 = CATransform3DMakeScale(-1.0, 1.0, 0.0)
let tr2 = CATransform3DMakeTranslation(pathW * 2.0 + paddedW + 8.0, 0.0, 0.0)
rightDashLayer.transform = CATransform3DConcat(tr1, tr2)
}
}
and an example controller:
class SomeTestVC: UIViewController {
let testViewA = DashedLineLabelView()
let testViewB = DashedLineLabelView()
let sampleStrings: [String] = [
"Sample Text",
"ABC",
"Testing",
"A Longer String",
]
var strIDX: Int = -1
override func viewDidLoad() {
super.viewDidLoad()
// add two Dashed Line Views
testViewA.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(testViewA)
testViewB.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(testViewB)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
testViewA.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
testViewA.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
testViewA.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
testViewA.heightAnchor.constraint(equalToConstant: 60.0),
testViewB.topAnchor.constraint(equalTo: testViewA.bottomAnchor, constant: 20.0),
testViewB.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
testViewB.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
testViewB.heightAnchor.constraint(equalToConstant: 60.0),
])
testViewA.backgroundColor = .systemYellow
testViewB.backgroundColor = .systemYellow
testViewA.pattern = [1, 16]
testViewB.pattern = [1, 16, 16, 16]
// set initial text
changeText()
}
func changeText() {
strIDX += 1
testViewA.text = sampleStrings[strIDX % sampleStrings.count]
testViewB.text = sampleStrings[strIDX % sampleStrings.count]
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
changeText()
}
}
It will look like this:
and each tap anywhere will cycle through a few sample strings: