Search code examples
iosswiftxibuistackview

addArrangedSubview is overlapping views


My problem - when I add arranged subviews to a stackview programmatically, they just pile up in the upper right-hand corner as shown below. Why aren't they stacking up? I've tried many, many things including using views that have an intrinsic size.

The stackview is called mainStack and comes from a xib. mainStack is a vertical stack with Alignment set to Fill and Distribution set to Fill Equally (see settings at the bottom of this question). It contains one UIView with a blue background. For the sake of this question I have added two views to mainStack using addArrangedSubviews. Here is what I'm getting:

enter image description here

And this is what I would expect:

enter image description here

This is the code for the xib:

class TaskSheet: UIView {

    @IBOutlet var contentView: UIView!
    @IBOutlet weak var mainStack: UIStackView!


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

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

    func setup() {
        let nib = UINib(nibName: "TaskSheet", bundle: nil)
        nib.instantiate(withOwner: self, options: nil)
        contentView.frame = bounds
        addSubview(contentView)
    }
}

And this is how I'm trying to add views to mainStack:

class PDFSheet: UIView {

    var taskSheet: TaskSheet!
    var sheetArray = [UIView]()

    func makeSheet() -> [UIView] {
        taskSheet = TaskSheet(frame: CGRect(x: 0, y: 0, width: 612, height: 792))

        let newView1 = UIView(frame: CGRect(x: 0, y: 0, width: 240, height: 128))
        newView1.heightAnchor.constraint(equalToConstant: 128).isActive = true
        newView1.widthAnchor.constraint(equalToConstant: 240).isActive = true
        newView1.backgroundColor = .green

        let newView2 = UIView(frame: CGRect(x: 0, y: 0, width: 120, height: 64))
        newView2.heightAnchor.constraint(equalToConstant: 64).isActive = true
        newView2.widthAnchor.constraint(equalToConstant: 120).isActive = true
        newView2.backgroundColor = .yellow

        taskSheet.mainStack.addArrangedSubview(newView1)
        taskSheet.mainStack.addArrangedSubview(newView2)
        sheetArray.append(taskSheet)
        return sheetArray
    }
}

And, here's the xib showing the stackview settings, just in case...

enter image description here


