How to resize content view in UIScrollView programmatically? - swift

Here is the issue: I need to add a view containing other subviews to a scroll view programmatically. In addition, I also need to make the frame of such a view to stick to the bounds of the main super view. The code below shows the approach I was trying to implement, but as you can see from the pictures below the 'contentView' is not updating its frame size when the screen is rotated. The initial code is taken from here for demonstration purposes. Any help would be greatly appreciated.
import UIKit
class TestViewController : UIViewController {
var contentViewSize = CGSize()
let contentView: UIView = {
let view = UIView()
view.backgroundColor = .magenta
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
let labelOne: UILabel = {
let label = UILabel()
label.text = "Scroll Top"
label.backgroundColor = .red
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
let labelTwo: UILabel = {
let label = UILabel()
label.text = "Scroll Bottom"
label.backgroundColor = .green
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
let scrollView: UIScrollView = {
let v = UIScrollView()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = .cyan
return v
}()
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
contentViewSize = view.bounds.size
labelTwo.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: (contentViewSize.width - labelTwo.frame.size.width - 16.0)).isActive = true
labelTwo.topAnchor.constraint(equalTo: contentView.topAnchor, constant: (contentViewSize.height - labelTwo.frame.size.height - 16.0)).isActive = true
}
override func viewDidLoad() {
super.viewDidLoad()
contentViewSize = view.bounds.size
view.backgroundColor = .yellow
self.view.addSubview(scrollView)
scrollView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 8.0).isActive = true
scrollView.topAnchor.constraint(equalTo: view.topAnchor, constant: 8.0).isActive = true
scrollView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -8.0).isActive = true
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -8.0).isActive = true
scrollView.addSubview(contentView)
contentView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor, constant: 16).isActive = true
contentView.topAnchor.constraint(equalTo: scrollView.topAnchor, constant: 16).isActive = true
contentView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor, constant: 16).isActive = true
contentView.rightAnchor.constraint(equalTo: scrollView.rightAnchor, constant: 16).isActive = true
contentView.addSubview(labelOne)
labelOne.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16.0).isActive = true
labelOne.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 16.0).isActive = true
contentView.addSubview(labelTwo)
labelTwo.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: (contentViewSize.width - labelTwo.frame.size.width - 16.0)).isActive = true
labelTwo.topAnchor.constraint(equalTo: contentView.topAnchor, constant: (contentViewSize.height - labelTwo.frame.size.height - 16.0)).isActive = true
labelTwo.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -16.0).isActive = true
labelTwo.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -16.0).isActive = true
}
}

You made some odd changes to the code from the answer you linked to. Also, that answer is a little out-of-date.
Here's a better example. Assuming you want only vertical scrolling, this will:
add a Cyan scroll view, inset 8-pts on each side from the safe-area
add a Magenta "content view" to the scroll view, with 16-pts on each side constrained to the scroll view's contentLayoutGuide, with a width 32-pts less than the scroll view's frame (16-pts on each side)
add a label at top-left of the content view
add a label at bottom-right of the content view
constrain the bottom label 1500-pts below the top label (so it will scroll vertically)
Code:
class ScrollTestViewController : UIViewController {
let contentView: UIView = {
let view = UIView()
view.backgroundColor = .magenta
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
let labelOne: UILabel = {
let label = UILabel()
label.text = "Scroll Top"
label.backgroundColor = .red
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
let labelTwo: UILabel = {
let label = UILabel()
label.text = "Scroll Bottom"
label.backgroundColor = .green
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
let scrollView: UIScrollView = {
let v = UIScrollView()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = .cyan
return v
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .yellow
// add the scroll view
self.view.addSubview(scrollView)
// add contentView to scroll view
scrollView.addSubview(contentView)
// add two labels to contentView
contentView.addSubview(labelOne)
contentView.addSubview(labelTwo)
// respect safe-area
let g = view.safeAreaLayoutGuide
// if you want to ignore the safe-area (bad idea),
// use this instead
//let g = view!
//scrollView.contentInsetAdjustmentBehavior = .never
// we're going to constrain the contentView to the scroll view's content layout guide
let scg = scrollView.contentLayoutGuide
NSLayoutConstraint.activate([
// constrain scrollView Top / Leading / Trailing / Bottom to view (safe-area)
// with 8-pts on each side
scrollView.topAnchor.constraint(equalTo: g.topAnchor, constant: 8.0),
scrollView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 8.0),
scrollView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -8.0),
scrollView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -8.0),
// constrain contentView Top / Leading / Trailing / Bottom to scroll view's Content Layout Guide
// with 16-pts on each side
contentView.topAnchor.constraint(equalTo: scg.topAnchor, constant: 16.0),
contentView.leadingAnchor.constraint(equalTo: scg.leadingAnchor, constant: 16.0),
contentView.trailingAnchor.constraint(equalTo: scg.trailingAnchor, constant: -16.0),
contentView.bottomAnchor.constraint(equalTo: scg.bottomAnchor, constant: -16.0),
// if we only want vertical scrolling, constrain contentView Width
// to scrollView's Frame Layout Guide minus 32-pts (because we have 16-pts on each side)
contentView.widthAnchor.constraint(equalTo: scrollView.frameLayoutGuide.widthAnchor, constant: -32.0),
// constrain labelOne Top / Leading 16-pts to contentView Top / Leading
// (so it shows up at top-left corner)
labelOne.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 16.0),
labelOne.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16.0),
// constrain labelTwo Bottom / Trailing 16-pts to contentView Bottom / Trailing
// (so it shows up at bottom-right corner)
labelTwo.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16.0),
labelTwo.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -16.0),
// constrain labelTwo Top to labelOne Bottom + 1500-pts
// so we'll have some vertical scrolling to get to it
labelTwo.topAnchor.constraint(equalTo: labelOne.bottomAnchor, constant: 1500.0),
])
}
}

