带有 UIPageViewController 的弹性标题

发布于 2025-01-20 19:53:59 字数 7131 浏览 1 评论 0原文

我的问题似乎很明显,而且重复了,但我无法设法使它起作用。

我正在尝试实现著名的弹力标头效果(滚动时,图像的顶部贴在uiscrollview的顶部),但使用uipageViewController而不是简单的图像。

我的结构是:

UINavigationBar
   |-- UIScrollView
         |-- UIView (totally optional container)
              |-- UIPageViewController (as UIView, embedded with addChild()) <-- TO STICK
              |-- UIHostingViewController (SwiftUI view with labels, also embedded)
              |-- UITableView (not embedded but could be)

我的uipageViewController包含制作旋转木马的图像,仅此而已。
我所有的视图都使用nslayoutconstraint s(带有容器中的垂直布局的视觉格式)列出。

我将页面控制器的视图 tot to self.view(带有或没有pircy> pircipity),但没有运气,无论如何我做的事情绝对没有改变。

我终于尝试使用snapkit,但它都不起作用(我对此一无所知,但它似乎只是nslayOutConcontaint s的包装器,所以我' M并不感到惊讶,它也行不通)。

我跟随 this教程and 工作。

(我如何实现我想要的东西?

编辑1: 为了澄清,我的轮播的强制高度为350。我想在我的整个旋转木马上实现此确切的效果(用单个uiimageView显示):

“

我想将此效果复制到我的整个uipageViewController/carousel,以便显示的页面/图像在滚动时可以具有此效果。

注意:如上所述,我有一个(透明的)导航栏,我的安全区域插图受到尊重(没有任何不在状态栏下)。我认为这不会改变解决方案(因为解决方案可能是一种将旋转木马顶部粘贴到self.view的方法,无论self> self.view 代码>),但我更喜欢您知道一切。

编辑2:
@Donmag答案的主要风险投资:

    private let info: UITableView = {
        let v = UITableView(frame: .zero, style: .insetGrouped)
        v.backgroundColor = .systemBackground
        v.translatesAutoresizingMaskIntoConstraints = false
        return v
    }()

    private lazy var infoHeightConstraint: NSLayoutConstraint = {
        // Needed constraint because else standalone UITableView gets an height of 0 even with usual constraints
        // I update this constraint in viewWillAppear & viewDidAppear when the table gets a proper contentSize
        info.heightAnchor.constraint(equalToConstant: 0.0)
    }()
    
    private let scrollView: UIScrollView = {
        let v = UIScrollView()
        v.contentInsetAdjustmentBehavior = .never
        v.translatesAutoresizingMaskIntoConstraints = false
        return v
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        ...

        // MARK: Views declaration
        // Container for carousel
        let stretchyView = UIView()
        stretchyView.translatesAutoresizingMaskIntoConstraints = false
        
        // Carousel
        let carouselController = ProfileDetailCarousel(images: [
            UIImage(named: "1")!,
            UIImage(named: "2")!,
            UIImage(named: "3")!,
            UIImage(named: "4")!
        ])
        addChild(carouselController)
        let carousel: UIView = carouselController.view
        carousel.translatesAutoresizingMaskIntoConstraints = false
        stretchyView.addSubview(carousel)
        carouselController.didMove(toParent: self)
        
        // Container for below-carousel views
        let contentView = UIView()
        contentView.translatesAutoresizingMaskIntoConstraints = false
        
        // Texts and bio
        let bioController = UIHostingController(rootView: ProfileDetailBio())
        addChild(bioController)
        let bio: UIView = bioController.view
        bio.translatesAutoresizingMaskIntoConstraints = false
        contentView.addSubview(bio)
        bioController.didMove(toParent: self)
        
        // Info table
        info.delegate = tableDelegate
        info.dataSource = tableDataSource
        tableDelegate.viewController = self
        contentView.addSubview(info)
        
        [stretchyView, contentView].forEach { v in
            scrollView.addSubview(v)
        }
        view.addSubview(scrollView)
        
        // MARK: Constraints
        let stretchyTop = stretchyView.topAnchor.constraint(equalTo: scrollView.frameLayoutGuide.topAnchor)
        stretchyTop.priority = .defaultHigh
        NSLayoutConstraint.activate([
            // Scroll view
            scrollView.topAnchor.constraint(equalTo: view.topAnchor),
            scrollView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
            scrollView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
            scrollView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
            
            // Stretchy view
            stretchyTop,
            
            stretchyView.leadingAnchor.constraint(equalTo: scrollView.frameLayoutGuide.leadingAnchor),
            stretchyView.trailingAnchor.constraint(equalTo: scrollView.frameLayoutGuide.trailingAnchor),
            stretchyView.heightAnchor.constraint(greaterThanOrEqualToConstant: 350.0),
            
            // Carousel
            carousel.topAnchor.constraint(equalTo: stretchyView.topAnchor),
            carousel.bottomAnchor.constraint(equalTo: stretchyView.bottomAnchor),
            carousel.centerXAnchor.constraint(equalTo: stretchyView.centerXAnchor),
            carousel.widthAnchor.constraint(equalTo: stretchyView.widthAnchor),
            
            // Content view
            contentView.leadingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.leadingAnchor),
            contentView.trailingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.trailingAnchor),
            contentView.bottomAnchor.constraint(equalTo: scrollView.contentLayoutGuide.bottomAnchor),
            contentView.widthAnchor.constraint(equalTo: scrollView.frameLayoutGuide.widthAnchor),
            contentView.topAnchor.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor, constant: 350.0),
            contentView.topAnchor.constraint(equalTo: stretchyView.bottomAnchor),
            
            // Bio
            bio.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 10.0),
            bio.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
            bio.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
            bio.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
            
            // Info table
            info.topAnchor.constraint(equalTo: bio.bottomAnchor, constant: 12.0),
            info.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
            info.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
            infoHeightConstraint
        ])
    }

