2

Cannot access uiview's frame after setting it's layout. Frame, and center is given as 0,0 points.

I should also mention that there are no storyboard's in this project. All views and everything are created programmatically.

I have created a UIView resultView programmatically and added it as a subview in scrollView, which is also added as a subview of view, then set it's constraints, anchors in a method called setupLayout() I call setupLayout() in viewDidLoad() and after that method, I call another method called configureShapeLayer(). Inside configureShapeLayer() I try to access my view's center as:

let center = resultView.center // should give resultView's center but gives 0

Then by using this center value I try to add two bezierPaths to have a status bar kind of view. But since resultView's center is not updated at that point, it appears as misplaced. Please see the pic below:

misplaced status bar layer

I also tried calling setupLayout() in loadView() then calling configureShapeLayer() in viewDidLoad() but nothing changed.

So I need a way to make sure all views are set in my view, all constraints, and layouts are applied before calling configureShapeLayer(). But how can I do this?

I also tried calling configureShapeLayer() in both viewWillLayoutSubviews() and viewDidLayoutSubviews() methods but it made it worse, and didnt work either.

Whole View Controller File is given below: First views are declared, then they are added into the view in prepareUI(), at the end of prepareUI(), another method setupLayout() is called. After it completes setting layout, as can be seen from viewDidLoad, finally configureShapeLayer() method is called.

import UIKit

