Sending datasource information only when UIView has been laid out?

大城市里の小女人 提交于 2020-01-06 06:34:16

问题


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


回答1:


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):

With 5 segments:

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



来源:https://stackoverflow.com/questions/56671161/sending-datasource-information-only-when-uiview-has-been-laid-out

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!