My problem seems obvious and duplicated but I can't manage to make it work.

I'm trying to achieve the famous stretchy header effect (image's top side stuck to top of UIScrollView when scrolling), but with an UIPageViewController instead of simply an image.

My structure is:

UINavigationBar
   |-- UIScrollView
         |-- UIView (totally optional container)
              |-- UIPageViewController (as UIView, embedded with addChild()) <-- TO STICK
              |-- UIHostingViewController (SwiftUI view with labels, also embedded)
              |-- UITableView (not embedded but could be)

My UIPageViewController contains images to make a carousel, nothing more.
All my views are laid out with NSLayoutConstraints (with visual format for vertical layout in the container).

I trie sticking topAnchor of the page controller's view to the one of self.view (with or without priority) but no luck, and no matter what I do it changes absolutely nothing.

I finally tried to use SnapKit but it doesn't work neither (I don't know much about it but it seems to only be a wrapper for NSLayoutConstaints so I'm not surprised it doesn't work too).

I followed this tutorial, this one and that one but none of them worked.

(How) can I achieve what I want?

EDIT 1:
To clarify, my carousel currently has a forced height of 350. I want to achieve this exact effect (that is shown with a single UIImageView) on my whole carousel:

gif

To clarify as much as possible, I want to replicate this effect to my whole UIPageViewController/carousel so that the displayed page/image can have this effect when scrolled.

NOTE: as mentioned in the structure above, I have a (transparent) navigation bar, and my safe area insets are respected (nothing goes under the status bar). I don't think it would change the solution (as the solution is probably a way to stick the top of the carousel to self.view, no matter the frame of self.view) but I prefer you to know everything.

