btrballin
btrballin

Reputation: 1464

How to get the dynamic distance between 2 elements in iOS with auto layout

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)
    }
}

enter image description here

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

Answers (2)

DonMag
DonMag

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.

  • Bottom layer: gray line
  • Middle layer: red-dotted line
  • Top layer: green "bar" line

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:

enter image description here

and, with the device rotated, so you can see it automatically handles the size change - no special code for that:

enter image description here

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

rob mayoff
rob mayoff

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

Related Questions