Related

UIScrollView does not scroll + second scrollView shows nothing

it is unpleasant for me but I need some help with my UIScrollViews. They are both arranged subviews of a stackView on my MainVC.
The weird thing is that only one of them is showing content, although I used the same code for both scrollViews. The second problem is that they do not scroll, here is my code:
class HomeVC: UIViewController, UIScrollViewDelegate {
var views = [UIImageView]()
//StackView
let stackView = UIStackView()
let topView = UIScrollView()
let bottomView = UIScrollView()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = MyColors.soft_pink
prepare_data()
print(views.count)
}
//MARK: - GUI
func setUpStackView() {
view.addSubview(stackView)
stackView.alignment = .center
stackView.axis = .vertical
stackView.distribution = .equalCentering
stackView.spacing = 5
stackView.addArrangedSubview(topView)
stackView.addArrangedSubview(bottomView)
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20).isActive = true
stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 10).isActive = true
stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -10).isActive = true
stackView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -25).isActive = true
setUpTopView()
setUpBottomView()
}
func setUpTopView() {
topView.delegate = self
topView.layer.cornerRadius = 25
topView.layer.masksToBounds = true
topView.layer.borderWidth = 10
topView.layer.borderColor = UIColor.white.cgColor
topView.contentMode = .scaleAspectFit
topView.showsHorizontalScrollIndicator = false
topView.isPagingEnabled = true
topView.contentSize = CGSize(width: topView.frame.width * CGFloat(views.count),height: topView.frame.height)
for i in 0..<views.count {
topView.addSubview(views[i])
views[i].frame = CGRect(x: topView.frame.width * CGFloat(i), y: 0, width: 350, height: 250)
views[i].layer.cornerRadius = 25
}
topView.translatesAutoresizingMaskIntoConstraints = false
topView.leadingAnchor.constraint(equalTo: stackView.leadingAnchor, constant: 10).isActive = true
topView.trailingAnchor.constraint(equalTo: stackView.trailingAnchor, constant: -10).isActive = true
topView.heightAnchor.constraint(equalToConstant: 250).isActive = true
}
func setUpBottomView() {
bottomView.delegate = self
bottomView.layer.cornerRadius = 25
bottomView.layer.masksToBounds = true
bottomView.layer.borderWidth = 10
bottomView.layer.borderColor = UIColor.white.cgColor
bottomView.contentMode = .scaleAspectFit
bottomView.showsHorizontalScrollIndicator = false
bottomView.isPagingEnabled = true
bottomView.contentSize = CGSize(width: bottomView.frame.width * CGFloat(views.count),height: bottomView.frame.height)
for i in 0..<views.count {
bottomView.addSubview(views[i])
views[i].frame = CGRect(x: bottomView.frame.width * CGFloat(i), y: 0, width: 350, height: 250)
views[i].layer.cornerRadius = 25
}
bottomView.translatesAutoresizingMaskIntoConstraints = false
bottomView.leadingAnchor.constraint(equalTo: stackView.leadingAnchor, constant: 10).isActive = true
bottomView.trailingAnchor.constraint(equalTo: stackView.trailingAnchor, constant: -10).isActive = true
bottomView.heightAnchor.constraint(equalToConstant: 250).isActive = true
}
func prepare_data() {
for x in 1...6 {
let woman = UIImage(named: "woman\(x)")
let womanView = UIImageView(image: woman)
womanView.contentMode = .scaleAspectFill
views.append(womanView)
}
setUpStackView()
}
}
Could someone please be so kind and tell me what I have wrong? Thank you in advance!
Try to debug by printing values. bottomView.frame.width was zero at initialisation, so update subviews in viewDidLayoutSubviews. There are more ways you can look for frame update detection.
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
DispatchQueue.main.async {
self.updateSubviewFrames()
}
}
func updateSubviewFrames() {
print(bottomView.frame)
bottomView.contentSize = CGSize(width: bottomView.frame.width * CGFloat(views.count),height: bottomView.frame.height)
for i in 0..<views.count {
views[i].frame = CGRect(x: bottomView.frame.width * CGFloat(i), y: 0, width: 350, height: 250)
views[i].layer.cornerRadius = 25
}
}
You've done a few things wrong...
First, because it's easy -- the reason you don't see anything in your Top scroll view is because you add your image views (from the views array) to topView, and then you add them to bottomView which removes them from topView!
So, you need one array of views for topView and an array of other views for bottomView.
Next, you are using auto-layout / constraints to size and position your stack view, then trying to use the frames of the stack view's arranged subviews -- for example:
bottomView.contentSize = CGSize(width: bottomView.frame.width * CGFloat(views.count),height: bottomView.frame.height)
but, that's all being done in functions called from viewDidLoad() when auto-layout has not yet configured the view frames.
You're also adding your scroll view's as arranged subviews of the stack view, but then constraining them to the stack view (which is not the way to do it):
topView.leadingAnchor.constraint(equalTo: stackView.leadingAnchor, constant: 10).isActive = true
As a side note: the easiest way to manage a paged scroll view is to embed the "pages" (your image views) in a horizontal stack view, setting the width of each view to the width of the scroll view's Frame Layout Guide (minus desired spacing).
Here's a modified version of your code to take a look at:
class HomeVC: UIViewController, UIScrollViewDelegate {
var topViews = [UIImageView]()
var botViews = [UIImageView]()
//StackView
let stackView = UIStackView()
let topView = UIScrollView()
let bottomView = UIScrollView()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemPink // MyColors.soft_pink
prepare_data()
setUpStackView()
setUpTopAndBottomViews()
}
func prepare_data() {
// create 6 image views
// for BOTH Top and Bottom scroll views
// I'll assume you have "woman" and "man" images
for x in 1...6 {
let woman = UIImage(named: "woman\(x)")
let man = UIImage(named: "man\(x)")
let womanView = UIImageView(image: woman)
womanView.contentMode = .scaleAspectFill
topViews.append(womanView)
let manView = UIImageView(image: man)
manView.contentMode = .scaleAspectFill
botViews.append(manView)
}
}
func setUpStackView() {
// setup stack view
view.addSubview(stackView)
// .alignment should be .fill, not .center
//stackView.alignment = .center
stackView.alignment = .fill
stackView.axis = .vertical
// let's use .fillEqually instead of .equalCentering
//stackView.distribution = .equalCentering
stackView.distribution = .fillEqually
stackView.spacing = 5
stackView.addArrangedSubview(topView)
stackView.addArrangedSubview(bottomView)
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20).isActive = true
stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 10).isActive = true
stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -10).isActive = true
stackView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -25).isActive = true
}
func setUpTopAndBottomViews() {
// setup both scroll views with the same properties
[topView, bottomView].forEach { v in
v.delegate = self
v.layer.cornerRadius = 25
v.layer.masksToBounds = true
v.layer.borderWidth = 10
v.layer.borderColor = UIColor.white.cgColor
v.showsHorizontalScrollIndicator = false
v.isPagingEnabled = true
}
// let's use auto-layout here
// if you want horizontal paged scrolling, easiest route is to
// use a horizontal stack view
// create a stack view
let topStack = UIStackView()
topStack.translatesAutoresizingMaskIntoConstraints = false
topStack.spacing = 10
// add stack view to topView
topView.addSubview(topStack)
for i in 0..<topViews.count {
topStack.addArrangedSubview(topViews[i])
topViews[i].layer.cornerRadius = 25
// set view width and height equal to
// topView's Frame Layout Guide
// allowing for 5-pts "padding" on the sides
topViews[i].widthAnchor.constraint(equalTo: topView.frameLayoutGuide.widthAnchor, constant: -10.0).isActive = true
topViews[i].heightAnchor.constraint(equalTo: topView.frameLayoutGuide.heightAnchor).isActive = true
}
// now we'll set constraints on the stack view to
// topView's Content Layout Guide
NSLayoutConstraint.activate([
topStack.topAnchor.constraint(equalTo: topView.contentLayoutGuide.topAnchor),
topStack.leadingAnchor.constraint(equalTo: topView.contentLayoutGuide.leadingAnchor, constant: 5.0),
topStack.trailingAnchor.constraint(equalTo: topView.contentLayoutGuide.trailingAnchor, constant: -5.0),
topStack.bottomAnchor.constraint(equalTo: topView.contentLayoutGuide.bottomAnchor),
])
// same thing with the bottom scroll view
// create a new stack view
let botStack = UIStackView()
botStack.translatesAutoresizingMaskIntoConstraints = false
botStack.spacing = 10
// add stack view to bottomView
bottomView.addSubview(botStack)
for i in 0..<botViews.count {
botStack.addArrangedSubview(botViews[i])
botViews[i].layer.cornerRadius = 25
// set view width and height equal to
// bottomView's Frame Layout Guide
// allowing for 5-pts "padding" on the sides
botViews[i].widthAnchor.constraint(equalTo: bottomView.frameLayoutGuide.widthAnchor, constant: -10.0).isActive = true
botViews[i].heightAnchor.constraint(equalTo: bottomView.frameLayoutGuide.heightAnchor).isActive = true
}
// now we'll set constraints on the stack view to
// bottomView's Content Layout Guide
NSLayoutConstraint.activate([
botStack.topAnchor.constraint(equalTo: bottomView.contentLayoutGuide.topAnchor),
botStack.leadingAnchor.constraint(equalTo: bottomView.contentLayoutGuide.leadingAnchor, constant: 5.0),
botStack.trailingAnchor.constraint(equalTo: bottomView.contentLayoutGuide.trailingAnchor, constant: -5.0),
botStack.bottomAnchor.constraint(equalTo: bottomView.contentLayoutGuide.bottomAnchor),
])
}
}