EDIT 2:
Main VC with @DonMag's answer:

    private let info: UITableView = {
        let v = UITableView(frame: .zero, style: .insetGrouped)
        v.backgroundColor = .systemBackground
        v.translatesAutoresizingMaskIntoConstraints = false
        return v
    }()

    private lazy var infoHeightConstraint: NSLayoutConstraint = {
        // Needed constraint because else standalone UITableView gets an height of 0 even with usual constraints
        // I update this constraint in viewWillAppear & viewDidAppear when the table gets a proper contentSize
        info.heightAnchor.constraint(equalToConstant: 0.0)
    }()
    
    private let scrollView: UIScrollView = {
        let v = UIScrollView()
        v.contentInsetAdjustmentBehavior = .never
        v.translatesAutoresizingMaskIntoConstraints = false
        return v
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        ...

        // MARK: Views declaration
        // Container for carousel
        let stretchyView = UIView()
        stretchyView.translatesAutoresizingMaskIntoConstraints = false
        
        // Carousel
        let carouselController = ProfileDetailCarousel(images: [
            UIImage(named: "1")!,
            UIImage(named: "2")!,
            UIImage(named: "3")!,
            UIImage(named: "4")!
        ])
        addChild(carouselController)
        let carousel: UIView = carouselController.view
        carousel.translatesAutoresizingMaskIntoConstraints = false
        stretchyView.addSubview(carousel)
        carouselController.didMove(toParent: self)
        
        // Container for below-carousel views
        let contentView = UIView()
        contentView.translatesAutoresizingMaskIntoConstraints = false
        
        // Texts and bio
        let bioController = UIHostingController(rootView: ProfileDetailBio())
        addChild(bioController)
        let bio: UIView = bioController.view
        bio.translatesAutoresizingMaskIntoConstraints = false
        contentView.addSubview(bio)
        bioController.didMove(toParent: self)
        
        // Info table
        info.delegate = tableDelegate
        info.dataSource = tableDataSource
        tableDelegate.viewController = self
        contentView.addSubview(info)
        
        [stretchyView, contentView].forEach { v in
            scrollView.addSubview(v)
        }
        view.addSubview(scrollView)
        
        // MARK: Constraints
        let stretchyTop = stretchyView.topAnchor.constraint(equalTo: scrollView.frameLayoutGuide.topAnchor)
        stretchyTop.priority = .defaultHigh
        NSLayoutConstraint.activate([
            // Scroll view
            scrollView.topAnchor.constraint(equalTo: view.topAnchor),
            scrollView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
            scrollView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
            scrollView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
            
            // Stretchy view
            stretchyTop,
            
            stretchyView.leadingAnchor.constraint(equalTo: scrollView.frameLayoutGuide.leadingAnchor),
            stretchyView.trailingAnchor.constraint(equalTo: scrollView.frameLayoutGuide.trailingAnchor),
            stretchyView.heightAnchor.constraint(greaterThanOrEqualToConstant: 350.0),
            
            // Carousel
            carousel.topAnchor.constraint(equalTo: stretchyView.topAnchor),
            carousel.bottomAnchor.constraint(equalTo: stretchyView.bottomAnchor),
            carousel.centerXAnchor.constraint(equalTo: stretchyView.centerXAnchor),
            carousel.widthAnchor.constraint(equalTo: stretchyView.widthAnchor),
            
            // Content view
            contentView.leadingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.leadingAnchor),
            contentView.trailingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.trailingAnchor),
            contentView.bottomAnchor.constraint(equalTo: scrollView.contentLayoutGuide.bottomAnchor),
            contentView.widthAnchor.constraint(equalTo: scrollView.frameLayoutGuide.widthAnchor),
            contentView.topAnchor.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor, constant: 350.0),
            contentView.topAnchor.constraint(equalTo: stretchyView.bottomAnchor),
            
            // Bio
            bio.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 10.0),
            bio.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
            bio.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
            bio.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
            
            // Info table
            info.topAnchor.constraint(equalTo: bio.bottomAnchor, constant: 12.0),
            info.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
            info.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
            infoHeightConstraint
        ])
    }

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

残月升风 2025-01-27 19:53:59

您的视图层次结构应该是:

UINavigationBar
   |-- UIScrollView
         |-- UIView ("stretchy" container view)
              |-- UIPageViewController (as UIView, embedded with asChild())
         |-- UIHostingViewController (SwiftUI view with labels, also embedded)

