1

I have my own custom view, ProgressBar, which I create it programatically:

let pb = ProgressBar(frame: CGRect(x: 0, y: 0, width: 200, height: 16))

I create it using a width of 200, but then I add this to a horizontal stack view. Because of this, the view will grow depending on the screen size, for example on iPhone X will grow to 250.

Inside this custom view I am trying to use the width, but it's always 200 and it is causing all kinds of issues.

What I really need is the value that the view has inside stack view, in my case 250.

Is there a way to get that value, inside my custom view ?
Or what are my alternatives ?

edit - added some code:

class CustomTableViewCell: UITableViewCell {
  func configure() {
     let progressBar: ProgressBar = {
         let pb = ProgressBar(frame: CGRect(x: 0, y: 0, width: 200, height: 16))
     }()
  }
}

And this code is called from the view controller:

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        if indexPath.row == 0 {
            let cell = tableView.dequeueReusableCell(withIdentifier: "cell1") as! CustomTableViewCell
            cell.configure()
            return cell
}

edit 2 - custom view code:

class ProgressBar: UIView {

    var progressView: UIView = {
        let v = UIView()
        v.backgroundColor = .white
        return v
    }()

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

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

    func configure() {
        backgroundColor = UIColor(white: 1.0, alpha: 0.1)
        layer.cornerRadius = 8.0
        clipsToBounds = true

        progressView.frame = bounds
        progressView.frame.size.width = 0.0
        addSubview(progressView)
    }

    func animate(_ pct: CGFloat) -> Void {
        UIView.animate(withDuration: 1.0, animations: {
            self.progressView.frame.size.width = self.bounds.size.width * pct
        })
    }
}

edit3 - calling animate

    let progressBar: ProgressBar = {
        let pb = ProgressBar(frame: CGRect(x: 0, y: 0, width: 200, height: 16))
        pb.translatesAutoresizingMaskIntoConstraints = false

        if data.count >= i {
            if let session = data[i - 1].session {
                let values = Array(session).sorted(by: { $0.date.compare($1.date) == .orderedDescending })
                var sum = 0.0
                for value in values {
                    sum += Double(value.score)
                }
                pb.animate(CGFloat(sum / 40)) // returns a value between 0 and 1
            }
        }

        return pb;
    }()

edit 4:

enter image description here

edit 5 - added debug constraints:

