Reputation: 1464
I have this following UI that needs to draw a dashed line. I'm not sure how to calculate the width needed especially with the autolayout dynamically determining the spacing by screen. The green bars are single UITableViewCells
. Below is the code for drawing the dashed line:
extension UIView {
func addDashedBorder(barWidth: Int, maxWidth: Int) {
//Create a CAShapeLayer
let shapeLayer = CAShapeLayer()
shapeLayer.strokeColor = UIColor.red.cgColor
shapeLayer.lineWidth = 2
// passing an array with the values [2,3] sets a dash pattern that alternates between a 2-user-space-unit-long painted segment and a 3-user-space-unit-long unpainted segment
shapeLayer.lineDashPattern = [2,3]
let path = CGMutablePath()
path.addLines(between: [CGPoint(x: 0, y: 0),
CGPoint(x: barWidth - maxWidth, y: 0)])
shapeLayer.path = path
layer.addSublayer(shapeLayer)
}
}
I need the red lines to overlap with the gray line. How would I do that given that finding the width of the gray line is inconsistent due to the when the delegate functions are called? I am currently attempting to get the width through the didMoveToSuperview
method with no luck
Upvotes: 2
Views: 1185
Reputation: 77486
There are various ways to approach this.
One option... instead of drawing only the red-dotted line as a shape layer, draw the gray line, the red-dotted line and the green bar as 3 sub-layers.
Since the green bar will be covering the red-dots and the gray line, those can each span the full width between the left edge of the cell and the left edge of the first label. That means the only piece that needs to change size is the green bar layer.
Here is some example code:
class MyProgressBarView: UIView {
var progress: CGFloat = 0.0 {
didSet {
setNeedsLayout()
}
}
let greenBar = CAShapeLayer()
let grayBar = CAShapeLayer()
let redBar = CAShapeLayer()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
layer.addSublayer(grayBar)
layer.addSublayer(redBar)
layer.addSublayer(greenBar)
redBar.strokeColor = UIColor.red.cgColor
redBar.lineWidth = 2
// passing an array with the values [2,3] sets a dash pattern that alternates between a 2-user-space-unit-long painted segment and a 3-user-space-unit-long unpainted segment
redBar.lineDashPattern = [2,3]
grayBar.strokeColor = UIColor.lightGray.cgColor
grayBar.lineWidth = 1
greenBar.strokeColor = UIColor.green.cgColor
}
override func layoutSubviews() {
super.layoutSubviews()
var path = CGMutablePath()
path.addLines(between: [CGPoint(x: 0, y: bounds.height * 0.5),
CGPoint(x: bounds.width, y: bounds.height * 0.5)])
grayBar.path = path
redBar.path = path
path = CGMutablePath()
// cell height may change, so set greenBar's height here
greenBar.lineWidth = bounds.height
path.addLines(between: [CGPoint(x: 0, y: bounds.height * 0.5),
CGPoint(x: bounds.width * progress, y: bounds.height * 0.5)])
greenBar.path = path
}
}
class ProgressCell: UITableViewCell {
@IBOutlet var val1Label: UILabel!
@IBOutlet var val2Label: UILabel!
@IBOutlet var progView: MyProgressBarView!
var val1: CGFloat = 0.0 {
didSet {
val1Label.text = "\(val1)"
// make sure we don't divide by Zero
progView.progress = val2 > 0 ? val1 / val2 : 0.0
}
}
var val2: CGFloat = 0.0 {
didSet {
val2Label.text = "$\(val2)M"
// make sure we don't divide by Zero
progView.progress = val2 > 0 ? val1 / val2 : 0.0
}
}
}
struct MyValues {
var v1: CGFloat = 0.0
var v2: CGFloat = 0.0
}
class ProgressTableViewController: UITableViewController {
var myData: [MyValues] = [
MyValues(v1: 50.0, v2: 250.0),
MyValues(v1: 80.0, v2: 250.0),
MyValues(v1: 105.0, v2: 250.0),
MyValues(v1: 127.0, v2: 250.0),
MyValues(v1: 93.0, v2: 250.0),
MyValues(v1: 80.0, v2: 250.0),
MyValues(v1: 205.0, v2: 250.0),
MyValues(v1: 177.0, v2: 250.0),
MyValues(v1: 245.0, v2: 250.0),
]
override func viewDidLoad() {
super.viewDidLoad()
}
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return myData.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "ProgressCell", for: indexPath) as! ProgressCell
let d = myData[indexPath.row]
cell.val1 = d.v1
cell.val2 = d.v2
cell.selectionStyle = .none
return cell
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
if let c = tableView.cellForRow(at: indexPath) as? ProgressCell {
let d = myData[indexPath.row]
var v = d.v1 + 10.0
v = min(v, d.v2)
myData[indexPath.row].v1 = v
c.val1 = v
}
}
}
And here is the Storyboard source:
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="15702" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="jKi-p9-QPh">
<device id="retina4_7" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15704"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--Progress Table View Controller-->
<scene sceneID="XK3-bb-mGO">
<objects>
<tableViewController id="xPK-h1-D8d" customClass="ProgressTableViewController" customModule="scratchy" customModuleProvider="target" sceneMemberID="viewController">
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" id="Ofl-mQ-aYH">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<prototypes>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" reuseIdentifier="ProgressCell" id="hIa-mP-rya" customClass="ProgressCell" customModule="scratchy" customModuleProvider="target">
<rect key="frame" x="0.0" y="28" width="375" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="hIa-mP-rya" id="fVi-sL-ouy">
<rect key="frame" x="0.0" y="0.0" width="375" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="1va-uW-uvw" customClass="MyProgressBarView" customModule="scratchy" customModuleProvider="target">
<rect key="frame" x="16" y="11" width="187" height="21.5"/>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
</view>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="150" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="UNh-No-HjD">
<rect key="frame" x="211" y="11" width="50" height="21.5"/>
<constraints>
<constraint firstAttribute="width" constant="50" id="ziy-02-rwL"/>
</constraints>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="$30.45M" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="aDM-ad-OT7">
<rect key="frame" x="269" y="11" width="90" height="21.5"/>
<constraints>
<constraint firstAttribute="width" constant="90" id="cMB-Uy-RTf"/>
</constraints>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<constraints>
<constraint firstItem="1va-uW-uvw" firstAttribute="leading" secondItem="fVi-sL-ouy" secondAttribute="leadingMargin" id="BVh-vs-fur"/>
<constraint firstItem="UNh-No-HjD" firstAttribute="leading" secondItem="1va-uW-uvw" secondAttribute="trailing" constant="8" id="Fh8-Nf-sU3"/>
<constraint firstAttribute="trailingMargin" secondItem="aDM-ad-OT7" secondAttribute="trailing" id="GC9-ua-CdP"/>
<constraint firstItem="1va-uW-uvw" firstAttribute="top" secondItem="fVi-sL-ouy" secondAttribute="topMargin" id="Mg5-6e-QBM"/>
<constraint firstItem="aDM-ad-OT7" firstAttribute="top" secondItem="fVi-sL-ouy" secondAttribute="topMargin" id="YCc-VG-5rv"/>
<constraint firstAttribute="bottomMargin" secondItem="UNh-No-HjD" secondAttribute="bottom" id="bJG-Mk-j6m"/>
<constraint firstItem="UNh-No-HjD" firstAttribute="top" secondItem="fVi-sL-ouy" secondAttribute="topMargin" id="eKv-UH-Opf"/>
<constraint firstItem="aDM-ad-OT7" firstAttribute="leading" secondItem="UNh-No-HjD" secondAttribute="trailing" constant="8" id="laQ-5R-RZa"/>
<constraint firstAttribute="bottomMargin" secondItem="1va-uW-uvw" secondAttribute="bottom" id="loG-KQ-7xC"/>
<constraint firstAttribute="bottomMargin" secondItem="aDM-ad-OT7" secondAttribute="bottom" id="wml-fK-ewt"/>
</constraints>
</tableViewCellContentView>
<connections>
<outlet property="progView" destination="1va-uW-uvw" id="Ips-T7-vZ5"/>
<outlet property="val1Label" destination="UNh-No-HjD" id="Gqe-bP-u0c"/>
<outlet property="val2Label" destination="aDM-ad-OT7" id="gMP-5N-FIb"/>
</connections>
</tableViewCell>
</prototypes>
<connections>
<outlet property="dataSource" destination="xPK-h1-D8d" id="iaK-Nh-quP"/>
<outlet property="delegate" destination="xPK-h1-D8d" id="1qh-e9-C6l"/>
</connections>
</tableView>
<navigationItem key="navigationItem" id="Gie-Xj-DLT"/>
</tableViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="9As-Tc-u8W" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="357.60000000000002" y="2048.7256371814096"/>
</scene>
<!--Navigation Controller-->
<scene sceneID="k92-RW-oNV">
<objects>
<navigationController automaticallyAdjustsScrollViewInsets="NO" id="jKi-p9-QPh" sceneMemberID="viewController">
<toolbarItems/>
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="HPb-es-8az">
<rect key="frame" x="0.0" y="0.0" width="375" height="44"/>
<autoresizingMask key="autoresizingMask"/>
</navigationBar>
<nil name="viewControllers"/>
<connections>
<segue destination="xPK-h1-D8d" kind="relationship" relationship="rootViewController" id="QXa-oP-Nwz"/>
</connections>
</navigationController>
<placeholder placeholderIdentifier="IBFirstResponder" id="vwe-Vd-YNO" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-581.60000000000002" y="2048.7256371814096"/>
</scene>
</scenes>
</document>
The result:
and, with the device rotated, so you can see it automatically handles the size change - no special code for that:
If you add this to a new project and run it, I've also implemented didSelectRowAt
-- each time you tap a row, it will increase the "left value" by 10.0
so you can see the green bar grow dynamically.
Upvotes: 2
Reputation: 385690
Since you can already lay out a view (the gray bar) exactly how you want, one way to solve this is to use a view to draw the dotted red line. You can make a new subclass of UIView
that uses a CAShapeLayer
as its layer, and set up the path and stroking parameters in the custom class’s layoutSubviews
method. I have demonstrated how to do this sort of thing (but not this exact thing) in other answers:
Upvotes: 0