要使可拉伸视图“粘在顶部”:

我们将可拉伸视图的顶部限制为滚动视图的 .frameLayoutGuide 顶部,但我们给该约束赋予小于-需要.priority,这样我们就可以将其“推”到屏幕上。

我们还为可​​拉伸视图设置了大于或等于 350 的高度约束。这将允许其垂直拉伸(但不能压缩)。

我们将 UIHostingViewController 中的视图称为我们的“contentView”...并且我们将其顶部约束到可拉伸视图的底部。

然后,我们给内容视图另一个顶部约束——这次是滚动视图的 .contentLayoutGuide,常量为 350(可拉伸视图的高度)。这加上前导/尾随/底部约束定义了“可滚动区域”。

当我们向下滚动(拉)时,内容视图将“拉下”可拉伸视图的底部。

当我们向上滚动(推动)时,内容视图将“推动”整个拉伸视图。

它的外观如下(太大,无法在此处添加为 gif): https://i.sstatic.net /4wvxK.jpg

下面是实现该功能的示例代码。一切都是通过代码完成的,因此不需要 @IBOutlet 或其他连接。另请注意,我使用了三个图像作为页面视图 -“ex1”、“ex2”、“ex3”:

视图控制器

class StretchyHeaderViewController: UIViewController {
    
    let scrollView: UIScrollView = {
        let v = UIScrollView()
        v.contentInsetAdjustmentBehavior = .never
        return v
    }()
    let stretchyView: UIView = {
        let v = UIView()
        return v
    }()
    let contentView: UIView = {
        let v = UIView()
        v.backgroundColor = .systemYellow
        return v
    }()
    
    let stretchyViewHeight: CGFloat = 350.0
    
    override func viewDidLoad() {
        super.viewDidLoad()

        // set to a greter-than-zero value if you want spacing between the "pages"
        let opts = [UIPageViewController.OptionsKey.interPageSpacing: 0.0]
        // instantiate the Page View controller
        let pgVC = SamplePageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal, options: opts)
        // add it as a child controller
        self.addChild(pgVC)
        // safe unwrap
        guard let pgv = pgVC.view else { return }
        pgv.translatesAutoresizingMaskIntoConstraints = false
        // add the page controller view to stretchyView
        stretchyView.addSubview(pgv)
        pgVC.didMove(toParent: self)
        
        NSLayoutConstraint.activate([
            // constrain page view controller's view on all 4 sides
            pgv.topAnchor.constraint(equalTo: stretchyView.topAnchor),
            pgv.bottomAnchor.constraint(equalTo: stretchyView.bottomAnchor),
            pgv.centerXAnchor.constraint(equalTo: stretchyView.centerXAnchor),
            pgv.widthAnchor.constraint(equalTo: stretchyView.widthAnchor),
        ])
        
        [scrollView, stretchyView, contentView].forEach { v in
            v.translatesAutoresizingMaskIntoConstraints = false
        }
        
        // add contentView and stretchyView to the scroll view
        [stretchyView, contentView].forEach { v in
            scrollView.addSubview(v)
        }
        
        // add scroll view to self.view
        view.addSubview(scrollView)
        
        let safeG = view.safeAreaLayoutGuide
        let contentG = scrollView.contentLayoutGuide
        let frameG = scrollView.frameLayoutGuide
        
        // keep stretchyView's Top "pinned" to the Top of the scroll view FRAME
        //  so its Height will "stretch" when scroll view is pulled down
        let stretchyTop = stretchyView.topAnchor.constraint(equalTo: frameG.topAnchor, constant: 0.0)
        // priority needs to be less-than-required so we can "push it up" out of view
        stretchyTop.priority = .defaultHigh
        
