How can a view determine its height based on a function of its width without using an intrinsicContentSize that is dependent on the frame? - ios

I am trying to figure out how best to create a view that determines its own height based on the width it is given. The behaviour I desire is very similar to how a vertical UIStackView behaves in that:
When constrained by its top, leading, and trailing edges, it should determine its own natural height based on its content.
When constrained on all edges, it should fill all the available space by expanding and collapsing as determined by those constraints.
In addition, I am looking to achieve this without using autolayout internally to my view.
To illustrate this, please consider the following example:
class ViewController: UIViewController {
let v = View()
override func viewDidLoad() {
super.viewDidLoad()
v.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(v)
NSLayoutConstraint.activate([
v.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
v.leadingAnchor.constraint(equalTo: view.leadingAnchor),
v.trailingAnchor.constraint(equalTo: view.trailingAnchor),
// Toggle this constraint on and off.
// v.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
])
}
}
class View: UIView {
// Imagine this came from computing the size of some child views
// and that it is relative to the width of the bounds.
var contentHeight: CGFloat { bounds.width * 0.5 }
var boundsWidth: CGFloat = 0 {
didSet { if oldValue != boundsWidth { invalidateIntrinsicContentSize() } }
}
override var intrinsicContentSize: CGSize {
.init(width: bounds.width, height: contentHeight)
}
override func layoutSubviews() {
super.layoutSubviews()
boundsWidth = bounds.width
backgroundColor = .red
}
}
This example achieves the behaviour I've described above but I have a big reservation with it because of how I'm using intrinsicContentSize.
The documentation for instrinsicContentSize states that it must be independent of the content frame, which I have not managed. I am calculating the intrinsic size based on the width of the frame.
How is it possible to achieve this behaviour and not make instrinsicContentSize rely on the bounds?

This can all be done with auto-layout constraints. No need to calculate anything.
All you need to do is make sure your custom view's content has appropriate constraints to define the layout.
For example, a UILabel has an intrinsic size based on its text. You can constrain a "top" label to the top of the view, a "middle" label to the bottom of the "top" label, and a "bottom" label to the bottom of the "middle" label and to the bottom of the view.
Here's an example (all via code):
class SizeTestViewController: UIViewController {
let v = ExampleView()
override func viewDidLoad() {
super.viewDidLoad()
v.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(v)
NSLayoutConstraint.activate([
v.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
v.leadingAnchor.constraint(equalTo: view.leadingAnchor),
v.trailingAnchor.constraint(equalTo: view.trailingAnchor),
])
v.topLabel.text = "This is the top label."
v.middleLabel.text = "This is a bunch of text for the middle label. Since we have it set to numberOfLines = 0, the text will wrap onto mutliple lines (assuming it needs to)."
v.bottomLabel.text = "This is the bottom label text\nwith embedded newline characters\nso we can see the multiline feature without needing word wrap."
// so we can see the view's frame
v.backgroundColor = .red
}
}
class ExampleView: UIView {
var topLabel: UILabel = {
let v = UILabel()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = .yellow
v.numberOfLines = 0
return v
}()
var middleLabel: UILabel = {
let v = UILabel()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = .cyan
v.numberOfLines = 0
return v
}()
var bottomLabel: UILabel = {
let v = UILabel()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = .green
v.numberOfLines = 0
return v
}()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
addSubview(topLabel)
addSubview(middleLabel)
addSubview(bottomLabel)
NSLayoutConstraint.activate([
// constrain topLabel 8-pts from top, leading, trailing
topLabel.topAnchor.constraint(equalTo: topAnchor, constant: 8.0),
topLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 8.0),
topLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -8.0),
// constrain middleLabel 8-pts from topLabel
// 8-pts from leading, trailing
middleLabel.topAnchor.constraint(equalTo: topLabel.bottomAnchor, constant: 8.0),
middleLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 8.0),
middleLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -8.0),
// constrain bottomLabel 8-pts from middleLabel
// 8-pts from leading, trailing
// 8-pts from bottom
bottomLabel.topAnchor.constraint(equalTo: middleLabel.bottomAnchor, constant: 8.0),
bottomLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 8.0),
bottomLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -8.0),
bottomLabel.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -8.0),
])
}
}
The result:
and rotated, so you can see the auto-sizing:
Edit
A little clarification on Intrinsic Content Size...
In this image, all 5 subviews have an intrinsicContentSize of 120 x 80:
class IntrinsicTestView: UIView {
override var intrinsicContentSize: CGSize {
return CGSize(width: 120, height: 80)
}
}
As you can see:
If I don't add constraints to specify Width - either with a Width constraint or Leading and Trailing constraints - the view will be 120-pts wide.
If I don't add constraints to specify Height - either with a Height constraint or Top and Bottom constraints - the view will be 80-pts tall.
Otherwise, the width and height will be determined by the constraints I've added.
Here's the complete code for that example:
class IntrinsicTestView: UIView {
override var intrinsicContentSize: CGSize {
return CGSize(width: 120, height: 80)
}
}
class IntrinsicExampleViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
var iViews: [IntrinsicTestView] = [IntrinsicTestView]()
var v: IntrinsicTestView
let colors: [UIColor] = [.red, .green, .blue, .yellow, .purple]
colors.forEach { c in
let v = IntrinsicTestView()
v.backgroundColor = c
v.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(v)
iViews.append(v)
}
let g = view.safeAreaLayoutGuide
// first view at Top: 20 / Leading: 20
v = iViews[0]
v.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0).isActive = true
v.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0).isActive = true
// second view at Top: 120 / Leading: 20
// height: 20
v = iViews[1]
v.topAnchor.constraint(equalTo: g.topAnchor, constant: 120.0).isActive = true
v.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0).isActive = true
v.heightAnchor.constraint(equalToConstant: 20).isActive = true
// third view at Top: 160 / Leading: 20
// height: 40 / width: 250
v = iViews[2]
v.topAnchor.constraint(equalTo: g.topAnchor, constant: 160.0).isActive = true
v.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0).isActive = true
v.heightAnchor.constraint(equalToConstant: 40).isActive = true
v.widthAnchor.constraint(equalToConstant: 250).isActive = true
// fourth view at Top: 220
// trailing: 20 / width: 250
v = iViews[3]
v.topAnchor.constraint(equalTo: g.topAnchor, constant: 220.0).isActive = true
v.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0).isActive = true
v.widthAnchor.constraint(equalToConstant: 250).isActive = true
// fourth view at Top: 400 / Leading: 20
// trailing: 20 / bottom: 20
v = iViews[4]
v.topAnchor.constraint(equalTo: g.topAnchor, constant: 400.0).isActive = true
v.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0).isActive = true
v.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0).isActive = true
v.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -20.0).isActive = true
}
}