Swift - Set the height of the item as same as parent in nested UIStackView programmingly

I have a UI with 3 main parts: header, keypad and a button.
Size of header and button are fixed, the remaining area should be occupied by keypad.
Like this:
The keypad part is build up by a nested UIStackView (vertical UIStackView parent, with 4 horizontal UIStackView children), 3 buttons will be added to each of the horizontal UIStackView.
Everything is fine except I would like the buttons to have the same height as the horizontal UIStackView, so that it should be easier for user to click on it.
I have tried
rowStackView.alignment = .fill
or
for button in numberButtons
{
button.translatesAutoresizingMaskIntoConstraints = false
button.heightAnchor.constraint(equalTo: button.superview!.heightAnchor).isActive = true
}
However, the numberPadStackView will be squeezed like following. How should I fix that? Thanks.
These are my codes for now:
headerStackView.axis = .vertical
headerStackView.alignment = .center
headerStackView.setContentHuggingPriority(.defaultHigh, for: .vertical)
headerStackView.setContentCompressionResistancePriority(.defaultHigh, for: .vertical)
view.addSubview(headerStackView)
numberPadStackView.axis = .vertical
numberPadStackView.distribution = .fillEqually
numberPadStackView.alignment = .center
view.addSubview(numberPadStackView)
initNumpad()
view.addSubview(requestLabel)
initNumPad()
private func initNumpad()
{
var rowStackView = UIStackView()
numberButtons.removeAll()
for i in 0 ..< 11
{
if i % 3 == 0
{
rowStackView = UIStackView()
rowStackView.axis = .horizontal
rowStackView.distribution = .fillEqually
rowStackView.alignment = .center
rowStackView.setContentHuggingPriority(.defaultLow, for: .vertical)
numberPadRowStackView.append(rowStackView)
numberPadStackView.addArrangedSubview(rowStackView)
}
let button = UIButton()
switch i
{
case 0 ..< 9:
button.setTitle("\(i + 1)", for: .normal)
case 9:
button.setTitle(".", for: .normal)
case 10:
button.setTitle("0", for: .normal)
default:
return
}
button.titleLabel?.textAlignment = .center
button.setContentHuggingPriority(.defaultLow, for: .vertical)
button.backgroundColor = UIColor.random()
numberButtons.append(button)
rowStackView.addArrangedSubview(button)
}
numberPadDeleteImageView.backgroundColor = UIColor.random()
rowStackView.addArrangedSubview(numberPadDeleteImageView)
}
Layout
headerStackView.translatesAutoresizingMaskIntoConstraints = false
numberPadStackView.translatesAutoresizingMaskIntoConstraints = false
requestLabel.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
headerStackView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 24),
headerStackView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: Padding),
headerStackView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -Padding),
numberPadStackView.topAnchor.constraint(equalTo: headerStackView.bottomAnchor, constant: 43),
numberPadStackView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: Padding),
numberPadStackView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -Padding),
requestLabel.topAnchor.constraint(equalTo: numberPadStackView.bottomAnchor, constant: 21),
requestLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 28),
requestLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -28),
requestLabel.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -5),
requestLabel.heightAnchor.constraint(equalToConstant: 40),
])
for rowStackView in numberPadRowStackView
{
rowStackView.translatesAutoresizingMaskIntoConstraints = false
rowStackView.widthAnchor.constraint(equalTo: numberPadStackView.widthAnchor).isActive = true
}
Couple notes...
Setting Content Hugging Priority on a UIStackView is generally not going to give you the expected results. That's because the stack view is arranging its subviews (based on the stack view's Alignment and Distribution properties). The Content Hugging Priority of the stack view's arranged subviews will be the controlling factor.
As is obvious, if I lay out 4 labels, vertically constrained top-to-bottom, each having the same Content Hugging priority (such as the default 250), I'll get something like this in Storyboard (note the Red layout-problem indicator):
and at runtime it may look like this:
Auto-layout is going to respect the (intrinsic) Height for Labels 1, 2 and 4, and then stretch Label 3 to complete the layout.
If I embed the top two and bottom two labels each in vertical stack views...
Storyboard will look like this (again, note the Red layout-problem indicator):
and we get the same thing at run-time:
Even if I set the Content Hugging Priority of the top stack view to 1000, it won't make a difference -- because auto-layout is using the arranged subviews to decide what to do.
So, for your layout, divide your screen into *three layout elements:
the "header" section
the "numberPad" section
the "process" section
and then tell auto-layout you want the Header and Process elements to maintain their heights, and allow the numberPad to stretch.
Since UI elements default to Hugging Priority of 250, probably the easiest way to manage that is reduce the Hugging Priority of the numberPad buttons.
Here is some example code. I'm not sure how you're laying out your "headerStackView" as it doesn't really look like it would lend itself to a stack view... so, I laid it out as a header UIView:
class WithNumpadViewController: UIViewController {
// three "parts" to our layout
let headerView = UIView()
let numberPadStackView = UIStackView()
let requestLabel = UILabel()
// data labels to be filled-in
let currencyLabel = UILabel()
let currValueLabel = UILabel()
let balanceLabel = UILabel()
// however you're using this
var numberButtons: [UIButton] = []
let Padding: CGFloat = 16
override func viewDidLoad() {
super.viewDidLoad()
if let vc = self.navigationController?.viewControllers.first {
vc.navigationItem.title = "Wallet"
}
self.navigationController?.navigationBar.barTintColor = .black
self.navigationController?.navigationBar.tintColor = .white
self.navigationController?.navigationBar.isTranslucent = false
self.navigationController?.navigationBar.titleTextAttributes = [NSAttributedString.Key.foregroundColor: UIColor.white]
title = "Withdraw"
// add "three parts" to view
[headerView, numberPadStackView, requestLabel].forEach {
$0.translatesAutoresizingMaskIntoConstraints = false
view.addSubview($0)
}
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
// constrain headerView to Top / Leading / Trailing (safe-area)
// let its content determine its height
headerView.topAnchor.constraint(equalTo: g.topAnchor, constant: 0.0),
headerView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
headerView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
// constrain numberPad 40-pts from Bottom headerView
numberPadStackView.topAnchor.constraint(equalTo: headerView.bottomAnchor, constant: 40),
// Leading / Trailing with Padding
numberPadStackView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: Padding),
numberPadStackView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -Padding),
// constrain requestLabel 21-pts from Bottom of numberPad
requestLabel.topAnchor.constraint(equalTo: numberPadStackView.bottomAnchor, constant: 21),
// Leading / Trailing with 28-pts padding
requestLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 28),
requestLabel.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -28),
// Bottom 5-pts from Bottom (safe-area)
requestLabel.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -5),
// explicit Height of 40-pts
requestLabel.heightAnchor.constraint(equalToConstant: 40),
])
// setup contents of headerView
initHeader()
// setup contents of numberPad
initNumpad()
// properties for requestLabel
requestLabel.backgroundColor = .black
requestLabel.textColor = .white
requestLabel.textAlignment = .center
requestLabel.text = "Process"
// fill-in data labels
currencyLabel.text = "HKD"
currValueLabel.text = "0"
balanceLabel.text = "Balance: HKD 2 (Available)"
// maybe add number pad button actions here?
numberButtons.forEach { b in
b.addTarget(self, action: #selector(self.numberPadButtonTapped(_:)), for: .touchUpInside)
}
}
#objc func numberPadButtonTapped(_ btn: UIButton) -> Void {
let t = btn.currentTitle ?? "Delete"
print("Tapped:", t)
// do what you want based on which button was tapped
}
private func initHeader()
{
// not clear how you're setting up your "header"
// so I'll guess at it
// view properties
headerView.backgroundColor = .black
headerView.clipsToBounds = true
headerView.layer.cornerRadius = 24
headerView.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner]
// static text label
let transferToLabel = UILabel()
transferToLabel.text = "Transfer to..."
transferToLabel.font = UIFont.systemFont(ofSize: 14.0, weight: .regular)
// PayPal button
let payPalButton = UIButton()
payPalButton.setTitle("PayPal", for: [])
payPalButton.backgroundColor = .white
payPalButton.setTitleColor(.blue, for: [])
// data label fonts
currencyLabel.font = UIFont.systemFont(ofSize: 28.0, weight: .bold)
currValueLabel.font = currencyLabel.font
balanceLabel.font = UIFont.systemFont(ofSize: 11.0, weight: .regular)
// label text color
[transferToLabel, currencyLabel, currValueLabel, balanceLabel].forEach {
$0.textColor = .white
}
// horizontal stack to hold currencyLabel, currValueLabel
let currValStack = UIStackView()
currValStack.axis = .horizontal
currValStack.spacing = 12
// vertical stack to hold currValStack, balanceLabel
let innerVStack = UIStackView()
innerVStack.axis = .vertical
innerVStack.alignment = .center
innerVStack.spacing = 2
// add labels to Horizontal stack
currValStack.addArrangedSubview(currencyLabel)
currValStack.addArrangedSubview(currValueLabel)
// add horizontal stack and balanceLabel to vertical stack
innerVStack.addArrangedSubview(currValStack)
innerVStack.addArrangedSubview(balanceLabel)
// view to hold vertical stack (so we can center it vertically)
let innerView = UIView()
// add vertical stack to innerView
innerView.addSubview(innerVStack)
// add elements to headerView
headerView.addSubview(transferToLabel)
headerView.addSubview(payPalButton)
headerView.addSubview(innerView)
// we'll be applying constraints
[headerView, transferToLabel, payPalButton, currencyLabel, currValueLabel, balanceLabel,
innerView, innerVStack].forEach {
$0.translatesAutoresizingMaskIntoConstraints = false
}
NSLayoutConstraint.activate([
// "Transfer to..." label Top: Padding, leading: Padding
transferToLabel.topAnchor.constraint(equalTo: headerView.topAnchor, constant: Padding),
transferToLabel.leadingAnchor.constraint(equalTo: headerView.leadingAnchor, constant: Padding),
// payPal button Top: Padding to transfer label Bottom
// Leading / Trailing to Leading / Trailing with Padding
payPalButton.topAnchor.constraint(equalTo: transferToLabel.bottomAnchor, constant: Padding),
payPalButton.leadingAnchor.constraint(equalTo: headerView.leadingAnchor, constant: Padding),
payPalButton.trailingAnchor.constraint(equalTo: headerView.trailingAnchor, constant: -Padding),
// payPalButton explicit height
payPalButton.heightAnchor.constraint(equalToConstant: 50.0),
// innerView Top: 0 to payPal button Bottom
// Leading / Trailing to Leading / Trailing with Padding
// Bottom: 0
innerView.topAnchor.constraint(equalTo: payPalButton.bottomAnchor, constant: 0.0),
innerView.leadingAnchor.constraint(equalTo: headerView.leadingAnchor, constant: Padding),
innerView.trailingAnchor.constraint(equalTo: headerView.trailingAnchor, constant: -Padding),
innerView.bottomAnchor.constraint(equalTo: headerView.bottomAnchor, constant: 0.0),
// innerVStack Top / Bottom to innerView Top / Bottom with 24-pts padding
// centerX
innerVStack.topAnchor.constraint(equalTo: innerView.topAnchor, constant: 32.0),
innerVStack.bottomAnchor.constraint(equalTo: innerView.bottomAnchor, constant: -32.0),
innerVStack.centerXAnchor.constraint(equalTo: innerView.centerXAnchor, constant: 0.0),
])
}
private func initNumpad()
{
numberButtons.removeAll()
// numberPad stack properties
numberPadStackView.axis = .vertical
numberPadStackView.alignment = .fill
numberPadStackView.distribution = .fillEqually
numberPadStackView.spacing = 4
// a little more logical way to manage button layout
let buttonLabels: [[String]] = [
["1", "2", "3"],
["4", "5", "6"],
["7", "8", "9"],
[".", "0", "<"],
]
// adjust as desired
let btnFontSize: CGFloat = 28
buttonLabels.forEach { thisRowLabels in
// create a "row" stack view
let rowStack = UIStackView()
rowStack.axis = .horizontal
rowStack.alignment = .fill
rowStack.distribution = .fillEqually
// same horizontal spacing as "number pad" stack's vertical spacing
rowStack.spacing = numberPadStackView.spacing
// for each number string
thisRowLabels.forEach { s in
// create button
let btn = UIButton()
if s == "<" {
// if it's the "delete button"
// set image here
let iconCfg = UIImage.SymbolConfiguration(pointSize: btnFontSize, weight: .bold, scale: .large)
if let normIcon = UIImage(systemName: "delete.left", withConfiguration: iconCfg)?.withTintColor(.black, renderingMode: .alwaysOriginal),
let highIcon = UIImage(systemName: "delete.left", withConfiguration: iconCfg)?.withTintColor(.lightGray, renderingMode: .alwaysOriginal)
{
btn.setImage(normIcon, for: .normal)
btn.setImage(highIcon, for: .highlighted)
}
} else {
// set number pad button title
btn.setTitle(s, for: [])
}
// number pad button properties
btn.backgroundColor = UIColor(white: 0.95, alpha: 1.0)
btn.setTitleColor(.black, for: .normal)
btn.setTitleColor(.lightGray, for: .highlighted)
btn.titleLabel?.font = UIFont.systemFont(ofSize: btnFontSize, weight: .bold)
// let's give 'em a rounded-corner border
btn.layer.borderColor = UIColor.blue.cgColor
btn.layer.borderWidth = 1
btn.layer.cornerRadius = 8
// allow buttons to stretch vertically!!!
btn.setContentHuggingPriority(UILayoutPriority(rawValue: 249), for: .vertical)
// add button to this row stack
rowStack.addArrangedSubview(btn)
// add button to numberButtons array
numberButtons.append(btn)
}
// add this rowStack to the number pad stack
numberPadStackView.addArrangedSubview(rowStack)
}
}
}
The result, on iPhone 8:
iPhone 11:
and iPhone 11 Pro Max:

Using UIScrollView with a minimum content top anchor causes visual glitch

I have a scroll view in which I have a content view. I set the scroll view's top anchor to be just above the bottom of an image. I set the content view's top anchor to actually be at the bottom of the image. That way you can pull down on the content and reveal up to the bottom of the image without being able to pull the content view down any further. However, this is causing the content to jump.
Here is my code:
class HomeParallaxScrollViewController: UIViewController {
private let topImageView = UIImageView(image: UIImage(named: "cat"))
private let contentView = UIView()
private let scrollView = UIScrollView()
private let label = UILabel()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .gray
topImageView.contentMode = .scaleAspectFill
contentView.backgroundColor = .white
label.text = "SOME\n\n\nRANDOM\n\n\nCONTENT\n\n\nSOME\n\n\nRANDOM\n\n\nCONTENT\n\n\nSOME\n\n\nRANDOM\n\n\nCONTENT\n\n\nSOME\n\n\nRANDOM\n\n\nCONTENT\n\n\nSOME\n\n\nRANDOM\n\n\nCONTENT\n\n\nSOME\n\n\nRANDOM\n\n\nCONTENT\n\n\nSOME\n\n\nRANDOM\n\n\nCONTENT"
label.textColor = .black
label.numberOfLines = 0
[contentView, label, topImageView, scrollView].forEach { $0.translatesAutoresizingMaskIntoConstraints = false }
scrollView.addSubview(contentView)
contentView.addSubview(label)
view.addSubview(topImageView)
view.addSubview(scrollView)
NSLayoutConstraint.activate([
topImageView.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor),
topImageView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
topImageView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
topImageView.heightAnchor.constraint(equalToConstant: 200),
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
scrollView.widthAnchor.constraint(equalTo: view.widthAnchor),
scrollView.topAnchor.constraint(equalTo: topImageView.bottomAnchor, constant: -30),
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
contentView.centerXAnchor.constraint(equalTo: scrollView.centerXAnchor),
contentView.widthAnchor.constraint(equalTo: scrollView.widthAnchor),
contentView.topAnchor.constraint(equalTo: scrollView.topAnchor),
contentView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor),
contentView.topAnchor.constraint(lessThanOrEqualTo: topImageView.bottomAnchor), //This is what's causing the glitch
label.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
label.topAnchor.constraint(equalTo: contentView.topAnchor),
label.bottomAnchor.constraint(equalTo: contentView.bottomAnchor)
])
}
}
And here is that is happening:
Trying to add another top constraint -- particularly to an element outside the scroll view -- is a bad idea, and, as you see, won't work. I'm sure you noticed auto-layout conflict messages being generated.
One approach is to implement scrollViewDidScroll delegate func:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
// limit drag-down in the scroll view to the overlap size
scrollView.contentOffset.y = max(scrollView.contentOffset.y, -30)
}
As the user drags-down to scroll, it will stop at 30-points.
Here is your example, with slight modifications -- I don't have your .plBackgroundLightGray or .PLSemiboldFont and I added an image load for the top image view -- but this should run as-is:
// conform to UIScrollViewDelegate
class HomeParallaxScrollViewController: UIViewController, UIScrollViewDelegate {
private let topImageView = UIImageView(image: UIImage(named: "cat"))
private let contentView = UIView()
private let scrollView = UIScrollView()
private let label = UILabel()
// this will be the "overlap" of the scroll view and top image view
private var scrollOverlap: CGFloat = 30.0
func scrollViewDidScroll(_ scrollView: UIScrollView) {
// limit drag-down in the scroll view to scrollOverlap points
scrollView.contentOffset.y = max(scrollView.contentOffset.y, -scrollOverlap)
}
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .lightGray // .plBackgroundLightGray
topImageView.contentMode = .scaleAspectFill
if let img = UIImage(named: "background") {
topImageView.image = img
}
contentView.backgroundColor = .white
label.text = "SOME\n\n\nRANDOM\n\n\nCONTENT\n\n\nSOME\n\n\nRANDOM\n\n\nCONTENT\n\n\nSOME\n\n\nRANDOM\n\n\nCONTENT\n\n\nSOME\n\n\nRANDOM\n\n\nCONTENT\n\n\nSOME\n\n\nRANDOM\n\n\nCONTENT\n\n\nSOME\n\n\nRANDOM\n\n\nCONTENT\n\n\nSOME\n\n\nRANDOM\n\n\nCONTENT"
label.font = UIFont.boldSystemFont(ofSize: 16) // .PLSemiboldFont(size: 16)
label.textColor = .black
label.numberOfLines = 0
[contentView, label, topImageView, scrollView].forEach { $0.translatesAutoresizingMaskIntoConstraints = false }
scrollView.addSubview(contentView)
contentView.addSubview(label)
view.addSubview(topImageView)
view.addSubview(scrollView)
NSLayoutConstraint.activate([
topImageView.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor),
topImageView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
topImageView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
topImageView.heightAnchor.constraint(equalToConstant: 200),
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
scrollView.widthAnchor.constraint(equalTo: view.widthAnchor),
scrollView.topAnchor.constraint(equalTo: topImageView.bottomAnchor, constant: scrollOverlap),
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
contentView.centerXAnchor.constraint(equalTo: scrollView.centerXAnchor),
contentView.widthAnchor.constraint(equalTo: scrollView.widthAnchor),
contentView.topAnchor.constraint(equalTo: scrollView.topAnchor),
contentView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor),
// nope, not a good idea -- will cause constraint conflicts
//contentView.topAnchor.constraint(lessThanOrEqualTo: topImageView.bottomAnchor), //This is what's causing the glitch
label.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
label.topAnchor.constraint(equalTo: contentView.topAnchor),
label.bottomAnchor.constraint(equalTo: contentView.bottomAnchor)
])
// set delegate to self
scrollView.delegate = self
}
}