        NSLayoutConstraint.activate([
            
            // scroll view Top to view Top
            scrollView.topAnchor.constraint(equalTo: view.topAnchor, constant: 0.0),

            // scroll view Leading/Trailing/Bottom to safe area
            scrollView.leadingAnchor.constraint(equalTo: safeG.leadingAnchor, constant: 0.0),
            scrollView.trailingAnchor.constraint(equalTo: safeG.trailingAnchor, constant: 0.0),
            scrollView.bottomAnchor.constraint(equalTo: safeG.bottomAnchor, constant: 0.0),
            
            // constrain stretchy view Top to scroll view's FRAME
            stretchyTop,
            
            // stretchyView to Leading/Trailing of scroll view FRAME
            stretchyView.leadingAnchor.constraint(equalTo: frameG.leadingAnchor, constant: 0.0),
            stretchyView.trailingAnchor.constraint(equalTo: frameG.trailingAnchor, constant: 0.0),
            
            // stretchyView Height - greater-than-or-equal-to
            //  so it can "stretch" vertically
            stretchyView.heightAnchor.constraint(greaterThanOrEqualToConstant: stretchyViewHeight),
            
            // content view Leading/Trailing/Bottom to scroll view's CONTENT GUIDE
            contentView.leadingAnchor.constraint(equalTo: contentG.leadingAnchor, constant: 0.0),
            contentView.trailingAnchor.constraint(equalTo: contentG.trailingAnchor, constant: 0.0),
            contentView.bottomAnchor.constraint(equalTo: contentG.bottomAnchor, constant: 0.0),

            // content view Width to scroll view's FRAME
            contentView.widthAnchor.constraint(equalTo: frameG.widthAnchor, constant: 0.0),

            // content view Top to scroll view's CONTENT GUIDE
            //  plus Height of stretchyView
            contentView.topAnchor.constraint(equalTo: contentG.topAnchor, constant: stretchyViewHeight),

            // content view Top to stretchyView Bottom
            contentView.topAnchor.constraint(equalTo: stretchyView.bottomAnchor, constant: 0.0),
    
        ])
        
        // add some content to the content view so we have something to scroll
        addSomeContent()
                
    }
    
    func addSomeContent() {
        // vertical stack view with 20 labels
        //  so we have something to scroll
        let stack = UIStackView()
        stack.axis = .vertical
        stack.spacing = 32
        stack.backgroundColor = .gray
        stack.translatesAutoresizingMaskIntoConstraints = false
        for i in 1...20 {
            let v = UILabel()
            v.text = "Label \(i)"
            v.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
            v.heightAnchor.constraint(equalToConstant: 48.0).isActive = true
            stack.addArrangedSubview(v)
        }
        contentView.addSubview(stack)
        NSLayoutConstraint.activate([
            stack.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 16.0),
            stack.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16.0),
            stack.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16.0),
            stack.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -16.0),
        ])
    }
    
}

每个页面的控制器

class OnePageVC: UIViewController {
    
    var image: UIImage = UIImage() {
        didSet {
            imgView.image = image
        }
    }
    let imgView: UIImageView = {
        let v = UIImageView()
        v.backgroundColor = .systemBlue
        v.contentMode = .scaleAspectFill
        v.clipsToBounds = true
        v.translatesAutoresizingMaskIntoConstraints = false
        return v
    }()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        view.backgroundColor = .systemBackground
        
        view.addSubview(imgView)
        NSLayoutConstraint.activate([
            // constrain image view to all 4 sides
            imgView.topAnchor.constraint(equalTo: view.topAnchor, constant: 0.0),
            imgView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 0.0),
            imgView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: 0.0),
            imgView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 0.0),
        ])
    }
}

示例页面视图控制器

class SamplePageViewController: UIPageViewController, UIPageViewControllerDelegate, UIPageViewControllerDataSource {
    
    var controllers: [UIViewController] = []
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let imgNames: [String] = [
            "ex1", "ex2", "ex3",
        ]
        for i in 0..<imgNames.count {
            let aViewController = OnePageVC()
            if let img = UIImage(named: imgNames[i]) {
                aViewController.image = img
            }
            self.controllers.append(aViewController)
        }