class TryViewController: UIViewController {

let score: CGFloat = 70

lazy var percentage: CGFloat = {
    return score / 100
}()

// MARK: View Declarations
private let scrollView: UIScrollView = {
    let scrollView = UIScrollView()
    scrollView.backgroundColor = .white
    return scrollView
}()

private let iconImageView: UIImageView = {
    let imageView = UIImageView()
    imageView.contentMode = .scaleAspectFit
    return imageView
}()

let scoreLayer = CAShapeLayer()
let trackLayer = CAShapeLayer()

let percentageLabel: UILabel = {
    let label = UILabel()
    label.text = ""
    label.textAlignment = .center
    label.font = TextStyle.systemFont(ofSize: 50.0)
    return label
}()

// This one is the one should have status bar at center.
private let resultView: UIView = {
    let view = UIView()
    view.backgroundColor = .purple
    return view
}()

override func viewDidLoad() {
    super.viewDidLoad()
    prepareUI()
    configureShapeLayer()
}

private func prepareUI() {

    resultView.addSubviews(views: percentageLabel)

    scrollView.addSubviews(views: iconImageView,
                           resultView)

    view.addSubviews(views: scrollView)
    setupLayout()
}

private func setupLayout() {
    scrollView.fillSuperview()
    iconImageView.anchor(top: scrollView.topAnchor,
                         padding: .init(topPadding: 26.0))
    iconImageView.widthAnchor.constraint(equalTo: scrollView.widthAnchor, multiplier: 0.31).isActive = true
    iconImageView.heightAnchor.constraint(equalTo: iconImageView.widthAnchor, multiplier: 0.67).isActive = true
    iconImageView.anchorCenterXToSuperview()

    //percentageLabel.frame = CGRect(x: 0, y: 0, width: 105, height: 60)
    //percentageLabel.center = resultView.center

    percentageLabel.anchorCenterXToSuperview()
    percentageLabel.anchorCenterYToSuperview()

    let resultViewTopConstraintRatio: CGFloat = 0.104
    resultView.anchor(top: iconImageView.bottomAnchor,
                      padding: .init(topPadding: (view.frame.height * resultViewTopConstraintRatio)))

    resultView.widthAnchor.constraint(equalTo: scrollView.widthAnchor, multiplier: 0.533).isActive = true
    resultView.heightAnchor.constraint(equalTo: resultView.widthAnchor, multiplier: 1.0).isActive = true
    resultView.anchorCenterXToSuperview()

    configureShapeLayer()
}

private func configureShapeLayer() {
    let endAngle = ((2 * percentage) * CGFloat.pi) - CGFloat.pi / 2

    let center = resultView.center // should give resultView's center but gives 0

    // Track Layer Part
    let trackPath = UIBezierPath(arcCenter: center, radius: 50, startAngle: -CGFloat.pi / 2, endAngle: 2 * CGFloat.pi, clockwise: true)

    trackLayer.path = trackPath.cgPath
    trackLayer.strokeColor = UIColor.lightGray.cgColor // to make different
    trackLayer.lineWidth = 10
    trackLayer.fillColor = UIColor.clear.cgColor
    trackLayer.lineCap = .round

    resultView.layer.addSublayer(trackLayer)

    // Score Fill Part
    let scorePath = UIBezierPath(arcCenter: center, radius: 50, startAngle: -CGFloat.pi / 2, endAngle: endAngle, clockwise: true)

    scoreLayer.path = scorePath.cgPath
    scoreLayer.strokeColor = UIColor.red.cgColor
    scoreLayer.lineWidth = 10
    scoreLayer.fillColor = UIColor.clear.cgColor
    scoreLayer.lineCap = .round
    scoreLayer.strokeEnd = 0

    resultView.layer.addSublayer(scoreLayer)

}
}
emrepun
  • 2,496
  • 2
  • 15
  • 33
  • 2
    You are saying that the whole view is created programmatically.. why dont you rather share your code instead of your assumptions about it? – Milan Nosáľ Dec 06 '18 at 16:37
  • You are right, but there are also redundant code for the purpose of question. I will try to refactor for the question and add my code by editing. @MilanNosáľ – emrepun Dec 06 '18 at 16:40
  • I have added my code in the question now @MilanNosáľ – emrepun Dec 06 '18 at 16:47
  • I just quickly skimmed the code - it seems that you are using autolyaout on `percentageLabel` and in the same time you are trying to set its frame directly. if that is the case, the frame will be always computed byt the autolayout, so any direct frame setting would be non-effective – Milan Nosáľ Dec 06 '18 at 16:51
  • Since you need to access some view frames info in your initialization, you need to move that initialization (or a part of it ) to viewWillAppear where all view are ready to be shown. in ViewdidLoad views are only loaded to memory so you can not get frames attributes. – Idali Dec 06 '18 at 17:05
  • I moved configureShapeLayer() method to viewWillAppear but its still the same :/ @Idali – emrepun Dec 06 '18 at 17:12
  • @MilanNosáľ Actually I dont have a problem with percentageLabel, my problem is with this line in configureShapeLayer() method: let center = resultView.center // should give resultView's center but gives 0 Since it cannot get resultView's center correctly, it acts like the center is at 0,0 nothing is set. – emrepun Dec 06 '18 at 17:14
  • try to do it in `viewDidLayoutSubviews` – Milan Nosáľ Dec 06 '18 at 17:23
  • That also doesn't fix that, when I call it in viewDidLayoutSubviews, the status bar shape is shifted further away down from resultView, and it seems like viewDidLayoutSubviews repeatedly and the status bar si just frozen. – emrepun Dec 06 '18 at 17:27
  • Why people don't use storyboard, that super easy – canister_exister Dec 06 '18 at 17:52
  • @canister_exister I usually do, but for this specific project we decided not to use. Just for experience. – emrepun Dec 06 '18 at 18:01
  • viewDidAppaer don't work too? – canister_exister Dec 06 '18 at 18:05

2 Answers2

4

You will be much better off creating a custom view. That will allow you to "automatically" update your bezier paths when the view size changes.

It also allows you to keep your drawing code away from your controller code.

Here is a simple example. It adds a button above the "resultView" - each time it's tapped it will increment the percentage by 5 (percentage starts at 5 for demonstration):

//
//  PCTViewController.swift
//
//  Created by Don Mag on 12/6/18.
//

import UIKit

class MyResultView: UIView {

    let scoreLayer = CAShapeLayer()
    let trackLayer = CAShapeLayer()

    var percentage = CGFloat(0.0) {
        didSet {
            self.percentageLabel.text = "\(Int(percentage * 100))%"
            self.setNeedsLayout()
        }
    }

