Search code examples
iosswiftuikitconstraints

Set Different Constraint in StackView


Q. How to make different constraint each views, in stackview, UIKit? enter image description here I want to make screen like captured one.

In below code, the result is ok. but I have errors Unable to simultaneously satisfy constraints...

In Before Q&A I've learnt StackView. but that screen has all same constraint. like wrote before, I set constraint for example titleLabel.leadingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.trailingAnchor, constant: 20)...

So, I've Tried the why the failure https://www.wtfautolayout.com/, the answer was set 0 constraints each views. and now I wrote this question. Thanks for reading.

import UIKit

class DetailViewController: UIViewController {
    
    @IBOutlet var centerNavtigationItems: UINavigationItem!

    private let titleLabel: UILabel = {
        let titleLabel = UILabel()
        titleLabel.numberOfLines = 2
        titleLabel.textAlignment = .left
        titleLabel.font = UIFont(name: "Pretendard-SemiBold", size: 24)
        titleLabel.backgroundColor = .yellow
        return titleLabel
    }()
    
    private let dateLabel: UILabel = {
        let view = UILabel()
        view.textAlignment = .right
        view.font = UIFont(name: "Pretendard-Light", size: 16)
        view.textColor = .gray
        return view
    }()
    
    private let scrollView: UIScrollView = {
        let view = UIScrollView()
        return view
    }()
    
    private let imageContainer: UIImageView = {
        let ic = UIImageView()
        return ic
    }()
    
    private let pageController: UIPageControl = {
        let controller = UIPageControl()
        controller.pageIndicatorTintColor = .gray
        controller.currentPageIndicatorTintColor = .darkGray
        return controller
    }()
    
    private let contentTextView: UILabel = {
        let content = UILabel()
        content.numberOfLines = 0
        content.font = UIFont(name: "Pretendard-Regular", size: 18)
        return content
    }()
    
    var fileName: String!
    var id: String!
    var entryNumber: Int!
    var dataSize: Int!
    var entry: Entry!
    var imageNames: [String] = []
    var imageIndex: Int = 0
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        
        // Get contents id and parse file name
        parsingFileName(id: id)
        
        let dateText = String(entry.year) + "." + String(entry.month) + "." + String(entry.day)
        
        centerNavtigationItems.title = dateText
        titleLabel.text = entry.title
        dateLabel.text = dateText
        
        // Add Image to an Array
        // If an image doesn't exist, then set empty array, and make hide UIImageView ... (1)
        if entry.isImage == true {
            
            for e in entry.imageName {
                imageNames.append(String(e + ".jpeg"))
            }
            
            pageController.numberOfPages = imageNames.count
            pageController.currentPage = 0

            print("image name in Array: \(imageNames)")
            imageContainer.image = UIImage(named: imageNames[0])
            
            pageController.addTarget(self, action: #selector(action(sender: )), for: .touchUpInside)
        }
    
        // Set Swipe Gesture for the image container
        let swipeRight = UISwipeGestureRecognizer(target: self, action: #selector(DetailViewController.reponderToSwipeGesture(_:)))
        swipeRight.direction = UISwipeGestureRecognizer.Direction.right
        self.imageContainer.addGestureRecognizer(swipeRight)
        
        let swipeLeft = UISwipeGestureRecognizer(target: self, action: #selector(DetailViewController.reponderToSwipeGesture(_:)))
        swipeLeft.direction = UISwipeGestureRecognizer.Direction.left
        self.imageContainer.addGestureRecognizer(swipeLeft)
        
        // Set textView Contents
        contentTextView.text = entry.article
        
        // stackView Setting
        let stackView = UIStackView()
        stackView.axis = .vertical
        stackView.spacing = 8
        
        // Arrange subviews to Stack View
        [titleLabel, dateLabel, imageContainer, contentTextView].forEach { v in
            v.translatesAutoresizingMaskIntoConstraints = false
            stackView.addArrangedSubview(v)
        }
        
        // If image doesn't exist (1) Set the image container hidden
        if entry.isImage == true {
            imageContainer.heightAnchor.constraint(equalToConstant: self.view.frame.width).isActive = true
        } else {
            imageContainer.isHidden = true
        }
        
        // Set stakcView and scrollView
        stackView.translatesAutoresizingMaskIntoConstraints = false
        scrollView.translatesAutoresizingMaskIntoConstraints = false
        scrollView.addSubview(stackView)
        view.addSubview(scrollView)
        
        // Set constraint
        let g = view.safeAreaLayoutGuide
        let cg = scrollView.contentLayoutGuide
        let fg = scrollView.frameLayoutGuide
        
        NSLayoutConstraint.activate([
            scrollView.topAnchor.constraint(equalTo: g.topAnchor, constant: 0),
            scrollView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0),
            scrollView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -0),
            scrollView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -0),
            
            stackView.topAnchor.constraint(equalTo: cg.topAnchor, constant: 0.0),
            stackView.leadingAnchor.constraint(equalTo: cg.leadingAnchor, constant: 0.0),
            stackView.trailingAnchor.constraint(equalTo: cg.trailingAnchor, constant: -0.0),
            stackView.bottomAnchor.constraint(equalTo: cg.bottomAnchor, constant: -0.0),
            stackView.widthAnchor.constraint(equalTo: fg.widthAnchor, constant: -0),
            
            // set detail components constraint
            titleLabel.trailingAnchor.constraint(equalTo: cg.trailingAnchor, constant: -20),
            titleLabel.leadingAnchor.constraint(equalTo: cg.leadingAnchor, constant: 20),

            dateLabel.trailingAnchor.constraint(equalTo: cg.trailingAnchor, constant: -20),
            dateLabel.leadingAnchor.constraint(equalTo: cg.leadingAnchor, constant: 20),
            
            imageContainer.leadingAnchor.constraint(equalTo: cg.leadingAnchor, constant: 0),
            imageContainer.trailingAnchor.constraint(equalTo: cg.trailingAnchor, constant: 0),
            
            contentTextView.leadingAnchor.constraint(equalTo: cg.leadingAnchor, constant: 20),
            contentTextView.trailingAnchor.constraint(equalTo: cg.trailingAnchor, constant: -20),
        ])
    }
    
}

