Adding a subviews than can change each constraint

58 Views Asked by At

What I'm trying to do in my project is to transfer images to the screen according to the location data coming from the firebase( for example x = 5 , y = 10, width = 30, height = 50). For example, let's say I have 10 images to display (maybe even 100 images). The first of these images should be at the top. Each incoming image must be below the previous one but image can be different in x and y axis. I tried to assign different constraint with a for loop to the image that I have defined only one. But my problem is I get the following constraint error. Can you help me how can I do this? The image that should be is as follows. The image here. My simulator shows only one view like this image.

Error:

[LayoutConstraints] Unable to simultaneously satisfy constraints.
    Probably at least one of the constraints in the following list is one you don't want. 
    Try this: 
        (1) look at each constraint and try to figure out which you don't expect; 
        (2) find the code that added the unwanted constraint or constraints and fix it. 
(
    "<NSLayoutConstraint:0x600000cd4dc0 UIView:0x12b304a70.height == 50   (active)>",
    "<NSLayoutConstraint:0x600000cd51d0 V:[UIView:0x12b304a70]-(60)-[UIView:0x12b304a70]   (active)>"
)

My code:

let View: UIView = {
    let view = UIView()
    view.backgroundColor = .red
    view.translatesAutoresizingMaskIntoConstraints = false
    return view
}()

var count = 1
var topAnchor: NSLayoutYAxisAnchor?
for views in 1...11{
    if count == 1{
        view.addSubview(View)
        View.anchor(top: view.safeAreaLayoutGuide.topAnchor, bottom: nil, leading: nil, trailing: nil, paddingTop: 10, paddingBottom: 0, paddingLeft: 0, paddingRight: 0, width: 50, height: 50)
        View.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
        topAnchor = View.bottomAnchor
        count += 1
    }
    else{ // if count
        view.addSubview(View)
        View.anchor(top: topAnchor, bottom: nil, leading: nil, trailing: nil, paddingTop: 60, paddingBottom: 0, paddingLeft: 0, paddingRight: 0, width: 30, height: 30)
        View.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
        topAnchor = View.topAnchor
    }
}

With this extension:

extension UIView{
    func anchor(top: NSLayoutYAxisAnchor?,
                bottom: NSLayoutYAxisAnchor?,
                leading: NSLayoutXAxisAnchor?,
                trailing: NSLayoutXAxisAnchor?,
                paddingTop: CGFloat,
                paddingBottom: CGFloat,
                paddingLeft: CGFloat,
                paddingRight: CGFloat,
                width: CGFloat,
                height: CGFloat
    ){
        if let top = top{
            self.topAnchor.constraint(equalTo: top, constant: paddingTop).isActive = true
        }
        if let bottom = bottom{
            self.bottomAnchor.constraint(equalTo: bottom, constant: paddingBottom).isActive = true
        }
        if let leading = leading{
            self.leadingAnchor.constraint(equalTo: leading, constant: paddingLeft).isActive = true
        }
        if let trailing = trailing{
            self.trailingAnchor.constraint(equalTo: trailing, constant: paddingRight).isActive = true
        }
        if width != 0{
            widthAnchor.constraint(equalToConstant: width).isActive = true
        }
        if height != 0{
            heightAnchor.constraint(equalToConstant: height).isActive = true
        }
    }
}
1

There are 1 best solutions below

0
Rob On

A few observations:

  1. As Geoff Hackworth pointed out, you are creating only one subview (one that was instantiated once with a closure). You really want to have a method that creates a new subview every time.

  2. In the else block, you are setting the top anchor of every subsequent view to be the top anchor of the preceding view. You presumably wanted to set the next view’s top anchor to be relative to the preceding view’s bottom anchor.

  3. As a minor detail, you are not setting the bottom anchor of the last view. Generally we want our constraints to be fully defined, so I would suggest adding a bottomAnchor for the last subview. This is especially true in self-sizing cells, that need an unambiguous set of vertical constraints.

So, perhaps:

func createView() -> UIView {
    let view = UIView()
    view.backgroundColor = .red
    view.translatesAutoresizingMaskIntoConstraints = false
    return view
}

func addSubviews() {
    var topAnchor: NSLayoutYAxisAnchor = view.safeAreaLayoutGuide.topAnchor
    var subview: UIView!
    for index in 1...11 {
        subview = createView()
        view.addSubview(subview)
        subview.activateAnchors(top: topAnchor, xCenter: view.centerXAnchor, paddingTop: 10, width: 50, height: 50)
        topAnchor = subview.bottomAnchor
    }
    view.activateAnchors(bottom: subview.bottomAnchor, paddingBottom: 10)
}

And, if you forgive me, I tweaked the UIView extension:

extension UIView{
    func activateAnchors(
        top: NSLayoutYAxisAnchor? = nil,
        bottom: NSLayoutYAxisAnchor? = nil,
        leading: NSLayoutXAxisAnchor? = nil,
        trailing: NSLayoutXAxisAnchor? = nil,
        xCenter: NSLayoutXAxisAnchor? = nil,
        yCenter: NSLayoutYAxisAnchor? = nil,
        paddingTop: CGFloat = 0,
        paddingBottom: CGFloat = 0,
        paddingLeft: CGFloat = 0,
        paddingRight: CGFloat = 0,
        width: CGFloat? = nil,
        height: CGFloat? = nil
    ) {
        if let top {
            topAnchor.constraint(equalTo: top, constant: paddingTop).isActive = true
        }
        if let bottom {
            bottomAnchor.constraint(equalTo: bottom, constant: paddingBottom).isActive = true
        }
        if let leading {
            leadingAnchor.constraint(equalTo: leading, constant: paddingLeft).isActive = true
        }
        if let trailing {
            trailingAnchor.constraint(equalTo: trailing, constant: paddingRight).isActive = true
        }
        if let xCenter {
            centerXAnchor.constraint(equalTo: xCenter).isActive = true
        }
        if let yCenter {
            centerYAnchor.constraint(equalTo: yCenter).isActive = true
        }
        if let width {
            widthAnchor.constraint(equalToConstant: width).isActive = true
        }
        if let height {
            heightAnchor.constraint(equalToConstant: height).isActive = true
        }
    }
}

In the above, I have also:

  1. Simplified the for loop so we do not need the if-else based upon the counter.

  2. I have given the UIView extension default values for its parameters, so you do not need to supply parameters of nil at the call point.


enter image description here


Needless to say, we often would consider a UIStackView:

func addSubviews() {
    let subviews = (1...11).map { _ in createView() } 

    let stackView = UIStackView(arrangedSubviews: subviews)
    stackView.translatesAutoresizingMaskIntoConstraints = false
    stackView.alignment = .center
    stackView.axis = .vertical
    stackView.spacing = 10
    view.addSubview(stackView)

    stackView.activateAnchors(top: view.topAnchor, bottom: view.bottomAnchor, xCenter: view.centerXAnchor, paddingTop: 10, paddingBottom: -10)
    subviews.forEach { $0.activateAnchors(width: 50, height: 50) }
}

That having been said, stack views within cells don’t always work perfectly, so you might have to resort to the prior technique.