As DonMag said in his response, intrinsicContentSize is not quite intended for complex layout management but rather as an indication of a View's preferred size when there are not enough constraints for his frame.
However, there is a simple example i can provide to hopefully point you in the right direction.
Let's assume i want a RatioView to be intrinsically sized so that its height is a linear function of its width:
let ratioView = RatioView()
ratioView.ratio = 0.5
in this example, ratioView will have an intrinsic height equal to half of his width.
The code for RatioView is the following:
class RatioView: UIView {
var contentHeight: CGFloat { bounds.width * ratio }
private var kvoObservation: NSKeyValueObservation?
var ratio: CGFloat = 1 {
didSet { invalidateIntrinsicContentSize() }
}
override var intrinsicContentSize: CGSize {
.init(width: bounds.width, height: contentHeight)
}
override func didMoveToSuperview() {
guard let _ = superview else { return }
kvoObservation = observe(\.frame, options: .initial) { [weak self] (_, change) in
self?.invalidateIntrinsicContentSize()
}
}
override func willMove(toSuperview newSuperview: UIView?) {
if newSuperview == nil {
kvoObservation = nil
}
}
}
There are two important highligths in the above code:
the view calls invalidateIntrinsicContentSize to inform the layout system that the value of his intrinsicContentSize property has changed.
the view is constantly monitoring via KVO observation any changes to his frame property, so that it can calculate its new height every time its width changes.
The second step is especially noteworthy: without it, the view wouldn't know when its size is changing and thus wouldn't have any chance to inform the layout system (via invalidateIntrinsicContentSize) that its intrinsicContentSize has been refreshed.

Related

ios Swift: ScrollView with dynamic content programmatic layout

