I was trying to create an onboarding UI using UIKit and Storyboard. I had a requirement where some parts needed to be static / sticky and some parts had to scroll horizontally as the user swiped on the onboarding.
我正在尝试使用UIKit和故事板创建入职用户界面。我有一个要求,其中一些部件需要是静态/粘滞的,而一些部件必须在用户滑动入门时水平滚动。
This should give you a better idea:
这应该会给你一个更好的想法:
To achieve the above, I used the following view hierarchy set up in storyboard
为了实现上述目标,我使用了在故事板中设置的以下视图层次结构
UIViewController
UIViewController
- Skip Button
- Container View containing a UIPageViewController to swipe horizontally
- UIViewController
- UIScrollView (This is another scroll view, not the page view controller's)
- UIStackView
- UIImage (big red cross)
- UILabel
- UILabel
- UIStackView
- Page Indicator
- Subscribe button
- Login button
While this works fine in a normal set up, I have the need to support dynamic / large text which the user can update from settings / control panel.
虽然这在正常设置中运行良好,但我需要支持动态/大文本,用户可以从设置/控制面板更新这些文本。
The issue I have now is that the content grows within the scrollview inside the UIPageViewController as shown
我现在遇到的问题是,内容在UIPageView控制器内的滚动视图中增长,如下所示
The white box above is not part of the UI but just me trying to show that the vertical scrolling is restricted to that area.
上面的白框不是用户界面的一部分,而只是我试图显示垂直滚动仅限于该区域。
I understand that because of my set up, this is the correct behavior.
我明白,由于我的安排,这是正确的行为。
My question / objective:
What would be a better layout structure so that the whole view grows when larger text fonts are activated such that the view scrolls as a whole rather than the scrolling being restricted to that small portion of the screen which would be mean the page indicator and bottom buttons going below the screen for larger text sizes.
我的问题/目标:什么是更好的布局结构,以便当激活较大的文本字体时,整个视图会增长,从而使视图作为一个整体滚动,而不是将滚动限制在屏幕的一小部分,这意味着页面指示器和底部按钮在屏幕下方显示较大的文本大小。
Please bear in mind that only the image, title and description should be horizontally scrollable.
请记住,只有图像、标题和描述应该是水平滚动的。
I tried to bring the scrollview and stackview out of the PageViewController to set up something like this:
我尝试将Scrollview和Stackview从PageView控制器中移出,以设置类似以下内容:
UIViewController
UIView控制器
- UIScrollView
- UIStackView
- Skip Button
- Container View containing a UIPageViewController to swipe horizontally
- UIViewController
- UIStackView
- UIImage (big red cross)
- UILabel
- UILabel
- UIStackView
- Page Indicator
- Subscribe button
- Login button
However, since I'm using autolayout, I get an error in the storyboard that the page view controller's height couldn't be determined.
但是,由于我使用的是自动布局,故事板中出现了无法确定页面视图控制器高度的错误。
What would be the right way to set this up in storyboard or programatically if not possible via storyboard ?
在故事板中设置它的正确方式是什么?如果不能通过故事板进行设置,那么通过编程设置它的正确方式是什么?
I'm open to switching to tableview or collectionview if that makes life easier.
我对切换到表格视图或收藏视图持开放态度,如果这样做会让生活更轻松的话。
更多回答
This seems like more of a Design question than a code / layout management question. For example - do you really want to allow your "Onboarding 1" label font to grow to that size? That is, will it be "user friendly" to see the word broken like that? Are you embedding the vertical scrollview because your "description" will potentially be many lines? Would it make more sense to vertically scroll only the text?
这看起来更像是一个设计问题,而不是代码/布局管理问题。例如,你真的想让你的“入职1”标签字体增大到那个大小吗?也就是说,看到这个词这样被打断会不会对用户友好呢?您嵌入垂直滚动视图是因为您的“描述”可能会有很多行吗?只垂直滚动文本是否更有意义?
@DonMag The title and description could potentially be multiple lines yes, however, we try to keep it to 1-2 lines max so as to avoid the need to scroll. The main reason for the scrollview is to allow the text size to grow when the user selects a bigger text size from settings / control center. If the text size is set to 150 - 300%, the text requires to be scrolled. Rather than just scrolling the text, I would prefer the whole view to be scrollable.
@DonMag标题和描述可能是多行的,但是,我们尽量将其保持在1-2行,以避免滚动的需要。滚动视图的主要原因是当用户从设置/控制中心选择更大的文本大小时,允许文本大小增长。如果文本大小设置为150 - 300%,则文本需要滚动。与其只是滚动文本,我更希望整个视图都可以滚动。
Is this close to your goal? i.stack.imgur.com/E7H0d.png ... page view controller frame has constant height, regardless of vertical-scrolling text inside it?
这离你的目标近吗?I.stack.imgur.com/E7H0d.png...页面视图控制器框架具有恒定的高度,而不考虑其中的垂直滚动文本?
@DonMag - Thanks for the illustration, it's almost what I want. I would like to remove the scroll view inside the PageVC and so the PageVC can grow based on the content. The only scrollview is the one outside which you marked in yellow so that the whole view scrolls in the safe area. When the content in the PageVC grows, the buttons etc would get pushed further down in the yellow scrollview from your illustration. Hope that makes sense ?
@DonMag-谢谢你的插图,这几乎就是我想要的。我想删除PageVC内的滚动视图,以便PageVC可以根据内容进行增长。唯一的滚动视图是您在其外部标记为黄色的滚动视图,以便整个视图在安全区域中滚动。当PageVC中的内容增长时,按钮等将在插图中的黄色滚动视图中进一步向下按下。希望这说得通吧?
So, you want the PageVC height to change based on the page content... What should happen to the buttons as the user swipes between pages? That brings us back to design question... Take a look at this image - I think you'll see what I mean (really, really big image): i.stack.imgur.com/wVfvB.jpg
因此,您希望PageVC高度根据页面内容进行更改。当用户在页面之间滑动时,按钮应该发生什么变化?这让我们回到了设计问题上。看看这张图片-我想你会明白我的意思(非常,非常大的图片):i.stack.imgur.com/wVfvB.jpg
After comment discussion, I think I understand your ultimate goal.
经过评论讨论,我想我理解你的最终目标。
You're creating an "Onboarding" UI, so I'll make a few assumptions...
您正在创建一个“入职”用户界面,所以我将做一些假设……
- likely not dozens of "pages"
- likely fairly "static" pages (no or very little user interaction on each "page")
Therefore, we shouldn't have to worry about memory issues if the "pages" are not dynamically allocated.
因此,如果“页面”不是动态分配的,我们就不必担心内存问题。
A UIPageViewController
is a nice component -- but, it does a LOT "behind-the-scenes." In particular, it sets the "page frames" to match its frame. In your case, you want its frame to grow in height to match the pages height.
UIPageView控制器是一个很好的组件--但是,它在“幕后”做了很多事情。特别是,它设置了与其框架相匹配的“页面框架”。在您的例子中,您希望它的框架高度增长以匹配页面高度。
That probably could be done, but it will be much easier to NOT use a page view controller.
这可能是可以做到的,但不使用页面视图控制器会容易得多。
Instead, let's use a standard UIScrollView
.
相反,让我们使用标准的UIScrollView。
First, if we embed a UILabel
in a UIView
, and constrain the Height of the view equal to the Height of the label (assuming we have 8-point constraints from the table to the view):
首先,如果我们在UIView中嵌入一个UILabel,并将视图的高度约束为等于标签的高度(假设从表到视图有8点约束):
someView.heightAnchor.constraint(equalTo: someLabel.heightAnchor, constant: 16.0).isActive = true
the Yellow view will grow in height as the label grows:
随着标签的增加,黄色视图的高度也会随之增加:
We can do the same thing with a UIScrollView
(red-dashed outline is the scroll view frame):
我们可以对UIScrollView执行相同的操作(红色虚线轮廓是滚动视图框):
The scroll view frame Height grows with the label height, and we can still scroll horizontally but not vertically.
滚动视图框高度随着标签高度的增加而增加,我们仍然可以水平滚动,但不能垂直滚动。
So, let's start by putting the "pages" into a horizontal stack view:
因此,让我们从将“页面”放入水平堆栈视图开始:
and then embed that stack view in a scroll view.
然后将该堆栈视图嵌入滚动视图中。
If we constrain each page Width to the Width of the scroll view, and set a Height constraint on the scroll view, we'll get this (thick black dashed outline is the scroll view frame):
如果我们将每个页面宽度限制为滚动视图的宽度,并在滚动视图上设置高度约束,则会得到以下结果(粗黑虚线轮廓是滚动视图框):
So far, nothing special.
到目前为止,没有什么特别的。
However, if the page heights change - such as using .adjustsFontForContentSizeCategory - true
on the labels - without doing anything else - we get this:
但是,如果页面高度发生更改-例如在标签上使用.adjustsFontFortentSizeCategory-true-不执行任何其他操作-我们将得到以下结果:
which results in vertical as well as horizontal scrolling -- as expected.
这导致了垂直和水平滚动--正如预期的那样。
For the target layout, though, we want the scroll view's Height to grow to match the new page Height.
但是,对于目标布局,我们希望滚动视图的高度增长到与新页面高度匹配。
Let's constrain the scroll view's Height to the stack view's Height:
让我们将滚动视图的高度限制为堆栈视图的高度:
NSLayoutConstraint.activate([
// "normal" Top/Leading/Trailing constraints
pageScrollView.topAnchor.constraint(equalTo: view.topAnchor),
pageScrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
pageScrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
// instead of setting a Height related to the view, or a constant value
// we'll constrain the Height to the *stack view's* height
pageScrollView.heightAnchor.constraint(equalTo: pageStackView.heightAnchor),
])
Now, as our "pages" grow in height, the scroll view frame will also grow:
现在,随着我们的“页面”高度的增加,滚动视图框也将增长:
Once we embed our pageScrollView
in an "outer" scroll view, and constrain it relative to the other "outer" UI elements, we'll reach the target layout.
一旦我们将pageScrollView嵌入到“外部”滚动视图中,并将其相对于其他“外部”UI元素进行约束,我们就可以到达目标布局。
Solid black outline is the "iPhone" frame; yellow long-dash outline is the "outer" scroll view frame; white dash outline is the "page" scroll view frame:
黑色实心轮廓是“iPhone”边框;黄色长虚线轮廓是“外层”滚动视图框;白色虚线轮廓是“页面”滚动视图框:
As the "page" height grows...
随着“页面”高度的增长。
we'll be able to scroll the entire "outer view" vertically...
我们将能够垂直滚动整个“外景”...
and continue to scroll the "pages" horizontally.
并继续水平滚动“页面”。
The end result - (scaled way down so it will post here):
最终结果-(按比例缩小,以便在此处发布):
And, finally, some sample code. Everything is done via code, so no complicated Storyboard designing or @IBOutlet
/ @IBAction
connections needed:
最后,还有一些示例代码。一切都是通过代码完成的,因此不需要复杂的故事板设计或@IBOutlet/@IBAction连接:
Simple struct for the "page" data:
“页面”数据的简单结构:
struct PageData {
var title: String = ""
var desc: String = ""
// maybe different images on each page?
var imgName: String = ""
}
Sample "page" view controller - which we'll add as children and grab the views. ImageView, Title and Description labels:
示例“页面”视图控制器--我们将作为子级添加它并获取视图。ImageView、标题和描述标签:
class SamplePageVC: UIViewController {
let titleLabel: UILabel = UILabel()
let descLabel: UILabel = UILabel()
let imgView: UIImageView = UIImageView()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .clear
guard let customFont1 = UIFont(name: "TimesNewRomanPSMT", size: 32.0),
let customFont2 = UIFont(name: "Verdana", size: 16.0)
else {
fatalError("Could not load font!")
}
titleLabel.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: customFont1)
descLabel.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: customFont2)
[titleLabel, descLabel].forEach { v in
v.adjustsFontForContentSizeCategory = true
v.textAlignment = .center
v.textColor = .white
v.numberOfLines = 0
v.setContentHuggingPriority(.required, for: .vertical)
v.setContentCompressionResistancePriority(.required, for: .vertical)
}
// might want to set a MAX Content Size Category?
//view.maximumContentSizeCategory = .accessibilityExtraLarge
let stackView = UIStackView()
stackView.axis = .vertical
stackView.alignment = .center
stackView.spacing = 6
[imgView, titleLabel, descLabel].forEach { v in
stackView.addArrangedSubview(v)
}
stackView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(stackView)
let g = view.safeAreaLayoutGuide
let bc: NSLayoutConstraint = stackView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -8.0)
bc.priority = .required - 1
NSLayoutConstraint.activate([
stackView.topAnchor.constraint(equalTo: g.topAnchor, constant: 0.0),
stackView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
stackView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
stackView.bottomAnchor.constraint(lessThanOrEqualTo: g.bottomAnchor, constant: -8.0),
bc,
imgView.widthAnchor.constraint(equalTo: stackView.widthAnchor, constant: -60.0),
imgView.heightAnchor.constraint(equalTo: imgView.widthAnchor, multiplier: 1.0),
titleLabel.widthAnchor.constraint(equalTo: stackView.widthAnchor, constant: -20.0),
descLabel.widthAnchor.constraint(equalTo: stackView.widthAnchor, constant: -20.0),
])
}
}
Sample "onboarding" view controller - uses everything discussed above:
示例“入职”视图控制器-使用上面讨论的所有内容:
class OnboardingVC: UIViewController, UIScrollViewDelegate {
let skipBtn: UIButton = UIButton()
let subscribeBtn: UIButton = UIButton()
let loginBtn: UIButton = UIButton()
let pgCtrl: UIPageControl = UIPageControl()
let outerScrollView: UIScrollView = UIScrollView()
let outerContentView: UIView = UIView()
let pageScrollView: UIScrollView = UIScrollView()
let pageStackView: UIStackView = UIStackView()
var pageData: [PageData] = [
PageData(title: "First Page", desc: "Some description about it.", imgName: "pgXC"),
PageData(title: "Short", desc: "This page has somewhat longer description text.", imgName: ""),
PageData(title: "A Longer Title", desc: "This page will have even more text in the description label. That will help demonstrate the height matching and resulting layout / scrolling changes.", imgName: ""),
PageData(title: "Final", desc: "This is the last page.", imgName: ""),
]
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .init(red: 1.0, green: 0.25, blue: 0.2, alpha: 1.0)
guard let skipFont = UIFont(name: "Verdana", size: 17.0),
let btnFont = UIFont(name: "Verdana-Bold", size: 16.0)
else {
fatalError("Could not load font!")
}
skipBtn.titleLabel?.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: skipFont)
subscribeBtn.titleLabel?.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: btnFont)
loginBtn.titleLabel?.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: btnFont)
for (btn, str) in zip([skipBtn, subscribeBtn, loginBtn], ["Skip", "Subscribe", "Login"]) {
btn.titleLabel?.adjustsFontForContentSizeCategory = true
btn.setTitle(str, for: [])
btn.layer.cornerRadius = 6
}
skipBtn.setTitleColor(.white, for: .normal)
skipBtn.setTitleColor(.lightGray, for: .highlighted)
skipBtn.setContentCompressionResistancePriority(.required, for: .vertical)
subscribeBtn.setTitleColor(.blue, for: .normal)
subscribeBtn.setTitleColor(.systemBlue, for: .highlighted)
subscribeBtn.backgroundColor = .white
loginBtn.setTitleColor(.white, for: .normal)
loginBtn.setTitleColor(.lightGray, for: .highlighted)
loginBtn.layer.borderColor = UIColor.white.cgColor
loginBtn.layer.borderWidth = 1
let btnStack: UIStackView = UIStackView()
btnStack.axis = .vertical
btnStack.spacing = 12
// let's add top and bottom padding for the buttons
// we're not using UIButtonConfiguration so we ignore the deprecation warnings
[subscribeBtn, loginBtn].forEach { v in
var edges = v.contentEdgeInsets
edges.top = 12.0
edges.bottom = 12.0
v.contentEdgeInsets = edges
}
// add Page Control and bottom buttons to vertical stack view
// set Hugging and Compression priorities to .required so they
// won't stretch or collapse vertically
[pgCtrl, subscribeBtn, loginBtn].forEach { v in
btnStack.addArrangedSubview(v)
v.setContentHuggingPriority(.required, for: .vertical)
v.setContentCompressionResistancePriority(.required, for: .vertical)
}
// add "pages" stack view to "page" scroll view
pageStackView.translatesAutoresizingMaskIntoConstraints = false
pageScrollView.addSubview(pageStackView)
// add elements to outerContentView
[skipBtn, pageScrollView, btnStack].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
outerContentView.addSubview(v)
}
// add outerContentView to outerScrollView
outerContentView.translatesAutoresizingMaskIntoConstraints = false
outerScrollView.addSubview(outerContentView)
// add outerScrollView to (self) view
outerScrollView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(outerScrollView)
let g = view.safeAreaLayoutGuide
var cg = pageScrollView.contentLayoutGuide
var fg = pageScrollView.frameLayoutGuide
NSLayoutConstraint.activate([
// constrain all 4 sides of pageStackView to pageScrollView.contentLayoutGuide
pageStackView.topAnchor.constraint(equalTo: cg.topAnchor, constant: 0.0),
pageStackView.leadingAnchor.constraint(equalTo: cg.leadingAnchor, constant: 0.0),
pageStackView.trailingAnchor.constraint(equalTo: cg.trailingAnchor, constant: 0.0),
pageStackView.bottomAnchor.constraint(equalTo: cg.bottomAnchor, constant: 0.0),
// constrain skip button Top/Trailing
skipBtn.topAnchor.constraint(equalTo: outerContentView.topAnchor, constant: 12.0),
skipBtn.trailingAnchor.constraint(equalTo: outerContentView.trailingAnchor, constant: -40.0),
// constrain pageScrollView
// Top 8-points below skip button Bottom
// Leading/Trailing to outerContentView (so, full width)
pageScrollView.topAnchor.constraint(equalTo: skipBtn.bottomAnchor, constant: 8.0),
pageScrollView.leadingAnchor.constraint(equalTo: outerContentView.leadingAnchor, constant: 0.0),
pageScrollView.trailingAnchor.constraint(equalTo: outerContentView.trailingAnchor, constant: 0.0),
// constrain pageScrollView HEIGHT to pageStackView HEIGHT
// now, the scroll view Height will match the "pages" height
pageScrollView.heightAnchor.constraint(equalTo: pageStackView.heightAnchor, constant: 0.0),
// constrain btnStack
// Top >= pageScrollView Bottom plus a little "padding space"
// Leading/Trailing to outerContentView plus a little "padding space" on the sides
// Bottom to outerContentView plus a little "padding space"
btnStack.topAnchor.constraint(greaterThanOrEqualTo: pageScrollView.bottomAnchor, constant: 12.0),
btnStack.leadingAnchor.constraint(equalTo: outerContentView.leadingAnchor, constant: 20.0),
btnStack.trailingAnchor.constraint(equalTo: outerContentView.trailingAnchor, constant: -20.0),
btnStack.bottomAnchor.constraint(equalTo: outerContentView.bottomAnchor, constant: -12.0),
])
cg = outerScrollView.contentLayoutGuide
fg = outerScrollView.frameLayoutGuide
// we want outerContentView to be the same Height as outerScrollView
// but less-than-required Priority so it can grow based on its subviews
let hc: NSLayoutConstraint = outerContentView.heightAnchor.constraint(equalTo: fg.heightAnchor, constant: 0.0)
hc.priority = .required - 1
NSLayoutConstraint.activate([
// constrain all 4 sides of outerContentView to outerScrollView.contentLayoutGuide
outerContentView.topAnchor.constraint(equalTo: cg.topAnchor, constant: 0.0),
outerContentView.leadingAnchor.constraint(equalTo: cg.leadingAnchor, constant: 0.0),
outerContentView.trailingAnchor.constraint(equalTo: cg.trailingAnchor, constant: 0.0),
outerContentView.bottomAnchor.constraint(equalTo: cg.bottomAnchor, constant: 0.0),
// outerContentView Width to outerScrollView.frameLayoutGuide Width
// so we will never get horizontal scrolling
outerContentView.widthAnchor.constraint(equalTo: fg.widthAnchor, constant: 0.0),
hc,
// constrain all 4 sides of outerScrollView to (self) view
outerScrollView.topAnchor.constraint(equalTo: g.topAnchor, constant: 0.0),
outerScrollView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
outerScrollView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
outerScrollView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: 0.0),
])
// add page view VCs for each data item
// we could do this with a "SamplePageView" UIView subclass
// but this shows how to use UIViewController if that's how the "pages" are setup
for (idx, d) in pageData.enumerated() {
let vc = SamplePageVC()
addChild(vc)
// add its view to pageStackView
pageStackView.addArrangedSubview(vc.view)
vc.didMove(toParent: self)
vc.titleLabel.text = d.title
vc.descLabel.text = d.desc
if !d.imgName.isEmpty, let img = UIImage(named: d.imgName) {
vc.imgView.image = img
} else if let img = UIImage(systemName: "\(idx).circle.fill") {
vc.imgView.image = img
vc.imgView.tintColor = .orange
}
// each page view Width is equal to pageScrollView.frameLayoutGuide width
vc.view.widthAnchor.constraint(equalTo: pageScrollView.frameLayoutGuide.widthAnchor, constant: 0.0).isActive = true
}
// we *probably* do not want to see scroll indicators
outerScrollView.showsHorizontalScrollIndicator = false
outerScrollView.showsVerticalScrollIndicator = false
pageScrollView.showsHorizontalScrollIndicator = false
pageScrollView.showsVerticalScrollIndicator = false
// enable paging for the "pages"
pageScrollView.isPagingEnabled = true
// we will implement scrollViewDidScroll() so we can update the page control
// when the user drags the pages left/right
pageScrollView.delegate = self
pgCtrl.numberOfPages = pageData.count
pgCtrl.addTarget(self, action: #selector(changePage(_:)), for: .valueChanged)
}
@objc func changePage(_ sender: UIPageControl) {
let x: CGFloat = pageScrollView.frame.width * CGFloat(sender.currentPage)
UIView.animate(withDuration: 0.3, animations: {
self.pageScrollView.contentOffset.x = x
})
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView == pageScrollView {
let pg: Int = Int(floor((scrollView.contentOffset.x + scrollView.frame.width * 0.5) / scrollView.frame.width))
pgCtrl.currentPage = pg
}
}
}
Sample Dev Mode "onboarding" view controller - subclass of OnboardingVC
the colorizes and outlines the UI elements. It will produce the screen-caps shown above (when run on an iPad) to make it easier to see what's going on:
示例开发模式“入职”视图控制器-OnboardingVC的子类对UI元素进行着色和轮廓。它将生成上面所示的屏幕(当在iPad上运行时),以便更容易地看到正在发生的事情:
// MARK: subclass of OnboardingVC
// for use during development
// colorizes and outlines view elements
// centers a "simulated" device frame so we can see "outside the frame"
class DevOnboardingVC: OnboardingVC {
override func viewDidLoad() {
super.viewDidLoad()
let g = view.safeAreaLayoutGuide
// we need to re-build the outerScrollView constraints
outerScrollView.removeFromSuperview()
view.addSubview(outerScrollView)
// we'll make the "device frame"
// 90% of the view height, or
// 640-points, whichever is smaller
let targetHeightC: NSLayoutConstraint = outerScrollView.heightAnchor.constraint(equalTo: g.heightAnchor, multiplier: 0.9)
targetHeightC.priority = .required - 1
let maxHeightC: NSLayoutConstraint = outerScrollView.heightAnchor.constraint(lessThanOrEqualToConstant: 640.0)
NSLayoutConstraint.activate([
outerScrollView.centerXAnchor.constraint(equalTo: g.centerXAnchor),
outerScrollView.centerYAnchor.constraint(equalTo: g.centerYAnchor),
outerScrollView.widthAnchor.constraint(equalToConstant: 300.0),
maxHeightC, targetHeightC,
])
view.backgroundColor = UIColor(white: 0.90, alpha: 1.0)
outerContentView.backgroundColor = .init(red: 1.0, green: 0.25, blue: 0.2, alpha: 1.0)
outerScrollView.clipsToBounds = false
pageScrollView.clipsToBounds = false
pageStackView.arrangedSubviews.forEach { v in
v.backgroundColor = .white.withAlphaComponent(0.5)
v.backgroundColor = .init(red: 1.0, green: 0.25, blue: 0.2, alpha: 1.0).withAlphaComponent(0.5)
v.layer.borderColor = UIColor.systemBlue.cgColor
v.layer.borderWidth = 2
}
self.children.forEach { vc in
if let vc = vc as? SamplePageVC {
vc.titleLabel.backgroundColor = .systemGreen
vc.descLabel.backgroundColor = .systemGreen
}
}
let dashV1 = DashedBorderView()
dashV1.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(dashV1)
let dashV2 = DashedBorderView()
dashV2.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(dashV2)
let dashV3 = DashedBorderView()
dashV3.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(dashV3)
NSLayoutConstraint.activate([
dashV1.topAnchor.constraint(equalTo: outerScrollView.topAnchor, constant: 0.0),
dashV1.leadingAnchor.constraint(equalTo: outerScrollView.leadingAnchor, constant: 0.0),
dashV1.trailingAnchor.constraint(equalTo: outerScrollView.trailingAnchor, constant: 0.0),
dashV1.bottomAnchor.constraint(equalTo: outerScrollView.bottomAnchor, constant: 0.0),
dashV2.topAnchor.constraint(equalTo: outerScrollView.topAnchor, constant: 0.0),
dashV2.leadingAnchor.constraint(equalTo: outerScrollView.leadingAnchor, constant: 0.0),
dashV2.trailingAnchor.constraint(equalTo: outerScrollView.trailingAnchor, constant: 0.0),
dashV2.bottomAnchor.constraint(equalTo: outerScrollView.bottomAnchor, constant: 0.0),
dashV3.topAnchor.constraint(equalTo: pageScrollView.topAnchor, constant: 0.0),
dashV3.leadingAnchor.constraint(equalTo: pageScrollView.leadingAnchor, constant: 0.0),
dashV3.trailingAnchor.constraint(equalTo: pageScrollView.trailingAnchor, constant: 0.0),
dashV3.bottomAnchor.constraint(equalTo: pageScrollView.bottomAnchor, constant: 0.0),
])
dashV1.color = .black
dashV1.lineWidth = 16
dashV1.position = .outside
dashV1.dashPattern = []
dashV2.color = .systemYellow
dashV2.lineWidth = 3
dashV2.dashPattern = [60, 8]
dashV3.color = .white
dashV3.lineWidth = 2
}
}
Dashed Border View - used by the "Dev Mode" class:
虚线边框视图-由“开发人员模式”类使用:
class DashedBorderView: UIImageView {
enum BorderPosition {
case inside, middle, outside
}
public var position: BorderPosition = .middle { didSet { setNeedsLayout() } }
public var dashPattern: [NSNumber] = [16, 16] { didSet { dashedLineLayer.lineDashPattern = dashPattern } }
public var lineWidth: CGFloat = 1.0 { didSet { dashedLineLayer.lineWidth = lineWidth } }
public var color: UIColor = .red { didSet { dashedLineLayer.strokeColor = color.cgColor } }
override class var layerClass: AnyClass { CAShapeLayer.self }
var dashedLineLayer: CAShapeLayer { layer as! CAShapeLayer }
init() {
super.init(frame: .zero)
commonInit()
}
override init(image: UIImage?) {
super.init(image: image)
commonInit()
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() {
// this view will usually be overlaid on top of an interactive view
// so disable by default
isUserInteractionEnabled = false
dashedLineLayer.fillColor = UIColor.clear.cgColor
dashedLineLayer.strokeColor = color.cgColor
dashedLineLayer.lineWidth = lineWidth
dashedLineLayer.lineDashPattern = dashPattern
}
override func layoutSubviews() {
super.layoutSubviews()
switch position {
case .inside:
dashedLineLayer.path = CGPath(rect: bounds.insetBy(dx: lineWidth * 0.5, dy: lineWidth * 0.5), transform: nil)
case .outside:
dashedLineLayer.path = CGPath(rect: bounds.insetBy(dx: -lineWidth * 0.5, dy: -lineWidth * 0.5), transform: nil)
case .middle:
dashedLineLayer.path = CGPath(rect: bounds, transform: nil)
}
}
}
Important Notes
重要说明
- This is Example Code Only!!! It is not intended to be, nor should be considered, "Production Ready"
- Design is for iPhone Portrait orientation... it will still work on an iPad, or on a phone rotated to Landscape, but I promise it won't look good :)
I also posted a project with all the above code here: https://github.com/DonMag/PageViewApproach
我还在这里发布了一个包含所有上述代码的项目:https://github.com/DonMag/PageViewApproach
Edit - a little more explanation...
编辑-更多的解释...
Big benefits of using a UIPageViewController
or UICollectionView
(whether using flow, custom or compositional layout) is memory management ... you could have 100s of "pages" and not have to worry.
使用UIPageView控制器或UICollectionView(无论是使用流、自定义还是组合布局)的最大好处是内存管理……你可以有100页的“页面”,而不必担心。
With this Layout goal, though, the dynamic height is always going to be the issue... because both of those classes are also designed to "layout their pages / cells" based on the frame we set for the PageViewController or CollectionView.
不过,有了这个布局目标,动态高度始终是问题所在。因为这两个类也被设计为基于我们为PageView控制器或CollectionView设置的框架来“布局它们的页面/单元格”。
Consider 5 "pages" that look like this:
考虑如下所示的5个“页面”:
In a page view controller or a collection view, we can't set the frame height (yellow rect) because we don't know the "max height needed" until Page 4 is instantiated.
在页面视图控制器或集合视图中,我们不能设置框架高度(黄色矩形),因为在实例化Page 4之前,我们不知道“所需的最大高度”。
We could calculate the max height on load, for example:
我们可以计算负载的最大高度,例如:
func getMaxHeight() -> CGFloat {
var maxHeight: CGFloat = 0.0
for i in 0..<numPages {
let v = MyPageView()
v.fillLabels(forPage: i)
let sz = v.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
maxHeight = max(maxHeight, sz.height)
}
return maxHeight
}
Or, if we have "cached" the page views in memory:
或者,如果我们在内存中“缓存”了页面视图:
func getMaxHeight() -> CGFloat {
var maxHeight: CGFloat = 0.0
pageViews.forEach { v in
let sz = v.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
maxHeight = max(maxHeight, sz.height)
}
return maxHeight
}
And because you're using Dynamic Type, we'd also need to call that on UITraitCollection.preferredContentSizeCategory
change and update the frame height.
因为您使用的是Dynamic Type,所以我们还需要在UITraitCollection.preferredContentSizeCategory Change上调用它并更新框架高度。
Personally, I would go with the scrollview approach, and simply let auto-layout handle it for me.
就我个人而言,我会采用滚动视图的方法,让自动布局为我处理它。
更多回答
Thanks a lot DonMag, amazing answer and explanation - exactly what I was trying to achieve. Really appreciate the time and effort you put into this. Your assumptions are right that we won't have more than 5-6 pages. As a final question, do you think something similar could also be achieved with a UICollectionView Compositional layout ? Thanks once again, really appreciate this.
非常感谢DonMag,令人惊叹的回答和解释--这正是我想要实现的。真的很感谢你在这件事上花费的时间和精力。你的假设是正确的,我们不会有超过5-6页。作为最后一个问题,您认为UICollectionView的构图布局是否也可以实现类似的效果?再次感谢,真的很感激。
@ShawnFrank - see the Edit at the bottom of my answer for additional explanation.
@ShawnFrank-有关更多说明,请参阅我答案底部的编辑。
Perfect, amazing answer, covers all basis and I've learn a ton from you - Thanks DonMag !
完美,令人惊叹的答案,涵盖了所有的基础,我从你那里学到了很多-谢谢DonMag!
我是一名优秀的程序员,十分优秀!