        self.dataSource = self
        self.delegate = self
        
        self.setViewControllers([controllers[0]], direction: .forward, animated: false)
    }
    
    func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
        if let index = controllers.firstIndex(of: viewController), index > 0 {
            return controllers[index - 1]
        }
        return nil
    }
    
    func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
        if let index = controllers.firstIndex(of: viewController), index < controllers.count - 1 {
            return controllers[index + 1]
        }
        return nil
    }

}

编辑

查看您在问题的编辑中发布的代码...这有点困难,因为我不知道您的 ProfileDetailBio 视图是什么,但这里有一些技巧可以帮助调试这种情况在开发过程中:

  • 为您的视图提供对比背景颜色...使您在运行应用程序时可以轻松看到框架,
  • 如果子视图填充其超级视图的宽度,请将其变窄一点,以便您可以看到它设置的“后面/下面”的内容
  • < code>.clipsToBounds = true 在您用作“容器”的视图上 - 例如 contentView...如果子视图“丢失”,您知道它已扩展到边界之外的 如果

因此,对于您的代码...

// so we can see the contentView frame
contentView.backgroundColor = .systemYellow

// leave some space on the right-side of bio view, so we
//   so we can see the contentView behind it
bio.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -100.0),

您运行应用程序,您可能会看到 contentView 仅延伸到 bio 的底部 - 而不是 的底部>信息。

如果您随后执行此操作:

contentView.clipsToBounds = true

info 可能根本不可见。

检查你的约束,你有:

bio.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
        
// Info table
info.topAnchor.constraint(equalTo: bio.bottomAnchor, constant: 12.0),

它应该在哪里:

// no bio bottom anchor
//bio.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),

// this is correct
// Info table
info.topAnchor.constraint(equalTo: bio.bottomAnchor, constant: 12.0),

// add this
info.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
        

运行应用程序,你应该现在再次看到info,并且contentView扩展到信息底部。

假设 bioinfo 的高度组合起来足够高,需要滚动,您可以撤消“debug / dev”更改,然后就可以开始了。

Your view hierarchy should be:

UINavigationBar
   |-- UIScrollView
         |-- UIView ("stretchy" container view)
              |-- UIPageViewController (as UIView, embedded with asChild())
         |-- UIHostingViewController (SwiftUI view with labels, also embedded)

To get the stretchy view to "stick to the top":

We constrain the stretchy view's Top to the scroll view's .frameLayoutGuide Top, but we give that constraint a less-than-required .priority so we can "push it" up and off the screen.

We also give the stretchy view a Height constraint of greater-than-or-equal-to 350. This will allow it to stretch - but not compress - vertically.

We'll call the view from the UIHostingViewController our "contentView" ... and we'll constrain its Top to the stretchy view's Bottom.

Then, we give the content view another Top constraint -- this time to the scroll view's .contentLayoutGuide, with a constant of 350 (the height of the stretchy view). This, plus the Leading/Trailing/Bottom constraints defines the "scrollable area."

When we scroll (pull) down, the content view will "pull down" the Bottom of the stretchy view.

When we scroll (push) up, the content view will "push up" the entire stretchy view.

Here's how it looks (too big to add as a gif here): https://i.sstatic.net/4wvxK.jpg

And here's the sample code to make that. Everything is done via code, so no @IBOutlet or other connections needed. Also note that I used three images for the page views - "ex1", "ex2", "ex3":

View Controller

class StretchyHeaderViewController: UIViewController {
    
    let scrollView: UIScrollView = {
        let v = UIScrollView()
        v.contentInsetAdjustmentBehavior = .never
        return v
    }()
    let stretchyView: UIView = {
        let v = UIView()
        return v
    }()
    let contentView: UIView = {
        let v = UIView()
        v.backgroundColor = .systemYellow
        return v
    }()
    
    let stretchyViewHeight: CGFloat = 350.0
    