Errors

Unknown class detail in Interface Builder file.
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:0x6000021406e0 UILabel:0x103c1f3a0.leading == _UIScrollViewLayoutGuide:0x600003b0ea00'UIScrollView-contentLayoutGuide'.leading + 20   (active)>",
    "<NSLayoutConstraint:0x600002140050 UIImageView:0x103c183d0.leading == _UIScrollViewLayoutGuide:0x600003b0ea00'UIScrollView-contentLayoutGuide'.leading   (active)>",
    "<NSLayoutConstraint:0x60000213b2f0 'UISV-alignment' UILabel:0x103c1f3a0.leading == UIImageView:0x103c183d0.leading   (active)>"
)

Will attempt to recover by breaking constraint 
<NSLayoutConstraint:0x60000213b2f0 'UISV-alignment' UILabel:0x103c1f3a0.leading == UIImageView:0x103c183d0.leading   (active)>

Make a symbolic breakpoint at UIViewAlertForUnsatisfiableConstraints to catch this in the debugger.
The methods in the UIConstraintBasedLayoutDebugging category on UIView listed in <UIKitCore/UIView.h> may also be helpful.
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:0x600002142800 UILabel:0x103c1f3a0.trailing == _UIScrollViewLayoutGuide:0x600003b0ea00'UIScrollView-contentLayoutGuide'.trailing - 20   (active)>",
    "<NSLayoutConstraint:0x600002140820 UIImageView:0x103c183d0.trailing == _UIScrollViewLayoutGuide:0x600003b0ea00'UIScrollView-contentLayoutGuide'.trailing   (active)>",
    "<NSLayoutConstraint:0x60000213b250 'UISV-alignment' UILabel:0x103c1f3a0.trailing == UIImageView:0x103c183d0.trailing   (active)>"
)

Will attempt to recover by breaking constraint 
<NSLayoutConstraint:0x60000213b250 'UISV-alignment' UILabel:0x103c1f3a0.trailing == UIImageView:0x103c183d0.trailing   (active)>