Need to create custom view, just 2 buttons and some content between. Problem is about create correct layout using scrollView and subviews with dynamic content.
For example, if there will be only one Label.
What is my mistake?
Now label isn't visible, and view looks like:
Here is code:
view inits this way:
let view = MyView(frame: .zero)
view.configure(with ...) //here configures label text
selv.view.addSubView(view)
public final class MyView: UIView {
private(set) var titleLabel: UILabel?
override public init(frame: CGRect) {
let closeButton = UIButton(type: .system)
closeButton.translatesAutoresizingMaskIntoConstraints = false
(button setup)
let scrollView = UIScrollView()
scrollView.translatesAutoresizingMaskIntoConstraints = false
scrollView.showsVerticalScrollIndicator = false
scrollView.alwaysBounceVertical = false
let contentLayoutGuide = scrollView.contentLayoutGuide
let titleLabel = UILabel()
titleLabel.translatesAutoresizingMaskIntoConstraints = false
(label's font and alignment setup)
let successButton = UIButton(type: .system)
successButton.translatesAutoresizingMaskIntoConstraints = false
(button setup)
super.init(frame: frame)
addSubview(closeButton)
addSubview(scrollView)
addSubview(successButton)
scrollView.addSubview(titleLabel)
self.textLabel = textLabel
let layoutGuide = UILayoutGuide()
addLayoutGuide(layoutGuide)
NSLayoutConstraint.activate([
layoutGuide.leadingAnchor.constraint(equalToSystemSpacingAfter: leadingAnchor, multiplier: 2),
trailingAnchor.constraint(equalToSystemSpacingAfter: layoutGuide.trailingAnchor, multiplier: 2),
layoutGuide.topAnchor.constraint(equalToSystemSpacingBelow: topAnchor, multiplier: 2),
bottomAnchor.constraint(equalToSystemSpacingBelow: layoutGuide.bottomAnchor, multiplier: 2),
closeButton.leadingAnchor.constraint(greaterThanOrEqualTo: layoutGuide.leadingAnchor),
layoutGuide.trailingAnchor.constraint(greaterThanOrEqualTo: closeButton.trailingAnchor),
closeButton.centerXAnchor.constraint(equalTo: layoutGuide.centerXAnchor),
closeButton.topAnchor.constraint(equalTo: layoutGuide.topAnchor),
closeButton.heightAnchor.constraint(equalToConstant: 33),
scrollView.topAnchor.constraint(equalTo: closeButton.bottomAnchor),
scrollView.leadingAnchor.constraint(equalTo: layoutGuide.leadingAnchor),
scrollView.trailingAnchor.constraint(equalTo: layoutGuide.trailingAnchor),
scrollView.bottomAnchor.constraint(equalTo: successButton.topAnchor),
scrollView.contentLayoutGuide.leadingAnchor.constraint(equalTo: layoutGuide.leadingAnchor),
scrollView.contentLayoutGuide.trailingAnchor.constraint(equalTo: layoutGuide.trailingAnchor),
successButton.leadingAnchor.constraint(equalTo: layoutGuide.leadingAnchor),
layoutGuide.trailingAnchor.constraint(equalTo: successButton.trailingAnchor),
successButton.heightAnchor.constraint(equalToConstant: 48),
layoutGuide.bottomAnchor.constraint(equalTo: successButton.bottomAnchor),
titleLabel.topAnchor.constraint(equalTo: scrollView.topAnchor, constant: 16),
titleLabel.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor, constant: 16),
titleLabel.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor, constant: -16),
titleLabel.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor, constant: -16),
])
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public func configure(with viewModel: someViewModel) {
titleLabel?.text = viewModel.title
}
}
If I'll add scrollView frameLayoutGuide height:
scrollView.frameLayoutGuide.heightAnchor.constraint(equalToConstant: 150),
, then all looks as expected, but I need to resize this label and all MyView height depending on content.
A UIScrollView is designed to automatically allow scrolling when its content is larger than its frame.
By itself, a scroll view has NO intrinsic size. It doesn't matter how many subviews you add to it... if you don't do something to set its frame, its frame size will always be .zero.
If we want to get the scroll view's frame to grow in height based on its content we need to give it a height constraint when the content size changes.
If we want it to scroll when it has a lot of content, we also need to give it a maximum height.
So, if we want MyView height to be max of 1/2 the screen (view) height, we constrain its height (in the controller) like this:
myView.heightAnchor.constraint(lessThanOrEqualTo: view.safeAreaLayoutGuide.heightAnchor, multiplier: 0.5)
and then constrain the scroll view height in MyView like this:
let svh = scrollView.heightAnchor.constraint(equalToConstant: scrollView.contentSize.height)
svh.priority = .required - 1
svh.isActive = true
Here is a modification to your code - lots of comments in the code so you should be able to follow.
First, an example controller:
class MVTestVC: UIViewController {
let myView = MyView()
let sampleStrings: [String] = [
"Short string.",
"This is a longer string which should wrap onto a couple lines.",
"Now let's use a really, really long string. This will make the label taller, but still not enough to require vertical scrolling.",
"We want to see what happens when we DO need scrolling.\n\nSo, let's use a long string, with some embedded newlines.\n\nThis will make the label tall enough that it would exceed one-half the screen height, so we can see that we do, in fact, get vertical scrolling.",
]
var strIndex: Int = 0
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .gray
myView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(myView)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
// 20-points on each side
myView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
myView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
// centered vertically
myView.centerYAnchor.constraint(equalTo: g.centerYAnchor),
// max 1/2 screen (view) height
myView.heightAnchor.constraint(lessThanOrEqualTo: g.heightAnchor, multiplier: 0.5),
])
myView.backgroundColor = .white
myView.configure(with: sampleStrings[0])
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
strIndex += 1
myView.configure(with: sampleStrings[strIndex % sampleStrings.count])
}
}
and the modified MyView class:
public final class MyView: UIView {
private let titleLabel = UILabel()
private let scrollView = UIScrollView()
// this will be used to set the scroll view height
private var svh: NSLayoutConstraint!
override public init(frame: CGRect) {
super.init(frame: frame)
let closeButton = UIButton(type: .system)
closeButton.translatesAutoresizingMaskIntoConstraints = false
//(button setup)
closeButton.setTitle("X", for: [])
closeButton.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
scrollView.translatesAutoresizingMaskIntoConstraints = false
scrollView.showsVerticalScrollIndicator = false
scrollView.alwaysBounceVertical = false
titleLabel.translatesAutoresizingMaskIntoConstraints = false
//(label's font and alignment setup)
titleLabel.font = .systemFont(ofSize: 24.0, weight: .light)
titleLabel.numberOfLines = 0
let successButton = UIButton(type: .system)
successButton.translatesAutoresizingMaskIntoConstraints = false
//(button setup)
successButton.setTitle("Success", for: [])
successButton.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
addSubview(closeButton)
addSubview(scrollView)
addSubview(successButton)
scrollView.addSubview(titleLabel)
let layoutGuide = UILayoutGuide()
addLayoutGuide(layoutGuide)
let contentLayoutGuide = scrollView.contentLayoutGuide
NSLayoutConstraint.activate([
layoutGuide.leadingAnchor.constraint(equalToSystemSpacingAfter: leadingAnchor, multiplier: 2),
trailingAnchor.constraint(equalToSystemSpacingAfter: layoutGuide.trailingAnchor, multiplier: 2),
layoutGuide.topAnchor.constraint(equalToSystemSpacingBelow: topAnchor, multiplier: 2),
bottomAnchor.constraint(equalToSystemSpacingBelow: layoutGuide.bottomAnchor, multiplier: 2),
closeButton.leadingAnchor.constraint(greaterThanOrEqualTo: layoutGuide.leadingAnchor),
layoutGuide.trailingAnchor.constraint(greaterThanOrEqualTo: closeButton.trailingAnchor),
closeButton.centerXAnchor.constraint(equalTo: layoutGuide.centerXAnchor),
closeButton.topAnchor.constraint(equalTo: layoutGuide.topAnchor),
closeButton.heightAnchor.constraint(equalToConstant: 33),
scrollView.topAnchor.constraint(equalTo: closeButton.bottomAnchor),
scrollView.leadingAnchor.constraint(equalTo: layoutGuide.leadingAnchor),
scrollView.trailingAnchor.constraint(equalTo: layoutGuide.trailingAnchor),
scrollView.bottomAnchor.constraint(equalTo: successButton.topAnchor),
successButton.leadingAnchor.constraint(equalTo: layoutGuide.leadingAnchor),
layoutGuide.trailingAnchor.constraint(equalTo: successButton.trailingAnchor),
successButton.heightAnchor.constraint(equalToConstant: 48),
layoutGuide.bottomAnchor.constraint(equalTo: successButton.bottomAnchor),
// constrain the label to the scroll view's Content Layout Guide
titleLabel.topAnchor.constraint(equalTo: contentLayoutGuide.topAnchor, constant: 16),
titleLabel.leadingAnchor.constraint(equalTo: contentLayoutGuide.leadingAnchor, constant: 16),
titleLabel.trailingAnchor.constraint(equalTo: contentLayoutGuide.trailingAnchor, constant: -16),
titleLabel.bottomAnchor.constraint(equalTo: contentLayoutGuide.bottomAnchor, constant: -16),
// label needs a width anchor, otherwise we'll get horizontal scrolling
titleLabel.widthAnchor.constraint(equalTo: scrollView.frameLayoutGuide.widthAnchor, constant: -32),
])
layer.cornerRadius = 12
// so we can see the framing
scrollView.backgroundColor = .red
titleLabel.backgroundColor = .green
}
public override func layoutSubviews() {
super.layoutSubviews()
// we want to update the scroll view's height constraint when the text changes
if let c = svh {
c.isActive = false
}
// on initial layout, the scroll view's content size will still be zero
// so force another layout pass
if scrollView.contentSize.height == 0 {
scrollView.setNeedsLayout()
scrollView.layoutIfNeeded()
}
// constrain the scroll view's height to the height of its content
// but with a less-than-required priority so we can use a maximum height
svh = scrollView.heightAnchor.constraint(equalToConstant: scrollView.contentSize.height)
svh.priority = .required - 1
svh.isActive = true
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
//public func configure(with viewModel: someViewModel) {
// titleLabel.text = viewModel.title
//}
public func configure(with str: String) {
titleLabel.text = str
// force the scroll view to update its layout
scrollView.setNeedsLayout()
scrollView.layoutIfNeeded()
// force self to update its layout
self.setNeedsLayout()
self.layoutIfNeeded()
}
}
Each tap anywhere on the screen will cycle through a few sample strings to change the text in the label, giving us this:

iOS: How to scroll navigation bar as view controller scrolls

I want to scroll the navigation bar as the user scrolls on the view controller. This should be similar to how the YouTube app's home page is working. When the user scrolls down, the navigation bar should be made visible. The navigation bar should move as much as the scroll amount.
I'm aware of hidesBarOnSwipe and setNavigationBarHidden, but these do not give precise control of the y-axis. I'm also reading that Apple does not support directly modifying the navigation bar frame.
So, how does YouTube do this? I'm looking for an MVP demonstrating navigation bar position change along with a UIScrollView offset change.
Without additional detail about what you want to do, I'll make some guesses.
First, the top of the YouTube app's home page is almost certainly not a UINavigationBar -- it doesn't behave like one, there is no pushing/popping of controllers going on, it's in a tab bar controller setup, etc.
So, let's assume it's a view with subviews - we'll call it a "sliding header view" - and your goal is:
don't let the header view's top scroll down
"push it up" when scrolling up
"pull it down" when scrolling down
We can accomplish this by constraining the Top of that header view to the Top of the scroll view's Frame Layout Guide.
when we start to scroll, we'll save the current .contentOffset.y
when we scroll, we'll get the relative scroll y distance
if we're scrolling Up, we'll change the Top Constraint .constant value to move the header view up
if we're scrolling Down, we'll change the Top Constraint .constant value to move the header view down
Here's how it will look at the start:
as we scroll up just a little:
after we've scrolled up farther:
as we scroll down just a little:
after we've scrolled down farther:
Here's the example code for that:
Simple two-label "header" view
class SlidingHeaderView: UIView {
// simple view with two labels
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() {
backgroundColor = .systemBlue
let v1 = UILabel()
v1.translatesAutoresizingMaskIntoConstraints = false
v1.text = "Label 1"
v1.backgroundColor = .yellow
addSubview(v1)
let v2 = UILabel()
v2.translatesAutoresizingMaskIntoConstraints = false
v2.text = "Label 2"
v2.backgroundColor = .yellow
addSubview(v2)
NSLayoutConstraint.activate([
v1.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 12.0),
v1.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -12.0),
v2.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 12.0),
v2.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -12.0),
v1.topAnchor.constraint(equalTo: topAnchor, constant: 8.0),
v2.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -8.0),
v2.topAnchor.constraint(equalTo: v1.bottomAnchor, constant: 4.0),
v2.heightAnchor.constraint(equalTo: v1.heightAnchor),
])
}
}
example view controller
class SlidingHeaderViewController: UIViewController {
let scrollView: UIScrollView = {
let v = UIScrollView()
v.contentInsetAdjustmentBehavior = .never
return v
}()
let slidingHeaderView: SlidingHeaderView = {
let v = SlidingHeaderView()
return v
}()
let contentView: UIView = {
let v = UIView()
v.backgroundColor = .systemYellow
return v
}()
// Top constraint for the slidingHeaderView
var slidingViewTopC: NSLayoutConstraint!
// to track the scroll activity
var curScrollY: CGFloat = 0
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
[scrollView, slidingHeaderView, contentView].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
}
// add contentView and slidingHeaderView to the scroll view
[contentView, slidingHeaderView].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
// we're going to change slidingHeaderView's Top constraint relative to the Top of the scroll view FRAME
slidingViewTopC = slidingHeaderView.topAnchor.constraint(equalTo: frameG.topAnchor, constant: 0.0)
NSLayoutConstraint.activate([
// scroll view Top to view Top
scrollView.topAnchor.constraint(equalTo: safeG.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 slidingHeaderView Top to scroll view's FRAME
slidingViewTopC,
// slidingHeaderView to Leading/Trailing of scroll view FRAME
slidingHeaderView.leadingAnchor.constraint(equalTo: frameG.leadingAnchor, constant: 0.0),
slidingHeaderView.trailingAnchor.constraint(equalTo: frameG.trailingAnchor, constant: 0.0),
// no Height or Bottom constraint for slidingHeaderView
// content view Top/Leading/Trailing/Bottom to scroll view's CONTENT GUIDE
contentView.topAnchor.constraint(equalTo: contentG.topAnchor, constant: 0.0),
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),
])
// add some content to the content view so we have something to scroll
addSomeContent()
// because we're going to track the scroll offset
scrollView.delegate = self
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if slidingHeaderView.frame.height == 0 {
// get the size of the slidingHeaderView
let sz = slidingHeaderView.systemLayoutSizeFitting(CGSize(width: scrollView.frame.width, height: .greatestFiniteMagnitude), withHorizontalFittingPriority: .required, verticalFittingPriority: .defaultLow)
// use its Height for the scroll view's Top contentInset
scrollView.contentInset = UIEdgeInsets(top: sz.height, left: 0, bottom: 0, right: 0)
}
}
func addSomeContent() {
// create a vertical stack view with a bunch of labels
// and add it to our content view 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),
])
}
}
extension SlidingHeaderViewController: UIScrollViewDelegate {
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
curScrollY = scrollView.contentOffset.y
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let diffY = scrollView.contentOffset.y - curScrollY
var newY: CGFloat = slidingViewTopC.constant - diffY
if diffY < 0 {
// we're scrolling DOWN
newY = min(newY, 0.0)
} else {
// we're scrolling UP
if scrollView.contentOffset.y <= -slidingHeaderView.frame.height {
newY = 0.0
} else {
newY = max(-slidingHeaderView.frame.height, newY)
}
}
// update slidingHeaderView Top constraint constant
slidingViewTopC.constant = newY
curScrollY = scrollView.contentOffset.y
}
}
Everything is done via code - no #IBOutlet or #IBAction connections needed.