    override func viewDidLoad() {
        super.viewDidLoad()

        // set to a greter-than-zero value if you want spacing between the "pages"
        let opts = [UIPageViewController.OptionsKey.interPageSpacing: 0.0]
        // instantiate the Page View controller
        let pgVC = SamplePageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal, options: opts)
        // add it as a child controller
        self.addChild(pgVC)
        // safe unwrap
        guard let pgv = pgVC.view else { return }
        pgv.translatesAutoresizingMaskIntoConstraints = false
        // add the page controller view to stretchyView
        stretchyView.addSubview(pgv)
        pgVC.didMove(toParent: self)
        
        NSLayoutConstraint.activate([
            // constrain page view controller's view on all 4 sides
            pgv.topAnchor.constraint(equalTo: stretchyView.topAnchor),
            pgv.bottomAnchor.constraint(equalTo: stretchyView.bottomAnchor),
            pgv.centerXAnchor.constraint(equalTo: stretchyView.centerXAnchor),
            pgv.widthAnchor.constraint(equalTo: stretchyView.widthAnchor),
        ])
        
        [scrollView, stretchyView, contentView].forEach { v in
            v.translatesAutoresizingMaskIntoConstraints = false
        }
        
        // add contentView and stretchyView to the scroll view
        [stretchyView, contentView].forEach { v in
            scrollView.addSubview(v)
        }
        
        // add scroll view to self.view
        view.addSubview(scrollView)
        
        let safeG = view.safeAreaLayoutGuide
        let contentG = scrollView.contentLayoutGuide
        let frameG = scrollView.frameLayoutGuide
        
        // keep stretchyView's Top "pinned" to the Top of the scroll view FRAME
        //  so its Height will "stretch" when scroll view is pulled down
        let stretchyTop = stretchyView.topAnchor.constraint(equalTo: frameG.topAnchor, constant: 0.0)
        // priority needs to be less-than-required so we can "push it up" out of view
        stretchyTop.priority = .defaultHigh
        
        NSLayoutConstraint.activate([
            
            // scroll view Top to view Top
            scrollView.topAnchor.constraint(equalTo: view.topAnchor, constant: 0.0),

            // scroll view Leading/Trailing/Bottom to safe area
            scrollView.leadingAnchor.constraint(equalTo: safeG.leadingAnchor, constant: 0.0),
            scrollView.trailingAnchor.constraint(equalTo: safeG.trailingAnchor, constant: 0.0),
            scrollView.bottomAnchor.constraint(equalTo: safeG.bottomAnchor, constant: 0.0),
            
            // constrain stretchy view Top to scroll view's FRAME
            stretchyTop,
            
            // stretchyView to Leading/Trailing of scroll view FRAME
            stretchyView.leadingAnchor.constraint(equalTo: frameG.leadingAnchor, constant: 0.0),
            stretchyView.trailingAnchor.constraint(equalTo: frameG.trailingAnchor, constant: 0.0),
            
            // stretchyView Height - greater-than-or-equal-to
            //  so it can "stretch" vertically
            stretchyView.heightAnchor.constraint(greaterThanOrEqualToConstant: stretchyViewHeight),
            
            // content view Leading/Trailing/Bottom to scroll view's CONTENT GUIDE
            contentView.leadingAnchor.constraint(equalTo: contentG.leadingAnchor, constant: 0.0),
            contentView.trailingAnchor.constraint(equalTo: contentG.trailingAnchor, constant: 0.0),
            contentView.bottomAnchor.constraint(equalTo: contentG.bottomAnchor, constant: 0.0),

            // content view Width to scroll view's FRAME
            contentView.widthAnchor.constraint(equalTo: frameG.widthAnchor, constant: 0.0),

            // content view Top to scroll view's CONTENT GUIDE
            //  plus Height of stretchyView
            contentView.topAnchor.constraint(equalTo: contentG.topAnchor, constant: stretchyViewHeight),

            // content view Top to stretchyView Bottom
            contentView.topAnchor.constraint(equalTo: stretchyView.bottomAnchor, constant: 0.0),
    
        ])
        