Make a symbolic breakpoint at UIViewAlertForUnsatisfiableConstraints to catch this in the debugger.
The methods in the UIConstraintBasedLayoutDebugging category on UIView listed in <UIKitCore/UIView.h> may also be helpful.
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:0x600002140aa0 UIStackView:0x103c27b90.leading == _UIScrollViewLayoutGuide:0x600003b0ea00'UIScrollView-contentLayoutGuide'.leading   (active)>",
    "<NSLayoutConstraint:0x6000021406e0 UILabel:0x103c1f3a0.leading == _UIScrollViewLayoutGuide:0x600003b0ea00'UIScrollView-contentLayoutGuide'.leading + 20   (active)>",
    "<NSLayoutConstraint:0x60000213b1b0 'UISV-canvas-connection' UIStackView:0x103c27b90.leading == UILabel:0x103c1f3a0.leading   (active)>"
)

Will attempt to recover by breaking constraint 
<NSLayoutConstraint:0x60000213b1b0 'UISV-canvas-connection' UIStackView:0x103c27b90.leading == UILabel:0x103c1f3a0.leading   (active)>

Make a symbolic breakpoint at UIViewAlertForUnsatisfiableConstraints to catch this in the debugger.
The methods in the UIConstraintBasedLayoutDebugging category on UIView listed in <UIKitCore/UIView.h> may also be helpful.
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:0x6000021414a0 UIStackView:0x103c27b90.trailing == _UIScrollViewLayoutGuide:0x600003b0ea00'UIScrollView-contentLayoutGuide'.trailing   (active)>",
    "<NSLayoutConstraint:0x600002142800 UILabel:0x103c1f3a0.trailing == _UIScrollViewLayoutGuide:0x600003b0ea00'UIScrollView-contentLayoutGuide'.trailing - 20   (active)>",
    "<NSLayoutConstraint:0x60000213b200 'UISV-canvas-connection' H:[UILabel:0x103c1f3a0]-(0)-|   (active, names: '|':UIStackView:0x103c27b90 )>"
)

Will attempt to recover by breaking constraint 
<NSLayoutConstraint:0x60000213b200 'UISV-canvas-connection' H:[UILabel:0x103c1f3a0]-(0)-|   (active, names: '|':UIStackView:0x103c27b90 )>

Make a symbolic breakpoint at UIViewAlertForUnsatisfiableConstraints to catch this in the debugger.
The methods in the UIConstraintBasedLayoutDebugging category on UIView listed in <UIKitCore/UIView.h> may also be helpful.

Solution

  • I think your main issues are the constraints you are setting on views in the stack view and your failure to fully configure the stack view. Since you want the labels to be indented some from the edges of the stack view and you want the image view to fill the width of the stack view, the following changes should give the desired results:

    1. Replace the following constraints:

      titleLabel.trailingAnchor.constraint(equalTo: cg.trailingAnchor, constant: -20),
      titleLabel.leadingAnchor.constraint(equalTo: cg.leadingAnchor, constant: 20),
      dateLabel.trailingAnchor.constraint(equalTo: cg.trailingAnchor, constant: -20),
      dateLabel.leadingAnchor.constraint(equalTo: cg.leadingAnchor, constant: 20),
      imageContainer.leadingAnchor.constraint(equalTo: cg.leadingAnchor, constant: 0),
      imageContainer.trailingAnchor.constraint(equalTo: cg.trailingAnchor, constant: 0),
      contentTextView.leadingAnchor.constraint(equalTo: cg.leadingAnchor, constant: 20),
      contentTextView.trailingAnchor.constraint(equalTo: cg.trailingAnchor, constant: -20),
      

      with:

      titleLabel.widthAnchor.constraint(equalTo: stackView.widthAnchor, constant: -40),
      dateLabel.widthAnchor.constraint(equalTo: stackView.widthAnchor, constant: -40),
      imageContainer.widthAnchor.constraint(equalTo: stackView.widthAnchor, constant: 0),
      contentTextView.widthAnchor.constraint(equalTo: stackView.widthAnchor, constant: -40),
      
    2. Fully setup the stack view by setting the distribution and alignment properties. You probably want to add the following two lines after setting the axis:

      stackView.distribution = .equalSpacing // Use each view's intrinsic height as-is when possible
      stackView.alignment = .center // Center each view in the stack view
      
    3. You should change the height constraint on the image view. Replace:

      imageContainer.heightAnchor.constraint(equalToConstant: self.view.frame.width).isActive = true
      

      with:

      imageContainer.heightAnchor.constraint(equalTo: imageContainer.widthAnchor).isActive = true
      

      This will make the image view's height the same as its width which is what I think you are trying to do (make it square).