ViewControlling full of textEdits not scrolling down with scrollview

I have a view controller with 8 to 9 textEdits where user has to fill them out to save to a database but it is taking a lot of my screen and some TE is not being shown because of the size of the iphones screen.I then decide to add a UIScrollView like this :
lazy var myScrollView : UIScrollView = {
let scrol = UIScrollView()
scrol.contentSize.height = 10000
scrol.backgroundColor = appBackgroundColor
scrol.translatesAutoresizingMaskIntoConstraints = false
return scrol
}()
...
view.addSubview(myScrollView)
myScrollView.addSubview(labelYear)
myScrollView.addSubview(labelAppTitle)
// then I added the constraints
NSLayoutConstraint.activate([
myScrollView.topAnchor.constraint(equalTo: view.topAnchor),
myScrollView.leftAnchor.constraint(equalTo: view.leftAnchor),
myScrollView.rightAnchor.constraint(equalTo: view.rightAnchor),
//enter code here
myScrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
labelAppTitle.leftAnchor.constraint(equalTo: myScrollView.leftAnchor,constant: 40),
labelAppTitle.topAnchor.constraint(equalTo: myScrollView.safeAreaLayoutGuide.topAnchor, constant: 10),
labelAppTitle.rightAnchor.constraint(equalTo:myScrollView.rightAnchor, constant: -40),
labelAppTitle.heightAnchor.constraint(equalToConstant: 90)
])
I have a lot more textEdits but I am not posting for sake of saving space.The problem is that it is not scrolling down like I wanted . How do I do this?
thank you
import UIKit
class TestController: UIViewController, UITextFieldDelegate {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
initUI()
}
func initUI() {
let scrollView = UIScrollView()
scrollView.translatesAutoresizingMaskIntoConstraints = false
scrollView.isUserInteractionEnabled = true
view.addSubview(scrollView)
let contentView = UIView()
contentView.translatesAutoresizingMaskIntoConstraints = false
contentView.isUserInteractionEnabled = true
contentView.isMultipleTouchEnabled = true
scrollView.addSubview(contentView)
let titleText = UITextField(frame: CGRect.zero)
titleText.translatesAutoresizingMaskIntoConstraints = false
titleText.borderStyle = .roundedRect
titleText.isEnabled = true
titleText.isUserInteractionEnabled = true
titleText.placeholder = "Constants.Messages.titlePlaceholder"
titleText.isUserInteractionEnabled = true
titleText.delegate = self
contentView.addSubview(titleText)
// scroll view
NSLayoutConstraint.activate([
scrollView.topAnchor.constraint(equalTo: view.topAnchor, constant: 100),
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 8.0),
scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -8.0),
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -8.0)
])
// content view
NSLayoutConstraint.activate([
contentView.topAnchor.constraint(equalTo: scrollView.topAnchor),
contentView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
contentView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor),
contentView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor),
contentView.widthAnchor.constraint(equalTo: scrollView.widthAnchor)
])
// title text field
NSLayoutConstraint.activate([
titleText.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 20.0),
titleText.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 8.0),
titleText.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -8.0),
titleText.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: 0)
])
}
}
This is an exmaple of using scrollView. When you create a scrollView, apple recommends to put a contentView in it and put it inside in scrollView and don't forget to use bottomAnchor. If you forgot to use that then it'll not scroll.