        // add some content to the content view so we have something to scroll
        addSomeContent()
                
    }
    
    func addSomeContent() {
        // vertical stack view with 20 labels
        //  so we have something to scroll
        let stack = UIStackView()
        stack.axis = .vertical
        stack.spacing = 32
        stack.backgroundColor = .gray
        stack.translatesAutoresizingMaskIntoConstraints = false
        for i in 1...20 {
            let v = UILabel()
            v.text = "Label \(i)"
            v.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
            v.heightAnchor.constraint(equalToConstant: 48.0).isActive = true
            stack.addArrangedSubview(v)
        }
        contentView.addSubview(stack)
        NSLayoutConstraint.activate([
            stack.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 16.0),
            stack.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16.0),
            stack.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16.0),
            stack.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -16.0),
        ])
    }
    
}

Controller for each Page

class OnePageVC: UIViewController {
    
    var image: UIImage = UIImage() {
        didSet {
            imgView.image = image
        }
    }
    let imgView: UIImageView = {
        let v = UIImageView()
        v.backgroundColor = .systemBlue
        v.contentMode = .scaleAspectFill
        v.clipsToBounds = true
        v.translatesAutoresizingMaskIntoConstraints = false
        return v
    }()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        view.backgroundColor = .systemBackground
        
        view.addSubview(imgView)
        NSLayoutConstraint.activate([
            // constrain image view to all 4 sides
            imgView.topAnchor.constraint(equalTo: view.topAnchor, constant: 0.0),
            imgView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 0.0),
            imgView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: 0.0),
            imgView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 0.0),
        ])
    }
}

Sample Page View Controller

class SamplePageViewController: UIPageViewController, UIPageViewControllerDelegate, UIPageViewControllerDataSource {
    
    var controllers: [UIViewController] = []
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let imgNames: [String] = [
            "ex1", "ex2", "ex3",
        ]
        for i in 0..<imgNames.count {
            let aViewController = OnePageVC()
            if let img = UIImage(named: imgNames[i]) {
                aViewController.image = img
            }
            self.controllers.append(aViewController)
        }

        self.dataSource = self
        self.delegate = self
        
        self.setViewControllers([controllers[0]], direction: .forward, animated: false)
    }
    
    func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
        if let index = controllers.firstIndex(of: viewController), index > 0 {
            return controllers[index - 1]
        }
        return nil
    }
    
    func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
        if let index = controllers.firstIndex(of: viewController), index < controllers.count - 1 {
            return controllers[index + 1]
        }
        return nil
    }

}

Edit

Looking at the code you posted in your question's Edit... it's a little tough, since I don't know what your ProfileDetailBio view is, but here are a couple tips to help debug this type of situation during development:

  • give your views contrasting background colors... makes it easy to see the frames when you run the app
  • if a subview fills its superview's width, make it a little narrower so you can see what's "behind / under" it
  • set .clipsToBounds = true on views you're using as "containers" - such as contentView... if a subview is then "missing" you know it has extended outside the bounds of the container

So, for your code...

// so we can see the contentView frame
contentView.backgroundColor = .systemYellow

// leave some space on the right-side of bio view, so we
//   so we can see the contentView behind it
bio.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -100.0),

If you run the app, you will likely see that contentView only extends to the bottom of bio - not to the bottom of info.

If you then do this:

contentView.clipsToBounds = true

info will likely not be visible at all.

Checking your constraints, you have:

bio.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
        
// Info table
info.topAnchor.constraint(equalTo: bio.bottomAnchor, constant: 12.0),

where it should be:

// no bio bottom anchor
//bio.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),

// this is correct
// Info table
info.topAnchor.constraint(equalTo: bio.bottomAnchor, constant: 12.0),

// add this
info.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
        

Run the app, and you should now again see info, and contentView extends to the bottom of info.

Assuming bio and info height combined are tall enough to require scrolling, you can undo the "debug / dev" changes and you should be good to go.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文