带有 UIPageViewController 的弹性标题
我的问题似乎很明显,而且重复了,但我无法设法使它起作用。
我正在尝试实现著名的弹力标头效果(滚动时,图像的顶部贴在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并不感到惊讶,它也行不通)。
(我如何实现我想要的东西?
编辑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 NSLayoutConstraint
s (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 NSLayoutConstaint
s 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:
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 技术交流群。

绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(1)
您的视图层次结构应该是:
要使可拉伸视图“粘在顶部”:
我们将可拉伸视图的顶部限制为滚动视图的
.frameLayoutGuide
顶部,但我们给该约束赋予小于-需要.priority
,这样我们就可以将其“推”到屏幕上。我们还为可拉伸视图设置了大于或等于 350 的高度约束。这将允许其垂直拉伸(但不能压缩)。
我们将 UIHostingViewController 中的视图称为我们的“contentView”...并且我们将其顶部约束到可拉伸视图的底部。
然后,我们给内容视图另一个顶部约束——这次是滚动视图的
.contentLayoutGuide
,常量为 350(可拉伸视图的高度)。这加上前导/尾随/底部约束定义了“可滚动区域”。当我们向下滚动(拉)时,内容视图将“拉下”可拉伸视图的底部。
当我们向上滚动(推动)时,内容视图将“推动”整个拉伸视图。
它的外观如下(太大,无法在此处添加为 gif): https://i.sstatic.net /4wvxK.jpg
下面是实现该功能的示例代码。一切都是通过代码完成的,因此不需要
@IBOutlet
或其他连接。另请注意,我使用了三个图像作为页面视图 -“ex1”、“ex2”、“ex3”:视图控制器
每个页面的控制器
示例页面视图控制器
编辑
查看您在问题的编辑中发布的代码...这有点困难,因为我不知道您的
ProfileDetailBio
视图是什么,但这里有一些技巧可以帮助调试这种情况在开发过程中:contentView
...如果子视图“丢失”,您知道它已扩展到边界之外的 如果因此,对于您的代码...
您运行应用程序,您可能会看到
contentView
仅延伸到bio
的底部 - 而不是的底部>信息。
如果您随后执行此操作:
info
可能根本不可见。检查你的约束,你有:
它应该在哪里:
运行应用程序,你应该现在再次看到
info
,并且contentView
扩展到信息
底部。假设
bio
和info
的高度组合起来足够高,需要滚动,您可以撤消“debug / dev”更改,然后就可以开始了。Your view hierarchy should be:
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
Controller for each Page
Sample Page View Controller
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:.clipsToBounds = true
on views you're using as "containers" - such ascontentView
... if a subview is then "missing" you know it has extended outside the bounds of the containerSo, for your code...
If you run the app, you will likely see that
contentView
only extends to the bottom ofbio
- not to the bottom ofinfo
.If you then do this:
info
will likely not be visible at all.Checking your constraints, you have:
where it should be:
Run the app, and you should now again see
info
, andcontentView
extends to the bottom ofinfo
.Assuming
bio
andinfo
height combined are tall enough to require scrolling, you can undo the "debug / dev" changes and you should be good to go.