autolayout-conform UILabel with vertical text (objC or Swift)?

How would I create an UIView / UILabel with vertical text flow which would look like the red view of this example screen?
I have read about view.transform = CGAffineTransform(... which allows for easy rotation, BUT it would break the auto-layout constraints.
I would be happy to use a third-party library, but I cannot find any.
As noted in Apple's docs:
In iOS 8.0 and later, the transform property does not affect Auto Layout. Auto layout calculates a view’s alignment rectangle based on its untransformed frame.
So, to get transformed views to "play nice" with auto layout, we need to - in effect - tell constraints to use the opposite axis.
For example, if we embed a UILabel in a UIView and rotate the label 90-degrees, we want to constrain the "container" view's Width to the label's Height and its Height to the label's Width.
Here's a sample VerticalLabelView view subclass:
class VerticalLabelView: UIView {
public var numberOfLines: Int = 1 {
didSet {
label.numberOfLines = numberOfLines
}
}
public var text: String = "" {
didSet {
label.text = text
}
}
// vertical and horizontal "padding"
// defaults to 16-ps (8-pts on each side)
public var vPad: CGFloat = 16.0 {
didSet {
h.constant = vPad
}
}
public var hPad: CGFloat = 16.0 {
didSet {
w.constant = hPad
}
}
// because the label is rotated, we need to swap the axis
override func setContentHuggingPriority(_ priority: UILayoutPriority, for axis: NSLayoutConstraint.Axis) {
label.setContentHuggingPriority(priority, for: axis == .horizontal ? .vertical : .horizontal)
}
// this is just for development
// show/hide border of label
public var showBorder: Bool = false {
didSet {
label.layer.borderWidth = showBorder ? 1 : 0
label.layer.borderColor = showBorder ? UIColor.red.cgColor : UIColor.clear.cgColor
}
}
public let label = UILabel()
private var w: NSLayoutConstraint!
private var h: NSLayoutConstraint!
private var mh: NSLayoutConstraint!
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() {
addSubview(label)
label.backgroundColor = .clear
label.translatesAutoresizingMaskIntoConstraints = false
// rotate 90-degrees
let angle = .pi * 0.5
label.transform = CGAffineTransform(rotationAngle: angle)
// so we can change the "padding" dynamically
w = self.widthAnchor.constraint(equalTo: label.heightAnchor, constant: hPad)
h = self.heightAnchor.constraint(equalTo: label.widthAnchor, constant: vPad)
NSLayoutConstraint.activate([
label.centerXAnchor.constraint(equalTo: self.centerXAnchor),
label.centerYAnchor.constraint(equalTo: self.centerYAnchor),
w, h,
])
}
}
I've added a few properties to allow the view to be treated like a label, so we can do:
let v = VerticalLabelView()
// "pass-through" properties
v.text = "Some text which will be put into the label."
v.numberOfLines = 0
// directly setting properties
v.label.textColor = .red
This could, of course, be extended to "pass through" all label properties we need to use so we wouldn't need to reference the .label directly.
This VerticalLabelView can now be used much like a normal UILabel.
Here are two examples - they both use this BaseVC to setup the views:
class BaseVC: UIViewController {
let greenView: UIView = {
let v = UIView()
v.backgroundColor = .green
return v
}()
let normalLabel: UILabel = {
let v = UILabel()
v.numberOfLines = 0
return v
}()
let lYellow: VerticalLabelView = {
let v = VerticalLabelView()
v.backgroundColor = UIColor(red: 1.0, green: 1.0, blue: 0.5, alpha: 1.0)
v.numberOfLines = 0
return v
}()
let lRed: VerticalLabelView = {
let v = VerticalLabelView()
v.backgroundColor = UIColor(red: 1.0, green: 0.5, blue: 0.5, alpha: 1.0)
v.numberOfLines = 0
return v
}()
let lBlue: VerticalLabelView = {
let v = VerticalLabelView()
v.backgroundColor = UIColor(red: 0.3, green: 0.8, blue: 1.0, alpha: 1.0)
v.numberOfLines = 1
return v
}()
let container: UIView = {
let v = UIView()
v.backgroundColor = .systemYellow
return v
}()
override func viewDidLoad() {
super.viewDidLoad()
let strs: [String] = [
"Multiline Vertical Text",
"Vertical Text",
"Overflow Vertical Text",
]
// default UILabel
normalLabel.text = "Regular UILabel wrapping text"
// add the normal label to the green view
greenView.addSubview(normalLabel)
// set text of vertical labels
for (s, v) in zip(strs, [lYellow, lRed, lBlue]) {
v.text = s
}
[container, greenView, normalLabel, lYellow, lRed, lBlue].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
}
// add greenView to the container
container.addSubview(greenView)
// add container to self's view
view.addSubview(container)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
// constrain container Top and CenterX
container.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
container.centerXAnchor.constraint(equalTo: g.centerXAnchor),
// comment next line to allow container subviews to set the height
container.heightAnchor.constraint(equalToConstant: 260.0),
// comment next line to allow container subviews to set the width
container.widthAnchor.constraint(equalToConstant: 160.0),
// green view at Top, stretched full width
greenView.topAnchor.constraint(equalTo: container.topAnchor, constant: 0.0),
greenView.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: 0.0),
greenView.trailingAnchor.constraint(equalTo: container.trailingAnchor, constant: 0.0),
// constrain normal label in green view
// with 8-pts "padding" on all 4 sides
normalLabel.topAnchor.constraint(equalTo: greenView.topAnchor, constant: 8.0),
normalLabel.leadingAnchor.constraint(equalTo: greenView.leadingAnchor, constant: 8.0),
normalLabel.trailingAnchor.constraint(equalTo: greenView.trailingAnchor, constant: -8.0),
normalLabel.bottomAnchor.constraint(equalTo: greenView.bottomAnchor, constant: -8.0),
])
}
}
The first example - SubviewsExampleVC - adds each as a subview, and then we add constraints between the views:
class SubviewsExampleVC: BaseVC {
override func viewDidLoad() {
super.viewDidLoad()
// add vertical labels to the container
[lYellow, lRed, lBlue].forEach { v in
container.addSubview(v)
}
NSLayoutConstraint.activate([
// yellow label constrained to Bottom of green view
lYellow.topAnchor.constraint(equalTo: greenView.bottomAnchor, constant: 0.0),
// Leading to container Leading
lYellow.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: 0.0),
// red label constrained to Bottom of green view
lRed.topAnchor.constraint(equalTo: greenView.bottomAnchor, constant: 0.0),
// Leading to yellow label Trailing
lRed.leadingAnchor.constraint(equalTo: lYellow.trailingAnchor, constant: 0.0),
// blue label constrained to Bottom of green view
lBlue.topAnchor.constraint(equalTo: greenView.bottomAnchor, constant: 0.0),
// Leading to red label Trailing
lBlue.leadingAnchor.constraint(equalTo: lRed.trailingAnchor, constant: 0.0),
// if we want the labels to fill the container width
// blue label Trailing constrained to container Trailing
lBlue.trailingAnchor.constraint(equalTo: container.trailingAnchor, constant: 0.0),
// using constraints to set the vertical label heights
lYellow.heightAnchor.constraint(equalToConstant: 132.0),
lRed.heightAnchor.constraint(equalTo: lYellow.heightAnchor),
lBlue.heightAnchor.constraint(equalTo: lYellow.heightAnchor),
])
// as always, we need to control which view(s)
// hug their content
// so, for example, if we want the Yellow label to "stretch" horizontally
lRed.setContentHuggingPriority(.required, for: .horizontal)
lBlue.setContentHuggingPriority(.required, for: .horizontal)
// or, for example, if we want the Red label to "stretch" horizontally
//lYellow.setContentHuggingPriority(.required, for: .horizontal)
//lBlue.setContentHuggingPriority(.required, for: .horizontal)
}
}
The second example = StackviewExampleVC - adds each as an arranged subview of a UIStackView:
class StackviewExampleVC: BaseVC {
override func viewDidLoad() {
super.viewDidLoad()
// horizontal stack view
let stackView = UIStackView()
// add vertical labels to the stack view
[lYellow, lRed, lBlue].forEach { v in
stackView.addArrangedSubview(v)
}
stackView.translatesAutoresizingMaskIntoConstraints = false
// add stack view to container
container.addSubview(stackView)
NSLayoutConstraint.activate([
// constrain stack view Top to green view Bottom
stackView.topAnchor.constraint(equalTo: greenView.bottomAnchor, constant: 0.0),
// Leading / Trailing to container Leading / Trailing
stackView.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: 0.0),
stackView.trailingAnchor.constraint(equalTo: container.trailingAnchor, constant: 0.0),
// stack view height
stackView.heightAnchor.constraint(equalToConstant: 132.0),
])
// as always, we need to control which view(s)
// hug their content
// so, for example, if we want the Yellow label to "stretch" horizontally
lRed.setContentHuggingPriority(.required, for: .horizontal)
lBlue.setContentHuggingPriority(.required, for: .horizontal)
// or, for example, if we want the Red label to "stretch" horizontally
//lYellow.setContentHuggingPriority(.required, for: .horizontal)
//lBlue.setContentHuggingPriority(.required, for: .horizontal)
}
}
Both examples produce this output:
Please note: this is Example Code Only - it is not intended to be, nor should it be considered to be, Production Ready