Solution

  • I think I understand what you're going for.

    Here's an example...

    xib layout (top label, vertical stack view, bottom label):

    enter image description here

    the stack view's properties:

    enter image description here

    Code for TaskSheet, PDFSheet and sample view controller:

    class TaskSheet: UIView {
    
        @IBOutlet var contentView: UIView!
        @IBOutlet var mainStack: UIStackView!
    
        override init(frame: CGRect) {
            super.init(frame: frame)
            setup()
        }
    
        required init?(coder aDecoder: NSCoder) {
            super.init(coder: aDecoder)
            setup()
        }
    
        func setup() {
            let nib = UINib(nibName: "TaskSheet", bundle: nil)
            nib.instantiate(withOwner: self, options: nil)
            addSubview(contentView)
            NSLayoutConstraint.activate([
    
                // constrain contentView on all 4 sides with 8-pts "padding"
                contentView.topAnchor.constraint(equalTo: topAnchor, constant: 8.0),
                contentView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -8.0),
                contentView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 8.0),
                contentView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -8.0),
    
            ])
        }
    }
    
    class PDFSheet: UIView {
    
        var taskSheet: TaskSheet!
        var sheetArray = [UIView]()
    
        override init(frame: CGRect) {
            super.init(frame: frame)
            _ = makeSheet()
        }
    
        required init?(coder: NSCoder) {
            super.init(coder: coder)
            _ = makeSheet()
        }
    
        func makeSheet() -> [UIView] {
    
            taskSheet = TaskSheet()
    
            let newView1 = UIView(frame: CGRect(x: 0, y: 0, width: 240, height: 128))
            newView1.heightAnchor.constraint(equalToConstant: 128).isActive = true
            newView1.widthAnchor.constraint(equalToConstant: 240).isActive = true
            newView1.backgroundColor = .green
    
            let newView2 = UIView(frame: CGRect(x: 0, y: 0, width: 120, height: 64))
            newView2.heightAnchor.constraint(equalToConstant: 64).isActive = true
            newView2.widthAnchor.constraint(equalToConstant: 120).isActive = true
            newView2.backgroundColor = .yellow
    
            taskSheet.mainStack.addArrangedSubview(newView1)
            taskSheet.mainStack.addArrangedSubview(newView2)
            sheetArray.append(taskSheet)
    
            addSubview(taskSheet)
    
            taskSheet.translatesAutoresizingMaskIntoConstraints = false
    
            NSLayoutConstraint.activate([
    
                // constrain taskSheet on all 4 sides
                taskSheet.topAnchor.constraint(equalTo: topAnchor, constant: 8.0),
                taskSheet.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -8.0),
                taskSheet.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 8.0),
                taskSheet.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -8.0),
    
            ])
    
            return sheetArray
        }
    }
    
    class TaskViewController: UIViewController {
    
        var theSheetView: PDFSheet!
    
        override func viewDidLoad() {
            super.viewDidLoad()
    
            theSheetView = PDFSheet()
            theSheetView.translatesAutoresizingMaskIntoConstraints = false
    
            view.addSubview(theSheetView)
    
            let g = view.safeAreaLayoutGuide
    
            NSLayoutConstraint.activate([
    
                // constrain the sheet view on all top, leading, trailing with 32-pts "padding"
                theSheetView.topAnchor.constraint(equalTo: g.topAnchor, constant: 32.0),
                theSheetView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 32.0),
                theSheetView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -32.0),
                // NO height or bottom constraint
            ])
    
        }
    
    }
    

    and here's the source of the xib file (to make it easy to check) Edit: whoops, pasted the wrong xml source -- fixed now:

    <?xml version="1.0" encoding="UTF-8"?>
    <document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="15505" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
        <device id="retina4_7" orientation="portrait" appearance="light"/>
        <dependencies>
            <deployment identifier="iOS"/>
            <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15510"/>
            <capability name="Safe area layout guides" minToolsVersion="9.0"/>
            <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
        </dependencies>
        <objects>
            <placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="TaskSheet" customModule="scratchy" customModuleProvider="target">
                <connections>
                    <outlet property="contentView" destination="TFh-sZ-4cx" id="zaP-M3-nAu"/>
                    <outlet property="mainStack" destination="oGz-Bu-nCT" id="oCb-IB-Q4i"/>
                </connections>
            </placeholder>
            <placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
            <view contentMode="scaleToFill" id="iN0-l3-epB">
                <rect key="frame" x="0.0" y="0.0" width="375" height="315"/>
                <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
                <subviews>
                    <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="TFh-sZ-4cx">
                        <rect key="frame" x="8" y="8" width="359" height="299"/>
                        <subviews>
                            <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Top Label" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="jZ3-yl-TaR">
                                <rect key="frame" x="0.0" y="0.0" width="359" height="21"/>
                                <color key="backgroundColor" red="0.92143100499999997" green="0.92145264149999995" blue="0.92144101860000005" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
                                <fontDescription key="fontDescription" type="system" pointSize="17"/>
                                <nil key="textColor"/>
                                <nil key="highlightedColor"/>
                            </label>
                            <stackView opaque="NO" contentMode="scaleToFill" axis="vertical" distribution="fillEqually" translatesAutoresizingMaskIntoConstraints="NO" id="oGz-Bu-nCT">
                                <rect key="frame" x="0.0" y="21" width="359" height="257"/>
                            </stackView>
                            <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Bottom Label" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="8bg-zV-k8Q">
                                <rect key="frame" x="0.0" y="278" width="359" height="21"/>
                                <color key="backgroundColor" red="0.92143100499999997" green="0.92145264149999995" blue="0.92144101860000005" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
                                <fontDescription key="fontDescription" type="system" pointSize="17"/>
                                <nil key="textColor"/>
                                <nil key="highlightedColor"/>
                            </label>
                        </subviews>
                        <color key="backgroundColor" red="0.36312681436538696" green="0.3205370306968689" blue="0.87124341726303101" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
                        <constraints>
                            <constraint firstItem="oGz-Bu-nCT" firstAttribute="top" secondItem="jZ3-yl-TaR" secondAttribute="bottom" id="3FB-p9-cGU"/>
                            <constraint firstItem="8bg-zV-k8Q" firstAttribute="leading" secondItem="TFh-sZ-4cx" secondAttribute="leading" id="7bO-hv-chQ"/>
                            <constraint firstItem="oGz-Bu-nCT" firstAttribute="leading" secondItem="TFh-sZ-4cx" secondAttribute="leading" id="G5h-mz-ag5"/>
                            <constraint firstItem="jZ3-yl-TaR" firstAttribute="top" secondItem="TFh-sZ-4cx" secondAttribute="top" id="T1H-hj-4jJ"/>
                            <constraint firstAttribute="bottom" secondItem="8bg-zV-k8Q" secondAttribute="bottom" id="TYr-rY-NAc"/>
                            <constraint firstAttribute="trailing" secondItem="oGz-Bu-nCT" secondAttribute="trailing" id="VA9-gN-L1a"/>
                            <constraint firstAttribute="trailing" secondItem="8bg-zV-k8Q" secondAttribute="trailing" id="Vv5-P9-EGo"/>
                            <constraint firstItem="jZ3-yl-TaR" firstAttribute="leading" secondItem="TFh-sZ-4cx" secondAttribute="leading" id="XZc-QB-dm1"/>
                            <constraint firstItem="8bg-zV-k8Q" firstAttribute="top" secondItem="oGz-Bu-nCT" secondAttribute="bottom" id="ayn-E8-jo9"/>
                            <constraint firstAttribute="trailing" secondItem="jZ3-yl-TaR" secondAttribute="trailing" id="v4D-bJ-ltC"/>
                        </constraints>
                    </view>
                </subviews>
                <color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
                <constraints>
                    <constraint firstItem="vUN-kp-3ea" firstAttribute="trailing" secondItem="TFh-sZ-4cx" secondAttribute="trailing" constant="8" id="cgO-BT-ruo"/>
                    <constraint firstItem="TFh-sZ-4cx" firstAttribute="leading" secondItem="vUN-kp-3ea" secondAttribute="leading" constant="8" id="rAA-Vf-F0B"/>
                    <constraint firstItem="vUN-kp-3ea" firstAttribute="bottom" secondItem="TFh-sZ-4cx" secondAttribute="bottom" constant="8" id="rhR-sH-KEq"/>
                    <constraint firstItem="TFh-sZ-4cx" firstAttribute="top" secondItem="vUN-kp-3ea" secondAttribute="top" constant="8" id="sag-F8-NuC"/>
                </constraints>
                <freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
                <viewLayoutGuide key="safeArea" id="vUN-kp-3ea"/>
                <point key="canvasLocation" x="148" y="49.925037481259373"/>
            </view>
        </objects>
    </document>
    

    Result:

    enter image description here


    EDIt Slightly modified code to produce the "expected result" image the OP added:

    • removed the labels (I had them there as an example of additional elements)
    • constrained stackView to 0 on all 4 sides
    • changed stackView to Aligment: Fill and Distribution: Fill Equally

    TaskSheet.xib

    <?xml version="1.0" encoding="UTF-8"?>
    <document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="15505" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
        <device id="retina4_7" orientation="portrait" appearance="light"/>
        <dependencies>
            <deployment identifier="iOS"/>
            <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15510"/>
            <capability name="Safe area layout guides" minToolsVersion="9.0"/>
            <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
        </dependencies>
        <objects>
            <placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="TaskSheet" customModule="scratchy" customModuleProvider="target">
                <connections>
                    <outlet property="contentView" destination="TFh-sZ-4cx" id="zaP-M3-nAu"/>
                    <outlet property="mainStack" destination="oGz-Bu-nCT" id="oCb-IB-Q4i"/>
                </connections>
            </placeholder>
            <placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
            <view contentMode="scaleToFill" id="iN0-l3-epB">
                <rect key="frame" x="0.0" y="0.0" width="375" height="315"/>
                <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
                <subviews>
                    <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="TFh-sZ-4cx">
                        <rect key="frame" x="8" y="8" width="359" height="299"/>
                        <subviews>
                            <stackView opaque="NO" contentMode="scaleToFill" axis="vertical" distribution="fillEqually" translatesAutoresizingMaskIntoConstraints="NO" id="oGz-Bu-nCT">
                                <rect key="frame" x="0.0" y="0.0" width="359" height="299"/>
                            </stackView>
                        </subviews>
                        <color key="backgroundColor" red="0.36312681436538696" green="0.3205370306968689" blue="0.87124341726303101" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
                        <constraints>
                            <constraint firstItem="oGz-Bu-nCT" firstAttribute="leading" secondItem="TFh-sZ-4cx" secondAttribute="leading" id="G5h-mz-ag5"/>
                            <constraint firstAttribute="bottom" secondItem="oGz-Bu-nCT" secondAttribute="bottom" id="SIv-DX-ZpP"/>
                            <constraint firstAttribute="trailing" secondItem="oGz-Bu-nCT" secondAttribute="trailing" id="VA9-gN-L1a"/>
                            <constraint firstItem="oGz-Bu-nCT" firstAttribute="top" secondItem="TFh-sZ-4cx" secondAttribute="top" id="hPW-P3-dsk"/>
                        </constraints>
                    </view>
                </subviews>
                <color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
                <constraints>
                    <constraint firstItem="vUN-kp-3ea" firstAttribute="trailing" secondItem="TFh-sZ-4cx" secondAttribute="trailing" constant="8" id="cgO-BT-ruo"/>
                    <constraint firstItem="TFh-sZ-4cx" firstAttribute="leading" secondItem="vUN-kp-3ea" secondAttribute="leading" constant="8" id="rAA-Vf-F0B"/>
                    <constraint firstItem="vUN-kp-3ea" firstAttribute="bottom" secondItem="TFh-sZ-4cx" secondAttribute="bottom" constant="8" id="rhR-sH-KEq"/>
                    <constraint firstItem="TFh-sZ-4cx" firstAttribute="top" secondItem="vUN-kp-3ea" secondAttribute="top" constant="8" id="sag-F8-NuC"/>
                </constraints>
                <freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
                <viewLayoutGuide key="safeArea" id="vUN-kp-3ea"/>
                <point key="canvasLocation" x="148" y="49.925037481259373"/>
            </view>
        </objects>
    </document>
    

    Classes

    class TaskSheet: UIView {
    
        @IBOutlet var contentView: UIView!
        @IBOutlet var mainStack: UIStackView!
    
        override init(frame: CGRect) {
            super.init(frame: frame)
            setup()
        }
    
        required init?(coder aDecoder: NSCoder) {
            super.init(coder: aDecoder)
            setup()
        }
    
        func setup() {
            let nib = UINib(nibName: "TaskSheet", bundle: nil)
            nib.instantiate(withOwner: self, options: nil)
            addSubview(contentView)
            NSLayoutConstraint.activate([
    
                // constrain contentView on all 4 sides with 0-pts "padding"
                contentView.topAnchor.constraint(equalTo: topAnchor, constant: 0.0),
                contentView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: 0.0),
                contentView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 0.0),
                contentView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: 0.0),
    
            ])
    
        }
    }
    
    class PDFSheet: UIView {
    
        var taskSheet: TaskSheet!
        var sheetArray = [UIView]()
    
        override init(frame: CGRect) {
            super.init(frame: frame)
            _ = makeSheet()
        }
    
        required init?(coder: NSCoder) {
            super.init(coder: coder)
            _ = makeSheet()
        }
    
        func makeSheet() -> [UIView] {
    
            taskSheet = TaskSheet()
    
            let newView1 = UIView()
            newView1.backgroundColor = .green
    
            let newView2 = UIView()
            newView2.backgroundColor = .yellow
    
            let spacerView = UIView()
            spacerView.backgroundColor = .clear
    
            // to get the "expected result" as shown in the OP's image,
            //  a 3-part stack view with equal heights,
            //  an easy way is to add a clear "spacer view" as the
            //  first - "top" - arranged subview
    
            taskSheet.mainStack.addArrangedSubview(spacerView)
            taskSheet.mainStack.addArrangedSubview(newView1)
            taskSheet.mainStack.addArrangedSubview(newView2)
            sheetArray.append(taskSheet)
    
            addSubview(taskSheet)
    
            taskSheet.translatesAutoresizingMaskIntoConstraints = false
    
            NSLayoutConstraint.activate([
    
                // constrain taskSheet on all 4 sides
                taskSheet.topAnchor.constraint(equalTo: topAnchor, constant: 0.0),
                taskSheet.bottomAnchor.constraint(equalTo: bottomAnchor, constant: 0.0),
                taskSheet.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 0.0),
                taskSheet.trailingAnchor.constraint(equalTo: trailingAnchor, constant: 0.0),
    
            ])
    
            return sheetArray
        }
    }
    
    class TaskViewController: UIViewController {
    
        var theSheetView: PDFSheet!
    
        override func viewDidLoad() {
            super.viewDidLoad()
    
            theSheetView = PDFSheet()
            theSheetView.translatesAutoresizingMaskIntoConstraints = false
    
            view.addSubview(theSheetView)
    
            let g = view.safeAreaLayoutGuide
    
            NSLayoutConstraint.activate([
    
                // constrain the sheet view on top and leading at 40-pts (just so it's not flush with top/left of the view)
                // with specified width: 612 and height 792
                theSheetView.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
                theSheetView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
                theSheetView.widthAnchor.constraint(equalToConstant: 612),
                theSheetView.heightAnchor.constraint(equalToConstant: 792),
            ])
    
        }
    
    }
    

    Result as run on iPad Air 3rd gen (to match the OP's width: 612, height: 792 specification):

    enter image description here


    Another Edit:

    This may be closer to the OP's intent.

    • PDFSheet class is now treated as a "view provider" rather than as a view itself.
    • The first element of the returned array of TaskSheet views (currently containing only one view) will be added as a subview to the viewController's view.

    Same .xib file as above.

    class TaskSheet: UIView {
    
        @IBOutlet var contentView: UIView!
        @IBOutlet var mainStack: UIStackView!
    
        override init(frame: CGRect) {
            super.init(frame: frame)
            setup()
        }
    
        required init?(coder aDecoder: NSCoder) {
            super.init(coder: aDecoder)
            setup()
        }
    
        func setup() {
            let nib = UINib(nibName: "TaskSheet", bundle: nil)
            nib.instantiate(withOwner: self, options: nil)
            addSubview(contentView)
    
            NSLayoutConstraint.activate([
    
                // constrain contentView on all 4 sides with 0-pts "padding"
                contentView.topAnchor.constraint(equalTo: topAnchor, constant: 0.0),
                contentView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: 0.0),
                contentView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 0.0),
                contentView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: 0.0),
    
            ])
    
        }
    }
    
    class PDFSheet: UIView {
    
        var taskSheet: TaskSheet!
        var sheetArray = [UIView]()
    
        func makeSheet() -> [UIView] {
    
            taskSheet = TaskSheet(frame: CGRect(x: 0, y: 0, width: 612, height: 792))
    
            let newView1 = UIView()
            newView1.backgroundColor = .green
    
            let newView2 = UIView()
            newView2.backgroundColor = .yellow
    
            let spacerView = UIView()
            spacerView.backgroundColor = .clear
    
            // to get the "expected result" as shown in the OP's image,
            //  a 3-part stack view with equal heights,
            //  an easy way is to add a clear "spacer view" as the
            //  first - "top" - arranged subview
    
            taskSheet.mainStack.addArrangedSubview(spacerView)
            taskSheet.mainStack.addArrangedSubview(newView1)
            taskSheet.mainStack.addArrangedSubview(newView2)
    
            sheetArray.append(taskSheet)
    
            return sheetArray
        }
    }
    
    class TaskViewController: UIViewController {
    
        var theSheetView: PDFSheet!
    
        override func viewDidLoad() {
            super.viewDidLoad()
    
            theSheetView = PDFSheet()
    
            let views: [UIView] = theSheetView.makeSheet()
    
            guard let v = views.first else {
                fatalError("PDFSheet failed to create TaskSheet")
            }
    
        // note: At this point, the view has not been added to the
        // view hierarchy. If you're going to do something with it,
        // such as output it to a png or pdf, for example, you need
        // to tell auto-layout to do its work
        v.setNeedsLayout()
        v.layoutIfNeeded()
    
        let s = v.exportAsPdfFromView()
            view.addSubview(v)
    
        }
    
    }
    

    and (visually) the same result. This time, the resulting TaskSheet view is just being added to the viewController's view, without any offset:

    enter image description here