    let percentageLabel: UILabel = {
        let label = UILabel()
        label.translatesAutoresizingMaskIntoConstraints = false
        label.text = ""
        label.textAlignment = .center
        label.font = UIFont.systemFont(ofSize: 40.0)
        return label
    }()

    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        commonInit()
    }

    func commonInit() -> Void {

        layer.addSublayer(trackLayer)
        layer.addSublayer(scoreLayer)

        addSubview(percentageLabel)

        NSLayoutConstraint.activate([
            percentageLabel.centerXAnchor.constraint(equalTo: centerXAnchor),
            percentageLabel.centerYAnchor.constraint(equalTo: centerYAnchor)
            ])

    }

    override func layoutSubviews() {
        super.layoutSubviews()

        let endAngle = ((2 * percentage) * CGFloat.pi) - CGFloat.pi / 2

        trackLayer.frame = self.bounds
        scoreLayer.frame = self.bounds

        let centerPoint = CGPoint(x: self.bounds.width / 2.0, y: self.bounds.height / 2.0)

        // Track Layer Part
        let trackPath = UIBezierPath(arcCenter: centerPoint, radius: 50, startAngle: -CGFloat.pi / 2, endAngle: 2 * CGFloat.pi, clockwise: true)

        trackLayer.path = trackPath.cgPath
        trackLayer.strokeColor = UIColor.lightGray.cgColor // to make different
        trackLayer.lineWidth = 10
        trackLayer.fillColor = UIColor.clear.cgColor
        // pre-Swift 4.2
        trackLayer.lineCap = kCALineCapRound
//      trackLayer.lineCap = .round

        // Score Fill Part
        let scorePath = UIBezierPath(arcCenter: centerPoint, radius: 50, startAngle: -CGFloat.pi / 2, endAngle: endAngle, clockwise: true)

        scoreLayer.path = scorePath.cgPath
        scoreLayer.strokeColor = UIColor.red.cgColor
        scoreLayer.lineWidth = 10
        scoreLayer.fillColor = UIColor.clear.cgColor
        // pre-Swift 4.2
        scoreLayer.lineCap = kCALineCapRound
//      scoreLayer.lineCap = .round

    }

}

class PCTViewController: UIViewController {

    let resultView: MyResultView = {
        let v = MyResultView()
        v.translatesAutoresizingMaskIntoConstraints = false
        v.backgroundColor = .purple
        return v
    }()

    let btn: UIButton = {
        let b = UIButton()
        b.translatesAutoresizingMaskIntoConstraints = false
        b.setTitle("Add 5 percent", for: .normal)
        b.backgroundColor = .blue
        return b
    }()

    var pct = 5

    @objc func incrementPercent(_ sender: Any) {
        pct += 5
        resultView.percentage = CGFloat(pct) / 100.0
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        view.addSubview(btn)
        view.addSubview(resultView)

        NSLayoutConstraint.activate([

            btn.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20.0),
            btn.centerXAnchor.constraint(equalTo: view.centerXAnchor),

            resultView.widthAnchor.constraint(equalToConstant: 300.0),
            resultView.heightAnchor.constraint(equalTo: resultView.widthAnchor),

            resultView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            resultView.centerYAnchor.constraint(equalTo: view.centerYAnchor),

            ])

        btn.addTarget(self, action: #selector(incrementPercent), for: .touchUpInside)

        resultView.percentage = CGFloat(pct) / 100.0

    }

}

Result:

enter image description here

DonMag
  • 69,424
  • 5
  • 50
  • 86
  • Looks nice, I will try to implement tomorrow and will let you know, thanks a lot! – emrepun Dec 06 '18 at 22:43
  • 1
    As another advantage, with just a couple edits to the code you can make this an `@IBDesignable` object... so you can place it in a View in a Storyboard and see how it looks during design-time. – DonMag Dec 07 '18 at 13:28
  • Yeah that would be perfect to use it later in projects with storyboard too. Thank you very much again :) – emrepun Dec 07 '18 at 13:33
0

This is not the ideal fix, but try calling configureShapeLayer() func on main thread, like this:

DispatchQueue.main.async {
    configureShapeLayer()
}

I had problem like that once and was something like that.

Pincha
  • 93
  • 1
  • 10