robb / Cartography

A declarative Auto Layout DSL for Swift :iphone::triangular_ruler:
Other
7.35k stars 525 forks source link

Implicitly update layout after "constrain" call? #258

Closed macabeus closed 7 years ago

macabeus commented 7 years ago

I had the follow difficulty:

constrain(self.viewCircle, self.labelResult, self.viewDashed) { circle, label, dashed in
    // ...

    dashed.top == circle.bottom
    dashed.height == self.frame.height - viewCircle.frame.height // change the viewDashed.frame.height
    dashed.width == 1
}

// some code that use viewDashed.frame

But, the code after constrain didn't work correctly, and I din't understand why.

Then, I debug the code, and, different that I thought, the value of viewCircle.frame don't change after constrain, although the layout is changed. Then, I needed make some changes:

constrain(self.viewCircle, self.labelResult, self.viewDashed) { circle, label, dashed in
    // ...

    dashed.top == circle.bottom
    dashed.height == self.frame.height - viewCircle.frame.height // change the viewDashed.frame.height
    dashed.width == 1
}

// viewDashed.frame.height -> 169.0

viewDashed.setNeedsLayout()
viewDashed.layoutIfNeeded()

// viewDashed.frame.height -> 118.0

// some code that use viewDashed.frame

Exists any way better? Maybe, is a good idea implicitly to call setNeedsLayout and layoutIfNeeded for each value used in constrain?

vfn commented 7 years ago

@brunomacabeusbr can you please post the whole code for constrain(self.viewCircle, self.labelResult, self.viewDashed) { ...?

macabeus commented 7 years ago

@vfn Of course

constrain(self.viewCircle, self.labelResult, self.viewDashed) { circle, label, dashed in
    circle.top == circle.superview!.top
    circle.centerX == circle.superview!.centerX
    circle.height == self.viewCircle.frame.width
    circle.width == self.viewCircle.frame.width

    label.center == circle.center

    dashed.top == circle.bottom
    dashed.centerX == dashed.superview!.centerX
    dashed.height == self.frame.height - viewCircle.frame.height
    dashed.width == 1
}

My UI is this:

vfn commented 7 years ago

@brunomacabeusbr 2 things:

  1. You should not reference views directly inside the constrain block.
  2. If dashed view goes from the bottom of the circle to the bottom of the superview, just set the constrain that way
constrain(self.viewCircle, self.labelResult, self.viewDashed) { circle, label, dashed in
    circle.top == circle.superview!.top
    circle.centerX == circle.superview!.centerX
    circle.height == circle.superview!.width
    circle.width == circle.superview!.width

    label.center == circle.center

    dashed.top == circle.bottom
    dashed.centerX == dashed.superview!.centerX
    dashed.bottom == dashed.superview!.bottom
    dashed.width == 1
}

Abraço!

macabeus commented 7 years ago

@vfn I wrote self.viewCircle.frame.width instead of circle.superview!.width because I want get the value setted in storyboard (well... this value is "constant"... is better I set literal number where... thank you). But circle.superview!.width not work for my situation because my circle should not fill the whole width in superview.

And, I tested dashed.bottom == dashed.superview!.bottom instead of dashed.height == self.frame.height - viewCircle.frame.height, but...

... not work correctly (image in left side). With dashed.height == self.frame.height - viewCircle.frame.height work (image in right side).

I know that is weird use self.frame in constrain closure, but, I don't know a better way.

I'm working with Cartography inside one of the cell of CollectionView. Big example:

image

Obrigado =]

vfn commented 7 years ago
let padding: CGFloat = 20
constrain(self.viewCircle, self.labelResult, self.viewDashed, self.greenCircle) { circle, label, dashed, greenCircle in
    circle.top == circle.superview!.top
    circle.centerX == circle.superview!.centerX
    circle.width == circle.superview!.width - 2 * padding // Use some sort of padding to make it smaller than the super view
    circle.height == circle.width

    label.center == circle.center

    dashed.top == circle.bottom
    dashed.centerX == dashed.superview!.centerX
    dashed.bottom == greenCircle.top // This will make the line stop just before the green circle
    dashed.width == 1
}
vfn commented 7 years ago

@brunomacabeusbr looking at the example where you have multiple circles, I'd suggest you to rethink your view hierarchy. Break it in smaller chunks, where each magenta-ish circle, dashed line, and text are a self contained view.

