If you had a max number of rows less than, say, 50, it would be much easier to use a normal scroll view with repeating subviews, rather than a table view.
However, if you may have 100 - or a few hundred - rows, you may run into memory issues.
So, one way to get that "curving dashed line" is to use a custom view class in your cell that draws the line with a shape layer.
It would look something like this (the yellow rectangle is showing the "cell" frame):
So the code generating the bezier path for the shape layer would be:
1
2
c
3
Shape lines/strokes are centered on the path. So, if we use a line width of 4
, 2-points will extend above the top of the cell/view, and 2-points will extend below the bottom .
If we layout those same 4 views with Zero vertical spacing, and alternate right / left / right / left, we get this:
We can then implement that in our table view cell:
A couple issues will crop up though...
First, because the rows have variable heights, the line-lengths will be different. The dash-patterns don't "stretch to fill" the line, so the ends will vary:
The other issue would hit if your rows are taller than one-half the width (actually, less than half because we allow space at the sides).
Here's what that means:
Of course, that is more of a design issue than a coding issue, as it would be up to you to decide how you want the line to look in that case.
Here's the code I used to generate those images:
enum - for left/right layout:
enum LayoutDirection: Int {
case left, right
}
the cells have 3 labels - so a simple 3-string struct for the data:
struct MyDataStruct {
var first: String = ""
var second: String = ""
var third: String = ""
}
PieView - a simple pie-shape UIView
subclass
class PieView: UIView {
private let shapeLayer1 = CAShapeLayer()
private let shapeLayer2 = CAShapeLayer()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
private func commonInit() {
[shapeLayer1, shapeLayer2].forEach { v in
layer.addSublayer(v)
v.fillColor = UIColor.systemOrange.cgColor
v.strokeColor = UIColor.systemOrange.cgColor
v.lineWidth = 2
}
shapeLayer1.fillColor = UIColor.clear.cgColor
}
override func layoutSubviews() {
super.layoutSubviews()
var bez: UIBezierPath!
let ptC: CGPoint = CGPoint(x: bounds.midX, y: bounds.midY)
let a1: Double = -90.0 * .pi / 180.0
let a2: Double = 135.0 * .pi / 180.0
bez = UIBezierPath()
bez.addArc(withCenter: ptC, radius: bounds.midX, startAngle: a2, endAngle: a1, clockwise: true)
shapeLayer1.path = bez.cgPath
bez = UIBezierPath()
bez.move(to: ptC)
bez.addArc(withCenter: ptC, radius: bounds.midX, startAngle: a1, endAngle: a2, clockwise: true)
bez.close()
shapeLayer2.path = bez.cgPath
}
}
MyDashedArcView - UIView
subclass that draws the dashed-arc
class MyDashedArcView: UIView {
public var layoutDirection: LayoutDirection = .left {
didSet {
setNeedsLayout()
}
}
private var shapeLayer: CAShapeLayer!
override class var layerClass: AnyClass {
return CAShapeLayer.self
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
private func commonInit() {
shapeLayer = self.layer as? CAShapeLayer
shapeLayer.strokeColor = UIColor.blue.cgColor
shapeLayer.lineWidth = 4
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.lineDashPattern = [20, 10]
}
override func layoutSubviews() {
super.layoutSubviews()
let inset: CGFloat = 32.0
let radius: CGFloat = bounds.midY
var ptC: CGPoint = CGPoint(x: 0.0, y: bounds.midY)
ptC.x = layoutDirection == .right ? bounds.maxX - (inset + radius) : inset + radius
let a1: Double = -90.0 * .pi / 180.0
let a2: Double = 90.0 * .pi / 180.0
let xOff: CGFloat = 0.0
let bez = UIBezierPath()
bez.move(to: CGPoint(x: bounds.midX + xOff, y: bounds.minY - 0.0))
bez.addLine(to: CGPoint(x: ptC.x, y: bounds.minY))
if layoutDirection == .right {
bez.addArc(withCenter: ptC, radius: bounds.midY, startAngle: a1, endAngle: a2, clockwise: true)
} else {
bez.addArc(withCenter: ptC, radius: bounds.midY, startAngle: a1, endAngle: a2, clockwise: false)
}
bez.addLine(to: CGPoint(x: bounds.midX + xOff, y: bounds.maxY))
shapeLayer.path = bez.cgPath
}
}
MyPieCell - table view cell
class MyPieCell: UITableViewCell {
private var layoutDirection: LayoutDirection = .right {
didSet {
// update horizontal constraints to position the pieView and labels stack view
let g = contentView
pieHorizontalConstraint.isActive = false
stackLeadingConstraint.isActive = false
stackTrailingConstraint.isActive = false
if layoutDirection == .left {
// pie is on the left
pieHorizontalConstraint = pieView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 48.0)
stackLeadingConstraint = stack.leadingAnchor.constraint(equalTo: pieView.trailingAnchor, constant: 20.0)
stackTrailingConstraint = stack.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0)
[firstLabel, secondLabel, thirdLabel].forEach { v in
v.textAlignment = .left
}
} else {
// pie is on the right
pieHorizontalConstraint = pieView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -48.0)
stackLeadingConstraint = stack.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0)
stackTrailingConstraint = stack.trailingAnchor.constraint(equalTo: pieView.leadingAnchor, constant: -20.0)
[firstLabel, secondLabel, thirdLabel].forEach { v in
v.textAlignment = .right
}
}
pieHorizontalConstraint.isActive = true
stackLeadingConstraint.isActive = true
stackTrailingConstraint.isActive = true
}
}
func fillData(_ str: MyDataStruct, direction: LayoutDirection) {
firstLabel.text = str.first
secondLabel.text = str.second
thirdLabel.text = str.third
layoutDirection = direction
arcView.layoutDirection = direction
}
private let pieView = PieView()
private let arcView = MyDashedArcView()
private let firstLabel: UILabel = {
let v = UILabel()
v.font = .systemFont(ofSize: 13.0, weight: .regular)
return v
}()
private let secondLabel: UILabel = {
let v = UILabel()
v.font = .systemFont(ofSize: 16.0, weight: .bold)
return v
}()
private let thirdLabel: UILabel = {
let v = UILabel()
v.font = .systemFont(ofSize: 13.0, weight: .regular)
v.numberOfLines = 0
return v
}()
// stack view for the labels
private let stack: UIStackView = {
let v = UIStackView()
v.axis = .vertical
v.spacing = 2
return v
}()
private var pieHorizontalConstraint: NSLayoutConstraint!
private var stackLeadingConstraint: NSLayoutConstraint!
private var stackTrailingConstraint: NSLayoutConstraint!
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
private func commonInit() {
[firstLabel, secondLabel, thirdLabel].forEach { v in
v.setContentCompressionResistancePriority(.required, for: .vertical)
stack.addArrangedSubview(v)
}
[arcView, pieView, stack].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(v)
}
let g = contentView
// initialize the horizontal constraints that we will update
// based on left or right layout
pieHorizontalConstraint = pieView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 60.0)
stackLeadingConstraint = stack.leadingAnchor.constraint(equalTo: pieView.trailingAnchor, constant: 20.0)
stackTrailingConstraint = stack.trailingAnchor.constraint(equalTo: pieView.leadingAnchor, constant: -20.0)
// giving avoid auto-layout complaints
// pieView is square (1:1 ratio)
// pieView width constant
pieView.widthAnchor.constraint(equalToConstant: 60.0).isActive = true
let pieHeightConstraint = pieView.heightAnchor.constraint(equalTo: pieView.widthAnchor)
pieHeightConstraint.priority = .required - 1
pieHeightConstraint.isActive = true
NSLayoutConstraint.activate([
// constrain arcView to all 4 sides
arcView.topAnchor.constraint(equalTo: g.topAnchor),
arcView.leadingAnchor.constraint(equalTo: g.leadingAnchor),
arcView.trailingAnchor.constraint(equalTo: g.trailingAnchor),
arcView.bottomAnchor.constraint(equalTo: g.bottomAnchor),
// center the pieView vertically
pieView.centerYAnchor.constraint(equalTo: g.centerYAnchor),
// we want at least 12-points above and below the pieView
pieView.topAnchor.constraint(greaterThanOrEqualTo: g.topAnchor, constant: 12.0),
pieView.bottomAnchor.constraint(lessThanOrEqualTo: g.bottomAnchor, constant: -12.0),
// center the labels stack view vertically
stack.centerYAnchor.constraint(equalTo: g.centerYAnchor),
// we want at least 12-points above and below the stack view
stack.topAnchor.constraint(greaterThanOrEqualTo: g.topAnchor, constant: 12.0),
stack.bottomAnchor.constraint(lessThanOrEqualTo: g.bottomAnchor, constant: -12.0),
])
// we need to see the table view's background view through the cells
contentView.backgroundColor = .clear
self.backgroundColor = .clear
// during development, if we want to see the framing
//pieView.backgroundColor = .green
//stack.backgroundColor = .yellow
}
}
SampleTableVC - example view controller with table view
class SampleTableVC: UIViewController, UITableViewDataSource, UITableViewDelegate {
var myData: [MyDataStruct] = []
let tableView = UITableView()
override func viewDidLoad() {
super.viewDidLoad()
// generate some sample data
let sampleStrings: [String] = [
"Short string.",
"Medium length string that may or may not wrap.",
"This is a very long string that will definitely wrap. When running on an iPhone 8 in portrait orientation, it should wrap to four lines.",
]
let thirdLabels: [Int] = [
0, 1, 0, 1, 0, 1, 2, 0, 1, 2, 1, 2, 2, 2,
]
var rowNum: Int = 0
thirdLabels.forEach { n in
var str: MyDataStruct = MyDataStruct()
str.first = "Level \(rowNum)"
str.second = "Foundation \(rowNum)"
str.third = sampleStrings[n % sampleStrings.count]
myData.append(str)
rowNum += 1
}
// and some more data, with increasing number of lines for the third label
for i in 4...16 {
var str: MyDataStruct = MyDataStruct()
str.first = "Level \(rowNum)"
str.second = "Foundation \(rowNum)"
str.third = (1...i).compactMap({"Line \($0)"}).joined(separator: "\n")
myData.append(str)
rowNum += 1
}
// and a few rows with extremely long strings
let reallyLongString = "UILabel - A label can contain an arbitrary amount of text, but UILabel may shrink, wrap, or truncate the text, depending on the size of the bounding rectangle and properties you set. You can control the font, text color, alignment, highlighting, and shadowing of the text in the label.\n\nUITextField - Displays a rounded rectangle that can contain editable text. When a user taps a text field, a keyboard appears; when a user taps Return in the keyboard, the keyboard disappears and the text field can handle the input in an application-specific way. UITextField supports overlay views to display additional information, such as a bookmarks icon. UITextField also provides a clear text control a user taps to erase the contents of the text field."
for _ in 1...5 {
var str: MyDataStruct = MyDataStruct()
str.first = "Level \(rowNum)"
str.second = "Foundation \(rowNum)"
str.third = reallyLongString
myData.append(str)
rowNum += 1
}
tableView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(tableView)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
tableView.topAnchor.constraint(equalTo: g.topAnchor, constant: 0),
tableView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0),
tableView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0),
tableView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: 0),
])
tableView.register(MyPieCell.self, forCellReuseIdentifier: "c")
tableView.dataSource = self
tableView.delegate = self
tableView.separatorStyle = .none
// because the dashed line will extend above the top of the first cell
// and below the bottom of the last cell
// we want to add a little "inset padding" on top and bottom of the table view
var defaultInset = tableView.contentInset
defaultInset.top += 8
defaultInset.bottom += 8
tableView.contentInset = defaultInset
tableView.contentInsetAdjustmentBehavior = .never
tableView.contentOffset.y = -8
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return myData.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let c = tableView.dequeueReusableCell(withIdentifier: "c", for: indexPath) as! MyPieCell
let dir: LayoutDirection = indexPath.row % 2 == 0 ? .right : .left
c.fillData(myData[indexPath.row], direction: dir)
return c
}
}
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.