(Swift 5) UIScrollView scrolls but none of the content scrolls (video included)

I'm trying to learn to build views without storyboard. I tried to build a scrollview. On that scrollview is a UISearchBar, a UIImageView with an image and a UILabel. It works but none of the content moves. The content is all just frozen in place like no matter how far I scroll the search bar will always be on top of the page. and the image on the bottom. I've attached a video to show what I mean. There's also a problem because none of the content is where I want it to be but that's another problem. I realize this is probably because I don't know enough about constraints and autolayout and building views without storyboards.
Here's the video
class HomePageViewController: UIViewController {
var searchedText: String = ""
let label = UILabel()
let searchBar: UISearchBar = {
let searchBar = UISearchBar()
searchBar.placeholder = "Where are you going?"
searchBar.translatesAutoresizingMaskIntoConstraints = false
searchBar.barTintColor = .systemCyan
searchBar.searchTextField.backgroundColor = .white
searchBar.layer.cornerRadius = 5
return searchBar
}()
let homeImage: UIImageView = {
let homeImage = UIImageView()
homeImage.translatesAutoresizingMaskIntoConstraints = false
homeImage.clipsToBounds = true
return homeImage
}()
let scrollView: UIScrollView = {
let scrollView = UIScrollView()
scrollView.translatesAutoresizingMaskIntoConstraints = false
scrollView.backgroundColor = .systemMint
scrollView.contentSize = CGSize(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height * 30)
return scrollView
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemPink
// setupLayout()
// tried this here doesn't do anything for me
}
func setupLayout() {
view.addSubview(scrollView)
self.scrollView.addSubview(searchBar)
homeImage.image = UIImage(named: "Treehouse")
self.scrollView.addSubview(homeImage)
label.text = "Inspiration for your next trip..."
self.scrollView.addSubview(label)
// not sure where this label is being added I want it to be underneath the image but it isn't t
let safeG = view.safeAreaLayoutGuide
let viewFrame = view.bounds
NSLayoutConstraint.activate([
scrollView.topAnchor.constraint(equalTo: view.topAnchor, constant: -10),
scrollView.leftAnchor.constraint(equalTo: view.leftAnchor),
scrollView.rightAnchor.constraint(equalTo: view.rightAnchor),
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
searchBar.topAnchor.constraint(equalTo: safeG.topAnchor, constant: 50.0),
searchBar.widthAnchor.constraint(equalTo: safeG.widthAnchor, multiplier: 0.9),
searchBar.centerXAnchor.constraint(equalTo: safeG.centerXAnchor),
homeImage.topAnchor.constraint(equalTo: safeG.topAnchor, constant: 150),
homeImage.widthAnchor.constraint(equalTo: safeG.widthAnchor, multiplier: 1.1),
homeImage.centerXAnchor.constraint(equalTo: safeG.centerXAnchor),
homeImage.heightAnchor.constraint(equalToConstant: viewFrame.height/2),
label.topAnchor.constraint(equalTo: homeImage.bottomAnchor, constant: 100)
])
// was doing all this in viewDidLayoutSubviews but not sure if this is better place for it
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
setupLayout()
// tried this in viewDidLoad() and it didn't solve it.
}
}
any help would be appreciated
First, when constraining subviews in a UIScrollView, you should constrain them to the scroll view's Content Layout Guide. You're constraining them to the view's safe area layout guide, so they're never going to go anywhere.
Second, it's difficult to center subviews in a scroll view, because the scroll view can scroll both horizontally and vertically. So it doesn't really have a "center."
You can either put subviews in a stack view, or, quite common, use a UIView as a "content" view to hold the subviews. If you constrain that content view's Width to the scroll view's Frame Layout Guide width, you can then horizontally center the subviews.
Third, it can be very helpful to comment your constraints, so you know exactly what you expect them to do.
Here's a modified version of your posted code:
class HomePageViewController: UIViewController {
var searchedText: String = ""
let label: UILabel = {
let v = UILabel()
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
let searchBar: UISearchBar = {
let searchBar = UISearchBar()
searchBar.placeholder = "Where are you going?"
searchBar.translatesAutoresizingMaskIntoConstraints = false
searchBar.barTintColor = .systemCyan
searchBar.searchTextField.backgroundColor = .white
searchBar.layer.cornerRadius = 5
return searchBar
}()
let homeImage: UIImageView = {
let homeImage = UIImageView()
homeImage.translatesAutoresizingMaskIntoConstraints = false
homeImage.clipsToBounds = true
return homeImage
}()
let scrollView: UIScrollView = {
let scrollView = UIScrollView()
scrollView.translatesAutoresizingMaskIntoConstraints = false
scrollView.backgroundColor = .systemMint
// don't do this
//scrollView.contentSize = CGSize(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height * 30)
return scrollView
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemPink
setupLayout()
}
func setupLayout() {
view.addSubview(scrollView)
//homeImage.image = UIImage(named: "Treehouse")
homeImage.image = UIImage(named: "natureBKG")
label.text = "Inspiration for your next trip..."
// let's use a UIView to hold the "scroll content"
let contentView = UIView()
contentView.translatesAutoresizingMaskIntoConstraints = false
// give it a green background so we can see it
contentView.backgroundColor = .green
contentView.addSubview(searchBar)
contentView.addSubview(homeImage)
contentView.addSubview(label)
scrollView.addSubview(contentView)
let safeG = view.safeAreaLayoutGuide
let svContentG = scrollView.contentLayoutGuide
let svFrameG = scrollView.frameLayoutGuide
NSLayoutConstraint.activate([
// constrain scrollView to all 4 sides of view
// (generally, constrain to safe-area, but this is what you had)
scrollView.topAnchor.constraint(equalTo: view.topAnchor),
scrollView.leftAnchor.constraint(equalTo: view.leftAnchor),
scrollView.rightAnchor.constraint(equalTo: view.rightAnchor),
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
// constrain contentView to all 4 sides of scroll view's Content Layout Guide
contentView.topAnchor.constraint(equalTo: svContentG.topAnchor, constant: 0.0),
contentView.leadingAnchor.constraint(equalTo: svContentG.leadingAnchor, constant: 0.0),
contentView.trailingAnchor.constraint(equalTo: svContentG.trailingAnchor, constant: 0.0),
contentView.bottomAnchor.constraint(equalTo: svContentG.bottomAnchor, constant: 0.0),
// constrain contentView Width equal to scroll view's Frame Layout Guide Width
contentView.widthAnchor.constraint(equalTo: svFrameG.widthAnchor),
// constrain searchBar Top to contentView Top + 50
searchBar.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 50.0),
// constrain searchBar Width to 90% of contentView Width
searchBar.widthAnchor.constraint(equalTo: contentView.widthAnchor, multiplier: 0.9),
// constrain searchBar centerX to contentView centerX
searchBar.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
// constrain homeImage Top to searchBar Bottom + 40
homeImage.topAnchor.constraint(equalTo: searchBar.bottomAnchor, constant: 40.0),
// constrain homeImage Width equal to contentView Width
homeImage.widthAnchor.constraint(equalTo: contentView.widthAnchor, multiplier: 1.0),
// constrain homeImage centerX to contentView centerX
homeImage.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
// constrain homeImage Height to 1/2 of scroll view frame Height
homeImage.heightAnchor.constraint(equalTo: svFrameG.heightAnchor, multiplier: 0.5),
// you probably won't get vertical scrolling yet, so increase the vertical space
// between the homeImage and the label by changing the constant
// from 100 to maybe 400
// constrain label Top to homeImage Bottom + 100
label.topAnchor.constraint(equalTo: homeImage.bottomAnchor, constant: 100.0),
// constrain label centerX to contentView centerX
label.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
// constrain label Bottom to contentView Bottom - 20
label.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -20.0),
])
}
}