Printing description of $16:
<UIView: 0x107d6a100; frame = (0 0; 200 16); layer = <CALayer: 0x1d003f3c0>>
Printing description of $17:
<StepUp.ProgressBar: 0x107d6c8e0; frame = (54.3333 0; 250 16); clipsToBounds = YES; layer = <CALayer: 0x1d0220880>>
Adrian
  • 19,440
  • 34
  • 112
  • 219
  • https://stackoverflow.com/questions/42890174/get-the-frame-of-a-uistackview-subviews/42893666#42893666 – agibson007 Jun 03 '18 at 14:56
  • Let me know if this does not work but I think this is a duplicate ^ – agibson007 Jun 03 '18 at 14:57
  • @agibson007: ok, but I need the real value inside my custom view. How do I get it there ? Also all my code is happening inside an custom UITableViewCell. – Adrian Jun 03 '18 at 14:59
  • You need to update the frame in layout subviews. Can you show me where the custom view is – agibson007 Jun 03 '18 at 15:02
  • @agibson007: I updated my question with some code. Let me know if you need more info – Adrian Jun 03 '18 at 15:06
  • @matt: I added the code for my custom view . The issue is inside the animate method. The view thinks it is 200 width, so when I animate the progress bar to max, it goes to 200, but the view is 250, so inside the UI is looking wrong. – Adrian Jun 03 '18 at 15:18
  • @matt: ok, I added the code where animate is called. I get some data from CoreData and trying to animate the progress bar based on the value, which is between 0 and 1. – Adrian Jun 03 '18 at 15:44
  • @matt: the issue that the width is 250 actually, not 200. That is because the stack view has stretched the view. So when I run the app, and I give a value of 1 to animate, it should animate all the view width, but it doesn't. It animates only to 200. My question is, how is my progress bar supposed to know the stretched value of the view ? – Adrian Jun 03 '18 at 15:50
  • @matt: I attached a screenshot at how it looks. This goes to 200. But it is not completed. I also debug the constraints, when running the app, and the width of the progress bar is 250. – Adrian Jun 03 '18 at 15:54
  • @matt: I also added the constraints how it is in debug. The white part from the progress bar is indeed 200, but the entire view is 250. – Adrian Jun 03 '18 at 15:59
  • Why are you using `translatesAutoresizingMaskIntoConstraints`? Did you use auto-layout? – nayem Jun 03 '18 at 16:29
  • @nayem: yes I am using autolayout – Adrian Jun 03 '18 at 16:30
  • 1
    Well then providing the `width` and `height` is pointless. Either you use frame based layout or the constraint based layout. – nayem Jun 03 '18 at 16:32
  • @nayem: ok, but then how do I access the real value of my progress bar if I am just using auto layout ? I am creating all of this programatically. I tried to just create ProgressBar() without providing a CGRect, still doesn't work. – Adrian Jun 03 '18 at 16:34
  • Changing a view's size embedded in a `StackView` is painful. Think again if you really need this view to be inside a stack view. If not, pull this view out of the stack view and place outside. – nayem Jun 03 '18 at 16:51
  • Well there are like 10 views one under the other, so a stack view makes more easy. It will be a pain to not use a stack view. – Adrian Jun 03 '18 at 16:56
  • Well you could have your other view elements in the stack view but this particular view. This view needs to be able to change its size. But the internal constraints of the stack view (alignment, distribution) don't let it to change the size. For this to work, you will have to sacrifice the `fill` alignment/distribution. But that will cause problems for the layout of other views I think. Now you decide. – nayem Jun 03 '18 at 17:05
  • @nayem: well all the views are the same, they are 10 progress bars. So yeah, if I stick to stack view I should give up on Fill distribution\alignment. – Adrian Jun 03 '18 at 17:09

1 Answers1

2

As I've mentioned in the comment section that it's not a wise idea to have the progress view inside the stack view, this will require tedious steps to achieve the width change animation.

First of all you need to get rid of the explicitly setting the frame that is: don't specify width & height when you use Auto Layout. Use CGRectZero instead.

Then you need to configure your stack view (I'm assuming you use vertical stack view) as:

stackView.alignment = .leading

You can't use any of the fill types here. Because if you use fill, then all of the arranged sub views should have the whole widths of the stack view. As a result you won't be able to modify any of their widths for your animation to take place.

Now as soon as you provide your stack view .leading alignment your ordinary views (the views that don't have intrinsic content size) will be given the width of zero. So when you run your application you will see none of your views. Here you need to do the tedious work, giving each of them widths. One solution may be: you enumerate through the subviews of your stack view and give all the views equal width as of your stack view leaving the progress view to have zero widths

// update these when the sub views of the view are layout already
// otherwise stack view won't have it's own size yet 
stackView.subviews.forEach { (view) in
    if view == progressView {
        // save this constraint for use when animating
        progressViewWidthConstraint = view.widthAnchor.constraint(equalToConstant: 0)
        progressViewWidthConstraint.isActive = true
    } else {
        view.widthAnchor.constraint(equalToConstant: stackView.frame.size.width).isActive = true
    }
}

Now your animation should be working as:

view.layoutIfNeeded()
progressViewWidthConstraint.constant = stackView.frame.size.width
UIView.animate(withDuration: 2) {
    self.view.layoutIfNeeded()
}

Here is the result: Animate UIView as progress view inside a UIStackView

I've made a demo if you are stuck anywhere in the process. Cheers

nayem
  • 7,285
  • 1
  • 33
  • 51