Remove space at the top of a UIScollView in Swift

I have created a ScrollView and a ImageView:
let scrollView: UIScrollView = {
let scroll = UIScrollView()
scroll.backgroundColor = UIColor.red
scroll.contentSize.height = 1234
scroll.translatesAutoresizingMaskIntoConstraints = false
return scroll
}()
let filmImageView: UIImageView = {
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFill
imageView.backgroundColor = UIColor.blue
imageView.clipsToBounds = true
imageView.translatesAutoresizingMaskIntoConstraints = false
return imageView
}()
And I have added these to my view with constraints, as followed:
self.view.addSubview(scrollView)
scrollView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 0).isActive = true
scrollView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: 0).isActive = true
scrollView.topAnchor.constraint(equalTo: view.topAnchor, constant: 0).isActive = true
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 0).isActive = true
scrollView.addSubview(filmImageView)
filmImageView.widthAnchor.constraint(equalToConstant: self.view.frame.width).isActive = true
filmImageView.topAnchor.constraint(equalTo: scrollView.topAnchor, constant: 0).isActive = true
filmImageView.heightAnchor.constraint(equalToConstant: 350).isActive = true
Everything works, except, on the iPhone X only, the imageView doesn't sit to the top of the screen. See the image below. (I made the scrollview background colour red so you can see)
I have tried adding the code to my scrollView.
scroll.contentInset = UIEdgeInsets.zero
scroll.scrollIndicatorInsets = UIEdgeInsets.zero
scroll.contentOffset = CGPoint(x: 0.0, y: 0.0)
Along with:
self.automaticallyAdjustsScrollViewInsets = false
But it makes no change. I have been unable to find anything on the internet on how to get my image to sit to the top of the scrollview for the iPhone X. All other iPhone devices display this fine.
for the scrollView:
setting contentInsets to "never" instead of "always" solved the problem for me