Search code examples
iosswiftuiviewdatasourcesubview

Sending datasource information only when UIView has been laid out?


I am having trouble understanding this. I have been able to do such a project earlier on but now a similar approach isn't working for me.

Here is the issue: I have a UIPageViewController that has 4 storyViewControllers. Each storyViewControllers has a custom containerView. I want to add 'n' number of UIViews on the containerView. I am sending the dataSource or 'n' from the view controller. However, I am not able to correctly lay out the subviews on the container view.

Essentially I want to know when to send the datasource info from the view controller. Obviously I would like to send it once the custom container view has been added.

I am using viewDidLayoutSubviews. This makes it work. However, I don't think it's the correct way. Now every time the view controller lays out subviews my delegate will be called.

I have tried doing it in viewDidLoad() but that doesn't work either.

This works. But just doesn't seem right. Als My storyViewController code

    override func viewDidLoad() {
    super.viewDidLoad()
    segmentContainerView = ATCStorySegmentsView()
    view.addSubview(segmentContainerView)
    configureSegmentContainerView()
    segmentContainerView.translatesAutoresizingMaskIntoConstraints = false

}

  override func viewDidLayoutSubviews() {
    segmentContainerView.delegate = self
     DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
        self.segmentContainerView.startAnimation() // I am also animating these views that are laid on the containerView. Doing them here starts the animation randomly whenever I scroll through the UIPageController
    }
}

In the containerView:

    var delegate: ATCSegmentDataSource? {
    didSet {
        addSegments()
    }
}

private func addSegments() {
    let numberOfSegment = delegate?.numberOfSegmentsToShow()
    guard let segmentQuantity = numberOfSegment else { return }
    layoutIfNeeded()
    setNeedsLayout()
    for i in 0..<segmentQuantity {
        let segment = Segment()
        addSubview(segment.bottomSegment)
        addSubview(segment.topSegment)
        configureSegmentFrame(index: i, segmentView: segment)
        segmentsArray.append(segment)
    }


}

private func configureSegmentFrame(index: Int, segmentView: Segment) {
    let numberOfSegment = delegate?.numberOfSegmentsToShow()
    guard let segmentQuantity = numberOfSegment else { return }

    let widthOfSegment : CGFloat = (self.frame.width - (padding * CGFloat(segmentQuantity - 1))) / CGFloat(segmentQuantity)

    let i = CGFloat(index)

    let segmentFrame = CGRect(x: i * (widthOfSegment + padding), y: 0, width: widthOfSegment, height: self.frame.height)
    segmentView.bottomSegment.frame = segmentFrame
    segmentView.topSegment.frame = segmentFrame
    segmentView.topSegment.frame.size.width = 0

}

This works the way it should. But when I scroll the UIPageViewController, the animations don't always start from the beginning. Since it relies on laying out subviews. Sometimes if I slowly scroll through the page controller then the subviews are laid out again and the animation starts from start. Other times when the views are already loaded, the animations starts from where I left out.

Question I want to know what is the best way to send datasource from view controller to the containerView? That datasource is what will be needed to generate the amount of views to be added on the containerView.

This is the result I get, if I send the datasource from viewDidLayoutSubviews. I have asked another question earlier today that lists other methods I used to send datasource. Take a look at that as well: Not able to lay out Subviews on a custom UIView in Swift

enter image description here