screen shot 2017-05-03 at 7 41 17 pm

Then you create constraints placing one view after the other.

By doing that, you can make the dashed line go form the circle to the bottom of it's own container view

macabeus commented 7 years ago

@vfn Wait, please. I'm creating a separate pod and I will create a new repository coming soon.

macabeus commented 7 years ago

@vfn Finish: https://github.com/brunomacabeusbr/InputStepByStep You can run the Example and see the code in InputStepByStep.

Thank you very much for your attention.

vfn commented 7 years ago

@brunomacabeusbr the issue there has nothing to do with Cartography. If you change the background color of your viewDashed to something that's not .clear you'll see that the height is correct when you make it go to the bottom of its superview.

The issue you have there is due to the fact that the dashed sublayer does not resize with the main layer.

In the image below the dashed view, in red, has the correct height, but the dashed layer does not.

screen shot 2017-05-03 at 9 46 45 pm

Have a look at http://stackoverflow.com/questions/29111099/calayer-not-resizing-with-autolayout

macabeus commented 7 years ago

@vfn Interesting. I don't know this expected behavior about UIKit. I thought that CALayer was resizing automatically when the constrains was update.

One question:

constrain(self.viewCircle, self.labelResult, self.viewDashed) { circle, label, dashed in
    ...
    dashed.height == self.frame.height - viewCircle.frame.height
    //dashed.bottom == dashed.superview!.bottom
}

viewDashed.setNeedsLayout()
viewDashed.layoutIfNeeded()

viewDashed.addDashedBorder()

This code work. The function addDashedBorder draw the dash correctly. But, when I use dashed.bottom == dashed.superview!.bottom instead of dashed.height == self.frame.height - viewCircle.frame.height, not work correctly. It's equal as your photo.

Do you know why I need set one height value? And, do you have any idea about one better way, without setNeedsLayout and layoutIfNeeded?

Thank you very much =]

vfn commented 7 years ago

@brunomacabeusbr your code works because you force viewDashed to layout before it should and then you add the sublayer, by calling viewDashed.addDashedBorder(). At that point viewDashed gets the correct height.

For now, your code works, but it's not future proof and you'll find issues as soon as you need to change the size of dashed view change, and the dashed layer will be stuck with the initial height.

Another issue with your code, is that your cell is not reusable. Any new attempt to reuse it will add another dashed layer to it.

I'd suggest you to create a UIView subclass, and manage the dashed layer inside the UIView subclass.

Make your viewDashed to use the following class

final class DashedView: UIView {

    override class var layerClass: AnyClass {
        return CAShapeLayer.self
    }

    override init(frame: CGRect = .zero) {
        super.init(frame: frame)
    }

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

    override func layoutSubviews() {
        super.layoutSubviews()

        self.setupDash()
    }

    private func setupDash() {
        self.alpha = 0.2
        self.backgroundColor = .clear
        self.clipsToBounds = true

        guard let shapeLayer = self.layer as? CAShapeLayer else {
            preconditionFailure()
        }
        let frameSize = self.frame.size
        let shapeRect = CGRect(x: 0, y: 0, width: frameSize.width, height: frameSize.height)

        shapeLayer.fillColor = UIColor.clear.cgColor
        shapeLayer.strokeColor = UIColor.white.cgColor
        shapeLayer.lineWidth = 1
        shapeLayer.lineJoin = kCALineJoinRound
        shapeLayer.lineDashPattern = [1, 1]
        shapeLayer.path = UIBezierPath(roundedRect: shapeRect, cornerRadius: 1).cgPath
    }

}

and then, make the startCell() look like

    func startCell() {
        viewCircle.asCircle()

        constrain(self.viewCircle, self.labelResult, self.viewDashed) { circle, label, dashed in
            circle.top == circle.superview!.top
            circle.centerX == circle.superview!.centerX
            circle.height == 32
            circle.width == 32

            label.center == circle.center

            dashed.top == circle.bottom
            dashed.centerX == dashed.superview!.centerX
            dashed.bottom == dashed.superview!.bottom ~ UILayoutPriorityRequired
            dashed.width == 1
        }
    }
macabeus commented 7 years ago

@vfn Many, many, many thank you!! Now, it's working elegantly.

I learned a lot because of your help =]