How to make UIView which is inside scrollview adapt to screen orientation when user changes screen from portrait to landscape in swift

How to make UIView which is inside scrollview adapt to screen orientation when user changes screen from portrait to landscape in swift?
var scrollView: UIScrollView = {
var scroll = UIScrollView()
scroll.translatesAutoresizingMaskIntoConstraints = false
return scroll
}()
view.addSubview(scrollView)
scrollView.topAnchor.constraint(equalTo: view.topAnchor, constant: 100).isActive = true
scrollView.centerXAnchor.constraint(equalTo: view.centerXAnchor, constant: 0).isActive = true
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 0).isActive = true
scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: 0).isActive = true
scrollView.heightAnchor.constraint(equalToConstant: 500).isActive = true
for i in 0..<arr.count {
var contentView = UIView()
contentView.frame = CGRect(x: i * Int(view.bounds.size.width) + 10, y: 0, width: Int(view.bounds.size.width) - 20 , height: Int(view.frame.height))
scrollView.contentSize = CGSize(width: (view.frame.size.width * CGFloat((Double(i)+1))) ,height: scrollView.frame.size.height)
}
Image
You really want to be using auto-layout instead of trying to calculate frame sizes. Let it do all the work for you.
Based on your code, it looks like you want each "contentView" to be the width of the scrollView's frame, minus 20 (so you have 10-pts of space on each side).
You can quite easily do this by embedding your contentViews in a UIStackView.
Here's a simple example:
class ViewController: UIViewController {
var scrollView: UIScrollView = {
var scroll = UIScrollView()
scroll.translatesAutoresizingMaskIntoConstraints = false
return scroll
}()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(scrollView)
// use a stack view to hold and arrange the scrollView's subviews
let stackView = UIStackView()
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.spacing = 20
// add the stackView to the scrollView
scrollView.addSubview(stackView)
// respect safe area
let safeG = view.safeAreaLayoutGuide
// use scrollView's Content Layout Guide to define scrollable content
let layoutG = scrollView.contentLayoutGuide
// use scrollView's Frame Layout Guide to define content height (since you want horizontal scrolling)
let frameG = scrollView.frameLayoutGuide
NSLayoutConstraint.activate([
scrollView.topAnchor.constraint(equalTo: safeG.topAnchor, constant: 100),
// you're setting leading and trailing, so no need for centerX
//scrollView.centerXAnchor.constraint(equalTo: view.centerXAnchor, constant: 0),
scrollView.leadingAnchor.constraint(equalTo: safeG.leadingAnchor, constant: 0),
scrollView.trailingAnchor.constraint(equalTo: safeG.trailingAnchor, constant: 0),
// let's constrain the scrollView bottom to the view (safe area) bottom
//scrollView.heightAnchor.constraint(equalToConstant: 500),
scrollView.bottomAnchor.constraint(equalTo: safeG.bottomAnchor, constant: -10.0),
// constrain Top and Bottom of the stackView to scrollView's Content Layout Guide
stackView.topAnchor.constraint(equalTo: layoutG.topAnchor),
stackView.bottomAnchor.constraint(equalTo: layoutG.bottomAnchor),
// 10-pts space on leading and trailing
stackView.leadingAnchor.constraint(equalTo: layoutG.leadingAnchor, constant: 10.0),
stackView.trailingAnchor.constraint(equalTo: layoutG.trailingAnchor, constant: -10.0),
// constrain stackView's height to scrollView's Frame Layout Guide height
stackView.heightAnchor.constraint(equalTo: frameG.heightAnchor),
])
// add some views to the stack view
let arr: [UIColor] = [
.red, .green, .blue, .yellow, .purple,
]
for i in 0..<arr.count {
let contentView = UIView()
contentView.backgroundColor = arr[i]
stackView.addArrangedSubview(contentView)
// constrain each "contentView" width to scrollView's Frame Layout Guide width minus 20
contentView.widthAnchor.constraint(equalTo: frameG.widthAnchor, constant: -20).isActive = true
// don't do this
//contentView.frame = CGRect(x: i * Int(view.bounds.size.width) + 10, y: 0, width: Int(view.bounds.size.width) - 20 , height: Int(view.frame.height))
// don't do this
//scrollView.contentSize = CGSize(width: (view.frame.size.width * CGFloat((Double(i)+1))) ,height: scrollView.frame.size.height)
}
}
}
Run that and see if that's what you're going for.
You need to add your subviews to the scroll view and setup their constraints - using of an auto-layout. Don't use contentView.frame = CGRect(...) and scrollView.contentSize = CGSize(...).
For example you can change your for-in to this:
Note: this is only example, change your for-in loop to your needs.
for i in 0..<arr.count {
// we need to distinguish the first and last subviews (because different constraints)
let topAnchor = i == 0 ? scrollView.topAnchor : scrollView.subviews.last!
let isLast = i == arr.count - 1
// here we will use a specific height for all subviews except the last one
let subviewHeight = 60
var contentView = UIView()
contentView.translatesAutoresizingMaskIntoConstraints = false
scrollView.addSubview(contentView)
if isLast {
NSLayoutConstraint.activate([
contentView.topAnchor.constraint(equalTo: topAnchor),
contentView.widthAnchor.constraint(equalTo: scrollView.widthAnchor),
contentView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor),
contentView.centerXAnchor.constraint(equalTo: scrollView.centerXAnchor)
]
} else {
NSLayoutConstraint.activate([
contentView.topAnchor.constraint(equalTo: topAnchor),
contentView.widthAnchor.constraint(equalTo: scrollView.widthAnchor),
contentView.heightAnchor.constraint(equalToConstant: subviewHeight),
contentView.centerXAnchor.constraint(equalTo: scrollView.centerXAnchor)
]
}
}

Resources