Solution

  • This is a very basic example...

    It has:

    • a Segment: UIView sub-class that holds a "bottomSegment" and a "topSegment"
    • a ATCStorySegmentsView: UIView sub-class with a UIStackView to layout the desired number of segments
    • a StoryViewController: UIViewController sub-class that adds ATCStorySegmentsView at the top of its view, and then tells that view to animate the first segment on viewDidAppear()

    class Segment: UIView {
    
        let topSegment: UIView = {
            let v = UIView()
            v.translatesAutoresizingMaskIntoConstraints = false
            v.backgroundColor = .white
            return v
        }()
    
        let bottomSegment: UIView = {
            let v = UIView()
            v.translatesAutoresizingMaskIntoConstraints = false
            v.backgroundColor = .gray
            return v
        }()
    
        var startConstraint: NSLayoutConstraint = NSLayoutConstraint()
        var endConstraint: NSLayoutConstraint = NSLayoutConstraint()
    
        override init(frame: CGRect) {
            super.init(frame: frame)
            commonInit()
        }
    
        required init?(coder aDecoder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }
    
        func commonInit() -> Void {
            addSubview(bottomSegment)
            addSubview(topSegment)
    
            // start constraint has width of Zero
            startConstraint = topSegment.widthAnchor.constraint(equalTo: bottomSegment.widthAnchor, multiplier: 0.0)
    
            // end constraint has width of bottomSegment
            endConstraint = topSegment.widthAnchor.constraint(equalTo: bottomSegment.widthAnchor, multiplier: 1.0)
    
            NSLayoutConstraint.activate([
    
                // bottomSegment constrained to all 4 sides
                bottomSegment.topAnchor.constraint(equalTo: topAnchor),
                bottomSegment.bottomAnchor.constraint(equalTo: bottomAnchor),
                bottomSegment.leadingAnchor.constraint(equalTo: leadingAnchor),
                bottomSegment.trailingAnchor.constraint(equalTo: trailingAnchor),
    
                // topSegment constrained top, bottom and leading
                topSegment.topAnchor.constraint(equalTo: topAnchor),
                topSegment.bottomAnchor.constraint(equalTo: bottomAnchor),
                topSegment.leadingAnchor.constraint(equalTo: leadingAnchor),
    
                // activate topSegemnt width constraint
                startConstraint,
    
                ])
        }
    
        func showTopSegment() -> Void {
            // deactivate startConstraint
            startConstraint.isActive = false
            // activate endConstraint
            endConstraint.isActive = true
        }
    
    }
    
    protocol ATCSegmentDataSource {
        func numberOfSegmentsToShow() -> Int
    }
    
    class ATCStorySegmentsView: UIView {
    
        let theStackView: UIStackView = {
            let v = UIStackView()
            v.translatesAutoresizingMaskIntoConstraints = false
            v.axis = .horizontal
            v.alignment = .fill
            v.distribution = .fillEqually
            v.spacing = 4
            return v
        }()
    
        var segmentsArray: [Segment] = [Segment]()
    
        var delegate: ATCSegmentDataSource? {
            didSet {
                addSegments()
            }
        }
    
        override init(frame: CGRect) {
            super.init(frame: frame)
            commonInit()
        }
    
        required init?(coder aDecoder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }
    
        func commonInit() -> Void {
            addSubview(theStackView)
    
            NSLayoutConstraint.activate([
    
                // constrain stack view to all 4 sides
                theStackView.topAnchor.constraint(equalTo: topAnchor),
                theStackView.bottomAnchor.constraint(equalTo: bottomAnchor),
                theStackView.leadingAnchor.constraint(equalTo: leadingAnchor),
                theStackView.trailingAnchor.constraint(equalTo: trailingAnchor),
    
                ])
        }
    
        private func addSegments() {
            let numberOfSegment = delegate?.numberOfSegmentsToShow()
            guard let segmentQuantity = numberOfSegment else { return }
    
            // add desired number of Segment subviews to teh stack view
            for _ in 0..<segmentQuantity {
                let seg = Segment()
                seg.translatesAutoresizingMaskIntoConstraints = false
                theStackView.addArrangedSubview(seg)
                segmentsArray.append(seg)
            }
        }
    
        func startAnimation() -> Void {
    
            // this will animate changing the topSegment's width
            self.segmentsArray.first?.showTopSegment()
            UIView.animate(withDuration: 1.5, animations: {
                self.layoutIfNeeded()
            })
    
        }
    
    
    }
    
    class StoryViewController: UIViewController, ATCSegmentDataSource {
    
        let segmentContainerView: ATCStorySegmentsView = ATCStorySegmentsView()
    
        override func viewDidLoad() {
            super.viewDidLoad()
    
            view.backgroundColor = .red
    
            view.addSubview(segmentContainerView)
    
            segmentContainerView.translatesAutoresizingMaskIntoConstraints = false
    
            NSLayoutConstraint.activate([
    
                // constrain segmentContainerView to top (safe-area) + 20-pts "padding",
                segmentContainerView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20.0),
    
                // leading and trailing with "padding" of 20-pts
                segmentContainerView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 20.0),
                segmentContainerView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -20.0),
    
                // constrain height to 10-pts
                segmentContainerView.heightAnchor.constraint(equalToConstant: 10.0),
    
                ])
    
            // set the delegate
            segmentContainerView.delegate = self
    
        }
    
        override func viewDidAppear(_ animated: Bool) {
            segmentContainerView.startAnimation()
        }
    
        func numberOfSegmentsToShow() -> Int {
            return 3
        }
    
    }
    

    Result (the white bar animates across the gray bar when the view appears):

    enter image description here

    enter image description here

    With 5 segments:

    enter image description here

    Note that, as auto-layout is designed to do, it also handles size-changes, such as when the device is rotated:

    enter image description here